Challenge
The challenge presents as an aimbot launcher (aimbot.exe). We don’t have any other information.
Solution
Executable
DIE strings (c-strings filter) output shows some interesting data:
The program might be a legitimate aimbot mixed with some malicious things, since we notice http connection and dynamic module resolution. Also, there is a path to %ProgramFilesX86%/Sauerbraten
. Searching on google we find that Sauerbraten is actually a FPS game:
Screen of the game:
Cool! We might have some fun before playing this ctf:))
Also, there are 3 specific resources in this executable, but they are obfuscated:
I expect somewhere a chain of the Win32 Resource api (FindResource
, LockResource
, …) to read them.
Ok what we have is that this might be an aimbot for the game Sauerbraten. Let’s check into the code with IDA:
It initially creates a window and expects the user pushes the button:
The callback associated to the button event is sub_402AF0
and the logic actually starts at sub_402150
. Here’s shown the flow of what happens inside the sub:
The application initially checks if Sauerbraten game is installed by checking the installation path. So we either bypass/patch the code or install the game otherwise it won’t continue.
If all ok, creates inside %appdata%
a directory called BananaBot
that will be used to store all the crypted resources.
Green areas is where blobs are decrypted. The symmetric key used is yummyvitamincjoy
. Each resource is decrypted and a total of 3 files are created (violet):
miner.exe
config.json
aimbot.dll
Initially only the first two are decrypted.miner.exe
turns up to be XMRIG, a famous mining application, loaded with the configuration fileconfig.json
Miner endpoint is accessible at 127.0.0.1:57328
and executable uses these parameters when checking that miner started correctly (blue rectangle in the drawing), otherwise it exits.
If ok, it proceeds to decrypt the last resource aimbot.dll
, saves on disk, starts the game and injects it into the game exe.
The injection at 401E80
is a classical one:
func = LoadLibraryA("aimbot.dll")
CreateRemoteThread(startAddress=func)
Finally, the application waits that the game finishes in order to
- clean up resources
- close XMRIG process
- delete
%appdata/BananaBot
directory and its contents
DLL
Ok, so miner.exe
is XMRIG, config.json
is just a json
file so we now proceed to start analyzing aimbot.dll
.
As usual, here’s the output of DIE strings:
NtQueryInformationProcess? DbgBreakPoint ? Seems like…
Evasion! In this case… anti-debug techniques :) But it’s only speculation for the moment.
Let’s proceed into the code. Here’s the flow of the first part of the code:
When DLL is loaded, 3 threads are launched:
- Thread 1 (@
0x62F41C30
): this thread uses math functions and Win32 APIs likeGetAsyncKeyState
. It’s the actual aimbot, so we’ll skip the analysis for this one since we consider this not malicious - Thread 2 (@
0x62F43070
): the actual malicious thread. Calls anti-debugging functions (@sub_62F32300
). - Thread 3 (@
0x62F42340
): continuously loop-checks there’s no debugger attached, calls the same anti-debugging functions (sub_62F32300
) We’ll dive into it later.
Doing a static analysis entirely is very difficult, so I’ll just use a mixed approach when I can. Sometimes patching/bypass all the debugging protections is hard or just not enough for DLL to continue, so I’ll just attach with IDA to the sauerbraten process when aimbot.dll
is loaded and give the DLL the environment it expects.
To do this with IDA, just go to debugging options and enable to stop when DLL is loaded. In this way, you open two IDA windows:
- The first with
aimbot.exe
, just stop onCreateRemoteThread
of the sub that injects the DLL - In the second window attach to sauerbraten exiting process, set the break on DLL option, click on continue and wait
- Now from the first window let the debugger to continue and start the thread, you will see a notitification pop-up on the other IDA window
- From here, go to the Modules section of IDA, double click on
aimbot.dll
, double click onDllMain
and set a breakpoint on DllMain. - If steps above are executed correctly, press “continue” and you will end up on the
DllMain
breakpoint
- From here, go to the Modules section of IDA, double click on
Anti-debug checks
Here are shown all the anti-dbg mechanisms we have to correctly pass to make the DLL continue.
Time check
Inside the 2nd thread, we notice a chain of GetLocalTime, Sleep, GetLocalTime
. This block of code performs a timing evasion:
What is really done here is:
- 1st
GetLocalTime
: take the start time Sleep
: sleeping for 180 seconds- 2nd
GetLocalTime
: take the end time
from loc_62F430C4:
- Convert time into
FileTime
- Check if the delta is > 180 seconds
In this way it bypass any sleep patching mechanism, because if I skip the sleep or decrease the sleeping time the delta will be always < 180 seconds and malware exits. In order to bypass this anti-dbg check, we have to act on the RIP register and follow the right control flow as the correct amount of time passed.
IsDebuggerPresent (sub_62F42300)
From MSDN:
Determines whether the calling process is being debugged by a user-mode debugger.
Modify RIP and go on.
CheckRemoteDebuggerPresent (sub_62F42300)
From MSDN:
Determines whether the specified process is being debugged. The “remote” in CheckRemoteDebuggerPresent does not imply that the debugger necessarily resides on a different computer; instead, it indicates that the debugger resides in a separate and parallel process. Use the IsDebuggerPresent function to detect whether the calling process is running under the debugger.
Modify RIP and go on.
NtQueryInformationProcess (sub_62F42020)
This block of code calls dynamically the function NtQueryInformationProcess
:
The function takes as 2nd parameter a PROCESS_INFORMATION_CLASS
, that in this case equals to 0x1E
, referring to the process debug ports. This is another way to check if the process is being debugged.
Modify RIP and go on.
PEB (sub_62F42020)
There’s another check without calling Win32 API, just using the Process Environment Block (PEB):
In this case, the field BeingDebugged
is checked.
Modify RIP and go on.
Check debuggers inside active processes (sub_62F42020)
In the same sub the code proceeds and create an array with these pre-defined values:
v26[0] = 0x3755DCD46855AF94i64;
v26[1] = 0xF255062FB2C6B4E9ui64;
v26[4] = 0x3755DCD46855AF94i64;
v26[2] = 0x374755BE5E620917i64;
v26[5] = 0xF255062FB2C6B4E9ui64;
v26[6] = 0x374755BE5E620917i64;
v26[7] = 0x3A083D2B843E42CCi64;
v26[3] = 0x3A083D2B843E42CCi64;
Then, it starts to enumerate modules (EnumProcess
Win32 API is called after) and for each process, it gets the name (GetModuleBaseNameA
), ROR’s it (ROT 13) and compares the obtained value to each of the values of the array. If it matches, increase a value that will call IDX
.
That’s the hardest part. After process check, it gets its parent PID (sub_62F41F90
) - aimbot.exe
- and makes a constant read @ memory buffer 0x406220
of that process:
And then actually returns a value?! So this function is not just an anti-debug sub, but it actually returns a value that is used by the next sub?! Also, Jesus Christ only knows what the F*** the value is referred to:
Ok… so it returns the content at aimbot.dll
’s base address + 0x13E8 + v21. Who is v21?
from the decompiled code we have that and
… but also
OMG my brain is rotting right now… let’s understand this sh?t.
When the process are enumerated, each name is ROT13’s and checked against the pre-defined hashes. We already said about IDX
. Ok, so IDX
is the actual v22
value and it is increased when a match is found. You’re asking all these hashes… what they refer to?!
I didn’t full analyze it but you just have to stop where v22
is increased and check rsp+arg_48
that contains the process name scanned, so you’ll know who they are. The most important thing is that there is also explorer.exe
being in the list BUT it doesn’t mean that you have to stop it!
You’ll better understand just bypassing all the above checks on IDA and getting the retval from this sub. The value will be 0x6499F8AA
.
The key will be used to de-obfuscate the user agent and url strings that need to be sent to XMRIG (get version from miner configuration, drawing). The decryption uses a XOR, and the actual result of the XOR will be:
- user-agent:
aanamabow 5030
- url:
kttp9//117.0-0.1957318/2,sumnary
These values seems to be incorrect, and the http function will fail! I lost days to actually understand that the KEY WAS INCORRECT and WHY.
In this case, 0x6499F8AA
is given by
GetModuleHandleA
retval (aimbot.dll base address)13E8
(this constant value no one knows about)v21 = v24 + v17
v17 = 0x1339
v24 = content @ 0x406220
All these values have been checked running IDA in different part of the code.
0x1139 = 0x1337 + 0x2
- so the code detected two matching hashes and added to the constant 0x1337
v24
is just the value read at the constant address 0x406220
.
A sane person will just remove the 0x2
to the resulting 0x1139
thinking that it will bypass the check, obtaining the final key 0x6499F8A8
.
Perfect, this key is now used for the xoring code and… f*** ! It doesn’t work, again!
It took me hours to understand that the correct key is generated when there’s exact one match, so the value must be 0x1338
! And why that? I said explorer.exe
is in the hashes… it isn’t malicious! It’s included as an anti-debug check that malware employs to protect itself from automated analysis. If I start the process by double-clicking it, its parent will be explorer.exe
, so it will be present active processes.
That’s it!
I discovered that xdbg
performs better on dynamic analysis, especially on anti-debug samples using its included plugin ScyllaHide
. From github:
ScyllaHide is an advanced open-source x64/x86 user mode Anti-Anti-Debug library. It hooks various functions to hide debugging.
So it just does the job for us, without modifying the RIP each time, by giving the process false information about being debugged.
We can attach to the process, and then enable ScyllaHide
with all the functions that are called by the sample to be sure that it correctly bypass them:
NOTE:
Another trick that took me some time before understanding. If you step over the ReadProcessMemory
with ScyllaHide enabled and attached the WinAPI will fail (ERROR_PARTIAL_COPY
). This is probably due to some memory protection mechanism. Idk, but when I disabled ScyllaHide before stepping over the ReadProcessMemory
it worked correctly.
We also notice that the final constant is computed correctly inside x64dbg
, since it is in some way hidden as a process or not included in the hash black list:
we get that rax
is 0x6499F8A9
. And this is the key!
Now that we have the key, we can just replace this function with a mox rax, 0x6499F8A9
and don’t care anymore about anti-debug steps!
Getting the key
Now the sample correctly opens an Internet WinAPi socket to the endpoint using these parameters:
- user-agent:
bananabot 5000
- url:
http://127.0.0.1:57238/2/summary
The API InternetReadFile
returns inside RSI the actual XMRIG internal server config:
{',0Ah
debug093:000002217A1988E0 db ' "id": "89rtd0000000e21fa",',0Ah
debug093:000002217A1988E0 db ' "worker_id": "ASHO-KASKD-PLOI",',0Ah
debug093:000002217A1988E0 db ' "uptime": 479,',0Ah
debug093:000002217A1988E0 db ' "restricted": true,',0Ah
debug093:000002217A1988E0 db ' "resources": {',0Ah
debug093:000002217A1988E0 db ' "memory": {',0Ah
debug093:000002217A1988E0 db ' "free": 13097803776,',0Ah
debug093:000002217A1988E0 db ' "total": 17178877952,',0Ah
debug093:000002217A1988E0 db ' "resident_set_memory": 13959168',0Ah
debug093:000002217A1988E0 db ' },',0Ah
debug093:000002217A1988E0 db ' "load_average": [0.0, 0.0, 0.0],',0Ah
debug093:000002217A1988E0 db ' "hardware_concurrency": 4',0Ah
debug093:000002217A1988E0 db ' },',0Ah
debug093:000002217A1988E0 db ' "features": ["api", "asm", "http", "hwloc", "tls", "opencl", '
debug093:000002217A1988E0 db '"cuda"],',0Ah
debug093:000002217A1988E0 db ' "results": {',0Ah
debug093:000002217A1988E0 db ' "diff_current": 0,',0Ah
debug093:000002217A1988E0 db ' "shares_good": 0,',0Ah
debug093:000002217A1988E0 db ' "shares_total": 0,',0Ah
debug093:000002217A1988E0 db ' "avg_time": 0,',0Ah
debug093:000002217A1988E0 db ' "avg_time_ms": 0,',0Ah
debug093:000002217A1988E0 db ' "hashes_total": 0,',0Ah
debug093:000002217A1988E0 db ' "best": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]',0Ah
debug093:000002217A1988E0 db ' },',0Ah
debug093:000002217A1988E0 db ' "algo": null,',0Ah
debug093:000002217A1988E0 db ' "connection": {',0Ah
debug093:000002217A1988E0 db ' "pool": "",',0Ah
debug093:000002217A1988E0 db ' "ip": null,',0Ah
debug093:000002217A1988E0 db ' "uptime": 0,',0Ah
debug093:000002217A1988E0 db ' "uptime_ms": 0,',0Ah
debug093:000002217A1988E0 db ' "ping": 0,',0Ah
debug093:000002217A1988E0 db ' "failures": 0,',0Ah
debug093:000002217A1988E0 db ' "tls": null,',0Ah
debug093:000002217A1988E0 db ' "tls-fingerprint": null,',0Ah
debug093:000002217A1988E0 db ' "algo": null,',0Ah
debug093:000002217A1988E0 db ' "diff": 0,',0Ah
debug093:000002217A1988E0 db ' "accepted": 0,',0Ah
debug093:000002217A1988E0 db ' "rejected": 0,',0Ah
debug093:000002217A1988E0 db ' "avg_time": 0,',0Ah
debug093:000002217A1988E0 db ' "avg_time_ms": 0,',0Ah
debug093:000002217A1988E0 db ' "hashes_total": 0',0Ah
debug093:000002217A1988E0 db ' },',0Ah
debug093:000002217A1988E0 db ' "version": "6.20.0",',0Ah
debug093:000002217A1988E0 db ' "kind": "miner",',0Ah
debug093:000002217A1988E0 db ' "ua": "XMRig/6.20.0 (Windows NT 10.0; Win64; x64) libuv/1.44.'
debug093:000002217A1988E0 db '2 gcc/11.2.0",',0Ah
debug093:000002217A1988E0 db ' "cpu": {',0Ah
debug093:000002217A1988E0 db ' "brand": "9th Gen Intel(R) Core(TM) i7-11950H @ 2.60GHz"'
debug093:000002217A1988E0 db ',',0Ah
debug093:000002217A1988E0 db ' "family": 6,',0Ah
debug093:000002217A1988E0 db ' "model": 141,',0Ah
debug093:000002217A1988E0 db ' "stepping": 1,',0Ah
debug093:000002217A1988E0 db ' "proc_info": 526033,',0Ah
debug093:000002217A1988E0 db ' "aes": true,',0Ah
debug093:000002217A1988E0 db ' "avx2": true,',0Ah
debug093:000002217A1988E0 db ' "x64": true,',0Ah
debug093:000002217A1988E0 db ' "64_bit": true,',0Ah
debug093:000002217A1988E0 db ' "l2": 5242880,',0Ah
debug093:000002217A1988E0 db ' "l3": 50331648,',0Ah
debug093:000002217A1988E0 db ' "cores": 4,',0Ah
debug093:000002217A1988E0 db ' "threads": 4,',0Ah
debug093:000002217A1988E0 db ' "packages": 2,',0Ah
debug093:000002217A1988E0 db ' "nodes": 1,',0Ah
debug093:000002217A1988E0 db ' "backend": "hwloc/2.9.0",',0Ah
debug093:000002217A1988E0 db ' "msr": "intel",',0Ah
debug093:000002217A1988E0 db ' "assembly": "intel",',0Ah
debug093:000002217A1988E0 db ' "arch": "x86_64",',0Ah
debug093:000002217A1988E0 db ' "flags": ["aes", "vaes", "avx", "avx2", "avx512f", "bmi2"'
debug093:000002217A1988E0 db ', "osxsave", "pdpe1gb", "sse2", "ssse3", "sse4.1", "popcnt", "vm"'
debug093:000002217A1988E0 db ']',0Ah
debug093:000002217A1988E0 db ' },',0Ah
debug093:000002217A1988E0 db ' "donate_level": 1,',0Ah
debug093:000002217A1988E0 db ' "paused": false,',0Ah
debug093:000002217A1988E0 db ' "algorithms": ["cn/1", "cn/2", "cn/r", "cn/fast", "cn/half", '
debug093:000002217A1988E0 db '"cn/xao", "cn/rto", "cn/rwz", "cn/zls", "cn/double", "cn/ccx", "c'
debug093:000002217A1988E0 db 'n-lite/1", "cn-heavy/0", "cn-heavy/tube", "cn-heavy/xhv", "cn-pic'
debug093:000002217A1988E0 db 'o", "cn-pico/tlo", "cn/upx2", "rx/0", "rx/wow", "rx/arq", "rx/gra'
debug093:000002217A1988E0 db 'ft", "rx/sfx", "rx/keva", "argon2/chukwa", "argon2/chukwav2", "ar'
debug093:000002217A1988E0 db 'gon2/ninja", "ghostrider"],',0Ah
debug093:000002217A1988E0 db ' "hashrate": {',0Ah
debug093:000002217A1988E0 db ' "total": [null, null, null],',0Ah
debug093:000002217A1988E0 db ' "highest": null',0Ah
debug093:000002217A1988E0 db ' },',0Ah
debug093:000002217A1988E0 db ' "hugepages": [0, 0]',0Ah
debug093:000002217A1988E0 db '}
Then, it decrypts another string: "version": "
and gets the position in which is find the occurrence of this key inside the decrypted config using strstr
.
This result - "version": "6.20.0"
that is actually the version of our XMRIG miner - is then passed as a parameter to the next xor function (sub_62F413B0
). Inside this sub we notice that in r9 is loaded the base pointer of the obtained string + 16:
Not all characters of the string are used: knowing that there’s a loop next, this will be probably the upper-bound and so the limit of our string:
In fact r9 contains the upper bound and is checked with rdx so the code knows when to exit. In other words, knowing the base pointer and the last char at string+10h
we can get the key used for this xoring, that is: "version": "6.20
.
Then, it decrypt other data sources and finally the last string: the decryption of the blob was successful
and compares it to another buffer. We check the buffer and we notice that, like the first executable, it uses the same decryption method: in fact we find at the start of the buffer the string the decryption of the blob was successful
immediately followed by the decrypted bytes. The blob has a size of 0x4470
, so we have to dump from the first byte after the prologue string to the last byte that is at a distance of 0x4470
. We dump in total 17.520
bytes.
We verify with DIE that we dumped correctly:
If we open the blob with IDA we notice that it is really shellcode, no startup functions and so on.
Shellcode 1
Ok so now we continue analyzing the shellcode. We notice that the blob invokes a function passing a Steam config path (C:\\Program Files (x86)\\Steam\\config\\config.vdf
), quite suspicious. From the forums:
Config.vdf contains the following potentially sensitive information:
- The filename and location of the sentry files for all Steam accounts used on your client
- The account names for all accounts used on your client
- The decryption keys for the installers of every game you’ve ever downloaded (I can’t believe these are plaintext, even on a local file)
- And a number of other sundry configuration keys
Shellcode activity:
CreateFileA(C:\Program Files (x86)\Steam\config\config.vdf)
is used to search the config.vdf
file located inside Steam data directory. The config file is actually a JSON file containing many key-value pairs, the most important are the first and the SentryFile
key. Then many I/O functions are employed to read the file size, content and memory heap is allocated - GetProcessHeap, RtlAllocateHeap
- to copy the value associated with the JSON key SentryFile
and saves into the depot path to a file named steamssfn
.
Then, the shellcode decrypts a resource using the first bytes of the config.vdf
file (key: "InstallSt
) and then jumps with rax
to the next stage.
NOTE. All the shellcodes have a dynamic function resolution. There’s an array of functions at a specific offset and all the calls are done using rax
+ idx
where idx is the offset of the function chosen. Here’s some found on IDA:
debug198:000002217E911782 off_2217E911782 dq offset kernel32_CloseHandle
debug198:000002217E911782 ; DATA XREF: sub_2217E91097D+14↑o
debug198:000002217E911782 ; sub_2217E91097D+6F↑o ...
debug198:000002217E91178A dq offset kernel32_CreateFileA
debug198:000002217E911792 dq offset kernel32_ExitProcess
debug198:000002217E91179A dq offset kernel32_FindClose
debug198:000002217E9117A2 dq offset kernel32_FindFirstFileA
debug198:000002217E9117AA dq offset kernel32_FindNextFileA
debug198:000002217E9117B2 dq offset kernel32_GetFileSize
debug198:000002217E9117BA dq offset kernel32_GetProcessHeap
debug198:000002217E9117C2 dq offset kernel32_ReadFile
debug198:000002217E9117CA dq offset kernel32_CopyFileA
debug198:000002217E9117D2 dq offset kernel32_ExpandEnvironmentStringsA
debug198:000002217E9117DA off_2217E9117DA dq offset ntdll_RtlAllocateHeap
debug198:000002217E9117DA ; DATA XREF: sub_2217E91097D+1F↑o
debug198:000002217E9117DA ; sub_2217E911349+2D↑o ...
debug198:000002217E9117E2 dq offset ntdll_RtlFreeHeap
Shellcode 2
The structure is quite similar, I just got the same subs from IDA. Shellcode activity:
As you see, the behavior is almost identical. But now the shellcode is targeting Discord local database information, in particular .ldb
files inside a specific path. From Github:
LevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.
On reading those files:
This could lead to unauthorized access to user profiles, messages, and other critical information, potentially compromising the security and privacy of the affected user
So the infostealer is trying now to steal our Discord messages (?)
I don’t know… but we just give it what it wants (I already bloated my VM with Sauerbraten) and continue until we see the jmp
instruction which points to the next blob.
Shellcode 3
The next stage looks similar as well as the others above:
Shellcode activity:
We notice the prologue (another stage waits for us) but now targeting Sparrow, a cryptocurrency manager. I’m sorry infostealer, but I’m fu?#ing poor. Anyway, let’s create quickly a wallet and give the stealer what it wants to continue to the next stage.
Shellcode 4
This shellcode is different from the others, it doesn’t target anything, it just sends the data acquired during the previous steps.
Shellcode activity:
Sorry, the writeup stops here. I’ll post the rest of the solution as soon as I understand the next steps!
See you in the next research!