linux qt getpid,[QTA] Android 动态注入原理分析
一、前言
Android 的 UI 自動化測試可以通過注入式和非注入式分別實現,通過注入式可以更加方便地與應用進行交互。QTA 團隊提供的 Android UI 自動化測試框架QT4A, 是通過動態注入的方式來獲取被測應用的控件樹信息等,從而達到自動化測試的目的。本文主要介紹該動態注入的原理。
二、Android 動態注入概述
QT4A 中的動態注入是借助 ptrace 函數,該函數常用于斷點調試或系統調用跟蹤,由于其動態附著到遠程進程的特性,我們可以在 Android UI 自動化測試中加以利用。QT4A 框架中將測試樁 so 動態庫鏈接到被測應用進程空間,使得 so 中的函數在被測進程有對應地址,通過該地址即可在被測進程中調用 QT4A 的函數,與被測應用進行交互。
三、Android 動態注入條件限制
需要注意的是,通過 ptrace 函數雖然可以跟蹤進程,修改被跟蹤進程的內存和寄存器值,但正因其強大的能力,它也需在以下任一條件下滿足才能成功執行:
設備已越獄 (root)
設備未 root 情況下,只能注入具有相同 uid 的進程。在 Android 中,可以通過如下兩種方法達到:
重打包 QT4A so 到被測 apk 包中實現 ;
部分支持 run-as 命令的 Android 設備,也可以通過該命令切換到被測應用 uid 下再進行注入,該命令可用情況下則無需重打包。
QT4A 結合了這兩種方案實現非 root 下的動態注入。
四、Android 動態注入整體流程
為了方便看結果,我們以注入一個簡單的 so(hello.so) 為例,而不以 QT4A 真正的 so 為例。hello.so 主要包括了一個入口函數,主要代碼如下:
int hook_entry(char * a){
LOGD("Hook success, pid = %d\n", getpid());
LOGD("Hello %s\n", a);
return 0;
}
我們目標是將其注入到被測 Android 應用進程中,預期結果是在被測應用中輸出上述日志內容。整體注入流程圖如下:
首先通過 PTRACE_ATTACH 附著到遠程進程:
ptrace(PTRACE_ATTACH, pid, NULL, 0)
在開始加載我們的 so 之前,我們先把遠程進程的現場進行保護:
ptrace_getregs( target_pid, ®s )
如上,獲取遠程進程 (進程 id 為 target_pid) 的寄存器,然后將其保存到 original_regs 中:
memcpy( &original_regs, ®s, sizeof(regs) );
加載 hello.so 后可以恢復現場并解除進程跟蹤:
ptrace_setregs( target_pid, &original_regs );
ptrace_detach( target_pid );
接下來重點介紹如何加載 hello.so。
五、獲取遠程函數地址
由于 hello.so 不在遠程進程中,在遠程進程中并沒有 hello.so 相關的地址,要在遠程進程加載 hello.so,首先需要分配內存空間寫入 so,我們可以在遠程進程中調用 mmap 函數為 hello.so 分配內存空間,但只有知道了函數地址才能開始調用,如何獲取遠程進程中的 mmap 函數地址呢?本節以獲取 mmap 遠程函數地址為例說明如何獲取遠程函數地址。
5.1 mmap 遠程函數地址獲取公式
同一系統庫 (例如 mmap 所在的系統庫 libc.so) 的 mmap 地址與 libc.so 基地址的偏移量,在當前進程和遠程進程 (Android 應用) 中是相同的,所以,只要獲取到當前進程的 libc 基地址 (假設用變量 local_handle 表示)、當前進程 mmap 地址 (local_addr)、遠程進程 libc.so 基地址 (remote_handle),即可根據如下公式獲取遠程 mmap 地址 (remote_addr):
如上圖,可獲得公式:
local_addr - local_handle + = remote_addr - remote_handle,式子可轉化為remote_addr = local_addr + remote_handle - local_handle
接下來首先獲取 libc.so 基地址 (local_handle/remote_handle) 和當前進程 mmap 函數地址 (local_addr)。
5.2 獲取 libc.so 基地址
獲取進程中 libc.so 模塊基地址 (local_handle/remote_handle) 的方法為:
即在/proc/{pid}/maps路徑中找到模塊名,其中 pid 替換為目標進程的進程 id,對應的行首地址即為模塊的基地址。如果在當前進程中讀取當前進程的模塊基地址,可讀取/proc/self/maps路徑下的模塊地址即可。通過該方法可求得 local_handle/remote_handle 的值。
5.3 獲取當前進程 mmap 函數地址
獲取當前進程的 mmap 函數地址,有兩種方法:
方法一:通過 dlopen/dlsym 的方式獲取,如下圖:
方法二:根據 elf 文件內容格式獲取符號相對基地址的偏移量,加上當前進程中 libc.so 基地址,即可求得當前進程函數地址。實現在get_symbol_offset函數中,后續可詳細見開源后的源碼。
兩種方法可以結合調用,更為可靠,整體調用代碼如下:
/*
* 獲取當前進程中的函數地址
* 調用:void* local_mmap_addr = get_func_addr(libc_path, "mmap");
*/
void* get_func_addr(const char* module_path, const char* func_name) {
void* handle = dlopen(module_path, RTLD_NOW);
if(handle != NULL){
void* addr = dlsym(handle, func_name);
if(addr != NULL) return addr;
}
uint32_t addr = get_symbol_offset(module_path, func_name);
if(addr == 0) return NULL;
return get_module_base(-1, module_path) + addr;
}
其中get_symbol_offset讀取到了函數偏移值,get_module_base獲取了 libc.so 基地址 (詳細見《獲取 libc.so 基地址》一節),兩者相加即為當前進程 mmap 函數地址 (local_addr)。
至目前為止,根據公式remote_addr = local_addr + remote_handle - local_handle,我們知道了 local_handle/remote_handle/local_addr 三個變量的值,從而可求得遠程 mmap 地址 (remote_addr)。
類似的,其他遠程函數地址的獲取方法類似上述過程,區別在于函數所在的庫不同、函數名不同而已,后續不再贅述。
六、遠程進程函數調用
6.1 調用遠程函數 mmap 分配內存空間
通過上一節分析,可知遠程進程函數的地址獲取方法,然后開始調用遠程進程函數 mmap 分配內存空間,需要借助 ptrace 函數進行調用:
void* ret = ptrace_call( pid, remote_mmap_addr, parameters, 6, regs );
如上,傳入所需的參數,可調用 mmap 函數分配內存空間并返回分配的內存地址。ptrace_call 函數首先會將調用的函數 (mmap) 所需的參數 (parameters) 從右到左壓入堆棧,同時寫入返回地址到對應寄存器中,并同步修改棧頂指針。請注意,不同的 CPU 架構所用的寄存器和數據壓入方式有一定的差異,請按不同的 CPU 架構對應處理,這里總結了部分的差異:
將堆棧和寄存器值都設置完畢后,通過調用 ptrace 函數,并傳入參數PTRACE_CONT使 mmap 函數得以執行。
6.2 往 mmap 分配的內存空間寫入 hello.so 路徑和參數 int ptrace_writedata( pid_t pid, uint8_t *dest, uint8_t *data, size_t size )
該函數實現往地址中寫入字符串的功能,其中了利用 ptrace 函數提供的寫內存空間的方法,通過傳入參數PTRACE_POKETEXT及其他所需參數進行寫入,我們首先將 hello.so 路徑寫入 mmap 分配的內存空間中 (remote_memory)。同理,hello.so 中的入口函數(hook_entry) 如果需要傳入參數,也可通過這種方法寫入 remote_memory 中。更多細節請參考后續開源出來的源碼。而對應的,如果需要進行讀操作,則傳入參數PTRACE_PEEKTEXT及其他參數。
6.3 遠程進程中調用 hello.so 的函數
目前為止,我們已經在遠程進程中分配了內存,寫入了 hello.so 和其函數hook_entry的參數。而我們又可以通過《獲取遠程函數地址》一節的方法,獲取 hello.so 的函數地址,用變量 remote_func_addr 表示,接下來可以調用hook_entry函數:
ret = (long)ptrace_call( target_pid, remote_func_addr, parameters, param_size, ®s );
上述 ptrace_call 的函數的詳細過程參考《6.1 調用遠程函數 mmap 分配內存空間》一節。
調用結果如下圖:
可以看到,hello.so 中的hook_entry函數中的日志 (Hook success……) 在目標進程(2422)中打印出來了,證明我們的注入已成功。
七、總結
本文分析了 QT4A 所涉及的 Android 動態注入過程,QT4A 利用該過程注入 QT4A 測試樁到被測 Android 應用進程中,達到與應用通信的目的。整個注入過程比較關鍵的是獲取遠程函數地址和調用遠程函數。調用遠程函數需要首先通過 mmap 分配內存寫入待注入 so(hello.so) 和其函數所需參數,同時需要維護寄存器和堆棧狀態,不同 CPU 架構有所差別。
感興趣的同學可以加入 QQ 群和公眾號交流
如果你想要了解更多資訊,歡迎關注我們的微信公眾號
我們會定時向大家推送團隊同學分享的經驗文章哦。
總結
以上是生活随笔為你收集整理的linux qt getpid,[QTA] Android 动态注入原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 汽车车牌JS正则表达式验证(含新能源车牌
- 下一篇: thinkpadx1mdt 网络启动_二