-
[C++] Using IPC via Named Pipes - An Example Client/Server Setup
Credits to this blog (Introduction to Win32 Named Pipes (C++))
Named Pipes has been mentioned here before - this is mostly just a little touch-up on the above code and an explanation of what that type of IPC can be used for. With DLL injection, it can be an involved process to communicate to an application outside the DLL. IPC via Windows Named Pipes can create a talking bridge between the DLL and its injector or host app. With the code below in a real world scenario, you would likely create a thread from DllMain to create the client after the host or injector app has created the server. You can then safely communicate between the processes without access violations. Run the server first, and it should work nicely.
I decided to go this route despite the extra parsing involved because communicating to my Direct X hook was getting overly complicated when trying to call in-game functions remotely. This isn't the best of code manners, it's to demonstrate the concept.
Server.cpp
Code:
#include <iostream>
#include <string>
#include <windows.h>
#include <tchar.h>
using namespace std;
HANDLE CreatePipe()
{
cout << "Creating an instance of a named pipe..." << endl;
return CreateNamedPipe(
L"\\\\.\\pipe\\myPipeName", // name of the pipe
PIPE_ACCESS_OUTBOUND, // 1-way pipe -- send only
PIPE_TYPE_BYTE, // send data as a byte stream
1, // only allow 1 instance of this pipe
0, // no outbound buffer
0, // no inbound buffer
0, // use default wait time
nullptr // use default security attributes
);
}
void WaitForClient(HANDLE pipe)
{
cout << "Waiting for a client to connect to the pipe..." << endl;
const bool result = ConnectNamedPipe(pipe, nullptr);
if (!result) {
cout << "Failed to make connection on named pipe." << endl;
// look up error code here using GetLastError()
CloseHandle(pipe); // close the pipe
system("pause");
}
cout << "Connected to client. Awaiting input..." << endl;
}
bool SendData(HANDLE pipe, const char* message)
{
cout << "Sending data to pipe..." << endl;
// This call blocks until a client process reads all the data
const auto* data = message;
DWORD numBytesWritten = 0;
const auto result = WriteFile(
pipe, // handle to our outbound pipe
data, // data to send
strlen(data) * sizeof(char*), // length of data to send (bytes)
&numBytesWritten, // will store actual amount of data sent
nullptr // not using overlapped IO
);
if (result)
cout << "Number of bytes sent: " << numBytesWritten << endl;
else
cout << "Failed to send data." << endl;
return result;
}
void GetInput(HANDLE pipe)
{
string input;
do
{
getline(cin, input);
if (!input.empty() && input != "exit")
SendData(pipe, input.c_str());
} while (input != "exit");
}
int main()
{
auto* const pipe = CreatePipe();
WaitForClient(pipe);
GetInput(pipe);
CloseHandle(pipe);
return 0;
}
Client.cpp
Code:
#include <iostream>
#include <tchar.h>
#include <windows.h>
using namespace std;
HANDLE CreatePipe()
{
cout << "Connecting to pipe..." << endl;
// Open the named pipe
// Most of these parameters aren't very relevant for pipes.
return CreateFile(
L"\\\\.\\pipe\\myPipeName",
GENERIC_READ, // only need read access
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
}
const char* ReadData(HANDLE pipe)
{
cout << "Reading data from pipe..." << endl;
// The read operation will block until there is data to read
char buffer[1024];
DWORD numBytesRead = 0;
const auto result = ReadFile(
pipe,
buffer, // the data from the pipe will be put here
127 * sizeof(char*), // number of bytes allocated
&numBytesRead, // this will store number of bytes actually read
nullptr // not using overlapped IO
);
if (result) {
buffer[numBytesRead / sizeof(char*)] = '\0'; // null terminate the string
cout << "Number of bytes read: " << numBytesRead << endl;
cout << "Message: " << buffer << endl;
return buffer;
}
else {
cout << "Communication terminated." << endl;
}
return "=+Failed+=";
}
void ReadInput(void* const pipe)
{
while(true)
{
const auto* message = ReadData(pipe);
if (strcmp(message, "=+Failed+=") == 0)
break;
}
}
int main()
{
auto* const pipe = CreatePipe();
ReadInput(pipe);
CloseHandle(pipe);
system("pause");
return 0;
}
Pipes.PNG
Last edited by GlittPrizes; 06-18-2020 at 10:51 AM.
Reason: wording
-
Post Thanks / Like - 3 Thanks
-
To give an idea of the implementation, what I did was turn the above into a class as part of a static library. You can then create a thread to spawn the server and do the same thing with a thread created in DllMain for making a client to connect to it.
namedPipes.png
-
[Release] Pipes standalone - Server/Client Library
From the code above, I ended up doing the reverse to make the server spawn from my injected dll, then the client connects from an exe. This is a static library for inter-process communication (GitHub - glubdos/Pipes-standalone: Named Pipes Server/Client).
How to use:
Initialize (dll payload)
Code:
// thread created on attach
DWORD Init(LPVOID hInstance)
{
auto* const hPipe = Pipes::Server::Init();
Window::Init(); // SetWindowLongPtr or initialize your WndProc subclass here
while (!_safeToExit)
{
RunPipes();
Sleep(10); // sleep interval in between ticks
}
WaitForSingleObject(hPipe, INFINITE); // wait for signal state or check handle exit code here
Window::Close(); // restore window pointer
FreeLibraryAndExitThread(HMODULE(hInstance), 0); // exit
}
Run on Tick
Code:
void RunPipes()
{
if (!Pipes::Server::Request.empty())
{
if (Pipes::Server::Request != "@close")
{
// _luaCommand = Pipes::Server::Request; // set communication request here such as a console lua call
const auto command = Pipes::Server::Request;
Pipes::Server::Request = ""; // after extraction don't let request linger
// LuaBase::State = ShouldExecute; // here you could trigger lua execution based on global _luaCommand
}
else
{
Pipes::Server::Response = "#closing"; // program exit call received
Pipes::Server::Cv.notify_one();
}
}
if (!Pipes::Server::Connected)
_safeToExit = true;
else
Window::Send(WM_USER_TICK); // push user message to WndProc callback
}
Handle Client (from exe)
Code:
// run client thread to connect and feed input (manually in this case)
void RunClient()
{
Pipes::Client::Init();
string input;
do
{
getline(cin, input);
if (!input.empty())
{
std::tuple<string, string> response (input.substr(0, input.find('=')), input);
// Convert input string to wchar_t*
auto* const wInput = new wchar_t[input.length() + 1];
std::copy(input.begin(), input.end(), wInput);
wInput[input.length()] = 0;
std::get<1>(response) = Pipes::Client::SendReceive(wInput);
std::cout << std::get<0>(response) << ": " << std::get<1>(response) << std::endl;
}
} while (input != "@close");
exit(0);
}
How to use with FrameScriptExecute:
Code:
// LuaBase.cpp
LuaState LuaBase::State = LuaBusy;
void LuaBase::Execute(string command)
{
string varName;
switch (State)
{
case ShouldExecute:
State = LuaBusy;
GameMethods::Execute(command);
varName = command.substr(0, command.find('=')); // this only allows for single returns and not myVar1,myVar2=...
if (varName == command)
varName = "";
if (!varName.empty())
Pipes::Server::Response = GameMethods::GetText(varName.c_str());
if (Pipes::Server::Response.empty())
Pipes::Server::Response = "nil";
Pipes::Server::Cv.notify_one();
case LuaBusy:
default:
break;
}
}
// Window.cpp (WndProc callback)
LRESULT WINAPI Window::Hook(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_USER_TICK:
GameMethods::EnumVisible(EnumObjectsCallback, 0); // should probably check in game flag before calling this here
if (LuaBase::State != LuaBusy)
LuaBase::Execute(_luaCommand);
return false;
default:
break;
}
// return original WndProc.
return CallWindowProc(WndProc, hWnd, uMsg, wParam, lParam);
}
Last edited by GlittPrizes; 07-23-2020 at 07:09 AM.
Reason: FSExecute
-
Post Thanks / Like - 2 Thanks
34D,
darkness92 (2 members gave Thanks to GlittPrizes for this useful post)