
一、课程目标
[ol]
[/ol]
二、工具
1.教程 Demo
2.IDEA
3.IDA
三、课程内容
一. 补文件访问
一、文件访问补环境的核心概念
文件访问 (File Access) 补环境是 Unidbg 应用中仅次于 JNI 补环境的重要环节。当 Unidbg 模拟执行的原生库(. So 文件)尝试通过文件 I/O 操作访问文件系统时——例如读取设备信息 (/proc/cpuinfo)、检测运行环境 (/proc/self/maps、/xbin/su) 或校验自身完整性 (APK 文件)——Unidbg 必须能够对这些访问请求作出响应。在一个纯净的 Unidbg 环境中,这些被访问的目标文件通常是不存在的。如果一个关键的文件访问失败或返回了不符合预期的内容,原生库可能会改变其执行逻辑、触发风控机制或直接中断执行。因此,文件访问补环境的目的就是通过 Unidbg 的 IOResolver 责任链机制拦截这些文件系统调用,并提供一个内容、权限都符合原生库逻辑预期的虚拟文件或目录。
二、核心机制:责任链模式与 IOResolver
Unidbg 并未将所有文件处理逻辑写死,而是采用了一种灵活的责任链模式。当代码尝试访问一个文件时,请求会依次通过一个处理器链,直到被成功处理或最终失败。
这个处理链的顺序通常是:
[ol]
这样的设计使得我们可以注入自定义逻辑来处理文件访问,而无需修改 Unidbg 源码,保证了项目的可移植性和环境的整洁性。

mermaid-diagram-2025-10-03-204232.png (359.27 KB, 下载次数: 1)
下载附件
2025-10-3 20:44 上传
[/ol]
添加自定义文件处理器:
你可以通过 emulator.getSyscallHandler().addIOResolver() 来添加一个文件处理器。这个操作必须在加载 SO 库(loadLibrary)之前完成才能生效。
// BiliIOResolver.java - 独立的处理器类
public class BiliIOResolver implements IOResolver {
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
// 打印所有文件访问请求,无论是否处理
System.out.println("File open request: " + pathname);
// 在这里添加具体的文件处理逻辑
return null; // 返回null表示未处理,交由下一个处理器
}
}
// Main.java - 在主类中添加处理器
public class Bili extends AbstractJni {
public Bili() {
// ...模拟器初始化代码...
emulator.getSyscallHandler().addIOResolver(new BiliIOResolver());
// ...加载SO和后续操作...
}
}
三、文件处理的三种返回结果
在 resolve 方法中,你可以返回一个 FileResult 对象,它有三种状态:
四、常规文件处理与 FileIO 对象
处理文件访问需要结合三方面的知识:Linux/Android 文件系统知识、对业务(风控/检测)的经验以及 Unidbg 的实现方法。
1. 使用 SimpleFileIO 处理物理文件
当需要返回一个真实存在的文件时,SimpleFileIO 是最佳选择。你需要将文件从真机上导出,并放置在你的项目路径下。
案例:补 boot_id 和 APK 文件
boot_id 常用于生成设备指纹,而 APK 文件访问常用于签名校验或资源读取,两者都必须处理。
boot_id 文件在开机时生成,在设备关机前不会改变内容。我们将这个文件从真机上 push 出来
[/ol]
C:\Users\zhengji>adb shell
* daemon not running; starting now at tcp:5037
* daemon started successfully
vermeer:/ $ cat /proc/sys/kernel/random/boot_id
c7de54b2-f238-481d-b8e1-41c05413b2cd
vermeer:/ $ cp /proc/sys/kernel/random/boot_id /sdcard
vermeer:/ $ exit
C:\Users\zhengji>adb pull /sdcard/boot_id D:\unidbg-master\unidbg-android\src\test\resources
/sdcard/boot_id: 1 file pulled, 0 skipped. 0.0 MB/s (37 bytes in 0.012s)
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
switch (pathname) {
// 注意:这里的路径需要和样本访问的完全一致
case "/proc/sys/kernel/random/boot_id":{
return FileResult.success(new SimpleFileIO(oflags, new File("unidbg-android/src/test/resources/cpu/boot_id"), pathname));
}
}
return null;
}
2. 使用 ByteArrayFileIO 动态生成文件内容
当文件内容需要动态生成或随机化时(例如躲避基于设备指纹的风控),ByteArrayFileIO 非常有用。它直接接收一个字节数组作为文件内容。
案例:随机化 boot_id 和补 CPU 频率
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
switch (pathname) {
case "/proc/sys/kernel/random/boot_id": {
// 动态生成UUID作为boot_id
String randomBootId = UUID.randomUUID().toString();
return FileResult.success(new ByteArrayFileIO(oflags, pathname, randomBootId.getBytes(StandardCharsets.UTF_8)));
}
case "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq": {
// 直接返回一个字符串作为文件内容
return FileResult.success(new ByteArrayFileIO(oflags, pathname, "1766400".getBytes()));
}
}
return null;
}
注意:ByteArrayFileIO 不支持写入操作,如果样本需要写入文件,使用它会抛出 UnsupportedOperationException。
4. 使用 DirectoryFileIO 和虚拟文件系统处理目录
4. 处理失败的文件访问
多数情况下,对于环境检测类的文件(如 su 文件),我们什么都不做,让它自然失败即可。但在某些特殊场景,需要手动返回失败。
案例:模拟无 Root 权限
有些 Root 检测会尝试访问 /data 目录的权限。由于 Unidbg 的虚拟文件系统会自动创建 /data 目录导致访问成功,我们需要手动拦截并返回“权限不足”。
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if ("/data".equals(pathname)) {
return FileResult.failed(UnixEmulator.EACCES); // EACCES = 13, Permission denied
}
return null;
}
五、关键点
[table]
[tr]
[td]文件路径 (Path)[/td]
[td]主要用途 (Purpose)[/td]
[td]推荐处理策略 (Recommended Strategy)[/td]
[td]注意事项 (Key Points)[/td]
[/tr]
[tr]
[td]/proc/self/cmdline