不同的样本具有不同程度的混淆,这里简单的基于微码去掉太复杂的icall选择trace。样本运行后会在屏幕上绘制敌方玩家位置。
跨进程读取
样本提供了多种跨进程读取方案
系统调用syscall
标准 syscall 270 → process_vm_readv
内核方案,ko内核和KPM内核

ko内核提供了多个版本,使用sh脚本一键安装,ko模块内嵌在sh脚本中
- 检查当前用户必须是 root:id -u == 0。
- 检查 Telegram 客户端缓存中是否存在指定频道文件,否则终止或打开 https://t.me/RTdrivers。
- 删除临时文件 /data/tmpf。
- 执行: 即删除脚本开头至 #离 标记的全部内容,把后面直接拼接的原始 ELF 模块写入 /data/tmpf。
- 添加权限:
- 加载模块: insmod /data/tmpf
- 加载成功后删除 /data/tmpf,然后运行: dmesg -C 这会清空内核日志,包括模块加载产生的记录。
对这个驱动进行分析:
- 利用 kprobe 找到 kallsyms_lookup_name。
- 定位 sys_call_table。
- 保存原始 sys_ioctl。
- 临时修改页表权限。
- 将 sys_call_table[29] 替换为 hook_ioctl
通讯功能如下
6010×259读取指定进程虚拟内存
6020x25A写入指定进程虚拟内存
6030x25B查询进程模块映射地址/BSS
6040x25C没有实现,转发原始 ioctl
6050x25D将调用进程从 PID 哈希表摘除,隐藏进程
6060x25E恢复原始 syscall 29,并关闭硬件断点系统
6070x25F将调用进程重新加入 PID 哈希表
6080×260获取并清空硬件断点命中记录
6090×261添加或删除硬件断点
读写实现
缺页处理方面:它会检测无效 PTE/PFN,但不会主动触发缺页,也不会换入 swapped-out 页面。目标页面不驻留时直接失败。页表遍历还硬编码了 30/21/12 位移,因此实际只适合 4 KB 页内核
模块地址查询
它遍历目标进程 VMA,通过 d_path() 获取映射路径并进行子串匹配:
- 普通模块名:收集并排序 RX、RO、RW 映射,index 用来选择对应映射。
- 名称带 :bss:寻找模块文件映射之后连续的匿名 RW 区域,返回 BSS 地址。
- index=0 通常返回最低的 RX 映射,即常说的 .so 加载基址。
断链隐藏自身驱动模块
struct list_head *self = &THIS_MODULE->list;
if (self->next && self->prev) {
if (__list_del_entry_valid(self)) {
struct list_head *next = self->next;
struct list_head *prev = self->prev;
next->prev = prev;
prev->next = next;
}
self->next = self;
self->prev = self;
}
删除前:
前一个模块 <──> hookdd <──> 后一个模块
删除后:
前一个模块 <──────────> 后一个模块
hookdd
↺ ↻
其实这些内核模块功能类似,安卓上也不太能对内核做保护,没啥可分析的,也不知道为啥外挂作者喜欢赛一堆垃圾内核模块给用户。哦,可能和大部分安卓外挂作者自身水平有关,记得之前分析某外挂样本,什么云驱动内核,我还以为是什么东西以为和win一样壳加载云下发,结果是syscall vm_read ….
KPM则是使用杂鱼面具或者apatch加载
内核通讯方案,hook某个系统调用做后门和之前分析的差不多,反正安卓没PG随便hook
用户态样本
→ 准备读内存请求结构
→ syscall(18, command, &request)
→ SVC #0
→ 定制内核的 syscall 18 处理函数
→ 内核读取目标进程地址
→ 将数据写入样本的本地缓冲区
→ 返回用户态
跨进程绘制
使用了imgui库底层使用的Vulkan 不是 OpenGL ES 看来外挂也在与时俱进, 创建独立 Surface 图层,以及把世界坐标投影后提交到屏幕 。 把图层放到游戏上方 ,并设置穿透属性不影响玩家正常触摸与游戏交互
游戏进程
│ process_vm_readv
▼
样本
│ 玩家坐标、相机矩阵
▼
世界坐标转屏幕坐标
│
▼
ImGui → Vulkan → ANativeWindow
│ Binder/BufferQueue
▼
SurfaceFlinger
│ 透明图层合成
▼
手机屏幕
ImGui 绘制命令
→ Vulkan CommandBuffer
→ Vulkan Swapchain
→ ANativeWindow
→ SurfaceFlinger 合成到屏幕
网络地址
https://pvp.qq.com/web201605/js/herolist.json 下载英雄资料:
引流QQ 群:群号:打码
wy.llua.cn:80 使用的网络验证域名
英雄详细资料页面按以下形式构造: https://pvp.qq.com/web201605/herodetail/m/{英雄ID或ename}.html
例如程序会把详情页面缓存成: /data/adb/yaziduck_icons/skill/hero_detail_{英雄ID}.html
英雄/皮肤头像资源按代码实际构造为: https://game.gtimg.cn/images/yxzj/img201606/heroimg/{英雄ID}/{皮肤资源ID}.png 这里下载资源为了绘制后能显示对应的信息
unicorn模拟执行解密下发shellcode
该游戏在触发某些风控后会下发匿名内存shellcode对关键数据进行加密,很多样本对下发函数进行hook ret(那实在是太没技术了不如直接直装重打包),也有使用hook拿到解密后的数据,不过比较有含金量的是纯算法解密不动内存。下面讲解这个样本的模拟执行对抗
缺页反模拟执行Unicorn 只能访问已经通过 uc_mem_map() 建立的 Guest 地址。解密函数一旦跟随某个指针访问其他对象,就会产生:
- UC_MEM_READ_UNMAPPED
- UC_MEM_WRITE_UNMAPPED
- UC_MEM_FETCH_UNMAPPED
该样本使用UC_HOOK_MEM_UNMAPPED,缺页时执行:
Unicorn 是用户态 CPU 模拟器,没有 Android 内核。执行 SVC #0 时不能直接完成系统调用,只会触发 UC_HOOK_INTR,所以在shellcode中掺杂系统调用来干扰模拟执行
该样本解法:
捕获 Guest 的 SVC #0
MOV X8, #29
SVC #0
Unicorn 产生中断,读取:
寄存器 ID 260 → PC
寄存器 ID 207 → X8,系统调用号
寄存器 ID 199 → X0,目标进程 FD
寄存器 ID 200 → X1,ioctl request
寄存器 ID 201 → X2,Guest 参数缓冲区
pc = uc_reg_read(PC);
syscall = uc_reg_read(X8);
targetFd = uc_reg_read(X0);
request = uc_reg_read(X1);
guestArg = uc_reg_read(X2);
Guest 的 X0 是游戏进程中的 FD,不能被当前样本直接使用 要复制过来
在宿主执行真实 ioctl
result = syscall(
29, // __NR_ioctl
hostFd,
request,
hostBuffer
);
将驱动输出复制回 Guest , 设置返回值并越过 SVC
最终流程:
Guest SVC #0
→ 读取 X8/X0/X1/X2
→ 确认 X8 == 29
→ _IOC_SIZE(request)
→ Guest 参数复制到 hostBuffer
→ targetFd 经 pidfd_getfd 变成 hostFd
→ syscall(29, hostFd, request, hostBuffer)
→ hostBuffer 写回 Guest
→ 返回值写入 X0
→ PC += 4
→ 继续 Unicorn
游戏核心解密依赖于线程上线文环境
PTRACE_ATTACH → PTRACE_GETREGSET(NT_ARM_TLS) → PTRACE_DETACH通过短暂的ptrace附加拿到 线程的 TLS 上下文
ptrace(PTRACE_ATTACH, 目标线程)
→ PTRACE_GETREGSET(NT_ARM_TLS)
→ 获得该线程 TPIDR_EL0
→ 写入 Unicorn 的 TPIDR_EL0
→ 映射该 TLS 周围需要的内存
→ 开始模拟执行
→ PTRACE_DETACH
游戏使用ARM64 的 TBI(Top Byte Ignore,忽略最高字节)机制 反模拟执行
一个普通地址可能是:
0x0000006FBA9515E0
游戏或 Android 内存分配器可以在最高 8 位存放“标签”:
0xB400006FBA9515E0
^^
标签
CPU访问内存时会忽略最高字节,因此这两个指针实际指向同一位置:
Unicorn 不会自动像手机 CPU 那样忽略最高字节 Unicorn 将它们视为两个不同地址,于是报告 UC_ERR_MAP 样本会去除这个标签让Unicorn正常执行
引擎解析
这里不太好说定位xxx.so然后+偏移的形式 定位到某个存储游戏实例全局变量,然后再加xxx偏移,一直遍历到玩家数组,真实数据就不放了
大概是这个样子
xxxx.so + xxxx
→ root
root + xxxx
→ CPlayerCenter
CPlayerCenter + xxxx
→ 玩家容器
玩家容器 + xxxxx
→ Player
Player + xxxxx
→ ActorRoot
+xxxx MovementComponent 拿坐标
这个游戏本来是unity il2cpp但是被日的太多了,加密魔改il2cpp那套拦不住大手子了,直接自研了一套引擎,当然也可能是其他原因,dddd






