ROP
ropasaurusrex (32- bit ROP chain)
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, strippedmain
int main(void) {
int res;
vuln();
res = write(1,&global_buf,4);
return res;
}first_func()
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>:
Note that founding addresses of GOT is even easier in gdb:
cyclic
cyclicTo 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:
leaking libc
libcSince the binary is x86, we need to put call arguments on the stack. We need the following stack layout:
...
sEBP / junk
sEIP / write in PLT (what we want to execute)
Frame of caller / cleaner gadget (cleans up the next three cells and puts a ret in order to jump at the address that follows the three stack cells just cleared, which is the address of the main
Frame of caller / arg #1 (1st argument of what we want to execute, the fd of the write target)
Frame of caller / arg #2 (2nd argument of what we want to execute, what we want to write to stdout)
Frame of caller / arg #3 (3rd argument of what we want to execute, how much to write)
Frame of caller / main address (return address after that the gadget cleans the stack)
...
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
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
emptyspaces (64-bit ROP chain)
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:
Moreover:
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
empty:
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:
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):
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:
Source code:
easyrop
Initial considerations
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:
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:
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:
array[...]
padding
...
...
* array[...] + 48
padding
sebp
junk
seip
g1 address
...
0 (stdin)
...
global variable address (/bin/sh location)
...
8 (#bytes to read)
...
0 (read syscall code)
...
g2 address
...
junk
...
g1 address
...
global variable address (/bin/sh location)
...
pointer to zero (rsi content)
...
pointer to zero (rdx content)
...
0x3b (execve syscall code)
...
g3 address
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, retg2:
syscall, nop, nop, pop rbp, retWe 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:
Last updated