1.4 Ptrace
前置知识:
- Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3, ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之,以此类推。
- 拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。
- 应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。
- Linux的hook一般发生在ring0和ring3层,其中ring0层通常是hook Linux系统调用表中的系统调用,而ring3层则一般hook的动态链接库中的函数。
1.4.1 理解
ptrace
是一种系统调用,也就是进程追踪(process trace);
用于对进程的执行进行干涉以及寄存器状态(值)的读取以及设置,内存的写入与读取;
我们常用的Linux下的gdb主要功能实现就是通过ptrace系统调用的:
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,void *addr, void *data);
- request : 请求ptrace执行的操作
- pid: 目标进程的ID
- addr: 目标进程的地址
- data: 不同操作下写入或读取的操作数据
1.4.2 示例
模拟gdb的break和continue操作
思路:
- 将需要下断点的地址的代码读出并保存;
- 将需要下断点的地址的代码修改为
0xcc
(int 3); - 将需要下断点的地址的代码恢复.
被attach的程序:re.c
#include <stdio.h>
#include <time.h>
#include <unistd.h>
void show() {
printf("Hello, ptrace! [pid:%d]\n", getpid());
}
int main() {
while(1) {
show();
sleep(2);
}
return 0;
}
编译:
gcc re.c -o re
Ptarce程序:hook.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h> /* For constants ORIG_EAX etc */
#include <sys/user.h>
void new_show() {
printf("Hooked by cc-sir!\n");
}
int main(int argc, char *argv[]) {
if(argc!=2) {
printf("Usage: %s pid\n", argv[0]);
return 0;
}
printf("new_show_addr: %p\n",new_show);
struct user_regs_struct reg;
pid_t pid = atoi(argv[1]);
ptrace(PTRACE_ATTACH, pid,NULL,NULL);
wait(NULL);
ptrace(PTRACE_GETREGS,pid,NULL,®);
printf("rip: 0x%lx\n",reg.rip);
long addr = reg.rip;
long show_addr = 0x400586;
long code = 0xcc80cd;
long back_code;
int id;
back_code = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); //保留源码
printf("back_code: %llx\n",back_code);
if(ptrace(PTRACE_POKETEXT, pid, addr, code) < 0){ //修改源码
perror("PTRACE_POKETEXT");
return 0;
}
ptrace(PTRACE_CONT, pid, NULL, NULL);
wait(NULL);
printf("The process has int 0x3!\n");
getchar();
if(ptrace(PTRACE_POKETEXT, pid, addr, back_code) < 0) { //还原代码
perror("PTRACE_POKETEXT");
return 0;
}
ptrace(PTRACE_SETREGS, pid, NULL, ®); //还原寄存器
ptrace(PTRACE_CONT, pid, NULL, NULL);
printf("The process has continue run!\n");
ptrace(PTRACE_DETACH, pid, NULL, NULL);
return 0;
}
gcc hook.c -o hook
**结果:**https://img-blog.csdnimg.cn/20200428195902777.gif
1.4.3 Ptrace总结
因此,在Linux下,除了使用LD_PRELOAD这种被动Glibc API注入方式,还可以使用基于调试器(Debuger)思想的ptrace()主动注入方式,总体思路如下:
- 使用Linux Module、或者LSM挂载点对进程的启动动作进行实时的监控,并通过Ring0-Ring3通信,通知到Ring3程序有新进程启动的动作
- 用ptrace函数attach上目标进程
- 让目标进程的执行流程跳转到mmap函数来分配一小段内存空间
- 把一段机器码拷贝到目标进程中刚分配的内存中去
- 最后让目标进程的执行流程跳转到注入的代码执行
ptrace的使用:
- 用PTRACE_ATTACH或者PTRACE_TRACEME 建立进程间的跟踪关系。
- PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSR等读取子进程内存/寄存器中保留的值。
- PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSR等把值写入到被跟踪进程的内存/寄存器中。
- 用PTRACE_CONT,PTRACE_SYSCALL, PTRACE_SINGLESTEP控制被跟踪进程以何种方式继续运行。
- PTRACE_DETACH, PTRACE_KILL 脱离进程间的跟踪关系。
1.4.4 参考文章
https://www.cnblogs.com/tangr206/articles/3094358.html
http://t.csdnimg.cn/Zx5eS
http://t.csdnimg.cn/Gul3s
ptrace 函数深入分析:https://www.cnblogs.com/heixiang/p/10988992.html
Linux 源码分析之 Ptrace:https://blog.csdn.net/u012417380/article/details/60468697
1.5 Kernel Inline Hook
1.5.1 理解
传统的kernel inline hook技术就是修改内核函数的opcode,通过写入jmp或push ret等指令跳转到新的内核函数中,从何达到劫持的目的。
对于这类劫持攻击,目前常见的做法是fireeye的"函数返回地址污点检测",通过对原有指令返回位置的汇编代码作污点标记,通过查找jmp,push ret等指令来进行防御。
我们知道实现一个系统调用的函数中一定会递归的嵌套有很多的子函数,即它必定要调用它的下层函数。
而从汇编的角度来说,对一个子函数的调用是采用"段内相对短跳转 jmp offset"来实现的,即CPU根据offset来进行一个偏移量的跳转。
如果我们把下层函数在上层函数中的offset替换成我们"Hook函数"的offset,这样上层函数调用下层函数时,就会跳到我们的"Hook函数"中,我们就可以在"Hook函数"中做过滤和劫持内容的工作。
1.5.2 示例
以sys_read作为例子\linux-2.6.32.63\fs\read_write.c
asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
file = fget_light(fd, &fput_needed);
if (file)
{
loff_t pos = file_pos_read(file);
ret = vfs_read(file, buf, count, &pos);
file_pos_write(file, pos);
fput_light(file, fput_needed);
}
return ret;
}
EXPORT_SYMBOL_GPL(sys_read);
在sys_read()中,调用了子函数vfs_read()来完成读取数据的操作,
在sys_read()中调用子函数vfs_read()的汇编命令是:
call 0xc106d75c <vfs_read>
等同于:jmp offset(相对于sys_read()的基址偏移)
所以,我们的思路很明确,找到call 0xc106d75c <vfs_read>这条汇编,把其中的offset改成我们的Hook函数对应的offset,就可以实现劫持目的了。
- 搜索sys_read的opcode
- 如果发现是call指令,根据call后面的offset计算要跳转的地址是不是我们要hook的函数地址
- 如果"不是"就重新计算Hook函数的offset,用Hook函数的offset替换原来的offset
- 如果"已经是"Hook函数的offset,则说明函数已经处于被劫持状态了,我们的Hook引擎应该直接忽略跳过,避免重复劫持
/*
参数:
1. handler是上层函数的地址,这里就是sys_read的地址
2. old_func是要替换的函数地址,这里就是vfs_read
3. new_func是新函数的地址,这里就是new_vfs_read的地址
*/
unsigned int patch_kernel_func(unsigned int handler, unsigned int old_func,
unsigned int new_func)
{
unsigned char *p = (unsigned char *)handler;
unsigned char buf[4] = "\x00\x00\x00\x00";
unsigned int offset = 0;
unsigned int orig = 0;
int i = 0;
DbgPrint("\n*** hook engine: start patch func at: 0x%08x\n", old_func);
while (1) {
if (i > 512)
return 0;
if (p[0] == 0xe8) {
DbgPrint("*** hook engine: found opcode 0x%02x\n", p[0]);
DbgPrint("*** hook engine: call addr: 0x%08x\n",
(unsigned int)p);
buf[0] = p[1];
buf[1] = p[2];
buf[2] = p[3];
buf[3] = p[4];
DbgPrint("*** hook engine: 0x%02x 0x%02x 0x%02x 0x%02x\n",
p[1], p[2], p[3], p[4]);
offset = *(unsigned int *)buf;
DbgPrint("*** hook engine: offset: 0x%08x\n", offset);
orig = offset + (unsigned int)p + 5;
DbgPrint("*** hook engine: original func: 0x%08x\n", orig);
if (orig == old_func) {
DbgPrint("*** hook engine: found old func at"
" 0x%08x\n",
old_func);
DbgPrint("%d\n", i);
break;
}
}
p++;
i++;
}
offset = new_func - (unsigned int)p - 5;
DbgPrint("*** hook engine: new func offset: 0x%08x\n", offset);
p[1] = (offset & 0x000000ff);
p[2] = (offset & 0x0000ff00) >> 8;
p[3] = (offset & 0x00ff0000) >> 16;
p[4] = (offset & 0xff000000) >> 24;
DbgPrint("*** hook engine: pachted new func offset.\n");
return orig;
}