dokan2.dll


分析sub_7FF8AD1E312E

使用SetWindowPos隐藏运行时的任务栏图标,接着的主要逻辑在sub_7FF8AD1E2141函数中,跟进

前面部分是回文算法,没什么用,疑似用于混淆,直接跳过

检查MZ标志与PE标志,解析PE头找到IAT

上面一大部分是回文算法,还是没用的混淆,直接跳过,IAT HOOK实现的关键点,hook的目标函数为:sub_7FF8AD1E2108

拼接sw.dat路径
由于改样本存在反射式dll加载,通过代码逻辑可以分析得到open file与read file函数位置

sub_7FF8AD27C950函数负责open sw.dat,sub_7FF8AD25BBA0函数负责read sw.dat

修改内存属性用于运行读取sw.dat数据(shellcode)
下断点运行,获得读取数据offset:0;size:33107h
可以直接从sw.dat dump也可以通过脚本读取
def extract_saw_dat(input_file, output_file, size_hex=0x33107):
try:
with open(input_file, 'rb') as f_in:
data = f_in.read(size_hex)
with open(output_file, 'wb') as f_out:
f_out.write(data)
print(f"成功提取 {size_hex} 字节({size_hex} 字节)到 {output_file}")
except FileNotFoundError:
print(f"错误:文件 {input_file} 不存在!")
except Exception as e:
print(f"发生错误:{e}")
input_filename = 'sw.dat'
output_filename = 'dump.dat'
extract_saw_dat(input_filename, output_filename)
先跟进v18(filename)分析

使用了hash api加载技术,数据过多,先进行静态分析,分析刚才dump的数据

明显的PEB\TEB遍历,API hash

核心的hash 算法,这个算法是重复的,每个需要调用API都会使用到这个算法,同时hash 种子也不同
uint32_t ComputeFunctionHash(const char* funcName) {
uint32_t hash = 0xFAF6F3EA; // 初始种子值 (-184639718 的补码表示)
while (*funcName) {
char c = *funcName;
// (A-Z转为小写)
if ((c >= 'A') && (c <= 'Z')) {
c |= 0x20; // 等价于 c = tolower(c)
}
// 核心哈希计算:FNV-1a变种
hash = 16777619 * (hash ^ c);
funcName++;
}
return hash;
}
算法伪代码
hash对应API:
| 825981789 | 0x3138B27D | GetModuleFileNameW | kernel32.dll | 获取当前进程可执行文件的完整路径。 |
|---|---|---|---|---|
| 297097716 | 0x11B428F4 | PathRemoveFileSpecW | shlwapi.dll | 从文件路径中移除文件名,得到文件所在目录。 |
| -1421191882 | 0xAB494D96 | IsUserAnAdmin | shell32.dll | 检查当前用户是否为管理员。 |
| -2061012367 | 0x8527F271 | LoadLibraryA | kernel32.dll | 加载指定的DLL库(这里用于加载Shell32.dll)。 |
| 487451824 | 0x1D0F2CB0 | ShellExecuteExW | shell32.dll | 执行一个操作,通过"runas"动词来请求UAC提权。 |
| -1160475044 | 0xBBCEA65C | memset | ntdll.dll | 将内存块(SHELLEXECUTEINFO结构体)清零。 |
| 1017289554 | 0x3CA28D52 | ExitProcess | kernel32.dll | 终止当前(非管理员)进程。 |
| 881625753 | 0x348A7A99 | CreateFileMappingW | kernel32.dll | 创建一个文件映射对象,可能用作互斥体或存放载荷。 |
|---|---|---|---|---|
| -1411882551 | 0xABE8D1A9 | MapViewOfFile | kernel32.dll | 将文件映射对象映射到当前进程的地址空间。 |
| -194833316 | 0xF4625A9C | memset | ntdll.dll | 清理内存。 |
| -215346181 | 0xF3294C5B | memcpy | ntdll.dll | 复制内存块(用于移动加密的载荷)。 |
| -811868201 | 0xCFA8F7F7 | CryptDecrypt | advapi32.dll | 核心功能:使用Windows加密API解密载荷。 |
| 771461096 | 0x2DF7D7E8 | CreateFileW | kernel32.dll | 创建或打开文件(用于释放恶意DLL和数据文件)。 |
| 50438492 | 0x03017B2C | WriteFile | kernel32.dll | 将数据写入文件(将解密后的载荷写入磁盘)。 |
| -976780978 | 0xC58AE14E | CreateServiceW | advapi32.dll | 持久化:创建一个新的Windows服务。 |
| -1189699346 | 0xB91540EE | StartServiceW | advapi32.dll | 启动一个服务。 |
| 513401500 | 0x1E99529C | CreateProcessW | kernel32.dll | 核心功能:创建一个新进程来执行主载荷。 |
|---|---|---|---|---|
| 994584314 | 0x3B4958FA | CloseHandle | kernel32.dll | 关闭一个打开的对象句柄(用于清理)。 |
| 94407602 | 0x05A17D92 | CloseHandle | kernel32.dll | 关闭另一个句柄(可能是新进程的线程句柄)。 |
| -1103439570 | 0xBE3C712E | MessageBoxW | user32.dll | 显示一个消息框(用于在失败时欺骗用户)。 |
| -332713028 | 0xEA2E2BBC | ExitProcess | kernel32.dll | 在出错或完成任务后终止进程。 |
有了对应表,分析就很简单了

判断权限,非admin权限就进行UAC弹窗提权

sub_3641疑似网络行为,sub_3C54函数存在疑似反沙箱技术

检测系统dll47个预设的、硬编码的内存地址内容是否为WriteData
解密API

创建服务
可能创建的文件:
C:\Windows\System32\BrokerClientCallback.dll
C:\Windows\System32\TimeBrokerClient.dll
C:\Windows\System32\msvchost.dat
C:\Windows\System32\system.dat
C:\Windows\System32\mswkscli.dll
C:\Windows\System32\wkscli.dll
动调只需要实现一个大致逻辑的简单loader即可
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>
#include <string.h>
const char* PAYLOAD_FILE = "sw.dat";
const DWORD PAYLOAD_SIZE = 0x33107;
typedef void (*ShellcodeFunc)(char*);
int main() {
char exeFullPath[MAX_PATH];
char payloadPath[MAX_PATH];
char* lastSlash = NULL;
if (GetModuleFileNameA(NULL, exeFullPath, MAX_PATH) == 0) {
return 1;
}
strncpy(payloadPath, exeFullPath, MAX_PATH);
lastSlash = strrchr(payloadPath, '\\');
if (lastSlash != NULL) {
*(lastSlash + 1) = '\0';
strcat(payloadPath, PAYLOAD_FILE);
}
else {
return 1;
}
printf(" file path: %s\n", payloadPath);
FILE* file = fopen(payloadPath, "rb");
if (file == NULL) {
return 1;
}
void* shellcode_mem = VirtualAlloc(
NULL,
PAYLOAD_SIZE,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (shellcode_mem == NULL) {
fclose(file);
return 1;
}
size_t bytesRead = fread(shellcode_mem, 1, PAYLOAD_SIZE, file);
if (bytesRead != PAYLOAD_SIZE) {
fclose(file);
VirtualFree(shellcode_mem, 0, MEM_RELEASE);
return 1;
}
fclose(file);
ShellcodeFunc shellcode_entry;
shellcode_entry = (ShellcodeFunc)shellcode_mem;
shellcode_entry(exeFullPath);
VirtualFree(shellcode_mem, 0, MEM_RELEASE);
return 0;
}
TimeBrokerClient.dll
…………
存在tlscallback

动调可以使用x64dbg,也可以选择不动调,直接解密shellcode就行

svchost中存储的是VirtualAlloc 函数的 RVA

Vigenère 变种解密脚本
import os
def decrypt_payload():
input_file = "msvchost.dat"
output_file = "decrypted.bin"
key_string = "svchost"
key_bytes = [ord(c) for c in key_string]
key_bytes.append(0x00)
key_len = len(key_bytes)
if not os.path.exists(input_file):
return
try:
with open(input_file, "rb") as f:
encrypted_data = bytearray(f.read())
decrypted_data = bytearray(len(encrypted_data))
for i in range(len(encrypted_data)):
dec_byte = (encrypted_data[i] - key_bytes[i % key_len]) & 0xFF
decrypted_data[i] = dec_byte
with open(output_file, "wb") as f:
f.write(decrypted_data)
except Exception as e:
print(f"[!] 发生错误: {e}")
if __name__ == "__main__":
decrypt_payload()
写一个load进行动调
(load名称使用svchost.exe,防止存在检查行为)
#include <windows.h>
#include <iostream>
#include <fstream>
#include <vector>
int main()
{
const char* filename = "decrypted.bin";
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file) {
system("pause");
return -1;
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
if (size <= 0) {
return -1;
}
std::vector<char> buffer(size);
if (!file.read(buffer.data(), size)) {
return -1;
}
void* exec_mem = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!exec_mem) {
return -1;
}
memcpy(exec_mem, buffer.data(), size);
((void(*)())exec_mem)();
return 0;
}