
此仓库包含对 Google 反机器人系统 (reCAPTCHA) 的技术分析,重点关注:
@g_recaptcha@lyxlobyxreCAPTCHA 是拥有最复杂混淆技术的反机器人系统之一,它采用一系列变换使代码难以阅读且更难进行逆向工程。大多数混淆可以通过抽象语法树 (AST) 轻松处理,但有些是在运行时处理的,这使得 AST 在这种情况下毫无用处。多态性也被应用于代码,以在脚本的每个版本中改变其结构。例如,代码不会直接执行操作,而是使用形状变化的对象或函数。
序列表达式
代码通过将每个块语句转换为连续的逗号分隔表达式而被扁平化,它可以出现在 if 语句、函数参数甚至对象内部

混合布尔算术
将算术运算(加、减、乘)与位运算(AND、OR、XOR、NOT)交织在一起以隐藏原始逻辑,例如 -2 * ~(h & H) + -2 + (h ^ H)
间接函数表
每个函数构建在一个表中,并使用其索引调用,例如 functions[index](https://github.com/elyelysiox/recaptcha/blob/main/args)
内联常量数组 一个局部数组字面量在表达式中内联赋值。该数组对在函数体内通过索引访问重复使用的常量(数字、字符串)进行分组
// b = [14, 1, "call"] assigned inline inside a sequence expression
function(Y, Q, c, l, G, X, W, J, b, P) {
(Y & 94) == Y && (b = [14, 1, "call"], ...)
W[b[2]](J, G) // W.call(J, G)
Y >> b[1] & b[0] // Y >> 1 & 14
}
函数多路复用 多个逻辑上不同的函数合并为一个,使用一个数值参数作为块选择器。活动块通过根据位条件评估参数来确定。调用者传递一个数值字面量作为选择器
function(N, y, U, Y, h, H, m, C, u) {
C = [26, 47, 6];
// block 1
if ((N - 2 ^ 14) < N && (N - C[2] | 28) >= N) {
// convert value to string logic
}
// block 2
if ((N + 4 & 40) >= N && (N + 5 & C[0]) < N) {
Y = bB();
throw Error(Y === void 0 ? "unexpected value " + U + y : Y);
}
return u;
}
逻辑运算符分支 用逻辑运算符短路评估替换 if 语句和 if/else 块,将控制流转换为表达式。结合序列表达式,多个分支显示为单个连续的逗号分隔表达式
// if (a) { block }
a && (block)
// if (!a) { block }
a || (block)
// if (a) { x } else { y }
a ? x : y
// combined with CFF and sequence expressions:
(Y | 1) & 14 || (c = Q.O, J = c.O.length + c.g.length),
(Y ^ 59) >> 3 == 3 && (Q.classList
? Q.classList.add(c)
: Z[31](31, Q, c) || (l = f[0](84, "string", "", Q), ...)),
绑定原生方法常量 将浏览器原生方法绑定到其原始接收者,将它们存储为常量以防止篡改
LO = (Tw = self) == null ? void 0 :
(K9 = Tw.Math) == null ? void 0 :
(v4 = K9.floor) == null ? void 0 :
(mF = v4.bind) == null ? void 0 :
mF.call(v4, Math) // Math.floor.bind(Math)
LO(x) // Math.floor(x)
U4() // Math.random()
Ge(obj, prop) // Object.defineProperty(obj, prop)
死代码 无法访问或未使用的代码块被注入到整个文件中,将其大小增加到超过 60,000 行,这使得静态分析和基于 LLM 的逆向工程变得困难
控制流扁平化 将代码的每个部分(声明和循环)转换为扁平的状态机。它通过将所有代码块路由到中央“调度器”块来隐藏原始执行逻辑
调度器可以改变形状;有些具有 2-3 个状态变量,循环/条件类型会发生变化。它们看起来像这样

这是一个带有 2 个状态变量的 CFF,一个处理 catch 块,另一个处理 try 块。
加密字符串池 所有字符串字面量(DOM API、浏览器属性、CSS 值、错误消息等)被加密到一个巨大的字符串池中。一个解密函数使用种子和基于 LCG 的 XOR 密码在运行时提取每个字符串
代码中有 1990 多个调用点,解密函数使用一个累积解码码点的运行密钥,使得每个字符依赖于所有前面的字符
X = function(J, b, P, F, U) {
U = ["codePointAt", 127, "char encrypted pool"];
for (F = (P = 0, b = "", l); P < Q; P++)
J = (U[2][U[0]](c + P) ^ F) & U[1], // XOR with running key
b += String.fromCodePoint(J),
F += J; // accumulate key
return G = b;
}
// call sites pass a seed to locate and decrypt each string
Z[23](64, 4, 54961, 103)() // → "lang"
Z[23](66, 4, 54961, 103)() // → "addEventListener"
Z[23](32, 12, 20287, 852)() // → "inline-block"

有状态值迭代器 reCAPTCHA 使用一个有状态函数,该函数按固定顺序返回一系列运行时对象和值(例如 window、document.body、数值常量),每次调用都会前进内部游标,乱序调用或调用次数过多会破坏所有后续读取。超时机制在固定间隔后使状态失效,对任何延迟读取返回 null
// sequential calls return different values:
c() // → window
c() // → document.body
c() // → 123
c() // → null (timeout expired)
l(c(), G[2], G[W[1]], G[1]) + l(c(), G[2], G[W[1]], 12)
// ↑ window ↑ window
10 * l(c(), G[2], G[W[1]], G[1]) + l(c(), G[2], G[W[1]], 12))
c().querySelectorAll(a[X[2]](98, X[1], X[1]))
// ↑ document.body
计算函数表
这与 间接函数表 类似,但这里要获取的函数索引是在运行时使用种子计算的,使用 XOR 和模运算
c = ((Q ^ no | U[1]) >> 5) + no
A = mN[(c % U[2] + U[2]) % U[2]] // mN is the function table (50+ functions)
q[29](5, 6977) // seed=6977 → index resolves to function at mN[X]
q[29](53, 6187) // seed=6187 → different index, different function

运行时值加密
某些值(验证码配置参数、锚点参数等)从不以明文形式存储;它们在收集后立即加密,仅在使用时解密,开头带有前缀 B

异步控制流混淆 同步逻辑被转换为基于生成器的状态机,这些状态机包装在递归 Promise 链中,通过调试器追踪任何值都会强制逐步执行多个异步处理程序,在每个 .then() 边界丢失原始执行上下文
初始化 reCAPTCHA 的组件``` ar: <WidgetInit?> k: co: hl: v: size: sa: anchor-ms: execute-ms: cb:
### 响应
响应包含:
- 验证码 iframe 窗口设计
- 用于载荷验证的锚点令牌 (/reload)
- 初始化的主要配置,在 `recaptcha.anchor.Main.init` 方法中执行,以便在 recaptcha_en.js 中接收
### 结构: