cryptopals

Matasano's cryptopals challenges (cryptopals.com).
Log | Files | Refs | README | LICENSE

commit ad2df1735810f234eaf99d77340d72968817ed94
parent 232a0ee5abe359af55b2f34f384dffacc5808c73
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon,  5 Jun 2023 23:13:11 +0400

Finish set 2.

Diffstat:
Mdocs/s2.md | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mlib/Cryptopals/AES.hs | 16+++++++++-------
Mlib/Cryptopals/Block/Attacks.hs | 29++++++++++++++++++-----------
Mlib/Cryptopals/Util.hs | 5+++++
4 files changed, 117 insertions(+), 23 deletions(-)

diff --git a/docs/s2.md b/docs/s2.md @@ -191,16 +191,17 @@ just feeding in more than a block's worth of them -- necessarily some E.g.: one can determine that "AAAAAAAAAAAAAAAA" encrypts to "57eef2e16c3867b9889350eb5732c183", so we can look for that ciphertext in the result in order to locate an "origin," only analyzing ciphertexts -in which it appears. By chopping that and any preceeding bytes from the -ciphertext, the attack reduces to the simpler version we've already -done. +in which it appears (since, if it doesn't happen to align perfectly in +a block, we won't see it in the ciphertext). By chopping that and any +preceeding bytes from the ciphertext, the attack reduces to the simpler +version we've already performed. The `Cryptopals.Block.Attacks.hardIncrByteEcbAttack` function will perform the attack; it's just a version of `incrByteEcbAttack` from challenge 12 adapted to handle a monadic oracle. `Cryptopals.Block.Attacks.attackProxy` wraps the `weirdEncrypter` oracle -and does the work of locating our malicious block and pruning the -ciphertext for us, so we can attack `weirdEncrypter` via: +and does the work of inserting/locating our malicious block and pruning +the ciphertext for us, so we can attack `weirdEncrypter` via: > plain <- hardIncrByteEcbAttack (attackProxy weirdEncrypter) gen > TIO.putStrLn $ TE.decodeUtf8 plain @@ -224,3 +225,82 @@ Nothing on inputs with invalid padding: Nothing #### 2.16 + +This one is pretty cool and tricky. AES encryption in CBC (cipher block +chaining) mode proceeds as follows: + + for plaintext p = (p_1, .., p_l) + block encryption w/key k enc_k + initialization vector IV + xor operator + + + let c_0 = IV + c_1 = enc_k(c_0 + m_1) + c_2 = enc_k(c_1 + m_2) + .. + c_l = enc_k(c_{l-1} + m_l) + + in ciphertext c = (c_0, c_1, c_2, .., c_l) + +and decryption goes like this: + + for ciphertext c = (c_0, c_1, c_2, .., c_l) + block decryption w/key k dec_k + xor operator + + + let p_1 = dec_k(c_1) + c_0 + p_2 = dec_k(c_2) + c_1 + .. + p_l = dec_k(c_1) + c_{l-1} + + in plaintext p = (p_1, p_2, .., p_l) + +Let plaintext block 3 be "AAAAA!admin!true". Since: + + p_2 = dec_k(c_2) + c_1 + +if ciphertext block 2 is ".....X.....X...." for unspecified bytes ".", +then we can recover the plaintext "AAAAA;admin=true" by substituting in +the following ciphertext block instead: + + .....X+a.....X+b.... + +for '+' the XOR operator, 'a' the byte such that '!' + 'a' = ';', and 'b' +the byte such that '!' + 'b' = '='. The second plaintext block, which is found +by passing `c_2` through AES decryption, will be effectively destroyed, but +the third plaintext block will be modified as desired. + +(Note by padding the malicious input block with 'A' bytes we line everything +up so as to take advantage of the following semicolon.) + +Using an IV of all zero bytes, `Cryptopals.Block.Attacks.bfcEncrypter` will +return the following, given the malicious input: + + 00000000000000000000000000000000 ................ + 63530f935e8a082aefc3010403ddd0c8 comment1=cooking + 5d7abe5ba83c8f15d5768e372b8d9d3e %20MCs;userdata= + d84ed53685df3c0fe1f047a8d8067e2b AAAAA!admin!true + ca8ca55382df2e963b10dec76fd282ce ;comment=%20like + cf39e6549b264c0eb44340b5f0e3ebdc %20a%20pound%20o + 214abfcb615d8c63406ee84093538051 fbacon__________ + +We can see that 0x3c and 0x37 in ciphertext block 2 (i.e. the third, counting +the IV) need to be changed. Some calculation shows what we need to XOR them +by: + + > showHex (ord '!' `B.xor` ord ';') mempty + "1a" + +and + + > showHex (ord '!' `B.xor` ord '=') mempty + "`c" + +so we replace 0x3c in `c_2` with `0x3c + 0x1a` and 0x37 +with `0x37 + 0x1c` and pass the resulting munged ciphertext +through the ";admin=true;" substring checker found in +`Cryptopals.Block.Attacks.bfcChecker`: + + > bfcChecker munged + True + diff --git a/lib/Cryptopals/AES.hs b/lib/Cryptopals/AES.hs @@ -22,10 +22,10 @@ decryptEcbAES128 key = CT.ecbDecrypt (initAES128 key) encryptCbcAES128 :: BS.ByteString -> BS.ByteString -> BS.ByteString -> BS.ByteString -encryptCbcAES128 iv key plaintext = loop iv mempty (BS.splitAt 16 plaintext) +encryptCbcAES128 iv key plaintext = loop iv iv (BS.splitAt 16 plaintext) where - loop !fiv !acc (b, bs) = - let xed = CU.fixedXor fiv b + loop las !acc (b, bs) = + let xed = CU.fixedXor las b enc = encryptEcbAES128 key xed nacc = acc <> enc in if BS.null bs @@ -33,12 +33,14 @@ encryptCbcAES128 iv key plaintext = loop iv mempty (BS.splitAt 16 plaintext) else loop enc nacc (BS.splitAt 16 bs) decryptCbcAES128 - :: BS.ByteString -> BS.ByteString -> BS.ByteString -> BS.ByteString -decryptCbcAES128 iv key ciphertext = loop iv mempty (BS.splitAt 16 ciphertext) + :: BS.ByteString -> BS.ByteString -> BS.ByteString +decryptCbcAES128 key ciphertext = + let (iv, cip) = BS.splitAt 16 ciphertext + in loop iv mempty (BS.splitAt 16 cip) where - loop !fiv !acc (b, bs) = + loop !las !acc (b, bs) = let dec = decryptEcbAES128 key b - nacc = acc <> CU.fixedXor dec fiv + nacc = acc <> CU.fixedXor dec las niv = b in if BS.null bs then nacc diff --git a/lib/Cryptopals/Block/Attacks.hs b/lib/Cryptopals/Block/Attacks.hs @@ -40,8 +40,7 @@ chaosEncrypter plaintext gen = do pos <- MWC.uniformR (5, 10) gen >>= flip bytes gen let tex = pre <> plaintext <> pos - pad = CU.roundUpToMul 16 (BS.length tex) - bs = CU.pkcs7 pad tex + bs = CU.lpkcs7 tex ecb <- MWC.uniform gen @@ -61,8 +60,7 @@ alienEncrypter plaintext = ] par = plaintext <> pos - pad = CU.roundUpToMul 16 (BS.length par) - bs = CU.pkcs7 pad par + bs = CU.lpkcs7 par in AES.encryptEcbAES128 consistentKey bs @@ -147,8 +145,7 @@ cpeEncrypt :: BS.ByteString -> BS.ByteString cpeEncrypt user = let tex = TE.encodeUtf8 $ profileFor (TE.decodeUtf8 user) - pad = CU.roundUpToMul 16 (BS.length tex) - bs = CU.pkcs7 pad tex + bs = CU.lpkcs7 tex in AES.encryptEcbAES128 consistentKey bs @@ -173,8 +170,7 @@ weirdEncrypter plaintext gen = do pre <- bytes bys gen let par = pre <> plaintext <> pos - pad = CU.roundUpToMul 16 (BS.length par) - bs = CU.pkcs7 pad par + bs = CU.lpkcs7 par pure $ AES.encryptEcbAES128 consistentKey bs @@ -206,6 +202,17 @@ attackProxy oracle input gen = loop gen where then loop g else pure $ BS.drop 16 target -nubplusplus :: (Eq a, Ord a) => [a] -> [a] -nubplusplus = fmap NE.head . NE.group . L.sort - +-- bitflipping CBC +bfcEncrypter :: BS.ByteString -> BS.ByteString +bfcEncrypter input = AES.encryptCbcAES128 iv consistentKey padded where + iv = BS.replicate 16 0 + filtered = BS.filter (`notElem` (BS.unpack ";=")) input + plaintext = "comment1=cooking%20MCs;userdata=" <> filtered <> + ";comment2=%20like%20a%20pound%20of%20bacon" + padded = CU.lpkcs7 plaintext + +bfcChecker :: BS.ByteString -> Bool +bfcChecker ciphertext = target /= mempty where + iv = BS.replicate 16 0 + plaintext = AES.decryptCbcAES128 consistentKey ciphertext + (_, target) = BS.breakSubstring ";admin=true;" plaintext diff --git a/lib/Cryptopals/Util.hs b/lib/Cryptopals/Util.hs @@ -2,6 +2,7 @@ module Cryptopals.Util ( CUB.chunks , CUB.hamming , fixedXor + , lpkcs7 , CUB.nhamming , CUS.often , CUB.panhamming @@ -42,6 +43,10 @@ pkcs7 tar bs = byt = tar - len `mod` tar in bs <> BS.replicate byt (fromIntegral byt) +-- lazy man's pkcs#7 padding +lpkcs7 :: BS.ByteString -> BS.ByteString +lpkcs7 bs = pkcs7 (roundUpToMul 16 (BS.length bs)) bs + unpkcs7 :: BS.ByteString -> Maybe BS.ByteString unpkcs7 bs = do (_, c) <- BS.unsnoc bs