Android SO 高階黑盒利用

作者:小無名

預估稿費:500RMB

投稿方式:發送郵件至linwei#360.cn,或登陸網頁版在線投稿

前言

Android 的開發者喜歡將一些核心認證演算法寫在SO中,以此來增加黑客利用其業務的難度。比起DEX文件,SO確實有不少優勢。

優勢一:SO中為原生ARM彙編,難以還原原始代碼。DEX文件很容易被各種反編譯工具直接還原成通俗易懂的Java代碼。

優勢二:SO調試成本高,而Java寫的程序更容易被調試,如使用SmaliIdea、Jeb、IDA、Xposed、插樁打日誌等多種方式。

優勢三:SO難以在x86生產環境中黑盒調用,而DEX文件可轉換成class文件,在生產環境中使用JNI直接傳參調用。

綜上所述,如果APP的核心演算法採用C/C++編寫並編譯為SO文件,那麼可以稍微增加點難度。但是這種難度對於經驗豐富的逆向人員來說,僅僅是給他們增加點生活費。

實際上,SO還有各種保護,比如反調試、區塊加密、OLLVM混淆、ARM VMP。OLLVM混淆是逆向人員的噩夢,這招確實能有效提高SO代碼的安全性。ARM VMP 兼容性問題比較多,還無法商業化。

OLLVM混淆是噩夢,是本文的重點,那麼我們要怎麼利用被OLLVM混淆過的代碼呢? 筆者將介紹一種無需逆向並且能在x86生產環境高效利用SO的小竅門。我以前就不斷的思考,是否可以像調用jar文件那樣調用ARM的SO文件呢?

眾所周知,我們的目標SO一般有且只有ARM指令集的SO文件,如果有x86的SO文件,是可以直接黑盒利用。如果只有ARM指令集,可以採用本文介紹的方式進行黑盒利用或者採用逆向的方法直接還原源代碼。很顯然,對於Ollvm混淆過的程序來說,還原源代碼是相當困難的。

如何調用ARM版本的SO文件?SO文件就是一個黑盒子,我們無須知道其內部原理~

unicorn引擎

unicorn引擎是一款跨平台跨語言的CPU模擬庫,支持ARM,ARM64....所以我採用這個庫來調用難以逆向的SO文件。

unicorn git地址:github.com/unicorn-engi

unicorn庫是支持Windows編譯的,我是在Mac系統下完成的編譯。

編譯unicorn很簡單,在unicorn根目錄下執行./make.sh 即可,不是直接make。

默認情況下,make.sh腳本編譯的是動態庫,如果需要靜態庫的小夥伴可以在config.mk修改UNICORN_STATIC屬性為YES.

unicorn學習方法?unicorn開發者並不喜歡寫太多的開發文檔!所以你在README.md中可能僅僅只能看到如何編譯Unicorn,至於unicorn提供了哪些API、宏都是沒有說明的。實際上最好的開發文檔常在unicorn.h頭文件中,另外demo也很豐富。

有了unicorn,我們就有了一顆完全可以操控的虛擬ARM CPU,可以通過API實現寄存器讀寫、流程式控制制、內存映射、接管中斷等等,完全可以將SO的代碼載入到unicorn中去運行。

我們通過一段代碼學習unicorn基本使用方法。

#define THUMB_CODE "x83xb0" // sub sp, #0xcnnstatic void test_thumb(void)n{nuc_engine *uc;nuc_err err;nuc_hook trace1, trace2;nint sp = 0x1234; // R0 registernprintf("Emulate THUMB coden");n// 初始化模擬器為Thmub模式nerr = uc_open(UC_ARCH_ARM, UC_MODE_THUMB, &uc);nif (err) {nprintf("Failed on uc_open() with error returned: %u (%s)n",nerr, uc_strerror(err));nreturn;n}n// 給模擬器映射2M內存,UC_PROT_ALL為所有內存許可權(讀寫執行)nuc_mem_map(uc, ADDRESS, 2 * 1024 * 1024, UC_PROT_ALL);n// 填充機器碼到虛擬內存中。nuc_mem_write(uc, ADDRESS, THUMB_CODE, sizeof(THUMB_CODE) - 1);n// 初始化寄存器nuc_reg_write(uc, UC_ARM_REG_SP, &sp);n// 設置回調函數nuc_hook_add(uc, &trace1, UC_HOOK_BLOCK, hook_block, NULL, 1, 0);nn//設置一個指令執行回調用,該回調函數會在指令執行前被調用nuc_hook_add(uc, &trace2, UC_HOOK_CODE, hook_code, NULL, ADDRESS, ADDRESS);nn n// 開始運行虛擬CPU,因為是Thumb模式,所以地址的最低位需要置位。nerr = uc_emu_start(uc, ADDRESS | 1, ADDRESS + sizeof(THUMB_CODE) -1, 0, 0);nif (err) {nprintf("Failed on uc_emu_start() with error returned: %unerror:%sn", err,uc_strerror(err));n}n// 輸出結果寄存器nprintf(">>> Emulation done. Below is the CPU contextn");nuc_reg_read(uc, UC_ARM_REG_SP, &sp);nprintf(">>> SP = 0x%xn", sp);nuc_close(uc);n}n

上述代碼已經非常詳細介紹了unicorn大概使用方法,具體信息可以參考unicorn.h 中的注釋。從代碼可以看出,unicorn的控制粒度非常細,靈活的API可以讓我們掌控虛擬機的各種信息,特別是Unicorn的Hook功能(類似於異常處理機制,可以藉助這個實現斷點之類的)。

Unicorn 與 黑盒利用的關係

Unicorn 是一款很輕便的CPU虛擬庫,我將使用Unicorn來運行我難以逆向的演算法。

黑盒調用需要解決哪些問題?

1、SO裝載

2、內存映射

3、棧分配

4、API調用

5、返回值讀取

虛擬內存與真實內存

Unicorn 基於qemu,所以擁有完善的內存管理機制。 虛擬機內部內存和外部內存是完全隔離開的,也就是說虛擬機內訪問0x400不等於外部地址0x400。虛擬機中的內存最初情況是一片空白,需用調用uc_mem_map映射內存。

映射內存之後就需要通過uc_mem_write向虛擬機內存中寫入真實數據(比如代碼、參數等)。

注意:uc_mem_map的大小和基地址都需要4kb對齊,否者會映射失敗。

SO的裝載

我們需要寫一個Loader來載入要黑盒利用的SO嗎?完全沒有必要!最簡單的方法是將SO文件直接載入到內存中,然後在虛擬機中偏移為0處,映射一段能裝下這個SO文件的內存,最後將SO的數據拷貝到虛擬機內存。

這樣做有什麼好處呢?我們能直接利用IDA中的函數地址了!

如果想用Unicorn運行一個函數,那麼一定要解決的問題就是棧。解決方案是調用uc_mem_map映射一段固定內存地址作為運行棧。映射之後再將地址通過uc_reg_write寫到SP寄存器中。

參考代碼如下:

uint32_t sp = 0x10000; // 分配Stacknuint32_t sp_start = sp + 0x200;nuc_mem_map(uc,sp,sp_start - sp,UC_PROT_ALL);nuc_reg_write(uc,UC_ARM_REG_SP,&sp);n

有了棧才能保證SO的正常運行。

Unicorn 的回調機制

Unicorn 強大在於它擁有粒度極細的回調機制,大概有內存訪問前、後、訪問異常、代碼異常、代碼執行等等回調,所以功能比目前Linux or Windows的調試API還完善,通過Unicorn完完全全是控制CPU。

添加回調的函數是:uc_hook_add

不同類型的回調有不同類型的回調聲明,具體可以參考unicorn.h 這個頭文件。

本文主要介紹指令執行回調(類似於斷點功能),該回調會在指令執行前調用,我一般用來列印關鍵點上下文信息接管API調用

uc_hook trace1,trace2;nuc_hook_add(uc, &trace2, UC_HOOK_CODE, (void *)hook_code, NULL,0, function_start + function_size);nstatic void hook_code(uc_engine *uc, uint64_t address, uint32_t size, void *user_data)n{//address 為當前執行位置、size為指令長度,user——data可忽略nswitch(address)n{ncase 0x1234: // process... break;ncase 0x1223: break:ndefault:nxxxxxx;n}nset_breakpoint(uc,0x83f4); //如果address地址是83f4就輸出寄存器信息nset_breakpoint(uc,0x853a);n}n//////////////////////////////////////////////nstatic void set_breakpoint(uc_engine *uc,uint address)n{nuint pc;nuc_reg_read(uc,UC_ARM_REG_PC,&pc); if(pc == address)n{ printf("========================n"); printf("Break on 0x%xn",pc);nuint values;nuc_reg_read(uc,UC_ARM_REG_R0,&values); printf("R0 = 0x%x n",values);nuc_reg_read(uc,UC_ARM_REG_R1,&values); printf("R1 = 0x%x n",values);nuc_reg_read(uc,UC_ARM_REG_R2,&values); printf("R2 = 0x%x n",values);nuc_reg_read(uc,UC_ARM_REG_R3,&values); printf("R3 = 0x%x n",values);nuc_reg_read(uc,UC_ARM_REG_R4,&values); printf("R4 = 0x%x n",values);nuc_reg_read(uc,UC_ARM_REG_R5,&values); printf("R5 = 0x%x n",values); printf("========================n");n}n}n

上述代碼看出,通過在每條指令上設置執行回調來監控狀態,還能通過檢查CPU是否執行到我感興趣的地址(斷點地址)來輸出寄存器信息。其中的switch address是做什麼用的呢?這個主要是用於執行到bl 或 blx到其它動態庫代碼時接管處理。

接管API調用

如果我們想運行的程序會調用不屬於這個模塊的代碼的情況怎麼處理呢?比如調用JNI的函數或者libc的函數,怎麼辦呢?

完全虛擬一個這樣的環境當然沒問題,但是不划算。所以我採用HOOK的方式實現函數調用。我一般會提前確認目標函數調用了哪些外部的函數,做一個統計。一般外部的函數都是系統庫的函數,在網上能查到其用法。

在上一段代碼中已經提到,switch address是用來處理跨庫調用的,address一般為bl / blx 或plt中函數橋接的第一行地址。這樣我們能及時攔截到函數執行時,控制權交還外部回調代碼,在回調中根據不同函數實現不同功能。

比如我要處理JNI中GetStringUTFCharts這個函數,那麼我可能需要加一個斷點在blx r3這個位置,當程序執行到這裡時,會陷入回調。回調中根據address分流處理。比如這個函數就是轉換字元串,那麼我們就模擬完成這個操作,按效果來說,在vm中分配一段內存,然後寫入字元串,最後修改R0和PC寄存器即可。

防範建議

避免使用純演算法,增加核心演算法的上下文依賴,防止黑盒調用。


推薦閱讀:

如何評價Zealer安卓版客戶端?
OkHttp在安卓中的使用?
為什麼 iOS 和原生 Android 沒有文件管理的概念?
Windowsphone上第三方軟體(非官方)出品的軟體怎麼做?
怎麼可以快速的學會並掌握Android開發?

TAG:Android开发 | Android |