论文解读:NPM 投毒包为什么总是抓不准?ProfMalPlus 用证据推理重做检测流程,三个月揪出 597 个恶意包
在做前端或 Node.js 项目时,往往要引入大量第三方包。一次 npm install 带来的,是一整串很难人工审查的依赖树。攻击者可以把恶意逻辑藏进仿冒包、更新版本、安装脚本或入口文件,在安装或导入时触发;混淆代码、动态属性、函数别名和第三方调用又会隐藏真实行为,让检测器即使看见系统信息读取、命令执行和网络请求,也难以判断它们是在完成正常功能,还是已经构成数据窃取。
针对上述难题,复旦大学团队在论文「ProfMalPlus: Agent-Coordinated Detection of Malicious NPM Packages via Static–Dynamic Analysis Synergy」中提出了一套证据驱动的检测流程:程序分析工具先还原代码行为、整理证据,再由多个 LLM Agent 分层判断;证据不足时,系统继续查询第三方资料或进入沙箱补充运行时信息。
2026 年 4 月至 6 月,研究持续监控 107,287 个新发布的 NPM 包,发现 597 个此前未知的恶意包。作者称,这些包随后均被 NPM 确认并移除。ProfMalPlus 的核心变化,是把一次性的分类判断,改造成围绕证据持续补充和复核的调查过程。
为什么总抓不准恶意包?
作者通过三个案例说明,现有检测器即使发现了危险操作,也可能看不清操作本身及其相互关系。
第一个恶意包 bitsoex_react-design-system_14.1.4 会读取用户信息、执行系统命令并发起网络请求,但 os.userInfo()、execSync() 和 https.request() 都被动态属性隐藏成类似 _0x26cfab(0x1b8) 的表达式。静态分析可以追踪控制关系和数据流,却难以确认运行时才生成的真实 API。
第二个案例利用了 JavaScript 的对象特性。代码把 os.hostname 和 os.homedir 作为函数对象传入 tryGet,再通过 toCall() 间接执行。源码里没有直接调用形式,依赖函数名或词法模式的检测器容易漏检。作者因此采用对象敏感分析,持续追踪变量、属性和参数实际指向的对象或函数。
第三个案例来自一个良性工具包。它会读取操作系统、执行磁盘命令并测试 HTTPS 连通性。只看 API 序列,这些操作与信息窃取程序很相似;但回到源码会发现,它们之间没有直接数据依赖,系统信息和命令结果也没有被发送出去。
三个案例共同暴露了一个核心矛盾:静态分析保留了代码上下文,却可能看不清运行时行为;动态分析能看到真实调用,又容易丢失行为之间的数据和控制关系。作者进一步将问题归纳为四类:混淆和动态调用难以解析,JavaScript 的对象引用与别名容易被遗漏,静态与动态证据缺少统一表示,以及行为抽象损失源码语义,难以支持可靠判断。ProfMalPlus 要解决的,正是如何把这些证据连起来,再围绕完整上下文判断代码意图。
为了便于计算,许多检测器会把源码抽象成词法特征、API 序列或行为图,再交给规则或模型分类。这些抽象能够降低代码复杂度,却可能丢失 URL 构造、数据内容和触发条件等源码语义,最终只输出一个难以追溯的“恶意”标签。
直接把完整源文件交给 LLM 同样存在问题:无关代码会稀释安全证据,并增加上下文和调用费用。ProfMalPlus 因此选择了一条中间路线:行为图负责还原关系,代码切片负责保留源码语义,LLM Agent 负责围绕证据判断意图。
从图分类器到证据推理:ProfMalPlus 如何重做检测流程?
ProfMalPlus 是团队前作 ProfMal 的扩展版本。它保留了 ProfMal 对象敏感的行为图构建和静动态融合能力,主要变化是将原来的图分类器替换为围绕注释代码切片展开的多 Agent 证据推理,并进一步加入第三方语义补全、按需动态增强和恶意代码定位。
作者将这种变化概括为:从端到端的图学习,转向源码语义层面的证据推理。
先把整个包整理成一份“带证据的案卷”
这条证据链从 package.json 开始。Script Analyzer 会检查 preinstall、install 和 postinstall 等安装脚本,识别可疑 Shell 命令,并找到安装或导入时可能被触发的 JavaScript 入口文件。
随后,系统使用 Joern 和 Jelly 构建行为图。行为图是一张程序执行关系图,记录控制流、控制依赖、数据依赖和函数调用,并标记敏感 API、条件敏感调用、未知调用、eval 和第三方库调用。
构建行为图后,系统从可疑节点出发,沿数据依赖和控制依赖提取相关源码,形成代码切片。切片只保留与可疑行为相关的数据来源、转换过程、触发条件和后续用途,同时修复语法结构,并保留跨函数、跨文件代码原有的位置和调用上下文。
程序分析得到的 API 类型、动态参数、返回值和行为图节点等证据,还会被直接写在对应源码旁边。
上图展示了从脚本分析、行为图和代码切片,到 Agent 判断、按需补证并更新切片的完整流程。
这个示例规模较小,因此切片保留了全部代码;面对更大的包时,系统会剔除与可疑节点无关的语句,只向 Agent 提供保留上下文和证据来源的相关代码。
多 Agent 如何完成判断与补证?
代码被整理成带注释的切片后,系统才进入多 Agent 推理阶段。
局部判断:单个片段能否证明恶意?
Local Behavior Judge Agent 会分别阅读每个代码切片,输出良性、恶意或待定。“待定”表示当前代码存在可疑行为,但缺少第三方方法语义、运行时命令等关键证据;系统会记录阻碍判断的具体节点,而不强行二选一。
自一致性验证:降低单次模型判断导致的误报告警
LLM 对同一段代码可能给出不同判断。ProfMalPlus 会让 Local Agent 对每个切片独立判断三次,再由 Verification Agent 综合检查。这里的自一致性并非简单多数投票:验证 Agent 会核对三次判断引用的代码节点、推理是否超出源码证据,以及不同结论之间是否矛盾;证据不足时,优先将结果调整为待定。
全局判断:多个片段能否拼成完整攻击链?
恶意行为可能分散在多个函数和切片中,例如分别完成凭据读取、数据编码和网络发送。Global Behavior Judge Agent 会按照控制流组织局部报告,识别信息外传、载荷下载执行、条件触发和文件写入加载等跨切片攻击链;只有链路得到具体节点证据支持时,才会判为恶意。若结论仍为待定,Router Agent 会识别缺失的证据类型,并选择后续补证方式。
先尝试补全第三方调用语义
如果缺少第三方方法的语义,系统会查询 NPM registry、README、类型声明和公开文档。考虑到包自身文档可能被攻击者控制,ProfMalPlus 还会综合 registry 和公开仓库中的发布、使用与维护信号,判断这些资料是否适合作为证据。这仍是一种启发式信任评估,不能保证文档真实可靠。
文档不够,再进入动态沙箱
如果缺少运行时参数、真实 API 名称或 eval 代码,系统会把包放进 Docker 沙箱,并用 NodeProf 进行插桩:记录函数调用关系、敏感 API 的参数和返回值,恢复动态调用、eval 代码及第三方方法实际触发的敏感行为。
这些证据会被合并回行为图和代码切片,供局部与全局 Agent 重新判断。整个流程按照“定位证据缺口—选择补证方式—更新切片—再次判断”循环,每一轮补充新的事实,而不是围绕同一份材料重复推理。
98.1% F1 背后:模型、成本与处理时间
作者在一个由 1,090 个恶意包和 3,000 个良性包组成的数据集上评测 ProfMalPlus。
最终结果为:
注:SocketAI 的模型后端在本实验中被替换为 GPT-5.4-mini。
ProfMalPlus 在作者设置下取得最高 F1,由 ProfMal 的 90.9% 提升至 98.1%,但结果需要结合数据和评测协议理解。作者从 13,467 个初始恶意样本中去重得到 1,658 个,再经过人工筛选保留 1,090 个;被移除的包括无实际恶意代码、无法在 Linux 沙箱执行的混淆样本,以及恶意行为不发生在安装或导入阶段的样本。良性集也只保留至少包含一个可疑锚点的包,因此并不等同于完整 NPM 分布。
此外,Cerebro、Malpacdetector 和 ProfMal 等学习型检测器采用 10 折交叉验证,GuardDog、SocketAI 和 ProfMalPlus 则在整个数据集上评测一次。更准确的结论是:在作者筛选后的安装期、导入期数据和既定协议下,ProfMalPlus 获得了最高 F1,横向比较时仍需考虑样本范围和评测方式差异。
LLM 骨干选择:更贵的模型表现会更好吗?
ProfMalPlus 的多个阶段都要调用 LLM,包括 Shell 命令分析、第三方语义补全、局部和全局判断、路由以及恶意代码定位。作者为此单独设置了 LLM 骨干实验:保持 Prompt、Agent 编排和其他参数不变,只更换模型。
在 ProfMalPlus 这条固定流水线中,DeepSeek-V4-Flash 同时取得了最高 F1、最高召回率和最低费用。Qwen-3.6-Flash 的精确率略高(98.1%),但召回率明显偏低,综合 F1 落后;GPT-5.4-mini 的费用是 DeepSeek-V4-Flash 的三倍多,三项效果指标却都是最低。
这组结果不能被扩展成“便宜模型普遍比贵模型更强”。作者只比较了三款轻量级模型,也没有测试更重的旗舰模型。
这组实验至少说明:在 ProfMalPlus 的固定流水线中,模型价格并不能直接预测实际效果。至于输入证据的组织方式、Prompt 和编排机制各自贡献多少,还需要进一步的单变量实验。
调用成本与处理时间
ProfMalPlus 的 LLM API 费用为 67.45 美元,约为 SocketAI 的 1/8,同时 F1 高出 3.3 个百分点。作者认为,原因之一是 SocketAI 会把完整源文件交给 LLM,而 ProfMalPlus 只输入安全相关切片。
这组数字只包含 LLM API 费用,不包括服务器、程序分析、沙箱和人工复核等成本;两套系统使用的模型后端也不同,因此不能把全部差距归因于代码切片。
从阶段分布看,89.9% 的良性终止决策流程和 89.5% 的恶意流程,都在静态行为图与注释切片的 Agent 判断阶段完成,无需第三方补全或动态增强。这里已经包含 LLM 推理,不能简化成“只靠传统静态分析”;表 3 统计的也是终止决策流程,而非包数量。作者没有进一步拆分各阶段在 67.45 美元中的费用占比。
费用更低,并不意味着处理更快。ProfMalPlus 平均分析一个包需要 394.37 秒,是所有对比工具中最慢的;SocketAI 为 176.91 秒,ProfMal 为 192.71 秒,Malpacdetector 只需 1.44 秒。
这反映了一个明确取舍:ProfMalPlus 减少了整文件 LLM 分析的 API 费用,却通过多轮证据收集和 Agent 推理增加了端到端处理时间。
从约 6.6 分钟/包的耗时推断,它目前更接近包平台审核、依赖准入或异步离线扫描工具,而不是要求低延迟的同步实时拦截器。这是基于作者耗时数据作出的工程判断,作者没有直接测试它在 CI 阻断场景中的表现。
消融实验说明:不同组件解决的是不同问题
最终 F1 只能说明系统整体有效,消融实验进一步解释了各组件的作用。
安装脚本检测负责补足攻击入口。 去掉恶意 Shell 命令检测后,召回率从 98.2% 降至 84.0%,F1 降至 90.4%,说明部分恶意行为直接藏在安装脚本中,后续 JavaScript 分析无法补回。
自一致性主要压制误报。 关闭“三次独立判断 + 验证 Agent”后,精确率从 98.0% 降至 89.5%,召回率则由 98.2% 微升至 98.4%。单次 LLM 判断更容易把证据不足的良性行为解释成恶意。
动态增强主要补回漏检。 去掉动态增强后,召回率降至 90.7%,F1 降至 94.3%,受影响的主要是依赖运行时参数、动态条件、混淆调用或 eval 才能确认的样本。
第三方语义补全主要降低沙箱调用。 去掉该模块后,F1 仅从 98.1% 降至 97.8%;完整系统中,Router Agent 在 187 个检测流程里调用了第三方语义补全,其中 143 个流程仅靠文档证据完成判断。第三方补全平均耗时 61.76 秒,动态增强为 357.43 秒;如果补全失败后仍进入沙箱,平均耗时会升至 406.58 秒。
四个组件分别承担不同职责:安装脚本检测补入口,自一致性压误报,动态增强补召回,第三方语义补全降低昂贵沙箱的调用频率。
它能不能定位并解释具体恶意代码?
ProfMalPlus 最后会将判断映射回具体文件和代码。Localization Agent 根据结论引用的证据节点,定位恶意源码,并保留载荷构造、触发条件和敏感调用等必要上下文。
作者从 1,070 个真阳性中抽取 283 个包,由两名安全专家独立标注。专家间行级 F1 为 92.4%,Cohen's κ 为 0.79;与专家共识相比,ProfMalPlus 的代码定位 F1 为 88.9%,解释质量平均为 1.87/2,其中 86.9% 获得最高等级评价。
剩余偏差主要来自两类情况:一类是定位范围过大,把邻近的良性辅助代码也纳入结果;另一类是定位范围不足,遗漏了平台检查、触发条件、模块导入或清理代码。部分解释虽然识别了“数据外传”或“下载并执行”,却没有明确写出数据来源、目标地址或载荷位置。
88.9% 并不意味着定位已经完全等同于人工审计,但它证明系统输出可以直接支持安全人员复核,而不只是提供一个难以追溯的标签。
真实 NPM 战场:找到恶意包只是第一步,告警还要能被人处理
离线数据集之外,团队从 2026 年 4 月到 6 月持续监控 NPM,每五分钟抓取一次新发布的包,三个月共分析 107,287 个包。ProfMalPlus 报告了 715 个疑似恶意包,其中 597 个经人工确认后被 NPM 认定为恶意并移除,误报率为 16.5%。
GuardDog 提供了一个很直观的对照。它一共报告 4,687 个候选,最终确认 567 个恶意包,误报率达到 87.9%。确认数量与 ProfMalPlus 接近,但安全人员要处理的候选告警约为后者的 6.6 倍。这个对比说明,真实部署中的价值不只取决于“找到了多少”,还取决于为了找到这些恶意包,安全团队需要人工排查多少噪声。
作者报告的 9.3% 漏报率,是以所有检测器共同发现并确认的恶意包并集为参照,只适合工具间的相对比较,不能代表整个 NPM 生态的绝对漏报水平。
真实监控样本发布于模型训练截止日期之后,降低了模型直接见过完整包的可能性,但不能彻底排除历史代码模板和攻击模式带来的训练数据影响。
能力边界与工程启发
ProfMalPlus 当前主要覆盖安装和导入阶段。如果恶意逻辑只有在用户主动调用业务函数后才触发,或者依赖 Windows、特定环境变量、钱包文件和其他特殊条件,系统可能无法观察到。论文在构建数据集时也移除了普通运行期样本,因此 98.1% F1 不能代表全部 NPM 攻击面。
对于本地或远程二进制文件,系统能够识别相关调用,却无法理解文件内部语义,因此倾向于保守标记为高风险。静态分析同样不覆盖 JavaScript 原型污染,Joern、Jelly 和 NodeProf 等底层工具的误差,也可能传递到代码切片和 Agent 判断。
LLM 部分仍受上下文窗口、非确定性和人工标注主观性影响,论文也只评估了轻量级模型。ProfMalPlus 因此更适合作为恶意包审查链路中的自动分析层,而不能独立替代其他安全工具和专家判断。
它对开发者和 Agent 工程的启发
下面几项是基于作者结果作出的工程延伸,并不是作者已经在所有场景中验证的普遍结论。
第一,在这项任务中,程序分析先提取安全相关切片,再由 LLM 围绕切片判断,取得了比 SocketAI 整文件分析更高的 F1 和更低的 API 费用。这提示我们:当有效证据只占源码的一小部分时,输入组织方式可能比单纯扩大上下文更重要。不过,两套系统的模型后端和完整流程不同,论文没有进行同模型、同 Prompt 的单变量对照,因此不能把全部差异归因于代码切片。
第二,“待定”应当成为 Agent 系统的一种正式状态。模型不必在证据不足时强行给出答案。只有明确记录不确定性来自哪个节点,后续系统才知道应该查文档、观察运行时参数,还是解析动态调用。
第三,工具路由应由证据缺口决定。第三方文档和动态沙箱解决的是不同问题,运行成本也明显不同。ProfMalPlus 不会固定运行所有工具,只在上一阶段无法判断时才升级证据。
第四,可解释性需要从底层证据结构开始设计。ProfMalPlus 从行为图阶段就保留节点 ID、代码位置、调用关系和行为链,因此最终解释能够回到实际源码,而非在检测结束后临时要求 LLM 为一个标签补写理由。
结语
ProfMalPlus 重新安排了程序分析、LLM Agent 和动态工具的职责:程序分析负责还原行为和整理证据,Agent 负责判断局部意图、拼接全局行为链,第三方资料和沙箱则在证据不足时介入,最终结果再回到具体代码供安全人员复核。
这套流程的代价也很明确:平均每包约需 394 秒,完整部署成本不只包含论文统计的 API 费用。
但这篇论文完成了一个值得关注的变化:它把恶意包检测从“提取特征后让分类器打标签”,重构成了“程序分析整理证据、多 Agent 分层判断、证据不足继续调查、最终回到源码定位”的审计流程。真正的主角不是 Agent 的数量,而是证据如何被发现、组织、补全和复核,并最终变成安全人员可以直接使用的结果。
浙公网安备 33010602011771号