Reverse Engineering QuasarRAT v1.4.1: Building a Fake Client Against a Live C2 with IP-Based Access Control
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
| Attribute | Value |
|---|---|
| Handle | evilgrou-tech |
| Nationality | Russian |
evilgrou@gmail.com | |
| GitHub | evilgrou-tech |
| RAT | QuasarRAT v1.4.1 |
| C2 IP | 91.124.98.29 |
| C2 Ports | 2525 (original, dead), 2626 (active) |
| Campaign Tag | Office04 |
| Status | Live (accepting TLS connections as of 2026-03-08) |
Configuration Extraction
Two .NET QuasarRAT v1.4.1 samples were analyzed:
| Sample | SHA256 |
|---|---|
| Sample A | f6f19c898956e618648964187d110f88542491cb30a69db18da0c58b5f422dbe |
| Sample B | 283d94b92c5af150941993e642612386dbefd44c6298898fb8e544fa3e389a4c |
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
| Setting | Value |
|---|---|
| VERSION | 1.4.1 |
| HOSTS | 91.124.98.29:2525 |
| SUBDIRECTORY | SubDir |
| INSTALLNAME | win.exe / RuntimeBroker.exe |
| MUTEX | 1c2f3018-68c2-42f1-b9ce-a159491732a9 |
| STARTUPKEY | Quasar Client Startup / Startup |
| ENCRYPTIONKEY | 2B817FAEAC306BC3D2E98F2F86FA181F91AE1645 |
| TAG | Office04 |
| LOGDIRECTORYNAME | Logs |
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:
| Material | Value |
|---|---|
| PBKDF2 Salt | bfeb1e56fbcd973bb219022430a57843003d5644d21e62b9d4f180e7e6c33941 |
| PBKDF2 Iterations | 50,000 |
| PBKDF2 Output | 96 bytes (32 AES + 64 HMAC) |
| AES-256 Key | d87f587b646ee59e3462d2a13096d48ebc4084acb6747d644858e43e88ab9fcf |
| HMAC-SHA256 Key | 5ee95f6e3c24a25e758fd1f138d63ac1... (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:
-
Protobuf serialization -- The
ClientIdentificationmessage is serialized using protobuf-net'sSerializeWithLengthPrefixwithPrefixStyle.Base128andfieldNumber=0. The message is wrapped in a SubType field (field number 100 forClientIdentification, the first registeredIMessagesubclass). -
Optional compression -- Standard builds pass the serialized bytes through SafeQuickLZ. This build had QuickLZ stripped.
-
Encryption -- AES-256-CBC with PKCS7 padding. A random 16-byte IV is generated. The HMAC-SHA256 is computed over
IV || ciphertext. The output isHMAC(32) || IV(16) || ciphertext. -
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
| Category | Variants Tested | Result |
|---|---|---|
| SubType IDs 1-158 | 158 | All: immediate close |
| Raw protobuf (no SubType) | 4 | All: immediate close |
| SafeQuickLZ 3-byte header | 40+ | All: immediate close |
| SafeQuickLZ 9-byte header | 40+ | All: immediate close |
| fieldNumber=0 vs fieldNumber=1 | 20+ | All: immediate close |
"password" as PBKDF2 key | 8 | All: immediate close |
| Alternative salts (7 variants) | 14 | All: immediate close |
| Alternative iteration counts | 6 | All: immediate close |
| Alternative password strings (12) | 12 | All: immediate close |
| Random garbage payload | 1 | Immediate close |
| Valid HMAC + garbage AES content | 1 | Immediate close |
| Correct HMAC + valid protobuf | 50+ | All: immediate close |
| Unencrypted protobuf | 1 | Immediate close |
| Big-endian length prefix | 1 | Immediate close |
| 2-byte length prefix | 1 | Immediate close |
| No length prefix | 1 | Immediate close |
| Empty frame (length=0) | 1 | Immediate 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:
-
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.
-
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.
-
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
-
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.
-
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.
-
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
| Element | Confidence |
|---|---|
| Configuration extraction | HIGH |
| Protocol understanding | HIGH |
| Key derivation correctness | HIGH (SHA1 cert match verified) |
| Fake client protocol accuracy | HIGH (matches QuasarRAT v1.4.1 source) |
| IP-based filtering hypothesis | HIGH (timing analysis) |
| Fake client registration | FAILED |
MITRE ATT&CK Mapping
| Technique | ID | Usage |
|---|---|---|
| Remote Access Software | T1219 | QuasarRAT v1.4.1 deployment |
| Encrypted Channel: Asymmetric Cryptography | T1573.002 | TLS 1.2 with self-signed cert |
| Encrypted Channel: Symmetric Cryptography | T1573.001 | AES-256-CBC payload encryption |
| Boot or Logon Autostart Execution: Registry Run Keys | T1547.001 | STARTUPKEY: Quasar Client Startup |
| Masquerading: Match Legitimate Name | T1036.005 | RuntimeBroker.exe install name |
| Application Layer Protocol | T1071 | Protobuf-net serialization over TLS |
| Ingress Tool Transfer | T1105 | File upload/download capabilities |
| System Information Discovery | T1082 | GetSystemInfo message type |
| Process Discovery | T1057 | GetProcesses message type |
| Input Capture: Keylogging | T1056.001 | Keylogger with Logs directory |
| Screen Capture | T1113 | GetDesktop / GetDesktopResponse |
| Credentials from Password Stores | T1555 | GetPassword / 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