烦恼一般都是想太多了。

0%

tcpdump源代码阅读记录

LibPcap 和 Tcpdump 是一个很好用的抓包工具,不过,做得是比较复杂。当我们需要在一些特性场景的项目上进行使用的话,直接使用 LibPcap 又未免太过低级了些,而直接使用 TcpDump 的话又太重了一些。因此,当我只需在 以太网上捕捉 IP 报文进行分析的话,就有必要对 Tcpdump 进行一些裁剪,而在此之前,了解一下 TcpDump 的实现,就非常的有必要到了。

基础

Tcpdump 是使用 libpacap 来进行捕获报文的,具体如何,且不必谈,但不外乎是用到了下面几个API:

  • pcap_lookupdev,pcap_findalldevs 来查找设备,当然,前一个已经被标记为弃用了。
  • pcap_open_live 打开一个设备
  • pcap_loop 开始进行循环捕捉,然后在回调中进行处理了。

Tcpdump

Tcpdump 就是这样做的,不过他提供了很多的选项来让我们设置以进行不同的捕捉和分析选项。具体而言,对于捕获到的报文是如何处理,如何打印,存储到文件等,都提供了选项,我们只关注一些比较核心的问题。

  • 当我们提供了 -w 选项,将捕捉的报文存储到文件时,指定了 -c count-G seconds 时,会使用 dump_packet_and_trunc 回调;否则使用 dump_packet 回调。
  • 未指定 -w 则会使用 print_packet 回调。

Tcpdump 有一个 netdissect_options 结构,用来指定我们需要进行分析的报文标志,其中还保留了一些函数指针。当确定回调的时候,就会根据我们打开的设备类型来查找对应的 if_printer,可以叫它接口打印器:

// tcpdump.c

if (WFileName) {
/* Do not exceed the default PATH_MAX for files. */
dumpinfo.CurrentFileName = (char *)malloc(PATH_MAX + 1);

if (dumpinfo.CurrentFileName == NULL)
error("malloc of dumpinfo.CurrentFileName");

/* We do not need numbering for dumpfiles if Cflag isn't set. */
if (Cflag != 0)
MakeFilename(dumpinfo.CurrentFileName, WFileName, 0, WflagChars);
else
MakeFilename(dumpinfo.CurrentFileName, WFileName, 0, 0);

pdd = pcap_dump_open(pd, dumpinfo.CurrentFileName);
#ifdef HAVE_LIBCAP_NG
/* Give up CAP_DAC_OVERRIDE capability.
* Only allow it to be restored if the -C or -G flag have been
* set since we may need to create more files later on.
*/
capng_update(
CAPNG_DROP,
(Cflag || Gflag ? 0 : CAPNG_PERMITTED)
| CAPNG_EFFECTIVE,
CAP_DAC_OVERRIDE
);
capng_apply(CAPNG_SELECT_BOTH);
#endif /* HAVE_LIBCAP_NG */
if (pdd == NULL)
error("%s", pcap_geterr(pd));
#ifdef HAVE_CAPSICUM
set_dumper_capsicum_rights(pdd);
#endif
if (Cflag != 0 || Gflag != 0) {
#ifdef HAVE_CAPSICUM
dumpinfo.WFileName = strdup(basename(WFileName));
if (dumpinfo.WFileName == NULL) {
error("Unable to allocate memory for file %s",
WFileName);
}
dumpinfo.dirfd = open(dirname(WFileName),
O_DIRECTORY | O_RDONLY);
if (dumpinfo.dirfd < 0) {
error("unable to open directory %s",
dirname(WFileName));
}
cap_rights_init(&rights, CAP_CREATE, CAP_FCNTL,
CAP_FTRUNCATE, CAP_LOOKUP, CAP_SEEK, CAP_WRITE);
if (cap_rights_limit(dumpinfo.dirfd, &rights) < 0 &&
errno != ENOSYS) {
error("unable to limit directory rights");
}
if (cap_fcntls_limit(dumpinfo.dirfd, CAP_FCNTL_GETFL) < 0 &&
errno != ENOSYS) {
error("unable to limit dump descriptor fcntls");
}
#else /* !HAVE_CAPSICUM */
dumpinfo.WFileName = WFileName;
#endif
callback = dump_packet_and_trunc;
dumpinfo.pd = pd;
dumpinfo.pdd = pdd;
pcap_userdata = (u_char *)&dumpinfo;
} else {
callback = dump_packet;
dumpinfo.WFileName = WFileName;
dumpinfo.pd = pd;
dumpinfo.pdd = pdd;
pcap_userdata = (u_char *)&dumpinfo;
}
if (print) {
dlt = pcap_datalink(pd);
ndo->ndo_if_printer = get_if_printer(dlt);
dumpinfo.ndo = ndo;
} else
dumpinfo.ndo = NULL;

#ifdef HAVE_PCAP_DUMP_FLUSH
if (Uflag)
pcap_dump_flush(pdd);
#endif
} else {
dlt = pcap_datalink(pd);
ndo->ndo_if_printer = get_if_printer(dlt);
callback = print_packet;
pcap_userdata = (u_char *)ndo;
}

我们可以用 pcap_datalink 来获取到链路层的类型,然后用 pcap_datalink_val_to_name 来获取链路层的名称化显示,如以太网的话会显示EN10MB

print.c 文件中,定义了所有的链路层类型的打印函数 printers,如我们的的 DLT_EN10MB 就定义为:

{ ether_if_print,	DLT_EN10MB },

我们在回调的 print_packet 中就会调用这个函数来进行打印信息。

ehter_if_print

// print-ehtner.c
void
ether_if_print(netdissect_options *ndo, const struct pcap_pkthdr *h,
const u_char *p)
{
ndo->ndo_protocol = "ether";
ndo->ndo_ll_hdr_len +=
ether_print(ndo, p, h->len, h->caplen, NULL, NULL);
}

u_int
ether_print(netdissect_options *ndo,
const u_char *p, u_int length, u_int caplen,
void (*print_encap_header)(netdissect_options *ndo, const u_char *),
const u_char *encap_header_arg)
{
ndo->ndo_protocol = "ether";
return ether_common_print(ndo, p, length, caplen, NULL, 0,
print_encap_header, encap_header_arg);
}

ehter_common_print 函数的定义在 这里,太长,我就不贴了。

具体而言其工作过程如下:

  1. ether_addresses_print 来打印出链路层地址(src/dst)

  2. ether_type_print 来打印出链路层承载的协议信息。如我们的 IP 报文,就会调用到 print-ip.c 中的 ip_print

    int
    ethertype_print(netdissect_options *ndo,
    u_short ether_type, const u_char *p,
    u_int length, u_int caplen,
    const struct lladdr_info *src, const struct lladdr_info *dst)
    {
    switch (ether_type) {

    case ETHERTYPE_IP:
    ip_print(ndo, p, length);
    return (1);

ip_print 中,就会分析 IP 层承载的协议信息了

ip_print

在这里面打印了 IP 层相关的信息后,就会打印其上承载的协议信息,这就调用到了 ip_demux_print 函数:

if ((off & IP_OFFMASK) == 0) {
uint8_t nh = GET_U_1(ip->ip_p);

if (nh != IPPROTO_TCP && nh != IPPROTO_UDP &&
nh != IPPROTO_SCTP && nh != IPPROTO_DCCP) {
ND_PRINT("%s > %s: ",
GET_IPADDR_STRING(ip->ip_src),
GET_IPADDR_STRING(ip->ip_dst));
}
ip_demux_print(ndo, (const u_char *)ip + hlen, len, 4,
off & IP_MF, GET_U_1(ip->ip_ttl), nh, bp);

ip_demux_print

这是一个 IPv4/IPv6 的载荷打印器

代码不贴了。很容易理解。

根据承载协议类型进行打印。如果是TCP 则调用 tcp_print ,UDP 则调用 udp_print 等等。

需要注意的是,在 tcp_print内,无法判断更多的协议类型了,而是使用对应的端口来判断调用什么打印器:

if (IS_SRC_OR_DST_PORT(TELNET_PORT)) {
telnet_print(ndo, bp, length);
} else if (IS_SRC_OR_DST_PORT(SMTP_PORT)) {
ND_PRINT(": ");
smtp_print(ndo, bp, length);
} else if (IS_SRC_OR_DST_PORT(WHOIS_PORT)) {
ND_PRINT(": ");
ndo->ndo_protocol = "whois"; /* needed by txtproto_print() */
txtproto_print(ndo, bp, length, NULL, 0); /* RFC 3912 */
} else if (IS_SRC_OR_DST_PORT(BGP_PORT))
bgp_print(ndo, bp, length);
else if (IS_SRC_OR_DST_PORT(PPTP_PORT))
pptp_print(ndo, bp);
else if (IS_SRC_OR_DST_PORT(REDIS_PORT))
resp_print(ndo, bp, length);
else if (IS_SRC_OR_DST_PORT(SSH_PORT))
ssh_print(ndo, bp, length);

OK,基本上就可以到此为止了。