以下の内容はhttps://daisuke20240310.hatenablog.com/entry/x86-64より取得しました。


x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)

前回 は、「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」を読んで、CTF の各ジャンルごとに使われている技術やツールについて、調べたり、実際に使ってみたりしました。

今回は、x86-64 ELF(Linux)のアセンブラを理解していきます。また、よく使う GDBコマンドや、バイナリに対してよく使うコマンド、x86-64 のよく使う命令を書きとめておこうと思います。

それでは、やっていきます。

参考文献

Ghidra の解説が主ですが、冒頭に、x86、x86-64 のアーキテクチャ、アセンブラが解説されています。今回は、この解説をもとに書いています。

  • CQ出版

GDB の本は少ないのですが、CQ出版の古い書籍は、よくまとまっていると思います。GDB のコマンドについても、ある程度、書いてくれているのですが、短縮版も書いててほしかったです。

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第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 は以下です。

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でstartを実行したところ
gdb-pedaでstartを実行したところ

gdb-peda の導入方法は以上です。

pwndbgの導入方法

gdb-peda では、ヒープ領域の情報が表示できませんでした(私の環境だけかもしれません)。そこで、よく聞く pwndbg を導入したいと思います。

pwndbg の公式サイトは以下です。

github.com

新しい導入方法

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;
   1718   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;
   1718   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 では、関数呼び出しでは、以下の動作を行います(関数のプロローグ)。

  1. call命令では、call命令の次の命令のアドレスをスタックに push して、RIP を関数の先頭アドレスにセットする(call xxx)
  2. 呼び出し元で使用していた RBP をスタックに push する(push rbp)
  3. 現在の RSP を RBP にセットする(mov rbp, rsp)

この後、関数の内部では、RBP を基準としてスタックを扱います。

また、関数の最後では以下の動作を行います(関数のエピローグ)。1. と 2. は leave命令でも同じ動作になります。

  1. RBP を RSP にセットする(mov rsp, rbp:RSP は上の関数呼び出しの 3. に戻る)
  2. スタックを RBP に pop する(pop rbp:RBP も関数呼び出し時の状態に戻る)
  3. 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],edimov 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 をダブルクリックします。

Ghidraでentryが表示されたところ
Ghidraでentryが表示されたところ

すると、main関数が表示されました。main関数とは書いてませんが、先ほどと同じアセンブラコードが並んでいます。

Ghidraでmain関数が表示されたところ
Ghidraで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命令の対象(実行する関数のアドレス)をセットします。その後、以下のプロローグに進みます。

  1. stp x29, x30, [sp, #-N]! のようなコードで、sp = sp - N によりスタックを確保し、x29(フレームポインタ)を sp の位置に保存、x30(リンクレジスタ)を sp + 8 の位置に保存する
  2. mov x29, sp のようなコードで、新しいフレームポインタを設定する
  3. stp x19, x20, [sp, #16] のようなコードで、関数内で使用するレジスタの値を保存(退避)する

この後、関数の内部では、x29 を基準としてスタックを扱います。

また、関数のエピローグでは以下の動作を行います。

  1. ldp x19, x20, [sp, #16] のようなコードで、レジスタの値を復帰させる
  2. ldp x29, x30, [sp], #N のようなコードで、sp の位置から x29(フレームポインタ)、sp + 8 の位置から x30(リンクレジスタ)を復帰させて、sp = sp + N を行い、sp を復帰させる
  3. 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万文字を超えました。だいぶ重いです(笑)。

今回は以上です。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/x86-64より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14