高级返回库函数exploit代码实现

本文详细介绍了高级返回库函数exploit的技术,包括传统方法的局限、"esp增长"方法、栈帧伪造、嵌入零字节的解决策略。文章探讨了如何在PaX保护下通过不同途径实现堆栈缓冲溢出,特别关注了如何绕过PaX的限制,同时提到了动态链接dl-resolve()函数的相关内容。通过具体的代码示例和详细解析,帮助读者理解并实施exploit代码。
摘要由CSDN通过智能技术生成

                     高级返回库函数exploit代码实现
作者:Nergal <nergal@owl.openwall.com>
翻译:null <roohacker@263.net>

-【1-介绍
1- 介绍
2- 传统返回函数库技术
3- 多个联结返回函数库调用
  3.1 - 传统方法的局限
  3.2 - "esp增长"的方法
  3.3 - 栈帧伪造
  3.4 - 嵌入零字节
  3.5 - 概要
  3.6 - 简单代码
4- PaX的特征
  4.1 - PaX基础
  4.2 - PaX和返回库函数的实现
  4.3 - PaX与mmap随机功能
5- 动态链接dl-resolve()函数
  5.1 -一些ELF(可执行和链接格式的文件)的数据类型
  5.2 -一些ELF的数据结构
  5.3 -如何从PLT(过程连接表)调用dl-resolve()
  5.4 -结论
6- 战胜PaX
  6.1 -必要的条件
  6.2 -构建exploit
7-其他更多
  7.1 -系统无关性
  7.2 -其他类型的缺陷
  7.3 -其他"不可执行"的解决方法
  7.4 -改进现有"不可执行"的设计
  7.5 -版本信息
8-参考的出版物和工程项目
9-附件:README.code代码部分及代码注解
这篇文章大致可以分成两部分。前一部分,描述了高级返回库函数技术。一些现有的观点,或是与其类似的,已经被其他人公开发表的一些观点。然而,这些重要的技术信息资源是零散的。通常,不同平台的实现中,伴随的那些源代码都不是很有教育作用,或者根本没有作用。因而,我决定集合一些有用的资源和我自己的一些想法,写进这篇文章中,它应当利于帮助人们方便的参考。从这些内容公布在众多的安全列表中,应该判断出,这些信息决不是现有的普通公共认识。

第二部分专注于对PaX保护下的系统,通过不同途径实现堆栈缓冲溢出。现在的PaX性能被改进增强了许多,通过堆栈随机地址处理和函数库地址映射的方法增加安全性,并以一种审慎重视的姿态挑战exploit编码者。最初的方法是通过直接调用动态链接标志来决定程序的流程被呈现出来。这种方法非常普遍,而成功的实现所需求的条件也相当容易。

由于PaX保护下Intel平台稍有不同,而一般的示范源代码是为linux i386 glibc系统设计的。PaX被大多数人认为不是很稳定;然而,现有的技术(为linux i386 glibc描述的)能够轻易的在其他系统/体系上实现,能够用于逃避不可执行的安全设计,包括一些在硬件级别保护的实现。
 
假定读者具备exploit技术的基础知识,在更进一步的学习前,已经对文章[1][2]的理解吸收,[1][2]包含的对ELF描述的实际操作。

-[2 -传统返回函数库技术
传统的返回函数库技术在文章[2]很好的描述了,所以在这里只是简单的摘要。该技术用于逃避堆栈不可执行的保护,是非常普遍的方法。与在堆栈中定位返回代码的地址不同,有缺陷的函数被返回到被动态库占用的内存区域。通过下面的堆栈构造溢出堆栈的缓冲达到目的,如下所示:
<- 堆栈增长的方向
   内存地址增长的方向 ->
------------------------------------------------------------------
| buffer fill-up(*)| function_in_lib | dummy_int32 | arg_1 | arg_2 | ...
------------------------------------------------------------------
                          ^
                          |
                          - int32 会被有缺陷的函数的返回地址保存覆盖

(*) buffer fill-up 会被保存的$ebp覆盖
包含有缓冲溢出的函数返回到function_in_lib库函数的地址处恢复"可执行"。通过改变函数的指针,dummy_int32将作为arg_1, arg_2 等参数的返回地址,转入该库函数中的系统调用函数的地址(libc system()),即该库函数的指令序列,让arg_1 指向"/bin/sh"。

-【3 -多个联结返回函数库调用
--[ 3.1 -传统方法的局限
前面提到的技术有两个明显的局限。首先,它不可能在function_in_lib函数后,请求调用另一个参数的函数。为什么?当function_in_lib返回后,将在dummy_int32的地址处恢复可执行。这样,它成了另一个库函数,它的参数将不得不占用function_in_lib的参数需占用的相同的空间。有时,这不是个问题(见文【3】中普遍的列子)

注意到多次的函数调用是频繁的。比如一个有缺陷的应用程序在需要的时候临时进入到超级用户权限状态(比如,一个setuid的应用程序能够seteuid(getuid()) ),通常一个在exploit代码实现中就要在调用system()前,通过调用setuid(0),恢复超级用户权限状态。

第二个局限是function_in_lib函数包含的参数变量中不能够含有零字节(一种典型的情况是字符串处理程序中导致溢出,如果有零字节,将停止处理),下面是两种不同的方法来联结多个库函数的调用来实现exploit。

--[ 3.2-"esp[栈指针]增长"的方法
这种方法攻击使用过"-fomit-frame-pointer"这种编译选项(该编译参数通常不保存帧指针在函数寄存器中,避免指令保存,建立,恢复帧指针)进行编译的文件而设计的,这种编译条件下,典型的函数结尾象这样:
eplg:
        addl    $LOCAL_VARS_SIZE,%esp
        ret
假设f1,f2是定位在库函数中的函数地址,我们建立下面的溢出字符串:
<- 堆栈增长的方向
   内存地址增长的方向 ->

---------------------------------------------------------------------------
| f1 | eplg | f1_arg1 | f1_arg2 | ... | f1_argn| PAD | f2 | dmm | f2_args...
---------------------------------------------------------------------------
 ^          ^                                        ^
 |          |                                        |
 |          | <---------LOCAL_VARS_SIZE------------->|
 |
 |-- int32 会被有缺陷的函数的返回地址保存覆盖
PAD处是一些非零字节构成,其长度增长到被f1及其参数的变量地址占用的空间,应等于LOCAL_VARS_SIZE。(见上)
它是如何工作的?有缺陷的函数返回到地址f1,f1将返回到函数结尾,而结尾处的指令
"addl $LOCAL_VARS_SIZE,%esp" 将让堆栈的指针增加LOCAL_VARS_SIZE,这样,指针将指向地址f2并贮存起来。而结尾的"ret" 指令将返回到f2的地址,这样,我们在一行中调用了两个函数。
类似的技术在文[5]中也有说明。和文[5]中介绍的返回到一个标准函数结尾稍有不同,一些程序(库函数)映像中具有下面的指令序列:
pop-ret:
        popl any_register
        ret
这样的顺序将编译出最优化的标准结尾的结果。很优美
现在,我们构建下面的堆栈
<- 堆栈增长的方向
   内存地址增长的方向 ->
------------------------------------------------------------------------------
| buffer fill-up | f1 | pop-ret | f1_arg | f2 | dmm | f2_arg1 | f2_arg2 ... 
------------------------------------------------------------------------------
                   ^
                   |
                    - int32 会被有缺陷的函数的返回地址保存覆盖
其工作原理和前面的列子类似,除了堆栈指针不被增长LOCAL_VARS_SIZE,"popl any_register"指令移动了堆栈指针4个字节,这样,f1全部参数变量最多可以传递4个字节到f1的地址。
如果指令的顺序是这样:
pop-ret2:
        popl any_register_1
        popl any_register_2
        ret
这样,我们可以通过2个参数每个都是4个字节传递给f1地址。
后面的技术中的问题是,不可能同时可以在"pop-ret"这种形式中用到3个以上的pops(出栈指令),因此,现在我们还只能够用到前面提到的那些变化情况。
在文[6]中能够找到和前面相似的想法,可惜的是那里写的很糟糕。

注意我们可以用这种方式联结任何形式的函数。另要注意:我们并不需要知道在堆栈中精确的定位(也就是我们并不需要知道堆栈中指针精确的数值)当然,如果调用函数请求数组参数中变量的指针,并且指针就在我们的堆栈内,那么我们就需要知道他的精确地址。


----[ 3.3 - 栈帧伪造(见文[4])
这第2中技术是攻击为没有使用" -fomit-frame-pointer"这种编译选项的程序而设计的。它的函数结尾象这样:
leaveret:
        leave
        ret
不管是否使用了最优化选项,gcc编译器总是"ret"和"leave"来结尾.所以,我们没能够找到有意义的在这种2进制文件中通过"esp增长"这种技术(不过请注意3.5节的结尾)

实际上,在libgcc.a 的文档中说明了,当用-fomit-frame-pointer 这些编译选项编译目标文件的时候,在编译的过程中, 默认编译器连接成可执行文件。因而在这些执行文件中可以找到如"add $imm,%esp; ret"这样的指令序列。可是我们不能够依靠gcc的这些特征,因为它还要取决于更多的因数(gcc的版本,编译使用的选项,等)

代替"esp[帧指针]增长"的方法,通过函数返回到"leaveret"。堆栈的构造应该逻辑的分成不同的部分,通常的exploit代码应该和"leaveret"接近。
<- 堆栈增长的方向
   内存地址增长的方向 ->
                       
                    保存基址寄存器  缺陷函数返回地址
--------------------------------------------
| buffer fill-up(*) | fake_ebp0 | leaveret |
-------------------------|------------------
                         |
   +---------------------+         (*)这种情况,buffer fill-up不能被覆盖写在栈帧指针
   |                 
   v
-----------------------------------------------
| fake_ebp1 | f1 | leaveret | f1_arg1 | f1_arg2 ...                    
-----|-----------------------------------------
     |                       第一栈帧
     +-+
       |
       v
     ------------------------------------------------
     | fake_ebp2 | f2 | leaveret | f2_arg1 | f2_argv2 ...
     -----|------------------------------------------
          |                  第二栈帧 
          +-- ...
fake_ebp0 是第一栈帧的地址,fake_ebp1 是第二栈帧的地址,依次类推。
现在,一些想法将被呈现在下面
1)有缺陷的函数的结尾(leave;ret)将fake_ebp0的地址赋予栈基指针,并返回到leaveret。
2)结尾的两个指令(leave;ret)放fake_ebp1 地址到栈基指针,并返回到f1的地址。
3)f1执行后返回leaveret。
重复2)3)步骤,用 f1,f2,。。。fn代替。

文[4]中的返回到函数结尾的技术没有过多的用途,因而作者提议如下,堆栈应该被构建成让exploit代码返回到F函数前面的库函数的后面,不要返回到F函数自身,这中技术和前面很类似,然而我们很快就会面对这种情形,当F函数仅通过过程连接表(PLT),这种情况,就不可能返回到函数F的地址加某个地址偏移。而只会返回到自身的地址。

注意,为了使用这个技术,必须知道精确的定位伪造的栈帧,因为fake_ebp必须按照规则设置。如果所有的栈帧定位在buffer fill-up的后面,那么必须知道在溢出后面堆栈指针的确定数值。然而,如果我们知道怎样控制一个伪造的栈帧定位在一个已经知道的内存区域(静态变量更适合),就没有必要猜测堆栈的指针数值了。

有可能攻击这种用 -fomit-frame-pointer这类的便宜选项的程序,这种情况,我们不需要找程序中的leave&ret代码,但通常它能够在一些常规的联结过程中发现。因此我们要改变这些零的块。

-------------------------------------------------------
| buffer fill-up(*) | leaveret | fake_ebp0 | leaveret |
-------------------------------------------------------
                          ^
                          |
                          |-- int32 会被有缺陷的函数的返回地址保存覆盖
两个leaverets是必要的,由于有缺陷的函数当返回的时候不会设置堆栈指针。由于"帧伪造"教"堆栈指针增长"有优势。一些时候是很必要的通过这种方法达到攻击。

----[ 3.4 - 嵌入零字节
还有一个问题:传给一个函数的参数中包含有零字节。当多个函数有效调用时,有一个简单的解决方法:先调用的函数通过嵌入零字节到下一个要调用的函数的参数的地址。

Strcpy是我们经常用到的一个函数,它的第二个参数指向一个程序映像中固定空间的零字节,第一个参数指向的是将无效的地址,我们在每一次函数调用的时候指定无效的零字节,在需要的int32位置可以放入零,这方法需要适当的后效的位置放置。比如,sprintf(some_writable_addr,"%n%n%n%n",ptr1, ptr2, ptr3, ptr4);可以在some_writable_addr地址使其无效,在ptr1, ptr2, ptr3, ptr4让int32放置于这些地址,同样无效。还有很多的函数可以达到这种方式的目的,比如scanf,详见文[5]。

注意,这种方法解决的一个深层次的问题。如果所有的库函数被通过含有零的地址进行地址映射处理,比如sun公司对Solar设计的堆栈不可执行补丁,我们将不能够直接的返回到一个库函数中,因为我们不能够传零字节到溢出的堆栈中。但是,如果被攻击的程序中调用过Strcpy(或者sprintf,见文[3]),并且有适当的(PLT)过程连接表入口,那么我们可以利用,开头调用函数包括strcpy这样的让函数的参数字节无效,但不是函数自身的地址的字节。在这些参数的后面,我们可以再从库函数中调用任意的函数了。

----[ 3.5 - 概要
如上两种的现有的方法都是比较类似的,从调用的函数中返回,而不是直接返回到后一个函数的地址。但在一些函数的结尾,通过调整堆栈指针或者是栈帧指针,将控制转移到同一链中的下一个函数中。

在上面两种方法中,我们都试图在可执行格式的文件的文件体中寻找一个适当的结尾。通常,最好是利用库函数的结尾。然而,一些时候,这种库函数映像的结尾是不能够直接的返回,比如库函数被通过零字节进行过地址映射处理的情况,已经被提及了。我们将面对另一种情况,可执行文件的映像不是一个固定的位置,而一定会在一个固定的位置进行随机的映射,发现,一些情况下,linux系统的这个地址是0x08048000,这样,我们可以利用这里地址顺利的返回到目标库函数。

----[ 3.6 - 简单代码
ex-move.c 和ex-frames.c是对vuln.c程序实现的exploit代码。这exploit代码中联结了一些象strcpy和mmap的函数调用。在4.2节中有更多的解释。总之,任何人可以通过这些给出的exploit代码作为模块,构建库函数返回技术的exploit实现代码。

--[ 4 - PaX的特征
----[ 4.1 - PaX基础
如果你没有听说过PaX linux内核补丁,建议你访问他们的项目主页[7]。下面是一些从PaX文档中的引用。
"该文档讨论在IA-32处理器上实现不可执行的可能性。(比如用户模式下的页面是可读,写的,但是不能够代码执行)不过该处理器还没有提供这项功能,这是很有价值的工作。"
"[...]为防止堆栈缓冲区溢出的攻击,有一些观点和方式,一种观点是,在数据段中有限的排除代码的情况下,可以通过页面的不可执行的方法达到遏止攻击[...]"
"[...]在内核模式下,通过DTLB和ITLB入口写代码将导致错误[...]可以创建数据段只可读,写,而不能都执行的状态,这是保证不被溢出攻击的根本"

总的而说,缓冲区溢出攻击通常试图在执行代码中使用一些数据达到攻击的过程。PaX的主要功能是不允许在任何数据段具有可执行---这种特点让典型的溢出实现代码失去效果。

--[ 4.2 - PaX和返回库函数的实现
最初,数据段不可执行是 PaX的唯一特征。呵呵,你已经猜到了,它对return-into-lib的exploit攻击还是远远不够的。这种代码实现通过定位在库函数中和二进制的文件完美的结合。在本文的3章中进行了描述。实现代码中调用多个库函数,这种技术在高级exploit代码实现中有其优势。

下面的代码可以在 PaX 保护的系统中成功的执行!
char shellcode[] = "arbitrary code here";
    mmap(0xaa011000, some_length, PROT_EXEC|PROT_READ|PROT_WRITE, 
                           MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS|MAP_SHARED, -1, some_offset);
    strcpy(0xaa011000+1, shellcode);
    return into 0xaa011000+1;
简单的解释:mmap函数调用时在地址0xaa011000(*start参数)分配内存单元。它并不和任何的目标文件有关系。但得感谢MAP_ANONYMOUS标志,和文件描述符(int fd)等于-1。当代码定位在0xaa011000时,在Pax会被执行(由于函数mmap函数中的参数PROT_EXEC(保护模式内存页面可执行)被设置了)。显然,任何代码如果代替上面代码中的"shellcode"都将被执行。

好,我们来看实现代码了。vuln.c 是一个被攻击的程序,有明显的溢出问题,开始编译它:)
$ gcc -o vuln-omit -fomit-frame-pointer vuln.c
$ gcc -o vuln vuln.c
-fomit-frame-pointer该编译参数通常不保存帧指针在函数寄存器中,避免指令保存,建立,恢复帧指针。其中ex-move.c是攻击vuln-omit的exploit代码;ex-frames.c是攻击vuln的exploit代码。
exploit实现代码依次调用strcpy()和mmap(),请参考README.code理解更多的指令。(详见《高级返回库函数exploit代码实现(下)》)

如果你要在最新的Pax保护的系统中演示以上代码,务必需要禁止随机映射的功能,如下:
$ chpax -r vuln; chpax -r vuln-omit

----[ 4.3 - PaX与mmap随机功能
为了抗击返回库函数实现的技术,PaX增加了一个可爱的功能。如果在内核配置中设置一些适当的功能,第一次装载的库函数将随机的映射地址,而下一个又在前一个的基础上进行随机处理。相似的应用在堆栈中,第一个库函数随机映射的地址以这种方式0x40000000+random*4k。堆栈顶部等于0xc0000000-random*16。可以看出,所谓的随机不过是一个16位的无符号的整数。通过函数get_random_bytes()获得随机调用且进行过加密的强壮数值。

我们可以通过命令"ldd -r some_binary"来查看调用的库函数并了解它的行为。并还可通过"cat /proc/$$/maps" ($$代表程序的进程号)了解Pax的随机过程。
呵呵,注意看下面,每次的运行ash所调用的库函数的地址都是不同的:)
nergal@behemoth 8 > ash (第一次)
$ cat /proc/$$/maps
08048000-08058000 r-xp 00000000 03:45 77590      /bin/ash
08058000-08059000 rw-p 0000f000 03:45 77590      /bin/ash
08059000-0805c000 rw-p 00000000 00:00 0
4b150000-4b166000 r-xp 00000000 03:45 107760     /lib/ld-2.1.92.so
4b166000-4b167000 rw-p 00015000 03:45 107760     /lib/ld-2.1.92.so
4b167000-4b168000 rw-p 00000000 00:00 0
4b16e000-4b289000 r-xp 00000000 03:45 107767     /lib/libc-2.1.92.so
4b289000-4b28f000 rw-p 0011a000 03:45 107767     /l

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值