Route AArch64 payloads to the native powershell.exe on Windows on ARM - #49
Open
vinicius-batistella wants to merge 2 commits into
Open
Route AArch64 payloads to the native powershell.exe on Windows on ARM#49vinicius-batistella wants to merge 2 commits into
vinicius-batistella wants to merge 2 commits into
Conversation
[IntPtr]::Size cannot tell an ARM64 process apart from an x64 one
(both have 8-byte pointers), so the existing wrapper picked the wrong
powershell.exe on Windows-on-ARM whenever the payload architecture
mattered. Metasploit's psexec module currently keeps AArch64 payloads
off the PowerShell delivery path for this reason.
Prepend two branches to the emitted architecture-detection block:
- PROCESSOR_ARCHITECTURE == ARM64 for native ARM64 processes. An
'aarch64' payload targets the current binary (System32 already
resolves to the ARM64 powershell.exe on WoA); an 'x86' payload
hops to SysWOW64 as before.
- PROCESSOR_ARCHITEW6432 == ARM64 for a 32-bit process on a WoA
host. An 'aarch64' payload escapes WOW64 filesystem redirection
via sysnative to reach the native ARM64 powershell.exe.
The existing IntPtr::Size branches are preserved and continue to
handle every non-WoA case. The 'aarch64' string is documented on
both run_hidden_psh and cmd_psh_payload.
No template changes: the reflection and dotnet templates already
transition memory PAGE_READWRITE -> PAGE_EXECUTE_READ via
VirtualProtect, which implicitly invalidates the ARM64 icache for
the range, so injected AArch64 shellcode is coherent by the time
CreateThread executes it. Verified on Windows 11 26200 (ARM64,
Qualcomm) with a minimal 'mov x0,0; ret' probe.
Bumps the gem to 0.1.105 for consumers.
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Updates Rex::Powershell::Command.run_hidden_psh to correctly route payload execution to a compatible powershell.exe on Windows on ARM by detecting WoA via PROCESSOR_ARCHITECTURE / PROCESSOR_ARCHITEW6432 before falling back to [IntPtr]::Size, and extends payload_arch handling to include aarch64.
Changes:
- Add WoA-aware interpreter selection logic and
aarch64routing inrun_hidden_psh. - Add RSpec examples covering
aarch64and WoA-specific routing behavior. - Bump gem version.
Impact Analysis:
- Blast radius: medium — affects all consumers that generate wrappers via
run_hidden_psh/cmd_psh_payload(including downstream usage from Metasploit); Windows-only behavior. - Data and contract effects: no schema/payload/storage changes; expands accepted
payload_archvalues to include'aarch64'. - Rollback and test focus: rollback is straightforward (revert selection logic); focus validation on
spec/rex/powershell/command_spec.rbplus manual WoA execution paths (native ARM64 PS and WOW64 x86 PS).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| spec/rex/powershell/command_spec.rb | Adds coverage asserting WoA env-var branching and aarch64/x86 routing behavior. |
| lib/rex/powershell/command.rb | Implements WoA-aware architecture detection and interpreter selection for aarch64. |
| lib/rex/powershell/version.rb | Bumps the gem version for the release containing WoA routing changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| intptr4_branch = payload_arch == 'x86' ? native_ps : sysnative_ps | ||
| intptr8_branch = payload_arch == 'x86' ? syswow64_ps : native_ps | ||
|
|
||
| archictecure_detection = <<EOS |
Rename the pre-existing archictecure_detection local so the generated-code wrapper is easier to search. Behavior is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rex::Powershell::Command.run_hidden_pshcurrently picks between the native and SysWOW64 copies ofpowershell.exepurely from[IntPtr]::Size. That check works fine on x86/x64 hosts, but it collapses two very different hosts into the same branch on Windows on ARM (WoA):[IntPtr]::Size == 8) — currently receives the same treatment as an x64 host, which is correct foraarch64payloads but leaves x86 payloads with no path to a compatible interpreter.[IntPtr]::Size == 4) — currently receives the same treatment as an x86 host, meaning anaarch64payload never reaches an ARM64-native PowerShell.This PR uses
PROCESSOR_ARCHITECTURE/PROCESSOR_ARCHITEW6432(which are stable across Windows 10/11 on ARM) to detect a WoA host before falling back to the existing[IntPtr]::Sizelogic.payload_archis extended to accept'aarch64', and the routing is:x86payloadx86_64payloadaarch64payloadThe reflection templates themselves need no changes: they lay the shellcode down with
VirtualAlloc(RW)and then transition toRXviaVirtualProtect, which is a documented I-cache-synchronising transition on ARM64 Windows. That was confirmed with a spike (see below) — a payload run without an explicitFlushInstructionCachestill executed cleanly.Verification
Spike: WoA environment + I-cache behaviour
A spike script was run inside 64-bit PowerShell on a Windows 11 ARM64 VM. The relevant findings:
[IntPtr]::Size=8,$env:PROCESSOR_ARCHITECTURE=ARM64,PROCESSOR_ARCHITEW6432unset — so the new environment-variable check reliably identifies a WoA host before the size check runs.powershell.exeresolves toC:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe(native ARM64), withC:\Windows\SysWOW64\...andC:\Windows\Sysnative\...populated as expected.mov x0, #0x1234 ; ret) fromVirtualAlloc(RW)→VirtualProtect(RX)returned0x1234without callingFlushInstructionCache, matching Microsoft's guarantee thatVirtualProtect's RW→RX transition flushes the I-cache on ARM64.End-to-end: session on ARM64 VM
With
metasploit-frameworkpinned to this branch, anaarch64payload was wrapped via the updatedrun_hidden_pshcode path and executed on the WoA VM:Pasting the resulting
psh-cmdstring into a Windows terminal on the ARM64 VM produced a live session:The launching PowerShell window closes after the
Start-Process -Hiddenreturns; that is the intended behaviour of thepsh-cmdwrapper (the shellcode is running inside the hidden childpowershell.exe, which owns the session).Test plan
bundle exec rspec spec/rex/powershell/command_spec.rb— newrun_hidden_pshexamples coveringaarch64payload on WoA (native + WOW64) andx86payload on WoA all pass; pre-existing examples untouched.aarch64payload viapsh-cmdwrapper opens awindows/aarch64/shell_reverse_tcpsession againstmsfconsole.Follow-up (separate PR)
Once this lands and a new gem is cut, I'll open a follow-up on
metasploit-frameworkto bump the gem constraint and wire the AArch64 target into thepsexecPowerShell path that @bwatters-r7 is preparing in rapid7#21779.Made with Cursor