cryptopals

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

commit 5e5e1796203eebf3ff4ff65306818f0051ede424
parent 369b82e1624fef10f6146fed2c280eb12ae90339
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 18 Aug 2023 22:04:25 -0230

Add 5.38.

Diffstat:
Mcryptopals.cabal | 16++++++++++++++++
Mdocs/s5.md | 55++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Alib/Cryptopals/SRP/Simple.hs | 375+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/OfflineDictionaryAttack.hs | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 511 insertions(+), 1 deletion(-)

diff --git a/cryptopals.cabal b/cryptopals.cabal @@ -33,6 +33,7 @@ library , Cryptopals.MAC , Cryptopals.MAC.Attacks , Cryptopals.SRP + , Cryptopals.SRP.Simple , Cryptopals.Stream.Attacks , Cryptopals.Stream.RNG , Cryptopals.Stream.RNG.MT19937 @@ -190,3 +191,18 @@ executable mt19937 , optparse-applicative , text +executable offline-dictionary-attack + main-is: OfflineDictionaryAttack.hs + ghc-options: -Wall -O2 + default-language: Haskell2010 + hs-source-dirs: src + build-depends: + base + , base16 + , binary + , bytestring + , cryptopals + , optparse-applicative + , text + , unordered-containers + diff --git a/docs/s5.md b/docs/s5.md @@ -280,7 +280,7 @@ its public key, then the server will compute S = 0 as its shared secret. Whoops! The client can then just pass along the appropriate MAC to authenticate. -Example, with the client using the 'srpZero' protocol and 'authZero' +Example, with the client using the 'srpzero' protocol and 'authZero' initial action: -- GHCi instance one @@ -297,3 +297,56 @@ initial action: (cryptopals) server: received MAC 5xO9hEUJOTX5EIU+DmYV0QOs1L1oVp3fphREooN/8L4= (cryptopals) server: OK +#### 5.38 + +The simplified protocol can be run with the 'server' and 'client' +functions in Cryptopals.SRP.Simple. + +For the MITM attack, the idea is that, posing as the server, Mallory has +control over the parameters 'salt', 'b', 'B', and 'u', but doesn't know +anything to do with 'x', and so has to guess at that. + +If Mallory supplies salt = mempty, B = g mod n, and u = 1, then the +client will compute: + + S = g ^ (a + x) mod n + +and forward him MAC = HMAC-SHA256(SHA256(S), mempty). Duly supplied with +the client's public key ('A') and MAC, and using a trivial b = 1 as a +secret key, Mallory can guess x = SHA256(password) to compute: + + S' = (A v) mod n + = (A g ^ x) mod n + +and then check if HMAC-SHA256(SHA256(S'), mempty) = MAC. If it verifies, +then he knows the password. + +To not make this too annoying, I'll draw the password to be cracked from +/usr/share/dict/words. Once Mallory provides the public key and MAC +from the client, we'll generate our dictionary and check if the MAC is +present in the keyspace using a compiled, optimized binary. + +Here's a run of the MITM protocol (`mallory "3000" mitm` and `client +"3000" srpsimple auth`): + + (cryptopals) mallory: LiSteNiNG.. + (cryptopals) client: session established + (cryptopals) client: sending authentication request + (cryptopals) mallory: rECeIvEd aUTheNtICaTioN ReQUesT fOr l33th4x0r@hotmail.com + (cryptopals) mallory: wiTh PuBLiC kEy 4992116105881074929461308645820763003777270799868975573291 + (cryptopals) mallory: aCKiNg AuTheNTicAtIon ReQueST FOr l33th4x0r@hotmail.com + (cryptopals) client: received authentication request ack + (cryptopals) client: sending MAC f20ac41224b4054d2f89a7c319ed5bf3f8bb68cf4169f620f45e49acb4dd179c + (cryptopals) mallory: rECeIvEd MAC f20ac41224b4054d2f89a7c319ed5bf3f8bb68cf4169f620f45e49acb4dd179c + (cryptopals) mallory: USiNg PaRaMeTeRs 4992116105881074929461308645820763003777270799868975573291 aNd f20ac41224b4054d2f89a7c319ed5bf3f8bb68cf4169f620f45e49acb4dd179c + (cryptopals) mallory: GoINg ofFLinE.. + +Now taking those parameters to the `offline-dictionary-attack` binary we get +the result pretty quickly: + + $ PK="4992116105881074929461308645820763003777270799868975573291" + $ MAC="f20ac41224b4054d2f89a7c319ed5bf3f8bb68cf4169f620f45e49acb4dd179c" + $ offline-dictionary-attack "$PK" "$MAC" + (cryptopals) success + (cryptopals) password: omniana + diff --git a/lib/Cryptopals/SRP/Simple.hs b/lib/Cryptopals/SRP/Simple.hs @@ -0,0 +1,375 @@ +module Cryptopals.SRP.Simple where + +import Control.Concurrent (threadDelay) +import Control.Monad +import Control.Monad.Primitive +import Control.Monad.IO.Class +import Control.Monad.Trans.Class +import Control.Monad.Trans.Reader +import Control.Monad.Trans.State +import qualified Cryptopals.Digest.Pure.SHA as CS +import qualified Cryptopals.DH as DH (modexp) +import qualified Data.Binary as DB +import qualified Data.Bits as B +import qualified Data.ByteString as BS +import qualified Data.ByteString.Base16 as B16 +import qualified Data.ByteString.Lazy as BL +import qualified Data.ByteString.Lazy.Char8 as BL8 +import qualified Data.HashMap.Lazy as HML +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Text.IO as TIO +import GHC.Generics (Generic) +import GHC.Word (Word64) +import Numeric.Natural +import Pipes +import qualified Pipes.Binary as PB +import qualified Pipes.Network as PN +import qualified System.Exit as SE +import qualified System.Random.MWC as MWC + +data Word128 = Word128 !Word64 !Word64 + deriving (Eq, Show, Generic) + +instance DB.Binary Word128 + +genWord128 :: PrimMonad m => MWC.Gen (PrimState m) -> m Word128 +genWord128 gen = Word128 <$> MWC.uniform gen <*> MWC.uniform gen + +word128toNat :: Word128 -> Natural +word128toNat w = foldr alg 0 . BS.unpack . BL.toStrict $ bs where + bs = DB.encode w + alg b a = a `B.shiftL` 8 B..|. fromIntegral b + +-- common parameters +data Env = Env { + en :: Natural + , eg :: Natural + , ek :: Natural + , ei :: BS.ByteString + , ep :: BS.ByteString + } deriving (Eq, Show, Generic) + +defaultEnv :: Env +defaultEnv = Env { + en = p192 + , eg = 2 + , ek = 3 + , ei = "l33th4x0r@hotmail.com" + , ep = "hunter2" + } + +genPassword :: MWC.Gen RealWorld -> IO BS.ByteString +genPassword gen = do + idx <- MWC.uniformR (0, 235885) gen + dict <- BL8.readFile "/usr/share/dict/words" + let ls = BL8.lines dict + pure . BL.toStrict $ ls !! idx + +initEnv :: MWC.Gen RealWorld -> IO Env +initEnv gen = do + ep <- genPassword gen + pure Env { + en = p192 + , eg = 2 + , ek = 3 + , ei = "l33th4x0r@hotmail.com" + , .. + } + +instance DB.Binary Env + +data Command = + Auth BS.ByteString Natural + | AckAuth BS.ByteString Natural Natural + | SendMAC BS.ByteString + | End + deriving (Eq, Show, Generic) + +instance DB.Binary Command + +-- generic state +data Sesh = Sesh { + shost :: T.Text + , ssalt :: Maybe BS.ByteString + , skey :: Natural + , sourpub :: Natural + , sherpub :: Maybe Natural + , sv :: Maybe Natural + , su :: Maybe Natural + , sgen :: IO (MWC.Gen RealWorld) + } + +instance Show Sesh where + show Sesh {..} = mconcat [ + "Sesh {\n" + , " shost = " <> show shost <> "\n" + , " ssalt = " <> show ssalt <> "\n" + , " skey = " <> show skey <> "\n" + , " sourpub = " <> show sourpub <> "\n" + , " sherpub = " <> show sherpub <> "\n" + , " sv = " <> show sv <> "\n" + , " su = " <> show su <> "\n" + , " sgen = <MWC.Gen>\n" + , "}" + ] + +type SRP m = StateT Sesh (ReaderT Env m) + +server + :: (DB.Binary b, DB.Binary c) + => PN.ServiceName + -> PN.Protocol (SRP IO) b c + -> IO () +server port eval = PN.serve "localhost" port $ \(sock, _) -> do + gen <- MWC.createSystemRandom + env <- initEnv gen + sesh <- initServer env gen + blog "server" "listening.." + let saction = runEffect (PN.session sock eval) + void $ runReaderT (evalStateT saction sesh) defaultEnv + +mallory + :: (DB.Binary b, DB.Binary c) + => PN.ServiceName + -> PN.Protocol (SRP IO) b c + -> IO () +mallory port eval = PN.serve "localhost" port $ \(sock, _) -> do + gen <- MWC.createSystemRandom + env <- initEnv gen + sesh <- initMallory env gen + blog "mallory" "LiSteNiNG.." + let saction = runEffect (PN.session sock eval) + void $ runReaderT (evalStateT saction sesh) defaultEnv + +client + :: (DB.Binary b, DB.Binary c) + => PN.ServiceName + -> PN.Protocol (SRP IO) b c + -> SRP IO Command + -> IO () +client port eval knit = PN.connect "localhost" port $ \(sock, _) -> do + gen <- MWC.createSystemRandom + env <- initEnv gen + sesh <- initClient defaultEnv gen + blog "client" "session established" + + (cmd, nex) <- runReaderT (runStateT knit sesh) defaultEnv + + runEffect $ + PB.encode cmd + >-> PN.toSocket sock + + let saction = runEffect (PN.session sock eval) + void $ runReaderT (runStateT saction nex) defaultEnv + +auth :: SRP IO Command +auth = do + Env {..} <- lift ask + pub <- gets sourpub + slog "sending authentication request" + pure (Auth ei pub) + +-- basic log +blog :: T.Text -> T.Text -> IO () +blog host msg = do + TIO.putStrLn $ "(cryptopals) " <> host <> ": " <> msg + suspense + +-- session log +slog :: MonadIO m => T.Text -> StateT Sesh m () +slog msg = do + host <- gets shost + liftIO . TIO.putStrLn $ "(cryptopals) " <> host <> ": " <> msg + liftIO suspense + +-- dramatic effect +suspense :: IO () +suspense = threadDelay 1000000 + +-- 2 ^ 192 - 2 ^ 64 - 1 +p192 :: Natural +p192 = 6277101735386680763835789423207666416083908700390324961279 + +initServer :: Env -> MWC.Gen RealWorld -> IO Sesh +initServer Env {..} gen = do + skey <- MWC.uniformRM (1, en - 1) gen + u <- word128toNat <$> genWord128 gen + salt <- DB.encode <$> (MWC.uniform gen :: IO Word64) + let xH = CS.sha256 (salt <> BL.fromStrict ep) + x = fromIntegral (CS.integerDigest xH) + v = DH.modexp eg x en + strsalt = BL.toStrict salt + sourpub = DH.modexp eg skey en + pure Sesh { + sgen = pure gen + , ssalt = pure strsalt + , sv = pure v + , su = pure u + , sherpub = Nothing + , shost = "server" + , .. + } + +initMallory :: Env -> MWC.Gen RealWorld -> IO Sesh +initMallory Env {..} gen = do + let skey = 1 + u = 1 + sourpub = 2 + pure Sesh { + sgen = pure gen + , ssalt = pure mempty + , sv = Nothing + , su = pure u + , sherpub = Nothing + , shost = "mallory" + , .. + } + +initClient :: Env -> MWC.Gen RealWorld -> IO Sesh +initClient Env {..} gen = do + skey <- MWC.uniformRM (1, en - 1) gen + let sourpub = DH.modexp eg skey en + pure Sesh { + sgen = pure gen + , sherpub = Nothing + , ssalt = Nothing + , sv = Nothing + , su = Nothing + , shost = "client" + , .. + } + +-- simple secure remote password protocol +srpsimple :: MonadIO m => PN.Protocol (SRP m) Command Command +srpsimple cmd = do + Env {..} <- lift ask + case cmd of + Auth i herpub -> do + let li = TE.decodeLatin1 i + slog $ "received authentication request for " <> li + if i /= ei + then do + slog $ "unknown user " <> li + pure End + else do + sesh@Sesh {..} <- get + put sesh { + sherpub = Just herpub + } + case (,) <$> ssalt <*> su of + Nothing -> do + slog "missing required parameters" + pure End + Just (salt, u) -> do + slog $ "acking authentication request for " <> li + pure (AckAuth salt sourpub u) + + AckAuth salt herpub u -> do + slog "received authentication request ack" + sesh@Sesh {..} <- get + put sesh { + ssalt = Just salt + , sherpub = Just herpub + , su = Just u + } + let x = fromIntegral + . CS.integerDigest + . CS.sha256 + $ BL.fromStrict (salt <> ep) + s = DH.modexp herpub (skey + u * x) en + k = CS.bytestringDigest + . CS.sha256 + . DB.encode + $ s + let mac = BL.toStrict + . CS.bytestringDigest + $ CS.hmacSha256 k (BL.fromStrict salt) + slog $ "sending MAC " <> B16.encodeBase16 mac + pure (SendMAC mac) + + SendMAC mac -> do + slog $ "received MAC " <> B16.encodeBase16 mac + sesh@Sesh {..} <- get + case (,,,) <$> ssalt <*> sv <*> sherpub <*> su of + Nothing -> do + slog "missing required parameters" + pure End + Just (salt, v, herpub, u) -> do + let s = DH.modexp (herpub * DH.modexp v u en) skey en + k = CS.bytestringDigest + . CS.sha256 + . DB.encode + $ s + hmac = BL.toStrict + . CS.bytestringDigest + $ CS.hmacSha256 k (BL.fromStrict salt) + if hmac == mac + then do + slog "OK" + pure End + else do + slog "couldn't verify MAC" + pure End + + End -> do + slog "ending session" + liftIO SE.exitSuccess -- XX close the socket + +-- MITM on simple secure remote password protocol +mitm :: MonadIO m => PN.Protocol (SRP m) Command Command +mitm cmd = do + Env {..} <- lift ask + case cmd of + Auth i herpub -> do + let li = TE.decodeLatin1 i + slog $ "rECeIvEd aUTheNtICaTioN ReQUesT fOr " <> li + slog $ "wiTh PuBLiC kEy " <> (T.pack . show) herpub + if i /= ei + then do + slog $ "unknown user " <> li + pure End + else do + sesh@Sesh {..} <- get + put sesh { + sherpub = Just herpub + } + case (,) <$> ssalt <*> su of + Nothing -> do + slog "missing required parameters" + pure End + Just (salt, u) -> do + slog $ "aCKiNg AuTheNTicAtIon ReQueST FOr " <> li + pure (AckAuth salt sourpub u) + + SendMAC mac -> do + slog $ "rECeIvEd MAC " <> B16.encodeBase16 mac + sesh@Sesh {..} <- get + + case sherpub of + Nothing -> do + slog "missing required parameters" + pure End + Just herpub -> do + slog $ "USiNg PaRaMeTeRs " <> (T.pack . show) herpub + <> " aNd " <> B16.encodeBase16 mac + slog "GoINg ofFLinE.." + pure End + + _ -> srpsimple cmd + +populate + :: MonadIO m + => Natural + -> ReaderT Env m (HML.HashMap BL.ByteString BL.ByteString) +populate herpub = do + Env {..} <- ask + dict <- liftIO $ BL8.readFile "/usr/share/dict/words" + let ls = BL8.lines dict + ns = fmap (fromIntegral . CS.integerDigest . CS.sha256) ls :: [Natural] + ss = fmap (\x -> herpub * DH.modexp eg x en) ns + hs = fmap (CS.bytestringDigest . CS.sha256 . DB.encode) ss + ms = fmap (\s -> CS.bytestringDigest (CS.hmacSha256 s mempty)) hs + + pure . HML.fromList $ zip ms ls + diff --git a/src/OfflineDictionaryAttack.hs b/src/OfflineDictionaryAttack.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Main where + +import Cryptopals.SRP.Simple (Env(..), defaultEnv) +import qualified Cryptopals.Digest.Pure.SHA as CS +import qualified Cryptopals.DH as DH +import qualified Data.Binary as DB +import qualified Data.ByteString.Base16 as B16 +import qualified Data.ByteString.Char8 as B8 +import qualified Data.ByteString.Lazy as BL +import qualified Data.ByteString.Lazy.Char8 as BL8 +import qualified Data.HashMap.Lazy as HML +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Text.IO as TIO +import Numeric.Natural +import qualified Options.Applicative as O + +populate :: Natural -> IO (HML.HashMap BL.ByteString BL.ByteString) +populate herpub = do + let Env {..} = defaultEnv + dict <- BL8.readFile "/usr/share/dict/words" + let derive x = ((herpub `mod` en) * (DH.modexp eg x en)) `mod` en + + let ls = BL8.lines dict + ns = fmap (fromIntegral . CS.integerDigest . CS.sha256) ls :: [Natural] + ss = fmap derive ns + hs = fmap (CS.bytestringDigest . CS.sha256 . DB.encode) ss + ms = fmap (\s -> CS.bytestringDigest (CS.hmacSha256 s mempty)) hs + + pure . HML.fromList $ zip ms ls + +data Args = Args { + argsNat :: Natural + , argsMAC :: T.Text + } + +ops :: O.Parser Args +ops = Args + <$> O.argument O.auto (O.metavar "PUBLICKEY") + <*> O.argument O.str (O.metavar "MAC") + +crack :: Args -> IO () +crack Args {..} = do + let mac = BL.fromStrict . B16.decodeBase16Lenient $ TE.encodeUtf8 argsMAC + dict <- populate argsNat + case HML.lookup mac dict of + Nothing -> TIO.putStrLn "(cryptopals) couldn't crack password" + Just pw -> do + let s = BL.toStrict pw + B8.putStrLn "(cryptopals) success" + B8.putStrLn $ "(cryptopals) password: " <> s + +main :: IO () +main = do + let pars = O.info (O.helper <*> ops) $ + O.fullDesc + <> O.progDesc "perform an offline dictionary attack" + <> O.header "offline-dictionary-attack" + + args <- O.execParser pars + + crack args +