Challenge
WeChall 上的 The
Last Hope (Linux, Cracking),由一个 32-bit ELF 二进制构成。
反调试绕过
二进制有 5 层反调试,必须全部 patch 掉才能正常运行或调试。
先用 rabin2 获取基本信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ rabin2 -I bsd_thelasthope.elf arch x86 binsz 10569 bintype elf bits 32 canary false class ELF32 compiler GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3 nx true os linux pic false relocs false static false stripped false subsys linux
Layer 1: .ctors
构造函数
在 main() 之前,.ctors 段中的
anti_ptrace 就会被调用:
1 2 $ r2 -q -c 'pf. S .ctors' bsd_thelasthope.elf 0x0804af00 ffffffff 5b8c0408 00000000 ....[.......
0x08048c5b 即 anti_ptrace。将其 patch 为
0xffffffff(sentinel 值),让反向遍历提前终止:
1 2 # .ctors entry @ VA 0x0804af04 → 文件偏移 = VA - 0x08049000 = 0x1f04 printf '\xff\xff\xff\xff' | dd of=bsd_thelasthope.elf bs=1 seek=$((0x1f04)) conv=notrunc
Layer 2
& 3: main 中的 anti_ptrace 调用 + SIGTRAP handler
用 r2 查看 main 的 disassembly:
1 $ r2 -A -q -c 'pdf @ main' bsd_thelasthope.elf
关键片段:
1 2 3 4 0x08048d59: call anti_ptrace 0x08048d5e: mov dword [esp+4], handler ; signal handler 0x08048d66: mov dword [esp], 5 ; SIGTRAP 0x08048d6d: call signal
Patch 这两处为 NOP:
1 2 # VA 0x08048d59 → 文件偏移 0x0d59: 5 bytes → NOP # VA 0x08048d6d → 文件偏移 0x0d6d: 5 bytes → NOP
Layer 4: INT3 + breakpoint
检测
1 2 3 4 5 6 7 8 0x08048d72: int3 ; SIGTRAP → handler 吞掉 0x08048d73: mov eax, 0x8048bd1 ; pw_check 地址 0x08048d78: add eax, 3 0x08048d7b: mov eax, [eax] 0x08048d7d: and eax, 0xff 0x08048d82: cmp eax, 0xcc ; 检查 pw_check+3 是否被设了 0xCC (int3) 0x08048d87: jne ... ; 如果发现 0xCC → 打印 "no,no breakpoints" → exit
Patch 方案:把 int3 (VA 0x08048d72 → 文件偏移 0x0d72)
NOP 掉,把 jne (VA 0x08048d87 → 0x0d87) 改为
jmp。
Layer 5:
ptrace(PTRACE_TRACEME)
1 2 3 0x08048dc0: call ptrace ; ptrace(PTRACE_TRACEME, 0, 1, 0) 0x08048dc5: test eax, eax 0x08048dc7: jns ... ; ≥0 → 正常; <0 → "oh oh DEBUGGING... Bye"
由于 Layer 1 的 fork 子进程已经 ptrace(ATTACH)
了父进程,PTRACE_TRACEME 必然失败(一个进程只能被一个 tracer
追踪)。改为 xor eax, eax:
1 # VA 0x08048dc0 → 文件偏移 0x0dc0: 5 bytes → xor eax,eax; nop; nop; nop (31 c0 90 90 90)
完整 patch 脚本见文末。
Username 逆向
(user_check)
函数调用链
用 r2 列出关键函数:
1 2 3 4 $ r2 -q -c 'afl~check' bsd_thelasthope.elf 0x08048863 user_check 0x08048bd1 pw_check 0x080487a6 length_check
main 中的处理流程:
fgets(username, 15, stdin) — 读入包含换行符(如
"whoami\n",strlen=7)
lc(username, len) — 检查前 len-1
个字符不含大写字母
uc(username, len) — 将前 len-1
个字符转为大写(原地修改,内部调用 toupper())
user_check(username, len) — 逐字符验证
由于 fgets
保留换行符,strlen("whoami\n")=7,循环 i=0..5
覆盖全部 6 个有效字符。换行符在 i=6 处被排除。
约束条件(应用于大写 ASCII
值)
用 pwntools 快速提取符号和关键常量:
1 2 3 4 5 6 7 8 from pwn import *elf = ELF('bsd_thelasthope.elf' ) print (f"user_check @ {elf.symbols['user_check' ]:#x} " )target = elf.read(0x08049050 , 10 ) print (f"encrypted target: {target} " )
user_check 对每个位置 i
执行不同的检查,将满足条件的值累加到 accumulator,最终与
0xcd5 (3285) 比较:
0, 1, 4 : 6c±1 双 Fermat + 硬编码,累加
+13c。f0=6c-1, f1=6c+1 均须为基-2 伪素数
2, 5 : 单 Fermat (c 自身),累加 +c。2^(c-1) mod c ==
1
3 : 整除性,累加 +c。c % 5 == 0
硬编码检查(在 fermat 通过后执行):
U[0]: 2×c == 0xae (174) → c = 87 = W
U[1]: 2×c == 0x90 (144) → c = 72 = H
U[4]: 2×c == 0x9a (154) → c = 77 = M
U[0], U[1], U[4] 被固定为 W, H,
M。同时要求 6c±1 为素数对(twin primes 模式):
W=87: 6×87-1=521(✓), 6×87+1=523(✓)
H=72: 6×72-1=431(✓), 6×72+1=433(✓)
M=77: 6×77-1=461(✓), 6×77+1=463(✓)
注意 :positions 0,1,4 的 fermat
检查后有一条看似多余的 c == floor(6c/5) 比较,实际上编译器
magic constant 0x2aaaaaab 做的是除以 6(而非除以
5)。floor(6c/6) == c 恒成立,实为 no-op。
求解
已知 13×(87+72+77) = 3068,剩余:3285 - 3068 = 217。
在 A-Z (65-90) 范围内筛选:
U[2], U[5] 须为 Fermat 伪素数: C, G, I, O, S, Y
U[3] 须被 5 整除: A, F, K, P, U, Z
求 U[2] + U[3] + U[5] = 217 的解:
1 2 3 U[2]=G(71), U[3]=K(75), U[5]=G(71) → WHGKMG (whgkmg) U[2]=I(73), U[3]=A(65), U[5]=O(79) → WHIAMO (whiamo) U[2]=O(79), U[3]=A(65), U[5]=I(73) → WHOAMI (whoami)
唯一形成有意义单词的是 WHOAMI,对应输入
whoami(程序通过 uc() 自动转大写)。
z3 求解(替代方案)
此题约束本质是 CSP:6 个变量、有限域(A-Z)、线性方程 + 素性谓词。z3
的 table-driven 模式很适合——预计算 26 个字符中哪些满足各位置约束,交给
z3 解线性部分:
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 47 48 49 50 51 52 53 from z3 import *def is_fermat_prime (n ): if n < 2 : return False return pow (2 , n-1 , n) == 1 valid_014 = [c for c in range (65 ,91 ) if is_fermat_prime(6 *c-1 ) and is_fermat_prime(6 *c+1 )] valid_25 = [c for c in range (65 ,91 ) if is_fermat_prime(c)] valid_3 = [c for c in range (65 ,91 ) if c % 5 == 0 ] table_014 = {c: Bool(f"twin_{c} " ) for c in range (65 , 91 )} table_25 = {c: Bool(f"prime_{c} " ) for c in range (65 , 91 )} table_3 = {c: Bool(f"mod5_{c} " ) for c in range (65 , 91 )} s = Solver() for c in range (65 , 91 ): s.add(table_014[c] == (c in valid_014)) s.add(table_25[c] == (c in valid_25)) s.add(table_3[c] == (c % 5 == 0 )) U = [BitVec(f'U{i} ' , 16 ) for i in range (6 )] for i in range (6 ): s.add(U[i] >= 65 , U[i] <= 90 ) s.add(Or([And(U[0 ] == c, table_014[c]) for c in range (65 , 91 )])) s.add(Or([And(U[1 ] == c, table_014[c]) for c in range (65 , 91 )])) s.add(Or([And(U[4 ] == c, table_014[c]) for c in range (65 , 91 )])) s.add(Or([And(U[2 ] == c, table_25[c]) for c in range (65 , 91 )])) s.add(Or([And(U[5 ] == c, table_25[c]) for c in range (65 , 91 )])) s.add(Or([And(U[3 ] == c, table_3[c]) for c in range (65 , 91 )])) s.add(2 * U[0 ] == 174 ) s.add(2 * U[1 ] == 144 ) s.add(2 * U[4 ] == 154 ) s.add(13 * (U[0 ] + U[1 ] + U[4 ]) + U[2 ] + U[3 ] + U[5 ] == 3285 ) while s.check() == sat: m = s.model() name = '' .join(chr (m[U[i]].as_long()) for i in range (6 )) print (f"{name} → {name.lower()} " ) s.add(Or([U[i] != m[U[i]] for i in range (6 )]))
为什么不用 angr? 此题有 x87
浮点指令(fprem/fmod 做 Fermat 检验)+
fork/ptrace 反调试 + INT3 断点检测。angr 的
VEX IR 对 x87 浮点栈支持弱,fork 会直接 concretize,且
patch 5 层反调试后 angr 能做的也只是验证已知路径——不如直接 z3
解约束。
Password 逆向
(pw_check)
XOR 加密
用 r2 反编译 encrypt 函数:
1 $ r2 -A -q -c 'pdf @ sym.encrypt' bsd_thelasthope.elf
main 中定义了 15 字节 XOR key(位于
[ebp-0x44]):
1 2 [0x1f, 0x0a, 0x1e, 0x11, 0x0b, 0x09, 0x19, 0x0f, 0x01, 0x14, 0x16, 0x0c, 0x06, 0x0d, 0x65]
encrypt() 函数对密码逐字节 XOR:
1 2 for (i = 0 ; i < len; i++) password[i] ^= key[i % 15 ];
然后 chomp() 去掉末尾换行符,pw_check()
将结果与硬编码字符串比较。
用 pwntools 直接读取目标字符串:
1 2 3 4 from pwn import *elf = ELF('bsd_thelasthope.elf' ) target = elf.read(0x08049050 , 10 ).decode() print (f"target: {target} " )
或直接用 r2:
1 2 $ r2 -q -c 'ps @ 0x08049050' bsd_thelasthope.elf Oxw|n]nfog
求解
直接逆向 XOR,key 和密文等长(均为 10 字节):
1 2 3 4 encrypted = b"Oxw|n]nfog" key = [0x1f , 0x0a , 0x1e , 0x11 , 0x0b , 0x09 , 0x19 , 0x0f , 0x01 , 0x14 ] password = '' .join(chr (e ^ key[i]) for i, e in enumerate (encrypted))
验证
1 2 3 4 5 6 7 8 9 10 O=0x4f ^ 0x1f = 0x50 = P x=0x78 ^ 0x0a = 0x72 = r w=0x77 ^ 0x1e = 0x69 = i |=0x7c ^ 0x11 = 0x6d = m n=0x6e ^ 0x0b = 0x65 = e ]=0x5d ^ 0x09 = 0x54 = T n=0x6e ^ 0x19 = 0x77 = w f=0x66 ^ 0x0f = 0x69 = i o=0x6f ^ 0x01 = 0x6e = n g=0x67 ^ 0x14 = 0x73 = s
完整 Patch 脚本
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 """Patch anti-debug measures in bsd_thelasthope.elf ELF segment layout: text: file 0x000000 → VA 0x08048000 (file_off = VA - 0x08048000) data: file 0x001f00 → VA 0x0804af00 (file_off = VA - 0x08049000) """ import sysdef patch (filepath ): with open (filepath, 'r+b' ) as f: f.seek(0x1f04 ) f.write(b'\xff\xff\xff\xff' ) f.seek(0x0d59 ) f.write(b'\x90' * 5 ) f.seek(0x0d6d ) f.write(b'\x90' * 5 ) f.seek(0x0d72 ) f.write(b'\x90' ) f.seek(0x0d87 ) f.write(b'\xeb' ) f.seek(0x0dc0 ) f.write(b'\x31\xc0\x90\x90\x90' ) print (f"Patched: {filepath} " ) if __name__ == '__main__' : patch(sys.argv[1 ] if len (sys.argv) > 1 else 'bsd_thelasthope.elf' )
Patch 后运行:
1 2 3 4 5 6 $ python3 patch.py bsd_thelasthope.elf $ chmod +x bsd_thelasthope.elf $ ./bsd_thelasthope.elf User: whoami Password: PrimeTwins Correct !! The solution is username_password
whoami_PrimeTwins