前言
DNS劫持
指在劫持的網路範圍內攔截域名解析的請求,分析請求的域名,把審查範圍以外的請求放行,否則返回假的IP地址或者什麼都不做使請求失去響應,其效果就是對特定的網路不能反應或訪問的是假網址。
DNS伺服器
的作用是將我們所能理解的域名解析成計算機直接讀取的ip
地址串,這個過程如上圖所示。但是在這個解析的過程中,可能會發生域名劫持。由於DNS
請求報文是明文狀態,可能會在請求過程中被監測,然後攻擊者偽裝DNS
伺服器向主機傳送帶有假ip
地址的響應報文,從而使得主機訪問到假的伺服器。
最常見的DNS
劫持是筆者在看視訊的時候,被劫持跳轉到了某個廣告頁(萬惡的運營商)。一般來說,對付網頁劫持的方案我們通過NSURLProtocol
來完成。
NSURLProtocol
NSURLProtocol
是蘋果提供給開發者的黑魔法之一,大部分的網路請求都能被它攔截並且篡改,以此來改變URL的載入行為。這使得我們不必改動網路請求的業務程式碼,也能在需要的時候改變請求的細節。作為一個抽象類,我們必須繼承自NSURLProtocol
才能實現中間攻擊的功能。
- 是否要處理對應的請求。由於網頁存在動態連結的可能性,簡單的返回
YES
可能會建立大量的NSURLProtocol
物件,因此我們需要保證每個請求能且僅能被返回一次YES
12+ (BOOL)canInitWithRequest: (NSURLRequest *)request;+ (BOOL)canInitWithTask: (NSURLSessionTask *)task; - 是否要對請求進行重定向,或者修改請求頭、域名等關鍵資訊。返回一個新的
NSURLRequest
物件來定製業務
1+ (NSURLRequest *)canonicalRequestForRequest: (NSURLRequest *)request; - 如果處理請求返回了
YES
,那麼下面兩個回撥對應請求開始和結束階段。在這裡可以標記請求物件已經被處理過
12- (void)startLoading;- (void)stopLoading;
當發起網路請求的時候,系統會像註冊過的NSURLProtocol
發起詢問,判斷是否需要處理修改該請求,通過一下程式碼來註冊你的子類
1 |
[NSURLProtocol registerClass: [CustomURLProtocol class]]; |
DNS解析
一般情況下,考慮DNS劫持
大多發生在使用webView
的時候。相較於使用網頁,正常的網路請求即便被劫持了無非是返回錯誤的資料、或者乾脆404
,而且對付劫持,普通請求還有其他方案選擇,所以本文討論的是如何處理網頁載入的劫持。
LocalDNS
LocalDNS
是一種常見的防劫持方案。簡單來說,在網頁發起請求的時候獲取請求域名,然後在本地進行解析得到ip
,返回一個直接訪問網頁ip
地址的請求。結構體struct hostent
用來表示地址資訊:
1 2 3 4 5 6 7 |
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
秒查詢:
1 2 3 4 5 6 7 8 9 10 11 12 |
+ (struct hostent *)getHostByName: (const char *)hostName { __block struct hostent * phost = NULL; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); NSOperationQueue * queue = [NSOperationQueue new]; [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
地址的處理細節:
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 |
+ (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; } |
擴充套件
localDNS
直接進行解析獲取的ip
地址可能不是最優選擇,另一種做法是讓應用每次啟動後從伺服器下發對應的DNS
解析列表,直接從列表中獲取ip
地址訪問。這種做法對比遞迴式的查詢,無疑效率要更高一些,需要注意的是在下發請求過程中如何避免解析列表被中間人篡改。
因為請求地址可能無效,需要以ip
對映host
的對映表來保證在訪問無效的地址之後能重新使用原來的域名發起請求。另外確定ip
無效後應該維護一個無效地址表,用來域名解析後判斷是否繼續使用地址訪問。整個域名解析過程大概如下
WebKit
WKWebView
是蘋果推出的UIWebView
的替代方案,但前者還不夠優秀以至於使用後者開發的大有人在。另外使用NSURLProtocol
實現防DNS劫持
功能的時候,在調起canInitWithRequest:
後就再無下文。通過查閱資料發現想實現WebKit
的請求攔截需要呼叫一些私有方法,讓 WKWebView 支援 NSURLProtocol文章已經做了很好的處理,在文中的基礎上,筆者對註冊協議的過程多加了一層處理(畢竟蘋果爸爸坑起我們來絕不手軟):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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()]); } |
本文demo:LXDAppMonitor
參考資料
NSURLProtocol
iOS網路請求優化之DNS對映
iOS應用支援IPV6,就那點事兒
讓 WKWebView 支援 NSURLProtocol