Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determining if a dll is a .valid CLR dll by reading the PE directly (64bit issue)

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?

like image 289
Vaevictus Avatar asked Dec 21 '11 16:12

Vaevictus


1 Answers

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.

like image 156
Hans Avatar answered Oct 16 '22 09:10

Hans