下载 http://www.busybox.net/downloads/binaries/latest/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
下载 http://www.busybox.net/downloads/binaries/latest/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
内核堆栈溢出通常有两种情况。一种是函数调用栈超出了内核栈THREAD_SIZE的大小, 这是栈底越界,另一种是栈上缓冲越界访问,这是栈顶越界。
以arm平台为例,内核栈THREAD_SIZE为8K,当调用栈层次过多或某调用栈上分配过大的 空间,就会导致它越界。越界后struct thread_info结构可能被破坏,轻则内核 panic,重则内核数据被覆盖仍继续运行。
对于栈顶越界,gcc提供了支持。打开内核配置CONFIG_CC_STACKPROTECTOR后,会打 开编译选项-fstack-protector.
CC_STACKPROTECT补丁是Tejun Heo在09年给主线kernel提交的一个用来防止内核堆栈溢出的补丁。默认的config是将这个选项关闭的,可以在编译内核的时候, 修改.config文件为CONFIG_CC_STACKPROTECTOR=y来启用。未来飞天内核可以将这个选项开启来防止利用内核stack溢出的0day攻击。这个补丁的防溢出原理是: 在进程启动的时候, 在每个buffer的后面放置一个预先设置好的stack canary,你可以把它理解成一个哨兵, 当buffer发生缓冲区溢出的时候, 肯定会破坏stack canary的值, 当stack canary的值被破坏的时候, 内核就会直接当机。那么是怎么判断stack canary被覆盖了呢? 其实这个事情是gcc来做的,内核在编译的时候给gcc加了个-fstack-protector参数, 我们先来研究下这个参数是做什么用的。
先写个简单的有溢出的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 |
|
反汇编看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
没什么特别的,我们在加上-fstack-protector参数看看:
1 2 3 4 |
|
这次程序打印了一条堆栈被溢出的信息,然后就自动退出了。
在反汇编看下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
使用-fstack-protector参数后, gcc在函数的开头放置了几条汇编代码:
1 2 3 |
|
将代码段gs偏移0×14内存处的值赋值给了ebp-4, 也就是第一个变量值的后面。
在call完memeset后,有如下汇编代码:
1 2 3 4 5 |
|
在memset后,gcc要检查这个操作是否发生了堆栈溢出, 将保存在ebp-4的这个值与原来的值对比一下,如果不相同, 说明堆栈发生了溢出,那么就会执行stack_chk_fail这个函数, 这个函数是glibc实现的,打印出上面看到的信息, 然后进程退出。
从这个例子中我们可以看出gcc使用了-fstack-protector参数后,会自动检查堆栈是否发生了溢出, 但是有一个前提就是内核要给每个进程提前设置好一个检测值放置在%gs:0×14位置处,这个值称之为stack canary。所以我们可以看到防止堆栈溢出是由内核和gcc共同来完成的。
gcc的任务就是放置几条汇编代码, 然后和%gs:0×14位置处的值进行对比即可。 主要任务还是内核如何来设置stack canary, 也是CC_STACKPROTECTOR补丁要实现的目的, 下面我们仔细来看下这个补丁是如何实现的。
既然gcc硬性规定了stack canary必须在%gs的某个偏移位置处, 那么内核也必须按着这个规定来设置。
对于32位和64位内核, gs寄存器有着不同的功能。
64位内核gcc要求stack canary是放置在gs段的40偏移处, 并且gs寄存器在每cpu变量中是共享的,每cpu变量irq_stack_union的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
gs_base只是一个40字节的站位空间, stack_canary就紧挨其后。并且在应用程序进出内核的时候,内核会使用swapgs指令自动更换gs寄存器的内容。
32位下就稍微有点复杂了。由于某些处理器在加载不同的段寄存器时很慢, 所以内核使用fs段寄存器替换了gs寄存器。 但是gcc在使用-fstack-protector的时候, 还要用到gs段寄存器, 所以内核还要管理gs寄存器,我们要把CONFIG_X86_32_LAZY_GS选项关闭, gs也只在进程切换的时候才改变。 32位用每cpu变量stack_canary保存stack canary。
1 2 3 4 5 |
|
内核是处于保护模式的, 因此gs寄存器就变成了保护模式下的段选子,在GDT表中也要有相应的设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
GDT表中的第28个表项用来定为stack canary所在的段。
1 2 |
|
GDT_STACK_CANARY_INIT在刚进入保护模式的时候被调用, 这个段描述符项被设置为基地址为0, 段大小设为24,因为只在基地址为0, 偏移为0×14处放置一个4bytes的stack canary, 所以24字节正好。不理解的同学可以看看intel保护模式的手册, 对着段描述符结构一个个看就行了。
在进入保护模式后, start_kernel()会调用boot_init_stack_canary()来初始话一个stack canary。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
随机出了一个值赋值给每cpu变量, 32位是stack_canary, 64位是irq_stack_union。
内核在进一步初始化cpu的时候,会调用setup_stack_canary_segment()来设置每个cpu的GDT的stack canary描述符项:
start_kernel()->setup_per_cpu_areas()->setup_stack_canary_segment:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
在内核刚进入保护模式的时候, stack canary描述符的基地址被初始化为0, 现在在cpu初始化的时候要重新设置为每cpu变量stack_canary的地址, 而不是变量保存的值。通过这些设置当内核代码在访问%gs:0×14的时候, 就会访问stack canry保存的值。注意:setup_stack_canary_segment是针对32位内核做设置, 因为64位内核中的irq_stack_union是每cpu共享的, 不用针对每个cpu单独设置。 然后就可以调用switch_to_new_gdt(cpu);来加载GDT表和加载gs寄存器。
经过上述初始化过程,在内核代码里访问%gs:0×14就可以定位stack canary的值了, 那么每个进程的stack canary是什么时候设置的呢?
在内核启动一个进程的时候, 会把gs寄存器的值设为KERNEL_STACK_CANARY
1 2 3 4 5 6 7 8 9 10 |
|
内核在fork一个进程的时候, 有如下操作:
1 2 3 4 5 6 |
|
随机初始化了一个stack_canary保存在task_struct结构中的stack_canary变量中。当进程在切换的时候, 通过switch宏把新进程的stack canary保存在每cpu变量stack_canary中, 当前进程的stack_canary也保存在一个每cpu变量中,完成stack canary的切换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
前面讲过当gcc检测到堆栈溢出的时候, 会调用glibc的stack_chk_fail函数, 但是当内核堆栈发生溢出的时候,不能调用glibc的函数,所以内核自己实现了一个stack_chk_fail函数:
kernel/panic.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
当内核堆栈发生溢出的时候,就会执行stack_chk_fail函数, 内核当机。
这就是这个补丁的原理,不懂的同学请参考:
http://www.pagefault.info/?p=403
这里分析的驱动代码是给予linux kernel 3.4.4
对应的文件在drivers/net/ethernet/intel 目录下,这个分析不涉及到很细节的地方,主要目的是理解下数据在协议栈和驱动之间是如何交互的。
首先我们知道网卡都是pci设备,因此这里每个网卡驱动其实就是一个pci驱动。并且intel这里是把好几个万兆网卡(82599/82598/x540)的驱动做在一起的。
首先我们来看对应的pci_driver的结构体,这里每个pci驱动都是一个pci_driver的结构体,而这里是多个万兆网卡共用这个结构体ixgbe_driver.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
然后是模块初始化方法,这里其实很简单,就是调用pci的驱动注册方法,把ixgbe挂载到pci设备链中。 这里不对pci设备的初始化做太多介绍,我以前的blog有这方面的介绍,想了解的可以去看看。这里我们只需要知道最终内核会调用probe回调来初始化ixgbe。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
这里不去追究具体如何调用probe的细节,我们直接来看probe函数,这个函数中通过硬件的信息来确定需要初始化那个驱动(82598/82599/x540),然后核心的驱动结构就放在下面的这个数组中。
1 2 3 4 5 |
|
ixgbe_probe函数很长,我们这里就不详细分析了,因为这部分就是对网卡进行初始化。不过我们关注下面几个代码片段。
首先是根据硬件的参数来取得对应的驱动值:
1
|
|
然后就是如何将不同的网卡驱动挂载到对应的回调中,这里做的很简单,就是通过对应的netdev的结构取得adapter,然后所有的核心操作都是保存在adapter中的,最后将ii的所有回调拷贝给adapter就可以了。我们来看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
最后需要关注的就是设置网卡属性,这些属性一般来说都是通过ethtool 可以设置的属性(比如tso/checksum等),这里我们就截取一部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
然后我们来看下中断的注册,因为万兆网卡大部分都是多对列网卡(配合msix),因此对于上层软件来说,就好像有多个网卡一样,它们之间的数据是相互独立的,这里读的话主要是napi驱动的poll方法,后面我们会分析这个.
到了这里或许要问那么网卡是如何挂载回调给上层,从而上层来发送数据呢,这里是这样子的,每个网络设备都有一个回调函数表(比如ndo_start_xmit)来供上层调用,而在ixgbe中的话,就是ixgbe_netdev_ops,下面就是这个结构,不过只是截取了我们很感兴趣的几个地方.
不过这里注意,读回调并不在里面,这是因为写是软件主动的,而读则是硬件主动的。现在ixgbe是NAPI的,因此它的poll回调是ixgbe_poll,是中断注册时候通过netif_napi_add添加进去的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
这里我们最关注的其实就是ndo_start_xmit回调,这个回调就是驱动提供给协议栈的发送回调接口。我们来看这个函数.
它的实现很简单,就是选取对应的队列,然后调用ixgbe_xmit_frame_ring来发送数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
而在ixgbe_xmit_frame_ring中,我们就关注两个地方,一个是tso(什么是TSO,请自行google),一个是如何发送.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
调用ixgbe_tso处理完tso之后,就会调用ixgbe_tx_map来发送数据。而ixgbe_tx_map所做的最主要是两步,第一步请求DMA,第二步写寄存器,通知网卡发送数据.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
上面的操作是异步的,也就是说此时内核还不能释放SKB,而是网卡硬件发送完数据之后,会再次产生中断通知内核,然后内核才能释放内存.接下来我们来看这部分代码。
首先来看的是中断注册的代码,这里我们假设启用了MSIX,那么网卡的中断注册回调就是ixgbe_request_msix_irqs函数,这里我们可以看到调用request_irq函数来注册回调,并且每个队列都有自己的中断号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
而对应的中断回调是ixgbe_msix_clean_rings,而这个函数呢,做的事情很简单(需要熟悉NAPI的原理,我以前的blog有介绍),就是调用napi_schedule来重新加入软中断处理.
1 2 3 4 5 6 7 8 9 10 11 |
|
而NAPI驱动我们知道,最终是会调用网卡驱动挂载的poll回调,在ixgbe中,对应的回调就是ixgbe_poll,那么也就是说这个函数要做两个工作,一个是处理读,一个是处理写完之后的清理.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
http://www.pagefault.info/?p=145
这次主要来看一下内核拥塞控制算法cubic的实现,在linux kernel中实现了很多种拥塞控制算法,不过新的内核(2.6.19之后)默认是cubic(想得到当前内核使用的拥塞控制算法可以察看/proc/sys/net/ipv4/tcp_congestion_control这个值).下面是最新的redhat 6的拥塞控制算法(rh5还是bic算法):
1 2 |
|
这个算法的paper在这里:
http://netsrv.csc.ncsu.edu/export/cubic_a_new_tcp_2008.pdf
拥塞控制算法会在tcp_ack中被调用,如果是正常的ack(比如不是重复的,不是sack等等)就会进入拥塞控制算法。
cubic会调用tcp_slow_start这个方法(基本上每种拥塞控制算法都会调用它),这个方法主要是处理slow start,而内核中的slow start是这样子的,接收一个ack,snd_cwnd就会加1,然后当cwnd大于设置的拥塞窗口阀值snd_ssthresh的时候,就会进入拥塞避免状态。而在发送数据包的时候,会判断in_flight(可以认为是发送还没确认的数据包,它等于发送未确认的数据包-sack的数据段-丢失的数据段+重传的数据段,我的前面的blog有详细解释这个数据段)是否大于snd_cwnd,如果大于等于则不会发送数据,如果小于才会继续发送数据。
而进入拥塞避免状态之后,窗口的增长速度将会减缓,
来看一下我用jprobe hook tcp_slow_start(slow start处理函数) 和 tcp_cong_avoid_ai (拥塞避免处理)的数据。
在下面的数据中sk表示当前socket的地址, in_flight packet表示发送还未接收的包, snd_cwnd表示发送拥塞窗口。 然后详细解释下count后面的两个值,其中第一个是snd_cwnd_cnt,表示在当前的拥塞窗口中已经发送的数据段的个数,而第二个是struct bictcp的一个域cnt,它是cubic拥塞算法的核心,主要用来控制在拥塞避免状态的时候,什么时候才能增大拥塞窗口,具体实现是通过比较它和snd_cwnd_cnt,来决定是否增大拥塞窗口,而这个值的计算,我这里不会分析,想了解的,可以去看cubic的paper。
还有一个需要注意的地方就是ssthresh,可以看到这个值在一开始初始化为一个最大的值,然后在进入拥塞避免状态的时候被设置为前一次拥塞窗口的大小.这个处理可以看rfc2581的这段:
The initial value of ssthresh may be arbitrarily high (i.e., the size of the advertised window), but it may be reduced in response to congestion. When cwnd < ssthresh, the slow-start algorithm is used and when cwnd > ssthresh, the congestion avoidance algorithm is used. When cwnd and ssthresh are equal, the sender may use either of them.
我们后面会看到这个值在cubic中是如何被设置的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
可以看到在slow start的状态,发送拥塞窗口就是很简单的每次加1,而当进入拥塞避免之后,明显的拥塞窗口的增大速度变慢很多。
接下来来看具体的代码是如何实现的.
首先来看bictcp_cong_avoid,也就是cubic拥塞控制算法的handler(一般来说在tcp_ack中被调用),它有3个参数,第一个是对应的sock,第二个是对应的ack序列号,而第三个就是比较重要的一个变量,表示发送还没有被ack的数据包(在linux 内核tcp拥塞处理一中详细介绍过内核中这些变量),这个变量是拥塞控制的核心。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
接下来就是看tcp_is_cwnd_limited,这个函数主要是实现RFC2861中对拥塞窗口的检测。它返回1说明拥塞窗口被限制,我们需要增加拥塞窗口,否则的话,就不需要增加拥塞窗口。
然后这里还有两个判断,先来看第一个 gso的概念,gso是Generic Segmentation Offload的简写,他的主要功能就是尽量的延迟数据包的传输,以便与在最恰当的时机传输数据包,这个机制是处于数据包离开协议栈与进入驱动之间。比如如果驱动支持TSO的话,gso就会将多个unsegmented的数据段传递给驱动。而TSO是TCP Segmentation Offload的缩写,它表示驱动支持协议栈发送大的MTU的数据段,然后硬件负责来切包,然后将数据发送出去,这样子的话,就能提高系统的吞吐。这几个东西(还有GRO),以后我会详细分析,现在只需要大概知道他们是干什么的。
而在这里如果支持gso,就有可能是tso defer住了数据包,因此这里会进行几个相关的判断,来看需不需要增加拥塞窗口。。
然后是burst的概念,主要用来控制网络流量的突发性增大,也就是说当left数据(还能发送的数据段数)大于burst值的时候,我们需要暂时停止增加窗口,因为此时有可能我们这边数据发送过快。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
接下来来看snd_ssthresh是如何被设置的,这个值在加载cubic模块的时候可以传递一个我们制定的值给它,不过,默认是很大的值,我这里是2147483647,然后在接收ack期间(slow start)期间会调整这个值,在cubic中,默认是16(一般来说说当拥塞窗口到达16的时候,snd_ssthresh会被设置为16).
在cubic中有两个可以设置snd_ssthresh的地方一个是hystart_update,一个是bictcp_recalc_ssthresh,后一个我这里就不介绍了,以后介绍拥塞状态机的时候会详细介绍,现在只需要知道,只有遇到拥塞的时候,需要调整snd_ssthres的时候,我们才需要调用bictcp_recalc_ssthresh。
而hystart_update是在bictcp_acked中被调用,而bictcp_acked则是基本每次收到ack都会调用这个函数,我们来看在bictcp_acked中什么情况就会调用hystart_update:
1 2 3 4 |
|
其中hystart是hybrid slow start打开的标志,默认是开启,hystart_low_window是设置snd_ssthresh的最小拥塞窗口值,默认是16。而tp->snd_ssthresh默认是一个很大的值,因此这里就知道了,当拥塞窗口增大到16的时候我们就会进去hystart_update来更新snd_ssthresh.因此hystart_updat换句话来说也就是主要用于是否退出slow start。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
然后是slow start的处理,这里有关abc的处理,注释都很详细了,这里就不解释了,我们主要看abc关闭的部分。这里使用cnt,也是主要为了打开abc之后的slow start。
这是abc(Appropriate Byte Counting)相关的rfc:
http://www.faqs.org/rfcs/rfc3465.html
Appropriate Byte Countin会导致拥塞控制算法很激进,比如打开它之后就不一定每次ack都会执行slow start,而且窗口也会增加的快很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
最后是拥塞避免的处理。这里主要的步骤就是通过判断当前的拥塞窗口下已经发送的数据段的个数是否大于算法计算出来的值w,如果大于我们才能增加拥塞窗口值,否则之需要增加snd_cwnd_cnt。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
http://my.oschina.net/kisops/blog/150995
解决:nf_conntrack: table full, dropping packet.
“连接跟踪表已满,开始丢包”!相信不少用iptables的同学都会见过这个错误信息吧,这个问题曾经也困扰过我好长一段时间。此问题的解决办法有四种(nf_conntrack 在CentOS 5 / kernel <= 2.6.19中名为 ip_conntrack ):
1 2 3 4 |
|
切记:在防火墙关闭状态下,不要通过iptables指令(比如 iptables -nL)来查看当前状态!因为这样会导致防火墙被启动,而且规则为空。虽然不会有任何拦截效果,但所有连接状态都会被记录,浪费资源且影响性能并可能导致防火墙主动丢包!
以64G的64位操作系统为例,CONNTRACK_MAX = 6410241024*1024/16384/2 = 2097152
即时生效请执行:
1
|
|
CONNTRACK_BUCKETS = CONNTRACK_MAX / 8
同样64G的64位操作系统,哈希最佳范围是 262144 ~ 1048576 。
运行状态中通过 sysctl net.netfilter.nf_conntrack_buckets 进行查看,通过文件 /sys/module/nf_conntrack/parameters/hashsize 进行设置
或者新建 /etc/modprobe.d/iptables.conf ,重新加载模块才生效:
1
|
|
sysctl -a | grep nf_conntrack
可以调优(/etc/sysctl.conf ):1 2 3 4 5 |
|
三、使用祼表,添加“不跟踪”标识。如下示例更适合桌面系统或随意性强的服务器。因为它开启了连接的状态机制,方便和外部通信。修改 /etc/sysconfig/iptables 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
或者干脆对所有连接都关闭跟踪,不跟踪任何连接状态。不过规则就限制比较严谨,进出都需要显式申明。示例 /etc/sysconfig/iptables :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
效果如下图:
lsmod | grep nf_conntrack
,不使用连接状态的跟踪功能。1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
另外,防火墙的配置文件最好也改下,不要加载任何额外模块(/etc/sysconfig/iptables-config):
1 2 3 4 5 6 7 8 |
|
往往我们对连接的跟踪都是基于操作系统的(netstat / ss ),防火墙的连接状态完全是它自身实现产生的。
总结:防火墙有条件还是交给上层设备完成会更好,使用防火墙一定要做调优;如果不需要防火墙的跟踪功能,规则简单的可以开启NOTRACK选项,条件允许的情况下就删除它吧!