Tags: aes pwn crypto
Rating:
## Description
> Внезапно тучи сгущаются. На вас налетает тропический ливень! С неба валятся цифры 0 и 1, от которых не спасает пляжный зонтик. Нужно срочно найти убежище, чтобы не стать мокрой двоичной курицей.
> Suddenly, the clouds thicken. A tropical downpour descends upon you! The numbers 0 and 1 are falling from the sky, and a beach umbrella is no help. You need to find shelter quickly, or you will become a wet binary chicken. _(machine translation)_
## Overview
We're given the TCP server which implements AES_128_ECB encryption using the static key.
The server gives us an encrypted flag and provides the following interface:
1. encrypt the arbitrary plaintext and output the ciphertext
2. print the previous plaintext and ciphertext
```c
char message[MAX_MESSAGE_LEN];
data_t data;
switch (choice) {
case MENU_AES128: {
printf("?️ Say something to the cave: ");
fflush(stdout);
if (fgets(message, MAX_MESSAGE_LEN, stdin) == NULL) break;
size_t recv_len = strlen(message);
if (recv_len > 0 && message[recv_len - 1] == '\n') {
message[recv_len - 1] = '\0';
recv_len--;
}
if (recv_len % 16 != 0) {
for (size_t i = recv_len; i % 16 != 0; i++) {
message[i] = 0;
recv_len++;
}
}
if (last_encryption->has_data) {
free(last_encryption->last_ciphertext);
if (last_encryption->is_malloced) {
free(last_encryption->last_plaintext);
}
}
if (recv_len <= 16) {
memcpy(data.plaintext, message, AES_BLOCKLEN);
last_encryption->last_ciphertext = aes_encrypt_128(&data, client_key);
last_encryption->last_plaintext = (uint8_t *) &data.plaintext;
last_encryption->is_malloced = false;
} else {
last_encryption->last_ciphertext = malloc(recv_len+1);
for (size_t i = 0; i < recv_len; i += 16) {
memcpy(data.plaintext, message + i, 16);
uint8_t * result = aes_encrypt_128(&data, client_key);
memcpy(last_encryption->last_ciphertext + i, result, 16);
free(result);
}
last_encryption->last_plaintext = malloc(recv_len+1);
memcpy(last_encryption->last_plaintext, message, recv_len);
last_encryption->is_malloced = true;
}
last_encryption->has_data = 1;
last_encryption->ciphertext_len = recv_len;
char * hex_result = malloc(recv_len*2 + 1);
bytes_to_hex(last_encryption->last_ciphertext, hex_result, recv_len);
printf(
"?️ The cave echoes back...\n"
"?️ You said: '%s'\n"
"? Encrypted echo: %s\n"
"? Your words bounce off the cave walls\n"
" and return as magical symbols...\n\n",
message, hex_result);
free(hex_result);
break;
}
case MENU_SHOW_LAST: {
if (last_encryption->has_data) {
char * hex_result = malloc(last_encryption->ciphertext_len * 2 + 1);
bytes_to_hex(last_encryption->last_ciphertext, hex_result, last_encryption->ciphertext_len);
printf(
"? ═══════ LAST ECHO ═══════ ?\n"
"?️ You said: '%s'\n"
"? Encrypted echo: %s\n"
"? The echo still reverberates from the cave walls...\n\n",
last_encryption->last_plaintext,
hex_result);
free(hex_result);
} else {
printf(
"? The echo in the cave has quieted...\n"
" You haven't said anything yet.\n"
" Say something to hear the echo!\n\n");
}
break;
}
}
```
## Investigation
Although the challenge is marked as pwn, there are no binary vulnerabilities in the provided code. But there is a hidden data leak instead. Note that during the `MENU_SHOW_LAST` option the content of `last_encryption->last_plaintext` buffer is printed as `%s` without any length check. We can abuse this property as follows.
Let's encrypt the single plaintext block (16 bytes), then the `last_plaintext` will be set to `data.plaintext`:
```c
if (recv_len <= 16) {
memcpy(data.plaintext, message, AES_BLOCKLEN);
last_encryption->last_ciphertext = aes_encrypt_128(&data, client_key);
last_encryption->last_plaintext = (uint8_t *) &data.plaintext;
last_encryption->is_malloced = false;
}
```
Let's look at the `data_t` structure:
```c
typedef struct {
uint8_t plaintext[16];
uint8_t ciphertext[16];
} data_t;
```
Right after `data.plaintext` there is `data.ciphertext`, so if the plaintext does not have null terminator the service will also output the content of the `data.ciphertext`. Let's check this:
```
> ./binrain.elf
?️ ═════════ VOICE OF THE CAVE ═════════ ?️
I hear your footsteps, traveler...
Say something, and I'll echo it back to you!
?️ 1. Say something to the cave (hear echo)
? 2. Read the last echo
? 3. Leave the cave
Choose your path (1-3): 1
?️ Say something to the cave: AAAAAAAAAAAAAAAA
?️ The cave echoes back...
?️ You said: 'AAAAAAAAAAAAAAAA'
? Encrypted echo: 2b25f123af2431d40754bccb89833cc3
? Your words bounce off the cave walls
and return as magical symbols...
?️ ═════════ VOICE OF THE CAVE ═════════ ?️
I hear your footsteps, traveler...
Say something, and I'll echo it back to you!
?️ 1. Say something to the cave (hear echo)
? 2. Read the last echo
? 3. Leave the cave
Choose your path (1-3): 2
? ═══════ LAST ECHO ═══════ ?
?️ You said: 'AAAAAAAAAAAAAAAA*L\x94e\xbelp|\xcc\xceL\xeb\xee\x06\x1f\xb4AAAAAAAAAAAAAAAA'
? Encrypted echo: 2b25f123af2431d40754bccb89833cc3
? The echo still reverberates from the cave walls...
?️ ═════════ VOICE OF THE CAVE ═════════ ?️
I hear your footsteps, traveler...
Say something, and I'll echo it back to you!
?️ 1. Say something to the cave (hear echo)
? 2. Read the last echo
? 3. Leave the cave
```
So we've got the following buffers:
```
'AAAAAAAAAAAAAAAA' (16 bytes) -> data.plaintext
'*L\x94e\xbelp|\xcc\xceL\xeb\xee\x06\x1f\xb4' (16 bytes) -> data.ciphertext
'AAAAAAAAAAAAAAAA' -> message
```
Please note that the received `data.ciphertext` does not match the resulting ciphertext given by the server. So it's obviously something different.
Let's look at the AES implementation in `aes.c`:
```c
void RoundFunction(state_t * state, const uint8_t* RoundKey, uint8_t round) {
SubBytes(state);
ShiftRows(state);
MixColumns(state);
AddRoundKey(round, state, RoundKey);
}
uint8_t* LastRound(state_t *state, const uint8_t* RoundKey, uint8_t round) {
SubBytes(state);
ShiftRows(state);
AddRoundKey(round, state, RoundKey);
uint8_t * output = (uint8_t *) malloc(AES_BLOCKLEN*sizeof(uint8_t));
memcpy(output, state, AES_BLOCKLEN);
return output;
}
static uint8_t* Cipher(state_t* state, const uint8_t* RoundKey) {
uint8_t round = 0;
state_t temp;
memcpy(&temp, state, AES_BLOCKLEN);
AddRoundKey(0, &temp, RoundKey);
for (round = 1; round < Nr ; ++round) {
memcpy(state, &temp, AES_BLOCKLEN);
RoundFunction(&temp, RoundKey, round);
}
return LastRound(&temp, RoundKey, round);
}
uint8_t * AES_ECB_encrypt(const struct AES_ctx* ctx, state_t* buf) {
return Cipher(buf, ctx->RoundKey);
}
uint8_t* aes_encrypt_128(data_t *data, const uint8_t *key) {
struct AES_ctx ctx;
AES_init_ctx(&ctx, key);
memcpy(data->ciphertext, data->plaintext, AES_BLOCKLEN);
return AES_ECB_encrypt(&ctx, (state_t*)data->ciphertext);
}
```
Note that `data->ciphertext` is casted to `state_t` struct and passed to internal AES operations. In the `Cipher()` function there are another `state_t` struct `temp` which is actually used within functions. After each step the value of `temp` is copied into the original `state`, except for the last `RoundFunction()` and for the `LastRound()`:
```c
static uint8_t* Cipher(state_t* state, const uint8_t* RoundKey) {
uint8_t round = 0;
state_t temp;
memcpy(&temp, state, AES_BLOCKLEN);
AddRoundKey(0, &temp, RoundKey);
for (round = 1; round < Nr ; ++round) {
memcpy(state, &temp, AES_BLOCKLEN); // <- the last copy
RoundFunction(&temp, RoundKey, round);
}
return LastRound(&temp, RoundKey, round);
}
```
So the `data->ciphertext` buffer contains the AES state before the last two rounds. In other words:
```
data->ciphertext = temp
// RoundFunction()
temp = SubBytes(temp)
temp = ShiftRows(temp)
temp = MixColumns(temp)
temp = AddRoundKey(9, temp, RoundKey)
// LastRound()
temp = SubBytes(temp)
temp = ShiftRows(temp)
temp = AddRoundKey(10, temp, RoundKey)
// output temp
ciphertext = temp
```
So we need to solve a 2-rounds AES to retrieve 9th and 10th round keys.
## Solution
Note that `SubBytes()`, `ShiftRows()` and `MixColumns()` are independent from the key schedule, so our target is simplified a bit:
```
state1 = MixColumns(ShiftRows(SubBytes(temp)))
state2 = AddRoundKey(ShiftRows(SubBytes(AddRoundKey(state2))))
```
We've got a relation between `state1` and `state2`, both states are known. Moreover, we're able to get arbitrary many pairs of `(state1, state2)` for the same AES key:
```python
def download_state_pair():
io.sendline(b'1')
io.sendline(os.urandom(4).hex())
io.recvuntil(b'You said: ')
io.sendline(b'2')
io.recvuntil(b'You said: ')
line = io.recvline().strip()[1:-1]
state1 = line[16:32]
assert len(state1) == 16, 'try again'
io.recvuntil(b'Encrypted echo: ')
line = io.recvline().strip()
state2 = bytes.fromhex(line.decode())
assert len(state2) == 16, 'try again'
# print(f'{state1 = }')
# print(f'{state2 = }')
state1 = sub_bytes(state1)
state1 = shift_rows(state1)
state1 = mix_columns(state1)
return state1, state2
```
Let's recover the 9th and 10th round keys. We will bruteforce each byte of the 9th key, calculate corresponding byte of the 10th key, and validate these bytes against each `state2` from the `pairs`:
```python
def recover_round_keys(pairs: List[Tuple[bytes, bytes]]):
K9 = bytearray(16)
K10 = bytearray(16)
for j in range(16):
i = SHIFT_ROWS[j]
found = False
for k9_candidate in range(256):
C0 = pairs[0][0][i]
s0 = pairs[0][1][j]
k10_candidate = SBOX[C0 ^ k9_candidate] ^ s0
for idx, (_, state2) in enumerate(pairs[1:], 1):
Cx = pairs[idx][0][i]
sy = state2[j]
if SBOX[Cx ^ k9_candidate] ^ sy != k10_candidate:
break
else:
K9[i] = k9_candidate
K10[j] = k10_candidate
found = True
break
if not found:
raise ValueError('try again')
return bytes(K9), bytes(K10)
```
When the `K10` is found, we just need to reverse the AES key schedule and recover the master key.
## Flag
```
alfa{ITs_Ra1ning_4Es_halLeLUjaH}
```