=========================

Adriod + 按步取匙

解题思路(必须包含文字说明+截图)

APK 包名 com.mobilezkp,核心逻辑在 native 层 libzkpcore.so(ARM64)。整个认证流程分四步:

  1. Login — 验证用户名/密码

  2. Challenge 1 — 输入 8 位 hex 验证码

  3. Challenge 2 — 输入 32 位 hex 令牌

  4. Challenge 3 — 输入 16 位 hex 密钥,依赖 intermediate_token

通过全部挑战后,nativeGenerateFlag() 输出最终 flag。

  1. 静态分析环境

  • 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


  1. 数据区中的关键字符串


偏移 内容 用途 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 转换表


  1. 用户名/密码逆向

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”

  1. 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”)

  1. Challenge 1

6.1 答案

8826FBF1

6.2 验证逻辑(函数 0x336c)

  1. 输入必须是 8 位 hex

  2. 解析为 32 位整数 hex(“8826FBF1”) = 0x8826FBF1

  3. 调用 0x3800 初始化 LCG 状态,0x3910 迭代 7 轮

  4. 用 4 组 XOR 常量混合结果

  5. 最终检查 result == 0x81BDB091

6.3 Key1 推导

key1 = SHA256(“t1_static_8826FBF1”)[:8]

= 2e3ef2469b9e757e

盐值 “t1_static_"(10 字节)拼接 8 字节输入 → SHA256 → 取前 8 字节。

  1. Challenge 2

7.1 答案

ffb9b64254eb5012ffb139045e3e3959

7.2 验证逻辑

  1. 函数 0x39e0 从 rodata 0x4bf 获取 seed 值 0x0d40727a 和 0x1cfb729d

  2. XOR 得到初始 seed:0x0d40727a ^ 0x1cfb729d = 0x11BB00E7

  3. LCG 7 轮:state = state * 0x49297c7f + 0xc0d9543f

  4. 最终 state 0x813bcd58 XOR 0x8b23fd92 = 0x0A1830CA

  5. 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

  1. 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。

  1. Challenge 3

9.1 答案

d5d0d8b2a381d381

9.2 验证逻辑(函数 0x43d0)

token = “1d98f7d4ff3f63a1128cc06f0303d02c”

  1. 检查 token:strcmp(input_token, 0x803d) == 0

  2. 读取 token 前 8 个 ASCII 字符 → 构建 64 位大端整数:

accum = 0

for i in range(8):

accum = (accum << 8) | ord(token[i])

accum = 0x3164393866376434 (ASCII “1d98f7d4”)

  1. XOR 常量:accum ^ 0xDA3E33D2BABBCBA9

XOR = 0xEB5A0AEADC8CAF9D

  1. 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”

  1. 比较:用户输入 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])

  1. 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}

  1. 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}")

34d12f2b518bb4a4