Comparative Analysis: Reversing Rust and C binaries

Ahmet Göker
8 min readJun 6, 2023

Hello everyone!

Today, I set out on an exciting adventure of reverse engineering, with the goal of uncovering the differences between Rust and C binaries. With enthusiasm, I immersed myself in the intricate realm of binary analysis, eager to unravel the unique characteristics of these programming languages. Now, filled with anticipation, let us begin this captivating journey together!

Rust Programming

Rust is a modern systems programming language known for its focus on safety, performance, and concurrency. Developed by Mozilla, Rust combines the low-level control language like C and C++ with high-level abstractions that enhance code reliability and security. It aims to provide developers with a productive and efficient language for building reliable software, especially in systems and embedded programming domains.

One of rust’s standout features is its ownership system, which enforces strict memory safety and eliminates common issues like Null pointer dereferences, data races, and memory leaks. It achieves this through a set of compile-time checks and guarentess, such as the ownership, borrowing, and lifetime concepts. These feautures make Rust programs less prone to bugs and vulnerabilities commonly associated with memory-related errors. Additionally, Rust’s rich type system, pattern matching, and functional programming capabilities enable expressive and elegant code, making it easier to write robust and maintainable software. Its comprehensives package manager, Cargo, simplifies dependency management and facilities the creation of reusable libraries and projects

Rust binary and C binary

In this purpose, I will use binaryninja. The goal of this blog is to illustrate being disassembled binaries and the differences of symbols. I will want to show the differences the symbols between Rust and C file.

Before explaining the differences between these two files, let me show you the file size:

As you can see that Rust file is bigger than C, but why?

  1. Rust is designed with a strong focus on memory safety and provides high-level abstraction that may result in additional code being generated during compilation.
  2. Rust does not have a runtime environment like some other programming languages, such as Java or C#. However,, it does provide certain features and runtime checks for memory safety, such as ownership and borrowing, which may result in additional code being generated.
  3. The size of the binary can be influenced by the libraries and dependencies used by the code. If a Rust program uses larger or more extensive libraries, it may contribute to a larger binary size.

Lets now dive into the symbols of two files:

Rust
C

The symbol used to represent Rust source code files is different from the binary file format used for C because they serve different purposes and have different structures.

Rust source code files typically use the file extension “.rs” and contain human-readable code written in the Rust programming language. These files are intended to be read and understood by developers and are used to write and compile programs. The differences in the symbol used for Rust source code files and the binary file format used for C reflect the distinction between human-readable code and machine-executable instructions. Rust source code symbols are chosen to represent the language and its features, while binary file formats for C are designed to encode machine instructions.

C
Rust

In the case of C, the calling convention used in most x86–64 systems is known as the system V AMD64 ABI(Application Binary Interface). According to this convention, the base pointer register RBPis typically used as a frame pointer to reference the current stack frame, which can be useful for debugging and stack unwinding. The return value of a value of a function is usually stored in the accumulator register RAX

Rust, being a systems programming language, can also use the System V AMD64 ABI or other calling conventions depending on the compiler and platform. Therefore, the usage of registers like RBPand RAXin Rust binaries can be similiar to C, as they both adhere to the same calling conventions.

The design goals and principles of a programming language can also influence how registers are used. Rust, for example, emphasizes memory safety and zero-cost abstractions. The compiler may make certain choices to enforce ownership and borrowing rules, which can affect register allocation strategies. C, being a lower-level language, provide more direct control over memory and hardware, which can result in different choices regarding register usage.

Rust
C

In the C programming language, hardcoded strings can be easily identified by searching for a minimum hex-to-string match. However, when searching for such strings, the variable declaration that holds the string, such as char*, may not be directly visible. On the other hand, in the Rust programming language, hardcoded strings are enclosed within a different code block or context. The value of the variable representing the string, such as a String type, can be observed before the remaining hardcoded string but its representation is contained within a distinct reference or structure.

Rust
C

Rust’s print call:

In Rust, the standard way to print or display information is by using the println! macro or other formatting macros. These macros provide a flexible and type-safe approach to printing values. When using these macros, you typically pass in a format string that specifies how the values should be formatted and displayed. The ArgumentV1 you are seeing is likely a representation of the argument placeholder used in the format string to indicate where the value should be inserted. The Display is to be mentioned to the trait in Rust that provides a general way to format and display types. It is commonly used as part of the formatting process.

C’s print call:

In C, the standard way to print or display information is typically done using the printf function from the C standard library. When using printf you pass in a format string that specifies how the values should be formatted and displayed. The format string in C uses different syntax and placeholders compared to Rust. The format specifiers in C are typically represented using a percent sign followed by a letter as an example %d In c, you will not see the ArgumentV1 or Display symbols because the format string syntax is different.

Rust
C

Technical explanation of the key oberservations:

  1. The _start function in the Rust binary is responsible for the initial execution of the program, similar to the entry point in a C binary
  2. The main function in the Rust binary does not directly start the program. Instead, it calls another main function, specifically rust::main . This additional layer of abstraction layer of abstraction may be present due to the way Rust organizes its code or handle initilization processes.
  3. Vector is passed as an argument to the _print function. The _print function can be inferred as a function responsible for printing or displaying the contents of the Vector in some way.

The benefits of using Rust over C programming

Compiling Rust binaries offers significant security advantages over C. Rust’s memory safety, thread safety, strong type system, absence of undefined behaviour, secure memory management, safe concurrency, and community focus on security all contribute to its reputation as a more secure programming language. These language design choices, combined with the feautures provided by Cargo and the security-conscious community, make Rust a compelling choice for developing secure and reliable software systems. Let me write some benefits of using Rust:

  1. Type safety : Rust’s ownership system and built in concurrency primitives help prevent data races and other concurrency-related bugs. By enforcing strict rule on mutable access to shared data, Rust promotes safer and more secure concurrent programming. In contrast, C lacks built-in-support for thread safety, making it more susceptible to data races and related security issues.
  2. Compiler-Enforced security checks: The Rust compiler performs extensive static analysis and security checks during compilation. It detects potential vulnerabilities, such as buffer overflows or integer overflows, and reports them as compile-time errors. This proactive approach to security helps developers identify and address security issues before they can be exploited.

In the next blog I will cover about binary-explotation . For this blog, I reckon that its enough to illustrate how Rust works.

Summary

In this blog post, I embarked on an interesting exploration of Rust and C binaries by studying their reverse engineering. I examined the unique features of these programming languages and compared their binary structures. This article serves as an introductory glimpse into the fascinating world of analyzing binary code, setting the stage for upcoming discussions.

In the upcoming chapters, we will delve even deeper into the captivating realm of binary analysis. We will explore the concepts of exploitation and mitigation techniques, understanding how vulnerabilities can be identified and exploited in C and Rust binaries. Furthermore, we will take a closer look at Rust’s robust security mechanisms, which offer strong protection against common software vulnerabilities. Get ready for an enlightening journey as we uncover the secrets of binary exploitation and discover the powerful security measures present in the Rust programming language. Stay tuned for a comprehensive exploration of secure coding practices and advanced mitigation strategies.

Ahmet Göker | Threat Cases operator

--

--