大家好,我是观宇战队的Yorikiko,今天为大家分享Hook技术在安卓安全领域的应用,无论是在渗透攻防还是安全分析与测试,Hook技术都可以提供非常有价值的支撑,可以说是安全研究人员必备的一项技能。
Hook 技术是一种能够劫持函数调用,改变函数执行结果的技术。中文译为“钩子”,即在事件开始到结束之间,截获并监控事件,并实现一些自定义的事件操作功能,达到想要的结果。无论是在渗透攻防还是安全分析与测试,Hook技术都可以提供非常有价值的支撑。例如能绕过系统检测、获取函数关键参数、辅助病毒木马分析以及APP隐私数据合规检测等。
Hook技术的本质就是劫持函数调用。在应用程序还没有调用该函数之前,Hook代码先于函数代码执行,这样既可以改变函数的执行结果,也可以强制结束函数的执行。
由于Android系统中,每个APP的进程空间是独立的,且互相不能访问,因此就需要进行so动态注入,将用于Hook的so注入到目标进程中,实现对目标进程空间的访问,修改目标进程空间中的代码,从而实现Hook的目的。动态注入一般使用ptrace函数来实现。
ptrace函数是一个系统调用,可以监控程序执行,查看或更改被监控程序的内存和寄存器,包括其指令空间、数据空间、堆栈以及所有的寄存器,通常用于软件的开发调试,gdb就是使用ptrace来实现软件调试功能的。在C语言标准库中,ptrace实际调用的是系统调用__NR_ptrace,在Android Kernel 4.4版本中,系统调用号为117。
/* kernel/ptrace.c */ #define __NR_ptrace 117 __SYSCALL(__NR_ptrace, sys_ptrace)
C语言中函数调用方式如下:
#include <sys/ptrace.h> int ptrace(int request, int pid, int addr, int data); 4个参数的含义分别为: 1、request:指示了ptrace要执行的命令; 2、pid: 指示ptrace要跟踪的进程; 3、addr: 指示要监控的内存地址; 4、data: 存放读取出的或者要写入的数据。
动态注入的方法主要分为6步:
1、获取目标APP的pid,并且关联到进程。程序中可以通过遍历查找“/proc/{pid}/cmdline”文件中是否包含目标进程名,APP一般是包名,如有则得到APP的pid值,然后调用ptrace(PTRACE_ATTACH)即可关联到目标进程。
2、获取并保存APP进程当前所有寄存器的值和堆栈,保存现场,调用ptrace(PTRACE_GETREGS, pid, NULL, &saved_regs)即可实现。
3、获取目标APP进程的dlopen和dlsym函数的绝对地址。这里需要了解dlopen函数绝对地址的计算方法,首先了解一下函数偏移的关系,由于同一个系统,使用的linker相同,因此在当前进程中dlopen函数偏移和目标进程中的偏移是相等的,如下图所示。
图1.1 dlopen函数偏移关系
因此,计算目标进程dlopen函数绝对地址的大致思路为,从/proc/{pid}/maps文件中分别获取到当前进程中linker的基地址base1和目标进程linker的基地址base2,然后获取当前进程dlopen函数的绝对地址local_dlopen,最后计算出目标进程dlopen的绝对地址target_dlopen=local_dlopen – base1 + base2,计算dlsym函数绝对地址同理。
4、将hook.so的绝对路径压栈,因为在调用dlopen函数时需要将参数压栈,这里和ROP的使用方式类似。
5、调用dlopen函数加载hook.so,调用dlsym函数获取hook.so的入口函数地址,并执行。
6、恢复目标进程的堆栈及所有寄存器的值,然后解除关联,这样就完成了so的动态注入。整个注入流程如下图所示:
图1.2 动态注入流程图
Hook技术功能十分强大,通过Hook大量的系统函数,可以监控病毒木马的文件操作行为、网络行为等,实现一个沙箱的功能,为分析提供辅助作用。
在逆向分析时配合Hook技术,可以实现加密算法的快速解密,修改函数的返回结果,破解软件等目的。
不仅如此,在当下的攻防实战中,移动端往往也存在很多的攻击面。但是大部分APP进行了各种加固处理,如请求数据全文加密、开启证书校验、root检测等复杂的情况。为了绕过这些复杂的检测逻辑,加密逻辑,降低我们测试的成本。我们可以运用Hook技术,通过Hook关键函数,在保证前后端代码正常运行的情况下,注入我们的Hook脚本,让测试过程只需关注输入输出即可,以此帮助我们更高效的发现安全问题。
Hook主流框架包括Xposed框架、Frida框架、Cydia Substrate框架、ADBI/DDI框架。其中Frida采用动态二进制插桩技术来实现,在目标程序运行时,动态注入frida-agent.so到目标进程中,该so内含js引擎,可以执行各种Hook操作。主要由PC控制端和frida_server两部分组成,frida_server运行在手机或者模拟器中,Hook代码运行在PC端。因此Frida框架相比其他框架较为较量级,下面针对轻量级Hook框架展开介绍。
Frida是一款基于Python + JavaScript的Hook框架, Frida的主要工作方式是将代码注入到目标进程中,架构图如图所示。
图2.1 Frida架构图
Frida兼容了低版本的Android系统,当Android版本低于5.0时,则在Dalvik虚拟机中实现Hook,这时,Hook的实现和Xposed原理相同,都是把被Hook的Java函数转为native函数,并修改函数的入口代码为自定义的代码,使得函数执行时跳转到我们自定义的代码里执行,实现Hook的目的。
在Android系统执行函数的过程中,系统函数dvmCallMethodV会根据该函数的accessFlags值判断是native函数还是Java函数,因此可以通过修改accessFlags的值来实现将Java函数转为native函数,从而进入native函数调用流程,跳转到Hook代码里执行。
为了更好的理解函数转换过程,这里分析一下Dalvik源码中Method结构体源码,代码如下:
struct Method { ClassObject* clazz; /* method所属的类对象*/ u4 accessFlags; /* 访问标记,表示为native函数还是java函数 */ u2 methodIndex; /* method索引 */ //三个size为边界值,对于native函数,这3个size均等于参数列表的size u2 registersSize; /* ins + locals */ u2 outsSize; u2 insSize; const char* name;//函数名称 DexProto prototype; const char* shorty; const u2* insns; int jniArgInfo; DalvikBridgeFunc nativeFunc; /* native函数指针 */ bool fastJni; bool noRef; bool shouldTrace; const RegisterMap* registerMap; bool inProfile; };
Frida修改了被Hook函数中的accessFlags、registersSize、outsSize、insSize和jniArgInfo,将原来的Java函数修改为native函数,并调用dvmUseJNIBridge函数为这个函数设置一个JNI bridge,将nativeFunc指向自定义的函数。Frida源代码如下:
function replaceDalvikImplementation (fn) { if (fn === null && dalvikOriginalMethod === null) { return; } //备份原来的method, if (dalvikOriginalMethod === null) { dalvikOriginalMethod = Memory.dup(methodId, DVM_METHOD_SIZE); dalvikTargetMethodId = Memory.dup(methodId, DVM_METHOD_SIZE); } if (fn !== null) { //自定的代码 implementation = implement(f, fn); let argsSize = argTypes.reduce((acc, t) => (acc + t.size), 0); if (type === INSTANCE_METHOD) { argsSize++; } // 把method变成native函数 /* * make method native (with kAccNative) * insSize and registersSize are set to arguments size */ const accessFlags = (Memory.readU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS)) | kAccNative) >>> 0; const registersSize = argsSize; const outsSize = 0; const insSize = argsSize; Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS), accessFlags); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_REGISTERS_SIZE), registersSize); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_OUTS_SIZE), outsSize); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_INS_SIZE), insSize); Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_JNI_ARG_INFO), computeDalvikJniArgInfo(methodId)); //调用dvmUseJNIBridge为这个Method设置一个Bridge,本质上是修改结构体中的nativeFunc为自定义的implementation函数 api.dvmUseJNIBridge(methodId, implementation); patchedMethods.add(f); } else { patchedMethods.delete(f); Memory.copy(methodId, dalvikOriginalMethod, DVM_METHOD_SIZE); implementation = null; } }
ART虚拟机和Dalvik虚拟机原理相同,也是将Java函数转为native函数,但ART虚拟机的运行机制较为复杂,实现也更复杂。ART虚拟机有两种函数执行模式,一种是quick code模式,该模式直接执行arm汇编指令,另一种是Interpreter模式,由解释器解释执行Dalvik字节码。
ART虚拟机源码中也定义了ARTMethod类,其中entry_point_from_quick_compiled_code_表示以quick code模式执行时的函数入口。
class ArtMethod { GcRoot<mirror::Class> declaring_class_; //method所属的class std::atomic<std::uint32_t> access_flags_;//访问标记,表示为native函数还是java函数 uint32_t dex_code_item_offset_; uint32_t dex_method_index_; uint16_t method_index_; uint16_t hotness_count_; struct PtrSizedFields { ArtMethod** dex_cache_resolved_methods_; void* data_; void* entry_point_from_quick_compiled_code_; // 以quick code模式执行时的函数入口 } ptr_sized_fields_; }
在ART虚拟机中,对每个函数,首先会尝试以quick code模式执行,并检查entry_point_from_quick_compiled_code_的值,这里有3中情况:
一、如果Java函数已经存在quick code, 则转到这个函数对应的quick code的起始地址执行;
二、如果Java函数不存在quick code,该值为 artQuickToInterpreterBridge函数地址,用以切换到 Interpreter 模式来解释执行;
三、如果native函数不存在quick code时,该值为 art_quick_generic_jni_trampoline函数地址,用以执行没有quick code的 native函数。
因此,如果Frida将Java函数修改为native函数时是不存在quick code的,需要将entry_point_from_quick_compiled_code_的值修改为art_quick_generic_jni_trampoline函数地址。同时,修改access_flags_的值为native,Frida源码如下。
patchMethod(methodId, { //jnicode入口entry_point_from_jni_改为自定义的代码 'jniCode': implementation, //修改为access_flags_为native 'accessFlags': (Memory.readU32(methodId.add(artMethodOffset.accessFlags)) | kAccNative | kAccFastNative) >>> 0, //entry_point_from_quick_compiled_code_ 'quickCode': api.artQuickGenericJniTrampoline, //entry_point_from_interpreter_ 'interpreterCode': api.artInterpreterToCompiledCodeBridge });
一、PC机配置:
Python3
pip install Frida
pip install Frida-tools
二、移动端配置:
首先下载Frida-server,且和PC机的版本保持一致。这里选择Frida-server 15.1.16,目标系统为android 32位系统,然后使用adb push将下载好的Frida-server传入手机中。
所有环境搭建完成后,首先启动移动端Frida-server,然后再frida-ps -U查看手机内进程信息,如下图所示则搭建成功。
图2.2:进程信息
由于Hook技术不仅可以劫持APP函数,还可以劫持Android系统函数,如果在没有任何防御措施的情况下,Hook技术几乎可以实现任何想要的结果,功能十分强大。如今Hook技术已经非常成熟,应用领域也十分广泛,如APP双开技术、敏感API监测以及分析加密算法等。
由于Android系统中不能同时运行两个相同的APP,但是很多时候我们需要多账号同时运行,因此APP双开技术就应运而生。但是传统的Hook框架都需要动态注入代码,这样就需要root权限才能实现,这大大增加了手机的安全风险,而且操作成本高,免root实现Hook便由此诞生。
该技术实现原理为将需要多开的APP以插件的方式运行,这样可以实现免安装运行任意应用。这里需要一个宿主APP,及为插件提供运行环境的应用软件,共涉及到Android系统的APP层、Framework层以及Native层。APP想要在Android系统中运行,就必须完成安装,系统才会接纳,但是不安装如何才能正常运行APP呢?答案只有“欺骗”系统,让系统认为已经安装了该应用,而实现“欺骗”系统的方法则通过实现一套虚拟化的Framework层和虚拟化的Native层,如图3.1所示。
图3.1 APP双开框架
虚拟化Framework层主要给Android Framework和插件APP做代理,这也是宿主APP的核心。宿主APP提供了一套自己的Framework,处于Android Framework与插件APP之间。
这样通过代理的方式,就可以实现APP双开的功能了,典型代表项目有360DroidPlugin、VirtualApp等。
随着网络安全法的不断完善,法律对APP的隐私合规性进行了严格规定,为了满足法律要求,越来越多的APP敏感API调用不合规,其检测方法除了使用沙箱外,还可以使用Hook来实现,如Xposed实现方法代码如下。
public class XposedHook implements IXposedHookLoadPackage { public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) { if (lpparam == null) { return; } XposedHelpers.findAndHookMethod( android.net.wifi.WifiInfo.class.getName(), lpparam.classLoader, "getMacAddress", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) { XposedBridge.log("调用getMacAddress()获取了mac地址"); } } ); XposedHelpers.findAndHookMethod( java.net.NetworkInterface.class.getName(), lpparam.classLoader, "getHardwareAddress", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) { XposedBridge.log("调用getHardwareAddress()获取了mac地址"); } } ); } }
这里使用findAndHookMethod检测获取手机MAC地址的getMacAddress和getHardwareAddress函数,如果检测到存在这两个函数的调用,并在日志中打印出检测结果。这里还可以增加其他获取用户敏感信息的API,实现全面监测。
Hook技术对于Android应用逆向分析也有很好的辅助作用,遇到复杂加密算法时,如果采用传统的逆向分析方法,反编译出APP源码,阅读代码及代码调用关系来分析加密算法特征,找出使用的加密算法。如果是已知的AES、RC4以及RSA算法,解密相对来说比较简单,找出密钥即可,但是遇到用户自定义的加密算法,或者已知算法的变体,则分析起来特别的费时费力,并且可能会遇到写不出解密算法的问题。这个时候采用Hook技术则可以方便快捷的得到加密前或解密后的明文数据,只需要逆向找出加解密算法的代码,确定Hook的目标函数即可。
这里使用Xposed为例,合理使用beforeHookedMethod和afterHookedMethod函数,就可以很快获取加密前或者解密后的明文数据。示例代码如下。
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { //HOOK doFinal XposedBridge.hookAllMethods(javax.crypto.Cipher.class, "doFinal", new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { // super.beforeHookedMethod(param); Cipher cipher = (Cipher) param.thisObject; //获取加密的类型 String type = cipher.getAlgorithm(); //获取加密后的返回值 byte[] res = (byte[]) param.getResult(); String data = new String(res); Log.d("TAG", type + ":doFinal返回值:" + data); } }); }
这里使用hookAllMethods函数指定Hook的类为“javax.crypto.Cipher”,Hook的目标函数为“doFinal”函数,因为doFinal比较通用,Java层的DES和AES等算法都会调用该函数,如图3.2所示。
图3.2 AES加解密算法
这里也可以以decrypt或encrypt函数作为目标函数进行Hook,但是实际Hook过程中可能会出现选择的目标函数无效,不能获取想要的数据的情况,这里需要根据实际情况,灵活选择目标函数,来达到获取数据的目的。有了Hook技术的辅助,大大提升了逆向分析效率。
APP存在root检测,一旦见到root情况,则自动退出,检测root方法有很多,这里总结了以下三点
1.检测su等文件,这些二进制文件在root时会安装到手机中。
/sbin/su /system/bin/su /system/bin/failsafe/su /system/xbin/su /system/xbin/busybox /system/sd/xbin/su /data/local/su /data/local/xbin/su /data/local/bin/su
2.常见root权限的应用程序及其相关文件和目录的包文件。
/system/app/Superuser.apk /system/etc/init.d/99SuperSUDaemon /dev/com.koushikdutta.superuser.daemon/ /system/xbin/daemonsu
3.检查已安装的应程序包。
com.thirdparty.superuser eu.chainfire.supersu com.noshufou.android.su com.koushikdutta.超级用户 com.zachspong.temprootremovejb com.ramdroid.appquarantine com.topjohnwu.magisk
这里可以使用Frida脚本进行绕过:
图3.3:frida脚本绕过
图3.4:绕过成功
此外,还可以反编译分析源码,找到检测root的关键函数,编写Hook脚本进行绕过。如某银行app中提取到的root检测代码,可以看到如果检测到开启了root权限,则返回为true,因此可以修改函数返回值为false来绕过。
图3.5:检测root代码
项目中遇到了某金融APP,截取请求包,发现请求体部分进行了加密处理,如下图所示:
图3.6:请求体被加密
这种情况对渗透攻防来说,会非常困难,如果无法修改请求包数据,则不能进行深入测试和利用,所以需要解决请求数据加密问题。主要有2种方式:
第一种:反编译apk,找出加密函数,获取密钥,然后利用burp进行自动加解密操作;
第二种:Hook传递明文信息的函数(此处的Hook不一定非要找到加解密函数,只需要找到传递明文数据的某个函数即可)。
这里因为apk使用了某企业级加固,直接用脱壳机脱壳,过程就不阐述了,然后在adb shell中打印最顶层的activity,找到有关的activity类。
dumpsys activity | grep "mFocusedActivity"s -------android8以下 dumpsys activity | grep "mResumedActivity" ---------android8及以上
图3.7:打印activity
在jadx里进行跟踪分析:
图3.8:jadx跟踪分析
经过Hook测试,kk.c传递的是请求url,与加密无关,然后继续测试d.a方法,先查看d.a的声明处,里面有很多重载的方法,我们挑重要的内容看,当然不嫌麻烦可以挨个Hook确认,我们在图3.12这个a方法中看到了setbody函数,初步确定该方法是用于设置请求体的内容,而b2是b方法的返回值:
图3.9:jadx跟踪分析
进一步查看b方法:
图3.10:jadx跟踪分析
编写Hook代码,打印b方法的两个参数,获取请求体明文数据。
图3.11:Hook代码
图3.12:打印明文
这里使用Frida来实现,因为Frida可以很方便的Hook Android系统的java层和Native层函数,因此可以总结出所有和隐私信息收集相关的系统函数,编写Frida脚本对APP进行行为监测,查看是否存在隐私信息收集行为。隐私收集API示例如下:
hook('android.telephony.TelephonyManager', [ // Android 8.0 {'methodName': 'getDeviceId', 'action': action, 'messages': '获取IMEI'}, // Android 8.1、9 android 10获取不到 {'methodName': 'getImei', 'action': action, 'messages': '获取IMEI'}, {'methodName': 'getLine1Number', 'action': action, 'messages': '获取电话号码标识符'}, {'methodName': 'getSimSerialNumber', 'action': action, 'messages': '获取IMSI/iccid'}, {'methodName': 'getSubscriberId', 'action': action, 'messages': '获取IMSI'}, {'methodName': 'getSimCountryIso', 'action': action, 'messages': '获取SIM卡国家代码'}, {'methodName': 'getCellLocation', 'action': action, 'messages': '获取电话当前位置信息'}, {'methodName': 'getAllCellInfo', 'action': action, 'messages': '获取电话当前位置信息'}, {'methodName': 'requestCellInfoUpdate', 'action': action, 'messages': '获取基站信息'}, ]);
以上只列举了部分隐私收集API,如常见的获取收集IMEI号的getDeviceId函数、获取本机号码的getLine1Number函数以及获取IMSI的getSubscriberId函数等,都可以通过Frida Hook来进行监测。在应用宝中任意下载一款APP,然后安装到模拟器中,如下图所示。
图3.13 腾讯应用宝截图
然后在模拟器中启动frida_server,开启端口转发,运行Frida实现隐私函数调用监测。监测结果如下:
图3.14 隐私函数API监测结果
不同的APP调用系统隐私数据收集函数的种类不同,但是都可以通过Frida框架来监测,从而检测APP是否合规。
Hook技术同样可以用于病毒木马分析,因为它不仅可以用于API监测,还可以用于加密算法解密,甚至可以用于制作沙箱。这在病毒木马分析中也非常有用,如果是锁屏勒索木马,则可以通过Hook来获取解锁密码,省去了逆向分析的繁琐过程,大大提升了解锁的效率。
这里以某锁屏病毒为例,用PKID查壳发现该病毒未使用加固保护,于是直接用jadx反编译得到源码,分析AndroidManifest.xml文件得到,该病毒入口为MainActivity,里面只启动了一个服务Myservice,继续分析该服务代码,找到解锁算法代码在m6L函数中,如下图所示:
图3.15 Myservice的onCreate函数代码
继续分析发现使用了复杂的加密算法,但是设置了一个监听事件,并且在该监听事件中会校验输入的解锁密码是否正确,于是这里就可以编写Hook脚本获取解锁密码了。这里Hook到相应参数后,需要进行简单的运算,算法如下,this$0.f6ck为界面显示的识别码。
(this$0.f6ck除以val$Admin)异或m2sb(f37val$)
图3.16 解锁密码验证代码
图3.17 锁机病毒界面
m2sb函数源码如下所示:
public static int m2sb(int i) { int i2 = i; if (i2 < 2) { return i2; } int i3 = 1; int i4 = 65537 / i2; int i5 = 65537 % i2; while (i5 != 1) { int i6 = i2 / i5; i2 %= i5; i3 = (i3 + (i4 * i6)) & 65530; if (i2 == 1) { return i3; } int i7 = i5 / i2; i5 %= i2; i4 = (i4 + (i3 * i7)) & 65531; } return (1 - i4) & 65532; }
最终Hook出解锁密码如下图所示。
图3.18 Hook结果
Hook还可以用于制作安卓沙箱,对获取root权限、sdcard操作以及获取用户敏感信息等系统函数进行监测,分析病毒木马的行为,类似于微步沙箱的功能。在纯逆向分析没有头绪时,可以带来意想不到的收获,对于病毒木马分析起到非常好的辅助作用。
根据对Hook技术的原理和框架的介绍,并结合示例分析,不难发现无论是在渗透攻防还是安全分析与测试过程中,Hook技术都可以提供非常有价值的支撑。不仅能绕过系统检测、获取函数关键参数,病毒木马分析方面也有着突出的贡献,而且在APP安全测试中,亦可用此方法对隐私数据收集函数进行监测,从而实现APP隐私数据合规检测。
《Android安全技术揭秘与防范》
《Android软件安全权威指南》
https://codeshare.Frida.re/@dzonerzy/Fridantiroot/
https://redfoxsec.com/blog/android-root-detection-bypass-using-frida/
https://www.cnblogs.com/xsj210/p/15774406.html
https://github.com/WooyunDota/DroidSSLUnpinning
https://mabin004.github.io/2018/07/31/Mac%E4%B8%8A%E7%BC%96%E8%AF%91Frida/
https://github.com/asLody/VirtualApp
新华三观宇战队,专注前沿防御技术研究,研究方向包括漏洞分析、攻击反制、病毒木马分析、APT分析等,长期招聘攻防研究员,简历投递:[email protected](请注明来自FreeBuf)