* 本文作者:androiddongjian,本文属FreeBuf原创奖励计划,未经许可禁止转载
本文介绍了一款用以提升逆向效率的,可在真机上查看二进制代码运行逻辑信息的小工具。
由于Java世界的特性所致,安卓应用在代码自身保护方面一直乏善可陈。所谓的Java混淆等技术,也不过是一层簿簿的面纱,极易被撕开,毫无秘密可言。所以,当前也没有谁敢拿“面纱”作为唯一保护措施。
既然Java世界无险可守,大家只好纷纷往底层退守,进入所谓的“二进制世界”。在二进制世界里,使用机器码(machinecode)与底层硬件和操作系统直接沟通,可用的技术和技巧变得相当丰富,保护力度也远超Java世界。
与其它系统不大一样,安卓系统的功能多以Java语言接口对外提供。虽然也提供NDK模式,允许使用JNI方式与安卓系统交互,然而毕竟没有使用Java语言那么直接方便。所以很多应用的数字财产——数据和运行逻辑,依旧保留在Java世界。因而,为安全起见,有必要把这些“财产”迁移至二进制世界,各种安卓应用加密或加固方案借此机会如雨后春笋般诞生。
在加固技术上,从整体dex文件到class或method,再到Dalvik指令,加密粒度不断变小,安全性也得以持续提升。把Dalvik指令集等价替换成为私有指令集,运行时解释私有指令,并通过JNI方式执行,这种技术在目前普遍流行。商业上多称之为VMP(虚拟机保护),不过我个人倒觉得若按技术分类,归为模拟器(Emulator)更为合适。
“源码面前,了无秘密”。当然,在逆向工程师眼里,即使只有目标代码,也能探个究竟。所谓攻防一体,简单介绍常见的逆向思路与对抗措施。
首先就是静态分析。使用反汇编工具把目标代码转换为汇编代码,甚至变成可读性更强的C代码。因此,合格的加固方案必须拥有反反汇编特性。清除各种符号(Symbol)、加密字符串等是例行操作。去除ELF文件的Section Headers是一个常用方法,当然遇上会修复SectionHeaders的人,那就从了吧。还有加入“花指令”也是个不错的方法,只是在专有的反汇编工具面前会真的沦为“花瓶”。故而,加壳是对抗静态分析的杀手锏。
然而,丑媳妇总得见公婆,壳再好,运行时也得卸下,露出核心代码的真容。通过动态调试,逐一审查每条机器指令,再深的秘密也无法藏匿。所以,对抗动态分析也必不可少。
动态调试的技术原理和其运行时暴露的各种特征,是制定反动态调试措施的依据。常见的有获取当前应用是否处于调试连接状态;查看是否存在IDA驻留程序;相应端口号是否被占用;ptrace功能是否启用,或者提前使用ptrace“占位”;设置“哨兵”进程;某段代码运行时长是否异常等等,不一而足。这些措施犹如一个个碉堡,大大延缓了进攻者的前进步伐。
可是,这些“碉堡”迟早要被一个个拔掉。为了进一步迟滞进攻者的推进速度,还可以布下巨大的“八卦阵”——控制流平坦化(ControlFlow Flattening)。这项技术让简单的执行逻辑复杂N倍。把一些核心操作分散置于其中,可以起到相当不错的保护效果,进攻者很容易深陷于一场“人民战争”的汪洋大海之中。
当然,没有攻不破的堡垒,也没有完美的保护。只要能延缓关键代码被暴露的措施,都是好的保护手段。相反,进攻者则希望快速识别和定位关键代码。时间,成了攻防双方所争夺的焦点。那么,有没有比较好的方法可以加速关键代码的定位呢?
显然,获取程序的全面运行信息是快速定位关键代码的基础。运行信息的粒度大小决定了其作用大小。从用户角度看到的程序行为过于粗犷,其作为全面运行信息的一个子集,偶尔能起辅助参考之用。从底层CPU角度可以获取程序的所有运行信息,可惜皆为指令级别,粒度过于细小,不利于人工分析。唯有函数级别的运行信息粒度大小适中,且忠实反映了程序作者的设计意图,是快速了解程序整体运行逻辑的最佳切入点。
提取程序的函数运行信息不是新鲜事。Linux系统上也有不少工具可以获取函数的动态调用关系,例如Systemtap、Ftracer、gprof,还有GCC的instrumentation功能等等。不过,这些工具或者需要内核支持,或者要有源码,的确不大适合只有二进制代码的逆向工程。当然,利用qemu等模拟器提取函数调用关系在技术上不是不可以,只是感觉像在用牛刀杀鸡。再者,如果一些程序存在反模拟器检测,那么必然得不到真实完整的运行信息。
因此,若能在真机上获得二进制代码的函数调用关系信息,将十分有利于对目标应用的进一步分析。于是,本文作者写了一个小工具,来实现此需求。
这个工具的设计思路也很简单:接管目标代码所有函数的序言(prologue)与尾声(epilogue),然后输出相关信息。此工具在某种程度上也可认为是一款轻量级的准虚拟机。
下面,结合实例展示工具的基本功能。
APK样本地址( http://zhushou.360.cn/detail/index/soft_id/3104614#nogo ),包名:com.financial360.nicaifu,版本号:2.2.9。静态分析就此略过,反正也得不到更多的有用信息,不如直接让它在真机上跑一遍。
图1展示了工具所捕获的运行信息,不同线程对应不同文件。文件名的格式为:包名_进程ID_线程ID。工具支持多进程多线程。由于Java世界也会创建许多线程,为简单起见,这里只展示有二进制代码(亦即so文件里的native代码)参与的线程信息。
图1
先打开主线程,见图2。每一行数据表示进入(+)或退出(-)某个函数,其中数字表示调用深度,方便后续图形化等处理(作者是个懒人,这个坑十有八九是填不了)。库函数没有区分进入或退出。在同一行内给出了函数的参数(位于函数名后的括号内)和返回结果(位于*符号后面)。除了JNI_OnLoad外,其它native代码函数名的格式统一为:偏移@so文件名。当然,也可以选择显示其实际函数名,前提是没有被去除。
图2
从图2可以看到DVM加载so文件(libjiagu104569824.so)后首先调用JNI_OnLoad函数。注意看JNI_OnLoad退出时左侧的行号,说明在整个初始化阶段发生了近十万次函数调用。而其中又多为8534,83f0,8044这几个函数,见图3。各种反动态调试检测和核心初始化操作都被分散于这几个函数调用之间,见图4。图中显示其正在检测TracerPid,反动态调试的例行操作。
图3
图4
这么安排的目的自然是为了增加动态调试的难度。一键运行易掉坑,单步执行又费时费力。如此复杂冗长的调用逻辑估计不是靠人力写就。否则,这位程序员的经历够写上N篇“凌晨三点……”的鸡汤文。编译器Obfuscator-LLVM的控制流平坦化功能应该可以达成此效果。
当部分检测通过后,开始解压并加载主so,然后调用其JNI_OnLoad函数,见图5。工具支持多so文件,包括显式和隐式。由于壳so和主so文件里的libname名称往往一致,为了区分,主so重命名为InnerLib。
图5
图5说明二进制代码正在通过JNI操纵Java世界,并动态注册几个新的JNI函数,为两个“世界”搭建新的桥梁。之后,创建入口函数相同的两个线程,见图6。从图中可知,两个线程操作的参数各异,实际上对应着待解密两个dex文件,不同地址不同大小。
图6
JNI_OnLoad执行完毕,就由Java代码发起调用刚才注册的动态JNI接口,见图7。同样是完成一些必要的初始化工作。
图7
所有准备工作就绪,执行权交还给原来的业务代码。由于Dalvik指令已被替换,所以仍需由C代码来解释,并通过JNI方式执行,见图8。
图8
图9是应用自身创建的线程,由libsmsdk.so文件负责处理一些“清场”措施,例如检查是否ROOT,是否存在两大主流hook框架——Xposed和Cydia Substrate,提取包括cache在内的硬件、软件及手机周围环境等大量数据。由于本文作者太懒,没有过滤这些个人数据,所以暂时无法提供这些运行信息文件的下载。
图9
从这也可以看出,安卓系统对隐私数据的保护存在架构上的缺陷。即使其权限管理日趋细化,实际上还是防君子不防小人,很多数据无需权限也可以通过native代码获取。这种方法目前已成惯例,不管正规商业应用还是恶意软件,都喜欢绕过安卓系统,直接与底层操作系统对话。
把大量安全相关操作置于二进制世界,的确提升了应用的安全性,但同时也使得恶意软件的检测变得更为困难。从技术角度看,除非深度改造操作系统,否则目前根本无法在客户端(用户侧)自动检测到新的恶意样本。
如果工具加上预处理(去冗余信息等)以及图形化等功能,相信可以在短时间内纵览程序运行逻辑全局,为后续逆向操作指引方向。
此工具的优点也是缺点,只能像侦察机在空中侦察。若要“打击”一些关键堡垒——破解译码逻辑、加解密过程、协议等等,还得靠重装部队(动态调试)。二者自然可以配合行动,在关键点处暂停运行,加入动态调试。这一切,都是为了提升逆向效率,争夺时间。
当然,也可以用作他途,譬如与“蜜罐”配合,自动收集、检测、分析各类恶意应用。
PS:
请允许在此打个小广告,近期打算换个环境,如有合适职位提供,请与我联系,谢谢!
* 本文作者:androiddongjian,本文属FreeBuf原创奖励计划,未经许可禁止转载