{
	"id": "ca1fec93-5648-48dd-abb3-172c2a6dbe6f",
	"created_at": "2026-04-06T00:19:42.985246Z",
	"updated_at": "2026-04-10T03:23:51.164839Z",
	"deleted_at": null,
	"sha1_hash": "7764ef040e08f3da8996f08b2774ad245bc11f0a",
	"title": "Analysis of CVE-2021-30860",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1069425,
	"plain_text": "Analysis of CVE-2021-30860\r\nArchived: 2026-04-05 21:33:01 UTC\r\nAnalysis of CVE-2021-30860\r\nthe flaw and fix of a zero-click vulnerability, exploited in the wild\r\nby: Tom McGuire / September 16, 2021\r\nObjective-See's research, tools, and writing, are supported by the \"Friends of Objective-See\" such as:\r\nThis guest blog post, was written by Tom McGuire, a senior instructor and cybersecurity focus area coordinator at\r\nJohns Hopkins and tech editor of my upcoming The Art of Mac Malware: Analysis book.\r\nHere, he shares his analysis of reversing Apple's patch for CVE-2021-30860 (a zero-click iOS/macOS\r\nvulnerability exploited in the wild) ...highlighting both the underlying flaw, and Apple's fix.\r\nMahalo for sharing Tom! 🤩\r\nWild, wild west - Quick Initial Analysis of CVE-2021-30860\r\nRecently, Apple released iOS/iPadOS 14.8 and macOS Big Sur 11.6 which fixes both an integer overflow and a\r\nuse after free vulnerability (the watchOS platform was also patched to fix the integer overflow issue). This blog\r\npost will analyze the integer overflow in CoreGraphics, CVE-2021-30860 . After examining the modified .dylib, it\r\nappears that there were other issues that were resolved as well, related to imaging processing. We will focus in on\r\nthe JBIG2 processing, specifically in the JBIG2::readTextRegionSeg .\r\nI could not find information about Apple’s use of JBIG2 libraries. However, as we will see there is a likely\r\nchance there was some collaboration with open source software (see:\r\nhttps://gitlab.freedesktop.org/poppler/poppler/-/blob/master/poppler/JBIG2Stream.cc). The source code shown is\r\nfrom poppler, but as shown in the header file the origin is “Copyright 2002-2003 Glyph \u0026 Cog, LLC”.\r\nAn integer overflow can lead to a variety of issues. A common result with an integer overflow is to cause a\r\ndynamic memory allocation (e.g. malloc(), calloc() etc..) to be too small. Later, data is copied from a source that is\r\nlarger than the allocated size, resulting in a heap buffer overflow. (Not all integer overflows will manifest this way,\r\nbut it is a common occurrence and relevant to this discussion.)\r\nCVE-2021-30860 is an integer overflow in the CoreGraphics component, specifically the decoding of a JBIG2\r\ndata. JBIG2 (Joint Bi-level Image Experts Group) is an image compression format which can be embedded as a\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 1 of 15\n\nstream in a PDF or PSD document, or potentially other formats as well. You can read more about it here.\r\nBefore we dive into the assembly and uncover the vulnerability and how it was fixed, we want to look at the\r\ndiscovery. CitizenLab reported this vulnerability, which they dubbed FORCEDENTRY (a knock at Apple’s recent\r\nsecurity component, BlastDoor!), to Apple after they had done some analysis on journalist’s phones suspected of\r\nbeing hacked. In their reporting, CitizenLab attributes the attacks to the NSO group, due to the Pegasus software\r\nthat was seen on these infected devices:\r\nCitizenLab thoughts on Pegaus\r\nDuring their analysis, they uncovered crash logs and noticed quite a few image files that seemed to crash the\r\nIMTranscoderAgent . IMTranscoderAgent is one of the components related to processing of iMessage data,\r\nincluding upon sending/receiving images!\r\nAccording to CitizenLab, they reported the vulnerability to Apple on Tuesday, September 7, 2021 and Apple\r\nconfirmed and released the patches for the issue on Monday, September 13, 2021. That is a quick turnaround, so\r\nlet’s see how well they did with the patching!\r\nImage file formats are notorious for having vulnerabilities that can lead to arbitrary remote code execution (RCE)\r\non devices (CVE-2009-1858, CVE-2015-6778, CVE-2020-1910, etc..).\r\nImaging parsing issues are not new!\r\nIt is not surprising that such an issue existed here. With this JBIG2 processing vulnerability (which exists in the\r\nreadTextRegionSeg method), I will note that another very similar vulnerability was previously patched. This\r\nissue is nearly the same logic as the one in FORCEDENTRY. The method readSymbolDictSeg contains integer\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 2 of 15\n\noverflow checks that help prevent the scenario that we will examine in this post! (Don’t worry, we will get back to\r\nthis and do a quick look to see this in assembly).\r\nOf particular note to the attacks reported by CitizenLab, the file formats were PDF files, with embedded JBIG2\r\nstreams. Zero-click iMessage vulnerabilities have existed before (see, here and here).\r\nIn an effort to help reduce this attack surface, Apple recently (iOS14) introduced the “BlastDoor” feature. Samuel\r\nGroß, of Google’s Project 0, posted an excellent write-up about this new feature:\r\nBlastDoor analysis by Google P0\r\nFor our purposes, what we need to understand is that the BlastDoor feature is meant to “sandbox” processing in\r\nthe iMessage chain. In other words, when an image or document is received via iMessage and automatically\r\nparsed, it is done in a sandboxed environment. The intent is that, if a vulnerability exists in some of the processing\r\nengine, the exploitation will be limited to this sandboxed environment, keeping the rest of the system ‘safe’. This\r\nis true for certain file formats, but it appears that Apple did not sandbox all potential automatically parsed formats\r\n(looking at you PSD files, and likely other raster formats).\r\nThough I have not gone through and analyzed any changes to BlastDoor since this patch, I can only hope that\r\nApple has increased the robustness of BlastDoor and has prevented PDF, PSD and other raster file format parsing\r\nfrom going through the IMTranscoderAgent . That is, going forward, the hope is these other notoriously prone\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 3 of 15\n\nformats are processed in the BlastDoor sandboxed environment…perhaps we can look at that in a future blog\r\npost!\r\nWith the background out of the way, let’s get to reversing and find out what happened and how it was fixed!\r\nIn order to examine this, we first need to grab a vulnerable version of the .dylib (we will be using macOS\r\n11.5.2) and a fixed version (macOS 11.6). I had Hopper and IDA for analysis as well, so for the sake of time, I\r\nutilized them both. First, we need to grab the CoreGraphics.dylib from the two systems. At first, I was looking\r\nin the usual spot ( /System/Library/Frameworks/CoreGraphics ) and quickly noticed this was not the correct\r\nlibrary. It turns out that on recent versions of macOS, many of the core frameworks are located in the dyld cache!\r\nThis is a very large file, but is located in /System/Library/dyld/dyld_shared_cache_x86_64 . (I’m using the\r\nx86_64 version).\r\ndyld cache from the respective folders\r\nArmed with the knowledge of where the dyld cache is located, we need to extract the CoreGraphics.dylib from\r\nit. One of the simplest ways is to use the Hopper disassembler. Opening the dyld_shared_cache_x86_64 file in\r\nHopper presents you with myriad of Frameworks to examine. Of course, we will filter on the “CoreGraphics” one\r\nto open it up.\r\nHopper opening dyld_cache\r\nFrom here, I was most interested in learning the differences between the 11.5.2 version and the 11.6 version. At\r\nthis point, I decided to use Hopper to output the CoreGraphics.dylib to its own dedicated Mach-O file. To do\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 4 of 15\n\nthis, we can use the “File-\u003eProduce New Executable” (or cmd+shift+e ). Doing this for both the 11.5.2\r\ndyld_cache and the 11.6 dyld_cache yields us the two CoreGraphics.dylib that we can easily analyze.\r\nHopper producing new executable\r\nIn order to diff these quickly, I decided to utilize IDA and BinDiff (we certainly can use other tools as well). So\r\nlet’s open both CoreGraphics-11_5_2.dylib and CoreGraphics-11_6.dylib in IDA, saving the corresponding\r\n.i64 files. After closing both databases, I re-opened the CoreGraphics-11_5_2.dylib and launched BinDiff\r\n( ctrl + 6 ). After choosing “Diff Database…” and selecting the CoreGraphics-11_6.i64 database, we wait for\r\nBinDiff to do its magic! It’s not that bad actually. If you’ve not used BinDiff, the matching functions is quite\r\nuseful. It also gives a guide for what has changed within a function. The BinDiff manual, from Zynamics site,\r\ngives a good description of the “Matched Functions Subview” and explains the “change” column.\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 5 of 15\n\nBinDiff\r\nOpen BinDiff from the 11.5.2 version (primary) and use the .i64 db for the 11.6 version (secondary)\r\nWe notice that there are 10 functions that have changed. It turns out that there was an API change or parameter\r\nsize change to one of them (one of the parameters was removed), thus 4 of these functions aren’t as “different” as\r\nthey first appear. In Figure 8 below, the left most column is the similarity. 1.00 is identical* while lower values are\r\nless similar. We notice that there are a few entries with 0.99 similarity. These functions are mostly similar up to\r\nvariance of some number of Instructions (I). The 0.92 similar function is the one of interest to us (some of the\r\nother functions are also worth examining…perhaps for another blogpost!). The “G”, in the 3rd column (change)\r\nindicates there is a graph change (number of basic blocks or number of edges differ). There are also differences in\r\nthe branch inversion, indicated by the “J”. The “L” indicates the number of loops has changed. The graph structure\r\nis an important change to look at, as this could indicate a new branch condition was added or altered!\r\nShowing the differences to focus our analysis!\r\nFor this post, the most interesting function related to the JBIG2 processing that differs between the 2 versions is\r\nlocated at: 00007FFF252466E0 (11.5.2 version) and 00007FFF25247710 (11.6 version) (In Figure 8, this is the\r\nreadTextRegionSeg_0 named routine).\r\nThis is the JBIG2::readTextRegionSeg function. As you can see, I didn’t have symbols when doing this,\r\nhowever, I did notice some interesting strings present in the CoreGraphics.dylib , which turned out to be very\r\nuseful in piecing together the code paths (obviously symbols would greatly help here, but even without them, we\r\ncan identify the root cause…with a little help from open source software!)\r\nUtilizing the strings located in the dylib, and the source code for a JBIG2Stream processor , we can match up\r\nsome of the code!\r\nUsing source code as a guide, we can look at the issue in source and then match it to the disassembled version\r\nconfirming the existence of the vulnerability in the 11.5.2 version.\r\nAs we can see below, the numSyms variable (an unsigned 32-bit integer), is incremented by the size of the\r\ncurrently processed segment. Thus, if there is more than one jbig2SegSymbolDict segment, numSyms will be\r\nupdated with the size of that segment. This can lead to an integer overflow as there is no checking surrounding\r\nthis area.\r\n 1966 // get symbol dictionaries and tables\r\n 1967 numSyms = 0;\r\n 1968 for (i = 0; i \u003c nRefSegs; ++i) {\r\n 1969 if ((seg = findSegment(refSegs[i]))) {\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 6 of 15\n\n1970 if (seg-\u003egetType() == jbig2SegSymbolDict) {\r\n 1971 numSyms += ((JBIG2SymbolDict *)seg)-\u003egetSize();\r\n 1972 } else if (seg-\u003egetType() == jbig2SegCodeTable) {\r\n 1973 codeTables.push_back(seg);\r\n 1974 }\r\n 1975 } else {\r\n 1976 error(errSyntaxError, curStr-\u003egetPos(),\r\n \"Invalid segment reference in JBIG2 text region\");\r\n 1977 return;\r\n 1978 }\r\n 1979 }\r\nAs you can see from the disassembly below from the vulnerable version (11.5.2), the add eax, [rbx+0ch]\r\n(which is a 32-bit calculation), has no checking to ensure this hasn’t wrapped. Thus, we have an integer overflow\r\nin which numSyms could wrap around.\r\n__text:00007FFF25246A56 nRefSegs_loop:\r\n__text:00007FFF25246A56 mov esi, [r13+r12*4+0]\r\n__text:00007FFF25246A5B mov rdi, r14\r\n__text:00007FFF25246A5E call findSegment\r\n__text:00007FFF25246A63 test rax, rax\r\n__text:00007FFF25246A66 jz loc_7FFF25246CDD\r\n__text:00007FFF25246A6C mov rbx, rax\r\n__text:00007FFF25246A6F mov rax, [rax]\r\n__text:00007FFF25246A72 mov rdi, rbx\r\n__text:00007FFF25246A75 call qword ptr [rax+10h] ; getType()\r\n__text:00007FFF25246A78 cmp eax, 1 ; jbig2SegSymbolDict\r\n__text:00007FFF25246A7B jnz short loc_7FFF25246A8E\r\n__text:00007FFF25246A7D mov eax, dword ptr [rbp+numSyms]\r\n__text:00007FFF25246A83 add eax, [rbx+0Ch] ; numSyms += getSize()\r\n__text:00007FFF25246A83 ; no overflow check here!\r\n__text:00007FFF25246A86 mov dword ptr [rbp+numSyms], eax\r\n__text:00007FFF25246A8C jmp short loc_7FFF25246AA7\r\n__text:00007FFF25246A8E ; --------------------------------------------------------------\r\n__text:00007FFF25246A8E\r\n__text:00007FFF25246A8E loc_7FFF25246A8E:\r\n__text:00007FFF25246A8E mov rax, [rbx]\r\n__text:00007FFF25246A91 mov rdi, rbx\r\n__text:00007FFF25246A94 call qword ptr [rax+10h] ; getType()\r\n__text:00007FFF25246A97 cmp eax, 3 ; jbig2SegCodeTable\r\n__text:00007FFF25246A9A jnz short loc_7FFF25246AA7\r\n__text:00007FFF25246A9C mov rdi, r15\r\n__text:00007FFF25246A9F mov rsi, rbx\r\n__text:00007FFF25246AA2 call push_back\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 7 of 15\n\nAs we noted earlier, an integer overflow is often paired with 1 or more other mistakes. For example, it is used in\r\nan allocation routine to allocate dynamic memory. That is exactly the case here!\r\nIn the assembly below, we can see the numSyms being moved into EDI (prepping for the first argument to\r\ngmallocn ). The numSyms value is controlled by the attacker. For example, we could have one segment be\r\n0xFFFFFFFF and the other be 2. We could also use 0x80000000 and 0x80000001 . The goal, of course, is to get\r\nnumSyms to be a small number so the allocator, gmallocn , will create a small allocation.\r\n__text:00007FFF25246AC1 mov edi, dword ptr [rbp+numSyms]\r\n__text:00007FFF25246AC7 cmp edi, 2\r\n__text:00007FFF25246ACA jb short loc_7FFF25246ADB\r\n__text:00007FFF25246ACC xor ecx, ecx\r\n__text:00007FFF25246ACE mov eax, 1\r\n__text:00007FFF25246AD3\r\n__text:00007FFF25246AD3 loc_7FFF25246AD3:\r\n__text:00007FFF25246AD3 inc ecx\r\n__text:00007FFF25246AD5 add eax, eax\r\n__text:00007FFF25246AD7 cmp eax, edi\r\n__text:00007FFF25246AD9 jb short loc_7FFF25246AD3\r\n__text:00007FFF25246ADB\r\n__text:00007FFF25246ADB loc_7FFF25246ADB:\r\n__text:00007FFF25246ADB mov [rbp+var_2C4], ecx\r\n__text:00007FFF25246AE1 mov esi, 8\r\n__text:00007FFF25246AE6 call gmallocn\r\n__text:00007FFF25246AEB mov r8, rax\r\n__text:00007FFF25246AEE xor ebx, ebx\r\nIf we assume the numSyms was 1 following the overflow, gmallocn will allocate an 8-byte region for this. But\r\nwhere does this small allocation get used? And can we get more data to be copied into this buffer than was\r\nallocated?\r\nLuckily we don’t have far to go to see where there is an issue! First, we will look at the source code. We notice\r\nthat this loop has similar processing to the vulnerable overflow one. In particular, it processes the\r\njbig2SegSymbolDict segment. In this code path, we can see that the getSize method is called again and the\r\nbounds of the loop are tied to this. Since getSize returns an unsigned int (and k is already an unsigned int),\r\nthis comparison is unsigned. Thus, even if getSize is 0x80000000 , this portion will execute.\r\nAs you can see on line 2004, the syms variable receives the bitmap. This syms was the result of the gmallocn\r\nallocation. Recall that only 8-bytes were allocated, in our example. But the getSize could be much larger,\r\nresulting in a heap buffer overflow!\r\n 1998 kk = 0;\r\n 1999 for (i = 0; i \u003c nRefSegs; ++i) {\r\n 2000 if ((seg = findSegment(refSegs[i]))) {\r\n 2001 if (seg-\u003egetType() == jbig2SegSymbolDict) {\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 8 of 15\n\n2002 symbolDict = (JBIG2SymbolDict *)seg;\r\n 2003 for (k = 0; k \u003c symbolDict-\u003egetSize(); ++k) {\r\n 2004 syms[kk++] = symbolDict-\u003egetBitmap(k); \u003c-- overflow!\r\n 2005 }\r\n 2006 }\r\n 2007 }\r\n 2008 }\r\n 2009\r\nLet’s confirm the existence of this in the 11.5.2 code as well. From the code below, we can see that the\r\ngetBitmap_copyloop is unbounded! Thus, a heap buffer overflow exists!\r\n__text:00007FFF25246AF0\r\n__text:00007FFF25246AF0 loc_7FFF25246AF0:\r\n__text:00007FFF25246AF0 mov r15, r8\r\n__text:00007FFF25246AF3 mov esi, [r13+rbx*4+0]\r\n__text:00007FFF25246AF8 mov rdi, r14\r\n__text:00007FFF25246AFB call findSegment\r\n__text:00007FFF25246B00 test rax, rax\r\n__text:00007FFF25246B03 jz short loc_7FFF25246B53\r\n__text:00007FFF25246B05 mov r12, rax\r\n__text:00007FFF25246B08 mov rax, [rax]\r\n__text:00007FFF25246B0B mov rdi, r12\r\n__text:00007FFF25246B0E call qword ptr [rax+10h] ; getType()\r\n__text:00007FFF25246B11 cmp eax, 1 ; jbig2SegSymbolDict\r\n__text:00007FFF25246B14 jnz short loc_7FFF25246B53\r\n__text:00007FFF25246B16 mov eax, [r12+0Ch] ; getSize()\r\n__text:00007FFF25246B1B test rax, rax\r\n__text:00007FFF25246B1E mov r8, r15\r\n__text:00007FFF25246B21 jz short loc_7FFF25246B56\r\n__text:00007FFF25246B23 mov r9, [rbp+var_2C0]\r\n__text:00007FFF25246B2A mov edx, r9d\r\n__text:00007FFF25246B2D xor ecx, ecx\r\n__text:00007FFF25246B2F\r\n__text:00007FFF25246B2F getBitmap_copyloop:\r\n__text:00007FFF25246B2F lea esi, [rdx+rcx]\r\n__text:00007FFF25246B32 mov rdi, [r12+10h]\r\n__text:00007FFF25246B37 mov rdi, [rdi+rcx*8]\r\n__text:00007FFF25246B3B mov [r8+rsi*8], rdi ; leads to a heap overflow\r\n__text:00007FFF25246B3F inc rcx\r\n__text:00007FFF25246B42 cmp rax, rcx\r\n__text:00007FFF25246B45 jnz short getBitmap_copyloop\r\nUnfortunately, I did not have a sample to examine, so I could not confirm how the specific sample that CitizenLab\r\nhad performed the attack.\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 9 of 15\n\nThe Patch\r\nIn order to examine the fix, we need to look at the 11.6 version of the CoreGraphics.dylib . I would’ve expected\r\nto see an integer overflow check in the calculation of numSyms in the first loop. However, that is not the case.\r\nBelow is the 11.6 version of the processing loop which is identical to 11.5.2! Maybe Apple will send out a proper\r\nfix soon :-)\r\n__text:00007FFF25247A79 nRefSegs_loop:\r\n__text:00007FFF25247A79 mov esi, [r12+r14*4]\r\n__text:00007FFF25247A7D mov rdi, r13\r\n__text:00007FFF25247A80 call findSegment\r\n__text:00007FFF25247A85 test rax, rax\r\n__text:00007FFF25247A88 jz loc_7FFF25247D1D\r\n__text:00007FFF25247A8E mov rbx, rax\r\n__text:00007FFF25247A91 mov rax, [rax]\r\n__text:00007FFF25247A94 mov rdi, rbx\r\n__text:00007FFF25247A97 call qword ptr [rax+10h] ; getType()\r\n__text:00007FFF25247A9A cmp eax, 1 ; jbig2SegSymbolDict\r\n__text:00007FFF25247A9D jnz short loc_7FFF25247AB0\r\n__text:00007FFF25247A9F mov eax, [rbp+numSyms]\r\n__text:00007FFF25247AA5 add eax, [rbx+0Ch] ; numSysm += getSize()\r\n__text:00007FFF25247AA5 ; still no overflow check!\r\n__text:00007FFF25247AA5 ; even in patched/11.6!\r\n__text:00007FFF25247AA8 mov [rbp+numSyms], eax\r\n__text:00007FFF25247AAE jmp short loc_7FFF25247ACD\r\n__text:00007FFF25247AB0 ; --------------------------------------------------------------\r\n__text:00007FFF25247AB0\r\n__text:00007FFF25247AB0 loc_7FFF25247AB0:\r\n__text:00007FFF25247AB0 mov rax, [rbx]\r\n__text:00007FFF25247AB3 mov rdi, rbx\r\n__text:00007FFF25247AB6 call qword ptr [rax+10h] ; getType()\r\n__text:00007FFF25247AB9 cmp eax, 3 ; jbig2SegSodeTable\r\n__text:00007FFF25247ABC jnz short loc_7FFF25247ACD\r\n__text:00007FFF25247ABE mov rdi, [rbp+var_2F0]\r\n__text:00007FFF25247AC5 mov rsi, rbx\r\n__text:00007FFF25247AC8 call push_back\r\nHrmm…not quite what I was expecting to see, but that’s OK..there are other changes in this function. Recall that\r\nwe noted that the integer overflow itself doesn’t always lead to an issue, but it is usually paired with 1 or more\r\nother conditions. In this case, there are 2 other conditions that lead to the exploitable case. First, as we saw, the\r\nsmall numSyms value is used to allocate a memory region. With a small allocated buffer and the second issue of\r\nthe copy loop using the larger values for its bounds (i.e. getSize ), we have a recipe for the heap buffer overflow!\r\nBased on that, and so far the fact that neither the numSyms calculation, nor the gmallocn area were changed, we\r\ncan hope that this is fixed in the copy loop! And this is exactly what happened.\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 10 of 15\n\nWe can see below that we only go into the getBitmap_copyloop for the numSyms times. But this is only half of\r\nthe problem. Since getBitmap is called in a loop, they also need to make sure that they stop the loop early there\r\nas well!\r\nYou can see that change in the getBitmap_copyloop , where they are now checking not only against the size of\r\nthe segment (seen at 00007FFF25247B6B ), but they are also checking to ensure that the data copied to that point\r\nwon’t exceed the allocated buffer size (seen at 00007FFF25247B5B and 00007FFF25247B7F )\r\n__text:00007FFF25247B23 loc_7FFF25247B23:\r\n__text:00007FFF25247B23 mov esi, [r12+r14*4]\r\n__text:00007FFF25247B27 mov rdi, r13\r\n__text:00007FFF25247B2A call findSegment\r\n__text:00007FFF25247B2F test rax, rax\r\n__text:00007FFF25247B32 jz short loc_7FFF25247B87\r\n__text:00007FFF25247B34 mov rbx, rax\r\n__text:00007FFF25247B37 mov rax, [rax]\r\n__text:00007FFF25247B3A mov rdi, rbx\r\n__text:00007FFF25247B3D call qword ptr [rax+10h] ; getType()\r\n__text:00007FFF25247B40 cmp eax, 1 ; jbig2SegSymbolDict\r\n__text:00007FFF25247B43 jnz short loc_7FFF25247B87\r\n__text:00007FFF25247B45 cmp r15d, [rbp+numSyms] ; new check to make sure we aren't\r\n__text:00007FFF25247B45 ; going beyond the number of symbols\r\n__text:00007FFF25247B45 ;\r\n__text:00007FFF25247B45 ; r15 is the 'counter' for that\r\n__text:00007FFF25247B45 ; originally set to 0\r\n__text:00007FFF25247B4C jnb short loc_7FFF25247B87\r\n__text:00007FFF25247B4E mov ecx, [rbx+0Ch] ; effectively ecx = getSize();\r\n__text:00007FFF25247B51 mov r15d, r15d\r\n__text:00007FFF25247B54 mov rdx, [rbp+orig_numSyms]\r\n__text:00007FFF25247B5B sub rdx, r15 ; keep track of symbols copied\r\n__text:00007FFF25247B5E mov rax, [rbp+var_318]\r\n__text:00007FFF25247B65 lea rsi, [rax+r15*8]\r\n__text:00007FFF25247B69 xor eax, eax ; copy loop counter\r\n__text:00007FFF25247B6B\r\n__text:00007FFF25247B6B getBitmap_copyloop:\r\n__text:00007FFF25247B6B cmp rcx, rax ; normal getSize() check\r\n__text:00007FFF25247B6E jz short loc_7FFF25247B84\r\n__text:00007FFF25247B70 mov rdi, [rbx+10h]\r\n__text:00007FFF25247B74 mov rdi, [rdi+rax*8]\r\n__text:00007FFF25247B78 mov [rsi+rax*8], rdi\r\n__text:00007FFF25247B7C inc rax\r\n__text:00007FFF25247B7F cmp rdx, rax ; this check ensures they won't write\r\n__text:00007FFF25247B7F ; out of bounds in the copy loop!\r\n__text:00007FFF25247B82 jnz short getBitmap_copyloop\r\n__text:00007FFF25247B84\r\n__text:00007FFF25247B84 loc_7FFF25247B84:\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 11 of 15\n\n__text:00007FFF25247B84 add r15d, eax\r\n__text:00007FFF25247B87\r\n__text:00007FFF25247B87 loc_7FFF25247B87:\r\n__text:00007FFF25247B87 inc r14\r\n__text:00007FFF25247B8A cmp r14, [rbp+nRefSegs]\r\n__text:00007FFF25247B91 jnz short loc_7FFF25247B23\r\nThis was certainly not the expected patch path when I first recognized the vulnerability. I would’ve thought the\r\noverflow would’ve been fixed at the point of calculation of numSyms. There may be a reason this is not the case.\r\nPerhaps that they still want the processing to occur even in the case of some ‘malformed’ PDFs for whatever\r\nreason. Who knows!\r\nreadSymbolDictSeg and Differences in the Patch\r\nAs we alluded to earlier, another method has a very similar processing loop, but it was actually protected from the\r\ninteger overflow before this release! In fact, the fix in this code checks for the integer overflow when calculating\r\nthe number of symbols!\r\nUsing our JBIG2 source code as an example, we can see the following processing. On lines 1536-1539, we see\r\nthe integer overflow check to ensure that when the statement on line 1540 is executed, it won’t overflow!\r\nIn addition, they are checking to ensure the number of new symbols hasn’t exceeded the bounds (lines 1548-1549)\r\n 1527 // get referenced segments: input symbol dictionaries and code tables\r\n 1528 numInputSyms = 0;\r\n 1529 for (i = 0; i \u003c nRefSegs; ++i) {\r\n 1530 // This is need by bug 12014, returning false makes it not crash\r\n 1531 // but we end up with a empty page while acroread is able to render\r\n 1532 // part of it\r\n 1533 if ((seg = findSegment(refSegs[i]))) {\r\n 1534 if (seg-\u003egetType() == jbig2SegSymbolDict) {\r\n 1535 j = ((JBIG2SymbolDict *)seg)-\u003egetSize();\r\n 1536 if (numInputSyms \u003e UINT_MAX - j) {\r\n 1537 error(errSyntaxError, curStr-\u003egetPos(),\r\n \"Too many input symbols in JBIG2 symbol dictionary\");\r\n 1538 goto eofError;\r\n 1539 }\r\n 1540 numInputSyms += j;\r\n 1541 } else if (seg-\u003egetType() == jbig2SegCodeTable) {\r\n 1542 codeTables.push_back(seg);\r\n 1543 }\r\n 1544 } else {\r\n 1545 return false;\r\n 1546 }\r\n 1547 }\r\n 1548 if (numInputSyms \u003e UINT_MAX - numNewSyms) {\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 12 of 15\n\n1549 error(errSyntaxError, curStr-\u003egetPos(),\r\n \"Too many input symbols in JBIG2 symbol dictionary\");\r\n 1550 goto eofError;\r\n 1551 }\r\nIn the assembly from 11.5.2, we can see the overflow check at addresses 00007FFF2524576D -\r\n00007FFF25245774 , with the branch at 00007FFF25245774 going down the error path:\r\n__text:00007FFF25245748 loc_7FFF25245748:\r\n__text:00007FFF25245748 mov rax, [rbp+var_68]\r\n__text:00007FFF2524574C mov esi, [rax+rbx*4]\r\n__text:00007FFF2524574F mov rdi, r14\r\n__text:00007FFF25245752 call findSegment\r\n__text:00007FFF25245757 test rax, rax\r\n__text:00007FFF2524575A jz short loc_7FFF25245791\r\n__text:00007FFF2524575C mov r12, rax\r\n__text:00007FFF2524575F mov rax, [rax]\r\n__text:00007FFF25245762 mov rdi, r12\r\n__text:00007FFF25245765 call qword ptr [rax+10h] ; getType()\r\n__text:00007FFF25245768 cmp eax, 1 ; jbig2SegSymbolDict\r\n__text:00007FFF2524576B jnz short loc_7FFF25245776\r\n__text:00007FFF2524576D add r13d, [r12+0Ch]\r\n__text:00007FFF25245772 jnb short loc_7FFF25245791 ; integer overflow check\r\n__text:00007FFF25245774 jmp short integer_overflow\r\nAs you can see, this overflow check was done during the calculation of the number of symbols. This is due to the\r\njnb instruction. The add instruction will perform both signed and unsigned operation and adjust the Overflow\r\nFlag ( OF ) and/or Carry Flag ( CF ) for signed and unsigned respectively. The jnb instruction (a pseudonym\r\nfor jnc ) indicates to jump if the carry flag is 0 (i.e. no integer wrapping occurred). In this case, this is the ‘good’\r\npath, whereas if the CF was set, this would indicate an integer wrapping and the corresponding error path is\r\ntaken!\r\nOn the other hand, the readTextRegionSeg method, the numSyms can still overflow, however, in the processing\r\nloop when the getBitmp method is copying to the allocated region, there is a check to ensure that this data is not\r\noverflowed.\r\nBased on the analysis and the abundance of common strings, it seems that Apple is likely using an opensource\r\nversion of the JBIG2 processing, and making their own modifications. (Admittedly, I did search for their notes\r\non this, but didn’t find it…if anyone confirms that they are using that would be awesome). It does seem that a\r\ndifferent developer implemented the fix in CVE-2021-30860 than the one found in the readSymbolDictSeg\r\nmethod.\r\nConcluding Thoughts\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 13 of 15\n\nThere were other functions that were patched as well. For example, in the 11.6 version, it is worth analyzing the\r\nfunctions at address 00007FFF24EF2684 and 00007FFF250F6301 . Perhaps for another blog post…\r\nAs we noted, this vulnerability is (well prior to the patch) exploitable through a crafted iMessage without any\r\nuser-interaction. In other words, a specially crafted PDF file could be sent to an iMessage recipient, and the\r\nvictim’s IMTranscoderAgent begins processing the malicious payload outside of the BlastDoor sandbox. As\r\nnoted in the beginning of this post, hopefully Apple will also update BlastDoor and prevent these dangerous file\r\nformats from being processed outside the Sandbox environment!\r\nApple’s iDevices have gotten more secure especially from allowing their system to be modified upon reboot.\r\nThus, a good practice for iOS users is to a) update when updates are available and b) reboot the phone every so\r\noften! Of course this won’t stop these 0day attacks, but it is at least a good security practice. It would be worth\r\ndownloading iVerify to help test for common infections as well as for recommendations to increase the security\r\nposture of your device!\r\niVerify\r\nPart 0x2\r\n…stay tuned! 🧐\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 14 of 15\n\nSource: https://objective-see.com/blog/blog_0x67.html\r\nhttps://objective-see.com/blog/blog_0x67.html\r\nPage 15 of 15",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://objective-see.com/blog/blog_0x67.html"
	],
	"report_names": [
		"blog_0x67.html"
	],
	"threat_actors": [
		{
			"id": "d90307b6-14a9-4d0b-9156-89e453d6eb13",
			"created_at": "2022-10-25T16:07:23.773944Z",
			"updated_at": "2026-04-10T02:00:04.746188Z",
			"deleted_at": null,
			"main_name": "Lead",
			"aliases": [
				"Casper",
				"TG-3279"
			],
			"source_name": "ETDA:Lead",
			"tools": [
				"Agentemis",
				"BleDoor",
				"Cobalt Strike",
				"CobaltStrike",
				"RbDoor",
				"RibDoor",
				"Winnti",
				"cobeacon"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434782,
	"ts_updated_at": 1775791431,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/7764ef040e08f3da8996f08b2774ad245bc11f0a.pdf",
		"text": "https://archive.orkl.eu/7764ef040e08f3da8996f08b2774ad245bc11f0a.txt",
		"img": "https://archive.orkl.eu/7764ef040e08f3da8996f08b2774ad245bc11f0a.jpg"
	}
}