Challenge
Find the Password (medium) 附件是一个要求输入密码的控制台程序:它把输入字符求和当 XOR key 来解密
encrypted.enc,只有算出的校验和命中目标值才把解密出的口令回显出来。
附件含 Windows 版 app7win.zip(app7win.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.enc→app7unix是ELF 64-bit LSB pie executable, x86-64, not stripped,app7win.exe是PE32 executable for MS Windows 4.00 (console), Intel i386, 3 sections,encrypted.enc是data。strings -a unix/app7unix里有Please enter the password:、encrypted.enc、Failed to open encrypted.enc、An error occured、Congratulations, The password is '%s'、Invalid Password,还残留源文件名app7win.c:Linux 版和 Windows 版来自同一份源码。encrypted.enc只有头几字节可打印(31 4d 39 35 33→1M953),后面几乎全是高位字节;480 字节里程序实际只碰前 5 字节,其余是干扰。
Step 1: 定位校验与解密逻辑
符号没去掉,直接反汇编 main。Windows 版逻辑更直白:
$ objdump -d -M intel --start-address=0x401000 --stop-address=0x4011d0 win/app7win.exe关键片段:
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..3时i&4==0继续,i=4时i&1==0继续,处理完第 5 个字节后i=5(5&4与5&1都非零)退出,所以只读encrypted.enc的前 5 字节。 - 每字节:
buffer[i] = (byte ^ key) & 0xff,然后重复key次buffer[i] = (buffer[i] >> 1) | ((buffer[i] & 1) << 7),最后buffer[i] += 3。 checksum累加的是完整 32 位的(byte ^ key),而不是低字节。
Step 2: 解出 key
校验和是 key 的确定函数,方程只有 5 项、原文数(encrypted.enc[:5])已知,直接把 key 搜索出来:
#!/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 a32-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 intoa checksum, and prints "The password is '%s'" only when that checksum equals0xdca. Reproducing the checksum lets us solve for the single key that passesthe gate; the same key turns encrypted.enc into the plaintext password."""MASK = 0xffffffffTARGET = 0xdca # cmp [checksum],0xdca in mainenc = 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)) & MASK18 collapsed lines
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()$ cd <hts-workspace>/challenges/hts-app/app7/work && cp ../solve.py . && uv run python solve.pychecksum 0xdca reached with key = 753 (0x2f1)encrypted.enc[:5] ^ key = c0bcc8c4c2plaintext password = 'caged'login input needs signed-byte sum 753 (newline adds 10)key 唯一等于 753(0x2f1):把 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:
$ 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:
$ printf 'aaaaaaa@\n' | ../unix/app7unixPlease enter the password:Invalid Password用 gdb 在校验点(main+0x1c9)断下,dump 运行时的 key / checksum / buffer:
$ 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 0x00key 同样是 753,但 Linux 版的 checksum 是 -310,buffer 也被移位循环饱和成了 0x02。原因在它的反汇编里:
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 版先movzx再movsx,只累加低字节的符号扩展值,单个字节最大 127,5 个字节能到的上限是 (因为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