【2025春节】解题领红包之安卓篇

查看 103|回复 9
作者:伤城幻化   
③ 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 这个值不能确定,但是附近就一个 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
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
跑一跑,得到 flag 生成算法:
flag{md5(uid+2025)}

⚠ 注意这个加号是字符串拼接,不是数值运算。

⑥ Windows|Android 二选一高级题 by 爱飞的猫 (我)
本来没想出安卓题的,但因为刚好题是用 C 写的,移植起来方便且感觉不同平台的难度应该都差不多,就试着移植了一下。
想看分析的话推荐这两个:
  • 2025吾愛解題領紅包活動(Android題解) by ngiokweng
  • 【2025春节】解题领红包之六(安卓版)——Writeup by jackyyue_cn

    我就注重说说设计上的那些东西吧。
    进入 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 了,直接给出它的表:
    [table]
    [tr]
    名称[/td]
    opcode[/td]
    operand[/td]
    [td]描述[/td]
    [/tr]
    [tr]
    [td]?

    函数, 虚拟机

  • Lcp1027   

    ida打开第六题的代码静态分析了好几天 GPT也整上了。我是想用frida做的 用frida的内存API来hook然后复现出switch里面的流程,奈何看着伪代码兴致勃勃的就是没法下口 感觉是对C的指针和内存了解太少了,也不懂这种VM虚拟机 这东西,还是学的太少了
    a5570622   

    感谢UP主精彩教程
    sherhase   

    感谢大佬分享 , 学习学习
    yycc1818   

    感谢分享,学习一下
    yur0   

    6666666++++
    兮陌丶凉   

    小白不太会写vm parser,我是在每个handler写ida python脚本trace流程,然后硬撕的
    llfly   

    师傅牛逼
    yycc1818   

    随便看看
    a5570622   

    感谢大佬分享 , 学习学习
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部