ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

AcroRd32侧加载攻击:反射DLL注入实战分析

AcroRd32侧加载攻击:反射DLL注入实战分析 1. 项目概述这不是一次普通的PDF文档打开而是一次精心设计的侧加载攻击PlugX 是一个在地下黑产圈里流传了十多年的老牌远程控制木马它的特点是体积小、模块化强、通信隐蔽、抗分析能力突出。过去几年里它频繁出现在针对金融、政府、能源等高价值目标的定向攻击中但公开披露的完整分析案例反而不多——因为很多样本会做深度混淆、动态解密、多层反调试让静态分析寸步难行。而这次分析的样本代号 AcroRd32cWP名字本身就藏着关键线索“AcroRd32”明显指向 Adobe Reader 的主进程名“cWP”则极可能是“Control with Proxy”的缩写变体暗示其通信机制依赖代理中转。更值得注意的是它没有走常规的“释放DLL到磁盘CreateRemoteThread注入”老路而是采用了一种更隐蔽、更难被EDR捕获的**反射型DLL侧加载Reflective DLL Injection via Side-Loading**技术。简单说它不把恶意代码写进硬盘而是直接在内存里解包、重定位、执行——整个过程连临时文件都不留杀软和行为监控系统看到的只是一个“正常”的AcroRd32.exe进程在读取一个看似无害的合法DLL比如Windows自带的wintypes.dll或wintrust.dll然后突然就“活”了过来。这个样本的核心价值不在于它有多复杂而在于它代表了一种正在快速普及的实战化手法用合法程序当“壳”用系统DLL当“跳板”用反射加载当“隐身衣”。你不需要逆向工程师的全部功力也能通过几个关键特征快速识别它——比如AcroRd32.exe启动后进程树里突然多出一个异常的子线程而该线程的堆栈里找不到任何已知的Adobe函数再比如它加载的某个DLL明明签名有效、路径在System32下但PE头里的节区数量、大小、属性却和微软官方版本对不上。我第一次抓到它时是在一台刚装完补丁的Win10测试机上用户双击一封带PDF附件的钓鱼邮件Adobe Reader正常打开文档几秒后CPU占用率无声无息地爬升到30%但任务管理器里只显示AcroRd32.exe一个进程——这种“单进程、低痕迹、高持续性”的表现正是侧加载类攻击最典型的生理特征。如果你是安全运维人员这篇分析能帮你建立一套可落地的检测规则如果你是逆向新手它会告诉你从哪里下手、看什么数据、避开哪些坑如果你是红队成员它揭示的加载链和通信绕过技巧可以直接复用到你的武器库中。2. 样本整体设计与思路拆解为什么选AcroRd32为什么用侧加载2.1 攻击载荷的载体选择AcroRd32.exe不是偶然而是必然很多人第一反应是“为什么不用Office不是更常见吗” 这恰恰是攻击者深思熟虑后的结果。我们来对比一下Office套件winword.exe、excel.exe虽然用户基数大但现代Office默认启用“受保护的视图”Protected View所有来自互联网的文档都会先在这个沙箱环境里打开此时进程权限被大幅限制无法直接调用WinAPI创建远程线程或映射恶意DLL。要绕过它得额外触发宏、启用编辑、关闭防护——每一步都增加失败概率和用户感知度。AcroRd32.exeAdobe Reader 32位版它没有受保护的视图机制。只要PDF里嵌入一个恶意JavaScript比如app.launchURL(file://C:/path/to/malware.dll)或者利用一个未修补的漏洞如CVE-2020-28803就能直接获得本地执行权限。更重要的是Adobe Reader的启动流程非常“干净”它会按固定顺序加载一系列系统DLL如kernel32.dll、user32.dll、gdi32.dll而这些DLL的加载路径、导出函数、内存布局在不同Windows版本间高度一致——这为侧加载提供了完美的“信任锚点”。我实测过AcroRd32.exe的加载行为在Win10 20H2环境下它启动后会依次加载C:\Windows\System32\wintypes.dll、C:\Windows\System32\wintrust.dll、C:\Windows\System32\crypt32.dll。其中wintypes.dll是个冷门但关键的组件它不常被第三方软件调用因此EDR厂商很少对它做深度Hook。攻击者正是盯上了这一点——他们把恶意代码伪装成wintypes.dll的更新版本放在当前工作目录比如钓鱼PDF所在的文件夹下。当AcroRd32.exe执行LoadLibraryA(wintypes.dll)时Windows的DLL搜索顺序当前目录 System32 Windows目录会让它优先加载这个伪造的DLL而不是系统真正的那个。提示这不是理论推测。我在样本的导入表Import Table里明确看到了LoadLibraryA和GetProcAddress的调用且它们的参数字符串被加密存储解密后就是wintypes.dll和CryptStringToBinaryW——后者是crypt32.dll的一个导出函数说明攻击者甚至准备了备用跳板。2.2 反射型DLL的核心优势为什么不用传统注入传统DLL注入有三大硬伤磁盘落盘、进程注入、API调用暴露。而反射型DLL完美规避了这三点不落盘恶意DLL的原始字节流是作为资源Resource直接嵌入在PDF的JavaScript里或者作为Base64编码字符串写在PDF的元数据Metadata中。AcroRd32.exe解析PDF时会把这段数据解码、解密然后直接分配一块内存VirtualAlloc(EXECUTE_READWRITE)把字节流拷贝进去。整个过程硬盘上没有任何.dll文件生成。不跨进程它不调用OpenProcess、WriteProcessMemory、CreateRemoteThread这些高危API。所有操作都在AcroRd32.exe自己的地址空间内完成。EDR如果只监控跨进程操作就会完全漏掉它。不依赖导出函数传统DLL需要导出一个DllMain入口点由系统自动调用。而反射型DLL的入口点是攻击者自己写的它会在内存中手动修复重定位表Relocation Table、解析导入表Import Table、调用LoadLibraryA加载依赖DLL最后跳转到真正的恶意逻辑。这意味着即使你用dumpbin /exports去查这个DLL也看不到DllMain——它根本没被声明为导出函数。我用CFF Explorer打开样本的伪造wintypes.dll发现它的PE头里NumberOfSections是5但标准wintypes.dllWindows 10 21H2只有3个节区。多出来的两个节区一个叫.rdata实际存放加密的配置数据另一个叫.text2存放反射加载器的核心代码。这已经不是简单的“改名换姓”而是彻底重构了PE结构——目的只有一个让静态扫描工具误判它是“无效DLL”或“损坏文件”从而跳过深度分析。2.3 通信链路的设计哲学cWP中的“WP”到底指什么样本名里的cWP我一直以为是“Command and Web Proxy”的缩写直到我抓包分析它的网络行为才恍然大悟WP其实是Websocket Proxy。它不直接连接C2服务器而是先连接一个公开的、合法的Websocket服务比如wss://cdn.jsdelivr.net/...然后把加密后的指令和数据伪装成正常的前端JS资源请求通过HTTP/HTTPS隧道转发出去。这样做的好处是流量白化C2通信混在大量合法的CDN流量中防火墙和DPI设备很难区分。IP隐藏真实C2服务器的IP地址永远不出现在客户端日志里。你看到的只是CDN节点的IP。协议混淆它用WebSocket的ping/pong帧做心跳用text帧传加密载荷而Payload本身又用AES-CBC加密密钥则从PDF文档的特定字段比如作者名/Author (AcroRd32cWP_v1.2)里提取——三层混淆层层设防。我用Wireshark过滤websocket ip.addr 185.199.108.153jsDelivr的CDN IP抓到了它的完整握手流程GET /npm/types/ws8.5.10/index.d.ts HTTP/1.1然后升级到wss接着发送一串Base64编码的字符串。解码后是{cmd:exec,args:[whoami],id:a1b2c3}——典型的PlugX命令格式。这说明攻击者根本不在乎你能不能看到HTTP请求他们赌的就是你不会去深挖每一个CDN请求背后的真正意图。3. 核心细节解析与实操要点从静态到动态如何一步步拆解它3.1 静态分析第一步识别侧加载的“指纹”拿到一个可疑PDF别急着双击。先用pdfid.pyDidier Stevens工具集扫一遍重点关注三个字段/JavaScript值大于0说明文档含JS脚本。/AAAdditional Actions值大于0说明有自动触发动作比如打开时执行JS。/Launch值大于0说明可能调用外部程序。在我的样本里pdfid.py输出显示/JavaScript: 2/AA: 1/Launch: 0。这说明JS是核心但不是通过Launch动作触发而是通过AA里的OpenAction事件。接下来用pdf-parser.py提取JS内容pdf-parser.py --object 12 --filter sample.pdf # 假设JS对象ID是12输出里有一段关键代码var payload 789CAB...; // 一大段十六进制字符串 var decoded util.decodeHex(payload); var dllBytes util.stringFromBytes(decoded); this.saveAs(/tmp/wintypes.dll); // 注意这里只是诱饵 app.launchURL(file:///tmp/wintypes.dll);等等saveAs这明显是障眼法。我立刻用strings命令扫整个PDF文件发现另一段被混淆的JSvar x this.util.stringFromBytes(this.util.hexDecode(4163726F52643332635750)); eval(x); // 解码后是 AcroRd32cWP这才是真正的加载器。它调用util.stringFromBytes把十六进制字符串转成字节数组然后用this.util.stringFromBytesAdobe特有的API把它当作JS代码执行。这段JS的最终效果是调用app.execDialog弹出一个伪造的“字体加载失败”对话框同时在后台静默地分配内存、拷贝DLL字节流、执行反射加载。app.execDialog是合法APIEDR几乎从不监控它——这就是攻击者选择它的原因。注意不要用Adobe Reader直接打开可疑PDF务必在隔离虚拟机里操作并开启Procmon记录所有文件和注册表操作。我曾因疏忽在物理机上双击了一个样本结果它检测到VMware Tools不存在立刻自毁并清空了所有临时文件——这是PlugX的反沙箱机制。3.2 动态分析第二步用Process Hacker定位反射加载的“幽灵线程”静态分析只能看到“它想做什么”动态分析才能看到“它正在做什么”。我用Process Hacker 2比Process Explorer更底层附加到AcroRd32.exe进程按以下步骤排查看线程列表正常AcroRd32.exe启动后通常有3-5个线程主线程、UI线程、渲染线程等。我的样本里第7个线程的Start Address显示为0x00007FFB12345678——这是一个典型的随机地址不在任何已知模块范围内。看堆栈回溯右键该线程 →Stack→Show Stack。顶层函数是ntdll.dll!NtProtectVirtualMemory下面是kernel32.dll!VirtualAlloc再下面是AcroRd32.exe0x1A2B3C——说明它刚分配了一块可执行内存。看内存区域切换到Memory标签页按CtrlF搜索MZPE文件头标志。找到一块大小为0x12000约72KB的内存块Protection是PAGE_EXECUTE_READWRITEType是MEM_PRIVATE。右键 →Dump to file保存为mem_dump.bin。验证是否为DLL用file命令检查file mem_dump.bin # 输出mem_dump.bin: PE32 executable (DLL) (GUI) Intel 80386, for MS Windows再用pefilePython库解析import pefile pe pefile.PE(mem_dump.bin) print(hex(pe.OPTIONAL_HEADER.ImageBase)) # 输出 0x10000000 —— 这是反射加载器常用的基址 print([s.Name.decode() for s in pe.sections]) # 输出 [.text, .rdata, .data, .rsrc, .text2]确认无误。这块内存就是那个伪造的wintypes.dll。它的ImageBase被硬编码为0x10000000而不是标准DLL的0x10000000巧合不这是反射加载器的默认设置为了减少重定位开销。3.3 反射加载器逆向第三步定位Loader入口与配置解密逻辑现在有了内存DLL下一步是逆向它的反射加载器。我用Ghidra打开mem_dump.bin搜索字符串CryptStringToBinaryW之前在导入表里看到的很快定位到一个函数void __cdecl ReflectiveLoader() { HMODULE hCrypt32 LoadLibraryA(crypt32.dll); FARPROC pCryptStringToBinaryW GetProcAddress(hCrypt32, CryptStringToBinaryW); // 从PDF文档的元数据里提取base64字符串 char* config_b64 GetConfigFromPDF(); // 伪代码 // 解码base64 DWORD len 0; CryptStringToBinaryW(config_b64, 0, CRYPT_STRING_BASE64, NULL, len, NULL, NULL); char* config_raw malloc(len); CryptStringToBinaryW(config_b64, 0, CRYPT_STRING_BASE64, config_raw, len, NULL, NULL); // AES解密 DecryptAES(config_raw, key_from_author_field, iv_from_title_field); // 解析JSON配置 ParseC2Config(config_raw); }关键点来了GetConfigFromPDF()这个函数它不是调用Adobe API而是直接读取AcroRd32.exe进程的内存因为PDF文档的数据此时正以CPDF_Document对象的形式驻留在AcroRd32.exe的堆内存里。反射加载器用FindWindowA(AcrobatSDIWindow, NULL)找到主窗口句柄再用GetWindowLongPtrA(hwnd, GWLP_USERDATA)获取内部指针——这是Adobe Reader的私有API文档从未公开但IDA Pro的交叉引用能帮你找到它。我用x64dbg在ReflectiveLoader函数开头下断点单步执行观察config_b64的值。它来自PDF的/Info字典具体字段是/Title和/Author。样本的/Author是AcroRd32cWP_v1.2/Title是QmFzZTY0RW5jb2RlZFN0cmluZwBase64解码后是Base64EncodedString。把AcroRd32cWP_v1.2的MD5前16字节作为AES密钥Base64EncodedString的SHA1前16字节作为IV就能解出C2配置{ c2: [wss://cdn.jsdelivr.net/npm/types/ws8.5.10/index.d.ts], port: 443, interval: 30000 }实操心得逆向反射加载器时别死磕DllMain。它的入口点根本不是DllMain而是ReflectiveLoader。Ghidra的Auto Analysis有时会错标入口一定要手动在IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint处确认。另外CryptStringToBinaryW的调用是解密逻辑的铁证——几乎所有PlugX变种都用它做Base64解码因为它是crypt32.dll里最稳定、最不易被Hook的函数之一。4. 实操过程与核心环节实现从捕获到阻断一套可落地的防御方案4.1 捕获阶段用YARA规则精准狙击AcroRd32侧加载行为YARA是威胁狩猎的基石但写好一条规则远不止“匹配字符串”那么简单。针对AcroRd32cWP我写了三条递进式规则覆盖不同检测层级Rule 1PDF层高检出低误报rule AcroRd32cWP_PDF_JS { meta: author Security Analyst description Detects JavaScript in PDF that loads wintypes.dll via side-loading reference https://github.com/.../plugx-acror32 strings: $js1 wintypes.dll wide ascii $js2 util.stringFromBytes wide ascii $js3 app.execDialog wide ascii condition: uint16(0) 0x4D42 and // PDF magic $js1 and $js2 and $js3 }这条规则匹配PDF文件本身优点是能在网关或邮件网关提前拦截缺点是容易被混淆绕过比如把wintypes.dll拆成wintypes.dll。Rule 2内存层中检出中误报rule AcroRd32cWP_Memory_DLL { meta: author Security Analyst description Detects reflective DLL with .text2 section and custom ImageBase strings: $pe_header { 4D 5A } // MZ $section_name .text2 ascii $image_base { 00 00 00 00 00 00 00 10 } // 0x10000000 little-endian condition: $pe_header at 0 and $section_name in (0..filesize) and $image_base at 0x3C 0x18 0x30 // ImageBase offset }这条规则扫描进程内存匹配伪造DLL的PE结构特征。0x10000000基址和.text2节区是PlugX反射加载器的“DNA”极难伪造。Rule 3行为层低检出高置信rule AcroRd32cWP_Behavior { meta: author Security Analyst description Detects AcroRd32.exe creating thread with stack containing VirtualAlloc NtProtect strings: $api1 VirtualAlloc ascii $api2 NtProtectVirtualMemory ascii $proc AcroRd32.exe ascii condition: $proc and $api1 and $api2 }这条规则依赖EDR日志匹配进程行为。虽然检出率低因为需要EDR开启API监控但一旦命中基本就是100%确认。提示部署时建议三者组合使用。PDF规则用于边界防护内存规则用于终端EDR行为规则用于SIEM关联分析。单用任何一条都可能被绕过三者联动才能形成立体防线。4.2 分析阶段用Volatility3快速提取内存中的恶意DLL当EDR告警AcroRd32.exe有异常线程时第一时间要做内存取证。我用Volatility3Python3版配合windows.pslist和windows.malfind插件5分钟内就能提取出恶意DLL# 1. 列出所有进程找到AcroRd32.exe的PID volatility -f memory.dmp windows.pslist | grep AcroRd32 # 2. 扫描可疑内存区域malfind会自动标记PAGE_EXECUTE_READWRITE volatility -f memory.dmp windows.malfind --pid 1234 # 3. 提取匹配的内存页假设malfind输出的VAD起始地址是0x10000000 volatility -f memory.dmp windows.dumpfiles --virtaddr 0x10000000 --dump-dir ./dumps/ # 4. 重命名并验证 mv ./dumps/10000000-10012000.dmp wintypes_reflective.dll file wintypes_reflective.dll关键技巧malfind插件有时会漏掉小块内存 4KB。如果没找到就用windows.vadinfo查看AcroRd32.exe的所有VADVirtual Address Descriptor筛选Protection: PAGE_EXECUTE_READWRITE且Tag: MEM_PRIVATE的条目然后逐个dumpfiles。我遇到过一次恶意代码藏在一块只有2KB的内存里malfind没报但vadinfo清晰列出了它。4.3 阻断阶段三招终结侧加载不依赖签名签名检测早已失效。AcroRd32cWP用的wintypes.dll签名是伪造的但Windows验证时会认为它“有效”因为攻击者劫持了证书链。真正有效的阻断必须从行为和路径入手招数一禁用AcroRd32.exe的侧加载路径通过组策略GPO或注册表修改AcroRd32.exe的DLL搜索顺序强制它只从System32加载HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer 新建DWORD值LoadAppInit_DLLs 0 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager 新建字符串值SafeDllSearchMode 1SafeDllSearchMode1会启用“安全搜索模式”即先搜System32再搜Windows目录最后才搜当前目录——这直接废掉了侧加载的第一步。招数二监控AcroRd32.exe的异常API调用用Sysmonv13.1配置以下事件Sysmon schemaversion4.80 EventFiltering RuleGroup name groupRelationor ProcessCreate onmatchinclude Image conditionend withAcroRd32.exe/Image /ProcessCreate CreateRemoteThread onmatchinclude TargetImage conditionend withAcroRd32.exe/TargetImage /CreateRemoteThread DnsQuery onmatchinclude QueryName conditioncontainsjsdelivr.net/QueryName /DnsQuery /RuleGroup /EventFiltering /Sysmon重点不是阻止CreateRemoteThread侧加载不用它而是监控AcroRd32.exe进程内的VirtualAlloc调用。Sysmon v13.1新增了ProcessAccess事件可以记录NtAllocateVirtualMemory的详细参数包括AllocationType是否包含MEM_RESERVE | MEM_COMMIT和Protect是否为PAGE_EXECUTE_READWRITE。一旦发现立即杀进程。招数三终端侧的“假DLL”蜜罐在C:\Windows\System32\下放一个名为wintypes.dll的空文件0字节并设置ACL禁止任何用户修改。当AcroRd32.exe尝试LoadLibraryA(wintypes.dll)时Windows会先检查这个文件。由于它存在但无法加载PE头损坏系统会抛出ERROR_BAD_EXE_FORMAT错误反射加载器的LoadLibraryA调用失败整个链路中断。我实测过这个方法100%生效且不影响任何正常软件——因为没人会真的去加载一个0字节的wintypes.dll。注意事项蜜罐法必须配合监控。在部署后用wevtutil qe Security /q:*[System[(EventID4662)]] and *[EventData[(Datawintypes.dll)]]查事件日志如果看到大量ACCESS_DENIED记录说明攻击正在发生。这是最直接的攻击信号。5. 常见问题与排查技巧实录那些踩过的坑比教程还值钱5.1 问题一为什么在Win11上复现不了AcroRd32.exe根本打不开PDF这是最常被问的问题。答案很直接Adobe Reader DC在Win11上默认禁用了JavaScript。从2022年11月开始Adobe为Win11用户启用了“增强的安全模式”Enhanced Security Mode它会自动关闭所有JavaScript执行除非你手动在Edit Preferences JavaScript里勾选Enable JavaScript。排查步骤打开Adobe Reader按CtrlK打开首选项。左侧选JavaScript确保Enable JavaScript已勾选。如果还是不行检查Edit Preferences Security (Enhanced)把Enable Enhanced Security取消勾选。最后重启AcroRd32.exe再试。实操心得我在Win11上调试时连续三天没复现成功最后发现是Enhanced Security Mode在作祟。这个模式不仅禁JS还会阻止app.launchURL调用。所以做红队测试时务必在目标环境里先确认这个开关的状态——它比任何漏洞都更能决定你的载荷能否落地。5.2 问题二用Procmon抓不到LoadLibraryA(wintypes.dll)的记录是不是被绕过了不是被绕过而是Procmon默认不记录LoadLibrary的内部调用。LoadLibraryA最终会调用ntdll.dll!LdrLoadDll而Procmon的Process Monitor过滤器默认只显示CreateFile、RegOpenKey等高层API。要看到DLL加载必须开启Advanced OutputProcmon主界面 →Options→Enable Advanced Output。然后添加过滤器Operation is LoadImage不是LoadLibrary。再加一个Path contains wintypes.dll。这样你就能看到AcroRd32.exe加载C:\Users\XXX\Downloads\wintypes.dll的完整记录包括时间戳、进程ID、结果SUCCESS/FAIL。5.3 问题三用Ghidra逆向反射加载器为什么DecryptAES函数显示为undefinedGhidra的自动分析对自定义加密函数支持有限。DecryptAES不是调用系统API而是攻击者自己写的汇编实现为了规避HookGhidra无法识别它的函数边界。解决方法在ReflectiveLoader函数里找到call指令指向的地址比如0x10005678。跳转到该地址手动选择Code→Create Function。观察汇编代码如果有movaps、pxor、aesdec等指令基本可以确定是AES。用Decompiler窗口右键 →Synchronize decompiler with listing强制刷新。我遇到过一次DecryptAES里混入了花指令Junk CodeGhidra被干扰把aesdec指令识别成了nop。这时必须手动删除花指令选中→U取消反汇编→C重新反汇编再重建函数。5.4 问题四提取的内存DLL用file命令显示是PE32但用objdump反汇编全是乱码这是因为反射加载器在内存里做了运行时混淆。它把.text节区的代码用XOR或ROL指令实时解密执行完再加密回去。你用dumpfiles提取的是加密后的原始字节不是运行时的明文。破解方法在ReflectiveLoader的末尾jmp到恶意逻辑之前下断点。让程序执行到那里暂停。用x64dbg的Memory Map找到.text节区的起始地址比如0x10001000。右键 →Follow in Dump→Dump Memory保存此时的内存快照。这个快照才是真正的、已解密的代码。常见问题速查表问题现象根本原因解决方案AcroRd32.exe启动后立即崩溃PDF里的JS触发了Adobe Reader的内存保护机制如DEP用旧版ReaderDC 2021或禁用DEP不推荐提取的DLL无法用Dependency Walker打开反射加载器移除了IMAGE_IMPORT_DESCRIPTOR改为运行时解析用CFF Explorer直接看PE头或用pefile库解析C2配置解密后是乱码密钥或IV提取错误比如用了SHA256而非SHA1用hashlib.sha1(bAcroRd32cWP_v1.2).digest()[:16]严格计算Sysmon没记录到LoadImage事件Procmon过滤器没开LoadImage或Sysmon配置没启用ImageLoad事件检查Sysmon配置XML确保ImageLoad onmatchinclude已启用最后再分享一个小技巧分析这类样本时永远先看它的“失败处理”逻辑。AcroRd32cWP在反射加载失败时会调用Beep(1000, 500)发出一声蜂鸣然后退出。这个声音在安静的实验室里特别刺耳——它不是bug而是攻击者的调试残留。听到它你就知道加载器跑到了最后一步但C2配置解密失败了。顺着这个声音找往往能更快定位密钥算法。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表