首页 > 系统相关 >从零开始实现一个Linux容器

从零开始实现一个Linux容器

时间:2022-12-05 23:22:36浏览次数:58  
标签:容器 cap container drop bound 从零开始 Linux include CAP

欢迎来到猫猫的C语言实验室喵!

序言:

文中所述源码是以MIT协议开源的,本文转载请注明原创作者为Moe-hacker,除此之外无其他要求。
作者其实想将本文改名为《Re:从零开始的container生活》,不过考虑到搜索引擎可见性就算了吧。
文章非基础教程,当然写这个容器实现前咱也是零基础的,所以可以放心观看喵~
有关容器安全原理的具体作用请看咱的另一篇文章:
浅谈Linux容器安全:chroot,capability与namespace技术
文章所有代码均为C语言实现。
所有代码均为root权限执行。
内容遵守最简代码原则,尽量以最少的代码展示C语言接口的调用。
选修部分代码未给出main()函数,请手动添加测试。
程序完善,异常处理与架构设计在选修章节。
本文容器目录为/data/alpine,作为最小测试系统。
文章分必修和选修两个部分,选修部分技术要求可能较高,里面用到的函数未给出详细解说,请自行查看相关文档学习。
成品展示:Moe-hacker/moe-container

头文件:

为了方便(其实是懒),本文所有C语言代码将共享以下头文件:

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <dirent.h>
#include <errno.h>
#include <linux/stat.h>
#include <linux/sched.h>
#include <linux/limits.h>
#include <sys/prctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/capability.h>

必修部分:

chroot实现:

chroot,顾名思义,更改根目录,容器技术的最基本实现。
演示代码:

#define container_dir "/data/alpine"
int main(){
  chroot(container_dir);
  char *login[]={"/bin/su","-",NULL};
  execv(login[0],login);
  return 0;
}

在termux中需要在tsu下删除LD_PRELOAD变量执行。
你已经完成创建了一个基本容器了,很简单吧,让我们继续喵!

namespace隔离:

namespace技术,linux中的内核隔离技术,可使得进程资源互相隔离。
此处略有些复杂。
运行此段代码请确认内核带有PID namespace支持,否则不会生效。
演示代码为pid namespace的简单利用,实现进程信息隔离。
演示代码:

int main(){
  unshare(CLONE_NEWPID);
  int pid=fork();
  if (pid == 0){
    mount("proc","/proc","proc",MS_NOSUID|MS_NOEXEC|MS_NODEV,NULL);
    system("/bin/ps -ae");
  }
  waitpid(pid,NULL,0);
  return 0;
}

运行结果:

PID TTY          TIME CMD
    1 pts/0    00:00:00 a.out
    2 pts/0    00:00:00 ps

可以看到进程信息被隔离。
注释:
unshare真正实现隔离后能够不受限制运行命令必须经历一次fork()才能实现,否则容器内部将无法fork出新进程运行命令。
fork()函数返回值:父进程返回子进程pid,子进程返回0。
waitpid()可以避免产生僵尸进程并解决容器中无法获取console的问题。
/proc由于在namespace创建之前已被挂载,内部进程信息依然可见,故内部需要重新挂载。

一些可用flags(unshare选项):

CLONE_NEWNS
CLONE_NEWUTS
CLONE_NEWIPC
CLONE_NEWPID
CLONE_NEWCGROUP
CLONE_NEWTIME
CLONE_SYSVSEM
CLONE_FILES
CLONE_FS

具体功能请自行查看相关文档,不做过多解释。

moe-container未实现功能:

CLONE_NEWNET:会导致容器内网络不可用,需手动创建网桥,但用处不大。
CLONE_NEWUSER:需要usermap映射,但是有capability在,用处貌似也不大?
相信你已经学会如何将进程自身隔离了,那我们继续吧喵~

capability管理:

capability,linux内核授予进程的特权,使得进程拥有相应权限。
演示代码为CAP_SYS_ADMIN权限的移除。
代码依赖于libcap库,需要添加-lcap参数编译。
演示代码:

int main(){
  cap_drop_bound(CAP_SYS_ADMIN);
  system("mount / /");
  return 0;
}

执行结果:

mount: '/'->'/': Operation not permitted

可以看到虽然以root权限运行,程序内部依然没有挂载权限。
原因就是父进程通过cap_drop_bound()函数主动放弃了挂载相应的特权。
以上是基于事实验证的结论,让我们也来康一康理论验证:

int main(){
  printf("drop前的进程权限:\n");
  system("cat /proc/self/status|grep Cap");
  printf("\n");
  cap_drop_bound(CAP_SYS_ADMIN);
  cap_drop_bound(CAP_SYS_MODULE);
  cap_drop_bound(CAP_SYS_RAWIO);
  cap_drop_bound(CAP_SYS_PACCT);
  cap_drop_bound(CAP_SYS_NICE);
  cap_drop_bound(CAP_SYS_RESOURCE);
  cap_drop_bound(CAP_SYS_TTY_CONFIG);
  cap_drop_bound(CAP_AUDIT_CONTROL);
  cap_drop_bound(CAP_MAC_OVERRIDE);
  cap_drop_bound(CAP_MAC_ADMIN);
  cap_drop_bound(CAP_NET_ADMIN);
  cap_drop_bound(CAP_SYSLOG);
  cap_drop_bound(CAP_DAC_READ_SEARCH);
  cap_drop_bound(CAP_LINUX_IMMUTABLE);
  cap_drop_bound(CAP_NET_BROADCAST);
  cap_drop_bound(CAP_IPC_LOCK);
  cap_drop_bound(CAP_IPC_OWNER);
  cap_drop_bound(CAP_SYS_PTRACE);
  cap_drop_bound(CAP_SYS_BOOT);
  cap_drop_bound(CAP_LEASE);
  cap_drop_bound(CAP_WAKE_ALARM);
  cap_drop_bound(CAP_BLOCK_SUSPEND);
  cap_drop_bound(CAP_SYS_CHROOT);
  cap_drop_bound(CAP_SETPCAP);
  cap_drop_bound(CAP_MKNOD);
  cap_drop_bound(CAP_AUDIT_WRITE);
  cap_drop_bound(CAP_CHOWN);
  cap_drop_bound(CAP_NET_RAW);
  cap_drop_bound(CAP_DAC_OVERRIDE);
  cap_drop_bound(CAP_FOWNER);
  cap_drop_bound(CAP_FSETID);
  cap_drop_bound(CAP_KILL);
  cap_drop_bound(CAP_SETGID);
  cap_drop_bound(CAP_NET_BIND_SERVICE);
  cap_drop_bound(CAP_SETFCAP);
  printf("drop后的进程权限:\n");
  system("cat /proc/self/status|grep Cap");
}

貌似写进一行也是可以的,不过这里是直接从moe-container中grep出的代码,懒得改了喵………
执行结果:

drop前的进程权限:
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

drop后的进程权限:
CapInh: 0000000000000000
CapPrm: 0000002002000080
CapEff: 0000002002000080
CapBnd: 0000002002000080
CapAmb: 0000000000000000

可以看到进程自身权限确实少了很多喵~
具体哪些权限需要移除,那些权限保留可以参照docker的实现。
好哎!容器基本原理你已经学会了,是时候为你的容器程序添砖加瓦了。

选修部分:

必修课已经学习完毕了,是时候学习一些新的东西了喵!

异常捕获:

大多函数都会定义有异常返回,作为是否执行成功的标志,你需要定义出现异常后所要执行的内容而不是出了bug不知道哪里有问题。
C语言默认没有bool类型,异常返回值一般为int型。

重点函数unshare()和exec()的异常捕获:

若出现异常,这两个函数均会返回-1。
unshare异常大概率是由于内核不支持,输出警告即可。
如下面这段所示:

if(unshare(CLONE_NEWNS) == -1){
  printf("\033[33mWarning: seems that mount namespace is not supported on this device\033[0m\n");
}

exec()函数大概率由于容器内su程序并不存在报错,为异常。
如下面这段所示:

if (execv(login[0],login) == -1){
  fprintf(stderr,"\033[31mFailed to execute `/bin/su`\n");
  fprintf(stderr,"execv() returned: %d\n",errno);
  fprintf(stderr,"error reason: %s\033[0m\n",strerror(errno));
  exit(1);
}

环境检查:

termux兼容:

termux中默认存在LD_PRELOAD变量,会导致exec()函数由于依赖库不同无法执行容器内命令。
解决方法:

char *ld_preload=getenv("LD_PRELOAD");
if(ld_preload != NULL){
  fprintf(stderr,"\033[31mError: please unset $LD_PRELOAD before running this program or use su -c `COMMAND` to run.\033[0m\n");
  exit(1);
}

权限检查:

容器需要以特权创建,否则会运行失败。
解决方法:

if (getuid() != 0){
  fprintf(stderr,"\033[31mError: this program should be run with root privileges !\033[0m\n");
  exit(1);
}

容器目录存在检查:

容器目录不存在会导致chroot()函数失败,检查方法:

DIR *direxist;
if((direxist=opendir(container_dir)) == NULL){
  fprintf(stderr,"\033[31mError: container directory does not exist !\033[0m\n");
  exit(1);
}else{
  closedir(direxist);
}

参数获取:

这段嵌套偏绕,方法有点笨,但是好用。
main()函数加入int argc,char **argv两个参数。
然后:

char *container_dir=NULL;
for (int arg=1;arg<argc;arg++){
  switch(argv[arg][0]){
    case '-' :
      switch(argv[arg][1]){
        case 'v':
          printf("发现参数-v了喵!\n");
          exit(0);
        default:
          fprintf(stderr,"%s%s%s\033[0m\n","\033[31mError: unknow option `",argv[arg],"`");
    }
    case '/':
    case '.':
      printf("%s%s\n","容器目录为",argv[arg]);
      container_dir=argv[arg];
      break;
    default:
      fprintf(stderr,"%s%s%s\033[0m\n","\033[31mError: unknow option `",argv[arg],"`");
      exit(1);
  }
}
if (!container_dir){
  fprintf(stderr,"\033[31mError: container directory is not set !\033[0m\n");
  exit(1);
}

注意break就行了。
这段代码功能有:

  • 获取参数 -v
  • 获取合法容器路径并记录到指针container_dir
  • 若参数异常自动退出

于是你学会了参数获取,让我们继续。

容器目录自动挂载:

容器内部需要挂载系统运行时所需目录,否则无法正常运行。
proc目录挂载前记得先umount两次。
sys直接挂载。
dev挂载为tmpfs,里面的设备可以根据docker普通容器设备列表进行映射和权限更改。
实现示例:

mkdir("/dev",S_IRUSR|S_IWUSR|S_IROTH|S_IWOTH|S_IRGRP|S_IWGRP);
mount("tmpfs","/dev","tmpfs",MS_NOSUID,"size=65536k,mode=755");
mkdir("/dev/pts",S_IRUSR|S_IWUSR|S_IROTH|S_IWOTH|S_IRGRP|S_IWGRP);
mount("devpts","/dev/pts","devpts",0,"gid=4,mode=620");
int dev;
dev=makedev(1,3);
mknod("/dev/null",S_IFCHR,dev);
chmod("/dev/null",S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
symlink("/proc/self/fd","/dev/fd");

具体实现请自行查看moe-container代码及里面的英文注释,懒得翻译回来了。
于是你学会了所有文件系统挂载所需要的知识。

其他函数定义:

你可能需要show_helps()和show_version_info()函数来完善你的程序。
当然猫猫还定义了一个show_greetings()函数作为彩蛋。

架构设计:

恭喜你已经完成了所有技术原理的学习,是时候准备毕业了喵!
此处由于代码逻辑过于简单,不遵守架构设计原理图形式(其实是懒得为了那东西再去学习了喵呜…)
所有重点函数:
show_version_info(),show_helps(),chroot_container()和main()。
基本运行逻辑:

                   ┌参数过少==>show_helps()并退出
程序执行==>获取参数==>│-参数异常==>show_helps()并退出
                   └参数正常
                           ├==>参数为-v==>show_version_info()并退出
                           ├==>参数为-h==>show_helps()并退出
                           └其他参数==>判断容器目录是否给出
                                                     ├==>目录未给出==>报错并退出
                                                     └目录已给出==>判断拥有特权,目录存在和不存在LD_PRELOAD变量
                                                                                                        ├不满足条件==>报错并退出
                                                                                                        └满足条件
                                                                                                             ├存在-u选项==>开启unshare----┐
                                                                                                             └不存在unshare函数==>继续执行┘--执行chroot_container()函数
                                                                                                                                                                     ├开启移除权限操作==>移除权限--┐
                                                                                                                                                                     └未开启移除权限操作==>继续执行┘--运行容器

大概原理就是这样,猫猫也尽力描述了。
于是根据这个原理,你完成了你自己的完整容器实现。

附加知识:

进程名修改:

prctl(PR_SET_NAME,"moe_container",NULL,NULL,NULL);

这样无论编译出的文件名称是什么,进程名都会被程序自身重命名为moe_container。

宏定义:

移除capability和调用unshare那段,可以在宏中为具体选项定义一个开关,1为开0为关,这样就可以编辑头文件来获得更丰富的自定义了。

clang编译相关:

优化参数:

-O3用于开启最高优化支持。

安全相关:

-z noexecstack -z now -fstack-protector-all -fPIE -pie
具体能不能用到猫猫也不知道,但是加上也没害处。

静态编译:

静态编译可以让程序自己包含自己的依赖库,从而不需要外部依赖,以提供更好的系统兼容性。
-static选项用于开启静态编译,你需要提前安装静态依赖库。
termux中需要-ffunction-sections -fdata-sections -Wl,--gc-sections参数来解决编译出的程序无法运行的问题。

Makefile编写:

贴出猫猫的Makefile,相信你基本能看懂:

all :
        cc -lcap -O3 -z noexecstack -z now -fstack-protector-all -fPIE -pie container.c -o container
        strip container
no :
        cc -lcap container.c -o container
static :
        cc -static -ffunction-sections -fdata-sections -Wl,--gc-sections -lcap -O3 -z noexecstack -z now -fstack-protector-all -fPIE container.c -o container
        strip container
staticfail :
        cc -static -ffunction-sections -fdata-sections -Wl,--gc-sections -lcap -O3 -z noexecstack -z now -fstack-protector-all -fPIE container.c -o container ./libcap.a
        strip container
install :all
        install -m 777 container ${PREFIX}/bin/container
clean :
        rm container||true
        rm libcap.a||true
help :
        @printf "\033[1;38;2;254;228;208mUsage:\n"
        @echo "  make all        :compile"
        @echo "  make install    :make all and install container to \$$PREFIX"
        @echo "  make static     :static compile"
        @echo "  make staticfail :static compile,fix errors"
        @echo "  make no         :compile without optimizations"
        @echo "  make clean      :clean"
        @echo "Dependent libraries:"
        @echo "  libc-client-static,libcap-static"
        @printf "If you got errors like \`undefined symbol: cap_drop_bound\` or \`undefined reference to \`cap_set_flag' when using static compile,please copy your \`libcap.a\` into current directory and use \`make staticfail\` instead\n\033[0m"

一个chroot命令的简单实现:

在群里吹水时写的,没啥大用但舍不得删了,就放在这里吧:
头文件依然遵循共享原则(懒死猫猫算了)

int main(int argc,char **argv){
  if (getuid() != 0){
    fprintf(stderr,"\033[31mError: this program should be run with root privileges !\033[0m\n");
    exit(1);
  }
  if (argc <= 1){
    fprintf(stderr,"\033[31mError: too few arguments !\033[0m\n");
    exit(1);
  }
  char *ld_preload=getenv("LD_PRELOAD");
  if(ld_preload != NULL){
    fprintf(stderr,"\033[31mError: please unset $LD_PRELOAD before running this program or use su -c `COMMAND` to run.\033[0m\n");
    exit(1);
  }
  char *container_dir=argv[1];
  char *login[1024]={0};
  if (argc==2){
    login[0]="/bin/su";
    login[1]="-";
    login[2]=NULL;
  }else{
    int login_arg=0;
    for (int arg=2;arg<argc;arg++){
      login_arg=arg-2;
      login[login_arg]=argv[arg];
    }
    login_arg+=1;
    login[login_arg]=NULL;
  }
  DIR *direxist;
  if((direxist=opendir(container_dir)) == NULL){
    fprintf(stderr,"\033[31mError: container directory does not exist !\033[0m\n");
    exit(1);
  }else{
    closedir(direxist);
  }
  chroot(container_dir);
  if (execv(login[0],login) == -1){
    fprintf(stderr,"\033[31mFailed to execute `/bin/su`\n");
    fprintf(stderr,"execv() returned: %d\n",errno);
    fprintf(stderr,"error reason: %s\033[0m\n",strerror(errno));
    exit(1);
  }
}

本不想声明著作权的,但又担心有人偷猫猫辛苦写出来的东西。

本文著作权归Moe-hacker所有

copyright (©) 2022 Moe-hacker

标签:容器,cap,container,drop,bound,从零开始,Linux,include,CAP
From: https://www.cnblogs.com/Moe-hacker/p/16953879.html

相关文章

  • 浅谈Linux容器安全:chroot,capability与namespace技术
    作者只是个萌新,大佬轻喷。文章最终确定以时间顺序浅谈Linux容器安全原理。安全原理相关知识网上已经有很多了,咱通过几个具体攻击实例来讲讲它们的真实作用。演示均在猫......
  • Linux Debian11使用Podman安装DVWA靶场环境
    一、DVWA靶场环境简介​1.DVWA一个用来进行安全脆弱性鉴定的PHP/MySQLWeb应用,旨在为安全专业人员测试自己的专业技能和工具提供合法的环境,帮助web开发者更好的理解web应用......
  • Spring源码-03-容器创建
    Spring源码-03-容器创建注解Bean方式publicclassAnnotationCtxMain02{ publicstaticvoidmain(String[]args){ newAnnotationConfigApplicationContext(MyCf......
  • Spring源码-02-Bean容器
    Spring源码-02-Bean容器一类关系二宏观视角......
  • Linux究极服务部署
    @目录环境准备ftp服务测试验证nfs服务测试验证smb服务测试验证www服务测试验证mail服务测试验证dhcp服务测试验证环境准备第一步,将下载的虚拟机文件上传到VMware中点......
  • linux之vim
     概述复制粘贴是文本编辑最常用的功能,但是在vim中复制粘贴还是有点麻烦的,有一点学习成本。本文总结了使用vim复制粘贴的典型场景和使用方法,希望对读者有帮助。vim......
  • Linux 文件基本属性
    Linux系统是一种典型的多用户系统,不同的用户处于不同的地位,拥有不同的权限。为了保护系统的安全性,Linux系统对不同的用户访问同一文件(包括目录文件)的权限做了不同的规定。......
  • LINUX一些命令
    linux常用的指令ls查看目录中的文件cd/home进入‘/home’目录;cd..返回上一级目录;cd../..返回上两级目录mkdirdir1创建一个叫做‘dir1’的目录rmdirdir......
  • linux基本
    linux没有错误就代表操作成功table按键自动补全cd进入cd..退出ls列出目录,可以组合使用-a查看全部文件,包括隐藏文件-l列出所有文件包括权限没有隐藏文件pwd......
  • Linux 小技巧
    /*每日一更新*//*从今天开始每天给大家提供一个小技巧,方便大家学习和LINUX知识!*//*以命令,系统管理,小技巧为主*/1.按内存从大到小排列进程:  ps-eo"%C:%p:%z:......