cryptopals

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

commit eace2b19a5f0ef8349783ae9a40aaa05c0638674
parent bc70e11286a48ba9efbf259c1a00ad98c076412e
Author: Jack Sangdahl <jack@pngu.org>
Date:   Fri, 17 Oct 2025 08:45:13 -0600

challenge 17: the cbc padding oracle

Diffstat:
Adata/17.txt | 10++++++++++
Asrc/17_cbc_padding_oracle.c | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 111 insertions(+), 0 deletions(-)

diff --git a/data/17.txt b/data/17.txt @@ -0,0 +1,10 @@ +MDAwMDAwTm93IHRoYXQgdGhlIHBhcnR5IGlzIGp1bXBpbmc= +MDAwMDAxV2l0aCB0aGUgYmFzcyBraWNrZWQgaW4gYW5kIHRoZSBWZWdhJ3MgYXJlIHB1bXBpbic= +MDAwMDAyUXVpY2sgdG8gdGhlIHBvaW50LCB0byB0aGUgcG9pbnQsIG5vIGZha2luZw== +MDAwMDAzQ29va2luZyBNQydzIGxpa2UgYSBwb3VuZCBvZiBiYWNvbg== +MDAwMDA0QnVybmluZyAnZW0sIGlmIHlvdSBhaW4ndCBxdWljayBhbmQgbmltYmxl +MDAwMDA1SSBnbyBjcmF6eSB3aGVuIEkgaGVhciBhIGN5bWJhbA== +MDAwMDA2QW5kIGEgaGlnaCBoYXQgd2l0aCBhIHNvdXBlZCB1cCB0ZW1wbw== +MDAwMDA3SSdtIG9uIGEgcm9sbCwgaXQncyB0aW1lIHRvIGdvIHNvbG8= +MDAwMDA4b2xsaW4nIGluIG15IGZpdmUgcG9pbnQgb2g= +MDAwMDA5aXRoIG15IHJhZy10b3AgZG93biBzbyBteSBoYWlyIGNhbiBibG93 diff --git a/src/17_cbc_padding_oracle.c b/src/17_cbc_padding_oracle.c @@ -0,0 +1,101 @@ +// The CBC padding oracle +#include "aes.h" +#include "base64.h" +#include "blocks.h" +#include "random.h" +#include <assert.h> + +/* + * this is the best-knowwn attach on modern block-cipher cryptography. combine + * your padding code and your cbc code to write two functions. the first + * function should select at random one of the 10 strings in [data/17.txt] + * ...generate a random aes key (which it should save for all future + * encryption), pad the string out to the 16-byte aes block size, and cbc + * encrypt it under that key, providing the ciphertext and the iv. the second + * function should consume the ciphertext produced by the first function, + * decrypt it, check its padding, and return true or false depending on whether + * the padding is valid. what you're doing here... this pair of functions + * approximates aes-cbc encryption as its deployed serverside in web + * applications; the second function models the server's consumption of an + * ecrypted session token, as if it was a cookie. it turns out that it's + * possible to decrypt the ciphertexts provided by the first function. the + * decryption here depends on a side-channel leak by the decryption function. + * the leak is the error message that the padding is valid or not. you can find + * 100 web pages on how this attack works, but the fundamental insight behind + * this attack is that the byte 01h is valid padding, and occur 1/256 trials of + * "randomised" plaintexts produced by decrypting a tampered ciphertext. 02h in + * isolation is not valid padding. 02h 02h is valid padding, but is much less + * likely to occur randomly than 01h. so you can assume that if you corrupt a + * decryption and it had valid padding, you know what that padding byte is. it + * is easy to get tripped up on the fact that cbc plaintexts are "padded". + * padding oracles have nothing to with the actual padding on a cbc plaintext. + * it's an attack that targets a specific bit of code that handles decryption. + * you can mount a padding oracle on any cbc block, whether it's padded or not. + */ + +#define LINE_LEN 81 + +unsigned char *key; +size_t key_len; +unsigned char iv[16] = {0}; + +bool is_valid_padding(unsigned char *encrypted, size_t encrypted_len) { + struct bytes decrypted = + aes_decrypt_cbc((unsigned char *)encrypted, encrypted_len, + (const unsigned char *)key, key_len, iv); + if (!decrypted.data) + return false; + free(decrypted.data); + return true; +} + +struct bytes padding_attack(unsigned char *encrypted, size_t encrypted_len, + unsigned char *iv) { + unsigned char **blocks = NULL, *prev_block = iv, attack_block[32] = {0}; + char *plaintext = (char *)malloc(1 + (encrypted_len * sizeof(char))); + plaintext[encrypted_len] = 0; + size_t num_blocks = chunk(encrypted, encrypted_len, 16, &blocks); + for (size_t block = 0; block < num_blocks; block++) { + memcpy(attack_block + 16, blocks[block], 16); + for (size_t i = 0; i < 16; i++) { + char pad_byte = i + 1; + for (size_t j = 0; j < i; j++) + attack_block[15 - j] = + prev_block[15 - j] ^ plaintext[(16 * block) + 15 - j] ^ pad_byte; + for (int byte = 0; byte < 256; byte++) { + attack_block[15 - i] = byte; + if (is_valid_padding(attack_block, sizeof(attack_block))) { + char decrypted = prev_block[15 - i] ^ byte ^ pad_byte; + int idx = (16 * block) + 15 - i; + plaintext[idx] = decrypted; + break; + } + } + } + memcpy(prev_block, blocks[block], 16); + } + struct bytes res = pkcs7_strip((unsigned char *)plaintext, encrypted_len); + return res; +} + +int main(void) { + char *decoded = NULL, line[LINE_LEN]; + struct bytes encrypted, decrypted; + key = random_bytes(16); + key_len = 16; + FILE *input = fopen("data/17.txt", "r"); + for (int line_num = 1; fgets(line, LINE_LEN, input); line_num++) { + int decoded_len = base64_decode(line, strlen(line), &decoded); + encrypted = aes_encrypt_cbc(decoded, decoded_len, + (const unsigned char *)key, key_len, iv); + if (encrypted.data) { + decrypted = padding_attack(encrypted.data, encrypted.len, iv); + printf("%s\n", decrypted.data); + assert(!strncmp((char *)decrypted.data, decoded, decoded_len)); + free(encrypted.data); + free(decrypted.data); + } + } + free(decoded); + return 0; +}