Disabling Android Verified Boot (AVB 2.0) by Patching vbmeta.img

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.


Disabling Android Verified Boot (AVB 2.0) by Patching vbmeta.img

After rooting a lot of devices with Magisk — and bricking a fair number of them along the way — I’ve built up some hard-won notes. Magisk offers three install routes:

  1. With root already available, install directly from the Magisk Manager.
  2. If you can get hold of boot.img, patch it and flash it via fastboot.
  3. Flash magisk.zip from a custom recovery.

There are plenty of tutorials for the three routes, so this post focuses on the partition-verification bricking that often follows a direct install or a patched boot.img flash, and how to fix it.

Notice that every install route touches boot.img. That’s the whole trick: Magisk’s core mechanism is modifying and replacing the Ramdisk inside boot.img — the image that contains the init process and the startup rc files that boot Android.

If you already have root, you can either install directly or patch boot.img manually. Both are the same operation — patch boot.img — differing only in where the flash happens (fastboot vs. Magisk Manager).

There are two ways to obtain boot.img:

  1. Extract it from the official ROM. Different vendors package ROMs differently: OnePlus uses payload.bin (with a dedicated extraction script), Google’s stock ROM is just a zip, and Samsung ships an AP-prefixed tar file.
  2. If you already have root, pull it straight from the device: find boot.img through the symlinks under /dev/block/by-name and dump it with dd. On A/B-partition devices this is usually boot_a.img. To be safe against unrecoverable bricks, back up recovery.img, boot.img, and vbmeta.img this way.
ls -l /dev/block/by-name/ | grep boot
dd if=/dev/block/sdc6 of=/sdcard/boot.img

If flashing Magisk leaves the device unbootable or in a boot loop, re-flashing the known-good boot.img from fastboot usually recovers it — which is exactly why the backup matters.

When AVB 2.0 verification blocks boot

On some devices, vbmeta.img verification prevents boot after modification. Verified Boot is a key Android security feature: it guards the boot images against tampering by layering a chain of trust over the boot sequence — the ROM code verifies the BootLoader, the BootLoader verifies the boot image (so the kernel boots from a known-good image), and the kernel verifies the system and vendor partitions.

Because the ROM code and BootLoader are usually supplied by the OEM (and implementations vary), Google introduced a unified framework — Android Verified Boot 2.0 — in Android O. It gives OEMs a standardized way to meet CDD requirements while keeping the flexibility to customize their boot verification.

The fastboot fix

In short: on some OEM devices AVB 2.0 checks whether boot.img was modified, so flashing Magisk or a custom recovery lands the device in a fake brick / boot loop. Re-flashing the backed-up vbmeta.img with verification disabled recovers it:

fastboot --disable-verity --disable-verification flash vbmeta vbmeta.img

That’s simple and fine for a single device. But in production — thousands of devices needing Magisk — it’s far too manual. So the alternative is to customize Magisk’s own code to do it in one shot.

Customizing Magisk’s source

Let’s see what fastboot actually does. Looking at the fastboot source:

// There's a 32-bit big endian |flags| field at offset 120 where
    // bit 0 corresponds to disable-verity and bit 1 corresponds to
    // disable-verification.
    //
    // See external/avb/libavb/avb_vbmeta_image.h for the layout of
    // the VBMeta struct.
    uint64_t flags_offset = 123 + vbmeta_offset;
    if (g_disable_verity) {
        data[flags_offset] |= 0x01;
    }
    if (g_disable_verification) {
        data[flags_offset] |= 0x02;
    }

It’s just two bits at offset 120 of vbmeta.img. So vbmeta.img controls whether verification is disabled via two flags. Following the same recipe:

  1. Extract the original vbmeta.img from the device (same approach as finding boot.img for a direct Magisk install).
  2. Patch the flag at offset 120 (mirroring how Magisk fixes the vbmeta.img inside Samsung AP files).
  3. Flash it back.

How Magisk flashes boot.img without fastboot

The hard part is step 3 — flashing the patched vbmeta.img back. For that I borrowed from how Magisk, with root, directly flashes new-boot.img after patching:

// MagiskInstaller.kt — direct-install entry point
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()

private fun flashBoot(): Boolean {
        if (!"direct_install $installDir $srcBoot".sh().isSuccess)
            return false
        "run_migrations".sh()
        return true
    }

And the shell side of it:

direct_install() {
  rm -rf $MAGISKBIN/* 2>/dev/null
  mkdir -p $MAGISKBIN 2>/dev/null
  chmod 700 $NVBASE
  cp -af $1/. $MAGISKBIN
  rm -f $MAGISKBIN/new-boot.img
  echo "- Flashing new boot image"
  flash_image $1/new-boot.img $2
  if [ $? -ne 0 ]; then
    echo "! Insufficient partition size"
    return 1
  fi
  rm -rf $1
  return 0
}

# $1 = path to the patched new-boot.img, $2 = boot.img path on the device
flash_image() {
  # Make sure all blocks are writable
  $MAGISKBIN/magisk --unlock-blocks 2>/dev/null
  case "$1" in
    *.gz) CMD1="$MAGISKBIN/magiskboot decompress '$1' - 2>/dev/null";;
    *)    CMD1="cat '$1'";;
  esac
  if $BOOTSIGNED; then
    CMD2="$BOOTSIGNER -sign"
    ui_print "- Sign image with verity keys"
  else
    CMD2="cat -"
  fi
  if [ -b "$2" ]; then
    local img_sz=`stat -c '%s' "$1"`
    local blk_sz=`blockdev --getsize64 "$2"`
    [ $img_sz -gt $blk_sz ] && return 1
    eval $CMD1 | eval $CMD2 | cat - /dev/zero > "$2" 2>/dev/null
  elif [ -c "$2" ]; then
    flash_eraseall "$2" >&2
    eval $CMD1 | eval $CMD2 | nandwrite -p "$2" - >&2
  else
    ui_print "- Not block or char device, storing image"
    eval $CMD1 | eval $CMD2 > "$2" 2>/dev/null
  fi
  return 0
}

It looks complex, but the actual write is this one line:

eval $CMD1 | eval $CMD2 | cat - /dev/zero > "$2" 2>/dev/null

That single command writes boot.img (and, by extension, other image partitions) from a shell without fastboot.

Rewriting vbmeta.img

vbmeta.img is simpler than boot.img — no signing or verification steps needed. So it reduces to:

# find vbmeta.img first
vbmeta_block_name=`find /dev/block \( -type b -o -type c -o -type l \) -iname vbmeta | head -n 1`
vbmeta_block=`readlink -f $vbmeta_block_name`
# write the patched vbmeta.img
eval 'cat '/sdcard/patched_vbmeta.img'' | eval 'cat -' | cat - /dev/zero > $vbmeta_block 2>/dev/null

Add the patching step and the complete implementation is:

fun disableVbMetaImg() {
        try {
            // find vbmeta.img block
            val vbmetaPath = ShellUtils.fastCmd("readlink -f `find /dev/block \\( -type b -o -type c -o -type l \\) -iname vbmeta | head -n 1`")
            if (TextUtils.isEmpty(vbmetaPath)) {
                Log.w(Const.TAG, "could not find vbmeta.img!")
                return
            }
            val patchedVbmetaPath = "/sdcard/patched_vbmeta.img"
            if (!Shell.sh("dd if=$vbmetaPath of=$patchedVbmetaPath").exec().isSuccess) {
                Log.w(Const.TAG, "extract vbmeta.img failed!")
                return
            }
            val patchedVbMeta = SuFile.open(patchedVbmetaPath).readBytes()
            // There's a 32-bit big endian |flags| field at offset 120 where
            // bit 0 corresponds to disable-verity and bit 1 corresponds to
            // disable-verification.
            ByteBuffer.wrap(patchedVbMeta).putInt(120, 2)
            File(patchedVbmetaPath).writeBytes(patchedVbMeta)
            // rewrite vbmeta.img
            Log.d(Const.TAG, "rewrite vbmeta.img finished: $patchedVbmetaPath to $vbmetaPath")
            Shell.sh("eval 'cat '$patchedVbmetaPath'' | eval 'cat -' | cat - /dev/zero > $vbmetaPath 2>/dev/null").exec()
        } catch (e: Throwable) {
            Log.i(Const.TAG, "disable vb_meta.img avb2.0 error:", e)
        }
    }

What this tells you about the security boundary

The educational takeaway here cuts both ways. Verified Boot is one of the strongest integrity guarantees Android ships with, and understanding exactly where it lives — a 32-bit flags field at offset 120 of vbmeta.img — is what makes it both powerful and, on a rooted device, defeatable. On an unlocked bootloader this is by design: the user has taken control of the device, and Verified Boot’s trust anchor (the OEM key) has already been replaced. What defenders should note is the flip side — on a locked bootloader, this same patch doesn’t silently succeed, because the device would reject an unsigned vbmeta.img. The lesson for anyone hardening a device or a fleet: keep the bootloader locked, and treat any device that shows orange/red boot state as no longer trustworthy for sensitive data.


Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch