2

I load the entire PE into an std::vector<Byte> fileContent using std::fstream.

Then I obtain the executable's dos header:

IMAGE_DOS_HEADER* imageDosHeader = (IMAGE_DOS_HEADER*)fileContent.data();

After that, I check whether the PE is valid(MZ and PE00 signatures).

If it is, I get its import descriptor:

IMAGE_IMPORT_DESCRIPTOR* imageImportDescriptor = (IMAGE_IMPORT_DESCRIPTOR*)((DWORD)imageDosHeader + imageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

//Note: the VirtualAddress field equals 0x4FFC, so I assume it's valid

Now, I iterate through the dlls and try to display their names as following:

for(DWORD i = 0; ; i++)
{
    bool isCurrentDllValid = true;

    //if all fields of the current dll are zeros, then this dll is the last one, so break the outer loop
    for(DWORD j = 0; j < sizeof(IMAGE_IMPORT_DESCRIPTOR); j++)
    {
        if((*(DWORD*)((DWORD)&imageImportDescriptor[i] + j)))
            break;
        else if(j == sizeof(IMAGE_IMPORT_DESCRIPTOR) - 1)
            isCurrentDllValid = false;
    }

    if(!isCurrentDllValid)
        break;

    char* dllName = (char*)((DWORD)imageDosHeader + imageImportDescriptor[i].Name);

The problem is: an attempt to display the dllName causes crash.

Also, the Name field is an RVA, but its value is 0x6C61766E (the same thing is with the rest of the fields, the smallest one is TimeDateStamp with 0x637465, still abnormal one), while size of the PE is less than 0x7000.

In hex editor, RVA of e.g. "KERNEL32.dll" is 0x46F0.

Have you any idea why is it so? Am I missing something really simple?

  • 1
    You are forgetting that rva is not the same as the offset in the file. Unless you have a dump from memory in your vector you will have to convert the rva to a file offset. – mrexodia Oct 18 '18 at 9:11
  • @mrexodia Thanks for your reply, I realized that yesterday. It's such a silly mistake. – anx199 Oct 18 '18 at 14:44
  • @mrexodia you should add this as an answer so it’s not hidden in a comment – Igor Skochinsky Oct 25 '18 at 6:54
  • I usually only use this site from my phone, so it’s always cumbersome to write answers... I wrote one though:) – mrexodia Oct 25 '18 at 7:09
2

Unfortunately the real life samples don’t always follow the nice and tidy specification; they only need to work on a specific implementation, not the ideal one described in the documentation.

Corkami gathers details about practical PE format features, for example:

If the Import Lookup Table is present, then it also determines the length of the Import Address Table, as they are parsed in parallel. In this case, the Import Address Table doesn't have to be null-terminated.

Or maybe this:

If the IAT is linked in the descriptor but empty, then the DLL is not loaded, and the file is loaded even with an invalid dll name - the descriptor is skipped.

So check if you have either situation.

4

Unless you are loading a mapped PE file in your vector, you have to convert all RVAs to file offsets.

The pseodocode looks something like this:

func rva2offset(pe, rva):
    for section in pe.sections:
        if rva >= section.rva and rva < section.rva + section.size:
            return section.fileoffset + (rva - section.rva)
    return nil

The code does not account for the actual way the kernel maps the PE file in memory (alignment and other edge cases), so use it with care.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.