Hey,
A long time ago, in a galaxy far, far away...
@JYam @
mistree and I released a 3 versions of a bypass (
ASAP Handles! v1,
v2, and
hSonic), along with an injector using the principle (
MEMEInjector).
Only a 2 or 3 weeks later, EAC already applied a patch (I didn't check for BattlEye).
Some results from the experiments you are about to read indicates that in fact they did NOT fix the issue, they just found a way to prevent us from getting the new process notification in time to outrun the kernel notification routines, but the unsecure delay seems to still be there. More on that further down the article.
I was a bit disappointed at first.
I knew obviously that these guys were lurking, but you know...
Sometimes you know something but you only realise it later, something happens and it become suddenly much more "real".
That was how I felt at that moment.
And it was extremely exciting.
It means, when I write my UnknownCheats articles, these guys are there, reading too.
The cat and mouse game just became more real, and at that moment, I set myself a challenge:
I am going to pwn the anti-cheats using the process handle that we are left with AFTER they modify its permissions.
In other words, I will use whatever is left past their security mechanisms and pwn them anyway with that.
I thought that would be a damn good lolz, and I thought it would make you guys laugh, because I'm just a clown deep down
To increase my motivation further, I even fired up Photoshop and made a little image that I intended to use in my future UC post, when I publish my results, I picked a name that suited the experiment:
PwnBack
Yeah it's a different name that this article, but I did not know what I would find at first, or if I would find at all.
Even having one seemed challenging.
So I started experimenting... A lot... And I found some stuff that might be valuable for you.
Don't get too hyped though, I don't consider any bypass method in here satisfactory.
Let me calm things down before we begin:
Yes these bypasses DO have detection vectors, I myself consider them risky to use (some of them have less than others, you'll make your own opinion), and last but not least, they do not rely on any novel ground-breaking technique.
However, this is a good demonstration that sometimes, you can do things without going complicated or overly-technical, you just need to have some ideas
Enough setup, let's dive in.
Attack surface for this challenge
As I said, I am going to use a handle that went through anti-cheat protection mechanisms, so let's check what permissions these handles have.
To get this information, I just do a simple OpenProcess requesting PROCESS_ALL_ACCESS on some protected games and check what permissions are left.
While discussing privately with @dracorx I noticed that there are sometimes different handles and permissions variations for a same anti cheat on different games, so I decided to check on different games that I own (too bad I don't own any games on which the security had the reputation to be aggressive like PUBG, but anyway those games will do).
EasyAntiCheat protecting 7 Days to die:
0x11fbc5 (Query limited information, Set information, Set quotas, Set session ID, Create processes, Duplicate handles, Suspend/resume, Terminate, Synchronize, Delete)
EasyAntiCheat protecting Miscreated:
0x11fbc5 (Query limited information, Set information, Set quotas, Set session ID, Create processes, Duplicate handles, Suspend/resume, Terminate, Synchronize, Delete)
BattlEye protecting Unturned:
0x1ffbc5 (Query limited information, Set information, Set quotas, Set session ID, Create processes, Duplicate handles, Suspend/resume, Terminate, Synchronize, Delete, Read control, Write DAC, Write owner)
BattlEye protecting DayZ:
0x1ffbc5 (Query limited information, Set information, Set quotas, Set session ID, Create processes, Duplicate handles, Suspend/resume, Terminate, Synchronize, Delete, Read control, Write DAC, Write owner)
Seems like the callbacks to "secure" handles by changing permissions are the same on different games for the same anti-cheat, and we also find commonalities between anti-cheats.
That's what I am left with, and since EAC has more restrictive permissions after stripping, I am going to limit myself to the permissions left by EAC, so any bypass should be compatible with both ACs)
In summary, I need to pwn them with Query limited information, Set information, Set quotas, Set session ID, Create processes, Duplicate handles, Suspend/resume, Terminate, Synchronize, and Delete...
From dickin' around to pwnin' around
My first choice was to start trying stuff with the Duplicate Handle permission, I tried some stuff with it, even dumb stuff that I myself didn't know where I was going with that, like GIVING handles to the game (WTF?)
But I couldn't find anything worth mentioning.
So, second choice: the Create Process permission.
Actually if you think a bit more, this one is actually very promissing, we can get the inheritable handles of a game this way.
So which kind of handles games have?
I have been reversing and hacking a bit on Miscreated (EAC protected) these days, so I experimented on this game.
It has natively several inheritable handles on some of its threads:
I quickly checked and so does DayZ actually:
I also checked a few games (non AC protected) and them too had inheritable thread handles.
That's intriguing, there are probably some common graphic libraries that are coded to have inheritable threads or something?
I can't imagine that being a willful decision from the game devs.
Anyway, whatever.
I make a code a small thing to spawn a program as child of another and inherit its inheritable handles (copy paste from hBastard).
Code:
#include <Windows.h>
#include <iostream>
#include <string>
using namespace std;
bool SetPrivilege(LPCWSTR lpszPrivilege, BOOL bEnablePrivilege = TRUE) {
TOKEN_PRIVILEGES priv = { 0,0,0,0 };
HANDLE hToken = NULL;
LUID luid = { 0,0 };
BOOL Status = true;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) {
Status = false;
goto EXIT;
}
if (!LookupPrivilegeValueW(0, lpszPrivilege, &luid)) {
Status = false;
goto EXIT;
}
priv.PrivilegeCount = 1;
priv.Privileges[0].Luid = luid;
priv.Privileges[0].Attributes = bEnablePrivilege ? SE_PRIVILEGE_ENABLED : SE_PRIVILEGE_REMOVED;
if (!AdjustTokenPrivileges(hToken, false, &priv, 0, 0, 0)) {
Status = false;
goto EXIT;
}
EXIT:
if (hToken)
CloseHandle(hToken);
return Status;
}
PROCESS_INFORMATION MakeBastardChild(DWORD dwParentPID, std::wstring childProcess, std::wstring cmdLineArgs) {
PROCESS_INFORMATION pi;
SecureZeroMemory(&pi, sizeof(pi));
// Initialising attributes for process creation
SIZE_T cbAttributeListSize = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &cbAttributeListSize);
PPROC_THREAD_ATTRIBUTE_LIST pAttributeList = NULL;
pAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, cbAttributeListSize);
if (NULL == pAttributeList)
return pi; // Failed
if (!InitializeProcThreadAttributeList(pAttributeList, 1, 0, &cbAttributeListSize))
return pi; // Failed
// Getting handle on parent with only PROCESS_CREATE_PROCESS permission (required by UpdateProcThreadAttribute)
HANDLE hParentProcess = NULL;
hParentProcess = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, dwParentPID);
if (NULL == hParentProcess)
return pi; // Failed
// Updating the attribute list with the desired parent for the future process to start
if (!UpdateProcThreadAttribute(pAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParentProcess, sizeof(HANDLE), NULL, NULL))
return pi; // Failed
// If child process is unspecified, we will spawning another instance of the current process, getting own path & image name
if (childProcess == L"") {
WCHAR thisProgram[MAX_PATH] = L"";
DWORD myLength = GetModuleFileName(NULL, (LPWSTR)&thisProgram, MAX_PATH);
childProcess = thisProgram;
}
// Command line arguments specified, formating the wide string to give CreateProcess
std::wstring cmdLineFullArgs;
if (cmdLineArgs != L"") {
cmdLineFullArgs = L'"' + childProcess + L'"' + L' ' + cmdLineArgs;
cmdLineFullArgs.resize(0x7FFF); // Getting the max possible size to avoid access violation (accordingly to CreateProcess documentation)
}
// Creating the bastard child process
STARTUPINFOEX sie = { sizeof(sie) };
sie.lpAttributeList = pAttributeList;
CreateProcess(childProcess.c_str(), (LPWSTR)cmdLineFullArgs.c_str(), NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE, NULL, NULL, &sie.StartupInfo, &pi);
// Whether it succeeded or not, returning the PROCESS_INFORMATION (can check if failed if everything is null inside, since it's zeroed)
return pi;
}
int main() {
SetPrivilege(SE_DEBUG_NAME);
SetPrivilege(SE_TCB_NAME);
PROCESS_INFORMATION pi;
pi = MakeBastardChild(18000, L"C:\\Users\\user\\Box Sync\\Code\\EmptyProgram\\x64\\Release\\EmptyProgram.exe", L"");
system("pause");
return EXIT_SUCCESS;
}
And yep, my program receives the thread handles without having the permissions modified:
If you experiment with your games to see what inheritable handles they have do not only start the game, for miscreated for example more handles are created when you go in certain menus and when you join the game. New libraries are loaded which can lead to more handle, or on the contrary some handles can be closed. Play a bit with it and see by yourself.
Okay so now we have a full permission thread handle, but we can't do much with it.
Being in possession of a full access thread handle is actually very frustrating: You know you are in control of execution, you know you can suspend, get the thread context and set it to execute something else but you don't have access to put a shellcode into an executable region of memory.
So you can control execution, but not what's going to be executed.
Yep, that kind of sucks.
Okay so now the goal is to be able to push shellcode into the game's memory, how can we do that?
A bunch of ideas came to my mind.
A first bypass: Push shellcode with LSASS/CSRSS handle and execute with inherited thread handle
This is just an idea, I did not experiment it, but maybe you'll be interested.
It is obvious and absolutely not new but we could use LSASS or CSRSS handles with a stealthy enough method.
Using their handle you write your shellcode in an already executable memory page and then you use the thread handle for execution.
After all we just need to do 1 write operation, we wouldn't be putting a shared memory or named pipe permanent bypass that ends up doing thousands of operations per second with the handle.
Plus these processes both import the functions to write to memory and make use of them in their normal execution (for example LSASS imports NtRVM and NtWVM and call it somwhere in lsasrv.dll).
If you can make that one write stealthy enough, then you should be good for the detection related to tampering with system processes.
I did not follow that road because I knew that most people in here are just fed up with handle hijacking stuff, and in addition to that, the ACs have set up monitoring methods on those processes looking for suspicious activity.
But if I had to write it I would certainly write my shellcode at the end an executable region where we know there is unused bytes, at the end of ntdll.dll or kernel32.dll for example, in addition to that they are mapped at the same address in every process.
You write the shellcode to NtWriteVirtualMemory your shellcode to be executed by the game at the end of ntdll.dll or kernel32.dll in LSASS, hijack a thread to execute and your shellcode pushed in the game at a known location, you can then hijack one of the game's thread with the inherited handles that we have.
Once you can execute shellcode, you can pretty much do anything, like make the game manual map a DLL for you or anything else you want it to do.
So how else could we push our shellcode and execute it?
I decided to start small, simply succeeding to write my shellcode into the game's memory, even in a non-executable memory region, even if it would be easily detected would be a good start.
With such as approach there are many ways possible, including using the existing system process handle, but also others:
For example one simple (and brutal) way is to use an hexadecimal editor on a file that will be mapped to memory when the game loads, paste the bytes of your shellcode at the end of the file or anywhere it doesn't disturb too much, then you just send a thread at that location.
However there is a problem to execute then: DEP (Data Execution Prevention)
A second bypass: Turn DEP off (per process or system-wide) to allow execution in non executable pages
Yep, you can simply turn DEP off, and in case you wonder:
Yes you can play online, I tried on Unturned (BE) first, no ban, tried on Miscreated then (EAC), no ban, and DayZ (BE), no ban either.
There are different ways you can turn off DEP: For the entire system, or per specific processes.
You can turn it on and off with a single command line:
Code:
bcdedit.exe /set {current} nx AlwaysOn
bcdedit.exe /set {current} nx AlwaysOff
Otherwise you can turn it off for specific processes by: Right click on computer, properties, advanced tab, click settings in performance, data execution prevention tab.
You'll get this:
Note: I couldn't deactivate per process on my own system, only system-wide, perhaps you can solve this mystery or perhaps you'll just be luckier, I didn't searched to know why
I don't know what's more suspicious: A system with DEP turned off entirely, or a system with DEP on but turned off for the game only... probably the latter.
If you decide to experiment with DEP, there are some Windows API function useful:
GetSystemDEPPolicy, SetSystemDEPPolicy, GetProcessDEPPolicy, SetProcessDEPPolicy mainly.
Now let's be clear: This bypass idea is shit, absolute garbage, don't use it for actual cheating, consider it for educational purposes only, please.
It is very suspicious, has lots of detection vectors, just shit I tell you.
So okay let's now find solutions WITHOUT turning DEP off, please, that just sucks.
Here comes the bit I am actually proud of.
For some time I cheat with a method that requires to load a signed, unmodified, genuine, legitimate DLL into my games.
This can be done by serveral ways, for example, you can simply go in your registry with Regedit, and add the DLL in the AppInit_DLLs list so it'll be loaded by all programs.
However, EAC prevents DLLs from AppInit to load, so I also sometimes use DLL replacement.
An easy way to do this is for example to have a program load an additional DLL, for example, if you turn on Discord Overlay, it'll load the overlay DLL into the game, the anti-cheat will obviously check the DLL signature and if it's whitelisted, it'll be loaded.
Since I need to load a genuine DLL, I just replace physically on disk the DLL of Discord by my other genuine DLL and it passes the security checks, since it is a genuine DLL, it doesn't matter that it's a DLL signed with another certificate, only the certificate has to be validated.
Okay so now we know that we can load any signed DLL into a protected game (and there's no easy fix for that, each computer as different drivers, programs, etc... that need to load various genuine DLLs to work).
Well, this way we can tackle 2 problems of the previous technique that were bothering me:
First, what if the game doesn't have an inheritable thread handle?
Well,
we could make a thread inheritable using Process Hacker in admin mode since it loads their driver KProcessHacker that lets you do just that, we could even do it programmatically, but let's not go that way (first I do not want Process Hacker's app or driver to be blacklisted, plus I bet that the anti-cheat detecting the sole presence of process hacker raises suspicion).
With our new capacity to load signed DLL, we just have to find a DLL that when loaded creates a thread that is is inheritable and boom we can make any game to have an inheritable thread handle to allow thread hijacking.
Second, how to be able to execute with DEP still on?
Well, once again,
we could find a DLL that when loaded gets an inheritable handle on an executable memory section.
We could then just inherit this handle, push the shellcode, and use the inherited thread handle to execute it.
But come on, this is one hell of a DLL that we are looking for now.
First in general DLLs only export functions to give features to programs, they don't actually do things on their own.
Second, generally, memory pages are created with VirtualAlloc which doesn't generate any handle, there's only a handle in special cases, like when creating shared memory for example (the handle is created by the call to CreateFileMapping in that case).
It would be like looking for a needle in giant fucking haystack of DLLs.
How would I find that DLL?
MAP ALL THE DLLS!
Well, I automated the process.
I did a program that does the following operations:
- Scan a directory (and its subdirectories) for DLLs
- Write the full list of DLLs to a text file
- Start a process of an empty program that does absolutely nothing (and has little to no inheritable handles)
Then start a big loop that:
- Start a program as child of the empty program
- Wait a bit for complete initialisation/stabilisation
- Take a snapshot of the handles that it has
- Loads the next untested DLL
- Wait a bit for complete initialisation
- Take a second snapshot of the handles that it has
- Write the results to the log file (= how many new handles, and new inheritable handles the DLL gave to the program)
If you are interested here is the first version of my code that tests DLLs (it had a problem and stopped working after a few thousand DLLs tested but I can't find the last version anywhere)
Code:
#include <Windows.h>
#include <iostream>
#include <fstream>
#include <vector>
#include <deque>
#include <string>
#include <algorithm>
#include <TlHelp32.h>
#include "GetHandles.hpp"
#define DLL_LIST_FILE L"DLLs.txt"
#define LAST_DLL_TREATED L"Last.txt"
#define LOG_FILE L"Results.txt"
#define PARENT_PROCESS L"EmptyProgram.exe"
#define DLL_STARTUP_TIME 5000
#define STABILISATION_TIME 2000
#define MAX_TIMEOUT_ANTIFREEZE 90000
#define CMDLINE_MAX_LENGTH 0x7FFF
using namespace std;
vector<wstring> FindFiles(wstring baseDirectory, wstring extension, bool recursively);
PROCESS_INFORMATION MakeBastardChild(DWORD dwParentPID, std::wstring childProcess = L"", std::wstring cmdLineArgs = L"");
std::vector<DWORD> GetPIDs(std::wstring targetProcessName = L"");
void ProcessNext();
void AntiFreeze();
int main() {
// Protection against freeze (start a thread that waits a minute before exiting and treating next
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)AntiFreeze, NULL, NULL, NULL);
// If DLLs.txt doesn't exist, scanning directory
WIN32_FIND_DATA FindDllList;
HANDLE hListDLLs = FindFirstFile(DLL_LIST_FILE, &FindDllList);
if (GetLastError() == ERROR_FILE_NOT_FOUND || hListDLLs == INVALID_HANDLE_VALUE) {
cout << "DLLs.txt doesn't exist. Scanning..." << endl;
vector<wstring> foundDLLs = FindFiles(L"C:\\Windows\\System32", L"dll", true);
if (foundDLLs.empty())
return EXIT_FAILURE;
wofstream dllFileCreation(DLL_LIST_FILE, ios::out | ios::trunc);
for (int i(0); i < foundDLLs.size(); ++i)
dllFileCreation << foundDLLs[i] << endl;
dllFileCreation.close();
cout << "Done." << endl;
}
// DLLs.txt exist, loading lines into a vector
cout << "Loading list of DLLs." << endl;
vector<wstring> listDLLs;
wifstream listDLLsFile(DLL_LIST_FILE);
wstring currentDLL;
while (listDLLsFile >> currentDLL)
listDLLs.push_back(currentDLL);
listDLLsFile.close();
// Getting last DLL treated
wstring lastDLLtreated;
bool first = false;
wifstream checkLastDLL(LAST_DLL_TREATED);
if (!checkLastDLL) {
lastDLLtreated = listDLLs[0];
wofstream createLastDLL(LAST_DLL_TREATED, ios::out | ios::trunc);
createLastDLL << lastDLLtreated << endl;
createLastDLL.close();
first = true;
} else {
checkLastDLL >> lastDLLtreated;
}
checkLastDLL.close();
// Getting next DLL to treat
wstring nextDLL = lastDLLtreated;
ptrdiff_t lastDLLid = 0;
if (!first) {
lastDLLid = find(listDLLs.begin(), listDLLs.end(), lastDLLtreated) - listDLLs.begin();
nextDLL = listDLLs[lastDLLid + 1];
}
wcout << "[" << lastDLLid << "/" << listDLLs.size() << "] Analysing DLL" << endl;
wcout << nextDLL << endl;
// Analysing DLL-generated handles (listing initial handles, loading lib, listing new handles)
wofstream setNextAsLastFile(LAST_DLL_TREATED, ios::out | ios::trunc);
setNextAsLastFile << nextDLL << endl;
setNextAsLastFile.close();
Sleep(STABILISATION_TIME);
vector<HANDLE> initialHandles = GetCurrentHandlesIDs();
cout << "Handles initially: " << initialHandles.size() << endl;
HMODULE dllTested = LoadLibrary(nextDLL.c_str());
if (dllTested == NULL)
ProcessNext();
Sleep(DLL_STARTUP_TIME); // Giving time for the library to start its threads, create handles and shit
vector<HANDLE> postHandles = GetCurrentHandlesIDs();
cout << "Handles with DLL: " << postHandles.size() << endl;
vector<HANDLE> newHandles;
for (int j(0); j < postHandles.size(); ++j)
if (find(initialHandles.begin(), initialHandles.end(), postHandles[j]) == initialHandles.end())
newHandles.push_back(postHandles[j]); // New handle
cout << "New HANDLEs: " << newHandles.size() << endl;
vector<HANDLE> newHandlesInheritable;
for (int j(0); j < newHandles.size(); ++j) {
DWORD handleFlags = NULL;
GetHandleInformation(newHandles[j], &handleFlags);
if (handleFlags & HANDLE_FLAG_INHERIT)
newHandlesInheritable.push_back(newHandles[j]);
}
cout << "New inheritable HANDLEs: " << newHandlesInheritable.size() << endl;
FreeLibrary(dllTested);
wofstream logResults(LOG_FILE, ios::out | ios::app);
logResults << "[" << newHandlesInheritable.size() << "/" << newHandles.size() << "/" << postHandles.size() << "/" << initialHandles.size() << "] "
<< "(" << lastDLLid << "/" << listDLLs.size() << ") " << nextDLL << endl;
logResults.close();
ProcessNext();
return EXIT_SUCCESS;
}
void AntiFreeze() {
Sleep(MAX_TIMEOUT_ANTIFREEZE);
ProcessNext();
}
void ProcessNext() {
vector<DWORD> parentPIDs = GetPIDs(PARENT_PROCESS);
MakeBastardChild(parentPIDs[0]);
ExitProcess(EXIT_SUCCESS);
}
vector<wstring> FindFiles(wstring baseDirectory, wstring extension, bool recursively) {
vector<wstring> foundFiles;
deque<wstring> dirsToSearch;
dirsToSearch.push_back(baseDirectory);
do {
wstring currentDir = dirsToSearch.front();
wstring searchString = currentDir + L"\\*";
WIN32_FIND_DATA ffd;
HANDLE hFind = FindFirstFile(searchString.c_str(), &ffd);
if (hFind == INVALID_HANDLE_VALUE) { // Error, skipping directory
dirsToSearch.pop_front();
continue;
}
do {
// If recursive search, adding to the sub-directory double-ended queue for treatment
if (recursively && (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
wstring foundDir = ffd.cFileName;
if (foundDir == L"." || foundDir == L"..")
continue; // To avoid recursion in parent directories
wstring fullPathNewDir = currentDir + L"\\" + foundDir;
if (find(dirsToSearch.begin(), dirsToSearch.end(), fullPathNewDir) == dirsToSearch.end())
dirsToSearch.push_back(fullPathNewDir); // Directory not in the list, adding
continue;
}
// Found file, checking if matches search criteria and adding to the list
wstring foundFile = ffd.cFileName;
if (foundFile.substr(foundFile.find_last_of(L".") + 1) == extension) {
wstring fullPathNewFile = currentDir + L"\\" + foundFile;
if (find(foundFiles.begin(), foundFiles.end(), fullPathNewFile) == foundFiles.end())
foundFiles.push_back(fullPathNewFile); // DLL not in the list, adding
}
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
dirsToSearch.pop_front();
} while (!dirsToSearch.empty());
return foundFiles;
}
PROCESS_INFORMATION MakeBastardChild(DWORD dwParentPID, std::wstring childProcess, std::wstring cmdLineArgs) {
PROCESS_INFORMATION pi;
SecureZeroMemory(&pi, sizeof(pi));
// Initialising attributes for process creation
SIZE_T cbAttributeListSize = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &cbAttributeListSize);
PPROC_THREAD_ATTRIBUTE_LIST pAttributeList = NULL;
pAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, cbAttributeListSize);
if (NULL == pAttributeList)
return pi; // Failed
if (!InitializeProcThreadAttributeList(pAttributeList, 1, 0, &cbAttributeListSize))
return pi; // Failed
// Getting handle on parent with only PROCESS_CREATE_PROCESS permission (required by UpdateProcThreadAttribute)
HANDLE hParentProcess = NULL;
hParentProcess = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, dwParentPID);
if (NULL == hParentProcess)
return pi; // Failed
// Updating the attribute list with the desired parent for the future process to start
if (!UpdateProcThreadAttribute(pAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParentProcess, sizeof(HANDLE), NULL, NULL))
return pi; // Failed
// If child process is unspecified, we will spawning another instance of the current process, getting own path & image name
if (childProcess == L"") {
WCHAR thisProgram[MAX_PATH] = L"";
DWORD myLength = GetModuleFileName(NULL, (LPWSTR)&thisProgram, MAX_PATH);
childProcess = thisProgram;
}
// Command line arguments specified, formating the wide string to give CreateProcess
std::wstring cmdLineFullArgs;
if (cmdLineArgs != L"") {
cmdLineFullArgs = L'"' + childProcess + L'"' + L' ' + cmdLineArgs;
cmdLineFullArgs.resize(CMDLINE_MAX_LENGTH); // Getting the max possible size to avoid access violation (accordingly to CreateProcess documentation)
}
// Creating the bastard child process
STARTUPINFOEX sie = { sizeof(sie) };
sie.lpAttributeList = pAttributeList;
sie.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
sie.StartupInfo.wShowWindow = SW_HIDE;
CreateProcess(childProcess.c_str(), (LPWSTR)cmdLineFullArgs.c_str(), NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE, NULL, NULL, &sie.StartupInfo, &pi);
// Whether it succeeded or not, returning the PROCESS_INFORMATION (can check if failed if everything is null inside, since it's zeroed)
return pi;
}
// Get PIDs from process name
std::vector<DWORD> GetPIDs(std::wstring targetProcessName) {
std::vector<DWORD> pids;
if (targetProcessName == L"")
return pids; // No process name given
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // All processes
PROCESSENTRY32W entry; // Current process
entry.dwSize = sizeof entry;
if (!Process32FirstW(snap, &entry)) // Start with the first in snapshot
return pids;
do {
if (std::wstring(entry.szExeFile) == targetProcessName)
pids.emplace_back(entry.th32ProcessID); // Names match, add to list
} while (Process32NextW(snap, &entry)); // Keep going until end of snapshot
CloseHandle(snap);
return pids;
}
And GetHandle.hpp:
Code:
#pragma once
#include <Windows.h>
#include <Winternl.h>
#include <ntstatus.h>
#include <Psapi.h>
#include <string>
#include <vector>
#define SYSTEMHANDLEINFORMATION 16
#pragma comment (lib, "ntdll.lib")
typedef struct _SYSTEM_HANDLE {
ULONG ProcessId;
UCHAR ObjectTypeNumber;
UCHAR Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, *PSYSTEM_HANDLE;
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG HandleCount; // Or NumberOfHandles if you prefer
SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
DWORD UniqueProcessId;
WORD HandleType;
USHORT HandleValue;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG TotalPagedPoolUsage;
ULONG TotalNonPagedPoolUsage;
ULONG TotalNamePoolUsage;
ULONG TotalHandleTableUsage;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
ULONG HighWaterPagedPoolUsage;
ULONG HighWaterNonPagedPoolUsage;
ULONG HighWaterNamePoolUsage;
ULONG HighWaterHandleTableUsage;
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccessMask;
BOOLEAN SecurityRequired;
BOOLEAN MaintainHandleCount;
UCHAR TypeIndex;
CHAR ReservedByte;
ULONG PoolType;
ULONG DefaultPagedPoolCharge;
ULONG DefaultNonPagedPoolCharge;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
EXTERN_C NTSTATUS NTAPI NtDuplicateObject(HANDLE, HANDLE, HANDLE, PHANDLE, ACCESS_MASK, BOOLEAN, ULONG);
bool SetPrivilege(LPCWSTR lpszPrivilege, BOOL bEnablePrivilege = TRUE);
// Find all handle IDs
std::vector<HANDLE> GetCurrentHandlesIDs() {
std::vector<HANDLE> currentHandles;
SetPrivilege(SE_DEBUG_NAME); // Getting required privileges
NTSTATUS status = STATUS_UNSUCCESSFUL;
PVOID buffer = NULL;
ULONG buffersize = 0;
while (true) {
status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SYSTEMHANDLEINFORMATION, buffer, buffersize, &buffersize);
if (!NT_SUCCESS(status)) {
if (status == STATUS_INFO_LENGTH_MISMATCH) {
if (buffer != NULL)
VirtualFree(buffer, 0, MEM_RELEASE);
buffer = VirtualAlloc(NULL, buffersize, MEM_COMMIT, PAGE_READWRITE);
}
continue;
}
else
break;
}
// Enumerate all handles on system
PSYSTEM_HANDLE_INFORMATION handleInfo = (PSYSTEM_HANDLE_INFORMATION)buffer;
ULONG buffersize2 = 0;
for (ULONG i = 0; i < handleInfo->HandleCount; i++) {
PSYSTEM_HANDLE_TABLE_ENTRY_INFO Handle = (PSYSTEM_HANDLE_TABLE_ENTRY_INFO)&handleInfo->Handles[i];
if (!Handle || !Handle->HandleValue || Handle->UniqueProcessId != GetCurrentProcessId())
continue; // Error, no handle, empty handle value, or doesn't belong to this program
HANDLE localHandle = (HANDLE)Handle->HandleValue;
currentHandles.push_back(localHandle);
continue;
}
VirtualFree(buffer, 0, MEM_RELEASE); // Empties buffers to avoid memory leaks
return currentHandles;
}
// Function provided by @etc thanks for finding the solution and providing the source !!
bool SetPrivilege(LPCWSTR lpszPrivilege, BOOL bEnablePrivilege) {
TOKEN_PRIVILEGES priv = { 0,0,0,0 };
HANDLE hToken = NULL;
LUID luid = { 0,0 };
BOOL Status = true;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) {
Status = false;
goto EXIT;
}
if (!LookupPrivilegeValueW(0, lpszPrivilege, &luid)) {
Status = false;
goto EXIT;
}
priv.PrivilegeCount = 1;
priv.Privileges[0].Luid = luid;
priv.Privileges[0].Attributes = bEnablePrivilege ? SE_PRIVILEGE_ENABLED : SE_PRIVILEGE_REMOVED;
if (!AdjustTokenPrivileges(hToken, false, &priv, 0, 0, 0)) {
Status = false;
goto EXIT;
}
EXIT:
if (hToken)
CloseHandle(hToken);
return Status;
}
This code will test only the DLLs inside your System32 directory, but don't forget that many other certificates are whitelisted by anti-cheat, don't hesitate to test others like NVIDIA's for example ...
Anyway, I have left that shit running for a long while and forgot about it and a few weeks later I finally decided to check the results and saw that I detected a bunch of DLLs that gave inheritable handles, so I decided to check that by myself (this code only logs the number of handles and inheritable handles, it doesn't check their type)
The very FIRST DLL that I loaded absolutely BLEW MY MIND.
Third bypass: Make the game load a signed DLL that make the loading process have an inheritable, all access, process handle ON ITSELF
Yep, I felt so stupid not to have thought about it but yeah a legit DLL that when loaded makes the process either do an OpenProcess or a DuplicateHandle on itself does the trick...
Once you have found such a DLL you can make you game load it with AppInit_DLL or DLL replacement then you spawn your cheat or your injector as child of the game, it'll have the handle:
Advice though: Do some extra operations to break the parenthood and get rid of the handle as fast as you can, all this is rather suspicious!
Okay now I am sorry to disappoint, but I won't be giving the list of DLLs that I found that allow just that, but there are SEVERAL in your own system.
If I do give a list, they'll be blacklisted or detected or whatever, but you can find them easily using my DLL scanning code (and more if you improve it certainly)
Only in System32 you have a few of them (don't get tricked though, you'll find several but some of them actually only load the same DLL that does get the handle, not all of them get the handle by themselves).
To find the magic DLLs I mention here, just run the DLL analyser, this is the source code right above.
Fourth bypass: Get executable memory section handle + thread hijacking
That was the idea I was going after at the beginning:
If you find a signed DLL that make the program loading it to have an inheritable handle on an executable section of memory, then you can just inherit it, write your shellcode, thread hijack to execute and you're done.
This will be harder for you to find, oppositely to the process handle DLLs, I only found one DLL that does this.
Hurry: The ACs are securing these techniques
When I found the process handle DLL thing a few months ago, it worked on both EAC and BE, unfortunately, after helping @
Janck7 to implement it on a BattlEye protected game (Fortnite), it appears that BattlEye strips the handle of inherited handles as well now, however, they don't filter the permissions of inherited handles of other types.
And for EAC, strangely enough, it's the other way around: I had some thread handles with lowered permissions while the process handle was inherited with full permissions, weird.
Pro tip: Automate the signed DLL "injection"/replacement
One big downside of those crappy bypasses are that they are really "DIY", rather messy, you have to rename/copy/replace some DLLs to get them loaded to work, it's not very practical.
One way to make that much easier is to use Discord.
In Discord you can select PER GAME which will have the overlay (and therefore the overlay DLL injected):
You can just place the DLL that gives you your wanted inheritable handle instead of the Discord overlay DLL and decide in one click in which games you want it to be loaded, easy
Fifth (very good) bypass ahead
This is probably what's most valuable for experienced hackers reading this article, because all this is fun to read, but it's very messy and not very usable.
Remember that hSonic exploited an unsecure time period when the game starts during which we can OpenProcess and get our handle without it being modified?
Well, this experience gives strong indication that the hSonic vulnerability is still there, uncorrected.
They apparently just found a way to prevent us to get the notification on time using the technique that we used (using job object).
How do I know that?
Well, loading a DLL that does an OpenProcess or DuplicateHandle on the process itself gives a full access process handle right?
Now that could be that the AC lets the game process do just that, so I decided to try myself, a minute after the game is loaded and started, use shellcode execution to OpenProcess or DuplicateHandle on itself, and guess what?
The handle gets modified.
That could indicate that the unsecure time period at the very beginning is still there, and you find a new way to get the notification, you have a new very elegant bypass, that I myself would consider satisfactory enough to use...
I hope you enjoyed your read, and that you have new ideas of experiments in mind now.
Happy hacking.