Author: ThomasKing
Weibo/Twitter: ThomasKing2014
Blog: thomasking2014.com
一、序
無論是逆向分析還是漏洞利用,我所理解的攻防博弈無非是二者在既定的某一階段,以高緯的方式進行對抗,并不斷地升級緯度。比如,逆向工程人員一般會選擇在Root的環境下對App進行調試分析,其是以root的高權限對抗受沙盒限制的低權限;在arm64位手機上進行root/越獄時,ret2usr利用技術受到PXN機制的約束,廠商從修改硬件特性的高緯度進行對抗,迫使漏洞研究者提高利用技巧。
下文將在Android逆向工程方面,分享鄙人早期從維度攻擊的角度所編寫的小工具。工具本身可能已經不能適應現在的攻防,“授人以魚不如授人以漁”,希望能夠給各位讀者帶來一些思路,構建自己的分析利器。
二、正
0x00 自定義Loader
早期Android平臺對SO的保護采用畸形文件格式和內容加密的方式來對抗靜態分析。隨著IDA以及F5插件地不斷完善和增多,IDA已經成為了逆向人員的標配工具。正因如此,IDA成為了畸形文件格式的對抗目標。畸形方式從減少文件格式信息到構造促使IDA加載crash的變化正應證了這一點。對此,鄙人研究通過重建文件格式信息的方式來讓IDA正常加載。
在完成編寫修復重建工具不久之后,鄙人在一次使用IDA的加載bin文件時,猛然意識到畸形文件格式的對抗目標是IDA對ELF文件的加載的默認loader。既然防御的假象和維度僅僅在于默認loader,那么以自定義的loader加載實現高緯攻擊,理論是毫無敵手的。
那如何來實現IDA自定義loader呢?
- 以Segment加載的流程對ELF文件進行解析,獲取和重建Section信息(參看上面所說貼子)。
- 把文件信息在IDA中進行展示,直接調用對應的IDAPython接口
實現加載bin文件的py代碼見文末github鏈接,直接放置于IDA/loaders目錄即可。由于早期少有64位的安卓手機,加載腳本僅支持arm 32位格式,有興趣讀者可以改寫實現全平臺通用。不同ndk版本所編譯文件中與動態加載無關的Section不一定存在,注釋相應的重建代碼即可。
0x01 Kernel Helper
以APP分析為例,對于加固過的應用通常會對自身的運行環境進行檢測。比如: 檢測自身調試狀態,監控proc文件等。相信各位讀者有各種奇淫技巧來繞過,早期鄙人構建hook環境來繞過。從維度的角度,再來分析這種對抗。對于APP或者bin文件而言,其僅運行于受限的環境中,就算exp提權后也只是權限的提升和對內核有一定的訪問控制權。對于Android系統而言,逆向人員不僅能夠拿到root最高權限,而且還可以修改系統的所有代碼。從攻防雙方在運行環境的維度來看,“魔”比”道“高了不只三丈,防御方猶如板上魚肉。而在代碼維度,防御方擁有源代碼的控制權,攻防處于完全劣勢。隨著代碼混淆和VMP技術的運用,防御方這塊魚肉越來越不好"啃"。
對于基于linux的安卓系統而言,進程的運行環境和結構是由內核來提供和維護的。從修改內核的維度來對抗,能達到一些不錯的效果。下文將詳述在內核態dump目標進程內存和系統調用監控。
1. 內存DUMP
對內核添加一些自定義功能時,通常可以采用內核驅動來實現。雖然一部分Android手機支持驅動ko文件加載,但內核提供的其他工具則不一定已經編譯到內核,在后文中可以看到。nexus系列手機是谷歌官方所支持的,編譯刷機都比較方便,推薦使用。
S1. 編譯內核
為了讓內核支持驅動ko文件的加載,在make memuconfig配置內核選項時,以下勾選:
[*] Enable loadable module support
次級目錄所有選項
編譯步驟參看谷歌官方提供的內核編譯步驟。
S2. 驅動代碼
linux系統支持多種驅動設備,這里采用最簡單的字符設備來實現。與其他操作系統類似,linux驅動程序也分為入口和出口。在module_init入口中,對字符設備進行初始化,創建/dev/REHelper字符設備。文末代碼采用傳統的方式對字符設備進行注冊,也可直接使用misc的方式。字符設備的操作方式通過注冊file_operations回調實現,其中ioctl函數比較靈活,滿足實現需求。
定義command ID:
#define CMD_BASE 0xC0000000
#define DUMP_MEM (CMD_BASE + 1)
#define SET_PID (CMD_BASE + 2)
構建dump_request參數:
struct dump_request{
pid_t pid; //目標進程
unsigned long addr; //目標進程dump起始地址
ssize_t count; //dump的字節數
char __user *buf; //用戶空間存儲buf
};
在ioctl中實現分支:
case DUMP_MEM:
target_task = find_task_by_vpid(request->pid); //對于用戶態,進程通過進程的pid來標示自身;在內核空間,通過pid找到對應的進程結構task_struct
if(!target_task){
printk(KERN_INFO "find_task_by_vpid(%d) failed\n", request->pid);
ret = -ESRCH;
return ret;
}
request->count = mem_read(target_task->mm, request->buf, request->count, request->addr); //進程的虛擬地址空間同樣由內核進程管理,通過mm_struct結構組織
mem_read其實是對mem_rw函數的封裝,mem_rw能夠讀寫目標進程,簡略流程:
static ssize_t mem_rw(struct mm_struct *mm, char __user *buf,
size_t count, unsigned long addr, int write)
{
ssize_t copied;
char *page;
...
page = (char *)__get_free_page(GFP_TEMPORARY); // 獲取存儲數據的臨時頁面
...
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE);
// 將寫入數據從用戶空間拷貝到內核空間
if (write && copy_from_user(page, buf, this_len)) {
copied = -EFAULT;
break;
}
// 對目標進程進行讀或寫操作,具體實現參看內核源碼
this_len = access_remote_vm(mm, addr, page, this_len, write);
// 將獲取到的目標進程數據從內核拷貝到用戶空間
if (!write && copy_to_user(buf, page, this_len)) {
copied = -EFAULT;
break;
}
...
}
...
}
內核驅動部分的dump功能實現,接著只需在用戶空間訪問驅動程序即可。
// 構造ioctl參數
request.pid = atoi(argv[1]);
request.addr = 0x40000000;
request.buf = buf;
request.count = 1000;
// 打開內核驅動
int fd = open("/dev/REHelper", O_RDWR);
// 發送讀取命令
ioctl(fd, DUMP_MEM, &request);
close(fd);
S3. 測試
文末代碼中,dump_test為目標進程,dump_host通過內核驅動獲取目標進程的數據。insmod和dump_host以root權限運行即可。
2. 系統調用監控
通常情況下,APP通過動態鏈接庫libc.so間接的進行系統調用,直接在用戶態hook libc.so的函數即可實現監控。而對于靜態編譯的bin文件和通過svc匯編指令實現的系統調用,用戶態直接hook是不好處理的。道理很簡單,系統調用由內核實現,hook也應該在內核。
linux系統的系統調用功能統一存在syscall表中,syscall表通常編譯放在內核映像的代碼段,修改syscall表需要修改內核頁屬性,感興趣的讀者可以找到linux rootkit方面的資料。本文對系統調用監控的實現,采用內核從2.6支持的probe功能來實現,選用的最重要原因是:通用性。在不同abi平臺通過匯編實現系統調用的讀者應該知道,不同abi平臺的系統調用功能號并不一定相同,這就意味其在syscall表中的數組索引是不一致的,還需要額外的判定,實現并不優雅。
linux內核提供了kprobe、jprobe和kretprobe三種方式。限于篇幅,僅介紹利用jprobe實現系統調用監控。感興趣的讀者可以參看內核Documentation/kprobes.txt文檔以及samples目錄下的例子。
S1. 編譯選項
為了能夠支持probe功能,需在上述開啟驅動ko編譯選項的基礎上勾選kprobe選項。如果沒有開啟內核驅動選項,是不會有kprobes(new)選項的
General setup --->
[*] Kprobes(New)
S2. 驅動代碼
以監控sys_open系統調用為例。首先,在module_init函數中對調用register_jprobes進行注冊。注冊信息封裝在struct jprobe結構中。
static struct jprobe open_probe = {
.entry = jsys_open, //回調函數
.kp = {
.symbol_name = "sys_open", //系統調用名稱
},
};
由于系統調用為所有進程提供服務,不加入過濾信息會造成監控信息過多。回調函數的聲明和被監控系統調用的聲明一致。
asmlinkage int jsys_open(const char *pathname, int flags, mode_t mode){
pid_t current_pid = current_thread_info()->task->tgid;
// 從當前上下文中獲取進程的pid
// monitor_pid初始化-1,0為全局監控。
if(!monitor_pid || (current_pid == monitor_pid)){
printk(KERN_INFO "[open] pathname %s, flags: %x, mode: %x\n",
pathname, flags, mode);
}
jprobe_return();
return 0;
}
對monitor_pid的設置通過驅動的ioctl來設置,參數簡單直接設置。
case SET_PID:
monitor_pid = (pid_t) arg;
S3. 測試
文末代碼bin_wrapper和ptrace_trace均為靜態編譯,bin_wrapper通過設置監控對ptrace_trace的進行監控。內核prink的打印信息通過cat /proc/kmsg獲取,輸出類似如下:
<6>[34728.283575] REHelper device open success!
<6>[34728.285504] Set monitor pid: 3851
<6>[34728.287851] [openat] dirfd: -100, pathname /dev/__properties__, flags: a8000, mode: 0
<6>[34728.289348] [openat] dirfd: -100, pathname /proc/stat, flags: 20000, mode: 0
<6>[34728.291325] [openat] dirfd: -100, pathname /proc/self/status, flags: 20000, mode: 0
<6>[34728.292016] [inotify_add_watch]: fd: 4, pathname: /proc/self/mem, mask: 23
<6>[34729.296569] PTRACE_PEEKDATA: [src]pid = 3851 --> [dst]pid = 3852, addr: 40000000, data: be919e38
三、尾
本文介紹了鄙人對攻防的維度思考,以及從維度分析來實現的早期工具的部分介紹。希望能夠給各位讀者帶來一些幫助和思考。限于鄙人水平,難免會有疏漏或者錯誤之處,敬請各位指出,謝謝。
四、附
https://github.com/ThomasKing2014/ReverseTinytoolDemo
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/228/
暫無評論