先问你一个问题:如果你在键盘上敲下一条命令,这条命令的内容会存在哪里?
答案可能出乎你的意料——它就躺在那个控制台程序的内存里,安安静静,等待被读取。安全研究员 Two Seven One Three 最近披露的一种新型 Windows 进程注入方法,正是抓住了这个再普通不过的细节,绕开了与远程代码注入绑得最紧的两个 API:VirtualAllocEx 和 WriteProcessMemory。
这项被命名为“控制台命名管道注入”(Console Named-Pipe Injection)的技术,把有效载荷字节通过子控制台进程重定向的标准输入悄悄送进去,然后“回收利用”Windows 已经分配好的内存。整个过程没有跨进程内存分配,没有跨进程内存写入,那些基于经典“分配—写入—执行”序列搭建起来的检测机制,在这一刻集体失语。
进程注入意味着什么,做过防御的人都清楚:攻击者让另一个进程替自己执行任意代码,合法应用程序就成了最好的掩体。MITRE ATT&CK 把这类行为归在 T1055 之下,无论是远线程注入、进程镂空还是线程劫持,万变不离其宗。而 EDR 厂商早就摸透了套路——只要盯死那几个敏感 API 的调用链,内存信号加线程信号一关联,注入行为基本无所遁形。
传统注入的“三板斧”,说起来简单得令人发指
回顾一下教科书式的远程线程注入:先用 OpenProcess 拿到目标进程的句柄,再用 VirtualAllocEx 在对方地址空间里圈出一块地,接着 WriteProcessMemory 把 shellcode 原样搬过去,最后 CreateRemoteThread 起一个新线程,让 RIP 指向那片刚刚铺好砖的内存。有些变种会改用“线程劫持”——SuspendThread 把线程按暂停,SetThreadContext 篡改指令指针,ResumeThread 放行,同样能达到目的。
这套流程之所以被盯得死死的,是因为跨进程的内存操作在良性软件里实在太罕见了。EDR 通过用户态钩子加内核回调,把 WriteProcessMemory 这一类调用视作高危遥测事件,现代攻击者想找一条绕开传统内存操作签名的路,已经不是一天两天的事。
灵感往往藏在最不起眼的地方
Two Seven One Three 的突破口,来自一个朴素的好奇:当你打开一个交互式控制台程序——比如 nslookup.exe 或者 netsh.exe——在 conhost.exe 的陪伴下敲入命令时,这些命令内容到底存放在进程的哪个角落?
答案是:就在程序自己的内存里。控制台总要先把输入缓冲起来,再解析执行,这中间必然存在一段可读写的内存区域。而关键在于,Windows 父进程创建子进程时,完全可以把命名管道的读取端指定为子进程的标准输入句柄,自己攥着写入端不放。微软的文档写得很明白,这是 CreateProcess 配合 STARTUPINFO 结构就能完成的标准操作。
这样一来,攻击路径豁然开朗:与其费尽周折往别人进程里写内存,不如创建一个带重定向标准输入的控制台子进程,然后用 WriteFile 把有效载荷字节顺着管道“喂”进去。对控制台程序来说,这不过是一次再正常不过的输入;对注入器来说,目标字节却已经神不知鬼不觉地落在了对方进程的地址空间里。
找到它,点亮它,劫持它
字节进去了,接下来的问题是定位。概念验证程序的做法相当巧妙:在有效载荷最前面拼接一段独特的标记字节,然后在可访问的内存中搜索这段标记,找到之后,标记末尾的地址就是 shellcode 的入口点,RIP 直接指向 marker_addr + sizeof(marker) 即可。
定位完成,还差最后两步。注入器调用 VirtualProtectEx,把已经提交的那片页面保护属性从“读写”翻成“可执行读写”。这一步需要 PROCESS_VM_OPERATION 权限,微软也建议在更改线程上下文之前先把线程挂起。随后,SuspendThread、SetThreadContext、ResumeThread 一气呵成,线程的指令指针被掰向那片刚刚获得执行权限的内存,恶意代码就此在“合法程序”的身体里苏醒过来。
演示输出里,研究员在 nslookup.exe 的内存区域中锁定了 368 字节的有效载荷,保护属性从 PAGE_READWRITE 翻转为 PAGE_EXECUTE_READWRITE,主线程被稳稳地重定向到目标地址。没有 VirtualAllocEx,没有 WriteProcessMemory,代码照样跑了起来。
天下没有免费的午餐:坏字符的紧箍咒
这项技术并非百无禁忌。控制台解析输入时,有几个字节会被当成特殊控制符处理,有效载荷必须避开它们:0x0D(回车符 CR)、0x0A(换行符 LF)以及 0x1A(Ctrl+Z,历史上一直被当作文件结束标记)。一旦 payload 里混入这些字节,子进程会把它当作普通命令去执行,屏幕上只留下一句“找不到命令”,精心准备的 shellcode 也会从内存里消失得无影无踪。
换句话说,生成有效载荷时就得把编码器安排上,把这几个字节剔除或替换掉。所幸受限制的字符并不多,相比动辄一箩筐坏字符的其他注入场景,这里的约束已经算得上宽松。
与前辈研究相比,它赢在了“更像正常行为”
进程注入这个圈子里从来不缺新花样。SensePost 的 Max Hirschberger 和 Ogulcan Ugur 曾提出过独立但思路相近的技术,他们实测绕过了四款主流 EDR 产品,modexp 也探索过相关方向。不过那些方法或多或少有些“不自然”的痕迹:要么需要以挂起状态启动子进程,要么得在 lpCommandLine 或 lpEnvironment 字段里塞入格式异常的数据。
控制台命名管道注入则完全避开了这些别扭之处。子进程正常启动、正常运行,命令行和环境变量干干净净,父进程启动一个控制台工具并与之管道交互,在 Windows 世界里简直是家常便饭。这种与正常行为的高度重合,恰恰是它最危险的地方——不是说它能骗过所有 EDR,而是它让“看起来可疑”这个最基础的判断依据开始失效。
检测不能押注在单个 API 上
那么防守方该怎么办?答案其实很明确:别再盯着某几个 API 单打一,把完整的行为链关联起来看。
值得重点留意的信号包括——异常父进程创建带重定向句柄的交互式控制台二进制文件;向类似二进制文件的匿名标准输入管道写入大量数据;跨进程内存扫描行为;VirtualProtectEx 将既有页面远程翻转为可执行权限;以及随后出现的 SetThreadContext 加线程恢复操作。这些动作单拎出来都不算稀奇,组合在一起就相当罕见了。
遥测层面,Sysmon 的事件 ID 17 和 18 能提供命名管道层面的可见性,不过匿名的标准输入管道想要拿到更丰富的端点与句柄级细节,可能还得依赖更底层的采集能力。实战中的建议是:为组织内的控制台自动化行为建立基准画像,然后去找那些偏离基准的稀有组合,而不是见到 conhost.exe、nslookup.exe 或者任何一次管道操作就拉响警报——告警疲劳本身就会摧毁检测体系。
写在最后
这项研究给所有从事进程注入检测工程的人上了一课:可靠的检测必须理解进程是如何被创建的、句柄是如何共享的、内存保护是如何变化的、控制流又是如何被劫持的,而不是死守着某一条单一的技术路线。攻击者扔掉了 WriteProcessMemory,检测思路也该跟着升级了。攻防之间的这场猫鼠游戏,从来不会因为某个 API 被监控就画上句号——它只会在下一条命名管道里,悄然开始新的一局。