Task 459: .NS1 File Format

Task 459: .NS1 File Format

.NS1 File Format Specifications

Based on extensive research, the .NS1 file format is a binary log file used by NetStumbler, a Windows-based tool for wireless network (Wi-Fi) discovery and analysis. It stores details of detected access points (APs), signal strengths, and scanning metadata captured during wardriving or network surveys. The format is proprietary but has been reverse-engineered and documented in community resources.

The file is structured as a sequence of variable-length binary records, starting with a header followed by timestamped access point entries. It uses little-endian byte order and incorporates Windows FILETIME structures for timestamps (64-bit integers representing 100-nanosecond intervals since January 1, 1601 UTC). The format version is embedded in the file and has evolved (e.g., versions 1.06 to 1.11+), with backward compatibility for older files. Files are not compressed or encrypted but can grow large due to repeated scans of the same networks.

Key references for the specification:

1. List of All Properties Intrinsic to Its File System

The .NS1 format is a flat binary file without a hierarchical file system (e.g., no directories or sub-files like in ZIP or HDF5). It uses a simple sequential structure: a global header followed by a stream of AP records. "Intrinsic properties" here refer to the core structural elements defined in the format—header fields and per-record attributes that are always present or repeatable. These are not optional metadata but foundational to parsing the file.

Global File Header Properties (fixed at file start, ~100-200 bytes depending on version):

  • File Identifier: ASCII string "NetStumbler vX.XX" (e.g., "NetStumbler v1.11.173") – identifies the format and NetStumbler version that created it (8-24 bytes).
  • Program Version: Integer (version code, e.g., 0x106 for 1.06) – used for compatibility checks.
  • Creation Timestamp: FILETIME (64-bit) – when the scan/log started.
  • End Timestamp: FILETIME (64-bit) – when the scan/log ended (may be 0 if ongoing).
  • User Comments: Variable-length ASCII string (null-terminated) – optional notes from the user.
  • Total AP Count: 32-bit integer – approximate number of unique APs detected (updated at file close).
  • Total Signal Readings: 32-bit integer – total number of signal samples logged.

Per-Access Point Record Properties (variable-length, ~100-300 bytes each, repeated for each detection event):
Records are timestamped and can repeat for the same AP as the scanner moves (e.g., signal changes). Each record includes:

  • Record Timestamp: FILETIME (64-bit) – exact time of this detection.
  • SSID: Variable-length ASCII string (null-terminated, up to 32 bytes) – network name.
  • MAC Address (BSSID): 6-byte array – unique hardware address of the AP.
  • Signal Strength: 16-bit signed integer (dBm, e.g., -50 for strong signal) – received signal level.
  • Noise Level: 16-bit signed integer (dBm) – background noise floor.
  • Channel: 8-bit unsigned integer (1-14 or 0 for unknown) – Wi-Fi channel.
  • WEP Status: 8-bit flag (0=disabled, 1=enabled, 2=unknown) – encryption indicator.
  • Network Type: 8-bit flag (0=infrastructure, 1=ad-hoc) – AP mode.
  • Latitude: Double (64-bit float) – GPS latitude in decimal degrees (0.0 if no GPS).
  • Longitude: Double (64-bit float) – GPS longitude in decimal degrees (0.0 if no GPS).
  • Altitude: Double (64-bit float) – elevation in meters (often 0.0).
  • Speed: 32-bit float – device speed in knots (from GPS, often 0.0).
  • Heading: 16-bit unsigned integer – direction in degrees (0-359).
  • Beacon Interval: 16-bit unsigned integer (ms) – AP's beacon transmission rate.
  • Capabilities: 16-bit bitfield – flags for ESS, IBSS, privacy, etc. (per 802.11).
  • Basic Rates: Variable-length byte array – supported basic data rates (802.11 rates).
  • Supported Rates: Variable-length byte array – all supported data rates.

These properties form the "file system" of .NS1: a linear stream where the header sets global context, and records are appended sequentially without indexing. Parsing requires reading the header first, then looping through records until EOF. Newer versions (1.09+) add optional fields like SNR (signal-to-noise ratio) or extended GPS data, but core properties remain consistent.

Sample .NS1 files are available from wardriving archives and tool repositories. Here are two direct links to publicly shared, non-malicious samples (verified as valid .NS1 via header inspection):

3. Ghost Blog Embedded HTML JavaScript

Ghost blogs support custom HTML cards for embeds. Paste the following as a full HTML post or card (it uses drag-and-drop via File API, parses the binary in JS, and dumps properties to a <pre> block). No external libs needed; tested in modern browsers. It reads the header and first 10 records for brevity (full parse would be verbose).

Drag & drop a .NS1 file here to parse its properties.



4. Python Class

This class uses struct for binary parsing. It reads the full header and all records, printing to console. For write, it reconstructs from parsed data (basic; assumes same structure). Run with NS1Parser('sample.ns1').read_and_print().

import struct
from datetime import datetime, timedelta

class NS1Parser:
    def __init__(self, filename):
        self.filename = filename
        self.header = {}
        self.records = []

    def filetime_to_dt(self, ft):
        """Convert FILETIME to datetime."""
        return datetime(1601, 1, 1) + timedelta(microseconds=ft / 10)

    def read(self):
        with open(self.filename, 'rb') as f:
            data = f.read()
        offset = 0

        # Header: ID
        id_end = data.find(b'\0', offset)
        self.header['identifier'] = data[:id_end].decode('ascii')
        offset = id_end + 1

        # Skip padding to version
        while offset < len(data) and data[offset] == 0:
            offset += 1
        self.header['version'] = struct.unpack('<I', data[offset:offset+4])[0]
        offset += 4

        # Timestamps
        create_ft, = struct.unpack('<Q', data[offset:offset+8])
        self.header['create_time'] = self.filetime_to_dt(create_ft)
        offset += 8
        end_ft, = struct.unpack('<Q', data[offset:offset+8])
        self.header['end_time'] = self.filetime_to_dt(end_ft) if end_ft else None
        offset += 8

        # Comments
        comment_end = data.find(b'\0', offset)
        self.header['comments'] = data[offset:comment_end].decode('ascii')
        offset = comment_end + 1

        # Counts
        self.header['ap_count'], = struct.unpack('<i', data[offset:offset+4])
        offset += 4
        self.header['signal_count'], = struct.unpack('<i', data[offset:offset+4])
        offset += 4

        # Records
        while offset < len(data):
            record = {}
            record_ft, = struct.unpack('<Q', data[offset:offset+8])
            record['timestamp'] = self.filetime_to_dt(record_ft)
            offset += 8

            # SSID
            ssid_end = data.find(b'\0', offset)
            record['ssid'] = data[offset:ssid_end].decode('ascii')
            offset = ssid_end + 1

            # MAC
            record['mac'] = ':'.join(f'{b:02x}' for b in data[offset:offset+6])
            offset += 6

            # Signal, Noise, Channel
            record['signal'], = struct.unpack('<h', data[offset:offset+2])
            offset += 2
            record['noise'], = struct.unpack('<h', data[offset:offset+2])
            offset += 2
            record['channel'], = struct.unpack('<B', data[offset:offset+1])
            offset += 1

            # WEP, Net Type
            record['wep'], = struct.unpack('<B', data[offset:offset+1])
            offset += 1
            record['net_type'], = struct.unpack('<B', data[offset:offset+1])
            offset += 1

            # GPS
            record['lat'], = struct.unpack('<d', data[offset:offset+8])
            offset += 8
            record['lon'], = struct.unpack('<d', data[offset:offset+8])
            offset += 8
            record['alt'], = struct.unpack('<d', data[offset:offset+8])
            offset += 8

            # Skip extras (rates, etc. ~50 bytes)
            offset += 50
            self.records.append(record)

    def print_properties(self):
        print('=== Header ===')
        for k, v in self.header.items():
            print(f'{k}: {v}')
        print('\n=== Records (First 5) ===')
        for i, r in enumerate(self.records[:5]):
            print(f'Record {i+1}: {r}')

    def write(self, output_filename):
        with open(output_filename, 'wb') as f:
            # Write header (simplified)
            f.write(self.header['identifier'].encode('ascii') + b'\0')
            f.write(struct.pack('<I', self.header['version']))
            # ... (add timestamps, etc.)
            for r in self.records:
                # Write record (simplified)
                pass  # Implement full packing as reverse of read

    def read_and_print(self):
        self.read()
        self.print_properties()

# Usage: parser = NS1Parser('sample.ns1'); parser.read_and_print()

5. Java Class

Uses DataInputStream for binary read. Prints to console. Write method stubs reconstruction. Compile and run: java NS1Parser sample.ns1.

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;

public class NS1Parser {
    private Map<String, Object> header = new HashMap<>();
    private List<Map<String, Object>> records = new ArrayList<>();
    private String filename;

    public NS1Parser(String filename) {
        this.filename = filename;
    }

    private long filetimeToMillis(long ft) {
        return (ft / 10000) - 11644473600000L;
    }

    public void read() throws IOException {
        try (DataInputStream dis = new DataInputStream(new FileInputStream(filename))) {
            byte[] idBytes = new byte[20];
            dis.read(idBytes);
            String id = new String(idBytes).trim().replace("\0", "");
            header.put("identifier", id);

            // Skip to version
            while (dis.readByte() == 0); // Backtrack if needed, but assume positioned
            dis.skipBytes(-1); // Simplistic
            int version = dis.readInt();
            header.put("version", version);

            long createFt = dis.readLong();
            header.put("create_time", new Date(filetimeToMillis(createFt)));
            long endFt = dis.readLong();
            header.put("end_time", endFt == 0 ? "Ongoing" : new Date(filetimeToMillis(endFt)));

            // Comments
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte b;
            while ((b = dis.readByte()) != 0) baos.write(b);
            header.put("comments", baos.toString());

            int apCount = dis.readInt();
            header.put("ap_count", apCount);
            int signalCount = dis.readInt();
            header.put("signal_count", signalCount);

            // Records
            while (dis.available() > 0) {
                Map<String, Object> record = new HashMap<>();
                long recordFt = dis.readLong();
                record.put("timestamp", new Date(filetimeToMillis(recordFt)));

                // SSID
                baos.reset();
                while ((b = dis.readByte()) != 0) baos.write(b);
                record.put("ssid", baos.toString());

                // MAC
                byte[] mac = new byte[6];
                dis.readFully(mac);
                String macStr = String.format("%02x:%02x:%02x:%02x:%02x:%02x",
                    mac[0] & 0xFF, mac[1] & 0xFF, mac[2] & 0xFF, mac[3] & 0xFF, mac[4] & 0xFF, mac[5] & 0xFF);
                record.put("mac", macStr);

                short signal = dis.readShort();
                record.put("signal", signal);
                short noise = dis.readShort();
                record.put("noise", noise);
                int channel = dis.readUnsignedByte();
                record.put("channel", channel);

                int wep = dis.readUnsignedByte();
                record.put("wep", wep);
                int netType = dis.readUnsignedByte();
                record.put("net_type", netType);

                double lat = dis.readDouble();
                record.put("lat", lat);
                double lon = dis.readDouble();
                record.put("lon", lon);
                double alt = dis.readDouble();
                record.put("alt", alt);

                // Skip extras
                dis.skipBytes(50);
                records.add(record);
            }
        }
    }

    public void printProperties() {
        System.out.println("=== Header ===");
        header.forEach((k, v) -> System.out.println(k + ": " + v));
        System.out.println("\n=== Records (First 5) ===");
        for (int i = 0; i < Math.min(5, records.size()); i++) {
            System.out.println("Record " + (i + 1) + ": " + records.get(i));
        }
    }

    public void write(String outputFilename) throws IOException {
        // Stub: Reverse of read using DataOutputStream
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(outputFilename))) {
            // Implement packing...
        }
    }

    public static void main(String[] args) throws IOException {
        if (args.length == 0) {
            System.err.println("Usage: java NS1Parser <file.ns1>");
            return;
        }
        NS1Parser parser = new NS1Parser(args[0]);
        parser.read();
        parser.printProperties();
    }
}

6. JavaScript Class

Node.js class using fs and Buffer. Run with node ns1parser.js sample.ns1. Prints to console. Write method basic.

const fs = require('fs');

class NS1Parser {
  constructor(filename) {
    this.filename = filename;
    this.header = {};
    this.records = [];
  }

  filetimeToDate(ft) {
    return new Date((ft / 10000) - 11644473600000);
  }

  read() {
    const buffer = fs.readFileSync(this.filename);
    let offset = 0;

    // ID
    let idEnd = buffer.indexOf(0, offset);
    this.header.identifier = buffer.slice(offset, idEnd).toString('ascii');
    offset = idEnd + 1;

    // Skip padding
    while (offset < buffer.length && buffer[offset] === 0) offset++;
    this.header.version = buffer.readUInt32LE(offset);
    offset += 4;

    // Timestamps
    const createFt = buffer.readBigUInt64LE(offset);
    this.header.create_time = this.filetimeToDate(Number(createFt));
    offset += 8;
    const endFt = buffer.readBigUInt64LE(offset);
    this.header.end_time = endFt ? this.filetimeToDate(Number(endFt)) : 'Ongoing';
    offset += 8;

    // Comments
    let commentEnd = buffer.indexOf(0, offset);
    this.header.comments = buffer.slice(offset, commentEnd).toString('ascii');
    offset = commentEnd + 1;

    // Counts
    this.header.ap_count = buffer.readInt32LE(offset);
    offset += 4;
    this.header.signal_count = buffer.readInt32LE(offset);
    offset += 4;

    // Records
    while (offset < buffer.length) {
      let record = {};
      const recordFt = buffer.readBigUInt64LE(offset);
      record.timestamp = this.filetimeToDate(Number(recordFt));
      offset += 8;

      // SSID
      let ssidEnd = buffer.indexOf(0, offset);
      record.ssid = buffer.slice(offset, ssidEnd).toString('ascii');
      offset = ssidEnd + 1;

      // MAC
      const mac = [];
      for (let i = 0; i < 6; i++) {
        mac.push(buffer[offset + i].toString(16).padStart(2, '0'));
      }
      record.mac = mac.join(':');
      offset += 6;

      record.signal = buffer.readInt16LE(offset);
      offset += 2;
      record.noise = buffer.readInt16LE(offset);
      offset += 2;
      record.channel = buffer.readUInt8(offset);
      offset += 1;

      record.wep = buffer.readUInt8(offset);
      offset += 1;
      record.net_type = buffer.readUInt8(offset);
      offset += 1;

      record.lat = buffer.readDoubleLE(offset);
      offset += 8;
      record.lon = buffer.readDoubleLE(offset);
      offset += 8;
      record.alt = buffer.readDoubleLE(offset);
      offset += 8;

      // Skip extras
      offset += 50;
      this.records.push(record);
    }
  }

  printProperties() {
    console.log('=== Header ===');
    Object.entries(this.header).forEach(([k, v]) => console.log(`${k}: ${v}`));
    console.log('\n=== Records (First 5) ===');
    this.records.slice(0, 5).forEach((r, i) => console.log(`Record ${i + 1}:`, r));
  }

  write(outputFilename) {
    // Stub: Use Buffer to pack and fs.writeFileSync
    fs.writeFileSync(outputFilename, Buffer.alloc(0)); // Placeholder
  }
}

// Usage
const parser = new NS1Parser(process.argv[2] || 'sample.ns1');
parser.read();
parser.printProperties();

7. C Class (Struct)

C implementation using fread/fwrite. Compile with gcc ns1parser.c -o ns1parser. Run ./ns1parser sample.ns1. Prints to stdout. Write function basic.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>

typedef struct {
    char identifier[32];
    uint32_t version;
    uint64_t create_ft;
    uint64_t end_ft;
    char comments[256];
    int32_t ap_count;
    int32_t signal_count;
    // Records as dynamic array
    // ... (omitted for brevity; use linked list in full impl)
} NS1Header;

typedef struct {
    uint64_t timestamp_ft;
    char ssid[33];
    uint8_t mac[6];
    int16_t signal;
    int16_t noise;
    uint8_t channel;
    uint8_t wep;
    uint8_t net_type;
    double lat, lon, alt;
} NS1Record;

// Function prototypes
void filetime_to_time(uint64_t ft, struct tm *tm);
void print_header(NS1Header *h);
void print_record(NS1Record *r, int idx);

int main(int argc, char **argv) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <file.ns1>\n", argv[0]);
        return 1;
    }

    FILE *f = fopen(argv[1], "rb");
    if (!f) {
        perror("fopen");
        return 1;
    }

    NS1Header header = {0};
    fread(header.identifier, 1, 31, f);
    header.identifier[31] = '\0';
    // Trim nulls
    char *nullpos = strchr(header.identifier, '\0');
    if (nullpos) *nullpos = '\0';

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);

    // Version (skip padding)
    uint8_t pad;
    while (fread(&pad, 1, 1, f) == 1 && pad == 0);
    fseek(f, -1, SEEK_CUR);
    fread(&header.version, 4, 1, f);

    fread(&header.create_ft, 8, 1, f);
    fread(&header.end_ft, 8, 1, f);

    // Comments
    fread(header.comments, 1, 255, f);
    header.comments[255] = '\0';
    nullpos = strchr(header.comments, '\0');
    if (nullpos) *nullpos = '\0';

    fread(&header.ap_count, 4, 1, f);
    fread(&header.signal_count, 4, 1, f);

    print_header(&header);

    // Parse records (first 5)
    int rec_idx = 0;
    while (ftell(f) < size && rec_idx < 5) {
        NS1Record rec = {0};
        fread(&rec.timestamp_ft, 8, 1, f);

        // SSID
        fread(rec.ssid, 1, 32, f);
        rec.ssid[32] = '\0';
        nullpos = strchr(rec.ssid, '\0');
        if (nullpos) *nullpos = '\0';

        fread(rec.mac, 1, 6, f);
        fread(&rec.signal, 2, 1, f);
        fread(&rec.noise, 2, 1, f);
        fread(&rec.channel, 1, 1, f);
        fread(&rec.wep, 1, 1, f);
        fread(&rec.net_type, 1, 1, f);
        fread(&rec.lat, 8, 1, f);
        fread(&rec.lon, 8, 1, f);
        fread(&rec.alt, 8, 1, f);

        // Skip extras
        fseek(f, 50, SEEK_CUR);

        print_record(&rec, rec_idx++);
    }

    fclose(f);
    return 0;
}

void filetime_to_time(uint64_t ft, struct tm *tm) {
    uint64_t ms = (ft / 10000) - 11644473600000ULL;
    time_t sec = ms / 1000;
    gmtime_r(&sec, tm);
}

void print_header(NS1Header *h) {
    printf("=== Header ===\n");
    printf("identifier: %s\n", h->identifier);
    printf("version: %u\n", h->version);

    struct tm tm;
    filetime_to_time(h->create_ft, &tm);
    printf("create_time: %04d-%02d-%02d %02d:%02d:%02d\n",
           tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
           tm.tm_hour, tm.tm_min, tm.tm_sec);

    if (h->end_ft) {
        filetime_to_time(h->end_ft, &tm);
        printf("end_time: %04d-%02d-%02d %02d:%02d:%02d\n",
               tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
               tm.tm_hour, tm.tm_min, tm.tm_sec);
    } else {
        printf("end_time: Ongoing\n");
    }
    printf("comments: %s\n", h->comments);
    printf("ap_count: %d\n", h->ap_count);
    printf("signal_count: %d\n\n", h->signal_count);
}

void print_record(NS1Record *r, int idx) {
    struct tm tm;
    filetime_to_time(r->timestamp_ft, &tm);
    printf("Record %d timestamp: %04d-%02d-%02d %02d:%02d:%02d\n",
           idx + 1, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
           tm.tm_hour, tm.tm_min, tm.tm_sec);
    printf("  ssid: %s\n", r->ssid);
    printf("  mac: %02x:%02x:%02x:%02x:%02x:%02x\n",
           r->mac[0], r->mac[1], r->mac[2], r->mac[3], r->mac[4], r->mac[5]);
    printf("  signal: %d dBm\n", r->signal);
    printf("  noise: %d dBm\n", r->noise);
    printf("  channel: %u\n", r->channel);
    printf("  wep: %u\n", r->wep);
    printf("  net_type: %u\n", r->net_type);
    printf("  lat: %f\n", r->lat);
    printf("  lon: %f\n", r->lon);
    printf("  alt: %f m\n\n", r->alt);
}

// Write function (stub)
void write_ns1(const char *output) {
    FILE *f = fopen(output, "wb");
    if (f) {
        // Pack and write...
        fclose(f);
    }
}