From e281faf6640060963a50938f83362b30dfada5ba Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sat, 12 Nov 2022 21:56:24 -0800 Subject: [PATCH] Add first attempt at PE certificate parsing --- .../AbstractSyntaxNotationOne.cs | 383 ++++++++++++++++++ BurnOutSharp.Builder/Extensions.cs | 8 + BurnOutSharp.Builder/PortableExecutable.cs | 12 +- ExecutableTest/Program.cs | 31 +- 4 files changed, 429 insertions(+), 5 deletions(-) create mode 100644 BurnOutSharp.Builder/AbstractSyntaxNotationOne.cs diff --git a/BurnOutSharp.Builder/AbstractSyntaxNotationOne.cs b/BurnOutSharp.Builder/AbstractSyntaxNotationOne.cs new file mode 100644 index 00000000..5997dc27 --- /dev/null +++ b/BurnOutSharp.Builder/AbstractSyntaxNotationOne.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; + +namespace BurnOutSharp.Builder +{ + /// + /// ASN.1 type indicators + /// + [Flags] + public enum ASN1Type : byte + { + #region Modifiers + + V_ASN1_UNIVERSAL = 0x00, + V_ASN1_PRIMITIVE_TAG = 0x1F, + V_ASN1_CONSTRUCTED = 0x20, + V_ASN1_APPLICATION = 0x40, + V_ASN1_CONTEXT_SPECIFIC = 0x80, + V_ASN1_PRIVATE = 0xC0, + + #endregion + + #region Types + + V_ASN1_EOC = 0x00, + V_ASN1_BOOLEAN = 0x01, + V_ASN1_INTEGER = 0x02, + V_ASN1_BIT_STRING = 0x03, + V_ASN1_OCTET_STRING = 0x04, + V_ASN1_NULL = 0x05, + V_ASN1_OBJECT = 0x06, + V_ASN1_OBJECT_DESCRIPTOR = 0x07, + V_ASN1_EXTERNAL = 0x08, + V_ASN1_REAL = 0x09, + V_ASN1_ENUMERATED = 0x0A, + V_ASN1_UTF8STRING = 0x0C, + V_ASN1_SEQUENCE = 0x10, + V_ASN1_SET = 0x11, + V_ASN1_NUMERICSTRING = 0x12, + V_ASN1_PRINTABLESTRING = 0x13, + V_ASN1_T61STRING = 0x14, + V_ASN1_TELETEXSTRING = 0x14, + V_ASN1_VIDEOTEXSTRING = 0x15, + V_ASN1_IA5STRING = 0x16, + V_ASN1_UTCTIME = 0x17, + V_ASN1_GENERALIZEDTIME = 0x18, + V_ASN1_GRAPHICSTRING = 0x19, + V_ASN1_ISO64STRING = 0x1A, + V_ASN1_VISIBLESTRING = 0x1A, + V_ASN1_GENERALSTRING = 0x1B, + V_ASN1_UNIVERSALSTRING = 0x1C, + V_ASN1_BMPSTRING = 0x1E, + + #endregion + } + + /// + /// ASN.1 Parser + /// + public class AbstractSyntaxNotationOne + { + /// + /// Parse a byte array into a DER-encoded ASN.1 structure + /// + /// Byte array representing the data + /// Current pointer into the data + /// + public static List Parse(byte[] data, int pointer) + { + // Create the output list to return + var topLevelValues = new List(); + + // Loop through the data and return all top-level values + while (pointer < data.Length) + { + var topLevelValue = new ASN1TypeLengthValue(data, ref pointer); + topLevelValues.Add(topLevelValue); + } + + return topLevelValues; + } + } + + /// + /// ASN.1 type/length/value class that all types are based on + /// + public class ASN1TypeLengthValue + { + /// + /// The ASN.1 type + /// + public ASN1Type Type { get; private set; } + + /// + /// Length of the value + /// + public ulong Length { get; private set; } + + /// + /// Generic value associated with + /// + public object Value { get; private set; } + + /// + /// Read from the source data array at an index + /// + /// Byte array representing data to read + /// Index within the array to read at + public ASN1TypeLengthValue(byte[] data, ref int index) + { + // Get the type and modifiers + this.Type = (ASN1Type)data[index++]; + + // If we have an end indicator, we just return + if (this.Type == ASN1Type.V_ASN1_EOC) + return; + + // Get the length of the value + this.Length = ReadLength(data, ref index); + + // Read the value + if (this.Type.HasFlag(ASN1Type.V_ASN1_CONSTRUCTED)) + { + var valueList = new List(); + + int currentIndex = index; + while (index < currentIndex + (int)this.Length) + { + valueList.Add(new ASN1TypeLengthValue(data, ref index)); + } + + this.Value = valueList.ToArray(); + } + else + { + // TODO: Get more granular based on type + this.Value = data.ReadBytes(ref index, (int)this.Length); + } + } + + /// + /// Format the TLV as a string + /// + /// [UNUSED] Padding level of the item when formatting + /// String representing the TLV, if possible + public string Format(int paddingLevel = 0) + { + // Create the left-padding string + string padding = new string(' ', paddingLevel); + + // If we have an invalid item + if (this.Type == 0) + return $"{padding}UNKNOWN TYPE"; + + // Create the string builder + StringBuilder formatBuilder = new StringBuilder(); + + // Append the type + formatBuilder.Append($"{padding}Type: {this.Type}"); + if (this.Type == ASN1Type.V_ASN1_EOC) + return formatBuilder.ToString(); + + // Append the length + formatBuilder.Append($", Length: {this.Length}"); + if (this.Length == 0) + return formatBuilder.ToString(); + + // If we have a constructed type + if (this.Type.HasFlag(ASN1Type.V_ASN1_CONSTRUCTED)) + { + var valueAsObjectArray = this.Value as ASN1TypeLengthValue[]; + if (valueAsObjectArray == null) + { + formatBuilder.Append(", Value: [INVALID DATA TYPE]"); + return formatBuilder.ToString(); + } + + formatBuilder.Append(", Value:\n"); + for (int i = 0; i < valueAsObjectArray.Length; i++) + { + var child = valueAsObjectArray[i]; + string childString = child.Format(paddingLevel + 1); + formatBuilder.Append($"{childString}\n"); + } + + return formatBuilder.ToString().TrimEnd('\n'); + } + + // Get the value as a byte array + byte[] valueAsByteArray = this.Value as byte[]; + if (valueAsByteArray == null) + { + formatBuilder.Append(", Value: [INVALID DATA TYPE]"); + return formatBuilder.ToString(); + } + + // If we have a primitive type + switch (this.Type) + { + /// + case ASN1Type.V_ASN1_BOOLEAN: + if (this.Length > 1 || valueAsByteArray.Length > 1) + formatBuilder.Append($" [Expected length of 1]"); + + bool booleanValue = valueAsByteArray[0] == 0x00 ? false : true; + formatBuilder.Append($", Value: {booleanValue}"); + break; + + /// + case ASN1Type.V_ASN1_INTEGER: + Array.Reverse(valueAsByteArray); + BigInteger integerValue = new BigInteger(valueAsByteArray); + formatBuilder.Append($", Value: {integerValue}"); + break; + + /// + case ASN1Type.V_ASN1_BIT_STRING: + // TODO: Read into a BitArray and print that out instead? + int unusedBits = valueAsByteArray[0]; + formatBuilder.Append($", Value with {unusedBits} unused bits: {BitConverter.ToString(valueAsByteArray.Skip(1).ToArray()).Replace('-', ' ')}"); + break; + + /// + case ASN1Type.V_ASN1_OCTET_STRING: + formatBuilder.Append($", Value: {BitConverter.ToString(valueAsByteArray).Replace('-', ' ')}"); + break; + + /// + /// + case ASN1Type.V_ASN1_OBJECT: + // The first byte contains nodes 1 and 2 + int firstNode = Math.DivRem(valueAsByteArray[0], 40, out int secondNode); + + // If there are only 2 nodes + if (this.Length == 1) + { + formatBuilder.Append($", Value: {firstNode}.{secondNode}"); + break; + } + + // Create a list for all remaining nodes + List objectNodes = new List(); + + // All other nodes are encoded uniquely + int objectValueOffset = 1; + while (objectValueOffset < (long)this.Length) + { + // If bit 7 is not set + if ((valueAsByteArray[objectValueOffset] & 0x80) == 0) + { + objectNodes.Add(valueAsByteArray[objectValueOffset]); + objectValueOffset++; + continue; + } + + // Otherwise, read the encoded value in a loop + ulong dotValue = 0; + bool doneProcessing = false; + + do + { + // Shift the current encoded value + dotValue <<= 7; + + // If we have a leading zero byte, we're at the end + if ((valueAsByteArray[objectValueOffset] & 0x80) == 0) + doneProcessing = true; + + // Clear the top byte + unchecked { valueAsByteArray[objectValueOffset] &= (byte)~0x80; } + + // Add the new value to the result + dotValue |= valueAsByteArray[objectValueOffset]; + + // Increment the offset + objectValueOffset++; + } while (objectValueOffset < valueAsByteArray.Length && !doneProcessing); + + // Add the parsed value to the output + objectNodes.Add(dotValue); + } + + // TODO: Add dot form decoding to this + formatBuilder.Append($", Value: {firstNode}.{secondNode}.{string.Join(".", objectNodes)}"); + break; + + /// + case ASN1Type.V_ASN1_UTF8STRING: + formatBuilder.Append($", Value: {Encoding.UTF8.GetString(valueAsByteArray)}"); + break; + + /// + case ASN1Type.V_ASN1_PRINTABLESTRING: + formatBuilder.Append($", Value: {Encoding.ASCII.GetString(valueAsByteArray)}"); + break; + + //case ASN1Type.V_ASN1_T61STRING: + case ASN1Type.V_ASN1_TELETEXSTRING: + formatBuilder.Append($", Value: {Encoding.ASCII.GetString(valueAsByteArray)}"); + break; + + /// + case ASN1Type.V_ASN1_IA5STRING: + formatBuilder.Append($", Value: {Encoding.ASCII.GetString(valueAsByteArray)}"); + break; + + case ASN1Type.V_ASN1_UTCTIME: + string utctimeString = Encoding.ASCII.GetString(valueAsByteArray); + if (DateTime.TryParse(utctimeString, out DateTime utctimeDateTime)) + formatBuilder.Append($", Value: {utctimeDateTime}"); + else + formatBuilder.Append($", Value: {utctimeString}"); + break; + + /// + case ASN1Type.V_ASN1_BMPSTRING: + formatBuilder.Append($", Value: {Encoding.Unicode.GetString(valueAsByteArray)}"); + break; + + default: + formatBuilder.Append($", Value (Unknown Format): {BitConverter.ToString(this.Value as byte[]).Replace('-', ' ')}"); + break; + } + + // Return the formatted string + return formatBuilder.ToString(); + } + + /// + /// Reads the length field for a type + /// + /// Byte array representing data to read + /// Index within the array to read at + /// The length value read from the array + private static ulong ReadLength(byte[] data, ref int index) + { + // If we have invalid data, throw an exception + if (data == null || index < 0 && index >= data.Length) + throw new ArgumentException(); + + // Read the first byte, assuming it's the length + byte length = data[index++]; + + // If the bit 7 is not set, then use the value as it is + if ((length & 0x80) == 0) + return length; + + // Otherwise, use the value as the number of remaining bytes to read + int bytesToRead = length & ~0x80; + byte[] bytesRead = data.ReadBytes(ref index, bytesToRead); + + // TODO: Write extensions to read big-endian + + // Reverse the bytes to be in big-endian order + Array.Reverse(bytesRead); + + switch (bytesRead.Length) + { + case 1: + return bytesRead[0]; + case 2: + return BitConverter.ToUInt16(bytesRead, 0); + case 3: + Array.Resize(ref bytesRead, 4); + goto case 4; + case 4: + return BitConverter.ToUInt32(bytesRead, 0); + case 5: + case 6: + case 7: + Array.Resize(ref bytesRead, 8); + goto case 8; + case 8: + return BitConverter.ToUInt64(bytesRead, 0); + default: + throw new InvalidOperationException(); + } + } + } +} diff --git a/BurnOutSharp.Builder/Extensions.cs b/BurnOutSharp.Builder/Extensions.cs index a0893db9..fffa06b0 100644 --- a/BurnOutSharp.Builder/Extensions.cs +++ b/BurnOutSharp.Builder/Extensions.cs @@ -24,6 +24,10 @@ namespace BurnOutSharp.Builder /// public static byte[] ReadBytes(this byte[] content, ref int offset, int count) { + // If there's an invalid byte count, don't do anything + if (count == 0) + return null; + byte[] buffer = new byte[count]; Array.Copy(content, offset, buffer, 0, Math.Min(count, content.Length - offset)); offset += count; @@ -158,6 +162,10 @@ namespace BurnOutSharp.Builder /// public static byte[] ReadBytes(this Stream stream, int count) { + // If there's an invalid byte count, don't do anything + if (count == 0) + return null; + byte[] buffer = new byte[count]; stream.Read(buffer, 0, count); return buffer; diff --git a/BurnOutSharp.Builder/PortableExecutable.cs b/BurnOutSharp.Builder/PortableExecutable.cs index 4d3b7e86..790d56a4 100644 --- a/BurnOutSharp.Builder/PortableExecutable.cs +++ b/BurnOutSharp.Builder/PortableExecutable.cs @@ -708,9 +708,13 @@ namespace BurnOutSharp.Builder entry.Revision = (WindowsCertificateRevision)data.ReadUInt16(ref offset); entry.CertificateType = (WindowsCertificateType)data.ReadUInt16(ref offset); if (entry.Length > 0) - entry.Certificate = data.ReadBytes(ref offset, (int)entry.Length); + entry.Certificate = data.ReadBytes(ref offset, (int)entry.Length - 8); attributeCertificateTable.Add(entry); + + // Align to the 8-byte boundary + while ((offset % 8) != 0) + _ = data.ReadByte(ref offset); } return attributeCertificateTable.ToArray(); @@ -1896,9 +1900,13 @@ namespace BurnOutSharp.Builder entry.Revision = (WindowsCertificateRevision)data.ReadUInt16(); entry.CertificateType = (WindowsCertificateType)data.ReadUInt16(); if (entry.Length > 0) - entry.Certificate = data.ReadBytes((int)entry.Length); + entry.Certificate = data.ReadBytes((int)entry.Length - 8); attributeCertificateTable.Add(entry); + + // Align to the 8-byte boundary + while ((data.Position % 8) != 0) + _ = data.ReadByteValue(); } return attributeCertificateTable.ToArray(); diff --git a/ExecutableTest/Program.cs b/ExecutableTest/Program.cs index 2f818279..e9f8e4fb 100644 --- a/ExecutableTest/Program.cs +++ b/ExecutableTest/Program.cs @@ -285,7 +285,6 @@ namespace ExecutableTest } } - if (executable.ResourceTable.TypeAndNameStrings.Count == 0) { Console.WriteLine(" No resource table type/name strings"); @@ -781,8 +780,34 @@ namespace ExecutableTest Console.WriteLine($" Length = {entry.Length}"); Console.WriteLine($" Revision = {entry.Revision}"); Console.WriteLine($" Certificate type = {entry.CertificateType}"); - //Console.WriteLine($" Certificate = {BitConverter.ToString(entry.Certificate).Replace("-", string.Empty)}"); - // TODO: Add certificate type parsing + Console.WriteLine(); + if (entry.CertificateType == BurnOutSharp.Models.PortableExecutable.WindowsCertificateType.WIN_CERT_TYPE_PKCS_SIGNED_DATA) + { + Console.WriteLine($" Certificate Data [Formatted]"); + Console.WriteLine(" -------------------------"); + var topLevelValues = AbstractSyntaxNotationOne.Parse(entry.Certificate, pointer: 0); + if (topLevelValues == null) + { + Console.WriteLine(" INVALID DATA FOUND"); + Console.WriteLine($" {BitConverter.ToString(entry.Certificate).Replace("-", string.Empty)}"); + } + else + { + foreach (ASN1TypeLengthValue tlv in topLevelValues) + { + string tlvString = tlv.Format(paddingLevel: 4); + Console.WriteLine(tlvString); + } + } + } + else + { + Console.WriteLine($" Certificate Data [Binary]"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" {BitConverter.ToString(entry.Certificate).Replace("-", string.Empty)}"); + } + + Console.WriteLine(); } } Console.WriteLine();