Cryptopals Set 2
Setup
Mix.install([
{:kino, "~> 0.5.2"}
])
:ok
9. Implement PKCS#7 padding
A block cipher transforms a fixed-sized block (usually 8 or 16 bytes) of plaintext into ciphertext. But we almost never want to transform a single block; we encrypt irregularly-sized messages.
One way we account for irregularly-sized messages is by padding, creating a plaintext that is an even multiple of the blocksize. The most popular padding scheme is called PKCS#7.
So: pad any block to a specific block length, by appending the number of bytes of padding to the end of the block. For instance,
"YELLOW SUBMARINE"
… padded to 20 bytes would be:
"YELLOW SUBMARINE\x04\x04\x04\x04"
defmodule Cryptopals.Set2.Challenge9 do
def pkcs7(plaintext, blocksize) do
padding = blocksize - rem(byte_size(plaintext), blocksize)
plaintext <> :binary.copy(<>, padding)
end
end
{:module, Cryptopals.Set2.Challenge9, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:pkcs7, 2}}
10. Implement CBC mode
CBC mode is a block cipher mode that allows us to encrypt irregularly-sized messages, despite the fact that a block cipher natively only transforms individual blocks.
In CBC mode, each ciphertext block is added to the next plaintext block before the next call to the cipher core.
The first plaintext block, which has no associated previous ciphertext block, is added to a “fake 0th ciphertext block” called the initialization vector, or IV.
Implement CBC mode by hand by taking the ECB function you wrote earlier, making it encrypt instead of decrypt (verify this by decrypting whatever you encrypt to test), and using your XOR function from the previous exercise to combine them.
The file here is intelligible (somewhat) when CBC decrypted against “YELLOW SUBMARINE” with an IV of all ASCII 0 (\x00\x00\x00 &c)
Don’t cheat. Do not use OpenSSL’s CBC code to do CBC mode, even to verify your results. What’s the point of even doing this stuff if you aren’t going to learn from it?
defmodule Cryptopals.Set2.Challenge10 do
import Cryptopals.Set2.Challenge9, only: [pkcs7: 2]
use Bitwise
@blocksize 16
def xor(a, b) when is_bitstring(a) and is_bitstring(b) do
bxor(:binary.decode_unsigned(a), :binary.decode_unsigned(b))
|> :binary.encode_unsigned()
end
def aes_cbc_encrypt(plaintext, key, iv) do
aes_cbc_encrypt_helper(pkcs7(plaintext, @blocksize), key, iv)
end
def aes_cbc_encrypt_helper(<<>>, _key, _prev_block) do
<<>>
end
def aes_cbc_encrypt_helper(
<>,
key,
prev_block
) do
block =
plaintext_block
|> xor(prev_block)
ciphertext_block = :crypto.crypto_one_time(:aes_128_ecb, key, block, encrypt: true)
ciphertext_block <> aes_cbc_encrypt_helper(rst, key, ciphertext_block)
end
def aes_cbc_decrypt(<<>>, _key, _prev_block) do
<<>>
end
# Note we're using strip_padding from a future challenge
import Cryptopals.Set2.Challenge15, only: [strip_padding: 1]
def aes_cbc_decrypt(
<>,
key,
prev_block
) do
:crypto.crypto_one_time(:aes_128_ecb, key, ciphertext_block, encrypt: false)
|> xor(prev_block)
|> strip_padding()
end
def aes_cbc_decrypt(
<>,
key,
prev_block
) do
plaintext_block =
:crypto.crypto_one_time(:aes_128_ecb, key, ciphertext_block, encrypt: false)
|> xor(prev_block)
plaintext_block <> aes_cbc_decrypt(rst, key, ciphertext_block)
end
end
{:module, Cryptopals.Set2.Challenge10, <<70, 79, 82, 49, 0, 0, 12, ...>>, {:aes_cbc_decrypt, 3}}
challenge10_input = Kino.Input.textarea("Challenge 10")
challenge10_ciphertext =
Kino.Input.read(challenge10_input)
|> String.replace("\n", "")
|> Base.decode64!()
key = "YELLOW SUBMARINE"
iv = :binary.copy("\x00", 16)
Cryptopals.Set2.Challenge10.aes_cbc_decrypt(challenge10_ciphertext, key, iv)
|> IO.puts()
I'm back and I'm ringin' the bell
A rockin' on the mike while the fly girls yell
In ecstasy in the back of me
Well that's my DJ Deshay cuttin' all them Z's
Hittin' hard and the girlies goin' crazy
Vanilla's on the mike, man I'm not lazy.
I'm lettin' my drug kick in
It controls my mouth and I begin
To just let it flow, let my concepts go
My posse's to the side yellin', Go Vanilla Go!
Smooth 'cause that's the way I will be
And if you don't give a damn, then
Why you starin' at me
So get off 'cause I control the stage
There's no dissin' allowed
I'm in my own phase
The girlies sa y they love me and that is ok
And I can dance better than any kid n' play
Stage 2 -- Yea the one ya' wanna listen to
It's off my head so let the beat play through
So I can funk it up and make it sound good
1-2-3 Yo -- Knock on some wood
For good luck, I like my rhymes atrocious
Supercalafragilisticexpialidocious
I'm an effect and that you can bet
I can take a fly girl and make her wet.
I'm like Samson -- Samson to Delilah
There's no denyin', You can try to hang
But you'll keep tryin' to get my style
Over and over, practice makes perfect
But not if you're a loafer.
You'll get nowhere, no place, no time, no girls
Soon -- Oh my God, homebody, you probably eat
Spaghetti with a spoon! Come on and say it!
VIP. Vanilla Ice yep, yep, I'm comin' hard like a rhino
Intoxicating so you stagger like a wino
So punks stop trying and girl stop cryin'
Vanilla Ice is sellin' and you people are buyin'
'Cause why the freaks are jockin' like Crazy Glue
Movin' and groovin' trying to sing along
All through the ghetto groovin' this here song
Now you're amazed by the VIP posse.
Steppin' so hard like a German Nazi
Startled by the bases hittin' ground
There's no trippin' on mine, I'm just gettin' down
Sparkamatic, I'm hangin' tight like a fanatic
You trapped me once and I thought that
You might have it
So step down and lend me your ear
'89 in my time! You, '90 is my year.
You're weakenin' fast, YO! and I can tell it
Your body's gettin' hot, so, so I can smell it
So don't be mad and don't be sad
'Cause the lyrics belong to ICE, You can call me Dad
You're pitchin' a fit, so step back and endure
Let the witch doctor, Ice, do the dance to cure
So come up close and don't be square
You wanna battle me -- Anytime, anywhere
You thought that I was weak, Boy, you're dead wrong
So come on, everybody and sing this song
Say -- Play that funky music Say, go white boy, go white boy go
play that funky music Go white boy, go white boy, go
Lay down and boogie and play that funky music till you die.
Play that funky music Come on, Come on, let me hear
Play that funky music white boy you say it, say it
Play that funky music A little louder now
Play that funky music, white boy Come on, Come on, Come on
Play that funky music
:ok
11. An ECB/CBC detection oracle
Now that you have ECB and CBC working:
Write a function to generate a random AES key; that’s just 16 random bytes.
Write a function that encrypts data under an unknown key — that is, a function that generates a random key and encrypts under it.
The function should look like:
encryption_oracle(your-input)
=> [MEANINGLESS JIBBER JABBER]
Under the hood, have the function append 5-10 bytes (count chosen randomly) before the plaintext and 5-10 bytes after the plaintext.
Now, have the function choose to encrypt under ECB 1/2 the time, and under CBC the other half (just use random IVs each time for CBC). Use rand(2) to decide which to use.
Detect the block cipher mode the function is using each time. You should end up with a piece of code that, pointed at a block box that might be encrypting ECB or CBC, tells you which one is happening.
defmodule Cryptopals.Set2.Challenge11 do
import Cryptopals.Set2.Challenge10
@blocksize 16
def encryption_oracle(input) do
padded_input = :rand.bytes(Enum.random(5..10)) <> input <> :rand.bytes(Enum.random(5..10))
case Enum.random([:cbc, :ecb]) do
:cbc ->
encrypted =
aes_cbc_encrypt(padded_input, :rand.bytes(@blocksize), :rand.bytes(@blocksize))
{:cbc, encrypted}
:ecb ->
encrypted =
:crypto.crypto_one_time(:aes_128_ecb, :rand.bytes(@blocksize), padded_input,
encrypt: true
)
{:ecb, encrypted}
end
end
# We just generate a text with lots of repeating 16-byte blocks and compare to see if two 16-byte sequences (after we drop some padding) are the same
# input = :binary.copy("YELLOW SUBMARINE", 20)
def detect_mode(output) do
<<_::binary-size(20), a::binary-size(16), b::binary-size(16), _::binary>> = output
if a == b do
:ecb
else
:cbc
end
end
end
{:module, Cryptopals.Set2.Challenge11, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:detect_mode, 1}}
12. Byte-at-a-time ECB decryption (Simple)
Copy your oracle function to a new function that encrypts buffers under ECB mode using a consistent but unknown key (for instance, assign a single random key, once, to a global variable).
Now take that same function and have it append to the plaintext, BEFORE ENCRYPTING, the following string:
Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg
aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq
dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg
YnkK
Spoiler alert.
Do not decode this string now. Don’t do it.
Base64 decode the string before appending it. Do not base64 decode the string by hand; make your code do it. The point is that you don’t know its contents.
What you have now is a function that produces:
AES-128-ECB(your-string || unknown-string, random-key)
It turns out: you can decrypt “unknown-string” with repeated calls to the oracle function!
Here’s roughly how:
- Feed identical bytes of your-string to the function 1 at a time — start with 1 byte (“A”), then “AA”, then “AAA” and so on. Discover the block size of the cipher. You know it, but do this step anyway.
- Detect that the function is using ECB. You already know, but do this step anyways.
- Knowing the block size, craft an input block that is exactly 1 byte short (for instance, if the block size is 8 bytes, make “AAAAAAA”). Think about what the oracle function is going to put in that last byte position.
- Make a dictionary of every possible last byte by feeding different strings to the oracle; for instance, “AAAAAAAA”, “AAAAAAAB”, “AAAAAAAC”, remembering the first block of each invocation.
- Match the output of the one-byte-short input to one of the entries in your dictionary. You’ve now discovered the first byte of unknown-string.
- Repeat for the next byte.
Congratulations.
This is the first challenge we’ve given you whose solution will break real crypto. Lots of people know that when you encrypt something in ECB mode, you can see penguins through it. Not so many of them can decrypt the contents of those ciphertexts, and now you can. If our experience is any guideline, this attack will get you code execution in security tests about once a year.
defmodule Cryptopals.Set2.Challenge12 do
@unknown_string """
Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg
aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq
dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg
YnkK
"""
|> String.replace("\n", "")
|> Base.decode64!()
@random_key :rand.bytes(16)
def oracle(input) do
:crypto.crypto_one_time(:aes_128_ecb, @random_key, input <> @unknown_string,
encrypt: true,
padding: :pkcs_padding
)
end
end
{:module, Cryptopals.Set2.Challenge12, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:oracle, 1}}
# 1) Find the blocksize
byte_size(Cryptopals.Set2.Challenge12.oracle("AAAAAA")) -
byte_size(Cryptopals.Set2.Challenge12.oracle("AAAAA"))
16
# 2) Confirm ECB
# pattern match on two identical blocks
<> =
Cryptopals.Set2.Challenge12.oracle(:binary.copy("A", 32))
<<123, 54, 65, 89, 78, 108, 121, 165, 146, 141, 39, 105, 139, 42, 72, 98, 123, 54, 65, 89, 78, 108,
121, 165, 146, 141, 39, 105, 139, 42, 72, 98, 119, 85, 159, 29, 147, 23, 72, 118, 244, 122, 223,
142, 62, 71, 163, 39, 238, 39, ...>>
defmodule Cryptopals.Set2.Challenge12Solution do
import Cryptopals.Set2.Challenge12
@blocksize 16
def decrypt(block_num \\ 0, prev_block \\ :binary.copy("A", @blocksize), plaintext \\ "") do
block_start = @blocksize * block_num
plaintext_block =
Enum.reduce(1..@blocksize, "", fn len, decrypted ->
prefix = binary_part(prev_block, len, @blocksize - len) <> decrypted
candidates =
for byte <- 0..255, into: %{} do
<> = oracle(prefix <> <>)
{block, <>}
end
<<_::binary-size(block_start), block::binary-size(@blocksize), _::binary>> =
oracle(:binary.copy("A", @blocksize - len))
# TODO: I'm not getting the padding out of my dictionary (hence the ""). Something wonky
decrypted <> Map.get(candidates, block, "")
end)
if byte_size(oracle("")) == block_start + @blocksize do
plaintext <> plaintext_block
else
decrypt(block_num + 1, plaintext_block, plaintext <> plaintext_block)
end
end
end
#
IO.puts(Cryptopals.Set2.Challenge12Solution.decrypt())
Rollin' in my 5.0
With my rag-top down so my hair can blow
The girlies on standby waving just to say hi
Did you stop? No, I just drove by
:ok
13. ECB cut-and-paste
Write a k=v parsing routine, as if for a structured cookie. The routine should take:
foo=bar&baz=qux&zap=zazzle
… and produce:
{
foo: 'bar',
baz: 'qux',
zap: 'zazzle'
}
(you know, the object; I don’t care if you convert it to JSON).
Now write a function that encodes a user profile in that format, given an email address. You should have something like:
profile_for("foo@bar.com")
… and it should produce:
{
email: 'foo@bar.com',
uid: 10,
role: 'user'
}
… encoded as:
email=foo@bar.com&uid=10&role=user
Your “profile_for” function should not allow encoding metacharacters (& and =). Eat them, quote them, whatever you want to do, but don’t let people set their email address to “foo@bar.com&role=admin”.
Now, two more easy functions. Generate a random AES key, then:
- Encrypt the encoded user profile under the key; “provide” that to the “attacker”.
- Decrypt the encoded user profile and parse it.
Using only the user input to profile_for()
(as an oracle to generate “valid” ciphertexts) and the ciphertexts themselves, make a role=admin profile.
defmodule Cryptopals.Set2.Challenge13 do
@random_key :rand.bytes(16)
def profile_for(email) do
# Note that Elixir's map is ordering the keys for us
# The instructions want the order a certain way, which makes this problem much easier
# So I'm using a keyword list to get it (and kind of cheat)
[email: email, uid: 10, role: "user"]
|> URI.encode_query()
end
def encrypted_profile_for(email) do
profile = profile_for(email)
:crypto.crypto_one_time(:aes_128_ecb, @random_key, profile,
encrypt: true,
padding: :pkcs_padding
)
end
def decrypt_profile(encrypted_profile) do
:crypto.crypto_one_time(:aes_128_ecb, @random_key, encrypted_profile,
encrypt: false,
padding: :pkcs_padding
)
|> URI.decode_query()
end
end
{:module, Cryptopals.Set2.Challenge13, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:decrypt_profile, 1}}
# 1. Get a block that ends in com&role=
email1 = :binary.copy("A", 10) <> "com"
<<_::binary-size(16), role_block::binary-size(16), rst::binary>> =
Cryptopals.Set2.Challenge13.encrypted_profile_for(email1)
# 2. Get a block that starts with admin&, and make the email valid
email2 = "foo@bar.admin"
<> =
Cryptopals.Set2.Challenge13.encrypted_profile_for(email2)
# 3. Put them together
admin_profile =
Cryptopals.Set2.Challenge13.decrypt_profile(a <> role_block <> admin_block <> rst)
|> IO.inspect()
admin_profile["role"] == "admin"
%{"email" => "foo@bar.com", "role" => "admin", "roluser" => "", "uid" => "10"}
true
14. Byte-at-a-time ECB decryption (Harder)
Take your oracle function from #12. Now generate a random count of random bytes and prepend this string to every plaintext. You are now doing:
AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key)
Same goal: decrypt the target-bytes.
Stop and think for a second. What’s harder than challenge #12 about doing this? How would you overcome that obstacle? The hint is: you’re using all the tools you already have; no crazy math is required.
Think “STIMULUS” and “RESPONSE”.
defmodule Cryptopals.Set2.Challenge14 do
@unknown_string """
Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg
aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq
dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg
YnkK
"""
|> String.replace("\n", "")
|> Base.decode64!()
@random_key :rand.bytes(16)
@random_prefix :rand.bytes(Enum.random(5..40))
def oracle(input) do
:crypto.crypto_one_time(:aes_128_ecb, @random_key, @random_prefix <> input <> @unknown_string,
encrypt: true,
padding: :pkcs_padding
)
end
end
{:module, Cryptopals.Set2.Challenge14, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:oracle, 1}}
defmodule Cryptopals.Set2.Challenge14Solution do
import Cryptopals.Set2.Challenge14
@blocksize 16
def find_ciphrertext_prefix_size() do
find_ciphrertext_prefix_size(oracle(:binary.copy("A", 200)), 0)
end
def find_ciphrertext_prefix_size(ciphertext, block_num) do
case ciphertext do
<> ->
block_num * @blocksize
<<_::binary-size(@blocksize), rst::binary>> ->
find_ciphrertext_prefix_size(rst, block_num + 1)
end
end
def find_plaintext_prefix_size(0), do: 0
def find_plaintext_prefix_size(ciphertext_prefix_size, num \\ 16) when num > 0 do
case oracle(:binary.copy("_", num) <> :binary.copy("A", 200)) do
<<_::binary-size(ciphertext_prefix_size), a::binary-size(@blocksize),
a::binary-size(@blocksize), _::binary>> ->
num
_ ->
find_plaintext_prefix_size(ciphertext_prefix_size, num - 1)
end
end
def decrypt() do
ciphertext_prefix_size = find_ciphrertext_prefix_size()
plaintext_prefix_size = find_plaintext_prefix_size(ciphertext_prefix_size)
plaintext_prefix = :binary.copy("_", plaintext_prefix_size)
decrypt_helper(plaintext_prefix, ciphertext_prefix_size)
end
def decrypt_helper(
plaintext_prefix,
ciphertext_prefix_size,
block_num \\ 0,
prev_block \\ :binary.copy("A", @blocksize),
plaintext \\ ""
) do
block_start = ciphertext_prefix_size + @blocksize * block_num
plaintext_block =
Enum.reduce(1..@blocksize, "", fn len, decrypted ->
prefix = plaintext_prefix <> binary_part(prev_block, len, @blocksize - len) <> decrypted
candidates =
for byte <- 0..255, into: %{} do
<<_::binary-size(ciphertext_prefix_size), block::binary-size(@blocksize), _::binary>> =
oracle(prefix <> <>)
{block, <>}
end
<<_::binary-size(block_start), block::binary-size(@blocksize), _::binary>> =
oracle(plaintext_prefix <> :binary.copy("A", @blocksize - len))
# TODO: I'm not getting the padding out of my dictionary (hence the ""). Something wonky
decrypted <> Map.get(candidates, block, "")
end)
if byte_size(oracle(plaintext_prefix)) == block_start + @blocksize do
plaintext <> plaintext_block
else
decrypt_helper(
plaintext_prefix,
ciphertext_prefix_size,
block_num + 1,
plaintext_block,
plaintext <> plaintext_block
)
end
end
end
Cryptopals.Set2.Challenge14Solution.decrypt()
|> IO.puts()
Rollin' in my 5.0
With my rag-top down so my hair can blow
The girlies on standby waving just to say hi
Did you stop? No, I just drove by
:ok
15. PKCS#7 padding validation
Write a function that takes a plaintext, determines if it has valid PKCS#7 padding, and strips the padding off.
The string:
"ICE ICE BABY\x04\x04\x04\x04"
… has valid padding, and produces the result “ICE ICE BABY”.
The string:
"ICE ICE BABY\x05\x05\x05\x05"
… does not have valid padding, nor does:
"ICE ICE BABY\x01\x02\x03\x04"
If you are writing in a language with exceptions, like Python or Ruby, make your function throw an exception on bad padding.
Crypto nerds know where we’re going with this. Bear with us.
defmodule Cryptopals.Set2.Challenge15 do
@blocksize 16
def strip_padding(""), do: ""
def strip_padding(<>) do
# cheating by knowning the blocksize
# We get a free exception from the cases not matching
case block do
<<16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16>> -> ""
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
<> -> c
end
end
def strip_padding(plaintext) when byte_size(plaintext) > @blocksize do
<> = plaintext
<>
end
end
warning: variable "rst" is unused (if the variable is not meant to be used, prefix it with an underscore)
set2.livemd#cell:30: Cryptopals.Set2.Challenge15.strip_padding/1
{:module, Cryptopals.Set2.Challenge15, <<70, 79, 82, 49, 0, 0, 12, ...>>, {:strip_padding, 1}}
16. CBC bitflipping attacks
Generate a random AES key.
Combine your padding code and CBC code to write two functions.
The first function should take an arbitrary input string, prepend the string:
"comment1=cooking%20MCs;userdata="
.. and append the string:
";comment2=%20like%20a%20pound%20of%20bacon"
The function should quote out the “;” and “=” characters.
The function should then pad out the input to the 16-byte AES block length and encrypt it under the random AES key.
The second function should decrypt the string and look for the characters “;admin=true;” (or, equivalently, decrypt, split the string on “;”, convert each resulting string into 2-tuples, and look for the “admin” tuple).
Return true or false based on whether the string exists.
If you’ve written the first function properly, it should not be possible to provide user input to it that will generate the string the second function is looking for. We’ll have to break the crypto to do that.
Instead, modify the ciphertext (without knowledge of the AES key) to accomplish this.
You’re relying on the fact that in CBC mode, a 1-bit error in a ciphertext block:
Completely scrambles the block the error occurs in Produces the identical 1-bit error(/edit) in the next ciphertext block. Stop and think for a second. Before you implement this attack, answer this question: why does CBC mode have this property?
defmodule Cryptopals.Set2.Challenge16 do
import Cryptopals.Set2.Challenge10
@blocksize 16
@random_key :rand.bytes(@blocksize)
@random_iv :rand.bytes(@blocksize)
@prefix "comment1=cooking%20MCs;userdata="
@postfix ";comment2=%20like%20a%20pound%20of%20bacon"
def encrypt(input) do
sanitized =
input
|> String.replace("=", "%3D")
|> String.replace(";", "%3B")
plaintext = @prefix <> sanitized <> @postfix
aes_cbc_encrypt(plaintext, @random_key, @random_iv)
# :crypto.crypto_one_time(:aes_128_cbc, @random_key, plaintext, encrypt: true, padding: :pkcs_padding)
end
def decrypt(ciphertext) do
aes_cbc_decrypt(ciphertext, @random_key, @random_iv)
end
def check_admin(ciphertext) do
ciphertext
|> decrypt()
|> String.split(";")
|> Enum.map(&List.to_tuple(String.split(&1, "=")))
|> Enum.member?({"admin", "true"})
end
end
{:module, Cryptopals.Set2.Challenge16, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:check_admin, 1}}
# We need a binary xor again
defmodule Cryptopals.Utils do
use Bitwise
def xor(<<>>, _), do: <<>>
def xor(<>, <>) do
<> <> xor(a, b)
end
end
# First we need to get a block to scramble
# Use 32 A's as input so we get two blocks of A's
userdata = :binary.copy("A", 32)
# The prefix aligns nicely along blocks, so we get our first encrypted block of As
<> =
Cryptopals.Set2.Challenge16.encrypt(userdata)
# We want to bitflip our ciphertext so the next block is turned into the target
target =
"AAAAA;admin=true"
|> Cryptopals.Utils.xor(:binary.copy("A", 16))
scrambled_block = Cryptopals.Utils.xor(block, target)
# Now we sub in our scrambled block to get the result we want
new_ciphertext = prefix <> scrambled_block <> rst
# We're admin!
Cryptopals.Set2.Challenge16.check_admin(new_ciphertext)
true
Testing it all
ExUnit.start(autorun: false)
defmodule Set2Test do
import Cryptopals.Set2.{Challenge9, Challenge11, Challenge13, Challenge15}
use ExUnit.Case, async: true
test "pkcs7 padding" do
assert pkcs7("YELLOW SUBMARINE", 20) ==
"YELLOW SUBMARINE\x04\x04\x04\x04"
assert pkcs7("YELLOW SUBMARIN", 16) ==
"YELLOW SUBMARIN\x01"
assert pkcs7("YELLOW SUBMARINE", 16) ==
"YELLOW SUBMARINE\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10"
end
test "ecb/cbc mode oracle" do
input = :binary.copy("YELLOW SUBMARINE", 20)
for _ <- 1..100 do
{mode, output} = encryption_oracle(input)
assert detect_mode(output) == mode
end
end
test "profile_for" do
assert profile_for("foo@bar.com") == "email=foo%40bar.com&uid=10&role=user"
profile = profile_for("foo@bar.com&role=admin") |> URI.decode_query()
assert profile["role"] == "user"
end
test "strip_padding" do
assert strip_padding("ICE ICE BABY\x04\x04\x04\x04") == "ICE ICE BABY"
assert_raise CaseClauseError, fn ->
strip_padding("ICE ICE BABY\x05\x05\x05\x05")
end
assert_raise CaseClauseError, fn ->
strip_padding("ICE ICE BABY\x01\x02\x03\x04")
end
end
end
ExUnit.run()
....
Finished in 0.00 seconds (0.00s async, 0.00s sync)
4 tests, 0 failures
Randomized with seed 849580
%{excluded: 0, failures: 0, skipped: 0, total: 4}