Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x86-32 CALL rel16 zeroing out high bytes of EIP missing from instruction specification #7418

Open
inakilbss opened this issue Jan 26, 2025 · 3 comments
Assignees
Labels
Feature: Processor/x86 Status: Triage Information is being gathered Type: Bug Something isn't working

Comments

@inakilbss
Copy link

Describe the bug
In 32-bit x86 code, instruction CALL rel16 (66 e8 cw) zeroes out the high bytes of EIP[1], making it suitable only for calls to code in 0x00000000-0000ffff. However, patching a CALL instruction will suggest it even for destinations outside that range, creating faulty binaries that will most likely crash if executed. The disassembler will also erroneously display the intended destination as the instruction's target, making the bug nontrivial to identify.

To Reproduce

  1. Open an x86 binary
  2. Locate a CALL instruction
  3. Patch the instruction to target an address less than 0x7000 away in either direction and higher than 0xffff
  4. 66 e8 #### will be suggested, which is incorrect

Expected behavior
CALL rel16 is not suggested outside of its real target range, and usages are correctly disassembled to destinations within said range.

Screenshots
Image

Attachments
I was going to include a sample ELF, but not a valid attachment

Environment:

  • OS: Ubuntu 22.04.5 LTS
  • Java Version: 17.0.13
  • Ghidra Version: 11.0
  • Ghidra Origin: Official GitHub distro

Additional context
[1]Intel® 64 and IA-32 Architectures Software Developer’s Manual, volume 2A, chapter 3, page 143:
IF near call THEN IF near relative call THEN [...] IF OperandSize = 16 THEN tempEIP := (EIP + DEST) AND 0000FFFFH; (* DEST is rel16 *) [...]

@GhidorahRex GhidorahRex added Type: Bug Something isn't working Feature: Processor/x86 Status: Waiting on customer Waiting for customer feedback labels Jan 27, 2025
@GhidorahRex
Copy link
Collaborator

Your screenshot isn't showing an issue. The instruction your patching is at 0x111e7, you're patching the instruction to address 0x1119d. Those CALL options all go to the same address.

@inakilbss
Copy link
Author

inakilbss commented Jan 27, 2025

Opcode e8 operates differently depending on its OperandSize:

IF OperandSize = 32
    THEN
        tempEIP := EIP + DEST; (* DEST is rel32 *)
        IF tempEIP is not within code segment limit THEN #GP(0); FI;
        IF stack not large enough for a 4-byte return address
            THEN #SS(0); FI;
        Push(EIP);
        IF ShadowStackEnabled(CPL) AND DEST != 0
            ShadowStackPush4B(EIP);
        FI;
        EIP := tempEIP;
FI;
IF OperandSize = 16
    THEN
        tempEIP := (EIP + DEST) AND 0000FFFFH; (* DEST is rel16 *)
        IF tempEIP is not within code segment limit THEN #GP(0); FI;
        IF stack not large enough for a 2-byte return address
            THEN #SS(0); FI;
        Push(IP);
        IF ShadowStackEnabled(CPL) AND DEST != 0
            (* IP is zero extended and pushed as a 32 bit value on shadow stack *)
            ShadowStackPush4B(IP);
        FI;
        EIP := tempEIP;
FI;

For OperandSize = 16, offset addition is done in 16 bits on a reduced EIP, rather than in 32 bits on an extended DEST; meaning the result will never be greater than 0xFFFF.

The same applies to jump instructions:

IF condition
    THEN
        tempEIP := EIP + SignExtend(DEST);
        IF OperandSize = 16
            THEN tempEIP := tempEIP AND 0000FFFFH;
        FI;
        IF tempEIP is not within code segment limit
            THEN #GP(0);
            ELSE EIP := tempEIP
        FI;
FI;

and RET:

ELSE (* OperandSize = 16 *)
    IF top 2 bytes of stack not within stack limits
        THEN #SS(0); FI;
    tempEIP := Pop();
    tempEIP := tempEIP AND 0000FFFFH;
    IF tempEIP not within code segment limits
        THEN #GP(0); FI;
    EIP := tempEIP;
    IF ShadowStackEnabled(CPL)
        tempSsEip = ShadowStackPop4B();
    IF EIP != tempSsEIP
        THEN #CP(NEAR_RET); FI;
FI;

The rel16 options can only be encoded by setting OperandSize to 16, so they are not suitable for most 32-bit code, as both the destination and return addresses must be in a very small range for the operations to behave as expected.

@GhidorahRex GhidorahRex self-assigned this Jan 28, 2025
@GhidorahRex GhidorahRex added Status: Triage Information is being gathered and removed Status: Waiting on customer Waiting for customer feedback labels Jan 28, 2025
@GhidorahRex
Copy link
Collaborator

Okay, I see the issue and I was able to reproduce it. It also appears to be a problem for jmp and jcc as well - since in the manual those are also zeroing the upper 16-bits of EIP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature: Processor/x86 Status: Triage Information is being gathered Type: Bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants