{"id":19451674,"url":"https://github.com/mrdotparasyte/GeetestReverseEngineering","last_synced_at":"2025-04-25T04:30:29.117Z","repository":{"id":161214679,"uuid":"466721788","full_name":"mrdotparasyte/GeetestReverseEngineering","owner":"mrdotparasyte","description":"Geetest Slide Verification (v7.8.6) Reverse Engineering","archived":false,"fork":false,"pushed_at":"2022-04-07T09:54:03.000Z","size":3472,"stargazers_count":29,"open_issues_count":2,"forks_count":14,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-03T15:52:29.415Z","etag":null,"topics":["geetest","geetest-crack","geetest-reverse","node","python","reverse-engineering"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrdotparasyte.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2022-03-06T11:50:28.000Z","updated_at":"2025-01-06T15:23:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"d632ad7e-3131-438b-810b-caf905fa36a3","html_url":"https://github.com/mrdotparasyte/GeetestReverseEngineering","commit_stats":null,"previous_names":["zmovane/geetestreverseengineering","yoosoftcc/geetestreverseengineering","mrdotparasyte/geetestreverseengineering"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrdotparasyte%2FGeetestReverseEngineering","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrdotparasyte%2FGeetestReverseEngineering/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrdotparasyte%2FGeetestReverseEngineering/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrdotparasyte%2FGeetestReverseEngineering/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrdotparasyte","download_url":"https://codeload.github.com/mrdotparasyte/GeetestReverseEngineering/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250754528,"owners_count":21481830,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["geetest","geetest-crack","geetest-reverse","node","python","reverse-engineering"],"created_at":"2024-11-10T16:42:37.206Z","updated_at":"2025-04-25T04:30:28.551Z","avatar_url":"https://github.com/mrdotparasyte.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# README\n\n# 极验滑块验证逆向分析\n\n不少极验滑块验证的破解都是通过 `selenium` 这类自动化工具模拟人的滑动来通过验证，而随着极验对行为识别的升级，这种破解方案越来越容易被识别为机器人，目前为止发现最可行的方案是通过训练滑动轨迹数据，再直接构造参数提交请求来通过验证，因此对极验滑块验证的逆向是关键一步。\n\n以下分析基于极验 7.8.6 版本，代码仅作为学习研究使用；\n\n## 逆向分析目标\n\n尝试登录使用了极验验证的网站，可以发现最终登录接口会带上跟极验验证相关的字段：`geetestChallenge`, `geetestSeccode`,`geetestValidate`，其中 `geetestSeccode` 只比 `geetestValidate` 多了 `|jordan` 的后缀。\n\n\u003cp align=\"start\" \u003e\n\u003cimg src=\"pic/login.png\" width=500\u003e\n \u003c/p\u003e\n\n而 `geetestChallenge` 则只需仿照 web 端构造请求获得\n\n\u003cp align=\"start\"\u003e\n\u003cimg src=\"pic/captcha.png\"  width=500\u003e\n\u003c/p\u003e\n\n因此如何取得 `geetestValidate` 才是逆向分析的重点。\n\n## 极验验证的请求链\n\n随便拖动一下滑块让验证失败，根据 log 发现涉及 geetest 验证的整个请求链如下：\n\n| 流程            | 关键请求参数                     | 关键响应参数               |\n| --------------- | -------------------------------- | -------------------------- |\n| getCaptcha      | 各网站后端实现不一，不在讨论范围 | gt，challenge              |\n| 第一次 get.php  | gt, challenge, w                 | status: 需确保 success     |\n| 第一次 ajax.php | gt, challenge, w                 | status: 需确保 success     |\n| 第二次 get.php  | gt, challenge                    | c, s, fullbg, bg, gct_path |\n| 第二次 ajax.php | gt, challenge, w                 | validate                   |\n\n\u003cp align=\"start\"\u003e\n\u003cimg src=\"pic/flow.png\" width=500\u003e\n\u003c/p\u003e\n\n再尝试一次成功的验证，注意 developer tools 上勾选 preserve log。\n\n\u003cp align=\"start\"\u003e\n\u003cimg src=\"pic/ajaxphp_success.png\" width=500\u003e\n\u003c/p\u003e\n\n第二次请求 `ajax.php` 返回的 `validate` 既是前面提到的 `geetestValidate`, 请求链代码大致如下：\n\n```javascript\nrequestCaptcha()\n  .then(({ gt, challenge }) =\u003e {\n    return requestGetPHP(STEP.ONE, { gt, challenge });\n  })\n  .then(({ gt, challenge }) =\u003e {\n    return requestAjaxPHP(STEP.ONE, { gt, challenge });\n  })\n  .then(({ gt, challenge }) =\u003e {\n    return requestGetPHP(STEP.TWO, { gt, challenge });\n  })\n  .then(({ gt, challenge, c, s, bg, fullbg, gctpath }) =\u003e {\n    return (async function () {\n      const offset = await calculateOffset(bg, fullbg);\n      const track = getTrack(offset);\n      const imgload = parseInt(Math.random() * 20 + 50);\n      const passtime = track[track.length - 1][2];\n      const gctPayload = await execGctjs(gctpath);\n      return {\n        gt,\n        challenge,\n        offset,\n        track,\n        passtime,\n        imgload,\n        c,\n        s,\n        gctPayload,\n      };\n    })();\n  })\n  .then((data) =\u003e {\n    return requestAjaxPHP(STEP.TWO, data);\n  });\n```\n\n但整个请求链又多了三个 `w` 参数, 因此我们的目标需要转移到对三个 `w` 参数的构造。\n\n## 构造参数\n\n注意到下图中的三个 js 文件，根据调用顺序大致猜测，`fullpage.xxx.js` 可能涉及构造`第一次的 get.php 请求`，`第一次的 ajax.php 请求`，`slide.xxx.js` 可能涉及构造 `第二次的 get.php 请求`，`第二次的 ajax.php 请求`，最后的 `gct.xxx.js`也可能涉及构造 `第二次的 ajax.php 请求`。\n\n\u003cp align=\"start\"\u003e\n\u003cimg src=\"pic/js.png\" width=500\u003e\n\u003c/p\u003e\n\n基于上面的猜测，我们将三个 js 文件拷贝下来，并进行格式化\n\n\u003cp align=\"start\"\u003e\n\u003cimg src=\"pic/jscode.png\" width=500\u003e\n\u003c/p\u003e\n\n首先需要将大量的 unicode 码转化成 ascii 码，这一步只需在 vscode 上找个 plugin 来完成，这里使用 [native-ascii-converter](https://marketplace.visualstudio.com/items?itemName=cwan.native-ascii-converter) 做转换。\n\n尝试在代码中搜索 `challenge` 等关键字，发现一些疑似构造请求的关键代码:\n\n\u003cp align=\"start\"\u003e\n\u003cimg src=\"pic/fullpage_raw.png\" width=500\u003e\n\u003c/p\u003e\n\n如下经过混淆的 js 代码实在难以阅读，经过 AST 工具发现大量类似以 `$_CFAGh(143)` 形式混淆的变量名都最终调用同一个方法，基于此简单编写还原变量名的代码，将三个 js 文件一并还原。更好的做法是通过处理 AST 进行反混淆，但这里足够应付。\n\n```javascript\nasync function convert(varPattern, filepath, convertFn) {\n  const lines = [];\n  const fileStream = fs.createReadStream(filepath);\n  const rl = readline.createInterface({\n    input: fileStream,\n    crlfDelay: Infinity,\n  });\n  for await (const line of rl) {\n    const groups = line.matchAll(varPattern);\n    let newLine = line;\n    for (const group of groups) {\n      const newText = convertFn(parseInt(group[1]));\n      newLine = newLine.replace(group[0], '\"' + newText + '\"');\n    }\n    lines.push(newLine);\n  }\n  const content = lines.join(\"\\n\");\n  fs.writeFileSync(`${filepath.replace(\".js\", \".converted.js\")}`, content);\n}\n\nconst fullpagejsVariantsPattern = /\\$_[a-zA-Z]{3,5}_?\\((\\d+)\\)/g;\nconst slidejsVariantsPattern = /\\$_[a-zA-Z]{3,5}_?\\((\\d+)\\)/g;\nconst gctjsVariantsPatten = /[a-zA-Z]{3,5}_?\\((\\d+)\\)/g;\n\nconvert(fullpagejsVariantsPattern, \"geetest/fullpage.js\", FAwFx.$_AU.$_DEHDy);\nconvert(slidejsVariantsPattern, \"geetest/slide.js\", lTloj.$_AG.$_DBHFa);\nconvert(gctjsVariantsPatten, \"geetest/gct.js\", ArXuv.Bak.sfR);\n```\n\n还原后类似如下的干扰代码也可以一并删除\n\n```javascript\nvar $_CFAFI = FAwFx.$_CQ,\n  $_CFAEl = [\"$_CFAIF\"].concat($_CFAFI),\n  $_CFAGh = $_CFAEl[1];\n$_CFAEl.shift();\nvar $_CFAHk = $_CFAEl[0];\n```\n\n\u003cp align=\"start\"\u003e\n\u003cimg src=\"pic/fullpage_formatted.png\" width=500\u003e\n\u003c/p\u003e\n\n此时代码的可读性大大提高，类似 `encrypt1`,`stringify` 关键词已经可以猜测它们的用途并进行追踪。如 `encrypt1` 是加密类的方法，`he[\"stringify\"]()` 实际是 `JSON.stringify()`。最终整理出构造三个 `w` (`fullpageW`, `fullpageW2`, `slideW`) 的方法：\n\n- fullpageW\n\n  ```javascript\n  //fullpage.reversed.js\n  function w(gt, challenge, seed) {\n    var r = $_CCFP(seed);\n    var encrypted = new $_BDg().encrypt1(\n      JSON.stringify(o(gt, challenge)),\n      seed\n    );\n    var { res, end } = $_GJq(encrypted);\n    var i = res + end;\n    return i + r;\n  }\n  ```\n\n- fullpageW2\n\n  ```javascript\n  // fullpage.reversed2.js\n  var w = \"\";\n  var _ = $_BDg();\n  ($_CEGn = (function l() {\n    var t = [\"bbOy\"];\n    return function (e) {\n      t[\"push\"](e[\"toString\"]());\n      var r = \"\";\n      !(function o(e, t) {\n        function n(e) {\n          var t = 5381,\n            n = e[\"length\"],\n            r = 0;\n          while (n--) t = (t \u003c\u003c 5) + t + e[\"charCodeAt\"](r++);\n          return (t \u0026= ~(1 \u003c\u003c 31));\n        }\n        100 \u003c new Date()[\"getTime\"]() - t[\"getTime\"]() \u0026\u0026 (e = \"qwe\"),\n          (msg[\"captcha_token\"] = n(\n            o[\"toString\"]() + n(n[\"toString\"]()) + n(e[\"toString\"]())\n          )),\n          (r = JSON.stringify(msg));\n      })(t[\"shift\"](), new Date()),\n        (w = p[\"$_HBh\"](_[\"encrypt\"](r, seed)));\n    };\n  })()),\n    $_CEGn(\"\");\n  return w;\n  ```\n\n- slideW\n\n  ```javascript\n  /**\n   *\n   * @param {*} gt\n   * @param {*} challenge\n   * @param {*} seed\n   * @param {*} offset     滑动位移\n   * @param {*} track      滑动轨迹数据\n   * @param {*} passtime   滑动时间\n   * @param {*} imgload    图片加载时间\n   * @param {*} c          请求 get.php 获得\n   * @param {*} s          请求 get.php 获得\n   * @param {*} gtcPayload 执行 gtc.xxx.js 获得\n   * @returns\n   */\n  function w(\n    gt,\n    challenge,\n    seed,\n    offset,\n    track,\n    passtime,\n    imgload,\n    c,\n    s,\n    gctPayload\n  ) {\n    var o = {\n      lang: \"zh-cn\",\n      userresponse: getUserResponse(offset, challenge),\n      passtime: passtime,\n      imgload: imgload,\n      aa: $_BBCA($_GEy(track), c, s),\n      ep: getEP(),\n      rp: j(`${gt}${challenge.slice(0, 32)}${passtime}`),\n    };\n    var u = $_CCFP(seed);\n    var l = new $_BDg().encrypt(JSON.stringify({ ...o, ...gctPayload }), seed);\n    var h = $_GFM(l);\n    return h + u;\n  }\n  ```\n\n  其中构造出 `fullpageW` 和 `fullpageW2` 较为容易从已知的`gt`, `challenge` 即可构造出来，而 `slideW` 需要更多的信息进行构造，其中关键的包括：\n\n- `offset`: 滑块位移;\n\n- `track`: 滑块滑动轨迹数据，通过 `offset` 获得；\n\n- `gctPayload`: 通过执行 `gct.xxx.js` 获得，值得注意的是此文件需要动态更新，需要通过 `第二次 get.php 请求` 可获得 `gct.xxx.js` 文件路径;\n\n### 滑块位移和滑动轨迹数据\n\n通过 `第二次 get.php 请求`，获得 `bg`, `fullbg` 的图片路径, 图片如下：\n\n\u003cp align=\"start\"\u003e\n\u003cimg src=\"pic/bg.jpg\" width=200\u003e\n\u003c/p\u003e\n\n这里直接调用流传较广的还原图片和计算滑动位移的 python 代码，js 里并没找到类似 python PIL 的知名的库。\n\n```javascript\nconst util = require(\"util\");\nconst exec = util.promisify(require(\"child_process\").exec);\nasync function calculateOffset(bg, fullbg) {\n  return Promise.resolve().then(() =\u003e\n    (async function () {\n      const { stdout } = await exec(`python3 python/img.py ${bg} ${fullbg}`);\n      const offset = parseInt(stdout.toString());\n      return offset;\n    })()\n  );\n}\n```\n\n### gct payload\n\n`gct payload` 需要动态执行 `gct.xxx.js` 获取\n\n```javascript\nasync function execGctjs(gctpath) {\n  const response = await request(\"GET\", gctpath, {}, header);\n  const js = response.data;\n  const pattern = /return\\s(function\\(t\\)\\{[\\s\\S]*?\\});/g;\n  const gctFn = js.match(pattern)[0];\n  const payload = { lang: \"zh-cn\", ep: getEP() };\n  const newjs =\n    \"window={};\" +\n    js.replace(\n      pattern,\n      `window._gct=${gctFn.replace(/^return/, \"\")};\\n${gctFn}`\n    ) +\n    `function execGct(ep){window._gct(ep);return ep;}; execGct(${JSON.stringify(\n      payload\n    )})`;\n  return eval(newjs);\n}\n```\n\n## 总结\n\n至此，对极验 7.8.6 版本关键参数的分析完毕，具体参考源码，极验每一个版本都会对流程或者参数等有不同程度的修改，因此不能保证代码持续有效，这里仅仅提供逆向思路并做下记录。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrdotparasyte%2FGeetestReverseEngineering","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrdotparasyte%2FGeetestReverseEngineering","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrdotparasyte%2FGeetestReverseEngineering/lists"}