您的位置:首页 > 移动开发 > Android开发

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

2016-06-03 16:25 507 查看
Android平台上的Ptrace已经流行很久了,我记得最早的时候是LBE开始使用Ptrace在Android上做拦截,大概三年前我原来的同事yuki (看雪上的古河) 写了一个利用Ptrace注入的例子,被广泛使用,听说他还因此当上了版主,呵呵:
Android平台上的注入代码

两年前的时候我也写过一篇文章介绍利用Ptrace进行程序控制:
利用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 *)¶ms[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, ®s );


第3步,调用dlopen,将libhook.so注入hook_d的进程空间

有了第2步的基础,这一步几乎没有任何障碍,完成之后我们可以看到hook_d的进程空间里面已经注入了libhook.so



第4步,在hook_d中Hook函数__android_log_print,将地址改到libhook.so中的hookfun

Hook进程内函数的方法有很多种,最简单的就是改got表,我们不采用这种方法,因为改got表只能Hook表里面有的函数,且只能在调用处hook, 我希望做到的是任意代码处的hook.

那么如何做到呢?基本的想法就是像调试器一样修改代码,我手工画了个草图,供大家参考:



我们将修改__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的入口指令:



呵呵,没事,前两条指令和$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:



然后运行hook_s, 可以看到程序将libhook.so注入hook_d, 修改指令hook函数__android_log_print,然后正常detach, 程序退出。



接着我们看hook_d的输出,已经hook成功,ProjetName变成了hookit!且程序正常运行:



OK,总结一下,我们完成了从so注入到程序Hook的整个过程,也详细介绍了过程中的每一个技术细节,相信大家看完以后大部分问题都能找到答案了,附件中是全部的源码,供大家进一步研究。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: