5 ms·
Control-flow integrity in Linux 5.13
- DSingularity 5y agoI am wary of any claim to CFI without secure tracking of the call-stack to verify the return destination. You need to compare return destinations against a shadow call-stack. If you dont protect this call stack then attackers will just evolve slightly. You can encrypt/decrypt the return address grsecurity style but we know that this can still be bypassed.
- jnwatson 5y agoIt looks like backwards-edge protection is in a different patch. Backwards-edge is a bit easier to implement.
- DSingularity 5y agoWell I am not sure I agree. It seems to me that there are significant security complications due to the need of making a runtime comparison to determine the integrity of the return destination. How are we to be convinced that the value at the top of the shadow-stack is not attacker-controlled?
- ndesaulniers 5y agoAn attacker could modify the shadow stack, it's just rather difficult to find where it's randomly placed and would require arbitrary read/write capabilities. The kernel zeros out the register used to write to the shadow stack as soon as possible after use. It's not impossible to defeat, but does raise the bar significantly. I encourage you to read the commit message of shadow call stack kernel patches, which as another commenter notes is only backwards edge protection; CFI is forwards edge protection.
- agumonkey 5y agodo you know who works on such topics ?
- DSingularity 5y agoI’m not sure what you mean. I’ve read a variety of papers and articles on this topic. Are you referring to the user that replied to me?
- yosefk 5y agoThis CFI implementation makes &func be different values in 2 different loadable kernel modules. C says they should be the same value. What do the language lawyers among the compiler writers, who explain why the execution of an entire program is rendered meaningless by undefined behavior, tell us about this? What is the definition of the language compiled with CFI enabled (it's not C but some close relative)?
- Joker_vD 5y agoCould you elaborate the scenario a bit? Two different loadable kernel modules would define two different (even if named the same) functions "func" so "&func" has to be two different pointers... or do you mean that if that "func" was defined in the kernel, and both of those modules are linked against the kernel dynamically, then "&func" should yield the same value? But even now they don't even without CFI: when "func" is defined in the dinamically-loaded library, the "&func" value is the pointer into the (calling object file's) PLT.
- derefr 5y ago> What is the definition of the language compiled with CFI enabled (it's not C but some close relative)? I think I saw mentioned, in the comments of a recent HN post about undefined behavior in C, that OS kernels aren't using "standard C", as they use a bunch of compiler flags to constrain what the compiler does in the face of UB, contrary to what the standard says the compiler is allowed to do. As such, OS kernels have always kind of been using a "close relative of C." (Same syntax, slightly different semantics.)
- bcoates 5y agoLoadable modules aren't part of the C spec. Whatever the meaning of &func in 2 different modules potentially compiled by 2 different compilers is is specified by the ABI. CFI is a breaking change to the contract the Linux kernel makes with kernel modules, but it doesn't have anything in particular to do with C the language.
- KMag 5y agoI'm not a language lawyer, but I'm pretty sure with x86 and x86_64 ELF dynamic symbols, &func is the address of the program linkage table (PLT) entry for the dynamic symbol, in the current ELF executable/library. The executable has its own PLT. Each library has its own PLT. The executable has its own global offset table (GOT). Each library has its own GOT. A given dynamically linked function will have one PLT entry and one GOT entry in each library that calls it. After the function's actual global entry point has been resolved, its address will be in the GOT entry. However, symbol resolution can be done lazily, and we don't want taking the address of a function to potentially perform the lookup, so I think the function address is just the start of the PLT entry. For instance, if the executable and several libraries it loads all call printf, they'll each have their own tiny machine code stub (PLT entry) that (after dynamic symbol resolution) just jumps to the actual printf implementation. The executable will use the start of its PLT entry for printf as its address for printf. Each library will use the start of its PLT entry for printf as its address for printf. The PLT entry is a tiny machine code stub within the same library or executable that does an indirect jump to the address held in a global offset table (GOT) entry. For lazily resolved symbols, the GOT is initialized to the address of a stub that does the dynamic symbol resolution and overwrites the GOT entry with the correct address (and then jumps to that address). Any later function call will still hit the PLT and jump to the address in the GOT, but the GOT will now point to the actual start of the function.
- ufo 5y agoDoes anyone know which tools the kernel developers used to identify all the places in the kernel that use function pointer equality? Changing how all the function pointers behave is a spooky change to make so they must have had some tools to help them find all the tricky places, right?
- twic 5y agoThis is a bit like defunctionalization: https://blog.sigplan.org/2019/12/30/defunctionalization-everybody-does-it-nobody-talks-about-it/ https://blog.sigplan.org/2019/12/30/defunctionalization-ever... It's basically replacing a function pointer with an enum. The kernel version is particularly simple, because the enum values don't have fields, they are a pure discriminant. Although I think the integritized function pointers aren't enum values or indices into the jump table, they're pointers into it. I think they could be indices; I'm not sure if there's a reason this is a bad idea.