iOS监控:DNS劫持

2018阿里云全部产品优惠券(好东东,强烈推荐)
领取地址 https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=82lei0yp

前言

DNS劫持指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能反应或访问的是假网址。

DNS服务器的作用是将我们所能理解的域名解析成计算机直接读取的ip地址串,这个过程如上图所示。但是在这个解析的过程中,可能会发生域名劫持。由于DNS请求报文是明文状态,可能会在请求过程中被监测,然后攻击者伪装DNS服务器向主机发送带有假ip地址的响应报文,从而使得主机访问到假的服务器。

最常见的DNS劫持是笔者在看视频的时候,被劫持跳转到了某个广告页(万恶的运营商)。一般来说,对付网页劫持的方案我们通过NSURLProtocol来完成。

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,而且对付劫持,普通请求还有其他方案选择,所以本文讨论的是如何处理网页加载的劫持。

推荐:使用dnspod进行简单的HTTP dns解析(防劫持)

[https://www.dnspod.cn/httpdns/guidehttps://www.dnspod.cn/misc/D%2B免费版本接口说明.pdf最简单的get接口去请求dnspod提供的ip地址,请求格式为:http://119.29.29.29/

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 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;
}

扩展

localDNS直接进行解析获取的ip地址可能不是最优选择,另一种做法是让应用每次启动后从服务器下发对应的DNS解析列表,直接从列表中获取ip地址访问。这种做法对比递归式的查询,无疑效率要更高一些,需要注意的是在下发请求过程中如何避免解析列表被中间人篡改。

因为请求地址可能无效,需要以ip映射host的映射表来保证在访问无效的地址之后能重新使用原来的域名发起请求。另外确定ip无效后应该维护一个无效地址表,用来域名解析后判断是否继续使用地址访问。整个域名解析过程大概如下

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()]);
}

本文demo:LXDAppMonitor

参考资料

NSURLProtocol
iOS网络请求优化之DNS映射
iOS应用支持IPV6,就那点事儿
让 WKWebView 支持 NSURLProtocol

推荐:iOS开发秘笈:如何通过WebView监控提升WebAPP性能 - CloudwiseAPM

[相对于需要专业移动开发人员的原生应用(Native APP),基于HTML5/CSS/JavaScript的WebAPP凭借开发者门槛低、迭代迅速、支持跨平台发布等特点,成为电商、银行等网络服务

相关推荐