吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3921|回复: 46
收起左侧

[Android 原创] 《安卓逆向这档事》第二十六课、Unidbg之补完环境我就睡(下)

  [复制链接]
正己 发表于 2025-10-9 10:21
本帖最后由 正己 于 2025-10-9 10:27 编辑

|600

一、课程目标

  1. 掌握 Unidbg 中处理不同场景的系统调用(Syscall)Hook 策略
  2. 学习通过高层与底层 Hook 联动,模拟复杂的库函数(如 popen)
  3. 应用 Dobby 等工具对关键库函数(如 gettid)进行修补,以绕过环境检测

二、工具

1.教程 Demo
2.IDEA
3.IDA

三、课程内容

一. 补系统调用

1.补系统调用环境的核心概念

系统调用(Syscall)补环境是 Unidbg 模拟执行中一个基础且高级的环节。当原生库(. So 文件)为了性能、对抗或功能需要,绕过标准库(libc)函数,通过 SVC 等底层指令直接向操作系统内核发起请求时,Unidbg 必须能够拦截并模拟这些内核级别的行为。由于 Unidbg 并非一个完整的操作系统,其对内核的模拟是不完备的。如果 so 文件请求了一个 Unidbg 未实现或模拟不完善的系统调用(通过系统调用号 NR 区分),通常会导致模拟流程出错、返回无效数据(如 fstat 对目录返回全零),或进入非预期的逻辑分支,最终使模拟失败或结果失真。因此,系统调用补环境的目的就是识别并接管这些对内核的底层请求,通过自定义 SyscallHandler 提供一个符合目标 so 逻辑预期的模拟行为或返回值,从而“欺骗”so 文件,使其相信自己正与一个真实的 Linux/Android 内核交互。

2.解读 unidbg 的警告日志

当 unidbg 遇到一个它无法处理的软中断(SVC)时,通常会打印出如下格式的 WARN 日志:

[00:46:49 186]  WARN [com.github.unidbg.linux.ARM64SyscallHandler] (ARM64SyscallHandler:399) - handleInterrupt intno=2, NR=165, svcNumber=0x0, PC=RX@0x401ba3d4[libc.so]0x6a3d4, LR=RX@0x40000770[libdemo.so]0x770, syscall=null

这个日志包含了定位问题的关键信息:

  • handleInterrupt intno=2: intno=2 代表这是一个 EXCP_SWI(Software Interrupt),即软中断,通常由 SVC 指令触发,这是系统调用的入口。
  • NR=165: 这是最重要的字段,系统调用号 (Syscall Number)。它唯一标识了是哪个系统调用。你需要根据 CPU 架构(ARM 32/ARM 64)去查找对应的系统调用表。例如,在 ARM 64 下,165 号系统调用是 getrusage。你可以访问这个网站来查询。
  • svcNumber=0x0: 这是 SVC 指令后的立即数。在 unidbg 的设计中,约定 svcNumber 为 0 的才是真正的系统调用
  • PCLR: PC (Program Counter) 指示了当前执行到的地址,通常在 libc.so 内部。LR (Link Register) 指示了调用该系统调用的返回地址,通常在你的目标 so 中,这能帮你快速定位到是 so 中哪块代码触发了该系统调用。
  • syscall=null: 如果 unidbg 对这个 NR 有部分认知,可能会在这里显示系统调用的名字,但大多数未实现的情况这里都是 null
    区分系统调用与 JNI 调用
    一个常见的困惑点是,JNI 函数调用失败的日志和系统调用非常相似,因为 unidbg 在底层都使用了 SVC 指令作为跳板。
    关键区分点在于 svcNumber
  • 系统调用: svcNumber 等于 0x0
  • JNI 调用: svcNumber 不等于 0x0,它是一个用于内部索引 JNI 函数的值。
    例如,一个 JNI 调用失败的日志可能如下,注意 svcNumber=0x16f
    [01:00:43 681]  WARN [com.github.unidbg.linux.ARM64SyscallHandler] ... svcNumber=0x16f ...
    java.lang.UnsupportedOperationException: com/aliyun/TigerTally/A->ct()Landroid/content/Context;

    并且,JNI 调用失败通常会紧跟着抛出 UnsupportedOperationException,并带有清晰的 JNI 方法签名。

3.核心方法论:自定义 SyscallHandler

解决系统调用问题的通用范式是创建一个继承自 unidbg 原生 SyscallHandler 的子类,并重写或补充其中的方法。
首先,你需要定义一个自己的 SyscallHandler。以 ARM 64 为例:

import com.github.unidbg.Emulator;
import com.github.unidbg.linux.ARM64SyscallHandler;
import com.github.unidbg.memory.SvcMemory;
// 继承自ARM64SyscallHandler,如果是32位则继承ARM32SyscallHandler
public class MySyscallHandler extends ARM64SyscallHandler {
    public MySyscallHandler(SvcMemory svcMemory) {
        super(svcMemory);
        setVerbose(true); // 可按需开启详细日志
    }
    // 在这里重写或添加你的处理逻辑
}

然后,在构建 Emulator 时,通过 AndroidEmulatorBuilder 将其替换掉默认的处理器。

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.file.linux.AndroidFileIO;
import com.github.unidbg.linux.android.AndroidARM64Emulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.memory.SvcMemory;
import com.github.unidbg.unix.UnixSyscallHandler;

AndroidEmulatorBuilder builder = new AndroidEmulatorBuilder(true) {  
    @Override  
    public AndroidEmulator build() {  
        return new AndroidARM64Emulator(processName, rootDir, backendFactories) {  
            @Override  
            protected UnixSyscallHandler<AndroidFileIO> createSyscallHandler(SvcMemory svcMemory) {  
                return new MySyscallHandler(svcMemory);  
            }  
        };  
    }  
};  
builder.setProcessName("com.zj.wuaipojie");  
emulator = builder.build();
4.场景一:系统调用未实现

这是最常见的情况,unidbg 对某个 NR 完全没有实现,直接在 handleInterrupt 处抛出警告。
案例 1:getcpu (NR=168)

在我们的 MySyscallHandler 中,重写 handleUnknownSyscall 方法,捕获未被处理的 NR

// In MySyscallHandler.java
import com.github.unidbg.Emulator;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.pointer.UnidbgPointer;
import com.sun.jna.Pointer;
import unicorn.Arm64Const;
@Override  
protected boolean handleUnknownSyscall(Emulator<?> emulator, int NR) {  
    System.err.println(">>> MySyscallHandler is processing syscall NR = " + NR);  
    Backend backend = emulator.getBackend();  
    switch (NR) {  

        /** getcpu (NR=168): x0=cpu*, x1=node* */  
        case 168: {  
            Pointer cpuPtr = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X0);  
            Pointer nodePtr = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1);  

            // C++ 代码会多次调用 getcpu,期望 cpu 号码会变化且不为0  
            // 我们用一个随机数来模拟 CPU 核心的切换  
            int currentCpu = rng.nextInt(8); // 模拟8核CPU  
            if (cpuPtr != null) {  
                cpuPtr.setInt(0, currentCpu);  
            }  
            if (nodePtr != null) {  
                // NUMA 节点通常为 0                nodePtr.setInt(0, 0);  
            }  
            // 成功返回 0            writeX(backend, Arm64Const.UC_ARM64_REG_X0, 0);  
            return true;  
        }  

    }  
    return super.handleUnknownSyscall(emulator, NR);  
}

案例 2:statx (NR=291)
statx 是 Linux 中一个现代化的、用于获取文件元数据(metadata)的系统调用。它是 stat, fstat, lstat 的继任者和升级版。

文件的元数据,就是描述文件属性的信息,例如:

  • 文件大小 (size)
  • 文件类型 (是普通文件、目录、还是链接?)
  • 权限 (mode, 例如 rwx-r-x--x)
  • 所有者 (UID, GID)
  • 时间戳 (访问时间 atime、修改时间 mtime、状态变更时间 ctime)

statx 相对于旧版 stat 的主要优势:
1.更丰富的信息statx 可以获取旧版 stat 无法提供的信息,最典型的就是文件的创建时间(birth time, btime
2.更高精度的时间戳:旧版 stat 的时间戳只精确到秒。而 statx 可以提供纳秒(nanosecond)级别的精度,这对于现代文件系统和应用至关重要。
3.更高的效率和灵活性 (Mask 机制):调用旧版 stat 时,内核会把所有元数据一次性全返回给你,即使你只关心文件大小。statx 引入了一个 mask(掩码)参数,允许调用者明确告诉内核:“我只对文件大小和修改时间感兴趣”。内核就会只获取并返回这些信息,避免了不必要的工作,提高了效率
4.
更好的扩展性**:它的结构体设计考虑了未来,留有备用字段,方便以后添加新的文件属性而不需要再次设计新的系统调用。
如何填充statx结构:  

struct statx {
    __u32 stx_mask;        /* Mask of fields returned */
    __u32 stx_blksize;     /* Block size for filesystem I/O */
    __u64 stx_attributes;  /* Extra file attribute hints */
    __u32 stx_nlink;       /* Number of hard links */
    __u32 stx_uid;         /* User ID of owner */
    __u32 stx_gid;         /* Group ID of owner */
    __u16 stx_mode;        /* File type and mode */
    __u16 __spare0[1];
    __u64 stx_ino;         /* Inode number */
    __u64 stx_size;        /* Total size in bytes */
    __u64 stx_blocks;      /* Number of 512B blocks allocated */
    __u64 stx_attributes_mask;
    /* The following fields are copied from struct statx_timestamp */
    struct statx_timestamp stx_atime; /* Last access */
    struct statx_timestamp stx_btime; /* Creation */
    struct statx_timestamp stx_ctime; /* Last status change */
    struct statx_timestamp stx_mtime; /* Last modification */
    /* If this structure is extended, then constants described in
       statx(2) will be defined to describe the new fields. */
};

struct statx_timestamp {
    __s64 tv_sec;
    __u32 tv_nsec;
    __s32 __spare;
};

接下来就是把上面的结构发给 AI 来伪造

/** statx (NR=291): x1=path, x4=statx* */
case 291: {
        // 1. 获取参数:路径指针和用于接收结果的 statx 结构体指针
        Pointer pathPtr = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1);
        Pointer stx = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X4);
        String path = pathPtr != null ? pathPtr.getString(0) : null;

        if (stx != null) {
                // 2. 核心技巧:使用ByteBuffer来构建内存中的结构体
                //    分配足够空间,并务必设置为LITTLE_ENDIAN(小端序),匹配ARM架构
                ByteBuffer bb = ByteBuffer.allocate(0x100).order(ByteOrder.LITTLE_ENDIAN);

                // 3. 按照 statx 结构体的定义,依次填充字段
                //    具体填充什么值,取决于目标so关心哪些字段。通常给一些非零的、看起来合理的值即可。
                bb.putInt(0x000007ff);  // stx_mask: STATX_ALL
                bb.putInt(4096);        // stx_blksize
                bb.putLong(0);          // stx_attributes
                bb.putInt(1);           // stx_nlink (链接数)
                bb.putInt(1000);        // stx_uid (user id)
                bb.putInt(1000);        // stx_gid (group id)

                // 根据路径判断是目录还是文件,并设置对应的模式
                int S_IFDIR = 0x4000, S_IFREG = 0x8000;
                int mode = (path != null && path.endsWith("/")) ? (S_IFDIR | 0755) : (S_IFREG | 0644);
                bb.putShort((short) mode); // stx_mode

                while (bb.position() < 0x20) bb.put((byte) 0); // 填充对齐

                bb.putLong(123456789L); // stx_ino (inode number)
                bb.putLong(4096L);      // stx_size (文件大小)
                bb.putLong(8L);         // stx_blocks (块数量)

                while (bb.position() < 0x58) bb.put((byte) 0); // 填充对齐

                // 填充时间戳 (statx_timestamp 结构体)
                long now = 1710000000L; // 给一个固定的未来时间戳
                putStatxTs(bb, now); // atime (access time)
                putStatxTs(bb, now); // btime (birth time)
                putStatxTs(bb, now); // ctime (change time)
                putStatxTs(bb, now); // mtime (modify time)

                // 4. 将构建好的ByteBuffer内容,写入到so传入的指针地址
                stx.write(0, bb.array(), 0, bb.limit());
        }
        // 5. 设置返回值为0,表示成功
        writeX(backend, Arm64Const.UC_ARM64_REG_X0, 0);
        return true;
}
5.场景二:系统调用模拟不完善或过于简单

Unidbg 实现了该系统调用,但存在缺陷。

案例 1:clock_gettime (NR=113, ARM 64)
clock_gettime(clockid_t clk_id, struct timespec *tp) 用于获取特定时钟的时间。Unidbg 实现了对 CLOCK_REALTIME (clk_id=0) 的处理,但没有实现 CLOCK_PROCESS_CPUTIME_ID (clk_id=2),导致传入 2 时抛出异常。
解决方案:重写 clock_gettime 方法。

@Override  
protected int clock_gettime(Emulator<?> emulator) {  
    Backend backend = emulator.getBackend();  

    long clkId = readX(backend, Arm64Const.UC_ARM64_REG_X0); // x0 = clk_id  
    Pointer tp = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1); // x1 = timespec*  
    if (tp == null) {  
        // 按 Linux 约定:失败返回 -errno;这里给个通用 EFAULT        
        return -14; // -EFAULT  
    }  
    // 构造各类时钟的返回值(64-bit timespec: tv_sec(8) + tv_nsec(8))  
    long nowMs = System.currentTimeMillis();  
    long nowNs = System.nanoTime();  

    long sec, nsec;  
    switch ((int) clkId) {  
        case 0: // CLOCK_REALTIME  
        case 8: // CLOCK_REALTIME_ALARM  
            sec  = nowMs / 1000L;  
            nsec = (nowMs % 1000L) * 1_000_000L;  
            break;  
        case 1: // CLOCK_MONOTONIC  
        case 4: // CLOCK_MONOTONIC_RAW(有的系统是 4)  
        case 7: // CLOCK_BOOTTIME  
        case 9: // CLOCK_BOOTTIME_ALARM  
            sec  = nowNs / 1_000_000_000L;  
            nsec = nowNs % 1_000_000_000L;  
            break;  
        case 2: // CLOCK_PROCESS_CPUTIME_ID  
        case 3: // CLOCK_THREAD_CPUTIME_ID  
            // 进程/线程 CPU 时间:给一个非零的、单调增长的“小值”即可  
            // 这里简单用 nanoTime 的低位模拟  
            sec = 0L;  
            nsec = (nowNs % 50_000_000L) + 10_000L; // ~0~50ms,避免全 0            break;  
        default:  
            // 未识别的 id:退化成 REALTIME,避免抛异常  
            sec = nowMs / 1000L;  
            nsec = (nowMs % 1000L) * 1_000_000L;  
            break;  
    }  

    tp.setLong(0, sec);  
    tp.setLong(8, nsec);  
    return 0; // 成功  
}

案例 2: sched_getaffinity (NR=123)  

有时候,我们会发现某个系统调用的实现在 Unidbg 的父类 ARM64SyscallHandler 中,但该实现方法被声明为 final,导致我们无法像 clock_gettime 那样直接 @Override 它。此外,有些系统调用是在一个巨大的 switch 语句(如 handleSyscall 方法)中处理的,我们只想修改其中一个 case 的行为,同样无法直接重写。

在这种情况下,我们需要在更早的阶段介入。系统调用的最顶层入口是 hook() 方法,它负责接收所有 SVC 中断,解析出系统调用号(NR),然后再分发给具体的处理方法。通过重写 hook(),我们可以在 Unidbg 分发之前“截胡”我们关心的系统调用,实现自定义逻辑。  

sched_getaffinity 用于检测当前进程可以运行在哪些 CPU 核心上,这常被用于环境检测或设备指纹生成。
解决方案:hook() 方法中,抢先处理目标 NR,然后“屏蔽”它,避免父类重复执行。

@Override
public void hook(Backend backend, int intno, int swi, Object user) {
    // 1. 在顶层入口,首先读取X8寄存器,拿到系统调用号 NR
    int nr = ((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X8)).intValue();

    // 2. 判断是否是我们想“截胡”的系统调用
    if (nr == 123) { // __NR_sched_getaffinity (arm64)
        // 读取参数:X1 = cpusetsize, X2 = mask 地址
        long cpusetsize = ((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X1)).longValue();
        long maskAddr    = ((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X2)).longValue();

        // 3. 实现自定义的模拟逻辑
        if (maskAddr != 0 && cpusetsize > 0) {
            final int cores = 8;                      // 模拟一个8核CPU
            final int size  = (int) cpusetsize;
            byte[] buf = new byte[size];              // 初始化为全0的字节数组
            int maxBits   = size * 8;
            int bitsToSet = Math.min(cores, maxBits); // 计算需要设置的位数
            // 构造CPU亲和度的bitmask (例如8核,就把低8位置为1)
            for (int cpu = 0; cpu < bitsToSet; cpu++) {
                int bi = cpu / 8;
                int bit = cpu % 8;
                buf[bi] |= (1 << bit);
            }
            backend.mem_write(maskAddr, buf); // 将结果写入到指针指向的内存
        }

        // 4. 设置返回值到 X0 寄存器
        backend.reg_write(Arm64Const.UC_ARM64_REG_X0, cpusetsize);

        // 5. 【关键步骤】将X8寄存器设置为一个无效的系统调用号
        //    这是为了防止接下来调用的 super.hook() 再次处理 NR=123,从而覆盖掉我们的结果。
        backend.reg_write(Arm64Const.UC_ARM64_REG_X8, -1);
    }

    // 6. 【必须调用】调用父类的hook方法,以确保PC指针能正确推进,
    //    并且其他不被我们处理的系统调用能够正常执行。否则模拟会卡死。
    super.hook(backend, intno, swi, user);
}

案例 3:fstat (NR=80)

fstatstatx 的核心目标都是获取文件元数据,但它们在设计、功能和使用方式上存在显著差异。简单来说,statxfstat 的现代、功能更全面的“超级升级版”

特性 / 方面 fstat (旧版) statx (现代版)
操作对象 只能作用于一个已经打开的文件描述符 (fd) 更灵活,既可以作用于文件描述符,也可以直接作用于文件路径
信息丰富度 返回 struct stat 结构体,信息相对基础。 返回 struct statx 结构体,信息更丰富,可以提供文件创建时间 (btime) 等。
时间戳精度 传统上只精确到 精确到纳秒 (nanosecond),更能满足现代高精度需求。
执行效率 All-or-Nothing (全量获取)。每次调用,内核都会填充所有字段,不管你是否需要。 On-Demand (按需获取)。可以通过 mask 参数精确指定需要哪些信息,内核按需返回,效率更高。
扩展性 struct stat 结构体已非常拥挤,难以扩展。 struct statx 设计上留有余地,方便未来添加新属性。
// 在 unidbg 源码 unidbg-android\src\main\java\com\github\unidbg\linux\file\DirectoryFileIO.java有相关实现
@Override  
public int fstat(Emulator<?> emulator, StatStructure stat) {  
    stat.st_mode = IO.S_IFDIR;  
    stat.st_dev = 0;  
    stat.st_size = 0;  
    stat.st_blksize = 0;  
    stat.st_ino = 0;  
    stat.pack();  
    return 0;  
}

对照着实现,并伪造一些模拟数据

if (nr == 80) { // __NR_fstat (arm64)  
    // 1. 获取参数:fd在x0, stat结构体指针在x1  
    int fd = ((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X0)).intValue();  
    UnidbgPointer statbuf = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1);  

    // 2. 判断这个fd是否是我们关心的目录类型  
    if (this.fdMap.get(fd) instanceof DirectoryFileIO) {  
        System.out.println("Hooked fstat(80) for directory fd=" + fd);  

        // 3. 使用Unidbg的StatStructure帮助类来填充数据  
        // 这比自己算偏移量和手动写ByteBuffer要简单得多  
        StatStructure stat = new Stat64(statbuf);  

        // 4. 伪造数据 
        stat.st_mode = S_IFDIR | 0755; // 模式:目录 + 权限  
        stat.st_dev = 1;  
        stat.st_size = 4096;  
        stat.st_blksize = 4096;  
        stat.st_ino = 12345;  
        stat.setSt_atim(1668267277L, 999999999L);  

        // 5. 调用pack()将以上设置好的字段,按照内存布局写入指针  
        stat.pack();  

        // 6. 设置返回值为0(成功),并屏蔽父类逻辑  
        backend.reg_write(Arm64Const.UC_ARM64_REG_X0, 0);  
        backend.reg_write(Arm64Const.UC_ARM64_REG_X8, -1);  
    }
6.场景三:处理依赖复杂系统调用链的库函数

有些库函数(如 popen)的实现依赖一整套复杂的系统调用(vfork, pipe2, dup2 / dup3, execve, wait4 等),这些都和进程创建、IPC(进程间通信)相关,是 unidbg 的弱项。尝试逐个修复这些系统调用会陷入泥潭。
因此,我们采用一种更高级、更高效的“组合拳”策略:在高层 Hook 函数意图,在底层伪造结果。  

案例:修复 popen
popen 函数会执行一个 shell 命令,并创建一个管道,返回一个文件流指针,后续代码可以通过 fread 等函数从这个文件流中读取命令的输出结果。  

**第一步:使用 xHook 拦截 popen 调用意图

使用 unidbg 的 Hook 功能(如 xhook),通过 xHook 拦截对 popen 的调用,主要目的不是替换它,而是获取它将要执行的命令字符串,并将其存入 Unidbg 的上下文中,供后续的底层 SyscallHandler 使用。
result.png   

private void hook_popen() {  
    IxHook xHook = XHookImpl.getInstance(emulator);  
    // 监控目标so对popen的调用
    xHook.register("libszstone.so", "popen", new ReplaceCallback() {  
        @Override  
        public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {  
            RegisterContext registerContext = emulator.getContext();  
            String command = registerContext.getPointerArg(0).getString(0);  
            // 将捕获到的命令存入上下文,键为 "command"
            emulator.set("command", command);  
            System.out.println("command:"+command);  
            // 让原始的 popen 函数继续执行,我们只“偷听”,不干扰
            return HookStatus.RET(emulator, originFunction);  
        }  
    }, true);  
    // 使其生效  
    xHook.refresh();  
}
// 在调用目标函数前,先执行 hook_popen ();
challenge.hook_popen();

第二步:在 SyscallHandler 中伪造关键系统调用的结果
当原始的 popen 函数执行时,它会触发一系列系统调用。我们不需要全部实现它们,只需要伪造其中最关键的几个,形成一个逻辑闭环即可。
1. 伪造 pipe2 (NR=59) - 创建通信管道
popen 首先会调用 pipe2 来创建一个管道,这个管道包含一个“读”端和一个“写”端。这是我们注入伪造结果的最佳时机。

@Override
protected int pipe2(Emulator<?> emulator) {
    // AArch64下,第一个参数通过 X0 寄存器传递,这里是指向 int pipefd[2] 的指针
    UnidbgPointer pipefd = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X0);

    // 分配两个新的文件描述符(fd)
    int write_fd = getMinFd();
    int read_fd = getMinFd();

    this.fdMap.put(write_fd, new DumpFileIO(write_fd));

    // 从上下文中取出刚才hook_popen时存入的命令
    String cmd = (String) emulator.get("command");
    if (cmd == null) cmd = "stat /data"; // 兜底

    // 【核心】根据命令,准备好要伪造的输出内容
    String stdout = "\n";
    if ("stat /data".equals(cmd)) {  
        stdout =  
            "  File: /data\n" +  
                    "  Size: 4096     Blocks: 16      IO Blocks: 512 directory\n" +  
                    "Device: 10305h/66309d    Inode: 2        Links: 53\n" +  
                    "Access: (0771/drwxrwx--x)       Uid: ( 1000/  system)   Gid: ( 1000/  system)\n" +  
                    "Access: 2022-04-22 16:08:42.656423789 +0800\n" +  
                    "Modify: 1970-02-05 00:02:38.459999996 +0800\n" +  
                    "Change: 1971-12-21 21:33:28.769999994 +0800\n";  
    }

    // 将伪造的输出内容包装成一个ByteArrayFileIO,并与“读”端fd关联
    this.fdMap.put(read_fd, new ByteArrayFileIO(0, "pipe2_read_side", stdout.getBytes()));

    // 【关键细节】将两个fd写回so传入的指针地址。
    // 即使在64位系统,pipefd参数也是一个 int[2] 数组,每个int占4字节。
    pipefd.setInt(0, read_fd);   // 写入读端fd
    pipefd.setInt(4, write_fd);  // 写入写端fd

    // Unidbg会自动将此方法的返回值(0)写入X0寄存器,表示调用成功
    return 0;
}

2. 伪造 fork - 创建子进程

popen 接着会调用 fork 来创建子进程。在 Unidbg 中,我们无需真的创建一个进程。fork 系统调用的特点是:在父进程中返回子进程的 PID(一个正整数),在子进程中返回 0。由于 popen 的后续流程在父进程中,我们只需要让它以为子进程创建成功了即可。  

// In MySyscallHandler.java
private int fakePid = 4242; // 维护一个假的PID

@Override
protected long fork(Emulator<?> emulator) {
    // 直接返回一个递增的、非零的假PID
    // 这会让调用fork的父进程逻辑认为子进程已成功创建
    return ++fakePid;
}

通过以上组合拳,popen 的执行流程被我们完美地“欺骗”了:
1.它调用 pipe2,得到了两个文件描述符。
2.它调用 fork,得到了一个看似成功的子进程 PID。
3.接下来,父进程会关闭管道的写端,然后从读端 read_fd 读取数据。
4.当它读取 read_fd 时,实际上读取的是我们预设在 ByteArrayFileIO 中的 stdout 字符串。
最终,so 获取到了我们想让它获取的任何结果,而我们完全没有涉及复杂的多进程模拟。

二. 补库函数

案例 2:补 gettid
gettid 函数详解
在 Linux 内核中,每个线程都有一个唯一的标识符,即线程 ID(TID)。gettid 函数是应用程序获取这个底层 ID 最直接的方式。
1. 它是做什么的?
gettid 是一个 Linux 特有的 C 函数,原型如下:

#include <sys/types.h>
pid_t gettid(void);
  • 功能:函数不接受任何参数,直接返回调用它的那个线程的 ID(TID)。
  • 返回值
    • 返回一个 pid_t 类型(通常是 int)的线程ID。
    • 在 Linux 中有一个非常重要的特性:对于一个进程的主线程,其 TID 总是与该进程的 PID(Process ID)相等
  • Java 层对应:当一个 App 的 Java 代码调用 android.os.Process.myTid() 时,其底层最终就会调用到这个 Native 函数。

2. 为什么它在补环境中如此重要?
gettid 是风控和反调试检测的“常客”,因为它能揭示程序运行的线程环境,而 Unidbg 的线程模型与真实设备有本质区别。检测点通常包括:

  • 主线程检测:程序会同时调用 getpid()gettid(),如果发现两者返回值不相等,就可以断定当前代码运行在一个子线程中。某些核心初始化或安全校验逻辑会强制要求必须在主线程执行。
  • 环境一致性:在 Unidbg 的单线程模拟下,gettid() 的返回值可能是一个固定的、不符合真实内核分配规律的值。检测逻辑可以通过多次调用 gettid(),或者将其与 getpid() 比较,来识破模拟环境。
  • 线程识别:某些 so 会使用 TID 作为日志、加解密或内部数据结构的唯一标识。一个错误的 TID 可能导致程序逻辑错乱或崩溃。

如果不修补 gettid,Unidbg 环境下的返回值很可能无法通过上述校验,导致 so 认为自己运行在异常线程或模拟器中,从而改变执行路径或直接退出。

3. 修补方法
对于 gettid,最直接有效的修补方式就是使用 Dobby 进行 Inline Hook,强制它返回一个我们期望的值。通常,我们会让它返回当前的进程 ID(PID),以完美模拟“主线程”的行为。  

// 修补方法:使用Dobby直接Hook原生C函数
private static void hookLibcGettidWithDobby(Emulator<?> emulator, int forcedTid) {
    // 第1步:定位gettid函数所在的模块 libc.so
    Module libc = emulator.getMemory().findModule("libc.so");
    if (libc == null) {
        throw new IllegalStateException("libc.so not found");
    }

    // 第2步:在模块中查找函数符号(Symbol),即函数的地址
    // 注意:不同Android版本,函数名可能是 gettid 或 __gettid
    Symbol sym = libc.findSymbolByName("gettid", false);
    if (sym == null) sym = libc.findSymbolByName("__gettid", false);
    if (sym == null) {
        throw new IllegalStateException("gettid/__gettid symbol not found in libc.so");
    }

    // 第3步:获取Dobby实例,并调用 replace 方法执行Hook
    Dobby dobby = Dobby.getInstance(emulator);
    dobby.replace(sym, new ReplaceCallback() {
        @Override
        public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
            // C函数原型: pid_t gettid(void);
            // 函数无参数,我们直接伪造返回值即可。

            // forcedTid 可以是 emulator.getPid() 或任何你期望的值
            int fakeTid = forcedTid; 
            System.out.println("[HOOK] gettid(): Faking return value to -> " + fakeTid);

            // 【核心】使用HookStatus.LR()直接返回,不再执行原始函数。
            // AArch64架构下,函数返回值在X0寄存器。
            // Dobby会将 fakeTid 写入X0寄存器,并立即返回到调用者。
            return HookStatus.LR(emulator, (long) (fakeTid & 0xffffffffL));
        }
    });
}

// 在主流程中调用:
// 通常我们会让TID等于PID来模拟主线程
hookLibcGettidWithDobby(emulator, emulator.getPid());

小结:
深入探讨了 Unidbg 中三种高级 Hook 技术以应对复杂的模拟挑战。首先,介绍了针对不同场景(如 final 方法)的系统调用 Hook 策略,通过重写顶层 hook() 方法实现精准拦截。演示了通过高层函数 Hook(xHook popen)与底层 Syscall 伪造(pipe2, fork)相结合的“组合拳”策略,高效模拟复杂调用链。最后,展示了如何应用 Dobby 直接修补 C 库函数(gettid)以绕过环境一致性检测。

放点gemini-2.5-pro的key给表哥们玩玩

AIzaSyDeYcYUxAN52DNrgZeFNcEfceVMoWJDjWk
AIzaSyDLMJARxMYqUt4Tl8ADjPPPB1m672tTVmY
AIzaSyAWY438PG4XacWuZU8nQtQ0popo9YAVB18
AIzaSyBouea_CZifzNGN_vC72TtyxYwQLR42tpM
AIzaSyBJtRKb0W0cg1f3Tv65yU1NaYAZ3Hiqn3g
AIzaSyBYPYP-SgvrqsdVrfgOFwvPSGsIFdRLlRs
AIzaSyDN1EtiR1ef6LG4xjtSiiDPNPkkJMh0auQ
AIzaSyCNbdDb9FsvXA8B2Gn_qzhQSxmuTHo_ifA
AIzaSyBJEAGhQB1Wou6yzHe1QCQPv2GaHgsdeTA
AIzaSyDgpkOpIGCuZif3BU9lcrrFOkY-onE263E
AIzaSyBmOVAdUB54vGOHroYOJ7OtC06YrFDOST0
AIzaSyAymb7h68s42zt6xY7jmJMBUjqPiMyU27M
AIzaSyCSHZuRBP1RPGcirYaCopJFi9FSM7le6Zc
AIzaSyBLlTKQaxeXm7DWa5Q1mmSIHBpHyWRdwMA
AIzaSyCxt7zx-cUzvkuroP1uaSp_m0SkoFU_j4A
AIzaSyBnOqAk-VfAjdFpbkveT2yJnNmFXv9-7Mw
AIzaSyDtsLIwVZgT5T8tqfYN2PIQKlflAk-90NI
AIzaSyCvXozHylyHSEFYAW-SZ1QXLyQeKJnt-ac
AIzaSyCy_5IaKck7-MrnJRYLjyV3n2FeCJ9FDGs
AIzaSyBTxaEzGGLVrskzx7UK374NgV4BXn-4nqs
AIzaSyCXSEStgw39a9znv6ATo8mRQZ0eRI5T-n0
AIzaSyA_tSR5vUDWRSIIXFdsOJtnSsueC496njQ
AIzaSyBl56gfbyjTY5qI1mzTP9-uK_Jg7c2ckaY
AIzaSyCQxdrjSaHUfAYikI64Co_mux09RRtYLe0
AIzaSyCl25sgM6WAbqYmY_QTqlMnYQCQxQEQQO8
AIzaSyBjuyyFRTJ5b4XOSAVosrfZShx1K-e6FyE
AIzaSyChOZyn4FgZm41gyg2VEValfDV47IIuw3Q
AIzaSyCg_axGIeK7mMj_XeYtozTQdj4iREPufSo
AIzaSyCmje3P-kGPwqE5hENYujKLKjIievNvmvM
AIzaSyDeQ2QYCW9bu32DX9nmuNrctgKC2oTyZW8
AIzaSyBySkCG5yLgtz4IANJySl5Y59Xxt9pdVWI
AIzaSyCrDCA1le2EY2tSMUu-JTXEusSnXFaXv6k
AIzaSyAevOZ8wu6LmrS6XQDH-S1prUb4Zs-WOqk
AIzaSyDwdVZuLn4W9Xz4YGNtqRcaKXFrl6HE3dA
AIzaSyDx5bncjiKOnbQ_1mz8j6kgwHlKGRWxQIo
AIzaSyDj14MIi4L7pCxXIxMfJVFTTaZ5h_stxdI
AIzaSyCxrE6u0KsbiAR6vLrBnC5PPqG05wl-bTA
AIzaSyDX3U9UEgus4Gdx7960ABTn7Ll3LYMxaF4
AIzaSyDEDxFwPbMXPv7GUb1sQduNLCgbpgvU8_I
AIzaSyCCGyjLLZNZnSm0OZOMtxQWe--ZOnsfe_4
AIzaSyDtDKmMytfIq9Ld5VUIOffIoOY7yXxWEv4
AIzaSyCuz7VK0DU7EtdmMHhNidccJfU36j6CADs
AIzaSyCV9dD4V90z7Sf_fWenXEb83ne0Iav5i3o
AIzaSyAqb4ZV4vCfhrfvinj_rGs3lZOP1--zyqc
AIzaSyCo1NhsZBJg5Yloi5HcrdeMnkNGrSloJ9E
AIzaSyCL9cN9Y3-qQbS5wmr9gxk3J9ZICWHsyiQ
AIzaSyDBacTP1FYjR61hHBUNLCEtLAyk5vqFPiI
AIzaSyCV077Q-1nwxMbPM_3f4Z1h1b7ZHjIBE78
AIzaSyDuExne7oG9NnfNZeaqDOSXVtxUdau7IBU
AIzaSyBHXLYc4hrlVFafKd3hsvqniK1Nytw6pDI
AIzaSyDRizynqIPgiZfgrMdqcosDOWxsTGNtwEM
AIzaSyCIjgG29HPVa7_db5NMrQL5O4oBgW18S5o
AIzaSyDa0wwQKBtfwVxP8wmZSTpq2hFxV0QeCY8
AIzaSyAT2aZC-EMqdjYaz2VgBWCtNFp8rE2If34
AIzaSyAvXG6KKI65N1jNOwIRwEQNoyWbXpdDcsg
AIzaSyAEWasCSVrPKoBXH8SEwFYMu6rbKjTFPcQ
AIzaSyAMWdJ5n3Ro5FdnQNJzw0wrUmNYBw0wzKU
AIzaSyDJnRYm3-t06ks4zrBFyEglV6qJyFvn8Qo
AIzaSyDfNTj4zJWiaXbzwvur812s9DPb5CE9y2w
AIzaSyBqopZ5Q_RKyzYYMI-LmcbSaJwONy16TzU
AIzaSyDR49YtJPhn0QJB8Jh6FZM4gSliW9W47xg
AIzaSyBMT1vW_L-VEqZdF_fMCowgtNbzIQduqMI
AIzaSyDsMf87ZAzDFUZHBnc1HQXmidOGDu8anhk
AIzaSyDweH2jQe7RLYDYUUfcJlAVxgcOmLkd3is
AIzaSyDzAOJND8nS3c7Mwzc0rU330eHucH9c-8E
AIzaSyBU-hzfR-sgVp_-OSb3YcPmUwbUOApUIPE
AIzaSyAXVwlY7urRPxPMlkE_T8Z-4hIc_dzgGE0
AIzaSyDRpJORsoLZMRG60l_68TEzH5b3jd6DGZ4
AIzaSyCBHcP4Seer76nBjvy9vOciNxHumZg7eFE
AIzaSyDqH5QxZxhb8SpQ-_LEmhTteXmzYmpSHgE
AIzaSyDHlry6Ma_CLUVyBGOH7ph0WZLKRG2ccbg
AIzaSyAq2b17eIBC4UJpyateDA3fwRgGe2uC6Ko
AIzaSyCfgPZn0kZZPmk5eaaVvlkR-SrGH2rXH4k
AIzaSyBoiXy-lnXJhOOF0YxMeFFbn7F4oOXtTIc
AIzaSyCHCrl4O31xv4msRwR9B52dpghDPmpKwKM
AIzaSyCQddEzfQidcNdtuDbq8SSlO6K3Ds-ayOc
AIzaSyADe2YtszUhEcTR546CNlc3UVssE7PStMU
AIzaSyCD8uwm3Ye7tBS6uFxOM6mao1sqssf_SHw
AIzaSyDlBv9Br45qcfbzGyr3AlcScyWQo3eSOPU
AIzaSyAu_PNVM03Pu1o380ZI_TKU61FSDeDV1Ks
AIzaSyCDz4gjhidaJvdzPjYHxdo1o0CAqiqll9k
AIzaSyDL12z-qYxT4uAv2v2hwjMHUe8iXcK0ETU
AIzaSyDtwjdZFSAW1pJSnAj1kURTN8kGeFWmATU
AIzaSyAUKqVhKd51nSHwZUxQrYTGqPsr31IdyCY
AIzaSyAUfL_KrYf9rY599fGi2v63IR2Z_PmMMcw
AIzaSyAxpV4OY_zVDst3jGaYNwnyHhFmZ0V4XJE
AIzaSyBoJIB7xIHrxVCw6dd8Zv_eJHSe67lmoQM
AIzaSyCaxTDfzcE5UqPCiZ5QmDEXsgQkdgLhp1w
AIzaSyDzQChJoC_jnb5xwn9UHkFyePD3qZYRaBU
AIzaSyCuZ0zCypYmrGT5GXopw3JPu3WGLzEMpGU
AIzaSyBMlGsH4vA9gcEimwnOyMTAFcPZ33J3akI
AIzaSyCXzlJRd3krr5e32pXTAS14mfS3a8dDsMA
AIzaSyAMgFD2kmgb7Cri49Z7-sOAFBejb37LWgQ
AIzaSyAQPkvdoV4_nzsaaYTZu-QqU__kV9988JI
AIzaSyASjNCk1oxREML4AFhcx5joggaSyvDVj9Q
AIzaSyBl5lM6Md8_YbDt7o-rmB2SiIyppPT32dU
AIzaSyDo3Y0qZ5HPmGGoTNGl_6xmxbOAE8q3cSY
AIzaSyBGFC9cnNJEC822NAKicHbX4PJsE1PGn4c
AIzaSyDUlDBDxfl7c33vb3ILKLayrQNROqHhUDg
AIzaSyDYlLPKXjcrvyTMl8ANYH_FgvKM1mrIdcM
AIzaSyCtJ4cwA90mL25n2f-aFUfnOP0t9zEgs7A
AIzaSyCX_TzqcMio44YuQnkFe0HAaUQNE5qa4n4
AIzaSyDQFUa1LGkonbd7152FK6roHyLs4FhMsVM
AIzaSyDNwmGY0aTaE0GFAndRe773JmZtoPBO8WU
AIzaSyBhITH3uj_6kHz4cIHQ5p7-M5cuOXvGwW0
AIzaSyBidS2L33bSz73nCywuv64Z-lxIx3IaifE
AIzaSyBe8kjRD-siRLDQh30xVRka5TmrsAZVwYc
AIzaSyDfO3oWskF7Pj99ua_72pZxEDQgfGEl8Fo
AIzaSyAFsWzfDNb-OE9DM7ucb4Nwx2rgr_5Horg
AIzaSyCNSdZWCnONcfrmjYI3STJAEO3TRGXxgbQ
AIzaSyCd5zymLO6nQPtaSBtDZJbIggZbQFeRCvc
AIzaSyAF5WnWgLOWJthnfD6_KpSuD7eI5FdAQBk
AIzaSyAj9qY_NbobH5D7wkF1QjWWJ0Wb-agLEgI
AIzaSyDRY5Iga1IYEZjK0MroKACshZYDSoDa5kQ
AIzaSyDQs4bJr4VHcO5XZMHM5Gg3GKVSi7hifoU
AIzaSyDnhCxgEgCMPtBgYi9evlCDm8NKWHrFbMc
AIzaSyDegIQZAzMZeiVajsStxrgWvGDMkfZ4GUg
AIzaSyAOIVHb7xcAMqx07D4kM45qwHNZeNu0Qas
AIzaSyChcczCZLpZbYlw7J_vcU5LE-pfKUFiS3o
AIzaSyBOUpORGxljVDusUs-fMVpLKfZNh02AnmU
AIzaSyCUfqhUXwgUzkw_t67wF3D4gsRCBgYm-50
AIzaSyAMZ7sJlD2ZlUyqObwLOZgJ9BGneYATG2o
AIzaSyBN5WVkfPSZ4EcqoTy06XfmaRjXrl0DbnU
AIzaSyBAn3hVZfgMUQYIm-o_-U0TaM1lGCCDItw
AIzaSyBhoK80uMGiY-885co3xqdnCOzXBjBC24k
AIzaSyAr8fG-NsCNjVe3QmnS_zMsu9Kaf6jVGa4
AIzaSyCLExLEYHiMK3Hyo3XoJBEug5wvfLdJ-p0
AIzaSyCLx3zZ7xGSMpuE0jc87gdJV7VNQ5DcSZM
AIzaSyCpoWTEbR9ggzR_74bkUUdjir_2Kw8ALm0
AIzaSyBeUkVJzv1yx9bVIQsveMma-dsAi8TcqME
AIzaSyCmS2TuyUXfXRREsiEEgmWeG8Plu__0SqA
AIzaSyDlwNcNuyR-HnUctFz8Bn2uS0ienqJz2zc
AIzaSyBaUKIYRqv4Bj2V-Kqx1C7IJ5zvlRwLO_E
AIzaSyDm4gztlWTcR2SUla8Y9bH3iXZbshuXD_4
AIzaSyDZ93djmNzYIRkFs8qxTJmRPWVaYf_fPxM
AIzaSyDo-2Ubl_SukYv1QoIHPREB0CYy-uFrHJc
AIzaSyCxdHELZeZykp4jGMNp8JGJKGFexRjc-i8
AIzaSyBJsFQon7QDK7QRslWWBGPxhbuUJBcWB0k
AIzaSyCF4NKD4O01ktWWoRPWluyx9fD2MaFg8iU
AIzaSyCKEpwjAEcy979dinuoTSsCD4eMB8JaHLA
AIzaSyBkunWP4l4SVzCSbaP_oVNHrXO9OI_R33k
AIzaSyCLiVBiLWYKH04qVhd8Dj45mmNaG-s8Bzc
AIzaSyBkm3UaXYRKLKwmXyFsjOfNQtG4slPAtMM
AIzaSyCXPk-S3qNYYU7f960YP1XIwKQMIUigWAE
AIzaSyDoYoM0xCbpbnkzmEOUzrOQKE2I3tGEiPw
AIzaSyCWDo6tT1HFlsXT50RPzFn3glMhzTXVHDg
AIzaSyDJzAJF422SyZ99Z6CGihnJqKIDlQn4CfM
AIzaSyCjH6NCAmwcrqxaWEhxeSaMqKc9A10Osec
AIzaSyBjTvXHlFStQbzCOg88jjbe1cAaxlaE9M8
AIzaSyBugkmHyJf32u7vlRtU3VviO3mE2RZGVQQ
AIzaSyALDOFJnoigN_k0lFRydMnnfJJjHJcDfiI
AIzaSyDCGHHwwKmlDYtLdXLiEnxzNfSKw6ewSbY
AIzaSyDNOtokPHTUm9WCJ1pOPaweUp_Rks9DhjI
AIzaSyCv4H5U4WJ7DDC4O-2N-sBSnH9GPMwurqk
AIzaSyBAKQdzi4bjkeHz_xnpVaUp0JbSgGB5mWc
AIzaSyCnTROIII2xrUW6rpzDn_WdRFhiLKuze5I
AIzaSyBE-m0-Np0mqdwHrGg1XNue4cz5qC3ecDA
AIzaSyBU41uwxx-G-Mkff20r0OzJWfOVI35rZwQ
AIzaSyDw79xgZTh2lYEPMF2JPs_RqUrteKz1eZU
AIzaSyBjatV-0puUSHYY-PYbUVyj3GuYJYV918g
AIzaSyAlrRP3TQWTaI_essPz0oaNQAhXeq8SaOw
AIzaSyBBdjoUVKVJZb1Ovv-6WbQD7XYIRg3BSKo
AIzaSyAcWQruU56ZLIvQQpMDoHBkRjBjnY4Ja2I
AIzaSyAduQyY6xC4Nma4uPvHwve2fxKNXIYe2dQ
AIzaSyBbhTEJfa9ArGgKKnjUOV3XL60UMpL_gd4
AIzaSyCqTYLyCG37Y2S8s_8NxY0RJD9zj-JgSGI
AIzaSyAAvmxbVy8qcLotR34MB36klygYgNGnS2g
AIzaSyDlyjDbO-HdJyLXVrpzd8R1zqd_KE8Mq4E
AIzaSyAzhXIpf500NeZdfl44Qjh5k7epNaa8WS8
AIzaSyAvuhTgURgvzTRIp51CzIggks-top10DRs
AIzaSyAUgrPIOR1Kokk1f29rvxhkIZom9mToA_o
AIzaSyBe6EkfS-eVWMOQalm2TpxL6ljGBC2z9dI
AIzaSyBIoS20jBYkfdUnMII-bfVMprmCpwWr9Lg
AIzaSyCW1nB9SIPs0BcAy_rUoR6oT0E1ePKaRuo
AIzaSyDu6pH5nfKPJzzanYwEp2yCsubvl495oyk
AIzaSyCEzmk05J35sn0ADAjbX3PPTrRjbl2Pqn0
AIzaSyANY9CZz7W7VRh8hObUhJMdAWTyLD3ngrk
AIzaSyALWlbXC2RpSrGqzSr7Ubjm_UwSY8W7IbY
AIzaSyCAVsYKBiAL0oXtZ5weAXXRbX7J6GOrJII
AIzaSyAN40P9Ad_3COVtCUTzxKzAmmw9D1vUrZM

完整代码:
ChallengeTenThree:  

package com.zj.wuaipojie.util;  

import com.github.unidbg.AndroidEmulator;  
import com.github.unidbg.Emulator;  
import com.github.unidbg.Module;  
import com.github.unidbg.Symbol;  
import com.github.unidbg.arm.HookStatus;  
import com.github.unidbg.arm.context.RegisterContext;  
import com.github.unidbg.file.FileResult;  
import com.github.unidbg.file.IOResolver;  
import com.github.unidbg.file.linux.AndroidFileIO;  
import com.github.unidbg.hook.HookContext;  
import com.github.unidbg.hook.ReplaceCallback;  
import com.github.unidbg.hook.hookzz.Dobby;  
import com.github.unidbg.hook.xhook.IxHook;  
import com.github.unidbg.linux.android.AndroidARM64Emulator;  
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;  
import com.github.unidbg.linux.android.AndroidResolver;  
import com.github.unidbg.linux.android.XHookImpl;  
import com.github.unidbg.linux.android.dvm.*;  
import com.github.unidbg.linux.file.ByteArrayFileIO;  
import com.github.unidbg.memory.Memory;  
import java.io.File;  
import java.io.FileNotFoundException;  
import java.nio.charset.StandardCharsets;  
import com.github.unidbg.linux.android.dvm.DvmClass;  
import com.github.unidbg.memory.SvcMemory;  
import com.github.unidbg.unix.UnixSyscallHandler;  

public class ChallengeTenThree extends AbstractJni implements IOResolver<AndroidFileIO>{  

    private final AndroidEmulator emulator;  
    private final VM vm;  
    private final Module module;  
    private static final String PACKAGE_NAME = "com.zj.wuaipojie";  
    private static final String APK_PATH = "/data/app/com.zj.wuaipojie-1/base.apk";  
    public ChallengeTenThree() {  
        AndroidEmulatorBuilder builder = new AndroidEmulatorBuilder(true) {  
            @Override  
            public AndroidEmulator build() {  
                return new AndroidARM64Emulator(processName, rootDir, backendFactories) {  
                    @Override  
                    protected UnixSyscallHandler<AndroidFileIO> createSyscallHandler(SvcMemory svcMemory) {  
                        return new MySyscallHandler(svcMemory);  
                    }  
                };  
            }  
        };  
        builder.setProcessName("com.zj.wuaipojie");  
        emulator = builder.build();  
        emulator.getSyscallHandler().setVerbose(true);  
        emulator.getSyscallHandler().addIOResolver(this);  
        final Memory memory = emulator.getMemory();  
        memory.setLibraryResolver(new AndroidResolver(23));  
        vm = emulator.createDalvikVM();  
        vm.setJni(this);  
        //vm.setVerbose(true);  

        File soFile = new File("unidbg-android/src/test/java/com/zj/wuaipojie/util/lib52pojie.so");  
        DalvikModule dm = vm.loadLibrary(soFile, true);  
        module = dm.getModule();  
        hookLibcGettidWithDobby(emulator, 0xFFFF);  
        dm.callJNI_OnLoad(emulator);  
    }  

    public void callUnidbgLevel3(int javaTid) {  
        System.out.println("====== 开始执行 unidbg_level3 函数 ======");  
        // 【新增】将 javaTid 存入上下文,供 MySyscallHandler 中的 gettid 使用  
        emulator.set("javaTid", javaTid);  

        DvmClass securityUtilClass = vm.resolveClass("com/zj/wuaipojie/util/SecurityUtil");  
        StringObject result = securityUtilClass.callStaticJniMethodObject(emulator,  
                "unidbg_level3(I)Ljava/lang/String;", javaTid  
        );  
        System.out.println("====== 函数执行完毕 ======");  
        System.out.println("JNI 函数返回结果: " + result);  

    }  

    public static void main(String[] args) throws FileNotFoundException {  
        ChallengeTenThree challenge = new ChallengeTenThree();  
        challenge.hook_popen();  
        challenge.hook_libc_open();  
        challenge.callUnidbgLevel3(0xFFFF);  
    }  

    //替换该open函数。当它从反模拟检查中检测到特定路径时,它将返回 -1    private void hook_libc_open() {  
        Module libc = emulator.getMemory().findModule("libc.so");  
        Symbol openSymbol = libc.findSymbolByName("open", false);  
        if (openSymbol == null) {  
            throw new IllegalStateException("Failed to find 'open' symbol in libc.so");  
        }  

        Dobby dobby = Dobby.getInstance(emulator);  
        dobby.replace(openSymbol, new ReplaceCallback() {  
            @Override  
            public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {  
                RegisterContext r = emulator.getContext();  
                String pathname = r.getPointerArg(0).getString(0);  

                if ("/proc/self/anti_syscall_test".equals(pathname)) {  
                    System.out.println("[HOOK] Intercepted open(\"/proc/self/anti_syscall_test\"). Returning -1 to defeat writability check.");  
                    return HookStatus.LR(emulator, -1L);  
                }  
                return HookStatus.RET(emulator, originFunction);  
            }  
        });  
    }  

    private static void hookLibcGettidWithDobby(Emulator<?> emulator, int forcedTid) {  
        // 1) 定位 libc.so        Module libc = emulator.getMemory().findModule("libc.so");  
        if (libc == null) {  
            throw new IllegalStateException("libc.so not found");  
        }  

        // 2) 查找符号:不同 bionic 版本可能是 gettid 或 __gettid        Symbol sym = libc.findSymbolByName("gettid", false);  
        if (sym == null) sym = libc.findSymbolByName("__gettid", false);  
        if (sym == null) {  
            throw new IllegalStateException("gettid/__gettid symbol not found in libc.so");  
        }  

        // 3) Dobby inline hook:直接改返回值  
        Dobby dobby = Dobby.getInstance(emulator);  
        final int retTid = forcedTid; // 例如 Java 侧的 Process.myTid() 或 0xFFFF  
        dobby.replace(sym, new ReplaceCallback() {  
            @Override  
            public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {  
                // AArch64 返回寄存器 X0 → 直接用 HookStatus.LR 设定函数返回值并“返回到调用者”  
                return HookStatus.LR(emulator, (long) (retTid & 0xffffffffL));  
            }  
        });  
    }  

    private void hook_popen() {  
        IxHook xHook = XHookImpl.getInstance(emulator);  
        xHook.register("libszstone.so", "popen", new ReplaceCallback() {  
            @Override  
            public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {  
                RegisterContext registerContext = emulator.getContext();  
                String command = registerContext.getPointerArg(0).getString(0);  
                emulator.set("command", command);  
                System.out.println("command:"+command);  
                return HookStatus.RET(emulator, originFunction);  
            }  
        }, true);  
        // 使其生效  
        xHook.refresh();  
    }  

    @Override  
    public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator, String pathname, int oflags) {  
        // 观察访问路径  
        System.out.println("[IOResolver] Intercepted file access -> Path: '" + pathname + "', Flags: 0x" + Integer.toHexString(oflags));  

        // ========= 2) 只读挂载需要的 /proc 文件 =========        switch (pathname) {  

            // 关卡9(1): /proc/self/status 必须非空,且 TracerPid: 0            case "/proc/self/status": {  
                String statusContent =  
                        "Name:\t" + PACKAGE_NAME + "\n" +  
                                "State:\tS (sleeping)\n" +  
                                "Tgid:\t12345\n" +  
                                "Pid:\t12345\n" +  
                                "PPid:\t1\n" +  
                                "Uid:\t1000\t1000\t1000\t1000\n" +  
                                "Gid:\t1000\t1000\t1000\t1000\n" +  
                                "TracerPid:\t0\n";  
                return FileResult.success(new ByteArrayFileIO(oflags, pathname,  
                        statusContent.getBytes(StandardCharsets.UTF_8)));  
            }  

            // 关卡9(2): /proc/cpuinfo 必须非空  
            case "/proc/cpuinfo": {  
                String cpuinfo =  
                        "Processor\t: ARMv8 Processor rev 1 (v8l)\n" +  
                                "BogoMIPS\t: 38.40\n" +  
                                "Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32\n" +  
                                "CPU implementer\t: 0x41\n" +  
                                "CPU architecture\t: 8\n" +  
                                "CPU variant\t: 0x0\n" +  
                                "CPU part\t: 0xd03\n" +  
                                "CPU revision\t: 1\n";  
                return FileResult.success(new ByteArrayFileIO(oflags, pathname,  
                        cpuinfo.getBytes(StandardCharsets.UTF_8)));  
            }  

            // 关卡9(3): /proc/self/maps 非空,且最好包含 libc.so(更稳)  
            case "/proc/self/maps": {  
                String mapsContent =  
                        "12000000-12100000 r-xp 00000000 00:00 0 /system/lib64/libc.so\n" +  // 关键:含 libc.so                                "70000000-7001f000 r-xp 00000000 00:00 0 /system/lib64/libart.so\n" +  
                                "7fbe852000-7fbe853000 r--p 00000000 00:00 0 " + APK_PATH + "\n";  
                return FileResult.success(new ByteArrayFileIO(oflags, pathname,  
                        mapsContent.getBytes(StandardCharsets.UTF_8)));  
            }  

        }  
        // 其他交给默认逻辑  
        return null;  
    }  
}

MySyscallHandler:  

package com.zj.wuaipojie.util;  

import com.github.unidbg.Emulator;  
import com.github.unidbg.arm.backend.Backend;  
import com.github.unidbg.file.linux.AndroidFileIO;  
import com.github.unidbg.file.linux.StatStructure;  
import com.github.unidbg.linux.ARM64SyscallHandler;  
import com.github.unidbg.linux.file.ByteArrayFileIO;  
import com.github.unidbg.linux.file.DirectoryFileIO;  
import com.github.unidbg.linux.file.DumpFileIO;  
import com.github.unidbg.linux.struct.Stat64;  
import com.github.unidbg.memory.SvcMemory;  
import com.github.unidbg.pointer.UnidbgPointer;  
import com.github.unidbg.unix.UnixEmulator;  
import com.sun.jna.Pointer;  
import unicorn.Arm64Const;  
import java.nio.ByteBuffer;  
import java.nio.ByteOrder;  
import java.util.Random;  

import static com.github.unidbg.unix.IO.S_IFDIR;  

public class MySyscallHandler extends ARM64SyscallHandler {  

    private final Random rng = new Random();  

    public MySyscallHandler(SvcMemory svcMemory) {  
        super(svcMemory);  
        setVerbose(true); // 可按需开启详细日志  
    }  

    /** 辅助:读/写寄存器 */  
    private static long readX(Backend backend, int reg) {  
        Number n = (Number) backend.reg_read(reg);  
        return n.longValue();  
    }  
    private static void writeX(Backend backend, int reg, long value) {  
        backend.reg_write(reg, value);  
    }  

    @Override  
    public void hook(Backend backend, int intno, int swi, Object user) {  
        // 模拟器实例是解决此问题的关键  
        Emulator<?> emulator = (Emulator<?>) user;  
        int nr = ((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X8)).intValue();  
        if (nr == 123) { // __NR_sched_getaffinity (arm64)  
            long cpusetsize = ((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X1)).longValue();  
            long maskAddr    = ((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X2)).longValue();  

            if (maskAddr != 0 && cpusetsize > 0) {  
                final int cores = 8;                      // 模拟 8 核  
                final int size  = (int) cpusetsize;  
                byte[] buf = new byte[size];              // 先全 0                int maxBits   = size * 8;  
                int bitsToSet = Math.min(cores, maxBits);  
                for (int cpu = 0; cpu < bitsToSet; cpu++) {  
                    int bi = cpu / 8;  
                    int bit = cpu % 8;                    // 低位到高位  
                    buf[bi] |= (1 << bit);  
                }  
                backend.mem_write(maskAddr, buf);  
            }  

            // 约定:返回写入的字节数(你之前的逻辑也是这样)  
            backend.reg_write(Arm64Const.UC_ARM64_REG_X0, (long) (cpusetsize & 0xffffffffL));  

            // 关键:把 X8 置为无效号,避免父类再按 123 处理覆盖你写的结果  
            backend.reg_write(Arm64Const.UC_ARM64_REG_X8, -1);  
        } else if (nr == 80) { // __NR_fstat (arm64)  
            // 1. 获取参数:fd在x0, stat结构体指针在x1  
            int fd = ((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X0)).intValue();  
            UnidbgPointer statbuf = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1);  

            // 2. 判断这个fd是否是我们关心的目录类型  
            if (this.fdMap.get(fd) instanceof DirectoryFileIO) {  
                System.out.println("Hooked fstat(80) for directory fd=" + fd);  

                // 3. 使用Unidbg的StatStructure帮助类来填充数据  
                // 这比自己算偏移量和手动写ByteBuffer要简单得多  
                StatStructure stat = new Stat64(statbuf);  

                // 4. 将您在DirectoryFileIO中验证成功的逻辑复制过来  
                stat.st_mode = S_IFDIR | 0755; // 模式:目录 + 权限  
                stat.st_dev = 1;  
                stat.st_size = 4096;  
                stat.st_blksize = 4096;  
                stat.st_ino = 12345;  
                stat.setSt_atim(1668267277L, 999999999L);  

                // 5. 调用pack()将以上设置好的字段,按照内存布局写入指针  
                stat.pack();  

                // 6. 设置返回值为0(成功),并屏蔽父类逻辑  
                backend.reg_write(Arm64Const.UC_ARM64_REG_X0, 0);  
                backend.reg_write(Arm64Const.UC_ARM64_REG_X8, -1);  
            }  
        }  

        // 必须调用父类以推进 PC(否则卡在 SVC 上)  
        // 如果上面的逻辑都没有命中,或者命中了但没有屏蔽(设置X8为-1),则会走默认处理  
        if (((Number) backend.reg_read(Arm64Const.UC_ARM64_REG_X8)).intValue() != -1) {  
            super.hook(backend, intno, swi, user);  
        }  
    }  

    @Override  
    protected int clock_gettime(Emulator<?> emulator) {  
        Backend backend = emulator.getBackend();  
        long clkId = readX(backend, Arm64Const.UC_ARM64_REG_X0); // x0 = clk_id  
        Pointer tp = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1); // x1 = timespec*  
        if (tp == null) {  
            // 按 Linux 约定:失败返回 -errno;这里给个通用 EFAULT            return -14; // -EFAULT  
        }  

        // 构造各类时钟的返回值(64-bit timespec: tv_sec(8) + tv_nsec(8))  
        long nowMs = System.currentTimeMillis();  
        long nowNs = System.nanoTime();  

        long sec, nsec;  
        switch ((int) clkId) {  
            case 0: // CLOCK_REALTIME  
            case 8: // CLOCK_REALTIME_ALARM  
                sec  = nowMs / 1000L;  
                nsec = (nowMs % 1000L) * 1_000_000L;  
                break;  
            case 1: // CLOCK_MONOTONIC  
            case 4: // CLOCK_MONOTONIC_RAW(有的系统是 4)  
            case 7: // CLOCK_BOOTTIME  
            case 9: // CLOCK_BOOTTIME_ALARM  
                sec  =  nowNs / 1_000_000_000L;  
                nsec =  nowNs % 1_000_000_000L;  
                break;  
            case 2: // CLOCK_PROCESS_CPUTIME_ID  
            case 3: // CLOCK_THREAD_CPUTIME_ID  
                // 进程/线程 CPU 时间:给一个非零的、单调增长的“小值”即可  
                // 这里简单用 nanoTime 的低位模拟  
                sec  = 0L;  
                nsec = (nowNs % 50_000_000L) + 10_000L; // ~0~50ms,避免全 0                break;  
            default:  
                // 未识别的 id:退化成 REALTIME,避免抛异常  
                sec  = nowMs / 1000L;  
                nsec = (nowMs % 1000L) * 1_000_000L;  
                break;  
        }  

        tp.setLong(0, sec);  
        tp.setLong(8, nsec);  
        return 0; // 成功  
    }  

    private int fakePid = 4242;  

    @Override  
    protected long fork(Emulator<?> emulator) {  
        return ++fakePid; // 父进程拿到正 pid    }  

    @Override  
    protected int pipe2(Emulator<?> emulator) {  
        // ✅ AArch64 取第1个参数:X0,是 pipefd 指针  
        UnidbgPointer pipefd = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X0);  

        // 写端:做个“黑洞”避免 dup2/dup3 抛 AbstractMethodError        int write = getMinFd();  
        this.fdMap.put(write, new DumpFileIO(write));  

        // 读端:放入我们要返回给 popen/fread 的 stdout 内容  
        int read = getMinFd();  

        // 若你已 hook popen 并 set 了 emulator.set("command", cmd),这里就能拿到  
        String cmd = (String) emulator.get("command");  
        if (cmd == null) cmd = "stat /data"; // 当前关卡固定就是它,兜底  

        String stdout = "\n";  
        if ("stat /data".equals(cmd)) {  
            stdout =  
                    "  File: /data\n" +  
                            "  Size: 4096     Blocks: 16      IO Blocks: 512 directory\n" +  
                            "Device: 10305h/66309d    Inode: 2        Links: 53\n" +  
                            "Access: (0771/drwxrwx--x)       Uid: ( 1000/  system)   Gid: ( 1000/  system)\n" +  
                            "Access: 2022-04-22 16:08:42.656423789 +0800\n" +  
                            "Modify: 1970-02-05 00:02:38.459999996 +0800\n" +  
                            "Change: 1971-12-21 21:33:28.769999994 +0800\n";  
        }  

        this.fdMap.put(read, new ByteArrayFileIO(0, "pipe2_read_side", stdout.getBytes()));  

        // ⚠️ Linux 的 pipefd 是 int[2],即使在 64 位也写 4 字节(int),偏移 0 和 4        pipefd.setInt(0, read);  
        pipefd.setInt(4, write);  

        // ARM64 返回值放 X0,这个方法 return 0 就行(Unidbg 会写回)  
        return 0;  
    }  

    @Override  
    protected boolean handleUnknownSyscall(Emulator<?> emulator, int NR) {  
        System.err.println(">>> MySyscallHandler is processing syscall NR = " + NR);  
        Backend backend = emulator.getBackend();  
        switch (NR) {  

            /** getcpu (NR=168): x0=cpu*, x1=node* */  
            case 168: {  
                Pointer cpuPtr = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X0);  
                Pointer nodePtr = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1);  

                // C++ 代码会多次调用 getcpu,期望 cpu 号码会变化且不为0  
                // 我们用一个随机数来模拟 CPU 核心的切换  
                int currentCpu = rng.nextInt(8); // 模拟8核CPU  

                if (cpuPtr != null) {  
                    cpuPtr.setInt(0, currentCpu);  
                }  
                if (nodePtr != null) {  
                    // NUMA 节点通常为 0                    nodePtr.setInt(0, 0);  
                }  

                // 成功返回 0                writeX(backend, Arm64Const.UC_ARM64_REG_X0, 0);  
                return true;  
            }  

            /** statx (NR=291): x1=path, x4=statx* */  
            case 291: {  
                Pointer pathPtr = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1);  
                Pointer stx = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X4);  
                String path = pathPtr != null ? pathPtr.getString(0) : null;  
                if (stx != null) {  
                    ByteBuffer bb = ByteBuffer.allocate(0x100).order(ByteOrder.LITTLE_ENDIAN);  
                    bb.putInt(0x000007ff);  // stx_mask: BASIC_STATS  
                    bb.putInt(4096);        // stx_blksize  
                    bb.putLong(0);          // stx_attributes  
                    bb.putInt(1);           // stx_nlink  
                    bb.putInt(1000);        // stx_uid  
                    bb.putInt(1000);        // stx_gid  
                    int S_IFDIR = 0x4000, S_IFREG = 0x8000;  
                    int mode = (path != null && path.endsWith("/")) ? (S_IFDIR | 0755) : (S_IFREG | 0644);  
                    bb.putShort((short) mode); // stx_mode  
                    while (bb.position() < 0x20) bb.put((byte) 0);  
                    bb.putLong(123456789L); // stx_ino  
                    bb.putLong(4096L);      // stx_size  
                    bb.putLong(8L);         // stx_blocks  
                    while (bb.position() < 0x58) bb.put((byte) 0);  
                    long now = 1710000000L;  
                    putStatxTs(bb, now); // atime  
                    putStatxTs(bb, now); // btime  
                    putStatxTs(bb, now); // ctime  
                    putStatxTs(bb, now); // mtime  
                    bb.position(0);  
                    stx.write(0, bb.array(), 0, bb.limit());  
                }  
                writeX(backend, Arm64Const.UC_ARM64_REG_X0, 0);  
                return true;  
            }  

        }  
        return super.handleUnknownSyscall(emulator, NR);  
    }  

    private static void putStatxTs(ByteBuffer bb, long sec) {  
        bb.putLong(sec);  
        bb.putInt(123456789);  
        bb.putInt(0);  
    }  

}

四、请作者喝杯咖啡

图片

六、视频及课件地址

百度云
阿里云
哔哩哔哩
教程开源地址
PS: 解压密码都是 52 pj,阿里云由于不能分享压缩包,所以下载 exe 文件,双击自解压

七、其他章节

《安卓逆向这档事》一、模拟器环境搭建
《安卓逆向这档事》二、初识APK文件结构、双开、汉化、基础修改
《安卓逆向这档事》三、初识smail,vip终结者
《安卓逆向这档事》四、恭喜你获得广告&弹窗静默卡
《安卓逆向这档事》五、1000-7=?&动态调试&Log插桩
《安卓逆向这档事》六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向
《安卓逆向这档事》七、Sorry,会Hook真的可以为所欲为-Xposed快速上手(上)模块编写,常用Api
《安卓逆向这档事》八、Sorry,会Hook真的可以为所欲为-xposed快速上手(下)快速hook
《安卓逆向这档事》九、密码学基础、算法自吐、非标准加密对抗
《安卓逆向这档事》十、不是我说,有了IDA还要什么女朋友?
《安卓逆向这档事》十二、大佬帮我分析一下
《安卓逆向这档事》番外实战篇1-某电影视全家桶
《安卓逆向这档事》十三、是时候学习一下Frida一把梭了(上)
《安卓逆向这档事》十四、是时候学习一下Frida一把梭了(中)
《安卓逆向这档事》十五、是时候学习一下Frida一把梭了(下)
《安卓逆向这档事》十六、是时候学习一下Frida一把梭了(终)
《安卓逆向这档事》十七、你的RPCvs佬的RPC
《安卓逆向这档事》番外实战篇2-【2024春节】解题领红包活动,启动!
《安卓逆向这档事》十八、表哥,你也不想你的Frida被检测吧!(上)
《安卓逆向这档事》十九、表哥,你也不想你的Frida被检测吧!(下)
《安卓逆向这档事》二十、抓包学得好,牢饭吃得饱(上)
《安卓逆向这档事》番外实战篇3-拨云见日之浅谈Flutter逆向
《安卓逆向这档事》第二十一课、抓包学得好,牢饭吃得饱(中)
《安卓逆向这档事》第二十二课、抓包学得好,牢饭吃得饱(下)
《安卓逆向这档事》第二十三课、黑盒魔法之Unidbg
《安卓逆向这档事》第二十四课、Unidbg之补完环境我就睡(上)
《安卓逆向这档事》第二十五课、Unidbg之补完环境我就睡(中)

八、参考文档

白龙unidbg教程

免费评分

参与人数 37威望 +1 吾爱币 +61 热心值 +34 收起 理由
fjplfjpl + 1 + 1 用心讨论,共获提升!
junjia215 + 1 + 1 用心讨论,共获提升!
C0l0RS + 1 + 1 用心讨论,共获提升!
itsrainy + 1 + 1 我很赞同!
twodoors + 1 + 1 谢谢@Thanks!
zglsje + 1 + 1 我很赞同!
zlnxlzht + 1 + 1 谢谢@Thanks!
斌斌有礼灬 + 1 + 1 谢谢@Thanks!
何当负途人 + 1 + 1 我很赞同!
breezehan + 1 + 1 谢谢@Thanks!
814182193 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
arctan1 + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
nice12358 + 1 + 1 用心讨论,共获提升!
努力的小七 + 1 + 1 我很赞同!
OrientalGlass + 3 + 1 表哥依旧高产
abaichuan + 1 + 1 谢谢@Thanks!,认真学习,刚开始看作者的安卓逆向
zhanglyl + 1 + 1 我很赞同!
xiaofeng4929 + 1 谢谢@Thanks!
Carinx + 1 + 1 用心讨论,共获提升!
世忘nb + 1 + 1 我很赞同!
sn954321 + 1 + 1 热心回复!
tianluo + 1 + 1 热心回复!
MFC + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
buluo533 + 2 + 1 谢谢@Thanks!
alongzhenggang + 1 + 1 谢谢@Thanks!
Courser + 1 + 1 我很赞同!
风子09 + 1 + 1 谢谢@Thanks!
jaffa + 1 谢谢@Thanks!
OVVO + 2 + 1 import 正己
smallchop + 1 + 1 我很赞同!
ZZ730605 + 1 + 1 我很赞同!
cure888 + 1 + 1 我很赞同!
melooon + 1 + 1 我很赞同!
smater77 + 1 用心讨论,共获提升!
nigelxiao + 2 我很赞同!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

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

qying 发表于 2025-10-24 20:01
WARN [com.github.unidbg.linux.ARM64SyscallHandler] (ARM64SyscallHandler:410) - handleInterrupt intno=2, NR=-130400, svcNumber=0x121, PC=unidbg@0xfffe02a4, LR=RX@0x120ba3f0[libbili.so]0xba3f0, syscall=null
这样报错svcNumber不等于0是jni调用出错了,那请问该怎么解决呢?

点评

你看第二十四课啊  详情 回复 发表于 2025-10-24 23:00
qying 发表于 2025-10-25 15:36
正己 发表于 2025-10-24 23:00
你看第二十四课啊

报的是com.github.unidbg.arm.backend.BackendException出错而不是jni的方法,所以我才问的&#128514;
后来发现是传参类型的问题,修好之后就能正常补环境了
c1026287787 发表于 2025-10-9 10:54
three 发表于 2025-10-9 10:56
厉害的我的兄弟,连续更新了, 学习了,哈哈哦
涛之雨 发表于 2025-10-9 12:14
好的表弟,key已收到

点评

差评,你怎么能只关注key?  详情 回复 发表于 2025-10-9 12:18
 楼主| 正己 发表于 2025-10-9 12:18
涛之雨 发表于 2025-10-9 12:14
好的表弟,key已收到

差评,你怎么能只关注key?
ZZ730605 发表于 2025-10-9 14:59
学习一个。
m-chris 发表于 2025-10-9 18:04
学习收藏打卡
marlborogolo 发表于 2025-10-10 02:20
第一次入來,從來未學習過,多謝用心教學,加油。
offerking 发表于 2025-10-10 07:40
更新多少年了这是
次谐波 发表于 2025-10-10 08:46
好的。这就开始搭建模拟器环境
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-2-1 14:21

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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