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

拾い物のコンパス

まともに書いたメモ

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

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

プラグイン基礎

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

plugin_cmds.h

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

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

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

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

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

geteip.c

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

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

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

/*
 * BOOL IsDebuggerPresent(VOID);
 */

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

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

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

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

実行

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

コンパイル

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

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

ロード

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

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

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

注意点

Anti-Qemu trick回避

IsDebuggerPresent Hook

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

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

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

課題プログラム

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

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

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

Level1: Blue

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

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

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

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


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

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

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

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

/*
 * DWORD GetTickCount(VOID)
 */

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

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

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

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

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

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

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

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

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

Level2: Charlie

コードは以下の通り.

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

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

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

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

static DECAF_Handle getsysteminfo_handle     = DECAF_NULL_HANDLE;

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

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

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

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

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

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

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

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

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

Level3: Delta

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

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

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

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

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

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

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

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

Level4: Echo

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

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

cmpxchg8b fs:[0x1000]

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

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

よくわからん!

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

Level4復帰

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

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

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

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

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

最後に

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

裏ワザ

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

なんだこれ

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

参考

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

Win32 Thread Information Block - Wikipedia, the free encyclopedia

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

OllyDbg Q&A (Digital Travesia)