原本打算下个月再水的,不过转念一想,反正也不可能做到每个月都水一篇,而且现在放开以后准备到处去玩了,所以还是赶紧写完了发出来。

偷梁换柱

思路的产生其实还是在上一篇搞去年年底那个刚出的游戏的时候,虽然在模拟器里用NativeBridge载入了arm的so实现了修改,但是马上就发现了一个小问题,这种使用系统函数载入的so会在maps里显示路径,加固只要检测一下maps就能发现这个异常的so。不过因为以前写Riru模块的时候看过部分源码,所以很快就想起了Riru的那个hide函数,先看看那个函数是怎么做的:

  1. 遍历maps取出带有要隐藏的so路径的段
  2. 解析段的起始地址,大小等信息
  3. mmap一块相同大小的backup
  4. memcpy把段复制到backup
  5. munmap
  6. 在相同起始地址上mmap一块匿名映射
  7. memcpy从backup复制回起始地址

不过这跟绕过.text的校验有什么关系呢?首先思考下反作弊是怎么校验.text段的,通常的做法就是:从本地lib中打开要校验的so,然后从maps中找到要校验的so在内存中的位置,分别解析后对比其中的.text段,你会发现它其实也是通过在maps中找so的路径来确认so在内存中的位置。所以回到上面Riru的hide函数,如果我们把要修改的so在maps中隐藏,然后让backup拥有so的路径信息,就能骗过反作弊,让它去检测backup而我们就可以在原本的so内存上为所欲为。而要让backup带上路径信息也很简单,创建的时候通过源文件映射就行。所以稍微修改一下hide函数就能实现了:

bool riru_hide(const std::set<std::string_view> &names) {
    procmaps_iterator *maps = pmparser_parse(-1);
    if (maps == nullptr) {
        LOGE("cannot parse the memory map");
        return false;
    }

    procmaps_struct **data = nullptr;
    size_t data_count = 0;
    procmaps_struct *maps_tmp;
    while ((maps_tmp = pmparser_next(maps)) != nullptr) {
        bool matched = false;
        matched = names.count(maps_tmp->pathname);

        if (!matched) continue;

        auto start = (uintptr_t) maps_tmp->addr_start;
        auto end = (uintptr_t) maps_tmp->addr_end;
        if (maps_tmp->is_r) {
            if (data) {
                data = (procmaps_struct **) realloc(data,
                                                    sizeof(procmaps_struct *) * (data_count + 1));
            } else {
                data = (procmaps_struct **) malloc(sizeof(procmaps_struct *));
            }
            data[data_count] = maps_tmp;
            data_count += 1;
        }
        LOGD("%" PRIxPTR"-%" PRIxPTR" %s %ld %s", start, end, maps_tmp->perm, maps_tmp->offset,
             maps_tmp->pathname);
    }

    auto fd = open(data[0]->pathname, O_RDONLY);
    struct stat sb;
    if (fstat(fd, &sb) == -1)
        LOGE("fstat");
    auto fileLength = sb.st_size;
    size_t copySize = 0;
    for (int i = 0; i < data_count; ++i) {
        auto procstruct = data[i];
        auto start = (uintptr_t) procstruct->addr_start;
        auto end = (uintptr_t) procstruct->addr_end;
        auto length = end - start;
        copySize += length;
    }
    LOGI("file length : %jd", fileLength);
    LOGI("copySize : %zu", copySize);
    auto backup_address = (uintptr_t) FAILURE_RETURN(
            mmap(nullptr, copySize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0),
            MAP_FAILED);
    close(fd);

    for (int i = 0; i < data_count; ++i) {
        auto procstruct = data[i];
        auto start = (uintptr_t) procstruct->addr_start;
        auto end = (uintptr_t) procstruct->addr_end;
        auto length = end - start;
        int prot = get_prot(procstruct);

        // backup
        LOGD("%" PRIxPTR"-%" PRIxPTR" %s %ld %s is backup to %" PRIxPTR, start, end,
             procstruct->perm, procstruct->offset, procstruct->pathname, backup_address);

        if (!procstruct->is_r) {
            LOGD("mprotect +r");
            FAILURE_RETURN(mprotect((void *) start, length, prot | PROT_READ), -1);
        }
        LOGD("memcpy -> backup");
        memcpy((void *) backup_address, (void *) start, length);

        // munmap original
        LOGD("munmap original");
        FAILURE_RETURN(munmap((void *) start, length), -1);

        // restore
        LOGD("mmap original");
        FAILURE_RETURN(mmap((void *) start, length, prot, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0),
                       MAP_FAILED);
        LOGD("mprotect +w");
        FAILURE_RETURN(mprotect((void *) start, length, prot | PROT_WRITE), -1);
        LOGD("memcpy -> original");
        memcpy((void *) start, (void *) backup_address, length);
        if (!procstruct->is_w) {
            LOGD("mprotect -w");
            FAILURE_RETURN(mprotect((void *) start, length, prot), -1);
        }

        LOGD("mprotect backup");
        FAILURE_RETURN(mprotect((void *) backup_address, length, prot), -1);
        backup_address += length;
    }

    if (data) free(data);
    pmparser_free(maps);
    return true;
}

完整流程就是,hook dlopen,等待要修改的so加载后,调用riru_hide,完成偷梁换柱。

拿了个之前用其他方法搞定的带.text校验的游戏测试,成功绕过,证明了这个思路至少在部分游戏上是可行的。

在模拟器上使用Zygisk修改游戏

前言 没想到距离上次发一篇教程性质的文章已经过去整整一年了,刚好最近搞了标题里提到的东西觉得还是可以写一下的,于是就来水了这一篇。 之前我一直...

阅读全文

使用Zygisk和Il2Cpp Api来修改游戏吧

在两年前发布Riru版Il2CppDumper的时候,就打算发布这篇配套的文章,结果码了一半就被我丢到了一旁,后来每次想起来的时候总是以”既然代码都发布了大概也不...

阅读全文

使用Riru修改手游

前篇 手游的注入与修改 使用VirtualXposed修改手游 前言 时隔半年终于把这坑填上了,一开始只是打算随便写写,不过现在看来刚好可以凑出一个系列,之前的文章...

阅读全文

12 条评论

  1. void hack_start(const char *game_data_dir) {
    bool load = false;
    for (int i = 0; i < 10; i++) {
    void *handle = xdl_open("libil2cpp.so", 0);
    if (handle) {
    std::set names = {“libil2cpp.so”};
    riru_hide(names);
    dalao,我这样调用有错么,添加hide后,游戏直接闪退了
    05-30 17:10:50.461 4023 4031 I Perfare : nb 0xe9899
    05-30 17:10:50.461 4023 4031 I Perfare : NativeBridgeLoadLibrary 0xcd5e7270
    05-30 17:10:50.461 4023 4031 I Perfare : NativeBridgeLoadLibraryExt 0xcd5e7860
    05-30 17:10:50.461 4023 4031 I Perfare : NativeBridgeGetTrampoline 0xcd5e72b0
    05-30 17:10:50.461 4023 4031 I Perfare : arm path /proc/self/fd/96
    05-30 17:10:50.462 4023 4031 I Perfare : arm handle : 0xe30f9180
    05-30 17:10:50.462 4023 4031 I Perfare : JNI_OnLoad 0xedd8a320

  2. 可以脱离riru吗各位,脱离ritu怎么实现,riru_hide 传递什么参数 :?: 期待一个实例demo

  3. procmaps_iterator* pmparser_parse(int pid);
    procmaps_struct* pmparser_next(procmaps_iterator* p_procmaps_it);
    void pmparser_free(procmaps_iterator* p_procmaps_it);

    在pmparser.h里并没有写它们的具体实现,仅仅只是声明,riru源码里也没有其他相关代码,那riru_hide是怎么跑起来的,能否解答下

    android studio 一直报错ld: error: undefined symbol: pmparser_parse 这几个

欢迎留言

7 + 8 =