参考资料
https://github.com/zhengmin1989/TheSevenWeapons/tree/master/LiBieGou
http://man7.org/linux/man-pages/man2/ptrace.2.html
在阅读之前,最好先看一下我的另一篇文章,关于怎么编译ARM程序
以及关于ptrace的基础知识
Playing with Ptrace Android
示例代码target.c
1 |
|
hook程序hook1.c
1 |
|
- PTRACE_SYSCALL
使被调试进程继续运行,但是在下一个系统调用的入口处或出口处停下,或者是执行完一条指令后停下.
例如,调试进程可以监视被调试进程系统调用入口处的参数,接着再使用SYSCALL,监视系统调用的返回值.
每当目标程序调用system call前的时候,就会暂停下载。然后我们就可以读取寄存器的值来获取system call的各项信息。然后我们再一次使用ptrace(PTRACE_SYSCALL, pid, NULL, NULL)这个函数就可以让system call在调用完后再一次暂停下来,并获取system call的返回值。
获取system call编号
1 | long getSysCallNo(int pid, struct pt_regs *regs) |
ARM架构上,所有的系统调用都是通过SWI来实现的。并且在ARM 架构中有两个SWI指令,分别针对EABI和OABI:
[EABI] 机器码:
1110 1111 0000 0000 – SWI 0
具体的调用号存放在寄存器r7中.
[OABI] 机器码:
1101 1111 vvvv vvvv – SWI immed_8
调用号进行转换以后得到指令中的立即数。立即数=调用号 | 0x900000
既然需要兼容两种方式的调用,我们在代码上就要分开处理。首先要获取SWI指令判断是EABI还是OABI,如果是EABI,可从r7中获取调用号。如果是OABI,则从SWI指令中获取立即数,反向计算出调用号。
OABI和EABI的区别
两种ABI在如下方面有区别:
A。调用规则(包括参数如何传递及如何获得返回值)
B。系统调用的数目以及应用程序应该如何去做系统调用
C。目标文件的二进制格式,程序库等
D。结构体中的填充(padding/packing)和对齐。
PTRACE_PEEKTEXT, PTRACE_PEEKDATA
形式:ptrace(PTRACE_PEEKTEXT, pid, addr, data)
ptrace(PTRACE_PEEKDATA, pid, addr, data)
描述:从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据。在Linux(i386)中用户代码段与用户数据段重合所以读取代码段和数据段数据处理是一样的。
hook程序运行逻辑
1 | if(0 != ptrace(PTRACE_ATTACH, pid, NULL, NULL)) |
被调试进程:被debugger attach ——被PTRACE_SYSCALL——->在syscall的入口处停止—被PTRACE_SYSCALL—>在syscall的出口处停止
进行调试的进程:attach要调试的进程——PTRACE_SYSCALL/wait——–>等待被调试进程停止—->读取调用入口处的参数—PTRACE_SYSCALL/wait—>监视系统调用的返回值
hook system call前的函数,和hook system call后的函数
1 | void hookSysCallBefore(pid_t pid) |
PTRACE_GETREGS
形式:ptrace(PTRACE_GETREGS, pid, 0, data)
描述:读取寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有17个基本寄存器的值。
在获取了system call的number以后,我们可以进一步获取个个参数的值.比如说write这个system call。
在arm上,如果形参个数少于或等于4,则形参由R0,R1,R2,R3四个寄存器进行传递。
若形参个数大于4,大于4的部分必须通过堆栈进行传递。
而执行完函数后,函数的返回值会保存在R0这个寄存器里。
我们可以看到第一个SysCallNo是162,也就是sleep函数。第二个SysCallNo是4,也就是write函数,因为printf本质就是调用write这个系统调用来完成的。关于system call number对应的具体system call可以参考我在github上的reference文件夹中的systemcalllist.txt文件,里面有对应的列表。我们的hook1程序还对write的参数做了解析,比如1表示stdout,0xadf020表示字符串的地址,19代表字符串的长度。而返回值19表示write成功写入的长度,也就是字符串的长度。
测试
ps | grep target
得到pid为10797
target
hook
我们可以看到第一个SysCallNo是162,也就是sleep函数。
第二个SysCallNo是4,也就是write函数,因为printf本质就是调用write这个系统调用来完成的。
关于system call number对应的具体system call
可以参考我在github上的reference文件夹中的systemcalllist.txt文件,里面有对应的列表。
我们的hook1程序还对write的参数做了解析,比如1表示stdout,0x1459020表示字符串的地址,19代表字符串的长度。而返回值19表示write成功写入的长度,也就是字符串的长度。
利用Ptrace动态修改内存
仅仅是用ptrace来获取system call的参数和返回值还不能体现出ptrace的强大,下面我们就来演示用ptrace读写内存。我们在hook1.c的基础上继续进行修改,在write被调用之前对要输出string进行翻转操作。
我们在hookSysCallBefore()函数中加入modifyString(pid, regs.ARM_r1, regs.ARM_r2)这个函数:
修改内存,转置字符串
1 | void hookSysCallBefore(pid_t pid) |
getdata和putdata
1 | void getdata(pid_t child, long addr, |
getdata()和putdata()分别使用PTRACE_PEEKDATA和PTRACE_POKEDATA对内存进行读写操作。
因为ptrace的内存操作一次只能控制4个字节,所以如果修改比较长的内容需要进行多次操作。
PTRACE_PEEKTEXT, PTRACE_PEEKDATA
从内存地址中读取四个字节,内存地址由addr给出。PTRACE_POKETEXT, PTRACE_POKEDATA
往内存地址中写入四个字节。内存地址由addr给出。
tip:arm里的word是指四个字节
测试
利用Ptrace动态执行sleep()函数——调用系统so库中的函数
目标函数是libc.so中的sleep函数.
正常情况是每输出一次暂停一秒,现在我们让它暂停10秒
总体思路
获取目标进程sleep函数地址
在目标进程内执行sleep函数
1 | void inject(pid_t pid) |
如何获取函数地址
- 已知条件: 本进程的基址、目标进程的基址、本进程中sleep函数的地址(当然,这些已知条件也是需要获得的。
/proc/pid/maps文件中存储的是进程内存映射详情,我们可以在这个文件中查询进程中so的基址;
sleep函数在本进程中的地址直接可以获得(void*) - 求解: 目标进程中sleep函数地址
- 计算: 本进程sleep地址 - 本进程基址 + 目标进程基址
获取so库的加载基址
因为libc.so在内存中的地址是随机的,所以我们需要先获取目标进程的libc.so的加载地址,再获取自己进程的libc.so的加载地址和sleep()在内存中的地址。然后我们就能计算出sleep()函数在目标进程中的地址了。要注意的是获取目标进程和自己进程的libc.so的加载地址是通过解析/proc/[pid]/maps得到的。
打开/proc/pid/maps文件找到基址.
1 | void* get_module_base(int pid, const char* module_name) |
计算目标进程中sleep函数地址
1 | long get_remote_addr(int target_pid, const char* module_name, void* local_addr) |
如何执行sleep函数
- 设置函数参数,如果参数个数小于等于4,参数按顺序放入R0~R4寄存器中;如果参数个数大于4,多余的部分需要入栈.
- 设置pc寄存器的值,设置当前指令集标志位.
- 应用以上寄存器的修改使之生效.
- 等待函数执行.
1 | //目标进程id,目标函数地址,参数地址,参数个数,寄存器地址 |
完整hook代码
1 |
|
关于waitpid
详细介绍可看官方文档.
参数status
wait函数调用过后,status指针指向可以被宏解析的值,这些宏在ndk目录下platforms/android-21/arch-arm/usr/include/sys/wait.h文件中定义.
高2字节用于表示导致子进程的退出或暂停状态信号值(WTERMSIG),低2字节表示子进程是退出(0x0)还是暂停(0x7f)状态(WEXITSTATUS)。
如:0xb7f就表示子进程为暂停状态,导致它暂停的信号量为11即sigsegv错误。
关于错误代码的文档可看这里,
定义在ndk目录下platforms/android-21/arch-arm/usr/include/asm/signal.h中.
其中两个宏:
WEXITSTATUS(statusPtr):
if the child process terminates normally, this macro evaluates to the lower 8 bits of the value passed to the exit or _exit function or returned from main.
WTERMSIG(statusPtr)
if the child process ends by a signal that was not caught, this macro evaluates to the number of that signal.
参数options
指定了waitpid的额外行动.选项有:
- WNOHANG:
告诉waitpid不等程序中止立即返回status信息.
正常情况是当主进程对子进程使用了waitpid,主进程就会阻塞直到waitpid返回status信息;如果指定了WNOHANG选项,主进程就不会阻塞了.
如果还没有可用的status信息,waitpid返回0. - WUNTRACED:
告诉waitpid,如果子进程进入暂停状态或者已经终止,那么就立即返回status信息,正常情况是子进程终止的时候才返回.
如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。
对于使用ptrace_cont运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。总结
程序中的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV了.
测试
正常的情况是target程序每秒输出一句话,但是用hook3程序hook后,就会暂停10秒钟的时间,因为我们利用ptrace运行了sleep(10)在目标程序中。
利用Ptrace动态加载so并执行自定义函数
总体思路
- 保存当前寄存器的状态
- 获取目标程序的mmap, dlopen, dlsym, dlclose函数地址
- 调用mmap分配空间保存参数信息
- 调用dlopen加载so库
- 调用dlsym找到目标函数地址
- 执行目标函数
- 调用dlclose卸载so库
- 恢复寄存器的状态
保存当前寄存器的状态
1 | struct pt_regs old_regs,regs; |
获取目标程序的mmap, dlopen, dlsym, dlclose函数地址
1 | long mmap_addr,dlopen_addr,dlsym_addr,dlclose_addr; |
调用mmap分配空间保存参数信息
mmap的原型如下:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数 描述
- addr 映射的起始地址,为0表示由系统决定映射的起始地址
- length 映射的长度
- prot 映射的内存保存属性,不能与文件的打开模式冲突
- flags 指定映射对象的类型,映射选项和映射页是否可以共享
- fd 有效的文件描述符,一般是由open()函数返回;其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射
- offset 被映射对象内容的起点
这里我们需要的调用语句是mmap(0,0x4000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE,0,0)
- PROT_EXEC表示可执行.
- PROT_READ表示可读.
- PROT_WRITE表示可写.
- MAP_PRIVATE表示建立一个写入时拷贝的私有映射.内存区域的写入不会影响到原文件.这个标志和以上标志是互斥的,只能使用其中一个.
- MAP_ANONYMOUS表示匿名映射,映射区不与任何文件关联.
mmap()可以用来将一个文件或者其它对象映射进内存,如果我们把flag设置为MAP_ANONYMOUS并且把参数fd设置为0的话就相当于直接映射一段内容为空的内存。
则:
1 | long parameters[10]; |
在我们使用ptrace_call(pid, mmap_addr, parameters, 6, ®s)调用完mmap()函数之后,要记得使用ptrace(PTRACE_GETREGS, pid, NULL, ®s); 用来获取保存返回值的regs.ARM_r0,这个返回值也就是映射的内存的起始地址。
mmap()映射的内存主要用来保存我们传给其他函数的参数。比如接下来我们需要用dlopen()去加载”/data/local/tmp/libinject.so”这个文件,所以我们需要先用putdata()将”/data/local/tmp/libinject.so”这个字符串放置在mmap()所映射的内存中,然后就可以将这个映射的地址作为参数传递给dlopen()了。接下来的dlsym(),so中的目标函数,dlclose()都是相同调用的方式,这里就不一一赘述了。
调用dlsym找到目标函数地址
原型:
void *dlsym(void *handle, const char *symbol);
参数 描述
handle so库的基址
symbol 函数名地址
这里我们需要的调用语句是dlsym(handle, function_name),则:
1 | writeData(pid, mapping_base, function_name, strlen(function_name)+1); |
执行目标函数
1 | writeData(pid, mapping_base, function_parameters, strlen(function_parameters)+1); |
调用dlclose卸载so库
原型:int dlclose(void *handle);
则:
1 | parameters[0] = handle; |
恢复寄存器的状态
ptrace(PTRACE_SETREGS,pid,NULL,&old_regs);
完整代码
1 |
|