【SEU-OS-Lab2】东南大学操作系统专题实践二教程

【SEU-OS-Lab2】东南大学操作系统专题实践二教程
小包子本教程区别于其他教程在于,本教程是在WSL2+Linux 6.6.36.6下实现hide系统调用
实验要求
实现一个系统调用hide,使得可以根据指定的参数隐藏进程,使用户无法使用ps或top观察到进程状态。
具体要求:
- 实现系统调用int hide(pid_t pid, int on),在进程pid有效的前提下,如果on置1,进程被隐藏,用户无法通过ps或top观察到进程状态;如果on置0,则恢复正常状态。
- 考虑权限问题,只有root用户才能隐藏进程。
- 设计一个新的系统调用int hide_user_processes(uid_t uid, char *binname),参数uid为用户ID号,当binname参数为NULL时,隐藏该用户的所有进程;否则,隐藏二进制映像名为binname的用户进程。该系统调用应与hide系统调用共存。
- 在/proc目录下创建一个文件/proc/hidden,该文件可读可写,对应一个全局变量hidden_flag,当hidden_flag为0时,所有进程都无法隐藏,即便此前进程被hide系统调用要求隐藏。只有当hidden_flag为1时,此前通过hide调用要求被屏蔽的进程才隐藏起来。
- 在/proc目录下创建一个文件/proc/hidden_process,该文件的内容包含所有被隐藏进程的pid,各pid之间用空格分开。
实验目的
- 加深理解进程控制块、进程队列等概念,了解进程管理的具体实施方法。
- 了解系统调用的实现原理,掌握Linux内核相关接口,实现自己的系统调用。
实验环境
- WSL2
- Ubuntu22.04
- Linux版本:Linux 6.6.36.6-microsoft-standard-WSL2
前置知识
本实验涉及到对WSL系统内核的重新编译和替换,可参考以下教程
WSL2更换Linux Kernel为6.6.36.6版本 - 知乎
实验内容
实现hide和hide_user_processes系统调用
在此部分,我将实现实验要求中的1,2,3。即实现hide和hide_user_processes系统调用,使得用户可以通过调用这两个系统调用来控制进程的隐藏,并增加验权流程。
实现思路
为了实现进程隐藏功能,我们需要了解 ps
和 top
等命令是如何获取进程信息的。这些命令实际上是通过读取 /proc
文件系统中的虚拟文件来获取进程信息的。每个进程在 /proc
中都有一个以其 PID 命名的目录,这些目录中包含了进程的 PCB(进程控制块)信息。
因此,我们可以通过在构建 /proc
文件系统时增加一个标志位 hidden
来实现进程隐藏。当系统检测到某个进程的 hidden
标志位为 1 时,就不在 /proc
中为其构建对应的目录。
流程图
具体实现步骤如下:
修改
task_struct
结构体:在
task_struct
(进程控制块)中添加一个hidden
标志位,用于标记进程是否被隐藏。- 即在
include/linux/sched.h
文件中的struct task_struct{}
结构体中按如下添加代码。用户自定义的参数必须添加在注释预留位与randomized_struct_fields_end
之间。
1
2
3
4
5
6/*
* New fields for task_struct should be added above here, so that
* they are included in the randomized portion of task_struct.
*/
int hidden; // 0:unhidden; 1:hidden
randomized_struct_fields_end- 即在
初始化
hidden
标志位:我们需要在进程创建的时候对这个标志位进行初始化,而进程的创建又是通过
fork
调用实现的。- 即在
kernel/fork.c
文件中的copy_process
函数中按如下添加代码,进行hidden
的初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13__latent_entropy struct task_struct *copy_process(struct pid *pid,
int trace,
int node,
struct kernel_clone_args *args)
{
// ...exist code...
p = dup_task_struct(current, node);
if (!p)
goto fork_out;
// 代码必须要放在这之后,因为上面的代码是对进程的task_struct *p进行初始化
p -> hidden = 0; // add code
// ...exist code...
}- 即在
实现
hide
系统调用:- 创建一个新的系统调用
SYSCALL_DEFINE2(hide, pid_t, pid, int, on)
。 - 在该系统调用中,通过
uid_eq(current_euid(), GLOBAL_ROOT_UID)
,确保只有 root 用户可以隐藏进程。 - 通过
pid_task(find_vpid(pid), PIDTYPE_PID)
查找目标进程。 根据
on
参数设置目标进程的hidden
标志位。- 即在
kernel/sys.c
文件末尾添加如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// 接受一个 pid 和 on 参数,根据 on 参数设置pid对应的进程的hidden标志位
SYSCALL_DEFINE2(hide, pid_t, pid, int, on)
{
if (pid < 0) {
return -EINVAL;
}
printk("hide process invoked with params: pid=%d on=%d\n", pid, on);
printk("current uid = %d\n", get_current_cred()->uid.val);
// 校验是否为root用户
// ps:还可使用capable(CAP_SYS_ADMIN)等来检测用户权限,而不是限制为root用户
if(!uid_eq(current_euid(), GLOBAL_ROOT_UID)) {
printk("you aren't the root user! try to use sudo!\n");
return -EPERM;
}
// 查找目标进程的 task_struct
// ps:还可使用如 find_task_by_vpid(pid), find_get_task_by_vpid(pid)来查找
struct task_struct *goal_task = pid_task(find_vpid(pid), PIDTYPE_PID);
if(goal_task) {
printk("goal_task current hidden value: %d\n", goal_task->hidden);
goal_task->hidden = on;
printk("Set goal_task hidden to %d\n", goal_task->hidden);
}
else {
printk("Process with pid %d not found\n", pid);
return -ESRCH;
}
printk("nice system call! goodbye!\n");
return 0;
}- 即在
- 创建一个新的系统调用
实现
hide_user_processes
系统调用:- 创建一个新的系统调用
SYSCALL_DEFINE2(hide_user_process, uid_t, uid, char __user *, binname)
。 - 在该系统调用中,通过
uid_eq(current_euid(), GLOBAL_ROOT_UID)
,确保只有 root 用户可以隐藏进程。 - 使用
for_each_process
遍历系统中的所有进程,匹配uid
和binname
。 对匹配的进程设置
hidden
标志位为1。- 即在
kernel/sys.c
文件末尾添加如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74// 传递一个用户 ID (uid) 和可选的可执行文件名 (binname),隐藏指定用户的进程
SYSCALL_DEFINE2(hide_user_process, uid_t, uid, char __user *, binname) {
struct task_struct *p;
struct user_namespace *ns = current_user_ns();
// TASK_COMM_LEN = 16,task_struct存储的进程名称最长只保留16位
char task_name[TASK_COMM_LEN] = {0}; // 存储当前进程名
char target_name[TASK_COMM_LEN] = {0}; // 存储目标进程名
int hide;
kuid_t kuid;
if (uid < 0) {
return -EINVAL;
}
kuid = make_kuid(ns, uid);
printk("hide user_process invoked with params: kuid=%d binname=%s\n", uid, binname);
// 检查权限
if (!uid_eq(current_euid(), GLOBAL_ROOT_UID)) {
printk("you aren't the root user! try to use sudo!\n");
return -EPERM;
}
// 复制目标进程名到target_name
if (binname != NULL) {
// 先检查用户空间指针是否可访问
if (!access_ok(binname, TASK_COMM_LEN-1)) {
printk("Invalid user space pointer\n");
return -EFAULT;
}
// 复制字符串,最多复制 TASK_COMM_LEN-1 个字符
if (strncpy_from_user(target_name, binname, TASK_COMM_LEN-1) < 0) {
printk("Failed to copy process name from user space\n");
return -EFAULT;
}
// 确保字符串以 null 结尾
target_name[TASK_COMM_LEN-1] = '\0';
printk("kernel space target_name = %s\n", target_name);
} else {
printk("kernel space target_name = NULL\n");
}
// 遍历进程
for_each_process(p) {
if (uid_eq(task_uid(p), kuid) && task_pid_vnr(p) != 0) {
hide = 1;
// 获取当前进程名
get_task_comm(task_name, p);
// 如果指定了进程名,则进行比较
if (binname != NULL) {
if (strcmp(task_name, target_name) != 0) {
hide = 0;
}
}
printk("scan on '%s' hide it? =%d", task_name, hide);
if (hide) {
p->hidden = 1; // 对当前进程进行隐藏
printk("uid = %d, hide pid = %d, process name = %s\n",
from_kuid(&init_user_ns, task_uid(p)),
task_pid_vnr(p),
task_name);
}
}
}
printk("nice system call! goodbye!\n");
return 0;
}- 即在
- 创建一个新的系统调用
- 修改
/proc
文件系统:- 在
/proc
文件系统的读取函数中,检查每个进程的hidden
标志位。如果某个进程的hidden
标志位为 1,则跳过该进程,不在/proc
中为其创建目录。- 即在
fs/proc/base.c
文件中的proc_pid_readdir
函数中,按如下添加代码,在循环构建/proc
文件系统时,当检测到进程的标志位hidden==1
就跳过它的构建,这样该进程就被隐藏啦。1
2
3
4
5
6
7
8
9
10
11
12
13
14int proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
// ...exist code...
for (iter = next_tgid(ns, iter);
iter.task;
iter.tgid += 1, iter = next_tgid(ns, iter))
{
if(iter.task->hidden==1) continue; //add code
char name[10 + 1];
unsigned int len;
// ...exist code...
}
// ...exist code...
}
- 即在
- 在
添加系统调用接口到内核文件中:
在
include/linux/syscalls.h
文件中添加如下代码,与其他系统调用放在一起。1
2asmlinkage long sys_hide( pid_t pid, int on ); //my system call
asmlinkage long sys_hide_user_process( uid_t uid, char __user * binname ); //my system call在
include/uapi/asm-generic/unistd.h
文件中添加系统调用编号,该编号只需要加在64位系统调用末尾,与其他编号不同即可(编号大于512的是留给32位系统调用的),记得改最后的末尾系统调用编号。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15before
__SYSCALL(__NR_fchmodat2, sys_fchmodat2)
after
__SYSCALL(__NR_fchmodat2, sys_fchmodat2)
__SYSCALL(__NR_hide, sys_hide)
__SYSCALL(__NR_hide_user_process, sys_hide_user_process)在对应系统版本的系统调用表中添加如下代码(例如我的是wsl内核,编译后的内核在x86目录下说明编译的是x86版本linux内核,因此我在
arch/x86/entry/syscalls/syscall_64.tbl
文件中添加),注意系统调用编号与上面代码中定义的相同。1
2454 64 hide sys_hide
455 64 hide_user_process sys_hide_user_process
实现/proc/hidden和/proc/hidden_process文件控制
在此部分,我将实现实验要求中的4,5。即实现 /proc/hidden
和 /proc/hidden_process
文件控制,使得用户可以通过 hidden
文件开关隐藏功能,通过 hidden_process
文件获取所有隐藏进程的pid。
实现思路
要想在 /proc
文件系统中添加文件,我们需要在 fs/proc/root.c
文件中的 proc_root_init
函数中去添加相关代码,让 /proc
文件系统初始化时,能自动创建 hidden
和 hidden_process
文件。
hidden
文件中就保存着 hidden_flag
标志位,对 hidden
文件的读写就是对 hidden_flag
的读写。将 hidden_flag
标志位与 hidden
标志位共同作用到fs/proc/base.c
文件中的 proc_pid_readdir
函数中即可实现整体进程隐藏功能的开关。
hidden_process
文件就作为一个函数的接口,读取该文件时就执行这个函数。该函数即遍历所有进程并输出 hidden
标志位为1的进程的pid。
对于hidden
文件:
创建一个所有内核文件都能获取的hidden_flag标志位:
可以在
fs/proc
目录下新建一个var.h
文件1
extern int hidden_flag;
在需要使用
hidden_flag
标志位的文件(fs/proc/root.c
和fs/proc/base.c
)中添加头文件引入1
在
proc_pid_readdir
函数中添加hidden文件初始化的代码:即在
fs/proc/root.c
文件中添加如下代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54// 获取全局变量
int hidden_flag = 0;
// 添加读写回调函数
static ssize_t hidden_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
char tmp[32];
int len;
// 如果已经读取过,返回0表示结束
if (*ppos > 0)
return 0;
// 将hidden_flag转换为字符串
len = snprintf(tmp, sizeof(tmp), "%d\n", hidden_flag);
// 确保不会超出用户提供的缓冲区
if (count < len)
return -EINVAL;
// 复制到用户空间
if (copy_to_user(buf, tmp, len))
return -EFAULT;
*ppos = len;
return len;
}
static ssize_t hidden_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
char tmp[32];
int new_value;
// 防止缓冲区溢出
if (count >= sizeof(tmp))
return -EINVAL;
// 从用户空间复制数据
if (copy_from_user(tmp, buf, count))
return -EFAULT;
// 确保字符串结束
tmp[count] = '\0';
// 将字符串转换为整数
if (kstrtoint(tmp, 10, &new_value) < 0)
return -EINVAL;
// 更新全局变量
hidden_flag = new_value;
return count;
}
static const struct proc_ops hidden_fops = {
.proc_read = hidden_read, // 调用了上面的回调函数
.proc_write = hidden_write,
};
//...exist code...
void __init proc_root_init(void){
//...exist code...
}在
proc_pid_readdir
函数末尾添加以下代码创建hidden
文件1
2
3
4
5
6
7void __init proc_root_init(void){
//...exist code...
// 创建 /proc/hidden 文件
if(!proc_create("hidden", 0600, NULL, &hidden_fops)) {
pr_err("Failed to create /proc/hidden\n");
}
}
修改
/proc
文件系统:在
fs/proc/base.c
文件中的proc_pid_readdir
函数中不仅加入对hidden
标志位的判断,还要加入对hidden_flag
标志位的判断。1
2
3
4
5
6
7
8
9
10
11
12
13
14int proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
// ...exist code...
for (iter = next_tgid(ns, iter);
iter.task;
iter.tgid += 1, iter = next_tgid(ns, iter))
{
if(hidden_flag==0 && iter.task->hidden==1) continue; //add code
char name[10 + 1];
unsigned int len;
// ...exist code...
}
// ...exist code...
}
对于 hidden_process
文件:
在
proc_pid_readdir
函数中添加hidden_process
文件初始化的代码:即在
fs/proc/root.c
文件中添加如下代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// hidden_process的读回调
static ssize_t hidden_process_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos) {
static char kbuf[1024*8]="";
char tmp[128];
struct task_struct *p;
int len;
pid_t pid;
if (*ppos > 0)
return 0;
// 清空缓冲区
memset(kbuf, 0, sizeof(kbuf));
// 遍历进程并处理
for_each_process(p) {
// 获取被隐藏的进程,这里只记录pid不为0的进程,因为pid=0的进程原本是看不到的
if (p->hidden == 1 && task_pid_vnr(p)!=0) {
pid = task_pid_vnr(p);
snprintf(tmp, sizeof(tmp), "%d ", pid);
strcat(kbuf, tmp);
}
}
len = strlen(kbuf);
// 将内核空间的数据复制到用户空间
if (copy_to_user(buf, kbuf, len))
return -EFAULT;
*ppos += len; // 更新文件位置
return len;
}
static const struct proc_ops hidden_process_fops = {
.proc_read = hidden_process_read,
};在
proc_pid_readdir
函数末尾添加以下代码创建hidden_process
文件1
2
3
4
5
6
7void __init proc_root_init(void){
//...exist code...
// 创建 /proc/hidden_process 文件
if(!proc_create("hidden_process", 0600, NULL, &hidden_process_fops)) {
pr_err("Failed to create /proc/hidden_process\n");
}
}
实验结果
测试函数 test_hide.c
1 |
|
隐藏pid为1的进程
before
有pid=1的进程
after
无pid=1的进程
验权
非root用户无法调用该系统调用
测试函数test_hide_user.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int main(int argc, char *argv[]) {
if (argc != 2 && argc != 3) {
printf("Usage:\n");
printf(" %s uid # Hide all processes of specified user\n", argv[0]);
printf(" %s uid binname # Hide specific program of specified user\n", argv[0]);
return 1;
}
// 解析用户ID
uid_t uid = atoi(argv[1]);
// 调用系统调用
long result;
if (argc == 2) {
// 只隐藏指定用户的所有进程
result = syscall(__NR_hide_user_process, uid, NULL);
}
else if(argc == 3){
// 隐藏指定用户的特定程序
result = syscall(__NR_hide_user_process, uid, argv[2]);
}
if (result == 0) {
printf("Successfully called hide_user_process\n");
if (argc == 2) {
printf("Hidden all processes of user %d\n", uid);
}
else {
printf("Hidden process '%s' of user %d\n", argv[2], uid);
}
}
else {
printf("System call failed with error code: %ld\n", result);
}
return 0;
}隐藏warma10032用户的所有进程(uid为1000)
before
有warma10032的进程
after
无warma10032的进程
验权
非root用户无法调用该系统调用
查看
/proc/hidden_process
获取所有隐藏进程的pid为之前隐藏的pid=1进程和warma10032用户的进程
控制
/proc/hidden
文件控制开关隐藏功能before
/proc/hidden
为0,pid=1的进程仍然被隐藏after
/proc/hidden
置1,被隐藏的进程都出现了
实验心得
实验如果按部就班的按教程在linux 2.x.x 版本上做应该不难,但因为低版本的虚拟机用起来不习惯,之前也有使用wsl的经验,就打算在wsl上完成本次实验。也许这是一个坏决定,因为教程中的很多方法在新版本的linux上已经不适用了,而且我这还是wsl定制版,在实验中难免踩很多坑。
在实验中,我觉得最难理解的部分就是linux中进程id的多变,首先linux中有进程和线程之间的包含关系,但它们的底层实现却是相同的task_truct结构体,这就导致在实验过程中要找到真正的进程需要花费一些力气,不仅需要通过task_truct中的pid,tid,pgid,tgid等综合来判断,这些“id们”在不同的namespace中的值也有可能不同。如task_pid_nr和task_pid_vnr两个函数一般会返回两个不同的值,因为一个是内核空间中的pid,一个是当前空间中的pid
在linux中进行内核实验还有一点难点在于,参数需要在用户空间与内核空间之间进行传递,这也是比较抽象的一点,相同的参数在不同的空间中的值可能并不相同。
总的来说,修改并编译一个自己的内核,还挺有hacker的感觉。