Headache
9 minutes to read
We have a binary called headache
:
$ file headache
headache: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
Basic reconnaissance
When we run it, it starts writing Initialising
and after some seconds, we are asked to enter a key:
$ ./headache
Initialising.....
Enter the key: asdf
Login Failed!
Let’s debug a bit using GDB. Once we are prompted to enter the key, we can hit ^C
and set a breakpoint after the read
instruction:
$ gdb -q headache
Reading symbols from headache...
(No debugging symbols found in headache)
gef➤ run
Starting program: ./headache
Initialising.....
Enter the key: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ecafd2 in __GI___libc_read (fd=0x0, buf=0x55555555a7f0, nbytes=0x400) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
gef➤ backtrace
#0 0x00007ffff7ecafd2 in __GI___libc_read (fd=0x0, buf=0x55555555a7f0, nbytes=0x400) at ../sysdeps/unix/sysv/linux/read.c:26
#1 0x00007ffff7e4db9f in _IO_new_file_underflow (fp=0x7ffff7fa9980 <_IO_2_1_stdin_>) at libioP.h:948
#2 0x00007ffff7e4ef86 in __GI__IO_default_uflow (fp=0x7ffff7fa9980 <_IO_2_1_stdin_>) at libioP.h:948
#3 0x00007ffff7e4086c in __GI__IO_getline_info (fp=fp@entry=0x7ffff7fa9980 <_IO_2_1_stdin_>, buf=buf@entry=0x7fffffffe5a0 "", n=n@entry=0x1d, delim=delim@entry=0xa, extract_delim=extract_delim@entry=0x1, eof=eof@entry=0x0) at iogetline.c:60
#4 0x00007ffff7e4096c in __GI__IO_getline (fp=fp@entry=0x7ffff7fa9980 <_IO_2_1_stdin_>, buf=buf@entry=0x7fffffffe5a0 "", n=n@entry=0x1d, delim=delim@entry=0xa, extract_delim=extract_delim@entry=0x1) at iogetline.c:34
#5 0x00007ffff7e3f6ca in _IO_fgets (buf=0x7fffffffe5a0 "", n=0x1e, fp=0x7ffff7fa9980 <_IO_2_1_stdin_>) at iofgets.c:53
#6 0x0000555555555478 in ?? ()
#7 0x0000000000000000 in ?? ()
gef➤ break *0x0000555555555478
Breakpoint 1 at 0x555555555478
gef➤ x/100i 0x0000555555555478
0x555555555478: lea rax,[rbp-0x30]
0x55555555547c: lea rsi,[rip+0x1bc3] # 0x555555557046
0x555555555483: mov rdi,rax
0x555555555486: call 0x555555555130
0x55555555548b: movabs rax,0x3a0e551c0f31312a
0x555555555495: movabs rdx,0x55112d1c12460d02
0x55555555549f: mov QWORD PTR [rbp-0x50],rax
0x5555555554a3: mov QWORD PTR [rbp-0x48],rdx
0x5555555554a7: mov DWORD PTR [rbp-0x40],0x19145c51
0x5555555554ae: mov BYTE PTR [rbp-0x3c],0x0
0x5555555554b2: lea rax,[rbp-0x30]
0x5555555554b6: mov rdi,rax
0x5555555554b9: call 0x555555555060
0x5555555554be: cmp rax,0x14
0x5555555554c2: je 0x5555555554da
0x5555555554c4: lea rdi,[rip+0x1b7d] # 0x555555557048
0x5555555554cb: call 0x555555555050
0x5555555554d0: mov edi,0x1
0x5555555554d5: call 0x555555555150
0x5555555554da: mov DWORD PTR [rbp-0x8],0x0
0x5555555554e1: jmp 0x555555555526
0x5555555554e3: mov edx,DWORD PTR [rbp-0x8]
0x5555555554e6: lea rax,[rbp-0x50]
0x5555555554ea: mov esi,edx
0x5555555554ec: mov rdi,rax
0x5555555554ef: call 0x55555555535f
0x5555555554f4: mov eax,DWORD PTR [rbp-0x8]
0x5555555554f7: cdqe
0x5555555554f9: movzx edx,BYTE PTR [rbp+rax*1-0x50]
0x5555555554fe: mov eax,DWORD PTR [rbp-0x8]
0x555555555501: cdqe
0x555555555503: movzx eax,BYTE PTR [rbp+rax*1-0x30]
0x555555555508: cmp dl,al
0x55555555550a: je 0x555555555522
0x55555555550c: lea rdi,[rip+0x1b35] # 0x555555557048
0x555555555513: call 0x555555555050
0x555555555518: mov edi,0x1
0x55555555551d: call 0x555555555150
0x555555555522: add DWORD PTR [rbp-0x8],0x1
0x555555555526: cmp DWORD PTR [rbp-0x8],0x13
0x55555555552a: jle 0x5555555554e3
0x55555555552c: lea rdi,[rip+0x1b23] # 0x555555557056
0x555555555533: call 0x555555555050
0x555555555538: mov edi,0x0
0x55555555553d: call 0x555555555150
0x555555555542: push rbp
0x555555555543: mov rbp,rsp
0x555555555546: push rbx
0x555555555547: sub rsp,0x28
0x55555555554b: mov QWORD PTR [rbp-0x28],rdi
0x55555555554f: mov DWORD PTR [rbp-0x14],0x0
0x555555555556: jmp 0x555555555580
0x555555555558: mov eax,DWORD PTR [rbp-0x14]
0x55555555555b: movsxd rdx,eax
0x55555555555e: mov rax,QWORD PTR [rbp-0x28]
0x555555555562: add rax,rdx
0x555555555565: movzx ecx,BYTE PTR [rax]
0x555555555568: mov eax,DWORD PTR [rbp-0x14]
0x55555555556b: movsxd rdx,eax
0x55555555556e: mov rax,QWORD PTR [rbp-0x28]
0x555555555572: add rax,rdx
0x555555555575: xor ecx,0x61
0x555555555578: mov edx,ecx
0x55555555557a: mov BYTE PTR [rax],dl
0x55555555557c: add DWORD PTR [rbp-0x14],0x1
0x555555555580: mov eax,DWORD PTR [rbp-0x14]
0x555555555583: movsxd rbx,eax
0x555555555586: mov rax,QWORD PTR [rbp-0x28]
0x55555555558a: mov rdi,rax
0x55555555558d: call 0x555555555060
0x555555555592: cmp rbx,rax
0x555555555595: jb 0x555555555558
0x555555555597: nop
0x555555555598: add rsp,0x28
0x55555555559c: pop rbx
0x55555555559d: pop rbp
0x55555555559e: ret
0x55555555559f: push rbp
0x5555555555a0: mov rbp,rsp
0x5555555555a3: sub rsp,0x50
0x5555555555a7: mov QWORD PTR [rbp-0x38],rdi
0x5555555555ab: mov QWORD PTR [rbp-0x40],rsi
0x5555555555af: mov QWORD PTR [rbp-0x48],rdx
0x5555555555b3: mov rax,QWORD PTR [rbp-0x40]
0x5555555555b7: add rax,0x2
0x5555555555bb: movabs rdx,0xaaaaaaaaaaaaaaab
0x5555555555c5: mul rdx
0x5555555555c8: mov rax,rdx
0x5555555555cb: shr rax,1
0x5555555555ce: lea rdx,[rax*4+0x0]
0x5555555555d6: mov rax,QWORD PTR [rbp-0x48]
0x5555555555da: mov QWORD PTR [rax],rdx
0x5555555555dd: mov rax,QWORD PTR [rbp-0x48]
0x5555555555e1: mov rax,QWORD PTR [rax]
0x5555555555e4: mov rdi,rax
0x5555555555e7: call 0x5555555550c0
0x5555555555ec: mov QWORD PTR [rbp-0x18],rax
0x5555555555f0: cmp QWORD PTR [rbp-0x18],0x0
0x5555555555f5: jne 0x555555555601
0x5555555555f7: mov eax,0x0
Above we can see a bit of assembly code. Let’s continue:
gef➤ continue
Continuing.
ABCD
Breakpoint 1, 0x0000555555555478 in ?? ()
Now, we can use ni
to go through the instructions step by step. Eventually, we will reach this instruction:
gef➤ x/i $rip
=> 0x5555555554be: cmp rax,0x14
gef➤ p/x $rax
$1 = 0x4
gef➤ x/s $rdx
0x7fffffffe5a0: "ABCD"
This points out that the length of the flag must be 0x14
(20 characters). So let’s set a breakpoint here and run the program again:
gef➤ break *0x5555555554be
Breakpoint 2 at 0x5555555554be
gef➤ run
Starting program: ./headache
Initialising.....
Enter the key: AAAAAAAAAAAAAAAAAAAA
Breakpoint 1, 0x0000555555555478 in ?? ()
We continue the first breakpoint:
gef➤ continue
Continuing.
Breakpoint 2, 0x00005555555554be in ?? ()
And we arrive at the length check again:
gef➤ x/i $rip
=> 0x5555555554be: cmp rax,0x14
gef➤ p/x $rax
$2 = 0x14
gef➤ x/s $rdx
0x7fffffffe5a0: 'A' <repeats 20 times>
Everything correct for the moment. Then, the program enters in a loop where some functions are called. Eventually, we can notice the instruction cmp dl, al
, where $rax
contains a character of our key (A
, 0x41
) and $rdx
contains the expected character (H
, 0x48
):
gef➤ x/i $rip
=> 0x555555555508: cmp dl,al
gef➤ p/x $rax
$3 = 0x41
gef➤ p/x $rdx
$4 = 0x48
Automation
So, let’s set a breakpoint and modify $rax
to equal $rdx
:
gef➤ break *0x555555555508
Breakpoint 3 at 0x555555555508
gef➤ set $rax = $rdx
gef➤ continue
Continuing.
Breakpoint 3, 0x0000555555555508 in ?? ()
Then we have the next expected character in $rdx
(T
, 0x54
):
gef➤ p/x $rax
$5 = 0x41
gef➤ p/x $rdx
$6 = 0x54
gef➤ set $rax = $rdx
gef➤ continue
Continuing.
Breakpoint 3, 0x0000555555555508 in ?? ()
And the next one (B
, 0x42
):
gef➤ p/x $rax
$7 = 0x41
gef➤ p/x $rdx
$8 = 0x42
So we can assume that it will be the flag. We can do it manually, but it will be better to automate the process using a Python script with pwntools
:
$ python3 solve.py
[+] Starting local process '/usr/bin/gdb': pid 1677093
[+] Flag: HTB{w0w_th4ts_c000l}
[*] Stopped process '/usr/bin/gdb' (pid 1677093)
And we find a flag. But it is not correct. Actually, there’s another fake flag inside the binary:
$ strings headache | grep HTB
HTB{not_so_easy_lol}
Actually, while analyzing again all the steps, we notice that not only $rdx
have the characters for a flag, but also $rsi
. However, the flag is still incorrect. However, register $rcx
is also holding a single byte. If we take it, we see an actual key:
$ python3 solve.py
[+] Starting local process '/usr/bin/gdb': pid 1709433
[+] Flag: bestkeyeverforrealxd
[*] Stopped process '/usr/bin/gdb' (pid 1709433)
But it is useless. So, this is going to be a headache… We might notice that the key is not correct outside GDB:
$ ./headache
Initialising.....
Enter the key: HTB{w0w_th4ts_c000l}
Login Failed!
$ gdb -q headache
Reading symbols from headache...
(No debugging symbols found in headache)
gef➤ run
Starting program: ./headache
Initialising.....
Enter the key: HTB{w0w_th4ts_c000l}
Login success!
[Inferior 1 (process 1688844) exited normally]
Bypassing anti-debugging methods
So, there must be some anti-debugging methods. Let’s try to bypass them. For that, let’s analyze the assembly code that comes at the beginning:
$ gdb -q headache
Reading symbols from headache...
(No debugging symbols found in headache)
gef➤ start
Stopped due to shared library event (no libraries added or removed)
[*] PIC binary detected, retrieving text base address
[+] Breaking at entry-point: 0x555555555190
gef➤ x/70i $rip
=> 0x555555555190: xor ebp,ebp
0x555555555192: mov r9,rdx
0x555555555195: pop rsi
0x555555555196: mov rdx,rsp
0x555555555199: and rsp,0xfffffffffffffff0
0x55555555519d: push rax
0x55555555519e: push rsp
0x55555555519f: lea r8,[rip+0xdea] # 0x555555555f90
0x5555555551a6: lea rcx,[rip+0xd83] # 0x555555555f30
0x5555555551ad: lea rdi,[rip+0xdfb] # 0x555555555faf
0x5555555551b4: call QWORD PTR [rip+0x3e26] # 0x555555558fe0
0x5555555551ba: hlt
0x5555555551bb: nop DWORD PTR [rax+rax*1+0x0]
0x5555555551c0: lea rdi,[rip+0x3f81] # 0x555555559148
0x5555555551c7: lea rax,[rip+0x3f7a] # 0x555555559148
0x5555555551ce: cmp rax,rdi
0x5555555551d1: je 0x5555555551e8
0x5555555551d3: mov rax,QWORD PTR [rip+0x3dfe] # 0x555555558fd8
0x5555555551da: test rax,rax
0x5555555551dd: je 0x5555555551e8
0x5555555551df: jmp rax
0x5555555551e1: nop DWORD PTR [rax+0x0]
0x5555555551e8: ret
0x5555555551e9: nop DWORD PTR [rax+0x0]
0x5555555551f0: lea rdi,[rip+0x3f51] # 0x555555559148
0x5555555551f7: lea rsi,[rip+0x3f4a] # 0x555555559148
0x5555555551fe: sub rsi,rdi
0x555555555201: sar rsi,0x3
0x555555555205: mov rax,rsi
0x555555555208: shr rax,0x3f
0x55555555520c: add rsi,rax
0x55555555520f: sar rsi,1
0x555555555212: je 0x555555555228
0x555555555214: mov rax,QWORD PTR [rip+0x3dd5] # 0x555555558ff0
0x55555555521b: test rax,rax
0x55555555521e: je 0x555555555228
0x555555555220: jmp rax
0x555555555222: nop WORD PTR [rax+rax*1+0x0]
0x555555555228: ret
0x555555555229: nop DWORD PTR [rax+0x0]
0x555555555230: cmp BYTE PTR [rip+0x3f31],0x0 # 0x555555559168
0x555555555237: jne 0x555555555268
0x555555555239: push rbp
0x55555555523a: cmp QWORD PTR [rip+0x3db6],0x0 # 0x555555558ff8
0x555555555242: mov rbp,rsp
0x555555555245: je 0x555555555253
0x555555555247: mov rdi,QWORD PTR [rip+0x3e7a] # 0x5555555590c8
0x55555555524e: call 0x555555555180
0x555555555253: call 0x5555555551c0
0x555555555258: mov BYTE PTR [rip+0x3f09],0x1 # 0x555555559168
0x55555555525f: pop rbp
0x555555555260: ret
0x555555555261: nop DWORD PTR [rax+0x0]
0x555555555268: ret
0x555555555269: nop DWORD PTR [rax+0x0]
0x555555555270: jmp 0x5555555551f0
0x555555555275: push rbp
0x555555555276: mov rbp,rsp
0x555555555279: sub rsp,0x40
0x55555555527d: mov rax,0x1
0x555555555284: mov rdi,0x1
0x55555555528b: mov rdx,0x64
0x555555555292: xor rax,rdx
0x555555555295: xor rdi,rdi
0x555555555298: xor rdx,rdx
0x55555555529b: syscall
0x55555555529d: mov DWORD PTR [rbp-0x4],eax
0x5555555552a0: movabs rax,0x68686a4d6c5a575a
0x5555555552aa: movabs rdx,0x784d575a79636a4e
0x5555555552b4: mov QWORD PTR [rbp-0x40],rax
There’s a suspicious syscall
. If we analyze it, we see that $rdx
is set to 0x64
and $rax
is set to 0x1
and then we have xor rax, rdx
, so $rax = 0x64 ^ 0x1 = 0x65
. If we take a look at a syscall
table for 64-bit systems, we have a sys_ptrace
. This syscall
traces the process and is able to check if the process is being debugged. So, we can bypass this by setting a breakpoint at the syscall
address (0x55555555529b
) and skipping it using jump 0x55555555529d
, so that we go to the next instruction.
We can also patch the binary to remove the syscall
instruction (turn 0f05
to 9090
, nop
instructions). But it is useless, it will behave the same way as in GDB:
$ xxd -p headache | tr -d \\n | sed s/0f05/9090/g | xxd -r -p > headache_patched
$ chmod +x headache_patched
$ ./headache_patched
Initialising.....
Enter the key: HTB{w0w_th4ts_c000l}
Login success!
Therefore, we might think that sys_ptrace
is there in order to make the flag. We need to trick the binary and make it think that it is outside a debugger. This can be done in GDB using catch syscall ptrace
. Once the syscall
has been called, we need to set $rax
to 0
as if the syscall
has been completed successfully.
In total, there are four sys_ptrace
calls. After that, we can continue the program execution. At some places, we will see another fake flag:
gef➤ x/s $rdx
0x7fffffffe530: "HTB{th4t_w4s_h4rd}"
And eventually, we will reach another cmp edx, eax
instruction inside a loop.
Flag
This time, the flag is the real one. So we can update the Python script with the sys_ptrace
bypass and get the flag:
$ python3 solve.py
[+] Starting local process '/usr/bin/gdb': pid 67724
[+] Flag: HTB{l4yl3_w4s_h3r3!}
[*] Stopped process '/usr/bin/gdb' (pid 67724)
$ ./headache
Initialising.....
Enter the key: HTB{l4yl3_w4s_h3r3!}
Login success!
The full script can be found in here: solve.py
.