Started work on integrating Windows agent icon customization prior to agent signing.
Ylian Saint-Hilaire committed
Aug 10, 2022 at 13:12 UTC
48c6b42a0b9f5c1bc78fa2dd5e8e389e8a8499bb
5 files changed
+109
-49
authenticode.js
+43
-38
@@ -50,6 +50,38 @@ function createOutFile(args, filename) {
50
args.out = outputFileName.join('.');
51
}
52
53
+// Hash an object
54
+function hashObject(obj) {
55
+ const hash = crypto.createHash('sha384');
56
+ hash.update(JSON.stringify(obj));
57
+ return hash.digest().toString('hex');
58
+}
59
+
60
+// Load a .ico file. This will load all icons in the file into a icon group object
61
+function loadIcon(iconFile) {
62
+ var iconData = null;
63
+ try { iconData = fs.readFileSync(iconFile); } catch (ex) { }
64
+ if ((iconData == null) || (iconData.length < 6) || (iconData[0] != 0) || (iconData[1] != 0)) return null;
65
+ const r = { resType: iconData.readUInt16LE(2), resCount: iconData.readUInt16LE(4), icons: {} };
66
+ if (r.resType != 1) return null;
67
+ var ptr = 6;
68
+ for (var i = 1; i <= r.resCount; i++) {
69
+ var icon = {};
70
+ icon.width = iconData[ptr + 0];
71
+ icon.height = iconData[ptr + 1];
72
+ icon.colorCount = iconData[ptr + 2];
73
+ icon.planes = iconData.readUInt16LE(ptr + 4);
74
+ icon.bitCount = iconData.readUInt16LE(ptr + 6);
75
+ icon.bytesInRes = iconData.readUInt32LE(ptr + 8);
76
+ icon.iconCursorId = i;
77
+ const offset = iconData.readUInt32LE(ptr + 12);
78
+ icon.icon = iconData.slice(offset, offset + icon.bytesInRes);
79
+ r.icons[i] = icon;
80
+ ptr += 16;
81
+ }
82
+ return r;
83
+}
84
+
85
// Load certificates and private key from PEM files
86
function loadCertificates(pemFileNames) {
87
var certs = [], keys = [];
@@ -720,38 +752,6 @@ function createAuthenticodeHandler(path) {
752
return pkcs7raw;
753
}
754
723
- // Hash an object
724
- obj.hashObject = function (obj) {
725
- const hash = crypto.createHash('sha384');
726
- hash.update(JSON.stringify(obj));
727
- return hash.digest();
728
- }
729
-
730
- // Load a .ico file. This will load all icons in the file into a icon group object
731
- obj.loadIcon = function (iconFile) {
732
- var iconData = null;
733
- try { iconData = fs.readFileSync(iconFile); } catch (ex) {}
734
- if ((iconData == null) || (iconData.length < 6) || (iconData[0] != 0) || (iconData[1] != 0)) return null;
735
- const r = { resType: iconData.readUInt16LE(2), resCount: iconData.readUInt16LE(4), icons: {} };
736
- if (r.resType != 1) return null;
737
- var ptr = 6;
738
- for (var i = 1; i <= r.resCount; i++) {
739
- var icon = {};
740
- icon.width = iconData[ptr + 0];
741
- icon.height = iconData[ptr + 1];
742
- icon.colorCount = iconData[ptr + 2];
743
- icon.planes = iconData.readUInt16LE(ptr + 4);
744
- icon.bitCount = iconData.readUInt16LE(ptr + 6);
745
- icon.bytesInRes = iconData.readUInt32LE(ptr + 8);
746
- icon.iconCursorId = i;
747
- const offset = iconData.readUInt32LE(ptr + 12);
748
- icon.icon = iconData.slice(offset, offset + icon.bytesInRes);
749
- r.icons[i] = icon;
750
- ptr += 16;
751
- }
752
- return r;
753
- }
754
-
755
// Get icon information from resource
756
obj.getIconInfo = function () {
757
const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
@@ -1661,11 +1661,15 @@ function createAuthenticodeHandler(path) {
1661
var fullHeaderLen = obj.header.SectionHeadersPtr + (obj.header.coff.numberOfSections * 40);
1662
var fullHeader = readFileSlice(written, fullHeaderLen);
1663
1664
+ // Compute the size of the resource segment
1665
+ //const resSizes = { tables: 0, items: 0, names: 0, data: 0 };
1666
+ //getResourceSectionSize(obj.resources, resSizes);
1667
+
1668
// Calculate the location and original and new size of the resource segment
1669
var fileAlign = obj.header.peWindows.fileAlignment
1670
var resPtr = obj.header.sections['.rsrc'].rawAddr;
1671
var oldResSize = obj.header.sections['.rsrc'].rawSize;
1668
- var newResSize = obj.header.sections['.rsrc'].rawSize; // Testing 102400
1672
+ var newResSize = obj.header.sections['.rsrc'].rawSize; // TODO: resSizes.data;
1673
var resDeltaSize = newResSize - oldResSize;
1674
1675
// Change PE optional header sizeOfInitializedData standard field
@@ -2041,12 +2045,12 @@ function start() {
2045
if (iconToAddSplit.length != 2) { console.log("The --icon format is: --icon [number],[file]."); return; }
2046
const iconName = parseInt(iconToAddSplit[0]);
2047
const iconFile = iconToAddSplit[1];
2044
- const icon = exe.loadIcon(iconFile);
2048
+ const icon = loadIcon(iconFile);
2049
if (icon == null) { console.log("Unable to load icon: " + iconFile); return; }
2050
if (icons[iconName] != null) {
2047
- const iconHash = exe.hashObject(icon); // Compute the new icon group hash
2048
- const iconHash2 = exe.hashObject(icons[iconName]); // Computer the old icon group hash
2049
- if (iconHash.toString('hex') != iconHash2.toString('hex')) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group
2051
+ const iconHash = hashObject(icon); // Compute the new icon group hash
2052
+ const iconHash2 = hashObject(icons[iconName]); // Computer the old icon group hash
2053
+ if (iconHash != iconHash2) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group
2054
} else {
2055
icons[iconName] = icon; // We are adding an icon group
2056
resChanges = true;
@@ -2261,4 +2265,5 @@ if (require.main === module) { start(); }
2265
// Exports
2266
module.exports.createAuthenticodeHandler = createAuthenticodeHandler;
2267
module.exports.loadCertificates = loadCertificates;
2264
-
2268
+module.exports.loadIcon = loadIcon;
2269
+module.exports.hashObject = hashObject;
\ No newline at end of file
meshcentral-config-schema.json
+1
@@ -574,6 +574,7 @@
574
"additionalProperties": false,
575
"description": "Use this section to set resource metadata of the Windows agents prior to signing. In Windows, you can right-click and select properties to view these values.",
576
"properties": {
577
+ "icon": { "type": "string", "default": null, "description": "DO NOT USE. THIS FEATURE DOES NOT WORK YET. Sets the agent icon, this is the name of a .ico file with the file placed in the meshcentral-data folder." },
578
"fileDescription": { "type": "string", "description": "Executable file description." },
579
"fileVersion": { "type": "string", "description": "Executable file version, in the form of 'n.n.n.n', for example: '1.2.3.4'." },
580
"internalName": { "type": "string", "description": "Executable internal name." },
meshcentral.js
+62
-10
@@ -1368,6 +1368,25 @@ function CreateMeshCentralServer(config, args) {
1368
if ((obj.config.domains[i].agentfileinfo.fileversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.fileversionnumber; }
1369
if ((obj.config.domains[i].agentfileinfo.productversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.productversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.productversionnumber; }
1370
if ((obj.config.domains[i].agentfileinfo.fileversionnumber == null) && (typeof obj.config.domains[i].agentfileinfo.fileversion == 'string') && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversion) != null)) { obj.config.domains[i].agentfileinfo.fileversionnumber = obj.config.domains[i].agentfileinfo.fileversion; }
1371
+ if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
1372
+ // Load the agent .ico file
1373
+ var icon = null;
1374
+ try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
1375
+ if (icon != null) {
1376
+ // The icon file was correctly loaded
1377
+ obj.config.domains[i].agentfileinfo.icon = icon;
1378
+ obj.config.domains[i].agentfileinfo.iconhash = require('./authenticode.js').hashObject(icon);
1379
+ } else {
1380
+ // Failed to load the icon file, display a server warning
1381
+ addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
1382
+ delete obj.config.domains[i].agentfileinfo.icon;
1383
+ delete obj.config.domains[i].agentfileinfo.iconhash;
1384
+ }
1385
+ } else {
1386
+ // Invalid icon file path
1387
+ delete obj.config.domains[i].agentfileinfo.icon;
1388
+ delete obj.config.domains[i].agentfileinfo.iconhash;
1389
+ }
1390
}
1391
}
1392
@@ -2951,24 +2970,41 @@ function CreateMeshCentralServer(config, args) {
2970
for (var i in versionProperties) {
2971
const prop = versionProperties[i], propl = prop.toLowerCase();
2972
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
2954
- if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
2973
+ if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; break; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
2974
} else {
2956
- if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
2975
+ if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; break; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
2976
}
2977
}
2978
2979
// Check file version number
2961
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
2962
- if (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
2963
- } else {
2964
- if (orgVersionStrings['~FileVersion'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
2980
+ if (destinationAgentOk == true) {
2981
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
2982
+ if (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
2983
+ } else {
2984
+ if (orgVersionStrings['~FileVersion'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
2985
+ }
2986
}
2987
2988
// Check product version number
2968
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
2969
- if (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
2970
- } else {
2971
- if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
2989
+ if (destinationAgentOk == true) {
2990
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
2991
+ if (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
2992
+ } else {
2993
+ if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
2994
+ }
2995
+ }
2996
+
2997
+ // Check the agent icon
2998
+ if ((destinationAgentOk == true) && (domain.agentfileinfo != null) && (domain.agentfileinfo.iconhash != null)) {
2999
+ const agentIconGroups = destinationAgent.getIconInfo();
3000
+ if (agentIconGroups != null) {
3001
+ const agentIconGroupNames = Object.keys(agentIconGroups);
3002
+ if (agentIconGroupNames.length > 0) {
3003
+ const agentMainIconGroupName = agentIconGroupNames[0];
3004
+ const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentIconGroups[agentMainIconGroupName]);
3005
+ if (agentMainIconGroupHash != domain.agentfileinfo.iconhash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
3006
+ }
3007
+ }
3008
}
3009
}
3010
@@ -3003,17 +3039,33 @@ function CreateMeshCentralServer(config, args) {
3039
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
3040
versionStrings = originalAgent.getVersionInfo();
3041
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
3042
+ // Change the agent string properties
3043
for (var i in versionProperties) {
3044
const prop = versionProperties[i], propl = prop.toLowerCase();
3045
if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
3046
}
3047
+ // Change the agent file version
3048
if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
3049
versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
3050
}
3051
+ // Change the agent product version
3052
if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
3053
versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
3054
}
3055
if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
3056
+
3057
+ // Change the agent icon
3058
+ if (domain.agentfileinfo.icon != null) {
3059
+ const agentIconGroups = originalAgent.getIconInfo();
3060
+ if (agentIconGroups != null) {
3061
+ const agentIconGroupNames = Object.keys(agentIconGroups);
3062
+ if (agentIconGroupNames.length > 0) {
3063
+ const agentMainIconGroupName = agentIconGroupNames[0];
3064
+ agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
3065
+ originalAgent.setIconInfo(agentIconGroups);
3066
+ }
3067
+ }
3068
+ }
3069
}
3070
3071
const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
sample-config-advanced.json
+1
@@ -308,6 +308,7 @@
308
"fileName": "compagnyagent"
309
},
310
"_agentFileInfo": {
311
+ "icon": "agent.ico",
312
"filedescription": "sample_filedescription",
313
"fileversion": "0.1.2.3",
314
"internalname": "sample_internalname",
views/default.handlebars
+2
-1
@@ -2328,7 +2328,8 @@
2328
19: "SMS gateway has limited use in LAN mode.",
2329
20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
2330
21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
2331
- 22: "Failed to sign agent {0}: {1}"
2331
+ 22: "Failed to sign agent {0}: {1}",
2332
+ 23: "Unable to load agent icon file: {0}."
2333
};
2334
var x = '';
2335
for (var i in message.warnings) {