操作系统专题实践——隐藏进程

由于是线上上课,老师就把实验内容发给我们,什么也不教就让我们自己做了,对于操作系统和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
    4
    Port 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 = WORKGROUP

      1
      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
    2
    $ smbpasswd -a root
    $ smbpasswd -e root
  • 守护 Samba 服务

    1
    2
    3
    4
    5
    6
    $ chkconfig 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身份安装新内核。

如何编译内核

还是决定抄一遍实验指导中的编译内核过程:

  1. 解压内核

    桌面上的linux-2.6.21.tar.gz是linux-2.6.21的内核代码压缩包,解压:

    1
    2
    3
    $ cd Desktop
    $ tar zxvf linux-2.6.21.tar.gz
    $ cd linux-2.6.21
  1. 生成内核配置文件

    将当前正在运行的内核对应的配置文件作为模板来生成.config文件,即将/boot目录下的已有的config文件复制到linux-2.6.21目录下

    1
    2
    $ make mrproper
    $ cp /boot/config-`uname -r` ./config

    第一个命令make mrproper用来保证内核树是干净的,如果内核第一次编译则可以省略。其中的uname –r命令可查看当前环境下的内核版本号。

    更新config文件:

    1
    $ make oldconfig

    部分新配置项会提示用户选择,都选N或者缺省即可,完成后即可生成.config文件。

  2. 编译安装内核

    在编译内核前,可以定义自己的内核版本号,在内核代码的根目录下有Makefile文件,例如将第4行改为:

    EXTRAVERSION = -seu

    这样新内核版本号就是2.6.21-seu

    1
    2
    3
    4
    5
    $ make all
    $ su
    # make modules_install
    # make install
    # make headers_install

    Make 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观察到进程状态。

具体要求:

  1. 实现系统调用int hide(pid_t pid, int on),在进程pid 有效的前提下,如果on置1,进程被隐藏,用户无法通 过ps或top观察到进程状态;如果on置0且此前为隐藏 状态,则恢复正常状态。

  2. 考虑权限问题,只有root用户才能隐藏进程。

  3. 设计一个新的系统调用int hide_user_processes(uid_t uid, char *binname), 参数uid为用户ID号,当binname参数为NULL时,隐藏该用户的所有进程;否则,隐藏二进制映像名为 binname的用户进程。该系统调用应与hide系统调用 共存。
  4. 在/proc目录下创建一个文件/proc/hidden,该文件可读可写,对应一个全局变量hidden_flag,当 hidden_flag为0时,所有进程都无法隐藏,即便此前进程被hide系统调用要求隐藏。只有当hidden_flag为 1时,此前通过hide调用要求被屏蔽的进程才隐藏起来。
  5. 在/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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
asmlinkage int sys_hide(pid_t pid, int on)
{
struct task_struct *p=NULL;
if(pid>0 && (current->uid)==0)//只有root用户可以隐藏进程
{
p=find_task_by_pid(pid);
p->hide=on;//设置进程隐藏状态
if(on==1)
{
printk("Process %d is hidden by root.\n",pid);
}
if(on==0)
{
printk("Process %d is displayed by root.\n",pid);
}
proc_flush_task(p);
}
else
printk("Sorry, you are not root user.Permission denied.\n");

return 0;
}

修改头文件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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
asmlinkage int sys_utimensat( void )
{
return 0;
}

asmlinkage int sys_signalfd( void )
{
return 0;
}

asmlinkage int sys_timerfd( void )
{
return 0;
}

asmlinkage int sys_eventfd( void )
{
return 0;
}

asmlinkage int sys_fallocate( void )
{
return 0;
}

最后修改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
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<sys/syscall.h>
#include<unistd.h>
int main()
{
int syscallNum=325;
pid_t pid=1;
int on=1;
syscall(syscallNum,pid,on);
return 0;
}

因为我的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
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

asmlinkage int sys_hide_user_processes(uid_t uid,char *binname,int recover){
struct task_struct *p=NULL;
if(recover==0)
{
if(current->uid==0)//1.Paragram recover=0;2.root => you can hide the process
{
if(binname==NULL)//when null, hide all the processes of the corresponding user
{
for_each_process(p)
{
if((p->uid)==uid)
{
p->hide=1;
proc_flush_task(p);
}
}
printk("All of the processes of uid %d are hidden.\n",uid);
}
else//otherwise, hide the process of the corresponding name
{
for_each_process(p)
{
char *s=p->comm;
if(strcmp(s,binname)==0 && p->uid==uid)
{
p->hide=1;
printk("Process %s of uid %d is hidden.\n",binname,uid);
proc_flush_task(p);
}
}
}
}
else
printk("Sorry, you are not root user. Permission denied.\n");
}
else if(recover != 0 && (current->uid)==0)//display all of the processes, including which are hidden
{
for_each_process(p)
{
p->hide=0;
}
}

return 0;
}

验证系统调用

编写代码验证系统调用能否工作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
#include<sys/syscall.h>
#include<unistd.h>
int main()
{
int syscallNum=322;
uid_t uid=500;
char *binname=NULL;
int recover=0;
syscall(syscallNum,uid,binname,recover);
return 0;
}

初始状态

先用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int hidden_flag=1;

static int proc_read_hidden(char *page,char **start,off_t off,int count,int *eof,void *data)
{
int len=0;
len=sprintf(page,"%d",hidden_flag);
return len;
}

static int proc_write_hidden(struct file *file,const char *buffer,unsigned long count,void *data)
{
int len=0;
char temp[BUF_LEN];
if(count > BUF_LEN)
len = BUF_LEN;
else
len = count;

copy_from_user(temp, buffer, len);//convert user's input to temp
temp[len]='\0';
hidden_flag=temp[0]-'0';//set the value of hidden_flag
return len;
}

创建hidden文件

在/fs/proc/proc_misc.c中proc_misc_init函数的最后添加创建hidden文件的代码,并指定其回调函数

1
2
3
struct proc_dir_entry *ptr=create_proc_entry("hidden",0644,NULL);
ptr->read_proc=proc_read_hidden;
ptr->write_proc=proc_write_hidden;

根据hidden_flag显示/隐藏进程

结合上面根据hide判断进程,这个实验与之类似,只需在fs/proc/base.c文件中,修改proc_pid_readdir函数以及proc_pid_lookup函数,在hide判断之前,增加hidden_flag对进程的约束。

验证

  1. 首先默认设置hidden_flag=1,使用hide_user_processes隐藏uid=500,即seu用户的所有进程。隐藏过程和之前一样。

    隐藏前的最后几个进程

​ 隐藏后

  1. 将hidden_flag的值改为0,这时再查看进程,所有被隐藏的进程又出现了。

  1. 将hidden_flag改回为1,查看进程,seu用户的进程又处于隐藏状态了。

在/proc目录下创建一个文件/proc/hidden_process

回调函数

该文件用于存储所有被隐藏进程的pid,因为这个文件暂时不涉及用户写入,所以只需设置其读回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int proc_read_hidden_processes(char *page,char **start,off_t off,int count,int *eof,void *data)
{
static char buf[1024*8]="";
char tmp[128];
struct task_struct *p;
if(off>0)
return 0;
sprintf(buf,"%s","");
for_each_process(p)
{
if(p->cloak==1)
{
sprintf(tmp,"%d ",p->pid); //a important step
strcat(buf,tmp);
}
}
sprintf(page,"%s",buf);//convert buf to page to display on the terminal
return strlen(buf);
}

写hidden_process 文件

验证

  1. 首先隐藏uid=500,即seu用户的所有进程(同上)

    查看hidden_process文件里的内容,发现所有被隐藏进程的pid都存在这里。

  2. 恢复所有进程为显示状态

    这时没有被隐藏的进程,hidden_process文件里的内容也为空。