Like my previous article, there is nothing terribly exciting with this one as we have to get through the necessary boring stuff first before we can have any real fun. Unfortunately, this article is very heavy text wise and there are not many relevant images for the concepts being described. Sorry about that, but once we get this out of the way, the sailing will be much smoother!
While I'm writing this article for the Silkroad section, the concepts still apply to any game and you only need to make a couple of changes for it to work in other games. Those changes are the command line for the game and whatever is required to fake the client into thinking the original loader was used.
The last thing I should notice is that I am not attaching any of the files for this article. The reason for that is so users can work out doing these things themselves and never have to worry about not having the skills to do it again in the future. If a user can read through these articles and follow them without much trouble (barring any mistakes on my part), then they are ready for developing with Silkroad.
Creating a Simple Loader with Injected DLL for Silkroad
I. Purpose
The purpose of this guide is to show the necessary framework required for having a simple loader and injected DLL. The methods shown in this guide are going to be built upon in future articles, so this article is a very important foundation to have in place. The code provided is going to be as simple as possible as to not have too many distractions. This guide is also going to make use of existing code I’ve written in the past to help speed it along. I will reference external links for additional reading as well along the way.
II. Requirements
This guide is written as an intermediate guide that most people should be able to follow as I’m not teaching programming per se. Rather I’m using existing code of my own and simplifying it into an easy to use project. It is expected you have some basic knowledge of C++ and some ASM knowledge would not hurt either to understand the code that is not covered.
In order to be able to follow along, you will need:
• Visual Studio 2008 (or equivalent)
You can use other version of Visual Studio, but be warned that some code does not work the same on older versions. I cannot test all the older versions of Visual Studio anymore since I’ve upgraded to Windows 7 beta and those installs were on my XP drive. I would recommend using at least version 2005 if you cannot get access to 2008. Likewise, I’ve not done any testing with 2010 as it is still in early beta stages.
III. Theory
The essence of any loader is simple. The user runs the loader, the loader performs some routines, and finally the loader launches the game client. Most games nowadays use loaders so they are commonplace. If you were to open up Silkroad.exe, the Silkroad client’s official loader, you might be able to follow some of the logic it does internally. However, that is not important for now. We are concerned with how to make our own loader.
Luckily for us, all we really have to do is create a program that has two abilities. The first ability is being able to create a process in a suspended state. The second ability is being able to inject a DLL into that suspend process. Once we have that done, then all of our real work takes place in the DLL that is going o be injected.
Some loaders do patch the client and make changes when a DLL is not needed, but that is not our scenario here. We need a DLL to be injected into the client in the least obstructive way, which is why our loader is so simple. There are many ways to go about creating a loader. You can even create a loader that uses Window’s workings so you don’t even need to create a loader executable. These are more advanced topics and will not be covered here as we want something simple to get up and running with the client.
That is all there is to a loader really. It is just a simple program that launches another program after performing some internal tasks. The loader we will create will be no different. We now need to look at what the DLL will be doing that we inject into the client.
Once a DLL is injected into a process, it becomes part of the process. That means the DLL can access anything in the executable’s memory as well as the actual code that is going to be executed. This is great for us because we can make patches to the client in real time, add in new functions to change the logic of existing functions, as well as detour certain areas to siphon useful information from the client.
If you look around at a lot of DLL injection tutorials, you will notice how the main bulk of the code is performed in the DLLMain entry function. For the most part, these tutorials are wrong and the suggestions they provide can be hazardous. If you want to create a DLL with a purpose like ours, you will want to be sure to understand the Best Practices for Creating DLLs. There is a lot of useful information to understand in that MSDN resource that most people skip and never make a mention of. As a result, their programs often have troubles when being used on different versions of Windows!
The DLL injection method that I am going to use is an older version of something I came up with a few years back to create a solution that would work with any executable, packed or not. The way it works is by modifying the entry point of the target to immediately jump into the DLL loading function. It will then load the DLL and call a specific function so the DLL can patch the entry point back to its original bytes.
The advantage to the method is that it is very fast, efficient, and provides a correct means to use a DLL in a process that is inline with the best practices suggested by Microsoft. The loader allocates a stub of memory in the targeted host first. It will then write out an assembly based loader procedure that is hand written. Finally, it will overwrite the entry point of the host so execution starts with calling the procedure to inject the DLL. The writing takes place when the process is suspended and the loading and injecting takes places when the application is running, so it al works out perfectly (assuming we need to inject before the process runs, which we do here).
The disadvantage to the method is that it is hard coded and not easy to change or understand if you are new. It is also a lot of code! I do not want to attach any additional files, so I will be embedding the code into the article. I’ve left in the comments to help follow the logic so as a result, the code listing is pretty long. I have worked on a newer much improved method, but it is too complicated to use for now and I’ve not even written about that approach yet.
We should have a basic idea of what our loader and injected DLL are going to be like. It is time to move on to the implementation stage!
IV. Implementation
To begin, we will need to create our Visual Studio workspace. I will spend a little extra time here describing how to go about that because a properly setup workspace can greatly reduce development time and headaches down the line. For this article I will be creating a workspace that has two projects: the loader and the DLL. The loader will also contain code to let the user select the Silkroad client as well as the DLL that is going to be injected. It will then store that information in a convenient location that is writeable on all systems. This means that once you go through the process of setting the client and DLL path, you can simply make changes to your DLL in Visual Studio, run the loader via Visual Studio and be able to test without copying over any files!
To create our workspace, we will first need to go to File -> New -> Project… In the new dialog that pops up (it says New Project), choose Other Project Types and then Visual Studio Solutions. Choose the Blank Solution under the Visual Studio installed templates area and type in a name for your workspace. Finally, hit Ok to create the Solution
Now, under the Solution Explorer on the left pane, right click on your new Solution. Choose Add -> New Project. Under the Visual C++ Project types area, choose Win32 and Win32 Project. We do not want to use a console for the Loader as we want as minimal user distractions so not having a console window is fine. Type in a new name for your Loader project and hit Ok. On the new dialog that pops up, choose Application Settings on the left pane. After the dialog switches, place a check in the Empty project checkbox and hit Finish.
We will repeat the previous step again for the DLL, except under the Application Settings, we will want to change the Application type to DLL and place a check in the Empty project checkbox. We now have our DLL and Loader workspace setup in one nice Visual Studio solution.
There is one more thing we need to do before we get into the specific implementation. When writing code for more than one project, we will most likely wish to share code between the projects without duplicating it. To accomplish this, we will need to create the files once in a location that all projects access. Right click on the Solution again in the Solution Explorer and choose Open Folder in Windows Explorer. In this folder you should see your Loader folder, your DLL folder, and the solution files for the workspace. Create a new folder here named Common. This is what my folder looks like:

Open the newly created Common folder and create two new files: common.h and common.cpp. You can simply create text files that you simply rename, but be careful of extension hiding settings in Windows. If the icons of the files do not change to Visual Studio icons, chances are your files are named common.txt.h and common.txt.cpp (but you only see common.cpp and common.h due to the setting to hide known extension types) and you will need to resave them through Notepad/WordPad. Go to Tools -> Folder Options in any Explorer window if you need to change this. Here is a reference screenshot of how the setting should look:

Once that is done, switch back to Visual Studio. For each of our projects, Loader and DLL, we will need to right click on the Project name and choose Add -> Existing Item. The location of the common files is above the projects directory, so navigate up one level and choose the Common folder and then both common files to add them to the project. When you are done, both projects should have the common.h and common.cpp files added to them. We will be working with only one copy of the common files now; changes made via either project reflect the changers seen in the other.
Just for reference, here is what my workspace looks like now:

Now we can add our main project file for each project. Right click on the DLL project and choose Add -> New Item. Click on the Code entry on the left pane and then choose a C++ File (.cpp) on the right pane. Give your file a name, I chose DLL.cpp and hit Add. Repeat this process for the Loader project as well. I named my file Loader.cpp there.
Now we are finally ready for some code! I am going to do things a little backwards. I’ll start out by showing the loader code, then the DLL code, and finally the code that makes up the Common functionality that is shared between both projects. The idea is to only have the bare minimal functionality needed to create a working loader and injected DLL while not being a pain to work with (this specific point will be address later).
Starting out with the loader code, Loader.cpp:
Code:
#include <windows.h>
#include <string>
#include <sstream>
#include "../common/common.h"
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// Let's get rid of any extra warnings we don't need
UNREFERENCED_PARAMETER(hInstance);
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
UNREFERENCED_PARAMETER(nCmdShow);
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
std::string pathToClient;
std::string pathToDll;
edx::ConfigFile cf;
bool bExists = false;
std::stringstream ini;
// Try to load the configuration file
ini << edx::GetWriteableDirectory("SilkroadFramework") << "SilkroadFramework.ini";
cf.Open(ini.str(), false, bExists);
if(bExists)
{
pathToClient = cf.Read("client");
pathToDll = cf.Read("dll");
}
if(bExists == false || pathToClient.empty() || pathToDll.empty())
{
// Choose the injection DLL if required
if(pathToDll.empty())
{
edx::FileChooser fc;
fc.AddFilter("Dynamic-link library", "*.dll");
fc.SetInitialDirectory(edx::GetAbsoluteDirectoryPath().c_str());
fc.SetDialogTitle("Please choose the DLL to inject...");
if(fc.ShowChooseFile(true) == false)
{
MessageBoxA(0, "The DLL selection process was canceled. The program will now exit.", "Fatal Error", MB_ICONERROR);
return 0;
}
pathToDll = fc.GetSelectedFilePath();
cf.Write("dll", pathToDll);
}
// Choose the game client if required
if(pathToClient.empty())
{
edx::FileChooser fc;
fc.SetDefaultFileName("sro_client.exe");
fc.AddFilter("Executable Files", "*.exe");
fc.SetDialogTitle("Please choose your \"sro_client.exe\" executable...");
if(fc.ShowChooseFile(true) == false)
{
MessageBoxA(0, "The client selection process was canceled. The program will now exit.", "Fatal Error", MB_ICONERROR);
return 0;
}
std::string title = fc.GetSelectedFileTitle();
if(title.find("sro_client") == std::string::npos)
{
std::stringstream ss;
ss << "You selected the file \"" << fc.GetSelectedFileName() << "\". Are you sure this is your \"sro_client.exe\" file?\n\nIf this file is not your \"sro_client.exe\" file, please choose No to exit the loader and try again. Otherwise please choose Yes to continue.";
int result = MessageBoxA(0, ss.str().c_str(), "Unexpected file name detected", MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2);
if(result == IDNO)
{
return 0;
}
}
pathToClient = fc.GetSelectedFilePath();
cf.Write("client", pathToClient);
}
// We have to restart the application now since the GetOpenFileName function messes up the working directories.
// Unfortunately on Vista/Win7, the old method of storing the previous working directory and reseting it
// doesn't seem to work anymore.
MessageBoxA(0, "The directory changes have been saved. Please relaunch the program now.", "Application restart required", MB_ICONINFORMATION);
return 0;
}
// Since we want to use a few functions
WSADATA wsaData = {0};
WSAStartup(MAKEWORD(2, 2), &wsaData);
bool bConnected = false;
// Figure out which login server we should use. This is what Silkroad.exe pretty much does
// in figuring out which last parameter to send to the client.
std::stringstream args;
args << "0 /18 0 ";
for(int x = 1; x <= 4; ++x)
{
std::stringstream ss;
ss << "gwgt" << x << ".joymax.com";
if(edx::CanGetHostFromAddress(ss.str()))
{
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
hostent * host = edx::GetHost(ss.str().c_str());
if(host)
{
sockaddr_in svr = {0};
svr.sin_addr.s_addr = *((unsigned long*)host->h_addr);
svr.sin_family = AF_INET;
svr.sin_port = htons(15779);
if(connect(s, (struct sockaddr*)&svr, sizeof(svr)) == 0)
{
bConnected = true;
args << (x - 1);
break;
}
}
closesocket(s);
}
}
if(bConnected == false)
{
WSACleanup();
MessageBoxA(0, "The address to the Silkroad servers could not be resolved. There are two causes for this error:\n\n1. The Silkroad servers are actually down and you will have to wait until they are up again.\n\n2.Your computer cannot obtain the address of the Silkroad servers. Please visit http://www.joymax.com/silkroad first and try launching the Loader again.\n\nIf that fails, try running Silkroad.exe and see if you get the Start button. If you get a Start button, try running the Loader again until it works. If you do not get a Start button, then there is an issue on your system preventing you from obtaining the addresses.", "Fatal Error", MB_ICONERROR);
return 0;
}
WSACleanup();
// Launch the client in a suspended state so we can patch it
std::string workingDir = pathToClient.substr(0, 1 + pathToClient.find_last_of("\\/"));
bool result = edx::CreateSuspendedProcess(pathToClient, args.str(), si, pi);
if(result == false)
{
MessageBoxA(0, "Could not start \"sro_client.exe\".", "Fatal Error", MB_ICONERROR);
return 0;
}
// Inject the DLL so we can have some fun
result = (FALSE != edx::InjectDLL(pi.hProcess, pathToDll.c_str(), "OnInject", static_cast<DWORD>(edx::GetEntryPoint(pathToClient.c_str())), false));
if(result == false)
{
TerminateThread(pi.hThread, 0);
MessageBoxA(0, "Could not inject into the Silkroad client process.", "Fatal Error", MB_ICONERROR);
return 0;
}
// Finally resume the client.
ResumeThread(pi.hThread);
ResumeThread(pi.hProcess);
// All done!
return 0;
}
If there are not any available values, we will create the file (done automatically by Windows when we write to the file) and allow the user to select the DLL to inject as well as the path to your Silkroad client. As mentioned before, we added in this code to make development easier. You just have to set the paths once and then you can continue to develop without having to worry about an annoying loader process. As you might notice in the application, we have to restart the program since functionality has changed a bit on Win7/Vista it seems.
After we have a valid path to the client and DLL to inject, our next task is to generate the command line to launch the client. The code you see simply checks to see if the address is obtainable and if it is, the command line is set to use that server. This is all the Silkroad loader does. We do not want to hard code a command line because when that login server is down, the users will get C9 errors and post like crazy about your program not working, as noticed in the past with older Silkroad utilities.
With the command line string built, we can now simply create our client process in a suspended state and inject our DLL. As discussed earlier, only the entry point of the process is patched in the client to let it load our DLL. All of the new logic and fun stuff comes in the DLL file. If our injection is successful, which it should be unless settings on your PC are preventing it, then we just reuse the client process and exit the loader.
That‘s all there is to our loader! That was pretty painless, right? Now we can look at our DLL, which is even simpler now. Here is the code for our DLL.cpp:
Code:
#include <windows.h>
#include "../common/common.h"
// Global instance handle to this DLL
HMODULE gInstance = NULL;
// Function prototype
void UserOnInject();
// Main DLL entry point
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReason, LPVOID lpReserved)
{
UNREFERENCED_PARAMETER(lpReserved);
if(ulReason == DLL_PROCESS_ATTACH)
{
gInstance = hModule;
// Do not notify this DLL of thread based events
DisableThreadLibraryCalls(hModule);
}
return TRUE;
}
// This is the main function that is called when the DLL is injected into the process
extern "C" __declspec(dllexport) void OnInject(DWORD address, LPDWORD bytes)
{
// Restore the original bytes at the OEP
DWORD wrote = 0;
WriteProcessMemory(GetCurrentProcess(), UlongToPtr(address), bytes, 6, &wrote);
// Call our user function to keep this function clean
UserOnInject();
}
// The function where we place all our logic
void UserOnInject()
{
// Create a debugging console
edx::CreateConsole("SilkroadFramework Debugging Console");
// Mutex for the launcher, no patches required to start Silkroad now
CreateMutexA(0, 0, "Silkroad Online Launcher");
CreateMutexA(0, 0, "Ready");
printf("Hello world from the SilkroadFramework DLL!\n");
}
With the Loader and the DLL code shown, it is time for the Common code that each project uses. This code is pretty much all you should need to do most of your client patching! Starting out with Common.h:
Code:
#pragma once
#include <windows.h>
#include <string>
namespace edx
{
// Injects a DLL into a process at the specified address
BOOL InjectDLL(HANDLE hProcess, const char * dllNameToLoad, const char * funcNameToLoad, DWORD injectAddress, bool bDebugAttach);
// Returns the entry point of an EXE
ULONGLONG GetEntryPoint(const char * filename);
// Returns a pointer to a hostent object for the specified address
hostent * GetHost(const char * address);
// Returns the absolute directory path of the executable
std::string GetAbsoluteDirectoryPath();
// Creates a suspended process
bool CreateSuspendedProcess(const std::string & filename, const std::string & fileargs, STARTUPINFOA & si, PROCESS_INFORMATION & pi);
// Returns true if the host is accessible
bool CanGetHostFromAddress(const std::string & address);
// Returns the writable directory for this framework.
// Type in "%appdata%/edxLabs" to access the directory.
std::string GetWriteableDirectory(std::string baseDir);
// Creates a codecave
BOOL CreateCodeCave(DWORD destAddress, BYTE patchSize, VOID (*function)(VOID));
// Patches bytes in the current process
BOOL WriteBytes(DWORD destAddress, LPVOID patch, DWORD numBytes);
// Reads bytes in the current process
BOOL ReadBytes(DWORD sourceAddress, LPVOID buffer, DWORD numBytes);
// Reads bytes of a process
BOOL ReadProcessBytes(HANDLE hProcess, DWORD destAddress, LPVOID buffer, DWORD numBytes);
// Writes bytes to a process
BOOL WriteProcessBytes(HANDLE hProcess, DWORD destAddress, LPVOID patch, DWORD numBytes);
// Creates a console, need to call FreeConsole before exit
VOID CreateConsole(CONST CHAR * winTitle);
// Forward declaration for the FileChooser class data so it is not exposed.
struct tFileChooserData;
// File chooser class
class FileChooser
{
private:
tFileChooserData* data;
public:
FileChooser();
~FileChooser();
// Sets the initial directory the file chooser looks in.
void SetInitialDirectory(const char * pDir);
// Sets the default dialog title of the file chooser component.
void SetDialogTitle(const char * pTitle);
// Sets the default filename in the file choose dialog.
void SetDefaultFileName(const char * pFileName);
// Adds a file browsing filter.
void AddFilter(const char * pFilterName, const char * pFilterExt);
// Allow the user to select a file. (open => true for param, save => false for param)
bool ShowChooseFile(bool open);
// Returns the file path of the selected file.
const char * GetSelectedFilePath();
// Returns the file directory of the selected file.
const char * GetSelectedFileDirectory();
// Returns the filename of the selected file.
const char * GetSelectedFileName();
// Returns the file title of the selected file.
const char * GetSelectedFileTitle();
// Returns the file extension of the selected file.
const char * GetSelectedFileExtension();
};
class ConfigFile
{
private:
std::string mFileName;
std::string mSection;
public:
ConfigFile();
~ConfigFile();
// Opens a file to work with
void Open(std::string filename, bool useCurrentPath, bool & fileExists);
// Set the section that the 'Write' and 'Read' functions use
void SetSection(const std::string & section);
// Get the section that the 'Write' and 'Read' functions use
std::string GetSection() const;
// Writes to the current section
void Write(const std::string & key, const std::string & data);
// Writes to any section
void WriteTo(const std::string & section, const std::string & key, const std::string & data);
// Read from the current section
std::string Read(const std::string & key);
// Read from any section
std::string ReadFrom(const std::string & section, const std::string & key);
};
}
Code:
#define _CRT_SECURE_NO_WARNINGS
#include "common.h"
#include <sstream>
#include <fstream>
#include <shlwapi.h>
#include <shlobj.h>
#include <io.h>
#include <fcntl.h>
#pragma comment(lib, "ws2_32.lib")
namespace edx
{
// Injects a DLL into a process at the specified address
BOOL InjectDLL(HANDLE hProcess, const char * dllNameToLoad, const char * funcNameToLoad, DWORD injectAddress, bool bDebugAttach)
{
// # of bytes to replace
DWORD byteCountToReplace = 6;
// Read in the original bytes that we will restore from the injected dll
BYTE userPatch[6] = {0};
memset(userPatch, 0x90, 6);
DWORD read = 0;
ReadProcessMemory(hProcess, UlongToPtr(injectAddress), userPatch, 6, &read);
// We want clear and byteRer to be local here
{
BYTE * clear = 0;
// Allocate a patch of NOPs to write over existing code
clear = (BYTE*)malloc(byteCountToReplace * sizeof(BYTE));
memset(clear, 0x90, byteCountToReplace* sizeof(BYTE));
// Clear out the original memory for a clean injection JMP to codecave
WriteProcessBytes(hProcess, injectAddress, clear, byteCountToReplace);
// Free the memory since we do not need it anymore
free(clear);
}
//------------------------------------------//
// Function variables. //
//------------------------------------------//
// Main DLL we will need to load
HMODULE kernel32 = NULL;
// Main functions we will need to import
FARPROC loadlibrary = NULL;
FARPROC getprocaddress = NULL;
FARPROC exitprocess = NULL;
// The workspace we will build the codecave on locally
LPBYTE workspace = NULL;
DWORD workspaceIndex = 0;
// The memory in the process we write to
LPVOID codecaveAddress = NULL;
DWORD dwCodecaveAddress = 0;
// Strings we have to write into the process
CHAR injectDllName[MAX_PATH + 1] = {0};
CHAR injectFuncName[MAX_PATH + 1] = {0};
CHAR injectError0[MAX_PATH + 1] = {0};
CHAR injectError1[MAX_PATH + 1] = {0};
CHAR injectError2[MAX_PATH + 1] = {0};
CHAR user32Name[MAX_PATH + 1] = {0};
CHAR msgboxName[MAX_PATH + 1] = {0};
// Placeholder addresses to use the strings
DWORD user32NameAddr = 0;
DWORD user32Addr = 0;
DWORD msgboxNameAddr = 0;
DWORD msgboxAddr = 0;
DWORD dllAddr = 0;
DWORD dllNameAddr = 0;
DWORD funcNameAddr = 0;
DWORD error0Addr = 0;
DWORD error1Addr = 0;
DWORD error2Addr = 0;
DWORD offsetOrigBytes = 0;
DWORD userVar = 0;
// Temp variables
DWORD dwTmpSize = 0;
// Where the codecave execution should begin at
DWORD codecaveExecAddr = 0;
//------------------------------------------//
// Variable initialization. //
//------------------------------------------//
// Get the address of the main DLL
kernel32 = LoadLibraryA("kernel32.dll");
// Get our functions
loadlibrary = GetProcAddress(kernel32, "LoadLibraryA");
getprocaddress = GetProcAddress(kernel32, "GetProcAddress");
exitprocess = GetProcAddress(kernel32, "ExitProcess");
// This section will cause compiler warnings on VS8,
// you can upgrade the functions or ignore them
// Build names
_snprintf(injectDllName, MAX_PATH, "%s", dllNameToLoad);
_snprintf(injectFuncName, MAX_PATH, "%s", funcNameToLoad);
_snprintf(user32Name, MAX_PATH, "user32.dll");
_snprintf(msgboxName, MAX_PATH, "MessageBoxA");
// Build error messages
_snprintf(injectError0, MAX_PATH, "Error");
_snprintf(injectError1, MAX_PATH, "Could not find the DLL \"%s\"", injectDllName);
_snprintf(injectError2, MAX_PATH, "Could not load the function \"%s\"", injectFuncName);
// Create the workspace
workspace = (LPBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024);
// Allocate space for the codecave in the process
codecaveAddress = VirtualAllocEx(hProcess, 0, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
dwCodecaveAddress = PtrToUlong(codecaveAddress);
//------------------------------------------//
// Data and string writing. //
//------------------------------------------//
// Write out the address for the user32 dll address
user32Addr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = 0;
memcpy(workspace + workspaceIndex, &dwTmpSize, 4);
workspaceIndex += 4;
// Write out the address for the MessageBoxA address
msgboxAddr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = 0;
memcpy(workspace + workspaceIndex, &dwTmpSize, 4);
workspaceIndex += 4;
// Write out the address for the injected DLL's module
dllAddr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = 0;
memcpy(workspace + workspaceIndex, &dwTmpSize, 4);
workspaceIndex += 4;
// Write out the original bytes to be restored
offsetOrigBytes = workspaceIndex + dwCodecaveAddress;
memcpy(workspace + workspaceIndex, userPatch, 6);
workspaceIndex += 6;
// Write out the address for the injected DLL's module
userVar = workspaceIndex + dwCodecaveAddress;
dwTmpSize = 0;
memcpy(workspace + workspaceIndex, &dwTmpSize, 4);
workspaceIndex += 4;
// User32 Dll Name
user32NameAddr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = (DWORD)strlen(user32Name) + 1;
memcpy(workspace + workspaceIndex, user32Name, dwTmpSize);
workspaceIndex += dwTmpSize;
// MessageBoxA name
msgboxNameAddr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = (DWORD)strlen(msgboxName) + 1;
memcpy(workspace + workspaceIndex, msgboxName, dwTmpSize);
workspaceIndex += dwTmpSize;
// Dll Name
dllNameAddr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = (DWORD)strlen(injectDllName) + 1;
memcpy(workspace + workspaceIndex, injectDllName, dwTmpSize);
workspaceIndex += dwTmpSize;
// Function Name
funcNameAddr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = (DWORD)strlen(injectFuncName) + 1;
memcpy(workspace + workspaceIndex, injectFuncName, dwTmpSize);
workspaceIndex += dwTmpSize;
// Error Message 1
error0Addr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = (DWORD)strlen(injectError0) + 1;
memcpy(workspace + workspaceIndex, injectError0, dwTmpSize);
workspaceIndex += dwTmpSize;
// Error Message 2
error1Addr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = (DWORD)strlen(injectError1) + 1;
memcpy(workspace + workspaceIndex, injectError1, dwTmpSize);
workspaceIndex += dwTmpSize;
// Error Message 3
error2Addr = workspaceIndex + dwCodecaveAddress;
dwTmpSize = (DWORD)strlen(injectError2) + 1;
memcpy(workspace + workspaceIndex, injectError2, dwTmpSize);
workspaceIndex += dwTmpSize;
// Pad a few INT3s after string data is written for separation
workspace[workspaceIndex++] = 0xCC;
workspace[workspaceIndex++] = 0xCC;
workspace[workspaceIndex++] = 0xCC;
// Store where the codecave execution should begin
codecaveExecAddr = workspaceIndex + dwCodecaveAddress;
if(bDebugAttach)
{
// For debugging - infinite loop, attach onto process and step over
workspace[workspaceIndex++] = 0xEB;
workspace[workspaceIndex++] = 0xFE;
}
// PUSHAD
workspace[workspaceIndex++] = 0x60;
//------------------------------------------//
// User32.dll loading. //
//------------------------------------------//
// User32 DLL Loading
// PUSH 0x00000000 - Push the address of the DLL name to use in LoadLibraryA
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &user32NameAddr, 4);
workspaceIndex += 4;
// MOV EAX, ADDRESS - Move the address of LoadLibraryA into EAX
workspace[workspaceIndex++] = 0xB8;
memcpy(workspace + workspaceIndex, &loadlibrary, 4);
workspaceIndex += 4;
// CALL EAX - Call LoadLibraryA
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// MessageBoxA Loading
// PUSH 0x000000 - Push the address of the function name to load
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &msgboxNameAddr, 4);
workspaceIndex += 4;
// Push EAX, module to use in GetProcAddress
workspace[workspaceIndex++] = 0x50;
// MOV EAX, ADDRESS - Move the address of GetProcAddress into EAX
workspace[workspaceIndex++] = 0xB8;
memcpy(workspace + workspaceIndex, &getprocaddress, 4);
workspaceIndex += 4;
// CALL EAX - Call GetProcAddress
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// MOV [ADDRESS], EAX - Save the address to our variable
workspace[workspaceIndex++] = 0xA3;
memcpy(workspace + workspaceIndex, &msgboxAddr, 4);
workspaceIndex += 4;
//------------------------------------------//
// Injected dll loading. //
//------------------------------------------//
// DLL Loading
// PUSH 0x00000000 - Push the address of the DLL name to use in LoadLibraryA
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &dllNameAddr, 4);
workspaceIndex += 4;
// MOV EAX, ADDRESS - Move the address of LoadLibraryA into EAX
workspace[workspaceIndex++] = 0xB8;
memcpy(workspace + workspaceIndex, &loadlibrary, 4);
workspaceIndex += 4;
// CALL EAX - Call LoadLibraryA
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// Error Checking
// CMP EAX, 0
workspace[workspaceIndex++] = 0x83;
workspace[workspaceIndex++] = 0xF8;
workspace[workspaceIndex++] = 0x00;
// JNZ EIP + 0x24 to skip over error code
workspace[workspaceIndex++] = 0x75;
workspace[workspaceIndex++] = 0x24;
// Error Code 1
// MessageBox
// PUSH 0x10 (MB_ICONHAND)
workspace[workspaceIndex++] = 0x6A;
workspace[workspaceIndex++] = 0x10;
// PUSH 0x000000 - Push the address of the MessageBox title
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &error0Addr, 4);
workspaceIndex += 4;
// PUSH 0x000000 - Push the address of the MessageBox message
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &error1Addr, 4);
workspaceIndex += 4;
// Push 0
workspace[workspaceIndex++] = 0x6A;
workspace[workspaceIndex++] = 0x00;
// MOV EAX, [ADDRESS] - Move the address of MessageBoxA into EAX
workspace[workspaceIndex++] = 0xA1;
memcpy(workspace + workspaceIndex, &msgboxAddr, 4);
workspaceIndex += 4;
// CALL EAX - Call MessageBoxA
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// FreeLibraryAndExitThread
// Push 0 (exit code)
workspace[workspaceIndex++] = 0x6A;
workspace[workspaceIndex++] = 0x00;
// PUSH [0x000000] - Push the address of the DLL module to unload
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0x35;
memcpy(workspace + workspaceIndex, &dllAddr, 4);
workspaceIndex += 4;
// MOV EAX, ADDRESS - Move the address of ExitProcess into EAX
workspace[workspaceIndex++] = 0xB8;
memcpy(workspace + workspaceIndex, &exitprocess, 4);
workspaceIndex += 4;
// CALL EAX - Call ExitProcess
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// Now we have the address of the injected DLL, so save the handle
// MOV [ADDRESS], EAX - Save the address to our variable
workspace[workspaceIndex++] = 0xA3;
memcpy(workspace + workspaceIndex, &dllAddr, 4);
workspaceIndex += 4;
// Load the initialize function from it
// PUSH 0x000000 - Push the address of the function name to load
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &funcNameAddr, 4);
workspaceIndex += 4;
// Push EAX, module to use in GetProcAddress
workspace[workspaceIndex++] = 0x50;
// MOV EAX, ADDRESS - Move the address of GetProcAddress into EAX
workspace[workspaceIndex++] = 0xB8;
memcpy(workspace + workspaceIndex, &getprocaddress, 4);
workspaceIndex += 4;
// CALL EAX - Call GetProcAddress
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// Error Checking
// CMP EAX, 0
workspace[workspaceIndex++] = 0x83;
workspace[workspaceIndex++] = 0xF8;
workspace[workspaceIndex++] = 0x00;
// JNZ EIP + 0x24 to skip error code
workspace[workspaceIndex++] = 0x75;
workspace[workspaceIndex++] = 0x1E;
// Error Code 2
// MessageBox
// PUSH 0x10 (MB_ICONHAND)
workspace[workspaceIndex++] = 0x6A;
workspace[workspaceIndex++] = 0x10;
// PUSH 0x000000 - Push the address of the MessageBox title
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &error0Addr, 4);
workspaceIndex += 4;
// PUSH 0x000000 - Push the address of the MessageBox message
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &error2Addr, 4);
workspaceIndex += 4;
// Push 0
workspace[workspaceIndex++] = 0x6A;
workspace[workspaceIndex++] = 0x00;
// MOV EAX, ADDRESS - Move the address of MessageBoxA into EAX
workspace[workspaceIndex++] = 0xA1;
memcpy(workspace + workspaceIndex, &msgboxAddr, 4);
workspaceIndex += 4;
// CALL EAX - Call MessageBoxA
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// ExitProcess
// Push 0 (exit code)
workspace[workspaceIndex++] = 0x6A;
workspace[workspaceIndex++] = 0x00;
// MOV EAX, ADDRESS - Move the address of ExitProcess into EAX
workspace[workspaceIndex++] = 0xB8;
memcpy(workspace + workspaceIndex, &exitprocess, 4);
workspaceIndex += 4;
// CALL EAX - Call ExitProcess
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// Now that we have the address of the function, we cam call it,
// if there was an error, the messagebox would be called as well.
// Push orig bytes address
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &offsetOrigBytes, sizeof(offsetOrigBytes));
workspaceIndex += 4;
// Push the address to restore bytes to
workspace[workspaceIndex++] = 0x68;
memcpy(workspace + workspaceIndex, &injectAddress, sizeof(injectAddress));
workspaceIndex += 4;
// CALL EAX - Call Initialize
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0xD0;
// Add ESP, 8 2 parameters
workspace[workspaceIndex++] = 0x83;
workspace[workspaceIndex++] = 0xC4;
workspace[workspaceIndex++] = 0x08;
workspace[workspaceIndex++] = 0x90;
// Restore registers with POPAD
workspace[workspaceIndex++] = 0x61;
// Pad a few NOPS before user code
workspace[workspaceIndex++] = 0x90;
workspace[workspaceIndex++] = 0x90;
// Pop off into local variable
workspace[workspaceIndex++] = 0x8F;
workspace[workspaceIndex++] = 0x05;
memcpy(workspace + workspaceIndex, &userVar, sizeof(userVar));
workspaceIndex += 4;
// Subtract 5 from local var
workspace[workspaceIndex++] = 0x83;
workspace[workspaceIndex++] = 0x2D;
memcpy(workspace + workspaceIndex, &userVar, sizeof(userVar));
workspaceIndex += 4;
workspace[workspaceIndex++] = 0x05;
// Pop off into local variable
workspace[workspaceIndex++] = 0xFF;
workspace[workspaceIndex++] = 0x35;
memcpy(workspace + workspaceIndex, &userVar, sizeof(userVar));
workspaceIndex += 4;
// Return back to where we should be
workspace[workspaceIndex++] = 0xC3;
// Try to write the final codecave patch to the process first.
// By doing this, worst case event is we add code to the process that is not used
if(!WriteProcessBytes(hProcess, PtrToUlong(codecaveAddress), workspace, workspaceIndex))
{
HeapFree(GetProcessHeap(), 0, workspace);
return FALSE;
}
// Now that the patch is written into the process, we need to make the process call it
{
// Make the program patch the starting address to inject the DLL
BYTE patch2[5] = {0xE8, 0x00, 0x00, 0x00, 0x00};
// Calculate the JMP offset ( + 5 for the FAR JMP we are adding)
DWORD toCC = codecaveExecAddr - (injectAddress + 5);
// Free the workspace memory
HeapFree(GetProcessHeap(), 0, workspace);
// Write the offset to the patch
memcpy(patch2 + 1, &toCC, sizeof(toCC));
// Make the patch that will JMP to the codecave
if(!WriteProcessBytes(hProcess, injectAddress, patch2, 5))
{
return FALSE;
}
}
// Success!
return TRUE;
}
// Returns the entry point of an EXE
ULONGLONG GetEntryPoint(const char * filename)
{
// Macro for adding pointers/DWORDs together without C arithmetic interfering
#define MakePtr( cast, ptr, addValue ) (cast)( (DWORD)(ptr)+(DWORD)(addValue))
ULONGLONG OEP = 0;
HANDLE hFile = NULL;
HANDLE hFileMapping = NULL;
PIMAGE_DOS_HEADER dosHeader = {0};
PBYTE g_pMappedFileBase = NULL;
PIMAGE_FILE_HEADER pImgFileHdr = NULL;
hFile = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if(hFile == INVALID_HANDLE_VALUE)
return 0;
hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if(hFileMapping == 0)
{
CloseHandle(hFile);
return 0;
}
g_pMappedFileBase = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
if(g_pMappedFileBase == 0)
{
CloseHandle(hFileMapping);
CloseHandle(hFile);
return 0;
}
dosHeader = (PIMAGE_DOS_HEADER)g_pMappedFileBase;
pImgFileHdr = (PIMAGE_FILE_HEADER)g_pMappedFileBase;
if(dosHeader->e_magic == IMAGE_DOS_SIGNATURE)
{
PIMAGE_NT_HEADERS pNTHeader = MakePtr( PIMAGE_NT_HEADERS, dosHeader, dosHeader->e_lfanew);
PIMAGE_NT_HEADERS64 pNTHeader64 = (PIMAGE_NT_HEADERS64)pNTHeader;
// First, verify that the e_lfanew field gave us a reasonable pointer, then verify the PE signature.
if(IsBadReadPtr(pNTHeader, sizeof(pNTHeader->Signature)) || pNTHeader->Signature != IMAGE_NT_SIGNATURE)
{
UnmapViewOfFile(g_pMappedFileBase);
CloseHandle(hFileMapping);
CloseHandle(hFile);
return 0;
}
if(pNTHeader->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
OEP = pNTHeader64->OptionalHeader.AddressOfEntryPoint + pNTHeader64->OptionalHeader.ImageBase;
else
OEP = pNTHeader->OptionalHeader.AddressOfEntryPoint + pNTHeader->OptionalHeader.ImageBase;
}
UnmapViewOfFile(g_pMappedFileBase);
CloseHandle(hFileMapping);
CloseHandle(hFile);
return OEP;
#undef MakePtr
}
// Returns a pointer to a hostent object for the specified address
hostent * GetHost(const char * address)
{
if(inet_addr(address) == INADDR_NONE)
{
return gethostbyname(address);
}
else
{
unsigned long addr = 0;
addr = inet_addr(address);
return gethostbyaddr((char *)&addr, sizeof(addr), AF_INET);
}
}
// Returns the absolute directory path of the executable
std::string GetAbsoluteDirectoryPath()
{
char tmpDirectory1[MAX_PATH + 1] = {0};
char tmpDirectory2[MAX_PATH + 1] = {0};
GetCurrentDirectoryA(MAX_PATH, tmpDirectory1);
GetFullPathNameA(tmpDirectory1, MAX_PATH, tmpDirectory2, 0);
return (std::string(tmpDirectory2) + std::string("\\"));
}
// Creates a suspended process
bool CreateSuspendedProcess(const std::string & filename, const std::string & fileargs, STARTUPINFOA & si, PROCESS_INFORMATION & pi)
{
si.cb = sizeof(STARTUPINFOA);
std::stringstream cmdLine;
cmdLine << "\"" << filename << "\" " << fileargs;
BOOL result = CreateProcessA(0, (LPSTR)cmdLine.str().c_str(), 0, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
return (result != 0);
}
// Returns true if the host is accessible
bool CanGetHostFromAddress(const std::string & address)
{
return (GetHost(address.c_str()) != NULL);
}
// Returns the writable directory for this framework.
// Type in "%appdata%/edxLabs" to access the directory.
std::string GetWriteableDirectory(std::string baseDir)
{
std::stringstream ss;
char strPath[MAX_PATH + 1] = {0};
if(SHGetSpecialFolderPathA(0, strPath, CSIDL_APPDATA, FALSE) == FALSE)
{
MessageBoxA(0, "Unable to retrieve the path to the special folder AppData", "Error", MB_ICONERROR);
throw 0;
}
else
{
ss << strPath << "\\edxLabs";
CreateDirectoryA(ss.str().c_str(), 0);
ss << "\\" << baseDir;
CreateDirectoryA(ss.str().c_str(), 0);
ss << "\\";
}
return ss.str();
}
//--------------------------------------------------------------------------------
// Private data for the FileChooser class
struct tFileChooserData
{
// File chooser struct
OPENFILENAMEA fn;
// Buffers for the file chooser
char setDirectory[2048];
char setDialogTitle[2048];
char setDefFileName[2048];
char setFilter[2048];
// User buffers to store data
char filepath[2048];
char filetitle[2048];
char filename[2048];
char fileext[2048];
char filedir[2048];
// Index in the filter string since we have to manually create it
int filterIndex;
// Can we use the ShowChooseFile function?
bool canShow;
};
// Constructor
FileChooser::FileChooser()
{
data = new tFileChooserData;
memset(data, 0, sizeof(tFileChooserData));
// Have to set this for the struct
data->fn.lStructSize = sizeof(OPENFILENAME);
// We can select a file
data->canShow = true;
}
// Destructor
FileChooser::~FileChooser()
{
delete data;
}
// Sets the initial directory the file chooser looks in
void FileChooser::SetInitialDirectory(const char * pDir)
{
_snprintf(data->setDirectory, 2047, "%s", pDir);
}
// Sets the default dialog title of the file chooser
void FileChooser::SetDialogTitle(const char * pTitle)
{
_snprintf(data->setDialogTitle, 2047, "%s", pTitle);
}
// Sets the default data->filename in the file choose dialog
void FileChooser::SetDefaultFileName(const char * pFileName)
{
_snprintf(data->setDefFileName, 2047, "%s", pFileName);
}
// Adds a file browsing filter
// pFilterName - Name of the extension to display, i.e. "Executable Files"
// pFilterExt - Extension of the filter, i.e. "*.exe"
void FileChooser::AddFilter(const char * pFilterName, const char * pFilterExt)
{
// First part is the name of the filter
_snprintf(data->setFilter + data->filterIndex, 2047 - data->filterIndex, "%s", pFilterName);
data->filterIndex += (int)strlen(pFilterName);
// Separate with a NULL terminator
data->setFilter[data->filterIndex++] = '\0';
// Second part is the extension of the filter, *.EXTENSION
_snprintf(data->setFilter + data->filterIndex, 2047 - data->filterIndex, "%s", pFilterExt);
data->filterIndex += (int)strlen(pFilterExt);
// Separate with a NULL terminator
data->setFilter[data->filterIndex++] = '\0';
}
// Will return a string in this format: "Filename"
const char * FileChooser::GetSelectedFileTitle()
{
return data->filetitle;
}
// Will return a string in this format: "Filename.Extension"
const char * FileChooser::GetSelectedFileName()
{
return data->filename;
}
// Will return a string in this format: "Drive:\Path\To\File\Filename.Extension"
const char * FileChooser::GetSelectedFilePath()
{
return data->filepath;
}
// Will return a string in this format: "Drive:\Path\To\File\"
const char * FileChooser::GetSelectedFileDirectory()
{
return data->filedir;
}
// Will return a string in this format: "Extension"
const char * FileChooser::GetSelectedFileExtension()
{
return data->fileext;
}
// Allow the user to select a file, returns true on success and false on failure
bool FileChooser::ShowChooseFile(bool open)
{
// If we cannot show the file dialog, return failure
if(!data->canShow)
return false;
// Store the current directory before we change it
char curDir[256] = {0};
GetCurrentDirectoryA(255, curDir);
// Have to set this for the struct
data->fn.lStructSize = sizeof(OPENFILENAME);
// Default directory
data->fn.lpstrInitialDir = data->setDirectory;
// Finish the file filter with the two NULLS it needs at the end
data->setFilter[data->filterIndex++] = '\0';
data->setFilter[data->filterIndex++] = '\0';
data->fn.lpstrFilter = data->setFilter;
// Tell the chooser to use our buffer
data->fn.lpstrFile = data->setDefFileName;
// Max size of buffer
data->fn.nMaxFile = 2047;
// Tell the chooser to use our buffer
data->fn.lpstrFileTitle = data->filename;
// Max size of buffer
data->fn.nMaxFileTitle = 2047;
// Title we wish to display
data->fn.lpstrTitle = data->setDialogTitle;
// Display the chooser!
if(open)
{
// Flags for selecting a file
data->fn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
if(!GetOpenFileNameA(&data->fn))
{
// Restore the old directory
SetCurrentDirectoryA(curDir);
// Error or the user canceled
return FALSE;
}
}
else
{
if(!GetSaveFileNameA(&data->fn))
{
// Restore the old directory
SetCurrentDirectoryA(curDir);
// Error or the user canceled
return FALSE;
}
}
// Restore the old directory
SetCurrentDirectoryA(curDir);
// Copy the file path from the struct into the buffer
_snprintf(data->filepath, 2047, data->fn.lpstrFile);
// Store the file directory
_snprintf(data->filedir, 2047, "%s", data->fn.lpstrFile);
// Loop though the string backwards
for(int x = (int)strlen(data->filedir) - 1; x >= 0; --x)
{
// Stop at the first directory separator
if(data->filedir[x] == '\\')
{
// Remove everything after it by setting a NULL terminator in the string
data->filedir[x + 1] = 0;
break;
}
}
// Store the filetitle
_snprintf(data->filetitle, 2047, "%s", data->filename);
// Loop though the string forwards
for(int x = (int)strlen(data->filetitle) - 1; x > 0; --x)
{
// Stop at the last extension separator
if(data->filetitle[x] == '.')
{
// Remove it and everything past it
data->filetitle[x] = 0;
break;
}
}
// Temp buffer to store the filename with extension
char temp[2048] = {0};
_snprintf(temp, 2047, "%s", GetSelectedFileName());
strcpy(data->fileext, temp + strlen(data->filetitle) + 1);
// We can no longer use this function again
data->canShow = false;
// Success
return TRUE;
}
//--------------------------------------------------------------------------------
ConfigFile::ConfigFile()
{
// Set a default section name
mSection = "Default";
}
ConfigFile::~ConfigFile()
{
}
// Opens a file to work with
void ConfigFile::Open(std::string filename, bool useCurrentPath, bool & fileExists)
{
// If we do not need to get the current file path, simply assign the path
if(!useCurrentPath)
{
mFileName = filename;
}
// Otherwise build the path
else
{
// Holds the current directory
char curDir[MAX_PATH + 1] = {0};
char fullDir[MAX_PATH + 1] = {0};
// Store the current directory
GetCurrentDirectoryA(MAX_PATH, curDir);
// Store the full path to the current directory
GetFullPathNameA(curDir, MAX_PATH, fullDir, 0);
// Build the filename now
mFileName = fullDir;
mFileName.append("\\");
mFileName.append(filename);
}
// If there is no filename, the file does not exist
if(!mFileName.size())
{
fileExists = false;
}
else
{
// File handle
std::ifstream inFile;
// Try to open the file
inFile.open(filename.c_str());
// Check to see if it is open or not
fileExists = inFile.is_open();
// Close the file
inFile.close();
}
// Force the system to read the mapping into shared memory, so that future invocations of the application will see it without the user having to reboot the system
WritePrivateProfileStringA(NULL, NULL, NULL, filename.c_str());
}
// Set the section that the 'Write' and 'Read' functions use
void ConfigFile::SetSection(const std::string& section)
{
mSection = section;
}
// Get the section that the 'Write' and 'Read' functions use
std::string ConfigFile::GetSection() const
{
return mSection;
}
// Writes to the current section
void ConfigFile::Write(const std::string& key, const std::string& data)
{
WritePrivateProfileStringA(mSection.c_str(), key.c_str(), data.c_str(), mFileName.c_str());
}
// Writes to any section
void ConfigFile::WriteTo(const std::string& section, const std::string& key, const std::string& data)
{
WritePrivateProfileStringA(section.c_str(), key.c_str(), data.c_str(), mFileName.c_str());
}
// Read from the current section
std::string ConfigFile::Read(const std::string& key)
{
static char buffer[131072] = {0};
SecureZeroMemory(buffer, 131072);
GetPrivateProfileStringA(mSection.c_str(), key.c_str(), NULL, buffer, 131071, mFileName.c_str());
return std::string(buffer);
}
// Read from any section
std::string ConfigFile::ReadFrom(const std::string& section, const std::string& key)
{
static char buffer[131072] = {0};
SecureZeroMemory(buffer, 131072);
GetPrivateProfileStringA(section.c_str(), key.c_str(), NULL, buffer, 131071, mFileName.c_str());
return std::string(buffer);
}
//--------------------------------------------------------------------------------
// Writes bytes to a process
BOOL WriteProcessBytes(HANDLE hProcess, DWORD destAddress, LPVOID patch, DWORD numBytes)
{
DWORD oldProtect = 0; // Old protection on page we are writing to
DWORD bytesRet = 0; // # of bytes written
BOOL status = TRUE; // Status of the function
// Change page protection so we can write executable code
if(!VirtualProtectEx(hProcess, UlongToPtr(destAddress), numBytes, PAGE_EXECUTE_READWRITE, &oldProtect))
return FALSE;
// Write out the data
if(!WriteProcessMemory(hProcess, UlongToPtr(destAddress), patch, numBytes, &bytesRet))
status = FALSE;
// Compare written bytes to the size of the patch
if(bytesRet != numBytes)
status = FALSE;
// Restore the old page protection
if(!VirtualProtectEx(hProcess, UlongToPtr(destAddress), numBytes, oldProtect, &oldProtect))
status = FALSE;
// Make sure changes are made!
if(!FlushInstructionCache(hProcess, UlongToPtr(destAddress), numBytes))
status = FALSE;
// Return the final status, note once we set page protection, we don't want to prematurely return
return status;
}
// Reads bytes of a process
BOOL ReadProcessBytes(HANDLE hProcess, DWORD destAddress, LPVOID buffer, DWORD numBytes)
{
DWORD oldProtect = 0; // Old protection on page we are writing to
DWORD bytesRet = 0; // # of bytes written
BOOL status = TRUE; // Status of the function
// Change page protection so we can read bytes
if(!VirtualProtectEx(hProcess, UlongToPtr(destAddress), numBytes, PAGE_READONLY, &oldProtect))
return FALSE;
// Read in the data
if(!ReadProcessMemory(hProcess, UlongToPtr(destAddress), buffer, numBytes, &bytesRet))
status = FALSE;
// Compare written bytes to the size of the patch
if(bytesRet != numBytes)
status = FALSE;
// Restore the old page protection
if(!VirtualProtectEx(hProcess, UlongToPtr(destAddress), numBytes, oldProtect, &oldProtect))
status = FALSE;
// Return the final status, note once we set page protection, we don't want to prematurely return
return status;
}
// Patches bytes in the current process
BOOL WriteBytes(DWORD destAddress, LPVOID patch, DWORD numBytes)
{
// Store old protection of the memory page
DWORD oldProtect = 0;
// Store the source address
DWORD srcAddress = PtrToUlong(patch);
// Result of the function
BOOL result = TRUE;
// Make sure page is writable
result = result && VirtualProtect(UlongToPtr(destAddress), numBytes, PAGE_EXECUTE_READWRITE, &oldProtect);
// Copy over the patch
memcpy(UlongToPtr(destAddress), patch, numBytes);
// Restore old page protection
result = result && VirtualProtect(UlongToPtr(destAddress), numBytes, oldProtect, &oldProtect);
// Make sure changes are made
result = result && FlushInstructionCache(GetCurrentProcess(), UlongToPtr(destAddress), numBytes);
// Return the result
return result;
}
// Reads bytes in the current process
BOOL ReadBytes(DWORD sourceAddress, LPVOID buffer, DWORD numBytes)
{
// Store old protection of the memory page
DWORD oldProtect = 0;
// Store the source address
DWORD dstAddress = PtrToUlong(buffer);
// Result of the function
BOOL result = TRUE;
// Make sure page is writable
result = result && VirtualProtect(UlongToPtr(sourceAddress), numBytes, PAGE_EXECUTE_READWRITE, &oldProtect);
// Copy over the patch
memcpy(buffer, UlongToPtr(sourceAddress), numBytes);
// Restore old page protection
result = result && VirtualProtect(UlongToPtr(sourceAddress), numBytes, oldProtect, &oldProtect);
// Return the result
return result;
}
// Creates a codecave
BOOL CreateCodeCave(DWORD destAddress, BYTE patchSize, VOID (*function)(VOID))
{
// Offset to make the codecave at
DWORD offset = 0;
// Bytes to write
BYTE patch[5] = {0};
// Number of extra nops we need
BYTE nopCount = 0;
// NOP buffer
static BYTE nop[0xFF] = {0};
// Is the buffer filled?
static BOOL filled = FALSE;
// Need at least 5 bytes to be patched
if(patchSize < 5)
return FALSE;
// Calculate the code cave
offset = (PtrToUlong(function) - destAddress) - 5;
// Construct the patch to the function call
patch[0] = 0xE8;
memcpy(patch + 1, &offset, sizeof(DWORD));
WriteBytes(destAddress, patch, 5);
// We are done if we do not have NOPs
nopCount = patchSize - 5;
if(nopCount == 0)
return TRUE;
// Fill in the buffer
if(filled == FALSE)
{
memset(nop, 0x90, 0xFF);
filled = TRUE;
}
// Make the patch now
WriteBytes(destAddress + 5, nop, nopCount);
// Success
return TRUE;
}
// Creates a console, need to call FreeConsole before exit
VOID CreateConsole(CONST CHAR * winTitle)
{
// http://www.gamedev.net/community/forums/viewreply.asp?ID=1958358
INT hConHandle = 0;
HANDLE lStdHandle = 0;
FILE *fp = 0 ;
// Allocate the console
AllocConsole();
// Set a title if we need one
if(winTitle) SetConsoleTitleA(winTitle);
// redirect unbuffered STDOUT to the console
lStdHandle = GetStdHandle(STD_OUTPUT_HANDLE);
hConHandle = _open_osfhandle(PtrToUlong(lStdHandle), _O_TEXT);
fp = _fdopen(hConHandle, "w");
*stdout = *fp;
setvbuf(stdout, NULL, _IONBF, 0);
// redirect unbuffered STDIN to the console
lStdHandle = GetStdHandle(STD_INPUT_HANDLE);
hConHandle = _open_osfhandle(PtrToUlong(lStdHandle), _O_TEXT);
fp = _fdopen(hConHandle, "r");
*stdin = *fp;
setvbuf(stdin, NULL, _IONBF, 0);
// redirect unbuffered STDERR to the console
lStdHandle = GetStdHandle(STD_ERROR_HANDLE);
hConHandle = _open_osfhandle(PtrToUlong(lStdHandle), _O_TEXT);
fp = _fdopen(hConHandle, "w");
*stderr = *fp;
setvbuf(stderr, NULL, _IONBF, 0);
}
}
As mentioned before, I said we would be adding some extra code to make life easier. That is what the code for the Config and FileChooser classes represent. Rather than having to create files ourselves and telling end users to do the same, we can make use of this code to efficiently develop and test our works. At any time if we want to choose a different client or DLL, we would need to modify or delete the SilkroadFramework.ini configuration file located in the “%appdata%/edxLabs/SilkroadFramework” folder.
Now, after you copy the code into the files for your project, you should be able to compile and run. Make sure o set the active startup project to the Loader as we can’t run DLLs from Visual Studio. To do this, right click on the Loader project and choose Set as StartUp Project. When the program first runs, you will need to select your DLL file. This will be located in the Debug folder of the Solution, not the actual Project’s Debug folder. Once you have selected the DLL, you will next need to select your Silkroad client, sro_client.exe. Do so and then rerun the loader after the paths are set.
Silkroad should startup and bring you to the login screen just like having launched Silkroad.exe would have. Congratulations! You made it through one of the most perceived hardest tasks in getting started with Silkroad development. Go ahead and close the game unless you plan on logging in or doing other things.
There is one last thing I need to mention. Before you get too far with playing with anything, please checkout my article: The Beginners Guide to Codecaves. I wrote this article a few years back and I highly recommend users read this to get a head start on some of the things we will be doing in the future. The more work you are willing to put into this stuff and learn, the better you will get at it.
V. Conclusion
By this point, you should have a basic understanding of how a Loader and injected DLL work. As you might have noticed, there is nothing really special about the process except for the method that I choose to use to inject the DLL itself, which is a bit advanced compared to traditional means, but a reliable approach that will serve us well.
With the ability to the start the client via your own Loader and inject a custom DLL as well as having access to some basic functions to make patches and codecaves, you know have the basic tools required to have some real fun with the Silkroad client. That fun will come later, but rest assured, it is on the way.
That wraps up this article. We needed to establish a common framework to use in our future endeavors, so that was the task we accomplished here. I hope you found this guide informational and beneficial. Stay tuned for future guides!
Drew “pushedx” Benton
edxLabs






