The Basics of DLL Injection Part I

Hello... sorry for not posting in a while.  I've been going through some recent career changes and haven't really come across any IT-related inspiration lately.  I thought I'd post a simple, bare-bones technique of injecting a DLL into a running process on a Windows computer.  It's really simple.

  1. Verify that the PID supplied is valid.
  2. Get a handle to the remote process.
  3. Allocate memory in the remote process.
  4. Write the DLL path into the remote process's memory.
  5. Call CreateRemoteThread to induce LoadLibrary in the remote process. 

As long as the process does not already have the DLL loaded, the remote process will automatically call DllMain (the injected DLL's entry point,) which can contain all sorts of fun code.  For instance, now that you have injected a DLL into the remote process, you are well on your way to performing something like API hooking, for example. You could modify the Import Address Table of the remote process... but I'm getting ahead of myself.  First things first.  Without further ado:

// InjectDLL.cpp
// Joseph Ryan Ries - 2015
// Injects a DLL into another process on the system.

#include 
#include 
#include 

// Returns true if the supplied file path exists and is a file.
// Returns false if the supplied path does not exist, or is a directory.
BOOL FileExists(LPCTSTR FilePath)
{
	DWORD FileAttributes = GetFileAttributes(FilePath);

	return (FileAttributes != INVALID_FILE_ATTRIBUTES && !(FileAttributes & FILE_ATTRIBUTE_DIRECTORY));
}

// Entry point. Returns 0 at the end if everything was successful.
// Returns 1 if something failed. Outputs messages to console.
int wmain(int argc, wchar_t *argv[])
{
	if (argc != 3)
	{
		wprintf_s(L"\nUsage: %s C:\\Temp\\MyDLL.dll 1337\n", argv[0]);
		wprintf_s(L"\nUse the full path to the DLL you want to inject and supply the\n");
		wprintf_s(L"process ID (PID) of the process you want to inject it in to.\n");
		return(1);
	}

	if (!FileExists(argv[1]))
	{
		wprintf_s(L"\nERROR: Unable to find file %s.\n", argv[1]);
		return(1);
	}

	wchar_t DLLPath[MAX_PATH] = { 0 };

	wcscpy_s(DLLPath, argv[1]);

	DWORD Pid = _wtoi(argv[2]);

	if (Pid == 0)
	{
		wprintf_s(L"\nERROR: Unable to interpret the supplied process ID.\n");
		return(1);
	}

	wprintf_s(L"Searching for PID %d.\n", Pid);

	DWORD CurrentProcesses[2048] = { 0 };
	DWORD ProcessListSize = 0;
	DWORD ProcessCount = 0;

	if (EnumProcesses(CurrentProcesses, sizeof(CurrentProcesses), &ProcessListSize) == 0)
	{
		wprintf_s(L"\nERROR: Unable to enumerate currently running processes!\nLastError: 0x%x.\n", GetLastError());
		return(1);
	}

	ProcessCount = ProcessListSize / sizeof(DWORD);

	wprintf_s(L"%d processes currently on the system.\n", ProcessCount - 1);

	BOOL ProcessFound = FALSE;

	for (unsigned int ProcessCounter = 0; ProcessCounter < ProcessCount; ProcessCounter++)
	{
		if (CurrentProcesses[ProcessCounter] == 0)
		{
			continue;
		}
		if (CurrentProcesses[ProcessCounter] == Pid)
		{
			ProcessFound = TRUE;
			break;
		}
	}

	if (ProcessFound == FALSE)
	{
		wprintf_s(L"\nERROR: A process with PID %d could not be found.\n", Pid);
		return(1);
	}

	HANDLE ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);

	if (ProcessHandle == NULL)
	{
		wprintf_s(L"\nERROR: Unable to open handle to process %d. LastError 0x%x.\n", Pid, GetLastError());
		return(1);
	}

	wprintf_s(L"Process with PID %d successfully opened.\n", Pid);

	void* RemoteDLLPathMemory = NULL;		// The DLL entry address in the remote process

	// VirtualAlloc will probably not give us less than 4k. Whatever. We only need ~520 bytes for MAX_PATH * 2.
	RemoteDLLPathMemory = VirtualAllocEx(ProcessHandle, NULL, (MAX_PATH * sizeof(wchar_t)), MEM_COMMIT, PAGE_READWRITE);

	if (RemoteDLLPathMemory == NULL)
	{
		wprintf_s(L"\nERROR: Unable to allocate memory in remote process %d. LastError: 0x%x.\n", Pid, GetLastError());
		return(1);
	}

	wprintf_s(L"Memory allocated in process %d.\n", Pid);

	SIZE_T BytesWritten = 0;

	if (WriteProcessMemory(ProcessHandle, RemoteDLLPathMemory, (void*)DLLPath, sizeof(DLLPath), &BytesWritten) == 0)
	{
		wprintf_s(L"\nERROR: Unable to write DLL path into remote process memory. LastError: 0x%x.\n", GetLastError());
		return(1);
	}

	wprintf_s(L"%I64d bytes written into the memory of process %d.\n", BytesWritten, Pid);
	
	HANDLE ThreadHandle = CreateRemoteThread(
		ProcessHandle,
		NULL,
		NULL,
		(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW"),
		RemoteDLLPathMemory,
		NULL,
		NULL);

	if (ThreadHandle == NULL)
	{
		wprintf_s(L"\nERROR: Unable to start remote thread in process %d. LastError: 0x%x.\n", Pid, GetLastError());
		return(1);
	}

	wprintf_s(L"Successfully loaded %s into process %d.\n", DLLPath, Pid);

	wprintf_s(L"Note: DllMain will only run if the DLL has not already been loaded by the process.\n");
	
	return(0);
}

Ignore the very last line (</psapi.h></windows.h></stdio.h>), the code editor wigged out.

Keeping My Sysinternals Tools Up To Date

Microsoft Sysinternals (primarily Mark Russinovich, but with occasional help from his buddies like Andrew Richards, Thomas Garnier, etc.,) has put out a ton of very useful Windows tools and utilities over the years. If you spend any time at all on Windows computers, you undoubtedly know and love these tools.

The tools are updated quite regularly, too.  And I hate having out of date stuff.  So, I wrote a little bit of Powershell this morning that will automatically scan my local folder to see which Sysinternals tools I have in that folder (like C:\Program Files\Sysinternals\,) then compare them with the latest and greatest versions of the executables on the internet (it's WebDAV,) and update my local copy if necessary.  I just run this script as a scheduled task once a week.

Set-StrictMode -Version Latest

[String]$EventSource    = 'UpdateSysinternals'
[String]$LocalDirectory = 'C:\Program Files\SysInternals\*.exe'
[String]$WebDAVShare    = '\\live.sysinternals.com\Tools'
[Int]$FilesUpdated      = 0

If (-Not([System.Diagnostics.EventLog]::SourceExists($EventSource)))
{
    Try
    {
        New-EventLog -LogName Application -Source $EventSource -ErrorAction Stop
        # MSDN says you shouldn't immediately use a new source after creating it.
        Start-Sleep -Seconds 10
        Write-EventLog -LogName Application -Source $EventSource -EntryType Information ` 
             -EventId 1 -Message 'Event source created.'
    }
    Catch
    {
        Exit
    }
}

$LocalFiles = Get-ChildItem $LocalDirectory

Try
{
    New-PSDrive -Name S -PSProvider FileSystem `
           -Root $WebDAVShare -ErrorAction Stop | Out-Null
}
Catch
{
    Write-EventLog -LogName Application -Source $EventSource -EntryType Error `
          -EventId 2 -Message "Failed to connect to $WebDAVShare!"
    Exit
}

Foreach ($LocalFile In $LocalFiles)
{    
    $RemoteFile = Get-ChildItem "S:\$($LocalFile.Name)"
    If ([Version]$RemoteFile.VersionInfo.ProductVersion -NE [Version]$LocalFile.VersionInfo.ProductVersion)
    {
        Write-EventLog -LogName Application -Source $EventSource -EntryType Information `
             -EventId 3 -Message "Remote file $($RemoteFile.Name) has version $($RemoteFile.VersionInfo.ProductVersion), which does not match local version $($LocalFile.VersionInfo.ProductVersion)."
        Copy-Item $RemoteFile.FullName $LocalFile.FullName -Force
        $FilesUpdated++
    }
}

Remove-PSDrive S

Write-EventLog -LogName Application -Source $EventSource -EntryType Information `
     -EventId 4 -Message "$FilesUpdated file(s) were updated."

UNC Hardening

A couple months ago, Microsoft published a couple of Windows patches to address some vulnerabilities found in the way that Windows machines access UNC paths over the network.

MS15-011

MS15-014

Guidance on Deployment of MS15-011 and MS15-014 by AskPFE Platforms

This is essentially another man-in-the-middle style SMB hijack, and these types of attacks have been well-known for a long time, maybe second only behind pass the hash stuff.  One of the countermeasures that we admins have had for years to help combat these sorts of SMB proxy attacks, is SMB signing:

Of course I'd recommend enabling this everywhere - on both domain controllers and domain members - but that's no longer quite enough.  Security researchers found a way of bypassing or disabling SMB signing, which is what prompted Microsoft to release those two security patches I mentioned above.  One of those hotfixes comes with a new Group Policy configuration setting, called UNC Hardening.

You can find this new setting in Computer Configuration > Policies > Administrative Templates > Network > Network Provider:

So keep in mind that just applying the patch alone doesn't award you any of the benefits of Hardened UNC Paths.  There is additional GPO configuration you must do to enable it.

In the GPO, an admin would specify the types of UNCs that he or she wanted to harden, so that when a client connects to a UNC that matches a certain pattern, that client applies additional security policies to that connection.

Wildcards are supported, but you must supply either a server name or share name, so no, you cannot do \\* or \\*\*.

To get the two most important UNC paths in an Active Directory domain, you'd configure the GPO thusly:

\\*\NETLOGON  RequireMutualAuthentication=1, RequireIntegrity=1
\\*\SYSVOL    RequireMutualAuthentication=1, RequireIntegrity=1

This additional layer of security costs very little, relative to the benefit of ensuring all your Windows clients will only connect to genuine, mutually authenticated domain controllers to get their Group Policies and logon scripts.  Especially if you have mobile clients on the go that connect from coffee shops and hotels!

ImAlive - RDP Activity Simulator

Sometimes I use Remote Desktop Protocol (RDP) to connect to machines remotely.  Good ole' mstsc.exe.  Sometimes, the remote server that I connect to has a policy that automatically disconnects idle sessions after some minutes.  This can be excruciatingly inconvenient when, for instance, I'm trying to transfer a large file to or from the server.  The file transfer may take hours, but I have to sit there and babysit the RDP session to make sure that the server doesn't disconnect me?

So that's why I made ImAlive.

Basically, this tiny program will send a "heartbeat" to all open RDP windows on your system every 10 seconds.  This keeps your RDP session in an "Active" state, even if you walk away from the keyboard for hours.  You run this program locally on your workstation, not on the remote machine.

Just launch the executable, and you will see this:

Press any key to terminate the program.  It will automatically locate all open RDP windows on your desktop, and send a "heartbeat" to them, thus keeping your session "Active," indefinitely.

Caveats and Limitations:

  • You mustn't minimize your RDP windows.  You can keep them in the background, but don't minimize them. If you minimize the RDP windows, they will not be able to receive the heartbeat messages.  It doesn't matter if the RDP session is full screen or windowed.
  • You can still use your computer to do other things while your RDP sessions are in the background, but you may see your RDP windows flash into the foreground for an instant as the heartbeats are sent to them.  After the heartbeat is sent, the program attempts to return focus to whatever window had focus before the heartbeat was sent.  If you have multiple monitors, I recommend shoving all your "idle" RDP windows to the side where they don't bother you if they briefly (like, 10 milliseconds, you may not even notice) flash to the foreground.
  • The program stops working if your workstation is locked or goes to sleep or hibernates.

If at any time I eliminate any of these bugs/limitations, I will update the program and post the updates to this page.

Download:

ImAlive.zip (90.6KB)

Local Admin Password Maintainer

Active Directory is great for robust, centralized management of a large amount of I.T. assets.  But even once you have Active Directory, you're still left with that problem of what to do with local administrator accounts on all of the domain members.  You probably don't want to disable the local admin account, because you'll need it in case the computer is ever in a situation where it can't contact a domain controller.  But you don't have a good way of updating and maintaining the local Administrator password across your entire environment, either.  Everyone knows better than to use Group Policy Preferences to update the local administrator password on domain members, as it is completely unsecure.  Most other solutions involve sending the administrator passwords across the network in clear-text, require an admin to manually run some scripts or software every time that may not work well in complicated networks, and they still leave you with the same local administrator password on every machine... so if an attacker knocks over any one computer in your entire domain, he or she now has access to everything.

This is the situation Local Admin Password Maintainer seeks to alleviate.  LAPM easily integrates into your Active Directory domain and fully automates the creation of random local administrator passwords on every domain member.  The updated password is then transmitted securely to a domain controller and stored in Active Directory.  Only users who have been given the appropriate permissions (Domain Administrators and Account Operators, by default) may view any password.

The solution is comprised of two files: Install.ps1, which is the one-time install script, and LAPM.exe, an agent that will periodically (e.g., once a month,) execute on all domain members.  Please note that these two files will always be digitally signed by me.

Minimum Requirements

  • Active Directory. You need to be a member of both Domain Admins and Schema Admins to perform the install. You must perform the installation on the forest schema master.
  • Forest and domain functional levels of 2008 or better. This software relies on a feature of Active Directory (confidential attributes) that doesn't technically require any certain forest or domain functional level, but enforcing this requirement is an easy way of ensuring that all domain controllers in your forest are running a modern version of Windows.
  • I do not plan on doing any testing of either the install or the agent on Windows XP or Server 2003.  I could hypothetically make this work on XP/2003 SP1, but I don't want to.  If you're still using those operating systems, you aren't that concerned with security anyway.
  • A Public Key Infrastructure (PKI,) such as Active Directory Certificate Services, or otherwise have SSL certificates installed on your domain controllers that enable LDAP over SSL on port 636.  This is because LAPM does not allow transmission of data over the network in an unsecure manner.  It is possible to just bang out some self-signed certificates on your domain controllers, and then distribute those to your clients via Group Policy, but I do not recommend it.
  • The installer requires Powershell 4. Which means you need Powershell 4 on your schema master. Which means it needs to be 2008 R2 or greater.  I could port the install script to an older version of Powershell, but I haven't done it yet.
  • The Active Directory Powershell module. This should already be present if you've met the requirements thus far.
  • The Active Directory Web Service should be running on your DCs. This should already be present if you've met the requirements thus far.
  • LAPM.exe (the "agent") will run on anything Windows Vista/Server 2008 or better, 32 or 64 bit.  I just don't feel like porting it back to XP/2003 yet.

COPYRIGHT AND DISCLAIMER NOTICE:

Copyright ©2015 Joseph Ryan Ries. All Rights Reserved.

IN NO EVENT SHALL JOSEPH RYAN RIES (HEREINAFTER REFERRED TO AS 'THE AUTHOR') BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND/OR ITS DOCUMENTATION, EVEN IF THE AUTHOR IS ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS IS". THE AUTHOR HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.


Installation Instructions

  • Download the installation package found below, and unzip it anywhere on your Active Directory domain controller that holds the Schema Master FSMO role.  (Use the netdom query fsmo command if you forgot which DC is your Schema Master.)
  • If necessary, use the Unblock-File Powershell cmdlet or use the GUI to unblock the downloaded zip file.
  • You can verify the integrity of the downloaded files like so:


  • If you need to change your Powershell execution policy in order to run scripts on your DC, do so now with Set-ExecutionPolicy RemoteSigned.
  • Execute the Install script by typing .\Install.ps1 in the same directory as the script and LAPM.exe.

  • The installation script will perform several prerequisite checks to ensure your Active Directory forest and environment meet the criteria. It will also create a log file that stores a record of everything that takes place during this install session.  If you see any red [ERROR] text, read the error message and try to correct the problem that is preventing the install script from continuing, then try again. (E.g. SSL certificate not trusted, you're not on the Schema Master, etc.)  It's important that you read and consider the warning text, especially the part about how extending the Active Directory schema is a permanent operation.
  • Type yes at the warning prompt to commit to the installation.

  • The installation will now make a small schema modification by adding the LAPMLocalAdminPassword attribute to the Active Directory schema, adding that attribute to the computer object, and then adding an access control entry (ACE) to the root of the domain that allows the SELF principal the ability to write to that attribute.  That means that a computer has the right to modify its own LAPMLocalAdminPassword attribute, but not the attribute of another computer. (A computer does not have the ability to read its own LAPMLocalAdminPassword attribute. It is write-only.)

  • Finally, the install script copies LAPM.exe to the domain's SYSVOL share. This is so all domain members will be able to access it.
  • You are now done with the script and are in the post-installation phase.  You have one small thing left to do.
  • Open Group Policy Management on your domain controller.

  • Create a new GPO and link it to the domain:

  • Name the new GPO Local Admin Password Maintainer.
  • Right click on the new GPO and choose Edit. This will open the GPO editor.
  • Navigate to Computer Configuration > Preferences > Control Panel Settings > Scheduled Tasks.

  • Right-click in the empty area and choose New > Scheduled Task (At least Windows 7).

  • Choose these settings for the new scheduled task. It is very important that the scheduled task be run as NT Authority\System, also known as Local System.


  • This task will be triggered on the first of every month.  It's advisable to configure the random delay shown in the screenshot above, as this will mitigate the flood of new password uploads to your domain controllers on the first of the month.

  • For the program to execute, point to \\YourDomain\SYSVOL\YourDomain\LAPM.exe. Remember that the second "YourDomain" in the path is a reparse point/symlink that looks like "domain" if you view it in File Explorer.  For the optional argument, type BEGIN_MAGIC, in all capital letters.  It is case sensitive.
  • Lastly, the "Remove this item when it is no longer applied" setting is useful.  Unchecking "allow this task to be run on demand" can also be useful.  As an administrator, you have some leeway here to do what makes the most sense for your environment.  You might even choose to scope this GPO to only a certain OU if you only want a subset of the members of your domain to participate in Local Admin Account Maintainer.

  • Click OK to confirm, and you should now have a new scheduled task that will execute on all domain members.
  • Close the Group Policy editor.

Don't worry if the scheduled task also applies to domain controllers.  LAPM.exe detects whether it is running on a domain controller before it does anything, and exits if it is.


It also doesn't matter what the local administrator's name is, in case the account has been renamed. LAPM uses the SID.

LAPM logs successes and failures to the Windows Application event log.  Here is an example of what you might see if a client can't connect to a DC for some reason, like if SSL certificates aren't configured correctly:

In an event like this, LAPM.exe exits before changing the local administrator password, so the password will just stay what it was until the next time the scheduled job runs.

LAPM will generate a random, 16-character long password.  The "randomness" comes from the cryptographically secure PRNG supplied by the Windows API.

Success looks like this:


Now, notice that the standard domain user "Smacky the Frog" is unable to read the LAPMLocalAdminPassword attribute from Active Directory:

However, a Domain Administrator or Account Operator can!

Of course, you can also see it in the GUI as well, with Active Directory Users and Computers with advanced view turned on, for example.

So there you have it. Be smart, test it out in a lab first, and then enjoy your 30-day, random rotating local admin passwords!

As I continue to update this software package, new versions will be published on this page.

Download:

LAPM-1.0.zip (54.4KB)