This document outlines the critical technical decisions, architectural patterns, and "lessons learned" during the development of DuckyIDE. It serves as a reference for future contributors to understand why the code is written the way it is.
Android's default init system aggressively manages the sys.usb.config property. Simply appending "hid" to this property often fails because the OS doesn't know how to construct a custom composite gadget.
- Conflict: Setting
sys.usb.configtriggers Android's USB HAL, which wipes manual changes to ConfigFS. - Ordering: Windows is picky about Interface Descriptor ordering. If ADB isn't first, or if interfaces aren't numbered sequentially, the driver installation fails.
- Naming: Strict kernels (Samsung/Pixel) require function symlinks in
configs/b.1/to be named sequentially (e.g.,f1,f2,f3) rather than their source names (e.g.,mass_storage.0).
We implemented a "Total Control" workflow that mimics Kali Nethunter's boot scripts:
- Disable UDC:
echo "" > UDCstops the gadget physically. - Kill the Daemon:
stop adbdensures the debugger doesn't fight us. - Reset State:
setprop sys.usb.config noneforces the OS to release its grip on ConfigFS. - Manual Composition: We manually symlink functions from
/functions/to/configs/b.1/.- Ordering: ADB (
f1) -> RNDIS (f2) -> MTP (f3) -> Mass Storage (f4) -> HID (f5). - Dynamic Naming: We assume the kernel needs aliases. We check for
ffs.adbbut link it asf1.
- Ordering: ADB (
- Re-Enable: Write the UDC name back to
UDCand manually restartadbd.
Sending keystrokes to /dev/hidg0 sounds simple, but doing it reliably and fast on Android is difficult.
echo -ne "\x...":- Failure: Android shells (mksh/toybox) behave inconsistently with escape sequences.
\x00often gets eaten or misinterpreted, leading to "stuck keys" (modifiers not releasing).
- Failure: Android shells (mksh/toybox) behave inconsistently with escape sequences.
- Binary Tool (
hid-keyboard):- Failure: Spawning a new process for every single keystroke (
Runtime.exec) is incredibly slow (10-20ms overhead per key). A long script takes minutes to type.
- Failure: Spawning a new process for every single keystroke (
- Current Solution: Base64 Stream Pipelining (Implemented in
DuckyParser.java)
To achieve atomic, binary-safe, and fast injection:
- Java Buffering: The
DuckyParserconverts the entire Ducky Script into a rawbyte[]array in memory (Java). Each keystroke is an 8-byte packet (Mod, Res, Key, 0, 0, 0, 0, 0). - Encoding: We Base64 encode this binary blob.
- Atomic Delivery: We generate a single shell command:
echo "BASE64_STRING..." | base64 -d | dd of=/dev/hidg0 bs=8 2>/dev/null
base64 -d: Decodes the data back to raw binary on the device side.dd of=/dev/hidg0 bs=8: Writes to the driver in 8-byte blocks. This is crucial. If you write 7 bytes or 9 bytes, the HID driver rejects the packet.ddguarantees block alignment.
Standard Runtime.exec() is insufficient because it doesn't capture exit codes or stderr effectively for chained commands.
RootShell.java: Uses a customCommandResultclass.- It executes commands via
suand appends a marker (echo ::::EXITCODE::::$?::::) to stdout. - This allows us to parse the exact exit code of the root command and throw Java
IOExceptions if a specific step in the USB setup fails.
- Logging: Logs are written to
Context.getExternalFilesDir()(/sdcard/Android/data/...) so they persist even if the app crashes, viewable via PC. - Dependencies: We previously used
hid-keyboardbinaries but moved to the Base64 method to remove binary dependencies and architecture mismatches (ARM vs ARM64).
- DuckyParser: Contains a hardcoded
ASCII_MAPmapping characters to HID usage codes (e.g., 'a' -> 0x04). - Modifiers: Supports explicit modifiers (CTRL, ALT, GUI, SHIFT) via bitwise OR operations on the modifier byte (Byte 0 of the report).
- Protocol: We strictly use the Boot Keyboard Protocol (8 bytes). Some kernels support 7 bytes, but 8 is the standard.
Generated by Gemini CLI Agent