土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ

前回 は、Pwnable問題に取り組みました。CTF のカテゴリの中でも、一番難しいと言われるだけあって、かなり苦労しました。

今回は、前回のPwnable問題でも使用した、実行ファイルのセキュリティ機構(脆弱性緩和技術とも言う)を調べるツールである「checksec」の深掘りをしたいと思います。

実行ファイルのセキュリティ機構とは、もし、実行ファイルに脆弱性が存在していたとしても、その脆弱性に対する攻撃をやりにくくする仕組みのことです。

例えば、スタックカナリヤは、関数開始時にスタックにランダムな値を格納しておき、関数終了時に、その値が変化していないかをチェックします。これによって、スタックを使った攻撃を成功させにくくすることが出来ます。

実行ファイルのセキュリティ機構には、いくつか種類があるので、それらの詳細と、実際に、コンパイルしたり、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コマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ ← 今回

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

github.com

環境は、VirtualBox+ParrotOS 6.1 です。

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

checksecの準備

checksec は、従来からシェルスクリプトで実装されていましたが、シェルスクリプト版として、2.7.x(現在の最新版は、2.7.1)が最終リリースで、以降の 3.x からは、Go言語による実装に代わるそうです。

上の URL にアクセスして、右側に見える Releases をクリックします。checksec.sh-2.7.1.zip がダウンロードできますので、任意の場所に解凍します。解凍したフォルダの中に「checksec」というファイル名のシェルスクリプトが入っていると思います。

ファイルの確認と、バージョンを確認してみます。

$ file ../../tools/checksec.sh-2.7.1/checksec
../../tools/checksec.sh-2.7.1/checksec: Bourne-Again shell script, ASCII text executable

$ ../../tools/checksec.sh-2.7.1/checksec --version
checksec v2.7.1, Brian Davis, github.com/slimm609/checksec.sh, Dec 2015
Based off checksec v1.5, Tobias Klein, www.trapkit.de, November 2011

以降では、この checksec を使っていきます。

簡単なC言語のプログラムを用意する

実際に、実行ファイルのセキュリティ機構がどうなっているかを見るために、簡単なプログラムを用意します。

このプログラムを実行すると、ユーザから2回入力してもらって、その合計値が、0 より大きかったら 0(正常終了)を返し、0以下だったら 1(異常終了)を返します。

ソースコードは以下の通りです。アドレスの確認が必要なので、先頭で、main関数のアドレス(実行するプログラムのアドレス)、ローカル変数のアドレス(スタックのアドレス)、malloc関数で確保したメモリのアドレス(ヒープのアドレス)、malloc関数のアドレス(共有ライブラリのアドレス)を表示しています。

#include <stdio.h>
#include <stdlib.h>

int sub( int data )
{
  int data2;
  
  printf( "input data2: " );
  
  scanf( "%d", &data2 );
  
  return data + data2;
}

int main( int argc, void *argv[] )
{
  int ret, data;
  char buf[20], *mbuf;
  
  mbuf = malloc( 20 );
  
  printf( "  main: %p\n", main );
  printf( "   buf: %p\n",  buf );
  printf( "  mbuf: %p\n", mbuf );
  printf( "malloc: %p\n", malloc );
  
  printf( "input data: " );
  
  scanf( "%d", &data );
  
  ret = sub( data );
  
  printf( "result: %d", ret );
  
  if( ret > 0 )
    return 0;
  else
    return 1;
}

ビルドには gcc を使います。v12.2 のようです。

ビルドして、実行してみます。1 と 2 を入力して、足し合わせた 3 が表示されて、正常終了しています。

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/12/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 12.2.0-14' --with-bugurl=file:///usr/share/doc/gcc-12/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-12 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-12-bTRWOB/gcc-12-12.2.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-12-bTRWOB/gcc-12-12.2.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 12.2.0 (Debian 12.2.0-14) 

$ gcc -g -o hello_hello.out hello_hello.c

$ ./hello_hello.out 
  main: 0x55555555518d
   buf: 0x7fffffffdf30
  mbuf: 0x5555555592a0
malloc: 0x7ffff7e62860
input data: 1
input data2: 2
result: 3

このプログラムを使って、実行ファイルのセキュリティ機構を見ていきます。

簡単なC言語のプログラムを普通にビルドした実行ファイルのセキュリティ機構を見てみる

先ほど、ビルドして実行した hello_hello.out に対して、checksec を実行してみたいと思います。1行目は、セキュリティ機構の項目名で、2行目がその結果です。例えば、スタックカナリヤはデフォルトでは無効のようです。

$ ../../tools/checksec.sh-2.7.1/checksec --file=./hello_hello.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   38 Symbols  No       0          1            ./hello_hello.out

現状の ASLR(Address Space Layout Randomization)も確認しておきます。/proc/sys/kernel/randomize_va_space を見ると確認できます。0 の場合はランダム化されません。1 は、一部をランダム化されます(共有ライブラリ、スタックなど)。2 の場合は完全にランダム化されます。1 の場合に加えて、brk() で管理されるメモリの開始アドレスもランダム化されるそうです(あまり分かってないですが、追加でヒープを確保するときの領域?)。

$ cat /proc/sys/kernel/randomize_va_space
2

セキュリティ機構について、簡単にまとめておきます。

項目 内容
RELRO RELocation Read Only のことで、No RELRO(GOT が書き込み可能)、Partial RELRO(__libc_start_main などのごく一部の GOT は書き込み禁止)、Full RELRO(GOT は書き込み禁止)のどれかになる。共有ライブラリのアドレスが格納されている GOT を書き込み禁止にする
STACK CANARY 関数開始時にスタックにランダムな値(カナリヤ)を格納しておき、関数終了時に、その値が変化していないかをチェックする(SSP とも言う)
NX No eXecute のことで、スタック領域のコードの実行を禁止にする
PIE Position Independent Executable のことで、この実行ファイルが配置されるアドレスをランダム化する
RPATH 実行ファイルに共有ライブラリのサーチするリストを格納していること示す(攻撃に利用されることがあるので、RPATH が有効かどうかを表示している)
RUNPATH RPATH と機能は同じだが、LD_LIBRALY_PATHが優先されるため、RPATHより安全と言われている(RUNPATH が有効かどうかを表示している)
Symbols 実行ファイルに含まれているシンボル情報の数(strip されてないことを表示する)
FORTIFY GCC、GLIBC におけるセキュリティ機能が有効かどうかを示す
Fortified FORTIFY の機能を有効にした関数の数
Fortifiable FORTIFY の機能数(有効にできる関数の数)
FILE 今回 checksec の対象としたプログラム

RELROについて具体的に確認する

まず、GOT(Global Offset Table)と PLT(Procefure Linkage Table)について調べます。

GOTとPLTについて具体的に確認する

共有ライブラリの関数(例えば、printf関数)を、プログラムから呼び出すときは、まず、PLT GOT とは、 PLT

GDB(gdb-peda 導入済み)を起動します。先頭の malloc関数をコールする直前で止まっているようです。

$ gdb -q hello_hello.out
: no key sequence terminator:
Reading symbols from hello_hello.out...
gdb-peda$ 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: 0x55555555518d (<main>:    push   rbp)
RBX: 0x7fffffffe358 --> 0x7fffffffe5d2 ("/home/user/svn/experiment/c/hello_hello.out")
RCX: 0x555555557dd0 --> 0x555555555100 (<__do_global_dtors_aux>:        endbr64)
RDX: 0x7fffffffe368 --> 0x7fffffffe5fe ("SHELL=/bin/bash")
RSI: 0x7fffffffe358 --> 0x7fffffffe5d2 ("/home/user/svn/experiment/c/hello_hello.out")
RDI: 0x1
RBP: 0x7fffffffe240 --> 0x1
RSP: 0x7fffffffe200 --> 0x7fffffffe358 --> 0x7fffffffe5d2 ("/home/user/svn/experiment/c/hello_                        hello.out")
RIP: 0x55555555519c (<main+15>: mov    edi,0x14)
R8 : 0x0
R9 : 0x7ffff7fcf680 (<_dl_fini>:        push   rbp)
R10: 0x7ffff7fcb878 --> 0xc00120000000e
R11: 0x7ffff7fe1930 (<_dl_audit_preinit>:       mov    eax,DWORD PTR [rip+0x1b4e2]        # 0x                        7ffff7ffce18 <_rtld_global_ro+888>)
R12: 0x0
R13: 0x7fffffffe368 --> 0x7fffffffe5fe ("SHELL=/bin/bash")
R14: 0x555555557dd0 --> 0x555555555100 (<__do_global_dtors_aux>:        endbr64)
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x555555554000 --> 0x10102464c457f
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x555555555191 <main+4>:     sub    rsp,0x40
   0x555555555195 <main+8>:     mov    DWORD PTR [rbp-0x34],edi
   0x555555555198 <main+11>:    mov    QWORD PTR [rbp-0x40],rsi
=> 0x55555555519c <main+15>:    mov    edi,0x14
   0x5555555551a1 <main+20>:    call   0x555555555050 <malloc@plt>
   0x5555555551a6 <main+25>:    mov    QWORD PTR [rbp-0x8],rax
   0x5555555551aa <main+29>:    lea    rax,[rip+0xffffffffffffffdc]        # 0x55555555518d <m                        ain>
   0x5555555551b1 <main+36>:    mov    rsi,rax
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe200 --> 0x7fffffffe358 --> 0x7fffffffe5d2 ("/home/user/svn/experiment/c/hello                        _hello.out")
0008| 0x7fffffffe208 --> 0x100000000
0016| 0x7fffffffe210 --> 0x0
0024| 0x7fffffffe218 --> 0x0
0032| 0x7fffffffe220 --> 0x0
0040| 0x7fffffffe228 --> 0x0
0048| 0x7fffffffe230 --> 0x0
0056| 0x7fffffffe238 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Temporary breakpoint 1, main (argc=0x1, argv=0x7fffffffe358) at hello_hello.c:20
20        mbuf = malloc( 20 );

まず、メモリ配置を確認しておきます。開始アドレスは、0x555555554000 です。

$ i proc map
process 319238
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x555555554000     0x555555555000     0x1000        0x0  r--p   /home/user/svn/experiment/c/hello_hello.out
      0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /home/user/svn/experiment/c/hello_hello.out
      0x555555556000     0x555555557000     0x1000     0x2000  r--p   /home/user/svn/experiment/c/hello_hello.out
      0x555555557000     0x555555558000     0x1000     0x2000  r--p   /home/user/svn/experiment/c/hello_hello.out
      0x555555558000     0x555555559000     0x1000     0x3000  rw-p   /home/user/svn/experiment/c/hello_hello.out
      0x7ffff7dc7000     0x7ffff7dca000     0x3000        0x0  rw-p
      0x7ffff7dca000     0x7ffff7df0000    0x26000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7df0000     0x7ffff7f45000   0x155000    0x26000  r-xp   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f45000     0x7ffff7f98000    0x53000   0x17b000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f98000     0x7ffff7f9c000     0x4000   0x1ce000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9c000     0x7ffff7f9e000     0x2000   0x1d2000  rw-p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9e000     0x7ffff7fab000     0xd000        0x0  rw-p
      0x7ffff7fc3000     0x7ffff7fc5000     0x2000        0x0  rw-p
      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     0x7ffff7ffd000     0x2000    0x30000  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
      0x7ffff7ffd000     0x7ffff7fff000     0x2000    0x32000  rw-p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0  rw-p   [stack]

malloc関数は一度しか使ってないので、次の printf関数で、GOT、PLT について確認します。逆アセンブラを表示して、全体を見ます。+20 のところで、malloc関数をコールして、+54 のところで printf関数をコールしています。そして、+81 のところで、2回目の printf関数をコールしています。

gdb-peda$ disas
Dump of assembler code for function main:
   0x000055555555518d <+0>:     push   rbp
   0x000055555555518e <+1>:     mov    rbp,rsp
   0x0000555555555191 <+4>:     sub    rsp,0x40
   0x0000555555555195 <+8>:     mov    DWORD PTR [rbp-0x34],edi
   0x0000555555555198 <+11>:    mov    QWORD PTR [rbp-0x40],rsi
=> 0x000055555555519c <+15>:    mov    edi,0x14
   0x00005555555551a1 <+20>:    call   0x555555555050 <malloc@plt>
   0x00005555555551a6 <+25>:    mov    QWORD PTR [rbp-0x8],rax
   0x00005555555551aa <+29>:    lea    rax,[rip+0xffffffffffffffdc]        # 0x55555555518d <main>
   0x00005555555551b1 <+36>:    mov    rsi,rax
   0x00005555555551b4 <+39>:    lea    rax,[rip+0xe5a]        # 0x555555556015
   0x00005555555551bb <+46>:    mov    rdi,rax
   0x00005555555551be <+49>:    mov    eax,0x0
   0x00005555555551c3 <+54>:    call   0x555555555030 <printf@plt>
   0x00005555555551c8 <+59>:    lea    rax,[rbp-0x30]
   0x00005555555551cc <+63>:    mov    rsi,rax
   0x00005555555551cf <+66>:    lea    rax,[rip+0xe4b]        # 0x555555556021
   0x00005555555551d6 <+73>:    mov    rdi,rax
   0x00005555555551d9 <+76>:    mov    eax,0x0
   0x00005555555551de <+81>:    call   0x555555555030 <printf@plt>
   0x00005555555551e3 <+86>:    mov    rax,QWORD PTR [rbp-0x8]
   0x00005555555551e7 <+90>:    mov    rsi,rax
   0x00005555555551ea <+93>:    lea    rax,[rip+0xe3c]        # 0x55555555602d
   0x00005555555551f1 <+100>:   mov    rdi,rax
   0x00005555555551f4 <+103>:   mov    eax,0x0
   0x00005555555551f9 <+108>:   call   0x555555555030 <printf@plt>

malloc関数や、printf関数は、@plt というのが付いてます。MAPファイルを確認してみます。0x5555555540000x1020 を足すと、PLT のアドレスになります。printf関数は、0x1030 と書かれてるので、0x555555554000+0x1030=0x555555555030 となり、上の逆アセンブラ表示のアドレスと一致しています。1つの PLT のエントリが 16byte となっています。

.plt            0x0000000000001020       0x30
 *(.plt)
 .plt           0x0000000000001020       0x30 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
                0x0000000000001030                printf@@GLIBC_2.2.5
                0x0000000000001040                __isoc99_scanf@@GLIBC_2.7

では、printf@plt に飛んだ後の状態を見てみます。16byte に入っているのは、printf@got.plt に格納されているアドレスへのジャンプと、0x555555555020 へのジャンプでした。

[-------------------------------------code-------------------------------------]
   0x555555555021:      xor    eax,0x2fca
   0x555555555026:      jmp    QWORD PTR [rip+0x2fcc]        # 0x555555557ff8
   0x55555555502c:      nop    DWORD PTR [rax+0x0]
=> 0x555555555030 <printf@plt>: jmp    QWORD PTR [rip+0x2fca]        # 0x555555558000 <printf@got.plt>
 | 0x555555555036 <printf@plt+6>:       push   0x0
 | 0x55555555503b <printf@plt+11>:      jmp    0x555555555020
 | 0x555555555040 <__isoc99_scanf@plt>: jmp    QWORD PTR [rip+0x2fc2]        # 0x555555558008 <__isoc99_scanf@got.plt>
 | 0x555555555046 <__isoc99_scanf@plt+6>:       push   0x1
 |->   0x555555555036 <printf@plt+6>:   push   0x0
       0x55555555503b <printf@plt+11>:  jmp    0x555555555020
       0x555555555040 <__isoc99_scanf@plt>:     jmp    QWORD PTR [rip+0x2fc2]        # 0x555555558008 <__isoc99_scanf@got.plt>
       0x555555555046 <__isoc99_scanf@plt+6>:   push   0x1
                                                                  JUMP is taken

MAPファイルで、printf@got.plt と書かれたアドレスを見てみます。_GLOBAL_OFFSET_TABLE_ と書かれています。0x555555554000+0x3fe8=0x555555557fe8 から、0x555555554000+0x3fe8+0x28=0x555555558010 が got.plt の領域になります。

.got.plt        0x0000000000003fe8       0x28
 *(.got.plt)
 .got.plt       0x0000000000003fe8       0x28 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
                0x0000000000003fe8                _GLOBAL_OFFSET_TABLE_
 *(.igot.plt)

printf@got.plt に入ってるアドレスを確認します。gdb-peda が下に表示してくれているように、0x555555555036push 0x0 です。次のアドレスですね。1回目は、0x555555555020 のジャンプ先でアドレス解決をして、printf@got.plt に printf関数のアドレスを格納してくれるはずです。それによって、2回目は、直接 printf関数にジャンプできるようになります。

gdb-peda$ x/g 0x555555558000
0x555555558000 <printf@got.plt>:        0x0000555555555036

printf@got.plt に格納されているアドレスにジャンプしてみます。push 0x0 に飛びました。

[-------------------------------------code-------------------------------------]
   0x555555555026:      jmp    QWORD PTR [rip+0x2fcc]        # 0x555555557ff8
   0x55555555502c:      nop    DWORD PTR [rax+0x0]
   0x555555555030 <printf@plt>: jmp    QWORD PTR [rip+0x2fca]        # 0x555555558000 <printf@got.plt>
=> 0x555555555036 <printf@plt+6>:       push   0x0
   0x55555555503b <printf@plt+11>:      jmp    0x555555555020
   0x555555555040 <__isoc99_scanf@plt>: jmp    QWORD PTR [rip+0x2fc2]        # 0x555555558008 <__isoc99_scanf@got.plt>
   0x555555555046 <__isoc99_scanf@plt+6>:       push   0x1
   0x55555555504b <__isoc99_scanf@plt+11>:      jmp    0x555555555020

この後、0x555555555020 にジャンプして、シングルステップ実行を1回すると、以下になります。1つ前の命令がおかしい(0x555555555020 だったのが、0x555555555021 に変わった)ですが、そこは気にしないでおきます。_dl_runtime_resolve_xsave というアドレス解決をしてくれそうなモジュールに飛ぶようです。

[-------------------------------------code-------------------------------------]
   0x55555555501d:      add    BYTE PTR [rax],al
   0x55555555501f:      add    bh,bh
   0x555555555021:      xor    eax,0x2fca
=> 0x555555555026:      jmp    QWORD PTR [rip+0x2fcc]        # 0x555555557ff8
 | 0x55555555502c:      nop    DWORD PTR [rax+0x0]
 | 0x555555555030 <printf@plt>: jmp    QWORD PTR [rip+0x2fca]        # 0x555555558000 <printf@got.plt>
 | 0x555555555036 <printf@plt+6>:       push   0x0
 | 0x55555555503b <printf@plt+11>:      jmp    0x555555555020
 |->   0x7ffff7fdd060 <_dl_runtime_resolve_xsave>:      push   rbx
       0x7ffff7fdd061 <_dl_runtime_resolve_xsave+1>:    mov    rbx,rsp
       0x7ffff7fdd064 <_dl_runtime_resolve_xsave+4>:    and    rsp,0xffffffffffffffc0
       0x7ffff7fdd068 <_dl_runtime_resolve_xsave+8>:    sub    rsp,QWORD PTR [rip+0x1fbe1]        # 0x7ffff7ffcc50 <_rtld_global_ro+432>
                                                                  JUMP is taken

この中は、ちょっと長そうなので、次の printf関数の呼び出しまで進めます。直前まで進めました。

[-------------------------------------code-------------------------------------]
   0x5555555551cf <main+66>:    lea    rax,[rip+0xe4b]        # 0x555555556021
   0x5555555551d6 <main+73>:    mov    rdi,rax
   0x5555555551d9 <main+76>:    mov    eax,0x0
=> 0x5555555551de <main+81>:    call   0x555555555030 <printf@plt>
   0x5555555551e3 <main+86>:    mov    rax,QWORD PTR [rbp-0x8]
   0x5555555551e7 <main+90>:    mov    rsi,rax
   0x5555555551ea <main+93>:    lea    rax,[rip+0xe3c]        # 0x55555555602d
   0x5555555551f1 <main+100>:   mov    rdi,rax
Guessed arguments:
arg[0]: 0x555555556021 ("   buf: %p\n")
arg[1]: 0x7fffffffe210 --> 0x0

中に入ります。先ほどの 1回目の printf関数と同じところにきました。

[-------------------------------------code-------------------------------------]
   0x555555555021:      xor    eax,0x2fca
   0x555555555026:      jmp    QWORD PTR [rip+0x2fcc]        # 0x555555557ff8
   0x55555555502c:      nop    DWORD PTR [rax+0x0]
=> 0x555555555030 <printf@plt>: jmp    QWORD PTR [rip+0x2fca]        # 0x555555558000 <printf@got.plt>
 | 0x555555555036 <printf@plt+6>:       push   0x0
 | 0x55555555503b <printf@plt+11>:      jmp    0x555555555020
 | 0x555555555040 <__isoc99_scanf@plt>: jmp    QWORD PTR [rip+0x2fc2]        # 0x555555558008 <__isoc99_scanf@got.plt>
 | 0x555555555046 <__isoc99_scanf@plt+6>:       push   0x1
 |->   0x7ffff7e1c5b0 <__printf>:       sub    rsp,0xd8
       0x7ffff7e1c5b7 <__printf+7>:     mov    QWORD PTR [rsp+0x28],rsi
       0x7ffff7e1c5bc <__printf+12>:    mov    QWORD PTR [rsp+0x30],rdx
       0x7ffff7e1c5c1 <__printf+17>:    mov    QWORD PTR [rsp+0x38],rcx
                                                                  JUMP is taken

同じように、0x555555558000 に格納されている値を見てみます。さっきは次の行の push 0x0 のアドレスが格納されていましたが、変化しています。

gdb-peda$ x/g 0x555555558000
0x555555558000 <printf@got.plt>:        0x00007ffff7e1c5b0

先ほどの i proc map の出力の一部を貼ります。0x00007ffff7e1c5b0 は、2行目の実行可能な x の付いてるところですね。libc なので、printf関数の実装が格納されていると思われます。

      0x7ffff7dca000     0x7ffff7df0000    0x26000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7df0000     0x7ffff7f45000   0x155000    0x26000  r-xp   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f45000     0x7ffff7f98000    0x53000   0x17b000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f98000     0x7ffff7f9c000     0x4000   0x1ce000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9c000     0x7ffff7f9e000     0x2000   0x1d2000  rw-p   /usr/lib/x86_64-linux-gnu/libc.so.6

ここまでの内容をまとめます。まず、plt(16byteのプログラムだった)には、got.plt(共有ライブラリのアドレスを格納したテーブルだった)からアドレスを取得して、ジャンプする処理と、1回目の printf関数で実行されていた _dl_runtime_resolve_xsave というアドレス解決をしてくれそうな関数にジャンプする処理が配置されていました。

2回目の printf関数では、同じように、plt に入って、同様に、got.plt からアドレスを取得しましたが、この got.plt に格納されたアドレスが、printf関数のアドレスに変化していました。1回目でアドレス解決して、got.plt に printf関数のアドレスを格納してくれたので、2回目以降は直接 printf関数にジャンプできるようになったということです。

今回確認したのは、plt と got.plt でした。しかし、ややこしいことに、got という領域と、plt.got という領域があります。ここについては、今は分からないので、調査できたら、ここに追記します。

Partial RELROとFULL RELROについて

RELRO は、GOT の書き込みを禁止するセキュリティ機構です。

コンパイルオプションの -Wl,-z,relro は、リンカに RELROセクションを作るように指示します。また、コンパイルオプションの -Wl,-z,now は、リンカにプログラム起動時にすべてのシンボルの解決を行うよう指示します。

以下に、3通りを試しました。結論としては、checksec の出力を Full RELRO にするには、-Wl,-z,now だけでいいということになります。

$ gcc -g -Wl,-z,relro -Wl,-Map=hello_hello_relro.map -o hello_hello_relro.out hello_hello.c 

$ ../../tools/checksec.sh-2.7.1/checksec --file=./hello_hello_relro.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols     FORTIFY Fortified   Fortifiable FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   39 Symbols  No      0           1           ./hello_hello_relro.out

$ gcc -g -Wl,-z,now -Wl,-Map=hello_hello_now.map -o hello_hello_now.out hello_hello.c 

$ ../../tools/checksec.sh-2.7.1/checksec --file=./hello_hello_now.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols     FORTIFY Fortified   Fortifiable FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   39 Symbols  No      0           1           ./hello_hello_now.out

$ gcc -g -Wl,-z,relro -Wl,-z,now -Wl,-Map=hello_hello_relro_now.map -o hello_hello_relro_now.out hello_hello.c 

$ ../../tools/checksec.sh-2.7.1/checksec --file=./hello_hello_relro_now.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols     FORTIFY Fortified   Fortifiable FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   39 Symbols  No      0           1           ./hello_hello_relro_now.out

最初に、コンパイルオプションの relro について調べます。relro を指定してない通常のプログラムと比較します。MAPファイルと逆アセンブラで比較しました。結果を見る限り、コンパイルオプションの relro を付けても変化はありませんでした。上の結果でも relro は影響が見られなかったので、そういうものかもしれません。

$ objdump -M intel -d hello_hello.out > hello_hello.s
$ objdump -M intel -d hello_hello_relro.out > hello_hello_relro.s

$ diff hello_hello.s hello_hello_relro.s
--- hello_hello.s       2024-09-14 16:37:21.724997508 +0900
+++ hello_hello_relro.s 2024-09-14 16:37:10.970791296 +0900
@@ -1,5 +1,5 @@

-hello_hello.out:     file format elf64-x86-64
+hello_hello_relro.out:     file format elf64-x86-64

$ diff hello_hello.map hello_hello_relro.map
--- hello_hello.map     2024-09-13 20:24:36.926962157 +0900
+++ hello_hello_relro.map       2024-09-13 22:50:58.807749077 +0900
@@ -6,7 +6,7 @@

 As-needed library included to satisfy reference by file (symbol)

-libc.so.6                     /tmp/ccVKmQjn.o (malloc@@GLIBC_2.2.5)
+libc.so.6                     /tmp/cc5qA2Go.o (malloc@@GLIBC_2.2.5)

 Discarded input sections

@@ -19,7 +19,7 @@
  .note.gnu.property
                 0x0000000000000000       0x20 /usr/lib/gcc/x86_64-linux-gnu/12/crtbeginS.o
  .note.GNU-stack
-                0x0000000000000000        0x0 /tmp/ccVKmQjn.o
+                0x0000000000000000        0x0 /tmp/cc5qA2Go.o
  .note.GNU-stack
                 0x0000000000000000        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/crtendS.o
  .note.gnu.property
@@ -37,7 +37,7 @@
 LOAD /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
 LOAD /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/crti.o
 LOAD /usr/lib/gcc/x86_64-linux-gnu/12/crtbeginS.o
-LOAD /tmp/ccVKmQjn.o
+LOAD /tmp/cc5qA2Go.o
 LOAD /usr/lib/gcc/x86_64-linux-gnu/12/libgcc.a
 LOAD /usr/lib/gcc/x86_64-linux-gnu/12/libgcc_s.so
 START GROUP
@@ -179,7 +179,7 @@
  .text          0x0000000000001082        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/crti.o
  *fill*         0x0000000000001082        0xe
  .text          0x0000000000001090       0xb9 /usr/lib/gcc/x86_64-linux-gnu/12/crtbeginS.o
- .text          0x0000000000001149      0x13c /tmp/ccVKmQjn.o
+ .text          0x0000000000001149      0x13c /tmp/cc5qA2Go.o
                 0x0000000000001149                sub
                 0x000000000000118d                main
  .text          0x0000000000001285        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/crtendS.o
@@ -201,7 +201,7 @@
  *(.rodata .rodata.* .gnu.linkonce.r.*)
  .rodata.cst4   0x0000000000002000        0x4 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
                 0x0000000000002000                _IO_stdin_used
- .rodata        0x0000000000002004       0x59 /tmp/ccVKmQjn.o
+ .rodata        0x0000000000002004       0x59 /tmp/cc5qA2Go.o

 .rodata1
  *(.rodata1)
@@ -220,7 +220,7 @@
  .eh_frame      0x00000000000020c8       0x40 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
  .eh_frame      0x0000000000002108       0x18 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
                                          0x30 (size before relaxing)
- .eh_frame      0x0000000000002120       0x40 /tmp/ccVKmQjn.o
+ .eh_frame      0x0000000000002120       0x40 /tmp/cc5qA2Go.o
                                          0x58 (size before relaxing)
  .eh_frame      0x0000000000002160        0x4 /usr/lib/gcc/x86_64-linux-gnu/12/crtendS.o
  *(.eh_frame.*)
@@ -334,7 +334,7 @@
  .data.rel.local
                 0x0000000000004018        0x8 /usr/lib/gcc/x86_64-linux-gnu/12/crtbeginS.o
                 0x0000000000004018                __dso_handle
- .data          0x0000000000004020        0x0 /tmp/ccVKmQjn.o
+ .data          0x0000000000004020        0x0 /tmp/cc5qA2Go.o
  .data          0x0000000000004020        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/crtendS.o
  .data          0x0000000000004020        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/crtn.o

@@ -359,7 +359,7 @@
  .bss           0x0000000000004020        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
  .bss           0x0000000000004020        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/crti.o
  .bss           0x0000000000004020        0x1 /usr/lib/gcc/x86_64-linux-gnu/12/crtbeginS.o
- .bss           0x0000000000004021        0x0 /tmp/ccVKmQjn.o
+ .bss           0x0000000000004021        0x0 /tmp/cc5qA2Go.o
  .bss           0x0000000000004021        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/crtendS.o
  .bss           0x0000000000004021        0x0 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/crtn.o
  *(COMMON)
@@ -406,7 +406,7 @@
  *(.comment)
  .comment       0x0000000000000000       0x1f /usr/lib/gcc/x86_64-linux-gnu/12/crtbeginS.o
                                          0x20 (size before relaxing)
- .comment       0x000000000000001f       0x20 /tmp/ccVKmQjn.o
+ .comment       0x000000000000001f       0x20 /tmp/cc5qA2Go.o
  .comment       0x000000000000001f       0x20 /usr/lib/gcc/x86_64-linux-gnu/12/crtendS.o

 .gnu.build.attributes
@@ -427,29 +427,29 @@
 .debug_aranges  0x0000000000000000       0x30
  *(.debug_aranges)
  .debug_aranges
-                0x0000000000000000       0x30 /tmp/ccVKmQjn.o
+                0x0000000000000000       0x30 /tmp/cc5qA2Go.o

 .debug_pubnames
  *(.debug_pubnames)

 .debug_info     0x0000000000000000      0x1ad
  *(.debug_info .gnu.linkonce.wi.*)
- .debug_info    0x0000000000000000      0x1ad /tmp/ccVKmQjn.o
+ .debug_info    0x0000000000000000      0x1ad /tmp/cc5qA2Go.o

 .debug_abbrev   0x0000000000000000      0x10d
  *(.debug_abbrev)
- .debug_abbrev  0x0000000000000000      0x10d /tmp/ccVKmQjn.o
+ .debug_abbrev  0x0000000000000000      0x10d /tmp/cc5qA2Go.o

 .debug_line     0x0000000000000000       0x93
  *(.debug_line .debug_line.* .debug_line_end)
- .debug_line    0x0000000000000000       0x93 /tmp/ccVKmQjn.o
+ .debug_line    0x0000000000000000       0x93 /tmp/cc5qA2Go.o

 .debug_frame
  *(.debug_frame)

 .debug_str      0x0000000000000000       0xdf
  *(.debug_str)
- .debug_str     0x0000000000000000       0xdf /tmp/ccVKmQjn.o
+ .debug_str     0x0000000000000000       0xdf /tmp/cc5qA2Go.o
                                         0x11e (size before relaxing)

 .debug_loc
@@ -483,7 +483,7 @@
                 0x0000000000000000       0x7a
  *(.debug_line_str)
  .debug_line_str
-                0x0000000000000000       0x7a /tmp/ccVKmQjn.o
+                0x0000000000000000       0x7a /tmp/cc5qA2Go.o
                                          0xb2 (size before relaxing)

 .debug_loclists
@@ -511,4 +511,4 @@
  *(.note.GNU-stack)
  *(.gnu_debuglink)
  *(.gnu.lto_*)
-OUTPUT(hello_hello.out elf64-x86-64)
+OUTPUT(hello_hello_relro.out elf64-x86-64)

次に、コンパイルオプションの now について調べます。now を指定してない通常のプログラムと比較します。MAPファイルを比較したところ、差異が多く、全部は貼れませんが、セクションのサイズが異なるところだけ貼ります。dynamic というセクションと、got のセクションのサイズが異なっていました。

また、ちょっと分かりにくいのですが、それぞれの .got 以降の行に注目します。now を指定しない方は、.got が 0x3fb8 から始まり、0x30 のサイズがあり、次に、.got.plt が 0x3fe8 から始まり、0x28 のサイズがあります。

一方、now を指定した方は、.got が 0x3fa8 から始まり、0x58 のサイズがあるだけです(0x3fa8+0x58=0x4000)。.got.plt があるように見えますが、これはサブセクションというらしく(from ChatGPT)、.got の中に含まれています。つまり、now を指定した方は、.got.plt というセクションは無くなっていて、.got に含まれる形になっていました。readelfコマンドの -S オプションで確認したところ、.got.plt というセクションはありませんでした。

/* hello_hello.map */
.dynamic        0x0000000000003dd8      0x1e0
 *(.dynamic)
 .dynamic       0x0000000000003dd8      0x1e0 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
                0x0000000000003dd8                _DYNAMIC

.got            0x0000000000003fb8       0x30
 *(.got)
 .got           0x0000000000003fb8       0x30 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
 *(.igot)
                0x0000000000003fe8                . = DATA_SEGMENT_RELRO_END (., (SIZEOF (.got.plt) >= 0x18)?0x18:0x0)
.got.plt        0x0000000000003fe8       0x28
 *(.got.plt)
 .got.plt       0x0000000000003fe8       0x28 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
                0x0000000000003fe8                _GLOBAL_OFFSET_TABLE_
 *(.igot.plt)

.data           0x0000000000004010       0x10

/* hello_hello_now.map */
.dynamic        0x0000000000003db8      0x1f0
 *(.dynamic)
 .dynamic       0x0000000000003db8      0x1f0 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
                0x0000000000003db8                _DYNAMIC

.got            0x0000000000003fa8       0x58
 *(.got.plt)
 .got.plt       0x0000000000003fa8       0x28 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
                0x0000000000003fa8                _GLOBAL_OFFSET_TABLE_
 *(.igot.plt)
 *(.got)
 .got           0x0000000000003fd0       0x30 /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o
 *(.igot)
                0x0000000000004000                . = DATA_SEGMENT_RELRO_END (., 0x0)

逆アセンブラを比較すると、これも差異が多くて貼れませんが、コードとしては一緒で、アドレスがズレていただけのようでした。

続いて、コンパイルオプションの now を指定した方を GDB で確認します。1回目の printf@plt まで進めました。先ほどの now を指定しなかったプログラムでは、0x555555555030 で、printf@got.plt に格納されていたアドレスは、次の行の 0x555555555036 でしたが、now を指定したプログラムでは、1回目の printf関数から、0x7ffff7e1c5b0(printf関数の実装が配置されたアドレス)が格納されていました。コンパイルオプションの now は、プログラム起動時に全てのシンボルを解決するという影響が確認できました。

[-------------------------------------code-------------------------------------]
   0x555555555021:      xor    eax,0x2f8a
   0x555555555026:      jmp    QWORD PTR [rip+0x2f8c]        # 0x555555557fb8
   0x55555555502c:      nop    DWORD PTR [rax+0x0]
=> 0x555555555030 <printf@plt>: jmp    QWORD PTR [rip+0x2f8a]        # 0x555555557fc0 <printf@got.plt>
 | 0x555555555036 <printf@plt+6>:       push   0x0
 | 0x55555555503b <printf@plt+11>:      jmp    0x555555555020
 | 0x555555555040 <__isoc99_scanf@plt>: jmp    QWORD PTR [rip+0x2f82]        # 0x555555557fc8 <__isoc99_scanf@got.plt>
 | 0x555555555046 <__isoc99_scanf@plt+6>:       push   0x1
 |->   0x7ffff7e1c5b0 <__printf>:       sub    rsp,0xd8
       0x7ffff7e1c5b7 <__printf+7>:     mov    QWORD PTR [rsp+0x28],rsi
       0x7ffff7e1c5bc <__printf+12>:    mov    QWORD PTR [rsp+0x30],rdx
       0x7ffff7e1c5c1 <__printf+17>:    mov    QWORD PTR [rsp+0x38],rcx
                                                                  JUMP is taken

i proc map を確認します。printf関数のアドレスが格納されている 0x555555557fc0 は、書き込みのフラグがなく、読み取り専用になっています。これによって、GOT を書き込む攻撃を防いでいるということになります。

gdb-peda$ i proc map
process 397724
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x555555554000     0x555555555000     0x1000        0x0  r--p   /home/user/svn/experiment/c/hello_hello_now.out
      0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /home/user/svn/experiment/c/hello_hello_now.out
      0x555555556000     0x555555557000     0x1000     0x2000  r--p   /home/user/svn/experiment/c/hello_hello_now.out
      0x555555557000     0x555555558000     0x1000     0x2000  r--p   /home/user/svn/experiment/c/hello_hello_now.out
      0x555555558000     0x555555559000     0x1000     0x3000  rw-p   /home/user/svn/experiment/c/hello_hello_now.out
      0x555555559000     0x55555557a000    0x21000        0x0  rw-p   [heap]
      0x7ffff7dc7000     0x7ffff7dca000     0x3000        0x0  rw-p
      0x7ffff7dca000     0x7ffff7df0000    0x26000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7df0000     0x7ffff7f45000   0x155000    0x26000  r-xp   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f45000     0x7ffff7f98000    0x53000   0x17b000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f98000     0x7ffff7f9c000     0x4000   0x1ce000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9c000     0x7ffff7f9e000     0x2000   0x1d2000  rw-p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9e000     0x7ffff7fab000     0xd000        0x0  rw-p
      0x7ffff7fc3000     0x7ffff7fc5000     0x2000        0x0  rw-p
      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     0x7ffff7ffd000     0x2000    0x30000  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
      0x7ffff7ffd000     0x7ffff7fff000     0x2000    0x32000  rw-p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0  rw-p   [stack]

now が付いてないプログラムの方の got.plt の領域を GDB で確認します。1回目のprintf関数に飛んだところです。0x555555558000 が対象のアドレスで、書き込みフラグが付いていることが確認できます。

[-------------------------------------code-------------------------------------]
   0x555555555021:      xor    eax,0x2fca
   0x555555555026:      jmp    QWORD PTR [rip+0x2fcc]        # 0x555555557ff8
   0x55555555502c:      nop    DWORD PTR [rax+0x0]
=> 0x555555555030 <printf@plt>: jmp    QWORD PTR [rip+0x2fca]        # 0x555555558000 <printf@got.plt>
 | 0x555555555036 <printf@plt+6>:       push   0x0
 | 0x55555555503b <printf@plt+11>:      jmp    0x555555555020
 | 0x555555555040 <__isoc99_scanf@plt>: jmp    QWORD PTR [rip+0x2fc2]        # 0x555555558008 <__isoc99_scanf@got.plt>
 | 0x555555555046 <__isoc99_scanf@plt+6>:       push   0x1
 |->   0x555555555036 <printf@plt+6>:   push   0x0
       0x55555555503b <printf@plt+11>:  jmp    0x555555555020
       0x555555555040 <__isoc99_scanf@plt>:     jmp    QWORD PTR [rip+0x2fc2]        # 0x555555558008 <__isoc99_scanf@got.plt>
       0x555555555046 <__isoc99_scanf@plt+6>:   push   0x1
                                                                  JUMP is taken

gdb-peda$ i proc map
process 397764
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x555555554000     0x555555555000     0x1000        0x0  r--p   /home/user/svn/experiment/c/hello_hello.out
      0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /home/user/svn/experiment/c/hello_hello.out
      0x555555556000     0x555555557000     0x1000     0x2000  r--p   /home/user/svn/experiment/c/hello_hello.out
      0x555555557000     0x555555558000     0x1000     0x2000  r--p   /home/user/svn/experiment/c/hello_hello.out
      0x555555558000     0x555555559000     0x1000     0x3000  rw-p   /home/user/svn/experiment/c/hello_hello.out
      0x555555559000     0x55555557a000    0x21000        0x0  rw-p   [heap]
      0x7ffff7dc7000     0x7ffff7dca000     0x3000        0x0  rw-p
      0x7ffff7dca000     0x7ffff7df0000    0x26000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7df0000     0x7ffff7f45000   0x155000    0x26000  r-xp   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f45000     0x7ffff7f98000    0x53000   0x17b000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f98000     0x7ffff7f9c000     0x4000   0x1ce000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9c000     0x7ffff7f9e000     0x2000   0x1d2000  rw-p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9e000     0x7ffff7fab000     0xd000        0x0  rw-p
      0x7ffff7fc3000     0x7ffff7fc5000     0x2000        0x0  rw-p
      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     0x7ffff7ffd000     0x2000    0x30000  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
      0x7ffff7ffd000     0x7ffff7fff000     0x2000    0x32000  rw-p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0  rw-p   [stack]

これまでの調べで、got の領域について推測ができます。got.plt は実行中にシンボル解決を行い、書き込み可能な領域であるが、got はプログラム起動時にシンボル解決(リロケーション)を行い読み取り専用の領域であるということです。

もう少し確認してみます。now を指定していない普通のプログラムで、GDB を起動して、以下は、malloc関数の中に入ったところです。

printf関数とは異なり、1回しか呼ばれないからなのか、最初から、libc のアドレスが格納されています。また、少し下に見えている __cxa_finalize という関数も、最初から libc のアドレスが格納されています。そもそも、0x555555557000 から 0x555555558000 までは読み取り専用です。

[-------------------------------------code-------------------------------------]
   0x555555555040 <__isoc99_scanf@plt>: jmp    QWORD PTR [rip+0x2fc2]        # 0x555555558008 <__isoc99_scanf@got.plt>
   0x555555555046 <__isoc99_scanf@plt+6>:       push   0x1
   0x55555555504b <__isoc99_scanf@plt+11>:      jmp    0x555555555020
=> 0x555555555050 <malloc@plt>: jmp    QWORD PTR [rip+0x2f7a]        # 0x555555557fd0
 | 0x555555555056 <malloc@plt+6>:       xchg   ax,ax
 | 0x555555555058 <__cxa_finalize@plt>: jmp    QWORD PTR [rip+0x2f82]        # 0x555555557fe0
 | 0x55555555505e <__cxa_finalize@plt+6>:       xchg   ax,ax
 | 0x555555555060 <_start>:     xor    ebp,ebp
 |->   0x7ffff7e62860 <__GI___libc_malloc>:     push   r12
       0x7ffff7e62862 <__GI___libc_malloc+2>:   push   rbp
       0x7ffff7e62863 <__GI___libc_malloc+3>:   push   rbx
       0x7ffff7e62864 <__GI___libc_malloc+4>:   mov    rbx,rdi
                                                                  JUMP is taken

gdb-peda$ x/g 0x555555557fe0
0x555555557fe0: 0x00007ffff7e07f40

だいぶ長くなりましたが、Partial RELRO について、だいぶ分かった気がします。ごく一部の GOT が書き込み禁止ということでしたが、もしかしたら、一度しか呼ばれない関数は書き込み禁止なのかもしれません。ですが、大部分の GOT は書き込み可能なんだと思います。

STACK CANARY(SSP)について具体的に確認する

冒頭で調べた通り、デフォルトでは、STACK CANARY(スタックカナリア)は無効でした。スタック保護ということで、SSP(Stack Smashing Protection)とも呼ばれます。というよりは、SSP の手段の一つが STACK CANARY かもしれませんね。ここでは、SSP と呼ぶことにします。

SSP を有効にする方法は、いくつかあるようですが、まずは、一番簡単なやつにします。-fstack-protector を指定してみました。checksec の結果が、Canary found に変わりました。逆アセンブラを確認したところ、main関数には、チェックが入っていましたが、sub関数にはチェックが入っていませんでした。

$ gcc -g -fstack-protector -Wl,-Map=hello_hello_ssp.map -o hello_hello_ssp.out hello_hello.c

$ ../../tools/checksec.sh-2.7.1/checksec --file=./hello_hello_ssp.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified  Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   40 Symbols      No      0          1            ./hello_hello_ssp.out

$ objdump -M intel -d hello_hello_ssp.out > hello_hello_ssp.s

続いて、もう少し SSP を強化した -fstack-protector-all をやってみます。checksec は上と同じ結果でした。こちらは、main関数に加えて、sub関数もスタックカナリアのチェックが入っていました。

$ gcc -g -fstack-protector-all -Wl,-Map=hello_hello_ssp_all.map -o hello_hello_ssp_all.out hello_hello.c

$ ../../tools/checksec.sh-2.7.1/checksec --file=./hello_hello_ssp_all.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified  Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   40 Symbols      No      0          1            ./hello_hello_ssp_all.out

$ objdump -M intel -d hello_hello_ssp_all.out > hello_hello_ssp_all.s

sub関数の 逆アセンブラを表示します。1164 の行で、乱数の値(FSセグメントのオフセット 0x28)を RAX に設定していて、116d の行で、スタックに格納しています。sub関数の終了直前の 11aa の行で、スタックから格納しておいた値を RDX に取り出し、11ae の行で、乱数の値と RDX を引き算して、11ae の行で、イコール(スタックカナリアに変化がない)なら leave命令にジャンプし、スタックカナリアに変化があったら __stack_chk_fail@plt を呼び出し、プログラムを終了させます。

FSセグメントとは、x86 のプロセッサが持つセグメントレジスタで管理されたセグメント(メモリ)のことだそうです。

0000000000001159 <sub>:
    1159:  55                     push   rbp
    115a: 48 89 e5               mov    rbp,rsp
    115d: 48 83 ec 20           sub    rsp,0x20
    1161:  89 7d ec                 mov    DWORD PTR [rbp-0x14],edi
    1164:  64 48 8b 04 25 28 00   mov    rax,QWORD PTR fs:0x28
    116b: 00 00 
    116d: 48 89 45 f8           mov    QWORD PTR [rbp-0x8],rax
    1171:  31 c0                   xor    eax,eax
    1173:  48 8d 05 8a 0e 00 00   lea    rax,[rip+0xe8a]        # 2004 <_IO_stdin_used+0x4>
    117a: 48 89 c7               mov    rdi,rax
    117d: b8 00 00 00 00           mov    eax,0x0
    1182:  e8 b9 fe ff ff           call   1040 <printf@plt>
    1187:  48 8d 45 f4             lea    rax,[rbp-0xc]
    118b: 48 89 c6               mov    rsi,rax
    118e: 48 8d 05 7d 0e 00 00   lea    rax,[rip+0xe7d]        # 2012 <_IO_stdin_used+0x12>
    1195:  48 89 c7               mov    rdi,rax
    1198:  b8 00 00 00 00           mov    eax,0x0
    119d: e8 ae fe ff ff           call   1050 <__isoc99_scanf@plt>
    11a2: 8b 55 f4                 mov    edx,DWORD PTR [rbp-0xc]
    11a5: 8b 45 ec                 mov    eax,DWORD PTR [rbp-0x14]
    11a8: 01 d0                   add    eax,edx
    11aa: 48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
    11ae: 64 48 2b 14 25 28 00   sub    rdx,QWORD PTR fs:0x28
    11b5: 00 00 
    11b7: 74 05                 je     11be <sub+0x65>
    11b9: e8 72 fe ff ff         call   1030 <__stack_chk_fail@plt>
    11be: c9                       leave
    11bf: c3                       ret

スタックカナリア(SSP)については以上です。

NXについて具体的に確認する

NX(No eXecute)は、メモリ領域に置かれたコードを実行できなくします。Windowsでは DEP(Data Execution Prevention)と呼ばれるそうです。デフォルトで有効になっていました。無効にするには、コンパイルオプション -z execstack を付けると出来るようです。やってみたところ、NX disabled に変化しました。

$ gcc -g -z execstack -Wl,-Map=hello_hello_nx.map -o hello_hello_nx.out hello_hello.c

$ ../../tools/checksec.sh-2.7.1/checksec --file=./hello_hello_nx.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified  Fortifiable  FILE
Partial RELRO   No canary found   NX disabled   PIE enabled     No RPATH   No RUNPATH   39 Symbols      No      0          1            ./hello_hello_nx.out

NX disabled になったプログラムを GDB で確認します。最後の行の [stack] に注目します。rwxp と実行可能になっています。ちなみに、NX enabled の通常のプログラムは、この記事の冒頭の結果を見てみると、0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack] となっています。

$ gdb -q hello_hello_nx.out

gdb-peda$ i proc map
process 398709
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x555555554000     0x555555555000     0x1000        0x0  r--p   /home/user/svn/experiment/c/hello_hello_nx.out
      0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /home/user/svn/experiment/c/hello_hello_nx.out
      0x555555556000     0x555555557000     0x1000     0x2000  r--p   /home/user/svn/experiment/c/hello_hello_nx.out
      0x555555557000     0x555555558000     0x1000     0x2000  r--p   /home/user/svn/experiment/c/hello_hello_nx.out
      0x555555558000     0x555555559000     0x1000     0x3000  rw-p   /home/user/svn/experiment/c/hello_hello_nx.out
      0x7ffff7dc7000     0x7ffff7dca000     0x3000        0x0  rw-p
      0x7ffff7dca000     0x7ffff7df0000    0x26000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7df0000     0x7ffff7f45000   0x155000    0x26000  r-xp   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f45000     0x7ffff7f98000    0x53000   0x17b000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f98000     0x7ffff7f9c000     0x4000   0x1ce000  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9c000     0x7ffff7f9e000     0x2000   0x1d2000  rw-p   /usr/lib/x86_64-linux-gnu/libc.so.6
      0x7ffff7f9e000     0x7ffff7fab000     0xd000        0x0  rw-p
      0x7ffff7fc3000     0x7ffff7fc5000     0x2000        0x0  rw-p
      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     0x7ffff7ffd000     0x2000    0x30000  r--p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
      0x7ffff7ffd000     0x7ffff7fff000     0x2000    0x32000  rw-p   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0  rwxp   [stack]

NX については以上です。

PIEとASLRについて具体的に確認する

現在は、普通にビルドすると、PIE が有効になっていて、実行するプログラムの配置アドレスは変化します。ASLR は、Linux なら通常は 2 になってるそうで、上で言った通り、共有ライブラリ、ヒープ、スタックの配置アドレスをランダム化します。

具体的に確認してみます。実行するプログラム、スタック、ヒープ、共有ライブラリのアドレスの全てが、確かに変化しています。

$ ./hello_hello.out 
  main: 0x558b2bd4018d
   buf: 0x7ffdfa3c3df0
  mbuf: 0x558b2cd972a0
malloc: 0x7f5973187860
input data: 1
input data2: 2
result: 3

$ ./hello_hello.out 
  main: 0x563e7e40d18d
   buf: 0x7ffc769104a0
  mbuf: 0x563e7fd652a0
malloc: 0x7efd5c029860
input data: 2
input data2: 3
result: 5

ASLR を一時的に 1 に変更してみます。1 でも、全て変化しています。

$ sudo su

# cat /proc/sys/kernel/randomize_va_space
2

# sysctl -w kernel.randomize_va_space=1
kernel.randomize_va_space = 1

# cat /proc/sys/kernel/randomize_va_space
1

# exit

$ ./hello_hello.out 
  main: 0x55bf00e6518d
   buf: 0x7fffbf786810
  mbuf: 0x55bf00e692a0
malloc: 0x7f386ad62860
input data: 1        
input data2: 2
result: 3

$ ./hello_hello.out 
  main: 0x55c26238418d
   buf: 0x7fffcde31f50
  mbuf: 0x55c2623882a0
malloc: 0x7f3cd7205860
input data: 1
input data2: 3
result: 4

では、0 にして、同じように確認してみます。全て一致しました。実行プログラムは変化するかな?と思いましたが、一致しました。

$ sudo su

# sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0

# cat /proc/sys/kernel/randomize_va_space
0

#exit

$ ./hello_hello.out 
  main: 0x55555555518d
   buf: 0x7fffffffdf30
  mbuf: 0x5555555592a0
malloc: 0x7ffff7e62860
input data: 1
input data2: 2
result: 3

$ ./hello_hello.out 
  main: 0x55555555518d
   buf: 0x7fffffffdf30
  mbuf: 0x5555555592a0
malloc: 0x7ffff7e62860
input data: 3
input data2: 4
result: 7

ASLR は、元の 2 に戻しておきます。

$ sudo su

# sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2

# cat /proc/sys/kernel/randomize_va_space
2

# exit

一方で、PIE を無効にした場合も確認してみます。-no-pie を付けると、PIE を無効にできます。

checksec を実行すると、No PIE ということで、PIE を無効化できています。実行してみると、上と比べて、ずいぶん小さいアドレスになりました。実行するプログラムは同じアドレスに配置されていますが、それ以外の領域は変化しています。

ASLR を 0 にしたときは、-no-pie を付けなくても、実行するプログラムも同じアドレスに配置されました。アドレスを見ると、同じアドレスですが、大きなアドレスに配置されていましたので、ASLR の効果は出ていることが分かります。PIE は ASLR が 0 ではないことが前提のセキュリティ機構なのかもしれません。

$ gcc -g -no-pie -Wl,-Map=hello_hello_nopie.map -o hello_hello_nopie.out hello_hello.c 

$ ../../tools/checksec.sh-2.7.1/checksec --file=./hello_hello_nopie.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols     FORTIFY Fortified   Fortifiable FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   37 Symbols  No      0           1           ./hello_hello_nopie.out

$ ./hello_hello_nopie.out
  main: 0x40118a
   buf: 0x7ffeb1e5e4d0
  mbuf: 0x5fd2a0
malloc: 0x7fad9d01d860
input data: 1
input data2: 2
result: 3

$ ./hello_hello_nopie.out
  main: 0x40118a
   buf: 0x7ffd6570c2d0
  mbuf: 0x21102a0
malloc: 0x7fb018f7a860
input data: 2
input data2: 3
result: 5

おわりに

今回は、checksec の結果を理解するために、いろいろ調べたり、実際に GDB で実行して確認をしました。だいぶ理解が進んだ気がします。

checksec が対応していないセキュリティ機構もあるらしいので、また分かったらこの記事に追記しようと思います。

今回は、ChatGPT にロゴを作ってもらいました。1日2回まで無料アカウントでも作ってもらえるそうです。今回は、運よく1回でいい感じの画像を生成してくれました。

お願いした内容は「checksec というツールのロゴ(PNG画像)を 200x200 のサイズで作ってください」です(笑)。PNG画像とお願いしましたが、webpという拡張子の画像ファイルが作られました。オンラインのツールでPNG画像に簡単に変換できました。

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

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

今回は以上です!

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