区长

一种全局拦截并监控 DNS 的方式

如果网络库使用的是OkHttp,那么OkHttp提供了Dns接口,用于自定义dns的实现,一般HttpDNS就可以通过这个接口去实现,但是这种方式有一个巨大的局限性,就是只能拦截到当前使用的OkHttpClient的DNS,无法拦截其他库中的OkHttpClient的DNS解析行为,而且如果使用的不是OkHttp,则一律拦截不到。那么有没有一种方式,可以拦截到整个App的DNS解析而不依赖OkHttp呢?

前几天看了两篇文章

觉得可行,自己试验了一下,最终得出结论:

可以拦截整个App的DNS解析并进行监控(如判断是否被劫持了),甚至连WebView中的DNS解析也可以拦截到,使WebView使用HttpDNS成为可能。

那么具体怎么拦截呢?

我们知道,Java层获取域名对应IP是通过InetAddress.getAllByName(“your.domain.com”)来获取的,而在Android上,其实现大致如下:

1
2
3
4
5
6
static final InetAddressImpl impl = new Inet6AddressImpl();
public static InetAddress[] getAllByName(String host)
throws UnknownHostException {
return impl.lookupAllHostAddr(host, NETID_UNSET).clone();
}

最终调用的是Inet6AddressImpl对象的lookupAllHostAddr方法,而Inet6AddressImpl是InetAddressImpl接口的实现类,可以完成ipv4和ipv6的dns解析。

通过上面的分析,发现InetAddressImpl是一个接口,于是我们完全可以代理掉原来的实现方法,但是不幸的是该接口不是public访问符,于是我们只有一种代理方式可以使用,那就是动态代理。

但是还有一个问题,impl是final的,在android上修改final字段和java平台有点区别,android上是否是final这个字段是记录在Field中的accessFlags中,而不是java平台上的modifiers字段中,因此反射修改的时候要特别注意。

简单的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
try {
//获取InetAddress中的impl
Field impl = InetAddress.class.getDeclaredField("impl");
impl.setAccessible(true);
//获取accessFlags
Field modifiersField = Field.class.getDeclaredField("accessFlags");
modifiersField.setAccessible(true);
//去final
modifiersField.setInt(impl, impl.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
//获取原始InetAddressImpl对象
final Object originalImpl = impl.get(null);
//构建动态代理InetAddressImpl对象
Object dynamicImpl = Proxy.newProxyInstance(originalImpl.getClass().getClassLoader(), originalImpl.getClass().getInterfaces(), new InvocationHandler() {
final Object lock = new Object();
Constructor<Inet4Address> constructor = null;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果函数名为lookupAllHostAddr,并且参数长度为2,第一个参数是host,第二个参数是netId
if (method.getName().equals("lookupAllHostAddr") && args != null && args.length == 2) {
Log.e("TAG", "lookupAllHostAddr:" + Arrays.asList(args));
//获取Inet4Address的构造函数,可能还需要Inet6Address的构造函数,为了演示,简单处理
if (constructor == null) {
synchronized (lock) {
if (constructor == null) {
try {
constructor = Inet4Address.class.getDeclaredConstructor(String.class, byte[].class);
constructor.setAccessible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
if (constructor != null) {
//这里实现自己的逻辑
//构造一个mock的dns解析并返回
if (args[0] != null && "www.baidu.com".equalsIgnoreCase(args[0].toString())) {
try {
Inet4Address inetAddress = constructor.newInstance(null, new byte[]{(byte) 61, (byte) 135, (byte) 169, (byte) 121});
return new InetAddress[]{inetAddress};
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return method.invoke(originalImpl, args);
}
});
//替换impl为动态代理对象
impl.set(null, dynamicImpl);
//还原final
modifiersField.setInt(impl, impl.getModifiers() & java.lang.reflect.Modifier.FINAL);
} catch (Exception e) {
e.printStackTrace();
}

特别值得注意的是,动态代理对象中,不能再调用InetAddress.getAllByName(“your.domain.com”),否则会出现死循环,造成java.lang.StackOverflowError异常,所以只能通过反射构造函数,构造一个InetAddress数组对象返回。

这种方式的优点很明显,可以拦截Java层的所有dns解析,并且连WebView中的dns解析也能拦截到,为WebView中使用HttpDns提供了可能
缺点也很明显,需要反射,在不同系统版本上可能存在兼容性问题,需要进行适配。上面的代码并没有进行过兼容性测试,如果需要使用,自行测试。

坚持原创技术分享,您的支持将鼓励我继续创作!
区长 WeChat Pay

微信打赏