來自高維的對抗 - 逆向TinyTool自制

1、序

不管是逆向分析仍是漏洞利用,我所理解的攻防博弈無非是兩者在既定的某一階段,以高維的方式進行對抗,並不斷地升級維度。好比,逆向工程人員通常會選擇在Root的環境下對App進行調試分析,其是以root的高權限對抗受沙盒限制的低權限;在arm64位手機上進行root/越獄時,ret2usr利用技術受到PXN機制的約束,廠商從修改硬件特性的高維度進行對抗,迫使漏洞研究者提升利用技巧。linux

下文將在Android逆向工程方面,分享鄙人早期從維度攻擊的角度所編寫的小工具。工具自己可能已經不能適應如今的攻防,「授人以魚不如授人以漁」,但願可以給各位讀者帶來一些思路,構建本身的分析利器。git

2、正

0x00 自定義Loader

早期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不必定存在,註釋相應的重建代碼便可。函數

0x01 Kernel Helper

以APP分析爲例,對於加固過的應用一般會對自身的運行環境進行檢測。好比: 檢測自身調試狀態,監控proc文件等。相信各位讀者有各類奇淫技巧來繞過,早期鄙人構建hook環境來繞過。從維度的角度,再來分析這種對抗。對於APP或者bin文件而言,其僅運行於受限的環境中,就算exp提權後也只是權限的提高和對內核有必定的訪問控制權。對於Android系統而言,逆向人員不只可以拿到root最高權限,並且還能夠修改系統的全部代碼。從攻防雙方在運行環境的維度來看,「魔」比」道「高了不僅三丈,防護方猶如板上魚肉。而在代碼維度,防護方擁有源代碼的控制權,攻防處於徹底劣勢。隨着代碼混淆和VMP技術的運用,防護方這塊魚肉愈來愈很差"啃"。工具

對於基於linux的安卓系統而言,進程的運行環境和結構是由內核來提供和維護的。從修改內核的維度來對抗,能達到一些不錯的效果。下文將詳述在內核態dump目標進程內存和系統調用監控。測試

1. 內存DUMP

對內核添加一些自定義功能時,一般能夠採用內核驅動來實現。雖然一部分Android手機支持驅動ko文件加載,但內核提供的其餘工具則不必定已經編譯到內核,在後文中能夠看到。nexus系列手機是谷歌官方所支持的,編譯刷機都比較方便,推薦使用。this

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結構組織

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);

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

3、尾

本文介紹了鄙人對攻防的維度思考,以及從維度分析來實現的早期工具的部分介紹。但願可以給各位讀者帶來一些幫助和思考。限於鄙人水平,不免會有疏漏或者錯誤之處,敬請各位指出,謝謝。

4、附

https://github.com/ThomasKing...

做者:團控@阿里聚安全,更多阿里安全類技術文章,請訪問阿里聚安全博客

相關文章
相關標籤/搜索