💾 Archived View for thrig.me › software › assembly › slab-of-code captured on 2024-08-31 at 12:41:30. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-12-28)

-=-=-=-=-=-=-

Run a Slab of Code

So whilst fiddling around with assembly (there are probably better things to do with your time) a notion was struck whether one could write a slab of code in memory and then execute it. Alternatives here are to use inline assembly, or to write the whole thing in assembly, which have various advantages and disadvantages. The "slab of code" approach was inspired by working through some Forth propaganda; that propaganda showed how easy it was to simply write the suitable opcodes into memory and then arrange for them to be called. Modern operating systems may however make this task difficult.

Some attempt will be made at a (ex post facto) iterative approach showing how the longer script was worked out. git branches or frequent commits may help to preserve such iterative details. What I actually was doing was fiddling with a bunch of files, a lot.

all the source files in one file

Boilerplate

The following code obviously does not do much. But it's a start. There is some error checking, and a value is printed so we can have some confidence that the code is being gotten through. If the value is no longer printed after fiddling with the code, we know that something has gone wrong. Without error checking, an unexpected failure to malloc could complicate matters and run contrary to our mental model of what the code should be doing. (Another school of thought is to run for as long as possible, and maybe to have a REPL to poke around at what happened.)

    #include <err.h>
    #include <stdio.h>
    #include <stdlib.h>

    #define SLABSIZE 4096

    int main(int argc, char *argv[]) {
        int value = 41;

        void *slab = malloc(SLABSIZE);
        if (!slab) err(1, "malloc");

        printf("%d\n", value);
    }

boilerplate.c

Also it may help to have working code to retreat to if some advance or the other fails, and you could diff the working and non-working code to see exactly what changed.

    $ cc boilerplate.c -o boilerplate
    $ ./boilerplate
    41

Jump

Next up is to instruct the code to jump to the random slab of memory. This will (probably) not work, as there is (most likely) invalid machine instructions in the slab of memory. If you're competent at this you could probably do multiple steps at once, but that's a great way to create unexpected issues from getting something wrong.

    #include <err.h>
    #include <stdio.h>
    #include <stdlib.h>

    typedef void (*fn)(void);

    #define SLABSIZE 4096

    int main(int argc, char *argv[]) {
        int value = 41;

        void *slab = malloc(SLABSIZE);
        if (!slab) err(1, "malloc");

        fn call = slab;
        call();

        printf("%d\n", value);
    }

jump.c

    $ cc jump.c -o jump
    $ ./jump
    Segmentation fault (core dumped)

Progress!

Fail Differently

Since we may not have any idea how to run valid code in a random slab of memory, another plan is to run invalid code. Deliberate fault injection is a debugging technique: if the code does not fail, then something else is probably going on. Is the code even being reached? Are you still testing against an outdated binary? Are you even on the right host? Etc.

    #include <err.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    typedef void (*fn)(void);

    #define SLABSIZE 4096

    int main(int argc, char *argv[]) {
        int value = 41;

        void *slab = malloc(SLABSIZE);
        if (!slab) err(1, "malloc");

        memset(slab, 0xCC, SLABSIZE);

        fn call = slab;
        call();

        printf("%d\n", value);
    }

fail.c

Here the 0xCC memset is the AMD64 opcode for INT3. If you are following along on some other platform, you'll need to find a suitable instruction, or emulate this code somehow. I am using OpenBSD 7.4 which will introduce some other issues, presently.

    $ cc fail.c -o fail
    $ ./fail
    Segmentation fault (core dumped)

This is not what we want; the expectation was that some other error should have been generated by INT3.

W^X Memory Protection

Writable or executable (but not both) is a feature on various operating systems. Maybe by marking our slab of memory as executable we will see a different error?

    #include <sys/mman.h>

    #include <err.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    typedef void (*fn)(void);

    #define SLABSIZE 4096

    int main(int argc, char *argv[]) {
        int value = 41;

        void *slab = malloc(SLABSIZE);
        if (!slab) err(1, "malloc");

        memset(slab, 0xCC, SLABSIZE);

        if (mprotect(slab, SLABSIZE, PROT_EXEC) != 0) err(1, "mprotect");
        fn call = slab;
        call();

        printf("%d\n", value);
    }

wxfail.c

    $ cc wxfail.c -o wxfail
    $ ./wxfail
    Trace/BPT trap (core dumped)

Success! The different failure message most likely indicates that a 0xCC in the slab of memory was reached.

Less Failure

The accumulation of core files has gotten a bit tedious; can we reduce the count by instead of INT3 maybe calling the RET opcode to simply return to whence it came?

    #include <sys/mman.h>

    #include <err.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    typedef void (*fn)(void);

    #define SLABSIZE 4096

    int main(int argc, char *argv[]) {
        int value = 41;

        void *slab = malloc(SLABSIZE);
        if (!slab) err(1, "malloc");

        memset(slab, 0xC3, SLABSIZE); // RET not INT3

        if (mprotect(slab, SLABSIZE, PROT_EXEC) != 0) err(1, "mprotect");
        fn call = slab;
        call();

        printf("%d\n", value);
    }

ret.c

    $ cc ret.c -o ret
    $ ./ret
    41

Looking good. Obviously we will want to increment that 41 to a more suitable value. (For those who belong to different cultures, the number 42 is significant due to "The Hitchhiker's Guide to the Galaxy". Some have made the point that unix is closer to a literary tradition than more graphically oriented systems.)

More than just RET

Now it is time to add code that (hopefully) does nothing. If the territory is new to you, then you may want to take small, careful steps. Others roll with large leaps.

morethanret.c

Also note that we are writing the slab of memory out to a file. This allows inspection (and disassembly) of what our code has written, which may not be what we thought it was.

    $ cc morethanret.c -o morethanret
    $ ./morethanret
    41
    $ hexdump -C -n 8 slab
    00000000  90 90 c3 90 c3 c3 c3 c3                           |........|
    00000008
    $ ndisasm -b 64 slab | sed 4q
    00000000  90                nop
    00000001  90                nop
    00000002  C3                ret
    00000003  90                nop

ndisasm is part of NASM, which is usually available in a ports or package system near you.

Sidequest: How About Some Guard Rails?

Running random code may cause our program to do random things. Backups aside, it may be good to restrict what the code can do, either by running it under a throwaway virt, or on OpenBSD to restrict what system calls it can make. The need for this depends on how dangerous the code is, and how likely damage would be if the code does go awry. Pledge support is pretty easy to add, and will kill our process with a message to syslog about what went wrong.

pledge.c

    $ cc pledge.c -o pledge
    $ ./pledge
    41

Sidequest: What Does Some Actual Code Look Like?

Now seems as good as time as any to show what a simple C program looks like when disassembled. I had been doing this as the above went along, though linear documents can only poorly capture the often wandering stumbles of a real life development session.

    #include <stdio.h>
    void plusone(int *n) { *n += 1; }
    int main(void) {
        int value = 41;
        plusone(&value);
        printf("%d\n", value);
    }

simple.c

    $ cc simple.c -o simple
    $ ./simple
    42
    $ grep diss ~/.kshrc
    alias diss='objdump -M intel -D'
    $ diss simple > s.out
    $ wc s.out
        1071    6010   48929 s.out

Modern systems can add a lot of boilerplate. If we focus on the plusone function call, the meat is some stack manipulation and an ADD call. The exact code may however depend on the compiler and any optimization flags (try -Oz for example), so check your local system for details.

    $ perl -00 -nle 'print if m/plusone>:/' s.out
    0000000000001a50 <plusone>:
        1a50:       f3 0f 1e fa             endbr64
        1a54:       4c 8b 1d 7d 12 00 00    mov    r11,QWORD PTR ds:0x127d
        1a5b:       4c 33 1c 24             xor    r11,QWORD PTR [rsp]
        1a5f:       55                      push   rbp
        1a60:       48 89 e5                mov    rbp,rsp
        1a63:       48 89 7d f8             mov    QWORD PTR [rbp-8],rdi
        1a67:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-8]
        1a6b:       8b 08                   mov    ecx,DWORD PTR [rax]
        1a6d:       83 c1 01                add    ecx,0x1
        1a70:       89 08                   mov    DWORD PTR [rax],ecx
        1a72:       5d                      pop    rbp
        1a73:       4c 33 1c 24             xor    r11,QWORD PTR [rsp]
        1a77:       4c 3b 1d 5a 12 00 00    cmp    r11,QWORD PTR ds:0x125a
        1a7e:       0f 84 0b 00 00 00       je     1a8f <plusone+0x3f>
        1a84:       cc                      int3
        1a85:       cc                      int3
        1a86:       cc                      int3
        1a87:       cc                      int3
        1a88:       cc                      int3
        1a89:       cc                      int3
        1a8a:       cc                      int3
        1a8b:       cc                      int3
        1a8c:       cc                      int3
        1a8d:       cc                      int3
        1a8e:       cc                      int3
        1a8f:       c3                      ret

The INT3 minefield that the code jumps over is a security measure. A "rop gadget" might be something to read up on.

Running Actual Code

Even just the meat of this program would be annoying to memset in a C program, so a better plan is for NASM to assemble it. An advantage here is that the assembly is easier to edit than fumbling around with hex codes and offsets. If you were compiling this on the fly you would likely end up with various routines that do the right thing.

    BITS 64
    endbr64
    push rbp
    mov rbp, rsp
    mov qword [rbp-8], rdi
    mov rax, qword [rbp-8]
    mov ecx, dword [rax]
    add ecx, 0x1
    mov dword [rax], ecx
    pop rbp

meat.asm

    $ nasm meat.asm -o meat
    $ wc -c meat
          24 meat
    $ perl -pe 's/(.)/$i++;sprintf "0x%vx,",$1/eg;s/,$/\n/;END{warn $i}' meat
    0xf3,0xf,0x1e,0xfa,0x55,0x48,0x89,0xe5,0x48,0x89,0x7d,0xf8,0x48,0x8b,0x45,0xf8,0x8b,0x8,0x83,0xc1,0x1,0x89,0x8,0x5d
    24 at -e line 1, <> line 1.

This run of hex characters (and how many there are) is fairly easy to paste into the C.

    #include <sys/mman.h>

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

    typedef void (*fn)(int *); // NOTE the signature change!

    #define SLABSIZE 4096

    int
    main(int argc, char *argv[])
    {
        int value = 41;

        unsigned char *slab = malloc(SLABSIZE);
        if (!slab) err(1, "malloc");
        memset(slab, 0xC3, SLABSIZE);
        memcpy(slab, &(unsigned char[]){0xf3, 0xf,  0x1e, 0xfa, 0x55, 0x48,
                                        0x89, 0xe5, 0x48, 0x89, 0x7d, 0xf8,
                                        0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x8,
                                        0x83, 0xc1, 0x1,  0x89, 0x8,  0x5d},
               24);
        int fd = open("slab", O_WRONLY | O_CREAT, 0666);
        if (fd <= 0) err(1, "open");
        write(fd, slab, SLABSIZE);
        close(fd);

        if (mprotect(slab, SLABSIZE, PROT_EXEC) != 0) err(1, "mprotect");
    #ifdef __OpenBSD__
        if (pledge("stdio", NULL) == -1) err(1, "pledge");
    #endif
        fn call = (fn) slab;

        call(&value);

        printf("%d\n", value);
    }

actual.c

    $ cc actual.c -o actual
    $ ./actual
    42

Success! Opcodes have been written into memory and executed. Of course this is hardly "mission accomplished" as there may be many more things one could do here, and many things that could break, but the ability to compile bits of assembly and execute them from a C program may be of some utility.

Piled Higher and Deeper

Possible improvements include:

Instigation

some Forth propaganda