高级返回库函数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 ... -----|-----------------------------------------
[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] ... 下一页 >> [办公软件]在sybase中插入图片、PDF、文本文件 [办公软件]安装Sybase ASE [办公软件]linux指令大全(完整篇) [办公软件]Linux新手入门常用命令大全 [办公软件]在RedHat Linux 9里安装gaim0.80 [办公软件]浅谈Linux 下Java 1.5 汉字方块问题解决方法 [办公软件]Linux程序员必读:中文化与GB18030标准 [办公软件]linux指令大全 [办公软件]制作Linux启动盘的四种方法 [办公软件]Linux文件系统的反删除方法
|