Adversarial C2 Protocol Implemented in Zig
1// Copyright 2026 Robby Zambito
2//
3// This file is part of zaprus.
4//
5// Zaprus is free software: you can redistribute it and/or modify it under the
6// terms of the GNU General Public License as published by the Free Software
7// Foundation, either version 3 of the License, or (at your option) any later
8// version.
9//
10// Zaprus is distributed in the hope that it will be useful, but WITHOUT ANY
11// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License along with
15// Zaprus. If not, see <https://www.gnu.org/licenses/>.
16
17const RawSocket = @This();
18
19const is_debug = builtin.mode == .Debug;
20
21fd: i32,
22sockaddr_ll: std.posix.sockaddr.ll,
23mac: [6]u8,
24
25pub const max_payload_len = 1000;
26
27const Ifconf = extern struct {
28 ifc_len: i32,
29 ifc_ifcu: extern union {
30 ifcu_buf: ?[*]u8,
31 ifcu_req: ?[*]std.os.linux.ifreq,
32 },
33};
34
35pub fn init() error{
36 SocketError,
37 NicError,
38 NoInterfaceFound,
39 BindError,
40}!RawSocket {
41 const socket: i32 = std.math.cast(i32, std.os.linux.socket(std.os.linux.AF.PACKET, std.os.linux.SOCK.RAW, 0)) orelse return error.SocketError;
42 if (socket < 0) return error.SocketError;
43
44 var ifreq_storage: [16]std.os.linux.ifreq = undefined;
45 var ifc = Ifconf{
46 .ifc_len = @sizeOf(@TypeOf(ifreq_storage)),
47 .ifc_ifcu = .{ .ifcu_req = &ifreq_storage },
48 };
49
50 if (std.os.linux.ioctl(socket, std.os.linux.SIOCGIFCONF, @intFromPtr(&ifc)) != 0) {
51 return error.NicError;
52 }
53
54 const count = @divExact(ifc.ifc_len, @sizeOf(std.os.linux.ifreq));
55
56 // Get the first non loopback interface
57 var ifr = for (ifreq_storage[0..@intCast(count)]) |*ifr| {
58 if (std.os.linux.ioctl(socket, std.os.linux.SIOCGIFFLAGS, @intFromPtr(ifr)) == 0) {
59 if (ifr.ifru.flags.LOOPBACK) continue;
60 break ifr;
61 }
62 } else return error.NoInterfaceFound;
63
64 // 2. Get Interface Index
65 if (std.os.linux.ioctl(socket, std.os.linux.SIOCGIFINDEX, @intFromPtr(ifr)) != 0) {
66 return error.NicError;
67 }
68 const ifindex: i32 = ifr.ifru.ivalue;
69
70 // 3. Get Real MAC Address
71 if (std.os.linux.ioctl(socket, std.os.linux.SIOCGIFHWADDR, @intFromPtr(ifr)) != 0) {
72 return error.NicError;
73 }
74 var mac: [6]u8 = ifr.ifru.hwaddr.data[0..6].*;
75 if (builtin.cpu.arch.endian() == .little) std.mem.reverse(u8, &mac);
76
77 // 4. Set Flags (Promiscuous/Broadcast)
78 if (std.os.linux.ioctl(socket, std.os.linux.SIOCGIFFLAGS, @intFromPtr(ifr)) != 0) {
79 return error.NicError;
80 }
81 ifr.ifru.flags.BROADCAST = true;
82 ifr.ifru.flags.PROMISC = true;
83 if (std.os.linux.ioctl(socket, std.os.linux.SIOCSIFFLAGS, @intFromPtr(ifr)) != 0) {
84 return error.NicError;
85 }
86
87 const sockaddr_ll = std.mem.zeroInit(std.posix.sockaddr.ll, .{
88 .family = std.posix.AF.PACKET,
89 .ifindex = ifindex,
90 .protocol = std.mem.nativeToBig(u16, @as(u16, std.os.linux.ETH.P.IP)),
91 });
92
93 const bind_ret = std.os.linux.bind(socket, @ptrCast(&sockaddr_ll), @sizeOf(@TypeOf(sockaddr_ll)));
94 if (bind_ret != 0) return error.BindError;
95
96 return .{
97 .fd = socket,
98 .sockaddr_ll = sockaddr_ll,
99 .mac = mac,
100 };
101}
102
103pub fn setTimeout(self: *RawSocket, sec: isize, usec: i64) error{SetTimeoutError}!void {
104 const timeout: std.os.linux.timeval = .{ .sec = sec, .usec = usec };
105 const timeout_ret = std.os.linux.setsockopt(self.fd, std.os.linux.SOL.SOCKET, std.os.linux.SO.RCVTIMEO, @ptrCast(&timeout), @sizeOf(@TypeOf(timeout)));
106 if (timeout_ret != 0) return error.SetTimeoutError;
107}
108
109pub fn deinit(self: *RawSocket) void {
110 _ = std.os.linux.close(self.fd);
111 self.* = undefined;
112}
113
114pub fn send(self: RawSocket, payload: []const u8) !void {
115 const sent_bytes = std.os.linux.sendto(
116 self.fd,
117 payload.ptr,
118 payload.len,
119 0,
120 @ptrCast(&self.sockaddr_ll),
121 @sizeOf(@TypeOf(self.sockaddr_ll)),
122 );
123 _ = sent_bytes;
124}
125
126pub fn receive(self: RawSocket, buf: []u8) ![]u8 {
127 const len = std.os.linux.recvfrom(
128 self.fd,
129 buf.ptr,
130 buf.len,
131 0,
132 null,
133 null,
134 );
135 if (std.os.linux.errno(len) != .SUCCESS) {
136 return error.Timeout; // TODO: get the real error, assume timeout for now.
137 }
138 return buf[0..len];
139}
140
141pub fn attachSaprusPortFilter(self: RawSocket, incoming_src_port: ?u16, incoming_dest_port: u16) !void {
142 const BPF = std.os.linux.BPF;
143 // BPF instruction structure for classic BPF
144 const SockFilter = extern struct {
145 code: u16,
146 jt: u8,
147 jf: u8,
148 k: u32,
149 };
150
151 const SockFprog = extern struct {
152 len: u16,
153 filter: [*]const SockFilter,
154 };
155
156 // Build the filter program
157 const filter = if (incoming_src_port) |inc_src| &[_]SockFilter{
158 // Load 2 bytes at offset 46 (absolute)
159 .{ .code = BPF.LD | BPF.H | BPF.ABS, .jt = 0, .jf = 0, .k = 46 },
160 // Jump if equal to port (skip 1 if true, skip 0 if false)
161 .{ .code = BPF.JMP | BPF.JEQ | BPF.K, .jt = 1, .jf = 0, .k = @as(u32, inc_src) },
162 // Return 0x0 (fail)
163 .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0x0 },
164 // Load 2 bytes at offset 48 (absolute)
165 .{ .code = BPF.LD | BPF.H | BPF.ABS, .jt = 0, .jf = 0, .k = 48 },
166 // Jump if equal to port (skip 0 if true, skip 1 if false)
167 .{ .code = BPF.JMP | BPF.JEQ | BPF.K, .jt = 0, .jf = 1, .k = @as(u32, incoming_dest_port) },
168 // Return 0xffff (pass)
169 .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0xffff },
170 // Return 0x0 (fail)
171 .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0x0 },
172 } else &[_]SockFilter{
173 // Load 2 bytes at offset 48 (absolute)
174 .{ .code = BPF.LD | BPF.H | BPF.ABS, .jt = 0, .jf = 0, .k = 48 },
175 // Jump if equal to port (skip 0 if true, skip 1 if false)
176 .{ .code = BPF.JMP | BPF.JEQ | BPF.K, .jt = 0, .jf = 1, .k = @as(u32, incoming_dest_port) },
177 // Return 0xffff (pass)
178 .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0xffff },
179 // Return 0x0 (fail)
180 .{ .code = BPF.RET | BPF.K, .jt = 0, .jf = 0, .k = 0x0 },
181 };
182
183 const fprog = SockFprog{
184 .len = @intCast(filter.len),
185 .filter = filter.ptr,
186 };
187
188 // Attach filter to socket using setsockopt
189 const rc = std.os.linux.setsockopt(
190 self.fd,
191 std.os.linux.SOL.SOCKET,
192 std.os.linux.SO.ATTACH_FILTER,
193 @ptrCast(&fprog),
194 @sizeOf(SockFprog),
195 );
196
197 if (rc != 0) {
198 return error.BpfAttachFailed;
199 }
200}
201
202const std = @import("std");
203const builtin = @import("builtin");