CSAPP阅读笔记-变长栈帧,缓冲区溢出攻击-来自第三章3.10的笔记-P192-P204

一.几个关于指针的小知识点:

1.  malloc是在堆上动态分配内存,返回的是void *,使用时会配合显式/隐式类型转换,用完后需要用free手动释放。

    alloca是标准库函数,可以在栈上分配任意字节数量的内存,用完自动释放。

2.指针的优先级较低:

  char (*p)[3],括号中优先级最高,所以p是一个指针,指向一个3个元素的char数组。

  char *p[3],  因为指针优先级较低,所以*与char结合,p代表一个3个元素的数组,每个元素都是一个char *。

3.函数指针:

     它的语法类似指向数组的指针,char (*f)(int *),同之前的分析,*f代表f是一个指针,比起char (*p)[3],此时后面的是(int *),所以此指针指向的是一个以int*为参数的函数,返回值类型是char。

  若改成 char *f(int *),则f不再是一个指向函数的指针,而是一个以char*为返回值,参数为int*的函数。

 

二.缓冲区溢出

先看个例子:

      

左边为c代码,右边为汇编代码,c代码中我们为char数组buf设置了8个字节的缓冲区,虽然在汇编代码中为它在栈上分配了24个字节(还记得吗,在3.7那里讲过什么时候要压栈,这里属于第二种情况,当被保存的局部变量需要被访问到地址时,比如这里的数组,要把这个局部变量压栈,而不能仅仅用寄存器传递),剩下16个字节是不被使用的,而在gets中,由于未指定目标缓冲区大小,当输入超过8个字符时,显然会破坏后续的栈空间,在23个之前不会有太大影响,而一旦超过23个,会破坏返回地址的值,若超过32个字节,会破坏调用函数(即echo)里,调用caller之前存的值((还记得吗,3.7中讲过,返回地址是在存完多余参数,局部变量,被调用者保存寄存器后,由call存入的,可以参考书中3.7节的图3-29来理解)。

具体情况如下图:

 

给一道很有意思的例题:

 

这道题里有很多值得注意的点,首先,这儿的返回地址是调用get_line的那个函数压入的,跳转到get_line函数后,get_line先把%rbx压入了栈,这可能是因为后面get_line函数用到%rbx寄存器(还记得3.7讲的被调用者保存寄存器吗,%rbx是其中之一),所以要先压栈保存当前值,此时第二行是01 23 45 67 89 AB CD EF,随后要为局部变量buf数组分配栈空间,分配了0x10=16个字节,所以第4行是%rsp的位置,要注意的是,调用gets后,存入输入的字符串的顺序是先存到%rsp,再到1(%rsp),一直延续的,所以第4行对应存前8个字符,且从后向前存,结果是: 37 36 35 34 33 32 31 30,同理,第三行结果是: 35 34 33 32 31 30 39 38,再向上存,第二行保存的%rbx值被覆盖为: 33 32 31 30 39 38 37 36,最后仍剩2个字符,4和空,所以返回地址对应的第一行会被覆盖为: 00 00 00 00 00 40 00 34。

最后,E题,对malloc的调用应该是malloc(strlen(buf)+1),因为strlen计算长度时并未包含空字符,而且malloc后未检查返回值是否为NULL。

 

根据上述内容,可以看出缓冲区溢出攻击的原理:在给程序输入的字符串中包含了一些恶意的攻击代码,同时会用一个指向攻击代码的指针覆盖掉返回地址,这样,函数返回时会“迷路”,转而去执行攻击代码。

对抗这种攻击有几种常用方法:

1.栈随机化,即程序开始时,在栈上随机分配一段0-n字节间的随机大小的空间(可用alloca实现),程序不使用这段空间,这样,通过浪费一段空间,可以使程序每次执行时后续的栈位置发生变化。然而这种方式仍有着被破解的可能,攻击者可以在攻击代码前放许多nop指令,这些指令唯一的作用就是指向下一条指令,假设本来栈随机化后栈空间地址的变化范围达到了223个字节,本来要精确地将返回地址改到攻击代码入口的对应的地址需要“精确投放”,即要尝试枚举223种可能,现在攻击代码加上这一堆nop指令,假设达到了28=256个字节,代表只要返回地址指向这些指令中的任何一条,都会导致最后进入攻击代码,因此只要枚举215种可能就行了,因此栈随机化不大保险。

2.栈破坏检测,基本思路是在栈的局部缓冲区插入一个哨兵值,它在程序每次运行时随机产生(比如可以从内存中某个地方取得),在函数返回以及恢复寄存器的值之前,程序会先检测哨兵值是否被改变,若改变了则程序异常终止。

3.限制可执行代码区域,即限制只有保存编译器产生的代码的那部分内存才是可执行的,其他内存区域被限制为只允许读和写。

 

三.变长栈帧

 当声明一个局部变长数组时,编译器无法一开始就确定栈帧的大小,要为之分配多少内存空间,因此需要用变长栈帧。

下面看一个实例,比较难:

左侧两个是相应的C和汇编代码,程序中声明的long *数组,其所占空间大小由传入的n决定,所以栈上至少需要8n个字节容纳这个数组,另外局部变量i由于用到了&i,也要存到栈上。首先%rbp是被调用者保存寄存器,要把%rbp原先的值存入栈,%rbp用来存初始的栈顶位置,它被称为“帧指针”。为什么要特意用一个被调用者保存寄存器来存初始的栈顶位置呢?是因为,在for循环中,i是不停变化的,因此局部变量i在被存到栈中后不能不管它,要在循环过程中修改它的值,而右图中可看到,i存放在初始栈顶向下的8个字节中,因此需要记录下初始栈顶位置以方便定位到i。第4行代码在栈上分配16个字节,其中8个存i,8个没用到。第5行计算得到8n+22,第6行andq $-16,%rax,注意-16补码是0x1111111111111110,即将%rax最后4位置0,也就是把8n+22向下舍入最接近的16的倍数,n是奇数时,结果是8n+8,n是偶数时,结果是8n+16,第7行开辟相应大小的栈空间,开辟后栈指针%rsp的地址由右图中的s1变到下方的s2。第8-10行计算(s2+7)/8 * 8,代表将s2向上舍入到最近的8的倍数,即对应右图中的p。注意左图显示的只是部分汇编代码,它们不是连续的,第11行和12行间有省略号,这里做了一些事情,比如注释中的 i in %rax,在上面的代码中没有操作过,因此这部分操作是被略过去的。13到19行用来完成for循环,没什么可说的。20行的leave指令,等价于 movq %rbp, %rsp  popq %rbp,即恢复寄存器%rbp的值,且栈被释放,恢复回之前的值。

 

完毕。

 

posted on 2019-02-17 18:57  暴躁法师  阅读(1593)  评论(0编辑  收藏  举报