样本名称:alg.exe
样本 SHA256:b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
样本 MD5:5615860923c90bda63ad7e3546e8490f
样本 SHA1:da6aff139b0513d933c67b6d04cc0efcda2ba3a9
文件类型:PE32+ executable (GUI) x86-64, 7 sections, 1,594,368 bytes
编译器特征:MSVC 14.20(msvcrt.dll + api-ms-win-core-* CRT)
诱饵身份:ALG.exe / Application Layer Gateway Service / Microsoft Corporation 10.0.19041.746
分析环境:Kali Linux / uv / Python / capstone / unicorn / objdump / tshark / strings
辅助模型:Qwen3.6 27B Abliterated / Grok 4.6 / GPT 5.6 Sol
摘要
样本是一个被植入 m0yv 解包器的伪造 ALG.exe。入口点被改写成先 call 假装成 .reloc 的节区末页,对该节 0x8C000 字节做 256 条 dword 级 add/sub/xor/not 补丁(每 0x400 字节窗口重复一遍,共 560 块),补丁完成后调用 RtlDecompressBuffer(0x102) 做 LZNT1 解压,得到 371,200 字节的 beacon 映像。解压产物剥掉 MZ/PE 签名、改用 PNG 签名(\x89PNG\r\n\x1a\n)伪装成图片,但 PE 可选头仍是 PE32+,导出名 x64.dll。互斥量 Global\Multiarch.m0yv-d6739753dac71fc6-inf 是该壳的水印。
Beacon 通过 WinHTTP.dll 出网。WinHttpConnect 的端口参数是 mov r8w, 0x50(80/tcp)。C2 地址不是明文域名,而是 .data RVA 0x58560 处 76 字节密文,用 beacon 自带的 ChaCha8(常量 "expand 32-byte k",4 个 double-round)解密。明文头四个字节为 24.74.72.2,其后 64 字节是两把 32 字节曲线公钥(X25519 ECDH + SHA-512 握手),不是主机名。HTTP 头里的 .biz 是完整 TLD,前面的标签由 wsprintfW("%x%x%x", hash(计算机名)) 在运行时生成。
沙箱 90 秒抓包没有访问该地址:PCAP 中的 DNS/HTTP 全部属于 Google、Windows Update 与微信,是分析虚拟机噪声。
| 项目 | 结论 |
|---|---|
| 外层身份 | 伪造 ALG.exe(资源版本 10.0.19041.746) |
| 壳 | m0yv(dword 窗口补丁 + LZNT1) |
| 内层 | 手工映射 PE,导出 x64.dll |
| 字符串保护 | 12 字节 XOR 导入名;.data 中 key16 || n || ct 记录 |
| C2 加密 | ChaCha8,ASCII 数字密钥/nonce |
| C2 IP | 24.74.72.2 |
| C2 端口 | 80/tcp |
| C2 协议 | WinHTTP POST,Host: + 本机哈希十六进制 + .biz |
| 持久化 | %APPDATA%\<id>.bin;写入 Adobe\ARM\1.0\armsvc.exe |
一、样本准备与初判
1.1 解压样本
样本、内存 dump、PCAP、沙箱 JSON 分四个压缩包,逐个解开。
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ unzip -P threatbook -o files/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe.zip -d extract/b901f02c
Archive: files/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe.zip
inflating: extract/b901f02c/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ unzip -P threatbook -o files/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe.dump.zip -d extract/b901f02c
Archive: files/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe.dump.zip
inflating: extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636721.memdump/c9684f8292838ae1b93f1fc41abb0b1f70de016830dcf18caea910c62738959f/3476_15778690255720482026
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ unzip -P threatbook -o files/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe.pcap.zip -d extract/b901f02c
Archive: files/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe.pcap.zip
inflating: extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636718.pcap/pcaps/dump.pcap
inflating: extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636718.pcap/pcaps/mitm_dump.pcap
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ unzip -P threatbook -o files/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe.report.zip -d extract/b901f02c
Archive: files/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe.report.zip
inflating: extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636724.json
下文将磁盘样本记为 extract/b901f02c/<hash>,将 PID 3476 的内存映像记为 dump(389,576 字节)。
1.2 识别文件类型与哈希
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ file b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe: PE32+ executable for MS Windows 10.00 (GUI), x86-64, 7 sections
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ sha256sum b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ md5sum b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
5615860923c90bda63ad7e3546e8490f b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ sha1sum b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
da6aff139b0513d933c67b6d04cc0efcda2ba3a9 b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ ls -l b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
-rw-r--r-- 1 z z 1594368 9月17日 17:18 b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
文件 1,594,368 字节。后续所有 VA 换算以节区表为准。
1.3 节区布局
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ objdump -h b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
Idx Name Size VMA LMA File off Algn
0 .text 0000e3a3 0000000140001000 0000000140001000 00000400 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rdata 00005672 0000000140010000 0000000140010000 0000e800 2**4
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000400 0000000140016000 0000000140016000 00014000 2**4
CONTENTS, ALLOC, LOAD, DATA
3 .pdata 00000d20 0000000140017000 0000000140017000 00014400 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .didat 00000118 0000000140018000 0000000140018000 00015200 2**2
CONTENTS, ALLOC, LOAD, DATA
5 .rsrc 00001fd0 0000000140019000 0000000140019000 00015400 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .reloc 0016e000 000000014001b000 000000014001b000 00017400 2**2
CONTENTS, LOAD, CODE, DATA
| 节区 | 虚拟地址 | 文件偏移 | 虚拟大小 | 原始大小 |
|---|---|---|---|---|
.text | 0x140001000 | 0x400 | 0xE3A3 | 0xE400 |
.rdata | 0x140010000 | 0xE800 | 0x5672 | 0x5800 |
.data | 0x140016000 | 0x14000 | 0xD08 | 0x400 |
.pdata | 0x140017000 | 0x14400 | 0xD20 | 0xE00 |
.didat | 0x140018000 | 0x15200 | 0x118 | 0x200 |
.rsrc | 0x140019000 | 0x15400 | 0x1FD0 | 0x2000 |
.reloc | 0x14001B000 | 0x17400 | 0x16F000 | 0x16E000 |
.reloc 原始大小约 1.4 MB,特征字为 CONTENTS, LOAD, CODE, DATA,与正常重定位节(通常为 READONLY 且只含少量 RVA 对)截然不同。这是第一个需要解释的异常。
1.4 数据目录:.reloc 不在重定位目录里
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ objdump -p b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
Magic 020b (PE32+)
MajorLinkerVersion 14
MinorLinkerVersion 20
AddressOfEntryPoint 000000000000db80
ImageBase 0000000140000000
SizeOfImage 0018a000
Subsystem 00000002 (Windows GUI)
The Data Directory
Entry 0 0000000000000000 00000000 Export Directory
Entry 1 0000000000014560 000001f4 Import Directory
Entry 2 0000000000019000 00001fd0 Resource Directory
Entry 3 0000000000017000 00000d20 Exception Directory
Entry 5 0000000000000000 00000000 Base Relocation Directory [.reloc]
Entry 6 0000000000011f20 00000054 Debug Directory
Entry a 0000000000010600 00000118 Load Configuration Directory
Entry c 0000000000010718 00000438 Import Address Table Directory
Entry d 0000000000014154 000000c0 Delay Import Directory
IMAGE_DIRECTORY_ENTRY_BASERELOC 为全零。名为 .reloc 的节因此不是加载器使用的重定位表,而是借用节名做掩护的载荷容器。入口 RVA 0xDB80,VA 0x14000DB80。
1.5 导入表作为能力声明
即时导入是 msvcrt、一串 api-ms-win-core-*、CRYPTBASE!SystemFunction036、MSWSOCK!AcceptEx、VirtualAlloc / VirtualProtect。延迟导入解析如下:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
p=open('extract/b901f02c/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe','rb').read()
off=0xe800+(0x14154-0x10000)
i=off
while True:
attrs,name,mod,iat,int_,_,_,_=struct.unpack_from('<IIIIIIII', p, i)
if name==0: break
noff=0xe800+(name-0x10000)
print(p[noff:noff+64].split(b'\x00')[0])
i+=32
"
b'OLEAUT32.dll'
b'api-ms-win-core-com-l1-1-0.dll'
b'api-ms-win-service-core-l1-1-0.dll'
b'api-ms-win-service-winsvc-l1-1-0.dll'
b'WS2_32.dll'
外层没有 WINHTTP.dll。WS2_32 与服务 API 属于 ALG 本体(应用层网关本来就要套接字与 SCM)。若样本后来走 HTTP C2,能力一定在解包后的内层。
1.6 版本资源与伪装标识
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ strings -el b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
CompanyName
Microsoft Corporation
FileDescription
Application Layer Gateway Service
FileVersion
10.0.19041.746 (WinBuild.160101.0800)
InternalName
ALG.exe
OriginalFilename
ALG.exe
SOFTWARE\Microsoft\ALG\ISV\{6E590D61-F6BC-4dad-AC21-7DC40D304059}
资源与注册表路径都在模仿正版 ALG。调试目录中还有 ALG.pdb。这是身份伪装,不是功能说明。
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
p=open('extract/b901f02c/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe','rb').read()
i=p.find(b'ALG.pdb')
print('pdb off', hex(i))
print(p[i:i+8])
"
pdb off 0x1078c
b'ALG.pdb\x00'
二、入口点与假 .reloc
2.1 入口只做一件事:调用 .reloc 末页
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ objdump -d -M intel --no-show-raw-insn --start-address=0x14000db80 --stop-address=0x14000db94 b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
000000014000db80 <.text+0xcb80>:
14000db80: sub rsp,0x28
14000db84: call 0x140188000
14000db89: add rsp,0x28
14000db8d: jmp 0x14000d910
14000db92: int3
0x140188000 落在 .reloc 内(节起始 0x14001B000,文件偏移 0x17400 + (0x188000-0x1B000) = 0x184400)。调用返回后跳到 0x14000D910,即原 ALG 入口。壳只劫持了 EP,ALG 代码仍在。
2.2 Stub 序言与补丁循环控制
┌──(z㉿kali)-[~/Projects/virus-re/extract/b901f02c]
└─$ objdump -d -M intel --no-show-raw-insn --start-address=0x140188000 --stop-address=0x140188040 b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe
0000000140188000 <.reloc+0x16d000>:
140188000: push rax
140188001: push rcx
140188002: push rdx
140188003: push rbx
140188004: push rbp
140188005: push rsi
140188006: push rdi
140188007: push r8
140188009: push r9
14018800b: push r10
14018800d: push r11
14018800f: push r12
140188011: push r13
140188013: push r14
140188015: push r15
140188017: push rbp
140188018: mov rbp,rsp
14018801b: sub rsp,0x20
14018801f: and rsp,0xfffffffffffffff0
140188023: lea rbx,[rip+0xffffffffffe931d6] # 0x14001b200
14018802a: push 0x0
14018802c: pop rcx
14018802d: push rbx
14018802e: add DWORD PTR [rbx+0x3c],0x54bf1c3a
140188035: not DWORD PTR [rbx+0x34]
objdump 在 not dword 处失去指令同步(把 81 前缀误判为 sub),从 0x140188038 起的反汇编是错的。用 capstone 按真实边界重读同一段:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CS_OP_MEM
p=open('extract/b901f02c/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe','rb').read()
stub=p[0x184400:0x184400+0x1000]
md=Cs(CS_ARCH_X86, CS_MODE_64); md.detail=True
ops=[]
for insn in md.disasm(stub, 0x140188000):
if insn.mnemonic=='add' and insn.op_str.startswith('rcx'):
print('loop', hex(insn.address), insn.mnemonic, insn.op_str)
break
if insn.mnemonic in ('add','sub','xor','not') and insn.operands and insn.operands[0].type==CS_OP_MEM:
disp=insn.operands[0].mem.disp
imm=insn.operands[1].imm & 0xffffffff if insn.mnemonic!='not' else None
ops.append((insn.mnemonic, disp, imm))
print('nops', len(ops), 'maxdisp', hex(max(d for _,d,_ in ops)))
print('first5', ops[:5])
"
loop 0x1401888dd add rcx, 0x400
nops 256 maxdisp 0x3fc
first5 [('add', 60, 1421810746), ('not', 52, None), ('add', 364, 1778471662), ('not', 596, None), ('not', 108, None)]
补丁循环之后是解压调用:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
p=open('extract/b901f02c/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe','rb').read()
stub=p[0x184400:0x184400+0x1200]
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(stub, 0x140188000):
if insn.address>=0x1401888d0:
print(hex(insn.address), insn.mnemonic, insn.op_str)
if insn.address>=0x140188900: break
"
0x1401888d7 not dword ptr [rbx + 0x39c]
0x1401888dd add rcx, 0x400
0x1401888e4 add rbx, 0x400
0x1401888eb cmp rcx, 0x8c000
0x1401888f2 jne 0x14018802e
0x1401888f8 pop rcx
0x1401888f9 call 0x1400a6b06
0x1401888fe mov rsp, rbp
控制流恢复:
push 全部寄存器
lea rbx, [rip-0x16CE2A] ; rbx = 0x14001B200
xor ecx, ecx
push rbx
; 256 次 dword add/sub/xor/not,位移 0..0x3FC
add ecx, 0x400
add rbx, 0x400
cmp ecx, 0x8C000
jne 补丁循环
pop rcx ; rcx = 解密缓冲区起始
call 0x1400A6B06
同一组 256 个操作作用在每个 0x400 字节窗口上,共 0x8C000/0x400 = 560 块。这是 m0yv 的典型 dword 替换,不是逐字节 XOR。0x14001B200 相对 .reloc 起始 0x14001B000 偏移 0x200,文件位置 0x17600。
三、应用补丁并读出解压头
把 143,360 次补丁(256 × 560)全部打完,取缓冲区头 32 字节:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64, CS_OP_MEM
import struct
p=open('extract/b901f02c/b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe','rb').read()
stub=p[0x184400:]
reloc=bytearray(p[0x17400:0x17400+0x16e000])
md=Cs(CS_ARCH_X86, CS_MODE_64); md.detail=True
ops=[]
for insn in md.disasm(stub, 0x140188000):
if insn.mnemonic=='add' and insn.op_str.startswith('rcx'):
break
if insn.mnemonic in ('add','sub','xor','not') and insn.operands and insn.operands[0].type==CS_OP_MEM:
disp=insn.operands[0].mem.disp
imm=insn.operands[1].imm & 0xffffffff if insn.mnemonic!='not' else None
ops.append((insn.mnemonic, disp, imm))
BASE=0x200; BLOCK=0x400; TOTAL=0x8c000
for i in range(TOTAL//BLOCK):
base=BASE+i*BLOCK
for m,disp,imm in ops:
o=base+disp
cur=struct.unpack_from('<I', reloc, o)[0]
if m=='not': new=(~cur)&0xffffffff
elif m=='add': new=(cur+imm)&0xffffffff
elif m=='sub': new=(cur-imm)&0xffffffff
else: new=cur^imm
struct.pack_into('<I', reloc, o, new)
plain=bytes(reloc[BASE:BASE+0x20])
print([hex(struct.unpack_from('<I', plain, i)[0]) for i in range(0,0x20,4)])
"
['0x46110', '0x3d362', '0x284', '0x49f54', '0x4a1a4', '0x8b906', '0x8c000', '0x398b13f8']
| 偏移 | 值 | 含义(由后续消费方确定) |
|---|---|---|
+0x04 | 0x3D362 | LZNT1 压缩长度 |
+0x10 | 0x4A1A4 | 压缩流在本缓冲内的偏移 |
+0x14 | 0x8B906 | 解压函数在本缓冲内的偏移(对应 call 0x1400A6B06) |
+0x18 | 0x8C000 | 整段缓冲长度 |
0x4A1A4 + 0x3D362 = 0x87506 < 0x8C000,压缩流落在 unpacker 之前,边界闭合。
四、LZNT1 解压
解压器对 je X; jne X 做不透明谓词(恒跳到 X,用于打断静态分析),随后 VirtualAlloc(size = 3 * [buf+4], MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE),再调用 RtlDecompressBuffer,格式参数为 ecx = 0x102,即 COMPRESSION_FORMAT_LZNT1 | COMPRESSION_ENGINE_MAXIMUM:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
src=open('.tmp/stage1.bin','rb').read()
code=src[0x8b906:0x8b906+0x60]
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(code, 0x1400a6b06):
print(hex(insn.address), insn.mnemonic, insn.op_str)
"
0x1400a6b06 push r15
0x1400a6b08 push r14
0x1400a6b0a push r13
0x1400a6b0c je 0x1400a6cfd
0x1400a6b12 jne 0x1400a6cfd
0x1400a6b18 lea rax, [rsp + 0x38]
0x1400a6b1d mov qword ptr [rsp + 0x28], rax
0x1400a6b22 mov qword ptr [rsp + 0x20], rdi
0x1400a6b27 mov ecx, 0x102
0x1400a6b2c mov rdx, rsi
按块实现 LZNT1(位移相对当前 4K 块,不是整段输出):
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
src=open('.tmp/stage1.bin','rb').read()
blob=src[0x4a1a4:0x4a1a4+0x3d362]
print('csize', len(blob), 'hdr', hex(struct.unpack_from('<H', blob, 0)[0]))
def chunk(c):
out=bytearray()
while c:
flags,c=c[0],c[1:]
for i in range(8):
if not c: break
if (flags>>i)&1==0:
out.append(c[0]); c=c[1:]; continue
if len(c)<2: break
flag=struct.unpack_from('<H',c,0)[0]; c=c[2:]
pos,l_mask,o_shift=len(out),0xFFF,12
p=pos-1 if pos else 0
while p>=0x10:
l_mask>>=1; o_shift-=1; p>>=1
length=(flag&l_mask)+3
offset=(flag>>o_shift)+1
for _ in range(length):
out.append(out[-offset])
if len(out)>0x1000: break
return bytes(out)
def lznt1(buf):
out=bytearray()
while len(buf)>=2:
hdr=struct.unpack_from('<H',buf,0)[0]
n=(hdr&0xFFF)+1
if len(buf)<2+n: break
ch,buf=buf[2:2+n],buf[2+n:]
out += chunk(ch) if hdr&0x8000 else ch
return bytes(out)
out=lznt1(blob)
print('out', len(out), out[:8])
"
csize 250722 hdr 0xb9e7
out 371200 b'\x89PNG\r\n\x1a\n'
解压得到 371,200 字节,产物 SHA256 与沙箱 buffer 记录完全一致:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import hashlib
print(hashlib.sha256(open('.tmp/beacon.bin','rb').read()).hexdigest())
"
a027e061daa0b016971bc902abe95b1f4f67cdf79fb8ec08276115760e62b20c
该值与沙箱 JSON buffer[0].sha256(a027e061…b20c)逐位相同,证明解包链路正确。
为何要换成 PNG 头。 DOS stub 被 \x89PNG\r\n\x1a\n 覆盖,MZ 签名因此消失,使 IsBadPE 类扫描和部分内存取证工具把这块当成图片或未知数据。但 PE 可选头仍完整保留在偏移 0x11C(COFF)、0x130(OptionalHeader)处,加载器照样能识别。
五、Beacon 映像
5.1 头部与导出
内存 dump 与解压产物前 371,200 字节的关系需要先厘清:解压产物是未映射的 PE,ImageBase = 0x180000000;内存 dump 是已映射并应用重定位后的映像,ImageBase = 0x1AB1EC00000,长度 0x60000(389,576 字节)。
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
b=open('.tmp/beacon.bin','rb').read()
d=open('.tmp/beacon.dump','rb').read()
print('unmapped size', len(b), 'imagebase', hex(struct.unpack_from('<Q', b, 0x130+24)[0]))
print('mapped size', len(d), 'imagebase', hex(struct.unpack_from('<Q', d, 0x130+24)[0]))
print('mapped entry ', hex(struct.unpack_from('<I', d, 0x130+16)[0]))
print('mapped imagesz', hex(struct.unpack_from('<I', d, 0x130+56)[0]))
"
unmapped size 371200 imagebase 0x180000000
mapped size 389576 imagebase 0x1ab1ec00000
mapped entry 0x37ef3
mapped imagesz 0x60000
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ xxd -l 32 extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636721.memdump/c9684f8292838ae1b93f1fc41abb0b1f70de016830dcf18caea910c62738959f/3476_15778690255720482026
00000000: 8950 4e47 0d0a 1a0a 1801 0000 0000 0000 .PNG............
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('.tmp/beacon.dump','rb').read()
coff=0x11c
machine,nsec,ts,_,_,opt,ch=struct.unpack_from('<HHIIIHH', d, coff)
print('machine', hex(machine), 'nsec', nsec, 'opt', hex(opt))
opt_start=coff+20
print('magic', hex(struct.unpack_from('<H', d, opt_start)[0]))
print('imagesize', hex(struct.unpack_from('<I', d, opt_start+56)[0]))
print('export name', d[0x56742:0x56742+8])
sec=opt_start+opt
for i in range(nsec):
name=d[sec:sec+8].split(b'\x00')[0]
vs,va,rs,ptr=struct.unpack_from('<IIII', d, sec+8)
print(name.decode(), 'va', hex(va), 'vs', hex(vs), 'raw', hex(ptr), 'rs', hex(rs))
sec+=40
"
machine 0x8664 nsec 6 opt 0xf0
magic 0x20b
imagesize 0x60000
export name b'x64.dll'
.text va 0x1000 vs 0x1ec01000 raw 0x400 rs 0x3dc00
.rdata va 0x3f000 vs 0x1ec3f000 raw 0x3e000 rs 0x18c00
.data va 0x58000 vs 0x1ec58000 raw 0x56c00 rs 0x1c00
.pdata va 0x5c000 vs 0x1ec5c000 raw 0x58800 rs 0xe00
.gfids va 0x5d000 vs 0x1ec5d000 raw 0x59600 rs 0x200
.reloc va 0x5e000 vs 0x1ec5e000 raw 0x59800 rs 0x1200
导出名 x64.dll(RVA 0x56742)。.rdata 同时存在 ChaCha/Salsa 常量 "expand 32-byte k" 与 "expand 16-byte k",说明后续流密码按 256-bit / 128-bit 两套布局准备。
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
d=open('.tmp/beacon.dump','rb').read()
i=d.find(b'expand 32-byte k')
print('rva', hex(i))
print(d[i:i+32])
"
rva 0x3fbd0
b'expand 32-byte kexpand 16-byte k'
IAT 已填成 0x7FFE… 用户态指针,dump 取自 beacon 已完成导入解析之后:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('.tmp/beacon.dump','rb').read()
print('iat0', hex(struct.unpack_from('<Q', d, 0x3f000)[0]))
print('iat1', hex(struct.unpack_from('<Q', d, 0x3f008)[0]))
"
iat0 0x7ffe2449b930
iat1 0x7ffe244854b0
5.2 导入名 XOR
导入 DLL 名不是明文,用 12 字节密钥 03 01 05 01 08 04 08 04 04 05 03 08 还原:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('.tmp/beacon.dump','rb').read()
KEY=bytes.fromhex('030105010804080404050308')
off=0x5674c
while off+20<=len(d):
ilt,ts,fwd,name,iat=struct.unpack_from('<IIIII', d, off)
if name==0: break
raw=d[name:name+40].split(b'\x00')[0]
print(bytes(raw[i]^KEY[i%12] for i in range(len(raw))))
off+=20
"
b'USER32.dll'
b'KERNEL32.dll'
b'ADVAPI32.dll'
b'WINHTTP.dll'
b'CRYPT32.dll'
b'ntdll.dll'
b'MPR.dll'
b'Secur32.dll'
b'SHLWAPI.dll'
b'DNSAPI.dll'
WINHTTP.dll 与 DNSAPI.dll 只出现在内层。外层导入表看不到 HTTP C2,是因为协议栈在解包之后才存在。密钥由 KERNEL32.dll 与密文 HDWOMH;6*aod 对拍得到:位置 0 起 H^K=0x03, D^E=0x01, W^R=0x05 …,12 字节循环。沙箱 dll_loaded 同样列出 WINHTTP.dll、DNSAPI.dll,与静态还原一致。
5.3 WinHTTP 函数名还原
WINHTTP 导入表项的函数名使用同一 XOR 密钥,前 12 字节可读,超过 12 字节处因密钥回绕而尾部乱码,但函数名前缀足以定位:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('.tmp/beacon.dump','rb').read()
KEY=bytes.fromhex('030105010804080404050308')
def dec(b):
raw=b.split(b'\x00')[0]
return bytes(raw[i]^KEY[i%12] for i in range(len(raw)))
ilt,ts,fwd,name,iat=struct.unpack_from('<IIIII', d, 0x56788)
i=0
while True:
thunk=struct.unpack_from('<Q', d, ilt+i*8)[0]
if thunk==0: break
print(hex(iat+i*8), dec(d[thunk+2:thunk+2+40]))
i+=1
"
0x3f4b0 b'WinHttpQueryNfrfMskohfaln'
0x3f4b8 b'WinHttpOpen'
0x3f4c0 b'WinHttpAddRe{rctxMog`bqs'
0x3f4c8 b'WinHttpSendRovsb\x7fq'
0x3f4d0 b'WinHttpConneis'
0x3f4d8 b'WinHttpOpenRovsb\x7fq'
0x3f4e0 b'WinHttpRecei|bTb\x7fuehwb'
0x3f4e8 b'WinHttpReadDksg'
0x3f4f0 b'WinHttpCloseBfhc``'
可辨识的前缀依次为 WinHttpQuery…、WinHttpOpen、WinHttpAdd…、WinHttpSend…、WinHttpConnect、WinHttpOpenRequest、WinHttpReceive…、WinHttpReadData、WinHttpClose…。这是一套完整的 WinHTTP 客户端会话 API 序列。
六、.data 短字符串:HTTP 外观
记录格式:16 字节 key || uint32 n || n 字节密文,整条记录再 16 字节对齐。第一遍用 ct[i] ^= key[i % 16] 能还原 n ≤ 16 的短串(.biz、POST、Host: ),但 n > 16 时尾部乱码。那不是密文被截断,而是回绕 XOR 不是真实算法。
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
d=open('.tmp/beacon.dump','rb').read()
off=0x58000
while off+20<=0x59200:
key=d[off:off+16]
n=struct.unpack_from('<I', d, off+16)[0]
if not 1<=n<=800:
off+=16; continue
ct=d[off+20:off+20+n]
pt=bytes(ct[i]^key[i%16] for i in range(len(ct)))
if len(pt)>=2 and pt[1]==0:
s=pt.decode('utf-16le','replace').split('\x00')[0]
else:
s=pt.decode('latin1','replace')
print(hex(off), n, repr(s)[:60])
step=20+n
off += step + ((16-step%16)%16)
"
0x58000 6 'exe'
0x58020 6 'scr'
0x58040 20 'chrome.eպό'
0x58070 20 'msedge.eͺ㽡'
0x580a0 16 '\\Windows'
0x580d0 22 'ProgramDͣՈݿ'
0x58100 32 'Applicatͫὣ㵰ᬬ㭎ὕᝪ\uf25d'
0x58140 20 '\\MicrosoͤŰ'
0x58220 6 'base64'
0x58240 10 'knock'
0x582c0 12 'MainID'
0x58310 34 'SessionI൦ܙ罠㭰畳\u1f5a煿畵\u0e72'
0x58350 30 'HostProcէᵯἍ祲ଛὙᝫ'
0x58430 18 'parentbiզ'
0x58540 8 '.biz'
0x585b0 406 'Mozilla/ķܮശତ畒睃ᅧ፺ɤ㩯੯\uf26b繐窧Ṍ∸༴㴮ጯ蠟杊涪⮦ㅻశ㰴\u1c39ﰰ氡咁㒇썜൧㍗ᥳ\ue776儑宬䖵틪ี㘳\ue5de\ueec6婣仯
0x58760 12 'Host: '
0x58780 8 'POST'
0x58ce0 14 'APPDATA'
0x58d60 14 '\\%s.bin'
0x58d90 2 'sm'
0x58ec0 8 '%x%x'
0x58ee0 12 '%x%x%x'
0x58f00 56 'Global\\Mͷլݪ᭭㭫\uf77eἕ\u139bฮᩭḨṱ㉦凞\ueb4d\ue68cയ᭩ᅸᕪ'
0x59130 14 'svchost'
0x59160 32 'TrustedIŬݳᵺ७ᕦ㝸畓ᎃ'
0x591d0 24 'explorerԬݥݶ\u197d'
6.1 真实解码器还原完整长串
解码入口 0xE050(rcx = 字符串对象,rdx = 记录)。它先 malloc(n+3),把密文拷进新缓冲,再调用 0xD9C0 做真正的变换。用 unicorn 跑样本自己的 0xE050(对 0x3715C/0x5D90 分配器做挂钩)得到完整明文,n > 16 不再截断:
| RVA | n | 明文 |
|---|---|---|
0x58040 | 20 | chrome.exe |
0x58070 | 20 | msedge.exe |
0x580D0 | 22 | ProgramData |
0x58100 | 32 | Application Data |
0x58310 | 34 | SessionIdentifier |
0x58350 | 30 | HostProcessName |
0x58540 | 8 | .biz |
0x585B0 | 406 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 MicroMessenger/6.5.2.501 NetType/WIFI WindowsWechat QBCore/3.43.884.400 QQBrowser/9.0.2524.400 |
0x58760 | 12 | Host: |
0x58780 | 8 | POST |
0x58D60 | 14 | \%s.bin |
0x58EC0 | 8 | %x%x |
0x58EE0 | 12 | %x%x%x |
0x58F00 | 56 | Global\Multiarch.m0yv-%s-inf |
0x58F50 | 52 | Global\Multiarch.m0yv-%s-b |
0x58FA0 | 48 | SeTakeOwnershipPrivilege |
0x59160 | 32 | TrustedInstaller |
0x591D0 | 24 | explorer.exe |
表里没有任何静态主机名。.biz 本身 n = 8,四个 UTF-16 字符,本来就是完整 TLD,不存在“长串被截断丢了 FQDN”。
与 C2 直接相关的是 .biz、Host: 、POST、User-Agent、%x%x%x。0x32604 是自实现宽字符拼接(逐字走到 \x00 再拷贝),把 TLD .biz 接到已经写好的标签后面:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
b=open('.tmp/beacon.bin','rb').read()
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(b[0x400+(0x32604-0x1000):0x400+(0x32604-0x1000)+0x30], 0x32604):
print(hex(insn.address), insn.mnemonic, insn.op_str)
"
0x32604 cmp word ptr [rcx], 0
0x32608 mov r8, rcx
0x3260b je 0x32618
0x3260d add r8, 2
0x32611 cmp word ptr [r8], 0
0x32616 jne 0x3260d
0x32618 sub r8, rdx
0x3261b movzx eax, word ptr [rdx]
0x3261e mov word ptr [r8 + rdx], ax
0x32623 lea rdx, [rdx + 2]
0x32627 test ax, ax
APPDATA + \%s.bin 对应沙箱落地 %APPDATA%\d6739753dac71fc6.bin。互斥量模板是 Global\Multiarch.m0yv-%s-inf / -b,%s 填入样本 id d6739753dac71fc6。
Host 头怎么拼。 0x150FC 把 .biz 记录送进 0xE050,随后 0x15163 call 0x32604 把它接到 r14(函数第三参,已有标签的缓冲)末尾。标签本身来自 0x2042B:解码格式串 %x%x%x,对 GetComputerNameW(IAT 0x3F170)的结果做 0x7C00 哈希,再 wsprintfW(USER32 IAT 0x3F4A0)打成三段十六进制。因此 HTTP 头是
Host: + wsprintfW("%x%x%x", hash(计算机名)) + .biz
这是本机标识,不是 C2 的 DNS 名字。TCP 连接目标仍是解密块里的 IP,不需要 .biz 查询——这也解释了 PCAP 里为什么既没有 24.74.72.2 也没有 .biz。
sm / gd / mid / mdt 等两字节键以及 command、module、cid、type、data、status、se_* 系列状态码,构成 beacon 的 C2 命令与响应字段集合。
七、ChaCha8 解密 C2 块
7.1 密钥与 nonce 就在常量旁边
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ xxd -s 0x3f878 -l 48 .tmp/beacon.dump
0003f878: 3832 3730 3937 3834 3534 3735 3833 3933 8270978454758393
0003f888: 3237 3438 3335 3237 3933 3237 3537 3438 2748352793275748
0003f898: 0030 3938 3433 3139 3700 7d00 0000 2c00 .09843197.}...,.
| 项目 | RVA | 值 |
|---|---|---|
| 256-bit 密钥 | 0x3F878 | ASCII 82709784547583932748352793275748 |
| 8 字节 nonce | 0x3F899 | ASCII 09843197 |
| ChaCha 常量 | 0x3FBD0 | expand 32-byte k |
| 密文 | 0x58560 | 76(0x4C)字节 |
密文:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ xxd -s 0x58560 -l 0x4c .tmp/beacon.dump
00058560: ce0e 211d 291f be1d 518e 1a2b 6388 aec9 ..!.)...Q..+c...
00058570: f11b 582a f4ce 16ef ffb4 1e3b 3bde 9491 ..X*.......;;...
00058580: d856 f506 7d47 1fc5 bb15 5f96 ea58 88ad .V..}G...._..X..
00058590: 08dd 90fd dacb eb29 155d e9f5 63fb c245 .......).]..c..E
000585a0: b8d6 ca09 ee35 0607 9b4c 74e3 .....5...Lt.
调用点 0x15333 的反汇编(注意 0x15332 处的 jmp 是不透明谓词产物的干扰字节,真实指令从 0x15333 开始):
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
b=open('.tmp/beacon.bin','rb').read()
print(b[0x400+(0x15333-0x1000):0x400+(0x15333-0x1000)+28].hex())
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(b[0x400+(0x15333-0x1000):0x400+(0x15333-0x1000)+28], 0x15333):
print(hex(insn.address), insn.mnemonic, insn.op_str)
"
488d15263204004889f94989f041b94c000000e8053d0100
0x15333 lea rdx, [rip + 0x43226]
0x1533a mov rcx, rdi
0x1533d mov r8, rsi
0x15340 mov r9d, 0x4c
0x15346 call 0x29050
rip + 0x43226 在 0x1533A 处解析为 0x1533A + 0x43226 = 0x58560,正是密文地址;r9d = 0x4C 是长度。0x29050 是一条 jmp 0x28CE0 的薄封装:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
b=open('.tmp/beacon.bin','rb').read()
print(b[0x400+(0x29050-0x1000):0x400+(0x29050-0x1000)+5].hex())
"
e98bfcffff
E9 8B FC FF FF → rel32 = 0xFFFFFC8B = -0x375,0x29055 - 0x375 = 0x28CE0。
7.2 状态布局:这是 ChaCha,不是 Salsa
0x28C50 是 key setup。它在 r8d == 0x100(256 位密钥)时把 64 字节状态装填为 ChaCha 矩阵:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
b=open('.tmp/beacon.bin','rb').read()
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(b[0x400+(0x28c50-0x1000):0x400+(0x28c50-0x1000)+0x70], 0x28c50):
print(hex(insn.address), insn.mnemonic, insn.op_str)
"
0x28c50 mov eax, dword ptr [rdx]
0x28c52 mov dword ptr [rcx + 0x10], eax
0x28c55 mov eax, dword ptr [rdx + 4]
0x28c58 mov dword ptr [rcx + 0x14], eax
0x28c5b mov eax, dword ptr [rdx + 8]
0x28c5e mov dword ptr [rcx + 0x18], eax
0x28c61 mov eax, dword ptr [rdx + 0xc]
0x28c64 mov dword ptr [rcx + 0x1c], eax
0x28c67 lea rax, [rdx + 0x10]
0x28c6b cmp r8d, 0x100
0x28c72 cmovne rax, rdx
0x28c76 lea r8, [rip + 0x16f53]
0x28c7d lea r9, [rip + 0x16f5c]
0x28c84 cmove r9, r8
0x28c88 mov edx, dword ptr [rax]
0x28c8a mov dword ptr [rcx + 0x20], edx
0x28c8d mov edx, dword ptr [rax + 4]
0x28c90 mov dword ptr [rcx + 0x24], edx
0x28c93 mov edx, dword ptr [rax + 8]
0x28c96 mov dword ptr [rcx + 0x28], edx
0x28c99 mov eax, dword ptr [rax + 0xc]
0x28c9c mov dword ptr [rcx + 0x2c], eax
0x28c9f mov dword ptr [rcx], 0x61707865
0x28ca5 mov rax, qword ptr [r9 + 4]
0x28ca9 mov qword ptr [rcx + 4], rax
0x28cad mov eax, dword ptr [r9 + 0xc]
0x28cb1 mov dword ptr [rcx + 0xc], eax
cmp r8d, 0x100 选择 32 字节还是 16 字节密钥;r8/r9 分别指向 "expand 32-byte k" 与 "expand 16-byte k"。mov dword [rcx], 0x61707865 即 "expa",常量占 word 0–3,密钥占 4–11,计数器 12–13,nonce 14–15。这是 ChaCha 矩阵,不是 Salsa(Salsa 的常量散布在四角)。
0x28CC0 是 nonce 与计数器设置:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
b=open('.tmp/beacon.bin','rb').read()
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(b[0x400+(0x28cc0-0x1000):0x400+(0x28cc0-0x1000)+0x18], 0x28cc0):
print(hex(insn.address), insn.mnemonic, insn.op_str)
"
0x28cc0 mov qword ptr [rcx + 0x30], 0
0x28cc8 mov eax, dword ptr [rdx]
0x28cca mov dword ptr [rcx + 0x38], eax
0x28ccd mov eax, dword ptr [rdx + 4]
0x28cd0 mov dword ptr [rcx + 0x3c], eax
64 位计数器(+0x30)清零,8 字节 nonce 写入 +0x38。
核心 0x28D90 的四分轮旋转常量为 16 / 12 / 8 / 7(ChaCha 特征;Salsa 是 7/9/13/18):
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
b=open('.tmp/beacon.bin','rb').read()
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(b[0x400+(0x28e50-0x1000):0x400+(0x28e50-0x1000)+0x20], 0x28e50):
print(hex(insn.address), insn.mnemonic, insn.op_str)
"
0x28e50 add ecx, r13d
0x28e53 xor edx, ecx
0x28e55 rol edx, 0x10
0x28e58 add ebp, edx
0x28e5a xor r13d, ebp
0x28e5d rol r13d, 0xc
0x28e61 add ecx, r13d
0x28e64 xor edx, ecx
0x28e66 rol edx, 8
0x28e69 add ebp, edx
0x28e6b xor r13d, ebp
0x28e6e rol r13d, 7
轮数由循环控制决定:[rsp+0x4c] 初值 0xFFFFFFF8(-8),每次 double-round 结束 add … , 2 后 jne 回跳,-8 → -6 → -4 → -2 → 0 共 4 个 double-round = 8 轮 = ChaCha8。
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
b=open('.tmp/beacon.bin','rb').read()
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(b[0x400+(0x28de8-0x1000):0x400+(0x28de8-0x1000)+0x10], 0x28de8):
print(hex(insn.address), insn.mnemonic, insn.op_str)
for insn in md.disasm(b[0x400+(0x28f90-0x1000):0x400+(0x28f90-0x1000)+0x8], 0x28f90):
print(hex(insn.address), insn.mnemonic, insn.op_str)
"
0x28de8 mov dword ptr [rsp + 0x4c], 0xfffffff8
0x28f90 add dword ptr [rsp + 0x4c], 2
0x28f95 jne 0x28e50
通用 ChaCha20 库默认 20 轮,对这份样本会解出乱码;必须按 8 轮实现。
7.3 两种独立途径解密,结果一致
不重写算法,直接让 unicorn 跑 dump 里的 0x28C50(key setup)、0x28CC0(nonce)、0x28CE0(keystream XOR):
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
from unicorn import Uc, UC_ARCH_X86, UC_MODE_64, UC_HOOK_CODE
from unicorn.x86_const import *
d=open('.tmp/beacon.dump','rb').read()
BASE=0x140000000; RET=0x42424242
uc=Uc(UC_ARCH_X86, UC_MODE_64)
uc.mem_map(BASE, 0x100000); uc.mem_write(BASE, d)
uc.mem_map(0x70000000, 0x20000)
uc.mem_map(0x71000000, 0x1000)
uc.mem_map(0x72000000, 0x1000)
uc.mem_map(0x42424000, 0x1000)
STATE, OUT = 0x71000000, 0x72000000
def call(fn, args):
rsp=0x7000fff8
uc.reg_write(UC_X86_REG_RSP, rsp)
uc.mem_write(rsp, struct.pack('<Q', RET))
for r,v in zip([UC_X86_REG_RCX,UC_X86_REG_RDX,UC_X86_REG_R8,UC_X86_REG_R9], args):
uc.reg_write(r, v)
def hook(uc, addr, size, user):
if addr==RET: uc.emu_stop()
h=uc.hook_add(UC_HOOK_CODE, hook)
uc.emu_start(BASE+fn, RET, timeout=3000000, count=500000)
uc.hook_del(h)
call(0x28c50, [STATE, BASE+0x3f878, 0x100, 0x40])
call(0x28cc0, [STATE, BASE+0x3f899])
call(0x28ce0, [STATE, BASE+0x58560, OUT, 0x4c])
pt=bytes(uc.mem_read(OUT, 0x4c))
print(pt.hex())
print('.'.join(str(b) for b in pt[:4]))
"
184a4802010000000200000066e34b34f63be6588aec2200594e41b6941ff4a4fa82d8cd57223f6f62ac8b112e64034dd35678ec7b58b8ac04c4e82944146d3ff9e07753610a4b3451a3014a
24.74.72.2
另用一份独立实现的纯 Python ChaCha8(不依赖样本代码)复核:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import struct
def qr(s,a,b,c,d):
s[a]=(s[a]+s[b])&0xffffffff; s[d]=((s[d]^s[a])<<16 | (s[d]^s[a])>>16)&0xffffffff
s[c]=(s[c]+s[d])&0xffffffff; s[b]=((s[b]^s[c])<<12 | (s[b]^s[c])>>20)&0xffffffff
s[a]=(s[a]+s[b])&0xffffffff; s[d]=((s[d]^s[a])<<8 | (s[d]^s[a])>>24)&0xffffffff
s[c]=(s[c]+s[d])&0xffffffff; s[b]=((s[b]^s[c])<<7 | (s[b]^s[c])>>25)&0xffffffff
def block(key, nonce, counter):
const=b'expand 32-byte k'
st=list(struct.unpack('<16I', const+key+struct.pack('<Q', counter)+nonce))
w=st[:]
for _ in range(4):
qr(w,0,4,8,12); qr(w,1,5,9,13); qr(w,2,6,10,14); qr(w,3,7,11,15)
qr(w,0,5,10,15); qr(w,1,6,11,12); qr(w,2,7,8,13); qr(w,3,4,9,14)
return struct.pack('<16I', *[(w[i]+st[i])&0xffffffff for i in range(16)])
d=open('.tmp/beacon.dump','rb').read()
key=d[0x3f878:0x3f878+32]
nonce=d[0x3f899:0x3f899+8]
ct=d[0x58560:0x58560+0x4c]
ks=b''
c=0
while len(ks)<len(ct):
ks+=block(key, nonce, c); c+=1
pt=bytes(a^b for a,b in zip(ct, ks))
print(pt[:16].hex())
print('.'.join(str(b) for b in pt[:4]))
"
184a4802010000000200000066e34b34
24.74.72.2
两条独立路径输出完全一致,明文布局:
| 偏移 | 字节 | 读取 |
|---|---|---|
+0 | 18 4A 48 02 | IPv4 24.74.72.2(按内存顺序 a.b.c.d) |
+4 | 01 00 00 00 | uint32 1(拷入会话对象 +0x18) |
+8 | 02 00 00 00 | uint32 2(拷入会话对象 +0x1C) |
+12 | 32 字节 | X25519 公钥(Montgomery u 坐标) |
+44 | 32 字节 | 第二把 32 字节曲线公钥(握手 / KDF) |
把 18 4A 48 02 当成小端 uint32 再按网络序解释会得到 2.72.74.24。Windows in_addr 在内存中是网络字节序,四个字节按出现顺序读成点分十进制即 24.74.72.2。ARIN 记录该地址属于 NET-24-74-0-0-1,Charter Communications / 原 Time Warner Cable 的 24.74.0.0/16,落在家宽网段,可能是失陷主机或跳板。
+12 起 64 字节不是主机名。 消费点把全局解密缓冲(0x59B60)加 0xC 后交给 0x2F810:
0x2181f mov rdx, [rip+0x3833a] ; 解密块指针
0x21826 add rdx, 0xc ; +12
0x21837 call 0x2f810 ; X25519
0x2F810 开头把 r8 的 32 字节拷到栈上做标量,并做 Curve25519 标准 clamp(首字节 and 0xF8,末字节 and 0x3F; or 0x40),再对 rdx 做 fe_frombytes(0x29570),然后 254 轮 Montgomery ladder(r13d 从 0xFE 减到 -1)。这是 X25519 ECDH,输入是 32 字节公钥,不是 ASCII 域名。
第二把 32 字节由 add r9, 0x2C(块基址 +44)送给 0x2F570。该函数用 SHA-512 初始化常量 0x6A09E667F3BCC908(0x2ED00)做三次 0x2EDA0 更新,并进入 0x2C5A0(21-bit limb 的 Ed25519 点解码)。两把钥匙分别用于 ECDH 与握手认证,二进制里没有把它们当主机名解析的路径。
+12 66e34b34f63be6588aec2200594e41b6941ff4a4fa82d8cd57223f6f62ac8b11
+44 2e64034dd35678ec7b58b8ac04c4e82944146d3ff9e07753610a4b3451a3014a
7.4 端口:WinHttpConnect 的 r8w
WinHttpConnect 的 IAT 项在 0x3F4D0。call 位于 0x1615E,FF 15 后 rip 相对寻址:rip + 6 = 0x16164,rel32 0x0002936C,0x16164 + 0x2936C = 0x3F4D0,对拍成立。
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
b=open('.tmp/beacon.bin','rb').read()
print(b[0x400+(0x16155-0x1000):0x400+(0x16155-0x1000)+20].hex())
md=Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(b[0x400+(0x16155-0x1000):0x400+(0x16155-0x1000)+14], 0x16155):
print(hex(insn.address), insn.mnemonic, insn.op_str)
import struct
d=open('.tmp/beacon.dump','rb').read()
print('IAT 0x3f4d0 =', hex(struct.unpack_from('<Q', d, 0x3f4d0)[0]))
"
e96641b850004531c9ff156c930200
0x16156 mov r8w, 0x50
0x1615b xor r9d, r9d
0x1615e call qword ptr [rip + 0x2936c]
IAT 0x3f4d0 = 0x7ffe1cdfb000
WINHTTP_INTERNET_PORT 是 16 位,mov r8w, 0x50 即端口 80;r9d = 0 为 dwReserved。协议栈与字符串一致:WinHTTP + POST + Host: 。连接目标是解密块里的 IP,不是 PCAP 里的域名。
八、沙箱行为与流量阴性
8.1 行为摘要
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import json
j=json.load(open('extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636724.json'))
print('score', j['info']['score'], 'duration', j['info']['duration'])
p=j['behavior']['processes'][0]
print('process', p['process_name'], 'pid', p['pid'], 'cmd', p['command_line'])
s=j['behavior']['summary']
print('mutex', s['mutex'])
print('written', s['file_written'])
print('dll', [x for x in s['dll_loaded'] if x.upper() in ('WINHTTP.DLL','DNSAPI.DLL')])
"
score 1.8 duration 90
process alg.exe pid 3476 cmd "C:\Users\Administrator\Desktop\alg.exe"
mutex ['Global\\Multiarch.m0yv-d6739753dac71fc6-inf', 'Global\\Multiarch.m0yv-d6739753dac71fc6974f67db-b']
written ['C:\\Users\\Administrator\\AppData\\Roaming\\d6739753dac71fc6.bin', 'C:\\Program Files (x86)\\Common Files\\Adobe\\ARM\\1.0\\armsvc.exe']
dll ['DNSAPI.dll', 'WINHTTP.dll']
沙箱解码出的互斥量名(Global\Multiarch.m0yv-d6739753dac71fc6-inf)与静态还原的 .data 记录(偏移 0x58f00,前缀 Global\M…)互相印证,d6739753dac71fc6 同时是落地文件名主体。
API 统计里出现一次 RtlDecompressBuffer、一次 StartServiceCtrlDispatcherW、两次 NtCreateMutant、一次 NtDelayExecution:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import json
j=json.load(open('extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636724.json'))
v=j['behavior']['apistats']['3476']
for k in ('RtlDecompressBuffer','StartServiceCtrlDispatcherW','NtCreateMutant','NtDelayExecution','NtAllocateVirtualMemory','NtProtectVirtualMemory','NtCreateFile','NtWriteFile'):
print(k, v.get(k))
"
RtlDecompressBuffer 1
StartServiceCtrlDispatcherW 1
NtCreateMutant 2
NtDelayExecution 1
NtAllocateVirtualMemory 262
NtProtectVirtualMemory 9
NtCreateFile 3
NtWriteFile 38
沙箱 90 秒内没有完成 C2 会话(network.tcp 为空、无 WinHTTP 调用记录),评分 1.8 偏低;但互斥量、落地文件、WINHTTP.dll 加载、RtlDecompressBuffer 调用已经足够把行为钉在这套 beacon 上。
落地 d6739753dac71fc6.bin 12,320 字节,与互斥量中的 16 位 hex 相同:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ uv run --no-project python -c "
import json
j=json.load(open('extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636724.json'))
for d in j['dropped']:
print(d['name'], d['size'], d['filepath'])
"
d6739753dac71fc6.bin 12320 C:\Users\Administrator\AppData\Roaming\d6739753dac71fc6.bin
armsvc.exe 路径是 Adobe ARM 服务目录,属于服务劫持类持久化(样本具备 StartServiceCtrlDispatcherW 导入,与 ALG 服务外壳一致)。
8.2 PCAP 不含 24.74.72.2
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ tshark -r extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636718.pcap/pcaps/dump.pcap -Y "ip.addr==24.74.72.2"
无输出,确认抓包中没有与 C2 地址的任何一个数据包。全部会话属于分析虚拟机噪声:
┌──(z㉿kali)-[~/Projects/virus-re]
└─$ tshark -r extract/b901f02c/win10_1903_x64_2016_b901f02cbc5d4779f475584cb260f78228512945fde8601742c191053dda16fe_1789636718.pcap/pcaps/dump.pcap -Y dns.flags.response==1 -T fields -e dns.qry.name -e dns.a
www.google.com 185.45.5.35
update.googleapis.com 0.0.0.0
ctldl.windowsupdate.com 218.61.116.102,222.136.225.197,42.56.88.118,222.136.225.187
www.googleapis.com 172.217.114.4,172.217.116.4,172.217.118.4,172.217.113.4,172.217.119.4,172.217.117.4,172.217.112.4,172.217.115.4
dns.weixin.qq.com.cn 220.196.154.84,116.130.224.150,111.206.147.156,111.206.147.210,157.148.62.186,157.148.61.231,157.148.61.199,140.207.56.91,116.130.224.205,140.207.56.104
szlong.weixin.qq.com 157.148.62.239,157.255.209.167,157.255.209.219,157.255.191.88,157.148.61.140,157.148.59.148,157.148.59.238,157.255.191.89
accounts.google.com 173.194.43.84
没有 24.74.72.2,也没有 .biz 查询。抓包不能用来否定静态还原:beacon 导入了 Sleep(NtDelayExecution 被记录一次),90 秒窗口可以整段睡过去;且 C2 以 IP 直连,本来也不需要 DNS。
九、执行流程
| 阶段 | 地址 / 位置 | 操作 |
|---|---|---|
| EP 劫持 | 0x14000DB80 | call 0x140188000,返回后 jmp 原 ALG(0x14000D910) |
| m0yv stub | 0x140188000 | 560 × 256 dword 补丁,范围 0x8C000(cmp ecx, 0x8C000) |
| LZNT1 | call 0x1400A6B06 | RtlDecompressBuffer(0x102) → 371,200 B |
| 映射 | 未映射 ImageBase=0x180000000;沙箱 0x1AB1EC00000 | PNG 头 + PE32+ x64.dll,解析内部 IAT |
| 互斥量 | Global\Multiarch.m0yv-d6739753dac71fc6-inf | 单实例 / 壳水印(2 个 mutant) |
| 落地 | %APPDATA%\d6739753dac71fc6.bin | 12,320 B |
| 服务路径 | Adobe\ARM\1.0\armsvc.exe | 写入 |
| C2 解密 | 0x28C50 / 0x28CC0 / 0x28CE0 | ChaCha8,密钥/nonce 见附录 |
| 连接 | 0x16156 | mov r8w, 80 + WinHttpConnect → 24.74.72.2:80 |
| HTTP | Host: + wsprintfW("%x%x%x", hash(计算机名)) + .biz,POST | 标签是本机标识,不是 C2 DNS |
| 握手 | 0x2F810 / 0x2F570 | X25519 ECDH(块 +12)+ SHA-512/Ed25519(块 +44) |
十、检测与处置
主机:
- PE 存在名为
.reloc、原始大小约0x16E000、且 BaseReloc 数据目录为空 的节 - 入口仅
call该节末页再跳回.text - 进程加载了版本资源自称
ALG.exe、却在内存中映射出导出x64.dll的映像 - 互斥量
Global\Multiarch.m0yv-*(两个,-inf与-b) %APPDATA%\下 16 位 hex 文件名.bin(本例 12,320 字节)- 向
Adobe\ARM\1.0\armsvc.exe写入 - 同进程内
RtlDecompressBuffer(0x102)后紧跟一次 RWXVirtualAlloc/VirtualProtect
静态:
- 导入表无
WINHTTP,运行后加载WINHTTP.dll、DNSAPI.dll - 内层
.data中出现key16 || n || ct记录,明文段含Host:、POST、.biz "expand 32-byte k"后紧跟 32 位 ASCII 数字密钥与 8 位 ASCII nonce09843197- 导入 DLL 名以 12 字节密钥
03 01 05 01 08 04 08 04 04 05 03 08循环 XOR 存储
网络:
alert tcp $HOME_NET any -> 24.74.72.2 80 (
msg:"b901f02c beacon WinHTTP POST";
content:"POST"; http_method;
content:"Host: "; http_header;
sid:1000002; rev:1;)
处置上,先在边界阻断 24.74.72.2:80。该地址落在家宽网段,可能是失陷主机或跳板,封锁后仍应在终端查互斥量与 %APPDATA% 落地文件。ChaCha 密钥写在映像里,改 IP 即可换马;检测不能只靠这一条 IP。内存保护策略方面,任意代码保护(Arbitrary Code Guard)会阻止加载器所需的 RWX 分配,从而切断“解密→执行”链条,这是比封 IP 更根本的一层。
原先两处未决均已闭合。+12 起 64 字节是两把 32 字节曲线公钥,不是主机名;.biz 是完整 TLD,Host 标签由 %x%x%x 在运行时从计算机名哈希生成,映像里没有静态 FQDN。连接目标仍是 24.74.72.2:80。
附录
| 常量 | 值 | 来源 |
|---|---|---|
| 样本 SHA256 | b901f02c…dda16fe | 磁盘文件 |
| 磁盘样本大小 | 1,594,368 字节 | ls -l |
| 时间戳 | 0x00000000(已清零) | COFF 头 0x134 |
| 入口 | 0x14000DB80 | OptionalHeader |
| 壳 stub | 0x140188000 | EP call 目标 |
| 补丁表起始 | 0x14001B200(文件 0x17600) | lea rbx, [rip-0x16CE2A] |
| 补丁窗口 | 0x400 × 560,共 0x8C000 | stub cmp ecx, 0x8C000 |
| 补丁操作数 | 256 条(add/sub/xor/not) | capstone 计数 |
| LZNT1 源 | 缓冲 +0x4A1A4,长 0x3D362 | 补丁后头字段 |
| LZNT1 格式 | 0x102(LZNT1 + MAXIMUM) | mov ecx, 0x102 |
| 解压产物 | 371,200 字节,a027e061…b20c | SHA256,与沙箱 buffer 一致 |
| Beacon 未映射 ImageBase | 0x180000000 | 解压产物 OptionalHeader |
| Beacon 沙箱映射基址 | 0x1AB1EC00000 | dump OptionalHeader |
| Beacon 导出 | x64.dll(RVA 0x56742) | 导出表 |
| 导入 XOR | 030105010804080404050308 | 与 KERNEL32.dll 对拍 |
| ChaCha 常量 | "expand 32-byte k" @ 0x3FBD0 | dump .rdata |
| ChaCha 轮数 | 8(4 double-round,add [rsp+0x4c], 2) | 0x28F90 |
| ChaCha 密钥 | 82709784547583932748352793275748 | dump RVA 0x3F878 |
| ChaCha nonce | 09843197 | dump RVA 0x3F899 |
| C2 密文 | 76 字节 @ RVA 0x58560 | lea rdx(0x15333)+ r9d=0x4C |
| C2 明文头 | 18 4A 48 02 01 00 00 00 02 00 00 00 | unicorn / 独立 ChaCha8 |
| X25519 公钥 | 32 B @ 明文 +12 | 0x2F810 clamp + fe_frombytes |
| 第二把曲线公钥 | 32 B @ 明文 +44 | add r9, 0x2C → 0x2F570 SHA-512 |
| C2 IP:端口 | 24.74.72.2:80 | 明文 +0;mov r8w, 0x50 @ 0x16156 |
| Host 标签 | wsprintfW("%x%x%x", hash(GetComputerNameW)) + .biz | 0x2042B / 0x15163 |
| 字符串解码 | 0xE050(非 key[i%16] 回绕) | unicorn 跑样本函数 |
| 互斥量 | Global\Multiarch.m0yv-d6739753dac71fc6-inf | 沙箱 + .data 0x58F00 |
| 落地文件 | %APPDATA%\d6739753dac71fc6.bin(12,320 B) | 沙箱 dropped |
| 服务路径 | Adobe\ARM\1.0\armsvc.exe | 沙箱 file_written |
| 诱饵身份 | ALG.exe 10.0.19041.746 / PDB ALG.pdb | 版本资源、调试目录 |