ISCC2026 WriteUp
题目类型: Mobile + Native SO 混合逆向
题目名称: 灰签名回廊(Mobile2)
解题思路
本题是一道 Android APK 与 Native SO 联合校验的逆向题。flag 被拆分成多个片段分散在 DEX 字节码、native 库和 APK 签名证书中。解题关键在于完整重建从 Java 层到 native 层的五阶段校验链,最后通过 SHA-256 哈希闭壳搜索收尾。
### 1. APK 解包与整体结构识别
使用 `zipfile` 解压 APK,提取三个关键组件:
classes.dex — Java 层校验逻辑,包含多个片段来源类
libnative-verify.so — native 校验库(优先取 x86_64 架构)
APK Signing Block v2 — 证书签名区块
Java 层关键类梳理:
`SecureValidator` — 主校验入口,存储前缀编码数组和方法码表
`SecretContentProvider` — 提供第二段校验片段
`SecretReceiver` — 广播接收器,提供第三段片段
`SecretActivity` — 提供第四段尾部标记数据
native 层导出函数:
`nativeVerifyPart1` ~ `nativeVerifyPart4` — 四阶段分段校验
`nativeChainToken` — 链式 token 派生
截图说明(Jadx 反编译视图):解包后 DEX 中可看到 `SecureValidator` 类包含 `<init>` 方法中的 `filled-new-array` 指令序列,native 校验函数注册在静态代码块中。
### 2. 前缀恢复 — SecureValidator 构造函数
在 `SecureValidator.<init>` 方法中,DEX 指令通过 `filled-new-array` 构造了一个 5 元素 int 数组。使用 DEX 字节码解析器提取该数组后,尝试与 0~255 所有可能的字节做异或,寻找以 `{` 结尾且全部可打印的字符串,唯一命中的就是 `ISCC{`。
int_array = [0x3a, 0x3c, 0x3e, 0x3e, 0x7e] // 示例
xor_key = 0x4e
prefix = ''.join(chr(v \^ 0x4e) for v in int_array) → "ISCC{"
DEX 字节码分析中 `fill-array-data-payload` 指令指向的数据表用 `filled-new-array` 收集的寄存器值索引获得。
### 3. 第一段恢复 — Part1 字节变换
`SecureValidator.a()` 方法中存在一个 7 字节的 `fill-array-data` 负载,该负载会送入 native 的 `nativeVerifyPart1`。
在 native SO 中定位 Part1 变换逻辑:搜索特征字节序列 `fe c1 80 f1`(对应 `inc cl; xor cl, imm8` 指令),提取立即数得到:
`p1_add = 1`(自增量)
`p1_xor`(异或常量)
逆向变换为:`plaintext[i] = ((ciphertext[i] ^ p1_xor) - p1_add) & 0xff`
同时,Part1 的实际校验值还与 APK 证书 SHA-256 的前 4 字节循环异或绑定,这一结果会作为链式 token 第一阶段的输入。
截图说明(IDA/Ghidra native 反汇编):`nativeVerifyPart1` 函数中可见循环结构,每条元素先异或立即数再减 1,与目标数组逐字节对比。
### 4. 第二段恢复 — ContentProvider + Part2
`SecretContentProvider.query()` 方法内通过 `filled-new-array` 构造了 4 个 int 值,随后使用 `xor-int/lit8` 指令进行异或解码,得到中间片段 `frag2`(`“Rp4N”`)。
在 native SO 中定位 Part2 检验逻辑。本题样本使用 scalar5 模式(非旧的 SIMD shuffle 模式),特征为 5 字节独立标量检查:
字节0: frag2[0] \^ p2_x0
字节1: frag2[1] \^ p2_x1
字节2: frag2[2] // 直通
字节3: frag2[3] \^ p2_x3
字节4: frag2[0] \^ p2_x4
从 SO 中通过正则匹配提取 `x0`、`x1`、`x3`、`x4` 四个异或常量,计算得出 Part2。
截图说明:native 反汇编中 Part2 的五条连续 `xor` + `cmp` 指令序列,每条对应一个字节位置。
### 5. 第三段恢复 — BroadcastReceiver + Part3
`SecretReceiver.onReceive()` 采取三层变换:`result[i] = i ^ arr_value[i] ^ xor_lit8_const`。提取 int 数组和异或常量后得 `frag3`(`“oyzz”`)。
在 native Part3 中定位到首个字节变换 `frag3[0] = ((cmp_byte - frag3[0]) & 0xff) ^ xor_key`,后续 4 字节分别与一个 32 位比较常量的各字节做类似运算:
p3[0] = ((first_cmp - frag3[0]) & 0xff) \^ xor
p3[i+1] = ((cmp[i] - frag3[idx[i]]) & 0xff) \^ xor
从 SO 中用 `0f b6 03 34 .. 41 02 45 00 3c` 模式定位单字节常量,用 `66 0f fc c8 66 0f 7e c8 3d` 定位 32 位比较常量。
### 6. 证书绑定与链式 Token 派生
APK 证书提取:从 APK Signing Block v2(魔数 `APK Sig Block 42`)中解析 pair ID 为 `0x7109871a` 的 signer block,提取第一个证书的 DER 编码,计算 SHA-256,得到 32 字节证书摘要。
链式 Token 构造:
token = chain_token(stage, seed, input_blob)
内部逻辑:
x = (stage * 0x100000001b3) \^ seed
for each byte b in blob:
x = rol64(((b \^ x) * 0x100000001b3), 13)
x = x \^ (x >> 33)
x = (x * 0xff51afd7ed558ccd) mod 2\^64
x = x \^ (x >> 33)
第一阶段输入为 `native_p1_return`(Part1 负载异或证书前 4 字节的结果),第二阶段输入 `frag2`,第三阶段输入 `frag3`。
截图说明:从 APK 二进制中定位 `APK Sig Block 42` 标记和后续的 signer pair 结构,16 进制视图中可见证书 DER 序列。
### 7. Part4 — RC4 密钥流与位掩码约束
从链式 token 第三阶段输出提取低 4 字节,异或 `“Secu”` 常量后作为 RC4 密钥前缀:
chain_low = token_bytes[0:4]
key_prefix = chain_low \^ b"Secu"
rc4_key = key_prefix + frag2 + frag3 + [0, 0]
keystream = rc4(rc4_key, 4) → [236, 72, 50, 39]
Part4 使用位掩码约束来大幅压缩搜索空间:
条件: (((o2 \^ (o1 << 3) \^ (o0 << 6)) << 12) \^ (o3 << 9)) & 0x03ff0000 == 0x02c40000
其中 o[i] = candidate_byte[i] \^ keystream[i]
该条件只检查 32 位结果中的 10 个 bit 位(mask = `0x03ff0000`),因此大量候选组合可以通过筛选。但结合已知前缀和最终 SHA-256 闭合验证,唯一正确的候选会被锁定。
### 8. 最终闭壳搜索
已知前缀为 `ISCC{FkJVk55_PfVw_v5qx`(22 字节),还需恢复 7 字节内容,末尾字节固定为 `}`,总长度 30 字节恰好占一个 SHA-256 块。
搜索策略:
1. 在大小为 64 的字符集上,对 Part4 的前 4 字节用位掩码条件筛选,获得候选四元组集合
2. 对每个候选四元组,在后 3 字节空间上三层循环,计算 SHA-256
3. 与目标哈希 `c6f81b343cb4dc69b6a62e5af71541119c861658c72a1167ad539cf5422f4d59` 对比
4. 命中后输出完整 flag
采用 numba JIT 编译加速 SHA-256 计算,配合并行化在秒级完成搜索。
Exp
完整解题脚本如下(Python 3),核心模块包括 DEX 解析器、APK 证书提取、native 常量匹配、分阶段还原和闭壳搜索。
\#!/usr/bin/env python3
"""
ISCC2026 Mobile2 "灰签名回廊" 解题脚本
流程:解包APK → 解析DEX → 提取native常量 → 恢复Part1-3 → RC4+位掩码筛选 → SHA-256闭壳搜索
"""
import base64
import hashlib
import os
import re
import struct
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
MASK64 = (1 << 64) - 1
def uleb(data, off):
res = 0
shift = 0
while True:
b = data[off]
off += 1
res |= (b & 0x7f) << shift
if b < 0x80:
return res, off
shift += 7
def signed(v, bits):
if v & (1 << (bits - 1)):
v -= 1 << bits
return v
class Dex:
"""精简 DEX 解析器 —— 解析字符串表、类方法表、指令序列及填充数组"""
def __init__(self, data: bytes):
self.data = data
d = data
self.string_ids_size, self.string_ids_off = struct.unpack_from('<II', d, 56)
self.type_ids_size, self.type_ids_off = struct.unpack_from('<II', d, 64)
self.proto_ids_size, self.proto_ids_off = struct.unpack_from('<II', d, 72)
self.field_ids_size, self.field_ids_off = struct.unpack_from('<II', d, 80)
self.method_ids_size, self.method_ids_off = struct.unpack_from('<II', d, 88)
self.class_defs_size, self.class_defs_off = struct.unpack_from('<II', d, 96)
self.strings = []
for i in range(self.string_ids_size):
off = struct.unpack_from('<I', d, self.string_ids_off + i * 4)[0]
_, off2 = uleb(d, off)
end = d.index(b'\\x00', off2)
self.strings.append(d[off2:end].decode('utf-8', 'replace'))
self.types = [self.strings[struct.unpack_from('<I', d, self.type_ids_off + i * 4)[0]]
for i in range(self.type_ids_size)]
self.methods = []
for i in range(self.method_ids_size):
cls, proto, name = struct.unpack_from('<HHI', d, self.method_ids_off + i * 8)
self.methods.append((self.types[cls], self.strings[name]))
self.class_methods = {}
for c in range(self.class_defs_size):
vals = struct.unpack_from('<IIIIIIII', d, self.class_defs_off + 32 * c)
cname = self.types[vals[0]]
class_data_off = vals[6]
if class_data_off:
self.class_methods[cname] = self._parse_class_data(class_data_off)
def _parse_class_data(self, off):
d = self.data
static_fields, off = uleb(d, off)
instance_fields, off = uleb(d, off)
direct_methods, off = uleb(d, off)
virtual_methods, off = uleb(d, off)
for _ in range(static_fields + instance_fields):
_, off = uleb(d, off)
_, off = uleb(d, off)
out = []
midx = 0
for _ in range(direct_methods):
diff, off = uleb(d, off)
_, off = uleb(d, off)
code_off, off = uleb(d, off)
midx += diff
cls, name = self.methods[midx]
out.append((name, code_off, 'direct'))
midx = 0
for _ in range(virtual_methods):
diff, off = uleb(d, off)
_, off = uleb(d, off)
code_off, off = uleb(d, off)
midx += diff
cls, name = self.methods[midx]
out.append((name, code_off, 'virtual'))
return out
def method_code(self, class_contains: str, method_name: str):
for cname, methods in self.class_methods.items():
if class_contains in cname:
for name, code_off, _ in methods:
if name == method_name and code_off:
return code_off
raise ValueError(f'method not found: {class_contains}->{method_name}')
def insns(self, code_off):
d = self.data
regs, ins, outs, tries, debug, insn_size = struct.unpack_from('<HHHHII', d, code_off)
start = code_off + 16
pc = 0
while pc < insn_size:
off = start + pc * 2
w = struct.unpack_from('<H', d, off)[0]
op = w & 0xff
hi = w >> 8
size = 1
info = {'pc': pc, 'op': op, 'hi': hi, 'off': off}
def u16(n): return struct.unpack_from('<H', d, off + 2 * n)[0]
def s16(n): return signed(u16(n), 16)
def u32(n): return struct.unpack_from('<I', d, off + 2 * n)[0]
def s32(n): return signed(u32(n), 32)
if op == 0x00 and hi == 0x03:
width = struct.unpack_from('<H', d, off + 2)[0]
sz = struct.unpack_from('<I', d, off + 4)[0]
size = 4 + ((width * sz + 1) // 2)
info.update({'payload': True, 'width': width, 'array_size': sz})
elif op in (0x00, 0x0e):
pass
elif op == 0x12:
info.update({'A': hi & 0xf, 'lit': signed(hi >> 4, 4)})
elif op == 0x13:
size = 2
info.update({'A': hi, 'lit': s16(1)})
elif op == 0x14:
size = 3
info.update({'A': hi, 'lit': s32(1)})
elif op == 0x1a:
size = 2
idx = u16(1)
info.update({'A': hi, 'string': self.strings[idx]})
elif op == 0x24:
size = 3
A = hi >> 4
G = hi & 0xf
w2 = u16(2)
regs_list = [w2 & 0xf, (w2 >> 4) & 0xf, (w2 >> 8) & 0xf, (w2 >> 12) & 0xf, G][:A]
info.update({'regs': regs_list})
elif op == 0x26:
size = 3
info.update({'A': hi, 'target_pc': pc + s32(1)})
elif op == 0xdf:
size = 2
w2 = u16(1)
info.update({'A': hi, 'B': w2 & 0xff, 'lit': signed(w2 >> 8, 8)})
elif op in (0x01, 0x04, 0x07, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83,
0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f):
pass
elif op in (0x02, 0x05, 0x08, 0x1c, 0x1f, 0x22, 0x23, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64,
0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d) or 0xd0 <= op <= 0xe2:
size = 2
elif op in (0x03, 0x06, 0x09, 0x18, 0x1b, 0x25, 0x2a, 0x2b, 0x2c, 0x6e, 0x6f, 0x70, 0x71,
0x72, 0x74, 0x75, 0x76, 0x77, 0x78):
size = 3 if op != 0x18 else 5
elif op in (0x16, 0x17, 0x15, 0x19, 0x29) or 0x2d <= op <= 0x3d or 0x44 <= op <= 0x51 or 0x90 <= op <= 0xaf:
size = 2
elif 0xb0 <= op <= 0xcf:
pass
else:
size = 1
yield info
pc += size
def fill_payload(self, code_off, target_pc):
d = self.data
start = code_off + 16
off = start + target_pc * 2
ident, width = struct.unpack_from('<HH', d, off)
if ident != 0x0300:
raise ValueError('bad fill-array-data payload')
sz = struct.unpack_from('<I', d, off + 4)[0]
raw = d[off + 8: off + 8 + width * sz]
vals = []
for i in range(sz):
if width == 1:
vals.append(raw[i])
elif width == 2:
vals.append(struct.unpack_from('<H', raw, i * 2)[0])
elif width == 4:
vals.append(struct.unpack_from('<I', raw, i * 4)[0])
elif width == 8:
vals.append(struct.unpack_from('<Q', raw, i * 8)[0])
return width, vals
def const_arrays(self, code_off):
regs = {}
arrays = []
for ins in self.insns(code_off):
op = ins['op']
if op in (0x12, 0x13, 0x14):
regs[ins['A']] = ins['lit']
elif op == 0x24:
vals = []
ok = True
for r in ins['regs']:
if r not in regs:
ok = False
break
vals.append(regs[r])
if ok:
arrays.append((ins['pc'], vals))
return arrays
def fill_arrays(self, code_off):
out = []
for ins in self.insns(code_off):
if ins['op'] == 0x26:
width, vals = self.fill_payload(code_off, ins['target_pc'])
out.append((ins['pc'], width, vals))
return out
def xor_lit8_after(self, code_off, pc_min=-1):
for ins in self.insns(code_off):
if ins['pc'] > pc_min and ins['op'] == 0xdf:
return ins['lit'] & 0xff
raise ValueError('xor-int/lit8 constant not found')
def extract_apk_cert_sha256(apk_bytes: bytes):
"""从 APK v2 签名块中提取签名证书并计算 SHA-256"""
eocd = apk_bytes.rfind(b'PK\\x05\\x06')
if eocd < 0:
raise ValueError('EOCD not found')
cd_off = struct.unpack_from('<I', apk_bytes, eocd + 16)[0]
if apk_bytes[cd_off - 16:cd_off] != b'APK Sig Block 42':
raise ValueError('APK Signing Block not found')
size2 = struct.unpack_from('<Q', apk_bytes, cd_off - 24)[0]
start = cd_off - (size2 + 8)
size1 = struct.unpack_from('<Q', apk_bytes, start)[0]
if size1 != size2:
raise ValueError('APK Signing Block size mismatch')
pos = start + 8
end = cd_off - 24
def read_u32_len(buf, off):
n = struct.unpack_from('<I', buf, off)[0]
return buf[off + 4:off + 4 + n], off + 4 + n
certs = []
while pos < end:
pair_len = struct.unpack_from('<Q', apk_bytes, pos)[0]
pos += 8
pair_id = struct.unpack_from('<I', apk_bytes, pos)[0]
val = apk_bytes[pos + 4:pos + pair_len]
pos += pair_len
if pair_id in (0x7109871a,):
signers, _ = read_u32_len(val, 0)
off = 0
while off < len(signers):
signer, off = read_u32_len(signers, off)
signed_data, o = read_u32_len(signer, 0)
cert_seq, o2 = read_u32_len(signed_data, o)
c_off = 0
while c_off < len(cert_seq):
cert, c_off = read_u32_len(cert_seq, c_off)
certs.append(cert)
if not certs:
raise ValueError('signing certificate not found')
return hashlib.sha256(certs[0]).digest()
def pick_native_so(zf: zipfile.ZipFile):
names = zf.namelist()
preferred = [
'lib/x86_64/libnative-verify.so',
'lib/x86/libnative-verify.so',
'lib/arm64-v8a/libnative-verify.so',
'lib/armeabi-v7a/libnative-verify.so',
]
for n in preferred:
if n in names:
return n, zf.read(n)
for n in names:
if n.endswith('/libnative-verify.so'):
return n, zf.read(n)
raise ValueError('libnative-verify.so not found')
def extract_native_consts(so: bytes):
"""从 native SO 中扫描特征指令提取各阶段变换常量"""
c = {}
# Part1: inc cl; xor cl, imm8 (或 add cl, imm8; xor cl, imm8)
xs = list(re.finditer(rb'\\xfe\\xc1\\x80\\xf1(.)', so, re.S))
if xs:
c['p1_add'] = 1
c['p1_xor'] = xs[-1].group(1)[0]
else:
m = re.search(rb'\\x80\\xc1(.)\\x80\\xf1(.)', so, re.S)
if not m:
raise ValueError('part1 transform constants not found')
c['p1_add'] = m.group(1)[0]
c['p1_xor'] = m.group(2)[0]
# Part2: 优先匹配新 scalar5 模式,回退到旧 SIMD shuffle 模式
m = re.search(rb'\\x41\\x32\\x4d\\x00\\x80\\xf9(.)\\x0f\\x94\\xc0.*?\\x66\\x0f\\x38\\x00\\x0d(.{4}).*?\\x81\\xf9(.{4})', so, re.S)
if m:
c['p2_mode'] = 'shuffle'
c['p2_first_cmp'] = m.group(1)[0]
disp_off = m.start(2)
disp = struct.unpack('<i', m.group(2))[0]
mask_off = disp_off + 4 + disp
mask = so[mask_off:mask_off + 16]
c['p2_mask'] = list(mask[:4])
c['p2_cmp'] = struct.unpack('<I', m.group(3))[0]
else:
m = re.search(
rb'\\x30\\xc2\\x80\\xf2(.)'
rb'\\x41\\x32\\x4c\\x24\\x01\\x80\\xf1(.)'
rb'\\x41\\x0f\\xb6\\x75\\x02\\x41\\x32\\x74\\x24\\x02\\x40\\x08\\xce'
rb'\\x41\\x0f\\xb6\\x4d\\x03\\x41\\x32\\x4c\\x24\\x03\\x80\\xf1(.)'
rb'\\x41\\x32\\x44\\x24\\x04\\x34(.)',
so, re.S,
)
if not m:
raise ValueError('part2 constants not found')
c['p2_mode'] = 'scalar5'
c['p2_x0'] = m.group(1)[0]
c['p2_x1'] = m.group(2)[0]
c['p2_x3'] = m.group(3)[0]
c['p2_x4'] = m.group(4)[0]
# Part3: 首字节异或和比较常量,后续 4 字节 32 位比较
m = re.search(rb'\\x0f\\xb6\\x03\\x34(.)\\x41\\x02\\x45\\x00\\x3c(.)', so, re.S)
if not m:
raise ValueError('part3 first-byte constants not found')
c['p3_xor'] = m.group(1)[0]
c['p3_first_cmp'] = m.group(2)[0]
m = re.search(rb'\\x66\\x0f\\xfc\\xc8\\x66\\x0f\\x7e\\xc8\\x3d(.{4})', so, re.S)
if not m:
raise ValueError('part3 cmp constant not found')
c['p3_cmp'] = struct.unpack('<I', m.group(1))[0]
# Part4: 位掩码与期望值 (AND mask; CMP expected)
ms = list(re.finditer(rb'\\x25(.{4})\\x3d(.{4})\\x0f\\x94\\xc0', so, re.S))
if not ms:
raise ValueError('part4 mask/cmp not found')
m = ms[-1]
c['p4_mask'] = struct.unpack('<I', m.group(1))[0]
c['p4_cmp'] = struct.unpack('<I', m.group(2))[0]
c['chain_seed_xor'] = b'Secu'
return c
def rol64(x, r):
return ((x << r) | (x >> (64 - r))) & MASK64
def chain_token(stage: int, seed: int, blob: bytes) -> int:
"""链式 token 派生:每阶段用 stage * prime \^ seed 初始化,然后吸收 blob 每个字节"""
prime = 0x100000001b3
x = ((stage * prime) & MASK64) \^ (seed & MASK64)
for b in blob:
x = rol64(((b \^ x) * prime) & MASK64, 13)
x \^= x >> 33
x = (x * 0xff51afd7ed558ccd) & MASK64
x \^= x >> 33
return x & MASK64
def rc4_keystream(key: bytes, n: int):
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xff
S[i], S[j] = S[j], S[i]
i = j = 0
out = []
for _ in range(n):
i = (i + 1) & 0xff
j = (j + S[i]) & 0xff
S[i], S[j] = S[j], S[i]
out.append(S[(S[i] + S[j]) & 0xff])
return bytes(out)
def solve_parts(apk_path: str):
apk_bytes = Path(apk_path).read_bytes()
with zipfile.ZipFile(apk_path, 'r') as zf:
dex = Dex(zf.read('classes.dex'))
so_name, so = pick_native_so(zf)
nconst = extract_native_consts(so)
cert_sha = extract_apk_cert_sha256(apk_bytes)
# 目标 SHA-256 在 DEX 字符串表中,固定 64 位 hex 串
hex64 = [s for s in dex.strings if re.fullmatch(r'[0-9a-fA-F]{64}', s)]
if not hex64:
raise ValueError('target SHA-256 string not found')
target_hex = hex64[0].lower()
# ---- 前缀: ISCC{ ----
init_code = dex.method_code('com/example/gnd/security/SecureValidator;', '<init>')
prefix_arr = None
for pc, vals in dex.const_arrays(init_code):
if len(vals) == 5:
prefix_arr = vals
break
if prefix_arr is None:
raise ValueError('flag prefix array not found')
prefix_candidates = []
for x in range(256):
s = ''.join(chr((v \^ x) & 0xff) for v in prefix_arr)
if s.endswith('{') and all(32 <= ord(ch) < 127 for ch in s[:-1]):
prefix_candidates.append((x, s))
if not prefix_candidates:
raise ValueError('flag prefix xor key not found')
prefer = [s for _, s in prefix_candidates if re.match(r'\^[A-Za-z0-9_]+\\{\$', s)]
flag_prefix = ('ISCC{' if any(s == 'ISCC{' for s in prefer) else (prefer[0] if prefer else prefix_candidates[0][1]))
# ---- Part1: SecureValidator.a 字节数组 → native 逆变换 ----
a_code = dex.method_code('com/example/gnd/security/SecureValidator;', 'a')
p1_payloads = [vals for _, w, vals in dex.fill_arrays(a_code) if w == 1 and len(vals) == 7]
if not p1_payloads:
raise ValueError('part1 payload not found')
p1_payload = bytes(p1_payloads[0])
part1 = bytes((((b \^ nconst['p1_xor']) - nconst['p1_add']) & 0xff) for b in p1_payload)
native_p1_return = bytes(p1_payload[i] \^ cert_sha[i % 4] for i in range(len(p1_payload)))
# ---- frag2: ContentProvider ----
provider_code = dex.method_code('com/example/gnd/hidden/SecretContentProvider;', 'query')
prov_arrs = [(pc, vals) for pc, vals in dex.const_arrays(provider_code) if len(vals) == 4]
if not prov_arrs:
raise ValueError('provider fragment array not found')
pc2, arr2 = prov_arrs[0]
imm2 = dex.xor_lit8_after(provider_code, pc2)
frag2 = bytes((v \^ imm2) & 0xff for v in arr2)
# ---- frag3: Receiver ----
recv_code = dex.method_code('com/example/gnd/hidden/SecretReceiver;', 'onReceive')
recv_arrs = [(pc, vals) for pc, vals in dex.const_arrays(recv_code) if len(vals) == 4]
if not recv_arrs:
raise ValueError('receiver fragment array not found')
pc3, arr3 = recv_arrs[-1]
imm3 = dex.xor_lit8_after(recv_code, pc3)
frag3 = bytes(((i \^ v \^ imm3) & 0xff) for i, v in enumerate(arr3))
# ---- frag4: Activity (尾部 2 字节标记) ----
act_code = dex.method_code('com/example/gnd/hidden/SecretActivity;', 'onCreate')
byte_payloads = [bytes(vals) for _, w, vals in dex.fill_arrays(act_code) if w == 1 and len(vals) >= 2]
if not byte_payloads:
raise ValueError('activity fragment payload not found')
payload4 = next((p for p in byte_payloads if b'ISCC' in p), byte_payloads[0])
frag4 = payload4[-2:]
# ---- Part2: native 第二阶段 5 字节标量方程 ----
key2 = frag2
p2 = [0] * 5
if nconst.get('p2_mode') == 'scalar5':
p2[0] = key2[0] \^ nconst['p2_x0']
p2[1] = key2[1] \^ nconst['p2_x1']
p2[2] = key2[2]
p2[3] = key2[3] \^ nconst['p2_x3']
p2[4] = key2[0] \^ nconst['p2_x4']
else:
p2[0] = key2[0] \^ nconst['p2_first_cmp']
cmp2 = nconst['p2_cmp'].to_bytes(4, 'little')
arr = [0] * 4
mask = nconst['p2_mask']
for i, m in enumerate(mask):
if m < 4:
arr[m] = cmp2[i] \^ key2[i]
p2[1:5] = arr
part2 = bytes(p2)
# ---- Part3: native 第三阶段首字节变换 + 4 字节反馈 ----
key3 = frag3
cmp3 = nconst['p3_cmp'].to_bytes(4, 'little')
p3 = [0] * 5
p3[0] = ((nconst['p3_first_cmp'] - key3[0]) & 0xff) \^ nconst['p3_xor']
idxs = [1 if len(key3) != 1 else 0, 2 % len(key3), 3 % len(key3), 4 % len(key3)]
for i in range(4):
p3[i + 1] = ((cmp3[i] - key3[idxs[i]]) & 0xff) \^ nconst['p3_xor']
part3 = bytes(p3)
# ---- 链式 token → RC4 密钥流 ----
seed = 1597463007
ch = chain_token(1, seed, native_p1_return)
ch = chain_token(2, ch, frag2)
ch = chain_token(3, ch, frag3)
chain_low = bytes((ch >> (8 * i)) & 0xff for i in range(4))
key_prefix = bytes(chain_low[i] \^ nconst['chain_seed_xor'][i] for i in range(4))
rc4_key = key_prefix + frag2 + frag3 + bytes([0, 0])
ks = rc4_keystream(rc4_key, 4)
known_prefix = (flag_prefix.encode() + part1 + part2 + part3)
return {
'target_hex': target_hex,
'flag_prefix': flag_prefix,
'part1': part1,
'part2': part2,
'part3': part3,
'frag2': frag2,
'frag3': frag3,
'frag4': frag4,
'known_prefix': known_prefix,
'ks': ks,
'p4_mask': nconst['p4_mask'],
'p4_cmp': nconst['p4_cmp'],
'so_name': so_name,
}
def brute_with_numba(info):
"""numba JIT 加速的 SHA-256 闭壳搜索"""
try:
import itertools
import numpy as np
from numba import njit, prange
except Exception:
return None
full = os.environ.get('CHARSET', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-')
upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
ks = info['ks']
mask = info['p4_mask']
cmpv = info['p4_cmp']
target = bytes.fromhex(info['target_hex'])
prefix = info['known_prefix']
# 位掩码预筛选:生成满足 part4 前 4 字节约束的所有候选
cand4 = []
for bcd in itertools.product(full, repeat=3):
s = '_' + ''.join(bcd)
a, b, c, d = [ord(x) for x in s]
o0, o1, o2, o3 = a \^ ks[0], b \^ ks[1], c \^ ks[2], d \^ ks[3]
if (((((o2 \^ (o1 << 3) \^ (o0 << 6)) << 12) \^ (o3 << 9)) & mask) == cmpv):
cand4.append([a, b, c, d])
if not cand4:
return None
cand4 = np.array(cand4, dtype=np.uint8)
upper_arr = np.frombuffer(upper.encode(), dtype=np.uint8)
full_arr = np.frombuffer(full.encode(), dtype=np.uint8)
target_words = np.frombuffer(target, dtype='>u4').astype(np.uint32)
prefix_arr = np.frombuffer(prefix, dtype=np.uint8)
ktab = np.array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
], dtype=np.uint32)
@njit(inline='always')
def rotr(x, n):
return ((x >> n) | (x << (32 - n))) & np.uint32(0xffffffff)
@njit(inline='always')
def ch(x, y, z):
return (x & y) \^ ((\~x) & z)
@njit(inline='always')
def maj(x, y, z):
return (x & y) \^ (x & z) \^ (y & z)
@njit(inline='always')
def ep0(x):
return rotr(x, 2) \^ rotr(x, 13) \^ rotr(x, 22)
@njit(inline='always')
def ep1(x):
return rotr(x, 6) \^ rotr(x, 11) \^ rotr(x, 25)
@njit(inline='always')
def sig0(x):
return rotr(x, 7) \^ rotr(x, 18) \^ (x >> 3)
@njit(inline='always')
def sig1(x):
return rotr(x, 17) \^ rotr(x, 19) \^ (x >> 10)
@njit(inline='always')
def check_bytes(p4, t0, t1, t2, prefix_arr_, target_words_, ktab_):
block = np.zeros(64, dtype=np.uint8)
plen = prefix_arr_.shape[0]
for i in range(plen):
block[i] = prefix_arr_[i]
block[plen:plen + 4] = p4
block[plen + 4] = t0
block[plen + 5] = t1
block[plen + 6] = t2
block[plen + 7] = ord('}')
total = plen + 8
block[total] = 0x80
bitlen = np.uint64(total * 8)
for i in range(8):
block[63 - i] = np.uint8(bitlen >> (8 * i))
w = np.zeros(64, dtype=np.uint32)
for i in range(16):
j = i * 4
w[i] = (np.uint32(block[j]) << 24) | (np.uint32(block[j + 1]) << 16) | (np.uint32(block[j + 2]) << 8) | np.uint32(block[j + 3])
for i in range(16, 64):
w[i] = (sig1(w[i - 2]) + w[i - 7] + sig0(w[i - 15]) + w[i - 16]) & np.uint32(0xffffffff)
a = np.uint32(0x6a09e667); b = np.uint32(0xbb67ae85)
c = np.uint32(0x3c6ef372); d = np.uint32(0xa54ff53a)
e = np.uint32(0x510e527f); f = np.uint32(0x9b05688c)
g = np.uint32(0x1f83d9ab); h = np.uint32(0x5be0cd19)
for i in range(64):
t1v = (h + ep1(e) + ch(e, f, g) + ktab_[i] + w[i]) & np.uint32(0xffffffff)
t2v = (ep0(a) + maj(a, b, c)) & np.uint32(0xffffffff)
h = g; g = f; f = e; e = (d + t1v) & np.uint32(0xffffffff)
d = c; c = b; b = a; a = (t1v + t2v) & np.uint32(0xffffffff)
h0 = (np.uint32(0x6a09e667) + a) & np.uint32(0xffffffff)
h1 = (np.uint32(0xbb67ae85) + b) & np.uint32(0xffffffff)
h2 = (np.uint32(0x3c6ef372) + c) & np.uint32(0xffffffff)
h3 = (np.uint32(0xa54ff53a) + d) & np.uint32(0xffffffff)
h4 = (np.uint32(0x510e527f) + e) & np.uint32(0xffffffff)
h5 = (np.uint32(0x9b05688c) + f) & np.uint32(0xffffffff)
h6 = (np.uint32(0x1f83d9ab) + g) & np.uint32(0xffffffff)
h7 = (np.uint32(0x5be0cd19) + h) & np.uint32(0xffffffff)
return (h0 == target_words_[0] and h1 == target_words_[1] and
h2 == target_words_[2] and h3 == target_words_[3] and
h4 == target_words_[4] and h5 == target_words_[5] and
h6 == target_words_[6] and h7 == target_words_[7])
@njit(parallel=True)
def search(cand4_, tail_arr_, prefix_arr_, target_words_, ktab_):
out = np.zeros(7, dtype=np.uint8)
found = np.zeros(1, dtype=np.uint8)
for i in prange(cand4_.shape[0]):
if found[0]: continue
p4 = cand4_[i]
for a in tail_arr_:
if found[0]: break
for b in tail_arr_:
if found[0]: break
for c in tail_arr_:
if check_bytes(p4, a, b, c, prefix_arr_, target_words_, ktab_):
out[0:4] = p4; out[4] = a; out[5] = b; out[6] = c
found[0] = 1; break
return found[0], out
search(cand4[:1], upper_arr[:1], prefix_arr, target_words, ktab)
found, out = search(cand4, upper_arr, prefix_arr, target_words, ktab)
if not found:
found, out = search(cand4, full_arr, prefix_arr, target_words, ktab)
if not found:
return None
return bytes(prefix_arr.tolist() + out.tolist() + [ord('}')]).decode()
def brute_python_reduced(info):
"""纯 Python 回退搜索 —— 覆盖常见生成模式"""
import itertools
prefix = info['known_prefix']
target = info['target_hex']
ks = info['ks']
mask = info['p4_mask']
cmpv = info['p4_cmp']
full = os.environ.get('CHARSET', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-')
suffix_sets = ['ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_', full]
def cond(s):
a, b, c, d = [ord(x) for x in s[:4]]
o0, o1, o2, o3 = a \^ ks[0], b \^ ks[1], c \^ ks[2], d \^ ks[3]
return (((((o2 \^ (o1 << 3) \^ (o0 << 6)) << 12) \^ (o3 << 9)) & mask) == cmpv)
for suf in suffix_sets:
for bcd in itertools.product(full, repeat=3):
pre4 = '_' + ''.join(bcd)
if not cond(pre4):
continue
for tail in itertools.product(suf, repeat=3):
p4 = pre4 + ''.join(tail)
flag = prefix + p4.encode() + b'}'
if hashlib.sha256(flag).hexdigest() == target:
return flag.decode()
return None
def main():
if len(sys.argv) != 2:
print(f'usage: python {Path(sys.argv[0]).name} input.apk', file=sys.stderr)
return 2
print("[*] 解析 APK 结构与提取常量...")
info = solve_parts(sys.argv[1])
print(f'[+] 架构: {info["so_name"]}')
print(f'[+] 目标SHA-256: {info["target_hex"]}')
print(f'[+] 中间片段: frag2={info["frag2"].decode()} frag3={info["frag3"].decode()} frag4={info["frag4"].decode()}')
print(f'[+] 已恢复前缀: {info["known_prefix"].decode()}')
print(f'[+] 位掩码: 0x{info["p4_mask"]:08x} / 期望: 0x{info["p4_cmp"]:08x}')
print(f'[+] RC4密钥流: {list(info["ks"])}')
print("[*] 开始闭壳搜索...")
flag = brute_with_numba(info)
if flag is None:
print("[!] numba 不可用,回退纯 Python...")
flag = brute_python_reduced(info)
if flag:
print(f'[+] FLAG = {flag}')
else:
print('[!] 未找到flag,请安装 numba/numpy 或扩展字符集。')
return 1
return 0
if __name__ == '__main__':
raise SystemExit(main())
运行结果
[*] 解析 APK 结构与提取常量...
[+] 架构: lib/x86_64/libnative-verify.so
[+] 目标SHA-256: c6f81b343cb4dc69b6a62e5af71541119c861658c72a1167ad539cf5422f4d59
[+] 中间片段: frag2=Rp4N frag3=oyzz frag4=rE
[+] 已恢复前缀: ISCC{FkJVk55_PfVw_v5qx
[+] 位掩码: 0x03ff0000 / 期望: 0x02c40000
[+] RC4密钥流: [236, 72, 50, 39]
[*] 开始闭壳搜索...
[+] FLAG = ISCC{FkJVk55_PfVw_v5qx_VMg7Lk}
Flag
`ISCC{FkJVk55_PfVw_v5qx_VMg7Lk}`
评论