Android注入要點記錄

雖然之前注入過android很多次,但所做的事情不過是在別人的框架下做些修改,調調bug,完全沒有徹底消化和掌握注入的知識和技術。所以我決定寫這一篇博文,總結android注入的實現要點。

android設備存在32位和64位之分(這裏專指ARM,而非x86),兩者ABI的差異導致它們的一些數據結構,函數的實現存在差異。下面的注入代碼可能牽扯到這些不同。這裏推薦兩種方法來查看這些差異。一是直接下載kernel源碼,例如如果要查看ptrace.h的不同實現,就可以在kernel目錄下找到32位和64位的ptrach頭文件。
二是通過android studio,在gradle中可以找到如下語句。

        externalNativeBuild {
            ndk{
                abiFilters "armeabi-v7a","arm64-v8a"
            }
        }

如果要看32位的頭文件,就保留上面的"armeabi-v7a",否則則保留"arm64-v8a"。之後在源文件中#include<asm/ptrace.h>,然後go to declaration,就可以分別看到32位和64位的頭文件了。

另外,關於arm調用約定的內容可去arm developer搜索call standard查看。

注入原理

代碼注入的過程其實就是讓目標進程加載我們的動態鏈接庫。Android是基於linux的系統,而linux中的dlopen()函數用來打開一個動態庫,並將其加載到進程的地址空間。所以注入過程要完成的事情只有一個:調用dlopen()。

這個調用過程必鬚髮生在目標進程,也就是讓目標進程去調用dlopen(),這樣動態鏈接庫才能加載到目標進程中。那麼該怎麼實現控制目標進程去調用dlopen()呢?在Linux中有一個系統調用ptrace(),它可以查看和修改目標進程的內存和寄存器中的數據,就像在gdb調試的時候可以中斷程序,然後查看修改變量一樣。通過ptrace()系統調用,就可以修改pc寄存器以控制目標進程的執行流程。例如可以修改pc寄存器的值爲ptrace()函數的地址,並在寄存器和棧中提前寫好參數,這樣就實現了使目標進程打開動態鏈接庫的目的。不僅如此,在注入完成後,還可以讓pc寄存器等於注入進去的動態鏈接庫的函數來調用該函數。

ptrace系統調用和waitpid

以下內容主要翻譯自linux man-pages。

ptrace()系統調用爲一個進程(追蹤進程)提供了一種觀察和控制其它進程(被追蹤進程)執行的方法,它可以查看並修改被追蹤進程的內存和寄存器。對於一個多線程的進程來說,它的每一個線程都可以被單獨追蹤。

追蹤可以採取以下兩種方式進行,一是父進程fork子進程且子進程發出PTRACE_TRACEME請求時,父進程可以追蹤子進程。二是當一個進程使用PTRACE_ATTACH 或 PTRACE_SEIZE請求時,它可以追蹤其它進程。

當被追蹤者產生一個信號時(SIGKILL信號除外),被追蹤者暫停,追蹤者會在waitpid中被喚醒,然後追蹤者可以使用各種ptrace請求來查看和修改被追蹤者的執行狀態。當追蹤者完成追蹤後,可以通過PTRACE_DETACH請求來結束被追蹤者的追蹤狀態。

ptrace的函數聲明如下

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,
            void *addr, void *data);

request代表請求,pid代表被追蹤者的進程,addr是讀或寫的地址,data是讀或寫的數據。其中addr和data可以爲NULL,視request而定。

接下來介紹注入過程中用到的幾個ptrace請求和waitpid函數,以及ptrace過程中stop的幾種情況。

waitpid

該函數用來等待子進程的狀態變化:進程終止,進程收到信號停下,進程收到信號繼續運行。當子進程狀態變化時,該函數會立馬返回。聲明如下:

pid_t waitpid(pid_t pid, int *wstatus, int options);

成功時返回子進程的id或0(設置了WHONANG),發生錯誤則返回-1.

options可以取以下任意幾項的或:

  1. WNOHANG:如果沒有子進程則馬上返回
  2. WUNTRACED:當子進程停下時返回。當子進程被追蹤時,即使沒有指明該項,子進程停下時也會返回。
  3. WCONTINUED:當子進程收到SIGONT信號繼續運行時返回。

當wstatus不取NULL時,它可以用來存儲狀態信息。信息可以通過以下幾個宏來獲得(用法:宏(*wstatus)???):

  1. WIFEXITED(wstatus):當子進程正常結束時返回真。 (即exit(3) 或 _exit(2)或從main正常返回)。
  2. WEXITSTATUS(wstatus):返回子進程的結束狀態,只有當WIFEXITED返回真時纔可以使用該宏。
  3. WIFSIGNALED(wstatus):當子進程被信號終止時返回
  4. WTERMSIG(wstatus):返回使子進程終止的信號數字。只有當WIFSIGNALED返回真時纔可以使用該宏。
  5. WCOREDUMP(wstatus):暫時用不到,跳過。
  6. WIFSTOPPED(wstatus):當子進程被信號停下(非終止)時返回真。只有當options中有 WUNTRACED或子進程正在被追蹤時纔可能發生這種情況。
  7. WSTOPSIG(wstatus):返回使子進程停下的信號的數字,只有當WIFSTOPPED返回真時纔可以使用該宏。
  8. WIFCONTINUED(wstatus):當子進程收到SIGONT信號繼續運行時返回真。應該與WCONTINUED的options搭配使用。

PTRACE_ATTACH

附在pid代表的進程上,追蹤該進程。追蹤者發送SIGSTOP信號給被追蹤者,但被追蹤者並不是馬上停止。追蹤者可以用waitpid來等待被追蹤者停止。使用該請求時addr參數和data參數會被忽略。

PTRACE_CONT

讓被追蹤進程開始運行。如果data參數非0,則代表發送給被追蹤者的信號。忽略addr參數。

PTRACE_SYSCALL, PTRACE_SINGLESTEP

PTRACE_SYSCALL讓被追蹤進程開始運行,但會在下次進入或退出系統調用時停止。PTRACE_SINGLESTEP則會讓被追蹤者在執行完下條指令後停下。另外,被追蹤者會在收到信號時也會停下。非0的data參數同樣代表信號。忽略addr參數。

PTRACE_DETACH

和PTRACE_CONT一樣,讓被追蹤進程開始運行,同時停止追蹤狀態。在linux下,無論任何形式進行的追蹤狀態都可以用該請求來停止。忽略addr參數。

PTRACE_DETACH請求需要被追蹤者處於停止狀態,若被追蹤者正在運行,需要先發送SIGSTOP信號使其停下。

PTRACE_GETREGS, PTRACE_GETFPREGS

獲取被追蹤者的通用或浮點寄存器。獲取的值會存儲到data指向的數據結構(pt_regs)中。忽略addr參數。

並不是所有的架構都有該請求。

PTRACE_GETREGSET

讀取被追蹤者寄存器。addr參數決定讀取寄存器的類型。如果addr是NT_PRSTATUS,則讀取通用寄存器。如果addr是NT_foo,則讀取浮點或向量寄存器(如果有的話)。data參數指向iovec類型:

struct iovec {
  void __user * iov_base;
  __kernel_size_t iov_len;
};

該類型描述存儲寄存器信息的指針(pt_regs*類型)和長度。內核會更新長度來告知實際讀取的長度。

PTRACE_POKETEXT, PTRACE_POKEDATA

往被追蹤者的內存寫入一個字大小的數據(32位中一個字爲4字節,64位中爲6字節)。要用到addr和data參數。

PTRACE_PEEKTEXT, PTRACE_PEEKDATA

從被追蹤者內存讀取一個字大小數據。該數據會被作爲返回值,所以只用到addr參數,忽略data參數。

signal-delivery-stop

當一個進程收到任何除了SIGKILL的信號時,它會隨機選擇一個線程來處理該信號(可以通過tgkill來選擇處理的線程)。如果該線程正在被追蹤,那麼該線程就會停下,把信號交給追蹤者。追蹤者可以選擇隱藏該信號(suppression),也可以把信號重新交給被追蹤者(signal injection)。

注意,只有signal-delivery-stop纔可以發生signal injection,即在PTRACE_CONT中重新發送一個信號。如果是其它ptrace-stop,可能會忽略PTRACE_CONT中的data參數

當發生signal-delivery-stop時,追蹤者waitpid函數的返回狀態可以使宏WIFSTOPPED(status)爲真,使宏WSTOPSIG(status)返回信號編號。具體見前面waitpid部分。

group-stop

當一個被追蹤者收到停止信號( SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU)時,進入group-stop。可以通過ptrace(PTRACE_GETSIGINFO, pid, 0, &siginfo)來區別group-stop和其它stop。

syscall-stops

syscall-stops分爲syscall-enter-stop和syscall-exit-stop。當使用PTRACE_SYSEMU請求來繼續運行被追蹤者時,被追蹤者會在進入系統調用前停下,發生syscall-enter-stop。

當使用PTRACE_SYSCALL時,不僅會發生syscall-enter-stop,被追蹤者還會再系統調用完成後停下,發生syscall-exit-stop。

信號引起的進程停止signal-delivery-stop不會發生在syscall-enter-stop和syscall-exit-stop間,只會發生在syscall-exit-stop後。

syscall-stops同樣可以用waitpid的宏判斷。具體見前面waipid部分。

函數地址獲取方法

既然要讓進程跳轉到dlopen()函數執行,首先要獲取目標進程中dlopen()函數的地址。dlopen()函數,dlsym()函數以及其它函數都是以動態鏈接的方式鏈接到程序中,它們都位於進程中的某個模塊,例如dlopen就位於linker模塊,如下圖。
在這裏插入圖片描述
可以看到,如果知道linker模塊的地址和dlopen()相對linker模塊的偏移,即圖中的①和②,那麼就可以將偏移和地址相加來得到dlopen()在進程中的地址。

這些信息可以從proc/pid/maps這個文件中得到。proc/pid/maps顯示進程映射了的內存區域。在這個文件中,可以讀到一條條以下格式的信息:
b6fc1000-b6fd1000 r-xp 00000000 b3:26 336 /system/bin/linker
從左到右依次爲動態庫的:起止地址 權限 偏移 設備 i結點號 名字。

首先在本進程(追蹤進程)中得到dlopen的地址,然後再查看本進程的proc文件,找到linker模塊的起止地址。把dlopen的地址減去linker模塊的地址就得到了圖中的②。

而圖中的①可以直接查詢被追蹤進程的的proc文件,查到linker模塊的起止地址,即圖中的①。

先看②具體是怎麼獲取的。

find_module_name_by_addr

首先是打開proc/pid/maps文件,代碼如下:

void* find_module_name_by_addr(pid_t pid, void* address, char *const module_name)
{
    char proc_map_name[32];
    void* return_addr = NULL;
    char line[256];
    if (pid < 0)
    {
        sprintf(proc_map_name, "/proc/self/maps");
    }
    else
    {
        sprintf(proc_map_name, "/proc/%d/maps", pid);
    }
    FILE *proc_map = fopen(proc_map_name, "r");
    //...

當pid爲-1時,代表本進程,所以文件名爲/proc/self/maps。否則則是/proc/pid/maps。

然後需要對從該文件中讀取的信息進行處理。在這個文件中,可以讀到一行行以下格式的信息:
b6fc1000-b6fd1000 r-xp 00000000 b3:26 336 /system/bin/linker
其中有用的信息是起始地址和模塊名。爲了處理該行信息,需要使用strtoul和strsep和函數。

#include<string.h>
char *strsep(char **stringp, const char *delim);
//以delim字符串中的任意一個字符作爲分割符,分割stringp,只分割一次,
//分成兩個字符串。會將位於stringp中的分割字符換成'\0',stringp會被更
//新爲'\0'的後一個位置(的地址),即指向第二個字符串,返回原stringp
//指向位置,即返回第一個字符串。
//如果沒找到分割符,則stringp設爲NULL,返回原字符串。
unsigned long int strtoul(const char *str, char **endptr, int base)
//把參數 str 所指向的字符串根據給定的 base 轉換爲一個無符號長整數。
//str是字符串,base是基數,若endptr不爲NULL,其值由函數設置爲 str 
//中數值後的下一個字符。
//返回轉換後的長整數,如果沒有執行有效的轉換,則返回一個零值。

通過strsep(&line_ptr, " "),就能以取得空格前面的字符串,並且使line_ptr指向空格後的字符串,方便下一次處理。函數後面部分的讀取和處理代碼如下:

//...
	//fgets() will read the '\n' and end with '\0'
    while (fgets(line, sizeof(line), proc_map))
    {
        char line_log[256];
        strcpy(line_log,line);
        char *line_ptr = line;
        //format of the str in line: b6fc1000-b6fd1000 r-xp 00000000 b3:26 336        /system/bin/linker
        void* start_addr = (void*)strtoul(strsep(&line_ptr, "-"), NULL, 16);
        void* end_addr = (void*)strtoul(strsep(&line_ptr, " "), NULL, 16);
        //if find the module that contains our address, obtains its name
        if (address >= start_addr && address < end_addr)
        {
            strsep(&line_ptr, " "); //skip permission
            strsep(&line_ptr, " "); //skip offset
            strsep(&line_ptr, " "); //skip dev
            strsep(&line_ptr, " "); //skip inode
            while (*line_ptr == ' ')
            {
                line_ptr++; //skip blank
            }
            if(module_name!=NULL)
                strcpy(module_name, strsep(&line_ptr,"\n"));    //elimate last '\n'
            return_addr = start_addr;
            break;
        }
    }
    fclose(proc_map);
    return return_addr;
}

這樣就得到了模塊在本進程的起始地址和名字了。接下來只要將函數地址-起始地址,就得到了偏移地址,即前面圖中的②。

find_module_addr_by_name

和find_module_name_by_addr基本相同,只不過由於需要獲取的是模塊在目標進程中的地址,所以從/proc/pid/maps中查找的信息和find_module_name_by_addr有所不同。代碼如下:

void* find_module_addr_by_name(pid_t pid, char *module_name)
{
    char proc_map_name[32];
    void* return_addr = NULL;
    char line[256];
    if (pid < 0)
    {
        sprintf(proc_map_name, "/proc/self/maps");
    }
    else
    {
        sprintf(proc_map_name, "/proc/%d/maps", pid);
    }
    FILE *proc_map = fopen(proc_map_name, "r");
    //...

打開文件的部分和find_module_name_by_addr一模一樣。繼續看後面。

    while (fgets(line, sizeof(line), proc_map))
    {
        char *line_ptr = line;
        //format: b6fc1000-b6fd1000 r-xp 00000000 b3:26 336        /system/bin/linker
        void* start_addr = (void*)strtoul(strsep(&line_ptr, "-"), NULL, 16);
        strsep(&line_ptr, " "); //skip end_address
        strsep(&line_ptr, " "); //skip permission
        strsep(&line_ptr, " "); //skip offset
        strsep(&line_ptr, " "); //skip dev
        strsep(&line_ptr, " "); //skip inode
        while (*line_ptr == ' ')
        {
            line_ptr++; //skip blank
        }
        //if the name does not exist, continue;
        if (*line_ptr == '\n')
        {
            continue;
        }
        if (strstr(line_ptr, module_name) != NULL)
        {
            //several module have the same name,
            //only the first one have executing permission
            //so directly break when find the first one
            return_addr = start_addr;
            break;

        }
    }
    fclose(proc_map);
    return return_addr;
}

就是讀取一行信息,匹配模塊名,若模塊名相同則返回其地址。

需要注意的是,在讀取/proc/pid/maps過程中,可能讀到好幾個有相同模塊名的信息。它們分別代表同一模塊的代碼塊,數據塊等。要找的是其中有執行權限的代碼模塊,也就是找到的第一條信息。

這樣就找到了前面圖中的①。接下來只要將上面兩個函數得到的結果進行計算,實現②+①。

get_fun_remote_addr

void* get_fun_remote_addr(pid_t pid, void* func_local_addr)
{
    char module_name[64];
    void* local_module_start = find_module_name_by_addr(-1, func_local_addr, module_name);
    if (local_module_start == NULL)
    {
        return NULL;
    }
    void* remote_module_start = find_module_addr_by_name(pid, module_name);
    if (remote_module_start == NULL)
    {
        return NULL;
    }
    return func_local_addr - local_module_start + remote_module_start;
}

該函數調用了find_module_name_by_addr和find_module_addr_by_name,根據前面的方法,計算出了函數在目標進程中的地址。使用時只需要傳入目標進程的pid和需要得到的函數在本進程的地址即可。

可以用該函數獲取dlopen和dlsym的地址。

pt_regs、ptrace_getregs和ptrace_setregs

在前面介紹過,PTRACE_GETRES和PTRACE_GETREGSET請求可以讀取被追蹤者的寄存器。用函數ptrace_getregs來封裝該ptrace調用。在介紹該函數之前,先看用來接收寄存器信息的結構pt_regs。查看ptace.h的方法在文章開頭已經介紹過了。

pt_regs在32位arm和64位arm的區別

//32位的ptrace.h
struct pt_regs {
  long uregs[18];
};
#define ARM_cpsr uregs[16]
#define ARM_pc uregs[15]
#define ARM_lr uregs[14]
#define ARM_sp uregs[13]
#define ARM_ip uregs[12]
#define ARM_fp uregs[11]
#define ARM_r10 uregs[10]
#define ARM_r9 uregs[9]
#define ARM_r8 uregs[8]
#define ARM_r7 uregs[7]
#define ARM_r6 uregs[6]
#define ARM_r5 uregs[5]
#define ARM_r4 uregs[4]
#define ARM_r3 uregs[3]
#define ARM_r2 uregs[2]
#define ARM_r1 uregs[1]
#define ARM_r0 uregs[0]
#define ARM_ORIG_r0 uregs[17]
#define ARM_VFPREGS_SIZE (32 * 8 + 4)
//64位的ptrace.h
struct user_pt_regs {
  __u64 regs[31];
  __u64 sp;
  __u64 pc;
  __u64 pstate;
};

64位arm的ptrace.h相比於32位arm有以下區別:

  1. pt_regs變爲了user_pt_regs。
  2. 成員long uregs[18]變成了__u64 regs[31]。
  3. 沒有了諸如ARM_r0等宏,sp,pc,cpsr等寄存器信息不存在在數組中,而是單獨存在user_pt_regs成員變量中。
  4. 另外,arm32和arm64的調用約定也有區別。

爲了屏蔽以上區別,需要在ptrace封裝函數所在的源文件開頭加入以下宏定義。

#if defined(__aarch64__)
#define pt_regs         user_pt_regs
#define uregs   regs
#define ARM_pc  pc
#define ARM_sp  sp
#define ARM_cpsr    pstate
#define ARM_lr      regs[30]
#define ARM_r0      regs[0]
#define PTRACE_GETREGS PTRACE_GETREGSET
#define PTRACE_SETREGS PTRACE_SETREGSET
#endif

其中PTRACE_GETREGS被換成了PTRACE_GETREGSET。這可能是由於PTRACE_GETREGS不存在64位arm架構中。
而ARM_lr替換成r30是64位Arm程序調用約定規定的。

ptrace_getregs

根據前面對PTRACE_GETREGS的介紹,在32位ARM處理機中獲取寄存器信息是非常簡單的。直接:

ptrace(PTRACE_GETREGS, pid, NULL, regs)

即可。其中regs是傳進來的pt_regs類型指針。

64位ARM處理機的代碼要稍微麻煩一些,因爲要使用PTRACE_GETREGSET請求,而該請求需要指明接收的寄存器類型,並且使用iovec類型而非pt_regs類型來接收寄存器信息。實現代碼如下:

int ptrace_getregs(pid_t pid, struct pt_regs * regs)
{
#if defined (__aarch64__)
    int regset = NT_PRSTATUS;//general-purpose registers
    struct iovec ioVec;

    ioVec.iov_base = regs;
    ioVec.iov_len = sizeof(*regs);
    if (ptrace(PTRACE_GETREGSET, pid, (void*)regset, &ioVec) < 0) {
        perror("[-] ptrace_getregs: Can not get register values");
        return -1;
    }

    return 0;
#else
    if (ptrace(PTRACE_GETREGS, pid, NULL, regs) < 0) {
        perror("[-] ptrace_getregs: Can not get register values");
        return -1;
    }

    return 0;
#endif
}

ptrace_setregs

ptrace_setregs和ptrace_setregs的代碼大同小異,只不過換了ptrace的請求。

int ptrace_setregs(pid_t pid, struct pt_regs * regs)
{
#if defined (__aarch64__)
    int regset = NT_PRSTATUS;
    struct iovec ioVec;

    ioVec.iov_base = regs;
    ioVec.iov_len = sizeof(*regs);
    if (ptrace(PTRACE_GETREGSET, pid, (void*)regset, &ioVec) < 0) {
        perror("[-] ptrace_setregs: Can not get register values");
        return -1;
    }

    return 0;
#else
    if (ptrace(PTRACE_SETREGS, pid, NULL, regs) < 0) {
        perror("[-] ptrace_setregs: Can not set register values");
        return -1;
    }

    return 0;
#endif
}

ptrace_writedata

PTRACE_POKEDATA請求每次只能寫一個字的數據,ptrace_writedata對其進行了封裝,該函數實現了往目標進程寫入任意大小數據的功能。

int ptrace_writedata(pid_t pid, uint8_t *dest, uint8_t *data, size_t size)
{
    long i, j, remain;
    uint8_t *laddr;
    size_t bytes_width = sizeof(long);

    union u {
        long val;
        char chars[sizeof(long)];
    } d;

    j = size / bytes_width;
    remain = size % bytes_width;

    laddr = data;

    for (i = 0; i < j; i ++) {
        memcpy(d.chars, laddr, bytes_width);
        ptrace(PTRACE_POKETEXT, pid, dest, d.val);

        dest  += bytes_width;
        laddr += bytes_width;
    }

    if (remain > 0) {
        d.val = ptrace(PTRACE_PEEKTEXT, pid, dest, 0);
        for (i = 0; i < remain; i ++) {
            d.chars[i] = *laddr ++;
        }

        ptrace(PTRACE_POKETEXT, pid, dest, d.val);
    }

    return 0;
}

值得注意的是裏面union的定義。由於要兼容32位和64位,所以union的長度是sizeof(long),long在32位機器中是4字節,在64位中是8字節。爲了方便複製數據,union的另一個成員是char指針,char指針每次指向一個字節的空間。

後面PEEKTEXT是爲了在寫入長度非整數字長的情況下,防止覆蓋目標進程空間後面的數據。

ptrace_call

當想要在目標進程中調用函數時,首先需要將參數寫入目標進程空間,然後設置函數跳轉地址和返回地址。這裏面牽扯到很多arm調用標準的內容。想看具體內容可去arm developer搜索相關文檔。要用到的主要調用約定如下:

  1. arm32中,函數的前四個參數放到r0-r3寄存器。其它參數從右到左入棧。被調用者實現棧平衡,返回值存放在 r0 中。
  2. arm32中,函數的前八個參數放到r0-r7寄存器。其它參數從右到左入棧。被調用者實現棧平衡,返回值存放在 r0 中。
  3. lr寄存器存放返回地址。

往目標進程寫入參數的代碼如下:

int ptrace_call(pid_t pid, uintptr_t addr, long *params, int num_params, struct pt_regs* regs)
{
    int i;
#if defined(__arm__)
    int num_param_registers = 4;
#elif defined(__aarch64__)
    int num_param_registers = 8;
#endif

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

    if (i < num_params) {
        regs->ARM_sp -= (num_params - i) * sizeof(long) ;
        ptrace_writedata(pid, (uint8_t *)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;
    }
    //...

然後要解決函數返回的問題。希望在函數調用完之後被追蹤者暫停,回到追蹤者也就是本進程。也就是說希望函數調用完之後被追蹤者發生signal-delivery-stop。

可以這樣實現,令lr寄存器爲0。當函數執行完後,目標進程會跳轉回lr寄存器中的地址。但0地址是不允許訪問的,於是便會產生SIGSEVG錯誤,目標進程暫停,信號傳回追蹤進程。

爲了防止其它信號的干擾,還需要設置一個ptrace_continue的循環。當收到的狀態不是0xb7f(7f代表被追蹤者暫停,b代表SIGSEVG的信號序號11)時,隱藏信號,繼續讓目標進程執行,直到收到SIGSEVG爲止。代碼如下:

	//...
    regs->ARM_lr = 0;

    if (ptrace_setregs(pid, regs) == -1
        || ptrace_continue(pid) == -1) {
        printf("error\n");
        return -1;
    }

    int stat = 0;
    waitpid(pid, &stat, WUNTRACED);
    while (stat != 0xb7f) {
        if (ptrace_continue(pid) == -1) {
            printf("[-] error\n");
            return -1;
        }
        waitpid(pid, &stat, WUNTRACED);
    }

    return 0;
}

ptrace_call結束後,函數的返回值會存儲到pt_regs的r0中。

ptrace_mmap

接下來解決函數參數的問題。當函數參數是整型,浮點型等這類數據時,可以直接在ptrace_call中把參數寫入寄存器或棧中。但如果參數是指針類型時,在把指針寫入寄存器或棧之前首先得在目標進程空間中準備好指針指向的數據。

例如當想要傳遞一個字符串(char*)給函數時,首先需要在目標進程找一塊空間,把字符串複製到這一塊空間中,再把字符串地址傳給函數。

通過在目標進程中調用mmap的方式來開闢空間。先看mmap的聲明:

void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

mmap在進程的虛擬空間中創建一塊映射區。length參數指明映射的長度,addr爲0時由系統決定映射區的起始地址,該地址會作爲返回值返回。prot參數描述該映射區的內存保護屬性,它可以爲PROT_NONE或以下值的或:

PROT_EXEC 頁內容可能被執行
PROT_READ 頁內容可能被讀
PROT_WRITE 頁內容可能被寫
PROT_NONE 頁不可訪問

flags參數決定該映射區的更新是否會導致文件或其它進程映射區的更新。這裏主要看匿名映射的參數MAP_ANONYMOUS。該參數說明映射區不與任何文件關聯,匿名映射下,fd參數會被忽略(或爲-1),offset參數應該爲0。映射區的空間會被0初始化。

也就是說,當想要分配空間時,需要以

//length代表空間的大小
mmap(0, length, PROT_READ | PROT_WRITE | PROT_EXEC, 
MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);

的形式在目標進程中調用mmap。知道參數後,構造參數然後直接ptrace_call即可。代碼如下:

void* find_space_by_mmap(int target_pid, int size){
    struct pt_regs regs;
    if (ptrace_getregs(target_pid, &regs) == -1)
        return 0;

    long parameters[10];

    /* call mmap */
    parameters[0] = 0;  // addr
    parameters[1] = size; // 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

    void* remote_mmap_addr = get_fun_remote_addr(target_pid,(void*)mmap);

    if (remote_mmap_addr == NULL) {
        LOGE("[-] Get Remote mmap address fails.\n");
        return 0;
    }
    LOGI("[+] start to call mmap, size of space to be mapped is %d",size);
    ptrace_call(target_pid, (uint32_t) remote_mmap_addr, parameters, 6, &regs);

    ptrace_getregs(target_pid, &regs);

    LOGD("[+] Target process returned from mmap, return r0=%x,  pc=%x, \n", regs.ARM_r0, regs.ARM_pc);

    return regs.ARM_pc == 0 ? (void *) regs.ARM_r0 : 0;
}

最後返回了分配的空間的地址。

ptrace_dlopen

接下來開始正式執行注入,即在目標進程調用dlopen加載動態庫。先看dlopen:

void *dlopen(const char *filename, int flags);

dlopen會加載filename命名的動態庫,並返回加載後的句柄handle。handle可以被dlsym,dlclose等函數使用。filename中的/會被視爲路徑。

flags參數必須包含以下二值之一:
RTLD_LAZY 延遲綁定,只有當符號第一次使用時纔開始綁定(PLT機制)
RTLD_NOW 立即綁定,模塊被加載時即完成所有的函數綁定工作。

flags還可以選擇或上RTLD_GLOBAL,它表示將被加載的模塊的全局符號合併到進程的全局符號表中,使得以後加載的模塊可以使用這些符號。

因此,要以以下形式:

dlopen(filename, RTLD_NOW | RTLD_GLOBAL);

在目標進程中調用dlopen。

需要注意的是,由於傳入的filename是字符串,首先需要find_space_by_mmap在目標進程找到一塊空間來存放該字符串,然後再傳遞字符串指針和flag調用dlopen。代碼如下:

void* ptrace_dlopen(pid_t pid, void* dlopen_addr, char* filename)
{
    struct pt_regs regs;
    ptrace_getregs(pid, &regs);
    long params[2];

    size_t filename_len = strlen(filename) + 1;
    LOGI("[+] Try to find space for string %s in target process",filename);
    void* filename_addr;
    filename_addr= find_space_by_mmap(pid, filename_len);
    LOGI("[+] String \"%s\" address %x",filename, filename_addr);
    if (filename_addr == NULL ) {
        LOGE("[-] Call Remote mmap fails.");
        return NULL;
    }
    ptrace_writedata(pid, (uint8_t*)filename_addr, (uint8_t*)filename, filename_len);
    params[0] = (long)filename_addr;  //filename pointer
    params[1] = RTLD_NOW | RTLD_GLOBAL; // flag

    if (dlopen_addr == NULL) {
        return NULL;
    }
    ptrace_call(pid, (uint32_t) dlopen_addr, params, 2, &regs);
    ptrace_getregs(pid, &regs);

    LOGD("[+] Target process returned from dlopen, return r0=%x, pc=%x, \n", regs.ARM_r0, regs.ARM_pc);

    return regs.ARM_pc == 0 ? (void *) regs.ARM_r0 : NULL;
}

該函數將handle值返回。handle值可以被dlsym使用,用來查找該動態庫中某個符號(函數)的地址。ptrace_dlsym的實現方法和ptrace_dlopen基本一樣:

void *ptrace_dlsym(pid_t target_pid, void *remote_dlsym_address, void *handle, char *symbol_name)
{
    LOGI("[+] start to dlsym %s",symbol_name);
    struct pt_regs regs;
    ptrace_getregs(target_pid, &regs);

    long params[2];
    size_t name_len=strlen((char*)symbol_name)+1;
    LOGI("[+] Try to find space for string %s in target process",symbol_name);
    void* symbol_name_address=find_space_by_mmap(target_pid,name_len);
    LOGI("[+] string %s address %p",symbol_name,symbol_name_address);
    if(symbol_name_address==NULL)
    {
        LOGE("[-] Call Remote mmap fails.");
        return NULL;
    }
    ptrace_writedata(target_pid,(uint8_t*)symbol_name_address,(uint8_t*)symbol_name,name_len);
    params[0]=(long)handle;
    params[1]=(long)symbol_name_address;
    ptrace_call(target_pid,remote_dlsym_address,params,2,&regs);

    ptrace_getregs(target_pid,&regs);

    LOGI("[+] Target process returned from dlysm, return r0=%x,pc=%x,\n",regs.ARM_r0,regs.ARM_pc);
    return regs.ARM_pc==0?(void*)regs.ARM_r0:NULL;
}

至此,注入代碼的主要原理和邏輯就結束了。但要真的成功實現注入,尤其是在高版本的android系統中實現注入,還需要做一些其它的準備工作。

find_pid_of獲取pid

從前面的代碼可以看到,進程注入需要目標進程的pid。最直接的pid獲取方法是在adb shell中使用ps指令來查詢某個進程的pid。但這樣太過麻煩,每次注入都要人工查詢。實際上只要知道進程的名字,就可以直接在程序中取得其pid。還是通過proc文件系統。

在proc/pid/cmdline中可以取得該進程的命令行參數,因而遍歷proc下所有進程的cmdline,找到和進程名匹配的命令行參數,其pid就是要找的pid。

int find_pid_of(const char *process_name)
{
    int id;
    pid_t pid = -1;
    DIR* dir;
    FILE *fp;
    char filename[32];
    char cmdline[256];

    struct dirent * entry;

    if (process_name == NULL)
        return -1;

    dir = opendir("/proc");
    if (dir == NULL)
        return -1;

    while((entry = readdir(dir)) != NULL) {
        id = atoi(entry->d_name);//read the name in /proc and try to turn it to number
        if (id != 0) {//if the name can be converted number, it represents the pid of process
            sprintf(filename, "/proc/%d/cmdline", id);
            fp = fopen(filename, "r");
            if (fp) {
                fgets(cmdline, sizeof(cmdline), fp);
                fclose(fp);
                LOGD("[d] cmdline: %s",cmdline);
                //strcmp(process_name, cmdline)
                //as we use strstr instead of strcmp,the process_name should be as accurate as
                //possible, otherwise it may find the pid of other process that have the similar name.
                if (strstr(cmdline,process_name) != NULL) {
                    // process found //
                    LOGI("[+] find the process, process name is %s, pid is %d",cmdline,id);
                    pid = id;
                    break;
                }
            }
        }
    }

    closedir(dir);
    return pid;
}

popen關閉selinux

爲了方便注入,建議直接把selinux關閉。可以在adb shell中使用setenforce 0關閉selinux,也可以在程序中使用popen關閉。popen定義如下:

FILE *popen(const char *command, const char *type);

int pclose(FILE *stream);

根據linux man pages,popen fork一個進程來執行shell命令,並且通過管道與本進程通信。由於管道是單向的,所以type參數只能是讀(“r")或者只能是寫(“w”)。command參數是包含shell命令的字符串指針。

popen的返回值是一個只能被pclose關閉的標準I/O流,如果失敗則返回NULL。pclose會等待相關進程結束然後返回結束狀態,如果失敗則返回-1。

所以關閉selinux只需要以下兩行代碼就好了。

    FILE* fp = popen("setenforce 0","r");
    pclose(fp);

解決dlopen對動態庫的限制問題

儘管在前面找到了dlopen的地址並調用了該函數,但由於高版本Android系統對動態庫的打開有所限制,所以這時候dlopen並不會成功,會返回0值。具體什麼限制可以去看android源碼。根據源碼,可以找到以下兩個解決方法。

如果要注入的進程是某個app,那麼只要把動態庫放到app的目錄下就好了。具體放在哪個目錄可以看log,看dlopen的異常信息中指明的default_library_paths等是什麼。

還有另外一種方法是把32位的動態庫放到/system/lib,把64位的動態庫放到system/lib64中(兩個目錄都要放),然後在/vendor/etc/public.libraries.txt或者/etc/public.libraries.txt中加入的動態庫名。前者在我的手機上不奏效,每次重啓後該文件自動恢復。後者在我的手機上是可行的。使用這種方法後,重啓手機,然後在adb shell中cat /proc/zygote_pid/maps |grep so名,就可以看到的動態庫。之後再在注入程序中dlopen就可以正常找到動態庫了。

在第二種方法中,由於要修改/system目錄下和/etc目錄下的文件,需要重新掛載這兩個目錄。命令如下:
mount -o rw,remount /
chmod 777 /system/lib
然後就可以adb push把文件push到裏面了。如果失敗,可能是adb的權限不夠,可以先把文件push到/data/local/tmp中,再在shell中以root的權限mv到/system目錄下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章