=========================
Adriod + 按步取匙
解题思路(必须包含文字说明+截图)
APK 包名 com.mobilezkp,核心逻辑在 native 层 libzkpcore.so(ARM64)。整个认证流程分四步:
Login — 验证用户名/密码
Challenge 1 — 输入 8 位 hex 验证码
Challenge 2 — 输入 32 位 hex 令牌
Challenge 3 — 输入 16 位 hex 密钥,依赖 intermediate_token
通过全部挑战后,nativeGenerateFlag() 输出最终 flag。
- 静态分析环境
libzkpcore.so:ARM64 动态库
.text:文件偏移 0x1d84,大小 0x3d10
.rodata:文件偏移 0x0f30,大小 0x5f8
工具:Capstone (ARM64 反汇编), Python, ADB
ELF 段映射:文件偏移 = 虚拟地址(PIE,基址 0)
JNI 导出函数(from .dynsym):
函数 地址 大小 nativeInit 0x1de4 0x1fc nativeLogin 0x21cc 0x17c nativeVerifyChallenge1 0x2348 0xe8 nativeVerifyChallenge2 0x2430 0x150 nativeGetIntermediateToken 0x2580 0xb8 nativeVerifyChallenge3WithToken 0x266c 0x14c nativeGenerateFlag 0x27b8 0xf0 JNI_OnLoad 0x28a8 0x40
- 数据区中的关键字符串
偏移 内容 用途 0x100f CTF_ZKP_FLAG_V2 Flag 计算前缀 0x1053 CTF_ZKP_INTERMEDIATE_SEED_V2 intermediate_token 盐值 0x13a5 CTF_ZKP_apk-13_SALT_V3 session_ctx HKDF 盐值 0x13d2 k3y_j4va_k3y!!!! username XOR 常量(截取 j4va_k3y!!!!) 0x13e5 \x11\x58\x09\x00\x07\x55\x05\x15\x3a\x19 username XOR 密钥(10 字节) 0xfe6 AUTH_OK HKDF info 参数 0x10eb t1_static key1 前缀 0x12bf 2_dynamic_\0 key2 前缀(含 null) 0x13bc t3_android_ key3 前缀 0x1388 Zkp_\0 密码第一部分 0x11a2 M_as\0 密码第二部分 0x100a t3er\0 密码第三部分 0x1159 0123456789abcdef hex 转换表
- 用户名/密码逆向
4.1 用户名 (zkp_master)
位于函数 0x2a0c。从 0x13d5 加载 16 字节常量 _j4va_k3y!!!!,与 0x13e5 的 10 字节密钥 XOR:
密钥: 11 58 09 00 07 55 05 15 3a 19
常量: 6b 33 79 5f 6a 34 76 61 5f 6b ("_j4va_k3y" 前 10 字节)
XOR: 7a 6b 70 5f 6d 61 73 74 65 72 → “zkp_master”
4.2 密码 (Zkp_M_ast3er)
密码通过三次字符串拼接构建(函数 0x2ce4):
strcpy(base, 0x1388) → “Zkp_”
strcat(base, 0x11a2) → “M_as”
strcat(base, 0x100a) → “t3er”
结果: “Zkp_M_ast3er”
- Session Context 计算
函数 0x2a0c 在登录验证后,使用 HKDF 算法生成 session_ctx:
5.1 计算各哈希值
concat = “zkp_master” + “Zkp_M_ast3er” + “CTF_ZKP_apk-13_SALT_V3”
↑ 此 SHA256 虽被计算但未使用(可能是废弃代码)
password_hash = SHA256(“Zkp_M_ast3er”)
salt_hash = SHA256(“CTF_ZKP_apk-13_SALT_V3”)
5.2 HKDF-Extract
PRK = HMAC-SHA256(key=salt_hash, data=password_hash)
值 哈希 salt_hash 86e456a838cae42cf0ca33bda4e0a42f0b92c264a1acc68d8a161a7450eeac90 password_hash 8f8f0869d7d2232f46ad5974aa66e4701534ac18697f62b45bff942cb22b63c1 PRK 7450328e52d419161d9857a3584f1285cf74b58e4d68b4128e62eb1541416abc
5.3 HKDF-Expand
T(1) = HMAC-SHA256(key=PRK, data=“AUTH_OK” || 0x01)
session_ctx[0:32] = T(1) = b2e01e06f14181d3dcc98da84a1b620cfe8c65880798edd6baef6b7ca327d53d
session_ctx[32:64] = SHA256(“CTF_ZKP_apk-13_SALT_V3”)
- Challenge 1
6.1 答案
8826FBF1
6.2 验证逻辑(函数 0x336c)
输入必须是 8 位 hex
解析为 32 位整数 hex(“8826FBF1”) = 0x8826FBF1
调用 0x3800 初始化 LCG 状态,0x3910 迭代 7 轮
用 4 组 XOR 常量混合结果
最终检查 result == 0x81BDB091
6.3 Key1 推导
key1 = SHA256(“t1_static_8826FBF1”)[:8]
= 2e3ef2469b9e757e
盐值 “t1_static_"(10 字节)拼接 8 字节输入 → SHA256 → 取前 8 字节。
- Challenge 2
7.1 答案
ffb9b64254eb5012ffb139045e3e3959
7.2 验证逻辑
函数 0x39e0 从 rodata 0x4bf 获取 seed 值 0x0d40727a 和 0x1cfb729d
XOR 得到初始 seed:0x0d40727a ^ 0x1cfb729d = 0x11BB00E7
LCG 7 轮:state = state * 0x49297c7f + 0xc0d9543f
最终 state 0x813bcd58 XOR 0x8b23fd92 = 0x0A1830CA
4 字节反转后 SHA256 → 取前 16 hex 字符
7.3 Key2 推导
salt = b"2_dynamic_\0” # 11 字节,含 null 结尾
key2_input = salt + b"ffb9b64254eb5012ffb139045e3e3959"
key2 = SHA256(key2_input)[:8]
= 61270bb48f63b0af
- Intermediate Token
Challenge 2 验证通过后,nativeGetIntermediateToken() 返回存储在 BSS 0x803d 的令牌。
8.1 计算方式
ch2 = “ffb9b64254eb5012ffb139045e3e3959” # 32 hex chars
seed = “CTF_ZKP_INTERMEDIATE_SEED_V2”
data = ch2.encode() + seed.encode() # 32 + 28 = 60 字节
token = SHA256(data).hex()[:32] # 16 raw bytes → 32 hex chars
token = “1d98f7d4ff3f63a1128cc06f0303d02c”
8.2 存储
函数 0x3c4c 将 SHA256(ch2 || seed) 的前 16 字节通过 0x59cc 转换为 32 hex 字符,写入 0x803d。
- Challenge 3
9.1 答案
d5d0d8b2a381d381
9.2 验证逻辑(函数 0x43d0)
token = “1d98f7d4ff3f63a1128cc06f0303d02c”
检查 token:strcmp(input_token, 0x803d) == 0
读取 token 前 8 个 ASCII 字符 → 构建 64 位大端整数:
accum = 0
for i in range(8):
accum = (accum << 8) | ord(token[i])
accum = 0x3164393866376434 (ASCII “1d98f7d4”)
- XOR 常量:accum ^ 0xDA3E33D2BABBCBA9
XOR = 0xEB5A0AEADC8CAF9D
- SHA256(8 字节):ARM64 str x8 以小端序存储字节
input_bytes = xored.to_bytes(8, ’little')
= [0x9D, 0xAF, 0x8C, 0xDC, 0xEA, 0x0A, 0x5A, 0xEB]
expected = SHA256(input_bytes).hex()[:16]
= “d5d0d8b2a381d381”
input_bytes = xored.to_bytes(8, ‘big’)
= [0xEB, 0x5A, 0x0A, 0xEA, 0xDC, 0x8C, 0xAF, 0x9D]
SHA256(input_bytes).hex()[:16]
= “34d12f2b518bb4a4”
- 比较:用户输入 vs 预期的前 16 位 hex 字符
9.3 字节序陷阱分析
这是整个挑战最隐蔽的陷阱:
循环使用 bfi x8, x9, #8, #56 以大端序构建 64 位累加器值
但 str x8, [sp] 以小端序将值存入内存
SHA256 直接读取内存字节作为输入
因此写入内存的 8 字节是大端值的小端表示,与累加器的字符顺序相反
大端累加值: 0x3164393866376434
内存中小端存储: [34, 64, 37, 66, 38, 39, 64, 31] ← 字符逆序
9.4 Key3 推导
key3 = SHA256(“t3_android_d5d0d8b2a381d3811d98f7d4ff3f63a1”)[:8]
= 9d7d5e248135e797
9.5 验证路径
数据在 0x803d(intermediate token)和 0x80 之间通过 strcmp 校验:
函数 0x43d0 签名:
x0 = challenge3 用户输入字符串
x1 = intermediate_token (从 Java 传入)
x2 = challenge_state
第一步: strcmp(x1, 0x803d) → 验证 token 一致性
第二步: accum = token[0:8] → 构建 64 位值
第三步: SHA256(accum ^ CONST) → 生成期望值
第四步: memcmp(x0, expected[:16])
- Flag 生成
函数 0x2f14:
prefix = b"CTF_ZKP_FLAG_V2" # 15 字节
flag_input = prefix + session_ctx[0:32] + key1 + key2 + key3
= 15 + 32 + 8 + 8 + 8 = 71 字节
hash = SHA256(flag_input)
= 71f6973b7f32d1507a7482deb3a64c2384f3656a8a804a99c3090ecc53f410c3
函数 0x59cc 转换 24 输入字节 → 48 hex 字符
但最终结果显示转换结果被截断为 40 hex 字符
flag = “ISCC{” + hash.hex()[:40] + “}”
注:0x59cc 的 w1=24 表示转换 24 raw bytes → 48 hex chars。实测 flag 长度为 40 hex chars,可能是 snprintf 或 null 截断导致,但不影响 flag 正确性。
import hashlib, hmac
====== 凭证 ======
username = b"zkp_master"
password = b"Zkp_M_ast3er"
====== Challenge 答案 ======
ch1 = “8826FBF1”
ch2 = “ffb9b64254eb5012ffb139045e3e3959”
ch3 = “d5d0d8b2a381d381” # LE 正确
token = “1d98f7d4ff3f63a1128cc06f0303d02c”
====== 推导 key1, key2, key3 ======
key1 = hashlib.sha256(b"t1_static_" + ch1.encode()).digest()[:8]
key2 = hashlib.sha256(b"2_dynamic_\0" + ch2.encode()).digest()[:8]
key3 = hashlib.sha256(b"t3_android_" + ch3.encode() + token[:16].encode()).digest()[:8]
====== 推导 session_ctx ======
salt_hash = hashlib.sha256(b"CTF_ZKP_apk-13_SALT_V3").digest()
pw_hash = hashlib.sha256(password).digest()
prk = hmac.new(salt_hash, pw_hash, hashlib.sha256).digest()
session_ctx = hmac.new(prk, b"AUTH_OK\x01", hashlib.sha256).digest()
====== 计算 Flag ======
flag_hash = hashlib.sha256(b"CTF_ZKP_FLAG_V2" + session_ctx + key1 + key2 + key3).hexdigest()
flag = “ISCC{” + flag_hash[:40] + “}”
print(f"Flag: {flag}")
ISCC{a77a839a76f98e6e47310d2bf359e144a49c804d930cb11d}
- Challenge 3
import hashlib
accum = 0
for c in token[:8]:
accum = (accum << 8) | ord(c)
accum = 0x3164393866376434
xor_const = 0xDA3E33D2BABBCBA9
xor_result = accum ^ xor_const
xor_result = 0xEB5A0AEADC8CAF9D
input_data = xor_result.to_bytes(8, ’little')
ch3_le = hashlib.sha256(input_data).hexdigest()[:16]
print(f"LE (correct): {ch3_le}")
d5d0d8b2a381d381
input_data = xor_result.to_bytes(8, ‘big’)
ch3_be = hashlib.sha256(input_data).hexdigest()[:16]
print(f"BE (wrong): {ch3_be}")
34d12f2b518bb4a4
Exp(如有,请粘贴完整代码,不允许截图!)
import hashlib, hmac
====== 凭证 ======
username = b"zkp_master"
password = b"Zkp_M_ast3er"
====== Challenge 答案 ======
ch1 = “8826FBF1”
ch2 = “ffb9b64254eb5012ffb139045e3e3959”
ch3 = “d5d0d8b2a381d381” # LE 正确
token = “1d98f7d4ff3f63a1128cc06f0303d02c”
====== 推导 key1, key2, key3 ======
key1 = hashlib.sha256(b"t1_static_" + ch1.encode()).digest()[:8]
key2 = hashlib.sha256(b"2_dynamic_\0" + ch2.encode()).digest()[:8]
key3 = hashlib.sha256(b"t3_android_" + ch3.encode() + token[:16].encode()).digest()[:8]
====== 推导 session_ctx ======
salt_hash = hashlib.sha256(b"CTF_ZKP_apk-13_SALT_V3").digest()
pw_hash = hashlib.sha256(password).digest()
prk = hmac.new(salt_hash, pw_hash, hashlib.sha256).digest()
session_ctx = hmac.new(prk, b"AUTH_OK\x01", hashlib.sha256).digest()
====== 计算 Flag ======
flag_hash = hashlib.sha256(b"CTF_ZKP_FLAG_V2" + session_ctx + key1 + key2 + key3).hexdigest()
flag = “ISCC{” + flag_hash[:40] + “}”
print(f"Flag: {flag}")
ISCC{a77a839a76f98e6e47310d2bf359e144a49c804d930cb11d}
import hashlib
accum = 0
for c in token[:8]:
accum = (accum << 8) | ord(c)
accum = 0x3164393866376434
xor_const = 0xDA3E33D2BABBCBA9
xor_result = accum ^ xor_const
xor_result = 0xEB5A0AEADC8CAF9D
input_data = xor_result.to_bytes(8, ’little')
ch3_le = hashlib.sha256(input_data).hexdigest()[:16]
print(f"LE (correct): {ch3_le}")
d5d0d8b2a381d381
input_data = xor_result.to_bytes(8, ‘big’)
ch3_be = hashlib.sha256(input_data).hexdigest()[:16]
print(f"BE (wrong): {ch3_be}")
评论