feat: Added root-only adb runner (tested on emulator)

I spent almost an entire day on this, you better be happy!
This commit is contained in:
Lucaskyy 2022-04-15 23:40:09 +02:00
parent c9941fe182
commit 37ecc5eaa6
No known key found for this signature in database
GPG Key ID: 1530BFF96D1EEB89
8 changed files with 487 additions and 19 deletions

View File

@ -4,7 +4,6 @@ plugins {
}
group = "app.revanced"
version = "1.0"
repositories {
mavenCentral()
@ -16,6 +15,9 @@ repositories {
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE!
}
}
maven {
url = uri("https://jitpack.io")
}
}
val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4"
@ -29,8 +31,12 @@ dependencies {
implementation("com.google.code.gson:gson:2.9.0")
implementation("me.tongfei:progressbar:0.9.3")
implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead.
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
}
val cliMainClass = "app.revanced.cli.Main"
tasks {
build {
dependsOn(shadowJar)
@ -42,7 +48,7 @@ tasks {
exclude(dependency(patchesDependency))
}
manifest {
attributes("Main-Class" to "app.revanced.cli.Main")
attributes("Main-Class" to cliMainClass)
attributes("Implementation-Title" to project.name)
attributes("Implementation-Version" to project.version)
}

View File

@ -1 +1,2 @@
kotlin.code.style=official
version = 1.0.0-dev

View File

@ -1,5 +1,6 @@
package app.revanced.cli
import app.revanced.cli.runner.Emulator
import app.revanced.cli.utils.PatchLoader
import app.revanced.cli.utils.Patches
import app.revanced.cli.utils.Preconditions
@ -8,6 +9,7 @@ import app.revanced.patcher.patch.PatchMetadata
import app.revanced.patcher.patch.PatchResult
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.required
import me.tongfei.progressbar.ProgressBarBuilder
import me.tongfei.progressbar.ProgressBarStyle
@ -23,7 +25,9 @@ class Main {
inApk: String,
inPatches: String,
inIntegrations: String?,
inOutput: String,
inOutput: String?,
inEmulate: String?,
hideResults: Boolean,
) {
val bar = ProgressBarBuilder()
.setTaskName("Working..")
@ -35,12 +39,9 @@ class Main {
.setExtraMessage("Initializing")
val apk = Preconditions.isFile(inApk)
val patchesFile = Preconditions.isFile(inPatches)
val output = Preconditions.isDirectory(inOutput)
bar.step()
val patcher = Patcher(
apk,
)
val patcher = Patcher(apk)
inIntegrations?.let {
bar.reset().maxHint(1)
@ -53,12 +54,14 @@ class Main {
bar.reset().maxHint(1)
.extraMessage = "Loading patches"
PatchLoader.injectPatches(patchesFile)
bar.step()
val patches = Patches.loadPatches().map { it() }
patcher.addPatches(patches)
bar.step()
bar.reset().maxHint(1)
.extraMessage = "Resolving signatures"
patcher.resolveSignatures()
bar.step()
val amount = patches.size.toLong()
bar.reset().maxHint(amount)
@ -70,24 +73,41 @@ class Main {
bar.reset().maxHint(-1)
.extraMessage = "Generating dex files"
val dexFiles = patcher.save()
bar.reset().maxHint(dexFiles.size.toLong())
.extraMessage = "Saving dex files"
dexFiles.forEach { (dexName, dexData) ->
Files.write(File(output, dexName).toPath(), dexData.data)
bar.step()
inOutput?.let {
val output = Preconditions.isDirectory(it)
val amount = dexFiles.size.toLong()
bar.reset().maxHint(amount)
.extraMessage = "Saving dex files"
dexFiles.forEach { (dexName, dexData) ->
Files.write(File(output, dexName).toPath(), dexData.data)
bar.step()
}
bar.stepTo(amount)
}
bar.close()
inEmulate?.let { device ->
Emulator.emulate(
apk,
dexFiles,
device
)
}
println("All done!")
printResults(results)
if (!hideResults) {
printResults(results)
}
}
private fun printResults(results: Map<PatchMetadata, Result<PatchResult>>) {
for ((metadata, result) in results) {
if (result.isSuccess) {
println("${metadata.name} was applied successfully!")
println("${metadata.shortName} was applied successfully!")
} else {
println("${metadata.name} failed to apply! Cause:")
println("${metadata.shortName} failed to apply! Cause:")
result.exceptionOrNull()!!.printStackTrace()
}
}
@ -98,6 +118,9 @@ class Main {
println("$CLI_NAME version $CLI_VERSION")
val parser = ArgParser(CLI_NAME)
// TODO: add some kind of incremental building, so merging integrations can be skipped.
// this can be achieved manually, but doing it automatically is better.
val apk by parser.option(
ArgType.String,
fullName = "apk",
@ -121,7 +144,18 @@ class Main {
fullName = "output",
shortName = "o",
description = "Output directory"
).required()
)
val emulate by parser.option(
ArgType.String,
fullName = "run-on",
description = "After the CLI is done building, which ADB device should it run on?"
)
// TODO: package name
val hideResults by parser.option(
ArgType.Boolean,
fullName = "hide-results",
description = "Don't print the patch results."
).default(false)
parser.parse(args)
runCLI(
@ -129,6 +163,8 @@ class Main {
patches,
integrations,
output,
emulate,
hideResults,
)
}
}

View File

@ -0,0 +1,140 @@
package app.revanced.cli.runner
import app.revanced.cli.utils.DexReplacer
import app.revanced.cli.utils.Scripts
import app.revanced.cli.utils.signer.Signer
import me.tongfei.progressbar.ProgressBar
import me.tongfei.progressbar.ProgressBarBuilder
import me.tongfei.progressbar.ProgressBarStyle
import org.jf.dexlib2.writer.io.MemoryDataStore
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder
import java.io.File
import java.nio.file.Files
import java.util.concurrent.Executors
object Emulator {
fun emulate(
apk: File,
dexFiles: Map<String, MemoryDataStore>,
deviceName: String
) {
lateinit var dvc: JadbDevice
pbar("Initializing").use { bar ->
dvc = JadbConnection().findDevice(deviceName)
?: throw IllegalArgumentException("No such device with name $deviceName")
if (!dvc.hasSu())
throw IllegalArgumentException("Device $deviceName is not rooted or does not have su")
bar.step()
}
lateinit var tmpFile: File // we need this file at the end to clean up.
pbar("Generating APK file", 3).use { bar ->
bar.step().extraMessage = "Creating APK file"
tmpFile = Files.createTempFile("rvc-cli", ".apk").toFile()
apk.copyTo(tmpFile, true)
bar.step().extraMessage = "Replacing dex files"
DexReplacer.replaceDex(tmpFile, dexFiles)
bar.step().extraMessage = "Signing APK file"
Signer.signApk(tmpFile)
}
pbar("Running application", 6, false).use { bar ->
bar.step().extraMessage = "Pushing mount scripts"
dvc.push(Scripts.MOUNT_SCRIPT, RemoteFile(Scripts.SCRIPT_PATH))
dvc.cmd(Scripts.CREATE_DIR_COMMAND).assertZero()
dvc.cmd(Scripts.MV_MOUNT_COMMAND).assertZero()
dvc.cmd(Scripts.CHMOD_MOUNT_COMMAND).assertZero()
bar.step().extraMessage = "Pushing APK file"
dvc.push(tmpFile, RemoteFile(Scripts.APK_PATH))
bar.step().extraMessage = "Mounting APK file"
dvc.cmd(Scripts.STOP_APP_COMMAND).startAndWait()
dvc.cmd(Scripts.START_MOUNT_COMMAND).assertZero()
bar.step().extraMessage = "Starting APK file"
dvc.cmd(Scripts.START_APP_COMMAND).assertZero()
bar.step().setExtraMessage("Debugging APK file").refresh()
println("\nWaiting until app is closed.")
val executor = Executors.newSingleThreadExecutor()
val p = dvc.cmd(Scripts.LOGCAT_COMMAND)
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.useExecutor(executor)
.start()
Thread.sleep(250) // give the app some time to start up.
while (dvc.cmd(Scripts.PIDOF_APP_COMMAND).startAndWait() == 0) {
Thread.sleep(250)
}
println("App closed, continuing.")
p.destroy()
executor.shutdown()
bar.step().extraMessage = "Unmounting APK file"
var exitCode: Int
do {
exitCode = dvc.cmd(Scripts.UNMOUNT_COMMAND).startAndWait()
} while (exitCode != 0)
}
tmpFile.delete()
}
}
private fun JadbDevice.push(s: String, remoteFile: RemoteFile) =
this.push(s.byteInputStream(), System.currentTimeMillis(), 644, remoteFile)
private fun JadbConnection.findDevice(device: String): JadbDevice? {
return devices.find { it.serial == device }
}
private fun JadbDevice.cmd(s: String): ShellProcessBuilder {
val args = s.split(" ") as ArrayList<String>
val cmd = args.removeFirst()
return shellProcessBuilder(cmd, *args.toTypedArray())
}
private fun JadbDevice.hasSu(): Boolean {
return cmd("su -h").startAndWait() == 0
}
private fun ShellProcessBuilder.startAndWait(): Int {
return start().waitFor()
}
private fun ShellProcessBuilder.assertZero() {
if (startAndWait() != 0) {
val cmd = getcmd()
throw IllegalStateException("ADB returned non-zero status code for command: $cmd")
}
}
private fun pbar(task: String, steps: Long = 1, update: Boolean = true): ProgressBar {
val b = ProgressBarBuilder().setTaskName(task)
if (update) b
.setUpdateIntervalMillis(250)
.continuousUpdate()
return b
.setStyle(ProgressBarStyle.ASCII)
.build()
.maxHint(steps + 1)
}
private fun ProgressBar.use(block: (ProgressBar) -> Unit) {
block(this)
stepTo(max) // step to 100%
extraMessage = "" // clear extra message
close()
}
private fun ShellProcessBuilder.getcmd(): String {
val f = this::class.java.getDeclaredField("command")
f.isAccessible = true
return f.get(this) as String
}

View File

@ -0,0 +1,31 @@
package app.revanced.cli.utils
import lanchon.multidexlib2.BasicDexFileNamer
import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
val NAMER = BasicDexFileNamer()
object DexReplacer {
fun replaceDex(source: File, dexFiles: Map<String, MemoryDataStore>) {
FileSystems.newFileSystem(
source.toPath(),
null
).use { fs ->
// Delete all classes?.dex files
Files.walk(fs.rootDirectories.first()).forEach {
if (
it.toString().endsWith(".dex") &&
NAMER.isValidName(it.fileName.toString())
) Files.delete(it)
}
// Write new dex files
dexFiles
.forEach { (dexName, dexData) ->
Files.write(fs.getPath("/$dexName"), dexData.data)
}
}
}
}

View File

@ -0,0 +1,34 @@
package app.revanced.cli.utils
// TODO: make this a class with PACKAGE_NAME as argument, then use that everywhere.
// make sure
object Scripts {
private const val PACKAGE_NAME = "com.google.android.youtube"
private const val DATA_PATH = "/data/adb/ReVanced"
const val APK_PATH = "/sdcard/base.apk"
const val SCRIPT_PATH = "/sdcard/mount.sh"
val MOUNT_SCRIPT =
"""
base_path="$DATA_PATH/base.apk"
stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' }
umount -l ${'$'}stock_path
rm ${'$'}base_path
mv "$APK_PATH" ${'$'}base_path
chmod 644 ${'$'}base_path
chown system:system ${'$'}base_path
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
mount -o bind ${'$'}base_path ${'$'}stock_path
""".trimIndent()
const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME"
private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)"
const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\""
const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\""
const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\""
const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh"
const val UNMOUNT_COMMAND = "su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\""
const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\""
const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\""
const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1"
}

View File

@ -0,0 +1,9 @@
package app.revanced.cli.utils.signer
import java.security.PrivateKey
import java.security.cert.X509Certificate
data class KeySet(
val publicKey: X509Certificate,
val privateKey: PrivateKey
)

View File

@ -0,0 +1,211 @@
/*
* Copyright (c) 2021 Juby210 & Vendicated
* Licensed under the Open Software License version 3.0
*/
package app.revanced.cli.utils.signer
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaCertStore
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cms.*
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.ContentSigner
import org.bouncycastle.operator.DigestCalculatorProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
import org.bouncycastle.util.encoders.Base64
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.math.BigInteger
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.security.*
import java.security.cert.X509Certificate
import java.util.*
import java.util.jar.Attributes
import java.util.jar.JarFile
import java.util.jar.Manifest
import java.util.regex.Pattern
const val CN = "ReVanced"
val PASSWORD = "revanced".toCharArray() // TODO: make it secure; random password should be enough
/**
* APK Signer.
* @author Aliucord authors
* @author ReVanced Team
*/
object Signer {
private fun newKeystore(out: File) {
val key = createKey()
val privateKS = KeyStore.getInstance("BKS", "BC")
privateKS.load(null, PASSWORD)
privateKS.setKeyEntry("alias", key.privateKey, PASSWORD, arrayOf(key.publicKey))
privateKS.store(FileOutputStream(out), PASSWORD)
}
private fun createKey(): KeySet {
val gen = KeyPairGenerator.getInstance("RSA")
gen.initialize(2048)
val pair = gen.generateKeyPair()
var serialNumber: BigInteger
do serialNumber =
BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
val x500Name = X500Name("CN=$CN")
val builder = X509v3CertificateBuilder(
x500Name,
serialNumber,
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
Locale.ENGLISH,
x500Name,
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
)
val signer: ContentSigner = JcaContentSignerBuilder("SHA1withRSA").build(pair.private)
return KeySet(JcaX509CertificateConverter().getCertificate(builder.build(signer)), pair.private)
}
private val stripPattern: Pattern = Pattern.compile("^META-INF/(.*)[.](MF|SF|RSA|DSA)$")
// based on https://gist.github.com/mmuszkow/10288441
// and https://github.com/fornwall/apksigner/blob/master/src/main/java/net/fornwall/apksigner/ZipSigner.java
fun signApk(apkFile: File) {
Security.addProvider(BouncyCastleProvider())
val ks = File(apkFile.parent, "revanced-cli.keystore")
if (!ks.exists()) newKeystore(ks)
val keyStore = KeyStore.getInstance("BKS", "BC")
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
val alias = keyStore.aliases().nextElement()
val keySet = KeySet(
(keyStore.getCertificate(alias) as X509Certificate),
(keyStore.getKey(alias, PASSWORD) as PrivateKey)
)
val zip = FileSystems.newFileSystem(apkFile.toPath(), null)
val dig = MessageDigest.getInstance("SHA1")
val digests: MutableMap<String, String> = LinkedHashMap()
for (entry in zip.allEntries) {
val name = entry.toString()
if (stripPattern.matcher(name).matches()) {
Files.delete(entry)
} else {
digests[name] = toBase64(dig.digest(Files.readAllBytes(entry)))
}
}
val sectionDigests: MutableMap<String, String> = LinkedHashMap()
var manifest = Manifest()
var attrs = manifest.mainAttributes
attrs[Attributes.Name.MANIFEST_VERSION] = "1.0"
attrs[Attributes.Name("Created-By")] = CN
val digestAttr = Attributes.Name("SHA1-Digest")
for ((name, value) in digests) {
val attributes = Attributes()
attributes[digestAttr] = value
manifest.entries[name] = attributes
sectionDigests[name] = hashEntrySection(name, attributes, dig)
}
ByteArrayOutputStream().use { baos ->
manifest.write(baos)
zip.writeFile(JarFile.MANIFEST_NAME, baos.toByteArray())
}
val manifestHash = getManifestHash(manifest, dig)
val tmpManifest = Manifest()
tmpManifest.mainAttributes.putAll(attrs)
val manifestMainHash = getManifestHash(tmpManifest, dig)
manifest = Manifest()
attrs = manifest.mainAttributes
attrs[Attributes.Name.SIGNATURE_VERSION] = "1.0"
attrs[Attributes.Name("Created-By")] = CN
attrs[Attributes.Name("SHA1-Digest-Manifest")] = manifestHash
attrs[Attributes.Name("SHA1-Digest-Manifest-Main-Attributes")] = manifestMainHash
for ((key, value) in sectionDigests) {
val attributes = Attributes()
attributes[digestAttr] = value
manifest.entries[key] = attributes
}
var sigBytes: ByteArray
ByteArrayOutputStream().use { sigStream ->
manifest.write(sigStream)
sigBytes = sigStream.toByteArray()
zip.writeFile("META-INF/CERT.SF", sigBytes)
}
val signature = signSigFile(keySet, sigBytes)
zip.writeFile("META-INF/CERT.RSA", signature)
zip.close()
}
private fun hashEntrySection(name: String, attrs: Attributes, dig: MessageDigest): String {
val manifest = Manifest()
manifest.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
ByteArrayOutputStream().use { baos ->
manifest.write(baos)
val emptyLen = baos.toByteArray().size
manifest.entries[name] = attrs
baos.reset()
manifest.write(baos)
var ob = baos.toByteArray()
ob = Arrays.copyOfRange(ob, emptyLen, ob.size)
return toBase64(dig.digest(ob))
}
}
private fun getManifestHash(manifest: Manifest, dig: MessageDigest): String {
ByteArrayOutputStream().use { baos ->
manifest.write(baos)
return toBase64(dig.digest(baos.toByteArray()))
}
}
private fun signSigFile(keySet: KeySet, content: ByteArray): ByteArray {
val msg: CMSTypedData = CMSProcessableByteArray(content)
val certs = JcaCertStore(Collections.singletonList(keySet.publicKey))
val gen = CMSSignedDataGenerator()
val jcaContentSignerBuilder = JcaContentSignerBuilder("SHA1withRSA")
val sha1Signer: ContentSigner = jcaContentSignerBuilder.build(keySet.privateKey)
val jcaDigestCalculatorProviderBuilder = JcaDigestCalculatorProviderBuilder()
val digestCalculatorProvider: DigestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build()
val jcaSignerInfoGeneratorBuilder = JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
jcaSignerInfoGeneratorBuilder.setDirectSignature(true)
val signerInfoGenerator: SignerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.publicKey)
gen.addSignerInfoGenerator(signerInfoGenerator)
gen.addCertificates(certs)
val sigData: CMSSignedData = gen.generate(msg, false)
return sigData.toASN1Structure().getEncoded("DER")
}
private fun toBase64(data: ByteArray): String {
return String(Base64.encode(data))
}
}
private val java.nio.file.FileSystem.allEntries: List<Path>
get() = buildList {
this@allEntries.rootDirectories.forEach { dir ->
Files.walk(dir).filter(Files::isRegularFile).forEach { file ->
this@buildList.add(file)
}
}
}
private fun java.nio.file.FileSystem.writeFile(path: String, bytes: ByteArray) {
Files.write(this.getPath("/$path"), bytes)
}