VM逆向

VM逆向

技术原理

虚拟机保护是通过开发者自定义的一套opcode,由虚拟机的dispatcher解释执行,从而起到代码混淆、增加逆向难度的技术

VM_start:是对虚拟机的初始化

VM_dispatcher:调度器,解释opcode,并选择相应的函数执行,一般为switch语句,根据地址码判断

VM_code:程序可执行代码形成的操作码

ida定义结构体

Dasctf——EasyVm

一开始有个花指令,是比较常见的永真跳转,先对call指令按u取消定义,将e8改为90,再重新弄成函数就可以了

先找到加密的函数

点进去看看

这里是base64变种,在最后加了一个异或操作,先把脚本写出来

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
#include<stdio.h>
#include<string.h>
char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int findIndex(char c, char b64_table[])
{
for (int i = 0; i < 64; ++i)
{
if (c == b64_table[i])
return i;
}
}
char* base64_decode(char code[], char str[], char b64_table[])
{
char memstr[200] = { 0 };
memcpy(memstr, code, strlen(code));
int len = strlen(code);
/*while (code[len] != '!' && code[len] != 0)
{
len++;
}*/
for (int i = 0, i_ = 0; i < len; i += 4, i_ += 3)
{
str[i_] = (findIndex(memstr[i]^0xa, b64_table) << 2) | (findIndex(memstr[i + 1]^0xb, b64_table) & 0x30) >> 4;
str[i_ + 1] = (findIndex(memstr[i + 1]^0xb, b64_table) & 0xf) << 4 | (findIndex(memstr[i + 2]^0xc, b64_table) & 0x3c) >> 2;
str[i_ + 2] = (findIndex(memstr[i + 2]^0xc, b64_table) & 0x03) << 6 | (findIndex(memstr[i + 3]^0xd, b64_table));
}
int str_len = (len / 4) * 3 + len % 4;
if (len % 4)
{
str_len -= 1;
}
str[str_len] = 0;
return str;
}


int main()
{
char code[100] = "";
char decode[] = { 0 };
base64_decode(code, decode, base64_table);
printf("%s", decode);
return 0;
}

接下来就是vm的部分

先看func函数的类型,是指针数组

这里需要结合动调看每条指令对应的操作

在这里下断点之后F7进入函数

这就是func数组存放的东西,因为是指针,所以要先按d转为dd才会显示

先把指令提取出来

1
2
3
4
5
6
7
unsigned char ida_chars[] =
{
0xCA, 0x00, 0x00, 0x00, 0x00, 0xCB, 0x00, 0x00, 0x00, 0x00,
0xCC, 0xCF, 0xC9, 0xEE, 0x00, 0x00, 0x00, 0xCF, 0xD1, 0xD3,
0x01, 0xFE, 0xC2, 0xD2, 0x39, 0x00, 0x00, 0x00, 0xD4, 0xEC,
0xFF, 0x00
};

F8单步调试

0xCA

0xCB

0xCC

0xCF

每执行完再进入func[2]都能知道当前位置,便于查看指令

0xc9

0xd1

这里本来赋值为0,1,2,为了保持字符相等的情况,把 this[5]全部赋值为1

0xd3

0xc2

0xd2

长度判断

0xd4

0xcc

接下来又回到0xcc,所以就能猜测是循环做了异或操作,然后判断

func[1] 指令集
func[2] 加密后的flag的字符
func[3] 0
func[4] 索引
func[5] 判断字符相等
func[6] 对比的flag
func[7] 0
func[8] 加密后的字符串
0xca 先将this[1]指令后的数据存放到this[3],然后往后跳转5,正好对应了下一条指令,一开始this[1]后一个数据为0,要先转为 dword
0xcb 先将this[1]指令后的数据存放到this[4],然后往后跳转5,这也是下一条指令
0xcc 把this[2]先赋值为this[8]+this[4],这里this[4]是个整数,所以猜测this[4]是索引,然后继续执行下一指令
0xcf 将this[3]和this[2]的值异或后存放在this[3],进入下一条指令
0xc9 先把this[1]下一个数据赋值给this[2],进入下一条指令,也就是0xee
0xcf 将this[3]和this[2]的值异或后存放在this[3],进入下一条指令
0xd1 根据this[4]的索引来进行字符比较,这里是调试过程,所以为了进行下一步,需要修改汇编指令
0xd3 v1是指针,解引用是v1下一个位置,也就是0x1,整个就是0xee,这时候然后指向下三个位置,也就是c2指令
0xc2 索引this[4]+1
0xd2 this[4]是索引,所以是判断是否结束,这里没结束,所以this[5]赋值为0
0xd4 ec+2==ee

整个过程就是这样

因为偶数次的异或等于不变,所以只需对奇数次的进行异或即可

最终脚本

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
#include<stdio.h>
#include<string.h>
char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int findIndex(char c, char b64_table[])
{
for (int i = 0; i < 64; ++i)
{
if (c == b64_table[i])
return i;
}
}
char* base64_decode(char code[], char str[], char b64_table[])
{
char memstr[200] = { 0 };
memcpy(memstr, code, strlen(code));
int len = strlen(code);
/*while (code[len] != '!' && code[len] != 0)
{
len++;
}*/
for (int i = 0, i_ = 0; i < len; i += 4, i_ += 3)
{
str[i_] = (findIndex(memstr[i], b64_table) << 2) | (findIndex(memstr[i + 1], b64_table) & 0x30) >> 4;
str[i_ + 1] = (findIndex(memstr[i + 1], b64_table) & 0xf) << 4 | (findIndex(memstr[i + 2], b64_table) & 0x3c) >> 2;
str[i_ + 2] = (findIndex(memstr[i + 2], b64_table) & 0x03) << 6 | (findIndex(memstr[i + 3], b64_table));
}
int str_len = (len / 4) * 3 + len % 4;
if (len % 4)
{
str_len -= 1;
}
str[str_len] = 0;
return str;
}


int main()
{
char s[] =
{
190, 54, 172, 39, 153, 79, 222, 68, 238, 95,
218, 11, 181, 23, 184, 104, 194, 78, 156, 74,
225, 67, 240, 34, 138, 59, 136, 91, 229, 84,
255, 104, 213, 103, 212, 6, 173, 11, 216, 80,
249, 88, 224, 111, 197, 74, 253, 47, 132, 54,
133, 82, 251, 115, 215, 13,0
};
char s2[] = { 0 };
for (int i = 0; i < 56; i+=2)
{
s[i] ^= 0xee;
}
for (int i = 0; i < 56; ++i)
{
s2[i] = s[i];
for (int k = i; k > 0; k--)
{
s2[i] ^= (s2[k - 1]);//该字符前面的全部都要异或上
}
}
//printf("%s", s2);
for (int i = 0; i < 56; i = i + 4)
{
s2[i] ^= 0xA;
s2[i + 1] ^= 0xB;
s2[i + 2] ^= 0xC;
s2[i + 3] ^= 0xD;
}
char decode[] = { 0 };
base64_decode(s2, decode, base64_table);
printf("%s", decode);

return 0;
}

hgame2022-week4-easyvm

第一次尝试写解释器,跟着别人的大致思路的

首先VM就是模仿汇编,用指令代替汇编,用数据段来模拟寄存器和数据段,所以我们关键是要找到数据段和操作数、opcode,以及一些数据存放内容的含义

先来给寄存器重新命名

根据main函数中switch可以找到类寄存器的位置,重命名

从switch的v3可以知道,前面给v3赋值的就是操作数

r0[0]开始是0,所以操作指令第一个存储的位置是r0[109],因为是int型,计算的时候×4,就可以找到地址

按g跳转,使用lazyida dump下来

接下来动调分析每条指令对应的汇编代码

先++,再赋值给它,就相当于push指令的入栈操作,然后还有一个r4后移一位

push data[r4++]

先–,再赋值给r5,相当于pop操作

pop r5

又回到0x12,还是push,但是因为r4++,所以已经后移,我们根据计算也可以找到存储数据的地址并提取出数据

回到汇编,这里的指令push了-5进入到堆栈,也就是寄存器下面的位置

push data[r4++]

pop r6

获取输入,并存储到a1中,这里的a1对应r3

getchar r3

熟悉的操作,把输入压入堆栈,记得栈顶往低处移动

push r3

这里的a1是r2

add r2,1

cmp r3,r5

根据前面的pop r5可以知道此时r5是0A,而我们的输入被存入了r3,也就是对我们的输入字符进行判断,那么r8应该就对应ZF标志位

验证了上面的想法,这里的a1是r6,也就是-5,指令跳转回去,相当于进行循环,输入完成后会有一个换行符,getchar会吸收,这里也就是结束我们的循环

jnz r0-5

直接在下一条指令下断点,F9运行

这里的a1是r2,用于记录输入长度,因为最后有一个回车符,所以要–

r2–

根据前面就可以先对数组进行注释

r4没写上去,是内存中存储的一段数据

继续往下走

将内存段的下一个数据入栈,也就是0x20

push data[r4++]

pop r5,这里懒得改了,可以点过去查看

接下来是0x12和0x9

push data[r4++]
pop r6

把r2的值赋给r3

mov r3,r2

push r3

cmp r3,r5

但是这次比较的是字符串的长度,也就是字符串长度是32

jnz r0+r6

这里r6存放的是2f,正好对应结束,所以这段就相当于exit

下面是0x12和0x09

push data[r4++]
pop r6

0x12和0x0A

这是0x0a的代码

push data[r4++]
pop r2

0x13

r2此时为0,+9正好是堆栈的位置,也是存放我们输入数据的位置

mov r3,stack[count]

从下面开始就是加密的部分了

push data[r4++]

0x0B

pop r7

0x15

add r3,r3

0x03

xor r3,r7

把处理完的数据重新放入栈中

mov stack[count],r3

add r2,1

mov r3,r2

0xF

cmp r3,r5

jnz r0+r6

此时的r6是-10,也就是跳转回到前十条指令,这就是循环加密,现在就差最终的密文了,前面加密的部分直接运行过去

再次来到0x12

push data[r4++]

0xA

pop r2

接下来是三个0x12

push data[r4++]
push data[r4++]
push data[r4++]

到了0x08,此时我们的比较数据已经被压入栈中

pop r5

0x13,将处理完的值重新从堆栈中取出

mov r3,satck[count]

0xF

cmp r3,r5

0x07

pop r3

0x04

mov stack[count],r3

0x09

pop r6

0x0D

r6是0x15,加上之后正好exit,和前面类似,压入栈是因为待会需要多次使用

jnz r0+r6

这里没有问题就会往下

0x09

pop r6

此时把-17给到r6

0x08

pop r5

0x05

push r5

0x06

push r6

0x04

push r3

0x01

add r2,1

0x00

mov r3,r2

0x0F//长度判断

cmp r3,r5

0xd,重新进入循环

最终的一些数据和分组和脚本

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
#include<stdio.h>
#include<string.h>

int main()
{
int opcode[65] = {
0x00000012, 0x00000008, 0x00000012, 0x00000009,
0x00000010, 0x00000004, 0x00000001, 0x0000000F, 0x0000000D, //接受我们的输入
0x00000002, 0x00000012, 0x00000008, 0x00000012, 0x00000009, 0x00000000, 0x00000004, 0x0000000F, 0x0000000D,//对flag进行存储操作,比较长度,因为getchar最后是32,所以flag长度为32
0x00000012, 0x00000009, 0x00000012, 0x0000000A, 0x00000013, 0x00000012, 0x0000000B, 0x00000015, 0x00000003, 0x00000014, 0x00000001, 0x00000000, 0x0000000F, 0x0000000D, //flag加密,flag[i]*2^3c存储的值
0x00000012, 0x0000000A, 0x00000012, 0x00000012, 0x00000012, 0x00000008, 0x00000013, 0x0000000F, 0x00000007, 0x00000004, 0x00000009, 0x0000000D, 0x00000009, 0x00000008, 0x00000005, 0x00000006, 0x00000004, 0x00000001, 0x00000000, 0x0000000F, 0x0000000D,
//字符串比较
0x00000012, 0x00000009, 0x00000012,
0x00000008, 0x00000012, 0x0000000A, 0x00000012, 0x00000007, 0x0000000F, 0x0000000C, 0x00000011, //打印
0x0000000E
};
unsigned char xor_table[32]={
0x5e,0x46,0x61,0x43,0x0e,0x53,0x49,0x1f,
0x51,0x5e,0x36,0x37,0x29,0x41,0x63,0x3b,
0x64,0x3b,0x15,0x18,0x5b,0x3e,0x22,0x50,
0x46,0x5e,0x35,0x4e,0x43,0x23,0x60,0x3b
};
unsigned char enc[32]={
0x8E, 0x88, 0xA3, 0x99, 0xC4, 0xA5, 0xC3, 0xDD,
0x19, 0xEC, 0x6C, 0x9B, 0xF3, 0x1B, 0x8B, 0x5B,
0x3E, 0x9B, 0xF1, 0x86, 0xF3, 0xF4, 0xA4, 0xF8,
0xF8, 0x98, 0xAB, 0x86, 0x89, 0x61, 0x22, 0xC1
};
unsigned int data[83] = {
//用于跳转和判断长度的数
0x0000000A, -5, 0x00000020, 0x0000002F, -10, 0x00000000,
//异或数据
0x0000005E, 0x00000046, 0x00000061, 0x00000043, 0x0000000E, 0x00000053, 0x00000049, 0x0000001F,
0x00000051, 0x0000005E, 0x00000036, 0x00000037, 0x00000029, 0x00000041, 0x00000063, 0x0000003B,
0x00000064, 0x0000003B, 0x00000015, 0x00000018, 0x0000005B, 0x0000003E, 0x00000022, 0x00000050,
0x00000046, 0x0000005E, 0x00000035, 0x0000004E, 0x00000043, 0x00000023, 0x00000060, 0x0000003B,
//用于跳转,以及赋值初始索引
0x00000000, -17, 0x00000015,
//密文
0x0000008E, 0x00000088, 0x000000A3, 0x00000099, 0x000000C4, 0x000000A5, 0x000000C3, 0x000000DD,
0x00000019, 0x000000EC, 0x0000006C, 0x0000009B, 0x000000F3, 0x0000001B, 0x0000008B, 0x0000005B,
0x0000003E, 0x0000009B, 0x000000F1, 0x00000086, 0x000000F3, 0x000000F4, 0x000000A4, 0x000000F8,
0x000000F8, 0x00000098, 0x000000AB, 0x00000086, 0x00000089, 0x00000061, 0x00000022, 0x000000C1,

0x00000002, 0x00000000, -6, 0x00000073, 0x00000075, 0x00000063, 0x00000063,
0x00000065, 0x00000073, 0x00000073
};
char flag[32]={0};
for(int i=0;i<32;++i)
{
enc[i]=(enc[i]^xor_table[i])/2;
}
printf("%s",enc);
return 0;
}