操作系统专题实践——隐藏进程
由于是线上上课,老师就把实验内容发给我们,什么也不教就让我们自己做了,对于操作系统和Linux内核的版本也没有明确的要求,只是说推荐Fedora7和Linux-2.6.21。
搜集了各种往年资料后,发现往年都是要求用Fedora7和Linux-2.6.21做的,但是一看Fedora官方版本,都已经更新到34了,Linux的低版本也几乎找不到。最后还要感谢我女朋友,帮我从之前上过该课的同学那要到了他们老师发的资料,好家伙,Fedora7的镜像里Linux-2.6.21也已经给准备好了,亏我花了一天找资源+尝试用Fedora34。如果有需要虚拟机或改版本内核可以联系我哦~
实验前准备
多亏了在github中一直关注的一位17级大佬,在他的博客中写了许多踩坑和技巧,这里贴一下大佬的博客地址
Fedora7启用SSH
先是学着学长给Fedora7启用SSH,几乎就是照着学长的博客一步一步来的。
关闭防火墙和SELinux
只是在本地做实验,果断关了虚拟机的防火墙。执行
setup
命令 选择Firewall configuration 防火墙配置,然后按下图配置
空格选择 上下左右移动
至于关闭SELinux,修改/etc/sysconfig/selinux 把enforcing改成disabled;然后命令行
setenforce 0
;检查是否已安装ssh服务
执行
rpm -qa | grep openssh-server
。如果有内容就说明已安装,否则用yum install openssh-server
安装之。修改配置文件
1
$ vim /etc/ssh/sshd_config
放开如下配置:
1
2
3
4Port 22
Protocol 2
PermitRootLogin yes
MaxAuthTries 6重启SSH服务
1
2$ /etc/rc.d/init.d/sshd restart
$ service sshd status成功的话是这样滴
如果报
bash: service: command not found
的错,需要su - root
进入root模式,单纯su
是不行的。查看ip
在root模式运行
ip add
可以看到虚拟机的ip
从vscode到samba
照着学长的博客想要用vscode的 Remote SSH 插件连接虚拟机,然而和学长一样,发现Fedora7过于古老,直接报错
于是照着学长用古老的Samba
安装Samba
执行
rpm -qa | grep samba
,观察是否输出如下三个包:如果没有,则缺哪个装哪个:
1
yum install samba samba-common samba-client
编辑 Samba 配置文件
执行
vim /etc/samba/smb.conf
,修改如下几个地方:```
(74行左右) workgroup = WORKGROUP1
2
3
- ```
(101行左右) security = user在 246 行左右 Share Definitions 下新增:
1
2
3
4
5
6
7
8[samba]
comment = samba
path = /
public = yes
writable = yes
valid users = root
create mask = 0700
directory mask = 0700最后看起来大概是这样:
添加 Samba 用户并激活
1
2smbpasswd -a root
smbpasswd -e root守护 Samba 服务
1
2
3
4
5
6chkconfig smb on
chkconfig --list smb # 检查2~5为on
chkconfig nmb on
chkconfig --list nmb # 检查2~5为on
/etc/rc.d/init.d/smb start
/etc/rc.d/init.d/nmb start配置至此,reboot 虚拟机。
在 Windows 下启用 Samba
Samba 在 Windows 10 下默认是关闭的。我们需要将它手动启动。打开控制面板 - 程序和功能 - 启用或关闭 Windows 功能,将 SMB 打勾:
通过 Samba 连接虚拟机
在文件资源管理器任意页面用双反斜杠加 IP 即可访问。如有需要也可将该地址做成快捷方式放到桌面。
这时,所有的文件都可以访问:
使用 VSCode 也可自由打开、编辑其中的文件。
配置 VSCode includePath
此时 VSCode 由于不了解 Linux 的 include 文件,无法正确进行代码提示,存在头文件划红线的情况。为此,可增加工作区配置。在项目目录新建
.vscode/settings.json
文件,键入如下内容:1
2
3{
"C_Cpp.default.includePath": ["\\\\<ip_to_linux>/samba/usr/include", "\\\\<ip_to_linux>/samba/usr/local/include"]
}
内核编译加速技巧
基本方法
众所周知,增大系统内存、使用 SSD、增加 CPU 核心数,都可以有效加快编译速度。如有条件,使用内存盘可以成吨加快编译。由于是虚拟机,咱不折腾,多给点内存就好了。
多线编译
make 命令可带参数 -j2、-j4 等启用二线、四线并发编译,这也可以有效加快编译速度,但并不一定是数字越大越好。在我的机器上,使用 -j2 ,按照实验指导书的配置,全套编译流程在 20 分钟左右。这个加速比较可观。
内核裁剪
按照实验指导书的配置,make 内核时,用的配置文件直接是 Fedora 7 系统带的配置。这意味着大量根本用不到的内核功能都会编译进去…在编译前 make menuconfig ,对不需要的功能进行裁剪,使用自定义的配置文件来编译内核,必然可以有效缩小内核大小,加快编译流程。对内核裁剪有兴趣的,可自行搜索相关资料研究,此处不述。
Linux内核源码树及相关知识
该部分内容来自于《Linux内核设计与实现》(中文第三版),由于我也并未认真阅读,这里只是列举些可能有用的内容。
获取内核源码
登录Linux内核官方网站http://www.kernel.org,可获得当前版本的Linux源代码,可以是完整压缩的形式也可以是增量补丁的形式。
另外可以用Git来获得最新提交到Linux版本树的一个副本,并可以pull
到最新的分支,这里不做过多叙述。
解压编译内核
下载下压缩文件后,
$ tar xvzf linux-x.y.z.tar.gz
解压缩,其中x.y.z为版本号,具体的大版本小版本大家可以自己去查。
内核源码一般安装在/usr/src/linux 目录下。但请注意不要用这个源码树进行开发,因为编译C库所用的内核版本就链接到这棵树。此外,不要以root身份对内核进行修改,而是仅以root身份安装新内核。
如何编译内核
还是决定抄一遍实验指导中的编译内核过程:
解压内核
桌面上的linux-2.6.21.tar.gz是linux-2.6.21的内核代码压缩包,解压:
1
2
3cd Desktop
tar zxvf linux-2.6.21.tar.gz
cd linux-2.6.21
生成内核配置文件
将当前正在运行的内核对应的配置文件作为模板来生成.config文件,即将/boot目录下的已有的config文件复制到linux-2.6.21目录下
1
2make mrproper
cp /boot/config-`uname -r` ./config第一个命令make mrproper用来保证内核树是干净的,如果内核第一次编译则可以省略。其中的uname –r命令可查看当前环境下的内核版本号。
更新config文件:
1
make oldconfig
部分新配置项会提示用户选择,都选N或者缺省即可,完成后即可生成.config文件。
编译安装内核
在编译内核前,可以定义自己的内核版本号,在内核代码的根目录下有Makefile文件,例如将第4行改为:
EXTRAVERSION = -seu
这样新内核版本号就是2.6.21-seu
1
2
3
4
5make all
su
make modules_install
make install
make headers_installMake all的执行过程可能比较长。
如果三个命令均成功执行,可以观察引导程序grub的配置文件/boot/grub/menu.lst的内容,在hiddenmenu之后可以看到刚刚编译安装的内核版本,将hiddenmenu那一行注释或删除,方便直接操作菜单:
#hiddenmenu
然后重启系统:
1
reboot
重启后可以看到grub菜单已经包含了新编译的内核。如果新内核启动失败,一般是由于配置或者内核代码修改的有问题,选择原先的内核启动,再进行修改、编译。
内核源码树
内核源码树由很多目录组成,而大多数目录又包含更多子目录。一下是一些内核根目录下目录的主要内容。(书上写的,新版本可能不太一样了)
目录 | 内容 |
---|---|
arch | 特定体系结构的源码 |
block | 块设备I/O层 |
crypto | 加密API |
Documentation | 内核源码文档 |
drivers | 设备驱动程序 |
firmware | 使用某些驱动程序而需要的设备固件 |
fs | VFS和各种文件系统 |
include | 内核头文件 |
init | 内核引导和初始化 |
ipc | 进程间通信代码 |
kernel | 像调度程序这样的核心子系统 |
lib | 通用内核函数 |
mm | 内存管理子系统和VM |
net | 网络子系统 |
samples | 示例,示范代码 |
scripts | 编译内核所用的脚本 |
security | Linux安全模块 |
sound | 语音子系统 |
usr | 早期用户空间代码 |
tools | 在Linux开发中有用的工具 |
virt | 虚拟化基础结构 |
隐藏进程
第一个实验是隐藏进程,包括隐藏指定PID的进程和隐藏指定UID用户的全部进程。
实验内容
实验目的:
通过实验,加深理解进程控制块、进程队列等概念, 了解进程管理的具体实施方法
实验内容:
实现一个系统调用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之间用 空格分开。
实现思路
思路一:
由于在top,ps等命令中,不会显示0号进程相关信息,所以可以把要隐藏的进程PID置为0。
思路二:
进程信息在/proc文件夹中,通过更改查询文件,实现进程隐藏。
实现步骤(思路二)
隐藏指定PID进程
设置标识
在include/linux/sched.h文件中的task_struct
结构体,就是管理控制进程的控制块PCB。对于每个进程,我们应设置一个标志位hide
,用来标识这个进程是否被隐藏。所以我们在task_struct
中增加一项数据成员hide
。
该结构体定义在include/linux/sched.h 的第800行,但我们将该标识定义到最后。
标识初始化
修改结构体task_struct
后,我们需在进程产生时对其初始化。进程都是由fork系统调用产生的,fork系统调用的实现代码在kernel/fork.c 中,具体实现的主要函数为do_fork ,do_fork 中调用copy_process 函数创建子进程,我们可以将hide的初始化添加在copy_process 中。
添加hide系统调用
添加系统调用的步骤都一样,系统调用函数都在/kernel/sys.c 中,在其最后加入我们的系统调用,对于某个指定pid的进程,实现将其隐藏或取消隐藏。其中current->uid
表示当前用户uid,只有root用户的uid为0。通过pid获取进程task_struct的内核函数为find_task_by_pid。在隐藏后最好调用函数proc_flush_task来清空VFS层的缓冲,解除已有的dentry项。
1 | asmlinkage int sys_hide(pid_t pid, int on) |
修改头文件unistd.h
/usr/src/linux-2.6.21.7/include/asm/unistd.h 这个是内核代码头文件
/usr/include/asm/unistd.h 标准C库的头文件
发现两个头文件定义的系统调用个数不同,为使这个一致,在/usr/src/linux-2.6.21.7/include/asm/unistd.h中加入/usr/include/asm/unistd.h
中多出来的几个系统调用,并且加上自定义的系统调用,如下图:
不要忘记修改 #define NR_syscall 327. 327代表总系统调用个数
同样修改/usr/include/asm下的unistd.h,使两者系统调用相同。
然后重新修改sys.c,增加新系统调用,donothing就行
1 | asmlinkage int sys_utimensat( void ) |
最后修改syscall_table
直接打开 /usr/src/linux-2.6.21.7/arch/i386/kernel/syscall_table.S
在末尾添加新系统调用,如下图所示。
修改proc_pid_readdir函数
修改proc_pid_readdir函数(在fs/proc/base.c文件中)其中使用for循环遍历进程,在遍历过程中添加判断,过滤掉被隐藏的进程。
修改proc_pid_lookup函数
修改proc_pid_lookup函数,在进程查找完成前过滤掉被隐藏的进程。
验证系统调用
编写代码验证系统调用能否工作,代码如下:
1 |
|
因为我的hide_proccess为325号系统调用,这里的syscallNum
设为325。这里我们想隐藏1号进程。
初始状态
先执行ps aux
看看进程列表
非root用户调用
非root用户无法进行隐藏操作。
root用户调用
可以看到,PID为1的进程被隐藏了。
取消隐藏
更改参数on=0
隐藏指定UID进程
添加系统调用hide_user_processes
遍历系统中所有的进程,隐藏满足要求的进程。使用函数for_each_process遍历所有进程,每个进程的task_struct中有成员变量uid和comm,uid为该进程的用户id,comm为进程名,根据要求隐藏对应进程。具体解释见代码部分。
1 |
|
验证系统调用
编写代码验证系统调用能否工作,代码如下:
1 |
|
初始状态
先用ps aux 查看进程列表
图同上,由于pid的前几个都是root用户的进程,所以这里截了最后几个进程。
非root用户调用
无法隐藏
root用户调用
用户seu的uid为500,隐藏uid=500的所有进程
原先seu用户的进程被隐藏。
隐藏root用户(uid为0),名称为init的进程
修改参数uid_t uid=0; char *binname=”init”。
更改参数recover=1,所有进程将恢复为显示状态
这里就不截图演示了。
在/proc目录下创建一个文件/proc/hidden
设置全局变量
设置全局变量hidden_flag。
因为这个实验又涉及到隐藏进程的问题,所以需要再设置一个标识作为判断,hidden文件的读写对它进行操作即可。
在/fs/proc目录下新建一个头文件,里面包含一个全局变量,其它文件中需要用到这个全局变量的时候,需使用include包含这个头文件。
1 | extern int hidden_flag; |
hidden文件的创建及读写
proc文件系统在初始化函数proc_root_init中会调用proc_misc_init函数,此函数用于创建/proc根目录下的文件,那么将创建hidden文件的代码插入到此函数中就可以在proc初始化时得到执行。在/fs/proc/proc_misc.c中添加回调函数,在/fs/proc/proc_misc.c中proc_misc_init函数的最后添加创建hidden文件的代码,并指定其回调函数,具体说明见代码部分。
回调函数
在/fs/proc/proc_misc.c中
1 | int hidden_flag=1; |
创建hidden文件
在/fs/proc/proc_misc.c中proc_misc_init函数的最后添加创建hidden文件的代码,并指定其回调函数
1 | struct proc_dir_entry *ptr=create_proc_entry("hidden",0644,NULL); |
根据hidden_flag显示/隐藏进程
结合上面根据hide判断进程,这个实验与之类似,只需在fs/proc/base.c文件中,修改proc_pid_readdir函数以及proc_pid_lookup函数,在hide判断之前,增加hidden_flag对进程的约束。
验证
首先默认设置hidden_flag=1,使用hide_user_processes隐藏uid=500,即seu用户的所有进程。隐藏过程和之前一样。
隐藏前的最后几个进程
隐藏后
- 将hidden_flag的值改为0,这时再查看进程,所有被隐藏的进程又出现了。
- 将hidden_flag改回为1,查看进程,seu用户的进程又处于隐藏状态了。
在/proc目录下创建一个文件/proc/hidden_process
回调函数
该文件用于存储所有被隐藏进程的pid,因为这个文件暂时不涉及用户写入,所以只需设置其读回调函数。
1 | static int proc_read_hidden_processes(char *page,char **start,off_t off,int count,int *eof,void *data) |
写hidden_process 文件
验证
首先隐藏uid=500,即seu用户的所有进程(同上)
查看hidden_process文件里的内容,发现所有被隐藏进程的pid都存在这里。
恢复所有进程为显示状态
这时没有被隐藏的进程,hidden_process文件里的内容也为空。