Something went wrong. Try again.
Adversarial C2 Protocol Implemented in Zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183// Copyright 2026 Robby Zambito//// This file is part of zaprus.//// Zaprus 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.//// Zaprus 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// Zaprus. If not, see <https://www.gnu.org/licenses/>.
//! A client is used to handle interactions with the network.
const base64_enc = std.base64.standard.Encoder;const base64_dec = std.base64.standard.Decoder;
const Client = @This();
const max_message_size = 2048;
pub const max_payload_len = RawSocket.max_payload_len;
socket: RawSocket,
pub fn init() !Client { const socket: RawSocket = try .init(); return .{ .socket = socket, };}
pub fn deinit(self: *Client) void { self.socket.deinit(); self.* = undefined;}
/// Sends a fire and forget message over the network./// This function asserts that `payload` fits within a single packet.pub fn sendRelay(self: *Client, io: Io, payload: []const u8, dest: [4]u8) !void { const io_source: std.Random.IoSource = .{ .io = io }; const rand = io_source.interface();
var headers: EthIpUdp = .{ .src_mac = .fromBytes(self.socket.mac), .ip = .{ .id = rand.int(u16), .src_addr = .fromBytes(.{ 0, 0, 0, 0 }), //rand.int(u32), .dst_addr = .fromBytes(.{ 255, 255, 255, 255 }), .len = undefined, }, .udp = .{ .src_port = rand.intRangeAtMost(u16, 1025, std.math.maxInt(u16)), .dst_port = 8888, .len = undefined, }, };
const relay: SaprusMessage = .{ .relay = .{ .dest = .fromBytes(&dest), .payload = payload, }, };
var relay_buf: [max_message_size - (@bitSizeOf(EthIpUdp) / 8)]u8 = undefined; const relay_bytes = relay.toBytes(&relay_buf); headers.setPayloadLen(relay_bytes.len);
var msg_buf: [max_message_size]u8 = undefined; var msg_w: Writer = .fixed(&msg_buf); msg_w.writeAll(&headers.toBytes()) catch unreachable; msg_w.writeAll(relay_bytes) catch unreachable; const full_msg = msg_w.buffered();
try self.socket.send(full_msg);}
/// Attempts to establish a new connection with the sentinel.pub fn connect(self: Client, io: Io, payload: []const u8) (error{ BpfAttachFailed, Timeout } || SaprusMessage.ParseError)!SaprusConnection { const io_source: std.Random.IoSource = .{ .io = io }; const rand = io_source.interface();
var headers: EthIpUdp = .{ .src_mac = .fromBytes(self.socket.mac), .ip = .{ .id = rand.int(u16), .src_addr = .fromBytes(.{ 0, 0, 0, 0 }), //rand.int(u32), .dst_addr = .fromBytes(.{ 255, 255, 255, 255 }), .len = undefined, }, .udp = .{ .src_port = rand.intRangeAtMost(u16, 1025, std.math.maxInt(u16)), .dst_port = 8888, .len = undefined, }, };
// udp dest port should not be 8888 after first const udp_dest_port = rand.intRangeAtMost(u16, 9000, std.math.maxInt(u16)); var connection: SaprusMessage = .{ .connection = .{ .src = rand.intRangeAtMost(u16, 1025, std.math.maxInt(u16)), .dest = rand.intRangeAtMost(u16, 1025, std.math.maxInt(u16)), // Ignored, but good noise .seq = undefined, .id = undefined, .payload = payload, }, };
log.debug("Setting bpf filter to port {}", .{connection.connection.src}); self.socket.attachSaprusPortFilter(null, connection.connection.src) catch |err| { log.err("Failed to set port filter: {t}", .{err}); return err; }; log.debug("bpf set", .{});
var connection_buf: [2048]u8 = undefined; var connection_bytes = connection.toBytes(&connection_buf); headers.setPayloadLen(connection_bytes.len);
log.debug("Building full message", .{}); var msg_buf: [2048]u8 = undefined; var msg_w: Writer = .fixed(&msg_buf); msg_w.writeAll(&headers.toBytes()) catch unreachable; msg_w.writeAll(connection_bytes) catch unreachable; var full_msg = msg_w.buffered(); log.debug("Built full message. Sending message", .{});
try self.socket.send(full_msg); var res_buf: [4096]u8 = undefined;
log.debug("Awaiting handshake response", .{}); // Ignore response from sentinel, just accept that we got one. const full_handshake_res = try self.socket.receive(&res_buf); const handshake_res = saprusParse(full_handshake_res[42..]) catch |err| { log.err("Parse error: {t}", .{err}); return err; }; self.socket.attachSaprusPortFilter(handshake_res.connection.src, handshake_res.connection.dest) catch |err| { log.err("Failed to set port filter: {t}", .{err}); return err; }; connection.connection.dest = handshake_res.connection.src; connection_bytes = connection.toBytes(&connection_buf);
headers.udp.dst_port = udp_dest_port; headers.ip.id = rand.int(u16); headers.setPayloadLen(connection_bytes.len);
log.debug("Building final handshake message", .{});
msg_w.end = 0;
msg_w.writeAll(&headers.toBytes()) catch unreachable; msg_w.writeAll(connection_bytes) catch unreachable; full_msg = msg_w.buffered();
try self.socket.send(full_msg);
return .{ .socket = self.socket, .headers = headers, .connection = connection, };}
const RawSocket = @import("./RawSocket.zig");
const SaprusMessage = @import("message.zig").Message;const saprusParse = SaprusMessage.parse;const SaprusConnection = @import("Connection.zig");const EthIpUdp = @import("./EthIpUdp.zig").EthIpUdp;
const std = @import("std");const Io = std.Io;const Writer = std.Io.Writer;const log = std.log;