本文博客地址:http://blog.csdn.net/qq1084283172/article/details/53942648
前面深入学习了古河的Libinject注入Android进程,下面来 深入学习一下作者ariesjzj的博文《Android中的so注入(inject)和挂钩(hook) - For both x86 and arm》,注入的思路和古河的是一样的,但是代码的兼容性更好更好理解,适用于arm和x86模式下so注入和函数的Hook,这份代码自己也测试了一下,确实可以Hook目标函数成功,只是被Hook修改的目标函数,可能没有被系统调用,导致测试效果和作者ariesjzj给出稍有区别。
按照作者ariesjzj提供的代码文件,在eclipse中构建一个ndk的Inject代码工程(用于实现so的注入)和一个ndk的Inject_so代码工程(被注入的so以及实现函数Hook),并将作者ariesjzj提供的代码分别导入到这两个ndk的代码工程中,并按照我在前面博客中提到的方法为ndk工程添加必要的include头文件(Paths
and Symbols),添加方法如下:
windows环境下,NDK编译需要添加的include头文件(根据编译的版本需要进行修改):
右击项目 --> Properties --> 左侧C/C++ General --> Paths and Symbols --> 右侧Includes --> GNU C++(.cpp) --> Add
${NDKROOT}\platforms\android-19\arch-arm\usr\include
${NDKROOT}\sources\cxx-stl\gnu-libstdc++\4.8\include
${NDKROOT}\sources\cxx-stl\gnu-libstdc++\4.8\libs\armeabi\include
${NDKROOT}\toolchains\arm-Linux-androideabi-4.8\prebuilt\windows\lib\gcc\arm-linux-androideabi\4.8\include
找不到头文件 的错误
需要修改头文件
#include
so注入Hook函数是基于ELF文件GOT表的Hook
说明的明白一点,就是被Hook的目标函数在该elf文件格式的so库文件中需要被导出,能被动态查找到(got表里存放的是外部符号的地址,不是所有符号的地址都能在got表找到)。当然了,一般在linux下,函数默认就是被导出的,顺便提一下,在so库文件中,函数没有被导出也是可以Hook的,因为函数调用地址在so库文件中的存储位置偏移是可以手工计算出来,只不过麻烦一点点,通用性差点。有关so文件的非导出函数的调用地址的计算,可以参考《linux下调用共享库非导出函数》里的方法。
生成共享库的代码 share.c :
#include <stdio.h>
// 默认的导出函数
void PrintABCD() //__attribute__((visibility("hidden")))
{
printf("ABCD\n");
sleep(-1);
}
// +++++++设置为非导出函数++++++++
__attribute__((visibility("hidden"))) void Printabcd()
{
printf("abcd\n");
sleep(-1);
}
// 根据参数来确定print ABCD or abcd(默认的导出函数)
void PrintIt(int isPrintCapital)
{
if(isPrintCapital)
{
PrintABCD();
}
else
{
Printabcd();
}
printf("over!\n");
}
生成共享库 share.so:
gl-linux@ubuntu:~/Desktop/AYR$ gcc -fPIC -shared -o share.so share.c
测试代码 test.c:
#include <stdio.h>
#include <dlfcn.h>
#include<unistd.h>
#define DLL_FILE_NAME "/home/gl-linux/Desktop/AYR/share.so"
int main()
{
long long addr = 0;
void (*func)(int);
void *handle = dlopen(DLL_FILE_NAME, RTLD_NOW);
if (handle == NULL)
{
fprintf(stderr, "Failed to open libaray %s error:%s\n", DLL_FILE_NAME, dlerror());
return -1;
}
addr = (long long)dlsym(handle, "PrintABCD");
printf("%lld ",addr);
addr = (long long)dlsym(handle, "Printabcd");
printf("%lld ",addr);
addr = (long long)dlsym(handle, "PrintIt");
printf("%lld ",addr);
func = addr;
func(1);
dlclose(handle);
sleep(-1);
return 0;
}
生成可执行文件 test:
gl-linux@ubuntu:~/Desktop/AYR$ gcc test.c -o test -ldl
运行可执行文件 test:
139725025953621 0 139725025953687 ABCD
从上面的输出结果可以看出,非导出函数Printabcd的地址没有被dlsym函数获取到,一般非导出函数也被称作 内部函数,应该和函数的生命周期也就是有效范围类似,当然了这个访问的限制是可以被突破的。
查看share.so中的导出函数偏移:
0000000000201048 B __bss_start
w __cxa_finalize
0000000000201048 D _edata
0000000000201050 B _end
00000000000007cc T _fini
w __gmon_start__
00000000000005e8 T _init
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
w _Jv_RegisterClasses
0000000000000755 T PrintABCD
0000000000000797 T PrintIt
U puts
U sleep
已知导出函数的偏移地址,又知道模块加载基址,怎么能知道非导出函数的地址呢?
使用计算后的地址,调用非导出函数Printabcd(),修改测试程序代码 TEST.c 如下:
#include <stdio.h>
#include <dlfcn.h>
#include<unistd.h>
#define DLL_FILE_NAME "/home/gl-linux/Desktop/AYR/share.so"
int main()
{
long long addr = 0;
void (*func)();
void *handle = dlopen(DLL_FILE_NAME, RTLD_NOW);
if (handle == NULL)
{
fprintf(stderr, "Failed to open libaray %s error:%s\n", DLL_FILE_NAME, dlerror());
return -1;
}
addr = (long long)dlsym(handle, "PrintABCD");
printf("%lld ",addr);
addr = (long long)dlsym(handle, "Printabcd");
printf("%lld ",addr);
addr = (long long)dlsym(handle, "PrintIt");
printf("%lld ",addr);
// 调用共享库share.so中的非导出函数。
func = addr-33;
func();
dlclose(handle);
sleep(-1);
return 0;
}
运行结果:
gl-linux@ubuntu:~/Desktop/AYR$ ./TEST
139670664996693 0 139670664996759 abcd
根据上面的输出结果,很显然调用 share.so文件中的非导出函数Printabcd成功。
有网友提示,下面这个Linux版本的 nm 可以查看so库文件中 非导出函数的位置偏移:
Linux ubuntu 3.17.1 #1 SMP Fri Oct 24 09:08:26 PDT 2014 x86_64 GNU/Linux版本nm -D xx.so,可以看到非导出函数的偏移, 只不过是t不是T
000000000000068c T PrintABCD
00000000000006ce T PrintIt
00000000000006ad t Printabcd
其实so库文件中的非导出函数的调用地址很简单,将so文件拖入到IDA中分析,找到静态下非导出函数和导出函数的函数调用地址的位置偏移offsetOfFunciton或者非导出函数调用地址offsetOfBaseAddr,然后so库文件中非导出函数的地址,即为动态内存的导出函数地址+offsetOfFunciton或者offsetOfBaseAddr+base(so文件的内存加载基地址)。
感谢连接:
http://bbs.pediy.com/showthread.php?t=211895
http://bbs.pediy.com/showthread.php?t=196864
so注入Hook目标函数的代码的兼容性的考虑的原因
在arm模式和x86模式下,函数的堆栈工作原理是一样的,但是在具体到工作的实现细节上还是有很多的区别,arm模式的函数是基于寄存器传参,x86模式的函数是基于堆栈传参。
arm模式下,当函数的参数低于4个时,通过R0~R3寄存器传递参数;当函数的参数超过4个时,前4个参数通过R0~R3寄存器传递,超过4个的参数通过函数的堆栈进行传递,函数的程序计数寄存器为PC(存储将要执行的指令),函数执行完成后的函数返回值在R0中,
程序的状态寄存器为CPSR,函数的返回地址放在LR(程序连接寄存器)中,arm的函数调用堆栈图如下:
x86模式下,函数一般通过堆栈进行传递所有函数的参数,C/C++默认的函数调用约定为 __cdecl 调用约定,调用方负责平衡堆栈,不定参数的函数可以使用;参数的入栈方式为从右往左依次压入系统栈中,紧接着函数的返回地址入栈保存;EIP寄存器控制着进程的指令的执行,函数的返回值保存在eax寄存器中,x86下函数的调用堆栈图如下:
示意图2
so注入Hook目标函数的代码中,x86模式下,导出函数的地址需要+2的讨论。
作者ariesjzj提供的Android
so注入Hook目标函数的代码中,获取目标进程库函数地址的代码如下,x86平台的函数地址=实际函数地址+2。
大牛wdfa给出的解答参考《Android Libinject X86平台EIP-2的分析》:
注入程序在ptrace_attach目标程序的时候,目标程序因为执行sleep(1)处于休眠状态。此时EIP为系统调用的用户态返回地址,如下的0xb7fe2424。无论是sysenter方式还是int
0x80方式,返回EIP都是该地址。
ptrace_attach首先向目标进程发送SIGSTOP信号,该信号把本来处于休眠的目标进程唤醒,然后进入等待目标进程状态发生改变。目标进程被唤醒后经内核调度开始执行,开始执行其实是从原来的休眠处开始的,显然继续执行将会结束sleep系统调用,并且返回结果不是sleep满足,而是sleep被中断了。目标进程准备退出sleep系统调用,在返回到用户态EIP之前,内核检测自身是否存在信号。显然,赤裸裸的躺着注入程序发送的SIGSTOP信号。于是,转入执行SIGSTOP信号处理,该信号处理把进程状态为STOP,挂起自身,激活注入进程的wait调用。
注入进程wait调用返回,ptrace_getregs 获取的EIP是确确实实的目标进程将来返回到用户态执行的EIP。
注入进程 ptrace_setreg 设置的EIP也确确实实是目标进程将来要返回到用户态执行的EIP。
注入进程 ptrace_continue 给目标进程发送信号SIGCON,信号发送本身将唤醒挂起的目标进程。目标进程因为收到SIGCON信号,恢复执行。此时进程的SIGSTOP信号即将处理完毕,系统检测该到信号来自系统调用,并且该信号处理不是由用户程序处理,这就意味着该信号导致了本系统调用失败,需要自动重新执行该系统调用。
自动重新本系统调用的方法:恢复用户态寄存器EAX为系统调用号,用户态EIP=EIP-2。而EIP-2恰好就是int
80系统调用指令。目标进程处理完信号后,开始执行系统调用返回到用户态。此时用户态的EIP由于-2的原因,不再是原来的pop
ebp指令,而是int 80指令。因此返回到用户态后,自动重新执行本系统调用。
因此,EIP-2的本质原因,在于ptrace_attach的时候,目标进程因系统调用进入了休眠,而attach发送的信号导致了目标进程调用中断返回,系统为了弥补中断返回的系统调用,在信号处理中将EIP-2来迫使中断的系统调用返回后自动重启本系统调用。
为何ptrace_attach+ptrace_syscall就没有EIP-2的问题?
在ptrace_attach后加入ptrace_syscall后不会导致eip-2,原因在于ptrace_syscall仅仅是设置目标进程的标志位,没有发送任何信号,也没有中断目标进程的任何系统调用。目标进程是主动在系统调用之前检查该标志位,主动挂起自己。这种条件下,注入进程设置目标进程eip,目标进程的系统调用自然是返回到设置的eip。
ptrace_attach一定会有问题吗?
NO。attach时刻目标进程,如果不是陷入系统调用,就不会触发自动重启系统调用导致的EIP-2。
ptrace_attach+ptrace_syscall一定安全吗?
NO。在ptrace_syscall+ptrace_setreg+ptrace_continue后,目标进程开始进行系统调用,如果系统调用是可中断的阻塞调用,在阻塞等待过程中,如果因为接受到其它信号,导致系统调用中断返回,那么系统会因为自动重启系统调用而设置eip-2,并且系统期待的eip-2处的代码为int
80。显然,注入进程设置的eip为某个函数,而eip-2就是个不伦不类的东西。
注:最后两个问题,没有实际测试,只是推断;SIGCON信号对于目标进程只是简单的忽略;不知道ARM平台存不存在因为自动重启系统调用而导致的EIP-2的问题。
大牛netsniffer给出的 重启系统调用 的解答:
arm下重启系统调用,也有调整用户态pc位置,目前bionic中libc.so采用ARM方式编译,svc
0指令占用4Bytes,所以会-4,arch\arm\kernel\signal.c
:
static void do_signal(struct pt_regs *regs, int syscall)
{
......
/*
* If we were from a system call, check for system call restarting...
*/
if (syscall) {
continue_addr = regs->ARM_pc;
restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4);
retval = regs->ARM_r0;
/*
* Prepare for system call restart. We do this here so that a
* debugger will see the already changed PSW.
*/
switch (retval) {
case -ERESTARTNOHAND:
case -ERESTARTSYS:
case -ERESTARTNOINTR:
regs->ARM_r0 = regs->ARM_ORIG_r0;
regs->ARM_pc = restart_addr;
break;
case -ERESTART_RESTARTBLOCK:
regs->ARM_r0 = -EINTR;
break;
}
}
signr = get_signal_to_deliver(&info, &ka, regs, NULL);
if (signr > 0) {
// 用户未设置SA_RESTART sigaction,继续执行以-EINTR返回给调用者
if (regs->ARM_pc == restart_addr) {
if (retval == -ERESTARTNOHAND
|| (retval == -ERESTARTSYS
&& !(ka.sa.sa_flags & SA_RESTART))) {
regs->ARM_r0 = -EINTR;
regs->ARM_pc = continue_addr;
}
}
x86下重启系统调用,int
0x80 指令2 Bytes,在文件 arch\x86\kernel\signal.c中:
static void do_signal(struct pt_regs *regs)
{
......
// x86下处理信号和arm下不同,先直接转到用户空间处理信号
signr = get_signal_to_deliver(&info, &ka, regs, NULL);
if (signr > 0) {
/* Whee! Actually deliver the signal. */
handle_signal(signr, &info, &ka, regs);
return;
}
// 后判断系统调用结果,根据情况PC-2
if (syscall_get_nr(current, regs) >= 0) {
/* Restart the system call - no handlers present */
switch (syscall_get_error(current, regs)) {
case -ERESTARTNOHAND:
case -ERESTARTSYS:
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break;
case -ERESTART_RESTARTBLOCK:
regs->ax = NR_restart_syscall;
regs->ip -= 2;
break;
}
}
网友justlovemm提出的问题:
感谢连接:
http://bbs.pediy.com/showthread.php?t=189475
http://bbs.pediy.com/showthread.php?t=174495
so注入工具inject工程的代码:
inject.c文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/user.h> // 修改头文件#include <asm/user.h>为#include <sys/user.h>
#include <asm/ptrace.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <dlfcn.h>
#include <dirent.h>
#include <unistd.h>
#include <string.h>
#include <elf.h>
#include <android/log.h>
#if defined(__i386__)
#define pt_regs user_regs_struct
#endif
// 调试模式
#define ENABLE_DEBUG 1
// log日志打印的支持
#if ENABLE_DEBUG
#define LOG_TAG "INJECT"
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG, fmt, ##args)
#define DEBUG_PRINT(format,args...) \
LOGD(format, ##args)
#else
#define DEBUG_PRINT(format,args...)
#endif
#define CPSR_T_MASK ( 1u << 5 )
const char *libc_path = "/system/lib/libc.so";
const char *linker_path = "/system/bin/linker";
// 读取目标进程中内存数据
int ptrace_readdata(pid_t pid, uint8_t *src, uint8_t *buf, size_t size)
{
uint32_t i, j, remain;
uint8_t *laddr;
union u {
long val;
char chars[sizeof(long)];
} d;
j = size / 4;
remain = size % 4;
laddr = buf;
for (i = 0; i < j; i ++) {
d.val = ptrace(PTRACE_PEEKTEXT, pid, src, 0);
memcpy(laddr, d.chars, 4);
src += 4;
laddr += 4;
}
if (remain > 0) {
d.val = ptrace(PTRACE_PEEKTEXT, pid, src, 0);
memcpy(laddr, d.chars, remain);
}
return 0;
}
// 写目标进程的内存写入数据
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;
j = size / 4;
remain = size % 4;
laddr = data;
for (i = 0; i < j; i ++) {
memcpy(d.chars, laddr, 4);
ptrace(PTRACE_POKETEXT, pid, dest, d.val);
dest += 4;
laddr += 4;
}
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;
}
// arm模式下,在目标pid进程中调用指定目标函数
#if defined(__arm__)
int ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct pt_regs* regs)
{
uint32_t i;
// 设置目标pid进程中被调用的函数的参数(arm的函数调用中前4个函数参数,通过r0-r3寄存器传递)
for (i = 0; i < num_params && i < 4; i ++) {
regs->uregs[i] = params[i];
}
//
// push remained params onto stack
// 设置目标pid进程中被调用的函数的超过4个参数的参数(arm的函数调用中超过4个参数之后的函数参数,通过栈进行传递)
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));
}
// 设置将被调用的函数的调用地址(pc为指令指针寄存器,控制着进程的具体执行)
regs->ARM_pc = addr;
// 根据当前进程的运行模式,设置进程的状态寄存器cpsr的值
if (regs->ARM_pc & 1) {
/* thumb */
regs->ARM_pc &= (~1u);
regs->ARM_cpsr |= CPSR_T_MASK;
} else {
/* arm */
regs->ARM_cpsr &= ~CPSR_T_MASK;
}
// 设置函数调用完的返回地址为0,触发地址0异常,程序的控制权又从目标pid进程回到了当前进程中
regs->ARM_lr = 0;
// 设置目标pid进程的寄存器的状态值--实现在目标pid进程中调用指定的目标函数
if (ptrace_setregs(pid, regs) == -1
// 让目标pid进程继续执行代码指令
|| ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
int stat = 0;
// 等待在目标pid进程中,调用指定的目标函数完成
waitpid(pid, &stat, WUNTRACED);
/***
WUNTRACED告诉waitpid,如果子进程进入暂停状态,那么就立即返回。
如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。
对于使用PTRACE_CONT运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。
这里的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
那么什么时候会发生这种错误呢?
显然,当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV。
***/
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
// 进程等待
waitpid(pid, &stat, WUNTRACED);
}
return 0;
}
// x86模式下,在目标pid进程中调用指定目标函数
#elif defined(__i386__)
long ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct user_regs_struct * regs)
{
// x86模式下,C函数调用约定一般是通过栈进行传递参数
// 抬高目标pid进程的栈顶,用于保存函数调用需要的参数
regs->esp -= (num_params) * sizeof(long) ;
// 将指定目标函数调用需要的函数参数,写入到函数栈中
ptrace_writedata(pid, (void *)regs->esp, (uint8_t *)params, (num_params) * sizeof(long));
// 无效的地址
long tmp_addr = 0x00;
// 再次抬高栈顶,用于保存指定目标函数调用完后的函数的返回地址,此处设置为 0x00,将触发无效0地址访问异常
// 目标pid进程中指定目标函数调用完成以后,进程的控制权从目标pid进程又回到了当前进程中
regs->esp -= sizeof(long);
// 将无效的0地址值,写入到指定目标函数的函数栈中
ptrace_writedata(pid, regs->esp, (char *)&tmp_addr, sizeof(tmp_addr));
// x86模式下,控制进程的执行流程的是eip寄存器
// 设置eip为将被调用的函数的调用地址
regs->eip = addr;
// 设置目标pid进程的寄存器状态值,用以调用目标pid进程中的指定目标函数
if (ptrace_setregs(pid, regs) == -1
// 让目标pid进程继续执行指令,调用目标函数
|| ptrace_continue( pid) == -1) {
printf("error\n");
return -1;
}
int stat = 0;
// 等待调用目标函数完成
waitpid(pid, &stat, WUNTRACED);
// 对0地址异常的处理
while (stat != 0xb7f) {
=
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
// 等待操作的完成
waitpid(pid, &stat, WUNTRACED);
}
return 0;
}
#else
#error "Not supported"
#endif
// 获取目标进程寄存器的状态值
int ptrace_getregs(pid_t pid, struct pt_regs * regs)
{
if (ptrace(PTRACE_GETREGS, pid, NULL, regs) < 0) {
perror("ptrace_getregs: Can not get register values");
return -1;
}
return 0;
}
// 设置目标进程的寄存器的状态值
int ptrace_setregs(pid_t pid, struct pt_regs * regs)
{
if (ptrace(PTRACE_SETREGS, pid, NULL, regs) < 0) {
perror("ptrace_setregs: Can not set register values");
return -1;
}
return 0;
}
// 让目标进程继续运行执行代码
int ptrace_continue(pid_t pid)
{
if (ptrace(PTRACE_CONT, pid, NULL, 0) < 0) {
perror("ptrace_cont");
return -1;
}
return 0;
}
// ++++++++++++++++++++++++++++++++++ ptrace附加调试目标进程 +++++++++++++++++++++++++++++++++++
int ptrace_attach(pid_t pid)
{
if (ptrace(PTRACE_ATTACH, pid, NULL, 0) < 0) {
perror("ptrace_attach");
return -1;
}
int status = 0;
waitpid(pid, &status , WUNTRACED);
return 0;
}
// ++++++++++++++++++++++++++++++++++ ptrace附加调试目标进程 +++++++++++++++++++++++++++++++++++
// 释放对目标进程的附加调试
int ptrace_detach(pid_t pid)
{
if (ptrace(PTRACE_DETACH, pid, NULL, 0) < 0) {
perror("ptrace_detach");
return -1;
}
return 0;
}
// 获取进程中指定名称模块的基址
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;
// 获取该so文件在当前进程中加载基址
local_handle = get_module_base(-1, module_name);
// 获取该so文件在目标pid进程中加载基址
remote_handle = get_module_base(target_pid, module_name);
DEBUG_PRINT("[+] get_remote_addr: local[%x], remote[%x]\n", local_handle, remote_handle);
// 获取指定函数在目标pid进程中调用地址
void * ret_addr = (void *)((uint32_t)local_addr + (uint32_t)remote_handle - (uint32_t)local_handle);
// 增加了对x86的支持,x86模式针对"/system/lib/libc.so"中,函数调用地址的特殊处理
#if defined(__i386__)
if (!strcmp(module_name, libc_path)) {
// 函数的调用地址+2
ret_addr += 2;
}
#endif
return ret_addr;
}
// 通过进程的文件路径名称,获取进程的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);
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;
}
// 对x86和arm模式的函数的返回值兼容处理(获取函数调用的返回值)
long ptrace_retval(struct pt_regs * regs)
{
#if defined(__arm__)
return regs->ARM_r0;
#elif defined(__i386__)
return regs->eax;
#else
#error "Not supported"
#endif
}
// 对x86和arm模式的指令指针的处理(获取函数调用完后的,指令指针值)
long ptrace_ip(struct pt_regs * regs)
{
#if defined(__arm__)
return regs->ARM_pc;
#elif defined(__i386__)
return regs->eip;
#else
#error "Not supported"
#endif
}
// 在目标pid进程中调用指定的函数并获取函数返回值
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);
// 在目标pid进程中调用指定的函数
if (ptrace_call(target_pid, (uint32_t)func_addr, parameters, param_num, regs) == -1)
return -1;
// 在目标pid进程中,调用完指定函数以后,获取此时寄存器的状态值(函数返回值、指令指针寄存器值)
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;
}
// --------------将指定的so文件注入到目标进程中并执行注入so文件中导出函数function_name-------------------------
int inject_remote_process(pid_t target_pid, const char *library_path, const char *function_name, const char *param, size_t param_size)
{
int ret = -1;
void *mmap_addr, *dlopen_addr, *dlsym_addr, *dlclose_addr, *dlerror_addr;
void *local_handle, *remote_handle, *dlhandle;
uint8_t *map_base = 0;
uint8_t *dlopen_param1_ptr, *dlsym_param2_ptr, *saved_r0_pc_ptr, *inject_param_ptr, *remote_code_ptr, *local_code_ptr;
// 用于保存目标pid进程的寄存器状态值
struct pt_regs regs, original_regs;
// 用于保存目标pid进程中的dopen函数调用地址以及参数的值、dlsym函数调用地址以及参数的值、以及诸如so的导出函数function_name以及参数值
extern uint32_t _dlopen_addr_s, _dlopen_param1_s, _dlopen_param2_s, _dlsym_addr_s, \
_dlsym_param2_s, _dlclose_addr_s, _inject_start_s, _inject_end_s, _inject_function_param_s, \
_saved_cpsr_s, _saved_r0_pc_s;
uint32_t code_length;
long parameters[10];
// 打印将被so注入的pid进程的值
DEBUG_PRINT("[+] Injecting process: %d\n", target_pid);
// ptrace附加调试目标pid进程
if (ptrace_attach(target_pid) == -1)
// 附加失败,跳转
goto exit;
// 获取目标pid进程当前寄存器的状态值
if (ptrace_getregs(target_pid, ®s) == -1)
goto exit;
/* save original registers 保存目标pid进程当前寄存器的状态值 */
memcpy(&original_regs, ®s, sizeof(regs));
// 获取目标pid进程中"/system/lib/libc.so"中的mmap函数的调用地址
mmap_addr = get_remote_addr(target_pid, libc_path, (void *)mmap);
DEBUG_PRINT("[+] Remote mmap address: %x\n", mmap_addr);
/* call mmap 准备调用目标pid进程中的mmap函数需要的参数 */
parameters[0] = 0; // addr
parameters[1] = 0x4000; // size--在目标pid进程中申请的内存空间的大小
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; // prot
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flags
parameters[4] = 0; //fd
parameters[5] = 0; //offset
// 调用目标pid进程中的mmap函数,在目标pid进程的内存空间中申请内存空间
if (ptrace_call_wrapper(target_pid, "mmap", mmap_addr, parameters, 6, ®s) == -1)
// 调用失败,跳转
goto exit;
// 获取调用目标pid进程的mmap函数完成的函数返回值即申请的内存空间地址
map_base = ptrace_retval(®s);
// 获取目标pid进程中dlopen函数的调用地址
dlopen_addr = get_remote_addr( target_pid, linker_path, (void *)dlopen );
// 获取目标pid进程中dlsym函数的调用地址
dlsym_addr = get_remote_addr( target_pid, linker_path, (void *)dlsym );
// 获取目标pid进程中dlclose函数的调用地址
dlclose_addr = get_remote_addr( target_pid, linker_path, (void *)dlclose );
// 获取目标pid进程中dlerror函数的调用地址
dlerror_addr = get_remote_addr( target_pid, linker_path, (void *)dlerror );
// 打印获取到目标pid进程中dlopen等函数的地址
DEBUG_PRINT("[+] Get imports: dlopen: %x, dlsym: %x, dlclose: %x, dlerror: %x\n",
dlopen_addr, dlsym_addr, dlclose_addr, dlerror_addr);
// 打印即将被注入的so库文件的文件路径
printf("library path = %s\n", library_path);
// 将要被注入到目标pid进程中的so库文件的路径字符串library_path写入到前面mmap申请的内存空间中
ptrace_writedata(target_pid, map_base, library_path, strlen(library_path) + 1);
// 设置调用dlopen函数的函数参数
parameters[0] = map_base; // library_path将被加载到目标pid进程中的so文件路径
parameters[1] = RTLD_NOW| RTLD_GLOBAL;
// 调用目标pid进程中的dlopen函数,加载library_path路径的so文件到目标pid进程中,实现so注入
if (ptrace_call_wrapper(target_pid, "dlopen", dlopen_addr, parameters, 2, ®s) == -1)
// 失败进行跳转
goto exit;
// 获取dlopen函数调用后的返回值即library_path指定的so文件在目标pid进程中的加载基址
void * sohandle = ptrace_retval(®s);
// 设置map_base中保存library_path指定的so文件的导出函数function_name字符串的内存偏移
#define FUNCTION_NAME_ADDR_OFFSET 0x100
// 将library_path指定的so文件的导出函数function_name的函数名称字符串写入到目标pid进程中前面mmap申请的内存空间offset=0x100的位置
ptrace_writedata(target_pid, map_base + FUNCTION_NAME_ADDR_OFFSET, function_name, strlen(function_name) + 1);
// 设置dlsym函数调用的函数参数
parameters[0] = sohandle; // so基址模块句柄
parameters[1] = map_base + FUNCTION_NAME_ADDR_OFFSET; // 将被获取的导出函数的调用地址
// 在目标pid进程中调用dlsym函数,获取上面加载的so文件中的导出函数function_name的调用地址
if (ptrace_call_wrapper(target_pid, "dlsym", dlsym_addr, parameters, 2, ®s) == -1)
// 失败,跳转
goto exit;
// 获取调用dlsym函数后,返回的导出函数function_name的调用地址
void * hook_entry_addr = ptrace_retval(®s);
// 打印获取到的导出函数function_name的调用地址
DEBUG_PRINT("hook_entry_addr = %p\n", hook_entry_addr);
// 设置map_base中保存调用导出函数function_name需要的函数参数的内存偏移
#define FUNCTION_PARAM_ADDR_OFFSET 0x200
// 将调用hook_entry_addr函数需要的函数参数保存到前面在目标pid进程中mmap申请的内存空间offset=0x200的位置
ptrace_writedata(target_pid, map_base + FUNCTION_PARAM_ADDR_OFFSET, param, strlen(param) + 1);
// 设置调用目标pid进程中hook_entry函数的函数参数
parameters[0] = map_base + FUNCTION_PARAM_ADDR_OFFSET;
// 调用注入到目标pid进程中的so库的导出函数hook_entry实现我们自定义的代码,可以是Hook目标pid进程的函数
if (ptrace_call_wrapper(target_pid, "hook_entry", hook_entry_addr, parameters, 1, ®s) == -1)
goto exit;
// 等待用户的输入
printf("Press enter to dlclose and detach\n");
getchar();
// 设置dlclose函数调用的函数参数
parameters[0] = sohandle;
// 调用目标pid进程中的dlclose函数卸载上面加载的library_path指定的so文件(实现so注入的卸载)
if (ptrace_call_wrapper(target_pid, "dlclose", dlclose, parameters, 1, ®s) == -1)
goto exit;
/* restore 恢复目标pid进程被ptrace附加时的运行状态 */
ptrace_setregs(target_pid, &original_regs);
// 释放对目标pid进程的附加
ptrace_detach(target_pid);
// 设置函数返回值
ret = 0;
exit:
return ret;
}
// ++++++++++++++++++++++++++++++++ 主函数 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
int main(int argc, char** argv) {
pid_t target_pid;
// 获取目标进程的pid
target_pid = find_pid_of("/system/bin/surfaceflinger");
if (-1 == target_pid) {
printf("Can't find the process\n");
return -1;
}
// 向目标进程注入so库文件,执行导出函数hook_entry,其中"I'm parameter!"为传入参数
inject_remote_process(target_pid, "/data/local/tmp/libinject_so.so", "hook_entry", "I'm parameter!", strlen("I'm parameter!"));
return 0;
}
// ++++++++++++++++++++++++++++++++ 主函数 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 编译后生成的模块的名称
LOCAL_MODULE := inject
# 参与编译的源码文件
LOCAL_SRC_FILES := inject.c
# 支持log日志打印需要加载链接的库
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog
#LOCAL_FORCE_STATIC_EXECUTABLE := true
# 编译生成可执行文件
include $(BUILD_EXECUTABLE)
Application.mk文件
# 编译后,生成模块运行支持的平台
APP_ABI := x86 armeabi-v7a
inject_so.c文件
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <android/log.h>
#include <EGL/egl.h>
#include <GLES/gl.h>
#include <elf.h>
#include <fcntl.h>
#include <sys/mman.h>
#define LOG_TAG "INJECT"
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args)
EGLBoolean (*old_eglSwapBuffers)(EGLDisplay dpy, EGLSurface surf) = -1;
// 当调用eglSwapBuffers函数时,将调用我们自定的new_eglSwapBuffers函数
EGLBoolean new_eglSwapBuffers(EGLDisplay dpy, EGLSurface surface)
{
LOGD("New eglSwapBuffers\n");
if (old_eglSwapBuffers == -1)
LOGD("error\n");
return old_eglSwapBuffers(dpy, surface);
}
// 获取目标进程中指定名称模块的加载基址
void* get_module_base(pid_t pid, const char* module_name)
{
FILE *fp;
long addr = 0;
char *pch;
char filename[32];
char line[1024];
// 格式化字符串得到 "/proc/pid/maps"
if (pid < 0) {
/* self process */
snprintf(filename, sizeof(filename), "/proc/self/maps", pid);
} else {
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
}
// 打开文件 /proc/pid/maps,获取指定pid进程加载的内存模块信息
fp = fopen(filename, "r");
if (fp != NULL) {
// 每次一行,读取文件 /proc/pid/maps中内容
while (fgets(line, sizeof(line), fp)) {
// 查找指定的so模块
if (strstr(line, module_name)) {
// 分割字符串
pch = strtok( line, "-" );
// 字符串转长整形
addr = strtoul( pch, NULL, 16 );
// 特殊内存地址的处理
if (addr == 0x8000)
addr = 0;
break;
}
}
// 关闭文件
fclose(fp) ;
}
return (void *)addr;
}
// Hook库/system/lib/libsurfaceflinger.so中的eglSwapBuffers函数
#define LIBSF_PATH "/system/lib/libsurfaceflinger.so"
int hook_eglSwapBuffers()
{
// 保存被Hook的目标函数的原始调用地址
old_eglSwapBuffers = eglSwapBuffers;
LOGD("Orig eglSwapBuffers = %p\n", old_eglSwapBuffers);
// 获取目标pid进程中"/system/lib/libsurfaceflinger.so"模块的加载地址
void * base_addr = get_module_base(getpid(), LIBSF_PATH);
LOGD("libsurfaceflinger.so address = %p\n", base_addr);
int fd;
// 打开内存模块文件"/system/lib/libsurfaceflinger.so"
fd = open(LIBSF_PATH, O_RDONLY);
if (-1 == fd) {
LOGD("error\n");
return -1;
}
// elf32文件的文件头结构体Elf32_Ehdr
Elf32_Ehdr ehdr;
// 读取elf32格式的文件"/system/lib/libsurfaceflinger.so"的文件头信息
read(fd, &ehdr, sizeof(Elf32_Ehdr));
// elf32文件中区段表信息结构的文件偏移
unsigned long shdr_addr = ehdr.e_shoff;
// elf32文件中区段表信息结构的数量
int shnum = ehdr.e_shnum;
// elf32文件中每个区段表信息结构中的单个信息结构的大小(描述每个区段的信息的结构体的大小)
int shent_size = ehdr.e_shentsize;
// elf32文件区段表中每个区段的名称存放的字符串区段,在区段表中的序号index
unsigned long stridx = ehdr.e_shstrndx;
// elf32文件中区段表的每个单元信息结构体(描述每个区段的信息的结构体)
Elf32_Shdr shdr;
// elf32文件中定位到存放每个区段名称的字符串表的信息结构体位置.shstrtab
lseek(fd, shdr_addr + stridx * shent_size, SEEK_SET);
// 读取elf32文件中的描述每个区段的信息的结构体(这里是保存elf32文件的每个区段的名称字符串的)
read(fd, &shdr, shent_size);
// 为保存elf32文件的所有的区段的名称字符串申请内存空间
char * string_table = (char *)malloc(shdr.sh_size);
// 定位到具体存放elf32文件的所有的区段的名称字符串的文件偏移处
lseek(fd, shdr.sh_offset, SEEK_SET);
// 从elf32内存文件中读取所有的区段的名称字符串到申请的内存空间中
read(fd, string_table, shdr.sh_size);
// 重新设置elf32文件的文件偏移为区段信息结构的起始文件偏移处
lseek(fd, shdr_addr, SEEK_SET);
int i;
uint32_t out_addr = 0;
uint32_t out_size = 0;
uint32_t got_item = 0;
int32_t got_found = 0;
// 循环遍历elf32文件的区段表(描述每个区段的信息的结构体)
for (i = 0; i < shnum; i++) {
// 依次读取区段表中每个描述区段的信息的结构体
read(fd, &shdr, shent_size);
// 判断当前区段描述结构体描述的区段是否是SHT_PROGBITS类型
if (shdr.sh_type == SHT_PROGBITS) {
// 获取区段的名称字符串在保存所有区段的名称字符串段.shstrtab中的序号
int name_idx = shdr.sh_name;
// 判断区段的名称是否为".got.plt"或者".got"
if (strcmp(&(string_table[name_idx]), ".got.plt") == 0
|| strcmp(&(string_table[name_idx]), ".got") == 0) {
// 获取区段".got"或者".got.plt"在内存中实际数据存放地址
out_addr = base_addr + shdr.sh_addr;
// 获取区段".got"或者".got.plt"的大小
out_size = shdr.sh_size;
LOGD("out_addr = %lx, out_size = %lx\n", out_addr, out_size);
// 遍历区段".got"或者".got.plt"获取保存的全局的函数调用地址
for (i = 0; i < out_size; i += 4) {
// 获取区段".got"或者".got.plt"中的单个函数的调用地址
got_item = *(uint32_t *)(out_addr + i);
// 判断区段".got"或者".got.plt"中函数调用地址是否是将要被Hook的目标函数地址
if (got_item == old_eglSwapBuffers) {
LOGD("Found eglSwapBuffers in got\n");
// 查找到要被Hook的目标函数的地址
got_found = 1;
// 获取当前内存分页的大小
uint32_t page_size = getpagesize();
// 获取内存分页的起始地址(需要内存对齐)
uint32_t entry_page_start = (out_addr + i) & (~(page_size - 1));
LOGD("entry_page_start = %lx, entry_page_start = %lx\n", entry_page_start, page_size);
// 修改内存属性为可读可写可执行
if (mprotect((uint32_t *)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
LOGD("mprotect false\n");
return -1;
}
// Hook目标函数之前
LOGD("%s, old_eglSwapBuffers = %lx, new_eglSwapBuffers = %lx\n", "befor hook function", got_item, new_eglSwapBuffers);
// Hook函数为我们自己定义的函数
//*(uint32_t *)(out_addr + i) = new_eglSwapBuffers; // Hook函数的作用,等价的
got_item = new_eglSwapBuffers;
// Hook目标函数之后
LOGD("%s, old_eglSwapBuffers = %lx, new_eglSwapBuffers = %lx\n", "after hook function", got_item, new_eglSwapBuffers);
// 恢复内存属性为可读可执行
if (mprotect((uint32_t *)entry_page_start, page_size, PROT_READ | PROT_EXEC) == -1) {
LOGD("mprotect false\n");
return -1;
}
break;
// 此时,目标函数的调用地址已经被Hook了
} else if (got_item == new_eglSwapBuffers) {
LOGD("Already hooked\n");
break;
}
}
// Hook目标函数成功,跳出循环
if (got_found)
break;
}
}
}
free(string_table);
close(fd);
}
// 注入so的导出函数(默认导出)--将被调用
int hook_entry(char * a){
LOGD("Hook into success\n");
LOGD("Start hooking\n");
// Hook目标pid进程的eglSwapBuffer函数
hook_eglSwapBuffers();
return 0;
}
Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
#LOCAL_ARM_MODE := arm
# 编译生成的模块的名称
LOCAL_MODULE := inject_so
# 参与编译的源码文件
LOCAL_SRC_FILES := inject_so.c
# 支持log日志的打印
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog -lEGL
# 编译生成动态库文件libinject_so.so
include $(BUILD_SHARED_LIBRARY)
Application.mk文件
# 编译生成的模块文件运行支持的平台
APP_ABI := x86 armeabi-v7a
# 编译生成模块运行支持的Andorid版本
APP_PLATFORM := android-19
执行Android so 注入和函数Hook的需要的文件截图:
log.bat查看注入输出日志的脚本文件:
adb logcat -s INJECT
运行Android so注入的脚本文件(真机)run_inject.bat:
adb push libinject_so.so /data/local/tmp
adb push inject /data/local/tmp
adb shell chmod 0777 /data/local/tmp/libinject_so.so
adb shell chmod 0777 /data/local/tmp/inject
adb shell su -c /data/local/tmp/inject
pause
Nexus 5,Android 4.4.4的测试效果如下图:
Android
so注入和函数Hook的结果和作者ariesjzj的稍有不同,也有网友在作者ariesjzj的博客下面留言说Hook函数不成功,我修改了一下作者ariesjzj的代码,将Hook后目标函数的地址打印出来了,其实呢,Hook目标函数是成功的,只是没有被系统调用,触发新的new_eglSwapBuffers函数被调用而已。
关于Android的so注入和Hook还有很多值得讨论的问题,感谢博文中提到的作者深入研究知识的精神,让我在学习的道路上又进了一步,后面还会继续深入讨论有关Android的so注入和函数Hook的问题。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章