TL;DR
Recently I decided to research AnyDesk 9.7.10 and found out it uses techniques that malware authors also use. Namely:
- PEB walking.
- An empty import table.
- Encrypted API names.
- Runtime export resolution.
- A large encrypted and compressed PE image.
- Pages made writable and executable.
- Manual relocations and IAT repair.
Which is pretty cool, so in this article we learn malware techniques from non-malicious software.
That makes this sample a useful laboratory for techniques often taught only through malware. We can study the mechanics without pretending that the mechanics settle the question of intent:
A technique is evidence. It is not a verdict.
This article follows the outer AnyDesk executable from its small, importless entry point to the 30,489,600-byte application image hidden inside it. Along the way, we will separate concepts that are often blurred together: PEB walking and export parsing, API name encryption and API hashing, decompression and the x86 BCJ transform, raw-file extraction and in-memory mapping, and EAT lookup and IAT repair.
The goal is not to declare these behaviors harmless. It is to describe them accurately enough that provenance, intent, and execution context can be evaluated separately.
The loader at a glance
The file is best understood as two cooperating loaders. Windows maps a small signed outer image. That image reconstructs a much larger inner PE. An exported loader inside the inner PE then finishes the job.
Signed AnyDesk.exe (8,343,992 bytes)
|
+-- Stage 1: PEB walk + EAT bootstrap
| |
| +-- LCG-XOR decrypts names and payload
| +-- LZMA decode + x86 BCJ reversal
| +-- copy headers and sections into reserved .itext
|
+-- Raw inner PE (30,489,600 bytes)
|
+-- Stage 2: relocations + imports + protections
+-- enter the AnyDesk application
The stages divide the normal work of the Windows image loader:
| Responsibility | Outer stage | Inner stage |
|---|---|---|
| Find a bootstrap module without imports | Yes | No |
| Resolve a small native API surface | Yes | Uses supplied callbacks |
| Decrypt and decompress the embedded PE | Yes | No |
| Copy PE headers and sections by RVA | Yes | No |
| Apply base relocations | No | Yes |
| Load 23 dependencies | No | Yes |
| Resolve 767 imports and repair the IAT | No | Yes |
| Set final per-section protections | No | Yes |
| Enter the real application | No | Yes |
This is not classic Reflective DLL Injection. No remote process is involved,
and there is no conventional ReflectiveLoader export. It is still reasonable
to call the broad mechanism reflective or manual loading, provided we state
which loader responsibilities each stage performs.
Establishing provenance
“Goodware” is useful shorthand, not a technical property. A digital signature cannot prove that code is bug-free or benevolent. It can tell us who signed a particular byte sequence and whether the signed content has changed.
For this study, the outer sample was checked locally before conclusions were drawn:
| Property | Observed value |
|---|---|
| Product version | AnyDesk 9.7.10 |
| Architecture | PE32 / x86 |
| File size | 8,343,992 bytes |
| SHA-256 | 46872febd9684df716d392b457aef6611ae7b8716d2ece6bca30fb97271bce1d |
| Authenticode status | Valid |
| Signer | AnyDesk Software GmbH |
| Certificate validity | 11 February 2026 to 13 February 2027 |
| Outer import directory | Empty |
The hash was recomputed for this article, and Windows reported “Signature verified.” AnyDesk's official download page listed 9.7.10 at 8.3 MB, while its changelog dated that version to 13 July 2026. Those facts do not make every possible execution benign, but together they establish a strong provenance baseline for the file being analyzed.
The recovered inner image is not independently signed. That is expected: Authenticode protects the outer file as a container, while decompression creates a new standalone byte sequence that the publisher did not separately sign. Microsoft documents the PE certificate table as file-oriented data that is not mapped into memory with the image.
This distinction matters in investigations:
- “The extracted payload is unsigned” is an observation.
- “Therefore it was injected by an attacker” is an inference, and a bad one here.
- “The signed container deterministically reconstructs this payload, whose embedded MD5 matches the recovered bytes” is stronger evidence.
The recovered inner image has these identifiers:
| Property | Recovered inner image |
|---|---|
| Size | 30,489,600 bytes (0x1D13C00) |
| Architecture | PE32 / x86 |
| Preferred image base | 0x10000000 |
| MD5 | c280dca9bf3927c8238854cf9dbc62e3 |
| SHA-1 | 5840c5218c0c31d82b1b4308a0979f384b76b75c |
| SHA-256 | 4117961150ab001e524a39be50250d221662f02b302c98e94248fc6ec450f282 |
| Imported DLLs | 23 |
| Imported symbols | 767 |
The MD5 is also carried in the private handoff data built by the outer loader, giving us an internal integrity cross-check.
Why it looks like malware
MITRE ATT&CK includes categories such as software packing, dynamic API resolution, encrypted or encoded files, embedded payloads, and compression. That taxonomy describes techniques adversaries use. It does not claim that adversaries own them.
The same mechanism can serve different engineering goals:
| Mechanism | Common malicious use | Plausible product-engineering use here |
|---|---|---|
| Empty import table | Conceal capabilities from static triage | Keep the bootstrap small and self-contained |
| PEB walking | Find DLLs without observable imports | Bootstrap API resolution without a conventional IAT |
| Encrypted API strings | Frustrate signatures and analysis | Reduce plaintext metadata in a packed wrapper |
| Compressed embedded PE | Hide or shrink a payload | Reduce a 30.49 MB image to an 8.34 MB single-file distribution |
| Manual PE mapping | Run an image outside normal loader telemetry | Reconstruct the application inside a reserved part of the wrapper |
| Temporary RWX memory | Stage executable code | Construct the image before assigning normal section protections |
The useful question is not “does this technique belong to malware?” It is:
What complete behavior does this mechanism enable in this specimen, under this provenance, in this execution context?
That question forces us to reverse the whole data flow instead of classifying a file from one suspicious-looking instruction sequence.
Stage 1 on disk: small code, large reservation
The outer PE layout almost explains the design by itself:
| Section | RVA | Virtual size | Raw size | Characteristics |
|---|---|---|---|---|
.text |
0x00001000 |
0x296D |
0x2A00 |
0x60000020 |
.itext |
0x00004000 |
0x1D33C00 |
0 |
0xC0000080 |
.rdata |
0x01D38000 |
0x434 |
0x600 |
0x40000040 |
.data |
0x01D39000 |
0x7E8344 |
0x7E8000 |
0xC0000040 |
.rsrc |
0x02522000 |
0x4878 |
0x4A00 |
0x40000040 |
.reloc |
0x02527000 |
0x84 |
0x200 |
0x42000040 |
Three details stand out:
.textcontains only about 10 KB of loader code..datacontains almost all the bytes that occupy disk space..itextoccupies no raw file bytes, yet asks the image mapping for 30,620,672 bytes of zero-backed virtual space.
The inner image is not mapped into a fresh, unrelated VirtualAlloc region.
Windows has already made .itext part of the outer module's address range.
Stage 1 turns that reservation into the destination for the reconstructed image.
SIGNED FILE ON DISK OUTER IMAGE IN MEMORY
.text ~10 KB loader ---------> .text loader code
.itext 0 raw bytes ---------> .itext 30,620,672-byte reservation
.data encrypted stream ---------> .data encrypted stream
|
| decrypt + decompress
v
heap buffer: raw inner PE
|
| copy by section RVA
v
reserved .itext range
The outer Import Directory RVA and size are both zero. It cannot begin by
calling GetModuleHandleW, GetProcAddress, HeapAlloc, or VirtualProtect
through a loader-populated IAT. It has to bootstrap itself from state already
present in the process.
Lesson 1: PEB walking solves the import-table paradox
Every normal user-mode process already has core Windows modules mapped. The problem for an importless program is learning where those modules are without first calling an imported function.
On 32-bit Windows, the loader begins with the Thread Environment Block (TEB),
reachable through the FS segment. From there it follows the Process
Environment Block (PEB), loader data, and then a linked list of loaded modules:
fs:[0x18] -> TEB
TEB + 0x30 -> PEB
PEB + 0x0C -> PEB_LDR_DATA
PEB_LDR_DATA + 0x0C -> InLoadOrderModuleList
LDR_DATA_TABLE_ENTRY + 0x18 -> DllBase
LDR_DATA_TABLE_ENTRY + 0x2C -> BaseDllName
The hard-coded offsets deserve a warning: Microsoft documents these as internal structures that may change. They are observations for this specific 32-bit build, not a portable API contract.
Stage 1 walks the list until the decrypted target name matches ntdll. It now
has the base address of a mapped PE but still does not have a function address.
That requires export parsing.
What to learn from PEB walking
PEB walking is not magic and it is not inherently stealthy. It is a manual answer to a dependency problem:
No imports -> no GetProcAddress
No GetProcAddress -> manually find a known module
Known module base -> parse its exports
One resolved loader API -> resolve everything else
For analysts, the useful signal is not merely access to fs:[0x30] or a
linked-list loop. It is the chain of use: a module base recovered from loader
state is subsequently treated as a PE and its export directory is parsed.
Lesson 2: EAT parsing turns a module base into callable APIs
A loaded DLL publishes its symbols through the Export Address Table (EAT) and
the related name and ordinal arrays. Given the base of ntdll, Stage 1 performs
the work normally hidden behind GetProcAddress:
- Validate the
MZheader. - Follow
e_lfanewto the PE headers. - Read the Export Directory from the optional header's data directories.
- Iterate
AddressOfNames. - Compare each exported name with the desired name.
- Use the matching
AddressOfNameOrdinalsentry as an index intoAddressOfFunctions. - Add the function RVA to the module base.
The core lookup looks like this in C-like pseudocode:
void *resolve_export(void *base, const char *wanted) {
uint8_t *image = (uint8_t *)base;
IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)image;
IMAGE_NT_HEADERS32 *nt =
(IMAGE_NT_HEADERS32 *)(image + dos->e_lfanew);
IMAGE_EXPORT_DIRECTORY *exp =
(IMAGE_EXPORT_DIRECTORY *)(
image + nt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
.VirtualAddress);
DWORD *names = (DWORD *)(image + exp->AddressOfNames);
WORD *ordinals = (WORD *)(image + exp->AddressOfNameOrdinals);
DWORD *functions = (DWORD *)(image + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
if (strcmp((char *)(image + names[i]), wanted) == 0) {
return image + functions[ordinals[i]];
}
}
return NULL;
}
The first important target is LdrGetProcedureAddress. Once that native loader
routine is callable, Stage 1 uses it to fill a compact ntdll table including
LdrLoadDll, LdrUnloadDll, NtProtectVirtualMemory, and several other native
functions. It then loads or locates kernel32 and resolves heap, process,
command-line, module, and protection functions.
PEB walk
-> ntdll base
-> manual EAT parse
-> LdrGetProcedureAddress
-> remaining ntdll APIs
-> LdrLoadDll("kernel32")
-> HeapAlloc, VirtualProtect, GetModuleHandleW, ...
API resolving is not necessarily API hashing
This sample corrects a common reverse-engineering shortcut. The loader protects API names, but it does not hash them.
| Technique | Desired name representation | Comparison operation |
|---|---|---|
| This AnyDesk loader | Encrypted bytes, decrypted at runtime | Plain string comparison |
| API hashing | Precomputed fixed-width integer | Hash each export name and compare integers |
A typical API-hash resolver rotates or multiplies an accumulator for every character, then compares the result with a constant embedded in code. That has recognizable consequences: collisions are possible, recovery requires reproducing the exact hash, and the binary may contain no reversible target string.
Here, the desired strings are reversible. Stage 1 decrypts
LdrGetProcedureAddress, walks the export names, and compares plaintext names.
The accurate description is encrypted API names plus dynamic API resolution.
The distinction changes the analysis method. A hash constant calls for algorithm recovery and collision testing. An encrypted string calls for keystream recovery. Calling both “API hashing” hides the most useful clue.
Lesson 3: LCG-XOR is obfuscation, not secure encryption
The wrapper uses the same small stream construction for module names, API names, private export names, and the large embedded payload.
Its state follows a linear congruential generator (LCG):
state[n+1] = (state[n] * 0x0019660D + 0x3C6EF35F) mod 2^32
key[n] = (state[n+1] >> 12) & 0xFF
plain[n] = cipher[n] XOR key[n]
Equivalent C-like pseudocode is short:
void lcg_xor(uint8_t *dst, const uint8_t *src,
size_t length, uint32_t seed) {
uint32_t state = seed;
for (size_t i = 0; i < length; i++) {
state = state * 0x0019660D + 0x3C6EF35F;
dst[i] = src[i] ^ (uint8_t)(state >> 12);
}
}
XOR is its own inverse, so the same loop encrypts and decrypts. The multiplier, increment, seeds, and ciphertext are all in the executable, so there is no durable secret. There is also no authentication tag, so the scheme provides no integrity guarantee.
That makes LCG-XOR a more informative label than “custom encryption.” Its practical effects are still real:
- plaintext API names disappear from simple string listings;
- the on-disk payload no longer begins with a recognizable compression or PE signature;
- import-based classifiers see almost nothing in the outer image; and
- an analyst must recover the loop before commodity tools can identify the blob.
After the recurrence is identified, however, recovery is deterministic. Notable strings produced by this routine include:
ntdll
kernel32
LdrGetProcedureAddress
VirtualProtect
GetModuleHandleW
ldr_thunk_data
loader_entry_thread_thunk
The last two names expose a private ABI between the outer wrapper and the inner image.
Recovering the real executable
At this point the loader stops looking mysterious and becomes a reproducible transformation pipeline.
Step 1: locate the encrypted stream
The encrypted blob begins at file offset 0x3400, the raw start of the outer
.data section. Stage 1 processes exactly 0x7E7F32 bytes, or 8,290,098
decimal, in place.
The payload-specific seed is 0x727F:
uint32_t state = 0x727F;
uint8_t *p = encrypted_blob;
for (uint32_t left = 0x7E7F32; left; left--, p++) {
state = state * 0x0019660D + 0x3C6EF35F;
*p ^= (uint8_t)(state >> 12);
}
Step 2: use recovered bytes as a hypothesis test
After decryption, the blob begins:
5D 00 00 00 04
Those are LZMA properties rather than an MZ header:
| Field | Decoded value |
|---|---|
| Properties byte | 0x5D |
lc |
3 |
lp |
0 |
pb |
2 |
| Dictionary size | 0x04000000 (64 MiB) |
A plausible standard header appearing at the exact start of the blob is a strong checkpoint. It shows that the seed, recurrence, keystream byte, file offset, and length interpretation are mutually consistent.
The compressed data begins after the five property bytes and has length
0x7E7F2D.
Step 3: decompress to the exact output size
The wrapper allocates 0x1D13C00 bytes (30,489,600 decimal) and runs an embedded
LZMA decoder. The function family follows the familiar LZMA SDK design:
property decoding, probability-model allocation and reset, range decoding,
dictionary handling, output flushing, and cleanup.
LZMA is a strong fit for a one-file product. The 7-Zip project describes it as suitable for embedded use because the decoder can be compact while still providing a high compression ratio.
Step 4: reverse the x86 BCJ transform
LZMA decompression alone is not the final step. The output was preconditioned with an x86 branch converter, or BCJ filter.
x86 CALL and JMP instructions commonly store destinations as relative
displacements. The encoded displacement depends on the instruction's address,
so identical logical call targets at different locations produce different byte
sequences. A BCJ encoder rewrites suitable branch operands into a representation
that exposes more repetition to the compressor. The decoder reverses that
rewrite after decompression.
Conceptually, for a relative branch:
target = address_after_instruction + relative_displacement
relative_displacement = target - address_after_instruction
BCJ does not encrypt code and does not make it executable. It improves compressibility, then restores the architecture-correct relative form. Calling it “anti-analysis” would mistake a compression transform for an intent claim.
Step 5: validate the recovered PE
After BCJ reversal, the 30,489,600-byte output begins with ordinary MZ and
PE headers. Its hashes match the values listed earlier, including the MD5
copied by the wrapper into its loader context.
file offset 0x3400: 8,290,098 encrypted bytes
-> LCG-XOR with seed 0x727F
-> LZMA properties: 5D 00 00 00 04
-> LZMA stream: 0x7E7F2D bytes
-> 30,489,600 bytes of architecture-filtered output
-> x86 BCJ reversal
-> raw PE32 image with MZ and PE headers
-> SHA-256 4117961150ab...e6ec450f282
The sequence suggests a general extraction rule: do not search only for the
final MZ. Identify each transform and use the output format of every layer as
a checkpoint.
From recovered file to runnable image
We now possess the actual inner executable as a raw file, but Stage 1 still has to place it into memory.
The outer mapper first changes the protection of the reserved .itext range to
PAGE_EXECUTE_READWRITE. It also changes the in-memory .itext section-header
characteristics to match .text. These are separate actions:
- editing the section header changes PE metadata in memory;
VirtualProtectchanges effective page permissions.
The mapper validates MZ and PE, copies SizeOfHeaders bytes, then copies
every section from PointerToRawData to destination + VirtualAddress.
It does not yet:
- apply base relocations;
- load dependencies;
- resolve imports;
- repair the IAT;
- apply final section protections; or
- run the application's normal initialization.
Its result is PE-shaped memory, not a fully bound image.
The private handoff
The recovered image exports three telling names:
| Export | RVA | Purpose |
|---|---|---|
loader_main_thunk |
0x00801740 |
Bridge to application startup |
ldr_thunk_data |
0x01C34548 |
Shared loader context storage |
loader_entry_thread_thunk |
0x00FD8010 |
Entry into the inner self-loader |
The outer stage parses the inner image's EAT to locate ldr_thunk_data, then
populates it with the mapped base, command line, metadata, error fields,
page-protection table, and native callbacks. Finally, it resolves and directly
calls loader_entry_thread_thunk.
Despite “thread” in the export name, the analyzed handoff is a direct function
call. An available RtlCreateUserThread pointer is not evidence that this path
creates a thread.
Stage 2 completes the loader's work
The inner loader then:
- Computes the relocation delta between the actual
.itextaddress and preferred base0x10000000. - Applies x86
IMAGE_REL_BASED_HIGHLOWrelocations. - Uses supplied
LdrLoadDllandLdrGetProcedureAddresscallbacks to load dependencies. - Walks import descriptors and resolves imports by name or ordinal.
- Writes resolved addresses to
FirstThunk, repairing the IAT. - Converts PE section flags into final page protections.
- Runs initialization phases and transfers control to the application.
The temporary construction-wide RWX permission therefore converges toward a conventional layout: executable/readable code, read-only data, and readable/writable data.
That temporal detail matters to detection. “The process contained RWX image pages” is true during construction. “The application permanently leaves its entire image RWX” is not supported by this path.
EAT lookup and IAT repair: similar acronyms, opposite directions
Packer analyses often say “it parses the IAT” when the code is actually resolving exports. In this loader, both operations happen, but at different stages and for different reasons.
| Question | EAT lookup | IAT repair |
|---|---|---|
| Perspective | Exporting module | Importing image |
| Input | Module base + desired export name | Import descriptor + lookup thunk |
| Reads | Export names, name ordinals, function RVAs | DLL names, import names or ordinals |
| Produces | Address of one exported symbol | Addresses written into many FirstThunk slots |
| Used here by | Outer bootstrap and private handoff | Inner self-loader |
The direction is the mnemonic:
EAT: what does this module offer?
IAT: what does this image need?
Why would normal software do this?
Static analysis can establish effects, not private product requirements. We should distinguish observed consequences from claims about developer intent.
Distribution size
The raw inner image is 30.49 MB. The signed outer file is 8.34 MB. LZMA plus BCJ provides a substantial reduction.
Single-file deployment
The application can be distributed as one portable executable rather than as a small launcher plus a separate large application DLL.
Controlled startup
The private loader ABI carries command-line, build, package, allocator, cleanup, dependency, and status information into the inner image. The vendor controls the transition from container to application.
A minimal bootstrap surface
The outer wrapper has no static imports and needs only enough code to locate Windows loader services, allocate memory, unpack bytes, and transfer control. The full application regains a large, normal import surface after extraction.
Analysis resistance
Whether primary or incidental, the design defeats shallow static triage:
- an imphash of the wrapper says little;
- API names are not visible as plaintext;
- the real application code is compressed and encrypted;
- the signed entry point leads only to a small loader; and
- final base addresses, IAT contents, and protections exist only at runtime.
The engineering goals and defensive side effects can be true at the same time.
What malware analysts can learn from goodware
1. Name the mechanism before assigning motive
“PEB walking” is precise. “Stealth” is an interpretation. “LCG-XOR removes plaintext strings” is demonstrable. “The author intended to evade EDR” is not established by that fact alone.
A good analysis keeps three layers separate:
Observation -> Mechanism -> Contextual judgment
For example:
Import Directory RVA is zero
-> wrapper bootstraps APIs dynamically
-> suspicious in isolation, explainable in this signed packing design
2. Treat provenance as evidence, not absolution
A valid signature is valuable. It raises the cost of an accidental-tampering explanation and ties the bytes to a publisher identity. It does not make behavior safe, prevent a stolen certificate, or rule out abuse of a legitimate tool.
Combine:
- signature validation;
- cryptographic hashes;
- source and release-channel matching;
- version metadata;
- complete behavior; and
- environmental context.
3. Follow data, not labels
The fastest route through this sample was to track transformations:
- Which region supplies the bytes?
- What loop changes them?
- What invariant appears after that loop?
- What size does the next stage expect?
- Is the output file-oriented or memory-oriented?
- Where does control go next?
This approach survives misleading function names, incomplete symbols, and familiar-looking patterns.
4. Use stage boundaries as dump points
The most useful points are semantic boundaries:
| Boundary | What exists there |
|---|---|
| After payload XOR loop | Valid LZMA properties and compressed stream |
| After LZMA + BCJ | Raw-file-oriented PE with meaningful raw offsets |
| After outer mapper | Memory-oriented, section-copied PE |
| At inner loader entry | Image ready for relocation and import repair |
| After import loop | Bound image with populated IAT |
Dumping “somewhere after unpacking” is less useful than knowing which representation exists at that moment.
5. Let false positives improve detections
A rule that fires only on PEB walking, RWX memory, or encrypted strings is a lead generator. A stronger analytic combines sequence and structure:
empty outer imports
-> PEB-derived module base
-> manual export parsing
-> decrypt embedded stream
-> decompress to PE
-> copy sections into image range
-> relocate and bind imports
-> evaluate context
|
+-- trusted release + expected path + known hash/signer
| -> likely product loader
|
+-- unknown origin + persistence + unexpected parent/network behavior
-> escalate investigation
The sequence describes mechanism. The branches begin only when context enters the analysis.
Static reproduction checklist
The key findings can be reproduced without executing the sample:
- Compute the outer SHA-256 and validate its Authenticode signature.
- Confirm PE32/x86, entry-point RVA
0x335F, and an empty Import Directory. - Inspect sections:
.itexthas virtual size0x1D33C00and zero raw size;.databegins at file offset0x3400. - Identify the LCG constants
0x0019660Dand0x3C6EF35F, then recover encrypted API names. - Apply the same recurrence with seed
0x727Fto0x7E7F32bytes at offset0x3400. - Check that the first decrypted bytes are
5D 00 00 00 04. - LZMA-decode the remaining
0x7E7F2Dbytes to exactly0x1D13C00output bytes. - Reverse the x86 BCJ transform.
- Validate
MZ,PE, the recovered hashes, and the three private exports. - Follow
loader_entry_thread_thunkinto relocation, import-binding, protection, and initialization loops.
These checks are deliberately redundant. Reverse engineering becomes much more reliable when offsets, lengths, headers, hashes, exports, and control flow all support the same model.
Limitations
This study covers one signed 32-bit AnyDesk 9.7.10 sample. Other versions, architectures, or distribution channels may use different wrappers. Names assigned to non-exported functions are analytical labels. Some decompiler types and calling conventions may be imperfect even when the underlying data flow is clear.
The analysis does not establish that:
- every AnyDesk build uses this loader;
- every API resolved by the wrapper is exercised on the observed path;
- the optional
.customdecoder path is active in this sample—it is not backed by a.customsection here; - LCG-XOR offers cryptographic confidentiality;
- temporary RWX memory is harmless in every context; or
- a valid signature replaces behavioral analysis.
Most importantly, it does not infer private design intent from implementation alone. Compression, single-file packaging, controlled initialization, and analysis resistance are observable consequences. Only the developer can rank the requirements that motivated them.
Conclusion
AnyDesk's outer executable looks strange because it is not the application in
the conventional sense. It is a signed container and bootstrap loader. It finds
ntdll through process structures, resolves exports without a static import
table, decrypts names and an embedded stream with LCG-XOR, expands that stream
with LZMA and an x86 BCJ reversal, and copies the recovered PE into a large
reservation inside its own mapped image.
A private loader in the recovered image then relocates itself, binds 767 imports from 23 DLLs, applies final protections, and enters normal application code.
Every step belongs in a malware analyst's vocabulary. None belongs exclusively to malware.
That is the real lesson in learning malware from goodware: better detection does not come from memorizing a list of “bad” techniques. It comes from reconstructing the whole mechanism, preserving stage boundaries, verifying provenance, and delaying judgment until the behavior has a context.
Suspicion should start the investigation. Precision should finish it.
References
- Microsoft, PE Format.
- Microsoft, PEB structure and PEB_LDR_DATA structure.
- Microsoft, Understanding Executable File Signing.
- 7-Zip, LZMA SDK.
- 7-Zip, 7z format and x86 BCJ converter.
- AnyDesk, Windows download page.
- AnyDesk, Windows changelog.
- MITRE ATT&CK, Software Packing (T1027.002), Dynamic API Resolution (T1027.007), Embedded Payloads (T1027.009), Encrypted/Encoded File (T1027.013), and Compression (T1027.015).