购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.2 Hook快速定位方案

在逆向分析的过程中,笔者一直推崇“Hook——主动调用——RPC”的三段式理论。展开来说,在协议分析的第一步,首先通过Hook的方式确定关键业务逻辑位置,然后通过主动调用实现关键业务逻辑的调用,最后通过RPC远程过程调用的方式进行关键业务逻辑的批量调用,以期达到后续利用的目的。在Hook的过程中,如何在大量的代码中快速定位关键业务逻辑的位置,减少逆向人员的工作量是一大重点,Frida的出现正是将Hook工作成功地从Xposed模块每次编译都需重启的循环中解放出来的契机,其即时生效的特点大大减少了逆向人员的时间成本,加快了逆向的进度。当然,Frida在快速逆向中的作用不止于此,本节将介绍基于Frida的两种更加快速的关键逻辑定位方式。

2.2.1 基于Trace枚举的关键类定位方式

相信有一定基础的读者都用过Android Studio中附带的DDMS工具,相较于搜索字符串这种大海捞针的方法,DDMS中的Method Trace功能能够让逆向人员快速得到一段时间内目标App执行过的函数记录,进而快速定位关键业务逻辑函数,而实现DDMS这项功能的就是笔者在这一小节中要介绍的函数Trace。

DDMS是Google官方提供的工具,其本意是帮助开发者分析和测试App中方法的速度与性能,因此其本身要求App的debuggable属性为true,而这是实际逆向分析中几乎不可能遇到的应用;此外,由于DDMS的Trace功能会记录所有的函数(包括App中的函数和系统函数),这样的操作十分占用系统性能,其具体效果总是差强人意,正因如此,Trace方式一直不温不火,但是Frida的出世为函数的Trace提供了另一条出路,Frida不仅无须应用处于debuggable状态,而且支持Trace指定类中的函数,支持Trace特定类中的所有函数,这样的效果大大缩小了函数Trace的范围,对系统性能的要求有了极大的改善。在这一小节中,我们并不直接编写Frida脚本来进行函数的Trace,而是通过介绍一些基于Frida封装的可用于进行Trace定位的工具来介绍基于Trace枚举的关键类定位方式所带来的优势。

首先不得不提的是前文介绍过的Objection工具在Trace中的作用。

在2.1.3节中介绍Objection的常用Hook命令时,曾经介绍过Objection支持Hook一个类中所有函数的功能,实际上笔者在使用Objection的过程中发现Objection还支持通过-c参数对指定文件中的所有命令进行执行的功能,如图2-15所示。

图2-15 Objection Trace功能

同样以“移动TV”样本为例,其包名为com.cz.babySister。在手机上启动frida-server和样本应用后,使用Objection对样本进行注入,并使用如下命令搜索包含包名的类:

# android hooking search classes com.cz.babySister

在获取到如图2-16所示的所有与包名相关的类后,便可以将搜索的类保存为文件,并在每一行行首加上android hooking watch class字符串,以使得每一行组成一条对Java类进行Hook的命令,最终组成的Trace文件的部分内容如图2-17所示。当然,这里在行首添加字符串的方式有很多种,这里通过VS Code编辑器的竖选功能完成内容的补全。

图2-16 包名相关类

图2-17 文件Hook命令

在得到包含Hook命令的文件后,便可以重新通过Objection对应用进行注入,完成对包含包名所有类的Hook工作,最终效果如图2-18所示。而如果此时再去触发我们所关心的业务逻辑,便能够快速筛选出关键业务逻辑所在类的范围,从而完成关键类的定位工作。

图2-18 Objection批量Hook

当然,这里需要注意的是,查看Objection 1.8.4版本的源码会发现其search class搜索类的命令是通过先获取所有已加载类后再筛选的方式来得到最终结果的,其获取所有类的实现如代码清单2-3所示。

代码清单2-3 getClasses

export const getClasses = (): Promise<string[]> => {
  return wrapJavaPerform(() => {
    return Java.enumerateLoadedClassesSync();
  });
};

忽略代码清单2-3中外部封装的部分,会发现其实本质上是使用Frida的API:enumerateLoadedClassesSync()函数完成类的获取。无论是从API名称还是官方的释义中都能够发现这个API只是列出内存中已经加载的类,而不是应用中所有的类。因此,若想获得尽可能完整的类列表,需要尽可能多地使用应用后再执行命令。如图2-19所示是笔者在登录账户前后获取到的包名相关的类列表。

图2-19 搜索相关类对比

如果读者打开Objection相应源码,从-c参数解析处进行分析会发现,该参数只是用于逐一执行文件中的代码而已,真正用于Trace的代码不过是通过getDeclaredMethods()反射相关函数获取特定类中所有函数,并对获得的函数一一Hook而已,其关键Hook核心代码如代码清单2-4所示。

代码清单2-4 Trace核心代码

另外还需要注意的是,如果App本身是加固的应用,在使用Frida对应用进行测试时,要尽量选择attach模式进行应用相关类的Trace/Hook,否则在应用还没启动时,App真实的类仍未被壳从内存中释放并加载,此时去Hook相关类会报如图2-20所示的ClassNotFoundException异常错误。这是由于加固App的ClassLoader在运行时的切换问题所导致的,由于ClassLoader的原理不属于本章讲述的范围,这里不再展开描述,相信有一定基础的读者都明白其中的缘由。

图2-20 ClassNotFoundException异常错误

Objection的Trace功能就暂且介绍到这里。接下来介绍一款基于Frida开发的专门用于Trace的工具——ZenTracer,其项目的地址为https://github.com/hluwa/ZenTracer。

由于ZenTracer是一款基于Frida和PyQt5的工具,因此在运行前先要通过pip安装PyQt5和Frida的依赖包,最终成功启动ZenTracer后,其界面如图2-21所示。

图2-21 ZenTracer主界面

要使用ZenTracer首先要单击图2-21上的Action菜单,选择Match RegEx或者Black RegEx完成类的过滤工作。顾名思义,Match RegEx就是Hook指定的与输入正则匹配的类,而Black RegEx是指不Hook指定的类。这里以Match RegEx功能为例,在输入想要Hook的类前加上M:就可以完成对包含指定pattern的类的Hook工作,比如这里想要Hook所有包含com.cz.babySister的类,其最终输入如图2-22所示。若使用Black RegEx功能,则只需将M:替换为B:即可指定不Hook相应类。

图2-22 Hook包含com.cz.babySister的类

在确定匹配规则后,再次单击Action菜单下的Start选项,ZenTracer就会完成对当前前台App(手机页面上正在显示的应用)中所有符合匹配规则的目标类的Hook工作,最终Trace效果如图2-23所示。

图2-23 ZenTracer Hook结果

观察图2-23,会发现图片下方是Hook的类的相关信息,而图片上方显示的是Hook后执行的函数记录与其参数和返回值信息。为了更好地观察结果与后续分析,ZenTracer还友好地提供了将Hook结果导出为JSON格式文件的功能,只需要依次单击File→Export JSON即可完成文件的导出。

此时再次查看ZenTracer Trace相关代码,会发现其Trace关键代码同样和Objection类似,只是将遍历得到的目标类直接传递给相应的Hook代码而已。相比于Objection需要先导出命令到文件再Trace的方式,ZenTracer更加方便快捷,其关键代码如代码清单2-5所示。

代码清单2-5 ZenTracer Trace关键代码

相比于DDMS,Objection和ZenTracer Trace函数的范围更加灵活,能够依据使用者的想法对执行函数进行跟踪,同时ZenTracer还支持对函数参数和返回值的打印工作,这大大缩减了从Trace结果中获取关键函数的时间成本,为逆向工作提供了更多的便利;另一方面,基于Frida的Objection和ZenTracer无须样本程序处于debuggable状态,减少了对应用程序另外操作的工作成本。当然,由于Frida本身不太稳定以及Trace本身对程序的侵入成本,被测试的程序肯定是会经常崩溃的,此时可以通过切换Android和Frida版本或者换更高性能的手机的方式来减少崩溃的频率。当然,即使按照上述方式提高了稳定性,Frida还是存在崩溃的可能性,但相信用过的读者都会觉得瑕不掩瑜,基于Frida的Objection以及ZenTracer对逆向工作效率的提高都是呈指数级的。

2.2.2 基于内存枚举的关键类定位方式

实际上,基于内存枚举的关键类定位在Xposed时代就出现了:当逆向分析人员通过分析发现某些类可能是关键类时,可以通过对关键类进行Hook去验证分析的结果,只是Xposed的Hook每次都需要对手机进行重启以生效,而Frida则能够更加有效率地去Hook验证分析结果。

当然,笔者认为相比于Xposed而言,Frida的进步不仅仅是提高了Hook的效率,Frida还支持对进程内存的漫游功能,能够通过Java.choose()这个API在目标进程的Java堆中寻找和修改已存在的Java对象实例,同时还能修改对象中属性的值,这样的功能使得逆向人员从只能单纯地通过Hook去获取和修改对象值的局限中释放出来,分析人员不仅能够对未执行的函数设置Hook,同时还能够对已经创建的实例进行操作,这大大拓宽了逆向工作的思路。

接下来以经典的针对OkHttp框架的Hook抓包问题为例进行介绍。

相信对OkHttp原理有一定了解的读者都知道OkHttp的核心其实是拦截器。简单来说,在OkHttp中一个完整的网络请求会被拆分成几个步骤,每个步骤都通过拦截器来完成,可以说通过拦截器就能够完整地得到每次发包和收包的数据。在笔者对OkHttp3的源码分析过程中发现,OkHttp中的拦截器由okhttp3.OkHttpClient类中的List成员_interceptors数组管理,这个数组中包含着对应Client中的所有拦截器。那么是不是意味着我们自己写一个简单的只是打印日志的LogInterceptor并添加到_interceptors数组中就可以完成对所有数据包的抓取呢?答案是肯定的。

笔者在研究过程中通过Java.choose()这个API从内存中搜刮okhttp3.OkHttpClient的实例对象,并修改原对象的_interceptors数组内容,最终使得这个拦截器的List数组中包含我们自定义的拦截器LogInterceptor,从而完成数据的抓包,其效果如图2-24所示。

图2-24 抓包效果

让我们回过头来观察实现修改内存中okhttp3.OkHttpClient对象的代码内容。观察代码清单2-6,发现除了主要的Java.choose() API的使用外,还有一些值得关注的地方。

首先,在代码清单2-6中,Java.openClassFile()这个API用于打开自定义的DEX文件,myok2curl.dex和okhttplogging.dex分别用于完成log日志的打印工作和具体拦截器的实现,在Frida执行脚本前,两个DEX文件已经事先放置于/data/local/tmp目录下并赋予其执行权限。再加上load()函数的使用,将打开的DEX文件加载进内存后,便可以在脚本中加载原本App并不存在的类和函数。

自定义DEX文件的加载主要是为了避免将Java翻译成JavaScript的复杂工作,相反可以直接使用Java语言编写自定义的类并编译成DEX文件供后续使用。

在这个代码中,笔者也实现了使用JavaScript编写自定义的类的方式——Java.registerClass(),观察这部分实现会发现,相比直接使用简单的Java.openClassFile(<dex>).load()函数来说,registerClass函数的实现着实有点隔靴搔痒的感觉。

而剩下的主体代码就是使用Java.choose函数找到对应的OkHttpClient对象,并向_interceptors数组中添加自定义的MyInterceptor拦截器对象。这样的功能毫无疑问Xposed是无法实现的。

代码清单2-6 hookOkHttp3.js

另外,在笔者的研究过程中还使用了一个Objection插件——WallBreaker,其项目地址为https://github.com/hluwa/Wallbreaker。WallBreaker可以用于快速定位一个类中所包含的属性与函数,甚至可以直接通过对象的句柄获取所在类中的所有属性的值。这个功能在笔者研究OkHttp3拦截器机制的过程中提供了巨大的帮助。图2-25是笔者在验证OkHttpClient类的_interceptors成员就是对应Client的拦截器时的截图。

图2-25 OkHttpClient对象解析

对WallBreaker的源码进行分析,会发现这个功能也是利用Java.choose()函数实现的对内存中类对象的搜索,如代码清单2-7所示。后续对对象属性和函数的打印其实是通过对象句柄值进行反射,从而获取相应成员和函数,具体这里不再分析,如果读者对其实现感兴趣,可以自行阅读项目源码。

代码清单2-7 objectsearch功能实现

相比于Xposed而言,Frida在native层的Hook也是颇有建树。逆向人员同样能够通过在native层进行内存枚举完成很多工作。以在Android安全中困扰着众多安全人员的脱壳问题为例,比如笔者在工作过程中经常使用的dexdump项目,其地址为https://github.com/hluwa/FRIDA-DEXDump,dexdump的核心原理是在目标进程的内存空间中遍历搜索包含DEX文件特征(dexdump主要利用文件头是dex03?模式的特征)的数据,并在匹配到符合特征的DEX数据后完成真实DEX文件的dump工作。这个方法的实现主要是利用Frida的Memory.scanSync()函数,dexdump的主要代码如代码清单2-8所示。

代码清单2-8 dexdump核心代码

当然,dexdump仅仅能够解决一代整体加固的脱壳工作,于是又有大牛利用Frida编写了frida_fart用于解决二代抽取加固的脱壳问题,其项目地址为https://github.com/hanbinglengyue/FART/blob/master/frida_fart.zip。frida_fart中存在着两种解决脱壳的方案,其中Hook版本是通过Hook在App运行过程中用于加载和执行DEX文件的ART虚拟机中的native函数——LoadMethod函数,并通过这个函数的参数分别获取加载的Java函数所在的DEX文件和函数的真实内容,其主要代码如代码清单2-9所示。

代码清单2-9 frida_fart_hook.js

观察代码清单2-9会发现,frida_fart针对二代抽取壳的解决方案,本质上使用的是Frida在native层Hook的基础API——Interceptor.attach()。而代码中针对dexFile内存对象和ArtMethod内存对象的解析主要是利用Frida中的Memory读取内存相关的API多内存数据进行读写操作,而这些都是在原生的Xposed中无法想象的工作。 g+cL0mGHHqSoXQuQi/oSKj4+PzbT6l+s7M1v/GVUnLq1uGEQzMwdjfoW3lL0bpKP

点击中间区域
呼出菜单
上一章
目录
下一章
×