吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6797|回复: 56
收起左侧

[Android 原创] 实现简易安卓inlinehook

  [复制链接]
从0开始的小小怪 发表于 2021-8-1 15:47
本帖最后由 从0开始的小小怪 于 2021-8-3 08:56 编辑

一直都想自己动手实现一个简易的安卓inlinehook, 毕竟从原理上来说并不是太难, 当然我是指最简陋的那种, 也是最方便的那种. 最近两天动手实操了一下, 写下这篇笔记记录一下相关内容, 包括收获和疑问.

编译环境

首先是cmake脚本, 在网上搜ndk大部分都会说要android studio写jni调用之类的, 但在这就没必要了, 因为我们只需要最简单的用c编写的hello world作为目标.

对于新手可能会比较困难搜索到相关的信息, 我主要参考了

脱离as交叉编译
cmake教程

cmake_minimum_required(VERSION 3.1)
#include(D:/android-sdks/build/ndk-bundle/cmake/android.toolchain.cmake)

add_compile_options(-fno-elide-constructors)
project(hello)
set(CMAKE_CXX_STANDARD 11)
add_executable(hello hello.cpp)

然后设置一个bat编译脚本

set abi=armeabi-v7a

if not exist %abi% md %abi%
cd %abi%

%ANDROID_SDK_HOME%/cmake/3.10.2.4988404/bin/cmake ^
  -DANDROID_ABI=%abi% ^
  -DANDROID_NDK=%ANDROID_SDK_HOME%/ndk-bundle ^
  -DCMAKE_BUILD_TYPE=Debug ^
  -DCMAKE_TOOLCHAIN_FILE=%ANDROID_SDK_HOME%/ndk-bundle/build/cmake/android.toolchain.cmake ^
  -DANDROID_NATIVE_API_LEVEL=16 ^
  -DANDROID_TOOLCHAIN=clang -DCMAKE_GENERATOR="Ninja" ^
  -DCMAKE_MAKE_PROGRAM=%ANDROID_SDK_HOME%/cmake/3.10.2.4988404/bin/ninja ^
  ..

%ANDROID_SDK_HOME%/cmake/3.10.2.4988404/bin/ninja
cd ..
pause

相关的环境可能还需要配置一下环境变量之类的, 之后双击bat脚本就能编译出安卓二进制了.

调试环境

我用的真机wifi调试, 真机有root, 比较方便一点. ida调试二进制也是一样的, 把android_server打开, 转发端口, 先打开hello的二进制, 不打开的话可能出现问题, 启动程序而不是附加程序.

将编译出来的libinject.so放到system/lib目录下, 给777权限, hello我放在了data/user目录下, 给777权限.

调试入口挂起配置

调试入口挂起配置

关于调试入口的配置, 如果需要调试libinject.so, 那就把library load这项勾上, 等模块加载完成后去模块窗口找到对应函数下断点. 如果仅仅是需要调试hello, 那就只要entry point这项勾上.

程序启动配置

程序启动配置

程序启动主要需要配置一下前三项路径, 然后启动程序就能够下断点调试了.

Hook对象和Hook代码

在这里, 我们的hook对象是一个简单的hello:

//hook对象 可执行文件hello
#include<stdio.h>
void fun(char *output)
{
        printf("Output is : %s\n", output);
}
int main()
{
        getchar();
        fun("hello");
}

hook目标是替换hello字符串. 加一个getchar()稳妥一点, 方便调试.

//hook代码
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<dirent.h>
#include<sys/mman.h>
#include<errno.h>

int getPID(char *PackageName)                //获得程序pid
{
    DIR *dir=NULL;
    struct dirent *ptr=NULL;
    FILE *fp=NULL;
    char filepath[256];
    char filetext[128];
    dir = opendir("/proc");
    if (NULL != dir)
    {
        while ((ptr = readdir(dir)) != NULL)
        {
            if ((strcmp(ptr->d_name, ".") == 0) || (strcmp(ptr->d_name, "..") == 0))
                continue;
            if (ptr->d_type != DT_DIR)
                continue;
            sprintf(filepath, "/proc/%s/status", ptr->d_name);
            fp = fopen(filepath, "r");
            if (NULL != fp)
            {
                fgets(filetext,sizeof(filetext),fp);
                if (strstr(filetext, "hello") != NULL)//第一个hello是基址
                        {
                                fclose(fp);
                            break;
                        }
                fclose(fp);
            }
        }
    }
    if (readdir(dir) == NULL)
    {
        return 0;
    }
    closedir(dir);
    return atoi(ptr->d_name);
}

unsigned long GetSoBase()                //获得hello模块基址
{
        char programName[] = "hello";
        int pid = getPID(programName);

        char mapsPath[64];
        sprintf(mapsPath, "/proc/%d/maps", pid);

        FILE *fp = fopen(mapsPath, "r");
        char buff[256];

        unsigned long base;
        unsigned long end;
        while (!feof(fp))
    {
        fgets(buff,sizeof(buff),fp);
        if (strstr(buff, "hello") != NULL)
        {
            sscanf(buff, "%lx-%lx", &base, &end);
            return base;
        }
    }
    base = 0;
    return base;
}

void HackFun()
{
        char s[] = "the string has been replaced.";                              //被替换的字符串

        //申请内存, 内存页属性可读可写可执行, 用来存放跳转后的二进制硬编码
        char *codePtr = (char*)mmap(NULL, PAGE_SIZE, PROT_READ |PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE, 0, 0);
        //获得基址, 目标函数偏移是0x2458
        char *patchAddr = (char*)(GetSoBase()+0x2458);
        //修改目标函数内存页属性可读可写可执行, 注意第一个参数必须是内存页起始位置
        mprotect((void*)((int)patchAddr-(int)patchAddr%PAGE_SIZE), PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC);
        //复制一部分fun的代码过去
        memcpy((void*)(codePtr+8), (void*)patchAddr, 128);

    //此处几行为之后的二进制硬编码, 注意点为小端排序
        *((unsigned int*)(codePtr)) = (unsigned int)s;
        *((unsigned int*)(codePtr+4)) = 0x8f85f;                                      //ldr r0,[pc,#-8]

        *((unsigned int*)(codePtr+18)) = 0xbf004a01;                             //ldr r2,[pc,#4]
        *((unsigned short*)(codePtr+22)) = 0x4697;                                //mov pc,r2
        *((unsigned int*)(codePtr+24)) = (unsigned int)patchAddr+10;

        *((unsigned int*)patchAddr) = 0x2f8df;                                        //ldr r0,[pc,#2]
        *((unsigned short*)(patchAddr+4)) = 0x4687;                             //mov pc,r0
        *((unsigned int*)(patchAddr+6)) = (unsigned int)codePtr+4;

        printf("patched done.\n");
}

void __attribute__((constructor)) _init()              //constructor属性使得so被加载时会执行这个函数
{
        printf("init enter.\n");                                                //输出提示, lief添加依赖无问题

        HackFun();                                                                           
}

原理

将其编译为二进制后查看反汇编代码:

.text:00002458 ; _DWORD __fastcall fun(char *)
.text:00002458                 EXPORT _Z3funPc
.text:00002458 _Z3funPc                                ; CODE XREF: main+12↓p
.text:00002458
.text:00002458 var_C           = -0xC
.text:00002458
.text:00002458 ; __unwind {
.text:00002458                 PUSH            {R7,LR}
.text:0000245A                 MOV             R7, SP
.text:0000245C                 SUB             SP, SP, #8
.text:0000245E                 STR             R0, [SP,#0x10+var_C]
.text:00002460                 LDR             R1, [SP,#0x10+var_C]
.text:00002462                 LDR             R0, =(aOutputIsS - 0x2468)
.text:00002464                 ADD             R0, PC  ; "Output is : %s\n"
.text:00002466                 BLX             printf
.text:0000246A                 ADD             SP, SP, #8
.text:0000246C                 POP             {R7,PC}
.text:0000246C ; End of function fun(char *)

........

.text:00002474 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00002474                 EXPORT main
.text:00002474 main                                    ; DATA XREF: .text:00002414↑o
.text:00002474                                         ; .got:main_ptr↓o
.text:00002474
.text:00002474 var_C           = -0xC
.text:00002474
.text:00002474                 PUSH            {R7,LR}
.text:00002476                 MOV             R7, SP
.text:00002478                 SUB             SP, SP, #8
.text:0000247A                 BLX             getchar
.text:0000247E                 LDR             R1, =(aHello - 0x2484)
.text:00002480                 ADD             R1, PC  ; "hello"
.text:00002482                 STR             R0, [SP,#0x10+var_C]
.text:00002484                 MOV             R0, R1  ; char *
.text:00002486                 BL              _Z3funPc ; fun(char *)
.text:0000248A                 MOVS            R0, #0
.text:0000248C                 ADD             SP, SP, #8
.text:0000248E                 POP             {R7,PC}
.text:0000248E ; End of function main

总体思路: arm的参数传递约定前四个是通用寄存器r0-r3, 也就是说想要改变fun的参数, 我们只需要把fun入口劫持, 将r0寄存器改成我们想要的值, 同时不改变其他寄存器状态. 然后返回原函数位置, 做到受害者无感知.

代码的注入时机: 可以参考frida-gadget的实现,使用lief工具把我们自己的libinject.so添加进hello二进制的依赖库。在so中使用constructor属性实现hook函数,这样注入的so和目标程序处于同一进程内存空间下。hook函数先枚举pid,然后找到主模块基址加上目标函数偏移。这样前期准备工作就完成了,之后是处理hook的细节。

hook具体的细节:

在实际逆向过程中还是有很多thumb指令, 所以这里目标也是thumb形式的二进制. 我们要编写尽可能少的汇编指令, 转化成二进制填到相关区域, 因为一旦被我们填掉的区域中含有相对pc寻址的指令, 那就麻烦多了. 比如这里的.text:00002462便是与当前pc寄存器值有关, 所以就要求被修改的指令长度不超过10个字节.

演示流程图

演示流程图

申请一块内存区域, 设置可读可写可执行, 将fun函数整体复制过去. 首先是被hook的fun函数入口, 用mprotect设置fun函数区域可读可写可执行, 众所周知thumb情况下读pc寄存器实际会读出pc+4(这里有疑问, 在后面会遇到). 最精简的代码是直接ldr pc,[pc], 但是这么做貌似会造成指令模式的切换, 具体我没太研究. 通用寄存器方面考虑r0可以使用, 因此有此方案, 总共正好10个字节长度.

ldr r0, [pc,#2]                    #加载第三行地址到r0                DF F8 02 00
mov pc, r0                         #跳转                             87 46
xx xx xx xx                        #目标地址                         4字节
#修改完跳转回来

跳转到目标地址后同理修改r0的值, 跳转回去. 跳转回去的时候r0存储了修改后的值, r1被使用了, 所以用r2寄存器

xx xx xx xx                        #替换的字符串地址
ldr r0,[pc,#-8]                    #替换r0值             5F F8 08 00
#执行以下5条被污染的指令, 均不涉及pc相关寻址
PUSH {R7,LR}                
MOV R7, SP
SUB SP, SP, #8
STR R0, [SP,#0x10+var_C]
LDR R1, [SP,#0x10+var_C]
ldr r2,[pc,#4]                     #注意是+4, 而不是+2    01 4A
NOP                                #补齐                 00 BF
mov pc,r2                          #跳转回去             97 46
xx xx xx xx                        #回去的地址

第9行位置并不是+2而是+4, 并且因为本身缩短为2字节, 要有nop来补足. 如果沿用fun函数处的修改则会出现问题, 其会把mov pc,r2这条指令和后面两个数据字节加在一起看做一条指令, 表面上以ida的视角是这样的. 单步执行下去ida会提示检测到未被识别为代码的数据, 问是否创建指令, 执行完这步就会使得取得的数据地址比预期中少两个字节. 具体原因未知, 也是比较困惑我的一点(下面图片中r0应该换成r2, 放错图了)

错误情况

错误情况

将其换成+4用nop补齐则解决了这一问题.

正确情况

正确情况

最后输出结果为

输出结果

输出结果

这样几行代码就能实现一个迷你的inlinehook效果了, 并且还是永久的, 实际上我随便找了一个游戏试了试也是有效果的. 原本的打算的直接操作elf的段, 在原有的二进制基础上进行注入, 凭空新增一个段总是会出现莫名其妙的问题, 然后退了一步, 以后有机会再尝试尝试.

虽然这样局限性非常的大, 具体问题要具体分析, 实现的功能复杂点就很难把握, 被污染的指令要是含有相对pc寻址的数据或跳转就需要做复杂的修复, 或者函数体非常小, 不到10个字节. 但实验完成了以后对于底层的把握就更加深刻了, 只要是能够对内存进行操作, 那就可以去随意拦截代码段, 比如gg修改器配合lua脚本能做到的就更多了, 而不仅仅是简单的搜索内存, 修改内存.

挖个坑, 初步打算是有时间再进一步一般化, 实现一个不怎么复杂的hook框架, 能够足以应对大部分情况.

补充一下出现跳转奇怪现象的二进制, 感兴趣的可以去琢磨琢磨
文件链接

点评

如果pc寄存器被用于相对寻址操作(如此处的ldr),会先被对齐到word(在32位CPU中即为4字节),即使是在Thumb模式下  发表于 2021-8-3 11:17

免费评分

参与人数 23威望 +2 吾爱币 +122 热心值 +21 收起 理由
lizhenqiang1990 + 1 热心回复!
xbxbxbxb + 1 + 1 热心回复!
victos + 1 + 1 谢谢@Thanks!
b000d + 1 + 1 谢谢@Thanks!
371564030 + 1 + 1 我很赞同!
yi辈子的诚若 + 1 + 1 已经处理,感谢您对吾爱破解论坛的支持!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
pdcba + 1 + 1 谢谢@Thanks!
Sponges + 1 + 1 谢谢@Thanks!
Capslock + 1 + 1 谢谢@Thanks!
gaosld + 1 + 1 谢谢@Thanks!
xmmyzht + 1 + 1 谢谢@Thanks!
GuiXiaoQi + 1 我很赞同!
XhyEax + 1 + 1 thumb情况下读pc寄存器=pc+4,是因为指令流水线
qtfreet00 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
chenaniah + 1 + 1 谢谢@Thanks!
laughtosky0 + 1 + 1 谢谢@Thanks!
hanlaoshi + 1 + 1 谢谢@Thanks!
Stubborn6 + 1 我很赞同!
cymyzero + 1 + 1 谢谢@Thanks!
lfm333 + 1 + 1 谢谢@Thanks!
shiina0831 + 1 + 1 谢谢@Thanks!
柒月v + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

zwill1 发表于 2021-8-1 18:55
谢谢分享
XhyEax 发表于 2021-8-3 10:38
本帖最后由 XhyEax 于 2021-8-3 12:05 编辑

调试发现ldr的地址未对齐4字节,就会出现这个问题

问题指令:
EA627012 ldr r2,[pc,#2]            #预期R2=[pc+4+2=EA627018],执行时变为[EA627016]
EA627016 mov r0, r0                #替换为 mov r0, r0 方便测试
EA627018 xx xx xx xx               #回去的地址,以46004600为例(小端模式)

对应arm指令:
DF F8 02 20
00 46
00 46 00 46

指令位置:EA627012,注意到并未4字节对齐

将指令位置对齐到4字节后,再次反编译,发现跳转地址正确
arm-address-4-byte.png

原因探究:

当pc寄存器被用于相对寄存器寻址操作时(如此处的ldr),会先被对齐到word(在32位CPU中即为4字节)(即使是在Thumb状态下)
由于相对PC寻址通常是指定标签而非手动计算偏移,所以这个细节很容易被忽略。

例子:
0x159a ldr...[pc]
pc计算:
0x159a+4 = 0x159e,再将其对齐到word,得到 0x159c


About arm pc value in thumb 16/32bits mixed instructions stream - Stack Overflow
To further complicate matters, when the PC is used as a base register for addressing operations (i.e. adr/ldr/str/etc.) it is always the word-aligned value that is used, even in Thumb state. So, whilst executing a load instruction at 0x159a, the PC register will read as 0x159e, but the base address of ldr...[pc] is Align(0x159e, 4), i.e. 0x159c. Since PC-relative addressing is normally written by specifying a label rather than calculating offsets manually, this detail can be easy to miss.


结论:
虽然被覆盖的指令未出现相对PC寻址,跳转回去时还是使用到了PC,所以出现了自动对齐问题

点评

原来如此, 之前也有想过字节对齐的事, 没有去查资料, 感谢  发表于 2021-8-3 11:40
hongII 发表于 2021-8-1 17:45
vms 发表于 2021-8-1 20:53
谢谢分享
shiina0831 发表于 2021-8-1 21:01
谢谢  感谢大佬的热心分享
goda 发表于 2021-8-1 22:30
谢谢分享
年轻的旅途 发表于 2021-8-1 23:24
谢谢分享
penz 发表于 2021-8-2 01:07
谢谢分享
lfm333 发表于 2021-8-2 01:11
感谢分亨
china08 发表于 2021-8-2 07:10
谢谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-15 12:08

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表