1. qiraとは
qiraとは世界的なハッカー、George Hotz氏 (ジョージ・ホッツ - Wikipedia)
によって開発された高機能バイナリトレーサーであり、qiraという名は(QEMU Interactive Runtime Analyser)の略である。
GitHub - BinaryAnalysisPlatform/qira: QEMU Interactive Runtime Analyser
略語を見れば分かるがuser mode QEMUを使用したバイナリ解析ツールであり、ELFなどの実行形式バイナリを実際に動作させて各命令のレジスタ、メモリへの操作を逐次記録する。
これらの記録はweb UIを通して好きな命令位置にカーソルを移動させるだけで見ることができ、その時のレジスタ、メモリの記録が再現される仕組みになっている。ソフトウェアのデバッグやCTFにおけるバイナリ解析において非常に有用である。
qiraはTimeless Debuggingという手法を用いており、これは各1命令をレジスタ,メモリへの1コミットとみなして逐次記録し、解析者は好きな命令位置に後からcheckoutしてその時の状態を見れるというものだ。
詳しくは開発者本人であるgeohot氏によるUSENIX Enigmaでの講演を参照されたい。
USENIX Enigma 2016 - Timeless Debugging
www.youtube.com
gdbにもReverse Debuggingのような過去の実行記録を辿れる機能はあるが、qiraのTimeless Debuggingはもっと対話的で、解析者の労力が減らされるような工夫がされている。
2. qiraの実装を知る意義
qiraは多くのアーキテクチャ向けバイナリ(x86,arm,mips,ppc...etc)の解析に対応している。これはQEMUでアーキテクチャの差を吸収しているからだ。基本的にトレーサーのコアロジックはQEMUを改造する事で実現されているものだが、実は他にも並行してradare2やida sdk, capstoneを用いて対象バイナリをあらかじめ静的解析したり、グラフビューを自前で用意するなど解析ツールとして非常に教材向きでもある。
ではそもそも解析ツールの実装を知る意義は何か。なぜ中身まで知る必要があるのか。それは既存のフレームワークやスクリプトでは対処しきれない、より高度なハックを行う時にこれらの知見が必要になるからだ。
本記事はそんな機会が来る(もしくは既に来た)人のための最初の一歩になれば幸いである。
逆に、そんな事しなくても世の中には充分良い物があるし、俺はそれを使って満足しているという人にとってはあまり有意義な記事じゃ無いかもしれない。
さてではここからは本題に戻ってqiraの実装を見ていく。
3. qira実行からQEMU起動まで
qiraは以下のコマンドで実行できる。
$ qira ./a.out
qira [target-binary] でトレース対象とするバイナリを指定する。
このqira本体はシェルスクリプトになっていて中からmiddleware/qira.pyを呼んでいる。
(qira)
#!/bin/bash -e unamestr=$(uname) if [[ "$unamestr" == 'Linux' ]]; then DIR=$(dirname $(readlink -f $0)) elif [[ "$unamestr" == "Darwin" ]]; then cmd=$(which "$0") if [ -L "$cmd" ]; then cmd=$(readlink "$cmd") fi DIR=$(dirname "$cmd") else echo "Only Linux and Mac OS X are supported!" exit fi unset PYTHONPATH source $DIR/venv/bin/activate exec /usr/bin/env python2.7 $DIR/middleware/qira.py $* [*1]
[*1]でqira.pyスクリプトを実行している。
qira.pyには様々なオプションがあり、上で指定したターゲットバイナリはbinaryオプションにパスが設定される。
(middleware/qira.py)
if __name__ == '__main__': # define arguments parser = argparse.ArgumentParser(description = 'Analyze binary. Like "qira /bin/ls /"') parser.add_argument('-s', "--server", help="bind on port 4000. like socat", action="store_true") parser.add_argument('-t', "--tracelibraries", help="trace into all libraries", action="store_true") parser.add_argument('binary', help="path to the binary") [*2] parser.add_argument('args', nargs='*', help="arguments to the binary") [*3] ... # creates the file symlink, program is constant through server run program = qira_program.Program(args.binary, args.args, qemu_args) [*4] # start the binary runner if args.server: qira_socat.start_bindserver(program, qira_config.SOCAT_PORT, -1, 1, True) else: print "**** running "+program.program program.execqira(shouldfork=not is_qira_running) [*5] if not is_qira_running: # start the http server qira_webserver.run_server(args, program) [*6]
上記のコードがqiraのメイン処理。
まず[*2],[*3]でそれぞれターゲットバイナリとバイナリに渡す引数をパースし、[*4]でターゲットバイナリを解析するための初期化を行い、[*5]で実際にQEMU上でターゲットバイナリを動作させている。最後に[*6]でweb UIとやり取りするためのサーバーを起動する。
これらがqiraの一連の処理である。それぞれの処理について詳細を見ていく。
まず[*4]のProgramクラスの初期化コードは以下のようになっている。
(middleware/qira_program.py)
class Program: def __init__(self, prog, args=[], qemu_args=[]): # call which to match the behavior of strace and gdb self.program = which(prog) self.args = args self.proghash = sha1(open(self.program, "rb").read()).hexdigest() print "*** program is",self.program,"with hash",self.proghash # this is always initted, as it's the tag repo self.static = static2.Static(self.program) [*7] # bring this back if self.program != "/tmp/qira_binary": try: os.unlink("/tmp/qira_binary") except: pass try: os.symlink(os.path.realpath(self.program), "/tmp/qira_binary") [*8] except: pass # defaultargs for qira binary self.defaultargs = ["-strace", "-D", "/dev/null", "-d", "in_asm", "-singlestep"]+qemu_args [*9] if qira_config.TRACE_LIBRARIES: self.defaultargs.append("-tracelibraries") self.identify_program() [*10]
まずは[*7]のstati2.Staticクラスの初期化だが、これはターゲットバイナリを静的解析するための初期化である。2章でも少し述べたようにqiraはターゲットバイナリをQEMUで動作させるだけでなく、同時にターゲットバイナリの静的解析も行っている。そのための初期化である。(詳細は後述)
次に[*8]でターゲットバイナリを/tmp/qira_binaryにsymbolic linkしている。以後ターゲットバイナリは/tmp/qira_binaryとしてアクセスされる。
[*9]は上記[*5]のexecqiraで実際にQEMUを動作させる際、QEMUに渡す引数を設定している。特に注目すべきなのは"-d in_asm"と"-singlestep"である。このオプションを与える理由は後ほど説明する。
[*10]のidentify_programでターゲットバイナリのフォーマットやアーキテクチャの解析を行っているがこれについては詳細は省く。よくあるオーソドックスなバイナリパース処理だからだ。(興味のある方はmiddleware/qira_program.pyのidentify_program関数を参照されたい)
ちなみにこのidentify_program関数は[*7]で初期化した静的解析クラスを使用していない完全に独立したバイナリパーサーである。
qiraには至る所に独自の静的解析の実装があり、既存のstatic2.Staticクラスと混同する事があるが、基本的にはstati2.Staticクラスはweb UIで表示されるスタックトレースやグラフビューを構築するために使われており、一方アーキテクチャ特定のためのバイナリパースといった基本的な静的解析機能はqiraが独自に実装しているケースが多い。
では[*5]まで戻ってexecqiraで実際にQEMU上でバイナリを動作させる処理を見てみよう。
(qira_program.py)
def execqira(self, args=[], shouldfork=True): if qira_config.USE_PIN: # is "-injection child" good? eargs = [self.pinbinary, "-injection", "child", "-t", self.pintool, "--", self.program]+self.args else: eargs = [self.qirabinary]+self.defaultargs+args+[self.program]+self.args #print "***",' '.join(eargs) os.execvp(eargs[0], eargs) [*11]
[*11]でself.qirabinaryを実行している。self.qira_binaryはx86_64環境の場合qira-x86_64というファイル名になる。このqira-[arch_name]というファイルはtracers/qemuにあって、それぞれ同ディレクトリのqemu-latest/[arch_name]-linux-user/qemu-*へのシンボリックリンクになっている。つまりqira_x86_64を実行するとx86_64用のユーザーモードQEMUが動作を開始する事になる。
これでターゲットバイナリをQEMUで動作させる所まで来た。
4. QEMUによるトレース
では次にバイナリをトレースするためにQEMUユーザーモードに加えている独自実装部分を見ていく。本家QEMUとの差分はパッチとしてtracers/qemu/qemu.patchにある。
qiraはQEMUのTCI(TCG Interprter)という機能をうまく利用する事でトレースを行っている。
そもそもTCGとは何か、簡単に説明する。(QEMUの実装に関するさらに詳しい話
QEMUのなかみ(QEMU internals) part1 - るくすの日記 ~ Out_Of_Range ~
を参照されたし)
QEMUはターゲットバイナリを一旦TCG micro codeという独自の中間表現に変換し、それをホスト側のアーキテクチャのマシン語に変換するという事を逐次行っている。これにより仮にターゲットバイナリがARMやMIPS,PPCなどであってもホスト(x86)用の命令に変換するため実行可能だ。
さてtracers/qemu/qemu.patchを見てみよう。パッチの875行目からtcg_qemu_tb_exec(tci.c)関数への追加差分が載っている。
/* Interpret pseudo code in tb. */ uintptr_t tcg_qemu_tb_exec(CPUArchState *env, uint8_t *tb_ptr) { +#ifdef QIRA_TRACKING + CPUState *cpu = ENV_GET_CPU(env); + TranslationBlock *tb = cpu->current_tb; + //TaskState *ts = (TaskState *)cpu->opaque; + + if (unlikely(GLOBAL_QIRA_did_init == 0)) { + // get next id + if (GLOBAL_id == -1) { GLOBAL_id = get_next_id(); } + + // these are the base libraries we load + write_out_base(env, GLOBAL_id); + + init_QIRA(env, GLOBAL_id); + + // these three arguments (parent_id, start_clnum, id) must be passed into QIRA + // this now runs after init_QIRA + if (GLOBAL_parent_id != -1) { + run_QIRA_log(env, GLOBAL_parent_id, GLOBAL_start_clnum); + run_QIRA_mods(env, GLOBAL_id); + } + + return 0; + } ...
tcg_qemu_tb_exec関数(tci.c)はQEMUがTCG micro codeからバックエンド,すなわちホストのマシン語に変換した物を実際にQEMUが実行するための関数である。
この関数はQEMUのビルド時にconfigureで--enable-tcg-interprterを有効にしたか否かで中身が変わる。もしこのオプションを付けなかった場合は上記tci.c内のtcg_qemu_tb_execではなく、変わりにtcg/tcg.h内で定義された同名のマクロが呼ばれる。このマクロは生成したホストコード片に直接ジャンプするような関数になっていて上記tci.cのtcg_qemu_tb_execとは全く違う中身になっている。(詳細はQEMUのなかみpart2
QEMUのなかみ(QEMU internals) part2 - るくすの日記 ~ Out_Of_Range ~
を参照)
qiraは--enable-tcg-interprterを有効にした状態でQEMUをビルドするようになっているが、このオプションを有効にするとQEMUはTCGの中間表現をホストのマシン語に変換するのではなく、直接インタプリタ的に中間表現を実行するようになる。これがTCI(TCG Interpreter)機能である。
なぜqiraはわざわざTCIを使っているのか。それはターゲットバイナリがTCG microcodeにどのように変換されるかを見れば分かる。
例えば以下のようなマシン語(x86)があったとする。
mov $0xffffffffc0084100,%rdi
これは以下のようなTCG micro codeに変換される。
movi_i64 tmp0,$0xffffffffc0084100 mov_i64 rdi,tmp0
ちなみにTCG micro codeではmov_i64は64bitのレジスタ間代入、movi_i64は64bitの即値代入を意味する。(TCG micro codeはintel記法なので注意)
そしてこの中間表現は(TCIが無効な時)以下のようなホストコード(x86)に変換される。
mov $0xffffffffc0084100,%rbp mov %rbp,0x38(%r14)
かなり非直感的なコードに変換されているが、実は簡単である。
まず上の何の前触れも無しに登場したrbpレジスタは中間表現中のtmp0を意味する。QEMUではtmp0のような一時レジスタは、その時使われていない適当なホストのレジスタを割り当てるような変換ロジックになっているため、たまたまtmp0にrbpレジスタが割り当てられているだけである。さてでは0x38(%r14)は何を表すのか。実はこれがターゲットバイナリのマシン語にあったrdiレジスタである。つまりターゲットバイナリ中のレジスタへのアクセスは全てホストでのメモリアクセスに変換される。ちなみにこのr14レジスタというのは固定でQEMU中のCPUArchStateという構造体の先頭を指している。つまり0x38(%r14)とはCPUArchState構造体のメンバへのアクセスになっている。0x38オフセットはCPUArchState->regs[R_RDI]を意味する。
そう、つまりQEMUの持つ仮想RDIレジスタへのアクセスになっているわけだ。
さて、では--enable-tcg-interprterを有効にした時はどのようになるか。
上でも述べたようにTCIが有効だと中間表現が直接実行されるが、実は少しだけ違う。
というのもTCIの場合は、TCG miro codeを更に変換してTCG micro code(backend)に変換してから実行されるからだ。
つまり--enable-tcg-interpreterがない場合
arm -> (TCG) -> x86 の用に変換されるが TCIの場合
arm -> (TCG) -> TCG backend のように変換される。
このTCG backend命令はTCG micro code(frontendと呼ぶ)と殆ど変わらない命令だが、セマンティックをもう少し回砕ようなイメージになる。例えば上の
movi_i64 tmp0,$0xffffffffc0084100 mov_i64 rdi,tmp0
というTCG micro code(frontend)は
movi_i64 tmp0,$0xffffffffc0084100 ld_64 0x38(T_AREG0), tmp0
に変換される。先ほどのホストコードと比較すれば分かると思うが、T_AREG0というのは上のr14レジスタと同じくCPUArchState構造体を指しているTCGのレジスタである。
TCIはTCG micro codeを直接実行するとは言ったが、こういったレジスタアクセスをホストメモリアクセスに変換するなどの作業を行ってからTCG micro code(backend)を実行するわけだ。
さて、つまりまとめるとターゲットバイナリにおけるレジスタへのアクセス、そして勿論メモリへのアクセスは全てホストマシンではメモリ(CPUArchStateなど)へのアクセスに変換されている事が分かった。(細かいことを言うと実はターゲットバイナリ内のメモリアクセス命令はld_64命令に変換されるのでは無く、qemu_ld64命令になる。これはターゲットバイナリ内でのldする対象アドレスはゲストアドレスを指すため、そのままのアドレスでホストのld_64命令に直すとおかしくなる。よってsoftmmuなどで変換をかけるために一旦qemu_ld64命令を通すのだが細かい話はまたいつか...)
qiraはこのQEMUのTCIの性質を使ってターゲットバイナリのレジスタ,メモリアクセスイベントを記録している。つまりld_64やld_32,st_64... などのTCG micro code(backend)さえフックしていればレジスタの変化を記録できる。ちなみにメモリアクセスに関してはqemu_ld64命令などをフックすれば取れる。(これについてはまたいつか説明する)
実際にtracers/qemu/qemu.patchを見てみると
(tracers/qemu/qemu.path: 1093行目)
case INDEX_op_ld_i64: t0 = *tb_ptr++; t1 = tci_read_r(&tb_ptr); t2 = tci_read_s32(&tb_ptr); + track_read(t1, t2, *(uint64_t *)(t1 + t2), 64); [*1] tci_write_reg64(t0, *(uint64_t *)(t1 + t2)); break;
ld_i64命令をTCIが実行するとき、[*1]で追加されているtrack_read関数でフックしてオペランド値を記録している。
(tracers/qemu/qemu.path: 576行目)
+void track_read(target_ulong base, target_ulong offset, target_ulong data, int size) { + QIRA_DEBUG("read: %x+%x:%d = %x\n", base, offset, size, data); + if ((int)offset < 0) return; + if (GLOBAL_logstate->is_filtered == 0) add_change(offset, data, size); [*2] +}
track_read関数はbase,offset,data,sizeを取るが、baseは使っていない。
[*2]のadd_changeでbase以外のoffset,data,sizeをデータベースに記録する。
これはつまり、例えばld_64 0x38(T_AREG0), tmp0という命令があった場合、"アドレス0x38にdataがsizeバイト書き込まれた"というようにDBに記録される。
通常0x38などというメモリにアクセスする事は無いため、このような非常に小さい範囲のアドレスへのアクセスは後でqiraのフロントエンドからDBを読む時に、レジスタへのアクセス記録だと判断するようになっている。
ちなみにadd_change関数によって記録されるトラック履歴のフォーマットは以下のような構造体で表される。
(tci.c)
// struct storing change data struct change { uint64_t address; uint64_t data; uint32_t changelist_number; uint32_t flags; }; #define IS_VALID 0x80000000 //有効なデータか(デバッグ用?) #define IS_WRITE 0x40000000 //書き込み #define IS_MEM 0x20000000 //メモリアクセス (なお下位bitはデータのサイズ) #define IS_START 0x10000000 //[address, address+data]が命令領域という事を知らせるためのフラグ #define IS_SYSCALL 0x08000000 //システムコール開始 #define SIZE_MASK 0xFF
5. Webサーバーの起動とDBの同期
3章の[*6]のrun_serverでwebサーバーを起動している。本性ではここからwebサーバーの動作を追っていこう。
(qira_webserver.py)
def run_server(largs, lprogram): # web static moved to external file import qira_webstatic qira_webstatic.init(lprogram) ... threading.Thread(target=mwpoller).start() [*1] try: socketio.run(app, host=qira_config.HOST, port=qira_config.WEB_PORT, log=open("/dev/null", "w")) [*2]
[*1]ではmwpollerという関数を実行する別のスレッドを起動している。mwpoller(qira_webserver.py)はmwpoll関数を0.2秒ごとに呼ぶだけの実装である。
このmwpoll関数はトレース中のターゲットバイナリの全プロセス(forkすれば子プロセスが増えるがこれも監視しているため1ターゲットバイナリに対して複数のプロセスが存在する可能性がある)のDBをチェックし、内容が更新されていればそれをweb UI側に反映させるような実装になっている。
(qira_webserver.py)
def mwpoll(): # poll for new traces, call this every once in a while for i in os.listdir(qira_config.TRACE_FILE_BASE): if "_" in i: continue i = int(i) if i not in program.traces: program.add_trace(qira_config.TRACE_FILE_BASE+str(i), i) [*3] did_update = False # poll for updates on existing for tn in program.traces: if program.traces[tn].db.did_update(): [*4] t = program.traces[tn] t.read_strace_file() socketio.emit('strace', {'forknum': t.forknum, 'dat': t.strace}, namespace='/qira') did_update = True # trace specific stuff if program.traces[tn].needs_update: push_trace_update(tn) if did_update: program.read_asm_file() [*5] push_updates(False)
[*3]のadd_traceでは各プロセス用のTraceスレッドを追加している。このTraceスレッドは解析対象のバイナリ1プロセスの情報を管理するスレッドで、バイナリの静的解析やトレースDBの作成などを行う。詳しく見ていこう。
(middleware/qira_program.py)
def add_trace(self, fn, i): self.traces[i] = Trace(fn, i, self, self.tregs[1], len(self.tregs[0]), self.tregs[2]) return self.traces[i] class Trace: def __init__(self, fn, forknum, program, r1, r2, r3): ... threading.Thread(target=self.analysis_thread).start() def analysis_thread(self): #print "*** started analysis_thread" while 1: time.sleep(0.2) # so this is done poorly, analysis can be incremental if self.maxclnum == None or self.db.get_maxclnum() != self.maxclnum: [*6] self.analysisready = False minclnum = self.db.get_minclnum() maxclnum = self.db.get_maxclnum() self.program.read_asm_file() [*7] self.flow = qira_analysis.get_instruction_flow(self, self.program, minclnum, maxclnum) [*8] self.dmap = qira_analysis.get_hacked_depth_map(self.flow, self.program) qira_analysis.analyse_calls(self) # hacky pin offset problem fix hpo = len(self.dmap)-(maxclnum-minclnum) if hpo == 2: self.dmap = self.dmap[1:] self.maxd = max(self.dmap) self.picture = qira_analysis.get_vtimeline_picture(self, minclnum, maxclnum) self.minclnum = minclnum self.maxclnum = maxclnum self.needs_update = True
Traceスレッドで実行されるanalysis_thread関数は0.2秒ごとにループを回し、QEMUのトレーサが吐き出したDBをチェックする。ちなみにDBの実体は/tmp/qira_logs/の下にある。(0,1などの連番の名前になっている)
このDBをチェックし[*6]でDB内の最新エントリー番号self.db.maxclnum()が以前から更新されている場合は、DBが更新されたとみなしている。
さてDBの更新を確認した後は[*7]のread_asm_fileでで実際にQEMU上で実行されたターゲットバイナリのアセンブリデータを読み出す。この関数はQEMUが-d in_asmオプションで吐き出したアセンブリファイル(/tmp/qira_asm)を読みに行き、パースする。
(qira_program.py)
def read_asm_file(self): if os.name == "nt": return ... for d in dat.split("\n"): thumb = False if len(d) == 0: continue # hacks try: if self.fb == 0x28: #thumb bit in front addr = int(d.split(" ")[0][1:].strip(":"), 16) else: addr = int(d.split(" ")[0].strip(":"), 16) except: continue if self.fb == 0x28: ... elif self.fb == 0xb7: # aarch64 inst = d[d.rfind(" ")+5:] else: inst = d[d.find(":")+2:] cnt += 1 # trigger disasm d = self.static[addr]['instruction'] [*9]
/tmp/qira_asmファイルは以下のようなテキストファイルになっている。
0xffffffffc0082040: xchg %ax,%ax 0xffffffffc0082045: push %rbp 0xffffffffc0082046: mov $0xffffffffc0084100,%rdi 0xffffffffc008204d: mov %rsp,%rbp 0xffffffffc0082050: callq 0xffffffff810f8220 0xffffffffc0082055: xor %eax,%eax
read_asm_fileで、読み出した/tmp/qira_asmのデータをパースする。ただしここでパースするのは0xffffffffc0082040:などのアドレスだけで、命令列は見ない。
[*9]のaddrがパースして得たアドレスであり、self.static[addr]['instruction']にアクセスすると__getitem__メンバが実行され該当addr箇所のディスアセンブルが行われて格納される実装になっている。
self.static[addr]はTagsクラスになっているため、データを取得しようとアクセスするとTagsクラスの__getitem__が呼ばれる。
(model.py)
class Tags: def __init__(self, static, address=None): self.backing = {} self.static = static self.address = address def __contains__(self, tag): return tag in self.backing def __getitem__(self, tag): if tag in self.backing: return self.backing[tag] else: # should reading the instruction tag trigger disasm? # and should dests be a seperate tag? if tag == "instruction": dat = self.static.memory(self.address, 0x10) [*10] # arch should probably come from the address with fallthrough self.backing['instruction'] = Instruction(dat, self.address, self.static[self.address]['arch']) [*11] self.backing['len'] = self.backing['instruction'].size() self.backing['type'] = 'instruction' return self.backing[tag] if tag == "crefs" or tag == "xrefs": # crefs has a default value of a new array self.backing[tag] = set() return self.backing[tag] if tag in self.static.global_tags: return self.static.global_tags[tag] return None
Tagsの__getitem__ではまず[*10]のself.static.memory関数で予め静的解析エンジン(radare2など)で読み込んでおいたターゲットバイナリの該当アドレスから0x10バイト読みだしている。
(static/static2.py)
# return the memory at address:ln # replaces get_static_bytes # TODO: refactor this! def memory(self, address, ln): dat = [] for i in range(ln): ri = address+i # hack for "RuntimeError: dictionary changed size during iteration" for (ss, se) in self.base_memory.keys(): if ss <= ri and ri < se: try: dat.append(self.base_memory[(ss,se)][ri-ss]) [*12] break except: return ''.join(dat) return ''.join(dat)
base_memory[(start_addr, end_addr)]に[start_addr, end_addr]のアドレス範囲にあるバイナリ列が格納されており、[*12]で引数で与えられた[address, address+len]の範囲のバイナリデータをbase_memoryから取り出している。
これで対象アドレスから命令列を取得することができた。
取得した命令列は[*11]でInstructionクラスとして初期化する。
(static/model.py)
class Instruction(object): def __new__(cls, *args, **kwargs): if qira_config.WITH_BAP: try: return BapInsn(*args, **kwargs) except Exception as exn: print "bap failed", type(exn).__name__, exn return CsInsn(*args, **kwargs) else: return CsInsn(*args, **kwargs)
このInstructionクラスはBAP(Binary Analysis Platform)の使用フラグがオンになっている場合はBapInsnクラスになり、そうでない場合はCapstoneというディスアセンブルフレームワークを使ったCsInsnクラスになる。どちらも与えられたバイナリデータをディスアセンブルして記録しておくためのクラスである。
さてではanalysis_threadに戻って、残りはanalysis_thread関数[*8]のget_instruciton_flowを始めとしたバイナリの解析関数だが、これらはグラフビューや関数のコールとレースを作成するために呼び出されている。
ここでは詳しく説明しないが興味のある人は実装を読んでみると面白いかもしれない。
6. Web UI上への表示
5章で説明したwebサーバーはQEMUトレーサーの出力したDBを監視し、内部データを逐次更新していた。この更新されたデータはWeb UI側にsocket IOを通して送られる。
qira_webserver.py内にある@socketio.onがWeb UI側から指定されたメッセージを受信した場合の動作になる。例えば命令列ダンプのUI(web/client/idump.js)は'getinstructions'というリクエストをsocketioを通じてサーバー側に送信する。これを受けたサーバー側の処理はqira_webserver.pyに実装されている。
(qira_webserver.py)
@socketio.on('getinstructions', namespace='/qira') [*] @socket_method def getinstructions(forknum, clnum, clstart, clend): trace = program.traces[forknum] slce = qira_analysis.slice(trace, clnum) ret = [] top = [] clcurr = clnum-1 while len(top) != (clnum - clstart) and clcurr >= 0: rret = get_instruction(clcurr) [*1] if rret != None: top.append(rret) clcurr -= 1 clcurr = clnum while len(ret) != (clend - clnum) and clcurr <= clend: rret = get_instruction(clcurr) if rret != None: ret.append(rret) clcurr += 1 ret = top[::-1] + ret emit('instructions', ret)
[*1]でget_instructionを呼び出して変更履歴clcurr番号に該当する命令列を取得している。
(qira_webserver.py)
def get_instruction(i): rret = trace.db.fetch_changes_by_clnum(i, 1) [*2] if len(rret) == 0: return None else: rret = rret[0] instr = program.static[rret['address']]['instruction'] rret['instruction'] = instr.__str__(trace, i) #i == clnum ...
[*2]でサーバーが保存しているQEMUトレーサーのDBから命令を読みだしている。
UIにおけるレジスタの表示も同じように'getregisters'リクエストをsocket IOで飛ばして行う。この辺りはqira_webserver.pyに網羅的に記述されているため興味のある人は読んでみてほしい。
7. おわりに
さて今回qiraの主要な処理を追ったが、このように多くのフレームワーク、ソフトウェアを組み合わせて同期的にデータを更新するような設計は、セキュリティツールだと非常に多い。
特にqiraは既存のQEMUや静的解析エンジン(radare2, ida sdk)などをうまく組み合わせている。本記事がqiraを始めとする様々な解析ツールハックの1歩になれば幸いである。