Instruction By Instruction

Hello, we meet again. Snagged a convo between c0z and pico the other day on real time encryption or Runtime Code Encryption, sometimes called IBI (Instruction-By-Instruction) though that name crawled out of the academic side (bunch of losers) Wanted to drop a reply in the back thread but it turned into a long ass wall of text, so I figured fuck it let’s write it up proper maybe someone learns something.

So what we implement here is a “page-granular demand decryption” code pages live encrypted on disk and in memory, and are decrypted exactly one page at a time only when the CPU is about to execute them. After execution leaves the page, it is re-encrypted. At any given moment, at most one code page exists in plaintext in the process address space. This is the invariant the design must maintain, and everything else follows from it.

Back in the day, cats were already poking at this. Earliest published work on runtime ELF encryption with anti-debug? Shiva scut and grugq, circa 2001. Shiva wraps an ELF binary, encrypts the payload, blasts it out decrypted at load time into an anonymous mapping, then exec’s it basically what you’d just call a loader today. The whole plaintext image is sitting naked in memory the second the constructor returns. Snapshot the process any time after startup and you’ve got the full code. Hard problem back then.

Also there was burneye (team teso) same fundamental idea but tightened up passphrase-derived keys, ptrace detection, TPM support in later versions. Dressed it up nice, but the bone structure was the same load-time bulk decryption. Pop it at the right moment and it’s all there.

Anyway not dropping this to flex scene history. Point is to actually understand the technique, and to do that right you gotta see what problem it’s trying to solve on Linux in the first place:

(a) the demand-decryption loop driven by SIGSEGV,
(b) the re-encryption loop driven by the x86 Trap Flag,
(c) with a stream cipher that supports random-access seeks,
(d) with the key isolated on a separately protected page,
(e) and all of it fitting in under few lines of code.

That is what this piece does.

— A

Consider a function ec_key that you want to protect. Without any protection, the linker places it in the .text section at some address, and it sits there as plaintext for the entire lifetime of the process. Anyone who can attach a debugger, take a core dump, or read /proc/$pid/mem gets it for free.

The idea is to place ec_key (and any other functions you want protected) in a separate ELF section we call it .foo that is:

1. Encrypted at rest in the binary on disk.
2. Mapped PROT_NONE at startup, so any access faults immediately.
3. Decrypted one page at a time, on demand, only when the CPU is
   about to execute that page.
4. Re-encrypted as soon as the CPU's instruction pointer leaves that
   page.

The first observation is that step (3) and (4) give us the invariant at most one page is ever plaintext. A process snapshot captures at most one page of code, which may not even contain the function you care about.

second observation is that steps (3) and (4) are driven entirely by the hardware’s memory protection mechanism and signal delivery. We do not patch the code, insert trampolines, or modify the calling the protected functions are called exactly like normal functions the first memory access just happens to fault into our handler.

Third observation is that the encryption has to be keyed at runtime, not at build time. A build-time key is extractable from the binary by static analysis. A key generated at startup is not in the binary at all.

Demand-Decryption Engine ?

Let me explain! at startup, the constructor does:

    mprotect(foo_base, foo_size, PROT_READ | PROT_WRITE);
    for each page in .foo:
        chacha20_xor(page);          /* encrypt in place */
    mprotect(foo_base, foo_size, PROT_NONE);

After this, .foo is opaque. Calling any function in .foo immediately generates a SIGSEGV the CPU tries to fetch the instruction at the function’s entry point, the MMU sees PROT_NONE, and delivers SIGSEGV with si_addr pointing to the faulting address.

Our SIGSEGV handler does:

1. Extract fault_addr from siginfo->si_addr.
2. Page-align it: fault_page = fault_addr & ~0xFFF.
3. Verify fault_page is within .foo. If not, re-raise SIGSEGV
   with the default handler (real crash).
4. If a different page is currently decrypted (g_hot != 0 and
   g_hot != fault_page), re-encrypt it first.
5. mprotect(fault_page, PROT_READ | PROT_WRITE).
6. Decrypt the page: chacha20_xor(fault_page).
7. mprotect(fault_page, PROT_READ | PROT_EXEC).
8. Set g_hot = fault_page.
9. Set the x86 Trap Flag in the saved ucontext (EFLAGS bit 8).
10. Return from the signal handler.

Returning from the signal handler resumes execution at the faulting instruction which now has PROT_READ|PROT_EXEC and is decrypted. The function runs normally.

Step (9) is the setup for re-encryption, w’ll talk more in the next section.

One the signal handler itself must not fault. We allocate a separate signal stack (sigaltstack) on a locked anonymous page. If the signal handler used the normal thread stack and the thread stack itself faulted, we would get a recursive fault and deadlock. The altstack
ensures the handler always has a valid stack regardless of the state of the protected region.

Another the altstack page is mlock()'d. Without mlock, the kernel might page it out, and the page-in on first signal delivery could theoretically cause a nested fault. This is paranoid but aight.

    static void _segv(int sig, siginfo_t *info, void *uctx)
    {
        uintptr_t fault = PGN((uintptr_t)info->si_addr);
        uintptr_t base  = PGN((uintptr_t)foo_start);
        uintptr_t top   = PGN((uintptr_t)(foo_end - 1)) + PGS;

        if (fault < base || fault >= top) {
            struct sigaction sa = { .sa_handler = SIG_DFL };
            sigaction(SIGSEGV, &sa, NULL);
            raise(SIGSEGV);
            return;
        }
        if (g_hot && g_hot != fault) {
            mprotect((void *)g_hot, PGS, PROT_READ | PROT_WRITE);
            _crypt_pg(g_hot);
            mprotect((void *)g_hot, PGS, PROT_NONE);
        }
        mprotect((void *)fault, PGS, PROT_READ | PROT_WRITE);
        _crypt_pg(fault);
        mprotect((void *)fault, PGS, PROT_READ | PROT_EXEC);
        g_hot = fault;

        ucontext_t *uc = uctx;
        uc->uc_mcontext.gregs[REG_EFL] |= 0x100;  /* set TF */
    }

– B

The cipher choice matters more than it might appear. We need:

(a) A cipher that can be applied to arbitrary page offsets without
    decrypting everything before them. If we used a CBC-mode block
    cipher, decrypting page 5 would require decrypting pages 0-4
    first. That kills the "one page at a time" property.

(b) A cipher that is an involution on XOR i.e., applying it twice
    gives back the original. This is why we can use the same
    _crypt_pg function for both encryption and decryption.

(c) Speed. mprotect is not free. We are calling it three times per
    page fault. We do not want the cipher to be the bottleneck.

ChaCha20 satisfies all three. It is a stream cipher based on the Salsa20 core. The keystream is generated in 64-byte blocks, each block independently indexed by a 32-bit counter. “Random access” into the stream is therefore O(1): to decrypt bytes starting at offset N, set the block counter to N/64 and skip N%64 bytes into that block.

We expose this as :

    void cc20_seek(cc20_ctx *c, uint64_t byte_off) {
        uint32_t blk = (uint32_t)(byte_off / 64);
        int rem      = (int)(byte_off % 64);
        c->s[12] = blk;
        if (rem) {
            uint32_t tmp[16];
            _block(tmp, c->s);
            memcpy(c->buf, tmp, 64);
            c->pos = rem;
            c->s[12]++;
        } else {
            c->pos = 64;
        }
    }

To decrypt page at address P where the section starts at base B, the byte offset is simply (P - B). Pages are 4096 bytes; 4096 is a multiple of 64 (the ChaCha20 block size), so each page starts at a clean block boundary. This means cc20_seek for any page is always called with rem == 0, and we just set the counter to (P-B)/64.

    cc20_init(&c, gk->key, gk->nonce, 0);
    cc20_seek(&c, pg - PGN((uintptr_t)foo_start));
    cc20_xor(&c, (uint8_t *)pg, (const uint8_t *)pg, PGS);

Note that cc20_xor reads from and writes to the same buffer (in-place). This works because XOR is its own inverse: E(E(P)) = P. Calling first one on an encrypted page decrypts it. Calling it on a decrypted page re-encrypts it. Same function, same key, same code path.

and after each use of the ChaCha20 context, we shall zero it ser this is not ceremonial. The context struct holds the expanded key schedule and the current position in the keystream. Leaving it on the stack would leak key material to anyone who can read process memory after the handler returns.

– C

Circeling back to The demand-decryption engine (A) decrypts a page when the CPU first accesses it. But when does re-encryption happen?

The naive answer “set the page back to PROT_NONE after the function returns” does not work. The handler has no way to know when the function returns. It could call other functions, take branches, loop. We cannot intercept the return without instrumenting the code.

hence x86 Trap Flag.

The TF is bit 8 of EFLAGS when TF=1, the CPU delivers a SIGTRAP after executing each instruction. This is the hardware mechanism behind single-step debugging.

We set TF=1 before returning from the SIGSEGV handler step 9 in The CPU then single-steps through the protected code, delivering a SIGTRAP after each instruction. Our SIGTRAP handler (_trap) checks the current instruction pointer:


    static void _trap(int sig, siginfo_t *info, void *uctx)
    {
        ucontext_t *uc  = uctx;
        uintptr_t   rip = (uintptr_t)uc->uc_mcontext.gregs[REG_RIP];

        if (!g_hot) {
            /* sus clear TF and pass to default */
            uc->uc_mcontext.gregs[REG_EFL] &= ~(greg_t)0x100;
            struct sigaction sa = { .sa_handler = SIG_DFL };
            sigaction(SIGTRAP, &sa, NULL);
            raise(SIGTRAP);
            return;
        }
        if (PGN(rip) == g_hot) {
            /* still on the hot page keep single-stepping */
            return;
        }
        /* CPU has left the hot page re-encrypt and clear TF */
        uc->uc_mcontext.gregs[REG_EFL] &= ~(greg_t)0x100;
        mprotect((void *)g_hot, PGS, PROT_READ | PROT_WRITE);
        _crypt_pg(g_hot);
        mprotect((void *)g_hot, PGS, PROT_NONE);
        g_hot = 0;
    }

TF stand for THE FUCK nah … The logic is if RIP is still inside the hot page, do nothing (keep
TF set, return). If RIP has moved to a different address whether that is a CALL target, a RET destination, a JMP, anything re-encrypt the old page and clear TF.

This handles every control flow transfer automatically, without any code instrumentation. something like:

    [ibi] SIGSEGV: decrypt page 0x555555559000   <- main calls check_key
    [ibi] SIGTRAP: leaving page, re-encrypt ...  <- check_key calls check_sum
    [ibi] SIGSEGV: decrypt page 0x555555557000   <- check_sum page fault
    [ibi] SIGTRAP: leaving page, re-encrypt ...  <- check_sum returns
    [ibi] SIGSEGV: decrypt page 0x555555559000   <- check_key resumes (was dark)
    [ibi] SIGTRAP: leaving page, re-encrypt ...  <- check_key calls check_product
    [ibi] SIGSEGV: decrypt page 0x555555558000   <- check_product page fault
    [ibi] SIGTRAP: leaving page, re-encrypt ...  <- check_product returns
    [ibi] SIGSEGV: decrypt page 0x555555559000   <- check_key final execution
    [ibi] SIGTRAP: leaving page, re-encrypt ...  <- check_key returns to main    

Three functions, three pages, 10 signal deliveries total. Every cross page control transfer produces exactly one SIGTRAP (re-encrypt old) and one SIGSEGV (decrypt new). The invariant holds throughout.

on performance a single-stepping is slow. Every instruction in the protected code generates a SIGTRAP, which involves saving and restoring signal context. For a tight loop of 1000 iterations, this is 1000 SIGTRAPs per page per loop. This technique is cool for code that runs infrequently license checks, key verification, authentication gates that’s it.

The ChaCha20 key and nonce live on a dedicated anonymous page that is PROT_NONE except during the window when it’s actively reading it. The lifecycle:

  - Allocated with mmap(MAP_PRIVATE|MAP_ANONYMOUS) and filled with
    getrandom() entropy.
  - Immediately mlock()'d to prevent it from being paged out to swap,
    where it would persist past process exit.
  - madvise(MADV_DONTDUMP) to exclude it from core dumps. This is
    belt-and-suspenders since we also disable core dumps with
    setrlimit(RLIMIT_CORE, {0,0}), but MADV_DONTDUMP survives a
    setrlimit reversal by an attacker who patches the init code.
  - madvise(MADV_WIPEONFORK) so that if the process forks, the child
    gets a zeroed key page rather than a copy of the key. A child that
    somehow triggered a .foo fault would call _die() rather than
    decrypt with a key it had inherited.
  - mprotect(PROT_NONE) immediately after the key is written.

the key page is opened for exactly the duration of the decrypt operation:

    mprotect(gk, PGS, PROT_READ);     /* open */
    cc20_init(&c, gk->key, ...);
    cc20_xor(...);
    explicit_bzero(&c, sizeof c);
    mprotect(gk, PGS, PROT_NONE);     /* close */

There is a brief window where gk is PROT_READ. An attacker who can interrupt the process precisely during this window (via a second thread that calls mprotect or reads /proc/$pid/mem) can read the key. This window is on the order of a few hundred nanoseconds. Closing it completely would require hardware memory encryption (AMD SME/SEV or Intel TME), which is outside the scope of a userspace implementation In other words skill issue.

and On program exit we call

    mprotect(gk, PGS, PROT_READ | PROT_WRITE);
    explicit_bzero(gk, PGS);
    munmap(gk, PGS);

explicit_bzero (or a volatile-loop equivalent on platforms without it) is used rather than memset() to prevent the compiler from optimizing out the zero a real thing since the key page is never read after the wipe in the C abstract machine.

– D

The constructor performs two environment checks before touching the key or the protected section: _traced() and _ld_hooked(). Both are implemented using raw syscalls rather than libc wrappers.

This matters because LD_PRELOAD hooking the most common dynamic analysis technique intercepts libc calls. If we called fopen() to read /proc/self/status, you could LD_PRELOAD a fake fopen that always returns “TracerPid:\t0”. Our raw syscall reads bypass the dynamic linker’s PLT resolution thingy

    long fd = syscall(SYS_openat, AT_FDCWD, "/proc/self/status",
                      O_RDONLY);
    long n  = syscall(SYS_read, fd, buf, sizeof buf - 1);
    syscall(SYS_close, fd);
    

We scan the buffer for “TracerPid:” and check whether the value is non-zero. This catches gdb, strace, and any ptrace-based tool.

For the environment check, we read /proc/self/environ (again via raw syscalls) and scan for LD_PRELOAD= and LD_AUDIT=. These variable names survive even if the we removes them from the environ array seen by getenv(), because /proc/self/environ reflects the original
execve() argument, not the current environment.

We also call prctl(PR_SET_DUMPABLE, 0) at the start of the constructor. This prevents /proc/$pid/mem from being readable by other processes (without CAP_SYS_PTRACE) and prevents core dumps from being written by the kernel even if RLIMIT_CORE is later raised.

setrlimit(RLIMIT_CORE, {0,0}) is belt-and-suspenders for core dumps very simple and clean but be my guest play with it how you like.

Also It is worth saying that

At all times, at most one page of .foo is in plaintext state
(PROT_READ|PROT_EXEC). All other pages remain PROT_NONE and
contain only ChaCha20 ciphertext. The ChaCha20 key exists only
on a PROT_NONE page, exposed for less than 1 microsecond during
any decrypt or re-encrypt operation.

This invariant is maintained by the cooperation of SIGSEGV (which decrypts pages on demand) and SIGTRAP (which re-encrypts them when the CPU leaves). The SIGSEGV handler also re-encrypts the current hot page before decrypting a new one, providing a fallback for cases where a page transition does not go through the TF path (theoretically possible with async signals).

– E

This ain’t no panacea, here’s some problems for you to chew on. Easy ones, but solid learning material.

.foo shows up clean in the ELF section header table. And even stripped, the PT_LOAD program header for those pages has a telltale alignment, 0x1000, that doesn’t match normal .text padding. Fix? objcopy --strip-section-headers and pick an alignment that blends into the surrounding segments like it was always there.

The key lives in memory and someone will find it. Sure, it’s sitting behind a PROT_NONE page with a tight access window, but it exists. Any process holding CAP_SYS_PTRACE can read it. One arbitrary-read vuln in your own process and it’s gone. Hardware memory encryption, AMD SME/SEV, is the only real answer for protecting in-memory key material from a sufficiently privileged attacker. Userspace can’t save you there.

The Trap Flag trick is old news. Setting it is a known anti-debug technique and aware debuggers, LLDB with the right scripts for one, handle it without breaking a sweat. Don’t lean on it like it’s a secret.

And some to find on your own … and solve.

Source code

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
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
- http://phrack.org/issues/61/8.html
- https://man7.org/linux/man-pages/
6 Likes

I’ve only skimmed through the source quickly but I was wondering if you accounted for edge cases where you have code that goes beyond two pages and have an instruction that is split between a page boundary. Since you are only exposing single pages at a time, surely that would break something. How do you think you would deal with that case?

1 Like

Amazing like always :heart: Agree that academia is a joke (also get those MacOS artifacts out of here :stuck_out_tongue:).

This is way better than doing what I was doing lol and it’s more in-line with VEH single stepping on Windows. I’ll look into the hardware encryption part. I wonder how much a pain in the ass this might be if paired with nanomites.

1 Like

nah that’s a gap for now as far as runnin’ it, this piece here doesn’t hit it cus all three functions are tiny, we talkin’ ~20 bytes each, and alignment puts 'em at page starts, so there’s several thousand bytes of nop padding before the next boundary. no instruction gets anywhere near the edge.

but the moment you have a FOO function big enough that its code body reaches the last 14 bytes of a page, you’re cooked either way.

how would I deal with it? mhmm, I could think of ways, but the clean one is an offline preprocessin’ step that disassembles each .foo page before encryption and inserts nop padding so no instruction starts within 14 bytes of a boundary so basically a simple UPX so for now it’s aight, but later, offline padding is the way.

1 Like

appreciate it 'em macOS extended attributes are a bitch Hahahaha and yeah it would be pretty nasty they both fight over SIGTRAP, but if done right, nanomites kill CFG recovery, ibi kills disassembly together they’re a proper nightmare to reverse

Nice post @0xf00s love this kind of topics. I have some question/comments tho.

You’d be surprised to know who you’re calling losers :slight_smile:

I don’t fully catch this. As per your sample code, the .foo functions are in plain text in the binary. They are only crypted at start-up. I can just objdump them and see the code. No need to run the program or dump it. This way I do not even have to do static analysis to get the build key. Am I missing something?

When running a CBC-mode cipher to decrypt block 5 you need the IV for block 5 that is actually the ciphered block 4 (that you have) so, for decoding you could actually use any cipher on CBC-mode. No need to decode all the previous pages. Note that this only works for decrypting. If you need to change some data and re-crypt, you would have to re-crypt all the remaining blocks.

Anyway, ChaCha20 is a great choice in this case but your sentence is a bit misleading

If that is your use case, the whole process looks a bit overkill. That kind of functions (key verification, license checks) are usually small and self-contained or if not, you can keep all the require code in the same page. After all, the execution of that code is pretty quick. For that use case, there are much simpler solutions and a small instrumentation may worth the save in space and performance.

However I’m curious about other use cases for which your approach may work better.

You can also use memset_s that is part of C11 and does a few more checks.

You can just strip the binary to get rid of that.

It was a nice read, thanks for taking the time to put all this together. It’s great from an educational/academic point of view :wink: .

1 Like

team teso

hmmm sounds familiar to me … but I forget, why do this sounds familiar

1 Like

Have you checked this? Maybe it’s true for Mac OS but it might not be for Linux. getenv takes from __environ

char *
getenv (const char *name)
{
  if (__environ == NULL || name[0] == '\0')
    return NULL;
  size_t len = strlen (name);
  for (char **ep = __environ; *ep != NULL; ++ep)
    {
      if (name[0] == (*ep)[0]
	  && strncmp (name, *ep, len) == 0 && (*ep)[len] == '=')
	return *ep + len + 1;
    }
  return NULL;
}

which takes from argv which comes from _start

_start:
xor    %ebp,%ebp				
mov    %rdx,%r9
pop    %rsi
mov    %rsp,%rdx
and    $0xfffffffffffffff0,%rsp
push   %rax
push   %rsp
lea    0x18a(%rip),%r8 # 6d0 <__libc_csu_fini>
lea    0x113(%rip),%rcx # 660 <__libc_csu_init>
lea    0xe6(%rip),%rdi # 63a <main>
callq  *0x200a86(%rip) # 200fe0<__libc_start_main@GLIBC_2.2.5>
hlt
nopl   0x0(%rax,%rax,1)

which comes the kernel’s create_elf_tables function that sets up the stack which will contain argc and argv

static int
create_elf_tables(struct linux_binprm *bprm, const struct elfhdr *exec,
		unsigned long interp_load_addr,
		unsigned long e_entry, unsigned long phdr_addr)
{
	struct mm_struct *mm = current->mm;
	unsigned long p = bprm->p;
	int argc = bprm->argc;
	int envc = bprm->envc;
	elf_addr_t __user *sp;

	// ...

	items = (argc + 1) + (envc + 1) + 1;
	bprm->p = STACK_ROUND(sp, items);

	/* Point sp at the lowest address on the stack */
#ifdef CONFIG_STACK_GROWSUP
	sp = (elf_addr_t __user *)bprm->p - items - ei_index;
	bprm->exec = (unsigned long)sp; /* XXX: PARISC HACK */
#else
	sp = (elf_addr_t __user *)bprm->p;
#endif


	/*
	 * Grow the stack manually; some architectures have a limit on how
	 * far ahead a user-space access may be in order to grow the stack.
	 */
	if (mmap_write_lock_killable(mm))
		return -EINTR;
	vma = find_extend_vma_locked(mm, bprm->p);
	mmap_write_unlock(mm);
	if (!vma)
		return -EFAULT;

	/* Now, let's put argc (and argv, envp if appropriate) on the stack */
	if (put_user(argc, sp++))
		return -EFAULT;

	/* Populate list of argv pointers back to argv strings. */
	p = mm->arg_end = mm->arg_start;
	while (argc-- > 0) {
		size_t len;
		if (put_user((elf_addr_t)p, sp++))
			return -EFAULT;
		len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
		if (!len || len > MAX_ARG_STRLEN)
			return -EINVAL;
		p += len;
	}
	if (put_user(0, sp++))
		return -EFAULT;
	mm->arg_end = p;

	/* Populate list of envp pointers back to envp strings. */
	mm->env_end = mm->env_start = p;
	while (envc-- > 0) {
		size_t len;
		if (put_user((elf_addr_t)p, sp++))
			return -EFAULT;
		len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
		if (!len || len > MAX_ARG_STRLEN)
			return -EINVAL;
		p += len;
	}
	if (put_user(0, sp++))
		return -EFAULT;
	mm->env_end = p;

	/* Put the elf_info on the stack in the right place.  */
	if (copy_to_user(sp, mm->saved_auxv, ei_index * sizeof(elf_addr_t)))
		return -EFAULT;
	return 0;
}

Note that the mm struct contains mm->arg_start, mm->arg_end, and mm->env_start. This same mm->env_start is used in the procfs environ handler

static ssize_t environ_read(struct file *file, char __user *buf,
			size_t count, loff_t *ppos)
{
	char *page;
	unsigned long src = *ppos;
	int ret = 0;
	struct mm_struct *mm = file->private_data;
	unsigned long env_start, env_end;

	/* Ensure the process spawned far enough to have an environment. */
	if (!mm || !mm->env_end)
		return 0;

	page = kmalloc(PAGE_SIZE, GFP_KERNEL);
	if (!page)
		return -ENOMEM;

	ret = 0;
	if (!mmget_not_zero(mm))
		goto free;

	spin_lock(&mm->arg_lock);
	env_start = mm->env_start;
	env_end = mm->env_end;
	spin_unlock(&mm->arg_lock);

	while (count > 0) {
		size_t this_len, max_len;
		int retval;

		if (src >= (env_end - env_start))
			break;

		this_len = env_end - (env_start + src);

		max_len = min_t(size_t, PAGE_SIZE, count);
		this_len = min(max_len, this_len);

		retval = access_remote_vm(mm, (env_start + src), page, this_len, FOLL_ANON);

		if (retval <= 0) {
			ret = retval;
			break;
		}

		if (copy_to_user(buf, page, retval)) {
			ret = -EFAULT;
			break;
		}

		ret += retval;
		src += retval;
		buf += retval;
		count -= retval;
	}
	*ppos = src;
	mmput(mm);

free:
	kfree(page);
	return ret;
}
2 Likes

Wasup pico thanks for takin’ the look is this a peer review ? maybe this time the reviewer actually read it let’s see

oh sure I do. still called it didn’t I :wink: Aight let’s see what you got mhmmmm nop, you’re not missing anything in that regard, as far as I’m concerned. I just didn’t cover it in the piece that was the hint, hence “and some to find on your own … and solve” and trust me, that’s doing a lot of work here. and the claim itself is kinda narrow, and that’s the point.

Well that’s fair is sitting right there in the binary, you don’t need to walk the chain sentence was sloppy but that’s not actually why CBC is fuckedup here so we have one function called identically for both encrypt and re-encryp that only works because XOR with a keystream is its own inverse. CBC encrypt and CBC decrypt are different operations, you can’t collapse them into one call as you know so ya’d already be dead on the “same code path both directions” and then yeah, the re-encryption cascade you called is the other half of it bla bla … so the conclusion holds, just for different reasons than what I wrote

Well simpler solution needs unpacking yeah a decrypt-at-entry re-encrypt-at-exit stub still needs a key somewhere, still needs you to instrument every call site, and immediately breaks when protected functions call each other now you’re tracking call depth and only re-encrypting on the outermost return, which is exactly what atleast aimed to do in this piece also the SIGSEGV+TF is doing this all kinda through hardware caller doesn’t know, call sites aren’t touched, mutual calls between protected functions just work. that’s the property worth paying for, not the license check specifically for a use cases ? I’d see it as anything where the binary runs in a hostile environment and runtime extraction is the actual threat and the ceiling gets more interesting when you start stacking techniques so …

I’ll stand by explicit_bzero also sounds cool what the fuck you talkin’ about it’s in glibc since 2.25 don’t quote me on that but it’s what Openbsd gave us specifically for this type shit and it does exactly one thing zero the buffer in a way the compiler cannot optimize out that’s all I need.

Well I can see what ya did there, I respect it I’m somewhat of an academic person myself Hahahaha alright that’s it see ya

1 Like

ya stopped before ld.so. dynamic linker runs before _start, walks the env to handle LD_PRELOAD and copies it to a different stack location. __environ ends up pointing at that copy. mm->env_start still has the kernel’s og

so environ_read hitting access_remote_vm(mm, env_start + src) is reading something __environ doesn’t even point at anymore. zero every string through __environ all you want, you’re zeroing the copy og is untouched

I can’t see why this would be true and I also can’t be bothered to compile and kernel debug so I just wrote an LD_PRELOAD test.

Source:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void print_proc_environ() {
  char ev[4096] = {};
  int fd = open("/proc/self/environ", O_RDONLY);
  read(fd, ev, sizeof(ev) - 1);

  printf("/proc/self/environ: %s\n", strstr(ev, "LD_PRELOAD="));
  close(fd);
}

__attribute__((constructor))
void preload_init() {
  printf("Find LD_PRELOAD in env\n");
  char* ev = getenv("LD_PRELOAD");
  printf("getenv: %s\n", ev);
  print_proc_environ();

  printf("Overwriting LD_PRELOAD in env\n");
  char* ev_start = ev - strlen("LD_PRELOAD=");
  printf("ev_start: %s\n", ev_start);
  memset(ev_start, 0, strlen(ev_start));

  printf("Find LD_PRELOAD in env again\n");
  ev = getenv("LD_PRELOAD");
  printf("New getenv: %s\n", ev);
  print_proc_environ();
}

Compile with gcc -shared -fPIC -o preload.o preload.c

Run it with your program:

$ LD_PRELOAD=/tmp/linux/preload.so ./target test
Find LD_PRELOAD in env
getenv: /tmp/linux/preload.so
/proc/self/environ: LD_PRELOAD=/tmp/linux/preload.so
Overwriting LD_PRELOAD in env
ev_start: LD_PRELOAD=/tmp/linux/preload.so
Find LD_PRELOAD in env again
New getenv: (null)
/proc/self/environ: (null)
key is yep

Which bypasses completely.

2 Likes

yeahhh …

giphy

that’s a legit one I think it’s like on versions where getenv hands you a live pointer into mm->env_start good catch nobody in the anti-debug literature seems to have written this up every implementation I found puts these checks in a regular constructor and has the same window you just exploited I’ll update the piece for sure

1 Like

Can’t say I’ve read any of the anti-debug literature for Linux but perhaps they are outdated? I came across this LD_PRELOAD check with /proc/pid/environ before and I had bypassed it in the same way so I was sure this was the same case. Maybe you will have to verify everything else you’ve read :distorted_face:

I forgot to mention but I think you missed the LD_LIBRARY_PATH check too. Not sure how to properly validate the integrity of your process with these injection vectors, but perhaps you will have to write an anti-cheat-style process integrity checker going over a whitelist of shared objects and verifying executable mappings. Seems like a lot of work though.

Ohh for sure not talkin’ about anythin’ new here the literature I know is thin and what exists is mostly old wouldn’t trust it as gospel either LD_LIBRARY_PATH is the same class of shit. What I will say is writin’ and shipping I can’t fuckin’ belive I just said that Hahahah the whitelist without making it trivially patchable, and handling legitimate variation across distros where libc lives in different paths not unsolvable, but yeah, it’s work lot more moving parts than an env var scan.

Yeah, there’s no real easy solution here. Might I suggest creating your own completely custom and hardened hardware just to host your software? Surely that would reduce the number of determined attackers to 0.01% lol

1 Like

TESO was a German and Austrian group active in the late 1990s and early 2000s. Their members included Scut, Skyper, and Halvar Flake, and they produced some of the more technically rigorous research of that era. - cite