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