diff --git a/Makefile b/Makefile index 6a96ff0..fb8d363 100644 --- a/Makefile +++ b/Makefile @@ -17,11 +17,17 @@ ALPINE_URL="http://dl-cdn.alpinelinux.org/alpine/v3.12/releases/x86_64/$(ALPINE_ # container is built and stored in here CACHE_DIR=.cache +PATCHELF=patchelf +OBJCOPY=$(CROSS_COMPILE)objcopy +OBJDUMP=$(CROSS_COMPILE)objdump + all: setup exec-in-container # rerun inside container exec-in-container: setup - rsync -av $(RKT_NAME) main_bc.c Makefile $(CACHE_DIR)/alpine/build/ + rsync -avR $(RKT_NAME) main_bc.c startup.c Makefile scripts/crimes.c scripts/crimes.h \ + $(CACHE_DIR)/alpine/build/ + sudo chmod 777 $(CACHE_DIR)/alpine/build $(CACHE_DIR)/alpine/build/scripts sudo systemd-nspawn -UD $(CACHE_DIR)/alpine -- sh -c "cd build; make $(APP_NAME)" cp $(CACHE_DIR)/alpine/build/$(APP_NAME) . @@ -29,9 +35,24 @@ clean-in-container: sudo systemd-nspawn -UD $(CACHE_DIR)/alpine -- sh -c "cd build; rm -rf *" # main build stuff -$(APP_NAME): main_bc.c app.o +DEBUG_CFLAGS=-g3 -no-pie -fno-pie +$(APP_NAME): main_bc.c app.o scripts/crimes startup.bin # $(CC) -o $@ -pipe -O3 -DAPP_NAME='"$(APP_NAME)"' -static $^ -lracket3m -lrktio -lucontext -lffi - $(CC) -o $@ -pipe -O3 -DAPP_NAME='"$(APP_NAME)"' $^ /usr/lib/libracket3m.a /usr/lib/librktio.a /usr/lib/libffi.a /lib/libucontext.a + $(CC) -o $@ -pipe -O3 $(DEBUG_CFLAGS) -DAPP_NAME='"$(APP_NAME)"' -Wl,-eelf_crimes \ + main_bc.c app.o /usr/lib/libracket3m.a /usr/lib/librktio.a /usr/lib/libffi.a /lib/libucontext.a + $(PATCHELF) --remove-needed $$($(OBJDUMP) -x $@ | grep NEEDED | awk '{print $$2}') $@ + $(OBJCOPY) --remove-section .interp -- $@ + scripts/crimes $@ + +scripts/crimes: scripts/crimes.c scripts/crimes.h + $(CC) -o $@ -pipe -O3 -g0 $< + +startup.elf: startup.c + $(CC) -flto -O3 -g3 -Wl,-z,norelro -ffunction-sections -fdata-sections -static -pipe \ + -Wl,-ecrimes_startup_run -fno-stack-protector -nostartfiles -fpie -pie -o $@ $< + +startup.bin: startup.elf + $(OBJCOPY) -O binary $< $@ # this is faster than --c-mods by a lot # it's less portable but like, we're containerized already so it'll work @@ -43,6 +64,7 @@ app.zo: $(RKT_NAME) clean: $(RM) $(APP_NAME) *.zo *.o + $(RM) scripts/crimes startup.bin startup.elf mrproper: clean sudo $(RM) -rf $(CACHE_DIR)/alpine diff --git a/main_bc.c b/main_bc.c index b2a3db5..2f23f5b 100644 --- a/main_bc.c +++ b/main_bc.c @@ -3,26 +3,55 @@ #include #include #include -#include // patchups for musl vs glibc stuff #include -int stat(const char *restrict path, struct stat *restrict buf) { +extern int __xstat(int, const char* restrict, struct stat* restrict); +extern int __lxstat(int, const char* restrict, struct stat* restrict); +extern int __fxstat(int, int, struct stat*); +int stat(const char* restrict path, struct stat* restrict buf) { return __xstat(1, path, buf); } -int fstat(int fildes, struct stat *buf) { +int fstat(int fildes, struct stat* buf) { return __fxstat(1, fildes, buf); } -int lstat(const char *restrict path, struct stat *restrict buf) { +int lstat(const char* restrict path, struct stat* restrict buf) { return __lxstat(1, path, buf); } extern void* __dso_handle; +extern int __cxa_atexit(void (*)(), void*, void*); int atexit(void (*function)(void)) { return __cxa_atexit(function, NULL, __dso_handle); } +// custom entry wrapper + +// the thing with this project is it's going to generate a binary that can run literally everwhere +// if you build it on exactly the right environment +// as opposed to the usual portable build, unportable output +// ok so this basically assumes startup.bin contains ONLY .text and .rodata and that it's PIE and +// that .rodata directly follows .text and that entry is at the top of .text, and like, this is all +// true but it's not _guaranteed_ so like, weh +void __attribute__((noinline)) crimes_startup_run(void* addr) { + // haha yes + asm volatile(".incbin \"startup.bin\"" :: "rm"(addr) : "memory"); +} + +#include +extern void _start(unsigned long, unsigned long, unsigned long, unsigned long); +void elf_crimes(void) { + // re-exec if necessary + crimes_startup_run(__builtin_frame_address(0)); + + // this _should_ tail call and all should be well...hopefully... + return _start(0, 0, 0, 0); +} + +// embedded zo (but fast, we avoid rendering and subsequently unrendering megabytes of C which is +// Mega Slow and simply ld -r -b the zo at the cost of build portability but idk i'm pretty sure +// bsd has -r -b too tbh) extern const char _binary_app_zo_start; extern const char _binary_app_zo_end; diff --git a/scripts/crimes.c b/scripts/crimes.c new file mode 100644 index 0000000..ba756a5 --- /dev/null +++ b/scripts/crimes.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include + +#include + +#define CRIMES_ELF_BITS 32 +#include "crimes.h" +#undef CRIMES_ELF_BITS +#define CRIMES_ELF_BITS 64 +#include "crimes.h" +#undef CRIMES_ELF_BITS + +int main(int argc, char* argv[]) { + if (argc < 2) { + printf("usage: %s \n", argv[0]); + return -1; + } + + char* fname = argv[1]; + int fd = open(fname, O_RDWR); + if (fd < 0) { + perror("open"); + return -1; + } + ssize_t sz = lseek(fd, 0, SEEK_END); + if (sz < 0) { + perror("lseek"); + return -1; + } + if (lseek(fd, 0, SEEK_SET) < 0) { + perror("lseek"); + return -1; + } + + void* elf = mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (elf == MAP_FAILED) { + perror("mmap"); + return -1; + } + close(fd); + + // we're not going to emulate wacky technically nonstandard behavior here + if (((uint8_t*) elf)[EI_CLASS] < ELFCLASS64) { + crimes_32(elf); + } else { + crimes_64(elf); + } + + munmap(elf, sz); +} diff --git a/scripts/crimes.h b/scripts/crimes.h new file mode 100644 index 0000000..79bc902 --- /dev/null +++ b/scripts/crimes.h @@ -0,0 +1,43 @@ +#include +#include +#include +#include + +#include + +#if CRIMES_ELF_BITS == 32 +#define Elf_Ehdr Elf32_Ehdr +#define Elf_Phdr Elf32_Phdr +#define CRIMES_PATCH_FUNC crimes_32 +#else +#define Elf_Ehdr Elf64_Ehdr +#define Elf_Phdr Elf64_Phdr +#define CRIMES_PATCH_FUNC crimes_64 +#endif + +#define crimes_str(x) #x + +void CRIMES_PATCH_FUNC(Elf_Ehdr* elf) { + uint8_t* phdrs = ((uint8_t*) elf) + elf->e_phoff; + ssize_t src = -1; + ssize_t dst = -1; + for (size_t i = 0; i < elf->e_phnum; i++) { + Elf_Phdr* phdr = (Elf_Phdr*) (phdrs + elf->e_phentsize * i); + if (phdr->p_type == PT_INTERP) { + src = i + 1; + dst = i; + } + } + if (src < 0) { + printf("[%s] couldn't find PT_INTERP - skipping\n", crimes_str(CRIMES_PATCH_FUNC)); + return; + } + memmove(phdrs + dst * elf->e_phentsize, phdrs + src * elf->e_phentsize, + (elf->e_phnum - dst) * elf->e_phentsize); + elf->e_phnum -= 1; + printf("[%s] patch complete\n", crimes_str(CRIMES_PATCH_FUNC)); +} + +#undef Elf_Ehdr +#undef Elf_Phdr +#undef CRIMES_PATCH_FUNC diff --git a/scripts/setup.sh b/scripts/setup.sh index 05625dd..7be0422 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -3,13 +3,15 @@ set -e apk upgrade --update-cache --available -apk add alpine-sdk ca-certificates libcrypto1.1 libssl1.1 chrpath libffi-dev libucontext-dev +apk add alpine-sdk ca-certificates libcrypto1.1 libssl1.1 chrpath libffi-dev libucontext-dev \ + patchelf cd /racket-*/src export CFLAGS="$CFLAGS -D_GNU_SOURCE" export LDFLAGS="$LDFLAGS -lucontext" rm -Rf src/foreign/libffi -./configure --prefix=/usr --sysconfdir=/etc --disable-docs --enable-strip --enable-libs --disable-gracket +./configure --prefix=/usr --sysconfdir=/etc --disable-docs --enable-strip --enable-libs \ + --disable-gracket make -j $(nproc) CPUS=$(nproc) make install raco pkg install --deps search-auto cext-lib diff --git a/startup.c b/startup.c new file mode 100644 index 0000000..d16512b --- /dev/null +++ b/startup.c @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include +#include +#include + +void crimes_startup_run(uint8_t* frame) { + // we trash argv up to hack on ld.so arguments without needing malloc + // this is pad space to catch that and not explode uwu + volatile char _pad[(sizeof(char*) * 4)]; + + // XXX: assumes stack goes up + // big weh + size_t argc = *((size_t*) (frame + sizeof(size_t))); + char** argv = ((char**) (frame + sizeof(size_t) + sizeof(char*))); + char** envp = argv + argc + 1; + + // fake usage of _pad + // also a barrier to ensure argc gets read + asm volatile("" :: "rm"(_pad) : "memory"); + + syscall(SYS_write, STDOUT_FILENO, "[crimes] detect maps\n", 21); + + int fd = syscall(SYS_open, "/proc/self/maps", O_RDONLY); + if (fd < 0) { + syscall(SYS_write, STDOUT_FILENO, "[crimes] open failed\n", 21); + syscall(SYS_exit, -1); + } + char buf[32]; + int state = 0; + while (true) { + ssize_t res = syscall(SYS_read, fd, buf, sizeof(buf)); + if (res == 0) { + break; + } else if (res < 0) { + syscall(SYS_write, STDOUT_FILENO, "[crimes] read error\n", 20); + syscall(SYS_exit, -1); + } + + for (ssize_t i = 0; i < res; i++) { + switch (state) { + case 0: + if (buf[i] == '/') { state++; } + else { state = 0; } + break; + case 1: + if (buf[i] == 'l') { state++; } + else { state = 0; } + break; + case 2: + if (buf[i] == 'd') { state++; } + else { state = 0; } + break; + case 3: + if (buf[i] == '-') { state++; } + else { state = 0; } + break; + default: + break; + } + if (state > 3) { + break; + } + } + if (state > 3) { + break; + } + } + + if (state > 3) { + syscall(SYS_write, STDOUT_FILENO, "[crimes] detected libc. going back to startup\n", 46); + return; + } + + syscall(SYS_write, STDOUT_FILENO, "[crimes] running ld.so\n", 23); + + // XXX: currently we hardcode the glibc parameters + // detect glibc or musl at runtime and load the appropriate params + char* ld = "/usr/lib/ld-2.32.so"; + char* pl = "--preload"; + char* ldlp = + "/usr/lib/libm.so.6:/usr/lib/libpthread.so.0:/usr/lib/libdl.so.2:/usr/lib/libc.so.6"; + uintptr_t args[4 + argc]; + args[0] = (uintptr_t)ld; + args[1] = (uintptr_t)pl; + args[2] = (uintptr_t)ldlp; + + for (size_t i = 0; i < argc; i++) { + args[3 + i] = (uintptr_t) argv[i]; + } + + args[3 + argc] = 0; + + syscall(SYS_execve, ld, args, envp); + + syscall(SYS_write, STDOUT_FILENO, "[crimes] exec failed!\n", 22); + syscall(SYS_exit, -1); +}