機械語とニーモニック
レジスタ
レジスタ(register)は、演算や実行状態が保持されるCPU内部のメモリです。x86アーキテクチャのCPUは、8個の汎用レジスタ(汎用ダブルワードレジスタ)、6個のセグメントレジスタ、1個のフラグレジスタ、1個の命令ポインタを持ちます。
機械語
CPUが唯一理解できるのは機械語です。CPUに機械語で命令することで、任意の 処理を実行させることができます。機械語はCPUの種類によって異なります。 機械語は2進数ですが、1と0だけで表記するとわかりづらく間違いやすいので、 一般的には16進数で表記します。機械語では、命令のことをオペレータ(operator)、命令の対象となるレジスタ、メモリアドレス、即値のことをオペランド(operand)といいます。
Intel社のx86 CPUで、「eaxレジスタとebxレジスタの値を加算した値をeax
レジスタに格納する」機械語命令は、0x01 0xd8です(0xは16進数の接頭辞)。
CPUの動作
機械語の命令は、CPUが次のような処理を繰り返すことで実行されます。① プログラムカウンタの示すメモリアドレスからインストラクションを読み込む
② インストラクションを実行し、レジスタやメモリの値の更新などを行う
③ 次に実行する命令のアレスをプログラムカウンタに設定する
④ ①に戻る
x86 CPUのプログラムカウンタはeipレジスタです。
② の実行によって、フラグレジスタ(= eflagsレジスタの値が変更されます。)
ニーモニック
機械語で直接プログラムを作成するのは大変なので、その代わりにアセンブリ言語 (assembly language)を用いてプログラミングするのが一般的です。アセンブリ言語では、機械語に対応したニーモニック(mnemonic)という符号を使用します。
アセンブラと逆アセンブラ
アセンブリ言語で記述したプログラムを機械語に変換するツールをアセンブラ (assembler)といいます、アセンブラとしては、asコマンドやnasmコマンドが よく使われます。 その反対に、機械語をアセンブリ言語(ニーモニック)に変換する ツールを逆アセンブラ(disassembler)といいます。逆アセンブラとしては、 objdumpコマンドがよく使われます。 バイナリ解析では対象バイナリの機械語を逆アセンブルして、ニーモニックから 解析します。ntel形式とAT&T形式
ニーモニックには、Intel形式とAT&T形式があります。 それぞれの形式には、次のような特徴があります。Intel形式
・オペランドは to、fromの順になる ・オペランドには特に記号を付加しない ・byte ptr、word ptr、dword ptrのようなptr演算子がある ・アドレス式は [ ] で囲むAT&T形式
・オペランドは from、toの順になる ・オペランドがレジスタには%、即値には$を付加する ・movb、movw、movlのようにオペレーションサフィックスを用いる ・アドレス式は ( ) で囲む$ objdump -d –M intel address ※実行形式ファイルaddressをIntel形式のニーモニックで逆アセンブル
◆Intel形式
address: ファイル形式 elf32-i386
セクション .text の逆アセンブル:
※アドレス↓ 機械語↓ ニーモニック(Intel形式)↓※
08048080 <start>:
8048080: bb b8 90 04 08 mov ebx,0x80490b8
8048085: c6 03 41 mov BYTE PTR [ebx],0x41
8048088: 66 c7 43 01 42 43 mov WORD PTR [ebx+0x1],0x4342
804808e: c7 43 03 44 45 46 47 mov DWORD PTR [ebx+0x3],0x47464544
8048095: b8 04 00 00 00 mov eax,0x4
804809a: bb 01 00 00 00 mov ebx,0x1
804809f: b9 b8 90 04 08 mov ecx,0x80490b8
80480a4: ba 08 00 00 00 mov edx,0x8
80480a9: cd 80 int 0x80
80480ab: b8 01 00 00 00 mov eax,0x1
80480b0: bb 00 00 00 00 mov ebx,0x0
80480b5: cd 80 int 0x80
◆AT&T形式のニーモニック
$ objdump -d address ※実行形式ファイルaddressをAT&T形式のニーモニックで逆アセンブル
※オプション -M att は省略可能
address: ファイル形式 elf32-i386
セクション .text の逆アセンブル:
08048080 <start>:
※アドレス↓ 機械語↓ ニーモニック(Intel形式)↓※
8048080: bb b8 90 04 08 mov $0x80490b8,%ebx
8048085: c6 03 41 movb $0x41,(%ebx)
8048088: 66 c7 43 01 42 43 movw $0x4342,0x1(%ebx)
804808e: c7 43 03 44 45 46 47 movl $0x47464544,0x3(%ebx)
8048095: b8 04 00 00 00 mov $0x4,%eax
804809a: bb 01 00 00 00 mov $0x1,%ebx
804809f: b9 b8 90 04 08 mov $0x80490b8,%ecx
80480a4: ba 08 00 00 00 mov $0x8,%edx
80480a9: cd 80 int $0x80
80480ab: b8 01 00 00 00 mov $0x1,%eax
80480b0: bb 00 00 00 00 mov $0x0,%ebx
80480b5: cd 80 int $0x80
x86ニーモニックのマニュアル
PDF版マニュアル
「IA-32 インテル® アーキテクチャソフトウェア・デベロッパーズ・マニュアル」 は、Intel社がPDF版で無償で提供しているマニュアルです。インターネットから ダウンロードすることができます。マニュアルは、上巻、中巻A、中巻B、下巻の4冊から構成されます。
中巻AではA~M、中巻BではN~Zで始まるニーモニックが解説されています。
GDB
デバッガ
プログラムの開発時に、プログラムに存在しているバグ(誤りや欠陥)を発見して、 プログラムを修正する作業を支援するソフトウェアをデバッガ(debugger)と いいます。デバッガはバイナリ解析にも使われます。 デバッガとしては、IDA、OllyDBG、Ghidraなどがよく使われます。GDB
GDB(GNU Debugger)は、GNUプロジェクトが開発して提供しているCUIの デバッガです。Unix系システムの多くで利用可能なデバッガであり、Linuxには 標準でインストールされています。CUIであるため、キーボードからコマンドを 入力して操作します。GDBの起動と終了
GDBを起動するためにはgdbコマンドを実行します。引数に解析対象の実行形式ファイルを指定しま す。-q オプションを指定することで、起動時のバナー表示を抑制できます。 起動すると、対話型のプロンプトである (gdb) が表示されます。 ※解析対象ファイルがhelloの場合の例です。 ``` $ gdb –q hello Reading symbols from hello...(no debugging symbols found)...done. (gdb) ``` GDBを終了するためにはquitコマンドを使用します。省略形はqです。 ※[Ctrl]+[d]でも終了できます。 ``` (gdb) q $ ```解析対象プログラムの実行
解析対象プログラムを実行するためにはrunコマンドを実行します。省略形はrです。 ※解析対象ファイルがhelloの場合の例です$ gdb –q hello
Reading symbols from hello...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/user/day1/exercises/hello
hello, world
[Inferior 1 (process 24554) exited normally] ←プログラムは正常終了した
プロセスのアタッチ
実行中のプロセスを対象にする場合には、解析対象ファイルを指定せずにGDBを起動して、attachコ マンドでプロセスIDを指定します。 ※ sleep コマンドをバックグラウンドで実行して、解析対象のプロセスにする場合の例です。(gdb) shell ps -a
PID TTY TIME CMD
24458 pts/8 00:00:00 sleep ←実行中の解析対象プロセス
24464 pts/8 00:00:00 gdb
24466 pts/8 00:00:00 ps
(gdb) at 24458
Attaching to process 24458
Reading symbols from /bin/sleep...(no debugging symbols found)...done.
Reading symbols from /lib/i386-linux-gnu/libc.so.6...Reading symbols from
/usr/lib/debug//lib/i386-linux-gnu/libc-2.23.so...done.
done.
Reading symbols from /lib/ld-linux.so.2...Reading symbols from
/usr/lib/debug//lib/i386-linux-gnu/ld-2.23.so...done.
done.
0xb776cbd1 in __kernel_vsyscall ()
ブレークポイントの設定
プログラムの実行を一時停止するアドレスをブレークポイント(breakpoint)といいます。 プログラムの実行中にブレークポイントに到達すると、プログラムは停止します。ブレークポイントはbreakコマンドで指定します。省略形はbです。アドレスの前には「」を付加します。アドレスの代わりにシンボルを指定することも可能です(アドレスに「」を付加しないとシンボルとみなされる)。
設定されているブレークポイントは info break コマンドで確認できます。省略形はi b です。
※ 解析対象ファイルhelloのエントリポイントにブレークポイントを設定する例です。
C言語で作成されたプログラムの場合には、startコマンドでmain関数の開始をブレークポイントとして停止することができます。
(gdb) b *0x8048054
Breakpoint 1 at 0x8048054
(gdb) r
Starting program: /home/user/day1/exercises/hello
Breakpoint 1, 0x08048054 in _start () ←ブレークポイントで停止
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048054 <_start>
breakpoint already hit 1 time
逆アセンブル
プログラムの実行コードを逆アセンブルするにはdisassembleコマンドを使用します。省略形はdisasです。 デフォルトでAT&T形式で表示されます。set disassembly-flavor intel で表示をIntel形式に変更できます。 次に実行される命令のアドレスに、「=>」のマーカーが表示されます。このアドレスはeipレジスタに格納されています。※ 恒久的にIntel形式にする場合には、GDBの初期化ファイル~/.gdbinitに設定します。
(gdb) disas
Dump of assembler code for function _start:
=> 0x08048054 <+0>: mov $0x4,%eax ←次に実行されるアドレス
0x08048059 <+5>: mov $0x1,%ebx
0x0804805e <+10>: mov $0x8048076,%ecx
0x08048063 <+15>: mov $0xd,%edx
0x08048068 <+20>: int $0x80
0x0804806a <+22>: mov $0x1,%eax
0x0804806f <+27>: mov $0x0,%ebx
0x08048074 <+32>: int $0x80
End of assembler dump.
ステップ実行
1命令づつ実行することをステップ実行といいます。実行命令がcall命令(関数/サブルーチン呼び出し)の場合、呼び出し先に入る(=ジャンプする)ことをステップイン、呼び出し先に入らずに次の行に移ることをステップオーバーといいます。ステップインはstepコマンド、ステップオーバーはnextコマンドを使用します。それぞれの省略形はsとnです。
ソースコードレベルではなく、機械語レベルでのステップ実行にはstepiとnextiを使用します。それぞれ省略形はsiとniです。
Breakpoint 1, 0x08048054 in _start () ←アドレス0x8048054で一時停止中
(gdb) ni
0x08048059 in _start ()
(gdb) disas
Dump of assembler code for function _start:
0x08048054 <+0>: mov $0x4,%eax
=> 0x08048059 <+5>: mov $0x1,%ebx ←1ステップ進んだ
0x0804805e <+10>: mov $0x8048076,%ecx
0x08048063 <+15>: mov $0xd,%edx
0x08048068 <+20>: int $0x80
0x0804806a <+22>: mov $0x1,%eax
0x0804806f <+27>: mov $0x0,%ebx
0x08048074 <+32>: int $0x80
End of assembler dump.
レジスタの表示
レジスタを表示するにはprintコマンドを使用します。省略形はpです。レジスタ名の前には$を付加します。 printコマンドはレジスタに格納されている値を10進数で表示します。16進数で表示したい場合には、出力フォーマットとして /x を指定します。(gdb) p $eax
$1 = 4
(gdb) p /x $eax
$2 = 0x4
全てのレジスタの値を表示するには info register コマンドを使用します。省略形はi rです。
(gdb) i r
eax 0x4 4
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xbffffc40 0xbffffc40
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
~(略)~
メモリ内容の表示
メモリの内容はxコマンドで表示できます。「/」 の後ろにフォーマットの指定、そしてその後にアドレスを指定します。フォーマットには、
表示するユニットの個数、
ユニットのサイズ(b=バイト、h=ハーフワード※注1、w=ワード※注2、g=巨大ワード※注3)、
表示方法(d=10進数、x=16進数、s=文字列、c=文字、i=インストラクション)を指定できます。
※注1 ハーフワードは16bit ※注2 ワードは32bit ※注3 巨大ワードは64bit
■メモリ内容の表示
(gdb) disas
Dump of assembler code for function _start:
0x08048054 <+0>: mov $0x4,%eax
0x08048059 <+5>: mov $0x1,%ebx
0x0804805e <+10>: mov $0x8048076,%ecx
0x08048063 <+15>: mov $0xd,%edx
=> 0x08048068 <+20>: int $0x80
0x0804806a <+22>: mov $0x1,%eax
0x0804806f <+27>: mov $0x0,%ebx
0x08048074 <+32>: int $0x80
End of assembler dump.
(gdb) x/s 0x8048076 ←アドレスの指定には*は不要
0x8048076: "hello, world¥n"
(gdb) x/s $ecx ←レジスタも指定可能
0x8048076: "hello, world¥n“
(gdb) x/s $ecx+7 ←ecxレジスタの値に7を加算したアドレス
0x804807d: "world¥n“
(gdb) x/4i $eip ←プログラムカウンタのアドレスから4つの命令をニーモニックで表示
=> 0x8048054 <_start>: mov $0x4,%eax
0x8048059 <_start+5>: mov $0x1,%ebx
0x804805e <_start+10>: mov $0x8048076,%ecx
0x8048063 <_start+15>: mov $0xd,%edx
(gdb) x/14c $ecx ←ecxレジスタの示すアドレスから14バイト分を10進数と文字で表示
0x8048076: 104 'h' 101 'e' 108 'l' 108 'l' 111 'o' 44 ',' 32 ' ' 119 'w'
0x804807e: 111 'o' 114 'r' 108 'l' 100 'd' 10 '¥n' 0 '¥000'
(gdb) x/4xw $esp ←スタックポインタのアドレスから4ワードを16進数で表示
0xbffffc40: 0x00000001 0xbffffd64 0x00000000 0xbffffd93
(gdb) x ←アドレスを省略すると前回指定したアドレスの続きが使われる
0xbffffc50: 0xbffffda7
レジスタやメモリ値の変更
レジスタやメモリの値はsetコマンドで変更できます。(gdb) disas
Dump of assembler code for function _start:
0x08048054 <+0>: mov $0x4,%eax
0x08048059 <+5>: mov $0x1,%ebx
0x0804805e <+10>: mov $0x8048076,%ecx
0x08048063 <+15>: mov $0xd,%edx
0x08048068 <+20>: int $0x80
0x0804806a <+22>: mov $0x1,%eax
0x0804806f <+27>: mov $0x0,%ebx ←ebxレジスタに0をセット
=> 0x08048074 <+32>: int $0x80
End of assembler dump.
(gdb) set $ebx=1 ←ebxレジスタの値を変更
(gdb) c
Continuing.
[Inferior 1 (process 29218) exited with code 01] ←終了ステータスが1になっている
(gdb)disas
Dump of assembler code for function _start:
0x08048054 <+0>: mov $0x4,%eax
0x08048059 <+5>: mov $0x1,%ebx
0x0804805e <+10>: mov $0x8048076,%ecx
0x08048063 <+15>: mov $0xd,%edx
=> 0x08048068 <+20>: int $0x80
0x0804806a <+22>: mov $0x1,%eax
0x0804806f <+27>: mov $0x0,%ebx
0x08048074 <+32>: int $0x80
End of assembler dump.
(gdb) x/s 0x8048076
0x8048076: "hello, world¥n"
(gdb) set {char}0x8048076=‘H’ ←アドレス0x8048076の値を文字’H’に変更
(gdb) set {char}(0x8048076+7)='W' ←アドレス0x804807dの値を文字’W’に変更
(gdb) x/s 0x8048076
0x8048076: "Hello, World¥n"
(gdb) c
Continuing.
Hello, World
実行アドレスをジャンプ
プログラムは、プログラムカウンタ(=eipレジスタ)のアドレスにある命令が実行されます。jump コマンドで任意のアドレスにジャンプすることができます。省略形はjです。(gdb) disas
Dump of assembler code for function _start:
0x08048054 <+0>: mov $0x4,%eax
0x08048059 <+5>: mov $0x1,%ebx
0x0804805e <+10>: mov $0x8048076,%ecx
0x08048063 <+15>: mov $0xd,%edx
=> 0x08048068 <+20>: int $0x80 ←次に実行される命令のアドレス
0x0804806a <+22>: mov $0x1,%eax
0x0804806f <+27>: mov $0x0,%ebx
0x08048074 <+32>: int $0x80
End of assembler dump.
(gdb) print/x $eip ←プログラムカウンタを表示
$1 = 0x8048068
(gdb) j *0x0804806a
Continuing at 0x804806a. ←スキップされたため、”hello, world”は表示されない
[Inferior 1 (process 29426) exited normally]