Q:

How to determine if a .NET assembly was built with platform target AnyCPU, AnyCPU Prefer32-bit, x86, x64 without using reflection and third party SW

I am interested in way how to read assembly platform target info from file directly by correct reading its PE header. I am aware of possibility to load assembly into new AppDomain by Assembly.ReflectionOnlyLoad(rawAssembly); and investigate it by assembly.ManifestModule.GetPEKind(out var peKind, out var machine);. I am also aware of third party tools as CorFlags.exe which can read CorFlag values. But I am interested in getting platform target info directly from file.
Thanks

A:

/// <summary>
/// Provides basic information about an Assembly derived from its metadata.
/// </summary>
public class CorFlagsReader
{
    #region Properties

    /// <summary>
    /// Gets the major version of the CLI runtime required for the assembly.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Typical runtime versions:
    /// <list type="bullet">
    /// <item>0 for unmanaged PE Files.</item>
    /// <item>2.0: .Net 1.0 / .Net 1.1</item>
    /// <item>2.5: .Net 2.0 / .Net 3.0 / .Net 3.5 / .Net 4.0</item>
    /// </list>
    /// </para>
    /// </remarks>
    public int MajorRuntimeVersion { get; }

    /// <summary>
    /// Gets the minor version of the CLI runtime required for the assembly.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Typical runtime versions:
    /// <list type="bullet">
    /// <item>0 for unmanaged PE Files.</item>
    /// <item>2.0: .Net 1.0 / .Net 1.1</item>
    /// <item>2.5: .Net 2.0 / .Net 3.0 / .Net 3.5 / .Net 4.0</item>
    /// </list>
    /// </para>
    /// </remarks>
    public int MinorRuntimeVersion { get; }

    /// <summary>
    /// Gets the processor architecture required for the assembly or 
    /// </summary>
    /// <returns>Possible return values: X86, Amd64, MSIL </returns>
    public ProcessorArchitecture ProcessorArchitecture { get; }

    /// <summary>
    /// If true the PE files does not contain any unmanaged parts. Otherwise it is a managed C++ Target.
    /// </summary>
    public bool IsPureIL { get; }

    /// <summary>
    /// Gets information whether Is32BitRequired flag is set in PE header
    /// </summary>
    public bool Is32BitReq { get; }

    /// <summary>
    /// Gets information whether Is32BitPrefered flag is set in PE header
    /// </summary>
    public bool Is32BitPref { get; }

    /// <summary>
    /// Returns true when the assembly is signed.
    /// </summary>
    public bool IsSigned { get; }

    #endregion

    #region Private classes & enums

    private enum PEFormat : ushort
    {
        PE32 = 0x10b,
        PE32Plus = 0x20b
    }

    [Flags]
    private enum CorFlags : uint
    {
        ILOnly = 0x00000001,
        Requires32Bit = 0x00000002,
        ILLibrary = 0x00000004,
        StrongNameSigned = 0x00000008,
        NativeEntryPoint = 0x00000010,
        TrackDebugData = 0x00010000,
        Prefers32Bit = 0x00020000,
    }

    private class Section
    {
        public uint VirtualAddress;
        public uint VirtualSize;
        public uint Pointer;
    }

    #endregion

    #region Constructor

    private CorFlagsReader(ushort majorRuntimeVersion, ushort minorRuntimeVersion, CorFlags corflags, PEFormat peFormat)
    {
        MajorRuntimeVersion = majorRuntimeVersion;
        MinorRuntimeVersion = minorRuntimeVersion;

        IsPureIL = (corflags & CorFlags.ILOnly) == CorFlags.ILOnly;
        Is32BitReq = (corflags & CorFlags.Requires32Bit) == CorFlags.Requires32Bit;
        Is32BitPref = (corflags & CorFlags.Prefers32Bit) == CorFlags.Prefers32Bit;
        IsSigned = (corflags & CorFlags.StrongNameSigned) == CorFlags.StrongNameSigned;

        ProcessorArchitecture = peFormat == PEFormat.PE32Plus
            ? ProcessorArchitecture.Amd64
            : (corflags & CorFlags.Requires32Bit) == CorFlags.Requires32Bit || !IsPureIL
                ? ProcessorArchitecture.X86
                : ProcessorArchitecture.MSIL;
    }

    #endregion

    #region Public static methods constructing CorFlagsReader instance

    /// <summary>
    /// Reads the PE file 
    /// </summary>
    /// <param name="fileName">PE file to read from</param>
    /// <returns>null if the PE file was not valid, an instance of the CorFlagsReader class containing the requested data.</returns>
    /// <exception cref="FileNotFoundException">When the file could not be found.</exception>
    public static CorFlagsReader ReadAssemblyMetadata(string fileName)
    {
        using (var fStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
        {
            return ReadAssemblyMetadata(fStream);
        }
    }

    /// <summary>
    /// Reads the PE file
    /// </summary>
    /// <param name="assemblyAsByteArray"></param>
    /// <returns></returns>
    public static CorFlagsReader ReadAssemblyMetadata(byte[] assemblyAsByteArray)
    {
        using (var stream = new MemoryStream(assemblyAsByteArray))
        {
            return ReadAssemblyMetadata(stream);
        }
    }

    /// <summary>
    /// Reads the PE file 
    /// </summary>
    /// <param name="stream">PE file stream to read from.</param>
    /// <returns>null if the PE file was not valid, an instance of the CorFlagsReader class containing the requested data.</returns>
    public static CorFlagsReader ReadAssemblyMetadata(Stream stream)
    {
        if (stream == null)
            throw new ArgumentNullException(nameof(stream));

        long length = stream.Length;
        if (length < 0x40)
            return null;

        using (var reader = new BinaryReader(stream, new UTF8Encoding(), true))
        {
            // Read the pointer to the PE header.
            stream.Position = 0x3c;
            uint peHeaderPtr = reader.ReadUInt32();
            if (peHeaderPtr == 0)
                peHeaderPtr = 0x80;

            // Ensure there is at least enough room for the following structures:
            //     24 byte PE Signature & Header
            //     28 byte Standard Fields         (24 bytes for PE32+)
            //     68 byte NT Fields               (88 bytes for PE32+)
            // >= 128 byte Data Dictionary Table
            if (peHeaderPtr > length - 256)
                return null;

            // Check the PE signature.  Should equal 'PE\0\0'.
            stream.Position = peHeaderPtr;
            var peSignature = reader.ReadUInt32();
            if (peSignature != 0x00004550)
                return null;

            // Read PE header fields.
            var  machine = reader.ReadUInt16();
            var numberOfSections = reader.ReadUInt16();
            var timeStamp = reader.ReadUInt32();
            var symbolTablePtr = reader.ReadUInt32();
            var numberOfSymbols = reader.ReadUInt32();
            var optionalHeaderSize = reader.ReadUInt16();
            var characteristics = reader.ReadUInt16();

            // Read PE magic number from Standard Fields to determine format.
            PEFormat peFormat = (PEFormat)reader.ReadUInt16();
            if (peFormat != PEFormat.PE32 && peFormat != PEFormat.PE32Plus)
                return null;

            // Read the 15th Data Dictionary RVA field which contains the CLI header RVA.
            // When this is non-zero then the file contains CLI data otherwise not.
            stream.Position = peHeaderPtr + (peFormat == PEFormat.PE32 ? 232 : 248);
            var cliHeaderRva = reader.ReadUInt32();
            if (cliHeaderRva == 0)
                return new CorFlagsReader(0, 0, 0, peFormat);

            // Read section headers.  Each one is 40 bytes.
            //    8 byte Name
            //    4 byte Virtual Size
            //    4 byte Virtual Address
            //    4 byte Data Size
            //    4 byte Data Pointer
            //  ... total of 40 bytes
            var sectionTablePtr = peHeaderPtr + 24 + optionalHeaderSize;
            var sections = new Section[numberOfSections];
            for (int i = 0; i < numberOfSections; i++)
            {
                stream.Position = sectionTablePtr + i * 40 + 8;

                Section section = new Section { VirtualSize = reader.ReadUInt32(), VirtualAddress = reader.ReadUInt32() };
                reader.ReadUInt32();
                section.Pointer = reader.ReadUInt32();

                sections[i] = section;
            }

            // Read parts of the CLI header.
            var cliHeaderPtr = ResolveRva(sections, cliHeaderRva);
            if (cliHeaderPtr == 0)
                return null;

            stream.Position = cliHeaderPtr + 4;
            var majorRuntimeVersion = reader.ReadUInt16();
            var minorRuntimeVersion = reader.ReadUInt16();
            var metadataRva = reader.ReadUInt32();
            var metadataSize = reader.ReadUInt32();
            CorFlags corflags = (CorFlags)reader.ReadUInt32();

            // Done.
            return new CorFlagsReader(majorRuntimeVersion, minorRuntimeVersion, corflags, peFormat);
        }
    }

    #endregion

    #region Private methods

    private static uint ResolveRva(Section[] sections, uint rva)
    {
        foreach (var section in sections)
        {
            if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize)
                return rva - section.VirtualAddress + section.Pointer;
        }

        return 0;
    }

    #endregion
}

And usage:
var res = CorFlagsReader.ReadAssemblyMetadata("file.exe");

Possible results:
----------------------------------------------------------------
| Assembly target | Processor architecture | Is32Pref| Is32Req |
----------------------------------------------------------------
| Any CPU         | MSIL                   | false   | false   |
----------------------------------------------------------------
| Any CPU Pref32  | x86                    | true    |         |
----------------------------------------------------------------
| x64             | Amd64                  | false   | false   |
----------------------------------------------------------------
| x86             | x86                    | false   | true    |
---------------------------------------------------------------- 

Solution is based on following links:
https://www.codeproject.com/Articles/1098551/Which-Platform-to-Target-your-NET-Applications,
https://archive.codeplex.com/?p=apichange#ApiChange.Api/src/Introspection/CorFlagsReader.cs
CorFlagsReader.cs - under Apache License, Version 2.0

