IP分片重组的分析和常见碎片攻击
一 前言
4 n* S/ U5 V+ e2 \( e8 H$ P+ R% h
# v1 ]0 f/ [% Z9 y" {本文对linux的IP组装算法进行了分析,因为IP碎片经常用于DOS等攻击,在文章后面我结合了一些攻击方法进行了更进一步的说明。内核主要参考版本是2.2.16,另外简要的介绍了2.4.0-test3中的一些变化. + C4 `1 s- |5 d4 B
3 ]% u0 x6 n: a, i; |8 [: R6 P
二 目录 : x& T! _, C; h5 @ l8 }
$ u0 s, _& c/ O [! k3 @1 W1- 概述 : ]5 }( W, x5 x6 s* H+ r3 |" Q
2- 关键数据结构
+ y0 M, _; _+ h3 K! E& P! D3- 重要函数说明 - _7 F- }2 n5 h
4- 2.4系列的变化 , Q6 L9 I% b& y9 T1 V' z8 P
5- 常见碎片攻击
/ D( y6 k" i4 f- E$ f' p! l' E, k
# `) u+ _0 c$ R6 x3 S0 U; _4 g0 k: G5 ^, B1 Q- I& R
1. 概述 ; a) S1 r/ g9 ~) F5 R0 n% [
[& A4 D8 l* x" Q* s
在linux源代码中,ip分片重组的全部程序几乎都在都在\net\ipv4\ip_fragment.c文件中。其对外提供一个函数接口ip_defrag()。其函数原型如下: " N! K5 U8 I3 T" ?2 d" n9 g
/ ^" u4 D; J( I) L
struct sk_buff *ip_defrag(struct sk_buff *skb) 6 t3 u% y& s& y B
! @! Y( [) `2 N2 X" C& W& a
众所周知,网络数据报在linux的网络堆栈中是以sk_buff的结构传送的,ip_defrag()的功能就是接受分片的数据包(sk_buff),并试图进行组合,当完整的包组合好时,将新的sk_buff返还,否则返回一个空指针。
0 G+ |* X5 E, G+ s* _+ ~7 Z
& ^6 x3 q* Q4 F/ _' v/ H! s G4 L0 Y此函数在其他文件中的调用如下: ; i2 u# M L* b8 q
1 X$ v8 s4 P6 D; B) \* |" [ip层接收主函数为ip_rcv()(\net\ipv4\ip_input.c),任何IP包都需经过此函数处理。如果此包是发往本机,则调用ip_local_deliver()函数(\net\ipv4\ip_input.c)进行处理,一般的系统碎片只有在到达最终目的的时候才进行重组(尽管在传输过程中可能被进一步分成更小的片)。在ip_local_deliver()中我们可发现如下代码:
: M+ Q4 H3 U! I4 P/ G8 R, m: U5 S; e. k" v/ V# ~: b8 z& a
if (sysctl_ip_always_defrag == 0 && /*编译时未设置提前组装*/ 8 ]' @* L" E2 _3 R
(iph-〉frag_off & htons(IP_MF|IP_OFFSET))) { /*判断是否是分片包*/
% e$ G- r+ c( h4 S+ Askb = ip_defrag(skb); /*条件满足,进行组装*/ ' c3 Z- ?: \! r* K' @" ?3 |
if (!skb) /*若组装好则进行下一步处理,出错 # W _" U: l G/ P! i2 Y% V
return 0; 或仍未组装完返回*/
) z/ q5 F# ?' H& A8 ]! l7 O; riph = skb-〉nh.iph; /*重新定位ip头的指针*/ * Y0 Z0 v2 f. s T
} ( E( O9 ^0 ~* v7 q% _: `
1 k! n* |5 q( A! \$ F8 [7 V
iph-〉frag_off只有在设置MF(more fragment)或offset!=0才意味着是分片包,因此此处的检验理所当然,但为什么判断sysctl_ip_always_defrag == 0呢?在看ip_rcv()时我们应该已经注意到在刚进行了版本号,长度,校验和等判断后,有如下一段代码: ' u/ d8 ~( X4 p, u- x& d# X- j* o" p
* C9 M; z4 P7 P" ]; \+ kif (sysctl_ip_always_defrag != 0 && ! o* C2 t' Z' {: S
iph-〉frag_off & htons(IP_MF|IP_OFFSET)) { & e5 S; m/ s( x
skb = ip_defrag(skb); / x9 x: m4 t1 H i/ M- x. I
if (!skb)
9 R$ Y D- ~7 F4 Areturn 0; 8 B# Z1 n: C: \8 F
iph = skb-〉nh.iph;
2 K- L# u @; V) Z1 Aip_send_check(iph); & |! {+ }& e" c9 `: m+ O; b
}
0 t" W2 t4 g: F8 f
( a& n1 x9 M3 d7 R: q即如果sysctl_ip_always_defrag==1的话,ip_defrag()的调用位置将有变化,对任何进来的IP分片都要进行重组,可以想像,如果此机器作路由器的话,将对所有的分片组装好后,才会进行转发。此举一般是没有必要的。这个值可以通过sysctl命令动态设置,用sysctl -a可以看到在一般的系统中,此值被设为0: 8 v+ t$ W5 v$ [1 |7 a
; A* r9 M. v6 @, P! @7 O9 u
#sysctl -a
2 y4 i. n; p# @; M/ }/ @* N......
5 k" \5 |# `2 F- M* D2 k5 onet.ipv4.ip_always_defrag = 0 `0 E' |, c) ~8 N. K! M7 _
......
, X5 N5 g: E% R$ {) T( Z. m \3 n4 b
8 k- [7 s% O b* c4 _8 h3 ]# I. j5 q5 r" ]+ Y
2. 关键数据结构(2.2系列) . N( x6 ^4 C8 e. ]3 e, g; J
+ s0 T9 u2 r: w, @1 U3 R9 {& Z每一个分片用ipfrag结构表示:
4 [# A8 l) V' t1 i0 T; l. A
* l& z: a: R; L: o4 ?. C5 J7 ?/* Describe an IP fragment. */
$ m; T5 Z* C; u7 o) d5 Estruct ipfrag {
8 R; r* c% l9 _, Z; z; ?int offset; /* offset of fragment in IP datagram */
|5 Q4 {& V6 S/ D3 dint end; /* last byte of data in datagram */ ) ?& R A5 k+ p6 [* |9 k; a; W
int len; /* length of this fragment */ + Z" |$ ?- Y; p' ^/ \9 L: @
struct sk_buff *skb; /* complete received fragment */
; l( i( n% o) ?$ {4 Bunsigned char *ptr; /* pointer into real fragment data */ . s. K9 L8 W3 d- r
struct ipfrag *next; /* linked list pointers */ $ `8 L4 R* o" ]
struct ipfrag *prev;
: r- F/ w; ?: p};
/ f8 @# ~. J5 Z$ a- n- f& [6 a J& N5 d; `- F
这些分片形成一个双向链表(在linux内核中,若需要使用链表,除非有特殊需要,否则推荐双向链表,见document\CodingStyle),表示一个未组装完的分片队列(属于一个ip包)。 6 a7 A7 U( Y! r% g
这个链表的头指针要放在ipq结构中:
# o) L( M. A+ i6 y) k! ^5 Q {: E) |
/* Describe an entry in the "incomplete datagrams" queue. */
/ y9 V% g& B+ N! ystruct ipq {
# T3 @3 Z; p6 Z$ _struct iphdr *iph; /* pointer to IP header */ / R* M% R! }* j: q
struct ipq *next; /* linked list pointers */
0 o; B* k$ Y K7 k% G- hstruct ipfrag *fragments; /* linked list of received fragments */ - g: b1 |( U+ s
int len; /* total length of original datagram */
7 E* l% ?$ y- I' `; |2 @1 nshort ihlen; /* length of the IP header */ 1 Z, N# Z Q+ d
struct timer_list timer; /* when will this queue expire? */
0 ^; x; @: C2 L+ `, T% Mstruct ipq **pprev;
/ o: X2 U, B) b1 x% P6 S+ nstruct device *dev; /* Device - for icmp replies */ % f# }3 ~! E6 ^& Y% x' s9 b6 I
}; * R* m2 _- V5 r% ?
' k9 [5 ^& E# D) K注意每个ipq保留了一个定时器(即struct timer_list timer;)。 2 _$ b+ [- I+ n
3 _& d5 B1 A, p j1 I, O! @* sipq也会形成一个链表,它们是内核当前未组装完的所有IP包。为了便于查找,保留了一个
# a, \+ e- c' U$ Z9 J' B5 Ghash表: & e% R. [7 Q8 S8 f8 K1 T& o
#define IPQ_HASHSZ 64
* e) v6 k8 W/ I& V9 nstruct ipq *ipq_hash[IPQ_HASHSZ]; $ U4 [- d$ B5 Q4 D6 y8 z! z/ m' }. u
#define ipqhashfn(id, saddr, daddr, prot) \ & e; `7 e( @9 T) f
((((id) 〉〉 1) ^ (saddr) ^ (daddr) ^ (prot)) & (IPQ_HASHSZ - 1))
- w, I% |. t" o# N1 [
1 V4 s+ w1 s; I8 P5 m* y& N f--------_____________ 8 F% h2 |/ X/ S' R. V$ [
| 1 | |
3 r1 C1 Z+ m) S+ F7 U6 R" @-------- ----------- ------------ ------------ 9 Q8 V& U) \& e
Hash表 | 2 | | ipq1 |----〉| ipfrag1 |-----〉| ipfrag2 |------〉.......
) r% k& H6 o% c/ O( G; u5 M, m-------- ------------ ------------- ------------ 7 U4 E9 Q1 g8 ~' g3 i" ~, B
...... | ! n0 w& v6 H5 S! i
-------- \/
5 A* h. a) M& M1 b| 63 | ------------ ------------- -----------
1 }4 r e j! c& F8 y, _1 }0 K-------- | ipq2 |----〉| ipfrag1 |-----〉| ipfrag2 |------〉.......
" |7 j9 z5 N D- g------------ ------------- ----------- ( y+ V* m q& `& L7 F( E1 `
| * k+ E. H8 Z s# m# X, ^) S
\/ : @; |3 L& T# H( T
------------ ------------- -----------
( O! i! {$ v$ Z& N# `| ipq3 |----〉| ipfrag1 |-----〉| ipfrag2 |------〉....... 4 T+ S% x9 ~/ V2 E9 @
------------ ------------- ----------- 1 U1 P4 y- V7 _% g- h& @8 ~ f
|
9 N0 r3 s0 y0 [0 U: J( {\/
$ s+ M' A" h3 Z........ , i/ V4 Z2 v6 d; C4 `0 ]8 H
! `2 G3 i9 C3 h' L. k5 t5 ]每个IP包用如下四元组表示:(id,saddr,daddr,protocol),四个值都相同的碎片保留在一个IPQ中,即可组装成一个完整的IP包。
% W: X4 k* i2 q; {' t$ y
# b5 l$ l5 q( n& o) O |$ ?此结构在2.4内核中有了改动,具体将在下文中声明。 ' `9 s+ d. ~5 u% a$ W
' x. R' J& m# [6 }: i9 L+ i$ `
3. 重要函数说明(2.2系列) 7 S8 \+ r) L6 h: i
/ X" D1 Z" m3 o3.1 ip_defrag() : U8 [1 T& P; C( D
ip_defrag()是整个流程的入口,下面我们首先对ip_defrag()作一定的说明。 ' m- `2 N9 X* ^* o+ Q
. F; ~: l7 o( \* B(1)为了防止因保留分片而造成内存消耗过大,linux设置了界限来防止这种情况,如果超过了内存使用的上限,则清空内存中最老的队列(ipq).所用内存的大小保存在变量ip_frag_mem中,当然,对它的读写都应是“原子”操作(atomic_sub,atomic_add,atomic_read,etc)。 , o9 {9 e2 y! w
其定义在文件ip_fragment.c前部: 8 Z& {' F9 B) P
: V/ k5 U) U7 u" n6 O! @
atomic_t ip_frag_mem = ATOMIC_INIT(0); /* Memory used for fragments */
3 p3 v$ V1 V) C4 y" v
5 ?% p6 e; ?) }if (atomic_read(&ip_frag_mem) 〉 sysctl_ipfrag_high_thresh) % H! R m K# M9 `+ a5 S3 K( h
ip_evictor(); 7 _& d+ }3 K3 H: U% p3 o
! O4 t) c' p- c) P8 Z+ R/ Cip_evicator的具体操作将在下文中描述。 8 J; V( _2 }6 \, X7 v7 a7 ~
! R6 g6 S2 F3 e- [(2)以id, saddr, daddr, protocol为标志检索是否已经建立了相应的ipq,若发现,则返回ipq的指针,并重置定时器。 1 f2 r* x9 \! ^1 H7 S$ y: P
. l0 f6 d s- a) O- R5 A5 `0 }& d) \qp = ip_find(iph, skb-〉dst); 1 a, c1 E% Z/ ]; p& E' ?1 Y
- F% P2 |8 Z; o5 o1 m' R(3)此时有一个if/else对,其作用是: 1 l, u) g9 @' }4 e* X
如果ipq已经存在,则证明已经有同一个包的其他分片到达。检查此片是不是第一个分片(因为分片到达顺序可能错乱),若是,将ip头信息和头长度保留在ipq结构中();
. i% d% W+ o; ~, S0 Kif (offset == 0) {
5 ?5 L7 n9 x0 O6 j. t4 h0 ~/* Fragmented frame replaced by unfragmented copy? */
- w; c8 Z7 g0 i2 C: J3 N3 lif ((flags & IP_MF) == 0) ; f7 ~: K6 g8 D0 z& k' N0 m7 t
goto out_freequeue;
& j' T7 ]7 R7 S& x: w2 _qp-〉ihlen = ihl; * G" k! G1 M( _" h; Y
memcpy(qp-〉iph, iph, (ihl + 8)); * S7 ]% p2 @2 f
} : _& M R5 W; |* x0 e& C
6 Z# f+ D$ V* \3 R# R5 |如果不存在,当然要建立一个了: 2 _3 w& F* S/ \( z# f9 i- \
qp = ip_create(skb, iph); " `3 r" [; ?' d! r& X
if (!qp) 2 z3 Q/ g$ i4 \4 _
goto out_freeskb;
9 b; `5 | }9 R* i
4 N Y! E5 B% K3 n- @! pip_create便是分配出一块内存,初始化这个ipq,并在hash表中登记。
+ D# p7 b. H0 t7 Z
" `$ e, M! g) o7 s9 _6 Q到此为止ipq已经肯定存在了,不管是已经存在的,还是我们刚才生成的。
$ t1 V" p3 ~& q1 q
2 |& D/ n7 A# } i+ ]& b! K. K(4)对包的长度进行检测,如果超过了ip包的最大范围,则报警,并丢弃此包。jolt2便是利用这点将window系统打瘫的。由于linux做了这种检查,所以基本免受其害。
/ J6 _& |: {. ?
1 D/ `1 ]6 I6 g1 @2 S( p" U(5)调节end值(数据的结尾位置),如果是最后一个包,则最终整个ip包的长度便可以知道了,为了组装时方便,将其记录到ipq中。
, M1 n6 v/ @8 `/* Determine the position of this fragment. */ 9 _0 P- U' E B/ u ?: G% V6 V: P
end = offset + ntohs(iph-〉tot_len) - ihl; 3 T; y+ }! U0 o7 j5 r9 p9 j
: u& c2 k7 p6 }+ g/* Is this the final fragment? */
7 p# r0 f0 v1 }. n u# yif ((flags & IP_MF) == 0)
6 m& ?) w1 Z/ R7 }4 M; ~' Rqp-〉len = end; 5 N: f; J( [5 g) z% \; b% ~+ u
5 l5 K. @- O$ _(6)接下来很长一段代码(line481-line586)便是定位这份分片在整个数据包中的位置。如果分片之间有重合(恶意攻击和其他异常),则能归并便归并。这个问题我们将在后面(常见碎片攻击中)详谈。 # t% ^. y) A% e# U+ U( F
# _5 ^* y: O. L4 T. A- v
(7)此时我们已经知道这个分片的具体位置了。我们要生成一份新的ipfrag结构,并将其放到 ' F) @# J" @8 C
我们刚才找到的正确位置上去。
1 j3 \7 b( N1 G# ]2 stfp = ip_frag_create(offset, end, skb, ptr); 7 H; Q O& ?/ Z# J9 d0 n2 @
if (!tfp) 5 V; X* `! U3 A
goto out_freeskb; - R: N6 h5 M3 R8 `
0 D6 c' b9 w6 E+ V/* Insert this fragment in the chain of fragments. */
* P7 o) ?' P) S0 L5 y- ~+ ~tfp-〉prev = prev;
- H8 w" A. \% r3 A: A: K# _: jtfp-〉next = next;
3 R$ m; T" \8 @& z' v- n; jif (prev != NULL)
% W6 m9 g8 \: V& {. fprev-〉next = tfp;
' Z4 l1 C# ` Telse 3 S" ~! r& m7 [, ~1 r
qp-〉fragments = tfp; 0 }$ ]. V [ {8 X2 l. g+ a. i. ?
$ X/ A$ u" k9 ~+ y
if (next != NULL)
, T" _: c+ C$ |* e( Q4 unext-〉prev = tfp;
7 b4 B I# Y5 d9 \4 Z" n8 Z) z# i' P$ }' M% S, Q2 V/ x, |, ]
(8)ip_done函数检查是否所有的分片已经到齐,如果到齐,则将其组装成一个新的sk_buff(调用ip_glue),并最终返回到调用ip_defrag的地方。
! S; c& K) c8 c+ \
, S j4 j+ o/ e! z+ b' J/ k8 ]if (ip_done(qp)) { /*全部到齐了么?*/
4 D; F3 Z' f; |8 O+ j/* Glue together the fragments. */ , u& `, V6 W- N: v& ~
skb = ip_glue(qp);
1 _! t3 h& d/ i; k' S& V" I$ |. ~/* Free the queue entry. */ 2 h$ I' j! E0 [3 _% U; j- o
out_freequeue: , \: C6 L7 q: J% M6 l* C
ip_free(qp); /*原有的ipq结构已经不需要了,释放。*/
! z& d: J3 M" p. G: hout_skb: ) l3 @$ O) j. n7 G2 F2 ^& m# z& Q
return skb; /*组装完成,可以返回了*/
. a$ J$ u" U" i* U: I4 o} i1 \' R4 H$ C- o+ |" Q$ K) I7 o
: `" k- ^$ ^6 _; f. p% D& X
如果没有到齐,则返回NULL.
5 o, D' ]( C/ D9 ]" M$ \( ]
& S/ `9 U/ V4 ~9 i至此全部组装过程结束。 * N" X6 A+ D* i8 F8 D( A
6 m" d8 n) ?) w. M5 k, A9 J2 i3.2 ip_evictor() , b& e4 S" g" ^+ _+ F7 V
( c: n r. X }$ f/ G( L当分片所用的内存超过一定的上限时(sysctl_ipfrag_high_thresh)会调用ip_evicator以释放内存。
+ r% q1 k: ?- F, xip_evicator会找寻可清空的IPQ,并将其清空,直到到达到可用的下限(sysctl_ipfrag_low_thresh)
# ~' `: I/ h2 ?/ k5 n2 o, L1 W' ~。 8 O' J- q5 s+ I
% G- D7 D1 u" U) R5 l这个值在ip_fragment.c中按如下定义:
2 [! E# D% K( F$ | k$ lint sysctl_ipfrag_high_thresh = 256*1024;
% p( `5 G5 T. k2 f. D" d) G4 Jint sysctl_ipfrag_low_thresh = 192*1024;
* T( }, t4 r/ E7 A% o4 D& W a* B3 r" m$ F1 T
同样,用sysctl -a可可看到这两参数,同时可以动态修改。 ( X+ i0 w( G& q1 W! y( ?$ A( b
#sysctl -a 2 W5 ]; t* J0 h: e3 s/ U
......
, B- z+ S. e$ Znet.ipv4.ipfrag_low_thresh = 196608
0 T) Q% u% |2 g1 k+ C6 K+ Inet.ipv4.ipfrag_high_thresh = 262144 ^" o) a( Z6 _9 |
...... ' m; G9 g3 c' C3 p- z$ H+ v
' j6 n* S; K0 c r% k# X9 B/ p: e
理论上ip_evicator应该采用LRU算法,将最古老的IPQ清除。但目前linux(包括2.4.0)没有实现此功能,只是将hash表按次序清空,这样的好处是简单易行。 ( G q5 o5 q" t6 W* o# x
) f0 K" S0 e. A) d; e
3.3 ip_glue() : U. \* ~* ^3 `$ A; e4 c# T- G2 k, U$ n
) n- B) g0 R& X% }% b
ip_glue()函数将负责将一个所有分片已经到齐的的IP包组合好。当这一步进行时,所有的分片已经按顺序排好,并解决了所有的重叠问题。因此其流程相应很简单。 . F. e' L% A1 p/ ]& l% G
首先生成一个足够大的(足以容纳所有的分片包长度的总和)新的skbuff:
* v4 l2 W4 @6 }- f& d. bskb = dev_alloc_skb(len); ! j% B- M/ M S( s. w$ V& e* ^2 G
if (!skb) 4 r! u$ s- f; h" _6 p& t
goto out_nomem;
/ c/ {5 i/ n- q+ A0 ]# |调整一些必要的指针后,就在一个while循环中依次将原有分片的内容用memcoy拷贝到新的skbuff中。再进行一些指针调整后,过程结束,将新的skbuff返回。 - c, U, E* S, i# J- g: V' {, i
( y2 R" H. p+ R% h
3.4 ip_expire()
9 k$ g* R0 J/ G! f6 u
9 D8 t+ x, L, y. V0 a前面已经提到,每个ipq保留了一个定时器,当一定时间以后组装还没有完成,将清空此队列。定时器的值保留在sysctl_ipfrag_time中: 7 s- A7 z, }) s9 x) O! A5 A+ ^2 s( V
int sysctl_ipfrag_time = IP_FRAG_TIME;
2 D1 K7 J; J4 d0 z6 U, U7 Z(在/include/net/ip.h中有#define IP_FRAG_TIME (30 * HZ) )
% Y3 |7 r/ j8 f& ^此值也可以用sysctl设置。 $ w& R+ T& t( ?& k3 ?
定时器的具体实现机制的没有分析。
' P/ @( }7 J% Q% x
, d) E" E( S9 i0 \4. 2.4系列的变化 # H' c: l9 K$ L
- X/ L1 e' g6 U6 z0 H7 l其实如果仔细看一下,2.4的分片组装代码的流程与2.2系列基本相同,不同的是将函数的分工变化了。由于原ipfrag结构保留的结构均可在skbuff中得到,在2.4中将此结构取消了,并对ipq结构做了一些修改。其他主要变化有:
8 U" h. J5 m5 y" Q8 F7 C( F( X% K M* S1)ip_defrag分成了ip_defrag和ip_frag_queue两部分。 ' q6 ]8 A: N8 d* d" ^
2)ip_glue换名成ip_frag_reasm,流程基本未动。 7 P/ K. n4 s: W7 n
3)现在ipq中用meat保留现有的分片长度的累加值(已经解决重叠),如果此值到达总长度,则意味着所有的分片到达,因此取消了ip_done函数,不必每次遍历一次链表,因此在效率上有了较大的提高,抗小碎片攻击的能力得到加强。