はじめに
この記事では、私が 2025 年 12 月 15 日に Daily AlpacaHack で出題した Flag Printer 2100 の解説をします。問題を先に解きたい方は こちら からどうぞ。
問題文
75 年後にフラグを出力するプログラムを作ったので、2100 年まで待ってね!
添付ファイル: print_flag (ELF ファイル)
解説
まず、与えられたファイルを ghidra などで静的解析します。main 関数を見ると、以下のようなコードが見えます。
int main() { printf("I'll sleep for 75 years...\n"); sleep(0x8d12cea0); // Sleep for 75 years show_flag(); return 0; }
このコードは、75 年間スリープした後に show_flag 関数を呼び出すというものです。ということで、問題文の通りに 75 年間待つことで解くことができそうですが、現実的ではありません。
show_flag 関数を見てみると、とても静的に動作を解析したくならない見た目をしています。そこで、動的解析を行うことを考えます。以下にいくつかの方針を示します。
1. パッチを当てる
バイナリエディタを用いて機械語を直接書き換えることを考えます。
まず、main 関数の objdump の結果を示します。
00000000000019d0 <main>: 19d0: f3 0f 1e fa endbr64 19d4: 55 push %rbp 19d5: 48 89 e5 mov %rsp,%rbp 19d8: 48 8d 05 31 06 00 00 lea 0x631(%rip),%rax # 2010 <_IO_stdin_used+0x10> 19df: 48 89 c7 mov %rax,%rdi 19e2: e8 a9 f6 ff ff call 1090 <puts@plt> 19e7: bf a0 ce 12 8d mov $0x8d12cea0,%edi 19ec: e8 bf f6 ff ff call 10b0 <sleep@plt> 19f1: b8 00 00 00 00 mov $0x0,%eax 19f6: e8 b5 f7 ff ff call 11b0 <show_flag> 19fb: b8 00 00 00 00 mov $0x0,%eax 1a00: 5d pop %rbp 1a01: c3 ret
ここにパッチを当てます。ここでは 2 通りの方法を示します。
まず、sleep に渡す引数を 0 にしてしまう方法です。
19e7: bf a0 ce 12 8d mov $0x8d12cea0,%edi
を
19e7: bf 00 00 00 00 mov $0x0,%edi
に変えることで sleep する時間が 0 秒になります。

もう 1 つは、sleep を呼び出している箇所を nop 命令で置き換える方法です。
19ec: e8 bf f6 ff ff call 10b0 <sleep@plt>
を
19ec: 90 nop 19ed: 90 nop 19ee: 90 nop 19ef: 90 nop 19f0: 90 nop
に変えることで、そもそも sleep が呼び出されなくなります。

2. デバッガーを利用する
デバッガーを用いてもこの問題を解くことができます。ここでは gdb を利用する解き方を示します。
もし gdb が入っていない場合
sudo apt install gdb
で導入できます。
まずは
gdb ./print_flag
で gdb を起動しましょう。
起動したら
run
または
r
で一度プログラムを実行し、途中で Ctrl+C で停止して、
disassemble main
または
disas main
を実行して main 関数のアドレスを確認してみましょう。
0x00005555555559d0 <+0>: endbr64 0x00005555555559d4 <+4>: push rbp 0x00005555555559d5 <+5>: mov rbp,rsp 0x00005555555559d8 <+8>: lea rax,[rip+0x631] # 0x555555556010 0x00005555555559df <+15>: mov rdi,rax 0x00005555555559e2 <+18>: call 0x555555555090 <puts@plt> 0x00005555555559e7 <+23>: mov edi,0x8d12cea0 0x00005555555559ec <+28>: call 0x5555555550b0 <sleep@plt> 0x00005555555559f1 <+33>: mov eax,0x0 0x00005555555559f6 <+38>: call 0x5555555551b0 <show_flag> 0x00005555555559fb <+43>: mov eax,0x0 0x0000555555555a00 <+48>: pop rbp 0x0000555555555a01 <+49>: ret
break *0x00005555555559ec
または
b *0x00005555555559ec
を実行して sleep 関数を呼ぶ地点でブレークポイントを設定した上で、再実行してみましょう。
ブレークポイントで止まったら、
info registers
または
i registers
でレジスタの状態を見ることができます。sleep 関数を呼ぶときの第一引数は rdi レジスタで渡されるので、
set $rdi=0
で第一引数を 0 に変更することで、sleep する時間を 0 秒できます。
continue
または
c
でプログラムの実行を再開すると、フラグが得られます。
あるいは、ブレークポイントから
0x00005555555559f6 <+38>: call 0x5555555551b0 <show_flag>
に処理を飛ばすことでもこの問題を解くことができます。
jump *0x00005555555559f6
または
j *0x00005555555559f6
を実行すればよいです。
3. その他の方法
他にも、LD_PRELOAD を使って sleep 関数をフックする方法や、show_flag を頑張って解析する方法など、さまざまな方法が考えられます。
まとめ
この問題を静的解析で解くのは大変ですが、動的解析を行うことで楽に解くことができました。
flag: Alpaca{G00d_Morning_AlpacaH4ck!}