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.
- Patch the binary so we can decompile.
- Ensure
SIGTRAP
signal is sent and always causeSIGFPE
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.
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!
- Patch binary sehingga bisa didekompilasi.
- Pastikan signal
SIGTRAP
terpanggil dan jika memungkinkan buat kondisi sehinggaSIGFPE
terpanggil. - Lakukan dynamic analysis dengan GDB untuk mendapatkan flag.
- Gunakan Z3 (dan sedikit tebakan) untuk mendapatkan bagian flag terakhir.
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!