0 前沿

本文主要分析了一份实现Android注入的代码的技术细节,但是并不涉及ptrace相关的知识,所以读者如果不了解ptrace的话,最好先学习下ptrace原理再来阅读本文。首先,感谢源代码的作者ariesjzj大牛,没有他的源码就没有本文~。文中有不对的地方,望各位大牛斧正!谢谢~

相关代码下载地址:

http://pan.baidu.com/s/1o6ul8eA

或者去代码原作者的blog:

http://blog.csdn.net/jinzhuojun/article/details/9900105

1 测试方法

①编译好inject和libhello.so之后,将inject和libhello.so放入/data/local/tmp/injecttest/目录下,并chmod 777.

②确定我们要inject的进程名。例如inject com.me.keygen.activity,那么我们首先使用ps来查看该进程的信息:

该进程的pid = 19989,进程名为"com.me.keygen.activity"。显然,对于apk而言,它的进程名就是apk的包名。

③确定hook进程之后,所要加入的so绝对路径。例如,本例要向com.me.keygen.activity中注入libhello.so,此libhello.so存放在/data/local/tmp/injecttest/目录下,那么它的绝对路径就为/data/local/tmp/injecttest/libhello.so

④开始注入,注入代码的格式为:

./inject  com.me.keygen.activity  /data/local/tmp/injecttest/libhello.so

注入成功的话就会显示:

library path = /data/local/tmp/injecttest/libhello.so

Press enter to dlclose and detach

⑤查看注入结果:

首先查看目的进程com.me.keygen.activity的内存空间中是否含有我们注入的libhello.so,方法如下:

我们在第②步获取了目的进程的pid = 19989,那么我们可以通过查看进程的mmaps文件:

root@android:/ # cat /proc/19989/maps |busybox grep hello

cat /proc/19989/maps |busybox grep hello

6674b000-6674e000 r-xp 00000000 b3:1a 3689       /data/local/tmp/injecttest/libhello.so

6674e000-6674f000 r--p 00002000 b3:1a 3689       /data/local/tmp/injecttest/libhello.so

6674f000-66750000 rw-p 00003000 b3:1a 3689       /data/local/tmp/injecttest/libhello.so

显然,so成功注入到了目的进程之中。

然后查看该so中的函数是否执行成功,方法如下:

由于我们注入目的进程后仅仅是执行libhello.so的hook_entry方法,该方法的代码如下:

int hook_entry(char * a){

LOGD("Hook success, pid = %d\n", getpid());

LOGD("Hello %s\n", a);

return 0;

}

而我们在Inject中调用该函数的代码为:

inject_remote_process(target_pid, hookSoPath, "hook_entry",  "I'm parameter!", strlen("I'm parameter!"));

所以我们理应在目的进程的logcat中找到字符串"Hello I'm parameter!"。查看指定进程的logcat方法如下:

1|root@android:/ # logcat |busybox grep 19989

logcat |busybox grep 19989

I/ActivityManager(  595): Start proc com.me.keygen.activity for activity com.me.

keygen.activity/.MainActivity: pid=19989 uid=10091 gids={3003, 1028}

D/ActivityThread(19989): setTargetHeapUtilization:0.25

D/ActivityThread(19989): setTargetHeapIdealFree:8388608

D/ActivityThread(19989): setTargetHeapConcurrentStart:2097152

I/HASH    (19989): 46e8b009bfa60ad36aaa6ad76aeb06d5

D/INJECT  (20047): [+] Injecting process: 19989

D/DEBUG   (19989): Hook success, pid = 19989

D/DEBUG   (19989): Hello I'm parameter!

 

测试成功!

2对inject代码的分析

2.1 find_pid_of(const char* targetProcessName)

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

if (id != 0) {

sprintf(filename, "/proc/%d/cmdline", id);

fp = fopen(filename, "r");

if (fp) {

fgets(cmdline, sizeof(cmdline), fp);

fclose(fp);

if (strcmp(process_name, cmdline) == 0) {

/* process found */

pid = id;

break;

}

}

}

}

closedir(dir);

return pid;

}

这个代码还是很简单的,就是通过遍历/proc目录下的所有子目录,获取这些子目录的目录名(一般就是进程的进程号pid)。获取子目录名后,就组合成/proc/pid/cmdline文件名,然后依次打开这些文件,cmdline文件里面存放的就是进程名,通过这样就可以获取进程的pid了。

2.2 inject_remote_process

此函数完整参数为:

inject_remote_process(pid_t target_pid, const char *library_path, const char *function_name, const char *param, size_t param_size)

其中library_path为我们想要注入到进程的so的绝对路径;

function_name为我们想要执行的函数名,此函数是so中的函数;

param为该函数的参数,param_size为参数大小,以字节为单位。

下面开始对此函数进行详细分析。

大致的注入过程如下:

ATTATCH,指定目标进程,开始调试;

GETREGS,获取目标进程的寄存器,保存现场;

SETREGS,修改PC等相关寄存器,使其指向mmap;

POPETEXT,把so path写入mmap申请的地址空间;

SETRESG,修改PC等相关寄存器,使其指向dlopen,调用dlopen,获取sohandle;

SETRESG,修改PC等相关寄存器,使其指向dlsym, functionaddr = dlsym(sohandle,"functionname");
SETRESG,修改PC等相关寄存器,使其指向functionaddr,调用function;

SETRESG,修改PC等相关寄存器,使其指向dlclose,调用dlclose(sohandle);

SETREGS,恢复现场;

DETACH,解除调试,使其恢复;

下面对照着代码进行分析。

①ATTATCH,指定目标进程,开始调试:

if
(ptrace_attach(target_pid) == -1)

goto exit;

②GETREGS,获取目标进程的寄存器,保存现场:

if
(ptrace_getregs(target_pid, &regs) == -1)

goto exit;

/* save original
registers */

memcpy(&original_regs, &regs, sizeof(regs));

③通过get_remote_addr函数获取目的进程的mmap函数的地址,以便为libxxx.so分配内存:

/*

需要对(void*)mmap进行说明:这是取得inject本身进程的mmap函数的地址,由于mmap函数在libc.so

库中,为了将libxxx.so加载到目的进程中,就需要使用目的进程的mmap函数,所以需要查找到libc.so库在目的进程的起始地址。

*/

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

这里需要对get_remote_addr函数进行说明:

/*

该函数为一个封装函数,通过调用get_module_base函数来获取目的进程的某个模块的起始地址,然后通过公式计算出指定函数在目的进程的起始地址。

*/

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);    //
获取远程pid的某个模块的起始地址

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

/*这需要我们好好理解:local_addr - local_handle的值为指定函数(如mmap)在该模块中的偏移量,然后再加上rempte_handle,结果就为指定函数在目的进程的虚拟地址*/

void * ret_addr = (void *)((uint32_t)local_addr -
(uint32_t)local_handle) + (uint32_t)remote_handle;        

return ret_addr;

}

显然,这里面核心的就是get_module_base函数:

/*

此函数的功能就是通过遍历/proc/pid/maps文件,来找到目的module_name的内存映射起始地址。

由于内存地址的表达方式是startAddrxxxxxxx-endAddrxxxxxxx的,所以会在后面使用strtok(line,"-")来分割字符串

如果pid = -1,表示获取本地进程的某个模块的地址,

否则就是pid进程的某个模块的地址。

*/

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, "-" ); //分解字符串为一组字符串。line为要分解的字符串,"-"为分隔符字符串。

addr = strtoul( pch, NULL, 16
);  //将参数pch字符串根据参数base(表示进制)来转换成无符号的长整型

if (addr == 0x8000)

addr = 0;

break;

}

}

fclose(fp) ;

}

return (void *)addr;

}

④通过ptrace_call_wrapper调用mmap函数,在目的进程中为libxxx.so分配内存:

/* call mmap (null,
0x4000, PROT_READ | PROT_WRITE | PROT_EXEC,

MAP_ANONYMOUS |
MAP_PRIVATE, 0, 0);

匿名申请一块0x4000大小的内存

*/

parameters[0] = 0;  // addr

parameters[1] = 0x4000; // 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

if (ptrace_call_wrapper(target_pid, "mmap",
mmap_addr, parameters, 6, &regs)
== -1)

goto exit;

........

ptrace_call_wrapper的代码:

int
ptrace_call_wrapper(pid_t target_pid, const char * func_name, void *
func_addr, long * parameters, int param_num, struct pt_regs * regs)

{

DEBUG_PRINT("[+] Calling %s in
target process.\n", func_name);

if (ptrace_call(target_pid, (uint32_t)func_addr, parameters,
param_num, regs)
== -1)   //详细见后面分析

return -1;

if (ptrace_getregs(target_pid, regs) == -1)

return -1;

DEBUG_PRINT("[+] Target process
returned from %s, return value=%x, pc=%x \n",

func_name, ptrace_retval(regs),
ptrace_ip(regs));

return 0;

}

ptrace_call函数比较复杂,我们可以看一下代码:

/*

功能总结:

1,将要执行的指令写入寄存器中,指令长度大于4个long的话,需要将剩余的指令通过ptrace_writedata函数写入栈中;

2,使用ptrace_continue函数运行目的进程,直到目的进程返回状态值0xb7f(对该值的分析见后面红字);

3,函数执行完之后,目标进程挂起,使用ptrace_getregs函数获取当前的所有寄存器值,方便后面使用ptrace_retval函数获取函数的返回值。

*/

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;    //将PC寄存器值设为目标函数的地址

if (regs->ARM_pc & 1) {  //进行指令集判断

/* thumb */

regs->ARM_pc &= (~1u);

regs->ARM_cpsr |= CPSR_T_MASK;  // #define CPSR_T_MASK  ( 1u << 5 )  CPSR为程序状态寄存器

} else {

/* arm */

regs->ARM_cpsr &= ~CPSR_T_MASK;

}

regs->ARM_lr = 0; //设置子程序的返回地址为空,以便函数执行完后,返回到null地址,产生SIGSEGV错误,详细作用见后面的红字分析

/*

*Ptrace_setregs就是将修改后的regs写入寄存器中,然后调用ptrace_continue来执行我们指定的代码

*/

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

printf("error\n");

return -1;

}

int stat = 0;

waitpid(pid, &stat, WUNTRACED);

/* WUNTRACED告诉waitpid,如果子进程进入暂停状态,那么就立即返回。如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。对于使用ptrace_cont运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。这里的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。那么什么时候会发生这种错误呢?显然,当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV了!

*/

while (stat != 0xb7f[w1] ) {  //这个循环是否必须我还不确定。因为目前每次ptrace_call调用必定会返回0xb7f,不过在这也算是增加容错性吧~

if (ptrace_continue(pid) == -1) {

printf("error\n");

return -1;

}

waitpid(pid, &stat, WUNTRACED);

}

return 0;

}

[w1]通过看ndk的源码sys/wait.h以及man waitpid可以知道这个0xb7f的具体作用。首先说一下stat的值:高2字节用于表示导致子进程的退出或暂停状态信号值,低2字节表示子进程是退出(0x0)还是暂停(0x7f)状态。0xb7f就表示子进程为暂停状态,导致它暂停的信号量为11即sigsegv错误。

其中ptrace_writedata的代码如下:

/*

Func : 将size字节的data数据写入到pid进程的dest地址处

@param dest: 目的进程的栈地址

@param data: 需要写入的数据的起始地址

@param size: 需要写入的数据的大小,以字节为单位

*/

int
ptrace_writedata(pid_t pid, uint8_t *dest, uint8_t *data, size_t size)

{

uint32_t i, j, remain;

uint8_t *laddr;

union u {

long val;

char chars[sizeof(long)];

} d; 
//很巧妙的联合体,这样就可以方便的以字节为单位写入4字节数据,再以long为单位ptrace_poketext到栈中

j = size / 4;

remain = size % 4;

laddr = data;

for (i = 0; i < j; i ++) {    //先以4字节为单位进行数据写入

memcpy(d.chars, laddr, 4);

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

dest 
+= 4;

laddr += 4;

}

if (remain > 0) {    //为了最大程度的保持原栈的数据,先读取dest的long数据,然后只更改其中的前remain字节,再写回

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;

}

总结一下ptrace_call_wrapper,它的完成两个功能:

一是调用ptrace_call函数来执行指定函数,执行完后将子进程挂起;

二是调用ptrace_getregs函数获取所有寄存器的值,主要是为了获取r0即函数的返回值。

⑤从寄存器中获取mmap函数的返回值,即申请的内存首地址:

map_base =
ptrace_retval(&regs);

⑥依次获取linker中dlopen、dlsym、dlclose、dlerror函数的地址:

dlopen_addr =
get_remote_addr( target_pid, linker_path, (void *)dlopen );

dlsym_addr =
get_remote_addr( target_pid, linker_path, (void *)dlsym );

dlclose_addr =
get_remote_addr( target_pid, linker_path, (void *)dlclose );

dlerror_addr =
get_remote_addr( target_pid, linker_path, (void *)dlerror );

⑦调用dlopen函数:

/*

①将要注入的so名写入前面mmap出来的内存

②写入dlopen代码

③执行dlopen("libxxx.so", RTLD_NOW ! RTLD_GLOBAL)

RTLD_NOW之类的参数作用可参考:

http://baike.baidu.com/view/2907309.htm?fr=aladdin

④取得dlopen的返回值,存放在sohandle变量中

*/

ptrace_writedata(target_pid,
map_base, library_path, strlen(library_path) + 1);

parameters[0] =
map_base;

parameters[1] =
RTLD_NOW| RTLD_GLOBAL;

if
(ptrace_call_wrapper(target_pid, "dlopen", dlopen_addr, parameters,
2, &regs) == -1)

goto exit;

void * sohandle =
ptrace_retval(&regs);

⑧调用dlsym函数:

/*

等同于hook_entry_addr =
(void *)dlsym(sohandle, "hook_entry");

*/

#define
FUNCTION_NAME_ADDR_OFFSET      
0x100  //为functionname另找一块区域

ptrace_writedata(target_pid, map_base + FUNCTION_NAME_ADDR_OFFSET,
function_name, strlen(function_name) + 1);

parameters[0] = sohandle;

parameters[1] = map_base + FUNCTION_NAME_ADDR_OFFSET;

if
(ptrace_call_wrapper(target_pid, "dlsym", dlsym_addr, parameters,
2, &regs) == -1)

goto
exit;

void *
hook_entry_addr = ptrace_retval(&regs);

DEBUG_PRINT("hook_entry_addr = %p\n", hook_entry_addr);

⑨调用hook_entry函数:

/*

hook_entry("I'm parameter!");

*/

#define
FUNCTION_PARAM_ADDR_OFFSET     
0x200

ptrace_writedata(target_pid, map_base + FUNCTION_PARAM_ADDR_OFFSET,
param, strlen(param) + 1);

parameters[0] = map_base + FUNCTION_PARAM_ADDR_OFFSET;

if
(ptrace_call_wrapper(target_pid, "hook_entry", hook_entry_addr,
parameters, 1, &regs) == -1)

goto
exit;

⑩调用dlclose关闭lib:

/*

等同于dlclose(sohandle);

*/

printf("Press
enter to dlclose and detach\n");

getchar();

parameters[0] = sohandle;

if
(ptrace_call_wrapper(target_pid, "dlclose", dlclose, parameters, 1,
&regs) == -1)

goto exit;

⑪恢复现场并退出ptrace:

ptrace_setregs(target_pid,
&original_regs);

ptrace_detach(target_pid);

总结

分析完整个注入代码,学到了很多东西:ptrace(特别是向目的进程的寄存器和栈中写入参数),信号量机制,以及在获取pid、模块基址时使用的方法等等等等。同时,也注意到这份代码与看雪论坛古河大大发出的代码有些许不同:后者是将dlopen,dlsym等函数放在了一个用汇编写的injectcode.s中,而用C写的注入代码仅仅将injectcode.s注入到目标进程中,过后就交由这个injectcode.s来完成后续工作了。显然,就学习而言,本代码完全用C语言实现,学习起来简单易懂,且injectcode.s的编写难度也大很多。同时需要注意的是,如果采用后者的方法,在注入代码之前一定得预留部分空间用作函数调用的栈空间:

// 设置远程代码存储空间地址

remote_code_ptr = map_base+0x3C00; // 这里就预留了0x3c00的空间

展望

那注入之后我们到底可以完成什么功能呢?目前,据我了解,大家主要还是用于hook so中的native函数。那么如何hook呢?可以参考这篇文章的NDK HOOK部分:

http://bbs.pediy.com/showthread.php?t=192047

Android注入完全剖析的更多相关文章

  1. Android源码剖析之Framework层升级版(窗口、系统启动)

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 看本篇文章之前,建议先查看: Android源码剖析之Framework层基础版 前面讲了frame ...

  2. Android 注入详解

    Android下的注入的效果是类似于Windows下的dll注入,关于Windows下面的注入可以参考这篇文章Windows注入术.而Android一般处理器是arm架构,内核是基于linux,因此进 ...

  3. Android注入事件的三种方法比较

    方法1:使用内部APIs 该方法和其他所有内部没有向外正式公布的APIs一样存在它自己的风险.原理是通过获得WindowManager的一个实例来访问injectKeyEvent/injectPoin ...

  4. 进击的Android注入术《二》

    继续 在<一>里,我把基本思路描写叙述了一遍,接下为我们先从注入開始入手. 注入 分类 我们平时所说的代码注入,主要静态和动态两种方式 静态注入,针对是可运行文件,比方平时我们改动ELF, ...

  5. Android系统架构剖析(一)

          要说剖析,可能这个词可能用的太大了,以下对Android系统的介绍也就是从我个人理解来说吧.       以前有人问我,Android是什么?当时这个问题问的我真的蒙了,我就简单的回了一下 ...

  6. Monkey源代码分析番外篇之Android注入事件的三种方法比較

    原文:http://www.pocketmagic.net/2012/04/injecting-events-programatically-on-android/#.VEoIoIuUcaV 往下分析 ...

  7. 进击的Android注入术《一》

    写在前面 这个系列本来是在公司的一个分享.内容比較多,所以就把这个PPT又一次组织整理成博客,希望对大家学习有所帮助.我会先以一个"短信拦截"作为样例,抛出问题,并提出了一种基于& ...

  8. android 注入框架 DI

    android 主要注入框架以及github如下: (1)Roboguice https://github.com/roboguice/roboguice (2)Butterknife https:/ ...

  9. Android示例程序剖析之记事本(一)

    Android SDK提供了很多示例程序,从这些示例代码的阅读和试验中能够学习到很多知识.本系列就是要剖析Android记事本示例程序,用意就是一步步跟着实例进行动手操作,在实践中体会和学习Andro ...

随机推荐

  1. Bootstrap 弹出框(Popover)插件

    Bootstrap 弹出框(Popover)插件与Bootstrap 提示工具(Tooltip)插件类似,提供了一个扩展的视图,用户只需要把鼠标指针悬停到元素上面即可.弹出框的内容完全由Bootstr ...

  2. java基础面试题:如何把一段逗号分割的字符串转换成一个数组? String s = "a" +"b" + "c" + "d";生成几个对象?

    package com.swift; public class Douhao_String_Test { public static void main(String[] args) { /* * 如 ...

  3. 对于新能源Can数据、电池BMS等字节和比特位的解析

    1.对于1个字节(8个bit)以上的数据需要先进行倒序(因为高位在前 低位在后). CanID CanData 排序后的 字节数据 十进制 分辨率(0.005) 偏移量(40) 0x18FEC117 ...

  4. Java 获取Web项目相对webapp地址

    例如, import java.io.File; import java.io.FileInputStream; import javax.servlet.http.HttpServletReques ...

  5. Java - 静态方法不具有多态性

    class A1 { public static void f() {  System.out.println("A1.f()"); }}class A2 extends A1 { ...

  6. 认识/etc/passwd和/etc/shadow

    认识/etc/passwd和/etc/shadow============================== /etc/passwd [root@aminglinux ~]# head -n1 /e ...

  7. git bush的一些基础命令

    git bush的一些基础命令(不区分大小写) 通过命令创建本地仓库 首先自己需要手动建一个文件夹用于本地仓库 进行如下输入,使用cd跳转到刚刚创建的文件夹中 之后再输入 git init 即可创建 ...

  8. 整合mybatis和spring时 Error creating bean with name 'sqlSessionFactory' defined in class path resource

    今天在整合mybatis和spring的时候出的错 报错如下 Exception in thread "main" org.springframework.beans.factor ...

  9. jupyter notebook(三)——IOPub_data_rate_limit报错

    一.问题 运行jupyter notebook,然后运行python代码,读取文件处理时,会报错.发现时IO读取时错误.应该是IO速率问题. 下面是问题报错: IOPub data rate exce ...

  10. Ubuntu下的定时备份数据库

    1.编写备份数据库的shell脚本 mysqldump -uUserName -pPassword dbName >/XXX/XXXX/XXXX/fileName_$(date +%Y%m%d_ ...