SECCON 14 CTF Quals Writeup: ez_flag_checker

Writeup for SECCON 14 CTF Quals Rev Eng Challenge ez-flag-checker

Author: Shruti Priya

Hello again and welcome to the writeup for SECCON 14 CTF Quals Reverse Engineer challenge: ez_flag_checker. This Reverse Engineering challenge required us to decrypt the flag hidden behind an encryption function in the binary.

Understanding the binary

Let’s start with the simplest way, running the binary and analysing the output. This challenge binary simply asks for the flag when executed.

1$ ./chall
2Enter flag: 

Let’s enter something random and see how the binary reacts. The flag format for this CTF begins with SECCON{ and ends with another } so let’s follow the format.

1$ ./chall
2Enter flag: SECCON{hello}
3wrong :(

The binary reacts with a string wrong :(. I’d wager the challenge takes the actual flag and checks it against something in the memory. If this check fails, we get the output wrong :(. Now, let’s disassemble the binary and analyse its functionality.

Disassembling the binary

Disassembling the binary in IDA, I notice that at one point the challenge compares the user’s input with the string SECCON{.

1loc_132C:
2lea     rax, [rbp+buf]
3mov     edx, 7          ; n
4lea     rcx, s2         ; "SECCON{"
5mov     rsi, rcx        ; s2
6mov     rdi, rax        ; s1
7call    cs:strncmp_ptr
8test    eax, eax
9jnz     short loc_137A

The strncmp_ptr compares the first 7 characters of the user’s input with the string in memory. Therefore, I was correct for entering the flag format. After checking these first 7 characters, the challenge continues to call another function called sigma_encrypt.

 1loc_1394:
 2lea     rax, [rbp+buf]
 3add     rax, 7
 4mov     [rbp+message], rax
 5lea     rcx, [rbp+user_tag]
 6mov     rax, [rbp+message]
 7mov     edx, 12h        ; len
 8mov     rsi, rcx        ; out
 9mov     rdi, rax        ; message
10call    sigma_encrypt
11lea     rax, [rbp+user_tag]
12mov     edx, 12h        ; n
13lea     rcx, flag_enc
14mov     rsi, rcx        ; s2
15mov     rdi, rax        ; s1
16call    cs:memcmp_ptr
17test    eax, eax
18setz    al
19movzx   eax, al
20mov     [rbp+ok], eax
21cmp     [rbp+ok], 0
22jz      short loc_1411

Notice also the flag_enc memory pointer. This is the encrypted version of the flag. The challenge takes the user input, compares the first 7 characters with SECCON{. If this check passes, the challenge calls the sigma_encrypt function to encrypt the user input. Then the challenge compares this output with the encrypted flag stored in memory.

Since I can access both the sigma_encrypt function and flag_enc, the intended pathway for solving this challenge must involve decrypting the flag.

Sigma Encrypt

The sigma_encrypt function takes 3 arguments: message, out, len. This function also has a curious array called sigma_words. Looking at the data in sigma_words, it contains 4 double words (1 double word is 4-bytes).

 1void __cdecl sigma_encrypt(const char *message, uint8_t *out, size_t len)
 2{
 3  int i; // [rsp+30h] [rbp-30h]
 4  uint32_t w; // [rsp+34h] [rbp-2Ch]
 5  size_t i_0; // [rsp+38h] [rbp-28h]
 6  uint8_t key_bytes[24]; // [rsp+40h] [rbp-20h]
 7  unsigned __int64 v7; // [rsp+58h] [rbp-8h]
 8
 9  v7 = __readfsqword(0x28u);
10  for ( i = 0; i <= 3; ++i )
11  {
12    w = sigma_words[i];
13    key_bytes[4 * i] = w;
14    key_bytes[4 * i + 1] = BYTE1(w);
15    key_bytes[4 * i + 2] = BYTE2(w);
16    key_bytes[4 * i + 3] = HIBYTE(w);
17  }
18  for ( i_0 = 0; i_0 < len; ++i_0 )
19    out[i_0] = (i_0 + key_bytes[i_0 & 0xF]) ^ message[i_0];
20  if ( v7 != __readfsqword(0x28u) )
21    _stack_chk_fail();
22}

The encryption function iterates over the sigma_words array and adds certain bytes to the key_bytes array. Then, the function iterates over the key_bytes, performs an AND operation between the key_byte and 0xF and XOR-s the result with the message byte.

Since the encryption performs an XOR operation, we can get the real flag by just plugging in the encrypted flag bytes instead of the message in the sigma_encrypt function.

Crafting the exploit

I create a Python implementation of the sigma_encrypt function and extract the flag-bytes from the flag_enc memory pointer.

 1sigma_words = [0x61707865, 0x3320646E, 0x79622D32, 0x6B206574]
 2
 3# Function to derive key_bytes from sigma_words
 4def derive_key_bytes():
 5    key_bytes = []
 6    for i in range(4): 
 7        w = sigma_words[i]
 8        key_bytes.append(w & 0xFF)          # Low byte
 9        key_bytes.append((w >> 8) & 0xFF)   # 2nd byte
10        key_bytes.append((w >> 16) & 0xFF)  # 3rd byte
11        key_bytes.append((w >> 24) & 0xFF)  # High byte
12    return key_bytes
13
14# Decryption function
15def sigma_decrypt(encrypted_message, key_bytes):
16    decrypted_message = bytearray(len(encrypted_message))
17    for i in range(len(encrypted_message)):
18        decrypted_message[i] = encrypted_message[i] ^ (i + key_bytes[i & 0xF])
19    return decrypted_message
20
21
22# flag_enc instead of message
23encrypted_message = bytearray([0x03,0x15,0x13,0x03,0x11,0x55,0x1f,0x43,0x63,0x61,0x59,0xef,0xbc,0x10,0x1f,0x43,0x54,0xa8])
24
25key_bytes = derive_key_bytes()
26
27decrypted_message = sigma_decrypt(encrypted_message, key_bytes)
28
29print("Decrypted message:", decrypted_message.decode())

Let’s execute this and get our flag!

1$ python ez-flag-exploit.py
2Decrypted message: flagc<9yYW5k<b19!!
3$ ./chall
4Enter flag: SECCON{flagc<9yYW5k<b19!!}
5wrong :(
6$ 

Unfortunately, this did not work. For some reason, the challenge does not accept our flag. Let’s debug this in pwndbg and see what is going wrong.

Debugging the exploit

1$ pwndbg ./chall
2pwndbg> b *main+240
3Breakpoint 1 at 0x1141: file main.c, line 9.
4pwndbg> r
5Starting program: ~/ctfs/seccon/ez_flag_checker/chall 
6[Thread debugging using libthread_db enabled]
7Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
8Enter flag: SECCON{flagc<9yYW5k<b19!!}

Lots of things happening here! I am running this binary from within pwndbg. I add a breakpoint at the instruction where the challenge compares the user input with the encrypted flag in memory. Then I run the binary and enter the flag.

RDI  0x7fffffffd890 ◂— 0x431f5b1103131503
RSI  0x555555556010 (flag_enc) ◂— 0x431f551103131503

Checking the registers right before the strings are compared, I notice that there is a slight difference in my flag and the encrypted flag. Notice how the 3rd byte is 0x5b in my input but 0x55 in the encrypted flag. This must be why the check is failing.

But the problem is that we have correctly used 0x55 in our encrypted_message. Let’s try replacing 0x55 with 0x5b and see what happens.

Final exploit

This is the new exploit script with the modified encrypted_message.

 1sigma_words = [0x61707865, 0x3320646E, 0x79622D32, 0x6B206574]
 2
 3# Function to derive key_bytes from sigma_words
 4def derive_key_bytes():
 5    key_bytes = []
 6    for i in range(4): 
 7        w = sigma_words[i]
 8        key_bytes.append(w & 0xFF)          # Low byte
 9        key_bytes.append((w >> 8) & 0xFF)   # 2nd byte
10        key_bytes.append((w >> 16) & 0xFF)  # 3rd byte
11        key_bytes.append((w >> 24) & 0xFF)  # High byte
12    return key_bytes
13
14# Decryption function
15def sigma_decrypt(encrypted_message, key_bytes):
16    decrypted_message = bytearray(len(encrypted_message))
17    for i in range(len(encrypted_message)):
18        decrypted_message[i] = encrypted_message[i] ^ (i + key_bytes[i & 0xF])
19    return decrypted_message
20
21
22# flag_enc instead of message
23encrypted_message = bytearray([0x03,0x15,0x13,0x03,0x11,0x5b,0x1f,0x43,0x63,0x61,0x59,0xef,0xbc,0x10,0x1f,0x43,0x54,0xa8])
24
25key_bytes = derive_key_bytes()
26
27decrypted_message = sigma_decrypt(encrypted_message, key_bytes)
28
29print("Decrypted message:", decrypted_message.decode())

Getting the decrypted message and checking it against the binary again.

1$ python ez-flag-exploit.py
2Decrypted message: flagc29yYW5k<b19!!
3$ ./chall
4Enter flag: SECCON{flagc29yYW5k<b19!!}
5correct flag!
6$ 

And we have it! The correct flag is SECCON{flagc29yYW5k<b19!!} and we have solved the ez_flag_checker challenge by SECCON 14 CTF Quals. Thank you for reading and I will see you in the next writeup!