最近看论坛里很多人在讨论一个日游——jp.gungho.*,特征so是lib__ 6dba__.so。
出于兴趣看了看,发现不是常规的自定义linker。于是花了三天时间把基本的加固流程和保护原理分析的差不多了,发现其中的so完整性检查(可用于anti frida)和mmap模块化调用很有新意,所以整理成文章和大家分享一下。
frida已经成为逆向爱好者必备的工具,各种app对于frida的检测也五花八门,甚至还有什么“某某企业壳frida关了也能给检测出来”这种吓人操作。网上也有大量相关的文章。
可是frida启动时做了什么却很少有人分析。通过阅读frida源码,可以发现frida在启动时留下了大量可供检测的特征。
使用frida的第一步是在目标机器上启动frida-server,即使我们什么app都不hook,只启动一个server。
1.1 注入zygote
frida-server一启动,就主动注入了zygote进程:
查看zygote进程的内存布局,在启动frida-server之前:
只启动frida-server,什么都不hook:
可以看到frida 的so已经注入zygote了。
1.2 hook一些必要的libc函数
frida-server启动注入zygote后,立刻自己hook了一些libc函数
主要有退出相关:exit,_exit,abort
fork相关:
和一些文件操作相关的函数。
1.3 修改页属性为RWX
frida是使用inline hook对函数进行hook的,inlinehook意味着要写内存,所以在hook前,frida会修改目标函数所在页的页属性为RWX:
在maps文件中,连续内存的不同页属性会被独立列出来。
启动frida-server前,zygote maps文件中libc.so的布局:
启动frida-server后:
可以看到libc.so的内存中多了很多长度为0x1000的rwx片段,这些片段正是frida自己hook 那些libc基本函数导致的。
1.4 影响
◆由于frida在退出时并没有还原页属性,所以即使关闭frida-server,zygote进程的libc.so内存布局仍然遗留有大量rwx片段。这个特性只有机器重启才会还原。
◆由于app都是有zygote fork出来的,所以启动frida-server之后,无论是否关闭frida,新的app中maps文件的libc.so部分也存在大量rwx片段。
这两点就是一些“企业壳”所谓在frida就算关了也能检测出来的基本原理。
2.lib__ 6dba__.so模块化保护
打开目标so,发现只有一个init函数,没有JNI_OnLoad,同时.text段一大堆0,很明显是个壳子。
于是跟入init函数调试看看。
2.1 自定义的section
首先通过解密字符串/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下来分析。
2.2 mmap 模块化调用
进入解密的代码块中,首先mmap了一块0x1800大小的内存,这块内存用来存储后面mmap出的模块的信息。
每个模块的基本信息如下:
struct module{int id;int isrodata;void* base;int64 size;};
首先插入了两个模块,id分别是0xe2和0xd0,其中0xe2模块的内存基址是0x709e426000,大小是0x257c8
而0xe2模块,其实就是2.1中解密出的模块,就是当前模块本身。
这个保护解密出来的模块分为三种类型,最核心的是解释器模块。
2.2.1解释器模块
解释器模块主要负责循环解密下一个模块。
1.解释器模块首先检查当前执行的模块id是否大于0xe2,如果是的话将id-1的模块移除(在模块列表里删除,同时将对应的内存清空(memset为0)释放(munmap))。
这个操作主要的作用是解释器替换。
因为解释器模块可能mmap解密出新的解释器模块,这样执行新的解释器模块后,会将上一个解释器模块释放掉,用新的解释器模块来解密。
2.循环解密新的模块
3.如果解密出来的模块有初始化函数,调用初始化函数。
主要有两个初始化函数
◆第一个用来将模块列表传递给新的模块。
◆第二个为新模块的逻辑入口。
新的模块入口函数执行后,如果返回1,继续解密下一个模块执行。如果返回0,会跳出循环,然后清除之前的所有模块,然后调用svc exit退出。
注意,解释器模块是不会返回的,因为上一个解释器已经被清掉了,返回会直接crash。
而逻辑模块则必须返回1。如果逻辑模块返回0,则必然会svc exit退出。
2.2.2逻辑模块
逻辑模块主要执行不同的逻辑,有的是安全模块,检查各种环境信息。有的是解密模块。
例如:
◆0x20模块是内存hash检查,如果成功返回1,执行下一个反调试模块0x54。如果检查不通过,则会返回0,然后解释器模块svc exit。
◆0x9b模块负责解密还原真实的so。(注意所有模块都只是壳的一部分)
◆0x98模块调用原始so的init array。
2.2.3 数据模块
没有入口函数,解密出来扔在模块列表里,供其他模块使用。
2.3 模块的执行路径
第一个模块0xe2由原始so的init函数解密出来并执行。
然后0xe2模块解密了
-
0x8f,0x8e两个模块。 -
0xe3模块(解释器)
0xe3模块解密出了
-
0xe4模块(解释器)
0xe4模块解密了
-
0x60模块(root检查) -
0x40模块(模拟器检测) -
0x20模块(hash校验和libc检查) -
0x54模块(反调试检查) -
0xe5模块(解释器)
0xe5解密出了
-
0xA4模块,fork出了一个子进程。 -
0xe6模块(解释器)
0xe6解密出了
-
0x72模块,意义不明,不是安全模块,不影响 -
0xe7模块(解释器)
0xe7模块解密出了
-
0x9b模块,主要使用多线程进行原始so的代码段解密和回填。 -
0xe8模块(解释器)
0xe8模块:
解密出了0x98模块,该模块执行原始so的init_array函数。然后返回到linker中。
当然还有其他一些模块,这里只列举出比较重要的,一共大概有20多个模块。
2.4 小结
为什么返回到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函数没什么作用。
3.关键模块分析
这个壳的安全部分就是
◆0x60模块(root检查)
◆0x40模块(模拟器检测)
◆0x20模块(hash校验和libc检查)
◆0x54模块(反调试检查)
这四个模块。
0x60模块
在 /proc/mount,/proc/pid/mounts文件中检查magisk,同时检查一些su文件和路径(/system/bin/su之类)
主要使用了自定义的strcmp函数检查字符串,svc调用newfstatat检查文件是否存在。
(在调试过程中解密出的字符串被我改了,为了绕过检测)
0x40模块
主要检查模拟器,通过检测文件路径和包名来判断是否存在对应的模拟器。
其中包名检查主要是构造/data/user/0/包名路径,然后svc调用newfstatat检查。
检查的包:
检查的路径:
包括使用__system_property_get()函数,获取一些系统信息:
包括检查ro.hardware,检查是否为goldfish,或者ranchu
0x20模块
主要检查so库的hash和libc
hash校验
首先通过解密函数解密/assets/6dba/data1.dat文件,解密出:
其中分别包含所有so的
◆文件大小
◆文件hash
◆.text段偏移
◆.text段大小
◆.text段hash
◆so名
如上图红框所示,分别是libopenal.so的文件大小:0x14e518,文件hash:0xbc53ce75 ,.text偏移:0x7e30
, .text大小:0x1b7c8, .text 段hash:0x176a9502
该模块会打开本地对应的so,检查文件的hash。如果当前模块是对应的so,同时会进行.text段的hash校验。
hash函数为自定义的,不是常见的crc32:
libc检查
该加固对libc检查的方式非常奇妙,这就用到了开头说的frida启动特征。
正常的libc在maps里的条目是比较少的,通常小于10个:
但是启动过frida的机器,或被frida hook的app,libc的maps里有很多rwx碎片:
该模块首先打开maps文件,查找起始地址大于libc .text起始,并小于libc .text终止的条目个数。
如果大于9个,则认为libc"不正常",然后返回0(解释器svc exit退出)
如果maps里条目数正常,该模块会映射一份本地libc,和内存中的libc的代码段用自定义的memcmp函数比较:
通常来讲都会比较libc代码段的crc32,但是其实直接memcmp也没什么问题。只要内存不一致就说明libc被修改了,没必要算crc.
0x54模块
反调试模块比较常规,就是打开/proc/pid/status 检查TracerPid。
同时还通过libart.so找到了SetJdwpAllowed函数地址:
然后执行了SetJdwpAllowed(false),似乎没什么用。
0x9b 模块
使用多线程解密原始so:
解密之后dump原始so的rx段回填,就可以看到原始so的代码了。
还缺重定位表和init_array信息,这两个拿到了就能修复so了,关于自定义linker部分这里就不细究了。但是应该在0x9b模块里是能拿到的。
这个加固将app里的每一个so都加壳了,每个so都要经过这样的模块化层层处理后,最终才会解密执行。
不同的是,除了lib__ 6dba__.so自身以外,其他的so在解密过程中只执行了0x20模块进行hash校验和libc检查,没有执行其他安全模块。
至于专门针对frida的检测,并没有看到。但是只要检查libc在maps中的个数,就可以完全对抗frida了。甚至frida启动过一次关了都能检查出来,必须重启机器才行。
对于模块化保护,需要熟悉elf结构,自定义libc函数,svc调用,无头无动态链接纯代码还原成so静态分析,自定义linker等,分析起来难度还是相当大的。
当然,最重要的是耐心,他的模块化执行路径我也是调试了几十次才最终搞清楚。总的来说这是一款有难度的安卓保护,mmap模块化匿名执行和maps检查antiFrida都是很新的思路。
https://bbs.kanxue.com/user-home-872365.htm
原文始发于微信公众号(看雪学苑):神奇日游保护分析——从Frida的启动说起
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论