Working CryptoPals Set 2 challenges in F#
My previous post covered approaches to challenges in Set 1 of CryptoPals challenges. This post will be more of the same for Set 2.
The code is available on GitHub.
Challenge 9
Implement PKCS#7 padding
let pad bytes length =
let diff = length - Array.length bytes
let pad = Seq.initInfinite (fun _ -> byte diff)
Seq.append bytes pad |> Seq.truncate length
let unpadded = "YELLOW SUBMARINE" |> stringToBytes
let padded = pad unpadded 20
Since unpadded
is 16 bytes, 4 less than the desired length of 20, we pad the end of it with byte values equal to 4. If unpadded
was 15 bytes, we’d be 5 short and would need to paid with byte values of 5.
Challenge 10
Implement CBC mode
We’ll need to implement CBC mode by hand using the ECB approach from the previous set. CBC mode differs from ECB in that the ciphertext output of each block is XOR’d with the plaintext input of the next block. Since the first plaintext block to be encrypted has no prior ciphertext block, an Initialization Vector (IV) used to bootstrap the process.
open System.Security.Cryptography
let cryptAlg key =
let aes = new AesManaged()
aes.Mode <- CipherMode.ECB
aes.Key <- key
aes.Padding <- PaddingMode.None
aes
let blockSize = 16
let encrypt (bytes: byte[]) key =
use aes = cryptAlg key
let encryptor = aes.CreateEncryptor(aes.Key, aes.IV)
[| for block in bytes |> Array.chunkBySize blockSize do
let padded = pad block blockSize |> Array.ofSeq
let output = Array.create padded.Length 0uy
encryptor.TransformBlock(padded, 0, padded.Length, output, 0) |> ignore
yield! output |]
encrypt (stringToBytes "YELLOW SUBMARINE") (stringToBytes "YELLOW SUBMARINE")
Above, we’ve defined the plain EBC encrypt
function again. This will be used in our CBC encrypt function below. Notice prevBlock
is used to store the IV initially, and then the ciphertext of each successive block, after it’s XOR’d with the plaintext input.
let encryptCBCWithIV (iv: byte[]) (bytes: byte[]) key =
let mutable prevBlock = iv
[| for block in bytes |> Array.chunkBySize blockSize do
let padded = pad block blockSize |> Array.ofSeq
let xorBytes = xorBytes padded prevBlock |> Array.ofSeq
let blockCipher = encrypt xorBytes key
prevBlock <- blockCipher
yield! blockCipher |]
And some very similar functions for decrypting.
let decrypt (aesBytes: byte[]) key =
use aes = cryptAlg key
let decryptor = aes.CreateDecryptor(aes.Key, aes.IV)
let output = Array.create aesBytes.Length 0uy
decryptor.TransformBlock(aesBytes, 0, aesBytes.Length, output, 0) |> ignore
output
let decryptCBC (bytes: byte[]) key =
let mutable prevBlock : byte[] = Array.create blockSize 0uy
[| for block in bytes |> Array.chunkBySize blockSize do
let padded = pad block blockSize |> Array.ofSeq
let blockCipher = decrypt padded key
let xorBytes = xorBytes blockCipher prevBlock |> Array.ofSeq
prevBlock <- padded
yield! xorBytes |]
Now we should be able to decrypt the challenge text using CBC mode. I think it’s just more Vanilla Ice lyrics.
let cbcBytes = readBase64Bytes (__SOURCE_DIRECTORY__ + "/Challenge10.txt")
decryptCBC cbcBytes (stringToBytes "YELLOW SUBMARINE") |> bytesToString |> printfn "%s"
Challenge 11
An ECB/CBC detection oracle
This one asks you to create an encrypt function that pads the plaintext and randomly chooses ECB/CBC mode. Then write a function to determine which mode it’s using. The idea here is to use big empty plaintext buffers to cause ECB mode to produce identical cipher blocks, then look for those.
let rnd = Random()
let coinflip () = rnd.NextDouble() < 0.5
let genAesKey () =
let bytes = Array.create 16 0uy
rnd.NextBytes bytes
bytes
let crazyCrypt bytes =
let leftPad = Array.create (rnd.Next(5,10)) 0uy
let rightPad = Array.create (rnd.Next(5,10)) 0uy
let bytes = Array.concat [| leftPad; bytes; rightPad |]
let rndKey = genAesKey ()
if coinflip ()
then encrypt bytes rndKey
else
let iv = Array.create blockSize 0uy
rnd.NextBytes iv
encryptCBCIV iv bytes rndKey
let isECB encrypt =
let bytes = encrypt (Array.create (blockSize * 6) 0uy) // use zeroed buffers to expose ECB repetition
let blocks = bytes |> Array.chunkBySize blockSize
blocks.Length > (blocks |> Array.distinct |> Array.length) // if there were duplicate blocks, ECB was used
isECB crazyCrypt
Challenge 12
Byte-at-a-time ECB decryption
I won’t repeat the official description, but you should read it. Essentially you want an encrypt function like AES-128-ECB(your-string || unknown-string, random-key)
where unknown-string
is used inside the function but not known to the attacker.
It turns out: you can decrypt
unknown-string
with repeated calls to the oracle function!
module Oracle12 =
let append = Convert.FromBase64String "Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK"
let rndKey = genAesKey ()
let encrypt bytes = encrypt (Array.append bytes append) rndKey
The first thing we need to know is the block size produced by the function. We kinda already know this, but it’s important to figure it out manually. This is a pretty goofy way of finding it:
let growingKeys = [for i in 1 .. 16 do yield Array.create i 0uy]
let lenDiffs = // look for ciphertext size diffs given diff inputs
growingKeys
|> List.map (Oracle12.encrypt >> Array.length)
|> List.pairwise
|> List.map (fun (x,y) -> abs(x-y))
|> List.where (fun x -> x > 0)
|> List.distinct
Per instructions, we should check the mode of the encrypt
function even though we know what it is.
isECB Oracle12.encrypt
Now for the interesting part. We know the block size, and we know that the unknown-string
gets appended to your-string
in the encrypt function. What would happen if we sent a plaintext input that was one shorter than the block size? The first character of secret string would be appended to the end of the first block.
Using a short buffer we can ensure the last character of the plaintext input will be the first character of the secret string—we can force it to be the last character of an otherwise “empty” block—so we can now brute force the first block with every byte, and compare it to the encrypted block of the “short” buffer. If the two blocks match, we’ve identified the first character of the secret string!
// how many blocks of secret text are there? find out by encrypting an empty buffer
let numBlocks = (Oracle12.encrypt [||] |> Array.length) / blockSize
let getBlock blockNum = Array.chunkBySize blockSize >> Array.tryItem blockNum
let secret = ResizeArray()
for blockNum in 0 .. numBlocks - 1 do
for blockOffset in 0 .. blockSize - 1 do
let shorty = Array.create (blockSize - (blockOffset + 1)) 0uy
let bruteCryptBlocks = seq {
for b in Byte.MinValue .. Byte.MaxValue do
let plaintext = Array.concat [| shorty; secret.ToArray(); [|b|] |]
let crypted = Oracle12.encrypt plaintext
yield b, getBlock blockNum crypted }
let shortyCryptBlock = shorty |> Oracle12.encrypt |> getBlock blockNum
bruteCryptBlocks
|> Seq.tryPick (fun (guessByte, cipherBlock) ->
if cipherBlock = shortyCryptBlock
then Some guessByte
else None)
|> Option.iter (secret.Add)
secret.ToArray() |> bytesToString |> printfn "%A"
I took an iterative approach to this problem, maybe easier to understand than purely functional. If a brute forced block matches the ciphertext of the short block, we have a match. For each offset of the block size, we shorten the input plaintext and accumulate the guessed key characters—and this is repeated for each block. Each block yields 16 characters, except when padded.
More to come
Check back later for updates.