Skip to content

Add RISC-V XOR encoders for riscv32le and riscv64le#21235

Open
bcoles wants to merge 1 commit into
rapid7:masterfrom
bcoles:riscv-encoders
Open

Add RISC-V XOR encoders for riscv32le and riscv64le#21235
bcoles wants to merge 1 commit into
rapid7:masterfrom
bcoles:riscv-encoders

Conversation

@bcoles
Copy link
Copy Markdown
Contributor

@bcoles bcoles commented Apr 5, 2026

Add four encoder variants for both RISC-V 32-bit and 64-bit
little-endian architectures:

  • longxor: Dword XOR encoder with a 72-byte decoder stub. XORs the
    payload 4 bytes at a time using an R-type XOR instruction and a
    length-based loop counter.

  • byte_xori: Byte XOR encoder with a 64-byte decoder stub. XORs one
    byte at a time using an I-type XORI instruction with the key
    embedded in the immediate field. Avoids the R-type XOR opcode byte
    (0x33), which can be useful when that byte is a badchar.

  • longxor_tag: Tag-based dword XOR encoder with a 64-byte decoder
    stub. Uses a sentinel value instead of a length counter to detect
    end-of-payload. Rejects payloads containing aligned zero dwords
    that would collide with the sentinel.

  • longxor_feedback: Dword XOR encoder with cipher feedback and a
    76-byte decoder stub. Each dword is XORed with the previous
    encoded dword rather than a static key, creating a chained
    dependency that makes the output more resistant to frequency
    analysis.

All four encoders use only RV32I/RV64I base integer instructions and
Linux syscall 259 (riscv_flush_icache) to flush the instruction cache
after decoding.


These are the first encoders for the RISC-V architecture in the
framework, enabling bad character avoidance for RISC-V payloads.
Unfortunately, 0x00 is unavoidable.

They were written entirely by Claude. Claude one-shotted them.
When asked about reliability and portability, it suggested implementing
a guard for a zero dword in the longxor encoder, which it implemented.

Verification

msfvenom -p linux/riscv64le/exec CMD=id -e riscv64le/byte_xori -f elf -o byte_xori-test64 && chmod +x byte_xori-test64
msfvenom -p linux/riscv64le/exec CMD=id -e riscv64le/longxor -f elf -o longxor-test64 && chmod +x longxor-test64
msfvenom -p linux/riscv64le/exec CMD=id -e riscv64le/longxor_tag -f elf -o longxor_tag-test64 && chmod +x longxor_tag-test64
msfvenom -p linux/riscv64le/exec CMD=id -e riscv64le/longxor_feedback -f elf -o longxor_feedback64 && chmod +x longxor_feedback64
msfvenom -p linux/riscv32le/exec CMD=id -e riscv32le/byte_xori -f elf -o byte_xori-test32 && chmod +x byte_xori-test32
msfvenom -p linux/riscv32le/exec CMD=id -e riscv32le/longxor -f elf -o longxor-test32 && chmod +x longxor-test32
msfvenom -p linux/riscv32le/exec CMD=id -e riscv32le/longxor_tag -f elf -o longxor_tag-test32 && chmod +x longxor_tag-test32
msfvenom -p linux/riscv32le/exec CMD=id -e riscv32le/longxor_feedback -f elf -o longxor_feedback32 && chmod +x longxor_feedback32
$ /home/user/qemu/build/qemu-riscv64 -strace byte_xori-test64
2644253 execve("/bin/sh",{"/bin/sh","-c","id",NULL})uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),138(libvirt),145(docker)
$ /home/user/qemu/build/qemu-riscv64 -strace longxor-test64
2644348 execve("/bin/sh",{"/bin/sh","-c","id",NULL})uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),138(libvirt),145(docker)
$ /home/user/qemu/build/qemu-riscv64 -strace longxor_tag-test64
2644440 execve("/bin/sh",{"/bin/sh","-c","id",NULL})uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),138(libvirt),145(docker)
$ /home/user/qemu/build/qemu-riscv64 -strace longxor_feedback64 
2668101 execve("/bin/sh",{"/bin/sh","-c","id",NULL})uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),138(libvirt),145(docker)
$ /home/user/qemu/build/qemu-riscv32 -strace byte_xori-test32
2644135 execve("/bin/sh",{"/bin/sh","-c","id",NULL})uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),138(libvirt),145(docker)
$ /home/user/qemu/build/qemu-riscv32 -strace ./longxor_tag-test32 
2633438 execve("/bin/sh",{"/bin/sh","-c","id",NULL})uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),138(libvirt),145(docker)
$ /home/user/qemu/build/qemu-riscv32 -strace longxor-test32
2634177 execve("/bin/sh",{"/bin/sh","-c","id",NULL})uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),138(libvirt),145(docker)
$ /home/user/qemu/build/qemu-riscv32 -strace longxor_feedback32
2668473 execve("/bin/sh",{"/bin/sh","-c","id",NULL})uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),138(libvirt),145(docker)

@bcoles
Copy link
Copy Markdown
Contributor Author

bcoles commented Apr 5, 2026

Broken tests are not my fault.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the first RISC-V encoders to Metasploit, enabling bad-character avoidance for riscv32le and riscv64le payloads by introducing XOR-based decoder stubs (including an xori-based variant and a tag-terminated variant) that flush the I-cache after decoding.

Changes:

  • Add dword XOR encoders (longxor) for riscv32le and riscv64le with length-based decode loops.
  • Add byte-wise XORI encoders (byte_xori) for riscv32le and riscv64le to avoid the R-type XOR opcode byte.
  • Add sentinel/tag-terminated dword XOR encoders (longxor_tag) for riscv32le and riscv64le.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
modules/encoders/riscv64le/longxor.rb Adds riscv64le dword XOR encoder + decoder stub with length counter and icache flush syscall.
modules/encoders/riscv64le/longxor_tag.rb Adds riscv64le tag-terminated dword XOR encoder + decoder stub and sentinel appending.
modules/encoders/riscv64le/byte_xori.rb Adds riscv64le byte-wise XORI encoder + decoder stub with badchar-aware immediate selection.
modules/encoders/riscv32le/longxor.rb Adds riscv32le dword XOR encoder mirroring the riscv64le implementation.
modules/encoders/riscv32le/longxor_tag.rb Adds riscv32le tag-terminated dword XOR encoder mirroring the riscv64le implementation.
modules/encoders/riscv32le/byte_xori.rb Adds riscv32le byte-wise XORI encoder mirroring the riscv64le implementation.

Comment thread modules/encoders/riscv64le/longxor.rb
Comment thread modules/encoders/riscv64le/byte_xori.rb
Comment thread modules/encoders/riscv32le/longxor.rb
Comment thread modules/encoders/riscv32le/byte_xori.rb
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Comment thread modules/encoders/riscv64le/longxor_feedback.rb Outdated
Comment thread modules/encoders/riscv64le/longxor_feedback.rb
Comment thread modules/encoders/riscv32le/longxor_feedback.rb Outdated
Comment thread modules/encoders/riscv32le/longxor_feedback.rb
Comment thread modules/encoders/riscv64le/byte_xori.rb
Comment thread modules/encoders/riscv32le/byte_xori.rb
Add four encoder variants for both RISC-V 32-bit and 64-bit
little-endian architectures:

- longxor: Dword XOR encoder with a 72-byte decoder stub. XORs the
  payload 4 bytes at a time using an R-type XOR instruction and a
  length-based loop counter.

- byte_xori: Byte XOR encoder with a 64-byte decoder stub. XORs one
  byte at a time using an I-type XORI instruction with the key
  embedded in the immediate field. Avoids the R-type XOR opcode byte
  (0x33), which can be useful when that byte is a badchar.

- longxor_tag: Tag-based dword XOR encoder with a 64-byte decoder
  stub. Uses a sentinel value instead of a length counter to detect
  end-of-payload. Rejects payloads containing aligned zero dwords
  that would collide with the sentinel.

- longxor_feedback: Dword XOR encoder with cipher feedback and a
  76-byte decoder stub. Each dword is XORed with the previous
  encoded dword rather than a static key, creating a chained
  dependency that makes the output more resistant to frequency
  analysis.

All four encoders use only RV32I/RV64I base integer instructions and
Linux syscall 259 (riscv_flush_icache) to flush the instruction cache
after decoding.
@bwatters-r7 bwatters-r7 self-assigned this Apr 15, 2026
Copy link
Copy Markdown
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I called out most but not all of the instances where the string literals need the #b method to be explicitly treated as bytes. The rest of the encoders seem sensible. I didn't validate every single opcode construction but issues that should show up during testing because there are no branches.

I'm assuming none of these instructions are available in metasm. If they are we should be using it but if not, we can leave things as they are and get to it... someday.

# the buffer being encoded.
#
def decoder_stub(state)
if state.badchars.to_s.include?("\x00")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if state.badchars.to_s.include?("\x00")
if state.badchars.to_s.include?("\x00".b)

# the buffer being encoded.
#
def decoder_stub(state)
if state.badchars.to_s.include?("\x00")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if state.badchars.to_s.include?("\x00")
if state.badchars.to_s.include?("\x00".b)

# the buffer being encoded.
#
def decoder_stub(state)
if state.badchars.to_s.include?("\x00")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if state.badchars.to_s.include?("\x00")
if state.badchars.to_s.include?("\x00".b)

].pack('V*')

state.decoder_key_offset = decoder.length
decoder + "\x00\x00\x00\x00"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
decoder + "\x00\x00\x00\x00"
decoder + "\x00\x00\x00\x00".b

# the buffer being encoded.
#
def decoder_stub(state)
if state.badchars.to_s.include?("\x00")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if state.badchars.to_s.include?("\x00")
if state.badchars.to_s.include?("\x00".b)

].pack('V*')

state.decoder_key_offset = decoder.length
decoder + "\x00\x00\x00\x00"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
decoder + "\x00\x00\x00\x00"
decoder + "\x00\x00\x00\x00".b

@bcoles
Copy link
Copy Markdown
Contributor Author

bcoles commented Apr 17, 2026

I called out most but not all of the instances where the string literals need the #b method to be explicitly treated as bytes.

You're welcome to add it.

@bwatters-r7 bwatters-r7 moved this from Todo to Ready in Metasploit Kanban Apr 20, 2026
@bwatters-r7 bwatters-r7 moved this from Ready to Waiting on Contributor in Metasploit Kanban Apr 28, 2026
@bwatters-r7
Copy link
Copy Markdown
Contributor

@bcoles: bcoles#11

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

riscv RISC-V

Projects

Status: Waiting on Contributor

Development

Successfully merging this pull request may close these issues.

4 participants