ANTIBOTS - PART VII
May 11, 2021
• Nero
We've been through quite a ride till now, but it only gets better. Today we'll talk about control flow flattening. Now don't think it will be really crazy, but it will be pretty interesting. The good...
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://nerodesu017.github.io/posts/2021-05-11-antibots-part-7
ANTIBOTS - PART VII - Control flow flattening
We've been through quite a ride till now, but it only gets better. Today we'll talk about control flow flattening. Now don't think it will be really crazy, but it will be pretty interesting. The good thing is, obfuscator.io uses a basic control flow flattening technique. Now, what is control flow flattening? The guys at jscrambler explained it best. I'll only have a few references here (since I'm bad at drawing and they already got pictures, that's how you can easily understand)

Control Flow Flattening is.. Just that.. Flattening the flow of a program, it is usually done with a switch statement and it makes you jump from case to case, which, these cases, are not in an ordered way. It might be hard to grasp, in part cause you've probably never dealt with them, but mostly cause I have no idea how to explain them. So, since words don't make it advantageous for me, let me show you how they look inside our script:
1var dewberries = "3|2|5|6|1|7|8|4|0".split("|"),
2 dragon_fruit = 0;
3
4 while (true) {
5 switch (dewberries[dragon_fruit++]) {
6 case "0":
7 ...
8 continue;
9
10 case "1":
11 ...
12 continue;
13
14 case "2":
15 ...
16 continue;
17
18 case "3":
19 ...
20 continue;
21
22 case "4":
23 ...
24 continue;
25
26 case "5":
27 ...
28 continue;
29
30 case "6":
31 ...
32 continue;
33
34 case "7":
35 ...
36 continue;
37
38 case "8":
39 ...
40 continue;
41 }
42
43 break;
44 }
45Basically how the switch is implemented is:
We have a variable declaration ("dewberries") which will hold the cases' index in the specified order
A second variable that cycles through the array ("dragon_fruit")
The actual switch case How shall we go about it? Well let's think for a second:
Firstly, we need to correctly identify the switch cases that are part of the control flow flattening. If we manually search for "switch(" in our file we get a few occurences and the ones we are most interested in are those who look like this: switch(arr1[index_counter++]), a SwitchStatement that has as discriminant a MemberExpression which has, in turn, as object an Identifier and as property an UpdateExpression.
Secondly, we have to go through cases in the order they are in our array and get the nodes from inside them.
Lastly, we replace the whole switch statement, the while loop it is wrapped in and the 2 variables before it (array and index_counter) with the nodes we collected in order.
1const controlFlowDeflatteningVisitor = {
2 SwitchStatement(path){
3 // First, we check to make sure we are at a good SwitchStatement node
4 if(types.isMemberExpression(path.node.discriminant) &&
5 types.isIdentifier(path.node.discriminant.object) &&
6 types.isUpdateExpression(path.node.discriminant.property) &&
7 path.node.discriminant.property.operator === "++" &&
8 path.node.discriminant.property.prefix === false){
9 // After we've made sure we got to the right node, we'll
10 // make a variable that will hold the cases in their order of execution
11 // and gather them in it
12 let nodesInsideCasesInOrder = [];
13 // we gotta get to the parent of the parent
14 // our SwitchStatement is wrapped inside a BlockStatement
15 // which that BlockStatement is the child of a WhileStatement
16 // which is in turn a child of another BlockStatement
17 // so if we go 3 levels up, we can get the previous 2 nodes
18 // (the array containing indexes, and index counter)
19 let mainBlockStatement = path.parentPath.parentPath.parentPath;
20 // after we got 3 levels up, we gotta know the index of our
21 // WhileStatement child in the body of the big BlockStatement
22 let whileStatementKey = path.parentPath.parentPath.key;
23 // after that, we can get the array with the cases in their execution order
24 // both are in the save VariableDeclaration node so we substract 1
25 // and get the first VariableDeclarator child
26 let arrayDeclaration = mainBlockStatement.node.body[whileStatementKey - 1].declarations[0];
27 let casesOrderArray = eval(generate(arrayDeclaration.init).code);
28 // next, we remember the order of the cases inside the switch
29 // we'll use a map like this: caseValue -> caseIndex
30 let casesInTheirOrderInSwitch = new Map();
31 for(let i = 0; i < path.node.cases.length; i++){
32 casesInTheirOrderInSwitch.set(path.node.cases[i].test.value, i);
33 }
34 // After we've got the cases test values and the cases' keys, we're ready to go!
35 for(let i = 0; i < casesOrderArray.length; i++){
36 let currentCase = path.node.cases[casesInTheirOrderInSwitch.get(casesOrderArray[i])];
37 for(let j = 0; j < currentCase.consequent.length; j++){
38 // Don't forget to make sure you don't take a hold of
39 // the continue statements
40 if(!types.isContinueStatement(currentCase.consequent[j]))
41 nodesInsideCasesInOrder.push(currentCase.consequent[j]);
42 }
43 }
44 // after we got the nodes, we first delete delete the VariableDeclaration before our WhileStatement
45 mainBlockStatement.get('body')[whileStatementKey - 1].remove();
46 // then we replace the WhileStatement (which has only our SwitchStatement)
47 // with our nodes we've extracted
48 path.parentPath.parentPath.replaceWithMultiple(nodesInsideCasesInOrder);
49 }
50 }
51}
52
53traverse(AST, controlFlowDeflatteningVisitor);
54traverse(AST, controlFlowDeflatteningVisitor);
55What I've done here, is not that hard to grasp. When you retry to make your own solution, remember to use ASTExplorer There's still 2 things I want to discuss:

Why did I use traverse twice? Well, as you will see if you research the script for yourself, there's usually a SwitchStatement inside another SwitchStatement, and since we are not transforming AST recursively on the nodes we edit, we need to do it twice (For the sake of this tutorial we won't do it recursively, it's efficacy here, not efficiency at the moment)
What is up with the
mainBlockStatement.get('body')
? I wanted to remove the VariableDeclaration node before the WhileStatement. To delete nodes you need to use their path in the AST, not get into the node itself. When using the path, to go through the children of a BlockStatement for example (since the BlockStatement node has a body property), we can use the.get()
method and pass it a query string as we've done 'body' in our case. In other cases (though I haven't tested it, yet), you might be able to use other query strings, like 'determinant', 'consequent', and what not! Let's execute our code! This is what it looked before:
This is what it looks like, after:

Till next time!