3.ROP
ROP 即 Return Oritented Programming ,其主要思想是在栈缓冲区溢出的基础上,通过程序和库函数中已有的小片段(gadgets)构造一组串联的指令序列,形成攻击所需要的 shellcode 来实现攻击效果。这种方式不需要注入新的指令到漏洞程序就可以改变某些寄存器或者变量的值,从而改变程序的执行流程。
攻击者可以通过工具对目标程序搜索,寻找所需要的指令序列。通常选取的指令序列以 ret 指令结尾,这样每一个单独的指令序列结束时,都会执行 ret 指令,即会将此时栈顶的数据放入寄存器 %eip 中。攻击者在构造输入时,只需将所需的指令序列的起始地址按顺序放置在栈中,这样前一个指令序列执行完成后即会通过 ret 指令获得位于栈上的下一个指令序列的地址,从而实现一个 ret 执行链,实现一定的攻击效果。
使用 ROP 技术主要需要两个方面的准备:(1)获得所需要的指令片段的地址;(2)构造栈上的数据,使得选取的指令片段能够串联在一起;
3.1 示例程序
这里仍使用 缓冲区溢出基础实践(一)——shellcode 与 ret2libc 中使用的实例程序进行说明。使用 gcc -m32 -static -fno-stack-protector -g -o hello hello.c 生成可执行文件 hello,此时 hello 的栈数据是不可执行的,这里为了使得程序中能有较多的可供利用的指令序列,使用静态链接的方式生成可执行文件。
gcc -m32 -static -fno-stack-protector -g -o hello hello.c //生成可执行文件 hello,其栈不可执行
3.2 ROP注入过程
这里主要通过 ROP 技术实现一个简单的 execve 函数调用 execve( "/bin/sh" , NULL , NULL ),之后通过 exit(0)返回。该过程主要通过以下几个步骤实现:
a.系统调用号为0xb,即令 %eax 值为0xb
b.第一个参数为字符串"/bin/bash"的地址,即令 %ebx 值为字符串地址
c.第二和第三个参数为NULL,即 %ecx 和 %edx 的值为 NULL,之后执行 int 0x80 即可
d.系统调用号为0x1,即令 %eax 值为0x1
e.第一个参数为0,放置在 %ebx 中,之后执行 int 0x80
为了实现上述函数调用,通过在程序中已有的指令序列中寻找所需的指令序列,实现对寄存器的修改和调用过程。
(1)寻找所需的指令序列和字符串信息
这里选用 ROPgadget 实现对目标程序指令序列的选取,ROPgadget 的 GitHub 地址在这里。
a.选择对 %eax 寄存器进行修改的指令序列,这里选择第三个序列,其地址为0x080b7f86.将其视为指令序列1.可在上述的步骤 a 和 d 中使用。
b.选择对 %ebx 寄存器进行修改的指令序列,这里选择以下指令序列,可同时对 %edx、%ecx 和 %ebx 的值进行修改,其地址为 0x0806ee00.将其视为指令序列2.
另外也获取一个仅对 %ebx 修改的指令序列,其地址为 0x080481c9,将其视为指令序列3.
c.获取所需的字符串"/bin/bash"的地址,通过 ROPgadget 直接进行指令搜索时并没有找到所需的字符串,但是正如之前 ret2libc 中所利用的,环境变量 SHELL 中会包含一个"/bin/bash"字符串,获取其地址即可。这里为 0xffffd337.
d.获取系统调用 int 0x80 的地址,可获得一个地址为 0x0806ca55 的指令序列。将其视为指令序列4.
(2) 根据获得的指令序列,即可构造所需的 ROP 工作链,构造出的输入数据应该为 填充数据( 76 字节) + 指令序列1的地址( 4字节 ) + %eax的值( 4字节,0xb ) + 指令序列2的地址( 4字节 ) + %edx 的值( 4字节,NULL ) + %ecx 的值( 4字节,NULL ) + %ebx 的值( 4字节,字符串地址 ) + 指令序列4的地址( 4字节 ) + 指令序列1的地址( 4字节 ) + %eax的值( 4字节,0x1 ) + 指令序列3的地址( 4字节 ) + %ebx 的值( 4字节,0 ) + 指令序列4的地址( 4字节 )。
使用指令序列1的地址覆盖原函数的返回地址,则原函数返回时控制流跳转至指令序列1执行(返回地址已出栈),此时构造数据中"eax的值"位于栈顶,指令序列1会将其出栈并赋值给寄存器 %eax,此时指令序列2的地址位于栈顶,指令序列1执行 ret 指令时,即跳转至指令序列2执行,依次类推,构造一条完整的执行链。
构造的数据如图所示
(3) 对目标执行注入过程,结果如图所示,可以看到成功打开了一个 shell 。
在上述过程中,同样需注意调试和运行时环境变量对程序中布局变量地址的影响,但指令序列位于 .text 段,不受位于栈上的环境变量的影响,而 ROP 攻击所需的字符串参数仍需从环境变量 SHELL 中获得,故而可在 gdb 运行时设置 wrapper 程序为 env -u LINES -u COLUMNS -u _ ,而程序直接运行则通过命令 env -u _ hello程序完整路径 ,具体可参考之前实验的过程。
3.3 总结
使用 ROP 技术进行缓冲区溢出实践,充分利用了可执行程序中已有的指令序列,通过合理构造栈上数据的顺序,实现对寄存器和控制流的修改,从而实现一个完整的攻击序列,这种方法不需要额外的指令注入,对于以寄存器传递参数的 x86_64 平台同样有效,故而较之单纯的 ret2libc 技术要更有使用价值。但同样的,简单的 ROP 技术利用的指令序列的地址是通过硬编码的方式写入缓冲区的,当开启 ALSR 机制后,上述指令序列的地址在程序加载时会发生变化,此时想要使用 ROP 机制则需要进行进一步的处理。
4.hijack GOT
目前的 ELF 编译系统使用一种成为延迟绑定( lazy binding )的技术来实现对共享库中函数的调用过程。该机制主要通过两个数据结构 GOT 和 过程链接表( Procedure Linkage Table , PLT )实现。其简化的原理为 : 当目标模块存在一个外部共享库的函数调用时,其在汇编层面使用 call 指令实现调用,其作用为跳转至对应函数的 PLT 表项处执行,该表项的第一条指令为 jmp *[ 对应 GOT 项的地址 ],第一次执行函数调用时,通过 GOT 与 PLT 的合作,会将最终调用函数的地址确定下来,并存放在其对应的 GOT 表项中。当后续再发生调用时, jmp *[ 对应 GOT 项的地址 ] 指令即表示直接跳转至目标函数处执行。
4.1 示例程序
根据上述延迟绑定技术的简化原理,我们可以知道当发生过一次函数调用后,该函数的 GOT 表项存放的即为函数的真实调用地址,之后的函数调用过程会直接使用这个地址,而若能够借助缓冲区溢出手段修改该 GOT 表项的地址,即可修改程序的执行流程,达到一定的攻击目的,该方式过去通常与格式化字符串漏洞一起使用。这里仅做一个简单的 hijack GOT 示例,供实践的程序如下。
1 #include <stdio.h> 2 #include <stdlib.h> 3 int main( int argc , char *argv[] , char *envp[] ) 4 { 5 char *pointer = NULL; 6 char array[10]; 7 pointer = array; 8 strcpy(pointer, argv[1]); 9 printf("Array contains %s at %p ", pointer, &pointer); 10 strcpy(pointer, argv[2]); 11 printf("Array contains %s at %p ", pointer, &pointer); 12 13 return 0; 14 }
使用以下命令编译源程序,得到可执行文件 hello.
gcc -m32 -fno-stack-protector -g -o hello hello.c
程序的执行效果如下图所示,通过对参数的拷贝,程序输出对应的参数字符串的值,但由于并未对参数的内容和长度做检查,使得可以通过构造数据实现对格式化字符串漏洞的利用。
4.2 注入过程
作为一个简化的示例程序,这里的第一个 printf 函数调用用于对 printf 函数的延迟绑定,中间的字符串拷贝过程完成 GOT 表项的修改,而第二个 printf 函数则实现最终 hijack GOT的效果。这里以将 printf 的 GOT 表项的地址修改为 system 函数的地址为例进行说明,需要完成以下步骤:
1.使得 pointer 变量执行 printf 的 GOT 表项;
2.通过 strcpy 完成对 pointer 指向的地址位置的内容的修改为 system 函数调用地址;
3.对 printf 的函数调用实际为 system 函数的功能;
具体的注入过程如下:
(1)通过对 hello 程序的反汇编阅读可以发现,指针变量 pointer 位于 %ebp - 12 处,而缓冲区数组 array 起始地址为 %ebp - 22 ,则可以通过构造数据覆盖 pointer 变量的值,使得 pointer 变量指向特定的地址位置;
(2)对程序进行动态调试,确定所需的 printf 函数的 GOT 表项的位置和 system 函数的地址。这里同样需要对环境变量进行控制,以保证使用 gdb 获得的地址至与程序直接运行时一致。
set exe-wrapper env -u LINES -u COLUMNS -u _ //设置 wrapper 函数,之后即可下断点调试
使用 gdb 获得 printf 函数的 GOT 表项的位置为 0x804a00c,在通过第一个 strcpy 函数进行溢出操作时,需使得 pointer 指向该地址处。
同样通过 gdb 获得 system 函数的地址,后续需通过 strcpy 将原 GOT 表项处的值修改为 0xf7e3cda0.
(3)构造输入数据实现 GOT hijacking,对于 argv[1],其数据应为 填充字节( 10字节 ) + 0x804a00c( 4字节 ),这样第一次 strcpy 调用完成后,pointer 变量即指向了 printf 函数的 GOT 表项。对于 argv[2],其数据应该为 0xf7e3cda0( 4字节 ),这样第二次 strcpy 调用完成后即完成对 GOT 表项的修改。
通过如下命令进行注入。
env -u _ ./hello `perl -e 'print "A"x10'``printf "x0cxa0x04x08"` `printf "xa0xcdxe3xf7"` //由于注入过程没有使用栈上的局部变量地址,故而可以直接使用 ./hello 启动程序
注入结果如图所示:
这是由于使用 system 函数替换 printf 函数后,system 调用会试图对原来 printf 的参数 "Array contains "进行解释,而其无法找到对应的执行对象造成的。这里即可通过自地义一个 Array 程序对其进行利用。
(4)编写一个简单的 Array 程序,其源代码如下。
1 #include<stdlib.h> 2 int main() 3 { 4 system("/bin/sh"); 5 return 0; 6 }
通过 gcc -o Array Array.c 编译获得一个 Array 可执行文件,并将其复制至 /bin 文件夹下。再次通过步骤(3)的方式对程序进行注入,即可获得一个 shell。
注:将 Array 程序复制至 /bin 目录需要 root 权限,这里这样做是为了演示方便,实际情况下可以选取的方案包括将当前目录 . 加入环境变量 PATH 中。
4.2 总结
通过对延时绑定技术实现中重要的数据结构 GOT 表的修改,Hijack GOT 实现了对目标函数执行流程的修改,这种方式同样可以绕过栈不可执行的保护限制,实现一定的攻击效果,其甚至可以将 SSP 机制中检测到缓冲区溢出后的处理函数 __stack_chk_fail 替换,从而使得 SSP 机制失效。但该方法在如何定位对应的 GOT 表项、如何构造输入数据实现对 GOT 表项的修改等问题上有一定的实现难度,即使在上述比较简单的例子的基础上,也需要结合较有利的格式化字符串漏洞实现 GOT 表项修改的过程。
参考资料:
(1)Return-into-libc 攻击及其防御 - IBM developerWorks
(2)基本ROP - CTF Wiki
(3)How to hijack the Global Offset Table with pointers for root shells