Skip to content

FuzzingLabs/sol-azy

Repository files navigation

sol-azy

sol-azy is a modular CLI toolchain for static analysis and reverse engineering of Solana sBPF programs.
It supports disassembly, control flow analysis, and custom Starlark-based rule evaluation.

Features

  • Compile Solana programs (Anchor or native SBF)
  • Build an audit-friendly snapshot of each program in an Anchor project. (NEW)
  • Perform static AST-based security analysis with Starlark rules
  • Reverse-engineer .so bytecode: disassembly (& rust equivalences), control flow graphs, and immediate value decoding
  • Modular CFG editing (dotting)
  • On-chain binary or raw AccountInfo retrieval (fetcher)

Getting Started

Prerequisites

Installation

git clone https://github.com/your-org/sol-azy.git
cd sol-azy
cargo build --release

Or for development:

cargo run -- <command> [options]

Usage

Here are some basic examples. See docs for full documentation.

Build a project

cargo run -- build --target-dir ./my_project --out-dir ./out/

Generate an audit-friendly snapshot of each program

cargo run -- recap -d ../my-solana-project

Helium protocol used as an example here

Program fanout

Crate: /home/ectario/Documents/solana/helium-program-library/programs/fanout

Instruction Signers Writable Constrained Seeded Memory
distribute_v0 payer fanout, payer, to_account, token_account, voucher fanout(has_one); receipt_account(constraint,spl); to_account(spl); voucher(has_one) voucher
initialize_fanout_v0 payer collection, collection_account, fanout, master_edition, metadata, payer, token_account collection(spl); collection_account(spl); token_account(spl) collection, fanout, master_edition, metadata fanout(space)
stake_v0 payer, staker collection_metadata, fanout, from_account, master_edition, metadata, mint, payer, receipt_account, stake_account, voucher fanout(has_one); from_account(spl); mint(constraint,spl); receipt_account(spl); stake_account(spl) collection_master_edition, collection_metadata, master_edition, metadata, voucher voucher(space)
unstake_v0 payer, voucher_authority fanout, mint, payer, receipt_account, sol_destination, stake_account, to_account, voucher fanout(has_one); receipt_account(constraint,spl); to_account(spl); voucher(has_one) voucher

Program rewards_oracle

Crate: /home/ectario/Documents/solana/helium-program-library/programs/rewards-oracle

Instruction Signers Writable Constrained Seeded Memory
set_current_rewards_wrapper_v0 oracle oracle, recipient key_to_asset(constraint); recipient(has_one) oracle_signer
set_current_rewards_wrapper_v1 oracle oracle, recipient key_to_asset(constraint); recipient(has_one) oracle_signer
set_current_rewards_wrapper_v2 payer payer, recipient key_to_asset(constraint); recipient(has_one); sysvar_instructions(address) oracle_signer

Program lazy_transactions

Crate: /home/ectario/Documents/solana/helium-program-library/programs/lazy-transactions

Instruction Signers Writable Constrained Seeded Memory
close_canopy_v0 authority canopy, lazy_transactions, refund lazy_transactions(has_one)
close_marker_v0 authority block, executed_transactions, lazy_transactions, refund lazy_transactions(has_one) block
execute_transaction_v0 payer executed_transactions, lazy_signer, lazy_transactions, payer block(constraint); lazy_transactions(has_one,constraint) block, lazy_signer
initialize_lazy_transactions_v0 payer canopy, executed_transactions, lazy_transactions, payer canopy(owner,constraint); executed_transactions(owner,constraint) lazy_transactions lazy_transactions(space)
set_canopy_v0 authority canopy, lazy_transactions lazy_transactions(has_one)
update_lazy_transactions_v0 authority canopy, executed_transactions, lazy_transactions canopy(owner,constraint); executed_transactions(owner,constraint); lazy_transactions(has_one)

.... SNIP ....

Run static analysis

cargo run -- sast --target-dir ./my_project --rules-dir ./rules/

HIGHLY RECOMMENDED: Using the --release is wayyyyy faster, so if you don’t need debug logs, I’d recommend using it

sastout

Reverse engineer a compiled .so

cargo run -- reverse --mode both --out-dir ./out --bytecodes-file ./program.so --labeling --reduced

Result in immediate_data_table, disassembly.out and cfg.dot (converted to svg):

0x100000000 (+ 0x0): b"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\xf7\x00\x01\x00\x00\x008\x01\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x [...SNIP...]"
0x100000120 (+ 0x120): b"y\x11\x00\x00\x00\x00\x00\x00\x85\x10\x00\x00#\x04\x00\x00\x95\x00\x00\x00\x00\x00\x00\x00\xbf\x12\x00\x00\x00\x00\x00\x00\xbf\xa1\x00\x00 [...SNIP...]"
0x1000021f8 (+ 0x21f8): b"y\x11\x00\x00\x00\x00\x00\x00\x05\x00\xff\xff\x00\x00\x00\x00\x95\x00\x00\x00\x00\x00\x00\x00\x18\x02\x00\x00Z\x85\xf6\x1d\x00\x00\x00\x00 [...SNIP...]"
0x100004210 (+ 0x4210): b"\xbf#\x00\x00\x00\x00\x00\x00a\x11\x00\x00\x00\x00\x00\x00\xb7\x02\x00\x00\x01\x00\x00\x00\x85\x10\x00\x00\xa4\xff\xff\xff\x95\x00\x00\x00\x00\x00\x00\x00"
0x100004238 (+ 0x4238): b"\xbf#\x00\x00\x00\x00\x00\x00y\x11\x00\x00\x00\x00\x00\x00\xb7\x02\x00\x00\x01\x00\x00\x00\x85\x10\x00\x00\x9f\xff\xff\xff\x95\x00\x00\x00\x00\x00\x00\x00"
0x100004260 (+ 0x4260): b"y\x13\x00\x00\x00\x00\x00\x00y\x11\x08\x00\x00\x00\x00\x00y\x14\x18\x00\x00\x00\x00\x00\xbf1\x00\x00\x00\x00\x00\x00\x8d\x00\x00\x00\x04\x00\x00\x00\x95\x00\x00\x00\x00\x00\x00\x00"
0x100004290 (+ 0x4290): b"\xbf$\x00\x00\x00\x00\x00\x00y\x13\x08\x00\x00\x00\x00\x00y\x12\x00\x00\x00\x00\x00\x00\xbfA\x00\x00\x00\x00\x00\x00\x85\x10\x00\x00#\xfe[...SNIP...]"
0x1000043e0 (+ 0x43e0): b"You win!"
0x1000043e8 (+ 0x43e8): b"You lose! + "
0x1000043f4 (+ 0x43f4): b"Not enough data. Need two u32 values.src/entrypoint.rs"
0x10000442a (+ 0x442a): b"Error: memory allocation failed, out of memory"
0x100004458 (+ 0x4458): b"Errorlibrary/alloc/src/raw_vec.rscapacity overflow"
0x10000448a (+ 0x448a): b"a formatting trait implementation returned an errorlibrary/alloc/src/fmt.rs\x00\x00\x00"
0x1000044d8 (+ 0x44d8): b"invalid argslibrary/core/src/fmt/mod.rsindex out of bounds: the len is :"
0x100004520 (+ 0x4520): b"panicked at "
0x10000452c (+ 0x452c): b":\x0a but the index is : "
0x100004542 (+ 0x4542): b"00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899\x00\x00\x00\x00\x00\x00"
0x100004610 (+ 0x4610): b"\x00\x00\x00\x00\xd0C\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf1C\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00"
0x100004630 (+ 0x4630): b"\x00\x00\x00\x00\xd8C\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00"
0x100004640 (+ 0x4640): b"\x00\x00\x00\x00\xd0C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
0x100004650 (+ 0x4650): b"\x00\x00\x00\x00\x19D\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00a\x01\x00\x00#\x00\x00\x00"
0x100004668 (+ 0x4668): b"\x00\x00\x00\x00\x80\x16\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008!\x00\x00\x00\x00\x00\x00\xe0!\x00\x00\x00\x00\x00\x00P\x16\x00\x00"
0x100004698 (+ 0x4698): b"\x00\x00\x00\x00yD\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00"
0x1000046a8 (+ 0x46a8): b"\x00\x00\x00\x00]D\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00!\x02\x00\x00\x05\x00\x00\x00"
0x1000046c0 (+ 0x46c0): b"\x00\x00\x00\x00x\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\x16\x00\x00"
0x1000046e0 (+ 0x46e0): b"\x00\x00\x00\x00\xbdD\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00d\x02\x00\x00 \x00\x00\x00"
0x1000046f8 (+ 0x46f8): b"\x00\x00\x00\x00\xd8D\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00"
0x100004708 (+ 0x4708): b"\x00\x00\x00\x00\xd8D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1fE\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1fE\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"
0x100004738 (+ 0x4738): b"\x00\x00\x00\x00\x08"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10"\x00\x00"
0x100004758 (+ 0x4758): b"\x00\x00\x00\x00\xffD\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.E\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00"
0x100004778 (+ 0x4778): b"\x00\x00\x00\x00\xd8D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@E\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00"
..SNIP...
entrypoint:
    mov64 r2, r1                                    r2 = r1
    mov64 r1, r10                                   r1 = r10
    add64 r1, -96                                   r1 += -96   ///  r1 = r1.wrapping_add(-96 as i32 as i64 as u64)
    call function_308                       
    ldxdw r7, [r10-0x48]                    
    ldxdw r8, [r10-0x58]                    
    ldxdw r1, [r10-0x38]                    
    mov64 r2, 8                                     r2 = 8 as i32 as i64 as u64
    jgt r2, r1, lbb_91                              if r2 > r1 { pc += 79 }
    ldxdw r1, [r10-0x40]                    
    ldxw r2, [r1+0x0]                       
    stxw [r10-0xa8], r2                     
    ldxw r1, [r1+0x4]                       
    stxw [r10-0xa4], r1                     
    mov64 r1, 0                                     r1 = 0 as i32 as i64 as u64
    stxdw [r10-0x40], r1                    
    lddw r1, 0x100004610 --> b"\x00\x00\x00\x00\xd0C\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00…        r1 load str located at 4294985232
    stxdw [r10-0x60], r1                    
    mov64 r1, 2                                     r1 = 2 as i32 as i64 as u64
    stxdw [r10-0x58], r1                    
    stxdw [r10-0x48], r1                    
    mov64 r1, r10                                   r1 = r10
    add64 r1, -136                                  r1 += -136   ///  r1 = r1.wrapping_add(-136 as i32 as i64 as u64)
    stxdw [r10-0x50], r1                    
    mov64 r1, r10                                   r1 = r10
    add64 r1, -164                                  r1 += -164   ///  r1 = r1.wrapping_add(-164 as i32 as i64 as u64)
    stxdw [r10-0x78], r1                    
    lddw r1, 0x100004210 --> b"\xbf#\x00\x00\x00\x00\x00\x00a\x11\x00\x00\x00\x00\x00\x00\xb7\x02\x00\x0…        r1 load str located at 4294984208
    stxdw [r10-0x70], r1                    
    stxdw [r10-0x80], r1                    
    mov64 r1, r10                                   r1 = r10
    add64 r1, -168                                  r1 += -168   ///  r1 = r1.wrapping_add(-168 as i32 as i64 as u64)
    stxdw [r10-0x88], r1                    
    mov64 r1, r10                                   r1 = r10
    add64 r1, -160                                  r1 += -160   ///  r1 = r1.wrapping_add(-160 as i32 as i64 as u64)
    mov64 r2, r10                                   r2 = r10
    add64 r2, -96                                   r2 += -96   ///  r2 = r2.wrapping_add(-96 as i32 as i64 as u64)
    call function_858                       
    ldxdw r1, [r10-0xa0]                    
    ldxdw r2, [r10-0x90]                    
    syscall [invalid]                       
    ldxw r1, [r10-0xa8]                     
    ldxw r2, [r10-0xa4]                     
    add64 r2, r1                                    r2 += r1   ///  r2 = r2.wrapping_add(r1)
    lsh64 r2, 32                                    r2 <>= 32   ///  r2 = r2.wrapping_shr(32)
    jne r2, 1337, lbb_58                            if r2 != (1337 as i32 as i64 as u64) { pc += 6 }
    lddw r1, 0x1000043e0 --> b"You win!"            r1 load str located at 4294984672
    mov64 r2, 8                                     r2 = 8 as i32 as i64 as u64
    syscall [invalid]                       
    mov64 r1, 987654321                             r1 = 987654321 as i32 as i64 as u64
    ja lbb_63                                       if true { pc += 5 }
lbb_58:
    lddw r1, 0x1000043e8 --> b"You lose!"           r1 load str located at 4294984680
    mov64 r2, 9                                     r2 = 9 as i32 as i64 as u64
    syscall [invalid]                       
    mov64 r1, 123456789                             r1 = 123456789 as i32 as i64 as u64
    
...SNIP...

cfg

Fetch deployed bytecode from the mainnet

cargo run -- fetcher --program-id <PROGRAM_ID> --out-dir ./out/
  1. Executable Accounts (Programs):
    • The fetcher automatically detects whether the account is marked as executable.
    • If the program uses the Upgradeable Loader (common on Solana), it transparently resolves the indirection to locate the actual ProgramData.
    • It trims any unnecessary bytes before the ELF header and saves a clean .so binary ready for disassembly or reverse engineering.
  2. Non-Executable Accounts (AccountInfo):
    • If the account isn’t executable, fetcher simply dumps the raw byte content to a .bin file.
    • Fetch Anchor's struct discriminator

Reinsert functions in a reduced CFG

cargo run -- dotting -c functions.json -r reduced.dot -f full.dot

From:

img

To:

img2

Documentation

The project includes full mdBook documentation:

Serve the docs

cd book
mdbook serve

Build the docs

cd book
mdbook build

License

This project is licensed under the Server Side Public License v1

Contact

If you have any questions, suggestions, or need support:

We're happy to help and value community engagement!

About

Sol-azy is a modular CLI toolchain for static analysis and reverse engineering of Solana sBPF programs

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •