Automating binary vulnerability discovery with Ghidra and Semgrep

“Humans are more suited to recognize food than to keep large graphs in their head.”
— Halvar Flake

TL;DR

I’ve released some tools to help automate vulnerability discovery tasks via static analysis techniques:

  • Rhabdomancer is a simple Ghidra script that locates all calls to potentially insecure API functions in a binary. Auditors can backtrace from these candidate points to find pathways allowing access from untrusted input.
  • Haruspex is another Ghidra script that is able to extract all pseudo-code generated by the Ghidra decompiler in a format that should be suitable to be imported into an IDE, such as VS Code, or parsed by static analysis tools, such as Semgrep.
  • My Semgrep rules are specially crafted to help auditors identify potential bugs and locate hotspots in C/C++ code on which to focus their attention.

Semgrep is a static analysis tool that works on source code, but thanks to Haruspex we can leverage its power also against closed source binaries.

Becoming a (better) security researcher

For some reason, I’ve always been fascinated by computer hacking. I’ve been lucky enough to make hacking my profession and I’ve been at it almost non-stop for more than two decades. Over the years, however, I’ve been sidetracked by my management career and for a while I almost stopped developing exploits.

Recently, I decided I wanted to get back in shape as a security researcher.

During my journey, among other things, I produced a trilogy of talks about vulnerability research and exploit development, with a focus on proprietary and closed-source software. If you’re interested in hunting for vulnerabilities in binary code and you have some spare time, I suggest you brush up on some concepts by watching the following videos.

A Bug’s Life: Story of a Solaris 0day

Party pack: https://github.com/0xdea/raptor_infiltrate19

The INFILTRATE Effect: 6 Bugs in 6 Months

Party pack: https://github.com/0xdea/raptor_infiltrate20

My Last Solaris Talk (Not Your Average Keynote)

Party pack: https://github.com/0xdea/raptor_romhack21

Challenges of binary vulnerability discovery

Now that we are on the same page with the fundamentals, it’s time to explore the challenges of vulnerability discovery in binary code. In this article, I purposefully won’t cover dynamic analysis and fuzzing. Instead, I’m going to focus on static analysis.

If it’s an undeniable truth that your ultimate target is the binary, having the source code at your disposal during the audit helps a lot. Binary analysis is cumbersome and usually slower than source code analysis, and it requires a deeper low-level knowledge of the target platform and compiler implementation. Static analysis tasks in particular benefit of access to the source code, and there are many commercial and free tools that aim to automate some of these tasks.

One such tool is Semgrep. I’m pretty fond of it and I’ve already covered it in some detail in a previous article. After developing a C/C++ ruleset focused on vulnerability discovery, I had the idea to apply it to the pseudo-code generated by a decompiler to speed up binary analysis. This isn’t an original idea: other researchers have recently applied this concept in slightly different ways. It turns out this approach is quite effective, albeit with some limitations. It’s not perfect, but *it works*.

A binary bug hunting toolkit

When I began my journey into becoming a better vulnerability researcher, I wanted to understand what I was doing instead of blindly firing up tools against target binaries. Therefore, I almost exclusively focused on manual analysis. Of course, this gets old pretty fast, especially with large binaries.

To improve my performance (and learn Ghidra scripting while I was at it), I developed Rhabdomancer, a simple Ghidra script that locates all calls to potentially insecure API functions in a binary. Auditors can backtrace from these candidate points to find pathways allowing access from untrusted input. Coupled with manual analysis, this allowed to discover some interesting vulnerabilities in large binaries in a reasonable amount of time.

The next logical step was to extract all pseudo-code generated by the Ghidra decompiler in a format suitable to be imported into an IDE, such as VS Code, or parsed by static analysis tools, such as Semgrep. This is exactly what my Haruspex script does. This setup should help auditors identify potential bugs and locate hotspots in binary code on which to focus their attention. Let’s see it in action against a real target.

It’s showtime!

We’re now going to apply my toolkit against some real-world vulnerabilities in Zyxel ZyWALL Unified Security Gateway (USG) appliances that I’ve recently discovered and reported to Zyxel.

The following memory corruption bugs in the zysh binary were collectively assigned CVE-2022-26531:

  • Buffer overflows in the “configure terminal > diagnostic” command.
  • Buffer overflow in the “debug” command.
  • Buffer overflow in the “ssh” command.
  • Format string bugs in the “extension” argument of some commands.

The following command injection vulnerability in the zysh binary was assigned CVE-2022-26532:

  • OS command injection in the “packet-trace” command.

To use Rhabdomancer against the zysh binary, we follow these steps:

  1. Auto analyze our target binary with the default analyzers (at least).
  2. Copy the script into our “ghidra_scripts” directory.
  3. Open the Script Manager in Ghidra and run the script (or run it via the Tools > Rhabdomancer menu or the shortcut “Y”).
  4. Open Window > Comments and navigate [BAD] candidate points in different badness tiers.

In the following screenshot, we can see an example of Rhabdomancer-generated comments (in some case edited following manual analysis) for our target binary:

To use Haruspex, instead, we follow these steps:

  1. Analyze our target binary and manually add/modify functions if needed.
  2. Copy the script into our “ghidra_scripts” directory.
  3. Open the Script Manager in Ghidra and run the script (or run it via the Tools > Haruspex menu or the shortcut “H”).
  4. Enter an output path in which the pseudo-code will be saved.

This will extract all pseudo-code and save it to the specified output directory:

At this point, we can browse the exported pseudo-code with our favorite IDE:

Finally, we can use Semgrep with my custom C/C++ rules to (re)discover some of the vulnerabilities:

Buffer overflows

The buffer overflow in the “debug” command is not immediately discoverable by my default “raptor-insecure-api-scanf-etc” rule, because the target binary calls a variation of the fscanf() API function: __isoc99_fscanf(). This requires some customization to be detected.

Format string bugs

As expected, all format string bugs are detected by the “raptor-format-string-bugs” rule.

OS command injection

Although the calls to sprintf(), stpcpy(), and strcat() that build the “tcpdump” command string are flagged by my ruleset, the specific instance of command injection that I’ve described in my advisory is not detected by the “raptor-command-injection” rule. Since the vulnerable code path is slightly convoluted and the injection involves abusing a GTFObins trick, this vulnerability can only be detected manually by understanding the context.

The bottom line is: not all vulnerabilities can be discovered through automated means. For some of them, you still need to use your brain 🤯  While effective, tools such as static analyzers and fuzzers can’t do the entire job. However, some degree of automation still helps: tools can guide you by showing where to apply focus.

In closing, don’t forget the most important lesson… Go get your hands dirty and have fun!