De1CTF2019 pwn unprintable writeup
TOC
原先从没见过,所以这里分开来写。靶机环境是glibc-2.23,该程序漏洞十分依赖其对应的libc。
源程序和相关文件:unprintable.zip 。
安全防护
ex@Ex:~/de1ctf/unprintable$ checksec unprintable |
溢出点
int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
标准的格式化漏洞。关闭了输出流。
但是buf
并不在栈上,所以我们只能利用栈上的地址。
通过查看栈布局,我们能获得一些信息。
Breakpoint *0x4007C1 |
在运行_start
之前,先运行的是一些链接库的_dl_init
,通过查看栈发现,其恰好残余下了一个很有用的指针0x7ffff7ffe168
,他就是_rtld_global._dl_ns[0]._ns_loaded
,这个是我们程序的link map
,对程序而言至关重要。
pwndbg> p _rtld_global._dl_ns[0]._ns_loaded |
该程序的溢出点主要在于,但我们修改了_rtld_global._dl_ns[0]._ns_loaded
的值,则在执行exit
函数时便会发生变数。
程序在退出时要完成内存的释放类似的工作,exit
函数保留了这样的思想,在退出是会调用_dl_fini
函数来执行各文件的fini
操作。而执行情况如下:
代码来自:/glibc/glibc-2.23/elf/dl-fini.c:128
internal_function |
其中变量l
就是我们的_rtld_global._dl_ns[0]._ns_loaded
,原本l->l_addr
为0,则array
的值就是正常的,但是l->l_addr
不为0的话则会使其发生偏移,我们则可以使其直接偏移到bss
段上,使其直接运行bss
段的地址,也即是buf
,则我们就控制了程序流。并且l->l_addr
正好位于l
的首部,所以我们可以直接利用printf
修改其值进行偏移。它的偏移对于第一次printf
而言就是26
。
思路
由于printf
不能使用%$n
在一次printf
中进行链接地址传递操作,所以我们只能将其分成多步,这样才能正常运行。
- 第一次
printf
将栈地址指向第二次printf
的返回地址,由于程序给了栈地址,我们可以直接进行计算。
last = (printf_ret - 904 + 0x10000) & 0xffff |
- 第二次则可以直接利用
%$n
修改第一次留下的地址,直接修改该printf
的返回地址,使得我们可以不断调用read
和printf
函数。 - 然后就可以多次使用
printf
完成链接地址操作,从而实现栈的任意写。
def load(position, ch): |
163就是0xa3
,也即是下面的地址,其主要作用是方便复用且不会改变栈地址。
.text:00000000004007A3 mov edx, 1000h ; nbytes |
由于 %$n ,最多只支持
0x2000
,所以栈地址的低字节必须要小于0x2000
才行。
if((printf_ret & 0xffff) > 0x2000): |
注意:对于靶机而言,当payload过长时会读取失败,导致执行错误。但是本地不会,我自己搭的服务器也不会,至于为什么靶机为什么会这样就不得而知了,所以写payload的时候应进行对payload进行压缩,同时要保证有足够的栈空间来执行函数。
写完栈后将printf
的返回地址改成ret
地址,则可以直接利用之前装载的内容进行rop了。
# 0x00000000004005d1 : ret |
由于不能直接用rop修改rdx的值,而且调用printf
后,rdx
的值会被污染导致read
函数无法使用,所以这里我直接利用0x0000000004007A3
地址来修改rdx
寄存器的值,由于现在rsp
已经指向了buf
,所以我们可以直接修改read
函数的返回值,并在后面加上ROP链。
payload = '/bin/sh 1>&0\0'.ljust(0x80 + 0x18, 'h') + flat(layout2).ljust(0x120 - 0x98, 'g') + flat(layout3) |
上面代码中layout2
则正好可以踩到read
函数的返回地址。
可以写栈了之后便是正常的ROP。
修改stdout
网上大部分大佬的思路都是利用残余的libc地址来找syscall
指令,这里我不太喜欢这样,我的做法是直接修改stdout->_fileno
,由于程序只关闭了文件描述符1
,却没有关闭文件描述符0
,所以我们可以修改stdout
的文件描述符_fileno
为0,则可以使得程序再次拥有了输出的能力,再用其输出got地址
,执行system
函数。
由于文件描述符1
被关闭,只要我们在执行system
函数时将其重定向到0
,则可以继续使用。也即是/bin/sh 1>&0
。
利用的stdout
地址是之前栈执行留下的地址,只要我们构造ROP然后进行部分覆盖就可以修改stdout->_fileno
的值。
其中要进行多次栈转移,多调试几次就好了,其实也并不复杂。
脚本
#!/usr/bin/python2 |
运行实例:
ex@pwntools23:~/de1ctf/unprintable$ ./exp.py |