和re-alloc一样,只不过开了PIE和RELRO,got表改不了了,要利用stdout结构体来leak libc,因为第一次做,而且过程稍微有些复杂,所以记录一下。
题目描述
pwnable.tw上的一道题,也就是在re-alloc上保护全开。
1 | Arch: amd64-64-little |
功能就不赘述了,因为binary和re-alloc一摸一样。
相关知识点
利用stdout结构体leak libc
当binary使用过puts函数时,会依照以下调用链调用到_IO_new_file_overflow
:
1 | _IO_puts --> _IO_sputn --> _IO_new_file_xsputn --> _IO_new_file_overflow |
分析_IO_new_file_overflow
源码:
1 | int _IO_new_file_overflow (FILE *f, int ch) |
在_IO_new_file_overflow
中,我们要利用的就是其中的_IO_do_write
。
在输出时,如果具有缓冲区,会输出_IO_write_base
开始的缓冲区内容,直到_IO_write_ptr
(也就是将_IO_write_base
一直到_IO_write_ptr
部分的值当做缓冲区,在无缓冲区时,两个指针指向同一位置,位于该结构体附近,也就是libc中),但是在setbuf
后,理论上会不使用缓冲区。然而如果能够修改_IO_2_1_stdout_
结构体的flags
部分,使得其认为stdout
具有缓冲区,再将_IO_write_base
处的值进行partial overwrite
,就可以泄露出libc地址了。
为了设置对应的flags
的值,需要进一步分析_IO_do_write
(其实就是_IO_new_do_write
):
1 | int _IO_new_do_write (FILE *fp, const char *data, size_t to_do) |
综上可以得到,flags
需要满足的条件为:
1 | _flags = 0xfbad0000 // Magic number |
同时可以将_IO_read_ptr
, _IO_read_end
, _IO_read_base
, _IO_write_base
设置为:
1 | _IO_read_ptr = 0; |
然后就可以根据输出的数据leak出libc地址了。
利用思路
- 利用
alloc
功能在size=0
时存在的uaf,以及realloc
中当size < old_size
而触发的free(remainder)
操作,形成chunk overlap,然后覆盖chunk的size至足够放进unsorted bin中(这里因为要爆破而且连远程的延迟比较大,所以尽量小)。 - 为了保证能够顺利地将chunk放进unsorted bin中,需要绕过这里的检查,也就是需要先free掉足够大小的chunk,保证该需要放进unsorted bin的nextchunk的prev_inuse area为1。由于每次分配最大的size为0x78也就是chunk的size最大为0x80,这里要进行多次的
alloc(0x68)
,realloc(0x78)
,free()
操作(为了防止tcache中刚被free掉的chunk又被取出来)。直到nextchunk正好指向size为0x80的fastbin。1
2if (__glibc_unlikely (!prev_inuse(nextchunk)))
malloc_printerr ("double free or corruption (!prev)"); - 此外,由于后续的操作需要保持unsorted bin中和tcache bin中同时存在该伪造的unsorted bin,从而能从该tcache中分配到位于
stdout
结构体的内存,所以要在前面提到的free(remainder)
形成的tcache bin初形成该chunk的double free,从而在分配该处的chunk时仍能将它保留在tcache中。1
2
3
4
5
6
7tcache bin ==> +--------+ <-- victim_chunk tcache bin ==> +--------+ <-- same victim_chunk
| | after malloc | |
+--------+<--+ ===================> +--------+
|fd | | | | |
+--------+ | +--------+
| |
+----------+ - 进一步地,由于
alloc
会对输入地字符串强制添加末尾\x00
,从而会将上一步中提到的double free链(也就是该tache bin的fd)的低字节覆盖为\x00
,这里需要将该chunk的地址保持为低字节是\x00
,从而即使低字节被覆盖也不影响double free链,而做法就是在最开始得时候分配一定size的chunk并free到tcache中去(其实这里的chunk在最后的exploite也会用到,因为那时unsorted bin已经被破坏了,不能分配tcache或者fastbin中没有的chunk,否则会造成从unsorted bin中取而报错)。 - 在伪造好相应的chunk后,分配并释放到unsorted bin中,再用uaf进行partial overwrite
unsorted bin->fd
为stdout
(bruteforce 4 bits),然后再从相应tcache bin中取出该chunk,使得tcache bin指向stdout1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23tcache bin ==> +--------+ <--victim_chunk |
| | |
+--------+ |
|fd | | |
+--------+ |
| |
+----------> +--------+ <--stdout |
|_flags | |
+--------+ |
| | |
+--------+ | after malloc
------------------------------------------------------ + =============> tcache bin ==> +--------+ <--stdout
unsorted bin ==> +--------+ <--same victim_chunk | |_flags |
| | | +--------+
+--------+ | | |
|fd | bk |--------> main_arena | +--------+
+--------+ |
| |
+----------> +--------+ <--stdout |
|_flags | |
+--------+ |
| | |
+--------+ | - 这个时候只要分配stdout出的chunk就能修改相应的stdout结构体,达到输出数据从而leak libc的目的。
- 之后因为unsorted bin被破坏的缘故,并且仅能使用一个heap进行exploite(另一个heap不能被free,否则会报错)和只能通过bins中已有的chunk进行利用,分配到
__realloc_hook
处的chunk,将__realloc_hook
改为malloc
,再将__malloc_hook
改为one_gadget
(为了调整栈帧,使得[rsp + 0x70] == NULL
。 - 触发
realloc
来getshell。
exp
1 | # context.log_level = "debug" |
小结
- 新姿势,
unsorted bin->fd
的partial overwrite改成stdout
,在没有show的情况下进行leak libc - 只有两个heap外加只有realloc操作再加各种崩坏的unsorted bin和tcache double free check,以及需要bruteforce,调试+写exp的过程对我来说那叫一个…
- 貌似还有一种改tcache struct的做法,目前还没研究,以后有时间搞一下
参考资料
- 思路来源,但是貌似这个脚本有问题:http://www.ntype.club/re-alloc_revenge/
- 改tcache stuct的做法(还没学着调过):https://sh1ner.github.io/2020/02/05/pwnable-tw-re-alloc-revenge/
- 利用stdout进行输出:https://github.com/ctf-wiki/ctf-wiki/blob/master/docs/pwn/linux/glibc-heap/tcache_attack-zh.md
- 同上:https://n0va-scy.github.io/2019/09/21/IO_FILE/
- glibc2.29源码:https://elixir.bootlin.com/glibc/glibc-2.29/source