Last weekend I played DEF CON CTF Quals 2020 with team A*0*E, having so much fun with my teammates and I successfully solved a shellcode challenge called nooopsled. At last 7 teams solved this challenge and you can download the files from OOO’s github repo.
This is a challenge in the format of golf 🏌️♂️, you can see the description of this new type of challenge on OOO’s official webpage. For short, we are required to input a shellcode in the length of 1024 bytes, in the architecture of RISC-V64 or arm64. The server will receive our shellcode and start to execute it from the index of 0, 1, 2 …..1024, and record the number of success attempt to read out the flag
file. The error threshold start from 1, and increase 1 every 84 seconds. Every team have 8 hours for preparing their shellcode.
I did not have any experience of writing arm64 or RISC-V shellcode. After short discussion, I chose RISC-V and one of my teammate will look at arm64.
Toolchain and examples
The first step is preparing the RISC-V compiling toolchain, I downloaded the precompiled toolchain from https://github.com/sifive/freedom-tools/releases. And quick search for a couple of RISC-V64 Linux Shellcoding exmple from: https://github.com/HosakaCorp/riscv-business/.
The provided shellcode example can be complied with the command:
1 | CC=../risc64/bin/riscv64-unknown-elf-gcc LD=../risc64/bin/riscv64-unknown-elf-ld make asm |
And I wrote a simple script for extracting shellcode from the binary.
1 | import subprocess |
Shellcoding
An intuitive idea is preparing a shellcode as short as possible and put it at the end of the input, then we fill nop
instructions from the start of the input. We can start from asm2.s
and write the read flag shellcode. For the syscall number, we can refer to https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
After reading a bit of RISC-V manul, I found there is a RVC extenstion instructions set, which will “compressed” some instructions into 2bytes. The prebuilt compiler also support generating these instructions, we can add -march=rv64gc
options for the gcc command. We started from the general instructions which is 4bytes each, and try to replace some of them with equivlent “compressed” instructions.
1 | .section .text |
The will compile into a shellcode of 44 bytes, it looks like this in objdump:
1 | 10078: 676175b7 lui a1,0x67617 |
As for the preamble, we chose a “nop-liked” compressed instructions with 2 identical bytes. 0x0505
is our choice for it stands for addi a0,a0, 1
,we will set a0
in the shellcode anyway, so this instruction can be used as a nop
.
We threw this simple input to the server and it complained ~550 errors. This is because RISC-V consider each instruction as 2 byte or 4 byte, when the starting index is odd number, a single 05
preamble will concatenate with the shellcode and corrupt the shellcode like:
1 | 10078: b705 j ff98 <_start-0xe0> |
Thoughts and solution
My first thought to fix this situation is designing some preamble which can skip some odd number of bytes if it starts from a wrong place, but later I realize it is really hard to design such preamble, as such logic will bring more chaos to the construction.
The first solution for this challenge appears at around 2 hour after the thershold increasing. The Thershold fix to 85 errors. While what we have will lead to 500+ errors now, it needs a strong optimization. Then I realize 85 errors means the actual shellcode length in their solution is no more than 85 bytes, which is aroud twice of 44. This discovery help me got the right direction, the first team maybe using two pieces of shellcode at the end of the input.
The preamble works like:
- If it starts from an odd number index, all preamble work as
nop
instructions and the execution sled into the first shellcode. - If it starts from an even number index, at the end of preamble work as a jmp-like instruction, skip the first corrupted shellocde and jump into the second piece of shellcode.
We can enumerate all instructions in the size of 2 bytes, and look for some jmp-like instructions with a suitable offset like ~40. I intend to use capstone
to finish the diassmbling but it seems like the tool does not support compressed instructions. so I found anotehr disassbler tool from https://github.com/michaeljclark/riscv-disassembler, and some simple modifications can help us get all compressed instructions.
1 |
|
Extraing all jmp-like instruction from the manual, I discover a excellent instruction for this:
1 | ea05 bnez a2,48 |
With this instruction as a pivot, we can still use 0505
as the nop instruction, if it start from the odd index and the remaining 05
will combine with ea
and jmp to the second shellcode!
So the result looks like:
1 | ## odd number of 05 preamble case: |
This shellcode help us get only 84 errors on the server, so we pass the test and get the flag. I think there might be some other fancy techniques to shink the shellcode, for example overlapping two pieces of shellcode together, but clearly the first solved team don’t have so much time to do crazy optimization.
Reference
- https://github.com/o-o-overflow/dc2020q-dc2020q-nooopsled-public
- https://oooverflow.io/dc-ctf-2020-quals/
- https://github.com/sifive/freedom-tools/releases
- https://github.com/HosakaCorp/riscv-business/
- https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
- https://github.com/michaeljclark/riscv-disassembler