Hi! Firstly, apologies if this seems like an awfully dumb question, I know those are way too common around here, but I've been trying to figure this one out for myself for a couple days now to no avail. Anyway, here goes!
I've decided to go back to dabbling with WoW memory editing/botting/reversing, whatever you want to call it, and am trying my hand at some injected C# fun. I've succeeded in the sense that I have a C# Winforms app that injects a native bootstrapper DLL, which in turn loads the CLR into WoW.exe, and executes a test method in a test assembly. This all works fine, my test method allocates a console, prints to it, frees the console, and returns a random int. I get the return code, everything is great. However, if I try to repeat this action after doing it the first time, WoW craps itself and crashes.
The first time round looks fine, the console appears, the message appears, the console disappears after calling FreeConsole(). The second time, WoW crashes on the call to Console.WriteLine();
I'm not sure what the problem is, my best guess would be maybe something handle related? Has anyone had any joy (or pain) in using a console window for output while injected (from managed or unmanaged code)? If I get rid of the console related stuff and just return the random int everything works fine.
For reference, here is my code:
The .NET assembly + function that gets executed
Code:
namespace TestAssemblyInjected
{
public class Class1
{
private static Random rand;
public static int Test(String argument)
{
var res = AttachConsole(-1); // -1 == ATTACH_PARENT_PROCESS
if (res == 0)
AllocConsole();
Console.WriteLine("Hello");
Console.ReadLine();
FreeConsole();
if (rand == null)
rand = new Random();
return rand.Next(0,1000);
}
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int AttachConsole(int HANDLE);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int FreeConsole();
}
}
The CLR bootstrapper DLL
Code:
#include "DOTNET Bootstrap DLL.h"
#include <metahost.h>
#include <strsafe.h>
#include <iostream>
#include <fstream>
#include <io.h>
#include <fcntl.h>
#pragma comment(lib, "mscoree.lib")
// What is this black magic? I don't think anyone knows. I sure don't.
#import "mscorlib.tlb" raw_interfaces_only \
high_property_prefixes("_get","_put","_putref") \
rename("ReportEvent", "InteropServices_ReportEvent")
DOTNETBOOTSTRAPDLL_API int __cdecl bootstrap(wchar_t* assembly_path, wchar_t* type_name, wchar_t* method_name)
{
HRESULT hr;
ICLRMetaHost *pMetaHost = nullptr;
ICLRRuntimeInfo *pRuntimeInfo = nullptr;
ICLRRuntimeHost *pClrRuntimeHost = nullptr;
DWORD dw = 1;
BOOL result;
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
if (SUCCEEDED(hr))
{
hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));
if (SUCCEEDED(hr))
{
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pClrRuntimeHost));
if (SUCCEEDED(hr))
{
hr = pClrRuntimeHost->Start();
if (FAILED(hr)) {
ErrorExit(TEXT("Start"));
}
hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(
assembly_path,
type_name,
method_name,
L"HERRO PREESE",
&dw
);
if (FAILED(hr)) {
ErrorExit(TEXT("ExecuteInDefaultAppDomain"));
}
hr = pClrRuntimeHost->Release();
} else {
ErrorExit(TEXT("GetInterface"));
}
hr = pRuntimeInfo->Release();
} else {
ErrorExit(TEXT("GetRuntime"));
}
hr = pMetaHost->Release();
} else {
ErrorExit(TEXT("CLRCreateInstance"));
}
return dw;
}
void ErrorExit(LPTSTR lpszFunction)
{
// Retrieve the system error message for the last-error code
LPVOID lpMsgBuf;
LPVOID lpDisplayBuf;
DWORD dw = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );
// Display the error message and exit the process
lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT,
(lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR));
StringCchPrintf((LPTSTR)lpDisplayBuf,
LocalSize(lpDisplayBuf) / sizeof(TCHAR),
TEXT("%s failed with error %d: %s"),
lpszFunction, dw, lpMsgBuf);
MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);
LocalFree(lpMsgBuf);
LocalFree(lpDisplayBuf);
}
And the relevant bits from my WinForms C# app that injects and calls the bootstrapper
Code:
//Called upon clicking a 'Find Wow' button
private void FindWoW(object sender, EventArgs e)
{
var wowProcs = ApplicationFinder.FromProcessName("Wow");
Process wowProc;
if (!wowProcs.Any())
{
// launch wow!
wowProc = Process.Start("D:\\Games\\World of Warcraft\\Wow.exe");
} else
{
wowProc = wowProcs.First();
}
Thread.Sleep(100);
this.wow = new MemorySharp(wowProc);
l_PID.Text = wow.Pid.ToString();
}
// Called upon clicking an 'Inject' button
private void DoInject(object sender, EventArgs e)
{
string bootstrapperPath = "bootstrapper.dll";
string testAssemblyPath = "TestAssemblyInjected.dll";
if (File.Exists(bootstrapperPath))
{
var bsf = new FileInfo(bootstrapperPath);
var taf = new FileInfo(testAssemblyPath);
bootstrapperPath = bsf.FullName;
testAssemblyPath = taf.FullName;
using (bootstrapper = wow.Modules.Inject(bootstrapperPath))
{
string test = "Hello!";
var ret = bootstrapper["bootstrap"].Execute(CallingConventions.Cdecl, AsUTF8(testAssemblyPath),
AsUTF8("TestAssemblyInjected.Class1"), AsUTF8("Test"));
l_ret.Text = ret.ToString();
}
} else
{
l_ret.Text = "Bootstrapper not found.";
}
}
// C++ Win32 API wants strings in UTF8, C# uses UTF16, let's make it happen
private static string AsUTF8(string s)
{
var utf16 = new UnicodeEncoding();
byte[] encodedBytes = utf16.GetBytes(s);
return Encoding.UTF8.GetString(encodedBytes);
}
Update:
So since I forgot to actually mention the specific error WoW is throwing, here it is...
Exception: 0xC0000005 (ACCESS_VIOLATION) at 0023:0C0D137F
The instruction at "0x0C0D137F" referenced memory at "0x0C0D137F". The memory could not be "executed".
I've also wrapped my console stuff in a try/catch block, and SOMETIMES instead of WoW crashing it will catch an exception:
The handle is invalid.
@ WriteLine()
So I guess something isn't being cleaned up properly the first time around, or my STDOUT etc handles aren't being correctly set the second time around. The fact that it only sometimes crashes is quite confusing though.