hctf2016_fheap
本题为一道经典的控制堆块的题目,对于这类题目通常的方法是将控制堆块申请出来当做用户堆块来使用,向其写入特定数据来篡改其中的函数指针。
本题为一道经典的控制堆块的题目,对于这类题目通常的方法是将控制堆块申请出来当做用户堆块来使用,向其写入特定数据来篡改其中的函数指针。
通过这道题的学习与收获有:
1、atol函数放入的数据应该是打包之前的,而非是打包之后的数据
2、exit函数执行流程,exit函数的调用流程exit函数—>run_exit_handlers函数—>_dl_fini函数—> rtld_lock_unlock_recursive指针 如果我们能够将最后的指针所指向的内容修改为one_gadget,那么即可获取shell。这个劫持exit_hook的可取之处在于,程序正常结束的话,最后都会调用这个exit函数
3、学会了重新绑定程序所对应的libc动态库(patch libc和ld)
4、用这个方法可以在开了PIE的程序中下断点。gdb.attach(p, ‘b * $rebase(0xd63)\nc’)
5、one_gadget加上参数-l2可以搜索更多的one_gadget。
6、即使相同的libc库,在小版本不同的情况下,某些内容的偏移也是不一样的。
由于本人比较菜,同时学pwn的时间不是太久,因此iscc的很多堆还没有能力去做,只把比赛的栈题给做完了 赛后又做了一下unlink那道题。其他堆题目前还没有复现。(有一个格式化字符串太简单了,我就不写wp了)
本题考察了 unlink
在之前做这道题的时候SWPUCTF_2019_p1KkHeap,就受到了tcache dup+tcache poisoning来爆破申请tcache_perthread_struct结构体的启发,结果在做这道题的时候就遇见了这种手法。由于还需要打io_leak再次爆破半个字节,因此这种手法成功的概率只有1/256。
通过本题的学习与总结有:
之前一直以为ret2libc必须得返回到原本的输入函数处,再次输入一次getshell。但有时候我们重新返回到原本的输入函数可能会出现一些问题,因此我们可以打一个栈迁移+rop执行read。就是先覆盖rbp为bss段上的地址,然后执行puts函数泄露libc,接着执行read函数往bss段上输入数据,最后执行leave ret完成栈迁移从而将执行流劫持到bss段上
插入到栈里的canary是从TLS结构体中的stack_guard成员变量赋值过来的(而函数返回时,会将栈里的canary与TLS中的stack_guard做对比)。主线程中的TLS通常位于mmap映射出来的地址空间里,而位置也比较随机,覆盖的可能性不大;子线程中的TLS则位于线程栈的顶部(高地址处),而这个子线程栈通常也是mmap映射出来的一段内存,这就给了我们栈溢出控制子线程中的TLS机会
TLS(Thread Local Storage) 线程局部存储。本身是一种机制,简单来说就是多个线程访问同一个全局变量或者静态变量可能会发生冲突,而这个机制类似于让每个线程都备份了一份全局变量或者静态变量,当前线程只能修改自己这份全局变量或者静态变量并不会影响其他线程的全局变量以及静态变量。
在glibc实现中,TLS被指向一个segment register fs(x86-64上),它的结构tcbhead_t定义如下:
typedef struct |
而上面的stack_guard也就是放到栈里的canary,而在程序里看见的这行代码
xor rdx, fs:28h
中的fs寄存器也就指向了TLS这个结构体,而偏移0x28的位置正好是stack_guard,canary是来自于内核生成的一个随机数。
最后要说一下这个子线程栈和父线程内存的关系。每个线程都会有自己单独的栈区,而子线程的栈区通常都是调用了mmap映射了一段内存。在父进程里我们依然可以看到这片内存
通过这道对于tcache机制有了一定的认识。
tcache_perthread_struct结构体是用来管理tcache链表的,它的源码如下:
typedef struct tcache_entry |
这个结构体中有两个成员变量,一个是64字节的counts数组。该数组的每个元素都各自对应了64个tcache链表其中一条上chunk的数量(最大为7)。另一个成员变量是一个结构体指针数组,该数组的大小为64*8字节,该数组存放的是每条tcache链表的头指针。