Snoozy

1.Sleep-inducing; tedious.

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です.楽しみですね.