# 前言

最近需要对一个固件的 arm64 linux 系统中的一个进程进行 inline hook, 然而当把 frida-inject 上传上去之后运行却发现,由于 linux 版本为 4.4 有些古老了,并且比正常的 linux 缺了一些文件,导致 frida 一运行就报错退出,所以就想着能不能自己去实现一个 inline hook. 但在这之前我们得先知道原理才行~如果想知道 inline hook 的原理是什么样子的,我认为最好的办法就是直接去看看 hook 之后的代码变成了什么样子,用调试的方法研究 frida 实现 inline hook 的具体实现

首先我们对一个测试 apk 注入下面的脚本实现 hook

function hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    //do your own code
                    hook_native()
                }
            }
        }
    );
}
function hook_native(){
    var module = Process.findModuleByName("liboacia.so");
    Interceptor.attach(module.base.add(0x10B0), {
        onEnter: function (args) {
            console.log("hook test apk at ",module.base.add(0x10B0))
            console.log("pid = ",Process.getCurrentThreadId())
            console.log(this.context.x0)
        },
        onLeave: function (ret) {
        }
    });
}
setImmediate(hook_dlopen, "liboacia.so")
oriole:/ # /data/local/tmp/fs16.3.3 &
frida -U -l .\hook.js -f com.oacia.apk_protect

被 hook 的代码如图所示

image-20240723231240202

注入完成之后,我们使用 lldb 去调试看看

# lldb 动态调试

先到 ndk\26.1.10909125\toolchains\llvm\prebuilt\windows-x86_64\lib\clang\17\lib\linux\aarch64lldb-server 推送到手机上

adb push .\lldb-server /data/local/tmp

随后在手机上启动 lldb-server

./lldb-server platform --server --listen unix-abstract:///data/local/tmp/debug.sock &

然后找到 ndk\26.1.10909125\toolchains\llvm\prebuilt\windows-x86_64\bin\lldb.cmd , 运行这个 cmd

.\lldb.cmd

进入后设置要连接的类型为 remote-android

platform select remote-android

image-20240723150910476

然后连接安卓中的 lldb-server

platform connect unix-abstract-connect:///data/local/tmp/debug.sock

image-20240723151030760

使用 platform status 查看是否已经连接

image-20240723151113720

之后 attach 到指定的包名上

process attach -p [pid]
error: attach failed: Connection shut down...

我在使用 lldb 的过程中出现了这个报错

(lldb) process attach -p 32883
error: attach failed: Connection shut down by remote side while waiting for reply to initial handshake packet

网上都说是开了 SELinux 的问题,我把 SELinux 临时设置为 0 还是没有解决这个 error

oriole:/data/local/tmp # setenforce 0
oriole:/data/local/tmp # getenforce                                             Permissive

没办法,只能用 logcat 看看日志了,随后发现了这条日志,但是木有作用

E adbd: failed to connect to socket 'localabstract:': could not connect to localabstract address 'localabstract:'

后来排查了一下午,原来是我为了防止进程名检测,保留了过去 ida-server 和 frida-server push 到手机里面之后修改文件名的习惯,把 llbd-server 的名字改成了 lldb26 , 然后…attach 的时候就报错了(改个文件名都不行 -,-

随后打个断点

(lldb) breakpoint set -s liboacia.so -a 0x10B0

通过对 hook 地址的反编译,发现了很有意思的点就是从 hook 地址开始的后 16 个字节的汇编都和原来不一样了,但是 lldb 的汇编看起来很奇怪

0x6e84a900b0: 0x58000050   ldr    x16, #0x8
    0x6e84a900b4: 0xd61f0200   br     x16
    0x6e84a900b8: 0xb092a400   adrp   x0, -895871
    0x6e84a900bc: 0x00000071   udf    #0x71

image-20240723232420799

这段汇编给 x16 寄存器赋值为相对偏移 #0x8 位置的值,然后再去进行 BR 跳转,这后两行的指令 adrp x0, -895871udf #0x71 压根就不是指令,而是一个八字节的一级跳板要跳往的地址

不得不说 lldb 确实是一个非常强大的动调工具,我觉得使用 lldb 的 python API 直接用脚本和汇编交互的样子一定非常的厉害,等之后分析抖音的 vmp 的时候在用它吧

想看正确的汇编怎么办呢,上 IDA!

# IDA 动态调试分析 hook 跳板

IDA 直接附加的话是附加不上的,原因在于 apk 设置了 android:extractNativeLibs="false" 导致 so 不会被解压到 /data/app/...com.oacia.apk_protect/lib/arm64 文件夹中,而是直接从 apk 中加载,所以 IDA 也就找不到这个 so 加载的时候了,这种情况下我们把要调试的 liboacia.so 用 MT 管理器直接复制到 /data/app/...com.oacia.apk_protect/lib/arm64 里面去就可以了,同时注意需要给这个 so rwx 的运行权限

# 一级跳板

有了 IDA 强大的反编译功能,这里的汇编也是显示了出来,从 hook 点开始的 16 个字节全部被一级跳板的字节码覆盖了,前 8 字节是两行汇编,用来实现跳板的跳转,后八字节存储了跳板要跳往的地址

image-20240724005026756

这里使用的是 x16 和 x17 寄存器,那为什么使用这两个寄存器呢?

根据 arm64 官网描述,x16 和 x17 是程序内调用临时寄存器,也就是平时根本不会用他们,所以把这两个寄存器用作跳板是再合适不过的

X16 and X17 are IP0 and IP1, intra-procedure-call temporary registers. These can be used by call veneers and similar code, or as temporary registers for intermediate values between subroutine calls. They are corruptible by a function. Veneers are small pieces of code which are automatically inserted by the linker, for example when the branch target is out of range of the branch instruction.

那顺便也记一下其他寄存器的作用好了

  • X0-X7: 参数寄存器,用于传递函数参数和返回结果,要是参数比八个多的话就会用堆栈去传递
  • X8: 用来存放函数返回值的,不过这个寄存器比较特殊一点,只有当返回值是大型结构体的时候才会用 x8 去返回,其余情况都是用 x0 去返回的
  • X9-X15: 临时寄存器,这 7 个寄存器是用来存储临时数据的
  • X16-X17: 他们是给编译器使用的,正常的程序都不会使用他们,所以当作跳板是相当的合适
  • X18: 平台寄存器,基本是不用的
  • X19-X28: 保存寄存器,这 10 个寄存器用来保存函数调用上下文
  • X29: FP 寄存器,用于连接栈帧
  • X30: 也叫 LR 寄存器,用于保存子程序的返回地址
  • X31: SP 寄存器,用于指向每个函数的栈顶
  • SP: SP 是栈顶指针寄存器,类似 Intel 64 中的 RSP 寄存器
  • PC: PC 寄存器存储当前要执行的指令地址,类似 Intel 64 中的 RIP 寄存器

# 二级跳板

二级跳板中 X17 保存了返回地址,也就是我们本身的 hook 地址的值,而 X16 则保存了三级跳版的位置,而在二级跳板的下方,竟然还有一个跳板,不过现在还没有看到它的用途所在

image-20240724012046288

这里二级跳板有啥用?为啥不直接跳到三级跳板?

# 三级跳板

这三级跳板总共干了三件事情

# 保存寄存器环境

image-20240827140044091

# 跳转到核心代码

image-20240724013031337

经过三个跳板的层层跳转,我们也是终于来到了 frida_agent_64.so 中执行我们编写的 hook 逻辑比如打印寄存器,读取内存等操作了

image-20240724013427577

# 恢复寄存器环境

使用 LDP 加载栈中保存的寄存器环境

image-20240724013125866

# 恢复执行流

由于 hook 所生成的跳板函数会对 hook 位置的 16 字节汇编进行覆盖,所以必须要对被覆盖的那 16 字节汇编再次执行,否则将会对执行流产生影响。在三级跳板的最后 RET X16 , 将会来到跳转到此处执行先前未完成的汇编

image-20240816101950696

# 自实现 inline hook

虽说要 hook 的目标是 arm64 linux, 但是本人对于安卓更为熟悉一些,所以这里的示例也使用的是 android (毕竟都是 arm64 架构,hook 肯定是通用的)

先写个一键编译的 run.bat 脚本,这样运行一下连带编译传输到手机上的步骤都一步到位了~

"E:\Program Files\CLion 2024.1\bin\cmake\win\x64\bin\cmake.exe" --build E:\analysis\frida-inline-hook\self_hook\cmake-build-android --target hook-server -j 14
adb push .\cmake-build-android\hook-server /data/local/tmp
adb shell "chmod 777 /data/local/tmp/hook-server"
adb shell su -c "/data/local/tmp/hook-server"

# ptrace 注入

如果我们要自己去实现对进程的注入,那么首先要做的就是使用 ptrace 的 PTRACE_ATTACH 附加到这个进程上面去

void ptraceAttach(pid_t pid){
    if(ptrace(PTRACE_ATTACH,pid,NULL,NULL)==-1){
        printf("[ptrace] Failed to attach:%d\n",pid);
    }
    else{
        printf("[ptrace] Attach to pid %d\n",pid);
    }
    int stat=0;
    /*  在用 ptrace 去 attach 一个进程之后,那个被 attach 的进程某种意义上说可以算作那个 attach
     *  进程的子进程,这种情况下,就可以通过 waitpid 这个函数来知道被调试的进程何时停止运行
     *
     *  @param option->WUNTRACED: 如果子进程进入暂停状态,则马上返回。*/
    waitpid(pid,&stat,WUNTRACED);
}

image-20240819152115591

一旦我们使用 ptrace 附加了上去,这就意味着我们有了对这个进程空间中的内存进行操作的权力,现在我们需要知道的是目标 so 库的基址在什么地方,这样我们才可以通过在 ida 中静态分析找到的 hook 点的偏移,来找到 hook 点的虚拟内存地址,而这可以通过遍历 maps 来实现

void* findModuleByName(pid_t pid,const char* libname){
    // 获取 hook-server 的 pid, 这样可以通过本地 libc 库函数地址 - 本地 libc 基址 + 远程 libc 基址 得到远程 libc 库函数地址
    if(pid==-1){
        pid=getpid();
    }
    char maps[MAX_PATH];
    void* base_addr = 0;
    snprintf(maps,MAX_PATH,"/proc/%d/maps",pid);
    FILE *f = fopen(maps,"r");
    char line[MAX_PATH],name[MAX_PATH];
    char *base;
    while(!feof(f)){
        memset(line,0,MAX_PATH);
        fgets(line,MAX_PATH,f);
        //printf("%s\n",line);
        // 查找指定模块是否在某行出现
        if(strstr(line,libname)){
            //maps 形式: base-end [rwxsp] offset dev inode pathname
            //eg. 6f1305e000-6f13060000 r-xp 00000000 fe:2e 90655 /lib/arm64/liboacia.so
            /*
             * https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html
             * The [offset] field is the offset into the file/whatever;
             * [dev] is the device (major:minor);
             * [inode] is the inode on that device.  0 indicates that no inode is associated with the memory
             * region, as would be the case with BSS (uninitialized data).
             * If  the [pathname] field is blank, this is an anonymous mapping as obtained
             * via the mmap(2) function.  There is no easy way to coordinate this back to
             * a process's source, short of running it through gdb(1), strace(1), or similar.*/
            base = strtok(line, "-");// 以 `-` 为分隔符,读取基址
            base_addr = (void*)strtoul(base, NULL, 16);
            printf("[maps] find module [%s] base at 0x%08lx\n",libname,base_addr);
            break;
        }
    }
    fclose(f);
    return base_addr;
}

image-20240819160333432

现在我们尝试去读取一下 hook 点的汇编,来看看 ptrace 究竟有没有生效,这可以通过 PTRACE_PEEKTEXT 来实现

void ptraceReadData(pid_t pid,void* addr,char*data,size_t len){
    size_t i=0;
    long rdata;
    for(;i<len;i+=sizeof(long)){
        rdata=ptrace(PTRACE_PEEKTEXT,pid,(long)addr+i,NULL);
        *(long*)&data[i]=rdata;
    }
}

image-20240819162616719

nice, 将通过 ptrace 读取的汇编和 ida 中反编译出来的汇编对比一下,简直就是一模一样~

image-20240819162708924

# 为字符串分配内存

接下来我们需要通过 dlopen 加载我们的功能库 so, 而这必定会涉及到字符串相关的内存,为了让目标字符串可以被远程的进程访问到,所以我们必须要通过 mmap 在远程的进程空间中分配一块属于我们自己的,可以自由操控的内存

在这之前,我们先对当前的寄存器环境进行保存,以便之后可以还原寄存器的环境

void ptraceGetRegs(pid_t pid,struct user_pt_regs *regs_addr){
    struct iovec io;
    io.iov_base = regs_addr;
    io.iov_len = sizeof(struct user_pt_regs);
    //NT_PRSTATUS: general-purpose registers, 定义在 elf.h 中
    /**
     * PTRACE_GETREGSET
     * 读取被追踪者寄存器。addr 参数决定读取寄存器的类型。
     * 如果 addr 是 NT_PRSTATUS,则读取通用寄存器。
     * 如果 addr 是 NT_foo,则读取浮点或向量寄存器(如果有的话)。data 参数指向 iovec 类型:
     */
    if(ptrace(PTRACE_GETREGSET,pid,NT_PRSTATUS,&io)==-1){
        printf("Get regs failed");
    }
}
void inject(pid_t pid){
    ...
	struct user_pt_regs oldRegs;
    struct user_pt_regs regs;
    // 保存寄存器环境
    ptraceGetRegs(pid,&oldRegs);
    memcpy(&regs,&oldRegs, sizeof(struct user_pt_regs));
    ...
}

接下来我们去获取一下 mmap 在远程空间中的位置,在 android 中, mmap 是在 libc 库中被定义的,具体的位置是在 /apex/com.android.runtime/lib64/bionic/libc.so , 由于 mmap 在 libc.so 中的相对偏移是固定的,而我们可以在 hook-server 内部直接获取到 mmap 函数的本地地址 (利用 *mmap 获取函数指针即可实现), 所以我们再读取一下 hook-server 中的 libc.so 的基址,相减即可得到 mmap 函数的相对偏移,把这个相对偏移再加上被 hook 的进程中的 libc.so 的基址,不就可以得到 mmap 函数在远程进程中的地址咯

void* getRemoteLibFunc(pid_t pid,const char* libname,void* LocalFuncAddr){
    void *LocalLibBase,*RemoteLibBase,*RemoteFuncAddr;
    LocalLibBase = findModuleByName(-1,libname);//-1 表示在当前 hook-server 的 maps 中寻找库的基址
    RemoteLibBase = findModuleByName(pid,libname);
    RemoteFuncAddr = LocalFuncAddr-LocalLibBase+RemoteLibBase;
    printf("LocalLibBase: 0x%08lx, LocalFuncAddr: 0x%08lx\nRemoteLibBase: 0x%08lx, func offset: 0x%08lx\n",LocalLibBase,LocalFuncAddr,RemoteLibBase,LocalFuncAddr-LocalLibBase);
    return RemoteFuncAddr;
}
void inject(pid_t pid){
    ...
    // 通过 mmap 为跳板分配内存
    void* RemoteMmapAddr = getRemoteLibFunc(pid,"libc.so",(void*)mmap);
    printf("[libc] find remote mmap addr at 0x%08lx\n",(long)RemoteMmapAddr);
    ...
        
}

随后再去设置一下寄存器和 pc 的值,mmap 就能被我们成功的调用了,ptrace 主动调用的细节可以看我写的注释↓

#define CPSR_T_MASK (1u<<5)
#define ARM_lr regs[30]
void ptraceCall(pid_t pid,void* funcaddr,int argc,long* argv,struct user_pt_regs *regs){
    // 比八个参数多的话,多出的参数通过栈去传参
    if(argc>8){
        regs->sp =regs->sp - (argc-8)*(sizeof(long));// 申请 8 个寄存器的栈空间
        ptraceWriteData(pid,(void*)regs->sp,(char*)&argv[8],sizeof(long)*(argc-8));
    }
    // 少于 8 个参数,就通过 x0~x7 寄存器去传参
    for(size_t i=0;i<8;i++){
        regs->regs[i] = argv[i];
    }
    regs->pc = (__u64) funcaddr;// 将 pc 寄存器的值修改为函数地址,这样我们就可以跳转到函数的目标地址去执行 arm64 指令了
    printf("[ptraceCall] funcaddr: 0x%08lx\n",regs->pc);
    if(regs->pc&1){
        //thumb 模式
        // 当 pc 的最后一位为 1, 即 pc 为奇数时,设置 pstate CPSR 的 T 标志位为 1, 表示接下来的指令以 thumb 模式执行
        regs->pc&=~1;
        //pstate, 这是 arm64v8a 的叫法,在 armv7a 中叫 CPSR 寄存器
        //more-> https://blog.csdn.net/longwang155069/article/details/105204547
        regs->pstate|=CPSR_T_MASK;
    }else{
        //arm 模式
        // 当 pc 的最后一位为 0, 即 pc 为偶数时,清除 pstate CPSR 的 T 标志位,表示接下来的指令以 arm 模式执行
        regs->pstate&=~CPSR_T_MASK;
    }
    regs->ARM_lr = 0;// 设置 lr 寄存器为 0, 要是函数执行完毕之后返回 0 地址会抛出异常,这个异常在后面是有用的
    ptraceSetRegs(pid,regs);// 设置寄存器的值,把函数的参数和地址传进寄存器里面
    int stat = 0;
    /**
     * 对于使用 ptrace_cont 重新运行的进程,它会在 3 种情况下进入暂停状态
     * 1. 下一次系统调用
     * 2. 子进程退出
     * 3. 子进程的执行发生错误
     * 这里的 0xb7f 我们可以拆分成两部分来看,后 2 字节 0x7f, 表示进程进入了暂停的状态
     * (如果后两字节是 0x00 则表示子进程退出状态), 而前两字节 0xb, 表示进程发送的错误信号为 11 (SIGSEGV),
     * 即内存访问异常,因为我们之前将 lr 寄存器的值设为了 0, 所以当远程函数调用完毕之后会抛出异常,
     * 当 ptrace 收到这个异常信号时,就知道远程函数调用以及完成了~
     */
    while(stat!=0xb7f){
        ptraceContinue(pid);// 让被 ptrace 的线程开始运行
        waitpid(pid,&stat,WUNTRACED);
        printf("[ptraceCall] stat: 0x%04x\n",stat);
    }
    ptraceGetRegs(pid,regs);// 当远程函数调用完成之后,读取寄存器获取返回值
}
void inject(pid_t pid){
    ...
    // 调用 mmap 函数
    long paras[6];
    paras[0]= 0;
    paras[1]=0x1000;
    paras[2]=PROT_READ|PROT_WRITE|PROT_EXEC;
    paras[3]=MAP_ANONYMOUS|MAP_PRIVATE;
    paras[4]=0;
    paras[5]=0;
    ptraceCall(pid,RemoteMmapAddr,6,paras,&regs);
    void *RemoteMemAddr = (void *)regs.regs[0];//mmap 返回值存储在 x0 中,从 x0 获取远程的 mmap 函数分配的内存
    printf("[libc] mmap alloc memory at 0x%08lx\n",(long)RemoteMemAddr);
    ...
}

image-20240820155113179

# 加载 hook 功能库 so

现在我们已经通过 mmap 成功的在被 hook 进程中分配了一块内存,接下来要做的是在被 hook 进程中加载我们的 hook 功能库 so, 这个 so 是跳板最终要跳往的地方,用来执行打印或修改寄存器的操作,我们现在就把这个 so 的名称叫做 libhook-agent.so 吧,当前代码如下,通过远程调用 work_func 来观察我们的 so 是否被成功的加载进来

//hook-agent.c
#include "stdio.h"
#include <android/log.h>
#define LOG_TAG "hook-agent"
#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG  , LOG_TAG, __VA_ARGS__))
int work_func(){
    LOGD("call work_func ok!");
    return 777;
}

首先我们先获取一下 dlopen 在远程进程中的地址,随后将要加载的 so 的绝对地址写入 mmap 分配的内存中,再去调用 ptrace 调用一下 dlopen 函数就可以啦

// 获取 dlopen 在远程进程中的地址
void* RemoteDlopenAddr = getRemoteLibFunc(pid,"libdl.so",(void*)dlopen);
printf("[libdl] find remote dlopen addr at 0x%08lx\n",(long)RemoteDlopenAddr);
/**
  * 将要加载的 so 的绝对地址写入 mmap 分配的内存中,可以把 so 放在 app 的私有目录下面,
  * 要是放在 /data/local/tmp 目录下面,会遇到 avc denied, 
  * 这个时候需要使用 setenforce 0 临时禁用掉 selinux 才可以
*/
ptraceWriteData(pid,RemoteMemAddr,"/data/data/com.oacia.apk_protect/libhook-agent.so",strlen("/data/data/com.oacia.apk_protect/libhook-agent.so")+1);
// 调用 dlopen 函数
paras[0] = (long) RemoteMemAddr;
paras[1]=RTLD_NOW|RTLD_GLOBAL;
ptraceCall(pid,RemoteDlopenAddr,2,paras,&regs);
void *HookAgentAddr = (void *)regs.regs[0];//dlopen 返回值存储在 x0 中,从 x0 获取远程的 dlopen 返回的 handle
printf("[libdl] dlopen libhook-agent.so addr: 0x%08lx\n\n",(long)HookAgentAddr);

HookAgentAddr 的值不为空,说明 libhook-agent.so 被成功的加载了进来

image-20240826150146133

随后我们再使用远程进程的 dlsym , 找到 libhook-agent.so 功能函数的地址,并主动调用 work_func 来看一下情况

//dlsym 获取调用函数的地址
void* RemoteDlsymAddr = getRemoteLibFunc(pid,"libdl.so",(void*)dlsym);
printf("[libdl] find remote dlsym addr at 0x%08lx\n",(long)RemoteDlsymAddr);
// 将被调用函数的函数名写入 mmap 分配的内存中
ptraceWriteData(pid,RemoteMemAddr,"work_func",strlen("work_func")+1);
paras[0] = (long) HookAgentAddr;
paras[1]= (long) RemoteMemAddr;
ptraceCall(pid,RemoteDlsymAddr,2,paras,&regs);
void* remoteFuncAddr = (void *)regs.regs[0];
printf("[libdl] dlsym find hook-agent function addr at 0x%08lx\n",(long)remoteFuncAddr);
ptraceCall(pid,remoteFuncAddr,0,paras,&regs);// 主动调用 hook-agent 中的 work_func
int checkOK = (int)regs.regs[0];
printf("call wrok_func in libhook-agent.so, ret -> %d\n",checkOK);

nice! 函数被成功的调用了,并且返回值也是和 work_func 中的返回值一模一样

image-20240826153126695

而此时的 logcat, 也有日志被正确的打印出来

image-20240826153223819

# 在 hook 点 patch 上跳板

现在我们需要在 hook 点 patch 跳板,目前来看最初使用的跨进程通过 ptrace 的 PTRACE_POKETEXT 在进程中写入跳板的方式,似乎是存在 bug 的,写入字节的次数的过多的话被 hook 进程就会崩溃,并且跨进程通过 ptrace 调用 mprotect 将 hook 处的页属性改为 rwx 没有生效,写入时进程就会崩溃

通过ptrace的`PTRACE_POKETEXT`进行patch

接下来我们使用 X16 寄存器,在 hook 点 patch 上一级跳板,这个一级跳板将会跳往二级跳板

void write_trampoline_stage1(pid_t pid,void* target,char* libname,int offset,char* save_code){
    void* RemoteLibBase = findModuleByName(pid,libname);
    long hook_addr = (long)RemoteLibBase+offset;
    // 生成跳板函数
    unsigned char trampoline[16] = {
        0x50,0x00,0x00,0x58,//LDR X16,#0x8
        0x00,0x02,0x1f,0xD6,//BR X16
    };
    for(int i=0;i<8;i++){
        trampoline[i+8] = *((char*)target+i);
    }
    // 读取即将被覆盖的指令
    ptraceReadData(pid, (void *) hook_addr, save_code, 0x10);
    // 写入跳板
    ptraceWriteData(pid, (void *) hook_addr, (char*)trampoline, 16,0);
}
// 写入一级跳板汇编
char save_code[16];// 保存被跳板覆盖的指令,在完成 hook 之后这 4 行汇编需要被执行
memset(save_code,0,16);
write_trampoline_stage1(pid,RemoteMemAddr,"liboacia.so",0x10B0,save_code);

在二级跳板这里,我们直接将保存寄存器环境,跳往功能函数,恢复寄存器环境,执行被跳板覆盖的汇编的这四个过程直接合在一起

void write_trampoline_stage2(pid_t pid,void* patch_addr,char* libname,int offset,const char* save_code,void* hook_agent_func_addr){
    void* RemoteLibBase = findModuleByName(pid,libname);
    long hook_addr = (long)RemoteLibBase+offset;
    long ret_addr = hook_addr+16;
    // 生成跳板函数,trampoline bytes are from frida hook decompile
    char trampoline[360] = {
            // 保存寄存器环境
            0xff,0x43,0x00,0xd1,
            0xfe,0x7f,0xbf,0xad,
            0xfc,0x77,0xbf,0xad,
            0xfa,0x6f,0xbf,0xad,
            0xf8,0x67,0xbf,0xad,
            0xf6,0x5f,0xbf,0xad,
            0xf4,0x57,0xbf,0xad,
            0xf2,0x4f,0xbf,0xad,
            0xf0,0x47,0xbf,0xad,
            0xee,0x3f,0xbf,0xad,
            0xec,0x37,0xbf,0xad,
            0xea,0x2f,0xbf,0xad,
            0xe8,0x27,0xbf,0xad,
            0xe6,0x1f,0xbf,0xad,
            0xe4,0x17,0xbf,0xad,
            0xe2,0x0f,0xbf,0xad,
            0xe0,0x07,0xbf,0xad,
            0xfd,0x7b,0xbf,0xa9,
            0xfb,0x73,0xbf,0xa9,
            0xf9,0x6b,0xbf,0xa9,
            0xf7,0x63,0xbf,0xa9,
            0xf5,0x5b,0xbf,0xa9,
            0xf3,0x53,0xbf,0xa9,
            0xf1,0x4b,0xbf,0xa9,
            0xef,0x43,0xbf,0xa9,
            0xed,0x3b,0xbf,0xa9,
            0xeb,0x33,0xbf,0xa9,
            0xe9,0x2b,0xbf,0xa9,
            0xe7,0x23,0xbf,0xa9,
            0xe5,0x1b,0xbf,0xa9,
            0xe3,0x13,0xbf,0xa9,
            0xe1,0x0b,0xbf,0xa9,
            0x01,0x42,0x3b,0xd5,
            0xe1,0x03,0xbf,0xa9,
            0xe0,0x43,0x0c,0x91,
            0xff,0x03,0xbf,0xa9,
            0xfe,0x8f,0x01,0xf9,
            0xfd,0x8b,0x01,0xf9,
            0xfd,0x43,0x0c,0x91,
            0xe1,0x03,0x00,0x91,
            0xe2,0x23,0x04,0x91,
            0xe3,0x43,0x0c,0x91,
            // 跳转到核心代码
            0xe0,0x03,0x11,0xaa,
            0x64,0x05,0x00,0x58,
            0x80,0x00,0x3f,0xd6,
            // 还原寄存器环境
            0xff,0x43,0x00,0x91,
            0xe1,0x03,0xc1,0xa8,
            0x01,0x42,0x1b,0xd5,
            0xe1,0x0b,0xc1,0xa8,
            0xe3,0x13,0xc1,0xa8,
            0xe5,0x1b,0xc1,0xa8,
            0xe7,0x23,0xc1,0xa8,
            0xe9,0x2b,0xc1,0xa8,
            0xeb,0x33,0xc1,0xa8,
            0xed,0x3b,0xc1,0xa8,
            0xef,0x43,0xc1,0xa8,
            0xf1,0x4b,0xc1,0xa8,
            0xf3,0x53,0xc1,0xa8,
            0xf5,0x5b,0xc1,0xa8,
            0xf7,0x63,0xc1,0xa8,
            0xf9,0x6b,0xc1,0xa8,
            0xfb,0x73,0xc1,0xa8,
            0xfd,0x7b,0xc1,0xa8,
            0xe0,0x07,0xc1,0xac,
            0xe2,0x0f,0xc1,0xac,
            0xe4,0x17,0xc1,0xac,
            0xe6,0x1f,0xc1,0xac,
            0xe8,0x27,0xc1,0xac,
            0xea,0x2f,0xc1,0xac,
            0xec,0x37,0xc1,0xac,
            0xee,0x3f,0xc1,0xac,
            0xf0,0x47,0xc1,0xac,
            0xf2,0x4f,0xc1,0xac,
            0xf4,0x57,0xc1,0xac,
            0xf6,0x5f,0xc1,0xac,
            0xf8,0x67,0xc1,0xac,
            0xfa,0x6f,0xc1,0xac,
            0xfc,0x77,0xc1,0xac,
            0xfe,0x7f,0xc1,0xac,
            0xf0,0x47,0xc1,0xa8,
            // 执行先前因 patch 跳板被覆盖的代码,16 字节占位
            0xaa,0xaa,0xaa,0xaa,
            0xaa,0xaa,0xaa,0xaa,
            0xaa,0xaa,0xaa,0xaa,
            0xaa,0xaa,0xaa,0xaa,
            // 返回 hook 点之后的位置继续执行逻辑
            0x50,0x00,0x00,0x58,
            0x00,0x02,0x1f,0xD6,
            //8 字节占位,存储功能函数的地址
            0xbb,0xbb,0xbb,0xbb,
            0xbb,0xbb,0xbb,0xbb,
            //8 字节占位,存储功能 hook 完成后返回到原来程序的地址
            0xcc,0xcc,0xcc,0xcc,
            0xcc,0xcc,0xcc,0xcc,
    };
    for(int i=0;i<16;i++){
        trampoline[i+320] = save_code[i];
    }
    for(int i=0;i<8;i++){
        trampoline[i+344] = *((char*)hook_agent_func_addr+i);
    }
    for(int i=0;i<8;i++){
        trampoline[i+352] = *((char*)ret_addr+i);
    }
    
    ptraceWriteData(pid, (void *) patch_addr, trampoline, 360,0);
}

所以可以想想换一种方法,既然 so 可以注入进去,那就直接在 so 加载的时候,通过 __attribute__((constructor)) , 在通过这个 so 在被 hook 进程内部执行 patch 跳板的逻辑不就可以了

/**
 * 完成一次完整的 hook, 需要先由 hook 点的一级跳板跳转到二级跳板的位置,
 * 一级跳板通过 BR X16 跳往二级跳板,二级跳板保存寄存器的环境,跳转到 hook 的
 * 功能函数位置,随后还原寄存器环境,并执行先前被覆盖的四条汇编
 * , 随后通过 BR X16 寄存器,跳往 hook 点之后的位置,完成一次完整的 hook
 *
 */
static __attribute__((constructor)) void ctor()
{
    u_long hook_addr = (u_long)findModuleByName(-1,"liboacia.so")+0x10B8;
    LOGD("hook-agent init!");
    extern u_long _trampoline_,_shellcode_addr_,_shellcode_start_,_shellcode_end_,_origin_patched_code_,_hook_main_func_addr_,_hook_finish_return_addr_;
    u_long total_len = (u_long)&_shellcode_end_ - (u_long)&_shellcode_start_;
    LOGD("shellcode len: %lu, hook_addr: 0x%08lx,offset: 0x%04x",total_len,hook_addr,0x10B8);
    // 为 shellcode 分配内存
    u_long page_size = getpagesize();
    u_long shellcode_mem_start = (u_long)mmap(0, page_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
    memset((void *)shellcode_mem_start, 0, page_size);
    memcpy((void *)shellcode_mem_start, (void *)&_shellcode_start_, total_len);
    LOGD("_shellcode_addr_: 0x%08lx,shellcode_mem_start: 0x%08lx",*(u_long*)&_shellcode_addr_, shellcode_mem_start);
    // 尝试了一下好像没有办法给_shellcode_addr_赋值 (很奇怪)
    // 所以索性直接这样赋值了 *(u_long*)(hook_addr + 8) = shellcode_mem_start;
    //*(u_long*)&_shellcode_addr_ = (u_long)shellcode_mem_start;
    // 通过相对偏移的方式,定位到需要替换的地址在 mmap 分配的内存中的地址
    u_long mem_hook_main_func_addr_ = (u_long)&_hook_main_func_addr_ - (u_long)&_shellcode_start_ + shellcode_mem_start;
    u_long mem_origin_patched_code_ = (u_long)&_origin_patched_code_ - (u_long)&_shellcode_start_ + shellcode_mem_start;
    u_long mem_hook_finish_return_addr_ = (u_long)&_hook_finish_return_addr_ - (u_long)&_shellcode_start_ + shellcode_mem_start;
    //hook 的功能函数的地址
    *(u_long*)mem_hook_main_func_addr_ = (u_long)hook_main;
    // 被跳板覆盖前,hook 点的 16 字节
    *(u_long*)mem_origin_patched_code_ = *(u_long*)hook_addr;
    *(u_long*)(mem_origin_patched_code_ + 8) = *(u_long*)(hook_addr + 8);
    //hook 执行完毕后的返回地址
    *(u_long*)mem_hook_finish_return_addr_ = (u_long)hook_addr + 0x10;
    //patch 上我们的跳板
    u_long entry_page_start = (u_long)(hook_addr) & (~(page_size-1));
    mprotect((u_long*)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC);
    *(u_long*)hook_addr = *(u_long*)&_trampoline_;
    *(u_long*)(hook_addr + 8) = shellcode_mem_start;
}

相关的 shellcode 如下,在保存寄存器环境时,CPSR 寄存器也要一起保存,但是看 frida hook 之后的样子,只需要保存 NZCV 寄存器就足够了,所以这里也不对 CPSR 寄存器的其他字段作保存了

#在hook点patch上跳板,跳转到我们的shellcode
.global _trampoline_
#shellcode所在的地址,跳板需要跳转到shellcode
.global _shellcode_addr_
#shellcode的起始地址和结束地址,用来定位shellcode的大小以及字节
.global _shellcode_start_
.global _shellcode_end_
#被跳板覆盖的16字节指令,这些指令我们需要在hook完成之后再次执行,才不会影响到原本的逻辑
.global _origin_patched_code_
#跳转到hook的核心功能函数的地址去执行读取寄存器/修改寄存器等操作
.global _hook_main_func_addr_
#当所有的hook逻辑执行完毕之后,需要返回到hook点后16字节的位置去执行后续的指令
.global _hook_finish_return_addr_
_trampoline_:
    LDR X16, SHELLCODE_ADDR
    BR x16
#这里需要先声明SHELLCODE_ADDR的地址,否则会出现ld: error: relocation R_AARCH64_LD_PREL_LO19 cannot be used against symbol '_shellcode_addr_'
#就是说不能用已经声明过全局的符号来作重定位
SHELLCODE_ADDR:
_shellcode_addr_:
    .dword 0x1234567812345678
_shellcode_start_:
    #本来想参考https://github.com/zzyccs/inlineHook/blob/master/app/src/main/cpp/inline_shellcode.S
    #手动通过STP寄存器的大小一个一个算出存入堆栈的地址的
    #但是看了一下frida,直接用STP Xt1, Xt2, [Xn|SP, #imm]! ; 64-bit
    #这种预索引的方式修改SP的值,这样就不需要size*0,size*1...的方式去计算寄存器存储的位置
    #可以达到持续入栈的效果,太厉害啦
    STP             Q30, Q31, [SP,#-0x20]!
    STP             Q28, Q29, [SP,#-0x20]!
    STP             Q26, Q27, [SP,#-0x20]!
    STP             Q24, Q25, [SP,#-0x20]!
    STP             Q22, Q23, [SP,#-0x20]!
    STP             Q20, Q21, [SP,#-0x20]!
    STP             Q18, Q19, [SP,#-0x20]!
    STP             Q16, Q17, [SP,#-0x20]!
    STP             Q14, Q15, [SP,#-0x20]!
    STP             Q12, Q13, [SP,#-0x20]!
    STP             Q10, Q11, [SP,#-0x20]!
    STP             Q8, Q9, [SP,#-0x20]!
    STP             Q6, Q7, [SP,#-0x20]!
    STP             Q4, Q5, [SP,#-0x20]!
    STP             Q2, Q3, [SP,#-0x20]!
    STP             Q0, Q1, [SP,#-0x20]!
    STP             X30, X31, [SP,#-0x10]!
    STP             X28, X29, [SP,#-0x10]!
    STP             X26, X27, [SP,#-0x10]!
    STP             X24, X25, [SP,#-0x10]!
    STP             X22, X23, [SP,#-0x10]!
    STP             X20, X21, [SP,#-0x10]!
    STP             X18, X19, [SP,#-0x10]!
    STP             X16, X17, [SP,#-0x10]!
    STP             X14, X15, [SP,#-0x10]!
    STP             X12, X13, [SP,#-0x10]!
    STP             X10, X11, [SP,#-0x10]!
    STP             X8, X9, [SP,#-0x10]!
    STP             X6, X7, [SP,#-0x10]!
    STP             X4, X5, [SP,#-0x10]!
    STP             X2, X3, [SP,#-0x10]!
    STP             X0, X1, [SP,#-0x10]!
    #特别注意,CPSR寄存器也要保存,不然只能打印一次值然后就崩溃了-m-
    #aarch64不能和aarch32一样,直接访问CPSR得到所有的值,所以得分开来访问
    #看frida的hook之后的样子,看上去只需要保存NZCV寄存器就足够了
    MRS             X1, NZCV
    #MRS             X0, DAIF
    STP             X0, X1,[SP,#-0x10]!
    MOV             X0, SP
    LDR             X16, HOOK_MAIN_FUNC_ADDR
    BLR             X16
    #恢复CPSR寄存器
    LDP             X0, X1, [SP],#0x10
    MSR             NZCV, X1
    #MSR             DAIF, X0
    #恢复X0-X31,Q0-Q31寄存器
    LDP             X0, X1, [SP],#0x10
    LDP             X2, X3, [SP],#0x10
    LDP             X4, X5, [SP],#0x10
    LDP             X6, X7, [SP],#0x10
    LDP             X8, X9, [SP],#0x10
    LDP             X10, X11, [SP],#0x10
    LDP             X12, X13, [SP],#0x10
    LDP             X14, X15, [SP],#0x10
    LDP             X16, X17, [SP],#0x10
    LDP             X18, X19, [SP],#0x10
    LDP             X20, X21, [SP],#0x10
    LDP             X22, X23, [SP],#0x10
    LDP             X24, X25, [SP],#0x10
    LDP             X26, X27, [SP],#0x10
    LDP             X28, X29, [SP],#0x10
    LDP             X30, X31, [SP],#0x10
    LDP             Q0, Q1, [SP],#0x20
    LDP             Q2, Q3, [SP],#0x20
    LDP             Q4, Q5, [SP],#0x20
    LDP             Q6, Q7, [SP],#0x20
    LDP             Q8, Q9, [SP],#0x20
    LDP             Q10, Q11, [SP],#0x20
    LDP             Q12, Q13, [SP],#0x20
    LDP             Q14, Q15, [SP],#0x20
    LDP             Q16, Q17, [SP],#0x20
    LDP             Q18, Q19, [SP],#0x20
    LDP             Q20, Q21, [SP],#0x20
    LDP             Q22, Q23, [SP],#0x20
    LDP             Q24, Q25, [SP],#0x20
    LDP             Q26, Q27, [SP],#0x20
    LDP             Q28, Q29, [SP],#0x20
    LDP             Q30, Q31, [SP],#0x20
_origin_patched_code_:
    .dword 0x1234567812345678
    .dword 0x1234567812345678
    LDR             X16, HOOK_FINISH_RETURN_ADDR
    BR              X16
HOOK_MAIN_FUNC_ADDR:
_hook_main_func_addr_:
    .dword 0x1234567812345678
HOOK_FINISH_RETURN_ADDR:
_hook_finish_return_addr_:
    .dword 0x1234567812345678
_shellcode_end_:

# 执行 hook 逻辑

这里我们的 hook 逻辑很简单,就是打印调用函数 strlen 之后 X0 的值

void hook_main(u_long sp){
    LOGD("enter hook main!");
    //sp -- sp+0x10 存储的是 NZCV 寄存器,从 sp+0x10 开始才是 x0-x31 的位置
    u_long strlen_ret_value = *(u_long*)(sp+0x10);
    LOGD("hook done! target function strlen return value is 0x%08lx",strlen_ret_value);
}

终于现在我们也可以自己完成一次简单的 hook 了!

image-20240829154247205

完整代码已经传到 github 上面啦 https://github.com/oacia/inlinehook_demo

# 参考资料

  • Mac 中:用 lldb 调试安卓 app 进程
  • 【工具使用】Android 调试利器之 LLDB
  • Android LLDB debugging
  • 使用 GDB、LLDB 调试安卓程序
  • [原创] Frida inlineHook 原理分析及简单设计一款 AArch64 inlineHook 工具
  • Android 下实现 ptrace 注入 & hook
  • 使用 ptrace 向已运行进程中注入.so 并执行相关函数
  • Android 注入要点记录
更新于 阅读次数