前言
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<NSURLSessionDelegate>)delegate delegateQueue: (NSOperationQueue *)queue;
最開始是想hook
這個類方法,然而在class_getClassMethod
獲取所有的方法列表輸出之后發現竟然不存在這個類方法,取而代之的是一個init
構造器:
不知道這是不是蘋果有意為之來誤導開發者(蘋果:我是爸爸,規則我來定)。但是通過代碼聯想又無法直接輸出這個函數,于是通過
category
的方式暴露這個方法名,并且hook
掉:
/// h文件
@interface NSURLSession (LXDIntercept)
- (instancetype)initWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id<NSURLSessionDelegate>)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<NSURLSessionDelegate>)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