前言
DNS劫持
指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应。
DNS劫持
的主要表现为看视频,点击之后莫名其妙的跳到了某些广告网站。正常情况下,当我们点击某个链接的时候,会向一个称作
DNS服务器
的东西发出请求,把链接转换成机器能够识别的
ip
地址,其过程如下:
域名->
ip
地址的过程被称作
DNS解析
。在这个过程中,由于
DNS
请求报文是明文状态,可能会在请求过程中被监测,然后攻击者伪装
DNS
服务器向主机发送带有假
ip
地址的响应报文,从而使得主机访问到假的服务器。
NSURLProtocol
NSURLProtocol
是苹果提供给开发者的黑魔法之一,大部分的网络请求都能被它拦截并且篡改,以此来改变URL的加载行为。这使得我们不必改动网络请求的业务代码,也能在需要的时候改变请求的细节。作为一个抽象类,我们必须继承自NSURLProtocol
才能实现中间攻击的功能。
是否要处理对应的请求。由于网页存在动态链接的可能性,简单的返回
YES
可能会创建大量的NSURLProtocol
对象,因此我们需要保证每个请求能且仅能被返回一次YES
+ (BOOL)canInitWithRequest: (NSURLRequest *)request;
+ (BOOL)canInitWithTask: (NSURLSessionTask *)task;是否要对请求进行重定向,或者修改请求头、域名等关键信息。返回一个新的
NSURLRequest
对象来定制业务
+ (NSURLRequest *)canonicalRequestForRequest: (NSURLRequest *)request;如果处理请求返回了
YES
,那么下面两个回调对应请求开始和结束阶段。在这里可以标记请求对象已经被处理过
- (void)startLoading;
- (void)stopLoading;
当发起网络请求的时候,系统会像注册过的NSURLProtocol
发起询问,判断是否需要处理修改该请求,通过一下代码来注册你的子类
[NSURLProtocol registerClass: [CustomURLProtocol class]];
DNS解析
一般情况下,考虑DNS劫持
大多发生在使用webView
的时候。相较于使用网页,正常的网络请求即便被劫持了无非是返回错误的数据、或者干脆404
,而且对付劫持,普通请求还有其他方案选择,所以本文讨论的是如何处理网页加载的劫持。
LocalDNS
LocalDNS
是一种常见的防劫持方案。简单来说,在网页发起请求的时候获取请求域名,然后在本地进行解析得到ip
,返回一个直接访问网页ip
地址的请求。结构体struct hostent
用来表示地址信息:
struct hostent {
char *h_name; // official name of host
char **h_aliases; // alias list
int h_addrtype; // host address type——AF_INET || AF_INET6
int h_length; // length of address
char **h_addr_list; // list of addresses
};
C函数gethostbyname
使用递归查询的方式将传入的域名转换成struct hostent
结构体,但是这个函数存在一个缺陷:由于采用递归方式查询域名,常常会发生超时。但是gethostbyname
本身不支持超时处理,所以这个函数调用的时候放到操作队列中执行,并且采用信号量等待1.5
秒查询:
+ (struct hostent *)getHostByName: (const char *)hostName {
__block struct hostent * phost = NULL;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSOperationQueue * queue = [NSOperationQueue new];
queue.maxConcurrentOperationCount = 1;
[queue addOperationWithBlock: ^{
phost = gethostbyname(hostName);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 1.5 * NSEC_PER_SEC));
[queue cancelAllOperations];
return phost;
}
然后通过函数inet_ntop
把结构体中的地址信息符号化,获得C字符串类型的地址信息。提供getIpAddressFromHostName
方法隐藏对ipv4
和ipv6
地址的处理细节:
+ (NSString *)getIpv4AddressFromHost: (NSString *)host {
const char * hostName = host.UTF8String;
struct hostent * phost = [self getHostByName: hostName];
if ( phost == NULL ) { return nil; }
struct in_addr ip_addr;
memcpy(&ip_addr, phost->h_addr_list[0], 4);
char ip[20] = { 0 };
inet_ntop(AF_INET, &ip_addr, ip, sizeof(ip));
return [NSString stringWithUTF8String: ip];
}
+ (NSString *)getIpv6AddressFromHost: (NSString *)host {
const char * hostName = host.UTF8String;
struct hostent * phost = [self getHostByName: hostName];
if ( phost == NULL ) { return nil; }
char ip[32] = { 0 };
char ** aliases;
switch (phost->h_addrtype) {
case AF_INET:
case AF_INET6: {
for (aliases = phost->h_addr_list; *aliases != NULL; aliases++) {
NSString * ipAddress = [NSString stringWithUTF8String: inet_ntop(phost->h_addrtype, *aliases, ip, sizeof(ip))];
if (ipAddress) { return ipAddress; }
}
} break;
default:
break;
}
return nil;
}
+ (NSString *)getIpAddressFromHostName: (NSString *)host {
NSString * ipAddress = [self getIpv4AddressFromHost: host];
if (ipAddress == nil) {
ipAddress = [self getIpv6AddressFromHost: host];
}
return ipAddress;
}
适配IPv6
苹果明确现在的的应用要支持IPv6
地址,对于开发者来说,并没有太大的改动,无非是将gethostbyname
改成另外一个函数:
phost = gethostbyname2(host, AF_INET6);
另外就是解析域名过程中优先获取IPv6
的地址而不是IPv4
:
+ (NSString *)getIpAddressFromHostName: (NSString *)host {
NSString * ipAddress = [self getIpv6AddressFromHost: host];
if (ipAddress == nil) {
ipAddress = [self getIpv4AddressFromHost: host];
}
return ipAddress;
}
扩展
localDNS
直接进行解析获取的ip
地址可能不是最优选择,另一种做法是让应用每次启动后从服务器下发对应的DNS
解析列表,直接从列表中获取ip
地址访问。这种做法对比递归式的查询,无疑效率要更高一些,需要注意的是在下发请求过程中如何避免解析列表被中间人篡改。
因为请求地址可能无效,需要以ip
映射host
的映射表来保证在访问无效的地址之后能重新使用原来的域名发起请求。另外确定ip
无效后应该维护一个无效地址表,用来域名解析后判断是否继续使用地址访问。整个域名解析过程大概如下:
此外,如果你的应用还没有服务器下发DNS
解析列表这一业务,那么直接使用Local DNS
解析可能会遇到解析出来的ip
无效问题。目前上面代码的处理是如果ip
无效,发起回调让webView
重新加载。除此之外有另外一种解决方案。应用本地存储一张需要访问到的域名表,然后在程序启动之后异步执行域名解析过程,参照DNS解析失败的处理 (支持IPv6)一文,提前做好无效解析的处理。
WebKit
WKWebView
是苹果推出的UIWebView
的替代方案,但前者还不够优秀以至于使用后者开发的大有人在。另外使用NSURLProtocol
实现防DNS劫持
功能的时候,在调起canInitWithRequest:
后就再无下文。通过查阅资料发现想实现WebKit
的请求拦截需要调用一些私有方法,让 WKWebView 支持 NSURLProtocol文章已经做了很好的处理,在文中的基础上,笔者对注册协议的过程多加了一层处理(毕竟苹果爸爸坑起我们来绝不手软):
static inline NSString * lxd_scheme_selector_suffix() {
return @"SchemeForCustomProtocol:";
}
static inline SEL lxd_register_scheme_selector() {
const NSString * const registerPrefix = @"register";
return NSSelectorFromString([registerPrefix stringByAppendingString: lxd_scheme_selector_suffix()]);
}
static inline SEL lxd_unregister_scheme_selector() {
const NSString * const unregisterPrefix = @"unregister";
return NSSelectorFromString([unregisterPrefix stringByAppendingString: lxd_scheme_selector_suffix()]);
}
NSURLSession
在AFNetworking
替换成NSURLSession
实现之后,常规的NSURLProtocol
已经不能拦截请求了。为了能继续实现拦截功能,需要在NSURLSessionConfiguration
中设置对拦截类的支持:
NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = @[LXDDNSInterceptor class];
由于AFNetworking
跟SDWebImage
都是采用默认的defaultSessionConfiguration
初始化请求会话对象的,因此直接hook
掉这个默认方法可以实现拦截适配:
+ (NSURLSessionConfiguration *)lxd_defaultSessionConfiguration {
NSURLSessionConfiguration * configuration = [self lxd_defaultSessionConfiguration];
configuration.protocolClasses = @[LXDDNSInterceptor class];
return configuration;
}
但是为了避免省字数出现[NSURLSessionConfiguration new]
的创建方式,hook
上面的方法并不能保证能够拦截到请求。于是我把hook
的目标放到了NSURLSession
上,发现存在一个类方法构造器生成实例:
+ (NSURLSession *)sessionWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id)delegate delegateQueue: (NSOperationQueue *)queue;
最开始是想hook
这个类方法,然而在class_getClassMethod
获取所有的方法列表输出之后发现竟然不存在这个类方法,取而代之的是一个init
构造器:
不知道这是不是苹果有意为之来误导开发者( 苹果:我是爸爸,规则我来定)。但是通过代码联想又无法直接输出这个函数,于是通过
category
的方式暴露这个方法名,并且
hook
掉:
/// h文件
@interface NSURLSession (LXDIntercept)
- (instancetype)initWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id)delegate delegateQueue: (NSOperationQueue *)queue;
@end
/// m文件
@implementation NSURLSession (LXDIntercept)
+ (void)load {
Method origin = class_getClassMethod([NSURLSession class], @selector(initWithConfiguration:delegate:delegateQueue:));
Method custom = class_getClassMethod([NSURLSession class], @selector(lxd_initWithConfiguration:delegate:delegateQueue:));
method_exchangeImplementations(origin, custom);
}
- (NSURLSession *)lxd_initWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id)delegate delegateQueue: (NSOperationQueue *)queue {
if (lxd_url_session_configure) {
lxd_url_session_configure(configuration);
}
return [self lxd_initWithConfiguration: configuration delegate: delegate delegateQueue: queue];
}
@end
于是,又能愉快的在项目里面玩耍网络拦截啦。
本文demo:LXDAppMonitor
参考资料
NSURLProtocol
iOS网络请求优化之DNS映射
iOS应用支持IPV6,就那点事儿
让 WKWebView 支持 NSURLProtocol