Signing bug

From WiiBrew
Jump to navigation Jump to search

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.

Technical explanation

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

#define SHA1_LENGTH 20
#define RSA_BLOCK_LENGTH 256
#define PADDING_LENGTH RSA_BLOCK_LENGTH - SHA1_LENGTH

struct decrypted_signature {
    u8 padding[PADDING_LENGTH]; // not verified
    u8 sha1hash[SHA1_LENGTH];
};

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

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

struct tmd {
    struct rsa_cert cert;
    struct tmdview view;
}

int verify_tmd (struct tmd stmd) {
  struct decrypted_signature decrypted_sig = (struct decrypted_signature) RSA_DecryptSig(CA_public_key, stmd.cert.rsa_signature);
  char sig_hash[SHA1_LENGTH] = decrypted_sig.sha1hash;
  char payload_hash[SHA1_LENGTH] = SHA1(stmd.view);

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

int is_a_valid_disc(struct tmd stmd, char *disc_hash) {
   if(verify_tmd(stmd) == SIG_BAD) {
     return DISC_BAD;
   }
   if(memcmp(stmd.view.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'). strncmp compared each byte in a binary block of data, stopping when either a NULL byte is reached, or the number of characters checked reaches the size passed in. Stopping prematurely still yields a positive result if both strings have the same NULL byte. This 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.

Fixing the bug

This bug was first fixed in IOS37. A month later, Korean consoles were released with IOS40 and above having this fixed. The fix was backported to IOS30 and IOS31 in the 3.3 update, and all remaining IOSes except IOS16 had it fixed in 3.3rev03. IOS16 was stubbed in 4.0.

Instead of changing the code to use memcmp, the updated code manually compares the hash. Interestingly, the code continues to check the entire hash after a mismatch.

iQue strncmp

Interestingly, the iQue version of libc implements strncmp the way memcmp is meant to be implemented.[1] While the hash checking itself used memcmp, this suggests that Nintendo had legitimate confusion over the difference between strncmp and memcmp even before the Wii was released.

Irritatingly, this prevents exploitation of the iQue Player's own signing bug, psychic paper,[2] due to the kernel's memory layout.

References

1. https://github.com/decompals/iQuePlayer-SecureKernel/blob/4a505b9a2c6913402090661b0c7a7dfaaf94998e/src/9FC031D0.c#L262
2. {{{1}}}