进入tcpdump.c(函数入口)之前,先看一些头文件 netdissect.h里定义了一个数据结构struct netdissect_options来描述tcdpump支持的所有参数动作,每一个参数有对应的flag, 在tcpdump 的main 里面, 会根据用户的传入的参数来增加相应flag数值, 最后根据这些flag数值来实现特定动作。各个参数含义请参考源代码注释。
1.netdissect.h 头文件
struct netdissect_options {
int ndo_bflag; /* 以 ASDOT 表示法打印 4 字节 AS 号 */
int ndo_eflag; /* 打印以太网头 */
int ndo_fflag; /* 不翻译“外部”IP 地址 */
int ndo_Kflag; /* 不检查 IP、TCP 或 UDP 校验和 */
int ndo_nflag; /* 将地址显示为数字形式 */
int ndo_Nflag; /* 打印主机名时去掉域名 */
int ndo_qflag; /* 简短输出 */
int ndo_Sflag; /* 打印原始 TCP 序列号 */
int ndo_tflag; /* 打印数据包到达时间 */
int ndo_uflag; /* 打印未解码的 NFS 句柄 */
int ndo_vflag; /* 冗余级别 */
int ndo_xflag; /* 以十六进制打印数据包 */
int ndo_Xflag; /* 以十六进制/ASCII 打印数据包 */
int ndo_Aflag; /* 仅以 ASCII 打印数据包,并将 TAB、LF、CR 和 SPACE 视为图形字符 */
int ndo_Hflag; /* 解析 802.11s 草案网状标准 */
const char *ndo_protocol; /* 协议 */
jmp_buf ndo_early_end; /* 用于 setjmp()/longjmp() 的 jmp_buf */
void *ndo_last_mem_p; /* 指向最后分配的内存块的指针 */
int ndo_packet_number; /* 在行首打印数据包编号 */
int ndo_print_sampling; /* 打印每 N 个数据包 */
int ndo_suppress_default_print; /* 对未知的数据包类型不使用 default_print() */
int ndo_tstamp_precision; /* 请求的时间戳精度 */
const char *program_name; /* 使用该库的程序名称 */
char *ndo_espsecret; /* ESP 密钥 */
struct sa_list *ndo_sa_list_head; /* 由 print-esp.c 使用 */
struct sa_list *ndo_sa_default;
char *ndo_sigsecret; /* 签名验证密钥 */
int ndo_packettype; /* 由 -T 指定的数据包类型 */
int ndo_snaplen; /* 抓取长度 */
int ndo_ll_hdr_len; /* 链路层头长度 */
/* 全局指针,指向当前数据包的开始和结束(在打印过程中) */
const u_char *ndo_packetp;
const u_char *ndo_snapend;
/* 保存的数据包边界和缓冲区信息的堆栈 */
struct netdissect_saved_packet_info *ndo_packet_info_stack;
/* 指向 if_printer 函数的指针 */
if_printer ndo_if_printer;
/* 指向输出内容的 void 函数的指针 */
void (*ndo_default_print)(netdissect_options *,
const u_char *bp, u_int length);
/* 指向执行常规输出的函数的指针 */
int (*ndo_printf)(netdissect_options *,
const char *fmt, ...)
PRINTFLIKE_FUNCPTR(2, 3);
/* 指向输出错误的函数的指针 */
void NORETURN_FUNCPTR (*ndo_error)(netdissect_options *,
status_exit_codes_t status,
const char *fmt, ...)
PRINTFLIKE_FUNCPTR(3, 4);
/* 指向输出警告的函数的指针 */
void (*ndo_warning)(netdissect_options *,
const char *fmt, ...)
PRINTFLIKE_FUNCPTR(2, 3);
};
字段解释:
-
标志位:
-
ndo_bflag
: 决定是否以 ASDOT 表示法打印 4 字节 AS 号。 -
ndo_eflag
: 决定是否打印以太网头。 -
ndo_fflag
: 决定是否不翻译“外部”IP 地址。 -
ndo_Kflag
: 决定是否不检查 IP、TCP 或 UDP 校验和。 -
ndo_nflag
: 决定是否将地址保留为数字形式。 -
ndo_Nflag
: 决定是否在打印主机名时去掉域名。 -
ndo_qflag
: 决定是否进行简短输出。 -
ndo_Sflag
: 决定是否打印原始 TCP 序列号。 -
ndo_tflag
: 决定是否打印数据包到达时间。 -
ndo_uflag
: 决定是否打印未解码的 NFS 句柄。 -
ndo_vflag
: 设置冗余级别。 -
ndo_xflag
: 决定是否以十六进制打印数据包。 -
ndo_Xflag
: 决定是否以十六进制和 ASCII 打印数据包。 -
ndo_Aflag
: 决定是否仅以 ASCII 打印数据包,并将 TAB、LF、CR 和 SPACE 视为图形字符。 -
ndo_Hflag
: 决定是否解析 802.11s 草案网状标准。
-
-
其他选项:
-
ndo_protocol
: 指定的协议。 ndo_early_end
: 用于 setjmp()/longjmp() 的跳转缓冲区。-
ndo_last_mem_p
: 指向最后分配的内存块的指针。 -
ndo_packet_number
: 决定是否在行首打印数据包编号。 -
ndo_print_sampling
: 打印每 N 个数据包。 -
ndo_suppress_default_print
: 决定是否对未知的数据包类型不使用 default_print()。 -
ndo_tstamp_precision
: 请求的时间戳精度。 -
program_name
: 使用该库的程序名称。
-
-
ESP 和签名相关:
-
ndo_espsecret
: ESP 密钥。 -
ndo_sa_list_head
,ndo_sa_default
: 由 print-esp.c 使用的 SA 列表。 -
ndo_sigsecret
: 签名验证密钥。
-
-
数据包和链接层相关:
-
ndo_packettype
: 由 -T 指定的数据包类型。 ndo_snaplen
: 抓取长度。-
ndo_ll_hdr_len
: 链路层头长度。 -
ndo_packetp
,ndo_snapend
: 当前数据包的开始和结束指针。 -
ndo_packet_info_stack
: 保存的数据包边界和缓冲区信息的堆栈。
-
-
函数指针:
-
ndo_if_printer
: 指向 if_printer 函数的指针。 -
ndo_default_print
: 指向输出内容的 void 函数的指针。 -
ndo_printf
: 指向执行常规输出的函数的指针。 -
ndo_error
: 指向输出错误的函数的指针。 -
ndo_warning
: 指向输出警告的函数的指针。
-
此次需要关注的是 “ndo_snaplen
: 抓取长度”变量。
2.从指针安全读取x字节的相关宏定义
在 print-eigrp.c 中有如下代码:
GET_BE_U_2(eigrp_com_header->checksum),
继续追踪到 extract.h 文件:
#define GET_BE_U_2(p) get_be_u_2(ndo, (const u_char *)(p))
继续追踪,仍在 extract.h 文件中:get_be_u_2 可见是一个内联函数
static inline uint16_t
get_be_u_2(netdissect_options *ndo, const u_char *p)
{
if (!ND_TTEST_2(p))
nd_trunc_longjmp(ndo);
return EXTRACT_BE_U_2(p);
}
这个内联函数 get_be_u_2
做了以下几件事:
- 调用
ND_TTEST_2(p)
来检查是否可以从指针p
安全地读取 2 个字节。 - 如果检查失败,调用
nd_trunc_longjmp(ndo)
进行错误处理,通常会跳转到某个提前定义的错误处理位置。 - 如果检查成功,调用
EXTRACT_BE_U_2(p)
从指针p
提取 2 个字节的数据,并将其从网络字节顺序转换为主机字节顺序。
继续追踪 ND_TTEST_2 宏函数,仍在 extract.h 文件中:
#define ND_TTEST_2(p) ND_TTEST_LEN((p), 2)
//这个宏调用 ND_TTEST_LEN,传递参数 p 和长度 2,表示要检查从指针 p 开始的 2 个字节。
继续追踪 ND_TTEST_LEN 宏函数,跳转到 netdissect.h 文件中:
/*
* True if "l" bytes from "p" were captured.
*
* The "ndo->ndo_snapend - (l) <= ndo->ndo_snapend" checks to make sure
* "l" isn't so large that "ndo->ndo_snapend - (l)" underflows.
*
* The check is for <= rather than < because "l" might be 0.
*
* We cast the pointers to uintptr_t to make sure that the compiler
* doesn't optimize away any of these tests (which it is allowed to
* do, as adding an integer to, or subtracting an integer from, a
* pointer assumes that the pointer is a pointer to an element of an
* array and that the result of the addition or subtraction yields a
* pointer to another member of the array, so that, for example, if
* you subtract a positive integer from a pointer, the result is
* guaranteed to be less than the original pointer value). See
*
* https://www.kb.cert.org/vuls/id/162289
*/
/*
* Test in two parts to avoid these warnings:
* comparison of unsigned expression >= 0 is always true [-Wtype-limits],
* comparison is always true due to limited range of data type [-Wtype-limits].
*/
/*
* 如果从 "p" 开始的 "l" 字节被捕获,则返回真。
*
* 通过 "ndo->ndo_snapend - (l) <= ndo->ndo_snapend" 的检查来确保
* "l" 不会大到使 "ndo->ndo_snapend - (l)" 发生下溢。
*
* 检查使用 <= 而不是 < 是因为 "l" 可能为 0。
*
* 我们将指针转换为 uintptr_t 是为了确保编译器不会优化掉这些测试
* (编译器被允许这样做,因为将整数加到指针上,或从指针上减去整数,
* 假定指针是一个数组元素的指针,并且加法或减法的结果会生成另一个数组成员的指针,
* 因此,例如,如果从指针上减去一个正整数,结果保证小于原始指针值)。参见
*
* https://www.kb.cert.org/vuls/id/162289
*/
/*
* 分两部分测试以避免这些警告:
* unsigned 表达式的比较总是大于等于 0 是始终为真的 [-Wtype-limits],
* 由于数据类型的有限范围,比较总是为真 [-Wtype-limits]。
*/
#define IS_NOT_NEGATIVE(x) (((x) > 0) || ((x) == 0))
#define ND_TTEST_LEN(p, l) \
(IS_NOT_NEGATIVE(l) && \
((uintptr_t)ndo->ndo_snapend - (l) <= (uintptr_t)ndo->ndo_snapend && \
(uintptr_t)(p) <= (uintptr_t)ndo->ndo_snapend - (l)))
-
这个宏检查以下几个条件:
-
IS_NOT_NEGATIVE(l)
:长度l
必须是非负数。 -
((uintptr_t)ndo->ndo_snapend - (l) <= (uintptr_t)ndo->ndo_snapend)
:确保ndo->ndo_snapend
减去l
不会发生下溢。 -
((uintptr_t)(p) <= (uintptr_t)ndo->ndo_snapend - (l))
:确保指针p
不会越过数据包的边界。
-
ND_TTEST_2
是一个宏,用于检查是否可以安全地从指定的指针 p
读取 2 个字节(即 16 位)。它通过验证内存边界和指针合法性来确保不会发生越界访问。具体来说,它结合了多个宏和函数,形成一个完整的边界检查机制。
总结
ND_TTEST_2
的主要目的是确保在从数据包读取数据时不会发生越界访问,从而保证程序的安全性和稳定性。它通过一系列的检查来验证指针和长度的合法性,防止因非法内存访问导致的崩溃或数据损坏。
3.setjmp/longjmp 非局部跳转函数的应用
现在存在的问题是没有搞清tcpdump 的执行流程:
调用顺序:
(gdb) bt
#0 eigrp_print (ndo=0x7fffffffcf00, pptr=0x8ea512 "\002\005\356h", len=40) at ./print-eigrp.c:233
#1 0x0000000000458255 in ip_demux_print (ndo=0x7fffffffcf00, bp=0x8ea512 "\002\005\356h", length=40, ver=4, fragmented=0, ttl_hl=2, nh=88 'X',
iph=0x8ea4fe "E\300") at ./print-ip-demux.c:143
#2 0x00000000004227cf in ip_print (ndo=0x7fffffffcf00, bp=0x8ea4fe "E\300", length=60) at ./print-ip.c:487
#3 0x000000000041dbbc in ethertype_print (ndo=0x7fffffffcf00, ether_type=2048, p=0x8ea4fe "E\300", length=60, caplen=60, src=0x7fffffffccb0,
dst=0x7fffffffcca0) at ./print-ether.c:536
#4 0x000000000041d680 in ether_common_print (ndo=0x7fffffffcf00, p=0x8ea4fe "E\300", length=60, caplen=60, print_switch_tag=0x0, switch_tag_len=0,
print_encap_header=0x0, encap_header_arg=0x0) at ./print-ether.c:391
#5 0x000000000041d7c1 in ether_print (ndo=0x7fffffffcf00, p=0x8ea4f0 "\001", length=74, caplen=74, print_encap_header=0x0, encap_header_arg=0x0)
at ./print-ether.c:448
#6 0x000000000041d81a in ether_if_print (ndo=0x7fffffffcf00, h=0x7fffffffce40, p=0x8ea4f0 "\001") at ./print-ether.c:464
#7 0x0000000000407ead in pretty_print_packet (ndo=0x7fffffffcf00, h=0x7fffffffce40, sp=0x8ea4f0 "\001", packets_captured=1) at ./print.c:414
#8 0x0000000000407343 in print_packet (user=0x7fffffffcf00 "", h=0x7fffffffce40, sp=0x8ea4f0 "\001") at ./tcpdump.c:3127
#9 0x00000000004f4f07 in pcap_offline_read (p=p@entry=0x8ea250, cnt=2147483647, cnt@entry=-1, callback=callback@entry=0x4072ed <print_packet>,
user=user@entry=0x7fffffffcf00 "") at ./savefile.c:691
#10 0x00000000004e2baf in pcap_loop (p=0x8ea250, cnt=-1, callback=0x4072ed <print_packet>, user=0x7fffffffcf00 "") at ./pcap.c:2916
#11 0x00000000004066c2 in main (argc=3, argv=0x7fffffffe3c8) at ./tcpdump.c:2569
setjmp() 函数是在 #7 的 pretty_print_packet() 函数中调用的。
参考:
1、非局部跳转:8.6 非本地跳转 | 深入理解计算机系统(CSAPP) (gitbook.io)
2、 【C指针(五)】6种转移表实现整合longjmp()/setjmp()函数和qsort函数详解分析&&模拟实现-支付宝开发者社区 (alipay.com)
3、tcpdump 源码解析:https://www.cnblogs.com/pangblog/p/3364690.html
分析2:tcpdump源码分析-****博客
4、tcpdump 越界访问追踪: tcpdump 4.5.1 crash 深入分析-安全客 - 安全资讯平台