diff --git a/README.md b/README.md index 2025292..137a062 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ Also need python 2.x(required by avbtool) and java 8. (1) Target boot.img(or recovery.img / recovery-two-step.img) MUST follows AOSP verified boot flow, either [Boot image signature](https://source.android.com/security/verifiedboot/verified-boot#signature_format) in VBoot 1.0 or [AVB HASH footer](https://android.googlesource.com/platform/external/avb/+/master/README.md#The-VBMeta-struct) in VBoot 2.0. -(2) These utilities are known to work for Nexus/Pixel (or Pixel compatible) boot.img(or recovery.img/recovery-two-step.img) for the following Android releases: +(2) These utilities are known to work for Nexus/Pixel boot.img(or recovery.img/recovery-two-step.img/vbmeta.img) for the following Android releases: - AOSP master - - Lollipop (API Level 21,22) - Oreo (API Level 26,27) + - Lollipop (5.0) - Pi (9) You can get a full [Android version list](https://source.android.com/source/build-numbers.html) here. diff --git a/bbootimg/src/main/kotlin/Signer.kt b/bbootimg/src/main/kotlin/Signer.kt index be32914..871c395 100644 --- a/bbootimg/src/main/kotlin/Signer.kt +++ b/bbootimg/src/main/kotlin/Signer.kt @@ -28,11 +28,26 @@ class Signer { ImgArgs.VerifyType.AVB -> { log.info("Adding hash_footer with verified-boot 2.0 style") val sig = readBack[2] as ImgInfo.AvbSignature - File(args.output + ".clear").copyTo(File(args.output + ".signed")) val ai = ObjectMapper().readValue(File(Avb.getJsonFileName(args.output)), AVBInfo::class.java) + //val alg = Algorithms.get(ai.header!!.algorithm_type.toInt()) + + //our signer + File(args.output + ".clear").copyTo(File(args.output + ".signed")) + Avb().add_hash_footer(args.output + ".signed", + sig.imageSize!!.toLong(), + false, + false, + salt = sig.salt, + hash_algorithm = sig.hashAlgorithm!!, + partition_name = sig.partName!!, + rollback_index = ai.header!!.rollback_index, + common_algorithm = sig.algorithm!!, + inReleaseString = ai.header!!.release_string) + //original signer + File(args.output + ".clear").copyTo(File(args.output + ".signed2")) val signKey = Algorithms.get(sig.algorithm!!) var cmdlineStr = "$avbtool add_hash_footer " + - "--image ${args.output}.signed " + + "--image ${args.output}.signed2 " + "--partition_size ${sig.imageSize} " + "--salt ${sig.salt} " + "--partition_name ${sig.partName} " + @@ -47,19 +62,6 @@ class Signer { cmdLine.addArgument(ai.header!!.release_string, false) DefaultExecutor().execute(cmdLine) verifyAVBIntegrity(args, avbtool) - - File(args.output + ".clear").copyTo(File(args.output + ".signed2")) - val alg = Algorithms.get(ai.header!!.algorithm_type.toInt()) - Avb().add_hash_footer(args.output + ".signed2", - sig.imageSize!!.toLong(), - false, - false, - salt = sig.salt, - hash_algorithm = sig.hashAlgorithm!!, - partition_name = sig.partName!!, - rollback_index = ai.header!!.rollback_index, - common_algorithm = sig.algorithm!!, - inReleaseString = ai.header!!.release_string) } } } @@ -80,4 +82,4 @@ class Signer { return "{ $sb }" } } -} \ No newline at end of file +} diff --git a/bbootimg/src/test/kotlin/ReadTest.kt b/bbootimg/src/test/kotlin/ReadTest.kt new file mode 100644 index 0000000..4fde8bc --- /dev/null +++ b/bbootimg/src/test/kotlin/ReadTest.kt @@ -0,0 +1,321 @@ +import org.junit.Test +import java.io.File +import java.util.regex.Matcher +import java.util.regex.Pattern + +class ReadTest { + data class Trigger( + var trigger: String = "", + var actions: MutableList = mutableListOf() + ) + + data class Import( + var initrc: String = "" + ) + + data class Service( + var name: String = "", + var cmd: String = "", + var theClass: String = "default", + var theUser: String? = null, + var theGroup: String? = null, + var theSeclabel: String? = null, + var theMiscAttr: MutableList = mutableListOf(), + var theCaps: MutableList = mutableListOf(), + var theSocket: String? = null, + var theWritePid: String? = null, + var theKeycodes: String? = null, + var thePriority: Int? = null, + var theIOPriority: String? = null, + var theOnRestart: MutableList = mutableListOf() + ) + + fun parseConfig(inRootDir: String, inPath: String, + triggers: MutableList, + services: MutableList) { + if (!File(inRootDir + inPath).exists()) { + println("Parsing " + inPath + " fail: 404"); + } + if (File(inRootDir + inPath).isFile()) { + parseConfigFile(inRootDir, inPath, triggers, services) + } else if (File(inRootDir + inPath).isDirectory()) { + parseConfigDir(inRootDir, inPath, triggers, services) + } + } + + fun parseConfigDir(inRootDir: String, inPath: String, + triggers: MutableList, + services: MutableList) { + println("Parsing directory $inPath ...") + File(inRootDir + inPath).listFiles().forEach { + parseConfig(inRootDir, + it.path.substring(inRootDir.length - 1), + triggers, services) + } + } + + fun parseConfigFile(inRootDir: String, inPath: String, + triggers: MutableList, + services: MutableList) { + if (!File(inRootDir + inPath).exists()) { + println("Parsing $inPath fail: 404"); + return + } + println("Parsing file $inPath ...") + var imports: MutableList = mutableListOf() + var aTrigger: Trigger? = null + var aService: Service? = null + var aImport: Import? = null + var toBeContinued = false + val lines = File(inRootDir + inPath).readLines() + for (item in lines) { + val line = item.trim(); + //comment + if (line.startsWith("#") || line.isEmpty()) { + continue + } + //continue + if (toBeContinued) { + if (line.endsWith("\\")) { + aService!!.cmd += " " + aService!!.cmd += line.substring(0, line.length - 1) + println(" CONTINUE:" + line.substring(0, line.length - 1)) + } else { + toBeContinued = false + aService!!.cmd += " " + aService!!.cmd += line + println(" END :$line") + } + continue + } + + val finderOn = Pattern.compile("^(on)\\s+(\\S+.*$)").matcher(line) + val finderService = Pattern.compile("^service\\s+(\\S+)\\s+(.*$)").matcher(line) + val finderImport = Pattern.compile("^import\\s+(\\S+)$").matcher(line) + if (finderOn.matches() || finderService.matches() || finderImport.matches()) { + //flush start >> + aTrigger?.let { /* println("[add] " + aTrigger); */ triggers.add(aTrigger!!); aTrigger = null } + aService?.let { /* println("[add] " + aService); */ services.add(aService!!); aService = null } + aImport?.let { /* println("[add] " + aImport); */ imports.add(aImport!!); aImport = null } + // << flush end + } + finderOn.reset() + finderService.reset() + finderImport.reset() + + if (finderOn.find()) { + //println(" |on| " + line) + //println(" group.cnt = " + finderOn.groupCount()) + //println(" " + line.substring(finderOn.start(), finderOn.end())) + //println(" >" + finderOn.group(1)) + //println(" >" + finderOn.group(2)) + aTrigger = Trigger(trigger = finderOn.group(2)) + } else if (finderService.find()) { + aService = Service() + aService!!.name = finderService.group(1) + aService!!.cmd = finderService.group(2) + if (finderService.group(2).endsWith("\\")) { //remove trailing slash + toBeContinued = true + aService!!.cmd = aService!!.cmd.substring(0, aService!!.cmd.length - 1) + } + } else if (finderImport.find()) { + aImport = Import() + aImport!!.initrc = finderImport.group(1) + if (aImport!!.initrc.startsWith("/")) { + aImport!!.initrc = aImport!!.initrc.substring(1) + } else { + //do nothing + } + val ro_hardware = "\${ro.hardware}" + val ro_zygote = "\${ro.zygote}" + aImport!!.initrc = aImport!!.initrc.replace(ro_hardware, "sequoia") + aImport!!.initrc = aImport!!.initrc.replace(ro_zygote, "zygote32") + } else { + if (aTrigger != null) { + aTrigger!!.actions.add(line) + } else if (aService != null) { + //class + var bParsed = false + lateinit var mm: Matcher + if (!bParsed) { + mm = Pattern.compile("^class\\s+(.*)").matcher(line) + if (mm.matches()) { + aService!!.theClass = mm.group(1) + bParsed = true + } + } + if (!bParsed) { + //user + mm = Pattern.compile("^user\\s+(.*)").matcher(line) + if (mm.matches()) { + aService!!.theUser = mm.group(1) + bParsed = true + } + } + if (!bParsed) { + //capabilities + mm = Pattern.compile("^capabilities\\s+(.*)").matcher(line) + if (mm.matches()) { + aService!!.theCaps.add(mm.group(1)) + bParsed = true + } + } + if (!bParsed) { + //group + mm = Pattern.compile("^group\\s+(.*)").matcher(line) + if (mm.matches()) { + aService!!.theGroup = mm.group(1) + bParsed = true + } + } + if (!bParsed) { + //seclabel + mm = Pattern.compile("^seclabel\\s+(.*)").matcher(line) + if (mm.matches()) { + aService!!.theSeclabel = mm.group(1) + bParsed = true + } + } + if (!bParsed) { + //writepid + mm = Pattern.compile("^writepid\\s+(.*)$").matcher(line) + if (mm.matches()) { + aService!!.theWritePid = mm.group(1) + bParsed = true + } + } + if (!bParsed) { + //onrestart + mm = Pattern.compile("^onrestart\\s+(.*)$").matcher(line) + if (mm.matches()) { + aService!!.theOnRestart.add(mm.group(1)) + bParsed = true + } + } + if (!bParsed) { + //socket + mm = Pattern.compile("^socket\\s+(.*)$").matcher(line) + if (mm.matches()) { + aService!!.theSocket = mm.group(1) + bParsed = true + } + } + if (!bParsed) { + //ioprio + mm = Pattern.compile("^ioprio\\s+(.*)$").matcher(line) + if (mm.matches()) { + aService!!.theIOPriority = mm.group(1) + bParsed = true + } + } + if (!bParsed) { + //priority + mm = Pattern.compile("^priority\\s+(\\S+)$").matcher(line) + if (mm.matches()) { + aService!!.thePriority = Integer.parseInt(mm.group(1)) + bParsed = true + } + } + if (!bParsed) { + //check space + mm = Pattern.compile("^\\S+$").matcher(line) + if (mm.matches()) { + aService!!.theMiscAttr.add(line) + bParsed = true + } + } + if (!bParsed) { + println("<< Dangling << $line") + } + } else { + println("<< Dangling << $line") + } + } + } + + //flush start >> + aTrigger?.let { /* println("[add] " + aTrigger); */ triggers.add(aTrigger!!); aTrigger = null } + aService?.let { /* println("[add] " + aService); */ services.add(aService!!); aService = null } + aImport?.let { /* println("[add] " + aImport); */ imports.add(aImport!!); aImport = null } + // << flush end + + imports.forEach { println(it) } + + //parse imports again + var iteratorImport: Iterator = imports.iterator() + while (iteratorImport.hasNext()) { + val item: Import = iteratorImport.next() + parseConfigFile(inRootDir, item.initrc, triggers, services) + } + println("Parsing file $inPath done") + } + + fun queueEventTrigger(inServices: MutableList, + inTriggers: List, inTriggerName: String, + inIndent: String = "") { + val aPre = inIndent + inTriggers.filter { it.trigger == inTriggerName }.forEach { aTrigger -> + println(aPre + " (on+${aTrigger.trigger})") + aTrigger.actions.forEach { aAction -> + aAction.executeCmd(inServices, inTriggers, aPre + " ") + } + } + } + + fun String.executeCmd(inServices: MutableList, + inTriggers: List, inIndent: String) { + val aPre = inIndent + " " + if (this.startsWith("trigger ")) { + println(aPre + "|-- " + this) + queueEventTrigger(inServices, inTriggers, this.substring(8).trim(), aPre + "| ") + } else if (this.startsWith("chmod")) { + } else if (this.startsWith("chown")) { + } else if (this.startsWith("mkdir")) { + } else if (this.startsWith("write")) { + } else if (Pattern.compile("class_start\\s+\\S+").matcher(this).find()) { + println(aPre + "|-- " + this) + val m = Pattern.compile("class_start\\s+(\\S+)$").matcher(this) + if (m.find()) { + inServices + .filter { + it.theClass != null + && it.theClass!!.split(" ").contains(m.group(1)) + } + .forEach { + println(aPre + "| \\-- Starting " + it.name + "...") + } + } else { + println("error") + } + } else if (this.startsWith("start")) { + println(aPre + "|-- " + this) + println(aPre + "| \\-- Starting " + this.substring(5).trim() + "...") + } else { + println(aPre + "|-- " + this) + } + } + + @Test + fun parseTest() { + System.out.println(System.getProperty("user.dir")) + var gTriggers: MutableList = mutableListOf() + var gServices: MutableList = mutableListOf() + + parseConfig("__temp/", "/init.rc", gTriggers, gServices) + parseConfig("__temp/", "/system/etc/init", gTriggers, gServices) + parseConfig("__temp/", "/vendor/etc/init", gTriggers, gServices) + parseConfig("__temp/", "/odm/etc/init", gTriggers, gServices) + + gTriggers.forEach { println(it) } + gServices.forEach { println(it) } + + println("Trigger count:" + gTriggers.size) + println("Service count:" + gServices.size) + + queueEventTrigger(gServices, gTriggers, "early-init") + queueEventTrigger(gServices, gTriggers, "init") + queueEventTrigger(gServices, gTriggers, "late-init") +// println(">> mount_all() returned 0, trigger nonencrypted") +// queueEventTrigger(gServices, gTriggers, "nonencrypted") + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 31f9a38..6046885 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ subprojects { // ---------------------------------------------------------------------------- // global // ---------------------------------------------------------------------------- -def workdir='build/unzip_boot' +def workdir = 'build/unzip_boot' project.ext.rootWorkDir = new File(workdir).getAbsolutePath() String activeImg = "boot.img" String activePath = "/boot" @@ -80,6 +80,35 @@ task _setup(type: Copy) { into '.' } +task pull() { + doFirst { + println("Pulling ...") + } + + doLast { + if (project.findProperty("group")) { + println("Pull: $group") + } else { + println("Pull /boot, /recovery, /vbmeta") + pullDefault() + } + } +} + +void pullDefault() { + Run(["adb", "shell", "dd if=/dev/block/by-name/boot of=/cache/boot.img"]) + Run(["adb", "shell", "dd if=/dev/block/by-name/recovery of=/cache/recovery.img"]) + Run(["adb", "shell", "dd if=/dev/block/by-name/vbmeta of=/cache/vbmeta.img"]) + + Run(["adb", "pull", "/cache/boot.img"]) + Run(["adb", "pull", "/cache/recovery.img"]) + Run(["adb", "pull", "/cache/vbmeta.img"]) + + Run(["adb", "shell", "rm /cache/boot.img"]) + Run(["adb", "shell", "rm /cache/recovery.img"]) + Run(["adb", "shell", "rm /cache/vbmeta.img"]) +} + void Run(List inCmd, String inWorkdir = null) { println("CMD:" + inCmd) if (inWorkdir == null) { @@ -89,7 +118,7 @@ void Run(List inCmd, String inWorkdir = null) { .directory(new File(inWorkdir)) .redirectErrorStream(true); Process p = pb.start() - p.inputStream.eachLine {println it} + p.inputStream.eachLine { println it } p.waitFor(); assert 0 == p.exitValue() } @@ -117,7 +146,7 @@ void updateBootImage(String activeImg) { } Run("adb root") Run("adb push " + activeImg + ".signed /cache/") - List cmd2 = ["adb", "shell", "dd if=/cache/" + activeImg + ".signed of=" + flashTarget]; + List cmd2 = ["adb", "shell", "dd if=/cache/" + activeImg + ".signed of=" + flashTarget] Run(cmd2) cmd2 = ["adb", "shell", "rm -f /cache/" + activeImg + ".signed"]; Run(cmd2) diff --git a/temp.txt b/temp.txt new file mode 100644 index 0000000..a6ab7cf --- /dev/null +++ b/temp.txt @@ -0,0 +1,34 @@ +Enable developer debugging for AVB-enabled devices + +### modify build/unzip_boot/vbmeta.img.avb.json + +* disable dm-verity hashtree verification +header -> flags: set to 1(AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED = 1) to disable dm-verity hashtree(system/vendor etc.) + +```diff + "descriptors_size" : 1384, + "rollback_index" : 0, +- "flags" : 0, ++ "flags" : 1, + "release_string" : "avbtool 1.0.0" + }, + "authBlob" : { +``` + +* disable all AVB verification +header -> flags: set to 2(AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED = 2) to disable all verification, including AVB hash_footer(boot/recovery etc.) and dm-verity hashtree(system/vendor etc.) + +```diff + "descriptors_size" : 1384, + "rollback_index" : 0, +- "flags" : 0, ++ "flags" : 2, + "release_string" : "avbtool 1.0.0" + }, + "authBlob" : { +``` + +### unlock bootloader +'unlock' state will pass flag AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR when verifying images, then you can disable + +