当前位置: 首页 > news >正文

理解stack_chk_guard

理解stack_chk_guard

title: 理解stack_chk_guard
author: zzidun
date: 2022-03-07 00:00
tags: 编程学习

栈溢出攻击

如果了解riscv的函数调用,会知道这样一件事:

进入一个函数之后,程序会把返回地址和原本的s0寄存器的值,存到栈的开头.

就像这样

  • [sp+24, sp+32): 返回地址寄存器ra的值
  • [sp+16, sp+24): 进入函数之前,寄存器s0的值
  • [sp,sp+16): 其他内容

着看起来没啥毛病,但是黑客不这么认为.

如果这个函数有一个读取输入的语句,并且可以输入很长的内容.

比如你让用户输入一个字符串,并存到栈上,但是没有对字符串的长度做限制.

那么输入的字符串就会把栈前面的数据也覆盖掉.

这时候攻击者可以输入一个超长的字符串,覆盖掉栈上记录的返回地址.

从而使函数返回时,跳转到攻击者想要的位置.

__stack_chk_guard的用处

如果我们在一个函数获取输入,并且存到一个局部变量中.

那么编译器除了会生成功能相关的代码之外,还会一些额外的代码,对一个叫做__stack_chk_guard的符号进行操作.

例如以下代码

#include <stdio.h>int main()
{int a = 0;scanf("%d", &a);return 0;
}

如果我们使用riscv64-linux-gnu-g++编译,会产生以下汇编代码(节选)

main:addi	sp,sp,-32 # 栈顶指针-32sd	ra,24(sp) # ra存到sp+24sd	s0,16(sp) # s0存到sp+16addi	s0,sp,32 # 计算新的栈底地址,s0=sp+32la	a5,__stack_chk_guard # 加载__stack_chk_guard的地址到寄存器a5ld	a5,0(a5) # 加载寄存器a5中所存的地址指向的内容到寄存器a5sd	a5,-24(s0) # 把寄存器a5中的内容存到s0-24(也就是sp+8)sw	zero,-28(s0) # 下面是输入的操作,不再多说addi	a5,s0,-28mv	a1,a5lla	a0,.LC0call	__isoc99_scanf@pltli	a5,0 # 把返回值传到寄存器a3中mv	a3,a5la	a5,__stack_chk_guard # 再次加载__stack_chk_guard的地址到寄存器a5ld	a4,-24(s0) # 把之前存到sp+8的内容拿出来,放到寄存器a4ld	a5,0(a5) # 加载寄存器a5中所存的地址指向的内容到寄存器a5beq	a4,a5,.L3 # 对比寄存器a4,a5的内容,如果相同就跳转到.L3call	__stack_chk_fail@plt # 否则报错
.L3mv	a0,a3 # 返回值传到寄存器a0ld	ra,24(sp) # 取出之前的rald	s0,16(sp) # 取出之前的s0addi	sp,sp,32 # sp恢复到-32前jr	ra # 跳转到返回地址ra

很显然,这个栈的布局是这样的

  • [sp+24, sp+32): 返回地址寄存器ra的值
  • [sp+16, sp+24): 进入函数之前,寄存器s0的值
  • [sp+8,sp+16): __stack_chk_guard
  • [sp+4,sp+8): 栈上定义的变量

另外多出一点,因为需要对齐.

我们在进入函数之前把一个数值__stack_chk_guard存到了栈上定义的变量之前.

在返回前,又把之前存的数值拿出来,和原本的__stack_chk_guard进行对比.

如果没有发生变化,就说明输入的时候没有没有影响到sp+8之前的值.

因为输入操作的影响通常是连续的,我们可以认为只要sp+8没有变化,前面的值就没有被影响.

因为__stack_chk_guard是一个随机值,攻击者应该无法将一模一样的值写回去.

这时候可以认为返回地址是安全的.