android hook 框架 ADBI 如何實現so注入


Android so注入-libinject2 簡介、編譯、運行

Android so注入-libinject2  如何實現so注入

Android so注入-Libinject 如何實現so注入

Android so注入掛鈎-Adbi 框架簡介、編譯、運行

Android so注入掛鈎-Adbi 框架如何實現so注入

Android so注入掛鈎-Adbi 框架如何實現so函數掛鈎

Android so注入掛鈎-Adbi 框架如何實現dalvik函數掛鈎

Android dalvik掛鈎-Xposed框架如何實現注入

Android dalvik掛鈎-Xposed框架如何實現掛鈎

 

源碼分析

hijack.c

這個文件實現了一個注入工具,可以向 -p 參數指定的進程注入一個so。

要實現這個效果,首先,需要得到目標進程若干函數如dlopen函數的地址,其次,需要能影響目標進程的正常執行流,讓其中間某個時候執行dlopen加載指定的庫,最后,還要能用動態加載的so里的函數覆蓋原有內存里的函數。

 

下面開始研究,如何得到目標進程指定函數的地址,首先要得到的是dlopen函數的地址,adbi是這么做的:

    void *ldl = dlopen("libdl.so", RTLD_LAZY);
    if (ldl) { dlopenaddr = (unsigned long)dlsym(ldl, "dlopen");//dlopenaddr 存放本進程的dlopen函數地址 dlclose(ldl); } unsigned long int lkaddr; unsigned long int lkaddr2; find_linker(getpid(), &lkaddr); find_linker(pid, &lkaddr2); dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr); // dlopenaddr 存放目標進程的dlopen函數的地址

 

上述代碼是為了得到目標進程的dlopen函數地址。

首先,dlopen加載libdl.so,由於進程啟動后libdl.so肯定會先加載好,所以這里返回已經加載好的libdl.so映射在本進程的起始地址空間,然后調用dlsym返回本進程的dlopen函數地址。

接着,find_linker函數利用 /proc/pid/maps 文件可以得到進程pid的地址空間進而得到libdl.so映射到內存的起始地址,其中,注入進程的libdl.so映射的初始地址是 lkaddr, 目標進程是lkaddr2

最后,再利用dlopen函數在libdl.so動態庫的代碼的偏移是固定的(注入進程和被注入進程使用的是同一個libdl.so),dlopenaddr - lkaddr 先算出這個偏移值,lkaddr2 再上上述偏移值即得到目標進程的 dlopen 函數的地址

 maps文件在linux和android上的地址塊命名有些區別,一般linux上libdl.so映射的地址是這樣的

 7f6a96672000-7f6a96695000 r-xp 00000000 08:01 397502                  /lib/x86_64-linux-gnu/ld-2.19.so

android 里的命名叫 linker

find_linker 函數調用了 load_memmap函數和 find_linker_mem函數,

static int find_linker(pid_t pid, unsigned long *addr)
{
    struct mm mm[1000]; unsigned long libcaddr; int nmm; char libc[256]; symtab_t s; if (0 > load_memmap(pid, mm, &nmm)) { printf("cannot read memory map\n"); return -1; } if (0 > find_linker_mem(libc, sizeof(libc), &libcaddr, mm, nmm)) { printf("cannot find libc\n"); return -1; } *addr = libcaddr; return 1; }

load_memmap 函數基本流程:打開maps文件,按照maps文件的格式解析成一個數組,每一項存放一個動態庫的名稱以及其映射到內存里的起始和結束地址

static int
load_memmap(pid_t pid, struct mm *mm, int *nmmp) { char raw[80000]; // this depends on the number of libraries an executable uses char name[MAX_NAME_LEN]; char *p; unsigned long start, end; struct mm *m; int nmm = 0; int fd, rv; int i; sprintf(raw, "/proc/%d/maps", pid); fd = open(raw, O_RDONLY); if (0 > fd) { printf("Can't open %s for reading\n", raw); return -1; } /* Zero to ensure data is null terminated */ memset(raw, 0, sizeof(raw)); p = raw; while (1) { rv = read(fd, p, sizeof(raw)-(p-raw)); if (0 > rv) { //perror("read"); return -1; } if (0 == rv) break; p += rv; if (p-raw >= sizeof(raw)) { printf("Too many memory mapping\n"); return -1; } } close(fd); p = strtok(raw, "\n"); m = mm; while (p) { /* parse current map line */ rv = sscanf(p, "%08lx-%08lx %*s %*s %*s %*s %s\n", &start, &end, name); p = strtok(NULL, "\n"); if (rv == 2) { m = &mm[nmm++]; m->start = start; m->end = end; strcpy(m->name, MEMORY_ONLY); continue; } if (strstr(name, "stack") != 0) { stack_start = start; stack_end = end; } /* search backward for other mapping with same name */ for (i = nmm-1; i >= 0; i--) { m = &mm[i]; if (!strcmp(m->name, name)) break; } if (i >= 0) { if (start < m->start) m->start = start; if (end > m->end) m->end = end; } else { /* new entry */ m = &mm[nmm++]; m->start = start; m->end = end; strcpy(m->name, name); } } *nmmp = nmm; return 0; }

find_linker_mem函數的流程:遍歷上述數組,根據動態庫名稱匹配,即可獲取libdl.so對應的數組元素,從而得到libdl.so在進程內的起始和終止地址,代碼這里就不貼了。

 

以上,是獲取目標進程某個動態庫內的函數在目標進程的真實地址的方法。那么目標進程,非動態庫函數的地址怎么獲取呢?

 

=== ==

接下去研究第二個問題,如何影響目標進程的執行流,這里必須介紹ptrace函數了。

 

ptrace

SYNOPSIS
       #include <sys/ptrace.h>

       long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);

DESCRIPTION
       The  ptrace()  system  call  provides  a  means  by which one
       process (the "tracer") may observe and control the  execution
       of another process (the "tracee"), and examine and change the
       tracee's memory and  registers.   It  is  primarily  used  to
       implement breakpoint debugging and system call tracing.

 動態庫注入技術一般都依賴於ptrace機制,ptrace是linux kernel 為了支持應用層debug功能而實現的系統調用,這個系統調用提供了“讓A進程關聯到B進程,並動態修改B進程的內存和寄存器”的機制,A進程可以通過修改B進程的寄存器讓B進程執行特定代碼,並加載特定代碼到B進程的特定內存。對於不同的CPU體系架構(x86,x86_64,arm,arm64,mips)等,寄存器的數據結構顯然是不一樣的,這個結構一般叫 struct pt_regs, 定義在 asm/ptrace.h 文件里, 下面是我在android5源碼根目錄搜索后得到的定義:

root@ubuntu:android-tsinghua# find . -name ptrace.h
./external/kernel-headers/original/uapi/linux/ptrace.h
./external/kernel-headers/original/uapi/asm-mips/asm/ptrace.h
./external/kernel-headers/original/uapi/asm-x86/asm/ptrace.h
./external/kernel-headers/original/uapi/asm-arm/asm/ptrace.h
./external/kernel-headers/original/uapi/asm-arm64/asm/ptrace.h

其中,adbi 項目適配的是 arm 架構,在 asm-arm/asm/ptrace.h 里, struct pt_regs 定義如下:

#ifndef __KERNEL__
struct pt_regs {
        long uregs[18];
};
#endif /* __KERNEL__ */

#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]

這個定義被adbi項目直接拷貝到其源碼里並重命名為 struct pt_regs2 了。

 

下面研究adbi是怎么使用ptrace達到目的的:

首先,attach到目標進程

  if (0 > ptrace(PTRACE_ATTACH, pid, 0, 0)) {
        printf("cannot attach to %d, error!\n", pid); exit(1); } waitpid(pid, NULL, 0);

 其次,獲取目標進程當前寄存器

ptrace(PTRACE_GETREGS, pid, 0, &regs);

接着,構造新的寄存器值,這一步是關鍵。

數組sc存放初始化的指令

unsigned int sc[] = {
0xe59f0040, //        ldr     r0, [pc, #64]   ; 48 <.text+0x48>
0xe3a01000, //        mov     r1, #0  ; 0x0
0xe1a0e00f, //        mov     lr, pc
0xe59ff038, //        ldr     pc, [pc, #56]   ; 4c <.text+0x4c>
0xe59fd02c, //        ldr     sp, [pc, #44]   ; 44 <.text+0x44>
0xe59f0010, //        ldr     r0, [pc, #20]   ; 30 <.text+0x30>
0xe59f1010, //        ldr     r1, [pc, #20]   ; 34 <.text+0x34>
0xe59f2010, //        ldr     r2, [pc, #20]   ; 38 <.text+0x38>
0xe59f3010, //        ldr     r3, [pc, #20]   ; 3c <.text+0x3c>
0xe59fe010, //        ldr     lr, [pc, #20]   ; 40 <.text+0x40>
0xe59ff010, //        ldr     pc, [pc, #20]   ; 44 <.text+0x44>
0xe1a00000, //        nop                     r0
0xe1a00000, //        nop                     r1 
0xe1a00000, //        nop                     r2 
0xe1a00000, //        nop                     r3 
0xe1a00000, //        nop                     lr 
0xe1a00000, //        nop                     pc
0xe1a00000, //        nop                     sp
0xe1a00000, //        nop                     addr of libname
0xe1a00000, //        nop                     dlopenaddr
};

下面使用ptrace獲取的寄存器值填充到sc數組的11到17,

    sc[11] = regs.ARM_r0;
    sc[12] = regs.ARM_r1; sc[13] = regs.ARM_r2; sc[14] = regs.ARM_r3; sc[15] = regs.ARM_lr; sc[16] = regs.ARM_pc; sc[17] = regs.ARM_sp;

然后用前面獲取到的目標進程的dlopen函數的地址填充到第19位置,18位置存放動態庫的名字字符串的地址,然后調用wirte_mem函數將動態庫的名字字符串寫到libaddr地址指定的內存區。

  sc[19] = dlopenaddr;
        // push library name to stack
    libaddr = regs.ARM_sp - n*4 - sizeof(sc); sc[18] = libaddr; // write library name to stack if (0 > write_mem(pid, (unsigned long*)arg, n, libaddr)) { printf("cannot write library name (%s) to stack, error!\n", arg); exit(1); }

其中,n是這么算的,動態庫(如 /data/local/tmp/libexample.so)的字節數+1,然后除以4,如果有余數,結果加1. 其實,得到的n就是以‘4字節’為單位的數值,這么算主要是 write_mem函數的實現,下面會看到。結合上面的代碼,這個字符串會被寫入 libaddr 對應的內存,這個內存地址是這么算的: 

regs.ARM_sp - n*4 - sizeof(sc); 即原來的棧頂指針往低地址移動 “sc 數組大小+動態庫字符串字節長度” 
            case 'l':
                                n = strlen(optarg)+1; n = n/4 + (n%4 ? 1 : 0); arg = malloc(n*sizeof(unsigned long)); memcpy(arg, optarg, n*4); break;

下面看write_mem是怎么將一塊數據寫入目標進程的內存地址的:

static int
write_mem(pid_t pid, unsigned long *buf, int nlong, unsigned long pos) { unsigned long *p; int i; for (p = buf, i = 0; i < nlong; p++, i++) if (0 > ptrace(PTRACE_POKETEXT, pid, (void *)(pos+(i*4)), (void *)*p)) return -1; return 0; }

pid是目標進程標識,buf是要寫入目標進程內存的數據塊,nlong是‘4字節’為單位的長度,pos是要寫入的地址。 由於數據buf是 long 型數組,所以循環一次即寫入4字節的數據。最終是調用 ptrace 函數,另第一個參數為  PTRACE_POKETEXT 實現寫入的。

接下去,寫入新的指令數據(即sc數組)到目標進程:

// write code to stack
    codeaddr = regs.ARM_sp - sizeof(sc);
    if (0 > write_mem(pid, (unsigned long*)&sc, sizeof(sc)/sizeof(long), codeaddr)) { printf("cannot write code, error!\n"); exit(1); }// calc stack pointer

可以看到,方法跟上述寫動態庫名字字符串類似,要寫入的目標地址 = 棧頂指針 - Sc 數組長度,然后調用write_mem函數將數組Sc寫入

接下去,移動棧頂指針為新的棧頂(往低地址移動”sc數組長度+動態庫名字字符串長度“),接下去,根據是否有 mprotect 調用,會有兩種執行流:如果沒有mprotect,則將PC寄存器的值變成sc數組開始的位置,即接下去直接執行Sc數組的指令。否則,pc寄存器的值設置為 mprotect 函數,然后將lr寄存器設置為sc數組。並將r0,r1,r2參數設置為 mprotect調用的參數,這樣,首先執行 mprotect 函數將 r0,r1指定的內存范圍設置為 r2指定的權限,在這個例子是,是將目標內存設置為 rwx, 執行完mprotect后再執行 sc 數組的指令。

    regs.ARM_sp = regs.ARM_sp - n*4 - sizeof(sc);

    // call mprotect() to make stack executable
    regs.ARM_r0 = stack_start; // want to make stack executable
    //printf("r0 %x\n", regs.ARM_r0);
    regs.ARM_r1 = stack_end - stack_start; // stack size
    //printf("mprotect(%x, %d, ALL)\n", regs.ARM_r0, regs.ARM_r1);
    regs.ARM_r2 = PROT_READ|PROT_WRITE|PROT_EXEC; // protections

    // normal mode, first call mprotect
    if (nomprotect == 0) { if (debug) printf("calling mprotect\n"); regs.ARM_lr = codeaddr; // points to loading and fixing code regs.ARM_pc = mprotectaddr; // execute mprotect()  } // no need to execute mprotect on old Android versions else {  regs.ARM_pc = codeaddr; // just execute the 'shellcode' }

經過上述設置后,新的寄存器值如下圖:

結合上面的圖,新的指令流執行如下(下面轉自”參考1“)

一點需要說明一下,對於ARM處理器來說,pc寄存器的值,指向的不是當前正在執行指令的地址,而是往下第二條指令的地址。

好,我們正式開始分析代碼的含義,指令將從codeaddr指示的位置從低到高依次執行。

第一條指令將pc寄存器的值加上64,讀出那個地方的內容(4個字節),然后放到寄存器r0中。剛才說過了,pc寄存器值指向的是當前指令位置加8個字節,也就是說這條指令實際讀出的是當前指令位置向下72個字節。由於sc數組是int型的,就是數組當前元素位置向下18個元素處。數一數,剛好是libaddr的位置。所以這條指令是為了讓r0寄存器指向.so共享庫路徑名字符串。

第二條指令很簡單,是將0賦值給寄存器r1。

第三條指令用來將pc寄存器值保存到lr寄存器中,這樣做的目的是為了調用dlopen()函數返回后,跳轉到指令“ldr sp, [pc, #56]”處。

第四條指令是將pc加上56處的數值加載到pc中,pc+56處是哪?當前指令位置往下64字節,16個元素,剛好是dlopen()函數的調用地址。所以,這條指令其實就是調用dlopen()函數,傳入的參數一個是r0寄存器指向的共享庫路徑名,另一個是r1寄存器中的0。

調用dlopen()返回后將繼續執行下面的所有指令,我就不一一分析了,作用就是恢復目標進程原來寄存器的值。先是sp,然后是r0、r1、r2、r3和lr,最后恢復原來pc的值,繼續執行被暫停之前的指令,就像什么都沒發生過一樣。

=====

最后,使用ptrace設置新的寄存器值進入目標內存,hook開始生效

      ptrace(PTRACE_SETREGS, pid, 0, &regs);
        ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);

 

最后,如果參數有 -s ,還會執行下述流程:

    if (appname) {    
        if (ptrace(PTRACE_SETOPTIONS, pid, (void*)1, (void*)(PTRACE_O_TRACEFORK))) {
            printf("FATAL ERROR: ptrace(PTRACE_SETOPTIONS, ...)");
            return -1;
        }
        ptrace(PTRACE_CONT, pid, (void*)1, 0);

        int t;
        int stat;
        int child_pid = 0;
        for (;;) {
            t = waitpid(-1, &stat, __WALL|WUNTRACED);

            if (t != 0 && t == child_pid) {char fname[256];
                sprintf(fname, "/proc/%d/cmdline", child_pid);
                int fp = open(fname, O_RDONLY);
                if (fp < 0) {
                    ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
                    continue;
                }
                read(fp, fname, sizeof(fname));
                close(fp);

                if (strcmp(fname, appname) == 0) {
                   // detach from zygote
                    ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);

                    // now perform on new process
                    pid = child_pid;
                    break;
                }
                else {
                    ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
                    continue;
                }
            }

            if (WIFSTOPPED(stat) && (WSTOPSIG(stat) == SIGTRAP)) {
                if ((stat >> 16) & PTRACE_EVENT_FORK) {
                    if (debug > 1)
                        printf("fork\n");
                    int b = t; // save parent pid
                    ptrace(PTRACE_GETEVENTMSG, t, 0, &child_pid); 
                    t = child_pid;          
                    ptrace(PTRACE_CONT, b, (void*)1, 0);
                    ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
                }
            }
        }
    }

    if (zygote) {
        int i = 0;
        for (i = 0; i < zygote; i++) {
            // -- zygote fix ---
            // we have to wait until the syscall is completed, IMPORTANT!
            ptrace(PTRACE_SYSCALL, pid, 0, 0);
            if (debug > 1)
                printf("/");
            waitpid(pid, NULL, 0);

            ptrace(PTRACE_GETREGS, pid, 0, &regs);    
            if (regs.ARM_ip != 0) {
                if (debug > 1)
                    printf("not a syscall entry, wait for entry\n");
                ptrace(PTRACE_SYSCALL, pid, 0, 0);
                waitpid(pid, NULL, 0);
            }

            ptrace(PTRACE_SYSCALL, pid, 0, 0);
            if (debug > 1)
                printf("\\");
            waitpid(pid, NULL, 0);
         
        }
    }

 

 

參考

http://blog.csdn.net/roland_sun/article/details/34109569 

ptrace運行原理及使用詳解

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM