去年0ctf的一道题,babystack,应该是最简单的一道题目。
这道题涉及到了一个知识点return to dl resolve,对操作系统层面的知识了解太少,程序员的自我修养还没看完….,感觉这种利用点自己不总结一遍弄懂每一个点时间长了就又会忘记。
题目简述
题目就是一个输入,如果输入过长则崩溃。
1 | $ ./babystack |
题目漏洞
使用ida反编译一下可以看到逻辑很简单,一个read函数,一个很明显的缓冲区漏洞,距离ebp为0x28,接受0x40长度的输入。
1 | int __cdecl main() |
没有libc,无法通过正常的泄露地址并计算system或one_gadget地址来get shell。看到一个利用方法是return to dl resolve,通过伪造.dynsym和.dynstr段中的内容,不用泄露libc即可执行system函数来get shell。这里我可能不会太多的去写原理了,参考链接里讲的很详细了,我可能重点分析利用过程中payload的构造。
利用过程
因为题目输入的长度为0x40,因此第一阶段的rop长度最多为0x40,使得程序跳转到read函数,为第二次输入做准备。构造的payload如下。另外我至今也没弄懂为什么要在bss_addr+0x800上构造。而且其他偏移是不可以的。
1 | bss_addr = 0x0804a000 |
这样可以有第二次输入的机会,第二次输入的payload将写在以bss_stage处。执行完read函数后将执行leave_ret,该指令的执行过程如下,此时ebp的值是bss_stage,因此最后返回到bss_stage+0x4,也就是0x0804a804处的指令继续执行。
1 | mov esp,ebp |
接下来进行第二次的输入,这里会用到elf文件某些节的地址:
1 | $ readelf -S babystack |
伪造elf表项
这里伪造了alarm函数的重定位表项,重定位表的结构如下:
1 | typedef struct { |
其中r_offset表示重定位入口的偏移,对于函数来说,这个值其实就是它在.got.plt全局函数偏移表中的值。r_info这个值表示了重定位入口的类型和符号,其中地8位表示重定位入口的偏移,高24位表示重定位入口的符号在.dynsym动态符号表中的下标。可以使用以下命令查看重定位表:
1 | $ readelf -r babystack |
alarm函数对应在.got.plt的地址就是0x0804a010:
.dynsym是动态符号表,中存储着与动态链接相关的符号,结构如下:
1 | typedef struct |
alarm函数的r_info为0x207,0x207 >> 8 = 2,那么alarm函数在.dynsym动态符号表中的下标就是2:
1 | $ readelf -s babystack |
.dynsym的起始地址是0x080481cc,每个Elf32_Sym结构的大小为0x10,我们可以看到alarm函数在动态符号表中的内容:
1 | gdb-peda$ x /4wx 0x080481cc+0x10*2 |
其中第一项是st_name,记录该函数在.dynstr字符串表中的下标,.dynstr存储了动态链接的字符串,每个成员均以’\x00’作为结尾,可以在0x0804822c+0x1f中看到“alarm”这个字符串:
1 | gdb-peda$ x /s 0x0804822c+0x1f |
我们需要伪造上面说的这些结构,使得函数在调用_dl_runtime_resolve进行符号解析时,伪造system函数为alarm函数。
在bss_stage+28处伪造.rel.plt重定位表,该表的r_info成员右移8位是函数在.dynsym动态符号表的下标,在bss_stage+36处伪造.dynsym动态符号表,因为.dynsym每一个表项的长度都是0x10,因此需要对齐到0x10,首先计算下标伪造重定位表:
1 | #计算下标 |
接下来伪造.dynsym,因为.dynsym的第一个成员是函数在.dynstr的下标,我们在bss_stage+36+0x10处,也就是fake dynsym后面伪造.dynstr:
1 | dynstr = 0x0804822c |
payload构造
得到这些伪造的表项,如何构造payload呢?由于系统的延迟绑定机制,在第一次调用该函数时才会将其真正的地址写入到GOT条目中。在调用alarm@plt时,程序会跳转到alarm在GOT表中的地址:
1 | gdb-peda$ x /i 0x08048310 |
在调用alarm函数之前,alarm函数的GOT表中存储的是alarm@plt的下一条指令的地址:
1 | gdb-peda$ x /wx 0x0804a010 |
0x08048316处的指令如下,将0x8压栈,这个0x8是alarm的重定位表项在.rel.plt的偏移,然后跳转至0x080482f0处,该地址是.plt的起始地址,也就是PLT[0]处执行:
1 | gdb-peda$ x /2i 0x08048316 |
0x080482f0处将0x804a004压栈,该地址是GOT[1],然后跳转至GOT[2]处执行:
1 | gdb-peda$ x /2i 0x80482f0 |
GOT[2]处保存着_dl_runtime_resolve函数的地址,那么0x8和GOT[1]就是该函数的参数,然后该函数完成alarm的重定位工作,然后再跳转至真正的alarm函数的地址处去执行。
1 | gdb-peda$ x /x 0x804a008 |
1 | gdb-peda$ x /8i 0xf7fee000 |
那么我们在构造时要想调用_dl_runtime_resolve函数,可以直接接上一个payload返回到PLT[0]处执行,然后手动将其在重定位表中的偏移压栈,然后程序根据我们伪造的各种表将system函数的地址写入到alarm的GOT表条目中,然后再调用system函数,获得shell。
1 | plt_0 = 0x080482f0 |
完整的exp如下:
1 | from pwn import * |
参考
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop/