前沿
? ? ? ?項目開發最近進行系統hook功能實現相關業務,主要在centos7和8系列環境開發下關功能。調研了相關知識點,發現在系統7和8上內核版本差別比較大,7-3.10.x系列版本,8-4.18.x系列版本。依據兩個系統的內核情況根對應的內核符號表進行數據業務的鉤子功能開發。開發過程發現兩個系統雖然都定義對應的鉤子目標函數定義,但是實際過程發現在7系統上直接定義于目標函數相同的函數進行hook即可,而系統8系列可以直接將參數定義成pt_regs進行處理。那么問題來了,這個結構是個什么?有什么用?如何使用?帶著這么幾個問題來逐個來介紹。
1.struct pt_regs是什么
? ? ? ? pt_regs內核定義一個結構,其文件定義路徑通常為arch/x86/include/asm/ptrace.h,如下結構定義:
(__i386__)
struct pt_regs {unsigned long bx;// bx %ebx 通用寄存器,通常用作基址寄存器(Base Register),保存數據或地址。unsigned long cx;// cx %ecx 通用寄存器,常用作計數器(Counter),如循環計數或字符串操作。unsigned long dx;// dx %edx 通用寄存器,通常與 %eax 配合使用,存放數據或 I/O 端口地址。unsigned long si;// si %esi 源索引寄存器(Source Index),用于內存操作的源地址(如 movs 指令)。unsigned long di;// di %edi 目的索引寄存器(Destination Index),用于內存操作的目的地址。unsigned long bp;// bp %ebp 基址指針寄存器(Base Pointer),指向當前棧幀的基地址,用于函數調用。unsigned long ax;// ax %eax 累加寄存器(Accumulator),用于算術運算和系統調用的返回值。unsigned long ds;// ds %ds 數據段寄存器(Data Segment),指向當前數據段的段選擇子。unsigned long es;// es %es 附加段寄存器(Extra Segment),用于某些內存操作(如字符串操作)。unsigned long fs;// fs %fs 附加段寄存器,Linux 內核中通常用于線程本地存儲(TLS)或特定內核用途。unsigned long gs;// gs %gs 附加段寄存器,用途與 %fs 類似,可能用于特定擴展。unsigned long orig_ax;// orig_ax - 原始系統調用號或中斷錯誤碼:- 系統調用時保存系統調用號(如 __NR_read)。- 中斷或異常時保存錯誤碼或中斷向量號。unsigned long ip;// ip %eip 指令指針寄存器(Instruction Pointer),指向下一條要執行的指令地址。unsigned long cs;// cs %cs 代碼段寄存器(Code Segment),保存當前代碼段的段選擇子。unsigned long flags;// flags %eflags 標志寄存器,保存 CPU 狀態標志(如中斷使能、方向標志、溢出標志等)。unsigned long sp;// sp %esp 棧指針寄存器(Stack Pointer),指向當前棧頂地址。unsigned long ss;// ss %ss 棧段寄存器(Stack Segment),指向當前棧段的段選擇子。
};
x86_64位:
struct pt_regs {unsigned long r15; // r15 %r15 通用寄存器,通常用于保存臨時數據或地址。在系統調用中可能作為第 6 個參數(若需要)。unsigned long r14; // r14 %r14 通用寄存器,用途同上。unsigned long r13; // r13 %r13 通用寄存器,用途同上。unsigned long r12; // r12 %r12 通用寄存器,用途同上。unsigned long bp; // bp %rbp 基址指針寄存器,指向當前棧幀的基地址,用于函數調用棧回溯。unsigned long bx; // bx %rbx 基址寄存器,常用于保存數據或地址(如內存操作的基地址)。unsigned long r11; // r11 %r11 通用寄存器,可能用于臨時存儲或特定指令(如 syscall 指令會破壞 %r11)。unsigned long r10; // r10 %r10 通用寄存器,在系統調用中作為第 4 個參數(若需要)。unsigned long r9; // r9 %r9 通用寄存器,在系統調用中作為第 5 個參數(若需要)。unsigned long r8; // r8 %r8 通用寄存器,在系統調用中作為第 6 個參數(若需要)。unsigned long ax; // ax %rax 累加寄存器,用于系統調用的返回值(ret 前內核會設置 regs->ax)。unsigned long cx; // cx %rcx 計數器寄存器,在 syscall 指令中保存返回地址(被破壞),某些場景下可能作為第 4 個參數。unsigned long dx; // dx %rdx 數據寄存器,通常用于系統調用的第 3 個參數。unsigned long si; // si %rsi 源索引寄存器,通常用于系統調用的第 2 個參數。unsigned long di; // di %rdi 目的索引寄存器,通常用于系統調用的第 1 個參數。unsigned long orig_ax; // orig_ax-原始系統調用號或中斷錯誤碼:- 系統調用時保存系統調用號(如 __NR_read),- 中斷或異常時保存中斷向量號或錯誤碼。unsigned long ip; // ip %rip 指令指針寄存器,指向觸發中斷/異常/系統調用的下一條指令地址(即用戶空間的返回地址)。unsigned long cs; // cs %cs 代碼段寄存器,保存當前代碼段的段選擇子(用戶態為 0x33,內核態為 0x10)。unsigned long flags; // flags %rflags 標志寄存器,保存 CPU 狀態標志(如中斷使能、方向標志、溢出標志等)。unsigned long sp; // sp %rsp 棧指針寄存器,指向用戶空間的棧頂地址。unsigned long ss; // ss %ss 棧段寄存器,保存當前棧段的段選擇子(用戶態為 0x2b)。
};
?上述兩個結構均為x86架構定義,但是實際次做中發現在arm重有偏差,這里簡要列出結構,后有文章詳細介紹相關用途,結構如下:
arm64架構pt_regs結構定義:
/** This struct defines the way the registers are stored on the stack during an* exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for* stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.*此結構定義了異常期間寄存器在堆棧上的存儲方式。需要注意的是,sizeof(struct pt_regs)必須是16的倍數(用于堆棧對齊)。*另外,結構user_pt_regs必須構成結構pt_regs的前綴。*/struct pt_regs {union {struct user_pt_regs user_regs;struct {u64 regs[31]; // X0-X30(通用寄存器)系統調用參數通過 regs[0]-regs[7](X0-X7)傳遞。u64 sp; // 棧指針 (SP_EL0) 保存用戶空間棧頂地址,用于恢復用戶態執行。u64 pc; // 程序計數器 (PC)保存觸發中斷/異常/系統調用的指令地址(即返回地址)。u64 pstate; // 處理器狀態 (PSTATE)};};u64 orig_x0; //保存系統調用 第一個參數的原始值。某些系統調用(如 restart_syscall)需要恢復原始參數
#ifdef __AARCH64EB__u32 unused2;s32 syscallno; //在傳統 ARM64 系統調用中,系統調用號通過 X8 寄存器傳遞,但內核可能將其復制到 syscallno 字段。
#elses32 syscallno;u32 unused2;
#endifu64 orig_addr_limit; //保存用戶態進程的地址訪問限制(如 USER_DS)。在內核態執行時臨時擴大地址限制(如 KERNEL_DS),執行完畢后恢復/* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */u64 pmr_save; //中斷優先級掩碼保存:僅當啟用 ARM64_HAS_IRQ_PRIO_MASKING 時有效。保存中斷處理前的 PMR 值,處理完成后恢復。u64 stackframe[2]; //父函數的幀指針(FP)。父函數的返回地址(LR)。
};
2.pt_regs有什么用
? ? ? ?通過該結構,當需要進行內核函數hook時,可以簡化對應目標鉤子函數的定義以及接口函數的標準化,避免大量的函數指針來制定具體的函數定義(參數個數和參數類型差別)。從而統一接口類型,使鉤子函數實現簡化執行。例如:
原函類型:
typedef asmlinkage long (*sys_call_file_mkdirat_t)(int dfd, const char __user * pathname, umode_t mode);
static sys_call_file_mkdirat_t old_sys_mkdirat;typedef asmlinkage long (*sys_call_file_unlinkat_t)(int dfd, const char __user * pathname, int flag);
static sys_call_file_unlinkat_t old_sys_unlinkat;
pt_regs:
typedef asmlinkage long (*sys_call_file_t)(const struct pt_regs *);static sys_call_file_t[2];// 統一hook函數接口,具體參數依據實際函數定義從regs結構中按照寄存器位獲取。// 例如
static sys_call_file_t[2]; // 初始化是會將需要hook的函數地址進行存儲處理asmlinkage long mkdir_hook(const struct pt_regs *regs)
{int dfd = regs->di;char *filename = (char *)regs->si;umode_t mode = regs->dx;sys_call_file_t origin_mkdirat = sys_call_file_t[__NR_mkdirat]; //獲取hook后的原函數地址//todo ...return origin_mkdirat(regs); // 原函數}asmlinkage long unlinkat_hook(const struct pt_regs *regs)
{int dfd = regs->di;char *filename = (char *)regs->si;int flag = regs->dx;sys_call_file_t origin_unlinkat = sys_call_file_t[__NR_unlinkat]; //獲取hook后的原函數地址//todo ...return origin_unlinkat(regs); // 原函數
}
3.pt_regs如何使用
例如__NR_write對應32位機器調用內核函數原型如下:
asmlinkage long sys_write(unsigned int fd, const char __user *buf,size_t count);
通過自定義函數結合pt_regs進行hook處理:
asmlinkage long self_write_hook(const struct pt_regs* regs)
{int fd = regs->bx; // 第一個參數 fd 通過bx 傳遞char *buf = (char *)regs->cx;// 第二個參數 buf 通過ecxsize_t count = regs->dx; // 第三個參數 count 通過edx// to ...return origin_read(regs); // origin_read為hook時存儲的原始函數地址
}
上述為針對sys_write函數的hook調用時通過regs結構來實現相關參數的獲取處理。
4.實際使用案例
? ? 通過對openat為例子進行hook,具體代碼如下:
my_openat_hook.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/unistd.h>
#include <linux/sched.h>
#include <linux/syscalls.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/fdtable.h>
#include <linux/uaccess.h>
#include <linux/kallsyms.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>
#include <linux/kprobes.h>
#include <linux/fs_struct.h>
#include <linux/mm.h>
#include <linux/delay.h>
#include <linux/namei.h>
#include <asm/syscall.h>
#include <uapi/linux/mount.h>//符號表獲取
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
kallsyms_lookup_name_t get_kallsyms_lookup_name(void)
{int ret;kallsyms_lookup_name_t pfun;static struct kprobe kp ={.symbol_name = "kallsyms_lookup_name",};ret = register_kprobe(&kp);if (ret < 0){printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);return NULL;}pfun = (kallsyms_lookup_name_t)kp.addr;unregister_kprobe(&kp);return pfun;
}static int obtain_sys_call_table_addr(unsigned long *sys_call_table_addr)
{unsigned long temp_sys_call_table_addr;kallsyms_lookup_name_t fn_kallsyms_lookup_name = 0;fn_kallsyms_lookup_name = get_kallsyms_lookup_name();if (fn_kallsyms_lookup_name == NULL){printk("Fail to get_allsyms_lookup_name\n");return -1;}temp_sys_call_table_addr = fn_kallsyms_lookup_name("sys_call_table");/* Return error if the symbol doesn't exist */if (0 == temp_sys_call_table_addr){printk("Can not found sys_call_table\n");return -1;}printk("Found sys_call_table: %p", (void *)temp_sys_call_table_addr);*sys_call_table_addr = temp_sys_call_table_addr;return 0;
}static unsigned long sys_call_table_ptr;
typedef asmlinkage long (*self_openat_hook_t)(const struct pt_regs*);
static self_openat_hook_t old_self_openat;
unsigned int disable_cr0(void)
{unsigned int cr0 = 0;unsigned int ret;asm volatile ("movq %%cr0, %%rax": "=a"(cr0));ret = cr0;cr0 &= 0xfffeffff;asm volatile ("movq %%rax, %%cr0"::"a"(cr0));return ret;
}
void enable_cr0(unsigned int val)
{asm volatile ("movq %%rax, %%cr0": : "a"(val));
}asmlinkage long self_openat_hook(const struct pt_regs* regs)
{int dfd = regs->di;char* filename = (char*)regs->si;int flag = regs->dx;umode_t mode = regs->r10;printk(KERN_INFO "dfd = %d,filename = %s,flag = %d,mode = %d",dfd,filename,flag,mode);return old_self_openat(regs);
}static int __init self_init(void)
{int cr0;obtain_sys_call_table_addr(&sys_call_table_ptr);cr0 = disable_cr0();old_self_openat = (self_openat_hook_t)((unsigned long*)sys_call_table_ptr)[__NR_openat];// 保留舊函數((unsigned long*)sys_call_table_ptr)[__NR_openat] = (unsigned long)self_openat_hook; // 設置新函數enable_cr0(cr0);printk("Success to hook openat func !!!");return 0;
}static void __exit self_exit(void) {int cr0;cr0 = disable_cr0();((unsigned long*)sys_call_table_ptr)[__NR_openat] = old_self_openat;enable_cr0(cr0);printk("Exit to hook openat func !!!");return;
}module_init(self_init)
module_exit(self_exit)
MODULE_LICENSE("GPL");
Makefile:
obj-m := my_openat_hook.oPWD := $(shell pwd)KERNEL_DIR := "/lib/modules/$(shell uname -r)/build"EXTRA_CFLAGS += -I$(src)/includemodules:@$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modulesclean:@rm -rf *.ko *.o *.mod.c *symvers *order .*cmd *cmd
將ko文件寫入到系統:
insmod my_openat.ko,通過dmesg -w查看系統日志:
?以上為使用pt_retgs結構實現的內核函數hook功能案例。