Safe Penetration Testing Labs · intermediate · ~15 min

Detect failed-login bursts in auth.log

By the end of this lesson you will be able to: - Read an in-memory `auth.log` buffer line by line in C11 using only standard library functions. - Classify each log line as a success, a failure, or noise, and explain why the exact substring you match (`Failed password`) matters for accuracy. - Extract the source IP that follows `from ` and stops before ` port `, safely and without buffer overruns. - Count failures per source IP by grouping, then report how many IPs meet or exceed a detection threshold. - Turn that count into a defensive signal: what to log about a suspected brute-force burst, and what you must never log. - Verify your detector rejects noise (successful logins, malformed lines) and accepts real bursts, all inside an isolated lab.

Overview

Security objective. The asset you are protecting is account access on a Linux host — specifically the SSH service (sshd) that lets users log in remotely. The threat is a brute-force attack: an attacker who repeatedly guesses passwords, hoping one works. Your job in this lesson is detection, not attack. You will build a small C program that reads the authentication log, spots the tell-tale burst of Failed password events coming from one source IP, and flags it so a human defender can respond.

Brute-force simply means trying many passwords in quick succession. Against a weak password it can succeed in minutes, so catching the attempt early — while it is still failing — is what gives defenders time to react (block the IP, force a password reset, enable rate limiting).

Where this fits: Linux records authentication events in /var/log/auth.log (Debian/Ubuntu/Kali) or /var/log/secure (RHEL/Fedora). A brute-force run leaves a very visible fingerprint there — dozens or hundreds of Failed password for ... from <ip> lines from the same address within seconds. A Security Operations Center (SOC) analyst reads exactly these lines to separate a real attack from background noise. Every off-the-shelf tool that does this (fail2ban, a SIEM correlation rule, an IDS signature) starts from the same core idea you are about to implement.

How this builds on your prerequisites. From C strings you already know a C string is a char * ending in a '\0', and that functions like strstr, strncmp, and strchr search and slice those bytes without allocating. You will lean on all three here. From The main function you know how int main(void) returns a status code and how a program's entry point drives the work; the detector you write is a plain function that main calls with a fixed test buffer. Crucially, the log bytes are a static buffer bundled with the harness — your code never opens /var/log/auth.log and never touches the network. That keeps the whole exercise safe and reproducible.

Why it matters

Triage — deciding what is a real attack and what is harmless — is the first job a SOC does after an alert fires. A busy internet-facing SSH server can log thousands of failed logins a day just from automated internet scanning. The skill that matters professionally is not "a failed login happened" (that is constant) but "this source IP failed this many times in this window, which is abnormal." That is precisely the grouped-count-versus-threshold logic in this lesson.

In authorized professional work this shows up everywhere:

  • Blue-team / SOC monitoring. Analysts write and tune detection rules that count failures per IP or per account and alert when a threshold is crossed. Understanding the parsing under the hood lets you tune thresholds instead of blindly trusting a vendor default.
  • Incident response. After a breach, responders grep auth.log to reconstruct when the brute force started, which IPs took part, and whether any attempt eventually succeeded (an Accepted line right after a burst of Failed lines from the same IP is a red flag).
  • Detection engineering. The count-and-threshold pattern generalizes to failed API keys, failed 2FA prompts, and failed database logins.

Because the logic is simple string parsing, it is a perfect first exercise for learning to build detections that are precise (few false positives) and honest about their limits.

Core concepts

1. The authentication log as a data source

Definition. /var/log/auth.log (Debian/Ubuntu/Kali) is a plain-text file where the system records authentication events — logins, sudo use, SSH sessions. On RHEL/Fedora the same information lives in /var/log/secure.

Plain explanation. Every time someone (or something) tries to log in, a line is appended. You read it top to bottom; each line is one event.

How it works. sshd writes a line per attempt through the system logger. A failed SSH password attempt looks like:

Jan 23 12:01:02 host sshd[1234]: Failed password for invalid user root from 192.0.2.7 port 5413 ssh2

When / when not. Use it for authentication-related detection. Do not treat it as tamper-proof: an attacker with root can edit or delete it, which is why production systems ship logs off-host to a central collector. In this lab the buffer is static and read-only.

Pitfall. Log formats vary slightly by OS and sshd version. Matching a substring that is stable across versions (Failed password) is safer than matching the whole line.

2. Line classification: failure vs success vs noise

Definition. Classifying means deciding what category each line belongs to before you count it.

Plain explanation. Only some lines are failed passwords. Accepted publickey and Accepted password are successes. Session-open messages, sudo lines, and cron noise are irrelevant. Count only what you mean to count.

How it works. Search each line for the exact substring "Failed password". If it is absent, skip the line. This is a substring test with strstr.

When / when not. For a first-pass burst detector, substring matching is enough. When you need airtight parsing (feeding a SIEM), you would parse structured fields instead of free text.

Pitfall. strstr matches anywhere in the line. A crafted username could contain the text Failed password. In this lab the buffer is trusted; in production you would anchor the match to the message portion after sshd[...]:.

3. Extracting the source IP

Definition. The source IP is the address the attempt came from — the token right after from and right before port.

Plain explanation. Find from , start reading after it, and stop at the next space (the one before port). The bytes in between are the IP.

How it works. strstr(line, "from ") gives you a pointer to from ; advance 5 bytes to reach the address; then copy characters until you hit a space or newline, always leaving room for the terminating '\0'.

When / when not. This boundary rule works for both IPv4 (192.0.2.7) and IPv6 addresses as printed by sshd. It fails on malformed lines that lack from — which you must handle by skipping, not crashing.

Pitfall. Copying into a fixed char ip[64] without a length cap is a classic overflow. Always bound the copy and terminate the string.

4. Grouping and thresholding

Definition. Grouping means accumulating a per-IP counter; thresholding means comparing that counter to a chosen limit.

Plain explanation. Keep a small table of {ip, count}. Each Failed password line: find the IP's row (or add one), increment its count. At the end, count how many rows reached threshold.

How it works. A linear scan of the table with strcmp is perfectly fine for a lab-sized buffer. Real tools use a hash map, but the logic is identical.

When / when not. Thresholds trade off sensitivity vs false positives. Too low and normal retries alert; too high and a slow brute force slips through. Tune against real baseline traffic.

Pitfall. Double-counting. If you add a fresh row every time an IP appears instead of finding the existing row, every IP looks unique and no threshold is ever crossed.

Threat model (text diagram)

            UNTRUSTED                      TRUST BOUNDARY                 TRUSTED (defender)
  +-----------------------+     |     +----------------------------+     +------------------+
  |  Attacker on internet |     |     |  sshd on the host          |     |  auth.log buffer |
  |  guesses passwords    |=====|====>|  (entry point: TCP :22)    |====>|  (static, in lab)|
  |  from source IP X     |  network  |  writes one line/attempt   |     +---------+--------+
  +-----------------------+  boundary +----------------------------+               |
                                                                                   v
                                                                    +-----------------------------+
                                                                    |  YOUR detector (this lesson)|
                                                                    |  count Failed-password per IP|
                                                                    |  flag IPs >= threshold       |
                                                                    +--------------+--------------+
                                                                                   |
                                                                                   v
                                                                    +-----------------------------+
                                                                    |  Alert + defensive log entry |
                                                                    |  (SOC analyst acts on it)    |
                                                                    +-----------------------------+

  Asset protected : account access on the host (the sshd login).
  Entry point     : sshd listening on TCP port 22.
  Trust boundary  : the network edge — data from source IPs is untrusted input.
  Insecure assumption an attacker exploits: "anyone reaching :22 may keep guessing forever."

Knowledge check.

  1. What asset is this detector ultimately protecting, and at which entry point does the attacker reach it?
  2. Where is the trust boundary, and why must you treat the IP and username text in each line as untrusted input even though it lives in a local file?
  3. Which single line in auth.log, appearing right after a long burst of failures from the same IP, would tell you the brute force succeeded — and why would that raise the incident's severity?

Syntax notes

The whole detector is built from four standard-library string operations plus a bounded copy. All are declared in <string.h>.

#include <string.h>

/* 1. Does this line contain the failure marker?
   strstr returns a pointer to the first match, or NULL if absent. */
char *marker = strstr(line, "Failed password");
if (marker == NULL) {
    /* not a failed-password line -> skip it */
}

/* 2. Find where the IP starts: just after "from ". */
char *f = strstr(line, "from ");
if (f != NULL) {
    char *ip_start = f + 5;          /* skip the 5 bytes of "from " */

/* 3. The IP ends at the next space or end-of-line. */
    size_t len = 0;
    while (ip_start[len] != ' ' &&
           ip_start[len] != '\n' &&
           ip_start[len] != '\0') {
        len++;
    }

/* 4. Copy it into a fixed buffer, NEVER exceeding its size, then terminate. */
    char ip[64];
    if (len >= sizeof ip) len = sizeof ip - 1;   /* clamp: leave room for '\0' */
    memcpy(ip, ip_start, len);
    ip[len] = '\0';
}

Key points:

  • strstr(haystack, needle) returns NULL when the needle is absent — always check before dereferencing.
  • f + 5 works because "from " is exactly five bytes; count them yourself rather than guessing.
  • The len >= sizeof ip clamp is what prevents a buffer overflow if a malformed line has no delimiter. sizeof ip - 1 reserves the last byte for '\0'.
  • To walk the buffer line by line, use strchr(p, '\n') to find each line end, or copy each line into a temporary buffer before processing.

Lesson

What the log records

The /var/log/auth.log file is the first place a defender looks after a suspicious event. It records authentication activity on the system.

A brute-force attack against SSH leaves a clear trail: many Failed password for ... lines from the same source IP, all within a short window.

(Brute-force means an attacker tries many passwords in quick succession, hoping one works.)

What this exercise teaches

This exercise focuses on the detection side. The steps are:

  1. Read the log buffer.
  2. Parse each line.
  3. Count failures per IP.
  4. Report which IPs crossed a threshold.

What the file looks like

Jan 23 12:01:02 host sshd[1234]: Failed password for invalid user root from 192.0.2.7 port 5413
Jan 23 12:01:04 host sshd[1235]: Failed password for invalid user admin from 192.0.2.7 port 5414
Jan 23 12:01:08 host sshd[1240]: Accepted publickey for gilos from 10.0.0.5 port 22001

The bytes you process are static and bundled in the harness. The function never opens a real log file.

Your job

Write a function that:

  • Walks the buffer line by line.
  • Counts Failed password for ... from <ip> lines per IP.
  • Returns the number of distinct IPs that appear at least threshold times.

Common mistakes

  • Counting the wrong lines. Accepted publickey is a success, not a failure. The string you want is Failed password.
  • Mis-reading the IP. The IP token ends right before the port that follows it. Use that boundary to extract it.
  • Double-counting. Do not add the same IP fresh every time it appears. Group first, then count.

Code examples

The example builds the detection in three stages: an intentionally sloppy version that miscounts and can overflow, the corrected secure version, and checks that prove the fix works. Everything runs on a static in-memory buffer — no files, no network.

Stage 1 — WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.

/* WARNING: intentionally vulnerable — use only in a local, isolated,
   authorized lab. Do not deploy.

   Two bugs on purpose:
   (A) counts "password" anywhere, so it also counts "Accepted password"
       (a SUCCESS) as if it were a failure -> false positives.
   (B) copies the IP into a fixed buffer with no length check -> a line
       missing the " port " delimiter overruns ip[] (buffer overflow). */
#include <stdio.h>
#include <string.h>

static int bad_detect(const char *log) {
    int hits = 0;
    const char *p = log;
    while (*p) {
        if (strstr(p, "password")) {          /* BUG A: too loose */
            const char *f = strstr(p, "from ");
            if (f) {
                char ip[8];                    /* far too small */
                int i = 0;
                const char *s = f + 5;
                while (*s != ' ') {            /* BUG B: no bound, no newline/EOF check */
                    ip[i++] = *s++;            /* overruns ip[] */
                }
                ip[i] = '\0';
                hits++;
            }
        }
        const char *nl = strchr(p, '\n');
        if (!nl) break;
        p = nl + 1;
    }
    return hits;
}

Stage 2 — the SECURE detector

#include <stdio.h>
#include <string.h>

#define MAX_IPS   256
#define IP_LEN     64
#define LINE_LEN 1024

/* One row per distinct source IP. */
struct ip_count {
    char ip[IP_LEN];
    int  count;
};

/* Find the row for `ip`, or create it. Returns index, or -1 if table full. */
static int find_or_add(struct ip_count *tbl, int *n, const char *ip) {
    for (int i = 0; i < *n; i++) {
        if (strcmp(tbl[i].ip, ip) == 0) return i;   /* group: reuse row */
    }
    if (*n >= MAX_IPS) return -1;                   /* table full: refuse */
    strncpy(tbl[*n].ip, ip, IP_LEN - 1);
    tbl[*n].ip[IP_LEN - 1] = '\0';
    tbl[*n].count = 0;
    return (*n)++;
}

/* Extract the source IP from one line into `out` (size IP_LEN).
   Returns 1 on success, 0 if the line has no usable "from <ip>". */
static int extract_ip(const char *line, char *out) {
    const char *f = strstr(line, "from ");
    if (f == NULL) return 0;
    const char *s = f + 5;                          /* skip "from " */
    size_t len = 0;
    while (s[len] != ' ' && s[len] != '\n' && s[len] != '\0') {
        len++;
    }
    if (len == 0) return 0;
    if (len >= IP_LEN) len = IP_LEN - 1;            /* clamp: no overflow */
    memcpy(out, s, len);
    out[len] = '\0';
    return 1;
}

/* Count how many distinct source IPs produced at least `threshold`
   "Failed password" events in the log buffer. */
int detect_bursts(const char *log, int threshold) {
    struct ip_count tbl[MAX_IPS];
    int n = 0;

    const char *p = log;
    while (*p != '\0') {
        /* Copy the current line into a bounded temporary. */
        char line[LINE_LEN];
        const char *nl = strchr(p, '\n');
        size_t linelen = nl ? (size_t)(nl - p) : strlen(p);
        if (linelen >= LINE_LEN) linelen = LINE_LEN - 1;   /* clamp */
        memcpy(line, p, linelen);
        line[linelen] = '\0';

        /* Classify: only exact "Failed password" lines count. */
        if (strstr(line, "Failed password") != NULL) {
            char ip[IP_LEN];
            if (extract_ip(line, ip)) {
                int idx = find_or_add(tbl, &n, ip);
                if (idx >= 0) tbl[idx].count++;
            }
        }

        if (nl == NULL) break;
        p = nl + 1;
    }

    int flagged = 0;
    for (int i = 0; i < n; i++) {
        if (tbl[i].count >= threshold) flagged++;
    }
    return flagged;
}

Stage 3 — VERIFY the fix rejects bad input and accepts good input

int main(void) {
    /* Static in-lab log: 192.0.2.7 fails 3x (a burst), 10.0.0.5 succeeds,
       198.51.100.9 fails once (noise). No real hosts; TEST-NET ranges only. */
    const char *log =
        "Jan 23 12:01:02 host sshd[1234]: Failed password for invalid user root from 192.0.2.7 port 5413 ssh2\n"
        "Jan 23 12:01:04 host sshd[1235]: Failed password for invalid user admin from 192.0.2.7 port 5414 ssh2\n"
        "Jan 23 12:01:06 host sshd[1236]: Failed password for gilos from 192.0.2.7 port 5415 ssh2\n"
        "Jan 23 12:01:08 host sshd[1240]: Accepted publickey for gilos from 10.0.0.5 port 22001 ssh2\n"
        "Jan 23 12:02:00 host sshd[1250]: Failed password for invalid user test from 198.51.100.9 port 3000 ssh2\n";

    /* ACCEPT good input: with threshold 3, exactly one IP (192.0.2.7). */
    int flagged = detect_bursts(log, 3);
    printf("threshold 3 -> flagged IPs: %d (expect 1)\n", flagged);

    /* REJECT the success line: the Accepted publickey from 10.0.0.5
       must never be counted as a failure. A high threshold flags nobody. */
    int none = detect_bursts(log, 99);
    printf("threshold 99 -> flagged IPs: %d (expect 0)\n", none);

    /* Sensitivity check: threshold 1 flags the two IPs that ever failed. */
    int lenient = detect_bursts(log, 1);
    printf("threshold 1 -> flagged IPs: %d (expect 2)\n", lenient);

    return 0;
}

Expected output

threshold 3 -> flagged IPs: 1 (expect 1)
threshold 99 -> flagged IPs: 0 (expect 0)
threshold 1 -> flagged IPs: 2 (expect 2)

Compile and run in your lab with:

gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined -o detect detect.c
./detect

The -fsanitize=address,undefined flags make the vulnerable Stage-1 copy crash loudly if you run it, proving the overflow is real; the Stage-2 code runs clean.

Line by line

Walking through detect_bursts on the Stage-3 log:

  1. struct ip_count tbl[MAX_IPS]; int n = 0; — an empty table of {ip, count} rows. n is how many rows are in use.
  2. The outer while (*p != '\0') walks the buffer one line at a time. strchr(p, '\n') finds the end of the current line; linelen is its length, clamped to fit the temporary line[LINE_LEN].
  3. memcpy(line, p, linelen); line[linelen] = '\0'; copies just this line and terminates it, so strstr never runs past the line.
  4. strstr(line, "Failed password") is the classifier. Success lines (Accepted publickey) return NULL here and are skipped — that is why the success from 10.0.0.5 is never counted.
  5. extract_ip finds from , jumps 5 bytes, and copies until the space before port, clamped to IP_LEN. For line 1 that yields 192.0.2.7.
  6. find_or_add searches the table with strcmp. First time it sees 192.0.2.7 it adds a row; the next two times it reuses that row — this is the grouping that prevents double-counting.
  7. After the loop, the final for counts rows whose count >= threshold.

Trace with threshold = 3:

Line Marker present? Extracted IP Table after step
1 Failed … 192.0.2.7 yes 192.0.2.7 {192.0.2.7:1}
2 Failed … 192.0.2.7 yes 192.0.2.7 {192.0.2.7:2}
3 Failed … 192.0.2.7 yes 192.0.2.7 {192.0.2.7:3}
4 Accepted … 10.0.0.5 no — (skipped) {192.0.2.7:3}
5 Failed … 198.51.100.9 yes 198.51.100.9 {192.0.2.7:3, 198.51.100.9:1}

Final pass with threshold = 3: 192.0.2.7 has 3 (>=3, flagged), 198.51.100.9 has 1 (not flagged). Result: 1. With threshold = 1 both rows qualify → 2. With threshold = 99 neither qualifies → 0.

Common mistakes

Mistake 1 — matching "password" instead of "Failed password".

  • WRONG: if (strstr(line, "password")).
  • WHY WRONG: Accepted password (a successful login) also contains password, so successes are counted as failures. Your "brute force" alert fires on legitimate users — a false positive that erodes trust in the detector.
  • CORRECTED: match the full "Failed password" substring.
  • RECOGNISE/PREVENT: add a test line with Accepted password and assert it is not counted (Stage-3 does this with the threshold 99 -> 0 check).

Mistake 2 — unbounded IP copy (buffer overflow).

  • WRONG: while (*s != ' ') ip[i++] = *s++; into a small char ip[8].
  • WHY WRONG: a malformed line without a trailing space, or an IP longer than the buffer, writes past ip[] and corrupts the stack — undefined behavior and a real security bug even in a parser.
  • CORRECTED: stop on space, '\n', or '\0', clamp len to IP_LEN - 1, and always write '\0'.
  • RECOGNISE/PREVENT: compile with -fsanitize=address; it aborts on the overrun immediately.

Mistake 3 — adding a new row per occurrence (double counting inverted).

  • WRONG: strncpy(tbl[n].ip, ip, ...); tbl[n].count = 1; n++; every time.
  • WHY WRONG: each Failed password becomes its own row, so no IP ever accumulates past 1 and no threshold is crossed — the detector silently detects nothing.
  • CORRECTED: find_or_add searches first and reuses an existing row.
  • RECOGNISE/PREVENT: assert that N identical-IP failures produce a count of N, not N rows of 1.

Mistake 4 — reading past the current line.

  • WRONG: running strstr/strchr on the raw p and assuming they stop at the line boundary.
  • WHY WRONG: strstr happily crosses '\n', so from on a later line can attach to the marker on an earlier line.
  • CORRECTED: copy each line into a bounded temporary first, then parse the temporary.
  • RECOGNISE/PREVENT: put two adjacent lines where only one is a failure and confirm the IP comes from the right one.

Mistake 5 — assuming logs are trustworthy.

  • WRONG: treating auth.log as ground truth of everything that happened.
  • WHY WRONG: an attacker with root can delete or forge entries; a burst that stops abruptly may mean the log was cleared, not that the attack ended.
  • CORRECTED: in production, ship logs to a separate collector; in the lab, note the assumption explicitly.
  • RECOGNISE/PREVENT: cross-check against a second source (netflow, the collector) before concluding.

Debugging tips

The count is always 0.

  • Print each extracted IP and the running table. Are IPs being extracted at all? If not, your from offset or delimiter is wrong.
  • Check you are reusing rows (find_or_add) rather than creating a new one each time.

The count is too high.

  • You are probably matching password loosely and counting Accepted password. Switch to the full "Failed password" marker and re-run the success-line test.
  • Confirm strstr is running on a single terminated line, not the whole buffer.

Segfault or AddressSanitizer abort.

  • Almost always the unbounded IP copy. Verify the len >= IP_LEN clamp and the '\n'/'\0' stop conditions are present.
  • Check strstr's return value before dereferencing — a NULL from a missing from will crash if you add 5 to it blindly.

Last line ignored.

  • If the buffer's final line has no trailing '\n', strchr returns NULL. Make sure your loop still processes that last chunk (the secure code uses strlen(p) in that case) before breaking.

Questions to ask when it fails.

  • Is the line I am parsing terminated, and does it end where I think it does?
  • Did strstr actually find the marker, or am I dereferencing NULL?
  • Is this IP a new row or an existing one — did grouping happen?
  • Does a deliberately malformed line (no from, no port) get skipped safely instead of crashing?

A fast way to localize bugs: temporarily printf("[%s] len=%zu -> ip=%s\n", classified?"FAIL":"skip", linelen, ip) per line, run once, then remove the prints.

Memory safety

This lesson has two safety dimensions: C memory safety and security detection/logging.

C memory safety for this parser.

  • Every copy is bounded. extract_ip clamps len to IP_LEN - 1; the line copy clamps to LINE_LEN - 1; find_or_add uses strncpy and then forces a terminator. Never copy attacker-influenced text into a fixed buffer without a length cap.
  • Always terminate. After every memcpy/strncpy of a slice, write '\0' yourself — strncpy does not terminate when the source is too long.
  • Check pointers before use. strstr and strchr return NULL; adding an offset to NULL is undefined behavior.
  • No heap here, so no leaks — but if you grow this to malloc a dynamic table, pair every malloc with free and check for NULL.
  • Build with -Wall -Wextra -fsanitize=address,undefined while developing; it catches overruns and bad reads at the exact line.

Security & safety: detection and logging. When your detector flags a burst, the alert it emits is itself security data. Log it carefully.

What to log for each flagged burst:

  • Timestamp (from the log line, plus detection time).
  • Source IP (the address that failed repeatedly).
  • Target resource / service (sshd, and the account names tried if you track them).
  • Result and security decision ("flagged: 47 failures >= threshold 20; recommend block").
  • A correlation id so this alert can be tied to related events and to any follow-up incident.

What to NEVER log:

  • The passwords that were attempted (auth logs already omit these — keep it that way).
  • Session tokens, cookies, private keys, or API keys.
  • Full payment card numbers or unnecessary PII. Log the account name only if you have a need; never secrets.

Which events signal abuse: a high failure-per-IP count in a short window; failures spread across many usernames from one IP (username enumeration); and — most serious — a burst of failures immediately followed by an Accepted line from the same IP, which suggests the brute force succeeded.

How false positives arise: a user or app with a stale cached password can retry rapidly and look like an attacker; a NAT gateway or corporate proxy makes many legitimate users share one source IP, inflating its count; and vulnerability scanners run by your own team generate bursts. Tune thresholds against a real baseline and maintain an allowlist for known-good sources so the signal stays trustworthy.

Real-world uses

Authorized real-world use case. A SOC deploys a lightweight agent (or a SIEM rule) that reads auth.log on each server, counts Failed password events per source IP over a rolling window, and raises a ticket when an IP crosses the threshold. An analyst then decides whether to block the IP at the firewall, confirm the targeted accounts are safe, and check for a following Accepted line. Tools like fail2ban automate exactly this loop on a single host; the detector you wrote is its beating heart.

Professional best-practice habits.

  • Input validation: treat every field parsed from a log as untrusted; bound every copy; skip malformed lines instead of crashing.
  • Least privilege: the log reader needs read-only access to the log and nothing else — no write, no network egress beyond the collector.
  • Secure defaults: ship logs to a separate collector so an attacker who roots one host cannot erase the evidence; keep thresholds conservative and documented.
  • Logging: record the who/what/when/decision/correlation-id of each alert; never record secrets.
  • Error handling: fail closed on parse errors (skip and count, do not silently drop whole files); surface parser errors so a broken detector cannot masquerade as "no attacks."

Beginner vs advanced.

Aspect Beginner Advanced
Data one static buffer live tail of rotating logs, multiple hosts
Matching substring Failed password structured field parse, anchored to the message
Storage linear {ip,count} array hash map + time-windowed / decaying counters
Window whole buffer rolling N-minute window per IP
Response print a flag enrich (geo/ASN), correlate, auto-ticket, rate-limit
Trust logs assumed intact tamper detection, off-host collection, integrity checks

Always remember: passing this or any automated check does not prove a system is secure, and nothing is ever "completely secure" — a detector reduces time-to-notice, it does not eliminate the threat.

Practice tasks

All tasks run on a static, in-memory log buffer in an isolated lab. Do not point any of this at a system you do not own or are not explicitly authorized to test.

Beginner 1 — Count all failed passwords.

  • Objective: write int count_failed(const char *log) returning the number of lines containing "Failed password".
  • Requirements: walk the buffer line by line; classify with the exact marker; ignore Accepted lines.
  • Input/output: the Stage-3 sample → 3.
  • Constraints: no dynamic allocation; do not read past each line.
  • Hints: strstr per line; increment on a non-NULL match.
  • Concepts: line iteration, substring classification.
  • Defensive close: confirm an Accepted password line is not counted before trusting the number.

Beginner 2 — Extract every source IP safely.

  • Objective: write int print_failed_ips(const char *log) that prints the source IP of each failed-password line and returns how many it printed.
  • Requirements: use a bounded extract_ip; skip lines with no from .
  • Input/output: Stage-3 sample → prints 192.0.2.7 three times and 198.51.100.9 once; returns 4.
  • Constraints: fixed char ip[64]; clamp the copy; always terminate.
  • Hints: reuse the extract_ip shape from the lesson; stop at space/newline/EOF.
  • Concepts: bounded copy, pointer offset after from .
  • Defensive close: run under -fsanitize=address and confirm a line missing port does not overrun.

Intermediate 1 — Per-IP burst detector.

  • Objective: implement int detect_bursts(const char *log, int threshold) (as in the lesson) returning the number of distinct IPs with at least threshold failures.
  • Requirements: group by IP; count; compare to threshold.
  • Input/output: sample with threshold 31; threshold 12; threshold 990.
  • Constraints: linear {ip,count} table; handle a full table by refusing new rows.
  • Hints: find_or_add before incrementing.
  • Concepts: grouping, thresholding, false-positive awareness.
  • Defensive close: add a test asserting the success line never contributes to any count.

Intermediate 2 — Enumeration signal.

  • Objective: extend the table to also track the number of distinct usernames each IP tried; return how many IPs tried at least user_threshold different usernames.
  • Requirements: parse the username between for (and optional invalid user ) and from; store a small per-IP username set.
  • Input/output: define your own lab lines where one IP tries root, admin, test.
  • Constraints: bound every buffer; cap the username set size and refuse overflow.
  • Hints: many distinct usernames from one IP is the enumeration fingerprint.
  • Concepts: multi-field parsing, second detection dimension.
  • Defensive close: document why counting distinct usernames catches attacks that a raw failure count might miss, and note the NAT false-positive risk.

Challenge — Windowed detector with a defensive report.

  • Objective: given lines that carry timestamps, flag IPs that reach threshold failures within any window_seconds span, then emit a one-line defensive alert per flagged IP (source IP, count, first/last timestamp, correlation id) — with NO secrets.
  • Requirements: parse the Mon DD HH:MM:SS timestamp to seconds-of-day; keep per-IP failure times; slide a window; produce the alert text your SOC would log.
  • Input/output: craft lab lines where an IP fails 5 times in 10 seconds (flag) and another fails 5 times spread over an hour (do not flag).
  • Constraints: fixed-size ring of timestamps per IP; bounded copies; skip malformed timestamps.
  • Hints: sort or track min/max within the window; do not log attempted passwords or any token.
  • Concepts: time-windowed thresholding, alert formatting, detection hygiene.
  • Authorization checklist before running any lab: (1) I own or am explicitly authorized to test this host; (2) it is localhost / a container / an intentionally-vulnerable VM / a CTF; (3) no production data or real user PII is present; (4) I have a reset plan.
  • Cleanup / reset: this exercise only reads an in-memory buffer, so no system state changes; if you extended it to read a lab file, delete any copies of that file and clear your shell history of paths you no longer need. Verify your remediation claim by re-running the detector and confirming the windowed IP is flagged and the slow one is not.

Summary

Main concepts. auth.log records SSH authentication events; a brute force shows up as many Failed password ... from <ip> lines from one source IP in a short window. Detection = classify each line, extract the source IP, group failures per IP, and flag IPs that reach a threshold. This is the core SOC triage routine and the heart of tools like fail2ban.

Key syntax / commands.

  • strstr(line, "Failed password") — classify (returns NULL if absent; always check).
  • strstr(line, "from ") + 5 — locate the IP start; copy until space/'\n'/'\0', clamped to the buffer size, then terminate.
  • A linear {ip, count} table with find_or_add to group, then a final pass counting count >= threshold.
  • Build/verify: gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined.

Common mistakes. Matching password (counts successes) instead of Failed password; unbounded IP copy (buffer overflow); adding a new row per occurrence instead of grouping; letting strstr cross line boundaries; trusting logs as tamper-proof.

What to remember. Precision beats volume — count this IP, this many times, not "a failure happened." Bound every copy and terminate every string. Log the who/what/when/decision/correlation-id of an alert; never log passwords, tokens, keys, or unneeded PII. Tune thresholds against real baselines (NAT and self-run scanners cause false positives). Decoding a log is not the same as proving an attack succeeded, passing an automated check does not prove a system is secure, and nothing is ever "completely secure." Only ever run this against systems you own or are explicitly authorized to test.

Practice with these exercises