Amlegit is an Apex legendary cheating software bundled with HWID deceiver, and its user base is slightly higher than 3000 users. Cheating itself provides a 2d frame esp, silent aiming and other functions. As you will see in this extensive article, this cheating is just a publicly released exploit and source code paste. The communication method shown below is the basic IOCTL hook of a system driver.
The launcher I'm going to talk about is actually their "second stage" launcher. The first stage of the initiator downloads and puts all QT files / executables in a random temporary folder, and then starts the second initiator. There are three files in this temporary folder, buffer.dll, inject.dll and mmap.dll. All these files are processed by VMP, but their export tables can still be parsed. Opening each of them in Ida shows me at least two exports (starting and another function defined by the amlegit programmer). Although the export table is resolvable, the function itself is not, and it is incomprehensible even when viewing the function data at run time. In order for me to call these export functions myself, I need to know the calling convention (probably fastcall), parameters and return type. This can be done by looking at what calls these export functions. Fortunately, the second stage initiator has xrefs for all these functions except GetDriver (referenced in the dump of inject.dll).
The exported connection takes a zero parameter and returns a Boolean value. This function establishes communication between the user state process and the kernel driver.
.text:00007FF7D93BA7E1 call export_connect .text:00007FF7D93BA7E3 test al, al
Export injection requires two parameters. The first is the name of the dll on the disk to be injected into the process specified by the second parameter. The second parameter takes the class name of the window. This function is used to inject dll into the game.
.text:00007FF7D93BABFE ; --------------------------------------------------------------------------- .text:00007FF7D93BABFE lea rdx, aRespawn001 ; "Respawn001" .text:00007FF7D93BAC05 lea rcx, aLapexDll ; "lapex.dll" .text:00007FF7D93BAC0C .text:00007FF7D93BAC0C loc_7FF7D93BAC0C: ; DATA XREF: sub_7FF7D95E2D10-97C6Dâo .text:00007FF7D93BAC0C call export_inject_addr ; ExportInject("lapex.dll", "Respawn001"); .text:00007FF7D93BAC0E test al, al
The exported payload takes no parameters and returns a Boolean value. This function loads the intel lan driver. (part of kdmapper).
.text:00007FF7D93BAD9E call export_loader_addr ; ExportLoad() .text:00007FF7D93BADA0 test al, al
The export map accepts a parameter that is a character pointer and returns a Boolean value. This function maps unsigned drivers to the kernel.
.text:00007FF7D93BAE03 lea rcx, aDriverSys ; "driver.sys" .text:00007FF7D93BAE0A call export_map_addr ; ExportMap("driver.sys") .text:00007FF7D93BAE0C test al, al
As shown in the figure above, mmap.dll exports two functions (three in total, including dllmain). Although these functions are exported from mmap.dll, you will not be able to resolve their role because they are highly virtualized and confused. Having said that, if we can parse the function call to any of these functions, we are likely to be able to determine the parameters and return types.
So if we can't parse what these functions are doing, why should we look inside the module? So simply load mmap.dll into buffer.dll and call GetDriver. This will allow us to view the parameters and return types of GetDriver.
.text:00000000000034FD lea rcx, aBufferDll ; "buffer.dll" .text:0000000000003504 call cs:GetModuleHandle .text:000000000000350A xor r12d, r12d .text:000000000000350D test rax, rax .text:0000000000003510 jz loc_390E .text:0000000000003516 lea rdx, aGetdriver ; "GetDriver" .text:000000000000351D mov rcx, rax ; hModule .text:0000000000003520 call cs:GetProcAddress_1 .text:0000000000003526 test rax, rax .text:0000000000003529 jz loc_390E .text:000000000000352F mov [rsp+0A8h+driver_size], 1 .text:0000000000003537 lea rcx, [rsp+0A8h+driver_size] .text:000000000000353C call rax .text:000000000000353E mov r15, rax
Let's decompose the assembly. The first thing we see is the call to GetModuleHandle. You can see that GetModuleHandle accepts a character pointer because we move the string address "buffer.dll" in this example to rcx (if you don't know it is the first register used to pass values in the fastcall calling convention) up to 64 bits). Next, we see a test rax, rax. In short, this instruction tests whether rax is zero, and then stores the result in the zero flag register. The test instruction also provides us with very important information about the return value (and size) of the function. In fastcall, rax is a register containing the return value of a value (byte, word, doubleword, qword) with a size of 64 bits or less. By reading the assembly, we can know that the entire 64 bit data is being used (which means it returns a long long). This is in line with our expectations for GetModuleHandle, which accepts a long pointer to an ac style string and returns a value with a size matching 64 bits. Remember that HMODULE can be 32-bit or 64 bit, depending on its operating environment.
//HMODULE GetModuleHandleA(LPCSTR lpModuleName); .text:00000000000034FD lea rcx, aBufferDll ; "buffer.dll" .text:0000000000003504 call cs:GetModuleHandle .text:000000000000350A ... .text:000000000000350D test rax, rax
After we call GetModuleHandle, we will call GetProcAddress_1. It is the wrapper function of the real GetProcAddress. First, we move the address of "GetDriver" into rbx, which is the register for the second parameter (integer value). Then we move the result from GetModuleHandle to rcx, which is the register of the first integer value parameter, as you may know by now. The result will be a 64 bit length, which is inferred from the test and branch operation rights. It is also consistent with the MSDN definition of GetProcAddress.
//FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName); .text:0000000000003516 lea rdx, aGetdriver ; "GetDriver" .text:000000000000351D mov rcx, rax ; hModule .text:0000000000003520 call cs:GetProcAddress_1 .text:0000000000003526 test rax, rax
Now we have the address of GetDriver in rax, and we will call it. If you look at this assembly, you will assume that we put 1 into rcx, but we are actually loading the effect address of 1 (lea). So this tells use GetDriver to take the pointer as its only parameter. The return value is also inferred by subsequent operations of rax.
//std::uintptr_t GetDriver(unsigned* size); .text:000000000000352F mov [rsp+0A8h+driver_size], 1 .text:0000000000003537 lea rcx, [rsp+0A8h+driver_size] .text:000000000000353C call rax .text:000000000000353E mov r15, rax