集训(三)-ezvm
0x01 题目来源
暑期训练
0x02 思路
vm保护,在逆向中,关键点主要有:vm_init(初始化),vm_start(虚拟机的启动入口),opcode(操作码),dispatcher(解释器,根据操作码进行操作)
进入,查看反汇编
经典switch语句,那么byte_140004000就是储存操作码的数组
但是观察switch与普通的vm不同的是,没有发现指针型或整形变量来替代寄存器,所以寻找类似寄存器的存在.
观察后发现character很可疑,"*(&)",类似于数组的操作方式(地址+偏移量),同时每次都是对其进行操作,与寄存器操作相同,更改类型为数组,改名reg(即为寄存器)
接下来就是读取指令
example push指令
case==1 时,将reg的值传入参数,判断变量是否为255(overflow一眼模拟栈操作),将某个变量转为char*类型,将v2进行"*4"操作,又是"地址+偏移量"的格式,即stack[n]=reg
example cmp指令
if语句,模拟cmp指令,改变v2(相当于汇编指令中的标志符),来为下面的jz以及jnz提供判断
0x03 解题
根据线性扫描来输出整个vm进行的操作(即完整输出指令,不进行跳转)
由于本题中跳转是根据当前地址跳转的,所以输出后还要进行运算,所以在编写时建议标记当前的地址
0x04 转换
#include <iostream>
unsigned char code[] =
{
9, 1, 1, 9, 2, 2, 18, 1, 20, 1,
12, 0, 2, 14, 11, 17, 11, 0, 8, 12,
0, 2, 14, 238, 13, 245, 18, 1, 20, 1,
1, 0, 2, 2, 18, 1, 20, 1, 1, 0,
2, 3, 18, 1, 20, 1, 1, 0, 2, 4,
9, 5, 5, 20, 5, 18, 1, 20, 1, 1,
0, 12, 5, 2, 14, 2, 13, 241, 18, 1,
20, 1, 1, 0, 2, 5, 18, 1, 20, 1,
1, 0, 2, 6, 18, 1, 20, 1, 1, 0,
2, 7, 18, 1, 9, 1, 1, 1, 0, 2,
8, 9, 2, 2, 22, 13, 4, 0, 0, 0,
9, 9, 9, 9, 11, 11, 9, 12, 12, 16,
9, 10, 10, 12, 9, 10, 14, 7, 20, 10,
10, 0, 8, 13, 244, 8, 11, 0, 20, 9,
12, 9, 13, 14, 2, 13, 228, 9, 9, 9,
16, 9, 10, 10, 12, 9, 10, 14, 7, 20,
10, 10, 0, 8, 13, 244, 8, 12, 0, 20,
9, 12, 9, 13, 14, 2, 13, 228, 9, 14,
14, 9, 15, 15, 3, 15, 3, 1, 12, 2,
9, 10, 9, 4, 3, 9, 5, 1, 12, 2,
10, 3, 10, 15, 9, 9, 10, 1, 12, 2,
10, 11, 10, 5, 3, 10, 6, 9, 9, 10,
3, 11, 9, 1, 11, 2, 9, 10, 9, 4,
3, 9, 7, 1, 11, 2, 10, 3, 10, 15,
9, 9, 10, 1, 11, 2, 10, 11, 10, 5,
3, 10, 8, 9, 9, 10, 3, 12, 9, 20,
14, 12, 14, 4, 14, 2, 13, 172, 2, 0,
12, 0, 11, 15, 16, 2, 0, 12, 0, 12,
15, 9, 20, 2, 12, 2, 13, 14, 7, 23,
110, 0, 0, 0, 24, 0, 24, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
int main() {
using namespace std;
int pc=0;int v2;
unsigned int reg[64] = {};
while (1) {
if (code[pc]) {
switch ( code[pc] )
{
case 1:
printf("pc=%d push(reg[%d])\n",pc,code[pc + 1]);
pc += 2;
continue;
case 2:
printf("pc=%d pop(reg[%d])\n",pc,code[pc + 1]);
pc += 2;
continue;
case 3:
printf("pc=%d reg[%d]+=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 4:
printf("pc=%d reg[%d]-=reg[%d]\n" ,pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 5:
printf("pc=%d reg[%d]*=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 6:
printf("pc=%d reg[%d]%%=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 7:
printf("pc=%d reg[%d]&=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 8:
printf("pc=%d reg[%d]|=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 9:
printf("pc=%d reg[%d]^=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 10:
printf("pc=%d reg[%d]<<=%d\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 11:
printf("pc=%d reg[%d]>>=%d\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 12:
v2 = 1;
printf("pc=%d cmp reg[%d] reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
pc += 3;
continue;
case 13:
printf("pc=%d pc+=%d\n",pc,(char)code[pc + 1] + 2);
pc += 2;
continue;
case 14:
printf("pc=%d if equal pc+=%d\n",pc,(char)code[pc+1]+2);
pc += 2;
continue;
case 15:
printf("pc=%d if not equal pc+=%d\n",pc,(char)code[pc+1]+2);
pc += 2;
continue;
case 16:
printf("pc=%d rge[0] = getchar()\n",pc);
++pc;
continue;
case 17:
printf("pc=%d putchar(reg[0])\n",pc);
++pc;
continue;
case 18:
printf("pc=%d reg[0] = aPlzShowMeYourF[reg[%d]]\n",pc,code[pc + 1]);
pc += 2;
continue;
case 19:
printf("pc=%d aPlzShowMeYourF[reg[%d]] = reg[0]\n",pc,code[pc + 1]);
pc += 2;
continue;
case 20:
printf("pc=%d ++reg[%d]\n",pc,code[pc + 1]);
pc += 2;
continue;
case 21:
printf("pc=%d --reg[%d]\n",pc,code[pc + 1]);
pc += 2;
continue;
case 22:
printf("pc=%d reg[%d] = *(_DWORD *)&%d \n",pc,code[pc + 1],code[pc + 2]);
pc += 6;
continue;
case 23:
printf("pc=%d pc = *(_DWORD *)&%d\n",pc,code[pc + 1]);
pc += 5;
continue;
case 24:
printf("pc=%d return %d\n",pc, code[pc + 1]);
pc += 2;
break;
default:
return 0;
}
}
}
return 0;
}
//最终的输出存在问题,在输出return 1时地址对不上,反正其他的都对我就不管了~
0x05 寻找规律
在解释器转换完毕后还要对输出的一大堆汇编指令寻找规律,从而理解程序的输出,输入,以及加密算法,最终达到解密的目的
贴两个关键的
input 实现
pc=123 cmp reg[9] reg[10]
pc=126 if equal pc+=9 //jmp 135
pc=128 ++reg[10] //reg10==1
pc=130 reg[0]<<=8 //reg0==0
pc=133 pc+=-10
pc=135 reg[11]|=reg[0] //第一个字符给reg11的低位
pc=138 ++reg[9]
pc=140 cmp reg[9] reg[13] //reg13==4
pc=143 if equal pc+=4
pc=145 pc+=-26 //jmp 119
//先读取4个字节,把flag的第一位赋给reg11的低位,以此类推reg11存储的是flag的1-4字节(由低位到高位)
crypto 实现
pc=178 reg[14]^=reg[14] //reg14=0
pc=181 reg[15]^=reg[15]
pc=184 reg[15]+=reg[3] //reg15=-1640531527
pc=187 push(reg[12])
pc=189 pop(reg[9]) //reg9=reg12(后四个字符)
pc=191 reg[9]<<=4
pc=194 reg[9]+=reg[5] //reg9<<=4,reg9+=1406675993
pc=197 push(reg[12])
pc=199 pop(reg[10]) //reg10=reg12(后四个字符)
pc=201 reg[10]+=reg[15] //reg10-=1640531527
pc=204 reg[9]^=reg[10] //reg9^=reg10
pc=207 push(reg[12])
pc=209 pop(reg[10])
pc=211 reg[10]>>=5 //reg10=reg12,reg10>>5
pc=214 reg[10]+=reg[6] //reg10+=816345593
pc=217 reg[9]^=reg[10] //reg9^=reg10
pc=220 reg[11]+=reg[9] //reg11+=reg9
//对前四个字节进行加密,其中的reg12来自于上一次进行加密
crypto中的循环
pc=261 cmp reg[14] reg[4] //reg4=32 循环32次
pc=264 if equal pc+=4
pc=266 pc+=-82
最后的判断
pc=270 cmp reg[0] reg[11]
pc=273 if not equal pc+=18
pc=275 pop(reg[0]) //reg0=-1599809085
pc=277 cmp reg[0] reg[12]
pc=280 if not equal pc+=11
pc=282 ++reg[2]
pc=284 cmp reg[2] reg[13]
//轮流出栈比较加密后的前八个字节
0x06 EXP
#include <iostream>
int num[]={-1433586645,89260403,-1158616304,714172226,-768283605,-1059298339,-1599809085,-1667336015};
int main() {
using namespace std;
int i=7;
while (i>0) {
int e=0;
for (int k=0;k<32;k++) {
e-=1640531527;
}
for (int j=0;j<32;j++) {
num[i-1]-=((((unsigned int)num[i])>>5)+1538544130)^(num[i]+e)^((num[i]<<4)+473825587);
num[i]-=((((unsigned int)num[i-1])>>5)+816345593)^(num[i-1]+e)^((num[i-1]<<4)+1406675993);
e+=1640531527;
}
i-=2;
}
printf(reinterpret_cast<char *>(num));
return 0;
}
最后输出的flag是四字节一个单位,倒置,详细自寻查看,可以通过python进行简单转换
str = "VM?}mp13y_s13Al1_a_RT_1t{1snflag"
fs = [str[i * 4:i * 4 + 4] for i in range(0, len(str) // 4)]
print("".join(fs[::-1]))
魏祖,古希腊掌管python脚本的神!~
0x07 总结
收获很多的一道题
1.拷贝数据的时候少了一个3导致最终的flag乱码(迫害了别人一个小时时间,我要成为cv糕手!)
2.更新了对于vm的印象,并非有变量来直接替代寄存器,其实数组也是一种很有效的方式
3.在进行reverse时,使用线性扫描,并且在跳转时要考虑自身指令的长度
4.最后位运算的加密也让人眼前一亮
5.对于题中的变量,一定要注意数据格式,如DWORD的类型不能按照char类型进行导出
综上,这道题是一道很有意义的vm保护,难度以及知识点把握的都很恰当(对我这样的新人来说),受益匪浅.