ronin-rb/ronin-support

View on GitHub
lib/ronin/support/encoding/powershell.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true
#
# Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-support is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-support is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-support.  If not, see <https://www.gnu.org/licenses/>.
#

require 'strscan'

module Ronin
  module Support
    class Encoding < ::Encoding
      #
      # Contains methods for encoding/decoding escaping/unescaping PowerShell
      # data.
      #
      # ## Core-Ext Methods
      #
      # * {Integer#powershell_encode}
      # * {Integer#powershell_escape}
      # * {String#powershell_escape}
      # * {String#powershell_unescape}
      # * {String#powershell_encode}
      # * {String#powershell_decode}
      # * {String#powershell_string}
      # * {String#powershell_unquote}
      #
      # @api public
      #
      module PowerShell
        # Special PowerShell bytes and their escaped Strings.
        ESCAPE_BYTES = {
          0x00 => "`0",
          0x07 => "`a",
          0x08 => "`b",
          0x09 => "`t",
          0x0a => "`n",
          0x0b => "`v",
          0x0c => "`f",
          0x0d => "`r",
          0x22 => '`"',
          0x23 => "`#",
          0x27 => "`'",
          0x5c => "\\\\", # \\
          0x60 => "``"
        }

        #
        # Encodes the byte as a PowerShell character.
        #
        # @param [Integer] byte
        #   The byte to escape.
        #
        # @return [String]
        #   The encoded PowerShell character.
        #
        # @raise [RangeError]
        #   The integer value is negative.
        #
        # @example
        #   Encoding::PowerShell.encode_byte(0x41)
        #   # => "[char]0x41"
        #   Encoding::PowerShell.encode_byte(0x0a)
        #   # => "`n"
        #
        # @example Encoding unicode characters:
        #   Encoding::PowerShell.encode_byte(1001)
        #   # => "`u{1001}"
        #
        def self.encode_byte(byte)
          if byte >= 0x00 && byte <= 0xff
            "$([char]0x%.2x)" % byte
          elsif byte > 0xff
            "$([char]0x%x)" % byte
          else
            raise(RangeError,"#{byte.inspect} out of char range")
          end
        end

        #
        # Escapes the byte as a PowerShell character.
        #
        # @param [Integer] byte
        #   The byte to escape.
        #
        # @return [String]
        #   The escaped PowerShell character.
        #
        # @raise [RangeError]
        #   The integer value is negative.
        #
        # @example
        #   Encoding::PowerShell.escape_byte(0x41)
        #   # => "A"
        #   Encoding::PowerShell.escape_byte(0x08)
        #   # => "`b"
        #   Encoding::PowerShell.escape_byte(0xff)
        #   # => "[char]0xff"
        #
        # @example Escaping unicode characters:
        #   Encoding::PowerShell.escape_byte(1001)
        #   # => "`u{1001}"
        #
        def self.escape_byte(byte)
          if byte >= 0x00 && byte <= 0xff
            ESCAPE_BYTES.fetch(byte) do
              if byte >= 0x20 && byte <= 0x7e
                byte.chr
              else
                encode_byte(byte)
              end
            end
          else
            encode_byte(byte)
          end
        end

        # PowerShell characters that must be grave-accent escaped.
        #
        # @see https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters?view=powershell-7.2
        BACKSLASHED_CHARS = {
          '0'  => "\0",
          'a'  => "\a",
          'b'  => "\b",
          't'  => "\t",
          'n'  => "\n",
          'v'  => "\v",
          'f'  => "\f",
          'r'  => "\r",
          '"'  => '"',
          '#'  => '#',
          "'"  => "'",
          "`"  => "`"
        }

        #
        # [PowerShell escapes][1] the special characters in the data.
        #
        # [1]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters?view=powershell-7.2
        #
        # @param [String] data
        #   The data to PowerShell escape.
        #
        # @return [String]
        #   The PowerShell escaped string.
        #
        # @example
        #   Encoding::PowerShell.escape("hello\nworld")
        #   # => "hello`nworld"
        #
        def self.escape(data)
          escaped = String.new

          if data.valid_encoding?
            data.each_codepoint do |codepoint|
              escaped << escape_byte(codepoint)
            end
          else
            data.each_byte do |byte|
              escaped << escape_byte(byte)
            end
          end

          return escaped
        end

        #
        # [PowerShell unescapes][1] the characters in the data.
        #
        # [1]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters?view=powershell-7.2
        #
        # @param [String] data
        #   The PowerShell encoded data to unescape.
        #
        # @return [String]
        #   The unescaped string.
        #
        # @example
        #   Encoding::PowerShell.unescape("hello`nworld")
        #   # => "hello\nworld"
        #
        def self.unescape(data)
          unescaped = String.new(encoding: Encoding::UTF_8)
          scanner   = StringScanner.new(data)

          until scanner.eos?
            unescaped << if (backslash_char = scanner.scan(/`[0abetnvfr]/)) # `c
                           BACKSLASHED_CHARS[backslash_char[1,1]]
                         elsif (hex_char     = scanner.scan(/\$\(\[char\]0x[0-9a-fA-F]{1,2}\)/)) # [char]0xXX
                           hex_char[10..-2].to_i(16).chr
                         elsif (hex_char     = scanner.scan(/\$\(\[char\]0x[0-9a-fA-F]{3,}\)/)) # [char]0xXX
                           hex_char[10..-2].to_i(16).chr(Encoding::UTF_8)
                         elsif (unicode_char = scanner.scan(/`u\{[0-9a-fA-F]+\}/)) # `u{XXXX}'
                           unicode_char[3..-2].to_i(16).chr(Encoding::UTF_8)
                         else
                           scanner.getch
                         end
          end

          return unescaped
        end

        #
        # [PowerShell encodes][1] every character in the data.
        #
        # [1]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters?view=powershell-7.2
        #
        # @param [String] data
        #   The data to encode.
        #
        # @return [String]
        #   The PowerShell encoded String.
        #
        # @example
        #   Encoding::PowerShell.encode("hello world")
        #   # => "$([char]0x68)$([char]0x65)$([char]0x6c)$([char]0x6c)$([char]0x6f)$([char]0x20)$([char]0x77)$([char]0x6f)$([char]0x72)$([char]0x6c)$([char]0x64)"
        #
        def self.encode(data)
          encoded = String.new

          if data.valid_encoding?
            data.each_codepoint do |codepoint|
              encoded << encode_byte(codepoint)
            end
          else
            data.each_byte do |byte|
              encoded << encode_byte(byte)
            end
          end

          return encoded
        end

        #
        # Alias for {unescape}.
        #
        # @param [String] data
        #   The PowerShell encoded data to unescape.
        #
        # @return [String]
        #   The unescaped string.
        #
        # @see unescape
        #
        def self.decode(data)
          unescape(data)
        end

        #
        # Converts the data into a double-quoted PowerShell escaped String.
        #
        # @param [String] data
        #   the data to escape and quote.
        #
        # @return [String]
        #   The quoted and escaped PowerShell string.
        #
        # @example
        #   Encoding::PowerShell.quote("hello\nworld")
        #   # => "\"hello`nworld\""
        #
        def self.quote(data)
          "\"#{escape(data)}\""
        end

        #
        # Removes the quotes an unescapes a PowerShell string.
        #
        # @param [String] data
        #   The PowerShell string to unquote.
        #
        # @return [String]
        #   The un-quoted String if the String begins and ends with quotes, or
        #   the same String if it is not quoted.
        #
        # @example
        #   Encoding::PowerShell.unquote("\"hello`nworld\"")
        #   # => "hello\nworld"
        #   Encoding::PowerShell.unquote("'hello''world'")
        #   # => "hello'world"
        #
        def self.unquote(data)
          if (data[0] == '"' && data[-1] == '"')
            unescape(data[1..-2])
          elsif (data[0] == "'" && data[-1] == "'")
            data[1..-2].gsub("''","'")
          else
            data
          end
        end
      end
    end
  end
end

require 'ronin/support/encoding/powershell/core_ext'