{
	"id": "884e6a8f-e2c5-4ac6-b08a-109e833977f8",
	"created_at": "2026-04-06T01:29:53.150196Z",
	"updated_at": "2026-04-10T13:12:16.292853Z",
	"deleted_at": null,
	"sha1_hash": "74694452b2a096a961b529580fd6d82e762c8a12",
	"title": "Unpacking the packer ‘pkr_mtsi’",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 124183,
	"plain_text": "Unpacking the packer ‘pkr_mtsi’\r\nBy Robert SimmonsRobert Simmons\r\nPublished: 2026-01-06 · Archived: 2026-04-06 00:49:16 UTC\r\nThis blog post presents an in-depth technical analysis of pkr_mtsi, a malicious Windows packer first observed in\r\nthe wild on April 24, 2025, and continuously deployed through the time of writing. The packer is actively\r\nleveraged in large-scale malvertising and SEO-poisoning campaigns to distribute trojanized installers for\r\nlegitimate software, enabling initial access and flexible delivery of follow-on payloads. In observed campaigns,\r\npkr_mtsi has been used to deliver a diverse set of malware families, including Oyster, Vidar, Vanguard Stealer,\r\nSupper, and more, underscoring its role as a general-purpose loader rather than a single-payload wrapper. \r\nThis analysis highlights pkr_mtsi’s evolution across campaigns over the past eight months, including the use of\r\nincreasingly sophisticated obfuscation, anti-analysis techniques, and evasive API resolution strategies. Despite this\r\nevolution, the pkr_mtsi packer retains consistent structural and behavioral characteristics that enable reliable\r\nidentification, behavioral detection, and signature development when analyzed holistically. This report discusses\r\nthose characteristics — and provides a YARA rule to detect all identified versions of the packer. \r\nIdentification Strategies\r\nThe pkr_mtsi packer is commonly distributed under the guise of a legitimate software installer. It has been\r\nobserved masquerading as installers for widely used utilities such as PuTTY, Rufus, and Microsoft Teams, among\r\nothers. Distribution of the malware is not the byproduct of supply chain compromises of the legitimate vendors.\r\nRather, it is typically facilitated by fake software download websites that pose as legitimate sources and achieve\r\nprominent placement in search engine results via malvertising campaigns and SEO poisoning techniques.\r\nAntivirus detections of pkr_mtsi (that are not purely generic) frequently include the substrings \"oyster\" or\r\n\"shellcoderunner.\" In addition, a single public YARA rule exists that identifies a limited subset of pkr_mtsi\r\nsamples under the name \"TextShell.\" However, this rule does not comprehensively cover all observed variants. To\r\naddress this gap, a more complete YARA rule that matches all identified samples of this packer is provided in\r\nAppendix A. Results of a recent retro hunt in Spectra Analyze using this rule are shown below (Figure 1).\r\nFigure 1: YARA hunting results showing “oyster” and “shellcoderunner” common threat types.\r\nCore Technical Features\r\nSamples attributable to pkr_mtsi, share a consistent set of core technical features. The first non-library function\r\ninvoked by main always allocates a region of memory into which the next execution stage is written. Earlier\r\nvariants perform this allocation via a direct call to VirtualAlloc, while more recent variants employ an obfuscated\r\ncall to ZwAllocateVirtualMemory.\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 1 of 13\n\nFollowing memory allocation, execution proceeds through a sequence of functions responsible for reconstructing\r\nthe next-stage payload. The payload is divided into chunks ranging from one to eight bytes, stored as immediate\r\nvalues. In later variants, the chunks are passed through a decoding routine. And in all variants, written to specific\r\noffsets within the allocated memory region. The unusually large number of these small \"loader\" instructions is a\r\nstrong indicator of this packer’s presence.\r\nEarly variants of pkr_mtsi that we analyzed resolved DLLs and API functions from plaintext strings. Later\r\nvariants have shifted to resolving both DLLs and APIs via hashed identifiers combined with Process Environment\r\nBlock (PEB) traversal. A further distinguishing feature across variants is the pervasive use of junk calls to GDI\r\nAPI functions, which serve no functional purpose and are intended to frustrate static and behavioral analysis.\r\nTogether, these characteristics form the foundation  for reliable identification and are captured in the detection\r\nlogic of the YARA rule included at the end of this report (Appendix A). \r\nIdentifying Characteristics\r\nThe pkr_mtsi packer has been observed in both executable (EXE) and dynamic-link library (DLL) forms. While\r\nthe overall unpacking logic is shared between these formats, the DLL variants support multiple execution\r\ncontexts. One execution path reliably triggers on DLL load and is responsible for unpacking the next stage and\r\nfinal payload. Additional execution paths may be invoked during DLL unload events, allowing alternative entry\r\npoints into the unpacked payload to be executed.\r\nIn several DLL samples, the packer exports DllRegisterServer, enabling the malware to be loaded via\r\nregsvr32.exe. This provides a convenient mechanism for persistence through registry-based COM registration\r\nwhile leveraging a trusted Windows utility for execution.\r\nThe intermediate stage produced by the packer is a modified UPX-packed module. Recent versions of UPX are\r\nused, but with selective removal of identifying components to evade detection. Across observed samples, portions\r\nof the UPX structure, including headers, magic values, and ancillary metadata, are stripped as long as execution\r\nremains viable. This deliberate degradation of the UPX module complicates both static identification and\r\nautomated unpacking.\r\nIn earlier samples, the image bases used were MSVC defaults for PE32+ EXEs: 0x140000000 and DLLs:\r\n0x180000000. And in later samples, these image bases changed to non-standard, very high numbers such as\r\n0x7ff662c10000. The precise motivation for this change is unclear. One plausible explanation is that it is an\r\nattempt to resemble a system DLL or an ASLR-relocated image even when relocation support is disabled.\r\nBehavioral and Code Analysis Findings\r\nThe pkr_mtsi packer exhibits a clear evolutionary trajectory across campaigns. Earlier variants preserve more\r\nrecognizable UPX artifacts and rely on simpler API resolution mechanisms, while later variants remove detectable\r\nfeatures and introduce obfuscation layers. These changes indicate an adversary that is actively iterating on the\r\npacker to counter detection and analysis.\r\nFor example, in earlier pkr_mtsi samples, execution begins with a distinctive sequence of adversary-controlled\r\nfunction calls invoked in rapid succession. The first of these functions allocates memory that will hold the\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 2 of 13\n\nunpacked next-stage payload. Subsequent functions reconstruct the payload by writing small fragments of data\r\ninto the allocated region. The total number of these payload-writing functions varies according to the size of the\r\nembedded payload, but their repetitive structure and volume form a recognizable behavioral pattern.\r\nThis execution pattern results in a dense cluster of functions whose sole purpose is to write chunks of data which\r\nrange in size from one to eight bytes into memory. These chunks are embedded as immediate values within the\r\ninstruction stream and may be passed through lightweight decoding routines before being written. The high\r\nfrequency of such write operations, combined with the absence of meaningful control flow or computation within\r\nthese functions, distinguishes this packer from more conventional loaders.\r\nFrom a behavioral perspective, the packer’s early-stage activity is dominated by memory allocation followed by\r\nintensive memory writes to a single contiguous region. This activity occurs prior to any meaningful interaction\r\nwith the unpacked payload logic. Later variants preserve this overall execution model but introduce additional\r\nobfuscation layers intended to frustrate static analysis. Despite these changes, the fundamental behavior of early\r\nallocation followed by staged payload reconstruction remains consistent across samples and campaigns.\r\nFigure 2: first set of functions in main in older vs recent samples of pkr_mtsi.\r\nAnti-Debugging Features\r\nRL’s analysis found that the pkr_mtsi packer employs a number of different anti-analysis features which are\r\nhighlighted in various locations in the report below according to where the feature is found in the samples.\r\nHowever, anti-debugging is used throughout many variants of this packer and those anti-debugging techniques are\r\ndescribed here. Two debugger detection API calls are utilized: IsDebuggerPresent and\r\nCheckRemoteDebuggerPresent. Many instances of these function calls simply cause the process to exit. However,\r\nsome instances use a different type of trap after the call that leads to an infinite loop with a jump instruction that\r\nleads to itself. This pattern (Figure 3) is easily detected using YARA. \r\nFigure 3: Anti-Debugging trap path to infinite loop shown with red arrow.\r\nMemory Allocation Functions\r\nThe allocate_memory function in older samples contained an indirect call to VirtualAlloc and was not obfuscated.\r\nThe exact bytes of this function, other than relative displacements and input parameter instruction order, are stable\r\nover all observed variants of the packer that call VirtualAlloc in the allocate_memory function. These byte patterns\r\nare used for identification of the variants that contain them. All function names for adversary functions herein are\r\nassigned during research and are not necessarily the adversary's name for the functions in their source code.\r\nFigure 4: Function to allocate memory for next stage that utilizes VirtualAlloc.\r\nMore recent campaigns reveal changes to the allocate_memory function. Specifically, they  use an obfuscated call\r\nto ZwAllocateVirtualMemory rather than VirtualAlloc. The transition to this obfuscated function was observed in\r\nsamples starting in August 2025. The constant values used as input parameters to ZwAllocateVirtualMemory are\r\nstable but appear in different orders and interspersed with differing numbers of junk instructions depending on the\r\nbuild. These features are randomized by the packer build process.\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 3 of 13\n\nFigure 5: Obfuscated call to ZwAllocateVirtualMemory in a more recent sample of pkr_mtsi.\r\nLoading The NTDLL Handle\r\nThe pkr_mtsi packer utilizes three general methods for getting a handle for ntdll.dll for later use resolving API\r\nfunctions. The method observed in the earliest samples is the most straightforward, loading a string containing the\r\nname of the DLL as an input parameter for a call to LoadLibraryA. The first evolution after that replaced the call\r\nto LoadLibraryA with a custom resolver function (Figure 6). The return value of the function is a pointer to\r\nntdll.dll in rax. That pointer is then stored in a global variable for use by other functions later.\r\nFigure 6: DLL handle resolver function call with return value stored in global: ntdll_handle.\r\nThe first step in the custom resolver function is to load the pointer to the Process Environment Block (PEB) from\r\nthe Thread Environment Block (TEB) located at offset 0x60 in the gs segment. The offset 0x60 is observed in two\r\nforms, one obfuscated and one not. The obfuscated variant uses a calculation combined with a series of\r\ninstructions that is similar to stack strings. However, some of the stack values are written and then immediately\r\nstomped on with new values over and over. The last value in the series of stomps is the one used in the subsequent\r\ncalculation. Figure 7 shows the algorithm input values (highlighted in green and marked with No. 1); the series of\r\nbytes that are stomped on one-after-another until the last one which is used in the calculation of the offset in gs\r\n(green arrow); two inputs to the calculation (highlighted in yellow and marked with No. 2) where they are moved\r\nfrom the stack to two registers; the subtraction calculation and the result (highlighted in red and marked with No.\r\n3); and the instruction where the pointer to the PEB is loaded from the gs segment (highlighted in blue and\r\nmarked No. 4).\r\nFigure 7: Obfuscated offset in gs segment using stack string stomping showing algorithm input values (1),\r\ninputs to the calculation (2), subtraction calculation and result (3), instruction where the pointer to the PEB is\r\nloaded from the gs segment (4).\r\nFrom the PEB, the InMemoryOrderModuleList is walked, comparing each module name in the list to the name of\r\nthe needed DLL, in this case ntdll.dll. This is done in two different ways in different samples: either a direct string\r\ncomparison or checking against a hash of the DLL name (Figure 8).\r\nThe main steps in this process are:\r\n1. Walking the linked list.\r\n2. Copying the DLL name to a location on the stack \r\n3. Calculating the hash of the DLL name (optional)\r\n4. Comparing the result to the hash that was the function input parameter (optional)\r\nIn some samples, the second and third steps are separated out into their own dedicated subroutines called from this\r\nfunction. In the step that copies the DLL name to the stack, any non-ASCII character is replaced with a question\r\nmark character. This is a feature that makes this algorithm detectable by leaving a 0x3f immediate value in the\r\ndisassembly that can be used to detect the function containing this step. \r\nAdditionally, there is a check for the length of the module name at 64 characters. This is significant in that it is an\r\nanti-analysis trick to catch filenames in a malware sandbox that are the SHA256 of the submitted file. If the\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 4 of 13\n\nfilename is a SHA256, the packer exits. This is interesting because this anti-analysis check works for EXEs, but\r\nnot always for DLLs — making it a strange choice for a malware feature that is found in both DLLs and EXEs.\r\nThe reason it doesn't always work for DLLs is the DLL's own filename is not located early in the linked list.\r\nRather, it is the module name of the EXE that loaded the DLL. This filename may not be a SHA256. It is more\r\nlikely to be rundll32.exe or regsvr32.exe.\r\nFigure 8: DLL handle resolver algorithm steps.\r\nResolving API Function Pointers\r\nThere are two general locations in pkr_mtsi where function pointers are resolved from either function name strings\r\nor from hashes. The first is in the allocate_memory function following the resolve_ntdll_handle function call\r\ndescribed above. The handle to that DLL is stored in a global data variable for later use in this function as well as\r\nelsewhere. All the steps in this function set up an obfuscated call to ZwAllocateVirtualMemory which allocates\r\nmemory where subsequent functions will write the next stage. In some samples, the size of the memory to allocate\r\nis located in one value, but in later samples, the value is calculated from a number of different instructions (Figure\r\n9). This is an anti-analysis feature to make static analysis more difficult. This specifically makes automated\r\nunpacking more challenging when the size of the next stage cannot be lifted directly from a single location in the\r\nbinary.\r\nFigure 9: Instructions used to spread the payload size across multiple locations as anti-analysis.\r\nAfter the handle to NTDLL has been resolved, the first two bytes of that location are checked for the DOS Header\r\nmagic number, MZ. If that is found, it parses the rest of the headers to locate the address of the export directory.\r\nThen in the export directory, it loops through each export and finds the name of the exported function. That string\r\nof characters is run through a hash algorithm. This algorithm in some samples is located in its own subroutine and\r\nin others (Figure 10) it is inlined in the allocate_memory or resolve_api_hash functions.\r\nThe particular variant of the hash algorithm shown in Figure 10 multiplies by 257 (0x101) with a signed add. This\r\nalgorithm is similar to hash algorithms listed in HashDB, but this exact one is not in that database. Across many\r\nsamples of pkr_mtsi, the hashing algorithm in any particular sample is the same general algorithm, but many have\r\ndifferent constants other than 257. A YARA rule for detecting this algorithm in any malware sample, not just this\r\npacker, is provided at the end of the blog. Note: this particular YARA rule should not be used to determine\r\nmaliciousness of a sample. It simply identifies the presence of this hashing algorithm.\r\nIf the calculated hash matches the hard-coded hash, in this case for ZwAllocateVirtualMemory, then the pointer to\r\nthat function is called in an obfuscated way. The input parameters for this call are in different orders in different\r\nsamples and sometimes are interleaved with junk API calls. Both of those are anti-analysis tricks to make writing\r\na signature more difficult. All of the steps described above are shown in the next figure.\r\nFigure 10: Get a function pointer in NTDLL based on a hash of the function name.\r\nIn addition to inline capabilities like shown above, a dedicated function is also sometimes used to resolve API\r\nfunctions from hashes. This function is structured similarly to the one above, but it is used to resolve arbitrary API\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 5 of 13\n\nfunction hashes, not just ones from NTDLL that are hard-coded. Again, in some samples the hash calculation is\r\nlocated in a dedicated subroutine. And in other samples, the hash calculation is inline (Figure 11).\r\nFigure 11: Function to resolve API hash to function pointer.\r\nLoading Other DLL Handles\r\nA separate function for resolving DLL handles is used on all DLLs other than ntdll.dll.  This resolver function uses\r\na numeric, hard-coded key along with pointers to the API functions RtlInitUnicodeString, LdrGetDllHandle, and\r\nLdrLoadDll as input parameters. The difference between a DLL hash and a numeric key is that the key does not\r\nuse a hashing function inside this type of resolver function. There is only a series of conditional comparisons to\r\nhard-coded keys, each of which corresponds to a code block containing the name of the desired DLL obfuscated in\r\na stack string. The number of possible DLL names corresponds to the number of stack string blocks and DLL keys\r\nin the resolver function. The more DLL handles that this particular payload requires, the longer the resolver\r\nfunction is. Figure 12 shows the input DLL key in register ebx being compared to each of the DLL keys in a\r\nseries. If the hard-coded value is not equal, it jumps to the next comparison. If it is equal, it jumps to the start of\r\nthe stack string containing the DLL name.\r\nFigure 12: Comparing the DLL key to each hard-coded value.\r\nEach block of instructions containing the obfuscated DLL name starts with a partial stack string with some\r\nlocations being stomped similar to the gs offset noted earlier.\r\nFigure 13: Partial stack string with some location stomping.\r\nThe remainder of the block performs a series of calculations to decode obfuscated bytes and then write the\r\ncharacters to the appropriate offset in the DLL name string. In Figure 14, an example of this is bordered in red (1).\r\nThis area also contains instructions that load the pointers to APIs called later in the function to the registers they\r\nare called from(2).\r\nFigure 14: Writing decoded characters to DLL name and preparing API function calls.\r\nThe obfuscated stack strings concealing the DLL names can be observed in a debugger. However, since they are\r\ncontiguous code blocks, they also lend themselves to the faster way of simply dumping the instructions and\r\nemulating them using Binary Refinery's vstack unit. The command for this emulation is the following:\r\nemit dump.dat | vstack -a x64 -p 1: -n 1: -I -M -v -w 500\r\nFigure 15: DLL names in output from Binary Refinery's vstack unit.\r\nThe DLLs used in this particular sample are ADVAPI32.dll, KERNEL32.DLL, msvcrt.dll, NETAPI32.dll, and\r\nWS2_32.dll (Figure 15). These DLL names are ASCII, but the API functions to load the DLL's handle take a\r\nunicode string structure as input. Therefore, an empty struct is initialized using RtlInitUnicodeString which is then\r\npopulated by a wide string version of the DLL name. The ASCII string is converted to the wide string using SIMD\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 6 of 13\n\ndata rearrangement instructions (Figure 16). This code block makes a very good byte pattern for detection in\r\nYARA and is used as the basis for the pkr_mtsi_UnpackMakeWide_1 rule provided in Appendix A. \r\nFigure 16: SIMD instructions block converting ASCII DLL name to a wide string.\r\nLastly, in this resolver function, an attempt is made to get the DLL handle for an already loaded DLL. If that fails,\r\nthe DLL is loaded outright. Finally, the handle is the return value of the function (Figure 17).\r\nFigure 17: Attempt to get DLL handle then load on failure.\r\nUPX TLS Callback Fixups\r\nThe EXE variants of pkr_mtsi are straightforward and pass execution to the UPX unpacker stub directly once. The\r\nDLLs, however, can have more than one pathway to execute the next stage. The main function always unpacks the\r\nnext stage module into newly allocated memory and then makes any necessary adjustments such as TLS\r\ncallbacks. A series of fixups which add the base address of the allocated memory to the relative offsets from the\r\nUPX module and then write the resulting virtual addresses to the module are shown in the next figure.\r\nFigure 18: TLS callback fixups to adjust them to the actual allocated memory base address.\r\nFortunately, UPX is open source, so one can consult the commented code on Github and figure out exactly what is\r\nlocated where these fixups occur (Figure 20). That set of fixups write virtual addresses in the UPX TLS directory\r\nas well as making a callback array that points to the PETLSC2 function in the UPX stub (Figure 19). \r\nFigure 19: TLS callback structs in UPX module.\r\nFigure 20: TLS callback support in UPX source code.\r\nUPX Import Fixups\r\nIn the main function, the DLL resolvers and the API function resolvers work in series. The first few API function\r\nhashes are resolved from NTDLL. That handle is loaded from the global data variable written by the\r\nallocate_memory function. From that DLL, three function hashes are resolved to the three functions used in the\r\ngeneric DLL resolver function. Those three hashes are LdrGetDllHandle, RtlInitUnicodeString, and LdrLoadDll.\r\nThe DLL handles from the generic resolvers are then used to resolve API function hashes. The resulting function\r\npointers are written to the next stage UPX module's imports. This series of actions is in lieu of UPX being able to\r\nperform its own import resolutions because it is not being loaded by a standard loading process. Figure 21 shows a\r\nsnippet that includes all three kinds of functions working in a series.\r\nFigure 21: API and DLL hash resolvers writing import to the next stage UPX module.\r\nPage Protection Mistakes\r\nThe final action the packer takes before calling the UPX entry point in the next stage is to attempt to change the\r\nmemory page protections on sections in the allocated memory where the payload has been written and adjusted.\r\nHowever, there is a fortuitous bug in the adversary's code here. There are three calls to NtProtectVirtualMemory\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 7 of 13\n\nused in this packer. This function uses the same protection constants as VirtualProtect. Look carefully at the\r\nconstants marked in red in the next figure. The 0x6 in all three is 0x02 | 0x4 which are PAGE_READONLY and\r\nPAGE_READWRITE. This results in an invalid page protection input value for the function.\r\nFigure 22: Invalid page protection input values to NtProtectVirtualMemory.\r\nThis programming flaw provides a detection opportunity. These three calls will generate three errors in a row that\r\ncan be used to develop behavioral detections in EDR telemetry. The error return value of C0000045\r\nSTATUS_INVALID_PAGE_PROTECTION is shown in the debugger in Figure 23.\r\nFigure 23: Invalid page protection errors in the debugger.\r\nUPX Execution From Main\r\nIn the DLL variants, when the main function of the packer is run, the Windows x64 loader parameter fdwReason is\r\nused by the packer to make a decision as to whether or not to call the UPX unpacker stub (Figure 24). This is\r\nprobably to prevent a second unnecessary unpacking process if main is triggered by DLL load and unload events\r\nthat are not specifically \"1\" (DLL_PROCESS_ATTACH).\r\nFigure 24: Conditional call to the next stage UPX module entry point at the unpacker stub.\r\nNote the three input parameters in the call to the next stage. The data from those parameters is passed all the way\r\nthrough the UPX unpacker stub into the original entry point of the payload. Figures 25, 26 and 27 show the\r\nunpacker stub function prologue and epilogue as well as the prologue of the payload showing where these three\r\ndata values are passed in through.\r\nFigure 25: Unpacker UPX stub prologue.\r\nFigure 26: Unpacker UPX stub epilogue.\r\nFigure 27: Payload OEP prologue.\r\nUPX Execution From DLL Export\r\nAlso, the packer's own DLL export appears to pass four values to the function that it calls in the payload (Figure\r\n28).\r\nFigure 28: Four parameters passed to payload function of same name as packer's export.\r\nWhat's interesting about these four values is that they are clobbered immediately in the payload function that is\r\ncalled (Figure 29). This indicates that this packer is coded to deliver a variety of payloads, and pass data to them\r\non execution. But this particular payload does not have that capability.\r\nFigure 29: Clobbered import values in the payload's exported function.\r\nNext-Stage UPX Static Analysis\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 8 of 13\n\nThe second stage of pkr_mtsi is a UPX module that has been modified to remove parts that could be detected from\r\nthe outer layer of the first stage. The second stage is broken up into small one to eight byte chunks that are then\r\ndispersed across the write_payloadN functions. \r\nIn earlier builds of this packer, there was no decoding process for any of these payload chunks. So, predictable\r\nbyte patterns from the second stage would \"shine through\" the outer packer (Figure 30).\r\nFigure 30: Chunks of plain ASCII from the second stage UPX module.\r\nMany samples have the entire DOS and PE headers of the UPX module missing. Later variants removed the UPX\r\nmagic number and headers located at the start of the UPX1 section (Figure 31).\r\nFigure 31: Sample to the right has UPX magic number and other headers removed.\r\nAnd even newer samples have text resources deleted. It seems like this adversary is removing parts of the UPX\r\nmodule to prevent detection and will remove anything as long as the module will still execute and load the\r\npayload final stage.\r\nConclusion\r\nThis analysis shows that, despite ongoing adversary iteration, pkr_mtsi exposes multiple durable detection and\r\nresponse opportunities that defenders can operationalize immediately. Preventive controls should emphasize\r\nbehavioral detections centered on early-stage execution patterns, including deterministic memory allocation\r\nfollowed by dense sequences of small immediate-value writes, obfuscated resolution of\r\nZwAllocateVirtualMemory, anomalous PEB traversal for API resolution, and excessive nonfunctional use of GDI\r\nAPIs. The programming flaw involving repeated NtProtectVirtualMemory calls with invalid protection flags\r\npresents a particularly high-signal opportunity for resilient EDR and telemetry-based detections.\r\nFor DFIR practitioners, understanding the packer’s staged architecture, modified UPX intermediary, and alternate\r\nexecution paths, especially DLL-based execution via regsvr32.exe, enables faster triage, more reliable unpacking,\r\nand clearer separation of packer behavior from payload functionality. Together, the techniques and detection logic\r\npresented in this report allow defenders to disrupt pkr_mtsi intrusion chains earlier in the attack lifecycle and\r\ninvestigate active incidents more efficiently and confidently. Complete analysis of the next stage and payloads will\r\nbe covered in an upcoming RL research post.\r\nAppendix A\r\nYARA Rules\r\nimport \"pe\"\r\nrule pkr_mtsi_1\r\n{\r\n meta:\r\n author = \"Malware Utkonos\"\r\n date = \"2025-10-27\"\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 9 of 13\n\ndescription = \"Matches pkr_mtsi packed samples.\"\r\n revision = 8\r\n strings:\r\n // VirtualAlloc call for location to write payload\r\n $alloc1 = { ba[4] 3?c9 41b800300000 41b940000000 ff15[4] 488905[4] 4883c42? }\r\n $alloc2 = { ba[4] 3?c9 41b940000000 41b800300000 ff15[4] 488905[4] 4883c42? }\r\n $alloc3 = { ba[4] 41b800300000 3?c9 41b940000000 ff15[4] 488905[4] 4883c42? }\r\n $alloc4 = { ba[4] 41b800300000 41b940000000 3?c9 ff15[4] 488905[4] 4883c42? }\r\n $alloc5 = { ba[4] 41b940000000 41b800300000 3?c9 ff15[4] 488905[4] 4883c42? }\r\n $alloc6 = { ba[4] 41b940000000 3?c9 41b800300000 ff15[4] 488905[4] 4883c42? }\r\n // 18008fb70 4883ec28 sub rsp, 0x28\r\n // 18008fb74 ba00900400 mov edx, 0x49000\r\n // 18008fb79 41b940000000 mov r9d, 0x40\r\n // 18008fb7f 41b800300000 mov r8d, 0x3000\r\n // 18008fb85 33c9 xor ecx, ecx {0x0}\r\n // 18008fb87 ff153bc50200 call qword [rel VirtualAlloc]\r\n // 18008fb8d 4889056cd40200 mov qword [rel data_1800bd000], rax\r\n // 18008fb94 4883c428 add rsp, 0x28\r\n // 18008fb98 c3 retn {__return_addr}\r\n // ZwAllocateVirtualMemory call for location to write payload\r\n $alloc7 = { c74424??40000000 [0-40] c74424??00300000 [0-40] 488d?424?? [0-40] ff }\r\n $alloc8 = { c74424??40000000 [0-40] 488d?424?? [0-40] c74424??00300000 [0-40] ff }\r\n $alloc9 = { c74424??00300000 [0-40] c74424??40000000 [0-40] 488d?424?? [0-40] ff }\r\n $alloc10 = { c74424??00300000 [0-40] 488d?424?? [0-40] c74424??40000000 [0-40] ff }\r\n $alloc11 = { 488d?424?? [0-40] c74424??00300000 [0-40] c74424??40000000 [0-40] ff }\r\n $alloc12 = { 488d?424?? [0-40] c74424??40000000 [0-40] c74424??00300000 [0-40] ff }\r\n // 1800371e6 4c8d4c2460 lea r9, [rsp+0x60]\r\n // 1800371eb c744242840000000 mov dword [rsp+0x28], 0x40\r\n // 1800371f3 4533c0 xor r8d, r8d {0x0}\r\n // 1800371f6 c744242000300000 mov dword [rsp+0x20], 0x3000\r\n // 1800371fe 488d542458 lea rdx, [rsp+0x58]\r\n // 180037203 48c7c1ffffffff mov rcx, 0xffffffffffffffff\r\n // 18003720a ffd3 call rbx\r\n // Loop used to copy DLL name to stack\r\n $find = { 0fb????? [0-20] ( b?3f000000 | c64424??3f ) [0-150] 4?ffc? }\r\n // 140035490 0fb71446 movzx edx, word [rsi+rax*2]\r\n // 140035494 41b83f000000 mov r8d, 0x3f\r\n // 14003549a 66413bd4 cmp dx, r12w\r\n // 14003549e 0fb6ca movzx ecx, dl\r\n // 1400354a1 440f42c1 cmovb r8d, ecx\r\n // 1400354a5 4488440430 mov byte [rsp+rax+0x30], r8b\r\n // 1400354aa 48ffc0 inc rax\r\n $ll = \"LoadLibraryA\"\r\n // Instruction that loads an 8 byte chunk of payload\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 10 of 13\n\n$load = { ( 48b8 | 48b9 | 48ba | 48bb | 48bd | 48be | 48bf | 49b9 | 49b8 | 49bf | 49bb | 49bd | 49be | 4\r\n // 180183805 48b884552e9032fc8c12 mov rax, 0x128cfc32902e5584\r\n // 18018380f 48898424c8050000 mov qword [rsp+0x5c8], rax {0x128cfc32902e5584}\r\n // Instructions that write payload chunks to an offset in memory\r\n $write1 = { 6641c78? } // Loose word-immediate memory store write via C7 (disp32, REX base)\r\n // 7ff68bc961fe 6641c7873cd80100d9b2 mov word [r15+0x1d83c], 0xb2d9 {0xb2d9}\r\n $write2 = { ~6641c78? } // Loose dword-immediate memory store write via C7 (disp32, REX base, no 0x66 p\r\n // 7ff68bc6830f 41c7867de0020080e93d4d mov dword [r14+0x2e07d], 0x4d3de980\r\n $write3 = { ~6641c78? } // Loose dword-immediate memory store write via C7 (disp32, REX base, no 0x66 p\r\n // 7ff68bc6830f 41c7867de0020080e93d4d mov dword [r14+0x2e07d], 0x4d3de980\r\n $write4 = { 66c78? } // Loose word-immediate memory store via 66+C7 (disp32 addressing; no SIB)\r\n // 1400074c6 66c78674680100dafe mov word [rsi+0x16874], 0xfeda {0xfeda}\r\n $write5 = { 48898424????0000 488b8424????0000 488b8c24????0000 488908 } // Block: spill qword to [rsp+d\r\n // 1400a480d 4889842418050000 mov qword [rsp+0x518], rax {-0x3ba224b6c292ac62}\r\n // 1400a4815 488b842410010000 mov rax, qword [rsp+0x110]\r\n // 1400a481d 488b8c2418050000 mov rcx, qword [rsp+0x518] {-0x3ba224b6c292ac62}\r\n // 1400a4825 488908 mov qword [rax], rcx {-0x3ba224b6c292ac62}\r\n // Many calls to CreateSolidBrush with random three byte colors\r\n $csb = { b9[3]00 ff15[3]00 }\r\n // 18000106c b956a76f00 mov ecx, 0x6fa756\r\n // 180001071 ff1531c00300 call qword [rel CreateSolidBrush]\r\n condition:\r\n uint16(0) == 0x5a4d and uint32(uint32(0x3c)) == 0x00004550 and\r\n 1 of ($alloc*) and\r\n // Samples that don't have LoadLibraryA will have the loop that moves a DLL name to the stack.\r\n ($find or $ll) and\r\n // The payload is broken into many 8 byte chunks located in immediate values moved into\r\n // a register by these instructions. There are at least 100 of the and more depending on\r\n // the size of the payload image.\r\n (\r\n #load \u003e 1000 or\r\n // There are various instructions that write a payload chunk to an offset in a memory location.\r\n #write1 + #write2 + #write3 + #write4 + #write5 \u003e 1000\r\n ) and\r\n // pkr_mtsi always imports more than 20 GDI functions to use as junk code.\r\n pe.imports(\"gdi32.dll\") \u003e 15 and\r\n // Many calls to CreateSolidBrush with random three byte colors\r\n for 20 i in (1..500) : (\r\n uint32(@csb[i] + 7) ==\r\n (\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 11 of 13\n\npe.import_rva(\"GDI32.dll\", \"CreateSolidBrush\")\r\n )\r\n -\r\n (\r\n (@csb[i] + !csb[i]) - pe.sections[pe.section_index(@csb[i])].raw_data_offset + pe.sections[pe.se\r\n )\r\n )\r\n}\r\nrule pkr_mtsi_UnpackMakeWide_1\r\n{\r\n meta:\r\n author = \"Malware Utkonos\"\r\n date = \"2025-11-15\"\r\n description = \"Instructions that unpack ascii strings into wide strings found in pkr_mtsi samples.\"\r\n strings:\r\n $op = { 660f6e4404?? 660f60c0 660f71e008 660fd64445?? 4883c004 483bc1 72 }\r\n // 18001b1f0 660f6e440430 movd xmm0, dword [rsp+rax+0x30]\r\n // 18001b1f6 660f60c0 punpcklbw xmm0, xmm0\r\n // 18001b1fa 660f71e008 psraw xmm0, 0x8\r\n // 18001b1ff 660fd64445a0 movq qword [rbp+rax*2-0x60], xmm0\r\n // 18001b205 4883c004 add rax, 0x4\r\n // 18001b209 483bc1 cmp rax, rcx\r\n // 18001b20c 72e2 jb 0x18001b1f0\r\n condition:\r\n uint16(0) == 0x5a4d and uint32(uint32(0x3c)) == 0x00004550 and\r\n $op\r\n}\r\nrule IsDebuggerPresent_LoopTrap_1\r\n{\r\n meta:\r\n author = \"Malware Utkonos\"\r\n date = \"2025-11-13\"\r\n description = \"Matches an infinite loop reached if IsDebuggerPresent is true.\"\r\n strings:\r\n $op = { ff15[4] 85c0 7402 ebfe }\r\n // 18000f398 ff15c25d0000 call qword [rel IsDebuggerPresent]\r\n // 18000f39e 85c0 test eax, eax\r\n // 18000f3a0 7402 je 0x18000f3a4\r\n // 18000f3a2 ebfe jmp 0x18000f3a2\r\n condition:\r\n uint16(0) == 0x5a4d and uint32(uint32(0x3c)) == 0x00004550 and\r\n for any i in (1..200) : (\r\n uint32(@op[i] + 2) ==\r\n (\r\n pe.import_rva(\"KERNEL32.dll\", \"IsDebuggerPresent\")\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 12 of 13\n\n)\r\n -\r\n (\r\n (@op[i] + 6) - pe.sections[pe.section_index(@op[i])].raw_data_offset + pe.sections[pe.section_in\r\n )\r\n )\r\n}\r\nrule Mul257_Add_Signed_1\r\n{\r\n meta:\r\n author = \"Malware Utkonos\"\r\n date = \"2025-12-10\"\r\n description = \"Matches loop that implements hashing algo: multiply 257 with signed add.\"\r\n warning = \"This rule detects the presence of this hash algorithm in benign and malicious samples.\"\r\n strings:\r\n $op = { 69??01010000 ( 488d??01 | 488d642401 | 4d8d??01 | 4d8d642401 ) ( 0fbe?? | 400fbe?? ) ( 01?? | 03\r\n // 18001b400 69c001010000 imul eax, eax, 0x101\r\n // 18001b406 488d5201 lea rdx, [rdx+0x1]\r\n // 18001b40a 0fbec9 movsx ecx, cl\r\n // 18001b40d 03c1 add eax, ecx\r\n // 18001b40f 0fb60a movzx ecx, byte [rdx]\r\n // 18001b412 84c9 test cl, cl\r\n // 18001b414 75ea jne 0x18001b400\r\n condition:\r\n $op\r\n}\r\nSource: https://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nhttps://www.reversinglabs.com/blog/unpacking-pkr_mtsi\r\nPage 13 of 13",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.reversinglabs.com/blog/unpacking-pkr_mtsi"
	],
	"report_names": [
		"unpacking-pkr_mtsi"
	],
	"threat_actors": [],
	"ts_created_at": 1775438993,
	"ts_updated_at": 1775826736,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/74694452b2a096a961b529580fd6d82e762c8a12.pdf",
		"text": "https://archive.orkl.eu/74694452b2a096a961b529580fd6d82e762c8a12.txt",
		"img": "https://archive.orkl.eu/74694452b2a096a961b529580fd6d82e762c8a12.jpg"
	}
}