恶意代码分析实战Lab7


这一节我们将接触运用windowsAPI编写的两个恶意程序

同时会展示该如何调用windowsAPI去恢复已经被污染的exe程序

Lab07-02

首先我们使用strings工具对该程序进行初步的分析

可以看到一些用于调用windows接口的函数,因此我们要使用一个新的工具Dependency walker去查询该程序的依赖项。

从而具体分析它调用了哪些函数

依赖项

得到了这样的依赖结构(我们在分析的时候会发现OLEAUT32.DLL中是通过序号引入的,此时便可以在下表中查找序号对应的函数)

我们尝试一下运行该程序

发现其效果是试图打开一个网页

接下来使用IDA进行高级静态分析

可以发现一开始的操作是要调用一个windows接口,我们深入其中分析一下调用的接口函数

clsid和iid的具体值

对应序号

我们对该iid进行查询,发现其对应的接口为IWebBrowser2

iid查询

接下来再通过clsid在本地注册表 HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0002DF01-0000-0000-C000-000000000046}中查询对应的程序

对应着Internet Explorer

继续我们的IDA分析,分析接下来对接口的调用部分

1763806077530

可以发现通过ecx将需要的URL赋给了pvarg这个变量,最后又调用了我们的接口,关于其具体的值,可以通过为其设置标准结构体查看

显示出调用的函数

Navigate函数如其名,为我们重定向到一个URL,正是之前给定的ecx中的网址

到此,该恶意程序的功能全部分析完毕

Lab07-03

首先使用strings进行exe分析,发现出现kernel32.dll和kerne132.dll,初步估计该程序将使用伪造的dll文件代替原有dll文件

再分析Lab07-03.dll,看到其内部调用了这几个DLL库,猜测该dll文件中存在后门操作,通过WS2_32中的函数创建远程连接

接下来使用IDA对两个文件进行具体分析,先来分析Lab07-03.exe

要求第二个参数必须存在

首先判断程序第二个参数是否为“WARNING_THIS_WILL_DESTROY_YOUR_MACHINE”,如果不是便退出

在内存中加载这两个库文件,并将Lab07-03.dll拷贝到”C:\Windows\System32\Kernel32.dll”路径下,伪装成系统文件

接下来进入sub_4011E0,查看具体发生了什么

经过初步分析可以得知,此处递归搜索了C盘中的每一个exe程序,并且对其进行了sub_4010A0操作,我们继续深入到该函数中进行分析

可以看到,这段代码通过内存映射文件的方式修改一个文件的内容,具体来说是将文件的导入表中 kernel32.dll 替换为 kerne132.dll。通过对PE文件结构的解析,精准找到导入表以及对应的kernel32.dll字符串的地址再通过方框中的操作进行替换。

接下来,我们来分析另一个文件Lab07-03.dll,,由于其内部的具体流程太过复杂,我们截取其中最为关键的函数调用来分析

通过dllmain函数的调用,我们可以得到程序的大体流程:

创建互斥量保证只有单一进程运行;远程连接到指定服务器”127.26.152.13”,并获取指令

可能接收到3种指令,”sleep”便休眠程序1min,”q”便清理资源退出程序,”exec”便执行该字段后的指令。此时你的终端便已经被远程主机获取。

我们尝试修复该程序造成的破坏:首先,运行带有参数的恶意程序,对本地系统进行感染破坏

查看之前分析的本地特征,确实发现存在kerne132.dll,并且C盘中的exe程序的依赖项已经被更改为kerne132.dll

原先的依赖项

感染后的依赖项

接下来我们要完成一个程序,其功能是遍历C盘中的所有exe程序,并修复其导入表为真正的系统库kernel32.dll,示例代码如下(由于笔者学习时尚未熟悉内存映射方式修改文件,该段代码使用的是文件读写的方式)

#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>

#define MAX_EXE_COUNT 10240
#define MAX_PATH_LEN  512
#define LOG_BUFFER_LEN 1024

// -------------------------- 全局变量 --------------------------
TCHAR* g_exeList[MAX_EXE_COUNT];
int    g_exeCount = 0;

// -------------------------- 函数原型声明 --------------------------
void FreeEXEList(void);
void SafeLog(const TCHAR* format, ...);
void ScanEXE(const TCHAR* path);
BOOL RepairPE(const TCHAR* exePath);
void Pause(void);

// 打印
void SafeLog(const TCHAR* format, ...) {
    TCHAR buffer[LOG_BUFFER_LEN];
    va_list args;
    va_start(args, format);
    _vstprintf_s(buffer, LOG_BUFFER_LEN, format, args);
    va_end(args);
    _tprintf(_T("%s"), buffer);
    fflush(stdout);
}
//扫描
void ScanEXE(const TCHAR* path) {
    TCHAR searchPath[MAX_PATH_LEN];
    _stprintf_s(searchPath, MAX_PATH_LEN, _T("%s\\*.*"), path) 

    WIN32_FIND_DATA findData;
    HANDLE hFind = FindFirstFile(searchPath, &findData);
    do {
        if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            if (_tcscmp(findData.cFileName, _T(".")) != 0 && 
                _tcscmp(findData.cFileName, _T("..")) != 0) {
                TCHAR subPath[MAX_PATH_LEN];
                _stprintf_s(subPath, MAX_PATH_LEN, _T("%s\\%s"), path, findData.cFileName);
                ScanEXE(subPath);
            }
        } else {
            TCHAR* ext = _tcsrchr(findData.cFileName, _T('.'));
            if (ext != NULL && _tcsicmp(ext, _T(".exe")) == 0) {
                TCHAR* fullPath = (TCHAR*)malloc(MAX_PATH_LEN * sizeof(TCHAR));
  
                _stprintf_s(fullPath, MAX_PATH_LEN, _T("%s\\%s"), path, findData.cFileName)
  
                g_exeList[g_exeCount++] = fullPath;
            }
        }
    } while (FindNextFile(hFind, &findData));

    FindClose(hFind);
}

// 修复
BOOL RepairPE(const TCHAR* exePath) {
    BOOL result = FALSE; 
    HANDLE hFile = INVALID_HANDLE_VALUE; // 初始化文件句柄为无效值

    // 1. 打开文件
    hFile = CreateFile(
        exePath,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );
    if (hFile == INVALID_HANDLE_VALUE) {
        DWORD err = GetLastError();
        SafeLog(_T("打开文件失败:%s(错误码:%d)\n"), exePath, err);
        goto cleanup; // 直接跳转到 cleanup 标签
    }

    // 2. 读取 DOS 头
    IMAGE_DOS_HEADER dosHeader;
    DWORD bytesRead;
    if (!ReadFile(hFile, &dosHeader, sizeof(dosHeader), &bytesRead, NULL) || 
        bytesRead != sizeof(dosHeader) || 
        dosHeader.e_magic != IMAGE_DOS_SIGNATURE) {
        SafeLog(_T("无效的 PE 文件:%s(不是 DOS 可执行文件)\n"), exePath);
        goto cleanup; // 读取失败,跳转到 cleanup
    }

    // 3. 读取 NT 头
    IMAGE_NT_HEADERS32 ntHeader;
    if (SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER ||
        !ReadFile(hFile, &ntHeader, sizeof(ntHeader), &bytesRead, NULL) || 
        bytesRead != sizeof(ntHeader) || 
        ntHeader.Signature != IMAGE_NT_SIGNATURE) {
        SafeLog(_T("无效的 PE 文件:%s(NT 头损坏)\n"), exePath);
        goto cleanup; // 读取失败,跳转到 cleanup
    }

    // 4. 定位导入表
    IMAGE_DATA_DIRECTORY importDir = ntHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    if (importDir.VirtualAddress == 0 || importDir.Size == 0) {
        SafeLog(_T("跳过无导入表的文件:%s\n"), exePath);
        goto cleanup; // 无导入表,跳转到 cleanup
    }

    // 5. 将 RVA 转换为文件偏移
    DWORD importFileOffset = 0;
    IMAGE_SECTION_HEADER sectionHeader;
    DWORD sectionTableOffset = dosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS32);
    BOOL foundSection = FALSE;

    for (WORD i = 0; i < ntHeader.FileHeader.NumberOfSections; i++) {
        if (SetFilePointer(hFile, sectionTableOffset + i * sizeof(IMAGE_SECTION_HEADER), NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER ||
            !ReadFile(hFile, &sectionHeader, sizeof(sectionHeader), &bytesRead, NULL) ||
            bytesRead != sizeof(sectionHeader)) {
            SafeLog(_T("读取节表失败:%s(节索引:%d)\n"), exePath, i);
            goto cleanup; // 读取失败,跳转到 cleanup
        }

        if (importDir.VirtualAddress >= sectionHeader.VirtualAddress &&
            importDir.VirtualAddress < sectionHeader.VirtualAddress + sectionHeader.SizeOfRawData) {
            importFileOffset = importDir.VirtualAddress - sectionHeader.VirtualAddress + sectionHeader.PointerToRawData;
            foundSection = TRUE;
            break;
        }
    }

    if (!foundSection) {
        SafeLog(_T("未找到导入表所在节:%s\n"), exePath);
        goto cleanup; // 未找到,跳转到 cleanup
    }

    // 6. 遍历导入表并修复恶意 DLL
    IMAGE_IMPORT_DESCRIPTOR importDesc;
    BOOL repaired = FALSE;
    DWORD currentOffset = importFileOffset;

    while (TRUE) {
        if (SetFilePointer(hFile, currentOffset, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER ||
            !ReadFile(hFile, &importDesc, sizeof(importDesc), &bytesRead, NULL) ||
            bytesRead != sizeof(importDesc)) {
            SafeLog(_T("读取导入描述符失败:%s(偏移:%d)\n"), exePath, currentOffset);
            break; // 这里 break 出 while 循环,之后会走到 cleanup
        }

        if (importDesc.Name == 0 && importDesc.FirstThunk == 0) {
            break; // 遍历结束
        }

        DWORD dllNameRVA = importDesc.Name;
        DWORD dllNameFileOffset = dllNameRVA - sectionHeader.VirtualAddress + sectionHeader.PointerToRawData;

        char dllName[256];
        if (SetFilePointer(hFile, dllNameFileOffset, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER ||
            !ReadFile(hFile, dllName, sizeof(dllName), &bytesRead, NULL) ||
            bytesRead == 0) {
            SafeLog(_T("读取 DLL 名称失败:%s(偏移:%d)\n"), exePath, dllNameFileOffset);
            currentOffset += sizeof(IMAGE_IMPORT_DESCRIPTOR);
            continue;
        }
        dllName[sizeof(dllName) - 1] = '\0';

        if (_stricmp(dllName, "kerne132.dll") == 0) {

            TCHAR backupPath[MAX_PATH_LEN];
            const char* correctDllName = "kernel32.dll";
            DWORD correctNameLen = strlen(correctDllName) + 1;
            if (SetFilePointer(hFile, dllNameFileOffset, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER ||
                !WriteFile(hFile, correctDllName, correctNameLen, &bytesRead, NULL) ||
                bytesRead != correctNameLen) {
                SafeLog(_T("修复 DLL 名称失败:%s(错误码:%d)\n"), exePath, GetLastError());
            } else {
                repaired = TRUE;
            }
        }
        currentOffset += sizeof(IMAGE_IMPORT_DESCRIPTOR);
    }
  
    result = repaired;

// 7.回收资源
cleanup:
    if (hFile != INVALID_HANDLE_VALUE) {
        CloseHandle(hFile);
    }
    return result;
}

//释放内存
void FreeEXEList() {
    for (int i = 0; i < g_exeCount; i++) {
        if (g_exeList[i] != NULL) {
            free(g_exeList[i]);
            g_exeList[i] = NULL;
        }
    }
    g_exeCount = 0;
}

void Pause() {
    SafeLog(_T("\n按回车键退出..."));
    while (getchar() != '\n');
    getchar();
}

int _tmain() {
    SafeLog(_T("============= PE 文件修复工具(WinXP 32位专用)=============\n"));
    SafeLog(_T("功能:扫描 C 盘所有 EXE 文件,修复导入表中恶意的 kerne132.dll\n"));
    SafeLog(_T("===========================================================\n\n"));

    SafeLog(_T("开始扫描 C 盘 EXE 文件...\n"));
    ScanEXE(_T("C:\\"));

    SafeLog(_T("\n扫描完成!共找到 %d 个 EXE 文件,开始修复...\n\n"), g_exeCount);
    for (int i = 0; i < g_exeCount; i++) {
        if (g_exeList[i] != NULL) {
            RepairPE(g_exeList[i]);
        }
    }

    SafeLog(_T("\n修复任务完成!\n"));
    FreeEXEList();
    Pause();
    return 0;
}

鉴于笔者在高版本主机中编译的该段程序,为了与winXP兼容,编译命令如下

gcc -o repair.exe repair.c -m32 -DWINVER=0x0501 -D_WIN32_WINNT=0x0501 -static-libgcc -static-libstdc++ -luser32 -lkernel32 -ladvapi32

其中的 -DWINVER=0x0501 -D_WIN32_WINNT=0x0501用于指定windows平台为XP。接下来在XP虚拟机中执行修复程序

执行修复程序

修复后再次查看C盘中的exe文件依赖项,发现已经被修复为了系统库Kernel32.dll

修复后的依赖

至此,本次实验圆满结束


文章作者: Yssx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yssx !
评论
  目录