吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3452|回复: 19
收起左侧

[Android CTF] 【2025春节】解题领红包之安卓篇

[复制链接]
爱飞的猫 发表于 2025-2-14 06:40
本帖最后由 爱飞的猫 于 2025-2-14 06:52 编辑

③ 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 ) {
    // 和最终 `ok` 无关的变量跳过啦

    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 这个值不能确定,但是附近就一个 aoa 函数,估计就是这两个中的一个了。实在不行就两个都实现一下,看看哪个能出结果。

fn_do_something 来自两个函数运算的值,aao。运气好挑其中一个做一下,做不出来看另一个就行,两个函数都长得差不多。

大概的流程就是将这个字符串传入给这个 aao 方法进行数据处理,看最终出来的数据和预期的数据是否相同。

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 DvmClass MainActivity;
    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(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/52pojie/libwuaipojie2025_zj_q4_final.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
//        dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
        module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块

//        MainActivity = vm.resolveClass("com/wuaipojie/crackme2025/MainActivity");

        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); // 清理内存,设置 PC/SP 寄存器等。

// 拷贝 base36 反查表和用户输入的 flag 到虚拟机内存
memcpy(&vm->memory[0x2000], vm_chars_table_rev, sizeof(vm_chars_table_rev));
memcpy(&vm->memory[0x1000], serial, 29);

// 参数入栈
vm_push(vm, uid); // uid
vm_push(vm, 0x1000);   // flag
vm_push(vm, 0x2000);   // table
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; // program counter 当前代码指针
    uint16_t sp; // stack pointer 当前栈指针
    bool halt; // 是否结束运行
    bool halt_on_explicit_request; // 主动调用 halt 指令结束的
};

刚好 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] 的值分别为 5111723 (每一段的起始偏移)
      • 等价数学公式:(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(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/52pojie/lib52pojie-2025qtf-v2.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
        dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
        module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块

        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) {
        // module.callFunction(emulator, des_scramble_u64, 0x3031323334353637L).longValue();
        // module.callFunction(emulator, des_scramble_u64, 0xec6cac2ccc4c8c0cL).longValue();
        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,支持inline hook,文档看https://github.com/jmpews/HookZz

        // 发现 f(f(x)) = x,不需要管它,需要的时候直接调用就行。
        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));
            }
        });

        // des(round_keys: u8[128], data: u64, mode: u32) -> u64
        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); // 0/1 决定是加密还是解密,忘了哪个是哪个了。
            }

            @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);
            }
        });

        // 提前知晓输入是 24 字节,直接调用可以看看捕捉的 des 参数。
        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");

        // 传入对比是否等于的数据
        // 15 31 7A 95 2E 8B 1A 7C E6 5D FC 62 35 E1 43 4B 5D 94 3F E9 3A 10 46 83
        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吧!

免费评分

参与人数 12吾爱币 +11 热心值 +11 收起 理由
civet + 1 我很赞同!
wojiushishuai + 1 谢谢@Thanks!
yueqian1202 + 1 鼓励转贴优秀软件安全工具和文档!
submarine1620 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Sydyanlei0 + 1 + 1 用心讨论,共获提升!
Coxxs + 3 + 1 谢谢@Thanks!
正己 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ngiokweng + 2 + 1 谢谢@Thanks!
lj0822 + 1 + 1 我很赞同!
jackyyue_cn + 1 + 1 谢谢@Thanks!
melooon + 1 + 1 热心回复!
lsb2pojie + 1 + 1 热心回复!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

伤城幻化 发表于 2025-2-14 12:32
ida打开第六题的代码静态分析了好几天 GPT也整上了。我是想用frida做的 用frida的内存API来hook然后复现出switch里面的流程,奈何看着伪代码兴致勃勃的就是没法下口 感觉是对C的指针和内存了解太少了,也不懂这种VM虚拟机 这东西,还是学的太少了
Lcp1027 发表于 2025-2-14 08:27
a5570622 发表于 2025-2-14 10:24
sherhase 发表于 2025-2-14 12:18
感谢分享,学习一下
yycc1818 发表于 2025-2-14 12:39
6666666++++
yur0 发表于 2025-2-15 01:26
小白不太会写vm parser,我是在每个handler写ida python脚本trace流程,然后硬撕的
正己 发表于 2025-2-15 19:29
师傅牛逼
兮陌丶凉 发表于 2025-2-17 08:53
随便看看
llfly 发表于 2025-2-17 17:51
感谢大佬分享 , 学习学习
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-5-22 02:36

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表