A Shitty FLARE-On 10 Writeup for Challenge 4 (Aimbot)

A Shitty FLARE-On 10 Writeup for Challenge 4 (Aimbot)

Basically me figuring out how to write writeups properly instead of blabbering technical jargon in my blog

·

12 min read

Cover Illustration by mocapoca_, the illustration is purchasable via https://twitter.com/mocapoca_/status/1728761352469324072 (Indonesian only)

This is a write up of FLARE-ON Challenge 4, which is unfortunately where I stopped doing the challenges due to work and also purely because it got harder after this. I guess for me, stopping at a cheating software for my first ever FLARE-On attempt is kinda… funny.

Initial File

After extracting the file, we obtained one executable file called aimbot.exe. This is an aimbot for the open-source FPS Sauerbraten. Upon execution it creates a window titled BananaAimBot that can launch the game with the purported aimbot. Looking into it in IDA, we can see that BananaAimBot is spawned with the callback function sub_402F0.

LRESULT __fastcall sub_402AF0(HWND hWndParent, UINT a2, WPARAM a3, HGDIOBJ a4)
{
    HWINSTANCE hInstance; // rax
    HWINSTANCE WindowLongPtrA; // rax

    if ( a2 == 2 )
    {
    DeleteObject(ho);
    PostQuitMessage(0);
    return 0i64;
    }
    if ( a2 == WM_COMMAND )
    {
        if ( ho == a4 && (a3 & CCM_COMMANDID_MASK_RESERVED) == 0 )
        {
            ShowWindow(hWndParent, 0);
            sub_402150();
            ExitProcess(0);
        }
        return 0i64;
    }
    if ( a2 != 1 )
        return DefWindowProcA(hWndParent, a2, a3, (LPARAM)a4);
    hInstance = (HINSTANCE)GetWindowLongPtrA(hWndParent, -6);
    ho = CreateWindowExit(
        0,
        L"BUTTON"
        L"Launch Sauerbraten with Aimbot!",
        05001001u,
        10,
        70,
        300,
        30,
        hWndParent,
        0i64,
        hInstance,
        0i64);

Everytime the button is clicked, the function will call sub_402150 that checks for the game and the version of the game via a hardcoded MD5 hash which is 180B22A08CF0C6D76C7AA5FF170BBF2D. This means that the aimbot only works for a specific version of Sauerbraten, specifically version 2020_12_21 which is available via Sourceforge.

sub 404650();
strcpy(v52, "PROGRAMFILE5(X86)%\\Sauerbraten\\bin64\\Waverbraten.exe");
ExpandenvironmentStringsA(v52, v56, 0x400u);
ExpandEnvironmentStringsA("PROGRAMFILE5(X86)%\\Sauerbraten", v57, Ox400u);
FileA = CreateFileA(v56, Ox80000000, 1u, 0i64, 3u, 0x80u, 0164);
v1 = FileA;
if ( FileA != (HANDLE)-li64 )
{
    if ( !GetFileSizeEx(FileA, &v49) || (LowPart = v49.Lowpart, v3 = malloc(v49.QuadPart), (v4 = v3) == 0i64)
    {
        CloseHandle(v1);
        return;
    }
    if ( !ReadFile(v1, v3, LowPart, &data, 0i4) )
    {
    CloseHandle(v1);
    free(v4);
    return;
} 
CloseHandle(v1);
sub_402C60(v54);
sub 402000(v54, (__int64)v4, data);
calcMD5(v54);
if ( v54[11] 0xD7C6F08CA0220B18ui64 88 v54[12] a= 0x2DBF0B17FFA57A6Ci64 )
{
    free(v4);
    memset(v58, 0, sizeof(v55));

Dropped Files

"After that, the program decrypts miner.exe, config.json, and aimbot.dll, then extracts the files into a folder at C:\Users\user\AppData\Roaming\BananaBot. The decryption uses standard AES-128/ECB with the key yummyvitamincjoy. The key is hardcoded in plaintext in the code.

decrypt_resource_401F50(void *buf, size_t Size) AES_key_derivation_401BA0(aes_ctx, "yummyvitamincjoy");

The program then executes miner.exe, which is a legit copy of XMRig used to mine monero using CPU resources. XMRig will send a HTTP GET request to http://127.0.0.1:57328/2/summary which the XMRig sample is configured to listen on via config.json.

{
    "http": {
        "enabled": true,
        "host": "127.0.0.1",
        "port": 57328,
        "access-token": null,
        "restricted": true
    },
    "autosave": true,
    "cpu": true,
    "opencl": false,
    "cuda": false,
    "pools": [
        {
            "url": "monerohash.com:9999",
            "user": "49jmq1dCvnAAGpeb6aCFyuaXNB8WMJ6fqLTG4twcSjwyNgHagoaQw5EbCw4mf832RPRpf2CH4srVhAxgtSb6A62P2VwJC47",
            "keepalive": true,
            "tls": true
        }

When the GET request succeeds, the program knows XMRig is active and executes the game that's injected with aimbot.dll by copying the DLL's file path into the memory space of the target process and then invoking LoadLibraryA using CreateRemoteThread within the context of the target process.

_BOOL8 __fastcall sub_401E80(HANDLE hProcess_Sauerbraten, char *aimbot_dll)
{
    SIZE_T v4; // rax
    void *v5; // rdi
    SIZE_T v6; // rax
    MODULE ModuleHandleA; // rax
    MODULE (__stdcall *LoadLibraryA)(LPCSTR); // rax

v4 = strlen(aimbot_dll);
v5 = VirtualAllocEx(hProcess_Sauerbraten,; 0i64, v4, 0x3000u, 0x40u);
if ( v5
    && (v6 & strlen(aimbot_dll), WriteProcessMemory (hProcess_Sauerbraten, v5, aimbot_dll, v6, 0i64))
    && (ModuleHandleA - GetModuleHandleA("kernel32.dll"),
        (LoadLibraryA = (HMODULE (__stdcall *)(LPCSTR))GetProcAddress(ModuleHandleA, 'LoadLibraryA')) != 0i64)) 
{
    return CreateRemoteThread(hProcess_Sauerbraten, 0i64, 0i64, (LPTHREAD_START_ROUTINE)LoadLibraryA, v5, 0, 0i64) != 0164;
}
else
{
    return 0164; 
}

Aimbot.dll and It's Anti-Debug Functions

aimbot.dll starts 3 threads

  • A thread for the aimbot's cheating functions through calling GetAsyncKeyState and uses common math libraries like atan2 and sqrt, this thread is irrelevant for the challenge but the aimbot works in-game

  • A thread for anti-debugging functions, which contains

    • Windows API anti-debugging checks through IsDebuggerPresent, CheckRemoteDebuggerPresent, and DbgBreakPoint

    • Checks for debuggers present such as IDA and x64dbg through hardcoded checksum validations

    • Checks that the DLL is running inside of the game via constant reading main module of the process's memory and the memory of the parent process

  • A thread for the cheating software's infostealing functions

Due to the anti-debug functions, we will need to decrypt the payload without static analysis. Analyzing the DLL, we can pay attention to address 0x62FE4020 that are decoded in function 62f439b0() using the hardcoded XOR key A9F89964 which gets us :

  • http://127.0.0.1:57328/2/summary<- the XMRig location

  • bananabot 5000 <- name of the user agent

  • “version”: “ <- XMRig version check

  • the decryption of this blob was successful

My initial plan involved a brute force attack on the last four bytes of the AES-256 key, thinking that these bytes were selected from printable ASCII characters because of the strstr call should align with the phrase "version": ", which has a length of 12 characters. Since an AES-256 key is 16 bytes long, we can focus on deciphering the remaining four bytes.

I tried to retrieve the XOR keystream from a known plaintext segment and apply it to our chosen ciphertext. However, this didn't work due to the nature of ECB encryption using the same keystream for each block leading to different XOR keystream bytes for varying inputs.

Then I analyzed the ECB encrypted data more closely and realized that by XOR'ing the first 16 bytes of the ciphertext with the first 16 bytes of a known plaintext segment we could deduce the keystream bytes. Applying this keystream to the second 16-byte block of the ciphertext allowed us to crack it.

from itertools import product
from string import digits
from Crypto.Cipher import AES
from Crypto.Hash import SHA256

key_prefix = '"version": "'
known_plaintext = b"the decryption of this blob was successful"
alphabet = digits + "."

# Read the first 16 bytes of ciphertext
with open("./program/aimbot_dll_payload_0xa6340_size_0x4470.bin", "rb") as f:
    ciphertext = f.read(16)

print("Bruteforcing last 4 chars")
for key_suffix in product(alphabet, repeat=4):
    key = bytes(key_prefix + "".join(key_suffix), "UTF-8")
    cipher = AES.new(key, AES.MODE_ECB)
    plaintext = cipher.decrypt(ciphertext)
    if plaintext == known_plaintext[:16]:
        print(f"Success, key = {key}")
        break

print("Decrypting payload")
with open("./program/aimbot_dll_payload_0xa6340_size_0x4470.bin", "rb") as f:
    ciphertext = f.read()

cipher = AES.new(key, AES.MODE_ECB)
plaintext = cipher.decrypt(ciphertext)
h = SHA256.new()
h.update(plaintext)

print(f"SHA256 = {h.hexdigest()}")
with open("./program/aimbot_dll_payload_0xa6340_size_0x4470_decrypted.bin", "wb") as f:
    f.write(plaintext)

With this, we can continue decrypting the shellcode, which is comprised of a series of chained blobs that execute in a sequence. The blobs execute in order from the first blob, extract information from a specific program and then forwards it to C:\depot and uses an RC4 key generated from a specific program's config files to decrypt the next blob of the malware.

SteamGuard Infostealer

The first stage seems to be targeting Steam's config.vdf which contains Steam's SentryFile SSFN ID which stores your SteamGuard authentication.

seg000:00000000000008AE 22 53 65 6E 74 72 79 46…aSentryfile     db '"SentryFile"',0     ; DATA XREF: sub_5FC:loc_693↑o
seg000:00000000000008BB 43 3A 5C 50 72 6F 67 72 aCProgramFilesX db 'C:\Program Files (x86)\Steam\config\config.vdf',0
seg000:00000000000008BB 61 6D 20 46 69 6C 65 73…                                        ; DATA XREF: sub_5FC+11↑o
seg000:00000000000008EA 43 3A 5C 64 65 70 6F 74 aCDepotSteamSsf db 'C:\depot\steam_ssfn',0
seg000:00000000000008EA 5C 73 74 65 61 6D 5F 73…                                        ; DATA XREF: sub_5FC+12D↑o
seg000:00000000000008FE 74 68 65 20 64 65 63 72 aTheDecryptionO db 'the decryption of this blob was successful',0

Within this file you can find :

"InstallConfigStore"
{
    "Software"
    {
        "Valve"
        {
            "Steam"
            {
                "AutoUpdateWindowEnabled"        "0"
                "ShaderCacheManager"
                {
                    "HasCurrentBucket"        "1"
                    "CurrentBucketGPU"        ""
                    "CurrentBucketDriver"        ""
                }
                "SentryFile"        "C:\\Program Files (x86)\\steam\\ssfnXXXXXXXXXXXXXXXXXXX"
                "Accounts"
                {
                    "xxxxxxx"
                    {
                        "SteamID"        "xxxxxxxxxxxxx"
                    }
                }

As the key length is 16 bytes the KSA calling the first 42 bytes of the encrypted next stage of the malware is again "the decryption of this blob was successful". The RC4 key should be the first 16 characters of the config.vdf file which is "InstallConfigSt.

Discord Stealer

The next stage of the malware looks a bit less straightforward at first sight. But is simpler (and funnier) than the one before.

- 0000000000000E71 FB 97 FD 0F                                    dd 0FFD97FBh            ; kernel32.dll!CloseHandle
- seg000:0000000000000E75 A5 17 00 7C                             dd 7C0017A5h            ; kernel32.dll!CreateFileA
- seg000:0000000000000E79 7E D8 E2 73                             dd 73E2D87Eh            ; kernel32.dll!ExitProcess
- seg000:0000000000000E7D 78 59 54 23                             dd 23545978h            ; kernel32.dll!FindClose
- seg000:0000000000000E81 65 C0 D6 63                             dd 63D6C065h            ; kernel32.dll!FindFirstFileA
- seg000:0000000000000E85 97 AC E1 A5                             dd 0A5E1AC97h           ; kernel32.dll!FindNextFileA
- seg000:0000000000000E89 AD 9B 7D DF                             dd 0DF7D9BADh           ; kernel32.dll!GetFileSize
- seg000:0000000000000E8D AE EC 0E A8                             dd 0A80EECAEh           ; kernel32.dll!GetProcessHeap
- seg000:0000000000000E91 16 65 FA 10                             dd 10FA6516h            ; kernel32.dll!ReadFile
- seg000:0000000000000E95 5E 89 EC 99                             dd 99EC895Eh            ; kernel32.dll!CopyFileA
- seg000:0000000000000E99 D8 85 B5 EE                             dd 0EEB585D8h           ; kernel32.dll!ExpandEnvironmentStringsA
- seg000:0000000000000E9D F2 DB 74 AD                             dd 0AD74DBF2h
- seg000:0000000000000EA1 26 25 19 3E                             dd 3E192526h            ; ntoskrnl.exe!RtlAllocateHeap
- seg000:0000000000000EA5 B8 12 DA 00                             dd 0DA12B8h             ; ntoskrnl.exe!RtlFreeHeap

The malware searches for .ldb files (Microsoft Access Lock Information Files) in C:\Users\three\AppData\Roaming\Discord\Local Storage\leveldb and searches for dQw4w9WgXcQ (the URL for Rick Astley - Never Gonna Give You Up music video) in the contents of the each .ldb file. It then reads the first 16 bytes of file C:\Users\three\AppData\Roaming\Discord\Network\Origin Bound Certs to get the RC4 key to decrypt the next blob, which the key is SQLite format 3\0.

Sparrow Wallet Stealer

The third stage is related to the Cryptowallet Sparrow, which I'm honestly less familiar with.

eg000:0000000000000EF4 FB 97 FD 0F                             dd 0FFD97FBh            ; kernel32.dll!CloseHandle
seg000:0000000000000EF8 A5 17 00 7C                             dd 7C0017A5h            ; kernel32.dll!CreateFileA
seg000:0000000000000EFC 7E D8 E2 73                             dd 73E2D87Eh            ; kernel32.dll!ExitProcess
seg000:0000000000000F00 78 59 54 23                             dd 23545978h            ; kernel32.dll!FindClose
seg000:0000000000000F04 65 C0 D6 63                             dd 63D6C065h            ; kernel32.dll!FindFirstFileA
seg000:0000000000000F08 97 AC E1 A5                             dd 0A5E1AC97h           ; kernel32.dll!FindNextFileA
seg000:0000000000000F0C AD 9B 7D DF                             dd 0DF7D9BADh           ; kernel32.dll!GetFileSize
seg000:0000000000000F10 AE EC 0E A8                             dd 0A80EECAEh           ; kernel32.dll!GetProcessHeap
seg000:0000000000000F14 16 65 FA 10                             dd 10FA6516h            ; kernel32.dll!ReadFile
seg000:0000000000000F18 5E 89 EC 99                             dd 99EC895Eh            ; kernel32.dll!CopyFileA
seg000:0000000000000F1C D8 85 B5 EE                             dd 0EEB585D8h           ; kernel32.dll!ExpandEnvironmentStringsA
seg000:0000000000000F20 F2 DB 74 AD                             dd 0AD74DBF2h
seg000:0000000000000F24 26 25 19 3E                             dd 3E192526h            ; ntoskrnl.exe!RtlAllocateHeap
seg000:0000000000000F28 B8 12 DA 00                             dd 0DA12B8h             ; ntoskrnl.exe!RtlFreeHeap

The shellcode searches for database files and their contents (some using a specific pattern) in C:\Users\three\AppData\Roaming\Sparrow\wallets and C:\Users\three\AppData\Roaming\Sparrow\config copies them to the depot folder. The config folder contains JSON files and recent used wallets in recentWalletFiles, which is the 17 char RC4 key for the next blob.

C2 Forwarder

After all of the files are collected to C:\depot, the malware moves it to C:\depot\output and forwards it to https://bighackies.flare-on.com/stolen.

seg000:0000000000000CEF 17 CA 2B 6E             dword_CEF       dd 6E2BCA17h            ; DATA XREF: sub_0+3A↑o
seg000:0000000000000CF3 FB 97 FD 0F                             dd 0FFD97FBh            ; kernel32.dll!CloseHandle
seg000:0000000000000CF7 A5 17 00 7C                             dd 7C0017A5h            ; kernel32.dll!CreateFileA
seg000:0000000000000CFB 7E D8 E2 73                             dd 73E2D87Eh            ; kernel32.dll!ExitProcess
seg000:0000000000000CFF 78 59 54 23                             dd 23545978h            ; kernel32.dll!FindClose
seg000:0000000000000D03 65 C0 D6 63                             dd 63D6C065h            ; kernel32.dll!FindFirstFileA
seg000:0000000000000D07 97 AC E1 A5                             dd 0A5E1AC97h           ; kernel32.dll!FindNextFileA
seg000:0000000000000D0B AD 9B 7D DF                             dd 0DF7D9BADh           ; kernel32.dll!GetFileSize
seg000:0000000000000D0F AE EC 0E A8                             dd 0A80EECAEh           ; kernel32.dll!GetProcessHeap
seg000:0000000000000D13 16 65 FA 10                             dd 10FA6516h            ; kernel32.dll!ReadFile
seg000:0000000000000D17 1F 79 0A E8                             dd 0E80A791Fh           ; kernel32.dll!WriteFile
seg000:0000000000000D1B F2 DB 74 AD                             dd 0AD74DBF2h
seg000:0000000000000D1F 26 25 19 3E                             dd 3E192526h            ; ntoskrnl.exe!RtlAllocateHeap
seg000:0000000000000D23 B8 12 DA 00                             dd 0DA12B8h             ; ntoskrnl.exe!RtlFreeHeap
seg000:0000000000000D27 A4 A2 9F ED                             dd 0ED9FA2A4h
seg000:0000000000000D2B 9F 76 DE F7                             dd 0F7DE769Fh           ; wininet.dll!HttpOpenRequestA
seg000:0000000000000D2F FA 45 2F FB                             dd 0FB2F45FAh           ; wininet.dll!HttpQueryInfoA
seg000:0000000000000D33 9D BE E6 2D                             dd 2DE6BE9Dh            ; wininet.dll!HttpSendRequestA
seg000:0000000000000D37 C7 69 9B FA                             dd 0FA9B69C7h           ; wininet.dll!InternetCloseHandle
seg000:0000000000000D3B 0E E8 4B 1E                             dd 1E4BE80Eh            ; wininet.dll!InternetConnectA
seg000:0000000000000D3F 29 44 E8 57                             dd 57E84429h            ; wininet.dll!InternetOpenA
seg000:0000000000000D43 8B 4B E3 5F                             dd 5FE34B8Bh            ; wininet.dll!InternetReadFile

The payload initiates contact with its C2 server, with the response from the C2 server containing CRC32 checksums for different segments of the exfiltrated data. The first four bytes of the C2 response are expected to be the CRC32 checksum of the entire exfiltrated data (0...n). Subsequent bytes follow a similar pattern, with each set representing the CRC32 checksum of a progressively smaller data segment.

InternetReadFile is called with dwNumberOfBytesToRead set to 7. However, the buffer (lpBuffer) is expected to contain 16 bytes of data. This discrepancy suggests that the server's response likely includes four CRC32 values, corresponding to different segments of the exfiltrated data.

The next blob is XOR encrypted with a 4 byte key, where the key is calculated by multiplying 0x1234567 with the integer value in lpBuffer. lpBuffer obtains its value from the HttpQueryInfoA function, specifically from the HTTP_QUERY_CONTENT_LENGTH flag, which returns the size of the resource as a 32-bit number. The XOR key is then used to decrypt the next stage of the payload blob.

Game Check

The final blob is related to the game, which yes, requires you to play the game.

seg000:0000000000000CF8 FB 97 FD 0F                             dd 0FFD97FBh            ; kernel32.dll!CloseHandle
seg000:0000000000000CFC A5 17 00 7C                             dd 7C0017A5h            ; kernel32.dll!CreateFileA
seg000:0000000000000D00 7E D8 E2 73                             dd 73E2D87Eh            ; kernel32.dll!ExitProcess
seg000:0000000000000D04 04 49 32 D3                             dd 0D3324904h           ; kernel32.dll!GetModuleHandleA
seg000:0000000000000D08 AE EC 0E A8                             dd 0A80EECAEh           ; kernel32.dll!GetProcessHeap
seg000:0000000000000D0C 16 65 FA 10                             dd 10FA6516h            ; kernel32.dll!ReadFile
seg000:0000000000000D10 AC 08 DA 76                             dd 76DA08ACh            ; kernel32.dll!SetFilePointer
seg000:0000000000000D14 D8 85 B5 EE                             dd 0EEB585D8h           ; kernel32.dll!ExpandEnvironmentStringsA
seg000:0000000000000D18 F2 DB 74 AD                             dd 0AD74DBF2h
seg000:0000000000000D1C 26 25 19 3E                             dd 3E192526h            ; ntoskrnl.exe!RtlAllocateHeap

The final shellcode reads data from C:\Program Files(x86)\Sauerbraten\packages\base\spcr2.cfg which is the map spcr2. The shellcode reads 4 bytes from offset 0 0xa45, which is maps and 8 bytes from offset 81 0xa73.

v0 = (*((__int64 (__fastcall **)(_QWORD))&dword_CAC + 3))(0164);
if ( (unsigned int)mem_cmp(v0 + 0x2458C0 (__int64)"spcr", 4ui64) )
    return 0i64;
if ( !*(_BYTE *)(v0 + 0x2458C4) )
    return 0i64;
sub_58A((__int64)v14, "%%PROGRAMFILES(X86)\\Sauerbraten\\packages\\base\\%s.cfg");
(*((void (__fastcall **)(char *, char *, __int64 &dword_CAC + 7))(v14, v15, 1024i64);
v2 = sub_6C6((__int64)v15, Ox80000000);
v3 = v2;

The function extracts the map data and sees if the player has achieved 1337 kills with exactly 1337 bullets in less than 5 minutes, which is very complicated to do in-game (even with an aimbot). A solution is to pull the key bytes statically and calculate the remaining key bytes with the CRC32 check to verify the correct flag value.

from binascii import crc32
from string import printable
from itertools import product

# Read the initial part of the flag from the file
with open("../files/spcr2.cfg", "rb") as f:
    dd_file_offset_4 = int.from_bytes(f.read(4), "little")
    f.seek(81)
    initial_flag_part = f.read(8)

# Set up the base flag
flag_base = 25 * b"\x20" + b"flare-on.com"
flag = bytearray(flag_base)
flag[0:8] = initial_flag_part
flag[8] = ord("_")

# Apply XOR operations to calculate parts of the flag
def xor(value, mask):
    return value ^ mask

flag[9:13] = [(dd_file_offset_4 ^ mask) & 0xFF for mask in [0xC, 0x120C, 0x3120C, 0x4203120C]]
flag[13:17] = xor(dd_file_offset_4, 0x1715151E).to_bytes(4, "little")
flag[17:21] = xor(dd_file_offset_4, 0x15040232).to_bytes(4, "little")
flag[24] = ord("@")

# Brute-force the remaining characters
for i in product(printable, repeat=3):
    flag[21:24] = [ord(c) for c in i]
    if crc32(flag[:25]) == 0xA5561586:
        print(f"Flag: {flag.decode('UTF-8')}")
        break
else:
    print("Failed bruting the CRC32 check!")
> python solve.py
Flag: computer_ass1sted_ctfing@flare-on.com

Why did I do this? I don't know. Will I do it again? Probably not. But alas, the reason why I don't usually do CTFs.