一、背景
在近几年,随着数字信息时代的飞速发展,人们越来越关注个人隐私和数据安全。各国政府和监管机构为了保护消费者的隐私权益,对APP的合规要求提出了更高的标准,特别是关于敏感信息的采集和处理。在这个背景下,MAC地址作为设备的唯一标识符,其采集和使用受到了严格的审查。因此,对于APP开发者和安全测试人员来说,监控APP是否采集了MAC地址成为了一个重要的任务。
比如工信部对各应用市场中的APP进行抽查发现很多应用存在用户为同意隐私协议的情况下采集MAC地址等隐私信息的违规行为,相关应用会被通知整改,如果整改不力会被通知下架的严重后果。本文将介绍如何检测APP中使用的第三方库是否有采集MAC地址行为。 当检测到有第三方库有采集MAC地址行为时,可以采用延迟初始化的方式,即用户同意隐私协议后在对这些库进行初始化。
为了有效地监控和测试APP是否非法采集了MAC地址,`Xposed`框架和`Frida`成为了两个主流的技术手段。这两个工具都能够在不修改APP代码的情况下,对APP的运行时行为进行监控和修改,但它们在实现方式和使用场景上各有特点。
二、基本介绍
### Xposed框架
Xposed是一种基于Android操作系统的动态代码修改工具。它通过替换`/system/bin/app_process`来加载自己的`Xposed Bridge`,从而在运行时修改系统和应用程序的行为。Xposed能够让用户安装各种模块来实现系统级别的定制化功能,包括对APP行为的监控。
Xposed的优势在于其深度集成于Android系统,能够非常方便地拦截到系统级别的调用,例如API调用、系统服务调用等。这使得Xposed特别适合进行系统底层的行为修改和监控。然而,Xposed需要设备的root权限,这限制了其在一些场景下的使用。
### Frida
Frida则是一个跨平台的动态代码插桩工具,支持包括Android、iOS、Windows、macOS和Linux在内的多种操作系统。Frida工作原理是注入自己的代码到目标进程中,通过挂钩(Hooking)函数调用来监控和修改应用程序的行为。
与Xposed相比,Frida的一个显著优点是不需要设备的root权限,只要能够以某种方式运行Frida Server即可开始测试。这让Frida在非root设备上的应用程序测试中具有更广泛的适用性。Frida提供了丰富的API和脚本支持,使得用户可以灵活地编写自定义的测试脚本来满足各种测试需求。
### Xposed与Frida的区别
尽管Xposed和Frida都可以用于监控APP是否采集了MAC地址,它们之间存在一些关键的不同:
- **系统要求:** Xposed需要root权限和对Android系统的深度修改,而Frida则可以在不root的设备上使用,对系统的侵入性更小。
- **跨平台能力:** Frida支持包括Android、iOS在内的多个平台,更加灵活和广泛;Xposed主要针对Android平台。
- **使用复杂度:** Xposed通过安装模块的方式来实现功能,对于非开发者来说可能更易于使用;而Frida则侧重于通过编写和执行脚本来实现更复杂的监控逻辑,需要一定的编程知识。
三、最近几年frida用的更广泛
近几年,Frida的使用比Xposed更为广泛,主要由于以下几个原因:
1. 跨平台支持
Frida提供了广泛的跨平台支持,包括Android、iOS、Windows、macOS和Linux等。相比之下,Xposed主要针对Android系统,这在一定程度上限制了其应用范围。虽然frida在ios的使用,前提是设备已经越狱,在当前,越狱的ios设备大部分都是老旧版本。但是frida在支持平台上,仍然有明显的优势。
2. frida无需Root权限
Frida可以在无需root权限的设备上运行,尤其对于iOS设备来说,这是一个重要优势。虽然在Android上使用Frida通常需要root权限或者某种形式的工作环境设置来启动Frida server,但Frida提供了更多灵活的部署选项,包括USB、网络和进程注入等。Xposed则需要对设备进行root,这增加了使用的复杂度。
3. 开发和社区支持
Frida拥有一个活跃的开发社区和持续的更新,提供了大量的文档、教程和示例代码。这些资源极大地降低了新用户的学习门槛,并为高级用户提供了强大的支持。与此同时,Xposed虽然也有一个支持社区,但近年来在更新和新功能开发方面相对较少。
4. 功能丰富和灵活
Frida不仅支持挂钩和修改运行中的应用程序代码,还提供了内存访问、动态分析和脚本编写等高级功能。这些功能使得Frida在应用程序分析、逆向工程和安全测试领域非常强大。而Xposed则主要通过模块化的方式提供特定的修改和定制功能,虽然强大但在某些高级场景下可能不如Frida灵活。
5. 安全测试和逆向工程需求的变化
随着移动应用和安全测试领域的发展,对工具的需求也在变化。Frida的动态分析能力非常适合现代的安全审计和快速迭代开发模式,能够及时发现和修复潜在的安全问题。相比之下,Xposed在定制Android系统行为和开发特定功能方面仍然有其独特的优势,但在安全测试和逆向工程方面可能不如Frida全面。
6,frida的易用性更好
相比起xposed框架需要进行java代码编写框架,frida提供了更灵活的javascript的方式来进行函数hook,在使用门槛和易用性上,要超过xposed。
四,如何使用xposed来进行mac获取函数的hook
一,在手机中安装VirtualXposed
首先需要确定需要检测的APP是32位还是64位架构,并下载相应版本的VirtualXposed源码进行编译或者直接下载APK
VirtualXposed<=0.18.2版本仅支持32位架构
VirtualXposed>0.18.2版本不在支持32位架构,仅支持64位架构
将VirtualXposed APK文件安装到测试手机(这里需要注意测试手机的CPU架构也需要跟VirtualXposed支持的架构保持一致),安装后需手动进入VirtualXposed的应用权限设置界面,将所有的权限都开启(VirtualXposed并不会自动请求权限,所以需要手动开启),然后运行VirtualXposed
二,编写Hook程序
编写Hook程序前需要先知道通过哪些接口可以获取到MAC地址
获取MAC地址方法一:调用NetworkInterface类的getHardwareAddress()方法
获取MAC地址方法二:调用WifiInfo类的getMacAddress()方法
获取MAC地址方法三:通过Runtime.getRuntime().exec("cat /sys/class/net/wlan0/address ")方法获取
明确了需要hook的方法,开始编写Hook程序:
1.新建一个APP项目
2.项目中新建一个Main类,实现IXposedHookLoadPackage接口
public class Mainimplements IXposedHookLoadPackage {
private static final StringTAG ="Main";
private StringHOOK_PREFIX ="my_hook -> ";
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam)throws Throwable {
hookMacAddress1(lpparam);
hookMacAddress2(lpparam);
hookMacAddress3(lpparam);
}
private StringgetStackTrace(Throwable ex) {
Writer writer =new StringWriter();
PrintWriter printWriter =new PrintWriter(writer);
try {
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause !=null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
return writer.toString();
}catch (Throwable e) {
Log.e(TAG, "getStackTrace", e);
}finally {
printWriter.close();
}
return ex.toString();
}
/**
* 检测App有没有获取mac地址,适用于7.0以上的系统
*/
private void hookMacAddress1(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class clazz = lpparam.classLoader.loadClass("java.net.NetworkInterface");
String methodName ="getHardwareAddress";
XposedHelpers.findAndHookMethod(clazz, methodName, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param)throws Throwable {
super.beforeHookedMethod(param);
Log.i(TAG, HOOK_PREFIX +"hookMacAddress1 getHardwareAddress");
Log.e(TAG, HOOK_PREFIX + getStackTrace(new Exception()));
}
});
}catch (Throwable e) {
Log.e(TAG, "hookMacAddress1", e);
}
}
/**
* 检测App有没有获取mac地址,适用于7.0以下系统
*/
private void hookMacAddress2(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class clazz = lpparam.classLoader.loadClass("android.net.wifi.WifiInfo");
String methodName ="getMacAddress";
XposedHelpers.findAndHookMethod(clazz, methodName, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param)throws Throwable {
super.beforeHookedMethod(param);
Log.i(TAG, HOOK_PREFIX +"hookMacAddress2 getMacAddress");
Log.e(TAG, HOOK_PREFIX + getStackTrace(new Exception()));
}
});
}catch (Throwable e) {
Log.e(TAG, "hookMacAddress2", e);
}
}
private void hookMacAddress3(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class clazz = lpparam.classLoader.loadClass("java.lang.Runtime");
String methodName ="exec";
XposedHelpers.findAndHookMethod(clazz, methodName, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param)throws Throwable {
super.beforeHookedMethod(param);
Log.i(TAG, HOOK_PREFIX +"hookMacAddress3 getMacAddress param = " + param.args[0]);
Log.e(TAG, HOOK_PREFIX + getStackTrace(new Exception()));
}
});
}catch (Throwable e) {
Log.e(TAG, "hookMacAddress3", e);
}
}
public static StringgetMacAddress1() {
String macAddress =null;
StringBuffer buf =new StringBuffer();
NetworkInterface networkInterface =null;
try {
networkInterface = NetworkInterface.getByName("eth1");
if (networkInterface ==null) {
networkInterface = NetworkInterface.getByName("wlan0");
}
if (networkInterface ==null) {
return "02:00:00:00:00:02";
}
byte[] addr = networkInterface.getHardwareAddress();
for (byte b : addr) {
buf.append(String.format("%02X:", b));
}
if (buf.length() >0) {
buf.deleteCharAt(buf.length() -1);
}
macAddress = buf.toString();
}catch (SocketException e) {
e.printStackTrace();
return "02:00:00:00:00:02";
}
return macAddress;
}
public static StringgetMacAddress2(Context context) {
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = wifi.getConnectionInfo();
return info.getMacAddress();
}
private StringgetMacAddress3() {
String macSerial =null;
String str ="";
try {
Process pp = Runtime.getRuntime().exec("cat /sys/class/net/wlan0/address ");
InputStreamReader ir =new InputStreamReader(pp.getInputStream());
LineNumberReader input =new LineNumberReader(ir);
for (; null != str; ) {
str = input.readLine();
if (str !=null) {
macSerial = str.trim();// 去空格
break;
}
}
}catch (IOException ex) {
// 赋予默认值
ex.printStackTrace();
}
return macSerial;
}
}
3.在src/main/目录下新建一个Assets Folder,并在Assets Folder中创建xposed_init文件,xposed_init文件中写入Main类的完成名称,例如:com.w.myhook.Main
4.在AndroidManifest.xml中声明xposed meta-data
<meta-data android:name="xposedmodule"
android:value="true"/>
<meta-data android:name="xposedminversion"
android:value="53"/>
<meta-data android:name="xposeddescription"
android:value="myAppHook"/>
5.配置xposed库依赖
compileOnly'de.robv.android.xposed:api:82'
compileOnly'de.robv.android.xposed:api:82:sources'
6.编译运行
三,安装MyHook模块到VirtualXposed
1. 启动VirtualXposed后,点击底部的按钮 - 添加应用 - 选择MyHook - 点击安装 - 选择VirtualXposed
2.返回VirtualXposed首页,向上滑动打开抽屉,启动Xposed Installer,开启MyHook模块
3.重启VirtualXposed
不需要重启手机,只需要将VirtualXposed APP的进程重启即可
四,安装需要检测的APP
可以直接从手机SD卡中安装,也可以像第三步的第1步安装MyHook那样把需要检测的APP安装到VirtualXposed,这里就不再赘述了,注:安装APP后不需要再添加模块或重启VirtualXposed了
五,运行被检测的APP
返回VirtualXposed首页,向上滑动打开抽屉,点击运行需要检测的APP
过滤日志关键字my_hook,就可以看到mac地址的调用情况和调用堆栈:
五,如何使用frida来进行mac获取函数的hook
一、环境配置
frida分为服务端和客户端,其中服务端运行在设备上,在本例中,就是运行在已经root的android手机,或者模拟器上。
我平时使用的模拟器居多,当然模拟器也有问题。现在很多app在启动的时候就会检测环境,如果是root手机,只会检测到已经root。而如果是模拟器,可能会被检测到已经root还有模拟器环境,在这种情况下,可能还需要进行模拟器检测的bypass。
如果使用的是PC上安装Frida可以直接使用pip进,确保你的PC上已安装Python和pip。
通过pip安装Frida:pip install frida-tools
在Android设备上安装Frida Server
根据你的设备架构下载对应版本的Frida Server(例如arm, arm64, x86等),可从Frida的GitHub发布页面找到。
https://github.com/frida/frida
将下载的Frida Server二进制文件推送到Android设备上,并确保其具有执行权限:
adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"
推送完成后,就可以在android脚本上启动frida-server
adb shell "/data/local/tmp/frida-server &"
二、确定android中需要hook的具体函数
这里用没有代码权限,需要反编译apk举个例子。
比如我们拿到了一个apk,这里用一个没有进行加固的apk举例,加固过的apk还涉及到脱壳,这里就不过于复杂了。
首先,你需要反编译APK文件来查看其代码。虽然你不能直接获取到源代码,但是通过反编译工具可以得到相对易读的smali代码或者Java代码(通过工具如jadx)。
jadx是一个非常强大的工具,它可以将DEX文件转换成Java源代码。直接用GUI版本的jadx打开APK文件,会更方便查看和搜索代码。这里我们通过jadx直接反编译apk。
如下图
反编译的目录结构:
源代码目录(源代码)
android:这通常包含了对Android SDK中的类的引用或者继承。
androidx:代表使用了AndroidX库,这是支持库的一个新版本,提供了向后兼容性。
com、cn、org、net:这些目录包含了按照包名组织的应用代码和依赖库的代码。
kotlin、kotlinx:包含了Kotlin语言编写的代码和扩展库。
demo、jcifs、okhttp3、okio、rx、p000360update、top、v0:可能是应用本身的代码或者第三方库的代码。
资源文件夹(资源文件夹)
assets:通常包含应用需要的资源文件,如配置文件、文本文件、数据库文件等。
res:包含了应用的资源文件,例如布局(layout)、字符串(values)、图片(drawable)等。
其他重要文件和目录
AndroidManifest.xml:这是APK文件的清单文件,包含了应用的包名、权限请求、活动(Activities)、服务(Services)、广播接收器(Broadcast Receivers)等。
classes.dex、classes2.dex、classes3.dex、classes4.dex:这些DEX文件包含了应用的编译后的代码。多个DEX文件通常表示应用使用了多DEX文件来绕过单个DEX文件大小的限制。
resources.arsc:这是编译后的资源文件,包含了所有的非代码资源,如字符串、样式等。
META-INF:包含了APK签名和其他元数据的文件夹。
lib:存放应用使用的原生库(Native libraries),可能按照不同的CPU架构有不同的子目录。
我们把jadx反编译后的文件保存到目录下, 然后使用idea打开,全局搜索关键函数
例如在com/xxxx/commonlib/utils/y.java这个y类下面有函数h,调用了gethardwareaddress(),那我们就可以针对这个函数进行frida脚本的hook.
三.编写Frida脚本
既然已经确定了需要hook的函数h位于com.xxxx.commonlib.utils.y类中,你可以编写一个Frida脚本来hook这个方法。下面是一个基本的Frida脚本示例,它将在调用h方法时输出当前类名和一些可能有用的调用
Java.perform(function () {
// 定位到类
var TargetClass = Java.use('com.xxxx.commonlib.utils.y');
// hook该类的h方法
TargetClass.h.overloads.forEach(function(overload) {
overload.implementation = function () {
// 输出日志
console.log('Called h from com.xxxx.commonlib.utils.y');
// 如果需要,这里可以输出方法的参数
// console.log('Arguments:', JSON.stringify(arguments));
// 调用原始方法
return this.h.apply(this, arguments);
};
});
});
这段脚本使用了Java.use函数来hook指定的Java类。overloads属性和方法用于hook所有的h方法重载。如果h方法没有重载,那么可以去掉overloads.forEach循环并直接替换implementation。
四。使用Frida监控Android应用
确定目标应用的包名,例如com.example.app。
使用Frida运行之前编写的脚本并附加到目标应用上:
frida -U -f com.example.app-l path/to/your/script.js--no-pause
这里的-U表示通过USB连接设备,-f用于指定目标应用的包名,-l指定要加载的Frida脚本路径,--no-pause意味着在应用启动时不暂停。
操作目标应用,尝试触发获取MAC地址的行为。如果应用尝试获取MAC地址,你将在控制台中看到相应的日志输出。