Pliant AllianceSoftware development notes & tools

Building a Simple Antivirus Scanner in C

Modern malware reaches Australian inboxes and browsers from a wide variety of sources, from compromised WordPress sites aimed at small businesses in Brisbane to phishing kits targeting mining engineers in Perth. The Australian Signals Directorate, through the Essential Eight framework, recommends application control and continuous malware detection as baseline protections. Beneath the glossy dashboards of commercial antivirus products sit surprisingly straightforward techniques, and a signature scanner, the oldest and still widely used detection method, can be implemented in just a few hundred lines of C. Understanding how that works is valuable for any developer who wants to write safer code or simply appreciate what tools such as Microsoft Defender, Avast, or ClamAV are doing on their behalf.

A scanner in C is a natural choice because the language sits close to the operating system. File reading, memory mapping, and direct byte comparison all happen with minimal overhead. Modern alternatives like Go, Rust, or Python exist, but C remains the lingua franca of security tooling. OpenSSL, the Linux kernel, and most commercial engines still write critical parsing code in C for that reason. Studying the architecture of a small scanner builds intuition for everything from buffer overflows to YARA rule writing later on.

The codebase described here targets POSIX-compatible systems, including macOS and most Linux distributions used by Australian universities and research groups. A small adaptation allows the same logic to compile against the Windows API, which matters because many local engineering firms maintain mixed fleets of Windows and Linux machines. The aim is educational rather than competitive — no hobby project will replace a vendor-grade engine. What it can do is demonstrate the core mechanics that every malware analyst eventually learns.

Because antivirus vendors keep their signatures proprietary, this implementation borrows from publicly documented formats such as ClamAV's CVD container and the MD5/SHA256 hash spaces that have been standard since the late 1990s. Readers who wish to extend the project will find plenty of room to experiment with YARA-style textual patterns, fuzzy hashing through ssdeep, or integration with VirusTotal's public API. Throughout, the focus is on keeping dependencies small, the source readable, and the build instructions compatible with a fresh Ubuntu Server install in a Sydney data centre.

How Signature-Based Malware Detection Works

At its core, a signature-based scanner compares files or memory regions against a database of fingerprints collected from previously analysed malicious code. Each fingerprint is a cryptographic hash, usually MD5, SHA-1, or SHA-256, calculated from a specific byte range inside the suspect file. When a match is found, the scanner flags the object. The technique is fast, deterministic, and easy to validate against test corpora, which is why it remains the backbone of nearly every consumer endpoint protection product sold through Australian retailers like JB Hi-Fi and Officeworks.

The first task when writing a scanner is choosing a signature format. A flat text file with one hash per line works for small projects, but anything beyond a few hundred entries needs a more efficient container. ClamAV uses a packed format that stores MD5 hashes alongside structural metadata, which is what allows fresh definitions to be delivered several times per day without overwhelming disk or memory. Implementing even a subset of that container logic gives a developer hands-on experience with endian handling, bit packing, and incremental decoding.

Two design choices shape the rest of the implementation. The first is whether signatures cover the entire file or only small regions. Whole-file hashes are compact and fast but yield no flexibility. Region hashes allow detection of a malicious payload embedded in an otherwise legitimate executable, which is far closer to how modern threats appear in samples submitted to the Australian Cyber Security Centre. The second choice is whether the engine streams files one block at a time or memory-maps them. Streaming suits very large files; memory mapping suits interaction with ELF or PE parsing libraries later.

Preparing the C Toolchain Locally

A C scanner can be built with nothing more than the GNU Compiler Collection, GNU Make, and zlib for optional compression support. On an Ubuntu 22.04 workstation in Melbourne or a Debian 12 server in Adelaide, installation is a one-liner pulled from the standard repository. macOS users add the Command Line Tools, while Windows developers usually rely on MinGW or MSYS2 if they prefer to stay outside Visual Studio. The codebase itself does not require exotic build flags, which keeps the entry barrier low for students and hobbyists.

Local skill development in Australia has produced several well-known security communities where questions about this kind of work can be raised. The Australian Information Security Association runs chapters in Sydney, Canberra, and Perth, while groups such as the Melbourne Security Reading Group meet regularly to dissect papers on endpoint protection. Universities such as UNSW and Monash include reverse-engineering electives that incorporate scanning projects, so beginners have access to mentors and recorded lectures as they work through their first compile errors.

Before any malware code is touched, a developer should be cautious. Australian privacy law treats identifiers that link back to a real device or person as personal information under the Privacy Act 1988. Working only with publicly available samples from repositories such as MalwareBazaar or theZoo protects the developer from inadvertent breaches. A small virtual machine, or at minimum a dedicated Linux user account isolated from shared family documents and bank logins, completes a reasonable safety posture.

Writing the Signature Database Loader

The loader turns a static file of hashes into something the scanner can search quickly. For a beginner-friendly implementation, a sorted array of 32-byte SHA-256 digests plus eight-byte internal offsets is enough. Reading the file into memory, validating each line as hexadecimal, and running a standard qsort routine prepares the data for binary search. The whole loader typically fits in under two hundred lines of C, including error handling.

Memory hygiene matters more than it might appear. Real-world signature databases run into the hundreds of megabytes, and the allocator's behaviour under fragmentation becomes a real concern. Valgrind, available through Ubuntu's universe repository, catches leaks and uninitialised reads that would later cause false positives. Developers who attend BSides events in regional centres often cite Valgrind as the tool that taught them the most about pointer safety in their first year.

For ergonomics, the loader can support an update check that pulls fresh definitions from a local mirror. A simple HTTP GET against a URL configured at build time, followed by a SHA-256 verification of the response, mirrors the secure update flow used by tools such as freshclam. That single addition turns a one-off scanner into something that can run nightly from cron on a home server in Adelaide, providing peace of mind without ongoing manual effort.

Walking the Filesystem and Comparing Hashes

With the signature database loaded, the scanner walks a target directory using opendir and readdir on POSIX systems or FindFirstFile on Windows. For each candidate file, the engine opens it, computes a hash over either the entire content or a defined region, then performs a binary search against the sorted array. A match increments a counter, logs the path, and continues. The whole pass on a typical home directory of mixed documents and binaries runs in seconds on a modest machine.

Several optimisations reveal themselves once a baseline version is working. Memory-mapped I/O via mmap reduces copies between user space and the kernel, particularly when scanning large video files where most bytes are irrelevant. Skipping known-good extensions reduces noise; opinionated blocklists of executables, scripts, and macro-enabled documents keep scan time reasonable on busy drives. Each of these refinements is implemented in a few dozen lines and offers a tangible lesson about prioritising work.

False negatives remain the more interesting failure mode. Polymorphic malware rewrites itself with each infection, breaking whole-file hashes but preserving functional regions. To handle those, the engine needs region hashes or, better still, fuzzy hashing libraries like ssdeep, which generate context-triggered piecewise hashes that survive small mutations. Integrating ssdeep requires a couple of extra #include directives and a wrapper for the library's three exposed functions, a worthwhile extension once the basic architecture feels familiar.

Adding Heuristic Detection Beyond Signatures

Signatures alone cannot catch novel threats, which is why every consumer product sold in Australian electronics chains bundles some form of heuristic engine. A hobby implementation can add a thin layer that flags suspicious combinations: an executable in a temporary directory, a script that calls functions associated with process injection, or a PE file with a tiny import table. None of these signals are conclusive on their own, but together they raise a verdict a user can investigate.

A practical heuristic loop reads the file header, looks for known markers, and scores the result against a small ruleset stored in plain C structs. Strings extracted via a simple Boyer-Moore search reveal suspicious API names such as VirtualAllocEx, WriteProcessMemory, or CreateRemoteThread — all staples of Windows-based offensive tooling. A scoring threshold above which the file is quarantined is configurable, mirroring the slider that users of tools like Bitdefender or ESET can adjust through their main interface.

The project also offers a useful way to engage with the broader Australian cybersecurity conversation. AARNet, the research and education network shared by every Australian university, runs threat intelligence feeds that participating institutions can query. Developers who polish a scanner to the point of reliable local detection often find themselves contributing to open projects, joining teams at firms like Atlassian or Canva that maintain internal red-team infrastructure, or eventually presenting their work at the annual Australian Cyber Conference held in Canberra. The path from a personal hobby script to a professional specialisation is rarely linear, but writing a small antivirus engine in C is a credible first step.