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. 結論

このチュートリアルでは、ファイルまたは文字列からマシンコードを分解する方法を見てきました。