前回 は、「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」を読んで、CTF の各ジャンルごとに使われている技術やツールについて、調べたり、実際に使ってみたりしました。
今回は、x86-64 ELF(Linux)のアセンブラを理解していきます。また、よく使う GDBコマンドや、バイナリに対してよく使うコマンド、x86-64 のよく使う命令を書きとめておこうと思います。
それでは、やっていきます。
参考文献
Ghidra の解説が主ですが、冒頭に、x86、x86-64 のアーキテクチャ、アセンブラが解説されています。今回は、この解説をもとに書いています。
GDB の本は少ないのですが、CQ出版の古い書籍は、よくまとまっていると思います。GDB のコマンドについても、ある程度、書いてくれているのですが、短縮版も書いててほしかったです。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
セキュリティの記事一覧
普通の GDB はデバッグするには不便なので、最初に gdb-peda を導入しておきます。
環境は、VirtualBox+ParrotOS 6.1 です。
それでは、やっていきます。
gdb-pedaの導入方法
gdb-peda の公式の GitHub は以下です。
github.com
Installation に、導入方法が書かれていますので、それに従います(インストール先は変更しています)。
$ git clone https://github.com/longld/peda.git
$ echo "source ~/Downloads/peda/peda.py" >> ~/.gdbinit
では、gdb-peda を導入した状態で、GDB を起動してみます。
$ gdb -q exec_me_revenge
この後、start を実行すると、以下のように、レジスタ、コード、スタックが常に表示されるようになります。とても便利ですね。
gdb-peda の導入方法は以上です。
pwndbgの導入方法
gdb-peda では、ヒープ領域の情報が表示できませんでした(私の環境だけかもしれません)。そこで、よく聞く pwndbg を導入したいと思います。
pwndbg の公式サイトは以下です。
github.com
公式サイトの導入手順通りに導入してみます。
たくさんインストールされたように見えます。
$ git clone https://github.com/pwndbg/pwndbg
$ cd pwndbg/
$ ./setup
完了したので、早速 GDB を起動してみましたが、エラーが出ます。gdb-pedaとの併用はできないようです。.gdbinit を編集します。gdb-peda と pwndbg は両方とも、.gdbinit に 1行ずつ書き込んでいるだけなので、gdb-peda の方の source をコメントアウトしました。すると、エラーは出なくなりました。
$ nano ~/.gdbinit
少し使ってみましたが、このままだと、teraterm で使うには厳しいですね。ASCIIコード以外の文字が使われているので、いたるところで、?になります。ASCIIコードだけで表示する方法もあると思うので、もうちょっと調べてみます。
簡単なプログラムでx86-64 ELFをGDBでデバッグを開始してみる
簡単な C言語のプログラムを自分で書いてみました。これを使って、GDB で動かしながら、x86-64 ELF の動作を理解していきます。
使用する簡単なC言語プログラム
main関数から、sub関数を呼び出し、sub関数の中で、printf関数を実行、scanf関数を実行して、数値を受け取り、戻り値でmain関数に返します。main関数は、戻り値が 0 超ならシステムに 0 を返し、それ以外なら 1 を返します。
#include <stdio.h>
int sub( void )
{
int data;
printf( "input data: " );
scanf( "%d", &data );
return data;
}
int main( int argc, void *argv[] )
{
int ret;
ret = sub();
if( ret > 0 )
return 0;
else
return 1;
}
簡単に実行してみます。
$ gcc -g -o hello_world.out hello_world.c
$ ./hello_world.out
input data: 0
$ echo $?
1
$ ./hello_world.out
input data: 1
$ echo $?
0
想定している通りに動作しているようです。
プログラムの概要を調べる
GDB で動作を確認する前に、プログラムの概要を調べます。
ELFヘッダによると、エントリポイントは、0x1060 です。セクションヘッダの textセクションも、0x1060 から始まっています。
$ file hello_world.out
hello_world.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=89d00684582cd697b573c0fd49c38d4f17146450, for GNU/Linux 3.2.0, with debug_info, not stripped
$ readelf -h hello_world.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2 s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 15088 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 37
Section header string table index: 36
$ readelf -l hello_world.out
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000670 0x0000000000000670 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x00000000000001b9 0x00000000000001b9 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000114 0x0000000000000114 R 0x1000
LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0 0x0000000000000250 0x0000000000000258 RW 0x1000
DYNAMIC 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0 0x00000000000001e0 0x00000000000001e0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x0000000000002014 0x0000000000000034 0x0000000000000034 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0 0x0000000000000230 0x0000000000000230 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
$ readelf -S hello_world.out
There are 37 section headers, starting at offset 0x3af0:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318 000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338 0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000358 00000358 0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c 0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0 0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003c8 000003c8 00000000000000c0 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000488 00000488 00000000000000a8 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 0000000000000530 00000530 0000000000000010 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000540 00000540 0000000000000040 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000580 00000580 00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000640 00000640 0000000000000030 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000 0000000000000017 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020 0000000000000030 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001050 00001050 0000000000000008 0000000000000008 AX 0 0 8
[15] .text PROGBITS 0000000000001060 00001060 0000000000000150 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 00000000000011b0 000011b0 0000000000000009 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000002000 00002000 0000000000000014 0000000000000000 A 0 0 4
[18] .eh_frame_hdr PROGBITS 0000000000002014 00002014 0000000000000034 0000000000000000 A 0 0 4
[19] .eh_frame PROGBITS 0000000000002048 00002048 00000000000000cc 0000000000000000 A 0 0 8
[20] .init_array INIT_ARRAY 0000000000003dd0 00002dd0 0000000000000008 0000000000000008 WA 0 0 8
[21] .fini_array FINI_ARRAY 0000000000003dd8 00002dd8 0000000000000008 0000000000000008 WA 0 0 8
[22] .dynamic DYNAMIC 0000000000003de0 00002de0 00000000000001e0 0000000000000010 WA 7 0 8
[23] .got PROGBITS 0000000000003fc0 00002fc0 0000000000000028 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000003fe8 00002fe8 0000000000000028 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004010 00003010 0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004020 00003020 0000000000000008 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00003020 000000000000001f 0000000000000001 MS 0 0 1
[28] .debug_aranges PROGBITS 0000000000000000 0000303f 0000000000000030 0000000000000000 0 0 1
[29] .debug_info PROGBITS 0000000000000000 0000306f 000000000000012d 0000000000000000 0 0 1
[30] .debug_abbrev PROGBITS 0000000000000000 0000319c 00000000000000eb 0000000000000000 0 0 1
[31] .debug_line PROGBITS 0000000000000000 00003287 000000000000006c 0000000000000000 0 0 1
[32] .debug_str PROGBITS 0000000000000000 000032f3 00000000000000bc 0000000000000001 MS 0 0 1
[33] .debug_line_str PROGBITS 0000000000000000 000033af 000000000000003f 0000000000000001 MS 0 0 1
[34] .symtab SYMTAB 0000000000000000 000033f0 0000000000000390 0000000000000018 35 18 8
[35] .strtab STRTAB 0000000000000000 00003780 0000000000000200 0000000000000000 0 0 1
[36] .shstrtab STRTAB 0000000000000000 00003980 000000000000016a 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
GDBでデバッグを開始してみる
では、早速起動してみます。起動すると、以下のように、入力待ちの状態になります。
$ gdb -q hello_world.out
no key sequence terminator:
Reading symbols from hello_world.out...
gdb-peda$
ここで、シンボルが残ってる(main関数が分かる)場合は、start を実行すると main関数の先頭で止まってくれます。一方、run を実行すると、main関数では止まらず、ブレークポイントで止まる、もしくは、プログラムの最後まで実行されます。
start を実行してみます。情報量多いですね。
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.
Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[----------------------------------registers-----------------------------------]
RAX: 0x555555555185 (<main>: push rbp)
RBX: 0x7fffffffe358 --> 0x7fffffffe5da ("/home/user/svn/experiment/c/hello_world.out")
RCX: 0x555555557dd8 --> 0x555555555100 (<__do_global_dtors_aux>: endbr64)
RDX: 0x7fffffffe368 --> 0x7fffffffe606 ("SHELL=/bin/bash")
RSI: 0x7fffffffe358 --> 0x7fffffffe5da ("/home/user/svn/experiment/c/hello_world.out")
RDI: 0x1
RBP: 0x7fffffffe240 --> 0x1
RSP: 0x7fffffffe220 --> 0x7fffffffe358 --> 0x7fffffffe5da ("/home/user/svn/experiment/c/hello_world.out")
RIP: 0x555555555194 (<main+15>: call 0x555555555149 <sub>)
R8 : 0x0
R9 : 0x7ffff7fcf680 (<_dl_fini>: push rbp)
R10: 0x7ffff7fcb878 --> 0xc00120000000e
R11: 0x7ffff7fe1930 (<_dl_audit_preinit>: mov eax,DWORD PTR [rip+0x1b4e2]
R12: 0x0
R13: 0x7fffffffe368 --> 0x7fffffffe606 ("SHELL=/bin/bash")
R14: 0x555555557dd8 --> 0x555555555100 (<__do_global_dtors_aux>: endbr64)
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x555555554000 --> 0x10102464c457f
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555555189 <main+4>: sub rsp,0x20
0x55555555518d <main+8>: mov DWORD PTR [rbp-0x14],edi
0x555555555190 <main+11>: mov QWORD PTR [rbp-0x20],rsi
=> 0x555555555194 <main+15>: call 0x555555555149 <sub>
0x555555555199 <main+20>: mov DWORD PTR [rbp-0x4],eax
0x55555555519c <main+23>: cmp DWORD PTR [rbp-0x4],0x0
0x5555555551a0 <main+27>: jle 0x5555555551a9 <main+36>
0x5555555551a2 <main+29>: mov eax,0x0
No argument
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe220 --> 0x7fffffffe358 --> 0x7fffffffe5da ("/home/user/svn/experiment/c/hello_world.out")
0008| 0x7fffffffe228 --> 0x100000000
0016| 0x7fffffffe230 --> 0x0
0024| 0x7fffffffe238 --> 0x0
0032| 0x7fffffffe240 --> 0x1
0040| 0x7fffffffe248 --> 0x7ffff7df124a (<__libc_start_call_main+122>: mov edi,eax)
0048| 0x7fffffffe250 --> 0x0
0056| 0x7fffffffe258 --> 0x555555555185 (<main>: push rbp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, main (argc=0x1, argv=0x7fffffffe358) at hello_world.c:18
18 ret = sub();
gdb-peda$
GDB では、ASLR(Address Space Layout Randomization)は、デフォルトで無効になっている(有効にすることもできる)ので、毎回同じアドレス配置になります。
また、最後まで実行した状態で、もう一度、run などを実行すると、再実行することが出来ます。
x86-64の基本的な動作を理解する
主要なレジスタの理解
まず、レジスタについて簡単に理解しておきます。
レジスタ名 |
概要 |
用途 |
RIP |
プログラムカウンタ |
現在のプログラムの位置アドレス |
RSP |
スタックポインタ |
現在のスタックポインタのアドレス |
RBP |
ベースポインタ |
関数内でスタック領域を扱う基準となるアドレス |
x86-64 では、関数呼び出しでは、以下の動作を行います。
- call命令では、call命令の次の命令のアドレスをスタックに push して、RIP を関数の先頭アドレスにセットする
- 呼び出し元で使用していた RBP をスタックに push する
- 現在の RSP を RBP にセットする
この後、関数の内部では、RBP を基準としてスタックを扱います。
また、関数の最後では以下の動作を行います。1. と 2. は leave命令でも同じ動作になります。
- RBP を RSP にセットする(RSP は上の関数呼び出しの 3. に戻る)
- スタックを RBP に pop する(RBP も関数呼び出し時の状態に戻る)
- ret命令では、現在のスタック(上の関数呼び出しの 1. で保存していた call命令の次の命令のアドレス)を pop して RIP にセットする
これにより、関数の呼び出し元では、call命令の実行前と実行後で、RSP、RBP が同じ状態が保持されます。
スタック
大きいアドレスから小さいアドレスに向かって、スタックは使われていきます。
関数の呼び出し規約
x86-64 の関数の呼び出し規約は、x86 と異なります。
x86 は、引数は逆順(3つの引数のとき、第3引数→第2引数→第1引数の順)で、全てスタックに積まれます。戻り値は EAX に格納され、関数の呼び出し元がスタックを解放します。
x86-64 では、引数は第6引数までレジスタで渡され、それ以上はスタックに積まれます。レジスタの順は、以下の通りです。
第1引数 |
第2引数 |
第3引数 |
第4引数 |
第5引数 |
第6引数 |
RDI |
RSI |
RDX |
RCX |
R8 |
R9 |
戻り値は RAX に格納されます。
システムコールの場合は、少し異なり、以下のようになっています。
アーキテクチャ |
命令 |
番号 |
第1引数 |
第2引数 |
第3引数 |
第4引数 |
x86 |
int 0x80 |
eax |
ebx |
ecx |
edx |
esi |
x86-64 |
syscall |
RAX |
RDI |
RSI |
RDX |
r10 |
破壊(揮発性)レジスタと非破壊(不揮発性)レジスタ
- 破壊(揮発性)レジスタ:RAX、RCX、RDX、R8~R11、XMM0~XMM5
- 非破壊(不揮発性)レジスタ:RBX、RBP、RDI、RSI、RSP、R12~R15、XMM6~XMM15
簡単なプログラムでx86-64 ELFをGDBでデバッグしてみる
これまでを踏まえて、理解したアセンブラの内容を書いていきます。
main関数のアセンブラの内容
まず、main関数です。
最初の2行は、main関数であっても、お決まりの2行です。その後の sub rsp,0x20
は、main関数で使用するローカル変数のために、スタックを確保しています。
mov DWORD PTR [rbp-0x14],edi
と mov QWORD PTR [rbp-0x20],rsi
は、確保したスタックにレジスタの値を退避しているのだと思いますが、理由は分かりません。RDI と RSI は、非破壊レジスタなので、上位でケアする必要はないはずです。また、main関数として退避してるのかと思いましたが、使用していないので必要ないはずです。
call命令で、sub関数を呼び出し、その後、戻り値が EAX に入ってるので、確保したスタックに格納しています。cmp命令で 0 と比較して、jle命令で分岐します。
cmp命令と test命令はステータスレジスタに結果を反映するだけで結果はレジスタに保存しません。jle命令は、最初に Jump の J が付いてるので、ジャンプ命令で、le は(たぶんですが)less than equal なので、小さいか等しい場合にジャンプします。
つまり、sub関数の戻り値が、0 と比べて、小さい、もしくは、等しい場合に main+36(1 を返す方)にジャンプします。そうでなければ、0 を返す方を通り、main+41 にジャンプします。
最後は、こちらも、main関数であっても、お決まりの2行です。
gdb-peda$ disas main
Dump of assembler code for function main:
0x0000555555555185 <+0>: push rbp
0x0000555555555186 <+1>: mov rbp,rsp
0x0000555555555189 <+4>: sub rsp,0x20
0x000055555555518d <+8>: mov DWORD PTR [rbp-0x14],edi
0x0000555555555190 <+11>: mov QWORD PTR [rbp-0x20],rsi
=> 0x0000555555555194 <+15>: call 0x555555555149 <sub>
0x0000555555555199 <+20>: mov DWORD PTR [rbp-0x4],eax
0x000055555555519c <+23>: cmp DWORD PTR [rbp-0x4],0x0
0x00005555555551a0 <+27>: jle 0x5555555551a9 <main+36>
0x00005555555551a2 <+29>: mov eax,0x0
0x00005555555551a7 <+34>: jmp 0x5555555551ae <main+41>
0x00005555555551a9 <+36>: mov eax,0x1
0x00005555555551ae <+41>: leave
0x00005555555551af <+42>: ret
End of assembler dump.
sub関数のアセンブラの内容
続いて、sub関数です。上で説明したものは省略します。
lea rax,[rip+0xeac] # 0x555555556004
は、RIP+0xEAC のアドレスを RAX に設定します。RIP は、1つ進んだところ(0x0000555555555158)になります。コメントの通り、結果は、0x555555556004
になります。GDB で、そのアドレスを見てみました。printf関数の引数が入っていました。
gdb-peda$ x/s 0x555555556004
0x555555556004: "input data: "
引数を RDI に格納して、printf関数を呼び出しています。その後、scanf関数を呼び出すために、また lea命令があります。第2引数から準備しています。スタックのアドレス(ローカル変数)を RSI にセットしています。第1引数については、一応内容を確認しておきます。正しく、%d
が入っていました。
$ x/s 0x555555556011
0x555555556011: "%d"
あとは、sub関数の戻り値として、scanf関数の結果の第2引数の値を戻り値の EAX にセットして終了です。
gdb-peda$ disas sub
Dump of assembler code for function sub:
0x0000555555555149 <+0>: push rbp
0x000055555555514a <+1>: mov rbp,rsp
0x000055555555514d <+4>: sub rsp,0x10
0x0000555555555151 <+8>: lea rax,[rip+0xeac]
0x0000555555555158 <+15>: mov rdi,rax
0x000055555555515b <+18>: mov eax,0x0
0x0000555555555160 <+23>: call 0x555555555030 <printf@plt>
0x0000555555555165 <+28>: lea rax,[rbp-0x4]
0x0000555555555169 <+32>: mov rsi,rax
0x000055555555516c <+35>: lea rax,[rip+0xe9e]
0x0000555555555173 <+42>: mov rdi,rax
0x0000555555555176 <+45>: mov eax,0x0
0x000055555555517b <+50>: call 0x555555555040 <__isoc99_scanf@plt>
0x0000555555555180 <+55>: mov eax,DWORD PTR [rbp-0x4]
0x0000555555555183 <+58>: leave
0x0000555555555184 <+59>: ret
End of assembler dump.
簡単なプログラムをstripしてGDBでデバッグしてみる
これまでは、strip されていない(デバッグ情報が残っている)プログラムを扱ってきましたが、普通は strip されている(デバッグ情報は残っていない)と思います。ここからは、先ほどのプログラムを strip して、GDB でデバッグしてみます。
$ cp hello_world.out hello_world_strip.out
$ strip hello_world_strip.out
$ ll hello_world.out hello_world_strip.out
-rwxr-xr-x 1 user user 18K Sep 7 22:19 hello_world.out*
-rwxr-xr-x 1 user user 15K Sep 8 17:59 hello_world_strip.out*
プログラムの概要を調べる
strip していない、デバッグ情報のあるプログラムでは、readelf の情報を使わなくてもデバッグ出来ましたが、strip されたプログラムの場合は、この情報が重要になります。
エントリポイントは、先ほどと同じで、0x1060 から始まっています。
$ file hello_world_strip.out
hello_world_strip.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=89d00684582cd697b573c0fd49c38d4f17146450, for GNU/Linux 3.2.0, stripped
$ readelf -h hello_world_strip.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2 s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 12624 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
$ readelf -l hello_world_strip.out
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000670 0x0000000000000670 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x00000000000001b9 0x00000000000001b9 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000114 0x0000000000000114 R 0x1000
LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0 0x0000000000000250 0x0000000000000258 RW 0x1000
DYNAMIC 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0 0x00000000000001e0 0x00000000000001e0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x0000000000002014 0x0000000000000034 0x0000000000000034 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0 0x0000000000000230 0x0000000000000230 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
$ readelf -S hello_world_strip.out
There are 29 section headers, starting at offset 0x3150:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318 000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338 0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000358 00000358 0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c 0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0 0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003c8 000003c8 00000000000000c0 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000488 00000488 00000000000000a8 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 0000000000000530 00000530 0000000000000010 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000540 00000540 0000000000000040 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000580 00000580 00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000640 00000640 0000000000000030 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000 0000000000000017 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020 0000000000000030 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001050 00001050 0000000000000008 0000000000000008 AX 0 0 8
[15] .text PROGBITS 0000000000001060 00001060 0000000000000150 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 00000000000011b0 000011b0 0000000000000009 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000002000 00002000 0000000000000014 0000000000000000 A 0 0 4
[18] .eh_frame_hdr PROGBITS 0000000000002014 00002014 0000000000000034 0000000000000000 A 0 0 4
[19] .eh_frame PROGBITS 0000000000002048 00002048 00000000000000cc 0000000000000000 A 0 0 8
[20] .init_array INIT_ARRAY 0000000000003dd0 00002dd0 0000000000000008 0000000000000008 WA 0 0 8
[21] .fini_array FINI_ARRAY 0000000000003dd8 00002dd8 0000000000000008 0000000000000008 WA 0 0 8
[22] .dynamic DYNAMIC 0000000000003de0 00002de0 00000000000001e0 0000000000000010 WA 7 0 8
[23] .got PROGBITS 0000000000003fc0 00002fc0 0000000000000028 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000003fe8 00002fe8 0000000000000028 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004010 00003010 0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004020 00003020 0000000000000008 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00003020 000000000000001f 0000000000000001 MS 0 0 1
[28] .shstrtab STRTAB 0000000000000000 0000303f 000000000000010a 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
Ghidraを使ってmain関数のアドレスを特定する
hello_world_strip.out について、Ghidra を使って解析します。
Ghidra の環境構築と使い方については、以下を参照してください。
daisuke20240310.hatenablog.com
daisuke20240310.hatenablog.com
Ghidra を起動して、hello-world-strip という名前でプロジェクトを作り、hello_world_strip.out を解析させます。解析が終わると、entry が表示されました。なお、Window → Memory Map を開き、家のアイコンをクリックして、Base Image Address は、0 に変更しました。
逆コンパイル画面を見ると、__libc_start_main()
が見えます。第1引数が main関数なので、FUN_00001185
をダブルクリックします。
すると、main関数が表示されました。main関数とは書いてませんが、先ほどと同じアセンブラコードが並んでいます。
main関数の先頭アドレスは、ファイルの先頭から 0x1185 にあることが分かりました。
GDBでデバッグを開始してみる
では、GDB を起動してみます。先ほどと違って、シンボル情報が読み込まれませんでした。
$ gdb -q hello_world_strip.out
no key sequence terminator:
Reading symbols from hello_world_strip.out...
(No debugging symbols found in hello_world_strip.out)
gdb-peda$
まず、先ほどと同じように、start を実行してみます。_start
で止まってくれました。
gdb-peda$ start
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x0
RDX: 0x0
RSI: 0x0
RDI: 0x0
RBP: 0x0
RSP: 0x7fffffffe340 --> 0x1
RIP: 0x7ffff7fe5a40 (<_start>: mov rdi,rsp)
R8 : 0x0
R9 : 0x0
R10: 0x0
R11: 0x0
R12: 0x0
R13: 0x0
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7ffff7fe5a35 <_dl_help+1285>: call 0x7ffff7fd1120 <_dl_init_paths>
0x7ffff7fe5a3a <_dl_help+1290>: jmp 0x7ffff7fe5560 <_dl_help+48>
0x7ffff7fe5a3f: nop
=> 0x7ffff7fe5a40 <_start>: mov rdi,rsp
0x7ffff7fe5a43 <_start+3>: call 0x7ffff7fe6640 <_dl_start>
0x7ffff7fe5a48 <_dl_start_user>: mov r12,rax
0x7ffff7fe5a4b <_dl_start_user+3>: mov rdx,QWORD PTR [rsp]
0x7ffff7fe5a4f <_dl_start_user+7>: mov rsi,rdx
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe340 --> 0x1
0008| 0x7fffffffe348 --> 0x7fffffffe5ce ("/home/user/svn/experiment/c/hello_world_strip.out")
0016| 0x7fffffffe350 --> 0x0
0024| 0x7fffffffe358 --> 0x7fffffffe600 ("SHELL=/bin/bash")
0032| 0x7fffffffe360 --> 0x7fffffffe610 ("NMAP_PRIVILEGED=")
0040| 0x7fffffffe368 --> 0x7fffffffe621 ("PWD=/home/user/svn/experiment/c")
0048| 0x7fffffffe370 --> 0x7fffffffe641 ("LOGNAME=user")
0056| 0x7fffffffe378 --> 0x7fffffffe64e ("XDG_SESSION_TYPE=tty")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, 0x00007ffff7fe5a40 in _start () from /lib64/ld-linux-x86-64.so.2
gdb-peda$
ここで、プロセスのマップを調べます。hello_world_strip.out は、0x555555554000 にロードされていることが分かります。先ほど、main関数の位置は、先頭から 0x1185 と分かったので、これらを足すと、0x555555551185 に main関数が存在しているはずです。
gdb-peda$ i proc map
process 81015
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0x555555554000 0x555555555000 0x1000 0x0 r--p /home/user/svn/experiment/c/hello_world_strip.out
0x555555555000 0x555555556000 0x1000 0x1000 r-xp /home/user/svn/experiment/c/hello_world_strip.out
0x555555556000 0x555555557000 0x1000 0x2000 r--p /home/user/svn/experiment/c/hello_world_strip.out
0x555555557000 0x555555559000 0x2000 0x2000 rw-p /home/user/svn/experiment/c/hello_world_strip.out
0x7ffff7fc5000 0x7ffff7fc9000 0x4000 0x0 r--p [vvar]
0x7ffff7fc9000 0x7ffff7fcb000 0x2000 0x0 r-xp [vdso]
0x7ffff7fcb000 0x7ffff7fcc000 0x1000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fcc000 0x7ffff7ff1000 0x25000 0x1000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ff1000 0x7ffff7ffb000 0xa000 0x26000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7fff000 0x4000 0x30000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack]
gdb-peda$
main関数を表示してみます。無事に、main関数が表示されました。
gdb-peda$ x/10i 0x555555555185
0x555555555185: push rbp
0x555555555186: mov rbp,rsp
0x555555555189: sub rsp,0x20
0x55555555518d: mov DWORD PTR [rbp-0x14],edi
0x555555555190: mov QWORD PTR [rbp-0x20],rsi
0x555555555194: call 0x555555555149
0x555555555199: mov DWORD PTR [rbp-0x4],eax
0x55555555519c: cmp DWORD PTR [rbp-0x4],0x0
0x5555555551a0: jle 0x5555555551a9
0x5555555551a2: mov eax,0x0
あとは、main関数にブレークポイントを設定して、実行すれば、先ほどと同じようにデバッグが出来ます。
$ b *0x555555555185
Breakpoint 2 at 0x555555555185
gdb-peda$ i b
Num Type Disp Enb Address What
2 breakpoint keep y 0x0000555555555185
gdb-peda$ c
Continuing.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[----------------------------------registers-----------------------------------]
RAX: 0x555555555185 (push rbp)
RBX: 0x7fffffffe348 --> 0x7fffffffe5ce ("/home/user/svn/experiment/c/hello_world_strip.out")
RCX: 0x555555557dd8 --> 0x555555555100 (endbr64)
RDX: 0x7fffffffe358 --> 0x7fffffffe600 ("SHELL=/bin/bash")
RSI: 0x7fffffffe348 --> 0x7fffffffe5ce ("/home/user/svn/experiment/c/hello_world_strip.out")
RDI: 0x1
RBP: 0x1
RSP: 0x7fffffffe238 --> 0x7ffff7df124a (<__libc_start_call_main+122>: mov edi,eax)
RIP: 0x555555555185 (push rbp)
R8 : 0x0
R9 : 0x7ffff7fcf680 (<_dl_fini>: push rbp)
R10: 0x7ffff7fcb878 --> 0xc00120000000e
R11: 0x7ffff7fe1930 (<_dl_audit_preinit>: mov eax,DWORD PTR [rip+0x1b4e2]
R12: 0x0
R13: 0x7fffffffe358 --> 0x7fffffffe600 ("SHELL=/bin/bash")
R14: 0x555555557dd8 --> 0x555555555100 (endbr64)
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x555555554000 --> 0x10102464c457f
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555555180: mov eax,DWORD PTR [rbp-0x4]
0x555555555183: leave
0x555555555184: ret
=> 0x555555555185: push rbp
0x555555555186: mov rbp,rsp
0x555555555189: sub rsp,0x20
0x55555555518d: mov DWORD PTR [rbp-0x14],edi
0x555555555190: mov QWORD PTR [rbp-0x20],rsi
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe238 --> 0x7ffff7df124a (<__libc_start_call_main+122>: mov edi,eax)
0008| 0x7fffffffe240 --> 0x0
0016| 0x7fffffffe248 --> 0x555555555185 (push rbp)
0024| 0x7fffffffe250 --> 0x100000000
0032| 0x7fffffffe258 --> 0x7fffffffe348 --> 0x7fffffffe5ce ("/home/user/svn/experiment/c/hello_world_strip.out")
0040| 0x7fffffffe260 --> 0x7fffffffe348 --> 0x7fffffffe5ce ("/home/user/svn/experiment/c/hello_world_strip.out")
0048| 0x7fffffffe268 --> 0x3c24da9e3bd1cf39
0056| 0x7fffffffe270 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 2, 0x0000555555555185 in ?? ()
gdb-peda$
strip されたプログラムを GDB でデバッグする方法は以上です。
現時点で分かってないこと
- lea命令などで、RAX に設定した後、他のレジスタに mov してるが、最初から他のレジスタを対象に lea命令を実行できないのか?
- ある関数で、ローカル変数としてスタックを 4byte しか使っていないのに、スタックは 16byte 確保されていたが、なぜか?(8byteでいいのでは?)
- 関数呼び出し時に EAX をゼロクリアしてから関数を呼び出していたが、なぜか?
- RDI と RSI は、非破壊レジスタなのに、上位で退避しているのはなぜか?
デバッグコンソールでよく使うGDBコマンド
GDB では、現在のレジスタの値を見たり、逆アセンブラコードを見たり、ステップ実行したりするときに、GDBのコマンドを一覧にしておきます。VSCode で、GDBコマンドを実行するには、デバッグコンソールを開いて、「-exec GDBコマンド」と入力します。
例えば、info registers のコマンドが実行したい場合は、「-exec info registers」と入力してリターンキーを押すと実行できます。もちろん、短縮形の「-exec i r」でも同じことが出来ます。
以下の表では、-exec
は省略しています。また、なるべく短縮形の方を書いていきます。
コマンド |
内容 |
start |
実行開始する(シンボル情報があればmain関数で止まる) |
r(run) |
実行開始する(main関数で止まらない) |
c(continue) |
実行を再開する |
s(step) |
C言語のステップ実行をする |
si |
アセンブラのステップ実行をする |
n(next) |
C言語のステップオーバー(関数に入らない)実行をする |
ni |
アセンブラのステップオーバー(関数に入らない)実行をする |
fin(finish) |
関数を抜けるまで処理を実行する |
b 関数名(break) |
指定した関数にブレークポイントを設定する |
b *アドレス |
指定したアドレスにブレークポイントを設定する |
tb *アドレス(tbreak) |
指定したアドレスに1度だけ有効なブレークポイントを設定する |
i b(info breakpoints) |
ブレークポイントの一覧を表示する |
d 削除するブレークポイント番号(delete) |
上のブレークポイントの一覧で削除したい番号を指定するとブレークポイントを削除できる |
i r(info registers) |
整数のレジスタを全て表示する |
i r $sp |
スタックポインタのレジスタを表示する |
i r $x0 $x1 |
x0 と x1 のレジスタを表示する |
i proc map |
メモリマップを表示する |
x/b $sp |
SPが指しているメモリを1バイト表示する |
x/xb $sp |
SPが指しているメモリの1バイトを16進数で表示する |
x/xw $sp |
SPが指しているメモリの1ワード(4byte)を16進数で表示する |
x/4xw $sp |
SPが指しているメモリの4ワード(4byte×4)を16進数で表示する |
x/xg $sp |
SPが指しているメモリの8バイトを16進数で表示する |
x/s $sp |
SPが指しているメモリの文字列を表示する |
x/10i アドレス |
指定したアドレスのコードを表示する |
バイナリを扱うコマンドのまとめ
よく使うバイナリを扱うコマンドを列挙します。
objdump による逆アセンブラの出力は、何も指定しない場合は、AT&T記法と呼ばれるフォーマットとなります。これは、GDB、Ghidra で見かける Intel記法とは、ソースとデスティネーションが入れ替わるため、全く異なります。常に、-M intel
を指定するのがおすすめです。
コマンド |
内容 |
file a.out |
a.outのファイルの概要を表示する |
strings a.out |
a.outに含まれる文字列のファイルの概要を表示する(デフォルト:可読部分が4文字以上連続) |
strip a.out |
a.outに含まれるシンボル情報を一部を残して削除する |
objdump -M intel -d a.out > a.s |
逆アセンブラを出力する |
objdump -M intel -d -j .plt a.out |
特定のセクションの逆アセンブラを出力する |
readelf -h a.out |
ELFヘッダを出力する |
readelf -l a.out |
プログラムヘッダを出力する |
readelf -S a.out |
セクションヘッダを出力する |
readelf -s a.out |
シンボルテーブルを出力する |
readelf -r a.out |
リロケーション情報を出力する |
checksec --file=a.out |
セキュリティ機構を出力する |
x86-64の命令まとめ
よく使う x86-64 の命令をまとめておきます。
命令 |
内容 |
mov dest, src |
src を dest にコピーする |
push value |
value をスタックに保存、RSP は -8 |
pop dest |
スタックの値を dest に取得、RSP は +8 |
add dest, src |
dest と src を加算して dest に保存 |
sub dest, src |
dest から src を減算して dest に保存 |
mul src |
RAX と src を乗算して上位32bitを RDX に下位32bitを RAX に保存(RDX:RAX) ※8bit同士の乗算は AX に保存されて RDX は影響を受けないが、それ以外は RDX は書き込まれることに注意 |
imul src |
オペランドが 1つの場合は mul の符号付版で、オペランドが2つ、3つの場合は結果が RAX に保存され、RDX は影響を受けない |
div src |
上位32bitを RDX に下位32bitを RAX(RDX:RAX)を src で除算して商を RAX に余り RDX に保存 |
xor dest, src |
dest と src を排他的論理和して dest に保存 |
call function |
関数呼び出し |
ret |
関数から呼び出し元に戻る |
shl dest, src |
dest を src だけ左論理シフトして dest に保存 |
cmp src1, src2 |
src1 と src2 を比較して結果を EFLAGSレジスタにセット |
jmp address |
address に無条件ジャンプ |
jz address |
ゼロの場合(ZF=1)は address にジャンプ |
jnz address |
ゼロでない場合(ZF=0)は address にジャンプ |
jl address |
小さい場合(SF=0)は address にジャンプ |
jle address |
小さい、または、等しい場合は address にジャンプ |
jg address |
大きい場合は address にジャンプ |
jge |
大きい、または、等しい場合は address にジャンプ |
おわりに
今回は、PC Linux のアセンブラを理解してみました。x86-64 のアセンブラは今回初めてでしたが、ARM とそこまで違うというわけではなかったので、何とか簡単なところは理解できたと思います。
文字数は 4万文字を超えました。だいぶ重いです(笑)。
今回は以上です。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。