前言
前面我们分析了ELF动态链接的整个过程[1],学习到了程序是如何通过plt去调用动态链接库中的函数的,有了前面的知识铺垫,本节应该就更好理解了。本节主内容是,根据动态链接的过程,去进行ROP的构造,从而实现缓存区溢出的攻击。
栈调用子函数
我之前写了一篇学习笔记,关于栈的工作过程[2],栈是自高地址向低地址增长的,但是我们进行溢出的时候,是从低地址向高地址进行覆盖的。从下图我们可以看到Callee是Caller的子函数,Caller调用Callee函数,Caller中包含了Callee函数的参数即arg1,arg2,...,argn..,在子函数Callee中Local Variables是Callee的局部变量。
我们发现Callee函数的局部变量Local Variables和Callee函数的参数之间是间隔了两个字的,即Retuen Address和Caller's ebp,在局部变量需要去调用Callee函数输入的实际的参数值的时候,是需要跳过两个字去调用输入的参数值的,所以我们在进行栈溢出的时候,这中间的两个字也是不能少的。

按照栈的正常执行,执行了Callee,Callee的最后一个汇编指令ret,会将Return Address中的地址存放在eip寄存器中,即下一条要执行命令的地址。这个Return Address即为原来Caller函数正常调用的下一个指令的地址,这样调用Callee子函数完成,又成功回归到了Caller函数,继续向下执行。
构造栈溢出内容
构造栈溢出[3]
假设Callee函数中的局部变量存在栈溢出漏洞,通过这个栈溢出漏洞,我们想要去调用system函数,这个system函数是动态链接库中的函数,我们现在知道了system函数的真实地址,我们该如何去构造这个溢出的栈内容呢?如下图所示。

system函数的汇编代码为:
1 | system: |
我们进行栈溢出攻击,将Return Address位置覆盖为system函数的真实地址,这样在回归正常的Caller函数时候,执行了ret指令后,eip寄存器中的地址就变成了system的真实地址了,那么下一条指令执行的就是system函数了。
那么现在开始去执行system函数,我们去看一下system函数的汇编代码,代码的第一个就是push ebp,这时候是system address已经被弹出栈,存放在eip寄存器中了,下面我画图来更好的理解这个过程(注意图中的内容部分都是地址,exit是地址,"/bin/sh"也是地址)。

执行了push ebp,然后再执行剩下的汇编代码,在汇编代码的局部变量部分会去调用system的参数。要想去成功的执行了system函数,输入参数/bin/sh是必须的。按照前面的Callee去引用Caller输入的参数,需要去向前跳两个字的距离,所以这里我在进行栈溢出的时候,我中间还加了一个exit函数,这就是为了恰好间隔两个字。当然这里使用exit函数是为了构造合法的结构,程序可能不会崩,假如32位的系统,我去使用'AAAA',4个'A'去进行覆盖也可以,已经可以成功执行system了,但是system退出的时候,程序可能会崩溃掉。
真实函数地址哪里来?
前面我们也说了,假设是知道system的真实地址,当然在实际环境中,这个不可能平白无故就知道了。我在之前的笔记[1:1]也说了,.got.plt中会记录程序调用动态链接库的函数的真实地址,那么就利用这个去获取函数真实的地址。
那么跟着前面的内容就能串起来,把system address直接换成system@plt就好了。

这样就可以整个连接成一个攻击链路了。
案例ret2libc1
下面看一个真实案例ret2libc1程序。
使用IDA pro进行反编译,明显的存在栈溢出漏洞。

再去看看secure函数,发现该函数调用了system,但是代码实际是走不到这个system函数的。这里出现了system就是为了在.got.plt表中存在system函数,表中是记录了system函数在动态链接库中的真实的地址的,方便我们利用这个获取shell。

我们再使用gdb进行动态的调试一下。

查看到了存在system@plt表项。存在栈溢出漏洞,去查看覆盖地址需要的偏移量。

0xffffd408 - 0xffffd39c = 108,偏移了108个字节,再去覆盖掉一个ebp,所以需要覆盖掉108 + 4 = 112字节。
再下面要覆盖的一个字,就是要跳转的动态链接库中的真实system函数地址,因为之前说过system函数在被弹出执行栈,执行system函数的时候,system函数第一个执行的就是push ebp,因为要间隔两个字,去找system函数实际输入的参数,发现中间还差了一个字,这时候就需要手动去添加一个字,我填充了BBBB字符。
ok到这里就差system函数的参数了,去程序中找/bin/sh字符串的地址,字符串的地址为0x8048720

找到了system函数的真实地址为0x8048460

完整的构造的栈溢出内容:

下面编写利用的ret2libc1_exp.py脚本:
1 | from pwn import * |
成功获取shell。

案例ret2libc2无/bin/sh
下面我们看另一个实例,如果程序中不存在/bin/sh的字符串怎么办?如何去进行后续的利用?
方法一(gets函数输入)
这个案例和ret2libc1类似,只是少了一个/bin/sh,那么就使用gets函数输入。

查看.got.plt发现存在gets和system表项。

函数有了肯定是要有变量去承载我们输入的字符串的,查看.bss段内容,发现buf2。那么,只要把字符串写进buf2,然后system再调用,就可以成功完成这个攻击流程了。

设计payload如下:

动态调试,查看详细需要进行注入的内容和相关的地址,和re2libc1类似,这里不再赘述了。
下面编写利用的ret2libc1_exp2.py脚本:
1 | from pwn import * |
这个脚本中需要注意的内容是,gets函数需要我们将想要的字符进行输入,所以sendline了一个b"/bin/sh\x00"。其中\x00是为了防止换行符的输入,sendline就是在输入内容的末尾,再输入一个换行符。
方法二(使用ROP,更通用)
前面了解了方法一,再去思考一下,gets@plt和system@plt在栈上一定是排在一起的嘛?能不能先调用gets函数,调用结束后,再去调用system函数?答案是可以的,反而这样理解起来更简单。
再看方法一的利用方法,在栈中,当执行了gets函数以后,需要把栈中的buf2的内容进行出栈才能执行后续的函数部分。怎么出栈呢?找pop的gadget!把原来方法一的system@plt,换成pop ebx ret的指令,如下图所示:

使用pop ebx ret,刚好填补了两个字的间隔,使得gets函数成功获取其输入的参数,同时也能成功弹出buf2在栈中的内容。使用pop ebx ret,这里不一定必须ebx,只要是通用寄存器存放栈中弹出的数据就好了。
思路清晰了以后,尝试去利用一下,先去程序中查找pop ebx ret类似的指令,使用ROPgadget --binary ret2libc2 --only "pop|ret",如下:

找到了一条0x0804843d : pop ebx ; ret
优化一下方法一的脚本ret2libc1_exp3.py如下:
1 | from pwn import * |
成功执行代码。

案例ret2libc3
首先是checksec查看程序的安全配置。

返回汇编代码,发现这里read了0x100的数据,即256字节到src变量中

跟进到Print_message函数中,查看汇编代码,发现将src复制到了dest中,dest变量只有56字节,所以说这里是存在栈溢出漏洞的。

找到了漏洞点,思考一下整个的栈是什么样子的?
栈内容大致如下,将src的空间较大,dest的空间较小,将src赋值到dest中会存在栈溢出。

尝试去构造溢出的内容。想去利用ret2text发现没有后门函数,前面使用 checksec,发现 NX: NX enabled,ret2shellcode也是不行的,再看rop的gadgets就这几个,想去ret2systemcall,去构造system函数也是不行的。那就只能是ret2libc了看来。

既然是ret2libc,那我们去找找程序中有无/bin/sh字符,发现没有。

再去看看plt,发现没有system函数。但是,有gets函数,那么/bin/sh的问题就可以解决了。那么怎么去找system函数的真实地址呢?

在反汇编程序后发现See_something是输出真实地址的函数。

ldd ret2libc3查看文件,调用的动态链接库为/lib/i386-linux-gnu/libc.so.6,然后将其保存到当前的exp的脚本文件夹中。

从libc.so.6文件中找到puts函数的真实地址,计算libc.so.6中system和puts函数的实际地址偏差是多少。那么程序在运行后,虽然服务器开了ASLR,真实地址会变,但函数间的相对位置不会变。
利用程序原本的函数See_something去查看puts函数的真实地址。
elf.got["puts"]返回的是GOT条目在内存中的地址(一个指针变量的位置),这个地址里存放一个指针值,这个指针值在运行时会指向puts在libc中的真实地址。
从下图可以看到,计算了基地址libcBase,然后还有puts函数和system函数的相对距离为176432。

计算system函数的真实地址

真实的system地址找到了,开始尝试进行溢出攻击。
对程序进行断点调试,计算覆盖的字节数56 = 0xd2b8 - 0xd280,再加上一个ebp寄存器的4个字节,一共要覆盖60个字节,下一个就是return Address,也就是system函数真实的地址。
构造溢出的结构如下:

查找字符sh的地址next(elf.search(b"sh\x00"))
1 | from pwn import * |
运行脚本。
