Posted by Amir Mazzarella on May 25, 2018
It's been about two months since I've made a blog post, and with school getting harder and finals coming up, it's a miracle that I can squeeze in this post. This post is going to be pretty long - it's a complete tutorial on how to "crack" VMP. You may be thinking, what do I mean by "crack" VMP? Well, let's talk about what VMP even is. VMP is an extra security measure implemented by Google for Widevine versions 1.4.8.984+. Through its addition, Google made service certificates mandatory. Service certificates add an extra step in the license acquisition process. Before 1.4.8.984, what would happen is that Widevine would read the initialization data from the stream manifest and form a license request with it, and send the license request to a license server, and process the resulting license. With service certificates, before Widevine even reads the stream manifest, Widevine sends a service certificate request to the license server. The service certificate request is two bytes, 08 04
, which is an encoded protocol buffer. This request is nonunique and does not depend on initialization data. The license server sends the service certificate back to Widevine, which is an encoded protocol buffer of a 2048 bit RSA public key signed by Google's 3072 bit RSA private key. The Widevine library validates the signature, loads the certificate into memory, and then encrypts the client_identification
block of the license request against the public key. Finally, it reads the initialization data and sends the license request. VMP takes advantage of the encryption. What VMP is is an extra block in the license request, a block containing the hashes and signatures of core chrome files (widevinecdm.dll, chrome.exe, etc.). The license server uses this data to make sure that you haven't tampered with the Widevine library. If you did tamper the Widevine library trying to look for exploits, VMP makes sure that you can't get a license. However, there's a way to bypass it. You could merely steal a legitimate VMP block from a good license request and include it in your license request. Of course, Widevine made sure to prevent this by only including VMP data in the encrypted section of the license request. 2048-bit RSA isn't going to be cracked any time soon, and you surely can't spoof a 3072-bit signature, so it seems like they knew about everything! There's one problem, though. The Widevine library doesn't verify the signature of the certificate after loading it into memory. So, you could just set a breakpoint on the generateRequest call in the EME player JS of, say, Netflix, so it pauses after the memory has loaded the certificate but before Widevine generates the request. Then you could go through memory and replace the RSA public key of the certificate with a public key that you have the private key for, then let Widevine generate and encrypt the request against your keypair, and then, of course, decrypt the client_identification
block. This requires that you have reverse engineered the protocol buffer fields for Widevine, however. In a later post, I'll show how that's unnecessary and how to make your own .proto
for encoding and decoding Widevine messages.
So let's go through this now. First, I'll start with downloading this helpful extension into Chrome: EME Logger. What this extension allows you to do is see all the EME messages back and forth from Widevine to the license server. It's beneficial considering Netflix's EME is all E2E encrypted, which is what we'll be using to grab our legit VMP blobs. After downloading the extension, load up Netflix in a new tab and open the developer console. Paste this function into it for easy access later:
arrayBufferToBase64 = function(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
for (var i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
Now, go ahead and load a video. You should see tons of EME messages popping up into the console. If not, refresh until you do.
Next, go to the Sources tab in the developer menu and find the cadmium-playercore JS file. Click on it to load it on the side, prettify if necessary. Ctrl+F for generateRequest
and set a breakpoint on it by clicking on the line number of the function call. Now, refresh the page. Chrome should pause, with Netflix loading perpetually. Go back to the console and redefine the function if necessary. Now it's time to comb through EME logs. Find the log of type promiseResult
right under the log of type SetServiceCertificateCall
. That promiseResult
log contains the result of the SetServiceCertificateCall
- the service certificate. Expand the log and click on the args
attribute. Right-click the ArrayBuffer and click "Store as global variable." It should be stored in a variable called temp1
. Then call arrayBufferToBase64(temp1);
in the console, and copy the output. That output is the base64 representation of Netflix's service certificate.
Now just decode the base64 service certificate using protoc
. Your command line input should look something like protoc --decode_raw < cert.bin
. You should see something like this:
1 {
1: 3
2: "\345D\272@\013\301\0177\323\2666\370\265O\373C"
3: 1383331975
4: "0\202\001\n\002\202\001\001\000\346\363\276[\266\205\224lj\220\017\307\200\032\340e\031\217\217\306\217\030\027R\320G\3411\322\220\3164\307\255\360A\354\225\306Q\004\243P\377OQdT\317\322\372\330\212l\230\210\031\244\364a\276KL\001\334$HN\014\0364Myp=\3671\0062K|\351\005\3363\372\300\211jm\276\216=Y\003\213\242\214M\235\304`\276\201\020Q\237\017V\240\374\260\354?\021\272\021\222\'\336N\022\334\340\334z>\230K1\270Nq\203\322\314\343\346n\256\217\375\232\314\366lX\tR4\024Tz9\217\3518\250\265\256\340\346\356\200Y\234\3642\310\002\210K\347\305\021\346M\027\236\370\263\272S\207l\3031w9\332\366\\\023\303\376\022\000\266\235\021_\302epf\263\005-\006\000\023\014\243j\215|(\222\210gr\227lY&\256\037\253\337K\241W\302,\320\241\222}M\330\322\030.\264\255@v\267\346\230\016n\370p\213\261\002\003\001\000\001"
7: "test.netflix.com"
}
2: "\204\323/2[\r\226\212~\214\333\366\360\357\346\252V\344\017\3034\277`nrI\213\360\010d\035\3209m\257xi\212O|Z(\32485+\350\374L\237VX\\\342\350\335\341\003\353\026\0063\264b\315\257\027\262D]\t\361|\3068\373/\277\323\034\366\342\307)\370\231B\354\266\324\246XX^\3075\347I\342\252@{\250*g\253\252\355\'\273\177b[z\\\3045\177+\261:i\244\253\344\200\364H(\273)\027\013\364\020\224\013=`\365\3167\260\330\370\350\030L\tbR\233I\261\325qCr\202\231Z\032c\344P S\2048{\247\376K\307\227\337\331\337\350>\222%\026\370j\236\331\2379\373\246\267\210\276d\275\337\025\243\233\276H\351@U@j:\246~*X\320\240-\211F\030$\002Rnm\203\236\272\217\357\340\346;\330fD\311\275\270\231\027\013c1ng\215\264\221\300\035\326\340*\312\243,p\342\253\t5&\231E2\200\275(\323)\332\225\ru\177S\221\312\355&\201\003\250\211\367\315\177i\372+]\267v\275\231Q\\\357\366K\343\032ki\207P\274\354\017\344\271\227\217\330\032\224\255\354\371\344\t\355\271\027\351\300\242\373e\376\210\031\2023\036\021\321rw\377X1\210\316K\211+bl)\337\"{\233\326s`$:#\304P7\214\177\310\271G\033\224G\377\345\215,\325,\350\220\031\321\361]\nl"
Go ahead and open a Python shell and copy the bytes of the 4th field in the 1st message. I will be using the pycryptodomex
library for my crypto operations.
>>> from Cryptodome.PublicKey import RSA
>>> key = RSA.importKey("0\202\001\n\002\202\001\001\000\346\363\276[\266\205\224lj\220\017\307\200\032\340e\031\217\217\306\217\030\027R\320G\3411\322\220\3164\307\255\360A\354\225\306Q\004\243P\377OQdT\317\322\372\330\212l\230\210\031\244\364a\276KL\001\334$HN\014\0364Myp=\3671\0062K|\351\005\3363\372\300\211jm\276\216=Y\003\213\242\214M\235\304`\276\201\020Q\237\017V\240\374\260\354?\021\272\021\222\'\336N\022\334\340\334z>\230K1\270Nq\203\322\314\343\346n\256\217\375\232\314\366lX\tR4\024Tz9\217\3518\250\265\256\340\346\356\200Y\234\3642\310\002\210K\347\305\021\346M\027\236\370\263\272S\207l\3031w9\332\366\\\023\303\376\022\000\266\235\021_\302epf\263\005-\006\000\023\014\243j\215|(\222\210gr\227lY&\256\037\253\337K\241W\302,\320\241\222}M\330\322\030.\264\255@v\267\346\230\016n\370p\213\261\002\003\001\000\001")
>>> print(hex(key.n)[2:].upper())
The output of the last print statement is the hex representation of the modulus of the service certificate's public RSA key. This is what you need to find and replace in memory. Let's generate our keypair so we can easily replace the modulus.
>>> privkey = RSA.generate(2048)
>>> print(hex(privkey.n)[2:].upper())
The output of that print statement is the modulus that will replace the one we are searching for. Now let's start our search for the service certificate modulus. I'll be using Cheat Engine.
Start off by opening Cheat Engine. If you select a process to open in Cheat Engine, you'll see multiple chrome.exes. How do we know which one to use? Well, let's see which one is Widevine. Open the task manager inside Chrome by pressing Shift+Esc. Look for "Utility: Content Decryption Module Service." This is Widevine. On the far right is the Process ID, which is what we'll use to see which process we should scan in Cheat Engine. For me, the PID for Widevine is 7500. You need to convert that number to hexadecimal, and since you have a Python shell open, you can just do hex(7500)
. The hex representation for 7500 is 0x1d4c, so I'll be looking for 00001D4C-chrome.exe
in the Process Explorer in Cheat Engine. Once you find it, select it and click Open. Now it's time to scan.
On the right, there's a drop-down box called Value Type. Change it to Array of byte, and paste the hex representation of the service certificate's modulus into the Array of byte box. Your window should be looking like this:
Now just click First Scan. On the left you should see multiple matches, with your window looking like this:
Now I don't know why multiple matches come up, possibly because it's just loaded in multiple places for safety, but you can just replace all of them. You can replace all of them by Ctrl-clicking all of them to select all of them and pressing Ctrl+E to change the value. Change the value to the modulus of your key pair.
After that's done, go back to Chrome and resume the breakpoint pause. You can do this by clicking the play button on the "Paused in debugger" widget. You should see a few EME logs pop up and then Netflix should error out. This is normal because the license request is encrypted against a different key than Netflix expected. Now comes the decryption part. Expand the MessageEvent
EME log and right click the message
attribute and click "Store as global variable." It should be stored in a variable called temp2
. Now just enter this into the console: arrayBufferToBase64(temp2);
. The output is a base64 representation of the encrypted license request. To decrypt it, first decode the base64 and store the output in a binary file. Decode the binary file the same way you decoded the service certificate. The output of protoc
should be similar. The largest block of bytes is the encrypted client identification. It is encrypted AES CBC against a key that is encrypted against the public key of the certificate, or in this case, our key pair. Copy the 5th field in the 8th message (the encrypted AES key) so we can decrypt it. In the same Python shell that you created your key pair in, enter the following:
>>> cipher = PKCS1_OAEP.new(privkey)
>>> key = cipher.decrypt(b"ENCRYPTED_KEY")
Of course, replace ENCRYPTED_KEY with your own encrypted key from the decoded protocol buffer. The variable key
should now contain the byte representation of the AES key. Now let's decrypt the client identification block itself. Before we do so, let's get the IV of the AES key. This is stored in the clear in the protocol buffer, it's just the 4th field in the 8th message. Do iv = IV
, replacing IV with the bytes in the protocol buffer, for easy access later. Enter the following into the same Python shell:
>>> from Cryptodome.Cipher import AES
>>> import base64
>>> cipher = AES.new(key, AES.MODE_CBC, iv=iv)
>>> client_id = Padding.unpad(cipher.decrypt(b"ENCRYPTED_CLIENTID"), 16)
>>> print(base64.b64encode(client_id).decode('utf8'))
The output of that print statement is the base64 representation of the decrypted client identification block. Decode that however you like and store the output in a binary file and decode it the same way we have been doing throughout this process. The output of protoc
should contain some info about the Widevine library used to make the license request, and the last message (7th) is the VMP data. You can see things like hashes for Chrome files like widevinecdm.dll
and more. You can encode that message to bytes and store it in a binary file if you like. In later blog posts, I'll detail how this is useful, but I've just shown you that VMP is a pointless security measure that is easily bypassed. Thanks for reading!