HD Anti-Bot: Part 1, deobfuscation and decryption
October 23, 2025
• obfio
In this post, I will be covering a dynamic JavaScript and Java anti-bot solution protecting the mobile app "melbet", this anti-bot is present on a ton of different apps though. In this post, we will...
This article may have been republished from another source and might not have been originally written for this site.⚠️ Some information, tools, or techniques discussed may have changed or evolved since the publishing of this article.
Originally published at https://antibot.blog/posts/1761245844133
Intro
In this post, I will be covering a dynamic JavaScript and Java anti-bot solution protecting the mobile app "melbet", this anti-bot is present on a ton of different apps though. In this post, we will be doing recon, busting open the app, then looking at the requests to see what we're working with, then deobfuscating the JS files and even teaching you how to decrypt the payloads!
I want to also give credit to a friend of mine that I worked on this project with. His name is hakeem github.com/0xhakeem and he was a huge help, the entire project was his idea the begin with and he wanted me to write the blog post!
Requests
So to start, I want to link back to another blog post I've done here because I cover everything you already need to know in order to intercept this apps requests too!
For those who don't want to read that though, I will give a general overview, I just won't go into detail. First you would get the APK off the internet, you can just google "melbet APK", I usually use uptodown for my APK downloads. Next, you want to connect ADB to your emulator of choice, I use MuMu personally. Now that you're connected to adb, you want to shell into it, then start your frida-server so you can use the frida unpin script. That's about it for a general overview!
Now I want to get into investigating the actual requests. For convenience, my friend took a burp log, filtered them, then color highlighted them for our convenience.
As you can see, we have a lot to work with. Let's remember our actual goal here though, it's to deobfuscate these JS files so we can decrypt payloads. Payloads are present in requests to /verify. We can see one has /m/ and 2 of them do not.

Well, what I would do here is go through the JS files and save whatever I think is relevant to my goal. For example, I know this anti-bot is dynamic, therefore any static JS files that are obfuscated are probably not anything worth looking at for me, at least not for our current goal. My first hunch would be that /m/UUIDv4.js and UUIDv4.js files are the dynamic ones, to confirm this in burp, we can right click -> send to repeater -> repeat the request. If the request results in a different script, it must be a dynamic file. My hunch is correct so let's save /m/ as UUID1.js and the other one as UUID2.js and move on to deobfuscation using github.com/T14Raptor/go-fAST.
Deobfuscation
For deobfuscation, I will be using my own fork of go-fAST, which can be found here. This fork adds nil checks to some parts of the generator package which allows us to set a node to nil in order to have generator not write it, effectively "removing" the node (not really). This is not really a good solution, I won't be doing a pull request for this but it will be used for this project :).
Okay, so the first thing we have to do for deobfuscation is use the parsed AST to grab the actual code from the file, we can do this by throwing the entire file into astexplorer.net and following the AST path, then just copying that path into our code pretty much. However, go-fAST is a little bit different with some key differences so let me show you the babel AST tree in astexplorer compared to what we have to do in go-fAST:
Babel:

go-fAST:
1func Deobfuscate(JS string) (string, error) {
2
3 parsed, err := parser.ParseFile(JS)
4 if err != nil {
5 return "", err
6 }
7 JS = parsed.Body[0].Stmt.(*ast.ExpressionStatement).Expression.Expr.(*ast.CallExpression).Callee.Expr.(*ast.CallExpression).ArgumentList[1].Expr.(*ast.StringLiteral).Value
8 //...You can see, it does translate fairly well, it just has these added .Expr which is easy to get used to! Anyways, now we have to re-parse the AST tree, however, we can't yet because the final node of this tree is actually this:
1return (() => {
2 if (H0R7mY7.hjTx5mG === 0x65) {
3 return "<SPOILER>";
4 }
5 return rQU0uC();
6})();Which is an invalid return statement. There's a few ways we can fix this, the way I chose to fix this is to just wrap everything in a "temporary" function, except it's not temporary because I never ended up removing it lol. Anyways, there's one more thing I should mention about go-fAST compared to babel, for those coming from babel. In babel, you should be used to doing something like path.scope.getBinding to get the parent path of let's say a call expression for instance. In babel, you should be used to it already resolving contextual awareness, go-fAST doesn't do this by default so we will need to call the "resolver". Okay, now that you know that, here's the code we have now:
1func Deobfuscate(JS string) (string, error) {
2
3 parsed, err := parser.ParseFile(JS)
4 if err != nil {
5 return "", err
6 }
7 JS = parsed.Body[0].Stmt.(*ast.ExpressionStatement).Expression.Expr.(*ast.CallExpression).Callee.Expr.(*ast.CallExpression).ArgumentList[1].Expr.(*ast.StringLiteral).Value
8 JS = "function a(){" + JS + "}"
9 parsed, err = parser.ParseFile(JS)
10 if err != nil {
11 return "", err
12 }
13 simplifier.Simplify(parsed, true)
14 resolver.Resolve(parsed)
15 //...Now that we have this output, I can cover something that will come in handy for us later. Let's cover how strings are obfuscated (encoded). To do this, let's trace a using of encoding by saving our current output like this:
1os.WriteFile("test.js", []byte(generator.Generate(parsed)), 0666)Let's trace an example down by the very bottom of the file, I call this the "java hash", you'll see why in part 2:
1return (() => {
2 if (H0R7mY7.hjTx5mG === 0x65) {
3 return Gi8ginl(0x2b7);
4 }
5 return rQU0uC();
6})();Now let's go to Gi8ginl:
1function Gi8ginl(po46Xz) {
2 if (typeof br6_Vtp[po46Xz] === "undefined") {
3 return br6_Vtp[po46Xz] = r2pQ2Hi(XMuuy5g[po46Xz]);
4 }
5 return br6_Vtp[po46Xz];
6}Well, we know po46Xz is the number that outputs our decoded string. The check we have here is typeof br6_Vtp[po46Xz] === "undefined", therefore, br6_Vtp is probably a lookup table to avoid decoding the same thing multiple times. That leaves XMuuy5g, what is that? Let's see:
1c0eVLu(br6_Vtp = {}, XMuuy5g = Z_l9Dv(["\"EeVT36\":;]r.aLn",/*...*/], 0x9));
2function Z_l9Dv(br6_Vtp, XMuuy5g, po46Xz) {
3 for (po46Xz = 0x0; po46Xz < XMuuy5g; po46Xz++) br6_Vtp.push(br6_Vtp.shift());
4 return br6_Vtp;
5}This looks like a simple shifted encoded string array, something you will often times see in obfuscator.io samples, though this is not obfuscator.io at all. Okay, so to decode strings, we would need to get this array and that 0x9 number and shift that array like it would be in JS. What else? Let's see that r2pQ2Hi function:
1function r2pQ2Hi(br6_Vtp) {
2 var XMuuy5g = ".nhrdJGCsaWtjK\"6kR8~y&!F*,]Li)>;YBXoxEU9+IS:3[|wO(/cvPuAz#qHDQpMb0V1{Ne`<@4g?m5Zl$T%2_=7f^}", po46Xz, g92lVj, aiKbc4x, abS7yFi, yxKoOT, ky8pHJ6, FSJmEi;
3 c0eVLu(po46Xz = "" + (br6_Vtp || ""), g92lVj = po46Xz.length, aiKbc4x = [], abS7yFi = 0x0, yxKoOT = 0x0, ky8pHJ6 = -1);
4 for (FSJmEi = 0x0; FSJmEi < g92lVj; FSJmEi++) {
5 var TkpKwR8 = XMuuy5g.indexOf(po46Xz[FSJmEi]);
6 if (TkpKwR8 === -1) continue;
7
8 if (ky8pHJ6 < 0x0) {
9 ky8pHJ6 = TkpKwR8;
10 } else {
11 c0eVLu(ky8pHJ6 += TkpKwR8 * 0x5b, abS7yFi |= ky8pHJ6 << yxKoOT, yxKoOT += (ky8pHJ6 & 0x1fff) > 0x58 ? 0xd : 0xe);
12 do {
13 c0eVLu(aiKbc4x.push(abS7yFi & 0xff), abS7yFi >>= 0x8, yxKoOT -= 0x8);
14 } while(yxKoOT > 0x7);
15 ky8pHJ6 = -1;
16 }
17 }
18 if (ky8pHJ6 > -1) {
19 aiKbc4x.push((abS7yFi | ky8pHJ6 << yxKoOT) & 0xff);
20 }
21 // return eqiDN78(aiKbc4x);
22 return aiKbc4x.toString(); // trust me bro
23}We can look at another similar function to see what changes, the only thing that changes is this string at the top, I'll call it the alphabet. So we need to port this function to golang and make it take in our encoded string, which would be br6_Vtp, and the alphabet used, which would be XMuuy5g in this function. In all honesty, I'm just going to feed this function into GPT and have it port it to golang for me. You'll see all that code later, shoutout GPT-5.
Now we go onto our first visitor, or rather our first two visitors, they serve the same purpose but they are different code-wise. Do you remember how I said go-fAST doesn't do the whole path.scope.getBinding thing? Well, that means we basically do that ourselves! So let's make our first visitor that just grabs every function in the file, super simple to start. Let's create a new file named getFunctionsVisitor.go and in this file we will put this code:
1type GetFunctionsVisitor struct {
2 ast.NoopVisitor
3 Functions []*ast.FunctionDeclaration
4}
5
6func (v *GetFunctionsVisitor) VisitFunctionDeclaration(node *ast.FunctionDeclaration) {
7 v.Functions = append(v.Functions, node)
8 node.VisitChildrenWith(v)
9}Now let me explain, every visitor you make will look like this. Whatever you're visiting will have a struct with ast.NoopVisitor with anything you need inside of it. The function name and param will be whatever it is you're visiting. I'm doing node.VisitChildrenWith so that it will continue to visit any other children if any exist within the parent, this way we will miss no functions. We will always use this method in our visitors.
Now how do we actually use this visitor? Let's go back to our main function and add this:
1// get all needed functions
2
3getFunctionsVisitor := &GetFunctionsVisitor{}
4getFunctionsVisitor.V = getFunctionsVisitor
5parsed.VisitChildrenWith(getFunctionsVisitor)
6
7fmt.Println(len(getFunctionsVisitor.Functions))Now you can run the code and it should print the total number of function declarations in the code! Why did I have us do this though? We're trying to deobfuscate the file, therefore we need to be able to see the string decoding CallExpressions and know which functions they are calling. But again, why? Let's look at one of these functions:
1let decodeString = function(po46Xz) {
2 var g92lVj = "JxDeQCTNOFWIKsqPUmMBEA|5[.g3z*)aw6f7~$4!1,?RbnvjGcZ@k2>r^]H&Vdh{#8_/:iS+9Yy%lL}=0p;Xt`<(\"ou",
3 aiKbc4x, br6_Vtp, XMuuy5g, abS7yFi, yxKoOT, ky8pHJ6, FSJmEi;
4 c0eVLu(aiKbc4x = "" + (po46Xz || ""), br6_Vtp = aiKbc4x.length, XMuuy5g = [], abS7yFi = 0x0, yxKoOT = 0x0, ky8pHJ6 = -1);
5 for (FSJmEi = 0x0; FSJmEi < br6_Vtp; FSJmEi++) {
6 var TkpKwR8 = g92lVj.indexOf(aiKbc4x[FSJmEi]);
7 if (TkpKwR8 === -1) continue;
8
9 if (ky8pHJ6 < 0x0) {
10 ky8pHJ6 = TkpKwR8;
11 } else {
12 c0eVLu(ky8pHJ6 += TkpKwR8 * 0x5b, abS7yFi |= ky8pHJ6 << yxKoOT, yxKoOT += (ky8pHJ6 & 0x1fff) > 0x58 ? 0xd : 0xe);
13 do {
14 c0eVLu(XMuuy5g.push(abS7yFi & 0xff), abS7yFi >>= 0x8, yxKoOT -= 0x8);
15 } while (yxKoOT > 0x7);
16 ky8pHJ6 = -1;
17 }
18 }
19 if (ky8pHJ6 > -1) {
20 XMuuy5g.push((abS7yFi | ky8pHJ6 << yxKoOT) & 0xff);
21 }
22 return eqiDN78(XMuuy5g);
23};You can look for yourself, sometimes these functions are defined as assignment expressions, sometimes they're function declarations, and you can confirm yourself that the string at the top (alphabet) changes between every function. That is why we need to know which function is used. That also is a great way to explain why we need this second visitor I mentioned earlier! I mentioned sometimes they're defined as assignment expressions, specifically though, they're always defined like this:
1if(!xxx){
2 yyy = function(...zzz) {/*...*/}
3}So let's go ahead and write a visitor that matches for this pattern, a visitor for if statements, with a test case, that tests for a NOT (!) token against an identifier type, who's consequent body is 1 in length and that body is just 1 assignment expression function literal. I know, that's quite a mouth full. I won't go that in-depth later, I write comments anyways :p
1type GetFunctionsInIfStmtVisitor struct {
2 ast.NoopVisitor
3 AssignExpression []*ast.AssignExpression
4}
5
6func (v *GetFunctionsInIfStmtVisitor) VisitIfStatement(node *ast.IfStatement) {
7 node.VisitChildrenWith(v)
8 // check if the test is a unary expression and the operator is "!" and the argument is an identifier
9 if _, ok := node.Test.Expr.(*ast.UnaryExpression); !ok {
10 return
11 }
12 if node.Test.Expr.(*ast.UnaryExpression).Operator != token.Not {
13 return
14 }
15 if _, ok := node.Test.Expr.(*ast.UnaryExpression).Operand.Expr.(*ast.Identifier); !ok {
16 return
17 }
18 // now make sure the body is only 1 long and it's an AssignmentExpression
19 if len(node.Consequent.Stmt.(*ast.BlockStatement).List) != 1 {
20 return
21 }
22 if _, ok := node.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ExpressionStatement); !ok {
23 return
24 }
25 assignExpr, ok := node.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ExpressionStatement).Expression.Expr.(*ast.AssignExpression)
26 if !ok {
27 return
28 }
29 v.AssignExpression = append(v.AssignExpression, assignExpr)
30}Then we can add it to the main code:
1getFunctionsInIfStmtVisitor := &GetFunctionsInIfStmtVisitor{}
2getFunctionsInIfStmtVisitor.V = getFunctionsInIfStmtVisitor
3parsed.VisitChildrenWith(getFunctionsInIfStmtVisitor)
4fmt.Println(len(getFunctionsInIfStmtVisitor.AssignExpression))Now we're good to go onto the next step, which will be deobfuscating the strings! First, we need to pull out the encoded strings array and the shifting number. If you remember from earlier, this is in a CallExpression with 2 arguments, the first one is an empty object assignment, the second is a CallExpression with 2 arguments, the first argument being the array, the second being the shift number.
There's one thing to note though, we want to remove this node from our AST tree. We're using my fork of go-fAST so we can do this by setting the node to nil but we want to actually visit the Expression not the CallExpression, so what we do is this:
1type GetEncodedStringsVisitor struct {
2 ast.NoopVisitor
3 EncodedStrings []string
4 ShiftNumber int
5}
6
7func (v *GetEncodedStringsVisitor) VisitExpression(node *ast.Expression) {
8 if node == nil || node.Expr == nil {
9 return
10 }
11 node.VisitChildrenWith(v)
12
13 // make sure the expr is a call expr
14 if _, ok := node.Expr.(*ast.CallExpression); !ok {
15 return
16 }
17 callExpr := node.Expr.(*ast.CallExpression)
18 // first check if the callExpression has 2 arguments
19 if len(callExpr.ArgumentList) != 2 {
20 return
21 }
22 // next check if the first argument is an empty object assignment expression
23 if _, ok := callExpr.ArgumentList[0].Expr.(*ast.AssignExpression); !ok {
24 return
25 }
26 if _, ok := callExpr.ArgumentList[0].Expr.(*ast.AssignExpression).Right.Expr.(*ast.ObjectLiteral); !ok {
27 return
28 }
29 if len(callExpr.ArgumentList[0].Expr.(*ast.AssignExpression).Right.Expr.(*ast.ObjectLiteral).Value) != 0 {
30 return
31 }
32 // next check if the second argument is an assignment expression to a callExpression with 2 arguments, first being our array, second being our shift number
33 if _, ok := callExpr.ArgumentList[1].Expr.(*ast.AssignExpression); !ok {
34 return
35 }
36 short := callExpr.ArgumentList[1].Expr.(*ast.AssignExpression).Right.Expr
37 if _, ok := short.(*ast.CallExpression); !ok {
38 return
39 }
40 if len(short.(*ast.CallExpression).ArgumentList) != 2 {
41 return
42 }
43 if _, ok := short.(*ast.CallExpression).ArgumentList[0].Expr.(*ast.ArrayLiteral); !ok {
44 return
45 }
46 if _, ok := short.(*ast.CallExpression).ArgumentList[1].Expr.(*ast.NumberLiteral); !ok {
47 return
48 }
49 v.ShiftNumber = int(short.(*ast.CallExpression).ArgumentList[1].Expr.(*ast.NumberLiteral).Value)
50 for _, x := range short.(*ast.CallExpression).ArgumentList[0].Expr.(*ast.ArrayLiteral).Value {
51 v.EncodedStrings = append(v.EncodedStrings, x.Expr.(*ast.StringLiteral).Value)
52 }
53 node.Expr = nil
54}You see we're "removing" the node by setting node.Expr to nil here. We also added this check if node == nil || node.Expr == nil which you will see later too, because we're just setting the node to nil, the visitor still thinks the node is there and will still visit it which will cause a panic if we try to do node.VisitChildrenWith(v) on a node that has a nil Expr.
Hopefully after I've explained the first 2 visitors, this visitor should be easy to understand, these are just basic validation checks, nothing fancy going on here. If you're confused, compare these checks to what you see on astexplorer.net and it should make sense!
Now we just need to shift the array:
1// file: ./deobfuscator/deobfuscator.go
2// get the encoded strings array + the shift number
3
4getEncodedStringsVisitor := &GetEncodedStringsVisitor{}
5getEncodedStringsVisitor.V = getEncodedStringsVisitor
6parsed.VisitChildrenWith(getEncodedStringsVisitor)
7
8// shift the array
9
10shiftedArr := shiftArr(getEncodedStringsVisitor.EncodedStrings, getEncodedStringsVisitor.ShiftNumber)
11
12// file: ./deobfuscator/util.go
13/*
14function shift(lhpj81, kzQxxTH, VyNFGf) {
15 for (VyNFGf = 0x0; VyNFGf < kzQxxTH; VyNFGf++) lhpj81.push(lhpj81.shift());
16 return lhpj81;
17}
18*/
19func shiftArr(lhpj81 []string, kzQxxTH int) []string {
20 for VyNFGf := 0; VyNFGf < kzQxxTH; VyNFGf++ {
21 lhpj81 = append(lhpj81, lhpj81[0])
22 lhpj81 = lhpj81[1:]
23 }
24 return lhpj81
25}Boom, we got what we need to make and run our visitor to decode the strings! Remember, we have two sets of functions, one being normal function definitions, and another being assignment expressions, this just means we're making two visitors that do basically identical things. So first, let's define our visitor struct:
1type DecodeStringsVisitor struct {
2 ast.NoopVisitor
3 Functions []*ast.FunctionDeclaration
4 EncodedStrings []string
5 UsedFuncPositions []ast.Id
6}You see I did UsedFuncPositions, this is so we know which functions are decoder functions used in the decoding routines, we're going to later remove these so we need to add them here. Don't worry, I'll explain what ast.Id is later. I told you early that any data we need for a visitor needs to be passed in thru the struct, that's what you're seeing with Functions and EncodedStrings.
Now I need to explain how we're going to context-aware figure out which function is the function we need to grab. So, in go-fAST for every *ast.Identifier which both FunctionDeclaration's, CallExpression's, and AssignmentExpression's have, they all have a method called ToId() that returns an ast.Id type. This is basically just a context-aware ID that is able to say that two identifiers are connected. So, we can make a function that takes in our slice of FunctionDeclaration's and our CallExpression, then have it output the correct FunctionDeclaration by comparing all the ast.Id's until we find the correct comparison:
1// file: ./deobfuscator/util.go
2func getFunctionByNode(functions []*ast.FunctionDeclaration, node *ast.CallExpression) *ast.FunctionDeclaration {
3 if _, ok := node.Callee.Expr.(*ast.Identifier); !ok {
4 return nil
5 }
6 callExprID := node.Callee.Expr.(*ast.Identifier).ToId()
7 for _, function := range functions {
8 if function == nil || function.Function == nil {
9 continue
10 }
11 if function.Function.Name.ToId() == callExprID {
12 return function
13 }
14 }
15 return nil
16}Okay, now let's do our visitor:
1func (v *DecodeStringsVisitor) VisitExpression(node *ast.Expression) {
2 if node == nil || node.Expr == nil {
3 return
4 }
5 node.VisitChildrenWith(v)
6 if _, ok := node.Expr.(*ast.CallExpression); !ok {
7 return
8 }
9 callExpr := node.Expr.(*ast.CallExpression)
10 // first check that the CallExpression has 1 argument only and that argument is a numeric literal
11 if len(callExpr.ArgumentList) != 1 {
12 return
13 }
14 if _, ok := callExpr.ArgumentList[0].Expr.(*ast.NumberLiteral); !ok {
15 return
16 }
17 // now get the function used
18 function := getFunctionByNode(v.Functions, callExpr)
19 if function == nil {
20 return
21 }
22 //...So, we visit the Expression, make sure it's a CallExpression, then make sure it's a CallExpression with 1 NumericLiteral argument. After that, we get the function it's calling. Good, onto the next step:
1//...
2// now try to get the if statement in the function, if there isn't an if statement, return
3var ifStmt *ast.IfStatement
4for _, stmt := range function.Function.Body.List {
5 if _, ok := stmt.Stmt.(*ast.IfStatement); ok {
6 ifStmt = stmt.Stmt.(*ast.IfStatement)
7 break
8 }
9}
10if ifStmt == nil {
11 return
12}
13//...The first function we hit is really a proxy-function type thing for the real decoder function, so to get to the real function we want, we need to get inside the if-statement as seen here, we want this r2pQ2Hi function:
1function Gi8ginl(po46Xz) {
2 if (typeof br6_Vtp[po46Xz] === "undefined") {
3 return br6_Vtp[po46Xz] = r2pQ2Hi(XMuuy5g[po46Xz]);
4 }
5 return br6_Vtp[po46Xz];
6}We still don't know if we're actually in a valid call though, so we need to do more validation. We will validate that the if statement's test is a BinaryExpression, with the operator === and the string being "undefined". Then a few more checks that the body of the if statement is a return statement of an AssignmentExpression where left is a MemberExpression and right is a CallExpression with 1 argument. I know, it's a mouth full, but that's pretty much it for the checks to make sure we're in a valid decoder:
1// now check that the if statement test is a BinaryExpression, operator being "==="
2if _, ok := ifStmt.Test.Expr.(*ast.BinaryExpression); !ok {
3 return
4}
5if ifStmt.Test.Expr.(*ast.BinaryExpression).Operator != token.StrictEqual {
6 return
7}
8// check that the right of the test is a StringLiteral, being the string "undefined"
9if _, ok := ifStmt.Test.Expr.(*ast.BinaryExpression).Right.Expr.(*ast.StringLiteral); !ok {
10 return
11}
12if ifStmt.Test.Expr.(*ast.BinaryExpression).Right.Expr.(*ast.StringLiteral).Value != "undefined" {
13 return
14}
15// now check that the consequent is a BlockStatement, being 1 ReturnStatement that's an AssignmentExpression where left is a MemberExpression and right is a CallExpression with 1 argument
16if _, ok := ifStmt.Consequent.Stmt.(*ast.BlockStatement); !ok {
17 return
18}
19if len(ifStmt.Consequent.Stmt.(*ast.BlockStatement).List) != 1 {
20 return
21}
22if _, ok := ifStmt.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ReturnStatement); !ok {
23 return
24}
25if _, ok := ifStmt.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ReturnStatement).Argument.Expr.(*ast.AssignExpression); !ok {
26 return
27}
28if _, ok := ifStmt.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ReturnStatement).Argument.Expr.(*ast.AssignExpression).Left.Expr.(*ast.MemberExpression); !ok {
29 return
30}
31if _, ok := ifStmt.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ReturnStatement).Argument.Expr.(*ast.AssignExpression).Right.Expr.(*ast.CallExpression); !ok {
32 return
33}
34if len(ifStmt.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ReturnStatement).Argument.Expr.(*ast.AssignExpression).Right.Expr.(*ast.CallExpression).ArgumentList) != 1 {
35 return
36}
37mainDecFuncCallExpr := ifStmt.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ReturnStatement).Argument.Expr.(*ast.AssignExpression).Right.Expr.(*ast.CallExpression)
38mainDecFunc := getFunctionByNode(v.Functions, mainDecFuncCallExpr)
39if mainDecFunc == nil {
40 return
41}Again, if you're confused about this, look in astexplorer.net and it will make more sense. Now there's one edge case I have to cover that I didn't cover previously having to do with the alphabet. You may have noticed by now that sometimes things are wrapped in this weird c0eVLu function for seemingly no reason, it does not effect execution at all. Well, this happens to the alphabet too sometimes, so we have to account for that. Sometimes the alphabet is defined like this:
1var g92lVj = "JxDeQCTNOFWIKsqPUmMBEA|5[.g3z*)aw6f7~$4!1,?RbnvjGcZ@k2>r^]H&Vdh{#8_/:iS+9Yy%lL}=0p;Xt`<(\"ou",aiKbc4x, br6_Vtp, XMuuy5g, abS7yFi, yxKoOT, ky8pHJ6, FSJmEi;Then other times, it's like this:
1c0eVLu(po46Xz.length = 0x1, po46Xz.a = "xZn5;)F:guX(UAc?{O6mB#Wzd$EHkh+3yY%^]R*polbe7>0GIJ<f9s=iwLM1ja&_~|\"t`,K2Q./4DPSVrTCv[@qN8}!", po46Xz[0x9b] = "" + (po46Xz[0x0] || ""), po46Xz[-145] = po46Xz[0x9b].length, po46Xz[0x4] = [], po46Xz[0x5] = 0x0, po46Xz[0x6] = 0x0, po46Xz[-113] = -1);I checked myself, in those cases, the alphabet is always the second argument in the CallExpression which makes it easier to deal with. Okay, now that we got that out of the way, let's get the alphabet:
1// get alphabet, it's the first var declarator in the first declaration
2// EDIT: sometimes we actually have it like this:
3// c0eVLu(po46Xz.length = 0x1, po46Xz.a = "xZn5;)F:guX(UAc?{O6mB#Wzd$EHkh+3yY%^]R*polbe7>0GIJ<f9s=iwLM1ja&_~|\"t`,K2Q./4DPSVrTCv[@qN8}!", po46Xz[0x9b] = "" + (po46Xz[0x0] || ""), po46Xz[-145] = po46Xz[0x9b].length, po46Xz[0x4] = [], po46Xz[0x5] = 0x0, po46Xz[0x6] = 0x0, po46Xz[-113] = -1);
4// so let's check if it's a vardec, if not, let's fallback to the edgecase
5alphabet := ""
6if _, ok := mainDecFunc.Function.Body.List[0].Stmt.(*ast.VariableDeclaration); ok {
7 alphabet = mainDecFunc.Function.Body.List[0].Stmt.(*ast.VariableDeclaration).List[0].Initializer.Expr.(*ast.StringLiteral).Value
8} else {
9 alphabet = mainDecFunc.Function.Body.List[0].Stmt.(*ast.ExpressionStatement).Expression.Expr.(*ast.CallExpression).ArgumentList[1].Expr.(*ast.AssignExpression).Right.Expr.(*ast.StringLiteral).Value
10}Now we're just down to actually decoding the string, earlier I said I'd pass the decoding function to GPT and give the code to us later, here is that later:
1func decodeString(in, alphabet string) string {
2 var idx [256]int
3 for i := 0; i < len(idx); i++ {
4 idx[i] = -1
5 }
6 for i := 0; i < len(alphabet); i++ {
7 idx[alphabet[i]] = i
8 }
9 out := make([]byte, 0, len(in))
10 var v uint32
11 var b uint32
12 n := -1
13 for i := 0; i < len(in); i++ {
14 c := in[i]
15 x := -1
16 if c < 255 {
17 x = idx[c]
18 }
19 if x == -1 {
20 continue
21 }
22 if n < 0 {
23 n = x
24 } else {
25 n += x * 91
26 v |= uint32(n) << b
27 if (n & 0x1fff) > 88 {
28 b += 13
29 } else {
30 b += 14
31 }
32 for b > 7 {
33 out = append(out, byte(v&0xff))
34 v >>= 8
35 b -= 8
36 }
37 n = -1
38 }
39 }
40 if n > -1 {
41 out = append(out, byte((v|uint32(n)<<b)&0xff))
42 }
43 return string(out)
44}Shoutout GPT cause this worked first try, no trouble shooting needed. If you're new to reverse engineering, don't use any AI please, it will just hinder your learning. I could've very easily ported that algorithm myself. I used AI because it saved me some time, not because I was incapable :), if you're new, be cautious with how you use AI in your learning journey.
Anyways, the final bit of code is super simple:
1number := int(callExpr.ArgumentList[0].Expr.(*ast.NumberLiteral).Value)
2decodedString := decodeString(v.EncodedStrings[number], alphabet)
3*node = ast.Expression{
4 Expr: &ast.StringLiteral{
5 Value: decodedString,
6 },
7}
8v.UsedFuncPositions = append(v.UsedFuncPositions, function.Function.Name.ToId())
9v.UsedFuncPositions = append(v.UsedFuncPositions, mainDecFunc.Function.Name.ToId())Now for the AssignmentExpression functions, that will be on the github for this project, since it's so similar, I won't bother putting it here in the blog. Here's how we'd run it though:
1// decode the strings
2
3decodeStringsVisitor := &DecodeStringsVisitor{
4 Functions: getFunctionsVisitor.Functions,
5 EncodedStrings: shiftedArr,
6}
7decodeStringsVisitor.V = decodeStringsVisitor
8parsed.VisitChildrenWith(decodeStringsVisitor)
9
10decodeStringsVisitor1 := &DecodeStringsVisitor1{
11 EncodedStrings: shiftedArr,
12 AssignExpression: getFunctionsInIfStmtVisitor.AssignExpression,
13}
14decodeStringsVisitor1.V = decodeStringsVisitor1
15parsed.VisitChildrenWith(decodeStringsVisitor1)Now, we just want a similar visitor to remove our decoder functions, since they're now unneeded. This will include basically a copy of our visitor from earlier to collect our AssignmentExpression function's, except this time we just remove them instead of collecting them:
1type RemoveUnusedFunctionsVisitor struct {
2 ast.NoopVisitor
3 UsedFuncPositions []ast.Id
4}
5
6func (v *RemoveUnusedFunctionsVisitor) VisitFunctionDeclaration(node *ast.FunctionDeclaration) {
7 node.VisitChildrenWith(v)
8 if slices.Contains(v.UsedFuncPositions, node.Function.Name.ToId()) {
9 *node = ast.FunctionDeclaration{}
10 return
11 }
12}
13
14func (v *RemoveUnusedFunctionsVisitor) VisitIfStatement(node *ast.IfStatement) {
15 node.VisitChildrenWith(v)
16 // check if the test is a unary expression and the operator is "!" and the argument is an identifier
17 if _, ok := node.Test.Expr.(*ast.UnaryExpression); !ok {
18 return
19 }
20 if node.Test.Expr.(*ast.UnaryExpression).Operator != token.Not {
21 return
22 }
23 if _, ok := node.Test.Expr.(*ast.UnaryExpression).Operand.Expr.(*ast.Identifier); !ok {
24 return
25 }
26 // now make sure the body is only 1 long and it's an AssignmentExpression
27 if len(node.Consequent.Stmt.(*ast.BlockStatement).List) != 1 {
28 return
29 }
30 if _, ok := node.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ExpressionStatement); !ok {
31 return
32 }
33 _, ok := node.Consequent.Stmt.(*ast.BlockStatement).List[0].Stmt.(*ast.ExpressionStatement).Expression.Expr.(*ast.AssignExpression)
34 if !ok {
35 return
36 }
37 *node = ast.IfStatement{}
38}Then we run it with:
1// remove unused functions
2
3removeUnusedFunctionsVisitor := &RemoveUnusedFunctionsVisitor{
4 UsedFuncPositions: decodeStringsVisitor.UsedFuncPositions,
5}
6removeUnusedFunctionsVisitor.V = removeUnusedFunctionsVisitor
7parsed.VisitChildrenWith(removeUnusedFunctionsVisitor)You'll see I defined two visitors, one for *ast.FunctionDeclaration and one for *ast.IfStatement, it will run both of those in the same visitor which is useful.
Now there's just three more things to do before we're done. One thing you'll notice in your output right now is these disgusting new lines: This is terrible, so I'm going to write a simple regex to remove these:

1var (
2 prettyRegex = regexp.MustCompile(`[ ]+\n[ ]+\n`)
3)
4
5//...
6func Deobfuscate(JS string) (string, error) {
7 //...
8 return prettyRegex.ReplaceAllString(generator.Generate(parsed), ""), nil
9}Now we need to handle these base64 encoded strings, which is something you probably didn't notice. If you look closely, you will notice it: The function responsible for this is simple:

1function fzwOVW5(br6_Vtp) {
2 return window["atob"](br6_Vtp);
3}Matching for this should be simple, I won't walk you through matching for this, here's the visitor, I will explain one thing that I found to be tricky:
1type Base64DecoderVisitor struct {
2 ast.NoopVisitor
3
4 Functions []*ast.FunctionDeclaration
5}
6
7func (v *Base64DecoderVisitor) VisitExpression(node *ast.Expression) {
8 if node == nil || node.Expr == nil {
9 return
10 }
11 node.VisitChildrenWith(v)
12 if _, ok := node.Expr.(*ast.CallExpression); !ok {
13 return
14 }
15 callExpr := node.Expr.(*ast.CallExpression)
16 // first check if the callExpression has 1 argument
17 if len(callExpr.ArgumentList) != 1 {
18 return
19 }
20 // next check if the argument is a string literal
21 if _, ok := callExpr.ArgumentList[0].Expr.(*ast.StringLiteral); !ok {
22 return
23 }
24 // now get the function used
25 function := getFunctionByNode(v.Functions, callExpr)
26 if function == nil {
27 return
28 }
29 // now check if the function is our base64 decoder function
30 if len(function.Function.Body.List) != 1 {
31 return
32 }
33
34 returnStmt, ok := function.Function.Body.List[0].Stmt.(*ast.ReturnStatement)
35 if !ok {
36 return
37 }
38
39 if _, ok := returnStmt.Argument.Expr.(*ast.CallExpression); !ok {
40 return
41 }
42
43 // check if the callExpression has 1 argument
44 if len(returnStmt.Argument.Expr.(*ast.CallExpression).ArgumentList) != 1 {
45 return
46 }
47 // check if object name is `window`
48 if _, ok := returnStmt.Argument.Expr.(*ast.CallExpression).Callee.Expr.(*ast.MemberExpression); !ok {
49 return
50 }
51 if _, ok := returnStmt.Argument.Expr.(*ast.CallExpression).Callee.Expr.(*ast.MemberExpression).Object.Expr.(*ast.Identifier); !ok {
52 return
53 }
54 if returnStmt.Argument.Expr.(*ast.CallExpression).Callee.Expr.(*ast.MemberExpression).Object.Expr.(*ast.Identifier).Name != "window" {
55 return
56 }
57 // check if property name is `atob`
58 if _, ok := returnStmt.Argument.Expr.(*ast.CallExpression).Callee.Expr.(*ast.MemberExpression).Property.Prop.(*ast.ComputedProperty); !ok {
59 return
60 }
61 if _, ok := returnStmt.Argument.Expr.(*ast.CallExpression).Callee.Expr.(*ast.MemberExpression).Property.Prop.(*ast.ComputedProperty).Expr.Expr.(*ast.StringLiteral); !ok {
62 return
63 }
64 if returnStmt.Argument.Expr.(*ast.CallExpression).Callee.Expr.(*ast.MemberExpression).Property.Prop.(*ast.ComputedProperty).Expr.Expr.(*ast.StringLiteral).Value != "atob" {
65 return
66 }
67 // now base64 decode the string
68 decoded, err := base64.StdEncoding.DecodeString(callExpr.ArgumentList[0].Expr.(*ast.StringLiteral).Value)
69 if err != nil {
70 return
71 }
72 *node = ast.Expression{
73 Expr: &ast.StringLiteral{
74 Value: string(decoded),
75 },
76 }
77}When I was writing this, I found matching for window["atob"] to be tricky, maybe that was a user issue. It took me a concerningly long time to figure out that I had to do this (*ast.ComputedProperty) to get the property name. If you think about it though, it makes sense, because a property can be both computed and not computed, so having a difference for each makes complete sense.
Now just one more thing, you may have noticed in your output that some strings look like this: These are BinaryExpressions that combine two strings into one, we can easily write a visitor that combine these strings for us, making it much easier to read:

1type StringConcatVisitor struct {
2 ast.NoopVisitor
3}
4
5func (v *StringConcatVisitor) VisitExpression(node *ast.Expression) {
6 if node == nil || node.Expr == nil {
7 return
8 }
9 node.VisitChildrenWith(v)
10 if _, ok := node.Expr.(*ast.BinaryExpression); !ok {
11 return
12 }
13 binaryExpr := node.Expr.(*ast.BinaryExpression)
14 if binaryExpr.Operator != token.Plus {
15 return
16 }
17 if _, ok := binaryExpr.Left.Expr.(*ast.StringLiteral); !ok {
18 return
19 }
20 if _, ok := binaryExpr.Right.Expr.(*ast.StringLiteral); !ok {
21 return
22 }
23 *node = ast.Expression{
24 Expr: &ast.StringLiteral{
25 Value: binaryExpr.Left.Expr.(*ast.StringLiteral).Value + binaryExpr.Right.Expr.(*ast.StringLiteral).Value,
26 },
27 }
28}Boom, now we're done! Unless there's something further you want to do. There's more you could do here, like unwrapping this useless c0eVLu CallExpression I was talking about earlier. For our purpose of this blog, I won't be doing that though.
Decrypting Payloads
To find where payloads are encrypted, I'm just going to skim through the code. Doing this, I see:
1async function PUkPu6(po46Xz, g92lVj, aiKbc4x, abS7yFi = 0xa, yxKoOT, ky8pHJ6, FSJmEi) {
2 FctlMjq(ky8pHJ6);
3 const TkpKwR8 = RyS4po(g92lVj, aiKbc4x, opj4hsT(abS7yFi["toString"]())), Z_l9Dv = H0R7mY7.sJ0rZhb["subtle"]["exportKey"]("raw", TkpKwR8), r2pQ2Hi = new Uint8Array(Z_l9Dv), UBlbPD = new Uint8Array(r2pQ2Hi["length"] + yxKoOT["length"]);
4 c0eVLu(UBlbPD["set"](r2pQ2Hi), UBlbPD["set"](new TextEncoder()["encode"](yxKoOT), r2pQ2Hi["length"]));
5 const k_tX0M = fwhbz1(UBlbPD), VXSwhNc = H0R7mY7.sJ0rZhb["subtle"]["importKey"]("raw", k_tX0M, {
6 ["name"]: "AES-GCM"
7 }, !0x1, ["encrypt"]), WvkfOc = H0R7mY7.sJ0rZhb["getRandomValues"](new Uint8Array(0xc)), R0wSJUg = IEgvoSb({
8 ["name"]: "AES-GCM",
9 ["iv"]: WvkfOc
10 }, VXSwhNc, new TextEncoder()["encode"](po46Xz)), _B2AjBN = new Uint8Array(R0wSJUg), oMOLHzE = new Uint8Array(0xc + _B2AjBN["length"]);
11 c0eVLu(oMOLHzE["set"](WvkfOc), oMOLHzE["set"](_B2AjBN, 0xc));
12 return P49B6d(oMOLHzE);
13}Which tells us that it's using AES-GCM as it's encryption method. Tracing where this method is used, we see this (cleaned for easier reading):
1Z_l9Dv = (abS7yFi = document["querySelector"]("meta[name=x-hd-trace-id]")) == null ? void 0x0 : abS7yFi["getAttribute"]("content")
2r2pQ2Hi = PUkPu6(JSON["stringify"](TkpKwR8["detail"]), "c25coWBm8Z8c/3v70z3QTLTW3tkoPmv4AOFQoYlWxEXhO3pv5EJDAg==", "VUOh4rfX0VWzMusS7o+fGp/dXz9Xkaw39hfSb/BilaNUuTVFJYU/Ou/yWN/ysD4G", "@", Z_l9Dv)The first thing I assume is that TkpKwR8["detail"] is the payload being encrypted, the 2 things that follow are the encrypted key and maybe something to get the IV (?). Next param I have no clue about, finally is something you'd have to find in the HTTP traffic, but it's returned by the request to get the JS content as a header and it's string value is ac42f242-34f9-45ec-a3a0-66cedcf4bfb7 for me.
Okay, so let's read the function. The first thing that stands out is WvkfOc = H0R7mY7.sJ0rZhb["getRandomValues"] is the IV, this means my assumption is wrong from earlier. I guess that means I have to see what that means about those 2 keys.
Those 2 keys are passed into this RyS4po function along with this @ symbol we are unsure about, except that @ symbol is passed through this opj4hsT function. Let's RE (reverse engineer) this opj4hsT function first:
1const H3XREH = "54";
2
3function opj4hsT(...br6_Vtp) {
4 return br6_Vtp[0x0]["charCodeAt"](0x0) - Number(H3XREH);
5}Okay, looks simple. Get the ascii char code of the @ symbol, then subtract this 54 number from it. I can honestly assume this 54 and @ will be dynamic in the future then. This means that the third param to RyS4po is a number. Okay, what I'm going to do is rename stuff in this RyS4po function to make it easier to read for us:
1async function RyS4po(key, salt, iterations = 0xa) {
2 const ky8pHJ6 = window["crypto"]["subtle"]["importKey"]("raw", HTEjSQ(key), "PBKDF2", false, ["deriveKey"]);
3 return Y0ZoIWX({
4 ["name"]: "PBKDF2",
5 ["salt"]: HTEjSQ(salt),
6 ["iterations"]: iterations,
7 ["hash"]: "SHA-256"
8 }, ky8pHJ6, {
9 ["name"]: "AES-GCM",
10 ["length"]: 0x100
11 }, !0x0, ["encrypt"]);
12}Looking at this HTEjSQ function, it basically just converts the key/salt to bytes. Now we know that this @ symbol represents iterations for deriving an encryption key using the method PBKDF2. Next we need to figure out what it's doing with this ac42f242-34f9-45ec-a3a0-66cedcf4bfb7.
Let me clean up this function to make it easier to read:
1async function PUkPu6(PAYLOAD, g92lVj, aiKbc4x, abS7yFi = 0xa, UUID, ky8pHJ6, FSJmEi) {
2 FctlMjq(ky8pHJ6);
3 const KEY = RyS4po(g92lVj, aiKbc4x, opj4hsT(abS7yFi["toString"]()))
4 Z_l9Dv = H0R7mY7.sJ0rZhb["subtle"]["exportKey"]("raw", KEY)
5 r2pQ2Hi = new Uint8Array(Z_l9Dv)
6 UBlbPD = new Uint8Array(r2pQ2Hi["length"] + UUID["length"]);
7 UBlbPD["set"](r2pQ2Hi)
8 UBlbPD["set"](new TextEncoder()["encode"](UUID), r2pQ2Hi["length"])
9 k_tX0M = fwhbz1(UBlbPD)
10 VXSwhNc = H0R7mY7.sJ0rZhb["subtle"]["importKey"]("raw", k_tX0M, {
11 ["name"]: "AES-GCM"
12 }, false, ["encrypt"])
13 WvkfOc = H0R7mY7.sJ0rZhb["getRandomValues"](new Uint8Array(0xc))
14 R0wSJUg = IEgvoSb({
15 ["name"]: "AES-GCM",
16 ["iv"]: WvkfOc
17 }, VXSwhNc, new TextEncoder()["encode"](PAYLOAD))
18 _B2AjBN = new Uint8Array(R0wSJUg)
19 oMOLHzE = new Uint8Array(0xc + _B2AjBN["length"]);
20 oMOLHzE["set"](WvkfOc)
21 oMOLHzE["set"](_B2AjBN, 0xc);
22 return P49B6d(oMOLHzE);
23}So, that's a lot to digest, what we need to look at is really what's happening to the key after it's made but before it's imported a second time. After looking for a while, I see what we really need to look at is fwhbz1, so let's look:
1async function fwhbz1(po46Xz, g92lVj, aiKbc4x) {
2 FctlMjq(g92lVj);
3 const abS7yFi = typeof po46Xz === "string" ? new TextEncoder()["encode"](po46Xz) : new Uint8Array(po46Xz), yxKoOT = H0R7mY7.sJ0rZhb["subtle"]["digest"]("SHA-256", abS7yFi);
4 return new Uint8Array(yxKoOT);
5}So, basically the encryption key is just the SHA-256 of the PBKDF2 key from earlier + the UUID. Then we encrypt the payload with AES-GCM.
How do we decrypt the payloads then? We need to make a function to derive a key from a given input key, salt, iterations (@ + 54 from our code), and UUID, then also take in the payload.
One thing before I get onto coding, the final payload is actually split into sections that are joined by a weird ascii character. Your full payload would be like this:
1large-string<ascii-char>random-string<ascii-char>encrypted-payload<ascii-char>UUID
2So when you go to get your base64 encoded payload, remember that it is sandwiched between two weird ascii characters.
Now let's grab 2 functions from the code to make things easier, this will also include our crypto import and our const definitions for key, salt, etc.:
1import crypto from 'crypto';
2
3const key = "c25coWBm8Z8c/3v70z3QTLTW3tkoPmv4AOFQoYlWxEXhO3pv5EJDAg=="
4const salt = "VUOh4rfX0VWzMusS7o+fGp/dXz9Xkaw39hfSb/BilaNUuTVFJYU/Ou/yWN/ysD4G"
5const iterationsStr = "@"
6const iterationsNum = "54"
7const UUID = "ac42f242-34f9-45ec-a3a0-66cedcf4bfb7"
8const payload = "YOUR LARGE PAYLOAD BASE64 ENCODED STRING"
9
10function HTEjSQ(po46Xz) {
11 return Uint8Array["from"](atob(po46Xz), (...po46Xz) => {
12 po46Xz.length = 0x1;
13 return po46Xz[0x0]["charCodeAt"](0x0);
14 });
15}
16
17function opj4hsT(...br6_Vtp) {
18 return br6_Vtp[0x0]["charCodeAt"](0x0) - Number(iterationsNum);
19}
20
21let iterations = opj4hsT(iterationsStr)Next, we need to build the PBKDF2 key, so we just need to basically copy and paste their code:
1// get key
2let ik = await crypto.subtle.importKey("raw", HTEjSQ(key), "PBKDF2", false, ["deriveKey"])
3
4let PBKDF2Key = await crypto.subtle.deriveKey({
5 ["name"]: "PBKDF2",
6 ["salt"]: HTEjSQ(salt),
7 ["iterations"]: iterations,
8 ["hash"]: "SHA-256"
9}, ik, {
10 ["name"]: "AES-GCM",
11 ["length"]: 256
12}, true, ["encrypt"])Now we're going to "mix" the key with the UUID and SHA-256 hash it to create the "final key":
1let exportedKey = new Uint8Array(await crypto.subtle.exportKey("raw", PBKDF2Key))
2let keyBytes = new Uint8Array(exportedKey)
3let finalKeyBytes = new Uint8Array(keyBytes.length + UUID.length)
4finalKeyBytes.set(keyBytes)
5finalKeyBytes.set(new TextEncoder().encode(UUID), keyBytes.length)
6let keySHA256 = await crypto.subtle.digest("SHA-256", finalKeyBytes)
7keySHA256 = new Uint8Array(keySHA256)
8const finalKey = await crypto.subtle.importKey("raw", keySHA256, { name: "AES-GCM" }, false, ["decrypt"])The only real difference you should notice here between our code and their code is that final line, in their code it says ["encrypt"] instead of ["decrypt"].
Finally, we just decrypt our payload. We know that there's an IV of 0xc bytes, which is 12 in decimal, that means the first 12 bytes of the payload is the IV and the rest of the payload is the encrypted payload:
1let payloadDecoded = new Uint8Array(Buffer.from(payload, "base64"))
2
3const iv = payloadDecoded.slice(0, 12)
4const payloadDecrypted = await crypto.subtle.decrypt({name: "AES-GCM", iv: iv }, finalKey, payloadDecoded.slice(12))
5
6console.log(new TextDecoder().decode(new Uint8Array(payloadDecrypted)))I can show an example output, unfortunately, the payload shows a lot of identifying information that I can't expose so I can't give my exact payload, here's an example output though:
1{
2 "lng": "en_GB",
3 "fp": {
4 "comp_d": "encrypted string",
5 "value": "encrypted string"
6 }
7}Wrap Up
This is my first blog back, hopefully you enjoyed! I'm mostly writing this blog to get go-fAST some more attention as I think go-fAST is a great package, it's only down-side is that it's lacking a lot of documentation/examples. Hopefully this project can act as a sort of nice example of what you can do with go-fAST (even though I forked it to make removing nodes easier).
This is a 2 part series though! In part 2, I'll be writing an extractor for the dynamic values that you'd need to solve this anti-bot at scale! This is a skill you'd need a lot now-a-days to solve most worth while anti-bots. It will likely take me a while to get part 2 out because I have an 11 day trip to canada coming up soon! After that trip though, it's all set to come out!
For those who have read my blog before, you might notice that this project is using a different github account. Unfortunately, I lost access to my other GitHub account. Unless anyone knows anyone at GitHub that can recover access for me, it's gone D:
References
GitHub for this project: github.com/post04/hd-deobf Friends GitHub: github.com/0xhakeem Old post for reference: antibot.blog/posts/1741712677486 go-fAST: github.com/T14Raptor/go-fAST My fork of go-fAST: github.com/post04/go-fAST Go-fAST Docs: yoghurtbot-io.gitbook.io/go-fast