webview接入HttpDNS實踐

本文是對去年做的webview接入HttpDNS工作的一個總結,拖的時間有點久了。主要分享了GOT Hook webview中域名解析函數的方法。

HttpDNS簡介

首先簡單介紹下移動App接入HttpDNS後有什麼好處,這裏直接引用騰訊雲文檔中的說明:

HttpDNS是通過將移動APP及桌面應用的默認域名解析方式,替換爲通過Http協議進行域名解析,以規避由運營商Local DNS服務異常所導致的用戶網絡接入異常。減少用戶跨網訪問,降低用戶解析域名時延。

更詳細的內容可以參考這篇文章:【鵝廠網事】全局精確流量調度新思路-HttpDNS服務詳解

移動端的實現原理

域名的解析工作將在HttpDNS服務器上完成,客戶端只要把待解析的域名作爲參數發起一個HTTP請求,HttpDNS服務器就會把解析結果下發給客戶端了。
在客戶端,默認的域名解析是系統的getaddrinfo()庫函數實現的,默認的域名解析請求會走到LocalDNS。
所以域名解析的工作必須要交給app應用層來實現。下面介紹幾種實現方案。

1、okhttp

okhttp的實現是建立在socket之上的,並且實現了HTTP協議。對於客戶端發起的http請求,okhttp首先會跟遠端服務器建立socket連接,在此之前okhttp會根據http請求中url的domain做域名解析,默認的實現是java網絡庫提供的InetAddress.getAllByName

如果項目中用的網絡庫是okhttp,所有的網絡請求都是通過它完成的話就可以使用okhttp提供的DNS解析接口,實現自己的DNS resolver,代碼如下:

public class HttpDns implements Dns {
    @Override
    public List<InetAddress> lookup(String hostname) throws UnknownHostException {
        //DNSHelper完成DNS解析的具體工作,向HttpDNS服務器請求服務。
        String ip = DNSHelper.getIpByHost(hostname);
        List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
        return inetAddresses;
    }
}

2、native hook的方法

通過Hook libc的getaddrinfo庫函數,將函數指針指向app應用層實現的DNS解析函數地址。
要深入瞭解linux native hook的技術的話,需要了解ELF文件格式和動態鏈接的相關知識,可參考ELF文件及android hook原理

getaddrinfo是在libc.so中的定義的,其它庫如libandroid_runtime.solibjavacore.so要使用這個函數的話,只能通過動態導入符號的形式,好在java網絡庫底層是就是通過這個方式實現的。

android nativehook原理

下面代碼是arm架構的一種實現方案,具體實現參考Andrey Petrov的blog.

#include "linker.h"  // get it from bionic

unsigned elfhash(const char *_name); //hash函數
//查找散列表。經典的鏈接法解決散列衝突

static Elf32_Sym *soinfo_elf_lookup(soinfo *si, unsigned hash, const char *name)  
 {  
   Elf32_Sym *s;  
   Elf32_Sym *symtab = si->symtab;  
   const char *strtab = si->strtab;  
   unsigned n;  
   n = hash % si->nbucket;  
   for(n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n]){  
     s = symtab + n;  
     if(strcmp(strtab + s->st_name, name)) continue;  
       return s;  
     }  
   return NULL;  
 }  


 //soname:動態庫名稱;
 //symbol:待hook的函數名;
 //newval:新函數地址
 int hook_call(char *soname, char *symbol, unsigned newval) {  
  soinfo *si = NULL;  
  Elf32_Rel *rel = NULL;  
  Elf32_Sym *s = NULL;   
  unsigned int sym_offset = 0;  
  //打開動態庫,得到soinfo對象
  si = (soinfo*) dlopen(soname, 0);  
 //通過查找散列表找到symbol對應符號表的索引
  s = soinfo_elf_lookup(si, elfhash(symbol), symbol);  
  sym_offset = s - si->symtab;  

  rel = si->plt_rel;//指向plt表的起始位置  
  //遍歷plt表
  for (int i = 0; i < si->plt_rel_count; i++, rel++) {  
   unsigned type = ELF32_R_TYPE(rel->r_info);  
   unsigned sym = ELF32_R_SYM(rel->r_info);  
   //加上動態庫的基址,定位到該符號重定向元素的內存
   unsigned reloc = (unsigned)(rel->r_offset + si->base);  
   uint32_t page_size = 0;
   uint32_t entry_page_start = 0;
   unsigned oldval = 0;  
   if (sym_offset == sym) {  //是否是待hook的符號位置
    switch(type) {  
      case R_ARM_JUMP_SLOT:
         //修改內存頁的屬性爲可讀寫  
         page_size = getpagesize();
         entry_page_start = reloc& (~(page_size - 1));
         int ret = mprotect((uint32_t *)entry_page_start, page_size, PROT_READ | PROT_WRITE); 

         oldval = *(unsigned*) reloc;  
         *((unsigned*)reloc) = newval;  //成功替換這塊內存的值爲新函數的地址值 
         return 1;  
      default:  
         return 0;  
    }  
   }  
  }  
  return 0;  
 } 

程序中調用mprotect的作用是: 修改一段指定內存區域的保護屬性。以防萬一,將這它改爲可讀寫,因爲後面就要對這塊內存做寫操作了。
函數原型爲:int mprotect(const void *start, size_t len, int prot);
mprotect()函數把自start開始的、長度爲len的內存區的保護屬性修改爲prot指定的值。
需要指出的是,指定的內存區間必須包含整個內存頁(4K)。區間開始的地址start必須是一個內存頁的起始地址,並且區間長度len必須是頁大小的整數倍。

用法:

hook_call("libjavacore.so", "getaddrinfo", &my_getaddrinfo);  
  • 1.調用dlopen拿到so的句柄,得到soinfo,它包含了符號表、重定位表、plt表等信息。
  • 2.查找需要hook的函數的符號,得到它在符號表中的索引。
  • 3.遍歷plt表,直到匹配第2步中找到的符號索引。
    如果是JUMP_SLOT類型(函數調用),替換爲新的符號地址(函數指針)。
    如下圖所示,my_code_func的函數地址替換了GOT表項中原來指向libc中的getaddrinfo函數地址,達到了hook的效果。

跟進一步地,可以把設備上的libjavacore.so導出,用IDA Pro打開,觀察getaddrinfo的引用關係,將有助於理解上面的代碼。

找到libjavacore.sogetaddrinfo導入符號的位置:

定位到getaddrinfo在plt表中引用的位置:

定位到getaddrinfo在GOT表中引用的位置:

定位到在代碼段中調用getaddrinfo的位置:

通過分析得知,雖然getaddrinfolibc.so的導出函數,但是這種方法無法hook導出函數,沒有一勞永逸的方法,只能hook導入函數,因爲這種方案是通過修改GOT表項實現的,這是它的缺陷。

3、webview

webview作爲H5的容器,在做網絡請求的時候也需要做DNS域名解析,要對其接入HttpDNS的一般做法是通過攔截WebView的各類網絡請求,截取URL請求的host,然後調用HttpDns解析該host,通過得到的ip組成新的URL來請求網絡地址。
不過這種方式只能處理資源,處理正常的http/https請求會存在問題。

public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 
    if (request.getMethod().equalsIgnoreCase("get")) { 
        String url = request.getUrl().toString(); 
        // HttpDns解析css文件的網絡請求及圖片請求 
        if (url.contains(".css") || url.endsWith(".png")) { 
        try { 
            URL oldUrl = new URL(url); 
            URLConnection connection = oldUrl.openConnection(); 
            // 獲取HttpDns域名解析結果 
            String ips = MSDKDnsResolver.getInstance().getAddrByName(oldUrl.getHost()); 
            String newUrl = url.replaceFirst(oldUrl.getHost(), ip); 
            // 設置HTTP請求頭Host域 
            connection = (HttpURLConnection) new URL(newUrl).openConnection(); 
            connection.setRequestProperty("Host", oldUrl.getHost()); 
            }
            return new WebResourceResponse("text/css", "UTF-8", connection.getInputStream()); 
        }
    }}

必須要對webview的DNS域名解析函數進行攔截替換。
webview的DNS域名解析函數具體實現是在chromiumn.so,不同版本的實現也不同,5.0版本的代碼見host_resolver.h
webview的DNS域名解析函數是否也跟java的網絡庫一樣最終調用的libc.so動態庫中getaddrinfo呢?
通過源碼註釋得知確實如此。

用Android Studio調試Framework層代碼中也對其進行過斷點調試。
所以解決方法很簡單,只需要hook libchromium_net.sogetaddrinfo導入符號即可。

hook_call("libchromium_net.so", "getaddrinfo", &my_getaddrinfo);  

機型問題

在實踐中我們發現,不同機型不同版本的android在實現DNS解析函數的導出符號是不同的,更糟糕的是調用DNS解析函數的動態庫也不一定就是libjavacore.so
我之前定位過Android5.0設備的DNS解析函數,發現它的名字改爲android_getaddrinfofornet

webview的so庫位置也曾遇到過找不到的問題。

解決方法是通過一個腳本,pull下測試設備上的所有so到主機上,然後用readelf工具查找so的導入符號,觀察是否有getaddrinfo字樣的導入符號。
爲此我寫了一個腳本,方便自動化進行。運行如下命令即可

$ python sofinder.py -e getaddrinfo


在上面輸出的第一行可以看到,android 5.0以上版本webview的so已經被放在system/app目錄中了。
需要寫全so的路徑:

hook_call("/system/app/WebViewGoogle/lib/arm/libwebviewchromium.so", "getaddrinfo", &my_getaddrinfo);  

參考

【鵝廠網事】全局精確流量調度新思路-HttpDNS服務詳解
ELF文件及android hook原理
Andrey Petrov’s blog

我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=2pfca6dje52c0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章