{
	"id": "cdbeb2fd-3fb7-4bc4-88a7-3b276844df2e",
	"created_at": "2026-04-06T00:16:53.429104Z",
	"updated_at": "2026-04-10T13:12:43.737306Z",
	"deleted_at": null,
	"sha1_hash": "d56881263ebd54121556ce60a18451deda0df261",
	"title": "ScatterBrain: Unmasking the Shadow of PoisonPlug's Obfuscator",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 9370394,
	"plain_text": "ScatterBrain: Unmasking the Shadow of PoisonPlug's Obfuscator\r\nBy Mandiant\r\nPublished: 2025-01-28 · Archived: 2026-04-05 22:04:12 UTC\r\nWritten by: Nino Isakovic\r\nIntroduction\r\nSince 2022, Google Threat Intelligence Group (GTIG) has been tracking multiple cyber espionage operations\r\nconducted by China-nexus actors utilizing POISONPLUG.SHADOW. These operations employ a custom\r\nobfuscating compiler that we refer to as \"ScatterBrain,\" facilitating attacks against various entities across Europe\r\nand the Asia Pacific (APAC) region. ScatterBrain appears to be a substantial evolution of ScatterBee, an\r\nobfuscating compiler previously analyzed by PWC.\r\nGTIG assesses that POISONPLUG is an advanced modular backdoor used by multiple distinct, but likely related\r\nthreat groups based in the PRC, however we assess that POISONPLUG.SHADOW usage appears to be further\r\nrestricted to clusters associated with APT41.\r\nGTIG currently tracks three known POISONPLUG variants:\r\nPOISONPLUG\r\nPOISONPLUG.DEED\r\nPOISONPLUG.SHADOW\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 1 of 77\n\nPOISONPLUG.SHADOW—often referred to as \"Shadowpad,\" a malware family name first introduced by\r\nKaspersky—stands out due to its use of a custom obfuscating compiler specifically designed to evade detection\r\nand analysis. Its complexity is compounded by not only the extensive obfuscation mechanisms employed but also\r\nby the attackers' highly sophisticated threat tactics. These elements collectively make analysis exceptionally\r\nchallenging and complicate efforts to identify, understand, and mitigate the associated threats it poses.\r\nIn addressing these challenges, GTIG collaborates closely with the FLARE team to dissect and analyze\r\nPOISONPLUG.SHADOW. This partnership utilizes state-of-the-art reverse engineering techniques and\r\ncomprehensive threat intelligence capabilities required to mitigate the sophisticated threats posed by this threat\r\nactor. We remain dedicated to advancing methodologies and fostering innovation to adapt to and counteract the\r\never-evolving tactics of threat actors, ensuring the security of Google and our customers against sophisticated\r\ncyber espionage operations.\r\nOverview\r\nIn this blog post, we present our in-depth analysis of the ScatterBrain obfuscator, which has led to the\r\ndevelopment of a complete stand-alone static deobfuscator library independent of any binary analysis frameworks.\r\nOur analysis is based solely on the obfuscated samples we have successfully recovered, as we do not possess the\r\nobfuscating compiler itself. Despite this limitation, we have been able to comprehensively infer every aspect of\r\nthe obfuscator and the necessary requirements to break it. Our analysis further reveals that ScatterBrain is\r\ncontinuously evolving, with incremental changes identified over time, highlighting its ongoing development.\r\nThis publication begins by exploring the fundamental primitives of ScatterBrain, outlining all its components and\r\nthe challenges they present for analysis. We then detail the steps required to subvert and remove each protection\r\nmechanism, culminating in our deobfuscator. Our library takes protected binaries generated by ScatterBrain as\r\ninput and produces fully functional deobfuscated binaries as output.\r\nBy detailing the inner workings of ScatterBrain and sharing our deobfuscator, we hope to provide valuable\r\ninsights into developing effective countermeasures. Our blog post is intentionally exhaustive, drawing from our\r\nexperience in dealing with obfuscation for clients, where we observed a significant lack of clarity in understanding\r\nmodern obfuscation techniques. Similarly, analysts often struggle with understanding even relatively simplistic\r\nobfuscation methods primarily because standard binary analysis tooling is not designed to account for them.\r\nTherefore, our goal is to alleviate this burden and help enhance the collective understanding against commonly\r\nseen protection mechanisms.\r\nFor general questions about obfuscating compilers, we refer to our previous work on the topic, which provides an\r\nintroduction and overview.\r\nScatterBrain Obfuscator\r\nIntroduction\r\nScatterBrain is a sophisticated obfuscating compiler that integrates multiple operational modes and protection\r\ncomponents to significantly complicate the analysis of the binaries it generates. Designed to render modern binary\r\nanalysis frameworks and defender tools ineffective, ScatterBrain disrupts both static and dynamic analyses.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 2 of 77\n\nProtection Modes: ScatterBrain operates in three distinct modes, each determining the overall structure\r\nand intensity of the applied protections. These modes allow the compiler to adapt its obfuscation strategies\r\nbased on the specific requirements of the attack.\r\nProtection Components: The compiler employs key protection components that include the following:\r\nSelective or Full Control Flow Graph (CFG) Obfuscation: This technique restructures the\r\nprogram's control flow, making it very difficult to analyze and create detection rules for.\r\nInstruction Mutations: ScatterBrain alters instructions to obscure their true functionality without\r\nchanging the program's behavior.\r\nComplete Import Protection: ScatterBrain employs a complete protection of a binary's import\r\ntable, making it extremely difficult to understand how the binary interacts with the underlying\r\noperating system.\r\nThese protection mechanisms collectively make it extremely challenging for analysts to deconstruct and\r\nunderstand the functionality of the obfuscated binaries. As a result, ScatterBrain poses a formidable obstacle for\r\ncybersecurity professionals attempting to dissect and mitigate the threats it generates.\r\nModes of Operation\r\nA mode refers to how ScatterBrain will transform a given binary into its obfuscated representation. It is distinct\r\nfrom the actual core obfuscation mechanisms themselves and is more about the overall strategy of applying\r\nprotections. Our analysis further revealed a consistent pattern in applying various protection modes at specific\r\nstages of an attack chain:\r\nSelective: A group of individually selected functions are protected, leaving the remainder of the binary in\r\nits original state. Any import references within the selected functions are also obfuscated. This mode was\r\nobserved to be used strictly for dropper samples of an attack chain.\r\nComplete: The entirety of the code section and all imports are protected. This mode was applied solely to\r\nthe plugins embedded within the main backdoor payload.\r\nComplete \"headerless\": This is an extension of the Complete mode with added data protections and the\r\nremoval of the PE header. This mode was exclusively reserved for the final backdoor payload.\r\nSelective\r\nThe selective mode of protection allows users of the obfuscator to selectively target individual functions within\r\nthe binary for protection. Protecting an individual function involves keeping the function at its original starting\r\naddress (produced by the original compiler and linker) and substituting the first instruction with a jump to the\r\nobfuscated code. The generated obfuscations are stored linearly from this starting point up to a designated \"end\r\nmarker\" that signifies the ending boundary of the applied protection. This entire range constitutes a protected\r\nfunction.\r\nThe disassembly of a call site to a protected function can take the following from:\r\n.text:180001000 sub rsp, 28h\r\n.text:180001004 mov rcx, cs:g_Imagebase\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 3 of 77\n\n.text:18000100B call PROTECTED_FUNCTION ; call to protected func\r\n.text:180001010 mov ecx, eax\r\n.text:180001012 call cs:ExitProcess\r\nFigure 1: Disassembly of a call to a protected function\r\nThe start of the protected function:\r\n.text:180001039 PROTECTED_FUNCTION\r\n.text:180001039 jmp loc_18000DF97 ; jmp into obfuscated code\r\n.text:180001039 sub_180001039 endp\r\n.text:000000018000103E db 48h ; H. ; garbage data\r\n.text:000000018000103F db 0FFh\r\n.text:0000000180001040 db 0C1h\r\nFigure 2: Disassembly inside of a protected function\r\nThe \"end marker\" consists of two sets of padding instructions, an int3 instruction and a single multi-nop\r\ninstruction:\r\nEND_MARKER:\r\n.text:18001A95C CC CC CC CC CC CC CC CC CC CC 66\r\n66 0F 1F 84 00 00 00 00 00\r\n.text:18001A95C int 3\r\n.text:18001A95D int 3\r\n.text:18001A95E int 3\r\n.text:18001A95F int 3\r\n.text:18001A960 int 3\r\n.text:18001A961 int 3\r\n.text:18001A962 int 3\r\n.text:18001A963 int 3\r\n.text:18001A964 int 3\r\n.text:18001A965 int 3\r\n.text:18001A966 db 66h, 66h ; @NOTE: IDA doesn't disassemble properly\r\n.text:18001A966 nop word ptr [rax+rax+00000000h]\r\n; -------------------------------------------------------------------------\r\n; next, original function\r\n.text:18001A970 ; [0000001F BYTES: COLLAPSED FUNCTION\r\n__security_check_cookie. PRESS CTRL-NUMPAD+ TO EXPAND]\r\nFigure 3: Disassembly listing of an end marker\r\nComplete\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 4 of 77\n\nThe complete mode protects every function within the .text section of the binary, with all protections integrated\r\ndirectly into a single code section. There are no end markers to signify protected regions; instead, every function is\r\nuniformly protected, ensuring comprehensive coverage without additional sectioning.\r\nThis mode forces the need for some kind of deobfuscation tooling. Whereas selective mode only protects the\r\nselected functions and leaves everything else in its original state, this mode makes the output binary extremely\r\ndifficult to analyze without accounting for the obfuscation.\r\nComplete Headerless\r\nThis complete mode extends the complete approach to add further data obfuscations alongside the code\r\nprotections. It is the most comprehensive mode of protection and was observed to be exclusively limited to the\r\nfinal payloads of an attack chain. It incorporates the following properties:\r\nFull PE header of the protected binary is removed.\r\nCustom loading logic (a loader) is introduced.\r\nBecomes the entry point of the protected binary\r\nResponsible of ensuring the protected binary is functional\r\nIncludes the option of mapping the final payload within a separate memory region distinct from the\r\ninitial memory region it was loaded in\r\nMetadata is protected via hash-like integrity checks.\r\nThe metadata is utilized by the loader as part of its initialization sequence.\r\nImport protection will require relocation adjustments.\r\nDone through an \"import fixup table\"\r\nThe loader’s entry routine crudely merges with the original entry of the binary by inserting multiple jmp\r\ninstructions to bridge the two together. The following is what the entry point looks like after running our\r\ndeobfuscator against a binary protected in headerless mode.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 5 of 77\n\nFigure 4: Deobfuscated loader entry\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 6 of 77\n\nThe loader's metadata is stored in the .data section of the protected binary. It is found via a memory scan that\r\napplies bitwise XOR operations against predefined constants. The use of these not only locates the metadata but\r\nalso serves a dual purpose of verifying its integrity. By checking that the data matches expected patterns when\r\nXORed with these constants, the loader ensures that the metadata has not been altered or tampered with.\r\nFigure 5: Memory scan to identify the loader's metadata inside the .data section\r\nThe metadata contains the following (in order):\r\nImport fixup table (fully explained in the Import Protection section)\r\nIntegrity-hash constants\r\nRelative virtual address (RVA) of the .data section\r\nOffset to the import fixup table from the start of the .data section\r\nSize, in bytes, of the fixup table\r\nGlobal pointer to the memory address that the backdoor is at\r\nEncrypted and compressed data specific to the backdoor\r\nBackdoor config and plugins\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 7 of 77\n\nFigure 6: Loader's metadata\r\nCore Protection Components\r\nInstruction Dispatcher\r\nThe instruction dispatcher is the central protection component that transforms the natural control flow of a\r\nbinary (or individual function) into scattered basic blocks that end with a unique dispatcher routine that\r\ndynamically guides the execution of the protected binary.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 8 of 77\n\nFigure 7: Illustration of the control flow instruction dispatchers induce\r\nEach call to a dispatcher is immediately followed by a 32-bit encoded displacement positioned at what would\r\nnormally be the return address for the call. The dispatcher decodes this displacement to calculate the destination\r\ntarget for the next group of instructions to execute. A protected binary can easily contain thousands or even tens of\r\nthousands of these dispatchers making manual analysis of them practically infeasible. Additionally, the dynamic\r\ndispatching and decoding logic employed by each dispatcher effectively disrupts CFG reconstruction methods\r\nused by all binary analysis frameworks.\r\nThe decoding logic is unique for each dispatcher and is carried out using a combination of add , sub , xor ,\r\nand , or , and lea instructions. The decoded offset value is then either subtracted from or added to the\r\nexpected return address of the dispatcher call to determine the final destination address. This calculated address\r\ndirects execution to the next block of instructions, which will similarly end with a dispatcher that uniquely\r\ndecodes and jumps to subsequent instruction blocks, continuing this process iteratively to control the program\r\nflow.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 9 of 77\n\nThe following screenshot illustrates what a dispatcher instance looks like when constructed in IDA Pro. Notice the\r\nscattered addresses present even within instruction dispatchers, which result from the obfuscator transforming\r\nfallthrough instructions—instructions that naturally follow the preceding instruction—into pairs of conditional\r\nbranches that use opposite conditions. This ensures that one branch is always taken, effectively creating an\r\nunconditional jump. Additionally, a mov instruction that functions as a no-op is inserted to split these branches,\r\nfurther obscuring the control flow.\r\nFigure 8: Example of an instruction dispatcher and all of its components\r\nThe core logic for any dispatcher can be categorized into the following four phases:\r\n1. Preservation of Execution Context\r\nEach dispatcher selects a single working register (e.g., RSI as depicted in the screenshot) during\r\nthe obfuscation process. This register is used in conjunction with the stack to carry out the intended\r\ndecoding operations and dispatch. \r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 10 of 77\n\nThe RFLAGS register in turn is safeguarded by employing pushfq and popfq instructions before\r\ncarrying out the decoding sequence.\r\n2. Retrieval of Encoded Displacement\r\nEach dispatcher retrieves a 32-bit encoded displacement located at the return address of its\r\ncorresponding call instruction. This encoded displacement serves as the basis for determining the\r\nnext destination address.\r\n3. Decoding Sequence\r\nEach dispatcher employs a unique decoding sequence composed of the following arithmetic and\r\nlogical instructions: xor , sub , add , mul , imul , div , idiv , and , or , and not . This\r\nvariability ensures that no two dispatchers operate identically, significantly increasing the\r\ncomplexity of the control flow.\r\n4. Termination and Dispatch\r\nThe ret instruction is strategically used to simultaneously signal the end of the dispatcher\r\nfunction and redirect the program's control flow to the previously calculated destination address.\r\nIt is reasonable to infer that the obfuscator utilizes a template similar to the one illustrated in Figure 9 when\r\napplying its transformations to the original binary:\r\nFigure 9: Instruction dispatcher template\r\nOpaque Predicates\r\nScatterBrain uses a series of seemingly trivial opaque predicates (OP) that appear straightforward to analysts but\r\nsignificantly challenge contemporary binary analysis frameworks, especially when used collectively. These\r\nopaque predicates effectively disrupt static CFG recovery techniques not specifically designed to counter their\r\nlogic. Additionally, they complicate symbolic execution approaches as well by inducing path explosions and\r\nhindering path prioritization. In the following sections, we will showcase a few examples produced by\r\nScatterBrain.\r\ntest OP\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 11 of 77\n\nThis opaque predicate is constructed around the behavior of the test instruction when paired with an immediate\r\nzero value. Given that the test instruction effectively performs a bitwise AND operation, the obfuscator\r\nexploits the fact that any value bitwise AND-ed with zero always invariably results in zero.\r\nHere are some abstracted examples we can find in a protected binary—abstracted in the sense that all instructions\r\nare not guaranteed to follow one another directly; other forms of mutations can be between them as can instruction\r\ndispatchers.\r\ntest bl, 0\r\njnp loc_56C96 ; we never satisfy these conditions\r\n------------------------------\r\ntest r8, 0\r\njo near ptr loc_3CBC8\r\n------------------------------\r\ntest r13, 0\r\njnp near ptr loc_1A834\r\n------------------------------\r\ntest eax, 0\r\njnz near ptr loc_46806\r\nFigure 10: Test opaque predicate examples\r\nTo grasp the implementation logic of this opaque predicate, the semantics of the test instruction and its effects\r\non the processor's flags register are required. The instruction can affect six different flags in the following manner:\r\nOverflow Flag (OF): Always cleared\r\nCarry Flag (CF): Always cleared\r\nSign Flag (SF): Set if the most significant bit (MSB) of the result is set; otherwise cleared\r\nZero Flag (ZF): Set if the result is 0; otherwise cleared\r\nParity Flag (PF): Set if the number of set bits in the least significant byte (LSB) of the result is even;\r\notherwise cleared\r\nAuxiliary Carry Flag (AF): Undefined\r\nApplying this understanding to the sequences produced by ScatterBrain, it is evident that the generated conditions\r\ncan never be logically satisfied:\r\nSequence Condition Description\r\ntest \u003creg\u003e, 0; jo OF is always cleared\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 12 of 77\n\ntest \u003creg\u003e, 0;\r\njnae/jc/jb\r\nCF is always cleared\r\ntest \u003creg\u003e, 0; js Resulting value will always be zero; therefore, SF can never be set\r\ntest \u003creg\u003e, 0;\r\njnp/jpo\r\nThe number of bits in zero is always zero, which is an even number; therefore,\r\nPF can never be set\r\ntest \u003creg\u003e, 0;\r\njne/jnz\r\nResulting value will always be zero; therefore, ZF will always be set\r\nTable 1: Test opaque predicate understanding\r\njcc OP\r\nThe opaque predicate is designed to statically obscure the original immediate branch targets for conditional branch\r\n( jcc ) instructions. Consider the following examples:\r\ntest eax, eax\r\nja loc_3BF9C\r\nja loc_2D154\r\ntest r13, r13\r\njns loc_3EA84\r\njns loc_53AD9\r\ntest eax, eax\r\njnz loc_99C5\r\njnz loc_121EC\r\ncmp eax, FFFFFFFF\r\njz loc_273EE\r\njz loc_4C227\r\nFigure 11: jcc opaque predicate examples\r\nThe implementation is straightforward: each original jcc instruction is duplicated with a bogus branch target.\r\nSince both jcc instructions are functionally identical except for their respective branch destinations, we can\r\ndetermine with certainty that the first jcc in each pair is the original instruction. This original jcc dictates the\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 13 of 77\n\ncorrect branch target to follow when the respective condition is met, while the duplicated jcc serves to confuse\r\nanalysis tools by introducing misleading branch paths.\r\nStack-Based OP\r\nThe stack-based opaque predicate is designed to check whether the current stack pointer ( rsp ) is below a\r\npredetermined immediate threshold—a condition that can never be true. It is consistently implemented by pairing\r\nthe cmp rsp instruction with a jb (jump if below) condition immediately afterward.\r\ncmp rsp, 0x8d6e\r\njb near ptr unk_180009FDA\r\nFigure 12: Stack-based opaque predicate example\r\nThis technique inserts conditions that are always false, causing CFG algorithms to follow both branches and\r\nthereby disrupt their ability to accurately reconstruct the control flow.\r\nImport Protection\r\nThe obfuscator implements a sophisticated import protection layer. This mechanism conceals the binary 's\r\ndependencies by transforming each original call or jmp instruction directed at an import through a unique\r\nstub dispatcher routine that knows how to dynamically resolve and invoke the import in question.\r\nFigure 13: Illustration of all the components involved in the import protection\r\nIt consists of the following components:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 14 of 77\n\nImport-specific encrypted data: Each protected import is represented by a unique dispatcher stub and a\r\nscattered data structure that stores RVAs to both the encrypted dynamic-link library (DLL) and application\r\nprogramming interface (API) names. We refer to this structure as obf_imp_t . Each dispatcher stub is\r\nhardcoded with a reference to its respective obf_imp_t .\r\nDispatcher stub: This is an obfuscated stub that dynamically resolves and invokes the intended import.\r\nWhile every stub shares an identical template, each contains a unique hardcoded RVA that identifies and\r\nlocates its corresponding obf_imp_t .\r\nResolver routine: Called from the dispatcher stub, this obfuscated routine resolves the import and returns\r\nit to the dispatcher, which facilitates the final call to the intended import. It begins by locating the\r\nencrypted DLL and API names based on the information in obf_imp_t . After decrypting these names, the\r\nroutine uses them to resolve the memory address of the API.\r\nImport decryption routine: Called from the resolver routine, this obfuscated routine is responsible for\r\ndecrypting the DLL and API name blobs through a custom stream cipher implementation. It uses a\r\nhardcoded 32-bit salt that is unique per protected sample.\r\nFixup Table: Present only in headerless mode, this is a relocation fixup table that the loader in headerless\r\nmode uses to correct all memory displacements to the following import protection components:\r\nEncrypted DLL names\r\nEncrypted API names\r\nImport dispatcher references\r\nDispatcher Stub\r\nThe core of the import protection mechanism is the dispatcher stub. Each stub is tailored to an individual import\r\nand consistently employs a lea instruction to access its respective obf_imp_t , which it passes as the only input\r\nto the resolver routine.\r\npush rcx ; save RCX\r\nlea rcx, [rip+obf_imp_t] ; fetch import-specific obf_imp_t\r\npush rdx ; save all other registers the stub uses\r\npush r8\r\npush r9\r\nsub rsp, 28h\r\ncall ObfImportResolver ; resolve the import and return it in RAX\r\nadd rsp, 28h\r\npop r9 ; restore all saved registers\r\npop r8\r\npop rdx\r\npop rcx\r\njmp rax ; invoke resolved import\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 15 of 77\n\nFigure 14: Deobfuscated import dispatcher stub\r\nEach stub is obfuscated through the mutation mechanisms outlined earlier. This applies to the resolver and import\r\ndecryption routines as well. The following is what the execution flow of a stub can look like. Note the scattered\r\naddresses that while presented sequentially are actually jumping all around the code segment due to the instruction\r\ndispatchers.\r\n0x01123a call InstructionDispatcher_TargetTo_11552\r\n0x011552 push rcx\r\n0x011553 call InstructionDispatcher_TargetTo_5618\r\n0x005618 lea rcx, [rip+0x33b5b] ; fetch obf_imp_t\r\n0x00561f call InstructionDispatcher_TargetTo_f00c\r\n0x00f00c call InstructionDispatcher_TargetTo_191b5\r\n0x0191b5 call InstructionDispatcher_TargetTo_1705a\r\n0x01705a push rdx\r\n0x01705b call InstructionDispatcher_TargetTo_05b4\r\n0x0105b4 push r8\r\n0x0105b6 call InstructionDispatcher_TargetTo_f027\r\n0x00f027 push r9\r\n0x00f029 call InstructionDispatcher_TargetTo_18294\r\n0x018294 test eax, 0\r\n0x01829a jo 0xf33c\r\n0x00f77b call InstructionDispatcher_TargetTo_e817\r\n0x00e817 sub rsp, 0x28\r\n0x00e81b call InstructionDispatcher_TargetTo_a556\r\n0x00a556 call 0x6afa (ObfImportResolver)\r\n0x00a55b call InstructionDispatcher_TargetTo_19592\r\n0x019592 test ah, 0\r\n0x019595 call InstructionDispatcher_TargetTo_a739\r\n0x00a739 js 0x1935\r\n0x00a73b call InstructionDispatcher_TargetTo_6eaa\r\n0x006eaa add rsp, 0x28\r\n0x006eae call InstructionDispatcher_TargetTo_6257\r\n0x006257 pop r9\r\n0x006259 call InstructionDispatcher_TargetTo_66d6\r\n0x0066d6 pop r8\r\n0x0066d8 call InstructionDispatcher_TargetTo_1a3cb\r\n0x01a3cb pop rdx\r\n0x01a3cc call InstructionDispatcher_TargetTo_67ab\r\n0x0067ab pop rcx\r\n0x0067ac call InstructionDispatcher_TargetTo_6911\r\n0x006911 jmp rax\r\nFigure 15: Obfuscated import dispatcher stub\r\nResolver Logic\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 16 of 77\n\nobf_imp_t is the central data structure that contains the relevant information to resolve each import. It has the\r\nfollowing form:\r\nstruct obf_imp_t { // sizeof=0x18\r\n uint32_t CryptDllNameRVA; // NOTE: will be 64-bits, due to padding\r\n uint32_t CryptAPINameRVA; // NOTE: will be 64-bits, due to padding\r\n uint64_t ResolvedImportAPI; // Where the resolved address is stored\r\n};\r\nFigure 16: obf_imp_t in its original C struct source form\r\nIt is processed by the resolver routine, which uses the embedded RVAs to locate the encrypted DLL and API\r\nnames, decrypting each in turn. After decrypting each name blob, it uses LoadLibraryA to ensure the DLL\r\ndependency is loaded in memory and leverages GetProcAddress to retrieve the address of the import.\r\nFully decompiled ObfImportResolver :\r\nFigure 17: Fully decompiled import resolver routine\r\nImport Encryption Logic\r\nThe import decryption logic is implemented using a Linear Congruential Generator (LCG) algorithm to generate a\r\npseudo-random key stream, which is then used in a XOR-based stream cipher for decryption. It operates on the\r\nfollowing formula:\r\nXn + 1 = (a • Xn\r\n + c) mod 2\r\n32\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 17 of 77\n\nwhere:\r\na is always hardcoded to 17 and functions as the multiplier\r\nc is a unique 32-bit constant determined by the encryption context and is unique per-protected sample\r\nWe refer to it as the imp_decrypt_const\r\nmod 232 confines the sequence values to a 32-bit range\r\nThe decryption logic initializes with a value from the encrypted data and iteratively generates new values using\r\nthe outlined LCG formula. Each iteration produces a byte derived from the calculated value, which is then\r\nXOR'ed with the corresponding encrypted byte. This process continues byte-by-byte until it reaches a termination\r\ncondition.\r\nA fully recovered Python implementation for the decryption logic is provided in Figure 18.\r\nFigure 18: Complete Python implementation of the import string decryption routine\r\nImport Fixup Table\r\nThe import relocation fixup table is a fixed-size array composed of two 32-bit RVA entries. The first RVA\r\nrepresents the memory displacement of where the data is referenced from. The second RVA points to the actual\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 18 of 77\n\ndata in question. The entries in the fixup table can be categorized into three distinct types, each corresponding to a\r\nspecific import component:\r\nEncrypted DLL names\r\nEncrypted API names\r\nImport dispatcher references\r\nFigure 19: Illustration of the import fixup table\r\nThe location of the fixup table is determined by the loader's metadata, which specifies an offset from the start of\r\nthe .data section to the start of the table. During initialization, the loader is responsible for applying the\r\nrelocation fixups for each entry in the table.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 19 of 77\n\nFigure 20: Loader metadata that shows the Import fixup table entries and metadata used to find it\r\nRecovery\r\nEffective recovery from an obfuscated binary necessitates a thorough understanding of the protection mechanisms\r\nemployed. While deobfuscation often benefits from working with an intermediate representation (IR) rather than\r\nthe raw disassembly—an IR provides more granular control in undoing transformations—this obfuscator\r\npreserves the original compiled code, merely enveloping it with additional protection layers. Given this context,\r\nour deobfuscation strategy focuses on stripping away the obfuscator's transformations from the disassembly to\r\nreveal the original instructions and data. This is achieved through a series of hierarchical phases, where each\r\nsubsequent phase builds upon the previous one to ensure comprehensive deobfuscation.\r\nWe categorize this approach into three distinct categories that we eventually integrate:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 20 of 77\n\n1. CFG Recovery\r\nRestoring the natural control flow by removing obfuscation artifacts at the instruction and  basic\r\nblock levels. This involves two phases:\r\nAccounting for instruction dispatchers: Addressing the core of control flow protection that\r\nobscure the execution flow\r\nFunction identification and recovery: Cataloging scattered instructions and reassembling\r\nthem into their original function counterparts\r\n2. Import Recovery\r\nOriginal Import Table: The goal is to reconstruct the original import table, ensuring that all\r\nnecessary library and function references are accurately restored.\r\n3. Binary Rewriting\r\nGenerating Deobfuscated Executables: This process entails creating a new, deobfuscated\r\nexecutable that maintains the original functionality while removing ScatterBrain's modifications.\r\nGiven the complexity of each category, we concentrate on the core aspects necessary to break the obfuscator by\r\nproviding a guided walkthrough of our deobfuscator's source code and highlighting the essential logic required to\r\nreverse these transformations. This step-by-step examination demonstrates how each obfuscation technique is\r\nmethodically undone, ultimately restoring the binary's original structure.\r\nOur directory structure reflects this organized approach:\r\n+---helpers\r\n| | emu64.py\r\n| | pefile_utils.py\r\n| |--- x86disasm.py\r\n|\r\n\\---recover\r\n | recover_cfg.py\r\n | recover_core.py\r\n | recover_dispatchers.py\r\n | recover_functions.py\r\n | recover_imports.py\r\n |--- recover_output64.py\r\nFigure 21: Directory structure of our deobfuscator library\r\nThis comprehensive recovery process not only restores the binaries to their original state but also equips analysts\r\nwith the tools and knowledge necessary to combat similar obfuscation techniques in the future.\r\nCFG Recovery\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 21 of 77\n\nThe primary obstacle disrupting the natural control flow graph is the use of instruction dispatchers. Eliminating\r\nthese dispatchers is our first priority in obtaining the CFG. Afterward, we need to reorganize the scattered\r\ninstructions back into their original function representations—a problem known as function identification, which\r\nis notoriously difficult to generalize. Therefore, we approach it using our specific knowledge about the obfuscator.\r\nLinearizing the Scattered CFG\r\nOur initial step in recovering the original CFG is to eliminate the scattering effect induced by instruction\r\ndispatchers. We will transform all dispatcher call instructions into direct branches to their resolved targets. This\r\ntransformation linearizes the execution flow, making it straightforward to statically pursue the second phase of our\r\nCFG recovery. This will be implemented via brute-force scanning, static parsing, emulation, and instruction\r\npatching.\r\nFunction Identification and Recovery\r\nWe leverage a recursive descent algorithm that employs a depth-first search (DFS) strategy applied to known entry\r\npoints of code, attempting to exhaust all code paths by \"single-stepping\" one instruction at a time. We add\r\nadditional logic to the processing of each instruction in the form of \"mutation rules\" that stipulate how each\r\nindividual instruction needs to be processed. These rules aid in stripping away the obfuscator's code from the\r\noriginal.\r\nRemoving Instruction Dispatchers\r\nEliminating instruction dispatchers involves identifying each dispatcher location and its corresponding dispatch\r\ntarget. Recall that the target is a uniquely encoded 32-bit displacement located at the return address of the\r\ndispatcher call. To remove instruction dispatchers, it is essential to first understand how to accurately identify\r\nthem. We begin by categorizing the defining properties of individual instruction dispatchers:\r\nTarget of a Near Call\r\nDispatchers are always the destination of a near call instruction, represented by the E8 opcode\r\nfollowed by a 32-bit displacement.\r\nReferences Encoded 32-Bit Displacement at Return Address\r\nDispatchers reference the encoded 32-bit displacement located at the return address on the stack by\r\nperforming a 32-bit read from the stack pointer. This displacement is essential for determining the\r\nnext execution target.\r\nPairing of pushfq and popfq Instructions to Safeguard Decoding\r\nDispatchers use a pair of pushfq and popfq instructions to preserve the state of the RFLAGS\r\nregister during the decoding process. This ensures that the dispatcher does not alter the original\r\nexecution context, maintaining the integrity of register contents.\r\nEnd with a ret Instruction\r\nEach dispatcher concludes with a ret instruction, which not only ends the dispatcher function but\r\nalso redirects control to the next set of instructions, effectively continuing the execution flow.\r\nLeveraging the aforementioned categorizations, we implement the following approach to identify and remove\r\ninstruction dispatchers:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 22 of 77\n\n1. Brute-Force Scanner for Near Call Locations\r\nDevelop a scanner that searches for all near call instructions within the code section of the\r\nprotected binary. This scanner generates a huge array of potential call locations that may serve as\r\ndispatchers.\r\n2. Implementation of a Fingerprint Routine\r\nThe brute-force scan yields a large number of false positives, requiring an efficient method to filter\r\nthem. While emulation can filter out false positives, it is computationally expensive to do it for the\r\nbrute-force results.\r\nIntroduce a shallow fingerprinting routine that traverses the disassembly of each candidate to\r\nidentify key dispatcher characteristics, such as the presence of pushfq and popfq sequences.\r\nThis significantly improves performance by eliminating most false positives before concretely\r\nverifying them through emulation.\r\n3. Emulation of Targets to Recover Destinations\r\nEmulate execution starting from each verified call site to accurately recover the actual dispatch\r\ntargets. Emulating from the call site ensures that the emulator processes the encoded offset data at\r\nthe return address, abstracting away the specific decoding logic employed by each dispatcher.\r\nA successful emulation also serves as the final verification step to confirm that we have identified a\r\ndispatcher.\r\n4. Identification of Dispatch Targets via ret Instructions\r\nUtilize the terminating ret instruction to accurately identify the dispatch target within the binary.\r\nThe ret instruction is a definitive marker indicating the end of a dispatcher function and the point\r\nat which control is redirected, making it a reliable indicator for target identification.\r\nBrute-Force Scanner\r\nThe following Python code implements the brute-force scanner, which performs a comprehensive byte signature\r\nscan within the code segment of a protected binary. The scanner systematically identifies all potential call\r\ninstruction locations by scanning for the 0xE8 opcode associated with near call instructions. The identified\r\naddresses are then stored for subsequent analysis and verification.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 23 of 77\n\nFigure 22: Python implementation of the brute-force scanner\r\nFingerprinting Dispatchers\r\nThe fingerprinting routine leverages the unique characteristics of instruction dispatchers, as detailed in the\r\nInstruction Dispatchers section, to statically identify potential dispatcher locations within a protected binary. This\r\nidentification process utilizes the results from the prior brute-force scan. For each address in this array, the routine\r\ndisassembles the code and examines the resulting disassembly listing to determine if it matches known dispatcher\r\nsignatures.\r\nThis method is not intended to guarantee 100% accuracy, but rather serve as a cost-effective approach to\r\nidentifying call locations with a high likelihood of being instruction dispatchers. Subsequent emulation will be\r\nemployed to confirm these identifications.\r\n1. Successful Decoding of a call Instruction\r\nThe identified location must successfully decode to a call instruction. Dispatchers are always\r\ninvoked via a call instruction. Additionally, dispatchers utilize the return address from the call\r\nsite to locate their encoded 32-bit displacement.\r\n2. Absence of Subsequent call Instructions\r\nDispatchers must not contain any call instructions within their disassembly listing. The presence\r\nof any call instructions within a presumed dispatcher range immediately disqualifies the call\r\nlocation as a dispatcher candidate.\r\n3. Absence of Privileged Instructions and Indirect Control Transfers\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 24 of 77\n\nSimilarly to call instructions, the dispatcher cannot include privileged instructions or indirect\r\nunconditional jmps . Any presence of any such instructions invalidates the call location.\r\n4. Detection of pushfq and popfq Guard Sequences\r\nThe dispatcher must contain pushfq and popfq instructions to safeguard the RFLAGS register\r\nduring decoding. These sequences are unique to dispatchers and suffice for a generic identification\r\nwithout worrying about the differences that arise between how the decoding takes place.\r\nFigure 23 is the fingerprint verification routine that incorporates all the aforementioned characteristics and\r\nvalidation checks given a potential call location:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 25 of 77\n\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 26 of 77\n\nFigure 23: The dispatch fingerprint routine\r\nEmulating Dispatchers to Resolve Destination Targets\r\nAfter filtering potential dispatchers using the fingerprinting routine, the next step is to emulate them in order to\r\nrecover their destination targets.\r\nFigure 24: Emulation sequence used to recover dispatcher destination targets\r\nThe Python code in Figure 24 performs this logic and operates as follows:\r\nInitialization of the Emulator\r\nCreates the core engine for simulating execution ( EmulateIntel64 ), maps the protected binary\r\nimage ( imgbuffer ) into the emulator's memory space, maps the Thread Environment Block (TEB)\r\nas well to simulate a realistic Windows execution environment, and creates an initial snapshot to\r\nfacilitate fast resets before each emulation run without needing to reinitialize the entire emulator\r\neach time.\r\nMAX_DISPATCHER_RANGE specifies the maximum number of instructions to emulate for each\r\ndispatcher. The value 45 is chosen arbitrarily, sufficient given the limited instruction count in\r\ndispatchers even with the added mutations.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 27 of 77\n\nA try / except block is used to handle any exceptions during emulation. It is assumed that\r\nexceptions result from false positives among the potential dispatchers identified earlier and can be\r\nsafely ignored.\r\nEmulating Each Potential Dispatcher\r\nFor each potential dispatcher address ( call_dispatch_ea ), the emulator's context is restored to the\r\ninitial snapshot. The program counter ( emu.pc ) is set to the address of each dispatcher.\r\nemu.stepi() executes one instruction at the current program counter, after which the instruction is\r\nanalyzed to determine whether we have finished.\r\nIf the instruction is a ret , the emulation has reached the dispatch point.\r\nThe dispatch target address is read from the stack using emu.parse_u64(emu.rsp) .\r\nThe results are captured by d.dispatchers_to_target , which maps the dispatcher address to the\r\ndispatch target. The dispatcher address is additionally stored in the d.dispatcher_locs lookup\r\ncache.\r\nThe break statement exits the inner loop, proceeding to the next dispatcher.\r\nPatching and Linearization\r\nAfter collecting and verifying every captured instruction dispatcher, the final step is to replace each call location\r\nwith a direct branch to its respective destination target. Since both near call and jmp instructions occupy 5\r\nbytes in size, this replacement can be seamlessly performed by merely patching the jmp instruction over the\r\ncall .\r\nFigure 25: Patching sequence to transform instruction dispatcher calls to unconditional jmps to their destination\r\ntargets\r\nWe utilize the dispatchers_to_target map, established in the previous section, which associates each dispatcher\r\ncall location with its corresponding destination target. By iterating through this map, we identify each dispatcher\r\ncall location and replace the original call instruction with a jmp . This substitution redirects the execution flow\r\ndirectly to the intended target addresses.\r\nThis removal is pivotal to our deobfuscation strategy as it removes the intended dynamic dispatch element that\r\ninstruction dispatchers were designed to provide. Although the code is still scattered throughout the code segment,\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 28 of 77\n\nthe execution flow is now statically deterministic, making it immediately apparent which instruction leads to the\r\nnext one.\r\nWhen we compare these results to the initial screenshot from the Instruction Dispatcher section, the blocks still\r\nappear scattered. However, their execution flow has been linearized. This progress allows us to move forward to\r\nthe second phase of our CFG recovery.\r\nFigure 26: Linearized instruction dispatcher control flow\r\nFunction Identification and Recovery\r\nBy eliminating the effects of instruction dispatchers, we have linearized the execution flow. The next step involves\r\nassimilating the dispersed code and leveraging the linearized control flow to reconstruct the original functions that\r\ncomprised the unprotected binary. This recovery phase involves several stages, including raw instruction recovery,\r\nnormalization, and the construction of the final CFG.\r\nFunction identification and recovery is encapsulated in the following two abstractions:\r\nRecovered instruction ( RecoveredInstr ): The fundamental unit for representing individual instructions\r\nrecovered from an obfuscated binary. Each instance encapsulates not only the raw instruction data but also\r\nmetadata essential for relocation, normalization, and analysis within the CFG recovery process.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 29 of 77\n\nRecovered function ( RecoveredFunc ): The end result of successfully recovering an individual function\r\nfrom an obfuscated binary. It aggregates multiple RecoveredInstr instances, representing the sequence of\r\ninstructions that constitute the unprotected function. The complete CFG recovery process results in an\r\narray of RecoveredFunc instances, each corresponding to a distinct function within the binary. We will\r\nutilize these results in the final Building Relocations in Deobfuscated Binaries section to produce fully\r\ndeobfuscated binaries.\r\nWe do not utilize a basic block abstraction for our recovery approach given the following reasons. Properly\r\nabstracting basic blocks presupposes complete CFG recovery, which introduces unnecessary complexity and\r\noverhead for our purposes. Instead, it is simpler and more efficient to conceptualize a function as an aggregation\r\nof individual instructions rather than a collection of basic blocks in this particular deobfuscation context.\r\nFigure 27: RecoveredInstr type definition\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 30 of 77\n\nFigure 28: RecoveredFunc type definition\r\nDFS Rule-Guided Stepping Introduction\r\nWe opted for a recursive-depth algorithm given the following reasons:\r\nNatural fit for code traversal: DFS allows us to infer function boundaries based solely on the flow of\r\nexecution. It mirrors the way functions call other functions, making it intuitive to implement and reason\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 31 of 77\n\nabout when reconstructing function boundaries. It also simplifies following the flow of loops and\r\nconditional branches.\r\nGuaranteed execution paths: We concentrate on code that is definitely executed. Given we have at least\r\none known entry point into the obfuscated code, we know execution must pass through it in order to reach\r\nother parts of the code. While other parts of the code may be more indirectly invoked, this entry point\r\nserves as a foundational starting point.\r\nBy recursively exploring from this known entry, we will almost certainly encounter and identify\r\nvirtually all code paths and functions during our traversal.\r\nAdapts to instruction mutations: We tailor the logic of the traversal with callbacks or \"rules\" that\r\nstipulate how we process each individual instruction. This helps us account for known instruction\r\nmutations and aids in stripping away the obfuscator's code.\r\nThe core data structures involved in this process are the following: CFGResult , CFGStepState , and\r\nRuleHandler :\r\nCFGResult : Container for the results of the CFG recovery process. It aggregates all pertinent information\r\nrequired to represent the CFG of a function within the binary, which it primarily consumes from\r\nCFGStepState .\r\nCFGStepState : Maintains the state throughout the CFG recovery process, particularly during the\r\ncontrolled-step traversal. It encapsulates all necessary information to manage the traversal state, track\r\nprogress, and store intermediate results.\r\nRecovered cache: Stores instructions that have been recovered for a protected function without any\r\nadditional cleanup or verification. This initial collection is essential for preserving the raw state of\r\nthe instructions as they exist within the obfuscated binary before any normalization or validation\r\nprocesses are applied after. It is always the first pass of recovery.\r\nNormalized cache: The final pass in the CFG recovery process. It transforms the raw instructions\r\nstored in the recovered cache into a fully normalized CFG by removing all obfuscator-introduced\r\ninstructions and ensuring the creation of valid, coherent functions.\r\nExploration stack: Manages the set of instruction addresses that are pending exploration during the\r\nDFS traversal for a protected function. It determines the order in which instructions are processed\r\nand utilizes a visited set to ensure that each instruction is processed only once.\r\nObfuscator backbone: A mapping to preserve essential control flow links introduced by the\r\nobfuscator\r\nRuleHandler : Mutation rules are merely callbacks that adhere to a specific function signature and are\r\ninvoked during each instruction step of the CFG recovery process. They take as input the current protected\r\nbinary, CFGStepState , and the current step-in instruction. Each rule contains specific logic designed to\r\ndetect particular types of instruction characteristics introduced by the obfuscator. Based on the detection of\r\nthese characteristics, the rules determine how the traversal should proceed. For instance, a rule might\r\ndecide to continue traversal, skip certain instructions, or halt the process based on the nature of the\r\nmutation.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 32 of 77\n\nFigure 29: CFGResult type definition\r\nFigure 30: CFGStepState type definition\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 33 of 77\n\nFigure 31: RuleHandler type definition\r\nThe following figure is an example of a rule that is used to detect the patched instruction dispatchers we\r\nintroduced in the previous section and differentiating them from standard jmp instructions:\r\nFigure 32: RuleHandler example that identifies patched instruction dispatchers and differentiates them from\r\nstandard jmp instructions\r\nDFS Rule-Guided Stepping Implementation\r\nThe remaining component is a routine that orchestrates the CFG recovery process for a given function address\r\nwithin the protected binary. It leverages the CFGStepState to manage the DFS traversal and applies mutation\r\nrules to decode and recover instructions systematically. The result will be an aggregate of RecoveredInstr\r\ninstances that constitute the first pass of raw recovery:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 34 of 77\n\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 35 of 77\n\nFigure 33: Flow chart of our DFS rule-guided stepping algorithm\r\nThe following Python code directly implements the algorithm outlined in Figure 33. It initializes the CFG\r\nstepping state and commences a DFS traversal starting from the function's entry address. During each step of the\r\ntraversal, the current instruction address is retrieved from the to_explore exploration stack and checked against\r\nthe visited set to prevent redundant processing. The instruction at the current address is then decoded, and a\r\nseries of mutation rules are applied to handle any obfuscator-induced instruction modifications. Based on the\r\noutcomes of these rules, the traversal may continue, skip certain instructions, or halt entirely.\r\nRecovered instructions are appended to the recovered cache, and their corresponding mappings are updated\r\nwithin the CFGStepState . The to_explore stack is subsequently updated with the address of the next sequential\r\ninstruction to ensure systematic traversal. This iterative process continues until all relevant instructions have been\r\nexplored, culminating in a CFGResult that encapsulates the fully recovered CFG.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 36 of 77\n\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 37 of 77\n\nFigure 34: DFS rule-guided stepping algorithm Python implementation\r\nNormalizing the Flow\r\nWith the raw instructions successfully recovered, the next step is to normalize the control flow. While the raw\r\nrecovery process ensures that all original instructions are captured, these instructions alone do not form a cohesive\r\nand orderly function. To achieve a streamlined control flow, we must filter and refine the recovered instructions—\r\na process we refer to as normalization. This stage involves several key tasks:\r\nUpdating branch targets: Once all of the obfuscator-introduced code (instruction dispatchers and\r\nmutations) are fully removed, all branch instructions must be redirected to their correct destinations. The\r\nscattering effect introduced by obfuscation often leaves branches pointing to unrelated code segments.\r\nMerging overlapping basic blocks: Contrary to the idea of a basic block as a strictly single-entry, single-exit structure, compilers can produce code in which one basic block begins within another. This\r\noverlapping of basic blocks commonly appears in loop structures. As a result, these overlaps must be\r\nresolved to ensure a coherent CFG.\r\nProper function boundary instruction: Each function must begin and end at well-defined boundaries\r\nwithin the binary's memory space. Correctly identifying and enforcing these boundaries is essential for\r\naccurate CFG representation and subsequent analysis.\r\nSimplifying with Synthetic Boundary Jumps\r\nRather than relying on traditional basic block abstractions—which can impose unnecessary overhead—we employ\r\nsynthetic boundary jumps to simplify CFG normalization. These artificial jmp instructions link otherwise\r\ndisjointed instructions, allowing us to avoid splitting overlapping blocks and ensuring that each function\r\nconcludes at a proper boundary instruction. This approach also streamlines our binary rewriting process when\r\nreconstructing the recovered functions into the final deobfuscated output binary.\r\nMerging overlapping basic blocks and ensuring functions have proper boundary instructions amount to the same\r\nproblem—determining which scattered instructions should be linked together. To illustrate this, we will examine\r\nhow synthetic jumps effectively resolve this issue by ensuring that functions conclude with the correct boundary\r\ninstructions. The exact same approach applies to merging basic blocks together.\r\nSynthetic Boundary Jumps to Ensure Function Boundaries\r\nConsider an example where we have successfully recovered a function using our DFS-based rule-guided\r\napproach. Inspecting the recovered instructions in the CFGState reveals a mov instruction as the final\r\noperation. If we were to reconstruct this function in memory as-is, the absence of a subsequent fallthrough\r\ninstruction would compromise the function's logic.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 38 of 77\n\nFigure 35: Example of a raw recovery that does not end with a natural function boundary instruction\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 39 of 77\n\nTo address this, we introduce a synthetic jump whenever the last recovered instruction is not a natural function\r\nboundary (e.g., ret , jmp , int3 ).\r\nFigure 36: Simple Python routine that identifies function boundary instructions\r\nWe determine the fallthrough address, and if it points to an obfuscator-introduced instruction, we continue forward\r\nuntil reaching the first regular instruction. We call this traversal \"walking the obfuscator's backbone\":\r\nFigure 37: Python routine that implements walking the obfuscator's backbone logic\r\nWe then link these points with a synthetic jump. The synthetic jump inherits the original address as metadata,\r\neffectively indicating which instruction it is logically connected to.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 40 of 77\n\nFigure 38: Example of adding a synthetic boundary jmp to create a natural function boundary\r\nUpdating Branch Targets\r\nAfter normalizing the control flow, adjusting branch targets becomes a straightforward process. Each branch\r\ninstruction in the recovered code may still point to obfuscator-introduced instructions rather than the intended\r\ndestinations. By iterating through the normalized_flow cache (generated in the next section), we identify\r\nbranching instructions and verify their targets using the walk_backbone routine. \r\nThis ensures that all branch targets are redirected away from the obfuscator's artifacts and correctly aligned with\r\nthe intended execution paths. Notice we can ignore call instructions given that any non-dispatcher call\r\ninstruction is guaranteed to always be legitimate and never part of the obfuscator's protection. These will,\r\nhowever, need to be updated during the final relocation phase outlined in the Building Relocations in\r\nDeobfuscated Binaries section. \r\nOnce recalculated, we reassemble and decode the instructions with updated displacements, preserving both\r\ncorrectness and consistency.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 41 of 77\n\nFigure 39: Python routine responsible for updating all branch targets\r\nPutting It All Together\r\nPutting it all together, we developed the following algorithm that builds upon the previously recovered\r\ninstructions, ensuring that each instruction, branch, and block is properly connected, resulting in a completely\r\nrecovered and deobfuscated CFG for an entire protected binary. We utilize the recovered cache to construct a new,\r\nnormalized cache. The algorithm employs the following steps:\r\n1. Iterate Over All Recovered Instructions\r\nTraverse all recovered instructions produced from our DFS-based stepping approach.\r\n2. Add Instruction to Normalized Cache\r\nFor each instruction, add it to the normalized cache, which captures the results of the normalization\r\npass.\r\n3. Identify Boundary Instructions\r\nDetermine whether the current instruction is a boundary instruction.\r\nIf it is a boundary instruction, skip further processing of this instruction and continue to\r\nthe next one (return to Step 1).\r\n4. Calculate Expected Fallthrough Instruction\r\nDetermine the expected fallthrough instruction by identifying the sequential instruction that follows\r\nthe current one in memory.\r\n5. Verify Fallthrough Instruction\r\nCompare the calculated fallthrough instruction with the next instruction in the recovered cache.\r\nIf the fallthrough instruction is not the next sequential instruction in memory, check\r\nwhether it's a recovered instruction we already normalized:\r\nIf it is, add a synthetic jump to link the two together in the normalized cache.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 42 of 77\n\nIf it is not, obtain the connecting fallthrough instruction from the recovery cache and\r\nappend it to the normalized cache.\r\nIf the fallthrough instruction matches the next instruction in the recovered cache:\r\nDo nothing, as the recovered instruction already correctly points to the fallthrough.\r\nProceed to Step 6.\r\n6. Handle Final Instruction\r\nCheck if the current instruction is the final instruction in the recovered cache.\r\nIf it is the final instruction:\r\nAdd a final synthetic boundary jump, because if we reach this stage, we failed the\r\ncheck in Step 3.\r\nContinue iteration, which will cause the loop to exit.\r\nIf it is not the final instruction:\r\nContinue iteration as normal (return to Step 1).\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 43 of 77\n\nFigure 40: Flow chart of our normalization algorithm\r\nThe Python code in Figure 41 directly implements these normalization steps. It iterates over the recovered\r\ninstructions and adds them to a normalized cache ( normalized_flow ), creates a linear mapping, and identifies\r\nwhere synthetic jumps are required. When a branch target points to obfuscator-injected code, it walks the\r\nbackbone ( walk_backbone ) to find the next legitimate instruction. If the end of a function is reached without a\r\nnatural boundary, a synthetic jump is created to maintain proper continuity. After the completion of the iteration,\r\nevery branch target is updated ( update_branch_targets ), as illustrated in the previous section, to ensure that\r\neach instruction is correctly linked, resulting in a fully normalized CFG:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 44 of 77\n\nFigure 41: Python implementation of our normalization algorithm\r\nObserving the Results\r\nAfter applying our two primary passes, we have nearly eliminated all of the protection mechanisms. Although\r\nimport protection remains to be addressed, our approach effectively transforms an incomprehensible mess into a\r\nperfectly recovered CFG.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 45 of 77\n\nFor example, Figure 42 and Figure 43 illustrate the before and after of a critical function within the backdoor\r\npayload, which is a component of its plugin manager system. Through additional analysis of the output, we can\r\nidentify functionalities that would have been impossible to delineate, much less in such detail, without our\r\ndeobfuscation process.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 46 of 77\n\nFigure 42: Original obfuscated shadow::PluginProtocolCreateAndConfigure routine\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 47 of 77\n\nFigure 43: Completely deobfuscated and functional shadow::PluginProtocolCreateAndConfigure routine\r\nImport Recovery\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 48 of 77\n\nRecovering and restoring the original import table revolves around identifying which import location is associated\r\nwith which import dispatcher stub. From the stub dispatcher, we can parse the respective obf_imp_t reference in\r\norder to determine the protected import that it represents.\r\nWe pursue the following logic:\r\nIdentify each valid call/jmp location associated to an import\r\nThe memory displacement for these will point to the respective dispatcher stub.\r\nFor HEADERLESS mode, we need to first resolve the fixup table to ensure the displacement points\r\nto a valid dispatcher stub.\r\nFor each valid location traverse the dispatcher stub to extract the obf_imp_t\r\nThe obf_imp_t contains the RVAs to the encrypted DLL and API names.\r\nImplement the string decryption logic\r\nWe need to reimplement the decryption logic in order to recover the DLL and API names.\r\nThis was already done in the initial Import Protection section.\r\nWe encapsulate the recovery of imports with the following RecoveredImport data structure:\r\nFigure 44: RecoveredImport type definition\r\nRecoveredImport serves as the result produced for each import that we recover. It contains all the relevant data\r\nthat we will use to rebuild the original import table when producing the deobfuscated image.\r\nLocate Protected Import CALL and JMP Sites\r\nEach protected import location will be reflected as either an indirect near call ( FF/2 ) or an indirect near jmp\r\n( FF/4 ):\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 49 of 77\n\nFigure 45: Disassembly of import calls and jmps representation\r\nIndirect near calls and jmps fall under the FF group opcode where the Reg field within the ModR/M byte\r\nidentifies the specific operation for the group:\r\n/2 : corresponds to CALL r/m64\r\n/4 : corresponds to JMP r/m64\r\nTaking an indirect near call as an example and breaking it down looks like the following:\r\n1. FF : group opcode.\r\n2. 15 : ModR/M byte specifying CALL r/m64 with RIP-relative addressing.\r\n15 is encoded in binary as 00010101\r\nMod (bits 6-7):  00\r\nIndicates either a direct RIP-relative displacement or memory addressing with no\r\ndisplacement.\r\nReg (bits 3-5): 010\r\nIdentifies the call operation for the group\r\nR/M (bits 0-2): 101\r\nIn 64-bit mode with Mod 00 and R/M 101 , this indicates RIP-relative addressing.\r\n3. \u003c32-bit displacement\u003e : added to RIP to compute the absolute address.\r\nTo find each protected import location and their associated dispatcher stubs we implement a trivial brute force\r\nscanner that locates all potential indirect near call/jmps via their first two opcodes.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 50 of 77\n\nFigure 46: Brute-force scanner to locate all possible import locations\r\nThe provided code scans the code section of a protected binary to identify and record all locations with opcode\r\npatterns associated with indirect call and jmp instructions. This is the first step we take, upon which we apply\r\nadditional verifications to guarantee it is a valid import site.\r\nResolving the Import Fixup Table\r\nWe have to resolve the fixup table when we recover imports for the HEADERLESS protection in order to identify\r\nwhich import location is associated with which dispatcher. The memory displacement at the protected import site\r\nwill be paired with its resolved location inside the table. We use this displacement as a lookup into the table to find\r\nits resolved location.\r\nLet's take a jmp instruction to a particular import as an example.\r\nFigure 47: Example of a jmp import instruction including its entry in the import fixup table and the associated\r\ndispatcher stub\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 51 of 77\n\nThe jmp instruction's displacement references the memory location 0x63A88 , which points to garbage data.\r\nWhen we inspect the entry for this import in the fixup table using the memory displacement, we can identify the\r\nlocation of the dispatcher stub associated with this import at 0x295E1 . The loader will update the referenced data\r\nat 0x63A88 with 0x295E1 , so that when the jmp instruction is invoked, execution is appropriately redirected to\r\nthe dispatcher stub. \r\nFigure 48 is the deobfuscated code in the loader responsible for resolving the fixup table. We need to mimic this\r\nbehavior in order to associate which import location targets which dispatcher.\r\n$_Loop_Resolve_ImpFixupTbl:\r\n mov ecx, [rdx+4] ; fixup , either DLL, API, or ImpStub\r\n mov eax, [rdx] ; target ref loc that needs to be \"fixed up\"\r\n inc ebp ; update the counter\r\n add rcx, r13 ; calculate fixup fully (r13 is imgbase)\r\n add rdx, 8 ; next pair entry\r\n mov [r13+rax+0], rcx ; update the target ref loc w/ full fixup\r\n movsxd rax, dword ptr [rsi+18h] ; fetch imptbl total size, in bytes\r\n shr rax, 3 ; account for size as a pair-entry\r\n cmp ebp, eax ; check if done processing all entries\r\n jl $_Loop_Resolve_ImpTbl\r\nFigure 48: Deobfuscated disassembly of the algorithm used to resolve the import fixup table\r\nResolving the import fixup table requires us to have first identified the data section within the protected binary\r\nand the metadata that identifies the import table ( IMPTBL_OFFSET , IMPTBL_SIZE ). The offset to the fixup table is\r\nfrom the start of the data section.\r\nFigure 49: Python re-implementation of the algorithm used to resolve the import fixup table\r\nHaving the start of the fixup table, we simply iterate one entry at a time and identify which import displacement\r\n( location ) is associated with which dispatcher stub ( fixup ).\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 52 of 77\n\nRecovering the Import\r\nHaving obtained all potential import locations from the brute-force scan and accounted for relocations in\r\nHEADERLESS mode, we can proceed with the final verifications to recover each protected import. The recovery\r\nprocess is conducted as follows:\r\n1. Decode the location into a valid call or jmp instruction\r\nAny failure in decoding indicates that the location does not contain a valid instruction and can be\r\nsafely ignored.\r\n2. Use the memory displacement to locate the stub for the import\r\nIn HEADERLESS mode, each displacement serves as a lookup key into the fixup table for the\r\nrespective dispatcher.\r\n3. Extract the obf_imp_t structure within the dispatcher\r\nThis is achieved by statically traversing a dispatcher's disassembly listing.\r\nThe first lea instruction encountered will contain the reference to the obf_imp_t .\r\n4. Process the obf_imp_t to decrypt both the DLL and API names\r\nUtilize the two RVAs contained within the structure to locate the encrypted blobs for the DLL and\r\nAPI names.\r\nDecrypt the blobs using the outlined import decryption routine.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 53 of 77\n\nFigure 50: Loop that recovers each protected import\r\nThe Python code iterates through every potential import location ( potential_stubs ) and attempts to decode each\r\npresumed call or jmp instruction to an import. A try / except block is employed to handle any failures,\r\nsuch as instruction decoding errors or other exceptions that may arise. The assumption is that any error invalidates\r\nour understanding of the recovery process and can be safely ignored. In the full code, these errors are logged and\r\ntracked for further analysis should they arise.\r\nNext, the code invokes a GET_STUB_DISPLACEMENT helper function that obtains the RVA to the dispatcher\r\nassociated with the import. Depending on the mode of protection, one of the following routines is used:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 54 of 77\n\nFigure 51: Routines that retrieve the stub RVA based on the protection mode\r\nThe recover_import_stub function is utilized to reconstruct the control flow graph (CFG) of the import stub,\r\nwhile _extract_lea_ref examines the instructions in the CFG to locate the lea reference to the obf_imp_t .\r\nThe GET_DLL_API_NAMES function operates similarly to GET_STUB_DISPLACEMENT , accounting for slight\r\ndifferences depending on the protection mode:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 55 of 77\n\nFigure 52: Routines that decrypt the DLL and API blobs based on the protection mode\r\nAfter obtaining the decrypted DLL and API names, the code possesses all the necessary information to reveal the\r\nimport that the protection conceals. The final individual output of each import entry is captured in a\r\nRecoveredImport object and two dictionaries:\r\nd.imports\r\nThis dictionary maps the address of each protected import to its recovered state. It allows for the\r\nassociation of the complete recovery details with the specific location in the binary where the\r\nimport occurs.\r\nd.imp_dict_builder\r\nThis dictionary maps each DLL name to a set of its corresponding API names. It is used to\r\nreconstruct the import table, ensuring a unique set of DLLs and the APIs utilized by the binary.\r\nThis systematic collection and organization prepare the necessary data to facilitate the restoration of the original\r\nfunctionality in the deobfuscated output. In Figure 53 and Figure 54, we can observe these two containers to\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 56 of 77\n\nshowcase their structure after a successful recovery:\r\nFigure 53: Output of the d.imports dictionary after a successful recovery\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 57 of 77\n\nFigure 54: Output of the d.imp_dict_builder dictionary after a successful recovery\r\nObserving the Final Results\r\nThis final step—rebuilding the import table using this data—is performed by the build_import_table function\r\nin the pefile_utils.py source file. This part is omitted from the blog post due to its unavoidable length and the\r\nnumerous tedious steps involved. However, the code is well-commented and structured to thoroughly address and\r\nshowcase all aspects necessary for reconstructing the import table.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 58 of 77\n\nNonetheless, the following figure illustrates how we generate a fully functional binary from a headerless-protected\r\ninput. Recall that a headerless-protected input is a raw, headerless PE binary, almost analogous to a shellcode\r\nblob. From this blob we produce an entirely new, functioning binary with the entirety of its import protection\r\ncompletely restored. And we can do the same for all protection modes.\r\nFigure 55: Display of completely restored import table for a binary protected in HEADERLESS mode\r\nBuilding Relocations in Deobfuscated Binaries\r\nNow that we can fully recover the CFG of protected binaries and provide complete restoration of the original\r\nimport tables, the final phase of the deobfuscator involves merging these elements to produce a functional\r\ndeobfuscated binary. The code responsible for this process is encapsulated within the recover_output64.py and\r\nthe pefile_utils.py Python files.\r\nThe rebuild process comprises two primary steps:\r\n1. Building the Output Image Template\r\n2. Building Relocations\r\n1. Building the Output Image Template\r\nCreating an output image template is essential for generating the deobfuscated binary. This involves two key\r\ntasks:\r\nTemplate PE Image: A Portable Executable (PE) template that serves as the container for the output\r\nbinary that incorporates the restoration of all obfuscated components. We also need to be cognizant of all\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 59 of 77\n\nthe different characteristics between in-memory PE executables and on-file PE executables.\r\nHandling Different Protection Modes: Different protection modes and input stipulate different\r\nrequirements.\r\nHeaderless variants have their file headers stripped. We must account for these variations to\r\naccurately reconstruct a functioning binary.\r\nSelective protection preserves the original imports to maintain functionality as well as includes a\r\nspecific import protection for all the imports leveraged within the selected functions. \r\n2. Building Relocations\r\nBuilding relocations is a critical and intricate part of the deobfuscation process. This step ensures that all address\r\nreferences within the deobfuscated binary are correctly adjusted to maintain functionality. It generally revolves\r\naround the following two phases:\r\nCalculating Relocatable Displacements: Identifying all memory references within the binary that require\r\nrelocation. This involves calculating the new addresses where these references will point to. The technique\r\nwe will use is generating a lookup table that maps original memory references to their new relocatable\r\naddresses.\r\nApply Fixups: Modifies the binary's code to reflect the new relocatable addresses. This utilizes the\r\naforementioned lookup table to apply necessary fixups to all instruction displacements that reference\r\nmemory. This ensures that all memory references within the binary correctly point to their intended\r\nlocations.\r\nWe intentionally omit the details of showcasing the rebuilding of the output binary image because, while essential\r\nto the deobfuscation process, it is straightforward enough and just overly tedious to be worthwhile examining in\r\nany depth. Instead, we focus exclusively on relocations, as they are more nuanced and reveal important\r\ncharacteristics that are not as apparent but must be understood when rewriting binaries.\r\nOverview of the Relocation Process\r\nRebuilding relocations is a critical step in restoring a deobfuscated binary to an executable state. This process\r\ninvolves adjusting memory references within the code so that all references point to the correct locations after the\r\ncode has been moved or modified. On the x86-64 architecture, this primarily concerns instructions that use RIP-relative addressing, a mode where memory references are relative to the instruction pointer.\r\nRelocation is necessary when the layout of a binary changes, such as when code is inserted, removed, or shifted\r\nduring deobfuscation. Given our deobfuscation approach extracts the original instructions from the obfuscator, we\r\nare required to relocate each recovered instruction appropriately into a new code segment. This ensures that the\r\ndeobfuscated state preserves the validity of all memory references and that the accuracy of the original control and\r\ndata flow is sustained.\r\nUnderstanding Instruction Relocation\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 60 of 77\n\nInstruction relocation revolves around the following:\r\nInstruction's memory address: the location in memory where an instruction resides.\r\nInstruction's memory memory references: references to memory locations used by the instruction's\r\noperands.\r\nConsider the following two instructions as illustrations:\r\nFigure 56: Illustration of two instructions that require relocation\r\n1. Unconditional jmp instructionThis instruction is located at memory address 0x1000. It references its\r\nbranch target at address 0x4E22 . The displacement encoded within the instruction is 0x3E1D , which is\r\nused to calculate the branch target relative to the instruction's position. Since it employs RIP-relative\r\naddressing, the destination is calculated by adding the displacement to the length of the instruction and its\r\nmemory address.\r\n2. lea instructionThis is the branch target for the jmp instruction located at 0x4E22 . It also contains a\r\nmemory reference to the data segment, with an encoded displacement of 0x157 .\r\nWhen relocating these instructions, we must address both of the following aspects:\r\nChanging the instruction's address: When we move an instruction to a new memory location during the\r\nrelocation process, we inherently change its memory address. For example, if we relocate this instruction\r\nfrom 0x1000 to 0x2000 , the instruction's address becomes 0x2000 .\r\nAdjusting memory displacements: The displacement within the instruction ( 0x3E1D for the jmp ,\r\n0x157 for the lea ) is calculated based on the instruction's original location and the location of its\r\nreference. If the instruction moves, the displacement no longer points to the correct target address.\r\nTherefore, we must recalculate the displacement to reflect the instruction's new position.\r\nFigure 57: Updated illustration demonstration of what relocation would look like\r\nWhen relocating instructions during the deobfuscation process, we must ensure accurate control flow and data\r\naccess. This requires us to adjust both the instruction's memory address and any displacements that reference other\r\nmemory locations. Failing to update these values invalidates the recovered CFG.\r\nWhat Is RIP-Relative Addressing?\r\nRIP-relative addressing is a mode where the instruction references memory at an offset relative to the RIP\r\n(instruction pointer) register, which points to the next instruction to be executed. Instead of using absolute\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 61 of 77\n\naddresses, the instruction encapsulates the referenced address via a signed 32-bit displacement from the current\r\ninstruction pointer.\r\nAddressing relative to the instruction pointer exists on x86 as well, but only for control-transfer instructions that\r\nsupport a relative displacement (e.g., JCC conditional instructions, near CALLs, and near JMPs). The x64 ISA\r\nextended this to account for almost all memory references being RIP-relative. For example, most data references\r\nin x64 Windows binaries are RIP-relative.\r\nAn excellent tool to visualize the intricacies of a decoded Intel x64 instruction is ZydisInfo. Here we use it to\r\nillustrate how a LEA instruction (encoded as 488D151B510600 ) references RIP-relative memory at 0x6511b .\r\nFigure 58: ZydisInfo output for the lea instruction\r\nFor most instructions, the displacement is encoded in the final four bytes of the instruction. When an immediate\r\nvalue is stored at a memory location, the immediate follows the displacement. Immediate values are restricted to a\r\nmaximum of 32 bits, meaning 64-bit immediates cannot be used following a displacement. However, 8-bit and 16-\r\nbit immediate values are supported within this encoding scheme.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 62 of 77\n\nFigure 59: ZydisInfo output for the mov instruction storing an immediate operand\r\nDisplacements for control-transfer instructions are encoded as immediate operands, with the RIP register\r\nimplicitly acting as the base. This is evident when decoding a jnz instruction, where the displacement is directly\r\nembedded within the instruction and calculated relative to the current RIP.\r\nFigure 60: ZydisInfo output for the jnz instruction with an immediate operand as the displacement\r\nSteps in the Relocation Process\r\nFor rebuilding relocations we take the following approach:\r\n1. Rebuilding the code section and creating a relocation mapWith the recovered CFG and imports, we\r\ncommit the changes to a new code section that contains the fully deobfuscated code. We do this by:\r\nFunction-by-function processing: rebuild each function one at a time. This allows us to manage\r\nthe relocation of each instruction within its respective function.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 63 of 77\n\nTracking instruction locations: As we rebuild each function, we track the new memory locations\r\nof each instruction. This involves maintaining a global relocation dictionary that maps original\r\ninstruction addresses to their new addresses in the deobfuscated binary. This dictionary is crucial for\r\naccurately updating references during the fixup phase.\r\n2. Applying fixupsAfter rebuilding the code section and establishing the relocation map, we proceed to\r\nmodify the instructions so that their memory references point to the correct locations in the deobfuscated\r\nbinary. This restores the binary's complete functionality and is achieved by adjusting memory references\r\nto code or data an instruction may have.\r\nRebuilding the Code Section and Creating a Relocation Map\r\nTo construct the new deobfuscated code segment, we iterate over each recovered function and copy all\r\ninstructions sequentially, starting from a fixed offset—for example, 0x1000 . During this process, we build a\r\nglobal relocation dictionary ( global_relocs ) that maps each instruction to its relocated address. This mapping is\r\nessential for adjusting memory references during the fixup phase.\r\nThe global_relocs dictionary uses a tuple as the key for lookups, and each key is associated with the relocated\r\naddress of the instruction it represents. The tuple consists of the following three components:\r\n1. Original starting address of the function: The address where the function begins in the protected binary.\r\nIt identifies the function to which the instruction belongs.\r\n2. Original instruction address within the function: The address of the instruction in the protected binary.\r\nFor the first instruction in a function, this will be the function's starting address.\r\n3. Synthetic boundary JMP flag: A boolean value indicating whether the instruction is a synthetic boundary\r\njump introduced during normalization. These synthetic instructions were not present in the original\r\nobfuscated binary, and we need to account for them specifically during relocation because they have no\r\noriginal address.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 64 of 77\n\nFigure 61: Illustration of how the new code segment and relocation map are generated\r\nThe following Python code implements the logic outlined in Figure 61. Error handling and logging code has been\r\nstripped for brevity.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 65 of 77\n\nFigure 62: Python logic that implements the building of the code segment and generation of the relocation map\r\n1. Initialize current offset\r\nSet the starting point in the new image buffer where the code section will be placed. The variable\r\ncurr_off is initialized to starting_off , which is typically 0x1000 . This represents the conventional\r\nstart address of the .text section in PE files. For SELECTIVE mode, this will be the offset to the start of\r\nthe protected function.\r\n2. Iterate over recovered functions\r\nLoop through each recovered function in the deobfuscated control flow graph ( d.cfg ). func_ea is the\r\noriginal function entry address, and rfn is a RecoveredFunc object encapsulating the recovered\r\nfunction's instructions and metadata.\r\n1. Handle the function start address first\r\n1. Set function's relocated start address: Assign the current offset to rfn.reloc_ea ,\r\nmarking where this function will begin in the new image buffer.\r\n2. Update global relocation map: Add an entry to the global relocation map\r\nd.global_relocs to map the original function address to its new location.\r\n2. Iterate over each recovered instruction\r\nLoop through the normalized flow of instructions within the function. We use the\r\nnormalized_flow as it allows us to iterate over each instruction linearly as we apply it to the new\r\nimage.\r\n1. Set instruction's relocated address: Assign the current offset to r.reloc_ea , indicating\r\nwhere this instruction will reside in the new image buffer.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 66 of 77\n\n2. Update global relocation map: Add an entry to d.global_relocs for the instruction,\r\nmapping its original address to the relocated address.\r\n3. Update the output image: Write the instruction bytes to the new image buffer\r\nd.newimgbuffer at the current offset. If the instruction was modified during deobfuscation\r\n( r.updated_bytes ), use those bytes; otherwise, use the original bytes ( r.instr.bytes ).\r\n4. Advance the offset: Increment curr_off by the size of the instruction to point to the next\r\nfree position in the buffer and move on to the next instruction until the remainder are\r\nexhausted.\r\n3. Align current offset to 16-byte boundaryAfter processing all instructions in a function, align curr_off\r\nto the next 16-byte boundary. We use 8 bytes as an arbitrary pointer-sized value from the last instruction to\r\npad so that the next function won't conflict with the last instruction of the previous function. This further\r\nensures proper memory alignment for the next function, which is essential for performance and correctness\r\non x86-64 architectures. Then repeat the process from step 2 until all functions have been exhausted.\r\nThis step-by-step process accurately rebuilds the deobfuscated binary's executable code section. By relocating\r\neach instruction, the code prepares the output template for the subsequent fixup phase, where references are\r\nadjusted to point to their correct locations.\r\nApplying Fixups\r\nAfter building the deobfuscated code section and relocating each recovered function in full, we apply fixups to\r\ncorrect addresses within the recovered code. This process adjusts the instruction bytes in the new output image so\r\nthat all references point to the correct locations. It is the final step in reconstructing a functional deobfuscated\r\nbinary.\r\nWe categorize fixups into three distinct categories, based primarily on whether they apply to control flow or data\r\nflow instructions. We further distinguish between two types of control flow instructions: standard branching\r\ninstructions and those introduced by the obfuscator through the import protection. Each type has specific nuances\r\nthat require tailored handling, allowing us to apply precise logic to each category.\r\n1. Import Relocations: These involve calls and jumps to recovered imports.\r\n2. Control Flow Relocations: All standard control flow branching branching instructions.\r\n3. Data Flow Relocations: Instructions that reference static memory locations.\r\nUsing these three categorizations, the core logic boils down to the following two phases:\r\n1. Resolving displacement fixups\r\nDifferentiate between displacements encoded as immediate operands (branching instructions) and\r\nthose in memory operands (data accesses and import calls).\r\nCalculate the correct fixup values for these displacements using the d . global_relocs map\r\ngenerated prior.\r\n2. Update the output image buffer\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 67 of 77\n\nOnce the displacements have been resolved, write the updated instruction bytes into the new code\r\nsegment to reflect the changes permanently.\r\nTo achieve this, we utilize several helper functions and lambda expressions. The following is a step-by-step\r\nexplanation of the code responsible for calculating the fixups and updating the instruction bytes.\r\nFigure 63: Helper routines that aid in applying fixups\r\nDefine lambda helper expressions\r\nPACK_FIXUP : packs a 32-bit fixup value into a little-endian byte array.\r\nCALC_FIXUP : calculates the fixup value by computing the difference between the destination\r\naddress ( dest ) and the end of the current instruction ( r.reloc_ea + size ), ensuring it fits within\r\n32 bits.\r\nIS_IN_DATA : checks if a given address is within the data section of the binary. We exclude\r\nrelocating these addresses, as we preserve the data section at its original location.\r\nResolve fixups for each instruction\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 68 of 77\n\nImport and data flow relocations\r\nUtilize the resolve_disp_fixup_and_apply helper function as both encode the\r\ndisplacement within a memory operand.\r\nControl flow relocations\r\nUse the resolve_imm_fixup_and_apply helper as the displacement is encoded in an\r\nimmediate operand.\r\nDuring our CFG recovery, we transformed each jmp and jcc instruction to its near jump\r\nequivalent (from 2 bytes to 6 bytes) to avoid the shortcomings of 1-byte short branches.\r\nWe force a 32-bit displacement for each branch to guarantee a sufficient range for\r\nevery fixup.\r\nUpdate the output image buffer\r\nDecode the updated instruction bytes to have it reflect within the RecoveredInstr that represents\r\nit.\r\nWrite the updated bytes to the new image buffer\r\nupdated_bytes reflects the final opcodes for a fully relocated instruction.\r\nWith the helpers in place, the following Python code implements the final processing for each relocation type.\r\nFigure 64: The three core loops that address each relocation category\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 69 of 77\n\nImport Relocations: The first for loop handles fixups for import relocations, utilizing data generated\r\nduring the Import Recovery phase. It iterates over every recovered instruction r within the\r\nrfn.relocs_imports cache and does the following:\r\nPrepare updated instruction bytes: initialize r.updated_bytes with a mutable copy of the\r\noriginal instruction bytes to prepare it for modification.\r\nRetrieve import entry and displacement: obtain the import entry from the imports dictionary\r\nd.imports and retrieve the new RVA from d.import_to_rva_map using the import's API name.\r\nApply fixup: use the resolve_disp_fixup_and_apply helper to calculate and apply the fixup for\r\nthe new RVA. This adjusts the instruction's displacement to correctly reference the imported\r\nfunction.\r\nUpdate image buffer: write r.updated_bytes back into the new image using\r\nupdate_reloc_in_img . This finalizes the fixup for the instruction in the output image.\r\nControl Flow Relocations: The second for loop handles fixups for control flow branching relocations\r\n( call , jmp , jcc ). Iterating over each entry in rfn.relocs_ctrlflow , it does the following:\r\nRetrieve destination: extract the original branch destination target from the immediate operand.\r\nGet relocated address: reference the relocation dictionary d.global_relocs to obtain the branch\r\ntarget's relocated address. If it's a call target, then we specifically look up the relocated address for\r\nthe start of the called function.\r\nApply fixup: use resolve_imm_fixup_and_apply to adjust the branch target to its relocated\r\naddress.\r\nUpdate buffer: finalize the fixup by writing r.updated_bytes back into the new image using\r\nupdate_reloc_in_img .\r\nData Flow Relocations: The final loop handles the resolution of all static memory references stored within\r\nrfn.relocs_dataflow . First, we establish a list of KNOWN instructions that require data reference\r\nrelocations. Given the extensive variety of such instructions, this categorization simplifies our approach\r\nand ensures a comprehensive understanding of all possible instructions present in the protected binaries.\r\nFollowing this, the logic mirrors that of the import and control flow relocations, systematically processing\r\neach relevant instruction to accurately adjust their memory references. \r\nAfter reconstructing the code section and establishing the relocation map, we proceeded to adjust each instruction\r\ncategorized for relocation within the deobfuscated binary. This was the final step in restoring the output binary's\r\nfull functionality, as it ensures that each instruction accurately references the intended code or data segments.\r\nObserving the Results\r\nTo demonstrate our deobfuscation library for ScatterBrain, we conduct a test study showcasing its functionality.\r\nFor this test study, we select three samples: a POISONPLUG.SHADOW headerless backdoor and two embedded\r\nplugins.\r\nWe develop a Python script, example_deobfuscator.py , that consumes from our library and implements all of\r\nthe recovery techniques outlined earlier. Figure 65 and Figure 66 showcase the code within our example\r\ndeobfuscator:\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 70 of 77\n\nFigure 65: The first half of the Python code in example_deobfuscator.py\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 71 of 77\n\nFigure 66: The second half of the Python code in example_deobfuscator.py\r\nRunning example_deobfuscator.py we can see the following. Note, it takes a bit given we have to emulate more\r\nthan 16,000 instruction dispatchers that were found within the headerless backdoor.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 72 of 77\n\nFigure 67: The three core loops that address each relocation category\r\nFocusing on the headerless backdoor both for brevity and also because it is the most involved in deobfuscating,\r\nwe first observe its initial state inside the IDA Pro disassembler before we inspect the output results from our\r\ndeobfuscator. We can see that it is virtually impenetrable to analysis.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 73 of 77\n\nFigure 68: Observing the obfuscated headerless backdoor in IDA Pro\r\nAfter running our example deobfuscator and producing a brand new deobfuscated binary, we can see the drastic\r\ndifference in output. All the original control flow has been recovered, all of the protected imports have been\r\nrestored, and all required relocations have been applied. We also account for the deliberately removed PE header\r\nof the headerless backdoor that ScatterBrain removes.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 74 of 77\n\nFigure 69: Observing the deobfuscated headerless backdoor in IDA Pro\r\nGiven we produce functional binaries as part of the output, the subsequent deobfuscated binary can be either run\r\ndirectly or debugged within your favorite debugger of choice.\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 75 of 77\n\nFigure 70: Debugging the deobfuscated headerless backdoor in everyone’s favorite debugger\r\nConclusion\r\nIn this blog post, we delved into the sophisticated ScatterBrain obfuscator used by POISONPLUG.SHADOW, an\r\nadvanced modular backdoor leveraged by specific China-nexus threat actors GTIG has been tracking since 2022.\r\nOur exploration of ScatterBrain highlighted the intricate challenges it poses for defenders. By systematically\r\noutlining and addressing each protection mechanism, we demonstrated the significant effort required to create an\r\neffective deobfuscation solution.\r\nUltimately, we hope that our work provides valuable insights and practical tools for analysts and cybersecurity\r\nprofessionals. Our dedication to advancing methodologies and fostering collaborative innovation ensures that we\r\nremain at the forefront of combating sophisticated threats like POISONPLUG.SHADOW. Through this exhaustive\r\nexamination and the introduction of our deobfuscator, we contribute to the ongoing efforts to mitigate the risks\r\nposed by highly obfuscated malware, reinforcing the resilience of cybersecurity defenses against evolving\r\nadversarial tactics.\r\nIndicators of Compromise\r\nA Google Threat Intelligence Collection featuring indicators of compromise (IOCs) related to the activity\r\ndescribed in this post is now available.\r\nHost-Based IOCs\r\nMD5 Associated Malware Family\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 76 of 77\n\n5C62CDF97B2CAA60448619E36A5EB0B6 POISONPLUG.SHADOW\r\n0009F4B9972660EEB23FF3A9DCCD8D86 POISONPLUG.SHADOW\r\nEB42EF53761B118EFBC75C4D70906FE4 POISONPLUG.SHADOW\r\n4BF608E852CB279E61136A895A6912A9 POISONPLUG.SHADOW\r\n1F1361A67CE4396C3B9DBC198207EF52 POISONPLUG.SHADOW\r\n79313BE39679F84F4FCB151A3394B8B3 POISONPLUG.SHADOW\r\n704FB67DFFE4D1DCE8F22E56096893BE POISONPLUG.SHADOW\r\nAcknowledgements\r\nSpecial thanks to Conor Quigley and Luke Jenkins from the Google Threat Intelligence Group for their\r\ncontributions to both Mandiant and Google’s efforts in understanding and combating the POISONPLUG threat.\r\nWe also appreciate the ongoing support and dedication of the teams at Google, whose combined efforts have been\r\ncrucial in enhancing our cybersecurity defenses against sophisticated adversaries.\r\nPosted in\r\nThreat Intelligence\r\nSource: https://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nhttps://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator\r\nPage 77 of 77\n\npseudo-random following formula: key stream, which is then used in a XOR-based stream cipher for decryption. It operates on the\nXn + 1 = (a • Xn + c) mod 2 32  \n   Page 17 of 77\n\n https://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator   \nshowcase their structure after a successful recovery: \nFigure 53: Output of the d.imports dictionary after a successful recovery\n   Page 57 of 77",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://cloud.google.com/blog/topics/threat-intelligence/scatterbrain-unmasking-poisonplug-obfuscator"
	],
	"report_names": [
		"scatterbrain-unmasking-poisonplug-obfuscator"
	],
	"threat_actors": [
		{
			"id": "4d5f939b-aea9-4a0e-8bff-003079a261ea",
			"created_at": "2023-01-06T13:46:39.04841Z",
			"updated_at": "2026-04-10T02:00:03.196806Z",
			"deleted_at": null,
			"main_name": "APT41",
			"aliases": [
				"WICKED PANDA",
				"BRONZE EXPORT",
				"Brass Typhoon",
				"TG-2633",
				"Leopard Typhoon",
				"G0096",
				"Grayfly",
				"BARIUM",
				"BRONZE ATLAS",
				"Red Kelpie",
				"G0044",
				"Earth Baku",
				"TA415",
				"WICKED SPIDER",
				"HOODOO",
				"Winnti",
				"Double Dragon"
			],
			"source_name": "MISPGALAXY:APT41",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "e698860d-57e8-4780-b7c3-41e5a8314ec0",
			"created_at": "2022-10-25T15:50:23.287929Z",
			"updated_at": "2026-04-10T02:00:05.329769Z",
			"deleted_at": null,
			"main_name": "APT41",
			"aliases": [
				"APT41",
				"Wicked Panda",
				"Brass Typhoon",
				"BARIUM"
			],
			"source_name": "MITRE:APT41",
			"tools": [
				"ASPXSpy",
				"BITSAdmin",
				"PlugX",
				"Impacket",
				"gh0st RAT",
				"netstat",
				"PowerSploit",
				"ZxShell",
				"KEYPLUG",
				"LightSpy",
				"ipconfig",
				"sqlmap",
				"China Chopper",
				"ShadowPad",
				"MESSAGETAP",
				"Mimikatz",
				"certutil",
				"njRAT",
				"Cobalt Strike",
				"pwdump",
				"BLACKCOFFEE",
				"MOPSLED",
				"ROCKBOOT",
				"dsquery",
				"Winnti for Linux",
				"DUSTTRAP",
				"Derusbi",
				"ftp"
			],
			"source_id": "MITRE",
			"reports": null
		},
		{
			"id": "2a24d664-6a72-4b4c-9f54-1553b64c453c",
			"created_at": "2025-08-07T02:03:24.553048Z",
			"updated_at": "2026-04-10T02:00:03.787296Z",
			"deleted_at": null,
			"main_name": "BRONZE ATLAS",
			"aliases": [
				"APT41 ",
				"BARIUM ",
				"Blackfly ",
				"Brass Typhoon",
				"CTG-2633",
				"Earth Baku ",
				"GREF",
				"Group 72 ",
				"Red Kelpie ",
				"TA415 ",
				"TG-2633 ",
				"Wicked Panda ",
				"Winnti"
			],
			"source_name": "Secureworks:BRONZE ATLAS",
			"tools": [
				"Acehash",
				"CCleaner v5.33 backdoor",
				"ChinaChopper",
				"Cobalt Strike",
				"DUSTPAN",
				"Dicey MSDN",
				"Dodgebox",
				"ForkPlayground",
				"HUC Proxy Malware (Htran)"
			],
			"source_id": "Secureworks",
			"reports": null
		}
	],
	"ts_created_at": 1775434613,
	"ts_updated_at": 1775826763,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/d56881263ebd54121556ce60a18451deda0df261.pdf",
		"text": "https://archive.orkl.eu/d56881263ebd54121556ce60a18451deda0df261.txt",
		"img": "https://archive.orkl.eu/d56881263ebd54121556ce60a18451deda0df261.jpg"
	}
}