作者:k0pwn_ko
预估稿费:700RMB(不服你也来投稿啊!)
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
最近这一段时间,NTP又搞事情了,很多安全媒体也进行了报道,很多NTP漏洞都很有意思,NTP是一个网络时间协议,用来同步各个计算机之间的时间,有一些DDoS就是利用NTP放大攻击来进行的。同样,这段时间连续爆出了多个NTP的拒绝服务漏洞,通过这种漏洞,可以造成NTP服务,甚至NTP服务器拒绝服务,之前Freebuf上面有过一篇关于Windows NTP拒绝服务漏洞的报道。
http://www.freebuf.com/vuls/121129.html
讲的是Windows下的一个NTP拒绝服务漏洞CVE-2016-9311,后来我在CNVD也看到了一篇公告,关于NTP拒绝服务漏洞的。
http://www.cnvd.org.cn/webinfo/show/3992
而我选取了其中一个Linux下的NTP拒绝服务漏洞CVE-2016-7434来进行完整分析,这个漏洞在复现的过程中发现不需要进行任何设置即可达到漏洞利用的效果,当然了,在ntp.org11月发布的最新版4.2.8p9中修复了这个漏洞,而4.2.8p8则受此漏洞影响。
NTP协议浅析与CVE-2016-7434
关于客户端和NTP服务器之间的NTP协议交互,同步时间的过程我不再详细说明了,用一副图可以简要说明时间同步的过程,在这个过程中,数据采取NTP协议传输,而与服务器交互的端口是123端口。
我们下载NTP 4.2.8p8,通过tar解压之后,用configure、make、make install进行安装,安装后,通过./ntpd -n -c [ntp.conf path]的方法运行ntpd,很多Linux系统自带NTP,需要切换到NTPD目录下执行该目录下的NTP才能确保版本是有问题的版本。
我们来看一下NTP协议格式。
关于NTP协议每一个字段的含义,网上都有相关解释,这里我就不再赘述,在这中间,涉及到一个Mode,它代表着工作模式,这里值得一提的是,在以前的NTP协议中,通常用Mode7的monlist特性来响应NTP请求,但是由于monlist存在漏洞,可以利用这个漏洞来进行NTP放大攻击,也就是DDoS,后来monlist特性被禁止了,被改成Mode6的mrulist特性,以此避免NTP放大攻击,而这次漏洞,就是由于mrulist导致的。
我们通过CVE-2016-7434的Payload发送一个畸形数据包,同时抓包分析数据。
可以看到,第一个字节是16,转换成二进制就是00010110,根据之前对于NTP协议格式的分析,第0、1比特代表的是Leap Indicator,当这个值为11的时候是告警状态,代表时间同步出现问题,其他则不处理,这里是00;随后第2、3、4比特是010,代表的是版本,之后的5、6、7比特110代表的是Mode,这里也就是6,代表着mrulist特性处理。
CVE-2016-7434漏洞分析
我们在Linux下用gdb attach的方法附加ntpd,发送payload之后,gdb捕获到ntpd崩溃。
通过bt命令,来回溯一下崩溃前的堆栈调用情况
__strlen_sse2_bsf () at ../sysdeps/i386/i686/multiarch/strlen-sse2-bsf.S:50
50../sysdeps/i386/i686/multiarch/strlen-sse2-bsf.S: No such file or directory.
(gdb) bt
#0 __strlen_sse2_bsf () at ../sysdeps/i386/i686/multiarch/strlen-sse2-bsf.S:50
#1 0x080948f0 in estrdup_impl (str=0x0) at emalloc.c:128
#2 0x0805f9b3 in read_mru_list (rbufp=0x89d3dd8, restrict_mask=0)
at ntp_control.c:4041
#3 0x0806a694 in receive (rbufp=0x89d3dd8) at ntp_proto.c:659
#4 0x080598f7 in ntpdmain (argc=0, argv=0xbff16c94) at ntpd.c:1329
#5 0x0804af9b in main (argc=4, argv=0xbff16c84) at ntpd.c:392
可以看到,在#1位置调用了emalloc.c中的estrdup_impl,参数str的值是0x0,直接看一下emalloc.c中对应部分的代码。
char *
estrdup_impl(
const char *str
#ifdef EREALLOC_CALLSITE
,
const char *file,
intline
#endif
)
{
char *copy;
size_tbytes;
bytes = strlen(str) + 1;
这里假如str的值是0x0的话,在strlen的部分会读取0x0地址位置存放的值长度,这个位置是不可读的。
gdb-peda$ x/10x 0x0
0x0: Cannot access memory at address 0x0
因此造成了拒绝服务的发生,在estrdup_impl调用之前,调用到了read_mru_list,这个函数就是处理mrulist特性的函数,而在这个函数之前调用了ntpdmain和receive函数用于接收。
来看一下read_mru_list处理mrulist特性的函数内容,在ntp_control.c中第4034行。
while (NULL != (v = ctl_getitem(in_parms, &val)) &&
!(EOV & v->flags)) {
int si;
if (!strcmp(nonce_text, v->text)) {
if (NULL != pnonce)
free(pnonce);
pnonce = estrdup(val);
这里在pnonce变量赋值位置调用了estrdup,也就是发生问题的函数调用,那么val的值就是0x0,在跟踪read_mru_list中,发现在函数入口处声明了val变量,之后在while循环入口,调用了ctl_getitem函数,其中val作为参数,之后就是estrdup的函数调用,也就是说,ctl_getitem函数中会对val变量进行赋值。
来看一下ctl_getitem的函数内容。
/*
* ctl_getitem - get the next data item from the incoming packet
*/
static const struct ctl_var *
ctl_getitem(
const struct ctl_var *var_list,
char **data
)
ctl_getitem的函数内容就是从数据包中获取下一个数据块内容,其中,data值就是我们关心的val值,下面我们动态跟踪一下val值获取的过程。首先,在read_mru_list处理mrulist特性的函数逻辑入口下断点跟踪。
在函数入口处,首先对可能在数据包中获取的块的名称进行赋值。
const charnonce_text[] ="nonce";
const charfrags_text[] ="frags";
const charlimit_text[] ="limit";
const charmincount_text[] ="mincount";
const charresall_text[] ="resall";
const charresany_text[] ="resany";
const charmaxlstint_text[] ="maxlstint";
const charladdr_text[] ="laddr";
const charresaxx_fmt[] ="0x%hx";
单步跟踪,可以看到调用了set_var进行一系列赋值
gdb-peda$ n
Program received signal SIGALRM, Alarm clock.
[----------------------------------registers-----------------------------------]
EAX: 0xbfade1e0 --> 0x99e98d0 --> 0x0
EBX: 0xbfade310 --> 0xa ('n')
ECX: 0x6
EDX: 0xbfade1ee ("frags")
ESI: 0x0
EDI: 0x74 ('t')
EBP: 0xbfade568 --> 0x99e6ddc --> 0x10920002
ESP: 0xbfade190 --> 0x0
EIP: 0x805f765 (<read_mru_list+357>: call 0x805ed90 <set_var>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x805f753 <read_mru_list+339>: lea eax,[ebp-0x388]
0x805f759 <read_mru_list+345>: mov ecx,0x6
0x805f75e <read_mru_list+350>: mov DWORD PTR [esp],0x0
=> 0x805f765 <read_mru_list+357>: call 0x805ed90 <set_var>
0x805f76a <read_mru_list+362>: lea edx,[ebp-0x374]
0x805f770 <read_mru_list+368>: lea eax,[ebp-0x388]
0x805f776 <read_mru_list+374>: mov ecx,0x6
0x805f77b <read_mru_list+379>: mov DWORD PTR [esp],0x0
Breakpoint 3, 0x0805f765 in set_var (def=0x0, size=0x6,
data=0xbfade1ee "frags", kv=0xbfade1e0) at ntp_control.c:4014
这里赋值的内容是frags,其实这里调用了很多set_var,主要是对in_parms进行初始化。
/*
* fill in_parms var list with all possible input parameters.
*/
in_parms = NULL;
set_var(&in_parms, nonce_text, sizeof(nonce_text), 0);
set_var(&in_parms, frags_text, sizeof(frags_text), 0);
set_var(&in_parms, limit_text, sizeof(limit_text), 0);
set_var(&in_parms, mincount_text, sizeof(mincount_text), 0);
set_var(&in_parms, resall_text, sizeof(resall_text), 0);
set_var(&in_parms, resany_text, sizeof(resany_text), 0);
set_var(&in_parms, maxlstint_text, sizeof(maxlstint_text), 0);
set_var(&in_parms, laddr_text, sizeof(laddr_text), 0);
初始化之后,会开始处理数据包中Data部分的数据,其中会分别获取每一个小块。
gdb-peda$ n
Program received signal SIGALRM, Alarm clock.
[----------------------------------registers-----------------------------------]
EAX: 0x99ea4e0 --> 0x0
EBX: 0xbfade310 ("addr.15")
ECX: 0x0
EDX: 0xbfade1e4 --> 0x0
ESI: 0x10
EDI: 0xbfade550 --> 0x8058ff0 (<finish>:mov eax,DWORD PTR [esp+0x4])
EBP: 0xbfade568 --> 0x99e6ddc --> 0x10920002
ESP: 0xbfade19c --> 0x805f961 (<read_mru_list+865>:test eax,eax)
EIP: 0x805a440 (<ctl_getitem>:push ebp)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x805a43b:xchg ax,ax
0x805a43d:xchg ax,ax
0x805a43f:nop
=> 0x805a440 <ctl_getitem>:push ebp
0x805a441 <ctl_getitem+1>:push edi
0x805a442 <ctl_getitem+2>:push esi
0x805a443 <ctl_getitem+3>:push ebx
0x805a444 <ctl_getitem+4>:sub esp,0x1c
Breakpoint 6, ctl_getitem (var_list=0x99ea4e0,
data=data@entry=0xbfade1e4) at ntp_control.c:3083
这里data就是in_parms的值,而var_list就是val的值,来看一下函数进入时值。
gdb-peda$ x/10s 0xbfade1e4
0xbfade1e4: ""
0xbfade1e5: ""
0xbfade1e6: ""
0xbfade1e7: ""
0xbfade1e8: "nonce"
0xbfade1ee: "frags"
0xbfade1f4: "limit"
0xbfade1fa: "laddr"
0xbfade200: "0x%hx"
这里data部分就是之前初始化in_parms用于存放后续做比较的所有块的名称,而val的值是0x0.
通过调用ctl_getitem来获取下一个小块的内容,而返回值v,就是下一小块的起始地址,val是下一小块存放内容的地址值。首先会对小块进行切割,以逗号作为结束标志,以等于号作为值判断的标志。
gdb-peda$ next
Program received signal SIGALRM, Alarm clock.
[----------------------------------registers-----------------------------------]
EAX: 0xb75536c8 --> 0xb769c760 --> 0x20002
EBX: 0x99e6e40 ("e, laddr=[]:Hrags=32, laddr=[]:WOP")
ECX: 0x65 ('e')
EDX: 0x0
ESI: 0x99e6e72 --> 0x0
EDI: 0xbfade550 --> 0x8058ff0 (<finish>: mov eax,DWORD PTR [esp+0x4])
EBP: 0x99e6e3c ("nonce, laddr=[]:Hrags=32, laddr=[]:WOP")
ESP: 0xbfade170 --> 0xbfade1e4 --> 0x0
EIP: 0x805a4b5 (<ctl_getitem+117>: add ebx,0x1)
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x805a4af <ctl_getitem+111>: nop
0x805a4b0 <ctl_getitem+112>: cmp cl,0x2c
0x805a4b3 <ctl_getitem+115>: je 0x805a4d8 <ctl_getitem+152>
=> 0x805a4b5 <ctl_getitem+117>: add ebx,0x1
0x805a4b8 <ctl_getitem+120>: cmp ebx,esi
0x805a4ba <ctl_getitem+122>: je 0x805a4d8 <ctl_getitem+152>
0x805a4bc <ctl_getitem+124>: movzx ecx,BYTE PTR [ebx]
0x805a4bf <ctl_getitem+127>: cmp cl,0x3d
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
3111 for (tp = NULL, cp = reqpt; cp != reqend; ++cp) {
gdb-peda$ next
Program received signal SIGALRM, Alarm clock.
[----------------------------------registers-----------------------------------]
EAX: 0xb75536c8 --> 0xb769c760 --> 0x20002
EBX: 0x99e6e41 (", laddr=[]:Hrags=32, laddr=[]:WOP")
ECX: 0x2c (',')
EDX: 0x0
ESI: 0x99e6e72 --> 0x0
EDI: 0xbfade550 --> 0x8058ff0 (<finish>: mov eax,DWORD PTR [esp+0x4])
EBP: 0x99e6e3c ("nonce, laddr=[]:Hrags=32, laddr=[]:WOP")
ESP: 0xbfade170 --> 0xbfade1e4 --> 0x0
EIP: 0x805a4bf (<ctl_getitem+127>: cmp cl,0x3d)
EFLAGS: 0x297 (CARRY PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
Legend: code, data, rodata, value
3112 if (*cp == '=' && tp == NULL)
gdb-peda$ next
Program received signal SIGALRM, Alarm clock.
[----------------------------------registers-----------------------------------]
EAX: 0xb75536c8 --> 0xb769c760 --> 0x20002
EBX: 0x99e6e41 (", laddr=[]:Hrags=32, laddr=[]:WOP")
ECX: 0x2c (',')
EDX: 0x0
ESI: 0x99e6e72 --> 0x0
EDI: 0xbfade550 --> 0x8058ff0 (<finish>: mov eax,DWORD PTR [esp+0x4])
EBP: 0x99e6e3c ("nonce, laddr=[]:Hrags=32, laddr=[]:WOP")
ESP: 0xbfade170 --> 0xbfade1e4 --> 0x0
EIP: 0x805a4b0 (<ctl_getitem+112>: cmp cl,0x2c)
EFLAGS: 0x293 (CARRY parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x805a4ab <ctl_getitem+107>: xor edx,edx
0x805a4ad <ctl_getitem+109>: jmp 0x805a4bf <ctl_getitem+127>
0x805a4af <ctl_getitem+111>: nop
=> 0x805a4b0 <ctl_getitem+112>: cmp cl,0x2c
0x805a4b3 <ctl_getitem+115>: je 0x805a4d8 <ctl_getitem+152>
0x805a4b5 <ctl_getitem+117>: add ebx,0x1
0x805a4b8 <ctl_getitem+120>: cmp ebx,esi
0x805a4ba <ctl_getitem+122>: je 0x805a4d8 <ctl_getitem+152>
[------------------------------------stack-------------------------------------]
0000| 0xbfade170 --> 0xbfade1e4 --> 0x0
0004| 0xbfade174 --> 0x80
0008| 0xbfade178 --> 0x99ea4e0 --> 0x0
0012| 0xbfade17c --> 0x99e6e72 --> 0x0
0016| 0xbfade180 --> 0xf
0020| 0xbfade184 --> 0x74 ('t')
0024| 0xbfade188 --> 0xbfade568 --> 0x99e6ddc --> 0x10920002
0028| 0xbfade18c --> 0xbfade310 ("addr.15")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
回头看一下我们之前的数据包,其中nonce块没有赋值,在ctl_getitem中,会对val的值赋值为NULL,如果没有等于号赋值,那么这个值就一直为NULL,这样当执行到返回的时候,val的值为NULL。ntp_control.c第3080行:
static const struct ctl_var *
ctl_getitem(
const struct ctl_var *var_list,
char **data
)
{
//3119行
*data = NULL;
data赋值为0x0后,会对data进行赋值,但是这里由于没有值,所以没有做处理,直接令其为0x0,然后返回。
gdb-peda$ c
Continuing.
Program received signal SIGALRM, Alarm clock.
[-------------------------------------code-------------------------------------]
0x805a670 <ctl_getitem+560>: pop esi
0x805a671 <ctl_getitem+561>: pop edi
0x805a672 <ctl_getitem+562>: pop ebp
=> 0x805a673 <ctl_getitem+563>: ret
Legend: code, data, rodata, value
Breakpoint 8, 0x0805a673 in ctl_getitem (var_list=<optimized out>,
data=data@entry=0xbfade1e4) at ntp_control.c:3195
3195 }
gdb-peda$ x/10x 0x99ea4e0
0x99ea4e0: 0x00000000
随后调用了strcmp做了一个比较,比较的内容就是v的text字段到底属于哪一段,第一个比较的值就是nonce,这里数据包中Data部分的地一小块就是nonce,因此进入nonce处理。
[-------------------------------------code-------------------------------------]
0x805f97e <read_mru_list+894>: sub esp,0x8
0x805f981 <read_mru_list+897>: push edi
0x805f982 <read_mru_list+898>: push eax
=> 0x805f983 <read_mru_list+899>: call 0x804a050 <strcmp@plt>
0x805f988 <read_mru_list+904>: add esp,0x10
0x805f98b <read_mru_list+907>: test eax,eax
0x805f98d <read_mru_list+909>:
jne 0x805f9c0 <read_mru_list+960>
0x805f98f <read_mru_list+911>: mov eax,DWORD PTR [ebp-0x3ac]
Guessed arguments:
arg[0]: 0xbfebcc18 ("nonce")
arg[1]: 0x88c78e8 ("nonce")
而nonce此时没有值,val被赋值为0x0,进入estrdup后,造成了空指针引用。
gdb-peda$ n
Program received signal SIGALRM, Alarm clock.
[-------------------------------------code-------------------------------------]
0x805f9a2 <read_mru_list+930>: add esp,0x10
0x805f9a5 <read_mru_list+933>: sub esp,0xc
0x805f9a8 <read_mru_list+936>: push DWORD PTR [ebp-0x384]
=> 0x805f9ae <read_mru_list+942>: call 0x80948e0 <estrdup_impl>
0x805f9b3 <read_mru_list+947>: add esp,0x10
0x805f9b6 <read_mru_list+950>: mov DWORD PTR [ebp-0x3ac],eax
0x805f9bc <read_mru_list+956>:
jmp 0x805f950 <read_mru_list+848>
0x805f9be <read_mru_list+958>: xchg ax,ax
Guessed arguments:
arg[0]: 0x0
[------------------------------------stack-------------------------------------]
0000| 0xbfebcbc0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 2, 0x0805f9ae in read_mru_list (rbufp=0x88c4dd8,
restrict_mask=0x0) at ntp_control.c:4041
4041 pnonce = estrdup(val);
gdb-peda$ x/10x $esp
0xbfebcbc0: 0x00000000
补丁对比
此漏洞是由于val作为参数传入estrdup_impl函数中,在函数中会调用strlen获取val长度,当val的值为0x0的时候,会由于空指针引用引发拒绝服务漏洞。来看一下在4.2.8p8版本中,ntp_control.c中关于val处理的部分,第4034行:
while (NULL != (v = ctl_getitem(in_parms, &val)) &&
!(EOV & v->flags)) {
int si;
if (!strcmp(nonce_text, v->text)) {
if (NULL != pnonce)
free(pnonce);
pnonce = estrdup(val);
这里没有对val进行判断,导致获取val之后直接传入estrdup中执行,在4.2.8p9,也就是ntp.org中11月更新的最新版中,对这一块处理进行了修复,ntp_control.c中第4048行:
while (NULL != (v = ctl_getitem(in_parms, (void*)&val)) &&
!(EOV & v->flags)) {
int si;
if (NULL == val)
val = nulltxt;
if (!strcmp(nonce_text, v->text)) {
free(pnonce);
pnonce = (*val) ? estrdup(val) : NULL;
可以看到在进入estrdup处理前,先对val本身的值进行了判断,当nonce的值没有未初始化时,会将nonce的值初始化,则将val赋值为nulltxt,关于nulltxt在ntp_control.c开始的位置进行了定义。
static const charnulltxt[1] = { '' };
赋值之后,在pnonce调用estrdup函数的时候对val存放的值进行了判断,如果为NULL,则直接pnonce,这样就不会再将vul空指针传入estrdup中调用strlen导致空指针引用了。
发表评论
您还未登录,请先登录。
登录