💾 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
⬅️ Previous capture (2023-12-28)
-=-=-=-=-=-=-
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
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); }
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
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); }
$ cc jump.c -o jump $ ./jump Segmentation fault (core dumped)
Progress!
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); }
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.
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); }
$ 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.
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); }
$ 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.)
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.
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.
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.
$ cc pledge.c -o pledge $ ./pledge 41
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); }
$ 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.
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
$ 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); }
$ 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.
Possible improvements include: