Posted by Amir Mazzarella on July 01, 2020
I guess it's about time I've discussed some additional ways you could talk to Netflix's MSL API. I don't mean other endpoints, like the NRDJS API or the legacy NCCP API, I mean more authentication methods and key exchanges that allow you to act as more advanced devices. Let's take my iPhone 11 Pro, the newest iPhone on the market as of the date this post goes up. It can play 4K UHD Netflix, which is pretty appealing to any content-thieves out there. What measures does Netflix take to prevent any aspiring pirates from nabbing this notable piece of media? Well, besides the DRM they use to encrypt the content itself (it's FairPlay Streaming on iOS), they employ several additional security measures in their MSL API to prevent pirates from spoofing an iPhone from a command-line.
You all know that I've written my own Python MSL client, and it works reasonably well. However, it's pretty limited in terms of authentication / key exchange methods. All it can do is NONE
for entity authentication and ASYMMETRIC_WRAPPED
for key exchange. Both of these methods are the least secure methods in their respective categories. To quote the Netflix MSL wiki for the NONE
entity authentication scheme:
"The unauthenticated entity authentication scheme does not provide encryption or authentication and only identifies the entity. Therefore entity identities can be harvested and spoofed."
And to quote the Netflix MSL wiki for the ASYMMETRIC_WRAPPED
key exchange scheme:
"Asymmetric wrapped key exchange uses a generated ephemeral asymmetric key pair for key exchange. It will typically be used when there is no other data or keys from which to base secure key exchange."
Something like the iPhone 11 Pro, trusted with handling 4K UHD media properly and securely, definitely wouldn't use either of these two schemes. Spoiler alert: it doesn't use anything close to either of those. I'll skip the boring part and let you know firsthand that after some reverse engineering of the MslClient and Nbp frameworks inside Argo.app ripped directly off my iPhone 11 Pro (thanks unc0ver), iOS uses the MGK
entity authentication and AUTHENTICATED_DH
key exchange. I'll go over both of these in detail.
MGK
, or Model Group Keys, is an entity authentication that relies on the fact that encryption and signing keys have already been pre-shared. There are two keys, the encryption key and signing key. They are both cryptographically related to the ESN of the requesting device. What's curious is that in the iOS frameworks, the encryption key is called kpe
, and the signing key is called kph
. According to the wiki, those two names correspond to the PSK
entity authentication scheme, but iOS uses MGK
. So, for this post, I'm going to call the encryption key kpe
and the signing key kph
. There is also a third key, kpw
, which is derived from both the kpe
and kph
. The kpw
is used in the key exchange, so as not to compromise the kpe
and kph
. The derivation process is public and discussed here on the Netflix MSL wiki, but here is a Python implementation:
from Cryptodome.Hash import HMAC, SHA256
SALT = bytes(bytearray([
0x02, 0x76, 0x17, 0x98, 0x4f, 0x62, 0x27, 0x53,
0x9a, 0x63, 0x0b, 0x89, 0x7c, 0x01, 0x7d, 0x69
]))
INFO = bytes(bytearray([
0x80, 0x9f, 0x82, 0xa7, 0xad, 0xdf, 0x54, 0x8d,
0x3e, 0xa9, 0xdd, 0x06, 0x7f, 0xf9, 0xbb, 0x91
]))
def derive_wrapping_key(kpe, kph):
catK = kpe + kph
# first HMAC(salt, catK)
sig1 = HMAC.new(SALT, catK, SHA256).digest()
# second HMAC(first HMAC, info)
sig2 = HMAC.new(sig1, INFO, SHA256).digest()
# truncation and final output
assert len(sig2) > 16
kpw = sig2[:16]
return kpw
The entity authentication block for MGK
is pretty simple:
entityauthdata = {
'scheme': 'MGK',
'authdata': {
'identity': self.msl_session['esn']
}
}
But, then you have to include that entity authentication inside an MSL ciphertext envelope, including a signature. The encryption key is the kpe
, and the signature is a standard MSL signature with the kph
being the HMAC key. Make sure to include a blank payload and encrypt/sign that as well.
Now, onto the key exchange. The key exchange method is AUTHENTICATED_DH
, which stands for Authenticated Diffie-Hellman. The wiki page for AUTHENTICATED_DH
is right here. To sum it up, this is what the very first key request should look like:
key_request_data = {
'scheme': 'AUTHENTICATED_DH',
'keydata': {
'mechanism': 'MGK',
'parametersid': '1',
'publickey': BASE64_DH_PUBKEY,
}
}
BASE64_DH_PUBKEY
is a base64 encoded Diffie-Hellman public key generated against a pre-shared set of Diffie-Hellman parameters. The parametersid
corresponds to the ID of the parameters your public key is generated against, so Netflix can generate one against the same parameters. In the example, it is set to 1 because that is what the wiki has it at (and that's the only one Netflix gives out). For the uninformed, let's use the cryptography
library in Python to generate some Diffie-Hellman keys. Assuming you know the Diffie-Hellman parameter's prime and generator values, you must first import the parameter set into a DHParameters
object for cryptography
. We can do so by first constructing a DHParameterNumbers
object and then using the .parameters
method to return a DHParameters
object:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import dh
parameters = dh.DHParameterNumbers(p=105742060993354776768552928993528049998819868915142585197177104616169622827383134271679938422457028341981086182562775424156961685555315914825318435259207190039004328124699512223999241200838459608690134163386419459498606157888928116051702048645186656740521887291558644222739566047010553257407113735288261584863, g=5).parameters(default_backend())
We can then generate a private key against those parameters:
private_key = parameters.generate_private_key()
For our public key, we don't send the entire public key over. That includes the prime of the Diffie-Hellman parameters, and it's unnecessary since Netflix knows what Diffie-Hellman parameters to generate a key against. So, we can just send just the 1024-bit public key number in base64:
public_key = base64.b64encode(
private_key.public_key().public_numbers().y.to_bytes(129, 'big')
).decode('utf8')
First, convert the private key to a DHPublicKey
object, then to a DHPublicNumbers
object to access the raw integers that make up the public key. Then, convert the 1024-bit public key integer y
to bytes and base64 encode it. Notice how I convert it to 129 bytes. This is necessary. This is to prepend a zero byte for compatibility purposes with Java. Quote from the Netflix MSL wiki:
"The public key should contain exactly one zero byte in the zeroth element. When creating or upon receipt of key request data this zero byte must be prepended if missing."
Include the public key in the key request data as publickey
. When you send it over, you should get a key response that looks like:
key_response_data = {
'wrapdata': BASE64,
'publickey': BASE64,
'parametersid': '1',
}
The field wrapdata
will be relevant later, ignore it for now. publickey
is Netflix's Diffie-Hellman public key generated against the parameter set that you told them to generate against. parametersid
is just the same one you sent, which I had as 1. So, how do we use their public key to derive an encryption and signing key to use for our MSL session? First, we have to calculate the Diffie-Hellman shared secret:
dh_pubkey = int.from_bytes(base64.b64decode(NETFLIX_PUBKEY), 'big')
dh_pubkey = dh.DHPublicNumbers(
dh_pubkey,
private_key.parameters().parameter_numbers()
).public_key(default_backend())
shared_secret = b'\x00' + private_key.exchange(dh_pubkey)
First we convert Netflix's public key to an integer from base64. Then we can import it as a DHPublicKey
object by first constructing a DHPublicNumbers
object from the integer our private key parameter set (our private key parameter set is equal to the public key parameter set, they were generated against the same parameters), and then using .public_key
to convert it to a DHPublicKey
object that we can use to generate a shared secret by using .exchange
on our DHPrivateKey
object. Notice that I prepended a zero byte to the shared secret. This is necessary.
Now that we have the shared secret let's use it to derive session keys! According to the Netflix wiki, the pseudocode is as follows:
switch (mechanism) {
case PSK: Kd = Kpw; break;
case MGK: Kd = Kdw; break;
case WRAP: Kd = Kwrap; break;
}
bytes = HMAC-SHA384(SHA384(Kd), shared_secret);
Kenc' = bytes[0...15]
Khmac' = bytes[16...47]
Kwrap' = derive(Kenc', Khmac')
Since the mechanism
we sent over is MGK
, our Kd
value is equal to Kdw
(I'm going to call it kpw
because MGK and PSK tend to share variable names and that's how it's called in the iOS code). Remember how I talked about kpw
earlier? This is where it comes into play. After we have our Kd
, we can just derive the encryption and signing session keys according to the pseudocode. Here is a Python implementation:
from Cryptodome.Hash import HMAC, SHA384
kd = derive_wrapping_key(kpe, kph)
kd_hash = SHA384.new(kd).digest()
kbytes = HMAC.new(kd_hash, shared_secret, SHA384).digest()
encryption_key = kbytes[:16]
sign_key = kbytes[16:]
kwrap_prime = derive_wrapping_key(encryption_key, sign_key)
We start off by finding kd
(kpw
) by just performing the derive_wrapping_key
function I put up above, and then we just follow the pseudocode hashing and truncating. I also find Kwrap'
, which, while I don't use now, I will be using and talking about it later.
So now we have session keys! We can do whatever we want now; everything else is done by the book. But, how do we even get all this stuff? The MGK encryption/signing keys, or even the Diffie-Hellman parameter set? It's time to do some reverse engineering. Inside Argo.app (the Netflix app), we can see that there is a MslClient framework. Let's load it into IDA:
Notice the functions AppleDAL.kpe, AppleDAL.kph, and AppleDAL.ESN. If we inspect them, they just seem to be returning the contents of whatever that property refers to. Great! So now, let's just call those methods ourselves and swipe the return values. It's a lot easier than it sounds. I use Frida, and my iPhone 11 Pro is jailbroken. All I have to do is plug in my phone to my computer and make a neat little Frida script to hook into those methods and call them while Netflix is running. Here's my Frida script:
var esn = ObjC.classes.AppleDAL['- ESN'];
Interceptor.attach(esn.implementation, {
onLeave: function(retval) {
var ret = new ObjC.Object(retval);
console.log('AppleDAL - ESN returned');
console.log(ret.toString());
}
})
var kpe = ObjC.classes.AppleDAL['- kpe'];
Interceptor.attach(kpe.implementation, {
onLeave: function(retval) {
var ret = new ObjC.Object(retval);
console.log("AppleDAL - kpe returned");
var buf = ret.bytes().readByteArray(ret.length());
console.log(hexdump(buf, { ansi: true }));
}
});
var kph = ObjC.classes.AppleDAL['- kph'];
Interceptor.attach(kph.implementation, {
onLeave: function(retval) {
var ret = new ObjC.Object(retval);
console.log("AppleDAL - kph returned");
var buf = ret.bytes().readByteArray(ret.length());
console.log(hexdump(buf, { ansi: true }));
}
});
This Frida script just hooks into those three methods and logs the return value to the Frida console. We still need a way to call them, though. Luckily, we can! Save the above script as frida.js
, plug your phone into your computer, and run:
$ frida -U -n Netflix -l frida.js
It will start a Frida console session while also running your script in the background. Now, you can run some commands to call those Objective-C methods. All three methods are part of the class AppleDAL
, but since Netflix is running, we can assume that class is already instantiated on the heap somewhere, so let's just use that instead of constructing a new one. We can do that like so:
[iPhone::Netflix]-> var instance = ObjC.chooseSync(ObjC.classes.AppleDAL)[0]
What we just did is found an already instantiated AppleDAL
object within the heap and just set it to the instance
variable. Now, we can call these methods!
[iPhone::Netflix]-> instance.kpe();
AppleDAL - kpe returned
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 e1 e6 87 38 39 71 e9 2b 31 ea 49 6e 13 e1 0c 9f ...89q.+1.In....
Easy, wasn't it. If everything went right your Frida script will hook into the method being called and log the return output in a nice format since we used hexdump
. That's it! There's your kpe
. We can do the same for the ESN and kph
:
[iPhone::Netflix]-> instance.kph();
AppleDAL - kph returned
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 3d 3a 56 2d 5e c8 ff db 92 50 9f 79 76 d6 da 94 =:V-^....P.yv...
00000010 62 66 8e e2 b2 fa 76 c7 b6 1c e4 08 4b e0 7b a4 bf....v.....K.{.
[iPhone::Netflix]-> instance.ESN();
AppleDAL - ESN returned
NFAPPL-02-IPHONE12=3-82778E4347E8EC874B44BE8CBC70E314A6AEB577FF809A1553D8A7505FC74CCD
You could also grab the kpw
if you wanted to, but since the derivation is public, you don't really have to and could just derive it yourself.
All we need now is those Diffie-Hellman parameters. Unfortunately, they're a bit trickier to find. They're not in the MslClient framework, open up the Nbp framework in IDA instead. You'll notice there's a method called:
netflix::msl::keyx::IosAdhKeyx::dhKeyGen
This is the one we want. It claims to generate a Diffie-Hellman key, so it must do it against a parameter set. Let's decompile it:
You can see the variable v65
first being allocated 0x80 bytes (128 bytes), and then being initialized as an array, with the first index pointing to unk_4D8DF4
. The other indices are just pointing to bytes further down the unk_4D8DF4
byte array. Also, the prime
member of netflix::msl::keyx::IosAdhKeyx::dhKeyGen
is set to v65
! That's our Diffie-Hellman prime! We can jump over to the data block, which looks like this:
We can just jump over to hex view and copy 128 bytes starting from 0x96, since we know the prime is 128 bytes long. Convert the hex to an integer and there's your prime! We still need the generator number, though. Don't fret, just look further down in the dhKeyGen
method:
v66
is initialized in much of the same way. It's first allocated 1 byte, and then set to 5. Then, the generator
member of the method is set to v66
. So, the Diffie-Hellman generator is 5. The corresponding parametersid
for this parameter set is indeed 1 by the way.
So now that we have everything, you could go off and start imitating iOS devices and getting 4K UHD manifests right now. But, you wouldn't exactly be doing it correctly. I haven't talked about the last part yet, and that's the function of Kwrap'
and wrapdata
. If you want to imitate iOS perfectly, you have to imitate the key ladder. iOS only uses the kpw
as Kd
in the session key derivation in the first key exchange it ever makes. From then on, it uses the Kwrap'
from the previous derivation as Kd
. This is to prevent constantly using the kpe
and kph
, as they are supposed to be kept secure. To properly implement this, follow these steps:
mechanism
set to MGK
and Kd
being set to the kpw
, SAVE the wrapdata
binary in the key exchange response and the Kwrap'
you generate. Remember, Kwrap'
is calculated by running the derive_wrapping_key
function on your encryption/signing session keys, NOT the kpe
and kph
.wrapdata
and Kwrap'
, in the next key exchange, set your mechanism
to WRAP
instead of MGK
, and use the previously derived Kwrap'
as your Kd
in the session key derivation.wrapdata
in your key exchange request to identify which Kwrap'
you're using.wrapdata
and Kwrap'
!This is called a key ladder. You're always keeping track of the last Kwrap'
and wrapdata
given to you, which also has the benefit of perfect forward secrecy since you're using different keys for each session. But also remember, your entity authentication data is still being encrypted and signed with kpe
and kph
! That part doesn't ever change. It's just that your session keys, the ones you use for encrypting and signing manifest requests and license requests, those have to be re-derived each session.
That's all for this post. Sorry for the length, I wanted to make sure I covered everything in regards to MGK
and Authenticated Diffie-Hellman. Thanks for reading!