章节大纲

  • 本实验在 SEEDUbuntu20.04 VM 中测试可行。你可以在本页面右端选择虚拟机版本为 SEEDUbuntu20.04,点击“创建虚拟机”来获取虚拟机平台的临时用户名与密码,登录虚拟机平台即可获得一台预先构建好的 SEEDUbuntu20.04 VM,该虚拟机以及用户名密码将在开启 24 小时后自动销毁。
     
    你也可以在其他 VM、物理机器以及云端 VM 上自行配置环境进行实验,但我们不保证实验能在其他 VM 下成功。实验所需的文件可根据你的芯片类型从下方下载,解压后会得到一个名为 Labsetup 的文件夹,该文件夹内包含了完成本实验所需的所有文件。
      • 现代操作系统使用地址空间随机化来随机化堆和栈的起始地址。这使得猜测确切地址变得困难,而猜测地址是格式化字符串攻击的关键步骤之一。为了简化本实验的任务,我们使用以下命令关闭地址随机化:

        $ sudo sysctl -w kernel.randomize_va_space=0
      • 本实验中使用的被攻击程序文件名为 format.c,可以在 server-code 文件夹中找到。这个程序有一个格式化字符串漏洞,你的任务是利用这个漏洞。下面列出的代码去除了非必要的信息,所以它与你从实验设置文件中得到的不一样。
         
        // 易受攻击的程序 format.c (去除了非必要信息)
        unsigned int  target = 0x11223344;
        char *secret = "A secret message\n";
        
        void myprintf(char *msg)
        {
            // 此行有一个格式化字符串漏洞
            printf(msg);
        }
        
        int main(int argc, char **argv)
        {
            char buf[1500];
            int length = fread(buf, sizeof(char), 1500, stdin);
            printf("Input size: %d\n", length);
        
            myprintf(buf);
        
            return 1;
        }
         
        上述程序从标准输入中读取数据, 然后将数据传递给 myprintf(),后者调用 printf() 打印数据。输入数据被送入 printf() 函数的方式是不安全的,会导致格式化字符串漏洞。我们将利用这个漏洞。
         
        程序将在具有 root 权限的服务器上运行,其标准输入将被重定向到服务器与远程用户之间的 TCP 连接。因此,程序实际上是从远程用户处获取数据的。如果用户可以利用这个漏洞,他们可以造成损害。
         
        在 server-code 文件夹中,你可以找到一个名为 server.c 的程序。 这是服务器的主要入口点。它监听端口 9090。 当它接收到 TCP 连接时,它调用 format 程序,并将TCP连接设置为 format 程序的标准输入。这样,当 format 从 stdin 读取数据时,它实际上从 TCP 连接中读取,即数据由 TCP 客户端的用户提供,你不需要阅读 server 的源代码。
      •  
        我们将编译 format 程序为 32 位和 64 位二进制文件(对于 Apple Silicon 机器,我们只编译成 64 位二进制文件)。我们预构建的 Ubuntu 20.04 VM 是 64 位 VM,但它仍然支持 32 位二进制文件。我们所需要做的就是在 gcc 命令中使用 -m32 选项。对于32位编译,我们还使用 -static 生成静态链接的二进制文件,它是自包含的,不依赖于任何动态库,因为 32 位动态库没有安装在我们的容器中。
         
        编译命令已在 Makefile 中提供。要编译代码,你需要输入 make 来执行这些命令。编译完成后,我们需要将二进制文件复制到 fmt-containers 文件夹中,以便它们可以被容器使用。以下命令执行编译和安装。
         
        $ make
        $ make install
         
        在编译过程中,你将看到一个警告消息。这个警告是由 gcc 编译器针对格式化字符串漏洞实现的防范措施,我们现在可以忽略这个警告。
         
        format.c: In function 'myprintf':
        format.c:33:5: warning: format not a string literal and no format arguments
                                [-Wformat-security]
           33 |     printf(msg);
              |     ^~~~~~
         
        需要指出的是,程序需要使用 "-z execstack" 选项编译,这允许栈可执行。我们的最终目标是将代码注入到服务器程序的栈中,然后触发代码。非可执行栈是对抗基于栈的代码注入攻击的对策,但是它可以使用 return-to-libc 技术被破解,这在另一个 SEED 实验中有详细介绍。在这个实验中,为了简单起见,我们禁用了这个可被破解的对策。
         
      • 解压 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 文件夹中。