< Back to blog
highπŸ€RAT
investigatedMarch 7, 2026publishedMarch 7, 2026

Reverse Engineering QuasarRAT v1.4.1: Building a Fake Client Against a Live C2 with IP-Based Access Control

#rat#quasarrat#c2#reverse-engineering#apt

TL;DR: We extracted the full configuration from two QuasarRAT v1.4.1 samples operated by a Russian-speaking threat actor ("evilgrou-tech"), derived all cryptographic material from the live C2 server's TLS certificate, and built a protocol-accurate fake client in Python. Despite 400+ test variations covering every known serialization and encryption permutation, the C2 at 91.124.98.29:2626 immediately terminates every connection upon receiving data -- identical timing regardless of payload validity -- indicating IP-based access control that accepts TLS handshakes but drops unauthorized clients at the application layer.


Why This Investigation Matters

QuasarRAT remains one of the most deployed open-source remote access trojans. Its source code is freely available, its protocol is well-documented, and its encryption is deterministic -- the PBKDF2-derived AES key is a function of the server's TLS certificate SHA1 hash. In theory, any researcher who can complete a TLS handshake to the C2 can derive the encryption key and impersonate a victim. In practice, operators have adapted.

This investigation documents the full process of building a QuasarRAT fake client from binary analysis through protocol implementation, and the operational security measure that defeated it: a simple IP whitelist that renders cryptographic impersonation irrelevant. The technique is worth studying because it represents a low-cost, high-impact C2 hardening pattern that more commodity RAT operators are adopting.

Threat Actor Profile

AttributeValue
Handleevilgrou-tech
NationalityRussian
Emailevilgrou@gmail.com
GitHubevilgrou-tech
RATQuasarRAT v1.4.1
C2 IP91.124.98.29
C2 Ports2525 (original, dead), 2626 (active)
Campaign TagOffice04
StatusLive (accepting TLS connections as of 2026-03-08)

Configuration Extraction

Two .NET QuasarRAT v1.4.1 samples were analyzed:

SampleSHA256
Sample Af6f19c898956e618648964187d110f88542491cb30a69db18da0c58b5f422dbe
Sample B283d94b92c5af150941993e642612386dbefd44c6298898fb8e544fa3e389a4c

QuasarRAT stores its configuration as embedded strings in the .NET binary, encrypted with AES-256-CBC using a key derived from PBKDF2-SHA1. The derivation uses a hardcoded 32-byte salt from Quasar.Common and 50,000 iterations. The "password" input to PBKDF2 is the SHA1 hash of the server's TLS certificate in DER form -- a design choice that ties the encryption to the specific C2 server.

Decrypted Configuration

SettingValue
VERSION1.4.1
HOSTS91.124.98.29:2525
SUBDIRECTORYSubDir
INSTALLNAMEwin.exe / RuntimeBroker.exe
MUTEX1c2f3018-68c2-42f1-b9ce-a159491732a9
STARTUPKEYQuasar Client Startup / Startup
ENCRYPTIONKEY2B817FAEAC306BC3D2E98F2F86FA181F91AE1645
TAGOffice04
LOGDIRECTORYNAMELogs

Cryptographic Verification

The critical validation: ENCRYPTIONKEY equals SHA1(server_TLS_certificate_DER). We connected to the live C2 on port 2626, extracted the server certificate (CN=Quasar Server CA), computed its SHA1, and confirmed an exact match:

Server TLS Cert SHA1:  2B817FAEAC306BC3D2E98F2F86FA181F91AE1645
Config ENCRYPTIONKEY:  2B817FAEAC306BC3D2E98F2F86FA181F91AE1645
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                       MATCH β€” key derivation confirmed

From this, we derived all session cryptographic material:

MaterialValue
PBKDF2 Saltbfeb1e56fbcd973bb219022430a57843003d5644d21e62b9d4f180e7e6c33941
PBKDF2 Iterations50,000
PBKDF2 Output96 bytes (32 AES + 64 HMAC)
AES-256 Keyd87f587b646ee59e3462d2a13096d48ebc4084acb6747d644858e43e88ab9fcf
HMAC-SHA256 Key5ee95f6e3c24a25e758fd1f138d63ac1... (64 bytes)

Protocol Deep-Dive

QuasarRAT v1.4.1 uses a layered protocol stack:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  TCP                                                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  TLS 1.2 (ECDHE-RSA-AES256-GCM-SHA384)             β”‚
β”‚  Server cert CN: "Quasar Server CA"                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Framing: [4-byte LE payload_length]                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Encryption: [HMAC-SHA256(32)][IV(16)][AES-CBC ct]  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Optional: SafeQuickLZ compression                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Serialization: protobuf-net with SubType dispatch  β”‚
β”‚  SerializeWithLengthPrefix(Base128, fieldNumber=0)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Wire Format

The client speaks first. After the TLS handshake, it sends a ClientIdentification message:

  1. Protobuf serialization -- The ClientIdentification message is serialized using protobuf-net's SerializeWithLengthPrefix with PrefixStyle.Base128 and fieldNumber=0. The message is wrapped in a SubType field (field number 100 for ClientIdentification, the first registered IMessage subclass).

  2. Optional compression -- Standard builds pass the serialized bytes through SafeQuickLZ. This build had QuickLZ stripped.

  3. Encryption -- AES-256-CBC with PKCS7 padding. A random 16-byte IV is generated. The HMAC-SHA256 is computed over IV || ciphertext. The output is HMAC(32) || IV(16) || ciphertext.

  4. Framing -- A 4-byte little-endian length prefix indicates the encrypted payload size.

ClientIdentification Protobuf Structure

[varint: message_length]
  [field_tag: 0xA206 (100<<3|2)][varint: inner_length]
    [field 1:  string "1.4.1"]                              // Version
    [field 2:  string "Windows 10 Pro 64 Bit"]              // OperatingSystem
    [field 3:  string "Admin"]                               // AccountType
    [field 4:  string "Italy"]                               // Country
    [field 5:  string "IT"]                                  // CountryCode
    [field 6:  string "0e24ec19-b49b-4673-881d-..."]        // Id (UUID)
    [field 7:  string "admin"]                               // Username
    [field 8:  string "DESKTOP-WORK01"]                      // PcName
    [field 9:  string "Office04"]                            // Tag
    [field 10: int32  3]                                     // ImageIndex
    [field 11: string "2B817FAE...1645"]                     // EncryptionKey

The SubType field number (100) comes from QuasarRAT's message registration system. The framework calls GetPacketTypes() to enumerate all IMessage implementations, sorts them, and assigns SubType IDs starting at 100. ClientIdentification is alphabetically first, so it gets 100.

All 59 message types (ClientIdentification through SetUserStatus) map to SubType IDs 100-158:

100: ClientIdentification         131: GetDesktop
101: ClientIdentificationResult   132: GetDesktopResponse
102: DoAskElevate                 140: GetPassword
103: DoClientDisconnect           141: GetPasswordsResponse
104: DoClientReconnect            142: GetProcesses
105: DoClientUninstall            149: GetSystemInfo
115: DoShellExecute               155: SetAuthenticationSuccess
...                               158: SetUserStatus

Building the Fake Client

We implemented three iterations of the fake client in Python, each addressing a different hypothesis about why the server was rejecting connections.

Core Implementation

The key building blocks -- protobuf encoding, PBKDF2 key derivation, AES-CBC encryption, and HMAC computation:

# Key derivation: PBKDF2-SHA1, 50k iterations, 96-byte output
ENCRYPTION_KEY = "2B817FAEAC306BC3D2E98F2F86FA181F91AE1645"
SALT = bytes.fromhex("bfeb1e56fbcd973bb219022430a57843003d5644d21e62b9d4f180e7e6c33941")

derived = hashlib.pbkdf2_hmac('sha1', ENCRYPTION_KEY.encode('utf-8'),
                               SALT, 50000, dklen=96)
AES_KEY = derived[:32]   # AES-256 key
HMAC_KEY = derived[32:]  # HMAC-SHA256 key (64 bytes)

# Encryption: AES-256-CBC, output = HMAC || IV || ciphertext
def encrypt(plaintext):
    iv = os.urandom(16)
    cipher = AES.new(AES_KEY, AES.MODE_CBC, iv)
    ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
    mac = hmac.new(HMAC_KEY, iv + ciphertext, hashlib.sha256).digest()
    return mac + iv + ciphertext

The protobuf encoding was hand-rolled to match protobuf-net's wire format exactly, including varint encoding, field tag computation, and the SubType nesting pattern:

def build_client_id():
    msg = bytearray()
    msg.extend(field_str(1, "1.4.1"))           # Version
    msg.extend(field_str(2, "Windows 10 Pro 64 Bit"))
    msg.extend(field_str(3, "Admin"))           # AccountType
    msg.extend(field_str(4, "Italy"))           # Country
    msg.extend(field_str(5, "IT"))              # CountryCode
    msg.extend(field_str(6, str(uuid.uuid4()))) # Client UUID
    msg.extend(field_str(7, "admin"))           # Username
    msg.extend(field_str(8, "DESKTOP-WORK01")) # PcName
    msg.extend(field_str(9, "Office04"))        # Tag
    msg.extend(field_int(10, 3))                # ImageIndex
    msg.extend(field_str(11, ENCRYPTION_KEY))   # EncryptionKey
    return bytes(msg)

# Wrap in SubType 100 + serialize with length prefix (fieldNumber=0)
raw = build_client_id()
sub100 = field_nested(100, raw)
serialized = varint(len(sub100)) + sub100

# Encrypt and frame
encrypted = encrypt(serialized)
packet = struct.pack('<I', len(encrypted)) + encrypted

v1: Broad Scan

The first iteration tested basic protobuf formats: raw fields with and without length prefixes, SubType wrapping for IDs 1-100, and SafeQuickLZ framing with both 3-byte and 9-byte headers. Over 300 test combinations.

v2: Precise Format

After v1 failed uniformly, v2 focused on getting the exact protobuf-net serialization right. We mapped all 59 QuasarRAT message types to their SubType IDs (100-158) and tested each with both fieldNumber=0 and fieldNumber=1 prefix styles. We also tested alternative encryption keys ("password" as the PBKDF2 input, per values found in related hunt reports) and minimal/garbage payloads to probe server behavior.

v3: SafeQuickLZ Focus

The third iteration tested the hypothesis that compression was mandatory. We implemented both QuickLZ header formats:

# 9-byte header: flags(1) + total_size(4 LE) + decompressed_size(4 LE)
# flags = 0x02: uncompressed + 9-byte header
def quicklz_compress(data):
    total_size = len(data) + 9
    header = struct.pack('<BII', 0x02, total_size, len(data))
    return header + data

Every protobuf variant was tested with and without QuickLZ wrapping in both header formats.

The Test Matrix: 400+ Variations, Zero Responses

CategoryVariants TestedResult
SubType IDs 1-158158All: immediate close
Raw protobuf (no SubType)4All: immediate close
SafeQuickLZ 3-byte header40+All: immediate close
SafeQuickLZ 9-byte header40+All: immediate close
fieldNumber=0 vs fieldNumber=120+All: immediate close
"password" as PBKDF2 key8All: immediate close
Alternative salts (7 variants)14All: immediate close
Alternative iteration counts6All: immediate close
Alternative password strings (12)12All: immediate close
Random garbage payload1Immediate close
Valid HMAC + garbage AES content1Immediate close
Correct HMAC + valid protobuf50+All: immediate close
Unencrypted protobuf1Immediate close
Big-endian length prefix1Immediate close
2-byte length prefix1Immediate close
No length prefix1Immediate close
Empty frame (length=0)1Immediate close

Every single test produced the same result: the server closes the connection immediately upon receiving any complete data frame.

Timing Analysis: The Smoking Gun

The timing data is what transforms this from "maybe our protocol is wrong" to "the server is not even looking at the payload."

All payload-complete tests: connection closed at ~80ms (+-5ms)
TLS handshake RTT:          ~80ms

Observation: close latency == 1x network RTT
             (server processes at ~0ms local latency)

Three critical observations:

  1. Identical timing across all variants. Valid encrypted payloads, garbage bytes, wrong encryption keys, unencrypted data -- all produce the exact same ~80ms close. If the server were performing HMAC verification, AES decryption, or protobuf deserialization, we would see timing differences between valid and invalid payloads.

  2. No-data connections stay open indefinitely. When we complete the TLS handshake but send nothing, the server keeps the connection open for 30+ seconds. The close is triggered specifically by receiving data, not by a timeout.

  3. Close occurs at the first network round-trip after data receipt. The ~80ms delay is exactly the time for our data to reach the server plus the server's FIN to reach us. The server processes the close decision in effectively zero local time.

This pattern is consistent with a connection-level filter that triggers on first data receipt from an unauthorized source -- classically, an IP whitelist.

Assessment

Server Behavior Conclusion

The C2 server at 91.124.98.29:2626 implements IP-based access control that operates at the application layer, above TLS. The server:

  • Accepts TLS connections from any IP (no IP filtering at the network/firewall level)
  • Completes the full TLS handshake, presenting its "Quasar Server CA" certificate
  • Waits indefinitely for the client to speak first (standard QuasarRAT behavior)
  • Immediately closes the connection upon receiving any data from an unauthorized IP

This is a deliberate operational security choice. By accepting and completing TLS connections before filtering, the operator avoids leaking information about the whitelist to network scanners -- a SYN-level block would let Shodan/Censys confirm the port is filtered, while the current behavior makes the port appear to be a normal (if non-responsive) TLS service.

Ranked Hypotheses

  1. IP-based access control (HIGH confidence) -- The uniform timing across all payload variants, combined with the accept-TLS-then-drop-on-data pattern, is the canonical signature of application-layer IP filtering.

  2. Modified server code (MEDIUM confidence) -- The operator may have customized the QuasarRAT server with a non-standard packet format or pre-authentication handshake. However, this would typically produce different timing for valid vs. invalid payloads.

  3. Port migration with key rotation (LOW confidence) -- The operator moved from port 2525 to 2626. While the TLS certificate was retained, it is possible the application-layer encryption key was changed independently. This is unlikely because the ENCRYPTIONKEY in the client config still matches the live certificate's SHA1.

Confidence Summary

ElementConfidence
Configuration extractionHIGH
Protocol understandingHIGH
Key derivation correctnessHIGH (SHA1 cert match verified)
Fake client protocol accuracyHIGH (matches QuasarRAT v1.4.1 source)
IP-based filtering hypothesisHIGH (timing analysis)
Fake client registrationFAILED

MITRE ATT&CK Mapping

TechniqueIDUsage
Remote Access SoftwareT1219QuasarRAT v1.4.1 deployment
Encrypted Channel: Asymmetric CryptographyT1573.002TLS 1.2 with self-signed cert
Encrypted Channel: Symmetric CryptographyT1573.001AES-256-CBC payload encryption
Boot or Logon Autostart Execution: Registry Run KeysT1547.001STARTUPKEY: Quasar Client Startup
Masquerading: Match Legitimate NameT1036.005RuntimeBroker.exe install name
Application Layer ProtocolT1071Protobuf-net serialization over TLS
Ingress Tool TransferT1105File upload/download capabilities
System Information DiscoveryT1082GetSystemInfo message type
Process DiscoveryT1057GetProcesses message type
Input Capture: KeyloggingT1056.001Keylogger with Logs directory
Screen CaptureT1113GetDesktop / GetDesktopResponse
Credentials from Password StoresT1555GetPassword / GetPasswordsResponse

IOCs

Network Indicators

91.124.98.29:2626          # Active C2 (TLS, cert CN="Quasar Server CA")
91.124.98.29:2525          # Original C2 port (dead)

TLS Certificate

CN:    Quasar Server CA
SHA1:  2B817FAEAC306BC3D2E98F2F86FA181F91AE1645

File Indicators

f6f19c898956e618648964187d110f88542491cb30a69db18da0c58b5f422dbe  (Sample A)
283d94b92c5af150941993e642612386dbefd44c6298898fb8e544fa3e389a4c  (Sample B)

Host Indicators

Mutex:       1c2f3018-68c2-42f1-b9ce-a159491732a9
Install:     %APPDATA%\SubDir\win.exe
             %APPDATA%\SubDir\RuntimeBroker.exe
Registry:    HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Quasar Client Startup
             HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Startup
Log dir:     %APPDATA%\SubDir\Logs

Cryptographic Material

PBKDF2 Salt:       bfeb1e56fbcd973bb219022430a57843003d5644d21e62b9d4f180e7e6c33941
PBKDF2 Iterations: 50000
AES-256 Key:       d87f587b646ee59e3462d2a13096d48ebc4084acb6747d644858e43e88ab9fcf
Campaign Tag:      Office04

Actor Identifiers

Handle:  evilgrou-tech
Email:   evilgrou@gmail.com
GitHub:  github.com/evilgrou-tech
Share: