Wstrzykiwanie biblioteki DLL do procesu

Poprzedni post skończył sie napisaniem (nieprzechodzących) testów. Teraz nadszedł czas na implementację mechanizmu wstrzykiwania biblioteki DLL do innego procesu.

Na początek stwórzmy bibliotekę, która będzie wstrzykiwana w docelowy proces. Niestety nie możemy jej napisać w F# ponieważ musi być ona natywna (i zgodna z architekturą docelowego procesu), tak więc wykorzystamy C++. Po załadowaniu do procesu wywołana zostanie funkcja DllMain:

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
injection();
return FALSE;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return FALSE;
}
view raw DllMain.cpp hosted with ❤ by GitHub

Zwrócenie wartości FALSE powoduje natychmiastowe wyładowanie biblioteki z pamięci. Dzięki temu nie będzie zaśmiecać docelowego procesu. Komunikacja z Patherem odbywać będzie się przy pomocy named pipe, a cała pętla obsługi znajduje się w funkcji injection:

void injection()
{
int i = 0;
wchar_t pipeName[1024];
wsprintf(pipeName, L"\\\\.\\pipe\\pather\\%d", GetCurrentProcessId());
HANDLE pipe = CreateFile(pipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
DWORD mode = PIPE_READMODE_MESSAGE | PIPE_WAIT;
SetNamedPipeHandleState(pipe, &mode, NULL, NULL);
while (true)
{
uint8_t commandCode;
if (!ReadFile(pipe, &commandCode, 1, NULL, NULL))
{
if (GetLastError() == ERROR_BROKEN_PIPE)
{
break;
}
}
switch (commandCode)
{
case 45:
pingPong(pipe);
break;
case 01:
setEnv(pipe);
break;
case 02:
readEnv(pipe);
break;
}
}
CloseHandle(pipe);
}
view raw injection.cpp hosted with ❤ by GitHub

Nazwa potoku budowana jest według wzoru: \\.\pipe\pather\<id-procesu>, a na na protokół składają sie trzy operacje: echo (kod 45), ustawianie (01) i odczytywanie (02) zmiennej środowiskowej. Na razie zostawy ich implementację i przyjrzyjmy się jak to wygląda stronie Pathera, która jest zdecydowanie bardziej złożona.

Na początek trzeba zaimportować kilka funkcji WinApi:

[<DllImport("kernel32.dll", SetLastError = true)>]
extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, int nSize, int * lpNumberOfBytesWritten);
[<DllImport("kernel32.dll", SetLastError = true)>]
extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
[<DllImport("kernel32.dll")>]
extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, UInt32 dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, UInt32 dwCreationFlags, IntPtr * lpThreadId);
[<DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)>]
extern bool IsWow64Process(IntPtr hProcess, bool * wow64Process);
view raw DllImports.fs hosted with ❤ by GitHub

Wstrzykiwanie biblioteki DLL do jakiegoś procesu opiera się na utworzeniu w nim wątku (z pomocą CreateRemoteThread) z funkcją LoadLibrary jako entrypointem i ścieżką do biblioteki jako parametrem w postaci wskaźnika na łańcuch znaków zakończony znakiem 0. Wymaga to uprzedniego umieszczenia go w pamięci procesu co umożliwia para VirtualAllocEx i WriteProcessMemory. Całość rozbudowana o wybranie odpowiedniej wersji biblioteki DLL (x86 albo x64) przedstawia się następująco:

let loadLibrary = findFunction processHandle "kernel32.dll" "LoadLibraryA"
let mutable isWow = false
IsWow64Process(processHandle, &&isWow) |> ignore
let libraryPath = if isWow then library.Path86 else library.Path64
let arg = VirtualAllocEx(processHandle, IntPtr.Zero, nativeint (libraryPath.Length + 1), AllocationType.Commit ||| AllocationType.Reserve, MemoryProtection.ReadWrite)
let argPtr = Marshal.StringToHGlobalAnsi(libraryPath)
let mutable written = 0
WriteProcessMemory(processHandle, arg, argPtr, libraryPath.Length + 1, &&written) |> ignore
let mutable threadId = IntPtr.Zero
CreateRemoteThread(processHandle, IntPtr.Zero, 0u, loadLibrary, arg, 0u, &&threadId) |> ignore
view raw inject.fs hosted with ❤ by GitHub

Funkcja findFunction pozwala na określenie adresu zadanej funkcji w przestrzeni adresowej docelowego procesu i będzie tematem następnego postu.

Mając mechanizm wstrzykiwania możemy obudować go w obsługę named-pipe:

let private injectionPath =
let basePath = System.AppDomain.CurrentDomain.BaseDirectory
{
Native.LibraryPath.Path86 = Path.Combine(basePath, "Injections", "Injection.x86.dll")
Native.LibraryPath.Path64 = Path.Combine(basePath, "Injections", "Injection.x64.dll")
}
let openChannel (processId: int) =
let proc = Process.GetProcessById(processId)
let pipeName = sprintf "pather\%d" processId
let pipe = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Message)
Native.injectLibrary proc.Handle injectionPath
pipe.WaitForConnection()
pipe
view raw openChannel.fs hosted with ❤ by GitHub

Mając podstawową obsługę komunikacji z obu stron możemy zaimplementować trzy operacje:

ze strony Pathera:

let echo (channel: NamedPipeServerStream) =
channel.WriteByte(45uy)
channel.ReadByte() = 54
let setEnvVar (channel: NamedPipeServerStream) (variable: string) (value: string) =
use writer = new BinaryWriter(channel, Encoding.Unicode)
writer.Write(01uy)
writer.Write(variable)
writer.Write(value)
channel
let readEnvVar (channel: NamedPipeServerStream) (variable: string) =
use writer = new BinaryWriter(channel, Encoding.Unicode)
writer.Write(02uy)
writer.Write(variable)
use reader = new BinaryReader(channel, Encoding.Unicode)
reader.ReadString()
let readPath (processId : int) =
use channel = openChannel processId
readEnvVar channel "PATH"
let setPath (processId : int) (path : string) =
use channel = openChannel processId
setEnvVar channel "PATH" path |> ignore
view raw funcs.fs hosted with ❤ by GitHub

oraz biblioteki DLL:

int readStringLength(HANDLE pipe)
{
int count = 0;
int shift = 0;
uint8_t b;
do
{
ReadFile(pipe, &b, 1, NULL, NULL);
count |= (b & 0x7F) << shift;
shift += 7;
} while ((b & 0x80) != 0);
return count;
}
void writeStringLength(HANDLE pipe, int length)
{
auto v = (uint32_t)length;
uint8_t b;
while (v >= 0x80) {
b = (uint8_t)(v | 0x80);
WriteFile(pipe, &b, 1, NULL, NULL);
v >>= 7;
}
b = (uint8_t)v;
WriteFile(pipe, &b, 1, NULL, NULL);
}
wchar_t * readString(HANDLE pipe)
{
int length = readStringLength(pipe);
auto value = new wchar_t[length / 2 + 1]();
int offset = 0;
while (offset < length)
{
DWORD readBytes;
auto r = ReadFile(pipe, value + offset / 2, length - offset, &readBytes, NULL);
offset += readBytes;
}
return value;
}
void writeString(HANDLE pipe, wchar_t * str)
{
auto length = wcslen(str);
writeStringLength(pipe, length * sizeof(wchar_t));
WriteFile(pipe, str, length * sizeof(wchar_t), NULL, NULL);
}
void setEnv(HANDLE pipe)
{
auto variable = readString(pipe);
auto value = readString(pipe);
SetEnvironmentVariable(variable, value);
delete variable;
delete value;
}
void readEnv(HANDLE pipe)
{
auto variable = readString(pipe);
auto valueLength = GetEnvironmentVariable(variable, NULL, 0);
wchar_t * value;
if (valueLength == 0)
{
value = L"";
}
else
{
value = new wchar_t[valueLength]();
GetEnvironmentVariable(variable, value, valueLength);
}
writeString(pipe, value);
delete variable;
}
void pingPong(HANDLE pipe)
{
uint8_t response = 54;
WriteFile(pipe, &response, 1, NULL, NULL);
}
view raw funcs.cpp hosted with ❤ by GitHub

Wyjaśnienia wymagają funkcje C++ readString, readStringLength, writeString oraz writeStringLength - są to funkcje implementujące ten sam sposób kodowania łańcuchów znaków co BinaryReader i BinaryWriter. Ich implementacja jest bliźniaczo podobna do ich odpowiedników w .NET (BinaryReader.ReadString, BinaryWriter.WriteString).

Świadomie nie skupiałem się na problemach związanych z aplikacjami 32-bitowymi na 64-bitowym systemie - jedynie funkcja injectLibrary wybiera odpowiednią bibliotekę na podstawie architektury docelowego procesu. Problemy związane z WoW64 zostaną opisane w następnym poście.

Obsługa wiersza polecenia

Obsługa wiersza polecenia, a własciwie argumentów przekazywanych w ten sposób, to temat rzeka. Istnieje niezliczona liczba bibliotek i ko...… Continue reading

Pakowanie aplikacji z ILRepack

Published on March 30, 2016

Paket i FAKE

Published on March 24, 2016