tl;dr
LOAD
andS_TYPE
opcodes lead to OOB when addr >DRAM_BASE+DRAM_SIZE
- Get libc and stack pointers and offset to obtain RIP offset and base
- Write ropchain on stack using libc gadgets
- Perform ORW on flag file
Challenge Points: 462
No. of solves: 29
Solved by: k1R4
Challenge Description
1 | To all my CS2100 Computer Organisation students, I hope you've enjoyed the lectures thus far on RISC-V assembly. |
Handout has the challenge binary, libc, server.py and Dockerfile
Initial Analysis
The binary is not stripped and has most mitigations turned on, typical for a binary compiled without explicit GCC flags.
Here is the checksec output:
1 | [k1r4@zg15 chal]$ checksec main |
This challenge seems to be a VM which implements the RISC-V architecture. There is a github repo provided from which the challenge seems to be based on. The next obvious step is to look at the src for bugs.
Source Code
The source code can be found here
Before this challenge, I had never tried and RISC based challenges. I went ahead with solving this challenge, without understanding the architecture. I solved it by reversing the instruction opcodes. In hindsight it would’ve been much easier if I went through the register and instruction structure first. Wikipedia has a decent explanation of the architecture design, which you can find here.
Moving on to the src, the src/cpu.c
file contains majority of the code that drives the VM. However there doesn’t appear to be any useful bugs on the surface. Since it is a VM challenge, the bug is probably OOB. In that case, the first instructions to look at are ones which involve memory derefences. The LOAD
and S_TYPE
opcodes seem to have the most potential in that case. Here is the implementation of the LD
instruction:
1 | void exec_LD(CPU* cpu, uint32_t inst) { |
Seems like memory is accessed through addresses which are passed to cpu_load()
which calls bus_load()
which again calls dram_load()
.
Bug
dram_load()
calls dram_load_x()
where x
is the number of bits. In the case of LD
, dram_load_64()
is called. It is implemented as follows:
1 | uint64_t dram_load_64(DRAM* dram, uint64_t addr){ |
The following code is from include/dram.h
:
1 |
|
In the end dram->mem
array is accessed, which is part of the CPU
struct located on the stack. Since addr
can be controlled, giving an addr
larger than DRAM_BASE+DRAM_SIZE
will lead to OOB on the stack.
Exploit Strategy
The LOAD
and S_TYPE
opcodes can be used to achieve OOB read and write respectively. Stack and libc pointers that are down the stack, can be copied and performed arithmetic on to obtain address of saved RIP and libc base. The LUI
instruction can be used to move immutables to upper 20 bytes of registers and ADDIW
can be used to add immutables to the lower 12 bits of registers. ADD
can be used to offset from libc base to get gadgets. This seems pretty straightforward but I ran into some trouble. The ADD
or LUI
instructions were causing the value to be off by 0x1000 sometimes, so I had to manually increase the offset in those cases.
Finally a ropchain is written at saved RIP of main, using SW
and SD
instructions. execve
isn’t feasible here since server.py is what we interact with and not the binary directly. So open,read,write is used instead.
Conclusion
I learnt a lot about the RISC-V architecture from this challenge and had a lot of fun solving this.
You can find the full exploit here
Flag: HackTM{Now_get_an_A_for_the_class!}