6

I am working on migrating a 32bit web application into 64bit and am having some problems with our plugin loader code.

In the 32bit version, we scan the webapps bin directory for all .net dlls, then load them with Assembly.Load to check for the presence of our plugin attributes.

We did this in a rather nifty way using public domain code:

/// <summary>
/// Returns true if the file specified is a real CLR type, 
/// otherwise false is returned.
/// False is also returned in the case of an exception being caught
/// </summary>
/// <param name="file">A string representing the file to check for 
/// CLR validity</param>
/// <returns>True if the file specified is a real CLR type, 
/// otherwise false is returned.
/// False is also returned in the case of an exception being 
/// caught</returns>
public static bool IsDotNetAssembly(String file)
{   
    Stream fs = new FileStream(@file, FileMode.Open, FileAccess.Read);

    try
    {
        BinaryReader reader = new BinaryReader(fs);
        //PE Header starts @ 0x3C (60). Its a 4 byte header.
        fs.Position = 0x3C;
        uint peHeader = reader.ReadUInt32();
        //Moving to PE Header start location...
        fs.Position = peHeader;
        uint peHeaderSignature = reader.ReadUInt32();
        ushort machine = reader.ReadUInt16();
        ushort sections = reader.ReadUInt16();
        uint timestamp = reader.ReadUInt32();
        uint pSymbolTable = reader.ReadUInt32();
        uint noOfSymbol = reader.ReadUInt32();
        ushort optionalHeaderSize = reader.ReadUInt16();
        ushort characteristics = reader.ReadUInt16();

        // PE Optional Headers 
        // To go directly to the datadictionary, we'll increase the stream's current position to with 96 (0x60). 
        // 28 bytes for Standard fields
        // 68 bytes for NT-specific fields 
        // 128 bytes DataDictionary 
        //  DataDictionay has 16 directories
        //  8 bytes per directory (4 bytes RVA and 4 bytes of Size.) 
        // 15th directory consist of CLR header! (if its 0, it is not a CLR file )

        uint[] dataDictionaryRVA = new uint[16];
        uint[] dataDictionarySize = new uint[16];            
        ushort dataDictionaryStart = Convert.ToUInt16(Convert.ToUInt16(fs.Position) + 0x60);

        fs.Position = dataDictionaryStart;
        for (int i = 0; i < 15; i++)
        {
            dataDictionaryRVA[i] = reader.ReadUInt32();
            dataDictionarySize[i] = reader.ReadUInt32();
        }
        if (dataDictionaryRVA[14] == 0)
        {
            fs.Close();
            return false;
        }
        else
        {
            fs.Close();
            return true;
        }
    }
    catch (Exception)
    {
        return false;
    }
    finally
    {
        fs.Close();
    }
}

Now the problem is that we now have to handle 64bit or platform independent dlls and the offset seems to have changed and this code fails. Does anyone know the correct modifications to the above code to return true for valid 64bit only OR platform independent dlls?

Rob
  • 45,296
  • 24
  • 122
  • 150
Vaevictus
  • 73
  • 4
  • 1
    Do you really have to use the code above? – Hans Dec 21 '11 at 18:13
  • 1
    Nifty? More like implementation-specific, assumption-making and bound-to-get-you-into-trouble... oh wait, it did already. :) – bzlm Dec 21 '11 at 18:46
  • the web application is huge - with over 100 dlls in the bin directory. Many of the dlls are not plugins for our app and many are not clr dlls, so using Assembly.Load is not a good option. – Vaevictus Dec 21 '11 at 20:32

3 Answers3

7

The reason your code is not working for x64-Bit DLLs is because the image optional header size of a x64-Bit DLL and a x86-Bit DLL is different. You have to take the different image optional header sizes into account in order to determine whether or not a given DLL is a .Net DLL.

The PE file format specification describes in section 3.4 (Optional Header) the different offsets to jump to the data directories:

  1. For PE32 (x86) images the offset is 0x60 (as it is in your code) and
  2. for PE32+ (x64) images the offset is 0x70.

In order to determine whether or not a given DLL is a x64 Bit DLL you have to read the magic bytes of the optional header:

  1. A value of 0x20b means PE32+,
  2. a value of 0x10b PE32.

I've extended your example:

Stream fs = new FileStream(@file, FileMode.Open, FileAccess.Read);

try
{
  BinaryReader reader = new BinaryReader(fs);
  //PE Header starts @ 0x3C (60). Its a 4 byte header.
  fs.Position = 0x3C;
  uint peHeader = reader.ReadUInt32();
  //Moving to PE Header start location...
  fs.Position = peHeader;
  uint peHeaderSignature = reader.ReadUInt32();
  ushort machine = reader.ReadUInt16();
  ushort sections = reader.ReadUInt16();
  uint timestamp = reader.ReadUInt32();
  uint pSymbolTable = reader.ReadUInt32();
  uint noOfSymbol = reader.ReadUInt32();
  ushort optionalHeaderSize = reader.ReadUInt16();
  ushort characteristics = reader.ReadUInt16();

  long posEndOfHeader = fs.Position;
  ushort magic = reader.ReadUInt16();

  int off = 0x60; // Offset to data directories for 32Bit PE images
                  // See section 3.4 of the PE format specification.
  if (magic == 0x20b) //0x20b == PE32+ (64Bit), 0x10b == PE32 (32Bit)
  {
    off = 0x70;  // Offset to data directories for 64Bit PE images
  }
  fs.Position = posEndOfHeader;       

  uint[] dataDictionaryRVA = new uint[16];
  uint[] dataDictionarySize = new uint[16];
  ushort dataDictionaryStart = Convert.ToUInt16(Convert.ToUInt16(fs.Position) + off);

  fs.Position = dataDictionaryStart;

  for (int i = 0; i < 15; i++)
  {
    dataDictionaryRVA[i] = reader.ReadUInt32();
    dataDictionarySize[i] = reader.ReadUInt32();
  }
  if (dataDictionaryRVA[14] == 0)
  {
    fs.Close();
    return false;
  }
  else
  {
    fs.Close();
    return true;
  }
 }
 catch (Exception)
 {
   return false;
 }
 finally
 {
   fs.Close();
 }

In the Windows SDK there are also structures defined for the PE32/PE32+ optional headers. A description of those structures can be found here MSDN.

Hope, this helps.

Hans
  • 12,902
  • 2
  • 57
  • 60
3

For an alternative that does not use reflection and does not load assemblies directly, try the Common Compiler Infrastructure Metadata API. It seems that you can fairly easily load a PE assembly and determine if it has a CLR module.

MetadataReaderHost host = new PeReader.DefaultHost();
var module = host.LoadUnitFrom(args[0]) as IModule;
if (module == null)
{
     Console.WriteLine(args[0]+" is not a PE file containing a CLR module or assembly.");
     return;
}
bobbymcr
  • 23,769
  • 3
  • 56
  • 67
2

Is there a reason you cannot use methods in the framework? Sample code below:

        var assembly = Assembly.Load("path to assembly");
        ImageFileMachine machine;
        PortableExecutableKinds peKind;
        assembly.ManifestModule.GetPEKind(out peKind, out machine);

GetPEKind method on MSDN and PortableExecutableKinds should get you started. The latter is basically corflags

Toni Parviainen
  • 2,217
  • 1
  • 16
  • 15
  • 4
    Not a solution, the Load() method will bomb if it is not a .NET assembly or if it targets the wrong .NET version or bitness. – Hans Passant Dec 21 '11 at 19:31