gen_pecoff_testdata.sh (11136B)
1 #!/bin/sh 2 # Regenerate the PE/COFF test images used by src/plugins/test_pecoff.c. 3 # 4 # Copyright (C) 2026 Christian Grothoff 5 # The generated files are dedicated to the public domain (CC0 1.0). 6 # 7 # Two files are produced: 8 # 9 # pecoff_test.exe -- a real x86-64 image built with mingw-w64. It is 10 # linked without the C runtime so that its import table has a 11 # single entry, which makes the import hash something the test can 12 # spell out. Needs the Debian package `gcc-mingw-w64-x86-64'; if 13 # the compiler is missing the existing file is left alone. 14 # 15 # pecoff_test.dll -- assembled here with python3 from the format 16 # specification. mingw cannot produce a Rich header (only 17 # Microsoft's linker writes one) or a CodeView debug record with a 18 # fixed GUID, and those are the two artifacts worth testing most. 19 # 20 # Both files are byte-for-byte reproducible: every timestamp is a 21 # constant in this script and the entropy filler is derived from SHA-256 22 # of a fixed counter. 23 set -e 24 25 srcdir=$(dirname "$0") 26 outdir=${1:-"$srcdir/../src/plugins/testdata"} 27 work=$(mktemp -d) 28 trap 'rm -rf "$work"' 0 29 30 # The build timestamp stamped into pecoff_test.exe: 2023-11-14T22:13:20Z. 31 EXE_TIMESTAMP=1700000000 32 33 CC=x86_64-w64-mingw32-gcc 34 RC=x86_64-w64-mingw32-windres 35 36 if ! command -v "$CC" >/dev/null 2>&1; then 37 echo "$CC not found (apt-get install gcc-mingw-w64-x86-64);" \ 38 "keeping the existing pecoff_test.exe" >&2 39 else 40 cat > "$work/pecoff_test.c" <<'EOF' 41 /* Placed in the public domain (CC0 1.0). */ 42 __declspec(dllimport) void __stdcall ExitProcess (unsigned int code); 43 44 void mainCRTStartup (void); 45 46 void 47 mainCRTStartup (void) 48 { 49 ExitProcess (0); 50 } 51 EOF 52 cat > "$work/pecoff_test.rc" <<'EOF' 53 1 VERSIONINFO 54 FILEVERSION 1,2,3,4 55 PRODUCTVERSION 5,6,7,8 56 FILEFLAGSMASK 0x3fL 57 FILEFLAGS 0x0L 58 FILEOS 0x40004L 59 FILETYPE 0x1L 60 FILESUBTYPE 0x0L 61 BEGIN 62 BLOCK "StringFileInfo" 63 BEGIN 64 BLOCK "040904b0" 65 BEGIN 66 VALUE "CompanyName", "GNU libextractor" 67 VALUE "FileDescription", "libextractor PE test binary" 68 VALUE "FileVersion", "1.2.3.4" 69 VALUE "InternalName", "pecoff_test" 70 VALUE "LegalCopyright", "CC0 1.0 Universal" 71 VALUE "OriginalFilename", "pecoff_test.exe" 72 VALUE "ProductName", "GNU libextractor test suite" 73 VALUE "ProductVersion", "5.6.7.8" 74 END 75 END 76 BLOCK "VarFileInfo" 77 BEGIN 78 VALUE "Translation", 0x409, 1200 79 END 80 END 81 EOF 82 "$RC" "$work/pecoff_test.rc" -O coff -o "$work/pecoff_test_res.o" 83 "$CC" -Os -nostdlib -nostartfiles \ 84 -Wl,-e,mainCRTStartup -Wl,--no-insert-timestamp \ 85 -Wl,--dynamicbase -Wl,--nxcompat -Wl,--high-entropy-va \ 86 -s -o "$work/pecoff_test.exe" \ 87 "$work/pecoff_test.c" "$work/pecoff_test_res.o" -lkernel32 88 # The linker was told to leave TimeDateStamp at zero so that the build 89 # is reproducible; put a fixed date there instead, since a plausible 90 # build date is one of the things the plugin reports. 91 python3 - "$work/pecoff_test.exe" "$EXE_TIMESTAMP" <<'EOF' 92 import struct 93 import sys 94 95 path, stamp = sys.argv[1], int(sys.argv[2]) 96 with open(path, "rb") as f: 97 image = bytearray(f.read()) 98 lfanew = struct.unpack_from("<I", image, 0x3C)[0] 99 assert image[lfanew:lfanew + 4] == b"PE\x00\x00" 100 struct.pack_into("<I", image, lfanew + 8, stamp) 101 with open(path, "wb") as f: 102 f.write(image) 103 EOF 104 cp "$work/pecoff_test.exe" "$outdir/pecoff_test.exe" 105 chmod 644 "$outdir/pecoff_test.exe" 106 fi 107 108 python3 - "$outdir/pecoff_test.dll" <<'EOF' 109 """Assemble a minimal PE32+ DLL carrying the artifacts MSVC leaves 110 behind: a Rich header, a CodeView record naming the PDB, an export 111 directory, a certificate table and a high-entropy section.""" 112 import hashlib 113 import struct 114 import sys 115 116 OUT = sys.argv[1] 117 118 TIMESTAMP = 1600000000 # 2020-09-13T12:26:40Z 119 RICH_KEY = 0x1A2B3C4D 120 # (product id, build number, use count), as the linker records them 121 RICH_ENTRIES = [(0x0104, 30729, 34), 122 (0x00FF, 30729, 12), 123 (0x0105, 30729, 1), 124 (0x0001, 0, 5)] 125 PDB_PATH = (b"C:\\Users\\builder\\source\\repos\\pecoff_test" 126 b"\\x64\\Release\\pecoff_test.pdb\x00") 127 CV_GUID = bytes([0x2A, 0x1E, 0x7B, 0x4C, # Data1 = 4C7B1E2A 128 0x3D, 0x9F, # Data2 = 9F3D 129 0x5C, 0x4B, # Data3 = 4B5C 130 0x8E, 0x6F, 0x0A, 0x1B, 0x2C, 0x3D, 0x4E, 0x5F]) 131 CV_AGE = 7 132 SIGNER_CN = b"GNU libextractor test signer" 133 EXPORT_NAMES = [b"ForensicEntryA", b"ForensicEntryB", b"ForensicEntryC"] 134 135 SEC_ALIGN = 0x1000 136 FILE_ALIGN = 0x200 137 E_LFANEW = 0x100 138 139 # ------------------------------------------------------------ DOS stub 140 dos = bytearray(64) 141 dos[0:16] = bytes([0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 142 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00]) 143 dos[16:32] = bytes([0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 144 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) 145 dos[0x3C:0x40] = struct.pack("<I", E_LFANEW) 146 147 stub = bytes([0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 148 0x21, 0xB8, 0x01, 0x4C, 0xCD, 0x21]) 149 stub += b"This program cannot be run in DOS mode.\r\r\n$" 150 stub = stub.ljust(64, b"\x00") 151 152 # --------------------------------------------------------- Rich header 153 words = [0x536E6144, 0, 0, 0] # 'DanS' plus three pad dwords 154 for prod, build, uses in RICH_ENTRIES: 155 words.append((prod << 16) | build) 156 words.append(uses) 157 rich = b"".join(struct.pack("<I", w ^ RICH_KEY) for w in words) 158 rich += b"Rich" + struct.pack("<I", RICH_KEY) 159 rich = rich.ljust(E_LFANEW - 0x80, b"\x00") 160 assert len(dos) + len(stub) + len(rich) == E_LFANEW 161 162 # ------------------------------------------------------------ sections 163 TEXT_RVA, TEXT_OFF, TEXT_SIZE = 0x1000, 0x400, 0x200 164 RDATA_RVA, RDATA_OFF, RDATA_SIZE = 0x2000, 0x600, 0x200 165 PACK_RVA, PACK_OFF, PACK_SIZE = 0x3000, 0x800, 0x400 166 CERT_OFF, CERT_SIZE = 0xC00, 0x200 167 168 text = b"\x48\x31\xC0\xC3".ljust(TEXT_SIZE, b"\x00") 169 170 # .pack stands in for a packed or encrypted section: bytes with no 171 # structure, but derived from a counter so the file never changes. 172 pack = b"".join(hashlib.sha256(b"libextractor-pecoff-%d" % i).digest() 173 for i in range(PACK_SIZE // 32)) 174 assert len(pack) == PACK_SIZE 175 176 rd = bytearray(RDATA_SIZE) 177 EXP_DIR, FUNCS, NAMES, ORDS = 0x000, 0x028, 0x034, 0x040 178 DLLNAME, NAME0, DBG_DIR, RSDS = 0x048, 0x058, 0x088, 0x0A8 179 180 struct.pack_into("<IIHHIIIIIII", rd, EXP_DIR, 181 0, # Characteristics 182 TIMESTAMP, # TimeDateStamp 183 0, 0, # Major/MinorVersion 184 RDATA_RVA + DLLNAME, # Name 185 1, # Base 186 len(EXPORT_NAMES), # NumberOfFunctions 187 len(EXPORT_NAMES), # NumberOfNames 188 RDATA_RVA + FUNCS, 189 RDATA_RVA + NAMES, 190 RDATA_RVA + ORDS) 191 for i in range(len(EXPORT_NAMES)): 192 struct.pack_into("<I", rd, FUNCS + 4 * i, TEXT_RVA + i) 193 struct.pack_into("<H", rd, ORDS + 2 * i, i) 194 rd[DLLNAME:DLLNAME + 16] = b"pecoff_test.dll\x00" 195 at = NAME0 196 for i, nm in enumerate(EXPORT_NAMES): 197 struct.pack_into("<I", rd, NAMES + 4 * i, RDATA_RVA + at) 198 rd[at:at + len(nm) + 1] = nm + b"\x00" 199 at += len(nm) + 1 200 assert at <= DBG_DIR 201 202 cv = b"RSDS" + CV_GUID + struct.pack("<I", CV_AGE) + PDB_PATH 203 assert RSDS + len(cv) <= RDATA_SIZE 204 rd[RSDS:RSDS + len(cv)] = cv 205 206 struct.pack_into("<IIHHIIII", rd, DBG_DIR, 207 0, # Characteristics 208 TIMESTAMP, # TimeDateStamp 209 0, 0, # Major/MinorVersion 210 2, # IMAGE_DEBUG_TYPE_CODEVIEW 211 len(cv), # SizeOfData 212 RDATA_RVA + RSDS, # AddressOfRawData 213 RDATA_OFF + RSDS) # PointerToRawData 214 215 # --------------------------------------------------------- certificate 216 # This is not a signature and verifies as nothing: it is the smallest 217 # blob that gives the commonName scan something to find. 06 03 55 04 03 218 # is the DER encoding of the object identifier 2.5.4.3 (commonName). 219 cert_body = (b"\x30\x82\x01\x00" 220 b"\x31\x0B\x30\x09\x06\x03\x55\x04\x06\x13\x02\x43\x48" 221 b"\x31\x35\x30\x33\x06\x03\x55\x04\x03" 222 + bytes([0x13, len(SIGNER_CN)]) + SIGNER_CN) 223 cert = (struct.pack("<IHH", CERT_SIZE, 0x0200, 0x0002) 224 + cert_body).ljust(CERT_SIZE, b"\x00") 225 226 # ---------------------------------------------------------- PE headers 227 sections = [ 228 (b".text", 0x10, TEXT_RVA, TEXT_SIZE, TEXT_OFF, 0x60000020), 229 (b".rdata", 0x200, RDATA_RVA, RDATA_SIZE, RDATA_OFF, 0x40000040), 230 (b".pack", PACK_SIZE, PACK_RVA, PACK_SIZE, PACK_OFF, 0xC0000040), 231 ] 232 233 OPT_SIZE = 112 + 16 * 8 234 coff = struct.pack("<HHIIIHH", 235 0x8664, # Machine: AMD64 236 len(sections), 237 TIMESTAMP, 238 0, 0, # symbol table 239 OPT_SIZE, 240 0x2022) # EXECUTABLE_IMAGE|LARGE_ADDRESS|DLL 241 242 opt = struct.pack("<HBBIIIII", 243 0x020B, # PE32+ 244 14, 38, # linker version 245 TEXT_SIZE, RDATA_SIZE + PACK_SIZE, 0, 246 TEXT_RVA, # AddressOfEntryPoint 247 TEXT_RVA) # BaseOfCode 248 opt += struct.pack("<Q", 0x180000000) # ImageBase 249 opt += struct.pack("<IIHHHHHHIIIIHH", 250 SEC_ALIGN, FILE_ALIGN, 251 6, 0, # OS version 252 0, 0, # image version 253 6, 0, # subsystem version 254 0, # Win32VersionValue 255 0x4000, # SizeOfImage 256 0x400, # SizeOfHeaders 257 0, # CheckSum 258 2, # Subsystem: Windows GUI 259 0xC160) # DllCharacteristics 260 opt += struct.pack("<QQQQ", 0x100000, 0x1000, 0x100000, 0x1000) 261 opt += struct.pack("<II", 0, 16) # LoaderFlags, NumberOfRvaAndSizes 262 263 dirs = [(0, 0)] * 16 264 dirs[0] = (RDATA_RVA + EXP_DIR, 0x88) # export 265 dirs[4] = (CERT_OFF, CERT_SIZE) # certificate: a file offset! 266 dirs[6] = (RDATA_RVA + DBG_DIR, 28) # debug 267 for va, sz in dirs: 268 opt += struct.pack("<II", va, sz) 269 assert len(opt) == OPT_SIZE 270 271 sectab = b"" 272 for name, vsize, rva, rsize, roff, chars in sections: 273 sectab += struct.pack("<8sIIIIIIHHI", 274 name, vsize, rva, rsize, roff, 275 0, 0, 0, 0, chars) 276 277 headers = bytes(dos) + stub + rich + b"PE\x00\x00" + coff + opt + sectab 278 assert len(headers) <= 0x400, hex(len(headers)) 279 image = headers.ljust(0x400, b"\x00") + text + bytes(rd) + pack + cert 280 assert len(image) == CERT_OFF + CERT_SIZE, hex(len(image)) 281 282 with open(OUT, "wb") as f: 283 f.write(image) 284 EOF 285 286 ls -l "$outdir/pecoff_test.exe" "$outdir/pecoff_test.dll"