Funcaptcha (ArkoseLab Captcha) 协议逆向笔记

2026-04-02

Funcaptcha是Arkose Labs开发的验证码系统,被用于Github注册,拥有极其猎奇的风控系统,还有非常恶心人的图片验证码,恶心了超过十万个甚至九万个合法用户。

funcaptcha.png

本短文记录了笔者对Funcaptcha的探索,以及开发Solver的过程。

初步探索

注册之后trace所有请求,我们可以看到第一个关于验证码的请求域名是octocaptcha.com

https://octocaptcha.com?origin_page=github_signup_redesign&responsive=true&require_ack=true&version=2&style_theme=light&datadome_js_tag=true&data=%7B%22email_address%22%3A%22cubk80@gmail.com%22%2C%22login%22%3A%22CaobiCaobiCaobi123%22%7D

这个请求除了一些基本的信息之外还传入了用户的邮箱和用户名,后来经过测试,其实这个是为了根据你的邮箱信誉度决定难度,如果是大牌邮箱比如Gmail难度会降低,杂牌邮箱或者容易滥用的邮箱难度会对应的提高。

请求之后返回了一个验证码块

html
<body style="margin: 0px">
  <div id="funcaptcha"
       data-target-origin="https://github.com"
       data-origin-page="github_signup_redesign"
       data-require-ack="true"
       data-version="2"
       data-data-exchange-payload="一些加密内容"
       data-style-theme="light"
       data-language="en"
  >
</div>

然后上方script块里有一个api.js引用,这个就是Funcaptcha的主体了。

1.png

打开这个URL就能看到api.js了,使用LLM辅助开发的反混淆器发布在了Gist。(当然也没反混淆的很好,只是能看的程度,比如Renamer肯定无法还原)

2.png

混淆的api.js

Settings请求

在加载api.js之后第一个请求为https://github-api.arkoselabs.com/v2/747B83EC-2CA3-43AD-A7DF-701F286FBABA/settings返回的是一些样式之类的设置,不太重要。

json
{
  "default": {
    "settings": {
      "observability": {
        "enabled": true,
        "samplePercentage": 100
      }
    },
    "theme": {
      "container": {
        "children": {
          ".game-core-frame": {
            "style": {
              "minWidth": "215px"
            }
          }
        }
      }
    }
  }
}

PublicKey请求

第二个请求比较关键

https://github-api.arkoselabs.com/fc/gt2/public_key/747B83EC-2CA3-43AD-A7DF-701F286FBABA

请求体是一个form-urlencoded我们

json
{
  "c": "加密base64一堆",
  "public_key": "747B83EC-2CA3-43AD-A7DF-701F286FBABA",
  "site": "https://octocaptcha.com",
  "userbrowser": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0",
  "capi_version": "4.0.16",
  "capi_mode": "inline",
  "style_theme": "light",
  "rnd": "0.3898489811818664",
  "language": "en",
  "data[origin_page]": "github_signup_redesign",
  // data-data-exchange-payload, octocaptcha.com返回的
  "data[blob]": "加密base64一堆"
}

public_key依旧是747B83EC-2CA3-43AD-A7DF-701F286FBABA已经在很多地方出现,这个其实就是sitekey,data[blob]是octocaptcha.com返回的data-data-exchange-payload原文,用来和服务器确认验证码难度什么的,这个rnd就是一个r("rnd", Math.random())没什么好看的。

重点是这个c

3.png

这个c是一个非常长的加密base64,在代码里是一个叫bda的东西,再往上追一点,可以看到这个这个bda其实是encryptedFPData,加密的浏览器指纹。

4.png

然后再回到这个请求,响应体也是一个巨大的json,也是一些settings

json
{
  "token": "66018a235cc293bb9.7598136404|r=ap-southeast-1|meta=3|meta_width=300|metabgclr=transparent|metaiconclr=%23555555|guitextcolor=%23000000|lang=en|pk=747B83EC-2CA3-43AD-A7DF-701F286FBABA|dc=1|at=40|ht=1|ag=101|cdn_url=https%3A%2F%2Fgithub-api.arkoselabs.com%2Fcdn%2Ffc|surl=https%3A%2F%2Fgithub-api.arkoselabs.com|smurl=https%3A%2F%2Fgithub-api.arkoselabs.com%2Fcdn%2Ffc%2Fassets%2Fstyle-manager",
  "challenge_url": "",
  "challenge_url_sri": null,
  "challenge_url_cdn": "https://github-api.arkoselabs.com/cdn/fc/assets/ec-game-core/bootstrap/1.33.1/standard/game_core_bootstrap.js",
  "challenge_url_cdn_sri": "sha384-jRnlOzL3aNB2X9mW9oPSbNK3pX+rvMpvkvFOq6lOD1Q1OpBOB99ryDjNiCFt7SO4",
  "noscript": "Disable",
  "inject_script_integrity": null,
  "inject_script_url": null,
  "mbio": true,
  "tbio": true,
  "kbio": true,
  "styles": [
    // 一些svg,css什么的设置
  ],
  "iframe_width": null,
  "iframe_height": null,
  "disable_default_styling": false,
  "string_table": {
    // UI字符串大全,我不知道为什么写到这里,其实js里还写了一遍
  },
  "compatibility_mode_enabled": true,
  "force_standard_mode": false,
  "pow": true
}

重点是这个token字段,之后所有请求几乎都是要附带这个token字段,所以这个请求应该是init,初始化一个验证会话。

然后还有一个pow字段,但是笔者测试了很多次,无论是什么IP,是否纯净,什么浏览器,什么操作系统这个pow都是true,理论上是可选的但是实际上就是必须实现的。

BDA

BDA是Funcaptcha的浏览器指纹系统,这个部分被虚拟化了,使用嵌入在api.js的栈式虚拟机执行。

示例:

"f76f3 a24e9"
"c942b F\ZUZC"
"a4549 '_QEXWREY\_' false"
"b51c4 1"
"c8051 L0"

第一段是opcode,第二段是XOR 103加密的字符串,比如这个_QEXWREY\_就是navigation,由于api.js已经反混淆,其实反虚拟化也很容易,只要给所有参数xor回去,然后opcode简单解读原意,然后借助AI从字节码生成代码就可以了。

反混淆之后的代码已放置在附录

Pow

在得到pow是true的结果之后会拉取一个游戏™从

  • https://github-api.arkoselabs.com/cdn/fc/assets/ec-game-core/bootstrap/1.33.1/standard/game_core_bootstrap.js
  • https://github-api.arkoselabs.com/cdn/fc/assets/ec-game-core/bootstrap/1.33.1/standard/pow.game_core_bootstrap.js

这几个是Pow Bootstrap但是实际上没什么重要的代码

pow的初始化请求为https://github-api.arkoselabs.com/pows/setup?session_token=66018a235cc293bb9.7598136404

这个session_token就是PublicKey接口返回的token的第一节,然后响应是这样的:

json
{
  "pow_token": "89818a235cc571029.8433425004",
  "url": "https://github-api.arkoselabs.com/cdn/fc/assets/pow/2.3.0/standard/index.html",
  "sequence": "https://github-api.arkoselabs.com/powseq/2vv3c40c-96b3-4f8f-bd86-49c2cec773fa/target/1141.js?Expires=1775043957&Signature=vr90yCxWQL6bCgJVJWnSpZ68WYd1jCcOcHze04XjyJ-XKSvb7bP5vgfzRVOmJJBssG6MDjRJBsqTiNuLeFkSQ1ZPgtdzsCPyVaZW1vqo4v-xRD6VCfe4j8jy8KtM9r3WAFUFv~x7UHZ2-DtIGPEue3ak9e5ptLBNCHMs1m2IOEPjT3uTIqUfVvgdEZ7vnjcJMXMCgfjCYYVfGDhgNKfAyRUsN32-F~d5xrDgJL6aIN5CnXZ1pSI4OD1smnohXmvn~L3MkJoW32vLhrW-398oTrFVtqOMIIsrY920AjqgwJaip6H~GTjThyiLQi7gpsaDEhRX6sQGK7RsqA-YUIFONA__&Key-Pair-Id=K2GG2TVX8JMCPF",
  "timeout": 75000,
  "work_type": 1,
  "transparent": false,
  "embedded_session_id_enabled": true,
  "work_config": {
    "seed": [
      {
        "Tj7yQMNW1s1T4aw7jS": "qpsc61vYR0vEJ31NTW",
        "x0ubujGiSL7hQzTowI": "EwN5JHWQUUxsluAm02"
      },
      {
        "1xneD0jMllV6xLlfsd": "PWLnG2AR5hxqm89uF5",
        "TWND0NlDc7Zng1wtKS": "3a0Y98cpXEmLAGhwKN",
        "ZHbwrxoyTnarMPvR42": "8IptmYINSPGJZ7cdUb"
      }
    ],
    "splits": [
      {
        "target_hash": "f85b5789a48ea31c20e4ab31dcfcc434d3931e3bccf59784ec3f4cc456db2ae3c1f5bc70dd883946da7449ddf8f5070c6f885ecb3c7c6b11f63875f6a47f7850"
      },
      {
        "target_hash": "19d44deeb336de2ac3cb83a18ffe296a31af3332ac6c88631d31e97b52d9c03b929e0c498cb221b1f3cee748c430d2d374f08d8d75cba4944edf181c5d9743ff"
      },
      {
        "target_hash": "66b23f6fb06444c00adb6126288d7a2176693fbcd80fc48ccd80f74992be41882c26d0d8f54e3eba30c1a35801e9ab8dcfb071c83d3d46aaa39acd86cec95f5c"
      },
      {
        "target_hash": "beeba8def17402213476f4d66f5879acde3d73cf6527d936420a328d2d9dda31acdf7277e7ea05bede238d2fb16885fdd03d081c0def64fc272676682d4830f1"
      },
      {
        "target_hash": "a30513c89daaa003d63e6aea42d51b078b6e4386475c9f3ddb9baeb959194dd6b4ceeee0b6efd69d495f245f5123b6ceb531c3429ba7909a0f4452c12fa7f33d"
      }
    ],
    "starting_nonce": 44631
  }
}

这个sequence是一个CloudFront URL,下载下来就是Pow Worker,这个JS是每次请求随机生成一个算法,经过测试每次请求transform都有略微的差异,实际算法是从starting_nonce递增,直到命中target_hash,然后你的邮箱信誉越低,这个挑战就会越难。

一个反混淆的样本放置在了附录

查看一下提交的请求https://github-api.arkoselabs.com/pows/check

json
{
  "pow_token": "89818a235cc571029.8433425004",
  "session_token": "66018a235cc293bb9.7598136404",
  "hash_rate": 134.36469864698648,
  "execution_time": 3252,
  "transform": [
    {
      "Tj7yQMNW1s1T4aw7jS": "?5\t\u0002\u000e\r\\{!?\\\t\u000e\u000f\r]\f\u0011",
      "x0ubujGiSL7hQzTowI": "\\\r\t{7_]]\u000e\u000e6\\\u0003[\u001d\u0013(\\",
      "Tj7yQMNW1s1T4aw7jSd": "4:",
      "x0ubujGiSL7hQzTowId": "8:",
      "Tj7yQMNW1s1T4aw7jS25": ":\u0003\u0006\f\u001d",
      "x0ubujGiSL7hQzTowI49": "\n3(!\u0019\u0003D",
      "Tj7yQMNW1s1T4aw7jSd25": ":\u0003\u0006\f\u001d",
      // ...
    },
    {
      "0": "=@=9;9",
      "1": "@=8:8?",
      "2": "@=>::9",
      "3": "<=:;<8",
      "4": "=@=<@9",
      "0d": "8",
      "1d": "8",
      "2d": "8",
      "3d": "8",
      "4d": "8"
    }
  ],
  "result": [
    {
      "target_hash": "f85b5789a48ea31c20e4ab31dcfcc434d3931e3bccf59784ec3f4cc456db2ae3c1f5bc70dd883946da7449ddf8f5070c6f885ecb3c7c6b11f63875f6a47f7850",
      "attempt_count": 131589
    },
    {
      "target_hash": "19d44deeb336de2ac3cb83a18ffe296a31af3332ac6c88631d31e97b52d9c03b929e0c498cb221b1f3cee748c430d2d374f08d8d75cba4944edf181c5d9743ff",
      "attempt_count": 207890
    },
    {
      "target_hash": "66b23f6fb06444c00adb6126288d7a2176693fbcd80fc48ccd80f74992be41882c26d0d8f54e3eba30c1a35801e9ab8dcfb071c83d3d46aaa39acd86cec95f5c",
      "attempt_count": 221856
    },
    {
      "target_hash": "beeba8def17402213476f4d66f5879acde3d73cf6527d936420a328d2d9dda31acdf7277e7ea05bede238d2fb16885fdd03d081c0def64fc272676682d4830f1",
      "attempt_count": 340452
    },
    {
      "target_hash": "a30513c89daaa003d63e6aea42d51b078b6e4386475c9f3ddb9baeb959194dd6b4ceeee0b6efd69d495f245f5123b6ceb531c3429ba7909a0f4452c12fa7f33d",
      "attempt_count": 481585
    }
  ]
}

result很好理解,就是每个target命中时的尝试次数,这个transform就是那个js拉出来的垃圾,然后每次请求算法都会随机调整,而且可能有一个非常庞大的算法库随机拼接,所以这是协议实现的巨大挑战之一。

响应为

json
{"action":"challenge"}

如果是blocked就是失败了,challenge是成功了,但是还需要进行图形验证码挑战,如果你的IP足够干净,可能会跳过challenge,pow完成之后就可以提交了。

图形验证码

5.png

第一个请求

GET https://github-api.arkoselabs.com/fc/init-load/?session_token=66018a235cc293bb9.7598136404

返回了一个空的body,有点意义不明,笔者推测只是帮助服务端切换状态什么的。

第二个请求

https://github-api.arkoselabs.com/fc/gfct/

请求体:

token=66018a235cc293bb9.7598136404&sid=ap-southeast-1&render_type=canvas&lang=en&isAudioGame=false&is_compatibility_mode=false&apiBreakerVersion=green&analytics_tier=40

这个sid也是在PublicKey接口里返回的token的一部分,然后这个isAudioGame取决于挑战开始之前你选择的是Visual Puzzle还是Audio Puzzle。

响应体东西很多:

json
{
  "session_token": "66018a235cc293bb9.7598136404",
  "challengeID": "25318a235cd359d91.7162892704",
  "challengeURL": "https://github-api.arkoselabs.com/fc/assets/match-game-ui/0.33.0/standard/index.html",
  "sec": 80,
  "game_data": {
    "customGUI": {
      "example_images": {
        "key": "https://github-api.arkoselabs.com/cdn/fc/assets/game4failureexamples/3dRollball_v2/prod_key1.png",
        "answer": "https://github-api.arkoselabs.com/cdn/fc/assets/game4failureexamples/3dRollball_v2/prod_ans1.png"
      },
      "timed_mode": 1,
      "encrypted_mode": 1,
      "_challenge_imgs": [ // 第一个图片
        "https://github-api.arkoselabs.com/rtig/image?challenge=0&expires=1775043961&gameToken=25318a235cd359d91.7162892704&sessionToken=66018a235cc293bb9.7598136404&signature=ESJDjJ8wTyIMo4XCPg2F2yCiw0bEfDoaZYs4XxR85G4ZdvFsOfgI3%2BU1dE2MXVDS9azXMMRKeWhMwE%2BIvD1KjmlR4wIG4Inlv21hLzPp64wmMaxY8zaRJLvirJoDepT3br%2FeVw572pr1zjq9XAEI5qjgiml%2FSJfuob2QZqWMcVO3fPhPazaZz9dEhsqeGt8YSa5dN90MMv9eWr07q29NXGQoU7wNOG3Br8yd4GstyMU5lXllgC1iZFXrl8SSbzKAkfEh1S0rfWRrhAmFhSP%2BhXPCaNvqLb55cKo5LxNEwrXFaopgjkQihO7veblVKDmRXcHNnV577kRBwlq0ZhDzQQ%3D%3D"
      ],
      "audio_disabled": false,
      "embedded_session_id_enabled": 1,
      "_guiTheme": 3,
      "_progress_prop": {
        "bgColor": "#f7f7f7"
      },
      "fc2_well_done_image": "/fc/assets/graphics/ecbase/StraightTickGrey.svg",
      "_meta_bg_colour": "transparent",
      "_guiTextColor": "#000000",
      "_meta_icon_colour": "#555555"
    },
    "waves": 8,
    "gameType": 4,
    "game_difficulty": 8,
    "instruction_string": "3DRollball_v2_var1",
    "answer_height": 200,
    "answer_width": 200,
    "key_height": 200,
    "key_width": 125
  },
  "sid": "ap-southeast-1",
  "lang": "en",
  "string_table_prefixes": [
    "github_v3_custom_instructions"
  ],
  "string_table": {
    "4.challenge_progress": "{{currentChallenge}} of {{numChallenges}}",
    "4.fail_button": "Try again",
    // 依旧UI字符串表
  },
  "style_theme": "light",
  "dapib_url": "https://github-api.arkoselabs.com/dapib/ap-southeast-1/45cb63bd-789a-417a-91a4-1eb62a7ceb2c/1126.js?expiry=1775042161575&mac=tpJLRXihg2JTEDoexWMrXNyN96ohlZlmKD30%2BM6oKQY%3D"
}

这个第一个图片就是加密的,怎么解密,查看下一个请求

https://github-api.arkoselabs.com/fc/ekey/

请求体:

session_token=66018a235cc293bb9.7598136404&game_token=25318a235cd359d91.7162892704&sid=ap-southeast-1

返回了数据:

json
{
  "time_end": 1775040483,
  "time_end_seconds": 119,
  "decryption_key": "60718a235cd539e16.0582037704"
}

这个decryption_key就是evpKDF密钥,解密第一个图片

6.jpg 7.jpg

解密出来的图片示例

然后这个dapib_url是另一个每次请求都随机生成算法的脚本,用来加密之后的一些请求内容,反混淆样本在附件。

提交图形验证码

让我们分析提交请求https://github-api.arkoselabs.com/fc/ca/

请求体(实际上是一个Urlencode,但是为了方便阅读,笔者在这里使用Reqable的自动转换为json):

json
{
  "session_token": "66018a235cc293bb9.7598136404",
  "game_token": "25318a235cd359d91.7162892704",
  "sid": "ap-southeast-1",
  "guess": "{\"ct\":\"fHNN5WGY8Th65XS+6gxbRw==\",\"iv\":\"a2591693d0768a45441531bccf099440\",\"s\":\"a8a6e40a5f584d60\"}",
  "render_type": "canvas",
  "analytics_tier": "40",
  "bio": "一个base64",
  "is_compatibility_mode": "false",
  "ecdata": "eyJoZWlnaHQiOjQ1MCwid2lkdGgiOjQwMH0=",
  "tguess": "{\"ct\":\"omv2t3M2+8gcMLa/xU2A3SN5q3VmTR3SuZIsKvwaoHnoSRd6+jcroS88WF/vkxp2RtO4QMdagxUzkjMCRmMfoH6ItCTKegqp6ARbwCo9M1VDEmpWS1rmgekP01CLc0gLGWz2kHMfgL2VAPfJbN2R5z641fchgVWcUTwschuW5j6m1tSYxUv2I3NcIuibcjof\",\"iv\":\"4d729246e83888d3289f3db8de914acb\",\"s\":\"9ff42baa374f5bc4\"}"
}

和一个x-requested-id请求头{"ct":"ahjI1ohU02G9DaoF8oD2MFNuVHUKtCjt/a0RmZ+Hzhw=","iv":"0a89d68bd18600afcd70b4b289303a86","s":"9909436dc9561ef8"}

guess是答案链,示例明文:[{"index":3},{"index":1},...]加密为使用evpKDF用SessionToken作为密钥加密。

这个index就是第一列哪个图片符合第二列。

tguess是使用DAPIB也就是上一个请求里的dapib_url里的脚本的方法加密guess

x-requested-id为明文 {"sc":[随机字符串,随机字符串]},然后使用"REQUESTED" + session_token + "ID"作为密钥用evpKDF加密。

bio是一个鼠标轨迹数据,但是经过笔者测试这个数据其实并没有任何高级验证,只要随机塞进去一堆就行了。

8.png

提交之后的响应为

json
{
  "response": "not answered",
  "solved": null,
  "incorrect_guess": null,
  "decryption_key": "70018a235cd39f498.7848585704",
  "time_end": 1775040489,
  "time_end_seconds": 119,
  "_challenge_imgs": [
    "https://github-api.arkoselabs.com/rtig/image?challenge=1&expires=1775043961&gameToken=25318a235cd359d91.7162892704&sessionToken=66018a235cc293bb9.7598136404&signature=RKOO2S3MdoJ7n0alrjxdC8IOUC9V07U5MHkLcK63eygHtj4BflJx1RS2VtvRCGhgKbFJRs4Pd4KtZRuR5nQuT5fhfW9Mwv1KY%2BFeNH0FEvq%2B1UlV47iBb6yonHwXVWIpVZd99pF0koP1VlaleFgRFKl6vzXGZWyvvyu7wp8do2rS7BOTZ0DLiEIc316yoYroHXyhUEdYkMDYR60OeW%2FsstbMcKujVGbOtqxDNEEggaoeh2NvDkZoVp5Y2DLetq2tm7%2FhTbp14Xk8whleULtUvV0Kuo8BaiAmJncHg7WoqoWoxaWY145bPp%2BKKcQ4S%2FjsaXvWm4ru8RfLAPns9bmgDw%3D%3D"
  ]
}

响应包含,下一个图片的decryption_key和url,用decryption_key解密下一个图片就行了。

作答最后一个图片之后会返回

json
{
  "response": "answered",
  "solved": true,
  "incorrect_guess": null,
  "decryption_key": null,
  "time_end": null,
  "time_end_seconds": null,
  "_challenge_imgs": null
}

这里还有一个非常猎奇的检测就是只有这个https://github-api.arkoselabs.com/rtig/image开头的端点有JA3检测,如果你的TLS指纹不是真实浏览器会在下载图片时候被Flag,然后服务器就不会承认你的答案,笔者使用了Impit这个库进行提交,解决了问题。

在得到answered之后可以直接使用session_token提交到GitHub注册的octocaptcha-token,完成验证。

9.png

附录

一个Pow Worker反混淆样本

js
const HASH_STATIC = "webkitSubtle";

function makeMessage(type) {
  return {
    type: type,
    data: {}
  };
}

const TASK_TYPE = {
  START: "start",
  NOOP: "noop"
};

const RESULT_TYPE = {
  ERROR: "error",
  DONE: "done",
  LOADED: "loaded"
};

class Timer {
  constructor() {
    this.startTime = 0;
    this.endTime = 0;
    this.elapsed = 0;
    if (typeof performance !== "undefined") {
      this.now = () => performance.now();
    } else {
      this.now = Date.now;
    }
  }

  start() {
    this.startTime = this.now();
  }

  stop() {
    this.endTime = this.now();
    this.elapsed = this.endTime - this.startTime;
    return this.elapsed;
  }
}

try {
  onmessage = async event => {
    let msg = event.data;

    switch (msg.type) {
      case TASK_TYPE.NOOP: {
        break;
      }

      case TASK_TYPE.START: {
        try {
          const seed = msg.data;

          const result = await (async function () {
            let hashCounter = 0;

            function toBase64(str) {
              const buf = new Uint16Array(str.length);
              for (let i = 0; i < str.length; i++) {
                buf[i] = str.charCodeAt(i);
              }
              return btoa(String.fromCharCode(...new Uint8Array(buf.buffer)));
            }

            async function solveAll(seedData, chunkMeta, startingNonce, timeout) {
              const timer = new Timer();
              timer.start();
              return new Promise(async (resolve, reject) => {
                try {
                  const encoded = toBase64(JSON.stringify(seedData));
                  let chunks = splitIntoChunks(encoded, chunkMeta.length);
                  chunks = chunks.map((chunk, i) => {
                    return {
                      target: chunkMeta[i].targetHash,
                      seed: chunk
                    };
                  });
                  let solved = await solveChunks(chunks, startingNonce, timeout);
                  resolve(solved);
                } catch (err) {
                  if (err.message === "TIMEOUT") {
                    reject({
                      message: "TIMEOUT",
                      hashRate: hashCounter / timer.stop()
                    });
                  } else {
                    reject(err);
                  }
                }
              });
            }

            async function solveChunks(chunks, startingNonce, timeout) {
              let nonce = startingNonce;
              const results = [];
              const timer = new Timer();
              timer.start();
              for (let i = 0; i < chunks.length; i++) {
                const solved = await solveOne(startingNonce, chunks[i], timer, timeout);
                nonce = solved.nonce;
                results.push(solved);
              }
              const elapsed = timer.stop();
              const lastNonce = results[results.length - 1].nonce;
              const hashRateInternal = (lastNonce - startingNonce) / elapsed;
              return {
                hashRateInternal: hashRateInternal,
                results: results,
                time: Math.round(elapsed)
              };
            }

            async function solveOne(startingNonce, chunk, timer, timeout) {
              let nonce = startingNonce;
              let hash = "";
              let hashFn = sha512;
              while (hash !== chunk.target && timer.stop() < timeout) {
                hash = await hashFn(chunk.seed + nonce);
                nonce++;
                hashCounter++;
              }
              if (timer.stop() >= timeout) {
                throw new Error("TIMEOUT");
              }
              return {
                target: chunk.target,
                nonce: nonce
              };
            }

            function splitIntoChunks(str, count) {
              let size = Math.floor(str.length / count);
              str = str.substring(0, size * count);
              let chunks = [];
              for (let i = 0; i < str.length - 1; i += size) {
                if (i === count - 1) {
                  chunks.push(str.substring(i));
                } else {
                  chunks.push(str.substring(i, i + size));
                }
              }
              return chunks;
            }

            async function transformAnswers(answers) {
              let result = answers;

              function levenshtein(a, b) {
                const matrix = new Array(a.length + 1)
                  .fill(null)
                  .map(() => new Array(b.length + 1).fill(null));
                for (let i = 0; i <= a.length; i++) {
                  matrix[i][0] = i;
                }
                for (let j = 0; j <= b.length; j++) {
                  matrix[0][j] = j;
                }
                for (let i = 1; i <= a.length; i++) {
                  for (let j = 1; j <= b.length; j++) {
                    const del = matrix[i - 1][j] + 1;
                    const ins = matrix[i][j - 1] + 1;
                    const sub = matrix[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
                    matrix[i][j] = Math.min(del, ins, sub);
                  }
                }
                return matrix[a.length][b.length];
              }

              function alphabet() {
                return Array.from(Array(26)).map((_, i) => i + 0x41).map(String.fromCharCode);
              }

              result = result.map(answer => {
                const out = answer;
                Object.keys(answer).forEach(key => {
                  const value = answer[key];
                  const offset = levenshtein(btoa(encodeURIComponent(value)), alphabet());
                  const shifted = value.split("").map(ch => {
                    const code = ch.charCodeAt(0) + offset;
                    if (code > 0x7e) return code - 0x41;
                    return code;
                  });
                  out[key] = shifted.map(c => String.fromCharCode(c)).join("");
                  out[key + "d"] = offset.toString();
                });
                return out;
              });

              result = result.map(answer => {
                const out = answer;
                Object.keys(answer).forEach(key => {
                  const value = key[0];
                });
                return out;
              });

              result = result.map(answer => {
                const out = answer;
                Object.keys(answer).forEach(key => {
                  const value = answer[key];
                  const half = Math.ceil(value.length / 2);
                  const first = value.slice(0, half);
                  const second = value.slice(half, value.length);
                  out[key] = second + first;
                });
                return out;
              });

              const shiftTable = [
                0x4e, 0x2a, 0x11, 0x5b, 0x3, 0x45, 0x3a, 0x48, 0x1d, 0x37, 0x62, 0x54,
                0x26, 0x57, 0xa, 0x33, 0x3f, 0x6, 0x21, 0xc, 0x28, 0x2, 0x4b, 0x19,
                0x5f, 0x32, 0x3d, 0x44, 0x25, 0xf, 0xe, 0x63, 0x2c, 0x35, 0xd, 0x5d,
                0x5c, 0x24, 0x3b, 0x9, 0x13, 0x8, 0x23, 0x1c, 0x36, 0x38, 0x2b, 0x16,
                0x42, 0x61, 0x29, 0x27
              ];

              function substitute(ch) {
                const code = ch.charCodeAt(0);
                if (code >= 0x61 && code <= 0x7a) {
                  return String.fromCharCode(shiftTable[code - 0x61]);
                }
                if (code >= 0x41 && code <= 0x5a) {
                  return String.fromCharCode(shiftTable[code - 0x27]);
                }
                return ch;
              }

              result = result.map(answer => {
                const out = answer;
                Object.keys(answer).forEach(key => {
                  const value = answer[key];
                  const chars = value.split("");
                  const transformed = chars.map(substitute).map(substitute).map(substitute);
                  out[key] = transformed.join("");
                });
                return out;
              });

              return result;
            }

            async function stringifyAndTransform(answers) {
              const stringified = answers.map(answer => {
                const out = answer;
                Object.keys(answer).forEach(key => {
                  out[key] = answer[key].toString();
                });
                return out;
              });
              let transformed = await transformAnswers(stringified);
              transformed = transformed.map(answer => {
                const out = answer;
                Object.keys(answer).forEach(key => {
                  let value = out[key];
                  out[key] = value;
                });
                return out;
              });
              return transformed;
            }

            const answers = seed.answers;
            const chunkMeta = seed.foxData;
            const startingNonce = seed.PenguinHorse;
            const timeout = seed.timeout;

            let transformedAnswers = await stringifyAndTransform(answers);
            const solved = await solveAll(transformedAnswers, chunkMeta, startingNonce, timeout);
            const nonceMap = Object.fromEntries(
              solved.results.map((r, i) => {
                return [i, r.nonce];
              })
            );
            const finalTransform = await stringifyAndTransform([...transformedAnswers, nonceMap]);

            const output = {
              hashRate: solved.hashRateInternal,
              time: solved.time,
              finalTransform: finalTransform,
              targetHashData: solved.results.map(r => {
                return {
                  iterations: r.nonce,
                  targetHash: r.target
                };
              })
            };
            return output;
          })();

          let doneMsg = makeMessage(RESULT_TYPE.DONE);
          doneMsg.data = result;
          postMessage(doneMsg);
        } catch (err) {
          let errMsg = makeMessage(RESULT_TYPE.ERROR);
          let message = "";
          if (typeof err === "object" && err) {
            message = err.message;
          }
          if (message === "TIMEOUT") {
            errMsg.data = {
              error: {
                data: {
                  hashRate: err.hashRate
                },
                type: "timeout",
                message: "done"
              }
            };
          } else {
            errMsg.data = {
              error: {
                data: message,
                trace: err.data,
                type: "runtime_error",
                message: "Error when processing work"
              }
            };
          }
          postMessage(errMsg);
        }
        break;
      }

      default: {
        let unknownMsg = makeMessage(RESULT_TYPE.ERROR);
        unknownMsg.data = {
          error: "unknown message type"
        };
        postMessage(unknownMsg);
      }
    }
  };

  const loadedMsg = makeMessage(RESULT_TYPE.LOADED);
  postMessage(loadedMsg);
} catch (err) {
  let errMsg = makeMessage(RESULT_TYPE.ERROR);
  errMsg.data = {
    error: {
      type: "init_error",
      data: err,
      message: "stringify error"
    }
  };
  postMessage(errMsg);
}

const sha512 = function () {
  function getWebkitSubtle() {
    return !!crypto && "webkitSubtle" in crypto ? crypto.webkitSubtle : null;
  }
  function getSubtle() {
    return !!crypto && crypto.subtle || getWebkitSubtle();
  }
  return async function hashStandard(data) {
    const msgBuffer = new TextEncoder().encode(data);
    const subtle = getSubtle();
    const hashBuffer = await subtle.digest("SHA-512", msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
    return hashHex;
  };
}();

DAPIB反混淆

js
(function () {
  function sendToParent(tanswer) {
    window.parent.ae.dapibReceive({ tanswer: tanswer });
  }

  function encodeCharCode(c) {
    const i = c - 35;
    const squared = i * i;
    const shifted = squared + 17;
    const halved = shifted / 2;
    const floored = Math.floor(halved);
    const mod = floored % 101;
    return mod + 35;
  }

  function transform(answers) {
    let result = answers;

    result = result.map(obj => {
      Object.keys(obj).forEach(key => {
        const val = obj[key];
        const lenStr = val.length.toString();
        const n = lenStr.length;
        const head = lenStr.slice(0, Math.floor(n / 2));
        const tail = lenStr.slice(Math.floor(n / 2), n);
        obj[head + key + tail] = val;
        delete obj[key];
      });
      return obj;
    });

    result = result.map(obj => {
      Object.keys(obj).forEach(key => {
        const chars = obj[key].split('');
        const encoded = chars.map(ch =>
          String.fromCharCode(encodeCharCode(ch.charCodeAt(0)))
        );
        for (let i = 4; i < encoded.length; i += 5) {
          encoded.splice(i, 0, '-');
        }
        obj[key] = encoded.join('');
      });
      return obj;
    });

    result = result.map(obj => {
      Object.keys(obj).forEach(key => {
        const val = obj[key];
        const lenStr = val.length.toString();
        const n = lenStr.length;
        const head = lenStr.slice(0, Math.floor(n / 2));
        const tail = lenStr.slice(Math.floor(n / 2), n);
        obj[head + key + tail] = val;
        delete obj[key];
      });
      return obj;
    });

    result = result.map(obj => {
      Object.keys(obj).forEach(key => {
        const val = obj[key];
        const lenStr = val.length.toString();
        const n = lenStr.length;
        const head = lenStr.slice(0, Math.floor(n / 2));
        const tail = lenStr.slice(Math.floor(n / 2), n);
        obj[head + key + tail] = val;
        delete obj[key];
      });
      return obj;
    });

    return result;
  }

  function process(answers, suffix) {
    (function selfDefense() {
      return arguments.callee.toString().search('(((.+)+)+)+$');
    })();

    const stringified = answers.map(obj => {
      Object.keys(obj).forEach(key => {
        obj[key] = obj[key].toString();
      });
      return obj;
    });

    let result = transform(stringified);

    result = result.map(obj => {
      Object.keys(obj).forEach(key => {
        obj[key] = obj[key] + (suffix ? suffix : '');
      });
      return obj;
    });

    sendToParent(result);
  }

  try {
    const doc = window.document;
    let poison = undefined;

    const isNodeLike =
      Object.prototype.toString.call(
        typeof process !== 'undefined' ? process : 0
      ) === '[object process]';

    const isPrerender =
      doc.hidden &&
      doc.visibilityState === 'prerender' &&
      typeof window.requestAnimationFrame === 'undefined' &&
      typeof window.cancelAnimationFrame === 'undefined';

    const isDomAbnormal = !(doc.activeElement instanceof Object);

    if (isNodeLike || isPrerender || isDomAbnormal) {
      poison = String.fromCharCode(Math.random() * 26 + 0x41);
    }

    const answer = window.parent.ae.answer;
    process(answer, poison);
  } catch (err) {
    sendToParent(err);
  }
})();

BDA反混淆

js
'use strict';

const RSA_PUBLIC_KEY_SPKI_B64 =
  "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8dYw7H536reZPgEFRZka62Lhgf" +
  "cJdbm5ugSL+ypixEMJq9FynLgdXUg7tEhQgM0bnfNlsiGuWjQSR/Y8Jd7n0ouV97gR4Efy" +
  "sYydmKowfwcwBlTIlvNC4crSqNpVkI09F4+S1UktEytN81dblxrNTY895rfoLy5H+IMfCk" +
  "TLuKC8j3PwTW1my6GsYv1fTVgu5vv3zhsg0nndPE/RL/BPpqUs9yR2iqIBLiT+2DnaJEx8" +
  "5bf9kOUn7FYQ6vCCJ8Mkcuzp0btOHU20PoqbjoikSzuxMaD3D40Q0TQPGzfcmhArcUJVee" +
  "+wnPHC81jOaZvPcUz3ySAfpbdFbxU4KC37nwIDAQAB";


function getNavigationEntryCount() {
  if (window.navigation) {
    if (window.navigation.entries) {
      const entries = window.navigation.entries();
      return entries.length;
    }
  }
  return null;
}

function getHeapSizeLimit() {
  const perf = window.performance;
  if (!perf) return [1, null];

  const mem = perf.memory;
  if (!mem) return [2, null];

  const limit = mem.jsHeapSizeLimit;
  if (!limit) return [3, null];

  return [0, limit];
}

function collectFingerprint(payload) {
  if (!payload || !payload.value) return;

  const navEntries = getNavigationEntryCount();
  const heapInfo = getHeapSizeLimit();

  payload.value.push({ key: "vsadsa", value: navEntries });
  payload.value.push({ key: "basfas", value: heapInfo });
  payload.value.push({ key: "lfasdgs", value: window.arkl.cbid });

  const f25 = payload.value[25];
  const f26 = payload.value[26];
  const f1 = payload.value[1];
  const f18 = payload.value[18];

  if (f25 && f1 && f18) {
    if (
      f1.value && f1.value.length > 12 &&
      f18.value && f18.value.length > 12
    ) {
      payload.value[25] = {
        key: f25.key(),
        value: f1.value.slice(0, 3) + f18.value.slice(0, 3),
      };
    } else {
      payload.value[25] = { key: f25.key(), value: "abcdef" };
    }
  }

  if (f26) {
    if (f26.value) {
      payload.value[26] = { key: f26.key(), value: f26.value * 3 };
    }
  }
}


async function encryptWebCrypto(payload, rsaPublicKeyB64, crypto) {
  if (!payload) return null;
  if (!rsaPublicKeyB64) return null;

  const plaintext = JSON.stringify(payload);

  async function generateAesKey() {
    return crypto.subtle.generateKey(
      { name: "AES-GCM", length: 256 },
      true,
      ["encrypt"]
    );
  }

  function generateIv() {
    return crypto.getRandomValues(new Uint8Array(12));
  }

  async function aesEncrypt(aesKey, iv, text) {
    const data = window.arkl.strToBytes(text);
    return crypto.subtle.encrypt(
      { name: "AES-GCM", tagLength: 128, iv: iv },
      aesKey,
      data
    );
  }

  function splitCiphertextAndTag(gcmOutput) {
    const bytes = new Uint8Array(gcmOutput);
    const TAG_LEN = 16;
    const ciphertext = new Uint8Array(bytes.subarray(0, bytes.length - TAG_LEN));
    const tag = new Uint8Array(bytes.subarray(bytes.length - TAG_LEN));
    return { ciphertextBuffer: ciphertext.buffer, tagBuffer: tag.buffer };
  }

  async function wrapAesKeyWithRsa(iv, aesKey) {
    const rawAesKey = await crypto.subtle.exportKey("raw", aesKey);

    const spkiDer = window.arkl.b64ToBytes(rsaPublicKeyB64);
    const rsaPubKey = await crypto.subtle.importKey(
      "spki",
      spkiDer,
      { name: "RSA-OAEP", hash: "SHA-256" },
      false,
      ["encrypt"]
    );

    return crypto.subtle.encrypt(
      { name: "RSA-OAEP", hash: "SHA-256" },
      rsaPubKey,
      rawAesKey
    );
  }

  const aesKey = await generateAesKey();
  const iv = generateIv();
  const gcmOut = await aesEncrypt(aesKey, iv, plaintext);
  const parts = splitCiphertextAndTag(gcmOut);
  const cipher = parts.ciphertextBuffer;
  const tag = parts.tagBuffer;
  const wrappedKey = await wrapAesKeyWithRsa(iv, aesKey);

  const cipherB64 = window.arkl.bytesToB64(cipher);
  const tagB64 = window.arkl.bytesToB64(tag);
  const ivB64 = window.arkl.bytesToB64(iv.buffer);
  const keyB64 = window.arkl.bytesToB64(wrappedKey);

  return keyB64 + ivB64 + tagB64 + cipherB64;
}


async function encryptMsrCrypto(payload, rsaPublicKeyB64, msrCrypto) {
  function seedPrng(entropySource) {
    const seed = [];
    for (let i = 0; i < entropySource.length; i++) {
      seed[i] = entropySource[i];
    }
    msrCrypto.initPrng(seed);
  }

  const entropy = new Uint8Array(48);
  window.msCrypto.getRandomValues(entropy);
  seedPrng(entropy);

  return await encryptWebCrypto(payload, rsaPublicKeyB64, msrCrypto);
}


function forgeParsePublicKey(rsaPublicKeyB64, forge) {
  const der = forge.util.decode64(rsaPublicKeyB64);
  const asn1 = forge.asn1.fromDer(der);
  return forge.pki.publicKeyFromAsn1(asn1);
}

function forgeRandomKey(forge) {
  return forge.random.getBytesSync(32);
}

function forgeRandomIv(forge) {
  return forge.random.getBytesSync(12);
}

function forgeAesEncrypt(aesKey, iv, plaintext, forge) {
  const cipher = forge.cipher.createCipher("AES-GCM", aesKey);
  cipher.start({ iv: iv });
  cipher.update(forge.util.createBuffer(plaintext, "utf8"));
  const ok = cipher.finish();
  if (!ok) return null;
  return cipher;
}

function forgeWrapAesKey(aesKey, rsaPublicKeyB64, forge) {
  const rsaPubKey = forgeParsePublicKey(rsaPublicKeyB64, forge);
  const md = forge.md.sha256.create();
  return rsaPubKey.encrypt(aesKey, "RSA-OAEP", { md: md });
}

function encryptForge(payload, rsaPublicKeyB64, forge) {
  if (!payload) return null;
  if (!forge) return null;
  if (!rsaPublicKeyB64) return null;

  const aesKey = forgeRandomKey(forge);
  const iv = forgeRandomIv(forge);

  const cipher = forgeAesEncrypt(aesKey, iv, JSON.stringify(payload), forge);
  if (!cipher) return null;

  const ciphertext = cipher.output.getBytes();
  const tag = cipher.mode.tag.getBytes();
  const wrappedKey = forgeWrapAesKey(aesKey, rsaPublicKeyB64, forge);

  const cipherB64 = forge.util.encode64(ciphertext);
  const tagB64 = forge.util.encode64(tag);
  const ivB64 = forge.util.encode64(iv);
  const keyB64 = forge.util.encode64(wrappedKey);

  return keyB64 + ivB64 + tagB64 + cipherB64;
}


async function main() {
  let result = undefined;

  const payload = window.arkl.pl;
  const rsaPublicKeyB64 = RSA_PUBLIC_KEY_SPKI_B64;

  collectFingerprint(payload);

  if (window.arkl.cryptoType === "crypto") {
    result = await encryptWebCrypto(payload, rsaPublicKeyB64, window.arkl.crypto);
  } else if (window.arkl.cryptoType === "msr") {
    result = await encryptMsrCrypto(payload, rsaPublicKeyB64, window.arkl.msrCrypto);
  } else if (window.arkl.cryptoType === "forge") {
    result = encryptForge(payload, rsaPublicKeyB64, window.arkl.forge);
  }

  window.arkl.rs = result;
}

main();

评论

加载评论中…