Signing bug

From WiiUBrew
Jump to navigation Jump to search

Simple explanation

The signing (also known as Trucha) bug was a bug present in earlier IOS versions that allowed the digital signatures (which show that Nintendo had approved the content in question) of software to be easily faked, which allowed the installation of software that Nintendo hadn't approved. Shortly after its widespread use appeared, it was patched; first in IOS37. This exploit was used in the original version of the Homebrew Channel installer, and is still used in many applications.

Detailed explanation

Here is a simplified pseudocode implementation that shows the hash-comparison bug present in some versions of IOS:

#define SHA1_LENGTH 20

struct rsa_cert {
    u32 signature_type;
    char rsa_signature[256]; // 2048 bits
    char unused[60];

struct tmd {
    char issuer[0x40];
    // more metadata...
    char content_hash[SHA1_LENGTH];
    // more content records and hashes...

struct signed_tmd {
    struct rsa_cert cert;
    struct tmd tmd;

int verify_tmd (struct signed_tmd stmd) {
  char decrypted_sig[256] = RSA_DecryptSig(CA_public_key, stmd.cert.rsa_signature);
  char sig_hash = decrypted_sig[256-SHA1_LENGTH:256];
  char payload_hash[SHA1_LENGTH] = SHA1(stmd.tmd);

  if (strncmp(payload_hash, sig_hash, SHA1_LENGTH) == 0) {
     return SIG_OK;
  } else {
     return SIG_BAD;

int is_a_valid_disc(struct signed_tmd tmd, char *disc_hash) {
   if(verify_tmd(stmd) == SIG_BAD) {
     return DISC_BAD;
   if(memcmp(stmd.tmd.content_hash, disc_hash, SHA1_LENGTH) != 0)  {
     return DISC_BAD;

   return DISC_OK;

The bug here is that payload_hash is a binary SHA1 hash (not an ASCII string), and therefore may contain a NULL byte ('\0'). To quote from the first google hit for strncmp:

Compares up to num characters of the C string str1 to those of the C string str2. This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ, until a terminating null-character is reached, or until num characters match in both strings, whichever happens first.

This last part means that when strncmp finds a NULL byte it stops comparing, even if there is more data after the NULL.

This reduces the effective length of the hash to the number of bytes before the NULL byte, and the difficulty of finding a hash collision is reduced from 28*SHA1_LENGTH to 28*(bytes up to the NULL). That is a big change if the NULL is early in the hash. If the NULL byte is at 4th byte, that means that there is a one in 28*4 chance that the hash matches (three data bytes and the final NULL, which must also match). This is an average of 4 294 967 296 attempts to produce a valid partial collision, which can be computed in under 30 minutes on a modern computer that can compute a few million hashes per second. Nintendo signatures that have these properties certainly exist.

Furthermore, since we can brute force both the SHA-1 hash of the content (by changing unused bytes) and the SHA-1 that results after decrypting (by changing the signature randomly, which is possible because Nintendo doesn't check the fixed padding in the decrypted RSA signature), all we have to do is brute force both until the first byte of the hash is zero. This only requires about 256 RSA decryptions and 256 SHA-1 sums, both of which can be computed in a fraction of a second.

The process can be simplified further by taking advantage of the mathematical properties of RSA. Given the signature m, and the public key (e, n), the decrypted signature is calculated as me mod n. A zero signature (m = 0) always results in a zero result, regardless of the values of e and n (that is, regardless of the certificate that is used to check the signature). All we have to do is zero out the signature and get a guaranteed result of all zeroes. This reduces the time needed to build a fake signature to an average of 256 short SHA-1 sums, which can be done in mere milliseconds. The actual number of attempts required can vary (and could theoretically be infinite), since SHA-1 behaves like a random number generator. This is why having to try a couple thousand times isn't uncommon, and why changing a single byte when bruteforcing is not sufficient.

tmbinc has a more thorough explanation here.

This bug was first fixed in IOS37. As of the 3.3 update the fix had spread to IOS30 & 31, and by Oct 23, 2008 it was in all but one IOS. This last IOS was fixed with the 4.0 update.