一種新的Android Runtime環境仿真及調試方法
現有的Android so庫調試方法
有次我在調試某Android so庫的時候,遇到一個很棘手的問題:如何更方便地對so庫進行調試?
常規的Android下so庫的調試方法是在Android環境里放一個gdbserver或者IDA的android_server,然后通過adb將端口轉出來,使用本機GDB或者IDA進行遠程連接調試。但是這種方法本身依賴于完整的Android環境、用起來比較麻煩且笨重不說,更重要的是這種方法無法實現一些比較高級的調試方式。比如在常規情況下,ARM Android環境里是無法設置內存斷點watchpoint的,更不用說受限于調試原理無法實現高效的trace記錄功能。
想要實現高效的trace,可以使用Frida(https://frida.re/)之類的DBI工具來進行插樁。Frida還提供了MemoryAccessMonitor API可以做內存監控,不過是通過修改頁的權限實現的,粒度比較大,實際情況下并不好用,并且難以對棧上的數據進行監控。
常規調試下watchpoint功能的受限及trace的低效是由于我們是使用軟件方式在用戶態進行操作,受到了CPU及操作系統的限制。而如果能在CPU層進行操作,便可以進行更加精細的控制。但我們無法修改CPU硬件,一些CPU層的相關功能(如Intel PT https://software.intel.com/content/www/us/en/develop/blogs/processor-tracing.html)功能上也比較受限并且難以使用。但還有另一個方法,便是使用軟件仿真的CPU。
軟件仿真的CPU最有名的莫過于QEMU(https://www.qemu.org/)了,通過它我們可以在一個CPU架構上仿真其他CPU架構的操作系統。但QEMU主要關注于仿真,對于安全分析來說并不友好。基于QEMU的PANDA框架(https://panda.re/)給我們提供了更多的相關接口,可以方便對特定位置做hook、進行插樁分析等。
PANDA曾經的1.x版本還集成了DroidScope可以對Android環境進行調試,不過在最新版中把這個功能去掉了。PANDA因為要用軟仿真實現的CPU運行完整操作系統,本身運行非常慢,但在PANDA中進行插樁操作由于相當于是在CPU層做的,帶來的額外開銷并不高。不過PANDA本身是全系統級的,雖然可以方便地對內核層進行分析,不過對于我們日常對用戶態程序分析來說還是太重了。
如果是進行一些簡單的仿真,目前最常用的是Unicorn引擎(https://www.unicorn-engine.org/)。Unicorn是把QEMU的CPU功能剝離出來,給我們提供一個裸的CPU運行接口,并在特定的hook位置添加了回調接口。
不過如果想要使用Unicorn運行二進制程序的話還需要考慮程序的內存加載以及系統調用實現的問題,不能直接使用。而對于我們的需求,針對Android so的仿真調試來說,可以使用基于Unicorn的AndroidNativeEmu(https://github.com/AeonLucid/AndroidNativeEmu)、ExAndroidNativeEmu(https://github.com/maiyao1988/ExAndroidNativeEmu)、以及基于多個后端的unidbg(https://github.com/zhkl0228/unidbg)等工具。
這幾款工具中,只有unidbg提供了GDB的調試接口,并且主要是為IDA進行連接設計的,在32位情況下無法使用GDB進行連接,也不支持watchpoint。另一方面,這幾款工具一個共有的缺陷是JNI中的函數是手工模擬實現的,難免存在實現不完全或者實現與真實情況有出入的情況。
前面這些方法都不能很好地滿足我對Android so進行仿真調試的需求,我希望能在X86環境下通過仿真方式啟起來一個完整的ARM Android Runtime環境,然后提供GDB接口可以連接上去進行相關調試。結合這個需求,一個新的基于Unicorn的仿真框架映入我的眼簾,那就Qiling仿真框架。
使用Qiling仿真框架運行Android Runtime
Qiling是一個高級的二進制仿真框架,并且它師出名門,是由Unicorn作者主導,主要解決的就是前面所提到的Unicorn無法直接運行二進制程序的問題。Qiling框架的關鍵就在于它完成了常規應該由操作系統來完成的一些事,如二進制程序內存加載、syscall功能提供,并且得益于Unicorn本身支持多種CPU架構的特性,使得Qiling成為了一個跨平臺、跨架構的相對完善的仿真框架。
作為一個框架,Qiling提供了一些接口和相關組件,其中一個很讓我激動的功能就是Qiling自帶一個GDB的遠程調試接口,可以使用本地GDB進行連接調試。
不過Qiling框架支不支持運行Android so呢?Qiling提供了一個樣例庫(https://github.com/qilingframework/rootfs/tree/76780c9e91471db1820b160d3b1d4a9ed6b13325),包含了不同操作系統、不同CPU架構的測試文件。我很高興地看到,其中有arm64_android這個文件夾,說明Qiling能運行ARM64架構的Android程序。不過我仔細研究才發現,這個樣例與我的需求還有一些距離。原因在于這個程序只是在控制臺打印了HelloWorld,并沒有涉及到JNI相關操作。
既然這樣的話,我們如果能給Qiling適配上對Android JNI的支持,就能滿足前面的需求。按說我們已經有了AndroidNativeEmu這類同樣基于Unicorn的Python項目,把其中的相關部分直接照搬進Qiling應該就可以了吧?
不過在我仔細研究后發現,Qiling框架與AndroidNativeEmu這類工具的底層原理是不同的。AndroidNativeEmu這類工具是模擬了Android下linker的功能,把Android so及相關依賴加載到內存里,并手工實現了JNI的相關接口。但為了做成一個更為通用的框架,Qiling模擬的是操作系統加載ld.so/linker之類動態鏈接器的過程,后續把程序及相關依賴加載進內存完全是由動態鏈接器來做的。
這就導致我們想用Qiling加載程序的話必須通過動態鏈接器完整啟動ELF程序,而不是像AndroidNativeEmu那樣鋪設好環境加載好so就直接可以去調用so中的指定函數了。Qiling的這種做法,以最小的成本保證了對各類各個版本的系統最大的適配性,并且也保證了程序運行狀態與真實環境差異較小。
由于JNI函數的實現都是在JVM里面,所以我們首先要解決的就是如何寫一個控制臺的ELF程序可以把JVM加載起來。
網上有篇Creating a Java VM from Android Native Code(https://calebfenton.github.io/2017/04/05/creating_java_vm_from_android_native_code)的文章就講了如何直接加載起來JVM。不過這篇文章還是加載的libdvm.so里面的Dalvik VM,對于今天來說未免太老了。
結合Stackoverflow上的一個提問及兩個網頁中的Github鏈接,最終形成了一個執行在Android命令行下的可以直接調起Android Runtime的程序,測試可以在Android 6.0下運行,并加載起來libart.so:
#include #include #include typedef int(*JNI_CreateJavaVM_t)(void *, void *, void *);JNIEXPORT void InitializeSignalChain () {}JNIEXPORT void ClaimSignalChain() {} int init_jvm(JavaVM **m_jvm, JNIEnv **m_env){ JavaVMOption opt[1]; opt[0].optionString = "-Xnorelocate"; JavaVMInitArgs args; args.version = JNI_VERSION_1_6; args.options = opt; args.nOptions = 1; void *libart_dso = dlopen("libart.so", RTLD_NOW); if (!libart_dso ) return -1; JNI_CreateJavaVM_t JNI_CreateJavaVM; JNI_CreateJavaVM = (JNI_CreateJavaVM_t)dlsym(libart_dso, "JNI_CreateJavaVM"); if (!JNI_CreateJavaVM) return -1; signed int result = JNI_CreateJavaVM(&(*m_jvm), &(*m_env), &args); if ( result != 0) return -1; return 0;} int main(){ JavaVM * vm = NULL; JNIEnv * env = NULL; int status = init_jvm(&vm, &env); if (status == 0) { printf("Initialization success (vm=%p, env=%p)", vm, env); } else { printf("Initialization failure (%i)", status); return -1; } jstring testy = (*env)->NewStringUTF(env, "Hello world!"); const char *str = (*env)->GetStringUTFChars(env, testy, NULL); printf("JNI: %s", str); return 0;}
可以通過Android NDK下的armv7a-linux-androideabi23-clang和aarch64-linux-android23-clang對該文件進行編譯:
/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi23-clang -Wl,--export-dynamic jniart.c -o arm_android_jniart /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android23-clang -Wl,--export-dynamic jniart.c -o arm64_android_jniart
現在我們要做的就是讓這個程序在Qiling下能運行起來。因為Qiling運行需要linker以及相關的庫,所以我們需要把Android的rootfs復制過來,然后執行腳本:
from qiling import *ql = Qiling(["android6.0/bin/arm64_android_jniart"], "android6.0")ql.run()
不過事情怎么會這么簡單,不出所料果然失敗了。
然后就是一番艱苦卓絕的分析調試過程,嘗試運行,找到運行不下去的地方,然后對Qiling的相關部分進行分析修復,然后程序在這個地方能跑通了,又到了下一個運行不下去的地方,再分析再修復。這其中主要發現的問題是Qiling的syscall實現還不完善,以及Qiling本身、甚至是Qiling所依賴的Unicorn引擎中存在著一些bug。
其中的具體細節就不多說了,我把所做的修復都提交到了上游,在這里(https://github.com/qilingframework/qiling/pulls?q=author%3Abet4it+created%3A%3C2021-11-19)可以看到我在這個過程中所有提交的PR。
在這個過程中我發現/dev/ashmem和/dev/pmsg0這兩個設備文件不好模擬,所以修改了AOSP中的 USE_ASHMEM 和 FAKE_LOG_DEVICE 兩個宏,重新編譯了不訪問這兩個文件的版本。
我們可以只編譯仿真所需要的部分:
make framework linker linker_32 libart libart_32 libstdc++ libstdc++_32
編譯生成的所有文件(rootfs):
├── bin│ ├── linker│ └── linker64├── framework│ ├── arm│ │ ├── boot.art│ │ └── boot.oat│ ├── arm64│ │ ├── boot.art│ │ └── boot.oat│ ├── framework.jar│ └── framework-res.apk├── lib│ ├── libart.so│ ├── libaudioutils.so│ ├── libbacktrace.so│ ├── libbase.so│ ├── libbinder.so│ ├── libcamera_client.so│ ├── libcamera_metadata.so│ ├── libcommon_time_client.so│ ├── libcrypto.so│ ├── libc++.so│ ├── libc.so│ ├── libcutils.so│ ├── libdl.so│ ├── libEGL.so│ ├── libexpat.so│ ├── libGLES_trace.so│ ├── libGLESv2.so│ ├── libGLESv3.so -> libGLESv2.so│ ├── libgui.so│ ├── libhardware.so│ ├── libicui18n.so│ ├── libicuuc.so│ ├── libjavacore.so│ ├── libjavacrypto.so│ ├── libkeymaster1.so│ ├── libkeymaster_messages.so│ ├── libkeystore_binder.so│ ├── libkeystore-engine.so│ ├── liblog.so│ ├── libmedia.so│ ├── libm.so│ ├── libnativebridge.so│ ├── libnativehelper.so│ ├── libnbaio.so│ ├── libpowermanager.so│ ├── libprotobuf-cpp-lite.so│ ├── librtp_jni.so│ ├── libsigchain.so│ ├── libsoftkeymasterdevice.so│ ├── libsonivox.so│ ├── libspeexresampler.so│ ├── libssl.so│ ├── libstagefright_amrnb_common.so│ ├── libstagefright_foundation.so│ ├── libstdc++.so│ ├── libsync.so│ ├── libui.so│ ├── libunwind.so│ ├── libutils.so│ └── libz.so├── lib64│ ├── libart.so│ ├── libaudioutils.so│ ├── libbacktrace.so│ ├── libbase.so│ ├── libbinder.so│ ├── libcamera_client.so│ ├── libcamera_metadata.so│ ├── libcommon_time_client.so│ ├── libcrypto.so│ ├── libc++.so│ ├── libc.so│ ├── libcutils.so│ ├── libdl.so│ ├── libEGL.so│ ├── libexpat.so│ ├── libGLES_trace.so│ ├── libGLESv2.so│ ├── libGLESv3.so -> libGLESv2.so│ ├── libgui.so│ ├── libhardware.so│ ├── libicui18n.so│ ├── libicuuc.so│ ├── libjavacore.so│ ├── libjavacrypto.so│ ├── libkeymaster1.so│ ├── libkeymaster_messages.so│ ├── libkeystore_binder.so│ ├── libkeystore-engine.so│ ├── liblog.so│ ├── libmedia.so│ ├── libm.so│ ├── libnativebridge.so│ ├── libnativehelper.so│ ├── libnbaio.so│ ├── libpowermanager.so│ ├── libprotobuf-cpp-lite.so│ ├── librtp_jni.so│ ├── libsigchain.so│ ├── libsoftkeymasterdevice.so│ ├── libsonivox.so│ ├── libspeexresampler.so│ ├── libssl.so│ ├── libstagefright_amrnb_common.so│ ├── libstagefright_foundation.so│ ├── libstdc++.so│ ├── libsync.so│ ├── libui.so│ ├── libunwind.so│ ├── libutils.so│ └── libz.so└── usr ├── icu │ └── icudt55l.dat └── share └── zoneinfo └── tzdata
運行腳本也需要進行一些相應的修改,最終版本腳本如下:
#!/usr/bin/env python3 from qiling import *from qiling.os.mapper import QlFsMappedObjectfrom collections import defaultdict class Fake_maps(QlFsMappedObject): def __init__(self, ql): self.ql = ql def read(self, size): stack = next(filter(lambda x : x[3]=='[stack]', self.ql.mem.map_info)) return ('%x-%x %s' % (stack[0], stack[1], stack[3])).encode() def fstat(self): return defaultdict(int) def close(self): return 0 if __name__ == "__main__": rootfs = "android6.0" test_binary = "android6.0/bin/arm64_android_jniart" env = {"ANDROID_DATA":"/data", "ANDROID_ROOT":"/system"} ql = Qiling([test_binary], rootfs, env, multithread = True) ql.add_fs_mapper("/proc/self/task/2000/maps", Fake_maps(ql)) ql.run()
目前所有的改動都已經同步到qiling官方倉庫,可以通過腳本test_android.py進行體驗。大家也可以選擇我fork的倉庫,里面自帶了運行所需的文件,可以直接進行測試,下載下來之后只需要運行python android.py便可看到創建完整ART環境的全過程(記得通過pip install -e .安裝相關依賴到最新版本)。
我們可以看到運行的過程中共啟動了6個線程,進行了上千次syscall調用,最終創建完成JavaVM和JNIEnv并調用JNI函數進行相關操作。
我們還可以打印出整個啟動過程中的內存加載情況:
Start End Perm Label00000012c00000 - 00000012c01000 rw- [syscall_mmap]00000012c01000 - 00000012e01000 rw- [syscall_mmap]00000012e01000 - 00000022c00000 --- [syscall_mmap]00000022c00000 - 00000022c01000 rw- [syscall_mmap]00000022c01000 - 00000032c00000 --- [syscall_mmap]00000032c00000 - 00000042c00000 rw- [syscall_mmap]00000070000000 - 00000070cf2000 rw- [mmap] boot.art00000070cf2000 - 00000072bcb000 r-- [mmap] boot.oat00000072bcb000 - 00000074b95000 r-x [mmap] boot.oat00000074b95000 - 00000074b96000 rw- [mmap] boot.oat00000074b96000 - 00000074b97000 rw- [syscall_mmap]00000074b97000 - 00000078b96000 --- [syscall_mmap]00555555554000 - 00555555555000 r-- arm64_android_jniart00555555555000 - 00555555556000 r-x arm64_android_jniart00555555556000 - 00555555557000 r-- arm64_android_jniart00555555557000 - 00555555558000 rw- arm64_android_jniart00555555558000 - 0055555555a000 rwx [hook_mem]007fffb7dd6000 - 007fffb7dd7000 --- [syscall_mmap]007fffb7dd7000 - 007fffb7ddb000 rw- [syscall_mmap]007fffb7ddb000 - 007fffb7ddc000 r-- [syscall_mmap]007fffb7ddc000 - 007fffb7ddd000 r-- [syscall_mmap]007fffb7ddd000 - 007fffb7dde000 rw- [syscall_mmap]007fffb7dde000 - 007fffb7ddf000 rw- [syscall_mmap]007fffb7ddf000 - 007fffb7de0000 rw- [syscall_mmap]007fffb7de0000 - 007fffb7de1000 r-- [syscall_mmap]007fffb7de6000 - 007fffb7ea9000 r-x [mmap] libc.so007fffb7ea9000 - 007fffb7eb8000 --- [syscall_mmap]007fffb7eb8000 - 007fffb7ebe000 r-- [mmap] libc.so007fffb7ebe000 - 007fffb7ec1000 rw- [mmap] libc.so007fffb7ec1000 - 007fffb7ecf000 rw- [syscall_mmap]007fffb7ed0000 - 007fffb7ed1000 rw- [syscall_mmap]007fffb7f40000 - 007fffb7f80000 rw- [syscall_mmap]007fffb7f91000 - 007fffb7f92000 r-- [syscall_mmap]007fffb8000000 - 007fffb8040000 rw- [syscall_mmap]007fffb8140000 - 007fffb8200000 rw- [syscall_mmap]007fffb8300000 - 007fffb83c0000 rw- [syscall_mmap]007fffb83d4000 - 007fffb89b5000 r-x [mmap] libart.so007fffb89b5000 - 007fffb89c5000 --- [syscall_mmap]007fffb89c5000 - 007fffb89d8000 r-- [mmap] libart.so007fffb89d8000 - 007fffb89da000 rw- [mmap] libart.so007fffb89da000 - 007fffb89dd000 rw- [syscall_mmap]007fffb89e2000 - 007fffb89ea000 r-x [mmap] libnativehelper.so007fffb89ea000 - 007fffb89f9000 --- [syscall_mmap]007fffb89f9000 - 007fffb89fa000 r-- [mmap] libnativehelper.so007fffb89fa000 - 007fffb89fb000 rw- [mmap] libnativehelper.so007fffb8a00000 - 007fffb8a03000 r-x [mmap] libnativebridge.so007fffb8a03000 - 007fffb8a12000 --- [syscall_mmap]007fffb8a12000 - 007fffb8a13000 r-- [mmap] libnativebridge.so007fffb8a13000 - 007fffb8a14000 rw- [mmap] libnativebridge.so007fffb8a19000 - 007fffb8a1a000 r-x [mmap] libsigchain.so007fffb8a1a000 - 007fffb8a29000 --- [syscall_mmap]007fffb8a29000 - 007fffb8a2a000 r-- [mmap] libsigchain.so007fffb8a2a000 - 007fffb8a2b000 rw- [mmap] libsigchain.so007fffb8a30000 - 007fffb8a3b000 r-x [mmap] libbacktrace.so007fffb8a3b000 - 007fffb8a4a000 --- [syscall_mmap]007fffb8a4a000 - 007fffb8a4c000 r-- [mmap] libbacktrace.so007fffb8a4c000 - 007fffb8a4d000 rw- [mmap] libbacktrace.so007fffb8a52000 - 007fffb8a75000 r-x [mmap] libutils.so007fffb8a75000 - 007fffb8a84000 --- [syscall_mmap]007fffb8a84000 - 007fffb8a86000 r-- [mmap] libutils.so007fffb8a86000 - 007fffb8a87000 rw- [mmap] libutils.so007fffb8a8c000 - 007fffb8a9d000 r-x [mmap] libcutils.so007fffb8a9d000 - 007fffb8aad000 --- [syscall_mmap]007fffb8aad000 - 007fffb8aae000 r-- [mmap] libcutils.so007fffb8aae000 - 007fffb8aaf000 rw- [mmap] libcutils.so007fffb8aaf000 - 007fffb8ab0000 r-- [syscall_mmap]007fffb8ab5000 - 007fffb8b8c000 r-x [mmap] libc++.so007fffb8b8c000 - 007fffb8b9b000 --- [syscall_mmap]007fffb8b9b000 - 007fffb8ba2000 r-- [mmap] libc++.so007fffb8ba2000 - 007fffb8ba3000 rw- [mmap] libc++.so007fffb8ba3000 - 007fffb8ba6000 rw- [syscall_mmap]007fffb8bab000 - 007fffb8be3000 r-x [mmap] libm.so007fffb8be3000 - 007fffb8bf3000 --- [syscall_mmap]007fffb8bf3000 - 007fffb8bf4000 r-- [mmap] libm.so007fffb8bf4000 - 007fffb8bf5000 rw- [mmap] libm.so007fffb8bfa000 - 007fffb8bff000 r-x [mmap] liblog.so007fffb8bff000 - 007fffb8c0e000 --- [syscall_mmap]007fffb8c0e000 - 007fffb8c0f000 r-- [mmap] liblog.so007fffb8c0f000 - 007fffb8c10000 rw- [mmap] liblog.so007fffb8c15000 - 007fffb8c1e000 r-x [mmap] libbase.so007fffb8c1e000 - 007fffb8c2d000 --- [syscall_mmap]007fffb8c2d000 - 007fffb8c2e000 r-- [mmap] libbase.so007fffb8c2e000 - 007fffb8c2f000 rw- [mmap] libbase.so007fffb8c34000 - 007fffb8c4f000 r-x [mmap] libunwind.so007fffb8c4f000 - 007fffb8c5e000 --- [syscall_mmap]007fffb8c5e000 - 007fffb8c5f000 r-- [mmap] libunwind.so007fffb8c5f000 - 007fffb8c60000 rw- [mmap] libunwind.so007fffb8c60000 - 007fffb8cc9000 rw- [syscall_mmap]007fffb8cca000 - 007fffb8ccb000 rw- [syscall_mmap]007fffb8d40000 - 007fffb8d80000 rw- [syscall_mmap]007fffb8e00000 - 007fffb8e40000 rw- [syscall_mmap]007fffb8e54000 - 007fffb8e7a000 r-- [mmap] boot.art007fffb8e7f000 - 007fffb8f7f000 rw- [syscall_mmap]007fffb8f7f000 - 007fffb907f000 rw- [syscall_mmap]007fffb907f000 - 007fffb908f000 rw- [syscall_mmap]007fffb908f000 - 007fffb948f000 rw- [syscall_mmap]007fffb948f000 - 007fffb988f000 rw- [syscall_mmap]007fffb988f000 - 007fffb989f000 rw- [syscall_mmap]007fffb989f000 - 007fffb9c9f000 rw- [syscall_mmap]007fffb9c9f000 - 007fffba09f000 rw- [syscall_mmap]007fffba09f000 - 007fffba0bf000 rw- [syscall_mmap]007fffba0bf000 - 007fffba0df000 rw- [syscall_mmap]007fffba0df000 - 007fffba15f000 rw- [syscall_mmap]007fffba15f000 - 007fffbae1f000 rw- [syscall_mmap]007fffbae1f000 - 007fffbae5f000 rw- [syscall_mmap]007fffbae5f000 - 007fffbb660000 rw- [syscall_mmap]007fffbb660000 - 007fffbbe61000 rw- [syscall_mmap]007fffbbe61000 - 007fffbbe63000 rw- [syscall_mmap]007fffbbe63000 - 007fffbbe65000 rw- [syscall_mmap]007fffbbe65000 - 007fffbbe67000 rw- [syscall_mmap]007fffbbe67000 - 007fffbbf2f000 rw- [syscall_mmap]007fffbbf2f000 - 007fffbbff7000 rw- [syscall_mmap]007fffbbff7000 - 007fffbbff9000 rw- [syscall_mmap]007fffbc040000 - 007fffbc080000 rw- [syscall_mmap]007fffbc0bd000 - 007fffbc10e000 r-x [mmap] libjavacore.so007fffbc10e000 - 007fffbc11e000 --- [syscall_mmap]007fffbc11e000 - 007fffbc120000 r-- [mmap] libjavacore.so007fffbc120000 - 007fffbc123000 rw- [mmap] libjavacore.so007fffbc123000 - 007fffbc124000 rw- [syscall_mmap]007fffbc129000 - 007fffbc22b000 r-x [mmap] libcrypto.so007fffbc22b000 - 007fffbc23a000 --- [syscall_mmap]007fffbc23a000 - 007fffbc24e000 r-- [mmap] libcrypto.so007fffbc24e000 - 007fffbc24f000 rw- [mmap] libcrypto.so007fffbc24f000 - 007fffbc250000 r-- [syscall_mmap]007fffbc255000 - 007fffbc276000 r-x [mmap] libexpat.so007fffbc276000 - 007fffbc286000 --- [syscall_mmap]007fffbc286000 - 007fffbc288000 r-- [mmap] libexpat.so007fffbc288000 - 007fffbc289000 rw- [mmap] libexpat.so007fffbc28e000 - 007fffbc411000 r-x [mmap] libicuuc.so007fffbc411000 - 007fffbc421000 --- [syscall_mmap]007fffbc421000 - 007fffbc433000 r-- [mmap] libicuuc.so007fffbc433000 - 007fffbc434000 rw- [mmap] libicuuc.so007fffbc434000 - 007fffbc438000 rw- [syscall_mmap]007fffbc43d000 - 007fffbc659000 r-x [mmap] libicui18n.so007fffbc659000 - 007fffbc669000 --- [syscall_mmap]007fffbc669000 - 007fffbc67d000 r-- [mmap] libicui18n.so007fffbc67d000 - 007fffbc67e000 rw- [mmap] libicui18n.so007fffbc683000 - 007fffbc69f000 r-x [mmap] libz.so007fffbc69f000 - 007fffbc6ae000 --- [syscall_mmap]007fffbc6ae000 - 007fffbc6af000 r-- [mmap] libz.so007fffbc6af000 - 007fffbc6b0000 rw- [mmap] libz.so007fffbc6b5000 - 007fffbc6b8000 r-x [mmap] libstdc++.so007fffbc6b8000 - 007fffbc6c7000 --- [syscall_mmap]007fffbc6c7000 - 007fffbc6c8000 r-- [mmap] libstdc++.so007fffbc6c8000 - 007fffbc6c9000 rw- [mmap] libstdc++.so007fffbc6ce000 - 007fffbc6ee000 rw- [syscall_mmap]007fffbc6ee000 - 007fffbdcee000 r-- [mmap] icudt55l.dat007fffbdcee000 - 007fffbdcef000 --- [syscall_mmap]007fffbdcef000 - 007fffbdcf0000 --- [syscall_mmap]007fffbdcf0000 - 007fffbdded000 rw- [syscall_mmap]007fffbdded000 - 007fffbddee000 --- [syscall_mmap]007fffbddee000 - 007fffbddf2000 rw- [syscall_mmap]007fffbde40000 - 007fffbde80000 rw- [syscall_mmap]007fffbdeb1000 - 007fffbdeb3000 rw- [syscall_mmap]007fffbdeb3000 - 007fffbdeb5000 rw- [syscall_mmap]007fffbdeb5000 - 007fffbdeb6000 --- [syscall_mmap]007fffbdeb6000 - 007fffbdeb7000 --- [syscall_mmap]007fffbdeb7000 - 007fffbdfba000 rw- [syscall_mmap]007fffbdfba000 - 007fffbdfbc000 rw- [syscall_mmap]007fffbdfbc000 - 007fffbdfbd000 --- [syscall_mmap]007fffbdfbd000 - 007fffbdfbe000 --- [syscall_mmap]007fffbdfbe000 - 007fffbe0c1000 rw- [syscall_mmap]007fffbe0c1000 - 007fffbe0c3000 rw- [syscall_mmap]007fffbe0c3000 - 007fffbe0c4000 --- [syscall_mmap]007fffbe0c4000 - 007fffbe0c5000 --- [syscall_mmap]007fffbe0c5000 - 007fffbe1c8000 rw- [syscall_mmap]007fffbe1c8000 - 007fffbe1ca000 rw- [syscall_mmap]007fffbe1ca000 - 007fffbe1cb000 --- [syscall_mmap]007fffbe1cb000 - 007fffbe1cc000 --- [syscall_mmap]007fffbe1cc000 - 007fffbe2cf000 rw- [syscall_mmap]007fffbe2cf000 - 007fffbe2d0000 --- [syscall_mmap]007fffbe2d0000 - 007fffbe2d4000 rw- [syscall_mmap]007fffbe2d4000 - 007fffbe2d5000 --- [syscall_mmap]007fffbe2d5000 - 007fffbe2d9000 rw- [syscall_mmap]007fffbe2d9000 - 007fffbe2da000 --- [syscall_mmap]007fffbe2da000 - 007fffbe2de000 rw- [syscall_mmap]007fffbe2de000 - 007fffbe2df000 --- [syscall_mmap]007fffbe2df000 - 007fffbe2e3000 rw- [syscall_mmap]007ffff7dd5000 - 007ffff7e0d000 r-x linker64007ffff7e1d000 - 007ffff7e1e000 r-- linker64007ffff7e1e000 - 007ffff7e24000 rw- linker64007ffffffde000 - 007ffffffdf000 --- [stack]007ffffffdf000 - 0080000000e000 rwx [stack]
完成這些之后,如果想要運行一個Android so,我們可以通過dlopen動態加載起來,通過dlsym獲取要運行的函數地址,然后將JNIEnv作為第一個參數傳進去運行。如果函數還需要其他什么參數的話,可以通過JNI進行創建。
我們這里提供的是一個完整的Android Runtime環境,基本不用擔心JNI實現不完全的問題(當然有可能有些JNI函數調用的syscall實現還不完全)。
總結:我這里通過Qiling模擬了啟動Android Runtime的全過程,展示了Qiling對復雜系統的仿真能力,也是對Qiling各項功能的充分驗證。
這里我選擇嘗試模擬執行Android 6.0環境,一方面是因為手頭有設備方便對照測試,一方面這是支持ART的比較早期的Android版本,相對來說代碼量比較小運行過程比較簡單,后面如果有誰有興趣的話可以嘗試對更高版本的Android環境進行仿真模擬。
目前運行Android so的方法還有些笨拙,后面如果有誰有興趣的話可以試著像Frida一樣在此基礎上添加Java接口,更加方便地進行相關調用。
對基于Unicorn的Qiling的新的調試方法
前面我們使用Qiling仿真框架運行了一個完整的Android Runtime環境,我們可以利用Qiling的一些功能對整個過程進行細致的分析,從而更加深入地理解Android Runtime的啟動過程。我們也可以對想要執行的Android so進行相關的分析。
如前面所說,選用Qiling框架一個比較重要的原因是它自帶一個GDB的遠程調試接口,那是不是就可以通過這個對Android Runtime進行調試呢?
然后我發現,事情并沒有想象的那么美好。 原因是,Qiling的GDB調試功能只能在單線程模式下使用(https://github.com/qilingframework/qiling/issues/722),而運行Android Runtime需要多線程的支持。
那是不是可以對Qiling做一些修改,使得GDB調試功能在多線程下也能使用?在我閱讀了Qiling相關代碼后,發現這一點并不好實現,因為目前的GDB調試功能與整個執行過程耦合度太高了,難以遷移至多線程模式。
這就有點尷尬了,本來做這些工作的一個很大目的就是能在跑起來之后對Android so進行調試的,費了這么大力氣跑起來了,卻沒辦法進行調試。
又想到目前基于Unicorn的項目如果想支持GDB遠程調試都要自己實現一套相關邏輯,那有沒有辦法給Unicorn提供一個通用的GDB調試功能呢?
說干就干!于是我就基于Unicorn自身提供的功能,實現了一套通用的GDB調試接口。不僅支持查看寄存器、查看內存、單步調試等基本操作,還支持了很多調試端都不支持的watchpoint功能,使得對數據的監控更加方便。
為了使得做出的項目更加通用,同時又能讓實現比較優雅,代碼通過Rust語言進行編寫,編譯成與C兼容的so文件,同時還提供了對Go、Java、Python等語言的接口。
這個項目的耦合度很低,只需要一個Code Hook便可以集成到已有的項目中。并且支持在正在運行的Unicorn實例的hook中進行調用,可以隨時隨地把調試接口啟起來。
項目的地址在這里:https://github.com/bet4it/udbserver
在安裝好so庫和python bindings后,只需要在ql.run()的前面加上兩句
from udbserver import udbserverudbserver(ql.uc, 1234, 0x5555555558D4)
便可以在運行程序main函數的時候開啟一個gdbserver,可以使用GDB遠程掛上去進行調試。
注意因為這個項目是基于Unicorn實現的,所以并不支持Qiling自己實現的多線程功能。不過也是可以用的,開啟udbserver的是哪個線程就對哪個線程進行控制。
我們同樣可以在其他項目中使用udbserver,比如對于ExAndroidNativeEmu中的example_jni.py(https://github.com/maiyao1988/ExAndroidNativeEmu/blob/master/example_jni.py),只需要加上兩句:
from udbserver import udbserverudbserver(emulator.mu, 1234, 0xcbbd2dec)
便可以開啟GDB遠程調試功能。
還可以搭配使用我之前寫的hyperpwn,獲得最佳的調試體驗。

