libextractor

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

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"