diff --git a/Cargo.toml b/Cargo.toml index 789f87f..211a287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ lazy_static = "1.4.0" siphasher = "0.3" chrono = "0.4.19" byteorder = "1.4.3" +flate2 = "1.0" [[bin]] name = "masscanned" diff --git a/src/proto/ghost.rs b/src/proto/ghost.rs new file mode 100644 index 0000000..98ca4dd --- /dev/null +++ b/src/proto/ghost.rs @@ -0,0 +1,57 @@ +// This file is part of masscanned. +// Copyright 2021 - The IVRE project +// +// Masscanned is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Masscanned is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Masscanned. If not, see . + +use log::*; +use std::io::Write; + +use flate2::write::ZlibEncoder; +use flate2::Compression; + +use crate::client::ClientInfo; +use crate::Masscanned; + +pub const GHOST_PATTERN_SIGNATURE: &[u8; 5] = b"Gh0st"; + +pub fn repl<'a>( + _data: &'a [u8], + _masscanned: &Masscanned, + _client_info: &mut ClientInfo, +) -> Option> { + debug!("receiving Gh0st data, sending one null byte payload"); + // Packet structure: + // GHOST_PATTERN_SIGNATURE + [ packet size ] + [ uncompressed payload size ] + payload + let mut result = GHOST_PATTERN_SIGNATURE.to_vec(); + let uncompressed_data = b"\x00"; + let mut compressed_data = ZlibEncoder::new(Vec::new(), Compression::default()); + compressed_data + .write_all(uncompressed_data) + .expect("Ghost: cannot decompress payload"); + let mut compressed_data = compressed_data + .finish() + .expect("Ghost: cannot decompress payload"); + let mut packet_len = compressed_data.len() + GHOST_PATTERN_SIGNATURE.len() + 4 * 2; + for _ in 0..4 { + result.push((packet_len % 256) as u8); + packet_len /= 256; + } + let mut uncompressed_len = uncompressed_data.len(); + for _ in 0..4 { + result.push((uncompressed_len % 256) as u8); + uncompressed_len /= 256; + } + result.append(&mut compressed_data); + Some(result) +} diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 2468bb7..78b65e1 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -33,9 +33,13 @@ use stun::{STUN_PATTERN_CHANGE_REQUEST, STUN_PATTERN_EMPTY, STUN_PATTERN_MAGIC}; mod ssh; use ssh::SSH_PATTERN_CLIENT_PROTOCOL; +mod ghost; +use ghost::GHOST_PATTERN_SIGNATURE; + const PROTO_HTTP: usize = 1; const PROTO_STUN: usize = 2; const PROTO_SSH: usize = 3; +const PROTO_GHOST: usize = 4; struct TCPControlBlock { proto_state: usize, @@ -76,6 +80,11 @@ fn proto_init() -> Smack { PROTO_SSH, SmackFlags::ANCHOR_BEGIN, ); + smack.add_pattern( + GHOST_PATTERN_SIGNATURE, + PROTO_GHOST, + SmackFlags::ANCHOR_BEGIN, + ); smack.compile(); smack } @@ -122,6 +131,7 @@ pub fn repl<'a>( PROTO_HTTP => http::repl(data, masscanned, client_info), PROTO_STUN => stun::repl(data, masscanned, &mut client_info), PROTO_SSH => ssh::repl(data, masscanned, &mut client_info), + PROTO_GHOST => ghost::repl(data, masscanned, &mut client_info), _ => { debug!("id: {}", id); None @@ -235,4 +245,34 @@ mod tests { }; } } + + #[test] + fn test_proto_dispatch_ghost() { + let mut client_info = ClientInfo::new(); + let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0); + client_info.ip.src = Some(IpAddr::V4(test_ip_addr)); + client_info.port.src = Some(65000); + let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3); + let mut ips = HashSet::new(); + ips.insert(IpAddr::V4(masscanned_ip_addr)); + /* Construct masscanned context object */ + let masscanned = Masscanned { + synack_key: [0, 0], + mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"), + iface: None, + ip_addresses: Some(&ips), + }; + /***** TEST GHOST *****/ + let payloads = [ + b"Gh0st\xad\x00\x00\x00\xe0\x00\x00\x00x\x9cKS``\x98\xc3\xc0\xc0\xc0\x06\xc4\x8c@\xbcQ\x96\x81\x81\tH\x07\xa7\x16\x95e&\xa7*\x04$&g+\x182\x94\xf6\xb000\xac\xa8rc\x00\x01\x11\xa0\x82\x1f\\`&\x83\xc7K7\x86\x19\xe5n\x0c9\x95n\x0c;\x84\x0f3\xac\xe8sch\xa8^\xcf4'J\x97\xa9\x82\xe30\xc3\x91h]&\x90\xf8\xce\x97S\xcbA4L?2=\xe1\xc4\x92\x86\x0b@\xf5`\x0cT\x1f\xae\xaf]\nr\x0b\x03#\xa3\xdc\x02~\x06\x86\x03+\x18m\xc2=\xfdtC,C\xfdL<<==\\\x9d\x19\x88\x00\xe5 \x02\x00T\xf5+\\" + ]; + for payload in payloads.iter() { + let _ghost_resp = + if let Some(r) = repl(&payload.to_vec(), &masscanned, &mut client_info) { + r + } else { + panic!("expected an answer, got nothing"); + }; + } + } } diff --git a/test/src/all.py b/test/src/all.py index 761599e..9dece7c 100644 --- a/test/src/all.py +++ b/test/src/all.py @@ -17,6 +17,7 @@ import logging from socket import AF_INET6 import struct +import zlib from scapy.compat import raw from scapy.data import ETHER_BROADCAST @@ -1105,6 +1106,65 @@ def test_ipv6_udp_ssh(iface): ) +@test +def test_ipv4_tcp_ghost(iface): + sport = 37184 + dports = [22, 23874] + for dport in dports: + seq_init = int(RandInt()) + banner = b"Gh0st\xad\x00\x00\x00\xe0\x00\x00\x00x\x9cKS``\x98\xc3\xc0\xc0\xc0\x06\xc4\x8c@\xbcQ\x96\x81\x81\tH\x07\xa7\x16\x95e&\xa7*\x04$&g+\x182\x94\xf6\xb000\xac\xa8rc\x00\x01\x11\xa0\x82\x1f\\`&\x83\xc7K7\x86\x19\xe5n\x0c9\x95n\x0c;\x84\x0f3\xac\xe8sch\xa8^\xcf4'J\x97\xa9\x82\xe30\xc3\x91h]&\x90\xf8\xce\x97S\xcbA4L?2=\xe1\xc4\x92\x86\x0b@\xf5`\x0cT\x1f\xae\xaf]\nr\x0b\x03#\xa3\xdc\x02~\x06\x86\x03+\x18m\xc2=\xfdtC,C\xfdL<<==\\\x9d\x19\x88\x00\xe5 \x02\x00T\xf5+\\" + syn = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP(flags="S", sport=sport, dport=dport, seq=seq_init) + ) + syn_ack = iface.sr1(syn, timeout=1) + assert syn_ack is not None, "expecting answer, got nothing" + check_ip_checksum(syn_ack) + assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary() + syn_ack = syn_ack[TCP] + assert syn_ack.flags == "SA" + ack = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="A", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + ) + _ = iface.sr1(ack, timeout=1) + req = ( + Ether(dst=MAC_ADDR) + / IP(dst=IPV4_ADDR) + / TCP( + flags="PA", + sport=sport, + dport=dport, + seq=seq_init + 1, + ack=syn_ack.seq + 1, + ) + / Raw(banner) + ) + resp = iface.sr1(req, timeout=1) + assert resp is not None, "expecting answer, got nothing" + check_ip_checksum(resp) + assert TCP in resp, "expecting TCP, got %r" % resp.summary() + tcp = resp[TCP] + assert "A" in tcp.flags, "expecting ACK flag, not set (%r)" % tcp.flags + assert "P" in tcp.flags, "expecting PSH flag, not set (%r)" % tcp.flags + data = raw(tcp.payload) + assert data, "expecting payload, got none" + assert data.startswith(b"Gh0st"), "unexpected banner: %r" % tcp.payload.load + data_len, uncompressed_len = struct.unpack("