Exploit the Fuzz – Exploiting Vulnerabilities in 5G Core Networks

Following on from our previous blog post ‘The Challenges of Fuzzing 5G Protocols’, in this post, we demonstrate how an attacker could use the results from the fuzz testing to produce an exploit and potentially gain access to a 5G core network.

In this blog post we will be using the PFCP bug (CVE-2021-41794) we’d previously found using Fuzzowski 5GC in Open5GS[1] (present from versions 1.0.0 through 2.3.3), to demonstrate the potential security risk a buffer overflow can cause.

We will cover how to examine the bug for possible exploit, how to create the actual exploit code, and how it is integrated into a PFCP message.  Finally, we will discuss mitigations that can help reduce/eliminate this type of attack.

In this blog we have taken steps to simplify the process in an effort to make it available to a wider audience.  We have used a proof of concept to explain the techniques and turned off some of the standard mitigations to reduce the complexity of the exploit.

Background

Previously we used Fuzzowski 5GC to fuzz the UPF component of the Open5GS project (version 2.2.9).  The fuzz testing found a buffer overflow bug in the function ‘ogs_fqdn_parse‘.  This type of bug is reasonably easy to exploit, and the basic idea is shown below.

We will use this bug to show how a malicious payload can be written to the variable ‘dnn’, the variable overflowed to set the return address, and execution gained to take control of the UPF process.

Test Environment

To make the exploit development easier and to keep the technical details as simple as possible, a few security mitigations were disabled:

  • ASLR – Address Space Layout Randomization
  • Stack protection
  • Stack non-exec (NX)

Many systems run with insufficient mitigations, so testing exploitation in this context makes sense for showing how exploitation might be possible against one of these platforms. Furthermore, most of these mitigations can be bypassed given the right bug conditions. Although we didn’t investigate bypassing these mitigations for this research, it may be possible that certain manifestations of the bug allow for exploitation on a more hardened platform.

The following environments and tools were used to test and develop the exploit:

  • Open5GS version 2.2.9 – The target
  • Ubuntu 20.04 VM – Host for Open5GS
  • Kali Linux 2021.1 VM – Tools for exploit development
  • MsfVenom – Metasploit standalone payload generator
  • Msf-pattern_create – Unique string pattern generator
  • Msf-pattern_offset – Finds substring in string generated by msf-pattern_create
  • GDB Debugger – For examining the execution
  • Visual Code – Source code editor
  • Netcat – To test the exploit

In the past low-level knowledge of assembler was often required to write the exploit, but with the advent of tools such as MsfVenom, generating exploit code is now ridiculously easy. However, there are still instances where custom shell code is required to develop a working exploit, for example if space is limited.

Where to Start?

First, we need to find exactly where the buffer overflow occurs and why.  This stage has already been covered so please refer to the previous blog post ‘The Challenges of Fuzzing 5G Protocols’ for more details.

For a quick recap we have a buffer created on the stack called ‘dnn’ which is defined as 100 bytes long.

This buffer is passed into the function ‘ogs_fqdn_parse’ as a pointer along with the source data buffer (message->pdi.network_instance.data) containing the value of ‘internet’ which is 8 bytes long (message->pdi.network_instance.len).

When the ‘ogs_fqdn_parse’ function executes, the memcpy copies data from the source to the destination overflowing the destination buffer by 5 bytes in this example.  By reviewing the source code, we can see that we have control over the contents of the ‘src’ parameter and the ‘length’ parameter.  Also note that we have read beyond the end of the ‘src’ buffer which could be used to leak information, but that’s a different type of bug that we won’t explore here.

On examining the function ‘ogs_fqdn_parse’, it’s possible to see that we can write as much data as we like into the destination buffer.  The only issue we face when trying to insert assembly code is the insertion of ‘.’ character or the null termination byte after the memcpy at line 302 and 304 above.

So, although we can write as much data as we want, we are limited to how much code can be inserted if we want to keep this example as simple as possible.  As the variable ‘len’ is an unsigned byte the maximum number of bytes for assembly code is limited to 255 bytes.  We could of course write some custom assembly code to jump over the ‘.’ characters but this adds extra complication.

Before rushing off to code our exploit, it is always worth looking at the other places this function is used as there may be a better opportunity to exploit the bug.  So lets search the source code for other calls to ‘ogs_fqdn_parse’.

As we can see this function is used by several other components in the 5G core which could also be potential targets.  This opens up the attack vectors as we now have potentially multiple 5G core components that can be exploited in a similar fashion.  While the fuzzer only found a single bug in one component (in this case the UPF), examining the code shows that other components such as the AMF, MME, SMF, SGWC may be susceptible to the same issue.  This also highlights that fuzz testing alone will not necessarily find all vulnerabilities, in this example we fuzzed the AMF, MME and failed to discover the same bug.

It is worth pointing out that some of these other calls are not exploitable due to size checks before the call to ‘ogs_fqdn_parse’.  For example, in the following code, the function ‘ogs_nas_5gs_decode_dnn’ the size of the input data is checked against the size of the data structure ‘ogs_nas_dnn_t’ on line 122 below.  This prevents the function ‘ogs_fqdn_parse’ from being called if the input data is larger than the destination structure, which prevents the stack from being corrupted.

For our example exploit we will continue to use the original location of the buffer overflow discovered by our fuzzer.  The function ‘ogs_pfcp_handle_create_pdr’ which is called when the UPF processes a PFCP Session Establishment Request message.

Proof of Concept

To demonstrate the exploit, it is easier to create a simple test program before attempting to exploit the actual component where the stack may be more complicated.

The code below shows a simple test program structured similar to how the function is called in the actual UPF application.  Initially we want to determine the offset on the stack of the function return address for the function ‘test’ in our example.

This simple test program creates a variable ‘dnn’ on the stack along with a unique string of characters for the ‘name’ variable.  The function ‘ogs_fqdn_parse’ is then called to cause the stack corruption and overwrite the return address of the ‘test’ function.  This should cause the test program to crash as the value written to the return address is unlikely to be a valid memory address.

Generating the data for the name variable is done by using the Metasploit Framework tool ‘pattern_create.rb’.  This generates a unique sequence of characters that can later be used to find the relevant offset for the return address.

Command to generate unique string of characters:
$ /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 1000

The function ‘ogs_fqdn_parse’ is expecting the data to start with a single byte indicating the length of the data to follow.  To copy the 1000 bytes of data that has been generated onto the stack, we need to split it up and specify the relevant sizes in bytes.

Before we can run the program, we need to switch off ASLR (Address Space Layout Randomization).

$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"

Then enable unlimited file size for core files.

$ ulimit -c unlimited

If we compile the test application and run it, we get the following output:

$ cc -g test_stack.c -o test_stack

$ ./test_stack 
dnn address: 0x7fffffffdfb0
zsh: segmentation fault (core dumped)  ./test_stack

As expected, the program crashed because we wrote 1000 bytes of data over important stack variables required for normal program execution.  The observant amongst you will have realized that we actually wrote 1010 bytes to the stack, but we will come back to that later.

So now we have run the test program and it has generated a core file, it’s now time to examine the crash and find the offset to the magic return address.  We will now examine the core file using the debugger gdb.

Using the following command let’s load the core file with gdb so we can examine it:

$ gdb ./test_stack --core=core

The following image shows the stack layout before the memcpy is executed.  Looking at the layout we should only need to write approximately 116 bytes to overwrite the return address, so writing 1000 bytes is a bit of an over kill for the proof of concept.  However, the stack in the UPF may have more variables between the variable ‘dnn’ and the return address which may require us to write a reasonable amount of data to the stack before we reach the location of the return address we are trying to change.

All we need to do now is use another Metasploit Framework tool called ‘pattern_offset.rb’ to calculate the offset of the return address.  To do this we take the address stored in the saved RIP register associated with the function ‘test’ stack frame (i.e. 0x4131654130654139), and use it as the query parameter for ‘pattern_offset.rb’.

$ /usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 1000 -q 0x4131654130654139
[*] Exact match at offset 119

An exact match is found at offset 119.  We need to be careful here as this is not the actual offset of the return address.  119 is the offset of part of our string from the beginning of our unique string.  As mentioned earlier we actually wrote 1010 bytes to the stack, which we now need to consider when calculating the final offset of the return address.

We can check this by displaying the first 128 bytes of the stack variable ‘dnn’ in our example program.

Using the following command in the debugger to show the contents of ‘dnn’
(gdb) x/128bb dnn 

Note there is a gap between the end of the ‘dnn’ variable and the frame pointer which is due to 16-byte alignment requirements.

Creating the Exploit

This is where in the good old days you would crack open the ‘64-IA-32 Architectures Software Developer Instruction Set Reference Manual[2]’ and start hand crafting some assembly code.  Fortunately, these days the Metasploit Framework has made this task very simple indeed.

The Metasploit Framework tool ‘MsfVenom’ can generate a number of different payloads/exploits by simply providing a few command line options.

We have chosen the payload ‘linux/x64/shell_bind_tcp’ which generates code to start a shell prompt when a connection is made to the specified port 5600.  The option to append code to exit the program has also been included.  The ‘-f’ option is used to generate C code.

As we can see from the output of ‘MsfVenom’, the payload is conveniently only 94 bytes long.  This means that we can copy it directly to the stack without worrying about the ‘.’ or null character messing up our assembly code. If the payload was longer than 255 bytes (the maximum size of a chunk we can copy) we would need to write some custom assembly code to skip over the ‘.’ character that is inserted after each chunk of data is copied.

If we now replace the unique ASCII string in our test program with the output from ‘MsfVenom’ and the return address, we should have a working exploit!

Exploiting the UPF Component

Now we have a working proof of concept we know our exploit works.  All we need to do now is repeat the process of finding the return address when returning from the function ‘ogs_pfcp_handle_create_pdr’, calculate the offset of the ‘dnn’ buffer and finally create a suitable PFCP Session Establish Request message to send the exploit to the UPF.

Sounds straightforward enough, however the reality is a little more complicated.  To start with the function ‘ogs_pfcp_handle_create_pdr’ is a bit more complicated than our simple ‘test’ function.  From the point where we overflow the stack variable ‘dnn’, we need execution to continue until the end of the function ‘ogs_pfcp_handle_create_pdr’ for our exploit code to be executed.  As there are several other function calls and variable assignments, we need these to complete without crashing the UPF.

The main problem we have is the variable ‘pdr’ which is a pointer on the stack.  This will be overwritten when we overwrite the return address because it is at a higher address on the stack compared to the ‘dnn’ variable.

To fix this we need to find the offset of the variable ‘pdr’ and assign it to something sensible when we overflow the variable ‘dnn’.  We can use the Metasploit tools ‘pattern_create.rb’ and ‘pattern_offset.rb’ again to determine this offset.  Once the variable ‘pdr’ has been fixed up, the function then executes to completion and returns, executing our exploit code.

The value of ‘pdr’ is at a fixed offset in our example as we have disabled ASLR.  This means that we can hard code the value in our exploit once we have calculated it.

To create the PFCP Session Establish Request message we used Fuzzowski 5GC and set the Network Instance Information Element to contain our exploit code.

We then tested the exploit by using Fuzzowski 5GC to send the PFCP messages to the UPF.

After successful testing of the exploit, we used the POC (Proof of Concept) feature of Fuzzowski 5GC to generate a standalone python exploit script.  This saves a lot of time compared to hand crafting the messages and having to calculate nested length fields and checksums etc.  Below is a section of the PFCP Session Establish Request message containing our exploit, the ‘pdr’ pointer fix and the return address.

Mitigations

To simplify this blog post and hopefully make it understandable to a wider audience, the standard mitigations that help prevent these kinds of bugs being exploited have been disabled.  This enabled a much simpler exploit process to be followed, allowing us to demonstrate the potential damage that can be done by exploiting buffer overflows.

The following mitigations would make the demonstrated exploit much harder, but not necessarily impossible to exploit.

  • ASLR – Address Space Layout Randomization
  • Stack protection
  • Stack execution prevention

ASLR – Address Space Layout Randomization

Randomizing the loading address of various parts of an application make it difficult for an attacker to locate parts of the application they want to target.  For example, most operating systems implement some form of ASLR which changes the address of things such as the stack, heap, and library modules.  Generally, applications need to be compiled with support for ASLR.

Stack Protection

By enabling stack protection options during compilation of an application, extra code can be inserted before and after each function in an attempt to detect stack corruption within a function.  This may prevent further exploitation to some degree, but it will cause the application to exit which is not necessarily desirable.

Stack Execution Prevention

By preventing the area of memory that contains the stack from being executable, the type of exploit demonstrated would not be possible.  There are however other techniques that can be used to circumvent this protection.

Other Mitigations

There are several other mitigations that could help prevent this type of bug in the first place.  For example:

  • Better functional/system testing
  • Better coding standards/practices
  • Use of more secure versions of functions to replace functions like memcpy
  • Code reviews
  • Use of static analysis tools
  • Fuzz Testing
  • Design with security in mind

The Bug Fix

In version 2.3.4 of Open5GS a fix was implemented for our original reported bug (CVE-2021-41794).  While this patch fixed the reported bug there are still issues with the function ‘ogs_fqdn_parse’.

As the src‘ buffer is effectively controlled by the attacker the above issues are possible depending upon how the function ‘ogs_fqdn_parse‘ is called.  If size checks have been done before calling the function, then it’s not so much of an issue, however it is asking for trouble to have the user of the function validating what’s passed in to prevent the buffer overflow.

The current patch in version 2.3.4 is susceptible to the second issue of writing a null byte beyond the end of the destination buffer.

This just highlights the importance of using all possible mitigations to avoid releasing vulnerable code in the first place.

The original PFCP bug (CVE-2021-41794) was concerned with the calculation of the ‘len‘ variable at line 328 above, and its use in the memcpy at line 334 without being validated. The expectation was for the function ‘ogs_fqdn_parse‘ to be completely rewritten instead of an ‘if‘ statement being added to only fix the original bug.

Although reading beyond the length of the ‘src‘ variable requires a coding error in the calling function, the one byte buffer overflow can be caused by data passed in by an attacker.

  • 01/11/2021 – Notified Open5GS of issues (read beyond ‘src‘ variable and one byte overflow to ‘dst‘ variable)
  • 06/11/2021 – Requested example packet to cause one byte overflow
  • 09/11/2021 – Sent example python script and screenshot to demonstrate one byte overflow
  • 15/11/2021 – Open5GS main branch patched

This will be fixed in Open5GS versions released after 2.3.6. However the solution of extending the buffers by one byte is not an ideal fix.

We hope this blog post has given you a high level technical insight into the dangers of a simple buffer overflow bug, and how it can potentially be exploited by an attacker!

Glossary

TermDescription
ASLRAddress Space Layout Randomization
UPFUser Plane Function
FuzzerSoftware that generates invalid input for testing applications
StackArea of memory used to store function call data
MMEMobility Management Entity

References

[1] GitHub – open5gs/open5gs: Open5GS is an Open Source implementation for 5G Core and EPC

[2] 64-IA-32 Architectures Software Developer Instruction Set Reference Manual

How to work with us on Commercial Telecommunications Security Testing

NCC Group has performed cybersecurity audits of telecommunications equipment for both small and large enterprises. We have experts in the telecommunications field and work with world-wide operators and vendors on securing their networks. NCC Group regularly undertake assessments of 3G/4G/5G networks as well as providing detailed threat assessments for clients. We have the consultant base who can look at the security threats in detail of your extended enterprise equipment, a mobile messaging platform or perhaps looking in detail at a vendor’s hardware. We work closely with all vendors and have extensive knowledge of each of the major vendor’s equipment.

NCC Group is at the forefront of 5G security working with network equipment manufacturers and operators alike. We have the skills and capability to secure your mobile network and provide unique insights into vulnerabilities and exploit vectors used by various attackers. Most recently, we placed first in the 5G Cyber Security Hack 2021 Ericsson challenge in Finland.

NCC Group can offer proactive advice, security assurance, incident response services and consultancy services to help meet your security needs.

If you are an existing customer, please contact your account manager, otherwise please get in touch with our sales team.