1 A little bit of theory

1.1 Calling convention

When dealing with 32-bit exploitation, one must be familiar with different call conventions. There are several:

  • cdecl
  • stdcall
  • fastcall
  • thiscall (C++)

The calling conventions define rules such as the passage of arguments or the cleaning of the Stack. In the x64 universe, Microsoft has decided to apply only one convention, namely the fastcall.

This convention defines the following rules:

  • The first 4 parameters of the function are placed in the registers RCX, RDX, R8, R9.
  • A 32-bit padding is made on the Stack (Shadow Stack) used by the function to move the first 4 function parameters on the Stack
  • The remaining arguments are placed after the Shadow Stack
  • The Stack must be aligned on 16 bytes

We will now check the information provided by Microsoft with practice. Let's compile the following program:

// cl test.c 
#include <stdio.h>
int func(char *a, char *b, char *c, int d, int e)
{
    printf("func done !\n");
    return 0;
}

int main(int argc, char **argv)
{
    printf("Main"); // help to find the function
    func("str1", "str2", "str3", 12, 10);
    return 0;
}

We open the compiled program with the debugger of your choice (x64dbg, windbg, Ida), and place a breakpoint before calling the func function.

On the screenshot below, we can see the passage of the arguments, the first 4 are actually in the registers and we can see that the last parameter of the func function is positioned after the Shadow Stack

Calling convention

1.2 Windows Security

We will now take a look at the security implemented by Microsoft to protect itself from Buffer Overflow attacks.

1.2.1 ASLR (Address Space Layout Randomization)

ASLR is a protection that appeared with Windows Vista. This protection makes it possible to randomize the memory addressing of system images (DLL, EXE, ...). In the Microsoft environment, the ASLR works differently from the Linux environment. On Linux, memory addressing is random each time a program is loaded. On windows, the addressing is changed each time the machine is restarted.
Moreover, it is not globally enabled on the system; this means that each binary, each library, can be compiled with the ASLR enabled or not

We will see a little further on how to find information about a binary.

1.2.2 DEP (Data Execution Prevention)

DEP is a protection that appeared with Windows XP. Two types of protection can be distinguished, the DEP Hardware and the DEP Software. Originally, processors did not have protection like code execution, that's why there is a DEP Software protection. It should be noted that it is not possible to disable this protection for programs compiled in 64 bits. The objective of this protection is to mark memory regions as non-executable means that code cannot be run from that region of memory. The DEP Hardware is activated on all programs.

It is possible to see the value of the DEP directly in the Windows task manager, by adding the appropriate column.

1.2.3 Stack Cookie

Stack Cookie is a protection that developers can add to the compilation (/GS). This option is enabled by default with Visual Studio. The purpose of this protection is to protect function returns. During the prologue, a cookie is placed on the stack, just with the return address of a function. Before returning to the calling function, a comparison is made between the value of the prolog cookie and the value before returning to the function. If the values are not identic, it induces a buffer overflow attack.

Without the stack canary, the memory mapping looks like this:

    +---------------------------------+
    |        Function argument        |
    +---------------------------------+
    |        Save Return address      |
    +---------------------------------+
    |        Save Frame pointer       |
    +---------------------------------+
    |        Padding                  |
    +---------------------------------+
    |        char badbuffer[64]       |
    +---------------------------------+

With the canary, memory mapping looks like this:

    +---------------------------------+
    |        Function argument        |
    +---------------------------------+
    |        Save Return address      |
    +---------------------------------+
    |        Save Frame pointer       |
    +---------------------------------+
    |        Padding                  |
    +---------------------------------+
    |        Stack Canary             |
    +---------------------------------+
    |        char badbuffer[64]       |
    +---------------------------------+

I know there are other protections, but they will be addressed during operation and their bypass.

2. Where to find this information ?

There are several ways to find this information:

The information we need is stored in the Optional Header structure of the PE format, in the DllCharacteristics field. Below is a small python script to perform the verification:

import lief
from sys import argv
import colorama

def _color_print(name):
    colorama.init(autoreset=True)
    def color_print(func):
        def wrapper(*args, **kwargs):
            ret = func(*args, **kwargs)
            if ret != False:
                color = colorama.Fore.GREEN
            else:
                color = colorama.Fore.RED
            print(color+name+": %s" % (ret))
        return wrapper
    return color_print

class PESecurity:
    def __init__(self, pe):
        self.pe = pe
        self.optional_header = pe.optional_header
        self.characteristics = self.optional_header.dll_characteristics_lists
        self.display_results()

    @_color_print("ASLR")
    def aslr(self):
        if lief.PE.DLL_CHARACTERISTICS.DYNAMIC_BASE in self.characteristics:
            return True
        else:
            return False

    @_color_print("SafeSEH")
    def seh(self):
        if lief.PE.DLL_CHARACTERISTICS.NO_SEH in self.characteristics:
            return True
        else:
            return False

    @_color_print("DEP")
    def dep(self):
        if lief.PE.DLL_CHARACTERISTICS.NX_COMPAT in self.characteristics:
            return True
        else:
            return False

    @_color_print("ControlFlowGuard")
    def cfg(self):
        if lief.PE.DLL_CHARACTERISTICS.GUARD_CF in self.characteristics:
            return True
        else:
            return False

    @_color_print("HighEntropyVA")
    def high_entropy_va(self):
        if lief.PE.DLL_CHARACTERISTICS.HIGH_ENTROPY_VA in self.characteristics:
            return True
        else:
            return False

    def display_results(self):
        self.aslr()
        self.seh()
        self.dep()
        self.cfg()
        self.high_entropy_va()

class ELFSecurity:
    # lief.segments (GNU_RELRO && DT_BIND_NOW -> full relro)
    # lief.segments (GNU_RELRO  -> partial relro)
    # lief.sections (__stack_chk_fail -> stack canary)
    def __init__(self, elf):
        self.elf = elf
        self.fortified_function = []
        self.display_results()

    @_color_print("RELRO")
    def relro(self):
        try:
            self.elf.get(lief.ELF.SEGMENT_TYPES.GNU_RELRO)
            if self.elf.get(lief.ELF.DYNAMIC_TAGS.BIND_NOW):
                return "FULL Relro"
            else:
                return "Partial Relro"
        except:
            return False

    @_color_print("Stack Canary")
    def canary(self):
        try:
            self.elf.get_symbol("__stack_chk_fail")
            return True
        except:
            return False

    @_color_print("NX")
    def nx(self):
        try:
            if self.elf.get(lief.ELF.SEGMENT_TYPES.GNU_STACK).flags == 6:
                return True
        except:
            return False

    @_color_print("Pie")
    def pie(self):
        return self.elf.is_pie

    @_color_print("RPATH")
    def rpath(self):
        try:
            if elf.get(lief.ELF.DYNAMIC_TAGS.RPATH):
                return True
        except:
            return "No RPATH"

    @_color_print("RUNPATH")
    def runpath(self):
        try:
            if elf.get(lief.ELF.DYNAMIC_TAGS.RUNPATH):
                return True
        except:
            return "No RUNPATH"

    @_color_print("Fortify")
    def fortify(self):
        func_fortified = 0
        for function in self.elf.symbols:
            if function.name.endswith("_chk"):
                func_fortified += 1
                self.fortified_function.append(function.name)

        if func_fortified > 0:
            return True
        else:
            return False

    def fortified_functions(self):
        print("Fortified Functions:")
        for function in self.fortified_function:
            print("{: >20}".format(function))

    def display_results(self):
        self.relro()
        self.canary()
        self.nx()
        self.pie()
        self.fortify()
        self.rpath()
        self.runpath()
        self.fortified_functions()

class Checker:
    def __init__(self, filename):
        self.binary = lief.parse(filename)
        if lief.is_elf(filename):
            ELFSecurity(self.binary)
        if lief.is_pe(filename):
            PESecurity(self.binary)

b = Checker(argv[1])

And if we run it on the binary powershell.exe:

PS C:\Users\neko\Desktop\dev> python .\checksec.py C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
ASLR: True
SafeSEH: False
DEP: True
ControlFlowGuard: True
HighEntropyVA: False

References

x64 Software Conventions
How DEP works
Windows 10 Mitigations