# 2025-12-17_"login-bonus": 251217 <https://alpacahack.com/daily/challenges/login-bonus> Level: Medium Type: Pwn solved day: 2025,12,17 ## solution ```sh ( python -c 'import sys; sys.stdout.buffer.write(b"A"*16 + b"\x00" + b"B"*15 + b"A"*16 + b"\n")' ; cat ) \ | nc 34.170.146.252 6555 [DEBUG] Generating secure password... Password: [DEBUG] Authenticicating... [+] Success! find / -maxdepth 1 -name 'flag-*' -type f -print /flag-d592fd27eb4de74af194fda7990796ec.txt cat /flag-d592fd27eb4de74af194fda7990796ec.txt Alpaca{h0w_d1d_U_gu3s5_i7} exit ``` ## How I thought. ```sh eza --tree . ├── compose.yaml ├── Dockerfile ├── flag.txt ├── login └── login.c ``` * 修正: `commen-login.c` は存在しない(typo/不要)。上の構成が実体。 ### 1. `login.c` を読む ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/random.h> #define debug_report(fmt, ...) printf("[DEBUG] " fmt "\n", ##__VA_ARGS__) char password[32]; char secret[32]; int main() { /* Input password */ printf("Password: "); scanf("%[^\n]", password); /* Check password */ debug_report("Authenticating..."); if (strcmp(password, secret)) { puts("[-] Wrong password"); debug_report("'%s' != '%s'", password, secret); } else { puts("[+] Success!"); system("/bin/sh"); } return 0; } __attribute__((constructor)) void setup() { int seed; setbuf(stdin, NULL); setbuf(stdout, NULL); /* Generate random password */ debug_report("Generating secure password..."); getrandom(&seed, sizeof(seed), 0); srand(seed); for (size_t i = 0; i < 16; i++) secret[i] = 'A' + (rand() % 26); } ``` * 挙動の要約: * `scanf("%[^\n]")` で改行までを **幅指定なし** で `password[32]` に読み込む。 * `secret` は A〜Z の英大文字16文字が設定される(`setup` で毎回生成)。 * 比較は `strcmp(password, secret)`(戻り値 0 が一致)。 * `debug_report("%s")` が `password` と `secret` を「文字列」として表示。 * 修正: 「`password`, `secret`それぞれに32文字分割り当てられているが、実際に使われているパスワード部は16文字だけ」という表現は紛らわしい。正しくは * `secret` は「英大文字16文字」+ グローバル配列の性質でその先は 0(ヌル)になり、**文字列としては長さ16**。 * `password` は **入力次第で任意長**だが、途中に NUL(`\x00`)を入れれば **文字列としての長さをそこで切れる**。 ## 2. どこが問題か(設計上の穴) * 入力が **幅指定なし**(`scanf("%[^\n]")`)なので、改行までのバイト列が **`password[32]` を超えても書き込まれる**。 → 実環境では `password` の直後に `secret` が .bss セクションで **連続配置**されがちなので、**境界外書き込みが `secret[0..]` を侵食**する。 * 文字列の終端(NUL, `'\0'`)に依存する比較 (`strcmp`) を使っている。 → ライブラリやフォーマットにより \*\*「どこに NUL が入るか」\*\*が勝敗を決める。 * デバッグで `'%s' != '%s'` を出しているため、クライアントに **文字列としての見え方(終端位置・長さ)** が露出する。 ## 3. 実際にどう崩したか(ペイロード設計) 入力のバイト並びを、次のように設計した。 S(16) = A×16 → password[0..15] NUL = \x00 → password[16] で「文字列」としての password を終端 X(15) = B×15 → password[17..31] をちょうど埋めて終わる(ここではまだ侵食しない) Y(16) = A×16 → 次の書き込み先は secret[0..15] NL = \n → scanf が読み取りを終了 * ポイント: * NUL で `password` の「文字列」としての長さを 16 に固定。 * NUL の後に **ちょうど 15 バイト**を書き、`password` の残りを埋め切ってから、次の 16 バイトが **`secret[0..15]` に落ちる**ように位置合わせ。 * これで **`secret` の「文字列」も A×16** になる。 → `strcmp(password, secret) == 0`(内容+長さが完全一致)で `[+] Success!`。 * 代替パターン(一般化): * `password` の「文字列」先頭16文字を `C0 + C1×15` にしたいなら、 * `S = C0 + C1×15` * NUL * `X = C0×15`(ここでは 15) * `Y = C1×16` * NL とすれば、`secret` は `C0 + C1×15` になる(位置合わせの考え方は同じ)。 ## 4. これが成り立つ理由(仕様と実装の間) * グローバルの `char secret[32]` は **未初期化でも 0 で埋まる**(BSS ゼロ初期化)。 → `setup` が 0..15 に英大文字を入れ、16 は 0 のまま。**文字列としては長さ16**。 * `scanf("%[^\n]")` は **改行で停止**するため、途中の NUL(`\x00`)は \*\*「普通のバイト」\*\*として配列に書かれる。 → ライブラリは書き終えた位置に **終端 NUL** も付けるので、`secret[16]` にも 0 が残る/付く。 * `strcmp` は **最初の NUL まで**を比較する。 → 文字列としての長さと内容が一致すれば 0(一致)。 ## 5. 実行環境の意図(Docker/xinetd) * `xinetd` が TCP/9999 を待受し、接続ごとに `/usr/bin/timeout 180 /home/pwn/login` を起動。 → STDIN/STDOUT がソケットに直結し、**逐次出力**(`setbuf(..., NULL)`)になる。 → 接続ごとに新プロセスなので、`setup` は毎回走り `secret` は毎回別の乱数列。 ## 6. 防御策(運用・コード修正) * 入力に **幅指定**を入れる: ```c scanf("%31[^\n]", password); // 配列32なら31+終端 // または if (fgets(password, sizeof(password), stdin)) { password[strcspn(password, "\n")] = '\0'; // 改行を除去 } ``` * `secret` を **明示的にヌル終端**する: ```c for (size_t i = 0; i < 16; i++) secret[i] = 'A' + (rand() % 26); secret[16] = '\0'; ``` * 比較の前に **長さも検証**する(完全一致のみ合格): ```c if (strlen(password) != 16 || strcmp(password, secret) != 0) { puts("[-] Wrong password"); return 1; } ``` * デバッグで **秘密を露出しない**: ```c // debug_report("'%s' != '%s'", password, secret); // 情報漏えいにつながる debug_report("len(password)=%zu, len(secret)=%zu", strlen(password), strlen(secret)); ``` ## 7. まとめ * 根本原因は、`scanf("%[^\n]")` が **幅指定なし**で改行まで無制限に書き込むため、`password` の境界外に書き込みが進み **`secret` を侵食**できること * グローバル配列のゼロ初期化と `strcmp` の **NUL終端依存**が重なり、「文字列としての長さ・内容」を **狙って一致に誘導**できた * 実環境では .bss で配列が **宣言順に連続**しがちで、未定義動作でありつつも **隣接配列破壊**が現象として起きやすい * 対策は入力の **幅指定/安全な読み取り**、**明示的な終端**、**長さ検証**、**ログの情報管理**