代码实例分析android中inline hook

以下内容通过1、实现目标注入程序,2、实现主程序,3、实现注入函数,4、thumb指令集实现等4个方面详细分析了android中inline hook的用法,以下是全部内容:

最近终于沉下心来对着书把hook跟注入方面的代码敲了一遍,打算写几个博客把它们记录下来。

第一次介绍一下我感觉难度最大的inline hook,实现代码参考了腾讯GAD的游戏安全入门。

inline hook的大致流程如下:

首先将目标指令替换为跳转指令,跳转地址为一段我们自己编写的汇编代码,这段汇编代码先是执行用户指定的代码,如修改寄存器的值,然后执行被替换掉的原指令2,最后再跳转回原指令3处,恢复程序的正常运行。

为了避开注入过程,我们通过hook自己进程加载的动态连接库进行演示。

1、实现目标注入程序

我们将这个程序编译为动态连接库,然后在主程序中加载,作为hook的目标。

target.h#ifndef TARGET_H_INCLUDED
#define TARGET_H_INCLUDED
void target_foo();
#endif // TARGET_H_INCLUDED
target.c
#include "target.h"
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
void target_foo()
{
 int a = 3;
 int b = 2;
 while(a--) {
  sleep(2);
  b = a * b;
  printf("[INFO] b is %d\n", b);
 }
 b = b + 2;
 b = b - 1;
 printf("[INFO] finally, b is %d\n", b);
}
Android.mkinclude $(CLEAR_VARS)
LOCAL_ARM_MODE := arm
LOCAL_MODULE := target
LOCAL_CFLAGS += -pie -fPIE -std=c11
LOCAL_LDFLAGS += -pie -fPIE -shared -llog
APP_ABI := armeabi-v7a
LOCAL_SRC_FILES := target.c
include $(BUILD_SHARED_LIBRARY)

注意Android.mkLOCAL_ARM_MODE := arm代表编译时使用4字节的arm指令集,而不是2字节的thumb指令集。

2、实现主程序

在主程序中我们首先加载之前编写的动态链接库,进行hook之后再对其中的函数target_foo进行调用。

main.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <stdbool.h>
#include "hook_inline.h"
typedef void (*target_foo)(void);
void my_func(struct hook_reg *reg)
{
 puts("here we go!");
}
void main()
{
 void *handler = dlopen("/data/local/tmp/libtarget.so", RTLD_NOW);
 target_foo foo = (target_foo)dlsym(handler, "target_foo");
 hook_inline_make("/data/local/tmp/libtarget.so", 0xde2, my_func, true);
 foo();
}
hook_inline.h
#ifndef HOOK_INLINE_H_INCLUDED
#define HOOK_INLINE_H_INCLUDED
#include <stdbool.h>
struct hook_reg {
 long ARM_r0; long ARM_r1; long ARM_r2; long ARM_r3;
 long ARM_r4; long ARM_r5; long ARM_r6; long ARM_r7;
 long ARM_r8; long ARM_r9; long ARM_r10;long ARM_r11;
 long ARM_r12;long ARM_sp; long ARM_lr; long ARM_cpsr;
};
typedef void (*hook_func)(struct hook_reg *reg);
bool hook_inline_make(const char *library, long address, hook_func func, bool isArm);
#endif // HOOK_INLINE_H_INCLUDED

这里我们hook功能的实现函数为hook_inline_make,4个参数分别为动态库路径,目标地址,用户函数,目标地址处指令集。

当程序执行到目标地址处时会回调我们传入的用户函数,可通过参数hook_reg来更改寄存器的值(不包括寄存器pc)。因为之前在动态链接库的Android.mk文件指定了使用arm指令集进行编译,所以此处指定最后一个参数为true

3、实现注入函数

现在到了最为关键的地方,为了实现这个功能还需要了解几个知识。

(1)、获取内存中动态链接库的基址

Linux系统中各个进程的内存加载信息可以在/proc/pid/maps文件中到,通过它我们可以获取到动态链接库在内存中的加载基址。

long get_module_addr(pid_t pid, const char *module_name)
{
 char file_path[256];
 char file_line[512];
 if (pid < 0) {
  snprintf(file_path, sizeof(file_path), "/proc/self/maps");
 } else {
  snprintf(file_path, sizeof(file_path), "/proc/%d/maps", pid);
 }
 FILE *fp = fopen(file_path, "r");
 if (fp == NULL) {
  return -1;
 }
 long addr_start = -1, addr_end = 0;
 while (fgets(file_line, sizeof(file_line), fp)) {
  if (strstr(file_line, module_name)) {
   if (2 == sscanf(file_line, "%8lx-%8lx", &addr_start, &addr_end)) {
    break;
   }
  }
 }
 fclose(fp);
 printf("library :%s %lx-%lx, pid : %d\n", module_name, addr_start, addr_end, pid);
 return addr_start;
}

(2)、更改内存中的二进制代码

现在的计算机系统中一般对内存进行分段式管理,不同的段有不同的读、写、执行的属性。一般来讲代码段只有读和执行的属性,不允许对代码段进行写操作。Linux系统中通过函数mprotect对内存的属性进行更改,需要注意的一点是需要以内存页的大小进行对齐。

bool change_addr_writable(long address, bool writable) {
 long page_size = sysconf(_SC_PAGESIZE);
 //align address by page size
 long page_start = (address) & (~(page_size - 1));
 //change memory attribute
 if (writable == true) {
  return mprotect((void*)page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) != -1;
 } else {
  return mprotect((void*)page_start, page_size, PROT_READ | PROT_EXEC) != -1;
 }
}

接下来就可以着手实现功能了,inline hook跟指令集密切相关,此处我们先演示arm指令集的情况,之后对thumb指令集进行讨论。这里实现的功能是用户可在自己注册的回调函数中对hook点寄存器的值进行修改。

为了实现32位地址空间的长跳转,我们需要两条指令的长度(8个字节)来实现。一般手机上的arm处理器为3级流水,所以pc寄存器的值总是指向当前执行指令后的第二条指令,因而使用ldr pc, [pc, #-4]来加载该指令之后的跳转地址。当程序跳转到shellcode后,首先对寄存器组进行备份,然后调用用户注册的回调函数,用户可在回调函数中修改备份中各个寄存器(pc寄存器除外)的值,然后从备份中恢复寄存器组再跳转到stubcode,stubcode的功能是执行被hook点的跳转指令替换掉的两条指令,最后跳回原程序。

ellcode.S 1 .global _shellcode_start_s .global _shellcode_end_s .global _hook_func_addr_s .global _stub_func_addr_s .data _shellcode_start_s:     @ 备份各个寄存器     push  {r0, r1, r2, r3}     mrs   r0, cpsr     str   r0, [sp, #0xc]     str   r14, [sp, #0x8]     add   r14, sp, #0x10     str   r14, [sp, #0x4]     pop   {r0}     push  {r0-r12}     @ 此时寄存器被备份在栈中,将栈顶地址作为回调函数的参数(struct hook_reg)     mov   r0, sp     ldr   r3, _hook_func_addr_s     blx   r3     @ 恢复寄存器值     ldr   r0, [sp, #0x3c]     msr   cpsr, r0     ldmfd sp!, {r0-r12}     ldr   r14, [sp, #0x4]     ldr   sp, [r13]     ldr   pc, _stub_func_addr_s _hook_func_addr_s: .word 0x0 _stub_func_addr_s: .word 0x0 _shellcode_end_s: .end

shellcode使用汇编实现,在使用时需要对里边的两个地址进行修复,用户回调函数地址(_hook_func_addr_s)跟stubcode地址(_stub_func_addr_s)。

接下来我们可以看一下函数hook_inline_make的具体实现了

 void hook_inline_make(const char *library, long address, hook_func func) {     //获取hook点在内存中的地址     long base_addr = get_module_addr(-1, library);     long hook_addr = base_addr + address;     //获取shellcode中的符号地址  extern long _shellcode_start_s;     extern long _shellcode_end_s;     extern long _hook_func_addr_s;     extern long _stub_func_addr_s;     void *p_shellcode_start = &_shellcode_start_s;     void *p_shellcdoe_end = &_shellcode_end_s;     void *p_hook_func = &_hook_func_addr_s;     void *p_stub_func = &_stub_func_addr_s;     //计算shellcode大小     int shellcode_size = (int)(p_shellcdoe_end - p_shellcode_start);     //新建shellcode     void *shellcode = malloc(shellcode_size);     memcpy(shellcode, p_shellcode_start, shellcode_size);     //添加执行属性     change_addr_writable((long)shellcode, true);     //在32bit的arm指令集中,stubcode中的4条指令占用16个字节的空间     //前两条指令为hook点被替换的两条指令     //后两条指令跳转回原程序     void *stubcode = malloc(16);     memcpy(stubcode, (void*)hook_addr, 8);     //ldr pc, [pc, #-4]     //[address]     //手动填充stubcode     char jump_ins[8] = {0x04, 0xF0, 0x1F, 0xE5};     uint32_t jmp_address = hook_addr + 8;     memcpy(jump_ins + 4, &jmp_address, 4);     memcpy(stubcode + 8, jump_ins, 8);     //添加执行属性     change_addr_writable((long)stubcode, true);     //修复shellcode中的两个地址值     uint32_t *shell_hook = shellcode + (p_hook_func - p_shellcode_start);     *shell_hook = (uint32_t)func;     uint32_t *shell_stub = shellcode + (p_stub_func - p_shellcode_start);     *shell_stub = (uint32_t)stubcode;     //为hook点添加写属性     change_addr_writable(hook_addr, true);     //替换hook点指令为跳转指令,跳转至shellcode     jmp_address = (uint32_t)shellcode;     memcpy(jump_ins + 4, &jmp_address, 4);     memcpy((void*)hook_addr, jump_ins, 8);     change_addr_writable(hook_addr, false);     //刷新cache     cacheflush(hook_addr, 8, 0); }

注意这里的change_addr_writable函数无论传入false还是true对应地址都会添加上执行属性。由于处理器采用流水线跟多级缓存,在更改代码后我们需要手动刷新cache,即函数cacheflush(第三个参数无意义)。

4、thumb指令集实现

由于thumb指令集的功能受到限制,虽然思路上跟arm指令集一致,但在实现上需要用更多条指令,下面是我自己想的一种实现方式,欢迎交流。

需要注意的是由于每条thumb指令为16bit,所以32位的跳转地址需要占用两条指令的空间,而且跳转时会污染r0寄存器所以要对其进行保护。我在实现程序时将shellcode编译为了arm指令集,所以在原程序、shellcode、stubcode之间相互跳转时需要使用bx指令进行处理器状态切换(需要跳转的地址代码为thumb指令集时,需要将地址的第1个bit位置位)。

(0)

相关推荐

  • 代码实例分析android中inline hook

    以下内容通过1.实现目标注入程序,2.实现主程序,3.实现注入函数,4.thumb指令集实现等4个方面详细分析了android中inline hook的用法,以下是全部内容: 最近终于沉下心来对着书把hook跟注入方面的代码敲了一遍,打算写几个博客把它们记录下来. 第一次介绍一下我感觉难度最大的inline hook,实现代码参考了腾讯GAD的游戏安全入门. inline hook的大致流程如下: 首先将目标指令替换为跳转指令,跳转地址为一段我们自己编写的汇编代码,这段汇编代码先是执行用户指定的

  • 实例分析Android中HandlerThread线程用法

    一.HandlerThread的介绍及使用举例      HandlerThread是什么鬼?其本质就是一个线程,但是HandlerThread在启动的时候会帮我们准备好一个Looper,并供外界使用,说白了就是使我们在子线程中更方便的使用Handler,比如没有HandlerThread我们要在子线程使用Handler,写法如下: private Handler mHandler;    @Override     public void run() {        super.run();

  • 分析Android中应用的启动流程

    前言 在我们开始之前,希望您能最好已经满足以下条件: 1.有一份编译后的Android源码(亲自动手实践才会有更深入的理解) 2.对Binder机制有一定的了解 本文启动流程分析基于Android 5.1的源码.为什么是5.1的源码呢?因为手边编译完的代码只有这个版本-另外,用什么版本的源码并不重要,大体的流程并无本质上的区别,仅仅是实现细节的调整,找一个你熟悉的版本就好. 1.启动时序图 作为一个轻微强迫症的人,整理的时序图,相信大家按图索骥,一定能搞明白整个启动流程: 说明:为了让大家更清楚

  • 实例分析javascript中的异步

    js 异步解析 一 .js单线程分析 我们都知道js的一大特点是单线程,也就是同一时间点,只能处理一件事,一句js代码.那为什么js要设计成单线程而不是多线程呢?这主要和js的用途有关,js作为浏览器端的脚本语言,主要的用途为用户与服务端的交互与操作dom.而操作dom就注定了js只能是单线程语言.假如js才取多线程将会出现,多个线程同时对一个dom进行操作的情况,浏览器将无法判断如何渲染.不仅js是单线程,浏览器渲染dom也是单线程的,js的执行和浏览器渲染dom共用的一个线程,这就导致了在h

  • 分析Android中线程和线程池

    目录 前言 HandlerThread IntentService 线程池的好处 ThreadPoolExecutor 线程池的分类 FixedThreadPool CachedThreadPool ScheduledThreadPool SingleThreadExecutor 前言 由于内容过多,所以将分为上下两部分,第一部分主要和大家谈谈Android中的线程,以及在Android中的常用的线程池.第二部分我们一起来了解一下AsyncTask的使用和工作原理. HandlerThread

  • 详细分析Android中onTouch事件传递机制

    onTach介绍 ontach是Android系统中整个事件机制的基础.Android中的其他事件,如onClick.onLongClick等都是以onTach为基础的. onTach包括从手指按下到离开手机屏幕的整个过程,在微观形式上,具体表现为action_down.action_move和action_up等过程. onTach两种主要定义形式如下: 1.在自定义控件中,常见的有重写onTouchEvent(MotionEvent ev)方法.如在开发中经常可以看到重写的onTouchEv

  • 实例分析浏览器中“JavaScript解析器”的工作原理

    浏览器在读取HTML文件的时候,只有当遇到<script>标签的时候,才会唤醒所谓的"JavaScript解析器"开始工作. JavaScript解析器工作步骤: 1."找一些东西": var. function. 参数:(也被称之为预解析) 备注:如果遇到重名分为以下两种情况: 遇到变量和函数重名了,只留下函数 遇到函数重名了,根据代码的上下文顺序,留下最后一个 2.逐行解读代码. 备注:表达式可以修改预解析的值 JS解析器在执行第一步预解析的时候,会

  • 详细分析Android中实现Zygote的源码

    概述 在Android系统中,所有的应用程序进程,以及用来运行系统关键服务的System进程都是由zygote进程负责创建的.因此,我们将它称为进程孵化器.zygote进程是通过复制自身的方式来创建System进程和应用程序进程的.由于zygote进程在启动时会在内部创建一个虚拟机实例,因此,通过复制zygote进程而得到的System进程和应用程序进程可以快速地在内部获得一个虚拟机实例拷贝. zygote进程在启动完成之后,会马上将System进程启动起来,以便它可以将系统的关键服务启动起来.

  • kotlin改善java代码实例分析

    序 本文主要举几个kotlin如何改善java代码的例子 字符串字面值及模板 字符串字面值 @Test fun testStringLiterals(){ val a = """if(a > 1) { | return a |}""".trimMargin() println(a) val b = """Foo Bar""".trimIndent() println(b) } 有了

  • 实例讲解Android中的AIDL内部进程通信接口使用

    首先描述下我们想要实现的内容,我们希望在一个应用中通过点击按钮,去操作另一个进程中应用的音乐播放功能. 如图,我们点击"播放"时,系统就会去远程调用我们提供的一个service(与当前service不是同一个应用哦),然后操作service中的音乐播放,点击"停止"则会终止播放.想要重新播放的话,必须先点"销毁service",再点播放按钮哦.(至于这里为什么要先点销毁按钮才能播放,完全是为了给大家展示下,远程调用service时,怎么去解绑se

随机推荐