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.