android 的hook技术,Android Native Hook技术(一)

android 的hook技术,Android Native Hook技术(一)原理分析ADBI是一个著名的安卓平台hook框架,基于动态库注入与inlinehook技术实现。该框架主要由2个模块构成:1)hijack负责将so注入到目标进程空间,2)libbase是注入的so本身,提供了inlinehook能力。源码目录中的example则是一个使用ADBI进行hookepoll_wait的示例。hijackhijack实现动态库注入功能,通过在目标进程插入d…

大家好,又见面了,我是你们的朋友全栈君。

原理分析

ADBI是一个著名的安卓平台hook框架,基于 动态库注入 与 inline hook 技术实现。该框架主要由2个模块构成:1)hijack负责将so注入到目标进程空间,2)libbase是注入的so本身,提供了inline hook能力。

源码目录中的example则是一个使用ADBI进行hook epoll_wait的示例。

hijack

hijack实现动态库注入功能,通过在目标进程插入dlopen()调用序列,加载指定so文件。要实现这个功能,主要做两件事情:

获得目标进程中dlopen()地址

在目标进程的栈空间上构造一处dlopen()调用

下面分别解决这两个问题

1. 获得目标进程中dlopen()地址

在ADBI中,通过下面代码来获得目标进程中dlopen()函数地址:

void *ldl = dlopen(“libdl.so”, RTLD_LAZY);

if (ldl) {

dlopenaddr = (unsigned long)dlsym(ldl, “dlopen”);

dlclose(ldl);

}

unsigned long int lkaddr;

unsigned long int lkaddr2;

find_linker(getpid(), &lkaddr);

find_linker(pid, &lkaddr2);

dlopenaddr = lkaddr2 + (dlopenaddr – lkaddr);

其中find_linker()函数功能是获取指定进程中linker的地址。

linker是Android提供的动态链接器,每个进程都会映射一份到自己的进程空间,而dlopen()函数就是在linker里面定义,其相对于linker头部偏移是固定的。

因此要计算某进程中dlopen()函数地址,只需分别取当前进程linker地址lkaddr和dlopen()地址dlopenaddr,并通过 /proc/pid_xxx/maps 读取被注入进程linker地址lkaddr2。

dlopenaddr = lkaddr2 + (dlopenaddr – lkaddr) 即为目标进程中dlopen()地址。

2. 在目标进程的栈空间上构造dlopen()调用

要修改目标进程寄存器等信息,需使用到ptrace()函数,gdb等程序拥有查看、修改调试进程寄存器等的能力就是因为使用了ptrace()。

先将hijack attach到目标进程上去:

if (0 > ptrace(PTRACE_ATTACH, pid, 0, 0)) {

printf(“cannot attach to %d, error!\n”, pid);

exit(1);

}

waitpid(pid, NULL, 0);

这时目标进程暂停,就可以通过ptrace对其进行修改了,以下代码获取寄存器值保存在regs中:

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

接下来要做的就是修改寄存器的值,在目标进程的栈空间上构造一处dlopen()调用,关键在sc数组:

unsigned int sc[] = {

0xe59f0040, //0 ldr r0, [pc, #64]

0xe3a01000, //1 mov r1, #0

0xe1a0e00f, //2 mov lr, pc

0xe59ff038, //3 ldr pc, [pc, #56]

0xe59fd02c, //4 ldr sp, [pc, #44]

0xe59f0010, //5 ldr r0, [pc, #16]

0xe59f1010, //6 ldr r1, [pc, #16]

0xe59f2010, //7 ldr r2, [pc, #16]

0xe59f3010, //8 ldr r3, [pc, #16]

0xe59fe010, //9 ldr lr, [pc, #16]

0xe59ff010, //10 ldr pc, [pc, #16]

0xe1a00000, //11 nop r0

0xe1a00000, //12 nop r1

0xe1a00000, //13 nop r2

0xe1a00000, //14 nop r3

0xe1a00000, //15 nop lr

0xe1a00000, //16 nop pc

0xe1a00000, //17 nop sp

0xe1a00000, //18 nop addr of libname

0xe1a00000, //19 nop dlopenaddr

};

接下来使用上文取到的寄存器值对sc数组进行初始化:

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;

sc[19] = dlopenaddr;

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

sc[18] = libaddr;

上面代码数组内容就是我们要写入到目标进程当前栈空间的指令,即一份shellcode,接下来看一下这段shellcode实现了什么样的功能:

ldr r0,[pc,#64]

将so路径字符串地址存入r0

ARM指令集中PC寄存器总 指向当前指令的下两条指令 地址处,这是为了加快指令执行速度,如下图第一条指令执行时,第三条指令已经在读取:

指令一 > 读取 解析 执行

指令二 > 读取 解析 执行

指令三 > 读取 解析 执行

因此PC+64实际指向sc[18]的位置,取其内容即为so路径字符串的地址

mov r1,#0

将0赋值给r1寄存器。

ldr pc,[pc,#56]

调用dlopen()函数,第一个参数r0为so路径符串地址,第二个参数r1为0。

ldr sp, [pc, #44] ldr r0, [pc, #16] ldr r1, [pc, #16] ldr r2, [pc, #16] ldr r3, [pc, #16] ldr lr, [pc, #16] ldr pc, [pc, #16]

函数执行完后,依次恢复保存的 sp/r0/r1/r2/r3/lr/pc 寄存器,并继续执行。

接下来我们通过ptrace调用,将上面构造的shellcode以及so路径字符串写入到目标进程栈上:

// so name写入栈

if (0 > write_mem(pid, (unsigned long*)arg, n, libaddr)) {

printf(“cannot write library name (%s) to stack, error!\n”, arg);

exit(1);

}

// shellcode 写入栈

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

}

/* Write NLONG 4 byte words from BUF into PID starting

at address POS. Calling process must be attached to PID. */

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;

}

写入栈以后,shellcode并不能执行,因为当前Android都开启了栈执行保护,需要先通过mprotect(),来修改栈的可执行权限:

// 计算栈顶指针

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

// 调用mprotect()设置栈可执行

regs.ARM_r0 = stack_start; // 栈起始位置

regs.ARM_r1 = stack_end – stack_start; // 栈大小

regs.ARM_r2 = PROT_READ|PROT_WRITE|PROT_EXEC; // 权限

if (nomprotect == 0) {

if (debug)

printf(“calling mprotect\n”);

regs.ARM_lr = codeaddr; // lr指向shellcode,mprotect()后执行

regs.ARM_pc = mprotectaddr;

}

// 旧版本Android没有栈保护,Android 2.3引入

else {

regs.ARM_pc = codeaddr;

}

这段代码首先计算栈顶位置,接着将栈 起始地址/栈大小/权限位 3个参数压栈,然后调用mprotect()函数设置栈的可执行权限,最后将lr寄存器设置为栈上代码的起始地址,这样当mprotect()函数返回后就可以正常执行栈上代码了。

最后,恢复目标进程的寄存器值,并恢复被ptrace()暂停的进程:

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

ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);

if (debug)

printf(“library injection completed!\n”);

到目前为止,我们已经能够在指定进程加载任意so库了!

libbase

其实so注入到目标进程中后,hook功能完全可以在init_array中实现,但ADBI为了方便我们使用,编写了一个通用的hook框架libbase.so

libbase依然要解决2个问题:

定位被 hook 函数位置

进行 inline hook

关于获取hook函数地址的方法这里不再赘述。直接看inline hook部分,这部分功能在hook.c的hook()函数中实现,先看hook_t结构体:

struct hook_t {

unsigned int jump[3]; // 跳转指令(ARM)

unsigned int store[3]; // 原指令(ARM)

unsigned char jumpt[20]; // 跳转指令(Thumb)

unsigned char storet[20]; // 原指令(Thumb)

unsigned int orig; // 被hook函数地址

unsigned int patch; // 补丁地址

unsigned char thumb; // 补丁代码指令集,1为Thumb,2为ARM

unsigned char name[128]; // 被hook函数名

void *data;

};

hook_t是一个标准inline hook结构体,保存了 跳转指令/跳转地址/指令集/被hook函数名 等信息。因为ARM使用了ARM和Thumb两种指令集,所以代码中需进行区分:

if (addr % 4 == 0) {

/* ARM指令集 */

} else {

/* Thumb指令集 */

}

这样进行判断的依据是,Thumb指令的地址最后一位固定为 1。

接下来看一下ARM指令集分支的处理流程,这是该问题解决的核心部分:

if (addr % 4 == 0) {

log(“ARM using 0x%lx\n”, (unsigned long)hook_arm)

h->thumb = 0;

h->patch = (unsigned int)hook_arm;

h->orig = addr;

h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0]

h->jump[1] = h->patch;

h->jump[2] = h->patch;

for (i = 0; i < 3; i++)

h->store[i] = ((int*)h->orig)[i];

for (i = 0; i < 3; i++)

((int*)h->orig)[i] = h->jump[i];

}

首先填充hook_t结构体,第一个for循环保存了原地址处3条指令,共12字节。第二个for循环用新的跳转指令进行覆写,关键的三条指令分别保存在jump[0]-[2]中:

jump[0]赋值0xe59ff000,翻译成ARM汇编为 ldr pc,[pc,#0] ,由于pc寄存器读出的值是当前指令地址加8,因此这条指令实际是将jump[2]的值加载到pc寄存器。

jump[2]保存的是hook函数地址。jump[1]仅用来4字节占位。Thumb分支原理与ARM分支一致,不再分析。

接下来我们注意到,函数最后调用了一处hook_cacheflush()函数:

hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));

我们知道,现代处理器都有指令缓存,用来提高执行效率。前面我们修改的是内存中的指令,为防止缓存的存在,使我们修改的指令执行不到,需进行缓存的刷新:

void inline hook_cacheflush(unsigned int begin, unsigned int end)

{

const int syscall = 0xf0002;

__asm __volatile (

“mov r0, %0\n”

“mov r1, %1\n”

“mov r7, %2\n”

“mov r2, #0x0\n”

“svc 0x00000000\n”

:

: “r” (begin), “r” (end), “r” (syscall)

: “r0”, “r1”, “r7”

);

}

参考资料:

[ARM Cache Flush on mmap’d Buffers with __clear_cache()]bb

原文:https://www.cnblogs.com/gm-201705/p/9864048.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/141548.html原文链接:https://javaforall.net

赞 (3)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • Java中所有的关键字及用法

    Java中所有的关键字及用法基本数据类型:int:int基本数据类型,内存空间占8位取值范围-128~127inti=10;floatfloat基本数据类型,内存空间占32位取值范围-2^31~-2^31-1floatf=10.0f;longlong基本数据类型内存空间占64位取值范围-2^63~-2^63-1longl=10l;shortshort基本数据类型内存空间占16位取值范围-2^15~-2^15-1s…

    2022年7月7日
  • 【原创】无锁编程技术及实现

    【原创】无锁编程技术及实现无锁编程技术及实现作者:jx(360电商技术组) 1.基于锁的编程的缺点 多线程编程是多CPU系统在中应用最广泛的一种编程方式,在传统的多线程编程中,多线程之间一般用各种锁的机制来保证正确的对共享资源(share resources)进行访问和操作。在多线程编程中只要需要共享某些数据,就应当将对它的访问串行化。比如像++count(count是整型变量)这样的简单操作也得加锁,因为即便是增量操作

    2022年5月1日
  • 简述django请求生命周期_django请求的生命周期

    简述django请求生命周期_django请求的生命周期Django请求生命周期分析1.客户端发送请求在浏览器输入url地址,例如www.baidu.com,浏览器会自动补全协议(http),变为http://www.baidu.com,现在部分网站都

    2022年7月30日
  • Android ListView 分组效果实现「建议收藏」

    Android ListView 分组效果实现「建议收藏」AndroidListView实现分组的功能,并且点击相应的分组可以对相应的分组展开和收缩。

    2022年7月16日
  • Oracle数据恢复顾问(Data Recovery Advisor)「建议收藏」

    Oracle数据恢复顾问(Data Recovery Advisor)「建议收藏」Oracle数据恢复顾问用于当数据发生错误或故障时,进行自动收集数据故障信息,并生成恢复脚本,用于完成数据恢复。数据恢复顾问也可以主动检查故障。在这种模式下,它可以在数据库进程发现数据损坏并发出错误之前进行潜在的检测并分析数据故障。数据故障可能非常严重。例如,如果您当前的日志文件丢失,则无法启动你的数据库。。。。。

    2022年7月17日
  • 《前端运维》一、Linux基础–04Shell变量

    这一篇文章,我们就要开始学习正式的Shell语言部分的内容。那在开始之前,我们回忆一下,javascript语言,大体都包含了哪些内容?比如数据类型(对象、字符串、数值),数据结构(对象、数组)、运算

    2022年3月25日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号