libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

gen_lnk_testdata.sh (7096B)


      1 #!/bin/sh
      2 # Regenerate the shell link test file used by src/plugins/test_lnk.c.
      3 #
      4 # Copyright (C) 2026 Christian Grothoff
      5 # The generated file is dedicated to the public domain (CC0 1.0).
      6 #
      7 # The file is assembled from [MS-SHLLINK] rather than produced by
      8 # Windows: that keeps it a few hundred bytes, keeps every timestamp,
      9 # serial number, machine name and MAC address a constant this script
     10 # owns, and lets it carry exactly the structures the plugin parses --
     11 # an ID list to skip over, a LinkInfo with a volume label and a path
     12 # split across its two halves, unicode string data, and a tracker block
     13 # whose object identifiers are version 1 UUIDs.
     14 #
     15 # Needs nothing but python3.
     16 set -e
     17 
     18 srcdir=$(dirname "$0")
     19 outdir=${1:-"$srcdir/../src/plugins/testdata"}
     20 
     21 python3 - "$outdir/lnk_test.lnk" <<'EOF'
     22 import struct
     23 import sys
     24 
     25 OUT = sys.argv[1]
     26 
     27 # ---------------------------------------------------------- constants
     28 CLSID = bytes([0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
     29                0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46])
     30 CREATION = 1614834367            # 2021-03-04T05:06:07Z
     31 ACCESS = 1657271411              # 2022-07-08T09:10:11Z
     32 WRITE = 1623767400               # 2021-06-15T14:30:00Z
     33 TARGET_SIZE = 123456
     34 ATTRIBUTES = 0x0021              # READONLY | ARCHIVE
     35 DRIVE_SERIAL = 0x1A2B3C4D        # reported as 1A2B-3C4D
     36 DRIVE_TYPE = 3                   # fixed disk
     37 VOLUME_LABEL = "EVIDENCE"
     38 BASE_PATH = "C:\\Users\\analyst\\Desktop\\"
     39 PATH_SUFFIX = "evidence.txt"
     40 NAME_STRING = "Evidence file from the lab machine"
     41 RELATIVE_PATH = "..\\..\\Desktop\\evidence.txt"
     42 WORKING_DIR = "C:\\Users\\analyst\\Desktop"
     43 ARGUMENTS = "/quiet /log C:\\temp\\out.log"
     44 ICON_LOCATION = "%SystemRoot%\\system32\\shell32.dll"
     45 MACHINE_ID = b"lab-ws-07"
     46 # 9b3a5e1c-7d42-4f80-a1e6-0c2d4b6f8a31, stored the Microsoft way
     47 DROID_VOLUME = bytes([0x1C, 0x5E, 0x3A, 0x9B, 0x42, 0x7D, 0x80, 0x4F,
     48                       0xA1, 0xE6, 0x0C, 0x2D, 0x4B, 0x6F, 0x8A, 0x31])
     49 # 5f3e2d1c-a4b6-11d2-8ae7-001b44113ab7: a version 1 UUID whose node
     50 # field is the MAC address 00:1b:44:11:3a:b7.  The version nibble lives
     51 # in the high half of byte 7 because the first three fields are stored
     52 # little-endian.
     53 DROID_FILE = bytes([0x1C, 0x2D, 0x3E, 0x5F, 0xB6, 0xA4, 0xD2, 0x11,
     54                     0x8A, 0xE7, 0x00, 0x1B, 0x44, 0x11, 0x3A, 0xB7])
     55 
     56 LNK_HAS_ID_LIST = 0x01
     57 LNK_HAS_LINK_INFO = 0x02
     58 LNK_HAS_NAME = 0x04
     59 LNK_HAS_RELATIVE_PATH = 0x08
     60 LNK_HAS_WORKING_DIR = 0x10
     61 LNK_HAS_ARGUMENTS = 0x20
     62 LNK_HAS_ICON_LOCATION = 0x40
     63 LNK_IS_UNICODE = 0x80
     64 
     65 
     66 def filetime(unix):
     67     """Seconds since the Unix epoch as 100ns units since 1601-01-01."""
     68     return (unix + 11644473600) * 10000000
     69 
     70 
     71 def utf16(text):
     72     """UTF-16LE, NUL terminated."""
     73     return text.encode("utf-16-le") + b"\x00\x00"
     74 
     75 
     76 def ansi(text):
     77     """Single-byte, NUL terminated."""
     78     return text.encode("ascii") + b"\x00"
     79 
     80 
     81 # ------------------------------------------------------ ShellLinkHeader
     82 flags = (LNK_HAS_ID_LIST | LNK_HAS_LINK_INFO | LNK_HAS_NAME
     83          | LNK_HAS_RELATIVE_PATH | LNK_HAS_WORKING_DIR
     84          | LNK_HAS_ARGUMENTS | LNK_HAS_ICON_LOCATION | LNK_IS_UNICODE)
     85 header = struct.pack("<I16sIIQQQIiIHHII",
     86                      0x0000004C,
     87                      CLSID,
     88                      flags,
     89                      ATTRIBUTES,
     90                      filetime(CREATION),
     91                      filetime(ACCESS),
     92                      filetime(WRITE),
     93                      TARGET_SIZE,
     94                      0,                  # IconIndex
     95                      1,                  # ShowCommand: SW_SHOWNORMAL
     96                      0,                  # HotKey
     97                      0, 0, 0)            # Reserved1..3
     98 assert len(header) == 0x4C, hex(len(header))
     99 
    100 # --------------------------------------------------- LinkTargetIDList
    101 # Two shell items and the terminating empty one.  The plugin only has
    102 # to step over this correctly, so the contents are the shortest thing
    103 # that is still shaped like the real structure: the "My Computer"
    104 # folder followed by a drive.
    105 item1 = struct.pack("<BB", 0x1F, 0x50) + bytes(
    106     [0xE0, 0x4F, 0xD0, 0x20, 0xEA, 0x3A, 0x69, 0x10,
    107      0xA2, 0xD8, 0x08, 0x00, 0x2B, 0x30, 0x30, 0x9D])
    108 item1 = struct.pack("<H", len(item1) + 2) + item1
    109 item2 = struct.pack("<B", 0x2F) + b"C:\\" + bytes(10)
    110 item2 = struct.pack("<H", len(item2) + 2) + item2
    111 idlist = item1 + item2 + struct.pack("<H", 0)
    112 
    113 # ----------------------------------------------------------- LinkInfo
    114 vol_label_w = utf16(VOLUME_LABEL)
    115 volume = struct.pack("<IIIII",
    116                      0,                  # VolumeIDSize, filled in below
    117                      DRIVE_TYPE,
    118                      DRIVE_SERIAL,
    119                      0x14,               # use the unicode label instead
    120                      0x14)               # VolumeLabelOffsetUnicode
    121 volume += vol_label_w
    122 volume = struct.pack("<I", len(volume)) + volume[4:]
    123 
    124 base_a = ansi(BASE_PATH)
    125 suffix_a = ansi(PATH_SUFFIX)
    126 base_w = utf16(BASE_PATH)
    127 suffix_w = utf16(PATH_SUFFIX)
    128 
    129 LI_HEADER = 0x24
    130 vol_off = LI_HEADER
    131 base_a_off = vol_off + len(volume)
    132 suffix_a_off = base_a_off + len(base_a)
    133 base_w_off = suffix_a_off + len(suffix_a)
    134 base_w_off += base_w_off % 2                     # keep UTF-16 aligned
    135 suffix_w_off = base_w_off + len(base_w)
    136 li_size = suffix_w_off + len(suffix_w)
    137 
    138 link_info = struct.pack("<IIIIIIIII",
    139                         li_size,
    140                         LI_HEADER,
    141                         0x1,             # VolumeIDAndLocalBasePath
    142                         vol_off,
    143                         base_a_off,
    144                         0,               # no network link
    145                         suffix_a_off,
    146                         base_w_off,
    147                         suffix_w_off)
    148 link_info += volume + base_a + suffix_a
    149 link_info += bytes(base_w_off - len(link_info))
    150 link_info += base_w + suffix_w
    151 assert len(link_info) == li_size, (len(link_info), li_size)
    152 
    153 # --------------------------------------------------------- StringData
    154 def string_data(text):
    155     """A counted, *not* NUL terminated UTF-16LE string."""
    156     raw = text.encode("utf-16-le")
    157     return struct.pack("<H", len(raw) // 2) + raw
    158 
    159 
    160 strings = (string_data(NAME_STRING)
    161            + string_data(RELATIVE_PATH)
    162            + string_data(WORKING_DIR)
    163            + string_data(ARGUMENTS)
    164            + string_data(ICON_LOCATION))
    165 
    166 # ---------------------------------------------------------- ExtraData
    167 tracker = struct.pack("<IIII",
    168                       0x60,              # BlockSize
    169                       0xA0000003,        # TrackerDataBlock
    170                       0x58,              # Length
    171                       0)                 # Version
    172 tracker += MACHINE_ID.ljust(16, b"\x00")
    173 tracker += DROID_VOLUME + DROID_FILE     # Droid
    174 tracker += DROID_VOLUME + DROID_FILE     # DroidBirth
    175 assert len(tracker) == 0x60, hex(len(tracker))
    176 
    177 extra = tracker + struct.pack("<I", 0)   # terminal block
    178 
    179 image = (header
    180          + struct.pack("<H", len(idlist)) + idlist
    181          + link_info
    182          + strings
    183          + extra)
    184 
    185 with open(OUT, "wb") as f:
    186     f.write(image)
    187 EOF
    188 
    189 ls -l "$outdir/lnk_test.lnk"