Normal view

There are new articles available, click to refresh the page.
Before yesterdayReverse Engineering

Troubleshooting NT_STATUS_ACCESS_DENIED from Samba on Manjaro Linux

8 November 2022 at 08:19

A few months ago, I switched my main desktop to Manjaro, and I’m glad about it. Manjaro Linux is a polished and well-designed Linux distribution. As I like simplicity and a minimalistic approach, I chose the XFCE Desktop edition. Switching to Linux did not make me abandon the Windows platform completely. I spend lots of my work and hobby time on this OS. But I run it in QEMU-KVM VMs, configured through the Virtual Manager. As I experiment with various system settings, I have a base VM image and clone it when necessary for new projects/research. Thanks to this configuration, I finally stopped breaking my main system 🙂 One thing I needed to figure out was a way to share files between my Linux host and Windows VMs. I picked Samba as I wanted something which would look native in Windows. And here my troubleshooting story begins 🙂 I could summarize it in one sentence: “always check the system journald log,” but if you’re interested in a more extended and convoluted approach, please read on 🙂

When Samba returns NT_STATUS_ACCESS_DENIED

My smb.conf file looks as follows:

[global]
   browse list = yes
   config backend = file
   debug pid = yes
   debug timestamp = yes
   debug uid = yes
   dns proxy = no
   follow symlinks = no
   guest account = nobody
   load printers = no
   log file = /var/log/samba/%m.log
   log level = 2
   logging = systemd file
   map to guest = Bad User
   max log size = 1000
   name resolve order = lmhosts bcast host wins
   passdb backend = tdbsam
   security = user
   server role = standalone server
   usershare path = /var/lib/samba/usershare
   usershare allow guests = yes
   usershare max shares = 100
   usershare owner only = yes
   workgroup = WORKGROUP

[homes]
   browseable = no
   comment = Home Directories
   create mask = 0660
   directory mask = 0770
   guest ok = no
   read only = no
   valid users = %S

[winshare]
   browseable = yes
   comment = Share directory
   guest ok = no
   path = /mnt/data/winshare
   read only = no
   force group = +winshare
   valid users = me,ssolnica
  
[symbols]
   browseable = yes
   comment = Symbols
   guest ok = no
   path = /mnt/data/symbols
   read only = no
   valid users = me

I created the Windows user (smbpasswd -a me) and enabled smb and nmb services (systemctl enable nmb && systemctl enable smb). I configured Samba in Server Standalone mode as I did not need any of the AD features (by the way, it’s incredible that you may set up the whole AD in Linux!). When I tried my shares in Windows, the \\mypc.local\me share was working fine, but \\mypc.local\winshare was returning NT_STATUS_ACCESS_DENIED. I stopped the Samba service and ran it manually with debug level set to 3 (alternatively, you could specify debug level in the smb.conf file):

# systemctl stop smb

# smbd --no-process-group --foreground -d 3 --debug-stdout

Then, I tried the share in smbclient:

$ smbclient -U me //mypc/winshare 
Password for [WORKGROUP\me]:
Try "help" to get a list of possible commands.
smb: \> ls
NT_STATUS_ACCESS_DENIED listing \*

The error reported by Samba pointed to the file system. So I restarted the service and attached strace to it. You need to make sure to trace the child processes (-f/-ff) as the primary Samba server launches a child server for each client session:

strace -p 4350 -ff -o smbd.strace

Here is some interesting content from the output file:

...
readlink("/mnt/data/winshare", 0x7ffe77011d00, 1023) = -1 EINVAL (Invalid argument)
setgroups(12, [956, 1000, 998, 991, 3, 90, 98, 1001, 962, 961, 150, 1002]) = 0
setresgid(-1, 1000, -1)                 = 0
getegid()                               = 1000
setresuid(1000, 1000, -1)               = 0
geteuid()                               = 1000
chdir("/mnt/data/winshare")             = 0
newfstatat(AT_FDCWD, ".", {st_mode=S_IFDIR|S_ISGID|0770, st_size=4096, ...}, 0) = 0
getcwd("/mnt/data/winshare", 4096)      = 19
getcwd("/mnt/data/winshare", 1024)      = 19
openat(AT_FDCWD, ".", O_RDONLY|O_NOFOLLOW|O_PATH) = 12
newfstatat(12, "", {st_mode=S_IFDIR|S_ISGID|0770, st_size=4096, ...}, AT_EMPTY_PATH) = 0
openat(12, ".", O_RDONLY|O_NOFOLLOW|O_PATH) = 26
newfstatat(26, "", {st_mode=S_IFDIR|S_ISGID|0770, st_size=4096, ...}, AT_EMPTY_PATH) = 0
newfstatat(25, "", {st_mode=S_IFREG|0600, st_size=45056, ...}, AT_EMPTY_PATH) = 0
munmap(0x7f4b82f0c000, 696)             = 0
mmap(NULL, 36864, PROT_READ|PROT_WRITE, MAP_SHARED, 25, 0x2000) = 0x7f4b82e63000
openat(AT_FDCWD, "/proc/self/fd/26", O_RDONLY|O_DIRECTORY) = -1 EACCES (Permission denied)
close(26)                               = 0
...

We can see that the Samba process switches the effective user and group to the authenticated user (me) and then performs actions on the file system. We can see in the trace that the openat syscall fails with the EACCESS error. I double-checked all file system permissions and made me the owner of the winshare folder. Still, the EACCESS error persisted. I was so confused that I even wrote a simple app to reproduce the syscalls above:

#include <iostream>
#include <array>
#include <sstream>

#include <unistd.h>
#include <grp.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, char* argv[]) {
    std::cout << "euid: " << ::geteuid() << std::endl;
    std::cout << "egid: " << ::getegid() << std::endl;

    std::array<gid_t, 12> groups {956, 1000, 998, 991, 3, 90, 98, 1001, 962, 961, 150, 1002};
    if (::setgroups(groups.size(), groups.data()) != 0) {
        std::cout << "setgroups error: " << errno << std::endl;
        return 2;
    }

    if (int err = ::setresgid(-1, 1000, -1); err != 0) {
        std::cout << "error: " << err << std::endl;
        return err;
    }

    if (int err = ::setresuid(1000, 1000, -1); err != 0) {
        std::cout << "error: " << err << std::endl;
        return err;
    }

    std::cout << "euid: " << ::geteuid() << std::endl;
    std::cout << "egid: " << ::getegid() << std::endl;

    if (int err = ::chdir("/mnt/data/winshare"); err != 0) {
        std::cout << "error: " << err << std::endl;
        return err;
    }

    std::array<char, 1024> cwd{};
    if (::getcwd(cwd.data(), cwd.size()) == nullptr) {
        std::cout << "getcwd error: " << errno << std::endl;
        return -1;
    }
    std::cout << "cwd: " << cwd.data() << std::endl;

    // strace: openat(AT_FDCWD, ".", O_RDONLY|O_NOFOLLOW|O_PATH) = 12
    if (int fd = ::openat64(AT_FDCWD, ".", O_RDONLY|O_NOFOLLOW|O_PATH); fd != -1) {
        std::cout << "Folder opened: " << fd << std::endl;

        // strace: openat(AT_FDCWD, "/proc/self/fd/26", O_RDONLY|O_DIRECTORY) = -1 EACCES (Permission denied)
        std::stringstream ss{};
        ss << "/proc/self/fd/" << fd;
        auto proc_path = ss.str();
        if (int proc_fd = ::openat64(AT_FDCWD, proc_path.c_str(), O_RDONLY|O_DIRECTORY); proc_fd != -1) {
            std:: cout << "Proc folder opened: " << proc_fd << std::endl;

            std::cin >> proc_path;

            ::close(proc_fd);
        } else {
            std::cout << "proc openat error: " << errno << std::endl;
        }

        ::close(fd);
        return 0;
    } else {
        std::cout << "openat error: " << errno << std::endl;
        return -1;
    }
}

As you may guess, there was no error when I ran it. I scratched my head, looking online for similar issues, but could find nothing. As I had a lot of pending work, I started using the \\mypc.local\me share. Samba worked fine except for two issues: it was impossible to list the browseable shares from the Windows machines, and, secondly, the initial I/O requests over Samba were often very slow. Still, the initial problem was bugging me the most.

After a few weeks, I finally found some time to give it a second try.

Filesystem security checks are not the only ones

I again struggled with Samba config (I read the whole smb.conf man page! :)), but ended with strace. As I had my sample application working, I started comparing the process properties in the proc file system. And there, I discovered the attr folder, which stores various security-related attributes. The /proc/{pid}/attr/current file for my sample process contained unconfined while for the smbd process, its content was smbd (enforce). After searching through manual pages and Arch Linux wiki, I found that those settings come from the AppArmor module. The aa-status command only confirmed that:

# aa-status
apparmor module is loaded.
80 profiles are loaded.
77 profiles are in enforce mode.
...
   samba-dcerpcd
   samba-rpcd
   samba-rpcd-classic
   samba-rpcd-spoolss
   smbd
...
9 processes are in enforce mode.
   /usr/bin/avahi-daemon (1479) avahi-daemon
   /usr/bin/avahi-daemon (1489) avahi-daemon
   /usr/bin/dnsmasq (1698) dnsmasq
   /usr/bin/dnsmasq (1699) dnsmasq
   /usr/bin/nmbd (1778) nmbd
   /usr/bin/smbd (1785) smbd
   /usr/bin/smbd (1787) smbd
   /usr/bin/smbd (1788) smbd
   /usr/bin/smbd (5225) smbd
...

Now, I needed to locate the problematic AppArmor profiles. But how to find their names? Obviously, in the system journal! I should have checked it in the very beginning. I was studying the smb unit logs while all the details were at my fingertips:

# journalctl -fx
...
lis 06 12:19:14 mypc audit[5535]: AVC apparmor="DENIED" operation="open" profile="smbd" name="/mnt/data/winshare/" pid=5535 comm="smbd" requested_mask="r" denied_mask="r" fsuid=1000 ouid=1000
...

The smbd profile, defined in /etc/apparmor.d/usr.sbin.smbd, denies access to my target folder. Let’s have a look at it (I left only the essential parts):

abi <abi/3.0>,

include <tunables/global>

profile smbd /usr/{bin,sbin}/smbd {
  ...

  /etc/mtab r,
  /etc/netgroup r,
  /etc/printcap r,
  /etc/samba/* rwk,
  @{PROC}/@{pid}/mounts r,
  @{PROC}/sys/kernel/core_pattern r,
  /usr/lib*/samba/vfs/*.so mr,
  /usr/lib*/samba/auth/*.so mr,
  /usr/lib*/samba/charset/*.so mr,
  /usr/lib*/samba/gensec/*.so mr,
  /usr/lib*/samba/pdb/*.so mr,
  /usr/lib*/samba/{,samba/}samba-bgqd Px -> samba-bgqd,
  /usr/lib*/samba/{,samba/}samba-dcerpcd Px -> samba-dcerpcd,
  /usr/lib*/samba/{lowcase,upcase,valid}.dat r,
  /usr/lib/@{multiarch}/samba/*.so{,.[0-9]*} mr,
  /usr/lib/@{multiarch}/samba/**/ r,
  /usr/lib/@{multiarch}/samba/**/*.so{,.[0-9]*} mr,
  /usr/share/samba/** r,
  /usr/{bin,sbin}/smbd mr,
  /usr/{bin,sbin}/smbldap-useradd Px,
  /var/cache/samba/** rwk,
  /var/{cache,lib}/samba/printing/printers.tdb mrw,
  /var/lib/samba/** rwk,
  /var/lib/sss/pubconf/kdcinfo.* r,
  @{run}/dbus/system_bus_socket rw,
  @{run}/smbd.pid rwk,
  @{run}/samba/** rk,
  @{run}/samba/ncalrpc/ rw,
  @{run}/samba/ncalrpc/** rw,
  @{run}/samba/smbd.pid rw,
  /var/spool/samba/** rw,

  @{HOMEDIRS}/** lrwk,
  /var/lib/samba/usershares/{,**} lrwk,

  # Permissions for all configured shares (file autogenerated by
  # update-apparmor-samba-profile on service startup on Debian and openSUSE)
  include if exists <samba/smbd-shares>
  include if exists <local/usr.sbin.smbd-shares>

  # Site-specific additions and overrides. See local/README for details.
  include if exists <local/usr.sbin.smbd>

Now, all is clear. AppArmor adds MAC (Mandatory Access Control) to the Samba process and interferes with the file system access checks. My share path (/mnt/data/winshare) was not in the AppArmor profile; thus, access was denied. I believe that Debian and openSUSE users might not experience this problem thanks to the update-apparmor-samba-profile script, but I haven’t had a chance to check it. Anyway, the solution for me was to create /etc/apparmor.d/local/usr.sbin.smbd-shares with the missing access rights (I will have more shares from the data drive, so I just gave access to /mnt/data).

While testing my shares with the system journal monitored, I discovered some more rules missing in the default AppArmor profiles. And I found that I wasn’t the only one with this problem. Inglebard reported a very similar issue and provided updates to the rules that worked for him. I added a comment with my findings. Finally, below are the updates that fixed all my problems with Samba.

$ cat /etc/apparmor.d/local/usr.sbin.smbd-shares
/mnt/data/** lrwk,
$ cat /etc/apparmor.d/local/samba-dcerpcd
# Site-specific additions and overrides for 'samba-dcerpcd'

@{run}/samba-dcerpcd.pid lrwk,

/var/cache/samba/** rwk,

@{HOMEDIRS}/** lrwk,
/var/lib/samba/usershares/{,**} lrwk,

include if exists <samba/smbd-shares>
include if exists <usr.sbin.smbd-shares>
$ cat /etc/apparmor.d/local/samba-rpcd
# Site-specific additions and overrides for 'samba-rpcd'

/var/cache/samba/** rwk,

@{HOMEDIRS}/** lrwk,
/var/lib/samba/usershares/{,**} lrwk,

include if exists <samba/smbd-shares>
include if exists <usr.sbin.smbd-shares>
$ cat /etc/apparmor.d/local/samba-rpcd-classic
# Site-specific additions and overrides for 'samba-rpcd-classic'

/var/cache/samba/** rwk,
/dev/urandom rwk,

@{HOMEDIRS}/** lrwk,
/var/lib/samba/usershares/{,**} lrwk,

include if exists <samba/smbd-shares>
include if exists <usr.sbin.smbd-shares>

Generating C# bindings for native Windows libraries 

23 November 2023 at 07:15

When writing system applications in C# we often need to interact with the system APIs directly. And it has always been a challenge to write proper PInvoke signatures. However, with the introduction of the Windows metadata project and later, cswin32, things changed significantly. In this post, I will walk you through the steps required to generate C# bindings for a sample native library. I picked Detours, because I needed it for withdll, my new tool inspired by the withdll example from the Detours repository. The post by Rafael Rivera describing how to create bindings for Rust language helped me tremendously in writing this post (and bindings 😊). 

Creating Windows metadata 

Preparing the metadata project 

Before we could see the generated C# code, we need to build a Windows metadata (winmd) file. Rafael describes the steps in details, so I will take a shortcut here and show you the generate.proj for the detours library: 

<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.Windows.WinmdGenerator/0.55.45-preview">
    <PropertyGroup Label="Globals">
        <OutputWinmd>winmd/detours.winmd</OutputWinmd>
        <WinmdVersion>0.1.0.0</WinmdVersion>
    </PropertyGroup>

    <ItemGroup>
        <Headers Include="../detours/include/detours.h" />

        <ImportLibs Include="../detours-dll/bin.x64$(BuildConfig)/detours.lib">
            <StaticLibs>detours=detours</StaticLibs>
        </ImportLibs>

        <Partition Include="main.cpp">
            <TraverseFiles>@(Headers)</TraverseFiles>
            <Namespace>Microsoft.Detours</Namespace>
            <ExcludeFromCrossarch>true</ExcludeFromCrossarch>
        </Partition> 
    </ItemGroup>
</Project>

My folder structure looks as follows: 

I also needed to add the ImportLibs item as my library is not in the folders searched normally by the MSVC compiler. Additionally, the output of the detours build is a static library (detours.lib) that we can link with our project. Theoretically, we can point Windows Metadata Generator to static libraries using the StaticLibs tag. However, I did not manage to make it work without creating an additional shared library. There is an old issue in the win32metadata project about importing static libraries directly, but it was never resolved. I noticed though that the generated methods have StaticLibraryAttribute attached to them. Still, I’m unsure what its purpose is. 

Building a wrapping DLL for the static library

Fortunately, creating a shared library for a static library is a straightforward process. You need a cpp file, for example: 

#include <windows.h>

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved) {
    UNREFERENCED_PARAMETER(lpReserved);

    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        ::DisableThreadLibraryCalls(hModule);
        break;
    case DLL_PROCESS_DETACH:
        break;
    default:
        break;
    }
    return TRUE;
}

And a module-definition file (detours.def) that will list the exported methods, for example: 

LIBRARY detours
EXPORTS
	DetourCreateProcessWithDllExW
	DetourCreateProcessWithDllsW

Then you need to compile your DLL, for example: 

cl.exe /I "..\detours\include" /nologo /LD /TP /DUNICODE /DWIN32 /D_WINDOWS /EHsc /W4 /WX /Zi /O2 /Ob1 /DNDEBUG -std:c++latest detours.cpp /link /DEF:detours.def ..\detours\lib.X64\detours.lib

Now, we may point ImportLibs to the detours-dll folder and try to build the detours.winmd file.

Building the metadata project 

This step should be as straightforward as running the dotnet build command. Sometimes, however, you may run into problems with the parsers. For detours, for example, I needed to remove a section from the detours header. Finally, the build was successful and I could verify in ILSpy that the detours.winmd file contains all the methods exported by my detours.dll: 

Generating and using bindings from the metadata project 

With the metadata file ready, it’s time to use it in our C# project. Firstly, we will install the cswin32 package that imports Win32 metadata and allows us to define which types and methods we want to import through the NativeMethods.txt file. Cswin32 by default understands only names defined in the Win32 metadata project. However, thanks to the ProjectionMetadataWinmd tag we can easily make it process our custom metadata files as well! 

<ItemGroup>
    <ProjectionMetadataWinmd Include="../detours-meta/winmd/*.winmd" />
</ItemGroup>

Now, we may reference the names that we want to import in the NativeMethods.txt and, finally, use it in our code: 

// NativeMethods.txt

// Windows
CloseHandle
DebugActiveProcessStop
// ...

// Detours
DetourCreateProcessWithDllsW
// ...
// An example C# code using the DetourCreateProcessWithDlls function

using PInvokeDetours = Microsoft.Detours.PInvoke;
using PInvokeWin32 = Windows.Win32.PInvoke;

var pcstrs = dllPaths.Select(path => new PCSTR((byte*)Marshal.StringToHGlobalAnsi(path))).ToArray();
try
{
    if (!PInvokeDetours.DetourCreateProcessWithDlls(null, ref cmdline, null, null, false,
        createFlags, null, null, startupInfo, out var processInfo,
        pcstrs, null))
    {
        throw new Win32Exception();
    }

    PInvokeWin32.CloseHandle(processInfo.hThread);
    PInvokeWin32.CloseHandle(processInfo.hProcess);

    if (debug)
    {
        PInvokeWin32.DebugActiveProcessStop(processInfo.dwProcessId);
    }
}
finally
{
    Array.ForEach(pcstrs, pcstr => Marshal.FreeHGlobal((nint)pcstr.Value));
}

The NativeAOT compilation gives us also an option to statically link the detours library:

<ItemGroup>    
    <DirectPInvoke Include="detours" />
    <NativeLibrary Include="../detours/lib.X64/detours.lib" />
</ItemGroup>

It’s fantastic that we can have all the native imports in one place. If you would like to examine a working project that uses the presented detours bindings, please check the withdll repository. You may use it to, for example, inject detours DLLs that hook Win API functions and trace them. I describe this usage scenario in a guide at wtrace.net

Updating PE file imports on process start

18 December 2023 at 05:50

When we need to change the PE file imports, we might either modify the binary file in the file system or perform updates after it has been loaded to the memory. In this post, I will focus on the latter approach, showing you moments in the process lifetime when such changes are possible. We will end up with a small app capable of updating imports in newly started remote processes.

What we will be modifying

Let’s begin with some basics on the PE file structure. Typically, the data about PE file imports resides in the .idata section. And we need to read the image import directory (IMAGE_DIRECTORY_ENTRY_IMPORT) in the NT Optional Header to understand how this data is laid out. In this directory, we will find an array of IMAGE_IMPORT_DESCRIPTOR structures:

typedef struct _IMAGE_IMPORT_DESCRIPTOR { 
    union { 
        DWORD   Characteristics;            // 0 for terminating null import descriptor 
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA) 
    } DUMMYUNIONNAME; 
    DWORD   TimeDateStamp;                  // 0 if not bound, 
                                            // -1 if bound, and real date\time stamp 
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) 
                                            // O.W. date/time stamp of DLL bound to (Old BIND) 
    DWORD   ForwarderChain;                 // -1 if no forwarders 
    DWORD   Name; 
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses) 
} IMAGE_IMPORT_DESCRIPTOR;

The Name field points to the name of the imported DLL and the OriginalFirstThunk and FirstThunk fields point to arrays of IMAGE_THUNK_DATA, which hold information about the functions imported from a given library. All those fields’ values are relative virtual addresses (RVAs), so offsets to the image base address after it has been loaded into memory. Additionally, a thunk could represent either an import by ordinal (IMAGE_ORDINAL_FLAG is set) or an import by name (the thunk holds an RVA to the IMAGE_IMPORT_BY_NAME structure). It is important to note that each thunk array must end with a zeroed thunk. You may be wondering why there are two thunk arrays per each DLL. At the beginning, they hold the same values, but once the imports are resolved, the loader will overwrite values in the FirstThunk array with actual addresses of the resolved functions. The thunks for all the imports are usually the first bytes of the .idata section and they are also referenced by the import address table (IAT) directory (I highly recommend downloading PE 102 by @corkami – a beautiful and very readable diagram of the PE file format).

Depending on what we want to achieve, we may either modify only the resolved thunk arrays or the whole descriptors array. For example, redirecting one function to another when both functions belong to already loaded DLLs could be achieved by simply overwriting the corresponding resolved address in a thunk array. However, injecting a new DLL to the process or adding a new function import to an existing DLL requires changes to the descriptors array. And that’s the case on which I will focus mainly in this post.

The application we are going to develop, named importando, accepts a list of tasks (-i)  to perform on the remote process image:

  • -i test.dll!TestMethod or test.dll#1 to inject a function by name or by ordinal 
  • -i test.dll!TestMethod:test2.dll!TestMethod to replace a given imported function with a different one 

Let’s now examine what are our options to implement those tasks.

Updating a suspended process

A common approach is to start the process as suspended (CREATE_SUSPENDED flag) and modify the imports table before the first thread resumes execution. Unfortunately, when the CreateProcess function returns, the loader has already resolved the imported function addresses (ntdll!LdrStateInit equals 2). Therefore, this approach will not work if we need to fix an incorrect import definition (for example, a wrong DLL or function name). However, as the loader has not yet reached completion (LdrStateInit is not 3), we still may perform some actions on the import directory. For example, we can inject a new DLL into a process (like DetoursCreateProcessWithDlls). We may also override addresses of the resolved functions. When the main thread resumes (ResumeThread), the loader will finish its work and the code will execute with our changes applied.

Updating a process from a debugger

If we need to have earlier access to the executable import directory data, we could resort to the debugger API. Running process under a debugger gives us a few more chances to apply import modifications. The first interesting debug event is CREATE_PROCESS_DEBUG_EVENT. When the debugger receives it, the loader has not yet started resolving the dependencies, but the executable image is already loaded into the memory. That is a perfect moment for fixing problems that are causing critical loader errors, for example, an infamous “entry not found” error:

The next interesting event is EXCEPTION_DEBUG_EVENT with ExceptionRecord.ExceptionCode equal to STATUS_BREAKPOINT (if we are debugging a 32-bit process with a 64-bit debugger, we should skip the first STATUS_BREAKPOINT and instead wait for STATUS_WX86_BREAKPOINT). It is the initial process breakpoint, triggered by the loader when it is in a state very similar to the one in an initially suspended process, described in the previous section (so LdrStateInit equals 2). Finally, the debugger also receives LOAD_DLL_DEBUG_EVENT for each loaded DLL before the loader started resolving its dependencies. Thus, in the handler of this event, we could fix issues in the import directories of the dependent libraries.

I also recorded a YouTube video where I present how you may make those fixes manually in WinDbg. It could be helpful to better visualize the steps we will perform in the importando code.

Implementing importando (in C#)

As you remember from the first section, our goal is to support both import redirects and new import injections. If you are wondering why importando, I thought it sounds nice and the name describes what we will be doing: import and override (it happens in the reverse order, but it is just a nitpick 😊). As we want to support all types of modifications to the import directory, the logical choice is to use the debugging API. Thanks to CsWin32, writing a native debugger in C# is not a very demanding task. Here is the debugger loop with the few events importando uses:

HANDLE processHandle = HANDLE.Null;
nuint imageBase = 0;
bool is64bit = false;
bool isWow64 = false;

ModuleImport[] originalImports = [];
ModuleImport[] newImports = [];

while (!cts.Token.IsCancellationRequested)
{
    if (WaitForDebugEvent(1000) is { } debugEvent)
    {
        switch (debugEvent.dwDebugEventCode)
        {
            case DEBUG_EVENT_CODE.CREATE_PROCESS_DEBUG_EVENT:
                {
                    logger.WriteLine($"CreateProcess: {debugEvent.dwProcessId}");

                    Debug.Assert(pid == debugEvent.dwProcessId);
                    var createProcessInfo = debugEvent.u.CreateProcessInfo;

                    // we are closing hFile handle after we finish reading the image data
                    using var pereader = new PEReader(new FileStream(
                        new SafeFileHandle(createProcessInfo.hFile, true), FileAccess.Read));

                    processHandle = createProcessInfo.hProcess;
                    is64bit = pereader.Is64Bit();
                    isWow64 = Environment.Is64BitProcess && !is64bit;
                    unsafe { imageBase = (nuint)createProcessInfo.lpBaseOfImage; }

                    (originalImports, newImports) = UpdateProcessImports(processHandle,
                        pereader, imageBase, importUpdates, forwards);
                }
                break;

            case DEBUG_EVENT_CODE.EXCEPTION_DEBUG_EVENT:
                if (debugEvent.u.Exception.ExceptionRecord.ExceptionCode == (
                    isWow64 ? NTSTATUS.STATUS_WX86_BREAKPOINT : NTSTATUS.STATUS_BREAKPOINT))
                {
                    // first breakpoint exception is the process breakpoint - it happens when loader finished its initial
                    // work and thunks are resolved
                    Debug.Assert(imageBase != 0 && !processHandle.IsNull);
                    UpdateForwardedImports(processHandle, is64bit, imageBase, originalImports, newImports, forwards);
                    cts.Cancel();
                }
                else
                {
                    logger.WriteLine($"Unexpected exception: {debugEvent.u.Exception.ExceptionRecord.ExceptionCode.Value:x}");
                }
                break;

            case DEBUG_EVENT_CODE.EXIT_PROCESS_DEBUG_EVENT:
                cts.Cancel();
                break;
            default:
                break;
        }

        if (!PInvoke.ContinueDebugEvent(debugEvent.dwProcessId,
            debugEvent.dwThreadId, NTSTATUS.DBG_EXCEPTION_NOT_HANDLED))
        {
            throw new Win32Exception(Marshal.GetLastPInvokeError(), $"{nameof(PInvoke.ContinueDebugEvent)} error");
        }
    }
}

I will mention that again later, but the full source code is available in the importando GitHub repository. In the post, I will rather focus on the crucial pieces of the solution, so please refer to the code in the repository in case you would like to check the skipped parts.

I also created a few wrapping record classes for the parsed import data. Using native structures could be an option, however, I wanted to make them more C# friendly and also architecture agnostic.

interface IFunctionImport { }

record FunctionImportByName(uint Rva, ushort Hint, string FunctionName) : IFunctionImport;

record FunctionImportByOrdinal(uint Ordinal) : IFunctionImport;

record NullImport : IFunctionImport;

record FunctionThunk(IFunctionImport Import);

record ModuleImport(string DllName, uint DllNameRva, uint OriginalFirstThunkRva,
    uint FirstThunkRva, FunctionThunk[] FirstThunks)

The handler of CREATE_PROCESS_DEBUG_EVENT, or rather the UpdateProcessImports function, reads the existing imports (PEImports.ReadModuleImports), prepares new import descriptors with thunk arrays for the updated ones (PEImports.PrepareNewModuleImports), and saves them in the remote process memory (PEImports.UpdateImportsDirectory). Btw., the PEReader class is a great helper in parsing PE structures. We also need to update the imports data directory in the NT optional header as it should point to our new import descriptors (UpdatePEDirectory):

static (ModuleImport[] OriginalImports, ModuleImport[] NewImports) UpdateProcessImports(HANDLE processHandle,
    PEReader imageReader, nuint imageBase, ImportUpdate[] importUpdates, (string ForwardFrom, string ForwardTo)[] forwards)
{
    var existingImports = PEImports.ReadModuleImports(imageReader);

    var newImports = PEImports.PrepareNewModuleImports(existingImports, importUpdates, forwards);

    var is64bit = imageReader.Is64Bit();
    var (importDirRva, importDirSize) = PEImports.UpdateImportsDirectory(processHandle, is64bit, imageBase, newImports);

    nuint dataDirectoriesRva = (nuint)(imageReader.PEHeaders.PEHeaderStartOffset +
        (is64bit ? Marshal.OffsetOf<IMAGE_OPTIONAL_HEADER64>("DataDirectory") : Marshal.OffsetOf<IMAGE_OPTIONAL_HEADER32>("DataDirectory")));

    UpdatePEDirectory(dataDirectoriesRva, IMAGE_DIRECTORY_ENTRY.IMAGE_DIRECTORY_ENTRY_IMPORT, importDirRva, importDirSize);
    UpdatePEDirectory(dataDirectoriesRva, IMAGE_DIRECTORY_ENTRY.IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT, 0, 0);

    return (existingImports, newImports);
}

Because most of the RVA addresses in PE file header are 4-byte long (DWORD), the PEImports.UpdateImportsDirectory needs to allocate space in the memory for the new imports as near to the image base address as possible. I ported to C# the FindAndAllocateNearBase function from the Detours library to achieve that.

To better show you what importando is doing, I draw a simple picture of the memory layout after an example forward of the shell32.dll!StrCpyNW to shlwapi.dll!StrCpyNW. Notice a new import descriptors table (separate from the original one) that is pointed by the import directory. Importando also needed to create new thunk arrays for imports requiring an update (in this case, shell32.dll) and for new ones (shlwapi.dll), but reused existing thunks for unmodified imports (user32.dll):

Once we updated the import directories, we are ready to resume the process execution and wait for the loader to perform its initial work.

This leads us to the next stop in the debugger loop, the EXCEPTION_DEBUG_EVENT handler. The name of the event may be a little misleading as it is triggered not only when code in the remote process throws an exception, but also when it hits a breakpoint. And Windows loader triggers a breakpoint (STATUS_BREAKPOINT) when it detects that there is a debugger attached to the starting process. In the WOW64 context (when a 64-bit debugger debugs a 32-bit application), there are actually two initial breakpoints, STATUS_BREAKPOINT and STATUS_WX86_BREAKPOINT, and it is the latter that interests us. At this point, the loader resolved all the addresses in the thunk arrays from the new imports directory. However, we are not done yet as the old thunks still hold RVA (unresolved) addresses. We need to update them as those thunks are referenced by the application code. And here comes the last step in our coding journey, the UpdateForwardedImports function:

static void UpdateForwardedImports(HANDLE processHandle, bool is64bit, nuint imageBase,
    ModuleImport[] originalImports, ModuleImport[] newImports, (string ForwardFrom, string ForwardTo)[] forwards)
{
    int thunkSize = is64bit ? Marshal.SizeOf<IMAGE_THUNK_DATA64>() : Marshal.SizeOf<IMAGE_THUNK_DATA32>();

    uint GetThunkRva(ModuleImport[] moduleImports, string importName)
    { /* ... */  }

    void CopyThunkValues(uint fromRva, uint toRva)
    { /* ... */  }

    foreach ((string forwardFrom, string forwardTo) in forwards)
    {
        var originalThunkRva = GetThunkRva(originalImports, forwardFrom);
        var newThunkRva = GetThunkRva(newImports, forwardTo);

        if (originalThunkRva != 0 && newThunkRva != 0)
        {
            // new thunk should be resolved by now, so we may copy its value to the original place
            // that could be referenced by application code
            CopyThunkValues(newThunkRva, originalThunkRva);
        }
        else
        {
            Console.WriteLine($"WARNING: could not find import {forwardFrom} or {forwardTo}");
        }
    }
}

We may now continue debugging or detach from the remote process (that’s what importando is doing) and let it freely run. Our job is done and we should see new imports in the modules list of our target application.

The importando’s source code and binaries are available in its GitHub repository.

❌
❌