一套强劲的 Il2cpp Hook 框架

发布于 2019-09-26  4.35k 次阅读


前言

前一段时间研究了一下Android的Native层Hook, 结果踩了一脚的雷
本文选用现成框架 VirtualApp 来实现Native层注入
对于il2cpp的Hook则采用的是替换类加载器进行针对性的HOOK
注意: VirtualApp 对安卓版本过高的系统不支持!

工具的准备

VirtualApp源码一份: GayHub
AndroidStudio和SDK: AS官网
NDK低版本一份: NDK官方下载

以上工具获取后请先自行编译vapp的源码, 能够正常编译出签名版本即可

通过vapp的接口进行拓展

定位到目标接口

定位到 VirtualApp/lib/src/main/jni/Foundation/IOUniformer.cpp 文件
vapp劫持了所有内部软件的dlopen调用, 留下了一个接口 onSoLoaded 以供开发人员进行其他操作

过滤无效的文件

onSoLoaded 的第一个形参是文件路径, 第二个形参是句柄
只需要判断 文件路径 是否包含我们需要的lib就好了

if (!strstr(name, "libil2cpp.so"))
    return;

通过dlsym定位il2cpp的内置函数

查看Unity项目源码中Class的定义, 会发现几个有意思的函数

namespace il2cpp {
namespace vm{
class Class{
    ...
    static TypeInfo* FromName (Il2CppImage* image, const char* namespaze, const char *name);
    static const MethodInfo* GetMethodFromName (TypeInfo *klass, const char* name, int argsCount);
    static const MethodInfo* GetMethodFromNameFlags (TypeInfo *klass, const char* name, int argsCount, int32_t flags);
    ...
};
} /* namespace vm */
} /* namespace il2cpp */

FromName 函数是用来获取类的镜像 (字节码文件?没学过C#蛋疼T T)
GetMethodFromName 则是用来获取指定的方法地址
可以直接通过 onSoLoaded 接口传入的 句柄 定位这两个函数

void *get_method_addr = dlsym(handle, "il2cpp_class_get_method_from_name");
void *get_class_addr = dlsym(handle, "il2cpp_class_from_name");

GetMethodFromName方法实际上会去调用GetMethodFromNameFlags
而flags参数是用来区分函数重载的各个函数, 默认为0

处理偏移

由于是导出函数, 编译器都会进行优化, 都是抬手就 B指令
导致没法使用Inline Hook, 想要使用Inline Hook去Hook函数, 这个函数就必须拥有两条指令以上才能Hook成功


IDA中的il2cpp_class_get_method_from_name
IDA中的il2cpp_class_get_method_from_name函数
IDA中的il2cpp_class_get_method_from_name_0函数


IDA中的il2cpp_class_from_name
IDA中的il2cpp_class_from_name函数
IDA中的il2cpp_class_from_name_0函数
IDA中的sub_5F41AC8函数


通过手动解析 B指令 地址, 获取到我们Hook的理想位置

inline void* CorrectBTarget(void *symbol){
    return ((char*)symbol + (*(int32_t*)(symbol) << 8 >> 6) + 8);
}
  1. *(int32_t *)(symbol) 获取内存中指令的HEX
  2. << 8 抹除B指令的HEX, 但是内存中的HEX跟IDA中看到HEX是倒过来的, 所以不能用右移
  3. >> 6 初步解析指令的偏移
  4. + 8 人工计算具体偏移. 初步的解析指令偏移并不能一次性到点, 还是得靠人工修正一下
  5. (char*)symbol + 初始的地址加上偏移的地址就是目标的地址

若不止跳转一次, 按照上面的步骤继续解析加人工偏移就好了

inline void* CorrectGetMethodAddr(void *symbol) {
    return ((char*)symbol + (*(int32_t*)(symbol) << 8 >> 6) + (*(int32_t*)((char*)symbol + (*(int32_t*)(symbol) << 8 >> 6) + 12) << 8 >> 6) + 20);
}
inline void* CorrectGetClassAddr(void *symbol) {
    return ((char*)symbol + (*(int32_t*)(symbol) << 8 >> 6) + (*(int32_t *)((char*)symbol + (*(int32_t *)(symbol) << 8 >> 6) + 8) << 8 >> 6) + 16);
}

开始Hook

创建要Hook的函数原型, 还有要替换进去的新函数
要替换进去的函数的参数必须跟原函数一样, 否则会替换失败
那些特殊的 类指针/结构体指针/等等 直接写 void* 就好了

void *(*_Il2cpp_ClassGetMethod)(void *, const char *, int, int32_t);
void *(*_Il2cpp_GetClass)(void *, const char *, const char *);

void *Il2cpp_ClassGetMethod(void *klass, const char *name, int argsCount, int32_t flags)
{
    return _Il2cpp_ClassGetMethod(klass, name, argsCount, flags);
}

void *Il2cpp_GetClass(void *image, const char *namespaze, const char *name)
{
    //LOGE("[namespaze]-> %s [name]-> %s", namespaze, name);
    return _Il2cpp_GetClass(image, namespaze, name);
}

写好函数原型后就可以进行HOOK了, 直接使用vapp集成好的 MSHookFunction 直接HOOK就完事

//GetMethodFromName
MSHookFunction(CorrectGetMethodAddr(get_method_addr),
               (void*)Il2cpp_ClassGetMethod, 
               (void**)&_Il2cpp_ClassGetMethod);
//GetClassFromName
MSHookFunction(CorrectGetClassAddr(get_class_addr),
               (void*)Il2cpp_GetClass, 
               (void**)&_Il2cpp_GetClass);

正常启动一遍vapp, 将目标应用安装进去后启动, 查看log输出 (这里偷懒就莫得图咯)

修改替换进去的函数实现针对性Hook

可以选择判断 名称空间 或者 类名 进行下一步操作
因为后面还有Hook, 不能重复执行, 来一个全局变量限制一下就好

bool (*_AvatarManager_GetIsAutoBattle)(void*);
bool AvatarManager_GetIsAutoBattle(void* self) {return true;}

bool hookLock = false;
void* Il2cpp_GetClass(void* image, const char* namespaze, const char* name) {
    void* result = _Il2cpp_GetClass(image, namespaze, name);
    //限制Hook
    if (isHookAgain || strcmp(namespaze, "MoleMole"))
        return result;
    isHookAgain = true;

    //通过具体的 名称空间 还有 类名, 获取到对应的类
    void* AvatarManager = Il2cpp_GetClass(image, "MoleMole", "AvatarManager");
    //通过指定的类去Hook指定的方法
    hook_Il2cppFunc(AvatarManager, "get_isAutoBattle", 0,
                    AvatarManager_IsAutoBattle, &_AvatarManager_IsAutoBattle);

    return result;
}

注意一下: 由于C#是面向对象的语言, 普通成员函数第一个参数始终是实例自身, 也就是this, C#里写代码的时候并不需要自己加上, 但是Hook的函数原型必须加上, 否则会出大问题

hook_Il2cppFunc 函数对GetMethod的方法进行了封装, 通过 类的image成员方法名, 即可获取到对应的函数地址

template <class t1, class t2>
void hook_Il2cppFunc(void *classImage, const char *funcName, int argsCount, t1 newFunc, t2 origFunc)
{
    if (classImage)
    {
        int32_t *addr = (int32_t *)Il2cpp_ClassGetMethod(classImage, funcName, argsCount, 0);
        if (addr)
        {
            MSHookFunction(*addr, (void*)newFunc, (void**)origFunc);
            LOGD("HOOK SUCCESS >>> [%s]", funcName);
        }
        else
            LOGD("HOOK ERROR!! >>> [%s] ", funcName);
    }
}

argsCount 是函数原型的参数个数, 无需将this算入其内

想要Hook其他方法, 只需要写上对应的 函数原型要Hook进去的新函数函数原型参数个数具体的类image, 并在 Il2cpp_GetClass 方法里面进行Hook, 就ok辣

总结

这套方案只要实现成功后基本上不需要过多的维护
用了一段时间了, 缺点总结两点
1. 遇到人工混淆的函数就嗝屁了
2. 内部类目前还未找到方法实现抓取(螺的岛就用到了大量的内部类)
(之前的文章由于媒体库炸了, 图片全丢失, 所以重新优化了一遍)

相关 教程/工具

unity游戏生成与修改so文件教程
Perfare大大的Il2CppDumper工具
将注入进行到底:利用Mono注入C#游戏脚本


一位被疯狂压榨的底层工作人员