Skip to content

Latest commit

 

History

History

cheat-console

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

Cheat Console


Points: 329 | Solves: 25/1035 | Category: Reverse

Grandpa Bill told you that in his youth he loved playing arcade machines. He was a notorious cheater and rarely played fair. He told you that one day in his favorite arcade (Pacman) he played around with the known cheat codes, trying to discover something new. Contrary to his expectation one code that he tried did not make him invincible against all enemies but showed a prompt asking for a password. Sadly he never was able to figure out the password and discovered what is hidden behind the prompt. Eventually the arcade center shut down and he couldn't try anymore. Unwilling to give up he contacted the developers which gave him a small program and said that it's the part which he was shown back then and that if he discovers the password he'll know what was hidden behind that prompt. Be a good grandson/granddaughter and help him with your awesome reversing skills.

On a side note: You might wanna lie to him once you solved the challenge (and found the hidden message in the password). ;)

The flag is the password used for the program. This challenge does not use the standard flag format.

Bahasa Indonesia

English

TL;DR

  • Patch the binary so we can decompile.
  • Ensure SIGTRAP signal is sent and always cause SIGFPE to be sent if possible.
  • Dynamic analysis with GDB to get the flag.
  • Use Z3 (and guessing) to get the final part of the flag.

Detailed Steps

We were given an ELF 32-bit binary. In this binary, there's a lot of functions that can't be decompiled by IDA because of int3 instructions (opcode = 0xCC).

.text:080486BB main:
...
.text:08048741           jnz     short loc_804872A
.text:08048743           mov     [ebp-68h], ecx
.text:08048743 ; ----------------------------------------------
.text:08048746           db 2 dup(0CCh)
.text:08048748 ; ----------------------------------------------
.text:08048748           cmp     dword ptr [ebp-68h], 32h
.text:0804874C           jnz     loc_8048890

We patched the binary and replaced the instructions to nop (opcode = 0x90) with hex editor. After that, functions can be decompiled. Let's take a look at main function.

int __cdecl main(int a1, char **a2) {
  int v2; // ecx
  unsigned int v3; // ebx
  int v4; // ST00_4
  int result; // eax
  char **v6; // [esp+0h] [ebp-7Ch]
  char v7; // [esp+18h] [ebp-64h]
  unsigned int v8; // [esp+60h] [ebp-1Ch]
  int *v9; // [esp+6Ch] [ebp-10h]

  v9 = &a1;
  v6 = a2;
  v8 = __readgsdword(0x14u);
  if ( a1 == 2 ) {
    ... // performs checking
  }
  else {
    puts("You might want to submit the password to gain access.");
    result = 1;
  }
  return result;
}

It takes an argument as a password then does some checking. Now take a look at function sub_80488B2.

signed int __cdecl sub_80488B2(const char *a1)
{
  size_t v1; // eax
  signed int i; // [esp+18h] [ebp-A0h]
  char v4; // [esp+1Ch] [ebp-9Ch]
  char v5[32]; // [esp+8Ch] [ebp-2Ch]
  unsigned int v6; // [esp+ACh] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  SHA256_Init(&v4);
  v1 = strlen(a1);
  SHA256_Update(&v4, a1, v1);
  SHA256_Final(v5, &v4);
  for ( i = 0; i <= 31; ++i )
  {
    if ( (v5[i] ^ 0x42) != *(i + 0x804B060) )
    {
      __asm { int     80h; LINUX - }
      return 0;
    }
  }
  return 1;
}

It calculates SHA-256 of our password, xored with 0x42, and compared to 0x804B060 which will be translated into a string Well done, but you were mislead.. We took a note that there might be many misleading functions and fake flags in this binary.

We noticed that there's an int 0x80 instruction (or syscall) in the function. Let's take a look at the disassembly.

.text:08048965                 movzx   eax, al
.text:08048968                 cmp     edx, eax
.text:0804896A                 jz      short loc_8048997
.text:0804896C                 movzx   edx, [ebp+var_A2]
.text:08048973                 mov     esi, offset sub_80489C6
.text:08048978                 xor     eax, eax
.text:0804897A                 mov     al, dl
.text:0804897C                 inc     eax
.text:0804897D                 push    8
.text:0804897F                 pop     ebx
.text:08048980                 push    0
.text:08048982                 push    0
.text:08048984                 push    0
.text:08048986                 push    esi
.text:08048987                 mov     ecx, esp
.text:08048989                 xor     edx, edx
.text:0804898B                 int     80h             ; LINUX -
.text:0804898D                 sub     esp, 10h
.text:08048990                 mov     eax, 0
.text:08048995                 jmp     short loc_80489AE

We learned that IDA can't decompile the function (and many other functions) completely. We decided to do dynamic analysis and set breakpoint at 0x0804898B to know what syscall is made.

$ gdb public/challenge
...
gdb-peda$ b *0x0804898B
Breakpoint 1 at 0x804898b
gdb-peda$ r asd
[----------------------------------registers-----------------------------------]
EAX: 0x43 ('C')
EBX: 0x8
ECX: 0xffffcda0 --> 0x80489c6 (push   ebp)
EDX: 0x0
ESI: 0x80489c6 (push   ebp)
EDI: 0x0
EBP: 0xffffce68 --> 0xffffcf08 --> 0x0
ESP: 0xffffcda0 --> 0x80489c6 (push   ebp)
EIP: 0x804898b (int    0x80)
EFLAGS: 0x200246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
...
Breakpoint 1, 0x0804898b in ?? ()
gdb-peda$

The system call number (EAX) is 0x43 or sys_sigaction. In short, it registers a signal handler with signal number (EBX) 0x8 or SIGFPE, and to be handled by a function (ECX) in 0x80489c6. SIGFPE is a floating point exception that will be occured in arithmetic operation error, such as division by zero.

Now let's take a look at function 0x80489c6, we'll call this handle_SIGFPE from now on.

int __cdecl handle_SIGFPE(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, unsigned __int8 *a12, int a13, int a14, int a15, int a16) {
  int v16; // eax
  int result; // eax

  if ( dword_804B084 == 'REGT' ) {
    v16 = sys_mprotect((main & 0xFFFFF000), 0x2000u, 7);
    result = a16 - 1;
    *result = 0x5B056A51;
    *(result + 4) = 0x8B58306A;
    *(result + 8) = 0x80CD944D;
    *(result + 12) = 89;
  }
  else {
    result = dword_804B084;
    if ( dword_804B084 != 'NOPP' ) {
      if ( dword_804B084 == 'FG03' ) {
        result = *a12;
      }
      else {
        result = dword_804B084;
        if ( dword_804B084 == 'FG41' )
          dword_804B084 = 'NOPP';
      }
    }
  }
  return result;
}

There's interesting code, if dword_804B084 == 'REGT' it will call mprotect and will modify address pointed by a16 - 1. We guessed it's modifying its code, so we used pwntools to disassembly the code.

>>> from pwn import *
>>> print disasm(p32(0x5B056A51)+p32(0x8B58306A)+p32(0x80CD944D)+chr(89))
   0:   51                      push   ecx
   1:   6a 05                   push   0x5
   3:   5b                      pop    ebx
   4:   6a 30                   push   0x30
   6:   58                      pop    eax
   7:   8b 4d 94                mov    ecx,DWORD PTR [ebp-0x6c]
   a:   cd 80                   int    0x80
   c:   59                      pop    ecx

Oh, another int 0x80! So to reach this code, we have to set dword_804B084 to 'REGT' and cause an arithmetic error so SIGFPE signal will be sent.

Now back to main function.

int __cdecl main(int a1, char **a2) {
    ...
    dword_804B084 = 'REGT';
    if ( sub_80488B2(*a2) ) {
        if ( strlen(v6[1]) != 42 )
            exit(1);
    }
    else {
        v2 = 0;
        do
            v3 = v6[1][v2++];
        while ( 0x42424242 % v3 );
        if ( v2 == 50 ) {
            if ( sub_8048B14(v6[1] + 3) ) {
            if ( sub_8048D7D(v6, v6[1] + 16) ) {
        ...
    }
}

We will never satisfy function sub_80488B2, therefore we concluded that password length checking, strlen(v6[1]) != 42, is fake.

More checking will be done if v2 == 50. To satisfy this, simply set the 50th character of our password to B or ! (they're divisible by 0x42424242). But remember, we already had SIGFPE handler! So if the 50th character is null byte and there's no ! and B, division by zero error will occur and cause handle_SIGFPE to be called. Also, dword_804B084 is already set to 'REGT' (to reach the modifying code). We concluded that our password length must be 49.

We set breakpoint at 0x080489FC where modifying code is executed inside handle_SIGFPE and run with 49 chars as password.

$ gdb public/challenge
gdb-peda$ b *0x080489FC
Breakpoint 1 at 0x80489fc
gdb-peda$ r 1234567890123456789012345678901234567890123456789
Stopped reason: SIGFPE
0x08048737 in ?? ()
gdb-peda$ c
Breakpoint 1, 0x080489fc in ?? ()
gdb-peda$ i r
eax            0x8048736	0x8048736
...

So it's modifying code at 0x8048736, let's continue stepping in.

gdb-peda$ ni
0x08048a02 in ?? ()
...
gdb-peda$ ni
0x08048736 in ?? ()
gdb-peda$ x/15i $eip
=> 0x8048736:	push   ecx
   0x8048737:	push   0x5
   0x8048739:	pop    ebx
   0x804873a:	push   0x30
   0x804873c:	pop    eax
   0x804873d:	mov    ecx,DWORD PTR [ebp-0x6c]
   0x8048740:	int    0x80
   0x8048742:	pop    ecx
   0x8048743:	mov    DWORD PTR [ebp-0x68],ecx
   0x8048746:	int3
   0x8048747:	int3
   0x8048748:	cmp    DWORD PTR [ebp-0x68],0x32
   0x804874c:	jne    0x8048890
gdb-peda$ ni
...
0x08048740 in ?? ()
gdb-peda$ i r
eax            0x30	0x30
ecx            0x8048a7f	0x8048a7f
edx            0x0	0x0
ebx            0x5	0x5

In short, it registers signal SIGTRAP to be handled by function at 0x8048a7f. We will call this function handle_SIGTRAP.

Now, if you observe disassembly of handle_SIGFPE and handle_SIGTRAP, there's a lot of code that is not decompiled into pseudocode. For example,

.text:08048A27      cmp     eax, 'FG03'
.text:08048A2C      jnz     short loc_8048A51
.text:08048A2E      mov     dword ptr [esp+38h], 0
.text:08048A36      mov     dword ptr [esp+34h], 64h
.text:08048A3E      mov     ecx, [esp+3Ch]
.text:08048A42      xor     eax, eax
.text:08048A44      mov     al, [ecx]
.text:08048A46      mov     [esp+40h], al
.text:08048A4A      add     dword ptr [esp+3Ch], 4
.text:08048A4F      jmp     short loc_8048A7B

only decompiled into

...
      if ( dword_804B084 == 'FG03' ) {
        result = *a12;
      }
      else {
...

So, we decided to do full dynamic analysis from here. Unfortunately, handle_SIGTRAP and handle_SIGFPE modify memory and can't be ignored. Therefore, we send SIGTRAP signal manually if we encounter int3 (we can do that by sending signal SIGTRAP command in GDB). For SIGFPE, we tried to cause division by zero error if possible. For example, in function sub_8048B14,

signed int __cdecl sub_8048B14(unsigned __int8 *a1) {
  dword_804B084 = 'FG03';
  if ( (*a1 % (100 - *a1)) >= 1 )
    exit(4);
...

we made sure that *a1 is 100 so it will cause SIGFPE. After a while doing dynamic analysis, we get the flag (without last checking part) xxxdxx_Ob50l3te_and_UnS0lv4bl3_l3v3l_f0r_CH3AT3R5.

Phew okay, now for the last checking part, function sub_80490A0.

_BOOL4 __cdecl sub_80490A0(_BYTE *a1) {
  _BYTE *v2; // [esp+Ch] [ebp-4h]

  v2 = a1 + 4;
  return a1[4] & 4
      && *v2 & 0x40
      && *v2 & 1
      && ~a1[5] == 145
      && !((a1[1] & 0x20) + (*a1 & 0x20) + (a1[2] & 0x20))
      && *a1 & 8
      && a1[2] & 4
      && a1[1] != *a1;
}

Since it's a constraint checking function, we can solve this with Z3.

from z3 import *

s = Solver()
xs = [BitVec('x%d' % i, 8) for i in xrange(6)]
for x in xs:
    s.add(Or(
        And(x >= ord('A'), x <= ord('Z')),
        And(x >= ord('a'), x <= ord('z')),
        And(x >= ord('0'), x <= ord('9')),
    ))

s.add(xs[4] & 4 != 0)
s.add(xs[4] & 0x40 != 0)
s.add(xs[4] & 1 != 0)
s.add(xs[5] == ord('n'))
s.add((xs[1] & 0x20) + (xs[0] & 0x20) + (xs[2] & 0x20) == 0)
s.add(xs[0] & 8 != 0)
s.add(xs[2] & 4 != 0)
s.add(xs[1] != xs[0])
s.add(xs[3] == ord('d'))

while s.check() == sat:
    m = s.model()
    print ''.join([chr(int(m[xs[i]].as_long())) for i in xrange(6)])
    s.add(Or([d() != m[d] for d in m]))

But there are more than one solutions that satisfy the constraints (CTF organizer had confirmed this). Since the 4th and 6th character is d and n, we guessed some possible words that make sense. We tried HIDdEn_Ob50l3te_and_UnS0lv4bl3_l3v3l_f0r_CH3AT3R5 and got the flag!

Bahasa Indonesia

Rangkuman

  • Patch binary sehingga bisa didekompilasi.
  • Pastikan signal SIGTRAP terpanggil dan jika memungkinkan buat kondisi sehingga SIGFPE terpanggil.
  • Lakukan dynamic analysis dengan GDB untuk mendapatkan flag.
  • Gunakan Z3 (dan sedikit tebakan) untuk mendapatkan bagian flag terakhir.

Langkah Detail

Diberikan sebuah binary ELF 32-bit. Banyak fungsi yang tidak bisa didekompilasi oleh IDA pada binary ini. Hal ini disebabkan oleh adanya instruksi int3 dengan opcode 0xCC.

.text:080486BB main:
...
.text:08048741           jnz     short loc_804872A
.text:08048743           mov     [ebp-68h], ecx
.text:08048743 ; ----------------------------------------------
.text:08048746           db 2 dup(0CCh)
.text:08048748 ; ----------------------------------------------
.text:08048748           cmp     dword ptr [ebp-68h], 32h
.text:0804874C           jnz     loc_8048890

Kami melakukan patch binary yaitu mengganti instruksinya menjadi nop dengan opcode 0x90 dengan hex editor. Setelah itu baru fungsi-fungsinya dapat didekompilasi oleh IDA. Sekarang kita lihat fungsi main.

int __cdecl main(int a1, char **a2) {
  int v2; // ecx
  unsigned int v3; // ebx
  int v4; // ST00_4
  int result; // eax
  char **v6; // [esp+0h] [ebp-7Ch]
  char v7; // [esp+18h] [ebp-64h]
  unsigned int v8; // [esp+60h] [ebp-1Ch]
  int *v9; // [esp+6Ch] [ebp-10h]

  v9 = &a1;
  v6 = a2;
  v8 = __readgsdword(0x14u);
  if ( a1 == 2 ) {
    ... // performs checking
  }
  else {
    puts("You might want to submit the password to gain access.");
    result = 1;
  }
  return result;
}

Binary ini membutuhkan satu parameter sebagai password untuk dilakukan pengecekan. Sekarang kita lihat fungsi sub_80488B2.

signed int __cdecl sub_80488B2(const char *a1) {
  size_t v1; // eax
  signed int i; // [esp+18h] [ebp-A0h]
  char v4; // [esp+1Ch] [ebp-9Ch]
  char v5[32]; // [esp+8Ch] [ebp-2Ch]
  unsigned int v6; // [esp+ACh] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  SHA256_Init(&v4);
  v1 = strlen(a1);
  SHA256_Update(&v4, a1, v1);
  SHA256_Final(v5, &v4);
  for ( i = 0; i <= 31; ++i )
  {
    if ( (v5[i] ^ 0x42) != *(i + 0x804B060) )
    {
      __asm { int     80h; LINUX - }
      return 0;
    }
  }
  return 1;
}

Fungsi ini menghitung hash SHA-256 dari password, melakukan operasi xor dengan 0x42, dan dicocokkan dengan 0x804B060 yang jika dihitung akan menghasilkan string Well done, but you were mislead.. Dari sini kami menduga bahwa banyak fungsi menjebak dan flag palsu pada binary.

Kami juga melihat ada instruksi int 0x80 (semacam syscall) pada fungsi tersebut. Kita lihat disassembly-nya.

.text:08048965                 movzx   eax, al
.text:08048968                 cmp     edx, eax
.text:0804896A                 jz      short loc_8048997
.text:0804896C                 movzx   edx, [ebp+var_A2]
.text:08048973                 mov     esi, offset sub_80489C6
.text:08048978                 xor     eax, eax
.text:0804897A                 mov     al, dl
.text:0804897C                 inc     eax
.text:0804897D                 push    8
.text:0804897F                 pop     ebx
.text:08048980                 push    0
.text:08048982                 push    0
.text:08048984                 push    0
.text:08048986                 push    esi
.text:08048987                 mov     ecx, esp
.text:08048989                 xor     edx, edx
.text:0804898B                 int     80h             ; LINUX -
.text:0804898D                 sub     esp, 10h
.text:08048990                 mov     eax, 0
.text:08048995                 jmp     short loc_80489AE

Kami menyadari bahwa IDA tidak dapat melakukan dekompilasi fungsi ini (dan banyak fungsi lainnya) secara lengkap. Dari sini kami memutuskan untuk melakukan dynamic analysis dan memasang breakpoint pada 0x0804898B untuk mengetahui syscall apa yang dipanggil.

$ gdb public/challenge
...
gdb-peda$ b *0x0804898B
Breakpoint 1 at 0x804898b
gdb-peda$ r asd
[----------------------------------registers-----------------------------------]
EAX: 0x43 ('C')
EBX: 0x8
ECX: 0xffffcda0 --> 0x80489c6 (push   ebp)
EDX: 0x0
ESI: 0x80489c6 (push   ebp)
EDI: 0x0
EBP: 0xffffce68 --> 0xffffcf08 --> 0x0
ESP: 0xffffcda0 --> 0x80489c6 (push   ebp)
EIP: 0x804898b (int    0x80)
EFLAGS: 0x200246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
...
Breakpoint 1, 0x0804898b in ?? ()
gdb-peda$

Nomor system call (EAX) adalah 0x43 yang merupakan sys_sigaction. Singkatnya, instruksi tersebut mendaftarkan signal handler dengan nomor signal (EBX) 0x8 yang merupakan SIGFPE, yang akan di-handle oleh fungsi (ECX) pada alamat 0x80489c6. SIGFPE adalah floating point exception yang terjadi jika terdapat error pada operasi aritmatika, seperti pembagian dengan 0.

Kita lihat fungsi 0x80489c6, yang akan kita sebut sebagai fungsi handle_SIGFPE.

int __cdecl handle_SIGFPE(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, unsigned __int8 *a12, int a13, int a14, int a15, int a16) {
  int v16; // eax
  int result; // eax

  if ( dword_804B084 == 'REGT' ) {
    v16 = sys_mprotect((main & 0xFFFFF000), 0x2000u, 7);
    result = a16 - 1;
    *result = 0x5B056A51;
    *(result + 4) = 0x8B58306A;
    *(result + 8) = 0x80CD944D;
    *(result + 12) = 89;
  }
  else {
    result = dword_804B084;
    if ( dword_804B084 != 'NOPP' ) {
      if ( dword_804B084 == 'FG03' ) {
        result = *a12;
      }
      else {
        result = dword_804B084;
        if ( dword_804B084 == 'FG41' )
          dword_804B084 = 'NOPP';
      }
    }
  }
  return result;
}

Terdapat kode yang menarik, jika dword_804B084 == 'REGT' fungsi tersebut akan memanggil mprotect dan mengubah memory pada alamat yang ditunjuk oleh a16 - 1. Kami menduga fungsi tersebut mengubah kodenya sendiri, lalu kami menggunakan pwntools untuk melihat disassembly-nya.

>>> from pwn import *
>>> print disasm(p32(0x5B056A51)+p32(0x8B58306A)+p32(0x80CD944D)+chr(89))
   0:   51                      push   ecx
   1:   6a 05                   push   0x5
   3:   5b                      pop    ebx
   4:   6a 30                   push   0x30
   6:   58                      pop    eax
   7:   8b 4d 94                mov    ecx,DWORD PTR [ebp-0x6c]
   a:   cd 80                   int    0x80
   c:   59                      pop    ecx

Terdapat int 0x80 lagi! Untuk mencapai kode ini, kita harus membuat dword_804B084 bernilai 'REGT' dan membuat error aritmatika sehingga signal SIGFPE akan terpanggil.

Sekarang kita kembali ke fungsi main.

int __cdecl main(int a1, char **a2) {
    ...
    dword_804B084 = 'REGT';
    if ( sub_80488B2(*a2) ) {
        if ( strlen(v6[1]) != 42 )
            exit(1);
    }
    else {
        v2 = 0;
        do
            v3 = v6[1][v2++];
        while ( 0x42424242 % v3 );
        if ( v2 == 50 ) {
            if ( sub_8048B14(v6[1] + 3) ) {
            if ( sub_8048D7D(v6, v6[1] + 16) ) {
        ...
    }
}

Fungsi sub_80488B2 tidak akan pernah bernilai true, dari sini kami menyimpulkan pengecekan panjang password, strlen(v6[1]) != 42, adalah palsu.

Pengecekan lebih lanjut jika v2 == 50 terpenuhi. Untuk memenuhi ini, cara gampangnya adalah membuat karakter ke-50 password kita bernilai B or ! (karena nilai tersebut habis membagi 0x42424242). Akan tetapi ingat, kita telah mempunyai handler untuk SIGFPE! Sehingga jika karakter ke-50 password kita adalah null byte dan tidak ada karakter B dan !, error karena pembagian dengan 0 akan terjadi dan handle_SIGFPE terpanggil. Selain itu, dword_804B084 juga telah bernilai 'REGT' untuk mencapai kode aneh (yang mengubah kode lain). Kami menyimpulkan bahwa panjang password adalah 49.

Kami memasang breakpoint pada 0x080489FC di mana kode tersebut dieksekusi dan menjalankan binary tersebut dengan password sepanjang 49 karakter.

$ gdb public/challenge
gdb-peda$ b *0x080489FC
Breakpoint 1 at 0x80489fc
gdb-peda$ r 1234567890123456789012345678901234567890123456789
Stopped reason: SIGFPE
0x08048737 in ?? ()
gdb-peda$ c
Breakpoint 1, 0x080489fc in ?? ()
gdb-peda$ i r
eax            0x8048736	0x8048736
...

Kode tersebut mengubah kode pada 0x8048736. Lalu lanjutkan analisisnya.

gdb-peda$ ni
0x08048a02 in ?? ()
...
gdb-peda$ ni
0x08048736 in ?? ()
gdb-peda$ x/15i $eip
=> 0x8048736:	push   ecx
   0x8048737:	push   0x5
   0x8048739:	pop    ebx
   0x804873a:	push   0x30
   0x804873c:	pop    eax
   0x804873d:	mov    ecx,DWORD PTR [ebp-0x6c]
   0x8048740:	int    0x80
   0x8048742:	pop    ecx
   0x8048743:	mov    DWORD PTR [ebp-0x68],ecx
   0x8048746:	int3
   0x8048747:	int3
   0x8048748:	cmp    DWORD PTR [ebp-0x68],0x32
   0x804874c:	jne    0x8048890
gdb-peda$ ni
...
0x08048740 in ?? ()
gdb-peda$ i r
eax            0x30	0x30
ecx            0x8048a7f	0x8048a7f
edx            0x0	0x0
ebx            0x5	0x5

Singkatnya, kode tersebut mendaftarkan signal SIGTRAP untuk di-handle oleh fungsi pada 0x8048a7f. Kita akan menyebut fungsi itu sebagai handle_SIGTRAP.

Jika kita lihat disassembly dari fungsi handle_SIGFPE dan handle_SIGTRAP, banyak kode yang tidak berhasil didekompilasi oleh IDA menjadi pseudocode. Sebagai contoh,

.text:08048A27      cmp     eax, 'FG03'
.text:08048A2C      jnz     short loc_8048A51
.text:08048A2E      mov     dword ptr [esp+38h], 0
.text:08048A36      mov     dword ptr [esp+34h], 64h
.text:08048A3E      mov     ecx, [esp+3Ch]
.text:08048A42      xor     eax, eax
.text:08048A44      mov     al, [ecx]
.text:08048A46      mov     [esp+40h], al
.text:08048A4A      add     dword ptr [esp+3Ch], 4
.text:08048A4F      jmp     short loc_8048A7B

hanya berhasil didekompilasi menjadi

      if ( dword_804B084 == 'FG03' ) {
        result = *a12;
      }
      else {

Dari sini kami memutuskan untuk melakukan dynamic analysis secara penuh. Sayangnya, handle_SIGTRAP dan handle_SIGFPE mengubah memory sehingga tidak dapat diabaikan begitu saja. Untuk itu, kami mengirim balik signal SIGTRAP secara manual jika kita mendapati instruksi int3. Hal ini dapat dilakukan dengan memberikan command signal SIGTRAP pada GDB. Untuk SIGFPE, kami mencoba untuk membuat error pembagian dengan 0 jika dimungkinkan. Contohnya, pada fungsi sub_8048B14,

signed int __cdecl sub_8048B14(unsigned __int8 *a1) {
  dword_804B084 = 'FG03';
  if ( (*a1 % (100 - *a1)) >= 1 )
    exit(4);
...

kami membuat *a1 bernilai 100 sehingga akan menyebabkan SIGFPE. Setelah beberapa saat dynamic analysis, kami mendapatkan potongan flag xxxdxx_Ob50l3te_and_UnS0lv4bl3_l3v3l_f0r_CH3AT3R5.

Huft oke, sekarang saatnya bagian pengecekan terakhir, fungsi sub_80490A0.

_BOOL4 __cdecl sub_80490A0(_BYTE *a1) {
  _BYTE *v2; // [esp+Ch] [ebp-4h]

  v2 = a1 + 4;
  return a1[4] & 4
      && *v2 & 0x40
      && *v2 & 1
      && ~a1[5] == 145
      && !((a1[1] & 0x20) + (*a1 & 0x20) + (a1[2] & 0x20))
      && *a1 & 8
      && a1[2] & 4
      && a1[1] != *a1;
}

Karena fungsi tersebut adalah pengecekan constraint, kita dapat menyelesaikannya dengan Z3.

from z3 import *

s = Solver()
xs = [BitVec('x%d' % i, 8) for i in xrange(6)]
for x in xs:
    s.add(Or(
        And(x >= ord('A'), x <= ord('Z')),
        And(x >= ord('a'), x <= ord('z')),
        And(x >= ord('0'), x <= ord('9')),
    ))

s.add(xs[4] & 4 != 0)
s.add(xs[4] & 0x40 != 0)
s.add(xs[4] & 1 != 0)
s.add(xs[5] == ord('n'))
s.add((xs[1] & 0x20) + (xs[0] & 0x20) + (xs[2] & 0x20) == 0)
s.add(xs[0] & 8 != 0)
s.add(xs[2] & 4 != 0)
s.add(xs[1] != xs[0])
s.add(xs[3] == ord('d'))

while s.check() == sat:
    m = s.model()
    print ''.join([chr(int(m[xs[i]].as_long())) for i in xrange(6)])
    s.add(Or([d() != m[d] for d in m]))

Akan tetapi, terdapat lebih dari solusi yang memenuhi constraint tersebut (pihak panitia juga telah mengonfirmasi hal ini). Karena karakter ke-4 dan ke-6 adalah huruf d and n, kami menebak beberapa kata yang masuk akal. Kami mencoba string HIDdEn_Ob50l3te_and_UnS0lv4bl3_l3v3l_f0r_CH3AT3R5 dan ternyata mendapatkan poin untuk flag!