一步一步教你Linux平台下栈溢出漏洞的利用。一起来动手吧!
一. 利用IDA对Linux程序进行静态分析
1.1 方法步骤:
1. 根据具体分析的程序选择IDA(32-bit)或者IDA(64-bit)。
2. 在quick start窗口中选择New。在弹出的文件选择框中选择需要分析的文件。
3. 如果有其他的对话框弹出,直接点“OK”将其点掉即可。
下面是通过静态分析获得的信息,如图1.1:
图 1.1 IDA静态分析
可见,IDA将程序中的函数信息和字符串信息,完全以明文字符串显示出来。
IDA的常用功能包括:
源码显示:F5
自定义名称:Ctrl+N
右键点击代码部分,可以进行Graph view和Text view之间进行转换。如图1.2。
图 1.2 代码图像化显示
如图1.3所示,使用F5以后,代码的逻辑流程简单清晰。
图 1.3 源码级调试
1.2 常见问题:
1. 按F5之后,可能并不能直接获得期望函数的源码,这可以通过IDA左侧的函数列表窗口对希望获得源码的函数进行选择。
2. 如果F5之后还是不能获取到源码数据,可能是使用IDA的版本不对,比如可能使用32位的IDA程序分析了64位的二进制程序。此时尝试更换更换IDA程序。
3. 源码显示功能只在该程序的代码段中才可以使用,如果当前显示的是动态链接库等外部的代码则不能获得源码信息。
二. 使用IDA远程调试Linux程序
2.1 方法步骤:
1. 打开IDA,此时应区分将要调试的Linux程序是32位应用程序还是64位应用程序。对于不同类别的程序应用分别对应IDA(32-bit)和IDA(64-bit)进行打开。
2. 此时一般会弹出IDA的quick start对话框,由于我们将要进行的是IDA的远程调试,所以此时选择“Go”。如果没有弹出quick start对话框则跳过此步即可。
3. 在进行远程调试之前需要对Linux平台进行一些准备工作。在IDA的安装目录中的dbgsrv文件夹中,选择linux_server或者linux_serverx64复制到需要调试Linux程序所在的目录下。将复制过来的文件赋予执行权限chmod a+x linux_server*或者chmod 711 linux_server*。执行该文件./linux_server或者./linux_server64。
4. 在IDA中选择菜单Debugger-Run-Remote Linux debugger。如图。分别将程序所在位置,程序所在目录,参数(没有可不写),主机IP,主机端口,用户密码填写在指定位置,点击OK。相对路径路径要填写相对于linux_server或者linux_serverx64的相对路径。
5. 此时,即可进行动态调试,调试界面如图2.1。常用快捷键包括:
a. 单步步过:F8
b. 单步步入:F7
c. 执行到光标位置:F4
d. 设置断点:F2
e. 顺序执行:F9
图 2.1 动态调试界面
2.2 问题解决:
1. 配置调试信息时的路径选择问题:
在我的Linux环境中,overflow文件的绝对路径是:/home/nsfocus/overflow/overflow。
所以调试信息就是如图2.2或图2.3所示。
图 2.2 绝对路径
图 2.3 相对路径
2. Windows 7以上系统中文件创建权限问题:
在windows 7以上版本中,配置完调试信息后,点击确定,会显示如图2.4的对话框。
图 2.4 权限错误
由于windows 7系统中默认用户不具有对C:\Program Files(86)\文件夹的修改权限,所以这时只需点击确定,为overflow.i64重新选择一个存放位置即可。
3. 解决so文件未找到:
配置信息填写完成后,可能会弹出对话框说“ld-linux-x86-64.so.2”和“libc.so.6”文件不能找到。在linux系统中可以使用locate命令来定位这两个文件的位置,但是将正确的文件位置填写到Destination中并不能解决问题。解决这个问题只需全部点击“Cancel”即可。如图2.5,图2.6,图2.7。
图 2.5
图 2.6
图 2.7
4. 这种IDA双机调试方法同样适用于调试Android程序。只需将IDA目录中dbgsrv文件夹中的android_server导入安卓设备,剩余过程与上面类似。
三. 利用栈溢出执行shellcode:
在接下来的两章中将对overflow文件进行破解。在破解之前需要对linux系统的堆栈地址随机化功能进行禁用。否则每次程序载入是,都会为堆栈重新随机化的分配内存地址,将对调试产生极大的干扰。
禁用方法:在root权限下执行 cat 0 > /proc/sys/kernel/randomize_va_space
在系统重启之后,这个文件中的值会恢复,所以在电脑重启以后,如需再次进行调试,需要重新执行该命令。
3.1 操作细节:
1. 先对overflow文件进行静态分析,了解其代码的执行流程,静态分析的结果如图。
可获得如下信息:
a. 该程序首先创建了一个UDP的socket。
b. 与本地环路ip地址和12345端口进行绑定。
c. 显示一个“waiting for message ….”的字符串。
d. 然后recvfrom。
e. 再根据收到的信息转换一下IP地址,显示信息。
f. 将输入的字符串进行加密,使用“abcdefghijklmn”作为加密的密钥。
g. 执行Calc函数。
h. 将字符串发送“You have failed …”发送回去,进行sleep,最后循环到c。
再仔细的分析一下可以看到,这个程序的利用点,在Calc函数当中,如图3.1
图 3.1 栈溢出所在位置
可以看出,如果去掉hook_foo函数不看,在Calc函数返回之前,紧跟一个memcpy,导致具有栈溢出的风险。
2. 然后使用IDA进行Linux的远程动态调试。
a. 在EncodeBuffer函数上下断点。执行,此时rdi,esi,esi当中的值分别是buff地址,输入字符串长度,密钥所在地址。
b. 按5次F8,执行到call Calc,F7步入。如图3.2,可以看出前面有3个跳转。
图 3.2 代码流程
分别是判断输入是否为空,输入长度是否为1,输入长度是否大于0x63。
并且此时需要记住一个值,即Calc的返回地址,如图:
图 3.3 Calc的返回地址
可以看出00007FFFFFFFBEB8当中存的就是calc的返回地址。在内存地址为400960处下断点,F9执行,此时观察寄存器的值可以看到RDI和RSI的值。
图 3.4 寄存器信息
c. 这是我们已知calc的返回地址是BEB8,memcpy的目的操作数是BDA0。
BEB8 – BDA0 = 118.所以第118个字节将会覆盖返回地址。
d. 所以我们可以这个设计我们的输入数据:
0 | ~ | 118 |
Shellcode | 填充数据 | 覆盖ret的值(BBE0) |
在输入数据的开始写入shellcode数据,第118个字节使用00007FFFFFFFBBE0来覆盖Calc的返回地址。
e. 由于overflow程序使用UDP socket来收发数据,所以我们首先要将数据数据写入文件(go.in)。然后使用cat go.in | nc –u 127.0.0.1 12345,将输入数据发送给overflow。
f. 再次执行到Calc的返回位置时,可以看到返回地址并不是实现设定好的(BBE0),如图3.5,原因就是输入的数据被EncodeBuffer给加密过了。可以看出这个加密函数使用abcdefghijklmn作为密钥进行加密,对加密函数的内部进行分析,并看不出什么头绪。可以试试,将刚才加密过的数据作为输入会得到什么结果。
图 3.5 发现输入数据被加密
g. 重新运行程序,在EncodeBuffer函数的断点停住,右键点击RDI,选择Open register window,这时应该可以看到咱们的输入数据,一直向下翻,可以看到连续的0,就是输入数据的结尾了。从第一个0,上面的一个数据开始,可以看出正是咱们的输入数据即00007FFFFFFFBEB0,这时按F8,此时的BEB0,就变成了加密后的数据。将该输入数据重新写到输入数据文件中。文件的数据应该是如图3.6。
图 3.6 加密后的输入数据
h. 重新运行程序,用修改后的输入数据作为输入。如图3.7:
图 3.7 Calc返回地址被修改
可以看到calc的返回地址成功的被修改成了输入buffer的内存地址。
i. 可以知道这个加密函数使用了对称加密,只要将加密后的数据作为输入即可使用真正的数据。同理,我们将加密后的shellcode作为输入,即可使用真正的shellcode。如图3.8:
图 3.8 成功跳转到shellcode
按F8执行后,发现弹出错误!如图3.9:
图 3.9 段错误
脱离IDA的调试环境,直接运行overflow,用刚刚的数据作为输入,会发现段错误。
这说明该程序有数据执行保护(DEP|NX),还需要其他技术才能获取shell。但是对于一般的栈溢出程序,使用以上的方法,就完全可以达到目的了。
其实进行调试分析之前可以使用gdb-peda对程序的安全机制进行检查,使用checksec命令,可以判断程序使用开启了DEP,ASLR等防护机制。
3.2 问题解决:
1. Shellcode的获取:
对于shellcode,可以自己编写,因为一般只需要获取shell即可功能并不复杂。也可以使用网上发布的shellcode,获取连接为:ExploitDatabase。可以从这里获取各个平台的shellcode。
2. 有时使用NC发送数据无效果:
可能调试多次,有残余的overflow进程没有关闭,这时可以使用lsof –i:12345命令,查看那些进程占用12345端口。然后使用kill -9 PID。结束该进程。
可能nc命令没有使用-u参数。这个程序使用UDP传输数据,而nc命令默认使用TCP协议。如果不加-u参数则overflow程序无法获取数据。
也可以自己编写一个简单的程序,来使用UDP协议发送数据。
3. 输入数据的填写问题:
由于我使用的是Ubuntu 64位,所以每个地址都是占8个字节的(8 * 8bit = 64bit)。比如输入缓冲区的地址就是00007FFFFFFFBEB8。另外根据高高低低规则,数据的高字节存放在内存的高位,数据的低字节存放在内存的低位。所以上面的地址写到文件中就是:B8 BE FF FF FF 7F 00 00。
四. 利用Ret2Lib突破DEP(NX)保护:
DEP述语是微软公司提出来的,在window XP操作系统开始支持该安全特性。DEP特性需要硬件页表机制来提供支持。X86 32位架构页表上没有NX(不可执行)位,只有X86 64位才支持NX位。 所以Window XP和Window 2003在64位CPU直接使用硬件的NX位来实现;而32位系统上则使用软件模拟方式去实现。
Linux在X86 32位CPU没有提供软件的DEP机制,在64位CPU则利用NX位来实现DEP(当前Linux很少将该特性说成DEP)。
DEP就是将非代码段的地址空间设置成不可执行属性,一旦系统从这些地址空间进行取指令时,CPU就是报内存违例异常,进而杀死进程。栈空间也被操作系统设置了不可执行属性,因此注入的Shellcode就无法执行了。
既然注入Shellcode无法执行,进程和动态库的代码段怎么也要执行吧,具有可执行属性,那攻击者能否利用进程空间现有的代码段进行攻击,答案是肯定的。
在系统函数库(Linux称为libc)有个system函数,它就是通过/bin/sh命令去执行一个用户执行命令或者脚本,我们完全可以利用system函数来实现Shellcode的功能。EIP改写成system函数地址后,在执行system函数时,它需要获取参数。而根据Linux X86 32位函数调用约定,参数是压到栈上的。但是栈空间完全由我们控制了,所以控制system的函数不是一件难事情。
这种攻击方法称之为ret2libc,即return-to-libc,返回到系统库函数执行的攻击方法。
但是我们使用的环境是64bit系统,它和32位系统的一个区别就是system函数的参数传递方式。32位系统使用堆栈来传参,在64位系统中使用RDI来传递参数,所以我们不仅需要控制系统栈,还需要控制RDI,这无疑给我们增加了许多难度,但是这并不是做不到的!
首先,我们要理清思路,确定我们都需要干什么:
1. 获取system函数的地址。
2. 获取“/bin/sh”字符串的地址。
3. 将RDI中的值,改成“/bin/sh”字符串的地址。
4.1 System函数地址:
这个函数存在于libc.so中,一般都会被程序所加载。也可以通过调试状态的IDA进行查看,如图4.1。获取system函数的方法有很多种。我使用的方法是很简单的。
图 4.1 已载入的模块信息
先写一个程序,代码如下:
int main()
{
system();
}
然后使用命令:
gdb -p dummy
run
p system (如果不好使,则去掉gdb的-p参数)
一般结果如下,不同电脑不同环境之间结果可能不同。
$1 = {} 0x7ffff7a5b640 <__libc_system>
4.2 获取“/bin/sh”字符串地址
“/bin/sh”字符串的地址是我在内存的数据中偶然看到的,也可以使用IDA的搜索方法,或者其他方法。
图 4.2 字符串所在位置
可以看出该字符串的地址是00007FFFF7B91CDB。
4.3 设置RDI的值:
由于该程序有数据执行保护,所以我们往栈中的填充的数据并不能实行。所以如何控制RDI的值是一个难点。目前采用的方法就是Ret2Lib。目前我们可以通过Calc的返回地址控制程序的RIP,所以我们也完全可以通过内存中以有的代码来完成我们的要求。所以在内存中最好找到类似“pop rdi, ret”这样的语句,由于我们可以完全控制栈中的数据,所以我们就可以通过pop为rdi赋值,再通过ret指令跳转到我们希望的地方。
但是很不幸,overflow这个程序并找不到这样的指令。
pop rdi 的机器码是 5f c3
然而 pop r15 的机器码是 41 5f c3,而且一般pop r15之后一般都是紧跟ret指令。
所以我们就可以使用pop r15指令的后半部分,即 5f (pop rdi)。
寻找pop r15指令比较简单,可以使用IDA直接进行搜索,也可以使用下面的命令:
Objdump –d /lib/x86_64-linux-gun/libc.so.6 | grep –B1 c3 | grep –C3 5f
可以搜索libc.so这个文件中的5f c3 机器码所在的偏移地址,在通过IDA等工具可以获取libc.so的加载基址。基址+偏移=机器码所在的内存位置。
在我调试的时候使用的地址是00007FFFF7A3855E。
4.4 构造输入数据
现在我们已知:
System函数的地址是0x00007ffff7a5b640
“/bin/sh”的地址是0x00007FFFF7B91CDB
“pop rdi,ret”的地址是00007FFFF7A3855E
填充数据 | Calc Ret/pop rdi | “/bin/sh” | Ret2Lib-system | 填充数据 |
0x20 | 00007FFFF7A3855E | 0x00007FFFF7B91CDB | 0x00007ffff7a5b640 | 0x20 |
所以构造好的输入数据就应该是这样,如图4.3。但是别忘了,这个程序回将输入的数据加密,所以为了达到解密的效果,我们就应该将加密后的数据作为输入数据。如图4.4。
图 4.3 加密前的数据
图 4.4 加密后的数据
在这份输入数据中真正数据的前后各4个字节我也给改成了加密后的样子,这样在调试的时候可以方便的看到输入看到3个地址数据的边界。成功后的效果如图4.5。
图 4.5 成功获取shell
五. 总结:
Linux系统中对应用程序的保护分为三个方面:
SSP(Stack-Smashing Protectot):堆栈防溢出保护,它会在每个函数的栈帧底部添加一个随机字节,每次函数将要返回时,都会这个随机字节进行验证,如果这个随机字节被篡改,则说明该栈帧发生数据溢出,报出异常,程序终止。在编译时可以通过-fno-stack-protector选项取消这项保护。
NX(Never eXecute):数据执行保护,在64位系统的CPU中增加一位NX位,用来标示数据如果可写就不可执行。在overflow这个程序中我们具有对栈数据写的权限,就没有对栈数据可执行的权限。
ASLR(Address Space Layout Randomization):地址空间随机化,在每次程序加载运行的时候,堆栈数据的定位都会进行随机化处理。由于每次程序运行时堆栈地址都会发生变化,所以无疑给溢出利用增加了很大的难度。可以通过这个命令
echo 0 > /proc/sys/kernel/randomize_va_space ,取消NX保护。
ps:在Linux平台中,进行逆向分析的方法有很多,比如功能超级强大的gdb。这里推荐下gdb的peda插件,可以更加方便的进行linux程序调试,并且对于构造shellcode,观察程序执行流程都很方便,而且要比IDA双机调试更简洁,高效。
ps:可以使用python的pwntools库来编写程序的payload脚本,使用socat程序来将端口输入转发到标砖输入。由于写这篇文章时还未接触到pwntools库和socat工具,所以此处使用cat+管道+nc的方式实现漏洞利用。
参考资料:
http://blog.csdn.net/linyt/article/details/43643499
https://www.exploit-db.com/exploits/37251/
http://crypto.stanford.edu/~blynn/rop/
http://www.dmi.unipg.it/bista/didattica/sicurezza-pg/buffer-overrun/hacking-book/0x2b0-exploit-in-not-executable-stack.html
https://github.com/longld/peda