# iOS开发之DNS劫持

语言：zh-CN
原文：[iOS开发之DNS劫持](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao75>)
中文：[中文 HTML](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao75>)
English: [English HTML](<https://allluckly.cn/en/journal/2017-05-03-dns-hijacking-and-request-handling-in-ios/>)
发布于: 2017-05-03T00:00:00+08:00
内容更新于: 2017-05-03T00:00:00+08:00
署名：SindriLin

从域名解析异常引出 DNS 劫持问题，介绍通过 NSURLProtocol 调整请求和解析流程，并讨论 IPv6、WebKit 与 NSURLSession 的适配边界，保留相关实现与参考资料。

原文署名SindriLin，Bison 编辑收录。

[原文所列来源](<http://sindrilin.com/apm/2017/03/31/DNS%E5%8A%AB%E6%8C%81>)

**阅读前** 这是 2015–2017 年间的技术记录，本次整理未重新运行验证示例。原文发表于 2017 年，网络拦截与解析适配方式需结合当时框架限制理解。

## 前言

`DNS劫持`指在劫持的网络范围内拦截域名解析的请求，分析请求的域名，把审查范围以外的请求放行，否则返回假的IP地址或者什么都不做使请求失去响应。

编辑：[Bison](<https://allluckly.cn/>)

来源：[SindriLin的小巢](<http://sindrilin.com/apm/2017/03/31/DNS%E5%8A%AB%E6%8C%81>)

![1](<https://allluckly.cn/images/blog/tuogao/tougao75/1.jpg>)

`DNS劫持`的主要表现为看视频，点击之后莫名其妙的跳到了某些广告网站。正常情况下，当我们点击某个链接的时候，会向一个称作`DNS服务器`的东西发出请求，把链接转换成机器能够识别的`ip`地址，其过程如下：

![1](<https://allluckly.cn/images/blog/tuogao/tougao75/2.jpg>)

域名-\>`ip`地址的过程被称作`DNS解析`。在这个过程中，由于`DNS`请求报文是明文状态，可能会在请求过程中被监测，然后攻击者伪装`DNS`服务器向主机发送带有假`ip`地址的响应报文，从而使得主机访问到假的服务器。

![1](<https://allluckly.cn/images/blog/tuogao/tougao75/3.jpg>)

## 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`方法隐藏对`ipv4`和`ipv6`地址的处理细节：

```
+ (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`无效后应该维护一个无效地址表，用来域名解析后判断是否继续使用地址访问。整个域名解析过程大概如下：

![1](<https://allluckly.cn/images/blog/tuogao/tougao75/4.jpg>)

此外，如果你的应用还没有服务器下发`DNS`解析列表这一业务，那么直接使用`Local DNS`解析可能会遇到解析出来的`ip`无效问题。目前上面代码的处理是如果`ip`无效，发起回调让`webView`重新加载。除此之外有另外一种解决方案。应用本地存储一张需要访问到的域名表，然后在程序启动之后异步执行域名解析过程，参照[DNS解析失败的处理 (支持IPv6)](<http://nszzy.me/2016/09/07/dns-resolving/>)一文，提前做好无效解析的处理。

## WebKit

`WKWebView`是苹果推出的`UIWebView`的替代方案，但前者还不够优秀以至于使用后者开发的大有人在。另外使用`NSURLProtocol`实现防`DNS劫持`功能的时候，在调起`canInitWithRequest:`后就再无下文。通过查阅资料发现想实现`WebKit`的请求拦截需要调用一些私有方法，[让 WKWebView 支持 NSURLProtocol](<https://blog.yeatse.com/2016/10/26/support-nsurlprotocol-in-wkwebview/>)文章已经做了很好的处理，在文中的基础上，笔者对注册协议的过程多加了一层处理（毕竟苹果爸爸坑起我们来绝不手软）：

```
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];
```

由于`AFNetworking`跟`SDWebImage`都是采用默认的`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;
```

5`hook`这个类方法，然而在`class_getClassMethod`获取所有的方法列表输出之后发现竟然不存在这个类方法，取而代之的是一个`init`构造器：

![1](<https://allluckly.cn/images/blog/tuogao/tougao75/5.jpg>)

不知道这是不是苹果有意为之来误导开发者（*苹果：我是爸爸，规则我来定*）。但是通过代码联想又无法直接输出这个函数，于是通过`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](<https://github.com/JustKeepRunning/LXDAppMonitor>)

## 参考资料

[NSURLProtocol](<http://nshipster.cn/nsurlprotocol/>) [iOS网络请求优化之DNS映射](<https://segmentfault.com/a/1190000004369289>) [iOS应用支持IPV6，就那点事儿](<http://blog.csdn.net/kaitiren/article/details/51503269>) [让 WKWebView 支持 NSURLProtocol](<https://blog.yeatse.com/2016/10/26/support-nsurlprotocol-in-wkwebview/>)

---

好文推荐：[分分钟解决iOS开发中App启动动画广告的功能](<https://allluckly.cn/lblaunchimagead/LBLaunchImageAd>)

