
reCAPTCHA의 문서화 및 리버스 엔지니어링
이 저장소는 Google의 안티봇(reCAPTCHA)에 대한 기술적 분석을 담고 있으며, 다음을 중점적으로 다룹니다:
@g_recaptcha@lyxlobyxreCAPTCHA는 가장 정교한 난독화 기법을 사용하는 안티봇 시스템 중 하나로, 코드를 덜 읽기 쉽게 만들고 리버스 엔지니어링을 더욱 어렵게 만드는 일련의 변환을 적용합니다. 대부분의 난독화는 AST(Abstract Syntax Tree)를 사용해 쉽게 조작할 수 있지만, 일부는 런타임에 처리되므로 이 경우 AST는 무용지물이 됩니다. 또한 코드에는 다형성(Polymorphism)이 적용되어 스크립트 버전마다 구조가 변경됩니다. 예를 들어, 코드는 작업을 직접 수행하지 않습니다. 대신 형태가 바뀌는 객체나 함수를 사용합니다
시퀀스 표현식(Sequence Expressions)
코드는 각 블록 문(block statement)을 연속적인 쉼표로 구분된 표현식으로 변환하여 평탄화됩니다. 이는 if 문, 함수 인수, 심지어 객체 내부에서도 나타날 수 있습니다.

혼합 부울 산술(Mixed Boolean Arithmetic)
산술 연산(덧셈, 뺄셈, 곱셈)과 비트 연산(AND, OR, XOR, NOT)을 서로 얽어 원래 로직을 숨깁니다. 예: -2 * ~(h & H) + -2 + (h ^ H)
간접 함수 테이블(Indirect Function Table)
각 함수는 테이블 내부에 구축되며 functions[index](https://github.com/elyelysiox/recaptcha/blob/main/args)처럼 인덱스를 사용하여 호출됩니다.
인라인 상수 배열(Inline Constant Array) 로컬 배열 리터럴이 표현식 중간에 인라인으로 할당됩니다. 이 배열은 함수 본문 전체에서 인덱스 접근을 통해 재사용되는 상수(숫자, 문자열)를 그룹화합니다.
// 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 Multiplexing) 논리적으로 구별되는 여러 함수가 하나로 병합되며, 숫자 매개변수를 블록 선택자로 사용합니다. 활성 블록은 매개변수를 비트 조건으로 평가하여 결정됩니다. 호출자는 선택자로 숫자 리터럴을 전달합니다.
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;
}
논리 연산자 분기(Logical Operator Branching) if 문과 if/else 블록을 논리 연산자 단락 평가(short-circuit evaluation)로 대체하여 제어 흐름을 표현식으로 변환합니다. 시퀀스 표현식과 결합하면 여러 분기가 단일한 연속 쉼표 구분 표현식으로 나타납니다.
// 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), ...)),
네이티브 메서드 상수 바인딩(Bind Native Methods Constants) 네이티브 브라우저 메서드를 원래 수신자에 바인딩하여 상수로 저장함으로써 변조를 방지합니다.
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)
데드 코드(Dead Code) 접근할 수 없거나 사용되지 않는 코드 블록이 파일 전체에 주입되어 파일 크기가 60,000줄 이상으로 늘어납니다. 이로 인해 정적 분석과 LLM 기반 리버스 엔지니어링이 어려워집니다.
제어 흐름 평탄화(Control Flow Flattening) 코드의 각 부분(선언 및 루프)을 평평한 상태 머신(flat state machine)으로 변환합니다. 모든 코드 블록을 중앙 "디스패처(dispatcher)" 블록을 통해 라우팅함으로써 원래 실행 로직을 숨깁니다.
디스패처는 형태가 바뀔 수 있습니다. 일부는 2~3개의 상태 변수를 가지며 루프/조건 유형도 변경됩니다. 다음과 같은 형태입니다.

이것은 2개의 상태 변수를 가진 CFF로, 하나는 catch 블록을 처리하고 다른 하나는 try 블록을 처리합니다.
암호화된 문자열 풀(Encrypted String Pool) 모든 문자열 리터럴(DOM API, 브라우저 속성, CSS 값, 오류 메시지 등)은 단일 대규모 문자열 풀로 암호화됩니다. 복호화 함수는 시드(seed)와 LCG 기반 XOR 암호를 사용하여 런타임에 각 문자열을 추출합니다.
코드 전체에 1990개 이상의 호출 지점이 분산되어 있으며, 복호화 함수는 디코딩된 코드포인트를 누적하는 실행 키(running key)를 사용하므로 각 문자가 이전의 모든 문자에 의존하게 됩니다.
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"

상태 저장 값 반복자(Stateful Value Iterator) 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
계산된 함수 테이블(Computed Function Table)
이것은 Indirect Function Table과 유사하지만, 여기서는 가져올 함수의 인덱스가 XOR과 Modulus를 사용하여 시드(seed)로 런타임에 계산됩니다.
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

런타임 값 암호화(Runtime Value Encryption)
일부 값 (캡차 구성 매개변수, 앵커 매개변수 등)은 평문으로 저장되지 않습니다. 수집 직후 암호화되어 사용 시점에만 복호화되며, 시작 부분에 B 접두사가 붙습니다.

비동기 제어 흐름 난독화(Async Control Flow Obfuscation) 동기 로직은 재귀적 Promise 체인으로 감싼 제너레이터 기반 상태 머신으로 변환됩니다. 디버거에서 값을 추적하면 여러 비동기 핸들러를 단계별로 거치게 되어 각 .then() 경계에서 원래 실행 컨텍스트가 손실됩니다.
reCAPTCHA를 초기화하는 구성 요소``` ar: <WidgetInit?> k: co: hl: v: size: sa: anchor-ms: execute-ms: cb:
### 응답
응답에는 다음이 포함됩니다:
- CAPTCHA iframe 창 디자인
- 페이로드 검증에 사용되는 앵커 토큰 (/reload)
- 초기화를 위한 주요 구성이며, `recaptcha.anchor.Main.init` 메서드에서 실행되어 recaptcha_en.js에서 이를 수신합니다.
### 구조: