CheetahSpec:用WebGPU和WGSL在浏览器中实现GPU并行密码求解 📅 发布时间:2026/8/29 1:54:17 👁 浏览次数: CheetahSpec 是一个跑在浏览器里的 WGSL 密码求解器核心目标是借助 WebGPU 把 GPU 的并行计算能力直接释放到前端用接近原生执行的速度完成密码恢复、哈希匹配这类计算密集任务。简单说不需要单独装 CUDA 工具链打开一个支持 WebGPU 的浏览器把候选口令列表和待匹配哈希交给页面GPU 就在后台按并行任务去算。适合三类人做 CTF 和密码学实验的开发者、需要评估自己账号密码强度的安全测试人员、以及想搞懂 WebGPU 和 WGSL 到底能做什么的前端工程师。在往下看之前先明确一个判断CheetahSpec 这类项目强调 native execution speed不代表它能完全替代原生工具。它更适合做小规模口令恢复、算法验证、教学演示和原型迭代。真要处理海量字典原生工具仍然更稳妥。但它的价值在于把“GPU 并行计算”这件事从桌面端搬到了浏览器里门槛低了不少。1. 先搞懂 CheetahSpec 到底在算什么别听到 cryptosolver 就想偏1.1 cryptosolver 解决的核心任务仍然是哈希匹配cryptosolver 听起来有点“攻击性”其实核心任务非常朴素给一个哈希值从一批候选口令里找到和它对应的输入。典型场景包括忘记了自己加密压缩包、加密文档的密码需要在本地做口令恢复。CTF 题目里给出哈希要求还原原始字符串。安全测试中评估内部系统是否存在弱口令但前提是系统是你自己的或者已经拿到授权。学习密码学算法时需要一个可见、可控、能断点的并行计算环境。这类任务的关键不是“想不想”也不是“有没有超能力”而是算力。单个候选口令做一次哈希很快但成千上万、上百万个候选口令叠在一起CPU 单线程循环就会很吃力。GPU 适合这类“大量独立小任务”的并行计算CheetahSpec 用 WGSL 写计算内核本质上是把 GPU 当成一个高吞吐的并行匹配器。注意只应该在自己的设备、自己的加密文件、或明确获得授权的测试环境中使用。不要拿去对别人的系统或接口做在线枚举那是另一回事也有法律风险。1.2 和 JS、WASM、CUDA 相比WGSL 方案的真实差异很多人容易把浏览器端高性能计算等同于 WASM。实际上 WASM 跑的是 CPU 指令能提速但并行度有限。JavaScript 更不用说单线程循环在数据量大时几乎是灾难。这里可以做一个直观对比方案计算单元并行能力启动门槛典型瓶颈JavaScriptCPU 单线程低最低循环速度慢Web WorkerCPU 多线程中低线程间通信、内存复制WASMCPU中中仍是 CPU 指令执行WGSL / WebGPUGPU高中数据搬运、调度开销原生 CUDAGPU很高高环境配置复杂依赖驱动CheetahSpec 想要的是最后两行里“接近原生”的体验。WGSL 写的计算着色器由 GPU 直接执行不是 CPU 模拟出来的。它和原生 CUDA 的差距主要在于WebGPU 有一套安全沙箱和校验机制调度时需要额外检查。JavaScript 侧数据和 GPU 侧数据之间需要显式搬运搬运成本不可忽略。浏览器不能直接访问本地文件系统和驱动细节少了底层控制能力。所以“native execution speed”更适合理解为“GPU 原生速度”而不是“和 C 程序完全一样的执行路径”。方向正确但要清楚边界。2. 跑起来之前先确认浏览器、GPU 和权限这三件事2.1 WebGPU 可用性检查比写代码更优先我见过不少同学拿到一个新 GPU 计算项目第一件事就开始写 WGSL 内核写完才发现浏览器不支持 WebGPU或者硬件加速被关了卡在环境上。CheetahSpec 这类项目跑在浏览器里第一步应该是检查navigator.gpu是否存在。可以直接打开浏览器控制台跑这段代码const hasWebGPU typeof navigator ! undefined !!navigator.gpu; console.log(WebGPU support:, hasWebGPU); if (hasWebGPU) { const adapter await navigator.gpu.requestAdapter(); console.log(Adapter:, adapter ? adapter.info : null); }如果navigator.gpu是undefined说明当前浏览器版本不支持 WebGPU或者处于禁用状态。常见原因有三个浏览器版本太旧还没有开放 WebGPU 接口。浏览器设置里关闭了硬件加速。当前是在远程桌面、虚拟机或显卡驱动异常环境下运行GPU 能力不可用。开头提到的“this browser supports webgl 2, but it is disabled or unavailable”这类提示很容易让人产生误解。WebGL 2 可用并不代表 WebGPU 一定可用但两者有一个共同的前置条件浏览器能正常调用 GPU。所以先打开浏览器设置确认“硬件加速”是开启状态再回头检查 WebGPU。2.2 最小配置建议与资源预判CheetahSpec 能在哪些显卡上跑其实取决于任务规模。如果只是教学样例候选数量在一万到十万级别集显也可能跑。但如果你想处理百万级以上候选或者在 WGSL 里实现完整 SHA-256、bcrypt 这类计算情况就不同了。以下是我个人建议的资源参考不写死版本号只给判断方向资源学习实验中等批量任务浏览器新版 Chrome / Edge新版 Chrome / Edge显卡支持 WebGPU 即可有独立显存更稳显存不要求越小任务越小4GB 以上比较舒服内存8GB 够跑小样例16GB 更稳磁盘几十 MB 就可以看候选词列表大小不要一上来就开最大并发。GPU 显存和浏览器安全限制都在那里任务开太大浏览器标签页可能直接白屏甚至把整个 GPU 进程拖崩。低配置能跑不代表能批量跑能批量跑不代表能开全量。先小后大是最稳的思路。2.3 浏览器权限和沙箱会影响任务设计浏览器环境有个特点不能随便读本地文件。如果你想用 CheetahSpec 恢复本地加密文件一般需要用户主动上传、拖拽文件或者从后端接口拉取数据。这和原生命令行工具不一样不能直接传一个路径进去。这带来的影响是候选口令列表要提前准备好作为数组或二进制文件传给页面。目标哈希也要从前端输入框、URL 参数、上传文件或接口获取。如果数据量大需要注意浏览器内存峰值不能一次性把所有候选都放进Uint32Array后再交给 GPU。另外浏览器对后台标签页有节流机制。如果你把页面切到后台GPU 计算频率可能被降低表现为速度突然变慢。实测时尽量保持页面在前台或者把计算结果通过 Web Worker 配合处理但 WebGPU 的 compute 任务仍在原页面上下文中执行。3. 最小可行示例用 64 个工作线程并行匹配一个哈希3.1 先设定一个足够小的任务CheetahSpec 如果一上来就做复杂哈希算法容易把 WGSL 语法问题和算法逻辑混在一起排错很痛苦。更合理的做法是先跑一个“最小匹配示例”候选列表里已经提前存好每个口令对应的哈希值每个哈希用一个u32表示目标哈希也是一个u32WGSL 只负责并行查找目标值在哪个下标。这个过程虽然简单但把 WebGPU 计算管线完整走了一遍数据准备、buffer 创建、WGSL 编译、绑定组组装、dispatch、结果读取。后续要换成真实 SHA-256只需要把“哈希计算”放到 WGSL 内部而不是用预计算好的哈希表。3.2 写一个极简 WGSL 计算内核WGSL 语法和 GLSL 类似但有自己的类型系统和属性写法。下面这个内核做了三件事读取全局线程 ID。判断当前下标是否越界。如果当前候选哈希等于目标哈希就把下标写回结果 buffer。struct Params { targetHash: u32, count: u32, }; group(0) binding(0) varstorage, read params: Params; group(0) binding(1) varstorage, read candidates: arrayu32; group(0) binding(2) varstorage, read_write result: arrayu32; compute workgroup_size(64) fn main(builtin(global_invocation_id) gid: vec3u32) { let i gid.x; if (i params.count) { return; } if (candidates[i] params.targetHash) { result[0] i; } }这里把result声明成arrayu32只是为了后续扩展多个命中结果做准备。result[0]表示第一个命中位置。真实场景中可能同时存在多个命中后面再讲怎么处理。workgroup_size(64)表示每个工作组包含 64 个线程。这个数值不是拍脑袋定的而是 WebGPU 最常见的配置之一。实际项目里可以改成 128、256 或 32但需要结合硬件限制和任务特性综合判断。3.3 JavaScript 侧如何调度 GPUWGSL 内核写完后JavaScript 侧要用 WebGPU API 把数据送上 GPU。完整流程如下// 1. 获取适配器和设备 const adapter await navigator.gpu.requestAdapter(); const device await adapter.requestDevice(); // 2. 准备数据 const params new Uint32Array([targetHash, candidateCount]); const candidates new Uint32Array(hashes); // 3. 创建 buffer const paramsBuffer device.createBuffer({ size: params.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); const candidatesBuffer device.createBuffer({ size: candidates.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); const resultBuffer device.createBuffer({ size: 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); // 4. 写入数据 device.queue.writeBuffer(paramsBuffer, 0, params); device.queue.writeBuffer(candidatesBuffer, 0, candidates); // 5. 创建着色器模块与管线 const shaderModule device.createShaderModule({ code: wgslCode }); const computePipeline device.createComputePipeline({ layout: auto, compute: { module: shaderModule, entryPoint: main }, }); // 6. 创建绑定组 const bindGroup device.createBindGroup({ layout: computePipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: paramsBuffer } }, { binding: 1, resource: { buffer: candidatesBuffer } }, { binding: 2, resource: { buffer: resultBuffer } }, ], }); // 7. 编码并提交计算任务 const encoder device.createCommandEncoder(); const pass encoder.beginComputePass(); pass.setPipeline(computePipeline); pass.setBindGroup(0, bindGroup); pass.dispatchWorkgroups(Math.ceil(candidateCount / 64)); pass.end(); device.queue.submit([encoder.finish()]);读回结果时需要把 buffer 映射到 JS 内存const cpuBuffer device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); const commandEncoder device.createCommandEncoder(); commandEncoder.copyBufferToBuffer(resultBuffer, 0, cpuBuffer, 0, 4); device.queue.submit([commandEncoder.finish()]); await cpuBuffer.mapAsync(GPUMapMode.READ); const result new Uint32Array(cpuBuffer.getMappedRange())[0]; cpuBuffer.unmap(); console.log(match index:, result);这里用了一次copyBufferToBuffer因为部分平台不允许直接把带有 STORAGE 标志的 buffer 映射为 MAP_READ。最稳妥的做法是再创建一个专门用于读回的 CPU 可见 buffer。如果你发现mapAsync一直不返回优先检查是否创建了不能映射的 buffer以及是否忘记unmap。3.4 先跑通再优化这个最小示例跑通后你至少可以确认四件事当前浏览器确实支持 WebGPU。WGSL 编译和调度流程没有语法错误。数据从 JS 到 GPU 再回传的链路是通的。你能拿到指定下标的初步结果。接下来再考虑换成真实哈希算法。CheetahSpec 这类项目的核心挑战就在这一步在 WGSL 里实现哈希算法保持字节序和填充规则和常规库一致。不要贪快先做单条验证再做小批量对比最后再上大规模并行。4. 真正影响 native execution speed 的五个细节4.1 workgroup_size 不是越大越好很多人听到 GPU 并行第一反应是线程拉满。实际上工作组大小受硬件限制比如maxComputeInvocationsPerWorkgroup通常允许 256 或更高但线程越多调度压力、寄存器占用和资源竞争也会增加。我建议在实际项目里做几次小实验分别用 32、64、128、256 跑同一批数据。每组跑 3 到 5 次取中位数。记录两次结果第一次启动和后续稳定运行。第一次通常包含编译和 warm up 成本不应该当作性能结论。如果 128 和 256 的耗时几乎没有差别说明调度开销占比不高瓶颈可能在数据搬运或算法本身而不是线程数。4.2 storage buffer 的数据布局比代码风格更重要WGSL 的 storage buffer 读取效率和数据的排列方式密切相关。如果你的候选数据是一个结构体数组比如每个候选包含 4 个字段那么访问时可能因为对齐和跨步产生额外开销。在 WebGPU 里简单连续排列的arrayu32往往比交错结构体更高效。原因在于 GPU 访问显存时有对齐要求字段交错会导致一次读取跨度变大浪费带宽。对于密码求解这类内存密集型任务数据布局的影响非常明显。实际项目中可以这样做把候选口令的哈希值统一放到一个连续Uint32Array。把口令本身放到另一个数组中或者只在下标命中后再回传。避免在 WGSL 里反复解包复杂结构体。如果你需要同时记录每个候选的附加信息可以拆成多个平行数组而不是一个结构体数组。虽然看起来不优雅但在 GPU 计算中访问模式往往优先于代码美感。4.3 dispatchWorkgroups 的数量要控制在合理范围WebGPU 对一次dispatchWorkgroups的维度有上限虽然不同硬件和浏览器实现会有差异但你不能假设可以一次性分发几十亿个线程。安全做法是分批 dispatch。比如每批处理 100 万个候选开Math.ceil(1000000 / 64)个工作组完成后看结果再处理下一批。这样做还有一个好处即使某次 dispatch 因为资源问题报错也能把错误限制在一个批次内不会把整个页面拖崩。分批的代价是增加了 JavaScript 和 GPU 之间的交互次数但相比一次性超过资源上限导致的崩溃这笔开销完全可以接受。4.4 时间测量不能只看 performance.now在 WebGPU 里用performance.now()包住device.queue.submit只能测到“从调用到提交”的时间并不能精确测出 GPU 实际执行时长。因为submit是异步的CPU 侧代码会继续往后跑。比较简单的做法是const start performance.now(); device.queue.submit([encoder.finish()]); await device.queue.onSubmittedWorkDone(); const end performance.now(); console.log(estimated time:, end - start);onSubmittedWorkDone()可以等队列执行完但也不能拿到 GPU 内部精确时间戳。如果项目需要更细粒度的性能数据可以查一下当前设备是否支持 timestamp query 扩展在创建 device 时启用对应 feature。需要注意这个功能并非所有浏览器环境都默认开启需要先检测。不管用哪种方式测量我建议固定同一套测量方法对比结果才有效。4.5 数据搬运是最容易忽略的隐形瓶颈最小示例里每次都要把候选哈希写到 GPU buffer再把结果读回。数据量小看不出来一旦候选列表达到几十 MB传输时间会非常明显。CheetahSpec 这类密码求解器如果要追求接近原生速度就要尽量减少 CPU 和 GPU 之间的来回搬运。具体做法有两种把多次计算合并成一次 dispatch让 GPU 内部循环处理而不是每轮由 JS 重新传数据。每次只回传命中结果不要回传整个候选数组。结果回传要克制。如果只想知道是否存在匹配结果 buffer 只需要一个标志位。如果需要定位多个匹配可以给每个工作组分配一个独立槽位或者用原子操作避免并发写冲突。5. 从单次求解到批量任务队列、回传和失败重试5.1 把任务拆成块而不是追求一次跑完浏览器里的 GPU 资源不是独占的。操作系统、其他标签页、浏览器进程本身都在共享 GPU。一次 dispatch 开太大可能触发设备丢失、校验失败甚至浏览器 GPU 进程重启。拆成块至少能保证单个任务失败后只影响当前块。可以分批记录进度不用重新跑全部。显存占用更可控不会因为 buffer 太大导致分配失败。批次大小取决于候选总量和显存。我给不出一个能通吃的数字但习惯是“先看总量再按 1/10 到 1/100 拆分”。如果单批 10 万候选能稳定跑就继续跑 50 万如果报错或速度明显下降就退回更小批次。5.2 回传设计不要让每个线程都写同一个变量简单示例里所有线程都把命中下标写到result[0]这在单命中场景下没问题。一旦有多个线程同时命中就会发生竞争。为了避免乱写可以给每个工作线程组分配一个专属槽位group(0) binding(2) varstorage, read_write result: arrayu32; compute workgroup_size(64) fn main(builtin(global_invocation_id) gid: vec3u32, builtin(local_invocation_id) lid: vec3u32) { let i gid.x; if (i params.count) { return; } if (candidates[i] params.targetHash) { result[gid.x] 1; } }这仍然不完美因为结果数组大小需要和候选数一样大内存开销很高。更合理的做法是每个工作组内部先把命中记录到 workgroupVariable再选出一个代表线程写回单独的结果槽。这样回传的数据量很小后续在 JS 侧合并即可。实际业务中我一般会先让 GPU 只负责“找出可能存在命中的块”再在 CPU 侧做二次确认。这样既利用 GPU 的并行过滤能力又避免了复杂并发控制。5.3 二次校验很重要GPU 计算并不是可以完全无条件信任的。数据绑错、遍历范围少一个、字节序不一致都会导致“找不到”或“找到错误内容”。所以真正落地时不能只看 GPU 回传的下标还要在 CPU 侧把该下标对应的候选重新计算一次哈希和目标值做最终比对。这一步看起来多余但能挡住很多隐蔽问题。尤其是刚把哈希算法搬到 WGSL 时最容易出现某个中间状态和参考实现不一致导致 GPU 全力跑了一个小时结果全错。5.4 失败重试要设计在任务队列里批量任务不能写成简单 for 循环。你需要一个任务队列每个任务包含输入批次、输出槽位、当前重试次数。某批次失败后先记录错误信息再决定是降低批次大小重试还是跳过。最终汇总所有批次结果而不是依赖某一次 dispatch 的状态。浏览器环境里没有原生后台守护进程所有状态都要自己维护。可以把任务状态存在页面内存中也可以使用 IndexedDB 做持久化。对于大多数实验场景页面内队列已经够用。6. 实测中容易踩的坑和我的排查顺序6.1 先看现象再定位原因同样一个报错背后的原因可能完全不同。我会按下面的顺序排查现象优先排查点navigator.gpu为 undefined浏览器版本、硬件加速、WebGPU 开关WGSL 编译失败getCompilationInfo()查看具体行列dispatch 后没有结果是否end()、是否mapAsync、buffer 是否映射成功执行速度很慢数据搬运、workgroup 大小、后台标签页节流GPU 进程崩溃批次太大、显存不足、同时开太多标签页不要太早怀疑“模型有问题”或“算法没写对”。在 CheetahSpec 这类项目里前置环境问题比例非常高。6.2 不要把 console 里的错误当成全部信息WebGPU 的错误不会全部自动打印到控制台。常见的校验错误需要通过 error scope 主动捕获device.pushErrorScope(validation); device.queue.submit([encoder.finish()]); await device.popErrorScope();如果返回的 error 是 null不代表没有运行时问题可能是错误发生在更早的创建阶段。建议在创建 buffer、pipeline、bind group 时也逐个包 error scope。也可以查看adapter.info获取设备名称和驱动信息方便判断是不是显卡太老或驱动有问题。6.3 功能边界不要硬碰CheetahSpec 听起来很酷但有几个边界要清楚浏览器里的 WebGPU 不能直接读写本地文件所有数据都要通过页面交互或网络接口。浏览器对 GPU 任务有超时、节流、资源限制不能像原生进程一样长期占满 GPU。在浏览器里实现复杂哈希算法代码调试能力远不如本地 C/C。简单哈希可以跑得很快但高计算复杂度算法在 GPU 上的优势不一定明显需要实测。如果你的目标是处理几十 GB 的字典、高 cost 哈希、或者生产级密码恢复原生 GPU 工具仍然更可靠。CheetahSpec 更适合学习、原型验证和中小规模任务。6.4 我的几条实战习惯最后留下几个我自己会长期遵守的习惯希望能少踩点坑先跑单条任务确认数据、WGSL 和回传链路都没问题。再跑一千条重点看 workgroup 大小和 dispatch 数量。能稳定跑完后再扩展到全量数据。每次只改一个参数不要同时调 workgroup、批次大小和 buffer 布局。记录每次实验的耗时、显存占用、结果数量形成自己的对照表。回传结果后在 CPU 侧做二次哈希验证不要直接信 GPU 下标。写到这里我觉得 CheetahSpec 这类项目最有价值的不是替代原生密码恢复工具而是把 WebGPU 的并行能力用 WGSL 技术完整验证了一遍。你把它当成一个能跑在浏览器里的密码求解器来学习会对数据布局、workgroup、buffer 回传、错误作用域有更直观的感受。如果后面真要落地第一步不是写更多 WGSL而是固定一套小规模测试集把耗时、资源占用、命中情况都记录清楚再慢慢扩大规模。你会发现很多问题并不是“GPU 不够快”而是数据没准备好、批次没切好、结果回传设计得太笨。