1. 程式人生 > >DNS防劫持

DNS防劫持

DNS劫持指在劫持的網路範圍內攔截域名解析的請求,分析請求的域名,把審查範圍以外的請求放行,否則返回假的IP地址或者什麼都不做使請求失去響應。

檢測網站是否被劫持
域名是否被牆
DNS汙染檢測
網站開啟速度檢測
網站是否被黑
被入侵
被改標題
被掛黑鏈 網站劫持檢測

 

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方法隱藏對ipv4ipv6地址的處理細節:

 
    
    
+ (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];
 
   
  
 

由於AFNetworkingSDWebImage都是採用預設的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
 
   
  
 

於是,又能愉快的在專案裡面玩耍網路攔截啦。