前置知识
关于fd_nextsize和bk_nextsize
1 |
|
编译:gcc -m32 fenpei.c -g -o fenpei
1 | pwndbg> b 14 |
注意当14行,这条语句运行前,还没有large bins,因为会先把free的chunk加入unsorted bins,然后当再次分配时,遍历unsorted,如果没有才把free的chunk加入到其该去的bins(这里就是large bins)。
1 | pwndbg> n |
结论:
- large bin里的chunk是按照从大到小排序的。
- 若chunk在large bin的末端,则其的fd_nextsize指向首部,也就是最大的chunk,否则,fd_nextsize指向的是比它小的chunk.
- 若chunk在large bin的首部,则其的bk_nextsize指向末端,也就是最小的chunk,否则,bk_nextsize指向的是比它大的chunk.
unlink
图中A1、B1、C1大小相同,是同一组chunk,A2是第二组,A3、B3大小相同,是第三组chunk。
1 | /* Take a chunk off a bin list */ |
large bins中的空闲chunk可能处于两个双向循环链表中,unlink时需要从两个链表中都删除,这里只分析large bin特有的删除操作,其他的参考我的另一篇文章
- if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0))
__builtin_expect((x),0)表示 x 的值为假的可能性更大,这是一个用于分值预测的命令,我们只要看里面的P->fd_nextsize != NULL即可。
只有当chunk为large bin且P->fd_nextsize != NULL 时才需要修改,即chunk是同组的第一个空闲chunk。 - assert (P->fd_nextsize->bk_nextsize == P);
assert (P->bk_nextsize->fd_nextsize == P);
这里进行两个检查。 - 根据具体情况适当设置fd_nextsize和bk_nextsize
第一种:
1 | if (FD->fd_nextsize == NULL) { |
即如果FD->fd_nextsize != NULL,说明FD是下一组尺寸相同的chunks的第一个chunk。
图示我要移除的P为A2,则FD是A3,FD的fd_nextsize!=NULL
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
第二种:
1 | if (FD->fd_nextsize == NULL) { \ |
如果FD->fd_nextsize == NULL,且P是仅有的唯一一组尺寸相同的 chunks的第一个chunk。
图示的chunk大小都相同,若P为A1,FD即B1。
则此时P->fd_nextsize仍为P,移除P后,FD就是第一个chunk,所以将FD的fd_nextsize和bk_nextsize都由NULL修改为指向它自己。
第三种:
1 | if (!in_smallbin_range (P->size) \ |
有多组chunks,且P为同组chunks的第一个chunk,且FD不是下一组尺寸相同的chunks的第一个chunk。
图示要移除的P为A2,则FD为B2。
1 | FD->fd_nextsize = P->fd_nextsize; |
FD继承P的fd_nextsize和bk_nextsize
1 | P->fd_nextsize->bk_nextsize = FD; |
修改P->fd_nextsize和P->bk_nextsize的指针
large bin的分配
下述内容来源《glibc内存管理ptmalloc源代码分析》p87及glibc2.12.1源码
这里略去之前fastbins的合并和对unsorted的检索。
当这些都不能分配合适的chunk的时候,就到了下面的large bin分配实现。
1 | /* |
其他
分析
准备工作
checksec
1 | sakura@ubuntu:~$ checksec 2ez4u |
去掉alarm
去掉之后,注意多nop几下……别让函数看上去断了,弄成我下面这样就行。
分析结构体
个人习惯先逆向分析一下结构体。
简单在注释里写了一下分析。
结论是:
- apple结构体
- 管理apple的数据结构
使用pwntools动态分析
动态分析时使用的脚本如下可以通过动态调试的方式检验自己的分析结果,对一开始还是蛮有帮助,熟悉了之后就不用了,这种菜单题的结构体大同小异。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
27from pwn import *
from ctypes import c_uint32
context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'DEBUG'
io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})
base_addr=0x555555554000
gdb.attach(io, 'b *0x%x' % (base_addr+0xD22))
def add(value, num, l, desc):
io.recvuntil('your choice:')
io.sendline('1')
io.recvuntil('color?(0:red, 1:green):')
io.sendline('0')
io.recvuntil('value?(0-999):')
io.sendline(str(value))
io.recvuntil('num?(0-16)')
io.sendline(str(num))
io.recvuntil('description length?(1-1024):')
io.sendline(str(l))
io.recvuntil('description of the apple:')
io.sendline(desc)
pass
add(1,2,0x60,'A'*0x60)
io.recvuntil('your choice:')
io.sendline('5')
解释一下pwntools里面的几条语句
context.log_level = 'DEBUG'
打开debug,可以看到自己的发送和接收(如图)io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})
代表使用指定的libc文件去链接,不过要注意一下,因为ld.so的版本原因,跨版本指定libc一般是会失败的,所以这题的话,请使用ubuntu16.04
如图:
1 | parallels@ubuntu:~/ctf/chal$ python m_struct.py |
另外解释一下,base_addr=0x555555554000
这是代码段的基地址(这里主要就是用作调试,所以本地调试需要关闭ASLR,不然这个地址会变化。)1
2sudo su
echo 0 > /proc/sys/kernel/randomize_va_space因为可以看到文件里都是按照偏移来写的地址。
gdb.attach(io, 'b *0x%x' % (base_addr+0xD22))
使用gdb attach调试,b *是下断,这里我在malloc下断,attach上去之后再在里面下断也行,没区别。另外使用IDA对二进制文件进行逆向分析的时候,可以把基地址重新选定,如下操作,可以看到现在基地址已经选定了。(注意这种情况下是在关闭ASLR调试时可以用,服务器上的地址并不是这个)
漏洞分析
添加函数在上面已经分析了。
查看函数
删除函数
修改函数
可以看出在free chunk后并没有将存储指针的全局变量删除,还能够对其进行编辑,典型的UAF漏洞。
利用
leak heap
首先构造两个大小在同一个bins中的large chunk,将其释放后,这两个chunk先进入unsorted bins中,再申请一个不满足这两个chunk大小的chunk,则unsorted bins中的这两个chunk将会进入large bins中。
同时fd_nextsize和bk_nextsize将被赋值,因为指向这两个chunk的指针还存放在全局变量中,所以依然可以打印(UAF)
调试脚本如下:
1 | #!/usr/bin/env python2.7 |
gdb挂上后,先查看全局变量,找到堆
1 | pwndbg> x /32gx 0x555555756040 |
添加了一个large bin之后。
1 | pwndbg> c |
1 | pwndbg> x /32gx 0x555555756040 |
则index 9
1 | pwndbg> x /20gx 0x00005555557577f0 |
index b
1 | pwndbg> x /20gx 0x0000555555757c40 |
即:chunk_size链为
1 | large bin: index b-->index 9(-->index b) |
此处–>均代表bk_nextsize
同时由fd和bk可以看出在large bin链中的顺序,判据如下图:
index b的bk为main_arena,index 9的fd为main_arena
顺便index b大小为0x3e0,index 9的大小为0x3d0,b>a,这也证明了large bin确实是从大到小排序的。
1 | [DEBUG] Received 0x94 bytes: |
leak libc
1 | #!/usr/bin/env python2.7 |
这部分的调试过程比较简单,就在我脚本里那个地方下断,就可以在每次添加删除或修改执行完,返回生成菜单的代码处断下,查看每次修改结果,不再详细描述。
利用思路需要再整理一下,我们每次能够改变的是desc
在large bin attack开始前断下,并查看修改前的index b。
1 | Breakpoint 1 at 0x55555555524e |
1 | target_addr = HEAP+0xb0 # 在index 1中 |
修改后,执行完edit(0xb, p64(chunk1_addr)),再在生成菜单的代码前断下,此时bk_nextsize指向伪造的chunk1,chunk1将在chunk1_addr这个地址构造。
最终构造出:
绕过p->fd->bk=p和p->bk->fd=p
以及p->bk_nextsize->fd_nextsize=p和p->fd_nextsize->bk_nextsize=p
同时要注意
1 | pwndbg> x /20gx 0x0000555555757c30 |
1 | pwndbg> x /20gx 0x0000555555757098 |
注意这句话edit(0x7, ‘7’*0x198+p64(0x410)+p64(0x411))
它是为了保证size的大小一致,在“相邻”的下一个chunk设置好prev_size。
chunk1的addr为130,加上size即410就是540.
1 | pwndbg> x /20gx 0x000055555575754 |
构造好之后,就是先删除6和3这两个大小在small bins范围里的chunk。
顺便一提,small bins是FIFO的规则,所以同一个链表中先被释放的chunk会先被分配出去。
然后再add(0x3f0, ‘3’*0x30+p64(0xdeadbeefdeadbeef))
因为我们之前说过了large bins的分配,首先找到first(bins),也就是free chunk的第一个,因为这是这条链里最大的,这里就是c30,然后从它的bk_nextsize开始遍历,即从130开始遍历,这里的130是我们伪造好的,它的大小为410,就被分配出去了。
检查一下,确实是这样,它被分配到了index 3的位置。
1 | pwndbg> x /32gx 0x555555756040 |
也可以看出触发了unlink
1 | pwndbg> x /6gx 0x0000555555757c30 |
因为之前的index 3即0x0000555555757190就是small bin,而它被包含在我们伪造的chunk的大小(130-540)中,所以被leak出来,其fd的值就在libc中。
1 | pwndbg> x /50gx 0x0000555555757130 |
leak出的libc并不是基地址,还要减去偏移,我不大清楚应该这么算,但是关了ASLR之后,我们可以看到基地址,先leak一遍然后算出编译,然后再重新运行leak脚本,把这个偏移值减去即可,有人知道怎么算的话,可以告知我一下~谢谢。
最终leak出的libc地址为:0x00007ffff7a0d000
覆盖__free_hook指针
利用这个malloc出来的chunk来修改fastbin的fd
通过修改fd,来malloc出top前一块空间,然后这样就可以修改main_arena上的top为free_hook上面一些的地方。
通过几次malloc,修改free_hook为system的地址
这部分的堆构造比较复杂,不过只要注意到是怎么在我们malloc出的fake_chunk里free出一个fastbin,后面就可以通过更改fastbin的fd指针,结合UAF实现任意地址写了。
exp
1 | #!/usr/bin/env python2.7 |