diff --git a/lib/rex/exploitation/powershell/obfu.rb b/lib/rex/exploitation/powershell/obfu.rb index 2a1d364884..18a7b241e7 100644 --- a/lib/rex/exploitation/powershell/obfu.rb +++ b/lib/rex/exploitation/powershell/obfu.rb @@ -1,6 +1,5 @@ # -*- coding: binary -*- -require 'zlib' require 'rex/text' module Rex @@ -13,21 +12,23 @@ module Powershell # # Create hash of string substitutions # + # @param strings [Array] array of strings to generate unique names + # + # @return [Hash] map of strings with new unique names def sub_map_generate(strings) map = {} strings.flatten.each do |str| - map[str] = "$#{Rex::Text.rand_text_alpha(rand(2)+2)}" - # Ensure our variables are unique - while not map.values.uniq == map.values - map[str] = "$#{Rex::Text.rand_text_alpha(rand(2)+2)}" - end + @rig.init_var(str) + map[str] = @rig[str] end - return map + + map end # # Remove comments # + # @return [String] code without comments def strip_comments # Multi line code.gsub!(/<#(.*?)#>/m,'') @@ -38,6 +39,7 @@ module Powershell # # Remove empty lines # + # @return [String] code without empty lines def strip_empty_lines # Windows EOL code.gsub!(/[\r\n]+/,"\r\n") @@ -49,6 +51,7 @@ module Powershell # Remove whitespace # This can break some codes using inline .NET # + # @return [String] code with whitespace stripped def strip_whitespace code.gsub!(/\s+/,' ') end @@ -56,6 +59,7 @@ module Powershell # # Identify variables and replace them # + # @return [String] code with variable names replaced with unique values def sub_vars # Get list of variables, remove reserved vars = get_var_names @@ -68,6 +72,8 @@ module Powershell # # Identify function names and replace them # + # @return [String] code with function names replaced with unique + # values def sub_funcs # Find out function names, make map # Sub map keys for values @@ -79,6 +85,7 @@ module Powershell # # Perform standard substitutions # + # @return [String] code with standard substitution methods applied def standard_subs(subs = %w{strip_comments strip_whitespace sub_funcs sub_vars} ) # Save us the trouble of breaking injected .NET and such subs.delete('strip_whitespace') unless string_literals.empty? @@ -87,7 +94,8 @@ module Powershell self.send(modifier) end code.gsub!(/^$|^\s+$/,'') - return code + + code end end # Obfu diff --git a/lib/rex/exploitation/powershell/parser.rb b/lib/rex/exploitation/powershell/parser.rb index fc25aa404c..7564d56082 100644 --- a/lib/rex/exploitation/powershell/parser.rb +++ b/lib/rex/exploitation/powershell/parser.rb @@ -1,8 +1,5 @@ # -*- coding: binary -*- -require 'zlib' -require 'rex/text' - module Rex module Exploitation @@ -69,26 +66,35 @@ module Powershell # # Get variable names from code, removes reserved names from return # + # @return [Array] variable names def get_var_names - our_vars = code.scan(/\$[a-zA-Z\-\_]+/).uniq.flatten.map(&:strip) - return our_vars.select {|v| !RESERVED_VARIABLE_NAMES.include?(v.downcase)} + our_vars = code.scan(/\$[a-zA-Z\-\_0-9]+/).uniq.flatten.map(&:strip) + our_vars.select {|v| !RESERVED_VARIABLE_NAMES.include?(v.downcase)} end # # Get function names from code # + # @return [Array] function names def get_func_names - return code.scan(/function\s([a-zA-Z\-\_]+)/).uniq.flatten + code.scan(/function\s([a-zA-Z\-\_0-9]+)/).uniq.flatten end + # # Attempt to find string literals in PSH expression + # + # @return [Array] string literals def get_string_literals - code.scan(/@"(.*)"@|@'(.*)'@/) + code.scan(/@"(.+?)"@|@'(.+?)'@/m) end # # Scan code and return matches with index # + # @param str [String] string to match in code + # @param source [String] source code to match, defaults to @code + # + # @return [Array[String,Integer]] matched items with index def scan_with_index(str,source=code) ::Enumerator.new do |y| source.scan(str) do @@ -100,6 +106,9 @@ module Powershell # # Return matching bracket type # + # @param char [String] opening bracket character + # + # @return [String] matching closing bracket def match_start(char) case char when '{' @@ -110,6 +119,8 @@ module Powershell ']' when '<' '>' + else + raise ArgumentError, "Unknown starting bracket" end end @@ -120,7 +131,16 @@ module Powershell # Once the balanced matching bracket is found, all script content # between idx and the index of the matching bracket is returned # + # @param idx [Integer] index of opening bracket + # + # @return [String] content between matching brackets def block_extract(idx) + raise ArgumentError unless idx + + if idx < 0 || idx >= code.length + raise ArgumentError, "Invalid index" + end + start = code[idx] stop = match_start(start) delims = scan_with_index(/#{Regexp.escape(start)}|#{Regexp.escape(stop)}/,code[idx+1..-1]) @@ -128,19 +148,36 @@ module Powershell c = 1 sidx = nil # Go through delims till we balance, get idx - while not c == 0 and x = delims.shift do + while ((c != 0) && (x = delims.shift)) do sidx = x[1] x[0] == stop ? c -=1 : c+=1 end - return code[idx..sidx] + + code[idx..sidx] end + # + # Extract a block of function code + # + # @param func_name [String] function name + # @param delete [Boolean] delete the function from the code + # + # @return [String] function block def get_func(func_name, delete = false) start = code.index(func_name) + + return nil unless start + idx = code[start..-1].index('{') + start func_txt = block_extract(idx) - code.delete(ftxt) if delete - return Function.new(func_name,func_txt) + + if delete + delete_code = code[0..idx] + delete_code << code[(idx+func_txt.length)..-1] + @code = delete_code + end + + Function.new(func_name,func_txt) end end # Parser diff --git a/lib/rex/exploitation/powershell/script.rb b/lib/rex/exploitation/powershell/script.rb index 9331f19fde..b60c667abc 100644 --- a/lib/rex/exploitation/powershell/script.rb +++ b/lib/rex/exploitation/powershell/script.rb @@ -1,7 +1,6 @@ # -*- coding: binary -*- -require 'zlib' -require 'rex/text' +require 'rex' module Rex module Exploitation @@ -36,6 +35,8 @@ module Powershell def initialize(code) @code = '' + @rig = Rex::RandomIdentifierGenerator.new() + begin # Open code file for reading fd = ::File.new(code, 'rb') diff --git a/spec/lib/rex/exploitation/powershell/obfu_spec.rb b/spec/lib/rex/exploitation/powershell/obfu_spec.rb new file mode 100644 index 0000000000..7dded636d8 --- /dev/null +++ b/spec/lib/rex/exploitation/powershell/obfu_spec.rb @@ -0,0 +1,127 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'rex/exploitation/powershell' + +describe Rex::Exploitation::Powershell::Obfu do + + let(:example_script_without_literal) do +""" +function Find-4624Logons +{ + + if (-not ($NewLogonAccountDomain -cmatch \"NT\\sAUTHORITY\" -or $NewLogonAccountDomain -cmatch \"Window\\sManager\")) + { + $Key = $AccountName + $AccountDomain + $NewLogonAccountName + $NewLogonAccountDomain + $LogonType + $WorkstationName + $SourceNetworkAddress + $SourcePort + if (-not $ReturnInfo.ContainsKey($Key)) + { + $Properties = @{ + LogType = 4624 + LogSource = \"Security\" + SourceAccountName = $AccountName + SourceDomainName = $AccountDomain + NewLogonAccountName = $NewLogonAccountName + NewLogonAccountDomain = $NewLogonAccountDomain + LogonType = $LogonType + WorkstationName = $WorkstationName + SourceNetworkAddress = $SourceNetworkAddress + SourcePort = $SourcePort + Count = 1 + Times = @($Logon.TimeGenerated) + } + + $ResultObj = New-Object PSObject -Property $Properties + $ReturnInfo.Add($Key, $ResultObj) + } + else + { + $ReturnInfo[$Key].Count++ + $ReturnInfo[$Key].Times += ,$Logon.TimeGenerated + } + } + } +}""" + + end + + let(:example_script) do +""" +function Find-4624Logons +{ + $some_literal = @\" + using System; + using System.Runtime.InteropServices; + namespace $kernel32 { + public class func { + [Flags] public enum AllocationType { Commit = 0x1000, Reserve = 0x2000 } + [Flags] public enum MemoryProtection { ExecuteReadWrite = 0x40 } + [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 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); + } + } +\"@ + if (-not ($NewLogonAccountDomain -cmatch \"NT\\sAUTHORITY\" -or $NewLogonAccountDomain -cmatch \"Window\\sManager\")) + { + $Key = $AccountName + $AccountDomain + $NewLogonAccountName + $NewLogonAccountDomain + $LogonType + $WorkstationName + $SourceNetworkAddress + $SourcePort + if (-not $ReturnInfo.ContainsKey($Key)) + { + $Properties = @{ + LogType = 4624 + LogSource = \"Security\" + SourceAccountName = $AccountName + SourceDomainName = $AccountDomain + NewLogonAccountName = $NewLogonAccountName + NewLogonAccountDomain = $NewLogonAccountDomain + LogonType = $LogonType + WorkstationName = $WorkstationName + SourceNetworkAddress = $SourceNetworkAddress + SourcePort = $SourcePort + Count = 1 + Times = @($Logon.TimeGenerated) + } + $literal2 = @\"parp\"@ + $ResultObj = New-Object PSObject -Property $Properties + $ReturnInfo.Add($Key, $ResultObj) + } + else + { + $ReturnInfo[$Key].Count++ + $ReturnInfo[$Key].Times += ,$Logon.TimeGenerated + } + } + } +}""" + + end + + let(:subject) do + Rex::Exploitation::Powershell::Script.new(example_script) + end + + let(:subject_no_literal) do + Rex::Exploitation::Powershell::Script.new(example_script_without_literal) + end + + describe "::sub_map_generate" do + it 'should return some unique variable names' do + map = subject.sub_map_generate(['blah','parp']) + map.should be + map.should be_kind_of Hash + map.empty?.should be_false + map.should eq map.uniq + end + + it 'should not match upper or lowercase reserved names' do + initial_vars = subject.get_var_names + subject.code << "\r\n$SHELLID" + subject.code << "\r\n$ShellId" + subject.code << "\r\n$shellid" + after_vars = subject.get_var_names + initial_vars.should eq after_vars + end + end + +end + diff --git a/spec/lib/rex/exploitation/powershell/parser_spec.rb b/spec/lib/rex/exploitation/powershell/parser_spec.rb new file mode 100644 index 0000000000..5705c63679 --- /dev/null +++ b/spec/lib/rex/exploitation/powershell/parser_spec.rb @@ -0,0 +1,159 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'rex/exploitation/powershell' + +describe Rex::Exploitation::Powershell::Parser do + + let(:example_script) do +""" +function Find-4624Logons +{ + $some_literal = @\" + using System; + using System.Runtime.InteropServices; + namespace $kernel32 { + public class func { + [Flags] public enum AllocationType { Commit = 0x1000, Reserve = 0x2000 } + [Flags] public enum MemoryProtection { ExecuteReadWrite = 0x40 } + [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 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); + } + } +\"@ + if (-not ($NewLogonAccountDomain -cmatch \"NT\\sAUTHORITY\" -or $NewLogonAccountDomain -cmatch \"Window\\sManager\")) + { + $Key = $AccountName + $AccountDomain + $NewLogonAccountName + $NewLogonAccountDomain + $LogonType + $WorkstationName + $SourceNetworkAddress + $SourcePort + if (-not $ReturnInfo.ContainsKey($Key)) + { + $Properties = @{ + LogType = 4624 + LogSource = \"Security\" + SourceAccountName = $AccountName + SourceDomainName = $AccountDomain + NewLogonAccountName = $NewLogonAccountName + NewLogonAccountDomain = $NewLogonAccountDomain + LogonType = $LogonType + WorkstationName = $WorkstationName + SourceNetworkAddress = $SourceNetworkAddress + SourcePort = $SourcePort + Count = 1 + Times = @($Logon.TimeGenerated) + } + $literal2 = @\"parp\"@ + $ResultObj = New-Object PSObject -Property $Properties + $ReturnInfo.Add($Key, $ResultObj) + } + else + { + $ReturnInfo[$Key].Count++ + $ReturnInfo[$Key].Times += ,$Logon.TimeGenerated + } + } + } +}""" + + end + + let(:subject) do + Rex::Exploitation::Powershell::Script.new(example_script) + end + + describe "::get_var_names" do + it 'should return some variable names' do + vars = subject.get_var_names + vars.should be + vars.should be_kind_of Array + vars.length.should be > 0 + vars.include?('$ResultObj').should be_true + end + + it 'should not match upper or lowercase reserved names' do + initial_vars = subject.get_var_names + subject.code << "\r\n$SHELLID" + subject.code << "\r\n$ShellId" + subject.code << "\r\n$shellid" + after_vars = subject.get_var_names + initial_vars.should eq after_vars + end + end + + describe "::get_func_names" do + it 'should return some function names' do + funcs = subject.get_func_names + funcs.should be + funcs.should be_kind_of Array + funcs.length.should be > 0 + funcs.include?('Find-4624Logons').should be_true + end + end + + describe "::get_string_literals" do + it 'should return some string literals' do + literals = subject.get_string_literals + literals.should be + literals.should be_kind_of Array + literals.length.should be > 0 + literals[0].include?('parp').should be_false + end + end + + describe "::scan_with_index" do + it 'should scan code and return the items with an index' do + scan = subject.scan_with_index('DllImport') + scan.should be + scan.should be_kind_of Array + scan.length.should be > 0 + scan[0].should be_kind_of Array + scan[0][0].should be_kind_of String + scan[0][1].should be_kind_of Integer + end + end + + describe "::match_start" do + it 'should match the correct brackets' do + subject.match_start('{').should eq '}' + subject.match_start('(').should eq ')' + subject.match_start('[').should eq ']' + subject.match_start('<').should eq '>' + expect { subject.match_start('p') }.to raise_exception(ArgumentError) + end + end + + describe "::block_extract" do + it 'should extract a block between brackets given an index' do + idx = subject.code.index('{') + block = subject.block_extract(idx) + block.should be + block.should be_kind_of String + end + + it 'should raise a runtime error if given an invalid index' do + expect { subject.block_extract(nil) }.to raise_error(ArgumentError) + expect { subject.block_extract(-1) }.to raise_error(ArgumentError) + expect { subject.block_extract(subject.code.length) }.to raise_error(ArgumentError) + expect { subject.block_extract(59) }.to raise_error(ArgumentError) + end + end + + describe "::get_func" do + it 'should extract a function from the code' do + function = subject.get_func('Find-4624Logons') + function.should be + function.should be_kind_of Rex::Exploitation::Powershell::Function + end + + it 'should return nil if function doesnt exist' do + function = subject.get_func(Rex::Text.rand_text_alpha(5)) + function.should be_nil + end + + it 'should delete the function if delete is true' do + function = subject.get_func('Find-4624Logons', true) + subject.code.include?('DllImport').should be_false + end + end +end +