不管是逆向分析仍是漏洞利用,我所理解的攻防博弈無非是兩者在既定的某一階段,以高維的方式進行對抗,並不斷地升級維度。好比,逆向工程人員通常會選擇在Root的環境下對App進行調試分析,其是以root的高權限對抗受沙盒限制的低權限;在arm64位手機上進行root/越獄時,ret2usr利用技術受到PXN機制的約束,廠商從修改硬件特性的高維度進行對抗,迫使漏洞研究者提升利用技巧。linux
下文將在Android逆向工程方面,分享鄙人早期從維度攻擊的角度所編寫的小工具。工具自己可能已經不能適應如今的攻防,「授人以魚不如授人以漁」,但願可以給各位讀者帶來一些思路,構建本身的分析利器。git
早期Android平臺對SO的保護採用畸形文件格式和內容加密的方式來對抗靜態分析。隨着IDA以及F5插件地不斷完善和增多,IDA已經成爲了逆向人員的標配工具。正因如此,IDA成爲了畸形文件格式的對抗目標。畸形方式從減小文件格式信息到構造促使IDA加載crash的變化正應證了這一點。對此,鄙人研究經過重建文件格式信息的方式來讓IDA正常加載。github
在完成編寫修復重建工具不久以後,鄙人在一次使用IDA的加載bin文件時,猛然意識到畸形文件格式的對抗目標是IDA對ELF文件的加載的默認loader。既然防護的假象和維度僅僅在於默認loader,那麼以自定義的loader加載實現高維攻擊,理論是毫無敵手的。數組
那如何來實現IDA自定義loader呢?安全
以Segment加載的流程對ELF文件進行解析,獲取和重建Section信息(參看上面所說貼子)。
把文件信息在IDA中進行展現,直接調用對應的IDAPython接口app
實現加載bin文件的py代碼見文末github連接,直接放置於IDA/loaders目錄便可。因爲早期少有64位的安卓手機,加載腳本僅支持arm 32位格式,有興趣讀者能夠改寫實現全平臺通用。不一樣ndk版本所編譯文件中與動態加載無關的Section不必定存在,註釋相應的重建代碼便可。函數
以APP分析爲例,對於加固過的應用一般會對自身的運行環境進行檢測。好比: 檢測自身調試狀態,監控proc文件等。相信各位讀者有各類奇淫技巧來繞過,早期鄙人構建hook環境來繞過。從維度的角度,再來分析這種對抗。對於APP或者bin文件而言,其僅運行於受限的環境中,就算exp提權後也只是權限的提高和對內核有必定的訪問控制權。對於Android系統而言,逆向人員不只可以拿到root最高權限,並且還能夠修改系統的全部代碼。從攻防雙方在運行環境的維度來看,「魔」比」道「高了不僅三丈,防護方猶如板上魚肉。而在代碼維度,防護方擁有源代碼的控制權,攻防處於徹底劣勢。隨着代碼混淆和VMP技術的運用,防護方這塊魚肉愈來愈很差"啃"。工具
對於基於linux的安卓系統而言,進程的運行環境和結構是由內核來提供和維護的。從修改內核的維度來對抗,能達到一些不錯的效果。下文將詳述在內核態dump目標進程內存和系統調用監控。測試
對內核添加一些自定義功能時,一般能夠採用內核驅動來實現。雖然一部分Android手機支持驅動ko文件加載,但內核提供的其餘工具則不必定已經編譯到內核,在後文中能夠看到。nexus系列手機是谷歌官方所支持的,編譯刷機都比較方便,推薦使用。this
爲了讓內核支持驅動ko文件的加載,在make memuconfig配置內核選項時,如下勾選:
[*] Enable loadable module support 次級目錄全部選項
編譯步驟參看谷歌官方提供的內核編譯步驟。
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結構組織
memread實際上是對memrw函數的封裝,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);
文末代碼中,dump_test爲目標進程,dump_host經過內核驅動獲取目標進程的數據。insmod和dump_host以root權限運行便可。
一般狀況下,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目錄下的例子。
爲了可以支持probe功能,需在上述開啓驅動ko編譯選項的基礎上勾選kprobe選項。若是沒有開啓內核驅動選項,是不會有kprobes(new)選項的
General setup ---> [*] Kprobes(New)
以監控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;
文末代碼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/ThomasKing...