cryptopals

Cryptopals Challenges
git clone https://git.pngu.org/cryptopals
Log | Files | Refs

commit 4c3995a8afb34136ab3e1bc02647f3d8e3824638
parent 452884f6a66f51ad48d04b8c7a2e94a57643651b
Author: Jack Sangdahl <jack@pngu.org>
Date:   Mon, 13 Oct 2025 12:36:38 -0600

challenge 12: byte-at-a-time ecb decryption (simple)

Diffstat:
Mlib/aes.c | 6+++---
Asrc/12_byte_ecb_decryption_1.c | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 109 insertions(+), 3 deletions(-)

diff --git a/lib/aes.c b/lib/aes.c @@ -141,13 +141,13 @@ struct bytes aes_encrypt_cbc(char *data, const size_t data_len, const EVP_CIPHER *cipher; switch (key_len) { case 16: - EVP_aes_128_cbc(); + cipher = EVP_aes_128_cbc(); break; case 24: - EVP_aes_192_cbc(); + cipher = EVP_aes_192_cbc(); break; case 32: - EVP_aes_256_cbc(); + cipher = EVP_aes_256_cbc(); break; } if (EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv) != 1) { diff --git a/src/12_byte_ecb_decryption_1.c b/src/12_byte_ecb_decryption_1.c @@ -0,0 +1,106 @@ +// Byte-at-a-time ECB decryption (Simple) +#include "aes.h" +#include "base64.h" +#include "random.h" +#include <assert.h> + +/* + * 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 variale). now take that same function and have it + * append to the plaintext before encrypting the following string: + * Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg + * YnkK. spoiler alert: do not decode this string now. base64 decode the string + * before appending it. do note 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: + * 1. 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 + * 2. detect that the function is using ecb. you already know, but do this step + * anyway as well + * 3. knowing the block sizee, 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 + * 4. 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 + * 5. match the output of the one byte short input to one ofthe entries in your + * dictionary. you've now discovered the first byte of 'unknown_string' + * 6. repeat for the next byte + * this is the first challenge 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 content 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. + */ + +const unsigned char *key; +char *secret; +size_t secret_len; + +struct bytes append_encrypt_ecb(char *data, size_t data_len) { + size_t input_len = data_len + secret_len; + char *input = (char *)malloc(input_len * sizeof(char)); + memcpy(input, data, data_len); + memcpy(input + data_len, secret, secret_len); + struct bytes res = aes_encrypt_ecb(input, input_len, key, 16); + free(input); + return res; +} + +struct bytes probe_encrypt(size_t len, char *suffix, size_t suffix_len) { + char *input = (char *)malloc(len * sizeof(char)); + memset(input, 'A', len - suffix_len); + memcpy(input + len - suffix_len, suffix, suffix_len); + struct bytes res = append_encrypt_ecb(input, len); + free(input); + return res; +} + +int main(void) { + srand(time(NULL)); + key = random_bytes(16); + struct bytes encoded, saved = {NULL, 0}; + int block_size, i; + char *base64 = "Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkga" + "GFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdX" + "N0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg"; + secret_len = base64_decode(base64, strlen(base64), &secret); + struct bytes zero_encoded = append_encrypt_ecb("", 0); + printf("Ciphertext size: %zu\n", zero_encoded.len); + for (i = 1; i < 32; i++) { + encoded = probe_encrypt(i, 0, 0); + block_size = encoded.len - zero_encoded.len; + if (block_size > 0) + break; + } + size_t res_len = zero_encoded.len - i; + printf("Block size: %d\n", block_size); + printf("Secret size: %zu\n", res_len); + char ones[64] = {1}; + encoded = append_encrypt_ecb(ones, 64); + char *detected_mode = aes_oracle(encoded.data, block_size); + printf("%s mode\n\n", detected_mode); + char *res = (char *)malloc(res_len * sizeof(char)); + for (size_t i = 0; i < res_len; i++) { + size_t sz = next_block(i, block_size); + saved = probe_encrypt(sz - i - 1, 0, 0); + for (unsigned char c = 1; c <= 128; c++) { + res[i] = c; + res[i + 1] = 0; + encoded = probe_encrypt(sz, res, i + 1); + if (!memcmp(encoded.data, saved.data, sz)) + break; + } + } + printf("%s\n", res); + assert(!strcmp(res, secret)); + free(res); + free(zero_encoded.data); + free(encoded.data); + free(saved.data); + return 0; +}