🧐 First, what is RISC-V?

RISC-V is an open standard instruction set architecture (ISA) that began as a project at UC-Berkeley in 2010. and is based on established reduced instruction set computer (RISC) principles. Unlike most other ISA designs, RISC-V is provided under open source licenses that do not require fees to use.

The applications of this new architecture are multiple, here is a non-exhaustive list of examples:

The increasing presence of this architecture on highly critical installations led me to question the security aspect.

Although I will soon invest in a development board to facilitate my research, when I wanted to start tinkering with this architecture I did not have one at my disposal. So I had to set up a development environment on my non-RISC-V machine, and this is the topic of this first publication about RISC-V systems.

I’d like to show you the setup I’ve built to ease the development of programs in assembly on a RISC-V architecture when you don’t have a RISC-V machine at home.

  1. How I will structure my setup.
  2. Download a Debian RISC-V 64 bits image
  3. Using QEMU to emulate a RISC-V machine
  4. Creating a shared directory between host and guest machine
  5. Installing debugging and compilation tools on my host machine
    1. GDB
    2. GCC
    3. Radare2
  6. Demonstration
  7. Conclusion
  8. To go further

βš™οΈ How I will structure my setup

I started by mapping out what I was going to need:

  • A Debian 64-bit RISC-V image since it is a linux distribution I know quite well. I could also have decided to work with Fedora since there is a RISC-V version of it.
  • A folder where I will store all my useful scripts to perform redundant actions (assembly code compilation, setup launch…)
  • A shared folder between the host machine, and the guest machine.
  • A folder where I store the assembly programs I write.

Here is how I built the directory tree of my project:

./RISC-V_Setup
β”œβ”€β”€ image
β”‚   └── Debian image
β”œβ”€β”€ scripts
β”‚   └── All the useful scripts (run.sh, compile.sh...)
β”œβ”€β”€ share
β”‚   └── Share directory between HOST (my computer) and GUEST machine (RISC-V emulator)
β”œβ”€β”€ src
β”‚   └── Directory where I write RISC-V assembly code

πŸ•΅οΈ Download a Debian RISC-V 64 bits image

I will create an image directory and download a pre-made RISC-V 64 Debian Image.

mkdir image
wget https://gitlab.com/api/v4/projects/giomasce%2Fdqib/jobs/artifacts/master/download?job=convert_riscv64-virt" -O ./image/debian-rv64.zip

We now only have to unzip it in the ./image folder.

The default credentials are debian:debian and root:root

🏭 Using QEMU to emulate a RISC-V machine

A small point of vocabulary to avoid confusion: hereafter we will call my main machine the host machine and the RISC-V machine emulated with QEMU the guest machine.

-> We emulate a 64 bits version of the RISC-V processor using QEMU. In order to do this we will need the package qemu-system-riscv64 which you can install with sudo apt-get install qemu-system-riscv64

For the purpose of this demonstration, we will use a 64-bit version of the RISC-V architecture, but it is however possible to emulate a 32-bit RISC-V processor with QEMU using the qemu-system-riscv32 package

qemu-system-riscv64 \
    -machine virt \
    -cpu rv64 \
    -m 1G \
    -device virtio-net-device,netdev=net \
    -netdev user,id=net,hostfwd=tcp::2222-:22 \
    -device virtio-blk-device,drive=hd \
    -drive file=./Image/artifacts/overlay.qcow2,if=none,id=hd \
    -bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
    -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
    -append "root=LABEL=rootfs console=ttyS0" \
    -nographic \
    -fsdev local,security_model=passthrough,id=fsdev0,path=./share \
    -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare

I think you may need some further explanation of this script:

-cpu rv64

-> We select a RISC-v 64 bits CPU

-m 1G

-> We allocate 1GB of RAM to the guest machine.

This value depends on your needs and the amount of RAM you are able to allocate to the guest machine.

-netdev user,id=net,hostfwd=tcp::2222-:22:

-> This line makes port 22 accessible as localhost:2222. This lets us forward SSH connections.

-bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \

-> If needed, replace with the location of your OpenBSI. But make sure it’s the same configuration.

-append "root=LABEL=rootfs console=ttyS0" \

-> The append line adds extra options to the kernel command line in UNIX derivatives.

-kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \

-> the path of my U-Boot image.

-nographic

-> With this option, you can totally disable graphical output so that QEMU is a simple command line application.

 -fsdev local,security_model=passthrough,id=fsdev0,path=./share \
 -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare

-> These two lines allow us to create a common folder between the host machine and the guest machine.

πŸ“‚ Creating a shared directory between Host and Guest machines

I have seen some people choosing to use scp to communicate between their host machine and their guest machine. I propose a different method by using a shared folder between the two machines.

flowchart TB subgraph RISC-V Setup Host-Machine--> |./share| Share-directory Guest-Machine--> |/mnt/share|Share-directory end

-> We run this script on the guest machine

SHARED_FOLDER="/mnt/share"

#  Create shared folder
mkdir ${SHARED_FOLDER}
mount -t 9p -o trans=virtio,version=9p2000.L hostshare ${SHARED_FOLDER}

Now the contents of ./share on the host machine and /mnt/share on the guest machine will be the same

βš™οΈ Installing debugging and compilation tools

for reverse engineering purposes, I sometimes need a debugger to analyze the behavior of a binary. I am familiar with GDB and Radare2, and I will show you how to use them in this case.

We will start by installing RISC-V GNU toolchain as it contains a compiler (GCC) and our favorite debugger (GDB), as well as other very useful tools, such as an assembler and a linker. Installation instructions can be found here

GDB

We can now debug a RISC-V binary with the command :

$ ~ riscv64-unknown-elf-gdb binary
GCC

If you want to compile a binary with gcc for the RISC-V architecture, here is the command to use

$ ~ riscv64-unknown-elf-gcc -ggdb -static -o binary binary.c
Radare2

Radare2 is pretty cool when it comes to working with RISC-V binaries since it has built-in RISC-V support.

You can analyze a binary simply by running it as usual:

$ ~ r2 ./riscv-binary

✨ Demonstration

-> Here is a quick reminder of how my setup is organized. :

./RISC-V_Setup
β”œβ”€β”€ share
β”‚   └── Shared directory between HOST (my computer) and GUEST machine (RISC-V emulator)
β”œβ”€β”€ src
β”‚   └── Directory where I write RISC-V assembly code
β”œβ”€β”€ ...
β”‚   └── ...

So we will write our assembly code in the ./src folder, and compile it into the ./share folder to be able to access it from the guest machine. To do this we will first open a file program.s which will be a basic RISC-V assembler program that displays a “Hello world” message on the standard output (We must start somewhere :D ) :

#
# Risc-V Assembler program to print "Hello World!"
# to stdout.
#
# a0-a2 - parameters to linux function services
# a7 - linux function number
#

.global _start      # Provide program starting address to linker

# Setup the parameters to print hello world
# and then call Linux to do it.

_start: addi  a0, x0, 1      # 1 = StdOut
        la    a1, helloworld # load address of helloworld
        addi  a2, x0, 13     # length of our string
        addi  a7, x0, 64     # linux write system call
        ecall                # Call linux to output the string

# Setup the parameters to exit the program
# and then call Linux to do it.

        addi    a0, x0, 0   # Use 0 return code
        addi    a7, x0, 93  # Service command code 93 terminates
        ecall               # Call linux to terminate the program

.data
helloworld:      .ascii "Hello World!\n"

Next we will have to compile this code for a 64-bit RISC-V architecture from our host machine. For this we will use several tools contained in the RISC-V GNU Compiler Toolchain.

  • We use riscv64-linux-gnu-as to assemble the program.
  • riscv64-linux-gnu-ld to link the object file into an executable file.

I wrote a bash script (stored in ./script) to automatically do the job:

#!/bin/bash

ASSEMBLY_DIR="$(dirname $0)/../src"
SHARE_DIR="$(dirname $0)/../share"

riscv64-linux-gnu-as -march=rv64imac -o ${SHARE_DIR}/program.o ${ASSEMBLY_DIR}/program.s
riscv64-linux-gnu-ld -o ${SHARE_DIR}/program ${SHARE_DIR}/program.o
rm ${SHARE_DIR}/program.o
chmod +x ${SHARE_DIR}/program

We launch it :

$ ~ ./scripts/compile.sh

We now have an executable binary named program in /mnt/share on the guest machine.

-> We can test if it works :

debian@debian:/mnt/share$ ./program
Hello World!

and it’s working!

We can now debug the binary on the host machine with riscv64-unknown-elf-gdb or with Radare2.

πŸŽ‰ Conclusion

So here is a setup I made to be able to work more easily under a RISC-V architecture in a linux environment. My goal was to facilitate the task of those who would like to develop in RISC-V assembler without being able to get a RISC-V machine, or to do reverse engineering. To do so, I published my complete setup with installation instructions here.

If you have any question or corrections to suggest for this article, I can be reached by mail at the following address: r0g3r5@protonmail.Com

You can also follow me on Twitter (even if I am very (very) little active) : @Rog3rSm1th

I hope I helped you, or taught you something :D

πŸ‘‰ To go further