Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ Player — from one file, fully automatically. Releases are grouped by the BlueS

---

## v10 — Custom Kitsune build: the in-app DenyList now works with ReZygisk · 2026-06-02

Swaps the bundled Magisk for a **custom build of Kitsune Mask v31** (still `31.0-kitsune`, versionCode
31000) that fixes a long-standing table mismatch. Kitsune's DenyList UI/CLI wrote app entries to the
`hidelist` table, but external ptrace-Zygisk modules (**ReZygisk / NeoZygisk**) read the `denylist` table —
so toggling an app in Magisk's own hide list had **no effect** under those modules, and you had to run
`magisk --sqlite "INSERT INTO denylist ..."` by hand.

- 🩹 **The patch (3 lines).** In `native/src/core/deny/utils.cpp` the deny module's default table is now
`denylist` instead of `hidelist` (SuList mode still uses `sulist`, which those modules also read). Built from
[`Jordan231111/KitsuneMagisk@25fa2159f`](https://github.com/Jordan231111/KitsuneMagisk/tree/kitsune), a fork
of `1q23lyc45/KitsuneMagisk`. Result: the **in-app DenyList toggle now actually hides root** for
ReZygisk/NeoZygisk-protected apps — no SQLite editing needed.
- 📦 **New embedded APK.** SHA-256 `fac319d2de262fcfff1684e13e1a5c61c486d2a773a7a8ffcfdbfe6f763a7fd4`
(12,574,128 bytes — same size as the stock v31 APK, since `"hidelist"`→`"denylist"` is byte-length-neutral).
Verified **rebuilt, not re-signed**: `lib/x86_64/libmagisk64.so` and `lib/x86/libmagisk32.so` both differ
from the stock APK.
- 🔁 **No blueStackRoot pipeline change.** Package id (`io.github.huskydg.magisk`) and the APK's `lib/$ABI` +
`assets/*.sh` layout are unchanged, so the version-agnostic extract/install/undo path roots exactly as before.
- 🛠️ **Repo kept in sync + transparent.** Re-spliced with `tools/reembed-apk.ps1` (byte-level, SHA-256
round-trip); refreshed `tools/magisk_databin/` via `tools/extract-databin.ps1`; updated the embedded-APK hash
asserted by `tests/Check-Embedded-Sync.ps1`; replaced the reference `Working Example & Fix/MagiskMyStableBuild.apk`;
README "Is this safe?" now states plainly that the APK is a custom build and links the source/diff. (Live E2E
`VERIFY PASS` to be re-confirmed on an instance.)

---

## v9 — Kitsune Mask v31 (release build) · 2026-06-02

Re-bundles **Kitsune Mask v31** using the version-tagged **31.0-kitsune** (versionCode 31000) build,
Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

**Root BlueStacks 5 / MSI App Player with real Magisk — from one file, with no traces left behind.**
**Run `blueStackRoot.cmd` as administrator, pick your Android version, and you're rooted.**
**The genuine Magisk Delta (Kitsune) APK is now bundled inside the `.cmd` itself** — no separate Magisk
**A Magisk Delta (Kitsune v31) build is now bundled inside the `.cmd` itself** — no separate Magisk
download, no other files, nothing to install. Works on the latest BlueStacks (5.22.169).

---
Expand Down Expand Up @@ -96,12 +96,18 @@ BlueStacks and carries binaries inside it. Heuristic scanners flag that pattern.
| `__BSR_ENGINE__` / `__BSR_MAGISK__` | The two PowerShell scripts above (plain text) | Diff against `tools/*.ps1` in this repo |
| `__BSR_DFS__` | `debugfs` from the standard Cygwin **e2fsprogs** suite | Standard open-source ext4 tool |
| `__BSR_SU__` / `__BSR_BSRSU__` | Tiny `su` binaries used only *during* install, then **erased** | Source in [`tools/su_src/`](tools/su_src) |
| `__BSR_APK__` | The **official, unmodified Kitsune Mask (Magisk Delta) v31** APK | SHA-256 below |

- **The Magisk APK is the real one.** Its SHA-256 is
`f554c9643a527cda4910e1a044a2bfabd5f034f456587bc995895092dfe9b933` (12,574,128 bytes) — the genuine
[Kitsune Mask v31 build](https://github.com/1q23lyc45/KitsuneMagisk/releases) (`magisk -c` → `31.0-kitsune`,
versionCode 31000). You're trusting Magisk, not me.
| `__BSR_APK__` | A **custom, open-source build of Kitsune Mask (Magisk Delta) v31** — one small documented patch | SHA-256 + source below |

- **The Magisk APK is a custom, open-source build of Kitsune Mask v31** (`magisk -c` → `31.0-kitsune`,
versionCode 31000). SHA-256
`fac319d2de262fcfff1684e13e1a5c61c486d2a773a7a8ffcfdbfe6f763a7fd4` (12,574,128 bytes). It is built from
[`Jordan231111/KitsuneMagisk`](https://github.com/Jordan231111/KitsuneMagisk/tree/kitsune) (commit
`25fa2159f`) — a fork of [`1q23lyc45/KitsuneMagisk`](https://github.com/1q23lyc45/KitsuneMagisk) with a
**single 3-line source patch**: the DenyList backend stores entries in the `denylist` table instead of
`hidelist`, so apps you toggle in Magisk's **own DenyList UI** are picked up by the Zygisk implementations
(ReZygisk / NeoZygisk) that read `denylist` — i.e. the in-app hide toggle finally hides root. Nothing else
is changed. **Don't just trust me — read the one-commit diff and rebuild it yourself**; the SHA-256 above
is exactly what's embedded in the `.cmd` (re-verifiable with `tests/Check-Embedded-Sync.ps1`).
- **Verify it yourself in 30 seconds.** Scan the file on [VirusTotal](https://www.virustotal.com/), or
extract any embedded blob and check its hash — open the `.cmd` in any text editor and the `__BSR_*__`
markers are right there. The whole point of this project is that you *don't* have to trust a black box.
Expand Down
Binary file modified Working Example & Fix/MagiskMyStableBuild.apk
Binary file not shown.
8,186 changes: 4,093 additions & 4,093 deletions blueStackRoot.cmd

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/BLUESTACKS_ROOTING_DEEP_DIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ Re‑verified from `recovered/BstkRooter/` (decompiled "Root" `fcn.14001d370` +
| Stock bindmount (extracted from factory) | `tools/su_src/bindmount.orig` (1339 B) |
| Modified bindmount (bootstrap) | `tools/su_src/bindmount.mod` |
| Magisk databin (extracted from APK) | `tools/magisk_databin/` |
| Magisk APK (manager + all binaries) | `Working Example & Fix/MagiskMyStableBuild.apk` |
| Magisk APK (manager + all binaries) | `Working Example & Fix/MagiskMyStableBuild.apk` — **custom Kitsune v31** (`denylist`-table patch; see README / CHANGELOG v10) |
| Engine (patch, conf, ext4, Root/Unroot) | `tools/bsr_engine.ps1` (embedded in `blueStackRoot.cmd`) |
| Offline su inject (bootstrap) | `tests/rootvhd-hook.ps1` |
| Offline bsr_su remove + stock bindmount restore | `tests/remove-bsr-su.ps1` |
Expand Down
220 changes: 220 additions & 0 deletions docs/HIDE_ROOT_FROM_APP_DENYLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Hide root from an app with ReZygisk — the DenyList SQLite fix (verified end‑to‑end)

How to make a root‑detecting app (e.g. **LIAPP**‑protected games) launch on a **Magisk Kitsune + ReZygisk**
BlueStacks instance, by putting the app on ReZygisk's **DenyList** so its Magisk mounts are unmounted.

This was proven end‑to‑end on **BlueStacks `Tiramisu64_9` (Android 13, adb port 5645)** with
**Idle Poseidon** (`com.mouseduck.seawar`, LIAPP v5.1.1.200): before the fix the game detected root and
exited; after the fix ReZygisk unmounts root for it and it boots to its login screen and stays up.

> **The one thing to remember:** the **Magisk app's hide toggle does NOT do this** in this setup — it writes
> to the wrong table. You must put the app in the **`denylist`** table, and the only non‑GUI way on this
> Kitsune build is `magisk --sqlite`. See [Why the Magisk UI doesn't work](#why-the-magisk-ui-toggle-doesnt-work-but-the-sqlite-insert-does).

---

## TL;DR — the fix

From the host, with the instance running and rooted (substitute your adb port and package):

```powershell
# 1. add the app to the DenyList table ReZygisk actually reads
adb -s 127.0.0.1:5645 shell su -c "magisk --sqlite 'INSERT OR REPLACE INTO denylist (package_name,process) VALUES (\"com.mouseduck.seawar\",\"com.mouseduck.seawar\")'"

# 2. reboot the instance (ReZygisk's daemon reads the DenyList once, at boot)
adb -s 127.0.0.1:5645 reboot
```

After it boots, launch the app. Done. No Magisk‑app toggle, no "Enforce DenyList", no MagiskHide needed.

> ⚠️ On BlueStacks, `adb reboot` sometimes leaves the guest stuck (HD‑Player stays up but the guest never
> re‑binds adb). If that happens, close the instance from the Multi‑Instance Manager and reopen it — that
> cold‑boots it cleanly. The DenyList row is on disk (`magisk.db`) and survives.

> **Note — `blueStackRoot` v10+ bundles a Kitsune build that already fixes the GUI path.** If you rooted with
> the custom build shipped since CHANGELOG v10, Magisk's deny module stores entries in the `denylist` table,
> so you can simply **tick the app in the Magisk app's DenyList UI** (no SQL, no reboot quirk) and ReZygisk
> picks it up. The `magisk --sqlite` method above still works everywhere and is what you need on **stock /
> older** Kitsune, where the UI writes to `hidelist` (which ReZygisk ignores).

---

## Applies to

| Component | Value (verified) |
|---|---|
| Magisk | **Kitsune Mask / Magisk Delta v31** (`magisk -c` → `31.0-kitsune`, 31000) |
| Zygisk impl | **ReZygisk v1.0.0** (module id `rezygisk`) — replaces Magisk's built‑in Zygisk |
| Xposed | Vector / LSPosed (`zygisk_vector`) — present, not required for this fix |
| Built‑in Magisk Zygisk | **OFF** (ReZygisk provides Zygisk instead) |

If your build matches the first two rows, this procedure applies verbatim.

---

## Why it breaks "after working for a while"

The DenyList lives in `/data/adb/magisk.db` → table `denylist`. It is **wiped whenever the Magisk DB is
recreated**, which happens when you **re‑run the rooting tool** (fresh `magisk.db`) or, often, when you
**reinstall the target app**. Symptom: it "worked for several launches, then stopped." The row is simply
gone, so ReZygisk has nothing to hide and the app sees root again.

Re‑applying is just the [TL;DR](#tldr--the-fix) two‑liner.

---

## Step‑by‑step for ANY app

### 1. Find the package name
```bash
adb -s 127.0.0.1:5645 shell pm list packages -3 | sort # third‑party packages
# or search: ... pm list packages | grep -i <keyword>
```

### 2. Find the process name(s) — usually you don't need to
ReZygisk's match query is a **prefix match on the `process` column**:
```sql
SELECT 1 FROM denylist WHERE "<runtime process>" LIKE process || '%' LIMIT 1
```
Because of the trailing `%`, a **single row with `process = <package>` also covers every child process**
(`com.pkg`, `com.pkg:gpu`, `com.pkg:push`, …). So for the vast majority of apps you only need one row:
`(package, package)`.

Only if an app runs a child process whose name does **not** start with the package name (rare) do you need
an extra row. To check what an app actually spawns, launch it and list its processes by uid:
```bash
uid=$(adb -s 127.0.0.1:5645 shell su -c "grep <pkg> /data/system/packages.list" | awk '{print $2}')
adb -s 127.0.0.1:5645 shell su -c "for p in \$(pgrep -u $uid); do tr '\0' ' ' </proc/\$p/cmdline; echo; done"
```

### 3. Insert the DenyList row
Cleanest from an interactive shell (avoids host quoting pain):
```bash
adb -s 127.0.0.1:5645 shell
su
magisk --sqlite "INSERT OR REPLACE INTO denylist (package_name,process) VALUES ('com.mouseduck.seawar','com.mouseduck.seawar')"
magisk --sqlite "SELECT * FROM denylist" # verify the row is there
```

### 4. Reboot the instance
ReZygisk's daemon (`zygiskd`) reads the DenyList **once, at boot** (post‑fs‑data). Editing the table while
it's running has **no effect until the daemon re‑initializes** — so reboot (or cold‑restart) the instance.

### 5. Verify (see below) and launch the app.

---

## Why the Magisk UI toggle doesn't work, but the SQLite insert does

This is the confusing part, and it's **not a random bug** — it's two tables and a mode mismatch. Verified
on the live instance:

**Magisk Kitsune has three independent app lists, in three tables of `magisk.db`:**

| Table | Belongs to | Who enforces it |
|---|---|---|
| `hidelist` | **MagiskHide** (classic ptrace hide) | Magisk core, only when `settings.magiskhide = 1` |
| `denylist` | **Zygisk DenyList** | **ReZygisk** (reads this table directly) |
| `sulist` | SuList (whitelist) mode | ReZygisk, only when `settings.sulist = 1` |

**What the Magisk app's hide toggle actually writes.** ReZygisk **disables Magisk's own built‑in Zygisk**
(its `service.sh` literally relabels itself "❌ Disable Magisk's built‑in Zygisk"). With built‑in Zygisk
**off**, the Magisk Kitsune app falls back to presenting **MagiskHide**, so when you tick an app in the
app's hide list it writes the row into **`hidelist`** — *not* `denylist`.

We confirmed this directly: after only ever inserting into `denylist` via SQL, the `hidelist` table already
contained `com.mouseduck.seawar` — that row could only have come from an **earlier Magisk‑app toggle**. So
the app's toggle *was* doing something; it was just filling the wrong table:

```
denylist → com.mouseduck.seawar (our SQL insert — the table ReZygisk reads) ✅ works
hidelist → com.mouseduck.seawar (the Magisk UI toggle — MagiskHide's table) ❌ ReZygisk never reads it
```

**So the UI toggle fails for two compounding reasons:**
1. **Wrong table.** It populates `hidelist` (MagiskHide); ReZygisk only reads `denylist`.
2. **Even MagiskHide is off.** `settings.magiskhide = 0`, so the `hidelist` it wrote isn't being enforced by
anything either.

**Why the SQLite insert works.** It writes straight into `denylist`, which is the exact table — and column,
with the exact prefix‑match semantics — that ReZygisk's daemon queries on every process spawn. It bypasses
the app's Zygisk‑mode gating entirely.

> Side note: the `magisk --denylist ...` **CLI applet is stripped out of this Kitsune build** (only `su` and
> `resetprop` are exposed). So even from a shell, `magisk --sqlite` is the only way to edit the DenyList
> without the GUI.

---

## Why "Enforce DenyList" and the MagiskHide toggle are irrelevant here

ReZygisk does **not** consult `settings.denylist` ("Enforce DenyList"). Pulled straight from the `zygiskd64`
binary, the only settings key it reads is `sulist` (to choose DenyList‑mode vs SuList‑mode):

```
SELECT 1 FROM denylist WHERE "%s" LIKE process || '%' LIMIT 1 ← decides hide/unmount
SELECT 1 FROM sulist WHERE process="%s" LIMIT 1
select value from settings where key = 'sulist' ← only settings key it reads
```

Empirically:
- `settings.denylist` (Enforce DenyList) **resets to `0` on every boot** in this setup (built‑in Zygisk is
off), and ReZygisk hides the app anyway. Toggling it on is pointless and doesn't stick.
- `settings.magiskhide = 0` and that's fine — MagiskHide is a different mechanism we're not using.

The **only** state that matters is: *is there a matching row in the `denylist` table when the daemon boots?*

---

## Verifying it worked

Right after launching the app, check the ReZygisk daemon log — this line is the proof it unmounted root for
the app's process:

```bash
adb -s 127.0.0.1:5645 shell su -c "logcat -d | grep -E 'Unmounting root|Unmounted /system/bin/magisk'"
```
Expected:
```
zygiskd64: [magisk] Unmounting root
zygiskd64: [magisk] Unmounted /sbin/magisk64 ... /system/bin/magisk ... /sbin
zygiskd64: [Magisk] Magisk Kitsune detected ... caching clean namespace fd.
```

Then confirm the app actually survives (root‑detectors usually kill within ~15 s of launch):
```bash
adb -s 127.0.0.1:5645 shell su -c "pidof <pkg>" # still returns a pid after 20–30 s = good
adb -s 127.0.0.1:5645 exec-out screencap -p > check.png # eyeball it: real screen, not a block dialog
```

A handy negative signal: if `zygisk_vector` (LSPosed) is installed, the line
`VectorZygiskBridge: ... callerUid=<app uid>` in logcat means the module is **still being injected** into
the app — i.e. it is **not** being denylisted yet (fix not applied / not rebooted). Once denylisted, ReZygisk
neither injects modules nor leaves Magisk mounted for that process.

---

## Quick reference

```bash
# add (covers all child processes via prefix match)
magisk --sqlite "INSERT OR REPLACE INTO denylist (package_name,process) VALUES ('PKG','PKG')"
# list
magisk --sqlite "SELECT * FROM denylist"
# remove
magisk --sqlite "DELETE FROM denylist WHERE package_name='PKG'"
# ...then reboot the instance.
```

| Symptom | Cause | Action |
|---|---|---|
| App detects root again after a while | DenyList wiped by re‑root or app reinstall | Re‑insert the row + reboot |
| Edited DenyList, still detected | Daemon hasn't re‑read | Reboot / cold‑restart the instance |
| Magisk‑app hide toggle "does nothing" | It wrote to `hidelist`, not `denylist` | Use the SQL insert into `denylist` |
| `magisk --denylist` says "invalid applet" | CLI stripped in Kitsune | Use `magisk --sqlite` |

---

*Scope: this covers hiding **root/Magisk** from an app on BlueStacks via ReZygisk. Emulator‑fingerprint
detection (qemu props, RIL strings, device files, etc.) is a separate layer — see the MuMu notes — and is
not addressed by the DenyList.*
3 changes: 3 additions & 0 deletions docs/RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Everything here is built from steps proven on this machine; the one offline step
- **Matching Magisk APK present.** Use the same build you want installed, e.g.
`Working Example & Fix\MagiskMyStableBuild.apk`. This single file provides both the manager app
and every Magisk binary. (A different Magisk version → pass that APK; nothing else changes.)
> The bundled `MagiskMyStableBuild.apk` is a **custom Kitsune v31 build** (a 3-line `denylist`-table patch so
> Magisk's in-app DenyList works with ReZygisk/NeoZygisk) — see the README "Is this safe?" section and
> CHANGELOG v10. To use stock upstream Magisk instead, just pass its APK here.
- **Know your instance name** (default `Rvc64`) and paths:
- Root.vhd: `C:\ProgramData\BlueStacks_nxt\Engine\<Instance>\Root.vhd`
- conf: `C:\ProgramData\BlueStacks_nxt\bluestacks.conf`
Expand Down
3 changes: 2 additions & 1 deletion tests/Check-Embedded-Sync.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ foreach ($p in @(@('ENGINE', 'tools\bsr_engine.ps1'), @('MAGISK', 'tools\bsr_mag

# --- embedded Magisk APK: decode the base64 blob and verify its SHA-256 (guards against APK drift;
# the APK is re-embedded by tools\reembed-apk.ps1, which round-trips this same hash). ---
$ApkSha256 = 'f554c9643a527cda4910e1a044a2bfabd5f034f456587bc995895092dfe9b933'
# Custom Kitsune v31 build (denylist-table patch) from Jordan231111/KitsuneMagisk@25fa2159f. See README / CHANGELOG.
$ApkSha256 = 'fac319d2de262fcfff1684e13e1a5c61c486d2a773a7a8ffcfdbfe6f763a7fd4'
try {
$apkB64 = (Extract 'APK') -replace '[^A-Za-z0-9+/=]', ''
$apkBytes = [Convert]::FromBase64String($apkB64)
Expand Down
5 changes: 5 additions & 0 deletions todolist.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Todo List

## Done
- [x] **bundled a custom Kitsune v31 build so the in-app DenyList works with ReZygisk/NeoZygisk** — the deny
module now stores entries in the `denylist` table (not `hidelist`), built from
`Jordan231111/KitsuneMagisk@25fa2159f`. Re-embedded (`reembed-apk.ps1`, SHA `fac319d2…`, round-trip OK),
refreshed `tools/magisk_databin/`, bumped `tests/Check-Embedded-Sync.ps1`, replaced the reference APK,
README transparency note + CHANGELOG v10. (live-instance E2E re-confirm pending)
- [x] **bumped bundled Magisk → Kitsune Mask v31** (`1q23lyc45` fork; `magisk -c` 31.0-kitsune / 31000) via new
`tools/reembed-apk.ps1` (byte-level splice + SHA-256 round-trip); refreshed `tools/magisk_databin/`
(`tools/extract-databin.ps1`); `tests/Check-Embedded-Sync.ps1` now guards the embedded APK SHA. No
Expand Down
Loading