Safe Penetration Testing Labs · intermediate · ~15 min

Count global symbols in an ELF .symtab

**What you will learn** - Read the fixed 24-byte layout of an `Elf64_Sym` entry and locate the `st_info` field by byte offset. - Use bitwise operators to split `st_info` into a *binding* (high nibble) and a *type* (low nibble). - Walk a raw `.symtab` byte buffer safely with a fixed stride, without casting to a struct pointer (avoiding alignment undefined behavior). - Validate untrusted input: reject buffers whose length is not a multiple of the entry size, and handle NULL/empty buffers correctly. - Interpret the global-symbol count as a triage signal that separates a stripped binary from an un-stripped one during authorized malware/reverse-engineering analysis.

Overview

Security objective. In an authorized reverse-engineering lab, the asset you protect is your analysis workstation and the correctness of your triage report. The threat is a hostile binary (an unknown sample) that hands you a deliberately malformed symbol table to crash your parser, read out of bounds, or make you report the wrong thing. This lesson teaches you to read one small slice of an ELF file — the symbol table (.symtab) — and count how many symbols have global binding, while treating every byte in that file as attacker-controlled.

An ELF file (Executable and Linkable Format) is the standard binary format on Linux: executables, shared libraries (.so), and object files (.o) are all ELF. Its symbol table lists the named functions and variables the program declares. Reverse-engineering tools — nm, objdump, readelf, Ghidra, radare2 — all start by reading this table. The global-symbol count is a one-glance signal: a stripped binary (symbols removed to slow analysis) has very few, while an un-stripped build exposes many.

This builds directly on your prerequisites. From pointers you already know how to hold a const uint8_t * and index into a buffer. From bitwise-operators you know >> (shift right) and & (mask), which we use to pull the binding out of a packed byte. From structs you know the idea of a fixed field layout — an Elf64_Sym is exactly that — but here you will read fields by explicit byte offset instead of casting, because untrusted, possibly-misaligned bytes make a struct cast unsafe.

Why it matters

In real, authorized security work you constantly receive files you did not create and cannot trust. A malware analyst pulls a suspicious sample from a sandbox; an incident responder recovers a binary from a compromised host; a firmware researcher extracts an executable from a device image. The very first questions are triage questions: Is this stripped? How much symbol information survived? Is the file even well-formed, or is it truncated/corrupt?

A global-symbol count answers the first two in one number, and a strict length check answers the third. Writing the parser yourself — instead of shelling out to readelf — matters because (1) you learn exactly what the tools do, so you can spot when a crafted file fools them; (2) you can run at scale over thousands of samples without spawning a process each time; and (3) building the habit of validate-before-you-trust on a tiny, well-defined structure is exactly the discipline that prevents parser bugs, which are one of the most common sources of memory-safety vulnerabilities in real security tooling.

Core concepts

1. The ELF symbol table (.symtab)

Definition. .symtab is a section inside an ELF file that lists named symbols — functions and data objects — as an array of fixed-size records.

Plain explanation. Think of it as a spreadsheet: one row per symbol, every row the same width. On a 64-bit ELF, each row (Elf64_Sym) is exactly 24 bytes. Because the width is fixed, you can jump to symbol i at byte offset i * 24 without parsing anything before it.

How it works. You do not need a full ELF parser to answer a counting question. Given just the raw .symtab bytes and their length, you can stride through them 24 at a time.

When / when not. Use raw walking when you already have the section bytes and want a fast, self-contained answer. Do not use it to resolve symbol names — the names live in a separate .strtab section, referenced by st_name. That is a different lesson.

Pitfall. Assuming the buffer length is always a clean multiple of 24. Attacker-supplied or truncated files break that assumption; a leftover partial entry means the table is corrupt.

2. The Elf64_Sym layout

Each 24-byte entry:

offset  size  field       meaning
0       4     st_name     index into .strtab (the name)
4       1     st_info     high nibble = binding, low nibble = type
5       1     st_other    visibility
6       2     st_shndx    section index
8       8     st_value    address / value
16      8     st_size     size in bytes

The one byte we care about is st_info at offset 4.

3. Binding vs. type — the packed st_info byte

Definition. st_info packs two 4-bit fields into one byte. The binding (visibility/linkage) is the high nibble; the type (what kind of symbol) is the low nibble.

How it works (bitwise).

binding = st_info >> 4;    // high nibble: LOCAL=0, GLOBAL=1, WEAK=2
type    = st_info & 0x0F;  // low nibble:  NOTYPE=0, OBJECT=1, FUNC=2, ...

We count an entry when binding == 1 (GLOBAL).

When / when not. Shift-right for the high nibble; mask for the low nibble. Do not mix them up — a common bug is to & 0xF when you meant >> 4.

Pitfall. Reading st_info as a signed char. Use unsigned char / uint8_t so the shift is well-defined and you never sign-extend the top bit.

4. Trust boundary and input validation

Definition. The trust boundary is the line between data you control and data you do not. Here it sits at the function parameter: the moment untrusted file bytes enter count_global_symbols, every assumption must be checked, not assumed.

How it works. Three guards: reject NULL with a positive length; accept NULL with zero length as an empty table; reject any length not divisible by 24.

THREAT MODEL — authorized ELF triage

  Untrusted input (an unknown sample)
        |  crafted / truncated .symtab bytes
        v
  +--------------------- TRUST BOUNDARY ---------------------+
  |  count_global_symbols(buf, n)                            |
  |    ASSET: parser integrity + correct triage result       |
  |    ENTRY POINT: the (buf, n) pair                        |
  |    GUARDS:                                               |
  |      - n % 24 != 0        -> reject (-1)  [corrupt]      |
  |      - buf == NULL, n > 0 -> reject (-1)  [invalid]      |
  |      - buf == NULL, n = 0 -> accept (0)   [empty]        |
  |    READS ONLY: buf[i*24 + 4]  (never past n)            |
  +----------------------------------------------------------+
        |
        v
  Trusted result: a count you can put in a report

Knowledge check.

  1. What asset is protected by the n % 24 != 0 check? (Your parser's memory safety and the correctness of the reported count — a partial trailing entry would otherwise make you read fields that do not exist.)
  2. Where is the trust boundary in this program? (At the count_global_symbols(buf, n) call — everything reachable through buf is attacker-controlled.)
  3. What insecure assumption would cause an out-of-bounds read here? (Assuming the buffer contains a whole number of 24-byte entries, then indexing buf[i*24 + 4] on a truncated final entry.)
  4. Why do this only in an authorized lab? (You are running/parsing an unknown, possibly hostile file; that is only appropriate on systems you own or are explicitly authorized to analyze, ideally isolated.)

Syntax notes

The whole technique is offset arithmetic plus two bitwise ops. Nothing exotic.

#include <stdint.h>   // uint8_t, fixed-width types
#include <stddef.h>   // size_t

#define ELF64_SYM_SIZE   24u   // one Elf64_Sym on disk
#define ST_INFO_OFFSET    4u   // st_info sits at byte 4
#define STB_GLOBAL        1u   // binding value for GLOBAL

/* For entry i, the st_info byte is: */
unsigned char st_info = buf[i * ELF64_SYM_SIZE + ST_INFO_OFFSET];

/* Split the packed byte: */
unsigned binding = (unsigned)st_info >> 4;      // high nibble
unsigned type    = (unsigned)st_info & 0x0Fu;   // low nibble

Key points:

  • Use uint8_t / unsigned char for raw bytes so shifts are defined and never sign-extend.
  • Index by byte with buf[i*24 + 4]; do not cast buf to Elf64_Sym * (alignment risk — see the memory-safety section).
  • Compute the entry count once as n / 24 after you have confirmed n % 24 == 0.

Lesson

Why this matters

Every reverse-engineering tool starts by reading the symbol table out of an ELF binary. This includes nm, objdump, readelf, Ghidra, and radare2.

An ELF file (Executable and Linkable Format) is the standard binary format on Linux. Its symbol table (.symtab) lists the named functions and variables in the program.

The on-disk layout is fixed and predictable:

  • Each entry is exactly 24 bytes.
  • The binding of a symbol lives in the top nibble (the high 4 bits) of the st_info field.

You do not need a full ELF parser to make use of .symtab. To count entries with a given property, you only need to walk the raw bytes.

What an Elf64_Sym looks like

Each 24-byte symbol entry has this layout:

offset  size  field
0       4     st_name
4       1     st_info    <- top 4 bits = binding, low 4 bits = type
5       1     st_other
6       2     st_shndx
8       8     st_value
16      8     st_size

The binding tells you the symbol's visibility:

  • 0 LOCAL
  • 1 GLOBAL (the ones we count)
  • 2 WEAK

The binding is the high nibble of st_info, so:

binding = st_info >> 4;

Your job

Implement:

int count_global_symbols(const uint8_t *buf, size_t n);

Here buf is the raw .symtab bytes and n is its length.

Return the number of GLOBAL-binding entries. Handle these cases:

  • n is not a multiple of 24 -> return -1. An incomplete entry means a corrupt symbol table.
  • buf is NULL and n == 0 -> return 0. An empty table is fine.
  • buf is NULL and n > 0 -> return -1.

Common mistakes

  • Mixing up binding and type. The type is the low nibble; the binding is the high nibble.
  • Forgetting the size check. n must be a multiple of 24.
  • Casting to a struct * and walking with it. This is alignment-unsafe on some hosts. Just index the bytes directly: buf[i*24 + 4].

What this is NOT

  • It is not a name resolver. We never read .strtab here.
  • It is not a relocation walker. .rela.dyn is a separate module.

Code examples

Below: an intentionally weak parser, the hardened version, and a self-test that proves the fix rejects bad input and accepts good input.

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

/* insecure_count.c — DO NOT USE IN PRODUCTION.
 * Two bugs: (a) casts to a struct pointer (alignment UB) and
 * (b) never checks that n is a whole number of entries, so a
 * truncated buffer reads past the end. */
#include <stdint.h>
#include <stddef.h>

struct Elf64_Sym {           /* mirrors the on-disk layout */
    uint32_t st_name;
    uint8_t  st_info;
    uint8_t  st_other;
    uint16_t st_shndx;
    uint64_t st_value;
    uint64_t st_size;
};

int count_global_bad(const uint8_t *buf, size_t n) {
    const struct Elf64_Sym *sym = (const struct Elf64_Sym *)buf; /* alignment UB */
    size_t count = 0;
    for (size_t i = 0; i < n / 24; i++) {        /* ignores a partial trailing entry ... */
        if ((sym[i].st_info >> 4) == 1) count++; /* ... and may read fields OOB */
    }
    return (int)count;   /* no NULL check, no size validation */
}

Why it is dangerous: casting an arbitrary buf to struct Elf64_Sym * is undefined behavior if buf is not suitably aligned, and struct padding is not guaranteed to match the 24-byte on-disk layout. With no n % 24 check, a crafted 23-byte-tail buffer makes the loop math and struct reads inconsistent with reality.

2. SECURE fix

/* secure_count.c */
#include <stdint.h>
#include <stddef.h>

#define ELF64_SYM_SIZE 24u
#define ST_INFO_OFFSET  4u
#define STB_GLOBAL      1u

/* Returns the number of GLOBAL-binding symbols, or -1 on invalid input. */
int count_global_symbols(const uint8_t *buf, size_t n) {
    if (buf == NULL) {
        return (n == 0) ? 0 : -1;      /* empty table ok; NULL with data is invalid */
    }
    if (n % ELF64_SYM_SIZE != 0) {
        return -1;                     /* partial trailing entry => corrupt table */
    }

    size_t entries = n / ELF64_SYM_SIZE;
    int count = 0;
    for (size_t i = 0; i < entries; i++) {
        unsigned char st_info = buf[i * ELF64_SYM_SIZE + ST_INFO_OFFSET];
        unsigned binding = (unsigned)st_info >> 4;   /* high nibble */
        if (binding == STB_GLOBAL) {
            count++;
        }
    }
    return count;
}

3. VERIFY — a self-test that rejects bad input and accepts good input

/* test_count.c — compile: cc -std=c11 -Wall -Wextra -fsanitize=address,undefined \
 *                          test_count.c secure_count.c -o test_count && ./test_count */
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <assert.h>

#define ST_INFO_OFFSET 4u   /* keep the test self-contained */

int count_global_symbols(const uint8_t *buf, size_t n);

/* Build one 24-byte entry with a given st_info at offset 4. */
static void make_entry(uint8_t *dst, uint8_t st_info) {
    for (int i = 0; i < 24; i++) dst[i] = 0;
    dst[ST_INFO_OFFSET] = st_info;
}

int main(void) {
    /* GOOD input: 3 entries — GLOBAL, LOCAL, GLOBAL => expect 2 */
    uint8_t good[72];
    make_entry(good +  0, 0x10); /* binding=1 GLOBAL, type=0 */
    make_entry(good + 24, 0x02); /* binding=0 LOCAL,  type=2 FUNC */
    make_entry(good + 48, 0x12); /* binding=1 GLOBAL, type=2 FUNC */
    assert(count_global_symbols(good, sizeof good) == 2);

    /* ACCEPT edge: empty table */
    assert(count_global_symbols(NULL, 0) == 0);
    assert(count_global_symbols(good, 0) == 0);

    /* REJECT: length not a multiple of 24 (truncated final entry) */
    assert(count_global_symbols(good, 72 - 1) == -1);
    assert(count_global_symbols(good, 23) == -1);

    /* REJECT: NULL with positive length */
    assert(count_global_symbols(NULL, 24) == -1);

    puts("all checks passed");
    return 0;
}

Expected output: all checks passed. Note the test #defines its own ST_INFO_OFFSET 4u so it stays self-contained; you could equally write dst[4] directly. Running under -fsanitize=address,undefined is the point: if any guard were missing, the truncated-length or NULL cases would trip the sanitizer instead of silently returning garbage.

Line by line

Walkthrough of count_global_symbols on the good buffer (3 entries, n = 72):

  1. if (buf == NULL)buf is a real 72-byte array, so this is false; we skip the NULL handling.
  2. if (n % 24 != 0)72 % 24 == 0, so the table is a whole number of entries; we continue.
  3. entries = n / 2472 / 24 = 3. We will loop exactly three times and never touch a byte past index 71.
  4. count = 0 — running tally starts empty.
  5. Loop iteration by iteration:
i byte read: buf[i*24 + 4] st_info (hex) binding = st_info >> 4 GLOBAL? count
0 buf[4] 0x10 0x10 >> 4 = 1 yes 1
1 buf[28] 0x02 0x02 >> 4 = 0 no 1
2 buf[52] 0x12 0x12 >> 4 = 1 yes 2
  1. Loop ends; return count; returns 2.

Why the shift gives the binding: 0x12 is 0001 0010 in binary. >> 4 slides the top nibble down: 0000 0001 = 1 = GLOBAL. The low nibble (0010 = 2 = FUNC) is discarded — that is the type, which this function ignores. For the rejection cases, step 2's n % 24 != 0 returns -1 before any indexing happens, so a truncated buffer can never cause an out-of-bounds read.

Common mistakes

1. Mixing up binding and type.

  • WRONG: if ((st_info & 0x0F) == 1) count++; to find GLOBAL.
  • WHY WRONG: & 0x0F extracts the low nibble (the type). GLOBAL binding lives in the high nibble.
  • CORRECTED: if ((st_info >> 4) == 1) count++;
  • RECOGNISE/PREVENT: name intermediates binding and type explicitly; write a quick assert that 0x12 yields binding 1 and type 2.

2. Skipping the size validation.

  • WRONG: loop for (i = 0; i < n/24; i++) with no n % 24 check.
  • WHY WRONG: integer division silently drops a partial entry, so a corrupt/truncated table is treated as valid and your report is wrong; combined with a struct cast it can read out of bounds.
  • CORRECTED: return -1 when n % 24 != 0 before looping.
  • RECOGNISE/PREVENT: test with n = 23 and n = 25; both must return -1.

3. Casting the buffer to Elf64_Sym *.

  • WRONG: const Elf64_Sym *s = (const Elf64_Sym *)buf;
  • WHY WRONG: undefined behavior if buf is misaligned, and compiler padding may not match the 24-byte on-disk layout.
  • CORRECTED: index raw bytes: buf[i*24 + 4].
  • RECOGNISE/PREVENT: run under -fsanitize=undefined; alignment violations are reported.

4. Reading st_info as signed char.

  • WRONG: char info = buf[...]; binding = info >> 4;
  • WHY WRONG: a byte like 0x80 becomes negative; shifting a negative value right is implementation-defined and can sign-extend.
  • CORRECTED: use unsigned char / uint8_t and cast to unsigned before shifting.
  • RECOGNISE/PREVENT: prefer uint8_t buffers everywhere; enable -Wsign-conversion.

5. Confusing NULL-with-zero and NULL-with-data.

  • WRONG: if (buf == NULL) return -1; for all NULL cases.
  • WHY WRONG: an empty table is legitimately (NULL, 0) and should return 0, not an error.
  • CORRECTED: return (n == 0) ? 0 : -1;

Debugging tips

Common errors and how to chase them.

  • The count is always the total number of entries. You are probably testing binding == 1 against the wrong nibble, or your test data has GLOBAL in every entry. Print each st_info in hex and the derived binding; confirm they differ across entries.
  • Segfault or ASan out-of-bounds on some files. You are indexing past n. Re-check the guard order: validate n % 24 and NULL before the loop, and confirm the loop bound is n / 24, not n.
  • Works on your build, crashes elsewhere. Classic sign of the struct-cast alignment bug. Switch to byte indexing and re-test under -fsanitize=address,undefined.
  • -1 returned for a file you believe is valid. The section you passed may include padding or may not be the real .symtab. Confirm the byte length with readelf -S <file> (look at the .symtab size) and check it is a multiple of 24 before blaming the parser.

Questions to ask when it fails.

  1. Is n exactly the .symtab size, or did I accidentally pass the whole file / a padded slice?
  2. Are my raw bytes uint8_t, and am I casting to unsigned before the shift?
  3. Does the guard run before any indexing?
  4. Did I build with the sanitizers on? If not, turn them on and re-run — the truncated and NULL cases should stay silent (returning -1/0), never trip a sanitizer.

Cross-check with a trusted tool (lab-safe): readelf -s ./yourbinary | grep -c GLOBAL gives an independent count you can compare against. If your number and readelf's disagree, decide which is right by inspecting a few entries with readelf -s.

Memory safety

Memory / UB safety for this parser

  • Never read past n. Every access is buf[i*24 + 4] with i < n/24, and only after n % 24 == 0 is confirmed — so the largest index touched is (n/24 - 1)*24 + 4 = n - 20, comfortably inside the buffer.
  • No struct cast. Byte indexing avoids alignment UB and padding surprises entirely.
  • Unsigned bytes only. uint8_t / unsigned char keep the shift well-defined.
  • No allocation, no free. This function owns nothing; it only reads a caller-owned, const buffer — so there is no leak or double-free surface.

Security & safety — detection and logging (for the tool that uses this parser)

When you wrap this in a triage pipeline over untrusted samples, log enough to investigate later without leaking sensitive data:

  • Log: timestamp; a sample identifier such as the file's SHA-256 hash (a correlation id); the resource examined (.symtab, its byte length); the security decision and result (accepted, entries=42, global=7 or rejected: length 4097 not a multiple of 24); and the analyst/host that ran it.
  • Never log: the raw file contents or extracted strings verbatim (a sample may embed stolen credentials, tokens, or private keys), full personal data pulled from the binary, or absolute paths that reveal another user's private directory.
  • Signals of abuse / malformed input worth flagging: a spike in -1 rejections (someone feeding truncated/corrupt tables to probe your parser), symbol counts wildly larger than the file size could hold, or the same hash repeatedly triggering a crash path.
  • False positives: a legitimately stripped binary has near-zero global symbols — that is normal for release builds and packed installers, not proof of malice. A -1 can also come from your own slicing bug rather than a hostile file; verify the section length before concluding the sample is corrupt.

Real-world uses

Authorized use case. A malware-analysis team receives thousands of ELF samples a day into an isolated sandbox. Before the expensive steps (disassembly, dynamic execution in a VM), a fast triage pass counts global symbols per sample: near-zero flags a stripped/packed binary for deeper attention, while a rich symbol table lets analysts jump straight to named functions. Doing this in-process — no readelf fork per file — keeps the pipeline fast at scale.

Professional best-practice habits.

Habit Beginner application Advanced application
Validate untrusted input Check n % 24 and NULL before looping Also validate the section is truly .symtab via the section header, and bound the entry count against file size
Least privilege Analyze files as an unprivileged user Run the whole pipeline in a container/VM with no network and read-only mounts
Secure defaults Return -1 on anything unexpected Fail closed: quarantine the sample and alert, rather than "best-effort" parsing
Logging Record accept/reject + count Correlate by file hash, track rejection-rate trends, alert on crash-path repeats
Error handling Distinguish empty (0) from invalid (-1) Surface why it was rejected so analysts can tell corruption from an attack

Authorization reminder. Only parse and (especially) run samples on systems you own or are explicitly authorized to analyze. Keep unknown binaries in an isolated lab (container, disposable VM, or an air-gapped host), never on your daily machine.

Practice tasks

All tasks are lab-only: work on files you created or are authorized to analyze, on an isolated machine.

Beginner 1 — Extract the binding.

  • Objective: implement int symbol_binding(unsigned char info) returning the high nibble (info >> 4).
  • Input/Output: 0x12 -> 1, 0x21 -> 2, 0x00 -> 0.
  • Constraints: no branches; one expression.
  • Hint: a nibble is 4 bits. Concepts: bitwise shift.

Beginner 2 — Extract the type.

  • Objective: implement int symbol_type(unsigned char info) returning the low nibble (info & 0x0F).
  • Input/Output: 0x12 -> 2, 0x2A -> 10, 0xF0 -> 0.
  • Constraints: one expression; use uint8_t.
  • Hint: mask, do not shift. Concepts: bitwise AND.

Intermediate 1 — Count by any binding.

  • Objective: generalize to int count_symbols_with_binding(const uint8_t *buf, size_t n, unsigned want) — count entries whose binding equals want.
  • Requirements: keep the same three input guards (NULL+n, n % 24); return -1 on invalid input.
  • Input/Output: over a 3-entry buffer with bindings 1,0,1 and want=1, return 2.
  • Constraints: byte indexing only; no struct cast. Hint: reuse the offset-4 read. Concepts: validation, bitwise shift.

Intermediate 2 — Type histogram.

  • Objective: fill int counts[16] with how many symbols have each type nibble (0..15); return -1 on invalid input, else 0.
  • Requirements: validate first, then one pass; index the histogram by st_info & 0x0F.
  • Constraints: never index the histogram out of range (the mask guarantees 0..15). Hint: initialise the array to zero. Concepts: bitwise AND, arrays, validation.

Challenge — Robust triage summary with a defensive conclusion.

  • Objective: write int symtab_summary(const uint8_t *buf, size_t n, int *out_total, int *out_global) that validates the buffer, sets *out_total and *out_global, and returns 0 on success or -1 on any invalid input (leaving the outputs untouched on failure).
  • Requirements: full input validation (NULL/n/multiple-of-24); no reads past n; no struct cast. Build a small test that feeds (a) a good multi-entry buffer, (b) a truncated buffer, (c) (NULL, 0), and (d) (NULL, 24), asserting the expected return and outputs. Compile with -std=c11 -Wall -Wextra -fsanitize=address,undefined.
  • Constraints: fail closed — on -1, callers must not read the outputs.
  • Defensive conclusion (remediate + verify): after it works, deliberately remove the n % 24 guard, watch the sanitizer flag the truncated case, then restore the guard and confirm the test passes again. That before/after is your proof the mitigation works.
  • Hint: dereference out_* only after all guards pass. Concepts: pointers, validation, defensive programming, mitigation verification.

Summary

Main concepts. An ELF64 symbol table is an array of fixed 24-byte Elf64_Sym records. The st_info byte at offset 4 packs binding (high nibble) and type (low nibble); GLOBAL binding is value 1. To count global symbols you stride the raw buffer 24 bytes at a time and test buf[i*24 + 4] >> 4 == 1.

Key syntax/commands.

  • binding = buf[i*24 + 4] >> 4; (high nibble)
  • type = buf[i*24 + 4] & 0x0F; (low nibble)
  • Cross-check in the lab: readelf -s <file> | grep -c GLOBAL.
  • Build with guards on: cc -std=c11 -Wall -Wextra -fsanitize=address,undefined.

Common mistakes. Confusing binding (>> 4) with type (& 0x0F); skipping the n % 24 check; casting buf to a struct pointer (alignment UB); reading bytes as signed char; treating (NULL, 0) as an error.

What to remember. Validate before you trust: reject n % 24 != 0 and NULL-with-data, accept the empty table, and only ever read within n. The global-symbol count is a triage signal, not a verdict — a stripped binary is normal, not proof of malice. Do this only on files and machines you are authorized to analyze, in an isolated lab, and log the decision (by file hash) without ever logging the raw sample contents.

Practice with these exercises