diff --git a/.checkpatch.ignore b/.checkpatch.ignore index 541c6ba0cb..97660c8b8a 100644 --- a/.checkpatch.ignore +++ b/.checkpatch.ignore @@ -1,3 +1,4 @@ # Ignore directories containing third-party files chapters/compute/synchronization/drills/tasks/threadsafe-data-struct/support content/assignments/async-web-server/src/http-parser +content/assignments/elf-loader/tests diff --git a/config.yaml b/config.yaml index 16f15d1578..df3a75a121 100644 --- a/config.yaml +++ b/config.yaml @@ -382,6 +382,7 @@ docusaurus: subsections: - Mini Libc/: chapters/software-stack/libc/projects/mini-libc/ - Memory Allocator/: content/assignments/memory-allocator/ + - ELF Loader/: content/assignments/elf-loader/ - Parallel Firewall/: content/assignments/parallel-firewall/ - Mini Shell/: content/assignments/minishell/ - Asynchronous Web Server/: content/assignments/async-web-server/ diff --git a/content/assignments/elf-loader/README.md b/content/assignments/elf-loader/README.md new file mode 100644 index 0000000000..9b15191c2f --- /dev/null +++ b/content/assignments/elf-loader/README.md @@ -0,0 +1,368 @@ +# ELF Loader Assignment + +## Objecives + +* Practice working with virtual memory, memory protection, and manual relocation. +* Understand the difference between different types of executables, like PIE, non-PIE, statically-linked, etc. +* Understand the stack layout expected by an executable, environment variables, auxiliary vector, command-line arguments, etc. + +## Statement + +Implement a custom minimal ELF loader, capabale of loading and executing statically linked binaries in Linux. + +Your loader must eventually support: + +* Minimal static binaries that make direct Linux syscalls (without libc) +* Statically linked **non-PIE** C programs using `libc` +* Statically linked **PIE** executables + +## Support Code + +The support code consists of three directories: + +* `src/` where you will create your sollution +* `test/` contains the test suite and a bash script to verify your work + +The test suite consists of source code files (`.c` and `.asm`), that will be compiled and then executed using your loader. +You can use the `Makefile` to compile all test files. + +## Implementation + +The assignment is split into **4 graded parts**, totaling **90 points** (10 points are given by the linter): + +### 1. ELF header validation (**10 points**) + +**Goal:** Before loading the ELF file, check if it is a valid, 64-bit ELF. +You must check for 2 cases: + +* Check the [ELF magic](https://unix.stackexchange.com/questions/153352/what-is-elf-magic), defined [here](https://chromium.googlesource.com/external/elfutils/+/dts-0.168/libelf/elf.h#120). +* Check the [ELF class](https://chromium.googlesource.com/external/elfutils/+/dts-0.168/libelf/elf.h#123), it should be `ELFCLASS64`. + +In case any of the items above are wrong, print one of the following messages and exit with the corresponding error code: + +`Not a valid ELF file`, exit with code 3. + +or + +`Not a 64-bit ELF`, exit with code 4. + +### 2. Minimal loader for syscall-only binaries (**10 points**) + +**Goal:** Make the loader work with extremely minimal ELF binaries (usually written in assembly) that make direct syscalls and do not use libc. + +* All memory segments can be loaded with `RWX` permissions. +* No need to set up `argv`, `envp`, or auxiliary vectors. +* These binaries call syscall instructions directly, so `libc` is not used. + +For this task, you will need to: + +* Open the file and map it somewhere in the memory +* Pass through the section headers, and for the `PT_LOAD` sections create new memory regions (they can have RWX permissions for now), then copy the section from the file into the newly created memory region. +* Pass the execution to the new ELF, by jumping to the entry point. + +**Examples/Resources:** + +* [ELF Specification](https://refspecs.linuxbase.org/elf/gabi4+/ch5.pheader.html) +* [OSDev](https://wiki.osdev.org/ELF) + +### 3. Load memory regions with correct permissions (**10 points**) + +**Goal:** Instead of RWX, check the memory protection flags (`PF_R`, `PF_W`, `PF_X`) from the ELF `Program Headers`. + +* Use `mprotect()` or map with the correct permissions directly using `mmap()`. + +**Key Concepts:** + +* `PT_LOAD` program headers contain `p_flags` to specify memory permissions. +* These must be respected to mimic the kernel loader. +* [ELF Specification](https://refspecs.linuxbase.org/elf/gabi4+/ch5.pheader.html) + +### 4. Support static non-PIE binaries with libc (**30 points**) + +**Goal:** Load and run statically linked **non-PIE** C binaries compiled with libc (e.g., via `gcc -static`). + +* Must set up a valid process **stack**, including: + + * `argc`, `argv`, `envp` + * `auxv` vector (with entries like `AT_PHDR`, `AT_PHENT`, `AT_PHNUM`, etc.) + +For this, you need to map a new memory region, that will become the new stack, then copy all the required information there. + +The executable expects the stack layout as seen in the figure below: + +![Stack Layout](./img/stack-layout.drawio.svg) + +You can see more details about the stack [here](https://lwn.net/Articles/631631/). + +You will have to reserve a memory region large enough for the stack (you can use the maximum allowed stack size, using `getrlimit`, or you can use a hardcoded value large enough to fit everything). +After that, you need to copy the argc, argv and envp in the expected layout, then set up the auxv. + +**Note:** `argv` and `envp`, since they consist of strings, will be placed as the **pointer to the string** on the stack, not the string itself. +**Note:** Make sure the mapped regions have the correct length, **be careful of the difference between `p_filesz` and `p_memsz`**. + +#### argc, argv (5 points out of 30) + +The command-line arguments must be placed first at the top of the stack, as seen in the picture above. +The loader can be used as `./elf_loader ./no-pie-exec arg1 arg2 arg3`. +`arg1`, `arg2` and `arg3` must be placed on the stack for the loaded executable. +`argc` will be also placed on the at the top of the stack. + +#### envp (5 points out of 30) + +The environment variables should be placed after the command-line arguments. +For this, you just have to copy everything from the `char **envp` array and place it on the stack. + +#### auxv (10 points out of 30) + +The auxiliary vector, auxv, is a mechanism for communicating information from the kernel to user space. +It's basically a list of key-value pairs that contains different information about the state of the executable. +You can see the keys and required values of the auxv [in the man pages](https://man7.org/linux/man-pages/man3/getauxval.3.html). +For example, for the key `AT_PAGESZ` (defined as 6 in [elf.h](https://elixir.bootlin.com/glibc/glibc-2.42.9000/source/elf/elf.h#L1205)), that needs to contain the value of the page size, the memory will look as follows: + +```text +0xfff...... --> High Addresses +----------- + 4096 # Page Size + 6 # AT_PAGESZ key +----------- +----------- +0x000...... --> Low Addresses +``` + +The auvx must end with an `AT_NULL` key with a 0 value, so an auxv that sets `AT_PAGESZ`, `AT_UID` and `AT_NULL` will look like this on the stack: + +![Auxv Example](./img/auxv-example.drawio.svg) + +**Note:** Beware of the `AT_RANDOM` entry, the application will crash if you do not set it up properly. + +**Docs:** + +* [How programs get run: ELF binaries](https://lwn.net/Articles/631631/) (See section: `Populating the stack`) +* [auxv man page](https://man7.org/linux/man-pages/man3/getauxval.3.html) + +### 5. Support static PIE executables (**30 points**) + +**Goal:** Make your loader support static **PIE (Position Independent Executable)** binaries. + +* ELF type will be `ET_DYN`, and segments must be mapped at a **random base address**. +* Entry point and memory segment virtual addresses must be adjusted by the `load_base`. + +**Additional Requirements:** + +* Must still build a valid stack (`argc`, `argv`, `auxv`, etc.) +* Handle relocation of entry point and program headers correctly. + +You will need to load all the segments relative to a random base address +Beware of the auxv entries, some of them will need to be adjusted to the offset. + +**Docs:** + +* [What is a PIE binary?](https://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries) +* [Example ELF Loader](https://0xc0ffee.netlify.app/osdev/22-elf-loader-p2) +* [Another ELF Loader Example](https://www.mgaillard.fr/2021/04/15/load-elf-user-mode.html) + +## Debugging + +Here are some useful tips and tools to debug your ELF loader: + +### General Tips + +* **Start simple**: First test with a syscall-only ELF binary (e.g., `write` + `exit`). +* **Use GDB**: Run `gdb ./elf_loader` and set breakpoints in the loader and inside the loaded ELF. + You can use `add-symbol-file path-to-elf start-address` to debug the libc entry and the elf execution with debugging symbols. +* **Check memory layout**: Print segment addresses and protections. You can use `pmap $(pidof elf-loader)` +* **Use PWNGDB**: Use [`PwnGDB`](https://github.com/pwndbg/pwndbg) or other similar plugins. They provide a lot of help during debugging. + +#### Useful Tools + +* `readelf -l -h your_binary` +* `objdump -d your_binary` +* `gdb ./elf_loader` +* `pmap $(pidof elf_loader)` + +#### Debugging Example + +Let's say the `no_pie` test fails with a segmentation fault, with no other messages printed. +In order to debug that, we must run `gdb ./src/elf-loader` and `run ./tests/snippets/no_pie`: + +```gdb +$rax : 0xcc0 +$rbx : 0x1 +$rcx : 0x0000000000427aee → 0xc7a777fffff0003d ("="?) +$rdx : 0x0 +$rsp : 0x00007ffff7df6bc0 → 0x0000000000401835 → 0x20ec8348e5894855 +$rbp : 0x00007ffff7df6c00 → 0x0000000000000000 +$rsi : 0x20 +$rdi : 0x0 +$rip : 0x0000000000403e7b → 0x894864c030028b48 +$r8 : 0x0 +$r9 : 0x00000000004a8480 → "glibc.malloc.mxfast" +$r10 : 0x53053053 +$r11 : 0x246 +$r12 : 0x00007ffff7df6c28 → 0x00007ffff7df7fe8 → "./tests/snippets/no_pie" +$r13 : 0x0 +$r14 : 0x00000000004aa000 → 0x00000000004582f0 → 0xffefc1c5fa1e0ff3 +$r15 : 0x00000000004004e8 → 0x0000000000000000 +$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification] +$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── +0x00007ffff7df6bc0│+0x0000: 0x0000000000401835 → 0x20ec8348e5894855 ← $rsp +0x00007ffff7df6bc8│+0x0008: 0x0000000000401710 → 0x8949ed31fa1e0ff3 +0x00007ffff7df6bd0│+0x0010: 0x0000000000000000 +0x00007ffff7df6bd8│+0x0018: 0x0000000000000002 +0x00007ffff7df6be0│+0x0020: 0x00007fffffffda98 → 0x00007fffffffde42 → "/home/stefan/projects/facultate/asist/elf-loader/a[...]" +0x00007ffff7df6be8│+0x0028: 0x00007fffffffdab0 → 0x00007fffffffdeb0 → "SHELL=/bin/bash" +0x00007ffff7df6bf0│+0x0030: 0x00007ffff7ff25e8 → 0x00007ffff7f42b60 → endbr64 +0x00007ffff7df6bf8│+0x0038: 0x0000000000000001 +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── + 0x403e6c mov edi, r13d + 0x403e6f call 0x428e80 + 0x403e74 mov rdx, QWORD PTR [rip+0xa5bcd] # 0x4a9a48 + → 0x403e7b mov rax, QWORD PTR [rdx] + 0x403e7e xor al, al + 0x403e80 mov QWORD PTR fs:0x28, rax + 0x403e89 cmp QWORD PTR [rip+0xa60f7], 0x0 # 0x4a9f88 + 0x403e91 je 0x403e9f + 0x403e93 call 0x0 +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── +[#0] Id 1, Name: "elf-loader", stopped 0x403e7b in ?? (), reason: SIGSEGV +``` + +Note that during this tutorial we use [`gef`](https://github.com/hugsy/gef), that is almost identical to `pwngdb`. +We advise you to use `pwngdb`, there will be no difference in commands used. + +We can see that the program crashes somewhere with no code or debugging symbols attached, so we can assume it is inside the loaded ELF. +To test this, let's break before we jump to the program entry point. + +```gdb +$ break elf-loader.c:197 # This is the line for `__asm__ __volatile__`, it will be a different line for you +Breakpoint 1 at 0x7ffff7f437c8: file elf-loader.c, line 197. +$ run ./tests/snippets/no_pie +●→ 0x7ffff7f437c8 mov rax, QWORD PTR [rbp-0x1d0] + 0x7ffff7f437cf mov rdx, QWORD PTR [rbp-0x190] + 0x7ffff7f437d6 mov rsp, rax + 0x7ffff7f437d9 xor rbp, rbp + 0x7ffff7f437dc jmp rdx + 0x7ffff7f437de nop +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── source:elf-loader.c+197 ──── + 192 *(uint64_t *)sp = argc; + 193 + 194 void (*entry)() = base + (void (*)())ehdr->e_entry; + 195 + 196 // Transfer control +●→ 197 __asm__ __volatile__( + 198 "mov %0, %%rsp\n" + 199 "xor %%rbp, %%rbp\n" + 200 "jmp *%1\n" + 201 : + 202 : "r"(sp), "r"(entry) +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── +[#0] Id 1, Name: "elf-loader", stopped 0x7ffff7f437c8 in load_and_run (), reason: BREAKPOINT +``` + +Now we do some `ni` to step with every instruction, until we reach some point where no c code is available anymore (after the `jmp rdx`). + +```gdb + 0x401707 pop rbp + 0x401708 ret + 0x401709 nop DWORD PTR [rax+0x0] + → 0x401710 endbr64 + 0x401714 xor ebp, ebp + 0x401716 mov r9, rdx + 0x401719 pop rsi + 0x40171a mov rdx, rsp + 0x40171d and rsp, 0xfffffffffffffff0 +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── +[#0] Id 1, Name: "elf-loader", stopped 0x401710 in ?? (), reason: SINGLE STEP +``` + +We check the mapping of the memory: + +```gdb +$ vmmap +[ Legend: Code | Stack | Heap ] +Start End Offset Perm Path +0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- +0x0000000000401000 0x000000000047f000 0x0000000000000000 r-x +0x000000000047f000 0x00000000004a5000 0x0000000000000000 r-- +0x00000000004a5000 0x00000000004b2000 0x0000000000000000 rwx +0x00007ffff7600000 0x00007ffff7e00000 0x0000000000000000 rw- +0x00007ffff7e72000 0x00007ffff7f33000 0x0000000000000000 r-- .../elf-loader/assignment-elf-loader/tests/snippets/no_pie +0x00007ffff7f33000 0x00007ffff7f35000 0x0000000000000000 r-- [vvar] +0x00007ffff7f35000 0x00007ffff7f37000 0x0000000000000000 r-- [vvar_vclock] +0x00007ffff7f37000 0x00007ffff7f39000 0x0000000000000000 r-x [vdso] +0x00007ffff7f39000 0x00007ffff7f42000 0x0000000000000000 r-- .../elf-loader/assignment-elf-loader/src/elf-loader +0x00007ffff7f42000 0x00007ffff7fc9000 0x0000000000009000 r-x .../elf-loader/assignment-elf-loader/src/elf-loader +0x00007ffff7fc9000 0x00007ffff7ff2000 0x0000000000090000 r-- .../elf-loader/assignment-elf-loader/src/elf-loader +0x00007ffff7ff2000 0x00007ffff7ff7000 0x00000000000b9000 r-- .../elf-loader/assignment-elf-loader/src/elf-loader +0x00007ffff7ff7000 0x00007ffff7ff9000 0x00000000000be000 rw- .../elf-loader/assignment-elf-loader/src/elf-loader +0x00007ffff7ff9000 0x00007ffff7fff000 0x0000000000000000 rw- +0x00007ffff7fff000 0x00007ffff8021000 0x0000000000000000 rw- [heap] +0x00007ffffffdd000 0x00007ffffffff000 0x0000000000000000 rw- [stack] +0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall] +``` + +Our current instruction is at address `0x401710`, which is inside a memory region allocated by `mmap` for the `no_pie` file, we can use `add-symbol-file`. +`add-symbol-file` expects the **start address of the `.text` section**, so let's get that. + +```bash +$ readelf -S tests/snippets/no_pie + +[...] + [ 7] .text PROGBITS **0000000000401100** 00001100 + 000000000007d880 0000000000000000 AX 0 0 64 + [ 8] .fini PROGBITS 000000000047e980 0007e980 +``` + +The `.text` address is the one placed inside `**`, `0x0000000000401100`. + +So, bach to `gdb`: + +```gdb +$ add-symbol-file tests/snippets/no_pie 0x0000000000401100 +add symbol table from file "tests/snippets/no_pie" at + .text_addr = 0x401100 +Reading symbols from tests/snippets/no_pie... +$ context +... + → 0x401710 <_start+0000> endbr64 + 0x401714 <_start+0004> xor ebp, ebp + 0x401716 <_start+0006> mov r9, rdx + 0x401719 <_start+0009> pop rsi +... +``` + +Now we can see where we are in the code of `no_pie`, the `_start` function. +Let's see where it crashes: + +```gdb + 0x403e74 <__libc_start_main_impl+0144> mov rdx, QWORD PTR [rip+0xa5bcd] # 0x4a9a48 <_dl_random+139145856> + → 0x403e7b <__libc_start_main_impl+014b> mov rax, QWORD PTR [rdx] + 0x403e7e <__libc_start_main_impl+014e> xor al, al + 0x403e80 <__libc_start_main_impl+0150> mov QWORD PTR fs:0x28, rax +``` + +**Note:** If the application crashes in the `__libc_start_main_impl` function, it's most likely because of the stack (`AUXV` values), or the memory layout (make sure the mapped regions have the correct length, **be careful of the difference between `p_filesz` and `p_memsz`**). + +In our case, we can see the `rdx` register, that is dereferenced, is 0. +We can also see on the instruction above, that `rdx` is set to `_dl_random+139145856`. + +The name of `_dl_random` suggests something to do with `auxv[AT_RANDOM]`. +Also, if we look into the libc source code (you are not required to do this, you should be able to solve all the issues using gdb, but sometimes looking at the source code helps), `_dl_random` [is actually set to `auxv[AT_RANDOM]`](https://elixir.bootlin.com/glibc/glibc-2.42.9000/source/sysdeps/unix/sysv/linux/dl-parse_auxv.h#L55). +We did not set the `AT_RANDOM` value in our loader, so it's `NULL`, which is why it will crash with a `SEGV`. + +We set the `AT_RANDOM` value to a memory region pointing to random data, as the [man page](https://man7.org/linux/man-pages/man3/getauxval.3.html) says, the crash disappears, and the `no_pie` elf is loaded properly. + +## Running the Checker + +In order to check the assignment in an environment as similar to the one on Gitlab CI, you can run the checker, including linters with: + +```console +student@so:~/.../assignments/elf-loader$ ./local.sh checker +``` + +## Compilation Tips + +To start the testing, run `make check` in the `tests/` directory. +You can modify the source files in `tests/snippets` and try different things. +To run the loader manually, use `./elf-loader ../tests/snippets/ arg1 arg2 ...`. diff --git a/content/assignments/elf-loader/img/auxv-example.drawio.svg b/content/assignments/elf-loader/img/auxv-example.drawio.svg new file mode 100644 index 0000000000..5eabdac464 --- /dev/null +++ b/content/assignments/elf-loader/img/auxv-example.drawio.svg @@ -0,0 +1,4 @@ + + + +
0x000...
0xfff...
6
AT_PAGESZ
4096
0
AT_NULL
0
11
AT_UID
1000
\ No newline at end of file diff --git a/content/assignments/elf-loader/img/auxv.drawio.svg b/content/assignments/elf-loader/img/auxv.drawio.svg new file mode 100644 index 0000000000..f7d99764fb --- /dev/null +++ b/content/assignments/elf-loader/img/auxv.drawio.svg @@ -0,0 +1,4 @@ + + + +
0x000...
0xfff...
9
AT_ENTRY
0x401234
0
AT_NULL
0
\ No newline at end of file diff --git a/content/assignments/elf-loader/img/stack-layout.drawio.svg b/content/assignments/elf-loader/img/stack-layout.drawio.svg new file mode 100644 index 0000000000..37c7aeed1c --- /dev/null +++ b/content/assignments/elf-loader/img/stack-layout.drawio.svg @@ -0,0 +1,4 @@ + + + +
0x000..
0xFFF...
argc
ESP
argv[0]
ESP + 4
argv[1]
...........
0
envp[0]
...........
0
auxv entries
auvx NULL entry
\ No newline at end of file diff --git a/content/assignments/elf-loader/src/.gitignore b/content/assignments/elf-loader/src/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/content/assignments/elf-loader/src/Makefile b/content/assignments/elf-loader/src/Makefile new file mode 100644 index 0000000000..63d0b052bb --- /dev/null +++ b/content/assignments/elf-loader/src/Makefile @@ -0,0 +1,9 @@ +CFLAGS= -g -static-pie -o + +all: elf-loader + +elf-loader: elf-loader.c + gcc $(CFLAGS) $@ $< + +clean: + -rm -f *.o elf-loader diff --git a/content/assignments/elf-loader/src/elf-loader.c b/content/assignments/elf-loader/src/elf-loader.c new file mode 100644 index 0000000000..8e77d41ef0 --- /dev/null +++ b/content/assignments/elf-loader/src/elf-loader.c @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include + +void *map_elf(const char *filename) +{ + // This part helps you store the content of the ELF file inside the buffer. + struct stat st; + void *file; + int fd; + + fd = open(filename, O_RDONLY); + if (fd < 0) { + perror("open"); + exit(1); + } + + fstat(fd, &st); + + file = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (file == MAP_FAILED) { + perror("mmap"); + close(fd); + exit(1); + } + + return file; +} + +void load_and_run(const char *filename, int argc, char **argv, char **envp) +{ + // Contents of the ELF file are in the buffer: elf_contents[x] is the x-th byte of the ELF file. + void *elf_contents = map_elf(filename); + + /** + * TODO: ELF Header Validation + * Validate ELF magic bytes - "Not a valid ELF file" + exit code 3 if invalid. + * Validate ELF class is 64-bit (ELFCLASS64) - "Not a 64-bit ELF" + exit code 4 if invalid. + */ + + /** + * TODO: Load PT_LOAD segments + * For minimal syscall-only binaries. + * For each PT_LOAD segment: + * - Map the segments in memory. Permissions can be RWX for now. + */ + + /** + * TODO: Load Memory Regions with Correct Permissions + * For each PT_LOAD segment: + * - Set memory permissions according to program header p_flags (PF_R, PF_W, PF_X). + * - Use mprotect() or map with the correct permissions directly using mmap(). + */ + + /** + * TODO: Support Static Non-PIE Binaries with libc + * Must set up a valid process stack, including: + * - argc, argv, envp + * - auxv vector (with entries like AT_PHDR, AT_PHENT, AT_PHNUM, etc.) + * Note: Beware of the AT_RANDOM, AT_PHDR entries, the application will + * crash if you do not set them up properly. + */ + void *sp = NULL; + + /** + * TODO: Support Static PIE Executables + * Map PT_LOAD segments at a random load base. + * Adjust virtual addresses of segments and entry point by load_base. + * Stack setup (argc, argv, envp, auxv) same as above. + */ + + // TODO: Set the entry point and the stack pointer + void (*entry)() = NULL; + + // Transfer control + __asm__ __volatile__( + "mov %0, %%rsp\n" + "xor %%rbp, %%rbp\n" + "jmp *%1\n" + : + : "r"(sp), "r"(entry) + : "memory" + ); +} + +int main(int argc, char **argv, char **envp) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + exit(1); + } + + load_and_run(argv[1], argc - 1, &argv[1], envp); + return 0; +} diff --git a/content/assignments/elf-loader/tests/.gitignore b/content/assignments/elf-loader/tests/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/content/assignments/elf-loader/tests/Makefile b/content/assignments/elf-loader/tests/Makefile new file mode 100644 index 0000000000..401c5680e0 --- /dev/null +++ b/content/assignments/elf-loader/tests/Makefile @@ -0,0 +1,41 @@ +export SRC_PATH ?= $(realpath ../src) +export UTILS_PATH ?= $(realpath ../utils) + +CC=gcc +NASM=nasm +CFLAGS_NO_PIE=-g -static -fcf-protection=none -fno-PIC -o +CFLAGS=-g -static-pie -o +LDFLAGS_NO_PIE=-no-pie +LDFLAGS= +NASMFLAGS=-felf64 -o + +SNIPPETS_PATH=$(PWD)/snippets + +.PHONY: all src snippets clean_src clean_snippets check lint + +all: src snippets + +src: + $(MAKE) -C $(SRC_PATH) + +snippets: + $(MAKE) -C $(SNIPPETS_PATH) + +clean_snippets: + $(MAKE) -C $(SNIPPETS_PATH) clean + +clean_src: + $(MAKE) -C $(SRC_PATH) clean + +check: + $(MAKE) clean_src clean_snippets src snippets + ./run_tests.sh + +check-fast: + $(MAKE) clean_src clean_snippets src snippets + ./run_tests.sh + +lint: + -cd .. && checkpatch.pl -f src/*.c tests/snippets/*.c + -cd .. && checkpatch.pl -f checker/*.sh tests/*.sh + -cd .. && shellcheck checker/*.sh tests/*.sh diff --git a/content/assignments/elf-loader/tests/grade.sh b/content/assignments/elf-loader/tests/grade.sh new file mode 100755 index 0000000000..e71c0b2788 --- /dev/null +++ b/content/assignments/elf-loader/tests/grade.sh @@ -0,0 +1,136 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause + +# Grade style based on build warnings and linter warnings / errors. +# Points are subtracted from the maximum amount of style points (10). +# - For 15 or more build warnings, all points (10) are subtracted. +# - For [10,15) build warnings, 6 points are subtracted. +# - For [5,10) build warnings, 4 points are subtracted. +# - For [1,5) build warnings, 2 points are subtracted. +# - For 25 ore more linter warnings / errors, all points (10) are subtracted. +# - For [15,25) linter warnings / errors, 6 points are subtracted. +# - For [7,15) linter warnings / errors, 4 points are subtracted. +# - For [1,7) linter warnings / errors, 2 points are subtracted. +# Final style points are between 0 and 10. Results cannot be negative. +# +# Result (grade) is stored in style_grade.out file. +# Collect summary in style_summary.out file. + +function grade_style() +{ + compiler_warn=$(< checker.out grep -v 'unused parameter' | grep -v 'unused variable' | \ + grep -v "discards 'const'" | grep -c '[0-9]\+:[0-9]\+: warning:') + + compiler_down=0 + if test "$compiler_warn" -ge 15; then + compiler_down=10 + elif test "$compiler_warn" -ge 10; then + compiler_down=6 + elif test "$compiler_warn" -ge 5; then + compiler_down=4 + elif test "$compiler_warn" -ge 1; then + compiler_down=2 + fi + + cpplint=$(< linter.out grep "Total errors found:" | rev | cut -d ' ' -f 1 | rev) + checkpatch_err=$(< linter.out grep 'total: [0-9]* errors' | grep -o '[0-9]* errors,' | \ + cut -d ' ' -f 1 | paste -s -d '+' | bc) + checkpatch_warn=$(< linter.out grep 'total: [0-9]* errors' | grep -o '[0-9]* warnings,' | \ + cut -d ' ' -f 1 | paste -s -d '+' | bc) + if test -z "$checkpatch_err"; then + checkpatch_err=0 + fi + if test -z "$checkpatch_warn"; then + checkpatch_warn=0 + fi + checkpatch=$((checkpatch_err + checkpatch_warn)) + checker_all=$((cpplint + checkpatch)) + + checker_down=0 + if test "$checker_all" -ge 25; then + checker_down=10 + elif test "$checker_all" -ge 15; then + checker_down=6 + elif test "$checker_all" -ge 7; then + checker_down=4 + elif test "$checker_all" -ge 1; then + checker_down=2 + fi + + full_down=$((compiler_down + checker_down)) + + if test "$full_down" -gt 10; then + full_down=10 + fi + style_grade=$((10 - full_down)) + + echo "$style_grade" > style_grade.out + + { + < linter.out grep -v 'unused parameter' | grep -v 'unused variable' | grep -v "discards 'const'" | \ + grep '[0-9]\+:[0-9]\+: warning:' + < linter.out grep "Total errors found: [1-9]" + < linter.out grep 'total: [1-9]* errors' + < linter.out grep 'total: 0 errors' | grep '[1-9][0-9]* warnings' + } > style_summary.out +} + +# Print grades: total, checker and style. +# Style grade is only awarded for assignments that have past 60 points +# of th checker grade. +print_results() +{ + checker_grade=$(< checker.out sed -n '/^Checker:/s/^.*[ \t]\+\([0-9\.]\+\)\/.*$/\1/p') + if test "$(echo "$checker_grade > 60" | bc)" -eq 1; then + style_grade=$(cat style_grade.out) + else + style_grade=0 + fi + final_grade=$(echo "scale=2; $checker_grade+$style_grade" | bc) + echo -e "\n\n### GRADE\n\n" + printf "Checker: %58s/ 90\n" "$checker_grade" + printf "Style: %60s/ 10\n" "$style_grade" + printf "Total: %60s/100\n" "$final_grade" + + echo -e "\n\n### STYLE SUMMARY\n\n" + cat style_summary.out +} + +run_interactive() +{ + echo -e "\n\n### CHECKER\n\n" + stdbuf -oL make check 2>&1 | stdbuf -oL sed 's/^Total:/Checker:/g' | tee checker.out + + echo -e "\n\n### LINTER\n\n" + stdbuf -oL make lint 2>&1 | tee linter.out + + grade_style + print_results +} + +run_non_interactive() +{ + make check 2>&1 | sed 's/^Total:/Checker:/g' > checker.out + make lint > linter.out 2>&1 + + grade_style + print_results + + echo -e "\n\n### CHECKER\n\n" + cat checker.out + + echo -e "\n\n### LINTER\n\n" + cat linter.out +} + +# In case of a command line argument disable interactive output. +# That is, do not show output as it generated. +# This is useful to collect all output and present final results at the +# beginning of the script output. +# This is because Moodle limits the output results, and the final results +# would otherwise not show up. +if test $# -eq 0; then + run_interactive +else + run_non_interactive +fi diff --git a/content/assignments/elf-loader/tests/ref/envp.ref b/content/assignments/elf-loader/tests/ref/envp.ref new file mode 100644 index 0000000000..e69de29bb2 diff --git a/content/assignments/elf-loader/tests/ref/error-bad-magic.ref b/content/assignments/elf-loader/tests/ref/error-bad-magic.ref new file mode 100644 index 0000000000..e69de29bb2 diff --git a/content/assignments/elf-loader/tests/ref/error-not-64.ref b/content/assignments/elf-loader/tests/ref/error-not-64.ref new file mode 100644 index 0000000000..e69de29bb2 diff --git a/content/assignments/elf-loader/tests/ref/no_pie.ref b/content/assignments/elf-loader/tests/ref/no_pie.ref new file mode 100644 index 0000000000..e69de29bb2 diff --git a/content/assignments/elf-loader/tests/ref/no_pie_argc.ref b/content/assignments/elf-loader/tests/ref/no_pie_argc.ref new file mode 100644 index 0000000000..b8626c4cff --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/no_pie_argc.ref @@ -0,0 +1 @@ +4 diff --git a/content/assignments/elf-loader/tests/ref/no_pie_argv.ref b/content/assignments/elf-loader/tests/ref/no_pie_argv.ref new file mode 100644 index 0000000000..8ea56cd489 --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/no_pie_argv.ref @@ -0,0 +1,4 @@ +4 +1 +2 +test diff --git a/content/assignments/elf-loader/tests/ref/no_pie_auxv.ref b/content/assignments/elf-loader/tests/ref/no_pie_auxv.ref new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/no_pie_auxv.ref @@ -0,0 +1 @@ +0 diff --git a/content/assignments/elf-loader/tests/ref/no_pie_envp.ref b/content/assignments/elf-loader/tests/ref/no_pie_envp.ref new file mode 100644 index 0000000000..dce8b5218f --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/no_pie_envp.ref @@ -0,0 +1 @@ +ENV: test diff --git a/content/assignments/elf-loader/tests/ref/no_pie_hello.ref b/content/assignments/elf-loader/tests/ref/no_pie_hello.ref new file mode 100644 index 0000000000..af5626b4a1 --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/no_pie_hello.ref @@ -0,0 +1 @@ +Hello, world! diff --git a/content/assignments/elf-loader/tests/ref/nolibc.ref b/content/assignments/elf-loader/tests/ref/nolibc.ref new file mode 100644 index 0000000000..e8d9a4003e --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/nolibc.ref @@ -0,0 +1,2 @@ +Hello, world! +Hello from rodata! diff --git a/content/assignments/elf-loader/tests/ref/nolibc_no_rwx_rodata.ref b/content/assignments/elf-loader/tests/ref/nolibc_no_rwx_rodata.ref new file mode 100644 index 0000000000..cbf81240bb --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/nolibc_no_rwx_rodata.ref @@ -0,0 +1 @@ +Hello, .rodata text! diff --git a/content/assignments/elf-loader/tests/ref/nolibc_no_rwx_text.ref b/content/assignments/elf-loader/tests/ref/nolibc_no_rwx_text.ref new file mode 100644 index 0000000000..e5d64ee90b --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/nolibc_no_rwx_text.ref @@ -0,0 +1 @@ +Hello, .text test! diff --git a/content/assignments/elf-loader/tests/ref/pie.ref b/content/assignments/elf-loader/tests/ref/pie.ref new file mode 100644 index 0000000000..af5626b4a1 --- /dev/null +++ b/content/assignments/elf-loader/tests/ref/pie.ref @@ -0,0 +1 @@ +Hello, world! diff --git a/content/assignments/elf-loader/tests/run_tests.sh b/content/assignments/elf-loader/tests/run_tests.sh new file mode 100755 index 0000000000..63b789224e --- /dev/null +++ b/content/assignments/elf-loader/tests/run_tests.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause + +tests=( +"error-bad-magic|5" +"error-not-64|5" +"nolibc|20" +"nolibc_no_rwx_rodata|10" +"nolibc_no_rwx_text|10" +"no_pie_hello|5" +"no_pie_argc|5" +"no_pie_argv|5" +"no_pie_envp|5" +"no_pie_auxv|10" +"pie|10" +) + +loader="$(pwd)/../src/elf-loader" +snippets="$(pwd)/snippets/" +out="$(pwd)/out/" +ref="$(pwd)/ref/" +total=0 + +#set -o pipefail + +print_test() +{ + func="$1" + result="$2" + points="$3" + + if test "$points" -gt 999; then + points=999 + fi + + printf "%-32s " "${func:0:31}" + printf "........................" + if test "$result" -eq 0; then + printf " passed ... %3d\n" "$points" + total=$((total + points)) + else + printf " failed ... 0\n" + fi +} + +ret_expected() +{ + testname="$1" + + if [[ "$testname" =~ "no_rwx" ]]; then + return 139 + fi + + if [[ "$testname" =~ "error-bad-magic" ]]; then + return 3 + fi + + if [[ "$testname" =~ "error-not-64" ]]; then + return 4 + fi + + return 0 +} + +run_tests() +{ + if test ! -d "$out"; then + mkdir "$out" + fi + + for tst in "${tests[@]}"; do + test_name="$(echo "$tst" | cut -d'|' -f1)" + test_points="$(echo "$tst" | cut -d'|' -f2)" + + execute_test "$test_name" "$test_points" + done + + echo "" + echo -n "Total: " + echo -n " " + LC_ALL=C printf "%3d/100\n" "$total" +} + +execute_test() +{ + filename="$1" + points="$2" + outf="$filename.out" + reff="$filename.ref" + + setsid bash -c "ENV_TEST=test timeout -k 3 2 $loader $snippets/$filename 1 2 test > $out/$outf" >/dev/null 2>&1 & pid=$!; wait $pid; + + ret_code=$? + ret_expected "$filename" + ret_exp=$? + + if test "$ret_code" -ne "$ret_exp"; then + print_test "$filename" 1 "$points" + return + fi + + diff "$ref/$reff" "$out/$outf" > /dev/null + print_test "$filename" $? "$points" +} + +run_tests diff --git a/content/assignments/elf-loader/tests/snippets/Makefile b/content/assignments/elf-loader/tests/snippets/Makefile new file mode 100644 index 0000000000..a98796c127 --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/Makefile @@ -0,0 +1,46 @@ +CFLAGS_NO_PIE= -g -static -fcf-protection=none -fno-PIC -o +CFLAGS= -g -static-pie -o +LDFLAGS_NO_PIE= -no-pie +LDFLAGS= +NASMFLAGS= -felf64 -o + +all: nolibc_no_rwx_rodata nolibc_no_rwx_text nolibc no_pie_hello no_pie_argc no_pie_argv no_pie_auxv pie no_pie_envp + +nolibc_no_rwx_rodata.o: nolibc_no_rwx_rodata.asm + nasm $(NASMFLAGS) $@ $? + +nolibc_no_rwx_rodata: nolibc_no_rwx_rodata.o + ld -nostdlib -o $@ $? + +nolibc_no_rwx_text.o: nolibc_no_rwx_text.asm + nasm $(NASMFLAGS) $@ $? + +nolibc_no_rwx_text: nolibc_no_rwx_text.o + ld -nostdlib -o $@ $? + +nolibc.o: nolibc.asm + nasm $(NASMFLAGS) $@ $? + +nolibc: nolibc.o + ld -nostdlib -o $@ $? + +no_pie_hello: hello.c libc.a + gcc $(CFLAGS_NO_PIE) $@ $? $(LDFLAGS_NO_PIE) + +no_pie_argc: argc.c libc.a + gcc $(CFLAGS_NO_PIE) $@ $? $(LDFLAGS_NO_PIE) + +no_pie_argv: argv.c libc.a + gcc $(CFLAGS_NO_PIE) $@ $? $(LDFLAGS_NO_PIE) + +no_pie_auxv: auxv.c libc.a + gcc $(CFLAGS_NO_PIE) $@ $? $(LDFLAGS_NO_PIE) + +no_pie_envp: envp.c libc.a + gcc $(CFLAGS_NO_PIE) $@ $? $(LDFLAGS_NO_PIE) + +pie: hello.c libc.a + gcc $(CFLAGS) $@ $? $(LDFLAGS) + +clean: + -rm -f *.o pie no_pie_hello no_pie_argc no_pie_argv no_pie_auxv no_pie_envp nolibc_no_rwx_text nolibc_no_rwx_rodata nolibc diff --git a/content/assignments/elf-loader/tests/snippets/argc.c b/content/assignments/elf-loader/tests/snippets/argc.c new file mode 100644 index 0000000000..dba0932ce1 --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/argc.c @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + printf("%d\n", argc); + fflush(stdout); + + syscall(SYS_exit_group, 0); + + return 0; // Should never be reached +} diff --git a/content/assignments/elf-loader/tests/snippets/argv.c b/content/assignments/elf-loader/tests/snippets/argv.c new file mode 100644 index 0000000000..715f29b949 --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/argv.c @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + printf("%d\n", argc); + for (int i = 1; i < argc; i++) + printf("%s\n", argv[i]); + fflush(stdout); + + syscall(SYS_exit_group, 0); + + return 0; // Should never be reached +} diff --git a/content/assignments/elf-loader/tests/snippets/auxv.c b/content/assignments/elf-loader/tests/snippets/auxv.c new file mode 100644 index 0000000000..040e68dec5 --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/auxv.c @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + unsigned long v = getauxval(AT_NULL); + + printf("%lu\n", v); + fflush(stdout); + + syscall(SYS_exit_group, 0); + + return 0; +} diff --git a/content/assignments/elf-loader/tests/snippets/envp.c b/content/assignments/elf-loader/tests/snippets/envp.c new file mode 100644 index 0000000000..3ad68eee43 --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/envp.c @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include + +const int a = 1; + +int main(int argc, char **argv) +{ + char *env = malloc(30); + + env = getenv("ENV_TEST"); + printf("ENV: %s\n", env); + fflush(stdout); + + syscall(SYS_exit_group, 0); + + return 0; // Should never be reached +} diff --git a/content/assignments/elf-loader/tests/snippets/error-bad-magic b/content/assignments/elf-loader/tests/snippets/error-bad-magic new file mode 100755 index 0000000000..c858576730 Binary files /dev/null and b/content/assignments/elf-loader/tests/snippets/error-bad-magic differ diff --git a/content/assignments/elf-loader/tests/snippets/error-not-64 b/content/assignments/elf-loader/tests/snippets/error-not-64 new file mode 100755 index 0000000000..48b0c21f6f Binary files /dev/null and b/content/assignments/elf-loader/tests/snippets/error-not-64 differ diff --git a/content/assignments/elf-loader/tests/snippets/hello.c b/content/assignments/elf-loader/tests/snippets/hello.c new file mode 100644 index 0000000000..b4898075b4 --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/hello.c @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + printf("Hello, world!\n"); + fflush(stdout); + + syscall(SYS_exit_group, 0); + + return 0; // Should never be reached +} diff --git a/content/assignments/elf-loader/tests/snippets/nolibc.asm b/content/assignments/elf-loader/tests/snippets/nolibc.asm new file mode 100644 index 0000000000..f8fbe1ac5e --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/nolibc.asm @@ -0,0 +1,31 @@ +; SPDX-License-Identifier: BSD-3-Clause + +section .data + msg db "Hello, world!", 10 ; Message + newline + msglen equ $ - msg ; Length of the message + +section .rodata + msg_rodata db "Hello from rodata!", 10 ; Message + newline + msglen_rodata equ $ - msg_rodata ; Length of the message + +section .text + global _start + +_start: + ; write(stdout=1, msg, msglen) + mov rax, 1 ; syscall: write + mov rdi, 1 ; file descriptor: stdout + mov rsi, msg ; pointer to message + mov rdx, msglen ; message length + syscall + + mov rax, 1 + mov rdi, 1 + mov rsi, msg_rodata + mov rdx, msglen_rodata + syscall + + ; exit(0) + mov rax, 60 ; syscall: exit + xor rdi, rdi ; status = 0 + syscall diff --git a/content/assignments/elf-loader/tests/snippets/nolibc_no_rwx_rodata.asm b/content/assignments/elf-loader/tests/snippets/nolibc_no_rwx_rodata.asm new file mode 100644 index 0000000000..a5e6b39fcc --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/nolibc_no_rwx_rodata.asm @@ -0,0 +1,24 @@ +section .data + msg db "Hello, .rodata text!", 10 ; Message + newline + msglen equ $ - msg ; Length of the message + +section .rodata + ro_msg db "test" + +section .text + global _start + +_start: + ; write(stdout=1, msg, msglen) + mov rax, 1 ; syscall: write + mov rdi, 1 ; file descriptor: stdout + mov rsi, msg ; pointer to message + mov rdx, msglen ; message length + syscall + + mov BYTE [ro_msg], 0 + + ; exit(0) + mov rax, 60 ; syscall: exit + xor rdi, rdi ; status = 0 + syscall diff --git a/content/assignments/elf-loader/tests/snippets/nolibc_no_rwx_text.asm b/content/assignments/elf-loader/tests/snippets/nolibc_no_rwx_text.asm new file mode 100644 index 0000000000..ee6f9488cf --- /dev/null +++ b/content/assignments/elf-loader/tests/snippets/nolibc_no_rwx_text.asm @@ -0,0 +1,24 @@ +section .data + msg db "Hello, .text test!", 10 ; Message + newline + msglen equ $ - msg ; Length of the message + +section .rodata + ro_msg db "test" + +section .text + global _start + +_start: + ; write(stdout=1, msg, msglen) + mov rax, 1 ; syscall: write + mov rdi, 1 ; file descriptor: stdout + mov rsi, msg ; pointer to message + mov rdx, msglen ; message length + syscall + + mov BYTE [_start], 0 + + ; exit(0) + mov rax, 60 ; syscall: exit + xor rdi, rdi ; status = 0 + syscall