跳转至

4.Ret2libc & ROP

约 1757 个字 预计阅读时间 6 分钟

1. Buffer overflow

Defenses

  • 使用更加安全的函数,Strcpy -> strncpy, Sprintf -> snprintf。
  • 替换库函数(动态链接时候),重定向到我们自己写的安全的库函数。
  • 静态分析,编译器在编译的时候进行警告。
  • 硬件中有一个影子栈(软件维护开销很大),存放重要数据,即将跳转的时候进行校验看数据是否被篡改。或者使用canary,放在return地址之前,只要覆盖了return地址那肯定会破坏canary,那么程序就会检测到栈数据被篡改。
  • OS: ASLR,让栈堆甚至是程序的main函数每次加载的位置都不一样。这样就很难去猜测某些变量的值,比如/bin/sh的地址,比如libc的地址。
  • 不可执行栈,系统中不可出现可读可写可执行的页,通过硬件来进行指示,而栈不可执行的话,就很难进行代码的注入攻击(payload是攻击者自己注入进去的)。

image-20230515134534394

防御开启时如何攻击

ASLR: brute force(爆破)

image-20230515134801929

Stack Guard

将当前栈的值和栈的canary的值,通过漏洞进行泄露,通过漏洞进行组合来进行攻击

image-20230515134819012

image-20230515134923959

2. Ret2libc

2.1 Stack Canary

image-20230521235837320

2.1.1 多种漏洞组合泄漏Stack Canary

只要有一个信息漏洞能够泄露栈上的某个值,那就可以泄漏canary,来完成攻击。

2.1.2 爆破Stack Canary

通过爆破(Brute-forcing)来获取Stack Canary。子进程与其父进程具有相同的canary,有些服务端的程序不会主动响应请求,而是会fork一个子进程来响应,这样当canary被修改的时候,那么程序就crush,我们还能继续猜测,假设有8个字节,我们先猜第一个字节,需要0xFF次,猜对后猜第二个字节,这样只需要\(8 \times 0xff\)次就能完成猜测。

而且一般canary的第一个字节都是0x00,为了截断字符串。

2.1.3 改进Stack Canary

每次fork,子进程再随机生成一个canary,但是这样的话会造成比较大的性能开销。

PESC:每一次系统调用(从用户态进入内核态)都随机生成一个canary。

2.2 Runtime Mitigation: DEP (NX)

栈上的代码无法运行,所以无法进行代码注入。

代码重用攻击

重用当前进程空间里面的代码(包括程序本身的代码和库代码),这样就不用注入了。

Return-to-libc:将返回地址替换为危险库函数的地址

Ret2libc without ASLR

通过重定向到库里面的代码去

Find the Address of the System Function:我们可以通过gdb调试去寻找system函数的地址image-20230522002223983

Find the String “/bin/sh” 的三种方法

  • 每次都会变,难以复用image-20230515142408880
  • 通过用户定义的环境变量image-20230515142452070
  • 真正使用的做法:libc中本来就有那个字符串,直接gdb find命令寻找“/bin/sh”image-20230515142601539

目前我们找到了system函数的地址和调用的“/bin/sh”参数,现在我们要进行参数传递并且调用system函数

Caller正常调用函数的时候,栈的变化,现在我们要进行模仿image-20230522002938054

我们仍然通过栈溢出的方法,在3的位置放系统函数的地址,2的地方随便放,1的地方放参数,也就是“/bin/sh”字符串的地址image-20230522004053930image-20230522004146589

Ret2libc with ASLR

libc的位置不是固定,也就是system函数不是固定的,我们并不能够事先找到系统函数的地址。

ASLR就是对关键的地址区域(比如stack, heap and libraries)进行了随机化的机制。

image-20230522004526852

动态链接的调用原理:main函数调用的其实是PLT表,而表中记录的是GOT的表项,GOT记录着当次进程的函数的位置,从而实现动态链接

image-20230522004933869

GOT(Global Offset Table):放置当次加载程序时,某些函数的位置。GOT是在运行时候由linker或者loader来填充的。

Lazy binding:The loader does not need to resolve all the dynamic functions when loading a library. The address is resolved when it’s firstly invoked.例子:比如0x804a018 is the GOT entry for puts function,但是一开始GOT表中并没有puts的真正的地址,所以先跳转到了GOT表中的8048376,在这个地址中,会跳转到8048330,这段代码会把puts真正的地址放到804a018。image-20230522005830204image-20230522090921961

现在我们所拥有的:The address of the PLT of the puts function is fixed (not always true, we disable the PIE option when compiling the program),也就是如上图所示的puts@plt段的三行代码是固定的。

我们把return地址改为puts@plt段的地址,然后构造puts的参数为GOT(puts),这样我们就得到了puts的地址,而puts与libc的基地址的偏移量是不变的,现在我们去找到libc.so来获得偏移量,最后获得libc的基地址。

image-20230522092802893

现在我们要把两次攻击串联起来形成一次完整的攻击链。第一次的返回地址是mian函数的地址,让他再进行一遍。

image-20230522121617205

image-20230522121737485

局限性

这种方法需要程序那no PIE,如果打开了PIE,那么main函数,plt位置都是随机的,那就无法实现攻击了。

总结
  • Find system address
  • Find /bin/sh
  • Jump to system()

3. ROP

注意代码注入攻击和代码重用攻击的区别

  • In code injection, we wrote the address of execve into buffer on the stack and modified return address to start executing at buffer.

  • In code reuse, we can modify the return address to point to execve directly, so we continue to execute code.

在很多攻击中,我们可以使用代码重用攻击调用系统函数来disable DEP,然后再进行代码注入攻击。

Gadget是一串以ret为结尾的指令序列,ROP通过一列gadget的串联来实现各种指定的功能。

ROP的函数

比如如下三个gadget的串联得到了一条store指令image-20230522123650731

我们通过一系列的ret指令来使esp变化,类似原本指令流中的nop指令,nop指令改变的是eip,而ret改变的是esp。image-20230522124012660

我们通过pop指令来实现往一个寄存器里面放立即数的事情image-20230522124114496

控制流在普通的机器里面是jmp指令,在ROP里面,我们pop esp即可image-20230522124231032

只要能找到足够的gadget,就能实现任何的操作。

如何防御ROP

Control-flow integrity (CFI)

ROP违反了程序控制流的完整性,因为ret是随便跳转的,我们可以事先生成一个控制流,如果运行时候ret跳转到的地方不在事先生成的某个地址范围内,则报错。但是这只是理论上的,控制流图的构建是NP-hard问题,

Runtime Mitigation: Randomization

ASLR使找gadget比较麻烦,即使找到了gadget,但是不知道libc基地址在哪,没办法调用gadget。使得构造gadget的效率变低,但是ASLR还是存在一些破解方法,比如爆破,或者是通过利用一些内存泄漏的漏洞。

本文总阅读量