
CheetahSpec 这个名字可以拆成两部分Cheetah 强调速度定位Spec 指它要处理的是规格化的求解任务。它是一个跑在浏览器里的密码学求解器browser cryptosolver核心思路是用 WebGPU 的着色语言 WGSL 编写计算内核把哈希搜索、nonce 命中、proof-of-work 这类可并行拆分的任务直接交给 GPU 执行从而让浏览器里的计算获得接近本机原生程序的执行速度。这类题目最核心的价值不在于“能跑”而在于线程模型、缓冲区生命周期、调度参数和验证链路是否完整。很多人在浏览器里第一次写 WGSL 计算着色器时最容易踩的坑是内核写完了设备初始化也成功了但读回来的结果全是空值或者浏览器直接报 ValidationError。这些问题通常不是算法错了而是绑定组没对齐、缓冲区用法标志少了、派发数量不是工作组大小的整数倍。下面从计算模型讲起逐步搭建一个可运行、可验证、可排错的 CheetahSpec 最小版本。1. 先看懂 CheetahSpec 的计算模型为什么哈希搜索适合 GPU1.1 浏览器加密求解器的典型任务加密求解器的任务形态一般是一个明确的判定问题给定挑战数据和目标条件在候选空间里找到满足条件的输入。最常见的例子是 proof-of-work 挑战服务器给出一段挑战字符串客户端需要找到一个 nonce使hash(challenge, nonce)的前几位等于指定值。这类问题有一个非常适合 GPU 的特点每个 nonce 的哈希计算彼此独立。第 1 个 nonce 和第 1000 万个 nonce 之间没有任何依赖关系理论上可以同时算。CPU 上即使开了多个线程核心数也就是几个到几十个而 GPU 的计算单元是成百上千甚至上万个天然适合做这种“一个线程算一个 nonce”的并行搜索。CheetahSpec 选择浏览器而不是原生程序目的不是替代专业 GPU 工具而是把验证、调试、演示和运行放在同一套 Web 环境里。对学习 WebGPU 的前端开发者、写 CTF 教学题目的安全爱好者、做 proof-of-work 校验服务的人来说一个能直接在浏览器控制台看到命中结果和耗时的求解器比在本地装 CUDA 工具链要轻得多。1.2 WebGPU 与 WGSL 的角色划分WebGPU 是浏览器提供的现代 GPU 接口负责管理设备、缓冲区、管线、命令提交这些主机端工作。WGSL 是 WebGPU 的着色语言运行在 GPU 上负责真正的计算逻辑。写求解内核时要清楚两个执行语境JavaScript 端只负责准备数据、创建缓冲区、创建计算管线、派发任务、读取结果。WGSL 端运行在 GPU 上每个线程独立执行同一个内核函数通过内置变量拿到自己的全局编号。WGSL 计算着色器的执行模型可以这样理解一次派发会启动一个二维或三维的网格网格由若干工作组workgroup组成每个工作组包含固定数量的调用实例invocation。内核函数里用builtin(global_invocation_id)拿到当前调用的全局编号然后用编号计算自己负责的 nonce。缓冲区通过绑定组bind group暴露给着色器。固定大小的参数可以用存储缓冲区或 uniform 缓冲区传递不定长的挑战数据、结果数组用存储缓冲区传递。数据从 JavaScript 写入 GPU 缓冲区后内核读取计算结果再写回另一个缓冲区最后由 JavaScript 映射读回。1.3 使用边界必须提前划清写浏览器加密求解器技术本身是中性的但使用场景必须清楚只求解自己拥有的数据、自己的服务器下发的挑战或者经过授权的教学、测试、性能基准任务。不要把这类工具用于未授权的密码恢复、登录绕过或其它攻击场景。浏览器里的 WebGPU 着色器运行在沙箱中不能直接读取页面数据之外的内存但应用层的数据准备、网络请求和结果用途仍然由开发者自己控制。本文所有示例都以“自己生成挑战、自己验证结果”为前提。2. 环境准备先确认 WebGPU 可用再写 WGSL 内核2.1 浏览器、硬件加速与安全上下文WebGPU 不是所有浏览器、所有环境都默认可用。本地调试前先检查三件事。第一浏览器版本。Chromium 系浏览器较新版本默认启用 WebGPUFirefox 和 Safari 的支持情况随版本变化落地前要确认目标浏览器版本。判断标准很简单在控制台输入navigator.gpu能输出对象就说明接口存在。第二硬件加速。WebGPU 需要真实 GPU 或可用的软件渲染路径。如果系统关闭了硬件加速或者显卡驱动被浏览器拉入黑名单即使navigator.gpu存在requestAdapter()也可能返回 null。排查时打开chrome://gpu看 WebGPU 相关状态是否正常。第三安全上下文。WebGPU 要求安全上下文。http://localhost和http://127.0.0.1算是安全上下文可以直接调试。如果通过局域网 IP 访问开发机就必须走 HTTPS否则navigator.gpu可能不存在。环境检查点说明Chromium 系浏览器navigator.gpu是否存在较新版本默认启用 WebGPUFirefoxnavigator.gpu、about:config中的相关开关支持状态随版本变化先验证Safari版本和实验特性开关以官方支持状态为准远程桌面或虚拟机requestAdapter()是否返回 nullGPU 可能不可见优先在真机验证局域网访问是否使用 HTTPS非 localhost 必须安全上下文2.2 用一段代码探测 WebGPU初始化 WebGPU 的标准路径是检查navigator.gpu请求 adapter再请求 device。建议尽早挂上uncapturederror监听这样后面出现校验错误时能在控制台看到聚合信息而不是只得到一个模棱两可的失败现象。async function initWebGPU() { if (!navigator.gpu) { throw new Error(当前浏览器不支持 WebGPU或 GPU 被禁用); } const adapter await navigator.gpu.requestAdapter(); if (!adapter) { throw new Error(没有可用的 GPU Adapter请检查硬件加速和显卡驱动); } const device await adapter.requestDevice(); device.addEventListener(uncapturederror, (event) { console.error(WebGPU 未捕获错误:, event.error); }); console.log(WebGPU 设备初始化成功); console.log(最大工作组大小:, adapter.limits.maxComputeWorkgroupSizeX); console.log(最大派发数量:, adapter.limits.maxComputeWorkgroupsPerDimension); return device; }这段代码里有两层检查。navigator.gpu存在只代表浏览器实现了 WebGPU 接口不代表一定有可用设备requestAdapter()返回 null 才是真正说明当前环境拿不到 GPU。后面写内核时工作组大小和派发数量都不能超过adapter.limits给出的上限。2.3 本地启动一个静态服务WGSL 代码可以放在 JavaScript 字符串里但更规范的做法是单独维护一个.wgsl文件用 fetch 加载方便后续接入构建工具。本地调试时直接在项目目录起一个静态服务打开页面即可。python3 -m http.server 8080或者用 Node 生态npx serve .访问http://localhost:8080打开控制台确认initWebGPU()没有抛错。页面里不需要渲染任何 3D 内容所以这里不涉及 canvas也不需要调用getPreferredCanvasFormat()纯计算场景控制在navigator.gpu到device这一条链路上。注意如果页面是通过file://协议打开的fetch 加载.wgsl文件会受跨域限制。优先用本地静态服务不要直接双击 HTML。3. 编写一个最小求解内核在 WGSL 里搜索匹配 nonce3.1 先定义问题挑战串加 nonce命中前缀条件为了让示例形成完整验证链路这里选择最直观的问题给定 4 字节挑战串找到一个 nonce使哈希结果的第 16 到 31 位等于目标前缀。哈希函数这里先用 FNV-1a 占位。它不是密码学安全哈希但结构足够简单能完整演示 WGSL 里的数组读取、循环、位运算、按条件写结果这些核心写法和调度流程。把内核里的哈希换成 SHA-256 或 Keccak 的 WGSL 实现后整个调度和缓冲区逻辑不需要变。具体计算过程是把挑战字符串编码成字节按小端顺序打包成一个u32把 nonce 也按小端顺序打包成一个u32对这两段共 8 个字节依次做 FNV-1a 运算取结果的第 16 到 31 位与目标前缀比较。GPU 端的打包顺序必须和 CPU 端验证器完全一致否则会出现“GPU 找到了CPU 验证却失败”的问题。3.2 WGSL 内核完整代码下面是 CheetahSpec 最小版本的计算着色器。它申请了三个绑定资源挑战数据、参数、结果数组。struct Params { base_nonce: u32, target_prefix: u32, }; group(0) binding(0) varstorage, read challenge: arrayu32; group(0) binding(1) varstorage, read params: Params; group(0) binding(2) varstorage, read_write results: arrayu32; fn fnv1a_word(v: u32, seed: u32) - u32 { var h seed; for (var i 0u; i 4u; i i 1u) { h (h ^ ((v (i * 8u)) 0xffu)) * 16777619u; } return h; } compute workgroup_size(64) fn main(builtin(global_invocation_id) gid: vec3u32) { let nonce params.base_nonce gid.x; var h 2166136261u; h fnv1a_word(challenge[0], h); h fnv1a_word(nonce, h); let candidate (h 16u) 0xFFFFu; let target params.target_prefix 0xFFFFu; if (candidate target) { results[gid.x] nonce; } else { results[gid.x] 0xFFFFFFFFu; } }关键点有三个。第一workgroup_size(64)声明每个工作组包含 64 个调用。64 是比较通用的起步值多数 GPU 内核都适合后续再根据性能观测调整。第二global_invocation_id.x直接用来计算 nonce。一次派发如果启动 N 个线程那么 nonce 覆盖范围就是[base_nonce, base_nonce N)。第三结果数组使用哨兵值。命中时写 nonce未命中写0xFFFFFFFF。这个写法比只写命中项更简单