質(zhì)量監(jiān)控-DNS劫持

前言

DNS劫持指在劫持的網(wǎng)絡范圍內(nèi)攔截域名解析的請求,分析請求的域名,把審查范圍以外的請求放行,否則返回假的IP地址或者什么都不做使請求失去響應。


DNS劫持的主要表現(xiàn)為看視頻,點擊之后莫名其妙的跳到了某些廣告網(wǎng)站。正常情況下,當我們點擊某個鏈接的時候,會向一個稱作DNS服務器的東西發(fā)出請求,把鏈接轉換成機器能夠識別的ip地址,其過程如下:

域名->ip地址的過程被稱作DNS解析。在這個過程中,由于DNS請求報文是明文狀態(tài),可能會在請求過程中被監(jiān)測,然后攻擊者偽裝DNS服務器向主機發(fā)送帶有假ip地址的響應報文,從而使得主機訪問到假的服務器。

NSURLProtocol

NSURLProtocol是蘋果提供給開發(fā)者的黑魔法之一,大部分的網(wǎng)絡請求都能被它攔截并且篡改,以此來改變URL的加載行為。這使得我們不必改動網(wǎng)絡請求的業(yè)務代碼,也能在需要的時候改變請求的細節(jié)。作為一個抽象類,我們必須繼承自NSURLProtocol才能實現(xiàn)中間攻擊的功能。

  • 是否要處理對應的請求。由于網(wǎng)頁存在動態(tài)鏈接的可能性,簡單的返回YES可能會創(chuàng)建大量的NSURLProtocol對象,因此我們需要保證每個請求能且僅能被返回一次YES
    + (BOOL)canInitWithRequest: (NSURLRequest *)request;
    + (BOOL)canInitWithTask: (NSURLSessionTask *)task;

  • 是否要對請求進行重定向,或者修改請求頭、域名等關鍵信息。返回一個新的NSURLRequest對象來定制業(yè)務
    + (NSURLRequest *)canonicalRequestForRequest: (NSURLRequest *)request;

  • 如果處理請求返回了YES,那么下面兩個回調(diào)對應請求開始和結束階段。在這里可以標記請求對象已經(jīng)被處理過
    - (void)startLoading;
    - (void)stopLoading;

當發(fā)起網(wǎng)絡請求的時候,系統(tǒng)會像注冊過的NSURLProtocol發(fā)起詢問,判斷是否需要處理修改該請求,通過一下代碼來注冊你的子類

[NSURLProtocol registerClass: [CustomURLProtocol class]];

DNS解析

一般情況下,考慮DNS劫持大多發(fā)生在使用webView的時候。相較于使用網(wǎng)頁,正常的網(wǎng)絡請求即便被劫持了無非是返回錯誤的數(shù)據(jù)、或者干脆404,而且對付劫持,普通請求還有其他方案選擇,所以本文討論的是如何處理網(wǎng)頁加載的劫持。

LocalDNS

LocalDNS是一種常見的防劫持方案。簡單來說,在網(wǎng)頁發(fā)起請求的時候獲取請求域名,然后在本地進行解析得到ip,返回一個直接訪問網(wǎng)頁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函數(shù)gethostbyname使用遞歸查詢的方式將傳入的域名轉換成struct hostent結構體,但是這個函數(shù)存在一個缺陷:由于采用遞歸方式查詢域名,常常會發(fā)生超時。但是gethostbyname本身不支持超時處理,所以這個函數(shù)調(diào)用的時候放到操作隊列中執(zhí)行,并且采用信號量等待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;
}

然后通過函數(shù)inet_ntop把結構體中的地址信息符號化,獲得C字符串類型的地址信息。提供getIpAddressFromHostName方法隱藏對ipv4ipv6地址的處理細節(jié):

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

蘋果明確現(xiàn)在的的應用要支持IPv6地址,對于開發(fā)者來說,并沒有太大的改動,無非是將gethostbyname改成另外一個函數(shù):

phost = gethostbyname2(host, AF_INET6);

另外就是解析域名過程中優(yōu)先獲取IPv6的地址而不是IPv4

+ (NSString *)getIpAddressFromHostName: (NSString *)host {
    NSString * ipAddress = [self getIpv6AddressFromHost: host];
    if (ipAddress == nil) {
        ipAddress = [self getIpv4AddressFromHost: host];
    }
    return ipAddress;
}

擴展

localDNS直接進行解析獲取的ip地址可能不是最優(yōu)選擇,另一種做法是讓應用每次啟動后從服務器下發(fā)對應的DNS解析列表,直接從列表中獲取ip地址訪問。這種做法對比遞歸式的查詢,無疑效率要更高一些,需要注意的是在下發(fā)請求過程中如何避免解析列表被中間人篡改。

因為請求地址可能無效,需要以ip映射host的映射表來保證在訪問無效的地址之后能重新使用原來的域名發(fā)起請求。另外確定ip無效后應該維護一個無效地址表,用來域名解析后判斷是否繼續(xù)使用地址訪問。整個域名解析過程大概如下:

此外,如果你的應用還沒有服務器下發(fā)DNS解析列表這一業(yè)務,那么直接使用Local DNS解析可能會遇到解析出來的ip無效問題。目前上面代碼的處理是如果ip無效,發(fā)起回調(diào)讓webView重新加載。除此之外有另外一種解決方案。應用本地存儲一張需要訪問到的域名表,然后在程序啟動之后異步執(zhí)行域名解析過程,參照DNS解析失敗的處理 (支持IPv6)一文,提前做好無效解析的處理。

WebKit

WKWebView是蘋果推出的UIWebView的替代方案,但前者還不夠優(yōu)秀以至于使用后者開發(fā)的大有人在。另外使用NSURLProtocol實現(xiàn)防DNS劫持功能的時候,在調(diào)起canInitWithRequest:后就再無下文。通過查閱資料發(fā)現(xiàn)想實現(xiàn)WebKit的請求攔截需要調(diào)用一些私有方法,讓 WKWebView 支持 NSURLProtocol文章已經(jīng)做了很好的處理,在文中的基礎上,筆者對注冊協(xié)議的過程多加了一層處理(畢竟蘋果爸爸坑起我們來絕不手軟):

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實現(xiàn)之后,常規(guī)的NSURLProtocol已經(jīng)不能攔截請求了。為了能繼續(xù)實現(xiàn)攔截功能,需要在NSURLSessionConfiguration中設置對攔截類的支持:

NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = @[LXDDNSInterceptor class];

由于AFNetworkingSDWebImage都是采用默認的defaultSessionConfiguration初始化請求會話對象的,因此直接hook掉這個默認方法可以實現(xiàn)攔截適配:

+ (NSURLSessionConfiguration *)lxd_defaultSessionConfiguration {
    NSURLSessionConfiguration * configuration = [self lxd_defaultSessionConfiguration];
    configuration.protocolClasses = @[LXDDNSInterceptor class];
    return configuration;
}

但是為了避免省字數(shù)出現(xiàn)[NSURLSessionConfiguration new]的創(chuàng)建方式,hook上面的方法并不能保證能夠攔截到請求。于是我把hook的目標放到了NSURLSession上,發(fā)現(xiàn)存在一個類方法構造器生成實例:

+ (NSURLSession *)sessionWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id<NSURLSessionDelegate>)delegate delegateQueue: (NSOperationQueue *)queue;

最開始是想hook這個類方法,然而在class_getClassMethod獲取所有的方法列表輸出之后發(fā)現(xiàn)竟然不存在這個類方法,取而代之的是一個init構造器:


不知道這是不是蘋果有意為之來誤導開發(fā)者(蘋果:我是爸爸,規(guī)則我來定)。但是通過代碼聯(lián)想又無法直接輸出這個函數(shù),于是通過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

于是,又能愉快的在項目里面玩耍網(wǎng)絡攔截啦。

本文demo:LXDAppMonitor

參考資料

NSURLProtocol
iOS網(wǎng)絡請求優(yōu)化之DNS映射
iOS應用支持IPV6,就那點事兒
讓 WKWebView 支持 NSURLProtocol

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • DNS(Domain Name System,域名系統(tǒng)),因特網(wǎng)上作為域名和IP地址相互映射的一個分布式數(shù)據(jù)庫,能...
    一直在努力hard閱讀 4,796評論 3 19
  • ******科普片** 1、DNS劫持的危害 不知道大家有沒有發(fā)現(xiàn)這樣一個現(xiàn)象,在打開一些網(wǎng)頁的時候會彈出一些與所...
    茉莉兒閱讀 32,078評論 84 217
  • 背景 前段時間在處理iOS端的HTTPDNS相關SDK,在接入和測試環(huán)節(jié)發(fā)現(xiàn)大家對HTTP的整體請求流程包括HTT...
    茉莉兒閱讀 3,163評論 5 16
  • 孩子是零基礎入小學的,對此,我對他的學業(yè)表現(xiàn)并沒有過高的期待。 比起那些學齡前就學習英語一兩年,或者上過拼音、識字...
    恰嘻貓和小虎閱讀 1,273評論 0 0
  • 前面我們討論了論題,和理由。那么,最后這一次,我們就來討論一下結論吧。 在我們的日常推理中,我們多半都會遇到從理由...
    QimuAI閱讀 652評論 0 1

友情鏈接更多精彩內(nèi)容