章节大纲

  • 本实验在 SEEDUbuntu20.04 VM 中测试可行。你可以在本页面右端选择虚拟机版本为 SEEDUbuntu20.04,点击“创建虚拟机”来获取虚拟机平台的临时用户名与密码,登录虚拟机平台即可获得一台预先构建好的 SEEDUbuntu20.04 VM,该虚拟机以及用户名密码将在开启 24 小时后自动销毁。
     
    你也可以在其他 VM、物理机器以及云端 VM 上自行配置环境进行实验,但我们不保证实验能在其他 VM 下成功。实验所需的文件可从下方下载,解压后会得到一个名为 Labsetup 的文件夹,该文件夹内包含了完成本实验所需的所有文件。
      • 在开始本实验之前,我们需要确保地址随机化防御措施已关闭;否则,攻击将会十分困难。你可以使用以下命令来实现:
         
        $ sudo /sbin/sysctl -w kernel.randomize_va_space=0
      • 本次实验中用于攻击的目标程序名为 stack.c,位于 server-code 文件夹内。该程序具有缓冲区溢出漏洞,你的任务是利用此漏洞获得 root 权限。下面列出的代码去除了部分非关键信息,所以与从实验设置文件获取的内容略有不同。
         
        /* 存在漏洞的程序 stack.c */
        #include <stdlib.h>
        #include <stdio.h>
        #include <string.h>
        
        /* 更改此值将改变堆栈布局 */
        #ifndef BUF_SIZE
        #define BUF_SIZE 100
        #endif
        
        int bof(char *str)
        {
            char buffer[BUF_SIZE];
        
            /* 下面的语句存在缓冲区溢出问题 */
            strcpy(buffer, str);          
            return 1;
        }
        
        void foo(char *str)
        {
            ...
            bof(str);
        }
        
        int main(int argc, char **argv)
        {
            char str[517];
        
            int length = fread(str, sizeof(char), 517, stdin);
            foo(str);
            fprintf(stdout, "==== Returned Properly ====\n");
            return 1;
        }
         
        上述程序存在缓冲区溢出漏洞。它从标准输入读取数据,然后将这些数据复制到 bof() 函数中的另一个缓冲区内。原始输入的最大长度为 517 字节,但 bof() 中的缓冲区只有BUF_SIZE 个字节长(小于 517)。因为 strcpy() 不会检查边界,会发生缓冲区溢出。
         
        该程序将在具有 root 权限的服务器上运行,并将标准输入重定向到服务器与远程用户之间的 TCP 连接。因此,程序实际上是从远程用户处获取数据。如果攻击者能够利用此缓冲区溢出漏洞,他们就可以在服务器上获得 root shell。
         
        要编译上述存在漏洞的程序,需要使用 -fno-stack-protector 和 -z execstack 选项关闭 StackGuard 和非可执行堆栈保护。下面是一个编译命令的例子(环境变量 L1 设定了 stack.c 中的 BUF_SIZE 常量值)
         
        $ gcc -DBUF_SIZE=$(L1) -o stack -z execstack -fno-stack-protector stack.c
         
        我们将把 stack 程序编译成 32 位和 64 位两个二进制文件。我们的预构建的 Ubuntu 20.04 虚拟机是 64 位虚拟机,但仍支持 32 位二进制文件。我们只需要在 gcc 命令中使用 -m32 选项来实现 32 位编译。
         
        对于 32 位编译,我们也使用 -static 生成一个静态链接的可执行文件,该文件是自包含的且不依赖于任何动态库。因为我们的容器中没有安装 32 位动态库。
         
        编译命令已提供在 Makefile 中。要编译代码,请输入 make 来执行这些命令。变量 L1、L2、L3 和 L4 在 Makefile 中被设置,并会在编译过程中使用。
         
        编译完成后,需要将可执行文件复制到 bof-containers 文件夹中以便容器可以使用。以下命令完成编译和安装。
         
        $ make
        $ make install
         
        在 server-code 文件夹中,可以找到名为 server.c 的程序。这是服务器的主要入口点。它侦听端口 9090。当接收到 TCP 连接时,会调用 stack 程序,并将该 TCP 连接作为标准输入提供给 stack 程序。这样,在 stack 读取标准输入数据时,实际上是读取了 TCP 连接上的数据,即这些数据由 TCP 客户端的用户提供。你们不需要阅读 server.c 的源代码。
         
      • 解压 Labsetup 压缩包, 进入 Labsetup 文件夹,然后用 docker-compose.yml 文件安装实验环境。 对这个文件及其包含的所有 Dockerfile 文件中的内容的详细解释都可以在用户手册(注意:如果你在部署容器的过程中发现从官方源下载容器镜像非常慢,可以参考手册中的说明使用当地的镜像服务器)中找到。 如果这是你第一次使用容器设置 SEED 实验环境,那么阅读用户手册非常重要。

        在下面,我们列出了一些与 Docker 和 Compose 相关的常用命令。 由于我们将非常频繁地使用这些命令,因此我们在 .bashrc 文件 (在我们提供的 SEED Ubuntu 20.04 虚拟机中)中为它们创建了别名。

        $ docker-compose build  # 建立容器镜像
        $ docker-compose up     # 启动容器
        $ docker-compose down   # 关闭容器
        
        // 上述 Compose 命令的别名
        $ dcbuild       # docker-compose build 的别名
        $ dcup          # docker-compose up 的别名
        $ dcdown        # docker-compose down 的别名

        所有容器都在后台运行。 要在容器上运行命令,我们通常需要获得容器里的 Shell 。 首先需要使用 docker ps 命令找出容器的 ID , 然后使用 docker exec 在该容器上启动 Shell 。 我们已经在 .bashrc 文件中为这两个命令创建了别名。

        $ dockps        // docker ps --format "{{.ID}}  {{.Names}}" 的别名
        $ docksh <id>   // docker exec -it <id> /bin/bash 的别名
        
        // 下面的例子展示了如何在主机 C 内部得到 Shell
        $ dockps
        b1004832e275  hostA-10.9.0.5
        0af4ea7a3e2e  hostB-10.9.0.6
        9652715c8e0a  hostC-10.9.0.7
        
        $ docksh 96
        root@9652715c8e0a:/#
        
        // 注: 如果一条 docker 命令需要容器 ID,你不需要
        //     输入整个 ID 字符串。只要它们在所有容器当中
        //     是独一无二的,那只输入前几个字符就足够了。

        如果你在设置实验环境时遇到问题,可以尝试从手册的“Miscellaneous Problems”部分中寻找解决方案。

        注意,在运行 docker-compose build 命令构建 Docker 镜像之前,我们需要编译并复制服务器代码到 bof-containers 文件夹中。