Kokino reads evidence that someone may have prepared to break forensic tools, and in lab mode it serves several examiners over the network. This page says how to report a problem, how Kokino defends itself, and what has been tested so far, with numbers.
Please write to lab@zeroscience.mk and do not open a public issue.
Helpful to include:
- the Kokino version and build (Help > About), and the operating system;
- what you did and what happened (a crash, a hang, memory use, a wrong result, access you should not have had);
- if a file triggers it, the smallest file you can share. Never send real case data: if the problem only shows on real evidence, describe it and we will work out a way to reproduce it.
We confirm receipt, keep you informed while we fix it, and credit you in the changelog unless you prefer otherwise. Fixes go into the next release; there are no separate patches for older versions.
| Who or what | What we assume | What Kokino does |
|---|---|---|
| Evidence images and the files inside them | Hostile: crafted to crash, hang or exhaust the examiner's machine, or to hide data | Read-only access; every parser bounds memory and time by the size of its input; unsafe Rust is forbidden in every Kokino crate; parsers fuzzed (below) |
| Databases inside evidence (browser history) | Hostile SQL schema (views, triggers) and corrupt pages | SQLite in defensive mode, trusted_schema=OFF, cell size checks, no memory mapping, a time limit per database, views standing in for tables refused |
| BitLocker passwords and recovery passwords | Secret | Used once to find the volume key, never stored or logged; the volume key is kept in the case's keys folder and never returned by the API; every unlock and every failed attempt is in the case audit log |
| Signed-in examiners and reviewers | Honest but limited to their cases and role | Per-case membership and roles enforced on the server; every action in a hash-chained audit log |
| Other machines on the lab network | Untrusted | LAN mode is HTTPS only; sign-in required; CSRF header and Host check on every request |
| Other tabs in the examiner's browser | Untrusted | Strict Content-Security-Policy; HttpOnly, SameSite=Strict session cookies |
| Plugins | Trusted code chosen by whoever runs the server | Off unless the server is started with --plugins <folder>; started only by examiners with write access; given a copy of one file at a time, never the evidence; no input, a reduced environment, a time limit (the whole process tree is stopped) and limits on output; every run audited with the SHA-256 of the plugin's files. A plugin can still do anything the server's account can: install only plugins you have read |
Every parser that reads bytes from evidence was fuzzed with a purpose-built
mutation fuzzer (crates/kokino-fuzz), on stable Rust:
- Inputs: small valid samples of each format (the test fixtures, plus
seeds made by
scripts/make_fuzz_seeds.py), changed by 1 to 8 stacked mutations: bit flips, random bytes, boundary values (0, 0x7F, 0x80, 0xFF, 0x7FFF_FFFF, 0xFFFF_FFFF and so on, little and big endian), filled ranges, copied blocks, truncation, deletion and insertion. Mutations are aimed mostly at the non-zero bytes, where the structures are. - What counts as a failure: a panic (integer overflow included: the fuzz build checks arithmetic, which release builds do not), a stack overflow on a 2 MiB stack (the size of the server's worker threads), one allocation over 256 MiB or more than 1 GiB in use from a small input, and any input that takes more than 20 seconds.
- Reproducible: each input is a pure function of (run seed, input
number), so every failure is saved and replayed exactly.
crates/kokino-fuzz/regress.txtlists them all, andcargo test -p kokino-fuzzreplays them on every test run.
| Target | What it covers | Round 1 (5 min) | Round 2 (20 min) | Round 3 (10 min) | Inputs in all | Distinct problems found |
|---|---|---|---|---|---|---|
volume |
MBR, extended and GPT partition tables, file system detection | 14,763 | 118,598 | 60,270 | 193,631 | none |
ntfs |
NTFS: boot sector, every MFT record, attributes and attribute lists, data runs, stream reads, free-space bitmap | 59,258 | 90,332 | 43,555 | 193,145 | 3 (hang, memory, panic) |
fat |
FAT12, FAT16, FAT32 and exFAT: boot sectors, FAT chains, directories, deleted entries, long names | 26,305 | 236,537 | 123,757 | 386,599 | 4 (memory, overflow) |
ewf |
E01 (EWF) images: sections, chunk tables, zlib chunks, stored hashes | 1,825 | 24,544 | 16,317 | 42,686 | 1 (overflow) |
registry |
Registry hives (SYSTEM, SOFTWARE, SAM, NTUSER.DAT, Amcache): cells, key and value lists, the whole key tree | 498,617 | 13,864,683 | 8,879,174 | 23,242,474 | 1 (hang) |
evtx |
Windows event logs: chunks, binary XML templates and substitutions | 115,682 | 2,399,729 | 673,160 | 3,188,571 | none |
lnk |
LNK shortcuts and shell item lists | 27,451,670 | 5,205,541 | 14,065,882 | 46,723,093 | none |
prefetch |
Prefetch files, including Windows 10/11 compressed ones | 5,492,226 | 17,137,541 | 4,574,931 | 27,204,698 | none |
jumplist |
Jump Lists: OLE compound files (automatic) and custom destinations | 112,454 | 9,454,092 | 10,774,058 | 20,340,604 | 1 (memory) |
xpress |
LZXPRESS Huffman decompression | 157,035 | 1,262,012 | 902,504 | 2,321,551 | none |
doc |
Text extraction: ZIP (nested), Word, Excel, PowerPoint, OpenDocument, GZIP, PDF | 8,186,311 | 6,616,068 | 6,774,250 | 21,576,629 | 1 (panic) |
photo |
Picture forensics: JPEG decoder, EXIF and thumbnails, quantization tables, PNG, BMP | 167,622 | 2,404,315 | 629,991 | 7,614,505 | 5 (overflow, hang, crash) |
carve |
File carving: header and end detection for every carved format | 748,261 | 5,243,806 | 13,246,883 | 19,238,950 | 1 (panic) |
browser |
Browser history: Chromium and Firefox SQLite databases | 1,467 | 49,437 | 6,129 | 57,033 | none (1 hang found by review) |
strings |
Strings and byte map | 578,058 | 16,647,346 | 4,912,367 | 22,137,771 | none |
| All | 15 targets | 43,611,554 | 80,754,581 | 65,683,228 | 194,461,940 | 17, all fixed |
Each round ran all 15 targets at once, one process each, on an 8-core workstation (Windows 10). Rounds 2 and 3 ran after the fixes from the rounds before them, with new random seeds; two more 5-minute photo rounds followed the JPEG fixes: the first (2,244,470 inputs) found one more crash, fixed, and the second (2,168,107 inputs) found nothing. Those are included in the photo total. Fast targets (LNK, Prefetch, strings) reach millions of inputs a minute; whole-volume targets (NTFS, FAT, EWF, browser databases) parse a full image or database per input and run thousands.
Follow-up rounds after the validation work (VALIDATION.md) added NTFS
decompression and new checks in the NTFS and FAT walkers: a new lznt1
target ran 1,938,929 inputs with no finding, and 10 more minutes each on
ntfs (190,379 inputs) and fat (470,899) found one more crash in the
ntfs crate, number 19 below. That brought the total to 197,062,147 inputs
and 18 distinct problems found by fuzzing.
The ext2/3/4 reader and the Linux artifact parsers were fuzzed before they shipped (in 1.2.6):
| Target | What it covers | Round 1 (10 min) | Round 2 | Inputs in all | Distinct problems found |
|---|---|---|---|---|---|
ext |
ext2, ext3 and ext4: superblock, group descriptors, inodes, extent trees, indirect blocks, directories and their slack, orphan scan, journal copies, block bitmap | 311,223 | 710,567 (25 min) + 310,635 (10 min, after the validation fixes) | 1,332,425 | 1 (memory) |
linuxlogs |
systemd journal files, wtmp/btmp, syslog lines and their dates, TZif time zones and POSIX rules, apt history, DHCP leases, dpkg status, shell command rules | 9,505,147 | 19,436,254 (15 min) | 28,941,401 | 3 (overflow) |
Virtual disks, optical media and Mac disks (1.2.7):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
vdisk |
VHD dynamic and differencing, VHDX (headers, region table, metadata, block table, sector bitmaps), VMDK (descriptor, sparse grain directory and tables, streamOptimized grains and footer); reads across the whole disk | 10 + 20 + 15 min | 203,015 | none (one 20 s stall in round 2 did not reproduce: the input replays in 9 ms; most likely the antivirus scanning the temporary file this target writes) |
optical |
ISO 9660 descriptors, sessions, directory records, Rock Ridge and Joliet; UDF anchor, volume descriptors, partition maps, file set, file entries, allocation descriptors and directory entries | 15 + 15 min | 4,318,442 | 3 (overflow, out-of-range reads) |
mac |
HFS+ volume header, catalog, extents and attributes B-trees, journal scan; APFS checkpoints, object maps, file-system trees, old-node scan; decmpfs with zlib and LZVN | 3 x 15 min | 232,151 | 3 (overflows, out-of-range read) |
Deeper NTFS and Windows (1.2.8):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
ntfs |
again, now that the walk also reads every folder's index slack and $LogFile for names | 10 min | 196,853 | none |
indx |
folder index (INDX) records with their fixups, deleted folders' index roots, $LogFile record pages; the name carver with a fixed folder and with any parent accepted | 10 min | 35,329,154 | none |
usn |
change journal records (versions 2, 3 and 4), split across two buffers as the reader hands them over, grouping into changes, $Max | 10 min | 8,954,455 | none |
shellbags |
BagMRU trees in a hive, every shell item kind (root, drive, folder with extension blocks, network, ZIP, URI, control panel, delegate) | 10 min | 15,785,595 | none |
ese |
ESE databases (seeds written by esent.dll, 4 and 32 KiB pages): header, pages and tags, B+ trees, catalog, every table's records, long values, 7-bit and XPRESS decompression, and the SRUM reader on top | 15 min | 532,995 | none |
registry |
again, as BAM and DAM are read from the SYSTEM hive | 5 min | 26,319,961 | none |
ese |
again, after the check against a real SRUM database: every branch level of deeper B+ trees, with a three-level tree as an extra seed | 10 min | 291,544 | none |
Scheduled tasks (1.2.9):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
task |
task XML (elements, entities, triggers, actions), the TaskCache Actions and DynamicInfo blobs, and TaskCache read from a SOFTWARE hive | 10 min | 19,733,957 | none |
Internet Explorer index.dat (1.2.9):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
iecache |
index.dat (header, hash tables, allocation bitmap, URL, LEAK and REDR records, history titles, cache headers, IE 9 download entries), the conversion of each kind of file into web activity, and records read from free space at every 128-byte boundary | 10 + 10 min | 21,653,615 | 1: a record cut off by the end of the file, with its data offset past the bytes that exist, was sliced out of range (panic); fixed |
Deleted registry keys (1.2.9):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
registry |
again, now also searching every hive bin's free space for deleted keys and values, rebuilding their paths and reading their value lists (the seed hives hold deleted keys) | 10 min | 18,700,883 | none |
Exports, legacy Office text and registry logs (1.3.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
doc |
again, now with Word, Excel and PowerPoint 97-2003, Outlook .msg and RTF seeds (piece tables, shared strings split over CONTINUE records, text atoms, property sets, RTF groups and escapes) | 10 min | 37,498,697 | none |
reglog |
registry transaction logs: base blocks, new-format log entries (Marvin32 checksums, dirty page lists, sequence order across two logs) and old-format dirty sector maps, applied to a dirty hive, then the hive read | 10 min | 58,565,421 | none |
Legacy event logs, email and journald compression (1.3.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
evt |
legacy .evt logs: header, records found at any 4-byte boundary, records wrapping around the end of the buffer, strings, SIDs | 10 min | 30,689,834 | none |
mail |
mbox splitting, headers and encoded words, MIME parts (nested, attached messages), quoted-printable, base64, charsets, and the search text built from messages and their attachments | 10 min | 7,542,816 | none |
journalz |
journald's compressed fields: XZ, LZ4 (Kokino's own decoder) and zstd, from the input and from offsets inside it | 10 min | 45,374,291 | none |
linuxlogs |
again, with a journal holding XZ and LZ4 fields | 10 min | 7,898,202 | none |
Shadow copies, BitLocker, triage collections, VHDX logs, Outlook and macOS (1.3.1):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
zip |
ZIP collections read as file systems (central directory, ZIP64, time fields, Velociraptor's escaped names) and nested image detection | 3 x 10 min | 5,383,514 | 2: a crash on escaped names next to multi-byte characters, and runaway memory from claimed sizes; fixed |
vss |
Shadow copy catalogs, block lists (full, overlay, forwarded blocks) and snapshot reads across the stores | 3 x 10 min | 9,887,067 | 2: integer overflows on offsets near 2^64, and a stack overflow from a catalog listing thousands of stores; fixed |
bde |
BitLocker volume headers, FVE metadata blocks and entries, key protectors, key unwrapping (clear key, startup key files), and the decrypting reader for every method | 3 x 10 min | 4,459,482 | 1: an integer overflow on a boot sector copy offset near 2^64; fixed |
tar |
tar and tar.gz collections (UAC): headers and checksums, base-256 sizes, GNU long names, PAX records, hard links, reads | 10 min | 4,169,869 | none |
vdisk |
again, with a VHDX whose log holds writes to replay (entries, descriptors, data sectors, the circular log) | 10 min | 36,659 | none |
pst |
Outlook PST and OST: header, node and block B-trees, blocks (decoding, inflating), data and subnode trees, heaps, property and table contexts, folders, messages rendered as RFC 822 and parsed back | 2 x 10 min + 5 min | 8,102,508 | 2: a crash on dates outside years 0 to 9999, and a read past the end of a one-byte subnode block; fixed (the third round, with compressed RTF bodies, found nothing) |
plist |
Apple property lists, binary (offset tables, object references, counts, every object type) and XML, every value walked | 10 + 5 min | 1,229,272 | 1: an integer overflow converting a huge date; fixed (found in a first round stopped to fix it and not counted) |
fsevents |
macOS FSEvents files: gzip, pages of versions 1 to 3, records, flags | 10 min | 50,215,572 | none |
webcache |
WebCache (Internet Explorer 10 and 11, old Edge): the ESE database, its Containers table and every container read into web activity | 10 min | 743,157 | none |
mail |
again, with Apple Mail .emlx (byte count, message, flags property list) | 5 min | 4,982,274 | none |
The first vss round ran an optimized build without overflow checks and
found nothing; the same inputs on the fuzz profile (overflow checks on)
found the overflows within seconds, and the stack overflow later. That
round and the first bde round were stopped to fix what they found and are
not counted in the inputs; the rounds after the fixes found nothing.
The vdisk target writes each input to a file (virtual disks are opened
by path), so it runs far fewer inputs than the others.
Watchlist (1.4.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
ioc |
the watchlist parser: indicator kinds, defanged forms, notes, URL hosts, domain and IP comparison | 10 min | 13,628,459 | 1: an IPv4-mapped IPv6 address (::ffff:192.0.2.1) did not match itself, so such an indicator found nothing; fixed (found in a first round stopped to fix it and not counted) |
STIX and MISP watchlists, SRUM app timeline, push and battery tables (1.5.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
ioc |
again, with STIX 2.1 bundles and MISP events as seeds, IP networks, and every value looked up as a hash, file, URL, host, mail field and address | 10 min | 5,076,839 | 1: a crash on a STIX pattern with a multi-byte character just before an object name; fixed (found in a first round stopped to fix it and not counted) |
ese |
again, with the SRUM fixtures holding the App Timeline Provider, push notification and energy tables | 10 min | 272,618 | 1: integer overflows adding up per-program totals (bytes sent, received, read and written, processor cycles, focus and input time) from huge values; fixed (same first round) |
That first round also stopped on a wrong assumption in the fuzzer's own check (an IP network does not "match itself" as an address); the check was corrected and is not counted as a problem.
ext folder names from the journal, JPEGs in two pieces, SRUM long-term energy (1.5.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
ext |
again, with every metadata block the journal holds and older copies of directory blocks parsed for names | 10 min | 235,678 | none |
carve |
again, with the JPEG join: the header walk, sectors classified as JPEG data or not, the search for the second piece and the strict decode check, with a split picture as an extra seed | 10 min | 43,267,718 | none |
ese |
again, with the long-term energy table | 10 min | 192,933 | none |
UDF sparable and virtual partitions, ESXi snapshots (1.5.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
optical |
again, with a sparable partition (sparing table) and a virtual partition (VAT found from the end of the image) as seeds | 10 min | 1,335,667 | none |
vdisk |
again, with ESXi VMFSSPARSE and SESPARSE extents (opened through a descriptor naming them) as seeds | 10 min | 45,992 | none |
Plugins and UDF sessions (1.5.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
plugin |
what a plugin prints (one object with rows and notes, or one row per line) and the patterns that choose its files | 10 min | 32,622,403 | none |
optical |
again, with a CD-R image holding two sessions' VATs (files of the earlier session walked through its own VAT) | 10 min | 1,140,617 | none |
JPEGs in three pieces (1.6.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
carve |
again, with middle pieces kept while looking for the last one and three-piece joins | 10 min | 71,875,548 | none |
PNGs in two pieces and deleted PST items (1.6.0):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
pst |
again, with the unused slots of node and block index pages read for deleted messages | 10 min | 2,133,614 | none |
carve |
again, with the PNG join (chunk walk with CRCs, break points, second piece search) | 10 min | 44,752,447 | none |
ZIP and PDF files in two pieces, deleted PST items in free space (1.6.1):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
carve |
again, with the ZIP join (central directory entries, shifted local headers, break search with inflate and CRC) | 10 min | 44,277,137 | none |
pst |
again, with blocks found by their trailers in free space, orphan messages, subnode matching | 10 min | 4,416,179 | none |
carve |
again, with the PDF join (cross-reference sections, object placement, break search with stream inflate), split ZIP and PDF seeds | 10 min | 9,942,283 | none |
AD1 and AFF4 images (1.6.1):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
ad1 |
AD1 logical images: image header, item tree (loops, damage), metadata records, chunk tables, zlib chunks | short rounds, then 10 min | 5,132,730 | 1 (memory) |
aff4 |
AFF4 containers: ZIP64 directory, Turtle, maps and their indexes, image streams and bevy indexes, snappy and LZ4, symbolic streams, AFF4-L files | short rounds, then 10 min | 10,077,348 | 2 (memory, hang) |
Ex01 and more PDF carving (1.6.2):
| Target | What it covers | Rounds | Inputs in all | Distinct problems found |
|---|---|---|---|---|
ewf |
again, with Ex01 (EWF2): sections read back to front, serialized case and device objects, 16-byte chunk tables, pattern-filled chunks; Ex01 seeds | 10 min | 57,238 | none |
carve |
again, with PDF cross-reference streams (predictors), /Prev and /XRefStm chains, gaps found from sections or objects, DCT pictures checked | 10 min | 9,896,461 | none |
In all: 951,747,137 inputs over 43 targets, and 43 distinct problems found by fuzzing, all fixed.
| # | Parser | Problem | Found by | Cause | Fix |
|---|---|---|---|---|---|
| 1 | NTFS | Hang and runaway memory | Fuzzing | The MFT size declared in $MFT was trusted: a few changed bytes claimed billions of records |
Bounded by the size of the volume |
| 2 | NTFS | Hang | Fuzzing | The data-run iterator of the ntfs crate can return the same error for ever; flatten() kept asking |
Stop at the first bad run, cap runs and attributes per record |
| 3 | NTFS | Crash (panic in the ntfs crate) |
Fuzzing | unreachable!() on a boot sector shorter than 512 bytes |
Length checked first |
| 4 | NTFS | Large allocations | Review (same pattern as 5) | Read buffers sized by the requested or declared length; the free-space bitmap sized by the boot sector | Buffers grow with the bytes read, bitmap bounded by the volume |
| 5 | FAT, exFAT | 16 GB allocation from a 2.6 MB image | Fuzzing | The cluster count from the boot sector sized the in-memory FAT | Capped at the FAT entries really present; reads grow with the data |
| 6 | exFAT | Allocation of up to 128 GiB per cluster | Review (while fixing 5) | Sector and cluster shifts not checked | The specification's limits enforced (512-byte to 4 KiB sectors, clusters up to 32 MiB) |
| 7 | FAT, exFAT | Integer overflow (3 places) | Fuzzing | Offsets and sizes from the boot sector multiplied unchecked | Saturating arithmetic |
| 8 | Jump Lists (OLE) | Over 500 MiB from a 3 KB file | Fuzzing | A DIFAT listing the same sector again and again; sector chains that loop; mini stream cutoff taken from the header | Each sector once per chain, FAT no larger than the file, cutoff fixed at 4096, 256 MiB in all |
| 9 | Registry | Hang | Fuzzing | A key listed under itself made the tree walk grow exponentially | Each key visited once |
| 10 | EWF (E01) | Integer overflow | Fuzzing | Chunk geometry multiplied unchecked | Checked arithmetic, image refused |
| 11 | JPEG | Integer overflow (2 places) | Fuzzing | A DC difference size from the file's Huffman table could reach 255 (valid: 0 to 16) | Refused above 16 |
| 12 | JPEG | Integer overflow | Fuzzing | Coefficient times quantizer on hostile values | Saturating arithmetic |
| 13 | JPEG | 4.6 seconds on a 6.5 KB file | Fuzzing | A second frame header claimed 1.2 gigapixels and the decoder kept decoding padding | Decoding stops 1 KiB past the end of the data (truncated pictures still show what they have) |
| 14 | PDF text | Crash | Fuzzing | A name sliced inside a multi-byte character | Sliced at a character boundary |
| 15 | Carving | Crash | Fuzzing | A 7-byte %PDF-1. read as 8 bytes |
Bounds checked |
| 16 | Browser history | Hang | Review (proof of concept made) | A database can define urls as a never-ending view |
SQLite hardening (threat model above); such databases refused |
| 17 | JPEG | Integer overflow | Review (same as 12) | The same multiplication in the gray-level decoder | Saturating arithmetic |
| 18 | JPEG | Crash | Fuzzing | A scan naming a component that a later frame header removed | Scans checked against the frame; DC plane needs a frame |
| 19 | NTFS | Crash (panic in the ntfs crate) |
Fuzzing (follow-up round) | unreachable!() on a $STANDARD_INFORMATION value shorter than its structure |
Every call into the crate guarded: a bad record is reported as unreadable |
| 20 | ext | 423 MB allocation from an 83 KB image | Fuzzing | A superblock claiming millions of block groups sized the group table and the block bitmap | Both bounded by what the image holds |
| 21 | Time zones (TZif rule) | Integer overflow | Fuzzing | Hours in a POSIX rule string multiplied unchecked | POSIX limits enforced (hours up to 167, minutes and seconds up to 59) |
| 22 | systemd journal | Integer overflow | Fuzzing | Object offsets near the top of the address range added to a header size | Checked arithmetic |
| 23 | systemd journal | Integer overflow | Fuzzing | A data object's size added to its offset | Checked arithmetic |
| 24 | UDF | Integer overflow | Fuzzing | The backup anchor position (image size minus 256 sectors) on images smaller than that | Saturating arithmetic |
| 25 | UDF | Crash (read past the end) | Fuzzing | A file entry cut short at the end of the image was read at its full size | Short entries read as zeros past the end |
| 26 | UDF | Crash (read past the end) | Fuzzing | A short last block of the volume descriptor sequence was read as a full descriptor | The sequence ends at a short block |
| 27 | HFS+ | Integer overflow | Fuzzing | A B-tree node claiming more records than its offset table can hold | The record loop stops before the table runs out |
| 28 | HFS+ | Crash (read past the end) | Fuzzing | A volume header cut short at the end of the image | Short headers refused |
| 29 | APFS | Integer overflow | Fuzzing | A checkpoint address from the superblock multiplied by the block size | Saturating arithmetic for every block address |
| 30 | ZIP collections | Crash (panic) | Fuzzing | A Velociraptor-escaped name (%xx) next to a multi-byte character was cut inside the character |
Escapes decoded byte by byte |
| 31 | ZIP collections | Runaway memory | Fuzzing | Sizes claimed by a damaged ZIP's directory sized the read and inflate buffers | Capped by the file's size; inflated output limited |
| 32 | Volume Shadow Copies | Integer overflow (2 places) | Fuzzing | A catalog or block list offset near 2^64 plus the block size | Checked arithmetic |
| 33 | Volume Shadow Copies | Stack overflow | Fuzzing | A catalog listing thousands of stores made the block lookup recurse once per store | The lookup is a loop; at most 512 stores, Windows' own limit |
| 34 | BitLocker | Integer overflow | Fuzzing | A boot sector copy or metadata offset near 2^64 | Saturating arithmetic |
| 35 | Outlook PST | Crash (panic in chrono) | Fuzzing | A message time far outside years 0 to 9999 was formatted as an RFC 2822 date | Such dates are left out of the rendered headers |
| 36 | Outlook PST | Crash (read past the end) | Fuzzing | A subnode block one byte long was read for its level | Short blocks read as empty |
| 37 | Property lists | Integer overflow | Fuzzing | A date of about 10^30 seconds converted to Windows time | Saturating arithmetic; such dates read as unknown |
| 38 | Watchlist | Wrong result | Fuzzing | Only one side of an IP comparison was unmapped, so an IPv4-mapped IPv6 indicator never matched | Both sides normalized before comparing |
| 39 | Watchlist (STIX) | Crash (slice inside a character) | Fuzzing | The start of an object name was found one byte after a multi-byte character | Character boundaries kept |
| 40 | SRUM | Integer overflow | Fuzzing | Per-program totals added hourly values near 2^63 | Saturating arithmetic |
| 41 | AD1 | Runaway memory | Fuzzing | The chunk size from the image header (up to 4 GB) and a chunk count sized buffers before anything was read | Chunk size limited to 16 MiB, the chunk table to what the image holds, reads grow with the data |
| 42 | AFF4 | Runaway memory | Fuzzing | Member sizes claimed by a damaged ZIP directory sized the read buffers | Reads grow with the bytes really there |
| 43 | AFF4 | Hang | Fuzzing | A map run ending at the top of the 64-bit range left a gap of length 0 that was read for ever; a map naming itself was read again at every level | The read stops there (zeros); every stream is known before it is read, so a map naming itself reads as missing |
Before the fixes, every one of these could stop or slow an ingest, a carving
job or a picture scan, or exhaust the examiner's memory, on purpose-made
evidence. None allowed code execution or wrote to evidence: Kokino has no
unsafe code, so the worst a parser bug could do was crash, hang or use
too much memory.
Alongside the fuzzing, the server code that handles requests and runs jobs was reviewed. Fixed:
- Evidence paths inside the cases directory. An examiner could add a file from the cases directory as evidence (another case's exports or audit log, the account file with its password hashes, the TLS key) and read it in the hex view. Such paths are now refused (symbolic links resolved, letter case ignored on Windows).
- A parser panic left its job "running" for ever, which also blocked deleting the case. Every background job now catches a panic and ends as failed, with a message asking to report it.
- Reports served without a sandbox. Case and evidence reports are HTML
built from evidence text. They were escaped, but now also carry
Content-Security-Policy: default-src 'none'; sandbox, so nothing in a report can run script or act as the signed-in examiner. - SQLite databases from evidence are opened as described in the threat model above.
- BitLocker unlocking (new): the password or recovery password is sent
once in the request body, used to derive the key and dropped. Only the
volume key is written, to
keys/<evidence>.jsonin the case folder, outsidecase.json, so no API response carries it. Reviewers cannot unlock (it is a write). Each attempt costs 2^20 SHA-256 rounds on the server, as it does on Windows.
Checked and found sound: the server folder browser (examiners and admins
only), export file names (no path separators survive),
report file names (a fixed pattern), downloads (nosniff, attachment
names from sanitized parts), session cookies and the CSRF and Host checks.
Each release carries SHA256SUMS.txt, a CycloneDX SBOM listing every
third-party crate in the build (version, package URL, the checksum
Cargo.lock pins, license), and a build provenance attestation for every
file, signed through GitHub's Sigstore integration: it ties the file to
the workflow run and commit that built it. Check a download with
gh attestation verify <file> --repo zeroscience/Kokino. The Windows and
macOS packages are not yet code-signed with a publisher certificate.
- The fuzzer is not coverage-guided and runs without sanitizers (no
libFuzzer or AddressSanitizer, which need nightly Rust). Since Kokino has
no
unsafecode, memory-safety bugs would have to come from a dependency; the fuzzer looks for panics, hangs and runaway memory, which are what safe Rust can still do wrong. - Third-party code is fuzzed only as Kokino uses it: the
ntfscrate through the NTFS target, zlib (flate2) through EWF, ZIP, GZIP and PDF, and SQLite through the browser target (SQLite itself is fuzzed continuously upstream by OSS-Fuzz). - The browser target is slow (each input is written to a temporary database), so it saw far fewer inputs than the others.
- There has been no external penetration test yet.
- In lab mode, examiners can still add any image file the server can read outside the cases directory. Labs that want to restrict this should run Kokino under an account that can read only the evidence storage.
cargo build --profile fuzz -p kokino-fuzz
./target/fuzz/kokino-fuzz list
./target/fuzz/kokino-fuzz run all --secs 600 --seed 4
./target/fuzz/kokino-fuzz run ntfs --secs 3600
./target/fuzz/kokino-fuzz repro ntfs target/fuzz-findings/ntfs/<file>.binFindings land in target/fuzz-findings/<target>/. Add each to
crates/kokino-fuzz/regress.txt once fixed.