SST-Lab4:Format String
下载labsetup.zip:SEED Project (seedsecuritylabs.org)
2.1
关闭地址空间随机化,简化实验难度
2.2
编译。Makefile中已经提供了编译命令。在编译过程中,将看到一条警告消息。 此警告是由gcc编译器针对格式字符串漏洞实施的对策生成的。
2.3
进入 Labsetup文件夹,使用docker-compose.yml文件设置实验室环境。容器fmt-server-1(server-10.9.0.5),fmt-server-2(server-10.9.0.6)创建完成。
Task 1
正常输入:
接收到6个字符并输出笑脸,服务器没有崩溃
恶意输入:
格式规定符是%s,printf()函数把获得到的值视为一个地址,并打印出该地址处的字符串。而栈帧保存的值并不都是合法的地址,它们可能是0(null指针)、指向受保护内存的地址或者没有映射物理地址的虚拟地址。当程序试图从一个非法地址获取数据时,该程序将崩溃。
输出结果显示:接收%s这3个字符,然鹅没有后续输出和笑脸,说明程序崩溃
2打印出服务器程序的内存
2A
修改build_string.py,编写一个生成字符串的python程序task2A.py。运行它,使字符串(0x20210806的字节形式和100个“%x|”)保存在badfile2A中。
(%x:printf将参数视为unsigned int类型(4字节),并用十六进制的格式打印出来。当printf()遇到%x时,它打印出va_list指针指向的数,并将va_list推进4个字节)。
向服务器server-10.9.0.5提供输入,输入为badfile2A的内容。得到服务器打印出的内容。
将服务器输出中20210806|及之前根据%x打印出的字符串复制到一个文件count.txt中。
通过grep相关命令得到|的个数为64。即可以得到需要64个 %x 格式说明符才能让服务器程序打印出输入的前四个字节0x20210806(63个%x将va_list指针移动至输入的起始地址)。
2B
根据服务器的输出The secret message’s address: 0x080b4008,得到秘密信息的地址为0x080b4008。修改build_string.py,编写一个生成字符串的python程序task2B.py。运行它,使字符串(由0x080b4008的字节形式,63个”%x”和”\nsecret message:%s”构成)保存在badfileB中。
向服务器server-10.9.0.5提供输入,输入为badfile2B的内容。得到服务器打印出的内容。故秘密信息(secret message)为A secret message
3修改服务器程序的内存
此任务的目标是修改服务器程序中定义的目标变量的值(我们将继续使用 10.9.0.5)。target 的原始值为 0x11223344。假设这个变量拥有一个重要的值,它会影响程序的控制流程。如果远程攻击者可以改变它的值,他们就可以改变这个程序的行为。我们有三个子任务。
3A改为不同的值
根据服务器的输出The target variable’s address: 0x080e5068,得到目标变量(target variable)的地址为0x080e5068。编写一个生成字符串的python程序task3A.py。运行它,使字符串(0x080e5068的字节形式、63个”.%x”、1个”%n”、结尾”\n”)保存在badfile3A中。
(当printf()遇到%n时,它会获取va_list指针指向的值,将视该值为一个内存地址,然后将数据(已打印出的字符的个数)写入该地址)。
向服务器server-10.9.0.5提供输入,输入为badfile3A的内容。得到服务器打印出的内容。得到目标变量(target variable)的值变为0x00000010c。
为什么是0x10c呢?从输出看出,打印了66*4=264个字符(一行打印66个字符,共打印4行)再加上content开头4字节由小端法插入的number地址,所以264+4=268(0x10c)
至于为什么打印出来很多||...|连在一起,因为中间8个0省略打印
3B
0x5000=20480=4+62*(8+1)+19917+1,故编写一个生成字符串(0x080e5068的字节形式(长度为4个字符)、62个”%.8x|”、1个”%.19917x”、1个”\n”、1个”%n”)的python程序task3B.py。运行它,使该字符串保存在badfile3B中。
(精度修饰符形如”.number“,当应用于整型值时,它控制最少打印多少位字符)
向服务器server-10.9.0.5提供输入,输入为badfile3B的内容。得到服务器打印出的内容。得到目标变量(target variable)的值变为0x00005000。
3C
因为0xAA=170<4*64,故选用%hn(视参数视为2字节字符型数),即每次只修改两个字节的值。根据小端法,目标变量(target variable)从最高两位字节的地址为0x080e506a、最低两位字节的地址为0x080e5068。
故字符串开头为数字0x080e506a+”@@@@”+数字0x080e5068。共12个字符。
(printf()通过格式化字符%x经过”@@@@”才能改变%n对应数据——已打印出的字符的个数,给下一个地址赋更大的值)
0xAABB=43707,43707=12+693*62+729
0xCCDD-0xAABB=8738。
故字符串由0x080e506a的字节形式、”@@@@”、0x080e5068的字节形式、62个”%.693x”、1个”%.729x”、1个”%hn”、1个”%.8738x”、1个”%hn”、结尾”\n”组成。
编写一个生成该字符串的python程序task3C.py。运行它,使该字符串保存在badfile3C中。
向服务器server-10.9.0.5提供输入,输入为badfile3C的内容。得到服务器打印出的内容。得到目标变量(target variable)的值变为0xaabbccdd。
4将恶意代码注入服务器程序
目标是将将myprintf函数返回地址修改为shellcode的入口地址。
执行恶意代码:
修改exploit.py,编写task4.py,再次输入命令到服务器查看The input buffer’s address和Frame Pointer (inside myprintf),按照这两个值用于修改buf_addr和ebp_addr。
将shellcode放在输入的尾部。使用%.numberx移动va_list指针并修改已打印字符的值,.number为精度修饰符。然后选用%hn,单次修改2个字节,将myprint的返回地址(ebp+4)修改为shellcode的入口地址。补全后的python程序task4.py及相关注释如下:
执行程序task4.py,得到badfile。向服务器server-10.9.0.5提供输入,输入为badfile的内容。得到服务器打印出的内容。可以看到,服务器执行了shellcode中预置的命令ls -l。
获取反向shell
使用ifconfig -a 查看攻击者服务器的ip地址是10.9.0.1。
修改task4.py中的32位shellcode_32中的恶意代码,-i表示是一个交互式shell,重定向输入输出和标准错误到tcp连接的10.9.0.1的9090端口,使得在攻击端实现输入和看到输出:
打开攻击端口,使用nc -nv -l 9090对端口9090进行监听。
打开目标端口,运行exploit.py,得到badfile。向服务器server-10.9.0.5提供输入,输入为badfile的内容。
在攻击端可以得到服务器server-10.9.0.5的shell,获取root权限:
5攻击64位服务器程序
新目标是10.9.0.6,它运行64位版本的格式化程序。让我们首先向该服务器发送一条hello消息。我们将看到目标容器打印出以下消息。
修改exploit.py,编写task5.py,输入hello基本命令到服务器查看The input buffer’s address和Frame Pointer (inside myprintf),按照这两个值用于修改buf_addr和ebp_addr。
根据任务2.A的经验,可以得到输入的起始地址是printf()第34个参数的位置。
修改task4.py中的64位shellcode_64中的恶意代码,-i表示是一个交互式shell,重定向输入输出和标准错误到tcp连接的10.9.0.1的9090端口,使得在攻击端实现输入和看到输出:
64 位地址带来的挑战:
对于每个地址(8 个字节),最高的两个字节始终为00,对应Ascii码\0。在攻击中,我们需要将地址放在格式字符串中。 对于 32 位程序,我们可以将地址放在任何地方,因为地址内没有00。 对于64 位程序,我们不能再这样做了。 如果将地址放在格式字符串的中间,当 printf() 解析格式字符串时,它会在遇到\0时停止解析。 基本上,格式字符串中第一个零(\0)之后的任何内容都不会被视为格式字符串的一部分。\0引起的问题与缓冲区溢出攻击不同,在缓冲区溢出攻击中,如果使用 strpcy() ,\0将终止内存复制。
一个有用的技巧:自由移动参数指针
在格式字符串中,我们可以使用%x将参数指针 va_list 移动到下一个可选参数。 我们也可以直接将指针移动到第k个可选参数。这是使用格式字符串的参数字段(以 k$ 的形式)完成的。 以下代码示例使用%3$.20x打印出第 3 个可选参数(数字 3)的值,然后使用%6$n将值写入第 6 个可选参数(变量 var,它的值将变为 20)。 最后,使用 %2$.10x,它将指针移回第二个可选参数(数字 2),并将其打印出来。 可以看到,使用这个方法,我们可以自由地来回移动指针。 此技术对于简化此任务中格式字符串的构造非常有用。
对策:
将shellcode放在输入的尾部。目标是将将myprintf函数返回地址修改为shellcode的入口地址。这里使用%hn一次修改两字节的值,其中返回地址最高两字节的值0x0000无需改变。
因为在64位中shellcode的入口地址是包含0x00的。将地址转换为字节时,00对应的Ascii码为\0。当 printf()解析格式化字符串时,在遇到零(\0)后会停止解析,之后的任何内容都不会被视为格式化字符串的一部分。故myprintf函数返回地址的所在地址的高四字节和低四字节应该放在格式化字符串的后面。
故先用%.numberx修改已打印字符数,然后使用%k$n将指针移动到printf()的第k个参数,这个参数应为要修改的值的地址。
格式化字符串的构成为:
”%.number1x”、“%k1$hn”、“%.number2x”、 “%k2$hn”、“%.number3x”、“%k3$hn”。
分别是所需要的数的大小,和所指向的参数的序号。分别计算。
关于k1、k2、k3的取值:输入最多为1500个字节,故k1、k2、k3各自最多占3个字节。
number1、number2、number3的取值不会超过FFFFFFFF,即4294967295,各自最多占11个字节。
故整个格式化字符串最多为63个字节,占不到8个参数的长度。
对应地将myprint函数的返回地址的0-8位、8-16位、16-24位所在地址,按照数值shellcode的入口地址的0-8位、8-16位、16-24位数值从小到大的顺序放在第42、43和44个参数的位置(34+8=42,34是需要多少个%x才能到修改地址处,8是格式化字符串占的),相对与输入起始地址的字节大小分别为64、72、80。
故k1=64,k2=72,k3=80。
类似于32位,我的exploit.py实现了自动化计算,每次使用只需修改两个服务器输出的地址The input buffer’s address和Frame Pointer (inside myprintf)即可。exploit.py代码如下:
打开攻击端口,使用nc -nv -l 9090对端口9090进行监听。
再打开目标端口,运行task5.c,得到badfile。向服务器server-10.9.0.6提供输入,输入为badfile的内容。
在攻击端口可以得到服务器server-10.9.0.6的shell,获取root权限:
6解决问题
gcc 编译器生成的警告信息,意思是:字符串格式并不是一个常量,而且没有格式化字符串的参数。
修复漏洞
将format.c中printf(msg)更改为printf(“%s”,msg),并重新编译,发现没有警告。
重新建立并开启docker,对32位服务器的target值进行攻击。发现target的值并没有改变,故攻击失败。