Introduction

‘Low-level memory exploitation’ involves leveraging memory implementation flaws to access restricted data, elevate privileges or gain remote code execution.  These flaws may exist at the hardware, kernel or software level and are used to alter a program’s intended code execution pathway. Amongst these, software level exploits are among the most well-known and widely studied and include stack based ‘buffer overflows’, and, ‘use-after-free’ vulnerabilities in programs with poor heap memory management.  All of these weaknesses typically allow an attacker to take full control of a program and, in worse-case scenarios, obtain system wide permissions –  if that program is running with root or administrator privileges.

More technical findings may target OS memory management facilities or other kernel components, such as the pipe and paging subsystems, as demonstrated by ‘Dirty Pipe’ (CVE-2022-0847) or the ‘copy-on-write’ mechanism evidenced in the ‘Dirty Cow’ (CVE-2016-5195) exploit.  These are often more severe allowing an attacker to gain control of the operating system itself and are particularly difficult to detect and mitigate. – Whilst a software exploit might permit an attacker to gain root privileges, all post exploitation actions will still occur within the context of the operating system, leaving records in audit and log files, traces on disk and other visible artefacts such as running processes and file metadata.  A kernel exploit, on the other hand, enables all operating system components to be bypassed such as launching undetectable processes, bypassing access controls and evading antivirus software.  Typically, the only mitigation for OS memory exploits is to install the relevant vendor patches.

Higher complexity hardware-level exploits go one step further, abusing vulnerabilities in the physical hardware components such as the CPU and DRAM chips.  ‘Rowhammer’ and ‘cold boot attacks’ being key examples of these.  The former, triggering a controlled bit-flip by exploiting the physical layout of capacitors in DRAM chips, and the latter employing a data extraction technique made possible by the ability for RAM chips to retain data shortly after the power supply has been removed.

This white-paper will provide a detailed overview of a select few attack vectors from the software category.  The vulnerabilities covered will be explained from a technical perspective and will draw on real world case studies to demonstrate their impact.  Best practices for preventing and mitigating such attacks will also be provided.  Some familiarity with low-level memory concepts will be assumed.

Stack based buffer-overflows

These vulnerabilities constitute the most common low-level memory attack vector and arise due to inadequate operating system protections and coding errors. To briefly summarise, an attacker leverages the vulnerability by overwriting the contents of a buffer beyond its pre-allocated memory space. This can cause data to overflow into adjacent memory regions, potentially overwriting other variables and ideally function return addresses.  Control over the return address gives an attacker control over the execution flow of the program.

In some scenarios, an attacker will be able to inject code directly into the buffer and simply overwrite the return address to point execution back to this location.  Alternatively, additional OS and compiler protections may call for the use of more elaborate techniques in order to investigate a program’s memory space and successfully manipulate execution.

A basic illustrative example of a vulnerable program is shown below:

The above code copies the value of argv[1] into VulnerableBuffer, which has a pre-allocated size of 250 bytes, without performing any bounds checking.  This will allow an attacker to provide input larger than the buffer size overwriting the contents of the function’s local variables, and eventually the return address as shown in the following diagram:

If the return address is overwritten, the program will execute the instructions located at the attacker-controlled address rather than returning control to the caller.  In a simple program with protections such as ‘non-executable stack’, ASLR and stack canaries disabled an attacker can write shell-code to the buffer and overwrite EIP to direct execution back to these instructions.

Although much rarer, exploitable buffer overflows may also exist on the heap leading to variable corruption and even arbitrary code execution in worst case scenarios.

In either case, safe coding practices and the use of stack / heap canaries can be effective partial mitigations.  If the general use of ‘memory-safe’ programming languages with automatic bounds checking is not an option (I.E. Go, Rust, Java, Python and Swift for instance) then compiler protections such as non-executable stack, position independent code and full RELRO should be enforced.  An example for enabling all of these protections in C may look like the following:

gcc -fstack-protector-strong -fPIE -pie -Wl,-z,relro,-z,now -o output_file input_file.c

Real World Case-studies

In addition to Dirty Cow (2016), WannaCry (2017) and Krook (2019); one of the more recent high impact buffer overflow exploits was disclosed under the name ‘SIGRed’; a critical vulnerability in Windows DNS server which allowed for remote code execution, by an unauthenticated user, with SYSTEM privileges.  The vulnerability affected all windows server versions from 2003 to 2019 and received a maximum severity rating of 10. By sending a HTTP payload, with a malformed SIG record (a type of DNS record), to port 53 of vulnerable DNS servers, a heap-based buffer overflow can be triggered to write up to 64KB of data beyond the pre-allocated memory region.  –  Plenty of space for shellcode.  The impact of the vulnerability was evidently critical; justified by the fact that a rogue, unauthenticated user, could exploit a vulnerable server remotely, and gain SYSTEM privileges leading to domain admin and potentially full infrastructure compromise.

Another case study showcasing the impact of memory corruption vulnerabilities; targets a weakness in the SSPORT.SYS printer driver and is outlined in CVE-2021-3438.  This stack-based buffer overflow allowed attackers to gain arbitrary code execution with SYSTEM privileges on any system with the vulnerable driver installed and affected around 380 products released by Samsung and HP as well as several others from Xerox.  The driver in question would be loaded by windows at boot regardless of whether a printer was connected or not and allowed the execution of code in kernel space enabling operating security controls to be bypassed.  The section of code containing the vulnerability, used the notoriously unsafe C ‘strncpy’ function, with no predefined size parameter, allowing data to be written beyond the allocated buffer size.  The issue gained attention primarily due to its severity and the fact that it had existed unknowingly since 2005.

Further Mitigations

Whilst there has been a recent and gradual transition to the use of memory-safe programming languages, much of the underlying code used in the Linux kernel and programs that require backwards compatibility are compiled in languages that impose manual memory management.  To harden applications where legacy coding languages must be used, software developers should utilise secure coding practices such as bounds checking on input data, avoiding the use of vulnerable functions like gets(), and implementing techniques like stack canaries and/or non-executable stacks. Security testing and code auditing can also help to identify and remediate vulnerabilities in software.

Use-After-Free Vulnerabilities

These exploits occur when a program frees a memory location then inadvertently allows the user to influence the contents of this location and finally re-references the de-allocated chunk of memory as if it were the old object. This can lead to program security control bypasses or even arbitrary code execution on the underlying system. UAF vulnerabilities are best explained with an example:

To fully grasp this exploit I would recommend reviewing the full challenge source code here: https://play.picoctf.org/practice/challenge/187

We can see from the code above that the ‘user’ variable is a pointer to an instance of the ‘cmd’ struct located on the heap.  This object contains a string called ‘username’ and a function pointer called ‘whatToDo’.

The printMenu() function will present the user with a list of choices, each of which is represented by a different function. Depending on the selection made, processInput() will either call the function directly or, in most cases, assign the relevant address to the whatToDo attribute. doProcess() will then call the function referenced by this pointer.

Unfortunately, the programmer has made several mistakes in this example.  Firstly, the function i() which is called when the ‘leave’ option is selected will free the memory chunk pointed to by our user object. Secondly, there is another call to malloc() in the leaveMessage() function which we can use to overwrite the freed chunk of memory, and the program will continue to reference this on each iteration of the while loop. The original instantiation of the user object and pointer is set only once outside and before the while loop on the second line of main().  Because this pointer is never reset and continues to be referenced inside the loop, even after it has been freed, a UAF vulnerability arises. Combined with the second call to malloc() which allows the user to trivially modify the contents of this memory region and another coding error whereby one of the intended sequences in the program allows us to reference the freed object, with our new value, without the application prematurely overwriting it, our exploit is possible. If the call to leaveMessage() was instead done by setting the value of user->WhatToDo and calling doProcess(). rather than with a direct function call in processInput() then the exploit would not be possible.

Since this program was written with the intention of teaching how a UAF vulnerability works, there is also a section of code that will allow us to put our exploit to use.  Upon inspection, you may notice selecting the ‘Subscribe’ option, by inputting the “S” character, will cause processInput() to assign the s() function pointer to user–>whatToDo which then gets called by doProcess().  Inside of s() is an intentional memory leak, printed to the screen, revealing the address of a hidden function that will print a flag.  The function’s label is hahaexploitgobrrr().

Knowing this we can intuit the sequence that will allow us to control the program’s execution and redirect it to call this secret function:

Input 1: ‘S’ ← This will leak the runtime memory address of hahaexploitgobrrr() which we can then write into the struct function pointer attribute whatToDo of our cmd object pointed to by user.
Input 2: ‘I’ ← This causes the the chunk of memory allocated on the heap for ‘user’ to be freed and is now overwritable as far as the heap manager is concerned.
Input 3: ‘l’ ← This calls the leaveMessage() function which contains another malloc() call allowing us to overwrite the previously freed chunk.  Since the first attribute of the ‘cmd’ struct is the function pointer we can insert our malicious address straight into the region of memory.

Execution then naturally passes to doProcess(), the next statement in our while loop, which attempts to call the function pointed to by the address we just inserted.
Using the pwntools python library, we can use the following script to run the above sequence, triggering the memory leak and writing this into the function pointer user→whatToDo.  Our flag printing function will then be called.  The script is a slightly modified version of a solution provided by Crypto-Cat which can be found here: https://github.com/Crypto-Cat/CTF/blob/main/ctf_events/pico_gym/pwn/unsubscriptions_are_free/uaf.py

Output:

Case Studies

There have been numerous instances of UAF vulnerabilities in the wild leading to arbitrary code execution; as was the case with Google Chrome’s recent bug tracked as (CVE-2022-3038).  Similar to the code above, the finding leveraged a UAF heap corruption vulnerability, triggered by having the victim visit a maliciously crafted HTML page.  This allowed an unauthenticated attacker to gain RCE with minimal user interaction (one click attack) and was rated by CVSS as an 8.8 high.  The issue was present in all desktop Chrome clients < 105.0.5195.52 for Mac and Linux, < .54 for windows and was resolved in a vendor released patch in the following software update.          

A slightly more complex UAF, involving a double-free race condition, affected the built in VPN Point to Point Tunneling Protocol on all Microsoft Windows versions, desktop and server (Windows 7 through to 11 and 2008 – 2022 server).  A detailed analysis can be found here:  https://labs.nettitude.com/blog/cve-2022-23270-windows-server-vpn-remote-kernel-use-after-free-vulnerability/

However, to summarise, when the same resource is freed more than once, often by different threads with access to the same resource that do not implement proper locking, this can allow the contents of those freed chunks to be edited by an attacker.  The vulnerable raspptp.sys driver in this particular vulnerability, (CVE-2022-23270), unintentionally provides shared, simultaneous multithreaded access to an object called the ‘call context structure’.  Accessing this object concurrently via two different threads initiates a race condition triggering the resource to be freed twice via CallFree() creating an entry point for exploitation.

This vulnerability has been demonstrably proven to be capable of crashing the target system and has the potential to achieve kernel RCE via further memory corruption.  Microsoft have since released a patch as of May 2022.

Several other recent UAFs have been reported but with limited technical details and PoC’s including CVE-2022-43552 which outlines a UAF in Curl versions 7.16.0 to 7.86.0 and CVE-2023-0266 -> A critical linux zero-day, affecting one of the built-in sound drivers resulting in privilege escalation with up to ring 0 access. UAFs have also been responsible for various iphone jailbreaks such as Pwn20wnd’s IOS 12.4 semi-untethered jailbreak.  The vulnerability (CVE-2019-8605) was originally patched by Apple in IOS 12.3 but was re-introduced in the 12.4 update.

Use-After-Free Mitigations

To recap, when a program written in a low-level language, without automatic memory management, allocates memory dynamically, it is responsible for freeing that memory when it is no longer needed.  An attacker can exploit a UAF vulnerability by causing the program to free a memory region that is still being used, or by tricking the program into using a pointer to a memory location that has already been freed. This may cause the program to behave unexpectedly or even execute code injected by the attacker.

To prevent UAF vulnerabilities, software developers can use secure coding practices such as nulling out pointers after freeing them on the heap and clearing the contents of any de-allocated memory regions – Especially, if they are used to store passwords or cryptographic keys. (memset() can be used to do this in C) which sets n number of contiguous bytes pointed to by str to the value of c as shown in the function prototype below: 

void *memset(void *str, int c, size_t n)

Simply using free() on an object will deallocate the chunk in the heap but does not zero out the contents for performance reasons, so memset() may be used immediately before the call to free().

Further to this, the risk of introducing UAFs into program code can be mitigated by avoiding the use of uninitialized pointers, implementing garbage collection and implementing proper resource locking mechanisms to avoid double-free race conditions via concurrent threads.   Security testing and code auditing can also help to identify and remediate UAF vulnerabilities.  The usage of memory safe programming languages is evidently an effective mitigation; whilst vulnerabilities do still arise they are much less frequent and much of the human error that leads to severe UAFs is removed by using a higher level language.

Format String Vulberabilities

Format string vulnerabilities (FSVs) arise when a program passes user controllable input directly into a string formatting function, such as ‘printf’ or ‘sprintf’.
If this vulnerability is present an attacker may cause a program to read or write data to or from arbitrary memory locations.  Consequently, string format vulnerabilities often lead to severe memory leaks, allowing all addresses and contents on the stack to be extracted.  As a result, FSVs can be immensely useful in facilitating other attacks where specific program addresses need to be identified (E.G. Libc functions in return-to-libc attacks) or when the program’s address space is randomised with ASLR.  FSVs can also be useful when the attacker does not have direct access to the binary itself, for local debugging purposes, but may control or observe the input / output of the program.  This may be the case when attacking applications remotely over the network / internet.

Whilst frequently used in conjunction with other attacks such as buffer overflows to achieve a more powerful exploit; due to the possibility of being able to write to arbitrary memory locations, FSVs can also be used to directly exploit a program without the need for a separate attack vector.  Consequently, they may lead to arbitrary code execution, denial of service, or information disclosure.

Technical Overview and Example Exploitation

Both printf() and sprintf() are variadic functions, meaning they can take a variable, indefinite number of inputs.  The only required argument is the initial format string which will be examined for placeholders used to retrieve and parse other values passed to the function.  As with any function call, passed arguments are placed onto the stack.  On 32-bit systems all arguments are stored here and on 64-bit systems arguments 7 and above will be placed onto the stack whilst 1-6 are stored in registers RDI, RSI, RDX, RCX, R8, R9 respectively.  Although the calling conventions do slightly differ between 32 and 64-bit systems, FSVs are exploitable on both platforms with minimal adjustment.

The following diagram illustrates how the contents of the stack is loaded by printf() in the accompanying example code:  
(Remember the stack grows downwards towards lower memory addresses so the most recent value pushed, will appear at the bottom of the diagram)

Void caller(char* ChangeMe, int isAdmin, char* FormatString){
    int CreditCardNumber = 123456789;
printf(FormatString);
}

We can see here that the arguments passed to printf() are pushed onto the stack in reverse order just before the return address.  The exact function declaration for printf() in the C library is as follows and shows that there are an undefined number of parameters denoted by the triple period:

int printf(const char *format, ...)

The only function input that printf() is aware of is the format string, which is inherently trusted to correctly reference other variables passed to the function.  Due to standardised calling conventions, printf() also knows that these additional variables will exist either in registers or in memory locations directly above the format string. For instance the ‘%N$x’ notation, inside the format string, instructs printf() to substitute the nth argument into the output string.  If N tries to select a specific argument which is never passed by the caller, for example:

printf(“%10$x”, name, age, height, dob); - Argument 10 does not exist.

printf() will happily walk up the stack and read whatever data it finds in the 10th position where it would expect to find the 10th argument.  Thus, if an attacker controls the format string they can read the contents of arbitrary memory locations, even those that lie beyond the currently active stack frame. Consider:

printf(“%500$x”, a, b ,c);

or even: 

printf(“%2000$x”);

This arbitrary read technique, via an attacker controlled format string, is elegantly showcased in the following code from the PICO CTF 2022 ‘flag leak’ challenge:

As we can see scanf() reads in a string, up to 127 characters in length, into the variable story.  This variable is then passed straight into printf() on line 32.  We also have a variable containing our flag on line 24 before printf() is ever called, so we can deduce that our flag is stored in memory somewhere above the location where our controllable format string is stored.  Fuzzing this number and reading from increasingly higher memory regions (interpreting the contents as a string, as we know our flag is a string) reveals that the flag is stored at position 24 above our format string:

Taking this further, another feature of printf() that can be exploited is the ‘%n’ notation which will write into a variable the value corresponding to the number of bytes emitted by printf(), up until the occurrence of ‘%n’.  Since we control the format string, we can insert an arbitrary number of characters and prepend them to ‘%n’ and control the value that will be written.  printf() also provides a shorthand way of doing this with a padding feature whereby prepending an integer to the ‘x’ character, will pad the argument to be printed with that number of zero’s.  For instance:

printf(“%50x%2000$”);  –  Will instruct printf() to substitute the value found at what it thinks is the memory location holding the 2000th argument and prepend this value with fifty zeros.  From here, appending ‘n’ after the ‘$’ will cause the number of emitted bytes to be written into the argument selected by ‘%2000$’, in this case the memory location supposedly holding the 2000th argument: printf(“%50x%2000$n”);

This technique would allow for even further exploitation such as overwriting return addresses to control program execution, or overwriting specific program data to gain additional privileges.

Real World Case-Studies

Whilst a public PoC was never released, a format string vulnerability affecting one of Asus’ high performance WiFi routers (the RT-AX88U) allowed an unauthenticated attacker to remotely write to arbitrary memory locations leading to RCE.  The exploit could be leveraged by sending a specially crafted packet to the router as an unauthenticated user.  The vulnerability was patched in a subsequent firmware update, however, this example showcases the impact that an FSV can have as well as the ease of exploitation.  The issue was assigned a severity rating of 9.8 (Critical) and is tracked under CVE-2022-26674.

A flaw affecting certain versions of MariaDB was discovered whereby a format specifier within the processing of SQL queries was user controllable.  This FSV allowed low privilege users to execute commands in the context of the service account resulting in a privilege escalation vector.  The vulnerability was rated as a 7.8 High and arose due to a lack of input validation.  This issue is tracked as CVE-2022-24051.

Mitigations

Format string vulnerabilities can be prevented by specifying the format string as a constant inside the program code rather than as user input.  If user input needs to be printed, then this can be stored safely in a buffer and then passed to printf as an argument as shown below:

Char buffer[50];
fgets(buffer, sizeof(buffer), stdin);  // Safe Call to fgets().
printf("You entered: %s\n", buffer);  // Pre-defined Format String.

This code safely writes user input (up to the allocated buffer length) into the buffer and then passes this buffer to printf() which has a predefined constant format string.  In this instance the user does not control the initial format string, and printf() will interpret all contents of the buffer as a literal string. <- So user controlled format specifiers are no longer possible.

Further mitigations may include the use of format guards which limit the types of format specifiers able to be used in string formatting functions.  For instance the programmer may decide to only allow %s and %d.  If any other format specifier is encountered then the program should gracefully exit.

Conclusion

This paper has provided a technical overview of some common binary exploitation techniques.  Whilst this isn’t an exhaustive list the vulnerabilities covered are the most frequently encountered in the wild and have been widely studied with regards to mitigations and preventative measures.  In any case, the best remediation tactic is to transition to memory safe program languages; however, if older languages must be used then user input validation and best coding practices should be implemented throughout.

References

  • https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/
  • https://www.youtube.com/watch?v=YGQAvJ__12k
  • https://labs.nettitude.com/blog/cve-2022-23270-windows-server-vpn-remote-kernel-use-after-free-vulnerability/
  • https://github.com/Crypto-Cat/CTF/blob/main/ctf_events/pico_gym/pwn/unsubscriptions_are_free/uaf.py
  • https://www.youtube.com/watch?v=PKqMsaKGdlM
  • https://youtu.be/3akzDXMVW8k
  • https://youtu.be/1S0aBV-Waeo
  • https://www.youtube.com/watch?v=2HxyGWD1htg
  • https://www.youtube.com/watch?v=DhVRI33s-D0
  • Hacking: The Art of Exploitation, 2nd Edition (Jon Erickson)
  • https://play.picoctf.org/practice/challenge/187