GameCart Research

11/29/25

Home
About Me
Software
Devices
Ramblings
Archive

 

GameCart Research - The Crystal Website

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? ..

well to answer this we need to pull out either, the "SD Specifications Part 1 Physical Layer Specification"; from the SD association or
 "JEDEC STANDARD Embedded Multi-Media Card (e•MMC) Electrical Standard (5.1)", both of these are kinda gatekept by capitalism, and demand a large amount of money in order to access, and even then probably only under NDA.

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;

packetlog.c:

#include <stdio.h>
#include <stdarg.h>
#include <vitasdkkern.h>
#include <taihen.h>

#include "pcap.h"

static int sendHook = -1;
static tai_hook_ref_t sendHookRef;

static int recvHook = -1;
static tai_hook_ref_t recvHookRef;

int SceSdifSendGcPacket_Patched(void* instance, char* buffer, int bufferSz) {
 write_pcap_packet(buffer, bufferSz, 1);
 int ret = TAI_CONTINUE(int, sendHookRef, instance, buffer, bufferSz);
 return ret;
} 

int SceSdifReceiveGcPacket_Patched(void* instance, char* buffer, int bufferSz) {
 int ret = TAI_CONTINUE(int, recvHookRef, instance, buffer, bufferSz);
 write_pcap_packet(buffer, bufferSz, 0);
 return ret;
}

void _start() __attribute__ ((weak, alias ("module_start")));
int module_start(SceSize argc, const void *args)
{

 write_pcap_hdr();

 sendHook = taiHookFunctionImportForKernel(KERNEL_PID,
  &sendHookRef,
  "SceSblGcAuthMgr",
  0x96D306FA, // SceSdifForDriver
  0xB0996641, // SceSdifSendGcPacket
  SceSdifSendGcPacket_Patched);
 ksceKernelPrintf("[started] %x %x\n", sendHook, sendHookRef);
  
 recvHook = taiHookFunctionImportForKernel(KERNEL_PID,
  &recvHookRef,
  "SceSblGcAuthMgr",
  0x96D306FA, // SceSdifForDriver
  0x134E06C4, // SceSdifReceiveGcPacket
  SceSdifReceiveGcPacket_Patched);
 ksceKernelPrintf("[started] %x %x\n", recvHook, recvHookRef);
}
int module_stop(SceSize argc, const void *args)
{
 if (recvHook >= 0)   taiHookReleaseForKernel(recvHook, recvHookRef);
 if (sendHook >= 0)   taiHookReleaseForKernel(sendHook, sendHookRef);
  
 return SCE_KERNEL_STOP_SUCCESS;
}
 


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:

"DD1025441523FDC0F9E91526DC2AE084A903A297D4BBF852D3D4942C8903CC77"

the format of the header is as follows:

NameTypeOffsetLengthDescription
magicbyte0x000x20Always "DD1025441523FDC0F9E91526DC2AE084A903A297D4BBF852D3D4942C8903CC77"
expected_response_codeuint32le0x200x4A unique code expected to get reflected back from the cart
request_sizeuint32le0x240x4same as additional_data_size;
expected_response_sizeuint32le0x280x4Expected size of the response from the cart.
commanduint80x2C0x1A unique number for every command
unknownuint32le0x2D0x4Always 0x00, not sure what its for
additonal_data_sizeuint32le0x310x40x3 + 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:

NameTypeOffsetLengthDescription
response_codeint32le0x000x4should match the expected_response_code from the request
additional_data_sizeint32le0x040x4Always 0x00
response_sizeint16be0x080x2Size of response, should be equal to endian-swapped expected_response_size from request.
error_codeint80x100x10 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 NameCommand ID
START0xC4
GET_STATUS0xC2
GENERATE_SESSION_KEY0xA1
EXCHANGE_SHARED_RANDOM0xA2
EXCHANGE_SECONDARY_KEY_AND_VERIFY_SESSION0xA3
VERIFY_SECONDARY_KEY0xA4
GET_P18_KEY_AND_CMAC_SIGNATURE0xB1
GET_P20_KEY_AND_CMAC_SIGNATURE0xC1

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:
NameTypeOffsetLengthDescription
startbyte0x000x10Always "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:
NameTypeOffsetLengthDescription
statusuint16be0x000x4"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:
NameTypeOffsetLengthDescription
unk0uint16be0x000x2Always "0xE000"
key_iduint16be0x020x2Identifer for what keyseed to use
unk1uint16be0x040x2Always "0x02"
unk2uint16be0x060x2Always "0x03"
cart_randombyte0x080x20Random 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 IDKey Seed
0x17f1fd065dd2f40b3e26579a6390b616d
0x80016f2285ed463a6e57c5f3550ddcc81feb
0x8002da9608b528825d6d13a7af1446b8ec08
0x8003368b2eb5437a821862a6c95596d8c135

The selected keyseed is then decrypted with AES-256-ECB using Keyslot 0x345 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;

i have the algorithm implemented in C below :

cmd56_sm.c:

void derive_primary_key(uint8_t* session_key_out, uint8_t* cart_random, int key_id) {
 const uint8_t* keyseed;

 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:
NameTypeOffsetLengthDescription
key_iduint16be0x000x2Same as in previous request.
shared_rand_vitabyte0x020x10Random data from the vita.
Response Structure:
NameTypeOffsetLengthDescription
shared_rand_cartbyte0x000x10Random data from the cart.
shared_rand_vitabyte0x100x10Same 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;

an example of this in C here:

exchange_shared_random.c:

int exchange_shared_random(vita_cmd56_state* state, cmd56_request* request, cmd56_response* response) {
 cmd56_request_start(request, CMD_EXCHANGE_SHARED_RANDOM, calc_size(exchange_shared_random_request), calc_size(exchange_shared_random_response), 0x3);

 // 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));

 send_packet(state, request, response);
 if (response->error_code == GC_AUTH_OK) {
  decrypt_cbc_zero_iv(&state->session_key, response->data, sizeof(exchange_shared_random_response));

  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;
 }

 LOG("(VITA) response->error_code: 0x%X\n", response->error_code);
 return GC_AUTH_ERROR_REPORTED;
}

  • 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:
NameTypeOffsetLengthDescription
secondary_keybyte0x000x10Random data from the vita, used as a key
shared_rand_vitabyte0x100x10Same as from previous step
shared_rand_cartbyte0x200x10Same 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:

gc_exchange_secondary_key_and_verify_session.c:

void handle_exchange_secondary_key_and_verify_session(gc_cmd56_state* state, cmd56_request* request, cmd56_response* response) {
 cmd56_response_start(request, response);

 // decrypt the request ...
 decrypt_cbc_zero_iv(&state->session_key, req, sizeof(exchange_secondary_key_and_verify_session_request));

 // remember secondary_key
 AES_init_ctx(&state->secondary_key, request->data.secondary_key);
 or_w_80(state->shared_random, sizeof(shared_random));
 or_w_80(&request->data.challenge_bytes, sizeof(shared_random));

 if (memcmp(&request->data.challenge_bytes, &state->shared_random, sizeof(shared_random)) == 0) {
  LOG("(GC) session key validated, unlocking cart\n");
  state->lock_status = GC_UNLOCKED;
 }
 else {
  LOG("(GC) session key not valid, cart remaining locked.\n");
  
  state->lock_status = GC_LOCKED;
  cmd56_response_error(response, 0xF1);
 }
}

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:
NameTypeOffsetLengthDescription
challenge_bytesbyte0x000x10Random data from the vita.
Response Structure:
NameTypeOffsetLengthDescription
pad0byte0x000x8Random byte padding.
challenge_bytesbyte0x080x10Same as from request
cart_randombyte0x180x20Same as from GENERATE_SESSION_KEY
pad1byte0x380x8Random 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:

verify_secondary_key.c:

int verify_secondary_key(vita_cmd56_state* state, cmd56_request* request, cmd56_response* response) {
 cmd56_request_start(request, CMD_VERIFY_SECONDARY_KEY, calc_size(verify_secondary_key_request), calc_size(verify_secondary_key_response), 0x7);

 // generate challenge bytes
 rand_bytes_or_w_80(request->data.challenge_bytes, sizeof(request->data.challenge_bytes));
 send_packet(state, request, response);

 // decrypt response ...
 decrypt_cbc_zero_iv(&state->secondary_key, response->data, sizeof(verify_secondary_key_response));

 // 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");

  if (memcmp(response->data.cart_random, state->cart_random, sizeof(state->cart_random)) == 0) {
   LOG("(VITA) cart_random matches!\n");
   return GC_AUTH_RETURN_STATUS;
  }
  else {
   LOG("(VITA) cart_random invalid!\n");
   return GC_AUTH_ERROR_VERIFY_CART_RANDOM_INVALID_CART_RANDOM;
  }
 }
 else {
  LOG("(VITA) invalid challenge bytes!\n");
  return GC_AUTH_ERROR_VERIFY_CART_RANDOM_CHALLENGE_INVALID;
 }

 return GC_AUTH_ERROR_VERIFY_CART_RANDOM_FAIL;
}

  • 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:
NameTypeOffsetLengthDescription
challenge_bytesbyte0x000x10Random data from the vita.
pad0byte0x100x0FAll 0's / nullbytes.
typeuint80x1F0x01Either 0x02 or 0x03
cmac_signaturebyte0x200x10Generated using secondary_key.
Response Structure:
NameTypeOffsetLengthDescription
challenge_bytesbyte0x000x10Same as from request
p18_keybyte0x100x20A Unique Per-Cartridge Key
cmac_signaturebyte0x300x10Generated 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:
NameTypeOffsetLengthDescription
headerbyte0x000x33 byte header
pad0byte0x030x0DAll 0's / nullbytes.
input_databyte0x10variousActual data to CMAC

finally the secondary_key is always used as the key to generate the CMAC hash

cmd56_cmac.c:

void do_cmd56_cmac_hash(AES_ctx* ctx, uint8_t* data, uint32_t header, uint8_t* output, size_t size) {
 uint8_t cmac_input[0x100];
 memset(cmac_input, 0x00, sizeof(cmac_input));

 // aes-128-cmac the whole thing
 memcpy(cmac_input, &header, 0x3);
 memcpy(cmac_input + 0x10, data, size); // copy data to cmac_input + 0x10

 AES_CMAC_buffer(ctx, cmac_input, size + 0x10, output); // caclulate the CMAC ...
}

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:

vita_get_p18_key.c:

int get_packet18_key(vita_cmd56_state* state, cmd56_request* request, cmd56_response* response, uint8_t type) {0
 cmd56_request_start(request, CMD_GET_P18_KEY_AND_CMAC_SIGNATURE, calc_size(get_p18_key_and_cmac_signature_request), calc_size(get_p18_key_and_cmac_signature_response), 0x11);

 uint8_t expected_challenge[0x10];
 rand_bytes_or_w_80(expected_challenge, sizeof(expected_challenge));

 memcpy(request->data.challenge_bytes, expected_challenge, sizeof(expected_challenge));
 memset(request->data.pad0, 0x00, sizeof(request->data.pad0));

 // 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);

 encrypt_cbc_zero_iv(&state->secondary_key, request->data, sizeof(get_p18_key_and_cmac_signature_request));

 // 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];

  // generate p18 cmac
  do_cmd56_cmac_hash(&state->secondary_key, response->data, response->response_size, expected_cmac, offsetof(get_p18_key_and_cmac_signature_response, cmac_signature));

  if (memcmp(expected_cmac, response->data.cmac_signature, sizeof(expected_cmac)) == 0) { // check cmac
   LOG("(VITA) CMAC Matches!\n");
   decrypt_cbc_zero_iv(&state->secondary_key, resp, offsetof(get_p18_key_and_cmac_signature_response, cmac_signature));

   // 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));

    return GC_AUTH_OK;
   }
   else {
    LOG("(VITA) Invalid p18 challenge response!\n");
    return GC_AUTH_ERROR_P18_KEY_CHALLANGE_FAIL;
   }
  }
  else {
   LOG("(VITA) Invalid p18 CMAC!\n");
   return GC_AUTH_ERROR_P18_KEY_INVALID_CMAC;
  }
 }
 LOG("(VITA) response->error_code: 0x%X\n", response->error_code);
 return GC_AUTH_ERROR_REPORTED;
}

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:
NameTypeOffsetLengthDescription
challenge_bytes byte0x000x10Random data from the vita.
Response Structure:
NameTypeOffsetLengthDescription
pad0byte0x000x8Random byte padding
challenge_bytesbyte0x080x10Same as from request
p20_keybyte0x180x20A Unique Per-Cartridge Key
pad1byte0x380x8Random byte padding
cmac_signaturebyte0x400x10Generated 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;

vita_get_p20_key.c:

int get_packet20_key(vita_cmd56_state* state, cmd56_request* request, cmd56_response* response) {
 cmd56_request_start(request, CMD_GET_P20_KEY_AND_CMAC_SIGNATURE, calc_size(get_p20_key_and_cmac_signature_request), calc_size(get_p20_key_and_cmac_signature_response), 0x19);

 rand_bytes_or_w_80(request->data.challenge_bytes, sizeof(request->data.challenge_bytes));
 send_packet(state, request, response);

 if (response->error_code == GC_AUTH_OK) {
  uint8_t expected_cmac[0x10];
  do_cmd56_cmac_hash(&state->secondary_key, response->data, response->response_size, expected_cmac, offsetof(get_p20_key_and_cmac_signature_response, cmac_signature));

  if (memcmp(expected_cmac, response->data.cmac_signature, sizeof(expected_cmac)) == 0) { // cmac check
   LOG("(VITA) p20 cmac check pass\n");
   decrypt_cbc_zero_iv(&state->secondary_key, resp, offsetof(get_p20_key_and_cmac_signature_response, cmac_signature));
   
   // check challenge bytes match (and replicate the bug)
   if (memcmp(request->data.challenge_bytes+0x1, response->data.challenge_bytes+0x1, sizeof(response->data.challenge_bytes)-1) == 0) {
    LOG("(VITA) p20 challenge matches!\n");

    memcpy(state->per_cart_keys.packet20_key, response->data.p20_key, sizeof(state->per_cart_keys.packet20_key));
    return GC_AUTH_OK;
   }
   else {
    LOG("(VITA) Invalid Challenge Response!\n");
    return GC_AUTH_ERROR_P20_KEY_CHALLANGE_FAIL;
   }
  }
  else {
   LOG("(VITA) Invalid CMAC!\n");
   return GC_AUTH_ERROR_P20_KEY_INVALID_CMAC;
  }
 }
 LOG("(VITA) response->error_code: 0x%X\n", response->error_code);
 return GC_AUTH_ERROR_REPORTED;
}

Security Implications:

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

so you can just use them directly- they are:

KeyringValue
0x34574C39CA4EF4F122915C71EDA46C88B55BBAD1F4033D755CEA0563CC341F92E66
0x348C026281413FA462CCDEED4BD6D08C37CA6C9322ABD4C40ADE72A0F544F4013AD

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
  • SD Spec - SD Card Specification
  • eMMC Spec - eMMC Specification

 

Home | About Me | Software | Devices | Ramblings | Archive

This site was last updated 10/29/25

Subscribe to our RSS Feed!

no ai webring previous next a rectangle with an animated, shifting rainbow pattern with the words NO AI / WEBRING on it in big fat letters. to either side of the rectangle are two arrows pointing left and right with animated, concentrically expanding rainbow patterns in them