# -*- coding: binary -*-
# Copyright (c) 2010, patrickHVE@googlemail.com
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * The names of the author may not be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL patrickHVE@googlemail.com BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

require 'pp'
require 'enumerator'
require 'rex/post/meterpreter/extensions/stdapi/railgun/api_constants'
require 'rex/post/meterpreter/extensions/stdapi/railgun/tlv'
require 'rex/post/meterpreter/extensions/stdapi/railgun/dll_helper'
require 'rex/post/meterpreter/extensions/stdapi/railgun/buffer_item'

module Rex
module Post
module Meterpreter
module Extensions
module Stdapi
module Railgun

# A easier way to call multiple functions in a single request
class MultiCaller

    include DLLHelper

    def initialize( client, parent, win_consts )
      @parent = parent
      @client = client

      # needed by DLL helper
      @win_consts = win_consts

      if( @client.platform =~ /x64/i )
        @native = 'Q<'
      else
        @native = 'V'
      end
    end

    def call(functions)

      request = Packet.create_request('stdapi_railgun_api_multi')
      function_results = []
      layouts          = []
      functions.each do |f|
        dll_name,funcname,args = f
        dll_host = @parent.get_dll( dll_name )

        if not dll_host
          raise "DLL #{dll_name} has not been loaded"
        end

        function = dll_host.functions[funcname]
        if not function
          raise "DLL #{dll_name} function #{funcname} has not been defined"
        end

        raise "#{function.params.length} arguments expected. #{args.length} arguments provided." unless args.length == function.params.length
        #puts "process_function_call(function.windows_name,#{PP.pp(args, "")})"

        # We transmit the immediate stack and three heap-buffers:
        # in, inout and out. The reason behind the separation is bandwidth.
        # We don't want to transmit uninitialized data in or no-longer-needed data out.

        # out-only-buffers that are ONLY transmitted on the way BACK
        out_only_layout = {} # paramName => BufferItem
        out_only_size_bytes = 0
        #puts " assembling out-only buffer"
        function.params.each_with_index do |param_desc, param_idx|
          #puts " processing #{param_desc[1]}"

          # Special case:
          # The user can choose to supply a Null pointer instead of a buffer
          # in this case we don't need space in any heap buffer
          if param_desc[0][0,1] == 'P' # type is a pointer
            if args[param_idx] == nil
              next
            end
          end

          # we care only about out-only buffers
          if param_desc[2] == "out"
            raise "error in param #{param_desc[1]}: Out-only buffers must be described by a number indicating their size in bytes " unless args[param_idx].class == Fixnum
            buffer_size = args[param_idx]
            # bump up the size for an x64 pointer
            if( @native == 'Q<' and buffer_size == 4 )
              args[param_idx] = 8
              buffer_size = args[param_idx]
            end

            if( @native == 'Q<' )
              raise "Please pass 8 for 'out' PDWORDS, since they require a buffer of size 8" unless buffer_size == 8
            elsif( @native == 'V' )
              raise "Please pass 4 for 'out' PDWORDS, since they require a buffer of size 4" unless buffer_size == 4
            end

            out_only_layout[param_desc[1]] = BufferItem.new(param_idx, out_only_size_bytes, buffer_size, param_desc[0])
            out_only_size_bytes += buffer_size
          end
        end

        tmp = assemble_buffer("in", function, args)
        in_only_layout = tmp[0]
        in_only_buffer = tmp[1]

        tmp = assemble_buffer("inout", function, args)
        inout_layout = tmp[0]
        inout_buffer = tmp[1]


        # now we build the stack
        # every stack dword will be described by two dwords:
        # first dword describes second dword:
        #	0 - literal,
        #	1 = relative to in-only buffer
        #	2 = relative to out-only buffer
        #	3 = relative to inout buffer

        # (literal numbers and pointers to buffers we have created)
        literal_pairs_blob = ""
        #puts " assembling literal stack"
        function.params.each_with_index do |param_desc, param_idx|
          #puts "  processing (#{param_desc[0]}, #{param_desc[1]}, #{param_desc[2]})"
          buffer = nil
          # is it a pointer to a buffer on our stack
          if ["PDWORD", "PWCHAR", "PCHAR", "PBLOB"].include? param_desc[0]
            #puts "   pointer"
            if args[param_idx] == nil # null pointer?
              buffer = [0].pack(@native) # type: DWORD  (so the dll does not rebase it)
              buffer += [0].pack(@native) # value: 0
            elsif param_desc[2] == "in"
              buffer = [1].pack(@native)
              buffer += [in_only_layout[param_desc[1]].addr].pack(@native)
            elsif param_desc[2] == "out"
              buffer = [2].pack(@native)
              buffer += [out_only_layout[param_desc[1]].addr].pack(@native)
            elsif param_desc[2] == "inout"
              buffer = [3].pack(@native)
              buffer += [inout_layout[param_desc[1]].addr].pack(@native)
            else
              raise "unexpected direction"
            end
          else
            #puts "   not a pointer"
            # it's not a pointer
            buffer = [0].pack(@native)
            case param_desc[0]
              when "LPVOID", "HANDLE"
                num     = param_to_number(args[param_idx])
                buffer += [num].pack(@native)
              when "DWORD"
                num     = param_to_number(args[param_idx])
                buffer += [num % 4294967296].pack(@native)
              when "WORD"
                num     = param_to_number(args[param_idx])
                buffer += [num % 65536].pack(@native)
              when "BYTE"
                num     = param_to_number(args[param_idx])
                buffer += [num % 256].pack(@native)
              when "BOOL"
                case args[param_idx]
                  when true
                    buffer += [1].pack('V')
                  when false
                    buffer += [0].pack('V')
                  else
                    raise "param #{param_desc[1]}: true or false expected"
                end
              else
                raise "unexpected type for param #{param_desc[1]}"
            end
          end

          #puts "   adding pair to blob"
          literal_pairs_blob += buffer
          #puts "   buffer size %X" % buffer.length
          #puts "   blob size so far: %X" % literal_pairs_blob.length
        end

        #puts "\n\nsending Stuff to meterpreter"

        group = Rex::Post::Meterpreter::GroupTlv.new(TLV_TYPE_RAILGUN_MULTI_GROUP)
        group.add_tlv(TLV_TYPE_RAILGUN_SIZE_OUT, out_only_size_bytes)
        group.add_tlv(TLV_TYPE_RAILGUN_STACKBLOB, literal_pairs_blob)
        group.add_tlv(TLV_TYPE_RAILGUN_BUFFERBLOB_IN, in_only_buffer)
        group.add_tlv(TLV_TYPE_RAILGUN_BUFFERBLOB_INOUT, inout_buffer)
        group.add_tlv(TLV_TYPE_RAILGUN_DLLNAME, dll_name )
        group.add_tlv(TLV_TYPE_RAILGUN_FUNCNAME, function.windows_name)
        request.tlvs << group

        layouts << [inout_layout, out_only_layout]
      end

      call_results = []
      res = @client.send_request(request)
      res.each(TLV_TYPE_RAILGUN_MULTI_GROUP) do |val|
        call_results << val
      end

      functions.each do |f|
        dll_name,funcname,args = f
        dll_host = @parent.get_dll( dll_name )
        function = dll_host.functions[funcname]
        response = call_results.shift
        inout_layout, out_only_layout = layouts.shift

        rec_inout_buffers = response.get_tlv_value(TLV_TYPE_RAILGUN_BACK_BUFFERBLOB_INOUT)
        rec_out_only_buffers = response.get_tlv_value(TLV_TYPE_RAILGUN_BACK_BUFFERBLOB_OUT)
        rec_return_value = response.get_tlv_value(TLV_TYPE_RAILGUN_BACK_RET)
        rec_last_error = response.get_tlv_value(TLV_TYPE_RAILGUN_BACK_ERR)
        rec_err_msg = response.get_tlv_value(TLV_TYPE_RAILGUN_BACK_MSG)

        # Error messages come back with trailing CRLF, so strip it out
        # if we do get a message.
        rec_err_msg.strip! if not rec_err_msg.nil?

        # The hash the function returns
        return_hash = {
          "GetLastError" => rec_last_error,
          "ErrorMessage" => rec_err_msg
        }

        #process return value
        case function.return_type
          when "LPVOID", "HANDLE"
            if( @native == 'Q<' )
              return_hash["return"] = rec_return_value
            else
              return_hash["return"] = rec_return_value % 4294967296
            end
          when "DWORD"
            return_hash["return"] = rec_return_value % 4294967296
          when "WORD"
            return_hash["return"] = rec_return_value % 65536
          when "BYTE"
            return_hash["return"] = rec_return_value % 256
          when "BOOL"
            return_hash["return"] = (rec_return_value != 0)
          when "VOID"
            return_hash["return"] = nil
          else
            raise "unexpected return type: #{function.return_type}"
        end
        #puts return_hash
        #puts "out_only_layout:"
        #puts out_only_layout


        # process out-only buffers
        #puts "processing out-only buffers:"
        out_only_layout.each_pair do |param_name, buffer_item|
          #puts "   #{param_name}"
          buffer = rec_out_only_buffers[buffer_item.addr, buffer_item.length_in_bytes]
          case buffer_item.datatype
            when "PDWORD"
              return_hash[param_name] = buffer.unpack('V')[0]
            when "PCHAR"
              return_hash[param_name] = asciiz_to_str(buffer)
            when "PWCHAR"
              return_hash[param_name] = uniz_to_str(buffer)
            when "PBLOB"
              return_hash[param_name] = buffer
            else
              raise "unexpected type in out-only buffer of #{param_name}: #{buffer_item.datatype}"
          end
        end
        #puts return_hash

        # process in-out buffers
        #puts "processing in-out buffers:"
        inout_layout.each_pair do |param_name, buffer_item|
          #puts "   #{param_name}"
          buffer = rec_inout_buffers[buffer_item.addr, buffer_item.length_in_bytes]
          case buffer_item.datatype
            when "PDWORD"
              return_hash[param_name] = buffer.unpack('V')[0]
            when "PCHAR"
              return_hash[param_name] = asciiz_to_str(buffer)
            when "PWCHAR"
              return_hash[param_name] = uniz_to_str(buffer)
            when "PBLOB"
              return_hash[param_name] = buffer
            else
              raise "unexpected type in in-out-buffer of #{param_name}: #{buffer_item.datatype}"
          end
        end
        #puts return_hash
        #puts "finished"

        function_results << return_hash
      end
      function_results
    end
    # process_multi_function_call

  protected

end # MultiCall

end; end; end; end; end; end
