Android Hook机制与实践详解(一)

上期回顾:java

     上一篇,分享了Android平台的抓包分析方法和技巧(固然不限于Android,其余移动平台也都适用),让咱们从网络通讯层面,了解样本的客户端与服务端如何通讯,进而判断是否存在隐私窃取的行为,是否符合《网络安全法》,以及《App 违法违规收集使用我的信息行为》的规定等等,这是一个必须掌握的技能。可是,若是咱们想知道程序某个函数的功能,可是函数加密了,加密算法又比较复杂或者加壳技术也比较难,鉴于时间成本,并不能硬碰硬,就得思考有没有办法不破解算法,还能获得想要的结果,答案是:有的,本篇文章目的就是解决工做中,遇到算法难以破解,或者没法下手时,换一种思路来达到解决问题的目的,这里咱们称它为Android Hook机制与实践详解。android


在研究Hook机制与实践相关知识以前,还有一个smali代码注入问题,有必要先了解一下,这个应用范畴与Hook很类似,也常常会用到,常言道“无论黑猫白猫,能解决问题的就是好猫”。咱们都知道Android安装包是一个ZIP压缩包,核心代码在压缩包中的classes.dex和动态库so文件,而smali代码就是classes.dex文件通过baksmali反编译后生成的代码。git


回想一下,一个完整的Android程序反编译后代码量仍是比较大,而且反编译后smali代码相比java代码可读性更差,有的人会问,那为何还要去读smali代码,直接读java代码不就好了,局部代码固然能够,可是,通常状况下,反编译生成的smali代码比java代码相对要全。那怎么办?经常使用的方法就是关键信息查找法+修改代码法(也叫smali注入法)。好比:阿里聚安全曾经举办的安全竞赛,要求必定时间内,破解其算法,获得答案。github

这种状况,通常代码中会加入不少无效的代码,目的就是为了干扰破解者,或者,咱们想知道一个函数的参数,一个函数的返回值等,均可以在代码中添加log语句,让程序运行时,自动打印相关参数。
算法


smali代码注入


smali代码注入,意思就是在反编译后生成的smali代码中插入咱们须要的smali代码,让其运行后,打印出须要的结果,以免逆向分析大量的代码,或者破解难度比较大的算法,这算一种投机取巧的办法,有点胜之不武,哎呀,兵不厌诈嘛。shell

使用方法:swift

步骤1:java -jar baksmali.jar classes.dex -o  smaliapi

步骤2: 采用关键信息查找法,找到须要插入代码的地方,加入咱们本身的smali代码;
安全

步骤3:java -jar smali.jar smali  -o  classes.dexbash

步骤4:   运行获得结果。

这个方法,编写smali对应的代码是问题关键,下面是一些经常使用的语句,供参考:

# 最经常使用增长log信息const-string v1, "TAG"invoke-static {v1,v2} ,Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I // V2是要输出的字符串# 打印函数参数const-string v1, "TAG"new-instance v3, Ljava/lang/StringBuilder;const-string v4, "XXXX"invoke-direct {v3, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)Vinvoke-virtual {v3, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;move-result-object v3invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;move-result-object v3invoke-static {v1, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
# 封装经常使用的注入代码到文件中,文件名为 MyCrack.smali(自定义).class public LMyCrack;.super Ljava/lang/Object;.source "MyCrack.java".method public static log(Ljava/lang/String;)V .locals 1    .prologue    const-string v0, "info" invoke-static {v0, p0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I return-void.end method
.method public static I(I)V .locals 2    .prologue    const-string v0, "info_int"    invoke-static {p0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;    move-result-object v1    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I return-void.end method
.method public static J(J)V .locals 2    .prologue    const-string v0, "info_long" invoke-static {p0, p1}, Ljava/lang/String;->valueOf(J)Ljava/lang/String; move-result-object v1 invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I return-void.end method
.method public static puts(Ljava/lang/String;)V .locals 7 .prologue :try_start_0 const-string v3, "/sdcard/debug.txt" new-instance v2, Ljava/io/FileOutputStream; const/4 v5, 0x1 invoke-direct {v2, v3, v5}, Ljava/io/FileOutputStream;-><init>(Ljava/lang/String;Z)V .line 19 new-instance v4, Ljava/io/OutputStreamWriter; const-string v5, "gb2312" invoke-direct {v4, v2, v5}, Ljava/io/OutputStreamWriter;-><init>(Ljava/io/OutputStream;Ljava/lang/String;)V .line 21 invoke-virtual {v4, p0}, Ljava/io/OutputStreamWriter;->write(Ljava/lang/String;)V const-string v5, "\r\n" invoke-virtual {v4, v5}, Ljava/io/OutputStreamWriter;->write(Ljava/lang/String;)V .line 23 invoke-virtual {v4}, Ljava/io/OutputStreamWriter;->flush()V .line 25 invoke-virtual {v4}, Ljava/io/OutputStreamWriter;->close()V .line 27 invoke-virtual {v2}, Ljava/io/FileOutputStream;->close()V :try_end_0 .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 .line 37 :cond_0 :goto_0 return-void .line 30 :catch_0 move-exception v0 .line 34 const-string v5, "debug" const-string v6, "file write error" invoke-static {v5, v6}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I goto :goto_0.end method
# 把上面的MyCrack.smali放到反编译后的smali根目录,在源代码中注入调用代码,格式以下
# 加入Log信息,保存了字符串类型的寄存器vx,添加代码:invoke-static {vx}, LMyCrack;->log(Ljava/lang/String;)V# 加入Log信息,保存了int类型的寄存器vx,添加代码:invoke-static {vx}, Lcrack;->I(I)V# 加入Log信息,保存了long类型的寄存器vx,添加代码:# [注意:vx为要查看的寄存器,同时确保vx+1在上面的代码中没有被使用过,且在.locals声明的可以使用的寄存器范围内。]invoke-static {vx, vx+1}, Lcrack;->J(J)V# 加入Log信息,将字符串输出到文本,添加代码:# [注意:字符串“\r\n”]invoke-static {vx}, Lcrack;->puts(Ljava/lang/String;)V
# 还有一种办法就是写一段java代码编译程Android程序,而后再反编译生成smali代码使用。

注意:smali注入方法使用也是有局限性的,首先前提是必须apk文件能够重打包,也就是当咱们插入相关代码后,必须可以重打包成完整的apk文件,安装后还要可以正常运行,不然就失败了,咱们就得换种思路来解决,这就到本篇文章提到到Hook,好了下面咱们来看Hook。


preload Hook原理


preload Hook 在PC平台、Linux平台使用的比较多,像inline hook,got hook,这里咱们只提Linux平台和Android有关系的内容,还记得以前文章提到过,ptrace有一个很重要的特色就是一个进程只能被一个进程ptrace,若是你本身调用ptarce,这样其它程序就没法经过 ptrace调试或者向你的程序进程注入代码,以达到反调试的效果。


在Linux中,LD_PRELOAD是一个环境变量,它能够影响程序运行时的动态连接,它容许你在程序运行前优先加载提早定义的动态连接库。这个功能主要就是用来有选择性的载入不一样动态连接库中的相同函数。经过这个环境变量,咱们能够在主程序和其动态连接库的中间加载别的动态连接库,甚至覆盖正常的函数库。根据LD_PRELOAD的这个特性,就能够编写Hook函数的动态连接库,提早放入到LD_PRELOAD环境变量中,实现对原始函数的替换。


以ptrace函数为例,编写动态连接库libpreload.so文件,在其中定义一个ptrace函数,实现对原始函数的替换。ptrace函数原型:int ptrace(intrequest, int pid, int addr, int data)

long ptrace(int request, pid_t pid, void *addr, void *data){ LOGE("my ptrace request:%d pid:%d\n", request, pid); if( request == 16 && pid == current_pid ) {        LOGE("hook__request is PTRACE_ATTACH and pid == fork()\n"); return -1;    }    else    { LOGE("hook___request is not PTRACE_ATTACH\n"); long (*original_ptrace)(int request, pid_t pid, void *addr, void *data); original_ptrace = dlsym(RTLD_NEXT, "ptrace");        return (*original_ptrace)(request, pid, addr, data); } return 0;}

加入export LD_PRELOAD="/path/libpreload.so",将libpreload.so放入LD_PRELOAD环境变量中,实现对ptrace函数的Hook。

更多preload Hook 知识参考:https://www.jianshu.com/p/f78b16bd8905


Android平台,不须要这么复杂,咱们能够利用开源框架xposed,Cydia Substrate,Frida实现对指定函数的Hook,已达到解决咱们遇到问题的目的。


基于xposed框架Hook实践


Xposed框架是一套开源的框架服务,能够在不修改apk文件的状况下影响程序运行,经过替换system/bin/App_process程序控制Zygote进程,使得App_process在启动过程当中加载XposedBridge.jar包,从而完成对Zygote进程及其建立的Dalivk虚拟机的劫持。

Xposed框架的实现基于一个Android本地服务应用Xposed Installer与一个提供API的XposedBridgeApi-54.jar文件。所以,在安装和使用Xposed框架以前,首先须要安装XposedInstall.apk本地服务应用,在获取ROOT权限以后,点击“安装/更新”激活框架,激活成功的界面如图1 所示。

图1 XposedInstaller激活成功界面

案例实践

步骤1:建立工程android2.3.3(其余版本也能够),能够不用activity。

步骤2:修改AndroidManifest.xml,代码以下。

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package=" com.demo.test"    android:versionCode="1"    android:versionName="1.0" >    <uses-sdk android:minSdkVersion="15" />    <application        android:icon="@drawable/ic_launcher"        android:label="@string/app_name" >        <meta-data                        android:name="xposedmodule"             android:value="true" />        <meta-data            android:name="xposeddescription"            android:value="Easy example" />        <meta-data            android:name="xposedminversion"            android:value="54" />    </application></manifest>

第一个meta-data中的xposedmodule表示本App将做为Xposed的一个模块,第二个meta-data表示Xposed的最低版本号,第三个meta-data是对该模块的描述。

步骤3:在工程目录下新建一个lib文件夹,放入下载好的XposedBridgeApi-54.jar包,而后在Eclipse工程里选中“XposedBridgeApi-54.jar”,右键单击“->Build Path->Add to Build Path”。

步骤4:新建一个类专门用于挂钩操做,代码以下。

package com.example.xposedHookdemo;import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;import android.content.Context;import android.util.Log;import de.robv.android.xposed.IXposedHookLoadPackage;import de.robv.android.xposed.XC_MethodHook;import de.robv.android.xposed.XposedBridge;import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;public class HookClass implements IXposedHookLoadPackage { public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { Log.i("package", lpparam.packageName); if (!lpparam.packageName.equals("com.demo.test")) return; /* 第一个参数为包名.类名 第二个参数固定为lpparam.classLoader 第三个参数为方法名(要被挂钩的函数名) 第四个参数为函数参数(用类型.class表示) 第五个参数为函数参数(用类型.class表示) 最后一个参数为new XC_MethodHook () {...} */        findAndHookMethod("com.demo.test.MainActivity", lpparam.classLoader, " test1"int.class, int.class, new XC_MethodHook (){ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // 挂钩函数以前执行的代码 Log.i("Before Hook", "Before Hook"); Integer para1 = (Integer)param.args[0]; Integer para2 = (Integer)param.args[1]; Log.i("para1: ", para1.toString());                    Log.i("para2: ",  para2.toString());                     }  protected void afterHookedMethod(MethodHookParam param) throws Throwable { // 挂钩函数以后执行的代码 } }); }}

注意:XC_MethodHook()中有两个重要的内部函数:beforeHookedMethod()和afterHookedMethod()。重写这两个函数能够实现对任意方法的挂钩,beforeHookedMethod()完成执行挂钩函数前须要完成的自定义操做,afterHookedMethod()完成执行挂钩函数以后须要完成的自定义操做。

步骤5:指定模块运行入口。在assert目录下新建xposed_init文件,编译好后,在Xposed中启用这个模块(也就是打上勾),如图2所示,而后重启手机以后,便可看见挂钩后的效果。

图2 启用激活模块

步骤6 新建测试程序示例

新建一个测试程序,该程序功能很是简单,定义一个加法函数test1,而后利用Xposed框架挂钩该函数,并打印函数的值,代码以下所示:

package com.demo.test;import android.App.Activity;import android.os.Bundle;import android.util.Log;import android.view.View;import android.view.View$OnClickListener;import android.widget.Button;import android.widget.TextView;
public class MainActivity extends Activity { Button btn_ok;    TextView mTextView; public MainActivity() { super(); } public int test1(int x, int y) { return x + y; } protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(2130903040); this.btn_ok = this.findViewById(2131230721); this.mTextView = this.findViewById(2131230720); this.btn_ok.setOnClickListener(new View$OnClickListener() { public void onClick(View v) { MainActivity.this.mTextView.Append("\n                    test1:" + MainActivity.this. test1(50, 50)); Log.v("demo_test""Java code is runing"); } }); }}

步骤7:验证测试结果。

运行测试程序,在eclipse的DDMS中能够看到已经Hook的test1函数,打印函数的两个参数值,如图3所示。本示例是在函数运行前挂钩,用户也能够自行测试在函数运行后挂钩,即在afterHookedMethod()中实现代码。

图3 测试函数打印界面

官方教程:

https://github.com/rovo89/XposedBridge/wiki/Development-tutorial


另外,推荐学习网络上共享的开源项目ZjDroid,进一步掌握xposed框架及应用。

# 命令执行结果adb shell logcat -s zjdroid-shell-{package name}# ZjDroid是基于Xposed Framewrok的动态逆向分析模块,逆向分析者能够经过ZjDroid完成如下工做:1、DEX文件的内存dump (适用于早期的压缩壳)2、基于Dalvik关键指针的内存BackSmali,有效破解加固应用3、敏感API的动态监控 (适用于样本的敏感函数监控) adb shell logcat -s zjdroid-apimonitor-{package name} 4、指定内存区域数据dump 5、获取应用加载DEX信息。6、获取指定DEX文件加载类信息。7dump Dalvik java堆信息。(经过java heap分析工具分析处理)8、在目标进程动态运行lua脚本。(经过Lua脚本动态调用java代码,能够动态调用解密函数,完成解密)

好了,本篇文章就分享到此。


未完,待续中



---------------------------------------------
本篇部份内容节选《Android 应用安全测试与防御》
若是您对App安全有任何问题
可在本公众号进行留言
咱们会进行回复~
---------------------------------------------
本书由中国工信出版集团人民邮电出版社(2020年5月)出版,可到各大电商平台(京东、天猫、当当等)购买,搜索书名《Android 应用安全测试与防御》便可。

欢迎关注本书公众号
获取更多App安全知识

       

本文分享自微信公众号 - App安全红宝书(apphongbaoshu)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索