- Network
- A
How I taught Discord voice chat to work through a proxy using system call interception and DLL Hijacking
I, like many others, use Discord and Chinese proxy clients, but unfortunately, Discord voice chat does not support working through a proxy. Because of this, I constantly have to enable TUN/VPN mode, which tunnels a lot of unnecessary traffic.
Can anything be done about this? Of course! In this article, we will figure out how to control and modify third-party applications, and here you can download a ready-made solution.
I'm not interested in technical details, how can I install this myself?
There are two ways to install it:
Automatic Installation
For your convenience, there is a ready-made installer. When you run it, you only need to specify the IP and port of your proxy, but the installer can automatically determine the parameters if you have one of these clients running:
v2rayN
NekoRay / NekoBox
Invisible Man - XRay (proxy mode must be socks)
Want to uninstall? Run the installer again, it will offer to remove the installed files.
Manual Installation
Want to install manually? No problem.
First, download
DWrite.dll
andforce-proxy.dll
from the release pageOpen
%LocalAppData%\Discord
in ExplorerFind the app folder with the latest version and place both dll files there
Create a file named proxy.txt and write the following in it:
SOCKS5_PROXY_ADDRESS=YOUR_PROXY_IP
SOCKS5_PROXY_PORT=YOUR_PROXY_PORT
Don't forget to restart Discord. Done!
Now to the point
Further in this article, there will be details on how this works and how it was created.
How to redirect the traffic of a single application to a SOCKS5 proxy
For an application that does not support working through a proxy by itself, the following options come to mind:
Use ready-made programs, for example, Proxifier. These programs are paid (and quite expensive), and there are also difficulties with payment from the Russian Federation. So this option does not suit us.
Take a disassembler and patch the executable file of the desired program. In addition to the laboriousness of this approach, there is also the problem that our modifications will live until the next update. So this is also not an option.
Write or take an already ready driver in which we will filter all OS packets, extract those related to specific processes, and redirect them to the proxy. The option on paper is quite viable, but not within the framework of a pet project of one person. The first problem is the relative complexity of implementing such a solution, the complexity of debugging, and the high price of an error - almost any error will send the user's computer to BSOD. But this can be lived with, I have some experience with such projects, but here the second problem arises - for any user to be able to load your driver, it needs to be signed with a certificate and sent for verification to Microsoft. And not just any certificate, but EV (Extended Validation), which can only be purchased by organizations. So this is not an option for me. Although if I were making my client, I would do just that.
Somehow get the ability to execute code in the context of the process we need, modify several Winsock 2 API functions in its memory so that they do what we need. A good option for delivering code is dynamic link libraries (DLL). So let's do it.
This project is ideologically divided into 2 parts:
@runetfreedom/force-proxy - this is a dll that intercepts socket APIs. I made it a separate project because it is not tied to Discord, and therefore it can be used for other tasks.
@runetfreedom/discord-voice-proxy - this is a proxy dll (proxy in the context of dll hijacking) that reads the config and loads force-proxy.dll into Discord processes. The installer is also part of this repository.
Implementation of system call interception
In this part of the article, we will consider the implementation of force-proxy.dll
Disclaimer - the code examples below will be given in chronological order of their writing. This means that as new features were implemented, I refactored what was already written, so don't be surprised by strange temporary solutions, most likely they were refactored. Also, the author is not a great writer in C++ (in the part where it is ++).
How to intercept a function call in x86-64
Since deep diving into hooks is not the topic of this article, the description will be quite superficial, just to provide a minimal understanding of how it works.
The simplest way to intercept a function is to write an unconditional jump instruction (jmp) at the very beginning of it. This allows you to jump to the execution of code at a given memory address without modifying the stack and registers (except for RIP, of course). Our function, which we jump to, is called Detour.
The jmp instruction in this case takes at least 5 bytes, so the first instructions of the original function in the first 5 bytes are overwritten. But the function cannot work without them, so a small area of executable memory is allocated, these instructions are moved there from the beginning of the original function, and another jump to the continuation of the original function is inserted. This area of memory is called Trampoline
So in the example further:
connect
- the original function, a jmp toMine_connect
will be inserted at its beginningMine_connect
- our detour function, which will take control when the program callsconnect
. It can either return something of its own or return control to the original connect function by callingReal_connect
Real_connect
- this is the trampoline, which contains the first 5 bytes from theconnect
function and a jmp to the connect+5 instruction
Fortunately, there are many libraries for automating the installation of hooks, for example MinHook or Microsoft Detours. I usually use MinHook for such things, but in this project I wanted to try Microsoft Detours. It's always interesting to try something new.
Intercepting TCP connections
The easiest way to start is by intercepting TCP connections. In fact, for this, it is enough to connect to the proxy server instead of connecting to the destination address, pass it the destination address, and that's it - then this connection will transparently forward packets through the proxy. So one hook will be enough here - for the connect
function.
SOCKS5 protocol
According to RFC1928 after connecting, we must perform a handshake and authenticate (optional). To do this, we must send 3 bytes: version (0x05), number of authentication methods (0x01), and authentication method (0x00) - NO AUTHENTICATION in our case. In response, we should receive 2 bytes - version (0x05) and confirmation of the authentication method (0x00)
After that, we must send a CONNECT request so that the proxy server connects to the destination server. The full request looks like this:
Proxy version - 1 byte (0x05)
Command - 1 byte (0x01 for CONNECT)
Reserved - 1 byte (0x00)
Destination address type - 1 byte (0x01 for IPv4)
Destination address - 4 bytes
Destination port - 2 bytes
Let's implement this:
int ConnectToProxy(SOCKET s)
{
SOCKADDR_IN proxyAddr;
proxyAddr.sin_family = AF_INET;
proxyAddr.sin_addr = g_ProxyAddress;
proxyAddr.sin_port = g_ProxyPort;
return Real_connect(s, (struct sockaddr*)&proxyAddr, sizeof(proxyAddr));
}
int SendSocks5Handshake(SOCKET s)
{
//Send socks5 handshake
uint8_t request[] = { 0x05, 0x01, 0x00 };
send(s, (const char*)request, sizeof(request), 0);
//Receive response
uint8_t response[2];
recv(s, (char*)response, sizeof(response), 0);
if (response[0] != 0x05 || response[1] != 0x00) {
return SOCKET_ERROR; // Socks5 auth error
}
return ERROR_SUCCESS;
}
int ConnectThroughSocks5(SOCKET s, const struct sockaddr_in* targetAddr)
{
if (ConnectToProxy(s) != ERROR_SUCCESS) {
return SOCKET_ERROR;
}
if (SendSocks5Handshake(s) != ERROR_SUCCESS) {
return SOCKET_ERROR;
}
// send CONNECT request
uint8_t connectRequest[10] = { 0x05, 0x01, 0x00, 0x01 }; // SOCKS5, CONNECT, reserved, IPv4
memcpy(connectRequest + 4, &targetAddr->sin_addr, 4); // Target ip
memcpy(connectRequest + 8, &targetAddr->sin_port, 2); // Target port
send(s, (const char*)connectRequest, sizeof(connectRequest), 0);
uint8_t connectResponse[10];
recv(s, (char*)connectResponse, sizeof(connectResponse), 0);
if (connectResponse[1] != 0x00) {
return SOCKET_ERROR; // Connection error
}
return ERROR_SUCCESS;
}
Implementation of the intercepted connect function
We have learned to connect to the proxy, now we need to declare the prototype of the connect function and write our own implementation:
We need to consider that we are not interested in redirecting every connection, at least we should skip connections to the proxy server and localhost. Also, we are currently only interested in the IPv4 address space (AF_INET)
extern "C" {
int (WINAPI* Real_connect)(SOCKET s, const sockaddr* name, int namelen) = connect;
}
int WINAPI Mine_connect(SOCKET s, const sockaddr* name, int namelen)
{
const struct sockaddr_in* addr_in = reinterpret_cast(name);
char taget[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(addr_in->sin_addr), taget, INET_ADDRSTRLEN);
//skip connection to localhost and proxy server
if (addr_in->sin_addr.s_addr == g_ProxyAddress.s_addr || !strcmp(taget, "0.0.0.0") || !strcmp(taget, "127.0.0.1")) {
return Real_connect(s, name, namelen);
}
if (addr_in->sin_family == AF_INET) {
return ConnectThroughSocks5(s, addr_in);
}
return Real_connect(s, name, namelen);
}
Interceptor
According to the documentation, we implement the hook installation
void InitHooks()
{
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach((PVOID*)(&Real_connect), Mine_connect);
DetourTransactionCommit();
}
And call it in our DllMain:
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "detours.lib")
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
if (DetourIsHelperProcess())
{
return TRUE;
}
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hModule);
DetourRestoreAfterWith();
InitHooks();
break;
}
return TRUE;
}
Excellent, in the test microprogram after loading the dll through LoadLibrary, all further TCP connections are intercepted.
Intercepting UDP
With UDP, everything is much more complicated. As you know, there is no such thing as a connection in UDP, and for socks5 we also have to encapsulate each udp packet.
To proxy udp packets, we need to connect to the proxy server and request UDP ASSOCIATE
from it. The proxy server on its side will bind and inform us of some port, when sending datagrams to which it will forward them through the proxy.
At the same time, each UDP datagram must be encapsulated - we must add (and then remove) a 10-byte header, which contains the destination address and port, and in which the server will return the sender's address and port.
The UDP ASSOCIATE
request is exactly the same as CONNECT, only the second byte must be 0x03. The datagram header looks like this:
Reserved - 2 bytes (0x00 0x00)
Fragmentation flag - 1 byte (0x00)
Destination / sender address type - 1 byte (0x01)
Address - 4 bytes
Port - 2 bytes
Implementation of the UDP association request
bool InitializeSocks5UdpAssociation(sockaddr_in *udpProxyAddr) {
//We need tmp socket to request udp association
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET) {
return false;
}
if (ConnectToProxy(s) != ERROR_SUCCESS) {
return false;
}
if (SendSocks5Handshake(s) != ERROR_SUCCESS) {
return false;
}
// Request UDP associate
// We don't have to specify dst since proxies usually get it from encapsulating header
uint8_t udpAssociateRequest[10] = { 0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0 }; // SOCKS5, UDP ASSOCIATE, reserved, IPv4, dst addr, dst port
send(s, (const char*)udpAssociateRequest, sizeof(udpAssociateRequest), 0);
uint8_t udpAssociateResponse[10];
recv(s, (char*)udpAssociateResponse, sizeof(udpAssociateResponse), 0);
if (udpAssociateResponse[1] != 0x00) {
return false;
}
Real_closesocket(s);
// Get address and port to send UDP packets
udpProxyAddr->sin_family = AF_INET;
memcpy(&udpProxyAddr->sin_addr, udpAssociateResponse + 4, 4);
memcpy(&udpProxyAddr->sin_port, udpAssociateResponse + 8, 2);
return true;
}
In response, the proxy server returns the IP and port to which we should send our UDP datagrams. Obviously, we need to remember this and use it in the intercepted data sending functions.
But when should we request the association? In my opinion, there are two options - either at the moment of creating a socket with the type SOCK_DGRAM
or at the moment of calling the bind
function (but then we need to check that the socket is indeed of the UDP type). The advantage of bind is that it is 1 interception instead of 3 for socket creation (socket
, WSASocketA
, WSASocketW
).
Intercept bind
std::shared_mutex g_SocketsMapsMutex;
std::map g_UDPAssociateMap;
bool IsUDPSocket(SOCKET s)
{
int32_t sockOptVal;
int32_t sockOptLen = sizeof(sockOptVal);
if (getsockopt(s, SOL_SOCKET, SO_TYPE, (char*)&sockOptVal, &sockOptLen) != 0)
return false;
return sockOptVal == SOCK_DGRAM;
}
bool SocketExistsInUdpAssociationMap(SOCKET s)
{
g_SocketsMapsMutex.lock_shared();
bool exists = g_UDPAssociateMap.count(s);
g_SocketsMapsMutex.unlock_shared();
return exists;
}
int WINAPI Mine_bind(SOCKET s, const sockaddr* addr, int namelen)
{
//not UDP or already exists
if (!IsUDPSocket(s) || SocketExistsInUdpAssociationMap(s))
return Real_bind(s, addr, namelen);
sockaddr_in udpProxyAddr;
if (!InitializeSocks5UdpAssociation(&udpProxyAddr)) {
return Real_bind(s, addr, namelen);
}
g_SocketsMapsMutex.lock();
g_UDPAssociateMap.insert(std::pair(s, udpProxyAddr));
g_SocketsMapsMutex.unlock();
return Real_bind(s, addr, namelen);
}
Okay, we remembered the association for a specific socket, but we need to clear it when closing the socket to avoid memory leaks.
int WINAPI Mine_closesocket(SOCKET s)
{
g_SocketsMapsMutex.lock();
g_UDPAssociateMap.erase(s);
g_SocketsMapsMutex.unlock();
return Real_closesocket(s);
}
Intercepting send and receive functions
In general, these functions are intercepted in pairs (sendto + WSASendTo and recvfrom + WSARecvFrom). But in this article, I will provide an implementation example of only one function of each type, because they are almost identical.
It should be noted that chrome under the hood of discord also uses mDNS (Multicast DNS). Obviously, we should not send multicast packets to the proxy, so let's take this into account.
Let's encapsulate the packet and send the data to our associated proxy port:
bool IsMultiCastAddr(const sockaddr *addr)
{
uint32_t ip = ntohl(((SOCKADDR_IN*)addr)->sin_addr.s_addr);
return (ip & 0xF0000000) == 0xE0000000;
}
void EncapsulateUDPPacket(WSABUF* target, char *buf, int len, const sockaddr* lpTo)
{
target->len = len + 10; // packet len + encasulated size
target->buf = (char *)malloc(target->len);
target->buf[0] = 0; // Reserved
target->buf[1] = 0; // Reserved
target->buf[2] = 0; // Fragmentation flag
target->buf[3] = 1; // IPv4
const struct sockaddr_in* addr = reinterpret_cast(lpTo);
memcpy(&target->buf[4], &addr->sin_addr.s_addr, sizeof(addr->sin_addr.s_addr)); //ip addr
memcpy(&target->buf[8], &addr->sin_port, sizeof(addr->sin_port)); // port
memcpy(&target->buf[10], buf, len); //copy whole packet
}
int WINAPI Mine_sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen)
{
if (SocketExistsInUdpAssociationMap(s) && !IsMultiCastAddr(to)) {
g_SocketsMapsMutex.lock_shared();
auto proxyAddr = &g_UDPAssociateMap[s];
g_SocketsMapsMutex.unlock_shared();
WSABUF destBuff;
EncapsulateUDPPacket(&destBuff, (char *)buf, len, to);
auto sended = Real_sendto(s, destBuff.buf, destBuff.len, 0, (const sockaddr*)proxyAddr, sizeof(*proxyAddr));
free(destBuff.buf);
return sended;
}
return Real_sendto(s, buf, len, flags, to, tolen);
}
Now we need to receive packets and return them to the application. For RTC to work, it is important that the from field is correct, so let's extract it from the header at the same time.
void ExtractSockAddr(char* buf, sockaddr* target)
{
const struct sockaddr_in* addr = reinterpret_cast(target);
memcpy((void*)&addr->sin_addr.s_addr, &buf[4], sizeof(addr->sin_addr.s_addr)); //ip addr
memcpy((void*)&addr->sin_port, &buf[8], sizeof(addr->sin_port)); // port
}
int WINAPI Mine_recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen)
{
auto received = Real_recvfrom(s, buf, len, flags, from, fromlen);
if (received != SOCKET_ERROR && SocketExistsInUdpAssociationMap(s)) {
//Encapsulated header is 10 bytes
if (received < 10) {
return SOCKET_ERROR;
}
ExtractSockAddr(buf, from);
memmove(buf, &buf[10], received -= 10);
};
return received;
}
Testing
It seems like we intercepted everything we needed - we can test it. We load it into a mini-program that sends TCP and UDP packets - it works, hooray. At this point, I am already relaxed and pleased with myself, and I go to test the DLL in combat conditions and inject it into Discord.
I do the injection and get a crash. Moreover, it's a strange crash in the Chrome subsystem. As a bonus, I get a nice message that in the release build, I am not entitled to an error message.
It's not clear what went wrong, especially considering that everything worked in the mini-program. The situation is exacerbated by the fact that Chromium spawns a bunch of processes for different tasks, which significantly complicates debugging with tools like IDA Pro or x64dbg.
At this point, I make the only right decision - I go to compile Chromium. I'm sure it could have been debugged without this, but a 200-megabyte binary scared me a bit, and I thought it would be too easy to get lost and confused in it. And since the source code is available, I decided to use it. Compiling Chromium took 1.5 hours on a 14900kf, and then Visual Studio took another 20 minutes to open the solution.
We embedded LoadLibraryA into Chromium's main, compiled it, started debugging, and...
It turns out that Chrome uses non-blocking sockets and expects us to return the WSAEWOULDBLOCK
error when attempting to connect. Okay, that doesn't sound too difficult, so first we need to determine that the socket is in non-blocking mode. To do this, we intercept WSAEventSelect
, ioctlsocket
, and check the passed parameters. Unfortunately, there is no API that allows us to determine the socket type.
std::map g_NonBlockingSockets;
bool SocketExistsInNonBlockingMap(SOCKET s)
{
g_SocketsMapsMutex.lock_shared();
bool exists = g_NonBlockingSockets.count(s);
g_SocketsMapsMutex.unlock_shared();
return exists;
}
int WSAAPI Mine_WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents)
{
g_SocketsMapsMutex.lock();
if (hEventObject != NULL && lNetworkEvents != 0) {
g_NonBlockingSockets.insert(std::pair(s, lNetworkEvents));
} else {
g_NonBlockingSockets.erase(s);
}
g_SocketsMapsMutex.unlock();
return Real_WSAEventSelect(s, hEventObject, lNetworkEvents);
}
int WSAAPI Mine_ioctlsocket(SOCKET s, long cmd, u_long* argp)
{
if (cmd == FIONBIO) {
g_SocketsMapsMutex.lock();
if (*argp) {
g_NonBlockingSockets.insert(std::pair(s, *argp));
} else {
g_NonBlockingSockets.erase(s);
}
g_SocketsMapsMutex.unlock();
}
return Real_ioctlsocket(s, cmd, argp);
}
Well, now we know the socket type, we can work with it as non-blocking. We obviously cannot subscribe to socket events ourselves, as this will break event delivery for the program, so we will use the select function. And we will add our wait functions before all send and recv when working with socks5 sockets.
bool WaitForWrite(SOCKET s, int timeoutSec)
{
fd_set writeSet;
FD_ZERO(&writeSet);
FD_SET(s, &writeSet);
timeval timeout = { timeoutSec, 0 };
int result = select(0, NULL, &writeSet, NULL, &timeout);
if (result > 0 && FD_ISSET(s, &writeSet)) {
return true;
}
return false;
}
bool WaitForRead(SOCKET s, int timeoutSec)
{
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(s, &readSet);
timeval timeout = { timeoutSec, 0 };
int result = select(0, &readSet, NULL, NULL, &timeout);
if (result > 0 && FD_ISSET(s, &readSet)) {
return true;
}
return false;
}
bool SetNonBlockingMode(SOCKET s, bool nonBlocked)
{
u_long mode = nonBlocked;
return Real_ioctlsocket(s, FIONBIO, &mode) == NO_ERROR;
}
I will not provide the modified code with the addition of waiting, as it is trivial. Also, in the connection to the proxy for TCP, we will add the correct status return
if (nonBlocking) {
WSASetLastError(WSAEWOULDBLOCK);
return SOCKET_ERROR;
}
During testing, another nuance arose - if the program uses non-blocking sockets, and we make a connection with a regular blocking socket inside bind, then after the successful completion of bind, the sockets enter mutual blocking. I did not quite understand how my separate socket for communicating with the proxy server is related to the non-blocking socket that is passed to bind, but nevertheless. However, the problem was simply solved by translating my socket into non-blocking mode.
I was very interested, I tried to debug it, but I realized that everything stops after accessing the kernel through NtDeviceIoControlFile
and I got lazy to set up a virtual machine and debug the kernel. But if anyone knows why this happens - tell me in the comments.
At this stage, we have a DLL that works and redirects traffic through a proxy.
How to load a DLL into Discord
Obviously, it's not enough to have a DLL, you also need to be able to load it, preferably automatically.
Here are the loading options we have:
Use any of the hundreds of DLL injectors. This method is as obvious as it is useless. Chromium spawns a bunch of processes, often spawning new ones when connecting to servers, so your hands will fall off injecting.
Use AppInit_DLLs - this is a registry parameter that asks the OS to load the specified DLL into ALL running processes. This method has a lot of problems: It requires the DLL to be signed with a certificate, creates a huge security hole, will lead to bans in online games, because the anti-cheat will not be happy with the loaded DLL, which can also hook network functions.
Exploit the oldest DLL search order vulnerability - DLL Hijacking. An excellent option that requires the user to simply place the file in the Discord folder.
DLL Hijacking
In a nutshell - we will exploit the fact that when trying to load a DLL, it is first searched in the application folder, and only then in C:\Windows\System32
What do we need? We need to find the library that the target application loads, create our own with the same name and the same exports, and redirect these exports to the original DLL. Our DLL in this case will be called proxy DLL. Our goal is to have DllMain
of our proxy DLL called, in which we will load the payload.
Let's look at Discord's imports and find a suitable library
Well, it wasn't difficult - we immediately found a library with a single importable function. So we will use it.
First, let's make our own DLL, which will load the original DWrite.dll and save the address of the DWriteCreateFactory function in a variable.
HMODULE OriginalDLL;
uintptr_t OrignalDWriteCreateFactory;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
char path[MAX_PATH];
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
DisableThreadLibraryCalls(hModule);
CopyMemory(path + GetSystemDirectoryA(path, MAX_PATH - 12), "\DWrite.dll", 13);
OriginalDLL = LoadLibraryA(path);
OrignalDWriteCreateFactory = (uintptr_t)GetProcAddress(OriginalDLL, "DWriteCreateFactory");
break;
}
case DLL_PROCESS_DETACH:
{
FreeLibrary(OriginalDLL);
break;
}
}
return TRUE;
}
Great, we loaded the original DLL and saved the address of the desired function, now we need to create our own export with the same name and redirect the call to this function. As we already know, the easiest way to do this is to insert a jmp. Since MSVC does not support inline assembler, we will create a separate .asm file for this
.code
extern OrignalDWriteCreateFactory:qword
DWriteCreateFactory proc EXPORT
jmp OrignalDWriteCreateFactory
DWriteCreateFactory endp
end
Loading force-proxy.dll
At this stage, we have a full-fledged proxy dll, and therefore the ability to execute any code in the context of the discord process. We will use this to parse the config and load force-proxy.dll
void LoadForceProxy()
{
std::ifstream file("proxy.txt");
std::string line;
while (file.is_open() && getline(file, line)) {
std::stringstream ss(line);
std::string key, value;
if (getline(ss, key, '=') && getline(ss, value)) {
SetEnvironmentVariableA(key.c_str(), value.c_str());
}
}
LoadLibraryA("force-proxy.dll");
}
Yes, it's that simple.
What about discord updates?
We already have a working solution, but discord is written to a new folder when updated and deletes the old one, which will lead to the deletion of our libraries as well.
After analyzing the discord update process, it became clear that it calls Discord.exe
from the new location after writing new files, which can be used.
Let's just intercept CreateProcessW according to the existing patterns, see what is being launched, and if it's discord in the new folder, just copy our files there
BOOL __stdcall Mine_CreateProcessW(LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation)
{
;
WCHAR path[MAX_PATH];
GetModuleFileNameW(NULL, path, MAX_PATH);
std::vector files = {
L"proxy.txt",
L"force-proxy.dll",
L"DWrite.dll"
};
if (lpApplicationName != NULL) {
auto targetAppName = std::wstring(lpApplicationName);
auto currentPath = std::wstring(path);
if (EndsWith(targetAppName, L"Discord.exe") && targetAppName != currentPath) {
auto currentDir = fs::path(currentPath).parent_path();
auto targetDir = fs::path(targetAppName).parent_path();
for (const auto& file : files) {
if (fs::exists(currentDir.wstring() + L"\" + file)) {
fs::copy(currentDir.wstring() + L"\" + file, targetDir.wstring() + L"\" + file, fs::copy_options::overwrite_existing);
}
}
}
}
return Real_CreateProcessW(lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);
}
Conclusion
This concludes our exciting fishing on WinAPI.
There is definitely room for improvement in the project - I am sure that I do not intercept enough functions to work in any situation. Also, it might make sense to add support for socks5 authentication.
force-proxy.dll definitely does not work in Chrome with sandbox mode enabled. I did not figure out why exactly, but it seems obvious that the rights of the processes are greatly reduced.
So it would be great if someone tested force-proxy.dll in other applications and, ideally, suggested PRs with improvements.
Write comment