38 Commits

Author SHA1 Message Date
snake-4
72f96df644 Bumped version to 2.1.0 2024-04-24 08:29:08 +02:00
snake-4
e03060eb22 Added Zygisk hide for 27.0 and renamed unmount.cpp 2024-04-24 07:45:04 +02:00
snake-4
71eee6bf92 Added ELFIO submodule 2024-04-24 07:01:50 +02:00
snake-4
29d4adca9e Changes to build system 2024-04-22 01:57:38 +02:00
snake-4
c0246483b0 Fixed companion FD for child zygotes 2024-04-17 00:27:40 +02:00
snake-4
e22e81d792 Updated AGP to 8.3.2...
and misc changes.
2024-04-16 23:16:21 +02:00
snake-4
5e8a2c4631 Added setresuid hook for older Android versions 2024-04-16 22:43:03 +02:00
snake-4
a7701e8b11 Updated logs and remount logic 2024-04-16 21:08:45 +02:00
snake-4
f200aa4561 Added root companion and logging macros 2024-04-16 21:08:10 +02:00
snake-4
2962997439 Moved unmount to unshare hook
This might break older Android versions. Not sure.
2024-04-16 21:07:47 +02:00
snake-4
6a61f6a4b6 Renamed some functions 2024-04-14 18:37:38 +02:00
snake-4
6e333581fb Added remount function and changed bind mount...
prefix.
2024-04-14 16:23:38 +02:00
snake-4
ac910f0ebe Unhook unshare at postAppSpecialize 2024-04-14 16:21:52 +02:00
snake-4
df3e492391 Update update.json 2024-04-12 12:50:47 +02:00
snake-4
7ec3214927 Bumped version to 2.0.4 2024-04-12 12:43:42 +02:00
snake-4
c797376230 Update main.cpp 2024-04-10 00:52:12 +02:00
snake-4
33a9ff93f4 Updated ci.yml and fixed build 2024-04-09 23:07:04 +02:00
snake-4
6f4d78b0fc Added maps parser and fixed child zygote namespaces 2024-04-09 22:35:40 +02:00
snake-4
08547864cd Added missing header guards, bumped to C++20 2024-04-09 20:32:28 +02:00
snake-4
d59ac2bf26 Updated AGP and compileSdkVersion 2024-04-09 05:34:27 +02:00
snake-4
52e5771205 Updated AGP to 8.3.0 2024-04-06 20:17:39 +02:00
snake-4
ce278b37f7 Update update.json 2024-04-01 05:10:13 +02:00
snake-4
fe05fbd621 Bumped version to v2.0.3 2024-04-01 05:05:17 +02:00
snake-4
83f2880922 Added mountinfo parser and bind mount hiding
closes #3
2024-04-01 04:58:18 +02:00
snake-4
112c4027fe Update update.json 2024-04-01 02:02:14 +02:00
snake-4
aa9a8a04e3 Bumped version to v2.0.2 2024-04-01 01:58:09 +02:00
snake-4
670767a82b Added exception for hosts bind mount 2024-04-01 01:33:12 +02:00
snake-4
7200b77043 Fixed module template copy task during build
Fixes Magisk compatibility.
2024-04-01 01:23:58 +02:00
snake-4
a07e62fcd7 Create update.json 2024-03-31 23:20:22 +02:00
snake-4
0ee57d586f Added updateJson in module.prop, bumped to v2.0.1 2024-03-31 23:15:32 +02:00
snake-4
b3d8493612 Added commit hash to version strings 2024-03-26 19:49:18 +01:00
snake-4
b63132e9f8 Updated README.md and added LICENSE 2024-03-26 18:55:55 +01:00
snake-4
58782b21f8 unshare on parent Zygote only
Calling unshare on child Zygotes crash at a sanity check but they end up inheriting the namespace either way.

closes #1
2024-03-26 18:26:24 +01:00
snake-4
72afc9e4fc dlclose on servers processes too 2024-03-26 18:24:20 +01:00
snake-4
916558491a Moved headers to includes, various changes...
+ Added better UID checks.
+ Added MS_SLAVE root mount in the new mount namespace.
2024-03-26 15:37:07 +01:00
snake-4
ba80d6d181 Bumped version to v2.0.0 (200) 2024-03-25 21:37:11 +01:00
snake-4
c1070182a2 Added usermode unmounter and mounts parser
+ Added a mounts parser implementation using mntent API.
+ Added logging functions.
+ Added usermode unmounter which calls unshare pre app-specialization.
* Updated module.prop.
2024-03-25 21:36:47 +01:00
snake-4
08cc6f7e33 Changed to C++14, updated README.md 2024-03-25 21:21:45 +01:00
29 changed files with 1041 additions and 48 deletions

View File

@@ -1,28 +1,24 @@
name: CI
on:
workflow_dispatch:
push:
branches: [ main ]
tags: [ v* ]
pull_request:
merge_group:
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: "recursive"
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build with Gradle
run: |

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "libcxx"]
path = module/jni/libcxx
url = https://github.com/topjohnwu/libcxx.git
[submodule "module/jni/elfio"]
path = module/jni/elfio
url = https://github.com/serge1/ELFIO.git

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 snake-4
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,51 @@
## Zygisk Assistant
This module makes Zygisk more stealthy.
<h3 align="center">Zygisk Assistant</h3>
The module sets **DLCLOSE_MODULE_LIBRARY** and **FORCE_DENYLIST_UNMOUNT** Zygisk flags for all non-root application processes.
<p align="center">
A Zygisk module that aims to hide the existence root and Zygisk.
<br />
<br />
<a href="https://github.com/snake-4/Zygisk-Assistant/issues">Report Bug</a>
·
<a href="https://github.com/snake-4/Zygisk-Assistant/issues">Request Feature</a>
·
<a href="https://github.com/snake-4/Zygisk-Assistant/releases">Latest Release</a>
</p>
</div>
Please note that installing this module could lead to compatibility issues with other Zygisk modules. If you encounter any problems, please create an issue in this repository.
<!-- ABOUT THE PROJECT -->
## About The Project
Using the **release** build is recommended over the debug build. Only use debug builds if you are going to make a bug report.
### KernelSU & APatch users:
1. Install ZygiskNext.
1. Make sure the unmount setting is enabled for the target app in the KernelSU/APatch Manager.
1. Disable `Enforce DenyList` in ZygiskNext settings if there is one.
### Magisk users:
1. Update your Magisk to 27.0 or newer for better hiding capabilities. (optional)
1. Turn on Zygisk in Magisk settings.
1. Turn off `Enforce DenyList` in Magisk settings.
1. Add the target app to the deny list unless you're using a Magisk fork with a white list instead.
<!-- CONTRIBUTING -->
## Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
Don't forget to give the project a star! Thanks again!
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/FeatureName`)
3. Commit your Changes (`git commit -m 'Add some FeatureName'`)
4. Push to the Branch (`git push origin feature/FeatureName`)
5. Open a Pull Request
<!-- LICENSE -->
## License
Distributed under the MIT License. See `LICENSE` for more information.

View File

@@ -1,11 +1,19 @@
import java.io.ByteArrayOutputStream
plugins {
alias(libs.plugins.agp.lib) apply false
id("com.android.library") version "8.3.2" apply false
}
val commitHash: String by extra {
val stdout = ByteArrayOutputStream()
rootProject.exec {
commandLine("git", "rev-parse", "--short", "HEAD")
standardOutput = stdout
}
stdout.toString().trim()
}
val moduleId by extra("zygisk-assistant")
val moduleName by extra("Zygisk Assistant")
val verName by extra("v1.1.0")
val verCode by extra(110)
val abiList by extra(listOf("armeabi-v7a","arm64-v8a","x86","x86_64"))
val verName by extra("v2.1.0")
val verCode by extra(210)

View File

@@ -1,5 +0,0 @@
[versions]
agp = "8.2.0"
[plugins]
agp-lib = { id = "com.android.library", version.ref = "agp" }

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@@ -1,12 +1,13 @@
import android.databinding.tool.ext.capitalizeUS
plugins {
alias(libs.plugins.agp.lib)
id("com.android.library")
}
val moduleId: String by rootProject.extra
val moduleName: String by rootProject.extra
val verCode: Int by rootProject.extra
val commitHash: String by rootProject.extra
val verName: String by rootProject.extra
val abiList: List<String> by rootProject.extra
@@ -15,9 +16,6 @@ android {
compileSdkVersion = "android-31"
defaultConfig {
minSdk = 21
ndk {
abiFilters.addAll(abiList)
}
externalNativeBuild {
ndkBuild {
arguments("-j${Runtime.getRuntime().availableProcessors()}")
@@ -36,10 +34,10 @@ androidComponents.onVariants { variant ->
val variantCapped = variant.name.capitalizeUS()
val buildTypeLowered = variant.buildType?.lowercase()
val libOutDir = layout.buildDirectory.dir("intermediates/stripped_native_libs/$variantLowered/out/lib").get()
val libOutDir = layout.buildDirectory.dir("intermediates/stripped_native_libs/$variantLowered/strip${variantCapped}DebugSymbols/out/lib").get()
val moduleDir = layout.buildDirectory.dir("outputs/module/$variantLowered").get()
val zipOutDir = layout.buildDirectory.dir("outputs/release").get()
val zipFileName = "$moduleName-$verName-$verCode-$buildTypeLowered.zip".replace(' ', '-')
val zipFileName = "$moduleName-$verName-$commitHash-$buildTypeLowered.zip".replace(' ', '-')
val prepareModuleFilesTask = task<Sync>("prepareModuleFiles$variantCapped") {
group = "module"
@@ -50,10 +48,13 @@ androidComponents.onVariants { variant ->
expand(
"moduleId" to moduleId,
"moduleName" to moduleName,
"versionName" to "$verName ($verCode-$variantLowered)",
"versionName" to "$verName ($commitHash-$variantLowered)",
"versionCode" to verCode
)
}
from("$projectDir/template") {
exclude("module.prop")
}
from(libOutDir) {
into("zygisk")
}

View File

@@ -1,8 +1,9 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include $(LOCAL_PATH)/elfio
LOCAL_MODULE := zygisk
LOCAL_SRC_FILES := main.cpp
LOCAL_SRC_FILES := utils.cpp map_parser.cpp mount_parser.cpp mountinfo_parser.cpp modules.cpp main.cpp
LOCAL_STATIC_LIBRARIES := libcxx
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

1
module/jni/elfio Submodule

Submodule module/jni/elfio added at 45af83bc67

View File

@@ -0,0 +1,219 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This file is consumed by build/tools/fs_config and is used
* for generating various files. Anything #define AID_<name>
* becomes the mapping for getpwnam/getpwuid, etc. The <name>
* field is lowercased.
* For example:
* #define AID_FOO_BAR 6666 becomes a friendly name of "foo_bar"
*
* The above holds true with the exception of:
* mediacodec
* mediaex
* mediadrm
* Whose friendly names do not match the #define statements.
*
* This file must only be used for platform (Google managed, and submitted through AOSP), AIDs. 3rd
* party AIDs must be added via config.fs, which will place them in the corresponding partition's
* passwd and group files. There are ranges in this file reserved for AIDs for each 3rd party
* partition, from which the system reads passwd and group files.
*/
#pragma once
/* This is the main Users and Groups config for the platform.
* DO NOT EVER RENUMBER
*/
#define AID_ROOT 0 /* traditional unix root user */
/* The following are for LTP and should only be used for testing */
#define AID_DAEMON 1 /* traditional unix daemon owner */
#define AID_BIN 2 /* traditional unix binaries owner */
#define AID_SYSTEM 1000 /* system server */
#define AID_RADIO 1001 /* telephony subsystem, RIL */
#define AID_BLUETOOTH 1002 /* bluetooth subsystem */
#define AID_GRAPHICS 1003 /* graphics devices */
#define AID_INPUT 1004 /* input devices */
#define AID_AUDIO 1005 /* audio devices */
#define AID_CAMERA 1006 /* camera devices */
#define AID_LOG 1007 /* log devices */
#define AID_COMPASS 1008 /* compass device */
#define AID_MOUNT 1009 /* mountd socket */
#define AID_WIFI 1010 /* wifi subsystem */
#define AID_ADB 1011 /* android debug bridge (adbd) */
#define AID_INSTALL 1012 /* group for installing packages */
#define AID_MEDIA 1013 /* mediaserver process */
#define AID_DHCP 1014 /* dhcp client */
#define AID_SDCARD_RW 1015 /* external storage write access */
#define AID_VPN 1016 /* vpn system */
#define AID_KEYSTORE 1017 /* keystore subsystem */
#define AID_USB 1018 /* USB devices */
#define AID_DRM 1019 /* DRM server */
#define AID_MDNSR 1020 /* MulticastDNSResponder (service discovery) */
#define AID_GPS 1021 /* GPS daemon */
#define AID_UNUSED1 1022 /* deprecated, DO NOT USE */
#define AID_MEDIA_RW 1023 /* internal media storage write access */
#define AID_MTP 1024 /* MTP USB driver access */
#define AID_UNUSED2 1025 /* deprecated, DO NOT USE */
#define AID_DRMRPC 1026 /* group for drm rpc */
#define AID_NFC 1027 /* nfc subsystem */
#define AID_SDCARD_R 1028 /* external storage read access */
#define AID_CLAT 1029 /* clat part of nat464 */
#define AID_LOOP_RADIO 1030 /* loop radio devices */
#define AID_MEDIA_DRM 1031 /* MediaDrm plugins */
#define AID_PACKAGE_INFO 1032 /* access to installed package details */
#define AID_SDCARD_PICS 1033 /* external storage photos access */
#define AID_SDCARD_AV 1034 /* external storage audio/video access */
#define AID_SDCARD_ALL 1035 /* access all users external storage */
#define AID_LOGD 1036 /* log daemon */
#define AID_SHARED_RELRO 1037 /* creator of shared GNU RELRO files */
#define AID_DBUS 1038 /* dbus-daemon IPC broker process */
#define AID_TLSDATE 1039 /* tlsdate unprivileged user */
#define AID_MEDIA_EX 1040 /* mediaextractor process */
#define AID_AUDIOSERVER 1041 /* audioserver process */
#define AID_METRICS_COLL 1042 /* metrics_collector process */
#define AID_METRICSD 1043 /* metricsd process */
#define AID_WEBSERV 1044 /* webservd process */
#define AID_DEBUGGERD 1045 /* debuggerd unprivileged user */
#define AID_MEDIA_CODEC 1046 /* mediacodec process */
#define AID_CAMERASERVER 1047 /* cameraserver process */
#define AID_FIREWALL 1048 /* firewalld process */
#define AID_TRUNKS 1049 /* trunksd process (TPM daemon) */
#define AID_NVRAM 1050 /* Access-controlled NVRAM */
#define AID_DNS 1051 /* DNS resolution daemon (system: netd) */
#define AID_DNS_TETHER 1052 /* DNS resolution daemon (tether: dnsmasq) */
#define AID_WEBVIEW_ZYGOTE 1053 /* WebView zygote process */
#define AID_VEHICLE_NETWORK 1054 /* Vehicle network service */
#define AID_MEDIA_AUDIO 1055 /* GID for audio files on internal media storage */
#define AID_MEDIA_VIDEO 1056 /* GID for video files on internal media storage */
#define AID_MEDIA_IMAGE 1057 /* GID for image files on internal media storage */
#define AID_TOMBSTONED 1058 /* tombstoned user */
#define AID_MEDIA_OBB 1059 /* GID for OBB files on internal media storage */
#define AID_ESE 1060 /* embedded secure element (eSE) subsystem */
#define AID_OTA_UPDATE 1061 /* resource tracking UID for OTA updates */
#define AID_AUTOMOTIVE_EVS 1062 /* Automotive rear and surround view system */
#define AID_LOWPAN 1063 /* LoWPAN subsystem */
#define AID_HSM 1064 /* hardware security module subsystem */
#define AID_RESERVED_DISK 1065 /* GID that has access to reserved disk space */
#define AID_STATSD 1066 /* statsd daemon */
#define AID_INCIDENTD 1067 /* incidentd daemon */
#define AID_SECURE_ELEMENT 1068 /* secure element subsystem */
#define AID_LMKD 1069 /* low memory killer daemon */
#define AID_LLKD 1070 /* live lock daemon */
#define AID_IORAPD 1071 /* input/output readahead and pin daemon */
#define AID_GPU_SERVICE 1072 /* GPU service daemon */
#define AID_NETWORK_STACK 1073 /* network stack service */
#define AID_GSID 1074 /* GSI service daemon */
#define AID_FSVERITY_CERT 1075 /* fs-verity key ownership in keystore */
#define AID_CREDSTORE 1076 /* identity credential manager service */
#define AID_EXTERNAL_STORAGE 1077 /* Full external storage access including USB OTG volumes */
#define AID_EXT_DATA_RW 1078 /* GID for app-private data directories on external storage */
#define AID_EXT_OBB_RW 1079 /* GID for OBB directories on external storage */
#define AID_CONTEXT_HUB 1080 /* GID for access to the Context Hub */
/* Changes to this file must be made in AOSP, *not* in internal branches. */
#define AID_SHELL 2000 /* adb and debug shell user */
#define AID_CACHE 2001 /* cache access */
#define AID_DIAG 2002 /* access to diagnostic resources */
/* The range 2900-2999 is reserved for the vendor partition */
/* Note that the two 'OEM' ranges pre-dated the vendor partition, so they take the legacy 'OEM'
* name. Additionally, they pre-dated passwd/group files, so there are users and groups named oem_#
* created automatically for all values in these ranges. If there is a user/group in a passwd/group
* file corresponding to this range, both the oem_# and user/group names will resolve to the same
* value. */
#define AID_OEM_RESERVED_START 2900
#define AID_OEM_RESERVED_END 2999
/* The 3000 series are intended for use as supplemental group id's only.
* They indicate special Android capabilities that the kernel is aware of. */
#define AID_NET_BT_ADMIN 3001 /* bluetooth: create any socket */
#define AID_NET_BT 3002 /* bluetooth: create sco, rfcomm or l2cap sockets */
#define AID_INET 3003 /* can create AF_INET and AF_INET6 sockets */
#define AID_NET_RAW 3004 /* can create raw INET sockets */
#define AID_NET_ADMIN 3005 /* can configure interfaces and routing tables. */
#define AID_NET_BW_STATS 3006 /* read bandwidth statistics */
#define AID_NET_BW_ACCT 3007 /* change bandwidth statistics accounting */
#define AID_READPROC 3009 /* Allow /proc read access */
#define AID_WAKELOCK 3010 /* Allow system wakelock read/write access */
#define AID_UHID 3011 /* Allow read/write to /dev/uhid node */
/* The range 5000-5999 is also reserved for vendor partition. */
#define AID_OEM_RESERVED_2_START 5000
#define AID_OEM_RESERVED_2_END 5999
/* The range 6000-6499 is reserved for the system partition. */
#define AID_SYSTEM_RESERVED_START 6000
#define AID_SYSTEM_RESERVED_END 6499
/* The range 6500-6999 is reserved for the odm partition. */
#define AID_ODM_RESERVED_START 6500
#define AID_ODM_RESERVED_END 6999
/* The range 7000-7499 is reserved for the product partition. */
#define AID_PRODUCT_RESERVED_START 7000
#define AID_PRODUCT_RESERVED_END 7499
/* The range 7500-7999 is reserved for the system_ext partition. */
#define AID_SYSTEM_EXT_RESERVED_START 7500
#define AID_SYSTEM_EXT_RESERVED_END 7999
#define AID_EVERYBODY 9997 /* shared between all apps in the same profile */
#define AID_MISC 9998 /* access to misc storage */
#define AID_NOBODY 9999
#define AID_APP 10000 /* TODO: switch users over to AID_APP_START */
#define AID_APP_START 10000 /* first app user */
#define AID_APP_END 19999 /* last app user */
#define AID_CACHE_GID_START 20000 /* start of gids for apps to mark cached data */
#define AID_CACHE_GID_END 29999 /* end of gids for apps to mark cached data */
#define AID_EXT_GID_START 30000 /* start of gids for apps to mark external data */
#define AID_EXT_GID_END 39999 /* end of gids for apps to mark external data */
#define AID_EXT_CACHE_GID_START 40000 /* start of gids for apps to mark external cached data */
#define AID_EXT_CACHE_GID_END 49999 /* end of gids for apps to mark external cached data */
#define AID_SHARED_GID_START 50000 /* start of gids for apps in each user to share */
#define AID_SHARED_GID_END 59999 /* end of gids for apps in each user to share */
/*
* This is a magic number in the kernel and not something that was picked
* arbitrarily. This value is returned whenever a uid that has no mapping in the
* user namespace is returned to userspace:
* https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/highuid.h?h=v4.4#n40
*/
#define AID_OVERFLOWUID 65534 /* unmapped user in the user namespace */
/* use the ranges below to determine whether a process is isolated */
#define AID_ISOLATED_START 90000 /* start of uids for fully isolated sandboxed processes */
#define AID_ISOLATED_END 99999 /* end of uids for fully isolated sandboxed processes */
#define AID_USER 100000 /* TODO: switch users over to AID_USER_OFFSET */
#define AID_USER_OFFSET 100000 /* offset for uid ranges for each user */
/*
* android_ids has moved to pwd/grp functionality.
* If you need to add one, the structure is now
* auto-generated based on the AID_ constraints
* documented at the top of this header file.
* Also see build/tools/fs_config for more details.
*/

View File

@@ -0,0 +1,16 @@
#pragma once
#include <android/log.h>
#include <cstring>
#include <cerrno>
#include <cinttypes>
#ifndef NDEBUG
static constexpr auto TAG = "ZygiskAssistant/JNI";
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#else
#define LOGD(...)
#define LOGW(...)
#define LOGE(...)
#endif

View File

@@ -0,0 +1,28 @@
#pragma once
#include <sstream>
#include <string>
#include <vector>
#include <sys/types.h>
class map_entry_t
{
public:
map_entry_t(uintptr_t address_start, uintptr_t address_end, uintptr_t offset,
const std::string &perms, const std::string &pathname, dev_t device, ino_t inode);
uintptr_t getAddressStart() const;
uintptr_t getAddressEnd() const;
const std::string &getPerms() const;
uintptr_t getOffset() const;
dev_t getDevice() const;
ino_t getInode() const;
const std::string &getPathname() const;
private:
uintptr_t address_start, address_end, offset;
std::string perms, pathname;
dev_t device;
ino_t inode;
};
std::vector<map_entry_t> parseMapsFromPath(const char *path);

View File

@@ -0,0 +1,5 @@
#pragma once
void doUnmount();
void doRemount();
void doHideZygisk();

View File

@@ -0,0 +1,26 @@
#pragma once
#include <string>
#include <vector>
#include <unordered_map>
#include <mntent.h>
class mount_entry_t
{
public:
mount_entry_t(::mntent *entry);
const std::string &getFsName() const;
const std::string &getMountPoint() const;
const std::string &getType() const;
const std::unordered_map<std::string, std::string> &getOptions() const;
int getDumpFrequency() const;
int getPassNumber() const;
private:
std::string fsname, dir, type;
std::unordered_map<std::string, std::string> opts_map;
int freq, passno;
};
std::vector<mount_entry_t> parseMountsFromPath(const char *path);
std::unordered_map<std::string, std::string> parseMountOptions(const std::string &input);

View File

@@ -0,0 +1,33 @@
#pragma once
#include <string>
#include <vector>
#include <unordered_map>
class mountinfo_entry_t
{
public:
mountinfo_entry_t(int mount_id, int parent_id, int major, int minor,
const std::string &root, const std::string &mount_point,
const std::string &mount_options, const std::string &optional_fields,
const std::string &filesystem_type, const std::string &mount_source,
const std::string &super_options);
int getMountId() const;
int getParentId() const;
int getMajor() const;
int getMinor() const;
const std::string &getRoot() const;
const std::string &getMountPoint() const;
const std::unordered_map<std::string, std::string> &getMountOptions() const;
const std::string &getOptionalFields() const;
const std::string &getFilesystemType() const;
const std::string &getMountSource() const;
const std::unordered_map<std::string, std::string> &getSuperOptions() const;
private:
int mount_id, parent_id, major, minor;
std::string root, mount_point, optional_fields, filesystem_type, mount_source;
std::unordered_map<std::string, std::string> mount_options, super_options;
};
std::vector<mountinfo_entry_t> parseMountinfosFromPath(const char *path);

View File

@@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <errno.h>
#include "logging.hpp"
#include "zygisk.hpp"
#define DCL_HOOK_FUNC(ret, func, ...) \
ret (*old_##func)(__VA_ARGS__) = nullptr; \
ret new_##func(__VA_ARGS__)
#define ASSERT_LOG(tag, expr) if(!(expr)) { \
LOGE("%s:%d Assertion %s failed. %d:%s", tag, __LINE__, #expr, errno, std::strerror(errno)); }
#define ASSERT_EXIT(tag, expr, ret) if(!(expr)) { \
LOGE("%s:%d Assertion %s failed. %d:%s", tag, __LINE__, #expr, errno, std::strerror(errno)); \
ret; }
bool switchMountNS(int pid);
int isUserAppUID(int uid);
bool hookPLTByName(zygisk::Api *api, const std::string &libName, const std::string &symbolName, void *hookFunc, void **origFunc);

View File

@@ -1,14 +1,58 @@
#include <cstdlib>
#include <unistd.h>
#include <fcntl.h>
#include <android/log.h>
#include <sched.h>
#include <sys/mount.h>
#include <cstdint>
#include <functional>
#include "zygisk.hpp"
#include "logging.hpp"
#include "utils.hpp"
#include "modules.hpp"
using zygisk::Api;
using zygisk::AppSpecializeArgs;
using zygisk::ServerSpecializeArgs;
static std::function<void()> callbackFunction = []() {};
/*
* [What's the purpose of this hook?]
* Calling unshare twice invalidates existing FD links, which fails Zygote sanity checks.
* So we prevent further namespaces by hooking unshare.
*
* [Doesn't Android already call unshare?]
* Whether there's going to be an unshare or not changes with each major Android version
* so we unconditionally unshare in preAppSpecialize.
* > Android 5: Conditionally unshares
* > Android 6: Always unshares
* > Android 7-11: Conditionally unshares
* > Android 12-14: Always unshares
*/
DCL_HOOK_FUNC(static int, unshare, int flags)
{
callbackFunction();
// Do not allow CLONE_NEWNS.
flags &= ~(CLONE_NEWNS);
if (!flags)
{
// If CLONE_NEWNS was the only flag, skip the call.
errno = 0;
return 0;
}
return old_unshare(flags);
}
/*
* The reason why we hook setresuid is because so far it has been unconditionally called
* and we still have CAP_SYS_ADMIN during this call.
*/
DCL_HOOK_FUNC(static int, setresuid, uid_t ruid, uid_t euid, uid_t suid)
{
callbackFunction();
return old_setresuid(ruid, euid, suid);
}
class ZygiskModule : public zygisk::ModuleBase
{
public:
@@ -25,22 +69,104 @@ public:
uint32_t flags = api->getFlags();
bool isRoot = (flags & zygisk::StateFlag::PROCESS_GRANTED_ROOT) != 0;
bool isOnDenylist = (flags & zygisk::StateFlag::PROCESS_ON_DENYLIST) != 0;
if (!isRoot && isOnDenylist && args->uid > 1000)
bool isChildZygote = args->is_child_zygote != NULL && *args->is_child_zygote;
if (isRoot || !isOnDenylist || !isUserAppUID(args->uid))
{
api->setOption(zygisk::Option::FORCE_DENYLIST_UNMOUNT);
LOGD("Skipping ppid=%d uid=%d isChildZygote=%d", getppid(), args->uid, isChildZygote);
return;
}
LOGD("Processing ppid=%d uid=%d isChildZygote=%d", getppid(), args->uid, isChildZygote);
/*
* Read the comment above unshare hook.
*/
ASSERT_EXIT("preAppSpecialize", unshare(CLONE_NEWNS) != -1, return);
/*
* Mount the app mount namespace's root as MS_SLAVE, so every mount/umount from
* Zygote shared pre-specialization namespace is propagated to this one.
*/
ASSERT_EXIT("preAppSpecialize", mount("rootfs", "/", NULL, (MS_SLAVE | MS_REC), NULL) != -1, return);
ASSERT_EXIT("preAppSpecialize", hookPLTByName("libandroid_runtime.so", "unshare", new_unshare, &old_unshare), return);
ASSERT_EXIT("preAppSpecialize", hookPLTByName("libandroid_runtime.so", "setresuid", new_setresuid, &old_setresuid), return);
int companionFd = -1;
ASSERT_LOG("preAppSpecialize", (companionFd = api->connectCompanion()) != -1);
ASSERT_LOG("preAppSpecialize", companionFd != -1 && api->exemptFd(companionFd));
callbackFunction = [fd = companionFd]()
{
bool result = false;
if (fd != -1)
{
do
{
pid_t pid = getpid();
ASSERT_EXIT("invokeZygiskCompanion", write(fd, &pid, sizeof(pid)) == sizeof(pid), break);
ASSERT_EXIT("invokeZygiskCompanion", read(fd, &result, sizeof(result)) == sizeof(result), break);
} while (false);
close(fd);
}
if (result)
LOGD("Invoking the companion was successful.");
else
{
LOGW("Invoking the companion failed. Performing operations in Zygote context!");
doUnmount();
doRemount();
}
doHideZygisk();
// Call only once per process.
callbackFunction = []() {};
};
}
void preServerSpecialize(ServerSpecializeArgs *args)
void preServerSpecialize(ServerSpecializeArgs *args) override
{
api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY);
}
void postAppSpecialize(const AppSpecializeArgs *args) override
{
if (old_unshare != nullptr)
ASSERT_LOG("postAppSpecialize", hookPLTByName("libandroid_runtime.so", "unshare", old_unshare));
if (old_setresuid != nullptr)
ASSERT_LOG("postAppSpecialize", hookPLTByName("libandroid_runtime.so", "setresuid", old_setresuid));
}
template <typename T>
bool hookPLTByName(const std::string &libName, const std::string &symbolName, T *hookFunction, T **originalFunction = nullptr)
{
return ::hookPLTByName(api, libName, symbolName, (void *)hookFunction, (void **)originalFunction) && api->pltHookCommit();
}
private:
Api *api;
JNIEnv *env;
};
void zygisk_companion_handler(int fd)
{
bool result = [&]() -> bool
{
pid_t pid;
ASSERT_EXIT("zygisk_companion_handler", read(fd, &pid, sizeof(pid)) == sizeof(pid), return false);
ASSERT_EXIT("zygisk_companion_handler", unshare(CLONE_NEWNS) != -1, return false);
ASSERT_EXIT("zygisk_companion_handler", switchMountNS(pid), return false);
LOGD("zygisk_companion_handler processing namespace of pid=%d", pid);
doUnmount();
doRemount();
return true;
}();
ASSERT_LOG("zygisk_companion_handler", write(fd, &result, sizeof(result)) == sizeof(result));
}
REGISTER_ZYGISK_MODULE(ZygiskModule)
REGISTER_ZYGISK_COMPANION(zygisk_companion_handler)

59
module/jni/map_parser.cpp Normal file
View File

@@ -0,0 +1,59 @@
#include <string>
#include <sstream>
#include <fstream>
#include <vector>
#include <sys/sysmacros.h> // For makedev
#include "map_parser.hpp"
#include "logging.hpp"
map_entry_t::map_entry_t(uintptr_t address_start, uintptr_t address_end, uintptr_t offset, const std::string &perms, const std::string &pathname, dev_t device, ino_t inode)
: address_start(address_start), address_end(address_end), perms(perms),
offset(offset), device(device), inode(inode), pathname(pathname) {}
uintptr_t map_entry_t::getAddressStart() const { return address_start; }
uintptr_t map_entry_t::getAddressEnd() const { return address_end; }
const std::string &map_entry_t::getPerms() const { return perms; }
uintptr_t map_entry_t::getOffset() const { return offset; }
dev_t map_entry_t::getDevice() const { return device; }
ino_t map_entry_t::getInode() const { return inode; }
const std::string &map_entry_t::getPathname() const { return pathname; }
std::vector<map_entry_t> parseMapsFromPath(const char *path)
{
std::vector<map_entry_t> ret;
std::ifstream ifs(path, std::ifstream::in);
if (!ifs)
{
LOGE("parseMapsFromPath could not open file \"%s\"", path);
return ret;
}
for (std::string line; std::getline(ifs, line);)
{
std::istringstream iss(line);
uintptr_t address_start, address_end, offset;
std::string perms;
std::string pathname;
ino_t inode;
int dev_major, dev_minor;
char dummy_char;
iss >> std::hex >> address_start >> dummy_char >> address_end >> perms >> offset >> dev_major >> dummy_char >> dev_minor >> std::dec >> inode;
if (iss.fail())
{
LOGE("parseMapsFromPath failed to parse line: %s", line.c_str());
continue;
}
// This operation can fail, it doesn't matter as it's an optional field.
std::getline(iss >> std::ws, pathname);
ret.emplace_back(map_entry_t(address_start, address_end, offset, perms, pathname, makedev(dev_major, dev_minor), inode));
}
return ret;
}

178
module/jni/modules.cpp Normal file
View File

@@ -0,0 +1,178 @@
#include <string>
#include <vector>
#include <set>
#include <unordered_map>
#include <cstdint>
#include <sys/mount.h>
#include <elfio/elfio.hpp>
#include "zygisk.hpp"
#include "logging.hpp"
#include "map_parser.hpp"
#include "mount_parser.hpp"
#include "mountinfo_parser.hpp"
#include "utils.hpp"
static const std::set<std::string> fsname_list = {"KSU", "APatch", "magisk", "worker"};
static const std::unordered_map<std::string, int> mount_flags_procfs = {
{"nosuid", MS_NOSUID},
{"nodev", MS_NODEV},
{"noexec", MS_NOEXEC},
{"noatime", MS_NOATIME},
{"nodiratime", MS_NODIRATIME},
{"relatime", MS_RELATIME},
{"nosymfollow", MS_NOSYMFOLLOW}};
static bool shouldUnmount(const mountinfo_entry_t &mount_info)
{
const auto &root = mount_info.getRoot();
// Unmount all module bind mounts
return root.starts_with("/adb/");
}
static bool shouldUnmount(const mount_entry_t &mount)
{
const auto &mountPoint = mount.getMountPoint();
const auto &type = mount.getType();
const auto &options = mount.getOptions();
// Unmount everything mounted to /data/adb
if (mountPoint.rfind("/data/adb", 0) == 0)
return true;
// Unmount all module overlayfs and tmpfs
if ((type == "overlay" || type == "tmpfs") && fsname_list.contains(mount.getFsName()))
return true;
// Unmount all overlayfs with lowerdir/upperdir/workdir starting with /data/adb
if (type == "overlay")
{
if (options.contains("lowerdir") && options.at("lowerdir").starts_with("/data/adb"))
return true;
if (options.contains("upperdir") && options.at("upperdir").starts_with("/data/adb"))
return true;
if (options.contains("workdir") && options.at("workdir").starts_with("/data/adb"))
return true;
}
return false;
}
void doUnmount()
{
std::vector<std::string> mountPoints;
// Check mounts first
for (auto &mount : parseMountsFromPath("/proc/self/mounts"))
{
if (shouldUnmount(mount))
{
mountPoints.push_back(mount.getMountPoint());
}
}
// Check mountinfos so that we can find bind mounts as well
for (auto &mount_info : parseMountinfosFromPath("/proc/self/mountinfo"))
{
if (shouldUnmount(mount_info))
{
mountPoints.push_back(mount_info.getMountPoint());
}
}
// Sort by string lengths, descending
std::sort(mountPoints.begin(), mountPoints.end(), [](const auto &lhs, const auto &rhs)
{ return lhs.size() > rhs.size(); });
for (const auto &mountPoint : mountPoints)
{
if (umount2(mountPoint.c_str(), MNT_DETACH) == 0)
{
LOGD("umount2(\"%s\", MNT_DETACH) returned 0", mountPoint.c_str());
}
else
{
LOGW("umount2(\"%s\", MNT_DETACH) returned -1: %d (%s)", mountPoint.c_str(), errno, strerror(errno));
}
}
}
void doRemount()
{
std::vector<mount_entry_t> mounts = parseMountsFromPath("/proc/self/mounts");
auto data_mount_it = std::find_if(mounts.begin(), mounts.end(), [](const mount_entry_t &mount)
{ return mount.getMountPoint() == "/data"; });
if (data_mount_it != mounts.end())
{
const auto &options = data_mount_it->getOptions();
// If errors=remount-ro, remount it with errors=continue
if (options.contains("errors") && options.at("errors") == "remount-ro")
{
unsigned long flags = MS_REMOUNT;
for (const auto &flagName : mount_flags_procfs)
{
if (options.contains(flagName.first))
flags |= flagName.second;
}
if (mount(NULL, "/data", NULL, flags, "errors=continue") == 0)
{
LOGD("mount(NULL, \"/data\", NULL, 0x%lx, \"errors=continue\") returned 0", flags);
}
else
{
LOGW("mount(NULL, \"/data\", NULL, 0x%lx, \"errors=continue\") returned -1: %d (%s)", flags, errno, strerror(errno));
}
}
}
}
/*
* Is it guaranteed to work? No.
* At least it has lots of error checking so if something goes wrong
* the state should remain relatively safe.
*/
void doHideZygisk()
{
using namespace ELFIO;
elfio reader;
std::string filePath;
uintptr_t startAddress = 0, bssAddress = 0;
for (const auto &map : parseMapsFromPath("/proc/self/maps"))
{
if (map.getPathname().ends_with("/libnativebridge.so") && map.getPerms() == "r--p")
{
// First ro page should be the ELF header
filePath = map.getPathname();
startAddress = map.getAddressStart();
break;
}
}
ASSERT_EXIT("doHideZygisk", startAddress != 0, return);
ASSERT_EXIT("doHideZygisk", reader.load(filePath), return);
size_t bssSize = 0;
for (const auto &sec : reader.sections)
{
if (sec->get_name() == ".bss")
{
bssAddress = startAddress + sec->get_address();
bssSize = static_cast<size_t>(sec->get_size());
break;
}
}
ASSERT_EXIT("doHideZygisk", bssAddress != 0, return);
LOGD("Found .bss for \"%s\" at 0x%" PRIxPTR " sized %" PRIuPTR " bytes.", filePath.c_str(), bssAddress, bssSize);
uint8_t *pHadError = reinterpret_cast<uint8_t *>(memchr(reinterpret_cast<void *>(bssAddress), 0x01, bssSize));
ASSERT_EXIT("doHideZygisk", pHadError != nullptr, return);
*pHadError = 0;
}

View File

@@ -0,0 +1,59 @@
#include <string>
#include <sstream>
#include <vector>
#include <unordered_map>
#include <mntent.h>
#include "mount_parser.hpp"
#include "logging.hpp"
#include "utils.hpp"
mount_entry_t::mount_entry_t(::mntent *entry)
: fsname(entry->mnt_fsname), dir(entry->mnt_dir), type(entry->mnt_type), freq(entry->mnt_freq), passno(entry->mnt_passno)
{
opts_map = parseMountOptions(entry->mnt_opts);
}
const std::string &mount_entry_t::getFsName() const { return fsname; }
const std::string &mount_entry_t::getMountPoint() const { return dir; }
const std::string &mount_entry_t::getType() const { return type; }
const std::unordered_map<std::string, std::string> &mount_entry_t::getOptions() const { return opts_map; }
int mount_entry_t::getDumpFrequency() const { return freq; }
int mount_entry_t::getPassNumber() const { return passno; }
std::vector<mount_entry_t> parseMountsFromPath(const char *path)
{
std::vector<mount_entry_t> result;
FILE *file;
ASSERT_EXIT("parseMountsFromPath", (file = setmntent(path, "r")) != NULL, return result);
struct mntent *entry;
while ((entry = getmntent(file)) != NULL)
{
result.emplace_back(mount_entry_t(entry));
}
endmntent(file);
return result;
}
std::unordered_map<std::string, std::string> parseMountOptions(const std::string &input)
{
std::unordered_map<std::string, std::string> ret;
std::istringstream iss(input);
std::string token;
while (std::getline(iss, token, ','))
{
std::istringstream tokenStream(token);
std::string key, value;
if (std::getline(tokenStream, key, '='))
{
std::getline(tokenStream, value); // Put what's left in the stream to value, could be empty
ret[key] = value;
}
}
return ret;
}

View File

@@ -0,0 +1,90 @@
#include <string>
#include <sstream>
#include <fstream>
#include <vector>
#include <unordered_map>
#include "mountinfo_parser.hpp"
#include "mount_parser.hpp"
#include "logging.hpp"
mountinfo_entry_t::mountinfo_entry_t(int mount_id, int parent_id, int major, int minor,
const std::string &root, const std::string &mount_point,
const std::string &mount_options, const std::string &optional_fields,
const std::string &filesystem_type, const std::string &mount_source,
const std::string &super_options)
: mount_id(mount_id), parent_id(parent_id), major(major), minor(minor),
root(root), mount_point(mount_point),
optional_fields(optional_fields), filesystem_type(filesystem_type),
mount_source(mount_source)
{
this->mount_options = parseMountOptions(mount_options);
this->super_options = parseMountOptions(super_options);
}
int mountinfo_entry_t::getMountId() const { return mount_id; }
int mountinfo_entry_t::getParentId() const { return parent_id; }
int mountinfo_entry_t::getMajor() const { return major; }
int mountinfo_entry_t::getMinor() const { return minor; }
const std::string &mountinfo_entry_t::getRoot() const { return root; }
const std::string &mountinfo_entry_t::getMountPoint() const { return mount_point; }
const std::unordered_map<std::string, std::string> &mountinfo_entry_t::getMountOptions() const { return mount_options; }
const std::string &mountinfo_entry_t::getOptionalFields() const { return optional_fields; }
const std::string &mountinfo_entry_t::getFilesystemType() const { return filesystem_type; }
const std::string &mountinfo_entry_t::getMountSource() const { return mount_source; }
const std::unordered_map<std::string, std::string> &mountinfo_entry_t::getSuperOptions() const { return super_options; }
std::vector<mountinfo_entry_t> parseMountinfosFromPath(const char *path)
{
std::vector<mountinfo_entry_t> ret;
std::ifstream ifs(path, std::ifstream::in);
if (!ifs)
{
LOGE("parseMountinfosFromPath could not open file \"%s\"", path);
return ret;
}
for (std::string line; std::getline(ifs, line);)
{
std::istringstream iss(line);
int mount_id, parent_id, major, minor;
std::string root, mount_point, mount_options, optional_fields, filesystem_type, mount_source, super_options;
char colon;
// Read the first 6 fields (major, colon and minor are the same field)
iss >> mount_id >> parent_id >> major >> colon >> minor >> root >> mount_point >> mount_options;
if (iss.fail())
{
LOGE("parseMountinfosFromPath failed to parse the first 6 fields of line: %s", line.c_str());
continue;
}
std::string field;
while (iss >> field && field != "-")
{
optional_fields += " " + field;
}
if (iss.fail())
{
LOGE("parseMountinfosFromPath failed to parse the optional fields of line: %s", line.c_str());
continue;
}
iss >> filesystem_type >> mount_source >> super_options;
if (iss.fail())
{
LOGE("parseMountinfosFromPath failed to parse the last 3 fields of line: %s", line.c_str());
continue;
}
ret.emplace_back(mountinfo_entry_t(mount_id, parent_id, major, minor,
root, mount_point, mount_options,
optional_fields, filesystem_type, mount_source,
super_options));
}
return ret;
}

48
module/jni/utils.cpp Normal file
View File

@@ -0,0 +1,48 @@
#include <string>
#include <format>
#include <unistd.h>
#include <sched.h>
#include <fcntl.h>
#include "map_parser.hpp"
#include "utils.hpp"
#include "android_filesystem_config.h"
#include "zygisk.hpp"
#include "logging.hpp"
bool hookPLTByName(zygisk::Api *api, const std::string &libName, const std::string &symbolName, void *hookFunc, void **origFunc)
{
for (const auto &map : parseMapsFromPath("/proc/self/maps"))
{
if (map.getPathname().ends_with("/" + libName))
{
api->pltHookRegister(map.getDevice(), map.getInode(), symbolName.c_str(), hookFunc, origFunc);
return true;
}
}
return false;
}
int isUserAppUID(int uid)
{
int appid = uid % AID_USER_OFFSET;
if (appid >= AID_APP_START && appid <= AID_APP_END)
return true;
if (appid >= AID_ISOLATED_START && appid <= AID_ISOLATED_END)
return true;
return false;
}
bool switchMountNS(int pid)
{
std::string path = std::string("/proc/") + std::to_string(pid) + "/ns/mnt";
int ret, fd;
if ((fd = open(path.c_str(), O_RDONLY)) < 0)
{
return false;
}
ret = setns(fd, 0);
close(fd);
return ret == 0;
}

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest/>

View File

@@ -2,5 +2,6 @@ id=${moduleId}
name=${moduleName}
version=${versionName}
versionCode=${versionCode}
author=snake-4 & yervant7
description=DLCLOSE_MODULE_LIBRARY and FORCE_DENYLIST_UNMOUNT for non-root processes.
author=snake-4
description=A Zygisk module to hide root.
updateJson=https://raw.githubusercontent.com/snake-4/Zygisk-Assistant/main/update_metadata/update.json

View File

@@ -14,6 +14,4 @@ dependencyResolutionManagement {
}
}
include(
":module"
)
include(":module")

View File

@@ -0,0 +1,12 @@
## 2.1.0
+ Added Zygisk hide for Magisk 27.0.
+ Fixed bind mount hiding. ReVanced is fully hidden now.
+ All Systemless Hosts modules are hidden now.
+ Fixed compatibility issues with other modules.
## 2.0.4
+ Fixed an issue causing root to be lost.
+ Fixed potential incompatibilities with other apps.
## 2.0.3
+ Bind mounts are also hidden now.

View File

@@ -0,0 +1,6 @@
{
"version": "v2.0.4",
"versionCode": 204,
"zipUrl": "https://github.com/snake-4/Zygisk-Assistant/releases/download/v2.0.4/Zygisk-Assistant-v2.0.4-7ec3214-release.zip",
"changelog": "https://raw.githubusercontent.com/snake-4/Zygisk-Assistant/main/update_metadata/CHANGELOG.md"
}