Updated authenticode.js in order to support changing icons in Windows executables.

Ylian Saint-Hilaire committed Aug 9, 2022 at 23:35 UTC c5315ba0fcdb7bdf19484331aec1db46a02edadc
1 file changed +216 -28
authenticode.js
+216 -28
@@ -571,7 +571,7 @@ function createAuthenticodeHandler(path) {
571 if ((resSizeTotal % fileAlign) != 0) { resSizeTotal += (fileAlign - (resSizeTotal % fileAlign)); }
572 const resSectionBuffer = Buffer.alloc(resSizeTotal);
573
574 - // Write the resource section, calling a recusrize method
574 + // Write the resource section, calling a recursive method
575 const resPointers = { tables: 0, items: resSizes.tables, names: resSizes.tables + resSizes.items, data: resSizes.tables + resSizes.items + resSizes.names };
576 createResourceSection(resources, resSectionBuffer, resPointers);
577 //console.log('generateResourceSection', resPointers);
@@ -720,6 +720,38 @@ function createAuthenticodeHandler(path) {
720 return pkcs7raw;
721 }
722
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;
@@ -777,9 +809,81 @@ function createAuthenticodeHandler(path) {
809 return r;
810 }
811
812 + // Set icon information
813 + obj.setIconInfo = function (iconInfo) {
814 + // Delete all icon and icon groups the the ressources
815 + var resourcesEntries = [];
816 + for (var i = 0; i < obj.resources.entries.length; i++) {
817 + if ((obj.resources.entries[i].name != resourceDefaultNames.icon) && (obj.resources.entries[i].name != resourceDefaultNames.iconGroups)) {
818 + resourcesEntries.push(obj.resources.entries[i]);
819 + }
820 + }
821 + obj.resources.entries = resourcesEntries;
822 +
823 + // count the icon groups
824 + var iconGroupCount = 0;
825 + for (var i in iconInfo) { iconGroupCount++; }
826 + if (iconGroupCount == 0) return; // If there are no icon groups, we are done
827 +
828 + // Add the new icons entry
829 + const iconsEntry = { name: resourceDefaultNames.icon, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } };
830 + for (var i in iconInfo) {
831 + for (var j in iconInfo[i].icons) {
832 + var name = j;
833 + if (parseInt(j) == name) { name = parseInt(j); }
834 + 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 } }] } }
835 + iconsEntry.table.entries.push(iconItemEntry);
836 + }
837 + }
838 + obj.resources.entries.push(iconsEntry);
839 +
840 + // Add the new icon group entry
841 + const groupEntry = { name: resourceDefaultNames.iconGroups, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } };
842 + for (var i in iconInfo) {
843 + // Build icon group struct
844 + var iconCount = 0, p = 6;
845 + for (var j in iconInfo[i].icons) { iconCount++; }
846 + const buf = Buffer.alloc(6 + (iconCount * 14));
847 + buf.writeUInt16LE(iconInfo[i].resType, 2);
848 + buf.writeUInt16LE(iconCount, 4);
849 + for (var j in iconInfo[i].icons) {
850 + buf[p] = iconInfo[i].icons[j].width;
851 + buf[p + 1] = iconInfo[i].icons[j].height;
852 + buf[p + 2] = iconInfo[i].icons[j].colorCount;
853 + buf.writeUInt16LE(iconInfo[i].icons[j].planes, p + 4);
854 + buf.writeUInt16LE(iconInfo[i].icons[j].bitCount, p + 6);
855 + buf.writeUInt32LE(iconInfo[i].icons[j].bytesInRes, p + 8);
856 + buf.writeUInt16LE(j, p + 12);
857 + p += 14;
858 + }
859 + var name = i;
860 + if (parseInt(i) == name) { name = parseInt(i); }
861 + const groupItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: buf, codePage: 0 } }] } }
862 + groupEntry.table.entries.push(groupItemEntry);
863 + }
864 + obj.resources.entries.push(groupEntry);
865 +
866 + // Sort the resources by name. This is required.
867 + function resSort(a, b) {
868 + if ((typeof a == 'string') && (typeof b == 'string')) { if (a < b) return -1; if (a > b) return 1; return 0; }
869 + if ((typeof a == 'number') && (typeof b == 'number')) { return a - b; }
870 + if ((typeof a == 'string') && (typeof b == 'number')) { return -1; }
871 + return 1;
872 + }
873 + const names = [];
874 + for (var i = 0; i < obj.resources.entries.length; i++) { names.push(obj.resources.entries[i].name); }
875 + names.sort(resSort);
876 + var newEntryOrder = [];
877 + for (var i in names) {
878 + for (var j = 0; j < obj.resources.entries.length; j++) {
879 + if (obj.resources.entries[j].name == names[i]) { newEntryOrder.push(obj.resources.entries[j]); }
880 + }
881 + }
882 + obj.resources.entries = newEntryOrder;
883 + }
884 +
885 // Decode the version information from the resource
886 obj.getVersionInfo = function () {
782 - //console.log('READ', getVersionInfoData().toString('hex'));
887 var r = {}, info = readVersionInfo(getVersionInfoData(), 0);
888 if ((info == null) || (info.stringFiles == null)) return null;
889 var StringFileInfo = null;
@@ -1822,34 +1926,44 @@ function start() {
1926 console.log(" node authenticode.js [command] [options]");
1927 console.log("Commands:");
1928 console.log(" info: Show information about an executable.");
1825 - console.log(" --exe [file] Required executable to view information.");
1826 - console.log(" --json Show information in JSON format.");
1929 + console.log(" --exe [file] Required executable to view information.");
1930 + console.log(" --json Show information in JSON format.");
1931 console.log(" sign: Sign an executable.");
1828 - console.log(" --exe [file] Required executable to sign.");
1829 - console.log(" --out [file] Resulting signed executable.");
1830 - console.log(" --pem [pemfile] Certificate & private key to sign the executable with.");
1831 - console.log(" --desc [description] Description string to embbed into signature.");
1832 - console.log(" --url [url] URL to embbed into signature.");
1833 - console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.");
1834 - console.log(" --time [url] The time signing server URL.");
1835 - console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
1932 + console.log(" --exe [file] Required executable to sign.");
1933 + console.log(" --out [file] Resulting signed executable.");
1934 + console.log(" --pem [pemfile] Certificate & private key to sign the executable with.");
1935 + console.log(" --desc [description] Description string to embbed into signature.");
1936 + console.log(" --url [url] URL to embbed into signature.");
1937 + console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.");
1938 + console.log(" --time [url] The time signing server URL.");
1939 + console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
1940 console.log(" unsign: Remove the signature from the executable.");
1837 - console.log(" --exe [file] Required executable to un-sign.");
1838 - console.log(" --out [file] Resulting executable with signature removed.");
1941 + console.log(" --exe [file] Required executable to un-sign.");
1942 + console.log(" --out [file] Resulting executable with signature removed.");
1943 console.log(" createcert: Create a code signging self-signed certificate and key.");
1840 - console.log(" --out [pemfile] Required certificate file to create.");
1841 - console.log(" --cn [value] Required certificate common name.");
1842 - console.log(" --country [value] Certificate country name.");
1843 - console.log(" --state [value] Certificate state name.");
1844 - console.log(" --locality [value] Certificate locality name.");
1845 - console.log(" --org [value] Certificate organization name.");
1846 - console.log(" --ou [value] Certificate organization unit name.");
1847 - console.log(" --serial [value] Certificate serial number.");
1944 + console.log(" --out [pemfile] Required certificate file to create.");
1945 + console.log(" --cn [value] Required certificate common name.");
1946 + console.log(" --country [value] Certificate country name.");
1947 + console.log(" --state [value] Certificate state name.");
1948 + console.log(" --locality [value] Certificate locality name.");
1949 + console.log(" --org [value] Certificate organization name.");
1950 + console.log(" --ou [value] Certificate organization unit name.");
1951 + console.log(" --serial [value] Certificate serial number.");
1952 console.log(" timestamp: Add a signed timestamp to an already signed executable.");
1849 - console.log(" --exe [file] Required executable to sign.");
1850 - console.log(" --out [file] Resulting signed executable.");
1851 - console.log(" --time [url] The time signing server URL.");
1852 - console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
1953 + console.log(" --exe [file] Required executable to timestamp.");
1954 + console.log(" --out [file] Resulting signed executable.");
1955 + console.log(" --time [url] The time signing server URL.");
1956 + console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
1957 + console.log(" icons: Show the icon resources in the executable.");
1958 + console.log(" --exe [file] Input executable.");
1959 + console.log(" saveicon: Save a single icon bitmap to a .ico file.");
1960 + console.log(" --exe [file] Input executable.");
1961 + console.log(" --out [file] Resulting .ico file.");
1962 + console.log(" --icon [number] Icon number to save to file.");
1963 + console.log(" saveicons: Save an icon group to a .ico file.");
1964 + console.log(" --exe [file] Input executable.");
1965 + console.log(" --out [file] Resulting .ico file.");
1966 + console.log(" --icongroup [groupNumber] Icon groupnumber to save to file.");
1967 console.log("");
1968 console.log("Note that certificate PEM files must first have the signing certificate,");
1969 console.log("followed by all certificates that form the trust chain.");
@@ -1865,11 +1979,13 @@ function start() {
1979 console.log(" --originalfilename [value]");
1980 console.log(" --productname [value]");
1981 console.log(" --productversion [value]");
1982 + console.log(" --removeicongroup [number]");
1983 + console.log(" --icon [groupNumber],[filename.ico]");
1984 return;
1985 }
1986
1987 // Check that a valid command is passed in
1872 - if (['info', 'sign', 'unsign', 'createcert', 'icons', 'saveicon', 'header', 'timestamp', 'signblock'].indexOf(process.argv[2].toLowerCase()) == -1) {
1988 + if (['info', 'sign', 'unsign', 'createcert', 'icons', 'saveicon', 'saveicons', 'header', 'timestamp', 'signblock'].indexOf(process.argv[2].toLowerCase()) == -1) {
1989 console.log("Invalid command: " + process.argv[2]);
1990 console.log("Valid commands are: info, sign, unsign, createcert, timestamp");
1991 return;
@@ -1909,6 +2025,36 @@ function start() {
2025 if (resChanges == true) { exe.setVersionInfo(versionStrings); }
2026 }
2027
2028 + // Parse the icon changes
2029 + resChanges = false;
2030 + var icons = null;
2031 + if (exe != null) {
2032 + icons = exe.getIconInfo();
2033 + if (typeof args['removeicongroup'] == 'string') { // If --removeicongroup is used, it's to remove an existing icon group
2034 + const groupsToRemove = args['removeicongroup'].split(',');
2035 + for (var i in groupsToRemove) { if (icons[groupsToRemove[i]] != null) { delete icons[groupsToRemove[i]]; resChanges = true; } }
2036 + } else if (typeof args['removeicongroup'] == 'number') {
2037 + if (icons[args['removeicongroup']] != null) { delete icons[args['removeicongroup']]; resChanges = true; }
2038 + }
2039 + if (typeof args['icon'] == 'string') { // If --icon is used, it's to add or replace an existing icon group
2040 + const iconToAddSplit = args['icon'].split(',');
2041 + if (iconToAddSplit.length != 2) { console.log("The --icon format is: --icon [number],[file]."); return; }
2042 + const iconName = parseInt(iconToAddSplit[0]);
2043 + const iconFile = iconToAddSplit[1];
2044 + const icon = exe.loadIcon(iconFile);
2045 + if (icon == null) { console.log("Unable to load icon: " + iconFile); return; }
2046 + 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
2050 + } else {
2051 + icons[iconName] = icon; // We are adding an icon group
2052 + resChanges = true;
2053 + }
2054 + }
2055 + if (resChanges == true) { exe.setIconInfo(icons); }
2056 + }
2057 +
2058 // Execute the command
2059 var command = process.argv[2].toLowerCase();
2060 if (command == 'info') { // Get signature information about an executable
@@ -1983,7 +2129,7 @@ function start() {
2129 if (resChanges == false) {
2130 if (exe.header.signed) {
2131 console.log("Unsigning to " + args.out);
1986 - exe.unsign(args); // Simple unsign, copy most of the original file.
2132 + exe.unsign(args); // Simple unsign, copy most of the original file.
2133 console.log("Done.");
2134 } else {
2135 console.log("Executable is not signed.");
@@ -2045,6 +2191,48 @@ function start() {
2191 fs.writeFileSync(args.out, Buffer.concat([buf, icon.icon]));
2192 console.log("Done.");
2193 }
2194 + if (command == 'saveicons') { // Save an icon group to file
2195 + if (exe == null) { console.log("Missing --exe [filename]"); return; }
2196 + if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; }
2197 + if (typeof args.icongroup != 'number') { console.log("Missing or incorrect --icongroup [number]"); return; }
2198 + const iconInfo = exe.getIconInfo();
2199 + const iconGroup = iconInfo[args.icongroup];
2200 + if (iconGroup == null) { console.log("Invalid or incorrect --icongroup [number]"); return; }
2201 +
2202 + // Count the number of icons in the group
2203 + var iconCount = 0;
2204 + for (var i in iconGroup.icons) { iconCount++; }
2205 +
2206 + // .ico header: https://en.wikipedia.org/wiki/ICO_(file_format)
2207 + const iconFileData = [];
2208 + const header = Buffer.alloc(6);
2209 + header.writeUInt16LE(1, 2); // 1 = Icon, 2 = Cursor
2210 + header.writeUInt16LE(iconCount, 4); // Icon Count, always 1 in our case
2211 + iconFileData.push(header);
2212 +
2213 + // Store each icon header
2214 + var offsetPtr = 6 + (16 * iconCount);
2215 + for (var i in iconGroup.icons) {
2216 + const buf = Buffer.alloc(16);
2217 + buf[0] = iconGroup.icons[i].width; // Width (0 = 256)
2218 + buf[1] = iconGroup.icons[i].height; // Height (0 = 256)
2219 + buf[2] = iconGroup.icons[i].colorCount; // Colors
2220 + buf.writeUInt16LE(iconGroup.icons[i].planes, 4); // Color planes
2221 + buf.writeUInt16LE(iconGroup.icons[i].bitCount, 6); // Bits per pixel
2222 + buf.writeUInt32LE(iconGroup.icons[i].icon.length, 8); // Size
2223 + buf.writeUInt32LE(offsetPtr, 12); // Offset
2224 + offsetPtr += iconGroup.icons[i].icon.length;
2225 + iconFileData.push(buf);
2226 + }
2227 +
2228 + // Store each icon
2229 + for (var i in iconGroup.icons) { iconFileData.push(iconGroup.icons[i].icon); }
2230 +
2231 + // Write the .ico file
2232 + console.log("Writing to " + args.out);
2233 + fs.writeFileSync(args.out, Buffer.concat(iconFileData));
2234 + console.log("Done.");
2235 + }
2236 if (command == 'signblock') { // Display the raw signature block of the executable in hex
2237 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2238 var buf = exe.getRawSignatureBlock();