HackThisSite - Application Mission 7

Challenge

Find the Password (medium) 附件是一个要求输入密码的控制台程序:它把输入字符求和当 XOR key 来解密 encrypted.enc,只有算出的校验和命中目标值才把解密出的口令回显出来。

附件含 Windows 版 app7win.zipapp7win.exe)和 Linux 版 app7unix.tar.gz(ELF app7unix),以及加密文件 encrypted.enc(480 字节)。程序把输入字符逐字节求和得到 key,用 key XOR encrypted.enc 的前 5 字节、边解边累加校验和;当校验和等于 0xdca 时,用 printf("Congratulations, The password is '%s'") 把解密结果当作口令打印出来。

Solution

Recon:

  • file unix/app7unix win/app7win.exe work/encrypted.encapp7unixELF 64-bit LSB pie executable, x86-64, not strippedapp7win.exePE32 executable for MS Windows 4.00 (console), Intel i386, 3 sectionsencrypted.encdata
  • strings -a unix/app7unix 里有 Please enter the password:encrypted.encFailed to open encrypted.encAn error occuredCongratulations, The password is '%s'Invalid Password,还残留源文件名 app7win.c:Linux 版和 Windows 版来自同一份源码。
  • encrypted.enc 只有头几字节可打印(31 4d 39 35 331M953),后面几乎全是高位字节;480 字节里程序实际只碰前 5 字节,其余是干扰。

Step 1: 定位校验与解密逻辑

符号没去掉,直接反汇编 main。Windows 版逻辑更直白:

1
$ objdump -d -M intel --start-address=0x401000 --stop-address=0x4011d0 win/app7win.exe

关键片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
401047:  movsx  ecx,BYTE PTR [ebp-0x4]   ; c = getchar()
40104b: mov edx,DWORD PTR [ebp-0x1c]
40104e: add edx,ecx ; key += (signed char)c
401050: mov DWORD PTR [ebp-0x1c],edx
401057: cmp eax,0xa ; 读到换行 '\n' 或 '\0' 才停
401062: jne 0x40103f
401093: mov edx,DWORD PTR [ebp-0x24] ; i
401096: and edx,0x4
401099: test edx,edx
40109b: je 0x4010ab ; (i & 4)==0 -> 继续读
40109d: mov eax,DWORD PTR [ebp-0x24]
4010a0: and eax,0x1
4010a3: test eax,eax
4010a5: jne 0x401180 ; (i&4)&&(i&1) -> 退出读取循环
4010d8: mov eax,DWORD PTR [ebp-0x20] ; 刚 fread 到的字节
4010db: and eax,0xff
4010e0: xor eax,DWORD PTR [ebp-0x1c] ; ^ key(完整 32 位)
4010e3: mov ecx,DWORD PTR [ebp-0x18]
4010e6: add ecx,eax
4010e8: mov DWORD PTR [ebp-0x18],ecx ; checksum += (byte ^ key)
4010eb: mov edx,DWORD PTR [ebp-0x20]
4010ee: and edx,0xff
4010f4: xor edx,DWORD PTR [ebp-0x1c]
4010fa: mov BYTE PTR [ebp+eax*1-0x14],dl ; buffer[i] = (byte ^ key) & 0xff
401131: sar eax,1 ; buffer[i] >>= 1
401143: or al,0x80 ; 原值奇数时补回 bit7
401169: add al,0x3 ; buffer[i] += 3
40118c: cmp DWORD PTR [ebp-0x18],0xdca ; checksum == 0xdca ?
401193: jne 0x4011a8 ; 不等 -> "Invalid Password"
401199: push 0x408094 ; "Congratulations, The password is '%s'"

推理:

  • key 是输入所有字符按 signed char 的求和,包含结尾换行 0x0a(先 add 再判换行)。
  • 读取循环的条件是 (i&4) && (i&1)i=0..3i&4==0 继续,i=4i&1==0 继续,处理完第 5 个字节后 i=55&45&1 都非零)退出,所以只读 encrypted.enc 的前 5 字节。
  • 每字节:buffer[i] = (byte ^ key) & 0xff,然后重复 keybuffer[i] = (buffer[i] >> 1) | ((buffer[i] & 1) << 7),最后 buffer[i] += 3
  • checksum 累加的是完整 32 位(byte ^ key),而不是低字节。

Step 2: 解出 key

校验和是 key 的确定函数,方程只有 5 项、原文数(encrypted.enc[:5])已知,直接把 key 搜索出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python3
"""Recover the HTS Application 7 password from encrypted.enc.

app7win.exe reads the first 5 bytes of encrypted.enc, XORs each of them with a
32-bit key derived from the login input (key = sum of the signed input bytes,
the trailing newline included), accumulates the *full* 32-bit XOR results into
a checksum, and prints "The password is '%s'" only when that checksum equals
0xdca. Reproducing the checksum lets us solve for the single key that passes
the gate; the same key turns encrypted.enc into the plaintext password.
"""
MASK = 0xffffffff
TARGET = 0xdca # cmp [checksum],0xdca in main
enc = open("encrypted.enc", "rb").read(5)


def checksum(key):
"""Windows build: sum of (byte ^ key) as full 32-bit values."""
total = 0
for b in enc:
total = (total + (b ^ key)) & MASK
return total


def transform(raw, key):
"""Per byte: repeat {buf = buf>>1 | ((buf&1)<<7)} key times, then += 3."""
out = bytearray()
for v in raw:
for _ in range(key):
v = (v >> 1) | (0x80 if v & 1 else 0)
out.append((v + 3) & 0xff)
return bytes(out)


def main():
key = next(k for k in range(1 << 20) if checksum(k) == TARGET)
print(f"checksum {TARGET:#x} reached with key = {key} ({key:#x})")
raw = bytes((b ^ key) & 0xff for b in enc)
print(f"encrypted.enc[:5] ^ key = {raw.hex()}")
pw = transform(raw, key)
print(f"plaintext password = {pw.decode()!r}")
# any login whose signed-byte sum (newline included) equals key passes the gate
print(f"login input needs signed-byte sum {key} (newline adds 10)")


if __name__ == "__main__":
main()
1
2
3
4
5
$ cd <hts-workspace>/challenges/hts-app/app7/work && cp ../solve.py . && uv run python solve.py
checksum 0xdca reached with key = 753 (0x2f1)
encrypted.enc[:5] ^ key = c0bcc8c4c2
plaintext password = 'caged'
login input needs signed-byte sum 753 (newline adds 10)

key 唯一等于 7530x2f1):把 encrypted.enc 前 5 字节 31 4d 39 35 33 与 753 相 XOR 得 c0 bc c8 c4 c2,再套移位循环(753 次,周期 8,等价 1 次)与 +3,还原出 caged

Step 3: 构造登录串并验证

key = 753,结尾换行贡献 10,所以输入字符之和必须是 743。取 7*'a' + '@' = 679 + 64 = 743

1
2
3
$ printf 'aaaaaaa@\n' | wine app7win.exe 2>/dev/null | tr -d '\r'
Please enter the password:
Congratulations, The password is 'caged'

程序在成功路径直接把解密出的口令回显出来,caged 就是提交给 HTS 的密码。

Step 4: Linux 版为什么跑不出结果

在同一目录用同样的输入跑 Linux 版,只得到 Invalid Password

1
2
3
$ printf 'aaaaaaa@\n' | ../unix/app7unix
Please enter the password:
Invalid Password

用 gdb 在校验点(main+0x1c9)断下,dump 运行时的 key / checksum / buffer:

1
2
3
4
5
6
7
$ gdb -q -batch -ex 'set pagination off' -ex 'break main' -ex 'run < /tmp/in2.txt' \
-ex 'break *main+0x1c9' -ex 'continue' -ex 'x/3dw $rbp-0x38' -ex 'x/6bx $rbp-0x20' \
../unix/app7unix
== key / checksum / i ==
0x7fffffffc6b8: 753 -310 5
== buffer ==
0x7fffffffc6d0: 0x02 0x02 0x02 0x02 0x02 0x00

key 同样是 753,但 Linux 版的 checksum-310buffer 也被移位循环饱和成了 0x02。原因在它的反汇编里:

1
2
3
4
5
6
12ff:  mov    eax,DWORD PTR [rbp-0x3c]  ; 读入的字节
1302: xor eax,edx ; ^ key
130b: mov BYTE PTR [rbp+rax*1-0x20],dl ; buffer[i] = (byte ^ key) & 0xff
1314: movzx eax,BYTE PTR [rbp+rax*1-0x20]
1319: movsx eax,al ; 只取低字节并符号扩展
131c: add DWORD PTR [rbp-0x34],eax ; checksum += signed(low byte)
  • Windows 版累加完整 32 位 (byte ^ key);Linux 版先 movzxmovsx,只累加低字节的符号扩展值,单个字节最大 127,5 个字节能到的上限是 5 * 127 = 635 < 0xdca(因为 checksum 实际只依赖 key & 0xff,穷举 key 的 256 种取值也无一命中),任何输入都过不了校验。
  • Linux 版移位用的是 8 位 sar al,1(保留符号位),会把结果饱和到 0xff+3 变成 0x02;Windows 版是零扩展 32 位 sar eax,1,才会得到 caged

所以 app7unix 是一份有偏差的移植,永远只会打印 Invalid Password;正确口令必须用 app7win.exe(Wine)跑,或按上面的算法离线复现。

caged