Adversarial C2 Protocol Implemented in Zig
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at dev 238 lines 9.3 kB view raw
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 Message = union(enum(u16)) { 18 relay: Message.Relay = 0x003C, 19 connection: Message.Connection = 0x00E9, 20 _, 21 22 pub const Relay = struct { 23 dest: Dest, 24 checksum: [2]u8 = undefined, 25 payload: []const u8, 26 27 pub const Dest = struct { 28 bytes: [relay_dest_len]u8, 29 30 /// Asserts bytes is less than or equal to 4 bytes 31 pub fn fromBytes(bytes: []const u8) Dest { 32 var buf: [4]u8 = @splat(0); 33 std.debug.assert(bytes.len <= buf.len); 34 @memcpy(buf[0..bytes.len], bytes); 35 return .{ .bytes = buf }; 36 } 37 }; 38 39 /// Asserts that buf is large enough to fit the relay message. 40 pub fn toBytes(self: Relay, buf: []u8) []u8 { 41 var out: Writer = .fixed(buf); 42 out.writeInt(u16, @intFromEnum(Message.relay), .big) catch unreachable; 43 out.writeInt(u16, @intCast(self.payload.len + 4), .big) catch unreachable; // Length field, but unread. Will switch to checksum 44 out.writeAll(&self.dest.bytes) catch unreachable; 45 out.writeAll(self.payload) catch unreachable; 46 return out.buffered(); 47 } 48 49 // test toBytes { 50 // var buf: [1024]u8 = undefined; 51 // const relay: Relay = .init( 52 // .fromBytes(&.{ 172, 18, 1, 30 }), 53 // // zig fmt: off 54 // &[_]u8{ 55 // 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 56 // 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 57 // }, 58 // // zig fmt: on 59 // ); 60 // // zig fmt: off 61 // var expected = [_]u8{ 62 // 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72, 63 // 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 64 // 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 65 // }; 66 // // zig fmt: on 67 // try expectEqualMessageBuffers(&expected, relay.toBytes(&buf)); 68 // } 69 }; 70 71 pub const Connection = struct { 72 src: u16, 73 dest: u16, 74 seq: u32, 75 id: u32, 76 reserved: u8 = undefined, 77 options: Options = .{}, 78 payload: []const u8, 79 80 /// Option values. 81 /// Currently used! 82 pub const Options = packed struct(u8) { 83 opt1: bool = false, 84 opt2: bool = false, 85 opt3: bool = false, 86 opt4: bool = false, 87 opt5: bool = false, 88 opt6: bool = false, 89 opt7: bool = false, 90 management: bool = false, 91 }; 92 93 /// Asserts that buf is large enough to fit the connection message. 94 pub fn toBytes(self: Connection, buf: []u8) []u8 { 95 var out: Writer = .fixed(buf); 96 out.writeInt(u16, @intFromEnum(Message.connection), .big) catch unreachable; 97 out.writeInt(u16, @intCast(self.payload.len + 14), .big) catch unreachable; // Saprus length field, unread. 98 out.writeInt(u16, self.src, .big) catch unreachable; 99 out.writeInt(u16, self.dest, .big) catch unreachable; 100 out.writeInt(u32, self.seq, .big) catch unreachable; 101 out.writeInt(u32, self.id, .big) catch unreachable; 102 out.writeByte(self.reserved) catch unreachable; 103 out.writeStruct(self.options, .big) catch unreachable; 104 out.writeAll(self.payload) catch unreachable; 105 return out.buffered(); 106 } 107 108 /// If the current message is a management message, return what kind. 109 /// Else return null. 110 pub fn management(self: Connection) ParseError!?Management { 111 const b64_dec = std.base64.standard.Decoder; 112 if (self.options.management) { 113 var buf: [1]u8 = undefined; 114 _ = b64_dec.decode(&buf, self.payload) catch return error.InvalidMessage; 115 116 return switch (buf[0]) { 117 'P' => .ping, 118 'p' => .pong, 119 else => error.UnknownSaprusType, 120 }; 121 } 122 return null; 123 } 124 125 pub const Management = enum { 126 ping, 127 pong, 128 }; 129 }; 130 131 pub fn toBytes(self: Message, buf: []u8) []u8 { 132 return switch (self) { 133 inline .relay, .connection => |m| m.toBytes(buf), 134 else => unreachable, 135 }; 136 } 137 138 pub fn parse(bytes: []const u8) ParseError!Message { 139 var in: Reader = .fixed(bytes); 140 const @"type" = in.takeEnum(std.meta.Tag(Message), .big) catch |err| switch (err) { 141 error.InvalidEnumTag => return error.UnknownSaprusType, 142 else => return error.InvalidMessage, 143 }; 144 const checksum = in.takeArray(2) catch return error.InvalidMessage; 145 switch (@"type") { 146 .relay => { 147 const dest: Relay.Dest = .fromBytes( 148 in.takeArray(relay_dest_len) catch return error.InvalidMessage, 149 ); 150 const payload = in.buffered(); 151 return .{ 152 .relay = .{ 153 .dest = dest, 154 .checksum = checksum.*, 155 .payload = payload, 156 }, 157 }; 158 }, 159 .connection => { 160 const src = in.takeInt(u16, .big) catch return error.InvalidMessage; 161 const dest = in.takeInt(u16, .big) catch return error.InvalidMessage; 162 const seq = in.takeInt(u32, .big) catch return error.InvalidMessage; 163 const id = in.takeInt(u32, .big) catch return error.InvalidMessage; 164 const reserved = in.takeByte() catch return error.InvalidMessage; 165 const options = in.takeStruct(Connection.Options, .big) catch return error.InvalidMessage; 166 const payload = in.buffered(); 167 return .{ 168 .connection = .{ 169 .src = src, 170 .dest = dest, 171 .seq = seq, 172 .id = id, 173 .reserved = reserved, 174 .options = options, 175 .payload = payload, 176 }, 177 }; 178 }, 179 else => return error.NotImplementedSaprusType, 180 } 181 } 182 183 test parse { 184 _ = try parse(&[_]u8{ 0x00, 0x3c, 0x00, 0x17, 0xac, 0x12, 0x01, 0x1e, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20, 0x6c, 0x6f, 0x67, 0x67, 0x65, 0x64 }); 185 186 { 187 const expected: Message = .{ 188 .connection = .{ 189 .src = 12416, 190 .dest = 61680, 191 .seq = 0, 192 .id = 0, 193 .reserved = 0, 194 .options = @bitCast(@as(u8, 100)), 195 .payload = &[_]u8{ 0x69, 0x61, 0x6d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74 }, 196 }, 197 }; 198 const actual = try parse(&[_]u8{ 0x00, 0xe9, 0x00, 0x18, 0x30, 0x80, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x69, 0x61, 0x6d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74 }); 199 200 try std.testing.expectEqualDeep(expected, actual); 201 } 202 } 203 204 test "Round trip" { 205 { 206 const expected = [_]u8{ 0x0, 0xe9, 0x0, 0x15, 0x30, 0x80, 0xf0, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x64, 0x36, 0x3a, 0x3a, 0x64, 0x61, 0x74, 0x61 }; 207 const msg = (try parse(&expected)).connection; 208 var res_buf: [expected.len + 1]u8 = undefined; // + 1 to test subslice result. 209 const res = msg.toBytes(&res_buf); 210 try expectEqualMessageBuffers(&expected, res); 211 } 212 } 213 214 // Skip checking the length / checksum, because that is undefined. 215 fn expectEqualMessageBuffers(expected: []const u8, actual: []const u8) !void { 216 try std.testing.expectEqualSlices(u8, expected[0..2], actual[0..2]); 217 try std.testing.expectEqualSlices(u8, expected[4..], actual[4..]); 218 } 219 220 pub const TypeError = error{ 221 NotImplementedSaprusType, 222 UnknownSaprusType, 223 }; 224 pub const ParseError = TypeError || error{ 225 InvalidMessage, 226 }; 227}; 228 229const relay_dest_len = 4; 230 231const std = @import("std"); 232const Allocator = std.mem.Allocator; 233const Writer = std.Io.Writer; 234const Reader = std.Io.Reader; 235 236test { 237 std.testing.refAllDecls(@This()); 238}