作者:Hcamael@知道創宇404實驗室
英文版本:http://www.bjnorthway.com/1028/
感恩節那天,meh在Bugzilla上提交了一個exim的uaf漏洞:https://bugs.exim.org/show_bug.cgi?id=2199,這周我對該漏洞進行應急復現,卻發現,貌似利用meh提供的PoC并不能成功利用UAF漏洞造成crash
漏洞復現
首先進行漏洞復現
環境搭建
復現環境:ubuntu 16.04 server
# 從github上拉取源碼
$ git clone https://github.com/Exim/exim.git
# 在4e6ae62分支修補了UAF漏洞,所以把分支切換到之前的178ecb:
$ git checkout ef9da2ee969c27824fcd5aed6a59ac4cd217587b
# 安裝相關依賴
$ apt install libdb-dev libpcre3-dev
# 獲取meh提供的Makefile文件,放到Local目錄下,如果沒有則創建該目錄
$ cd src
$ mkdir Local
$ cd Local
$ wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile
$ cd ..
# 修改Makefile文件的第134行,把用戶修改為當前服務器上存在的用戶,然后編譯安裝
$ make && make install
然后再修改下配置文件/etc/exim/configure文件的第364行,把
accept hosts = : 修改成 accept hosts = *
PoC測試
從https://bugs.exim.org/attachment.cgi?id=1050獲取到meh的debug信息,得知啟動參數:
$ /usr/exim/bin/exim -bdf -d+all
PoC有兩個:
需要先安裝下pwntools,直接用pip裝就好了,兩個PoC的區別其實就是padding的長度不同而已
然后就使用PoC進行測試,發現幾個問題:
- 我的debug信息在最后一部分和meh提供的不一樣
- 雖然觸發了crash,但是并不是UAF導致的crash
debug信息不同點比較:
# 我的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進行調試,發現:
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,則就不會拋異常了
$ 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:
# 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漏洞:
$ /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:
*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];
However, in receive.c:1787, store_release frees the whole block, leaving the new pointer points to a freed location. Any further usage of this buffer leads to a use-after-free vulnerability.
To trigger this bug, BDAT command is necessary to perform an allocation by raising an error. Through our research, we confirm that this vulnerability can be exploited to remote code execution if the binary is not compiled with PIE.
An RIP controlling PoC is in attachment poc.py. The following is the gdb result of this PoC:
Program received signal SIGSEGV, Segmentation fault.
0x00000000deadbeef in ?? ()
(gdb)
-------------------------------------------------------------
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:
1. send unrecognized command to adjust yield_length and make it less than 0x100
2. send BDAT 1
3. send one character to reach the length of BDAT
3. 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
4. send a huge message until store_extend called
5. 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中
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行進行的初始化:
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函數:
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命令進入下面這個分支:
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文件中:
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代碼中:
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:
s = 'a'*6 + p64(0xdeadbeef)*(0x1e00/8)
r.send(s+ ':\r\n')
再回到receive.c文件中,讀取用戶輸入的是1788行的循環,然后根據meh所說,UAF的觸發點是下面這幾行代碼:
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中,有這幾行判斷代碼:
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, 釋放后重用漏洞
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所以進入該分支:
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:
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函數的循環,所以沒辦法構造成一個利用鏈
引用
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/469/