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:
- With root already available, install directly from the Magisk Manager.
- If you can get hold of
boot.img, patch it and flash it via fastboot. - Flash
magisk.zipfrom 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:
- 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 anAP-prefixed tar file. - If you already have root, pull it straight from the device: find
boot.imgthrough the symlinks under/dev/block/by-nameand dump it withdd. On A/B-partition devices this is usuallyboot_a.img. To be safe against unrecoverable bricks, back uprecovery.img,boot.img, andvbmeta.imgthis 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:
- Extract the original
vbmeta.imgfrom the device (same approach as findingboot.imgfor a direct Magisk install). - Patch the flag at offset 120 (mirroring how Magisk fixes the
vbmeta.imginside SamsungAPfiles). - 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