{
	"id": "eace9ffc-7b46-4995-b02c-c298027acef1",
	"created_at": "2026-04-06T00:14:31.595945Z",
	"updated_at": "2026-04-10T03:36:47.836007Z",
	"deleted_at": null,
	"sha1_hash": "4224ea36c73ee2b8d684b96178970e476380f63f",
	"title": "Exploit Kits vs. Google Chrome",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2988619,
	"plain_text": "Exploit Kits vs. Google Chrome\r\nBy Threat Research TeamThreat Research Team\r\nArchived: 2026-04-05 20:00:02 UTC\r\nIn October 2021, we discovered that the Magnitude exploit kit was testing out a Chromium exploit chain in the\r\nwild. This really piqued our interest, because browser exploit kits have in the past few years focused mainly on\r\nInternet Explorer vulnerabilities and it was believed that browsers like Google Chrome are just too big of a target\r\nfor them.\r\nAbout a month later, we found that the Underminer exploit kit followed suit and developed an exploit for the same\r\nChromium vulnerability. That meant there were two exploit kits that dared to attack Google Chrome: Magnitude\r\nusing CVE-2021-21224 and CVE-2021-31956 and Underminer using CVE-2021-21224, CVE-2019-0808, CVE-2020-1020, and CVE-2020-1054.\r\nWe’ve been monitoring the exploit kit landscape very closely since our discoveries, watching out for any new\r\ndevelopments. We were waiting for other exploit kits to jump on the bandwagon, but none other did, as far as we\r\ncan tell. What’s more, Magnitude seems to have abandoned the Chromium exploit chain. And while Underminer\r\nstill continues to use these exploits today, its traditional IE exploit chains are doing much better. According to our\r\ntelemetry, less than 20% of Underminer’s exploitation attempts are targeting Chromium-based browsers.\r\nThis is some very good news because it suggests that the Chromium exploit chains were not as successful as the\r\nattackers hoped they would be and that it is not currently very profitable for exploit kit developers to target\r\nChromium users. In this blog post, we would like to offer some thoughts into why that could be the case and why\r\nthe attackers might have even wanted to develop these exploits in the first place. And since we don’t get to see a\r\nnew Chromium exploit chain in the wild every day, we will also dissect Magnitude’s exploits and share some\r\ndetailed technical information about them.\r\nExploit Kit Theory\r\nTo understand why exploit kit developers might have wanted to test Chromium exploits, let’s first look at things\r\nfrom their perspective. Their end goal in developing and maintaining an exploit kit is to make a profit: they just\r\nsimply want to maximize the difference between money “earned” and money spent. To achieve this goal, most\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 1 of 14\n\nmodern exploit kits follow a simple formula. They buy ads targeted to users who are likely to be vulnerable to\r\ntheir exploits (e.g. Internet Explorer users). These ads contain JavaScript code that is automatically executed, even\r\nwhen the victim doesn’t interact with the ad in any way (sometimes referred to as drive-by attacks). This code can\r\nthen further profile the victim’s browser environment and select a suitable exploit for that environment. If the\r\nexploitation succeeds, a malicious payload (e.g. ransomware or a coinminer) is deployed to the victim. In this\r\nscenario, the money “earned” could be the ransom or mining rewards. On the other hand, the money spent is the\r\ncost of ads, infrastructure (renting servers, registering domain names etc.), and the time the attacker spends on\r\ndeveloping and maintaining the exploit kit.\r\nModus operandi of a typical browser exploit kit\r\nThe attackers would like to have many diverse exploits ready at any given time because it would allow them to\r\ncast a wide net for potential victims. But it is important to note that individual exploits generally get less effective\r\nover time. This is because the number of people susceptible to a known vulnerability will decrease as some people\r\npatch and other people upgrade to new devices (which are hopefully not plagued by the same vulnerabilities as\r\ntheir previous devices). This forces the attackers to always look for new vulnerabilities to exploit. If they stick\r\nwith the same set of exploits for years, their profit would eventually reduce down to almost nothing.\r\nSo how do they find the right vulnerabilities to exploit? After all, there are thousands of CVEs reported each year,\r\nbut only a few of them are good candidates for being included in an exploit kit. Weaponizing an exploit generally\r\ntakes a lot of time (unless, of course, there is a ready-to-use PoC or the exploit can be stolen from a competitor),\r\nso the attackers might first want to carefully take into account multiple characteristics of each vulnerability. If a\r\nvulnerability scores well across these characteristics, it looks like a good candidate for inclusion in an exploit kit.\r\nSome of the more important characteristics are listed below.\r\nPrevalence of the vulnerability\r\nThe more users are affected by the vulnerability, the more attractive it is to the attackers. \r\nExploit reliability\r\nMany exploits rely on some assumptions or are based on a race condition, which makes them fail some of\r\nthe time. The attackers obviously prefer high-reliability exploits.\r\nDifficulty of exploit development\r\nThis determines the time that needs to be spent on exploit development (if the attackers are even capable of\r\nexploiting the vulnerability). The attackers tend to prefer vulnerabilities with a public PoC exploit, which\r\nthey can often just integrate into their exploit kit with minimal effort.\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 2 of 14\n\nTargeting precision\r\nThe attackers care about how hard it is to identify (and target ads to) vulnerable victims. If they misidentify\r\nvictims too often (meaning that they serve exploits to victims who they cannot exploit), they’ll just lose\r\nmoney on the malvertising.\r\nExpected vulnerability lifetime\r\nAs was already discussed, each vulnerability gets less effective over time. However, the speed at which the\r\neffectiveness drops can vary a lot between vulnerabilities, mostly based on how effective is the patching\r\nprocess of the affected software.\r\nExploit detectability\r\nThe attackers have to deal with numerous security solutions that are in the business of protecting their users\r\nagainst exploits. These solutions can lower the exploit kit’s success rate by a lot, which is why the attackers\r\nprefer more stealthy exploits that are harder for the defenders to detect. \r\nExploit potential\r\nSome exploits give the attackers System , while others might make them only end up inside a sandbox.\r\nExploits with less potential are also less useful, because they either need to be chained with other LPE\r\nexploits, or they place limits on what the final malicious payload is able to do.\r\nLooking at these characteristics, the most plausible explanation for the failure of the Chromium exploit chains is\r\nthe expected vulnerability lifetime. Google is extremely good at forcing users to install browser patches: Chrome\r\nupdates are pushed to users when they’re ready and can happen many times in a month (unlike e.g. Internet\r\nExplorer updates which are locked into the once-a-month “Patch Tuesday” cycle that is only broken for\r\nexceptionally severe vulnerabilities). When CVE-2021-21224 was a zero-day vulnerability, it affected billions of\r\nusers. Within a few days, almost all of these users received a patch. The only unpatched users were those who\r\nmanually disabled (or broke) automatic updates, those who somehow managed not to relaunch the browser in a\r\nlong time, and those running Chromium forks with bad patching habits.\r\nA secondary reason for the failure could be attributed to bad targeting precision. Ad networks often allow the\r\nattackers to target ads based on various characteristics of the user’s browser environment, but the specific version\r\nof the browser is usually not one of these characteristics. For Internet Explorer vulnerabilities, this does not matter\r\nthat much: the attackers can just buy ads for Internet Explorer users in general. As long as a certain percentage of\r\nInternet Explorer users is vulnerable to their exploits, they will make a profit. However, if they just blindly\r\ntargeted Google Chrome users, the percentage of vulnerable victims might be so low, that the cost of malvertising\r\nwould outweigh the money they would get by exploiting the few vulnerable users. Google also plans to reduce the\r\namount of information given in the User-Agent string. Exploit kits often heavily rely on this string for precise\r\ninformation about the browser version. With less information in the User-Agent header, they might have to come\r\nup with some custom version fingerprinting, which would most likely be less accurate and costly to manage.\r\nNow that we have some context about exploit kits and Chromium, we can finally speculate about why the\r\nattackers decided to develop the Chromium exploit chains. First of all, adding new vulnerabilities to an exploit kit\r\nseems a lot like a “trial and error” activity. While the attackers might have some expectations about how well a\r\ncertain exploit will perform, they cannot know for sure how useful it will be until they actually test it out in the\r\nwild. This means it should not be surprising that sometimes, their attempts to integrate an exploit turn out worse\r\nthan they expected. Perhaps they misjudged the prevalence of the vulnerabilities or thought that it would be easier\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 3 of 14\n\nto target the vulnerable victims. Perhaps they focused too much on the characteristics that the exploits do well on:\r\nafter all, they have reliable, high-potential exploits for a browser that’s used by billions. It could also be that this\r\nwas all just some experimentation where the attackers just wanted to explore the land of Chromium exploits.\r\nIt’s also important to point out that the usage of Internet Explorer (which is currently vital for the survival of\r\nexploit kits) has been steadily dropping over the past few years. This may have forced the attackers to experiment\r\nwith how viable exploits for other browsers are because they know that sooner or later they will have to make the\r\nswitch. But judging from these attempts, the attackers do not seem fully capable of making the switch as of now.\r\nThat is some good news because it could mean that if nothing significant changes, exploit kits might be forced to\r\nretire when Internet Explorer usage drops below some critical limit.\r\nCVE-2021-21224\r\nLet’s now take a closer look at the Magnitude’s exploit chain that we discovered in the wild. The exploitation\r\nstarts with a JavaScript exploit for CVE-2021-21224. This is a type confusion vulnerability in V8, which allows\r\nthe attacker to execute arbitrary code within a (sandboxed) Chromium renderer process. A zero-day exploit for this\r\nvulnerability (or issue 1195777, as it was known back then since no CVE ID had been assigned yet) was dumped\r\non Github on April 14, 2021. The exploit worked for a couple of days against the latest Chrome version, until\r\nGoogle rushed out a patch about a week later.\r\nIt should not be surprising that Magnitude’s exploit is heavily inspired by the PoC on Github. However, while\r\nboth Magnitude’s exploit and the PoC follow a very similar exploitation path, there are no matching code pieces,\r\nwhich suggests that the attackers didn’t resort that much to the “Copy/Paste” technique of exploit development. In\r\nfact, Magnitude’s exploit looks like a more cleaned-up and reliable version of the PoC. And since there is no\r\nobfuscation employed (the attackers probably meant to add it in later), the exploit is very easy to read and debug.\r\nThere are even very self-explanatory function names, such as confusion_to_oob , addrof , and arb_write , and\r\nvariable names, such as oob_array , arb_write_buffer , and oob_array_map_and_properties . The only way\r\nthis could get any better for us researchers would be if the authors left a couple of helpful comments in there…\r\nInterestingly, some parts of the exploit also seem inspired by a CTF writeup for a “pwn” challenge from *CTF\r\n2019, in which the players were supposed to exploit a made-up vulnerability that was introduced into a fork of V8.\r\nWhile CVE-2021-21224 is obviously a different (and actual rather than made-up) vulnerability, many of the\r\ntechniques outlined in that writeup apply for V8 exploitation in general and so are used in the later stages of the\r\nMagnitude’s exploit, sometimes with the very same variable names as those used in the writeup.\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 4 of 14\n\nThe core of the exploit, triggering the vulnerability to corrupt the length of vuln_array\r\nThe root cause of the vulnerability is incorrect integer conversion during the SimplifiedLowering phase. This\r\nincorrect conversion is triggered in the exploit by the Math.max call, shown in the code snippet above. As can be\r\nseen, the exploit first calls foofunc in a loop 0x10000 times. This is to make V8 compile that function because\r\nthe bug only manifests itself after JIT compilation. Then, helper[\"gcfunc\"] gets called. The purpose of this\r\nfunction is just to trigger garbage collection. We tested that the exploit also works without this call, but the authors\r\nprobably put it there to improve the exploit’s reliability. Then, foofunc is called one more time, this time with\r\nflagvar=true , which makes xvar=0xFFFFFFFF . Without the bug, lenvar should now evaluate to\r\n-0xFFFFFFFF and the next statement should throw a RangeError because it should not be possible to create an\r\narray with a negative length. However, because of the bug, lenvar evaluates to an unexpected value of 1 . The\r\nreason for this is that the vulnerable code incorrectly converts the result of Math.max from an unsigned 32-bit\r\ninteger 0xFFFFFFFF to a signed 32-bit integer -1 . After constructing vuln_array , the exploit calls\r\nArray.prototype.shift on it. Under normal circumstances, this method should remove the first element from\r\nthe array, so the length of vuln_array should be zero. However, because of the disparity between the actual and\r\nthe predicted value of lenvar , V8 makes an incorrect optimization here and just puts the 32-bit constant\r\n0xFFFFFFFF into Array.length (this is computed as 0-1 with an unsigned 32-bit underflow, where 0 is the\r\npredicted length and -1 signifies Array.prototype.shift decrementing Array.length ). \r\nA demonstration of how an overwrite on vuln_array can corrupt the length of oob_array\r\nNow, the attackers have successfully crafted a JSArray with a corrupted Array.length , which allows them to\r\nperform out-of-bounds memory reads and writes. The very first out-of-bounds memory write can be seen in the\r\nlast statement of the confusion_to_oob function. The exploit here writes 0xc00c to vuln_array[0x10] . This\r\nabuses the deterministic memory layout in V8 when a function creates two local arrays. Since vuln_array was\r\ncreated first, oob_array is located at a known offset from it in memory and so by making out-of-bounds memory\r\naccesses through vuln_array , it is possible to access both the metadata and the actual data of oob_array . In\r\nthis case, the element at index 0x10 corresponds to offset 0x40 , which is where Array.length of oob_array\r\nis stored. The out-of-bounds write therefore corrupts the length of oob_array , so it is now too possible to read\r\nand write past its end.\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 5 of 14\n\nThe addrof and fakeobj exploit primitives\r\nNext, the exploit constructs the addrof and fakeobj exploit primitives. These are well-known and very powerful\r\nprimitives in the world of JavaScript engine exploitation. In a nutshell, addrof leaks the address of a JavaScript\r\nobject, while fakeobj creates a new, fake object at a given address. Having constructed these two primitives, the\r\nattacker can usually reuse existing techniques to get to their ultimate goal: arbitrary code execution. \r\nA step-by-step breakdown of the addrof primitive. Note that just the lower 32 bits of the address get leaked, while\r\n%DebugPrint returns the whole 64-bit address. In practice, this doesn’t matter because V8 compresses pointers by\r\nkeeping upper 32 bits of a\r\nBoth primitives are constructed in a similar way, abusing the fact that vuln_array[0x7] and oob_array[0]\r\npoint to the very same memory location. It is important to note here that  vuln_array is internally represented by\r\nV8 as HOLEY_ELEMENTS , while oob_array is PACKED_DOUBLE_ELEMENTS (for more information about internal\r\narray representation in V8, please refer to this blog post by the V8 devs). This makes it possible to write an object\r\ninto vuln_array and read it (or more precisely, the pointer to it) from the other end in oob_array as a double.\r\nThis is exactly how addrof is implemented, as can be seen above. Once the address is read, it is converted using\r\nhelper[\"f2ifunc\"] from double representation into an integer representation, with the upper 32 bits masked out,\r\nbecause the double takes 64 bits, while pointers in V8 are compressed down to just 32 bits. fakeobj is\r\nimplemented in the same fashion, just the other way around. First, the pointer is converted into a double using\r\nhelper[\"i2ffunc\"] . The pointer, encoded as a double, is then written into oob_array[0] and then read from\r\nvuln_array[0x7] , which tricks V8 into treating it as an actual object. Note that there is no masking needed in\r\nfakeobj because the double written into oob_array is represented by more bits than the pointer read from\r\nvuln_array .\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 6 of 14\n\nThe arbitrary read/write exploit primitives\r\nWith addrof and fakeobj in place, the exploit follows a fairly standard exploitation path, which seems heavily\r\ninspired by the aforementioned *CTF 2019 writeup. The next primitives constructed by the exploit are arbitrary\r\nread/write. To achieve these primitives, the exploit fakes a JSArray (aptly named fake in the code snippet\r\nabove) in such a way that it has full control over its metadata. It can then overwrite the fake JSArray’s elements\r\npointer, which points to the address where the actual elements of the array get stored. Corrupting the elements\r\npointer allows the attackers to point the fake array to an arbitrary address, and it is then subsequently possible to\r\nread/write to that address through reads/writes on the fake array.\r\nLet’s look at the implementation of the arbitrary read/write primitive in a bit more detail. The exploit first calls the\r\nget_arw function to set up the fake JSArray. This function starts by using an overread on oob_array[3] in\r\norder to leak map and properties of oob_array (remember that the original length of oob_array was 3\r\nand that its length got corrupted earlier). The map and properties point to structures that basically describe the\r\nobject type in V8. Then, a new array called point_array gets created, with the oob_array_map_and_properties\r\nvalue as its first element. Finally, the fake JSArray gets constructed at offset 0x20 before point_array . This\r\noffset was carefully chosen, so that the the JSArray structure corresponding to fake overlaps with elements of\r\npoint_array . Therefore, it is possible to control the internal members of fake by modifying the elements of\r\npoint_array . Note that elements in point_array take 64 bits, while members of the JSArray structure usually\r\nonly take 32 bits, so modifying one element of point_array might overwrite two members of fake at the same\r\ntime. Now, it should make sense why the first element of point_array was set to\r\noob_array_map_and_properties . The first element is at the same address where V8 would look for the map and\r\nproperties of fake . By initializing it like this, fake is created to be a PACKED_DOUBLE_ELEMENTS JSArray,\r\nbasically inheriting its type from oob_array .\r\nThe second element of point_array overlaps with the elements pointer and Array.length of fake . The\r\nexploit uses this for both arbitrary read and arbitrary write, first corrupting the elements pointer to point to the\r\ndesired address and then reading/writing to that address through fake[0] . However, as can be seen in the exploit\r\ncode above, there are some additional actions taken that are worth explaining. First of all, the exploit always\r\nmakes sure that addrvar is an odd number. This is because V8 expects pointers to be tagged, with the least\r\nsignificant bit set. Then, there is the addition of 2\u003c\u003c32 to addrvar . As was explained before, the second\r\nelement of point_array takes up 64 bits in memory, while the elements pointer and Array.length both take\r\nup only 32 bits. This means that a write to point_array[1] overwrites both members at once and the 2\u003c\u003c32\r\njust simply sets the Array.length , which is controlled by the most significant 32 bits. Finally, there is the\r\nsubtraction of 8 from addrvar . This is because the elements pointer does not point straight to the first\r\nelement, but instead to a FixedDoubleArray structure, which takes up eight bytes and precedes the actual element\r\ndata in memory.\r\nA dummy WebAssembly program that will get hollowed out and replaced by Magnitude’s shellcode\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 7 of 14\n\nThe final step taken by the exploit is converting the arbitrary read/write primitive into arbitrary code execution.\r\nFor this, it uses a well-known trick that takes advantage of WebAssembly. When V8 JIT-compiles a WebAssembly\r\nfunction, it places the compiled code into memory pages that are both writable and executable (there now seem to\r\nbe some new mitigations that aim to prevent this trick, but it is still working against V8 versions vulnerable to\r\nCVE-2021-21224). The exploit can therefore locate the code of a JIT-compiled WebAssembly function, overwrite\r\nit with its own shellcode and then call the original WebAssembly function from Javascript, which executes the\r\nshellcode planted there.\r\nMagnitude’s exploit first creates a dummy WebAssembly module that contains a single function called main ,\r\nwhich just returns the number 42 (the original code of this function doesn’t really matter because it will get\r\noverwritten with the shellcode anyway). Using a combination of addrof and arb_read , the exploit obtains the\r\naddress where V8 JIT-compiled the function main . Interestingly, it then constructs a whole new arbitrary write\r\nprimitive using an ArrayBuffer with a corrupted backing store pointer and uses this newly constructed primitive to\r\nwrite shellcode to the address of main . While it could theoretically use the first arbitrary write primitive to place\r\nthe shellcode there, it chooses this second method, most likely because it is more reliable. It seems that the first\r\nmethod might crash V8 under some rare circumstances, which makes it not practical for repeated use, such as\r\nwhen it gets called thousands of times to write a large shellcode buffer into memory.\r\nThere are two shellcodes embedded in the exploit. The first one contains an exploit for CVE-2021-31956. This\r\none gets executed first and its goal is to steal the SYSTEM token to elevate the privileges of the current process.\r\nAfter the first shellcode returns, the second shellcode gets planted inside the JIT-compiled WebAssembly function\r\nand executed. This second shellcode injects Magniber ransomware into some already running process and lets it\r\nencrypt the victim’s drives.\r\nCVE-2021-31956\r\nLet’s now turn our attention to the second exploit in the chain, which Magnitude uses to escape the Chromium\r\nsandbox. This is an exploit for CVE-2021-31956, a paged pool buffer overflow in the Windows kernel. It was\r\ndiscovered in June 2021 by Boris Larin from Kaspersky, who found it being used as a zero-day in the wild as a\r\npart of the PuzzleMaker attack. The Kaspersky blog post about PuzzleMaker briefly describes the vulnerability\r\nand the way the attackers chose to exploit it. However, much more information about the vulnerability can be\r\nfound in a two–part blog series by Alex Plaskett from NCC Group. This blog series goes into great detail and\r\npretty much provides a step-by-step guide on how to exploit the vulnerability. We found that the attackers behind\r\nMagnitude followed this guide very closely, even though there are certainly many other approaches that they\r\ncould have chosen for exploitation. This shows yet again that publishing vulnerability research can be a double-edged sword. While the blog series certainly helped many defend against the vulnerability, it also made it much\r\neasier for the attackers to weaponize it.\r\nThe vulnerability lies in ntfs.sys , inside the function NtfsQueryEaUserEaList , which is directly reachable\r\nfrom the syscall NtQueryEaFile . This syscall internally allocates a temporary buffer on the paged pool (the size\r\nof which is controllable by a syscall parameter) and places there the NTFS Extended Attributes associated with a\r\ngiven file. Individual Extended Attributes are separated by a padding of up to four bytes. By making the padding\r\nstart directly at the end of the allocated pool chunk, it is possible to trigger an integer underflow which results in\r\nNtfsQueryEaUserEaList writing subsequent Extended Attributes past the end of the pool chunk. The idea behind\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 8 of 14\n\nthe exploit is to spray the pool so that chunks containing certain Windows Notification Facility (WNF) structures\r\ncan be corrupted by the overflow. Using some WNF magic that will be explained later, the exploit gains an\r\narbitrary read/write primitive, which it uses to steal the SYSTEM token.\r\nThe exploit starts by checking the victim’s Windows build number. Only builds 18362, 18363, 19041, and 19042\r\n(19H1 – 20H2) are supported, and the exploit bails out if it finds itself running on a different build. The build\r\nnumber is then used to determine proper offsets into the _EPROCESS structure as well as to determine correct\r\nsyscall numbers, because syscalls are invoked directly by the exploit, bypassing the usual syscall stubs in ntdll .\r\nCheck for the victim’s Windows build number\r\nNext, the exploit brute-forces file handles, until it finds one on which it can use the NtSetEAFile syscall to set its\r\nNTFS Extended Attributes. Two attributes are set on this file, crafted to trigger an overflow of 0x10 bytes into\r\nthe next pool chunk later when NtQueryEaFile gets called.\r\nSpecially crafted NTFS Extended Attributes, designed to cause a paged pool buffer overflow\r\nWhen the specially crafted NTFS Extended Attributes are set, the exploit proceeds to spray the paged pool with\r\n_WNF_NAME_INSTANCE and _WNF_STATE_DATA structures. These structures are sprayed using the syscalls\r\nNtCreateWnfStateName and NtUpdateWnfStateData , respectively. The exploit then creates 10 000 extra\r\n_WNF_STATE_DATA structures in a row and frees each other one using NtDeleteWnfStateData . This creates holes\r\nbetween _WNF_STATE_DATA chunks, which are likely to get reclaimed on future pool allocations of similar size. \r\nWith this in mind, the exploit now triggers the vulnerability using NtQueryEaFile , with a high likelihood of\r\ngetting a pool chunk preceding a random _WNF_STATE_DATA chunk and thus overflowing into that chunk. If that\r\nreally happens, the _WNF_STATE_DATA structure will get corrupted as shown below. However, the exploit doesn’t\r\nknow which _WNF_STATE_DATA structure got corrupted, if any. To find the corrupted structure, it has to iterate\r\nover all of them and query its ChangeStamp using NtQueryWnfStateData . If the ChangeStamp contains the\r\nmagic number 0xcafe , the exploit found the corrupted chunk. In case the overflow does not hit any\r\n_WNF_STATE_DATA chunk, the exploit just simply tries triggering the vulnerability again, up to 32 times. Note that\r\nin case the overflow didn’t hit a _WNF_STATE_DATA chunk, it might have corrupted a random chunk in the paged\r\npool, which could result in a BSoD. However, during our testing of the exploit, we didn’t get any BSoDs during\r\nnormal exploitation, which suggests that the pool spraying technique used by the attackers is relatively robust.\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 9 of 14\n\nThe corrupted _WNF_STATE_DATA instance. AllocatedSize and DataSize were both artificially increased, while\r\nChangeStamp got set to an easily recognizable value.\r\nAfter a successful _WNF_STATE_DATA corruption, more _WNF_NAME_INSTANCE structures get sprayed on the pool,\r\nwith the idea that they will reclaim the other chunks freed by NtDeleteWnfStateData . By doing this, the attackers\r\nare trying to position a _WNF_NAME_INSTANCE chunk after the corrupted _WNF_STATE_DATA chunk in memory. To\r\nexplain why they would want this, let’s first discuss what they achieved by corrupting the _WNF_STATE_DATA\r\nchunk.\r\nThe _WNF_STATE_DATA structure can be thought of as a header preceding an actual WnfStateData buffer in\r\nmemory. The WnfStateData buffer can be read using the syscall NtQueryWnfStateData and written to using\r\nNtUpdateWnfStateData . _WNF_STATE_DATA.AllocatedSize determines how many bytes can be written to\r\nWnfStateData and _WNF_STATE_DATA.DataSize determines how many bytes can be read. By corrupting these\r\ntwo fields and setting them to a high value, the exploit gains a relative memory read/write primitive, obtaining the\r\nability to read/write memory even after the original WnfStateData buffer. Now it should be clear why the\r\nattackers would want a _WNF_NAME_INSTANCE chunk after a corrupted _WNF_STATE_DATA chunk: they can use the\r\noverread/overwrite to have full control over a _WNF_NAME_INSTANCE structure. They just need to perform an\r\noverread and scan the overread memory for bytes 03 09 A8 , which denote the start of their\r\n_WNF_NAME_INSTANCE structure. If they want to change something in this structure, they can just modify some of\r\nthe overread bytes and overwrite them back using NtUpdateWnfStateData .\r\nThe exploit scans the overread memory, looking for a _WNF_NAME_INSTANCE header. 0x0903 here represents\r\nthe NodeTypeCode, while 0xA8 is a preselected NodeByteSize.\r\nWhat is so interesting about a _WNF_NAME_INSTANCE structure, that the attackers want to have full control over it?\r\nWell, first of all, at offset 0x98 there is _WNF_NAME_INSTANCE.CreatorProcess , which gives them a pointer to\r\n_EPROCESS relevant to the current process. Kaspersky reported that PuzzleMaker used a separate information\r\ndisclosure vulnerability, CVE-2021-31955, to leak the _EPROCESS base address. However, the attackers behind\r\nMagnitude do not need to use a second vulnerability, because the _EPROCESS address is just there for the taking.\r\nAnother important offset is 0x58 , which corresponds to _WNF_NAME_INSTANCE.StateData . As the name suggests,\r\nthis is a pointer to a _WNF_STATE_DATA structure. By modifying this, the attackers can not only enlarge the\r\nWnfStateData buffer but also redirect it to an arbitrary address, which gives them an arbitrary read/write\r\nprimitive. There are some constraints though, such as that the StateData pointer has to point 0x10 bytes before\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 10 of 14\n\nthe address that is to be read/written and that there has to be some data there that makes sense when interpreted as\r\na _WNF_STATE_DATA structure.\r\nThe StateData pointer gets first set to _EPROCESS+0x28 , which allows the exploit to read\r\n_KPROCESS.ThreadListHead (interestingly, this value gets leaked using ChangeStamp and DataSize , not\r\nthrough WnfStateData ). The ThreadListHead points to _KTHREAD.ThreadListEntry of the first thread, which\r\nis the current thread in the context of Chromium exploitation. By subtracting the offset of ThreadListEntry , the\r\nexploit gets the _KTHREAD base address for the current thread. \r\nWith the base address of _KTHREAD , the exploit points StateData to _KTHREAD+0x220 , which allows it to\r\nread/write up to three bytes starting from _KTHREAD+0x230 . It uses this to set the byte at _KTHREAD+0x232 to\r\nzero. On the targeted Windows builds, the offset 0x232 corresponds to _KTHREAD.PreviousMode . Setting its\r\nvalue to SystemMode=0 tricks the kernel into believing that some of the thread’s syscalls are actually originating\r\nfrom the kernel. Specifically, this allows the thread to use the NtReadVirtualMemory and\r\nNtWriteVirtualMemory syscalls to perform reads and writes to the kernel address space.\r\nThe exploit corrupting _KTHREAD.PreviousMode\r\nAs was the case in the Chromium exploit, the attackers here just traded an arbitrary read/write primitive for yet\r\nanother arbitrary read/write primitive. However, note that the new primitive based on PreviousMode is a\r\nsignificant upgrade compared to the original StateData one. Most importantly, the new primitive is free of the\r\nconstraints associated with the original one. The new primitive is also more reliable because there are no longer\r\nrace conditions that could potentially cause a BSoD. Not to mention that just simply calling\r\nNtWriteVirtualMemory is much faster and much less awkward than abusing multiple WNF-related syscalls to\r\nachieve the same result.\r\nWith a robust arbitrary read/write primitive in place, the exploit can finally do its thing and proceed to steal the\r\nSYSTEM token. Using the leaked _EPROCESS address from before, it finds _EPROCESS.ActiveProcessLinks ,\r\nwhich leads to a linked list of other _EPROCESS structures. It iterates over this list until it finds the System\r\nprocess. Then it reads System’s _EPROCESS.Token and assigns this value (with some of the RefCnt bits masked\r\nout) to its own _EPROCESS structure. Finally, the exploit also turns off some mitigation flags in\r\n_EPROCESS.MitigationFlags .\r\nNow, the exploit has successfully elevated privileges and can pass control to the other shellcode, which was\r\ndesigned to load Magniber ransomware. But before it does that, the exploit performs many cleanup actions that\r\nare necessary to avoid blue screening later on. It iterates over WNF-related structures using TemporaryNamesList\r\nfrom _EPROCESS.WnfContext and fixes all the _WNF_NAME_INSTANCE structures that got overflown into at the\r\nbeginning of the exploit. It also attempts to fix the _POOL_HEADER of the overflown _WNF_STATE_DATA chunks.\r\nFinally, the exploit gets rid of both read/write primitives by setting _KTHREAD.PreviousMode back to\r\nUserMode=1 and using one last NtUpdateWnfStateData syscall to restore the corrupted StateData pointer\r\nback to its original value.\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 11 of 14\n\nFixups performed on previously corrupted _WNF_NAME_INSTANCE structures\r\nFinal Thoughts\r\nIf this isn’t the first time you’re hearing about Magnitude, you might have noticed that it often exploits\r\nvulnerabilities that were previously weaponized by APT groups, who used them as zero-days in the wild. To name\r\na few recent examples, CVE-2021-31956 was exploited by PuzzleMaker, CVE-2021-26411 was used in a high-profile attack targeting security researchers, CVE-2020-0986 was abused in Operation Powerfall, and CVE-2019-\r\n1367 was reported to be exploited in the wild by an undisclosed threat actor (who might be DarkHotel APT\r\naccording to Qihoo 360). The fact that the attackers behind Magnitude are so successful in reproducing complex\r\nexploits with no public PoCs could lead to some suspicion that they have somehow obtained under-the-counter\r\naccess to private zero-day exploit samples. After all, we don’t know much about the attackers, but we do know\r\nthat they are skilled exploit developers, and perhaps Magnitude is not their only source of income. But before we\r\njump to any conclusions, we should mention that there are other, more plausible explanations for why they should\r\nprioritize vulnerabilities that were once exploited as zero-days. First, APT groups usually know what they are\r\ndoing[citation needed]. If an APT group decides that a vulnerability is worth exploiting in the wild, that generally\r\nmeans that the vulnerability is reliably weaponizable. In a way, the attackers behind Magnitude could abuse this to\r\nlet the APT groups do the hard work of selecting high-quality vulnerabilities for them. Second, zero-days in the\r\nwild usually attract a lot of research attention, which means that there are often detailed writeups that analyze the\r\nvulnerability’s root cause and speculate about how it could get exploited. These writeups make exploit\r\ndevelopment a lot easier compared to more obscure vulnerabilities which attracted only a limited amount of\r\nresearch.\r\nAs we’ve shown in this blog post, both Magnitude and Underminer managed to successfully develop exploit\r\nchains for Chromium on Windows. However, none of the exploit chains were particularly successful in terms of\r\nthe number of exploited victims. So what does this mean for the future of exploit kits? We believe that unless\r\nsome new, hard-to-patch vulnerability comes up, exploit kits are not something that the average Google Chrome\r\nuser should have to worry about much. After all, it has to be acknowledged that Google does a great job at\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 12 of 14\n\npatching and reducing the browser’s attack surface. Unfortunately, the same cannot be said for all other\r\nChromium-based browsers. We found that a big portion of those that we protected from Underminer were running\r\nChromium forks that were months (or even years) behind on patching. Because of this, we recommend avoiding\r\nChromium forks that are slow in applying security patches from the upstream. Also note that some Chromium\r\nforks might have vulnerabilities in their own custom codebase. But as long as the number of users running the\r\nvulnerable forks is relatively low, exploit kit developers will probably not even bother with implementing exploits\r\nspecific just for them.\r\nFinally, we should also mention that it is not entirely impossible for exploit kits to attack using zero-day or n-day\r\nexploits. If that were to happen, the attackers would probably carry out a massive burst of malvertising or\r\nwatering hole campaigns. In such a scenario, even regular Google Chrome users would be at risk. The damage\r\ndone by such an attack could be enormous, depending on the reaction time of browser developers, ad networks,\r\nsecurity companies, LEAs, and other concerned parties. There are basically three ways that the attackers could get\r\ntheir hands on a zero-day exploit: they could either buy it, discover it themselves, or discover it being used by\r\nsome other threat actor. Fortunately, using some simple math we can see that the campaign would have to be very\r\nsuccessful if the attackers wanted to recover the cost of the zero-day, which is likely to discourage most of them.\r\nRegarding n-day exploitation, it all boils down to a race if the attackers can develop a working exploit sooner than\r\na patch gets written and rolled out to the end users. It’s a hard race to win for the attackers, but it has been won\r\nbefore. We know of at least two cases when an n-day exploit working against the latest Google Chrome version\r\nwas dumped on GitHub (this probably doesn’t need to be written down, but dumping such exploits on GitHub is\r\nnot a very bright idea). Fortunately, these were just renderer exploits and there were no accompanying sandbox\r\nescape exploits (which would be needed for full weaponization). But if it is possible to win the race for one\r\nexploit, it’s not unthinkable that an attacker could win it for two exploits at the same time.\r\nIndicators of Compromise (IoCs)\r\nMagnitude\r\nUnderminer\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 13 of 14\n\nA group of elite researchers who like to stay under the radar.\r\nSource: https://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nhttps://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/\r\nPage 14 of 14",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://decoded.avast.io/janvojtesek/exploit-kits-vs-google-chrome/"
	],
	"report_names": [
		"exploit-kits-vs-google-chrome"
	],
	"threat_actors": [
		{
			"id": "1dadf04e-d725-426f-9f6c-08c5be7da159",
			"created_at": "2022-10-25T15:50:23.624538Z",
			"updated_at": "2026-04-10T02:00:05.286895Z",
			"deleted_at": null,
			"main_name": "Darkhotel",
			"aliases": [
				"Darkhotel",
				"DUBNIUM",
				"Zigzag Hail"
			],
			"source_name": "MITRE:Darkhotel",
			"tools": null,
			"source_id": "MITRE",
			"reports": null
		},
		{
			"id": "08c8f238-1df5-4e75-b4d8-276ebead502d",
			"created_at": "2023-01-06T13:46:39.344081Z",
			"updated_at": "2026-04-10T02:00:03.294222Z",
			"deleted_at": null,
			"main_name": "Copy-Paste",
			"aliases": [],
			"source_name": "MISPGALAXY:Copy-Paste",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "9f101d9c-05ea-48b9-b6f1-168cd6d06d12",
			"created_at": "2023-01-06T13:46:39.396409Z",
			"updated_at": "2026-04-10T02:00:03.312816Z",
			"deleted_at": null,
			"main_name": "Earth Lusca",
			"aliases": [
				"CHROMIUM",
				"ControlX",
				"TAG-22",
				"BRONZE UNIVERSITY",
				"AQUATIC PANDA",
				"RedHotel",
				"Charcoal Typhoon",
				"Red Scylla",
				"Red Dev 10",
				"BountyGlad"
			],
			"source_name": "MISPGALAXY:Earth Lusca",
			"tools": [
				"RouterGod",
				"SprySOCKS",
				"ShadowPad",
				"POISONPLUG",
				"Barlaiy",
				"Spyder",
				"FunnySwitch"
			],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "b13c19d6-247d-47ba-86ba-15a94accc179",
			"created_at": "2024-05-01T02:03:08.149923Z",
			"updated_at": "2026-04-10T02:00:03.763147Z",
			"deleted_at": null,
			"main_name": "TUNGSTEN BRIDGE",
			"aliases": [
				"APT-C-06 ",
				"ATK52 ",
				"CTG-1948 ",
				"DUBNIUM ",
				"DarkHotel ",
				"Fallout Team ",
				"Shadow Crane ",
				"Zigzag Hail "
			],
			"source_name": "Secureworks:TUNGSTEN BRIDGE",
			"tools": [
				"Nemim",
				"Tapaoux"
			],
			"source_id": "Secureworks",
			"reports": null
		},
		{
			"id": "2b4eec94-7672-4bee-acb2-b857d0d26d12",
			"created_at": "2023-01-06T13:46:38.272109Z",
			"updated_at": "2026-04-10T02:00:02.906089Z",
			"deleted_at": null,
			"main_name": "DarkHotel",
			"aliases": [
				"T-APT-02",
				"Nemim",
				"Nemin",
				"Shadow Crane",
				"G0012",
				"DUBNIUM",
				"Karba",
				"APT-C-06",
				"SIG25",
				"TUNGSTEN BRIDGE",
				"Zigzag Hail",
				"Fallout Team",
				"Luder",
				"Tapaoux",
				"ATK52"
			],
			"source_name": "MISPGALAXY:DarkHotel",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "c0cedde3-5a9b-430f-9b77-e6568307205e",
			"created_at": "2022-10-25T16:07:23.528994Z",
			"updated_at": "2026-04-10T02:00:04.642473Z",
			"deleted_at": null,
			"main_name": "DarkHotel",
			"aliases": [
				"APT-C-06",
				"ATK 52",
				"CTG-1948",
				"Dubnium",
				"Fallout Team",
				"G0012",
				"G0126",
				"Higaisa",
				"Luder",
				"Operation DarkHotel",
				"Operation Daybreak",
				"Operation Inexsmar",
				"Operation PowerFall",
				"Operation The Gh0st Remains the Same",
				"Purple Pygmy",
				"SIG25",
				"Shadow Crane",
				"T-APT-02",
				"TieOnJoe",
				"Tungsten Bridge",
				"Zigzag Hail"
			],
			"source_name": "ETDA:DarkHotel",
			"tools": [
				"Asruex",
				"DarkHotel",
				"DmaUp3.exe",
				"GreezeBackdoor",
				"Karba",
				"Nemain",
				"Nemim",
				"Ramsay",
				"Retro",
				"Tapaoux",
				"Trojan.Win32.Karba.e",
				"Virus.Win32.Pioneer.dx",
				"igfxext.exe",
				"msieckc.exe"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "18a7b52d-a1cd-43a3-8982-7324e3e676b7",
			"created_at": "2025-08-07T02:03:24.688416Z",
			"updated_at": "2026-04-10T02:00:03.734754Z",
			"deleted_at": null,
			"main_name": "BRONZE UNIVERSITY",
			"aliases": [
				"Aquatic Panda",
				"Aquatic Panda ",
				"CHROMIUM",
				"CHROMIUM ",
				"Charcoal Typhoon",
				"Charcoal Typhoon ",
				"Earth Lusca",
				"Earth Lusca ",
				"FISHMONGER ",
				"Red Dev 10",
				"Red Dev 10 ",
				"Red Scylla",
				"Red Scylla ",
				"RedHotel",
				"RedHotel ",
				"Tag-22",
				"Tag-22 "
			],
			"source_name": "Secureworks:BRONZE UNIVERSITY",
			"tools": [
				"Cobalt Strike",
				"Fishmaster",
				"FunnySwitch",
				"Spyder",
				"njRAT"
			],
			"source_id": "Secureworks",
			"reports": null
		},
		{
			"id": "6abcc917-035c-4e9b-a53f-eaee636749c3",
			"created_at": "2022-10-25T16:07:23.565337Z",
			"updated_at": "2026-04-10T02:00:04.668393Z",
			"deleted_at": null,
			"main_name": "Earth Lusca",
			"aliases": [
				"Bronze University",
				"Charcoal Typhoon",
				"Chromium",
				"G1006",
				"Red Dev 10",
				"Red Scylla"
			],
			"source_name": "ETDA:Earth Lusca",
			"tools": [
				"Agentemis",
				"AntSword",
				"BIOPASS",
				"BIOPASS RAT",
				"BadPotato",
				"Behinder",
				"BleDoor",
				"Cobalt Strike",
				"CobaltStrike",
				"Doraemon",
				"FRP",
				"Fast Reverse Proxy",
				"FunnySwitch",
				"HUC Port Banner Scanner",
				"KTLVdoor",
				"Mimikatz",
				"NBTscan",
				"POISONPLUG.SHADOW",
				"PipeMon",
				"RbDoor",
				"RibDoor",
				"RouterGod",
				"SAMRID",
				"ShadowPad Winnti",
				"SprySOCKS",
				"WinRAR",
				"Winnti",
				"XShellGhost",
				"cobeacon",
				"fscan",
				"lcx",
				"nbtscan"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "d53593c3-2819-4af3-bf16-0c39edc64920",
			"created_at": "2022-10-27T08:27:13.212301Z",
			"updated_at": "2026-04-10T02:00:05.272802Z",
			"deleted_at": null,
			"main_name": "Earth Lusca",
			"aliases": [
				"Earth Lusca",
				"TAG-22",
				"Charcoal Typhoon",
				"CHROMIUM",
				"ControlX"
			],
			"source_name": "MITRE:Earth Lusca",
			"tools": [
				"Mimikatz",
				"PowerSploit",
				"Tasklist",
				"certutil",
				"Cobalt Strike",
				"Winnti for Linux",
				"Nltest",
				"NBTscan",
				"ShadowPad"
			],
			"source_id": "MITRE",
			"reports": null
		}
	],
	"ts_created_at": 1775434471,
	"ts_updated_at": 1775792207,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/4224ea36c73ee2b8d684b96178970e476380f63f.pdf",
		"text": "https://archive.orkl.eu/4224ea36c73ee2b8d684b96178970e476380f63f.txt",
		"img": "https://archive.orkl.eu/4224ea36c73ee2b8d684b96178970e476380f63f.jpg"
	}
}