Add Ghost RAT protocol

This commit is contained in:
Pierre Lalet 2021-12-16 00:47:21 +01:00
parent 6da8a23ede
commit c127fec54c
4 changed files with 158 additions and 0 deletions

View file

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

57
src/proto/ghost.rs Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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<Vec<u8>> {
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)
}

View file

@ -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");
};
}
}
}

View file

@ -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("<II", data[5:13])
assert len(data) == data_len, "invalid Ghost payload: %r" % data
assert len(zlib.decompress(data[13:])) == uncompressed_len, (
"invalid Ghost payload: %r" % data
)
def test_all(iface):
global TESTS
# execute tests