6 ms·
For anybody unfamiliar with this, as I was, this appears to refer to Intel's Indirect Branch Tracking feature[1] (and the equivalent on ARM, BTI). The idea is t
by messe 3y ago
For anybody unfamiliar with this, as I was, this appears to refer to Intel's Indirect Branch Tracking feature[1] (and the equivalent on ARM, BTI). The idea is that an indirect branch can only pass control to a location that starts with an "end branch" instruction. An indirect branch is one that jumps to a location whose value is loaded or computed from either a register or memory address: think calling a function pointer in C.
Without IBT, you'd have this equivalence between C and assembly:
main() {
void (*f)();
f = foo;
f();
}
void foo() { }
---
main:
movl $foo, %edx
call *%edx
ret
foo:
ret
If IBT is enabled, the above code triggers an exception because foo doesn't begin with an "end branch" instruction. When IBT is enabled by the compiler, the above code gets assembled as:
main:
endbr64
movl $foo, %edx
call *%edx
ret
foo:
endbr64
ret
Now the compiler inserts endbr64 at the start of each function prologue. The reason for this feature, is to use as a defense in depth against JOP, and COP attacks, as it means that the only "widgets" available to you are entire functions, which can be far harder to exploit and chain.
[1]: https://www.intel.com/content/dam/develop/external/us/en/documents/catc17-introduction-intel-cet-844137.pdf https://www.intel.com/content/dam/develop/external/us/en/doc...
- rwmj 3y agoThe fun fact being that older CPUs decode ENDBR64 as a slightly weird NOP (with no architectural effects), but it'll fault on original Pentiums: https://stackoverflow.com/questions/56120231/how-do-old-cpus-execute-the-new-endbr64-and-endbr32-instructions https://stackoverflow.com/questions/56120231/how-do-old-cpus...
- SomeRndName11 3y agoNOP on intels is in fact xchg eax, eax
- mattgreenrocks 3y agoThat’s really clever use of the opcode space. Thanks for passing that along.
- rollcat 3y agoVarious architectures do other interesting things with NOPs, IIRC one convention on PowerPC had something vaguely related to debugging or tracing (I can't remember the details or find any references right now).
- messe 3y agoNot just architectures, but different OSes and ABIs have found ways to repurpose no-ops. One example[1] is Windows using the 2-byte "MOV EDI, EDI" as a hot-patch point: it gets replaced by a "JMP $-5" instruction which jumps 5 bytes before the start of a function into a spot reserved for patching. That 5 bytes is enough to contain a full jump instruction that can then jump wherever you need it to. ## Why do Windows functions all begin with a pointless MOV EDI, EDI instruction? [1]: https://devblogs.microsoft.com/oldnewthing/20110921-00/?p=9583 https://devblogs.microsoft.com/oldnewthing/20110921-00/?p=95...
- gcoakes 3y agoGood read. Thank you. This just worsens my fear of changing "unnecessary" code when I don't know the original motivation for it.
- jchw 3y agoI wonder if this is still true. Whenever I go to hook Win32 API functions, I use an off-the-shelf length disassembler to create a trampoline with the first n bytes of instructions and a jmp back, and then just patch in a jmp to my hook, but if this hot-patch point exists it'd be a lot less painful since you can avoid basically all of that. Though, I guess even if it was, it'd be silly to rely on it even on x86 only. Maybe it would still make for a nice fast-path? Dunno.
- jeffbee 3y agoInteresting, thanks for pointing this out! Just yesterday I was gazing at some program containing two consecutive xor rax, rax. I thought what’s the point? But as you point out it might be a NOP sled designed to be that specific length.
- dataflow 3y agoThere's a good question in the comments there that I still don't see the answer to. How does this work if there's an interrupt between the branch and the endbranch? Does the OS need to save/restore the "branchness" bit?
- drdrey 3y agothere is no branchness bit, if there's an endbranch you can jump to it
- dataflow 3y agoAh so when you return from an interrupt, the check is no longer done?
- simcop2387 3y agoI'd assume so since it wouldn't be a call/jmp coming from a computed address in a register. That said I haven't read the documentation for any of this. But interrupts should be having a stack pointer change and other things happening that would be different, which is why they use the IRET instruction and not the RET one.
- muricula 3y agoYes, on arm the branch type is saved in SPSR_EL1 in the BTYPE field. That stands for Saved Program State Register for Kernel Mode (Exception Level 1) and Branch Type. https://developer.arm.com/documentation/ddi0595/2021-12/AArch64-Registers/SPSR-EL1--Saved-Program-Status-Register--EL1- https://developer.arm.com/documentation/ddi0595/2021-12/AArc...
- __failbit 3y agoThank you for the explanation!
- haberman 3y agoInteresting. I was able to get Clang to generate this using `-fcf-protection=branch`: https://godbolt.org/z/rooP8vPsM https://godbolt.org/z/rooP8vPsM It looks like endbr64 is a 4-byte instruction. That could be a significant code size overhead for jump tables with lots of targets: https://godbolt.org/z/xTPToaddh https://godbolt.org/z/xTPToaddh
- notaplumber1 3y agoOpenBSD disables jump tables in Clang on amd64 due to IBT, some architectures also had jump tables disabled as part of the switch to --execute-only ("xonly") binaries by default, e.g: powerpc64/sparc64/hppa. https://marc.info/?l=openbsd-cvs&m=168254711511764&w=2 https://marc.info/?l=openbsd-cvs&m=168254711511764&w=2 E.g: https://marc.info/?l=openbsd-cvs&m=167337396024167&w=2 https://marc.info/?l=openbsd-cvs&m=167337396024167&w=2
- saagarjha 3y agoAny idea what the performance impact is?
- codedokode 3y agoWhy should every function start with endbr64 command? Aren't functions usually called directly? Also, is it required to insert endbr64 command after function calls (for return address)?
- messe 3y agoC allows for any function to be called via a function pointer, and functions can be in different translation units, so the compiler can't simply assume that a function will never be called indirectly and has to pessimistically insert endbr64 in order to maintain a reasonable ABI. And no, as I understand it, this is only for branch/calls not returns.
- Joker_vD 3y agoWell, if the function is marked "static", the compiler can actually check whether the function's address is taken in the current compilation unit or not and omit/emit ENDBR64 accordingly (passing pointers to static functions to code in another compilation units is legal, and should still work).
- messe 3y agoGood catch. Yeah, as long as the functions address is never taken the compiler has a lot of leeway with static functions; it can even avoid emitting code for them entirely if it can prove they're never called or if it's able to compute their results at compile-time.
- josephg 3y agoYep. Or inline them at every call site if that makes sense to do based on the optimization level and flags.
- MobiusHorizons 3y agoIs this theoretically something lto could remove?
- wongarsu 3y agoInteresting. Seems like enforcement on Intel CPUs is supported since Tiger Lake (so ~2020). Windows has basically the same feature implemented in software since 2015, called Control Flow Guard [1]. I wonder what the story there is, and if Windows has any plans to (get everyone to) switch to the hardware version once those CPUs have sufficient market share. 1: https://learn.microsoft.com/en-us/windows/win32/secbp/control-flow-guard https://learn.microsoft.com/en-us/windows/win32/secbp/contro...
- andersa 3y agoWindows also recently implemented a far better version of this called Extended Flow Guard (XFG) that not only checks whether the location is a valid destination, but also whether it's a valid destination for that specific source. For example, for any virtual function call or function pointer call, the destination must have a correct tag with the hash of the arguments. It's much more secure, and also faster, since loading the tag from memory can be merged with loading the actual code after it. I wish this was the one implemented in hardware..
- simcop2387 3y agoThat does sound like it would be more robust, but definitely sounds like it'd require a lot more silicon than the IBT that they did implement. Something like it might be something that comes in some future revisions.
- saagarjha 3y agoARM does it!
- ComputerGuru 3y agoThere’s a great article on XFG here [0] but it observed that a failed XFG check downgrades to a regular CFG check instead of a denial.. meaning it adds zero extra protection? Perhaps this behavior has changed since the preview they tested, though! [0]: https://www.offsec.com/offsec/extended-flow-guard/ https://www.offsec.com/offsec/extended-flow-guard/
- asveikau 3y agoIt was an old joke that the opposite of "goto" is "come from", or that if goto is considered harmful, nobody said anything about a "come from". Marking something as a branch target reminds me of this. https://en.m.wikipedia.org/wiki/COMEFROM https://en.m.wikipedia.org/wiki/COMEFROM
- dejj 3y ago> GOTO considered harmful COMEFROM considered harm-mitigating It ingeniously makes Return Oriented Programming (ROP) a lot harder.
- messe 3y ago> COMEFROM considered harm-mitigating You know, that’d be a fantastic OpenBSD release name. Here’s hoping a dev sees this comment; there’s already been a few commenting in this thread.
- cratermoon 3y agoIn case anyone wants a very simple introduction to JOP/COP exploits and mitigations of this type: <https://www.theregister.com/2020/06/15/intel_cet_tiger_lake/ https://www.theregister.com/2020/06/15/intel_cet_tiger_lake/>