③ Android 初级题 by 正己
题:三折叠,怎么折,都有面!
我:第三题,怎么划,都不变!
上手后没搞明白要怎么划,直接上 JEB 分析了。
直接进 MainActivity
瞅一眼 —— 一干二净,啥都没有。
于是在 com.zj.wuaipojie2025
这里随便翻,发现了 xxtea
的东西:
package com.zj.wuaipojie2025;
class TO {
public final String db(String value) { }
public final String eb(String value) { }
public static final int $stable = 0;
public static final Companion Companion = null;
private static final String YYLX = "my-xxtea-secret";
static {
TO.Companion = new Companion(null);
}
}
其中 db
函数能找到两处引用,eb
没找到,因此直接看第一个函数被谁调用了。
第一处:
public final void s(Context context0, int v, String s) {
Intrinsics.checkNotNullParameter(context0, "context");
Intrinsics.checkNotNullParameter(s, "value");
context0.getSharedPreferences("F", 0).edit().putString(String.valueOf(v), TO.Companion.db(s)).apply();
}
对该方法继续查找引用,得到一串密文,记录一下:
if((FoldFragment2.this.a >= f9)) {
Context context0 = FoldFragment2.this.requireContext();
Intrinsics.checkNotNullExpressionValue(context0, "requireContext(...)");
SPU.INSTANCE.s(context0, 1, "2hyWtSLN69+QWLHQ");
}
继续看 db
函数第二个引用:
SPU.INSTANCE.s(context0, 2, "hjyaQ8jNSdp+mZic7Kdtyw==");
this.getParentFragmentManager().beginTransaction().replace(id.fold2, new FoldFragment1()).addToBackStack(null).commit();
Toast.makeText(this.requireContext(), "快去寻找flag吧!", 0).show();
于是就猜这玩意是不是 xxtea(base64_decode(data), "my-xxtea-secret")
(毕竟密钥都说是 xxtea
了),拿到 CyberChef 尝试解密发现能得出结果:
密文 明文
2hyWtSLN69+QWLHQ flag{
hjyaQ8jNSdp+mZic7Kdtyw== xnkl2025!}
于是稍微拼接一下得到答案 flag{xnkl2025!}
④ Android 中级题 by 正己
拿 7z 看看,发现有 lib/*/*.so
文件。挑了 aarch64 版本解压(感觉 IDA 静态分析 aarch64 架构的安卓 so 最好),扔到 IDA 看看。
可以在 JNI_Onload
发现它动态注册了个 Check
方法。输入 jstring
,返回 jbool
,多半就是判断是否注册成功了。
一堆乱七八糟的东西,顺着返回值从下往上看,然后整理:
bool __fastcall sub_E8C54(JNIEnv *env, __int64 a2, void *a3) {
user_flag = (*env)->GetStringUTFChars(env, a3, 0LL);
if ( user_flag ) {
v22[1] = *(_OWORD *)off_15A638;
v22[0] = *(_OWORD *)off_15A628;
fn_do_something = *(void (__fastcall **)(_QWORD *, const char *, __int64, _QWORD *))((unsigned __int64)v22 & 0xFFFFFFFFFFFFFFF7LL | (8LL * (((unsigned __int8)(v11 | v14) ^ (((unsigned int)ao ^ (unsigned int)a) >> 24)) & 1)));
dword_16359C = -559038669;
seed[0] = 0LL;
seed[1] = 0LL;
out_buffer = (_QWORD *)operator new[](0x13uLL);
fn_do_something(seed, user_flag, 19LL, out_buffer);
ok = *out_buffer == 0x72ECF89BAF8F2748LL
&& out_buffer[1] == 0xB63AE26B0C720798LL
&& *(_QWORD *)((char *)out_buffer + 11) == 0xF75942B63AE26B0CLL;
operator delete[](out_buffer);
(*env)->ReleaseStringUTFChars(env, a3, user_flag);
} else {
return 0;
}
return ok;
}
注意这里还有个 IDA 识别到的 nullsub_1
,属于无效指令,直接将这个 CALL 改 NOP 即可,IDA 就能正常识别出它在干嘛了。
fn_do_something
这个值不能确定,但是附近就一个 ao
和 a
函数,估计就是这两个中的一个了。实在不行就两个都实现一下,看看哪个能出结果。
fn_do_something
来自两个函数运算的值,a
或 ao
。运气好挑其中一个做一下,做不出来看另一个就行,两个函数都长得差不多。
大概的流程就是将这个字符串传入给这个 a
或 ao
方法进行数据处理,看最终出来的数据和预期的数据是否相同。
看 a
:
void __fastcall a(uint8_t *seed, uint8_t *in, size_t len, uint8_t *out) {
uint8_t buffer[0x10];
memcpy(buffer, seed, 0x10);
for ( i = 0LL; i < len; ++i ) {
offset = i & 0xF;
if ( (i & 0xF) == 0 )
scramble_data_E9954(buffer);
value = buffer[offset] ^ in[i];
out[i] = value;
buffer[offset] = value;
}
}
scramble_data_E9954
实际上是所谓的白盒 AES(事后和正己老师交流得到的“内幕”信息)。当时因为没接触过这部分所以我直接取名叫打乱数据
,把它当成高强度的随机数生成器了。
传入的 seed
是十六字节长度的指针,被 scramble_data_E9954
处理生成一个新的十六字节的数据,然后对我们输入的数据进行 XOR 一次。每处理 16 字节后生成下一批 16 字节。
再组合两边的线索:
seed_0 = make_u128(0, 0);
seed_1 = make_u128(0x72ECF89BAF8F2748, 0xB63AE26B0C720798);
seed_2 = make_u128(0xF75942, 0);
flag_0 = scramble_data_E9954(seed_0) ^ seed_1;
flag_1 = scramble_data_E9954(seed_1) ^ seed_2;
flag = flag_0 + flag_1[:3]
这算法看着就复杂,所以就没想着自己整了… Unidbg,启动!
package dev.afdm_52pojie;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.sun.jna.Pointer;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class zj2025_q4_final {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final int fn_scramble_data;
private final boolean logging;
private final Memory memory;
zj2025_q4_final(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.qidian.dldl.official")
.addBackendFactory(new Unicorn2Factory(true))
.build();
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM();
vm.setVerbose(logging);
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/52pojie/libwuaipojie2025_zj_q4_final.so"), false);
module = dm.getModule();
fn_scramble_data = 0xE9954;
}
void destroy() {
IOUtils.close(emulator);
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
zj2025_q4_final test = new zj2025_q4_final(true);
test.work();
test.destroy();
}
public static byte[] longToBytes(long value) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putLong(value);
return buffer.array();
}
String decrypt(long[] seeds, int len) {
StringBuilder result = new StringBuilder();
MemoryBlock buffer = memory.malloc(0x10, true);
UnidbgPointer p_buffer = buffer.getPointer();
int left = len;
for (int i = 0, k = 0; i < len; i += 16, k+=2) {
p_buffer.setLong(0, seeds[k]);
p_buffer.setLong(8, seeds[k + 1]);
module.callFunction(emulator, fn_scramble_data, UnidbgPointer.nativeValue(p_buffer));
byte[] out = p_buffer.getByteArray(0, 16);
byte[] b1 = longToBytes(seeds[k + 2]);
byte[] b2 = longToBytes(seeds[k + 3]);
for (int j = 0; j < 8; j++) {
out[j] ^= b1[j];
out[j + 8] ^= b2[j];
}
result.append(new String(Arrays.copyOfRange(out, 0, Math.min(16, left)), StandardCharsets.UTF_8));
left -= 16;
}
return result.toString();
}
void work() {
String text = decrypt(new long[]{0, 0, 0x72ECF89BAF8F2748L, 0xB63AE26B0C720798L, 0xF75942, 0}, 19);
System.out.println(text);
}
}
跑一跑,得到 flag 生成算法:
flag{md5(uid+2025)}
⚠ 注意这个加号是字符串拼接,不是数值运算。
⑥ Windows|Android 二选一高级题 by 爱飞的猫 (我)
本来没想出安卓题的,但因为刚好题是用 C 写的,移植起来方便且感觉不同平台的难度应该都差不多,就试着移植了一下。
想看分析的话推荐这两个:
我就注重说说设计上的那些东西吧。
进入 VM 前的初始化是这样的:
vm_power_on(vm);
memcpy(&vm->memory[0x2000], vm_chars_table_rev, sizeof(vm_chars_table_rev));
memcpy(&vm->memory[0x1000], serial, 29);
vm_push(vm, uid);
vm_push(vm, 0x1000);
vm_push(vm, 0x2000);
vm_run(vm);
虚拟机启动时的堆栈顶部分别是:0x2000 (b36 码表地址), 0x1000 (用户输入 flag 地址), uid
。
其中 vm_run
就是模拟执行虚拟机。在这里会进行读取、解码、执行这三步来解释执行字节码。
- 读取:获取当前
PC
地址所指向的内存的值
- 解码:
- 高 5 位为
opcode
,低 3
位为小 operand
- 若是
operand
的值为 7 (0b111)
,则读入下一个字节为它的 operand
。
- 执行:根据
opcode
,执行不同的行为。
虚拟机实现的结构是这样的:
constexpr size_t kVMMemorySize = 0x10000;
struct vm_t
{
uint8_t memory[kVMMemorySize];
uint16_t pc;
uint16_t sp;
bool halt;
bool halt_on_explicit_request;
};
刚好 uint16_t
的寻址范围就是 0-FFFF
,也就不需要检查越界了,毕竟不管怎么算都在内存空间内。应该不会有人把这当成 pwn 题做吧…
这里就不分析虚拟机 handler 了,直接给出它的表:
名称 |
opcode |
operand |
描述 |
? |
00 |
N/A |
未使用 |
XOR |
01 |
❌ |
出栈两个值,XOR,再入栈 |
NEG |
02 |
❌ |
栈顶乘以 -1名称 |
? |
00 |
N/A |
未使用 |
XOR |
01 |
❌ |
出栈两个值,XOR,再入栈 |
NEG |
02 |
❌ |
栈顶乘以 -1 |
DROP |
03 |
✅ |
从栈顶删除 N 项 |
? |
04 |
N/A |
未使用 |
OR |
05 |
❌ |
出栈两个值,OR,再入栈 |
RET |
06 |
✅ |
出栈并设为 PC,再从堆栈弹出 N 项 |
NE |
07 |
❌ |
出栈两个值,入栈新值 (a != b) |
SWP |
08 |
✅ |
将栈顶与低 N 项交换 |
AND |
09 |
❌ |
出栈两个值,AND,再入栈 |
SHL |
0A |
✅ |
栈顶 << N |
NOT |
0B |
❌ |
栈顶取反 |
? |
0C |
N/A |
未使用 |
ADD |
0D |
❌ |
出栈两个值,相加,再入栈 |
? |
0E |
N/A |
未使用 |
JMP |
0F |
✅ |
跳转到指定地址 |
HALT |
10 |
❌ |
停止虚拟机运行 |
? |
11 |
N/A |
未使用 |
SHR |
12 |
✅ |
栈顶 >> N |
MOD |
13 |
❌ |
出栈两个值(栈顶为 a),入栈 (b % a) 的值。 |
? |
14 |
N/A |
未使用 |
LOBYTE |
15 |
❌ |
栈顶值取低 8 位 |
MUL |
16 |
❌ |
出栈两个值,相乘,入栈 |
CALL |
17 |
✅ |
将 CALL 之后的地址入栈,然后跳到参数指定的地址 |
JE |
18 |
✅ |
出栈两个值,若是相等则跳转 |
JNE |
19 |
✅ |
出栈两个值,若是不等则跳转 |
NOP |
1A |
❌ |
操作 |
INDEX |
1B |
❌ |
出栈两个值(栈顶为 a),入栈 byte[b + a] 的值。 |
DUP |
1C |
✅ |
将栈顶第 N 项的值再入栈一次。 |
? |
1D |
N/A |
未使用 |
LIT |
1E |
✅ |
入栈字面值 |
DEC |
1F |
❌ |
栈顶值减一 |
难度降低后有提供一份代码清单,但没有给上面这个表。或许给了这个表会更容易一些?
直接对着提供的代码清单分析 + 注解,也能看个七七八八:
; 堆栈: p_rev_table, p_flag, uid
lb_C000: CALL $+0x7f # lb_C081
lb_C002: HALT
; 堆栈: ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C003: LIT 0x00 ; result
lb_C004: LIT 0x00 ; i
; 堆栈: i, result, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
.validate_part_loop:
; 堆栈: i, result, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C005: DUP
lb_C006: DUP 0x06
; 堆栈: len, i, i, result, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C007: JE .validate_part_loop_end ; lb_C01D
; 堆栈: i, result, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C009: SWP
; 堆栈: result, i, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C00A: LIT 0x24 ; 36
lb_C00C: MUL
; 堆栈: result * 36, i, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C00D: DUP 0x03 ; p_rev_table
lb_C00E: DUP 0x05 ; p_flag_part
lb_C00F: DUP 0x03 ; i
lb_C010: INDEX ; => p_flag_part[i]
lb_C011: INDEX ; => p_rev_table[p_flag_part[i]]
; 堆栈: p_rev_table[p_flag_part[i]], result * 36, i, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
; if (p_rev_table[p_flag_part[i]] == 0) goto lb_C03B
lb_C012: DUP
lb_C013: LIT 0x00
lb_C014: JE .validate_part_bad_2 # lb_C03B
; 堆栈: p_rev_table[p_flag_part[i]], result * 36, i, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C016: ADD
lb_C017: DEC ; result = result * 36 + p_rev_table[p_flag_part[i]] - 1
; 堆栈: result, i, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C018: SWP
lb_C019: LIT 0x01
lb_C01A: ADD
; 堆栈: i + 1, result, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C01B: JMP .validate_part_loop # lb_C005
.validate_part_loop_end:
; 堆栈: i, result, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C01D: DROP
lb_C01E: DUP
lb_C01F: SHR 0x19 ; >> 25
; 堆栈: result >> 25, result, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
; if (result >> 25 == 0) goto .validate_part_bad
lb_C021: LIT 0x00
lb_C022: JE .validate_part_bad # lb_C040
; 堆栈: result, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C024: LIT 0x01
lb_C025: ADD
; 堆栈: result + 1, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C026: DUP 0x05 ; uid_hash & 0xff
lb_C027: LIT 0x13541 ; ((((0xda >> 7) << 8) | 0x35) << 8) | 0x41
lb_C035: MUL
; 堆栈: (uid_hash & 0xff) * 0x13541, result + 1, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C036: DUP 0x04
lb_C037: LOBYTE
lb_C038: ADD
; 堆栈: (p_flag_part & 0xff) + (uid_hash & 0xff) * 0x13541, result + 1, ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C039: MOD
; 堆栈: (result + 1) % ((p_flag_part & 0xff) + (uid_hash & 0xff) * 0x13541), ret_addr, p_rev_table, p_flag_part, len, uid_hash & 0xff
lb_C03A: RET 0x04
.validate_part_bad_2:
lb_C03B: OR
lb_C03C: LIT 0x01
lb_C03D: OR
lb_C03E: OR
lb_C03F: RET 0x04
.validate_part_bad:
lb_C040: LIT 0x01
lb_C041: OR
lb_C042: RET 0x04
vm1_validate:
; 堆栈: ret_addr, p_rev_table, p_flag, uid_hash
lb_C043: DUP 0x02 ; p_flag
lb_C044: CALL $+0x43 # lb_C089 => validate_flag_format
; 堆栈: bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C046: LIT 0x05 # i = 5
; 堆栈: i, bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C047: JMP .loop_begin # lb_C054
.loop_again:
; 堆栈: i, mod_result | bad, ret_addr, p_rev_table, p_flag, uid_hash >> 8
lb_C049: SWP
; 堆栈: mod_result | bad, i, ret_addr, p_rev_table, p_flag, uid_hash >> 8
lb_C04A: DUP 0x04 ; p_flag
lb_C04B: DUP 0x02 ; i
lb_C04C: INDEX ; => p_flag[i]
lb_C04D: LIT '-' ; '-'
lb_C04F: NEQ ; => p_flag[i] != '-' ? 1 : 0
; 堆栈: p_flag[i] != '-', mod_result | bad, i, ret_addr, p_rev_table, p_flag, uid_hash >> 8
lb_C050: OR
; 堆栈: p_flag[i] != '-' | mod_result | bad, i, ret_addr, p_rev_table, p_flag, uid_hash >> 8
lb_C051: SWP
; 堆栈: i, p_flag[i] != '-' | mod_result | bad, ret_addr, p_rev_table, p_flag, uid_hash >> 8
lb_C052: LIT 0x01
lb_C053: ADD ; i += 1
; bad = p_flag[i] != '-' | mod_result | bad
; 堆栈: i, bad, ret_addr, p_rev_table, p_flag, uid_hash >> 8
.loop_begin:
; 堆栈: i, bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C054: DUP 0x05 ; uid_hash
lb_C055: LOBYTE ; uid_hash & 0xff
; 堆栈: uid_hash & 0xff, i, bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C056: LIT 0x05
; 堆栈: 5, uid_hash & 0xff, i, bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C057: DUP 0x06 ; p_flag
lb_C058: DUP 0x03 ; i
lb_C059: ADD ; => push &p_flag[i]
; 堆栈: &p_flag[i], 5, uid_hash & 0xff, i, bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C05A: DUP 0x06
; 堆栈: p_rev_table, &p_flag[i], 5, uid_hash & 0xff, i, bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C05B: CALL $-0x5a # lb_C003
; 堆栈: mod_result, i, bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C05D: DUP 0x02
lb_C05E: OR
lb_C05F: SWP 0x02
lb_C060: DROP
; 堆栈: i, mod_result | bad, ret_addr, p_rev_table, p_flag, uid_hash
lb_C061: DUP 0x05
lb_C062: SHR 0x08
lb_C064: SWP 0x06
lb_C065: DROP
; 堆栈: i, mod_result | bad, ret_addr, p_rev_table, p_flag, uid_hash >> 8
lb_C066: LIT 0x05
lb_C067: ADD ; i += 5
; 堆栈: i, mod_result | bad, ret_addr, p_rev_table, p_flag, uid_hash >> 8
lb_C068: DUP
lb_C069: LIT 28
; if (i != 28) goto .loop_again
lb_C06B: JNE .loop_again # lb_C049
; 堆栈: i, mod_result | bad, ret_addr, p_rev_table, p_flag, uid_hash >> 8
.loop_end:
lb_C06D: DROP
; 堆栈: bad, ret_addr, p_rev_table, p_flag, uid_hash >> 8
; 如果坏: bad 的值是任何非零值
; 如果好: bad 的值是 0
lb_C06E: DEC
; 堆栈: 0xffff_ffff, ret_addr, p_rev_table, p_flag, uid_hash >> 8
lb_C073: LIT ((((0xc1 << 8 | 0x53) << 8) | 0x03) << 8) | 0xfb ; => 0xc15303fb
lb_C07F: XOR
; 好的情况: 0xffff_ffff ^ 0xc15303fb => 0x3eacfc04
lb_C080: RET 0x03
; 堆栈: ret_addr, p_rev_table, p_flag, uid
lb_C081: DUP 0x03 ; uid
lb_C082: CALL $+0x32 # lb_C0B6 => hash_uid
; 堆栈: uid_hash, ret_addr, p_rev_table, p_flag, uid
lb_C084: DUP 0x03 ; p_flag
lb_C085: DUP 0x03 ; p_rev_table
; 堆栈: p_rev_table, p_flag, uid_hash, ret_addr, p_rev_table, p_flag, uid
lb_C086: CALL $-0x45 # lb_C043
lb_C088: HALT
validate_flag_format:
lb_C089: LIT 0x00
lb_C08A: LIT 'f'
lb_C08C: DUP 0x03
lb_C08D: LIT 0x00
lb_C08E: INDEX
lb_C08F: XOR
lb_C090: OR
lb_C091: LIT 'l'
lb_C093: DUP 0x03
lb_C094: LIT 0x01
lb_C095: INDEX
lb_C096: XOR
lb_C097: OR
lb_C098: LIT 'a'
lb_C09A: DUP 0x03
lb_C09B: LIT 0x02
lb_C09C: INDEX
lb_C09D: XOR
lb_C09E: OR
lb_C09F: LIT 'g'
lb_C0A1: DUP 0x03
lb_C0A2: LIT 0x03
lb_C0A3: INDEX
lb_C0A4: XOR
lb_C0A5: OR
lb_C0A6: LIT '{'
lb_C0A8: DUP 0x03
lb_C0A9: LIT 0x04
lb_C0AA: INDEX
lb_C0AB: XOR
lb_C0AC: OR
lb_C0AD: LIT '}'
lb_C0AF: DUP 0x03
lb_C0B0: LIT 0x1c
lb_C0B2: INDEX
lb_C0B3: XOR
lb_C0B4: OR
lb_C0B5: RET 0x01
hash_uid:
; 堆栈: uid, ret_addr
lb_C0B6: LIT 0x80808080
lb_C0C1: LIT 0xffffffff ; CRC32 初始值
; 堆栈: -1, 0x8080_8080, uid, ret_addr
lb_C0C3: LIT '2'
lb_C0C5: CALL $+0x61 # lb_C128
; 堆栈: crc, 0x8080_8080, uid, ret_addr
lb_C0C7: LIT '0'
lb_C0C9: CALL $+0x5d # lb_C128
lb_C0CB: LIT '2'
lb_C0CD: CALL $+0x59 # lb_C128
lb_C0CF: LIT '5'
lb_C0D1: CALL $+0x55 # lb_C128
lb_C0D3: DUP 0x03 ; uid
lb_C0D4: SHR 24 ; SHR (5+6+3+5+4)
lb_C0D9: CALL $+0x4d # lb_C128
lb_C0DB: LIT '5'
lb_C0DD: CALL $+0x49 # lb_C128
lb_C0DF: LIT '2'
lb_C0E1: CALL $+0x45 # lb_C128
lb_C0E3: LIT 'p'
lb_C0E5: CALL $+0x41 # lb_C128
lb_C0E7: LIT 'o'
lb_C0E9: CALL $+0x3d # lb_C128
lb_C0EB: LIT 'j'
lb_C0ED: CALL $+0x39 # lb_C128
lb_C0EF: LIT 'i'
lb_C0F1: CALL $+0x35 # lb_C128
lb_C0F3: LIT 'e'
lb_C0F5: CALL $+0x31 # lb_C128
lb_C0F7: DUP 0x03 ; UID
lb_C0F8: SHR 16 ; (6+4+6)
lb_C0FB: CALL $+0x2b # lb_C128
lb_C0FD: LIT 'a'
lb_C0FF: CALL $+0x27 # lb_C128
lb_C101: LIT 'f'
lb_C103: CALL $+0x23 # lb_C128
lb_C105: LIT 'd'
lb_C107: CALL $+0x1f # lb_C128
lb_C109: LIT 'm'
lb_C10B: CALL $+0x1b # lb_C128
lb_C10D: DUP 0x03 ; UID
lb_C10E: SHR 8 ; (2+6)
lb_C110: CALL $+0x16 # lb_C128
lb_C112: LIT '2'
lb_C114: CALL $+0x12 # lb_C128
lb_C116: LIT '0'
lb_C118: CALL $+0x0e # lb_C128
lb_C11A: LIT '2'
lb_C11C: CALL $+0x0a # lb_C128
lb_C11E: LIT '5'
lb_C120: CALL $+0x06 # lb_C128
lb_C122: DUP 0x03 ; UID
lb_C123: CALL $+0x03 # lb_C128
; 堆栈: crc, 0x8080_8080, uid, ret_addr
lb_C125: NOT
lb_C126: OR # => crc32("...") | 0x80808080
lb_C127: RET 0x01
; 堆栈: ret_addr, next_byte, prev_crc
crc32_update:
lb_C128: LIT 0xedb88320 ; CRC32 常数
lb_C139: LIT 0x07 ; i = 7
lb_C13C: DUP 0x04 ; prev_crc
lb_C13D: DUP 0x04 ; next_byte
lb_C13E: LOBYTE ; next_byte &= 0xff
; 堆栈: next_byte & 0xff, prev_crc, i, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C13F: XOR
; 堆栈: crc, i, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C140:
lb_C140: DUP
lb_C141: LIT 0x01
lb_C142: AND
lb_C143: NEG
; 堆栈: -(crc & 1), crc, i, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C144: DUP 0x03
lb_C145: AND
; 堆栈: (-(crc & 1) & 0xedb88320), crc, i, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C146: SWP
; 堆栈: crc, (-(crc & 1) & 0xedb88320), i, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C147: SHR 0x01
; 堆栈: crc >> 1, (-(crc & 1) & 0xedb88320), i, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C148: XOR
; 堆栈: (crc >> 1) ^ (-(crc & 1) & 0xedb88320), i, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C149: DUP 0x01
lb_C14A: DEC
; 堆栈: i - 1, (crc >> 1) ^ (-(crc & 1) & 0xedb88320), i, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C14B: SWP 0x02
; 堆栈: i, (crc >> 1) ^ (-(crc & 1) & 0xedb88320), i - 1, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C14C: LIT 0x00
lb_C14D: JNE $-0x0f # lb_C140
; (crc >> 1) ^ (-(crc & 1) & 0xedb88320), i - 1, 0xedb88320, ret_addr, next_byte, prev_crc
lb_C14F: SWP 0x02
lb_C150: DROP 0x02
; (crc >> 1) ^ (-(crc & 1) & 0xedb88320), ret_addr, next_byte, prev_crc
lb_C151: RET 0x02
; 后面其实没了,都是随机生成的填充物
因为虚拟机的限制不能直接入栈一个 32 位的数,所以需要这么大的数字的时候都是分段载入(LIT + SHL + OR
组合)。
大概流程:
- 验证输入的 flag 格式为
flag{XXXXX-XXXXX-XXXXX-XXXXX}
- 将 UID 与一些其它数据凑在一起,进行
crc32
计算得到一个 32 位的数字。
- 与
0x80808080
位或,这样每 8 位的最高位都被设定为 1(确保用户不会得到 0
)。
- 分为四个 8 位数字,记作
uid_hash[i]
(小端序表示的四个字节)。
- 解析序列号的四段(上面的
XXXXX
)
- 每一段做
base36
解码
- 对应的反查表在虚拟机内存的
0x2000
位置。
- 对于这个值(记为
x[i]
),需要满足以下条件:
- 至少有 25 位:
(x[i] >> 25) != 0
- 数学等式成立:
x[i] % (79169 * uid_hash[i] + offset[i]) = x[i] - 1
- 其中
offset[i]
的值分别为 5
、11
、17
、23
(每一段的起始偏移)
- 等价数学公式:
(x[i] + 1) % (79169 * uid_hash[i] + offset[i]) = 0
- 格式正确且四段的验证通过则成功。
⑥ 后记
出这个题的想法就是做一个“传统”的 CM 题;现在软件的验证模式很多都改成了云端验证,或使用更安全的现代加密算法对密钥进行验证。
如果读者有分析过 IDM 的注册算法,那么你会发现这两者有很多相似的地方(当然,我就没加联网验证了)。有兴趣可以看上面的分析贴来对比下该题的算法。
但话又说回来了,单纯整一个这样的算法题没什么意思 —— 用 IDA Pro 的 F5 生成一下伪码就能看出题目在问什么了。
就想着做一个简单的虚拟机,让大家在这个模拟出来的虚拟机中对抗,没有加反调试或其它奇奇怪怪的坑。
一开始想的是整一个 6502 模拟器,可惜它的这个汇编实在是不太会写,而且完整的模拟器实现起来也没时间;最终决定自己琢磨一个简单的指令集。为了避免让虚拟机太复杂,就只有堆栈 —— 不过后来也被 @侃遍天下无二人 提醒:“栈式虚拟机就是直接给代码,流程也不是那么简单的”。
不过自己写一个指令集还有个好处,就是可以将字节码的映射乱序,也可以自己随意扩展。
因为不太懂语法树这些高科技,就简简单单做了个逐行读取的汇编器。虽然不是很高级,但凑合着出个题还是够用的。
在任务提交页面如果传入了错误的序列号,会给出一个提示 “要不要试试 Frida?”。如果使用 frida(不止安卓,也可以 hook Windows 程序哦),感觉就有点像前端常见的 jsvmp 的对抗了 —— 在分发器代码加个钩子,每次经过的时候看看堆栈有什么东西,应该就能把算法看懂个七七八八了吧。不过这也是理论上的,这题我还没试过它;如果有人写个基于 frida 的 WP 的话我会很开心。我个人分析这类虚拟机的时候更喜欢把它抽象成类似汇编一样的代码(也就是降低难度提供的那个代码清单),然后再分析它在干什么。
⑧ Android 高级题 by qtfreet00
🔥 作为内测用户,提前体验过早期没上混淆的版本,比较容易看算法过程,所以也就没参与答题了…
这混淆对我来说太难了,如果没做过早期版本我是做不出来的… 所以分析过程还是推荐去看 ngiokweng 佬的 WP!
综上所述,混淆干不动。所以就只贴一下我的 Unidbg 代码:
package dev.afdm_52pojie;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Base64;
public class qtf2025v2 {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass MainActivity;
private final long des_init;
private final long des_decrypt;
private final long des_encrypt;
private final long des_scramble_u64;
private final MemoryBlock des_inst;
private final boolean logging;
private final UnidbgPointer des_key_1;
private final UnidbgPointer des_key_2;
private final UnidbgPointer des_key_3;
qtf2025v2(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.qidian.dldl.official")
.addBackendFactory(new Unicorn2Factory(true))
.build();
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM();
vm.setVerbose(logging);
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/52pojie/lib52pojie-2025qtf-v2.so"), false);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();
MainActivity = vm.resolveClass("com/wuaipojie/crackme2025/MainActivity");
des_init = 0x18A00;
des_scramble_u64 = 0x184A8;
des_decrypt = 0x15D84;
des_encrypt = 0x1629C;
des_inst = memory.malloc(16 * 8 * 3, true);
des_key_1 = des_inst.getPointer();
des_key_2 = des_key_1.share(16 * 8, des_key_1.getSize());
des_key_3 = des_key_1.share(16 * 8 * 2, des_key_1.getSize());
System.out.println("init keys");
module.callFunction(emulator, des_init, UnidbgPointer.nativeValue(des_key_1), 1, 2, 3);
System.out.println("init keys ok");
}
public static byte[] longToBytes(long value) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putLong(value);
return buffer.array();
}
public static long bytesToLong(byte[] bytes) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
buffer.order(ByteOrder.LITTLE_ENDIAN);
return buffer.getLong();
}
public long scramble_des_data(long in) {
return module.callFunction(emulator, des_scramble_u64, in).longValue();
}
public long encrypt(long value) {
value = scramble_des_data(value);
value = module.callFunction(emulator, des_encrypt, UnidbgPointer.nativeValue(des_key_1), value).longValue();
value = module.callFunction(emulator, des_decrypt, UnidbgPointer.nativeValue(des_key_2), value).longValue();
value = module.callFunction(emulator, des_encrypt, UnidbgPointer.nativeValue(des_key_3), value).longValue();
return value;
}
public void encryptBytes(byte[] bytes) {
assert bytes.length % 8 == 0;
for (int i = 0; i < bytes.length; i += 8) {
byte[] data = new byte[8];
System.arraycopy(bytes, i, data, 0, 8);
byte[] encryptedBytes = longToBytes(encrypt(bytesToLong(data)));
System.arraycopy(encryptedBytes, 0, bytes, i, 8);
}
}
public long decrypt(long value) {
value = module.callFunction(emulator, des_decrypt, UnidbgPointer.nativeValue(des_key_3), value).longValue();
value = module.callFunction(emulator, des_encrypt, UnidbgPointer.nativeValue(des_key_2), value).longValue();
value = module.callFunction(emulator, des_decrypt, UnidbgPointer.nativeValue(des_key_1), value).longValue();
value = scramble_des_data(value);
return value;
}
public void decryptBytes(byte[] bytes) {
assert bytes.length % 8 == 0;
for (int i = 0; i < bytes.length; i += 8) {
byte[] data = new byte[8];
System.arraycopy(bytes, i, data, 0, 8);
byte[] decryptedBytes = longToBytes(decrypt(bytesToLong(data)));
System.arraycopy(decryptedBytes, 0, bytes, i, 8);
}
}
public static String b64_encode(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
public static byte[] b64_decode(String base64) {
return Base64.getDecoder().decode(base64);
}
void destroy() {
IOUtils.close(emulator);
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
qtf2025v2 test = new qtf2025v2(true);
test.work();
test.destroy();
}
void work() {
System.out.println("hook start");
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + des_scramble_u64, new WrapCallback<HookZzArm64RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
long x0_input = ctx.getXLong(0);
System.out.println("des_decrypt_transform x0_input =" + Long.toHexString(x0_input));
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
long x0_output = ctx.getXLong(0);
System.out.println("des_decrypt_transform x0_output=" + Long.toHexString(x0_output));
}
});
hookZz.wrap(module.base + 0x16CA0, new WrapCallback<HookZzArm64RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
Pointer x0_des_key = ctx.getXPointer(0);
long x1_des_data_ptr = ctx.getXLong(1);
int x2_des_mode = ctx.getXInt(2);
Inspector.inspect(x0_des_key.getByteArray(0, 16 * 8), "des_decrypt des_key");
System.out.println("des_decrypt x1=" + Long.toHexString(x1_des_data_ptr));
System.out.println("des_decrypt x2_des_mode=" + x2_des_mode);
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
super.postCall(emulator, ctx, info);
long x0 = ctx.getXLong(0);
System.out.println("des_decrypt result=" + x0);
}
});
boolean result = MainActivity.callStaticJniMethodBoolean(emulator, "checkSn(Ljava/lang/String;)Z", "000000001111111122222222");
System.out.println("result=" + result);
System.out.println("-----------------------------------------------------------------------------------------");
Inspector.inspect(des_key_1.getByteArray(0, 16 * 8), "des_key_1");
Inspector.inspect(des_key_2.getByteArray(0, 16 * 8), "des_key_2");
Inspector.inspect(des_key_3.getByteArray(0, 16 * 8), "des_key_3");
byte[] input_data = b64_decode("FTF6lS6LGnzmXfxiNeFDS12UP+k6EEaD");
Inspector.inspect(input_data, "input_data");
decryptBytes(input_data);
Inspector.inspect(input_data, "decrypted");
}
}
最后得到答案
>-----------------------------------------------------------------------------<
[00:00:00 000]decrypted, md5=24f2880846a591c1ed3a0a7368d37029, hex=3532506f6a694548615070796e4577593361723230323521
size: 24
0000: 35 32 50 6F 6A 69 45 48 61 50 70 79 6E 45 77 59 52PojiEHaPpynEwY
0010: 33 61 72 32 30 32 35 21 3ar2025!
^-----------------------------------------------------------------------------^
⑧ 题后记
当初这一串 A→B→A
的调用且参数一样,就猜大概率是 3-DES 了(去年 2024 年手动实现过一次,印象深刻)。
所以我的思路就是构造一下变量,跑一下它的初始化代码,然后反过来用 B→A→B
的顺序调用就能将加密反转成解密步骤,得到正确的明文。
安卓篇后记
本来没想发 ⑧ 的,但我看其他人的 WP 好像没怎么用 Unidbg,所以就把我的脚本发一发。Unidbg 真好用啊。
还没学过 Unidbg 的同学快去看 @正己 老师出的《安卓逆向这档事》第二十三课:黑盒魔法之Unidbg吧!