Bypassing ADB Restrictions on the Viwoods AiPaper Reader
1. Background
The Viwoods AiPaper Reader is a 6.13" e-ink device running Android 16 on a MediaTek SoC (ARM big.LITTLE: 6x Cortex-A55 + 2x Cortex-A76). It ships with 4 GB RAM and 106 GB internal storage.
On firmware v1.3.4 and earlier, the device had:
- Full ADB shell access over USB and Wi-Fi
- Factory root access via
/system/xbin/su(no Magisk or SuperSU needed) - Working
adb push,adb pull,adb install,scrcpy, andlogcat - Wireless ADB for cable-free file transfers
This made the device a power user's dream—full control over an e-ink Android tablet. For a deep dive into what the hardware actually contains, see Inside the Viwoods AiPaper Reader: A Complete Hardware Audit via ADB.
2. The Problem: ADB Lockdown After Firmware Update
After updating from firmware v1.3.4 to v1.4.0, every ADB command began failing with a cryptic error:
$ adb shell id
error: not support command id
$ adb shell ls
error: not support command ls
$ adb push file.txt /sdcard/
error: not support command
$ adb install app.apk
Error: not support command
The device was visible to ADB and reported as device (connected), but a custom restricted ADB daemon on the device rejected every command.
/system/xbin/su.
What ADB reported
$ adb devices -l
<SERIAL> device usb:1-1 product:AiPaper Reader model:AiPaper_Reader
$ adb get-state
device
$ adb features
shell_v2, cmd, stat_v2, ls_v2, abb, abb_exec, remount_shell, ...
The device advertised full feature support including shell_v2—it just refused to execute anything.
3. Diagnosis: What Still Works?
Systematic testing revealed the extent of the lockdown:
| Command | Status | Notes |
|---|---|---|
adb devices | Working | Device detected normally |
adb get-state | Working | Returns "device" |
adb get-serialno | Working | Returns serial number |
adb features | Working | Full feature list returned |
adb forward | Working | Port forwarding works |
adb shell * | Blocked | "not support command" |
adb push/pull | Blocked | Protocol fault |
adb install | Blocked | "not support command" |
adb logcat | Blocked | "not support command" |
adb screencap | Blocked | "not support command" |
scrcpy | Blocked | Can't push server binary |
The restriction was at the device's ADB daemon level—the host-side ADB client and server functioned normally, but the device-side daemon rejected all shell and file transfer operations.
4. The Viwoods Debug Tool
Online research revealed that Viwoods had intentionally disabled ADB and provided a proprietary Viwoods Debug Tool as a workaround to select users. A community member shared the tool: viwoods-debug-tools-20260206.zip (135 MB).
Tool Contents
viwoods-debug-tools-20260206/
├── Viwoods Debug Tool user guide.pdf (1.4 MB)
├── viwoods_debug_tools-linux.zip (45.7 MB)
├── viwoods_debug_tools-macOS.zip (47.9 MB)
└── viwoods_debug_tools-win.zip (47.4 MB)
The tool is a .NET Avalonia desktop app (self-contained AOT binary) that bundles its own ADB binary and provides:
- Device detection and connection management
- Logcat capture (start/stop/export/clear)
- Performance log export (battery, power, wakeup)
- System crash log capture
- A "Custom Command" input box for arbitrary ADB commands
The Key Observation
The Custom Command box in the tool successfully executed shell commands that standard ADB could not:
09:45:51 - Executeing: shell getprop ro.build.version.release
09:45:52 - 16
09:44:44 - Executeing: shell su 0 id
09:44:44 - uid=0(root) ...
5. Protocol Interception: Setting the Trap
To understand how the tool bypassed the restriction, we intercepted the ADB protocol traffic between the tool and the ADB server using a TCP proxy.
Step 1: Move the real ADB server
The ADB server normally listens on localhost:5037. We restarted it on port 6037:
$ adb kill-server
$ ANDROID_ADB_SERVER_PORT=6037 adb start-server
* daemon started successfully
Step 2: Insert a logging proxy on port 5037
A Python TCP proxy was placed on port 5037 to intercept all traffic and forward it to the real server on 6037:
import socket, threading
def forward(src, dst, label):
while True:
data = src.recv(4096)
if not data: break
LOG.write(f'{label}: {data!r}\n')
dst.sendall(data)
# Proxy on :5037 → :6037
server.bind(('127.0.0.1', 5037))
# ... accept and forward connections
┌──────────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────┐
│ Viwoods Debug │────▶│ TCP Proxy │────▶│ ADB Server │────▶│ Device │
│ Tool │◀────│ (port 5037) │◀────│ (port 6037)│◀────│ │
└──────────────────┘ └──────┬───────┘ └────────────┘ └──────────┘
│
┌─────▼─────┐
│ Log File │
│ (capture) │
└───────────┘
Step 3: Capture a normal (failing) command
First, we captured what standard adb shell id sends:
host:versionS→C:
OKAY 0029C→S:
host-serial:<SERIAL>:featuresS→C:
OKAY shell_v2,cmd,stat_v2,...C→S:
host:tport:serial:<SERIAL>S→C:
OKAYC→S:
shell,v2,TERM=xterm-256color,raw:idS→C:
error: not support command id
Step 4: Capture the Viwoods tool's command
Then the same shell id was executed through the Viwoods Debug Tool:
host:versionS→C:
OKAY 0029C→S:
host-serial:<SERIAL>:featuresS→C:
OKAY shell_v2,cmd,...C→S:
host:tport:serial:<SERIAL>S→C:
OKAYC→S:
shell,v2,TERM=xterm-256color,raw:setprop persist.wisky.hold_enable_adb SE03znbjb@6932S→C:
OKAY (exit code 0)ADB is now unlocked, tool sends the actual command
C→S:
shell,v2,TERM=xterm-256color,raw:idS→C:
OKAY uid=2000(shell) gid=2000(shell) ...After command completes, tool re-locks ADB
C→S:
shell,v2,TERM=xterm-256color,raw:setprop persist.wisky.hold_enable_adb ' 'S→C:
OKAY (exit code 0)
6. The Discovery: A Hardcoded Unlock Key
The Viwoods Debug Tool's strategy is simple:
persist.wisky.hold_enable_adb to the unlock key SE03znbjb@6932' ' (a single space)—re-locking ADBHow the device-side restriction works
The custom ADB daemon on the device checks the value of persist.wisky.hold_enable_adb before executing any shell command. If the value matches the hardcoded key SE03znbjb@6932, the command is allowed. Otherwise, it returns error: not support command.
wisky, suggesting Viwoods uses Wisky Technology as their ODM (Original Design Manufacturer) for the hardware/firmware base. This is a common arrangement in the Chinese e-ink device market.
Security implications
The restriction is security through obscurity—the unlock key is hardcoded and transmitted in plaintext over the ADB protocol. Anyone with the ability to intercept localhost TCP traffic (which requires no special privileges) can extract the key.
Notably, the setprop command itself is not blocked by the restriction—only the subsequent shell commands are gated. This means the unlock property can be set even when ADB is in its "locked" state.
7. Permanent Unlock
Since the property uses the persist. prefix, setting it once makes it survive reboots—it's written to /data/property/persistent_properties on the device.
$ adb shell "setprop persist.wisky.hold_enable_adb SE03znbjb@6932"
$ adb shell id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),
1011(adb),1015(sdcard_rw),1028(sdcard_r),... context=u:r:shell:s0
$ adb shell su 0 id
uid=0(root) gid=0(root) groups=0(root),... context=u:r:su:s0
adb shell "setprop persist.wisky.hold_enable_adb SE03znbjb@6932"—this permanently unlocks full ADB access including shell, push, pull, install, logcat, screencap, and scrcpy. The setting survives reboots.
8. Verification: Everything Restored
USB ADB
$ adb -s <SERIAL> shell id
uid=2000(shell) gid=2000(shell) ... ✓
$ adb -s <SERIAL> shell su 0 id
uid=0(root) gid=0(root) ... ✓
$ adb push test.txt /sdcard/
1 file pushed. 0.1 MB/s ✓
Wireless ADB
$ adb tcpip 5555
restarting in TCP mode port: 5555
$ adb connect <DEVICE_IP>:5555
connected to <DEVICE_IP>:5555
$ adb -s <DEVICE_IP>:5555 shell id
uid=2000(shell) gid=2000(shell) ... ✓
$ adb -s <DEVICE_IP>:5555 push test.txt /sdcard/
1 file pushed. ✓
Full capability matrix after unlock
| Capability | Before Unlock | After Unlock |
|---|---|---|
| ADB shell | Blocked | Working |
| Root (su) | Blocked | Working |
| adb push / pull | Blocked | Working |
| adb install | Blocked | Working |
| logcat | Blocked | Working |
| screencap | Blocked | Working |
| scrcpy | Blocked | Working |
| Wireless ADB | Blocked | Working |
| Wireless file transfer | Blocked | Working |
9. Technical Analysis
Device specifications
| Property | Value |
|---|---|
| Device | Viwoods AiPaper Reader |
| Android Version | 16 |
| Firmware | 1.4.0 |
| Chipset | MediaTek (0x0e8d) |
| CPU | 8-core ARMv8: 6x Cortex-A55 (0xD05) + 2x Cortex-A76 (0xD0B) |
| RAM | 3.66 GB |
| Storage | 106 GB (dm-verity protected system partitions) |
| Battery | ~2,503 mAh Li-ion |
| Root method | Factory su binary at /system/xbin/su (setuid root) |
| ODM | Wisky Technology (inferred from persist.wisky.* properties) |
Firmware version history
| Version | Date | ADB Status | Notes |
|---|---|---|---|
| 1.0.7 | ~2025-09 | Full access | Factory firmware |
| 1.1.0 | ~2025-12 | Full access | First OTA update |
| 1.2.3 | ~2026-01 | Full access | Review firmware |
| 1.3.4 | ~2026-02 | Restricted | ADB lockdown introduced |
| 1.3.8 | ~2026-03 | Restricted | Beta release |
| 1.4.0 | ~2026-05 | Restricted | Current; fully locked |
The restriction mechanism
The ADB daemon on the device has been modified to check a system property before allowing shell command execution:
// Pseudocode of the device-side ADB restriction
if (getprop("persist.wisky.hold_enable_adb") != "SE03znbjb@6932") {
return "error: not support command " + cmd;
}
// else: proceed with normal shell execution
Key observations:
- The
setpropcommand itself is exempted from the restriction—otherwise the unlock would be impossible - The key
SE03znbjb@6932appears to be a randomly generated string, not derived from any device-specific value - The same key likely works on all Viwoods/Wisky devices running firmware v1.3.4+
- The
persist.prefix means the property is stored in/data/property/persistent_propertiesand survives reboots - ADB host-side operations (device listing, feature queries, port forwarding) are not affected by the restriction
Viwoods Debug Tool internals
| Property | Value |
|---|---|
| Framework | .NET (Avalonia UI)—self-contained AOT binary |
| Libraries | libAvaloniaNative.dylib, libSkiaSharp.dylib, libHarfBuzzSharp.dylib |
| Bundled ADB | v36.0.0 (same as standard platform-tools) |
| Binary size | ~86 MB (macOS arm64) |
| Tool version | 1.0.1 (2026-02-06) |
| Communication | Standard ADB protocol via localhost:5037 |
| Unlock method | Sends setprop before each command, clears after |
10. TL;DR: Quick Unlock Guide
adb shell "setprop persist.wisky.hold_enable_adb SE03znbjb@6932"
That's it. Run this once and you have full, persistent ADB access—shell, root, push, pull, install, scrcpy, wireless ADB, everything. Survives reboots.
If you also want persistent wireless ADB:
adb shell "setprop persist.wisky.hold_enable_adb SE03znbjb@6932"
adb shell "su 0 setprop persist.adb.tcp.port 5555"
adb tcpip 5555
adb connect <device-ip>:5555
To verify it worked:
$ adb shell getprop persist.wisky.hold_enable_adb
SE03znbjb@6932
$ adb shell id
uid=2000(shell) ...
$ adb shell su 0 id
uid=0(root) ...
If ADB gets re-locked (e.g., after using the Viwoods Debug Tool):
Just run the unlock command again—it's the same single command.
