333 lines
10 KiB
Ruby
333 lines
10 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Encoder
|
|
Rank = ManualRanking
|
|
|
|
ASM_SUBESP20 = "\x83\xEC\x20"
|
|
|
|
SET_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
|
SET_SYM = '!@#$%^&*()_+\\-=[]{};\'":<>,.?/|~'
|
|
SET_NUM = '0123456789'
|
|
SET_FILESYM = '()_+-=\\/.,[]{}@!$%^&='
|
|
|
|
CHAR_SET_ALPHA = SET_ALPHA + SET_SYM
|
|
CHAR_SET_ALPHANUM = SET_ALPHA + SET_NUM + SET_SYM
|
|
CHAR_SET_FILEPATH = SET_ALPHA + SET_NUM + SET_FILESYM
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'Sub Encoder (optimised)',
|
|
'Description' => %q{
|
|
Encodes a payload using a series of SUB instructions and writing the
|
|
encoded value to ESP. This concept is based on the known SUB encoding
|
|
approach that is widely used to manually encode payloads with very
|
|
restricted allowed character sets. It will not reset EAX to zero unless
|
|
absolutely necessary, which helps reduce the payload by 10 bytes for
|
|
every 4-byte chunk. ADD support hasn't been included as the SUB
|
|
instruction is more likely to avoid bad characters anyway.
|
|
|
|
The payload requires a base register to work off which gives the start
|
|
location of the encoder payload in memory. If not specified, it defaults
|
|
to ESP. If the given register doesn't point exactly to the start of the
|
|
payload then an offset value is also required.
|
|
|
|
Note: Due to the fact that many payloads use the FSTENV approach to
|
|
get the current location in memory there is an option to protect the
|
|
start of the payload by setting the 'OverwriteProtect' flag to true.
|
|
This adds 3-bytes to the start of the payload to bump ESP by 32 bytes
|
|
so that it's clear of the top of the payload.
|
|
},
|
|
'Author' => 'OJ Reeves <oj[at]buffered.io>',
|
|
'Arch' => ARCH_X86,
|
|
'License' => MSF_LICENSE,
|
|
'Decoder' => { 'BlockSize' => 4 }
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptString.new( 'ValidCharSet', [ false, "Specify a known set of valid chars (ALPHA, ALPHANUM, FILEPATH)" ]),
|
|
OptBool.new( 'OverwriteProtect', [ false, "Indicate if the encoded payload requires protection against being overwritten", false])
|
|
],
|
|
self.class)
|
|
end
|
|
|
|
#
|
|
# Conver the shellcode into a set of 4-byte chunks that can be
|
|
# encoding while making sure it is 4-byte aligned.
|
|
#
|
|
def prepare_shellcode(sc, protect_payload)
|
|
# first instructions need to be ESP offsetting if the payload
|
|
# needs to be protected
|
|
sc = ASM_SUBESP20 + sc if protect_payload == true
|
|
|
|
# first of all we need to 4-byte align the payload if it
|
|
# isn't already aligned, by prepending NOPs.
|
|
rem = sc.length % 4
|
|
sc = @asm['NOP'] * (4 - rem) + sc if rem != 0
|
|
|
|
# next we break it up into 4-byte chunks, convert to an unsigned
|
|
# int block so calculations are easy
|
|
chunks = []
|
|
sc = sc.bytes.to_a
|
|
while sc.length > 0
|
|
chunk = sc.shift + (sc.shift << 8) + (sc.shift << 16) + (sc.shift << 24)
|
|
chunks << chunk
|
|
end
|
|
|
|
# return the array in reverse as this is the order the instructions
|
|
# will be written to the stack.
|
|
chunks.reverse
|
|
end
|
|
|
|
#
|
|
# From the list of characters given, find two bytes that when
|
|
# ANDed together result in 0. Returns nil if not found.
|
|
#
|
|
def find_opposite_bytes(list)
|
|
list.each_char do |b1|
|
|
list.each_char do |b2|
|
|
if b1.ord & b2.ord == 0
|
|
return (b1 * 4), (b2 * 4)
|
|
end
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
#
|
|
# Entry point to the decoder.
|
|
#
|
|
def decoder_stub(state)
|
|
return state.decoder_stub if state.decoder_stub
|
|
|
|
# configure our instruction dictionary
|
|
@asm = {
|
|
'NOP' => "\x90",
|
|
'AND' => { 'EAX' => "\x25" },
|
|
'SUB' => { 'EAX' => "\x2D" },
|
|
'PUSH' => {
|
|
'EBP' => "\x55", 'ESP' => "\x54",
|
|
'EAX' => "\x50", 'EBX' => "\x53",
|
|
'ECX' => "\x51", 'EDX' => "\x52",
|
|
'EDI' => "\x57", 'ESI' => "\x56"
|
|
},
|
|
'POP' => { 'ESP' => "\x5C", 'EAX' => "\x58", }
|
|
}
|
|
|
|
# set up our base register, defaulting to ESP if not specified
|
|
@base_reg = (datastore['BufferRegister'] || 'ESP').upcase
|
|
|
|
# determine the required bytes
|
|
@required_bytes =
|
|
@asm['AND']['EAX'] +
|
|
@asm['SUB']['EAX'] +
|
|
@asm['PUSH']['EAX'] +
|
|
@asm['POP']['ESP'] +
|
|
@asm['POP']['EAX'] +
|
|
@asm['PUSH'][@base_reg]
|
|
|
|
# generate a sorted list of valid characters
|
|
char_set = ""
|
|
case (datastore['ValidCharSet'] || "").upcase
|
|
when 'ALPHA'
|
|
char_set = CHAR_SET_ALPHA
|
|
when 'ALPHANUM'
|
|
char_set = CHAR_SET_ALPHANUM
|
|
when 'FILEPATH'
|
|
char_set = CHAR_SET_FILEPATH
|
|
else
|
|
for i in 0 .. 255
|
|
char_set += i.chr.to_s
|
|
end
|
|
end
|
|
|
|
# remove any bad chars and populate our valid chars array.
|
|
@valid_chars = ""
|
|
char_set.each_char do |c|
|
|
@valid_chars << c.to_s unless state.badchars.include?(c.to_s)
|
|
end
|
|
|
|
# we need the valid chars sorted because of the algorithm we use
|
|
@valid_chars = @valid_chars.chars.sort.join
|
|
@valid_bytes = @valid_chars.bytes.to_a
|
|
|
|
all_bytes_valid = @required_bytes.bytes.reduce(true) { |a, byte| a && @valid_bytes.include?(byte) }
|
|
|
|
# determine if we have any invalid characters that we rely on.
|
|
unless all_bytes_valid
|
|
raise EncodingError, "Bad character set contains characters that are required for this encoder to function."
|
|
end
|
|
|
|
unless @asm['PUSH'][@base_reg]
|
|
raise EncodingError, "Invalid base register"
|
|
end
|
|
|
|
# get the offset from the specified base register, or default to zero if not specifed
|
|
reg_offset = (datastore['BufferOffset'] || 0).to_i
|
|
|
|
# calculate two opposing values which we can use for zeroing out EAX
|
|
@clear1, @clear2 = find_opposite_bytes(@valid_chars)
|
|
|
|
# if we can't then we bomb, because we know we need to clear out EAX at least once
|
|
unless @clear1
|
|
raise EncodingError, "Unable to find AND-able chars resulting 0 in the valid character set."
|
|
end
|
|
|
|
# with everything set up, we can now call the encoding routine
|
|
state.decoder_stub = encode_payload(state.buf, reg_offset, datastore['OverwriteProtect'])
|
|
|
|
state.buf = ""
|
|
state.decoder_stub
|
|
end
|
|
|
|
#
|
|
# Determine the bytes, if any, that will result in the given chunk
|
|
# being decoded using SUB instructions from the previous EAX value
|
|
#
|
|
def sub_3(chunk, previous)
|
|
carry = 0
|
|
shift = 0
|
|
target = previous - chunk
|
|
sum = [0, 0, 0]
|
|
|
|
4.times do |idx|
|
|
b = (target >> shift) & 0xFF
|
|
lo = md = hi = 0
|
|
|
|
# keep going through the character list under the "lowest" valid
|
|
# becomes too high (ie. we run out)
|
|
while lo < @valid_bytes.length
|
|
# get the total of the three current bytes, including the carry from
|
|
# the previous calculation
|
|
total = @valid_bytes[lo] + @valid_bytes[md] + @valid_bytes[hi] + carry
|
|
|
|
# if we matched a byte...
|
|
if (total & 0xFF) == b
|
|
# store the carry for the next calculation
|
|
carry = (total >> 8) & 0xFF
|
|
|
|
# store the values in the respective locations
|
|
sum[2] |= @valid_bytes[lo] << shift
|
|
sum[1] |= @valid_bytes[md] << shift
|
|
sum[0] |= @valid_bytes[hi] << shift
|
|
break
|
|
end
|
|
|
|
hi += 1
|
|
if hi >= @valid_bytes.length
|
|
md += 1
|
|
hi = md
|
|
end
|
|
|
|
if md >= @valid_bytes.length
|
|
lo += 1
|
|
hi = md = lo
|
|
end
|
|
end
|
|
|
|
# we ran out of chars to try
|
|
if lo >= @valid_bytes.length
|
|
return nil, nil
|
|
end
|
|
|
|
shift += 8
|
|
end
|
|
|
|
return sum, chunk
|
|
end
|
|
|
|
#
|
|
# Helper that writes instructions to zero out EAX using two AND instructions.
|
|
#
|
|
def zero_eax
|
|
data = ""
|
|
data << @asm['AND']['EAX']
|
|
data << @clear1
|
|
data << @asm['AND']['EAX']
|
|
data << @clear2
|
|
data
|
|
end
|
|
|
|
#
|
|
# Write instructions that perform the subtraction using the given encoded numbers.
|
|
#
|
|
def create_sub(encoded)
|
|
data = ""
|
|
encoded.each do |e|
|
|
data << @asm['SUB']['EAX']
|
|
data << [e].pack("L")
|
|
end
|
|
data << @asm['PUSH']['EAX']
|
|
data
|
|
end
|
|
|
|
#
|
|
# Encoding the specified payload buffer.
|
|
#
|
|
def encode_payload(buf, reg_offset, protect_payload)
|
|
data = ""
|
|
|
|
# prepare the shellcode for munging
|
|
chunks = prepare_shellcode(buf, protect_payload)
|
|
|
|
# start by reading the value from the base register and dropping it into EAX for munging
|
|
data << @asm['PUSH'][@base_reg]
|
|
data << @asm['POP']['EAX']
|
|
|
|
# store the offset of the stubbed placeholder
|
|
base_reg_offset = data.length
|
|
|
|
# Write out a stubbed placeholder for the offset instruction based on
|
|
# the base register, we'll update this later on when we know how big our payload is.
|
|
encoded, _ = sub_3(0, 0)
|
|
raise EncodingError, "Couldn't offset base register." if encoded.nil?
|
|
data << create_sub(encoded)
|
|
|
|
# finally push the value of EAX back into ESP
|
|
data << @asm['PUSH']['EAX']
|
|
data << @asm['POP']['ESP']
|
|
|
|
# start instruction encoding from a clean slate
|
|
data << zero_eax
|
|
|
|
# keep track of the previous instruction, because we use that as the starting point
|
|
# for the next instruction, which saves us 10 bytes per 4 byte block. If we can't
|
|
# offset correctly, we zero EAX and try again.
|
|
previous = 0
|
|
chunks.each do |chunk|
|
|
encoded, previous = sub_3(chunk, previous)
|
|
|
|
if encoded.nil?
|
|
# try again with EAX zero'd out
|
|
data << zero_eax
|
|
encoded, previous = sub_3(chunk, 0)
|
|
end
|
|
|
|
# if we're still nil here, then we have an issue
|
|
raise EncodingError, "Couldn't encode payload" if encoded.nil?
|
|
|
|
data << create_sub(encoded)
|
|
end
|
|
|
|
# Now that the entire payload has been generated, we figure out offsets
|
|
# based on sizes so that the payload overlaps perfectly with the end of
|
|
# our decoder
|
|
total_offset = reg_offset + data.length + (chunks.length * 4) - 1
|
|
encoded, _ = sub_3(total_offset, 0)
|
|
|
|
# if we're still nil here, then we have an issue
|
|
raise EncodingError, "Couldn't encode protection" if encoded.nil?
|
|
patch = create_sub(encoded)
|
|
|
|
# patch in the correct offset back at the start of our payload
|
|
data[base_reg_offset .. base_reg_offset + patch.length] = patch
|
|
|
|
# and we're done finally!
|
|
data
|
|
end
|
|
end
|
|
|