**🐚coding** *Project 1 for [CS 361S](.)* *Due: Friday, February 2nd, 11:59 PM* # Goal The goal of this assignment is to gain hands-on experience with shellcoding: writing executable exploit payloads subject to constraints on the bytes that make up the payload. The classic constraint is the exclusion of NUL (0) bytes imposed by C string functions like `strcpy`, but some target programs have more stringent security checks. For example, input might be checked that it's [all printable characters](https://www.usenix.org/system/files/woot20-paper-patel_0.pdf), or [grammatical English](https://www.cs.jhu.edu/~sam/ccs243-mason.pdf), or [hella sweet ASCII art](https://ascii.skylined.nl/). In this assignment, you will be writing an *encoder* that takes an arbitrary 64-bit RISC-V payload and encodes it to a new 64-bit RISC-V executable payload that has the same effect but is not only free of NUL bytes but also valid UTF-8. Because not every RISC-V instruction can be expressed using such characters, you cannot simply translate each instruction in the input payload to an equivalent instruction in the output payload. (As we've already noted, the `ecall` system call instruction contains three NUL bytes.) Instead, you will output a two-stage payload. The job of the first stage is to write the original arbitrary-byte payload onto the stack and to jump to it. You have already read the classic article on x86 alphanumeric encoding by [rix](http://phrack.org/issues/57/15.html). (There is another by [SkyLined](https://web.archive.org/web/20150403114315/http://skypher.com/wiki/index.php?title=Www.edup.tudelft.nl%2F~bjwever%2Fwhitepaper_shellcode.html.php).) Now read the [paper on alphanumeric (plus one symbol) shellcoding for RISC-V](https://www.usenix.org/system/files/woot19-paper_barral.pdf) by Hаdrien Ваrrаl, Rémi Géraud-Stewart, Georges-Axel Jaloyan, and David Naccache. (For even more fun, check out the [paper on RISC-V emoji shellcoding](https://wootconference.org/papers/woot23-paper5.pdf) by some of the same authors.) For background on UTF-8 encoding, see the Phrack article on [UTF-8 (x86) shellcoding](http://phrack.org/issues/62/9.html) by greuff. Alphanumeric shellcoding for RISC-V is hard! Your task is considerably easier. To make it more fun, we encourage you to incorporate as many emoji as possible into your encoded payloads. The payload your encoder produces may make the following assumptions: * You may assume that `sp`, the stack pointer, points to a memory region that is readable, writeable, and executable (that is, that noexec stack is not enabled). * You may assume that the memory region from which your payload is executing is likewise readable, writeable, and executable (meaning that self-modifying code is allowed, though it should not be necessary). The payload your encoder produces may not assume anything about the contents of any other register or of any part of memory, and you may not hardcode an address either for the stack or for the location of your payload. In addition, you should not overwrite or modify the already-allocated part of the stack (i.e., any memory location with address greater than or equal to whatever is in `sp` when your shellcode starts running. Your payload should not have any 0 (NUL). It should not have any sequences disallowed by the UTF-8 specification, such as the byte `0xff`. Using UTF-8 it is possible to represent a code point that fits in _k_ bytes in _k+1_ or more bytes, but these longer representations are _not_ valid UTF-8, and you should not use them. For example, the uppercase letter A is supposed to be encoded only as the one-byte sequence `41`; its longer encodings `c1 81`, `e0 81 81`, etc. are _not_ valid UTF-8. And your payload should not encode any symbol that isn't an assigned Unicode code point. (For example, no assigned Unicode code point takes more than 4 UTF-8 bytes to encode, so you should not use any 5-byte or 6-byte UTF-8 sequences.) The `check` utility included in the assignment environment will help you ensure that your payload includes only valid UTF-8 with only assigned Unicode code points. # The environment We assume that you have set up the course virtual machine environment for CS 361S as described in the [exercise 1 writeup](ex1.html). In addition to the development tools you installed for exercise 1, we'll need a library for parsing ELF binary files. As root, run `apt install libelf-dev` (you may need to run `apt update` first). Download and unpack the project tarball in the VM using the command ``` curl -O https://www.cs.utexas.edu/~hovav/class/cs361s-s24/proj1.tar.gz ``` and unpack the tarball using the command `tar xvzf proj1.tar.gz`. Run `make` to build the utilities `util/dump` and `util/check`. ## Example payloads In the `payloads/` subdirectory, we have included four simple assembly programs: `break.s`, which when run will trigger a debugger breakpoint; `exit1.s`, which when run will exit with status code 1; `hello.s`, which when run will print a message; and `shell.s`, which when run will `execve` a shell as in the classic Aleph One shellcode. These programs are all written simply, using whatever instructions are most convenient (including assembler pseudoinstructions like `li` and `la`). They are not written to avoid NUL bytes or be valid UTF-8; that is the job of the encoders described below. The project Makefile includes rules to assemble these programs into binaries. You can type `make payloads/break.bin`, `make payloads/exit1.bin`, etc., to build the corresponding (RISC-V) binaries. To run the binaries, just type, e.g., `payloads/exit1.bin`. The payload programs are intentionally written to bypass the usual C runtime, so that the only executable code present in the binaries is the payload routine itself. (By contrast, you saw in exercise 1 how many functions are present in a typical C executable besides `main`.) The `dump` utility will extract just this raw executable payload from the ELF binary. The Makefile will arrange to do this for you if you type `make payloads/break.raw`, etc. You can use `od -t x1` or the hex viewer of your choice to examine these raw files, but without the ELF headers you can't execute them on the command line. Note that the Makefile is written using pattern-matching rules, and does not hardcode the names of the four example payloads we supply. If you write your own payload, say `payloads/foo.s`, then `make payloads/foo.bin` and `make payloads/foo.raw` should work, with no need to modify the Makefile. ## The nonul encoder In class, we described a strategy for encoding an arbitrary executable payload in a way that avoids any 0 (NUL) bytes: represent each word $X_i$ in the payload using two words $A_i$ and $B_i$, chosen such $X_i = A_i \oplus B_i$ but neither $A_i$ nor $B_i$ includes a NUL byte. Using this strategy requires carefully writing a first-stage payload (carefully written because it can't itself contain any NUL bytes) that reads each $A_i$ and $B_i$, computes their xor $X_i$ and writes $X_i$ to some location in memory that is both writable and executable and, when all words of the second stage have been written, transfers control to the second stage. In `nonul-template.s`, we have supplied you with a concrete 42-byte first stage payload that implements this strategy. This payload expects to find the encoded second stage immediately after the first-stage payload. You should study this payload carefully. It first finds where it is executing and calculates the address of the byte immediately after the payload itself, using an `auipc` instruction and some arithmetic, and stores this address in the register `s0`. This allows the same shellcode to execute regardless of where in memory it is placed; the traditional shellcoding term for a code snippet like this is "getpc" code. The payload then reads the first two words stored at `s0` and xors them together to find $n$, the number of payload words to write. This allows the same shellcode first stage to work regardless of the size of the second stage. The payload stores $n$ in `s1`. Now the payload starts the main decoding loop, which it will run $n$ times (until `s1` reaches 0). In each iteration, it loads two words from `s0`, xors them together, and pushes the result onto the stack. Note that the _last_ word of the payload must be pushed first so it ends up last in memory (the RISC-V stack grows downward). In other words, this decoder expects the encoded payload to be stored as $A_n, B_n, A_{n-1}, B_{n-1}, \ldots, A_1, B_1$. Once the decoding loop has completed, the first stage transfers control to the instruction at `sp`, which (if all has gone well) is the first instruction of the second-stage payload. How does the second-stage payload end up encoded in the format expected by the first stage? That is the job of `encode-nonul.c`, which you should likewise study carefully. This program reads a raw payload from a file (e.g., `payloads/hello.raw`), xor-encodes it into two arrays `bufA` and `bufB` along with its length, and then prints those words out in reverse order, formatted as an assembler snippet (in this case consisting of 64-bit words, preceded by the directive `.dword`). A simple sed script (`template.sed`) will arrange to substitute the output of `encode-nonul` into the first-stage template `nonul-template.s`, in place of the line ``` # SECOND STAGE SUBSTITUTED HERE ``` (If you edit the template file `nonul-template.s`, make sure you don't modify this line.) The Makefile includes pattern rules that will take care of all of this substitution for you. If you run `make nonul/hello.s`, for example, the Makefile will generate the unencoded raw payload `payloads/hello.raw` as above, then run `encode-nonul` on that raw payload and substitute the output into `nonul-template.s` and write the resulting assembly program to `nonul/hello.s`. You can assemble this into a binary by running `make nonul/hello.bin` and extract the raw payload from that binary by running `make nonul/hello.raw`. Like `payloads/hello.bin`, `nonul/hello.bin`, when run, should print a message and exit. But, unlike `payloads/hello.raw`, `nonul/hello.raw` should be free of NUL bytes, something you can check with `hexdump -C` or by running `util/check nonul/hello.raw`. When run without the `-u` flag, as here, the `check` utility looks for NUL bytes; if it finds any it will complain. Indeed, when you run `make nonul/hello.raw`, the Makefile will automatically run `check` for you. You can try to modify the encoding in `encode-nonul.c` so it does produce NUL bytes to see what happens. As before, the Makefile infrastructure uses pattern rules instead of hardcoding the names of the four example payloads we have supplied. If you create a new payload `payloads/foo.s`, then `make nonul/foo.raw` will assemble the new payload to a binary, extract the raw executable payload from that binary, encode that raw payload using `encode-nonul` and substitute the encoded output into the first-stage payload `nonul-template.s` to produce `nonul/foo.s`, then assemble that program into a binary, extract the raw payload from _that_ binary, and finally run `check` on the extracted payload to make sure it is free of NUL bytes. All in one command. [America!](https://www.newyorker.com/magazine/2009/08/03/travels-in-siberia-i#:~:text=He%20turned%20to%20us%20and%20spread%20his%20arms%20wide%2C%20indicating%20the%20beams%20brightly%20filling%20the%20room.%20%E2%80%9CAhhh%2C%E2%80%9D%20he%20said%2C%20triumphantly.%20%E2%80%9CAmerika!%E2%80%9D) ## The UTF-8 encoder Here is where you come in. The same infrastructure is in place for encoding payloads into NUL-free, valid UTF-8: There's `utf8-template.s`, the first stage payload; there's `encode-utf8.c`, the encoder for the second stage; and there's Makefile infrastructure to let you run, say, `make utf8/hello.raw` to turn an arbitrary payload program `payloads/hello.s` into a raw payload in `utf8/hello.raw` and to check this raw payload using `check -u`. When run with the `-u` flag, the `check` command will make sure that its input is free not only of NUL bytes but also of invalid UTF-8 sequences and unassigned Unicode code points. (The `check` program gets this functionality courtesy of the [utf8proc library](https://github.com/JuliaStrings/utf8proc).) There's just one problem: The `utf8-template.s` and `encode-utf8.c` files in the assignment tarball are just copies of `nonul-template.s` and `encode-nonul.c`. The first stage doesn't consist of valid UTF-8 and the second stage isn't encoded as valid UTF-8, either. # The assignment You will modify `utf8-template.s` so that, when assembled, the first-stage payload is valid UTF-8. (Note that this does _not_ mean that each instruction, in isolation, must be the UTF-8 encoding of some bytecode. Instruction boundaries and UTF-8 code point boundaries don't have to line up; it's okay, for example, for the last byte of an instruction to have most significant bits `110` and then have the first byte of the next instruction have most significant bits `10`.) You will modify the second-stage encoding in `encode-utf8.c` so that its output is also valid UTF-8 when appended to the first-stage payload. (More on this below.) You should not modify any file other than `utf8-template.s` and `encode-utf8.c`. In particular, you should not need to modify the Makefile or the `dump` or `check` utilities. While it might be possible to encode arbitrary payloads so that they are valid UTF-8 using the xor strategy described above, it will not be easy: If the most significant bit of a byte in $X_i = A_i \oplus B_i$ is set then the most significant bit of the corresponding byte in either $A_i$ or $B_i$ must also be set, and now you need to worry about how that byte interacts with the surrounding bytes. We strongly suggest that you modify the encoding strategy while keeping the $2\times$ expansion of the xor strategy. This will, of course, also require your first-stage decoder to compute a different formula to recover $X_i$ from $A_i$ and $B_i$. One strategy that we know will work, and therefore recommend: Split each byte of $X_i$ into two 4-bit chunks (aka nibbles). One nibble goes in $A_i$, the other goes in $B_i$. Place any value between 1 and 7 in the upper nibble of $A_i$ and $B_i$ bytes to guarantee that each byte is non-NUL and a 7-bit ASCII character (and therefore valid UTF-8 by itself). For example, the byte `0x42` could be split into the bytes `0x12` (for the lower nibble) and `0x14` (for the upper nibble). Reconstituting $X_i$ will require a sequence of bit operations including ors, ands, and shifts. As you work on your first-stage payload, you will need to change the instructions you use. Stay flexible about what registers to use! With 4-byte, uncompressed instructions, odd-numbered registers will tend to cause the most significant bit of a byte to be set; whereas many compressed instructions can access only the 8 registers `x8` through `x15` (aka `s0`, `s1`, and `a0` through `a5`). You may also need to add instructions that don't have a useful effect but which have the right bit representation to cancel out the UTF-8 effect of some instruction you really need. # References The RISC-V instruction set manual is refreshingly readable. The [latest draft version of the ISA manual](https://github.com/riscv/riscv-isa-manual/releases) is available on Github. You will want to spend most of your time with Chapter 28, "RV32/64G Instruction Set Listings", for its tables of encodings of 32-bit instructions and with Section 18.8, "RVC Instruction Set Listings," for its tables of encodings of 16-bit compressed instructions. You can also get some of the same information from [rv8](https://github.com/michaeljclark/rv8)'s `make map` feature, which relies on a [programmatic encoding of RISC-V instructions](https://github.com/michaeljclark/riscv-meta) that you might also wish to consult. The Unicode Consortium maintains a site with lots of helpful technical information; the [Unicode technical FAQ](https://www.unicode.org/faq/) makes a good starting point. You may also want to consult the [emoji table](https://unicode.org/emoji/charts/full-emoji-list.html). As you experiment with UTF-8 encodings, you may want to keep a Python 3 REPL open: You can, for example, use the `decode` method to check that a particular byte sequence is the valid UTF-8 representation of an assigned Unicode code point. Refer to [the Python Unicode documentation](https://docs.python.org/3/howto/unicode.html) for more. # Logistics You will submit using Gradescope. You should submit a zip file of your solution, where `encode-utf8.c` and `utf8-template.s` are at the root of the zip file directory structure. Your solution should include at least the following: * `encode-utf8.c`: This is your payload encoder. * `utf8-template.s`: This is your first-stage payload. * `README.txt`, which should explain your encoding strategy and how you structured your first-stage decoder. Please also include any feedback you have about the assignment in your README. As noted above, you should not modify any file included in the project tarball except `encode-utf8.c` and `utf8-template.s`. We will test your solution with an unmodified Makefile, an unmodified `template.sed` script, and unmodified `dump` and `check` utilities. You should not need to use any external libraries in your encoder. Please check with the course staff before using any such libraries. You must not copy code from any shellcode encoders. # Grading TBD.