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"