authenticode.js improvements: Icon handling, support for diffrent hashing methods.
Ylian Saint-Hilaire committed
Jun 2, 2022 at 02:58 UTC
d88f35c96a99d574b457f81a9e0d6a1a7b52f8f4
1 file changed
+124
-10
authenticode.js
+124
-10
@@ -329,6 +329,72 @@ function createAuthenticodeHandler(path) {
329
return str;
330
}
331
332
+ var resourceDefaultNames = {
333
+ 'bitmaps': 2,
334
+ 'icon': 3,
335
+ 'dialogs': 5,
336
+ 'iconGroups': 14,
337
+ 'versionInfo': 16,
338
+ 'configurationFiles': 24
339
+ }
340
+
341
+ // Get icon information from resource
342
+ obj.getIconInfo = function () {
343
+ const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
344
+
345
+ // Find and parse each icon
346
+ const icons = {}
347
+ for (var i = 0; i < obj.resources.entries.length; i++) {
348
+ if (obj.resources.entries[i].name == resourceDefaultNames.icon) {
349
+ for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) {
350
+ const iconName = obj.resources.entries[i].table.entries[j].name;
351
+ const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData;
352
+ const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size;
353
+ const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
354
+ icons[iconName] = readFileSlice(actualPtr, size);
355
+ }
356
+ }
357
+ }
358
+
359
+ // Find and parse each icon group
360
+ for (var i = 0; i < obj.resources.entries.length; i++) {
361
+ if (obj.resources.entries[i].name == resourceDefaultNames.iconGroups) {
362
+ for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) {
363
+ const groupName = obj.resources.entries[i].table.entries[j].name;
364
+ const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData;
365
+ const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size;
366
+ const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
367
+ const group = {};
368
+ const groupData = readFileSlice(actualPtr, size);
369
+
370
+ // Parse NEWHEADER structure: https://docs.microsoft.com/en-us/windows/win32/menurc/newheader
371
+ group.resType = groupData.readUInt16LE(2);
372
+ group.resCount = groupData.readUInt16LE(4);
373
+
374
+ // Parse many RESDIR structure: https://docs.microsoft.com/en-us/windows/win32/menurc/resdir
375
+ group.icons = {};
376
+ for (var p = 6; p < size; p += 14) {
377
+ var icon = {}
378
+ icon.width = groupData[p];
379
+ icon.height = groupData[p + 1];
380
+ icon.colorCount = groupData[p + 2];
381
+ icon.planes = groupData.readUInt16LE(p + 4);
382
+ icon.bitCount = groupData.readUInt16LE(p + 6);
383
+ icon.bytesInRes = groupData.readUInt32LE(p + 8);
384
+ icon.iconCursorId = groupData.readUInt16LE(p + 12);
385
+ icon.icon = icons[icon.iconCursorId];
386
+ group.icons[icon.iconCursorId] = icon;
387
+ }
388
+
389
+ // Add an icon group
390
+ r[groupName] = group;
391
+ }
392
+ }
393
+ }
394
+
395
+ return r;
396
+ }
397
+
398
// Decode the version information from the resource
399
obj.getVersionInfo = function () {
400
var r = {}, info = readVersionInfo(getVersionInfoData(), 0);
@@ -344,9 +410,9 @@ function createAuthenticodeHandler(path) {
410
// Return the version info data block
411
function getVersionInfoData() {
412
if (obj.resources == null) return null;
347
- var ptr = obj.header.sections['.rsrc'].rawAddr;
413
+ const ptr = obj.header.sections['.rsrc'].rawAddr;
414
for (var i = 0; i < obj.resources.entries.length; i++) {
349
- if (obj.resources.entries[i].name == 16) {
415
+ if (obj.resources.entries[i].name == resourceDefaultNames.versionInfo) {
416
const verInfo = obj.resources.entries[i].table.entries[0].table.entries[0].item;
417
const actualPtr = (verInfo.offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
418
return readFileSlice(actualPtr, verInfo.size);
@@ -433,10 +499,8 @@ function createAuthenticodeHandler(path) {
499
if (r.wLength == 0) return t;
500
r.wValueLength = buf.readUInt16LE(ptr + 2);
501
r.wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary
436
- var szKey = unicodeToString(buf.slice(ptr + 6, ptr + 6 + (r.wLength - 6))); // String value
437
- var splitStr = szKey.split('\0');
438
- r.key = splitStr[0];
439
- for (var i = 1; i < splitStr.length; i++) { if (splitStr[i] != '') { r.value = splitStr[i]; } }
502
+ r.key = unicodeToString(buf.slice(ptr + 6, ptr + (r.wLength - (r.wValueLength * 2)))); // Key
503
+ r.value = unicodeToString(buf.slice(ptr + r.wLength - (r.wValueLength * 2), ptr + r.wLength)); // Value
504
//console.log('readStringStruct', r.wLength, r.wValueLength, r.wType, r.key, r.value);
505
t.push(r);
506
ptr += r.wLength;
@@ -466,11 +530,20 @@ function createAuthenticodeHandler(path) {
530
// Sign the file using the certificate and key. If none is specified, generate a dummy one
531
obj.sign = function (cert, args) {
532
if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); }
469
- var fileHash = obj.getHash('sha384');
533
+
534
+ // Set the hash algorithm hash OID
535
+ var hashOid = null, fileHash = null;
536
+ if (args.hash == null) { args.hash = 'sha384'; }
537
+ if (args.hash == 'sha256') { hashOid = forge.pki.oids.sha256; fileHash = obj.getHash('sha256'); }
538
+ if (args.hash == 'sha384') { hashOid = forge.pki.oids.sha384; fileHash = obj.getHash('sha384'); }
539
+ if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); }
540
+ if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); }
541
+ if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); }
542
+ if (hashOid == null) return false;
543
544
// Create the signature block
545
var p7 = forge.pkcs7.createSignedData();
473
- 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(forge.pki.oids.sha384).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] };
546
+ 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') }] }] };
547
p7.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())]);
548
p7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
549
p7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
@@ -597,6 +670,7 @@ function start() {
670
console.log(" --pem [pemfile] Certificate & private key to sign the executable with.");
671
console.log(" --desc [description] Description string to embbed into signature.");
672
console.log(" --url [url] URL to embbed into signature.");
673
+ console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.");
674
console.log(" unsign: Remove the signature from the executable.");
675
console.log(" --exe [file] Required executable to un-sign.");
676
console.log(" --out [file] Resulting executable with signature removed.");
@@ -616,7 +690,7 @@ function start() {
690
}
691
692
// Check that a valid command is passed in
619
- if (['info', 'sign', 'unsign', 'createcert'].indexOf(process.argv[2].toLowerCase()) == -1) {
693
+ if (['info', 'sign', 'unsign', 'createcert', 'icons', 'saveicon'].indexOf(process.argv[2].toLowerCase()) == -1) {
694
console.log("Invalid command: " + process.argv[2]);
695
console.log("Valid commands are: info, sign, unsign, createcert");
696
return;
@@ -636,7 +710,7 @@ function start() {
710
if (command == 'info') { // Get signature information about an executable
711
if (exe == null) { console.log("Missing --exe [filename]"); return; }
712
if (args.json) {
639
- var r = { }, versionInfo = exe.getVersionInfo();
713
+ var r = {}, versionInfo = exe.getVersionInfo();
714
if (versionInfo != null) { r.versionInfo = versionInfo; }
715
if (exe.fileHashAlgo != null) {
716
r.signture = {};
@@ -662,6 +736,8 @@ function start() {
736
}
737
if (command == 'sign') { // Sign an executable
738
if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; }
739
+ 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; } }
740
+ if (args.hash == null) { args.hash = 'sha384'; }
741
createOutFile(args, args.exe);
742
const cert = loadCertificates(args.pem);
743
if (cert == null) { console.log("Unable to load certificate and/or private key, generating test certificate."); }
@@ -682,6 +758,44 @@ function start() {
758
fs.writeFileSync(args.out, pki.certificateToPem(cert.cert) + '\r\n' + pki.privateKeyToPem(cert.key));
759
console.log("Done.");
760
}
761
+ if (command == 'icons') { // Show icons in the executable
762
+ if (exe == null) { console.log("Missing --exe [filename]"); return; }
763
+ if (args.json) {
764
+ var r = {}, iconInfo = exe.getIconInfo();
765
+ if (iconInfo != null) { r.iconInfo = iconInfo; }
766
+ console.log(JSON.stringify(r, null, 2));
767
+ } else {
768
+ var iconInfo = exe.getIconInfo();
769
+ if (iconInfo != null) {
770
+ console.log("Icon Information:");
771
+ 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); } }
772
+ }
773
+ }
774
+ }
775
+ if (command == 'saveicon') { // Save an icon to file
776
+ if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; }
777
+ if (typeof args.icon != 'number') { console.log("Missing or incorrect --icon [number]"); return; }
778
+ const iconInfo = exe.getIconInfo();
779
+ var icon = null;
780
+ for (var i in iconInfo) { if (iconInfo[i].icons[args.icon]) { icon = iconInfo[i].icons[args.icon]; } }
781
+ if (icon == null) { console.log("Unknown icon: " + args.icon); return; }
782
+
783
+ // .ico header: https://en.wikipedia.org/wiki/ICO_(file_format)
784
+ var buf = Buffer.alloc(22);
785
+ buf.writeUInt16LE(1, 2); // 1 = Icon, 2 = Cursor
786
+ buf.writeUInt16LE(1, 4); // Icon Count, always 1 in our case
787
+ buf[6] = icon.width; // Width (0 = 256)
788
+ buf[7] = icon.height; // Height (0 = 256)
789
+ buf[8] = icon.colorCount; // Colors
790
+ buf.writeUInt16LE(icon.planes, 10); // Color planes
791
+ buf.writeUInt16LE(icon.bitCount, 12); // Bits per pixel
792
+ buf.writeUInt32LE(icon.icon.length, 14); // Size
793
+ buf.writeUInt32LE(22, 18); // Offset, always 22 in our case
794
+
795
+ console.log("Writing to " + args.out);
796
+ fs.writeFileSync(args.out, Buffer.concat([buf, icon.icon]));
797
+ console.log("Done.");
798
+ }
799
800
// Close the file
801
if (exe != null) { exe.close(); }