这篇文章大致可以分成两部分。前一部分,描述了高级返回库函数技术。一些现有的观点,或是与其类似的,已经被其他人公开发表的一些观点。然而,这些重要的技术信息资源是零散的。通常,不同平台的实现中,伴随的那些源代码都不是很有教育作用,或者根本没有作用。因而,我决定集合一些有用的资源和我自己的一些想法,写进这篇文章中,它应当利于帮助人们方便的参考。从这些内容公布在众多的安全列表中,应该判断出,这些信息决不是现有的普通公共认识。
第二部分专注于对PaX保护下的系统,通过不同途径实现堆栈缓冲溢出。现在的PaX性能被改进增强了许多,通过堆栈随机地址处理和函数库地址映射的方法增加安全性,并以一种审慎重视的姿态挑战exploit编码者。最初的方法是通过直接调用动态链接标志来决定程序的流程被呈现出来。这种方法非常普遍,而成功的实现所需求的条件也相当容易。
由于PaX保护下Intel平台稍有不同,而一般的示范源代码是为linux i386 glibc系统设计的。PaX被大多数人认为不是很稳定;然而,现有的技术(为linux i386 glibc描述的)能够轻易的在其他系统/体系上实现,能够用于逃避不可执行的安全设计,包括一些在硬件级别保护的实现。
假定读者具备exploit技术的基础知识,在更进一步的学习前,已经对文章[1][2]的理解吸收,[1][2]包含的对ELF描述的实际操作。
2 传统返回函数库技术
传统的返回函数库技术在文章[2]很好的描述了,所以在这里只是简单的摘要。该技术用于逃避堆栈不可执行的保护,是非常普遍的方法。与在堆栈中定位返回代码的地址不同,有缺陷的函数被返回到被动态库占用的内存区域。通过下面的堆栈构造溢出堆栈的缓冲达到目的,如下所示:
内存地址增长的方向
| buffer fillup(*)| function_in_lib | dummy_int32 | arg_1 | arg_2 | ...
int32 会被有缺陷的函数的返回地址保存覆盖
(*) buffer fillup 会被保存的$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[栈指针]增长"的方法
这种方法攻击使用过"fomitframepointer"这种编译选项(该编译参数通常不保存帧指针在函数寄存器中,避免指令保存,建立,恢复帧指针)进行编译的文件而设计的,这种编译条件下,典型的函数结尾象这样:
eplg:
addl $LOCAL_VARS_SIZE,%esp
ret
假设f1,f2是定位在库函数中的函数地址,我们建立下面的溢出字符串:
内存地址增长的方向
| f1 | eplg | f1_arg1 | f1_arg2 | ... | f1_argn| PAD | f2 | dmm | f2_args...
^ ^ ^
| | |
| | |
|
| int32 会被有缺陷的函数的返回地址保存覆盖
PAD处是一些非零字节构成,其长度增长到被f1及其参数的变量地址占用的空间,应等于LOCAL_VARS_SIZE。(见上)
它是如何工作的?有缺陷的函数返回到地址f1,f1将返回到函数结尾,而结尾处的指令
"addl $LOCAL_VARS_SIZE,%esp" 将让堆栈的指针增加LOCAL_VARS_SIZE,这样,指针将指向地址f2并贮存起来。而结尾的"ret" 指令将返回到f2的地址,这样,我们在一行中调用了两个函数。
类似的技术在文[5]中也有说明。和文[5]中介绍的返回到一个标准函数结尾稍有不同,一些程序(库函数)映像中具有下面的指令序列:
popret:
popl any_register
ret
这样的顺序将编译出最优化的标准结尾的结果。很优美
现在,我们构建下面的堆栈
内存地址增长的方向
| buffer fillup | f1 | popret | f1_arg | f2 | dmm | f2_arg1 | f2_arg2 ...
int32 会被有缺陷的函数的返回地址保存覆盖
其工作原理和前面的列子类似,除了堆栈指针不被增长LOCAL_VARS_SIZE,"popl any_register"指令移动了堆栈指针4个字节,这样,f1全部参数变量最多可以传递4个字节到f1的地址。
如果指令的顺序是这样:
popret2:
popl any_register_1
popl any_register_2
ret
这样,我们可以通过2个参数每个都是4个字节传递给f1地址。
后面的技术中的问题是,不可能同时可以在"popret"这种形式中用到3个以上的pops(出栈指令),因此,现在我们还只能够用到前面提到的那些变化情况。
在文[6]中能够找到和前面相似的想法,可惜的是那里写的很糟糕。
注意我们可以用这种方式联结任何形式的函数。另要注意:我们并不需要知道在堆栈中精确的定位(也就是我们并不需要知道堆栈中指针精确的数值)当然,如果调用函数请求数组参数中变量的指针,并且指针就在我们的堆栈内,那么我们就需要知道他的精确地址。
3.3
栈帧伪造(见文[4])
这第2中技术是攻击为没有使用" fomitframepointer"这种编译选项的程序而设计的。它的函数结尾象这样:
leaveret:
leave
ret
不管是否使用了最优化选项,gcc编译器总是"ret"和"leave"来结尾.所以,我们没能够找到有意义的在这种2进制文件中通过"esp增长"这种技术(不过请注意3.5节的结尾)
实际上,在libgcc.a 的文档中说明了,当用fomitframepointer 这些编译选项编译目标文件的时候,在编译的过程中, 默认编译器连接成可执行文件。因而在这些执行文件中可以找到如"add $imm,%esp; ret"这样的指令序列。可是我们不能够依靠gcc的这些特征,因为它还要取决于更多的因数(gcc的版本,编译使用的选项,等)
代替"esp[帧指针]增长"的方法,通过函数返回到"leaveret"。堆栈的构造应该逻辑的分成不同的部分,通常的exploit代码应该和"leaveret"接近。
内存地址增长的方向
保存基址寄存器 缺陷函数返回地址
| buffer fillup(*) | fake_ebp0 | leaveret |
|
|
++ (*)这种情况,buffer fillup不能被覆盖写在栈帧指针
|
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 fillup的后面,那么必须知道在溢出后面堆栈指针的确定数值。然而,如果我们知道怎样控制一个伪造的栈帧定位在一个已经知道的内存区域(静态变量更适合),就没有必要猜测堆栈的指针数值了。
有可能攻击这种用 fomitframepointer这类的便宜选项的程序,这种情况,我们不需要找程序中的leave&ret代码,但通常它能够在一些常规的联结过程中发现。因此我们要改变这些零的块。
| buffer fillup(*) | leaveret | fake_ebp0 | leaveret |
| int32 会被有缺陷的函数的返回地址保存覆盖
两个leaverets是必要的,由于有缺陷的函数当返回的时候不会设置堆栈指针。由于"帧伪造"教"堆栈指针增长"有优势。一些时候是很必要的通过这种方法达到攻击。
3.4
嵌入零字节
还有一个问题:传给一个函数的参数中包含有零字节。当多个函数有效调用时,有一个简单的解决方法:先调用的函数通过嵌入零字节到下一个要调用的函数的参数的地址。
Strcpy是我们经常