前回 は、「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」を読んで、CTF の各ジャンルごとに使われている技術やツールについて、調べたり、実際に使ってみたりしました。
今回は、x86-64 ELF(Linux)のアセンブラを理解していきます。また、よく使う GDBコマンドや、バイナリに対してよく使うコマンド、x86-64 のよく使う命令を書きとめておこうと思います。
それでは、やっていきます。
- 参考文献
- はじめに
- gdb-pedaの導入方法
- pwndbgの導入方法
- 簡単なプログラムでx86-64 ELFをGDBでデバッグを開始してみる
- x86-64の基本的な動作を理解する
- 簡単なプログラムでx86-64 ELFをGDBでデバッグしてみる
- 簡単なプログラムをstripしてGDBでデバッグしてみる
- 現時点で分かってないこと
- aarch64の基本的な動作を理解する
- よく使うGDBコマンド
- バイナリを扱うコマンドのまとめ
- x86-64の命令まとめ
- ユーザ入力関数のまとめ
- セキュリティ機構
- socatコマンドの使い方
- ROPgadgetの使い方
- おわりに
参考文献
Ghidra の解説が主ですが、冒頭に、x86、x86-64 のアーキテクチャ、アセンブラが解説されています。今回は、この解説をもとに書いています。
GDB の本は少ないのですが、CQ出版の古い書籍は、よくまとまっていると思います。GDB のコマンドについても、ある程度、書いてくれているのですが、短縮版も書いててほしかったです。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方とGPUで実行したときの時間を見積もってみる
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(この記事はクリア状況を随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく) ← 今回
普通の GDB はデバッグするには不便なので、最初に gdb-peda を導入しておきます(現在は、ヒープ領域を確認できる pwndbg に移行済みです)。
環境は、VirtualBox+ParrotOS 6.1 です。
それでは、やっていきます。
gdb-pedaの導入方法
gdb-peda の公式の GitHub は以下です。
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 の公式サイトは以下です。
新しい導入方法
VirtualBox から WSL に環境を移行したので、pwndbg を新しくインストールします。公式サイトを見ると、インストール方法が変わっていたので、新しい導入方法で、pwndbg を入れてみたいと思います。
以前は、インストールに、すごく時間がかかった印象でしたが、新しい導入方法ではすぐにインストールされました。
$ curl -qsL 'https://install.pwndbg.re' | sh -s -- -t pwndbg-gdb Installing system-wide... Requesting 'sudo' privileges. You may be prompted for your password... [sudo] password for ubuntu: Downloading... https://github.com/pwndbg/pwndbg/releases/download/2025.10.10/pwndbg_2025.10.10_x86_64-portable.tar.xz /tmp/tmp.iaKCYuWKmJ/pwndbg_2025.10.10_x86_64-portable.tar.xz 100%[================================================================================================================================================================>] 104.56M 47.2MB/s in 2.2s Installing... pwndbg-gdb in /usr/local/lib/pwndbg-gdb Creating... symlink in /usr/local/bin/pwndbg Installation complete. 🚀 Run binary with: pwndbg
ちょっと使ってみたのですが、以前と少し違ってました。
これは GDB のバージョンが Ubuntu 15以降になったからなのか、最初に、デバッグ情報が無いファイルがあるけど、インターネットからダウンロードするか?と聞かれるようになりました。ChatGPT によると、GDB のバージョンが、Ubuntu 12以降で、聞かれるようになったらしいです。今までは、pwndbg が聞かれないようにしていたのか、分かりませんが、初めて聞かれました。とりあえず、yes にしておきました。
$ gdb xxx GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from xxx... (gdb) start Temporary breakpoint 1 at 0x47fe: file xxx.c, line 29. Starting program: /home/ubuntu/svn/oss/xxx This GDB supports auto-downloading debuginfo from the following URLs: <https://debuginfod.ubuntu.com> Enable debuginfod for this session? (y or [n]) y Debuginfod has been enabled. To make this setting permanent, add 'set debuginfod enabled on' to .gdbinit. Downloading separate debug info for system-supplied DSO at 0x7ffff7fc3000 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Downloading separate debug info for /lib/x86_64-linux-gnu/libusb-1.0.so.0 Downloading separate debug info for /lib/x86_64-linux-gnu/libstdc++.so.6 Downloading separate debug info for /lib/x86_64-linux-gnu/libgcc_s.so.1 Downloading separate debug info for /lib/x86_64-linux-gnu/libudev.so.1 Downloading separate debug info for /lib/x86_64-linux-gnu/libcap.so.2 warning: could not find '.gnu_debugaltlink' file for /lib/x86_64-linux-gnu/libcap.so.2 Downloading separate debug info for /lib/x86_64-linux-gnu/libcap.so.2 Temporary breakpoint 1, main () at xxx.c:29 29 { (gdb) quit
また、上の起動でもそうなってますが、gdb を起動しても、pwndbg が起動しなくなりました。これは、以前の導入方法(ソースからインストール)と変更したからかもしれません。$ gdb xxx の代わりに、$ pwndbg xxx とすれば、以前通りに、pwndbg が起動します。
以前と同じ使い方にしたいなら、git clone で pwndbg のソースを入手して、setup.sh でインストールする方法にした方が良さそうです。
今回行った導入方法で、以前と同じ使い方が出来ないのは、~/.gdbinit に pwndbg を記載してないのが原因かと思って、以下のようにしました。
$ nano ~/.gdbinit source /usr/local/lib/pwndbg-gdb/lib/python3.13/site-packages/pwndbginit/gdbinit.py
しかし、これで起動すると、以下のようにエラーになります。
$ gdb xxx GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Python Exception <class 'ModuleNotFoundError'>: No module named 'pwndbginit' /home/ubuntu/.gdbinit:1: Error in sourced command file: Error occurred in Python: No module named 'pwndbginit' Reading symbols from xxx... (gdb)
正しい方法が分かってない状況なので、現状は、エイリアス(alias gdb='pwndbg')を設定しておくことにします。また、正しい導入方法が分かったら、ここに追記しようと思います。
以前の導入方法
公式サイトの導入手順通りに導入してみます。
たくさんインストールされたように見えます。
$ git clone https://github.com/pwndbg/pwndbg
$ cd pwndbg/
$ ./setup.sh
設定ファイル
完了したので、早速 GDB を起動してみましたが、エラーが出ます。gdb-peda との併用はできないようです。.gdbinit を編集します。gdb-peda と pwndbg は両方とも、.gdbinit に 1行ずつ書き込んでいるだけなので、gdb-peda の方の source をコメントアウトしました。すると、エラーは出なくなりました。
$ nano ~/.gdbinit $ cat ~/.gdbinit #source ~/Downloads/peda/peda.py source /home/user/Downloads/pwndbg/gdbinit.py
少し使ってみましたが、このままだと、TeraTerm で使うには厳しいですね。ASCIIコード以外の文字が使われているので、いたるところで、?になります。ASCIIコードだけで表示する方法もあると思うので、もうちょっと調べてみます。
後日、調べました。TeraTerm のバージョン5以降を使うと、上の ? になってしまう問題は解消します。TeraTerm の公式サイトによると、Unicode への対応が進んだようです。これにより、—? となっていたところが、—▸ と意図通りに表示できるようになったのだと思います。
簡単なプログラムで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 で動作を確認する前に、プログラムの概要を調べます。
まず、表層解析です。
$ 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 $ pwndbg -q --batch -ex "file hello_world.out" -ex "checksec" -ex "quit" pwndbg: loaded 211 pwndbg commands. Type pwndbg [filter] for a list. pwndbg: created 13 GDB functions (can be used with print/break). Type help function to see them. File: /home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out Arch: amd64 RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No Debuginfo: Yes
続いて、少し詳しく見てみます。
ELFヘッダを見ると、エントリポイントは、0x1060 です。セクションヘッダの textセクションも、0x1060 から始まっています。
$ 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でデバッグを開始してみる
早速起動してみます。
pwndbgの場合
起動すると、以下のように、入力待ちの状態になります。
$ gdb -q hello_world.out pwndbg: loaded 211 pwndbg commands. Type pwndbg [filter] for a list. pwndbg: created 13 GDB functions (can be used with print/break). Type help function to see them. Reading symbols from hello_world.out... ------- tip of the day (disable with set show-tips off) ------- Use vmmap -A|-B <number> <filter> to display <number> of maps after/before filtered ones pwndbg>
ここで、シンボルが残ってる(main関数が分かる)場合は、start を実行すると main関数の先頭で止まってくれます。一方、run を実行すると、main関数では止まらず、ブレークポイントで止まる、もしくは、プログラムの最後まで実行されます。
start を実行してみます。情報量多いですね。
pwndbg> start Temporary breakpoint 1 at 0x1194: file hello_world.c, line 18. This GDB supports auto-downloading debuginfo from the following URLs: <https://debuginfod.ubuntu.com> Debuginfod has been disabled. To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit. [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Temporary breakpoint 1, main (argc=1, argv=0x7fffffffded8) at hello_world.c:18 18 ret = sub(); LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA ───────────────────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────────────────────────────────────────────────────────────── RAX 0x555555555185 (main) ◂— push rbp RBX 0x7fffffffded8 —▸ 0x7fffffffe1c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' RCX 0x555555557dd8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555100 (__do_global_dtors_aux) ◂— endbr64 RDX 0x7fffffffdee8 —▸ 0x7fffffffe202 ◂— 'SHELL=/bin/bash' RDI 1 RSI 0x7fffffffded8 —▸ 0x7fffffffe1c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' R8 0 R9 0x7ffff7fca380 (_dl_fini) ◂— endbr64 R10 0x7fffffffdad0 ◂— 0x800000 R11 0x203 R12 1 R13 0 R14 0x555555557dd8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555100 (__do_global_dtors_aux) ◂— endbr64 R15 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f RBP 0x7fffffffddb0 —▸ 0x7fffffffde50 —▸ 0x7fffffffdeb0 ◂— 0 RSP 0x7fffffffdd90 —▸ 0x7fffffffded8 —▸ 0x7fffffffe1c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' RIP 0x555555555194 (main+15) ◂— call sub ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ► 0x555555555194 <main+15> call sub <sub> 0x555555555199 <main+20> mov dword ptr [rbp - 4], eax 0x55555555519c <main+23> cmp dword ptr [rbp - 4], 0 0x5555555551a0 <main+27> jle main+36 <main+36> 0x5555555551a2 <main+29> mov eax, 0 EAX => 0 0x5555555551a7 <main+34> jmp main+41 <main+41> ↓ 0x5555555551ae <main+41> leave 0x5555555551af <main+42> ret 0x5555555551b0 <_fini> sub rsp, 8 0x5555555551b4 <_fini+4> add rsp, 8 0x5555555551b8 <_fini+8> ret ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── In file: /home/ubuntu/svn/experiment-old/c/hello_world/hello_world.c:18 13 14 int main( int argc, void *argv[] ) 15 { 16 int ret; 17 ► 18 ret = sub(); 19 20 if( ret > 0 ) 21 return 0; 22 else 23 return 1; ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7fffffffdd90 —▸ 0x7fffffffded8 —▸ 0x7fffffffe1c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' 01:0008│-018 0x7fffffffdd98 ◂— 0x1f7fe5af0 02:0010│-010 0x7fffffffdda0 —▸ 0x7fffffffde90 —▸ 0x555555555060 (_start) ◂— xor ebp, ebp 03:0018│-008 0x7fffffffdda8 —▸ 0x7fffffffded8 —▸ 0x7fffffffe1c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' 04:0020│ rbp 0x7fffffffddb0 —▸ 0x7fffffffde50 —▸ 0x7fffffffdeb0 ◂— 0 05:0028│+008 0x7fffffffddb8 —▸ 0x7ffff7c2a1ca (__libc_start_call_main+122) ◂— mov edi, eax 06:0030│+010 0x7fffffffddc0 —▸ 0x7fffffffde00 —▸ 0x555555557dd8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555100 (__do_global_dtors_aux) ◂— endbr64 07:0038│+018 0x7fffffffddc8 —▸ 0x7fffffffded8 —▸ 0x7fffffffe1c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ► 0 0x555555555194 main+15 1 0x7ffff7c2a1ca __libc_start_call_main+122 2 0x7ffff7c2a28b __libc_start_main+139 3 0x555555555081 _start+33 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg>
GDB では、ASLR(Address Space Layout Randomization)は、デフォルトで無効になっている(有効にすることもできる)ので、毎回同じアドレス配置になります。確認してみます。
デフォルトが無効であることを確認できて、ASLR を有効にしました。有効を確認するには、再スタートが必要とのことなので、再スタートしました。すると、ASLR が有効であることを確認できました。また、実際にアドレスも、大きく変わっていることが確認できました。
pwndbg> aslr ASLR is OFF (read status from process' personality) pwndbg> aslr on Change will take effect when the process restarts ASLR is OFF (read status from process' personality) pwndbg> start Temporary breakpoint 2 at 0x555555555194: file hello_world.c, line 18. [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Temporary breakpoint 2, main (argc=1, argv=0x7ffff5af00d8) at hello_world.c:18 18 ret = sub(); LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA ───────────────────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────────────────────────────────────────────────────────────── RAX 0x62b960d8f185 (main) ◂— push rbp RBX 0x7ffff5af00d8 —▸ 0x7ffff5af11c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' RCX 0x62b960d91dd8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x62b960d8f100 (__do_global_dtors_aux) ◂— endbr64 RDX 0x7ffff5af00e8 —▸ 0x7ffff5af1202 ◂— 'SHELL=/bin/bash' RDI 1 RSI 0x7ffff5af00d8 —▸ 0x7ffff5af11c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' R8 0 R9 0x7c804606b380 (_dl_fini) ◂— endbr64 R10 0x7ffff5aefcd0 ◂— 0x800000 R11 0x203 R12 1 R13 0 R14 0x62b960d91dd8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x62b960d8f100 (__do_global_dtors_aux) ◂— endbr64 R15 0x7c804609e000 (_rtld_global) —▸ 0x7c804609f2e0 —▸ 0x62b960d8e000 ◂— 0x10102464c457f RBP 0x7ffff5aeffb0 —▸ 0x7ffff5af0050 —▸ 0x7ffff5af00b0 ◂— 0 RSP 0x7ffff5aeff90 —▸ 0x7ffff5af00d8 —▸ 0x7ffff5af11c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' RIP 0x62b960d8f194 (main+15) ◂— call sub ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ► 0x62b960d8f194 <main+15> call sub <sub> 0x62b960d8f199 <main+20> mov dword ptr [rbp - 4], eax 0x62b960d8f19c <main+23> cmp dword ptr [rbp - 4], 0 0x62b960d8f1a0 <main+27> jle main+36 <main+36> 0x62b960d8f1a2 <main+29> mov eax, 0 EAX => 0 0x62b960d8f1a7 <main+34> jmp main+41 <main+41> ↓ 0x62b960d8f1ae <main+41> leave 0x62b960d8f1af <main+42> ret 0x62b960d8f1b0 <_fini> sub rsp, 8 0x62b960d8f1b4 <_fini+4> add rsp, 8 0x62b960d8f1b8 <_fini+8> ret ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── In file: /home/ubuntu/svn/experiment-old/c/hello_world/hello_world.c:18 13 14 int main( int argc, void *argv[] ) 15 { 16 int ret; 17 ► 18 ret = sub(); 19 20 if( ret > 0 ) 21 return 0; 22 else 23 return 1; ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7ffff5aeff90 —▸ 0x7ffff5af00d8 —▸ 0x7ffff5af11c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' 01:0008│-018 0x7ffff5aeff98 ◂— 0x146086af0 02:0010│-010 0x7ffff5aeffa0 —▸ 0x7ffff5af0090 —▸ 0x62b960d8f060 (_start) ◂— xor ebp, ebp 03:0018│-008 0x7ffff5aeffa8 —▸ 0x7ffff5af00d8 —▸ 0x7ffff5af11c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' 04:0020│ rbp 0x7ffff5aeffb0 —▸ 0x7ffff5af0050 —▸ 0x7ffff5af00b0 ◂— 0 05:0028│+008 0x7ffff5aeffb8 —▸ 0x7c8045e2a1ca (__libc_start_call_main+122) ◂— mov edi, eax 06:0030│+010 0x7ffff5aeffc0 —▸ 0x7ffff5af0000 —▸ 0x62b960d91dd8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x62b960d8f100 (__do_global_dtors_aux) ◂— endbr64 07:0038│+018 0x7ffff5aeffc8 —▸ 0x7ffff5af00d8 —▸ 0x7ffff5af11c4 ◂— '/home/ubuntu/svn/experiment-old/c/hello_world/hello_world.out' ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ► 0 0x62b960d8f194 main+15 1 0x7c8045e2a1ca __libc_start_call_main+122 2 0x7c8045e2a28b __libc_start_main+139 3 0x62b960d8f081 _start+33 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> aslr ASLR is ON (read status from process' personality) pwndbg>
gdb-pedaの場合
起動すると、以下のように、入力待ちの状態になります。
$ 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] # 0x7ffff7ffce18 <_rtld_global_ro+888>) 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 | ベースポインタ | 関数内でスタック領域を扱う基準となるアドレス |
| RAX、RBX、RCX、RDX | レジスタ | 以前はそれぞれの役割はあったようですが、現在は他の汎用レジスタと同じと考えてよさそう |
| RDI、RSI | レジスタ | 上と同じく、現在は他の汎用レジスタと同じと考えてよさそう |
| R8、R9、R10、R11、R12、R13、R14、R15 | レジスタ | 汎用レジスタ |
x86-64 では、関数呼び出しでは、以下の動作を行います(関数のプロローグ)。
- call命令では、call命令の次の命令のアドレスをスタックに push して、RIP を関数の先頭アドレスにセットする(call xxx)
- 呼び出し元で使用していた RBP をスタックに push する(push rbp)
- 現在の RSP を RBP にセットする(mov rbp, rsp)
この後、関数の内部では、RBP を基準としてスタックを扱います。
また、関数の最後では以下の動作を行います(関数のエピローグ)。1. と 2. は leave命令でも同じ動作になります。
- RBP を RSP にセットする(mov rsp, rbp:RSP は上の関数呼び出しの 3. に戻る)
- スタックを RBP に pop する(pop rbp:RBP も関数呼び出し時の状態に戻る)
- ret命令では、現在のスタック(上の関数呼び出しの 1. で保存していた call命令の次の命令のアドレス)を pop して RIP にセットする
これにより、関数の呼び出し元では、call命令の実行前と実行後で、RSP、RBP が同じ状態が保持されます。
スタックの動作
大きいアドレスから小さいアドレスに向かって、スタックは使われていきます。
スタックの操作で使われる push命令では、rsp←rsp-8 して、オペランドの値を rsp が指す位置に格納します。pop命令では、rsp が指す位置の値をレジスタに格納して、rsp←rsp+8 します。
関数の呼び出し規約
x86-64 の関数の呼び出し規約は、x86 と異なります。
x86 は、引数は逆順(3つの引数のとき、第3引数→第2引数→第1引数の順)で、全てスタックに積まれます。戻り値は EAX に格納され、関数の呼び出し元がスタックを解放します。
x86-64 では、引数は第6引数までレジスタで渡され、それ以降はスタックに積まれます。レジスタの順は、以下の通りです。ただし、これは C言語の場合であり、C++ のメンバ関数の場合、RDI には、thisポインタが入るので、1つずつズレるので注意が必要です。
| 第1引数 | 第2引数 | 第3引数 | 第4引数 | 第5引数 | 第6引数 |
|---|---|---|---|---|---|
| RDI | RSI | RDX | RCX | R8 | R9 |
戻り値は RAX に格納されます。
システムコールの場合は、少し異なり、以下のようになっています。
| アーキテクチャ | 命令 | 番号 | 第1引数 | 第2引数 | 第3引数 | 第4引数 | 第5引数 | 第6引数 |
|---|---|---|---|---|---|---|---|---|
| x86 | int 0x80 | eax | ebx | ecx | edx | esi | edi | ebp |
| x86-64 | syscall | RAX | RDI | RSI | RDX | r10 | r8 | r9 |
破壊(揮発性)レジスタと非破壊(不揮発性)レジスタ
- 破壊(揮発性)レジスタ: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] # 0x555555556004 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] # 0x555555556011 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] # 0x7ffff7ffce18 <_rtld_global_ro+888>) 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 は、非破壊レジスタなのに、上位で退避しているのはなぜか?
aarch64の基本的な動作を理解する
主要なレジスタの理解と関数のプロローグとエピローグ
まず、レジスタについて簡単に理解しておきます。
aarch64 では、x0 から x30 までの 31個のレジスタを持ちます。この 31個のレジスタは、w0 から w30 として、32bit のレジスタとしても使えます。
| レジスタ名 | 概要 | 用途 |
|---|---|---|
| PC | プログラムカウンタ | 現在のプログラムの位置アドレス |
| SP | スタックポインタ | 現在のスタックポインタのアドレス |
| w0~w28 | 汎用レジスタ | |
| w29 | FP:フレームポインタ | 現在の関数内のスタック領域の基準となるアドレス(初期SP) |
| w30 | LR:リンクレジスタ | リターンアドレス |
関数のプロローグでは、以下の動作を行います。
まず、bl命令により、ハードウェア的に、x30(リンクレジスタ)に戻り番地を格納し、PC に bl命令の対象(実行する関数のアドレス)をセットします。その後、以下のプロローグに進みます。
stp x29, x30, [sp, #-N]!のようなコードで、sp = sp - N によりスタックを確保し、x29(フレームポインタ)を sp の位置に保存、x30(リンクレジスタ)を sp + 8 の位置に保存するmov x29, spのようなコードで、新しいフレームポインタを設定するstp x19, x20, [sp, #16]のようなコードで、関数内で使用するレジスタの値を保存(退避)する
この後、関数の内部では、x29 を基準としてスタックを扱います。
また、関数のエピローグでは以下の動作を行います。
ldp x19, x20, [sp, #16]のようなコードで、レジスタの値を復帰させるldp x29, x30, [sp], #Nのようなコードで、sp の位置から x29(フレームポインタ)、sp + 8 の位置から x30(リンクレジスタ)を復帰させて、sp = sp + N を行い、sp を復帰させる- ret命令では、x30(リンクレジスタ)の値を PC に設定する
これにより、関数の呼び出し元では、call命令の実行前と実行後で、SP が同じ状態が保持されます。
関数の呼び出し規約
aarch64 の関数の呼び出し規約について、まとめておきます。
表にするほどでもありませんね、とてもシンプルです。これを超える数の引数を渡す場合はスタックを使って渡されます。
| 第1引数 | 第2引数 | 第3引数 | 第4引数 | 第5引数 | 第6引数 | 第7引数 | 第8引数 |
|---|---|---|---|---|---|---|---|
| x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 |
戻り値は x0 に格納されます。
また、x19 から x28 までは、Callee-savedレジスタとして利用されます。これらのレジスタは、関数呼び出し時の内容を保持しておく必要があります。つまり、これらのレジスタを関数内で使用する場合は、あらかじめ、スタックなどに退避しておき、関数終了時に復帰させる必要があります。
よく使うGDBコマンド
以下の表に、GDB でよく使うコマンドを整理しました(なるべく短縮形の方を書いています)。GDB では、現在のレジスタの値を見たり、逆アセンブラコードを見たり、ステップ実行したりするときに、GDBのコマンドを使います。
また、デバッグ対象のプログラムにコマンドライン引数を与えたい場合は、以下の表の set args で指定する方法と、GDB起動時に指定する方法があります。gdb -q --args xxx.out --foo --bar という感じで、--args を使い、末尾に、プログラムに与えたい引数を並べます。
VSCode では、GDBコマンドを実行するには、デバッグコンソールを開いて、「-exec GDBコマンド」と入力します。例えば、info registers のコマンドが実行したい場合は、「-exec info registers」と入力してリターンキーを押すと実行できます。もちろん、短縮形の「-exec i r」でも同じことが出来ます。
| コマンド | 内容 |
|---|---|
| show args | 設定されているコマンドライン引数を表示する |
| set args | コマンドライン引数を設定する(例:set args --foo --bar) |
| 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/xc $sp | SPが指しているメモリの1バイトを10進数とASCII文字で表示する |
| 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が指しているメモリの文字列を表示する |
| disassemble アドレス | 指定したアドレスのコードを表示する(デバッグ情報が無いプログラムの場合は失敗するかもしれない) |
| x/10i アドレス | 指定したアドレスのコードを10行表示する(デバッグ情報が無いプログラムでも使える) |
| l(list) | ソースコードを表示する |
| l 10 | 10行目の前後10行のソースコードを表示する |
| p(print) | 指定した式を表示する |
| p/x | 指定した式を16進数で表示する |
| set $rax=0x10 | 指定のレジスタに値を書き込む |
| bt | バックトレースを表示する |
以降は、pwndbg を導入した場合に使えるコマンドです。
| コマンド | 内容 |
|---|---|
| checksec | セキュリティ機構を出力する |
| tele | メモリをいい感じに表示してくれる、デフォルトはいつも表示してくれている [ STACK ] が表示される |
| tele rsp 20 | スタック(rsp)を先頭として20個分の内容をいい感じに表示してくれる |
| nearpc | 逆アセンブラを表示する、デフォルトはいつも表示してくれている逆アセンブラ |
| nearpc 20 | 逆アセンブラを20行分表示する |
| elfheader | セクションのアドレス表示 |
| got | GOT領域の表示 |
| aslr | 現在のASLRの状態を表示 |
| aslr on/off | ASLRの状態を変更する(リスタート後に有効) |
| vmmap | メモリマップの表示(i proc mapより見やすく色分けされてる) |
| retaddr | リターンアドレスの表示 |
| canary | canaryの値を表示 |
| heap | ヒープ領域のチャンクが表示される |
| arena | arenaの情報の表示 |
| bins | binsの情報の表示 |
| fastbins | fastbinsの情報の表示 |
| tcachebins | tcachebinsの情報の表示 |
| unsortedbin | unsortedbinの情報の表示 |
| largebins | largebinsの情報の表示 |
| smallbins | smallbinsの情報の表示 |
バイナリを扱うコマンドのまとめ
よく使うバイナリを扱うコマンドを列挙します。
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 -j .plt -d a.out | 特定のセクションの逆アセンブラを出力する |
| readelf -h a.out | ELFヘッダを出力する(出力には、エントリポイント、プログラムヘッダの先頭位置とサイズ、セクションヘッダの先頭位置とサイズが含まれる) |
| readelf -l a.out | プログラムヘッダを出力する(出力には .interp の動的リンカのパスが含まれる) |
| 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 にジャンプ |
ユーザ入力関数のまとめ
fgets関数、scanf関数、など、ユーザ入力を受け付ける関数はいくつかありますが、関数の仕様が少しずつ異なっていて、全く覚えられないので、ここでまとめておきます。
| 関数名 | 読み取り終了条件 | 備考 |
|---|---|---|
| gets | 改行文字、EOF が出現する | 引数はバッファのポインタだけであり、サイズが設定できないため、バッファオーバーフローを引き起こします(非推奨関数) |
| fgets | 改行文字(¥n)、EOF が出現する、または、読み込まれた文字数が n-1 に達した | 末尾に自動でヌル文字を付加する、改行文字も含めて保持する |
| scanf | 半角スペース、タブ、改行などの空白類が出現する | 最小フィールド幅(%31sなど)で文字数の制限を設定できる、末尾に自動でヌル文字を付加する |
| read | 改行文字(¥n)、EOF が出現する、または、読み込まれた文字数が n-1 に達した | ヌル文字を付加しない、改行文字も含めて保持する |
簡単なソースコードを書いて、試してみます。
#include <stdio.h> #include <string.h> #include <unistd.h> int main( int argc, char *argv[] ) { int ret; char buf1[10] = { "012345678" }; char buf2[10] = { "012345678" }; char buf3[10] = { "012345678" }; setbuf( stdout, NULL ); // ユーザ入力関数のまとめ // char *fgets(char *string, int n, FILE *stream); printf( "fgets() >> " ); fgets( buf1, 5, stdin ); printf( "fgets(): buf1=%s, strlen(buf1)=%d\n", buf1, strlen(buf1) ); // int scanf(const char *format-string, argument-list); printf( "scanf() >> " ); ret = scanf( "%s", buf2 ); printf( "scanf(): buf2=%s, strlen(buf2)=%d, ret=%d\n", buf2, strlen(buf2), ret ); // int read(int handle, void *buf, unsigned n); printf( "read() >> " ); ret = read( STDIN_FILENO, buf3, 5 ); printf( "read(): buf3=%s, strlen(buf3)=%d, ret=%d\n", buf3, strlen(buf3), ret ); return 0; }
普通の入力からやってみます。fgets関数については、abcd の後の改行文字が、どこへ行ったのか、少し気になりますが、scanf関数は %s で受け取るので、改行文字の影響は受けないのだと思います。read関数は、改行文字も入力と認識しているようです(ret=5)。ヌル文字を自動で付加しないため、初期化したときの数字("5678")が出力されてしまっています。
$ gcc -o input1.out input1.c $ ./input1.out fgets() >> abcd fgets(): buf1=abcd, strlen(buf1)=4 scanf() >> efgh scanf(): buf2=efgh, strlen(buf2)=4, ret=1 read() >> ijkl read(): buf3=ijkl 5678, strlen(buf3)=9, ret=5
3文字の入力の場合です。fgets関数は、改行文字も含めて保持しているようです。read関数も、改行文字を保持しています。
$ ./input1.out fgets() >> abc fgets(): buf1=abc , strlen(buf1)=4 scanf() >> def scanf(): buf2=def, strlen(buf2)=3, ret=1 read() >> ghi read(): buf3=ghi 45678, strlen(buf3)=9, ret=4
どちらにも、空白×3 と a の 4文字を入力してみます。結果は、fgets関数は空白をそのまま保持していますが、scanf関数は空白は無視しています。どちらもヌル文字を自動で付加してくれていそうです。
$ ./input1.out fgets() >> a fgets(): buf1= a, strlen(buf1)=4 scanf() >> b scanf(): buf2=b, strlen(buf2)=1, ret=1 read() >> c read(): buf3= c 5678, strlen(buf3)=9, ret=5
空白×4 と a の 5文字を入力してみます。結果は、fget関数は 4つの空白として認識して、入力バッファに残った a と改行文字は、次の scanf関数の入力に使われたようです。read関数については、5文字なので改行文字が含まれないため、想定通りの状態です。
$ ./input1.out fgets() >> a fgets(): buf1= , strlen(buf1)=4 scanf() >> scanf(): buf2=a, strlen(buf2)=1, ret=1 read() >> b read(): buf3= b5678, strlen(buf3)=9, ret=5
scanf関数と read関数で、空白がたくさん入力された場合です。scanf関数は、空白以外の b の 1文字だけが認識されています。一方、read関数は、空白だけが保持されているようです。入力バッファに残った文字列は、コマンドラインにも影響を与えるようです。
$ ./input1.out fgets() >> a fgets(): buf1= a, strlen(buf1)=4 scanf() >> b scanf(): buf2=b, strlen(buf2)=1, ret=1 read() >> c read(): buf3= 5678, strlen(buf3)=9, ret=5 $ c -bash: c: コマンドが見つかりません
scanf関数で、間に空白を挟んだ場合です。scanf関数は、空白を区切り文字とするので、その通りの結果になったようです。read関数はこれまで通りの動きです。
$ ./input1.out fgets() >> a fgets(): buf1= a, strlen(buf1)=4 scanf() >> b b scanf(): buf2=b, strlen(buf2)=1, ret=1 read() >> c c read(): buf3= c c 5678, strlen(buf3)=9, ret=5
次は、2回連続で入力させる実装を試してみます。
#include <stdio.h> #include <string.h> #include <unistd.h> int main( int argc, char *argv[] ) { int ret; char buf1[10] = { "012345678" }; char buf2[10] = { "012345678" }; char buf3[10] = { "012345678" }; char buf4[10] = { "012345678" }; char buf5[10] = { "012345678" }; char buf6[10] = { "012345678" }; setbuf( stdout, NULL ); // ユーザ入力関数のまとめ // char *fgets(char *string, int n, FILE *stream); printf( "fgets() >> " ); fgets( buf1, 5, stdin ); printf( "fgets(): buf1=%s, strlen(buf1)=%d\n", buf1, strlen(buf1) ); printf( "fgets() >> " ); fgets( buf2, 5, stdin ); printf( "fgets(): buf2=%s, strlen(buf2)=%d\n", buf2, strlen(buf2) ); // int scanf(const char *format-string, argument-list); printf( "scanf() >> " ); ret = scanf( "%s", buf3 ); printf( "scanf(): buf3=%s, strlen(buf3)=%d, ret=%d\n", buf3, strlen(buf3), ret ); printf( "scanf() >> " ); ret = scanf( "%s", buf4 ); printf( "scanf(): buf4=%s, strlen(buf4)=%d, ret=%d\n", buf4, strlen(buf4), ret ); // int read(int handle, void *buf, unsigned n); printf( "read() >> " ); ret = read( STDIN_FILENO, buf5, 5 ); printf( "read(): buf5=%s, strlen(buf5)=%d, ret=%d\n", buf5, strlen(buf5), ret ); printf( "read() >> " ); ret = read( STDIN_FILENO, buf6, 5 ); printf( "read(): buf6=%s, strlen(buf6)=%d, ret=%d\n", buf6, strlen(buf6), ret ); return 0; }
先ほどと同じように、普通の入力から始めます。2回目の fgets関数が想定と異なる振る舞いでした。やはり、4文字の後に改行文字があるため、それが入力バッファに残っているようです。scanf関数と read関数は、これまで通りの動きです。
$ ./input2.out fgets() >> abcd fgets(): buf1=abcd, strlen(buf1)=4 fgets() >> fgets(): buf2= , strlen(buf2)=1 scanf() >> efgh scanf(): buf3=efgh, strlen(buf3)=4, ret=1 scanf() >> ijkl scanf(): buf4=ijkl, strlen(buf4)=4, ret=1 read() >> mnop read(): buf5=mnop 5678, strlen(buf5)=9, ret=5 read() >> qrst read(): buf6=qrst 5678, strlen(buf6)=9, ret=5
3文字入力の場合です。特に変わった動きはありません。
$ ./input2.out fgets() >> abc fgets(): buf1=abc , strlen(buf1)=4 fgets() >> def fgets(): buf2=def , strlen(buf2)=4 scanf() >> ghi scanf(): buf3=ghi, strlen(buf3)=3, ret=1 scanf() >> jkl scanf(): buf4=jkl, strlen(buf4)=3, ret=1 read() >> mno read(): buf5=mno 45678, strlen(buf5)=9, ret=4 read() >> pqr read(): buf6=pqr 45678, strlen(buf6)=9, ret=4
同様に、空白×3 と a の 4文字を入力します。そろそろ予想できますね。scanf関数は %s で受けているため、改行文字の影響は受けないようです。
$ ./input2.out fgets() >> a fgets(): buf1= a, strlen(buf1)=4 fgets() >> fgets(): buf2= , strlen(buf2)=1 scanf() >> b scanf(): buf3=b, strlen(buf3)=1, ret=1 scanf() >> c scanf(): buf4=c, strlen(buf4)=1, ret=1 read() >> d read(): buf5= d 5678, strlen(buf5)=9, ret=5 read() >> e read(): buf6= e 5678, strlen(buf6)=9, ret=5
困ったときは、この結果を見て、解析したいと思います。
セキュリティ機構
セキュリティ機構とは、ASLR や、スタックカナリア、NX(No eXecute)など、脆弱性が悪用されるリスクを低減、防止する仕組みのことです。脆弱性緩和機構などとも呼ばれますが、しっかりした名前が使われていない印象です。
セキュリティ機構については、以下の記事でまとめています。
daisuke20240310.hatenablog.com
socatコマンドの使い方
socatコマンドとは、汎用性の高いプロキシツールと説明されています。私の使い方としては、入力を受け付けたり、出力がされたりする、一般的なローカルで実行するプログラムを、TCPサーバとして、通信を経由して問い合わせできるようにしてくれるツールです。
例えば、以下の感じで使います。
$ socat TCP-LISTEN:4000,reuseaddr,fork EXEC:"./bof4"
| オプション | 内容 | 備考 |
|---|---|---|
| fork | 通信を受け付ける度にプロセスをforkする | これを付けないとsocatは1回で終了してしまう |
| reuseaddr | ポート番号を再利用(再bind)できるようにする | TCPのSO_REUSEADDRと同じ |
ROPgadgetの使い方
ROPgadget は、pwntools をインストールすると、一緒にインストールされます。私の場合、最初は、rp++ を使っていましたが、ROPgadget の方は、改めてインストールする必要がないので、こちらを使っていこうと思います。
$ pwn version [*] Pwntools v4.15.0 $ ROPgadget -v Version: ROPgadget v7.7 Author: Jonathan Salwan Author page: https://twitter.com/JonathanSalwan Project page: http://shell-storm.org/project/ROPgadget/
使い方を簡単に紹介します。rp++ と一緒に使って説明してみます。
以下の記事で、以前扱ったプログラムでやってみます。rp++ の方は、3命令以内で探します。ROPgadget の場合は --depth というオプションがありますが、これはおそらくバイト数なので、ちょっと扱いが難しそうです。あと、ROPgadget は、最後が ret じゃないものも出力してくれますが、今は ret で終わってほしいので、「grep 'ret$'」でフィルタします。
daisuke20240310.hatenablog.com
rp++ と同じ結果になりました。
$ rp-lin -f ./baby_stack -r 3 | grep 'pop rdi' 0x44a282: pop rdi ; adc eax, 0x24448900 ; and byte [rcx], bh ; ret ; (1 found) 0x42274f: pop rdi ; add byte [rax], al ; add rsp, 0x20 ; ret ; (1 found) 0x470931: pop rdi ; or byte [rax+0x39], cl ; ret ; (1 found) $ ROPgadget --binary ./baby_stack | grep 'pop rdi' | grep 'ret$' 0x000000000044a282 : pop rdi ; adc eax, 0x24448900 ; and byte ptr [rcx], bh ; ret 0x000000000042274f : pop rdi ; add byte ptr [rax], al ; add rsp, 0x20 ; ret 0x0000000000470931 : pop rdi ; or byte ptr [rax + 0x39], cl ; ret
以下は、しばらく使ってみて、分かったことを追記しています。
--only というオプションの使い方です。ヘルプに以下のような使用例がありました。これは、指定した命令だけを使ったガジェットを出力してくれるようです。しかし、自分が使いたい命令以外は、なんでもよかったりするので、ちょっと使いにくいですね。
ROPgadget.py --binary ./test-suite-binaries/elf-Linux-x86 --only "mov|ret" ROPgadget.py --binary ./test-suite-binaries/elf-Linux-x86 --only "mov|pop|xor|ret"
おわりに
今回は、PC Linux のアセンブラを理解してみました。x86-64 のアセンブラは今回初めてでしたが、ARM とそこまで違うというわけではなかったので、何とか簡単なところは理解できたと思います。
文字数は 4万文字を超えました。だいぶ重いです(笑)。
今回は以上です。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。