欢迎光临
我们一直在努力

英雄联盟辅助分析

不同的样本具有不同程度的混淆,这里简单的基于微码去掉太复杂的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添加或删除硬件断点

读写实现

  • 通过 PID 找到 task_struct 和 mm_struct。
  • 从目标进程的 mm->pgd 手工遍历页表。
  • 按 PGD → PMD → PTE 将虚拟地址翻译为物理页。
  • 检查 PTE present 位和 pfn_valid()。
  • 按 4 KB 页面分段处理。
  • 读时通过 vmap() 映射页面,再复制到用户缓冲区。
  • 写时通过内核线性映射直接修改物理页。
  • 缺页处理方面:它会检测无效 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,缺页时执行:

  • 将故障地址向下按 0x1000 对齐;
  • 判断该地址是否是合法用户空间地址;
  • 从目标游戏进程读取对应的 4096 字节;
  • 在 Unicorn 相同地址建立 Guest 页面;
  • 把远程页面内容写进去;
  • Hook 返回 true,让 Unicorn 重试刚才失败的指令
  • 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

    赞(0)
    未经允许不得转载:171主机测评 » 英雄联盟辅助分析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址