Skip to content

Instantly share code, notes, and snippets.

@yrp604
Last active December 13, 2021 07:28
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save yrp604/2e1b13f76c18186ea203 to your computer and use it in GitHub Desktop.
Save yrp604/2e1b13f76c18186ea203 to your computer and use it in GitHub Desktop.
CFI Notes

Starting in clang 3.7 they've introduced a new argument -fsanitize=cfi which aims to protect indirect calls from overwrites.

All the code and binaries I used can be downloaded here

Protecting C Function pointers

First, I thought I would look at how CFI applied to simple C structs with function pointers. After fighting with the compiler to get it to stop optimizing my code, (i.e. call <puts> instead of call rcx because clang realized that rcx was always going to be puts(3)) I got it calling things from memory. However, there was no CFI protection on the call. I played around with this a bit (interestingly, clang will optimize use of un-initialized memory to the ud2 instruction) but was unable to get any CFI protection in place.

Protection C++ virtual calls

Reading a bit of the clang manual, it talked a lot about C++ virtual methods, so I thought I would look at those. I also played around with non virtual calls, those were replaced with static calls as you'd expect and were fairly uninteresting. I wrote some C++ for the second time in a decade:

class obj {
public:
    virtual void fp() { printf("[-] ?\n"); }
};

class thing : public obj {
public:
    void fp() { printf("[-] :(\n"); }
};
// ...
thing *t;

t = new thing;
printf("[+] t @ %llx\n", t); // This is important, not sure why yet
t->fp();

This generates the following assembly:

mov    rdi,QWORD PTR [rsp+0x8]
mov    rax,QWORD PTR [rdi]
lea    rcx,[rip+0x1eb4]        # 2cc0 <_ZTV5thing+0x10>
cmp    rax,rcx
jne    e34 <main+0x244>
call   QWORD PTR [rax]
# ...
ud2                            # 0xe34 <main+0x244>

To understand what's going on here, here's a brief (slightly simplified) diagram of how C++ objects are commonly laid out in memory:

Heap or Stack:   Heap:                  .data.rel.ro.local:  .text:
                 thing object           thing vtable         printf
ptr to obj    -> +0x0: ptr to vtable -> +0x0: fp          -> [printf code]

Normally when exploiting vtables you aim for one of the first two locations in memory (.text and .data.rel.ro.local are read-only). Either we want to overwrite the pointer to the object (i.e. type confusion), the vtable pointer with a pointer to our own malicious vtable. So, how does the above assembly protect these objects?

The ud2 guard

In the assembly above [rsp+0x8] is our stack based pointer to the object. We dereference that to get the object itself in rdi. We then dereference rdi to the address of the vtable, which we compare against a known offset from rip. This is basically checking the first and second link in the above chain. If this check fails, we execute a ud2 instruction, which will crash the program.

A more complicated example

A friend helpfully pointed out my examples were too trivial, and indeed after making things slightly more complex I found some different behavior. Let's just add a function to take advantage of the polymorphism in our objects.

void __attribute__ ((noinline)) doit(obj *o) {
    o->fp();
}
// ...
obj *;
thing *t;

o = new obj;
t = new thing;
doit(o);
doit(t);

In the prior example the compiler knew the type of every object at the time we're accessing it's function pointers. However, in this case there are two valid vtables for the given object (obj's and thing's). The assembly now looks like this:

mov    rax,QWORD PTR [rdi]     # t
lea    rcx,[rip+0x1382]        # 1cf0 <_ZTV5thing+0x10>
mov    rdx,rax
sub    rdx,rcx
rol    rdx,0x3b
cmp    rdx,0x2
jae    982 <_Z4doitP3obj+0x22>
call   QWORD PTR [rax]
# ...
ud2                            # 0x982 <_Z4doitP3obj+0x22>

After playing around with what I believe is happening is the vtables are being laid out in memory such that:

+0xce0 <_ZTV5thing>:    0x0000000000000000      0x0000555555555d40
+0xcf0 <_ZTV5thing+16>: 0x00005555555549f0      0x0000000000000000
+0xd00 <_ZTV3obj>:      0x0000000000000000      0x0000555555555d30
+0xd10 <_ZTV3obj+16>:   0x0000555555554a10      0x0000000000000000

(vtables at +16)

By putting the pointers within a fixed, known-at-compile-time range from leaves of the object hierarchy, CFI can check that the vtables being passed are one of the expected, allowed options. This is somewhat corroborated by the fact that if we add object types to our hierarchy, the guards change. Specifically, the constant 0x2 in cmp rdx,0x2 gets incremented with every object we add. This means that presuming you can overflow the vtable pointer (the second link in the diagram above) you should only be able to redirect it to another vtable within the object hierarchy (and same with type confusion). I haven't played with it enough to see what happens if we have huge vtables or more complicated object hierarchies.

Final thoughts

-fsanitize=cfi does at least two things currently. First, it protects the initial object pointers and the object -> vtable pointers with a guard. In the case of where we have multiple types in a function, it restricts the vtables to one within the object hierarchy (and possibly a subset of that?). In the case of leaf objects, it restricts it to the specific vtable.

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