前回 は、Pwnable問題に取り組みました。CTF のカテゴリの中でも、一番難しいと言われるだけあって、かなり苦労しました。
今回は、前回の Pwnable問題でも使用した、実行ファイルのセキュリティ機構(脆弱性緩和技術とも言う)を調べるツールである「checksec」の深掘りをしたいと思います。
実行ファイルのセキュリティ機構とは、もし、実行ファイルに脆弱性が存在していたとしても、その脆弱性に対する攻撃をやりにくくする仕組みのことです。
例えば、スタックカナリヤは、関数開始時にスタックにランダムな値を格納しておき、関数終了時に、その値が変化していないかをチェックします。これによって、スタックを使った攻撃を成功させにくくすることが出来ます。
実行ファイルのセキュリティ機構には、いくつか種類があるので、それらの詳細と、実際に、コンパイルしたり、GDB で確認したりしてみたいと思います。
それでは、やっていきます。
参考文献
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
セキュリティの記事一覧
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]
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]
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]
0x00005555555551b1 <+36>: mov rsi,rax
0x00005555555551b4 <+39>: lea rax,[rip+0xe5a]
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]
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]
0x00005555555551f1 <+100>: mov rdi,rax
0x00005555555551f4 <+103>: mov eax,0x0
0x00005555555551f9 <+108>: call 0x555555555030 <printf@plt>
malloc関数や、printf関数は、@plt
というのが付いてます。MAPファイルを確認してみます。0x555555554000
に 0x1020
を足すと、PLT のアドレスになります。printf関数は、0x1030
と書かれてるので、0x555555554000+0x1030=0x555555555030
となり、上の逆アセンブラ表示のアドレスと一致しています。1つの PLT のエントリが 16byte となっています。PLT の領域は、hello_hello.out に含まれています。
.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]
0x55555555502c: nop DWORD PTR [rax+0x0]
=> 0x555555555030 <printf@plt>: jmp QWORD PTR [rip+0x2fca]
| 0x555555555036 <printf@plt+6>: push 0x0
| 0x55555555503b <printf@plt+11>: jmp 0x555555555020
| 0x555555555040 <__isoc99_scanf@plt>: jmp QWORD PTR [rip+0x2fc2]
| 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]
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
と .got.plt
は、両方とも GOT領域と呼ばれるそうです。どちらも、hello_hello.out に含まれています。
.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 が下に表示してくれているように、0x555555555036
の push 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]
0x55555555502c: nop DWORD PTR [rax+0x0]
0x555555555030 <printf@plt>: jmp QWORD PTR [rip+0x2fca]
=> 0x555555555036 <printf@plt+6>: push 0x0
0x55555555503b <printf@plt+11>: jmp 0x555555555020
0x555555555040 <__isoc99_scanf@plt>: jmp QWORD PTR [rip+0x2fc2]
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]
| 0x55555555502c: nop DWORD PTR [rax+0x0]
| 0x555555555030 <printf@plt>: jmp QWORD PTR [rip+0x2fca]
| 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]
JUMP is taken
この中は、ちょっと長そうなので、次の printf関数の呼び出しまで進めます。直前まで進めました。
[-------------------------------------code-------------------------------------]
0x5555555551cf <main+66>: lea rax,[rip+0xe4b]
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]
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]
0x55555555502c: nop DWORD PTR [rax+0x0]
=> 0x555555555030 <printf@plt>: jmp QWORD PTR [rip+0x2fca]
| 0x555555555036 <printf@plt+6>: push 0x0
| 0x55555555503b <printf@plt+11>: jmp 0x555555555020
| 0x555555555040 <__isoc99_scanf@plt>: jmp QWORD PTR [rip+0x2fc2]
| 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(hello_hello.out に含まれている 16byteのプログラムだった)には、got.plt(hello_hello.out に含まれている、共有ライブラリをコールするためのアドレスを格納したテーブルだった)からアドレスを取得してジャンプする処理と、1回目の printf関数で実行されていた _dl_runtime_resolve_xsave
というアドレス解決をしてくれそうな関数にジャンプする処理が配置されていました。
2回目の printf関数では、同じように、plt に入って、同様に、got.plt からアドレスを取得しましたが、この got.plt に格納されたアドレスが、printf関数のアドレスに変化していました。1回目でアドレス解決して、got.plt に printf関数のアドレスを格納してくれたので、2回目以降は直接 printf関数にジャンプできるようになったということです。
今回確認したのは、plt と got.plt でした。しかし、GOT領域は、got と got.plt というセクションのことです。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 のサイズがあり(0x3fb8+0x30=0x3fe8)、次に、.got.plt
が 0x3fe8 から始まり、0x28 のサイズがあります(0x3fe8+0x28=0x4010)。0x555555554000 から 0x555555558000 までは書き込み禁止であり、0x555555558000 以降は書き込み可能です。つまり、.got.plt
の末尾の 16byte だけが書き込み可能ということになります。
一方、now を指定した方は、.got
が 0x3fa8 から始まり、0x58 のサイズがあるだけです(0x3fa8+0x58=0x4000)。.got.plt
があるように見えますが、これはサブセクションというらしく(from ChatGPT)、.got
の中に含まれています。つまり、now を指定した方は、.got.plt
というセクションは無くなっていて、.got
に含まれる形になっていました。readelfコマンドの -S オプションで確認したところ、.got.plt
というセクションはありませんでした。GOT領域は、書き込み禁止ということになります。
/* 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]
0x55555555502c: nop DWORD PTR [rax+0x0]
=> 0x555555555030 <printf@plt>: jmp QWORD PTR [rip+0x2f8a]
| 0x555555555036 <printf@plt+6>: push 0x0
| 0x55555555503b <printf@plt+11>: jmp 0x555555555020
| 0x555555555040 <__isoc99_scanf@plt>: jmp QWORD PTR [rip+0x2f82]
| 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]
0x55555555502c: nop DWORD PTR [rax+0x0]
=> 0x555555555030 <printf@plt>: jmp QWORD PTR [rip+0x2fca]
| 0x555555555036 <printf@plt+6>: push 0x0
| 0x55555555503b <printf@plt+11>: jmp 0x555555555020
| 0x555555555040 <__isoc99_scanf@plt>: jmp QWORD PTR [rip+0x2fc2]
| 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]
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]
0x555555555046 <__isoc99_scanf@plt+6>: push 0x1
0x55555555504b <__isoc99_scanf@plt+11>: jmp 0x555555555020
=> 0x555555555050 <malloc@plt>: jmp QWORD PTR [rip+0x2f7a]
| 0x555555555056 <malloc@plt+6>: xchg ax,ax
| 0x555555555058 <__cxa_finalize@plt>: jmp QWORD PTR [rip+0x2f82]
| 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]
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]
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
2
kernel.randomize_va_space = 1
1
$ ./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
kernel.randomize_va_space = 0
0
$ ./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
kernel.randomize_va_space = 2
2
一方で、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画像に簡単に変換できました。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。