dns 劫持

dns 劫持

声明:转发整理 原地址 已贴入链接

访问 营运商 dns 服务器 遭到 ip 篡改 返回与请求 不符合的 网址内容

Android 网络优化,使用 HTTPDNS 优化 DNS,从原理到 OkHttp 集成

聊聊DNS,HTTPDNS

OkHttp接入HttpDNS,最佳实践

阿里云 HttpDns 接入指南

# Http 请求dns 劫持

解决方案:

  • HttpDns 服务器接入 「阿里云 收费 腾讯HttpDns 服务器免费(接入方案 七牛云 sdk)」

  • OkHttp HttpDns + 证书验证

# OkHttp HttpDns + 证书验证

OkHttp 是一个处理网络请求的开源项目,是 Android 端最火热的轻量级网络框架。在 OkHttp 中,默认是使用系统的 DNS 服务 InetAddress 进行域名解析

而想在 OkHttp 中使用 HTTPDNS,有两种方式。

  • 通过拦截器,在发送请求之前,将域名替换为 IP 地址。
  • 通过 OkHttp 提供的 .dns() 接口,配置 HTTPDNS。

对这两种方法来说,当然是推荐使用标准 API 来实现了。拦截器的方式,也建议有所了解,实现很简单,但是有坑。

# OkHttp 拦截器接入方式

拦截器是 OkHttp 中,非常强大的一种机制,它可以在请求和响应之间,做一些我们的定制操作。

在 OkHttp 中,可以通过实现 Interceptor 接口,来定制一个拦截器。使用时,只需要在 OkHttpClient.Builder 中,调用 addInterceptor() 方法来注册此拦截器即可。

class HTTPDNSInterceptor : Interceptor{
    override fun intercept(chain: Interceptor.Chain): Response {
        val originRequest = chain.request()
        val httpUrl = originRequest.url()

        val url = httpUrl.toString()
        val host = httpUrl.host()

        val hostIP = HttpDNS.getIpByHost(host)
        val builder = originRequest.newBuilder()

        if(hostIP!=null){
            builder.url(HttpDNS.getIpUrl(url,host,hostIP))
            builder.header("host",hostIP)
        }
        val newRequest = builder.build()
        val newResponse = chain.proceed(newRequest)
        return newResponse
    }
}

在拦截器中,使用 HttpDNS 这个帮助类,通过 getIpByHost() 将 Host 转为对应的 IP。

如果通过抓包工具抓包,你会发现,原本的类似 http://www.cxmydev.com/api/user 的请求,被替换为:http://220.181.57.xxx/api/user

拦截器接入的坏处:

使用拦截器,直接绕过了 DNS 的步骤,在请求发送前,将 Host 替换为对应的 IP 地址。

这种方案,在流程上很清晰,没有任何技术性的问题。但是这种方案存在一些问题,例如:HTTPS 下 IP 直连的证书问题、代理的问题、Cookie 的问题等等。

其中最严重的问题是,此方案(拦截器+HTTPDNS)遇到 https 时,如果存在一台服务器支持多个域名,可能导致证书无法匹配的问题。

在说到这个问题之前,就要先了解一下 HTTPS 和 SNI。

HTTPS 是为了保证安全的,在发送 HTTPS 请求之前,首先要进行 SSL/TLS 握手,握手的大致流程如下:

  1. 客户端发起握手请求,携带随机数、支持算法列表等参数。
  2. 服务端根据请求,选择合适的算法,下发公钥证书和随机数。
  3. 客户端对服务端证书,进行校验,并发送随机数信息,该信息使用公钥加密。
  4. 服务端通过私钥获取随机数信息。
  5. 双方根据以上交互的信息,生成 Session Ticket,用作该连接后续数据传输的加密密钥。

在这个流程中,客户端需要验证服务器下发的证书。首先通过本地保存的根证书解开证书链,确认证书可信任,然后客户端还需要检查证书的 domain 域和扩展域,看看是否包含本次请求的 HOST。

在这一步就出现了问题,当使用拦截器时,请求的 URL 中,HOST 会被替换成 HTTPDNS 解析出来的 IP。当服务器存在多域名和证书的情况下,服务器在建立 SSL/TLS 握手时,无法区分到底应该返回那个证书,此时的策略可能返回默认证书或者不返回,这就有可能导致客户端在证书验证 domain 时,出现不匹配的情况,最终导致 SSL/TLS 握手失败。

这就引发出来 SNI 方案,SNI(Server Name Indication)是为了解决一个服务器使用多个域名和证书的 SSL/TLS 扩展。

SNI 的工作原理,在连接到服务器建立 SSL 连接之前,先发送要访问站点的域名(hostname),服务器根据这个域名返回正确的证书。现在,大部分操作系统和浏览器,都已经很好的支持 SNI 扩展。

3. 拦截器 + HTTPDNS 的解决方案

这个问题,其实也有解决方案,这里简单介绍一下。

针对 “domain 不匹配” 的问题,可以通过 hook 证书验证过程中的第二步,将 IP 直接替换成原来的域名,再执行证书验证。

而 HttpURLConnect,提供了一个 HostnameVerifier 接口,实现它即可完成替换。

public interface HostnameVerifier {
    public boolean verify(String hostname, SSLSession session);
}

如果使用 OkHttp,可以参考 OkHostnameVerifier (source://src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java) 的实现,进行替换。

本身 OkHttp 就不建议通过拦截器去做 HTTPDNS 的支持,所以这里就不展开讨论了,这里只提出解决的思路,有兴趣可以研究研究源码

# OkHttp 标准 Api 接入

OkHttp 其实本身已经暴露了一个 Dns 接口,默认的实现是使用系统的 InetAddress 类,发送 UDP 请求进行 DNS 解析

我们只需要实现 OkHttp 的 Dns 接口,即可获得 HTTPDNS 的支持。

在我们实现的 Dns 接口实现类中,解析 DNS 的方式,换成 HTTPDNS,将解析结果返回。

class HttpDns : Dns {
    override fun lookup(hostname: String): List<InetAddress> {
        val ip = HttpDnsHelper.getIpByHost(hostname)
        if (!TextUtils.isEmpty(ip)) {
            //返回自己解析的地址列表
            return InetAddress.getAllByName(ip).toList() 
        } else {
            // 解析失败,使用系统解析
            return Dns.SYSTEM.lookup(hostname)
        }
    }
}

使用也非常的简单,在 OkHttp.build() 时,通过 dns() 方法配置。

mOkHttpClient = httpBuilder
        .dns(HttpDns())
        .build();

这样做的好处在于:

  • 还是用域名进行访问,只是底层 DNS 解析换成了 HTTPDNS,以确保解析的 IP 地址符合预期。

  • HTTPS 下的问题也得到解决,证书依然使用域名进行校验。

OkHttp 既然暴露出 dns 接口,我们就尽量使用它。

# WebView loadUrl() dns 劫持

Android Webview场景下防止dns劫持的探索

解决方案:

  • HttpDns

  • webViewClient 配置

  • 腾讯 x5 引擎 x5WebView 自带防劫持

# webView webViewClient

void setWebViewClient(WebViewClient client)
@SuppressLint("NewApi")
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {

            final String scheme = request.getUrl().getScheme().trim();
            final String url = request.getUrl().toString();
            final Map<String, String> headerFields = request.getRequestHeaders();

            // #1 只拦截get方法
            if (request.getMethod().equalsIgnoreCase("get") && (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
                try {
                    final URL oldUrl = new URL(url);
                    HttpURLConnection conn;

                    // #2 通过httpdns替换ip
                    final String ip = mService.getIpByHostAsync(oldUrl.getHost());
                    if (!TextUtils.isEmpty(ip)) {
                        final String host = oldUrl.getHost();
                        final String newUrl = url.replaceFirst(host, ip);

                        // #3 设置HTTP请求头Host域
                        conn = (HttpURLConnection) new URL(newUrl).openConnection();
                        conn.setRequestProperty("Host", host);

                        // #4 设置HTTP请求header
                        for (String header : headerFields.keySet()) {
                            conn.setRequestProperty(header, headerFields.get(header));
                        }

                        // #5 处理https场景
                        if (conn instanceof HttpsURLConnection) {
                            ((HttpsURLConnection) conn).setHostnameVerifier(new HostnameVerifier() {
                                @Override
                                public boolean verify(String hostname, SSLSession session) {
                                    return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
                                }
                            });
                        }

                        // #6 拿到MINE和encoding
                        final String contentType = conn.getContentType();
                        final String mine = getMine(contentType);
                        final String encoding = getEncoding(contentType);

                        // #7 MINE和encoding拿不到的情况下,不拦截
                        if (TextUtils.isEmpty(mine) || TextUtils.isEmpty(encoding)) {
                            return super.shouldInterceptRequest(view, request);
                        }

                        return new WebResourceResponse(mine, encoding, conn.getInputStream());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            return super.shouldInterceptRequest(view, request);
        }
### DNS劫持的概念 DNS劫持是一种网络攻击形式,其核心在于通过非法手段修改目标用户的DNS解析过程,使得原本合法的域名解析指向恶意控制的目标IP地址。这种行为可能导致用户访问到伪造网站或其他未经授权的服务[^1]。 ### 原理分析 DNS劫持主要利用了DNS协议本身的安全漏洞来实现攻击目的。具体来说: - **缺乏身份验证机制**:当一台DNS服务器向另一台DNS服务器发起查询时,并不会对响应方的身份进行严格验证。这为中间人攻击提供了机会——黑客能够伪装成合法的DNS服务器并返回虚假的数据记录。 - **缓存污染效应**:一旦某个DNS服务器接收到错误的信息并将之存储在其本地高速缓冲区中,则在此项数据的有效期限内,所有依赖于这个受感染节点执行名称转换操作的人都可能受到影响。这意味着后续针对相同主机名的所有请求都会获得已经被篡改过的IP地址作为回应结果。 ### 防范措施 为了有效抵御此类威胁,可以从以下几个方面着手加强防护力度: #### 使用安全扩展技术 部署支持DNSSEC(Domain Name System Security Extensions)的技术方案非常重要。它通过对资源记录集签名以及公钥基础设施的应用,确保了从权威源获取来的信息真实性得以保障的同时也增加了伪造难度[^3]。 #### 实施严格的ACL策略 管理员应该合理配置防火墙规则或者路由器上的访问控制列表(Access Control Lists),仅允许来自可信区域内的递归查询到达内部根提示文件所指定的位置;对外部不可信环境下的开放代理则采取更加谨慎的态度加以限制。 #### 定期更新软件版本 保持操作系统及其附属组件处于最新状态对于减少已知漏洞暴露至关重要。厂商通常会在补丁程序里修复发现的各种安全隐患,其中包括那些可能间接影响到DNS服务正常运作的部分。 #### 加密通信链路 采用TLS/SSL等方式保护客户端与上游转发器之间的传输路径免遭窃听或篡改变更是不可或缺的一环。例如DoT(DNS over TLS) 和 DoH (DNS over HTTPS), 这些新兴标准不仅提高了隐私水平还增强了整体抗干扰能力[^4]。 ```python import dns.resolver from dnspython import * def check_dnssec(domain_name): try: answers = dns.resolver.resolve(domain_name, 'A', raise_on_no_answer=False) if isinstance(answers.response, dns.message.Message): for answer in answers.response.answer: print(f"{domain_name} has the following A records:") for a_record in answer.items: print(a_record.address) # Check if there's RRSIG record which indicates DNSSEC usage. additional_section = answers.response.additional rrsig_found = any(isinstance(rrset, dns.rdtypes.ANY.RRSIG.RRSIG) for rrset in additional_section) return f"DNSSEC enabled: {rrsig_found}" except Exception as e: return str(e) if __name__ == "__main__": domain_to_check = "example.com" result = check_dnssec(domain_to_check) print(result) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值