前言:作者将自家浏览器扩展的通信协议混淆当成攻击目标逆向了一遍,发现x-gw自定义请求头暴露算法、明文时间戳当XOR密钥、JS混淆可被AST还原、报文可延展可重放等致命问题,混淆不等于安全。文章给出信封协议、AES-GCM认证加密、Service Worker与Wasm三步改造方案,让敌人看得懂也拿你没办法。
我把自己家的通信协议逆向了一遍,越扒越心虚
前两天深夜,我干了一件有点自虐的事。
把自己写的浏览器扩展,当成敌人的东西,从头到尾逆向了一遍。
起因说来好笑。这个扩展里有一套我挺得意的通信混淆协议,当初设计完的时候,我跟朋友吹过牛,说这玩意儿别人抓包也看不懂,流量特征也认不出来,稳得很。
即然吹了牛,总得自己验一验吧。
于是我打开F12,点开Network面板,盯着自己发出去的请求,看了三秒钟。
好家伙。
一排x-gw-开头的自定义请求头,整整齐齐码在那儿。x-gw-method,x-gw-path,x-gw-encrypted,x-gw-ts,一个不少。路由永远是那个/gw/,Content-Type永远是application/octet-stream。
我当时的心情,就像深夜回家,发现自己大门不但没锁,门上还贴了张纸条,写着「屋主几点睡觉,用的是什么锁」。

先给不写代码的朋友补个背景。
这套协议是干嘛的呢。简单说,就是我不想让网络中间的任何人看懂我的扩展在跟服务器聊什么,也不想让它一眼就被防火墙认出来干掉。于是我做了一层混淆,把请求内容用XOR搅乱,再拿一个时间戳当密钥,前端JS用jsjiami.com.v7混淆一遍,最后在页面里挂了个叫ah.proxy的全局代理,把所有请求统一收口封包。
当年做完,我是真有点得意,觉得自己搞了个大发明。
然后,就开始了这次自爆式审查。
第一刀,砍在脸上。
x-gw-encrypted这个头,值直接写着xor2。翻译过来就是,「此包裹采用XOR混淆,第二版」。
你想想看,这就好比在快递箱上贴了张说明书,告诉拆家的人用什么工具开箱。算法名字大大方方写在请求头里,我居然从来没觉得这有什么问题。
第二刀,砍在密钥上。
为啥一个时间戳就能当密钥?我当时盯着x-gw-ts这个头,心里咯噔了一下。
因为这串数字,是明文的。是每一个抓包的人都能直接看到的。拿它做XOR的key,等于钥匙就挂在门外的钉子上,还贴心地写了个「勿动」。
说到这个,我得替XOR说句公道话。XOR本身不是原罪,密码学里有个东西叫一次一密,1917年就提出来了,理论上是被证明绝对安全的,而它的核心运算,恰恰就是XOR。但一次一密有个苛刻到变态的前提,密钥必须真随机,而且用一次就扔。
我倒好,拿一个所有人可见的时间戳,当密钥反复用。
把理论上绝对安全的算法,硬生生玩成了零安全。

第三刀,砍在前端JS上。
jsjiami的混淆,变量名全是_0x4f2a这种天书,当时觉得挺唬人。结果用Babel那套AST还原工具跑了一下,几分钟,逻辑全出来了,变量名该叫啥叫啥。
更尴尬的是,我在还原出来的代码里搜字符串,直接搜到了一个硬编码的密钥后缀,「kainy」。就明晃晃写在JS里,像在藏宝图的背面写了「宝藏在正面」。
你敢信???
其实代码里预留了AES-GCM的模式,也留了salt的位置。但salt还是从请求头里来的,密钥后缀还是那个硬编码字符串。等于保险柜确实买了,钥匙还是挂在门框上。
第四刀,砍在那个全局代理上。
我在页面里改写了window.fetch,本意是拦住所有请求统一封包,自认为天衣无缝。但浏览器这个环境,你永远能找到干净的引用。开个iframe,从里面取一个没被Hook过的原生fetch,这层代理就是个摆设。
防君子,不防小人。
第五刀,也是最扎心的一刀。
我本来想着,行吧,就算以上全被破了,至少数据没人能改吧,传输过程总是完整的吧。
结果我翻了下XOR的特性,看到一个词,叫可延展性。
意思是,密文翻转一个比特,解出来的明文就在对应位置翻转一个比特。中间人根本不需要解密,不需要懂我的协议,直接在密文上动手,接收方毫无察觉。
因为没有MAC,没有任何完整性校验。

这就像寄一个没贴封条的快递,路上谁都能打开,把里面的东西换掉,再原样合上。收件人拆开的时候,只会觉得东西完好无损。
再加上时间戳的校验窗口是5分钟。5分钟之内,同一个请求,抓下来随便重放。
就像一张电影票根,5分钟内,同一个检票口,随便进。
我甚至能想象出那个画面。某个大学宿舍,凌晨一点,一个刚学会抓包的学生,鬼使神差地扒了我的扩展,发现密钥就是时间戳的那一刻,估计直接笑出了声。
而我还在自己的小圈子里,管这个叫「混淆协议」。
写到这,肯定有朋友要说了,混淆本来就是防小白不防专家的,你拿专业逆向的标准去要求它,是不是有点过分了?
我非常理解这种想法,而且它是对的。混淆确实有价值,它能提高门槛,过滤掉99%顺手牵羊的人。就像小区门口的道闸,挡不住真想翻进去的人,但能让大多数人知难而退。
但我的问题在于,我混淆了「混淆」和「安全」这两件事。
这里必须请出一位老先生。1883年,一个叫柯克霍夫斯的荷兰人,在《军事密码学》里写下了密码学史上最著名的一条原则。一个加密系统,应该做到,即使除了密钥以外的一切全部公开,它依然是安全的。
一百四十年前,人家就把这件事想明白了。
我在2026年得意的这点小聪明,人家1883年就写进论文了。愚钝如我。
顺着这个再往下想,还有个更根本的问题,我到底想防什么?
防审查和防篡改,是两个完全不同的目标。防审查,是让流量看起来像正常的业务请求,不被防火墙认出来。防篡改,是让数据在路上没人能偷看、没人能改动。
而我现在的实现,两头都不占。
防审查这边,一排x-gw-头,一个雷打不动的/gw/路由,清一色的octet-stream,安全设备写条正则就能一抓一个准。GET还全被强转成了POST,这本身就是一个响亮的特征。
防篡改那边,可延展,可重放,裸奔。
等于窗帘没挡住路人,门也没锁住小偷。

真要改的话,我觉得可以分三步走,成本从低到高,我自己也是按这个顺序在排期。
最便宜的一步,把那些显眼的自定义头全部收拢,连同路径、方法、时间戳,一起塞进加密后的body里。外面拟态成一个平平无奇的普通业务请求,该走哪个正常路由走哪个。这叫信封协议,信封外面啥也没有,信纸折在里面。
再往上一步,把XOR换成正经的认证加密,AES-GCM或者ChaCha20-Poly1305,浏览器WebCrypto原生支持,不用引任何库。GCM的密文自带16字节的认证tag,密文哪怕动一个比特,解密直接抛错。就像给快递贴上了防拆封条,动一下,撕胶的声音你自己都听得见。顺手把时间窗缩到60秒,服务端再用Redis存一个短期的nonce集合,同一个nonce只能用一次,重放这条路就堵死了。
最贵但也最彻底的一步,是架构层面动刀。把网络逻辑从页面挪到扩展后台的Service Worker里,页面和后台之间走浏览器内部通信,F12的网络面板根本看不到原始请求。核心的加解密和封包逻辑,用Rust写完编译成Wasm,AST还原工具对着二进制字节码,就没了脾气。
当然,这些都不是免费的。
nonce的管理,状态机一不小心就写出bug。Wasm更不用说,Rust的学习曲线明晃晃摆在那,我自己的Rust也就是能跑的水平,这块我还在摸索,理论上很美好,实操起来肯定有一堆坑在等我。
但方向是对的。先让窗帘变成门,再让门变成保险柜,一步一步来。
那天晚上逆向完,我在椅子上坐了会儿,愣了半天。。。
真的就是一声叹息。
不是因为被自己打脸有多疼,而是突然意识到,我以为我给数据穿了件隐身衣,其实只是拉了道窗帘,还在门上贴了张纸条,热情地告诉每一个路过的人,我家用的是什么锁。
屏幕前的你,如果也自己造过类似的轮子,真心建议你也找个深夜,把自己当成敌人,逆向一遍自己。
那种心虚的感觉,比任何安全培训都管用。
至于那张贴在门上的纸条,我已经撕了。
门,也准备换一把真正的好锁。一把不怕别人研究工作原理的锁。
因为安全从来不是让敌人看不懂。
而是让敌人看得懂,也拿你没办法。
/ 注意,本项目仅在 SyncMein 的500人小群分享。
/ 谢谢你看完了我的文章,我们下次再见吧。
/ 作者:明察
/ 投稿或爆料,请联系邮箱:mingcha@gqmg.com














暂无评论内容