Entropia is a compiled language designed specifically for writing Beacon Object Files and position-independent shellcode on Windows x86-64. Not a framework on top of C, not a template generator, not yet another Python wrapper that spits out C. It’s its own (small) language, with its own compiler, and it gives you the artifact you need without intermediate steps or linker gymnastics.

I spent some time with it, and this is what I got out of it.

why this matters

The traditional BOF development pipeline looks something like this: write C, include the BOF SDK headers, manually declare every dynamic function resolution, cross-compile with MinGW, test with a harness, fix crash, repeat. Every step can break something, including your will to live, and it’s more the toolchain than the logic.

Entropia collapses all of that into one step. You write a .etpy file, run entc compile, and get a .x64.o ready to load. The compiler handles position independence, import resolution, relocations, and all the plumbing that normally lives in your head as tribal knowledge.

the language itself

The syntax is minimal and learnable in an afternoon. It feels like a stripped-down C with modern ergonomics. Here’s the shape of a BOF entry point:

1
2
3
4
5
6
use bof;

fn go(args: char*, len: int) -> void {
    // your code here
    ret;
}

That use bof; directive tells the compiler to emit a COFF object with the right relocations and Beacon API stubs. No headers, no macros, just this little piece of code. Need Windows structs? Instead of copy-pasting struct definitions from MSDN or fighting with SDK headers:

1
use_c "stdlib/win32/toolhelp.h";

This pulls struct layouts and constants directly from the Windows SDK metadata. PROCESSENTRY32, TH32CS_SNAPPROCESS, all of it, correct and ready to use.

lifecycle stages

Entropia has a concept of lifecycle stages. You can write modules that hook into the Init stage, running before your actual code:

1
2
3
use opsec_unhook;    // restore ntdll from disk
use opsec_etw;       // patch ETW
use opsec_amsi;      // disable AMSI

Three lines and your BOF runs in a cleaned environment. Each is a [Stage(Init)] hook that executes before go() is called. You don’t have to remember the ordering or wonder if you patched AMSI before loading the reflective DLL.

You can also write your own stages. Want to add random delays between API calls to break behavioral signatures? Write a stage that sleeps for a jittered interval before execution and use it everywhere.

a real example

I hammered together an ipconfig-equivalent BOF to test the whole pipeline. In C this would be a solid chunk of boilerplate between the DFR macros, the manual struct definitions for IP_ADAPTER_INFO and FIXED_INFO, and the two-pass buffer allocation dance.

In entropia:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
use bof;

// IP_ADAPTER_INFO x64 layout (from iphlpapi.h).
static AI_NEXT:        int = 0;
static AI_NAME:        int = 12;
static AI_DESC:        int = 272;
static AI_ADDR_LEN:    int = 404;
static AI_ADDR:        int = 408;
static AI_TYPE:        int = 420;
static AI_IPADDR:      int = 448;

// FIXED_INFO x64 layout (from iphlpapi.h).
static FI_HOSTNAME:    int = 0;
static FI_DOMAIN:      int = 132;
static FI_DNS_NEXT:    int = 272;
static FI_DNS_ADDR:    int = 280;

fn go(args: char*, len: int) -> void {
    var info_size: u32 = 0;
    Iphlpapi.GetAdaptersInfo((void*)0, &info_size);
    if info_size == 0 { ret; }

    var info: u64 = (u64)Kernel32.HeapAlloc(
        Kernel32.GetProcessHeap(), 8, (u64)info_size);
    if info == 0 { ret; }

    var result: u32 = (u32)Iphlpapi.GetAdaptersInfo((void*)info, &info_size);
    if result != 0 {
        BeaconPrintf(CALLBACK_ERROR, "could not get network adapter info\n");
        Kernel32.HeapFree(Kernel32.GetProcessHeap(), 0, (void*)info);
        ret;
    }

    var net_size: u32 = 0;
    Iphlpapi.GetNetworkParams((void*)0, &net_size);
    var net_info: u64 = 0;
    if net_size > 0 {
        net_info = (u64)Kernel32.HeapAlloc(
            Kernel32.GetProcessHeap(), 8, (u64)net_size);
        if net_info != 0 {
            if (u32)Iphlpapi.GetNetworkParams((void*)net_info, &net_size) != 0 {
                Kernel32.HeapFree(Kernel32.GetProcessHeap(), 0, (void*)net_info);
                net_info = 0;
            }
        }
    }

    var fmt: formatp;
    BeaconFormatAlloc(&fmt, 8192);

    var p: u64 = info;
    while p != 0 {
        BeaconFormatPrintf(&fmt, str.format("{s}", (char*)(p + AI_NAME)));
        BeaconFormatPrintf(&fmt, "\n");

        var atype: u32 = *(u32*)(p + AI_TYPE);
        if atype == (u32)6 {
            BeaconFormatPrintf(&fmt, "\tEthernet\n");
        }
        if atype != (u32)6 {
            BeaconFormatPrintf(&fmt, "\tUnknownType\n");
        }

        BeaconFormatPrintf(&fmt, "\t");
        BeaconFormatPrintf(&fmt, str.format("{s}", (char*)(p + AI_DESC)));
        BeaconFormatPrintf(&fmt, "\n");

        var addr_len: u32 = *(u32*)(p + AI_ADDR_LEN);
        var hex_tbl: char* = (char*)"0123456789ABCDEF";
        var mac: u8[20];
        mem.zero(&mac, 20);
        var mi: int = 0;
        var wi: int = 0;
        while mi < (int)addr_len {
            var bval: int = (int)*(u8*)(p + AI_ADDR + mi);
            var hi: int = bval / 16;
            var lo: int = bval - hi * 16;
            mac[wi] = (u8)*(hex_tbl + hi);
            mac[wi + 1] = (u8)*(hex_tbl + lo);
            wi = wi + 2;
            if mi < (int)addr_len - 1 {
                mac[wi] = (u8)45;
                wi = wi + 1;
            }
            mi = mi + 1;
        }
        mac[wi] = (u8)0;
        BeaconFormatPrintf(&fmt, "\t");
        BeaconFormatPrintf(&fmt, str.format("{s}", (char*)&mac));
        BeaconFormatPrintf(&fmt, "\n");

        BeaconFormatPrintf(&fmt, "\t");
        BeaconFormatPrintf(&fmt, str.format("{s}", (char*)(p + AI_IPADDR)));
        BeaconFormatPrintf(&fmt, "\n");

        p = *(u64*)p;
    }

    if net_info != 0 {
        BeaconFormatPrintf(&fmt, "Hostname: \t");
        BeaconFormatPrintf(&fmt, str.format("{s}", (char*)(net_info + FI_HOSTNAME)));
        BeaconFormatPrintf(&fmt, "\n");

        BeaconFormatPrintf(&fmt, "DNS Suffix: \t");
        BeaconFormatPrintf(&fmt, str.format("{s}", (char*)(net_info + FI_DOMAIN)));
        BeaconFormatPrintf(&fmt, "\n");

        BeaconFormatPrintf(&fmt, "DNS Server: \t");
        BeaconFormatPrintf(&fmt, str.format("{s}", (char*)(net_info + FI_DNS_ADDR)));
        BeaconFormatPrintf(&fmt, "\n");

        var dns_next: u64 = *(u64*)(net_info + FI_DNS_NEXT);
        while dns_next != 0 {
            BeaconFormatPrintf(&fmt, "\t\t");
            BeaconFormatPrintf(&fmt, str.format("{s}", (char*)(dns_next + 8)));
            BeaconFormatPrintf(&fmt, "\n");
            dns_next = *(u64*)dns_next;
        }

        Kernel32.HeapFree(Kernel32.GetProcessHeap(), 0, (void*)net_info);
    }

    var out_len: int = 0;
    var out: char* = BeaconFormatToString(&fmt, &out_len);
    BeaconOutput(CALLBACK_OUTPUT, out, out_len);
    BeaconFormatFree(&fmt);

    Kernel32.HeapFree(Kernel32.GetProcessHeap(), 0, (void*)info);
    ret;
}

As you can see there’s no more DFR, and struct definitions from MSDN are gone. Iphlpapi.GetAdaptersInfo resolves imports at compile time, the compiler handles PEB resolution at runtime.

Compile it clean, run strings on the output, and every error message is in cleartext, every import symbol spelled out as __imp_IPHLPAPI$GetAdaptersInfo, __imp_KERNEL32$HeapAlloc. Add --opsec=all --seed=random and the __imp_LIB$FUNC symbols disappear, resolved at runtime via PEB walk. The identifying strings ("could not get network adapter info", "Ethernet", "0123456789ABCDEF") get moved to the stack. The code goes through polymorphic rewriting and NOP sled insertion. Run it again with a different seed and you get a bytewise-different artifact from the same source.

You have full control over your output, built one piece at a time, reusable (call the same opsec passes across all your BOFs), and easy to plug into any CI/CD or deployment pipeline.

how i got here

The whole adventure started, as most things do in 2026, by dumping the entire documentation locally and letting a model guide me through the conversion of my existing BOFs to .etpy. I steered, it (he/she?) did, and in the process I learned. A weird loop, but it works.

By the end I came out with a devcontainer purpose-built for the entropia toolchain, a skill that assists in BOF development and conversion, and all the pieces I need to keep writing in this language. Setting up a build & deploy pipeline on top of it couldn’t have been simpler.

The project is pre-1.0 and labeled experimental, but the compiler produces working artifacts and the language surface is stable. I’ll be keeping an eye on it. Kudos to the creators.

resources