栈溢出攻击c语言_栈溢出攻击及防护方法简介

0. 引言

如果你学的第一门程序语言是C语言,那么下面这段程序很可能是你写出来的第一个有完整的 “输入---处理---输出” 流程的程序:

#include

int main() {

char name[64];

printf("What's your name?");

scanf("%s", name);

printf("Hello, %s!\n", name);

return 0;

}

也许这段小程序给你带来了小小的成就感,也许直到课程结束也没人说这个程序有什么不对,也许你的老师在第一时间就指出这段代码存在栈溢出的漏洞,也许你后来又看到无数的文章指出这个问题同时强调千万要慎用scanf函数,也许你还知道stackoverflow是最好的程序员网站。。。

但可能从来没有人告诉你,什么是栈溢出、栈溢出有什么危害、黑客们可以利用栈溢出来进行什么样的攻击,还有你最想知道的,他们是如何利用栈溢出来实现攻击的,以及如何防护他们的攻击。

本文将一一为你解答这些问题。

1. 准备工具及知识

你需要准备以下工具:

一台64位Linux操作系统的x86计算机(虚拟机也可)

gcc编译器、gdb调试器以及nasm汇编器(安装命令:sudo apt-get install build-essential gdb nasm)

本文中所有代码均在Debian8.1(amd64)、gcc4.9.2、gdb7.7.1和nasm2.11.05以下运行通过,如果你使用的版本不一致,编译选项和代码中的有关数值可能需要根据实际情况略作修改。

你需要具备以下基础知识:

熟练使用C语言、熟悉gcc编译器以及Linux操作系统

熟悉x86汇编,熟练使用mov, push, pop, jmp, call, ret, add, sub这几个常用命令

了解函数的调用过程以及调用约定

考虑到大部分学校里面使用的x86汇编教材都是32位、windows平台下的,这里简单介绍一下64位Linux平台下的汇编的不同之处(如果你已熟悉Linux下的X86-64汇编,那你可以跳过以下内容,直接阅读第2节):

第一个不同之处在于寄存器,64位的寄存器有rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, rip等,对应32位的eax, ebx, ecx, edx, esi, edi, esp, ebp, eip,另外64位cpu中增加了r9, r10, ..., r15寄存器。

第二个不同之处在于函数的调用约定,x86-32位架构下的函数调用一般通过栈来传递参数,而x86-64位架构下的函数调用的一般用rdi,rsi,rdx,rcx,r8和r9寄存器依次保存前6个整数型参数,浮点型参数保存在寄存器xmm0,xmm1...中,有更多的参数才通过栈来传递参数。

第三个不同之处在于Linux系统特有的系统调用方式,Linux提供了许多很方便的系统调用(如write, read, open, fork, exec等),通过syscall指令调用,由rax指定需要调用的系统调用编号,由rdi,rsi,rdx,r10,r9和r8寄存器传递系统调用需要的参数。Linux(x64)系统调用表详见 linux system call table for x86-64。

Linux(x64)下的Hello world汇编程序如下:

[section .text]

global _start

_start:

mov rax, 1 ; the system call for write ("1" for sys_write)

mov rdi, 1 ; file descriptor ("1" for standard output)

mov rsi, Msg ; string's address

mov rdx, 12 ; string's length

syscall

mov rax, 0x3c ; the system call for exit("0x3c" for sys_exit)

mov rdi, 0 ; exit code

syscall

Msg:

DB "Hello world!"

将以上代码另存为hello-x64.asm,再在终端输入以下命令:

$ nasm -f elf64 hello-x64.asm

$ ld -s -o hello-x64 hello-x64.o

$ ./hello-x64

Hello world!

将编译生成可执行文件hello-x64,并在终端输出Hello world!。

另外,本文所有汇编都是用intel格式写的,为了使gdb显示intel格式的汇编指令,需在home目录下新建一个.gdbinit的文件,输入以下内容并保存:

set disassembly-flavor intel

set disassemble-next-line on

display

2. 经典的栈溢出攻击

现在回到最开始的这段程序:

#include

int main() {

char name[64];

printf("What's your name?");

scanf("%s", name);

printf("Hello, %s!\n", name);

return 0;

}

将其另存为victim.c,用gcc编译并运行:

$ gcc victim.c -o victim -zexecstack -g

$ ./victim

What's your name?Jack

Hello, Jack!

上面的编译选项中-g表示输出调试信息,-zexecstack的作用后面再说。先来仔细分析一下源程序,这段程序声明了一个长度为64的字节型数组,然后打印提示信息,再读取用户输入的名字,最后输出Hello和用户输入的名字。代码似乎没什么问题,name数组64个字节应该是够了吧?毕竟没人的姓名会有64个字母,毕竟我们的内存空间也是有限的。但是,往坏处想一想,没人能阻止用户在终端输入100甚至1000个的字符,当那种情况发生时,会发生什么事情?name数组只有64个字节的空间,那些多余的字符呢,会到哪里去?

为了回答这两个问题,需要了解程序运行时name数组是如何保存在内存中的,这是一个局部变量,显然应该保存在栈上,那栈上的布局又是怎样的?让我们来分析一下程序中的汇编指令吧,先将目标程序的汇编码输出到victim.asm文件中,命令如下:

objdump -d victim -M intel > victim.asm

然后打开victim.asm文件,找到其中的main函数的代码:

0000000000400576 :

400576: 55 push rbp

400577: 48 89 e5 mov rbp,rsp

40057a: 48 83 ec 40 sub rsp,0x40

40057e: bf 44 06 40 00 mov edi,0x400644

400583: b8 00 00 00 00 mov eax,0x0

400588: e8 b3 fe ff ff call 400440

40058d: 48 8d 45 c0 lea rax,[rbp-0x40]

400591: 48 89 c6 mov rsi,rax

400594: bf 56 06 40 00 mov edi,0x400656

400599: b8 00 00 00 00 mov eax,0x0

40059e: e8 cd fe ff ff call 400470 <__isoc99_scanf>

4005a3: 48 8d 45 c0 lea rax,[rbp-0x40]

4005a7: 48 89 c6 mov rsi,rax

4005aa: bf 59 06 40 00 mov edi,0x400659

4005af: b8 00 00 00 00 mov eax,0x0

4005b4: e8 87 fe ff ff call 400440

4005b9: b8 00 00 00 00 mov eax,0x0

4005be: c9 leave

4005bf: c3 ret

可以看出,main函数的开头和结尾和32位汇编中的函数几乎一样。该函数的开头的push rbp; mov rbp, rsp; sub rsp, 0x40,先保存rbp的数值,再令rbp等于rsp,然后将栈顶指针rsp减小0x40(也就是64),相当于在栈上分配长度为64的空间,main函数中只有name一个局部变量,显然这段空间就是name数组,即name的起始地址为rbp-0x40。再结合函数结尾的leave; ret,同时类比一下32位汇编中的函数栈帧布局,可以画出本程序中main函数的栈帧布局如下(请注意下图是按栈顶在上、栈底在下的方式画的):

Stack

+-------------+

| ... |

+-------------+

| ... |

name(-0x40)--> +-------------+

| ... |

+-------------+

| ... |

+-------------+

| ... |

+-------------+

| ... |

rbp(+0x00)--> +-------------+

| old rbp |

(+0x08)--> +-------------+

| ret rip |

+-------------+

| ... |

+-------------+

| ... |

+-------------+

rbp即函数的栈帧基指针

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值