无害病毒样本集合:多语言恶作剧程序与虚拟机隔离实践

发布时间:2026/11/6 10:24:29
无害病毒样本集合:多语言恶作剧程序与虚拟机隔离实践

简介:CuteVirusCollection 是一份以「可爱但致命」为主题的小型病毒演示样本合集,面向对恶意软件机理、系统资源耗尽与崩溃现象感兴趣的 Windows 学习者与安全初学者,适合在隔离虚拟机中观察各类脚本对系统的实际影响。内容按灾害等级分为三层:狼级包含无休止弹窗、鼠标指针随机移动与点击、窗口乱跳以及强制关机;老虎级涵盖超大字符串淹没内存、无限命令提示符窗口、浏览器窗口轰炸、fork 炸弹与进程无限创建;恶魔级以批处理的永久递归为主,用于理解资源被逐步吃尽后的连锁反应。压缩包约 47KB,体量轻巧,涉及 JavaScript、C、Java、Shell 与批处理等脚本和源码类型,分别对应网页层的弹窗与内存压力、系统层的进程与输入输出占用。整体以代码与分级说明为主,读者可按等级逐条阅读、复现与对照,观察单一资源被击穿后整机失稳的完整过程,并据此建立对资源型攻击的基本判断。目前已有 2078 人浏览学习。

1. 一个「可爱但致命」的小集合,适合拿来当安全入门的第一课

第一次把这套小集合拖进虚拟机时,我的预期是「又一个靠标题唬人的仓库」。双击第一个批处理文件,桌面在十秒内被一堆记事本窗口铺满,鼠标焦点被反复抢走,任务栏叠成一片;关掉窗口,又弹出一个仿系统扫描的进度条,写着「正在清理」。它没有改注册表,没有写启动项,也没有向外发一个字节的网络包,折腾完之后只要恢复一个快照,就又干净了。这套「可爱但致命」的病毒集合,本质是一批小型、无害、讨厌、有趣的恶作剧程序,覆盖 Windows XP / 7 / 10 三代桌面系统,用 javascript、c、java、shell、batch、CHTML 几种语言各写了一份「骚扰链路的实现样例」。它适合两类人:想搞清楚「恶意软件到底是怎么恶心人的」的安全入门者,以及想在自己机器上做一次可控演示、给同事做安全意识培训的开发。不适合想拿它去整人的——因为作者在每一档里都留了明确的停止条件,真正危险的写法恰恰是被删掉的那部分。

2. 技术栈与目录拆解:五种语言各管一段骚扰链路

看这套集合的第一个反直觉点:它没有统一技术栈。同一个「弹窗烦你」的效果,仓库里能找出批处理版、C 版、Java 版、JavaScript 版,外加一段类 Unix 的 shell 脚本。这不是作者偷懒凑数,而是刻意按「运行载体」分档——载体决定了这个程序需要多少权限、要不要编译、能不能被一键终止,也决定了它离「真正的恶意软件」有多远。把这层想清楚,整套目录结构就一目了然了。

2.1 多语言混编的选型逻辑

批处理是这套集合里门槛最低的一档。Windows 原生支持,双击就跑,不需要编译器和运行时,一个.bat文件就是全部。它的能力上限也很明确:能开进程、能弹窗、能改窗口标题、能反复循环,但做不了注入、做不了持久化。正因为「上限低」,它特别适合用来演示「一个毫无技术含量的脚本,是怎么把人的操作节奏打乱的」。

C 这一档用的是 Win32 API,编译出来是 exe,双击之后进程名不像批处理那样一眼看穿。仓库里 C 的作用是把「像真的」这件事做出来——MessageBox带图标带标题、定时器按节奏触发、窗口置顶抢焦点。这一步开始接近真实恶意软件给人的观感,所以也是唯一需要编译环境(gcc 或 MSVC)的一档,作者故意留了这道门槛,让你在编译之前至少读一遍源码。

Java 和 JavaScript 承担的是「跨平台可视化恶作剧」。Java 的 Swing 能做出假进度条、假扫描窗口、能被拖动但关不掉的对话框;JavaScript + HTML 那一档干脆跑在浏览器里,摇一摇页面、跑马灯标题、禁用右键,一个文件零安装。这两档的骚扰半径最小,因为浏览器沙箱和 JVM 把能做的事框死了,属于「随便玩,关机就好」。

shell 那一档是给类 Unix 环境准备的,几行脚本级别的循环和输出,用来对照说明「同样的骚扰逻辑换个系统怎么写」。CHTML 则是老式 Windows 帮助文档格式,属于那个年代的产物,在 XP/7 上还有用武之地,放进来更多是历史标本的价值。

按风险从低到高排一下,这个选型逻辑就非常清晰了:

目录/载体语言运行依赖骚扰半径隔离要求
浏览器档JavaScript / HTML任意现代浏览器单标签页无
脚本档batchWindows 原生单用户会话建议快照
类 Unix 档shellsh/bash单终端会话建议容器
桌面档C (Win32)需编译成 exe单用户会话必须虚拟机
桌面档JavaJRE单用户会话必须虚拟机

提示:这张表最大的用处不是分类,而是决定你在哪台机器上点。凡是要编译成可执行文件的,一律进虚拟机,不要心存侥幸。

2.2 目录结构与「一档一目录」的约定

仓库的组织方式很朴素,一档一个目录,目录名就是它的载体名,进去之后源码、编译脚本、说明各就各位。典型结构大致是这样:

cute-collection/ ├── bat/ # 批处理档:零依赖,双击即跑 │ ├── popup.bat # 有限次数的窗口弹出 │ ├── fake-scan.bat # 假进度条 │ └── STOP.flag.example ├── c/ # Win32 档:需要编译 │ ├── msgbox.c │ ├── build.bat │ └── build.sh ├── java/ # Swing 档:需要 JRE │ └── ShakeDemo.java ├── web/ # 浏览器档:最安全 │ ├── index.html │ └── shake.js ├── sh/ # 类 Unix 档 │ └── loop-demo.sh └── docs/ # 说明与风险分级

值得注意的是STOP.flag.example这个文件。它不是一个程序,而是这套集合贯穿始终的设计约定:每个会循环的脚本在执行前都会检查同目录下有没有STOP.flag,有就立刻退出。这份「后悔药」被放进示例文件里,意味着你要跑之前得先知道怎么停。

2.3 风险分级是怎么体现在代码里的

真正让这套集合停留在「无害」区间内的,不是功能本身,而是三条硬约束:有次数上限、有退出标志、有间隔延迟。如果看过市面上流传的整人脚本,会知道它们基本都不写这三样,一个while true配上零延迟,机器几秒钟就卡死。删掉上限就是危害,加上上限就是演示——这也是我建议读者在读源码时重点盯的地方:先找上限,再找退出条件,最后看它到底动了系统里的哪些东西。

3. 核心模块拆解:一个骚扰程序从触发到收尾的完整链路

把五种语言放一边,这套集合里每个程序的结构其实是同一个模板:初始化参数 → 检查退出标志 → 循环体(做动作 + 计数 + 延迟)→ 到达上限或读到标志 → 收尾清理。差别只在「动作」是什么、「延迟」怎么写。下面按语言挑三个有代表性的拆。

3.1 批处理档:窗口风暴的计数控制流

批处理没有真正的函数和异常,全靠goto和set /a拼控制流,所以这一档的代码读起来最像汇编。仓库里「窗口弹出」那个脚本大致是这么写的:

@echo off setlocal enabledelayedexpansion REM 上限、间隔都做成变量,二次开发时改这里就够了 set MAX=6 set DELAY=2 set COUNT=0 cd /d "%~dp0" if exist "STOP.flag" goto done :loop if exist "STOP.flag" goto done if !COUNT! geq %MAX% goto done start "" notepad.exe set /a COUNT+=1 REM 用 timeout 做延迟,XP 上要换成 ping -n timeout /t %DELAY% /nobreak >nul goto loop :done echo 已停止,共触发 !COUNT! 次 pause

逐段说一下。setlocal enabledelayedexpansion是必须的,因为循环体里对COUNT的自增和判断要在同一层解析,不开延迟展开,!COUNT!会一直读到旧值,循环直接变成无限次——这是批处理最经典的坑。

cd /d "%~dp0"把工作目录切到脚本所在目录。少了这一行,从桌面双击和从命令行执行的行为会不一样,STOP.flag的检查路径也会飘,排查起来非常难受。start "" notepad.exe中的空引号是标题占位,不能省,省了第一个参数会被当成窗口标题。

timeout /t %DELAY% /nobreak >nul是节奏控制,/nobreak防止用户按键跳过等待。>nul把倒计时提示吞掉,界面上看起来更「干净」,演示效果更好。XP 上没有timeout,常见做法是换成ping -n 3 127.0.0.1 >nul,用 ping 的次数当秒数。

最后:done那段打印计数并pause,是为了防止双击运行时窗口一闪而过什么都看不见——新手第一次跑基本都会踩这个坑,所以作者在每个批处理末尾都留了pause。

3.2 C 档:MessageBox 定时器的三条线

C 这一档的重点不在「弹出窗口」,而在「弹得像个正经程序」。Win32 里做这件事最直接的方式是MessageBoxA加一个Sleep节奏,代码不长,但三个参数的含义值得说清:

#include <windows.h> /* 全局计数与硬上限:任何演示程序都应该有天花板 */ static int g_count = 0; static const int kMax = 5; int main(void) { /* 后悔药:同目录存在 STOP.flag 就直接不启动 */ if (GetFileAttributesA("STOP.flag") != INVALID_FILE_ATTRIBUTES) { return 0; } while (g_count < kMax) { MessageBoxA( NULL, /* 父窗口,NULL 表示无父窗口 */ "这是一个无害的演示弹窗,点确定继续", "Cute Demo", MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND ); g_count++; Sleep(1500); /* 毫秒,两个弹窗之间的间隔 */ } return 0; }

MessageBoxA是阻塞式调用,弹窗不点掉,函数不返回,所以循环的节奏天然受用户操作控制,不会出现「一秒钟堆满屏幕」的情况。第三个参数是标题,图标和按钮用位或拼在第四个参数里,MB_SETFOREGROUND让窗口强制到最前,这就是「抢焦点」效果的来源。

Sleep(1500)是毫秒级延迟,写 1500 就是 1.5 秒。这个值调小到 100 以下,体验会从「有点烦」直接跳到「没法用」,建议二次开发时把它做成命令行参数或者配置文件读取,别硬编码。

编译这一步新手最容易翻车。用 MinGW 的话是:

gcc msgbox.c -o msgbox.exe -mwindows

-mwindows告诉链接器走 GUI 子系统,不然会额外弹一个黑框控制台。用 MSVC 开发者命令行则是cl msgbox.c user32.lib /SUBSYSTEM:WINDOWS。源码里的中文如果编译出来是乱码,把文件存成 UTF-8 带 BOM,或者干脆存成 ANSI,别用无 BOM 的 UTF-8。

3.3 Java 与 JavaScript 档:跨平台可视化骚扰的最小实现

Java 那一档的价值在于演示「关不掉的窗口」是怎么做出来的。核心是一个volatile停止标志加一个 Swing 定时器:

import javax.swing.*; import java.awt.*; import java.awt.event.*; public class ShakeDemo { /* volatile 保证停止信号能被定时器线程立刻看到 */ private static volatile boolean running = true; public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame f = new JFrame("摇一摇"); JLabel label = new JLabel("按 Esc 停止", SwingConstants.CENTER); f.add(label); f.setSize(320, 160); f.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // 关不掉 f.setLocationRelativeTo(null); /* 键盘监听作为唯一出口 */ f.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { running = false; f.dispose(); } } }); final int[] ticks = {0}; new Timer(80, e -> { if (!running || ticks[0]++ > 300) { // 双保险:标志 + 次数上限 ((Timer) e.getSource()).stop(); return; } Point p = f.getLocation(); f.setLocation(p.x + (ticks[0] % 2 == 0 ? 8 : -8), p.y); }).start(); f.setVisible(true); f.requestFocus(); }); } }

这里有两个关键点。DO_NOTHING_ON_CLOSE让右上角的叉失效,制造「关不掉」的观感;但程序必须给一个真实出口,否则就成了纯粹的耍流氓,所以加了 Esc 监听,同时定时器里还压了一道 300 次的硬上限,双保险。SwingUtilities.invokeLater是 Swing 的铁律,所有 UI 操作必须在事件分发线程里做,直接在主线程setVisible在某些 JDK 版本上会卡住。

JavaScript 那一档更简单,DOM 变换加setInterval,同样带次数上限:

const box = document.getElementById('box'); const MAX_TICKS = 40; // 硬上限,约 2.4 秒 let ticks = 0; const timer = setInterval(() => { if (ticks++ >= MAX_TICKS) { clearInterval(timer); // 达到上限必须清掉,否则页面一直抖 box.style.transform = 'translateX(0)'; return; } const offset = ticks % 2 === 0 ? 6 : -6; box.style.transform = `translateX(${offset}px)`; }, 60);

setInterval的返回值是定时器句柄,不清掉的话即使元素被移除,回调还在跑,内存和 CPU 都会慢慢涨上去。这是前端做动画最常见的疏漏,也是这套集合里少数「不删上限也能靠浏览器关掉」的一档——关标签页就行。

4. 跑起来:隔离环境、分批执行、干净收尾

这套集合的门槛不在代码,在环境。我第一遍是在物理机上跑的,结果「窗口风暴」那档把任务管理器都挤到后面去了,靠Ctrl+Shift+Esc加连点才恢复。从那以后我的顺序固定成:先快照,再断网,最后从最安全的一档开始往上试。

4.1 环境准备:快照、断网、限资源

虚拟机用任意一款都行,分配 2 核 2G 足够,系统镜像建议准备两份:一份 Windows 7,一份 Windows 10。XP 那一代的部分 CHTML 和批处理语法在 Win10 上会失效,想验证兼容性就得有老系统。装好之后第一件事是打快照,命名里带上日期,比如clean-2024-初版,这样后面无论跑成什么样都能一键回去。

断网这一步是必须的。虽然这套集合按源码看不发网络包,但任何从外部拿来的可执行文件都不该在联网环境下裸奔。虚拟机的网络适配器设置成「仅主机」或者干脆移除,用共享目录传文件。

资源限制也得设。VMware 和 VirtualBox 都能限制 CPU 核心数和内存上限,把虚拟机压到 1 核,真出问题的时候宿主机还有余量去点「强制关机」。

注意:尤其不要在生产机或公司电脑上跑。哪怕代码是无害的,杀毒软件的启发式规则也会因为文件名和行为特征直接把它隔离,事后还得走一遍白名单流程,非常麻烦。

4.2 分批执行:从浏览器档开始

执行顺序我建议按风险表从下往上走,每一步做完都恢复一次快照,别嫌麻烦。

第一步先跑web/里的 HTML。双击用浏览器打开,看抖动效果、看跑马灯标题,确认能正常停止。这一档跑完什么都不用清理,关标签页即可,用它来熟悉「这套集合的代码风格」。

第二步跑bat/。先把STOP.flag.example复制成同目录的STOP.flag,验证脚本会直接退出——这是在确认「后悔药」真的生效。然后把STOP.flag删掉,正式跑一次,观察弹出的窗口数量是不是等于MAX。

第三步才是c/和java/。C 档先编译,看编译能不能过;Java 档用javac ShakeDemo.java && java ShakeDemo跑起来,按 Esc 确认能停。

整个过程里准备一个记事本,记下每次跑的耗时、窗口数、有没有卡顿。这份记录在排查问题时非常有用,因为恶作剧程序的「症状」往往是模糊的——「很卡」到底是几秒一次卡,得靠数字说话。

4.3 终止与清理:不止任务管理器

常规终止手段是任务管理器结束进程树。批处理起的notepad.exe会有好几个同名进程,选中父进程右键「结束进程树」能一次性带走。如果任务管理器本身被窗口挡住,Ctrl+Shift+Esc之后再按Alt+空格调出窗口菜单,选「移动」,用方向键把窗口挪出来。

更硬的手段是taskkill:

# 按镜像名批量结束,/F 强制,/T 连子进程一起 taskkill /F /IM notepad.exe /T # 结束掉所有 cmd 窗口(慎用,会连你自己开的终端一起关) taskkill /F /IM cmd.exe

如果连命令行都调不出来,就直接在虚拟机菜单里选「关机」或者「重置」。因为提前打了快照,重置的成本是零。清理完之后我会再检查三件事:任务管理器里有没有残留进程、%TEMP%目录下有没有新文件、启动项(msconfig)有没有被改动。这套集合按源码看不写启动项,但养成这个检查习惯,以后跑任何来路不明的样本都用得上。

5. 避坑记录:五次翻车和它们的成因

5.1 现象:双击批处理,窗口一闪就没了

原因是没有pause或者脚本内部报错后直接退出,控制台窗口在命令执行完的瞬间关闭,什么都看不到。另一种情况是脚本里用了相对路径,从桌面双击时工作目录是C:\Users\xxx\Desktop,STOP.flag检查的路径完全不对。

解决办法是在脚本开头加cd /d "%~dp0"把工作目录钉死在脚本所在目录,结尾加pause。调试阶段还可以在关键分支上插echo打点,确认执行到了哪一步。批处理没有断点调试,echo就是唯一的黑匣子。

5.2 现象:物理机上试了「无限弹窗」,整台机器失去响应

原因是那一版代码里没有次数上限,while循环里延迟又设得极短,同进程反复抢焦点,任务管理器被压在窗口堆底下,键盘鼠标事件队列被打满。这不是「病毒厉害」,纯粹是自己没留退路。

解决办法只有一条:所有会循环的东西,第一行写MAX,第二行写DELAY。跑之前先确认STOP.flag的存在,并且知道在窗口堆里怎么用键盘把任务管理器挪出来。血泪经验是——不要在物理机上验证任何「循环」类样本,一分钟都别试。

5.3 现象:C 编译出来的 exe 双击后闪一下就退出,什么都没弹

两个成因。一是编译时没加-mwindows,程序走了控制台子系统,MessageBoxA的返回值没有被使用,main立刻返回,窗口还没来得及渲染。二是源码文件编码不对,中文字符串在编译期就被破坏了。

解决办法:MinGW 下固定加-mwindows;源码统一存成 UTF-8 with BOM 或者 ANSI;在main结尾加一句return 0;之前打印一下g_count,确认循环真的跑过了。如果还是不行,用GetLastError()把错误码打出来,比猜快得多。

5.4 现象:Java 的窗口在别的机器上字体乱码,或者按 Esc 停不下来

乱码是字体问题,Swing 默认字体在不同系统上映射不一样,中文可能落到一个不含中文字形的字体上。停止失效通常是两个原因:停止标志没加volatile,定时器线程读到的一直是缓存里的旧值;或者键盘监听加在了JFrame上但焦点在别的组件,事件根本没派发过去。

解决办法是用逻辑字体名(SansSerif)而不是具体字体名,停止标志声明成volatile,requestFocus()在setVisible(true)之后调用,同时把 Esc 监听挂到JRootPane上,覆盖面更广。

5.5 现象:杀毒软件把整个解压目录直接隔离,文件都找不到了

原因是这类样本的文件名、字符串常量、行为特征(反复弹窗 + 启动子进程)恰好命中了启发式规则。有些杀软还会连带把父目录标记掉。

解决办法是提前在虚拟机里把整个目录排除掉,或者干脆先断网再解压。真被隔离了,去隔离区恢复并加白名单,别在原目录反复重试——重试只会让规则命中得更坚决。这也是我一直坚持在虚拟机里跑的原因,隔离发生在虚拟机里,宿主机的环境一点不受影响。

6. 二次开发:给每个样本装一个「安全阀」

这套集合真正值得学的地方,不是那些恶作剧效果,而是它示范了一种可复用的工程约定:任何有循环、有副作用、有观感冲击的演示程序,都必须自带安全阀。如果你要基于它改造,我建议第一刀就切在参数化上。

把MAX、DELAY、STOP.flag的路径从硬编码改成命令行参数或配置文件读取。批处理用%1 %2,C 用argv,Java 用args。改完之后,同一份代码既能跑 3 次的演示模式,也能跑 30 次的压力模式,不用改源码。这一步做完,你的样本就从「玩具」变成了「可配置的演示工具」。

第二刀是加日志。每次触发动作就往同目录的run.log追加一行,写时间戳、动作类型、当前计数。别小看这个,恶作剧程序的症状本身就是模糊的,有日志之后「到底弹了多少次、间隔多久」全都有据可查。C 里用fopen+fprintf就够,Java 用Files.writeString加APPEND参数,批处理用echo ... >> run.log。

第三刀是统一回收。写一个cleanup.bat,把所有可能残留的进程名、临时文件、标志文件一次性清掉:

@echo off REM 统一回收:进程、临时文件、退出标志 taskkill /F /IM notepad.exe /T >nul 2>&1 taskkill /F /IM java.exe /T >nul 2>&1 del /q "%TEMP%\cute-demo-*" >nul 2>&1 del /q "%~dp0STOP.flag" >nul 2>&1 echo 清理完成 pause

>nul 2>&1把标准输出和错误输出一起吞掉,避免「进程不存在」这类噪音刷屏。这句在批处理里几乎是必备写法,值得记住。

验证方法我习惯用一张对照表:改完之后,把每一档分别跑三遍,第一遍手动按停止条件退出,第二遍等它自然到上限,第三遍中途强杀进程。三遍都跑完检查日志行数、进程残留、临时文件数量,全对才算改对了。

验证项期望结果检查方式
次数上限触发次数 == MAX数日志行
退出标志存在 STOP.flag 时零触发先建文件再跑
中途强杀无残留进程任务管理器
清理脚本临时文件归零清理前后对比目录

最后说个我自己的教训。早些年我觉得「加个上限」是多余代码,演示脚本嘛,跑完手动关掉就行。直到有一次在同事的机器上演示,那个循环恰好没写延迟,十几秒内桌面全被占满,人家正在写的文档还没保存。从那以后我每次写任何带循环的演示程序,第一行都是MAX =,第二行是延迟,第三行才写业务逻辑;跑之前必定先打快照。这个顺序我保持了几年,一次都没再翻过车。希望帮到你。

本文还有配套的精品资源,点击获取