|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
缓冲区溢出的危害及避免缓冲区溢出的三种方法' h, C# B4 O4 Z8 ] [6 _; ?5 |% I
面试官不讲武德,居然让我讲讲蠕虫和金丝雀!* j' D# ~% O/ e @1 K' M( s
1. 蠕虫病毒简介
% a. s9 A, C& T/ {. T2. 缓冲区溢出
/ S- z A& ~, Y3 J3. 缓冲区溢出举例
8 E4 C: m/ @9 L( m x4. 缓冲区溢出的危害
. q+ v* V+ Y# J1 [4 i5. 内存在计算机中的排布方式
5 O) k, C) |! t8 m' l" Z6. 计算机中越界访问的后果$ ~8 ]/ V" s6 t# F& W
7. 避免缓冲区溢出的三种方法
4 e! b" i3 Q' I% V! |7.1 栈随机化
( A7 @% D- L6 d# i5 D7.2 检测栈是否被破坏% f1 K& G' n; H2 z
7.3 限制可执行代码区域
6 _: A1 e# f9 w" `* `8. 总结1 { P5 w- ^/ V, }! z4 e* w7 c0 q$ ?
4 n0 j" N8 W9 _8 O0 E" Y蠕虫病毒是一种常见的利用Unix系统中的缺点来进行攻击的病毒。缓冲区溢出一个常见的后果是:黑客利用函数调
* n9 s3 N- A4 t( a$ G+ R用过程中程序的返回地址,将存放这块地址的指针精准指向计算机中存放攻击代码的位置,造成程序异常中止。为
5 M I- ~3 [% v% J了防止发生严重的后果,计算机会采用栈随机化,利用金丝雀值检查破坏栈,限制代码可执行区域等方法来尽量避
. G+ K2 v! y. D" l免被攻击。虽然,现代计算机已经可以“智能”查错了,但是我们还是要养成良好的编程习惯,尽量避免写出有漏洞
) w R4 T$ S0 O1 y8 ?% m0 S# T的代码,以节省宝贵的时间!
l8 U6 a/ x; g$ t+ w' y/ I1. 蠕虫病毒简介蠕虫病毒简介
4 v0 `7 m9 r9 x: ]% W( C: f, }蠕虫是一种可以自我复制的代码,并且通过网络传播,通常无需人为干预就能传播。蠕虫病毒入侵并完全控制一台计算机
, r) i8 [4 |$ S2 ?1 {. }之后,就会把这台机器作为宿主,进而扫描并感染其他计算机。当这些新的被蠕虫入侵的计算机被控制之后,蠕虫会以这些计
! }2 I) x4 k( s% l& A6 i算机为宿主继续扫描并感染其他计算机,这种行为会一直延续下去。蠕虫使用这种递归的方法递归的方法进行传播,按照指数增长指数增长的规
3 p0 g" ]. d+ h/ l' y0 R' H6 C律分布自己,进而及时控制越来越多的计算机。
, C+ E% a6 |9 D7 D+ }! J2. 缓冲区溢出缓冲区溢出
! d. R& N a: |+ \ l: @6 X缓冲区溢出是指计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想的情. c/ Y$ \7 U# u- J
况是:程序会检查数据长度,而且并不允许输入超过缓冲区长度的字符。但是绝大多数程序都会假设数据长度总是与所分绝大多数程序都会假设数据长度总是与所分
; ]/ w% K- }* P/ e% d% A2 H配的储存空间相匹配配的储存空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区,又被称为“堆栈”,在各个操作进程之间,指+ }5 `3 N4 P, ^2 B. e
令会被临时储存在“堆栈”当中,“堆栈堆栈”也会出现缓冲区溢出也会出现缓冲区溢出。9 k- {' v$ i) {( s, X/ h, M' A
3. 缓冲区溢出举例缓冲区溢出举例& E- h4 t* K" @+ G
void echo()
$ q$ A: }* X" d$ F" l+ n{ char buf[4]; /*buf故意设置很小*/1 l6 F& H. a I. `3 M
gets(buf);& x/ k7 L" U4 g9 F3 f
puts(buf);: q! j8 d4 y; S, S* r z
}: }8 G& N4 h3 T1 e/ Q! _
void call_echo()
' a; {. r' ]% i$ Z( P7 e{ echo();
7 F& v/ Q( L, f0 n}2 v/ j5 t9 p" Q) J! U
反汇编如下:: u, O# U1 Y$ J, c) o# I
/*echo*/
& P) v- ~; U0 P$ W. A* v000000000040069c <echo>:- L k! y5 x$ i& k
40069c:48 83 ec 18 sub $0x18,%rsp /*0X18 == 24,分配了24字节内存。计算机会多分配一些给缓冲区*/
* n2 ?4 C( g) ~0 G% Y$ E$ K) N$ h14006a0:48 89 e7 mov %rsp,%rdi
1 F" Q8 b4 R) u0 S$ d4006a3:e8 a5 ff ff ff callq 40064d <gets>. R: v# B I0 q& O( s, S
4006a8::48 89 e7 mov %rsp,%rdi
# R) q5 [( i$ h% c" v( d3 f4006ab:e8 50 fe ff ff callq callq 400500 <puts@plt>$ N/ Z/ O: l$ Z3 W
4006b0:48 83 c4 18 add $0x18,%rsp
! m) |* H' q4 i8 H4006b4:c3 retq* i* j7 S+ L6 x3 R/ M
/*call_echo*/' ~0 p/ X7 s+ T {' F7 v6 u: E9 W
4006b5:48 83 ec 08 sub $0x8,%rsp& t) n4 j# s2 D/ `; N7 i7 t
4006b9:b8 00 00 00 00 mov $0x0,%eax
) L- q% b* h* a/ A4 ?& e( T- [4006be:e8 d9 ff ff ff callq 40069c <echo>" Q* T: ?1 D9 j% o) e3 h; i
4006c3:48 83 c4 08 add $0x8,%rsp# q& s p$ h; q' T u: z
4006c7:c3 retq
' J0 o' u% z O. I在这个例子中,我们故意把buf设置的很小。运行该程序,我们在命令行中输入012345678901234567890123,程序立马就会报
+ K8 o( Z9 C: s* G0 j+ Y错:Segmentation fault。
4 U4 ?, m% g# K( X+ D要想明白为什么会报错,我们需要通过分析反汇编来了解其在内存是如何分布的在内存是如何分布的。具体如下图所示:
& H4 c6 [2 c0 Z! l0 U. J如下图所示,此时计算机为buf分配了24字节空间,其中20字节还未使用。$ c: c8 |- Q3 G& O
此时,准备调用echo函数,将其返回地址压栈。* f8 s& B$ }. b' P) p, n
当我们输入“0123456789012345678 9012"时,缓冲区已经溢出,但是并没有破坏程序的运行状态并没有破坏程序的运行状态。/ N, P8 i4 r B! r
当我们输入:“012345678901234567 890123"。缓冲区溢出,返回地址被破坏返回地址被破坏,程序返回 0x0400600。
5 u. t! d5 m" B这样程序就跳转到了计算机中其他内存的位置,很大可能这块内存已经被使用。跳转修改了原来的值跳转修改了原来的值,所以程序就会中止运
3 o' j$ ^4 |8 V! t0 c) s$ G行。
5 n7 G+ G0 @; \+ n: O) ?2 S5 h黑客可以利用这个漏洞,将程序精准跳转到其存放木马的位置(如nop sled技术),然后就会执行木马程序,对我们的计算机1 u- X. `1 C5 h# T
造成破坏。
% h* q: |; Y& u% p+ K1 S8 O- N4. 缓冲区溢出的危害缓冲区溢出的危害
- U2 v% f! I- F3 C) [缓冲区溢出可以执行非授权指令,甚至可以取得系统特权,进而进行各种非法操作。第一个缓冲区溢出攻击--Morris蠕虫,发
; u0 {! Z* `/ ]( Z- A生在二十年前,它曾造成了全世界6000多台网络服务器瘫痪。* K; h' l( }! D4 G
在当前网络与分布式系统安全中,被广泛利用的50%以上都是缓冲区溢出,其中最著名的例子是1988年利用fingerd漏洞的蠕; l# L5 h! k: s4 n
虫。而缓冲区溢出中,最为危险的是堆栈溢出堆栈溢出。因为入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其函数返回时改变返回程序的地址,让其
( n+ A9 q3 ~, Y% m3 G跳转到任意地址跳转到任意地址。带来的危害有两种,一种是程序崩溃导致拒绝服务,另外一种就是跳转并且执行一段恶意代码,比如得到: _6 N! l0 C2 A7 z# [
shell,然后为所欲为。7 `8 n5 U# y- U. v7 P- h
5. 内存在计算机中的排布方式内存在计算机中的排布方式
, A/ q' T5 a# }+ E+ D; }) r内存在计算机中的排布方式如下,从上到下依次为共享库,栈,堆,数据段,代码段。各个段的作用简介如下:
4 ^& g5 Z ~) P: l# }共享库共享库:共享库以.so结尾.(so==share object)在程序的链接时候并不像静态库那样在拷贝使用函数的代码,而只是作些标记。然后; x6 a( g7 N7 T2 J
在程序开始启动运行的时候,动态地加载所需模块。所以,应用程序在运行的时候仍然需要共享库的支持。共享库链接出来的1 L, c! z3 d% x4 j$ _2 ?" H! S
文件比静态库要小得多。
8 K; P# w" [' T2 h& M+ [1 G# x栈栈:栈又称堆栈,是用户存放程序临时创建的变量程序临时创建的变量,也就是我们函数{}中定义的变量,但不包括但不包括static声明的变量声明的变量,static意
! X5 i' ?& E x' H- m2 U味着在数据段中存放变量数据段中存放变量。
+ F; y3 y4 V `) `3 i除此之外,在函数被调用时,其参数也会被压入发起调用的进程栈中参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回的返回值也会被存放回
" m3 H1 t$ P; b: G$ @5 O栈中栈中,由于栈的先进后出特点,所以栈特别方便用来保存、恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄
A5 A$ z2 |9 n& l存,交换临时数据的内存区。在X86-64 Linux系统中,栈的大小一般为8M(用ulitmit - a命令可以查看)。' h9 O/ V3 U2 ?# A% B8 D6 T: p) X5 D
堆堆:堆是用来存放进程中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存: P7 f# n7 [& e# S+ i% J a
时,新分配的内存就被动态分配到堆上,当利用free等函数释放内存时,被释放的内存从堆中被剔除。
& Z7 s: j1 p; F2 x/ g* @" \, n0 W堆存放new出来的对象,栈里面所有对象都是在堆里面有指向的。假如栈里指向堆的指针被删除,堆里的对象也要释放
. n8 x! W: H- I, n$ P. q: ~1 Q(C++需要手动释放)。当然现在面向对象程序都有'垃圾回收机制',会定期的把堆里没用的对象清除出去。& [6 y9 v% I" L0 \ e: k
2数据段数据段:数据段通常用来存放程序中已初始化的全局变量和已初始化为非已初始化的全局变量和已初始化为非0的静态变量的静态变量的一块内存区域,属于静态内存分
& I2 d- z7 B9 Z配。直观理解就是C语言程序中的全局变量(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函
! c. H- d8 l; L! Y4 u数的数据数的数据)8 i& b8 c5 o# E4 I- x; f
代码段代码段:代码段通常用来存放程序执行代码执行代码的一块区域。这部分区域的大小在程序运行前就已经确定了,通常这块内存区域9 [0 F5 l; ?6 L2 h
属于只读只读,有些架构也允许可写,在代码段中也有可能包含以下只读的常数变量,例如字符串常量等。' a b2 D( r$ d3 v$ k1 m
下面举个例子来看下代码中各个部分在计算机中是如何排布的。
8 T4 P* h Q- A2 V, {# G6 J#include <stdio.h>, C5 q' f, n4 N7 {) Q- |9 c" a: N+ ]/ t1 W
#include <stdlib.h>4 b: Z! x. G+ Q! Z
char big_array[1L<<24]; /*16 MB*/
4 P u& X/ _( i& }* `5 W/ pchar huge_array[1L<<31]; /*2 GB*/
; ?8 s; e) C, rint global = 0;- q5 q- y: Z% B" F p" D
int useless() {return 0;}4 G/ X/ T4 G H( \8 z- N
int main()
6 x9 E1 r/ |/ M+ f- a% u ~{ void *phuge1,*psmall2,*phuge3,*psmall4;
% z* `; c& A0 C# [# K1 X0 ~! rint local = 0;
( K2 K2 X, ]/ G1 Wphuge1 = malloc(1L<<28); /*256 MB*/
6 l8 P8 f# l/ p$ Npsmall2 = malloc(1L<<8); /*256 B*/
7 p5 E- n4 ]& P/ u* D1 G* E0 K8 Z6 f( yphuge3 = malloc(1L<<32); /*4 GB*/8 X) s- ^ p- {3 Q* Z Y: k
psmall4 = malloc(1L<<8); /*256 B*/
* p- x0 H$ M# z0 E, F g) K}
# V# h0 Z- H0 J# z+ ?5 O+ O0 a上述代码中,程序中的各个变量在内存的排布方式如下图所示。根据颜色可以一一对应起来。由于了local变量存放在栈区,四! _, c; a& w, _" v# [* `* e
个指针变量使用了malloc分配了空间, 所以存放在堆上,两个数组big_ array,huge_array存放在数据段,main,useless函数的其# j: X6 M! [# @. O3 [
他部分存放在代码段中。8 v" [% h) [" p; }
6. 计算机中越界访问的后果计算机中越界访问的后果& S5 y, n- w2 j8 P7 D$ z
下面再看一个例子,看下越界访问内存会有什么结果。
2 R& `3 J) z, y) X& B; i4 s5 ^; }& n8 Btypedef struct- g, W- \* f3 e: N2 }+ R
{ int a[2];
! x' q0 ^6 W: c4 M- bdouble d;; h& O7 q `6 ~: b& N
}struct_t;' X+ a+ P; \: v8 R9 J
double fun(int i)
% B1 u2 f4 A: ?; A5 H0 i0 C{% v1 r+ f7 e6 U- q
volatile struct_t s;# ~& i( @0 V: f8 f
s.d = 3.14;
& A* t, ~2 `; e# ~s.a[i] = 1073741824; /*可能越界*/, Y5 s1 ]. {- g8 b5 Q0 }1 `0 f! C
return s.d;
6 f I$ {, X3 s: ^# q% C0 Y! v( W}( ]/ @/ O) s' ]4 ?) k
int main()9 T- C- Z v) U* s0 R5 l. z! u
{8 F: H: B. Y$ q# U: g
printf("fun(0):%lf\n",fun(0));( c& ~& A) q4 B: l! x% P: Y
printf("fun(1):%lf\n",fun(1));" P5 p4 w: i( p% N; Q! u4 v
printf("fun(2):%lf\n",fun(2)); M/ K9 y$ B$ ^1 a7 K
printf("fun(3):%lf\n",fun(3));
* o* S5 b4 S, _2 |: j+ Dprintf("fun(6):%lf\n",fun(6));
5 {. j1 U) F- Sreturn 0;. h$ a, m. w' G# n1 d; z" ^; g
}1 e4 ^5 M8 |( `+ ?8 ]# z
打印结果如下所示:
* l |5 K: ~' f: R( nfun(0):3.14
" H6 Z# P+ I9 C* `fun(1):3.14
# J( B. Y' \, y2 r% @) ufun(2):3.1399998664856
2 d' h8 Y. _ D7 F4 J7 @fun(3):2.00000061035156- i& e7 ~& G" h8 F
fun(6):Segmentation fault; H% W% l% ^1 N! e3 L. g% g
在上面的程序中,我们定义了一个结构体,其中 a 数组中包含两个整数值,还有 d 一个双精度浮点数。在函数fun中,fun函数
" ~, L+ n9 d: V% U根据传入的参数i来初始化a数组。显然,i的值只能为的值只能为0和和1。在fun函数中,同时还设置了d的值为3.14。当我们给fun函数传入0
4 A8 {" {3 B2 s& B8 u: t$ K和1时可以打印出正确的结果3.14。但是当我们传入2,3,6时,奇怪的现象发生了。为什么为什么fun((2)和)和fun((3)的值会接近)的值会接近" {6 |1 I5 b7 d4 k0 S+ q
3.14,而,而fun((6)会报错呢?)会报错呢?2 x+ p: q3 n4 v0 ?
3要搞清楚这个问题,我们要明白结构体在内存中是如何存储的,具体如下图所示。
& X- S* d, o( O! ?& H结构体在内存中的存储方式4 A1 W, M0 Q+ t5 T% q3 _
GCC默认不检查数组越界(除非加编译选项)。而越界会修改某些内存的值而越界会修改某些内存的值,得出我们意想不到的结果。即使有些数据相隔' [2 h# E; n/ T; t
万里,也可能受到影响。当一个系统这几天运行正常时,过几天可能就会崩溃。(如果这个系统是运行在我们的心脏起搏器,
3 v/ Y8 J u: @3 b4 y又或者是航天飞行器上,那么这无疑将会造成巨大的损失!)
! d/ m% \" A' h$ M: t6 L如上图所示,对于最下面的两个元素,每个块代表 4 字节。a数组占用8个字节,d变量占用8字节,d排布在a数组的上方。所以
8 P% Z3 m! w; i3 p9 E1 z我们会看到,如果我引用 a[0] 或者 a[1],会按照正常修改该数组的值。但是当我调用 fun(2) 或者 fun(3)时,实际上修改的是这实际上修改的是这" A E- e5 m0 t1 g& b
个浮点数个浮点数 d 所对应的内存位置所对应的内存位置。这就是为什么我们打印出来的fun(2)和fun(3)的值如此接近3.14的原因。
7 k# c5 k0 [% [9 D8 `+ Y当输入 6 时,就修改了对应的这块内存的值。原来这块内存可能存储了其他用于维持程序运行的内容,而且是已经分配原来这块内存可能存储了其他用于维持程序运行的内容,而且是已经分配! ~* i& z0 D" X# y3 ~8 [
的内存的内存。所以,我们程序就会报出Segmentation fault的错误。
/ C2 ~0 g" A4 |, a% z7. 避免缓冲区溢出的三种方法避免缓冲区溢出的三种方法
) k6 M7 w& T3 Z0 g: u4 ?为了在系统中插入攻击代码,攻击者既要插入代码插入代码,也要插入指向这段代码的指针这段代码的指针。这个指针也是攻击字符串的一部分。产2 h( c3 [/ d8 v8 A* H( L
生这个指针需要知道这个字符串放置的栈地址栈地址。在过去,程序的栈地址非常容易预测。对于所有运行同样程序和操作系统版
. _- g2 L& H. v/ @( s6 _本的系统来说,在不同的机器之间,栈的位置是相当固定的栈的位置是相当固定的。因此,如果攻击者可以确定一个常见的Web服务器所使用的栈
, \$ Z! ^6 [0 x空间,就可以设计一个在许多机器上都能实施的攻击。- W& a- N/ y: k; Q
7.1 栈随机化栈随机化7 x" P: C3 Q$ z V1 v
栈随机化的思想使得栈的位置在程序每次运行时都有变化栈的位置在程序每次运行时都有变化。因此,即使许多机器都运行同样的代码,它们的栈地址都是不同9 F7 {5 _0 o1 i7 f6 W
的。实现的方式是:程序开始时,在栈上分配一段0 ~ n字节之间的随机大小的空间,例如,使用分配函数alloca在栈上分配指) ]5 l( R. o- y" b7 ]+ N
定字节数量的空间。程序不使用这段空间程序不使用这段空间,但是它会导致程序每次执行时后续的栈位置发生了变化程序每次执行时后续的栈位置发生了变化。分配的范围n必须足
0 z% H1 k% f: w. e% E* h够大,才能获得足够多的栈地址变化,但是又要足够小,不至于浪费程序太多的空间。
+ v* x) R0 S5 P3 Kint main()( A3 d1 ~- u5 f$ W. b5 ]
{long local;0 Z) f3 q7 m J F+ W
printf("local at %p\n",&local);. h7 O8 G. T6 b' {
return 0;7 t* J$ \9 N: m. o- ]# R
}
. J* J+ o1 F# H% `( U这段代码只是简单地打印出main函数中局部变量的地址。在32位 Linux上运行这段代码10000次,这个地址的变化范围为3 Q, P* X- |& b' z
0xff7fc59c到0xffffd09c,范围大小大约是 。在64位 Linux机器上运行,这个地址的变化范围为0x7fff0001b698到0x7ffffffaa4a8,范
- D( ^# _/ ?$ p/ S. z% m$ H围大小大约是 。
4 A/ H; [8 T9 [1 W" \8 g其实,一个好的黑客专家,可以使用暴力破坏栈的随机化。对于32位的机器,我们枚举 个地址就能猜出来栈的地址。) Q+ |5 }, _7 I6 S% b
对于64位的机器,我们需要枚举 次。如此看来,栈的随机化降低了病毒或者蠕虫的传播速度,但是也不能提供完全的
E( D8 Y# L- y' a安全保障。( L# X* ?+ h8 l5 B6 ^5 o
7.2 检测栈是否被破坏检测栈是否被破坏! i5 E! {. c6 T5 J' Q
计算机的第二道防线是能够检测到何时栈已经被破坏。我们在echo函数示例中看到,当访问缓冲区越界时,会破坏程序的运行' I' G4 e" W0 S" ^8 g+ J. B. O1 K
状态。在C语言中,没有可靠的方法来防止对数组的越界写。但是,我们能够在发生了越界写的时候,在造成任何有害结果之
% X) Y. z& b+ c5 L前,尝试检测到它。/ Y, k6 N+ Y( H! [. i0 y& J7 V
GCC在产生的代码中加人了一种栈保护者机制,来检测缓冲区越界。其思想是在栈帧中任何局部缓冲区与栈状态之间存储在栈帧中任何局部缓冲区与栈状态之间存储5 @7 g! V. D |
一个特殊的金丝雀值一个特殊的金丝雀值,如下图所示: p8 [% v, S# i' M; T4 R- x* h
这个金丝雀值,也称为哨兵值,是在程序每次运行时随机产生的,因此,攻击者很难猜出这个哨兵值。在恢复寄存器状态和从1 ~8 {6 M" F& w1 i; ]
函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个函数的某个操作改变了程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个函数的某个操作改变了。如果是3 Z' G4 a$ B$ `$ m) D; ^
的,那么程序异常中止。7 f3 M. U+ c" \" I% W1 ~
英国矿井饲养金丝雀的历史大约起始1911年。当时,矿井工作条件差,矿工在下井时时常冒着中毒的生命危险。后, [8 T G% z: t, [) Y
来,约翰·斯科特·霍尔丹(John Scott Haldane)在经过对一氧化碳一番研究之后,开始推荐在煤矿中使用金丝雀检测
3 ^: V$ s: h5 e3 N; c4 F一氧化碳和其他有毒气体。金丝雀的特点是极易受有毒气体的侵害,因为它们平常飞行高度很高,需要吸入大量空" \8 K! P6 z q$ A
气吸入足够氧气。因此,相比于老鼠或其他容易携带的动物,金丝雀会吸入更多的空气以及空气中可能含有的有毒/ V8 `$ a& Q* Y5 }" a
物质。这样,一旦金丝雀出了事,矿工就会迅速意识到矿井中的有毒气体浓度过高,他们已经陷入危险之中,从而
- C0 \+ o H" Z1 P8 F" a8 t及时撤离。
. d W% K( F5 U6 d) A; n1 D/ k4GCC会试着确定一个函数是否容易遭受栈溢出攻击,并且自动插入这种溢出检测。实际上,对于前面的栈溢出展示,我们可8 f3 W R5 J" [# N( S/ Y
以使用命令行选项“-fno- stack- protector”来阻止GCC产生这种代码。当用这个选项来编译echo函数时(允许使用栈保护),得到' ]2 z) T/ t1 l: A$ J- P, x! P4 B
下面的汇编代码
$ m3 ]7 ` S! K2 b' t* u7 w& F//void echo T+ i7 e, _" A2 o6 L
subq $24,%rsp Allocate 24 bytes on stack
1 s5 U/ k( K+ V! `movq %fs:40,%rax Retrieve canary. @' G7 r6 H v/ J+ ]0 t
movq %rax,8(%rsp) Store on stack
! c( I+ f1 C; o; W7 exorl %eax, %eax Zero out register //从内存中读出一个值
4 b. c. C+ a: q; E2 e! p3 i8 M4 omovq %rsp, %rdi Compute buf as %rsp+ Z: M7 N9 i6 P: [- h' A, L. a
call gets Call gets |2 s, {, L5 a* L
movq ‰rsp,%rdi Compute buf as %rsp+ X& H5 v6 k# K) R$ }& m4 B
call puts Call puts
) s: c% C; `$ c( amovq 8(%rsp),%rax Retrieve canary" \5 u3 A9 ^. [+ h1 H1 t
xorq %fs:40,%rax Compare to stored value //函数将存储在栈位置处的值与金丝雀值做比较! t0 |+ t0 g$ w, u- |! f$ f
je .L9 If =, goto ok) `3 U9 s; |4 X4 c/ d6 P! h8 a
call __stack_chk_fail Stack corrupted
' x- L( k5 Q. G% F4 j.L99 |8 X. A* w2 h7 E: G; i: }
addq $24,%rsp Deallocate stack space
C: s* x6 P5 @* nret5 n: u v. R; h+ m! ]1 o/ S
这个版本的函数从内存中读出一个值(第4行),再把它存放在栈中相对于%rsp偏移量为8的地方。指令参数各fs:40指明金丝雀2 Z# G, D1 u! _8 t3 p; p- Y
值是用段寻址从内存中读入的。段寻址机制可以追溯到80286的寻址,而在现代系统上运行的程序中已经很少见到了。将金丝
3 y6 e1 F/ ?1 N' |4 h) n雀值存放在一个特殊的段中,标记为只读只读,这样攻击者就不能覆盖存储金丝雀值这样攻击者就不能覆盖存储金丝雀值。在恢复寄存器状态和返回前,函数将存函数将存
/ G8 w$ O: n( k储在栈位置处的值与金丝雀值做比较储在栈位置处的值与金丝雀值做比较(通过第10行的xorq指令)。如果两个数相同,xorq指令就会得到0,函数会按照正常的
/ W% O9 \6 [2 N* O/ _方式完成。非零的值表明栈上的金丝雀值被修改过,那么代码就会调用一个错误处理例程。
p0 t7 Z; E+ d7 F" D" Y栈保护很好地防止了缓冲区溢出攻击破坏存储在程序栈上的状态。一般只会带来很小的性能损失。
2 z" b- f; @' ?$ ^# ~7 x7.3 限制可执行代码区域限制可执行代码区域
+ ~6 o& O, c- V( p2 k2 j {最后一招是消除攻击者向系统中插入可执行代码的能力。一种方法是限制哪些内存区域能够存放可执行代码限制哪些内存区域能够存放可执行代码。在典型的程
" n. F, ?9 m& V* E) d- x. {序中,只有保存编译器产生的代码的那部分内存才需要是可执行的只有保存编译器产生的代码的那部分内存才需要是可执行的。其他部分可以被限制为只允许读和写。
( {9 ^# O N, c许多系统都有三种访问形式:读(从内存读数据)、写(存储数据到内存)和执行(将内存的内容看作机器级代码)读(从内存读数据)、写(存储数据到内存)和执行(将内存的内容看作机器级代码)。
" P0 N$ m' H% c& B3 H以前,x86体系结构将读和执行访问控制合并成一个1位的标志,这样任何被标记为可读的页也都是可执行的。栈必须是既可读
% `4 ]/ e* j. r) P又可写的,因而栈上的字节也都是可执行的。已经实现的很多机制,能够限制一些页是可读但是不可执行的,然而这些机制通
) V/ P/ q% H4 k7 ?5 |" C常会带来严重的性能损失。
8 q- j" n/ }# C! b8. 总结总结/ M8 G/ i* B$ ^2 E0 c$ b3 B8 ~
计算机提供了多种方式来弥补我们犯错可能产生的严重后果,但是最关键的还是我们尽量减少犯错。
' ^& n4 _# \6 Z u- Y例如,对于gets,strcpy等函数我们应替换为 fgets,strncpy等。在数组中,我们可以将数组的索引声明为size_t类型,从根本上防
# { A8 s% C2 h' N/ g$ O6 W8 L) C止它传递负数。此外,还可以在访问数组前来加上num小于ARRAY_MAX 语句来检查数组的上界。总之,要养成良好的编程习6 T6 t* T0 Q9 u+ {
惯,这样可以节省很多宝贵的时间。同时最后也推荐两本相关书籍如下所示。8 [8 c: }0 K# v: t! ]
代码大全(第二版): K9 R L# o& Z, Y9 }7 d& C2 v' ^4 h
高质量程序设计指南$ O* D; S+ U3 G9 o2 B6 t
- W' s" L y x6 a( t) t0 A |
|