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:
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;
+}