IDP.HELU.PSE85: the antivirus killed my script silently, and it was right to be suspicious
A script that read a local credential file and POSTed it onward stopped running. No error, no exit code, no log line, no popup. It matched the behavioural signature of a token stealer, and the fix was to stop looking like one rather than to add an exception.
TL;DR · THE FIX
Behavioural antivirus heuristics block silently: there is no exit code, no log line and often no notification, so you need a side-effect tell to notice at all. Read a credential file, then POST it with TLS verification off, is the exact signature of a token stealer, and base64 in -EncodedCommand makes it worse. The file-path exception dialog has no parameters field, so only a command-line exception matches an interpreter invocation, and the parent process chain counts. Better than any exception: stop being obfuscated. Swap -EncodedCommand for -File script.ps1 and never whitelist a bare -EncodedCommand pattern.
The symptom
A scheduled job stopped doing its work, consistently and without any error. The task reported success with exit code 0, the log had a start line and no end line, and the thing the script was supposed to produce did not appear. Running the same command by hand from a terminal worked every time.
Works by hand and does nothing on a schedule sends you in a specific direction: environment differences, working directory, the account the task runs as, a PATH missing something. I spent a while there and all of it was fine. The tell, when I finally found it, was in the antivirus’s own history pane:
Threat blocked: IDP.HELU.PSE85
Process: powershell.exe
Why it is so hard to notice
A behavioural block is different from a file-scan quarantine. Nothing is moved or renamed, and nothing appears in a quarantine folder you might browse. The process is terminated partway through, and every mechanism you would normally use to find out is either absent or misleading. There is no exception, because the process was killed rather than failing. There is no useful exit code: the parent sees a terminated child, and in a scheduled task or a shell wrapper that frequently surfaces as success because the wrapper itself exited cleanly. There is no log line from your own code, because it never reached the part that writes one. And there is often no popup, particularly when the process was launched by something running in the background, since the alert has no interactive session to appear in.
So you need a side-effect tell, something outside the process that changes only when the work really happened: a file written at the end of the run with a timestamp, a row in a table, a line appended after the last step rather than before the first. Without one, a silently killed process and a successful one leave identical evidence, and there is nothing to work backwards from later.
Why it was flagged
The script read a local file containing an API credential, opened an HTTPS connection to a remote host with certificate verification disabled, and POSTed the contents onward. Written out like that, it is the behavioural signature of a token stealer. Each step on its own is something a legitimate script does; the sequence is what the heuristic watches for, and the heuristic was right about the shape even though it was wrong about the intent.
Two things made it worse, and both were mine. The invocation used powershell.exe -EncodedCommand <base64>, chosen years ago to dodge a quoting problem. From the outside, a background process launching PowerShell with a base64 payload that then reads credentials and phones home is not a grey area. And TLS verification was off, because a local TLS-intercepting proxy was breaking the connection otherwise. A real fix existed for that; disabling verification was the shortcut, and it also removed the one signal that might have distinguished this traffic from an exfiltration attempt.
The parent chain mattered too. The same work launched directly by Task Scheduler was not flagged, while the same work reached through node -> cmd -> powershell was. Heuristics weigh who started you, and interpreters spawning interpreters is itself part of the pattern.
The fix
Exceptions were the first thing I reached for, and they are the wrong tool here. A file-path exception does not match an interpreter invocation: excluding powershell.exe by path is useless, because the dialog for a path exception has no parameters field and so cannot express “this script”, and dangerous, because if it worked it would exempt every PowerShell process on the machine. Only a command-line exception can match a specific invocation, and not every product offers one. And never whitelist a bare -EncodedCommand pattern. That exception says “ignore any base64 PowerShell payload”, which is the pattern real malware uses and the reason the heuristic looks for it.
What worked was to stop matching the signature. Swapping -EncodedCommand <base64> for -File script.ps1 turned the payload into a readable file on disk that a scanner can inspect, sign, and reason about; that alone removed most of the suspicion, and it made the thing easier to debug for unrelated reasons. Turning certificate verification back on, and solving the intercepting proxy properly by trusting its root in the certificate bundle the tool reads, restored the signal that verification-off had removed. Letting the scheduler launch the interpreter directly shortened the parent chain. And where the design allows, the credential no longer gets read and immediately sent: the remote end holds it, or a derived token goes instead, or an SDK handles auth rather than a hand-rolled read-and-POST.
After those changes the detection stopped with no exception configured anywhere. An exception suppresses a warning about behaviour that has not changed. Changing the behaviour means the next scanner, the next machine, and your colleague’s laptop all agree.
The lesson
A heuristic that flags your script is telling you what your script looks like from outside, and it is usually right about that. Treat the block as a code review from a reader with no context, which is the reader you should be writing for when the code handles credentials.
Any process that can be killed without producing an error needs a completion tell, something outside the process that only changes when the work finished. Otherwise a silent kill and a clean run produce identical evidence, and you will spend your debugging time on the environment because that is where the differences are visible.
Obfuscation has a cost. Base64 in a command line, disabled TLS verification, a chain of interpreters: each was a small local convenience, and together they built something a scanner had every reason to stop. Removing them was less work than the exception would have been, and the result is a script that is easier to read, easier to debug, and no longer indistinguishable from the thing it was mistaken for.
Discussion
Powered by GitHub. Sign in to leave a comment.