cryptopals

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

commit 64357d513f00456eab1de95b0917fe73ba37aff2
parent d6300a6ddf03e6ff84b49cddaed8b907a45a11c3
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue,  1 Aug 2023 22:24:59 -0230

Finish set 3.

Diffstat:
M.ghci | 2++
Mcryptopals.cabal | 2++
Mdocs/s3.md | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Alib/Cryptopals/Stream/Attacks.hs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/Cryptopals/Stream/RNG/MT19937.hs | 9++++-----
5 files changed, 163 insertions(+), 13 deletions(-)

diff --git a/.ghci b/.ghci @@ -1,5 +1,7 @@ :set prompt "> " :set -XOverloadedStrings +import qualified Data.Binary.Get as BG +import qualified Data.Binary.Put as BP import qualified Data.Bits as B import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL diff --git a/cryptopals.cabal b/cryptopals.cabal @@ -26,6 +26,7 @@ library , Cryptopals.Util , Cryptopals.Util.ByteString , Cryptopals.Util.Similarity + , Cryptopals.Stream.Attacks , Cryptopals.Stream.RNG , Cryptopals.Stream.RNG.MT19937 build-depends: @@ -39,6 +40,7 @@ library , mwc-random , primitive , text + , time , unordered-containers , vector diff --git a/docs/s3.md b/docs/s3.md @@ -112,7 +112,7 @@ gist of it: (MT19937) PRNG in standard return-the-generator fashion: > let gen = seed 42 - > bytes 3 gen + > tap 3 gen > ([1608637542,3421126067,4083286876],<MT19937.Gen>) The only annoying thing about this problem was finding a test vector @@ -172,8 +172,8 @@ So, via the same timestamp calculator, it was seeded at Sun Jul 30 2023 #### 3.23 A Mersenne Twister outputs elements of its internal state transformed -by a tempering map. The state is perturbed every 624 iterations (the -"twist"), but if we have all 624 outputs generated from a constant +by a linear tempering map. The state is perturbed every 624 iterations +(the "twist"), but if we have all 624 outputs generated from a constant internal state, that state can be recovered by running the outputs through the inverse tempering map. @@ -187,7 +187,7 @@ expressed by: e3 = ls t c e4 = rs l -and the inverse transform by: +and its inverse by: untemper :: Word32 -> Word32 untemper = n1 . n2 . n3 . n4 where @@ -196,7 +196,8 @@ and the inverse transform by: n3 = lsinv t c n4 = rsinv l -given the following salad of internal functions: +given the following salad of internal functions (you either know how to invert +an xorshift operation or you don't): ls :: Word32 -> Word32 -> Word32 -> Word32 ls s m a = a `B.xor` (B.shiftL a (fi s) .&. m) @@ -235,11 +236,83 @@ untemper them to recover the internal state for those 624 iterations any intermediate twists of the internal state): > let gen = seed 42 - > let (bs, g) = bytes 624 gen + > let (bs, g) = tap 624 gen > let cloned = Gen 624 (VU.fromList . fmap untemper $ bs) - > fst (bytes 3 g) + > fst (tap 3 g) [108880612,791707097,4134543476] - > fst (bytes 3 cloned) + > fst (tap 3 cloned) [108880612,791707097,4134543476] +As stated in sec 1.6 of the original Mersenne Twister paper, the key +to hardening the PRNG is to pass the outputs through a secure hash +function. + +#### 3.24 + +The first challenge here is to recover the stream cipher's seed from +some ciphertext, given a (mostly-) known plaintext. The issue is that +at 16 bits the seed is tiny, and so it can be easily be brute-forced by +just iterating through the possible word values: + + mtCipherAttack :: BS.ByteString -> Word16 + mtCipherAttack cip = loop 0 where + l = BS.length cip + t = BS.replicate 14 65 + loop j + | j > (maxBound :: Word16) = error "impossible seed" + | otherwise = + let g = MT.seed (fromIntegral j) + bs = keystream l g + pt = BS.drop (l - 14) (bs `CU.fixedXor` cip) + in if pt == t + then j + else loop (succ j) + +Running it on some ciphertext I created reveals the seed used in a +minute or two: + + > B16.encodeBase16 ciphertext + "df2c20f5025fed9e86a986e47d8bee063213afc1" + > mtCipherAttack ciphertext + 50000 + +The token seeded by system time is also trivial to crack, since we can +just generate a seed from the current time and check the result directly: + + pwntToken :: IO T.Text + pwntToken = do + s <- fmap (fromIntegral . TS.systemSeconds) TS.getSystemTime + let g = MT.seed s + pure $ B64.encodeBase64 (keystream 16 g) + + notPwntToken :: IO T.Text + notPwntToken = do + g <- MWC.createSystemRandom + bs <- fmap BS.pack $ replicateM 16 (MWC.uniformR (32, 126) g) + pure $ B64.encodeBase64 bs + + isPwnt :: T.Text -> IO Bool + isPwnt token = do + s <- fmap (fromIntegral . TS.systemSeconds) TS.getSystemTime + let g = MT.seed s + ks = keystream 16 g + pure $ token == B64.encodeBase64 ks + +(N.b., 'notPwntToken' uses /dev/random or /dev/urandom to generate a +seed, instead of the system time.) + +Some examples: + + > pwntToken + "2Pi2LO0cn3XXyw1xwrLlHQ==" + > pwntToken + "WqPvmtGTfc3QkhVs78uOqQ==" + > notPwntToken + "V0codSgtXyNvLSJ4XjNyNQ==" + > notPwntToken + "STA3ZnxVe1tQW0Q4TF0pbg==" + > pwntToken >>= isPwnt + True + > notPwntToken >>= isPwnt + False diff --git a/lib/Cryptopals/Stream/Attacks.hs b/lib/Cryptopals/Stream/Attacks.hs @@ -0,0 +1,74 @@ +module Cryptopals.Stream.Attacks where + +import Control.Monad +import qualified Control.Monad.ST as ST +import qualified Data.Binary.Put as BP +import qualified Data.ByteString as BS +import qualified Data.ByteString.Base64 as B64 +import qualified Data.ByteString.Lazy as BSL +import qualified Data.Text as T +import qualified Data.Time.Clock.System as TS +import qualified Cryptopals.Stream.RNG.MT19937 as MT +import qualified Cryptopals.Util as CU +import GHC.Word (Word16, Word8) +import qualified System.Random.MWC as MWC + +keystream :: Int -> MT.Gen -> BS.ByteString +keystream nb g = + let l = nb `quot` 4 + nb `rem` 4 + ws = fst (MT.tap l g) + in BS.take nb . BSL.toStrict . BP.runPut $ loop ws + where + loop bs = case bs of + [] -> pure () + (h:t) -> do + BP.putWord32le h + loop t + +encryptMT19937 :: Word16 -> BS.ByteString -> BS.ByteString +encryptMT19937 s pt = pt `CU.fixedXor` bs where + g = MT.seed (fromIntegral s) + bs = keystream (BS.length pt) g + +decryptMT19937 :: Word16 -> BS.ByteString -> BS.ByteString +decryptMT19937 = encryptMT19937 + +ciphertext :: BS.ByteString +ciphertext = encryptMT19937 50000 $ ST.runST $ do + g <- MWC.create + n <- MWC.uniformR (1, 10) g + bs <- fmap BS.pack $ replicateM n (MWC.uniformR (32, 126) g) + pure (bs <> BS.replicate 14 65) + +mtCipherAttack :: BS.ByteString -> Word16 +mtCipherAttack cip = loop 0 where + l = BS.length cip + t = BS.replicate 14 65 + loop j + | j > (maxBound :: Word16) = error "impossible seed" + | otherwise = + let g = MT.seed (fromIntegral j) + bs = keystream l g + pt = BS.drop (l - 14) (bs `CU.fixedXor` cip) + in if pt == t + then j + else loop (succ j) + +pwntToken :: IO T.Text +pwntToken = do + s <- fmap (fromIntegral . TS.systemSeconds) TS.getSystemTime + let g = MT.seed s + pure $ B64.encodeBase64 (keystream 16 g) + +notPwntToken :: IO T.Text +notPwntToken = do + g <- MWC.createSystemRandom + bs <- fmap BS.pack $ replicateM 16 (MWC.uniformR (32, 126) g) + pure $ B64.encodeBase64 bs + +isPwnt :: T.Text -> IO Bool +isPwnt token = do + s <- fmap (fromIntegral . TS.systemSeconds) TS.getSystemTime + let g = MT.seed s + ks = keystream 16 g + pure $ token == B64.encodeBase64 ks diff --git a/lib/Cryptopals/Stream/RNG/MT19937.hs b/lib/Cryptopals/Stream/RNG/MT19937.hs @@ -2,10 +2,9 @@ module Cryptopals.Stream.RNG.MT19937 ( Gen , seed , extract - , bytes + , tap - , temper - , untemper + , clone ) where import qualified Control.Monad.ST as ST @@ -48,8 +47,8 @@ data Gen = Gen !Word32 !(VU.Vector Word32) instance Show Gen where show Gen {} = "<MT19937.Gen>" -bytes :: Int -> Gen -> ([Word32], Gen) -bytes = loop mempty where +tap :: Int -> Gen -> ([Word32], Gen) +tap = loop mempty where loop !acc j gen | j == 0 = (reverse acc, gen) | otherwise =