mirror of
https://github.com/ivre/masscanned.git
synced 2025-10-02 06:38:21 +00:00
Add Ghost RAT protocol
This commit is contained in:
parent
6da8a23ede
commit
c127fec54c
4 changed files with 158 additions and 0 deletions
|
@ -37,6 +37,7 @@ lazy_static = "1.4.0"
|
||||||
siphasher = "0.3"
|
siphasher = "0.3"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
byteorder = "1.4.3"
|
byteorder = "1.4.3"
|
||||||
|
flate2 = "1.0"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "masscanned"
|
name = "masscanned"
|
||||||
|
|
57
src/proto/ghost.rs
Normal file
57
src/proto/ghost.rs
Normal 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)
|
||||||
|
}
|
|
@ -33,9 +33,13 @@ use stun::{STUN_PATTERN_CHANGE_REQUEST, STUN_PATTERN_EMPTY, STUN_PATTERN_MAGIC};
|
||||||
mod ssh;
|
mod ssh;
|
||||||
use ssh::SSH_PATTERN_CLIENT_PROTOCOL;
|
use ssh::SSH_PATTERN_CLIENT_PROTOCOL;
|
||||||
|
|
||||||
|
mod ghost;
|
||||||
|
use ghost::GHOST_PATTERN_SIGNATURE;
|
||||||
|
|
||||||
const PROTO_HTTP: usize = 1;
|
const PROTO_HTTP: usize = 1;
|
||||||
const PROTO_STUN: usize = 2;
|
const PROTO_STUN: usize = 2;
|
||||||
const PROTO_SSH: usize = 3;
|
const PROTO_SSH: usize = 3;
|
||||||
|
const PROTO_GHOST: usize = 4;
|
||||||
|
|
||||||
struct TCPControlBlock {
|
struct TCPControlBlock {
|
||||||
proto_state: usize,
|
proto_state: usize,
|
||||||
|
@ -76,6 +80,11 @@ fn proto_init() -> Smack {
|
||||||
PROTO_SSH,
|
PROTO_SSH,
|
||||||
SmackFlags::ANCHOR_BEGIN,
|
SmackFlags::ANCHOR_BEGIN,
|
||||||
);
|
);
|
||||||
|
smack.add_pattern(
|
||||||
|
GHOST_PATTERN_SIGNATURE,
|
||||||
|
PROTO_GHOST,
|
||||||
|
SmackFlags::ANCHOR_BEGIN,
|
||||||
|
);
|
||||||
smack.compile();
|
smack.compile();
|
||||||
smack
|
smack
|
||||||
}
|
}
|
||||||
|
@ -122,6 +131,7 @@ pub fn repl<'a>(
|
||||||
PROTO_HTTP => http::repl(data, masscanned, client_info),
|
PROTO_HTTP => http::repl(data, masscanned, client_info),
|
||||||
PROTO_STUN => stun::repl(data, masscanned, &mut client_info),
|
PROTO_STUN => stun::repl(data, masscanned, &mut client_info),
|
||||||
PROTO_SSH => ssh::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);
|
debug!("id: {}", id);
|
||||||
None
|
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");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
import logging
|
import logging
|
||||||
from socket import AF_INET6
|
from socket import AF_INET6
|
||||||
import struct
|
import struct
|
||||||
|
import zlib
|
||||||
|
|
||||||
from scapy.compat import raw
|
from scapy.compat import raw
|
||||||
from scapy.data import ETHER_BROADCAST
|
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):
|
def test_all(iface):
|
||||||
global TESTS
|
global TESTS
|
||||||
# execute tests
|
# execute tests
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue