{
	"id": "2bd3a687-9ec9-461a-902b-efaa8039ee99",
	"created_at": "2026-04-06T00:08:30.951657Z",
	"updated_at": "2026-04-10T03:35:28.979965Z",
	"deleted_at": null,
	"sha1_hash": "8afd919a407acc8fa64c172b2e7d7350cef64f54",
	"title": "A deep dive into an NSO zero-click iMessage exploit: Remote Code Execution",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 68650,
	"plain_text": "A deep dive into an NSO zero-click iMessage exploit: Remote Code\r\nExecution\r\nArchived: 2026-04-05 16:33:55 UTC\r\nPosted by Ian Beer \u0026 Samuel Groß of Google Project Zero\r\nWe want to thank Citizen Lab for sharing a sample of the FORCEDENTRY exploit with us, and Apple’s Security\r\nEngineering and Architecture (SEAR) group for collaborating with us on the technical analysis. The editorial\r\nopinions reflected below are solely Project Zero’s and do not necessarily reflect those of the organizations we\r\ncollaborated with during this research.\r\nEarlier this year, Citizen Lab managed to capture an NSO iMessage-based zero-click exploit being used to target a\r\nSaudi activist. In this two-part blog post series we will describe for the first time how an in-the-wild zero-click\r\niMessage exploit works.\r\nBased on our research and findings, we assess this to be one of the most technically sophisticated exploits we've\r\never seen, further demonstrating that the capabilities NSO provides rival those previously thought to be accessible\r\nto only a handful of nation states.\r\nThe vulnerability discussed in this blog post was fixed on September 13, 2021 in iOS 14.8 as CVE-2021-30860.\r\nNSO\r\nNSO Group is one of the highest-profile providers of \"access-as-a-service\", selling packaged hacking solutions\r\nwhich enable nation state actors without a home-grown offensive cyber capability to \"pay-to-play\", vastly\r\nexpanding the number of nations with such cyber capabilities.\r\nFor years, groups like Citizen Lab and Amnesty International have been tracking the use of NSO's mobile spyware\r\npackage \"Pegasus\". Despite NSO's claims that they \"[evaluate] the potential for adverse human rights impacts\r\narising from the misuse of NSO products\" Pegasus has been linked to the hacking of the New York Times\r\njournalist Ben Hubbard by the Saudi regime, hacking of human rights defenders in Morocco and Bahrain, the\r\ntargeting of Amnesty International staff and dozens of other cases.\r\nLast month the United States added NSO to the \"Entity List\", severely restricting the ability of US companies to\r\ndo business with NSO and stating in a press release that \"[NSO's tools] enabled foreign governments to conduct\r\ntransnational repression, which is the practice of authoritarian governments targeting dissidents, journalists and\r\nactivists outside of their sovereign borders to silence dissent.\"\r\nCitizen Lab was able to recover these Pegasus exploits from an iPhone and therefore this analysis covers NSO's\r\ncapabilities against iPhone. We are aware that NSO sells similar zero-click capabilities which target Android\r\ndevices; Project Zero does not have samples of these exploits but if you do, please reach out.\r\nFrom One to Zero\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 1 of 9\n\nIn previous cases such as the Million Dollar Dissident from 2016, targets were sent links in SMS messages:\r\nScreenshots of Phishing SMSs reported to Citizen Lab in 2016\r\nsource: https://citizenlab.ca/2016/08/million-dollar-dissident-iphone-zero-day-nso-group-uae/\r\nThe target was only hacked when they clicked the link, a technique known as a one-click exploit. Recently,\r\nhowever, it has been documented that NSO is offering their clients zero-click exploitation technology, where even\r\nvery technically savvy targets who might not click a phishing link are completely unaware they are being targeted.\r\nIn the zero-click scenario no user interaction is required. Meaning, the attacker doesn't need to send phishing\r\nmessages; the exploit just works silently in the background. Short of not using a device, there is no way to prevent\r\nexploitation by a zero-click exploit; it's a weapon against which there is no defense.\r\nOne weird trick\r\nThe initial entry point for Pegasus on iPhone is iMessage. This means that a victim can be targeted just using their\r\nphone number or AppleID username.\r\niMessage has native support for GIF images, the typically small and low quality animated images popular in\r\nmeme culture. You can send and receive GIFs in iMessage chats and they show up in the chat window. Apple\r\nwanted to make those GIFs loop endlessly rather than only play once, so very early on in the iMessage parsing\r\nand processing pipeline (after a message has been received but well before the message is shown), iMessage calls\r\nthe following method in the IMTranscoderAgent process (outside the \"BlastDoor\" sandbox), passing any image\r\nfile received with the extension .gif:\r\n  [IMGIFUtils copyGifFromPath:toDestinationPath:error]\r\nLooking at the selector name, the intention here was probably to just copy the GIF file before editing the loop\r\ncount field, but the semantics of this method are different. Under the hood it uses the CoreGraphics APIs to\r\nrender the source image to a new GIF file at the destination path. And just because the source filename has to end\r\nin .gif, that doesn't mean it's really a GIF file.\r\nThe ImageIO library, as detailed in a previous Project Zero blogpost, is used to guess the correct format of the\r\nsource file and parse it, completely ignoring the file extension. Using this \"fake gif\" trick, over 20 image codecs\r\nare suddenly part of the iMessage zero-click attack surface, including some very obscure and complex formats,\r\nremotely exposing probably hundreds of thousands of lines of code.\r\nNote: Apple inform us that they have restricted the available ImageIO formats reachable from IMTranscoderAgent\r\nstarting in iOS 14.8.1 (26 October 2021), and completely removed the GIF code path from IMTranscoderAgent\r\nstarting in iOS 15.0 (20 September 2021), with GIF decoding taking place entirely within BlastDoor.\r\nA PDF in your GIF\r\nNSO uses the \"fake gif\" trick to target a vulnerability in the CoreGraphics PDF parser.\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 2 of 9\n\nPDF was a popular target for exploitation around a decade ago, due to its ubiquity and complexity. Plus, the\r\navailability of javascript inside PDFs made development of reliable exploits far easier. The CoreGraphics PDF\r\nparser doesn't seem to interpret javascript, but NSO managed to find something equally powerful inside the\r\nCoreGraphics PDF parser...\r\nExtreme compression\r\nIn the late 1990's, bandwidth and storage were much more scarce than they are now. It was in that environment\r\nthat the JBIG2 standard emerged. JBIG2 is a domain specific image codec designed to compress images where\r\npixels can only be black or white.\r\nIt was developed to achieve extremely high compression ratios for scans of text documents and was implemented\r\nand used in high-end office scanner/printer devices like the XEROX WorkCenter device shown below. If you used\r\nthe scan to pdf functionality of a device like this a decade ago, your PDF likely had a JBIG2 stream in it.\r\nA Xerox WorkCentre 7500 series multifunction printer, which used JBIG2\r\nfor its scan-to-pdf functionality\r\nsource: https://www.office.xerox.com/en-us/multifunction-printers/workcentre-7545-7556/specifications\r\nThe PDFs files produced by those scanners were exceptionally small, perhaps only a few kilobytes. There are two\r\nnovel techniques which JBIG2 uses to achieve these extreme compression ratios which are relevant to this exploit:\r\nTechnique 1: Segmentation and substitution\r\nEffectively every text document, especially those written in languages with small alphabets like English or\r\nGerman, consists of many repeated letters (also known as glyphs) on each page. JBIG2 tries to segment each page\r\ninto glyphs then uses simple pattern matching to match up glyphs which look the same:\r\nSimple pattern matching can find all the shapes which look similar on a page,\r\nin this case all the 'e's\r\nJBIG2 doesn't actually know anything about glyphs and it isn't doing OCR (optical character recognition.) A JBIG\r\nencoder is just looking for connected regions of pixels and grouping similar looking regions together. The\r\ncompression algorithm is to simply substitute all sufficiently-similar looking regions with a copy of just one of\r\nthem:\r\nReplacing all occurrences of similar glyphs with a copy of just one often yields a document which is still quite\r\nlegible and enables very high compression ratios\r\nIn this case the output is perfectly readable but the amount of information to be stored is significantly reduced.\r\nRather than needing to store all the original pixel information for the whole page you only need a compressed\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 3 of 9\n\nversion of the \"reference glyph\" for each character and the relative coordinates of all the places where copies\r\nshould be made. The decompression algorithm then treats the output page like a canvas and \"draws\" the exact\r\nsame glyph at all the stored locations.\r\nThere's a significant issue with such a scheme: it's far too easy for a poor encoder to accidentally swap similar\r\nlooking characters, and this can happen with interesting consequences. D. Kriesel's blog has some motivating\r\nexamples where PDFs of scanned invoices have different figures or PDFs of scanned construction drawings end\r\nup with incorrect measurements. These aren't the issues we're looking at, but they are one significant reason why\r\nJBIG2 is not a common compression format anymore.\r\nTechnique 2: Refinement coding\r\nAs mentioned above, the substitution based compression output is lossy. After a round of compression and\r\ndecompression the rendered output doesn't look exactly like the input. But JBIG2 also supports lossless\r\ncompression as well as an intermediate \"less lossy\" compression mode.\r\nIt does this by also storing (and compressing) the difference between the substituted glyph and each original\r\nglyph. Here's an example showing a difference mask between a substituted character on the left and the original\r\nlossless character in the middle:\r\nUsing the XOR operator on bitmaps to compute a difference image\r\nIn this simple example the encoder can store the difference mask shown on the right, then during decompression\r\nthe difference mask can be XORed with the substituted character to recover the exact pixels making up the\r\noriginal character. There are some more tricks outside of the scope of this blog post to further compress that\r\ndifference mask using the intermediate forms of the substituted character as a \"context\" for the compression.\r\nRather than completely encoding the entire difference in one go, it can be done in steps, with each iteration using a\r\nlogical operator (one of AND, OR, XOR or XNOR) to set, clear or flip bits. Each successive refinement step\r\nbrings the rendered output closer to the original and this allows a level of control over the \"lossiness\" of the\r\ncompression. The implementation of these refinement coding steps is very flexible and they are also able to \"read\"\r\nvalues already present on the output canvas.\r\nA JBIG2 stream\r\nMost of the CoreGraphics PDF decoder appears to be Apple proprietary code, but the JBIG2 implementation is\r\nfrom Xpdf, the source code for which is freely available.\r\nThe JBIG2 format is a series of segments, which can be thought of as a series of drawing commands which are\r\nexecuted sequentially in a single pass. The CoreGraphics JBIG2 parser supports 19 different segment types which\r\ninclude operations like defining a new page, decoding a huffman table or rendering a bitmap to given coordinates\r\non the page.\r\nSegments are represented by the class JBIG2Segment and its subclasses JBIG2Bitmap and JBIG2SymbolDict.\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 4 of 9\n\nA JBIG2Bitmap represents a rectangular array of pixels. Its data field points to a backing-buffer containing the\r\nrendering canvas.\r\nA JBIG2SymbolDict groups JBIG2Bitmaps together. The destination page is represented as a JBIG2Bitmap, as\r\nare individual glyphs.\r\nJBIG2Segments can be referred to by a segment number and the GList vector type stores pointers to all the\r\nJBIG2Segments. To look up a segment by segment number the GList is scanned sequentially.\r\nThe vulnerability\r\nThe vulnerability is a classic integer overflow when collating referenced segments:\r\n  Guint numSyms; // (1)\r\n  numSyms = 0;\r\n  for (i = 0; i \u003c nRefSegs; ++i) {\r\n    if ((seg = findSegment(refSegs[i]))) {\r\n      if (seg-\u003egetType() == jbig2SegSymbolDict) {\r\n        numSyms += ((JBIG2SymbolDict *)seg)-\u003egetSize();  // (2)\r\n      } else if (seg-\u003egetType() == jbig2SegCodeTable) {\r\n        codeTables-\u003eappend(seg);\r\n      }\r\n    } else {\r\n      error(errSyntaxError, getPos(),\r\n            \"Invalid segment reference in JBIG2 text region\");\r\n      delete codeTables;\r\n      return;\r\n    }\r\n  }\r\n...\r\n  // get the symbol bitmaps\r\n  syms = (JBIG2Bitmap **)gmallocn(numSyms, sizeof(JBIG2Bitmap *)); // (3)\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 5 of 9\n\nkk = 0;\r\n  for (i = 0; i \u003c nRefSegs; ++i) {\r\n    if ((seg = findSegment(refSegs[i]))) {\r\n      if (seg-\u003egetType() == jbig2SegSymbolDict) {\r\n        symbolDict = (JBIG2SymbolDict *)seg;\r\n        for (k = 0; k \u003c symbolDict-\u003egetSize(); ++k) {\r\n          syms[kk++] = symbolDict-\u003egetBitmap(k); // (4)\r\n        }\r\n      }\r\n    }\r\n  }\r\nnumSyms is a 32-bit integer declared at (1). By supplying carefully crafted reference segments it's possible for the\r\nrepeated addition at (2) to cause numSyms to overflow to a controlled, small value.\r\nThat smaller value is used for the heap allocation size at (3) meaning syms points to an undersized buffer.\r\nInside the inner-most loop at (4) JBIG2Bitmap pointer values are written into the undersized syms buffer.\r\nWithout another trick this loop would write over 32GB of data into the undersized syms buffer, certainly causing a\r\ncrash. To avoid that crash the heap is groomed such that the first few writes off of the end of the syms buffer\r\ncorrupt the GList backing buffer. This GList stores all known segments and is used by the findSegments routine to\r\nmap from the segment numbers passed in refSegs to JBIG2Segment pointers. The overflow causes the\r\nJBIG2Segment pointers in the GList to be overwritten with JBIG2Bitmap pointers at (4).\r\nConveniently since JBIG2Bitmap inherits from JBIG2Segment the seg-\u003egetType() virtual call succeed even on\r\ndevices where Pointer Authentication is enabled (which is used to perform a weak type check on virtual calls) but\r\nthe returned type will now not be equal to jbig2SegSymbolDict thus causing further writes at (4) to not be reached\r\nand bounding the extent of the memory corruption.\r\nA simplified view of the memory layout when the heap overflow occurs showing the undersized-buffer below the\r\nGList backing buffer and the JBIG2Bitmap\r\nBoundless unbounding\r\nDirectly after the corrupted segments GList, the attacker grooms the JBIG2Bitmap object which represents the\r\ncurrent page (the place to where current drawing commands render).\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 6 of 9\n\nJBIG2Bitmaps are simple wrappers around a backing buffer, storing the buffer’s width and height (in bits) as well\r\nas a line value which defines how many bytes are stored for each line.\r\nThe memory layout of the JBIG2Bitmap object showing the segnum, w, h and line fields which are corrupted\r\nduring the overflow\r\nBy carefully structuring refSegs they can stop the overflow after writing exactly three more JBIG2Bitmap pointers\r\nafter the end of the segments GList buffer. This overwrites the vtable pointer and the first four fields of the\r\nJBIG2Bitmap representing the current page. Due to the nature of the iOS address space layout these pointers are\r\nvery likely to be in the second 4GB of virtual memory, with addresses between 0x100000000 and 0x1ffffffff.\r\nSince all iOS hardware is little endian (meaning that the w and line fields are likely to be overwritten with 0x1 —\r\nthe most-significant half of a JBIG2Bitmap pointer) and the segNum and h fields are likely to be overwritten with\r\nthe least-significant half of such a pointer, a fairly random value depending on heap layout and ASLR somewhere\r\nbetween 0x100000 and 0xffffffff.\r\nThis gives the current destination page JBIG2Bitmap an unknown, but very large, value for h. Since that h value is\r\nused for bounds checking and is supposed to reflect the allocated size of the page backing buffer, this has the\r\neffect of \"unbounding\" the drawing canvas. This means that subsequent JBIG2 segment commands can read and\r\nwrite memory outside of the original bounds of the page backing buffer.\r\nThe heap groom also places the current page's backing buffer just below the undersized syms buffer, such that\r\nwhen the page JBIG2Bitmap is unbounded, it's able to read and write its own fields:\r\nThe memory layout showing how the unbounded bitmap backing buffer is able to reference the JBIG2Bitmap\r\nobject and modify fields in it as it is located after the backing buffer in memory\r\nBy rendering 4-byte bitmaps at the correct canvas coordinates they can write to all the fields of the page\r\nJBIG2Bitmap and by carefully choosing new values for w, h and line, they can write to arbitrary offsets from the\r\npage backing buffer.\r\nAt this point it would also be possible to write to arbitrary absolute memory addresses if you knew their offsets\r\nfrom the page backing buffer. But how to compute those offsets? Thus far, this exploit has proceeded in a manner\r\nvery similar to a \"canonical\" scripting language exploit which in Javascript might end up with an unbounded\r\nArrayBuffer object with access to memory. But in those cases the attacker has the ability to run arbitrary\r\nJavascript which can obviously be used to compute offsets and perform arbitrary computations. How do you do\r\nthat in a single-pass image parser?\r\nMy other compression format is turing-complete!\r\nAs mentioned earlier, the sequence of steps which implement JBIG2 refinement are very flexible. Refinement\r\nsteps can reference both the output bitmap and any previously created segments, as well as render output to either\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 7 of 9\n\nthe current page or a segment. By carefully crafting the context-dependent part of the refinement decompression,\r\nit's possible to craft sequences of segments where only the refinement combination operators have any effect.\r\nIn practice this means it is possible to apply the AND, OR, XOR and XNOR logical operators between memory\r\nregions at arbitrary offsets from the current page's JBIG2Bitmap backing buffer. And since that has been\r\nunbounded… it's possible to perform those logical operations on memory at arbitrary out-of-bounds offsets:\r\nThe memory layout showing how logical operators can be applied out-of-bounds\r\nIt's when you take this to its most extreme form that things start to get really interesting. What if rather than\r\noperating on glyph-sized sub-rectangles you instead operated on single bits?\r\nYou can now provide as input a sequence of JBIG2 segment commands which implement a sequence of logical bit\r\noperations to apply to the page. And since the page buffer has been unbounded those bit operations can operate on\r\narbitrary memory.\r\nWith a bit of back-of-the-envelope scribbling you can convince yourself that with just the available AND, OR,\r\nXOR and XNOR logical operators you can in fact compute any computable function - the simplest proof being\r\nthat you can create a logical NOT operator by XORing with 1 and then putting an AND gate in front of that to\r\nform a NAND gate:\r\nAn AND gate connected to one input of an XOR gate. The other XOR gate input is connected to the constant\r\nvalue 1 creating an NAND.\r\nA NAND gate is an example of a universal logic gate; one from which all other gates can be built and from which\r\na circuit can be built to compute any computable function.\r\nPractical circuits\r\nJBIG2 doesn't have scripting capabilities, but when combined with a vulnerability, it does have the ability to\r\nemulate circuits of arbitrary logic gates operating on arbitrary memory. So why not just use that to build your\r\nown computer architecture and script that!? That's exactly what this exploit does. Using over 70,000 segment\r\ncommands defining logical bit operations, they define a small computer architecture with features such as registers\r\nand a full 64-bit adder and comparator which they use to search memory and perform arithmetic operations. It's\r\nnot as fast as Javascript, but it's fundamentally computationally equivalent.\r\nThe bootstrapping operations for the sandbox escape exploit are written to run on this logic circuit and the whole\r\nthing runs in this weird, emulated environment created out of a single decompression pass through a JBIG2\r\nstream. It's pretty incredible, and at the same time, pretty terrifying.\r\nIn a future post (currently being finished), we'll take a look at exactly how they escape the\r\nIMTranscoderAgent sandbox.\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 8 of 9\n\nSource: https://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nhttps://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html\r\nPage 9 of 9\n\ninclude operations on the page. like defining a new page, decoding a huffman table or rendering a bitmap to given coordinates\nSegments are represented by the class JBIG2Segment and its subclasses JBIG2Bitmap and JBIG2SymbolDict.\n   Page 4 of 9",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"ETDA",
		"Malpedia"
	],
	"references": [
		"https://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html"
	],
	"report_names": [
		"a-deep-dive-into-nso-zero-click.html"
	],
	"threat_actors": [
		{
			"id": "42a6a29d-6b98-4fd6-a742-a45a0306c7b0",
			"created_at": "2022-10-25T15:50:23.710403Z",
			"updated_at": "2026-04-10T02:00:05.281246Z",
			"deleted_at": null,
			"main_name": "Silence",
			"aliases": [
				"Whisper Spider"
			],
			"source_name": "MITRE:Silence",
			"tools": [
				"Winexe",
				"SDelete"
			],
			"source_id": "MITRE",
			"reports": null
		},
		{
			"id": "75108fc1-7f6a-450e-b024-10284f3f62bb",
			"created_at": "2024-11-01T02:00:52.756877Z",
			"updated_at": "2026-04-10T02:00:05.273746Z",
			"deleted_at": null,
			"main_name": "Play",
			"aliases": null,
			"source_name": "MITRE:Play",
			"tools": [
				"Nltest",
				"AdFind",
				"PsExec",
				"Wevtutil",
				"Cobalt Strike",
				"Playcrypt",
				"Mimikatz"
			],
			"source_id": "MITRE",
			"reports": null
		},
		{
			"id": "eb5915d6-49a0-464d-9e4e-e1e2d3d31bc7",
			"created_at": "2025-03-29T02:05:20.764715Z",
			"updated_at": "2026-04-10T02:00:03.851829Z",
			"deleted_at": null,
			"main_name": "GOLD WYMAN",
			"aliases": [
				"Silence "
			],
			"source_name": "Secureworks:GOLD WYMAN",
			"tools": [
				"Silence"
			],
			"source_id": "Secureworks",
			"reports": null
		},
		{
			"id": "88e53203-891a-46f8-9ced-81d874a271c4",
			"created_at": "2022-10-25T16:07:24.191982Z",
			"updated_at": "2026-04-10T02:00:04.895327Z",
			"deleted_at": null,
			"main_name": "Silence",
			"aliases": [
				"ATK 86",
				"Contract Crew",
				"G0091",
				"TAG-CR8",
				"TEMP.TruthTeller",
				"Whisper Spider"
			],
			"source_name": "ETDA:Silence",
			"tools": [
				"EDA",
				"EmpireDNSAgent",
				"Farse",
				"Ivoke",
				"Kikothac",
				"LOLBAS",
				"LOLBins",
				"Living off the Land",
				"Meterpreter",
				"ProxyBot",
				"ReconModule",
				"Silence.Downloader",
				"TiniMet",
				"TinyMet",
				"TrueBot",
				"xfs-disp.exe"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434110,
	"ts_updated_at": 1775792128,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/8afd919a407acc8fa64c172b2e7d7350cef64f54.pdf",
		"text": "https://archive.orkl.eu/8afd919a407acc8fa64c172b2e7d7350cef64f54.txt",
		"img": "https://archive.orkl.eu/8afd919a407acc8fa64c172b2e7d7350cef64f54.jpg"
	}
}