This is kind of a thought i've had on-and-off while in the vita hacking scene, and that is wouldn't it be cool to have a "Flash Cartridge" something like an r4 on the DS. which ultimately leads to the question "How do gamecarts work anyway?" and more generally; "How does the vita know if a gamecart is "legit" or not?"
if you've been around here awhile, you may have heard that it uses something called "CMD56" to validate gamecarts; and if you've been around even longer, you may know this article from wololo.net about dumping game cartridges with custom hardware.
if your looking into something like this, its often good to read up on previous research, since chances are your not the first to look at something, from this we can learn 2 things;
Game Cartridges are just regular MMC cards, and use the SD Protocol
The Vita verifies that their legitimate by sony, using the generic command "CMD56"?
CMD56 consists of 10 requests and 10 responses, totaling 20 packets in total ??
there have been many attempts to look into and try figure out how the "CMD56" authentication works, however no one ever came up with anything from it.
All this research however was done before the vita was ever hacked, it pre-dates henkaku, or taihen which actually makes alot of what they did figure out, kind of a bit impressive. but like this also asks more questions than it answers, like what the heck is CMD56 anyway? ..
however with a bit of messing around with your favorite search engine with certain tags like "filetype:pdf" you it is possible to find it just sitting on random http servers w directory listings enabled; which is much cheaper and much easier and lets me tell you about it!! highly recommend!
anyway, once you have that, you can find that the way SD & eMMC cards work is via some numbered commands theres also CMD1, CMD2, CMD3, etc which all mean different things, the one we're interested in here, as has been kinda hinted at is CMD56, which is defined under "Application Specific Commands" and is labeled "Generic Command" or "GEN_CMD;
its defined as "the same as single block read or write commands (CMD24/CMD17) ... but the argument denotes the direction of the transfer, not its address ... the data block is not memory payload data but has a vendor specific format and meaning ..."
What this effectively means is that this on any given SD or eMMC card, this command can be used for anything so sony just uses them for an authentication mechanism, vita gamecarts are just regular MMC devices with a different firmware, one that responds to CMD56 in whatever way the playstation vita expects; this is presumably what they mean by "10 requests, 10 responses", perhaps it does 10 "write" operations and 10 "read" operations, totaling 20 'packets' .. but unfortunately though this also means the actual gamecart mechanism itself is non-standard, and so in order to get anywhere it'll have to be reverse engineered
and you would have to reverse engineer it; from either the console itself or the gamecarts itself, or maybe(?) what we can figure out from just looking at the packets in a hex editor; there is actually some efforts to try reverse engineer it before hand, like this project from motoharu which looks promising, however if you actually look into it you'll find its very incomplete; and seems to be more(?) of a reverse engineering of the entirety of GcAuthMgr, (which does more than just GC Auth, thanks Sony-) but more so, that would mean that even if it were complete, it would only contain the part for the vita part of the communication, not the cartridge side;
which leaves that naturally of course, i am going to have to reverse engineer it myself, first order of business then would be to try obtain a dump of the actual cmd56 packets thankfully this is much easier for us to do now, than it was back when everyone else tried, while everyone else had to resort to very expensive oscilloscopes or custom FPGA solutions to log the packets, for us today, its as simple as just writing a plugin to hook the consoles cmd56 "read" and "write" functions in the kernel,
but we have to find them first;
on the vita, the authentication is handled by "GcAuthMgr" located at os0:/kd/gcauthmgr.skprx; and- there is also a 'secure processor' (F00D) counterpart, os0:/sm/gcauthmgr_sm.self, and there are some NIDs documented on the vita developer wiki which indicates a function named "cmd56_handshake" with NID 0x68781760;
looking at this in Ghidra, there are two constant calls to SceSdifForDriver, and it's pretty obvious these are for sending and receiving from CMD56 (also Sdif, in SceSdif likely stands for "SD-Interface") after finding that, its pretty trivial to write some code to hook these functions with TaiHen to log everything sent or received
I opted to try make it use the pcap format because it's actually relatively simple, and allows me to analyze these packets using standard tools for packet analysis like Wireshark Which even allows me to write a descriptor in LUA to parse out the format; to figure out how it works;
and now we just insert a vita cart into the device and ... the cart doesn't authenticate at all anymore?? what? why?? well if we take a look at ghidra decompilation. between one of the scesdifSend and sceSdifRecv there is a timing check! it calls ksceKernelGetSystemTimeWide() at before sending anything, and then again after sending it. and if it takes more than 5000 microseconds it'll fail the authentication! .. but.. why?? for what reason could they possibly have done this? its not just a timeout since it only seems to cover 2 of the 20 packets!
Well lets just say, i have a bit of a suspicion ..
This if you were not around in the really early vita hacking days, is "The Cobra Blackfin". it is effectively the only ever successful attack on vita gamecart authentication, it was the first time you could load backup games on your vita, predating the henkaku hack and later utilites like NoNpDrm (or even Vitamin) the way this bad boy worked, is NOT by reverse-enginering and reimplementing CMD56, but instead by connecting over the internet and proxying the CMD56 authentication to who actually owns the game.
and in that same wololo article about it also mentions an interesting observation, being that: "Cobra blackfin is not compatible with 3.60, The Blackfin team advises people to stay on 3.57." and wouldn't you know- it just so happens that this timing check is missing on 3.57 and older,
so this must be how Sony patched the cobra blackfin! the network connection to the gamecart would be very slow, such that a timing check is enough to effectively block this approach; and we must be hitting this same timing check when dumping packets as the IO on the vita is also very slow-
anyway this is important information for anyone who might want to implement their own flashcarts, that they have to respond fast enough (around 5000µs). in order for it to have any chance of working on an unmodified vita; however since we have complete control over the vita kernel, i don't actually need to care about it for our purposes of just trying to dump the packets and can simply just patch out sony's patch-
the approach i took was simply hooking "ksceKernelGetSystemTimeWide" to always return 0; rather amusingly, this should also allow you to use a Cobra Blackfin on FW3.60; but anyway, after adding this, it finally works! i can now view the CMD56 packets in Wireshark!
NOTE: all the code for the packet logging and for the cobra blackfin patches, can be found in the "PythonWhiteFin" on my git server.
So, we have packet logs, so now to try explain whats actually happening, first- i want to say the obvious but CANNOT figure out the format from just packet logs alone; this is why Cobra Blackfin and other researchers back then never got any further than this,
and not just that but you CANNOT figure out what its doing without being able to read the F00D/Secure Processor code and decryption access; which is likely why Motoharu's attempt never got anywhere ... (well, that and GcAuthMgr decompiled code is awful to read..)
Let it be abundantly clear here: i am standing on the shoulders of giants for the following analysis:
Also a disclaimer:
What i have written below is based on my re-implementation, that is written in a fairly stateful way; which makes it *significantly* easier to follow; and although this does produce the same results
you should also be aware that gcauthmgr on console is completely stateless (based) and instead it sends the primitives used to construct the secrets to the security subprocessor (F00D) which in turn reconstructs the relevant secrets for every single packet sent;
for example, cart_random is sent to the secure sub-processor; and then session_key is derived from that on there- on every single occasion where it is used,
not only that, but the code on the kernel side is heavily inlined with lots of unrolled loops an optimization feature maybe, but this also acts as a kinda pesudo-obfuscation
A high-level overview:
The authentication has 2 primary security features;
eMMC lock out, can only accept read/write commands after authentication is complete.
2 per-cart keys (p18 & p20), used to derive a per-cart rif key, which is used to derive a per-game decryption key for SceNpDrm.
These are accomplished by doing the following
The Console verifying its connected to a real Vita Cartridge
The Cartridge verifying its connected to a real PS Vita
Only then can the contents be read, and encryption keys be disclosed to the console the keys are actually never revealed to the primary (ARMv7) CPU directly, they reside entirely within the consoles security subprocessor (F00D)
furthermore verification of the console and cart are not done via Asymmetric private/public keys as you might expect; instead, the security is using Symmetric Keys, and depends on the Consoles hardware-sealed "bigmac" keyrings;
these are accessed via a device exposed to the Security Processor where you never provide the key directly instead you provide a keyring (in this case, 0x345 and 0x348 are used respectively-)
the cartridge has a copy of these keys, and the entire security of the authentication depends on these not being known; there are multiple vulnerabilities however that makes this not necessarily the case in general, to understand that, we need to dig a bit deeper;
in this we will refer to packets going TO the gamecart as "requests" and activity coming FROM the gamecart, as "responses"; all packets are always exactly 1028 bytes long, however do not ever actually use the full size,
or more generally- a "write" to CMD56 is a "request", while a "read" from CMD56, is a "response";
first about requests, all requests start with the same header, which always begins with a constant 32-byte value:
A unique code expected to get reflected back from the cart
request_size
uint32le
0x24
0x4
same as additional_data_size;
expected_response_size
uint32le
0x28
0x4
Expected size of the response from the cart.
command
uint8
0x2C
0x1
A unique number for every command
unknown
uint32le
0x2D
0x4
Always 0x00, not sure what its for
additonal_data_size
uint32le
0x31
0x4
0x3 + request body size
Following that, is the actual contents of the request, a "request_size" buffer of which the format of differs for every command value, after each request; the console then attempts to 'read' a response;
each response also has the same header at the start, but different data inside depending on what command was sent to it; and is then padded out to exactly 1028 bytes. the response header is not in the same format and is instead as follows:
Name
Type
Offset
Length
Description
response_code
int32le
0x00
0x4
should match the expected_response_code from the request
additional_data_size
int32le
0x04
0x4
Always 0x00
response_size
int16be
0x08
0x2
Size of response, should be equal to endian-swapped expected_response_size from request.
error_code
int8
0x10
0x1
0 On success, not 0 on error.
And then anything following that for the next "response_size" buffer of which the format differs for every command value received; and padded out after that until it fits in exactly 1028 bytes;
NOTE: An error is most likely thrown if the actual size of the response does not fit the expected response size, however i did not test this- the vita never makes a request where this would be the case, therefore the cartridge never would trigger this case in normal use; trying to test these things on-console is a bit annoying; but it is something to look into at some point
As for the commands, there are 8 (known) commands, that are used for the gc authentication, i have tried my best to come up with "reasonable" names for them, they are as follows
Command Name
Command ID
START
0xC4
GET_STATUS
0xC2
GENERATE_SESSION_KEY
0xA1
EXCHANGE_SHARED_RANDOM
0xA2
EXCHANGE_SECONDARY_KEY_AND_VERIFY_SESSION
0xA3
VERIFY_SECONDARY_KEY
0xA4
GET_P18_KEY_AND_CMAC_SIGNATURE
0xB1
GET_P20_KEY_AND_CMAC_SIGNATURE
0xC1
a few observant of you might notice that given this is 8 commands, but there are 20 packets in total (10 request, and 10 responses); however only 8 unique commands are shown here, this is because some commands are re-used throughout the authentication process;
The first few commands are relatively simple, in such a way that they can even be figured out by just looking at packet logs. (and are some of the few motoharu actually had implemented.) but lets go over them one by one; in more technical detail how they actually work;
START: probably used to initialize the authentication process, or possibly just for identifying it even does cmd56 at all.
No request data is ever sent for this request
Request size is 0x3
Response size is 0x13;
Expected Response Code is 0x31
Response is always exactly "00000000000000000000000000010104"
Response Structure:
Name
Type
Offset
Length
Description
start
byte
0x00
0x10
Always "00000000000000000000000000010104"
GET_STATUS: used to determine the state of the game cart. they start out locked initially if the device is locked, typically all reads and writes will fail, the authentication process is used to unlock the device.
No request data is ever sent for this request
Request size is 0x3;
Response size is 0x5;
Expected Response Code is 0x23;
Response is "FF00" when locked, "0000" when unlocked.
Response Structure:
Name
Type
Offset
Length
Description
status
uint16be
0x00
0x4
"0xFF00" when locked, "0x0000" when unlocked
NOTE: the vitas GcAuthMgr driver expects the cart to be locked at this point; and will abort the authentication process if the cart reports that it is already unlocked! the response MUST be 0xFF00 at this point!
beyond here, it stops being so easy and requires a fair bit of reverse engineering to understand, this is also about where motoharu's reverse engineering work ends;
GENERATE_SESSION_KEY this is a used to derive a unique per-session key; this key is then used to encrypt all communications from then forward;
No request data is ever sent for this request
Request size is 0x3;
Response size is 0x2b;
Expected Response Code is 0x2
Response data: is a 16-bit key id and 0x20 byte random number sequence generated by the cartridge (CART_RANDOM);
Response Structure:
Name
Type
Offset
Length
Description
unk0
uint16be
0x00
0x2
Always "0xE000"
key_id
uint16be
0x02
0x2
Identifer for what keyseed to use
unk1
uint16be
0x04
0x2
Always "0x02"
unk2
uint16be
0x06
0x2
Always "0x03"
cart_random
byte
0x08
0x20
Random data from the cart.
then after this, both the cart and the vita, will generate a unique the 'session key' is derived via the following algorithm :
first the 16 bit "key id" has 4 valid values, which can be used to pick one of 4 key seeds; these values are 0x8001, 0x8002, 0x8003, and 0x1; the key seeds are as follows:
Key ID
Key Seed
0x1
7f1fd065dd2f40b3e26579a6390b616d
0x8001
6f2285ed463a6e57c5f3550ddcc81feb
0x8002
da9608b528825d6d13a7af1446b8ec08
0x8003
368b2eb5437a821862a6c95596d8c135
The selected keyseed is then decrypted with AES-256-ECB using Keyslot0x345 and saved in Keyslot 0x21 .. then a AES-128-CMAC of the the random number provided by the cart using the AES-ECB decrypted keyseed from the previous step;
if the Key ID is 0x8001, 0x8002, or 0x8003, then we are done and the result of this CMAC operation is the session key, however- in the case where the Key ID is 0x1 there is an extra step-
where the resulting CMAC hash, is then; decrypted again with AES-128-CBC but this time, using Keyring 0x348 and saved in Keyslot 0x24 and an IV of "8b14c8a1e96f30a7f101a96a3033c55b"; then the resulting plaintext is used as the session key;
switch (key_id) { case PROTOTYPE_KEY_ID1: keyseed = GCAUTHMGR_0x8001_KEY; break; case PROTOTYPE_KEY_ID2: keyseed = GCAUTHMGR_0x8002_KEY; break; case PROTOTYPE_KEY_ID3: keyseed = GCAUTHMGR_0x8003_KEY; break; case RETAIL_KEY_ID: keyseed = GCAUTHMGR_0x1_KEY; break; } AES_256_ECB_decrypt(BIGMAC_KEY_0x345, keyseed, 0x10); AES_128_CMAC(keyseed, cart_random, 0x20, session_key_out);
if (key_id == 0x1) { AES_128_CBC_decrypt(BIGMAC_KEY_0x348, session_key_out, 0x10, GCAUTHMGR_0x1_IV); }
}
Final Fantasy X HD showing Key ID: 0x01
NOTE: despite the code for deriving a key based on other Key ID than 0x1 is still present in the secure kernel which lacks the last 0x348 decrypt step; it won't actually work in practice;
the vita kernel's (gcauthmgr.skprx) after FW0.998 or so; will check if(key_id > 0x8001) then fail the authentication if true; - this supposedly locks out all Key ID except 0x1* from being used in any retail firmware, and supposedly means that 0x8001-0x8003 are only usable on prototype firmware.
however due to what i can only assume is a bug, this is written as a greater than (>), and not greater than or equal (>=), therefore, 0x8001 specifically is actually usable;
the session key is used with AES-128-CBC decrypt using an IV of all 0x00 in the structure definitions; i will label sections encrypted using the session key with a red background
likewise i dont think any Key ID besides 0x1 is ever used; all the games i have report KeyID 0x1 you can check this in GcToolKit
.. if you see anything else in here you may have a prototype cartridge!
NOTE: cart_random is actually always the same for any given cart, its only random across different carts i suspect this might be because gamecarts don't have any real entropy source; likewise there seems to be a bug(?) where the first 0xC bytes are not random;
EXCHANGE_SHARED_RANDOM this is used to validate that the vita derived the right key; it is done by both the vita and the cart exchanging a random sequence. encrypted with the session key, then the vita checks it matches what was sent to it.
Request data: vita includes the received key_id from previous request and then a random 0x10 bytes sequence (shared_rand_vita);
Request size is 0x15;
Response size is 0x23;
Expected Response Code is 0x3;
Response data: 0x20 byte random data from cart and vita: top 0x10 is generated by cartridge (shared_rand_cart) and then lower 0x10 is from the same (shared_rand_vita) with first byte logical OR'd by 0x80; the entire response is encrypted with the session_key.
Request Structure:
Name
Type
Offset
Length
Description
key_id
uint16be
0x00
0x2
Same as in previous request.
shared_rand_vita
byte
0x02
0x10
Random data from the vita.
Response Structure:
Name
Type
Offset
Length
Description
shared_rand_cart
byte
0x00
0x10
Random data from the cart.
shared_rand_vita
byte
0x10
0x10
Same as from request
the packet response is received by the vita, and the packet is decrypted using the session key;
the lower 0x10 bytes are compared to the random bytes the console sent to the cart;
and they match then the authentication continues; otherwise it fails right here, thus requiring that the cart must have derived the same session_key. as the vita-
as if this is not the case then the shared_rand_vita from the cart will not match what was generated by the vita;
BUG: The vita random bytes comparison is off-by-one, presumably because of the 0x80 logical OR from the cart however the correct way to do it would be for the vita to logical OR the values itself and compare to those;
// copy key id into request request->data.key_id = endian_swap(state->key_id);
// randomize vita portion of shared random rand_bytes(request->data.shared_rand_vita, sizeof(request->data.shared_rand_vita)); memcpy(state->shared_random.vita_part, request->data.shared_rand_vita, sizeof(state->shared_random.vita_part));
if (memcmp(response->data.shared_rand_vita + 0x1, state->shared_random.vita_part + 0x1, sizeof(state->shared_random.vita_part)-0x1) == 0) { LOG("(VITA) cart and vita have the same shared_random.vita_part ...\n");
// copy cart part into global state shared_random memcpy(state->shared_random.cart_part, response->data.shared_rand_cart, sizeof(response->data.shared_rand_cart)); return GC_AUTH_OK; } else { LOG("(VITA) invalid shared_random.vita_part!\n"); return GC_AUTH_ERROR_VERIFY_SHARED_RANDOM_INVALID; } return GC_AUTH_ERROR_VERIFY_SHARED_RANDOM_FAIL; }
EXCHANGE_SECONDARY_KEY_AND_VERIFY_SESSION this is used for two purposes, the first; is to exchange a new key, which we will call the "secondary_key", as well as this, it is used for the cart to verify that the *vita* generated the correct session key.
Request data: a new random 0x10 byte "secondary key", the full 0x20 byte contents of the shared_random; with index 0x00 and 0x10 logically OR'd with 0x80; finally the entire section is encrypted using the session_key from earlier,
Request size is 0x33;
Response size is 0x23;
Expected Response code is 0x3;
No response data is ever sent for this request;
Request Structure:
Name
Type
Offset
Length
Description
secondary_key
byte
0x00
0x10
Random data from the vita, used as a key
shared_rand_vita
byte
0x10
0x10
Same as from previous step
shared_rand_cart
byte
0x20
0x10
Same as from previous step
on the gamecart, it will then decrypt the data using the session_key; then it will store the secondary_key in memory somewhere,
and then will take the shared_random generated from the previous step, perform a logical OR bytes at index 0x00 and 0x10 from it with 0x80; then compare the result to the shared_random obtained from the previous step.
if these do match, then the cart will set the cart_status to 0x0000 (CART_UNLOCKED) and will now accept read and write commands from here onwards;
however if they do not match, then response_code and error_code are both set to 0xF1, and the cart_status remains 0xFF00 (CART_LOCKED)
this effectively requires that the vita also derived the same session_key as the cart. because if it didn't then the shared_random wouldn't match, and in that case; the cart is never unlocked;
the vita will also check if the error_code result is 0x00, and abort the authentication if its not that (like i.e if its 0xF1)
once again i have written an example of this in C but this time from the cartridge side of things:
after this request, the vita sends GET_STATUS again; refer to the previous documentation on this command from near the start- however this time it expects the response to br 0x0000 (GC_UNLOCKED) and will fail the authentication if it is still locked,
this check could be skipped with CFW; however it would be redundant since this would also mean that the vita is unable to read any sectors of the gamecart- so you wont be able to play the game on it.
moving on from there we have now validated the cart and vita, and the rest of the protocol is about exchanging two per-cart keys; (p18 and p20) which are used to decrypt the license rif to derive the klicensee for decrypting the game executable.
everything after this point is encrypted using the secondary_key, once again, this is AES-128-CBC, with an IV of all 0s; in the structure definitions; i will label sections encrypted using the secondary_key with a blue background
VERIFY_SECONDARY_KEY this is used to validate that both the cart and vita have the secondary key, this is done by the vita exchanging a random value, and the cart responding with its CART_RANDOM from the previous GENERATE_SESSION_KEY.
Request data: 0x10 random sequence, with the first byte logical OR'd with 0x80.
Request size is 0x13;
Response size is 0x43;
Expected Response code is 0x7;
Response data: first 0x8 bytes are random, after that is the same random sequence from the request, with first byte logical OR with 0x80; then the CART_RANDOM from GENERATE_SESSION_KEY, then 0x8 more random padding the whole response is encrypted using SECONDARY_KEY; generated in the previous step;
Request Structure:
Name
Type
Offset
Length
Description
challenge_bytes
byte
0x00
0x10
Random data from the vita.
Response Structure:
Name
Type
Offset
Length
Description
pad0
byte
0x00
0x8
Random byte padding.
challenge_bytes
byte
0x08
0x10
Same as from request
cart_random
byte
0x18
0x20
Same as from GENERATE_SESSION_KEY
pad1
byte
0x38
0x8
Random byte padding.
The VITA then should decrypt the result using the secondary_key, and validate that the random sequence it sent matches, AND that the cart_random matches; if both of these are true then it continues on, if not then the authentication fails; by doing so it can be sure that the cart has the correct secondary_key;
BUG: The vita random bytes comparison is off-by-one, presumably because of the 0x80 logical OR from the cart however the correct way to do it would be for the vita to logical OR the values itself and compare to those;
NOTE: You *MUST* Complete this step in under 5000μs, or else you will be deemed a filthy cobra blackfin by the vita kernel.
once again i have an implementation of this in C if you would like to take a closer look:
// replicate the bug where the first byte doesn't have to match if (memcmp(response->data.challenge_bytes+0x1, request->data.challenge_bytes+0x1, sizeof(response->data.challenge_bytes) - 1) == 0) { LOG("(VITA) decrypted secondary_key challenge matches !\n");
GET_P18_KEY_AND_CMAC_SIGNATURE this is used to send over the p18 unique per-cartridge key, as well as a request type (either 0x2 or 0x3) which does nothing; and then also a CMAC signature derived using secondary_key.
Request data: 0x10 random sequence with the first byte logical OR'd with 0x80. then, 0xf padding bytes of all 00's, and then finally a single byte set to the request type. that is is encrypted with the secondary_key; and then a CMAC Hash of the above is appended to the end;
Request size is 0x33;
Response size is 0x43;
Expected Response code is 0x11;
Response data: the first 0x10 byte random sequence, followed by the 0x20 byte per-cartridge p18_key; a 0x10 bytes of padding with 0x00, finally this is encrypted with secondary_key, then a CMAC hash of the entire thing is appended to the end
Request Structure:
Name
Type
Offset
Length
Description
challenge_bytes
byte
0x00
0x10
Random data from the vita.
pad0
byte
0x10
0x0F
All 0's / nullbytes.
type
uint8
0x1F
0x01
Either 0x02 or 0x03
cmac_signature
byte
0x20
0x10
Generated using secondary_key.
Response Structure:
Name
Type
Offset
Length
Description
challenge_bytes
byte
0x00
0x10
Same as from request
p18_key
byte
0x10
0x20
A Unique Per-Cartridge Key
cmac_signature
byte
0x30
0x10
Generated using secondary_key.
the vita then validates the cmac hash of the response, decrypts the response with secondary_key. then compares the 0x10 byte random with what it sent, then records the p18 key;
after this request GET_P18_KEY_AND_CMAC_SIGNATURE is sent again, for some reason(?) it is sent twice first time with "request type" set to 0x2 then later 0x3; this value does not seem to do much of anything really. nor can i figure out any real reason to have this be sent twice
the way the CMAC is generated is not just by CMACing the raw data, but rather is in a particular format that is as follows:
a 0x3 byte header
then 0xD byte padding of (0x00)
then finally, the data to hash with CMAC
CMAC Structure:
Name
Type
Offset
Length
Description
header
byte
0x00
0x3
3 byte header
pad0
byte
0x03
0x0D
All 0's / nullbytes.
input_data
byte
0x10
various
Actual data to CMAC
finally the secondary_key is always used as the key to generate the CMAC hash
in the case of the cmd56 request, the cmac hash is
"make_int24(request->command, 0x00, request->additional_data_size)" where make_int24 is "((b3 << 16) | (b2 << 8) | (b1 << 0))"
for the responses the header is just "response->response_size"
BUG: The vita random bytes comparison is off-by-one, presumably because of the 0x80 logical OR from the cart however the correct way to do it would be for the vita to logical OR the values itself and compare to those;
and an implementation of this in C on vita is as follows:
// i dont know what this is for, its just all that changes between the two calls to it, // honestly i dont know why this is command is issued twice; request->data.type = type; LOG("(VITA) get_p18_key: type 0x%x\n", type);
// create a cmac of all the p18 data, place it at the end of the request. do_cmd56_cmac_hash(&state->secondary_key, request->data, make_int24(request->command, 0x00, request->additional_data_size), request->data.cmac_signature,offsetof(get_p18_key_and_cmac_signature_request, cmac_signature));
send_packet(state, request, response); if (response->error_code == GC_AUTH_OK) { // check status from gc uint8_t expected_cmac[0x10];
// replicate the bug where it only checks after the first byte if (memcmp(expected_challenge+0x1, response->data.challenge_bytes+0x1, sizeof(expected_challenge) - 0x1) == 0) { LOG("(VITA) p18 challenge success!\n"); memcpy(state->per_cart_keys.packet18_key, response->data.p18_key, sizeof(state->per_cart_keys.packet18_key));
this is the final request sent in the authentication; it is similar to the p18 step, except the random bytes in the request are not encrypted or CMAC hashed, only the response is;
GET_P20_KEY_AND_CMAC_SIGNATURE this is used to send the p20 unique per-cartridge key.
Request data: a 0x10 random byte sequence with the first byte logical or'd with 0x80.
Request size is 0x13;
Response size is 0x53;
Expected Response code is 0x19;
Response data: the first 0x8 bytes are random padding, followed by the 0x10 random from the request after that, is 0x20 p20 key; then 0x8 more random padding bytes; finally, this whole thing is encrypted with secondary_key, and a CMAC hash of that is appended to the end
Request Structure:
Name
Type
Offset
Length
Description
challenge_bytes
byte
0x00
0x10
Random data from the vita.
Response Structure:
Name
Type
Offset
Length
Description
pad0
byte
0x00
0x8
Random byte padding
challenge_bytes
byte
0x08
0x10
Same as from request
p20_key
byte
0x18
0x20
A Unique Per-Cartridge Key
pad1
byte
0x38
0x8
Random byte padding
cmac_signature
byte
0x40
0x10
Generated using secondary_key.
the vita then verifies the CMAC hash (which is generated the same way as mentioned in the previous step); and decrypts the response with the secondary_key; it then validates that the 0x10 random sequence it sent, matches the response it then records the 0x20 byte per-cart p20 key;
finally, a final 'rif_key' is derived, by using sha256(p20_key . p18_key) the dot (.) operator denotes concatenation; the result of this is a rif_key that is then later used by "npdrm.skprx" used to decrypt the klicensee from the license.rif file of the game;
NOTE: p20 and p18 are different on every gamecart (even for the same game) and as such every copy of Gravity Rush (PS Vita) is personalized. (as well as every other vita game)
and as such, i hope now that this diagram comparing different methods of backing up games included in GcToolKit's readme, maybe makes a bit more sense! a C version of this command handling is also provided below as usual;
both "EXCHANGE_SECONDARY_KEY_AND_VERIFY_SESSION" & EXCHANGE_SHARED_RANDOM" combined, require that; both the vita and the cart generate the same session_key, and in order to do that, then both the vita and cart need to knows keyring 0x345 and 0x348.
however, EXCHANGE_SECONDARY_KEY_AND_VERIFY_SESSION is only actually needed if you are trying to read an official cart someone trying to implement a game cartridge would not need to fully implement this; you can simply always unlock the cart and always return a success code (0x00) regardless of if the vita provides the correct shared_random or not; decrypting the packet using the session key and obtaining the secondary_key is still necessary though;
as that is used to exchange the p18 & p20 keys as well as the CMAC challenge that occurs later- so its not too useful
Cryptographic features:
I suspect constant 'logical or with 0x80' steps in the algorithm, is meant to prevent ciphertext reuse granted, AES-CBC already does this, however since the IV is nullbytes, this means the first AES block is equivalent to AES-ECB; likewise they also often moved the challenge request to another part in the response (like w 0x8 byte padding) which also prevents ciphertext reuse
another point is that algorithm surprisingly uses only symmetric cryptography, one would think that you would use a public key on the cart; and a private key on the vita; instead they rely on shared secrets; notably session_key and secondary_key;
in practice the security is entirely dependant on the "GENERATE_SESSION_KEY"; as everything else that comes afterwards is encrypted using the session key derived here, even the secondary_key that is used later is simply sent to the cart encrypted using the previous session_key;
and the session key derivation security is in theory based on hardware sealed keyrings, unless a vulnerability is found in the derivation algorithm extracting those keyrings is required; but that is non-trivial because 0x345 and 0x348 are never directly accessible by the console or exposed to the CPU; however if they ever were extracted then this protocol would be broken forever;
Vulnerabilities:
* the check blocking the prototype key_id is if(key_id > 0x8001) when it should be >= this means, that a key_id of exactly 0x8001 is allowed; this allows for a custom cartridge to be able to skip the last decrypt step, allowing you to (via the racoon exploit) extract the decrypt from 0x345 and run the CMAC manually allowing the session key to be determined;
* the random number used for the CMAC generation is what is sent via the cart in the "GENERATE_RANDOM_KEY" packet; and is controlled 100% by the cartridge itself; therefore a custom cart can simply provide the same "random number" every time, doing this will result in the same session key being derived every time; this allows you to use key_id 0x1, and use (the racoon exploit) again to extract the final resulting session key
... combining these approaches, allows you to discover a session key without even needing raccoon exploit at all; because the CMAC result is actually stored to a temporary buffer, and not to another bigmac keyring; and since on these prototype key_id the CMAC result is the session key this allows the final session key to be extracted with only F00D Code Execution, and no raccoon exploit at all-
keep in mind: that this only allows for creating a flash cartridge, but NOT for making a cart dumper, you would still need to dump cartridges on console (doing this also requires F00D code execution btw!)
However as cool as this is, it has also all become completely irrelevant since a few years ago, all hardware sealed keys for the console were extracted, including 0x345 & 0x348; however; was not the case when i started looking into this initially
which effectively means the security of vita cartridges are effectively broken forever at this point. but i thought this was neat either way;
Conclusions:
oh and a little thing; with a bit of effort you can create a vita plugin to intercept cmd56 read/write commands. combine that with an eMMC to SD adapter, with a raw DD dump of a game, and put that in a SD2VITA, and you can actually test if your cmd56 implementation works for real :)
and of course it does; Now, stay tuned for part 2 where i do this for real on FPGA; so it works on OFW, w no plugins at all :)
Relevant Resources:
LibCmd56 - Complete implementation of GC Auth on both vita and gamecart side
GcToolKit - A homebrew Vita Game Cart Dumper that includes p18 and p20 keys
PythonWhiteFin - Packet logging & code to disable the blackfin checks
PsvCmd56 - Motoharu's (incomplete) vita cmd56 implementation