Add RISC-V XOR encoders for riscv32le and riscv64le#21235
Conversation
|
Broken tests are not my fault. |
There was a problem hiding this comment.
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) forriscv32leandriscv64lewith length-based decode loops. - Add byte-wise XORI encoders (
byte_xori) forriscv32leandriscv64leto avoid the R-type XOR opcode byte. - Add sentinel/tag-terminated dword XOR encoders (
longxor_tag) forriscv32leandriscv64le.
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. |
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.
smcintyre-r7
left a comment
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
| 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") |
There was a problem hiding this comment.
| 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") |
There was a problem hiding this comment.
| 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" |
There was a problem hiding this comment.
| 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") |
There was a problem hiding this comment.
| 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" |
There was a problem hiding this comment.
| decoder + "\x00\x00\x00\x00" | |
| decoder + "\x00\x00\x00\x00".b |
You're welcome to add it. |
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