题源:nctf2018
逆向+虚拟机鬼畜题,对萌新来说是非常好的一道虚拟机入门题
如果缺少虚拟机相关知识先看一下这篇文章:https://www.52pojie.cn/forum.php?mod=viewthread&tid=713219
首先我们用LINUX执行这个文件,提示要我们输入字符,随便输几个之后输入文本结束符号。我们发现程序没办法继续运行下去了。于是扔进ida开始调试。
进入main函数,我们发现系统在输出提示字符串之后进入了一个函数,点进这个函数进去看,发现是一大串赋值和switch语句
根据刚才这篇文章中的提示,很明显这是一个虚拟机结构。因此我们第一步先找出如图所示的几个变量,并且搞懂它们的含义。如下图所示
如果怕自己判断错误这些变量,或者看得不是很懂的,可以先动态调试一下,然后再分析。
然后对switch语句进行分析。容易知道每一个case语句都对应了一个handler。因此我们逐条进行分析。如果分析起来比较困难的话可以选择伪代码按下tab键显示汇编语言,也能大致理解虚拟过程的汇编代码。我们就可以写个程序还原出虚拟机的执行过程:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int EIP[]={
0x8,0x1,0x0,0x8,0x3,0x46,0xe,0x15,0xa,0x1,
0x9,0x2,0xb,0xa,0x1,0xa,0x2,0x9,0x1,0x11,
0x1,0xd,0x1,0x3,0xf,0x8,0x8,0x1,0x0,0x8,0x3,
0x47,0xe,0x46,0xa,0x1,0x1a,0x2,0x6,0x1d,
0x1,0x4,0x14,0x2,0x1,0x19,0x1,0x2,0x1b,
0x1,0x1,0x1d,0x1,0x6e,0x13,0x1,0x63,0x15,
0x1,0x74,0x13,0x1,0x66,0x1c,0x2,0x1,0x9,
0x1,0x11,0x1,0xd,0x1,0x3,0xf,0x22,0x64
};
int main(){
int i=0;
while(i<80){
if(EIP[i]==8){
printf("%d mov R%d %d\n",i,EIP[i+1]-1,EIP[i+2]);
i+=3;
}
if(EIP[i]==9){
printf("%d pop R%d\n",i,EIP[i+1]-1);
i+=2;
}
if(EIP[i]==10){
printf("%d push R%d\n",i,EIP[i+1]-1);
i+=2;
}
if(EIP[i]==11){
printf("%d R0=getchar()\n",i);
i++;
}
if(EIP[i]==12){
printf("%d R0=puchar()\n",i);
i++;
}
if(EIP[i]==13){
printf("%d cmp R%d R%d\n",i,EIP[i+1]-1,EIP[i+2]-1);
printf(" jnz %d\n",i+3);
printf(" mov a, 80\n");
i+=3;
}
if(EIP[i]==14){
printf("%d jmp %d\n",i,EIP[i+1]);
i+=2;
}
if(EIP[i]==15){
printf("%d and a, 80\n",i);
printf(" test a a\n");
printf(" jnz %d\n",EIP[i+1]);
i+=2;
}
if(EIP[i]==16){
printf("%d and a, 80\n",i);
printf(" test a a\n");
printf(" jz %d\n",EIP[i+1]);
i+=2;
}
if(EIP[i]==17){
printf("%d inc R%d\n",i,EIP[i+1]-1);
i+=2;
}
if(EIP[i]==18){
printf("%d inc R%d\n",i,EIP[i+1]-1);
i+=2;
}
if(EIP[i]==19){
printf("%d add R%d, %d\n",i,EIP[i+1]-1,EIP[i+2]);
i+=3;
}
if(EIP[i]==20){
printf("%d sub R%d, R%d\n",i,EIP[i+1]-1,EIP[i+2]-1);
i+=3;
}
if(EIP[i]==21){
printf("%d xor R%d, %d\n",i,EIP[i+1]-1,EIP[i+2]);
i+=3;
}
if(EIP[i]==22){
printf("%d and R%d, R%d\n",i,EIP[i+1]-1,EIP[i+2]-1);
i+=3;
}
if(EIP[i]==23){
printf("%d or R%d, R%d\n",i,EIP[i+1]-1,EIP[i+2]-1);
i+=3;
}
if(EIP[i]==25){
printf("%d mov R%d, R%d\n",i,EIP[i+1]-1,EIP[i+2]-1);
i+=3;
}
if(EIP[i]==26){
printf("%d mov R%d, R%d\n",i,EIP[i+1]-1,EIP[i+2]-1);
i+=3;
}
if(EIP[i]==27){
printf("%d mov R%d, [R%d]\n",i,EIP[i+1]-1,EIP[i+2]-1);
i+=3;
}
if(EIP[i]==28){
printf("%d mov [R%d], R%d\n",i,EIP[i+1]-1,EIP[i+2]-1);
i+=3;
}
if(EIP[i]==29){
printf("%d mul R%d, %d\n",i,EIP[i+1]-1,EIP[i+2]);
i+=3;
}
}
}
程序运行之后得到下面的东西:
0 mov R0 0
3 mov R2 70
6 jmp 21
8 push R0
10 pop R1
12 R0=getchar()
13 push R0
15 push R1
17 pop R0
19 inc R0
21 cmp R0,R2
jnz 24
mov a, 80
24 and a, 80
test a a
jnz 8
26 mov R0 0
29 mov R2 71
32 jmp 70
34 push R0
36 mov R1, 5
39 mul R0, 4
42 sub R1, R0
45 mov R0, R1
48 mov R0, [R0]
51 mul R0, 110
54 add R0, 99
57 xor R0, 116
60 add R0, 102
63 mov [R1], R0
66 pop R0
68 inc R0
70 cmp R0 R2
jnz 73
mov a, 80
73 and a, 80
test a a
jnz 34
这就是被虚拟机保护的那一串关键代码。翻译起来比较简单。大致就是输入一个长度为70的字符串s。然后对于s[i],加密过程为((s[i]*110)+99)^116+102,得到的东西在switch中case100的情况进行比较。找到比较的字节内容为dword_6020A0,然后逆序还原即可
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
int s[]={
0x36D3, 0x2AFF, 0x2ACB, 0x2B95, 0x2B95, 0x2B95, 0x169F, 0x186D,
0x18D7, 0x1611, 0x18D7, 0x2B95, 0x2C23, 0x2CA9, 0x1611, 0x1611,
0x18D7, 0x2AFF, 0x1849, 0x18FB, 0x2ACB, 0x2A71, 0x1735, 0x18D7,
0x1611, 0x2ACB, 0x15DD, 0x18D7, 0x2C23, 0x169F, 0x15DD, 0x2B95,
0x169F, 0x156B, 0x186D, 0x2AFF, 0x1611, 0x1611, 0x15DD, 0x2AFF,
0x2C23, 0x2ACB, 0x15DD, 0x15DD, 0x186D, 0x1849, 0x2B95, 0x156B,
0x1735, 0x18FB, 0x18FB, 0x2A71, 0x2AFF, 0x1735, 0x2C23, 0x15DD,
0x18D7, 0x2A71, 0x18D7, 0x18D7, 0x2C23, 0x2AFF, 0x156B, 0x2C23,
0x169F, 0x35AF, 0x2CA9, 0x32B5, 0x2AFF, 0x3039
};
int c;
int main(){
for(int i=69;i>=0;i--){
c=(((s[i]-102)^116)-99)/110;
putchar((char)c);
}
}
得到flag nctf{3e1ce77b70e4cb9941d6800aec022c813d03e70a274ba96c722fed72783dddac}