Linuxでのマシンコードの分解
1. 概要
すべての実行可能ファイルには、プロセッサで実行できるマシンコードが含まれています。 しかし、別の形式に変換せずに人間がバイナリファイルを開いて読み取ることは意味がありません。
このチュートリアルでは、Linuxでマシンコードを読み取る方法を確認します。
2. 問題
2つの問題シナリオを見てみましょう。 マシンコードがファイルまたは文字列として保存されているとしましょう。
さまざまなツールを使用して分解する方法を見てみましょう。
2.1. ファイルからの読み取り
簡単なCプログラムを使用してバイナリファイルを作成します。 次に、そのバイナリのマシンコードをアセンブリ言語に変換する方法を確認できます。
Cプログラムからバイナリを作成しましょう。
$ cat test.c
#include
void main() {
int i = 0;
i += 20;
return;
}
$ gcc test.c -o test
$ ls
test test.c
$
上に示したように、変数iに20を追加するCプログラムがあります。 次に、Cプログラムをコンパイルしてバイナリを生成しました。 -c フラグを使用してコンパイルすると、拡張子が.oのオブジェクトファイルが出力されます。
$ gcc -c test.c
$ ls
test test.c test.o
$
これで、バイナリファイルとオブジェクトファイルの準備が整いました。
2.2. 文字列から読み取る
ランダムなシェルコードを分析して、それが何をするのかを確認したい場合があります。
いくつかのマシンコードを見てみましょう:
54: push esp
55: push ebp
90: nop
それでは、これを後で読み取って分解できるファイルに保存しましょう。
$ echo -ne '\x54\x55\x90' > code
$ ls
code test test.c test.o
$
上記のコマンドを使用して、シェルコード文字列をcodeという名前のバイナリファイルにエコーしました。
次に、これらのファイルを読み取る方法を確認します。
3. objdumpコマンドの使用
objdump コマンドは、通常、オブジェクトファイルとバイナリファイルを検査するために使用されます。 オブジェクトファイルのさまざまなセクション、それらの仮想メモリアドレス、論理メモリアドレス、デバッグ情報、シンボルテーブル、およびその他の情報を出力します。
一般的な使用法は次のとおりです。
objdump OPTIONS objfile ...
ここでは、このツールを使用してファイルを分解する方法を説明します。
3.1. ファイルからの読み取り
-d オプションを使用すると、バイナリのアセンブリコードを確認できます。
$ objdump -d test
test: file format elf64-x86-64
..
00000000000005fa <main>:
5fa: 55 push %rbp
5fb: 48 89 e5 mov %rsp,%rbp
5fe: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
605: 83 45 fc 14 addl $0x14,-0x4(%rbp)
609: 90 nop
60a: 5d pop %rbp
60b: c3 retq
60c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000000610 <__libc_csu_init>:
..
$
バイナリファイルには、実行可能ファイルの起動時に実行可能ファイルを適切にロードするためのアドレスとメタデータを含むELF形式のセクションが多数含まれています。 -dフラグを使用したため、すべての実行可能セクションが出力されます。 ここでは、他のセクションを削除した後、関連するmainセクションを確認できます。
メモリアドレス605の変数iに20(0x14)を追加するadd命令が表示されます。
これが分解であることを確認するために、Cプログラムを変更してコンパイルし、objdumpコマンドを再度実行して変更を確認します。
同様に、オブジェクトファイルに対して同じコマンドを実行して、コードを逆アセンブルできます。
$ objdump -d test.o
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
b: 83 45 fc 14 addl $0x14,-0x4(%rbp)
f: 90 nop
10: 5d pop %rbp
11: c3 retq
$
上記のように、バイナリファイルとは異なり、オブジェクトファイルにはメインセクションのみが表示されます。
デフォルトでは、ATTニーモニックでの分解が表示されます。 Intelに変更する必要がある場合は、-Mオプションを使用できます。
$ objdump -d test.o -M intel
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
b: 83 45 fc 14 add DWORD PTR [rbp-0x4],0x14
f: 90 nop
10: 5d pop rbp
11: c3 ret
$
2.3. 文字列から読み取る
文字列をファイルに保存したら、次のコマンドを使用して分解を表示できます。
$ objdump -D -b binary -m i386 code
code: file format binary
Disassembly of section .data:
00000000 <.data>:
0: 54 push %esp
1: 55 push %ebp
2: 90 nop
$
上記のように、これは生のファイルであるため、正しく分解するためにobjdumpコマンドに詳細情報を提供する必要があります。
上記のコマンドで使用されるオプションは次のとおりです。
- -D :すべてのセクションを分解します
- -b :オブジェクトコード形式。binaryと言います。
- -m :コードがどのアーキテクチャーであるかは、i386と言います。
その結果から、ファイル内のシェルコードが出力に正しく出力されていることがわかります。
3. gdbコマンドの使用
何かをデバッグする必要がある場合は、gdbが頼りになるツールです。 gdb、を使用して、コードを逆アセンブルすることもできます。
$ gdb test
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000000005fa <+0>: push %rbp
0x00000000000005fb <+1>: mov %rsp,%rbp
0x00000000000005fe <+4>: movl $0x0,-0x4(%rbp)
0x0000000000000605 <+11>: addl $0x14,-0x4(%rbp)
0x0000000000000609 <+15>: nop
0x000000000000060a <+16>: pop %rbp
0x000000000000060b <+17>: retq
End of assembler dump.
(gdb) q
$
上記のように、バイナリを gdb にロードし、 main関数でdisassembleコマンドを実行して、アセンブリコードを確認しました。
4. ndisasmコマンドの使用
ndisasm ユーティリティには、nasmパッケージが付属しています。 これは主にシェルコードを分解するために使用されます。 バイナリファイルを分解できますが、セクションが正しく表示されません。 したがって、構造を理解することは非常に困難です。
一般的な使用法は次のとおりです。
ndisasm [-b16 | -b32] filename
以前にファイルに保存したマシンコードの文字列を分解するためにそれを使用する方法の例を見てみましょう。
$ ndisasm -b32 code
00000000 54 push esp
00000001 55 push ebp
00000002 90 nop
$
上に示したように、プロセッサモードを32ビットとして渡し、そのためのアセンブリコードを生成しました。
5. 結論
このチュートリアルでは、ファイルまたは文字列からマシンコードを分解する方法を見てきました。