感恩节那天,meh在Bugzilla上提交了一个exim的uaf漏洞:https://bugs.exim.org/show_bug.cgi?id=2199 ,这周我对该漏洞进行应急复现,却发现,貌似利用meh提供的PoC并不能成功利用UAF漏洞造成crash
漏洞复现 首先进行漏洞复现
环境搭建 复现环境:ubuntu 16.04 server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ git clone https://github.com/Exim/exim.git $ git checkout ef9da2ee969c27824fcd5aed6a59ac4cd217587b $ apt install libdb-dev libpcre3-dev $ cd src $ mkdir Local $ cd Local $ wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile $ cd .. $ make && make install
然后再修改下配置文件/etc/exim/configure
文件的第364行,把accept hosts = :
修改成 accept hosts = *
PoC测试 从https://bugs.exim.org/attachment.cgi?id=1050 获取到meh的debug信息,得知启动参数:
1 $ /usr/exim/bin/exim -bdf -d+all
PoC有两个:
https://bugs.exim.org/attachment.cgi?id=1049
https://bugs.exim.org/attachment.cgi?id=1052
需要先安装下pwntools,直接用pip装就好了,两个PoC的区别其实就是padding的长度不同而已
然后就使用PoC进行测试,发现几个问题:
我的debug信息在最后一部分和meh提供的不一样
虽然触发了crash,但是并不是UAF导致的crash
debug信息不同点比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 # 我的debug信息 12:15:09 8215 SMTP>> 500 unrecognized command 12:15:09 8215 SMTP<< BDAT 1 12:15:09 8215 chunking state 1, 1 bytes 12:15:09 8215 search_tidyup called 12:15:09 8215 SMTP>> 250 1 byte chunk received 12:15:09 8215 chunking state 0 12:15:09 8215 SMTP<< BDAT 12:15:09 8215 LOG: smtp_protocol_error MAIN 12:15:09 8215 SMTP protocol error in "BDAT \177" H=(test) [10.0.6.18] missing size for BDAT command 12:15:09 8215 SMTP>> 501 missing size for BDAT command 12:15:09 8215 host in ignore_fromline_hosts? no (option unset) 12:15:09 8215 >>Headers received: 12:15:09 8215 : ...一堆不可显字符 **** debug string too long - truncated **** 12:15:09 8215 12:15:09 8215 search_tidyup called 12:15:09 8215 >>Headers after rewriting and local additions: 12:15:09 8215 : ......一堆不可显字符 **** debug string too long - truncated **** 12:15:09 8215 12:15:09 8215 Data file name: /var/spool/exim//input//1eKcjF-00028V-5Y-D 12:15:29 8215 LOG: MAIN 12:15:29 8215 SMTP connection from (test) [10.0.6.18] lost while reading message data 12:15:29 8215 SMTP>> 421 Lost incoming connection 12:15:29 8215 LOG: MAIN PANIC DIE 12:15:29 8215 internal error: store_reset(0x2443048) failed: pool=0 smtp_in.c 841 12:15:29 8215 SMTP>> 421 Unexpected failure, please try later 12:15:29 8215 LOG: MAIN PANIC DIE 12:15:29 8215 internal error: store_reset(0x2443068) failed: pool=0 smtp_in.c 841 12:15:29 8215 SMTP>> 421 Unexpected failure, please try later 12:15:29 8215 LOG: MAIN PANIC DIE 12:15:29 8215 internal error: store_reset(0x2443098) failed: pool=0 smtp_in.c 841 12:15:29 8215 SMTP>> 421 Unexpected failure, please try later 12:15:29 8215 LOG: MAIN PANIC DIE 12:15:29 8215 internal error: store_reset(0x24430c8) failed: pool=0 smtp_in.c 841 12:15:29 8215 SMTP>> 421 Unexpected failure, please try later 12:15:29 8215 LOG: MAIN PANIC DIE 12:15:29 8215 internal error: store_reset(0x24430f8) failed: pool=0 smtp_in.c 841 12:15:29 8215 SMTP>> 421 Unexpected failure, please try later 12:15:29 8215 LOG: MAIN PANIC DIE 12:15:29 8215 internal error: store_reset(0x2443128) failed: pool=0 smtp_in.c 841 12:15:29 8215 SMTP>> 421 Unexpected failure, please try later 12:15:29 8215 LOG: MAIN PANIC DIE 12:15:29 8215 internal error: store_reset(0x2443158) failed: pool=0 smtp_in.c 841 12:15:29 8215 SMTP>> 421 Unexpected failure, please try later 12:15:29 8215 LOG: MAIN PANIC DIE 12:15:29 8215 internal error: store_reset(0x2443188) failed: pool=0 smtp_in.c 841 12:16:20 8213 child 8215 ended: status=0x8b 12:16:20 8213 signal exit, signal 11 (core dumped) 12:16:20 8213 0 SMTP accept processes now running 12:16:20 8213 Listening... -------------------------------------------- # meh的debug信息 10:31:59 21724 SMTP>> 500 unrecognized command 10:31:59 21724 SMTP<< BDAT 1 10:31:59 21724 chunking state 1, 1 bytes 10:31:59 21724 search_tidyup called 10:31:59 21724 SMTP>> 250 1 byte chunk received 10:31:59 21724 chunking state 0 10:31:59 21724 SMTP<< BDAT 10:31:59 21724 LOG: smtp_protocol_error MAIN 10:31:59 21724 SMTP protocol error in "BDAT \177" H=(test) [127.0.0.1] missing size for BDAT command 10:31:59 21724 SMTP>> 501 missing size for BDAT command 10:31:59 21719 child 21724 ended: status=0x8b 10:31:59 21719 signal exit, signal 11 (core dumped) 10:31:59 21719 0 SMTP accept processes now running 10:31:59 21719 Listening...
发现的确是抛异常了,但是跟meh的debug信息在最后却不一样,然后使用gdb进行调试,发现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 RAX 0xfbad240c *RBX 0x30 *RCX 0xffffffffffffffd4 RDX 0x2000 *RDI 0x2b *RSI 0x4b7e8e ◂— jae 0x4b7f04 /* 'string.c' */ *R8 0x0 *R9 0x24 *R10 0x24 *R11 0x4a69e8 ◂— push rbp *R12 0x4b7e8e ◂— jae 0x4b7f04 /* 'string.c' */ *R13 0x1a9 *R14 0x24431b8 ◂— 0x0 *R15 0x5e *RBP 0x2000 *RSP 0x7ffd75b862c0 —▸ 0x7ffd75b862d0 ◂— 0xffffffffffffffff *RIP 0x46cf1b (store_get_3+117) ◂— cmp qword ptr [rax + 8], rdx -------------- > 0x46cf1b <store_get_3+117> cmp qword ptr [rax + 8], rdx ------------ Program received signal SIGSEGV (fault address 0xfbad2414)
根本就不是meh描述的利用UAF造成的crash,继续研究,发现如果把debug all的选项-d+all
换成只显示简单的debug信息的选项-dd
,则就不会抛异常了
1 2 3 4 5 6 7 8 $ sudo ./build-Linux-x86_64/exim -bdf -dd ...... 8266 Listening... 8268 Process 8268 is handling incoming connection from [10.0.6.18] 8266 child 8268 ended: status=0x0 8266 normal exit, 0 8266 0 SMTP accept processes now running 8266 Listening...
又仔细读了一遍meh在Bugzilla上的描述,看到这句,所以猜测有没有可能是因为padding大小的原因,才导致crash失败的?所以写了代码对padding进行爆破,长度从0-0x4000,爆破了一遍,并没有发现能成功造成crash的长度。
This PoC is affected by the block layout(yield_length), so this line: r.sendline('a'*0x1250+'\x7f')
should be adjusted according to the program state.
所以可以排除是因为padding长度的原因导致PoC测试失败。
而且在漏洞描述页,我还发现Exim的作者也尝试对漏洞进行测试,不过同样测试失败了,还贴出了他的debug信息,和他的debug信息进行对比,和我的信息几乎一样。(并不知道exim的作者在得到meh的Makefile和log后有没有测试成功)。
所以,本来一次简单的漏洞应急,变为了对该漏洞的深入研究
浅入研究 UAF全称是use after free,所以我在free之前,patch了一个printf:
1 2 3 4 5 6 7 8 9 10 # src/store.c ...... 448 void 449 store_release_3(void *block, const char *filename, int linenumber) 450 { ...... 481 printf("--------free: %8p-------\n", (void *)bb); 482 free(bb); 483 return; 484 }
重新编译跑一遍,发现竟然成功触发了uaf漏洞:
1 2 3 4 5 6 7 8 $ /usr/exim/bin/exim -bdf -dd 8334 Listening... 8336 Process 8336 is handling incoming connection from [10.0.6.18] --------free: 0x1e2c1b0------- 8334 child 8336 ended: status=0x8b 8334 signal exit, signal 11 (core dumped) 8334 0 SMTP accept processes now running 8334 Listening...
然后gdb调试的信息也证明成功利用uaf漏洞造成了crash:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 *RAX 0xdeadbeef *RBX 0x1e2e5d0 ◂— 0x0 *RCX 0x1e29341 ◂— 0xadbeef000000000a /* '\n' */ *RDX 0x7df *RDI 0x1e2e5d0 ◂— 0x0 *RSI 0x46cedd (store_free_3+70) ◂— pop rbx *R8 0x0 R9 0x7f054f32b700 ◂— 0x7f054f32b700 *R10 0xffff80fab41c4748 *R11 0x203 *R12 0x7f054dc69993 (state+3) ◂— 0x0 *R13 0x4ad5b6 ◂— jb 0x4ad61d /* 'receive.c' */ *R14 0x7df *R15 0x1e1d8f0 ◂— 0x0 *RBP 0x0 *RSP 0x7ffe169262b8 —▸ 0x7f054d9275e7 (free+247) ◂— add rsp, 0x28 *RIP 0xdeadbeef ------------------------------------------ Invalid address 0xdeadbeef
PS: 这里说明下./build-Linux-x86_64/exim
这个binary是没有patch printf的代码,/usr/exim/bin/exim
是patch了printf的binary
到这里就很奇怪了,加了个printf就能成功触发漏洞,删了就不能,之后用puts
和write
代替了printf
进行测试,发现puts
也能成功触发漏洞,但是write
不能。大概能猜到应该是stdio的缓冲区机制的问题,然后继续深入研究。
深入研究 来看看meh在Bugzilla上对于该漏洞的所有描述:
Hi, we found a use-after-free vulnerability which is exploitable to RCE in the SMTP server.
According to receive.c:1783, 1783 if (!store_extend(next->text, oldsize, header_size)) 1784 { 1785 uschar *newtext = store_get(header_size); 1786 memcpy(newtext, next->text, ptr); 1787 store_release(next->text); 1788 next->text = newtext; 1789 }
when the buffer used to parse header is not big enough, exim tries to extend the next->text with store_extend function. If there is any other allocation between the allocation and extension of this buffer, store_extend fails. store.c 276 if ((char *)ptr + rounded_oldsize != (char *)(next_yield[store_pool]) || 277 inc > yield_length[store_pool] + rounded_oldsize - oldsize) 278 return FALSE;
Then exim calls store_get, and store_get cut the current_block directly. store.c 208 next_yield[store_pool] = (void *)((char *)next_yield[store_pool] + size); 209 yield_length[store_pool] -= size; 210 211 return store_last_get[store_pool];
In receive.c, exim used receive_getc to get message. 1831 ch = (receive_getc)(GETC_BUFFER_UNLIMITED); When exim is handling BDAT command, receive_getc is bdat_getc. In bdat_getc, after the length of BDAT is reached, bdat_getc tries to read the next command. smtp_in.c 536 next_cmd: 537 switch(smtp_read_command(TRUE, 1)) 538 { 539 default: 540 (void) synprot_error(L_smtp_protocol_error, 503, NULL, 541 US”only BDAT permissible after non-LAST BDAT”);
synprot_error may call store_get if any non-printable character exists because synprot_error uses string_printing.
string.c 304 /* Get a new block of store guaranteed big enough to hold the 305 expanded string. */ 306 307 ss = store_get(length + nonprintcount * 3 + 1); receive_getc becomes bdat_getc when handling BDAT data. Oh, I was talking about the source code of 4.89. In the current master, it is here:https://github.com/Exim/exim/blob/master/src/src/receive.c#L1790
What this PoC does is:
send unrecognized command to adjust yield_length and make it less than 0x100
send BDAT 1
send one character to reach the length of BDAT
send an BDAT command without size and with non-printable character -> trigger synprot_error and therefore call store_get // back to receive_msg and exim keeps trying to read header
send a huge message until store_extend called
uaf
This PoC is affected by the block layout(yield_length), so this line: r.sendline('a'*0x1250+'\x7f')
should be adjusted according to the program state. I tested on my ubuntu 16.04, compiled with the attached Local/Makefile (simply make -j8). I also attach the updated PoC for current master and the debug report.
在这里先提一下,在Exim中,自己封装实现了一套简单的堆管理,在src/store.c中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 void * store_get_3(int size, const char *filename, int linenumber) { /* Round up the size to a multiple of the alignment. Although this looks a messy statement, because "alignment" is a constant expression, the compiler can do a reasonable job of optimizing, especially if the value of "alignment" is a power of two. I checked this with -O2, and gcc did very well, compiling it to 4 instructions on a Sparc (alignment = 8). */ if (size % alignment != 0) size += alignment - (size % alignment); /* If there isn't room in the current block, get a new one. The minimum size is STORE_BLOCK_SIZE, and we would expect this to be the norm, since these functions are mostly called for small amounts of store. */ if (size > yield_length[store_pool]) { int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size; int mlength = length + ALIGNED_SIZEOF_STOREBLOCK; storeblock * newblock = NULL; /* Sometimes store_reset() may leave a block for us; check if we can use it */ if ( (newblock = current_block[store_pool]) && (newblock = newblock->next) && newblock->length < length ) { /* Give up on this block, because it's too small */ store_free(newblock); newblock = NULL; } /* If there was no free block, get a new one */ if (!newblock) { pool_malloc += mlength; /* Used in pools */ nonpool_malloc -= mlength; /* Exclude from overall total */ newblock = store_malloc(mlength); newblock->next = NULL; newblock->length = length; if (!chainbase[store_pool]) chainbase[store_pool] = newblock; else current_block[store_pool]->next = newblock; } current_block[store_pool] = newblock; yield_length[store_pool] = newblock->length; next_yield[store_pool] = (void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK); (void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]); } /* There's (now) enough room in the current block; the yield is the next pointer. */ store_last_get[store_pool] = next_yield[store_pool]; /* Cut out the debugging stuff for utilities, but stop picky compilers from giving warnings. */ #ifdef COMPILE_UTILITY filename = filename; linenumber = linenumber; #else DEBUG(D_memory) { if (running_in_test_harness) debug_printf("---%d Get %5d\n", store_pool, size); else debug_printf("---%d Get %6p %5d %-14s %4d\n", store_pool, store_last_get[store_pool], size, filename, linenumber); } #endif /* COMPILE_UTILITY */ (void) VALGRIND_MAKE_MEM_UNDEFINED(store_last_get[store_pool], size); /* Update next pointer and number of bytes left in the current block. */ next_yield[store_pool] = (void *)(CS next_yield[store_pool] + size); yield_length[store_pool] -= size; return store_last_get[store_pool]; } BOOL store_extend_3(void *ptr, int oldsize, int newsize, const char *filename, int linenumber) { int inc = newsize - oldsize; int rounded_oldsize = oldsize; if (rounded_oldsize % alignment != 0) rounded_oldsize += alignment - (rounded_oldsize % alignment); if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) || inc > yield_length[store_pool] + rounded_oldsize - oldsize) return FALSE; /* Cut out the debugging stuff for utilities, but stop picky compilers from giving warnings. */ #ifdef COMPILE_UTILITY filename = filename; linenumber = linenumber; #else DEBUG(D_memory) { if (running_in_test_harness) debug_printf("---%d Ext %5d\n", store_pool, newsize); else debug_printf("---%d Ext %6p %5d %-14s %4d\n", store_pool, ptr, newsize, filename, linenumber); } #endif /* COMPILE_UTILITY */ if (newsize % alignment != 0) newsize += alignment - (newsize % alignment); next_yield[store_pool] = CS ptr + newsize; yield_length[store_pool] -= newsize - rounded_oldsize; (void) VALGRIND_MAKE_MEM_UNDEFINED(ptr + oldsize, inc); return TRUE; } void store_release_3(void *block, const char *filename, int linenumber) { storeblock *b; /* It will never be the first block, so no need to check that. */ for (b = chainbase[store_pool]; b != NULL; b = b->next) { storeblock *bb = b->next; if (bb != NULL && CS block == CS bb + ALIGNED_SIZEOF_STOREBLOCK) { b->next = bb->next; pool_malloc -= bb->length + ALIGNED_SIZEOF_STOREBLOCK; /* Cut out the debugging stuff for utilities, but stop picky compilers from giving warnings. */ #ifdef COMPILE_UTILITY filename = filename; linenumber = linenumber; #else DEBUG(D_memory) { if (running_in_test_harness) debug_printf("-Release %d\n", pool_malloc); else debug_printf("-Release %6p %-20s %4d %d\n", (void *)bb, filename, linenumber, pool_malloc); } if (running_in_test_harness) memset(bb, 0xF0, bb->length+ALIGNED_SIZEOF_STOREBLOCK); #endif /* COMPILE_UTILITY */ free(bb); return; } } }
UAF漏洞所涉及的关键函数:
store_get_3 堆分配
store_extend_3 堆扩展
store_release_3 堆释放
还有4个重要的全局变量:
chainbase
next_yield
current_block
yield_length
第一步 发送一堆未知的命令去调整yield_length
的值,使其小于0x100。
yield_length
表示的是堆还剩余的长度,每次命令的处理使用的是src/receive.c 代码中的receive_msg
函数
在该函数处理用户输入的命令时,使用next->text
来储存用户输入,在1709行进行的初始化:
1 2 3 1625 int header_size = 256; ...... 1709 next->text = store_get(header_size);
在执行1709行代码的时候,如果0x100 > yield_length
则会执行到newblock = store_malloc(mlength);
,使用glibc的malloc申请一块内存,为了便于之后的描述,这块内存我们称为heap1。
根据store_get_3
中的代码,这个时候:
current_block->next = heap1 (因为之前current_block==chainbase,所以这相当于是chainbase->next = heap1)
current_block = heap1
yield_length = 0x2000
next_yield = heap1+0x10
return next_yield
next_yield = next_yield+0x100 = heap1+0x110
yield_length = yield_length - 0x100 = 0x1f00
第二步 发送BDAT 1
,进入receive_msg
函数,并且让receive_getc
变为bdat_getc
第三步 发送BDAT \x7f
相关代码在src/smtp_in.c 中的bdat_getc
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 int bdat_getc(unsigned lim) { uschar * user_msg = NULL; uschar * log_msg; for(;;) { #ifndef DISABLE_DKIM BOOL dkim_save; #endif if (chunking_data_left > 0) return lwr_receive_getc(chunking_data_left--); receive_getc = lwr_receive_getc; receive_getbuf = lwr_receive_getbuf; receive_ungetc = lwr_receive_ungetc; #ifndef DISABLE_DKIM dkim_save = dkim_collect_input; dkim_collect_input = FALSE; #endif /* Unless PIPELINING was offered, there should be no next command until after we ack that chunk */ if (!pipelining_advertised && !check_sync()) { unsigned n = smtp_inend - smtp_inptr; if (n > 32) n = 32; incomplete_transaction_log(US"sync failure"); log_write(0, LOG_MAIN|LOG_REJECT, "SMTP protocol synchronization error " "(next input sent too soon: pipelining was not advertised): " "rejected \"%s\" %s next input=\"%s\"%s", smtp_cmd_buffer, host_and_ident(TRUE), string_printing(string_copyn(smtp_inptr, n)), smtp_inend - smtp_inptr > n ? "..." : ""); (void) synprot_error(L_smtp_protocol_error, 554, NULL, US"SMTP synchronization error"); goto repeat_until_rset; } /* If not the last, ack the received chunk. The last response is delayed until after the data ACL decides on it */ if (chunking_state == CHUNKING_LAST) { #ifndef DISABLE_DKIM dkim_exim_verify_feed(NULL, 0); /* notify EOD */ #endif return EOD; } smtp_printf("250 %u byte chunk received\r\n", FALSE, chunking_datasize); chunking_state = CHUNKING_OFFERED; DEBUG(D_receive) debug_printf("chunking state %d\n", (int)chunking_state); /* Expect another BDAT cmd from input. RFC 3030 says nothing about QUIT, RSET or NOOP but handling them seems obvious */ next_cmd: switch(smtp_read_command(TRUE, 1)) { default: (void) synprot_error(L_smtp_protocol_error, 503, NULL, US"only BDAT permissible after non-LAST BDAT"); repeat_until_rset: switch(smtp_read_command(TRUE, 1)) { case QUIT_CMD: smtp_quit_handler(&user_msg, &log_msg); /*FALLTHROUGH */ case EOF_CMD: return EOF; case RSET_CMD: smtp_rset_handler(); return ERR; default: if (synprot_error(L_smtp_protocol_error, 503, NULL, US"only RSET accepted now") > 0) return EOF; goto repeat_until_rset; } case QUIT_CMD: smtp_quit_handler(&user_msg, &log_msg); /*FALLTHROUGH*/ case EOF_CMD: return EOF; case RSET_CMD: smtp_rset_handler(); return ERR; case NOOP_CMD: HAD(SCH_NOOP); smtp_printf("250 OK\r\n", FALSE); goto next_cmd; case BDAT_CMD: { int n; if (sscanf(CS smtp_cmd_data, "%u %n", &chunking_datasize, &n) < 1) { (void) synprot_error(L_smtp_protocol_error, 501, NULL, US"missing size for BDAT command"); return ERR; } chunking_state = strcmpic(smtp_cmd_data+n, US"LAST") == 0 ? CHUNKING_LAST : CHUNKING_ACTIVE; chunking_data_left = chunking_datasize; DEBUG(D_receive) debug_printf("chunking state %d, %d bytes\n", (int)chunking_state, chunking_data_left); if (chunking_datasize == 0) if (chunking_state == CHUNKING_LAST) return EOD; else { (void) synprot_error(L_smtp_protocol_error, 504, NULL, US"zero size for BDAT command"); goto repeat_until_rset; } receive_getc = bdat_getc; receive_getbuf = bdat_getbuf; receive_ungetc = bdat_ungetc; #ifndef DISABLE_DKIM dkim_collect_input = dkim_save; #endif break; /* to top of main loop */ } } } }
BDAT命令进入下面这个分支:
1 2 3 4 5 6 f (sscanf(CS smtp_cmd_data, "%u %n", &chunking_datasize, &n) < 1) { (void) synprot_error(L_smtp_protocol_error, 501, NULL, US"missing size for BDAT command"); return ERR; }
因为\x7F
所以sscanf获取长度失败,进入synprot_error
函数,该函数同样是位于smtp_in.c
文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static int synprot_error(int type, int code, uschar *data, uschar *errmess) { int yield = -1; log_write(type, LOG_MAIN, "SMTP %s error in \"%s\" %s %s", (type == L_smtp_syntax_error)? "syntax" : "protocol", string_printing(smtp_cmd_buffer), host_and_ident(TRUE), errmess); if (++synprot_error_count > smtp_max_synprot_errors) { yield = 1; log_write(0, LOG_MAIN|LOG_REJECT, "SMTP call from %s dropped: too many " "syntax or protocol errors (last command was \"%s\")", host_and_ident(FALSE), string_printing(smtp_cmd_buffer)); } if (code > 0) { smtp_printf("%d%c%s%s%s\r\n", FALSE, code, yield == 1 ? '-' : ' ', data ? data : US"", data ? US": " : US"", errmess); if (yield == 1) smtp_printf("%d Too many syntax or protocol errors\r\n", FALSE, code); } return yield; }
然后在synprot_error
函数中有一个string_printing
函数,位于src/string.c 代码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 const uschar * string_printing2(const uschar *s, BOOL allow_tab) { int nonprintcount = 0; int length = 0; const uschar *t = s; uschar *ss, *tt; while (*t != 0) { int c = *t++; if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++; length++; } if (nonprintcount == 0) return s; /* Get a new block of store guaranteed big enough to hold the expanded string. */ ss = store_get(length + nonprintcount * 3 + 1); /* Copy everything, escaping non printers. */ t = s; tt = ss; while (*t != 0) { int c = *t; if (mac_isprint(c) && (allow_tab || c != '\t')) *tt++ = *t++; else { *tt++ = '\\'; switch (*t) { case '\n': *tt++ = 'n'; break; case '\r': *tt++ = 'r'; break; case '\b': *tt++ = 'b'; break; case '\v': *tt++ = 'v'; break; case '\f': *tt++ = 'f'; break; case '\t': *tt++ = 't'; break; default: sprintf(CS tt, "%03o", *t); tt += 3; break; } t++; } } *tt = 0; return ss; }
在string_printing2
函数中,用到store_get
, 长度为length + nonprintcount * 3 + 1
,比如BDAT \x7F
这句命令,就是6+1*3+1 => 0x0a
,我们继续跟踪store中的全局变量,因为0xa < yield_length
,所以直接使用的Exim的堆分配,不会用到malloc,只有当上一次malloc 0x2000的内存用完或不够用时,才会再进行malloc
0xa 对齐-> 0x10
return next_yield = heap1+0x110
next_yield = heap1+0x120
yield_length = 0x1f00 - 0x10 = 0x1ef0
最后一步 就是PoC中的发送大量数据去触发UAF:
1 2 s = 'a'*6 + p64(0xdeadbeef)*(0x1e00/8) r.send(s+ ':\r\n')
再回到receive.c
文件中,读取用户输入的是1788行的循环,然后根据meh所说,UAF的触发点是下面这几行代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 if (ptr >= header_size - 4) { int oldsize = header_size; /* header_size += 256; */ header_size *= 2; if (!store_extend(next->text, oldsize, header_size)) { uschar *newtext = store_get(header_size); memcpy(newtext, next->text, ptr); store_release(next->text); next->text = newtext; } }
当输入的数据大于等于0x100-4
时,会触发store_extend
函数,next->text
的值上面提了,是heap1+0x10
,oldsize=0x100, header_size = 0x100*2 = 0x200
然后在store_extend
中,有这几行判断代码:
1 2 3 if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) || inc > yield_length[store_pool] + rounded_oldsize - oldsize) return FALSE;
其中next_yield = heap1+0x120
, ptr + 0x100 = heap1+0x110
因为判断的条件为true,所以store_extend
返回False
这是因为在之前string_printing
函数中中分配了一段内存,所以在receive_msg
中导致堆不平衡了,
随后进入分支会修补这种不平衡,执行store_get(0x200)
return next_yield = heap1+0x120
next_yield = heap1+0x320
yield_length = 0x1ef0 - 0x200 = 0x1cf0
然后把用户输入的数据复制到新的堆中
随后执行store_release
函数,问题就在这里了,之前申请的0x2000的堆还剩0x1cf0,并没有用完,但是却对其执行glibc的free操作,但是之后这个free后的堆却仍然可以使用,这就是我们所知的UAF, 释放后重用漏洞
1 2 3 4 5 6 7 8 9 10 for (b = chainbase[store_pool]; b != NULL; b = b->next) { storeblock *bb = b->next; if (bb != NULL && CS block == CS bb + ALIGNED_SIZEOF_STOREBLOCK) { b->next = bb->next; ....... free(bb); return; }
其中,bb = chainbase->next = heap1
, 而且next->text == bb + 0x10
所以能成功执行free(bb)
因为输入了大量的数据,所以随后还会执行:
store_extend(next->text, 0x200, 0x400)
store_extend(next->text, 0x400, 0x800)
store_extend(next->text, 0x800, 0x1000)
但是这些都不能满足判断:if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) || inc > yield_length[store_pool] + rounded_oldsize - oldsize)
所以都是返回true,不会进入到下面分支
但是到store_extend(next->text, 0x1000, 0x2000)
的时候,因为满足了第二个判断0x2000-0x1000 > yield_length[store_pool]
, 所以又一次返回了False
所以再一次进入分支,调用store_get(0x2000)
因为0x2000 > yield_length
所以进入该分支:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 if (size > yield_length[store_pool]) { int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size; int mlength = length + ALIGNED_SIZEOF_STOREBLOCK; storeblock * newblock = NULL; if ( (newblock = current_block[store_pool]) && (newblock = newblock->next) && newblock->length < length ) { /* Give up on this block, because it's too small */ store_free(newblock); newblock = NULL; } if (!newblock) { pool_malloc += mlength; /* Used in pools */ nonpool_malloc -= mlength; /* Exclude from overall total */ newblock = store_malloc(mlength); newblock->next = NULL; newblock->length = length; if (!chainbase[store_pool]) chainbase[store_pool] = newblock; else current_block[store_pool]->next = newblock; } current_block[store_pool] = newblock; yield_length[store_pool] = newblock->length; next_yield[store_pool] = (void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK); (void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]); }
这里就是该漏洞的关键利用点
首先:newblock = current_block = heap1
然后:newblock = newblock->next
我猜测的meh的情况和我加了printf
进行测试的情况是一样的,在printf
中需要malloc一块堆用来当做缓冲区,所以在heap1下面又多了一块堆,在free了heap1后,heap1被放入了unsortbin,fd和bk指向了arena
所以这个时候,heap1->next = fd = arena_top
之后的流程就是:
current_block = arena_top
next_yield = arena_top+0x10
return next_yield = arena_top+0x10
next_yield = arena_top+0x2010
在执行完store_get
后就是执行memcpy
:
1 memcpy(newtext, next->text, ptr);
上面的newtext
就是store_get
返回的值arena_top+0x10
把用户输入的数据copy到了arena中,最后达到了控制RIP=0xdeadbeef
造成crash的效果
但是实际情况就不一样了,因为没有printf,所以heap1是最后一块堆,再free之后,就会合并到top_chunk中,fd和bk字段不会被修改,在释放前,这两个字段也是用来储存storeblock结构体的next和length,所以也是没法控制的
总结 CVE-2017-16943的确是一个UAF漏洞,但是在我的研究中却发现没法利用meh提供的PoC造成crash的效果
之后我也尝试其他利用方法,但是却没找到合适的利用链
发现由于Exim自己实现了一个堆管理,所以在heap1之后利用store_get
再malloc一块堆是不行的因为current_block也会被修改为指向最新的堆块,所以必须要能在不使用store_get
的情况下,malloc一块堆,才能成功利用控制RIP,因为exim自己实现了堆管理,所以都是使用store_get
来获取内存,这样就只能找printf
这种有自己使用malloc的函数,但是我找到的这些函数再调用后都会退出receive_msg
函数的循环,所以没办法构造成一个利用链
引用
Exim源码
Bugzilla-2199