What's actually inside a £30 Android projector
· 15 min read
I bought a VisualCube X1AR for £30. It’s a cheap “smart” projector that mirrors your phone and runs Android apps. Before putting it on the living-room TV I dropped it on my network to see what it did. I ended up spending a weekend pulling it apart, and most of what I found is bad.
Short version:
- It ships with two ways to get root, on a normal retail build.
- SELinux is set to Permissive, so Android’s sandboxing is effectively off across the whole device.
- There’s a root-owned debug console listening on the network with no password, and it has register read/write commands.
- The built-in app store fetches and installs APKs over plain HTTP, doesn’t check who signed them, and its request signing key is hardcoded to
1234567887654321. It checks in on its own at boot. - All ten preinstalled apps share one Linux UID (
android.uid.system), so popping any one of them lands you in all of them. - A projector asks for camera, microphone and location, despite having no camera or mic at all. There’s also a leftover “test” app with most of those plus secure-settings and reboot.
- It reports your serial, MAC and app-usage in cleartext to servers in China, and the Baidu SDK in the launcher is configured to collect GPS, cell and Wi-Fi location.
The one thing they got right is firmware signing. Everything around it leaks.
Test unit: VisualCube X1AR, “LuminaOS” launcher on BigdroidOS 3.0.3 / Android 14, Allwinner sun50iw15p1, firmware Projector_20251119.140743, channel X1AR_p2_en_LuminaOS.
A note on rigour before we start. This is network and static analysis. Everything below is either output from the device itself or its decompiled code. Where I’m inferring something from code rather than having run it, I say so. I did not weaponise the install path or fire destructive commands at a live unit.
The device
“LuminaOS” is a launcher skin, not an OS. Under it is fairly stock Android 14 on an Allwinner TV chip. The vendor names are scattered through the system: aodin, hotack, triplesai, HanYing, mphotool. So this isn’t one company’s product. It’s a reference image with a pile of third-party SDKs bolted on and a sticker over the top. No single party in that chain looks like it owns or audits the result.
A stranger on the network
There was a device on my LAN I didn’t recognise, 192.168.0.3, with an unfamiliar MAC. Nothing useful answered on the usual ports, so I asked the box what it was listening on:
$ adb -s 192.168.0.3:5555 shell 'netstat -tln | grep LISTEN'
tcp 0 0 0.0.0.0:1234 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN # AirPlay
tcp 0 0 0.0.0.0:7002 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:7102 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:9528 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:55556 0.0.0.0:* LISTEN
tcp6 0 0 :::7889 :::* LISTEN
tcp6 0 0 :::7890 :::* LISTEN
tcp6 0 0 :::8890 :::* LISTEN
tcp6 0 0 :::8899 :::* LISTEN
tcp6 0 0 :::10008 :::* LISTEN
tcp6 0 0 :::10012 :::* LISTEN
tcp6 0 0 :::10013 :::* LISTEN # hands out the installed-app list
A dozen open ports on a projector. AirPlay, a stack of casting services, an endpoint on 10013 that returns the full installed-app list to anyone who asks, and port 1234, which I had not expected to find at all.
The console on 1234
I connected to 1234 and it answered:
$ printf '\r\nhelp\r\n' | nc 192.168.0.3 1234
Build: Oct 21 2025 10:04:53
Copyright: (c) 2025 CF-Vision
CF: help
Command List:
vbi ----- for vbi debug
swpll ----- for crtc debug only
crtc ----- for crtc debug only
vhdmi ----- virtual HDMI RX
pq ----- for pq debug only
win ----- for win unit test
wce ----- for wce debug
bs ----- for bs debug only
app ----- for app debug only
dbgs ----- debug server
haltest ----- for hal debug only
hal ----- for hal debug only
matest ----- for memory agent debug only
ma ----- for memory agent debug only
hdmirx ----- for hdmirx debug only
reg ----- for reg debug only
log ----- configure the logger
clear ----- clear console
help ----- show command info
This is a factory calibration console, the “display processor shell” (/vendor/lib/lib_dp_shell.so). It’s meant to be driven over a serial cable on the production line. Here it’s bound to 0.0.0.0 and answers anyone on the LAN.
Two things I checked rather than assumed. First, it runs as root: the listening socket on 1234 is owned by UID 0 in /proc/net/tcp. Second, there’s no authentication. The library has a Please input password: prompt, but nothing enforces it, and every command works without one.
It is not a general shell. You can’t run arbitrary programs, only the fixed set above, most of which are video pipeline debug. But two of them matter. Asking reg for usage, with no password and no setup, gives you this:
CF: reg
Usage: app [option] command
command
r8 / w8 / wm8 read / write 8-bit register
r16 / w16 / wm16 read / write 16-bit register
r32 / w32 / wm32 read / write 32-bit register
So a network client with no credentials gets root-level register read and write, plus a ma “memory agent” command I didn’t exercise. I didn’t run any reads or writes against the live unit, because a bad address can hang or brick the display, so this comes from the command interface, not a destructive test. I also couldn’t establish which registers reg reaches, whether that’s only the display IP or arbitrary SoC MMIO, since the handler lives in a root process I couldn’t pull. Scope aside, a root-owned register poke interface listening on 0.0.0.0 with no password is not something that should ship.
Two ways to root, and SELinux off
This is a user build, and it’s pre-rooted with its access-control layer disabled:
$ adb -s 192.168.0.3:5555 shell 'getprop ro.build.type; getprop ro.secure; getprop ro.debuggable'
user
1
0
$ adb -s 192.168.0.3:5555 shell getenforce
Permissive
$ adb -s 192.168.0.3:5555 shell 'ls -l /system/xbin/hotacksu'
-rwsr-sr-x 1 root shell 5788 2008-12-31 18:00 /system/xbin/hotacksu
getenforce returns Permissive, so SELinux logs policy violations and then allows them. The part of Android that keeps a compromised process inside its sandbox is switched off for the whole device.
/system/xbin/hotacksu is a setuid+setgid root su clone. A stock user build doesn’t ship one. Its only check is whether the caller is shell or root, so it exists to hand out root. There’s a second route as well: /system/bin/qw --daemon, the koush SuperUser root daemon, running as root with a world-writable control socket.
I want to be precise about impact. I did not get an interactive root shell over adb. Android restricts the capability bounding set for the adb shell user, so the setuid binary can’t complete its transition from a plain shell, and no app is debuggable to bridge the gap. That restriction is stock Android hardening the vendor happened to leave in place. The vendor’s own contributions were two root mechanisms and a disabled SELinux. Since all ten preinstalled apps run as android.uid.system, the realistic path to full root is to compromise any one of them and call hotacksu from that already-privileged context. After that there’s nothing left to contain it.
Every app is system, and the store will install anything
All ten preinstalled apps carry the same line in their manifest:
<manifest ... android:sharedUserId="android.uid.system">
There’s no sandbox between them. Compromise the weakest one and you have all of them at system privilege.
The interesting one is the app store, StoreOs. Three problems stack up.
The request signing key is hardcoded:
// com.htc.storeos, request signing
String chanKey = SystemProperties.get("persist.sys.ChannelKey", "1234567887654321");
// the AES key for the "encrypted" deviceId is just MD5(chanId + chanKey).substring(8, 24)
The key falls back to 1234567887654321, so the signature stops nobody. Anyone can produce a valid signed request, and the same weakness lets an attacker forge the server’s responses.
It installs APKs with no signer check. The endpoints are plain HTTP (http://store-api.aodintech.com/sign/app/list), and installs go through PackageInstaller and pm install -r -d. There is no signature verification anywhere in the decompiled app:
$ grep -rl "checkSignatures" decompiled/store_smali
$ grep -rl "GET_SIGNATURES" decompiled/store_smali
$ echo "exit=$?"
exit=1 # nothing across 2,790 files: it never checks who signed the APK it installs
And the install record the server controls carries isSilentInstall, isBootStartup, launchType and launchParam: silent install, auto-start on boot, auto-launch a chosen component. I also saw the store hit /sign/app/list in the boot capture, so the fetch happens on its own. It doesn’t wait for you to open a store screen.
There’s a second way in that doesn’t even need the network. StoreOs exposes a service, PublicRequestService, under the action com.htc.requesetService (their typo), exported with an intent filter and no guarding permission. It drives the same install pipeline, and the app it lives in holds INSTALL_PACKAGES, DELETE_PACKAGES, WRITE_SECURE_SETTINGS and CLEAR_APP_USER_DATA. So any other app on the device, including a low-privilege one you sideload, can hand it an intent and have it install and launch something at system privilege.
So the code shows StoreOs pulling an app list over forgeable plain HTTP, installing whatever it’s told without checking the signer, and auto-launching it on boot, on its own at startup, and the same pipeline is reachable locally with no permission. Over the network, an attacker who can get in the middle of that traffic (ARP spoofing on the LAN, a poisoned DNS record for store-api.aodintech.com, or a compromised router or upstream) can push and persist code on the device. I did not stand up a rogue server and run the full install through, so this is a vulnerability confirmed by reading the code and the manifest, not a packaged exploit. The one thing stopping the network version is that updating an existing package needs a matching signature, so the clean attack delivers new apps, which is what a store does anyway.
Camera, mic and location on a projector
The permission requests don’t fit a device that only projects an image:
| App | Notable permissions |
|---|---|
HanYing cast (com.android.toofifi) | CAMERA, RECORD_AUDIO, FINE/COARSE_LOCATION, INJECT_EVENTS |
HYK_TEST (leftover test app) | CAMERA, RECORD_AUDIO, CAPTURE_AUDIO_OUTPUT, INJECT_EVENTS, WRITE_SECURE_SETTINGS, REBOOT |
USBCastServer | RECORD_AUDIO |
tpjz_allwinner | CAMERA |
Here’s the part that’s almost funny: the device has no camera and no microphone. It only declares android.hardware.audio.output, there are no /dev/video* nodes, and the camera service sees nothing.
$ adb -s 192.168.0.3:5555 shell 'ls /dev/video*; pm list features | grep -iE "camera|microphone"'
ls: /dev/video*: No such file or directory
feature:android.hardware.audio.output
So the CAMERA and RECORD_AUDIO permissions can’t do anything. They’re dead weight copied in from whatever phone or TV image these apps came from. That makes them less scary and more telling: nobody pruned the manifests, nobody checked what this hardware actually is. The one exception is HYK_TEST’s CAPTURE_AUDIO_OUTPUT, which records the audio the device plays rather than anything from a mic, so that one isn’t hardware-gated. A shipped test app holding system-audio-capture, secure-settings and reboot still has no business being on a retail unit.
It reports home at boot, in cleartext
Within about ten seconds of power-on, with no interaction, the projector starts reporting. From the proxy:
POST http://event-api.aodintech.com/report
{
"deviceId": "08:09:f2:78:62:e0", // MAC address
"deviceSn": "1c000c8975c3c94234f", // hardware serial
"channel": "X1AR_p2_en_LuminaOS",
"eventList": [
{ "eventCode": 100001, "timestamp": ... }, // boot
{ "eventCode": 100002, "content": { "pkgName": "com.htc.luminaos",
"runTime": 1258652 } } // per-app usage
]
}
Plain HTTP. The telemetry app EventUploadService sends MAC, serial, per-app foreground time and boot times every couple of hours. That’s the whole schema, so credit where it’s due: no GPS, no SSID, no screen contents. The launcher’s Baidu analytics SDK is the part to worry about. Its manifest sets BaiduMobAd_GPS_LOCATION=true, BaiduMobAd_CELL_LOCATION=true and BaiduMobAd_WIFI_LOCATION=true, the app holds ACCESS_FINE_LOCATION, and it keeps a WebSocket open to wss://mtjsocket.baidu.com. So the SDK is configured to collect GPS, cell and Wi-Fi location. It also wires up Baidu’s autoTracker and circleConfig endpoints, the SDK’s automatic UI-event tracking that logs which screens and elements you touch without the app having to instrument anything.
One app doesn’t bother with transport security, and says so in its own log:
W/HanYing: No keystore or trust store configured. ACCEPTING UNTRUSTED CERTIFICATES!!!!!
Where the traffic goes
I resolved and geolocated every endpoint. The split is what stands out:
| Endpoint | Function | Hosted |
|---|---|---|
store-api.aodintech.com | silent app push | Tencent Cloud, Hong Kong |
event-api.aodintech.com | telemetry | Tencent Cloud, Hong Kong |
ota.triplesai.com / hardcoded 139.199.190.220 | firmware OTA | Tencent Cloud, Guangzhou / Shenzhen |
hotackiot.com:5503 | third OTA/download server | China Telecom, Chengdu |
mtjsocket.baidu.com, dxp.baidu.com | Baidu telemetry / location | Baidu (China Unicom / Mobile) |
relay.luminago.app | remote-control relay | Cloudflare (origin hidden) |
server.mphotool.com | cast licensing | AWS, US |
develop.waxrain.com | AirPlay licensing | Hong Kong |
The countries matter less than the split by function. Everything that can push code or collect identifiers lives on Chinese cloud or telecom over plain HTTP, while the harmless licensing traffic for casting and AirPlay sits on Western clouds. One firmware server even runs from a China Telecom address in Chengdu, ISP-grade infrastructure rather than a hardened cloud.
One gap I’ll own: the device also keeps a TLS connection up to a remote-control relay, relay.luminago.app, behind Cloudflare. I could see it dial out, but the relay client and its commands live in a system component I didn’t pull, so I can’t say what the cloud can actually tell the device to do. The proven channel home is the cleartext telemetry above. The relay is for a follow-up.
What they got right
Two things, in fairness.
Firmware updates are signed properly. The OTA app passes the package to RecoverySystem.installPackage, and recovery checks it against the OEM key in otacerts.zip on reboot. A man-in-the-middle can’t forge firmware. I assumed otherwise at first; reading the code corrected me.
There are no hardcoded private keys or API secrets in any of the APKs. The one “RSA PRIVATE KEY” string I found turned out to be empty PEM template text inside an mbedTLS-based SDK, not a key.
The quality is uneven. The hard thing, signed firmware, is done right. The easy things, like not shipping root backdoors, not turning off SELinux, pinning the installer’s certificates and using HTTPS, aren’t. As an example, here’s the entire signing routine the store and OTA stack lean on. There’s no secret in it:
// com.util.SignatureUtil (decompiled)
public static String getSign(Map<String, String> params) {
StringBuffer sb = new StringBuffer();
String[] keys = params.keySet().toArray(new String[0]);
Arrays.sort(keys); // 1. sort the parameter keys
for (String k : keys) {
if (!k.equals("")) {
String v = params.get(k);
sb.append(k).append(v == null ? "" : v); // 2. concat key+value
}
}
// 3. SHA1 over the concatenation, hex, lowercase. No key, no salt, no HMAC.
return new String(Hex.encodeHex(
DigestUtils.sha1(sb.toString().getBytes()))).toLowerCase();
}
Every input is public, so anyone can reproduce the signature.
Why it matters
The useful question isn’t whether the manufacturer is watching you. It’s that you’re putting a pre-rooted, unpatched device with an open root console onto your home network. Anyone who shares that network, a guest, a phone with malware on it, a compromised laptop, a hostile router, gets:
- a root-owned register console with no password (the console on 1234),
- a silent installer they can redirect to their own APK for persistence (the store),
- and, once they have any foothold in a system app, two vendor root paths with SELinux already off to finish the job.
A compromised unit makes a quiet botnet node or a foothold to reach everything else on the LAN. Whether the cause is intent or a sloppy supply chain doesn’t change much for the owner. An open root console is exploitable either way.
Disclosure
There’s no real way to report this. The product is a rebadge on an opaque ODM stack (aodin, hotack, triplesai) with no published security contact or disclosure process for anyone in the chain. The servers receiving the telemetry and driving the installer belong to companies the buyer never deals with. I’m writing it up because the fix that exists is the owner’s to apply, not the vendor’s, and because people buying a “smart projector” should know what’s in the box.
If you own one
- Keep it off your main network. Guest VLAN or IoT SSID with client isolation.
- Block its outbound traffic at the router. It still works as a projector.
- Treat casting and the built-in store as attack surface, not features.
- If you care about the network it’s on, a plain projector plus a streaming stick you trust is the safer setup.
Artifacts (APKs, pulled binaries, the traffic capture and the disassembly) are kept for verification. Findings reproduce on the same model and firmware build, Projector_20251119.140743.