PE Import Table and custom DLL paths

Once upon a time, an interesting software vulnerability vector called DLL Hijacking became very popular, thanks to a Slovenian security research outfit – ACROS Security, as well as HD Moore and his DLL Hijacking Audit Kit. In short, the vulnerability class allowed an attacker to execute arbitrary code in the context of an application, which had been used to open an associated file from a remote share. The root cause of its existence was a combination of the three, following facts:

  1. When opening a file from a remote share (such as WebDAV), the application’s Current Working Directory is set to the remote share path,
  2. Using the LoadLibrary API with a relative path results in following a (Safe) Dynamic-Link Library Search Order. When the specified library cannot be found in the program’s directory and system folders, it is loaded from the CWD,
  3. Some (flawed) applications try to load non-existent modules by their names (and react accordingly, if the DLL is not found).

The first two points have been well-documented in the MSDN Library, while the latter one was quite a typical Windows developers’ attitude, until the vulnerability class drew so much public attention. In this post, I would like to describe an idea, which I (together with gyn) had long before the DLL Hijacking hype. It is actually the very opposite of the previous concept – this time, our interest is primarily focused around static Portable Executable imports (rather than dynamic ones), and complex paths, instead of straight-forward library names (e.g. dx9draw.dll). The concept was than developed (and implemented, in the form of a straight-forward SMB server) by Gynvael Coldwind, which I would like to thank here.

A great majority of the Windows executable images contains an Import Table – a list of external modules (and their exported functions), which are utilized by the considered application. Each imported module is described by the following structure:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
  _ANONYMOUS_UNION union {
    DWORD Characteristics;
    DWORD OriginalFirstThunk;
  } DUMMYUNIONNAME;
  DWORD TimeDateStamp;
  DWORD ForwarderChain;
  DWORD Name;
  DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;

The Import Table consists of numerous IMAGE_IMPORT_DESCRIPTOR structures, and serves as information for the Windows PE loader, residing inside ntdll.dll. Out of all the fields, we are mostly interested in Name, whose meaning is described in the official Microsoft PE and COFF Specification as follows:

Offset Size Field Description
12 4 Name RVA The address of an ASCII string that contains the name of the DLL. This address is relative to the image base.

Apparently, the authors of the document didn’t consider the field worth being described in more detail, thus several issues – e.g. what the string can, or cannot be – remain unknown. In order to figure out, how the import parsing is actually performed, we have to take a look ntdll.dll, or more specifically, ntdll!LdrpMapDll. The routine is used to map a certain DLL file into the local process context, and is used by both process-initialization, and dynamic library loading code. It first translates the user-specified path into an internal NT format:

.text:7C91C617                 push    ebx             ; int
.text:7C91C618                 push    ebx             ; int
.text:7C91C619                 lea     eax, [ebp+var_48]
.text:7C91C61C                 push    eax             ; int
.text:7C91C61D                 push    [ebp+var_38]    ; wchar_t *
.text:7C91C620                 call    _RtlDosPathNameToNtPathName_U@16 ; RtlDosPathNameToNtPathName_U(x,x,x,x)
.text:7C91C625                 test    al, al
.text:7C91C627                 jz      loc_7C94073E
.text:7C91C62D                 lea     eax, [ebp+var_20]
.text:7C91C630                 push    eax
.text:7C91C631                 push    [ebp+arg_8]
.text:7C91C634                 lea     eax, [ebp+var_88]
.text:7C91C63A                 push    eax
.text:7C91C63B                 push    [ebp+var_98]
.text:7C91C641                 lea     eax, [ebp+var_48]
.text:7C91C644                 push    eax
.text:7C91C645                 call    _LdrpCreateDllSection@20 ; LdrpCreateDllSection(x,x,x,x,x)

and then creates a section in the local memory context, based on the DLL contents:

.text:7C91C923 ; __stdcall LdrpCreateDllSection(x, x, x, x, x)
.text:7C91C923 _LdrpCreateDllSection@20 proc near      ; CODE XREF: LdrpMapDll(x,x,x,x,x,x)+7DD p
.text:7C91C923
(...)
.text:7C91C955                 mov     [ebp+var_20], 18h
.text:7C91C95C                 mov     [ebp+var_1C], ebx
.text:7C91C95F                 mov     [ebp+var_14], 40h
.text:7C91C966                 mov     [ebp+var_10], ebx
.text:7C91C969                 mov     [ebp+var_C], ebx
.text:7C91C96C                 call    _NtOpenFile@24  ; NtOpenFile(x,x,x,x,x,x)
.text:7C91C971                 mov     esi, eax
.text:7C91C973                 cmp     esi, ebx
.text:7C91C975                 jl      loc_7C93F968
.text:7C91C97B
.text:7C91C97B loc_7C91C97B:                           ; CODE XREF: LdrpCreateDllSection(x,x,x,x,x)+18D j
(...)
.text:7C91C98D                 push    esi
.text:7C91C98E                 call    _NtCreateSection@28 ; NtCreateSection(x,x,x,x,x,x,x)
(...)

The NtOpenFile call can be, in turn, translated into the following C-like code (with all the parameters’ flags translated):

NtOpenFile(&FileHandle,
           SYNCHRONIZE | FILE_EXECUTE
           &ObjectAttributes,
           &IoStatus,
           FILE_SHARE_READ | FILE_SHARE_DELETE,
           FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT);

As can be seen, one can set the imported DLL name to any path, which can be successfully represented in the low-level NT format, and can be opened with the above flags. Consequently, it is possible to use data files, logical, virtual, and physical devices and volumes, represented by absolute, relative, and network paths. This observation provides a great deal of possible (more or less) interesting usages, some of which are described in the following sections.

Absolute paths

Making use of absolute paths makes it possible for the application developer to specifiy one, specific library to be loaded, regardless of any additional factors, such as Dynamic-Load Library Search Order. For example, one might want to use the following path:

C:\Program Files\Custom Application\kernel32.dll

in order to import from a custom library called kernel32.dll (normally impossible, kernel32 being one of the Known Dlls). One has to keep in mind, however, that hard-coding an absolute path may result in program unreliability, i.e. every time the location of the required DLL is different from the expected one, the process will not be created.

Relative paths

Thanks to relative paths used in the context of PE imports, a developer doesn’t have to put all of the dependencies into the main executable’s directory. Instead, one can group the modules by putting them into separate folders, and link to them using paths such as “\Network\Winsock.dll”. Another possible usages of relative paths might be to take advantage of the ..\ marker, e.g. reference a library on the root of the currently considered path (with regard to the standard search order):

\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\module.dll

provided that the original path is no more than sixteen-level deep.

Network paths

Specifying remote shares as the DLL import filename is by far the most useful and interesting field of the discussed area. This is mainly a consequence of the fact, that the concept can be used to interact with remote machines over the internet, creating plenty attractive possibilities. Let’s take a look…

Debugger detection and disruption

Since all of the modern, mainstream debuggers aim to provide advanced and complex functionality to the reverser / developer, they are forced to actively interact with the executable images, loaded in the context of the analyzed program. One example of such functionality might be Symbol Management – in order to find and apply the internal names, a debugger must first search for PDB (or other) files, open the images, parse their Export Tables, and so on. When one (or more) DLLs are loaded from a remote share, the debugger starts to use the network, as well – and that’s where we can really break something.

Let’s take a look at the Process Monitor output, generated upon launching an Ollydbg instance with a specially crafted target (importing from \\127.0.0.1\c\a.dll):

Ollydbg in action

Apparently, the debugger sends numerous requests, in search of symbol files, associated with one of the loaded modules. Unfortunately, the remote server receives these requests, and is able to react, accordingly. This very same behavior can also be observed for Immunity Debugger, for obvious reasons. When it comes to the remaining debuggers – Windbg and IDA Pro Debugger, the results are as follows:

WinDbg in action
IDA Pro Advanced in action

As shown, all of the commonly-used debuggers (with their default configuration) try to find several, external debugging files at the remote server (worst case), or at least open the file (list its directory etc). The question is – how can a server react, knowing that an application is being debugged somewhere in the net? Personally, I can see two possible options:

  1. Perform a high-latency attack, i.e. reply to the debugger’s file requests in a very slow manner. By sending a single byte every ten seconds, it would take forever for the debugger to load the entire file (if there was any file, at all), and the reverser would have to give up (or find another way to perform the analysis). One would have to adjust the latencies in such a way, that the connection doesn’t get closed due to a timeout; this, however, is just a matter of a few empirical tests,
  2. Import two libraries from a remote server; then use one for anti-debugging purposes (so that the server observes the accompanying network traffic), and then the other one for regular purposes – the server can provide the application with a fake second DLL, if a background debugger is detected.

In general, I find the technique very amusing, and realize it is not the most reliable one. Even though I don’t know any way to disable the symbol-searching process in most of the discussed debuggers, I suppose that dumping the remote executable image on disk, and changing the import name to a local path would be enough to defeat the concept (unless additional protection measures would not be applied). :-)

Automatic updates

Obviously, network paths could be used for a more practical purpose. Instead of updating the executable files by downloading their new versions on the hard drive, an application can simply download the file from a network share every time it is launched, which guarantees the usage of the newest, up-to-date image version.

Users’ IP and activity monitoring

The fact that a SMB server is informed every time when a program is executed, can be taken advantage of in order to monitor various factors, such as daily users’ activity (i.e. the number of application launches), IP addresses, and other kind of information, which can be retrieved or deducted, based on the number, time, and contents of the incoming requests.

Conclusion

To sum everything up, custom paths in static PE imports can be really useful in certain situations, and the feature is worth to be kept in mind. Please, stay tuned for the upcoming blog posts (not only Sunday Blog Entries), since some awesome, fresh Elevation of Privileges (and other) vulnerabilities are going to be discussed here, past July’s Microsoft Patch Tuesday. Cheers!

7 thoughts on “PE Import Table and custom DLL paths”

  1. Pingback: IDELIT
  2. @Han: thanks! and well, the recent posts are meant to be technically mid-level, there’s not much I can do about it… :-)
    However, if there’s anything you don’t understand in particular, feel free to drop me a line for an explanation.

  3. This has also been used as an anti-debug trick in software protectors by overflowing the .dll names and or the api names. This will stop Ollydbg before it can even load the file. However, this can be fixed by simply opening the file in CFF or a similar application and just deleting all the dead space in the names. Of course the Windows loader isn’t affected.

Comments are closed.