# 前言
最近需要对一个固件的 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 的代码如图所示
注入完成之后,我们使用 lldb 去调试看看
# lldb 动态调试
先到 ndk\26.1.10909125\toolchains\llvm\prebuilt\windows-x86_64\lib\clang\17\lib\linux\aarch64
把 lldb-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 |
然后连接安卓中的 lldb-server
platform connect unix-abstract-connect:///data/local/tmp/debug.sock |
使用 platform status
查看是否已经连接
之后 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 |
这段汇编给 x16 寄存器赋值为相对偏移 #0x8 位置的值,然后再去进行 BR 跳转,这后两行的指令 adrp x0, -895871
和 udf #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 字节是两行汇编,用来实现跳板的跳转,后八字节存储了跳板要跳往的地址
这里使用的是 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 则保存了三级跳版的位置,而在二级跳板的下方,竟然还有一个跳板,不过现在还没有看到它的用途所在
这里二级跳板有啥用?为啥不直接跳到三级跳板?
# 三级跳板
这三级跳板总共干了三件事情
# 保存寄存器环境
# 跳转到核心代码
经过三个跳板的层层跳转,我们也是终于来到了 frida_agent_64.so
中执行我们编写的 hook 逻辑比如打印寄存器,读取内存等操作了
# 恢复寄存器环境
使用 LDP
加载栈中保存的寄存器环境
# 恢复执行流
由于 hook 所生成的跳板函数会对 hook 位置的 16 字节汇编进行覆盖,所以必须要对被覆盖的那 16 字节汇编再次执行,否则将会对执行流产生影响。在三级跳板的最后 RET X16
, 将会来到跳转到此处执行先前未完成的汇编
# 自实现 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); | |
} |
一旦我们使用 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; | |
} |
现在我们尝试去读取一下 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; | |
} | |
} |
nice, 将通过 ptrace 读取的汇编和 ida 中反编译出来的汇编对比一下,简直就是一模一样~
# 为字符串分配内存
接下来我们需要通过 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(®s,&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,®s); | |
void *RemoteMemAddr = (void *)regs.regs[0];//mmap 返回值存储在 x0 中,从 x0 获取远程的 mmap 函数分配的内存 | |
printf("[libc] mmap alloc memory at 0x%08lx\n",(long)RemoteMemAddr); | |
... | |
} |
# 加载 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,®s); | |
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
被成功的加载了进来
随后我们再使用远程进程的 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,®s); | |
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,®s);// 主动调用 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
中的返回值一模一样
而此时的 logcat, 也有日志被正确的打印出来
# 在 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 了!
完整代码已经传到 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 注入要点记录