32C3 CTF-readme
1.常规checksec,开了NX,Canary,FORTIFY。然后IDA找漏洞,sub_4007E0函数中第一次输入名字时存在栈溢出:
1 | #注释头 |
2.研究程序,有数据byte_600D20提示,点进去提示远程会读取flag到这个地方,由于这里有Canary和栈溢出,那么我们直接利用Canary的检查函数___stack_chk_fail来泄露程序中byte_600D20处的flag。
3.前置知识:
(1)libc2.24及以下的___stack_chk_fail函数检查到canary被修改后,会在程序结束时打印*** stack smashing detected** ”:[./readme.bin] terminate。这里红色的部分就是程序名字,程序初始化时就会读入存储到argv[0]这个参数里面。
(需要注意的是,程序最开始读入的是程序的pwd绝对路径,类似于/home/ctf/readme.bin,之后会在___stack_chk_fail函数中对argv[0]中保存的字符串进行拆解,从而只打印出程序名字)
(2)由于argv[0]参数是main函数的参数,程序初始化时就存储在栈上的较高地址处,我们的输入肯定是在main函数及以后的函数栈中,基本都处于较低地址处,所以一旦栈溢出足够,那么就可以覆盖到argv[0],从而将___stack_chk_fail函数打印程序名字的地方覆盖成我们想要知道的某个地址中的值,这里也就是flag,byte_600D20。
4.所以进行足够长度的覆盖,将argv[0]覆盖为0x600d20,但是由于以下函数
1 | #注释头 |
即使我们覆盖掉了argv[0],那么打印出来的也不会是flag。这里需要用到另一个知识点:
▲动态加载器根据程序头将程序映射到内存,由于程序没开PIE,所以各段的加载地址均已经固定,flag位于0x600D20,处于第二个LOAD中,会被映射到内存中的第一个LOAD中,所以0x600D20处的flag即使被覆盖,那么第一个LOAD中的flag依然存在。所以这里选择将argv[0]覆盖为第一个LOAD中的flag。
第一个LOAD中的flag寻找方法,peda可以查找到:
5.现在考虑寻找argv[0]的位置,由于最开始读取的是pwd绝对路径,所以利用这个来寻找,将断点下在b *0x40080E,这里我的绝对路径是/ctf/AAAAAAAA:
上图中画红线的两段都有可能是,都尝试一下,可以知道相差536字节,也就是第一条红线才是正确的。
简单方法:直接用pwndbg>p &__libc_argv[0]
6.尝试编写payload:
1 | #注释头 |
却发现没办法打印出来,连*** stack smashing detected ***都没办法接受到,那么肯定是远程的环境变量将stderr错误输出流设置为0,只打印在远程本地。这里用socat搭建一下,可以验证出来,远程本地上能打印出来:
*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere… terminated
7.那么如果想通过该方法获取远程打印的flag,就需要将远程的环境变量stderr设置为1,也就是LIBC_FATAL_STDERR_=1。那么如何修改远程的环境变量呢,可以再通过gdb调试,输入stack 100:
这里的536就是所在argv[0],再看下下面的一些数据,552,556都是环境变量,那么在远程肯定是需要调用的,这里选择修改552处的环境变量。那么之后又如何将LIBC_FATAL_STDERR_=1传过去呢?这里就想到程序有第二个输入,本来用来覆盖0x600D20的就可以被利用了。通过第二次输入将LIBC_FATAL_STDERR_=1传过去,保存在0x600D20前面部分,之后将552处的内容修改为0x600D20,这样环境变量就被更改了。
8.总Payload:
1 | #注释头 |
9.发送完payload后再发送LIBC_FATAL_STDERR_=1就可以将flag打印在本地了。
参考资料:
比较多,网上不太全,这个比较全
https://github.com/ctfs/write-ups-2015/tree/master/32c3-ctf-2015/pwn/readme-200