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

本短文记录了笔者对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难度会降低,杂牌邮箱或者容易滥用的邮箱难度会对应的提高。
请求之后返回了一个验证码块
<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的主体了。

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

混淆的api.js
Settings请求
在加载api.js之后第一个请求为https://github-api.arkoselabs.com/v2/747B83EC-2CA3-43AD-A7DF-701F286FBABA/settings返回的是一些样式之类的设置,不太重要。
{
"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我们
{
"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

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

然后再回到这个请求,响应体也是一个巨大的json,也是一些settings
{
"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.jshttps://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的第一节,然后响应是这样的:
{
"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
{
"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拉出来的垃圾,然后每次请求算法都会随机调整,而且可能有一个非常庞大的算法库随机拼接,所以这是协议实现的巨大挑战之一。
响应为
{"action":"challenge"}如果是blocked就是失败了,challenge是成功了,但是还需要进行图形验证码挑战,如果你的IP足够干净,可能会跳过challenge,pow完成之后就可以提交了。
图形验证码

第一个请求
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。
响应体东西很多:
{
"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
返回了数据:
{
"time_end": 1775040483,
"time_end_seconds": 119,
"decryption_key": "60718a235cd539e16.0582037704"
}这个decryption_key就是evpKDF密钥,解密第一个图片

解密出来的图片示例
然后这个dapib_url是另一个每次请求都随机生成算法的脚本,用来加密之后的一些请求内容,反混淆样本在附件。
提交图形验证码
让我们分析提交请求https://github-api.arkoselabs.com/fc/ca/
请求体(实际上是一个Urlencode,但是为了方便阅读,笔者在这里使用Reqable的自动转换为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是一个鼠标轨迹数据,但是经过笔者测试这个数据其实并没有任何高级验证,只要随机塞进去一堆就行了。

提交之后的响应为
{
"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解密下一个图片就行了。
作答最后一个图片之后会返回
{
"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,完成验证。

附录
一个Pow Worker反混淆样本
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反混淆
(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反混淆
'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();订阅更新
如果您喜欢我的文章,输入您的邮箱订阅更新,有新的文章会通知您
登录 GitHub 后参与讨论
加载评论中…