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);
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章