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
17pub const IpAddr = packed struct {
18 int: I,
19
20 const V = @Vector(4, u8);
21 const I = u32;
22
23 pub fn fromBytes(s: V) IpAddr {
24 return .{ .int = @bitCast(s) };
25 }
26};
27
28pub const MacAddr = packed struct {
29 int: I,
30
31 const V = @Vector(6, u8);
32 const I = @Int(.unsigned, @bitSizeOf(V));
33
34 pub fn fromBytes(s: V) MacAddr {
35 return .{ .int = @bitCast(s) };
36 }
37};
38
39pub const EthIpUdp = packed struct(u336) { // 42 bytes * 8 bits = 336
40 // --- UDP (Last in memory, defined first for LSB->MSB) ---
41 udp: packed struct {
42 checksum: u16 = 0,
43 len: u16,
44 dst_port: u16,
45 src_port: u16,
46 },
47
48 // --- IP ---
49 ip: packed struct {
50 dst_addr: IpAddr,
51 src_addr: IpAddr,
52 header_checksum: u16 = 0,
53 protocol: u8 = 17, // udp
54 ttl: u8 = 0x40,
55
56 // fragment_offset (13 bits) + flags (3 bits) = 16 bits
57 // In Big Endian, flags are the high bits of the first byte.
58 // To have flags appear first in the stream, define them last here.
59 fragment_offset: u13 = 0,
60 flags: packed struct(u3) {
61 reserved: u1 = 0,
62 dont_fragment: u1 = 1,
63 more_fragments: u1 = 0,
64 } = .{},
65
66 id: u16,
67 len: u16,
68 tos: u8 = undefined,
69
70 // ip_version (4 bits) + ihl (4 bits) = 8 bits
71 // To have version appear first (high nibble), define it last.
72 ihl: u4 = 5,
73 ip_version: u4 = 4,
74 },
75
76 // --- Ethernet ---
77 eth_type: u16 = std.os.linux.ETH.P.IP,
78 src_mac: MacAddr,
79 dst_mac: MacAddr = .fromBytes(@splat(0xff)),
80
81 pub fn toBytes(self: @This()) [336 / 8]u8 {
82 var res: [336 / 8]u8 = undefined;
83 var w: Writer = .fixed(&res);
84 w.writeStruct(self, .big) catch unreachable;
85 return res;
86 }
87
88 pub fn setPayloadLen(self: *@This(), len: usize) void {
89 self.ip.len = @intCast(len + (@bitSizeOf(@TypeOf(self.udp)) / 8) + (@bitSizeOf(@TypeOf(self.ip)) / 8));
90
91 // Zero the checksum field before calculation
92 self.ip.header_checksum = 0;
93
94 // Serialize IP header to big-endian bytes
95 var ip_bytes: [@bitSizeOf(@TypeOf(self.ip)) / 8]u8 = undefined;
96 var w: Writer = .fixed(&ip_bytes);
97 w.writeStruct(self.ip, .big) catch unreachable;
98
99 // Calculate checksum over serialized bytes
100 self.ip.header_checksum = onesComplement16(&ip_bytes);
101
102 self.udp.len = @intCast(len + (@bitSizeOf(@TypeOf(self.udp)) / 8));
103 }
104};
105
106fn onesComplement16(data: []const u8) u16 {
107 var sum: u32 = 0;
108
109 // Process pairs of bytes as 16-bit words
110 var i: usize = 0;
111 while (i + 1 < data.len) : (i += 2) {
112 const word: u16 = (@as(u16, data[i]) << 8) | data[i + 1];
113 sum += word;
114 }
115
116 // Handle odd byte if present
117 if (data.len % 2 == 1) {
118 sum += @as(u32, data[data.len - 1]) << 8;
119 }
120
121 // Fold 32-bit sum to 16 bits
122 while (sum >> 16 != 0) {
123 sum = (sum & 0xFFFF) + (sum >> 16);
124 }
125
126 // Return ones' complement
127 return ~@as(u16, @truncate(sum));
128}
129
130const std = @import("std");
131const Writer = std.Io.Writer;