面试题答案
一键面试基于栈的缓冲区溢出原理
- 函数调用过程中栈的结构变化
- 函数调用开始:
- 当一个函数被调用时,调用者会把返回地址压入栈中。返回地址是函数调用结束后程序应该继续执行的下一条指令的地址。
- 然后,调用者会为被调用函数的局部变量在栈上分配空间。例如,如果被调用函数定义了一个数组
char buffer[10]
,这10个字节的空间会在栈上被预留。
- 函数执行过程:
- 在函数执行期间,对局部变量的访问和操作都是基于栈指针进行的。例如,向
buffer
数组写入数据时,通过栈指针偏移来确定buffer
在栈中的位置。
- 在函数执行期间,对局部变量的访问和操作都是基于栈指针进行的。例如,向
- 函数调用结束:
- 函数执行完毕后,栈上为局部变量分配的空间被释放,通过恢复栈指针到函数调用前的位置来实现。然后,从栈中弹出返回地址,程序跳转到该返回地址继续执行。
- 函数调用开始:
- 缓冲区溢出发生机制:
- 当向一个基于栈的缓冲区(如局部数组)写入数据时,如果写入的数据长度超过了该缓冲区的实际大小,就会发生基于栈的缓冲区溢出。例如,
char buffer[10]; strcpy(buffer, "123456789012345");
,这里strcpy
试图将长度为15的字符串复制到只有10个字节大小的buffer
中。 - 溢出的数据会覆盖栈上紧邻该缓冲区的其他数据,可能是其他局部变量、函数的返回地址等。如果返回地址被覆盖,当函数返回时,程序会跳转到错误的地址,可能导致程序崩溃、执行恶意代码等严重后果。
- 当向一个基于栈的缓冲区(如局部数组)写入数据时,如果写入的数据长度超过了该缓冲区的实际大小,就会发生基于栈的缓冲区溢出。例如,
安全编程规范下防范措施
- 边界检查:
- 对输入数据长度进行检查:在将数据写入缓冲区之前,确保数据长度不超过缓冲区的大小。例如,使用
strncpy
替代strcpy
,strncpy(buffer, source, sizeof(buffer));
,strncpy
最多只会复制sizeof(buffer) - 1
个字符到buffer
中,防止溢出。 - 在循环操作缓冲区时检查边界:如果通过循环向缓冲区写入数据,要确保循环变量不会导致访问越界。例如:
- 对输入数据长度进行检查:在将数据写入缓冲区之前,确保数据长度不超过缓冲区的大小。例如,使用
for (int i = 0; i < sizeof(buffer); i++) {
buffer[i] = data[i];
}
- 使用安全函数:
- 除了
strncpy
替代strcpy
外,还有snprintf
替代sprintf
。sprintf
可能会因为格式化字符串产生的数据过长而导致缓冲区溢出,而snprintf
会确保输出不超过指定的缓冲区大小。例如:snprintf(buffer, sizeof(buffer), "%s", source);
- 除了
- 编译器保护机制:
- 启用栈保护选项:现代编译器(如GCC)提供了栈保护选项,如
-fstack - protector
系列选项。这些选项会在函数的栈帧中插入额外的保护数据(通常称为金丝雀值),当函数返回时,检查该值是否被修改。如果被修改,说明发生了缓冲区溢出,程序会终止执行。
- 启用栈保护选项:现代编译器(如GCC)提供了栈保护选项,如
- 代码审查:
- 在开发过程中,定期进行代码审查,仔细检查对缓冲区的操作,特别是涉及字符串处理、数组访问等操作,确保没有潜在的缓冲区溢出风险。例如,审查是否有未检查边界的数组访问操作:
int arr[10]; int index = getIndex(); arr[index] = value;
,这里如果getIndex
返回的值大于9,就会发生溢出,通过代码审查可以发现并修正此类问题。
- 在开发过程中,定期进行代码审查,仔细检查对缓冲区的操作,特别是涉及字符串处理、数组访问等操作,确保没有潜在的缓冲区溢出风险。例如,审查是否有未检查边界的数组访问操作: