1. 程式人生 > >Android平臺的 Ptrace, 注入, Hook 全攻略

Android平臺的 Ptrace, 注入, Hook 全攻略

Android平臺上的Ptrace已經流行很久了,我記得最早的時候是LBE開始使用Ptrace在Android上做攔截,大概三年前我原來的同事yuki (看雪上的古河) 寫了一個利用Ptrace注入的例子,被廣泛使用,聽說他還因此當上了版主,呵呵:
Android平臺上的注入程式碼

自從我寫過文章之後就不斷的有人來問我各種各樣的問題,問的最多的是能不能和yuki的程式結合起來實現任意程式碼的hook (不僅僅是函式)並轉至注入的so執行,技術上當然是可以的,但我一直沒時間去實現.

幾年過去了,沒想到Android上的Ptrace依然火爆,成為各位安全人員和黑客手中的第一利器,於是這週末抽點時間,實現一下大家一直在問的問題,利用Ptrace進行so注入,並Hook任意函式,轉至注入的so中執行。希望這次能多寫一些細節,分享給大家。這次的程式參考了部分我自己原先的程式碼和yuki的程式碼,在此向yuki表示敬意。

這次有三個獨立的程式:
1) hook_d, 這個程式是目標程式,裡面迴圈呼叫logcat列印日誌
2) hook_so, 這是個動態連結庫,編出來的名字叫libhook.so, 包含一個函式hookfun, 我們將把這個so注入到hook_d中, 並攔截hook_d的logcat日誌列印函式,轉到hookfun執行
3) hook_s, 這個是注入和Hook的主程式,將so注入hook_d並hook logcat函式

我們首先看 hook_d, 程式碼很簡單,一個死迴圈,不斷用logcat列印日誌:

#include <stdio.h>
#include <dlfcn.h>
#include <pthread.h> #include <fcntl.h> #include <unistd.h> #include <android/log.h> int main() { int n=0; char p1[]="ProjectName"; char p2[]="I am:%d\n"; __android_log_print(ANDROID_LOG_INFO, p1, p2, 0); while(1) { n=n+1; __android_log_print(ANDROID_LOG_INFO, p1, p2, n); sleep
(1); } return 0; }

然後看hook_so,我們將在hook_d中注入這個so,然後hook __android_log_print 函式,轉到so中的hookfun執行,在hookfun中我們將第二個引數地址上的字串改為hookit!, 第二個引數對應logcat的專案名稱ProjectName,所以如果我們成功了應該能看到ProjectName變成hookit!

#include <stdio.h>
#include <dlfcn.h>
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>

int test()
{
    printf("Hello~\n");
}

int hookfun(void* p1, void* p2, void* p3, void* p4)
{
    memcpy(p2, "hookit!\0", 8);
    return 0;
}

接下來是最主要的工作了,編寫hook_s, 整個過程分為好幾個步驟,包括不少技術小細節,我們一個個來看。

第1步,利用Ptrace attach到目標程序,沒有任何難度

第2步,在目標程式hook_d的程序空間裡執行mmap, 對映一塊記憶體,用於後續我們存放參數和機器指令。
這一步會遇到幾個問題,我們首先要知道mmap在目標程序中的位置,解決的方法如下:
1) 檢視hook_s自己的的maps表,找到libc.so的起始地址 loc_addr
2) 根據mmap函式指標的地址算出mmap相對於libc.so起始地址的偏移offset=mmap - loc_addr
3) 檢視hook_d的maps表,找到libc.so的起始地址 remote_addr
4) 根據這幾個值算出mmap在hook_d中的地址:remote_addr+offset

利用這個方法可以算出每個函式在hook_d中的位置:

void* get_module_base( pid_t pid, const char* module_name )
{
    FILE *fp;
    long addr = 0;
    char *pch;
    char filename[32];
    char line[1024];

    if ( pid < 0 )
    {
        /* self process */
        snprintf( filename, sizeof(filename), "/proc/self/maps", pid );
    }
    else
    {
        snprintf( filename, sizeof(filename), "/proc/%d/maps", pid );
    }

    fp = fopen( filename, "r" );

    if ( fp != NULL )
    {
        while ( fgets( line, sizeof(line), fp ) )
        {
            if ( strstr( line, module_name ) )
            {
                pch = strtok( line, "-" );
                addr = strtoul( pch, NULL, 16 );

                if ( addr == 0x8000 )
                    addr = 0;

                break;
            }
        }

                fclose( fp ) ;
    }

    return (void *)addr;
}


void* get_remote_addr( pid_t target_pid, const char* module_name, void* local_addr )
{
    void* local_handle, *remote_handle;

    local_handle = get_module_base( -1, module_name );
    remote_handle = get_module_base( target_pid, module_name );

    printf( "get_remote_addr: local[%x], remote[%x]\n", local_handle, remote_handle );

    return (void *)( (uint32_t)local_addr + (uint32_t)remote_handle - (uint32_t)local_handle );
}

mmap_addr = get_remote_addr( tar_pid, "/system/lib/libc.so", (void *)mmap );

緊接著我們遇到的一個問題是如何在hook_d的程序空間中呼叫mmap,實現並不難,我們只要將$PC指向mmap,並在暫存器中準備好引數就可以了,但問題是如何返回?我們希望不破壞原有的程式執行,mmap結束之後肯定會跳到LR指向的地址,而不是我們希望的跳回程式繼續執行。這個問題的解決方法是將LR置為0,mmap結束後跳轉就會引發一個NPE,而這時程式處於除錯狀態,這個異常是可以被hook_s捕獲的,捕獲以後我們就可以還原上下文,讓程式繼續執行了。

int ptrace_call( pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct pt_regs* regs )
{
    uint32_t i;

    for ( i = 0; i < num_params && i < 4; i ++ )
    {
        regs->uregs[i] = params[i];
    }

    //
    // push remained params onto stack
    //
    if ( i < num_params )
    {
        regs->ARM_sp -= (num_params - i) * sizeof(long) ;
        ptrace_writedata( pid, (void *)regs->ARM_sp, (uint8_t *)&params[i], (num_params - i) * sizeof(long) );
    }

    regs->ARM_pc = addr;
    if ( regs->ARM_pc & 1 )
    {
        /* thumb */
        regs->ARM_pc &= (~1u);
        regs->ARM_cpsr |= CPSR_T_MASK;
    }
    else
    {
        /* arm */
        regs->ARM_cpsr &= ~CPSR_T_MASK;
    }


    regs->ARM_lr = 0;   

    if ( ptrace_setregs( pid, regs ) == -1 
        || ptrace_continue( pid ) == -1 )
    {
        return -1;
    }


    waitpid( pid, NULL, WUNTRACED );

    return 0;
}

parameters[0] = 0;  // addr
parameters[1] = 0x1000; // size
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC;  // prot
parameters[3] =  MAP_ANONYMOUS | MAP_PRIVATE; // flags
parameters[4] = 0; //fd
parameters[5] = 0; //offset
ret = ptrace_call( tar_pid, (uint32_t)mmap_addr, parameters, 6, &regs );

第3步,呼叫dlopen,將libhook.so注入hook_d的程序空間
有了第2步的基礎,這一步幾乎沒有任何障礙,完成之後我們可以看到hook_d的程序空間裡面已經注入了libhook.so

7618E27E_AACB_4392_BBCF_5EDED9F84305

第4步,在hook_d中Hook函式__android_log_print,將地址改到libhook.so中的hookfun
Hook程序內函式的方法有很多種,最簡單的就是改got表,我們不採用這種方法,因為改got表只能Hook表裡面有的函式,且只能在呼叫處hook, 我希望做到的是任意程式碼處的hook.
那麼如何做到呢?基本的想法就是像偵錯程式一樣修改程式碼,我手工畫了個草圖,供大家參考:

IMG_20140720_215710

我們將修改__android_log_print入口處的指令 (當然任意位置的指令也可以),讓他跳轉到一個bridge, 然後從bridge再跳轉到 hookfun, hookfun執行完成後跳回bridge,再跳回__android_log_print,不影響程式繼續執行。那麼bridge是什麼?bridge是我們寫在mmap分配的空間裡面的一段指令,為什麼要用bridge?因為我們在hook的時候有一些操作要做,但我們希望儘量少改動原有的程式指令,所以我們把複雜的操作放在bridge裡面,原有的程式只改4Byte,做一次跳轉。

這麼一說大家應該都明白了,第4步又可以分成兩個小步驟:
1)修改__android_log_print的入口指令,讓他跳轉到bridge. 那麼是不是所有的指令都可以修改呢?不是的,因為我們只打算做一次修改,不再改回去了,所以被覆蓋的4Byte指令我們要放在別處執行,這樣的話如果是基於相對$PC定址的指令就比較麻煩了,所以修改的時候要儘量避開這種指令。不過函式入口一般都沒事, 大都是些PUSH指令。我們看一下__android_log_print的入口指令:

B5401091_D6EE_4898_98CC_78FDCBA15142

呵呵,沒事,前兩條指令和$PC沒毛關係,大膽覆蓋。但我們如果想寫比較好的程式當然最好驗證一下要覆蓋的指令有沒有相對定址和跳轉。對於覆蓋指令寫成什麼樣,取決於被覆蓋的指令的形式,基本上有3種可能:Thumb, Thumb-2, ARM, 具體情況具體對待,可以做成自動識別。我看了一下我的這個機子,ARMv7的CPU,當前函式是Thumb-2指令,所以我就生成BL.W來跳轉:

void* build_jmp(int jump_forward, int jump_offset)
{
    int bl_h,bl_l;
    void* temp;

    if(jump_forward==0)
        jump_offset = 0-jump_offset;

    bl_h = (jump_offset >> 12) & 0x7ff;
    bl_l = (jump_offset >> 1) & 0xfff;

    temp = (((bl_l | 0xf800) & 0xbfff) << 16) | ((bl_h | 0xf000) & 0xf7ff);

    return temp;
}

if(logcat_addr < buff_addr)
{
jump_forward = 1;
jump_offset = buff_addr - (logcat_addr+4);
}
else
{
jump_forward = 0;
jump_offset = (logcat_addr+4) - buff_addr;
}
jmp_op = build_jmp(jump_forward, jump_offset);

2) 然後建立bridge, 寫一組彙編指令:

void build_bridge(pid_t pid, void* buff_addr, void* src_addr, void* op_fill, void* hookfun_addr)
{

    void* jmp_back;
    /*
    "push {r0-r7} \t\n"
    "ldr r5, loc_tar \t\n"
    "mov r6, lr \r\n"
    "mov r4, pc \r\n"
    "add r4, r4, #5 \t\n"
    "mov lr, r4 \t\n"
    "mov pc, r5 \t\n"
    "mov lr, r6 \t\n"
    "pop {r0-r7} \t\n"

    "mov r0, r0 \t\n" //exec fill
    "mov r0, r0 \t\n" //exec fill
    "mov r0, r0 \t\n" //b.w src
    "mov r0, r0 \t\n" //b.w src
    "mov r0, r0 \t\n"

    "loc_tar: \t\n"
    ".word 0x11111111 \t\n"

    */
    char op_code[]={0xFF, 0xB4, 0x06, 0x4D, 0x76, 0x46, 0x7C, 0x46, 0x05, 0x34, 0xA6, 0x46, 0xAF, 0x46, 0xB6, 0x46, 0xFF, 0xBC, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x11, 0x11, 0x11, 0x11};
    char* op_codep = op_code;
    int jump_forward;
    int jump_offset;
    int fill_opcount = 9;
    int jback_opcount = 11;
    int hook_opcount = 14;
    int i;

    if(buff_addr < src_addr)
    {
        jump_forward = 1;
        jump_offset = (src_addr+4) - (buff_addr+(jback_opcount*2)+4);
    }
    else
    {
        jump_forward = 0;
        jump_offset = (buff_addr+(jback_opcount*2)+4) - (src_addr+4);
    }

    jmp_back = build_jmp(jump_forward, jump_offset);
    printf("jmp_back op:%x\n",jmp_back);

    memcpy(op_codep+(fill_opcount*2), &op_fill, 4);
    memcpy(op_codep+(jback_opcount*2), &jmp_back, 4);
    memcpy(op_codep+(hook_opcount*2), &hookfun_addr, 4);

    ptrace_writedata(pid, buff_addr, op_code, sizeof(op_code));
    //memcpy(buff_addr, op_code, sizeof(op_code));

    printf("bridge op:");
    for(i=0; i<sizeof(op_code); i++)
        printf("0x%x,", op_code[i]);
    printf("\n");
}

bridge裡面指令的含義大致是這樣:
需要用的暫存器壓棧
儲存LR
設定LR為hookfun的返回地址,因為hookfun一定會用BX LR來返回
載入hookfun的地址
跳轉至hookfun執行
從hookfun返回後把前面壓棧的暫存器出棧
執行被覆蓋的4Byte指令
跳回__android_log_print繼續執行

當然bridge裡面可以做的事情還很多,比如根據hookfun的返回值決定是否直接終止被hook的函式等等,總之想要的基本都能做到。

說到這裡大部分的工作都已經做完了,我們看一下實際執行的效果,先執行hook_d:

C609A475_19BE_4F7D_894E_67FE45BE99A1

然後執行hook_s, 可以看到程式將libhook.so注入hook_d, 修改指令hook函式__android_log_print,然後正常detach, 程式退出。

1097DBA5_7850_4094_BABD_C40B6BBF9C94

接著我們看hook_d的輸出,已經hook成功,ProjetName變成了hookit!且程式正常執行:

E82246EE_F290_48A2_88E6_779131858761

OK,總結一下,我們完成了從so注入到程式Hook的整個過程,也詳細介紹了過程中的每一個技術細節,相信大家看完以後大部分問題都能找到答案了,附件中是全部的原始碼,供大家進一步研究。