Skip to content

Fix/xor dynamic register preservation#21670

Merged
bwatters-r7 merged 2 commits into
rapid7:masterfrom
zeroSteiner:fix/xor-dynamic-register-preservation
Jul 13, 2026
Merged

Fix/xor dynamic register preservation#21670
bwatters-r7 merged 2 commits into
rapid7:masterfrom
zeroSteiner:fix/xor-dynamic-register-preservation

Conversation

@zeroSteiner

@zeroSteiner zeroSteiner commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Description

This PR marks the dynamic XOR encoders as unable to preserve registers and adds regression coverage for stage encoding when a preserved register is required.

The bug is that x64/xor_dynamic and x86/xor_dynamic advertised register preservation support even though their decoder stubs do not preserve the registers they use. On the upstream master base commit, x64/xor_dynamic returns true from #can_preserve_registers? at modules/encoders/x64/xor_dynamic.rb#L22-L23, but the decoder stub immediately loads the encoded payload pointer into rdi with pop rdi, searches through memory using scas ... [rdi], decodes bytes through [rdi], and increments rdi at modules/encoders/x64/xor_dynamic.rb#L30-L41. That means the encoder clobbers rdi.

The same issue exists in x86/xor_dynamic: it returns true from #can_preserve_registers? at modules/encoders/x86/xor_dynamic.rb#L22-L23, then moves ebx into edi, searches through [edi], decodes through [edi], and increments edi at modules/encoders/x86/xor_dynamic.rb#L29-L38.

Stage encoding can be asked to preserve registers needed by the payload stage. In the failing Windows x64 bind Meterpreter case, the stage expects the socket handle in rdi. Because x64/xor_dynamic claimed it could preserve registers, Framework could select it for EnableStageEncoding=true; the decoder then clobbered rdi, causing the staged bind session to fail after the stage was sent. Marking these encoders as non-preserving makes the encoder selection logic skip them when register preservation is required.

Related Issue: N/A

Breaking Changes

None expected for normal encoder selection. If a user explicitly forces x64/xor_dynamic or x86/xor_dynamic while also requiring preserved registers and disabling fallback, Framework can now reject that incompatible selection instead of emitting a stage that violates the register preservation contract.

Reviewer Notes

The implementation change is intentionally small: the dynamic XOR encoders now return false from #can_preserve_registers? because their current stubs do not implement SaveRegisters.

The regression spec avoids asserting that a specific fallback encoder must be selected. It constrains the available x64 encoders to x64/xor_dynamic and x64/xor, requires rdi preservation, and verifies that x64/xor_dynamic is not selected while the payload still encodes successfully.

Verification Steps

  1. Run bundle exec rspec spec/lib/msf/core/encoded_payload/stage_encoding_spec.rb.
  2. Run bundle exec rspec spec/lib/msf/core/encoded_payload_spec.rb.
  3. Run bundle exec rubocop modules/encoders/x64/xor_dynamic.rb modules/encoders/x86/xor_dynamic.rb spec/lib/msf/core/encoded_payload/stage_encoding_spec.rb.
  4. Run ruby tools/dev/msftidy.rb modules/encoders/x64/xor_dynamic.rb modules/encoders/x86/xor_dynamic.rb.
  5. Reproduce the staged Windows x64 bind payload case with EnableStageEncoding=true; the stage encoder should not use x64/xor_dynamic when rdi must be preserved, and the bind session should remain valid after the stage is sent.

Test Evidence

Old and broken on the left (1181d3f), new and fixed on the right (1e762bb).

image

Environment

Field Details
Operating System Windows Server 2019
Target Software/Hardware Windows Server 2019
Docker Image / Vagrant Setup N/A

AI Usage Disclosure

Codex was used to help investigate the stage encoding failure, draft the fix and regression spec, and draft this PR description.

Pre-Submission Checklist

  • Included a corresponding documentation markdown file in documentation/modules (new modules only)
  • No sensitive information (IP addresses, credentials, API keys, hashes) in code or documentation
  • Tested on the target environment specified in the Environment section above
  • Included RSpec tests for library changes (encouraged for lib/ changes)
  • Read the CONTRIBUTING.md and module acceptance guidelines

@adfoster-r7
adfoster-r7 changed the base branch from master to 6.5 July 11, 2026 23:18
@adfoster-r7
adfoster-r7 changed the base branch from 6.5 to master July 11, 2026 23:19
@smcintyre-r7 smcintyre-r7 moved this from Todo to Ready in Metasploit Kanban Jul 13, 2026
@bwatters-r7 bwatters-r7 added this to the Version 6.5 milestone Jul 13, 2026
@bwatters-r7 bwatters-r7 self-assigned this Jul 13, 2026
@github-project-automation github-project-automation Bot moved this from Ready to In Progress in Metasploit Kanban Jul 13, 2026
@bwatters-r7
bwatters-r7 merged commit 972b893 into rapid7:master Jul 13, 2026
19 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Metasploit Kanban Jul 13, 2026
@bwatters-r7 bwatters-r7 added the rn-fix release notes fix label Jul 13, 2026
@bwatters-r7

Copy link
Copy Markdown
Contributor

Release Notes

Marks the dynamic XOR encoders as unable to preserve registers and adds regression coverage for stage encoding when a preserved register is required.

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

Labels

rn-fix release notes fix

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants