Android virtual camera for authorized security testing. ReCam injects a payload into
cameraserver and replaces the pixels in camera buffers before they reach the app, so
every camera client on the device sees whatever you feed it: a video file, a live
desktop stream, an RTMP publisher, or a webcam.
Nothing is modified in the target app. No Xposed, no repackaging, no per-app hooks. The substitution happens once, in the camera service, and applies to everything.
Root is required.
This is a tool for testing systems you own or have written authorization to test. Camera input is a trust boundary for KYC, liveness, document capture, and attendance systems, and the only way to know whether one can be fooled is to try it against your own deployment. Using it against services you do not own or have permission to test is likely illegal where you live.
recam_inject resolves android::camera3::Camera3OutputStream::returnBufferCheckedLocked
in the running cameraserver, attaches with ptrace, and loads librecam.so. The
payload installs an inline hook on that function. Each time the camera service returns
a filled buffer to a stream, the hook locks the gralloc buffer, overwrites the YUV
planes, and lets the original function continue.
Frame metadata is never touched. Timestamps, exposure, focus state and sensor characteristics are whatever the real camera reported, so the substitution does not show up in capture results.
Pixels arrive through a shared memory ring that recam_feed fills from a separate
process, so decoding and scaling never run inside the camera service.
- Works with any camera app. The hook is in the camera service, below the app layer.
- Preview, stills and video recording. Including
IMPLEMENTATION_DEFINEDbuffers, where the layout is read from the gralloc driver rather than guessed. - Several stream sizes at once. The ring keeps four size-keyed lanes, so an app running preview and analysis at different resolutions gets correct frames on both.
- Live desktop streaming. H.264 over TCP through
adb forward. A built-in RTMP listener accepts OBS and anything else that can publish. - Any local capture device. Including OBS Virtual Camera.
- Reversible.
recam_stopremoves the hook and restores the real camera without restartingcameraserver. - One payload at a time. Re-injecting stands the previous copy down first.
- Self-test.
recam_testruns 63 checks inside the camera service covering the hook, buffer layouts, fence ownership and the frame ring.
Device
- Rooted, with a working
su - arm64-v8a, Android 11 (API 30) or newer
- USB debugging
Host
adbon PATHffmpegon PATH, for the live, RTMP and camera sources- Python 3.9+ and PySide6, for the desktop console
The full chain is verified on one physical device: Android 16, arm64-v8a, MediaTek (PowerVR gralloc). Symbol resolution is verified across Android 11 through 16, and the dynamic-linkage path is verified on an Android 14 x86_64 emulator.
Gralloc behaviour on Qualcomm and Exynos is untested. Where the device cannot describe a buffer, the hook passes the real frame through rather than writing a guess, so an unsupported device shows the real camera instead of corrupted output.
Build requirements
- Android NDK r27 or newer (tested with r27.3.13750724)
- Python 3.9+
- PyInstaller, only for a frozen release
Device binaries:
cd android
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=jni/Application.mk -j8Output lands in android/libs/arm64-v8a. The desktop console finds it there
automatically, so this is enough for development.
Desktop console:
cd desktop
pip install -r requirements.txt
python main.pyPick a device, pick a source, press Start. The console pushes the device binaries when their content hash differs from what is already on the device, so there is no separate deploy step.
A frozen release with the device binaries bundled:
export ANDROID_NDK_HOME=/path/to/ndk
python tools/package.pyThis runs ndk-build, folds the six device binaries into the package, and writes
dist/ReCam. adb and ffmpeg are not bundled and remain PATH dependencies.
The console offers five. adb handles the transport; ffmpeg does any encoding.
| Source | What happens |
|---|---|
| Test pattern | Scrolling colour bars with a moving marker |
| RTMP | ffmpeg listens; publish to the Server and Stream Key shown beside the radio button |
| Video file | Pushed to /data/local/tmp/recam_source.mp4 and looped on the device |
| FFmpeg | A path, a URL, or raw input flags if the text starts with - |
| Camera | Any local capture device, including OBS Virtual Camera |
Virtual camera. In OBS press Start Virtual Camera, then pick OBS Virtual Camera
under Camera and press Start. Nothing else to configure.
RTMP. Select RTMP and press Start first, so ffmpeg is listening. Then in OBS go to Settings -> Stream -> Service: Custom and copy the Server and Stream Key shown beside the radio button. The key must match exactly; the listener rejects any other. Set Output -> Encoder -> B-frames to 0, because ReCam synthesises timestamps in decode order and B-frames would reorder the output.
The virtual camera is less to get wrong. RTMP is worth the extra steps only if you want OBS's own encoder settings to reach the device.
The console is a front end for these. They take no arguments.
| Tool | Does |
|---|---|
recam_inject |
Loads the payload and installs the hook |
recam_feed |
Fills the frame ring from a file, a live sender, or a built-in test pattern |
recam_stop |
Removes the hook and restores the real camera |
recam_status |
Prints state as JSON; read-only and safe to poll |
recam_test |
Runs the self-test inside cameraserver |
recam_watch |
Re-injects when cameraserver respawns |
adb push android/libs/arm64-v8a/* /data/local/tmp/
adb shell su -c 'chmod 755 /data/local/tmp/recam_*'
adb shell su -c '/data/local/tmp/recam_inject'
adb shell su -c '/data/local/tmp/recam_feed' # Ctrl-C to stop
adb shell su -c '/data/local/tmp/recam_stop'recam_feed plays /data/local/tmp/recam_source.mp4 if it exists, accepts an H.264
stream on 127.0.0.1:27183, and otherwise generates a test pattern. A live sender takes
priority over the file.
android/ device side, C++ and ndk-build
jni/ Android.mk, Application.mk
src/ common, payload, injector, feed
sepolicy/ the two SELinux rules, both from observed denials
patches/ local change to the vendored injector
desktop/ host side, Python and PySide6
tools/ build and release scripts
- AndKittyInjector by MJ, MIT. Vendored at
6d55e9fwith one local patch, seeandroid/patches. - XZ Embedded, public domain, used to
read
.gnu_debugdatawhen the target symbol is not exported.
MIT. See LICENSE.