Safe code & pitfalls: DLL side-loading, WinAPI and C++

Why your C++ (or not) app is probably vulnerable right now

Brief reminder

Theory

Practice

typedef BOOL(WINAPI *pfnSetDefaultDllDirectories)(DWORD DirectoryFlags);
typedef BOOL(WINAPI *pfnSetDllDirectoryW)(LPCWSTR lpPathName);
#define LOAD_LIBRARY_SEARCH_SYSTEM32 0x00000800int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR /*lpstrCmdLine*/, int /*nCmdShow*/) { HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll"); auto pSetDefaultDllDirectories = (pfnSetDefaultDllDirectories)GetProcAddress(hKernel32, "SetDefaultDllDirectories");
if (pSetDefaultDllDirectories != nullptr) {
pSetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32);
}
auto pSetDllDirectory = (pfnSetDllDirectoryW)GetProcAddress(hKernel32, "SetDllDirectoryW");
if (pSetDllDirectory != nullptr) {
pSetDllDirectory(L"");
}
}
kernel32.dll!LoadLibraryExWStub#()
Vulnerable.exe!try_load_library_from_system_directory()
Vulnerable.exe!try_get_module(const `anonymous-namespace'::module_id id=api_ms_win_core_synch_l1_2_0)
Vulnerable.exe!try_get_first_available_module()
Vulnerable.exe!try_get_proc_address_from_first_available_module()
Vulnerable.exe!try_get_function()
Vulnerable.exe!try_get_InitializeCriticalSectionEx()
Vulnerable.exe!__vcrt_InitializeCriticalSectionEx()
Vulnerable.exe!__vcrt_initialize_locks()
Vulnerable.exe!__vcrt_initialize()
Vulnerable.exe!__scrt_initialize_crt()
Vulnerable.exe!__scrt_common_main_seh()
Vulnerable.exe!__scrt_common_main()
Vulnerable.exe!wWinMainCRTStartup()
kernel32.dll!BaseThreadInitThunk#()
ntdll.dll!RtlUserThreadStart#()
KernelBase.dll!LoadLibraryExW()
KernelBase.dll!LoadLibraryExA#()
Vulnerable.exe!__delayLoadHelper2(const ImgDelayDescr * pidd=0x00007ff658b6a080, __int64(*)() * ppfnIATEntry=0x00007ff658b6c690) C++
Vulnerable.exe!__tailMerge_USER32_dll#()
Vulnerable.exe!`dynamic initializer for ‘WM_RESET_WINDOW_POSITION_DB’’()
Vulnerable.exe!_initterm(void(*)() * first=0x00007ff657333000, void(*)() * last=0x00007ff657341818)
Vulnerable.exe!__scrt_common_main_seh()
Vulnerable.exe!__scrt_common_main()
Vulnerable.exe!wWinMainCRTStartup()
kernel32.dll!BaseThreadInitThunk#()
ntdll.dll!RtlUserThreadStart#()
// Static variable to ensure that winsock is initialised before main, and
// therefore before any other threads can get started.
static const winsock_init<>& winsock_init_instance = winsock_init<>(false);
KernelBase.dll!LoadLibraryExW()
KernelBase.dll!LoadLibraryExA#()
Vulnerable.exe!__delayLoadHelper2()
Vulnerable.exe!__tailMerge_WS2_32_dll#()
Vulnerable.exe!boost::asio::detail::winsock_init_base::startup()Vulnerable.exe!boost::asio::detail::winsock_init<2,0>::winsock_init<2,0>()
Vulnerable.exe!boost::asio::detail::`dynamic initializer for ‘winsock_init_instance’’()
Vulnerable.exe!_initterm()
Vulnerable.exe!__scrt_common_main_seh()
Vulnerable.exe!__scrt_common_main()
Vulnerable.exe!wWinMainCRTStartup()
kernel32.dll!BaseThreadInitThunk#()
ntdll.dll!RtlUserThreadStart#()
#pragma comment(linker, "/ENTRY:CustomMainCrtStartup")int APIENTRY CustomMainCrtStartup()
{
// set directories here
return wWinMainCRTStartup();
}
#pragma optimize(“”, off)
#pragma init_seg(“.CRT$XCB”)
struct _dll_load_directory_t {
_dll_load_directory_t() {
// set directories here
}
};
namespace {
_dll_load_directory_t _dll_load_directory;
}
Windows 7 x64 with the latest updates
// Some libraries are still loaded from the current directories.
// If we pre-load them with an absolute path then we are good.
void PreloadLibs()
{
wchar_t sys32Folder[MAX_PATH];
GetSystemDirectory(sys32Folder, MAX_PATH);
std::wstring version = (std::wstring(sys32Folder) + L"\\version.dll");
std::wstring logoncli = (std::wstring(sys32Folder) + L"\\logoncli.dll");
std::wstring sspicli = (std::wstring(sys32Folder) + L"\\sspicli.dll");
LoadLibrary(version.c_str());
LoadLibrary(logoncli.c_str());
LoadLibrary(sspicli.c_str());
}
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include “wininet.h”
#pragma comment(lib, “wininet.lib”)int main() {
DWORD dummy;
InternetCombineUrlA(nullptr, nullptr, nullptr, &dummy, 0);
}
ntdll.dll!LdrpSearchPath()
ntdll.dll!LdrpFindOrMapDll()
ntdll.dll!LdrpLoadDll()
ntdll.dll!LdrpSnapThunk()
ntdll.dll!LdrpSnapIAT()
ntdll.dll!LdrpHandleOneOldFormatImportDescriptor()
ntdll.dll!LdrpProcessStaticImports()
ntdll.dll!LdrpLoadDll()
ntdll.dll!LdrLoadDll()
KernelBase.dll!LoadLibraryExW()
KernelBase.dll!LoadLibraryExA()
ConsoleApplication.exe!__delayLoadHelper2()
ConsoleApplication.exe!__tailMerge_wininet_dll()
ConsoleApplication.exe!main()
ConsoleApplicationexe!invoke_main()
ConsoleApplication.exe!__scrt_common_main_seh()
ConsoleApplication.exe!__scrt_common_main()
ConsoleApplication.exe!mainCRTStartup()
kernel32.dll!BaseThreadInitThunk()
ntdll.dll!RtlUserThreadStart()

Research

/* If we have process parameters, get the default path and current path */
if (ProcessParameters)
{
/* Check if we have a Dll Path */
if (ProcessParameters->DllPath.Length)
{
/* Get the path */
LdrpDefaultPath = *(PUNICODE_STRING)&ProcessParameters->DllPath;
}
/* Get the DLL Path */
DllPathString = BaseComputeProcessDllPath(FullPath, lpEnvironment);
if (!DllPathString)
...
/* Initialize Strings */
RtlInitUnicodeString(&DllPath, DllPathString);
RtlInitUnicodeString(&ImageName, FullPath);
UNICODE_STRING DllPath = NtCurrentPeb()->ProcessParameters->DllPath;
UNICODE_STRING ImagePathDir = NtCurrentPeb()->ProcessParameters->ImagePathName;
if (DllPath.Buffer == nullptr) {
return;
}
// Get first dir from PEB’s DllPath
{
wchar_t *delim = wcschr(DllPath.Buffer, L’;’);
if (delim != nullptr) {
DllPath.Length = (USHORT)WCHARS_TO_BYTES(delim — DllPath.Buffer);
}
}
// Get dir of current image
{
wchar_t *slash = wcsrchr(ImagePathDir.Buffer, L’\\’);
if (slash == nullptr) {
slash = wcsrchr(ImagePathDir.Buffer, L’/’);
if (slash == nullptr) {
return;
}
}
ImagePathDir.Length = (USHORT)WCHARS_TO_BYTES(slash — ImagePathDir.Buffer) + sizeof(wchar_t); // don't remove last backslash in case of root path (e.g. C:\)
if (!(ImagePathDir.Length == 6 &&
iswalpha(ImagePathDir.Buffer[0]) &&
ImagePathDir.Buffer[1] == L':' &&
(ImagePathDir.Buffer[2] == L'\\' || ImagePathDir.Buffer[2] == L'/'))) {
ImagePathDir.Length -= sizeof(wchar_t);
}
}
if (RtlCompareUnicodeString(&DllPath, &ImagePathDir, TRUE) == 0) {
wmemset(DllPath.Buffer, L’;’, BYTES_TO_WCHARS(DllPath.Length));
}
It does!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store