As demonstration purpose, i have written the following app. As you already got what the code does (a simple msgbox says “Hello World from .Net App”), we are going to change it “Hello world from .Injected DLL”
Not sharing this simple code cause i have uploaded all the code related with that on my github (you can find the appropriate links at the end of this writing)
So now, our target app is ready.
Next we need to write our injector app. This is going to be a native x64 app, however, you can do the same job just by pinvoking the same functions within a .net console/form app.
#include<Windows.h>
#include<Psapi.h>
#include<sddl.h>
#include<stdio.h>
#pragma comment(lib, "advapi32.lib")
HANDLE GetProcessByName(LPWSTR procname, DWORD *pdwPID)
{
DWORD need;
DWORD pids[2048] = { 0 };
if (!EnumProcesses(pids, sizeof(pids), &need))
return NULL;
for (DWORD i = 0; i < need / sizeof(DWORD); ++i)
{
DWORD dw;
HMODULE mod;
HANDLE proc;
WCHAR pn[MAX_PATH] = { 0 };
proc = OpenProcess
(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pids[i]);
if (proc) {
if (EnumProcessModules(proc, &mod, sizeof(mod), &dw)) {
GetModuleBaseNameW(proc, mod, pn, sizeof(pn) /
sizeof(char));
if (_wcsicmp(pn, procname) == 0) {
CloseHandle(proc);
*pdwPID = pids[i];
return OpenProcess
(PROCESS_ALL_ACCESS, FALSE, pids[i]);
}
}
CloseHandle(proc);
}
}
return NULL;
}
int wmain(int argc, LPWSTR argv[])
{
DWORD dw;
DWORD dwPID;
HANDLE hThread;
HANDLE hProcess;
PVOID pvBuffer;
PTHREAD_START_ROUTINE psrLoadLibrary;
HANDLE hPipe;
WCHAR wszPipeName[256] = { L'\0' };
WCHAR wszNetDLLInfo[4096] = { L'\0' };
SECURITY_ATTRIBUTES sa = {
.nLength = sizeof(SECURITY_ATTRIBUTES),
.bInheritHandle = FALSE
};
if (argc != 7) {
wprintf(L"usage: Injector.exe \"Process name\"\"NativeDLL
path\"\".Net DLL path\"\".Net Type Name\"\".Net Method
Name\"\".Net Method Parameter\"");
return EXIT_FAILURE;
}
wprintf(L"Finding process %s\n", argv[1]);
hProcess = GetProcessByName(argv[1], &dwPID);
if (!hProcess) {
wprintf(L"Process %s not found.\n", argv[1]);
return EXIT_FAILURE;
}
wprintf(L"Found process %s\n", argv[1]);
wprintf(L"Allocating memory in %s\n", argv[1]);
pvBuffer = VirtualAllocEx(hProcess, NULL, (wcslen(argv[2]) + 1) *
sizeof(WCHAR), MEM_COMMIT, PAGE_READWRITE);
if (!pvBuffer) {
wprintf(L"VirtualAllocEx failed. LastErrorCode: %X\n",
GetLastError());
return EXIT_FAILURE;
}
wprintf(L"Memory allocated %p in %s\n", pvBuffer, argv[1]);
wprintf(L"Writing %s at %p\n", argv[2], pvBuffer);
if (!WriteProcessMemory(hProcess, pvBuffer, (LPVOID)argv[2],
(wcslen(argv[2]) + 1) * sizeof(WCHAR), NULL))
{
wprintf(L"WriteProcessMemory failed. LastErrorCode: %X\n",
GetLastError());
return EXIT_FAILURE;
}
wprintf(L"Finding LoadLibraryW\n");
psrLoadLibrary = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(L"Kernel32"), "LoadLibraryW");
if (!psrLoadLibrary) {
wprintf(L"WriteProcessMemory failed. LastErrorCode: %X\n",
GetLastError());
return EXIT_FAILURE;
}
wprintf(L"Found LoadLibraryW at %p\n", psrLoadLibrary);
ConvertStringSecurityDescriptorToSecurityDescriptor(
L"D:(A;OICI;GRGWGX;;;WD)",
SDDL_REVISION_1,
&(sa.lpSecurityDescriptor),
NULL);
wsprintf(wszPipeName, L"\\\\.\\pipe\\NetDLL%X", dwPID);
wprintf(L"Creating named pipe %s\n", wszPipeName);
hPipe = CreateNamedPipe(wszPipeName, PIPE_ACCESS_OUTBOUND,
PIPE_TYPE_BYTE, PIPE_UNLIMITED_INSTANCES, 4096 * sizeof(WCHAR),
4096 * sizeof(WCHAR), 0, &sa);
if (hPipe == INVALID_HANDLE_VALUE)
{
wprintf(L"CreateNamedPipe failed. LastErrorCode: %X\n",
GetLastError());
return EXIT_FAILURE;
}
wprintf(L"Creating remote thread\n");
hThread = CreateRemoteThread(hProcess, NULL, 0, psrLoadLibrary,
pvBuffer, 0, NULL);
if (!hThread)
{
wprintf(L"CreateRemoteThread failed. LastErrorCode: %X\n",
GetLastError());
return EXIT_FAILURE;
}
wprintf(L"Remote thread created %p\n", hThread);
wsprintf(wszNetDLLInfo, L"%s|%s|%s|%s|", argv[3], argv[4], argv[5],
argv[6]);
wprintf(L"Connecting named pipe\n");
ConnectNamedPipe(hPipe, NULL);
wprintf(L"Writing named pipe %s\n", wszNetDLLInfo);
WriteFile(hPipe, wszNetDLLInfo, sizeof(wszNetDLLInfo), &dw, NULL);
wprintf(L"Success.\n");
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
CloseHandle(hPipe);
return 0;
}
This app takes six arguments, first executable to inject, second dll to be injected (NativeDLL, will talk about it next)(note that, the dll paths should be exact, cannot be relative as you already know), third .net dll path, fourth .net type name, fifth .net method name and the last .net method argument.
Application works as it first inject NativeDLL into the target process then creates a named pipe with the name like \\.\pipe\NetDLL%PID% (where %PID% is the hex representation of the process id) and all of the remaining arguments written into that pipe with pipe symbol (‘|’) separated.
NativeDLL opens that pipe and reads the .net dll information to be injected into the .net app domain and executes the method (which is our fifth parameter) using ExecuteInDefaultAppDomain.
Method definition should be as follows
Here is the code of NativeDLL
// dllmain.cpp : Defines the entry point for the DLL application.
#include<Windows.h>
#include<sddl.h>
#include<metahost.h>
#include"mscoree.h"
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "mscoree.lib")
ICLRRuntimeHost* GetCLRRuntimeHost()
{
ICLRMetaHost* clrMetaHost = NULL;
ICLRRuntimeInfo* clrRuntimeInfo = NULL;
ICLRRuntimeHost* clrRuntimeHost = NULL;
if (CLRCreateInstance(&CLSID_CLRMetaHost, &IID_ICLRMetaHost,
&clrMetaHost) != S_OK)
{
MessageBox(NULL, L"Cannot obtain clr meta host.", L"Error", MB_OK
| MB_ICONERROR);
goto exit;
}
if (clrMetaHost->lpVtbl->GetRuntime(clrMetaHost, L"v4.0.30319",
&IID_ICLRRuntimeInfo, &clrRuntimeInfo) != S_OK)
{
MessageBox(NULL, L"Cannot obtain clr runtime info.", L"Error",
MB_OK | MB_ICONERROR);
goto exit;
}
if (clrRuntimeInfo->lpVtbl->GetInterface
(
clrRuntimeInfo,
&CLSID_CLRRuntimeHost,
&IID_ICLRRuntimeHost,
&clrRuntimeHost) != S_OK)
{
MessageBox(NULL, L"Cannot obtain clr runtime host.", L"Error",
MB_OK | MB_ICONERROR);
goto exit;
}
exit:
if (clrRuntimeInfo) {
clrRuntimeInfo->lpVtbl->Release(clrRuntimeInfo);
}
if (clrMetaHost) {
clrMetaHost->lpVtbl->Release(clrMetaHost);
}
return clrRuntimeHost;
}
BOOL ParseNetDLLInfo
(
PWSTR pwszNetDLLInfo,
PWSTR* ppwszAssemblyPath,
PWSTR* ppwszAssemblyTypeName,
PWSTR* ppwszAssemblyMethodName,
PWSTR* ppwszAssemblyArgument)
{
PWSTR p = pwszNetDLLInfo;
*ppwszAssemblyPath = p;
p = wcsstr(p, L"|");
if (!p) {
returnFALSE;
}
*p = L'\0';
++p;
*ppwszAssemblyTypeName = p;
p = wcsstr(p, L"|");
if (!p) {
return FALSE;
}
*p = L'\0';
++p;
*ppwszAssemblyMethodName = p;
p = wcsstr(p, L"|");
if (!p) {
return FALSE;
}
*p = L'\0';
++p;
*ppwszAssemblyArgument = p;
p = wcsstr(p, L"|");
if (!p) {
return FALSE;
}
*p = L'\0';
++p;
return TRUE;
}
BOOL ReadNetDLLInfo(PWSTR* ppwszAssemblyPath, PWSTR* ppwszAssemblyTypeName, PWSTR* ppwszAssemblyMethodName, PWSTR* ppwszAssemblyArgument)
{
DWORD dw;
BOOL bRet = FALSE;
HANDLE hPipe;
WCHAR wszPipeName[256] = { L'\0' };
static WCHAR wszNetDLLInfo[4096] = { L'\0' };
SECURITY_ATTRIBUTES sa = {
.nLength = sizeof(SECURITY_ATTRIBUTES),
.bInheritHandle = FALSE
};
wsprintf(wszPipeName, L"\\\\.\\pipe\\NetDLL%X",
GetCurrentProcessId());
ConvertStringSecurityDescriptorToSecurityDescriptor(
L"D:(A;OICI;GRGWGX;;;WD)",
SDDL_REVISION_1,
&(sa.lpSecurityDescriptor),NULL);
hPipe = CreateFile
(
wszPipeName,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
&sa,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hPipe == INVALID_HANDLE_VALUE) {
MessageBox(NULL, L"Cannot open named pipe.", L"Error", MB_OK |
MB_ICONERROR);
goto exit;
}
if (!ReadFile(hPipe, wszNetDLLInfo, sizeof(wszNetDLLInfo), &dw, NULL))
{
MessageBox(NULL, L"Cannot read from named pipe.", L"Error", MB_OK
| MB_ICONERROR);
goto exit;
}
if (!ParseNetDLLInfo
(
wszNetDLLInfo,
ppwszAssemblyPath,
ppwszAssemblyTypeName,
ppwszAssemblyMethodName,
ppwszAssemblyArgument))
{
MessageBox(NULL, L"Cannot parse net dll info string.", L"Error",
MB_OK | MB_ICONERROR);
goto exit;
}
bRet = TRUE;
exit:
if (hPipe != INVALID_HANDLE_VALUE) {
CloseHandle(hPipe);
}
return bRet;
}
void ExecuteNetDLL()
{
PWSTR pwszAssemblyPath;
PWSTR pwszAssemblyTypeName;
PWSTR pwszAssemblyMethodName;
PWSTR pwszAssemblyArgument;
DWORD dwReturn;
ICLRRuntimeHost* clrRuntimeHost = NULL;
if (!ReadNetDLLInfo
(
&pwszAssemblyPath,
&pwszAssemblyTypeName,
&pwszAssemblyMethodName,
&pwszAssemblyArgument))
{
goto exit;
}
clrRuntimeHost = GetCLRRuntimeHost();
if (!clrRuntimeHost)
{
goto exit;
}
if (clrRuntimeHost->lpVtbl->ExecuteInDefaultAppDomain
(
clrRuntimeHost,
pwszAssemblyPath,
pwszAssemblyTypeName,
pwszAssemblyMethodName,
pwszAssemblyArgument,
&dwReturn) != S_OK)
{
MessageBox(NULL, L"ExecuteInDefaultAppDomain error.", L"Error",
MB_OK | MB_ICONERROR);
goto exit;
}
exit:
if (clrRuntimeHost) {
clrRuntimeHost->lpVtbl->Release(clrRuntimeHost);
}
}
BOOL APIENTRY DllMain(
HMODULE hModule,
DWORD dwReason,
LPVOID lpReserved
)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
ExecuteNetDLL();
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
And our main purpose DLL code is as follows
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace NetDLL
{
public class Injected
{
private static void btnSayHiFromInjected_Click
(
object sender,
EventArgs e)
{
MessageBox.Show("Hello world from .Injected DLL");
}
private static void RemoveNetAppEventHandler(Button btnSayHi)
{
object current = null;
EventHandlerList events = null;
events = (EventHandlerList)typeof(Component)
.GetProperty
(
"Events",
BindingFlags.NonPublic | BindingFlags.Instance
)
.GetValue(btnSayHi, null);
current = events?.GetType()
.GetField
(
"head",
BindingFlags.NonPublic | BindingFlags.Instance
)
.GetValue(events);
while (current != null)
{
object handler = current.GetType()
.GetField
(
"handler",
BindingFlags.NonPublic | BindingFlags.Instance
)
.GetValue(current);
if (handler is EventHandler eh &&
eh.Method.Name == "btnSayHi_Click")
{
object key = current.GetType()
.GetField
(
"key",
BindingFlags.NonPublic | BindingFlags.Instance
)
.GetValue(current);
events.RemoveHandler(key, eh.GetInvocationList()[0]);
break;
}
current = current.GetType()
.GetField
(
"next",
BindingFlags.NonPublic | BindingFlags.Instance
)
.GetValue(current);
}
}
public static int InjectedMethod(string pwzArgument)
{
IntPtr ptrMainWndHandle = IntPtr.Zero;
ptrMainWndHandle = Process.GetCurrentProcess()
.MainWindowHandle;
if (ptrMainWndHandle == IntPtr.Zero ||
!(Form.FromHandle(ptrMainWndHandle) is Form form))
{
MessageBox.Show
(
"Error",
"Could not get main window",
MessageBoxButtons.OK, MessageBoxIcon.Error
);
return 1;
}
form.Invoke(new MethodInvoker(() =>
{
form.Text = ".NET DLL Injected";
RemoveNetAppEventHandler(form.Controls["btnSayHi"]
as Button);
(form.Controls["btnSayHi"]
as Button).Click += btnSayHiFromInjected_Click;
}));
return 0;
}
}
}
Simple explanation of the functions of the dll;
InjectedMethod our main function, which will be executed after injection. It tries to get Main Window handle. If it does, calls Invoke method in order to be able to change main form states. (We are using Invoke, just because our code runs in a different thread than main ui thread)
RemoveNetAppEventHandler removes the connection between btnSayHi.Click and btnSayHi_Click
(form.Controls[“btnSayHi”] as Button).Click += btnSayHiFromInjected_Click; Add a connection between our function and btnSayHi.Click
Before injecting the dll
After the injection process
Source: Medium - Gurhan Polat
The Tech Platform
Comments