通過 WebView 攻擊 Android 應用

WebView 可在應用中嵌入一個內置的 Web 瀏覽器,是 Android 應用開發常用的組件之一。通過 WebView 對 Android 應用的攻擊案例屢見不鮮,比如幾年前就被玩壞的 addJavascriptInterface 遠程代碼執行。但修復了 addJavascriptInterface 並不表示就能高枕無憂。應用在 WebView 上為 Javascript 提供的擴展介面,可能因為介面本身的問題而變成安全漏洞。

除此之外,在沒有啟用進程隔離的 WebView 與 App 具有相同許可權,獲得任意代碼執行後可以訪問應用私有數據或其他系統介面,可以將瀏覽器漏洞移植到手機平台上對應用進行針對性攻擊。部分廠商使用自行基於開源瀏覽器引擎 fork 而來的內核,也可能因為同步上游補丁代碼不及時而出現可利用的漏洞。

在 Android N 中增加了一個開發者選項,就是在所有的應用中將 WebView 的渲染進程運行在獨立的沙箱中。即使惡意網頁通過漏洞在渲染進程中執行了代碼,還需要更多的漏洞繞過沙箱的限制。這一特性將在 Android O 中默認啟用。但在這一緩解措施正式部署到大部分設備之前,通過攻擊 WebView 獲得遠程代碼執行進而直接攻擊應用仍然是可行的。

Beyond addJavascriptInterface

本文並不打算炒 addJavascriptInterface 的冷飯,而是關注在介面本身的實現上。

即使是使用了相對安全的通信手段(如 shouldOverrideUrlLoading 或 onJsAlert 之類回調的方案,或是其他基於類似方案的開源通信庫),如果應用介面設計不當,仍然存在被惡意頁面通過 js 執行任意代碼的可能。

利用可寫入的可執行文件

這一種攻擊方式需要結合兩種類型的漏洞,一是能在本地寫入路徑和內容可控的文件,二是應用中存在動態載入不可信代碼的邏輯。邏輯漏洞不涉及內存破壞,利用起來非常穩定。另外此類漏洞調用邏輯相對複雜,可能較難通過完全自動化的方式掃描識別。

在 Android 中因為開發者不嚴謹造成任意文件寫入的漏洞較為常見。首先是寫文件的介面可能本身設計上就允許傳入任意路徑的參數,另一種情況就是直接拼接路徑導致可以 「…/」 進行目錄穿越。

常見的場景有:

  • 下載遠程文件到指定的路徑
  • 解壓 zip 文件時未對 ZipEntry 文件名進行合法性檢查,可路徑穿越
  • 下載時未對 Content-Disposition: 進行合法性檢查,可路徑穿越

最後一個點比較少人注意到。Content Disposition 是常見的 HTTP 協議 header,在文件下載時可以告訴客戶端瀏覽器下載的文件名。例如伺服器返回 Content-Disposition: attachment; filename="cool.html" ,瀏覽器將彈出另存為對話框(或直接保存),默認的文件名就是 cool.html。

但這個 filename 參數顯然是不可信任的。例如惡意網站返回的文件名包含 ../,當 Android 應用嘗試將這個文件保存到 /sdcard/Downloads 時,攻擊者就有機會把文件寫入到 /data/ 目錄中了:

如果用戶不小心點擊確認下載,文件將會被寫入到指定的位置。這種攻擊甚至完全不需要 WebView 允許執行 Javascript(setJavaScriptEnabled(true)),只要簡單在 HTTP 伺服器中添加一個惡意 header 即可實現。

在寫入文件後便是代碼的載入。幾種常見的 Android 下動態載入可執行代碼的方式:

  • DexClassLoader 動態載入應用可寫入的 dex 可執行文件
  • java.lang.Runtime.exec 方法執行應用可寫入的 elf 文件
  • System.load 和 System.loadLibrary 動態載入應用可寫入的 elf 共享對象
  • 本地代碼使用 system、popen 等類似函數執行應用可寫入的 elf 文件
  • 本地代碼使用 dlopen 載入應用可寫入的 elf 共享對象
  • 利用 Multidex 機制:A Pattern for Remote Code Execution using Arbitrary File Writes and MultiDex Applications

如果應用動態載入代碼之前未做簽名校驗,利用存在任意文件寫入問題的 WebView 擴展介面進行覆蓋,可實現穩定的任意代碼執行。此外由於在文件系統中寫入了可執行文件,還可以實現持久化攻擊的效果。

SQLite 介面

部分應用為 WebView 提供了可執行任意 SQL 語句的擴展介面,允許打開和查詢文件名可控的資料庫;除此之外,在 WebKit 中有一個比較少用的 WebDatabase 功能,已被 W3C 標準廢棄,但 WebKit 和 Chromium 仍然保留了實現。SQLite3 中存在一些已知的攻擊面(如 load_extension 和 fts3_tokenizer 等),因此瀏覽器的 WebSQL 對 SQL 中可查詢的函數做了白名單限制。

但長亭安全實驗室發現,即使是瀏覽器白名單中的 SQLite3 函數依然存在可利用的安全性問題,最終可實現一套利用在 Chrome 和 Safari 兩大瀏覽器上通用的代碼執行。此漏洞被用於 2017 年 Pwn2Own 黑客大賽上攻擊 Safari 瀏覽器。此漏洞影響所有支持 WebDatabase 的瀏覽器(Windows、Linux、macOS、iOS、Android 上的 Chrome、Safari),包括多個 App 廠商基於 blink 或 WebKit 分支開發的瀏覽器引擎,影響數量非常可觀。漏洞目前已被 SQLite 和相關瀏覽器引擎修復。關於漏洞利用細節,長亭安全實驗室將在 BlackHat 大會上進行詳細講解:

blackhat.com/us-17/brie

即使是做了許可權限制的 WebDatabase 依然會出現問題,而我們不時可以看到一些應用直接將 SQLite 查詢介面不做任何限制就暴露給了 WebView。這意味著使用之前已知的攻擊方式(fts3_tokenizer、load_extension、attach 外部資料庫等)將可以結合腳本的能力得到充分利用。

一些應用允許通過參數打開指定文件名,實現上存在任意路徑拼接的漏洞。惡意頁面可以打開任意 App 沙盒目錄下任意資料庫進行查詢,將私有數據完全暴露給攻擊者。

為了安全以及實際開發工程量考慮,我們建議在開發混合應用時,如需為 HTML5 應用提供離線存儲能力,可直接使用 localStorage、IndexedDB 等 API。

其他可通過擴展介面觸發的問題

擴展介面在增強了 Web 內容的表現力的同時,也為應用增大了攻擊面。一些需要本地才能觸發的問題,如 Intent、ContentProvider 等,可以通過擴展介面提供的便利得以遠程利用。

例如,使用 js 喚起 Activity 是很常見的功能;開啟 setAllowContentAccess 後 WebView 可以通過 content:// 訪問 ContentProvider,甚至擴展介面本身提供了這樣的能力……這些原本需要本地安裝惡意應用,需要導出 Activity、ContentProvider 才能觸發的問題,可以被遠程調用了。

應用本身的實現也有可能存在命令注入、允許 js 訪問反射等安全問題。比如這篇文章介紹了某 Android 上的瀏覽器 App,存在任意文件寫入、SQL 注入、XSS 等問題,最終可以跨域獲取用戶信息、遠程執行代碼:d3adend.org/blog/?

應用開發者在做介面的時候,不僅需要小心避免代碼本身的安全漏洞,在 js 調用者的域上做好限制。

從 shellcode 到攻擊載荷

由於目前(< Android O)默認沒有啟用隔離進程的 WebView,將瀏覽器引擎的漏洞移植到 Android 平台來攻擊帶 WebView 的應用。多數瀏覽器引擎漏洞利用會最終執行一段 shellcode。不過僅僅反彈一個 shell 顯然不足以實現攻擊 App,還要有針對性地調用一些 Android 虛擬機運行時的特性。

例如通過 App 許可權讀取簡訊、聯繫人,或者需要解密應用自身使用的某個 SQLite 資料庫的內容,就需要使用 JNI 實現相應的邏輯。

載荷的載入

就攻擊特定應用的場景而言,將載荷完全使用 shellcode 甚至 ROP 並非不可能,但或多或少增加工作量。有一個 shell 之後可以做什麼?很容易想到下載一個可執行文件然後載入。Android 沒有自帶 wget 或 curl,除非用戶自行 root 並安裝 busybox。不過有 xxd 命令可以使用,使用 echo 和管道重定向的方式還是可以實現下載可執行文件的。

如果不想在文件系統留下痕迹,手工模擬動態鏈接、重定位 ELF,可在內存中直接載入可執行文件。BadKernel 是一個利用了 V8 上游已經修補,但未及時同步到第三方 fork 中的漏洞,攻擊某知名即時聊天應用的案例。在 BadKernel 的利用代碼 中,調用 JNI 查詢 ContentProvider 獲取簡訊的邏輯是單獨編譯到一個 so 中的。

在作者公開的利用代碼中,首先通過 javascript 任意地址讀寫,搜索一行調用 dlsym 的機器碼,從中解析出 dlopen@plt 的地址,再加上三條指令的長度獲得 dlsym@plt 的地址。觸發任意代碼執行時將這兩個函數指針傳入 shellcode,以進一步解析所需的各種符號。最後進入 shellcode 中實現的簡化版 linker,直接將 ELF 文件內容放在 RWX 內存中重定位處理後,執行其 so_main 導出函數。

JNI 基礎

Android 中 JVM 和 C/C++ 開發的本地代碼互相調用,可以使用 JNI(Java Native Interface)。在 System.loadLibrary 載入一個動態鏈接庫之後,JVM 會調用 ELF 中導出的 JNI_OnLoad(JavaVM *jvm, void *reserved) 函數,在這裡可以做一些初始化的工作,以及使用 JNIEnv 的 RegisterNatives 方法動態將 Java 方法與本地代碼綁定。

本地代碼為 JNI 提供的方法的第一個參數是 JNIEnv 的指針,通過這個上下文可以訪問 JVM 當前載入的類,通過反射機制調用 Java 層的功能。例如如下 Java 代碼:

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MicroMsg/CompatibleInfo.cfg"));HashMap<Integer, String> hashMap = (HashMap<Integer, String>)ois.readObject();String deviceId = hashMap.get(Integer.valueOf(258));

使用 JNI 實現如下:

char *id = (char*)malloc(64);jstring filename = (*env)->NewStringUTF(env, "MicroMsg/CompatibleInfo.cfg");jclass clsFileInputStream = (*env)->FindClass(env, "java/io/FileInputStream");jclass clsObjectInputStream = (*env)->FindClass(env, "java/io/ObjectInputStream");jclass clsHashMap = (*env)->FindClass(env, "java/util/HashMap");jmethodID constructor = (*env)->GetMethodID(env, clsFileInputStream, "<init>", "(Ljava/lang/String;)V");jobject fileInputStream = (*env)->NewObject(env, clsFileInputStream, constructor, filename);constructor = (*env)->GetMethodID(env, clsObjectInputStream, "<init>", "(Ljava/io/InputStream;)V");jobject objInputStream = (*env)->NewObject(env, clsObjectInputStream, constructor, fileInputStream);jmethodID readObject = (*env)->GetMethodID(env, clsObjectInputStream, "readObject", "()Ljava/lang/Object;");jobject hashmap = (*env)->CallObjectMethod(env, objInputStream, readObject);// cast to hash mapjmethodID get = (*env)->GetMethodID(env, clsHashMap, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");jmethodID toString = (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Object"), "toString", "()Ljava/lang/String;");jclass clsInteger = (*env)->FindClass(env, "java/lang/Integer");jmethodID valueOf = (*env)->GetStaticMethodID(env, clsInteger, "valueOf", "(I)Ljava/lang/Integer;");jobject key = (*env)->CallStaticObjectMethod(env, clsInteger, valueOf, 258);jstring val = (*env)->CallObjectMethod(env, hashmap, get, key);strncpy(id, (*env)->GetStringUTFChars(env, val, 0), len);

正常情況下,JNIEnv 是系統初始化並傳給 native 方法的。但在開發利用載荷的時候不是使用標準的方式載入鏈接庫,因此需要使用一些私有 API。如果代碼直接運行在 App 進程中,可通過 android::AndroidRuntime::getJNIEnv 直接獲取,或者 JNI_GetCreatedJavaVMs 獲得當前進程的唯一 JVM 實例後調用其 GetEnv 方法。如果使用獨立的可執行文件,可通過 JNI_CreateJavaVM 創建一個新的 JVM。

Android 調用 JVM 的一些問題

Android N 對 NDK 鏈接的行為做了變更,禁止鏈接到私有 API,包括上文提到的 JVM 相關函數。一個非常簡單的繞過方式是向 dlopen 傳入空指針作為的文件名,dlsym 將會在所有已載入的共享對象中查找符號。

typedef jint (JNICALL *GetCreatedJavaVMs)(JavaVM **, jsize, jsize *);void *handle = dlopen(NULL, RTLD_NOW);GetCreatedJavaVMs JNI_GetCreatedJavaVMs = (GetCreatedJavaVMs) dlsym(handle, "JNI_GetCreatedJavaVMs");

另外一個坑是,在 ART 下,一個可執行文件如果要調用 JNI_CreateJavaVM 創建 JVM,那麼它必須導出 InitializeSignalChain、ClaimSignalChain、UnclaimSignalChain、InvokeUserSignalHandler、EnsureFrontOfChain 這幾個回調函數,否則會在 logcat 里看到大量類似

"InitializeSignalChain is not exported by the main executable." 的提示,然後 SIGABRT。

AOSP 對應的代碼如下,可以看到在輸出這行日誌之後就會調用 abort():

android.googlesource.com

解決方案非常簡單,只要在源文件里創建這幾個對應的函數,代碼留空,然後加上 JNIEXPORT 宏設置為導出符號即可:

JNIEXPORT void InitializeSignalChain() { }JNIEXPORT void ClaimSignalChain() { }JNIEXPORT void UnclaimSignalChain() { }JNIEXPORT void InvokeUserSignalHandler() { }JNIEXPORT void EnsureFrontOfChain() { }

小結

WebView 在 Android 應用開發中應用廣泛,功能複雜,是頗為理想的攻擊面。點開一個鏈接或者掃描一個二維碼就會執行惡意代碼並不僅僅是都市傳說。開發者在使用 WebView 的時候不僅要注意老生常談的各種 getSettings()、javascriptInterface 點,還要注意防範通過擴展介面暴露的攻擊面和安全問題。

參考資料

  1. developer.mozilla.org/e
  2. nowsecure.com/blog/2015
  3. d3adend.org/blog/?
  4. docs.oracle.com/javase/
  5. github.com/secmob/BadKe
  6. android.googlesource.com

推薦閱讀:

手機安全軟體給你留下了哪些感受?
安全意識和安全思維是什麼,如何培養?

TAG:移动安全 | Android | 黑客Hacker |