Ramnit感染病毒分析报告

简介

本文主要涵盖对Ramnit家族的3个变种(Ramnit.X,Ramnit.AS和Ramnit.A)的分析以及修复方案。其中Ramnit.X变种和Ramnit.AS变种感染文件的方式与之前分析的06—感染型病毒是一样的,所以本文将以Ramnit.A为主要分析对象来进行说明,并会说明Ramnit.A与Ramnit.X变种和Ramnit.AS变种的不同点。由于本文主要是对Ramnit家族的分析与修复方案,将不会特别细致的阐述每一个点,若想看更加详细的分析或想看Ramnit.X变种和Ramnit.AS变种的详细分析,可看Ramnit感染病毒分析报告02。

基本信息

Ramnit变种名 感染母体文件 文件对应MD5
Ramnit.A WaterMark.exe BA4610E9CA3EBF61EC5800955A797C13
Ramnit.X,AS DesktopLayer.exe FF5E1F27193CE51EEC318714EF038BEF

样本详细分析

病毒主体

复制自身

运行病毒样本后,会检索当前文件的路径,并判断该目录是否是C:\Program File\Microsoft\,若不是便将自身复制到该目录下,并执行该目录下的病毒母体文件,然后退出当前进程。代码如图:

image-20191123105648286

Ramnit.A变种与 Ramnit.X,AS 变种的不同点在于释放的感染母体文件不同,Ramnit.A变种释放名为WaterMark.exe, Ramnit.X,AS 变种释放名为 DesktopLayer.exe

inline Hook

先将出主线程外的其他线程挂起,再对ntdll模块中的函数ZwWriteVirtualMemory进行Hook,如图:

image-20191123111817606

inline Hook的实现过程,如图:

image-20191123111231623

创建傀儡进程

由于之前Hook了函数ZwWriteVirtualMemory,恶意样本通过调用CreateProcess创建进程时,执行恶意样本自己写的Hookmain函数。

Hookmain函数主要行为是:往子进程中远程开辟虚拟空间,将PE文件展开并写入远程开辟的虚拟空间中,还写入3了个函数(修复IAT函数,修改区段属性函数,主行为逻辑函数),然后再修改子进程的入口点,从而创建傀儡进程,代码如图:

image-20191123112513326

Ramnit.A变种与 Ramnit.X,AS 变种的不同点

Ramnit.A变种

Ramnit.A变种Hook了两次ZwWriteVirtualMemory(分别进行了脱钩),并创建了两个Svchost.exe的傀儡进程,且注入的PE文件不是同一个文件,其中一个为感染傀儡进程,另一个简单看了一下维持进程当互斥体不在时,便遍历进程并注入所有进程。

image-20191123115437069

Ramnit.X,AS 变种

Ramnit.X,AS 变种只Hook了一次,创建了一个浏览器(IE、Chrome等)傀儡进程。

image-20191123115608329

感染傀儡进程

Ramnit.A变种创建了两个进程,其中只有一个是进行感染的,并且与Ramnit.X,AS 变种差不太多。

感染傀儡进程主要行为:

  • 创建线程1,将感染母体文件(WaterMark.exe或DesktopLayer.exe)添加到注册表Winlogon表中的userinit项中,使其开机自启。
  • 创建线程2,与google、bing、Yahoo进行连接,获取时间,来记录样本运行了多长时间。
  • 创建线程3,将线程2中获取的样本运行的时间,记录到文件dmlconf.dat 来记录样本运行了多长时间。
  • 创建线程4,进行网络通信,按照一定时间,将获取到的数据发送到C&C服务器 ==poopthree.com== 或 ==fget-career.com==。
  • 感染PE文件和网页文件。

感染傀儡进程的主要行为代码,如下图:

image-20191123120239592

添加自启动项

将感染母体文件(WaterMark.exe或DesktopLayer.exe)添加到注册表Winlogon表中的userinit项中,使其开机自启。代码如图:

image-20191123155112622

加工感染数据

先读去感染母体的PE文件数据,然后根据函数GetTickCount获取到的自操作系统启动后的时间作为密钥,来加密感染母体PE文件数据,然后在将VBscript与加密后的感染母体PE文件进行拼接。代码如图:

image-20191123151951744

感染固定介质

遍历固定介质(硬盘),当磁盘大小大于512K时,开始遍历磁盘文件,通过匹配文件后缀名的方式,来判断要感染的文件。Ramnit.X,AS 变种和Ramnit.A变种都只感染4种文件后缀的文件,分别为:==.exe,.dll,.htm,.html==文件,如图:

image-20191123142516691

感染PE文件

判断感染标志

感染==.exe和.dll==文件,先判断感染标志是否存在。

Ramnit.A变种的感染标记为文件附加数据的最后24个字节,通过将最后24个字节每4个字节与第一个4字节进行异或,然后与特定的值进行比较。将最后24个字节进行异或解密,如图:

image-20191123143147913

与特定值进行比较,来判断文件是否被感染,如图:

image-20191123143208716

Ramnit.X,AS 变种的感染标记,遍历节表,查看文件是否有rmnet节,来判断文件是否被感染。

image-20191123143908926

判断是否感染

判断目标文件是否是32位程序,是否是.net,是否有签名,当不是32位程序,不是.net,没有签名时,便不感染,代码如图:

image-20191123144112278

遍历目标文件OFTs表

遍历目标文件的OFts表,获取LoadLibrary和GetProcAddr函数的FTs(IAT)的RVA,以及遍历导入表获取模块名为kernel32.dll的导入表位置,然后将获取到的RVA存到全局变量中,后面会跟着写入函数一同写到被感染文件中,如图:

image-20191123144750293

添加新节

添加新区段,并设置感染区段的可读可写可执行属性,代码如图:

image-20191123150704865

Ramnit.A变种添加的区段名为.text

Ramnit.X,AS 变种添加的区段名为.rmnet

修改OEP

先获取到目标文件的OEP,然后将==添加的新节RVA与OEP的差值存储在全局变量==中,后面会写入被感染的文件中,在将添加的新节的VA设置为新的OEP,代码如图:

image-20191123150200713

Ramnit.A变种的OEP存储在感染区段偏移为 ==0x771== 的地址处。

Ramnit.X,AS 变种的OEP存储在感染区段偏移为 ==0x328== 的地址处。

写入加密后的感染母体及解密函数

由于之前将感染母体PE进行了加密处理,所以在感染的时候还需要将==解密函数==同==加密后的感染母体文件==同时写入被感染文件中去。下图中进行了两次写入,分别是写入解密函数,和加密后的感染母体文件(WaterMark或DesktopLayer),如图:

image-20191123145649043

写入密钥和感染标志

这里Ramnit.A变种就与 Ramnit.X,AS 变种不太一样了,Ramnit.X,AS 变种并没有这些操作。

之前在加工感染数据时提到过,Ramnit.A变种根据获取到的自操作系统启动的时间作为密钥,来加密感染母体PE文件,因此,Ramnit.A变种便将密钥经过加工写入被感染文件的附加数据中,也还会写入Ramnit.A变种的感染标志(文件末尾24字节),代码如图:

image-20191123152630234

修复文件

修复文件校验和,文件时间以及文件属性,代码如图:

image-20191123152742074

Ramnit.A变种会修复文件校验、修改时间和文件属性

Ramnit.X,AS 变种只会修复文件校验和(但好像修复不成功)

感染网页文件

判断感染标志

Ramnit.A变种的网页文件的感染标志与PE文件差不多,唯一区别为PE文件是直接读取最后24字节,而网页文件读取文件末尾倒数第27字节往后的24字节,其他的都一样,代码如图:

image-20191123153219238

Ramnit.X,AS 变种的网页感染标志为文件末尾的9个字节是否是</script>

image-20191123153344408

写入感染脚本

Ramnit.A变种先写入感染脚本,再写入解密感染母体需要的密钥和感染标志,代码如图:

image-20191123153834055

Ramnit.X,AS 变种就相对简单,直接写入感染脚本

image-20191123153907994

修复文件

Ramnit.A变种会修复修改时间和文件属性,代码如图:

image-20191123154122711

Ramnit.X,AS 变种对与网页文件并没有任何修复操作。

感染可移动介质

Ramnit.A变种Ramnit.X,AS 变种对于可移动介质的感染操作,都是将恶意PE写入可移动介质的==autorun.inf==文件中,使当可移动介质插入计算机时自启动。

修复分析

修复目标:

  • 判断当前操作系统感染类型
  • 关闭创建的傀儡进程svchost(Ramnit.A变种)和默认浏览器IE,chrome等(Ramnit.X,AS 变种),防止边修复遍感染。
  • 删除感染母体文件,文件路径为C:\Program File\Microsoft\,目录下的感染母体文件WaterMark(Ramnit.A变种)或是DesktopLayer(Ramnit.X,AS 变种
  • 删除自启动项,修改注册表HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon
  • 根据当前感染类型,修复.exe文件、.dll文件、.htm文件、.html文件

文件修复

判断文件是否被感染

Ramnit.A变种对于.exe和.dll文件根据文件末尾最后24字节与特定特征去比较,对于.htm和.html文件根据文件倒数第27字节读取到的24字节与特征去比较,本人写的判断Ramnit.A变种是否被感染的代码如下:

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
//判断文件末尾的感染特征  返回true,说明该文件被感染
//flag = 0 为PE判断
//flag = 3 为网页判断
BOOL JudgeCharacteristic(char filename[MAX_PATH], int flag)
{
DWORD Buffer[30];
HANDLE hfile;
DWORD NumberOfBytesRead;
DWORD FileSizeHigh;
DWORD characteristic = 0xFA1BC352;
hfile = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, 0, 3u, 0x80u, 0);
if (hfile != INVALID_HANDLE_VALUE)
{
DWORD filesize = GetFileSize(hfile, &FileSizeHigh);
if (filesize > flag + 24 && filesize != -1 && !FileSizeHigh)
{
SetFilePointer(hfile, filesize - (flag + 0x24), 0, 0);
ReadFile(hfile, Buffer, 0x24, &NumberOfBytesRead, 0);
CloseHandle(hfile);
int j = 9;
for (DWORD i = Buffer[0];; Buffer[j] ^= i)
{
--j;
if (!(j * 4))
break;
}
if (Buffer[1] == characteristic && Buffer[2] == 5 && Buffer[3] == 0 && Buffer[4] == 0x0D)
return true; //返回true,说明该文件被感染
else
return false; //返回false,说明该文件没有被感染,或是不属于该文件类型
}
else
{
printf("打开文件失败!");
CloseHandle(hfile);
return false; //文件大小不满足条件,没有被感染
}

}
else
return false; //文件打开失败
}

Ramnit.X,AS 变种的判断逻辑相对简单,对于.exe和.dll文件遍历PE文件的节表,查找是否有.rmnet节,若有便是被感染的,对于.htm和.html文件,则匹配最后9个字节的数据,本人写的代码(没有封装好)如下:

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
对于.exe和.dll文件
//判断.rmnet节表是否存在
//返回1,说明rmnet节存在
//返回0,说明rmnet节不存在
BOOL JudgeSection()
{
char* sectionName = (char*)sectionHeader[numOfSection - 1].Name;
if (strcmp(sectionName, ".rmnet") == 0)
{
return 1; //返回1,说明rmnet节表存在
}
return 0; //返回0,说明节表不存在
}
对于.htm和.html文件
FILE* fp;
BOOL JudgeHtm()
{
char buffer[15] = { 0 };
fopen_s(&fp,fileName, "r+");
fseek(fp, -14, SEEK_END);
fread(buffer, 14, 1, fp);
if (strcmp(buffer, "//--></SCRIPT>") == 0)
return 1; //返回1,说明感染类型为Ramnit.x或Ramnit.AS
else
return 0; //返回3,该文件没有被感染
}

获取原始OEP

根据Ramnit.A变种的OEP存储在感染区段偏移为 ==0x771== 的地址处。Ramnit.X,AS 变种的OEP存储在感染区段偏移为 ==0x328== 的地址处。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Ramnit.X,AS 变种的OEP
void GetOEP()
{
//声明
DWORD median;
DWORD RAW;

RAW = sectionHeader[numOfSection - 1].PointerToRawData;
median = *(PDWORD)((PBYTE)dosHeader + RAW + 0x328);
OEP = optionalHeader->AddressOfEntryPoint - median;
printf("|成功获取到原始OEP:%x\n",OEP);
}
Ramnit.A变种的OEP
void GetOEP()
{
//声明
DWORD median;
DWORD RAW;

RAW = sectionHeader[numOfSection - 1].PointerToRawData;
median = *(PDWORD)((PBYTE)dosHeader + RAW + 0x771);
OEP = optionalHeader->AddressOfEntryPoint - median;
printf("|成功获取到原始OEP:%x\n",OEP);
}

设置感染标记

对于Ramnit.A变种,在文件末尾添加任意被感染文件末尾24字节的感染标记即可。

1
EF DA DA 59 BD 19 C1 A3 EA DA DA 59 EF DA DA 59 E2 DA DA 59 3D EF 0C 15 65 9B 0F 04 8D D4 DF 59 EA DA DA 59

对于Ramnit.X,AS 变种,添加.rmnet节表

删除感染节表

删除感染节表

1
2
3
4
5
6
7
8
9
10
11
12
//删除感染节表
void FixSection()
{
//删除多余节表数据
optionalHeader->SizeOfImage -= sectionHeader[numOfSection - 1].Misc.VirtualSize; //修正sizeofimage
memset(sectionHeader[numOfSection - 1].PointerToRawData + (PBYTE)dosHeader, 0, sectionHeader[numOfSection - 1].SizeOfRawData);

//修正DOS头
fileHeader->NumberOfSections--; //节表个数-1
memset(&sectionHeader[numOfSection - 1], 0, 0x28); //将最后一个节的数据全置0
printf("|成功修复多余节表!\n");
}

删除感染数据

删除感染PE数据

Ramnit.A变种和Ramnit.X,AS 变种删除.exe和.dll文件都可用如下方法删除感染数据

1
2
3
4
5
6
7
void deldata()
{
DWORD Distance = (sectionHeader[numOfSection - 2].SizeOfRawData + sectionHeader[numOfSection - 2].PointerToRawData);
HANDLE hFile1 = CreateFileA(filename, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
SetFilePointer(hFile1, Distance, 0, FILE_BEGIN);
SetEndOfFile(hFile1);
}

删除感染网页数据

删除网页的感染数据稍微不同,因为Ramnit.A变种网页数据后面还有随机长度的密钥以及感染标志,因此删除网页感染数据代码如下:

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
//修复Ramnit.A
void repairHtml2(char filepath[MAX_PATH])
{
char Buffer[600];
HANDLE hfile;
DWORD NumberOfBytesRead;
SetFileAttributesA(filepath, FILE_ATTRIBUTE_NORMAL);
hfile = CreateFileA(filepath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hfile != INVALID_HANDLE_VALUE)
{
DWORD filesize = GetFileSize(hfile, 0);
if (filesize != -1)
{
SetFilePointer(hfile, filesize - 539, 0, 0); //539为附加随机数据的最大长度
ReadFile(hfile, Buffer, 539, &NumberOfBytesRead, 0); //读取文件末尾539字节的数据到buffer中
const char strcharacteristic[20] = "</SCRIPT><!--";
char* ret = strstr(Buffer, strcharacteristic);
DWORD Distance = filesize - (262630 + (539 - (ret - Buffer) - 13));
SetFilePointer(hfile, Distance, 0, FILE_BEGIN);
SetEndOfFile(hfile);
CloseHandle(hfile);
}
}
else
CloseHandle(hfile);
}

//修复Ramnit.X,AS 变种
void repairHtml1(char filepath[MAX_PATH])
{
char Buffer[20];
HANDLE hfile;
DWORD NumberOfBytesRead;
SetFileAttributesA(filepath, FILE_ATTRIBUTE_NORMAL);
hfile = CreateFileA(filepath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hfile != INVALID_HANDLE_VALUE)
{
DWORD filesize = GetFileSize(hfile, 0);
if (filesize != -1)
{
SetFilePointer(hfile, filesize - 280034, 0, FILE_BEGIN);
SetEndOfFile(hfile);
CloseHandle(hfile);
}
}
}