|
| 1 | +## |
| 2 | +# This module requires Metasploit: http//metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +require 'msf/core' |
| 7 | + |
| 8 | +class Metasploit3 < Msf::Encoder |
| 9 | + |
| 10 | + Rank = ManualRanking |
| 11 | + |
| 12 | + ASM_SUBESP20 = "\x83\xEC\x20" |
| 13 | + |
| 14 | + SET_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' |
| 15 | + SET_SYM = '!@#$%^&*()_+\\-=[]{};\'":<>,.?/|~' |
| 16 | + SET_NUM = '0123456789' |
| 17 | + SET_FILESYM = '()_+-=\\/.,[]{}@!$%^&=' |
| 18 | + |
| 19 | + CHAR_SET_ALPHA = SET_ALPHA + SET_SYM |
| 20 | + CHAR_SET_ALPHANUM = SET_ALPHA + SET_NUM + SET_SYM |
| 21 | + CHAR_SET_FILEPATH = SET_ALPHA + SET_NUM + SET_FILESYM |
| 22 | + |
| 23 | + def initialize |
| 24 | + super( |
| 25 | + 'Name' => 'Sub encoder (optimised)', |
| 26 | + 'Description' => %q{ |
| 27 | + Encodes a payload using a series of SUB instructions and writing the |
| 28 | + encoded value to ESP. This concept is based on the known SUB encoding |
| 29 | + approach that is widely used to manually encode payloads with very |
| 30 | + restricted allowed character sets. It will not reset EAX to zero unless |
| 31 | + absolutely necessary, which helps reduce the payload by 10 bytes for |
| 32 | + every 4-byte chunk. ADD support hasn't been included as the SUB |
| 33 | + instruction is more likely to avoid bad characters anyway. |
| 34 | +
|
| 35 | + The payload requires a base register to work off which gives the start |
| 36 | + location of the encoder payload in memory. If not specified, it defaults |
| 37 | + to ESP. If the given register doesn't point exactly to the start of the |
| 38 | + payload then an offset value is also required. |
| 39 | +
|
| 40 | + Note: Due to the fact that many payloads use the FSTENV approach to |
| 41 | + get the current location in memory there is an option to protect the |
| 42 | + start of the payload by setting the 'OverwriteProtect' flag to true. |
| 43 | + This adds 3-bytes to the start of the payload to bump ESP by 32 bytes |
| 44 | + so that it's clear of the top of the payload. |
| 45 | + }, |
| 46 | + 'Author' => 'OJ Reeves <oj@buffered.io>', |
| 47 | + 'Arch' => ARCH_X86, |
| 48 | + 'License' => MSF_LICENSE, |
| 49 | + 'Decoder' => { 'BlockSize' => 4 } |
| 50 | + ) |
| 51 | + |
| 52 | + register_options( |
| 53 | + [ |
| 54 | + OptString.new( 'ValidCharSet', [ false, "Specify a known set of valid chars (ALPHA, ALPHANUM, FILEPATH)" ]), |
| 55 | + OptBool.new( 'OverwriteProtect', [ false, "Indicate if the encoded payload requires protection against being overwritten" ]) |
| 56 | + ], |
| 57 | + self.class) |
| 58 | + end |
| 59 | + |
| 60 | + # |
| 61 | + # Conver the shellcode into a set of 4-byte chunks that can be |
| 62 | + # encoding while making sure it is 4-byte aligned. |
| 63 | + # |
| 64 | + def prepare_shellcode(sc, protect_payload) |
| 65 | + # first instructions need to be ESP offsetting if the payload |
| 66 | + # needs to be protected |
| 67 | + sc = ASM_SUBESP20 + sc if protect_payload == true |
| 68 | + |
| 69 | + # first of all we need to 4-byte align the payload if it |
| 70 | + # isn't already aligned, by prepending NOPs. |
| 71 | + rem = sc.length % 4 |
| 72 | + sc = @asm['NOP'] * (4 - rem) + sc if rem != 0 |
| 73 | + |
| 74 | + # next we break it up into 4-byte chunks, convert to an unsigned |
| 75 | + # int block so calculations are easy |
| 76 | + chunks = [] |
| 77 | + sc = sc.bytes.to_a |
| 78 | + while sc.length > 0 |
| 79 | + chunk = sc.shift + (sc.shift << 8) + (sc.shift << 16) + (sc.shift << 24) |
| 80 | + chunks << chunk |
| 81 | + end |
| 82 | + |
| 83 | + # return the array in reverse as this is the order the instructions |
| 84 | + # will be written to the stack. |
| 85 | + chunks.reverse |
| 86 | + end |
| 87 | + |
| 88 | + # |
| 89 | + # From the list of characters given, find two bytes that when |
| 90 | + # ANDed together result in 0. Returns nil if not found. |
| 91 | + # |
| 92 | + def find_opposite_bytes(list) |
| 93 | + list.each_char do |b1| |
| 94 | + list.each_char do |b2| |
| 95 | + if b1.ord & b2.ord == 0 |
| 96 | + return (b1 * 4), (b2 * 4) |
| 97 | + end |
| 98 | + end |
| 99 | + end |
| 100 | + return nil, nil |
| 101 | + end |
| 102 | + |
| 103 | + # |
| 104 | + # Entry point to the decoder. |
| 105 | + # |
| 106 | + def decoder_stub(state) |
| 107 | + return state.decoder_stub if state.decoder_stub |
| 108 | + |
| 109 | + # configure our instruction dictionary |
| 110 | + @asm = { |
| 111 | + 'NOP' => "\x90", |
| 112 | + 'AND' => { 'EAX' => "\x25" }, |
| 113 | + 'SUB' => { 'EAX' => "\x2D" }, |
| 114 | + 'PUSH' => { |
| 115 | + 'EBP' => "\x55", 'ESP' => "\x54", |
| 116 | + 'EAX' => "\x50", 'EBX' => "\x53", |
| 117 | + 'ECX' => "\x51", 'EDX' => "\x52", |
| 118 | + 'EDI' => "\x57", 'ESI' => "\x56" |
| 119 | + }, |
| 120 | + 'POP' => { 'ESP' => "\x5C", 'EAX' => "\x58", } |
| 121 | + } |
| 122 | + |
| 123 | + # set up our base register, defaulting to ESP if not specified |
| 124 | + @base_reg = (datastore['BufferRegister'] || 'ESP').upcase |
| 125 | + |
| 126 | + # determine the required bytes |
| 127 | + @required_bytes = |
| 128 | + @asm['AND']['EAX'] + |
| 129 | + @asm['SUB']['EAX'] + |
| 130 | + @asm['PUSH']['EAX'] + |
| 131 | + @asm['POP']['ESP'] + |
| 132 | + @asm['POP']['EAX'] + |
| 133 | + @asm['PUSH'][@base_reg] |
| 134 | + |
| 135 | + # generate a sorted list of valid characters |
| 136 | + char_set = "" |
| 137 | + case (datastore['ValidCharSet'] || "").upcase |
| 138 | + when 'ALPHA' |
| 139 | + char_set = CHAR_SET_ALPHA |
| 140 | + when 'ALPHANUM' |
| 141 | + char_set = CHAR_SET_ALPHANUM |
| 142 | + when 'FILEPATH' |
| 143 | + char_set = CHAR_SET_FILEPATH |
| 144 | + else |
| 145 | + for i in 0 .. 255 |
| 146 | + char_set += i.chr.to_s |
| 147 | + end |
| 148 | + end |
| 149 | + |
| 150 | + # remove any bad chars and populate our valid chars array. |
| 151 | + @valid_chars = "" |
| 152 | + char_set.each_char do |c| |
| 153 | + @valid_chars << c.to_s unless state.badchars.include?(c.to_s) |
| 154 | + end |
| 155 | + |
| 156 | + # we need the valid chars sorted because of the algorithm we use |
| 157 | + @valid_chars = @valid_chars.chars.sort.join |
| 158 | + @valid_bytes = @valid_chars.bytes.to_a |
| 159 | + |
| 160 | + all_bytes_valid = @required_bytes.bytes.reduce(true) { |a, byte| a && @valid_bytes.include?(byte) } |
| 161 | + |
| 162 | + # determine if we have any invalid characters that we rely on. |
| 163 | + unless all_bytes_valid |
| 164 | + raise RuntimeError, "Bad character set contains characters that are required for this encoder to function." |
| 165 | + end |
| 166 | + |
| 167 | + unless @asm['PUSH'][@base_reg] |
| 168 | + raise RuntimeError, "Invalid base register" |
| 169 | + end |
| 170 | + |
| 171 | + # get the offset from the specified base register, or default to zero if not specifed |
| 172 | + reg_offset = (datastore['BufferOffset'] || 0).to_i |
| 173 | + |
| 174 | + # calculate two opposing values which we can use for zeroing out EAX |
| 175 | + @clear1, @clear2 = find_opposite_bytes(@valid_chars) |
| 176 | + |
| 177 | + # if we can't then we bomb, because we know we need to clear out EAX at least once |
| 178 | + unless @clear1 |
| 179 | + raise RuntimeError, "Unable to find AND-able chars resulting 0 in the valid character set." |
| 180 | + end |
| 181 | + |
| 182 | + protect_payload = (datastore['OverwriteProtect'] || "").downcase == "true" |
| 183 | + |
| 184 | + # with everything set up, we can now call the encoding routine |
| 185 | + state.decoder_stub = encode_payload(state.buf, reg_offset, protect_payload) |
| 186 | + |
| 187 | + state.buf = "" |
| 188 | + state.decoder_stub |
| 189 | + end |
| 190 | + |
| 191 | + # |
| 192 | + # Determine the bytes, if any, that will result in the given chunk |
| 193 | + # being decoded using SUB instructions from the previous EAX value |
| 194 | + # |
| 195 | + def sub_3(chunk, previous) |
| 196 | + carry = 0 |
| 197 | + shift = 0 |
| 198 | + target = previous - chunk |
| 199 | + sum = [0, 0, 0] |
| 200 | + |
| 201 | + 4.times do |idx| |
| 202 | + b = (target >> shift) & 0xFF |
| 203 | + lo = md = hi = 0 |
| 204 | + |
| 205 | + # keep going through the character list under the "lowest" valid |
| 206 | + # becomes too high (ie. we run out) |
| 207 | + while lo < @valid_bytes.length |
| 208 | + # get the total of the three current bytes, including the carry from |
| 209 | + # the previous calculation |
| 210 | + total = @valid_bytes[lo] + @valid_bytes[md] + @valid_bytes[hi] + carry |
| 211 | + |
| 212 | + # if we matched a byte... |
| 213 | + if (total & 0xFF) == b |
| 214 | + # store the carry for the next calculation |
| 215 | + carry = (total >> 8) & 0xFF |
| 216 | + |
| 217 | + # store the values in the respective locations |
| 218 | + sum[2] |= @valid_bytes[lo] << shift |
| 219 | + sum[1] |= @valid_bytes[md] << shift |
| 220 | + sum[0] |= @valid_bytes[hi] << shift |
| 221 | + break |
| 222 | + end |
| 223 | + |
| 224 | + hi += 1 |
| 225 | + if hi >= @valid_bytes.length |
| 226 | + md += 1 |
| 227 | + hi = md |
| 228 | + end |
| 229 | + |
| 230 | + if md >= @valid_bytes.length |
| 231 | + lo += 1 |
| 232 | + hi = md = lo |
| 233 | + end |
| 234 | + end |
| 235 | + |
| 236 | + # we ran out of chars to try |
| 237 | + if lo >= @valid_bytes.length |
| 238 | + return nil, nil |
| 239 | + end |
| 240 | + |
| 241 | + shift += 8 |
| 242 | + end |
| 243 | + |
| 244 | + return sum, chunk |
| 245 | + end |
| 246 | + |
| 247 | + # |
| 248 | + # Helper that writes instructions to zero out EAX using two AND instructions. |
| 249 | + # |
| 250 | + def zero_eax |
| 251 | + data = "" |
| 252 | + data << @asm['AND']['EAX'] |
| 253 | + data << @clear1 |
| 254 | + data << @asm['AND']['EAX'] |
| 255 | + data << @clear2 |
| 256 | + data |
| 257 | + end |
| 258 | + |
| 259 | + # |
| 260 | + # Write instructions that perform the subtraction using the given encoded numbers. |
| 261 | + # |
| 262 | + def create_sub(encoded) |
| 263 | + data = "" |
| 264 | + encoded.each do |e| |
| 265 | + data << @asm['SUB']['EAX'] |
| 266 | + data << [e].pack("L") |
| 267 | + end |
| 268 | + data << @asm['PUSH']['EAX'] |
| 269 | + data |
| 270 | + end |
| 271 | + |
| 272 | + # |
| 273 | + # Encoding the specified payload buffer. |
| 274 | + # |
| 275 | + def encode_payload(buf, reg_offset, protect_payload) |
| 276 | + data = "" |
| 277 | + |
| 278 | + # prepare the shellcode for munging |
| 279 | + chunks = prepare_shellcode(buf, protect_payload) |
| 280 | + |
| 281 | + # start by reading the value from the base register and dropping it into EAX for munging |
| 282 | + data << @asm['PUSH'][@base_reg] |
| 283 | + data << @asm['POP']['EAX'] |
| 284 | + |
| 285 | + # store the offset of the stubbed placeholder |
| 286 | + base_reg_offset = data.length |
| 287 | + |
| 288 | + # Write out a stubbed placeholder for the offset instruction based on |
| 289 | + # the base register, we'll update this later on when we know how big our payload is. |
| 290 | + encoded, _ = sub_3(0, 0) |
| 291 | + raise RuntimeError, "Couldn't offset base register." if encoded.nil? |
| 292 | + data << create_sub(encoded) |
| 293 | + |
| 294 | + # finally push the value of EAX back into ESP |
| 295 | + data << @asm['PUSH']['EAX'] |
| 296 | + data << @asm['POP']['ESP'] |
| 297 | + |
| 298 | + # start instruction encoding from a clean slate |
| 299 | + data << zero_eax |
| 300 | + |
| 301 | + # keep track of the previous instruction, because we use that as the starting point |
| 302 | + # for the next instruction, which saves us 10 bytes per 4 byte block. If we can't |
| 303 | + # offset correctly, we zero EAX and try again. |
| 304 | + previous = 0 |
| 305 | + chunks.each do |chunk| |
| 306 | + encoded, previous = sub_3(chunk, previous) |
| 307 | + |
| 308 | + if encoded.nil? |
| 309 | + # try again with EAX zero'd out |
| 310 | + data << zero_eax |
| 311 | + encoded, previous = sub_3(chunk, 0) |
| 312 | + end |
| 313 | + |
| 314 | + # if we're still nil here, then we have an issue |
| 315 | + raise RuntimeError, "Couldn't encode payload" if encoded.nil? |
| 316 | + |
| 317 | + data << create_sub(encoded) |
| 318 | + end |
| 319 | + |
| 320 | + # Now that the entire payload has been generated, we figure out offsets |
| 321 | + # based on sizes so that the payload overlaps perfectly with the end of |
| 322 | + # our decoder |
| 323 | + total_offset = reg_offset + data.length + (chunks.length * 4) - 1 |
| 324 | + encoded, _ = sub_3(total_offset, 0) |
| 325 | + |
| 326 | + # if we're still nil here, then we have an issue |
| 327 | + raise RuntimeError, "Couldn't encode protection" if encoded.nil? |
| 328 | + patch = create_sub(encoded) |
| 329 | + |
| 330 | + # patch in the correct offset back at the start of our payload |
| 331 | + data[base_reg_offset .. base_reg_offset + patch.length] = patch |
| 332 | + |
| 333 | + # and we're done finally! |
| 334 | + data |
| 335 | + end |
| 336 | +end |
| 337 | + |
0 commit comments