高级返回库函数exploit代码实现(上)
高级返回库函数exploit代码实现(上)
作者:Nergal <[email protected]>
翻译:null(盈星满月)<[email protected]>
-【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 /lib/libc-2.1.92.so
4b28f000-4b293000 rw-p 00000000 00:00 0
bff78000-bff7b000 rw-p ffffe000 00:00 0
$ exit
nergal@behemoth 9 > 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
48b07000-48b1d000 r-xp 00000000 03:45 107760 /lib/ld-2.1.92.so(地址不同前面)
48b1d000-48b1e000 rw-p 00015000 03:45 107760 /lib/ld-2.1.92.so(下同)
48b1e000-48b1f000 rw-p 00000000 00:00 0
48b25000-48c40000 r-xp 00000000 03:45 107767 /lib/libc-2.1.92.so
48c40000-48c46000 rw-p 0011a000 03:45 107767 /lib/libc-2.1.92.so
48c46000-48c4a000 rw-p 00000000 00:00 0
bff76000-bff79000 rw-p ffffe000 00:00 0
可见,增加的CONFIG_PAX_RANDMMAP特征,将不容易返回到库函数了,每次运行调用的库函数的地址都不同。
同时,Pax还不是很完善,有下面一些明显的瑕疵,不过有些可以解决完善。
1)如果/proc/pid_of_attacked_process/maps 具有可读的情况,那么本地的攻击代码中的库函数和被映射的堆栈地址可以从maps被获得。如果被攻击的程序开始运行后,攻击程序将有通过运行的被攻击程序覆盖堆栈的缓冲的准备时间,一个攻击者可以利用可用的信息来构建堆栈中的溢出代码数据。比如,如果溢出代码中用到的数据来自于程序中调用函数的某个参数或者是该程序的环境,那么攻击者会无所作为,但是,如果溢出代码是来自于一些I/O(输入/输出)的操作(比如,socket或是read files),那么攻击这将成功。
解决方法:禁止访问/proc 目录。象其他的一些安全补丁一样:)
2)攻击者可以"暴力"猜测出mmap后的库函数的地址。通常(将6。1节的结尾)有足够的条件能够"暴力"猜测到库函数的基址地址,通过大约数万次的尝试,攻击者有一半的机会猜对。当然,每一次的失败都会被系统日志记录,但是,对安全而言并不可靠,如果日志被篡改:)
解决方法:配置segvguard(见文[8]),它是一个守护精灵,每一次的一个进程通过信号SIGSEGV或类似的信号进入内核将被通报,Segvguard将临时禁止该程序继续执行(可以防止暴力猜测攻击进行),并且Segvguard还有一些有趣的功能,在没有Pax 的情况下,它值得一试。
3)库函数的信息和堆栈的地址,如果利用格式化漏洞,将被泄露。比如,wuftpd的缺陷,攻击者可以通过site exec [eat stack]%x.%x.%x...命令来探测堆栈的情况。隐藏在堆栈中变量的指针将自动的在栈基呈现出来。由于对一个目标的库函数而言,动态链接和库函数的启动程序里保留有堆栈的一些指针和一些函数返回地址,利用这些,攻击者可以推论出库函数的基址地址。
4)一些特殊的情况,在被攻击的程序中,攻击者可以找到一类函数,它们的位置是固定的,而且不能被mmap函数进行随机映射。比如,"su"就是这样的一种函数(被证明调用成功),利用它可以获得root权限和执行的shell,这已经足够了:)
5)对有缺陷的函数,其所有的库函数的调用都要经过PLT过程连接表的入口,象这样一个程序,PLT必须在一个固定的地址,有缺陷的程序通常是很大的并且调用许多的库函数,这种条件很可能在PLT找出一些有趣的东西来,达到攻击目的。
实际上,后面3个问题还不能够解决,而且上面的5点,我都不能够保证能够成功的代码实现,特别是4种情况是很少的。我们的确需要更普遍的方法来实现。
在下面的章节中,我将描述动态链接库中的dl-resolve() 函数的界面。如果它的适当的一个参数可以又ascii支持的字符串的函数名字,并且能够决定实际的函数地址,和dlsym()相类似。使用dl-resolve() 函数,我们可以构建返回库函数的实现代码,它返回到一个函数,但是在我们构建代码的时候,我们并不知道它返回的确切地址。见文[12]描述了从函数名字中获得该函数地址的途径,不过现有的技术还没能达到我们的目的。
--[ 5 - 动态链接dl-resolve()函数
这章尽量的简单的说明,更详尽的描述请参阅文[9],和glibc的源码,特别是dl-runtime.c。请见文[12]。
----[ 5.1 - 一些ELF(可执行和链接格式的文件)的数据类型
下面是头文件elf.h中的一些定义:
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
/* How to extract and insert information held in the r_info field. */
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
----[ 5.2 - 一些ELF的数据结构
ELF可执行文件包含一些我们感兴趣的数据结构(主要是数组),这些结构的位置可以从ELF执行文件的动态区域找到。使用 "objdump -x file"命令可以显示文件动态区域的的头部信息(动态符号表、重定位入口等)。
$ objdump -x some_executable
... 得到一些有用的信息(动态符号表、串表,重定位入口,etc)...
Dynamic Section:(动态段区域)
...
STRTAB 0x80484f8 the location of string table (type char *)
SYMTAB 0x8048268 the location of symbol table (type Elf32_Sym*)
....
JMPREL 0x8048750 the location of table of relocation entries
related to PLT (type Elf32_Rel*)
...
VERSYM 0x80486a4 the location of array of version table indices
(type uint16_t*)
"objdump -x" 也能够显示出 .plt(过程连接表)处的动态段的信息, 0x08048894地址的如下列子:
11 .plt 00000230 08048894 08048894 00000894 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
----[ 5.3 - 如何从PLT过程连接表中调用dl-resolve()
一个典型的PLT过程连接表入口(当elf格式为elf32-i386)象下面这样:
(gdb) disas some_func
Dump of assembler code for function some_func:
0x804xxx4 <some_func>: jmp *some_func_dyn_reloc_entry
0x804xxxa <some_func+6>: push $reloc_offset
0x804xxxf <some_func+11>: jmp beginning_of_.plt_section --->过程连接表入口
PLT过程连接表入口值不同,仅仅取决于$reloc_offset的数值(包括some_func_dyn_reloc_entry的值,以后,它作为运算中的符号)。我们可以看到在代码段,压$reloc_offset入栈,并跳到.plt过程连接表段的入口。这几个指令后,控制权转到dl-resolve()函数,reloc_offset作为其一个参数,下面是函数dl-resolve()简要的运算说明:
1)计算函数入口地址
Elf32_Rel * reloc = JMPREL + reloc_offset
2)计算函数symtab入口地址
Elf32_Sym * sym = &SYMTAB[ ELF32_R_SYM (reloc->r_info) ];
3)结构完整检查
assert (ELF32_R_TYPE(reloc->r_info) == R_386_JMP_SLOT);
4)glibc 2.1.x 或更新的版本,要做另一项检查,如果符号不等于零sym->st_other & 3 != 0,则假设符号已经由前面决定了,就要转向其他的运算法则方式。我们必须确保符号等于零sym->st_other & 3 == 0。
5)如果符号版本正确,检查版本索引表
uint16_t ndx = VERSYM[ ELF32_R_SYM (reloc->r_info) ];
并找出版本信息
const struct r_found_version *version =&l->l_versions[ndx];
l是link_map 的参数,这里重要的是ndx必须是一个合法的数值,0最好,0意味着"local symbol"。
6)函数名字(asciiz字符串)的检查
name = STRTAB + sym->st_name;
7)检查函数的地址,收集足够的信息,隐藏的两个变量的Elf32的地址定位在reloc->r_offset和sym->st_value。
8)调整堆栈指针,准备调用一些函数
注意:在一些glibc下,fixup()函数执行完成运算的规则,dl-runtime-resolve()完成调用工作。
----[ 5.4 -结论
假设我们溢出堆栈缓冲区如下所示:
--------------------------------------------------------------------------
| buffer fill-up | .plt start | reloc_offset | ret_addr | arg1 | arg2 ...
--------------------------------------------------------------------------
^
|
- int32 会被有缺陷的函数的返回地址保存覆盖
如果我们准备适当的sym和reloc变量值(分别是Elf32_Sym和Elf32_Rel类型),并计算reloc_offset的数值,将控制权传递给函数,函数名可以找到在STRTAB + sym->st_name,适当的放置参数arg1, arg2 ,我们就有机会返回到另一个函数(ret_addr)。
dl-resolve.c 是个简单的执行以上描述的技术的攻击代码,注意,你需要编译这个代码两次,详见后面的README.code代码部分。
--[ 6 - 战胜PaX
----[ 6.1 - 必要的条件
为了使用在第5章中描述的返回动态链接技术,我们需要适当的位置安置一些结构,我们还需要一个可以移动字节到选定区域的函数,呵呵,最佳人选是strcpy,不过strncpy, sprintf 等也很不错。象文[3]中那样。我们要求在被攻击的程序映像中的strcpy有PLT(过程连接表)的入口。
返回动态链接技术解决了被随机映射的库函数的问题。然而在堆栈中,还不能得到解决,如果溢出的代码在堆栈中寄存,那么我们不能够知道其确切地址,我们也不能通过strcpy够插入0的块(见3。3节)。很遗憾,还不能够拿出一个一般的解决这个问题的方法。(你可以吗?)下面两种方法可能达到要求。
1) 如果在PLT过程连接表中,scanf()是有效的函数。我们可以执行象下面的一些事情:
scanf("%s\n",fixed_location)
这样它将一些构建堆栈的结构的输入拷贝到fixed_location,如果我们用栈帧伪造的技术,那么堆栈的栈帧将脱节,所以我们用fixed_location 代替。
2) 如果被攻击的程序使用了-fomit-frame-pointer编译选项,我们通过esp增长的技术,联结调用多个strcpy函数,甚至可以不知道堆栈指针的数值(见3.2节结尾的注意事项)。第n个strcpy应该具有下面的参数形式:
strcpy(fixed_location+n, a_pointer_within_program_image)
这种方式我们可以一个字节一个字节的构建适当的栈帧在fixed_location。这样做了后,我们灵活的组合"esp增长"和栈帧伪造的技术,这种方法请见3。3节。
很多类似的工作区构建可以被设计,不过没有多少必要,这些工作很象拷贝一些用户控制的数据到静态数据段或者内存中划分变量区域。因此,上面就是我们的主要工作了,无须多说。
总而言之,我们需要见到两种条件:
6.1.1) strcpy (或 strncpy, sprintf及其类似的函数)有效的通过PLT过程连接表入口。
6.1.2) 在被攻击程序执行的一般过程中,拷贝了用户私有数据到静态变量区域或是内存中划分的变量区域。
----[ 6.2 - 构建exploit
我们将仿照dl-resolve.c这个实现代码的列子,通过调用mmap函数实现返回动态链接技术的方法获得一个可以读,写,执行的内存区域,利用strcpy函数将shellcode 放置在这个区域作为返回函数的地址.我们讨论的被攻击的程序没有使用 -fomit-frame-pointer 这些编译选项,和栈帧伪造.
我们需要确保下面3个有关联的结构适当的放置位置.
1) Elf32_Rel reloc
2) Elf32_Sym sym
3) unsigned short verind (which should be 0)
verind的地址是怎样和符号sym关联的?将ELF32_R_SYM 的数值赋给"real_index" (reloc->r_info);
则:
sym(符号) is at SYMTAB+real_index*sizeof(Elf32_Sym)
verind is at VERSYM+real_index*sizeof(short)
将verind放置在.data数据段 或者 .bss未被初始化的数据段,并通过两次strcpy函数调用使其为零,看起来很自然,不过不幸运的,在这种情况下,real_index将变得非常的大,sizeof(Elf32_Sym)=16远远大于了sizeof(short), sym的地址将远离程序进程的数据段.所以,应该明白为什么在dl-resolve.c 代码中需要从内存中分配上万字节的空间.
好了,现在可以通过设置MALLOC_TOP_PAD_ 的环境变量来扩大任意的执行进程的数据段,不过只能在本地执行代码,我们寻找更普遍的途径来实现,我们通常可以降低verind的位置,放置在被映射的只读区域,我们寻找零短字节,该实现代码通过verind的地址来定位"sym"结构.
我们在那里寻找零短字节?首先,在溢出发生时,我们需要通过被攻击的程序的执行过程中在/proc/pid/maps文件中确定被mmap映射的能够写的,数据段可执行的内存区域的范围.是个包括[地地址,高地址]这样的区域.我们拷贝符号结构到这里.通过简单的计算可以知道real_index的地址在[(low_addr-SYMTAB)/16,(hi_addr-SYMTAB)/16]这个区域中,我们因而在[VERSYM+(low_addr-SYMTAB)/8, VERSYM+(hi_addr-SYMTAB)/8]该区域寻找短零字节.
当找到了适合的verind后,还得检查如下这些:
1) 符号的地址不能够和伪造栈帧相交叉.
2) 符号的地址不能覆盖在任何内在的连接数据,比如strcpy的GOT全局偏移表入口)
3)须知道堆栈指针会被转移到静态的数据段.必须要有足够的空间来放置动态链接程序所在的栈帧栈.所以,最好,但不是必须的,在伪造栈帧后面放置符号"sym".
一个建议:寻找零短字节的方式,最好使用gdb,比通过命令"objdump -s" 输出分析要好一些.因为后者不能够显示出.rodata 后面的内存区域:)
实现代码icebreaker.c 是一个很简单的攻击pax.c程序. vuln.c和 pax.c唯一的不同是拷贝到静态缓冲的环境变量不同罢了.(注意看后面的代码:)
--[ 7 - 其他更多
----[ 7.1 - 系统无关性
由于PaX是为linux系统设计的,所以在这篇文章中讨论的焦点也是对这种系统而言.然而 ,当今技术的发展和实现,趋向于系统的无关性.栈和栈帧指针,C函数调用的标准,ELF的描述,所有这些都在当今被广泛的使用和实现了.对dl-resolve.c 代码而言,我已经成功的在Solaris i386和 FreeBSD实现了.
不过mmap的第4个参数必须被调整,比如在BSD系统,MAP_ANON 的数值就不同的.在我实现的这两个系统,动态链接对符号版本影响不大,所以,不应该不容易实现:)
----[ 7.2 -其他类型的缺陷
对缓冲溢出而言,现有的技术是很基础的.返回某个地址的代码实现依靠的是不能更改堆栈%eip的单一的溢出,但是,在返回的函数地址,我们能够放置函数的参数在栈顶.
另外两个大的缺陷分类,malloc控制结构腐败和格式化字符串攻击.对前一个问题,我们用任意的数值覆盖其整型字节---非常的下,以至于逃脱了PaX的保护.后一个问题,我们通常改变任意字节的数目.如果我们能够覆盖%ebp和函数的%eip,我们不需要做其他的了.但是由于栈基是随机的地址,没有办法确定任何栈帧的地址.
通过改变一些 GOT全局偏移表 的入口,不过只能控制得到指令下一个的地址,对逃避Pax的保护还远不够.
不过,在这里有个开发实现代码的想法很可能能够实现.我们假设下面3个条件:
1)被攻击程序使用了-fomit-frame-pointer 编译选项.
2)一个f1的函数,它分配的堆栈缓冲区可以由我们来控制.
3)函数f2有一个格式话漏洞(或者错误使用free()),它被f1调用或者间接调用.
有缺陷的代码如下:
void f2(char * buf)
{
printf(buf); // 格式化漏洞
some_libc_function();
}
void f1(char * user_controlled)
{
char buf[1024];
buf[0] = 0;
strncat(buf, user_controlled, sizeof(buf)-1);
f2(buf);
}
假设函数f1()正被调用,通过构造的格式化字符串,我们可以改变some_libc_function库函数的GOT全局偏移表入口地址,变成下面代码的入口地址.代码如下:
addl $imm, %esp
ret
这是个函数的结尾代码,这种情况,库函数被调用,通过"addl $imm, %esp"指令改变了堆栈指针.如果我们选择适当的$imm,让堆栈指针指向我们能够控制的堆栈缓冲里面.这样一来,就象一个堆栈的缓冲溢出,我们可以链接函数,返回链接技术等等溢出方式来实现.
另一种情况:单一字节的堆栈缓冲溢出.这种溢出控制栈帧指针最后的一个字节,在第二个函数返回的时候,攻击者有机会可以通过单一字节的溢出技术通过控制%esp,获得堆栈的整个控制权,改变弹出的%eip转到执行我们的实现代码,现有的所有技术已能够实现.
----[ 7.3 - 其他"不可执行"的解决方法
对此,我知道两个解决的方法,在Linux i386系统,让所有的数据段不可执行.第一个是RSX[见文10].
由于,这种解决方法没有在堆栈中实现,也没有让库函数基址地址随机化,所以在3章中讲述了通过链接多个函数调用来实不可执行的突破.
如果我们要执行任意的代码,我们还需要额外的努力.比如在RSX系统,在保护模式可写的内存区域不能够放置执行代码.所以前面的mmap(...PROT_READ|PROT_WRITE|PROT_EXEC) 欺骗方法就不能够工作.但是任何不可执行的设计必须允许共享库函数里可以执行代码.
在RSX系统,通过这种办法,包含有shellcode的mmap(...PROT_READ|PROT_EXEC)达到了目的.远程利用的实现,其联结允许我们先创建这个文件。
第二种解决方法,和前一种类似的,是kNoX[见文11],不同的地方是对库函数进行了随机地址映射.其地址开始于0x00110000.(和Solar的补丁类似)在本文3.4节的末提及.这种保护仍然还不够:)
----[ 7.4 - 改进现有"不可执行"的设计
不知道是幸运还是不幸运,对保护者也许不幸运.呵呵,我没有见到可以让Pax更加牢固的方法以避免现有的技术攻击:)显然,由于 ELF 的标准描述过于详细的将其的某些对攻击者有用的技术细节公之于众,当然,一些现有的欺骗手段能够被得以制止.比如使用内核补丁.但不能够限制共享库函数在exploit技术中的实现.除非是对函数的联结来限制用法云云.
另一方面,如果正确的配置Pax,将会让现有的exploit技术的实现变得困难甚至是不能够实现.当Pax变得更加可靠的时候,应易于使用.呵呵,简单是另一种层次的防守:)
----[ 7.5 - 版本信息
所有代码均在以下补丁版本中测试:
pax-linux-2.4.16.patch
kNoX-2.2.20-pre6.tar.gz
rsx.tar.gz for kernel 2.4.5
代码可以在任何2.4.x 内核版本的系统运行,但不能够在2.2.x的版本!
--[ 8 - 参考的出版物和工程项目
[1] Aleph One
the article in phrack 49 that everybody quotes
[2] Solar Designer
"Getting around non-executable stack (and fix)"
http://www.securityfocus.com/archive/1/7480
[3] Rafal Wojtczuk
"Defeating Solar Designer non-executable stack patch"
http://www.securityfocus.com/archive/1/8470
[4] John McDonald
"Defeating Solaris/SPARC Non-Executable Stack Protection"
http://www.securityfocus.com/archive/1/12734
[5] Tim Newsham
"non-exec stack"
http://www.securityfocus.com/archive/1/58864
[6] Gerardo Richarte, "Re: Future of buffer overflows ?"
http://www.securityfocus.com/archive/1/142683
[7] PaX team
PaX
http://pageexec.virtualave.net
[8] segvguard
ftp://ftp.pl.openwall.com/misc/segvguard/
[9] ELF specification
http://fileformat.virtualave.net/programm/elf11g.zip
[10] Paul Starzetz
Runtime addressSpace Extender
http://www.ihaquer.com/software/rsx/
[11] Wojciech Purczynski
kNoX
http://cliph.linux.pl/knox
[12] grugq
"Cheating the ELF"
http://hcunix.7350.org/grugq/doc/subversiveld.pdf
申明:本文译者保留中文修改权力,未经译者同意,不得任意拷贝,转载!!
p.s Any problem( concerning the technique, or translation), welcome pass the E-mail<mail to:[email protected]> to proceed the discussion,and very welcome give any advise, and
thank.(任何问题,不吝赐教,谢谢~)
发布人:盈星满月 来自: