
One day, I decided to reverse-engineer a Solana program, only to realize that my usual go-to tool, IDA, had no support for it. The only available options were Solana’s command-line utilities, a Binary Ninja plugin, and a Ghidra plugin that hadn’t been maintained for a long time. There might be other implementations out there, but for IDA — one of the most widely used tools among reverse engineers — there was nothing suitable.
After diving into Solana’s architecture and reading through some source code, I decided to fill this gap by developing a Solana processor plugin for IDA, which is now published at Github:
GitHub - Decurity/solana-ebpf-ida-processor: Solana Virtual Machine bytecode processor for IDA Pro
First, a few words about the blockchain. At its core, the Solana VM (SVM) uses a modified version of eBPF — a technology that started out filtering network packets in Linux. Over time, it evolved into an environment for running secure programs in a privileged context.
There are several SVM implementations. One of them is Solana rBPF — a fork of the Rust virtual machine, which was developed by Solana Labs and is maintained now by the Anza team. This implementation is used by the Agave validator and allows programs to run via the interpreter or using the Just-in-Time compilation into x86–64 architecture before execution.
Smart contracts in Solana are called programs. They are compiled into ELF files and are stored on-chain after deployment. Every program has an initial entrypoint() function where execution begins. During execution, programs can access special syscalls defined at the low level of SVM, implementing important features such as logging, invoking other programs, calculating hashes, retrieving VM state information, and more.
Thanks to the LLVM compiler infrastructure, Solana programs can be written in any language that targets LLVM’s BPF backend. However, Rust remains the most popular choice for Solana development.
As I mentioned earlier, I came across a few existing solutions for disassembling SVM programs. Some of them are:
However, I personally prefer working with IDA. So, after this brief research, I decided to start developing my own IDA plugin.
Let’s talk a bit about processor modules that are used by IDA to support different architectures. Such modules allow IDA to “understand” an instruction set and disassemble corresponding binaries compiled for a specific architecture.
Processors can be written by anyone in C++ or Python and basically should have:
So, the Solana eBPF plugin developed is essentially the processor module that understands the SVM assembler. It is based on the existing eBPF processor implementation that was forked and modified to support Solana-specific changes.
The following diff highlights the changes made between the original rBPF and Solana rBPF:
https://github.com/qmonnet/rbpf/compare/main...solana-labs:rbpf:main
Some of the key modifications include:
Syscalls in Solana eBPF are similar to BPF’s helper functions. Here is the full list of Solana syscalls:
https://github.com/solana-labs/solana/blob/7700cb3128c1f19820de67b81aa45d18f73d2ac0/sdk/program/src/syscalls/definitions.rs#L39
During the reversing process, their detection helps a lot, as they highlight key moments when the program interacts with the low-level environment.
For example, hash calculations can be easily tracked by looking for sol_sha256, sol_keccak256, sol_blake3 syscalls. Interaction with other programs (Cross-Program Invocations) is done via sol_invoke_signed_c and sol_invoke_signed_rust syscalls, while data exchange between programs occurs through sol_get_return_data and sol_set_return_data. PDA calculation and creation rely on sol_try_find_program_address and sol_create_program_address syscalls.
Other handful syscalls include sol_log_, sol_log_data, sol_log_pubkey, and similar ones. They often print logs related to the execution, which helps to understand the code better. For example, when a program uses the popular Anchor framework, instruction handlers log the names of the instructions, making analysis much easier:

All of these syscalls are detected by the plugin and highlighted via cross-references in IDA.
Identifying strings and their references in code is usually one of the most helpful techniques when analyzing a binary. When searching for where a specific functionality is executed or trying to understand a piece of assembly, a reverse engineer can rely on strings that resemble execution logs, intermediate data, or other meaningful references. However, in Rust binaries, this becomes a challenge since strings are stored as a contiguous blob without null terminators. Below is an example of the blob:

Hex-Rays has even written a plugin that tries to resolve the problem. The Solana eBPF processor follows a quite similar approach.
Usually, all strings are located in the read-only section of the binary. While analyzing the binary, cross-references from the executable code are identified, which almost always point to the start of a string. Based on this, the following algorithm was implemented:
For example, imagine that the read-only section contains a contiguous string blob: String2String1String3. To extract individual strings, we define an empty strings array, which will store tuples of string offsets and lengths. The algorithm works as follows:
The approach is optimized and detects strings with their corresponding lengths quite efficiently:

Almost every binary relies on various library functions, many of which are included during the linking stage of compilation. Detecting these functions during reverse engineering significantly speeds up the process, as it gives much more clues about the context of the code and avoids unnecessary analysis of already known library routines. If a binary retains all symbols, no additional steps are needed to identify these functions. However, in most cases, binaries are stripped and do not contain symbols. Solana binaries are no exception — by default, all programs deployed on-chain are stripped.
To detect functions when symbols aren’t available, Hex-Rays developed a great technology called FLIRT.
FLIRT (Fast Library Identification and Recognition Technology) allows to determine functions in binaries based on pre-generated signatures from different libraries. Generally, the signatures generation process follows these steps and involves the usage of utilities from the IDA’s FLAIR toolset:

For a library file, the corresponding PAT file is generated. This is a text file that contains entries for every module within the library (functions in our case), its public names, and internal references. Each entry is written on the new line.
The format of an entry is the following, where ASUM is the CRC16 sum from the next ALEN bytes:

If some of the bytes are mutable, they are marked by two dots and won’t be considered later in the signature matching. Below is the example of an entry for the __read library function that has two internal references to __openfd and __IOERROR :

Refer to this file for more detailed documentation.
When a PAT file is generated, it can be packed into the SIG file that contains an optimized tree-based information about these signatures and can be applied to a binary. The step is executed via the sigmake tool and ensures that no duplicates are detected during the generation. Otherwise, the EXC file is created with exceptions that should be resolved.
Now, the question is: where can we find the libraries needed to generate Solana signatures?
When a Solana program written in Rust is compiled, a new target directory is created, containing various intermediate files generated during the compilation process. For example, here’s the basic structure of this folder for a simple hello world program:

The target/sbf-solana-solana subdirectory contains compiled eBPF binaries including libraries with the .rlib extension. All dependencies will be placed in the release/deps folder, from which we can take libraries used by the binary.
That’s good, but… Is the Solana eBPF compilation deterministic?
At first, I thought that the answer is no and the classic FLIRT approach would work badly, as function code can change each time for several reasons:
Based on these considerations, a separate plugin was even developed that targets more flexible signature descriptions, but with a loss in the speed of their detection.
However, later I found several discussions on this topic. One of them is here, where developers discuss the verification of on-chain Solana programs. Additionally, Anchor has a feature to produce verifiable builds, described here.
So, theoretically, if all dependency versions and optimization settings remain the same, the generated machine code should also be identical. This makes the FLIRT approach quite effective — if all versions of the core libraries used in Solana program compilation are built and their signatures are extracted, they may be detected with a low false-positive rate.
For convenience, the Solana IDA signatures generation factory was developed:
GitHub - Decurity/solana-ida-signatures-factory
It has scripts to automatically perform the following steps:
The final signatures file can be generated via the sigmake tool from the Hex-Rays FLAIR toolkit.
This is how function detection looks when using generated signatures for the core solana-program library:

Even though code decompilation isn’t possible with this approach — since that part of IDA is closed-source and doesn’t support custom architectures — there are still many ways to improve the plugin. For example, the following planned to be implemented:
Feel free to suggest your ideas and contribute!
Reversing Solana programs with IDA was originally published in Decurity on Medium, where people are continuing the conversation by highlighting and responding to this story.
The researchers who write this are the ones who run the audits.