热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android下的挂钩(hook)和代码注入(inject)

Android是基于linux内核的操作系统,根据语言环境可以简单的划分为java层、nativeC层、linux内核层。java层通过jni与native层交互,使用linux提供的底层函数功能。

Android是基于linux内核的操作系统,根据语言环境可以简单的划分为java层、native C层、linux内核层。java层通过jni与native层交互,使用linux提供的底层函数功能。

因此,类似linux系统,我们可以在Android下实现对另一个进程的挂钩和代码注入。在这简单介绍下挂钩和代码注入的方法和两个库,以及针对《刀塔传奇》实现的代码注入。

利用libinject实现so注入和API Hook

一. so注入

Linux上有一个强大的系统调用ptrace,它提供了父进程观察和控制子进程的能力,并允许父进程检查和替换子进程寄存器的值(大名鼎鼎的gdb也是基于ptrace的),当使用ptrace后,发送给子进程的信号会转发给父进程,而子进程会被阻塞,父进程收到信号后,就可以对子进程进行检查和修改。其原型为:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

1). enum __ptrace_request request:ptrace要执行的命令。
2). pid_t pid: 指示ptrace要跟踪的进程。
3). void *addr: 指示要监控的内存地址。
4). void *data: 存放读取出的或者要写入的数据。

在获得root权限的情况下,我们可以再Android上ptrace另一个进程,读取和修改该进程的内存数据。看雪论坛上有大神实现了Android下的so注入libinject,代码的大致原理:

  1. ptrace attack到目标进程,保持寄存器数据,接管程序的运行。

  2. 找到目标进程的mmap函数地址,调用mmap分配一段内存空间。

  3. 找到目标进程的dlopen、dlclose、dlsym的地址,调用dlopen载入.so文件,调用dlsym获取.so文件的地址。

  4. 找到so中” hook_entry”函数的地址并执行。

  5. 收尾,调用dlclose,还原寄存器,执行ptrace_detach,把控制权还给目标程序。

libinject中有两个主要功能(寻找函数地址和调用函数),函数分别为get_remote_addrptrace_call_wrapper,代码如下:

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

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

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

#if defined(__i386__)
if (!strcmp(module_name, libc_path)) {
ret_addr += 2;
}
#endif
return ret_addr;
}

int ptrace_call_wrapper(pid_t target_pid, const char * func_name, void * func_addr, long * parameters, int param_num, struct pt by ZoomApp" rel="nofollow noopener">REGS * by ZoomApp" rel="nofollow noopener">REGS)
{
DEBUG_PRINT("[+] Calling %s in target process.\n", func_name);
if (ptrace_call(target_pid, (uint32_t)func_addr, parameters, param_num, by ZoomApp" rel="nofollow noopener">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;
}

so的地址是通过分析/proc/pid/maps文件得到的。 Maps文件如下:

在arm处理器下,执行函数的代码如下:

#elif defined(__i386__) 
long ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct user by ZoomApp" rel="nofollow noopener">REGS_struct * by ZoomApp" rel="nofollow noopener">REGS)
{
by ZoomApp" rel="nofollow noopener">REGS->esp -= (num_params) * sizeof(long) ;
ptrace_writedata(pid, (void *)regs->esp, (uint8_t *)params, (num_params) * sizeof(long));

long tmp_addr = 0x00;
regs->esp -= sizeof(long);
ptrace_writedata(pid, regs->esp, (char *)&tmp_addr, sizeof(tmp_addr));

regs->eip = addr;

if (ptrace_setregs(pid, regs) == -1
|| ptrace_continue( pid) == -1) {
printf("error\n");
return -1;
}

int stat = 0;
waitpid(pid, &stat, WUNTRACED);
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
waitpid(pid, &stat, WUNTRACED);
}

return 0;
}

把返回地址设置为0的目的是让目标进程继续执行完后找不到返回地址出错,进程会被挂起,控制权又回到了父进程也就是libinject手上。

二. API Hook

api hook技术有2种elf hook 和inline hook。Elf hook 通过修改动态连接库的PLT/GOT表,从而达到函数调用的重定向目的,这种方法只能hook模块的外部调用,例如hook打开文件的系统函数检测程序打开文件的情况,hook系统时间相关的函数,达到加速的目的(市面上的加速外挂基本都是采取这种方法)。但是这种方法不能hook模块的内部调用,因为模块内部调用不需要查GOT表。而游戏引擎的功能都封装在一个动态连接库里,基本都是内部调用,ELF HOOK无法生效。本文所采用的是另外一个方法:INLINE HOOK。

INLINE HOOK的思路大致是这样:首先找到目标函数在内存中的地址,然后把该地址块设置为可写,修改目标函数地址的内容,让游戏调用目标函数时跳转到我们自己的函数地址,我们的函数执行完后再跳转回来。这样不论是模块内部调用或外部调用,INLINE HOOK都能生效。具体步骤如下:

  1. 找到目标函数在内存中的地址,跟上文提到的寻找函数地址的方法不一样,注入so后,我们的代码是在目标进程空间中执行的,无法通过so基址的偏移计算函数在内存的地址,因为目标so在内存中只有一份。通过另外一种方式寻找函数地址,在linux下,可执行文件和动态连接库都是使用ELF文件格式的,ELF结构体中包含了所有符号的信息,通过解析ELF,可以获取到目标函数在内存中的地址。ELF文件格式这里就不介绍,感兴趣的同学可以查看ELF文件介绍。

  2. 代码段加载进内存后,一般不需要修改,所以代码段是没有写属性的,需要调用mprotect()把内存块加上PROT_WRITE属性。

  3. 接下来就可以把汇编指令写入指定地址了,以Arm指令集为例,把目标函数的头12个字节(ARM每条指令32位,即4个字节)先备份下来,然后替换成如下内容:

0xe59ff000, ldr pc, [pc, #0] ; 跳转到hook_func处开始执行 (ARM模式下,由于采用多级流水线结构,PC实际值为当前指令地址+8)

0xe1a08008, 同 nop。

hook_func, hook函数的地址。

当执行到我们的目标函数时,会被跳转到目标函数地址+2的位置,也就是我们的hook函数。在我们的hook函数里先把目标函数的头12个字节还原,然后再调用目标函数,调用完后再把头12个字节修改回来。关键代码如下:

unsigned int orig=0;  
unsigned int store[3] = {0,0,0};
int jump_code[3] = {0xe59ff000, 0xe1a08008, 0};
//addr:目标函数地址,hookf:hook函数。
int hook_direct(unsigned int addr, void *hookf)
{
//设置hook函数
jump_code[2] = (unsigned int)hookf;
orig = addr;//保持好目标函数的地址
//备份前3个int
for (i = 0; i < 3; i++)
store[i] = ((int*)addr)[i];
for (i = 0; i < 3; i++)
//修改为我们的跳转指令
((int*)orig)[i] = jump_code[i]
return 1;
}
void hook_func()//hook函数
{
printf(hello\n);
//还原目标函数的头12个字节
for (i = 0; i < 3; i++)
((int*)orig)[i] = store[i]
void(*orig_func)() = (void*)orig;
orig_func();//执行目标函数
//重新修改目标函数的头12个字节。
for (i = 0; i < 3; i++)
((int*)orig)[i] = jump_code[i];
}

这种方式没有处理函数的返回值,如果目标函数有返回值的话就会有问题。严谨的做法如下:

int jump_pre_code[12] = {  
0xe92d0008, 0xe1a0300f, 0xe283301c, 0xe583e000, 0xe89d0008,
0xe28dd004, 0xe1a0e00f, 0xe59ff008, 0xe59fe000, 0xe1a0f00e,0,0
};
Jump_pre_code[11]= (unsigned int)hookf;
for(int i = 0; i<12; i++)
jump_code[i+2] = jump_pre_code[i];

jump_pre_code所对应的汇编指令如下:

stmdb sp!, {r3}     ; 将r3压入栈中,保存r3因为后边要修改r3,栈顶指针-1;  
mov r3, pc ; 将4指令所在地址赋给r3
add r3, r3, #0x1C ; 将r3加上0x1c即从4指令所在地址往下7条指令,即就是jump_pre_code[10]赋给r3
str lr, [r3] ; 将调用原函数的返回地址存入jump_pre_cod[10]中
ldmia sp, {r3} ; 从栈中取出之前保存的r3
add sp, sp, #4 ;还原调用栈
mov lr, pc ; 将返回地址存入lr中
ldr pc, [pc, #0x8] ; 将10指令所在地址往下2条指令处的内容赋给pc,即PC跳到hookf我们自定义的hook函数
ldr lr, [pc, #0] ; 执行完我们自定的hook函数后就会返回到这了,此处将当前指令往下2条指令出的内容即jump_pre_cod[10]取出赋给lr,即还原原先调用者的返回地址
mov pc, lr ; 跳到原调用者的返回地址去
addr_ret ;存放原函数的返回值
hookf ;hook函数

利用adbi实现对《刀塔传奇》so注入和API Hook

对于Android下的inject和hook,github上有个adbi框架。这个框架所使用的方法和上面介绍的差不多,采用inline hook但是没有处理返回值的情况,根据刚才介绍的方法,可以把返回值处理的功能加上,Thumb指令集的hook有个BUG,没有正确的跳转到hook函数的地址,跳转地址少了算2个字节,应该是jumpt[18]到jumpt[21]这4个字节存放hook_func的地址。

该框架包括2个模块:hijack和instruments,hijack编译出来后是一个可执行文件,用来注入动态链接库的。Instruments里包括base和example,example是一个hook的例子,编译出来是一个so文件。

《刀塔传奇》是利用cocos2d-x+lua编写的游戏,我们可以利用adbi实现对cocos2dx+lua中常用的函数如lua_pushstring的inject和hook从而来执行我们自己的lua脚本。lua_pushstring的声明如下:

LUA_API void (lua_pushstring) (lua_State *L, const char* str);

基本思路是拿到lua 虚拟机的地址,然后调用luaL_loadstring和lua_call就可以在游戏进程中执行我们自己的lua脚本。主要代码如下:

int (*my_lua_call)(lua_State *, int, int) = NULL;  
int (*my_luaL_loadstring)(lua_State*, const char *) =NULL;
//目标进程lua_pushstring的hook函数,调用lua_pushstring时会先调用它
void my_lua_pushstring (lua_State *L, const char* str);
{
int (*orig_lua_isuserdata)(lua_State *L, int idx);
//得到原函数
orig_lua_pushstring = (void*)eph.orig;
log("start hook func..precall...\n");
hook_precall(&eph); //还原原函数首地址
orig_lua_pushstring (L, str);//调用原函数。
if (counter) {
log("lua_pushstring() called\n");
counter--;
if (!counter)
log("removing hook for lua_pushstring ()\n");
}
//如果找到了lua_call和luaL_loadstring的话就执行自己的lua代码。
if(my_lua_call!=NULL&&my_luaL_loadstring!=NULL)
{
my_luaL_loadstring(L, LUACODE);
my_lua_call(L,0,0);
}
hook_postcall(&eph);//备份
}
void my_init(void) //so注入成功后会调用这个函数
{
counter = 3;

log("%s my_init started\n", __FILE__);
set_logfunction(my_log);
unsigned long int lua_call_addr;
unsigned long int lua_loadstring_addr;
//在进程中找lua_call函数地址
find_name(getpid(),"lua_call", soname, &lua_call_addr);
//在目标进程中找luaL_loadstring函数地址.
find_name(getpid(), "luaL_loadstring",soname,
&lua_loadstring_addr);
my_lua_call = lua_call_addr;
my_luaL_loadstring = lua_loadstring_addr;
log("lua_call addr:%p, luaL_loadstring addr:%p\n", lua_call_addr, lua_loadstring_addr);
//执行hook。
hook(&eph, getpid(), soname, "lua_pushstring", my_lua_pushstring_arm, my_lua_pushstring);
}

(1)adbi主要包括hijack和libbase。Hijack提供了代码注入的功能,libbase提供了挂钩和卸载钩子的功能。

(2)编译adbi。主要是利用Android的NDK分别编译hijack和libbase,编译过后会生成一个libexample.so文件,并保存到/data/local/tmp/目录下,赋予执行权限,如下图:

(3)查找所需挂钩游戏的so文件,可以通过两种方式,一种是解压apk文件查看lib目录下的so文件,另一种是使用命令“cat /proc/PID/maps”查看。如图显示了部分so文件:

(4)保存执行hijack命令,如下图所示,其中776是《刀塔传奇》的进程ID。

(5)运行《刀塔传奇》,在游戏调用lua_pushstring函数时,会跳转执行自己的函数,主要函数功能如下:

local function Run20()
local label = CCLabelTTF:create("hello cocos2.x", "Arial", 60)
local MenuItem = CCMenuItemLabel:create(label)
MenuItem:registerScriptTapHandler(MainMenuCallback)
local s = CCDirector:sharedDirector():getWinSize()
local Menu = CCMenu:create()
Menu:addChild(MenuItem)
Menu:setPosition(100, 100)
MenuItem:setPosition(250, 25)
local scene = CCDirector:sharedDirector():getRunningScene()
scene:addChild(Menu)
Menu:setZOrder(99999999)
log("crate menuitem......")
end

(6)执行上述代码的效果如下图(使用的是MTL的HTC手机)

参考:

Android中的so注入(inject)和挂钩(hook)

发个Android平台上的注入代码


推荐阅读
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • MATLAB函数重名问题解决方法及数据导入导出操作详解
    本文介绍了解决MATLAB函数重名的方法,并详细讲解了数据导入和导出的操作。包括使用菜单导入数据、在工作区直接新建变量、粘贴数据到.m文件或.txt文件并用load命令调用、使用save命令导出数据等方法。同时还介绍了使用dlmread函数调用数据的方法。通过本文的内容,读者可以更好地处理MATLAB中的函数重名问题,并掌握数据导入导出的各种操作。 ... [详细]
  • Week04面向对象设计与继承学习总结及作业要求
    本文总结了Week04面向对象设计与继承的重要知识点,包括对象、类、封装性、静态属性、静态方法、重载、继承和多态等。同时,还介绍了私有构造函数在类外部无法被调用、static不能访问非静态属性以及该类实例可以共享类里的static属性等内容。此外,还提到了作业要求,包括讲述一个在网上商城购物或在班级博客进行学习的故事,并使用Markdown的加粗标记和语句块标记标注关键名词和动词。最后,还提到了参考资料中关于UML类图如何绘制的范例。 ... [详细]
  • 本文分析了Wince程序内存和存储内存的分布及作用。Wince内存包括系统内存、对象存储和程序内存,其中系统内存占用了一部分SDRAM,而剩下的30M为程序内存和存储内存。对象存储是嵌入式wince操作系统中的一个新概念,常用于消费电子设备中。此外,文章还介绍了主电源和后备电池在操作系统中的作用。 ... [详细]
author-avatar
D之phper
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有