Amatera 4.1.5-alpha: from protected strings to configuration-driven collection
Contents
Amatera 4.1.5-alpha is a 32-bit Windows infostealer whose server-supplied configuration selects distinct collection and execution paths. A negotiated endpoint map leads to an encoded JSON configuration. Different consumers interpret its arrays differently, some inventory and cache reads sit outside those array guards, and the download-and-execute and screenshot branches run after the core collector has already submitted its final identity record.
This analysis follows those relationships through the original executable: the string transform and API-name hashes that hide its dependencies; the short mixed-mode startup path; the application cryptography inside TLS; and the specific file, browser, Steam, screenshot and staged-execution paths that were exercised. The distinction between collecting a database and decrypting its contents is particularly important. The Chromium results are raw SQLite files. The Steam result demonstrates same-user DPAPI decryption of a fabricated cache value. Neither result establishes access to a real account.
Analyzed sample
The analyzed executable is the x86 Amatera build carrying GETWELLV2 and 4.1.5-alpha. Its SHA-256 is c2256961d9b7704e2bb85ae9512b4c1ee75fea95532434230c93cd5e38ef8aa7.
Reconstructed execution and protocol flow, supported by separate observed workflows. Core f precedes ld, then g; the dashed fallback has no observed successful HTTP exchange.
Recovering the program beneath its protection
This Amatera payload is a 32-bit Windows GUI executable, but a conventional import-table walk reveals very little. Its import, export and TLS directories are empty, and its PE timestamp is zero. None of those properties alone identifies a packer or proves that the file is damaged. Here, the useful trail is in the code: an entry thunk at 0x4A5898 reaches the initialization orchestrator at 0x47AB20, while small helpers recover strings, locate loaded modules and resolve exported functions at runtime.
The obfuscation is not one mechanism. It combines protected strings, several families of API-name hashes, flattened control flow, arithmetic identities, dead instruction islands and a short excursion into 64-bit execution. Treating everything unusual as an anti-analysis check would obscure the distinction that matters most: some apparent decisions are algebraically fixed, while other startup tests can genuinely reject the environment.
A per-byte key, not a single XOR constant
The string decoder at 0x49E50D receives a destination, a ciphertext pointer, a byte count and a selector. It derives a new 32-bit key for every position, transforms that ciphertext byte, then writes an additional NUL after the requested output. The selector is not a secret supplied by a server: the callers pass it alongside the source and length.
The seed helper at 0x49E68B uses the multipliers associated with the SplitMix64 finalizer. That resemblance describes the arithmetic, not a claim that the sample implements the full generator. Define F as two XOR-shift/multiply steps, with each multiplication reduced modulo 2^64:
F(x):
x = (x XOR (x >> 30)) * BF58476D1CE4E5B9
x = (x XOR (x >> 27)) * 94D049BB133111EB
return x
t = F(736F6D6570736575 XOR selector)
seed = t XOR (t >> 31)
x = (seed XOR 263105264399D38F)
+ selector * C6A4A7935BD1E995
+ index * 517CC1B727220A95
x = F(F(F(F(x))))
key32 = ((x XOR (x >> 31)) AND FFFF07FF) + 100
Constants are hexadecimal. The selector and index are unsigned 32-bit inputs; seed and mixing arithmetic are 64-bit; the final key is reduced to 32 bits. In particular, the final shift by 31 is applied after the four mixing rounds, not inside every round. That distinction is easy to lose in a reconstruction.
Let k0 through k3 be the key's bytes, least significant first. The normal path through 0x408490 is much smaller than its disassembly first suggests:
plain = ROR8(((cipher XOR k3) - k2) mod 256, k1) XOR k0
The byte rotate uses its effective count modulo eight. For example, the first byte of the marker at 0x4A9E30 is 8E, and selector 7 at index zero produces key 1FB601AA. Thus 8E XOR 1F = 91; subtracting B6 modulo 256 gives DB; rotating right by one gives ED; and ED XOR AA = 47, the ASCII G. Applying the transform to all nine bytes yields GETWELLV2.
The same decoder exposes both implementation plumbing and identifying material:
| Call instruction | Ciphertext source | Length / selector | Recovered bytes |
|---|---|---|---|
0x452513 |
0x4A99A0 |
9 / 0 | ntdll.dll |
0x455417 |
0x4A9E30 |
9 / 7 | GETWELLV2 |
0x455A63 |
0x4A9EE0 |
11 / 15 | 4.1.5-alpha |
0x47B9B0 |
0x4AAF20 |
2 / 19 | ld |
0x47B9F0 |
0x4AAF30 |
1 / 20 | g |
This is a reversible, sample-specific string-protection scheme, not evidence of a cryptographically secret key. It still imposes an analysis cost: a plain string listing misses the material until the selector, position-dependent key and byte transform are reconstructed together.
The arithmetic agrees with an independent x86 register-pair transcription. An adjacent-constant extractor recovered 469 callsites and left 47 unresolved; that subset missed GetEndpoints, later recovered through its consumers. Absence from the extracted strings is therefore not evidence that a protocol operation is absent.
At 0x49E50D, each byte receives a selector- and position-dependent key before the byte transform; the wrapper appends a NUL after the decoded bytes.
The recovered DLL name also appears at the decoder's runtime return. In the
following paired crops, the process is paused at relocated return address
0x00B32518, corresponding to 0x452518 in the preferred-base image.
Two crops from the same paused view: EAX is 9, and the buffer at 0x00697818 contains ntdll.dll followed by NUL. The displayed instructions after EIP have not yet executed.
Names become hashes, then pointers
Module lookup and export lookup form a second layer of indirection. The helper at 0x4067AC obtains a PEB-derived loader list, considers bounded UTF-16 module names that can be represented in ASCII, and hashes the resulting bytes. The export resolver at 0x434C9C checks PE signatures and export metadata, walks exported names, compares their hashes, then uses the matching ordinal to recover a function RVA.
There is no single universal “Amatera API hash” in this file. Even this pair of routines uses distinct constants for DLL names and function names. Both first require a NUL within the supplied positive bound, then process the preceding bytes. Only ASCII A–Z is folded to lowercase. For the two routines below, the recurrence is:
h = (ROL32(h XOR X, r) * M) XOR lowercase_ascii_byte
All arithmetic is modulo 2^32.
| Purpose / hash helper | Initial h | X | Rotate r | Multiplier M |
|---|---|---|---|---|
Module name, 0x406A41 |
12649D2F |
3B65434D |
10 | D2966FB1 |
Export name used by 0x434C9C, 0x434F90 |
75BAC4FB |
922F2956 |
19 | F0959F6F |
Module-name hashing at 0x406A41: initialization, ASCII case folding and the rotate-left-by-10 recurrence remain visible beneath the obfuscated control flow.
The export-name variant at 0x434F90 uses a different XOR constant, rotate count and multiplier. The entry-point seed is outside this crop.
The first recurrence maps kernel32.dll to 91DEC7EC. The second maps GetTickCount to F9FC8A7F. The orchestrator at 0x47AB20 passes the former value to module lookup and the latter to 0x434C9C, caching the returned pointer in 0x4B3F08. In a debugger run, the resolver received the loaded kernel32.dll base 0x76700000 and hash 0xF9FC8A7F, then returned 0x7671DBE0. That address matched the loaded kernel32!GetTickCount export and the pointer in the relocated cache at 0x00B93F08. The name-hash calculation is therefore corroborated by the resolved runtime target, rather than inferred from a hash match alone.
Two crops from the same resolver-return stop: EIP remains at 0x00B27234, while EAX is 0x7671DBE0. The disassembly follows that pointer to the GetTickCount stub; the cache at 0x00B93F08 holds the same DWORD. This view shows neither a call to the API nor the instruction that wrote the cache.
Other resolver/hash pairs vary both constants and operation order. Consequently, a hash dictionary computed with 0x434F90 cannot simply be reused for every indirect call in the sample. The correct unit of analysis is the caller, its selected resolver and that resolver's hash helper.
One implementation detail also matters when interpreting missing resolutions: 0x434C9C refuses a function RVA that falls inside the export directory, where a forwarded export contains a string rather than executable code. That behavior belongs to this helper. It does not establish how every other resolver or wrapper handles forwarded exports, nor that all of its PE bounds checks constitute a hardened parser.
Separating fake branches from real checks
The byte decoder hides its useful instructions behind unconditional jumps over push/pop pairs, self-moves, tests and no-ops. Following the actual jump targets reduces the zero-guard path to the transform above. Elsewhere, state variables drive long chains of comparisons, turning small loops into flattened dispatchers. The export-name hash at 0x434F90 is a compact example: once its state transitions are separated from the work, the algorithm is a bounded NUL scan followed by a single accumulator update per byte.
A recurring mutation guard tests (x * ~x) & 1. Exactly one of x and its bitwise complement is even, so the product's low bit is always zero. Code guarded by the opposite outcome cannot be taken under the modeled integer semantics. Mixed Boolean/arithmetic expressions add another layer: (a | b) - (a & b) reduces to a XOR b, while (a | b) + (a & b) reduces to addition at the same width. These identities simplify expressions; they do not automatically make the surrounding global state irrelevant.
Two later guards make the same lesson concrete. At 0x490AFC, two operands are constrained to 2–255 before comparing a³ + b³ with (a+b+1)³; the latter is always larger and the largest intermediate remains below signed 32-bit overflow. At 0x490B97, one DWORD read is squared and masked with three. The result must be zero or one, and either case is accepted. Raw instructions show the final comparison using the cached result, despite pseudocode that appears to repeat the shared-memory expression.
Not every suspicious branch is disposable. 0x408490 checks an independent byte at 0x4B4030 before decoding. That byte is initially zero in this file; the offline model covers only that normal path. Its alternate path is not justified away by the parity proof. Likewise, the startup initializer performs actual environment checks.
The 32-bit payload's short 64-bit detour
The PEB getter at 0x4A4EF8 first checks a cached DWORD at 0x4AD1C0. The original file initializes that cache to zero. On a cache miss, a far return selects code segment 0x33; the bytes beginning at 0x4A4F0D must then be decoded in 64-bit mode:
mov rax, qword ptr gs:[0x30]
add rax, 0x2000
mov eax, dword ptr [rax+0x30]
The helper returns through segment 0x23 to 32-bit code and caches EAX. In a linear x86 disassembly, the 48 REX prefixes look like dec eax, producing a misleading account of the same bytes. The transition, not the PE's nominal architecture alone, determines how this block must be read.
The loader-list accesses that follow identify the result as the intended 32-bit PEB. In an instrumented Windows 11 observation, the native TEB was 0x4AE000, the OS-reported 32-bit PEB was 0x4AD000, and both the derived slot and populated cache contained 0x4AD000. The fixed relationship worked there; it is not a universal Windows layout guarantee. The original zero-initialized cache also rules out a stale pointer embedded in the file.
The initializer at 0x401000 separately rejects a nonzero BeingDebugged byte or any of the tested NtGlobalFlag bits 0x70. It also requires three transition-pointer readings to be nonzero and equal, with four-byte alignment, before storing the gateway. Additional initialization checks follow; these observations are necessary conditions, not a complete success specification. Rejection clears the gateway and records state 3; success records state 2. Unlike the parity predicate, these branches test real runtime values.
Failed bootstrap can terminate cleanly
Bootstrap failure can reach the shared termination wrapper at 0x4A5073 with arguments (-1, 0): the current process and a zero exit status. A focused debugger observation traced primary hostname resolution returning WSAHOST_NOT_FOUND, followed by a fallback address-resolution failure before HTTP and a call to that wrapper. A clean exit status therefore need not mean successful collection. The shared termination site itself does not identify which earlier predicate failed.
ScyllaHide instrumented the transition gateway in the debugger environment. The wrapper's decoded service value 0x2C and the loaded NtTerminateProcess selector 0x7002C shared a low service index, but were not identical. Arguments, surrounding code and a recorded zero-status debugger exit support the termination interpretation, not an uninstrumented syscall trace or a causal explanation for every short-lived standalone execution.
Endpoint discovery and the protocol inside TLS
The configured primary hostname is metrics[.]demobunfiber[.]top. The address-normalization helper at 0x4975E0 distinguishes dotted-decimal-shaped text from names that require resolution. Its numeric branch checks lengths and separators rather than fully validating IPv4 octets. The hostname path reaches getaddrinfo with IPv4 and stream-socket hints.
The fallback is a separate dead-drop reader at 0x4976CD. It contains https://telegra[.]ph/Functions-04-03 and sends a GET for that resource. After a successful, nonempty HTTP 200 response, it searches for the first r.] marker and the first subsequent )0( marker. The intervening Base64 decodes to replacement endpoint text. It does not need to understand the surrounding HTML as a document.
There are small but consequential parsing details. The closing marker must have at least one byte after it; a marker ending exactly at the response boundary is not accepted. An invalid first marker pair does not lead to a search for a later valid pair. The Base64 decoder accepts ordinary whitespace and canonical unpadded final groups, but rejects nonzero unused tail bits and the URL-safe alphabet. The extracted input is measured as a C string, so an embedded NUL truncates it. These are reconstructed parser behaviors. The successful collection observations used the primary path and do not demonstrate native acceptance of a fallback page.
TLS is only the outer layer
The primary transport uses TCP port 443 and Schannel. Its credential flags 0x18 and context-request mask 0x8811C select manual credential validation; the HTTP host identity supplies the target-name argument. No separate certificate-validation step appears on the reviewed successful path. The original sample accepted a locally generated self-signed certificate without adding a trust anchor or modifying its validation code. In the recorded exchange, TLS 1.2 retained the primary hostname as SNI even though the connection terminated locally.
Inside TLS, the sample creates another cryptographic session. The first application request is a binary POST / with X-Request-ID: 0, an explicit content length, Content-Type: application/octet-stream and a Chrome-shaped User-Agent. Its body begins with a P-256 public point:
public_x_be[32] | public_y_be[32] | padding[0..63]
The coordinates are big-endian and do not have the SEC1 04 prefix. A usable server response has status 200, at least 64 body bytes containing a valid peer point, and a nonempty X-Request-ID header of at most 63 bytes. Invalid or infinite points are rejected. Bytes after the peer's first 64 coordinate bytes are not used by this handshake.
After scalar multiplication, the sample serializes the shared point's X coordinate as exactly 32 big-endian bytes and derives:
K = SHA256(SHA256(shared_x_be[32]))
The key installer at 0x40A53E stores this 32-byte digest at transport-object offset 0x68. The application session identifier is separate, at 0x88. It is neither the ECDH secret nor a JSON identity field.
The key installer serializes the coordinate to 32 big-endian bytes, applies SHA-256 twice and clears intermediate buffers.
Subsequent authenticated messages use:
nonce[12] | ciphertext[plaintext_length] | tag[16]
The cipher is ChaCha20-Poly1305, with a 96-bit nonce, a 32-bit counter and 20 ChaCha rounds. Block zero supplies the Poly1305 one-time key; encryption begins at counter one. There is no associated data. The MAC processing includes padded ciphertext and the encoded lengths. A successful response's nonempty body is authenticated before its pointer and length are replaced with plaintext. The fixed envelope overhead is 28 bytes; there is no fixed overall message size.
This layering affects packet interpretation. Decrypting TLS exposes HTTP and the application envelope, not automatically the underlying JSON, ZIP or image. Application authentication must be verified separately. Likewise, possession of a TLS key log is not evidence of a weakness in the inner key derivation.
The server assigns the routes
The first authenticated plaintext observed from the original sample was:
{"Command":"GetEndpoints","lu":"en-US","ls":"en-US","d":"WORKGROUP","ukr":false}
That particular serialization is 80 bytes, producing a 108-byte envelope. The two locale fields come through separate conversion paths; their precise API-to-field assignment is not established here. The domain field comes from the TCP/IP Parameters registry Domain value, with WORKGROUP as fallback. The Boolean ukr has not been tied to a fully characterized predicate, so its name is not enough to claim a particular geofence.
The encrypted reply installs string-valued endpoints named b, m, o, w, err, g, t, p, f, a and c. The implementation also retains a bounded auxiliary name/path table. The serializer subsequently copies the chosen path into the request line while retaining the transport host identity.
These letters are endpoint-map keys, not a fixed list of remote URI paths. Throughout this article, notation such as <path assigned to b> denotes the value supplied by the server. Simplified paths in examples are illustrative, not embedded malware IOCs. The Wireshark figures show literal routes assigned by the local emulator, including suffixes such as /result/b; those names are not hardcoded in the executable. The same route key can receive more than one artifact type: extension storage and Chromium databases both use b.
The configuration request goes to the returned c path. Its plaintext contains one embedded build identifier:
{"Id":"019d5422-4706-774a-b979-f5ffb6a3d96c"}
This exact record is 45 bytes, or 73 bytes inside the authenticated envelope. Its Id is fixed in the executable; it is not generated from the victim and is not the later run-correlation string.
After application decryption, a successful nonempty configuration response is Base64-decoded, then XOR-decoded, then parsed as JSON. The repeating XOR key is ten bytes, including its NUL:
38 35 32 31 34 39 37 32 33 00
"852149723" followed by NUL
Thus configuration travels through two content transformations inside authenticated encryption. Results do not reuse that extra Base64/XOR representation. Confusing the two formats produces the wrong plaintext even when the application key is correct.
The XOR stage at 0x45731D uses the key's string length plus one as its period, retaining the NUL position. The preceding Base64 decoding is outside this view.
The following Follow HTTP view shows a configuration exchange from a browser research run: a 73-byte protected request and its own 204-byte response. These are genuine TLS-decrypted packet views, not renderings of receiver logs. The specimen's public Host value remains visible, although the connection was redirected to the isolated emulator. Black rectangles hide only the lab-specific route prefix and session identifier; the remaining assigned route, body bytes, lengths and response are unchanged. This same narrow redaction applies to the later HTTP figures.
Configuration request and response from the browser replay. TLS removal exposes the application ciphertext, not the JSON configuration; the decoded examples above describe the next layers separately. Lab route prefix and session identifier are redacted.
Configuration selects consumers, not interchangeable opcodes
The core collector at 0x48BBFF receives structured configuration. Its JSON wrapper accepts an optional UTF-8 BOM, parses one value and rejects trailing non-whitespace data. Array-only lookup distinguishes arrays from strings, numbers, objects, Booleans and null. Consequently, omitting a key and providing an empty array can lead to different behavior.
The major input groups have different contracts:
| Configuration group | Role |
|---|---|
sW, sM, sO |
File-selection objects interpreted by a shared consumer with different wrapper modes and caller guards |
b |
Browser descriptors shared by consumers with different requirements |
exW, exP, exG |
Extension selectors combined with browser/profile information |
ld |
Post-core retrieval and execution entries |
g |
Post-core screen capture, followed by optional file-selection entries |
This is not a one-letter command dispatcher in which all entries have a common argument block. For example, browser database preparation requires fields that Chromium extension collection can omit. A string-valued path may be a suffix under AppData in one consumer and a literal path selected by a numeric mode in another.
Nor is {} a “do nothing” command. Host-information gathering and the Steam cache consumer are called outside these array guards. An empty g array remains a nonnull node, and its consumer attempts a screenshot before iterating over its elements. Empty arrays can suppress a particular element loop without suppressing preparatory reads or neighboring consumers.
The ordering matters too. The core collects and submits its results, drains its work, and sends f. Only after a successful core return does the orchestrator consult ld and then g. The record named f is therefore a core identity marker, not proof that every configured operation has completed.
Built-in inventory and the identity shared by results
The initial a output contains six fields: l, bt, hi, bv, lu and ls. They carry the run correlation value, build tag, host-derived identifier, build version and locale values. The later host-information producer at 0x45556E builds a larger 18-field JSON object for endpoint p:
| Fields | Meaning supported by the producers |
|---|---|
o, a, c, r |
OS label, architecture, processor count and physical memory in MiB |
un, p |
Username and computer name from the process environment |
l, hi, dn |
Run identity, host-derived identity, and domain/fallback |
bt, dp, el, lt |
Build tag, build/locale description, false Boolean and local-time text |
s, g |
Primary screen dimensions and primary display-device description |
is, li, pl |
Installed-software display names, adapter friendly names and process image names |
The reused letters have local meanings: metadata p is the computer name, not a path, and metadata g is display information, not the top-level screenshot configuration. GetSystemMetrics supplying width and height is also not itself screenshot capture.
Several data sources are more limited than their labels might suggest. The process list comes from system-information class 5 and contains image names, not process dumps or command lines. The software list reads machine Uninstall registration and is not demonstrated to cover every registry view. Adapter entries are friendly names, not necessarily addresses. The display routine returns the first primary-device description rather than an inventory of every GPU.
The host-derived identifier uses computer-name and MachineGuid-related material, with alternate and failure branches. A separate producer at 0x40CF1C combines time, a small generated component and host-derived text into the run value used by l. Its full grammar and random-helper behavior should not be guessed from one observed string.
Three identities must therefore stay separate: the build's fixed configuration Id, the runtime correlation string, and the fixed archive filename f1575b64-8492-4e8b-b102-4d26e8c70371.txt. The filename is constant; its contents vary with the run. Comparing those contents with metadata l connects artifacts even when the sample establishes another application session between uploads.
The metadata producer at 0x45556E queues its archive only after entry insertion and ZIP finalization succeed. Queue admission is separate from transmission.
File selection: why r:false does not mean nonrecursive
The arrays sW, sM and sO converge on mw_collect_configured_files at 0x439698. Their wrappers differ: sW selects mode 1; the other two select mode 0, with extra caller predicates around sO. Mode 1 can latch the selected-file flag later consulted by download entries. It is not a traversal-depth flag.
Each object supplies a filename-pattern array f, path string p, numeric path mode tp, output prefix n and endpoint-map key a. The Boolean lookup for r defaults to false, but the returned value is unused at this callsite. The walker receives recursion options independently.
The path resolver at 0x40CC89 switches on the first character of the numeric text:
| Leading tp character | Path construction |
|---|---|
1 |
Use p unchanged |
2 |
Discovered root + \AppData + p |
3 |
Discovered root + \Desktop + p |
4 |
Discovered root + p |
5 |
Discovered root + \Documents + p |
These are prefix tests, not full numeric-equality comparisons. A JSON string containing a digit does not satisfy the same typed lookup as a number. The native experiment used canonical integer 1, not unusual numeric spellings.
In 0x40CC89, ASCII 4 selects root-plus-path construction; ASCII 1 selects a copy of the supplied path. Recursion and wildcard matching occur elsewhere.
Root discovery happens beneath the parent of USERPROFILE before all remaining fields are checked. Literal path mode still runs within the outer root loop, so one absolute source can be revisited for multiple roots. A filename pattern is applied to basenames with ASCII case-insensitive * and ? matching; it does not stop the walker from traversing directories before filtering. The reviewed recursion path has no reparse-point exclusion. These facts make r:false an especially misleading shorthand for a narrowly scoped collection.
Selected bytes enter the archive under n plus the relative path, with backslashes converted to forward slashes. The field a selects an installed endpoint name; it is not an arbitrary URL field. Two exact prefixes, m\8 and m\7, additionally select key/local-state handling. An empty pattern array does not automatically suppress those special branches.
The standalone file workflow selected one nonempty ordinary marker from a synthetic-only directory. In normalized notation, its archive contained:
<configured prefix>/<marker basename>
f1575b64-8492-4e8b-b102-4d26e8c70371.txt
The first member was byte-for-byte equal to the 51-byte input; the second held the same 53-byte identity as the other results. The sample submitted five identical 439-byte ZIPs through endpoint w. The repeated literal path across discovered roots explains why five uploads do not mean five commands or five distinct files. This covers ordinary reading and finalization; the alternate read paths, special prefixes, sM and sO remain unexercised.
The same run emitted a, p and f. Exact archive plaintext comes from the authenticated receiver; the paired capture provides transport correlation, not independently decrypted HTTP.
A separate debugger replay reached the selected-file ZIP-entry call at
0x43A152, relocated to 0x00B1A152. After execution resumed, its receiver
recorded five identical 438-byte ZIPs, each containing the exact 51-byte marker
and that run's identity. The one-byte archive-size difference from the
standalone result did not change the collected file.
EIP is on the call at 0x00B1A152; insertion has not yet executed at this stop. The code crop establishes the reached boundary, while the later archive records establish the returned contents. No file-buffer interpretation is assigned to the other displayed registers.
The replay's first w upload appears below with its own empty HTTP 200 response. Its 466-byte body consists of the 438-byte ZIP protected by the 28-byte application envelope. The visible /result/w suffix was assigned by the emulator; the ciphertext does not expose the selected filename or its contents.
File replay: the protected upload and its acknowledgment in one Follow HTTP view. Lab route prefix and session identifier are redacted; the ZIP was recovered through application authentication, not by TLS decryption alone.
Extension storage uses a different browser contract
Chromium extension collection at 0x435BB4 accepts browser descriptors with p and n strings. It permits a missing pn and does not consult t. That differs from the prepared database path, which requires both. A descriptor can therefore be usable for extension storage while producing no prepared database entries.
The selected exW object supplies string id and n members. Profile discovery reads Local State and its profile.profiles_order array before iterating over extension entries. Unlike the database-preparation path discussed below, this extension routine does not show the same filesystem fallback for an absent or empty preferred profile list.
For each selected profile, the code examines Local Extension Settings, Sync Extension Settings, IndexedDB and related LevelDB locations. The tested path was an ordinary file under:
<user-root>\AppData\<configured browser suffix>\Default\
Sync Extension Settings\<fixed extension ID>\
The recursive directory collector at 0x436D2F passes entries to 0x43699E. The latter skips the basename LOCK and constructs archive paths from the browser label, profile index, extension label, storage category and relative filename:
<browser>/0/Ext/<extension>/SyncExtSettings/<marker basename>
f1575b64-8492-4e8b-b102-4d26e8c70371.txt
The standalone workflow returned one authenticated b result containing the exact 50-byte ordinary text marker and the shared 53-byte identity in a 538-byte ZIP. Those bytes establish file acquisition and member naming for Sync Extension Settings. The input was not a browser-generated LevelDB database, wallet or account credential. Discovering this directory also enables a later Local Storage check; that directory was absent, so the result does not establish its contents or fallback behavior.
The experiment deliberately omitted pn and t. Database preparation could not populate its descriptor table, while the extension consumer retained enough information to proceed. That separation is stronger evidence about dispatch than interpreting every b result as “browser passwords.”
The extension callback at 0x43699E passes an acquired buffer and its length to the ZIP writer, then checks the archive's flush threshold. This is file packaging, not credential decoding.
A debugger replay exposed that interface immediately before its call at
0x00B16C9E (0x436C9E before relocation). Four stack DWORDs supplied the
archive context, member-name pointer, data pointer 0x008AD8D8 and length
0x32—50 bytes. A separate bounded read matched the data to the marker; the
later result was a 537-byte ZIP containing that marker and its run identity.
Two crops from the same paused view: the call boundary and its four arguments at 0x0058F584. The last DWORD is 0x32; the marker bytes and subsequent insertion result are not shown in these panels.
Firefox's extension consumer at 0x406F5C has another contract. It requires a type beginning with 2, reads prefs.js, parses extensions.webextensions.uuids and maps configured IDs to profile-specific storage UUIDs before locating storage. Its presence in the implementation is not a native Firefox result. Both families ultimately use the negotiated b endpoint, so attribution requires member paths and content, not the endpoint letter alone.
The extension replay's packet view shows a 565-byte upload—the 537-byte ZIP plus the 28-byte envelope—and its own empty HTTP 200. Its assigned b route is shared with browser database results; the decrypted archive member path and marker, not that route alone, identify it as extension storage.
Extension replay: application ciphertext remains after TLS decryption. The server-assigned route suffix is preserved; lab prefix and session identifier are redacted.
Browser databases: acquiring files without decrypting rows
The Chromium-oriented path is best understood as a database-acquisition
pipeline. It selects familiar files, checks their structure, gathers any
available companion files and places the acquired buffers in ZIP entries.
Neither the reviewed dispatchers nor their archive producer translates those
buffers into plaintext password or cookie records. That distinction matters:
receiving a file named Login Data proves acquisition of that file, not
decryption of the records it might contain.
A browser descriptor is more than a filesystem path
The core at 0x48BBFF retrieves b through an array-only configuration lookup.
After its surrounding predicates, it calls descriptor preparation at
0x43750E, followed by the login, cookie and form-data dispatchers at
0x43890C, 0x4385BA and 0x438CAA. An additional extension stage has its own
array guard. Descriptor cleanup at 0x438520 precedes the separately called
Steam collector. The core does not turn each dispatcher's return value into
proof of delivery; that requires examining the resulting uploads.
Preparation requires four members from each object in b:
| Member | Accepted input | Role |
|---|---|---|
n |
String | Browser label used in archive paths |
p |
String | Suffix appended to the current user's AppData path |
pn |
String | Process-name and App Paths lookup input |
t |
Number | Browser-family selector: leading textual 1 or 2 |
The type test is unusual. Numeric lookup returns the parser's numeric-text
pointer; preparation checks its first byte for ASCII 1 or 2, rather than
performing a full integer-equality comparison. A string containing a digit is
not equivalent to a JSON number. The observed run used canonical numeric
1; it did not explore other number spellings.
Missing or wrongly typed members prevent descriptor acceptance, but an empty
string is not necessarily missing. In particular, an empty p leaves the
base at AppData rather than disabling collection. The base is constructed from
the executing user's USERPROFILE, with the native \??\ prefix and
\AppData, before the suffix is appended. This preparation path does not use
the earlier extension collector's multi-user-root discovery.
Each accepted object occupies 48 bytes:
| Offset | Stored value |
|---|---|
+0, +4, +8 |
Label, constructed base path and process-name pointers |
+12 |
Normalized type, 1 or 2 |
+16 |
App Paths lookup result; failure can become an empty string |
+20/+24, +28/+32 |
Two optional key-buffer pointer/length pairs |
+36/+40 |
Profile-vector pointer and profile count |
+44 |
Process-name-presence byte |
Descriptor preparation at 0x43750E accepts leading type characters 1 or 2, stores the normalized type and records process-name presence at offset 44. The typed JSON lookups precede this crop.
The pn consumers have distinct jobs. 0x436E5C searches per-user and
machine App Paths locations, including a machine Wow6432Node alternative,
then trims surrounding whitespace and quotes from the returned value.
0x43D64C compares normalized image names against a class-5 process inventory.
Neither wrapper directly launches the named executable. A missing registration
or absent process does not by itself reject the descriptor. Consequently,
ordinary readable files can be collected without a running browser, although
the process-presence flag also influences later acquisition strategy selection.
Type 1 reads Local State. Its two branches should not be conflated:
os_crypt.encrypted_key can reach 0x4983C8, while
os_crypt.app_bound_encrypted_key reaches 0x498598 only after a minimum-length
and QVBQQg prefix test. Separately, profile.profiles_order supplies profile
names. The tested input omitted os_crypt entirely and contained only:
{"profile":{"profiles_order":["Default"]}}
That selected one profile without supplying either key. It does not prove the
two key helpers work, recover application-bound secrets or bypass a browser's
protection. If the preferred profile array is absent, empty, wrongly typed or
unusable, preparation can instead enumerate directories matching Default,
Guest Profile, System Profile, or Profile followed by a digit. Omitting
the list is therefore not a reliable way to suppress profile discovery.
Four filenames converge on one archive producer
The dispatchers require both an accepted descriptor and a nonempty profile
vector. Their type-1 calls converge on mw_archive_chromium_database at
0x440E9C, with separate source and archive suffixes:
| Dispatcher | Selected source beneath the profile | Main archive member |
|---|---|---|
0x43890C |
Login Data |
<browser>/<index>/Login Data |
0x43890C |
Login Data For Account |
<browser>/<index>/Login Data For Account |
0x4385BA |
Network\Cookies |
<browser>/<index>/Cookies |
0x4385BA, conditional fallback |
Cookies |
<browser>/<index>/Cookies |
0x438CAA |
Web Data |
<browser>/<index>/Web Data |
The login dispatcher attempts both filenames independently. The cookie
dispatcher first uses Network\Cookies; a nonzero producer return causes an
attempt at the legacy Cookies location. The output basename alone cannot
identify which source supplied it. In the native test only the Network source
existed, and its exact bytes were returned. The legacy fallback was not
separately demonstrated.
The producer constructs the source path from the descriptor base, profile
name and selected suffix. Its output combines the configured browser label,
decimal profile index and output suffix, then normalizes separators to /.
It emits a separate ZIP for each acquired database—not a single ZIP requiring
all four database entries. The main file is inserted before the fixed
run-identity member:
<browser>/0/<selected database>
f1575b64-8492-4e8b-b102-4d26e8c70371.txt
Optional buffers would add <browser>/key and <browser>/key1, without the
profile-index component. Available sidecars use the main member name with
-wal, -shm or -journal appended. Those members were absent from the
native result; their source-level construction is not a demonstrated key
recovery or concurrent-database consistency result.
At 0x440E9C, the selected path and descriptor process state enter the SQLite-bundle reader; a returned data buffer gates ZIP initialization.
Later in the same producer, data and the bundle's size go directly to mw_zip_add_entry: the archive receives the database buffer, not decoded SQL rows.
Acquisition includes a SQLite sanity check
mw_acquire_sqlite_bundle at 0x49E7AD receives the source path, pn, the
process-presence flag and a 72-byte output bundle. The bundle holds main-file,
WAL, SHM and rollback-journal pointer/length pairs at offsets 0/4, 8/12,
16/20 and 24/28, followed by validation information, status at +64 and
strategy number at +68. Passing that bundle through to the ZIP writer is
different from decoding SQL rows.
Without a positive process-presence flag, acquisition initially tries strategy
2, then strategy 1; a nonempty process name can still make a later strategy-3
fallback eligible if ordinary acquisition fails. Strategy 2 enters
0x43ED7B, opens the existing file with read-oriented access and sharing, and
passes the handle to 0x43EBBC. The latter checks standard file information,
rejects directory/delete-pending state, bounds size from 1 byte through 100 MiB, creates a
read-only section and maps it into the current process (-1). It copies
the actual file length into its own buffer and then unmaps and closes handles.
A section mapping here is not evidence of remote-process injection.
Strategy 1 reaches 0x4A1A50 through 0x4A1A4B. This ordinary read path
accepts files from 1 byte through 256 MiB, allocates a buffer and requires the returned byte
count to equal the requested length. A pending read is followed by a relative
wait of 50,000,000 100-ns units—five seconds. The saved call shapes support
native file-open/query/section-map/read/wait semantics; they are not a dynamic
trace of named Windows API invocations. The process-related fallback remains
outside the demonstrated path.
The bundle reader also probes sidecars and compares the big-endian main-file change counter at offset 24 against a reread. A changed value can cause one retry after 75 ms. Thus a closed fixture without sidecars does not establish that no additional reads occurred.
mw_validate_sqlite_main_header at 0x49F608 rejects buffers shorter than
101 bytes and checks the SQLite format 3\0 signature. It verifies the
64/32/32 payload fractions, the zero reserved range at offsets 72–91,
power-of-two page size (at least 512, with encoded 1 meaning 65,536),
read/write versions 1 or 2, schema format 1–4 and text encoding 1–3. File
length must contain whole pages; a valid header page count must fit the file.
The first page's B-tree type must be 2, 5, 10 or 13. Further checks handle WAL
layout and page-size agreement, and a WAL-mode main file requires its WAL.
These are structural acceptance checks, not SQLite integrity_check, table
schema validation or decryption of the database contents.
A separate debugger observation stopped in this validator with a 0x400
(1,024-byte) input at 0x0100E270. The buffer began with the SQLite signature
and header, including 512-byte pages and read/write versions 1/1. That
research run subsequently returned all four databases byte-for-byte equal
to the fixed inputs, connecting the in-memory acquisition boundary to the
archive contents without implying password-row decryption.
The first 128 bytes of the 1,024-byte validator input show SQLite format 3 and the database header. Full-file identity is established by the returned archives, not by this header crop alone. This debugger observation and the browser HTTP figure below come from separate executions against the same fixed inputs.
The returned bytes resolve the collection claim
The standalone browser experiment supplied four distinct closed SQLite images, each
1,024 bytes with 512-byte pages and rollback-mode version bytes 1/1.
Each held one generic two-TEXT-column marker table with one nonfunctional row,
not a browser password or cookie schema. A key-free Local State selected
Default, and the fixed pn named no registered or running executable.
In that standalone run, the unchanged sample returned four distinct database artifacts, byte-for-byte equal to the corresponding inputs:
| Returned main basename | Database bytes | ZIP bytes | Application-envelope bytes, derived |
|---|---|---|---|
Login Data |
1,024 | 575 | 603 |
Login Data For Account |
1,024 | 606 | 634 |
Cookies |
1,024 | 564 | 592 |
Web Data |
1,024 | 567 | 595 |
Each archive contained only that database and the same 53-byte run identifier. Independent ZIP inspection checked member order, lengths, DEFLATE output and CRC-32, and compared the complete database bytes—not merely the names or a receiver success flag. There were no keys, sidecars or additional members.
All four results use negotiated endpoint key b. The HTTP URI is supplied by
the endpoint map; it is not a fixed malware IOC. The body is the ZIP inside the
shared ChaCha20-Poly1305 record, nonce[12] | ciphertext | tag[16], with no
associated data. The table's last column is the ZIP length plus that 28-byte
overhead, not a measurement from TLS-decrypted packets. Unlike configuration,
the ZIP is not additionally wrapped in Base64 and repeating XOR.
That standalone run's sequence was a, p, three b results, then the remaining b
and f on a second application session. The shared run identifier, rather
than one transport-session ID, joins the outputs. Archive plaintext comes
from the authenticated receiver; the paired PCAP was not independently
TLS-decrypted.
This is evidence for four raw-file cases in one browser collection workflow.
It does not demonstrate browser credential decryption, application-bound key
recovery, the legacy cookie source, sidecars under concurrent writes or
process-based acquisition. Type 2's logins.json, cookies.sqlite and
formhistory.sqlite branches, plus auxiliary Firefox key/certificate files,
remain static findings; form history must not be mislabeled browsing history.
For detection, the useful correlation is an unexpected executable reading Local State and several profile databases, optionally probing companions, then transmitting correlated binary results. The ordinary mapping path makes file-access visibility important; a rule looking only for a SQL password query or a conventional browser process name could miss this acquisition pattern. These are telemetry opportunities inferred from the implementation, not measured detection coverage.
A separate browser replay returned the same four fixed database images and supplied the TLS-decrypted exchange below. Its first b request carries 603 bytes and receives its own empty HTTP 200. Here the body length is a packet observation; the earlier table describes the standalone run. The assigned b path and binary HTTP body still do not distinguish a raw database from extension storage without authenticating and inspecting the enclosed archive.
Browser replay: one complete protected database upload and its acknowledgment. This network capture is from a different execution than the SQLite-header debugger crop. Lab route prefix and session identifier are redacted; the body is application ciphertext, not visible SQLite bytes.
Steam cache: account-name entropy feeds a DPAPI loop
Steam collection is not selected by a dedicated array in the tested
configuration. mw_collect_steam_cached_tokens at 0x442FE0 is called outside
the browser and file-selection array guards. An empty configuration object
{} therefore does not make the sample inert: the built-in metadata and
Steam checks still execute.
Two files connect installation discovery to the current user
The installation lookup at 0x441DE7 resolves RegOpenKeyExA,
RegQueryValueExA and RegCloseKey. It opens the literal key
HKLM\SOFTWARE\WOW6432Node\Valve\Steam with access mask 0x20019, without an
explicit WOW64 view-selection flag, and requires a nonempty REG_SZ
InstallPath. The collector uses it for one file and the executing user's
profile for the other:
<InstallPath>\config\loginusers.vdf
<USERPROFILE>\AppData\Local\Steam\local.vdf
The exact x86 lookup read the synthetic installation path successfully. Its literal key spelling alone does not establish every physical registry-view alias on x64 Windows.
The parser at 0x441AC4 is a small tree builder. Its 20-byte nodes hold name
and value pointers, a child-pointer array, child count and capacity.
0x441BF2 accepts a quoted key followed by either a quoted scalar or a braced
child object. 0x4434EF copies bytes until a quote or NUL; it does not
interpret escape sequences. Whitespace is ASCII 09–0D or 20. Malformed
input can leave a partial tree rather than an all-or-nothing parse failure,
so this is not a strict general VDF implementation.
0x442282 gathers each AccountName scalar under users in the first file.
A missing scalar becomes an empty string. In the second it follows the path:
MachineUserConfigStore / Software / Valve / Steam / ConnectCache
Each ConnectCache child's value is consumed as hex; its key name does not
select or authenticate a corresponding account. The decoder allocates
floor(string_length/2) bytes, combines pairs of nibbles, ignores an odd
trailing character and converts invalid hex characters to nibble zero. These
permissive parsing details explain the implementation; the experiment used
complete valid hex, not malformed inputs.
The account name is entropy, not a decryption key
For every decoded blob, the nested loop tries every collected account name as
CryptUnprotectData optional entropy. The input DATA_BLOB contains the decoded
hex bytes; the entropy DATA_BLOB points directly at the account-name bytes
with length strlen(AccountName), excluding NUL. Description, reserved and
prompt arguments are null and flags are zero. No additional account-name hash
or KDF appears at this callsite. The normal DPAPI user context still matters;
knowing the account name is not equivalent to possessing a portable DPAPI key.
On success, the consumer appends the returned DPAPI byte count to its aggregate
buffer, then one LF and a terminating NUL. It does not stop after the first
successful entropy candidate or deduplicate plaintexts. The output is released
through resolved LocalFree when available. Later, the wrapper measures the
aggregate as a C string, so an embedded plaintext NUL could truncate the
archived result despite the earlier explicit-length append.
At 0x442282, account-name bytes and their length become DPAPI optional entropy. Successful plaintext is copied into the aggregate, followed by LF and NUL; the accumulated length advances by the plaintext length plus one.
A nonnull aggregate whose C-string length is at least five reaches the ZIP producer. Unlike the Chromium archive, this producer inserts the shared identity first:
f1575b64-8492-4e8b-b102-4d26e8c70371.txt
o/41/tokens.txt
The archive is queued through negotiated endpoint key o. This is not a VDF
upload or a raw DPAPI blob upload: its second member contains the accumulated
successful plaintext plus delimiters. Conversely, the tokens.txt label is
not proof of usable credentials. The reviewed consumer neither validates a
Steam token schema nor contacts Steam to test account access.
A synthetic plaintext separates DPAPI execution from account access
The supplied cache used one fabricated 24-byte ASCII account name and one
45-byte nonfunctional plaintext marker. Ordinary same-user CryptProtectData
protected that marker with the account-name bytes as optional entropy. Its
262-byte output became the hex value in a fabricated local-cache VDF; the
loginusers VDF supplied the matching fabricated account. No Steam client,
installation login, real cache or account token was used.
The delivered configuration was exactly two-byte {}. Its configuration
transform yielded four ASCII bytes, Q0g=, inside the existing authenticated
response. The standalone run returned a 382-byte ZIP on
endpoint o: a 53-byte identity member followed by the exact marker plus one
LF, 46 bytes. The identity used DEFLATE, 53 to 52 bytes; the marker member
was stored uncompressed. Both entries used UTF-8 flag 0x0800. Independently
decoded member bytes and CRCs match the static construction and original
synthetic plaintext.
The result's application envelope is therefore 410 bytes by the fixed
28-byte overhead. It uses the shared binary ZIP record, not the configuration's
Base64/XOR format. The sequence was a, p, o, f over one TLS/application
session; metadata and both identity members agree.
A separate debugger observation captured the resolved DPAPI call returning
EAX=1. Its output DATA_BLOB held a 45-byte length (0x2D) and pointer
0x006B45E0; a bounded read matched the synthetic plaintext. That run's
o archive again contained the exact marker plus LF and its matching run
identity, this time in a 381-byte ZIP. The archive's total size is therefore
run-specific, while the returned plaintext and delimiter relationship remain
the same.
Two crops from the same post-DPAPI stop: EAX=1 reaches the test at 0x00B22B73; the output DATA_BLOB at 0x001AF64C reports 45 bytes at 0x006B45E0. The plaintext itself was checked separately and is not shown in these crops.
This joins installation discovery, VDF traversal, hex decoding, same-user DPAPI, newline formatting and authenticated archive delivery in one observed path. It establishes decryption of a fabricated marker—not another user's DPAPI bypass, a valid Steam session, token replay or account compromise. The historical standalone capture supports the transport correlation; its plaintext is from the authenticated listener journal, not a decrypted Wireshark stream or dynamic API trace.
The strongest endpoint correlation is the sequence of Steam installation lookup, access to both VDF locations, DPAPI activity and subsequent archive transmission by an unrelated executable. DPAPI use alone is common and weak evidence. The filenames, account-entropy flow and sender identity provide the more specific context; their usefulness to a detector still requires suitable telemetry.
The later DPAPI debugger replay also has a TLS-decrypted o exchange: its 381-byte ZIP becomes a 409-byte request body, followed by an empty HTTP 200. Those measurements belong to this replay, not the standalone 382-byte archive described above. The assigned /result/o suffix identifies the negotiated route; it does not reveal the protected marker.
The o upload from the same replay as the DPAPI return and output-blob crops. Lab route prefix and session identifier are redacted. Visible body bytes remain application ciphertext; the synthetic plaintext was established separately from the authenticated archive.
A queued ZIP, an HTTP acknowledgment and f are different events
The result producers assemble ZIP32 archives in memory. Local and central headers carry UTF-8 flag 0x0800; entries use stored method 0 or DEFLATE method 8. Compression is retained only when its output is smaller than the original. Standard CRC-32, member sizes, filenames and DOS-style timestamps accompany the data, and the final directory uses an ordinary zero-comment end record.
The complete ZIP is the plaintext of the application-encrypted POST:
POST <path assigned to the result key>
X-Request-ID: <application session>
Content-Type: application/octet-stream
nonce[12] | ciphertext[ZIP bytes] | tag[16]
This is normalized framing, not a literal request from a capture. Filenames and file contents are inside the archive and application ciphertext, not separate HTTP fields. The result is neither multipart form data nor the configuration's Base64/XOR representation.
At 0x44A7E7, successful admission copies an archive into owned queue memory. A return from this producer says that work was queued, not transmitted. The worker and synchronous-drain paths later invoke 0x4495D7. That helper treats HTTP 200 as success, 404 as terminal, and 401/409/413/500 as another terminal category. Responses such as 429 can lead to retries. These branches can establish a new application session, which is why the archive identity matters beyond one X-Request-ID value.
An empty HTTP 200 response is accepted without another application envelope. A nonempty 200 body must pass the authenticated-response checks. The empty acknowledgment does not carry the submitted image, file or an independent verdict on its meaning; it acknowledges the HTTP transaction under the sample's return-code rules.
The f producer constructs a separate, one-member ZIP containing only the run-correlation text. It does not bundle all previous results, count collected files or attest that they were accepted. Queue shutdown can remove terminal failures as well as successful sends, and the core retains its drain result even if a later final upload fails. An empty queue, nonzero core return or natural exit therefore cannot substitute for the actual expected archives.
These distinctions also constrain negative findings. Repeated identical uploads can reflect traversal or retries; a new session need not represent a new configuration. Conversely, a server-side success event does not prove the intended process or desktop effect. For ld and g, the strongest evidence is necessarily later than the core f record.
ld: downloaded bodies become staged processes
The post-core ld array enters 0x478566. The routine retains object entries and orders positive numeric p values first, with input position breaking ties. Missing or nonpositive priorities sort with 0x7FFFFFFF. The Boolean w defaults to false; when true, the entry additionally requires the selected-file flag that earlier mode-1 file collection can set.
Each entry requires string u and numeric tf/tr fields. Their leading numeric-text characters select retrieval, staging or buffer-oriented branches. This is not three independent opcodes named ld, tf and tr. The surrounding priority and dependency checks are part of the entry's behavior.
The downloaded body is not a collection result
The URL splitter at 0x41C7E8 separates an HTTP or HTTPS prefix, an authority and a path, defaulting to /. Its dotted-decimal classification does not validate IPv4 octet ranges, and the reviewed code does not implement dedicated custom-port, userinfo or IPv6 parsing. It should not be described as a standards-compliant URL parser.
The downloader at 0x41CC9D makes a bodyless GET with explicit Host and Connection: close headers. HTTP selects port 80; HTTPS selects TLS on port 443. A successful outcome requires status 200 and a nonempty body. That body is not placed inside the application's ChaCha20-Poly1305 result envelope or its configuration XOR/Base64 transform. TLS protection and application authentication are different properties.
The file branch chooses a staging base from TEMP, then TMP, then USERPROFILE. A ZIP classifier recognizes three four-byte prefixes; recognizing a prefix is not complete archive validation. An extraction wrapper delegates entry handling to other routines whose overwrite and confinement behavior remain unresolved. When choosing among extracted executables, the parent prefers the shortest name, with a lexical tie-breaker. The direct-write wrapper at 0x41CA8E issues one native-style write; no complete byte-count comparison is visible in that wrapper.
The file dispatcher at 0x4776A8 distinguishes executables, command scripts, DLLs, PowerShell scripts and MSI packages. The corresponding suffix selectors include 1→.exe, 2→.cmd, 3→.dll, 4→.ps1 and 6→.msi. Their paths converge on different builders: the executable reaches the common process launcher; scripts use interpreter construction; the DLL branch includes a SysWOW64 rundll32 ordinal-1 form; and the MSI branch uses msiexec, including recognition of status 3010 in its waiting path. These are reconstructed mechanisms, not demonstrations of all five formats.
The common launcher at 0x477F2C resolves CreateProcessW. Its ten-argument call was obscured by an incorrect nine-argument decompiler type, hiding the output process-information structure. Original instructions at 0x4780B4–0x4780C7 establish the missing argument. This is a useful warning against promoting an apparently uninitialized decompiler variable into a malware defect.
The file launcher reaches CreateProcessW with all ten argument positions and tests the return value. The pointer-like decompiler cast shown here is not the API's actual BOOL return type.
Following an executable through staging and launch
The standalone demonstration selected the executable-file branch: tf=1, tr=1, p=1 and w=false, with one fixed local HTTPS resource. Its response body was a 3,584-byte benign PE that starts the operating system's Calculator without arguments. The final positive priority selected the waiting branch.
Correlated a/p/f archives arrived first. The sample then made the download GET, and the exact served PE was found at the newly staged path. Security event 4688 recorded the sample creating that PE and the PE creating C:\Windows\System32\calc.exe.
A new Calculator application also displayed a fully rendered, visible, uncloaked window. Its activation was brokered: Windows recorded sihost.exe as its creator, rather than treating the application as an ordinary direct descendant of the sample. The absent prior Calculator baseline, staged-file identity, two process-creation edges and new rendered application together establish the fixed host effect. The earlier f upload cannot supply that evidence.
Display state also affected the visible result: a direct system-Calculator control remained blank until ordinary display wake made the same process render. In this environment, process creation alone was insufficient evidence of a usable window.
A separate debugger replay made the process-creation boundary directly
observable. The pre-call arguments at 0x00B580C7 included flags 0x08000004,
combining CREATE_NO_WINDOW with CREATE_SUSPENDED, and an output
PROCESS_INFORMATION pointer at 0x004FF690. The return stop at
0x00B580CD had EAX=1. The returned structure contained process handle
0x31C, thread handle 0x318, process ID 7428 and thread ID 2212. Creation
succeeded; with the initial thread created suspended, that return alone is
not evidence of the child's subsequent execution.
Two crops from the same return stop: the call at 0x00B580C7 has returned to 0x00B580CD, and the structure at 0x004FF690 identifies process 7428 and thread 2212. The pre-call flags were recorded separately; they are not visible in these crops.
The staged executable in this replay matched all 3,584 bytes of the fixed benign PE, and a Calculator interface rendered later. Its brokered application was observed separately from the returned process. This replay did not record a fresh process tree; the earlier 4688 chain belongs to the standalone run, not to the debugger screenshots above.
Calculator rendered later in the same replay. This separate window capture shows the interface, not ancestry or a direct parent edge from process 7428.
The same replay's HTTPS download is visible below. The reassembled 3,584-byte response body was byte-for-byte identical to the fixed benign PE and the staged file. Unlike collection POSTs, the TLS-decrypted body begins directly with MZ and PE headers: there is no inner result envelope. The resource path came from the supplied configuration, not from an embedded Calculator-specific command.
Download GET and its own response from the process-creation replay. Only the beginning of the PE body is shown; the full-body identity was checked separately. The lab route prefix is redacted. This is transfer evidence, not an execution acknowledgment or a fresh process-tree observation.
Other ld paths remain separate implementations
The buffer-oriented PowerShell branch at 0x458D18 normalizes text and byte-order marks, builds a standard-input invocation, and prepares child-process and pipe state. Its worker at 0x45C776 advances through supplied bytes until completion, write failure or no progress. Its completion flag describes delivery to the handle, not successful script execution; it inserts no newline itself. This is not evidence of in-process CLR hosting.
A different buffer branch at 0x476F80 is truncated by an indirect jump in the initial decompilation. Original table arithmetic leads the nonempty path to 0x476FD5. There, the code constructs a dllhost path, creates a suspended child, prepares section-backed mappings, copies supplied bytes into a local writable view, maps the section executable into the child, and performs thread-oriented queue/resume operations. That sequence is consistent with section-backed APC injection. Exact initialized syscall mappings and a runtime injection effect have not been established. The fixed Calculator-file result must not be used as proof of either buffer branch.
g: an empty array still uploads the desktop
The g consumer at 0x412AE4 attempts screen capture before inspecting its first array element. Thus {"g":[]} is a screenshot request in practice, even though there are no file-selection entries. This is a concrete example of why an empty configuration array cannot be treated as a universal “disabled” value.
Resolved API-name hashes identify OpenInputDesktop and SetThreadDesktop, followed by compatible DC/bitmap creation and BitBlt. The desktop-switch result is not checked, so the routine does not promise capture from a hidden or private desktop. Its first raster operation combines CAPTUREBLT | SRCCOPY; plain SRCCOPY is a fallback.
The capture branch at 0x412AE4 first uses 0x40CC0020 (CAPTUREBLT | SRCCOPY), then 0x00CC0020 (SRCCOPY) if that call fails.
Capture geometry comes from virtual-desktop metrics 76–79, not necessarily the primary-display width and height reported in host inventory. The bitmap representation uses a 14-byte file header and 40-byte DIB header, 24-bit pixels and negative height for top-down storage. Its row stride is four-byte aligned. JPEG conversion requests quality 70; the preferred archive member is g/screen/screen.jpg, with g/screen/screen.bmp as fallback. The correlation member is added before submission to the negotiated g endpoint.
The JPEG-copy code uses GlobalSize allocation length rather than logical IStream length. Therefore bytes after an encoded JPEG cannot simply be assumed absent or silently discarded. That is a static concern about this producer, not a claim that the observed image had extra bytes.
A debugger replay reached the screenshot's ZIP-entry call at 0x00AF77EC
(0x4177EC before relocation). The stopped argument tuple identified the
member-name pointer 0x014D4A28, data pointer 0x014FCF50 and length
0x468D3—288,979 bytes. The bounded memory view begins with JPEG SOI/APP0
bytes and the JFIF identifier; a separate stack annotation preserves the full
member name, g/screen/screen.jpg.
Three crops from one paused view: the call boundary, the first 64 image bytes and the complete member-name annotation. The full argument tuple was read separately; these panels do not show a completed ZIP insertion or a capture-API trace.
After resuming, this replay produced a 244,128-byte authenticated ZIP with the 288,979-byte JPEG and the shared identity member. The image fully decoded as RGB at 1,920 × 1,080 and ended at EOI without a trailing allocation tail. The receiver recorded the resulting 244,156-byte application envelope and completed transmission of its empty acknowledgment. Those later records, rather than the pre-call screenshot, establish archive delivery to the receiver.
The image, request and acknowledgment agree
The earlier standalone capture provides a separate image-to-wire comparison. It produced an authenticated 111,713-byte ZIP containing exactly two members: a 128,231-byte RGB JPEG at g/screen/screen.jpg and the same 53-byte run identity used by a/p/f. The JPEG fully decoded to 1,920 × 1,080 pixels and matched an independently captured reference desktop, including wallpaper, icons and console placement. It ended at JPEG EOI with no allocation tail. Neither observed JPEG exercises BMP fallback or excludes padding under different allocation conditions.
The observed f preceded g: stopping interpretation at the core identity record would miss the screenshot entirely.
That standalone workflow also has byte-verified network evidence. TLS decryption of its capture reconstructed the complete 111,741-byte screenshot POST body: the 111,713-byte ZIP plus the 28-byte application-envelope overhead. Those wire bytes matched the preserved authenticated request envelope. The empty HTTP 200 response likewise matched the response actually sent, rather than merely a buffer prepared for transmission.
Application plaintext and transport decryption remain separate evidence. The TLS-decrypted HTTP body is still ChaCha20-Poly1305 ciphertext. The authenticated receiver supplied the ZIP and image; the packet reconstruction independently established that the same envelope and acknowledgment crossed the connection. No application session key was exported for a separate offline application-decryption claim.
The later debugger replay supplies a second byte-exact comparison, shown in the two views below. Its reconstructed POST body is 244,156 bytes and matches the complete authenticated request envelope retained by the receiver. The response is that request's own empty HTTP 200. These are the same replay's 244,128-byte ZIP and 288,979-byte JPEG described at the ZIP-entry boundary, not the earlier standalone image.
Beginning of the screenshot replay's assigned g request. Lab route prefix and session identifier are redacted. The visible bytes are the protected archive, not a JPEG header.
End of the same request and its acknowledgment. These are separate beginning/end crops from one Follow HTTP stream; the body middle is omitted. They are not a continuous or composited view. Complete-body equality comes from packet reassembly and the retained envelope, rather than the displayed excerpts alone.
File selection follows the capture stage
After its screen stage, the same routine can process objects containing p, tp, gl, n, f and r. Here r really does control recursion, unlike the sW consumer. The depth field gl defaults to 0, and path mode defaults to a leading 4. A shortcut branch can resolve .lnk targets and inspect their names and extensions before insertion.
The reviewed per-file insertion check applies a 2-MiB ceiling after the read; larger files may therefore still be read. Five-MiB ZIP flushes and 50-MiB aggregate accounting bound output in this path. These constants belong to g, not the earlier generic file collector.
Literal paths can be revisited across discovered user roots while the same archive is open, making duplicate member names possible. The observed configuration contained no file entries or ld task: it exercised capture and upload, leaving this file-selection loop a static finding.
Detection opportunities at the actual boundaries
Amatera's endpoint letters and labels are useful for understanding a decoded session, but weak standalone network signatures. The server assigns the URI values, TLS hides HTTP, and application encryption still hides content after TLS inspection. A detector should not hardcode a path chosen by an emulator or search a TLS stream for plaintext ZIP member names.
The stronger network relationship is a short binary handshake with X-Request-ID: 0, followed by session-bearing binary POSTs, a 45-byte configuration-identity plaintext inside a 73-byte authenticated body, and correlated result traffic. The latter sizes describe this build's fixed record, not a general encrypted-packet fingerprint: TLS segmentation, padding in the handshake and other records change observable packet lengths. SNI metrics[.]demobunfiber[.]top and the embedded Telegraph path are specimen indicators, while configurable routes are not. No inference about current infrastructure availability follows from their presence.
On the endpoint, useful correlations are more specific than “suspicious API use.” An unrelated executable combining Local State discovery with Login Data, Network Cookies and Web Data acquisition fits the browser path. A section mapped read-only into the current process should be classified as file acquisition, not automatically as injection. Steam installation lookup followed by both VDF reads, DPAPI activity and an outbound archive is more informative than DPAPI alone.
For the download branch, correlate retrieval with a newly written temporary executable and subsequent process creation. In the fixed demonstration, the direct edges ended at calc.exe, while the visible application was brokered. Rules that insist every visible effect remain in a simple descendant chain can miss that distinction. The separate section-backed execution branch needs its own telemetry before assigning it an observed injection technique.
For screenshotting, the meaningful sequence is input-desktop access, graphics capture, image encoding and a later g upload. Inventory calls that merely obtain screen dimensions do not establish capture, and the earlier f record does not establish that the later image arrived.
Static detections can combine the protected-string routine's constants and structure, the decoder's rotate/subtract/XOR path, and the particular module/export-hash implementations. No single constant or empty import table is sufficient. The useful signature is the combination of code structure and behavior, with network routes, database acquisition, staged execution and post-core capture interpreted at their actual boundaries.





































