ROP Emporium - write4
With basic knowledge of how the GOT and PLT work and how function calls go through them along with a basic understanding of the amd64 ABI calling convention we can start looking for real gadgets now. In fact in this assignment we’ll look at a really helpful way of loading arbitrary data into memory.
Exploring the binary⌗
Just like before, let’s start off by exploring the binary bit to get a feel for what we’re dealing with here:
jasper@ropper:~/ropemporium/write4$ checksec write4
[*] '/home/jasper/ropemporium/write4/write4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
jasper@ropper:~/ropemporium/write4$ rabin2 -i write4
[Imports]
Num Vaddr Bind Type Name
1 0x004005d0 GLOBAL FUNC puts
2 0x004005e0 GLOBAL FUNC system
3 0x004005f0 GLOBAL FUNC printf
4 0x00400600 GLOBAL FUNC memset
5 0x00400610 GLOBAL FUNC __libc_start_main
6 0x00400620 GLOBAL FUNC fgets
7 0x00000000 WEAK NOTYPE __gmon_start__
8 0x00400630 GLOBAL FUNC setvbuf
7 0x00000000 WEAK NOTYPE __gmon_start__
Previously the calls to system()
had /bin/cat flag.txt
as the argument setup by the binary, however that is not the case here as the assignment states:
Our first foray into proper gadget use. A call to system() is still present but we’ll need to write a string into memory somehow.
Which is quickly verified too:
Throughout these posts I’m exploring r2 myself too and while r2 has quite a few commands these are some of the most oft-used:
aaa
: analyse the binary in detailafl
: list local and imported functions
: seek to (address, function, string, etc)pdb
: disassemble basic block
A good resource I referenced was the Radare2 book along with the Radare2 wiki.
Looking for gadgets⌗
The challenge page for write4 explains how we can still go about getting our string into memory. For this assignment I went with /bin/cat flag.txt
however we might just as easily pop a shell or invoke any arbitrary command.
Whenever I need to load arbitrary data into the address space of a binary I’m exploiting I first look at the memory map of when it’s running to see the permissions:
The range from 0x00601000
to 0x00602000
is writable and part of it corresponds to the .bss
:
This section is empty in the on-disk ELF file, but it expands when loaded to the specified size (0x30
bytes here). It’s oftentimes a safe space to write into, so we can use 0x601060
as the destination for our string.
Now that we have a suitable location it’s time to look for a gadget that will help us write to this address. The basic format we’re looking for is:
or more precisely in Intel assembler format, any of the following:
These allow for writing 2, 4 or 8 bytes of data respectively. The bracket notation means “the address pointed to by this register”.
By putting the address of .bss
into this regA
register, we can write the N bytes of data in regB
to this address.
Using r2 we can search for gadgets with a regular expression:
We have two useful gadgets here:
0x00400820 4d893e mov qword [r14], r15
0x00400823 c3 ret
and
0x00400821 893e mov dword [rsi], edi
0x00400823 c3 ret
Notice how close the addresses are! Both of these are valid instructions and this is what makes the x86_64 instruction set so susceptible for ROP because the instruction are of variable width. Meaning, the instructions encoded as 4d893e
and 893e
are both valid despite being of different lengths. Furthermore, this is an instruction set comprised of a lot of instructions which makes it more likely that any given sequence of bytes is actually a valid sequence of instructions. ROP attacks make thankful use of these as gadgets. Platforms such as ARM are fixed width and don’t suffer from this problem/side-effect in part also because they’re RISC architectures.
Exploit time⌗
For this assignment I wrote two functions, write_to_mem()
and write_to_mem8()
. The latter felt a bit like cheating when using the string /bin//sh
which is exactly 8 bytes and used the gadget at 0x00400820
. So I used that initially to verify the approach.
Then I implemented the solution using the double word move which means it has to take into account the amount of data and write it to memory in chunks of 4 bytes. This is the done in the loop of write_to_mem()
. Once the destination memory has been prepared we pop the address into RDI and call system()
through the PLT:
Here is the final exploit: