質量監控-DNS劫持

前言

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方法隱藏對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

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

本文demo:LXDAppMonitor

參考資料

NSURLProtocol
iOS網絡請求優化之DNS映射
iOS應用支持IPV6,就那點事兒
讓 WKWebView 支持 NSURLProtocol

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容