├── .gitignore ├── Cargo.toml ├── README.md ├── benches └── generate_token.rs └── src ├── bin ├── deobfuscator │ └── main.rs └── token │ └── main.rs ├── deobfuscate ├── computed_member_expr.rs ├── math_expr.rs ├── mod.rs ├── proxy_vars.rs └── strings.rs ├── lib.rs └── shared_cursor.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | ### Rust template 4 | # Generated by Cargo 5 | # will have compiled files and executables 6 | debug/ 7 | target/ 8 | 9 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 10 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 11 | Cargo.lock 12 | 13 | # These are backup files generated by rustfmt 14 | **/*.rs.bk 15 | 16 | # MSVC Windows builds of rustc generate these, which store debugging information 17 | *.pdb 18 | 19 | ### CLion template 20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 21 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 22 | 23 | # User-specific stuff 24 | .idea/**/workspace.xml 25 | .idea/**/tasks.xml 26 | .idea/**/usage.statistics.xml 27 | .idea/**/dictionaries 28 | .idea/**/shelf 29 | 30 | # AWS User-specific 31 | .idea/**/aws.xml 32 | 33 | # Generated files 34 | .idea/**/contentModel.xml 35 | 36 | # Sensitive or high-churn files 37 | .idea/**/dataSources/ 38 | .idea/**/dataSources.ids 39 | .idea/**/dataSources.local.xml 40 | .idea/**/sqlDataSources.xml 41 | .idea/**/dynamic.xml 42 | .idea/**/uiDesigner.xml 43 | .idea/**/dbnavigator.xml 44 | 45 | # Gradle 46 | .idea/**/gradle.xml 47 | .idea/**/libraries 48 | 49 | # Gradle and Maven with auto-import 50 | # When using Gradle or Maven with auto-import, you should exclude module files, 51 | # since they will be recreated, and may cause churn. Uncomment if using 52 | # auto-import. 53 | # .idea/artifacts 54 | # .idea/compiler.xml 55 | # .idea/jarRepositories.xml 56 | # .idea/modules.xml 57 | # .idea/*.iml 58 | # .idea/modules 59 | # *.iml 60 | # *.ipr 61 | 62 | # CMake 63 | cmake-build-*/ 64 | 65 | # Mongo Explorer plugin 66 | .idea/**/mongoSettings.xml 67 | 68 | # File-based project format 69 | *.iws 70 | 71 | # IntelliJ 72 | out/ 73 | 74 | # mpeltonen/sbt-idea plugin 75 | .idea_modules/ 76 | 77 | # JIRA plugin 78 | atlassian-ide-plugin.xml 79 | 80 | # Cursive Clojure plugin 81 | .idea/replstate.xml 82 | 83 | # SonarLint plugin 84 | .idea/sonarlint/ 85 | 86 | # Crashlytics plugin (for Android Studio and IntelliJ) 87 | com_crashlytics_export_strings.xml 88 | crashlytics.properties 89 | crashlytics-build.properties 90 | fabric.properties 91 | 92 | # Editor-based Rest Client 93 | .idea/httpRequests 94 | 95 | # Android studio 3.1+ serialized cache file 96 | .idea/caches/build_file_checksums.ser 97 | 98 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vercel-anti-bot" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.71" 8 | base64 = "0.21.2" 9 | serde = "1.0.167" 10 | serde_json = "1.0.100" 11 | swc = "0.264.13" 12 | swc_core = { version = "0.79.14", features = ["ecma_plugin_transform", "common", "ecma_codegen", "swc_ecma_parser"] } 13 | swc_ecma_parser = "0.137.2" 14 | swc_ecma_transforms = "0.221.7" 15 | 16 | [dev-dependencies] 17 | criterion = { version = "0.5.1", features = ["html_reports"] } 18 | 19 | [[bench]] 20 | name = "generate_token" 21 | harness = false 22 | 23 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vercel-anti-bot 2 | Reverse engineering and analysis of Vercel's bot protection used on https://sdk.vercel.ai 3 | (and potentially more of their platforms). 4 | 5 | ## Note 6 | This repository is outdated. Vercel now uses [Kasada](https://www.kasada.io/) on their platforms instead of their own custom solution. 7 | The code in this repository can also be heavily improved; this was originally made when I was fairly new to Rust. 8 | 9 | ## Usage 10 | The `generate_token` function in `src/lib.rs` takes in the data from the `/openai.jpeg` response, 11 | which returns a valid token for usage in the `custom-encoding` header on a protected request. 12 | 13 | While this repository does not provide request automation, you can generate a token and replay 14 | a request from the browser with the generated token. Keep in mind the data returned from `/openai.jpeg` 15 | seems to be *very* short-lived. 16 | 17 | Disclaimer: this repository is intended for criticism only. 18 | 19 | ### Benchmarks 20 | 21 | Token generation time: 22 | 23 | | CPU | Average time | 24 | |---------------|--------------| 25 | | Apple M2 | 100.66 µs | 26 | | Ryzen 9 5950X | 213.37 µs | 27 | 28 | ## Background 29 | I first became aware of this after seeing [this tweet](https://twitter.com/jaredpalmer/status/1675192755763412992?s=20) 30 | from Vercel's VP claiming they have reduced costs by 100x since implementing this solution (as well as rate limiting). 31 | The tweet claims their solution is "quite promising" and encouraged anyone interested to contact him. 32 | 33 | This sounds convincing at first, but when taking a look at how their bot protection works, unfortunately it's *very easy*. 34 | This is extremely disappointing especially if you read [this reply](https://twitter.com/jaredpalmer/status/1675196288831311876?s=20) 35 | claiming Vercel's CTO who previously ran Google Search built this bot protection system. 36 | For clarification, a system like this can be built easily by anyone in the cybersecurity space, and a lot of people - 37 | including myself - can easily do better. 38 | 39 | The analysis below explains how the system works and how this repository circumvents it. 40 | 41 | ## Analysis 42 | If you navigate to https://sdk.vercel.ai, open DevTools, navigate to the Sources tab and then 43 | use the Search feature at the bottom using this filter: 44 | 45 | `file:* function useAntibotToken()` 46 | 47 | You should come across a function in a JavaScript file that looks like this: 48 | ```js 49 | function useAntibotToken() { 50 | let {data, mutate, isValidating} = (0, 51 | swr__WEBPACK_IMPORTED_MODULE_0__.ZP)("antibot-token", async()=>{ 52 | let response = await fetch("/openai.jpeg") 53 | , data = JSON.parse(atob(await response.text())) 54 | , ret = eval("(".concat(data.c, ")(data.a)")); 55 | return btoa(JSON.stringify({ 56 | r: ret, 57 | t: data.t 58 | })) 59 | } 60 | , { 61 | fallbackData: "", 62 | refreshInterval: 6e4, 63 | dedupingInterval: 2e3 64 | }); 65 | return [data, mutate] 66 | } 67 | ``` 68 | 69 | From this code, we can see that: 70 | 1) The browser makes a request to https://sdk.vercel.ai/openai.jpeg. 71 | 2) The response is base64 decoded and parsed as JSON. 72 | 3) The following code is evaluated using the `eval` function: `(c)(data.a)`, where `c` is the `c` property of the JSON object. 73 | 4) The function returns a base64 encoded JSON object, with `r` being the evaluated value and `t` being the `t` property from the JSON object. 74 | 75 | The response from the `/openai.jpeg` request is a large string. For this example, we'll be using this one: 76 | ``` 77 | eyJ0IjoiZXlKaGJHY2lPaUprYVhJaUxDSmxibU1pT2lKQk1qVTJSME5OSW4wLi45UnRnbGU3VmtaVW80N1VwLjZCZkFkYkRnMERuVFJfcDJhb0JhMzhDMktYZHp0bEdKaHppem5kdzBsRGJZUWNLRjRwMjVRckhqYV9ZWG5IY3V2UkhDNURMZFJyTm9iYU5DeThVMXZ2OVVsWnlXdHFsU3VSUEdhdkpsVzNIZnp5VzlRN2JwQUJTMmtQQ1dWWTAuWFd6b1I2Ym5HTmVjaEJESlZZMXB6dyIsImMiOiJmdW5jdGlvbihhKXsoZnVuY3Rpb24oZSxzKXtmb3IodmFyIHQ9eCxuPWUoKTtbXTspdHJ5e3ZhciBpPXBhcnNlSW50KHQoMzA1KSkvMStwYXJzZUludCh0KDMwNykpLzIqKC1wYXJzZUludCh0KDMxMCkpLzMpK3BhcnNlSW50KHQoMzAzKSkvNCstcGFyc2VJbnQodCgyOTkpKS81K3BhcnNlSW50KHQoMzAyKSkvNistcGFyc2VJbnQodCgzMDApKS83KigtcGFyc2VJbnQodCgzMDkpKS84KStwYXJzZUludCh0KDMwMSkpLzkqKC1wYXJzZUludCh0KDMwNCkpLzEwKTtpZihpPT09cylicmVhaztuLnB1c2gobi5zaGlmdCgpKX1jYXRjaHtuLnB1c2gobi5zaGlmdCgpKX19KShyLDEyMjA5MSoxKzY2NTQ3NCstMjkzMzM3KTtmdW5jdGlvbiB4KGUscyl7dmFyIHQ9cigpO3JldHVybiB4PWZ1bmN0aW9uKG4saSl7bj1uLSgtNTM1KzQzMyozKy00NjcpO3ZhciBjPXRbbl07cmV0dXJuIGN9LHgoZSxzKX1mdW5jdGlvbiByKCl7dmFyIGU9W1wiNDYxMzRpbGdWU09cIixcIjI2NjA3NDRqaEdtb1BcIixcIjMzODYwMHhlR25iSFwiLFwiOTY2NjY5aHZRSXBPXCIsXCJMTjEwXCIsXCI2MjA3MnBEZnVOc1wiLFwibG9nMlwiLFwiOGdQZmRJaVwiLFwiNjlmRnNXZmFcIixcImtleXNcIixcIm1hcmtlclwiLFwicHJvY2Vzc1wiLFwiMzQzNDAwNW1EWmN1elwiLFwiNTM0MjQ5MU1HYk93WFwiLFwiMTM1dE9CZGR2XCJdO3JldHVybiByPWZ1bmN0aW9uKCl7cmV0dXJuIGV9LHIoKX1yZXR1cm4gZnVuY3Rpb24oKXt2YXIgZT14O3JldHVyblthL01hdGhbZSgzMDgpXShhKk1hdGhbZSgzMDYpXSksT2JqZWN0W2UoMzExKV0oZ2xvYmFsVGhpc1tlKDI5OCldfHx7fSksZ2xvYmFsVGhpc1tlKDI5NyldXX0oKX0iLCJhIjowLjUyNTY4ODU3Mjk2MDM1NDR9 78 | ``` 79 | 80 | We can use this simple Python script and run it in the terminal to see what the JSON object is. 81 | ```python 82 | import base64 83 | import json 84 | raw_data = "" # the data you want to decode 85 | decoded_data = base64.b64decode(raw_data) 86 | data = json.loads(decoded_data) 87 | with open("data.json", "w") as f: # change file name to anything you like 88 | json.dump(data, f) 89 | ``` 90 | Now that we've decoded the data, we can see the JSON object: 91 | ```json 92 | { 93 | "t": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..9Rtgle7VkZUo47Up.6BfAdbDg0DnTR_p2aoBa38C2KXdztlGJhzizndw0lDbYQcKF4p25QrHja_YXnHcuvRHC5DLdRrNobaNCy8U1vv9UlZyWtqlSuRPGavJlW3HfzyW9Q7bpABS2kPCWVY0.XWzoR6bnGNechBDJVY1pzw", 94 | "c": "function(a){(function(e,s){for(var t=x,n=e();[];)try{var i=parseInt(t(305))/1+parseInt(t(307))/2*(-parseInt(t(310))/3)+parseInt(t(303))/4+-parseInt(t(299))/5+parseInt(t(302))/6+-parseInt(t(300))/7*(-parseInt(t(309))/8)+parseInt(t(301))/9*(-parseInt(t(304))/10);if(i===s)break;n.push(n.shift())}catch{n.push(n.shift())}})(r,122091*1+665474+-293337);function x(e,s){var t=r();return x=function(n,i){n=n-(-535+433*3+-467);var c=t[n];return c},x(e,s)}function r(){var e=[\"46134ilgVSO\",\"2660744jhGmoP\",\"338600xeGnbH\",\"966669hvQIpO\",\"LN10\",\"62072pDfuNs\",\"log2\",\"8gPfdIi\",\"69fFsWfa\",\"keys\",\"marker\",\"process\",\"3434005mDZcuz\",\"5342491MGbOwX\",\"135tOBddv\"];return r=function(){return e},r()}return function(){var e=x;return[a/Math[e(308)](a*Math[e(306)]),Object[e(311)](globalThis[e(298)]||{}),globalThis[e(297)]]}()}", 95 | "a": 0.5256885729603544 96 | } 97 | ``` 98 | 99 | We can now see that the `c` property is a JavaScript function that has one parameter, `a`, which is the `a` property 100 | of the JSON object as we mentioned previously from looking at the `eval` code. The `t` property doesn't appear to be used 101 | in the code (at least from what we know so far) and is only used as a field in the encoded JSON object that is returned. 102 | 103 | If you take the `c` property and paste it into https://beautifier.io/, the code is now much easier to read: 104 | ```js 105 | function(a) { 106 | function x(e, s) { 107 | var t = r(); 108 | return x = function(n, i) { 109 | n = n - (71 * -137 + 5097 + 4754); 110 | var c = t[n]; 111 | return c 112 | }, x(e, s) 113 | } 114 | return function(e, s) { 115 | for (var t = x, n = e(); 116 | [];) try { 117 | var i = -parseInt(t(135)) / 1 + parseInt(t(126)) / 2 + -parseInt(t(124)) / 3 * (parseInt(t(128)) / 4) + -parseInt(t(130)) / 5 + parseInt(t(133)) / 6 * (parseInt(t(131)) / 7) + parseInt(t(132)) / 8 + parseInt(t(125)) / 9; 118 | if (i === s) break; 119 | n.push(n.shift()) 120 | } catch { 121 | n.push(n.shift()) 122 | } 123 | }(r, -170842 + -1 * 92122 + 375877), 124 | function() { 125 | var e = x; 126 | return [a * Math[e(127)](a * Math.E), Object[e(134)](globalThis[e(129)] || {}), globalThis[e(136)]] 127 | }(); 128 | 129 | function r() { 130 | var e = ["7WUOLfS", "406424fiusCg", "293790OLgwin", "keys", "176487LGrtxs", "data", "69177FwHYUB", "1387242vPbovG", "223906qcnyvM", "log1p", "12xdPxHN", "process", "36410PdKtQR"]; 131 | return r = function() { 132 | return e 133 | }, r() 134 | } 135 | } 136 | ``` 137 | 138 | As you can probably tell, the code is obfuscated, however fortunately for us the obfuscation used here is https://obfuscator.io/, 139 | a public obfuscation tool that has public deobfuscation tools available, and also is pretty easy to reverse engineer yourself 140 | if you have experience with JavaScript AST libraries, like SWC or Babel. 141 | 142 | Unfortunately https://deobfuscate.io/ did not work for me (the browser just froze for a second and produced nothing), 143 | so I decided to make my own deobfuscator using SWC, which can be found in the `src/deobfuscate` directory. 144 | 145 | I first noticed what I call "proxy variables" which is a type of transformation obfuscator.io does. 146 | It introduces variables that simply refer to other identifiers (only functions in this case) to 147 | make the deobfuscation process more annoying. Take this example: 148 | ```js 149 | function x() {} 150 | var y = x; 151 | y(); 152 | ``` 153 | This code can easily just be: 154 | ```js 155 | function x() {} 156 | x(); 157 | ``` 158 | This is what the `proxy_vars` transformer does. It removes these extra variables and modifies all 159 | `CallExpression` nodes to use the real identifier instead. 160 | 161 | However, we do also need to be aware of special cases like these: 162 | ```js 163 | function x() {} 164 | var y = x; 165 | function doStuff(x) { 166 | y(); 167 | } 168 | ``` 169 | If we replaced `y()` with `x()` in this case, we'd be pointing to the `x` parameter, which is incorrect. 170 | To see more about how I handled this, take a look at the visitor code yourself in `src/deobfuscate/proxy_vars.rs`. 171 | 172 | After dealing with the proxy vars, I reversed the string obfuscation. 173 | Fortunately for me I already knew how their obfuscation works, 174 | but if you don't know, it's pretty simple; an array of strings (the `e` variable in this case) is returned 175 | from the function `r` as a *reference*, meaning the returned array can be modified by callers of `r`. 176 | An IIFE (Immediately Invoked Function Expression) modifies the array, which is where the `parseInt` stuff 177 | comes in: an expression is computed that produces either a number or NaN. If the number doesn't match the 178 | second argument (the constant expression `-170842 + -1 * 92122 + 375877`), then the first element of 179 | the array is removed and pushed to the back of the array. This continues until the expression evaluates to 180 | the correct answer, which then stops the loop. The obfuscated strings (now de-obfuscated) are indexed by 181 | the `x` function, which basically gets the string at *i* where *i* in this case is the given argument 182 | subtracted by `(71 * -137 + 5097 + 4754)`. It's important to note that these expressions change for each 183 | script, and the schematics of the code can also slightly change, since obfuscator.io introduces some randomness. 184 | After we've reversed the strings, we can simply replace all the `CallExpression` nodes with a `StringLiteral` node 185 | by computing the real index (using the offset we mentioned), and simply get the string from the modified array. 186 | 187 | After we've reversed the strings, and removed all related code, we now get this: 188 | ```js 189 | function(a) { 190 | return function() { 191 | return [ 192 | a / Math["log2"](a * Math["LN10"]), 193 | Object["keys"](globalThis["process"] || {}), 194 | globalThis["marker"] 195 | ]; 196 | }(); 197 | }; 198 | ``` 199 | 200 | This is a lot more readable, and we can now see what the script is really doing; it's returning an array of 201 | three elements, the first being a math expression, the second getting the keys of the `process` object (if it exists), 202 | and the third getting the value of the `globalThis.marker` variable. 203 | 204 | After reading this myself, I suspected that the script is not static and was instead randomly generated. 205 | I decided to take another payload from a browser request and decode it, which then showed this code: 206 | ```js 207 | function(a) { 208 | return function() { 209 | return [ 210 | a - Math["log"](a % Math.E), 211 | Object["keys"](globalThis["process"] || {}), 212 | globalThis["marker"] 213 | ]; 214 | }(); 215 | }; 216 | ``` 217 | This confirmed my suspicion that the math expression is random, however the remaining two elements 218 | are static and can be hard-coded. 219 | 220 | After applying the computed_member_expr transformation to transform expressions like `Math["log"]` into 221 | `Math.log` to make deigning visitors easier, I began making the math_expr visitor. Unfortunately SWC 222 | does not have a way of evaluating expressions like the one above, so I designed two functions to handle 223 | these math expressions; one function that gets the value of a field (like `Math.PI` -> `3.141592653589793`), 224 | and one that computes a function call (like `Math.max(1, 2)` -> `2`). You can see the code for this and 225 | how I designed these functions in `src/deobfuscate/math_expr.rs`. After we've replaced all these fields 226 | and calls, we're left with a constant expression like `5 * 7 + 1` where we can simply use `expr_simplifier`, 227 | an SWC visitor, that simplifies expressions into a constant value, and then we have the answer to the challenge. 228 | 229 | From this point on all I had to do was design the token generation logic which can be found in `src/lib.rs`. 230 | 231 | If you run the benchmark using `cargo +nightly bench`, you can see that the average execution time is very low 232 | (for me it was 100.66 µs = 0.10066 ms). Running the same script in node and the browser took around 0.11-0.27 ms, 233 | meaning our solution with parsing AST is the same, if not faster, than evaluating the JavaScript code. 234 | 235 | ## Conclusion 236 | Making bot protection that simply evaluates a math expression and queries the keys of the `process` object 237 | is a very bad idea (especially since math can be platform-dependent, which would lead to incorrect results 238 | server-side). Trying to conceal the token generation request by making its path as an image (`jpeg`) is 239 | completely laughable and does not stop anyone at all. 240 | -------------------------------------------------------------------------------- /benches/generate_token.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion, black_box}; 2 | 3 | fn criterion_benchmark(c: &mut Criterion) { 4 | use vercel_anti_bot::generate_token; 5 | const TEST_DATA: &str = "eyJ0IjoiZXlKaGJHY2lPaUprYVhJaUxDSmxibU1pT2lKQk1qVTJSME5OSW4wLi4yMHA0T3VUcTFDVGRkVXRmLmhxMm4wbkVHOXFwZ2NlbWE2T1Rma1o0d3F2aTJ4SlJqaXd1YVhqTkZIai1ET1JRbDFyUGVaYXFDREdlc19sNXU5NFBTVHpnUHFlN3RNZGZxbUhGemVyRjBpNjJxSzlVV3Z1MDRaaG1iM3R1MjQ1eVJ2aGd1aXdtRmZONEt6VGcuYlRZTXBOZXg1cmhQNnpScFZUVG5NZyIsImMiOiJmdW5jdGlvbihhKXtmdW5jdGlvbiB4KGUscyl7dmFyIHQ9cigpO3JldHVybiB4PWZ1bmN0aW9uKG4saSl7bj1uLSgtODkxNSsyMjczKzMzODcqMik7dmFyIGM9dFtuXTtyZXR1cm4gY30seChlLHMpfShmdW5jdGlvbihlLHMpe2Zvcih2YXIgdD14LG49ZSgpO1tdOyl0cnl7dmFyIGk9cGFyc2VJbnQodCgxNDYpKS8xKigtcGFyc2VJbnQodCgxMzIpKS8yKStwYXJzZUludCh0KDE0MSkpLzMrcGFyc2VJbnQodCgxMzUpKS80KihwYXJzZUludCh0KDEzMykpLzUpKy1wYXJzZUludCh0KDEzOSkpLzYqKHBhcnNlSW50KHQoMTM3KSkvNykrcGFyc2VJbnQodCgxNDcpKS84KihwYXJzZUludCh0KDE0MikpLzkpK3BhcnNlSW50KHQoMTM0KSkvMTArcGFyc2VJbnQodCgxNDApKS8xMSooLXBhcnNlSW50KHQoMTQzKSkvMTIpO2lmKGk9PT1zKWJyZWFrO24ucHVzaChuLnNoaWZ0KCkpfWNhdGNoe24ucHVzaChuLnNoaWZ0KCkpfX0pKHIsLTk4MTA0MystMTMxNDEzKjUrMjI5ODEwMSk7ZnVuY3Rpb24gcigpe3ZhciBlPVtcIm1hcmtlclwiLFwia2V5c1wiLFwiMzEwODk4V21vbnBtXCIsXCI0NDcwNDU2SVFmZVZhXCIsXCI2S1BveGN4XCIsXCI3NzM5NWVUWHJTWFwiLFwiNTE4MjczMFZjcXRyZlwiLFwiMjI4eGVweWxhXCIsXCJsb2cxcFwiLFwiODQ3bXJJbmFHXCIsXCJwcm9jZXNzXCIsXCI2NTM1OG1KTGJVRlwiLFwiNDQzM1ZMS3JzclwiLFwiMjkxMzMxMlNQRlNpTVwiLFwiOVl0RkRXUlwiLFwiNTg4dUJIUU5MXCJdO3JldHVybiByPWZ1bmN0aW9uKCl7cmV0dXJuIGV9LHIoKX1yZXR1cm4gZnVuY3Rpb24oKXt2YXIgZT14O3JldHVyblthK01hdGhbZSgxMzYpXShhL01hdGguUEkpLE9iamVjdFtlKDE0NSldKGdsb2JhbFRoaXNbZSgxMzgpXXx8e30pLGdsb2JhbFRoaXNbZSgxNDQpXV19KCl9IiwiYSI6MC42NzM3ODM4NzE5MjA3MTEyfQ=="; 6 | 7 | c.bench_function("generate_token", |b| b.iter(|| generate_token(black_box(TEST_DATA)))); 8 | } 9 | 10 | criterion_group!(benches, criterion_benchmark); 11 | criterion_main!(benches); 12 | -------------------------------------------------------------------------------- /src/bin/deobfuscator/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use swc::config::Options; 4 | use swc_core::common::errors::{ColorConfig, Handler}; 5 | use swc_core::common::{chain, FileName, Globals, GLOBALS, Mark, SourceMap}; 6 | use swc_core::common::comments::SingleThreadedComments; 7 | use swc_core::ecma::transforms::base::pass::noop; 8 | use swc_ecma_transforms::optimization::simplify::expr_simplifier; 9 | use vercel_anti_bot::{decode_data, deobfuscate}; 10 | use swc_core::ecma::visit::as_folder; 11 | 12 | // Deobfuscates the script from the given data. 13 | // This is mainly intended for debug purposes. 14 | fn main() { 15 | // Get data 16 | let args: Vec = env::args().collect(); 17 | let data = match args.get(1) { 18 | Some(v) => v, 19 | None => { 20 | println!("You must pass in the challenge data."); 21 | println!("This can be obtained from the request to /openai.jpeg. Read README for more info."); 22 | return; 23 | } 24 | }; 25 | // Decode challenge 26 | let challenge = decode_data(data.as_str().trim()) 27 | .expect("failed to decode challenge"); 28 | 29 | let cm = Arc::::default(); 30 | let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone())); 31 | let c = swc::Compiler::new(cm.clone()); 32 | let fm = cm.new_source_file( 33 | FileName::Custom("usage.js".into()), 34 | format!("({})", challenge.code).into() 35 | ); 36 | 37 | let globals = Globals::new(); 38 | GLOBALS.set(&globals, || { 39 | let output = c.process_js_with_custom_pass( 40 | fm, 41 | None, 42 | &handler, 43 | &Options::default(), 44 | SingleThreadedComments::default(), 45 | |_| noop(), 46 | |_| chain!( 47 | expr_simplifier(Mark::new(), Default::default()), 48 | as_folder(deobfuscate::proxy_vars::Visitor::default()), 49 | as_folder(deobfuscate::strings::Visitor), 50 | as_folder(deobfuscate::computed_member_expr::Visitor), 51 | 52 | // You can un-comment this line to evaluate the math expression. 53 | // Since we don't know the input here, we can only use a dummy input, 54 | // unless you specify the input manually. 55 | // This is mainly used for debug purposes. 56 | //as_folder(deobfuscate::math_expr::Visitor::new(0.2)) 57 | ) 58 | ) 59 | .expect("process_js_with_custom_pass failed"); 60 | 61 | println!("{}", output.code); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/bin/token/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use vercel_anti_bot::generate_token; 3 | 4 | // Generates a valid token from the given response from the /openai.jpeg request. 5 | fn main() { 6 | let args: Vec = env::args().collect(); 7 | let data = match args.get(1) { 8 | Some(v) => v, 9 | None => { 10 | println!("You must pass in the challenge data."); 11 | println!("This can be obtained from the request to /openai.jpeg. Read README for more info."); 12 | return; 13 | } 14 | }; 15 | 16 | let token = generate_token(data.as_str()) 17 | .expect("failed to generate token"); 18 | 19 | println!("{}", token); 20 | } 21 | -------------------------------------------------------------------------------- /src/deobfuscate/computed_member_expr.rs: -------------------------------------------------------------------------------- 1 | use swc_core::ecma::ast::{Expr, Ident, Lit, MemberExpr, MemberProp}; 2 | use swc_core::ecma::visit::{VisitMut, VisitMutWith}; 3 | 4 | /// Replaces computed member properties with identifiers. 5 | pub struct Visitor; 6 | 7 | impl VisitMut for Visitor { 8 | fn visit_mut_member_expr(&mut self, member_expr: &mut MemberExpr) { 9 | member_expr.visit_mut_children_with(self); 10 | 11 | if let MemberProp::Computed(property) = &member_expr.prop { 12 | if let Expr::Lit(Lit::Str(s)) = &*property.expr { 13 | member_expr.prop = MemberProp::Ident(Ident::new( 14 | s.value.clone(), 15 | property.span 16 | )); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/deobfuscate/math_expr.rs: -------------------------------------------------------------------------------- 1 | use swc_core::common::Mark; 2 | use swc_core::ecma::ast::{ArrayLit, Callee, Expr, ExprStmt, FnExpr, Id, Lit, MemberProp, Number, Pat, Stmt}; 3 | use swc_core::ecma::visit::{VisitMut, VisitMutWith}; 4 | use swc_ecma_transforms::optimization::simplify::expr_simplifier; 5 | 6 | /// Computes the math expression, which resolves the challenge answer. 7 | pub struct Visitor { 8 | /// The input value from the challenge. 9 | input: f64, 10 | 11 | /// The [Id] of the parent function's input parameter. 12 | input_param: Option, 13 | 14 | /// If we're inside an ArrayLiteral. 15 | is_inside_array_lit: bool, 16 | 17 | /// If we're inside the math expression. 18 | is_inside_correct_expr: bool, 19 | 20 | /// The computed answer from the math expression. 21 | pub answer: Option 22 | } 23 | 24 | impl Visitor { 25 | /// Constructs a new [Visitor] with the input from the challenge. 26 | pub fn new(input: f64) -> Self { 27 | Self { 28 | input, 29 | input_param: None, 30 | is_inside_array_lit: false, 31 | is_inside_correct_expr: false, 32 | answer: None 33 | } 34 | } 35 | } 36 | 37 | /// Gets the value of a `Math` field, like `Math.PI`. 38 | /// If no field with the given name exists, `None` is returned. 39 | fn get_field(field: &str) -> Option { 40 | use std::f64::consts::*; 41 | 42 | match field { 43 | "E" => Some(E), 44 | "LN10" => Some(LN_10), 45 | "LN2" => Some(LN_2), 46 | "LOG10E" => Some(LOG10_E), 47 | "LOG2E" => Some(LOG2_E), 48 | "PI" => Some(PI), 49 | "SQRT1_2" => Some(FRAC_1_SQRT_2), 50 | "SQRT2" => Some(SQRT_2), 51 | _ => None 52 | } 53 | } 54 | 55 | /// Computes a `Math` function call with the given arguments. 56 | /// For example, `Math.max(1, 2)` will return `Some(2)`. 57 | /// This function also handles missing arguments, ie `Math.sign()` 58 | /// returns `Some(f64::NAN)`. 59 | /// 60 | /// If the function with the given name isn't found, `None` is returned. 61 | fn compute_call(fn_name: &str, args: &[f64]) -> Option { 62 | /// Gets the argument at index, defaulting to NaN if it doesn't exist. 63 | macro_rules! get_arg { 64 | ($index:expr) => { 65 | *args.get($index).unwrap_or(&f64::NAN) 66 | } 67 | } 68 | 69 | // Functions from: 70 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math 71 | match fn_name { 72 | "abs" => Some(get_arg!(0).abs()), 73 | "acos" => Some(get_arg!(0).acos()), 74 | "acosh" => Some(get_arg!(0).acosh()), 75 | "asin" => Some(get_arg!(0).asin()), 76 | "asinh" => Some(get_arg!(0).asinh()), 77 | "atan" => Some(get_arg!(0).atan()), 78 | "atan2" => Some(get_arg!(0).atan2(get_arg!(1))), 79 | "atanh" => Some(get_arg!(0).atanh()), 80 | "cbrt" => Some(get_arg!(0).cbrt()), 81 | "ceil" => Some(get_arg!(0).ceil()), 82 | "clz32" => Some((get_arg!(0) as i32).leading_zeros() as f64), 83 | "cos" => Some(get_arg!(0).cos()), 84 | "cosh" => Some(get_arg!(0).cosh()), 85 | "exp" => Some(get_arg!(0).exp()), 86 | "expm1" => Some(get_arg!(0).exp_m1()), 87 | "floor" => Some(get_arg!(0).floor()), 88 | "fround" => Some(get_arg!(0) as f32 as f64), 89 | "hypot" => Some(get_arg!(0).hypot(get_arg!(1))), 90 | "imul" => Some((get_arg!(0) as i32 * get_arg!(1) as i32) as f64), 91 | "log" => Some(get_arg!(0).ln()), 92 | "log10" => Some(get_arg!(0).log10()), 93 | "log1p" => Some(get_arg!(0).ln_1p()), 94 | "log2" => Some(get_arg!(0).log2()), 95 | "max" => Some(get_arg!(0).max(get_arg!(1))), 96 | "min" => Some(get_arg!(0).min(get_arg!(1))), 97 | "pow" => Some(get_arg!(0).powf(get_arg!(1))), 98 | // "random" would go here, but it wouldn't make sense for them to use it 99 | "round" => Some(get_arg!(0).round()), 100 | "sign" => Some({ 101 | let v = get_arg!(0); 102 | if v == f64::NAN { 103 | f64::NAN 104 | } else if v > 0.0 { 105 | 1.0 106 | } else if v < 0.0 { 107 | -1.0 108 | } else { 109 | 0.0 110 | } 111 | }), 112 | "sin" => Some(get_arg!(0).sin()), 113 | "sinh" => Some(get_arg!(0).sinh()), 114 | "sqrt" => Some(get_arg!(0).sqrt()), 115 | "tan" => Some(get_arg!(0).tan()), 116 | "tanh" => Some(get_arg!(0).tanh()), 117 | "trunc" => Some(get_arg!(0).trunc()), 118 | _ => None 119 | } 120 | } 121 | 122 | impl VisitMut for Visitor { 123 | fn visit_mut_fn_expr(&mut self, fn_expr: &mut FnExpr) { 124 | if !self.input_param.is_some() { 125 | if let Some(param) = fn_expr.function.params.get(0) { 126 | if let Pat::Ident(input_param) = ¶m.pat { 127 | self.input_param = Some(input_param.to_id()); 128 | } 129 | } 130 | } 131 | 132 | fn_expr.visit_mut_children_with(self); 133 | } 134 | 135 | fn visit_mut_array_lit(&mut self, array_lit: &mut ArrayLit) { 136 | if let Some(Some(_)) = array_lit.elems.get(0) { 137 | let old_is_inside_array_lit = self.is_inside_array_lit; 138 | self.is_inside_array_lit = true; 139 | array_lit.visit_mut_children_with(self); 140 | self.is_inside_array_lit = old_is_inside_array_lit; 141 | } else { 142 | array_lit.visit_mut_children_with(self); 143 | } 144 | } 145 | 146 | fn visit_mut_expr(&mut self, expr: &mut Expr) { 147 | if self.is_inside_array_lit { 148 | let old_is_inside_correct_expr = self.is_inside_correct_expr; 149 | self.is_inside_correct_expr = true; 150 | expr.visit_mut_children_with(self); 151 | 152 | if self.answer.is_none() && self.is_inside_correct_expr && !old_is_inside_correct_expr { 153 | let mut stmt = Stmt::Expr(ExprStmt { 154 | span: Default::default(), 155 | expr: Box::new(expr.clone()) 156 | }); 157 | let mut simplifier = expr_simplifier( 158 | Mark::new(), 159 | Default::default() 160 | ); 161 | stmt.visit_mut_with(&mut simplifier); 162 | // Try to get literal value 163 | if let Stmt::Expr(expr_stmt) = &stmt { 164 | if let Expr::Lit(Lit::Num(number)) = &*expr_stmt.expr { 165 | self.answer = Some(number.value); 166 | *expr = Expr::Lit(Lit::Num(Number::from(number.value))); 167 | } 168 | } 169 | } 170 | 171 | self.is_inside_correct_expr = old_is_inside_correct_expr; 172 | } else { 173 | expr.visit_mut_children_with(self); 174 | } 175 | 176 | if !self.is_inside_correct_expr { 177 | return; 178 | } 179 | 180 | if let Expr::Ident(id) = expr { 181 | // Handle input parameter 182 | if let Some(input_param) = &self.input_param { 183 | // Ignore identifiers that aren't the input parameter 184 | if id.to_id() != *input_param { 185 | return; 186 | } 187 | // Replace identifier with input value 188 | *expr = Expr::Lit(Lit::Num(Number::from(self.input))); 189 | } 190 | } else if let Expr::Member(member_expr) = expr { 191 | // Handle expressions like Math.PI 192 | if let Expr::Ident(obj) = &*member_expr.obj { 193 | // Ignore non-Math objects 194 | if obj.sym.to_string().as_str() != "Math" { 195 | return; 196 | } 197 | // Get property as &str 198 | let field_name = if let MemberProp::Ident(id) = &member_expr.prop { 199 | id.sym.clone().to_string() 200 | } else { 201 | return; 202 | }; 203 | // Replace field with value 204 | if let Some(value) = get_field(field_name.as_str()) { 205 | *expr = Expr::Lit(Lit::Num(Number::from(value))); 206 | } 207 | } 208 | } else if let Expr::Call(call_expr) = expr { 209 | // Handle calls like Math.max(1, 2) 210 | 211 | // Get callee as MemberExpression 212 | let member_expr = if let Callee::Expr(callee) = &call_expr.callee { 213 | if let Expr::Member(member_expr) = &**callee { 214 | member_expr 215 | } else { 216 | return; 217 | } 218 | } else { 219 | return; 220 | }; 221 | 222 | // Get property as Identifier 223 | let obj = if let Expr::Ident(id) = &*member_expr.obj { 224 | id 225 | } else { 226 | return; 227 | }; 228 | 229 | // Ignore non-Math objects 230 | if obj.sym.to_string() != "Math" { 231 | return; 232 | } 233 | 234 | // Get function name as &str 235 | let fn_name = if let MemberProp::Ident(property) = &member_expr.prop { 236 | property.sym.to_string() 237 | } else { 238 | return; 239 | }; 240 | 241 | // Convert arguments to f64 242 | let args: Vec = call_expr.args 243 | .clone() 244 | .into_iter() 245 | .map(|arg| { 246 | if let Expr::Lit(Lit::Num(number)) = &*arg.expr { 247 | number.value 248 | } else { 249 | // Try evaluate the expression 250 | let mut stmt = Stmt::Expr(ExprStmt { 251 | span: Default::default(), 252 | expr: Box::new(*arg.expr.clone()) 253 | }); 254 | let mut simplifier = expr_simplifier( 255 | Mark::new(), 256 | Default::default() 257 | ); 258 | stmt.visit_mut_with(&mut simplifier); 259 | // Try to get literal value 260 | if let Stmt::Expr(expr_stmt) = &stmt { 261 | if let Expr::Lit(Lit::Num(number)) = &*expr_stmt.expr { 262 | number.value 263 | } else { 264 | f64::NAN 265 | } 266 | } else { 267 | f64::NAN 268 | } 269 | } 270 | }) 271 | .collect::>(); 272 | // Compute result 273 | if let Some(result) = compute_call(fn_name.as_str(), &args) { 274 | *expr = Expr::Lit(Lit::Num(Number::from(result))); 275 | } 276 | } 277 | } 278 | } -------------------------------------------------------------------------------- /src/deobfuscate/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod strings; 2 | pub mod proxy_vars; 3 | pub mod math_expr; 4 | pub mod computed_member_expr; 5 | -------------------------------------------------------------------------------- /src/deobfuscate/proxy_vars.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::default::Default; 3 | use swc_core::common::SyntaxContext; 4 | use swc_core::common::util::take::Take; 5 | use swc_core::ecma::visit::{VisitMut, VisitMutWith}; 6 | use swc_core::ecma::ast::{Decl, Expr, FnDecl, Id, Ident, ModuleItem, Pat, Program, Stmt, VarDeclarator}; 7 | use swc_core::ecma::atoms::JsWord; 8 | 9 | /// Replaces proxy variables with references to the real variable. 10 | /// 11 | /// Example: 12 | /// ```js 13 | /// function doStuff() {} 14 | /// function helloWorld() { 15 | /// var a = doStuff; 16 | /// return a(); 17 | /// } 18 | /// ``` 19 | /// 20 | /// is replaced with: 21 | /// 22 | /// ```js 23 | /// function doStuff() {} 24 | /// function helloWorld() { 25 | /// return doStuff(); 26 | /// } 27 | /// ``` 28 | #[derive(Default)] 29 | pub struct Visitor { 30 | /// Function identifiers obtained from [FunctionVisitor]. 31 | /// Used to check if an [Id] is a FunctionDeclaration. 32 | functions: HashSet, 33 | 34 | /// A map of [JsWord]'s along with the lowest [SyntaxContext]. 35 | /// This is used for checking if an identifier's name exists 36 | /// in an upper scope to avoid name collisions. 37 | identifiers: HashMap, 38 | 39 | /// Variable replacements. The key is the variable [Id], 40 | /// and the value is the [JsWord] of the FunctionDeclaration 41 | /// being called. 42 | replacements: HashMap, 43 | 44 | /// Counter for generating unique names. 45 | new_name_counter: usize, 46 | 47 | /// Function identifier replacements. 48 | /// The key is the current function name, and the value is the new name. 49 | function_replacements: HashMap 50 | } 51 | 52 | impl VisitMut for Visitor { 53 | fn visit_mut_program(&mut self, program: &mut Program) { 54 | /* 55 | Because of cases like these: 56 | 57 | var r = c; 58 | function c() {} 59 | 60 | We have to run a separate visitor first to get all the functions, 61 | instead of just traversing down. 62 | */ 63 | let mut fn_visitor = FunctionVisitor::default(); 64 | program.visit_mut_children_with(&mut fn_visitor); 65 | self.functions = fn_visitor.functions; 66 | self.identifiers = fn_visitor.identifiers; 67 | 68 | // Replace identifiers and remove variables 69 | program.visit_mut_children_with(self); 70 | 71 | // Rename functions that were marked for renaming due to collisions 72 | if !self.function_replacements.is_empty() { 73 | let mut renamer = RenameFunctionVisitor { 74 | replacements: self.function_replacements.clone() 75 | }; 76 | program.visit_mut_children_with(&mut renamer); 77 | } 78 | } 79 | 80 | fn visit_mut_var_declarator(&mut self, declarator: &mut VarDeclarator) { 81 | declarator.visit_mut_children_with(self); 82 | 83 | // Is the declarator's name an identifier? 84 | if let Pat::Ident(var_id) = &declarator.name { 85 | // Is init an identifier? 86 | if let Some(expr) = &declarator.init { 87 | if let Expr::Ident(fn_id) = &**expr { 88 | // Is the identifier a FunctionDeclaration? 89 | if self.functions.contains(&fn_id.to_id()) { 90 | /* 91 | Get the replacement JsWord. 92 | 93 | The replacement, in most cases, is `id.sym`, but there is 94 | a special case we MUST handle to avoid breaking code. 95 | Observe the following code: 96 | 97 | var r = c; 98 | function c() {} 99 | function doStuff(c) { 100 | r(); 101 | } 102 | 103 | This looks normal at first, but the problem here is if we replace 104 | r() with c(), then we'll be calling the parameter passed into the 105 | function instead of the "c" function in the upper scope. 106 | To avoid this, we have a map of JsWord -> SyntaxContext, each 107 | value being the lowest SyntaxContext. We can check if a variable 108 | of the same name exists in the upper scope by comparing the lowest 109 | context (lowest_ctx) with the syntax context of the real function 110 | being called (fn_id.span.ctxt). 111 | */ 112 | let replacement_sym = if let Some(highest_ctx) = self.identifiers.get(&fn_id.sym) { 113 | if fn_id.span.ctxt < *highest_ctx { 114 | // Generate new name 115 | let mut new_name = String::from("proxyFn"); 116 | new_name.push_str(self.new_name_counter.to_string().as_str()); 117 | self.new_name_counter += 1; 118 | 119 | // Set function replacement 120 | let replacement = JsWord::from(new_name); 121 | self.function_replacements.insert(fn_id.to_id(), replacement.clone()); 122 | 123 | replacement 124 | } else { 125 | fn_id.sym.clone() 126 | } 127 | } else { 128 | fn_id.sym.clone() 129 | }; 130 | 131 | // Add a replacement 132 | self.replacements.insert(var_id.to_id(), Ident { 133 | span: fn_id.span.clone(), 134 | sym: replacement_sym, 135 | optional: false, 136 | }); 137 | // Mark declarator for deletion 138 | declarator.name.take(); 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | // Replace identifiers with their replacement. 146 | fn visit_mut_ident(&mut self, ident: &mut Ident) { 147 | if let Some(new_id) = self.replacements.get(&ident.to_id()) { 148 | *ident = new_id.clone(); 149 | } 150 | } 151 | 152 | // All code below this line is for deleting marked nodes. 153 | 154 | // Remove marked declarators 155 | fn visit_mut_var_declarators(&mut self, declarators: &mut Vec) { 156 | declarators.visit_mut_children_with(self); 157 | 158 | declarators.retain(|node| !node.name.is_invalid()); 159 | } 160 | 161 | // Remove empty VariableDeclaration nodes 162 | fn visit_mut_stmt(&mut self, stmt: &mut Stmt) { 163 | stmt.visit_mut_children_with(self); 164 | 165 | if let Stmt::Decl(Decl::Var(var)) = stmt { 166 | if var.decls.is_empty() { 167 | stmt.take(); 168 | } 169 | } 170 | } 171 | 172 | // Remove top-level statements. 173 | fn visit_mut_module_items(&mut self, stmts: &mut Vec) { 174 | stmts.visit_mut_children_with(self); 175 | 176 | stmts.retain(|stmt| !matches!(stmt, ModuleItem::Stmt(Stmt::Empty(..)))); 177 | } 178 | } 179 | 180 | #[derive(Default)] 181 | struct FunctionVisitor { 182 | /// Function [Id]'s. 183 | functions: HashSet, 184 | 185 | /// Identifier names and their highest (deepest) scopes. 186 | /// Used for collision checking. 187 | identifiers: HashMap, 188 | } 189 | 190 | impl VisitMut for FunctionVisitor { 191 | // Store FunctionDeclaration's identifiers so we can check in 192 | // visit_mut_var_declarator if an Id is a function 193 | fn visit_mut_fn_decl(&mut self, fn_decl: &mut FnDecl) { 194 | self.functions.insert(fn_decl.ident.to_id()); 195 | fn_decl.visit_mut_children_with(self); 196 | } 197 | 198 | // Store the highest (deepest) scope index for each identifier. 199 | fn visit_mut_ident(&mut self, ident: &mut Ident) { 200 | if let Some(v) = self.identifiers.get(&ident.sym) { 201 | if ident.span.ctxt > *v { 202 | self.identifiers.insert(ident.sym.clone(), ident.span.ctxt); 203 | } 204 | } else { 205 | self.identifiers.insert(ident.sym.clone(), ident.span.ctxt); 206 | } 207 | } 208 | } 209 | 210 | /// Renames functions. 211 | struct RenameFunctionVisitor { 212 | replacements: HashMap 213 | } 214 | 215 | impl VisitMut for RenameFunctionVisitor { 216 | fn visit_mut_fn_decl(&mut self, fn_decl: &mut FnDecl) { 217 | // Remove id from replacements and set the function name 218 | if let Some(new_name) = self.replacements.remove(&fn_decl.ident.to_id()) { 219 | fn_decl.ident.sym = new_name; 220 | 221 | // Visit children if we have remaining functions to replace 222 | if !self.replacements.is_empty() { 223 | fn_decl.visit_mut_children_with(self); 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/deobfuscate/strings.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::default::Default; 3 | use std::str::FromStr; 4 | use swc_core::common::{Mark, Span}; 5 | use swc_core::common::errors::HANDLER; 6 | use swc_core::common::util::take::Take; 7 | use swc_core::ecma::visit::{VisitMut, VisitMutWith}; 8 | use swc_core::ecma::ast::{ArrayLit, AssignExpr, BinaryOp, BinExpr, Callee, CallExpr, Decl, Expr, ExprStmt, FnDecl, Id, Ident, Lit, ModuleItem, Number, op, Program, Stmt, Str, VarDeclarator}; 9 | use swc_core::ecma::atoms::JsWord; 10 | use swc_ecma_transforms::optimization::simplify::expr_simplifier; 11 | 12 | /// Replaces obfuscated strings with the real strings. 13 | pub struct Visitor; 14 | 15 | impl VisitMut for Visitor { 16 | fn visit_mut_program(&mut self, program: &mut Program) { 17 | /// Matches `$e` to `Some`. If `None`, `$err` is emitted and the function returns. 18 | macro_rules! try_unwrap { 19 | ($e:expr, $err:expr) => { 20 | match $e { 21 | Some(v) => v, 22 | None => { 23 | HANDLER.with(|handler| { 24 | handler.err($err); 25 | }); 26 | return; 27 | } 28 | } 29 | }; 30 | } 31 | 32 | // Find function that returns the obfuscated strings, along with the 33 | // initial obfuscated strings 34 | let mut obf_strings = FindObfuscatedStringsVisitor::default(); 35 | program.visit_mut_children_with(&mut obf_strings); 36 | // The function Id that returns the obfuscated strings 37 | let get_obf_strings_fn_id = try_unwrap!( 38 | obf_strings.fn_id, 39 | "Couldn't find obfuscated strings function" 40 | ); 41 | // The obfuscated strings 42 | let mut obfuscated_strings = try_unwrap!( 43 | obf_strings.obfuscated_strings, 44 | "Couldn't find obfuscated strings" 45 | ); 46 | 47 | // Find the function that indexes the obfuscated strings array 48 | let mut index_fn_visitor = FindIndexFunctionVisitor::new( 49 | get_obf_strings_fn_id.clone() 50 | ); 51 | program.visit_mut_children_with(&mut index_fn_visitor); 52 | // Offset and operand 53 | let index_data = try_unwrap!(index_fn_visitor.index, "Index data not found"); 54 | // Index function id 55 | let index_fn_id = try_unwrap!(index_fn_visitor.fn_id, "Index function not found"); 56 | 57 | // Find the expression that is used to calculate the answer 58 | // used for array modification 59 | let mut expr_visitor = FindObfExpression::new( 60 | get_obf_strings_fn_id.clone() 61 | ); 62 | program.visit_mut_children_with(&mut expr_visitor); 63 | // The original expression 64 | let original_expr = try_unwrap!( 65 | expr_visitor.expr, 66 | "Array compute function not found" 67 | ); 68 | // The answer to compare against 69 | let answer = try_unwrap!(expr_visitor.answer, "Answer not found"); 70 | // The original obfuscated strings. We use this for failure checking 71 | // so we don't loop infinitely if we some how don't compute the 72 | // strings correctly. 73 | let original_obfuscated_strings = obfuscated_strings.clone(); 74 | // Modify obfuscated_strings until we get the correct answer 75 | loop { 76 | // Convert parseInt calls to literal values 77 | let mut expr = original_expr.clone(); 78 | let mut expr_evaluator = ExprVisitor::new( 79 | index_data.clone(), 80 | &obfuscated_strings 81 | ); 82 | expr.visit_mut_children_with(&mut expr_evaluator); 83 | // Evaluate the expression using expr_simplifier transform 84 | let mut stmt = Stmt::Expr(ExprStmt { 85 | span: Default::default(), 86 | expr: Box::new(Expr::Bin(expr)), 87 | }); 88 | let mut simplifier = expr_simplifier( 89 | Mark::new(), 90 | Default::default() 91 | ); 92 | stmt.visit_mut_with(&mut simplifier); 93 | // Try get literal value 94 | if let Stmt::Expr(expr) = &stmt { 95 | if let Expr::Lit(lit) = &*expr.expr { 96 | if let Lit::Num(n) = &lit { 97 | if n.value == answer { 98 | // Answer matches, stop looping 99 | break; 100 | } 101 | } 102 | } 103 | } 104 | // Got NaN, or the wrong answer. Continue modifying the deque. 105 | let first = try_unwrap!( 106 | obfuscated_strings.pop_front(), 107 | "Obfuscated strings are empty" 108 | ); 109 | obfuscated_strings.push_back(first); 110 | // If the deque becomes original_obfuscated_strings (what we started with) 111 | // then this means we failed to find the correct answer. 112 | // This shouldn't happen, but is here for safety purposes so we don't 113 | // loop forever. 114 | // In this case, we emit an error and return. 115 | if obfuscated_strings == original_obfuscated_strings { 116 | HANDLER.with(|handler| { 117 | handler.err("Failed to compute obfuscated strings"); 118 | }); 119 | return; 120 | } 121 | } 122 | 123 | // Remove call expressions and related code 124 | let mut cleanup_visitor = CleanupVisitor::new(index_fn_id, index_data, &obfuscated_strings); 125 | program.visit_mut_children_with(&mut cleanup_visitor); 126 | } 127 | } 128 | 129 | /// Finds the function that returns the obfuscated strings 130 | /// along with the obfuscated strings. 131 | #[derive(Default)] 132 | struct FindObfuscatedStringsVisitor { 133 | /// If we're currently inside a FunctionDeclaration. 134 | /// This is only used internally. 135 | is_inside_fn: bool, 136 | 137 | /// The [Id] of the function that returns the obfuscated strings. 138 | fn_id: Option, 139 | 140 | /// The obfuscated strings. 141 | obfuscated_strings: Option> 142 | } 143 | 144 | impl VisitMut for FindObfuscatedStringsVisitor { 145 | fn visit_mut_fn_decl(&mut self, fn_decl: &mut FnDecl) { 146 | // Stop visiting if we already found the function 147 | if self.fn_id.is_some() { 148 | return; 149 | } 150 | 151 | // Visit children of this function 152 | let old_is_inside_fn = self.is_inside_fn; 153 | self.is_inside_fn = true; 154 | fn_decl.visit_mut_children_with(self); 155 | self.is_inside_fn = old_is_inside_fn; 156 | 157 | // If the obfuscated strings were found then set fn_id 158 | if self.obfuscated_strings.is_some() { 159 | self.fn_id = Some(fn_decl.ident.to_id()); 160 | fn_decl.take(); 161 | } 162 | } 163 | 164 | fn visit_mut_array_lit(&mut self, array: &mut ArrayLit) { 165 | if !self.is_inside_fn { 166 | return; 167 | } 168 | 169 | let mut obfuscated_strings = VecDeque::new(); 170 | for element in &array.elems { 171 | if let Some(v) = element { 172 | // Spread operator shouldn't be present on any elements 173 | if v.spread.is_some() { 174 | return; 175 | } 176 | 177 | if let Expr::Lit(Lit::Str(s)) = &*v.expr { 178 | obfuscated_strings.push_back(s.value.clone()); 179 | } else { 180 | // All elements should be string literals 181 | return; 182 | } 183 | } else { 184 | // All elements should be Some 185 | return; 186 | } 187 | } 188 | 189 | self.obfuscated_strings = Some(obfuscated_strings); 190 | } 191 | } 192 | 193 | /// Finds the function that indexes the obfuscated strings. 194 | struct FindIndexFunctionVisitor { 195 | /// The [Id] of the function that returns the obfuscated strings, 196 | /// obtained from [FindObfuscatedStringsVisitor]. 197 | get_obfuscated_strings_fn: Id, 198 | 199 | /// If the visitor is currently inside a function declaration. 200 | /// This is only used internally. 201 | is_inside_fn_decl: bool, 202 | 203 | /// If the visitor is currently inside the index function. 204 | /// This is only used internally. 205 | is_inside_correct_fn: bool, 206 | 207 | /// The index data, containing the offset and the binary operator. 208 | index: Option, 209 | 210 | /// The [Id] of the function that indexes strings. 211 | fn_id: Option 212 | } 213 | 214 | #[derive(Copy, Clone)] 215 | struct Index { 216 | /// The operand to use to get the real index. 217 | offset: f64, 218 | 219 | /// The operator to use with offset. 220 | op: BinaryOp 221 | } 222 | 223 | /// Computes a fake index into the real index. 224 | fn get_index(index: u32, offset: u32, op: BinaryOp) -> Option { 225 | match op { 226 | BinaryOp::LShift => Some(index << offset), 227 | BinaryOp::RShift => Some(index >> offset), 228 | BinaryOp::ZeroFillRShift => Some(index >> offset), 229 | BinaryOp::Add => Some(index + offset), 230 | BinaryOp::Sub => Some(index - offset), 231 | BinaryOp::Mul => Some(index * offset), 232 | BinaryOp::Div => Some(index / offset), 233 | BinaryOp::Mod => Some(index % offset), 234 | BinaryOp::BitOr => Some(index | offset), 235 | BinaryOp::BitXor => Some(index ^ offset), 236 | BinaryOp::BitAnd => Some(index & offset), 237 | BinaryOp::Exp => Some(index.pow(offset)), 238 | _ => None 239 | } 240 | } 241 | 242 | impl FindIndexFunctionVisitor { 243 | /// Creates a new [FindIndexFunctionVisitor] with the obfuscated strings function identifier 244 | /// obtained from [FindObfuscatedStringsVisitor]. 245 | fn new(get_obfuscated_strings_fn: Id) -> Self { 246 | Self { 247 | get_obfuscated_strings_fn, 248 | is_inside_fn_decl: false, 249 | is_inside_correct_fn: false, 250 | index: None, 251 | fn_id: None 252 | } 253 | } 254 | } 255 | 256 | impl VisitMut for FindIndexFunctionVisitor { 257 | fn visit_mut_fn_decl(&mut self, fn_decl: &mut FnDecl) { 258 | let old_is_inside_fn_decl = self.is_inside_fn_decl; 259 | self.is_inside_fn_decl = true; 260 | fn_decl.visit_mut_children_with(self); 261 | self.is_inside_fn_decl = old_is_inside_fn_decl; 262 | 263 | // Set fn_id. 264 | // 265 | // We add an extra check at the end as the function that 266 | // returns the obfuscated strings calls itself. 267 | if self.is_inside_correct_fn && self.fn_id.is_none() && self.get_obfuscated_strings_fn != fn_decl.ident.to_id() { 268 | self.fn_id = Some(fn_decl.ident.to_id()); 269 | fn_decl.take(); 270 | } 271 | 272 | // Reset state 273 | if self.is_inside_fn_decl && self.is_inside_correct_fn { 274 | self.is_inside_correct_fn = false; 275 | } 276 | } 277 | 278 | fn visit_mut_call_expr(&mut self, call: &mut CallExpr) { 279 | // Ignore if we're in the correct function 280 | if self.is_inside_correct_fn { 281 | return; 282 | } 283 | 284 | if let Callee::Expr(expr) = &call.callee { 285 | if let Expr::Ident(id) = expr.as_ref() { 286 | if id.to_id() == self.get_obfuscated_strings_fn { 287 | self.is_inside_correct_fn = true; 288 | } 289 | } 290 | } 291 | } 292 | 293 | fn visit_mut_assign_expr(&mut self, assignment: &mut AssignExpr) { 294 | assignment.visit_mut_children_with(self); 295 | 296 | // Skip if we're not inside the correct function, or if we 297 | // already got the expression 298 | if !self.is_inside_correct_fn || self.index.is_some() { 299 | return; 300 | } 301 | 302 | // Skip if the assignment operator isn't "=", 303 | // or if the right side of the assignment isn't a BinaryExpression 304 | if assignment.op != op!("=") { 305 | return; 306 | } 307 | 308 | // Is the right side of the assignment a binary expression? 309 | if let Expr::Bin(bin) = &*assignment.right { 310 | // Is the right side of the binary expression a numeric literal? 311 | if let Expr::Lit(Lit::Num(n)) = &*bin.right { 312 | self.index = Some(Index { 313 | offset: n.value, 314 | op: bin.op 315 | }); 316 | } 317 | } 318 | } 319 | } 320 | 321 | /// Finds the answer and expression. 322 | struct FindObfExpression { 323 | /// The function that returns the obfuscated strings. 324 | get_obfuscated_strings_fn: Id, 325 | 326 | /// If we're inside the correct CallExpression. 327 | /// This is only used internally. 328 | is_inside_correct_call_expr: bool, 329 | 330 | /// The expected answer for the computation. 331 | answer: Option, 332 | 333 | /// The expression used to compute the potential answer. 334 | expr: Option 335 | } 336 | 337 | impl FindObfExpression { 338 | fn new(get_obfuscated_strings_fn: Id) -> Self { 339 | Self { 340 | get_obfuscated_strings_fn, 341 | is_inside_correct_call_expr: false, 342 | answer: None, 343 | expr: None 344 | } 345 | } 346 | } 347 | 348 | impl VisitMut for FindObfExpression { 349 | fn visit_mut_call_expr(&mut self, call_expr: &mut CallExpr) { 350 | // Is the first argument the function that returns the obfuscated strings? 351 | if let Some(fn_id_arg) = call_expr.args.get(0) { 352 | if let Expr::Ident(id) = &*fn_id_arg.expr { 353 | if id.to_id() != self.get_obfuscated_strings_fn { 354 | return; 355 | } 356 | } 357 | } 358 | 359 | // Value to use to check if the obfuscation reverse process has completed 360 | let answer = if let Some(answer_arg) = call_expr.args.get(1) { 361 | if let Expr::Lit(Lit::Num(n)) = &*answer_arg.expr { 362 | n.value 363 | } else { 364 | return; 365 | } 366 | } else { 367 | return; 368 | }; 369 | 370 | self.answer = Some(answer); 371 | call_expr.span.take(); 372 | 373 | // Set state 374 | let old_is_inside_correct_call_expr = self.is_inside_correct_call_expr; 375 | self.is_inside_correct_call_expr = true; 376 | call_expr.visit_mut_children_with(self); 377 | self.is_inside_correct_call_expr = old_is_inside_correct_call_expr; 378 | } 379 | 380 | fn visit_mut_var_declarator(&mut self, declarator: &mut VarDeclarator) { 381 | if !self.is_inside_correct_call_expr { 382 | return; 383 | } 384 | declarator.visit_mut_children_with(self); 385 | 386 | if let Some(expr) = &declarator.init { 387 | if let Expr::Bin(bin) = &**expr { 388 | self.expr = Some(bin.clone()); 389 | } 390 | } 391 | } 392 | } 393 | 394 | /// Replaces the `parseInt` calls in the expression and the calls to the index function. 395 | struct ExprVisitor<'strings> { 396 | /// The index data. 397 | index_data: Index, 398 | 399 | /// The obfuscated strings. 400 | obfuscated_strings: &'strings VecDeque 401 | } 402 | 403 | impl<'strings> ExprVisitor<'strings> { 404 | fn new(index_data: Index, obfuscated_strings: &'strings VecDeque) -> Self { 405 | Self { 406 | index_data, 407 | obfuscated_strings 408 | } 409 | } 410 | } 411 | 412 | /// Parses a string as an integer, ignoring non-numeric characters. 413 | /// This is the equivalent to `parseInt` in JavaScript. 414 | fn atoi(input: &str) -> Result::Err> { 415 | let i = input 416 | .find(|c: char| !c.is_numeric()) 417 | .unwrap_or_else(|| input.len()); 418 | 419 | input[..i].parse::() 420 | } 421 | 422 | impl<'strings> VisitMut for ExprVisitor<'strings> { 423 | fn visit_mut_expr(&mut self, expr: &mut Expr) { 424 | expr.visit_mut_children_with(self); 425 | 426 | if let Expr::Call(call) = expr { 427 | // Check if this is a call to parseInt 428 | if let Callee::Expr(callee_expr) = &call.callee { 429 | if let Expr::Ident(id) = &**callee_expr { 430 | if id.sym.to_string() != "parseInt" { 431 | return; 432 | } 433 | } else { 434 | return; 435 | } 436 | } else { 437 | return; 438 | } 439 | 440 | // Get call to get_index 441 | if let Some(argument) = call.args.get(0) { 442 | if let Expr::Call(get_index_call) = &*argument.expr { 443 | // Is a CallExpression 444 | if let Some(offset_arg) = get_index_call.args.get(0) { 445 | if let Expr::Lit(Lit::Num(offset)) = &*offset_arg.expr { 446 | // NaN as a node 447 | let nan = Expr::Ident( 448 | Ident::new( 449 | JsWord::from("NaN"), 450 | Span::default() 451 | ) 452 | ); 453 | 454 | // Replace node 455 | *expr = match get_index(offset.value as u32, self.index_data.offset as u32, self.index_data.op) { 456 | Some(index) => { 457 | match self.obfuscated_strings.get(index as usize) { 458 | Some(s) => match atoi::(s.to_string().as_str()) { 459 | Ok(n) => Expr::Lit(Lit::Num(Number::from(n))), 460 | Err(_) => nan 461 | }, 462 | None => nan 463 | } 464 | }, 465 | None => nan 466 | }; 467 | } 468 | } 469 | } 470 | } 471 | } 472 | } 473 | } 474 | 475 | /// Replaces calls to the index function with the plaintext strings and 476 | /// removes related code to string obfuscation. 477 | struct CleanupVisitor<'strings> { 478 | /// The [Id] of the index function. 479 | index_fn_id: Id, 480 | 481 | /// The index data. 482 | index_data: Index, 483 | 484 | /// The deobfuscated strings. 485 | plaintext_strings: &'strings VecDeque 486 | } 487 | 488 | impl<'strings> CleanupVisitor<'strings> { 489 | fn new(index_fn_id: Id, index_data: Index, plaintext_strings: &'strings VecDeque) -> Self { 490 | Self { 491 | index_fn_id, 492 | index_data, 493 | plaintext_strings 494 | } 495 | } 496 | } 497 | 498 | impl<'strings> VisitMut for CleanupVisitor<'strings> { 499 | fn visit_mut_stmt(&mut self, s: &mut Stmt) { 500 | s.visit_mut_children_with(self); 501 | 502 | if let Stmt::Expr(expr_stmt) = s { 503 | if matches!(&*expr_stmt.expr, Expr::Invalid(..)) { 504 | s.take(); 505 | } 506 | } else if let Stmt::Decl(Decl::Fn(fn_decl)) = s { 507 | if fn_decl.ident.is_dummy() { 508 | // Remove FunctionDeclaration's that return obfuscated strings and index 509 | // the obfuscated strings 510 | s.take(); 511 | } 512 | } 513 | } 514 | 515 | // Remove empty statements 516 | fn visit_mut_stmts(&mut self, stmts: &mut Vec) { 517 | stmts.visit_mut_children_with(self); 518 | 519 | stmts.retain(|s| !matches!(s, Stmt::Empty(..))); 520 | } 521 | 522 | // Remove empty ModuleItem's 523 | fn visit_mut_module_items(&mut self, stmts: &mut Vec) { 524 | stmts.visit_mut_children_with(self); 525 | stmts.retain(|stmt| !matches!(stmt, ModuleItem::Stmt(Stmt::Empty(..)))); 526 | } 527 | 528 | // Remove invalid expressions 529 | fn visit_mut_exprs(&mut self, exprs: &mut Vec>) { 530 | exprs.visit_mut_children_with(self); 531 | exprs.retain(|expr| !matches!(**expr, Expr::Invalid(..))); 532 | } 533 | 534 | fn visit_mut_expr(&mut self, expr: &mut Expr) { 535 | expr.visit_mut_children_with(self); 536 | 537 | if let Expr::Call(call_expr) = expr { 538 | // Remove marked CallExpression's 539 | if call_expr.span.is_dummy() { 540 | expr.take(); 541 | return; 542 | } 543 | 544 | // Replace calls to index function with the plaintext string 545 | if let Callee::Expr(callee_expr) = &call_expr.callee { 546 | if let Expr::Ident(id) = &**callee_expr { 547 | // Does the callee match the index function identifier? 548 | if id.to_id() != self.index_fn_id { 549 | return; 550 | } 551 | 552 | // Get index value 553 | let index = if let Some(arg) = call_expr.args.get(0) { 554 | if let Expr::Lit(Lit::Num(n)) = &*arg.expr { 555 | n.value 556 | } else { 557 | return; 558 | } 559 | } else { 560 | return; 561 | }; 562 | 563 | // Replace call with literal value 564 | if let Some(real_index) = get_index(index as u32, self.index_data.offset as u32, self.index_data.op) { 565 | if let Some(plaintext) = self.plaintext_strings.get(real_index as usize) { 566 | *expr = Expr::Lit(Lit::Str(Str::from(plaintext.clone()))); 567 | } 568 | } 569 | } 570 | } 571 | } 572 | } 573 | } 574 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | use std::io::Write; 3 | use std::sync::Arc; 4 | use base64::alphabet::STANDARD; 5 | use base64::Engine; 6 | use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}; 7 | use serde_json::Number; 8 | use swc::config::IsModule; 9 | use swc_core::common::{chain, FileName, GLOBALS, Globals, Mark, SourceMap}; 10 | use swc_core::common::errors::{EmitterWriter, Handler}; 11 | use swc_core::ecma::ast::EsVersion; 12 | use swc_core::ecma::visit::as_folder; 13 | use swc_ecma_parser::{EsConfig, Syntax}; 14 | 15 | pub mod deobfuscate; 16 | mod shared_cursor; 17 | 18 | /// A token generation error. 19 | #[derive(Debug)] 20 | pub enum GenerateTokenError { 21 | /// Failed to decode the "data" input. 22 | DataError(DecodeDataError), 23 | 24 | /// A JSON encoding error. 25 | JsonError(serde_json::Error), 26 | 27 | /// Failed to generate the math answer. 28 | GenerateAnswerError(GenerateAnswerError), 29 | } 30 | 31 | impl Display for GenerateTokenError { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 33 | match self { 34 | Self::DataError(e) => write!(f, "DataError: {}", e), 35 | Self::JsonError(e) => write!(f, "JsonError: {}", e), 36 | Self::GenerateAnswerError(e) => write!(f, "GenerateAnswerError: {}", e) 37 | } 38 | } 39 | } 40 | 41 | impl std::error::Error for GenerateTokenError {} 42 | 43 | impl From for GenerateTokenError { 44 | fn from(err: DecodeDataError) -> Self { 45 | Self::DataError(err) 46 | } 47 | } 48 | 49 | impl From for GenerateTokenError { 50 | fn from(err: serde_json::Error) -> Self { 51 | Self::JsonError(err) 52 | } 53 | } 54 | 55 | impl From for GenerateTokenError { 56 | fn from(err: GenerateAnswerError) -> Self { 57 | Self::GenerateAnswerError(err) 58 | } 59 | } 60 | 61 | /// A challenge request. 62 | #[derive(serde::Deserialize)] 63 | pub struct Challenge { 64 | /// The input value. 65 | #[serde(rename(deserialize = "a"))] 66 | pub input: f64, 67 | 68 | /// The code for the browser to evaluate to produce the answer. 69 | /// 70 | /// Note: this library does not evaluate JavaScript. It parses 71 | /// this code using SWC and computes the result with zero virtualization. 72 | /// See [generate_token] for more information. 73 | #[serde(rename(deserialize = "c"))] 74 | pub code: String, 75 | 76 | /// The challenge tag. 77 | #[serde(rename(deserialize = "t"))] 78 | pub tag: String 79 | } 80 | 81 | /// A data decoding error. 82 | #[derive(Debug)] 83 | pub enum DecodeDataError { 84 | /// A base64 decoding error. 85 | DecodeError(base64::DecodeError), 86 | 87 | /// A JSON parse error. 88 | JsonError(serde_json::Error) 89 | } 90 | 91 | impl Display for DecodeDataError { 92 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 93 | match self { 94 | Self::DecodeError(e) => write!(f, "DecodeError: {}", e), 95 | Self::JsonError(e) => write!(f, "JsonError: {}", e) 96 | } 97 | } 98 | } 99 | 100 | impl std::error::Error for DecodeDataError {} 101 | 102 | impl From for DecodeDataError { 103 | fn from(err: base64::DecodeError) -> Self { 104 | Self::DecodeError(err) 105 | } 106 | } 107 | 108 | impl From for DecodeDataError { 109 | fn from(err: serde_json::Error) -> Self { 110 | Self::JsonError(err) 111 | } 112 | } 113 | 114 | /// Decodes the given data. 115 | pub fn decode_data(data: &str) -> Result { 116 | // JavaScript can produce padding, but may not. 117 | const PAD_OPTIONAL_CONFIG: GeneralPurposeConfig = GeneralPurposeConfig::new() 118 | .with_decode_padding_mode(DecodePaddingMode::Indifferent); 119 | const PAD_OPTIONAL: GeneralPurpose = GeneralPurpose::new(&STANDARD, PAD_OPTIONAL_CONFIG); 120 | 121 | // Decode from base64 122 | let decoded_data = PAD_OPTIONAL.decode(data)?; 123 | // Parse JSON 124 | Ok(serde_json::from_slice(&decoded_data)?) 125 | } 126 | 127 | /// A solved challenge. 128 | #[derive(serde::Serialize)] 129 | struct SolvedChallenge { 130 | /// The challenge answer. 131 | /// The first element is the produced answer from the math expression, 132 | /// the second is `Object.keys(globalThis.process || {})` (empty array in a legitimate 133 | /// environment), and the third element is `globalThis.marker`, which is currently 134 | /// set to `mark`. 135 | #[serde(rename(serialize = "r"))] 136 | answer: [serde_json::Value; 3], 137 | 138 | /// The challenge tag. 139 | #[serde(rename(serialize = "t"))] 140 | tag: String 141 | } 142 | 143 | /// Generates a token with the given response from the `/openai.jpeg` request. 144 | pub fn generate_token(data: &str) -> Result { 145 | // Decode challenge 146 | let challenge = decode_data(data)?; 147 | 148 | // Generate math answer 149 | let math_answer = generate_answer( 150 | challenge.input, 151 | format!("({})", challenge.code) 152 | )?.unwrap_or(f64::NAN); 153 | // Create answer array 154 | let answer: [serde_json::Value; 3] = [ 155 | // NaN and Infinity are not valid JSON. 156 | // JavaScript produces null for these cases in JSON.stringify, 157 | // so we do the same. 158 | Number::from_f64(math_answer) 159 | .map_or( 160 | serde_json::Value::Null, 161 | |n| serde_json::Value::Number(n) 162 | ), 163 | 164 | // Object.keys(globalThis.process || {}) 165 | // Always an empty array in a legitimate environment. 166 | serde_json::Value::Array(Vec::new()), 167 | 168 | // globalThis.marker 169 | // Currently set in their JavaScript code to "mark" and 170 | // appears to be static. 171 | serde_json::Value::String(String::from("mark")) 172 | ]; 173 | 174 | // Encode JSON 175 | let encoded = serde_json::to_vec(&SolvedChallenge { 176 | answer, 177 | tag: challenge.tag, 178 | })?; 179 | // Encode to base64 180 | Ok(base64::engine::general_purpose::STANDARD.encode(encoded)) 181 | } 182 | 183 | #[derive(Debug)] 184 | pub enum GenerateAnswerError { 185 | /// SWC failed to parse the JavaScript code. 186 | ParseError(anyhow::Error), 187 | 188 | /// Failed to parse transform error(s). 189 | TransformErrorParseError(std::string::FromUtf8Error), 190 | 191 | /// One or more errors were emitted from a transform. 192 | TransformErrors(Vec) 193 | } 194 | 195 | impl Display for GenerateAnswerError { 196 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 197 | match self { 198 | Self::ParseError(e) => write!(f, "ParseError: {}", e), 199 | Self::TransformErrorParseError(e) => write!(f, "TransformErrorParseError: {}", e), 200 | Self::TransformErrors(errs) => write!(f, "TransformErrors: {}", errs.join(", ")) 201 | } 202 | } 203 | } 204 | 205 | impl std::error::Error for GenerateAnswerError {} 206 | 207 | impl From for GenerateAnswerError { 208 | fn from(err: anyhow::Error) -> Self { 209 | Self::ParseError(err) 210 | } 211 | } 212 | 213 | impl From for GenerateAnswerError { 214 | fn from(err: std::string::FromUtf8Error) -> Self { 215 | Self::TransformErrorParseError(err) 216 | } 217 | } 218 | 219 | impl From> for GenerateAnswerError { 220 | fn from(errors: Vec) -> Self { 221 | Self::TransformErrors(errors) 222 | } 223 | } 224 | 225 | /// Generates the answer to the challenge. 226 | /// The returned `Option` is `None` if the expression couldn't be computed. 227 | fn generate_answer(input: f64, code: String) -> Result, GenerateAnswerError> { 228 | let cm = Arc::::default(); 229 | let err_dst = shared_cursor::SharedCursor::new(); 230 | let handler = Handler::with_emitter( 231 | false, 232 | false, 233 | Box::new(EmitterWriter::new( 234 | Box::new(err_dst.clone()) as Box, 235 | None, 236 | true, 237 | false 238 | )) 239 | ); 240 | let compiler = swc::Compiler::new(cm.clone()); 241 | let fm = cm.new_source_file(FileName::Custom("input.js".into()), code); 242 | 243 | let mut answer = None; 244 | let globals = Globals::new(); 245 | let mut parse_error = None; 246 | GLOBALS.set(&globals, || { 247 | // We can't return an error inside a closure, so we use a match instead. 248 | let program = match compiler.parse_js( 249 | fm, 250 | &handler, 251 | EsVersion::latest(), // Who knows what version they target, but this works 252 | Syntax::Es(EsConfig::default()), 253 | IsModule::Bool(false), 254 | None 255 | ) { 256 | Ok(v) => v, 257 | Err(e) => { 258 | parse_error = Some(e); 259 | return; 260 | } 261 | }; 262 | 263 | // Run the transformations. 264 | use swc_ecma_transforms::optimization::simplify::expr_simplifier; 265 | use swc_ecma_transforms::resolver; 266 | // Visitor that computes the value we need 267 | let mut math_expr_visitor = deobfuscate::math_expr::Visitor::new(input); 268 | 269 | compiler.transform(&handler, program, true, chain!( 270 | // Squash the expressions like 4 + 5 * 2 into constant values 271 | expr_simplifier(Mark::new(), Default::default()), 272 | // Resolve identifiers to scope-aware values 273 | resolver(Mark::new(), Mark::new(), false), 274 | // Remove proxy variables 275 | as_folder(deobfuscate::proxy_vars::Visitor::default()), 276 | // Remove string obfuscation 277 | as_folder(deobfuscate::strings::Visitor), 278 | // Convert expressions like Math["floor"] to Math.floor 279 | as_folder(deobfuscate::computed_member_expr::Visitor), 280 | // Compute math expression to a constant value 281 | as_folder(&mut math_expr_visitor) 282 | )); 283 | 284 | // Set answer 285 | answer = math_expr_visitor.answer; 286 | }); 287 | // Return deferred parse error 288 | if let Some(e) = parse_error { 289 | return Err(GenerateAnswerError::from(e)); 290 | } 291 | 292 | // Parse emitted errors 293 | let errors: Vec = String::from_utf8(err_dst.get_ref().unwrap().clone())? 294 | .split("\n") 295 | .filter(|s| !s.is_empty()) 296 | .map(String::from) 297 | .collect(); 298 | // Return error if not empty 299 | if !errors.is_empty() { 300 | return Err(GenerateAnswerError::from(errors)); 301 | } 302 | 303 | Ok(answer) 304 | } 305 | 306 | #[cfg(test)] 307 | mod tests { 308 | use super::*; 309 | 310 | // Test data taken from browser 311 | const TEST_DATA: &str = "eyJ0IjoiZXlKaGJHY2lPaUprYVhJaUxDSmxibU1pT2lKQk1qVTJSME5OSW4wLi4yMHA0T3VUcTFDVGRkVXRmLmhxMm4wbkVHOXFwZ2NlbWE2T1Rma1o0d3F2aTJ4SlJqaXd1YVhqTkZIai1ET1JRbDFyUGVaYXFDREdlc19sNXU5NFBTVHpnUHFlN3RNZGZxbUhGemVyRjBpNjJxSzlVV3Z1MDRaaG1iM3R1MjQ1eVJ2aGd1aXdtRmZONEt6VGcuYlRZTXBOZXg1cmhQNnpScFZUVG5NZyIsImMiOiJmdW5jdGlvbihhKXtmdW5jdGlvbiB4KGUscyl7dmFyIHQ9cigpO3JldHVybiB4PWZ1bmN0aW9uKG4saSl7bj1uLSgtODkxNSsyMjczKzMzODcqMik7dmFyIGM9dFtuXTtyZXR1cm4gY30seChlLHMpfShmdW5jdGlvbihlLHMpe2Zvcih2YXIgdD14LG49ZSgpO1tdOyl0cnl7dmFyIGk9cGFyc2VJbnQodCgxNDYpKS8xKigtcGFyc2VJbnQodCgxMzIpKS8yKStwYXJzZUludCh0KDE0MSkpLzMrcGFyc2VJbnQodCgxMzUpKS80KihwYXJzZUludCh0KDEzMykpLzUpKy1wYXJzZUludCh0KDEzOSkpLzYqKHBhcnNlSW50KHQoMTM3KSkvNykrcGFyc2VJbnQodCgxNDcpKS84KihwYXJzZUludCh0KDE0MikpLzkpK3BhcnNlSW50KHQoMTM0KSkvMTArcGFyc2VJbnQodCgxNDApKS8xMSooLXBhcnNlSW50KHQoMTQzKSkvMTIpO2lmKGk9PT1zKWJyZWFrO24ucHVzaChuLnNoaWZ0KCkpfWNhdGNoe24ucHVzaChuLnNoaWZ0KCkpfX0pKHIsLTk4MTA0MystMTMxNDEzKjUrMjI5ODEwMSk7ZnVuY3Rpb24gcigpe3ZhciBlPVtcIm1hcmtlclwiLFwia2V5c1wiLFwiMzEwODk4V21vbnBtXCIsXCI0NDcwNDU2SVFmZVZhXCIsXCI2S1BveGN4XCIsXCI3NzM5NWVUWHJTWFwiLFwiNTE4MjczMFZjcXRyZlwiLFwiMjI4eGVweWxhXCIsXCJsb2cxcFwiLFwiODQ3bXJJbmFHXCIsXCJwcm9jZXNzXCIsXCI2NTM1OG1KTGJVRlwiLFwiNDQzM1ZMS3JzclwiLFwiMjkxMzMxMlNQRlNpTVwiLFwiOVl0RkRXUlwiLFwiNTg4dUJIUU5MXCJdO3JldHVybiByPWZ1bmN0aW9uKCl7cmV0dXJuIGV9LHIoKX1yZXR1cm4gZnVuY3Rpb24oKXt2YXIgZT14O3JldHVyblthK01hdGhbZSgxMzYpXShhL01hdGguUEkpLE9iamVjdFtlKDE0NSldKGdsb2JhbFRoaXNbZSgxMzgpXXx8e30pLGdsb2JhbFRoaXNbZSgxNDQpXV19KCl9IiwiYSI6MC42NzM3ODM4NzE5MjA3MTEyfQ=="; 312 | 313 | #[test] 314 | fn test_generate_token() { 315 | let result = generate_token(TEST_DATA) 316 | .expect("generate_token failed"); 317 | 318 | // Token on right taken from browser 319 | assert_eq!(result, "eyJyIjpbMC44NjgwOTMzNDIwMDg1MDAxLFtdLCJtYXJrIl0sInQiOiJleUpoYkdjaU9pSmthWElpTENKbGJtTWlPaUpCTWpVMlIwTk5JbjAuLjIwcDRPdVRxMUNUZGRVdGYuaHEybjBuRUc5cXBnY2VtYTZPVGZrWjR3cXZpMnhKUmppd3VhWGpORkhqLURPUlFsMXJQZVphcUNER2VzX2w1dTk0UFNUemdQcWU3dE1kZnFtSEZ6ZXJGMGk2MnFLOVVXdnUwNFpobWIzdHUyNDV5UnZoZ3Vpd21GZk40S3pUZy5iVFlNcE5leDVyaFA2elJwVlRUbk1nIn0="); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/shared_cursor.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | /// A [std::io::Cursor] that has shared ownership. 5 | pub struct SharedCursor { 6 | inner: Arc>>> 7 | } 8 | 9 | impl SharedCursor { 10 | /// Constructs a new [SharedCursor]. 11 | pub fn new() -> Self { 12 | Self { 13 | inner: Arc::new(Mutex::new(std::io::Cursor::new(Vec::new()))) 14 | } 15 | } 16 | 17 | pub fn get_ref(&self) -> std::io::Result> { 18 | let lock = self.inner.lock().unwrap(); 19 | Ok(lock.get_ref().clone()) 20 | } 21 | } 22 | 23 | impl Clone for SharedCursor { 24 | fn clone(&self) -> Self { 25 | Self { 26 | inner: Arc::clone(&self.inner) 27 | } 28 | } 29 | } 30 | 31 | impl Write for SharedCursor { 32 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 33 | let mut lock = self.inner.lock().unwrap(); 34 | lock.write(buf) 35 | } 36 | 37 | fn flush(&mut self) -> std::io::Result<()> { 38 | let mut lock = self.inner.lock().unwrap(); 39 | lock.flush() 40 | } 41 | } 42 | --------------------------------------------------------------------------------