前几天,收到公司App违规收取用户隐私的邮件,说是存在收集设备MAC地址的行为。
这就让我很方了,上次已经整改过一次违规获取用户隐私的问题了,这次又来。。
因为上次整改的时候,已将所有的第三方库移到用户同意了隐私协议后,才去初始化的,自己的代码又不会去获取这些数据,理应不会再出现获取,所以就很奇怪,不知道哪里出了问题。
后来想到,既然是去获取了MAC地址,必定要调用系统的API,那么我只要去HOOK系统的方法,就可以知道在什么时候,去获取了MAC地址了。
由于各个系统版本获取MAC地址的方式不同,所以特意拿了个Android5.1的手机进行测试。
/**
* Android 6.0 之前(不包括6.0)获取mac地址
* 必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
*
* @param context * @return
*/
private String getMacDefault(Context context) {
String mac = "0";
if (context == null) {
return mac;
}
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = null;
try {
info = wifi.getConnectionInfo();
} catch (Exception e) {
e.printStackTrace();
}
if (info == null) {
return null;
}
mac = info.getMacAddress();
return mac;
}
可以看到,我们在Android6.0以前是通过WifiManager.getConnectionInfo()
的方式来获取相关数据的。
我们来看下这个方法。
IWifiManager mService;
public WifiInfo getConnectionInfo() {
try {
return mService.getConnectionInfo(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
可以看到,这里调用的是IWifiManager.getConnectionInfo(),IWifiManager是一个接口
interface IWifiManager{
WifiInfo getConnectionInfo(String callingPackage);
...
}
那么这个IWifiManager
是什么时候被赋值的呢?我们回到context.getSystemService(Context.WIFI_SERVICE);
我们知道,context的实现类其实是ContextImpl.java
,我们直接来看ContextImpl.getSystemService()
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
我们可以看到,这里调用了SystemServiceRegistry.getSystemService(this, name);
我们再来看这个类
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
这里的SYSTEM_SERVICE_FETCHERS
是一个Map<String, ServiceFetcher<?>>
,从这我们可以知道,通过SYSTEM_SERVICE_FETCHERS
获取到一个ServiceFetcher
,然后再调用ServiceFetcher.getSErvice(ctx)
,从而得到Service。
那么SYSTEM_SERVICE_FETCHERS是从哪里开始赋值的呢?
我们可以在SystemServiceRegistry
找到一端静态代码块
static{
...
registerService(Context.WIFI_SERVICE, WifiManager.class,
new CachedServiceFetcher<WifiManager>() {
@Override
public WifiManager createService(ContextImpl ctx) throws ServiceNotFoundException {
IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_SERVICE);
IWifiManager service = IWifiManager.Stub.asInterface(b);
return new WifiManager(ctx.getOuterContext(), service,
ConnectivityThread.getInstanceLooper());
}});
...
}
其中,就有注册我们需要的WIFI_SERVICE
,并实现了createService方法,方法内使用了AIDL,进行了跨进程的访问。
我们来看下CachedServiceFetcher
,这是个抽象类
static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
private final int mCacheIndex;
CachedServiceFetcher() {
// Note this class must be instantiated only by the static initializer of the
// outer class (SystemServiceRegistry), which already does the synchronization,
// so bare access to sServiceCacheSize is okay here.
mCacheIndex = sServiceCacheSize++;
}
@Override
@SuppressWarnings("unchecked")
public final T getService(ContextImpl ctx) {
final Object[] cache = ctx.mServiceCache;
final int[] gates = ctx.mServiceInitializationStateArray;
for (;;) {
boolean doInitialize = false;
synchronized (cache) {
// Return it if we already have a cached instance.
T service = (T) cache[mCacheIndex];
if (service != null || gates[mCacheIndex] == ContextImpl.STATE_NOT_FOUND) {
//如果缓存不为null,直接返回service
return service;
}
// If we get here, there's no cached instance.
// Grr... if gate is STATE_READY, then this means we initialized the service
// once but someone cleared it.
// We start over from STATE_UNINITIALIZED.
if (gates[mCacheIndex] == ContextImpl.STATE_READY) {
gates[mCacheIndex] = ContextImpl.STATE_UNINITIALIZED;
}
// It's possible for multiple threads to get here at the same time, so
// use the "gate" to make sure only the first thread will call createService().
// At this point, the gate must be either UNINITIALIZED or INITIALIZING.
if (gates[mCacheIndex] == ContextImpl.STATE_UNINITIALIZED) {
doInitialize = true;
gates[mCacheIndex] = ContextImpl.STATE_INITIALIZING;
}
}
if (doInitialize) {
// Only the first thread gets here.
T service = null;
@ServiceInitializationState int newState = ContextImpl.STATE_NOT_FOUND;
try {
// This thread is the first one to get here. Instantiate the service
// *without* the cache lock held.
//第一次调用,回去调用createService,也就是registerService时候去实现的那个方法
service = createService(ctx);
newState = ContextImpl.STATE_READY;
} catch (ServiceNotFoundException e) {
onServiceNotFound(e);
} finally {
synchronized (cache) {
cache[mCacheIndex] = service;
gates[mCacheIndex] = newState;
cache.notifyAll();
}
}
return service;
}
// The other threads will wait for the first thread to call notifyAll(),
// and go back to the top and retry.
synchronized (cache) {
while (gates[mCacheIndex] < ContextImpl.STATE_READY) {
try {
cache.wait();
} catch (InterruptedException e) {
Log.w(TAG, "getService() interrupted");
Thread.currentThread().interrupt();
return null;
}
}
}
}
}
public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
}
我们可以看下ContextImpl
中的mServiceCache
final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
这个mServiceCache
的也是从SystemServiceRegistry
中获取的,而且只返回了一个简单的数组。
public static Object[] createServiceCache() {
return new Object[sServiceCacheSize];
}
可以看到,getService
的时候,优先会去拿缓存,而缓存的规则是根据ContextImpl
来的,由于Application
、Activity
、Service
的ContextImpl
不是同一个,所以获取到的WifiManager不是同一个。
看到这里,我们就知道WifiManager的创建过程了。所以我们分别对Application
、Activity
、Service
获取的WifiManager,分别替换其IWifiManager
,就可以达到Hook的目的了。
附上代码
public class HookUtils {
private static WifiInfo cacheWifiInfo = null;
public static void hookMacAddress(String tag, Context context) {
try {
Class<?> iWifiManager = Class.forName("android.net.wifi.IWifiManager");
Field serviceField = WifiManager.class.getDeclaredField("mService");
serviceField.setAccessible(true);
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
// real mService
Object realIwm = serviceField.get(wifi);
// replace mService with Proxy.newProxyInstance
serviceField.set(wifi, Proxy.newProxyInstance(iWifiManager.getClassLoader(),
new Class[]{iWifiManager},
new InvocationHandler(tag, "getConnectionInfo", realIwm)));
Log.i(tag, "wifiManager hook success");
} catch (Exception e) {
Log.e(tag, "printStackTrace:" + e.getMessage());
e.printStackTrace();
}
}
public static class InvocationHandler implements java.lang.reflect.InvocationHandler {
private final String tag;
private final String methodName;
private Object real;
public InvocationHandler(String tag, String methodName, Object real) {
this.real = real;
this.methodName = methodName;
this.tag = tag;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d(tag, "method invoke " + method.getName());
if (methodName.equals(method.getName())) {
if (cacheWifiInfo != null) {
Log.d(tag, "cacheWifiInfo:" + cacheWifiInfo);
return cacheWifiInfo;
}
WifiInfo wifiInfo = null;
try {
Class cls = WifiInfo.class;
wifiInfo = (WifiInfo) cls.newInstance();
Field mMacAddressField = cls.getDeclaredField("mMacAddress");
mMacAddressField.setAccessible(true);
mMacAddressField.set(wifiInfo, "");
cacheWifiInfo = wifiInfo;
Log.d(tag, "wifiInfo:" + wifiInfo);
} catch (Exception e) {
Log.e(tag, "WifiInfo error:" + e.getMessage());
}
return wifiInfo;
} else {
return method.invoke(real, args);
}
}
}
}
然后进行Hook
HookUtils.hookMacAddress("Z-Application",getApplicationContext());
HookUtils.hookMacAddress("Z-Activity",MainActivity.this);
HookUtils.hookMacAddress("Z-Service",MyService.this);
最后,运行程序,可以看到,当获取Mac地址的时候,会打印相关日志。
method invoke getConnectionInfo
至此,我们就Hook成功了。
然后,我们就可以在App出现这个日志的时候,定位到是哪个功能调用了Mac地址。
我们这最终定位到是点击了隐私协议,通过腾讯X5 WebView显示H5页面的时候,调用了Mac地址。
最终,通过Hook MAC地址方法,当获取MAC地址的时候,进行全局的拦截,返回一个空的MAC地址,使其无法获取到真正的MAC地址。
还有另一个违规问题看这边 Android 违规获取用户隐私(获取软件安装列表信息)整改