<:: Introduction
Alpha-Anti-Leak is a client-side anti-cheat software built primarily for Minecraft which uses a wide variety of methods to prevent and to detect cheaters which servers may encounter. While not the most popular anti-cheat for Minecraft, it is a notable member of the anti-cheat club.
While each module has its own system of protection and reverse-engineering deterrence, I have compiled a list of notable subroutines and actions which they do. This article will be split into three sections, one for each respective module of the anti-cheat that I have spent time analyzing and researching.
- AALProtect.sys
- AlphaAntiLeak.exe
- Manual Mapped Module
<:: AALProtect.sys
By far the smallest module in their anti-cheat, the Alpha-Anti-Leak driver, AALProtect.sys
is a very small contributor to their anti-cheat, although I often see it mentioned anytime this anti-cheat is brought up. Regardless, I will go over everything it does and what you need to keep in mind.
::> Process & Thread Handle Operations
Anyone familiar with driver-based anti-cheats are familiar with ObRegisterCallbacks
, or have at least heard of it before. ObRegisterCallbacks
is often used to strip handles from processes it wants to protect to prevent unwanted programs from accessing it and performing unwarranted operations. Alpha-Anti-Leak is no different.
Alpha-Anti-Leak registers callbacks for both process and thread operations, both pointing to the same pre-operation callback (which has a different operation for both types) while also having no post-operation callback.
OB_OPERATION_REGISTRATION *v7;
OB_OPERATION_REGISTRATION ProcessCallbacks;
OB_OPERATION_REGISTRATION ThreadCallbacks;
UNICODE_STRING DestinationString;
callback_version = ObGetFilterVersion();
if ( callback_version == 0x100 ) // OB_FLT_REGISTRATION_VERSION
{
ProcessCallbacks.ObjectType = (PVOID)PsProcessType;
ProcessCallbacks.PobPreOperationCallback = (void *(__fastcall *)(PVOID, PVOID))ObjectPreOperationCallback;
ProcessCallbacks.PobPostOperationCallback = (void *(__fastcall *)(PVOID, PVOID))nullsub_1;
ThreadCallbacks.PobPreOperationCallback = (void *(__fastcall *)(PVOID, PVOID))ObjectPreOperationCallback;
ThreadCallbacks.PobPostOperationCallback = (void *(__fastcall *)(PVOID, PVOID))nullsub_1;
ThreadCallbacks.ObjectType = (PVOID)PsThreadType;
LODWORD(ProcessCallbacks.Operations) = 3;
LODWORD(ThreadCallbacks.Operations) = 3;
Dst = 0x20100;
RtlInitUnicodeString(&DestinationString, L"362841");
v6 = &v11;
v7 = &ProcessCallbacks;
ret = ObRegisterCallbacks(&Dst, &ObjectCallbackRegistration);
}
return ret;
The callback registered for both threads and processes is pretty simple in its own way. The executed operation for both PsProcessType
and PsThreadType
are almost identical, except for where it gets the current process of the thread. You can see the main executed operation below.
process = (struct _EPROCESS *)IoThreadToProcess(thread);
if ( !(OperationInformation->Flags & 1) && qword_140007090 != PsGetCurrentThreadId() )
{
v14 = 0;
if ( IsProcessChild((__int64)process) )
{
if ( process != IoGetCurrentProcess() )
{
v15 = IoGetCurrentProcess();
if ( !IsProcessCrssOrAudioDg((__int64)v15) )
{
if ( OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE )
{
v16 = &v21;
do
{
v17 = OperationInformation->Parameters;
if ( *v16 & v17->OriginalDesiredAccess )
v17->DesiredAccess &= ~*v16;
++v14;
++v16;
}
while ( v14 < 8 );
}
else if ( OperationInformation->Operation == OB_OPERATION_HANDLE_DUPLICATE )
{
v18 = &v21;
do
{
v19 = OperationInformation->Parameters;
if ( *v18 & v19->OriginalDesiredAccess )
v19->DesiredAccess &= ~*v18;
++v14;
++v18;
}
while ( v14 < 8 );
}
}
}
}
}
The driver’s object handle callback does exactly what you thought it does, stripping the handles to prevent access from unwanted sources by inverting the desired access. This is pretty normal for any object handle callback and you will see it in just about every other driver anti-cheat.
::> File Operation Minifilters
The anti-cheat driver registers a minifilter post IO operation routine which checks for a specific list of modules (dll) and will cancel the operation if it detects them. This operation is shown below in the post operation callback.
if ( v2 >= 0 && v2 != 260 && (signed int)FltGetFileNameInformation(a1, 257i64, &v10) >= 0 )
{
FltParseFileNameInformation(v10);
*(_DWORD *)&String2.Length = 524294;
String2.Buffer = L"dll";
v5 = 0;
if ( RtlEqualUnicodeString((PCUNICODE_STRING)(v10 + 56), &String2, 1u) )
{
v6 = IoThreadToProcess(*(_QWORD *)(v4 + 8));
v7 = PsGetProcessId(v6);
if ( !GetChildProcessId(v7) )
return 0i64;
v13 = L"aswhooka.dll";
v15 = L"aswhook.dll";
v17 = L"bgagent.dll";
v19 = L"guard64.dll";
v21 = L"iseguard64.dll";
v23 = L"atcuf64.dll";
v25 = L"avcuf64.dll";
v27 = L"dtrampo.dll";
v29 = L"jhook.dll";
v31 = L"a2hooks.dll";
v33 = L"avghooka.dll";
v35 = L"sysfer.dll";
v37 = L"keycrypt64.dll";
v39 = L"keycrypt64(1).dll";
v41 = L"keycrypt64(2).dll";
v43 = L"keycrypt64(3).dll";
v45 = L"keycrypt64(4).dll";
v47 = L"keycrypt64(5).dll";
v49 = L"lavasofttcpservice64.dll";
v51 = L"iswshex.dll";
do
{
v5 = RtlEqualUnicodeString((PCUNICODE_STRING)(v10 + 88), (PCUNICODE_STRING)&v12[4 * v8], 1u);
if ( v5 )
break;
++v8;
}
while ( v8 < 0x14 );
}
FltReleaseFileNameInformation(v10);
if ( v5 )
{
FltCancelFileOpen(*(_QWORD *)(v3 + 24), *(_QWORD *)(v3 + 32));
}
}
The minifilters pretty much speak for themselves, if you want to look into it anymore you can look into where they are registered and the communication port is declared at 0x0000000140001AE8
.
::> Kernel Debugger Check
The driver has a very basic kernel debugger check using ZwQuerySystemInformation
. The driver accesses ZwQuerySystemInformation
through MmGetSystemRoutineAddress
, passing “ZwQuerySystemInformation” as a string. The kernel debugger check passes request for the SystemKernelDebuggerInformation
system class and then checks if it is present or not enabled.
if ( !InitializeZwQuerySystemInformation() )
return 0xC0000002i64;
result = pZwQuerySystemInformation(0x23i64, &v4, 2i64, &v5);
if ( (signed int)result >= 0 )
{
if ( (_BYTE)v4 || !BYTE1(v4) )
v2 = 1;
*v1 = v2;
result = 0i64;
}
::> Misc.
The anti-cheat driver does a lot of logging, which is unusual for a device which is made to keep cheaters and reverse engineers out, while also doing nothing with the data that it logs. The objects/devices which it logs are probably the first thing anyone who opens the driver will notice. Before AALProtect.sys registers any of its minifilters or registers its object handle callbacks it logs multiple hardware identifiers, such as the CPUID and HDD serial information.
result = CheckWindowsNTVersion();
if ( (signed int)result < 0 )
return result;
v3 = GetCpuProcessorBrand((__int64)&P);
DbgPrintEx(77i64, 0i64, "CPUID: %d %Z\n", v3, P);
if ( (v3 & 0x80000000) != 0 )
return v3;
FreeAALPools(P);
v3 = GetSystemFirmwareTableData(&v16);
DbgPrintEx(77i64, 0i64, "SMBIOS: %d %d\n", v3, v16);
if ( (v3 & 0x80000000) != 0 )
return v3;
v4 = sub_140001FCC((__int64)v16);
DbgPrintEx(77i64, 0i64, "SMBIOS handles %d\n", v4, v14);
for ( i = 0; i < v4; ++i )
{
v6 = (_BYTE *)sub_140001C34((__int64)v16, i);
if ( v6 )
{
if ( *v6 == 4 )
{
LOBYTE(v7) = v6[15];
v8 = sub_1400020BC(v6, v7);
DbgPrintEx(77i64, 0i64, "SMBIOS CPU: %s\n", v8, v15);
}
}
}
FreeAALSPool(v16);
v19 = 0x3A0038;
v20 = L"\\Device\\Harddisk0\\Partition0";
PrintHDDSerial((UNICODE_STRING *)&v19, &v18);
There’s not much to really say about this since all this does is print the hardware specifications to the driver debug output, instantly freeing all the pools which the identifiers are stored inside of them.
<:: AlphaAntiLeak.exe
AlphaAntiLeak.exe is the main forefront of the anti-cheat capabilities, being the main process which is run when using any Minecraft client which utilizes AlphaAntiLeak. It is important to know that I have not completed a full analysis of this module primarily due to the fact that a large portion of it is virtualized by VMProtect, which is instantly evident due to the entrypoint containing an entrance to the VM. There is quite a lot to go over in this module specifically so I will list all of the different sections below.
- TLS Callback
- Scanning Functions
- Hooked Functions
- Detection Codes
- Other Notable Functions
::> TLS Callback
One of the first things I do when reversing a new program is check the exports and imports. The first thing you should see when you check the exports of AlphaAntiLeak.exe is that it utilizes a TLS Callback, a subroutine that is called before the main entrypoint of the program. It should be immediately obvious what this TLS Callback is doing by taking a glance at it.
ntdll = GetModuleHandleA("ntdll.dll");
LdrLoadDll = GetProcAddress(ntdll, "LdrLoadDll");
LdrLoadDllAddress = (__int64)LdrLoadDll;
v5 = j_AAL_PlaceJmpRipHook((__int64)&Buffer, (__int64)LdrLoadDll, (__int64)Hook_LdrLoadDll, 0i64);
IsLdrLoadDllHookInitialized = *(_OWORD *)v5;
LdrLoadDll_AllocatedMemory = *(_OWORD *)(v5 + 0x20);
If you don’t understand what’s going on, I’ll break it down for you. The code above is getting a pointer to LdrLoadDll from ntdll using GetProcAddress. It then passes the address to their hooking function which will place a jmp [rip+addr]
instruction in the prologue of the function, redirection flow of the function to Hook_LdrLoadDll
which will be analyzed later. It then saves the allocated memory and whether the hook has initialized successfully as global variables to use later.
The TLS callback also handles new thread spawns, killing it if it is not inside of a MEM_IMAGE
.
v9 = GetCurrentThread();
v10 = (const void *)DuplicateThread(v9);
if ( VirtualQuery(v10, &Buffer, 0x30ui64) && Buffer.Type != 0x1000000 )// is new thread in MEM_IMAGE?
{
v11 = GetCurrentThread(); // Terminate
TerminateThread(v11, 0);
}
*localalloc = 1;
If you were ever injecting a DLL into any process protected by AlphaAntiLeak, you shouldn’t be creating threads, however you can also see it doesn’t look like they report anything to the server for this activity in the TLS Callback.
::> Scanning Functions
AlphaAntiLeak has a large variety of scanning functions held inside of the .text section and unvirtualized which are all run on runtime, some very easy to detect and others managing to stay under the radar. Keep in mind not all of their detection methods reside in the .text section. Information on how to bypass all of these checks will be at the bottom of this section.
::>::> AAL_DetectDebuggers
Most programs have a simple debugger check using IsDebuggerPresent(), AlphaAntiLeak.exe is no different, however it has a lot more methods of also detecting if the current process is being debugged.
The runtime function starts by setting up the encryption key to the server, which you will see being done in anything that sends information to the server.
linux_time = j_GetLinuxTime();
real_time = 100 * (linux_time + 100000);
encryption_key.finalkey = ((unsigned __int64)((unsigned __int128)(real_time * (signed __int128)0x112E0BE826D694B3i64) >> 64) >> 63)
+ ((signed __int64)((unsigned __int128)(real_time * (signed __int128)0x112E0BE826D694B3i64) >> 64) >> 26);
encryption_key.sharedkey = real_time - 1000000000 * LODWORD(encryption_key.finalkey);
The debug scanner runs through all checks, sending a detection packet for each check which is verified. All of these checks are listed in order with a code snippet attached.
IsDebuggerPresent check:
if ( IsDebuggerPresent() )
{
LOBYTE(found_debugger) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v135, found_debugger);
}
CheckRemoteDebuggerPresent check:
CheckRemoteDebuggerPresent(current_process, &pbDebuggerPresent);
if ( pbDebuggerPresent )
{
LOBYTE(found_remote_debugger) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v136, found_remote_debugger);
}
PEB->BeingDebugged check:
PEB = __readgsqword(0x60u);
if ( *(_BYTE *)(PEB + 2) )
{
LOBYTE(being_debugged) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v132, being_debugged);
}
E Flags Trap check:
e_flags = __readeflags();
if ( _bittest64((const signed __int64 *)&e_flags, 8u) )
{
LOBYTE(trap_flag) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v133, trap_flag)
}
ReadProcessMemory Stack check:
current_process_id = GetCurrentProcessId();
process_handle = OpenProcess(PROCESS_ALL_ACCESS, 0, current_process_id);
if ( ReadProcessMemory(process_handle, &BaseAddress, &BaseAddress, 8ui64, &NumberOfBytesRead) )
{
LOBYTE(stack_detection) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v134, stack_detection);
}
::>::> AAL_DetectModifiedModules
Upon running, the following modules are scanned, stored in a list, and then compared to the modules on disk, checking for discrepancies and then sending a detection packet if one is found.
- kernel32.dll
- ntdll.dll
- user32.dll
- kernelbase.dll
- jvm.dll
- AlphaAntiLeak.exe
The iteration itself is plainly simple and the check to see if the module has been modified is also evident to the naked eye.
for ( loaded_module = (__int64 *)module_list; loaded_module != v6; ++loaded_module )
{
AAL_CompareModuleHash(v1, (__int64)&disk_module, *loaded_module, 0i64);
v12 = disk_module;
v13 = v67;
if ( disk_module != v67 )
{
LOBYTE(detected_modified_dll) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v89, detected_modified_dll);
}
}
Something to note about this scanning method is that it sends two different detection packets to the server, one for a modified dll and another for a detected modified AlphaAntiLeak.exe. You’ll notice that the check is very similar, except it gets the base module as the compare parameter.
base_module = GetModuleHandleA(0i64);
AAL_CompareModuleHash(v1, (__int64)&v77, (__int64)base_module, 0i64);
v36 = v78;
v37 = (__int64)v77;
if ( disk_module != v78 )
{
LOBYTE(detected_modified_exe) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v90, detected_modified_exe);
}
::>::> AAL_DetectCEDriver
One of the most popular cheating and runtime memory analysis tools, Cheat Engine
, is used by almost every video game cheat developer, and for a reason, it is one of the best tools for what it does. The developers of AlphaAntiLeak know this however, and they put detections in to stop this. The first protection is one you have already seen, by stripping the handles of the application it makes it harder to actually get the process id of the application from usermode. The get around this, Cheat Engine has their own driver you can use. This scanning function is centered around checking if the driver is running or if it exists. The check itself is very self explanatory and it should be easy enough to figure out what is going on.
sub_7FF772EA3599((__int64)&v31, (__int64)L"CEDRIVER60", 10i64);
LOBYTE(v4) = 1;
if ( (unsigned int)j_AAL_DoesServiceExist((__int64)&v31, v4) == 1
|| (unsigned __int8)j_AAL_DoesFileExist(v2, (__int64)L"\\\\.\\CEDRIVER60", (__int64)&i) )
{
LOBYTE(cheat_engine_driver_detected) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v38, cheat_engine_driver_detected);
}
::>::> AAL_DetectModifiedHooks
This was one of the more difficult runtime detections to reverse due to the fact that most of the hooks scanned in this subroutine are stored inside of RuntimeDetection* class (RCX) and I have no way to see what that pointer is without devirtualizing the module and seeing where this function is called from.
The first and easiest hook to see is where they check to ensure that LdrLoadDll
is still hooked, comparing it to the original bytes of the function stored in the .data section.
if ( (_BYTE)IsLdrLoadDllHookInitialized
&& (**((_QWORD **)&IsLdrLoadDllHookInitialized + 1) != *((_QWORD *)&LdrLoadDll_AllocatedMemory + 1)
|| *(_DWORD *)(*((_QWORD *)&IsLdrLoadDllHookInitialized + 1) + 8i64) != (_DWORD)xmmword_7FF773295900
|| *(_WORD *)(*((_QWORD *)&IsLdrLoadDllHookInitialized + 1) + 12i64) != WORD2(xmmword_7FF773295900)) )
{
LOBYTE(IsLdrLoadDllHookInitialized) = 0;
v5 = 0;
}
else
{
v5 = 1;
}
if ( !v5 )
{
LOBYTE(detected_modified_ldrloaddll) = 1;
j_AAL_InitializeDetectionInformations((__int64)&v116, detected_modified_ldrloaddll);
v6 = *((_QWORD *)&IsLdrLoadDllHookInitialized + 1);
if ( (_BYTE)IsLdrLoadDllHookInitialized )
v6 = LdrLoadDll_AllocatedMemory;
There are three additional hook checks within this function which I am not able to verify where they come from due to the reasons listed above.
Bypassing these scans is actually very simple, and it can be done it two ways. You can either bypass the VMProtect .text modification check and hook the scanning functions, returning them, or you can find the origins of the thisptr for all of these scanning functions and set thisptr + 0x68
to true
, preventing the anti-cheat from running any of these scans.
::> Hooked Functions
There are a few Windows functions which we can identify are hooked very easily, one of them I already mentioned. A list of hooked Windows functions follows below:
- ntdll.dll!LdrLoadDll
- ntdll.dll!NtCreateThread
- ntdll.dll!NtCreateThreadEx
- User32.dll!WH_KEYBOARD_LL
- User32.dll!WH_MOUSE_LL
::>::> LdrLoadDll
The LdrLoadDll hook is placed in the TLS callback of the application and does multiple checks on execution to determine if the library should be allowed to load. The original function is only called if RtlCaptureStackBackTrace
returns zero or if the module is not in a list of allowed modules.
The LdrLoadDll hook contains direct bytecode strings to detoured.dll
and ig9icd64.dll
. I have not found a reference to detoured.dll
in any other module.
::>::> NtCreateThread & NtCreateThreadEx
The hooks for NtCreateThread
and NtCreateThreadEx
are the same for the most part, so I will be putting both in this section. No detections are sent to the server on execution of these functions, however it is tracked when a thread is created. The original function of NtCreateThread
and NtCreateThreadEx
are always called and returned.
::>::> AAL_LowLevelKeyboardHook
I see mentioned on many forums that you can’t use an autoclicker (at least a normal one) on clients protected by AAL, and this is due to the lowlevel hooks they place on both the keyboard and mouse. Both hooks are placed using SetWindowsHookExA
and is called from the virtualized section of the application, so when these are initialized is unknown. Before the hook is placed, the current thread is set to THREAD_PRIORITY_TIME_CRITICAL
priority, meaning that it will be the first run anytime Windows needs to call one of these callbacks. The hook itself is a callback of LowLevelKeyboardProc
. The hook itself is extremely basic, returning 1
if the hook is called from an injected source (emulated).
LRESULT __fastcall AAL_LowLevelKeyboardHook(int nCode, WPARAM wParam, LPARAM lParam)
{
LRESULT result; // rax
if ( nCode || !(param->flags & LLKHF_INJECTED)) )
result = CallNextHookEx(qword_7FF7732AA140, nCode, wParam, lParam);
else
result = 1i64;
return result;
}
::>::> AAL_LowLevelMouseHook
Similar in loading fashion to the LowLevelKeyboard hook, the LowLevelMouse hook is initialized right before the keyboard hook and does. Similar to the keyboard hook, if the parameter is injected it will immediately return 1
, effectively canceling the input. However, the developers know this can be bypassed and they have setup a secondary defense by logging the amount of times a specific mouse button is clicked. The code below is commented to be read easily.
LRESULT __fastcall AAL_LowLevelMouseHook(int nCode, WPARAM a2, LPARAM a3)
{
LPARAM lParam; // rsi
WPARAM wParam; // rdi
_DWORD *v7; // rbx
int mouseData; // eax
unsigned int v9; // eax
unsigned int v10; // eax
lParam = a3;
wParam = a2;
if ( !nCode )
{
if ( *(_BYTE *)(a3 + 0xC) & 1 ) // Injected?
return 1i64;
switch ( a2 )
{
case 0x201ui64: // WM_LBUTTONDOWN
v7 = WM_LBUTTONDOWN_ClickCount;
LABEL_16:
v9 = sub_7FF772EA91B0((__int64)&unk_7FF7732AA150);
if ( v9 )
sub_7FF772EA69E7(v9); // Increment click count
++*v7;
v10 = sub_7FF772EA7C9D((__int64)&unk_7FF7732AA150);
if ( v10 )
sub_7FF772EA69E7(v10);
return CallNextHookEx(hhk, nCode, wParam, lParam);
case 0x204ui64: // WM_RBUTTONDOWN
v7 = &WM_RBUTTONDOWN_ClickCount;
goto LABEL_16;
case 0x20Bui64: // WM_XBUTTONDOWN
mouseData = *(_DWORD *)(a3 + 8);
if ( mouseData == 1 ) // XBUTTON1
{
v7 = &XBUTTON1_ClickCount;
goto LABEL_16;
}
if ( mouseData == 2 ) // XBUTTON2
{
v7 = &XBUTTON2_ClickCount;
goto LABEL_16;
}
break;
default:
if ( a2 == 0x20A && *(_DWORD *)(a3 + 8) == 120 )// WM_MOUSEWHEEL && mouseData == WHEEL_DELTA
{
v7 = &WM_MOUSEWHEEL_ClickCount;
goto LABEL_16;
}
break;
}
}
return CallNextHookEx(hhk, nCode, wParam, lParam);
}
::> Detection Codes
Below I have compiled a list of different detection codes which I have come across and are sent to the server in a detection packet. There are of course a lot more detection packets, however a lot of them reside in the virtualized section of the program.
- 5 -> RemoteDebugger found
- 6 -> PEB->BeingDebugged set to true
- 37 -> Modified jvm.dll, kernelbase.dll, user32.dll, ntdll.dll, kernel32.dll
- 38 -> eflags trap flag set to true
- 39 -> Modified AlphaAntiLeak.exe
- 42 -> LdrLoadDll Unhooked
- 50 -> Something about modules in address space
- 53 -> Cheat Engine Service detected
::> Other Notable Functions
This is a list of other notable function I found while reversing this module, some more important than others, in no particular order.
::>::> AAL_UploadCrashReport
Using WinINet.dll functions to open an internet connection and send a POST request to alphaantileak.net/api/v3/users/crash/report
with a json struct containing information sent from AAL_Crash
. Pretty self-explanatory.
::>::> AAL_Crash
This funciton is called upon a fatal and unrecoverable exception, getting the time of the exception and dumping the entire stack as well as walking it backwards. It stores the resulting crash information inside of TEMP
and then sends the file as .json to the server in AAL_UPLOADCRASHREPORT
.
::>::> TFTP
AlphaAntiLeak utilizes TFTP to send files to their server from your computer. I have not put in enough time to really decipher what they’re sending just yet, but it should be something you keep in mind.
::>::> AAL_DefineDynamicJavaClasses
AlphaAntiLeak touts their “powerful technique” to inject “your classes into memory”, however you can very easily dump these “injected” java classes by hooking this function where they are stored into a Java VM byte array and then defined as a class under the AAL scope. This function is quite complex, and you cannot see exactly where it is called from since it is executed from within the virtualized section. Something that should help you is to know that the first argument rcx
is a pointer to the JNIEnv*
.
v173 = -2i64;
v3 = a3;
v172 = a3;
a2a = a2;
v5 = ((__int64 (__fastcall *)(JNIEnv *, __int64))JNIEnv->vtable->GetStringLength)(JNIEnv, a3);
v171 = ((__int64 (__fastcall *)(JNIEnv *, __int64, _QWORD))JNIEnv->vtable->GetStringUTFChars)(JNIEnv, v3, 0i64);
v6 = 0;
v183 = 0i64;
v184 = 15i64;
LOBYTE(v182) = 0;
sub_7FF772EAE624((__int64)&v182, v171, v5);
_mm_storeu_si128((__m128i *)&v189, _mm_load_si128((const __m128i *)&xmmword_7FF77317AA30));
LOBYTE(v188) = 0;
sub_7FF772EAE624((__int64)&v188, (__int64)"net.aal.", 8i64);
v7 = sub_7FF772EA77E8((__int64)&v182, (__int64)&v188);
Above you can see where they are getting the name of the class which is passed as a parameter and converting the java string into UTF characters before concatenating it with net.aal.
.
byte_len = sub_7FF772EADE7C(v29);
v31 = sub_7FF772EADE7C(v29);
class_bytes_1 = sub_7FF772EA1564(v29, v31);
class_loader = ((__int64 (__fastcall *)(JNIEnv *, const char *))JNIEnv->vtable->FindClass)(
JNIEnv,
"java/lang/ClassLoader");
if ( ((unsigned __int8 (__fastcall *)(JNIEnv *))JNIEnv->vtable->ExceptionCheck)(JNIEnv) )
((void (__fastcall *)(JNIEnv *))JNIEnv->vtable->ExceptionDescribe)(JNIEnv);
preDefineClass = ((__int64 (__fastcall *)(JNIEnv *, __int64, const char *, const char *))JNIEnv->vtable->GetMethodID)(
JNIEnv,
class_loader,
"preDefineClass",
"(Ljava/lang/String;Ljava/security/ProtectionDomain;)Ljava/security/ProtectionDomain;");
defineClassSourceLocation = ((__int64 (__fastcall *)(JNIEnv *, __int64, const char *, const char *))JNIEnv->vtable->GetMethodID)(
JNIEnv,
class_loader,
"defineClassSourceLocation",
"(Ljava/security/ProtectionDomain;)Ljava/lang/String;");
postDefineClass = ((__int64 (__fastcall *)(JNIEnv *, __int64, const char *, const char *))JNIEnv->vtable->GetMethodID)(
JNIEnv,
class_loader,
"postDefineClass",
"(Ljava/lang/Class;Ljava/security/ProtectionDomain;)V");
if ( ((unsigned __int8 (__fastcall *)(JNIEnv *))JNIEnv->vtable->ExceptionCheck)(JNIEnv) )
((void (__fastcall *)(JNIEnv *))JNIEnv->vtable->ExceptionDescribe)(JNIEnv);
j_AAL_EncryptClassBytes(class_bytes_1, byte_len);
Here we can see where the function gets the standard JVM ClassLoader
to be used further along to actually define the class and is used to get the methods preDefineClass
and postDefineClass
from the JVM. This is all followed by an exception check and then a call to AAL_EncryptClassBytes
which will be analyzed later. The subroutine then goes to check if the input class
file is actually valid, running it through multiple checks before decrypting the passed class into its bytes and initializing a new array with it and calling preDefineClass
with the name of the concatonated class.
class_byte_array = ((__int64 (__fastcall *)(JNIEnv *, _QWORD))JNIEnv->vtable->NewByteArray)(JNIEnv, v101);
((void (__fastcall *)(JNIEnv *, __int64, _QWORD, _QWORD, __int64, unsigned __int64))JNIEnv->vtable->SetByteArrayRegion)(
JNIEnv,
class_byte_array,
0i64,
v101,
class_bytes,
v154);
concat_string = (__m128i *)&concat_string;
utf_class_string = ((__int64 (__fastcall *)(JNIEnv *, __m128i *))JNIEnv->vtable->NewStringUTF)(
JNIEnv,
concat_string);
preDefineClass = j_JNI_CallObjectMethod(JNIEnv, a2a, preDefineClass, utf_class_string, 0i64);
j_JNI_CallObjectMethod(JNIEnv, a2a, defineClassSourceLocation, preDefineClass);
After doing more checks after the preDefineClass call, the subroutine will finally declare the new class inside of the JVM by calling DefineClass and passing the deobfuscated class bytes.
class_name = &v179;
if ( v111 >= 0x10 )
class_name = v120;
LODWORD(class_byte_length) = v101;
v126 = ((__int64 (__fastcall *)(JNIEnv *, __m128i *, __int64, __int64, __int64))JNIEnv->vtable->DefineClass)(// DefineClass
JNIEnv,
class_name,
loader,
class_bytes,
class_byte_length);
j_JNI_CallVoidMethodV(JNIEnv, loader, postDefineClass, klass, preDefineClass);
The function returns a jclass
pointer to the newly created/defined class by using FindClass so that it can check if it messed up during creation.
::>::> AAL_EncryptClassBytes
This small function is responsible for encrypting the Java class bytes passed to it according to a global hash table. There’s not much to say about it. You can trace it back to everywhere the application needs to encrypt the bytes of a class.
unsigned __int64 __fastcall AAL_EncryptClassBytes(__int64 bytes, unsigned __int64 len)
{
v2 = bytes;
v3 = v15;
v4 = 8i64;
v5 = (__int128 *)&HashTable;
do
{
v3 += 128;
v6 = *v5;
v7 = v5[1];
v5 += 8;
*((_OWORD *)v3 - 8) = v6;
v8 = *(v5 - 6);
*((_OWORD *)v3 - 7) = v7;
v9 = *(v5 - 5);
*((_OWORD *)v3 - 6) = v8;
v10 = *(v5 - 4);
*((_OWORD *)v3 - 5) = v9;
v11 = *(v5 - 3);
*((_OWORD *)v3 - 4) = v10;
v12 = *(v5 - 2);
*((_OWORD *)v3 - 3) = v11;
v13 = *(v5 - 1);
*((_OWORD *)v3 - 2) = v12;
*((_OWORD *)v3 - 1) = v13;
--v4;
}
while ( v4 );
result = 0i64;
if ( len )
{
do
{
*(_BYTE *)(v2 + result) ^= v15[result & 0x3FF];// Cap at 1024
++result;
}
while ( result < len );
}
return result;
}
As stated at the start of this section, AlphaAntiLeak.exe
contains the most protections and detections, and unfortunately they recently started virtualizing a lot of their module so I am unable to go much further without devirtualizing the module. If anyone has an old version of this module, I would gladly take it.
<:: Manually Mapped Module
During runtime, the server sends multiple manually mapped modules to the client to be loaded in. Many of these modules contain little to no info, but one of them I noticed was a lot bigger than the others. Upon opening, we can see that the PE header is erased and data seems to be strewn all over the place, a lot of functions leading to no where and a lot of references to memory that isn’t in the current module.
After scrolling through this module for a little while I noticed a recurring theme of what would look almost like a naked hook, except it was executing a call. Following the call address, which didn’t exist in the current module, I was able to find that it was pointing to the jvm.dll module directly.
Due to the nature of the JVM, any function that wants to access elements inside of the VM must first ‘enter’ it and set the Thread state as well as preserve all current registers, which is what this function is emulating. Creating a quick signature of the naked prologue which preserves the registers I am able to generate a list of all naked entrances to the JVM.
The list of them is quite huge, so below I have put a picture of just some of the functions which I found to be interested, all of them called with a ‘naked-call’.
It’s very easy to determine where this module accesses the JVM, which can be done by doing a quick byte scan of the JVM module base address. After spending a lot of time going through many of these references I have come up with a few which I thought were interesting and should be looked into further.
::> HandleExceptionFromCodeCache
This tiny function calls the Windows specific exception filter HandleExceptionFromCodeCache
. With over 50 references to this location (none of which are in any subroutines, which can be attributed to the way this module is streamed) has me thinking they use this as a fail-safe exception fallback.
::> raw_exception_handler_for_return_address
Another small function related to exceptions which I’m also assuming is related to failed calls/executions as it is called almost 500 times from this module alone.
::> CRC Hashing
AlphaAntiLeak seems to have implemented their own crc hashing function within this mapped module while still using the pre-existing JVM CRC hash table. I did some CRC spoofing not too long ago, and a lot of this looks all too familiar to me which is how I found it in the first place. Where it is actually used is a little bit harder to decipher though.
Having no list of imports, exports, and no decipherable strings makes this mapped module a lot harder to actually reverse engineer, and I will likely come back to it in the future to look into it more.
<:: Additional Notes
The JVM packaged with LunarClient/AlphaAntiLeak is completely open-source which means you could find everything they’re referencing within their modules quite easily. However, this also means that they are able to modify everything within the JVM themselves and send it directly to you. The first modification which you are very likely to notice is how they made JNI_GetCreatedJavaVMs
return nothing. This is just a small example of modifications they can make without having to be runtime at all. The created VM list still exists, just look at the CreateJavaVM function to find it if you need it.
If you need addresses for any of the functions I have mentioned above, please feel free to reach out to me and ask for them and I will be happy to provide.
Thank you for reading!
<:: Credits
Huge thanks to Forza for his continued advice and guidance on the virtualization of AlphaAntiLeak.exe and many of their methods that I was confused on. I would not have been able to release this much information without his help.
Awesome post! Even though there are no comments here, don’t forget that there is always someone reading it 🙂