前言
学习一下恶意软件编写,从0到1。
Windows恶意软件在执行恶意代码前通常会进行一些初始化操作,例如运行单一实例、DLL延迟加载以及资源释放。
本章内容始于《Windows黑客编程技术详解》:从中学习,并扩展知识点;例如原文中运行单一实例的方法只介绍了使用广泛且简单的创建系统命名互斥对象。
运行单一实例
当恶意软件植入用户计算机后,它需要确保自己是唯一的(因为如果计算机中存在多份木马进程,木马暴露的风险会增加)。恶意软件如何确保自己是唯一运行的实例?可以通过扫描进程列表、枚举程序窗口,也可以通过共享全局变量以及创建系统命名互斥对象的方式来实现。
扫描进程列表
恶意软件通过扫描进程列表来确保自己是唯一运行的实例,可以使用Windows API来遍历当前系统的所有进程,并检查是否有相同的进程已经运行。
实现
-
获取当前进程名称:获取恶意软件的进程名称。
-
遍历系统中的所有进程:使用
CreateToolhelp32Snapshot
、Process32First
和Process32Next
等 API 函数遍历系统中的所有进程。 -
比较进程名称:将遍历到的每个进程的名称与当前进程的名称进行比较。
-
退出或删除其中一个进程(或其他恶意操作)
// 示例
#include <windows.h>
#include <tlhelp32.h>
#include <tchar.h>
bool IsAnotherInstanceRunning() {
// 获取当前进程名称
TCHAR currentProcessName[MAX_PATH];
GetModuleFileName(NULL, currentProcessName, MAX_PATH);
_tcsupr(currentProcessName); // 转换为大写以进行不区分大小写的比较
// 创建进程快照
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return false;
}
// 遍历进程列表
PROCESSENTRY32 pe;
pe.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe)) {
do {
// 获取每个进程的名称
TCHAR processName[MAX_PATH];
_tcscpy(processName, pe.szExeFile);
_tcsupr(processName); // 转换为大写以进行不区分大小写的比较
// 比较进程名称
if (_tcscmp(processName, currentProcessName) == 0) {
if (pe.th32ProcessID != GetCurrentProcessId()) {
// 找到相同名称但不同进程ID的进程
CloseHandle(hSnapshot);
return true; // 发现另一个实例在运行
}
}
} while (Process32Next(hSnapshot, &pe));
}
CloseHandle(hSnapshot);
return false; // 未发现其他实例
}
int main() {
if (IsAnotherInstanceRunning()) {
MessageBox(NULL, _T("Another instance is already running."), _T("Error"), MB_OK | MB_ICONERROR);
return 1; // 退出程序
}
// 继续执行恶意软件的其他操作
MessageBox(NULL, _T("No other instances found. Running malware..."), _T("Info"), MB_OK | MB_ICONINFORMATION);
// ... 其他恶意操作 ...
return 0;
}
枚举程序窗口
通过WIndows APIEnumWindows
函数 可以遍历所有顶层窗口,并检查是否有已经运行的实例的窗口:
-
定义一个回调函数,检查窗口。
-
调用
EnumWindows
函数,遍历系统中的所有顶层窗口 -
判断是否是唯一运行的实例
代码
#include <windows.h>
#include <tchar.h>
const TCHAR* WINDOW_TITLE = _T("MyUniqueWindow");
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
TCHAR windowTitle[MAX_PATH];
GetWindowText(hwnd, windowTitle, MAX_PATH);
if (_tcscmp(windowTitle, WINDOW_TITLE) == 0) {
// 找到匹配的窗口
HWND* pFoundWindow = (HWND*)lParam;
*pFoundWindow = hwnd;
return FALSE; // 停止枚举
}
return TRUE; // 继续枚举
}
bool IsAnotherInstanceRunning() {
HWND foundWindow = NULL;
EnumWindows(EnumWindowsProc, (LPARAM)&foundWindow);
return (foundWindow != NULL);
}
int main() {
if (IsAnotherInstanceRunning()) {
MessageBox(NULL, _T("Another instance is already running."), _T("Error"), MB_OK | MB_ICONERROR);
return 1; // 退出程序
}
// 创建一个唯一的窗口
HWND hwnd = CreateWindow(
_T("STATIC"), // 窗口类名
WINDOW_TITLE, // 窗口标题
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 300, 200,
NULL, NULL, NULL, NULL
);
if (!hwnd) {
MessageBox(NULL, _T("Failed to create window."), _T("Error"), MB_OK | MB_ICONERROR);
return 1; // 退出程序
}
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
MessageBox(NULL, _T("No other instances found. Running..."), _T("Info"), MB_OK | MB_ICONINFORMATION);
// 主消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
EnumWindows
函数原型:
BOOL EnumWindows(
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);
lpEnumFunc
:指向应用程序定义的回调函数EnumWindowsProc
。该函数会被每个顶层窗口调用一次。
lParam:传递给回调函数的应用程序定义值,可以用来传递需要的信息。
共享全局变量(内存映射)
内存映射文件(Memory-Mapped File)允许多个进程共享一块内存。它可以通过文件系统或者分页文件进行映射。在创建映射文件时,如果内存映射文件已经存在,则说明另一个实例已经在允许。
关键函数:CreateFileMapping
该函数是一个Windows API,用于创建一个文件映射对象,该对象可以与文件或者系统分页文件关联。
函数定义:
HANDLE CreateFileMapping(
HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName
);
-
hFile:文件的句柄,表示要映射的文件。如果要创建基于分页文件的内存映射对象,则将其设为 INVALID_HANDLE_VALUE。
-
lpFileMappingAttributes:指向 SECURITY_ATTRIBUTES 结构的指针,指定文件映射对象的安全属性。
-
flProtect:保护类型,指定页面的访问权限。即只读、读写、写时复制等。
-
dwMaximumSizeHigh:文件映射对象的最大尺寸(高位字节),如果映射文件的尺寸超过 4 GB,则使用该参数。若为 0,则表示文件的尺寸由 dwMaximumSizeLow 参数指定。
-
dwMaximumSizeLow:文件映射对象的最大尺寸(低位字节),若为 0,则文件映射对象的大小取决于文件的当前大小。
-
lpName:指向以空字符结尾的字符串,指定文件映射对象的名称。如果文件映射对象是全局对象,则应以 Global\ 为前缀;如果是本地对象,则以 Local\ 为前缀。若为 NULL,则创建一个未命名的文件映射对象。
具体实现:
#include <windows.h>
#include <tchar.h>
const TCHAR* SHARED_MEMORY_NAME = _T("Global\\MyUniqueSharedMemory");
bool IsAnotherInstanceRunning() {
// 尝试创建内存映射文件
HANDLE hMapFile = CreateFileMapping(
INVALID_HANDLE_VALUE, // 使用分页文件
NULL, // 默认安全属性
PAGE_READWRITE, // 读写权限
0, // 最大对象大小(高位字)
sizeof(int), // 最大对象大小(低位字)
SHARED_MEMORY_NAME); // 内存映射对象的名称
if (hMapFile == NULL) {
return false; // 创建失败
}
// 检查是否已经存在相同名称的内存映射文件
if (GetLastError() == ERROR_ALREADY_EXISTS) {
CloseHandle(hMapFile);
return true; // 已经存在一个实例在运行
}
// 创建视图以访问共享内存
LPVOID pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(int));
if (pBuf == NULL) {
CloseHandle(hMapFile);
return false; // 映射视图失败
}
// 初始化共享内存
int* pSharedFlag = (int*)pBuf;
*pSharedFlag = 1;
// 保持内存映射文件句柄和视图直到程序退出
// 在程序退出时调用 UnmapViewOfFile 和 CloseHandle
return false; // 未发现其他实例
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {
if (IsAnotherInstanceRunning()) {
MessageBox(NULL, _T("Another instance is already running."), _T("Error"), MB_OK | MB_ICONERROR);
return 1; // 退出程序
}
MessageBox(NULL, _T("No other instances found. Running..."), _T("Info"), MB_OK | MB_ICONINFORMATION);
// 主程序代码
// ...
return 0;
}
通过CreateFileMapping
创建一个内存映射文件吗,使用GetLastError
检查返回的错误码来判断内存映射文件是否存在,创建一个视图(MapViewOfFile
),该操作将内存映射文件的内容映射到当前进程的地址空间,返回指向共享内存的指针;将共享内存中的的整数值设置为1,表示当前实例正在运行。
创建系统命名互斥对象的方法和这个方法类似,不过要相对简单一些。
创建系统命名互斥对象
互斥对象(Mutex)是操作系统中用于同步进程或线程访问共享资源的机制。互斥对象有一个特殊的属性:一旦某一个进程成功创建或打开一个命名互斥对象,其它尝试创建相同命名互斥对象的进程将无法再次创建,而只能打开这个已存在的互斥对象。利用这一个特性,恶意软件可以确保它的实例在系统中是唯一的。
Windows API 提供了一个函数CreateMutex
用于创建互斥对象。函数声明:
HANDLE CreateMutexA(
[in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
[in] BOOL bInitialOwner,
[in, optional] LPCSTR lpName
);
-
lpMutexAttributes指向一个
SECURITY_ATTRIBUTES
结构的指针,该结构决定了新互斥对象的安全描述符和是否可以被子进程继承。如果该参数为 NULL,则互斥对象不能被子进程继承,且使用默认的安全描述符。 -
bInitialOwner,BOOL类型,指定调用线程是否在互斥对象创建时立即拥有该对象,如果参数为TRUE,则调用线程获得互斥对象的初始拥有权。
-
lpName,互斥对象的名称(大小写敏感),如果名称以
Global\
或Local\
开头,则分别表示在全局或会话命名空间中创建或打开互斥对象。否则,默认为会话命名空间。
代码实现:
#include "stdafx.h"
#include <Windows.h>
// 判断是否重复运行
BOOL IsAlreadyRun()
{
HANDLE hMutex = NULL;
hMutex = ::CreateMutex(NULL, FALSE, "TEST");
if (hMutex)
{
if (ERROR_ALREADY_EXISTS == ::GetLastError())
{
return TRUE;
}
}
return FALSE;
}
int _tmain(int argc, _TCHAR* argv[])
{
// 判断是否重复运行
if (IsAlreadyRun())
{
printf("Already Run!!!!\n");
}
else
{
printf("NOT Already Run!\n");
}
system("pause");
return 0;
}
代码中还使用了GetLastError
函数,该函数用于获取错误代码,如果错误代码为ERROR_ALREDAY_EXISTS
则表示系统中已经存在一个相同名称的互斥对象。
DLL延迟加载
DLL 延迟加载(Delay Loading of DLLs)是一种技术,允许应用程序在运行时按需加载动态链接库(DLL),而不是在程序启动时加载。这可以加快程序启动时间,并减少内存占用,特别是当某些 DLL 只有在特定情况下才会被使用时。
在恶意软件中,恶意软件就可以通过将DLL以资源形式插入程序中,在运行时,将资源中的DLL释放到本地,再使用DLL延迟加载来执行其它恶意行为。
原理
延迟加载通过一个占位符(Thunk)来替代DLL函数的实际地址。当程序第一次调用该DLL函数时,运行时库会触发一个特殊的处理程序,加载DLL并解析实际的函数地址。之后,调用将直接跳转到实际的函数地址,而不触发加载过程。说简单的话,就是从导入表中将某DLL删去了,等到正式调用的时候,才会加载该DLL文件。
在VS中,可以通过链接器选项直接实现DLL的延迟加载。
设置步骤:
选择项目-属性-链接器-输入-延迟加载的DLL-输入dll
代码测试
工具 visual studio,创建一个dll项目,添加一个MyDll.h,将dllmain.cpp改为MyDll.cpp
代码如下:
//MyDll.h
#ifndef MYDLL_H
#define MYDLL_H
#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif
MYDLL_API void fnMyDll(void);
#endif // MYDLL_H
//MyDll.cpp
#include <windows.h>
#include <delayimp.h>
#include "MyDll.h"
#include <stdio.h>
#pragma comment(lib, "delayimp.lib")
#pragma comment(lib, "MyDll.lib")
int main()
{
BOOL TestReturn;
// MyDLL.DLL will load at this point
fnMyDll();
// MyDLL.dll will unload at this point
TestReturn = __FUnloadDelayLoadedDLL2("MyDll.dll");
if (TestReturn)
printf("\nDLL was unloaded");
else
printf("\nDLL was not unloaded");
return 0;
}
右键解决方案,选择添加新项目(控制台应用程序)命名为MainApp。在MainApp中添加引用MyDll。
创建一个main.cpp如下:
#include <windows.h>
#include <delayimp.h>
#include "MyDll.h"
#include <stdio.h>
#pragma comment(lib, "delayimp.lib")
#pragma comment(lib, "MyDll.lib")
int main()
{
BOOL TestReturn;
// MyDLL.DLL will load at this point
fnMyDll();
// MyDLL.dll will unload at this point
TestReturn = __FUnloadDelayLoadedDLL2("MyDll.dll");
if (TestReturn)
printf("\nDLL was unloaded");
else
printf("\nDLL was not unloaded");
return 0;
}
设置延迟加载,如上所说,打开属性找到延迟加载选项添加MyDll.dll
生成解决方案。得到一个可执行文件。成功!
如果报错无法找到MyDll.h
头文件,可以在属性页->配置属性->C/C++->常规中的附加包含目录里面添加MyDll.h
文件所在的目录。
使用PE工具查看该EXE,如下:
并未发现MyDll.dll
。
其实,在PE结构中,DLL延迟加载的信息存储在ImgDelayDescr
迟导入表中,可以通过数据目录DataDirectory
中的MAGE_DIRECTORY_ENTRY_DELAY_IMPORT
获取延迟导入表RVA相对的偏移地址和数据大小。
在PE工具中也可以直接看到:
代码分析
在上述代码中有一个比较陌生的头文件delayimp.h
,它包含了延迟加载相关的函数和宏定义。一样的,delayimp.lib
就是延迟加载机制所需要的库文件,使用#pragm comment引入。
在mian函数中,第一次调用FnMydll()
时,延迟加载机制会动态加载MyDll.dll
,之后加载完使用__FUnloadDelayLoadedDLL2("MyDll.dll")
尝试卸载DLL,这里实现了对dll的动态管理。
资源释放
恶意软件广泛使用资源释放技术,是因为它可以让程序变得更为简洁。曾经分析过一个.NET恶意软件SnakeKeylogger正是如此,一个exe,经过三阶段的解密才得到最终执行核心恶意行为的exe文件。
通过这种技术,恶意软件可以变得很“小”,这样就只需要植入exe到用户计算机上,降低了被发现的风险。
代码实现
可以在visual studio中右键资源添加资源,不过我个人更喜欢编辑rc文件,和资源头文件。
创建一个控制台应用程序。添加一个resource.h
头文件,main.cpp
,在资源文件里添加一个MyResource.rc
,最后项目目录下创建一个MyTextFile.txt
,内容随意。
具体代码如下,resource.h
:
#define IDR_MYTEXTFILE 101
MyResource.rc
:
#include "resource.h"
IDR_MYTEXTFILE RCDATA "MyTextFile.txt"
main.cpp
:
#include <windows.h>
#include <fstream>
#include "resource.h"
bool ExtractResourceToFile(int resourceId, const char* outputFileName) {
// 查找资源
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(resourceId), RT_RCDATA);
if (hRes == NULL) {
return false;
}
// 加载资源
HGLOBAL hResData = LoadResource(NULL, hRes);
if (hResData == NULL) {
return false;
}
// 锁定资源
void* pResData = LockResource(hResData);
if (pResData == NULL) {
return false;
}
// 获取资源大小
DWORD resSize = SizeofResource(NULL, hRes);
// 将资源数据写入文件
std::ofstream outFile(outputFileName, std::ios::binary);
if (!outFile) {
return false;
}
outFile.write(static_cast<const char*>(pResData), resSize);
outFile.close();
return true;
}
int main() {
// 释放资源到本地文件系统
if (ExtractResourceToFile(IDR_MYTEXTFILE, "ExtractedTextFile.txt")) {
MessageBox(NULL, L"Text file extracted successfully!", L"Success", MB_OK);
}
else {
MessageBox(NULL, L"Failed to extract text file.", L"Error", MB_OK);
}
return 0;
}
生成编译运行后,会生成一个txt文件,成功释放资源。
代码分析
Windows API提供了一系列函数,用于在程序运行时将嵌入到可执行文件或DLL中的资源提取并写入到本地文件系统。
-
FindResource:查找指定资源在资源文件中的位置。
-
LoadResource:加载指定的资源到内存中。
-
LockResource:锁定资源数据,获取资源数据的内存指针。
-
SizeofResource:获取指定资源的大小(以字节为单位)。
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)