最近在学虚拟机逆向的题目,上学期就触碰到的知识点,但是对于当时的我来说太难了,这学期遇到的比赛还是有虚拟机的题目的,而且学长在校内比赛很喜欢出,而且我每次都不会,于是决心好好补一下。先从简单的做起。
IDA打开,找到主函数,vm_operad应该就是虚拟机的函数,这个v4又传入了这个函数,可能是opcode,我们点进该函数当中。
这里有个while的循环语句,switch语句,那肯定就是虚拟机的调度器了,这里的a1就是v4,也就是opcode,而v9作为opcode数组的索引,再结合后面代码它的变化,应该就相当于汇编语言当中的eip寄存器。当eip大于a2的时候,这个函数就会结束,而a2在主函数传入的是数字114,这应该是opcode的大小,我们回到主函数去看一下opcode。
但是大小不止114,其中有很多零。而且每两个数字之间都刚好有三个零,我们Shift+e导出来,然后把零去掉看看。
还要去掉其中的255,发现刚刚好大小是114,我们就得到了opcode,关键的一步!
然后再分析一下虚拟机函数
int __cdecl vm_operad(int *opcode, int a2)
{
int result; // eax
char Str[200]; // [esp+13h] [ebp-E5h] BYREF
//这里Str相当于一个开辟的内存空间
char v4; // [esp+DBh] [ebp-1Dh]
int v5; // [esp+DCh] [ebp-1Ch]
int v6; // [esp+E0h] [ebp-18h]
int v7; // [esp+E4h] [ebp-14h]
int v8; // [esp+E8h] [ebp-10h]
int eip_0; // [esp+ECh] [ebp-Ch]
eip_0 = 0;
v8 = 0;
v7 = 0;
v6 = 0;
v5 = 0;
//五个重要的变量
while ( 1 )
{
result = eip_0;
if ( eip_0 >= a2 )
//当eip的值大于opcode的大小,即执行完虚拟机的调度器操作后,结束函数
return result;
switch ( opcode[eip_0] )
{
case 1:
Str[v6 + 100] = v4;
++eip_0;
++v6;
++v8;
break;
//将Str的内存分成了两块,一块存储我们的输入,一块用于存储数据运算后得到的v4
//唯一一个对v8和v6进行运算的情况。
case 2:
v4 = opcode[eip_0 + 1] + Str[v8];
eip_0 += 2;
break;
case 3:
v4 = Str[v8] - LOBYTE(opcode[eip_0 + 1]);
eip_0 += 2;
break;
case 4:
v4 = opcode[eip_0 + 1] ^ Str[v8];
eip_0 += 2;
break;
case 5:
v4 = opcode[eip_0 + 1] * Str[v8];
eip_0 += 2;
break;
#上面的case 2 到case 5 都是将opcode中的当前取值的下一位和我们的输入进行运算处理,然后赋值给v4
case 6:
++eip_0;
break;
//没啥用的
case 7:
if ( Str[v7 + 100] != opcode[eip_0 + 1] )
{
printf("what a shame...");
exit(0);
}
++v7;
eip_0 += 2;
break;
//最后的比较,这里
case 8:
Str[v5] = v4;
++eip_0;
++v5;
break;
//我们存储在内存当中的输入经过case 2 到case 5中某个处理之后,发生改变。
//唯一一个对v5进行运算的情况
case 10:
read(Str);
++eip_0;
break;
//读取一个长度为15的输入,可以点进read函数看
case 11:
v4 = Str[v8] - 1;
++eip_0;
break;
case 12:
v4 = Str[v8] + 1;
++eip_0;
break;
//case 11到case 12也是对数据进行运算操作
default:
continue;
}
}
}
这里我们其实思路已经比较明了了,上面的opcode,所有的7后面的数据就是要密文,就是要比较的数据,第一个数据是34。我们其实可以尝试一下手搓flag第一位。
读取输入后,eip寄存器加1,当前opcode为4,进入case 4,此时v4为flag的第一位与16进行异或运算,然后eip寄存器加2,当前opcode为8,进入case 8,然后将v4存入Str[v5],此时v5为0,也就是Str[0]所以就是覆盖Str的第一位,这个第一位往后好像一开始存的是我们的flag。执行完case 8之后,v5=1,然后进入case 3,其实就是v4 = v4- opcode[eip_0 + 1],v4=v4 – 5。然后直接进入case 1,将第一位flag处理完后,也就是v4,放入另外一个内存空间当中。
那flag[0]=(34+5)^16=55,这是Ascii编号,转化成字符就是7,我们可以一直这样动态调试手搓。
得到15个算法是
0x22 = (flag[0] ^ 0xa) - 5
0x3f = (flag[1] ^ 0x20) * 3
0x34 = (flag[2] - 2) - 1
0x32 = (flag[3] + 1) ^ 4
0x72 = (flag[4] * 3) - 0x21
0x33 = (flag[5] - 1) - 1
0x18 = (flag[6] ^ 9) - 0x20
0xa7 = (flag[7] + 0x51) ^ 0x24
0x31 = (flag[8] +1 ) - 1
0xf1 = (flag[9] * 2) + 0x25
0x28 = (flag[10] + 0x36) ^ 0x41
0x84 = (flag[11] + 0x20) * 1
0xc1 = (flag[12] *3) + 0x25
0x1e = (flag[13] ^ 9) - 0x20
0x7a = (flag[14] + 0x41) + 1
然后写个脚本
b=[34,63,52,50,114,51,24,167,49,241,40,132,193,30,122]
flag=[0]*15
flag[0]=(b[0]+5)^0x10
flag[1]=(b[1]//3)^0x20
flag[2]=(b[2]+3)
flag[3]=(b[3]^4)-1
flag[4]=(b[4]+0x21)//3
flag[5]=b[5]+2
flag[6]=(b[6]+0x20)^9
flag[7]=(b[7]^0x24)-0x51
flag[8]=b[8]
flag[9]=(b[9]-0x25)//2
flag[10]=(b[10]^0x41)-0x36
flag[11]=(b[11]-0x20)
flag[12]=(b[12]-0x25)//3
flag[13]=(b[13]+0x20)^9
flag[14]=(b[14]-1)-0x41
result = ""
for i in range(len(flag)):
result += chr(flag[i])
print(result)
#输出757515121f3d478
所以flag就是flag{757515121f3d478}
这题用angr也是可以跑的,脚本如下
import angr
import sys
def Go():
path_to_binary = "C:\\Users\\Daki\Desktop\\signal.exe"
project = angr.Project(path_to_binary, auto_load_libs=False)
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state)
print_good_address = 0x4017A5
simulation.explore(find=print_good_address)
if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno()) # 大概意思是dump出输入
print(solution)
else:
raise Exception('Could not find the solution')
if __name__ == "__main__":
Go()
#输出b'757515121f3d478\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
看了其他大佬的Writeup,看到也有正向爆破的方法,这个应该也是不错的方法,可以学习一下。
这题不是算很常规的虚拟机逆向的题目,但是能让我们明白虚拟机的结构,是干啥的,怎么对这类题目进行分析。