Messages in this thread | | | From | solar@sun1 ... | Subject | Re: 2nd Linux kernel patch to remove stack exec | Date | Mon, 14 Apr 1997 18:19:06 -0300 (GMT) |
| |
Hello!
> The basic idea is that buffer exploit calls rely on executing code after > a "return" instruction. The trampoline codes generated by compilers > apparently do so using a jump instruction, so you can tell the > difference.
Actually, the trampolines are not the code that jumps onto the stack, they're on the stack themselves. It is a normal function call (via a function pointer) that gets the control to the trampoline on the stack. And the instruction causing a GPF is a call %reg, generated by GCC.
> So if the EIP changes to a stack instruction, you take a GPF, and then > you figure out how you got there; if it was via a return instruction, > you assume it was due to a buffer exploit bug.
Correct. However, there still is a little hole with my implementation (why did noone find it? I understood myself today). It is correct that the first instruction to move control a different way in most cases has to be a RET, however, as I mentioned earlier, in some cases the program may already have some suitable code in its code segment. Now, the problem is: the size of such code is too small, so it's likely to almost always exist. An example: we find a RETF instruction (one byte) somewhere in the code segment (not even necessarily on an instruction boundary), and overwrite the return address to point to that instruction, and some more bytes with the address for the RETF. That way our RET jumps to a place in the code segment (no GPF here), and the RETF can jump onto the stack, since the GPF handler only checks for a near return instruction, not a far return one.
Adding a check for RETF also will help somewhat, but not much, since there're many two-byte instructions to pass the control onto the stack, and that still leaves quite a high probability that some suitable bytes exist in the code segment. The solution I did now (running this while typing this message) is to only allow a few instructions, not reject a few, in the GPF handler. Those allowed are only call %reg, where %reg is any of %eax, %ecx, %edx, %ebx, %ebp, %esi, %edi (all GP registers, except for %esp). These are the only instructions that GCC can generate for trampoline calls. Note that this way it is also required that a register has the correct value to exploit, so most likely only one of those 7 instructions will fit -- call %ebp (I wish I could disable this one, but GCC can still use it AFAIK if compiled with -fomit-frame-pointer). I did a search for the two bytes (call %ebp) in a bunch of binaries in my /bin, and found out that only about 5% of them had the bytes, which I find reasonable.
It looks like we'll have to decide between disabling everything but these call instructions (might break something yet unknown), and disabling the two return instructions only (shouldn't break anything, but leaves some extra two-byte instructions working)...
In any way, I have to admit that the GPF handler stuff is a bit less secure than totally disabling stack execution for some processes. This means that a flag for total execution permission disabling, that you could put on a binary, might still be useful...
> Solar (sorry I don't know your real name), have you checked to make sure > none of the trampoline codes call subroutines from stack code? If so,
Of course they do, that is what they're for.
> then a user-space return instruction back into trampoline code might > erroneously cause your GPF handler to conclude that a buffer exploit was > in progress.
It won't, since I enable stack execution permission for the rest of the entire process as soon as one trampoline is detected.
> Another problem --- if a signal handler doesn't actually return, but > instead uses longjmp(), the CS register won't be reset, and you will no > longer be protected against executable code on the stack (since now the > CS segment register will be HUGE_CS which does allow stack execution).
True.
> Short of futzing with the longjmp()/setjmp() implementaiton, though, > there probably isn't much you can do about this.
Well, I think this can be fixed in libc, by saving/restoring the CS value also. Another fix might be to keep a flag for each task, whether it should have its stack executable or not, and another flag, if it's inside a signal handler or not, both are quite easy to implement, and set the CS on return from every syscall accordingly. That way the program will become protected again after a longjmp() as soon as it makes any syscall. A really weird solution though, and is probably too complicated for a not so significant problem. I don't like it myself, it's just an example of what can be fixed by means of the kernel.
Signed, Solar Designer
| |