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("