Skip to content

Commit c51cbf8

Browse files
committed
Make apps forcefully debuggable: work-in-progress
1 parent 6869725 commit c51cbf8

File tree

4 files changed

+148
-20
lines changed

4 files changed

+148
-20
lines changed

README.md

+19-3
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,28 @@ ANDROID_SDK_ROOT=/path/to/sdk ./gradlew assemble
3636
The `deployer` folder in this project contains a convenience application to push the CoverageAgent
3737
to an Android device using `adb`.
3838

39-
In order to instrument apps that don't have the `android:debuggable` attribute set, you must ensure
40-
you have root access on the device and `ro.debuggable` is set.
41-
4239
```bash
4340
gradle run --args="your.android.package.name"
4441
```
4542

4643
It will locate the app's data directory and push the coverage agent into the
4744
`DATA_DIR/code_cache/startup_agents` directory.
45+
46+
## Using with non-debuggable apps
47+
48+
In order to instrument apps that don't have the `android:debuggable` attribute set, you must ensure
49+
you have root access on the device and `ro.debuggable` is set. The deployer can toggle the
50+
debuggable bit in the system process. Firstly, ensure that
51+
52+
```bash
53+
setprop persist.debug.dalvik.vm.jdwp.enabled 1
54+
```
55+
56+
is set and restart the device after setting this property.
57+
58+
Next, invoke the deployer with the `--force-debuggable` to have it deploy the coverage agent and
59+
flip the debug bit for you.
60+
61+
```bash
62+
gradle run --args="your.android.package.name --force-debuggable"
63+
```

deployer/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ repositories {
88
}
99

1010
dependencies {
11+
implementation 'org.jdiscript:jdiscript:0.9.0'
12+
1113
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1'
1214
}
1315

deployer/src/main/java/com/ammaraskar/coverageagent/Deployer.kt

+24-17
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import java.io.File
44
import java.util.*
55
import java.util.concurrent.TimeUnit
66

7-
class Deployer {
7+
class Deployer(private val adbDeviceName: String?) {
88

99
private val soName = "libcoverage_instrumenting_agent.so"
1010

11-
fun deploy(packageName: String, adbDeviceName: String?) {
11+
fun deploy(packageName: String) {
1212
println("Instrumenting app $packageName with coverage agent.")
1313
// Get the architecture of the device.
1414
val architecture = getDeviceArchitecture(adbDeviceName)
@@ -18,11 +18,11 @@ class Deployer {
1818
val library = File("runtime_cpp/build/intermediates/merged_native_libs/debug/out/lib/${architecture}/${soName}")
1919
println("[i] Using library: ${library.absolutePath}")
2020

21-
runAdbCommand(adbDeviceName, "push", library.absolutePath, "/data/local/tmp/")
21+
runAdbCommand("push", library.absolutePath, "/data/local/tmp/")
2222
println("[+] Pushed library to /data/local/tmp/${soName}")
2323

2424
println("[i] Trying to use run-as to copy to startup_agents")
25-
val copyDestinationWithRunAs = tryToCopyLibraryWithRunAs(packageName, adbDeviceName)
25+
val copyDestinationWithRunAs = tryToCopyLibraryWithRunAs(packageName)
2626
if (copyDestinationWithRunAs.isPresent) {
2727
println("[+] Library copied to ${copyDestinationWithRunAs.get()}")
2828
return
@@ -31,7 +31,7 @@ class Deployer {
3131
println("[x] run-as failed, using su permissions instead.")
3232

3333
// Use dumpsys package to figure out the data directory and user id of the application.
34-
val dumpSysOutput = runAdbCommand(adbDeviceName, "shell", "dumpsys", "package", packageName)
34+
val dumpSysOutput = runAdbCommand("shell", "dumpsys", "package", packageName)
3535

3636
var dataDir: String? = null
3737
var userId: String? = null
@@ -46,33 +46,33 @@ class Deployer {
4646
}
4747
println("[i] Grabbed app's dataDir=$dataDir and userId=$userId")
4848

49-
runAdbCommand(adbDeviceName,
49+
runAdbCommand(
5050
"shell", "su", userId, "\"mkdir -p $dataDir/code_cache/startup_agents/\"")
51-
runAdbCommand(adbDeviceName,
51+
runAdbCommand(
5252
"shell", "su", userId, "\"cp /data/local/tmp/${soName} $dataDir/code_cache/startup_agents/\"")
5353
println("[+] Library copied to $dataDir/code_cache/startup_agents/")
5454
}
5555

5656
private fun getDeviceArchitecture(adbDeviceName: String?): String {
57-
return runAdbCommand(adbDeviceName, "shell", "getprop", "ro.product.cpu.abi").trim()
57+
return runAdbCommand("shell", "getprop", "ro.product.cpu.abi").trim()
5858
}
5959

60-
private fun tryToCopyLibraryWithRunAs(packageName: String, adbDeviceName: String?): Optional<String> {
60+
private fun tryToCopyLibraryWithRunAs(packageName: String): Optional<String> {
6161
return try {
62-
runAdbCommand(adbDeviceName, "shell", "run-as", packageName, "mkdir -p code_cache/startup_agents/")
63-
runAdbCommand(adbDeviceName, "shell", "run-as", packageName, "cp /data/local/tmp/${soName} code_cache/startup_agents/")
62+
runAdbCommand("shell", "run-as", packageName, "mkdir -p code_cache/startup_agents/")
63+
runAdbCommand("shell", "run-as", packageName, "cp /data/local/tmp/${soName} code_cache/startup_agents/")
6464

65-
Optional.of(runAdbCommand(adbDeviceName, "shell", "run-as", packageName, "pwd"))
65+
Optional.of(runAdbCommand("shell", "run-as", packageName, "pwd"))
6666
} catch (e: RuntimeException) {
6767
Optional.empty()
6868
}
6969
}
7070

71-
private fun runAdbCommand(adbDeviceName: String?, vararg command: String): String {
71+
fun runAdbCommand(vararg command: String): String {
7272
val adbCommand = mutableListOf("adb")
73-
if (adbDeviceName != null) {
73+
if (this.adbDeviceName != null) {
7474
adbCommand.add("-s")
75-
adbCommand.add(adbDeviceName)
75+
adbCommand.add(this.adbDeviceName)
7676
}
7777
adbCommand.addAll(command)
7878
return runCommandAndGetOutput(adbCommand)
@@ -100,9 +100,16 @@ class Deployer {
100100

101101
fun main(args: Array<String>) {
102102
if (args.isEmpty()) {
103-
println("Usage: Deployer <android-package-name> [adb-device-name]")
103+
println("Usage: Deployer <android-package-name> [--device=adb-device-name] [--force-debuggable]")
104104
return
105105
}
106106

107-
Deployer().deploy(packageName = args[0], adbDeviceName = args.getOrNull(1))
107+
val deviceName = args.filter { it.startsWith("--device=") }.map { it.replace("--device=", "") }.firstOrNull()
108+
109+
val deployer = Deployer(deviceName)
110+
if ("--force-debuggable" in args) {
111+
ForceAppDebuggable(deployer).makeDebuggable(packageName = args[0])
112+
}
113+
114+
deployer.deploy(packageName = args[0])
108115
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.ammaraskar.coverageagent
2+
3+
import com.sun.jdi.*
4+
import org.jdiscript.JDIScript
5+
import org.jdiscript.util.VMSocketAttacher
6+
import java.lang.IllegalArgumentException
7+
8+
/**
9+
* Local port to forward the jdwp connection to.
10+
*/
11+
const val HOST_PORT = 8690;
12+
13+
const val PACKAGE_SETTINGS_CLASS = "com.android.server.pm.PackageSetting";
14+
15+
/**
16+
* Forces an app to be debuggable by connecting to Android with a jvm debugger and altering the
17+
* PackageManager's memory.
18+
*/
19+
class ForceAppDebuggable(private var deployer: Deployer) {
20+
21+
fun makeDebuggable(packageName: String) {
22+
// Kill any existing adb servers, Android Studio's adb server hijacks all jdwp connections
23+
// and makes them not work for any other clients.
24+
println("[i] Restarting adb daemon")
25+
deployer.runAdbCommand("kill-server")
26+
deployer.runAdbCommand("start-server")
27+
deployer.runAdbCommand("wait-for-device")
28+
29+
println("[i] Retrieving pid for system_server")
30+
31+
val pid = deployer.runAdbCommand("shell", "pidof", "system_server").trim()
32+
println("[+] Got system_server pid: $pid")
33+
34+
println("[i] Forwarding jdwp port for system_server")
35+
deployer.runAdbCommand("forward", "tcp:$HOST_PORT", "jdwp:$pid")
36+
37+
println("[i] Connecting to jdwp socket with jdiscript...")
38+
changeDebugFlagWithDebugger(packageName)
39+
}
40+
41+
private fun changeDebugFlagWithDebugger(packageName: String) {
42+
val vm = VMSocketAttacher("localhost", HOST_PORT, 30).attach()
43+
println("[+] Debugger attached!")
44+
val j = JDIScript(vm)
45+
46+
val packageSettingsClass = j.vm().classesByName(PACKAGE_SETTINGS_CLASS).firstOrNull()
47+
?: throw IllegalStateException("$PACKAGE_SETTINGS_CLASS not found in system server java process");
48+
49+
var nameField: Field? = null;
50+
var flagsField: Field? = null;
51+
52+
for (field in packageSettingsClass.allFields()) {
53+
if (field.name().equals("name", ignoreCase = true) || field.name().equals("mName", ignoreCase = true)) {
54+
nameField = field;
55+
}
56+
if (field.name().equals("pkgFlags", ignoreCase = true)) {
57+
flagsField = field;
58+
}
59+
}
60+
61+
// Make sure we actually managed to find the fields.
62+
if (nameField == null) {
63+
throw IllegalStateException("nameField was null :(")
64+
}
65+
if (flagsField == null) {
66+
throw IllegalStateException("flagsField was null :(")
67+
}
68+
69+
for (settings in packageSettingsClass.instances(512)) {
70+
val instancePackageName = getJdiValueAsString(settings.getValue(nameField));
71+
//println("Package name: $instancePackageName")
72+
if (!instancePackageName.equals(packageName, ignoreCase = true)) {
73+
continue;
74+
}
75+
76+
val flags = getJdiValueAsLong(settings.getValue(flagsField));
77+
println("[+] Found package settings, changing flags.")
78+
println("Instance - ${settings.uniqueID()}, mName=$instancePackageName flags=$flags")
79+
80+
// Binary OR with the debuggable flag, 0x2
81+
val newFlags = flags or 0x2
82+
settings.setValue(flagsField, j.vm().mirrorOf(newFlags))
83+
84+
println("[+] Flag changed.")
85+
}
86+
87+
j.vm().dispose()
88+
}
89+
90+
private fun getJdiValueAsLong(value: Value): Int {
91+
if (value is IntegerValue) {
92+
return value.value();
93+
}
94+
throw IllegalArgumentException("getJdiValueAsLong called on value of type ${value.type()}")
95+
}
96+
97+
private fun getJdiValueAsString(value: Value): String {
98+
if (value is StringReference) {
99+
return value.value();
100+
}
101+
throw IllegalArgumentException("getJdiValueAsString called on value of type ${value.type()}")
102+
}
103+
}

0 commit comments

Comments
 (0)