| 1 | /** |
| 2 | * @description Authenticode parsing |
| 3 | * @author Ylian Saint-Hilaire & Bryan Roe |
| 4 | * @copyright Intel Corporation 2018-2022 |
| 5 | * @license Apache-2.0 |
| 6 | * @version v0.0.1 |
| 7 | */ |
| 8 | |
| 9 | /*jslint node: true */ |
| 10 | /*jshint node: true */ |
| 11 | /*jshint strict:false */ |
| 12 | /*jshint -W097 */ |
| 13 | /*jshint esversion: 6 */ |
| 14 | "use strict"; |
| 15 | |
| 16 | const fs = require('fs'); |
| 17 | const crypto = require('crypto'); |
| 18 | const forge = require('node-forge'); |
| 19 | const pki = forge.pki; |
| 20 | const p7 = require('./pkcs7-modified'); |
| 21 | |
| 22 | // Generate a test self-signed certificate with code signing extension |
| 23 | function createSelfSignedCert(args) { |
| 24 | var keys = pki.rsa.generateKeyPair(2048); |
| 25 | var cert = pki.createCertificate(); |
| 26 | cert.publicKey = keys.publicKey; |
| 27 | cert.serialNumber = (typeof args.serial == 'string')?args.serial:'012345'; // Serial number must always have a single leading '0', otherwise toPEM/fromPEM will not work right. |
| 28 | cert.validity.notBefore = new Date(); |
| 29 | cert.validity.notAfter = new Date(); |
| 30 | cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10); |
| 31 | var attrs = []; |
| 32 | if (typeof args.cn == 'string') { attrs.push({ name: 'commonName', value: args.cn }); } |
| 33 | if (typeof args.country == 'string') { attrs.push({ name: 'countryName', value: args.country }); } |
| 34 | if (typeof args.state == 'string') { attrs.push({ name: 'ST', value: args.state }); } |
| 35 | if (typeof args.locality == 'string') { attrs.push({ name: 'localityName', value: args.locality }); } |
| 36 | if (typeof args.org == 'string') { attrs.push({ name: 'organizationName', value: args.org }); } |
| 37 | if (typeof args.orgunit == 'string') { attrs.push({ name: 'OU', value: args.orgunit }); } |
| 38 | cert.setSubject(attrs); |
| 39 | cert.setIssuer(attrs); |
| 40 | cert.setExtensions([{ name: 'basicConstraints', cA: false }, { name: 'keyUsage', keyCertSign: false, digitalSignature: true, nonRepudiation: false, keyEncipherment: false, dataEncipherment: false }, { name: 'extKeyUsage', codeSigning: true }, { name: "subjectKeyIdentifier" }]); |
| 41 | cert.sign(keys.privateKey, forge.md.sha384.create()); |
| 42 | return { cert: cert, key: keys.privateKey, extraCerts: [] }; |
| 43 | } |
| 44 | |
| 45 | // Create the output filename if not already specified |
| 46 | function createOutFile(args, filename) { |
| 47 | if (typeof args.out == 'string') return; |
| 48 | var outputFileName = filename.split('.'); |
| 49 | outputFileName[outputFileName.length - 2] += '-out'; |
| 50 | args.out = outputFileName.join('.'); |
| 51 | } |
| 52 | |
| 53 | // Hash an object |
| 54 | function hashObject(obj) { |
| 55 | if (obj == null) { return null; } |
| 56 | const hash = crypto.createHash('sha384'); |
| 57 | if (Buffer.isBuffer(obj)) { hash.update(obj); } else { hash.update(JSON.stringify(obj)); } |
| 58 | return hash.digest().toString('hex'); |
| 59 | } |
| 60 | |
| 61 | // Load a .bmp file. |
| 62 | function loadBitmap(bitmapFile) { |
| 63 | var bitmapData = null; |
| 64 | try { bitmapData = fs.readFileSync(bitmapFile); } catch (ex) { } |
| 65 | if ((bitmapData == null) || (bitmapData.length < 14) || (bitmapData[0] != 0x42) || (bitmapData[1] != 0x4D)) return null; |
| 66 | return bitmapData.slice(14); |
| 67 | } |
| 68 | |
| 69 | // Load a .ico file. This will load all icons in the file into a icon group object |
| 70 | function loadIcon(iconFile) { |
| 71 | var iconData = null; |
| 72 | try { iconData = fs.readFileSync(iconFile); } catch (ex) { } |
| 73 | if ((iconData == null) || (iconData.length < 6) || (iconData[0] != 0) || (iconData[1] != 0)) return null; |
| 74 | const r = { resType: iconData.readUInt16LE(2), resCount: iconData.readUInt16LE(4), icons: {} }; |
| 75 | if (r.resType != 1) return null; |
| 76 | var ptr = 6; |
| 77 | for (var i = 1; i <= r.resCount; i++) { |
| 78 | var icon = {}; |
| 79 | icon.width = iconData[ptr + 0]; |
| 80 | icon.height = iconData[ptr + 1]; |
| 81 | icon.colorCount = iconData[ptr + 2]; |
| 82 | icon.planes = iconData.readUInt16LE(ptr + 4); |
| 83 | icon.bitCount = iconData.readUInt16LE(ptr + 6); |
| 84 | icon.bytesInRes = iconData.readUInt32LE(ptr + 8); |
| 85 | icon.iconCursorId = i; |
| 86 | const offset = iconData.readUInt32LE(ptr + 12); |
| 87 | icon.icon = iconData.slice(offset, offset + icon.bytesInRes); |
| 88 | r.icons[i] = icon; |
| 89 | ptr += 16; |
| 90 | } |
| 91 | return r; |
| 92 | } |
| 93 | |
| 94 | // Load certificates and private key from PEM files |
| 95 | function loadCertificates(pemFileNames) { |
| 96 | var certs = [], keys = []; |
| 97 | if (pemFileNames == null) return; |
| 98 | if (typeof pemFileNames == 'string') { pemFileNames = [pemFileNames]; } |
| 99 | for (var i in pemFileNames) { |
| 100 | try { |
| 101 | // Read certificate |
| 102 | var pem = fs.readFileSync(pemFileNames[i]).toString(); |
| 103 | var pemCerts = pem.split('-----BEGIN CERTIFICATE-----'); |
| 104 | for (var j in pemCerts) { |
| 105 | var k = pemCerts[j].indexOf('-----END CERTIFICATE-----'); |
| 106 | if (k >= 0) { certs.push(pki.certificateFromPem('-----BEGIN CERTIFICATE-----' + pemCerts[j].substring(0, k) + '-----END CERTIFICATE-----')); } |
| 107 | } |
| 108 | var PemKeys = pem.split('-----BEGIN RSA PRIVATE KEY-----'); |
| 109 | for (var j in PemKeys) { |
| 110 | var k = PemKeys[j].indexOf('-----END RSA PRIVATE KEY-----'); |
| 111 | if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN RSA PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END RSA PRIVATE KEY-----')); } |
| 112 | } |
| 113 | PemKeys = pem.split('-----BEGIN PRIVATE KEY-----'); |
| 114 | for (var j in PemKeys) { |
| 115 | var k = PemKeys[j].indexOf('-----END PRIVATE KEY-----'); |
| 116 | if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END PRIVATE KEY-----')); } |
| 117 | } |
| 118 | } catch (ex) { } |
| 119 | } |
| 120 | if ((certs.length == 0) || (keys.length != 1)) return; // No certificates or private keys |
| 121 | var r = { cert: certs[0], key: keys[0], extraCerts: [] } |
| 122 | if (certs.length > 1) { for (var i = 1; i < certs.length; i++) { r.extraCerts.push(certs[i]); } } |
| 123 | return r; |
| 124 | } |
| 125 | |
| 126 | function createAuthenticodeHandler(path) { |
| 127 | const obj = {}; |
| 128 | obj.header = { path: path } |
| 129 | |
| 130 | // Read a file slice |
| 131 | function readFileSlice(start, length) { |
| 132 | var buffer = Buffer.alloc(length); |
| 133 | var len = fs.readSync(obj.fd, buffer, 0, buffer.length, start); |
| 134 | if (len < buffer.length) { buffer = buffer.slice(0, len); } |
| 135 | return buffer; |
| 136 | } |
| 137 | |
| 138 | // Close the file |
| 139 | obj.close = function () { |
| 140 | if (obj.fd == null) return; |
| 141 | fs.closeSync(obj.fd); |
| 142 | delete obj.fd; |
| 143 | } |
| 144 | |
| 145 | // Private OIDS |
| 146 | obj.Oids = { |
| 147 | SPC_INDIRECT_DATA_OBJID: '1.3.6.1.4.1.311.2.1.4', |
| 148 | SPC_STATEMENT_TYPE_OBJID: '1.3.6.1.4.1.311.2.1.11', |
| 149 | SPC_SP_OPUS_INFO_OBJID: '1.3.6.1.4.1.311.2.1.12', |
| 150 | SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.21', |
| 151 | SPC_COMMERCIAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.22', |
| 152 | SPC_MS_JAVA_SOMETHING: '1.3.6.1.4.1.311.15.1', |
| 153 | SPC_PE_IMAGE_DATA_OBJID: '1.3.6.1.4.1.311.2.1.15', |
| 154 | SPC_CAB_DATA_OBJID: '1.3.6.1.4.1.311.2.1.25', |
| 155 | SPC_TIME_STAMP_REQUEST_OBJID: '1.3.6.1.4.1.311.3.2.1', |
| 156 | SPC_SIPINFO_OBJID: '1.3.6.1.4.1.311.2.1.30', |
| 157 | SPC_PE_IMAGE_PAGE_HASHES_V1: '1.3.6.1.4.1.311.2.3.1', |
| 158 | SPC_PE_IMAGE_PAGE_HASHES_V2: '1.3.6.1.4.1.311.2.3.2', |
| 159 | SPC_NESTED_SIGNATURE_OBJID: '1.3.6.1.4.1.311.2.4.1', |
| 160 | SPC_RFC3161_OBJID: '1.3.6.1.4.1.311.3.3.1' |
| 161 | } |
| 162 | |
| 163 | // Open the file and read header information |
| 164 | function openFile() { |
| 165 | if (obj.fd != null) return true; |
| 166 | |
| 167 | // Open the file descriptor |
| 168 | obj.path = path; |
| 169 | try { obj.fd = fs.openSync(path, 'r'); } catch (ex) { return false; } // Unable to open file |
| 170 | obj.stats = fs.fstatSync(obj.fd); |
| 171 | obj.filesize = obj.stats.size; |
| 172 | if (obj.filesize < 64) { obj.close(); return false; } // File too short. |
| 173 | |
| 174 | // Read the DOS header (64 bytes) |
| 175 | var buf = readFileSlice(60, 4); |
| 176 | obj.header.peHeaderLocation = buf.readUInt32LE(0); // The DOS header is 64 bytes long, the last 4 bytes are a pointer to the PE header. |
| 177 | obj.header.peOptionalHeaderLocation = obj.header.peHeaderLocation + 24; // The PE optional header is located just after the PE header which is 24 bytes long. |
| 178 | |
| 179 | // Check file size and signature |
| 180 | if (obj.filesize < (160 + obj.header.peHeaderLocation)) { obj.close(); return false; } // Invalid SizeOfHeaders. |
| 181 | if (readFileSlice(obj.header.peHeaderLocation, 4).toString('hex') != '50450000') { obj.close(); return false; } // Invalid PE header, must start with "PE" (HEX: 50 45 00 00). |
| 182 | |
| 183 | // Read the COFF header |
| 184 | // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image |
| 185 | var coffHeader = readFileSlice(obj.header.peHeaderLocation + 4, 20) |
| 186 | obj.header.coff = {}; |
| 187 | obj.header.coff.machine = coffHeader.readUInt16LE(0); |
| 188 | obj.header.coff.numberOfSections = coffHeader.readUInt16LE(2); |
| 189 | obj.header.coff.timeDateStamp = coffHeader.readUInt32LE(4); |
| 190 | obj.header.coff.pointerToSymbolTable = coffHeader.readUInt32LE(8); |
| 191 | obj.header.coff.numberOfSymbols = coffHeader.readUInt32LE(12); |
| 192 | obj.header.coff.sizeOfOptionalHeader = coffHeader.readUInt16LE(16); |
| 193 | obj.header.coff.characteristics = coffHeader.readUInt16LE(18); |
| 194 | |
| 195 | // Read the entire PE optional header |
| 196 | var optinalHeader = readFileSlice(obj.header.peOptionalHeaderLocation, obj.header.coff.sizeOfOptionalHeader); |
| 197 | |
| 198 | // Decode the PE optional header standard fields |
| 199 | // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only |
| 200 | obj.header.peStandard = {}; |
| 201 | obj.header.peStandard.magic = optinalHeader.readUInt16LE(0); |
| 202 | switch (obj.header.peStandard.magic) { // Check magic value |
| 203 | case 0x020B: obj.header.pe32plus = 1; break; |
| 204 | case 0x010B: obj.header.pe32plus = 0; break; |
| 205 | default: { obj.close(); return false; } // Invalid Magic in PE |
| 206 | } |
| 207 | obj.header.peStandard.majorLinkerVersion = optinalHeader[2]; |
| 208 | obj.header.peStandard.minorLinkerVersion = optinalHeader[3]; |
| 209 | obj.header.peStandard.sizeOfCode = optinalHeader.readUInt32LE(4); |
| 210 | obj.header.peStandard.sizeOfInitializedData = optinalHeader.readUInt32LE(8); |
| 211 | obj.header.peStandard.sizeOfUninitializedData = optinalHeader.readUInt32LE(12); |
| 212 | obj.header.peStandard.addressOfEntryPoint = optinalHeader.readUInt32LE(16); |
| 213 | obj.header.peStandard.baseOfCode = optinalHeader.readUInt32LE(20); |
| 214 | if (obj.header.pe32plus == 0) { obj.header.peStandard.baseOfData = optinalHeader.readUInt32LE(24); } |
| 215 | |
| 216 | // Decode the PE optional header windows fields |
| 217 | // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-windows-specific-fields-image-only |
| 218 | obj.header.peWindows = {} |
| 219 | if (obj.header.pe32plus == 0) { |
| 220 | // 32bit header |
| 221 | //obj.header.peWindows.imageBase = optinalHeader.readUInt32LE(28); |
| 222 | obj.header.peWindows.sectionAlignment = optinalHeader.readUInt32LE(32); |
| 223 | obj.header.peWindows.fileAlignment = optinalHeader.readUInt32LE(36); |
| 224 | obj.header.peWindows.majorOperatingSystemVersion = optinalHeader.readUInt16LE(40); |
| 225 | obj.header.peWindows.minorOperatingSystemVersion = optinalHeader.readUInt16LE(42); |
| 226 | obj.header.peWindows.majorImageVersion = optinalHeader.readUInt16LE(44); |
| 227 | obj.header.peWindows.minorImageVersion = optinalHeader.readUInt16LE(46); |
| 228 | obj.header.peWindows.majorSubsystemVersion = optinalHeader.readUInt16LE(48); |
| 229 | obj.header.peWindows.minorSubsystemVersion = optinalHeader.readUInt16LE(50); |
| 230 | obj.header.peWindows.win32VersionValue = optinalHeader.readUInt32LE(52); |
| 231 | obj.header.peWindows.sizeOfImage = optinalHeader.readUInt32LE(56); |
| 232 | obj.header.peWindows.sizeOfHeaders = optinalHeader.readUInt32LE(60); |
| 233 | obj.header.peWindows.checkSum = optinalHeader.readUInt32LE(64); |
| 234 | obj.header.peWindows.subsystem = optinalHeader.readUInt16LE(68); |
| 235 | obj.header.peWindows.dllCharacteristics = optinalHeader.readUInt16LE(70); |
| 236 | //obj.header.peWindows.sizeOfStackReserve = optinalHeader.readUInt32LE(72); |
| 237 | //obj.header.peWindows.sizeOfStackCommit = optinalHeader.readUInt32LE(76); |
| 238 | //obj.header.peWindows.sizeOfHeapReserve = optinalHeader.readUInt32LE(80); |
| 239 | //obj.header.peWindows.sizeOfHeapCommit = optinalHeader.readUInt32LE(84); |
| 240 | obj.header.peWindows.loaderFlags = optinalHeader.readUInt32LE(88); |
| 241 | obj.header.peWindows.numberOfRvaAndSizes = optinalHeader.readUInt32LE(92); |
| 242 | } else { |
| 243 | // 64bit header |
| 244 | //obj.header.peWindows.imageBase = optinalHeader.readBigUInt64LE(24); // TODO: readBigUInt64LE is not supported in older NodeJS versions |
| 245 | obj.header.peWindows.sectionAlignment = optinalHeader.readUInt32LE(32); |
| 246 | obj.header.peWindows.fileAlignment = optinalHeader.readUInt32LE(36); |
| 247 | obj.header.peWindows.majorOperatingSystemVersion = optinalHeader.readUInt16LE(40); |
| 248 | obj.header.peWindows.minorOperatingSystemVersion = optinalHeader.readUInt16LE(42); |
| 249 | obj.header.peWindows.majorImageVersion = optinalHeader.readUInt16LE(44); |
| 250 | obj.header.peWindows.minorImageVersion = optinalHeader.readUInt16LE(46); |
| 251 | obj.header.peWindows.majorSubsystemVersion = optinalHeader.readUInt16LE(48); |
| 252 | obj.header.peWindows.minorSubsystemVersion = optinalHeader.readUInt16LE(50); |
| 253 | obj.header.peWindows.win32VersionValue = optinalHeader.readUInt32LE(52); |
| 254 | obj.header.peWindows.sizeOfImage = optinalHeader.readUInt32LE(56); |
| 255 | obj.header.peWindows.sizeOfHeaders = optinalHeader.readUInt32LE(60); |
| 256 | obj.header.peWindows.checkSum = optinalHeader.readUInt32LE(64); |
| 257 | obj.header.peWindows.subsystem = optinalHeader.readUInt16LE(68); |
| 258 | obj.header.peWindows.dllCharacteristics = optinalHeader.readUInt16LE(70); |
| 259 | //obj.header.peWindows.sizeOfStackReserve = optinalHeader.readBigUInt64LE(72); |
| 260 | //obj.header.peWindows.sizeOfStackCommit = optinalHeader.readBigUInt64LE(80); |
| 261 | //obj.header.peWindows.sizeOfHeapReserve = optinalHeader.readBigUInt64LE(88); |
| 262 | //obj.header.peWindows.sizeOfHeapCommit = optinalHeader.readBigUInt64LE(96); |
| 263 | obj.header.peWindows.loaderFlags = optinalHeader.readUInt32LE(104); |
| 264 | obj.header.peWindows.numberOfRvaAndSizes = optinalHeader.readUInt32LE(108); |
| 265 | } |
| 266 | |
| 267 | // Decode the PE optional header data directories |
| 268 | // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only |
| 269 | obj.header.dataDirectories = {} |
| 270 | const pePlusOffset = (obj.header.pe32plus == 0) ? 0 : 16; // This header is the same for 32 and 64 bit, but 64bit is offset by 16 bytes. |
| 271 | obj.header.dataDirectories.exportTable = { addr: optinalHeader.readUInt32LE(96 + pePlusOffset), size: optinalHeader.readUInt32LE(100 + pePlusOffset) }; |
| 272 | obj.header.dataDirectories.importTable = { addr: optinalHeader.readUInt32LE(104 + pePlusOffset), size: optinalHeader.readUInt32LE(108 + pePlusOffset) }; |
| 273 | obj.header.dataDirectories.resourceTable = { addr: optinalHeader.readUInt32LE(112 + pePlusOffset), size: optinalHeader.readUInt32LE(116 + pePlusOffset) }; // Same as .rsrc virtual address & size |
| 274 | obj.header.dataDirectories.exceptionTableAddr = { addr: optinalHeader.readUInt32LE(120 + pePlusOffset), size: optinalHeader.readUInt32LE(124 + pePlusOffset) }; // Same as .pdata virtual address & size |
| 275 | obj.header.dataDirectories.certificateTable = { addr: optinalHeader.readUInt32LE(128 + pePlusOffset), size: optinalHeader.readUInt32LE(132 + pePlusOffset) }; |
| 276 | obj.header.dataDirectories.baseRelocationTable = { addr: optinalHeader.readUInt32LE(136 + pePlusOffset), size: optinalHeader.readUInt32LE(140 + pePlusOffset) }; // Same as .reloc virtual address & size |
| 277 | obj.header.dataDirectories.debug = { addr: optinalHeader.readUInt32LE(144 + pePlusOffset), size: optinalHeader.readUInt32LE(148 + pePlusOffset) }; |
| 278 | // obj.header.dataDirectories.architecture = optinalHeader.readBigUInt64LE(152 + pePlusOffset); // Must be zero |
| 279 | obj.header.dataDirectories.globalPtr = { addr: optinalHeader.readUInt32LE(160 + pePlusOffset), size: optinalHeader.readUInt32LE(164 + pePlusOffset) }; |
| 280 | obj.header.dataDirectories.tLSTable = { addr: optinalHeader.readUInt32LE(168 + pePlusOffset), size: optinalHeader.readUInt32LE(172 + pePlusOffset) }; |
| 281 | obj.header.dataDirectories.loadConfigTable = { addr: optinalHeader.readUInt32LE(176 + pePlusOffset), size: optinalHeader.readUInt32LE(180 + pePlusOffset) }; |
| 282 | obj.header.dataDirectories.boundImport = { addr: optinalHeader.readUInt32LE(184 + pePlusOffset), size: optinalHeader.readUInt32LE(188 + pePlusOffset) }; |
| 283 | obj.header.dataDirectories.iAT = { addr: optinalHeader.readUInt32LE(192 + pePlusOffset), size: optinalHeader.readUInt32LE(196 + pePlusOffset) }; |
| 284 | obj.header.dataDirectories.delayImportDescriptor = { addr: optinalHeader.readUInt32LE(200 + pePlusOffset), size: optinalHeader.readUInt32LE(204 + pePlusOffset) }; |
| 285 | obj.header.dataDirectories.clrRuntimeHeader = { addr: optinalHeader.readUInt32LE(208 + pePlusOffset), size: optinalHeader.readUInt32LE(212 + pePlusOffset) }; |
| 286 | // obj.header.dataDirectories.reserved = optinalHeader.readBigUInt64LE(216 + pePlusOffset); // Must be zero |
| 287 | |
| 288 | // Get the certificate table location and size |
| 289 | obj.header.sigpos = obj.header.dataDirectories.certificateTable.addr; |
| 290 | obj.header.siglen = obj.header.dataDirectories.certificateTable.size |
| 291 | obj.header.signed = ((obj.header.sigpos != 0) && (obj.header.siglen != 0)); |
| 292 | |
| 293 | // The section headers are located after the optional PE header |
| 294 | obj.header.SectionHeadersPtr = obj.header.peOptionalHeaderLocation + obj.header.coff.sizeOfOptionalHeader; |
| 295 | |
| 296 | // Read the sections |
| 297 | obj.header.sections = {}; |
| 298 | for (var i = 0; i < obj.header.coff.numberOfSections; i++) { |
| 299 | var section = {}; |
| 300 | buf = readFileSlice(obj.header.SectionHeadersPtr + (i * 40), 40); |
| 301 | if ((buf[0] != 46) && (buf[0] != 95)) { obj.close(); return false; }; // Name of the section must start with a dot or underscore. If not, something is wrong. |
| 302 | var sectionName = buf.slice(0, 8).toString().trim('\0'); |
| 303 | var j = sectionName.indexOf('\0'); |
| 304 | if (j >= 0) { sectionName = sectionName.substring(0, j); } // Trim any trailing zeroes |
| 305 | section.ptr = obj.header.SectionHeadersPtr + (i * 40); |
| 306 | section.virtualSize = buf.readUInt32LE(8); |
| 307 | section.virtualAddr = buf.readUInt32LE(12); |
| 308 | section.rawSize = buf.readUInt32LE(16); |
| 309 | section.rawAddr = buf.readUInt32LE(20); |
| 310 | section.relocAddr = buf.readUInt32LE(24); |
| 311 | section.lineNumbers = buf.readUInt32LE(28); |
| 312 | section.relocNumber = buf.readUInt16LE(32); |
| 313 | section.lineNumbersNumber = buf.readUInt16LE(34); |
| 314 | section.characteristics = buf.readUInt32LE(36); |
| 315 | obj.header.sections[sectionName] = section; |
| 316 | } |
| 317 | |
| 318 | // Compute the checkSum value for this file |
| 319 | obj.header.peWindows.checkSumActual = runChecksum(); |
| 320 | |
| 321 | // If there is a .rsrc section, read the resource information and locations |
| 322 | if (obj.header.sections['.rsrc'] != null) { |
| 323 | obj.resources = readResourceTable(obj.header.sections['.rsrc'].rawAddr, 0); // Read all resources recursively |
| 324 | } |
| 325 | |
| 326 | if (obj.header.signed) { |
| 327 | // Read signature block |
| 328 | |
| 329 | // Check if the file size allows for the signature block |
| 330 | if (obj.filesize < (obj.header.sigpos + obj.header.siglen)) { obj.close(); return false; } // Executable file too short to contain the signature block. |
| 331 | |
| 332 | // Remove the padding if needed |
| 333 | var i, pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8); |
| 334 | var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4; |
| 335 | if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); } |
| 336 | |
| 337 | // Decode the signature block and check that it's valid |
| 338 | var pkcs7der = null, valid = false; |
| 339 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw)); } catch (ex) { } |
| 340 | try { valid = ((pkcs7der != null) && (forge.asn1.derToOid(pkcs7der.value[1].value[0].value[2].value[0].value) == "1.3.6.1.4.1.311.2.1.4")); } catch (ex) { } |
| 341 | if (pkcs7der == null) { |
| 342 | // Can't decode the signature |
| 343 | obj.header.sigpos = 0; |
| 344 | obj.header.siglen = 0; |
| 345 | obj.header.signed = false; |
| 346 | } else { |
| 347 | // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future |
| 348 | // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1" |
| 349 | pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data; |
| 350 | |
| 351 | // Decode the PKCS7 message |
| 352 | var pkcs7 = null, pkcs7content = null; |
| 353 | try { |
| 354 | pkcs7 = p7.messageFromAsn1(pkcs7der); |
| 355 | pkcs7content = pkcs7.rawCapture.content.value[0]; |
| 356 | } catch (ex) { } |
| 357 | |
| 358 | if ((pkcs7 == null) || (pkcs7content == null)) { |
| 359 | // Can't decode the signature |
| 360 | obj.header.sigpos = 0; |
| 361 | obj.header.siglen = 0; |
| 362 | obj.header.signed = false; |
| 363 | } else { |
| 364 | // Verify a PKCS#7 signature |
| 365 | // Verify is not currently supported in node-forge, but if implemented in the future, this code could work. |
| 366 | //var caStore = forge.pki.createCaStore(); |
| 367 | //for (var i in obj.certificates) { caStore.addCertificate(obj.certificates[i]); } |
| 368 | // Return is true if all signatures are valid and chain up to a provided CA |
| 369 | //if (!pkcs7.verify(caStore)) { throw ('Executable file has an invalid signature.'); } |
| 370 | |
| 371 | // Get the signing attributes |
| 372 | obj.signingAttribs = []; |
| 373 | try { |
| 374 | for (var i in pkcs7.rawCapture.authenticatedAttributes) { |
| 375 | if ( |
| 376 | (pkcs7.rawCapture.authenticatedAttributes[i].value != null) && |
| 377 | (pkcs7.rawCapture.authenticatedAttributes[i].value[0] != null) && |
| 378 | (pkcs7.rawCapture.authenticatedAttributes[i].value[0].value != null) && |
| 379 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1] != null) && |
| 380 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value != null) && |
| 381 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0] != null) && |
| 382 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value != null) && |
| 383 | (forge.asn1.derToOid(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value) == obj.Oids.SPC_SP_OPUS_INFO_OBJID)) { |
| 384 | for (var j in pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value) { |
| 385 | if ( |
| 386 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j] != null) && |
| 387 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value != null) && |
| 388 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0] != null) && |
| 389 | (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value != null) |
| 390 | ) { |
| 391 | var v = pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value; |
| 392 | if (v.startsWith('http://') || v.startsWith('https://') || ((v.length % 2) == 1)) { obj.signingAttribs.push(v); } else { |
| 393 | var r = ''; // This string value is in UCS2 format, convert it to a normal string. |
| 394 | for (var k = 0; k < v.length; k += 2) { r += String.fromCharCode((v.charCodeAt(k + 8) << 8) + v.charCodeAt(k + 1)); } |
| 395 | obj.signingAttribs.push(r); |
| 396 | } |
| 397 | } |
| 398 | } |
| 399 | } |
| 400 | } |
| 401 | } catch (ex) { } |
| 402 | |
| 403 | // Set the certificate chain |
| 404 | obj.certificates = pkcs7.certificates; |
| 405 | |
| 406 | // Set the signature |
| 407 | obj.signature = Buffer.from(pkcs7.rawCapture.signature, 'binary'); |
| 408 | |
| 409 | // Get the file hashing algorithm |
| 410 | var hashAlgoOid = forge.asn1.derToOid(pkcs7content.value[1].value[0].value[0].value); |
| 411 | switch (hashAlgoOid) { |
| 412 | case forge.pki.oids.sha256: { obj.fileHashAlgo = 'sha256'; break; } |
| 413 | case forge.pki.oids.sha384: { obj.fileHashAlgo = 'sha384'; break; } |
| 414 | case forge.pki.oids.sha512: { obj.fileHashAlgo = 'sha512'; break; } |
| 415 | case forge.pki.oids.sha224: { obj.fileHashAlgo = 'sha224'; break; } |
| 416 | case forge.pki.oids.md5: { obj.fileHashAlgo = 'md5'; break; } |
| 417 | } |
| 418 | |
| 419 | // Get the signed file hash |
| 420 | obj.fileHashSigned = Buffer.from(pkcs7content.value[1].value[1].value, 'binary') |
| 421 | |
| 422 | // Compute the actual file hash |
| 423 | if (obj.fileHashAlgo != null) { obj.fileHashActual = obj.getHash(obj.fileHashAlgo); } |
| 424 | } |
| 425 | } |
| 426 | } |
| 427 | return true; |
| 428 | } |
| 429 | |
| 430 | // Make a timestamp signature request |
| 431 | obj.timeStampRequest = function (args, func) { |
| 432 | // Create the timestamp request in DER format |
| 433 | const asn1 = forge.asn1; |
| 434 | const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data; |
| 435 | const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data; |
| 436 | const asn1obj = |
| 437 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 438 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid), |
| 439 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 440 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid), |
| 441 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 442 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.signature.toString('binary')) // Signature here |
| 443 | ]) |
| 444 | ]) |
| 445 | ]); |
| 446 | |
| 447 | // Serialize an ASN.1 object to DER format in Base64 |
| 448 | const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64'); |
| 449 | |
| 450 | // Make an HTTP request |
| 451 | const options = { url: args.time, proxy: args.proxy }; |
| 452 | |
| 453 | // Make a request to the time server |
| 454 | httpRequest(options, requestBody, function (err, data) { |
| 455 | if (err != null) { func(err); return; } |
| 456 | |
| 457 | // Decode the timestamp signature block |
| 458 | var timepkcs7der = null; |
| 459 | try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; } |
| 460 | |
| 461 | // Decode the executable signature block |
| 462 | var pkcs7der = null; |
| 463 | try { |
| 464 | var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(obj.getRawSignatureBlock(), 'base64').toString('binary'))); |
| 465 | |
| 466 | // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable |
| 467 | // TODO: We could look to see if the certificate is already present in the executable |
| 468 | const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value; |
| 469 | for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); } |
| 470 | |
| 471 | // Remove any existing time stamp signatures |
| 472 | var newValues = []; |
| 473 | for (var i in pkcs7der.value[1].value[0].value[4].value[0].value) { |
| 474 | const j = pkcs7der.value[1].value[0].value[4].value[0].value[i]; |
| 475 | if ((j.tagClass != 128) || (j.type != 1)) { newValues.push(j); } // If this is not a time stamp, add it to out new list. |
| 476 | } |
| 477 | pkcs7der.value[1].value[0].value[4].value[0].value = newValues; // Set the new list |
| 478 | |
| 479 | // Get the time signature and add it to the executables PKCS7 |
| 480 | const timeasn1Signature = timepkcs7der.value[1].value[0].value[4]; |
| 481 | const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data; |
| 482 | const asn1obj2 = |
| 483 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| 484 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 485 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid), |
| 486 | timeasn1Signature |
| 487 | ]) |
| 488 | ]); |
| 489 | pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2); |
| 490 | |
| 491 | // Re-encode the executable signature block |
| 492 | const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary'); |
| 493 | |
| 494 | // Open the output file |
| 495 | var output = null; |
| 496 | try { output = fs.openSync(args.out, 'w+'); } catch (ex) { } |
| 497 | if (output == null) return false; |
| 498 | var tmp, written = 0; |
| 499 | var executableSize = obj.header.sigpos ? obj.header.sigpos : this.filesize; |
| 500 | |
| 501 | // Compute pre-header length and copy that to the new file |
| 502 | var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); |
| 503 | var tmp = readFileSlice(written, preHeaderLen); |
| 504 | fs.writeSync(output, tmp); |
| 505 | written += tmp.length; |
| 506 | |
| 507 | // Quad Align the results, adding padding if necessary |
| 508 | var len = executableSize + p7signature.length; |
| 509 | var padding = (8 - ((len) % 8)) % 8; |
| 510 | |
| 511 | // Write the signature header |
| 512 | var addresstable = Buffer.alloc(8); |
| 513 | addresstable.writeUInt32LE(executableSize); |
| 514 | addresstable.writeUInt32LE(8 + p7signature.length + padding, 4); |
| 515 | fs.writeSync(output, addresstable); |
| 516 | written += addresstable.length; |
| 517 | |
| 518 | // Copy the rest of the file until the start of the signature block |
| 519 | while ((executableSize - written) > 0) { |
| 520 | tmp = readFileSlice(written, Math.min(executableSize - written, 65536)); |
| 521 | fs.writeSync(output, tmp); |
| 522 | written += tmp.length; |
| 523 | } |
| 524 | |
| 525 | // Write the signature block header and signature |
| 526 | var win = Buffer.alloc(8); // WIN CERTIFICATE Structure |
| 527 | win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length |
| 528 | win.writeUInt16LE(512, 4); // WORD revision |
| 529 | win.writeUInt16LE(2, 6); // WORD type |
| 530 | fs.writeSync(output, win); |
| 531 | fs.writeSync(output, p7signature); |
| 532 | if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); } |
| 533 | written += (p7signature.length + padding + 8); |
| 534 | |
| 535 | // Compute the checksum and write it in the PE header checksum location |
| 536 | var tmp = Buffer.alloc(4); |
| 537 | tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4))); |
| 538 | fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64); |
| 539 | |
| 540 | // Close the file |
| 541 | fs.closeSync(output); |
| 542 | |
| 543 | // Indicate we are done |
| 544 | func(null); |
| 545 | } catch (ex) { func('' + ex); return; } |
| 546 | }); |
| 547 | } |
| 548 | |
| 549 | // Read a resource table. |
| 550 | // ptr: The pointer to the start of the resource section |
| 551 | // offset: The offset start of the resource table to read |
| 552 | function readResourceTable(ptr, offset) { |
| 553 | var buf = readFileSlice(ptr + offset, 16); |
| 554 | var r = {}; |
| 555 | r.characteristics = buf.readUInt32LE(0); |
| 556 | r.timeDateStamp = buf.readUInt32LE(4); |
| 557 | r.majorVersion = buf.readUInt16LE(8); |
| 558 | r.minorVersion = buf.readUInt16LE(10); |
| 559 | var numberOfNamedEntries = buf.readUInt16LE(12); |
| 560 | var numberOfIdEntries = buf.readUInt16LE(14); |
| 561 | |
| 562 | r.entries = []; |
| 563 | var totalResources = numberOfNamedEntries + numberOfIdEntries; |
| 564 | //console.log('readResourceTable', offset, 16 + (totalResources) * 8, offset + (16 + (totalResources) * 8)); |
| 565 | for (var i = 0; i < totalResources; i++) { |
| 566 | buf = readFileSlice(ptr + offset + 16 + (i * 8), 8); |
| 567 | var resource = {}; |
| 568 | resource.name = buf.readUInt32LE(0); |
| 569 | var offsetToData = buf.readUInt32LE(4); |
| 570 | if ((resource.name & 0x80000000) != 0) { |
| 571 | var oname = resource.name; |
| 572 | resource.name = readLenPrefixUnicodeString(ptr + (resource.name - 0x80000000)); |
| 573 | //console.log('readResourceName', offset + (oname - 0x80000000), 2 + (resource.name.length * 2), offset + (oname - 0x80000000) + (2 + resource.name.length * 2), resource.name); |
| 574 | } |
| 575 | if ((offsetToData & 0x80000000) != 0) { resource.table = readResourceTable(ptr, offsetToData - 0x80000000); } else { resource.item = readResourceItem(ptr, offsetToData); } |
| 576 | r.entries.push(resource); |
| 577 | } |
| 578 | return r; |
| 579 | } |
| 580 | |
| 581 | // Read a resource item |
| 582 | // ptr: The pointer to the start of the resource section |
| 583 | // offset: The offset start of the resource item to read |
| 584 | function readResourceItem(ptr, offset) { |
| 585 | //console.log('readResourceItem', offset, 16, offset + 16); |
| 586 | var buf = readFileSlice(ptr + offset, 16), r = {}; |
| 587 | r.offsetToData = buf.readUInt32LE(0); |
| 588 | r.size = buf.readUInt32LE(4); |
| 589 | //console.log('readResourceData', r.offsetToData - obj.header.sections['.rsrc'].virtualAddr, r.size, r.offsetToData + r.size - obj.header.sections['.rsrc'].virtualAddr); |
| 590 | r.codePage = buf.readUInt32LE(8); |
| 591 | //r.reserved = buf.readUInt32LE(12); |
| 592 | return r; |
| 593 | } |
| 594 | |
| 595 | // Read a unicode stting that starts with the string length as the first byte. |
| 596 | function readLenPrefixUnicodeString(ptr) { |
| 597 | var nameLen = readFileSlice(ptr, 2).readUInt16LE(0); |
| 598 | var buf = readFileSlice(ptr + 2, nameLen * 2), name = ''; |
| 599 | for (var i = 0; i < nameLen; i++) { name += String.fromCharCode(buf.readUInt16LE(i * 2)); } |
| 600 | return name; |
| 601 | } |
| 602 | |
| 603 | // Generate a complete resource section and pad the section |
| 604 | function generateResourceSection(resources) { |
| 605 | // Call a resursive method the compute the size needed for each element |
| 606 | const resSizes = { tables: 0, items: 0, names: 0, data: 0 }; |
| 607 | getResourceSectionSize(resources, resSizes); |
| 608 | |
| 609 | // Pad the resource section & allocate the buffer |
| 610 | const fileAlign = obj.header.peWindows.fileAlignment |
| 611 | var resSizeTotal = resSizes.tables + resSizes.items + resSizes.names + resSizes.data; |
| 612 | var resNoPadding = resSizeTotal + 4; // TODO: Not sure why this is off by 4 |
| 613 | if ((resSizeTotal % fileAlign) != 0) { resSizeTotal += (fileAlign - (resSizeTotal % fileAlign)); } |
| 614 | const resSectionBuffer = Buffer.alloc(resSizeTotal); |
| 615 | |
| 616 | // Write the resource section, calling a recursive method |
| 617 | const resPointers = { tables: 0, items: resSizes.tables, names: resSizes.tables + resSizes.items, data: resSizes.tables + resSizes.items + resSizes.names }; |
| 618 | createResourceSection(resources, resSectionBuffer, resPointers); |
| 619 | //console.log('generateResourceSection', resPointers); |
| 620 | |
| 621 | // Done, return the result |
| 622 | return { size: resNoPadding, data: resSectionBuffer }; |
| 623 | } |
| 624 | |
| 625 | // Return the total size of a resource header, this is a recursive method |
| 626 | function getResourceSectionSize(resources, sizes) { |
| 627 | sizes.tables += (16 + (resources.entries.length * 8)); |
| 628 | for (var i in resources.entries) { |
| 629 | if (typeof resources.entries[i].name == 'string') { |
| 630 | var dataSize = (2 + (resources.entries[i].name.length * 2)); |
| 631 | if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); } |
| 632 | sizes.names += dataSize; |
| 633 | } |
| 634 | if (resources.entries[i].table) { getResourceSectionSize(resources.entries[i].table, sizes); } |
| 635 | else if (resources.entries[i].item) { |
| 636 | sizes.items += 16; |
| 637 | if (resources.entries[i].item.buffer) { |
| 638 | sizes.data += resources.entries[i].item.buffer.length; |
| 639 | } else { |
| 640 | var dataSize = resources.entries[i].item.size; |
| 641 | if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); } |
| 642 | sizes.data += dataSize; |
| 643 | } |
| 644 | } |
| 645 | } |
| 646 | } |
| 647 | |
| 648 | // Write the resource section in the buffer, this is a recursive method |
| 649 | function createResourceSection(resources, buf, resPointers) { |
| 650 | var numberOfNamedEntries = 0, numberOfIdEntries = 0, ptr = resPointers.tables; |
| 651 | //console.log('createResourceSection', resPointers, ptr); |
| 652 | |
| 653 | // Figure out how many items we have to save |
| 654 | for (var i in resources.entries) { |
| 655 | if (typeof resources.entries[i].name == 'string') { numberOfNamedEntries++; } else { numberOfIdEntries++; } |
| 656 | } |
| 657 | |
| 658 | // Move the table pointer forward |
| 659 | resPointers.tables += (16 + (8 * numberOfNamedEntries) + (8 * numberOfIdEntries)); |
| 660 | |
| 661 | // Write the table header |
| 662 | buf.writeUInt32LE(resources.characteristics, ptr); |
| 663 | buf.writeUInt32LE(resources.timeDateStamp, ptr + 4); |
| 664 | buf.writeUInt16LE(resources.majorVersion, ptr + 8); |
| 665 | buf.writeUInt16LE(resources.minorVersion, ptr + 10); |
| 666 | buf.writeUInt16LE(numberOfNamedEntries, ptr + 12); |
| 667 | buf.writeUInt16LE(numberOfIdEntries, ptr + 14); |
| 668 | |
| 669 | // For each table entry, write the entry for it |
| 670 | for (var i in resources.entries) { |
| 671 | // Write the name |
| 672 | var name = resources.entries[i].name; |
| 673 | if (typeof resources.entries[i].name == 'string') { |
| 674 | // Set the pointer to the name |
| 675 | name = resPointers.names + 0x80000000; |
| 676 | |
| 677 | // Write the name length, followed by the name string in unicode |
| 678 | buf.writeUInt16LE(resources.entries[i].name.length, resPointers.names); |
| 679 | for (var j = 0; j < resources.entries[i].name.length; j++) { |
| 680 | buf.writeUInt16LE(resources.entries[i].name.charCodeAt(j), 2 + resPointers.names + (j * 2)); |
| 681 | } |
| 682 | |
| 683 | // Move the names pointer forward, 8 byte align |
| 684 | var dataSize = (2 + (resources.entries[i].name.length * 2)); |
| 685 | if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); } |
| 686 | resPointers.names += dataSize; |
| 687 | } |
| 688 | buf.writeUInt32LE(name, ptr + 16 + (i * 8)); |
| 689 | |
| 690 | // Write the data |
| 691 | var data; |
| 692 | if (resources.entries[i].table) { |
| 693 | // This is a pointer to a table entry |
| 694 | data = resPointers.tables + 0x80000000; |
| 695 | createResourceSection(resources.entries[i].table, buf, resPointers); |
| 696 | } else if (resources.entries[i].item) { |
| 697 | // This is a pointer to a data entry |
| 698 | data = resPointers.items; |
| 699 | |
| 700 | // Write the data |
| 701 | var entrySize = 0; |
| 702 | if (resources.entries[i].item.buffer) { |
| 703 | // Write the data from given buffer |
| 704 | resources.entries[i].item.buffer.copy(buf, resPointers.data, 0, resources.entries[i].item.buffer.length); |
| 705 | entrySize = resources.entries[i].item.buffer.length; |
| 706 | } else { |
| 707 | // Write the data from original file |
| 708 | const actualPtr = (resources.entries[i].item.offsetToData - obj.header.sections['.rsrc'].virtualAddr) + obj.header.sections['.rsrc'].rawAddr; |
| 709 | const tmp = readFileSlice(actualPtr, resources.entries[i].item.size); |
| 710 | tmp.copy(buf, resPointers.data, 0, tmp.length); |
| 711 | entrySize = resources.entries[i].item.size;; |
| 712 | } |
| 713 | |
| 714 | // Write the item entry |
| 715 | buf.writeUInt32LE(resPointers.data + obj.header.sections['.rsrc'].virtualAddr, resPointers.items); // Write the pointer relative to the virtual address |
| 716 | buf.writeUInt32LE(entrySize, resPointers.items + 4); |
| 717 | buf.writeUInt32LE(resources.entries[i].item.codePage, resPointers.items + 8); |
| 718 | buf.writeUInt32LE(resources.entries[i].item.reserved, resPointers.items + 12); |
| 719 | |
| 720 | // Move items pointers forward |
| 721 | resPointers.items += 16; |
| 722 | var dataSize = entrySize; |
| 723 | if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); } |
| 724 | resPointers.data += dataSize; |
| 725 | } |
| 726 | buf.writeUInt32LE(data, ptr + 20 + (i * 8)); |
| 727 | } |
| 728 | } |
| 729 | |
| 730 | // Convert a unicode buffer to a string |
| 731 | function unicodeToString(buf) { |
| 732 | var r = '', c; |
| 733 | for (var i = 0; i < (buf.length / 2) ; i++) { |
| 734 | c = buf.readUInt16LE(i * 2); |
| 735 | if (c != 0) { r += String.fromCharCode(c); } else { return r; } |
| 736 | } |
| 737 | return r; |
| 738 | } |
| 739 | |
| 740 | // Convert a string to a unicode buffer |
| 741 | // Input is a string, a buffer to write to and the offset in the buffer (0 is default). |
| 742 | function stringToUnicode(str, buf, offset) { |
| 743 | if (offset == null) { offset = 0; } |
| 744 | for (var i = 0; i < str.length; i++) { buf.writeInt16LE(str.charCodeAt(i), offset + (i * 2)); } |
| 745 | } |
| 746 | |
| 747 | var resourceDefaultNames = { |
| 748 | 'bitmaps': 2, |
| 749 | 'icon': 3, |
| 750 | 'dialogs': 5, |
| 751 | 'iconGroups': 14, |
| 752 | 'versionInfo': 16, |
| 753 | 'configurationFiles': 24 |
| 754 | } |
| 755 | |
| 756 | // Return the raw signature block buffer with padding removed |
| 757 | obj.getRawSignatureBlock = function () { |
| 758 | if ((obj.header.sigpos == 0) || (obj.header.siglen == 0)) return null; |
| 759 | var pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8); |
| 760 | var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4; |
| 761 | if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); } |
| 762 | return pkcs7raw; |
| 763 | } |
| 764 | |
| 765 | |
| 766 | // Get bitmaps information from resource |
| 767 | obj.getBitmapInfo = function () { |
| 768 | const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr; |
| 769 | |
| 770 | // Find and parse each icon |
| 771 | const bitmaps = {} |
| 772 | for (var i = 0; i < obj.resources.entries.length; i++) { |
| 773 | if (obj.resources.entries[i].name == resourceDefaultNames.bitmaps) { |
| 774 | for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) { |
| 775 | const bitmapName = obj.resources.entries[i].table.entries[j].name; |
| 776 | const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData; |
| 777 | const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size; |
| 778 | const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr; |
| 779 | bitmaps[bitmapName] = readFileSlice(actualPtr, size); |
| 780 | } |
| 781 | } |
| 782 | } |
| 783 | |
| 784 | return bitmaps; |
| 785 | } |
| 786 | |
| 787 | // Get icon information from resource |
| 788 | obj.getIconInfo = function () { |
| 789 | const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr; |
| 790 | |
| 791 | // Find and parse each icon |
| 792 | const icons = {} |
| 793 | for (var i = 0; i < obj.resources.entries.length; i++) { |
| 794 | if (obj.resources.entries[i].name == resourceDefaultNames.icon) { |
| 795 | for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) { |
| 796 | const iconName = obj.resources.entries[i].table.entries[j].name; |
| 797 | const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData; |
| 798 | const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size; |
| 799 | const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr; |
| 800 | icons[iconName] = readFileSlice(actualPtr, size); |
| 801 | } |
| 802 | } |
| 803 | } |
| 804 | |
| 805 | // Find and parse each icon group |
| 806 | for (var i = 0; i < obj.resources.entries.length; i++) { |
| 807 | if (obj.resources.entries[i].name == resourceDefaultNames.iconGroups) { |
| 808 | for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) { |
| 809 | const groupName = obj.resources.entries[i].table.entries[j].name; |
| 810 | const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData; |
| 811 | const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size; |
| 812 | const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr; |
| 813 | const group = {}; |
| 814 | const groupData = readFileSlice(actualPtr, size); |
| 815 | |
| 816 | // Parse NEWHEADER structure: https://docs.microsoft.com/en-us/windows/win32/menurc/newheader |
| 817 | group.resType = groupData.readUInt16LE(2); |
| 818 | group.resCount = groupData.readUInt16LE(4); |
| 819 | |
| 820 | // Parse many RESDIR structure: https://docs.microsoft.com/en-us/windows/win32/menurc/resdir |
| 821 | group.icons = {}; |
| 822 | for (var p = 6; p < size; p += 14) { |
| 823 | var icon = {} |
| 824 | icon.width = groupData[p]; |
| 825 | icon.height = groupData[p + 1]; |
| 826 | icon.colorCount = groupData[p + 2]; |
| 827 | icon.planes = groupData.readUInt16LE(p + 4); |
| 828 | icon.bitCount = groupData.readUInt16LE(p + 6); |
| 829 | icon.bytesInRes = groupData.readUInt32LE(p + 8); |
| 830 | icon.iconCursorId = groupData.readUInt16LE(p + 12); |
| 831 | icon.icon = icons[icon.iconCursorId]; |
| 832 | group.icons[icon.iconCursorId] = icon; |
| 833 | } |
| 834 | |
| 835 | // Add an icon group |
| 836 | r[groupName] = group; |
| 837 | } |
| 838 | } |
| 839 | } |
| 840 | |
| 841 | return r; |
| 842 | } |
| 843 | |
| 844 | // Set bitmap information |
| 845 | obj.setBitmapInfo = function (bitmapInfo) { |
| 846 | // Delete all bitmaps resources |
| 847 | var resourcesEntries = []; |
| 848 | for (var i = 0; i < obj.resources.entries.length; i++) { |
| 849 | if (obj.resources.entries[i].name != resourceDefaultNames.bitmaps) { |
| 850 | resourcesEntries.push(obj.resources.entries[i]); |
| 851 | } |
| 852 | } |
| 853 | obj.resources.entries = resourcesEntries; |
| 854 | |
| 855 | // Add all bitmap entries |
| 856 | const bitmapEntry = { name: resourceDefaultNames.bitmaps, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } }; |
| 857 | for (var i in bitmapInfo) { |
| 858 | var name = i; |
| 859 | if (parseInt(i) == name) { name = parseInt(i); } |
| 860 | const bitmapItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: bitmapInfo[i], codePage: 0 } }] } } |
| 861 | bitmapEntry.table.entries.push(bitmapItemEntry); |
| 862 | } |
| 863 | obj.resources.entries.push(bitmapEntry); |
| 864 | |
| 865 | // Sort the resources by name. This is required. |
| 866 | function resSort(a, b) { |
| 867 | if ((typeof a == 'string') && (typeof b == 'string')) { if (a < b) return -1; if (a > b) return 1; return 0; } |
| 868 | if ((typeof a == 'number') && (typeof b == 'number')) { return a - b; } |
| 869 | if ((typeof a == 'string') && (typeof b == 'number')) { return -1; } |
| 870 | return 1; |
| 871 | } |
| 872 | const names = []; |
| 873 | for (var i = 0; i < obj.resources.entries.length; i++) { names.push(obj.resources.entries[i].name); } |
| 874 | names.sort(resSort); |
| 875 | var newEntryOrder = []; |
| 876 | for (var i in names) { |
| 877 | for (var j = 0; j < obj.resources.entries.length; j++) { |
| 878 | if (obj.resources.entries[j].name == names[i]) { newEntryOrder.push(obj.resources.entries[j]); } |
| 879 | } |
| 880 | } |
| 881 | obj.resources.entries = newEntryOrder; |
| 882 | } |
| 883 | |
| 884 | // Set icon information |
| 885 | obj.setIconInfo = function (iconInfo) { |
| 886 | // Delete all icon and icon groups resources |
| 887 | var resourcesEntries = []; |
| 888 | for (var i = 0; i < obj.resources.entries.length; i++) { |
| 889 | if ((obj.resources.entries[i].name != resourceDefaultNames.icon) && (obj.resources.entries[i].name != resourceDefaultNames.iconGroups)) { |
| 890 | resourcesEntries.push(obj.resources.entries[i]); |
| 891 | } |
| 892 | } |
| 893 | obj.resources.entries = resourcesEntries; |
| 894 | |
| 895 | // Count the icon groups and re-number all icons |
| 896 | var iconGroupCount = 0, nextIconNumber = 1; |
| 897 | for (var i in iconInfo) { |
| 898 | iconGroupCount++; |
| 899 | var xicons = {}; |
| 900 | for (var j in iconInfo[i].icons) { xicons[nextIconNumber++] = iconInfo[i].icons[j]; } |
| 901 | iconInfo[i].icons = xicons; |
| 902 | } |
| 903 | if (iconGroupCount == 0) return; // If there are no icon groups, we are done |
| 904 | |
| 905 | // Add the new icons entry |
| 906 | const iconsEntry = { name: resourceDefaultNames.icon, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } }; |
| 907 | for (var i in iconInfo) { |
| 908 | for (var j in iconInfo[i].icons) { |
| 909 | var name = j; |
| 910 | if (parseInt(j) == name) { name = parseInt(j); } |
| 911 | const iconItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: iconInfo[i].icons[j].icon, codePage: 0 } }] } } |
| 912 | iconsEntry.table.entries.push(iconItemEntry); |
| 913 | } |
| 914 | } |
| 915 | obj.resources.entries.push(iconsEntry); |
| 916 | |
| 917 | // Add the new icon group entry |
| 918 | const groupEntry = { name: resourceDefaultNames.iconGroups, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } }; |
| 919 | for (var i in iconInfo) { |
| 920 | // Build icon group struct |
| 921 | var iconCount = 0, p = 6; |
| 922 | for (var j in iconInfo[i].icons) { iconCount++; } |
| 923 | const buf = Buffer.alloc(6 + (iconCount * 14)); |
| 924 | buf.writeUInt16LE(iconInfo[i].resType, 2); |
| 925 | buf.writeUInt16LE(iconCount, 4); |
| 926 | for (var j in iconInfo[i].icons) { |
| 927 | buf[p] = iconInfo[i].icons[j].width; |
| 928 | buf[p + 1] = iconInfo[i].icons[j].height; |
| 929 | buf[p + 2] = iconInfo[i].icons[j].colorCount; |
| 930 | buf.writeUInt16LE(iconInfo[i].icons[j].planes, p + 4); |
| 931 | buf.writeUInt16LE(iconInfo[i].icons[j].bitCount, p + 6); |
| 932 | buf.writeUInt32LE(iconInfo[i].icons[j].bytesInRes, p + 8); |
| 933 | buf.writeUInt16LE(j, p + 12); |
| 934 | p += 14; |
| 935 | } |
| 936 | var name = i; |
| 937 | if (parseInt(i) == name) { name = parseInt(i); } |
| 938 | const groupItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: buf, codePage: 0 } }] } } |
| 939 | groupEntry.table.entries.push(groupItemEntry); |
| 940 | } |
| 941 | obj.resources.entries.push(groupEntry); |
| 942 | |
| 943 | // Sort the resources by name. This is required. |
| 944 | function resSort(a, b) { |
| 945 | if ((typeof a == 'string') && (typeof b == 'string')) { if (a < b) return -1; if (a > b) return 1; return 0; } |
| 946 | if ((typeof a == 'number') && (typeof b == 'number')) { return a - b; } |
| 947 | if ((typeof a == 'string') && (typeof b == 'number')) { return -1; } |
| 948 | return 1; |
| 949 | } |
| 950 | const names = []; |
| 951 | for (var i = 0; i < obj.resources.entries.length; i++) { names.push(obj.resources.entries[i].name); } |
| 952 | names.sort(resSort); |
| 953 | var newEntryOrder = []; |
| 954 | for (var i in names) { |
| 955 | for (var j = 0; j < obj.resources.entries.length; j++) { |
| 956 | if (obj.resources.entries[j].name == names[i]) { newEntryOrder.push(obj.resources.entries[j]); } |
| 957 | } |
| 958 | } |
| 959 | obj.resources.entries = newEntryOrder; |
| 960 | } |
| 961 | |
| 962 | // Decode the version information from the resource |
| 963 | obj.getVersionInfo = function () { |
| 964 | var r = {}, info = readVersionInfo(getVersionInfoData(), 0); |
| 965 | if ((info == null) || (info.stringFiles == null)) return null; |
| 966 | var StringFileInfo = null; |
| 967 | for (var i in info.stringFiles) { if (info.stringFiles[i].szKey == 'StringFileInfo') { StringFileInfo = info.stringFiles[i]; } } |
| 968 | if ((StringFileInfo == null) || (StringFileInfo.stringTable == null) || (StringFileInfo.stringTable.strings == null)) return null; |
| 969 | const strings = StringFileInfo.stringTable.strings; |
| 970 | for (var i in strings) { r[strings[i].key] = strings[i].value; } |
| 971 | r['~FileVersion'] = (info.fixedFileInfo.dwFileVersionMS >> 16) + '.' + (info.fixedFileInfo.dwFileVersionMS & 0xFFFF) + '.' + (info.fixedFileInfo.dwFileVersionLS >> 16) + '.' + (info.fixedFileInfo.dwFileVersionLS & 0xFFFF); |
| 972 | r['~ProductVersion'] = (info.fixedFileInfo.dwProductVersionMS >> 16) + '.' + (info.fixedFileInfo.dwProductVersionMS & 0xFFFF) + '.' + (info.fixedFileInfo.dwProductVersionLS >> 16) + '.' + (info.fixedFileInfo.dwProductVersionLS & 0xFFFF); |
| 973 | return r; |
| 974 | } |
| 975 | |
| 976 | // Encode the version information to the resource |
| 977 | obj.setVersionInfo = function (versions) { |
| 978 | // Convert the version information into a string array |
| 979 | const stringArray = []; |
| 980 | for (var i in versions) { if (!i.startsWith('~')) { stringArray.push({ key: i, value: versions[i] }); } } |
| 981 | |
| 982 | // Get the existing version data and switch the strings to the new strings |
| 983 | var r = {}, info = readVersionInfo(getVersionInfoData(), 0); |
| 984 | if ((info == null) || (info.stringFiles == null)) return; |
| 985 | var StringFileInfo = null; |
| 986 | for (var i in info.stringFiles) { if (info.stringFiles[i].szKey == 'StringFileInfo') { StringFileInfo = info.stringFiles[i]; } } |
| 987 | if ((StringFileInfo == null) || (StringFileInfo.stringTable == null) || (StringFileInfo.stringTable.strings == null)) return; |
| 988 | StringFileInfo.stringTable.strings = stringArray; |
| 989 | |
| 990 | // Set the file version |
| 991 | if (versions['~FileVersion'] != null) { |
| 992 | const FileVersionSplit = versions['~FileVersion'].split('.'); |
| 993 | info.fixedFileInfo.dwFileVersionMS = (parseInt(FileVersionSplit[0]) << 16) + parseInt(FileVersionSplit[1]); |
| 994 | info.fixedFileInfo.dwFileVersionLS = (parseInt(FileVersionSplit[2]) << 16) + parseInt(FileVersionSplit[3]); |
| 995 | } |
| 996 | |
| 997 | // Set the product version |
| 998 | if (versions['~ProductVersion'] != null) { |
| 999 | const ProductVersionSplit = versions['~ProductVersion'].split('.'); |
| 1000 | info.fixedFileInfo.dwProductVersionMS = (parseInt(ProductVersionSplit[0]) << 16) + parseInt(ProductVersionSplit[1]); |
| 1001 | info.fixedFileInfo.dwProductVersionLS = (parseInt(ProductVersionSplit[2]) << 16) + parseInt(ProductVersionSplit[3]); |
| 1002 | } |
| 1003 | |
| 1004 | // Re-encode the version information into a buffer |
| 1005 | var verInfoResBufArray = []; |
| 1006 | writeVersionInfo(verInfoResBufArray, info); |
| 1007 | var verInfoRes = Buffer.concat(verInfoResBufArray); |
| 1008 | |
| 1009 | // Display all buffers |
| 1010 | //console.log('--WRITE BUF ARRAY START--'); |
| 1011 | //for (var i in verInfoResBufArray) { console.log(verInfoResBufArray[i].toString('hex')); } |
| 1012 | //console.log('--WRITE BUF ARRAY END--'); |
| 1013 | //console.log('OUT', Buffer.concat(verInfoResBufArray).toString('hex')); |
| 1014 | |
| 1015 | // Set the new buffer as part of the resources |
| 1016 | for (var i = 0; i < obj.resources.entries.length; i++) { |
| 1017 | if (obj.resources.entries[i].name == resourceDefaultNames.versionInfo) { |
| 1018 | const verInfo = obj.resources.entries[i].table.entries[0].table.entries[0].item; |
| 1019 | delete verInfo.size; |
| 1020 | delete verInfo.offsetToData; |
| 1021 | verInfo.buffer = verInfoRes; |
| 1022 | obj.resources.entries[i].table.entries[0].table.entries[0].item = verInfo; |
| 1023 | } |
| 1024 | } |
| 1025 | } |
| 1026 | |
| 1027 | // Return the version info data block |
| 1028 | function getVersionInfoData() { |
| 1029 | if (obj.resources == null) return null; |
| 1030 | const ptr = obj.header.sections['.rsrc'].rawAddr; |
| 1031 | for (var i = 0; i < obj.resources.entries.length; i++) { |
| 1032 | if (obj.resources.entries[i].name == resourceDefaultNames.versionInfo) { |
| 1033 | const verInfo = obj.resources.entries[i].table.entries[0].table.entries[0].item; |
| 1034 | if (verInfo.buffer != null) { |
| 1035 | return verInfo.buffer; |
| 1036 | } else { |
| 1037 | const actualPtr = (verInfo.offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr; |
| 1038 | return readFileSlice(actualPtr, verInfo.size); |
| 1039 | } |
| 1040 | } |
| 1041 | } |
| 1042 | return null; |
| 1043 | } |
| 1044 | |
| 1045 | // Create a VS_VERSIONINFO structure as a array of buffer that is ready to be placed in the resource section |
| 1046 | // VS_VERSIONINFO structure: https://docs.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo |
| 1047 | function writeVersionInfo(bufArray, info) { |
| 1048 | const buf = Buffer.alloc(40); |
| 1049 | buf.writeUInt16LE(0, 4); // wType |
| 1050 | stringToUnicode('VS_VERSION_INFO', buf, 6); |
| 1051 | bufArray.push(buf); |
| 1052 | |
| 1053 | var wLength = 40; |
| 1054 | var wValueLength = 0; |
| 1055 | if (info.fixedFileInfo != null) { |
| 1056 | const buf2 = Buffer.alloc(52); |
| 1057 | wLength += 52; |
| 1058 | wValueLength += 52; |
| 1059 | buf2.writeUInt32LE(info.fixedFileInfo.dwSignature, 0); // dwSignature |
| 1060 | buf2.writeUInt32LE(info.fixedFileInfo.dwStrucVersion, 4); // dwStrucVersion |
| 1061 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileVersionMS, 8); // dwFileVersionMS |
| 1062 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileVersionLS, 12); // dwFileVersionLS |
| 1063 | buf2.writeUInt32LE(info.fixedFileInfo.dwProductVersionMS, 16); // dwProductVersionMS |
| 1064 | buf2.writeUInt32LE(info.fixedFileInfo.dwProductVersionLS, 20); // dwProductVersionLS |
| 1065 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileFlagsMask, 24); // dwFileFlagsMask |
| 1066 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileFlags, 28); // dwFileFlags |
| 1067 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileOS, 32); // dwFileOS |
| 1068 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileType, 36); // dwFileType |
| 1069 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileSubtype, 40); // dwFileSubtype |
| 1070 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileDateMS, 44); // dwFileDateMS |
| 1071 | buf2.writeUInt32LE(info.fixedFileInfo.dwFileDateLS, 48); // dwFileDateLS |
| 1072 | bufArray.push(buf2); |
| 1073 | } |
| 1074 | |
| 1075 | if (info.stringFiles != null) { wLength += writeStringFileInfo(bufArray, info.stringFiles); } |
| 1076 | |
| 1077 | buf.writeUInt16LE(Buffer.concat(bufArray).length, 0); // wLength |
| 1078 | buf.writeUInt16LE(wValueLength, 2); // wValueLength |
| 1079 | return wLength; |
| 1080 | } |
| 1081 | |
| 1082 | // StringFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo |
| 1083 | function writeStringFileInfo(bufArray, stringFiles) { |
| 1084 | var totalLen = 0; |
| 1085 | for (var i in stringFiles) { |
| 1086 | var l = 6 + (stringFiles[i].szKey.length * 2); |
| 1087 | if (stringFiles[i].szKey == 'VarFileInfo') { l += 4; } // TODO: This is a hack, not sure what the correct code should be |
| 1088 | const buf2 = Buffer.alloc(padPointer(l)); |
| 1089 | buf2.writeUInt16LE(1, 4); // wType |
| 1090 | stringToUnicode(stringFiles[i].szKey, buf2, 6); |
| 1091 | bufArray.push(buf2); |
| 1092 | |
| 1093 | var wLength = 0, wValueLength = 0; |
| 1094 | |
| 1095 | if (stringFiles[i].szKey == 'StringFileInfo') { wLength += writeStringTableStruct(bufArray, stringFiles[i].stringTable); } |
| 1096 | if (stringFiles[i].szKey == 'VarFileInfo') { wLength += writeVarFileInfoStruct(bufArray, stringFiles[i].varFileInfo); } |
| 1097 | |
| 1098 | buf2.writeUInt16LE(l + wLength, 0); // wLength |
| 1099 | buf2.writeUInt16LE(wValueLength, 2); // wValueLength |
| 1100 | totalLen += buf2.length + wLength; |
| 1101 | } |
| 1102 | return totalLen; |
| 1103 | } |
| 1104 | |
| 1105 | // VarFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/var-str |
| 1106 | function writeVarFileInfoStruct(bufArray, varFileInfo) { |
| 1107 | var l = 8 + (varFileInfo.szKey.length * 2); |
| 1108 | const buf = Buffer.alloc(padPointer(l)); |
| 1109 | buf.writeUInt16LE(0, 4); // wType |
| 1110 | stringToUnicode(varFileInfo.szKey, buf, 6); |
| 1111 | bufArray.push(buf); |
| 1112 | |
| 1113 | var wLength = 0; |
| 1114 | var wValueLength = 0; |
| 1115 | |
| 1116 | if (varFileInfo.value) { |
| 1117 | bufArray.push(varFileInfo.value); |
| 1118 | wLength += varFileInfo.value.length; |
| 1119 | wValueLength += varFileInfo.value.length; |
| 1120 | } |
| 1121 | buf.writeUInt16LE(buf.length + wLength, 0); // wLength |
| 1122 | buf.writeUInt16LE(wValueLength, 2); // wValueLength |
| 1123 | return buf.length + wLength; |
| 1124 | } |
| 1125 | |
| 1126 | // StringTable structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringtable |
| 1127 | function writeStringTableStruct(bufArray, stringTable) { |
| 1128 | //console.log('writeStringTableStruct', stringTable); |
| 1129 | var l = 6 + (stringTable.szKey.length * 2); |
| 1130 | const buf = Buffer.alloc(padPointer(l)); |
| 1131 | buf.writeUInt16LE(1, 4); // wType |
| 1132 | stringToUnicode(stringTable.szKey, buf, 6); |
| 1133 | bufArray.push(buf); |
| 1134 | |
| 1135 | var wLength = 0; |
| 1136 | var wValueLength = 0; |
| 1137 | |
| 1138 | if (stringTable.strings) { wLength += writeStringStructs(bufArray, stringTable.strings); } |
| 1139 | buf.writeUInt16LE(l + wLength, 0); // wLength |
| 1140 | buf.writeUInt16LE(wValueLength, 2); // wValueLength |
| 1141 | return buf.length + wLength; |
| 1142 | } |
| 1143 | |
| 1144 | // String structure: https://docs.microsoft.com/en-us/windows/win32/menurc/string-str |
| 1145 | function writeStringStructs(bufArray, stringTable) { |
| 1146 | //console.log('writeStringStructs', stringTable); |
| 1147 | var totalLen = 0, bufadd = 0; |
| 1148 | for (var i in stringTable) { |
| 1149 | //console.log('writeStringStructs', stringTable[i]); |
| 1150 | const buf = Buffer.alloc(padPointer(6 + ((stringTable[i].key.length + 1) * 2))); |
| 1151 | var buf2, wLength = buf.length; |
| 1152 | var wValueLength = 0; |
| 1153 | stringToUnicode(stringTable[i].key, buf, 6); |
| 1154 | bufArray.push(buf); |
| 1155 | bufadd += buf.length; |
| 1156 | if (typeof stringTable[i].value == 'string') { |
| 1157 | // wType (string) |
| 1158 | buf.writeUInt16LE(1, 4); |
| 1159 | var l = (stringTable[i].value.length + 1) * 2; |
| 1160 | buf2 = Buffer.alloc(padPointer(l)); |
| 1161 | stringToUnicode(stringTable[i].value, buf2, 0); |
| 1162 | bufArray.push(buf2); |
| 1163 | bufadd += buf2.length; |
| 1164 | wValueLength = stringTable[i].value.length + 1; |
| 1165 | wLength += l; |
| 1166 | } |
| 1167 | if (typeof stringTable[i].value == 'object') { |
| 1168 | // wType (binary) |
| 1169 | buf.writeUInt16LE(2, 4); // TODO: PADDING |
| 1170 | bufArray.push(stringTable[i].value); |
| 1171 | bufadd += stringTable[i].value.length; |
| 1172 | wValueLength = stringTable[i].value.length; |
| 1173 | wLength += wValueLength; |
| 1174 | } |
| 1175 | buf.writeUInt16LE(wLength, 0); // wLength |
| 1176 | buf.writeUInt16LE(wValueLength, 2); // wValueLength |
| 1177 | //console.log('WStringStruct', buf.toString('hex'), buf2.toString('hex')); |
| 1178 | totalLen += wLength; |
| 1179 | } |
| 1180 | //return totalLen; |
| 1181 | return bufadd; |
| 1182 | } |
| 1183 | |
| 1184 | // VS_VERSIONINFO structure: https://docs.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo |
| 1185 | function readVersionInfo(buf, ptr) { |
| 1186 | const r = {}; |
| 1187 | if (buf.length < 2) return null; |
| 1188 | const wLength = buf.readUInt16LE(ptr); |
| 1189 | if (buf.length < wLength) return null; |
| 1190 | const wValueLength = buf.readUInt16LE(ptr + 2); |
| 1191 | const wType = buf.readUInt16LE(ptr + 4); |
| 1192 | r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 36)); |
| 1193 | if (r.szKey != 'VS_VERSION_INFO') return null; |
| 1194 | //console.log('getVersionInfo', wLength, wValueLength, wType, r.szKey.toString()); |
| 1195 | if (wValueLength == 52) { r.fixedFileInfo = readFixedFileInfoStruct(buf, ptr + 40); } |
| 1196 | r.stringFiles = readStringFilesStruct(buf, ptr + 40 + wValueLength, wLength - 40 - wValueLength); |
| 1197 | return r; |
| 1198 | } |
| 1199 | |
| 1200 | // VS_FIXEDFILEINFO structure: https://docs.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo |
| 1201 | function readFixedFileInfoStruct(buf, ptr) { |
| 1202 | if (buf.length - ptr < 50) return null; |
| 1203 | var r = {}; |
| 1204 | r.dwSignature = buf.readUInt32LE(ptr); |
| 1205 | if (r.dwSignature != 0xFEEF04BD) return null; |
| 1206 | r.dwStrucVersion = buf.readUInt32LE(ptr + 4); |
| 1207 | r.dwFileVersionMS = buf.readUInt32LE(ptr + 8); |
| 1208 | r.dwFileVersionLS = buf.readUInt32LE(ptr + 12); |
| 1209 | r.dwProductVersionMS = buf.readUInt32LE(ptr + 16); |
| 1210 | r.dwProductVersionLS = buf.readUInt32LE(ptr + 20); |
| 1211 | r.dwFileFlagsMask = buf.readUInt32LE(ptr + 24); |
| 1212 | r.dwFileFlags = buf.readUInt32LE(ptr + 28); |
| 1213 | r.dwFileOS = buf.readUInt32LE(ptr + 32); |
| 1214 | r.dwFileType = buf.readUInt32LE(ptr + 36); |
| 1215 | r.dwFileSubtype = buf.readUInt32LE(ptr + 40); |
| 1216 | r.dwFileDateMS = buf.readUInt32LE(ptr + 44); |
| 1217 | r.dwFileDateLS = buf.readUInt32LE(ptr + 48); |
| 1218 | return r; |
| 1219 | } |
| 1220 | |
| 1221 | // StringFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo |
| 1222 | function readStringFilesStruct(buf, ptr, len) { |
| 1223 | var t = [], startPtr = ptr; |
| 1224 | while (ptr < (startPtr + len)) { |
| 1225 | const r = {}; |
| 1226 | const wLength = buf.readUInt16LE(ptr); |
| 1227 | if (wLength == 0) return t; |
| 1228 | const wValueLength = buf.readUInt16LE(ptr + 2); |
| 1229 | const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary |
| 1230 | r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 6 + (wLength - 6))); // String value |
| 1231 | //console.log('readStringFileStruct', wLength, wValueLength, wType, r.szKey); |
| 1232 | if (r.szKey == 'StringFileInfo') { r.stringTable = readStringTableStruct(buf, ptr + 36); } |
| 1233 | if (r.szKey == 'VarFileInfo') { r.varFileInfo = readVarFileInfoStruct(buf, ptr + 32); } |
| 1234 | t.push(r); |
| 1235 | ptr += wLength; |
| 1236 | ptr = padPointer(ptr); |
| 1237 | } |
| 1238 | return t; |
| 1239 | } |
| 1240 | |
| 1241 | // VarFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/var-str |
| 1242 | function readVarFileInfoStruct(buf, ptr) { |
| 1243 | const r = {}; |
| 1244 | const wLength = buf.readUInt16LE(ptr); |
| 1245 | const wValueLength = buf.readUInt16LE(ptr + 2); |
| 1246 | const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary |
| 1247 | r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + wLength)); // "VarFileInfo" |
| 1248 | r.value = buf.slice(ptr + wLength - wValueLength, ptr + wLength) |
| 1249 | //console.log('readVarFileInfoStruct', wLength, wValueLength, wType, r.szKey, r.value.toString('hex')); |
| 1250 | return r; |
| 1251 | } |
| 1252 | |
| 1253 | // StringTable structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringtable |
| 1254 | function readStringTableStruct(buf, ptr) { |
| 1255 | const r = {}; |
| 1256 | const wLength = buf.readUInt16LE(ptr); |
| 1257 | const wValueLength = buf.readUInt16LE(ptr + 2); |
| 1258 | const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary |
| 1259 | //console.log('RStringTableStruct', buf.slice(ptr, ptr + wLength).toString('hex')); |
| 1260 | r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 6 + 16)); // An 8-digit hexadecimal number stored as a Unicode string. |
| 1261 | //console.log('readStringTableStruct', wLength, wValueLength, wType, r.szKey); |
| 1262 | r.strings = readStringStructs(buf, ptr + 24 + wValueLength, wLength - 22); |
| 1263 | return r; |
| 1264 | } |
| 1265 | |
| 1266 | // String structure: https://docs.microsoft.com/en-us/windows/win32/menurc/string-str |
| 1267 | function readStringStructs(buf, ptr, len) { |
| 1268 | var t = [], startPtr = ptr; |
| 1269 | while ((ptr + 6) < (startPtr + len)) { |
| 1270 | const r = {}; |
| 1271 | const wLength = buf.readUInt16LE(ptr); |
| 1272 | if (wLength == 0) return t; |
| 1273 | |
| 1274 | //console.log('RStringStruct', buf.slice(ptr, ptr + wLength).toString('hex')); |
| 1275 | |
| 1276 | const wValueLength = buf.readUInt16LE(ptr + 2); |
| 1277 | const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary |
| 1278 | |
| 1279 | //console.log('R', buf.slice(ptr, ptr + wLength).toString('hex')); |
| 1280 | |
| 1281 | r.key = unicodeToString(buf.slice(ptr + 6, ptr + (wLength - (wValueLength * 2)) - 2)); // Key |
| 1282 | if (wType == 1) { r.value = unicodeToString(buf.slice(ptr + wLength - (wValueLength * 2), ptr + wLength - 2)); } // String value |
| 1283 | if (wType == 2) { r.value = buf.slice(ptr + wLength - (wValueLength * 2), ptr + wLength); } // Binary value |
| 1284 | //console.log('readStringStruct', wLength, wValueLength, wType, r.key, r.value); |
| 1285 | t.push(r); |
| 1286 | ptr += wLength; |
| 1287 | ptr = padPointer(ptr); |
| 1288 | } |
| 1289 | return t; |
| 1290 | } |
| 1291 | |
| 1292 | // Return the next 4 byte aligned number |
| 1293 | function padPointer(ptr) { return ptr + (((ptr % 4) == 0) ? 0 : (4 - (ptr % 4))); } |
| 1294 | //function padPointer(ptr) { return ptr + (ptr % 4); } |
| 1295 | |
| 1296 | // Hash the file using the selected hashing system |
| 1297 | // This hash skips the executables CRC and code signing data and signing block |
| 1298 | obj.getHash = function(algo) { |
| 1299 | const hash = crypto.createHash(algo); |
| 1300 | runHash(hash, 0, obj.header.peHeaderLocation + 88); |
| 1301 | runHash(hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); |
| 1302 | runHash(hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize); |
| 1303 | return hash.digest(); |
| 1304 | } |
| 1305 | |
| 1306 | // Hash of an open file using the selected hashing system |
| 1307 | // This hash skips the executables CRC and code signing data and signing block |
| 1308 | obj.getHashOfFile = function(fd, algo, filesize) { |
| 1309 | const hash = crypto.createHash(algo); |
| 1310 | runHashOnFile(fd, hash, 0, obj.header.peHeaderLocation + 88); |
| 1311 | runHashOnFile(fd, hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); |
| 1312 | runHashOnFile(fd, hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : filesize); |
| 1313 | return hash.digest(); |
| 1314 | } |
| 1315 | |
| 1316 | // Hash the file using the selected hashing system skipping resource section |
| 1317 | // This hash skips the executables CRC, sections table, resource section, code signing data and signing block |
| 1318 | obj.getHashNoResources = function (algo) { |
| 1319 | if (obj.header.sections['.rsrc'] == null) { return obj.getHash(algo); } // No resources in this executable, return a normal hash |
| 1320 | |
| 1321 | // Get the sections table start and size |
| 1322 | const sectionHeaderPtr = obj.header.SectionHeadersPtr; |
| 1323 | const sectionHeaderSize = obj.header.coff.numberOfSections * 40; |
| 1324 | |
| 1325 | // Get the resource section start and size |
| 1326 | const resPtr = obj.header.sections['.rsrc'].rawAddr; |
| 1327 | const resSize = obj.header.sections['.rsrc'].rawSize; |
| 1328 | |
| 1329 | // Get the end-of-file location |
| 1330 | const eof = obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize; |
| 1331 | |
| 1332 | // Hash the remaining data |
| 1333 | const hash = crypto.createHash(algo); |
| 1334 | runHash(hash, 0, obj.header.peHeaderLocation + 88); |
| 1335 | runHash(hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); |
| 1336 | runHash(hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, sectionHeaderPtr); |
| 1337 | runHash(hash, sectionHeaderPtr + sectionHeaderSize, resPtr); |
| 1338 | runHash(hash, resPtr + resSize, eof); |
| 1339 | return hash.digest(); |
| 1340 | } |
| 1341 | |
| 1342 | // Hash the file using the selected hashing system skipping resource section |
| 1343 | // This hash skips the executables CRC, sections table, resource section, code signing data and signing block |
| 1344 | obj.getHashOfSection = function (algo, sectionName) { |
| 1345 | if (obj.header.sections[sectionName] == null) return null; |
| 1346 | |
| 1347 | // Get the section start and size |
| 1348 | const sectionPtr = obj.header.sections[sectionName].rawAddr; |
| 1349 | const sectionSize = obj.header.sections[sectionName].rawSize; |
| 1350 | |
| 1351 | // Hash the remaining data |
| 1352 | const hash = crypto.createHash(algo); |
| 1353 | runHash(hash, sectionPtr, sectionPtr + sectionSize); |
| 1354 | return hash.digest(); |
| 1355 | } |
| 1356 | |
| 1357 | // Hash the file from start to end loading 64k chunks |
| 1358 | function runHash(hash, start, end) { |
| 1359 | var ptr = start; |
| 1360 | while (ptr < end) { const buf = readFileSlice(ptr, Math.min(65536, end - ptr)); hash.update(buf); ptr += buf.length; } |
| 1361 | } |
| 1362 | |
| 1363 | // Hash the open file loading 64k chunks |
| 1364 | // TODO: Do chunks on this!!! |
| 1365 | function runHashOnFile(fd, hash, start, end) { |
| 1366 | const buf = Buffer.alloc(end - start); |
| 1367 | const len = fs.readSync(fd, buf, 0, buf.length, start); |
| 1368 | if (len != buf.length) { console.log('BAD runHashOnFile'); } |
| 1369 | hash.update(buf); |
| 1370 | } |
| 1371 | |
| 1372 | // Checksum the file loading 64k chunks |
| 1373 | function runChecksum() { |
| 1374 | var ptr = 0, c = createChecksum(((obj.header.peOptionalHeaderLocation + 64) / 4)); |
| 1375 | while (ptr < obj.filesize) { const buf = readFileSlice(ptr, Math.min(65536, obj.filesize - ptr)); c.update(buf); ptr += buf.length; } |
| 1376 | return c.digest(); |
| 1377 | } |
| 1378 | |
| 1379 | // Checksum the open file loading 64k chunks |
| 1380 | function runChecksumOnFile(fd, filesize, checksumLocation) { |
| 1381 | var ptr = 0, c = createChecksum(checksumLocation), buf = Buffer.alloc(65536); |
| 1382 | while (ptr < filesize) { var len = fs.readSync(fd, buf, 0, Math.min(65536, filesize - ptr), ptr); c.update(buf, len); ptr += len; } |
| 1383 | return c.digest(); |
| 1384 | } |
| 1385 | |
| 1386 | // Steaming checksum methods |
| 1387 | // TODO: Works only with files padded to 4 byte. |
| 1388 | function createChecksum(checksumLocation) { |
| 1389 | const obj = { checksum: 0, length: 0 }; |
| 1390 | obj.update = function (data, len) { |
| 1391 | if (!len) { len = data.length; } |
| 1392 | for (var i = 0; i < (len / 4) ; i++) { |
| 1393 | if (((obj.length / 4) + i) == checksumLocation) continue; // Skip PE checksum location |
| 1394 | const dword = data.readUInt32LE(i * 4); |
| 1395 | var checksumlo = (obj.checksum > 4294967296) ? (obj.checksum - 4294967296) : obj.checksum; |
| 1396 | var checksumhi = (obj.checksum > 4294967296) ? 1 : 0; |
| 1397 | obj.checksum = checksumlo + dword + checksumhi; |
| 1398 | if (obj.checksum > 4294967296) { |
| 1399 | checksumlo = (obj.checksum > 4294967296) ? (obj.checksum - 4294967296) : obj.checksum; |
| 1400 | checksumhi = (obj.checksum > 4294967296) ? 1 : 0; |
| 1401 | obj.checksum = checksumlo + checksumhi; |
| 1402 | } |
| 1403 | } |
| 1404 | obj.length += len; |
| 1405 | } |
| 1406 | obj.digest = function () { |
| 1407 | obj.checksum = (obj.checksum & 0xffff) + (obj.checksum >>> 16); |
| 1408 | obj.checksum = (obj.checksum) + (obj.checksum >>> 16); |
| 1409 | obj.checksum = obj.checksum & 0xffff; |
| 1410 | obj.checksum += obj.length; |
| 1411 | return obj.checksum; |
| 1412 | } |
| 1413 | return obj; |
| 1414 | } |
| 1415 | |
| 1416 | // Compute the PE checksum of an entire file |
| 1417 | function getChecksum(data, checksumLocation) { |
| 1418 | var checksum = 0; |
| 1419 | for (var i = 0; i < (data.length / 4) ; i++) { |
| 1420 | if (i == (checksumLocation / 4)) continue; // Skip PE checksum location |
| 1421 | var dword = data.readUInt32LE(i * 4); |
| 1422 | var checksumlo = (checksum > 4294967296) ? (checksum - 4294967296) : checksum; |
| 1423 | var checksumhi = (checksum > 4294967296) ? 1 : 0; |
| 1424 | checksum = checksumlo + dword + checksumhi; |
| 1425 | if (checksum > 4294967296) { |
| 1426 | checksumlo = (checksum > 4294967296) ? (checksum - 4294967296) : checksum; |
| 1427 | checksumhi = (checksum > 4294967296) ? 1 : 0; |
| 1428 | checksum = checksumlo + checksumhi; |
| 1429 | } |
| 1430 | } |
| 1431 | checksum = (checksum & 0xffff) + (checksum >>> 16); |
| 1432 | checksum = (checksum) + (checksum >>> 16); |
| 1433 | checksum = checksum & 0xffff; |
| 1434 | checksum += data.length; |
| 1435 | return checksum; |
| 1436 | } |
| 1437 | |
| 1438 | // Sign the file using the certificate and key. If none is specified, generate a dummy one |
| 1439 | obj.sign = function (cert, args, func) { |
| 1440 | if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); } |
| 1441 | |
| 1442 | // Set the hash algorithm hash OID |
| 1443 | var hashOid = null, fileHash = null; |
| 1444 | if (args.hash == null) { args.hash = 'sha384'; } |
| 1445 | if (args.hash == 'sha256') { hashOid = forge.pki.oids.sha256; fileHash = obj.getHash('sha256'); } |
| 1446 | if (args.hash == 'sha384') { hashOid = forge.pki.oids.sha384; fileHash = obj.getHash('sha384'); } |
| 1447 | if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); } |
| 1448 | if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); } |
| 1449 | if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); } |
| 1450 | if (hashOid == null) { func('Invalid signing hash: ' + args.hash); return; }; |
| 1451 | |
| 1452 | // Create the signature block |
| 1453 | var xp7 = forge.pkcs7.createSignedData(); |
| 1454 | var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] }; |
| 1455 | xp7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]); |
| 1456 | xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content])); |
| 1457 | xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign. |
| 1458 | xp7.addCertificate(cert.cert); |
| 1459 | if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { xp7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain |
| 1460 | |
| 1461 | // Build authenticated attributes |
| 1462 | var authenticatedAttributes = [ |
| 1463 | { type: forge.pki.oids.contentType, value: forge.pki.oids.data }, |
| 1464 | { type: forge.pki.oids.messageDigest } // This value will populated at signing time by node-forge |
| 1465 | ] |
| 1466 | if ((typeof args.desc == 'string') || (typeof args.url == 'string')) { |
| 1467 | var codeSigningAttributes = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [] }; |
| 1468 | if (args.desc != null) { // Encode description as big-endian unicode. |
| 1469 | var desc = "", ucs = Buffer.from(args.desc, 'ucs2').toString() |
| 1470 | for (var k = 0; k < ucs.length; k += 2) { desc += String.fromCharCode(ucs.charCodeAt(k + 1), ucs.charCodeAt(k)); } |
| 1471 | codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': desc }] }); |
| 1472 | } |
| 1473 | if (args.url != null) { codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 1, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': args.url }] }); } |
| 1474 | authenticatedAttributes.push({ type: obj.Oids.SPC_SP_OPUS_INFO_OBJID, value: codeSigningAttributes }); |
| 1475 | } |
| 1476 | |
| 1477 | // Add the signer and sign |
| 1478 | xp7.addSigner({ |
| 1479 | key: cert.key, |
| 1480 | certificate: cert.cert, |
| 1481 | digestAlgorithm: forge.pki.oids.sha384, |
| 1482 | authenticatedAttributes: authenticatedAttributes |
| 1483 | }); |
| 1484 | xp7.sign(); |
| 1485 | var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64'); |
| 1486 | |
| 1487 | if (args.time == null) { |
| 1488 | // Sign the executable without timestamp |
| 1489 | signEx(args, p7signature, obj.filesize, func); |
| 1490 | } else { |
| 1491 | // Decode the signature block |
| 1492 | var pkcs7der = null; |
| 1493 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; } |
| 1494 | |
| 1495 | // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future |
| 1496 | // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1" |
| 1497 | pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data; |
| 1498 | |
| 1499 | // Decode the PKCS7 message |
| 1500 | var pkcs7 = p7.messageFromAsn1(pkcs7der); |
| 1501 | |
| 1502 | // Create the timestamp request in DER format |
| 1503 | const asn1 = forge.asn1; |
| 1504 | const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data; |
| 1505 | const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data; |
| 1506 | const asn1obj = |
| 1507 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 1508 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid), |
| 1509 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 1510 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid), |
| 1511 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 1512 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here |
| 1513 | ]) |
| 1514 | ]) |
| 1515 | ]); |
| 1516 | |
| 1517 | // Re-decode the PKCS7 from the executable, this time, no workaround needed |
| 1518 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; } |
| 1519 | |
| 1520 | // Serialize an ASN.1 object to DER format in Base64 |
| 1521 | const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64'); |
| 1522 | |
| 1523 | // Make an HTTP request |
| 1524 | const options = { url: args.time, proxy: args.proxy }; |
| 1525 | |
| 1526 | // Make a request to the time server |
| 1527 | httpRequest(options, requestBody, function (err, data) { |
| 1528 | if (err != null) { func(err); return; } |
| 1529 | |
| 1530 | // Decode the timestamp signature block |
| 1531 | var timepkcs7der = null; |
| 1532 | try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; } |
| 1533 | |
| 1534 | try { |
| 1535 | // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable |
| 1536 | // TODO: We could look to see if the certificate is already present in the executable |
| 1537 | const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value; |
| 1538 | for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); } |
| 1539 | |
| 1540 | // Get the time signature and add it to the executables PKCS7 |
| 1541 | const timeasn1Signature = timepkcs7der.value[1].value[0].value[4]; |
| 1542 | const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data; |
| 1543 | const asn1obj2 = |
| 1544 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| 1545 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 1546 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid), |
| 1547 | timeasn1Signature |
| 1548 | ]) |
| 1549 | ]); |
| 1550 | pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2); |
| 1551 | |
| 1552 | // Re-encode the executable signature block |
| 1553 | const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary'); |
| 1554 | |
| 1555 | // Write the file with the signature block |
| 1556 | signEx(args, p7signature, obj.filesize, func); |
| 1557 | } catch (ex) { func('' + ex); } |
| 1558 | }); |
| 1559 | } |
| 1560 | } |
| 1561 | |
| 1562 | // Make a HTTP request, use a proxy if needed |
| 1563 | function httpRequest(options, requestBody, func) { |
| 1564 | // Decode the URL |
| 1565 | const timeServerUrl = new URL(options.url); |
| 1566 | options.protocol = timeServerUrl.protocol; |
| 1567 | options.hostname = timeServerUrl.hostname; |
| 1568 | options.path = timeServerUrl.pathname; |
| 1569 | let http = require("http") |
| 1570 | if (options.protocol === "https:"){ |
| 1571 | http = require("https") |
| 1572 | } |
| 1573 | options.port = ((timeServerUrl.port == '') ? (options.protocol === "https:" ? 443 : 80) : parseInt(timeServerUrl.port)); |
| 1574 | |
| 1575 | if (options.proxy == null) { |
| 1576 | // No proxy needed |
| 1577 | |
| 1578 | // Setup the options |
| 1579 | delete options.url; |
| 1580 | options.method = 'POST'; |
| 1581 | options.headers = { |
| 1582 | 'accept': 'application/octet-stream', |
| 1583 | 'cache-control': 'no-cache', |
| 1584 | 'user-agent': 'Transport', |
| 1585 | 'content-type': 'application/octet-stream', |
| 1586 | 'content-length': Buffer.byteLength(requestBody) |
| 1587 | }; |
| 1588 | |
| 1589 | // Set up the request |
| 1590 | var responseAccumulator = ''; |
| 1591 | var req = http.request(options, function (res) { |
| 1592 | res.setEncoding('utf8'); |
| 1593 | res.on('data', function (chunk) { responseAccumulator += chunk; }); |
| 1594 | res.on('end', function () { func(null, responseAccumulator); }); |
| 1595 | }); |
| 1596 | |
| 1597 | // Post the data |
| 1598 | req.on('error', function (err) { func('' + err); }); |
| 1599 | req.write(requestBody); |
| 1600 | req.end(); |
| 1601 | } else { |
| 1602 | // We are using a proxy |
| 1603 | // This is a fairly basic proxy implementation, should work most of the time. |
| 1604 | |
| 1605 | // Setup the options and decode the proxy URL |
| 1606 | var proxyOptions = { method: 'CONNECT' }; |
| 1607 | if (options.proxy) { |
| 1608 | const proxyUrl = new URL(options.proxy); |
| 1609 | proxyOptions.protocol = proxyUrl.protocol; |
| 1610 | proxyOptions.hostname = proxyUrl.hostname; |
| 1611 | proxyOptions.path = options.hostname + ':' + options.port; |
| 1612 | proxyOptions.port = ((proxyUrl.port == '') ? (options.protocol === "https:" ? 443 : 80) : parseInt(proxyUrl.port)); |
| 1613 | } |
| 1614 | |
| 1615 | // Set up the proxy request |
| 1616 | var responseAccumulator = ''; |
| 1617 | var req = http.request(proxyOptions); |
| 1618 | req.on('error', function (err) { func('' + err); }); |
| 1619 | req.on('connect', function (res, socket, head) { |
| 1620 | // Make a request over the HTTP tunnel |
| 1621 | socket.write('POST ' + options.path + ' HTTP/1.1\r\n' + |
| 1622 | 'host: ' + options.hostname + ':' + options.port + '\r\n' + |
| 1623 | 'accept: application/octet-stream\r\n' + |
| 1624 | 'cache-control: no-cache\r\n' + |
| 1625 | 'user-agent: Transport\r\n' + |
| 1626 | 'content-type: application/octet-stream\r\n' + |
| 1627 | 'content-length: ' + Buffer.byteLength(requestBody) + '\r\n' + |
| 1628 | '\r\n' + requestBody); |
| 1629 | socket.on('data', function (chunk) { |
| 1630 | responseAccumulator += chunk.toString(); |
| 1631 | var responseData = parseHttpResponse(responseAccumulator); |
| 1632 | if (responseData != null) { try { socket.end(); } catch (ex) { console.log('ex', ex); } socket.xdone = true; func(null, responseData); } |
| 1633 | }); |
| 1634 | socket.on('end', function () { |
| 1635 | if (socket.xdone == true) return; |
| 1636 | var responseData = parseHttpResponse(responseAccumulator); |
| 1637 | if (responseData != null) { func(null, responseData); } else { func("Unable to parse response."); } |
| 1638 | }); |
| 1639 | }); |
| 1640 | req.end(); |
| 1641 | } |
| 1642 | } |
| 1643 | |
| 1644 | // Parse the HTTP response and return data if available |
| 1645 | function parseHttpResponse(data) { |
| 1646 | var dataSplit = data.split('\r\n\r\n'); |
| 1647 | if (dataSplit.length < 2) return null; |
| 1648 | |
| 1649 | // Parse the HTTP header |
| 1650 | var headerSplit = dataSplit[0].split('\r\n'), headers = {}; |
| 1651 | for (var i in headerSplit) { |
| 1652 | if (i != 0) { |
| 1653 | var x = headerSplit[i].indexOf(':'); |
| 1654 | headers[headerSplit[i].substring(0, x).toLowerCase()] = headerSplit[i].substring(x + 2); |
| 1655 | } |
| 1656 | } |
| 1657 | |
| 1658 | // If there is a content-length in the header, keep accumulating data until we have the right length |
| 1659 | if (headers['content-length'] != null) { |
| 1660 | const contentLength = parseInt(headers['content-length']); |
| 1661 | if (dataSplit[1].length < contentLength) return null; // Wait for more data |
| 1662 | return dataSplit[1]; |
| 1663 | } |
| 1664 | return dataSplit[1]; |
| 1665 | } |
| 1666 | |
| 1667 | // Complete the signature of an executable |
| 1668 | function signEx(args, p7signature, filesize, func) { |
| 1669 | // Open the output file |
| 1670 | var output = null; |
| 1671 | try { output = fs.openSync(args.out, 'w+'); } catch (ex) { } |
| 1672 | if (output == null) { func('Unable to open output file: ' + args.out); return; } |
| 1673 | var tmp, written = 0, executableSize = obj.header.sigpos ? obj.header.sigpos : filesize; |
| 1674 | |
| 1675 | // Compute pre-header length and copy that to the new file |
| 1676 | var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); |
| 1677 | var tmp = readFileSlice(written, preHeaderLen); |
| 1678 | fs.writeSync(output, tmp); |
| 1679 | written += tmp.length; |
| 1680 | |
| 1681 | // Quad Align the results, adding padding if necessary |
| 1682 | var len = executableSize + p7signature.length; |
| 1683 | var padding = (8 - ((len) % 8)) % 8; |
| 1684 | |
| 1685 | // Write the signature header |
| 1686 | var addresstable = Buffer.alloc(8); |
| 1687 | addresstable.writeUInt32LE(executableSize); |
| 1688 | addresstable.writeUInt32LE(8 + p7signature.length + padding, 4); |
| 1689 | fs.writeSync(output, addresstable); |
| 1690 | written += addresstable.length; |
| 1691 | |
| 1692 | // Copy the rest of the file until the start of the signature block |
| 1693 | while ((executableSize - written) > 0) { |
| 1694 | tmp = readFileSlice(written, Math.min(executableSize - written, 65536)); |
| 1695 | fs.writeSync(output, tmp); |
| 1696 | written += tmp.length; |
| 1697 | } |
| 1698 | |
| 1699 | // Write the signature block header and signature |
| 1700 | var win = Buffer.alloc(8); // WIN CERTIFICATE Structure |
| 1701 | win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length |
| 1702 | win.writeUInt16LE(512, 4); // WORD revision |
| 1703 | win.writeUInt16LE(2, 6); // WORD type |
| 1704 | fs.writeSync(output, win); |
| 1705 | fs.writeSync(output, p7signature); |
| 1706 | if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); } |
| 1707 | written += (p7signature.length + padding + 8); |
| 1708 | |
| 1709 | // Compute the checksum and write it in the PE header checksum location |
| 1710 | var tmp = Buffer.alloc(4); |
| 1711 | tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4))); |
| 1712 | fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64); |
| 1713 | |
| 1714 | // Close the file |
| 1715 | fs.closeSync(output); |
| 1716 | func(null); |
| 1717 | } |
| 1718 | |
| 1719 | // Save an executable without the signature |
| 1720 | obj.unsign = function (args) { |
| 1721 | // Open the file |
| 1722 | var output = fs.openSync(args.out, 'w+'); |
| 1723 | var written = 0, totalWrite = obj.header.sigpos; |
| 1724 | |
| 1725 | // Compute pre-header length and copy that to the new file |
| 1726 | var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); |
| 1727 | var tmp = readFileSlice(written, preHeaderLen); |
| 1728 | fs.writeSync(output, tmp); |
| 1729 | written += tmp.length; |
| 1730 | |
| 1731 | // Write the new signature header |
| 1732 | fs.writeSync(output, Buffer.alloc(8)); |
| 1733 | written += 8; |
| 1734 | |
| 1735 | // Copy the rest of the file until the start of the signature block |
| 1736 | while ((totalWrite - written) > 0) { |
| 1737 | tmp = readFileSlice(written, Math.min(totalWrite - written, 65536)); |
| 1738 | fs.writeSync(output, tmp); |
| 1739 | written += tmp.length; |
| 1740 | } |
| 1741 | |
| 1742 | // Compute the checksum and write it in the PE checksum header at position |
| 1743 | var tmp = Buffer.alloc(4); |
| 1744 | tmp.writeUInt32LE(runChecksumOnFile(output, written)); |
| 1745 | fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64); |
| 1746 | |
| 1747 | fs.closeSync(output); |
| 1748 | } |
| 1749 | |
| 1750 | // Find where a directory value is in the old sections and map it to the new sections |
| 1751 | function correctDirectoryValue(oldSections, newSections, value) { |
| 1752 | for (var i in oldSections) { |
| 1753 | if ((value >= oldSections[i].virtualAddr) && (value < (oldSections[i].virtualAddr + oldSections[i].virtualSize))) { |
| 1754 | return newSections[i].virtualAddr + (value - oldSections[i].virtualAddr); |
| 1755 | } |
| 1756 | } |
| 1757 | return 0; |
| 1758 | } |
| 1759 | |
| 1760 | // Save the executable |
| 1761 | obj.writeExecutable = function (args, cert, func) { |
| 1762 | // Open the file |
| 1763 | var output = fs.openSync(args.out, 'w+'); |
| 1764 | var tmp, written = 0; |
| 1765 | |
| 1766 | // Compute the size of the complete executable header up to after the sections header |
| 1767 | var fullHeaderLen = obj.header.SectionHeadersPtr + (obj.header.coff.numberOfSections * 40); |
| 1768 | var fullHeader = readFileSlice(written, fullHeaderLen); |
| 1769 | |
| 1770 | // Create the resource section |
| 1771 | const rsrcSectionX = generateResourceSection(obj.resources); // This section is created with padding already included |
| 1772 | var rsrcSection = rsrcSectionX.data; |
| 1773 | var rsrcSectionVirtualSize = rsrcSectionX.size; |
| 1774 | var rsrcSectionRawSize = rsrcSection.length; |
| 1775 | |
| 1776 | // Calculate the location and original and new size of the resource segment |
| 1777 | var fileAlign = obj.header.peWindows.fileAlignment |
| 1778 | var resPtr = obj.header.sections['.rsrc'].rawAddr; |
| 1779 | var oldResSize = obj.header.sections['.rsrc'].rawSize; |
| 1780 | var newResSize = rsrcSection.length; |
| 1781 | var resDeltaSize = newResSize - oldResSize; |
| 1782 | |
| 1783 | // Compute the sizeOfInitializedData |
| 1784 | var sizeOfInitializedData = 0; |
| 1785 | for (var i in obj.header.sections) { |
| 1786 | if (i != '.text') { |
| 1787 | if (i == '.rsrc') { |
| 1788 | sizeOfInitializedData += rsrcSectionRawSize; |
| 1789 | } else { |
| 1790 | sizeOfInitializedData += obj.header.sections[i].rawSize; |
| 1791 | } |
| 1792 | } |
| 1793 | } |
| 1794 | |
| 1795 | // Change PE optional header sizeOfInitializedData standard field |
| 1796 | fullHeader.writeUInt32LE(sizeOfInitializedData, obj.header.peOptionalHeaderLocation + 8); |
| 1797 | |
| 1798 | // Update the checksum to zero |
| 1799 | fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 64); |
| 1800 | |
| 1801 | // We are going to setup the old a new sections here, we need this to correct directory values |
| 1802 | var oldSections = obj.header.sections; |
| 1803 | var newSections = {}; |
| 1804 | |
| 1805 | // Make changes to the segments table |
| 1806 | var virtualAddress = 4096; |
| 1807 | for (var i in obj.header.sections) { |
| 1808 | const section = obj.header.sections[i]; |
| 1809 | newSections[i] = { virtualSize: section.virtualSize }; |
| 1810 | if (i == '.rsrc') { |
| 1811 | // Change the size of the resource section |
| 1812 | fullHeader.writeUInt32LE(rsrcSectionVirtualSize, section.ptr + 8); // virtualSize |
| 1813 | fullHeader.writeUInt32LE(rsrcSectionRawSize, section.ptr + 16); // rawSize |
| 1814 | |
| 1815 | // Set the virtual address of the section |
| 1816 | fullHeader.writeUInt32LE(virtualAddress, section.ptr + 12); // Virtual address |
| 1817 | newSections[i].virtualAddr = virtualAddress; |
| 1818 | var virtualAddressPadding = (rsrcSectionVirtualSize % 4096); |
| 1819 | virtualAddress += rsrcSectionVirtualSize; |
| 1820 | if (virtualAddressPadding != 0) { virtualAddress += (4096 - virtualAddressPadding); } |
| 1821 | } else { |
| 1822 | // Change the location of any other section if located after the resource section |
| 1823 | if (section.rawAddr > resPtr) { fullHeader.writeUInt32LE(section.rawAddr + resDeltaSize, section.ptr + 20); } |
| 1824 | |
| 1825 | // Set the virtual address of the section |
| 1826 | fullHeader.writeUInt32LE(virtualAddress, section.ptr + 12); // Virtual address |
| 1827 | newSections[i].virtualAddr = virtualAddress; |
| 1828 | var virtualAddressPadding = (section.virtualSize % 4096); |
| 1829 | virtualAddress += section.virtualSize; |
| 1830 | if (virtualAddressPadding != 0) { virtualAddress += (4096 - virtualAddressPadding); } |
| 1831 | } |
| 1832 | } |
| 1833 | |
| 1834 | // Make change to the data directories header to fix resource segment size and add/remove signature |
| 1835 | const pePlusOffset = (obj.header.pe32plus == 0) ? 0 : 16; // This header is the same for 32 and 64 bit, but 64bit is offset by 16 bytes. |
| 1836 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.exportTable.addr), obj.header.peOptionalHeaderLocation + 96 + pePlusOffset); |
| 1837 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.importTable.addr), obj.header.peOptionalHeaderLocation + 104 + pePlusOffset); |
| 1838 | fullHeader.writeUInt32LE(rsrcSectionVirtualSize, obj.header.peOptionalHeaderLocation + 116 + pePlusOffset); // Change the resource segment size |
| 1839 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.exceptionTableAddr.addr), obj.header.peOptionalHeaderLocation + 120 + pePlusOffset); |
| 1840 | fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 128 + pePlusOffset); // certificate table addr (TODO) |
| 1841 | fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 132 + pePlusOffset); // certificate table size (TODO) |
| 1842 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.baseRelocationTable.addr), obj.header.peOptionalHeaderLocation + 136 + pePlusOffset); |
| 1843 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.debug.addr), obj.header.peOptionalHeaderLocation + 144 + pePlusOffset); |
| 1844 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.globalPtr.addr), obj.header.peOptionalHeaderLocation + 160 + pePlusOffset); |
| 1845 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.tLSTable.addr), obj.header.peOptionalHeaderLocation + 168 + pePlusOffset); |
| 1846 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.loadConfigTable.addr), obj.header.peOptionalHeaderLocation + 176 + pePlusOffset); |
| 1847 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.boundImport.addr), obj.header.peOptionalHeaderLocation + 184 + pePlusOffset); |
| 1848 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.iAT.addr), obj.header.peOptionalHeaderLocation + 192 + pePlusOffset); |
| 1849 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.delayImportDescriptor.addr), obj.header.peOptionalHeaderLocation + 200 + pePlusOffset); |
| 1850 | fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.clrRuntimeHeader.addr), obj.header.peOptionalHeaderLocation + 208 + pePlusOffset); |
| 1851 | |
| 1852 | // Write size of image. We put the next virtual address. |
| 1853 | fullHeader.writeUInt32LE(virtualAddress, obj.header.peOptionalHeaderLocation + 56); // sizeOfImage |
| 1854 | |
| 1855 | // Write the entire header to the destination file |
| 1856 | //console.log('Write header', fullHeader.length, written); |
| 1857 | fs.writeSync(output, fullHeader); |
| 1858 | written += fullHeader.length; |
| 1859 | |
| 1860 | // Write the entire executable until the start to the resource segment |
| 1861 | var totalWrite = resPtr; |
| 1862 | //console.log('Write until res', totalWrite, written); |
| 1863 | while ((totalWrite - written) > 0) { |
| 1864 | tmp = readFileSlice(written, Math.min(totalWrite - written, 65536)); |
| 1865 | fs.writeSync(output, tmp); |
| 1866 | written += tmp.length; |
| 1867 | } |
| 1868 | |
| 1869 | // Write the new resource section |
| 1870 | fs.writeSync(output, rsrcSection); |
| 1871 | written += rsrcSection.length; |
| 1872 | //console.log('Write res', rsrcSection.length, written); |
| 1873 | |
| 1874 | // Write until the signature block |
| 1875 | if (obj.header.sigpos > 0) { |
| 1876 | // Since the original file was signed, write from the end of the resources to the start of the signature block. |
| 1877 | totalWrite = obj.header.sigpos + resDeltaSize; |
| 1878 | } else { |
| 1879 | // The original file was not signed, write from the end of the resources to the end of the file. |
| 1880 | totalWrite = obj.filesize + resDeltaSize; |
| 1881 | } |
| 1882 | |
| 1883 | //console.log('Write until signature', totalWrite, written); |
| 1884 | while ((totalWrite - written) > 0) { |
| 1885 | tmp = readFileSlice(written - resDeltaSize, Math.min(totalWrite - written, 65536)); |
| 1886 | fs.writeSync(output, tmp); |
| 1887 | written += tmp.length; |
| 1888 | } |
| 1889 | //console.log('Write to signature', written); |
| 1890 | |
| 1891 | // Write the signature if needed |
| 1892 | if (cert != null) { |
| 1893 | //if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); } |
| 1894 | |
| 1895 | // Set the hash algorithm hash OID |
| 1896 | var hashOid = null, fileHash = null; |
| 1897 | if (args.hash == null) { args.hash = 'sha384'; } |
| 1898 | if (args.hash == 'sha256') { hashOid = forge.pki.oids.sha256; fileHash = obj.getHashOfFile(output, 'sha256', written); } |
| 1899 | if (args.hash == 'sha384') { hashOid = forge.pki.oids.sha384; fileHash = obj.getHashOfFile(output, 'sha384', written); } |
| 1900 | if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHashOfFile(output, 'sha512', written); } |
| 1901 | if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHashOfFile(output, 'sha224', written); } |
| 1902 | if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHashOfFile(output, 'md5', written); } |
| 1903 | if (hashOid == null) { func('Bad hash method OID'); return; } |
| 1904 | |
| 1905 | // Create the signature block |
| 1906 | var xp7 = forge.pkcs7.createSignedData(); |
| 1907 | var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] }; |
| 1908 | xp7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]); |
| 1909 | xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content])); |
| 1910 | xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign. |
| 1911 | xp7.addCertificate(cert.cert); |
| 1912 | if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { xp7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain |
| 1913 | |
| 1914 | // Build authenticated attributes |
| 1915 | var authenticatedAttributes = [ |
| 1916 | { type: forge.pki.oids.contentType, value: forge.pki.oids.data }, |
| 1917 | { type: forge.pki.oids.messageDigest } // This value will populated at signing time by node-forge |
| 1918 | ] |
| 1919 | if ((typeof args.desc == 'string') || (typeof args.url == 'string')) { |
| 1920 | var codeSigningAttributes = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [] }; |
| 1921 | if (args.desc != null) { // Encode description as big-endian unicode. |
| 1922 | var desc = "", ucs = Buffer.from(args.desc, 'ucs2').toString() |
| 1923 | for (var k = 0; k < ucs.length; k += 2) { desc += String.fromCharCode(ucs.charCodeAt(k + 1), ucs.charCodeAt(k)); } |
| 1924 | codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': desc }] }); |
| 1925 | } |
| 1926 | if (args.url != null) { codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 1, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': args.url }] }); } |
| 1927 | authenticatedAttributes.push({ type: obj.Oids.SPC_SP_OPUS_INFO_OBJID, value: codeSigningAttributes }); |
| 1928 | } |
| 1929 | |
| 1930 | // Add the signer and sign |
| 1931 | xp7.addSigner({ |
| 1932 | key: cert.key, |
| 1933 | certificate: cert.cert, |
| 1934 | digestAlgorithm: forge.pki.oids.sha384, |
| 1935 | authenticatedAttributes: authenticatedAttributes |
| 1936 | }); |
| 1937 | xp7.sign(); |
| 1938 | var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64'); |
| 1939 | //console.log('Signature', Buffer.from(p7signature, 'binary').toString('base64')); |
| 1940 | |
| 1941 | if (args.time == null) { |
| 1942 | // Write the signature block to the output executable without time stamp |
| 1943 | writeExecutableEx(output, p7signature, written, func); |
| 1944 | } else { |
| 1945 | // Decode the signature block |
| 1946 | var pkcs7der = null; |
| 1947 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; } |
| 1948 | |
| 1949 | // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future |
| 1950 | // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1" |
| 1951 | pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data; |
| 1952 | |
| 1953 | // Decode the PKCS7 message |
| 1954 | var pkcs7 = p7.messageFromAsn1(pkcs7der); |
| 1955 | |
| 1956 | // Create the timestamp request in DER format |
| 1957 | const asn1 = forge.asn1; |
| 1958 | const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data; |
| 1959 | const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data; |
| 1960 | const asn1obj = |
| 1961 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 1962 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid), |
| 1963 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 1964 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid), |
| 1965 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ |
| 1966 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here |
| 1967 | ]) |
| 1968 | ]) |
| 1969 | ]); |
| 1970 | |
| 1971 | // Re-decode the PKCS7 from the executable, this time, no workaround needed |
| 1972 | try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; } |
| 1973 | |
| 1974 | // Serialize an ASN.1 object to DER format in Base64 |
| 1975 | const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64'); |
| 1976 | |
| 1977 | // Make an HTTP request |
| 1978 | const options = { url: args.time, proxy: args.proxy }; |
| 1979 | |
| 1980 | // Make a request to the time server |
| 1981 | httpRequest(options, requestBody, function (err, data) { |
| 1982 | if (err != null) { func(err); return; } |
| 1983 | |
| 1984 | // Decode the timestamp signature block |
| 1985 | var timepkcs7der = null; |
| 1986 | try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; } |
| 1987 | |
| 1988 | // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable |
| 1989 | // TODO: We could look to see if the certificate is already present in the executable |
| 1990 | try { |
| 1991 | var timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value; |
| 1992 | for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); } |
| 1993 | |
| 1994 | // Get the time signature and add it to the executables PKCS7 |
| 1995 | const timeasn1Signature = timepkcs7der.value[1].value[0].value[4]; |
| 1996 | const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data; |
| 1997 | const asn1obj2 = |
| 1998 | asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ |
| 1999 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ |
| 2000 | asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid), |
| 2001 | timeasn1Signature |
| 2002 | ]) |
| 2003 | ]); |
| 2004 | pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2); |
| 2005 | |
| 2006 | // Re-encode the executable signature block |
| 2007 | const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary'); |
| 2008 | |
| 2009 | // Write the file with the signature block |
| 2010 | writeExecutableEx(output, p7signature, written, func); |
| 2011 | } catch (ex) { func('' + ex); return; } // Something failed |
| 2012 | }); |
| 2013 | } |
| 2014 | return; |
| 2015 | } |
| 2016 | |
| 2017 | // Close the file |
| 2018 | fs.closeSync(output); |
| 2019 | |
| 2020 | // Indicate success |
| 2021 | func(null); |
| 2022 | } |
| 2023 | |
| 2024 | function writeExecutableEx(output, p7signature, written, func) { |
| 2025 | // Quad Align the results, adding padding if necessary |
| 2026 | var len = written + p7signature.length; |
| 2027 | var padding = (8 - ((len) % 8)) % 8; |
| 2028 | |
| 2029 | // Write the signature block header and signature |
| 2030 | var win = Buffer.alloc(8); // WIN CERTIFICATE Structure |
| 2031 | win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length |
| 2032 | win.writeUInt16LE(512, 4); // WORD revision |
| 2033 | win.writeUInt16LE(2, 6); // WORD type |
| 2034 | fs.writeSync(output, win); |
| 2035 | fs.writeSync(output, p7signature); |
| 2036 | if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); } |
| 2037 | |
| 2038 | // Write the signature header |
| 2039 | var addresstable = Buffer.alloc(8); |
| 2040 | addresstable.writeUInt32LE(written); |
| 2041 | addresstable.writeUInt32LE(8 + p7signature.length + padding, 4); |
| 2042 | var signatureHeaderLocation = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16)); |
| 2043 | fs.writeSync(output, addresstable, 0, 8, signatureHeaderLocation); |
| 2044 | written += (p7signature.length + padding + 8); // Add the signature block to written counter |
| 2045 | |
| 2046 | // Compute the checksum and write it in the PE header checksum location |
| 2047 | var tmp = Buffer.alloc(4); |
| 2048 | tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4))); |
| 2049 | fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64); |
| 2050 | |
| 2051 | // Close the file |
| 2052 | fs.closeSync(output); |
| 2053 | |
| 2054 | // Indicate success |
| 2055 | func(null); |
| 2056 | } |
| 2057 | |
| 2058 | // Return null if we could not open the file |
| 2059 | return (openFile() ? obj : null); |
| 2060 | } |
| 2061 | |
| 2062 | function start() { |
| 2063 | // Parse the arguments |
| 2064 | const args = require('minimist')(process.argv.slice(2)); |
| 2065 | |
| 2066 | // Show tool help |
| 2067 | if (process.argv.length < 3) { |
| 2068 | console.log("MeshCentral Authenticode Tool."); |
| 2069 | console.log("Usage:"); |
| 2070 | console.log(" node authenticode.js [command] [options]"); |
| 2071 | console.log("Commands:"); |
| 2072 | console.log(" info: Show information about an executable."); |
| 2073 | console.log(" --exe [file] Required executable to view information."); |
| 2074 | console.log(" --json Show information in JSON format."); |
| 2075 | console.log(" sign: Sign an executable."); |
| 2076 | console.log(" --exe [file] Required executable to sign."); |
| 2077 | console.log(" --out [file] Resulting signed executable."); |
| 2078 | console.log(" --pem [pemfile] Certificate & private key to sign the executable with."); |
| 2079 | console.log(" --desc [description] Description string to embbed into signature."); |
| 2080 | console.log(" --url [url] URL to embbed into signature."); |
| 2081 | console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512."); |
| 2082 | console.log(" --time [url] The time signing server URL."); |
| 2083 | console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://"); |
| 2084 | console.log(" unsign: Remove the signature from the executable."); |
| 2085 | console.log(" --exe [file] Required executable to un-sign."); |
| 2086 | console.log(" --out [file] Resulting executable with signature removed."); |
| 2087 | console.log(" createcert: Create a code signging self-signed certificate and key."); |
| 2088 | console.log(" --out [pemfile] Required certificate file to create."); |
| 2089 | console.log(" --cn [value] Required certificate common name."); |
| 2090 | console.log(" --country [value] Certificate country name."); |
| 2091 | console.log(" --state [value] Certificate state name."); |
| 2092 | console.log(" --locality [value] Certificate locality name."); |
| 2093 | console.log(" --org [value] Certificate organization name."); |
| 2094 | console.log(" --ou [value] Certificate organization unit name."); |
| 2095 | console.log(" --serial [value] Certificate serial number."); |
| 2096 | console.log(" timestamp: Add a signed timestamp to an already signed executable."); |
| 2097 | console.log(" --exe [file] Required executable to timestamp."); |
| 2098 | console.log(" --out [file] Resulting signed executable."); |
| 2099 | console.log(" --time [url] The time signing server URL."); |
| 2100 | console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://"); |
| 2101 | console.log(" bitmaps: Show bitmap resources in the executable."); |
| 2102 | console.log(" --exe [file] Input executable."); |
| 2103 | console.log(" savebitmap: Save a single bitmap to a .bmp file."); |
| 2104 | console.log(" --exe [file] Input executable."); |
| 2105 | console.log(" --out [file] Resulting .ico file."); |
| 2106 | console.log(" --bitmap [number] Bitmap number to save to file."); |
| 2107 | console.log(" icons: Show the icon resources in the executable."); |
| 2108 | console.log(" --exe [file] Input executable."); |
| 2109 | console.log(" saveicon: Save a single icon bitmap to a .ico file."); |
| 2110 | console.log(" --exe [file] Input executable."); |
| 2111 | console.log(" --out [file] Resulting .ico file."); |
| 2112 | console.log(" --icon [number] Icon number to save to file."); |
| 2113 | console.log(" saveicons: Save an icon group to a .ico file."); |
| 2114 | console.log(" --exe [file] Input executable."); |
| 2115 | console.log(" --out [file] Resulting .ico file."); |
| 2116 | console.log(" --icongroup [groupNumber] Icon groupnumber to save to file."); |
| 2117 | console.log(""); |
| 2118 | console.log("Note that certificate PEM files must first have the signing certificate,"); |
| 2119 | console.log("followed by all certificates that form the trust chain."); |
| 2120 | console.log(""); |
| 2121 | console.log("When doing sign/unsign, you can also change resource properties of the generated file."); |
| 2122 | console.log(""); |
| 2123 | console.log(" --fileversionnumber n.n.n.n"); |
| 2124 | console.log(" --productversionnumber n.n.n.n"); |
| 2125 | console.log(" --filedescription [value]"); |
| 2126 | console.log(" --fileversion [value]"); |
| 2127 | console.log(" --internalname [value]"); |
| 2128 | console.log(" --legalcopyright [value]"); |
| 2129 | console.log(" --originalfilename [value]"); |
| 2130 | console.log(" --productname [value]"); |
| 2131 | console.log(" --productversion [value]"); |
| 2132 | console.log(" --removeicongroup [number]"); |
| 2133 | console.log(" --removebitmap [number]"); |
| 2134 | console.log(" --icon [groupNumber],[filename.ico]"); |
| 2135 | console.log(" --bitmap [number],[filename.bmp]"); |
| 2136 | return; |
| 2137 | } |
| 2138 | |
| 2139 | // Check that a valid command is passed in |
| 2140 | if (['info', 'sign', 'unsign', 'createcert', 'icons', 'bitmaps', 'saveicon', 'saveicons', 'savebitmap', 'header', 'sections', 'timestamp', 'signblock'].indexOf(process.argv[2].toLowerCase()) == -1) { |
| 2141 | console.log("Invalid command: " + process.argv[2]); |
| 2142 | console.log("Valid commands are: info, sign, unsign, createcert, timestamp"); |
| 2143 | return; |
| 2144 | } |
| 2145 | |
| 2146 | var exe = null; |
| 2147 | if (args.exe) { |
| 2148 | // Check the file exists and open the file |
| 2149 | var stats = null; |
| 2150 | try { stats = require('fs').statSync(args.exe); } catch (ex) { } |
| 2151 | if (stats == null) { console.log("Unable to executable open file: " + args.exe); return; } |
| 2152 | exe = createAuthenticodeHandler(args.exe); |
| 2153 | if (exe == null) { console.log("Unable to parse executable file: " + args.exe); return; } |
| 2154 | } |
| 2155 | |
| 2156 | // Parse the string resources and make any required changes |
| 2157 | var resChanges = false, versionStrings = null; |
| 2158 | if (exe != null) { |
| 2159 | versionStrings = exe.getVersionInfo(); |
| 2160 | var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion']; |
| 2161 | for (var i in versionProperties) { |
| 2162 | const prop = versionProperties[i], propl = prop.toLowerCase(); |
| 2163 | if (args[propl] && (args[propl] != versionStrings[prop])) { versionStrings[prop] = args[propl]; resChanges = true; } |
| 2164 | } |
| 2165 | if (args['fileversionnumber'] != null) { |
| 2166 | const fileVerSplit = args['fileversionnumber'].split('.'); |
| 2167 | if (fileVerSplit.length != 4) { console.log("--fileversionnumber must be of format n.n.n.n, for example: 1.2.3.4"); return; } |
| 2168 | for (var i in fileVerSplit) { var n = parseInt(fileVerSplit[i]); if ((n < 0) || (n > 65535)) { console.log("--fileversionnumber numbers must be between 0 and 65535."); return; } } |
| 2169 | if (args['fileversionnumber'] != versionStrings['~FileVersion']) { versionStrings['~FileVersion'] = args['fileversionnumber']; resChanges = true; } |
| 2170 | } |
| 2171 | if (args['productversionnumber'] != null) { |
| 2172 | const productVerSplit = args['productversionnumber'].split('.'); |
| 2173 | if (productVerSplit.length != 4) { console.log("--productversionnumber must be of format n.n.n.n, for example: 1.2.3.4"); return; } |
| 2174 | for (var i in productVerSplit) { var n = parseInt(productVerSplit[i]); if ((n < 0) || (n > 65535)) { console.log("--productversionnumber numbers must be between 0 and 65535."); return; } } |
| 2175 | if (args['productversionnumber'] != versionStrings['~ProductVersion']) { versionStrings['~ProductVersion'] = args['productversionnumber']; resChanges = true; } |
| 2176 | } |
| 2177 | if (resChanges == true) { exe.setVersionInfo(versionStrings); } |
| 2178 | } |
| 2179 | |
| 2180 | // Parse the icon changes |
| 2181 | resChanges = false; |
| 2182 | var icons = null, bitmaps = null; |
| 2183 | if (exe != null) { |
| 2184 | icons = exe.getIconInfo(); |
| 2185 | bitmaps = exe.getBitmapInfo(); |
| 2186 | if (typeof args['removeicongroup'] == 'string') { // If --removeicongroup is used, it's to remove an existing icon group |
| 2187 | const groupsToRemove = args['removeicongroup'].split(','); |
| 2188 | for (var i in groupsToRemove) { if (icons[groupsToRemove[i]] != null) { delete icons[groupsToRemove[i]]; resChanges = true; } } |
| 2189 | } else if (typeof args['removeicongroup'] == 'number') { |
| 2190 | if (icons[args['removeicongroup']] != null) { delete icons[args['removeicongroup']]; resChanges = true; } |
| 2191 | } |
| 2192 | if (typeof args['removebitmap'] == 'string') { // If --removebitmap is used |
| 2193 | const bitmapsToRemove = args['removebitmap'].split(','); |
| 2194 | for (var i in bitmapsToRemove) { if (bitmaps[bitmapsToRemove[i]] != null) { delete bitmaps[bitmapsToRemove[i]]; resChanges = true; } } |
| 2195 | } else if (typeof args['removebitmap'] == 'number') { |
| 2196 | if (bitmaps[args['removebitmap']] != null) { delete bitmaps[args['removebitmap']]; resChanges = true; } |
| 2197 | } |
| 2198 | if (typeof args['icon'] == 'string') { // If --icon is used, it's to add or replace an existing icon group |
| 2199 | const iconToAddSplit = args['icon'].split(','); |
| 2200 | if (iconToAddSplit.length != 2) { console.log("The --icon format is: --icon [number],[file]."); return; } |
| 2201 | const iconName = parseInt(iconToAddSplit[0]); |
| 2202 | const iconFile = iconToAddSplit[1]; |
| 2203 | const icon = loadIcon(iconFile); |
| 2204 | if (icon == null) { console.log("Unable to load icon: " + iconFile); return; } |
| 2205 | if (icons[iconName] != null) { |
| 2206 | const iconHash = hashObject(icon); // Compute the new icon group hash |
| 2207 | const iconHash2 = hashObject(icons[iconName]); // Computer the old icon group hash |
| 2208 | if (iconHash != iconHash2) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group |
| 2209 | } else { |
| 2210 | icons[iconName] = icon; // We are adding an icon group |
| 2211 | resChanges = true; |
| 2212 | } |
| 2213 | } |
| 2214 | if (typeof args['bitmap'] == 'string') { // If --bitmap is used, it's to add or replace an existing bitmap |
| 2215 | const bitmapToAddSplit = args['bitmap'].split(','); |
| 2216 | if (bitmapToAddSplit.length != 2) { console.log("The --bitmap format is: --bitmap [number],[file]."); return; } |
| 2217 | const bitmapName = parseInt(bitmapToAddSplit[0]); |
| 2218 | const bitmapFile = bitmapToAddSplit[1]; |
| 2219 | const bitmap = loadBitmap(bitmapFile); |
| 2220 | if (bitmap == null) { console.log("Unable to load bitmap: " + bitmapFile); return; } |
| 2221 | if (bitmaps[bitmapName] != null) { |
| 2222 | const bitmapHash = hashObject(bitmap); // Compute the new bitmap hash |
| 2223 | const bitmapHash2 = hashObject(bitmaps[bitmapName]); // Computer the old bitmap hash |
| 2224 | if (bitmapHash != bitmapHash2) { bitmaps[bitmapName] = bitmap; resChanges = true; } // If different, replace the new bitmap |
| 2225 | } else { |
| 2226 | bitmaps[bitmapName] = bitmap; // We are adding an new bitmap |
| 2227 | resChanges = true; |
| 2228 | } |
| 2229 | } |
| 2230 | if (resChanges == true) { |
| 2231 | exe.setIconInfo(icons); |
| 2232 | exe.setBitmapInfo(bitmaps); |
| 2233 | } |
| 2234 | } |
| 2235 | |
| 2236 | // Execute the command |
| 2237 | var command = process.argv[2].toLowerCase(); |
| 2238 | if (command == 'info') { // Get signature information about an executable |
| 2239 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2240 | if (args.json) { |
| 2241 | var r = {}, stringInfo = exe.getVersionInfo(); |
| 2242 | if (stringInfo != null) { |
| 2243 | r.versionInfo = {}; |
| 2244 | r.stringInfo = {}; |
| 2245 | for (var i in stringInfo) { if (i.startsWith('~')) { r.versionInfo[i.substring(1)] = stringInfo[i]; } else { r.stringInfo[i] = stringInfo[i]; } } |
| 2246 | } |
| 2247 | if (exe.fileHashAlgo != null) { |
| 2248 | r.signture = {}; |
| 2249 | if (exe.fileHashAlgo != null) { r.signture.hashMethod = exe.fileHashAlgo; } |
| 2250 | if (exe.fileHashSigned != null) { r.signture.hashSigned = exe.fileHashSigned.toString('hex'); } |
| 2251 | if (exe.fileHashActual != null) { r.signture.hashActual = exe.fileHashActual.toString('hex'); } |
| 2252 | if (exe.signingAttribs && exe.signingAttribs.length > 0) { r.signture.attributes = exe.signingAttribs; } |
| 2253 | } |
| 2254 | console.log(JSON.stringify(r, null, 2)); |
| 2255 | } else { |
| 2256 | var versionInfo = exe.getVersionInfo(); |
| 2257 | if (versionInfo != null) { |
| 2258 | console.log("Version Information:"); |
| 2259 | for (var i in versionInfo) { if (i.startsWith('~') == true) { console.log(' ' + i.substring(1) + ': ' + versionInfo[i] + ''); } } |
| 2260 | console.log("String Information:"); |
| 2261 | for (var i in versionInfo) { if (i.startsWith('~') == false) { if (versionInfo[i] == null) { console.log(' ' + i + ': (Empty)'); } else { console.log(' ' + i + ': \"' + versionInfo[i] + '\"'); } } } |
| 2262 | } |
| 2263 | console.log("Checksum Information:"); |
| 2264 | console.log(" Header CheckSum: 0x" + exe.header.peWindows.checkSum.toString(16)); |
| 2265 | console.log(" Actual CheckSum: 0x" + exe.header.peWindows.checkSumActual.toString(16)); |
| 2266 | console.log("Signature Information:"); |
| 2267 | if (exe.fileHashAlgo != null) { |
| 2268 | console.log(" Hash Method:", exe.fileHashAlgo); |
| 2269 | if (exe.fileHashSigned != null) { console.log(" Signed Hash:", exe.fileHashSigned.toString('hex')); } |
| 2270 | if (exe.fileHashActual != null) { console.log(" Actual Hash:", exe.fileHashActual.toString('hex')); } |
| 2271 | } else { |
| 2272 | console.log(" This file is not signed."); |
| 2273 | } |
| 2274 | if (exe.signingAttribs && exe.signingAttribs.length > 0) { console.log("Signature Attributes:"); for (var i in exe.signingAttribs) { console.log(' ' + exe.signingAttribs[i]); } } |
| 2275 | } |
| 2276 | } |
| 2277 | if (command == 'header') { // Display the full executable header in JSON format |
| 2278 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2279 | console.log(exe.header); |
| 2280 | // Check that the header is valid |
| 2281 | var ptr = 1024, sizeOfCode = 0, sizeOfInitializedData = 0; |
| 2282 | for (var i in exe.header.sections) { |
| 2283 | if (i == '.text') { sizeOfCode += exe.header.sections[i].rawSize; } else { sizeOfInitializedData += exe.header.sections[i].rawSize; } |
| 2284 | if (exe.header.sections[i].rawAddr != ptr) { console.log('WARNING: ' + i + ' section should have a rawAddr or ' + ptr + ', but has ' + exe.header.sections[i].rawAddr + ' instead.'); } |
| 2285 | ptr += exe.header.sections[i].rawSize; |
| 2286 | } |
| 2287 | if (exe.header.peStandard.sizeOfCode != sizeOfCode) { console.log('WARNING: Size of code is ' + exe.header.peStandard.sizeOfCode + ', should be ' + sizeOfCode + '.'); } |
| 2288 | if (exe.header.peStandard.sizeOfInitializedData != sizeOfInitializedData) { console.log('WARNING: Size of initialized data is ' + exe.header.peStandard.sizeOfInitializedData + ', should be ' + sizeOfInitializedData + '.'); } |
| 2289 | } |
| 2290 | if (command == 'sections') { // Display sections in CSV format |
| 2291 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2292 | var csvHeader = 'section'; |
| 2293 | for (var i in exe.header.sections['.text']) { csvHeader += ',' + i; } |
| 2294 | console.log(csvHeader); |
| 2295 | for (var i in exe.header.sections) { |
| 2296 | var csvData = i; |
| 2297 | for (var j in exe.header.sections[i]) { csvData += ',' + exe.header.sections[i][j]; } |
| 2298 | console.log(csvData); |
| 2299 | } |
| 2300 | } |
| 2301 | if (command == 'sign') { // Sign an executable |
| 2302 | if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; } |
| 2303 | if (typeof args.hash == 'string') { args.hash = args.hash.toLowerCase(); if (['md5', 'sha224', 'sha256', 'sha384', 'sha512'].indexOf(args.hash) == -1) { console.log("Invalid hash method, must be SHA256 or SHA384"); return; } } |
| 2304 | if (args.hash == null) { args.hash = 'sha384'; } |
| 2305 | createOutFile(args, args.exe); |
| 2306 | var cert = loadCertificates(args.pem); |
| 2307 | if (cert == null) { console.log("Unable to load certificate and/or private key, generating test certificate."); cert = createSelfSignedCert({ cn: 'Test' }); } |
| 2308 | if (resChanges == false) { |
| 2309 | console.log("Signing to " + args.out); |
| 2310 | exe.sign(cert, args, function (err) { // Simple signing, copy most of the original file. |
| 2311 | if (err == null) { console.log("Done."); } else { console.log(err); } |
| 2312 | if (exe != null) { exe.close(); } |
| 2313 | }); |
| 2314 | return; |
| 2315 | } else { |
| 2316 | console.log("Changing resources and signing to " + args.out); |
| 2317 | exe.writeExecutable(args, cert, function (err) { // Signing with resources decoded and re-encoded. |
| 2318 | if (err == null) { console.log("Done."); } else { console.log(err); } |
| 2319 | if (exe != null) { exe.close(); } |
| 2320 | }); |
| 2321 | return; |
| 2322 | } |
| 2323 | } |
| 2324 | if (command == 'unsign') { // Unsign an executable |
| 2325 | if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; } |
| 2326 | createOutFile(args, args.exe); |
| 2327 | if (resChanges == false) { |
| 2328 | if (exe.header.signed) { |
| 2329 | console.log("Unsigning to " + args.out); |
| 2330 | exe.unsign(args); // Simple unsign, copy most of the original file. |
| 2331 | console.log("Done."); |
| 2332 | } else { |
| 2333 | console.log("Executable is not signed."); |
| 2334 | } |
| 2335 | } else { |
| 2336 | console.log("Changing resources and unsigning to " + args.out); |
| 2337 | exe.writeExecutable(args, null, function (err) { // Unsigning with resources decoded and re-encoded. |
| 2338 | if (err == null) { console.log("Done."); } else { console.log(err); } |
| 2339 | if (exe != null) { exe.close(); } |
| 2340 | }); |
| 2341 | } |
| 2342 | } |
| 2343 | if (command == 'createcert') { // Create a code signing certificate and private key |
| 2344 | if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; } |
| 2345 | if (typeof args.cn != 'string') { console.log("Missing --cn [name]"); return; } |
| 2346 | if (typeof args.serial == 'string') { if (args.serial != parseInt(args.serial)) { console.log("Invalid serial number."); return; } else { args.serial = parseInt(args.serial); } } |
| 2347 | if (typeof args.serial == 'number') { args.serial = '0' + args.serial; } // Serial number must be a integer string with a single leading '0' |
| 2348 | const cert = createSelfSignedCert(args); |
| 2349 | console.log("Writing to " + args.out); |
| 2350 | fs.writeFileSync(args.out, pki.certificateToPem(cert.cert) + '\r\n' + pki.privateKeyToPem(cert.key)); |
| 2351 | console.log("Done."); |
| 2352 | } |
| 2353 | if (command == 'bitmaps') { // Show bitmaps in the executable |
| 2354 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2355 | if (args.json) { |
| 2356 | var bitmapInfo = exe.getBitmapInfo(); |
| 2357 | console.log(JSON.stringify(bitmapInfo, null, 2)); |
| 2358 | } else { |
| 2359 | var bitmapInfo = exe.getBitmapInfo(); |
| 2360 | if (bitmapInfo != null) { |
| 2361 | console.log("Bitmap Information:"); |
| 2362 | for (var i in bitmapInfo) { console.log(' ' + i + ': ' + bitmapInfo[i].length + ' byte' + ((bitmapInfo[i].length > 1) ? 's' : '') + '.'); } |
| 2363 | } |
| 2364 | } |
| 2365 | } |
| 2366 | if (command == 'savebitmap') { // Save an bitmap to file |
| 2367 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2368 | if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; } |
| 2369 | if (typeof args.bitmap != 'number') { console.log("Missing or incorrect --bitmap [number]"); return; } |
| 2370 | const bitmapInfo = exe.getBitmapInfo(); |
| 2371 | if (bitmapInfo[args.bitmap] == null) { console.log("Unknown bitmap: " + args.bitmap); return; } |
| 2372 | |
| 2373 | console.log("Writing to " + args.out); |
| 2374 | var bitmapHeader = Buffer.from('424D000000000000000036000000', 'hex'); |
| 2375 | bitmapHeader.writeUInt32LE(14 + bitmapInfo[args.bitmap].length, 2); // Write the full size of the bitmap file |
| 2376 | fs.writeFileSync(args.out, Buffer.concat([bitmapHeader, bitmapInfo[args.bitmap]])); |
| 2377 | console.log("Done."); |
| 2378 | } |
| 2379 | if (command == 'icons') { // Show icons in the executable |
| 2380 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2381 | if (args.json) { |
| 2382 | var r = {}, iconInfo = exe.getIconInfo(); |
| 2383 | if (iconInfo != null) { r.iconInfo = iconInfo; } |
| 2384 | console.log(JSON.stringify(r, null, 2)); |
| 2385 | } else { |
| 2386 | var iconInfo = exe.getIconInfo(); |
| 2387 | if (iconInfo != null) { |
| 2388 | console.log("Icon Information:"); |
| 2389 | for (var i in iconInfo) { console.log(' Group ' + i + ':'); for (var j in iconInfo[i].icons) { console.log(' Icon ' + j + ': ' + ((iconInfo[i].icons[j].width == 0) ? 256 : iconInfo[i].icons[j].width) + 'x' + ((iconInfo[i].icons[j].height == 0) ? 256 : iconInfo[i].icons[j].height) + ', size: ' + iconInfo[i].icons[j].icon.length); } } |
| 2390 | } |
| 2391 | } |
| 2392 | } |
| 2393 | if (command == 'saveicon') { // Save an icon to file |
| 2394 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2395 | if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; } |
| 2396 | if (typeof args.icon != 'number') { console.log("Missing or incorrect --icon [number]"); return; } |
| 2397 | const iconInfo = exe.getIconInfo(); |
| 2398 | var icon = null; |
| 2399 | for (var i in iconInfo) { if (iconInfo[i].icons[args.icon]) { icon = iconInfo[i].icons[args.icon]; } } |
| 2400 | if (icon == null) { console.log("Unknown icon: " + args.icon); return; } |
| 2401 | |
| 2402 | // .ico header: https://en.wikipedia.org/wiki/ICO_(file_format) |
| 2403 | var buf = Buffer.alloc(22); |
| 2404 | buf.writeUInt16LE(1, 2); // 1 = Icon, 2 = Cursor |
| 2405 | buf.writeUInt16LE(1, 4); // Icon Count, always 1 in our case |
| 2406 | buf[6] = icon.width; // Width (0 = 256) |
| 2407 | buf[7] = icon.height; // Height (0 = 256) |
| 2408 | buf[8] = icon.colorCount; // Colors |
| 2409 | buf.writeUInt16LE(icon.planes, 10); // Color planes |
| 2410 | buf.writeUInt16LE(icon.bitCount, 12); // Bits per pixel |
| 2411 | buf.writeUInt32LE(icon.icon.length, 14); // Size |
| 2412 | buf.writeUInt32LE(22, 18); // Offset, always 22 in our case |
| 2413 | |
| 2414 | console.log("Writing to " + args.out); |
| 2415 | fs.writeFileSync(args.out, Buffer.concat([buf, icon.icon])); |
| 2416 | console.log("Done."); |
| 2417 | } |
| 2418 | if (command == 'saveicons') { // Save an icon group to file |
| 2419 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2420 | if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; } |
| 2421 | if (typeof args.icongroup != 'number') { console.log("Missing or incorrect --icongroup [number]"); return; } |
| 2422 | const iconInfo = exe.getIconInfo(); |
| 2423 | const iconGroup = iconInfo[args.icongroup]; |
| 2424 | if (iconGroup == null) { console.log("Invalid or incorrect --icongroup [number]"); return; } |
| 2425 | |
| 2426 | // Count the number of icons in the group |
| 2427 | var iconCount = 0; |
| 2428 | for (var i in iconGroup.icons) { iconCount++; } |
| 2429 | |
| 2430 | // .ico header: https://en.wikipedia.org/wiki/ICO_(file_format) |
| 2431 | const iconFileData = []; |
| 2432 | const header = Buffer.alloc(6); |
| 2433 | header.writeUInt16LE(1, 2); // 1 = Icon, 2 = Cursor |
| 2434 | header.writeUInt16LE(iconCount, 4); // Icon Count, always 1 in our case |
| 2435 | iconFileData.push(header); |
| 2436 | |
| 2437 | // Store each icon header |
| 2438 | var offsetPtr = 6 + (16 * iconCount); |
| 2439 | for (var i in iconGroup.icons) { |
| 2440 | const buf = Buffer.alloc(16); |
| 2441 | buf[0] = iconGroup.icons[i].width; // Width (0 = 256) |
| 2442 | buf[1] = iconGroup.icons[i].height; // Height (0 = 256) |
| 2443 | buf[2] = iconGroup.icons[i].colorCount; // Colors |
| 2444 | buf.writeUInt16LE(iconGroup.icons[i].planes, 4); // Color planes |
| 2445 | buf.writeUInt16LE(iconGroup.icons[i].bitCount, 6); // Bits per pixel |
| 2446 | buf.writeUInt32LE(iconGroup.icons[i].icon.length, 8); // Size |
| 2447 | buf.writeUInt32LE(offsetPtr, 12); // Offset |
| 2448 | offsetPtr += iconGroup.icons[i].icon.length; |
| 2449 | iconFileData.push(buf); |
| 2450 | } |
| 2451 | |
| 2452 | // Store each icon |
| 2453 | for (var i in iconGroup.icons) { iconFileData.push(iconGroup.icons[i].icon); } |
| 2454 | |
| 2455 | // Write the .ico file |
| 2456 | console.log("Writing to " + args.out); |
| 2457 | fs.writeFileSync(args.out, Buffer.concat(iconFileData)); |
| 2458 | console.log("Done."); |
| 2459 | } |
| 2460 | if (command == 'signblock') { // Display the raw signature block of the executable in hex |
| 2461 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2462 | var buf = exe.getRawSignatureBlock(); |
| 2463 | if (buf == null) { console.log("Executable is not signed."); return } else { console.log(buf.toString('hex')); return } |
| 2464 | } |
| 2465 | if (command == 'timestamp') { |
| 2466 | if (exe == null) { console.log("Missing --exe [filename]"); return; } |
| 2467 | if (exe.signature == null) { console.log("Executable is not signed."); return; } |
| 2468 | if (typeof args.time != 'string') { console.log("Missing --time [url]"); return; } |
| 2469 | createOutFile(args, args.exe); |
| 2470 | console.log("Requesting time signature..."); |
| 2471 | exe.timeStampRequest(args, function (err) { |
| 2472 | if (err == null) { console.log("Done."); } else { console.log(err); } |
| 2473 | if (exe != null) { exe.close(); } |
| 2474 | }) |
| 2475 | return; |
| 2476 | } |
| 2477 | |
| 2478 | // Close the file |
| 2479 | if (exe != null) { exe.close(); } |
| 2480 | } |
| 2481 | |
| 2482 | // If this is the main module, run the command line version |
| 2483 | if (require.main === module) { start(); } |
| 2484 | |
| 2485 | // Exports |
| 2486 | module.exports.createAuthenticodeHandler = createAuthenticodeHandler; |
| 2487 | module.exports.loadCertificates = loadCertificates; |
| 2488 | module.exports.loadIcon = loadIcon; |
| 2489 | module.exports.loadBitmap = loadBitmap; |
| 2490 | module.exports.hashObject = hashObject; |