Difference between revisions of "Signing bug"

From WiiBrew
Jump to navigation Jump to search
(→‎Technical explanation: renaming in the code, and left a marker where the bug is (I also almost fixed the signing bug in that code, oops))
(→‎Technical explanation: struct decrypted_signature for easier understanding of padding)
Line 5: Line 5:
 
<source lang=c>
 
<source lang=c>
 
#define SHA1_LENGTH 20
 
#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 {
 
struct rsa_cert {
 
     u32 signature_type;
 
     u32 signature_type;
     char rsa_signature[256]; // 2048 bits
+
     char rsa_signature[RSA_BLOCK_LENGTH]; // 256 bytes, 2048 bits
 
     char unused[60];
 
     char unused[60];
 
};
 
};
Line 25: Line 32:
  
 
int verify_tmd (struct tmd stmd) {
 
int verify_tmd (struct tmd stmd) {
   char decrypted_sig[256] = RSA_DecryptSig(CA_public_key, stmd.cert.rsa_signature);
+
   struct decrypted_signature decrypted_sig = (struct decrypted_signature) RSA_DecryptSig(CA_public_key, stmd.cert.rsa_signature);
   char sig_hash = decrypted_sig[256-SHA1_LENGTH:256];
+
   char sig_hash[SHA1_LENGTH] = decrypted_sig.sha1hash;
 
   char payload_hash[SHA1_LENGTH] = SHA1(stmd.view);
 
   char payload_hash[SHA1_LENGTH] = SHA1(stmd.view);
  

Revision as of 20:33, 15 August 2021

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.

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.