{"slug": "running-third-party-kernel-extensions-on-virtualization-framework-macos-guest", "title": "Running Third Party Kernel Extensions on Virtualization Framework macOS Guest VMs", "summary": "Apple's Virtualization framework in macOS 12 (Monterey) supports macOS guest VMs but imposes significant limitations, including the inability to install third-party kernel extensions in guests. The author has developed a complex workaround that patches iBoot modules and the VM's kernel cache to enable running third-party kernel extensions on Apple Silicon Macs, which is useful for developers porting or creating kernel extensions without risking damage to their physical hardware. This workaround also provides insights for reverse engineering macOS, as the unencrypted iBoot modules in the Virtualization framework offer a rare opportunity to study Apple's boot process.", "body_md": "# Running Third Party Kernel Extensions on Virtualization Framework macOS Guest VMs\n\nAs of macOS 12 (Monterey), Apple's [Virtualization framework](https://developer.apple.com/documentation/virtualization)\nhas nice support for macOS guest virtual machines, but with severe\nlimitations: For example you can't install a macOS guest on Intel\nMacs, install guests with newer versions of macOS than the host, copy\nand paste between the host and the guest, or install third party\nkernel extensions in the guest. As usual for Apple, the functionality\nthey do support is nicely implemented, but they've left out so much\nthat the result is only marginally useful -- at least compared to\nthird-party implementations available on Intel Macs, like VMware\nFusion and Parallels.\n\nI've been working on the last of these limitations, and have found a\nworkaround. It's rather complex, but should be very useful for those\ndeveloping kernel extensions for macOS on Apple Silicon, or (more\nlikely) porting existing Intel kexts to Apple Silicon. Developing\nkernel extensions on bare metal is a pain -- not least because you\nmight end up damaging your nice new Apple Silicon Mac. But doing it in\na virtual machine isolates the potential damage to the VM itself -- at\nworst you'll only need to trash it and create a replacement.\n\nParts of my workaround are interesting in themselves, even if you\ndon't use them to run third party kernel extensions on macOS guest\nVMs. For example I've found a way to patch the VM's kernel cache\n(containing the kernel and built-in kernel extensions), which should\nbe very useful in reverse engineering macOS.\n\nMost of the discoveries here are ones I've made on my own, by trial\nand error. But I had a very helpful starting point --\n[NyanSatan's Virtual-iBoot-Fun project](https://github.com/NyanSatan/Virtual-iBoot-Fun).\n\n## Prerequisites\n\n- An Apple Silicon Mac running macOS 12 or higher. Mine is a 2020 Mac\n  Mini (Macmini9,1).\n\n- A decent virtual machine host that uses Apple's Virtualization\n  framework. I use [UTM](https://github.com/utmapp/UTM/).\n\n- One or more decent disassemblers. I use\n  [Ghidra](https://github.com/NationalSecurityAgency/ghidra) and\n  [Hopper Disassembler](https://www.hopperapp.com/). I also installed\n  [Nick Botticelli's ghidra-iboot plugin](https://github.com/nick-botticelli/ghidra-iboot)\n\n- [Tihmstar's img4tool](https://github.com/tihmstar/img4tool).\n\n- A decent hex editor. I use [Hex Fiend](https://hexfiend.com/). I\n  change its default edit mode to \"Overwrite\" and its line number\n  format to \"Hexadecimal\".\n\n- A binary diff tool. I use [VBinDiff](https://www.cjmweb.net/vbindiff/).\n\n- A calculator with support for radix modes. Apple's Calculator in\n  Programmer mode will do.\n\n## Background\n\nMy workaround patches three iBoot modules and the VM's kernel cache.\n\nThe iBoot modules load early in the macOS (and IOS) boot sequence,\nbefore the kernel. There are at least three variants of iBoot, used in\nthree diffent \"stages\" -- Stage 0 (as I call it), Stage 1 and Stage\n2. On bare metal, Stage 0 is implemented in hardware (in Apple's\n[\"Secure Enclave\"](https://support.apple.com/guide/security/secure-enclave-sec59b0b31ff/web)).\nStage 1 (aka LLB) and Stage 2 (iBoot properly so called) are\n[implemented in software](https://eclecticlight.co/2022/01/05/booting-an-m1-mac-from-hardware-to-kexts-2-llb-and-iboot/),\nthough only the Stage 2 iBoot exists (inside `iBoot.img4`) in the\nmacOS file system.\n\nBy design, the Secure Enclave Processor (SEP) is inaccessible to\nordinary mortals. And on bare metal the LLB and iBoot modules are\nencrypted (using a key held in the SEP). But for a VM, the modules for\nall three stages are unencrypted. Stage 0 is implemented (on the host)\nin `/System/Library/Frameworks/Virtualization.framework/Resources/AVPBooter.vmapple2.bin`.\nStage 1 is embedded (in `img4` format) in an `AuxiliaryStorage` file\n(on the host) associated with the DMG file (with an `img` extension)\nthat stores the VM's image. Stage 2 (inside `iBoot.img4`) exists in\nthe VM's file system.\n\nNone of the iBoot variants call any external functions, and none have\nany symbols. So they're difficult to decipher. But the fact that the\nVirtualization framework uses them without encryption is a golden\nopportunity to reverse engineer them, and understand them better.\n\nIt's apparently common knowledge that the iBoot modules all contain a\nfunction to check the \"digests\" (DGST) of the many `img4` images that\ngovern the macOS boot process (including LLB, `iBoot.img4` and the\nkernel cache itself). Each \"digest\" is a hash, used to ensure that\nnone of these images has changed since they were \"signed\" by\nApple. The source code for a similar function\n(`image4_validate_property_callback()`) is available\n[here](https://github.com/xerub/img4lib/blob/master/libvfs/vfs_img4.c#L1327).\n\nThe images in question are those to be loaded at the next stage. So\n(among other things) Stage 0 iBoot checks the digest of the Stage 1\niBoot `img4` image, Stage 1 iBoot checks the digest of the Stage 2\niBoot `img4` file, and Stage 2 iBoot checks the digest of the\n`kernelcache` file. Aside from the file system itself, nothing checks\nthe integrity of the stage 0 iBoot file (`AVPBooter.vmapple2.bin`). So\nall you need to stop these integrity checks is to [defeat the file\nsystem's\nprotection](https://gist.github.com/macshome/15f995a4e849acd75caf14f2e50e7e98)\nand patch this function (call it\n`image4_validate_property_callback()`) at every \"stage\".\n\nAs NyanSatan mentions, the standard approach is to patch out\n`image4_validate_property_callback()` completely -- to replace it with\ncode that \"returns 0\". I find this causes problems, so my patch is\nmore surgical -- I allow `image4_validate_property_callback()` to\nperform all its normal checks, and only patch its return value (to\n'0') just before it returns.\n\nSome time ago I discovered that [failed calls to\n`_validate_acm_context()` in the VM's `AppleVPBootPolicy.kext` stop\nthe VM from creating an auxiliary kext\ncache](https://github.com/utmapp/UTM/issues/4026#issuecomment-1338282487). Since\nthird-party kexts are stored here, this prevents them from loading in\na Virtualization framework VM. The failed calls originate from two\nother functions in `AppleVPBootPolicy.kext` --\n`_command_create_linked_manifest()` and\n`_command_update_local_policy_for_kcos()`. My workaround is to patch\nout the calls to `_validate_acm_context()` from both of these\nfunctions. This is a more \"surgical\" approach than patching out\n`_validate_acm_context()` itself, which is called from many additional\nfunctions in `AppleVPBootPolicy.kext`.\n\nBut this isn't enough by itself. An auxiliary kext cache does get\ncreated (you can see it using `kmutil inspect`). But macOS still\ncomplains that your third-party kext needs to be rebuilt. To fix this\nyou need to go back to the Stage 2 iBoot module (inside\n`iBoot.img4`). One of its purposes is to fill the \"device tree\" with\nall the appropriate devices. But it (or the variant available in a\nVirtualization framework VM) refuses to allow any \"AuxKC\" entries to\nbe added to `chosen/memory-map` in a VM, even when the auxiliary kext\ncache is present. Fixing this requires finding the function that\nchecks whether a given boot object (originally in `img4` format) should\nbe processed, and patching out the call to it. Its first parameter is\na [four-character code](https://en.wikipedia.org/wiki/FourCC) -- for\nexample `illb`, `ibot`, `krnl` or `auxk`. `validate_boot_object()` (my\nname for this function) indirectly calls\n`image4_validate_property_callback()`. So if you patch out the former\nyou don't need to patch the latter. At some point I'll describe how I\nfound `validate_boot_object()`.\n\nTo display the contents of `chosen/memory-map`, run the following\ncommand in a Terminal prompt:\n\n```\nioreg -p IODeviceTree -n \"memory-map\" -w 0 -r -t\n```\n\n## Settings Changes (Host and Guest)\n\nSome settings changes are required on the host by [How to Defang macOS\nSystem Protections](https://gist.github.com/macshome/15f995a4e849acd75caf14f2e50e7e98),\nwhich allows changes to `AVPBooter.vmapple2.bin.`\n\n- In System Settings, under Software Update, disable \"Download new\n  updates when available\". Otherwise your host can become unbootable.\n\n- Boot into Recovery Mode, run Terminal and do the following:\n\n```\ncsrutil disable\ncsrutil authenticated-root disable\n```\n\nYou also, of course, need to create a macOS guest VM. Make sure it\nuses the Virtualization framework. In\n[UTM](https://github.com/utmapp/UTM) this is accomplished by choosing\n\"Virtualize\" and then \"macOS 12+\".\n\nTerminal will be used heavily below. So, to avoid lots of annoying\nprompts to give it permission to access files, it's best to give the\nTerminal app \"full disk access\" on the guest (in Privacy and Security\nin System Settings).\n\nThen boot into Recovery Mode on the guest. On macOS 12 you'll need to\nuse [my hack](https://github.com/utmapp/UTM/issues/3904#issuecomment-1100924393)\nto do this.\n\n- Run the Startup Security Utility. Choose Reduced Security and \"Allow\n  user management of kernel extensions from identified developers\".\n\n- Run Terminal and do the following:\n\n- `csrutil disable`, then `y` to \"Allow booting unsigned\n  operating systems and any kernel extensions\".\n\n## Special Considerations Once You've Made the Changes Described Here\n\nBelow you'll create (and boot from) an APFS snapshot on your host\ncomputer to accomodate changes to the Stage 0 iBoot module\n(`AVMBooter.vmapple2.bin`). You need to keep your host computer in\nthis state as long as you're using third party kernel extensions in\nyour macOS guest VM. (Without the patched `AVPBooter.vmapple2.bin` it\nwill simply refuse to start.) You also need to keep csrutil's\n\"authenticated-root\" disabled, and keep \"Download new updates when\navailable\" disabled under Software Update. But don't use Software\nUpdate to update macOS on your host while it's booted from this custom\nsnapshot. I don't *know* that this will cause trouble, but I expect it\nmight.\n\nUse the following command to revert your snapshot (and\n`AVPBooter.vmapple2.bin`), then reboot your computer:\n\n```\nsudo bless --mount / --last-sealed-snapshot\n```\n\nGuest VMs running release versions of macOS 13 and up can be upgraded\nto newer versions of macOS. But before you do so you should revert\nthis document's changes to the Stage 2 iBoot module (`iBoot.img4`) and\nthe kernel cache (`kernelcache`): Boot into Recovery Mode, run the\nStartup Security Utility, and choose Full Security. Afterwards you'll\nneed to go through all the following steps again, from scratch. Among\nother things you'll need to create new `iBoot.img4.org` and\n`kernelcache.org` files, and possibly also a new `LLB.img4.org` file.\n\nmacOS 12 guests can't be upgraded. The Virtualization process\n(`com.apple.Virtualization.VirtualMachine`) crashes and your VM\nbecomes unbootable. This is an Apple bug. I've seen it myself, and\nhave seen it documented\n[here](https://github.com/utmapp/UTM/issues/5307). The same [is true\nfor beta-version macOS 14 and macOS 15\nguests](https://github.com/insidegui/VirtualBuddy/discussions/194),\nthough the consequences are less severe.\n\nIn this case your only option is to create a new guest VM directly\nfrom an IPSW file, then work through the following steps.\n\n## Finding the Modules to be Patched\n\n### iBoot Stage 0 (`AVPBooter.vmapple2.bin`)\n\nThis one's on the host, and is easy to find. Copy it and rename the\ncopy to `AVPBooter.vmapple2.bin.org`.\n\n```\n/System/Library/Frameworks/Virtualization.framework/Resources/AVPBooter.vmapple2.bin\n```\n\n### iBoot Stage 1 (LLB)\n\nThis one's also on the host, but is harder to find. By default, VMs\ncreated by [UTM](https://github.com/utmapp/UTM/) are stored in the\nfollowing directory:\n\n```\n~/Library/Containers/com.utmapp.UTM/Data/Documents\n```\n\nEach VM is stored in a \"package\" with the extension `.utm`. Inside the\npackage, in its `Data` subdirectory, is a file named\n`AuxiliaryStorage`. LLB is embedded here, in `img4` format. It's\nfollowed immediately by an `img4` format image of the \"logo\". You'll\nneed to get copies of both.\n\nIf you've upgraded your guest VM at least once to a newer version of\nmacOS, there will be *two* copies of the LLB image in the\n`AuxiliaryStorage` file, each followed by a \"logo\" image. The first\nLLB image is always at offset `0x24000`. The second, if it exists,\nshould be at offset `0x224000`. To be sure you've found all the LLB\nimages, and their correct locations, search (in your hex editor) on\n\"illb\" (the [four character\ncode](https://en.wikipedia.org/wiki/FourCC) for the LLB image).\n\nOnly one of these two sets of images is active. Each time you upgrade\nmacOS, the \"other\" LLB/logo image set becomes active. The original\ninstallation uses the set at offset `0x24000`. The first upgrade\nswitches to the one at offset `0x224000` (and leaves the set at offset\n`0x24000` unchanged). The second upgrade switches back to the one at\noffset `0x24000` and overwrites it (leaving the one at `0x224000`\nunchanged). And so forth.\n\nHere's how to find out which set is active.\n\nAt offsets `0x4000` and `0x5000` in `AuxiliaryStorage` are two more\nfile images, each starting with \"HUFA\". Each corresponds to one of the\nLLB/logo image sets. If you only have one LLB/logo image set, only one\n\"HUFA\" image exists, at offset `0x4000`. The structure of these \"HUFA\"\nimages is as follows:\n\n```\nstruct HUFA {\n  unsigned char magic[4];   // Always \"HUFA\" (unknown meaning).\n  uint32_t file_version;    // Always '1'.\n  uint32_t upgrade_count;   // 'n' == which (re)installation of macOS\n                            // created this HUFA image and its\n                            // corresponding LLB/logo images. '1'\n                            // means the original installation. '2'\n                            // and up mean each subsequent upgrade.\n  uint32_t LLB_offset;      // Offset of corresponding LLB/logo images\n                            // from the start of the first HUFA image\n                            // at offset 0x4000 -- either 0x20000 or\n                            // 0x220000.\n  uint32_t unknown[4];\n  unsigned char hash[32];   // Some kind of hash value -- type and\n                            // target unknown.\n  unsigned char fill[4032]; // Filled with 0xff.\n};\n```\n\nIf there are two sets of LLB/logo images, the one that's active has\nthe highest `upgrade_count`. This is the one you'll need to work on.\n\n- Open `AuxiliaryStorage` in a hex editor. I use [Hex\n  Fiend](https://hexfiend.com/), and will tailor my steps to it\n  specifically.\n\n- Jump to the offset of the active set of LLB/logo images -- `0x24000`\n  or `0x224000`. If an LLB image exists at this location, its first\n  two bytes should be `3083`.\n\n- The `img4` image is in DER format (used by ASN1), so the first five\n  bytes are `3083` followed by a three digit hexadecimal number in big\n  endian format (for example `037FD1`). This number is the length of\n  the image, exclusive of the length of its header. So the total\n  length is `0x37FD6`.\n\n- Select the first five bytes of the image, then choose \"Extend\n  Selection\". In this case, you'd extend it by `0x37FD1` bytes. Scroll\n  down to the end of the selection (without disturbing it) and check\n  that it's in the correct location (just before the \"logo\" image). If\n  it is, CMD-C to copy the image, CMD-N to open a new window, CMD-V to\n  paste in its contents, and save the file as `LLB.img4.org`.\n\n- The header for the \"logo\" image should be `3082` followed by a two\n  digit hexdecimal number (for example `36DA`). Select the first four\n  bytes, extend the selection by `0x36DA` bytes and scroll down to its\n  end. The following bytes should be a bunch of NULLs (`00`). CMD-C to\n  copy the image, CMD-N to open another new window, CMD-V to paste in\n  its contents, and save this second new file as `logo.img4.org`.\n\n### iBoot Stage 2 (`iBoot.img4`) and the Kernel Cache (`kernelcache`)\n\nThese modules are stored in files on the VM. So run the VM and do the\nfollowing in it, at a Terminal prompt:\n\n- Run `kmutil inspect`, and observe where the boot kernel cache exists\n  in the VM's file system. The following is an example, which will get\n  used in the following steps. The path's exact contents will differ\n  from case to case.\n\n```\n/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache\n```\n\n- Note the long hexadecimal number just before\n  `/System/Library/Caches` -- in this case\n  `FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83`. This\n  is the \"Next Stage Image4 Hash (nsih)\", observable in the output of\n  `sudo bputil -d`.\n\n- Copy this file and rename it to `kernelcache.org`.\n\n- `cd /` and `sudo find . -name iBoot.img4 -exec ls -al \\{\\} \\;`.\n  There will be at least two hits. Choose the one whose path contains\n  the Next Stage Image4 Hash. Copy it and rename the copy to\n  `iBoot.img4.org`. You might find the original `iBoot.img4` here, for\n  example:\n\n```\n/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4\n```\n\n## Patching These Modules\n\n### iBoot Stage 0 (`AVPBooter.vmapple2.bin.org`)\n\nFor this I use\n[Ghidra](https://github.com/NationalSecurityAgency/ghidra) with the\n[ghidra-iboot plugin](https://github.com/nick-botticelli/ghidra-iboot). Copy\n`AVPBooter.vmapple2.bin.org` to `AVPBooter.vmapple2.bin`. Then run\nGhidra and create a new project. Then:\n\n- Run Ghidra's CodeBrowser and import `AVPBooter.vmapple2.bin`.\n\n- Choose Search : Program Text : All Fields. Type \"0x4447\" ('DG' of\n  DGST) in \"Search for\", then choose \"Search All\". You should find two\n  hits, both in the same function. Click on one to move the cursor to\n  its location.\n\n- Once again choose Search : Program Text : All Fields, and type\n  \"retab\" in \"Search For\". Then choose \"Next\". Now you should see the\n  code that runs just before the function returns. It will look\n  something like as follows. In this example, you want to change the\n  instruction at address `0x00102a0c` (`mov x0,x20`) to `mov\n  x0,#0x0`. (AARCH64 machine code uses the `X0` register to store a\n  return value.)\n\n```\n001029f4 a8 83 5a f8     ldur       x8,[x29, #local_68]\n001029f8 29 f9 37 d0     adrp       x9,0x70028000\n001029fc 1f 20 03 d5     nop\n00102a00 29 a1 40 f9     ldr        x9,[x9, #0x140]=>DAT_70028140\n00102a04 3f 01 08 eb     cmp        x9,x8\n00102a08 21 06 00 54     b.ne       LAB_00102acc\n00102a0c e0 03 14 aa     mov        x0,x20\n00102a10 fd 7b 4c a9     ldp        x29=>local_10,x30,[sp, #0xc0]\n00102a14 f4 4f 4b a9     ldp        x20,x19,[sp, #local_20]\n00102a18 f6 57 4a a9     ldp        x22,x21,[sp, #local_30]\n00102a1c f8 5f 49 a9     ldp        x24,x23,[sp, #local_40]\n00102a20 fa 67 48 a9     ldp        x26,x25,[sp, #local_50]\n00102a24 fc 6f 47 a9     ldp        x28,x27,[sp, #local_60]\n00102a28 ff 43 03 91     add        sp,sp,#0xd0\n00102a2c ff 0f 5f d6     retab\n```\n\n- Right click on the `mov x0,0x20` instruction and choose \"Patch\n  Instruction\". Then change `x0,0x20` to `x0,#0x0`.\n\n- In Ghidra's CodeBrowser, choose File : Export Program. Then choose\n  Format : Raw Bytes, and overwrite `AVPBooter.vmapple2.bin`.\n\n- Use `vbindiff AVPBooter.vmapple2.bin.org AVPBooter.vmapple2.bin` to\n  check your results. There should be just one change, to a four-byte\n  value -- the length of one AARCH64 instruction.\n\n### iBoot Stage 1 (`LLB.img4`)\n\nAll the other modules that need patching are \"wrapped\" in `img4`\nformat. So to get at their actual content you need to use\n[`img4tool`](https://github.com/tihmstar/img4tool) to unpack\nthem. Then you'll patch them and rewrap them in new `img4` format\nfiles.\n\n- `img4tool -e -p LLB.im4p.org LLB.img4.org`\n\n- `img4tool -e -m LLB.im4m.org LLB.img4.org`\n\n- `img4tool -e -o LLB.bin.org LLB.im4p.org`\n\nThe iBoot Stage 1 module should now be in `LLB.bin.org`. Copy it to\n`LLB.bin` and patch it according to the instructions for the iBoot\nStage 0 binary (`AVPBooter.vmapple2.bin`) above.\n\n- Run `img4tool LLB.im4p.org`, which will produce output something\n  like what follows. Use the information from it to run the next\n  command. The value for \"desc\" differs from one version of macOS to\n  another. Note that you do *not* want to use compression, even though\n  `img4tool` supports it. `img4tool` uses Apple's\n  `libcompression.dylib` to implement its compression (and\n  decompression). But, even though it's the same type (\"bvx2\"), it's\n  incompatible with the decompression used by the iBoot binaries,\n  which don't have access to any external modules.\n\n```\nimg4tool version: 0.199-ed194718f9d6a035a432f2fdfe9fc639b72cba6c-RELEASE\nCompiled with plist: YES\nIM4P: ---------\ntype: illb\ndesc: iBoot-8422.141.2\nsize: 0x000369df\n\nCompression: bvx2\nUncompressed size: 0x0006dc90\nIM4P does not contain KBAG values\n```\n\n- `img4tool -c LLB.im4p -t illb -d \"iBoot-8422.141.2\" LLB.bin`\n\n- `img4tool -c LLB.img4 -p LLB.im4p -m LLB.im4m.org`\n\nThe patched iBoot Stage 1 module should now be in `LLB.img4`.\n\n### iBoot Stage 2 (`iBoot.img4`)\n\nOnce again you'll need to unwrap the iBoot Stage 2 binary, patch it,\nand then rewrap it in `img4` format. But this time the process is more\ncomplicated: `iBoot.im4p.org` contains a PAYP structure, tacked on to\nits end, that you'll need to append to `iBoot.im4p` by hand.\n\n- `img4tool -e -p iBoot.im4p.org iBoot.img4.org`\n\n- `img4tool -e -m iBoot.im4m.org iBoot.img4.org`\n\n- `img4tool -e -o iBoot.bin.org iBoot.im4p.org`\n\nHere's how to copy the PAYP structure from `iBoot.im4p.org` to a\nseperate file, which you'll later append to `iBoot.im4p`.\n\n- `openssl asn1parse -inform der -in iBoot.im4p.org -i -dump`\n\nThis outputs the entire `iBoot.im4p.org` file in human-readable ASN1\nformat. The PAYP structure appears at the end, and looks something\nlike this. Use the structure's offset (`259719` in this case) in the\nnext command.\n\n```\n259719:d=1  hl=2 l=  28 cons:  cont [ 0 ]        \n259721:d=2  hl=2 l=  26 cons:   SEQUENCE          \n259723:d=3  hl=2 l=   4 prim:    IA5STRING         :PAYP\n259729:d=3  hl=2 l=  18 cons:    SET               \n259731:d=4  hl=7 l=  11 cons:     priv [ 1768907638 ] \n259738:d=5  hl=2 l=   9 cons:      SEQUENCE          \n259740:d=6  hl=2 l=   4 prim:       IA5STRING         :iocv\n259746:d=6  hl=2 l=   1 prim:       INTEGER           :03\n```\n\n- `xxd -p -s 259719 iBoot.im4p.org iBoot.payp.hex`\n\n`iBoot.payp.hex` is a hex dump. The following command will convert it\nto a binary (in DER format):\n\n- `xxd -p -r iBoot.payp.hex iBoot.payp.bin`\n\nCheck the contents of `iBoot.payp.bin` by running the following command:\n\n```\nopenssl asn1parse -inform der -in iBoot.payp.bin -i\n```\n\nNow copy `iBoot.bin.org` to `iBoot.bin`. Then patch out the call to\n`validate_boot_object()`. As mentioned above, you can do this instead\nof patching `image4_validate_property_callback()`.\n\n```\ne5 03 04 aa \n04 00 80 52 \n```\n\n- In the Ghidra CodeBrowser, choose Search : For Instruction Patterns,\n  then Edit Bytes and Input Mode Hex. Copy the above two lines of\n  hexadecimal code and paste it into the Edit Bytes box, then choose\n  Apply. Choose Search All and you should find one hit, with code that\n  looks like the following:\n\n```\n                     FUN_700ac1fc                                    XREF[1]:     FUN_70063a78:70063fbc(c)  \n700ac1fc e5 03 04 aa     mov        x5,x4\n700ac200 04 00 80 52     mov        w4,#0x0\n700ac204 01 00 00 14     b          LAB_700ac208\n\n                     LAB_700ac208                                    XREF[1]:     700ac204(j)  \n700ac208 7f 23 03 d5     pacibsp\n700ac20c ff 03 03 d1     sub        sp,sp,#0xc0\n```\n\n- Double-click on the cross reference (`FUN_70063a78:70063fbc(c)` in\n  this case). That should take you to code that looks like this:\n\n```\n70063fb0 e1 e3 0c 91     add        param_2,sp,#0x338\n70063fb4 e4 43 03 91     add        param_5,sp,#0xd0\n70063fb8 e3 03 16 aa     mov        param_4,x22\n70063fbc 90 20 01 94     bl         FUN_700ac1fc                                     undefined FUN_700ac1fc()\n70063fc0 a0 03 00 34     cbz        param_1,LAB_70064034\n70063fc4 14 7b 00 51     sub        w20,w24,#0x1e\n70063fc8 07 00 00 14     b          LAB_70063fe4\n```\n\n- In this example, right-click on the `bl FUN_700ac1fc` instruction\n  and change it to `mov x0,#0x0`.\n\n- On recent versions of macOS, there may be more than one cross\n  reference. In this case you should double-click on each one, in\n  turn, and change its target to `mov x0,#0x0`.\n\n- Choose File : Export Program. Then choose Format : Raw Bytes, and\n  overwrite `iBoot.bin`.\n\n- Run `img4tool iBoot.im4p.org`, which will produce output something\n  like this. Use the information from it to run the next command. The\n  value of \"desc\" differs from one version of macOS to another.\n\n```\nimg4tool version: 0.199-ed194718f9d6a035a432f2fdfe9fc639b72cba6c-RELEASE\nCompiled with plist: YES\nIM4P: ---------\ntype: ibot\ndesc: iBoot-8422.141.2\nsize: 0x0003f655\n\nCompression: bvx2\nUncompressed size: 0x0007dde8\nPAYP:\niocv: iocv: 3\n\nIM4P does not contain KBAG values\n```\n\n- `img4tool -c iBoot.im4p -t ibot -d \"iBoot-8422.141.2\" iBoot.bin`\n\n- `dd if=iBoot.payp.bin >> iBoot.im4p`\n\n- Use [Hex Fiend](https://hexfiend.com/) to open `iBoot.im4p` and\n  correct its length value. Make sure File : Mode is set to Override.\n\n- Observe `iBoot.im4p`'s five-byte header and length value -- for\n  example `308307DE0B`. Convert the length (`0x7DE0B`) to decimal\n  (`515595`) and add `30` (for the length of `iBoot.payp.bin` in this\n  case). So the new length in this case is `515625` (== `0x7DE29`).\n\n- Correct the length value. The header in this case will now be\n  `308307DE29`. Save `iBoot.im4p`.\n\n- `img4tool -c iBoot.img4 -p iBoot.im4p -m iBoot.im4m.org`\n\nThe patched Stage 2 module should now be in `iBoot.img4`.\n\n### The Kernel Cache (`kernelcache`)\n\nAs with the iBoot Stage 2 module, you'll need to unwrap the kernel\ncache, patch it, and rewrap it in `img4` format.\n\n- `img4tool -e -p kernelcache.im4p.org kernelcache.org`\n\n- `img4tool -e -m kernelcache.im4m.org kernelcache.org`\n\n- `img4tool -e -o kernelcache.bin.org kernelcache.im4p.org`\n\n`kernelcache.im4p.org`, like `iBoot.im4p.org`, has a PAYP structure at\nits end. Copy the PAYP structure to a separate file, which you'll\nlater append to `kernelcache.im4p`.\n\n- `openssl asn1parse -inform der -in kernelcache.im4p.org -i -dump`\n\nThis outputs the entire `kernelcache.im4p.org` file in human-readable\nASN1 format. The PAYP structure appears at the end, and looks\nsomething like this. Use the structure's offset (`18030138` in this\ncase) in the next command.\n\n```\n18030138:d=1  hl=3 l= 186 cons:  cont [ 0 ]        \n18030141:d=2  hl=3 l= 183 cons:   SEQUENCE          \n18030144:d=3  hl=2 l=   4 prim:    IA5STRING         :PAYP\n18030150:d=3  hl=3 l= 174 cons:    SET               \n18030153:d=4  hl=7 l=  19 cons:     priv [ 1801676144 ] \n18030160:d=5  hl=2 l=  17 cons:      SEQUENCE          \n18030162:d=6  hl=2 l=   4 prim:       IA5STRING         :kcep\n18030168:d=6  hl=2 l=   9 prim:       INTEGER           :FFFFFE0007B7D488\n18030179:d=4  hl=7 l=  14 cons:     priv [ 1801677926 ] \n18030186:d=5  hl=2 l=  12 cons:      SEQUENCE          \n18030188:d=6  hl=2 l=   4 prim:       IA5STRING         :kclf\n18030194:d=6  hl=2 l=   4 prim:       INTEGER           :030E4000\n18030200:d=4  hl=7 l=  19 cons:     priv [ 1801677935 ] \n18030207:d=5  hl=2 l=  17 cons:      SEQUENCE          \n18030209:d=6  hl=2 l=   4 prim:       IA5STRING         :kclo\n18030215:d=6  hl=2 l=   9 prim:       INTEGER           :FFFFFE0007004000\n18030226:d=4  hl=7 l=  14 cons:     priv [ 1801677946 ] \n18030233:d=5  hl=2 l=  12 cons:      SEQUENCE          \n18030235:d=6  hl=2 l=   4 prim:       IA5STRING         :kclz\n18030241:d=6  hl=2 l=   4 prim:       INTEGER           :AF0000\n18030247:d=4  hl=7 l=  11 cons:     priv [ 1801679462 ] \n18030254:d=5  hl=2 l=   9 cons:      SEQUENCE          \n18030256:d=6  hl=2 l=   4 prim:       IA5STRING         :kcrf\n18030262:d=6  hl=2 l=   1 prim:       INTEGER           :00\n18030265:d=4  hl=7 l=  14 cons:     priv [ 1801679482 ] \n18030272:d=5  hl=2 l=  12 cons:      SEQUENCE          \n18030274:d=6  hl=2 l=   4 prim:       IA5STRING         :kcrz\n18030280:d=6  hl=2 l=   4 prim:       INTEGER           :02C64000\n18030286:d=4  hl=7 l=  14 cons:     priv [ 1801680742 ] \n18030293:d=5  hl=2 l=  12 cons:      SEQUENCE          \n18030295:d=6  hl=2 l=   4 prim:       IA5STRING         :kcwf\n18030301:d=6  hl=2 l=   4 prim:       INTEGER           :02C64000\n18030307:d=4  hl=7 l=  13 cons:     priv [ 1801680762 ] \n18030314:d=5  hl=2 l=  11 cons:      SEQUENCE          \n18030316:d=6  hl=2 l=   4 prim:       IA5STRING         :kcwz\n18030322:d=6  hl=2 l=   3 prim:       INTEGER           :480000\n```\n\n- `xxd -p -s 18030138 kernelcache.im4p.org kernelcache.payp.hex`\n\n`kernelcache.payp.hex` is a hex dump. The following command will\nconvert it to a binary (in DER format):\n\n- `xxd -p -r kernelcache.payp.hex kernelcache.payp.bin`\n\nCheck the contents of `kernelcache.payp.bin` by running the following\ncommand:\n\n```\nopenssl asn1parse -inform der -in kernelcache.payp.bin -i\n```\n\nNow copy `kernelcache.bin.org` to\n`kernelcache.bin`. [Ghidra](https://github.com/NationalSecurityAgency/ghidra)\ndoesn't work well with kernel cache files, so I use [Hopper\nDisassembler](https://www.hopperapp.com/) to patch `kernelcache.bin`.\n\n- In Hopper choose Read Executable to Disassemble (CMD-SHIFT-O) and\n  select `kernelcache.bin`. Then click on Loader, scroll down to\n  `com.apple.security.AppleVPBootPolicy` and click OK.\n\n- Select Labels, type \"_validate_acm_context\" and click on it in the\n  list below. This should take you to the function, whose top few\n  lines should look like this:\n\n```\n                     __validate_acm_context:\nfffffe0008be57ec         pacibsp\nfffffe0008be57f0         sub        sp, sp, #0x40\nfffffe0008be57f4         stp        x20, x19, [sp, #0x20]\nfffffe0008be57f8         stp        fp, lr, [sp, #0x30]\nfffffe0008be57fc         add        fp, sp, #0x30\nfffffe0008be5800         sturb      wzr, [fp, var_11]\nfffffe0008be5804         cbz        w0, loc_fffffe0008be5868\n```\n\n- You want to find the calls to `_validate_acm_context()` from\n  `_command_create_linked_manifest()` and\n  `_command_update_local_policy_for_kcos()`, and patch each one out\n  by replacing it with a `nop` instruction.\n\n- Click on `_validate_acm_context()`'s first line (`pacibsp`), then\n  press the \"x\" key. This should open a dialog listing all the calls\n  to `_validate_acm_context()` from elsewhere in\n  `AppleVPBootPolicy`. Find `_command_create_linked_manifest()` and\n  `_command_update_local_policy_for_kcos()` in this list, and click on\n  each of them in turn. The calls to `_validate_acm_context()` should\n  look something like this:\n\n```\nfffffe0008bd4e74         add        x22, x21, #0x66\nfffffe0008bd4e78         mov        w0, #0x1\nfffffe0008bd4e7c         mov        x1, x22\nfffffe0008bd4e80         bl         __validate_acm_context\nfffffe0008bd4e84         tbnz       w0, 0x0, loc_fffffe0008bd4ea8\n```\n\n- Select the call to `_validate_acm_context()` (`bl\n  __validate_acm_context`) and change to Hexadecimal mode (from ASM\n  mode). For each of the following four hex values, double-click on it\n  and replace it with values from the following list (binary code for\n  a `nop` instruction). (Hit Enter to update each value after you've\n  typed in its replacement.)\n\n```\n1f 20 03 d5\n```\n\n- Repeat the previous two steps until you've patched the calls to\n  `_validate_acm_context()` from both\n  `_command_create_linked_manifest()` and\n  `_command_update_local_policy_for_kcos()`.\n\n- Choose File : New Executable and overwrite `kernelcache.bin`\n\n- Use `vbindiff kernelcache.bin.org kernelcache.bin` to check your\n  results. There should be just two changes, each to a four-byte value\n  -- the length of a single AARCH64 instruction.\n\n- Run `img4tool kernelcache.im4p.org`, which will produce output\n  something like this. Use the information from it to run the next\n  command. The value of \"desc\" differs from one version of macOS to\n  another. Note that the current version of `img4tool` can't yet deal\n  with `kernelcache.im4p.org`'s PAYP structure.\n\n```\nimg4tool version: 0.199-ed194718f9d6a035a432f2fdfe9fc639b72cba6c-RELEASE\nCompiled with plist: YES\nIM4P: ---------\ntype: krnl\ndesc: KernelManagement_host-354.140.3\nsize: 0x01131df6\n\nCompression: bvx2\nUncompressed size: 0x03bd4000\nPAYP:\nkcep: kcep: [Error] img4tool: failed with exception:\n[exception]:\nwhat=assure failed\ncode=15597586\nline=238\nfile=ASN1DERElement.cpp\ncommit count=199\ncommit sha  =ed194718f9d6a035a432f2fdfe9fc639b72cba6c\n```\n\n- `img4tool -c kernelcache.im4p -t krnl -d \"KernelManagement_host-354.140.3\" kernelcache.bin`\n\n- `dd if=kernelcache.payp.bin >> kernelcache.im4p`\n\n- Use [Hex Fiend](https://hexfiend.com/) to open `kernelcache.im4p`\n  and correct its length value. Make sure File : Mode is set to\n  Override.\n\n- Observe `kernelcache.im4p`'s six-byte header and length value -- for\n  example `308403BD4033`. Convert the length (`0x03BD4033`) to decimal\n  (`62734387`) and add `189` (for the length of `kernelcache.payp.bin`\n  in this case). So the new length in this case is `62734576` (==\n  `0x3BD40F0`).\n\n- Correct the length value. The header in this case will now be\n  `308403BD40F0`. Save `kernelcache.im4p`.\n\n- `img4tool -c kernelcache -p kernelcache.im4p -m kernelcache.im4m.org`\n\nThe patched kernel cache should now be in `kernelcache`.\n\n## Copying the Patched Modules to their Final Destinations\n\n### iBoot Stage 0 (`AVPBooter.vmapple2.bin`)\n\n`AVPBooter.vmapple2.bin` is a system file. By default it's protected\nby the macOS file system, and can't be changed. To get around this I\nborrow from [How to Defang macOS System\nProtections](https://gist.github.com/macshome/15f995a4e849acd75caf14f2e50e7e98). Before\nyou follow these steps, you must make the settings changes I described\nabove under Settings Changes.\n\n- `mkdir /tmp/mount`\n\n- Run `mount` at a Terminal prompt and observe its results, for\n  example as follows. Use the contents of the first line in the next\n  command.\n\n```\n/dev/disk5s1s1 on / (apfs, sealed, local, read-only, journaled)\ndevfs on /dev (devfs, local, nobrowse)\n/dev/disk5s6 on /System/Volumes/VM (apfs, local, noexec, journaled, noatime, nobrowse)\n/dev/disk5s2 on /System/Volumes/Preboot (apfs, local, journaled, nobrowse)\n/dev/disk5s4 on /System/Volumes/Update (apfs, local, journaled, nobrowse)\n/dev/disk1s2 on /System/Volumes/xarts (apfs, local, noexec, journaled, noatime, nobrowse)\n/dev/disk1s1 on /System/Volumes/iSCPreboot (apfs, local, journaled, nobrowse)\n/dev/disk1s3 on /System/Volumes/Hardware (apfs, local, journaled, nobrowse)\n/dev/disk5s5 on /System/Volumes/Data (apfs, local, journaled, nobrowse, protect)\n/dev/disk2s3 on /Volumes/Boot3 (apfs, sealed, local, read-only, journaled)\n/dev/disk7s3 on /Volumes/Boot2 (apfs, sealed, local, read-only, journaled)\n/dev/disk4s1 on /Volumes/Boot4 - Data (apfs, local, journaled, nobrowse, protect)\n/dev/disk2s1 on /Volumes/Boot3 - Data (apfs, local, journaled, nobrowse, protect)\n/dev/disk4s3 on /Volumes/Boot4 (apfs, sealed, local, read-only, journaled)\n/dev/disk6s1 on /Volumes/Boot1 - Data (apfs, local, journaled, nobrowse, protect)\n/dev/disk6s3 on /Volumes/Boot1 (apfs, sealed, local, read-only, journaled)\n/dev/disk7s1 on /Volumes/Boot2 - Data (apfs, local, journaled, nobrowse, protect)\nmap auto_home on /System/Volumes/Data/home (autofs, automounted, nobrowse)\n```\n\n- `sudo mount -o nobrowse -t apfs /dev/disk5s1 /tmp/mount`\n\n- `cd /tmp/mount/System/Library/Frameworks/Virtualization.framework/Resources`\n\n- `sudo cp /path/to/patched/AVPBooter.vmapple2.bin .`\n\n- `sudo bless --mount /tmp/mount --bootefi --create-snapshot`\n\n- Reboot your host computer.\n\nOn reboot, check the contents of\n`/System/Library/Frameworks/Virtualization.framework/Resources/AVPBooter.vmapple2.bin`\nto make sure it's what you expect.\n\nYou need to keep your host computer in this state as long as you're\nusing third party kernel extensions in your macOS guest VM. (Without\nthe patched `AVPBooter.vmapple2.bin` it will simply refuse to start.)\n\nIf need be, use the following command to revert your snapshot (and\n`AVPBooter.vmapple2.bin`), then reboot your host computer:\n\n```\nsudo bless --mount / --last-sealed-snapshot\n```\n\n### iBoot Stage 1 (`LLB.img4`)\n\nYou'll need to copy both `LLB.img4` and `logo.img4.org` over their\noriginal contents in `AuxiliaryStorage`. This is because `LLB.img4`\nmight be a different length than `LLB.img4.org`, and Apple's\nvirtualization infrastructure expects the logo image to immediately\nfollow the LLB image. [Above](#iboot-stage-1-llb) you determined which\nset of these images to work with. Now you'll be copying your changes\nover the original set.\n\n- Use [Hex Fiend](https://hexfiend.com/) to open `LLB.img4`,\n  `logo.img4.org` and `AuxiliaryStorage`. Make sure Edit : Mode for\n  `AuxilaryStorage` is Overwrite.\n\n- Jump to offset of the active set of LLB/logo images (either\n  `0x24000` or `0x224000`) in `AuxiliaryStorage`. If an LLB image\n  exists at this location, its first two bytes should be `3083`.\n\n- Copy the contents of `LLB.img4` (CMD-A, CMD-C) and paste them into\n  `AuxilaryStorage`.\n\n- Copy the contents of `logo.img4.org` and paste them into\n  `AuxiliaryStorage`, then save the file.\n\n### iBoot Stage 2 (`iBoot.img4`) and the Kernel Cache (`kernelcache`)\n\nCopy these files to your macOS guest VM and do the following there:\n\n- Run `kmutil inspect`, and observe where the boot kernel cache exists\n  in the VM's file system. The following is an example, which will get\n  used in the following steps. The path's exact contents will differ\n  from case to case.\n\n```\n/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache\n```\n\n- Note the \"Next Stage Image4 Hash\" just before\n  `/System/Library/Caches` -- in this case\n  `FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83`.\n\n- `sudo cp kernelcache /System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache`\n\n- Run `cd /` and `sudo find . -name iBoot.img4 -exec ls -al \\{\\} \\;`.\n  Observe the hit whose path contains the Next Stage Image4 Hash. Use\n  the results in the following command. For example:\n\n- `sudo cp iBoot.img4 /System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4`\n\nShut down your your macOS guest VM and reboot it into Recovery\nMode. Then perform the following steps. You'll be looking for the same\nNext Stage Image4 Hash as in the previous steps.\n\nRun Terminal in Recovery Mode, then do the following:\n\n- Run `cd /` and `find . -name iBoot.img4 -exec ls -al \\{\\} \\;`.\n  Expect two hits this time, which should look like the following:\n\n```\n/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4\n/System/Volumes/Data/private/tmp/Recovery/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4\n```\n\n- Copy `iBoot.img4` over both of them.\n\nYou may also see another hit that looks like the following. Ignore\nit. You can tell by its date and file size that you already copied the\npatched `iBoot.img4` over it above.\n\n```\n/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4\n```\n\n- Run `cd /` and `find . -name kernelcache -exec ls -al \\{\\} \\;`.\n  Once again expect two hits, which should look like the\n  following (maybe plus one superfluous hit, as above).\n\n```\n/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache\n/System/Volumes/Data/private/tmp/Recovery/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache\n```\n\n- Copy `kernelcache` over both of them.\n\n- Reboot your macOS guest VM and start playing with third-party kernel extensions on it!\n", "url": "https://wpnews.pro/news/running-third-party-kernel-extensions-on-virtualization-framework-macos-guest", "canonical_source": "https://gist.github.com/steven-michaud/fda019a4ae2df3a9295409053a53a65c", "published_at": "2023-09-02 21:23:55+00:00", "updated_at": "2026-05-22 12:39:44.550312+00:00", "lang": "en", "topics": ["developer-tools", "research", "open-source"], "entities": ["Apple", "Virtualization framework", "macOS", "Intel", "VMware Fusion", "Parallels", "Apple Silicon"], "alternates": {"html": "https://wpnews.pro/news/running-third-party-kernel-extensions-on-virtualization-framework-macos-guest", "markdown": "https://wpnews.pro/news/running-third-party-kernel-extensions-on-virtualization-framework-macos-guest.md", "text": "https://wpnews.pro/news/running-third-party-kernel-extensions-on-virtualization-framework-macos-guest.txt", "jsonld": "https://wpnews.pro/news/running-third-party-kernel-extensions-on-virtualization-framework-macos-guest.jsonld"}}