Hacking Book | Free Online Hacking Learning

Home

using c language to realize control flow protection (cfg) in linux

Posted by verstraete at 2020-04-13
all

Translation: myswsun

Estimated contribution fee: 200rmb

Submission method: send an email to Linwei Chen 360.cn, or log in to the web page for online submission

0x00 Preface

The latest version of windows has a new mitigation measure called control flow protection (CFG). Before a non direct call -- for example, function pointers and virtual functions -- check the target address for the table of valid call addresses. If the address is not an entry to a known function, the program will terminate.

If a program has a buffer overflow vulnerability, an attacker can use it to override a function address and control the program execution flow by calling that pointer. This is a method of ROP attack. The attacker constructs a series of accessory address chains. A accessory is a set of instruction sequences containing RET instructions, which are all in the original program and can be used as the starting point of indirect calls. The execution process will jump from one accessory to another to do what the attacker wants to do without any code provided by the attack.

Two very broad techniques to mitigate ROP attacks are address space layout randomization (alsr) and stack protection. The former is to randomize the loading base address of the module to achieve unexpected results. Addresses in ROP attacks rely on real-time memory layout, so attackers must find and exploit information leaks to bypass ASLR.

With regard to stack protection, the compiler assigns a value above the other stack assignments and sets it to a random value for each thread. If the function return address is overwritten by a buffer overflow, this value will also be overwritten. This value is verified before the function returns. If it does not match a known value, the program will terminate.

The principle of CFG is similar. Before the control is transmitted to the pointer address, a check is made, not a value, but the target address itself. This is very complex, unlike stack protection, which requires platform coordination. This verification must be notified in all reliable call targets, whether from the main program or the dynamic library.

Although not widely deployed, it is worth mentioning that clang's safestack. Each thread has two stacks: a "secure stack" to hold return pointers and other values that can be accessed safely, and a "non secure stack" to hold data such as buffers. Buffer overflow will break other buffers, but will not overwrite the return address, which limits the impact of ring breaking.

0x01 examples

Using a small C program, demo. C:

    int     main(void)     {         char name[8];         gets(name);         printf("Hello, %s.n", name);         return 0;     }

It reads a name and stores it in the buffer, and prints it at the end of the line feed. The sparrow has all five internal organs. A native call to gets() does not verify the bounds of the buffer and can be used to exploit buffer overflow vulnerabilities. It's clear that both the compiler and linker throw warnings.

For simplicity, assume that the program contains a hazard function.

    void         self_destruct(void)     {         puts("**** GO BOOM! ****");     }

The attacker calls this dangerous function with a buffer overflow.

To make the attack simple, assume that the program does not use ASLR (for example, the - fpie and - pie compilation options are not used in GCC / clang). First, find the address of the self_destruct() function.

    $ readelf -a demo | grep self_destruct         46: 00000000004005c5  10 FUNC  GLOBAL DEFAULT 13 self_destruct

Because it's on a 64 bit system, it's a 64 bit address. The size of the name buffer is 8 bytes. In the assembly, I see an additional 8-byte allocation, so there are 16 bytes filled in, and then 8 bytes cover the return pointer of self_destruct.

    $ echo -ne 'xxxxxxxxyyyyyyyyxc5x05x40x00x00x00x00x00' > boom         $ ./demo < boom     Hello, xxxxxxxxyyyyyyyy?@.     **** GO BOOM! ****     Segmentation fault

With this input I have successfully used buffer overflow to control execution. When main tries to go back to libc, it jumps to the threat code and crashes. Turning on stack protection can prevent this exploitation.

    $ gcc -Os -fstack-protector -o demo demo.c         $ ./demo < boom     Hello, xxxxxxxxaaaaaaaa?@.     *** stack smashing detected ***: ./demo terminated     ======= Backtrace: =========     ... lots of backtrace stuff ...

Stack protection successfully prevented utilization. In order to bypass this, I will have to guess the Canary value or find the information leakage that can be used.

The conversion of stack protection to program looks like this:

    int         main(void)     {         long __canary = __get_thread_canary();         char name[8];         gets(name);         printf("Hello, %s.n", name);         if (__canary != __get_thread_canary())             abort();         return 0;     }

However, it is not possible to implement stack protection in C, buffer overflow is an uncertain behavior, and canary is only valid for buffer overflow, and also allows the compiler to optimize it.

0x02 function pointer and virtual function

After the attacker's successful utilization, the upper management added password protection measures. It looks like this:

    void         self_destruct(char *password)     {         if (strcmp(password, "12345") == 0)             puts("**** GO BOOM! ****");     }

The password is hard coded, it's stupid, but suppose it's not known to the attacker. Stack protection has been required by upper management, so it is assumed to be turned on.

In addition, the program also makes a little change. Now it uses a function pointer to realize polymorphism.

    struct greeter {             char name[8];         void (*greet)(struct greeter *);     };          void     greet_hello(struct greeter *g)     {         printf("Hello, %s.n", g->name);     }          void     greet_aloha(struct greeter *g)     {         printf("Aloha, %s.n", g->name);     }

There is now a greeter object and function pointer to implement runtime polymorphism. Think of him as a virtual function of handwritten C. Here is the new main function:

    int         main(void)     {         struct greeter greeter = {.greet = greet_hello};         gets(greeter.name);         greeter.greet(&greeter);         return 0;     }

(in a real program, something else will provide a greeter and pick its own function pointer)

Instead of overriding the return pointer, an attacker has the opportunity to override the function pointer in the structure. Let's use it again as before.

    $ readelf -a demo | grep self_destruct         54: 00000000004006a5  10 FUNC  GLOBAL DEFAULT  13 self_destruct

We don't know the password, but we do know that the password verification is 16 bytes. The attack should skip 16 bytes, that is, skip the verification (0x4006a5 + 16 = 0x4006b5).

    $ echo -ne 'xxxxxxxxxb5x06x40x00x00x00x00x00' > boom         $ ./demo < boom     **** GO BOOM! ****

Whether it's stack protection or password protection, it doesn't help. Stack protection only protects return pointers, not function pointers in structures.

This is where CFG works. With CFG turned on, the compiler inserts a check before calling greet(). It must point to the beginning of a known function, or it will terminate the program like stack protection. Because self_destruct() is not the beginning of the function, but the program will terminate after utilization.

However, there is no CFG mechanism in Linux. So I intend to achieve it myself.

0x03 function address table

As described in the PDF link at the top of the article, CFG on windows is implemented using bitmap. Each bit represents 8 bytes of memory. If more than 8 bytes contain the beginning of the function, this bit is set to 1. Verifying a pointer means verifying its associated bit in bitmap.

With regard to my CFG, I decided to keep the same 8-byte solution: the lower 3 bits of the target address would be discarded. The remaining 24 bits are used as the index of the bitmap. All other bits in the pointer are ignored. A 24 bit index means that the maximum bitmap size can only be 2MB.

24 bit is enough for 32-bit system, but it is not enough for 64 bit system: some addresses can not represent the beginning of the function, but set their bit to 1. This is acceptable, especially if only known functions are used as the target of indirect calls, which reduces the adverse factors.

Note: the bits converted from pointers to integers are unspecified and not portable, but this implementation works well no matter where.

Here are the parameters of CFG. I encapsulate them as macros for compilation. This CFG bit is an integer type that supports bitmap arrays. CFG? Resolution is the number of bits discarded, and "3" at a time is a granularity of 8 bytes.

    typedef unsigned long cfg_bits;         #define CFG_RESOLUTION  3     #define CFG_BITS        24

Given a function pointer F, the following macro exports the index of bitmap.

    #define CFG_INDEX(f)              (((uintptr_t)f >> CFG_RESOLUTION) & ((1UL << CFG_BITS) - 1))     struct cfg {             cfg_bits bitmap[(1UL << CFG_BITS) / (sizeof(cfg_bits) * CHAR_BIT)];     };

Manually register the function in bitmap using CFG < register().

    void         cfg_register(struct cfg *cfg, void *f)     {         unsigned long i = CFG_INDEX(f);         size_t z = sizeof(cfg_bits) * CHAR_BIT;         cfg->bitmap[i / z] |= 1UL << (i % z);     }

Because registering functions at run time requires consistency with ASLR. If ASLR is on, bitmap will run differently each time. It is worthwhile to XOR each element of bitmap with a random number, which makes it more difficult for attackers. After the registration is complete, the bitmap also needs to be adjusted to read-only permissions (mprotect()).

Finally, the validation function is used before the indirect call. It ensures that f is first passed to cfg_register(). Because it is called frequently, it needs to be as fast and simple as possible.

    void         cfg_check(struct cfg *cfg, void *f)     {         unsigned long i = CFG_INDEX(f);         size_t z = sizeof(cfg_bits) * CHAR_BIT;         if (!((cfg->bitmap[i / z] >> (i % z)) & 1))             abort();     }

Done, now use it in Main:

    struct cfg cfg;              int     main(void)     {         cfg_register(&cfg, self_destruct);  // to prove this works         cfg_register(&cfg, greet_hello);         cfg_register(&cfg, greet_aloha);              struct greeter greeter = {.greet = greet_hello};         gets(greeter.name);         cfg_check(&cfg, greeter.greet);         greeter.greet(&greeter);         return 0;     }

Now take advantage of:

    $ ./demo < boom         Aborted

Under normal circumstances, self destruct() will not be registered because it is not a legal target for indirect calls, but utilization still cannot work because it is called in the middle of self destruct(), and it is not a reliable address in bitmap. Verification will terminate the program before use.

In a real application, I will use a global CFG bitmap, and use the inline function in the header file to define the CFG "check().

Although it is possible to implement directly in C without using tools, it will become more cumbersome and error prone. It is right to implement CFG in the compiler.