はじめまして、キャディでバックエンドエンジニアをやっている秋山です。 趣味で粛々とやっていたバイナリ解析について2022年5月17日に開催された社内勉強会で発表させていただきました :tada:
[toc]
バイナリ解析とは?
バイナリ解析とは、製品や実行ファイルなどソフトウェアの内部の詳細を分析し、一般に公開されてないシステム設計や仕様を明確化し調査することです。 その他にマルウェアを静的解析する際もバイナリ解析を使用して調べたりします。
バイナリ解析って、やってもいいの?
法律的に、脆弱性診断のためのリバースエンジニアリングは合法です。 ただし、市販の製品に対してリバースエンジニアリングを行い、その情報を開発などに使用したり情報を開示すると特許法上や著作権上の違法性が認められる場合があります。 また、ソフトウェアの使用許諾契約書などにリバースエンジニアリング禁止条項として記載されている場合は要注意です。
どう楽しむといいのか?
安全かつ楽しんで解析しましょう。 具体的な方法は以下の2つです。
- CTFに参加する
- マルウェア捕まえて解析する
CTFに参加する
セキュリティコンテストはいくつかあるので、参加してみるといいかもしれません。
終了後にはWriteup
という形で解答を掲載する人もいるため、コンテスト終了後に復習できることがあります。
- SECCON Beginners
- picoCTF
- WaniCTF
マルウェア捕まえて解析する
解析対象のバイナリ入手方法
マルウェアを収集するツール
マルウェアを収集できるツールをご紹介します。
- ハニーポッド(Dionaea) マルウェア収集用のハニーポットです。 検体の収集はもちろん、インシデントログからネットワーク経由の攻撃を再現することもできます。 T-POTを入れると諸々入るのでサクッと試せます。
VIRUSTOTAL 検体をダウンロードすることも可能なサービスです。 普段の用途としては、怪しいファイルをアップロードするとよしわるしをパッと判断してくれます。
※ アップしたファイルは公開されるので、機密ファイルなどは避けてください。
Cuckoo Sandbox サンドボックスで怪しいファイルを動作させて、動的に解析できるツール 実行時のキャプチャや書き換えられたレジストリ、どこと通信しようとしているかなどを確認できます。 一家に一台あると安心です。
ランサムウェアのWannaCryを動かしてみるとこんな感じです。 1. ファイルをドロップします。
- 少し待つと、Summaryやキャプチャなどがかえってきます。
解析手法の種類
バイナリを解析するときの手法としては、大きく3つほどあります。 今回は、静的解析をやってみたいと思います。 ① 表層解析 どういった実行形式で実行されているのか、ファイルのハッシュ値(VIRUSTOTALでハッシュ値を検索すればマルウェアが判明することも)など、どういったファイルであるかをデータという視点で解析します。 ② 動的解析 プログラムを実行すると何が見えるかを解析します。※ 検体を実行する場合は手元ではなくCuckooSandboxなどを利用することをお勧めします。
③ 静的解析
プログラムを可視化して構造や動作を解析します。
バイナリ解析(静的解析)で使うツール
解析する上で色々なツールがありますが、今回は radare2 を使ってみます。- radare2
← 今回はこれをやってみます。
- ghidra
- gdb
- objdump
- IDA Pro(有料)
- OllyDbg
バイナリ解析(静的解析)を行う前におさえておきたいこと
解析するうえでABIの部分と、命令セットアーキテクチャ(ISA)の部分をおさえておきましょう。ABIとは
- ABIとはApplication Binary Interfaceの略のことで、バイナリレベルで関数やデータの仕様を規定したものです。 関数をコールするときの引数の渡し方などを規定します。
- コンパイラで生成されるバイナリはABIに準拠しており、ABIを知ることで一覧のアセンブリ命令が何をしようとしているのかを理解することができます。
ABIは何によって決まるの?
- ABIは、ISAとOSにより決まります。 厳密には、言語やコンパイラごとに異なることもあるらしいです。
今回の環境は
x86-64
とLinux
なのでSystem V AMD64 ABI
を参考にしています。参照: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
レジスタとは
CPU内部にある記憶装置のことで、アセンブリが何かをやる際に利用します。
汎用レジスタ
色々な用途に使用できるレジスタで、状況に応じて様々な用途に用いることができます。
- rax
- rbx
- rcx
- rdx
- rsi
- rdi
特殊レジスタ
それぞれ専門的な用途があるレジスタです。
- rbp:現在のスタックフレームにおける底のアドレスを保有。
- rsp:現在のスタックトップのアドレスを保有。
- rip:次に実行するアセンブリ命令のアドレスを保有。
メモ:RCXとECX 64bitでは、RCXなど頭にRがついたレジスタがあります。 32bitでは、ECXなどのレジスタ名になります。 今回のアセンブリでは、64bitの環境なのに頭がEのレジスタを使っています。 これは格納する値が32bitで事足りるので下位32bitだけ使った結果こうなっているらしいです。 参照:https://www.mztn.org/lxasm64/amd04.html
バイナリ解析(静的解析)を行う
それでは、バイナリ解析(静的解析)を実際にやってみましょう。
解析対象のソース
今回利用するソースコードです。
#include <stdio.h>
int add(int a, int b, int c, int d, int e) {
int sum = 0;
sum = a + b + c + d + e;
return sum;
}
int main(void)
{
printf("sum = %d\n", add(1, 2, 3, 4, 0));
return 0;
}
コンパイルする
gcc sample.c
実行時のメモリレイアウトは以下が参考になるかと思います。 https://twitter.com/404death/status/968381431146778624/photo/1
解析する
以下のコマンドで解析します。
r2 -d a.out
関数一覧を見てみる
aaa
でバイナリ全体を解析しています。afl
でバイナリのなかにどういった関数があるのかを一覧で出しています。
[0x7f7b5ad84090]> aaa
[0x7f7b5ad84090]> afl
0x5564c6ff7050 1 42 entry0
0x5564c6ff9fe0 1 4124 reloc.__libc_start_main
0x5564c6ff7080 4 41 -> 34 sym.deregister_tm_clones
0x5564c6ff70b0 4 57 -> 51 sym.register_tm_clones
0x5564c6ff70f0 5 57 -> 50 entry.fini0
0x5564c6ff7040 1 6 sym..plt.got
0x5564c6ff7130 1 5 entry.init0
0x5564c6ff7000 3 23 map.root_a.out.r_x
0x5564c6ff7210 1 1 sym.__libc_csu_fini
0x5564c6ff7135 1 58 sym.add
0x5564c6ff7214 1 9 sym._fini
0x5564c6ff71b0 4 93 sym.__libc_csu_init
0x5564c6ff716f 1 61 main
0x5564c6ff7030 1 6 sym.imp.printf
0x5564c6ff6000 3 404 -> 393 loc.imp._ITM_deregisterTMCloneTable
0x5564c6ff61aa 5 32 -> 55 fcn.5564c6ff61aa
mainとsym.addのアセンブリ全体像
下図の左側がmain
のなかのアセンブラで、右側がadd
関数のアセンブラです。
AT&T構文とIntel構文があるのですが、Intel構文で表示されています。
main関数
関数呼び出しをする際のお決まり(Function prologue)を実行する
関数呼び出しをする際のお決まりのようなものです。
コールされた関数は、呼び出し元に戻る時にスタックの状態が呼び出し時と同じ状態になるようにrbp
を退避しておきます。
push rbp
mov rbp,rsp
有効な命令アドレスの範囲は0x00007FFFFFFFFFFFです。
push rbp
が実行されると、スタックの一番上に積まれます。
push rbp
mov rbp,rsp
が実行されると、スタックの番地をrbp
に保存します。
これは、main関数におけるスタックの底を指定する処理になります。
この状態になっていると、現在の関数でスタックを好きに使っても呼び出し元に戻る時には元の状態にして戻すことができます。
mov rbp,rsp
引数を渡す
add関数に渡すための値をレジスタに設定します。
mov r8d, 0
mov ecx, 4
mov edx, 3
mov esi, 2
mov edi, 1
メモ:関数呼び出しに関する決まりごと 引数の渡し方はABIに準拠しており、関数呼び出しに関する決まりごとはABIのドキュメントの「Figure 3.4: Register Usage」に記載があります。
関数をコールする
call
を実行すると、スタックにリターンアドレスをプッシュします。
call sym.add
printf関数に渡す値を設定する
printf関数に渡すための値をレジスタに設定します。
第一引数(rdi
)にはフォーマットとなる文字列 sum = %d\n
のアドレスを設定し、第二引数(esi
)には一つ目の%d
に表示するための合計値を格納しています。
mov esi, eax ; addの戻り値を第二引数に設定する
lea rdi, qword str.sum____d ; 0x5564c6ff8004 ; "sum = %d\n"
mov eax, 0
call sym.imp.printf ; int printf(const char *format)
メモ:mov eax, 0命令
mov eax, 0
命令は、今まで見てきた引数渡しのパターンに当てはまらないため調べてみました。可変長引数に浮動小数点の値を必要とする場合、その数を設定する必要があるらしいです。今回は整数型なので0を設定しています。
こちらもABIの「3.5.7 Variable Argument Lists」で説明されています。
main関数を終了する
main関数の戻り値としてeax
に0を設定し、スタックに積んでいたベースポインタの値をrbp
にポップ(Function epilogue)してきたらretでmain関数を終了します。
mov eax, 0
pop rbp
ret
add関数
次にadd関数内の処理を呼んでみます。
関数呼び出しをする際のお決まり(Function prologue)を実行する
push rbp
mov rbp, rsp
コールスタックに引数を格納する
渡された引数をコールスタックに格納しています。
メモ
[rbp-0x14]
という中途半端な場所からスタートしているのは「アライメント境界」というのが関係しているらしいです。
スタックには赤い四角で囲まれているように、引数が積まれています。
sum変数を初期化する
変数領域sumを0で初期化しています。
mov dword [var_4h], 0
引数の合計値を求める
渡された引数1から引数5までの値を足しています。
mov edx, dword [var_14h] ; 1
mov eax, dword [var_18h] ; 2
add edx, eax
mov eax, dword [var_1ch] ; 3
add edx, eax
mov eax, dword [var_20h] ; 4
add edx, eax
mov eax, dword [var_24h] ; 0
add eax, edx
sum変数に結果を格納する
足し算の結果を変数sumに当たるアドレス[rbp-0x4]に格納して、戻り値としてeaxレジスタに値を格納しています。
mov dword [var_4h], eax
mov eax, dword [var_4h]
add関数を終了する
add関数の戻り値としてeaxに0を設定して、スタックに積んでいたベースポインタの値をrbpにポップ(Function epilogue)してきたら、retでmain関数の呼び出し元に戻ります。
pop rbp
ret
最後までお読みいただきまして、ありがとうございます。