不错的文章--续--CS:APP bufbomb 缓冲区溢出攻击

CS:APP bufbomb 缓冲区溢出攻击

要攻击的程序源代码:

http://csapp.cs.cmu.edu/public/1e/ics/code/asm/bufbomb.c

目标是输入特定的字符,让程序最终输出0xdeadbeef.

需要注意的是该程序接受数据是字符的16进制编码,比如你如果想让getbuf函数中的buf中的内容变成“0123”,那么你要输入30 31 32 33.

以前听到这个缓冲区溢出攻击这个词总觉得高深莫测,那是黑客的本领,最近在看CS:APP发现上面就有缓冲区溢出的实验,从这本书的名字可以看出,就是让你知道计算机底层都在干什么,推荐每个写程序的人都看一下。

/* $begin getbuf-c */
int getbuf()
{
    char buf[12];
    getxs(buf);
    return 1;
}
 
void test()
{
  int val;
  printf("Type Hex string:");
  val = getbuf();
  printf("getbuf returned 0x%x\n", val);
}

上面的代码可以看出,val的值应该是总是为1的,所以输出总是1,现在就是让你写入特定的字符,让其输出为0xdeadbeef.这个实验只是简单介绍了缓冲区溢出攻击的原理,但是随着编译器的发展,这个实验也越来越难做了。目前我只能做到禁用gcc的堆栈保护,而且只在gdb中成功,真实运行的时候虽然成功的输出了0xdeadbeef,但是程序结束后总是伴随着段错误。这里记录一下学习的过程,我觉得这个实验还有改进的余地,只是目前对编译器以及汇编的掌握还不够熟练。

这个程序的缓冲区溢出攻击是十分依赖于机器的,编译器的版本,系统内核的版本等等因素都会导致生成的可执行文件不同,所以这个实验需要了解程序的机器级表示的相关知识,也就是CS:APP的第3章的内容,windows版本的攻击应该是更为简单的,目前我还没有听到windows有类似linux的Exec Shield Overflow Protection.

可以说,通过这次实验的学习,发现了很多对程序运行过程的误解,操作系统学的真烂=.=

首先,一个进程是怎么在内存里布局的?强烈建议读一下这篇blog:http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory,在现代的操作系统中,每个进程都运行在自己的虚拟地址空间中,类似一个沙盒,让每一个进程都感觉自己运行在一个4G的内存空间中,当然这只是给进程以及用户的一种幻觉,实际上每个进程的虚拟地址空间都会被分页机制映射到物理地址的页或者虚拟内存的页,虚拟内存就是当内存不够时用硬盘充当一部份内存使用,比如linux的swap分区。

下面仅以linux说明可执行文件的运行过程。

其中,在0-0xFFFFFFFF的虚拟地址空间中,高地址的1G(0xC0000000-0xFFFFFFFF)范围被linux用作内核空间,用户空间是低的3G范围。如下图所示(来自上面链接的文章):

这里吗可以看出中间有许多random offset,这也加大了缓冲区溢出攻击的难度,其中栈区是有固定大小的,可以输入ulimit -s来确定,我的机子返回的是8192,也就是8M的空间,当你写了一个无限递归的函数时,这个空间就会很快被填满,然后导致stack overflow,堆区理论上可以一直向上申请到Memory Mapping Segment,不过一般没有程序会使用这么大的内存。

这里我很好奇的是0×08048000这个数是怎么来的,Google了一下,好像都没有说明,不知道有没有什么历史。不过这个数是作为linux应用程序的起始地址。但是如果用objdump反汇编一个程序,会发现程序的文本段,比如main函数的位置并不是从0×08048000开始的,这是因为0×08048000首先存放的是ELF header,包含了这个程序的信息。

每个进程的虚拟内存布局可以通过查看/proc/pid/maps这个文件来查看,其中pid换成想要查看的进程编号,比如实验的./bufbomb布局如下:

这里可以看到0×08048000以下的地址被用来映射许多动态链接库,比如libc和ld,这和上图的布局有些出入,估计是因为linux的内核升级,将memory mapping segment存放到了0×08048000下面?以前最下面的空白区是不做任何用途的。

缓冲区溢出攻击只是针对程序的栈这一部分,在栈里面会存放程序运行的栈帧,因为当一个函数调用另一个函数时,要将当前运行的信息保存起来(相当于入栈),然后再跳转到另一个函数的地址执行,执行完后要回到调用这个函数的下一条指令接着执行,然后就将保存的信息取出(相当于出栈),入栈出栈的操作是通过ebp寄存器和esp寄存器来完成的。这两个寄存器都指向栈区,可以这么理解,ebp指向某个过程的基地址,esp指向某个过程的栈顶,这里的某个过程通常来说就是指函数。ebp和esp之间,就是某个过程(函数)运行需要的所有数据。

拿上述的bufbomb.c来说,用gcc 将其编译为可执行文件:

$gcc bufbomb.c -o bufbomb -fno-stack-protector

之所以加上后面的-fno-stack-protector是为了禁用gcc的堆栈保护。

然后用objdump将bufbomb反汇编

$objdump -d bufbomb

然后可以得到bufbomb的代码如下,只取test函数和getbuf函数的:

08048532 <getbuf>:
8048532: 55                  push %ebp
8048533: 89 e5               mov %esp,%ebp
8048535: 83 ec 28            sub $0x28,%esp
8048538: 8d 45 ec            lea -0x14(%ebp),%eax
804853b: 89 04 24            mov %eax,(%esp)
804853e: e8 21 ff ff ff      call 8048464 <getxs>
8048543: b8 01 00 00 00      mov $0x1,%eax
8048548: c9 leave
8048549: c3 ret

0804854a <test>:
804854a: 55                  push %ebp
804854b: 89 e5               mov %esp,%ebp
804854d: 83 ec 28            sub $0x28,%esp
8048550: b8 c0 86 04 08      mov $0x80486c0,%eax
8048555: 89 04 24            mov %eax,(%esp)
8048558: e8 03 fe ff ff      call 8048360 <printf@plt>
804855d: e8 d0 ff ff ff      call 8048532 <getbuf>
8048562: 89 45 f4            mov %eax,-0xc(%ebp)
8048565: b8 d1 86 04 08      mov $0x80486d1,%eax   #printf args 0
804856a: 8b 55 f4            mov -0xc(%ebp),%edx
804856d: 89 54 24 04         mov %edx,0x4(%esp)
8048571: 89 04 24            mov %eax,(%esp)
8048574: e8 e7 fd ff ff      call 8048360 <printf@plt>
8048579: c9                  leave
804857a: c3                  ret

然后通过gdb调试可以得到下面的栈帧图,我保存在了google doc里面:

左边绿色的部分是test函数的栈帧,黄色的部分是getbuf函数的栈帧,蓝色部分是下面的栈帧执行完后要跳转到的指令地址,这个地址每次call的时候自动填充,所以理论上应该是属于上衣个栈帧的内容,这里由于返回后的esp并没有指向这个返回地址,所以没有将其包含进栈帧。目前我明确的参数都有所注释,还有许多地址中的数据我还不明白其中的含义,所以这里还值得研究一下。目前该图含有的信息:

一个函数执行完成后要跳转到的地址(由ret指令来完成),比如getbuf函数的栈帧(黄色区域),其执行完后eip的地址即为0×08048562。

printf的参数放在第27,28行,分别表示printf的第0个参数,第一个参数是1,因为最后printf只会打印一个1出来,第0个参数则是字符串的地址,用gdb可以获得其数据:

需要注意的是栈帧图中的28行为0x080486c0,而不是上面gdb调试中的0x080486d1,是因为0x080486c0是第一次printf的参数,也就是打印”Type Hex String:”字符串的地址,而0x080486d1才是第二个printf的字符串地址。这个地址怎么得到的?可以通过gdb单指令调试,其实汇编语言中就有,见上文汇编代码中注释的那一行。

现在我们的目标是让test中的第二个printf输出0xdeadbeef,而不是程序中默认的1。

首先,buf数组开的大小有12,但是系统并不检查边界(现在的gcc版本可能会有相关的处理机制),所以我们可以一直写,写多了,就把高地址的数据覆盖了。我们的目标至少要覆盖到printf的第二个参数,也就是上面栈帧图中红色的区域,当然上面多了一行,只是为了演示如果我们想,完全可以把整个栈都覆盖掉。

现在的关键就是要写入什么数据,随便写肯定程序的eip都不知道运行到那去了。

首先要保证第30行的数据不变,因为这里保存了函数返回后test的ebp。

然后我们要绕过给val赋值为1,并用printf输出这一块,可以让getbuf返回后直接执行printf,所以要让eip直接返回到printf函数的指令,即将第29行的数据写为0×08048574,看上面的汇编代码可以知道,这里存放的就是printf的指令。

现在可以直接执行printf了,剩下的就是设置参数了,第0个参数设置为0x080486d1,原因上文已经说过了,第1个参数就是输出的内容,应该是0xdeadbeef.

ok,这就是我们需要注意的所有地方,至于从31到35行要填入什么数据就随意了,需要注意的是,由于一次填写2个字符,所以要将上述必须填的顺序逆序的输入,比如你想存0×12345678,输入的时候应该是78 56 34 12 。

这样,就完成了一次缓冲区溢出攻击:)

但是,不再gdb下运行的时候,虽然也返回了0xdeadbeef,但是总是伴随着段错误。

 

应该还是有改进的空间,下一步可以尝试在stack protector下的溢出攻击。

Posted in 学习编程 Tagged  2 Replies

关于内存对齐

最近在看Ptypes这个库的代码,发现了许多以前根本就没有考虑到的问题,现在对内存对齐的学习总结下。

以前听李龙海老师讲课的时候听到过,但是一直没有研究过,只知道有这么一回事。内存对齐的设计是为了加快计算机读取内存的速度。这里的文章通过实验以及图表分析了计算机是怎么读取内存的,会点英语的最好看这篇。

简单来说,计算机设计的时候对内存的读取都是按块读取的,比如现在大部分计算机还是32位的,虽然64位的正在逐渐普及。所以说下面说的都针对32位的计算机。32位的计算机由于一次能处理32bit的数据,所以一般数据总线也是32bit,也就是说一次可以从内存读取或者写入4个字节的数据。

Click To expand

但是CPU是比较懒的,它总是从4的整数倍的位置开始读,比如一下子读取0,1,2,3这4个字节的数据,或者读取4,5,6,7字节的数据。但是如果让CPU读取1,2,3,4这4个字节的数据,CPU会先读入0,1,2,3字节的数据,执行一次位移操作,将第0个数据的字节丢掉,然后将1,2,3个字节写入寄存器,然后读取4,5,6,7字节的数据,将5,6,7的数据位移后丢掉,写入寄存器。这样一来,同样是4个字节,CPU却用了2个读写周期。而且随机取4个字节有3/4的概率会执行2次操作,平均要用V=1/4+2*3/4=7/4个周期。为了加快程序的运行速度,所以才有了内存对齐的技术。

基本数据类型都有固定大小,如int,char等,内存对齐的技术主要针对于自定义的类型,比如结构体,类等。

写程序的时候可以通过#pragma pack(n)来指定内存对齐的字节大小,设这个数位pN

下面以结构体为例介绍内存对齐的规则:

  1. 结构体中第一个数据的位置在0这个位置(相对于结构体的起始位置),并且每个成员x的相对于结构体开始位置必须为min(pN,sizeof(x))的倍数。
  2. 最后结构体整体也要对齐,而且大小必须为min(pN,max length of its’ member size),也就是说其结构体中最大的数据成员的大小,和pN的值取最小值,所得数的倍数。

举例来说,有一结构体:

   1:  
   2: typedef struct
   3: {
   4:     char a;
   5:     int b;
   6:     short c;
   7: }node;

 

我利用指针的强制转换输出这个结构体内部的细节,代码如下:

   1:  
   2: int main()
   3: {
   4:     node x; 
   5:     printf("sizeof(x)=%d\n",sizeof(x));
   6:  
   7:     x.a=1;
   8:     x.b=2;
   9:     x.c=3;
  10:     char *p=(char*)&x;
  11:     int i;
  12:     for(i=0;i<sizeof(x);++i)
  13:     {
  14:         printf("%x -> %x\n",p,(int)*p);
  15:         p++;
  16:     }
  17:      return 0;
  18: }

运行结果如下:

image

因为Intel的机器都是小字节对齐的,所以比如x.b=2后,地址的高位都是0,至于出现的fffffcc这是由于内存对齐所带来的填充数据,图中可以很清晰的看出内存对齐的策略。

Continue reading 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值