elyelysiox/recaptcha
GitHub: elyelysiox/recaptcha
一份系统性逆向分析 Google reCAPTCHA 反机器人系统的技术文档,涵盖代码混淆技术、指纹采集机制、虚拟机字节码及通信负载结构。
Stars: 210 | Forks: 36
## Description
This repository contains a technical analysis of Google's antibot (reCAPTCHA) focusing on:
- Payload Structure and Values
- Fingerprinting Techniques
- Obfuscation Techniques
- Anti-debugging/Tampering Techniques
- Virtual Machines
## Contact
- Discord: `@g_recaptcha`
- Telegram: `@lyxlobyx`
## Obfuscation Techniques
reCAPTCHA is one of the anti-bot systems with the most sophisticated obfuscation techniques, employing a series of transformations that make the code less readable and more difficult to reverse engineer. Most obfuscations can be easily manipulated using the Abstract Syntax Tree (AST), but some are processed at runtime, rendering the AST useless in this case. Polymorphism is also applied to the code to change its structure in each version of the script. For example, the code doesn't perform the action directly. Instead, it uses objects or functions that change shape
- ***Sequence Expressions***
The code is flattened by converting each block statement into a continuous comma-separated expression, it can appear in if statements, function arguments, and even within objects

- ***Mixed Boolean Arithmetic***
Intertwines arithmetic operations (addition, subtraction, multiplication) with bitwise operations (AND, OR, XOR, NOT) to hide the original logic, for example `-2 * ~(h & H) + -2 + (h ^ H)`
- ***Indirect Function Table***
Each function is built within a table, and is called using its index, like `functions[index](args)`
- ***Inline Constant Array***
A local array literal is assigned inline, mid-expression, the array groups constants (numbers, strings) that are reused throughout the function body via index access
// 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***
Multiple logically distinct functions are merged into one, using a numeric parameter as a block selector. The active block is determined by evaluating the parameter against bitwise conditions. Callers pass a numeric literal as the selector
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***
Replaces if statements and if/else blocks with logical operator short-circuit evaluation, converting control flow into expressions. combined with sequence expressions, multiple branches appear as a single continuous comma-separated expression
// 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***
Binds native browser methods to their original receivers, storing them as constants to prevent tampering
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***
Inaccessible or unused blocks of code are injected throughout the file, increasing its size to over 60,000 lines, this makes static analysis and LLM-based reverse engineering difficult
- ***Control Flow Flattening***
Transforms each part of the code (declarations and loops) into a flat state machine. It hides the original execution logic by routing all code blocks through a central "dispatcher" block
Dispatchers can change shape; some have 2-3 state variables, and the loop/condition type changes. They look like this

This is a CFF with 2 state variables, one handles the catch block and the other the try block.
- ***Encrypted String Pool***
All string literals (DOM APIs, browser properties, CSS values, error messages, etc) are encrypted into a single massive string pool. A decryption function uses a seed and an LCG-based XOR cipher to extract each string at runtime
There are 1990+ call sites spread across the code, the decryption function uses a running key that accumulates decoded codepoints, making each character dependent on all previous ones
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 uses stateful function that returns a sequence of runtime objects and values (like window, document.body, numeric constants) in a fixed order, each call advances an internal cursor, calling it out of sequence or too many times corrupts all subsequent reads. A timeout mechanism invalidates the state after a fixed interval, returning null for any late reads
// 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***
This is similar to `Indirect Function Table`, but here the index of the function to be obtained is calculated at runtime with a seed, using XOR and Modulus
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***
Some values (captcha configuration parameters, anchor parameters, etc) are never stored in plain text; they are encrypted immediately after collection and decrypted only at the time of use, have a prefix `B` at the beginning

- ***Async Control Flow Obfuscation***
Synchronous logic is converted into generator-based state machines wrapped in recursive Promise chains, tracing any value through the debugger forces stepping through multiple async handlers, losing the original execution context at each .then() boundary
## Anchor Payload/Response
### Payload Structure
Components to initialize reCAPTCHA
ar:
k:
co:
hl:
v:
size:
sa:
anchor-ms:
execute-ms:
cb:
### Response
The response contains:
- CAPTCHA iframe window design
- Anchor token used for payload validation (/reload)
- The main configuration for initialization, executed in the `recaptcha.anchor.Main.init` method to receive it in recaptcha_en.js
### Structure:
Note: Recaptcha BotGuard was removed `04/01/2026`, You can see some samples [here](https://github.com/elyelysiox/recaptcha-payload/tree/main/botguard_scripts)
[
"ainput",
[
"bgdata",
"",
"LyogQW50aS1zcGFtLiBXYW50IHRvIHNheSBoZWxsbz8gQ29ud...", // BotGuard Script Base64-Encoded
"YWVtZ0h5MGNCWXJDQ2lidGh0RW13RmhWdlc3aU5yeVpzSmx6U...", // BotGuard VM Bytecode Double Base64-Encoded
"wrB8fsOVU8K0YAzDsyQpw7ZHw6jDnMK1AMO6SRcvw53CsGlQw6/DuMO0wqr" // VM Config Bytecode Base64-Encoded
],
null,
[
"conf",
null,
"6LfTV4gkAAAAACDVrUvp9_DalxUPvFSU7M2HJDO-", // Website Key
0,
null,
null,
null,
1,
// Indexes for fingerprinting or something else
[16, 21, 125, 63, 73, 95, 87, 41, 43, 42, 83, 102, 105, 109, 121],
[-7614991, 137],
0,
null,
null,
null,
null,
0,
null,
0,
null,
700, // Time Start for Time Variances of Fingerprint Values
1,
null,
0,
// Encoded metadata for the VM's main bytecode constructs
"CvsCEg8I8ajhFRgAOgZUOU5CNWISDwjmjuIVGAA6BlFCb29IYxIPCMfm1DgYADoGZHhkTmlkEg8Is4qgOBgBOgZMV0o1a2ISDwiB7OgVGAA6Bkh1dlBqZhIPCK6e6zcYADoGR2JpT1FkEg8I94jmNxgAOgZvaWxlRGQSDwjwzeMVGAE6BmZJVkloYhIPCOLKoDcYAToGZ0xOQ0hjEg8I3r+3NxgBOgZlYXp1NmQSDwjY0oEyGAA6BkFxNzd2ZhIOCLjllDIYAToFUUJPRDASDwio7784GAA6BkdGVU5jZhIPCLWzqzgYAToGb3JZVjZkEg8I0tuVNxgAOgZmZmFXQWUSDwiV2JQyGAE6BlBxNjBuZBIPCKuRyjgYADoGQVo0TmJiEg8IxfOINxgAOgZhWG9pYWMSDwiNyoYyGAE6Bk93TjR0ZhIOCIr9iDcYADoFTWZJSTQaJwgDEiMdJv3NgTUZp4oJGYQKGZzijAIZzPMRGa+VQBnqsCYZ5a8MGQ==",
0,
1,
null,
null,
1,
null,
null,
0,
null,
null,
0,
0,
"cf5e3c3a6ab6c494c829a7932d9577c1e6de862dd4912e55dc7fe2e40a1f7604"
],
"https://who.clickbait.team:443", // Website URL:Port
null,
[
3,
1,
1
],
null,
null,
null,
1,
3600,
[
"https://www.google.com/intl/es/policies/privacy/",
"https://www.google.com/intl/es/policies/terms/"
],
"vQWNjTpMwdevQPWmE8akDzp8jsOZfa1VqTzVHaPiQ/c=",
1,
0,
null,
1,
1777669303203, // Fingerprint Data Encryption Key
0,
0,
[203], // Config Bytecode Key
null,
[185], // Config Bytecode Key
"RC--rg1OSRnOD0ayA",
null,
null,
null,
null,
null,
// Bft Token, Used in reCAPTCHA V2
"0dAFcWeA4RepD9zDjeMQE73pAT27pXZ7Nz_419U2K36QNqzHaKtLIDkwZQLi-Ud8OvSZHbDEcxQxusBQnsF5QQErMRpJMcUplaFg",
1777752103288 // "oc" Data Encryption Key
]
## Fingerprint
The values are based on the decrypted values from the first sample fingerprint, look [here](https://github.com/elyelysiox/recaptcha/blob/main/fingerprint/decrypted_values.json)
Each subfield of the fingerprint values has a base format:
`[value, key, elapsed]`
Where `key` is the encryption key, and `elapsed` is the time it took the collector to obtain and encrypt the value. This allows reCAPTCHA to detect:
- Excessively fast execution
- Hooks
- Breakpoints and sandboxing
### Collector Execution Order
The system uses an internal scheduler:
[42, 45, 53, 30, 28, 54, 29, 31, 32, 33, 34, 35, 37, 36, 38, 39, 43, 40, 41, 46, 48, 57, 58, 60, 61, 62, 63, 64, 66, 68, 69, 71, 72, 79, 55]
This represents:
- Execution Order
- Collector Pipeline
- Signal Generation Sequence
### Fingerprint Signals Codes & Key Derivation
Transforms fingerprint signals into deterministic encryption keys through a multi-stage derivation pipeline
Each fingerprint value is:
- Normalized
- Encoded into a compact signal code
- Converted into a numeric encryption key
- Encrypted using the derived key
The process is deterministic, meaning the same input value always generates the same signal code and encryption key
Raw Value
↓
Signal Code Derivation
↓
Compact Signal Code
↓
Numeric Key Derivation
↓
Encryption Key
↓
Value Encryption
### Example
***Input Signal***
"BUTTON,195a81c9"
***Step 1 - Derive Signal Code***
deriveSignalCode("BUTTON,195a81c9")
// → "wg"
The generated signal code is a compact deterministic identifier for the original value.
***Step 2 - Derive Numeric Key***
deriveKey("wg")
// → 3792
The signal code is transformed into a numeric encryption key.
***Step 3 — Encrypt Value***
encryptValueWithKey(3792, "wgia1z9pwq")
// → "bYVbh6BUsE_5pLA"
The next value will be encrypted with the derived key, following the collector order.
### Aggregate Fingerprint Structure
All generated signal codes are aggregated into a compact serialized structure
**Process**
value -> "BUTTON,195a81c9" seed code -> wg key -> 3792
value -> "wgia1z9pwq" seed code -> 21 key -> 1599
value -> 1 seed code -> p1 key -> 3521
value -> "8cc68d83" seed code -> ld key -> 3448
value -> "https://nextcaptcha.com/demo..." seed code -> 9p key -> 1879
value -> 4 seed code -> op key -> 3553
value -> false seed code -> 1r key -> 1633
value -> 7 seed code -> qf key -> 3605
value -> "jYAQSHAEAI" seed code -> 1z key -> 1641
value -> "" seed code -> 80 key -> 1784
value -> 2 seed code -> jk key -> 3393
value -> "AAAAAAAAAA" seed code -> 1z key -> 1641
value -> "h3" seed code -> 9p key -> 1879
value -> "0,BUTTON,195a81c9" seed code -> ia key -> 3352
value -> -1 seed code -> 1u key -> 1636
value -> 0 seed code -> wq key -> 3802
value -> "74,fbfbc5b3" seed code -> 6z key -> 1796
value -> 0 seed code -> wq key -> 3802
value -> 0 seed code -> wq key -> 3802
value -> 2 seed code -> jk key -> 3393
value -> 0 seed code -> wq key -> 3802
value -> "-1,-1" seed code -> 1m key -> 1628
value -> "static.cloudflareinsights.co,..." seed code -> 1g key -> 1622
value -> "" seed code -> 80 key -> 1784
value -> "https://nextcaptcha.com,https...." seed code -> pd key -> 3572
value -> "[4,\"CABIBQAggA\",\"BAgAAgAAgA\"]" seed code -> 1n key -> 1629
value -> "Demostración empresarial de reCAPTCHA v3..." seed code -> 1k key -> 1626
value -> "[2,76852,77850,77351]" seed code -> 9b key -> 1865
value -> "sha384-TNm" seed code -> 23 key -> 1601
value -> "[1680,1050,876,1166,844,876]" seed code -> 13 key -> 1570
value -> "[300,null,1778502450481]" seed code -> ex key -> 3251
value -> "[null,null,\"\",\"\"]" seed code -> rx key -> 3654
value -> "[2147483648,70806416,65303240]" seed code -> 2r key -> 1664
value -> "GA1.1.354395113.1778502448" seed code -> 1g key -> 1622
value -> false seed code -> 1r key -> 1633
value -> "[[[1,\"wg\"],..." seed code -> 85 key -> 1789
**Final**
[
[
[1, "wg"], [1, "21"], [1, "p1"],
[1, "ld"], [1, "9p"], [1, "op"],
[1, "1r"], [1, "qf"], [1, "1z"],
[1, "80"], [1, "jk"], [1, "1z"],
[1, "9p"], [1, "ia"], [1, "1u"],
[1, "wq"], [1, "6z"], [1, "wq"],
[1, "wq"], [1, "jk"], [1, "wq"],
[1, "1m"], [1, "1g"], [1, "80"],
[1, "pd"], [1, "1n"], [1, "1k"],
[1, "9b"], [1, "23"], [1, "13"],
[1, "ex"], [1, "rx"], [1, "2r"],
[1, "1g"], [1, "1r"]
],
"54"
]
### Signals
- **Idx 4** (`string`)
- Value: `3ccb`
- Hashed: `true`
- Description: Generates an HMAC key from `window.localStorage.getItem("rc::a
") + "6d"` converted to bytes, calculates `HMAC-SHA256(siteKey)` with that key, and returns the first 4 hexadecimal characters of the result.
The value of `rc::a` is a randomly generated base64.
- **Idx 5** (`integer`)
- Value: `window.localStorage.length * 2`
- Hashed: `false`
- Description:`window.localStorage` length multiplied by 2
- **Idx 16** (`string`)
- Value: `yM5Us/j/fn6EDgtmPlP4Pxj605nMJN9dRYHyy5Mn`
- Hashed: `true`
- Description: 40-char base64 Bloom filter fingerprint of all `` nodes. Walks each child element collecting tag names, attributes, and text content, serialized and run through a djb2 hash.
Each digest is fed into a `BitHash(240 bits, 7 rounds, max 25 nodes)` that sets bits across a 40×6 grid, then encodes each 6-bit segment as a base64 char. reCAPTCHA's own script tags are excluded via a dynamic regex on src.
- **Idx 18** (`string`)
- Value: `a2hyNTFuNDI4NnA3`
- Hashed: `false`
- Description: Randomly generated value encoded in Base64
- **Idx 27** (`string`)
- Value: `location.origin`
- Hashed: `false`
- Description: Website URL.
- **Idx 28** (`boolean`)
- Value: `false`
- Hashed: `false`
- Description: `window.parent != window ? true : window.frameElement != null ? true : false`
- **Idx 29** (`string`)
- Value: `e2a3cd70`
- Hashed: `true`
- Description: Hashed `grecaptcha.execute` function body with SHA-256, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/hashGrecaptchaThenBody.js)
- **Idx 30** (`integer`)
- Value: `0`
- Hashed: `false`
- Description: The index script containing the reCAPTCHA Script `https://www.gstatic.com/recaptcha/releases//`, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/getRecaptchaScriptIndex.js)
- **Idx 31** (`string`)
- Value: `AAgkAQI0SB`
- Hashed: `true`
- Description: Hashed `document.cookie` keys, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/hashCookies.js)
- **Idx 32** (`string`)
- Value: `""`
- Hashed: `false`
- Description: Referrer URL `document.referrer`.
- **Idx 33** (`integer`)
- Value: `2`
- Hashed: `false`
- Description: Element of the reCAPTCHA box where rendering is done.
// like grecaptcha.execute('.grecaptcha') -> this div
// Basically, it returns a number or the depth or level of that div in the document.
// Example: html > body > main > form > fieldset > div.recaptcha.form-field
// 5 4 3 2 1 0
// out = 5
nM = document.getElementsByClassName('grecaptcha-badge')[0] // or document.getElementsByClassName('g-recaptcha')[0] v2
let count = 0;
for (; nM = nM.parentElement || null;) {
count++;
}
const result = count;
- **Idx 34** (`string`)
- Value: `AAAAAAAAAA`
- Hashed: `true`
- Description: Hashed all `` attribute names, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/hashInputElements.js)
- **Idx 35** (`string`)
- Value: `0,DIV,f0c7414e`
- Hashed: `true`
- Description: `document.activeElement` Parse if it's a purchase element using a regex `/buy|pay|place|order|donate|purchase/i` with textContent, classNames and id, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/computeActiveElement.js)
- **Idx 36** (`string`)
- Value: `h3`, `h2` or `http/1.1`
- Hashed: `false`
- Description: `nextHopProtocol` of navigation performance `performance.getEntriesByType("navigation")[0].nextHopProtocol`
- **Idx 37** (`integer`)
- Value: `-1`
- Hashed: `false`
- Description: `performance.timing.unloadEventStart`
- **Idx 38** (`integer`)
- Value: `96`
- Hashed: `false`
- Description: DNS lookup `performance.timing.domainLookupStart - performance.timing.domainLookupEnd`
- **Idx 39** (`integer`)
- Value: `0`
- Hashed: `false`
- Description: Navigation type `performance.navigation.type`
- **Idx 40** (`integer`)
- Value: `0`
- Hashed: `false`
- Description: Last scroll-Y position `window.scrollY` or `document.defaultView.pageYOffset`
- **Idx 41** (`string`)
- Value: `DIV,a08cd360`
- Hashed: `true`
- Description: The `:hover` element where the mouse was positioned at that same moment, get the last element and hash tagName, classNames and id, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/computeHoveredElement.js)
- **Idx 42** (`string`)
- Value: `9,e3b0c442`
- Hashed: `true`
- Description: Hashed a random text script with SHA-256 plus the index, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/hashRandomScript.js)
- **Idx 44** (`string`)
- Value: `2v591z63wq`
- Hashed: `true`
- Description: `:hover` element, `document.activeElement`, all `` elements hashed, `location.url`, `window.scrollY`, Example:
- const element = document.querySelectorAll(":hover");
const current = element[element.length - 1];
const result = [
deriveSignalCode(hash(current)),
deriveSignalCode(hash(document.activeElement)),
deriveSignalCode(hash(document.querySelectorAll('input'))),
deriveSignalCode(location.url),
deriveSignalCode(String(window.scrollY))
].join('')
- **Idx 45** (`integer`)
- Value: `4`
- Hashed: `false`
- Description: Length of `window.history`.
- **Idx 46** (`string`)
- Value: `https://static.kogstatic.com/0000/34c...`
- Hashed: `false`
- Description: An error occurred in a script on the page and the `line:column` where it happened.
- **Idx 47** (`integer`)
- Value: `0`
- Hashed: `false`
- Description: Returns the length of the text currently selected on the page by the user `window.getSelection().toString().length || 0`
- **Idx 49** (`string`)
- Value: `h2-0`
- Hashed: `false`
- Description: Find the Performance Resource Timing entry for the reCAPTCHA script, get its `nextHopProtocol`, and determine whether its `duration` is zero. Return the result in the format `protocol-isZero`, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/recaptchaResourceTiming.js)
- **Idx 50** (`array`)
- Value: `[1,0,null,1,1,["805b1z63wq"],"MWc2MzZwMnR0ZHNkMw==",0,null,[[1]]]`
- Hashed: `true`
- Description: Contains user behaviors, hashed `:hover` elements and a timeout that increments each time another `:hover` element is collected. It analyzes user activations to accumulate the number of times the user has interacted with or is still present on the page by calculating `(10 * IsActive + IsBeenActive)`
*Base Format*:
[
count,
timeout,
null,
timeoutMultiplier,
transitionCount,
[ elementsHoverHashed ],
sessionId,
userActivationScore,
null,
[ [ flags ] ]
]
- **Idx 51** (`array`)
- Value: `[0]`
- Hashed: `false`
- Description: Search `document.body.innerText` for matches with the words "try again", "incorrect", "invalid", or "declined" and return the total number of matches found. Otherwise return 0, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/pageErrorKeywordCouint.js)
- **Idx 52** (`integer`)
- Value: `11`
- Hashed: `false`
- Description: `10 * navigator.userActivation.isActive + navigator.userActivation.hasBeenActive`
- **Idx 53** (`integer`)
- Value: `4`
- Hashed: `false`
- Description: It takes the current website URL, converts it to a character array, truncates it to 100 characters, and then converts it back to a string, [Sample](https://github.com/elyelysiox/recaptcha/blob/main/locationLengthParity.js)
Then it's 5 if the length of that truncated string is even, or 4 if it's odd.
- **Idx 54** (`boolean`)
- Value: `false`
- Hashed: `false`
- Description: It indicates whether the document is hidden from the user, that is, whether it is not visible on the screen, `window.document.hidden`
- **Idx 55** (`array`)
- Value: `[[[1,"2v"],[1,"9z"],[1,"z8"],[1,"us"],[1,..`
- Hashed: `false`
- Description: Ordering of generated signals codes by fingerprint values.
- **Idx 56** (`string`)
- Value: `-1,-1`
- Hashed: `false`
- Description: `window.opener` checks, returns "-1,-1" if null.
- **Idx 57** (`string`)
- Value: `www.gstatic.com,_,static.kogstatic.com,www.google.com,www.googletagmanager.,...`
- Hashed: `false`
- Description: Collects the hosts from the src of all elements `