Tutorial How to Pass multiple arguments With CreateRemoteThread To Injected DLL

Hexui Undetected CSGO Cheats Sinkicheat PUBG Cheat

Rake

Cesspool Admin
Administrator
Jan 21, 2014
12,151
78,998
2,396
How long you been coding/hacking?
5 years
As it relates to a standard injector, you can't really pass any arguments to your DLL in a simple way. When you're injecting you use CreateRemoteThread() to call LoadLibrary() and you pass 1 variable which is the path to the DLL you want to load, which is a null terminated char array. In our standard injector we use WriteProcessMemory() to write this path to the target process.

You can only pass the 1 argument.

What if we want to pass multiple args? It's not possible with a simple call to CreateRemoteThread(). I have seen this question 100 times on Stack Overflow so I wanted to come up with some sort of solution. Many people have said you can pass a pointer to a structure but this is FALSE, despite being selected as best answer on multiple SO questions.

See my answer for why passing a struct doesn't work if you're interested:
If the function has more than one parameter it is not possible to pass them to the function being called by CreateRemoteThread() without using shellcode.

Passing a pointer to a structure or an array of arguments will not work.

The first argument will get passed correctly, the other arguments will exist in the memory you wrote them too, but they will not be placed into registers or the stack where they are required for the calling convention to properly access them.

If the function takes 2 arguments and you pass a pointer to a structure (as I have seen mentioned in other answers) the first argument will be placed on the stack or in a register correctly, but when the function tries to access the subsequent arguments, it will just pull whatever data was on the stack or inside the registers prior to that.

Essentially it will consider this junk data to be the arguments.

The only way to properly get the arguments where they need to be is to write shellcode to the process which loads the arguments into the proper registers and stack before performing a call or jmp to the target function.

You can easily test this by trying to perform either of these with CreateRemoteThread:
C++:
MessageBoxA(0,0,0,0);
Beep(500, 500);
You can follow the assembly yourself and easily see the problem, at no point does the assembly ever attempt to touch the addresses following the first argument. Instead it simply touches the data in the positions where the arguments are supposed to be (on the stack and in the registers, not in your structure you wrote to memory).

If your function takes a pointer to a structure, then the methods provided in the other answers will work.

Keep in mind, the correct way to do inter-process communication is to use named pipes. This is just me screwing around trying to come up with a solution that even a noob could understand (hopefully).

We can abuse the fact that LoadLibrary() will get the char array and stop once it reaches the null terminator, and write our arguments after the string. In this way we only have 1 call to WriteProcessMemory. Then inside our DLL we can access the args by offsetting past the char array to access the args.

But how do you access the args from inside the DLL?

My first thought was, somehow you can get the address where we wrote the DLL path from inside DllMain:

C++:
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
        {
            thisDLL = hModule;
        }
    }

    return TRUE;
}
But you can't. (if someone has a trick for doing this, I would love to hear it, besides pattern scanning for it)

So I had to come up with a slightly different solution, which uses 1 call to WPM and 2 calls to CRT:
  1. Write DLL path & arguments to target process with 1 call to WPM
  2. Inject DLL that does nothing during DLL_PROCESS_ATTACH, using CRT
  3. Export a init() function that takes a pointer to an arg structure
  4. Use CRT again to call your init() function, passing the address of your arg structure
  5. Your Init() function parses the args and then calls your real hack init() function
Injector Header:
C++:
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <string>

uintptr_t GetModuleBaseAddress(DWORD procId, const char* modName)
{
    uintptr_t modBaseAddr = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procId);
    if (hSnap != INVALID_HANDLE_VALUE)
    {
        MODULEENTRY32 modEntry;
        modEntry.dwSize = sizeof(modEntry);
        if (Module32First(hSnap, &modEntry))
        {
            do
            {
                if (!_stricmp(modEntry.szModule, modName))
                {
                    modBaseAddr = (uintptr_t)modEntry.modBaseAddr;
                    break;
                }
            } while (Module32Next(hSnap, &modEntry));
        }
    }
    CloseHandle(hSnap);
    return modBaseAddr;
}

DWORD GetProcId(const char* procName)
{
    DWORD procId = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (hSnap != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32 procEntry;
        procEntry.dwSize = sizeof(procEntry);

        if (Process32First(hSnap, &procEntry))
        {
            do
            {
                if (!_stricmp(procEntry.szExeFile, procName))
                {
                    procId = procEntry.th32ProcessID;
                    break;
                }
            } while (Process32Next(hSnap, &procEntry));
        }
    }
    CloseHandle(hSnap);
    return procId;
}

#define ReCa reinterpret_cast
//iPower's function
uintptr_t GetProcAddressEx(HANDLE hProcess, DWORD pid, const char* module, const char* function)
{
    if (!module || !function || !pid || !hProcess)
        return 0;

    uintptr_t moduleBase = GetModuleBaseAddress(pid, module); //toolhelp32snapshot method

    if (!moduleBase)
        return 0;

    IMAGE_DOS_HEADER Image_Dos_Header = { 0 };

    if (!ReadProcessMemory(hProcess, ReCa<LPCVOID>(moduleBase), &Image_Dos_Header, sizeof(IMAGE_DOS_HEADER), nullptr))
        return 0;

    if (Image_Dos_Header.e_magic != IMAGE_DOS_SIGNATURE)
        return 0;

    IMAGE_NT_HEADERS Image_Nt_Headers = { 0 };

    if (!ReadProcessMemory(hProcess, ReCa<LPCVOID>(moduleBase + Image_Dos_Header.e_lfanew), &Image_Nt_Headers, sizeof(IMAGE_NT_HEADERS), nullptr))
        return 0;

    if (Image_Nt_Headers.Signature != IMAGE_NT_SIGNATURE)
        return 0;

    IMAGE_EXPORT_DIRECTORY Image_Export_Directory = { 0 };
    uintptr_t img_exp_dir_rva = 0;

    if (!(img_exp_dir_rva = Image_Nt_Headers.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress))
        return 0;

    if (!ReadProcessMemory(hProcess, ReCa<LPCVOID>(moduleBase + img_exp_dir_rva), &Image_Export_Directory, sizeof(IMAGE_EXPORT_DIRECTORY), nullptr))
        return 0;

    uintptr_t EAT = moduleBase + Image_Export_Directory.AddressOfFunctions;
    uintptr_t ENT = moduleBase + Image_Export_Directory.AddressOfNames;
    uintptr_t EOT = moduleBase + Image_Export_Directory.AddressOfNameOrdinals;

    WORD ordinal = 0;
    SIZE_T len_buf = strlen(function) + 1;
    char* temp_buf = new char[len_buf];

    for (size_t i = 0; i < Image_Export_Directory.NumberOfNames; i++)
    {
        uintptr_t tempRvaString = 0;

        if (!ReadProcessMemory(hProcess, ReCa<LPCVOID>(ENT + (i * sizeof(uintptr_t))), &tempRvaString, sizeof(uintptr_t), nullptr))
            return 0;

        if (!ReadProcessMemory(hProcess, ReCa<LPCVOID>(moduleBase + tempRvaString), temp_buf, len_buf, nullptr))
            return 0;

        if (!lstrcmpi(function, temp_buf))
        {
            if (!ReadProcessMemory(hProcess, ReCa<LPCVOID>(EOT + (i * sizeof(WORD))), &ordinal, sizeof(WORD), nullptr))
                return 0;

            uintptr_t temp_rva_func = 0;

            if (!ReadProcessMemory(hProcess, ReCa<LPCVOID>(EAT + (ordinal * sizeof(uintptr_t))), &temp_rva_func, sizeof(uintptr_t), nullptr))
                return 0;

            delete[] temp_buf;
            return moduleBase + temp_rva_func;
        }
    }
    delete[] temp_buf;
    return 0;
}
Injector Main:
C++:
struct ArgStruct
{
    const char dllPath[MAX_PATH] = "C:\\Users\\User\\Desktop\\Injector Multiple Args\\TestDLL\\Debug\\TestDLL.dll";

    //example args
    int num = 3;
    char text[5] = "dank";
};

int main()
{
    ArgStruct argstruct;
    DWORD procId = 0;

    while (!procId)
    {
        procId = GetProcId("ac_client.exe");
        Sleep(30);
    }

    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, 0, procId);

    if (hProc && hProc != INVALID_HANDLE_VALUE)
    {
        void* argsAddr = VirtualAllocEx(hProc, 0, MAX_PATH + sizeof(argstruct), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

        WriteProcessMemory(hProc, argsAddr, &argstruct, sizeof(argstruct), 0);

        HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, argsAddr, 0, 0);

        Sleep(500);

        void* fnPtr = (void*)GetProcAddressEx(hProc, procId, "TestDLL.dll", "Init1");

        hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)fnPtr, argsAddr, 0, 0);

        if (hThread)
            CloseHandle(hThread);
    }

    if (hProc)
        CloseHandle(hProc);

    return 0;
}
DLL Main:
C++:
#include <windows.h>
#include <iostream>

HMODULE thisDLL = 0;

struct ArgStruct
{
    const char dllPath[MAX_PATH];

    //example args
    int num;
    char text[5];
};

ArgStruct* args;

void Init2(HMODULE hModule)
{
    //your actual hack would be in here

    AllocConsole();
    FILE* f;
    freopen_s(&f, "CONOUT$", "w", stdout);

    std::cout << "Injected\n";
    std::cout << "num = " << args->num << std::endl;
    std::cout << "text = " << args->text << std::endl;
}

extern "C" __declspec(dllexport) DWORD __stdcall Init1(ArgStruct * argstruct)
{
    args = argstruct;
    Init2(thisDLL);
    return 0;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
            thisDLL = hModule;
    }
    return TRUE;
}
What your arg struct looks like in memory:
1593283132009.png


DLL outputting the args you passed:
1593282587129.png


Yay we did it :FeelsGoodMan:

Unmangled function symbols

?Init1@@YAXPAUArgStruct@d‰3à

Not nice. You can change project settings and use different function definitions but they all end up with retarded symbol names, especially when changing calling convention.

The solution is to use a module definition file:

Right click project properties -> Add New Item -> Search for "definition"

inside the definition file:
Code:
LIBRARY
EXPORTS
Init1=Init1
Now it's never mangled

Thank you @iPower for the GetProcAddressEx function
 

Attachments

Last edited:

Rake

Cesspool Admin
Administrator
Jan 21, 2014
12,151
78,998
2,396
I'm injecting a DLL, which exports a function.

I want to call the exported function using an external application.

my exported function:
C++:
extern "C" __declspec(dllexport) DWORD __stdcall Init1(ArgStruct * argstruct)
{
    args = argstruct;

    Init2(thisDLL);
    return 0;
}
Trying to call the function:
C++:
void* fnPtr = (void*)GetProcAddressEx(hProc, procId, "TestDLL.dll", "Init1");

hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)fnPtr, argsAddr, 0, 0);
The function never executes, what am I doing wrong?
  • fnPtr is good
  • CRT returns a thread handle
  • GetLastError() = 0
  • I am debugging the DLL and no breakpoints get hit
edit:

I have no idea what the problem was but now it works



I will say tho, mangled symbol names are annoying AF. Everytime I ran the program it would be different, makes no sense. You can change project settings and use different function definitions but they all end up with retarded symbol names.

The solution is to use a module definition file:

Right click project properties -> Add New Item -> Search for "definition"

inside the definition file:
Code:
LIBRARY
EXPORTS
Init1=Init1
Now it's never mangled
 
Last edited:

timb3r

Semi-Retired
Dank Tier VIP
Jul 15, 2018
765
24,668
47
There's a few ways you can do one old method that malware abused the crap out of was creating a struct with the dll path as the first parameter, then adding all the additional data afterwards one call to wpm and one call to crt load library.

In addition you can use shared dll memory and there's some other tricks you can use.

Pipes is definately what you want to do if you're going to be sending alot of data back and forth.

 

Rake

Cesspool Admin
Administrator
Jan 21, 2014
12,151
78,998
2,396
There's a few ways you can do one old method that malware abused the crap out of was creating a struct with the dll path as the first parameter, then adding all the additional data afterwards one call to wpm and one call to crt load library.
that's what i'm doing :) 2 calls to CRT tho, you could make it 1 if you utilize pattern scanning in the DLL, similar to your command line WPM-less injection

if anyone knows a better way without pattern scanning or 2 calls to CRT that'd be dope
 

timb3r

Semi-Retired
Dank Tier VIP
Jul 15, 2018
765
24,668
47
that's what i'm doing :) 2 calls to CRT tho, you could make it 1 if you utilize pattern scanning in the DLL, similar to your command line WPM-less injection

if anyone knows a better way without pattern scanning or 2 calls to CRT that'd be dope
So you are I just skimmed the code didn't see your Struct. Whelp coffee time.

 

KF1337

*copies code from tutorials, then breaks it.*
Dank Tier Donator
Full Member
Nobleman
Jan 30, 2020
144
3,453
0
Actually a cool idea: you inject a module and control it with your external cheat.

Is this part of a new Rake tutorial? :love:
 

Rake

Cesspool Admin
Administrator
Jan 21, 2014
12,151
78,998
2,396
Actually a cool idea: you inject a module and control it with your external cheat.

Is this part of a new Rake tutorial? :love:
Anytime I find a question that gets asked 5000 times without a proper answer I feel like it is my duty to create a solution. "How to pass multiple arguments via CreateRemoteThread" has been a question I have seen at least 50 times and all the answers are incorrect, so I was doing this so I could be big PP man, here's the finished tutorial:

Tutorial - Pass multiple arguments to Injected DLL with CreateRemoteThread
 
  • Like
Reactions: Petko123 and KF1337
Attention! Before you post:

Read the How to Ask Questions Guide
99% of questions are answered in the Beginner's Guide, do it before asking a question.

No Hack Requests. Post in the correct section.  Search the forum first. Read the rules.

How to make a good post:

  • Fill out the form correctly
  • Tell us the game name & coding language
  • Post everything we need to know to help you
  • Ask specific questions, be descriptive
  • Post errors, line numbers & screenshots
  • Post code snippets using code tags
  • If it's a large project, zip it up and attach it

If you do not comply, your post may be deleted.  We want to help, please make a good post and we will do our best to help you.

Community Mods