最近看论坛里很多人在讨论一个日游——jp.gungho.*,特征so是lib__ 6dba__.so。
出于兴趣看了看,发现不是常规的自定义linker。于是花了三天时间把基本的加固流程和保护原理分析的差不多了,发现其中的so完整性检查(可用于anti frida)和mmap模块化调用很有新意,所以整理成文章和大家分享一下。
frida已经成为逆向爱好者必备的工具,各种app对于frida的检测也五花八门,甚至还有什么“某某企业壳frida关了也能给检测出来”这种吓人操作。网上也有大量相关的文章。
可是frida启动时做了什么却很少有人分析。通过阅读frida源码,可以发现frida在启动时留下了大量可供检测的特征。
使用frida的第一步是在目标机器上启动frida-server,即使我们什么app都不hook,只启动一个server。
frida-server一启动,就主动注入了zygote进程:

查看zygote进程的内存布局,在启动frida-server之前:

只启动frida-server,什么都不hook:

可以看到frida 的so已经注入zygote了
frida-server启动注入zygote后,立刻自己hook了一些libc函数
主要有退出相关:exit,_exit,abort

fork相关:

和一些文件操作相关的函数。
frida是使用inline hook对函数进行hook的,inlinehook意味着要写内存,所以在hook前,frida会修改目标函数所在页的页属性为RWX:

在maps文件中,连续内存的不同页属性会被独立列出来:
启动frida-server前,zygote maps文件中libc.so的布局:

启动frida-server后:

可以看到libc.so的内存中多了很多长度为0x1000的rwx片段,这些片段正是frida自己hook 那些libc基本函数导致的。
这两点就是一些“企业壳”所谓在frida就算关了也能检测出来的基本原理。
打开目标so,发现只有一个init函数,没有JNI_OnLoad,同时.text段一大堆0,很明显是个壳子。

于是跟入init函数调试看看。
首先通过解密字符串/proc/self/maps打开maps文件,找到自身so的内存地址:

使用的是自实现的svc函数mmap。这个壳没有使用任何libc函数,所有的字符串操作:strcpy,strcmp,内存操作:memcmp等,都是自己实现,或者直接走svc 0。

这无形中增加了逆向的难度。

不过这个操作没什卵用,更重要的是打开了本地的lib__ 6dba__.so,然后解析elf文件,寻找type为0x80000000的section:

sh_type为0x80000000 - 0xffffffff为用户自定义区间:

可以看到这里存放了大量的加密数据:

然后对这段加密数据进行解密,首先解密出元信息,包括模块的大小,初始函数入口偏移等信息:

如0x257c8是解密的模块长度,0x5d4是解密后入口的偏移。
接下来会mmap一段0x257c8长度的内存,然后将数据解密过去,然后根据偏移跳转到解密出来的代码里执行:

注意,这里解密出来的是一个模块而不仅仅是一个函数。里面包含了几十个函数,所以我们需要将其dump下来分析。
进入解密的代码块中,首先mmap了一块0x1800大小的内存,这块内存用来存储后面mmap出的模块的信息。
每个模块的基本信息如下:
struct module{
int id;
int isrodata;
void* base;
int64 size;
};

首先插入了两个模块,id分别是0xe2和0xd0,其中0xe2模块的内存基址是0x709e426000,大小是0x257c8

而0xe2模块,其实就是2.1中解密出的模块,就是当前模块本身。
这个保护解密出来的模块分为三种类型,最核心的是解释器模块:
解释器模块主要负责循环解密下一个模块。
1.解释器模块首先检查当前执行的模块id是否大于0xe2,如果是的话将id-1的模块移除(在模块列表里删除,同时将对应的内存清空(memset为0)释放(munmap))。

这个操作主要的作用是解释器替换。
因为解释器模块可能mmap解密出新的解释器模块,这样执行新的解释器模块后,会将上一个解释器模块释放掉,用新的解释器模块来解密。
2.循环解密新的模块

3.如果解密出来的模块有初始化函数,调用初始化函数。
主要有两个初始化函数

新的模块入口函数执行后,如果返回1,继续解密下一个模块执行。如果返回0,会跳出循环,然后清除之前的所有模块,然后调用svc exit退出。

注意,解释器模块是不会返回的,因为上一个解释器已经被清掉了,返回会直接crash。
而逻辑模块则必须返回1。如果逻辑模块返回0,则必然会svc exit退出。
逻辑模块主要执行不同的逻辑,有的是安全模块,检查各种环境信息。有的是解密模块。
例如:
没有入口函数,解密出来扔在模块列表里,供其他模块使用。

第一个模块0xe2由原始so的init函数解密出来并执行。
然后0xe2模块解密了
0xe3模块解密出了
0xe4模块解密了
0xe5解密出了
0xe6解密出了
0xe7模块解密出了
0xe8模块:
解密出了0x98模块,该模块执行原始so的init_array函数。然后返回到linker中。
当然还有其他一些模块,这里只列举出比较重要的,一共大概有20多个模块。
为什么返回到linker中了?因为我们从init函数来的,最终所有模块执行完了(如果都成功的话),会返回到linker掉用init函数的地方。
以上所有模块都是mmap在内存中,直接调用入口函数执行。如果没有研究清楚,就会觉得在无限次mmap。
模块在执行中解释器从0xe2-0xe3-0xe4-0xe5-0xe6-0xe7-0xe8,一共换了7次,所以显得非常复杂。但其实基本逻辑是相同的,研究清楚了很好跟进。
对于每个模块,只有plt函数和代码段,没有elf头,动态链接信息,section header等。所以dump下来后需要自己将dump下来的代码自己修复为一个so,然后用ida打开分析。否则所有代码在内存中,并且没有符号,很难分析。
每个模块都自己实现了一套libc基本函数,svc调用和字符串解密函数,所以hook libc函数没什么作用。
这个壳的安全部分就是
0x60模块(root检查)
0x40模块(模拟器检测)
0x20模块(hash校验和libc检查)
0x54模块(反调试检查)
这四个模块。
在 /proc/mount,/proc/pid/mounts文件中检查magisk,同时检查一些su文件和路径(/system/bin/su之类)
主要使用了自定义的strcmp函数检查字符串,svc调用newfstatat检查文件是否存在。

(在调试过程中解密出的字符串被我改了,为了绕过检测)
主要检查模拟器,通过检测文件路径和包名来判断是否存在对应的模拟器。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2025-6-10 00:48
被乐子人编辑
,原因: 排版