はじめに
バイナリファイルの解析はアプリケーションやファームウェアのデバッグ、マルウェアの解析等目的に応じて手段となるツールが異なります。
本記事はバイナリファイルの解析を行うために必要となる基本的な知識や、リバースエンジニアリングツールの概要について記載しています。
リバースエンジニアリングツールはIDA Proなど有料のプロダクトがありますが、本記事ではフリーで利用可能なツールに着目しています。
バイナリファイルとは
コンピュータが処理する情報(データ)は基本的にテキストファイルと、バイナリファイルに大別できます。
テキストファイルはテキストエディタなどを使用して何らかの文字コードでエンコードされたテキストファイルのことです。テキストファイルに書かれた内容は人間にしか理解できません。
バイナリファイルは2進数の0と1が並んだビット列で表現されたファイルです。画像、動画、音声、実行形式のアプリケーションファイル等、テキストファイル以外は全てバイナリファイルに区別できます。
バイナリファイルの解析
バイナリファイルの操作コマンド
バイナリファイルを操作するための手段として、OS毎にバイナルファイルの操作コマンドが用意されいます。
本記事ではLinuxを例に、デバッグなどの調査で使われることが多いコマンドについて一部解説します。
objdump
高級言語であるC言語で記述されたソースコードだけでは、プログラムとして実行することができません。何故ならば、コンピュータは機械語(マシン語)1しか理解できないためです。
コンピュータが命令の記述されたプログラムを実行するためには、CPUが命令を理解できるように機械語である必要があります。そのため、命令を記述したソースコードはコンパイラを用いてコンパイルを行ない、機械語に翻訳します。従って、コンパイル実行後、ソースコードはCPUが理解できるオブジェクトコードの形式に変換されます。
また、オブジェクトコードだけではプログラムが実行可能な状態ではないため、更にオブジェクトコードに対して、必要なライブラリ群をリンクさせることにより、Windowsで言うexeファイルなど実行可能なプログラム形式に変換されます。
objdump
コマンドはオブジェクトファイルの情報を出力するためのコマンドです。
ここでは、下記のサンプルプログラムがどの様にコンパイルされいてるか確認します。
このソースコードはHello, world
を10回出力するだけの単純なプログラムです。
#include <stdio.h>
int main()
{
int i;
for (i = 0;i < 10; i++)
{
printf("Hello, world\n");
}
return 0;
}
Linux環境で上記ソースコードをGCC2でコンパイルすると、a.out
ファイルが生成されます。
なお、出力後のファイル名については-o 任意のファイル名
で変更することができます。
a.out
フォーマットの形式は2019年にLinus Torvaldsよりa.out: remove core dumping supportでサポート廃止のアナウンスが行われている通りに、現在は非推奨になっています。本記事ではバイナリの学習目的のため題材として取り扱っています。
- 参考:a.outフォーマット
以下のコマンドを実行すると、バイナリファイルの中身を表示します。また、以下の例ではオプションを付けて、Intelのシンタックス形式で見やすいように出力しています。
$ objdump -M intel -D a.out | grep -A20 main.:
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push rbp
114e: 48 89 e5 mov rbp,rsp
1151: 48 83 ec 10 sub rsp,0x10
1155: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
115c: eb 10 jmp 116e <main+0x25>
115e: 48 8d 3d 9f 0e 00 00 lea rdi,[rip+0xe9f] # 2004 <_IO_stdin_used+0x4>
1165: e8 e6 fe ff ff call 1050 <puts@plt>
116a: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1
116e: 83 7d fc 09 cmp DWORD PTR [rbp-0x4],0x9
1172: 7e ea jle 115e <main+0x15>
1174: b8 00 00 00 00 mov eax,0x0
1179: c9 leave
117a: c3 ret
117b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
0000000000001180 <__libc_csu_init>:
1180: f3 0f 1e fa endbr64
1184: 41 57 push r15
1186: 4c 8d 3d 2b 2c 00 00 lea r15,[rip+0x2c2b] # 3db8 <__frame_dummy_init_array_entry>
見方として左から、メモリ空間の位置情報を表すアドレス、16進数のバイト郡がCPUに対する機械語の命令、右側にアセンブリ言語で表記した命令が出力されます。
メモリ空間から指定されたアドレスのデータを読み込むためには、アドレスの指定が必要になります。CPUはメモリアドレスを指定しながら逐次、命令を処理します。なお、アドレス空間の大きさはCPUアーキテクチャによって異なります。
16進数のバイト郡では人間から見て分かりにくいため、右側のアセンブリ言語で表記した命令が出力されています。そのため、アセンブリ言語もCPUアーキテクチャによって異なります。
gdb
CPUはデータの読み書きなどの処理を行うために、一時的な記憶領域としてレジスタを使用します。また、レジスタは特別な機能を持たない汎用レジスタと専用レジスタなどが存在し、それぞれ役割が異なります。
レジスタの構成を理解することは、CPUがどの様に命令を実行しているかを理解するのに役立ちます。GNU開発ツールであるGDBデバッガはレジスタの内容を表示することができます。
以下のコマンドを実行すると、main()関数に対してブレークポイントを設定し、main()関数が実行される直前で処理が停止します。また、run
実行後、レジスタの情報を出力します。
$ gdb -q ./a.out
ubuntu@ubuntu:~$ gdb -q ./a.out
Reading symbols from ./a.out...
(gdb) break main
Breakpoint 1 at 0x1149: file firstprog.c, line 4.
(gdb) run
Starting program: /home/ubuntu/a.out
Breakpoint 1, main () at firstprog.c:4
4 {
(gdb) info registers
rax 0x555555555149 93824992235849
rbx 0x555555555180 93824992235904
rcx 0x555555555180 93824992235904
rdx 0x7fffffffe528 140737488348456
rsi 0x7fffffffe518 140737488348440
rdi 0x1 1
rbp 0x0 0x0
rsp 0x7fffffffe428 0x7fffffffe428
r8 0x0 0
r9 0x7ffff7fe0d50 140737354009936
r10 0x0 0
r11 0x0 0
r12 0x555555555060 93824992235616
r13 0x7fffffffe510 140737488348432
r14 0x0 0
r15 0x0 0
rip 0x555555555149 0x555555555149 <main>
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb) quit
A debugging session is active.
Inferior 1 [process 11722] will be killed.
Quit anyway? (y or n) y
次の例では、CPUに対する次に実行する命令(Instruction Pointer)を格納しているレジスタの情報を出力しています。info register
コマンドの引数に、レジスタ名を指定することで実行中のアドレスの情報を表示できます。なお、レジスタ名はCPUのアーキテクチャによって異なります。
(gdb) info register rip
rip 0x555555555149 0x555555555149 <main>
また、コマンドは省略可能なため、次のコマンドは同じ情報を出力します。
(gdb) i r rip
rip 0x555555555149 0x555555555149 <main>
GDBデバッガにはメモリの内容をチェックするためのexamine
コマンド(省略形:x)が用意されています。
また、メモリにデータを格納する際にバイトの並べ方として、エンディアンという仕組みが存在します。現在のデバッグツールやコンパイラは自動的に考慮してくれいることが多いですが、メモリを直接操作する場合は、このような仕組みにも注意が必要です。
strings
strings
コマンドはバイナリファイルを指定して実行すると、読み取った文字列を出力します。デフォルトでは、4文字以上連続している文字列を出力します。
- 実行例
$ strings <ファイル名>
cmp
cmp
コマンドは指定したファイルをバイト単位で比較することができます。用途としてバイナリファイルの比較に利用できます。
- 実行例
$ cmp <ファイル名A> <ファイル名B>
ltrace
ltrace
コマンドはコマンドラインの診断およびデバッグのツールです。ELFを指定して実行すると、呼び出しされた共有ライブラリーを出力することができます。使用方法はstrace
コマンドと良く似ていて、呼び出された動的ライブラリ3を確認することができます。そのため、プログラムのデバッグにも利用できます。
- 実行例
$ ltrace <ファイル名>
kali@kali:~$ ltrace ./a.out
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
puts("Hello, world"Hello, world
) = 13
+++ exited (status 0) +++
上記の場合、Hello, world
を10回出力するために、puts
関数が10回呼びされていることが分かります。
readelf
readelf
コマンドはELFを指定して実行すると、ELF形式のオブジェクトファイルに関する情報を表示します。
- 実行例
$ readelf -a <ファイル名>
リバースエンジニアリングツール
リバースエンジニアリングはハードウェア及び、ソフトウェアの動作を解析するなどして、製品の構造を分析して明らかにすることが目的です。
ソフトウェアの場合はバイナリファイルを基に、人間が理解しやすいアセンブリ言語による変換する逆アセンブルや、開発時に用いられたプログラミング言語に戻して変換するデコンパイルなどの手法が存在します。
Ghidra
GhidraはNSAの研究局によって開発されたリバースエンジニアリングツールです。Ghidraのリポジトリより入手できます。
リポジトリのREADME.mdを参照し、事前にJDK 11 64-bit
をインストールます。
また、環境変数のPATHを設定し、既存のjavaが存在し、バージョンが違う場合はインストールしたJDKが存在する場合は先に通すように設定します。
準備完了後、以下のコマンドを実行して起動します。なお、本記事のMac環境では/usr/local/bin/ghidraRun
をシンボリックリンクを作成し、/opt/
配下に格納したファイルにリンクさせて起動しています。
$ ghidraRun
Ghidra起動後、Projectフォルダーを作成し、バイリナファイルを開くことで解析を始めることができます。
逆コンパイル実行時にDecompiler: Unable to initialize the DecompilerInterface: Could not find decompiler executable
の様なメッセージが出力される場合、逆コンパイルで使用されるコンポーネントが見つからないため、出力されています。原因としてGhidraの初回起動時に、macOSのセキュリティ機構によってブロックされた際に削除した可能性があります。
JADX
JADXはAndroidのApkファイルなどをデコンパイルしてJavaソースコードを生成するためのコマンドラインおよびGUIツールです。下記の手順でビルドして使用します。
- ソースのダウンロード及びビルド
$ git clone https://github.com/skylot/jadx
$ cd jadx/
$ ./gradlew dist
Downloading https://services.gradle.org/distributions/gradle-7.4.1-bin.zip
...........10%...........20%...........30%...........40%...........50%...........60%...........70%...........80%...........90%...........100%
Welcome to Gradle 7.4.1!
Here are the highlights of this release:
- Aggregated test and JaCoCo reports
- Marking additional test source directories as tests in IntelliJ
- Support for Adoptium JDKs in Java toolchains
For more details see https://docs.gradle.org/7.4.1/release-notes.html
Starting a Gradle Daemon (subsequent builds will be faster)
> Configure project :
jadx version: dev
The CreateStartScripts.mainClassName property has been deprecated. This is scheduled to be removed in Gradle 8.0. Please use the mainClass property instead. See https://docs.gradle.org/7.4.1/dsl/org.gradle.jvm.application.tasks.CreateStartScripts.html#org.gradle.jvm.application.tasks.CreateStartScripts:mainClassName for more details.
> Task :jadx-core:compileJava
注意:一部の入力ファイルは推奨されないAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
> Task :jadx-gui:compileJava
注意:/Users/kenichikato/Documents/Code/GitHub/jadx/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.javaは推奨されないAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
注意:/Users/kenichikato/Documents/Code/GitHub/jadx/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.javaの操作は、未チェックまたは安全ではありま
BUILD SUCCESSFUL in 2m 23skedオプションを指定して再コンパイルしてください。
ビルド成功後、以下の実行ファイルを実行すると、GUIのツールが起動します。ツール起動後、デコンパイルしたいファイルを選択するとデコンパイルされたJavaのソースコードが表示されます。以下の例ではapkファイルを基にデコンパイルされたソースコードが確認できます。
$ build/jadx/bin/jadx-gui
radare2
radare2
コマンドはバイナリコードの逆アセンブルやデバッグを行うことができます。
radare2のリポジトリから取得できます。
以下の例では分析と書き込みモードを指定して開いています。
- 実行例
$ r2 -A -w <バイナリファイル名>
kali@kali:~$ r2 -A -w ./a.out
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
?
を入力するとヘルプを表示します。
[0x00001050]> ?
Usage: [.][times][cmd][~grep][@[@iter]addr!size][|>pipe] ; ...
Append '?' to any char command to get detailed help
Prefix with number to repeat command N times (f.ex: 3x)
| %var=value alias for 'env' command
| *[?] off[=[0x]value] pointer read/write data/values (see ?v, wx, wv)
| (macro arg0 arg1) manage scripting macros
| .[?] [-|(m)|f|!sh|cmd] Define macro or load r2, cparse or rlang file
| _[?] Print last output
| =[?] [cmd] send/listen for remote commands (rap://, raps://, udp://, http://, <fd>)
| <[...] push escaped string into the RCons.readChar buffer
| /[?] search for bytes, regexps, patterns, ..
| ![?] [cmd] run given command as in system(3)
| #[?] !lang [..] Hashbang to run an rlang script
| a[?] analysis commands
| b[?] display or change the block size
| c[?] [arg] compare block with given data
| C[?] code metadata (comments, format, hints, ..)
| d[?] debugger commands
| e[?] [a[=b]] list/get/set config evaluable vars
| f[?] [name][sz][at] add flag at current address
| g[?] [arg] generate shellcodes with r_egg
| i[?] [file] get info about opened file from r_bin
| k[?] [sdb-query] run sdb-query. see k? for help, 'k *', 'k **' ...
| l [filepattern] list files and directories
| L[?] [-] [plugin] list, unload load r2 plugins
| m[?] mountpoints commands
| o[?] [file] ([offset]) open file at optional address
| p[?] [len] print current block with format and length
| P[?] project management utilities
| q[?] [ret] quit program with a return value
| r[?] [len] resize file
| s[?] [addr] seek to address (also for '0x', '0x1' == 's 0x1')
| t[?] types, noreturn, signatures, C parser and more
| T[?] [-] [num|msg] Text log utility (used to chat, sync, log, ...)
| u[?] uname/undo seek/write
| v visual mode (v! = panels, vv = fcnview, vV = fcngraph, vVV = callgraph)
| w[?] [str] multiple write operations
| x[?] [len] alias for 'px' (print hexadecimal)
| y[?] [len] [[[@]addr Yank/paste bytes from/to memory
| z[?] zignatures management
| ?[??][expr] Help or evaluate math expression
| ?$? show available '$' variables and aliases
| ?@? misc help for '@' (seek), '~' (grep) (see ~??)
| ?>? output redirection
| ?|? help for '|' (pipe)
afl
を入力すると、バイナリファイルで用意されている関数のリストを表示します。
[0x00001050]> afl
0x00001050 1 42 entry0
0x00001080 4 41 -> 34 sym.deregister_tm_clones
0x000010b0 4 57 -> 51 sym.register_tm_clones
0x000010f0 5 57 -> 50 entry.fini0
0x00001130 1 5 entry.init0
0x00001000 3 23 sym._init
0x000011d0 1 1 sym.__libc_csu_fini
0x000011d4 1 9 sym._fini
0x00001170 4 93 sym.__libc_csu_init
0x00001135 4 46 main
0x00001030 1 6 sym.imp.puts
s main
を入力し、main関数に移動後、pdf
を入力すると逆アセンブルして表示することができます。
[0x00001050]> s main
[0x00001135]> pdf
; DATA XREF from entry0 @ 0x106d
┌ 46: int main (int argc, char **argv, char **envp);
│ ; var signed int64_t var_4h @ rbp-0x4
│ 0x00001135 55 push rbp
│ 0x00001136 4889e5 mov rbp, rsp
│ 0x00001139 4883ec10 sub rsp, 0x10
│ 0x0000113d c745fc000000. mov dword [var_4h], 0
│ ┌─< 0x00001144 eb10 jmp 0x1156
│ │ ; CODE XREF from main @ 0x115a
│ ┌──> 0x00001146 488d3db70e00. lea rdi, qword str.Hello__world ; 0x2004 ; "Hello, world" ; const char *s
│ ╎│ 0x0000114d e8defeffff call sym.imp.puts ; int puts(const char *s)
│ ╎│ 0x00001152 8345fc01 add dword [var_4h], 1
│ ╎│ ; CODE XREF from main @ 0x1144
│ ╎└─> 0x00001156 837dfc09 cmp dword [var_4h], 9
│ └──< 0x0000115a 7eea jle 0x1146
│ 0x0000115c b800000000 mov eax, 0
│ 0x00001161 c9 leave
└ 0x00001162 c3 ret
v
を入力するとVisualモードで表示させることができます。以下はGUI環境で実行している場合の例です。
おわりに
リバースエンジニアリングを知ることは世界が広がります。