Stressing LLMs - Triage Stage

Packers, cryptors, and code obfuscation are all methods used to bypass signature-based scanners in AV/EDR or to slow down the reverse engineering process. Many people are now using Large Language Models (LLMs) to reverse engineer or thwart these protections. It is increasingly common to see examples of frontier models solving CTF challenges or being used to port old video games to modern code. It is somewhat morbidly fascinating to consider how LLMs could drive an arms race with DRM systems.

When thinking about LLMs for reverse engineering, I keep asking: at what point does randomization degrade tokenization or code comprehension? This is a reasonable question in the context of compiled executables. 

In my view, there are two types of potential attacks against LLMs in the context of static analysis of compiled binaries. The first is making the code so complex that the context size and token cost are no longer practical. The second is what I am calling "Tokenization Inflation" is an attempt to inflate or fragment tokens to increase processing cost or reduce coherence. These “attacks” may not even be effective against LLMs, especially for trivial tasks, but they are still worth exploring. This blog post is one of two. This is the first of two posts: this one outlines the approaches and code; the second tests the hypothesis.

Complexity Attack 

The complexity attack focuses on increasing computational complexity by generating binaries with a large number of interdependent functions. The goal is to force the analysis to scale beyond what is practical for static reasoning. It consists of an executable that contains a toy XOR cipher with a keystream derived from a set of N functions. Within N being the number of functions. The executable is generated using a Python script that produces C source code with an embedded encrypted string and decryption loop. GCC is then used to compile the C source into an executable. At runtime, the decrypted string is printed to the console. To make this concrete, we can walk through generating the code and compiling it.

python gen_fixture.py generate --seed 0xdeadbeef --rounds 5--symbol-len 16 --symbol-pad 0 --message "Hello, World" --out fixture.c
Wrote fixture.c
Generated symbol prefix length: 16
Compile with:
gcc -O0 -g3 -gdwarf-5 -fno-omit-frame-pointer -fno-inline -std=c11 fixture.c -o fixture.exe 

Here is fixture.c. Note that there are 5 functions named TokenizerBench, which matches the number of rounds specified in the command line. If we were to increase this to 16,397 rounds, there would be 16,397 functions generated.

// Generated CTF-style static-analysis fixture
//
// Suggested build:
//   gcc -O0 -g3 -gdwarf-5 -fno-omit-frame-pointer -fno-inline -std=c11 fixture.c -o fixture.exe
//
// seed=0xdeadbeef
// rounds=5
// const_mode=rand
// const_seed=0xc001d00d
// symbol_len=16
// symbol_pad=0
// generated_prefix_length=16
//
// Notes:
// - Per-function constants are baked into each generated function.
// - Function bodies vary by generated round variant.
// - The plaintext is stored encrypted in the binary and decrypted at runtime.

#include <stdint.h>
#include <stdio.h>
#include <stddef.h>

typedef struct TokenizerBench___Type__LongRecord__With__Lots__Of__Nested__Like__Tokens {
    uint64_t a;
    uint64_t b;
    uint64_t c;
} TokenizerBench___Type__LongRecord__With__Lots__Of__Nested__Like__Tokens;

static uint32_t xorshift32(uint32_t x) {
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    return x;
}


__attribute__((used, noinline))
uint32_t TokenizerBench___R0(TokenizerBench___Type__LongRecord__With__Lots__Of__Nested__Like__Tokens *p) {
    uint32_t m1 = xorshift32(0x9336956du ^ 0x31bbf978u ^ (uint32_t)p->a);
    uint32_t m2 = xorshift32(0xcd6f55fcu ^ (uint32_t)p->b);
    p->a ^= ((uint64_t)m1 << 32) | (uint64_t)m2;
    p->b += (uint64_t)(0x9336956du ^ m2);
    p->b = (p->b << 10) | (p->b >> 54);
    p->c = (p->c + p->a) ^ (uint64_t)(0x31bbf978u ^ 0xcd6f55fcu);
    uint64_t r = p->a ^ p->b ^ p->c ^ (uint64_t)0x9336956du ^ (uint64_t)0x31bbf978u ^ (uint64_t)0xcd6f55fcu;
    return (uint32_t)(r ^ (r >> 32));
}


__attribute__((used, noinline))
uint32_t TokenizerBench___R1(TokenizerBench___Type__LongRecord__With__Lots__Of__Nested__Like__Tokens *p) {
    uint32_t m = xorshift32(0x366856bbu ^ (uint32_t)p->a);
    p->a ^= ((uint64_t)0x366856bbu << 32) | (uint64_t)m;
    p->b += p->a ^ (p->c + (uint64_t)0x72fcd409u);
    p->c = ((p->c ^ (uint64_t)0x3afd4cabu) << 24) | ((p->c ^ (uint64_t)0x3afd4cabu) >> 40);
    uint64_t r = p->a ^ p->b ^ p->c ^ (uint64_t)0x366856bbu ^ (uint64_t)0x72fcd409u ^ (uint64_t)0x3afd4cabu;
    return (uint32_t)(r ^ (r >> 32));
}


__attribute__((used, noinline))
uint32_t TokenizerBench___R2(TokenizerBench___Type__LongRecord__With__Lots__Of__Nested__Like__Tokens *p) {
    uint32_t m = xorshift32(0x046d6ad2u ^ (uint32_t)p->b);
    p->b ^= ((uint64_t)m << 32) | (uint64_t)0xc719f452u;
    p->c += p->b ^ (uint64_t)0x0fc1bdd9u;
    p->a = (p->a + (uint64_t)0x046d6ad2u);
    p->a = (p->a >> 21) | (p->a << 43);
    uint64_t r = p->a ^ p->b ^ p->c ^ (uint64_t)0xc719f452u ^ (uint64_t)0x046d6ad2u ^ (uint64_t)0x0fc1bdd9u;
    return (uint32_t)(r ^ (r >> 32));
}


__attribute__((used, noinline))
uint32_t TokenizerBench___R3(TokenizerBench___Type__LongRecord__With__Lots__Of__Nested__Like__Tokens *p) {
    uint32_t m = xorshift32(0xc55b15eeu + (uint32_t)p->c);
    p->a += ((uint64_t)m << 32) | (uint64_t)0x0d11e683u;
    p->c ^= p->a;
    p->c = (p->c >> 16) | (p->c << 48);
    p->b ^= (uint64_t)(0xc8e57b40u + m);
    uint64_t r = p->a ^ p->b ^ p->c ^ (uint64_t)0xc8e57b40u ^ (uint64_t)0x0d11e683u ^ (uint64_t)0xc55b15eeu;
    return (uint32_t)(r ^ (r >> 32));
}


__attribute__((used, noinline))
uint32_t TokenizerBench___R4(TokenizerBench___Type__LongRecord__With__Lots__Of__Nested__Like__Tokens *p) {
    uint32_t m1 = xorshift32(0xdaf09eaeu ^ 0xf6f1f787u ^ (uint32_t)p->a);
    uint32_t m2 = xorshift32(0xe0cf500du ^ (uint32_t)p->b);
    p->a ^= ((uint64_t)m1 << 32) | (uint64_t)m2;
    p->b += (uint64_t)(0xdaf09eaeu ^ m2);
    p->b = (p->b << 21) | (p->b >> 43);
    p->c = (p->c + p->a) ^ (uint64_t)(0xf6f1f787u ^ 0xe0cf500du);
    uint64_t r = p->a ^ p->b ^ p->c ^ (uint64_t)0xdaf09eaeu ^ (uint64_t)0xf6f1f787u ^ (uint64_t)0xe0cf500du;
    return (uint32_t)(r ^ (r >> 32));
}


__attribute__((used, noinline))
uint32_t derive_state(uint32_t seed) {
    TokenizerBench___Type__LongRecord__With__Lots__Of__Nested__Like__Tokens x = {
        seed,
        seed ^ 0x12345678ULL,
        seed + 0x9ULL
    };

    uint32_t s = seed;
    s ^= TokenizerBench___R0(&x);
    s ^= TokenizerBench___R1(&x);
    s ^= TokenizerBench___R2(&x);
    s ^= TokenizerBench___R3(&x);
    s ^= TokenizerBench___R4(&x);

    s = xorshift32(s);
    return s;
}

int main(void) {
    uint8_t encrypted[] = { 0xcf, 0x7a, 0xe5, 0x10, 0x3c, 0x49, 0xe6, 0x0b, 0x79, 0xcb, 0xf9, 0x3d, 0x00 };
    uint32_t s = derive_state(0xdeadbeef);

    for (size_t i = 0; i < sizeof(encrypted) - 1; i++) {
        s = xorshift32(s + 0xA5A5A5A5u);
        encrypted[i] ^= (uint8_t)(s & 0xffu);
    }

    puts((const char *)encrypted);
    return 0;
}
 
Below is the creation and execution of a 100,000-round binary. 
 
> python gen_fixture.py generate --seed 0xdeadbeef --rounds 100000 --message "Hello, World" --out fixture-100k.c --symbol-len 16 --symbol-pad 0

Wrote fixture-100k.c
Generated symbol prefix length: 16
Compile with:
gcc -O0 -g3 -gdwarf-5 -fno-omit-frame-pointer -fno-inline -std=c11 fixture-100k.c -o fixture-100k.exe
> gcc -O0 -g3 -gdwarf-5 -fno-omit-frame-pointer -fno-inline -std=c11 fixture-100k.c -o fixture-100k.exe
.\fixture-100k.exe
Hello, World 
The 100k function binary was over 55 MB. It is worth noting that dynamic analysis could bypass this obfuscation with a single breakpoint, but the focus here is on static analysis. The interesting part of this approach is that the number of functions scales easily for testing, and each function contributes to the final state. If the analysis is incomplete or incorrect, the derived decryption key will also be incorrect. 

Tokenization Inflation

Once a prompt is sent to an LLM, it is tokenized into integers. A simple way to think about this is mapping chunks of text to IDs. These IDs are then used to index into the model’s embedding table. This may seem similar to compression algorithms, since both map variable-length sequences to codes. The difference is that tokenization uses a fixed vocabulary optimized for model performance, while compression builds or applies dictionaries to reduce size by exploiting repetition in the data. 

 

A potential weakness in both compression and tokenization is that long inputs increase computational cost.  Repetitive or structured data can also affect how efficiently it is represented as tokens. Most modern implementations handle this reasonably well, but there is still a cost. In an executable, one of the most common ways to introduce large amounts of data is through strings. However, not all strings are surfaced or prioritized during analysis. One type of string that is often preserved and exposed is debug information. With GCC, DWARF debug metadata can be used to store extremely long function names. 

We can generate function names of arbitrary length using the Python script. By passing the flags -g3 -gdwarf-5, GCC emits DWARF metadata. Disassemblers (e.g Binary Ninja, Ghidra, IDA) can read this metadata, recover the names, and in some workflows pass them along to an LLM, which then tokenizes the text. The following command generates 5 rounds with a function name length of 7,331 characters.

> python gen_fixture.py generate --seed 0xdeadbeef --rounds 5 --message "Hello, World" --out fixture-p.c --symbol-len 7331 --symbol-pad 1337
Wrote fixture-p.c
Generated symbol prefix length: 8672
Compile with:
gcc -O0 -g3 -gdwarf-5 -fno-omit-frame-pointer -fno-inline -std=c11 fixture-p.c -o fixture-p.exe
> gcc -O0 -g3 -gdwarf-5 -fno-omit-frame-pointer -fno-inline -std=c11 fixture-p.c -o fixture-p.exe

Below is a screenshot of a graph view in IDA. It gives a sense of how long the function names are, although they are truncated after 1024 characters in IDA.

 
Here is an example of a complete function name (keep scrolling). 

uint32_t __cdecl TokenizerBench__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char___0(TokenizerBench__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__DemangleLike__std__basic_string__char__std__char_traits__char__std__allocator__char__vector__pair__basic_string__int__PadXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_Type__LongRecord__With__Lots__Of__Nested__Like__Tokens_0 *p)

Combining code complexity with extremely long function names results in a large corpus of highly similar strings within a single function. This can stress tokenization efficiency and increase the cost of processing. This specific approach can be mitigated by excluding DWARF debug metadata during analysis, but it highlights how auxiliary data can impact LLM-based workflows.
 
Summary 
The goal is not to make binaries impossible to reverse, but to push LLM-based analysis into inefficient paths. One approach scales interdependent functions to force complexity. The other inflates token-heavy inputs through debug metadata to stress context limits and attention costs. These are better understood as attempts to trigger worst-case behavior in the analysis pipeline, not attacks on tokenization itself. This highlights a shift in where the pressure points are within LLMs. Context windows, token budgets, and attention scaling become part of the attack surface. If LLMs are used in reverse engineering workflows, understanding where they degrade may matter as much as improving their capability.
 
The next step is validating whether these ideas actually hold up in practice. That means testing them in a way in which I don't go broke with token cost and/or get banned by Anthropic or OpenAI. Odds are my first attempts will be locally using resources referenced in this gist.
 
Feel free to send me an email if you have any ideas at alexander dot hanel at gmail dot com.   
 
Here is the source code 
 
 

Introduction to Malware Binary Triage (IMBT) Course

Looking to level up your skills? Get 10% off using coupon code: MWNEWS10 for any flavor.

Enroll Now and Save 10%: Coupon Code MWNEWS10

Note: Affiliate link – your enrollment helps support this platform at no extra cost to you.

Article Link: Hooked on Mnemonics Worked for Me: Stressing LLMs - Triage Stage

1 post - 1 participant

Read full topic



Malware Analysis, News and Indicators - Latest topics
Next Post Previous Post