11 Commits
v2.0 ... v3.1

Author SHA1 Message Date
Dave
2dea346723 Fix bug importing keyboard layouts (#302) 2024-12-29 13:48:06 -06:00
Dave
8b11882c52 Pico2 support (#299)
* Add support for Pico2/2W boards

* Update documentation to include Pico2/2W

* update language support
2024-12-21 12:00:23 -06:00
Dave
8bcd2aa456 Add bundle script 2024-12-20 23:41:27 -06:00
Tony
f1636c3e45 Add functions, while loops, and vars support (#264)
* Add functions, while loops, and vars support

* Add back REPEAT

* Add support for nested WHILE loops in function

---------

Co-authored-by: Tony Le <tonyle@coxautoinc.com>
2024-09-21 12:18:16 -05:00
Dave
5dd0783886 Add debugging instructions 2024-03-12 19:54:10 -05:00
William
3caf2ceb98 added new wait_for_button_press functionality (#234) 2024-02-13 20:16:43 -06:00
Dave
70eb2cd8b0 Add link to defcon31-ducky project 2024-01-16 19:18:05 -06:00
Piotr Ginał
0113b0e004 Fix typo (#197) 2023-09-17 15:59:29 -05:00
Dave
74ef11770b Clarify getting latest release 2023-06-24 13:07:19 -05:00
Dave
a7992c2fb3 Update installation instructions 2023-06-24 12:58:58 -05:00
Dave
49e81125c1 Updated issue template 2023-06-24 10:01:39 -05:00
10 changed files with 326 additions and 38 deletions

View File

@@ -19,5 +19,11 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Debug info**
If possible, include debug serial data.
On Windows, use PuTTY, connect to the debug serial port (commonly COM3, but could vary)
On Linux, use minicom (minicom -b 115200 -o -D /dev/ttyACM0, where ttyACM0 corresponds to your device)
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

6
DEBUG.md Normal file
View File

@@ -0,0 +1,6 @@
# Instructions on how to collect debug logs
* On Windows, use [PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html) to connect to the debug serial port (commonly COM3, but could vary on your machine).
* On Linux, use minicom (minicom -b 115200 -o -D /dev/ttyACM0, where ttyACM0 corresponds to your device).
* On Ubuntu: `sudo apt install minicom`

View File

@@ -16,20 +16,84 @@
<br /> <br />
## Install ## Quick Start Guide
Install and have your USB Rubber Ducky working in less than 5 minutes.
1. Download the latest release from the [Releases](https://github.com/dbisu/pico-ducky/releases) page.
2. Plug the device into a USB port while holding the boot button. It will show up as a removable media device named RPI-RP2.
3. Install CircutlPython on the Pico or Pico W
If using a Pico board:
Copy the adafruit-circuitpython-raspberry_pi_pico-en_US-9.2.1.uf2 file to the root of the Pico (RPI-RP2). The device will reboot and after a second or so, it will reconnect as CIRCUITPY.
If using a Pico W board:
Copy the adafruit-circuitpython-raspberry_pi_pico_w-en_US-9.2.1.uf2 file to the root of the Pico (RPI-RP2). The device will reboot and after a second or so, it will reconnect as CIRCUITPY.
If using a Pico 2 board:
Copy the adafruit-circuitpython-raspberry_pi_pico2-en_US-9.2.1.uf2 file to the root of the Pico (RPI-RP2). The device will reboot and after a second or so, it will reconnect as CIRCUITPY.
If using a Pico 2W board:
Copy the adafruit-circuitpython-raspberry_pi_pico2_w-en_US-9.2.1.uf2 file to the root of the Pico (RPI-RP2). The device will reboot and after a second or so, it will reconnect as CIRCUITPY.
4. Copy the lib folder to the root of the CIRCUITPY
5. Copy *.py to the root of the CIRCUITPY
6. Follow the instructions in README.md to enter setup mode
7. Copy your payload as payload.dd to the root of the CIRCUITPY
8. Unplug the device from the USB port and remove the setup jumper.
Enjoy your Pico-Ducky.
## Setup mode
To edit the payload, enter setup mode by connecting the pin 1 (`GP0`) to pin 3 (`GND`), this will stop the pico-ducky from injecting the payload in your own machine.
The easiest way to do so is by using a jumper wire between those pins as seen bellow.
![Setup mode with a jumper](images/setup-mode.png)
## USB enable/disable mode
If you need the pico-ducky to not show up as a USB mass storage device for stealth, follow these instructions.
- Enter setup mode.
- Copy your payload script to the pico-ducky.
- Disconnect the pico from your host PC.
- Connect a jumper wire between pin 18 (`GND`) and pin 20 (`GPIO15`).
This will prevent the pico-ducky from showing up as a USB drive when plugged into the target computer.
- Remove the jumper and reconnect to your PC to reprogram.
Pico: The default mode is USB mass storage enabled.
Pico W: The default mode is USB mass storage **disabled**
![USB enable/disable mode](images/usb-boot-mode.png)
-----
# Full Install Instructions
Install and have your USB Rubber Ducky working in less than 5 minutes. Install and have your USB Rubber Ducky working in less than 5 minutes.
1. Clone the repo to get a local copy of the files. `git clone https://github.com/dbisu/pico-ducky.git` 1. Clone the repo to get a local copy of the files. `git clone https://github.com/dbisu/pico-ducky.git`
2. Download [CircuitPython for the Raspberry Pi Pico](https://circuitpython.org/board/raspberry_pi_pico/). *Updated to 8.0.0 2. Download [CircuitPython for the Raspberry Pi Pico](https://circuitpython.org/board/raspberry_pi_pico/). *Updated to 9.2.1
Download [CircuitPython for the Raspberry Pi Pico W](https://circuitpython.org/board/raspberry_pi_pico_w/). *Updated to 8.0.0 Download [CircuitPython for the Raspberry Pi Pico W](https://circuitpython.org/board/raspberry_pi_pico_w/). *Updated to 9.2.1
Download [CircuitPython for the Raspberry Pi Pico 2](https://circuitpython.org/board/raspberry_pi_pico2/). *Updated to 9.2.1
Download [CircuitPython for the Raspberry Pi Pico 2W](https://circuitpython.org/board/raspberry_pi_pico2_w/). *Updated to 9.2.1
3. Plug the device into a USB port while holding the boot button. It will show up as a removable media device named `RPI-RP2`. 3. Plug the device into a USB port while holding the boot button. It will show up as a removable media device named `RPI-RP2`.
4. Copy the downloaded `.uf2` file to the root of the Pico (`RPI-RP2`). The device will reboot and after a second or so, it will reconnect as `CIRCUITPY`. 4. Copy the downloaded `.uf2` file to the root of the Pico (`RPI-RP2`). The device will reboot and after a second or so, it will reconnect as `CIRCUITPY`.
5. Download `adafruit-circuitpython-bundle-8.x-mpy-YYYYMMDD.zip` [here](https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases/latest) and extract it outside the device. 5. Download `adafruit-circuitpython-bundle-9.x-mpy-YYYYMMDD.zip` [here](https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases/latest) and extract it outside the device.
6. Navigate to `lib` in the recently extracted folder and copy `adafruit_hid` to the `lib` folder on your Raspberry Pi Pico. 6. Navigate to `lib` in the recently extracted folder and copy `adafruit_hid` to the `lib` folder on your Raspberry Pi Pico.
@@ -46,7 +110,7 @@ Install and have your USB Rubber Ducky working in less than 5 minutes.
12. *For Pico W Only* Create the file `secrets.py` in the root of the Pico W. This contains the AP name and password to be created by the Pico W. 12. *For Pico W Only* Create the file `secrets.py` in the root of the Pico W. This contains the AP name and password to be created by the Pico W.
`secrets = { 'ssid' : "BadAPName", 'password' : "badpassword" }` `secrets = { 'ssid' : "BadAPName", 'password' : "badpassword" }`
13. Find a script [here](https://github.com/hak5/usbrubberducky-payloads) or [create your own one using Ducky Script](https://docs.hak5.org/hak5-usb-rubber-ducky/ducky-script-basics/hello-world) and save it as `payload.dd` in the Pico. Currently, pico-ducky only supports DuckyScript 1.0, not 3.0. 13. Find a script [here](https://github.com/hak5/usbrubberducky-payloads) or [create your own one using Ducky Script](https://docs.hak5.org/hak5-usb-rubber-ducky/ducky-script-basics/hello-world) and save it as `payload.dd` in the Pico. Currently, pico-ducky only supports DuckyScript 1.0, and some of 3.0.
14. Be careful, if your device isn't in [setup mode](#setup-mode), the device will reboot and after half a second, the script will run. 14. Be careful, if your device isn't in [setup mode](#setup-mode), the device will reboot and after half a second, the script will run.
@@ -70,14 +134,14 @@ API endpoints
/api/run/<filenumber> /api/run/<filenumber>
``` ```
### Setup mode ## Setup mode
To edit the payload, enter setup mode by connecting the pin 1 (`GP0`) to pin 3 (`GND`), this will stop the pico-ducky from injecting the payload in your own machine. To edit the payload, enter setup mode by connecting the pin 1 (`GP0`) to pin 3 (`GND`), this will stop the pico-ducky from injecting the payload in your own machine.
The easiest way to do so is by using a jumper wire between those pins as seen bellow. The easiest way to do so is by using a jumper wire between those pins as seen bellow.
![Setup mode with a jumper](images/setup-mode.png) ![Setup mode with a jumper](images/setup-mode.png)
### USB enable/disable mode ## USB enable/disable mode
If you need the pico-ducky to not show up as a USB mass storage device for stealth, follow these instructions. If you need the pico-ducky to not show up as a USB mass storage device for stealth, follow these instructions.
- Enter setup mode. - Enter setup mode.
@@ -92,7 +156,7 @@ Pico W: The default mode is USB mass storage **disabled**
![USB enable/disable mode](images/usb-boot-mode.png) ![USB enable/disable mode](images/usb-boot-mode.png)
### Multiple payloads ## Multiple payloads
Multiple payloads can be stored on the Pico and Pico W. Multiple payloads can be stored on the Pico and Pico W.
To select a payload, ground one of these pins: To select a payload, ground one of these pins:
@@ -101,7 +165,7 @@ To select a payload, ground one of these pins:
- GP10 - payload3.dd - GP10 - payload3.dd
- GP11 - payload4.dd - GP11 - payload4.dd
### Changing Keyboard Layouts ## Changing Keyboard Layouts
Copied from [Neradoc/Circuitpython_Keyboard_Layouts](https://github.com/Neradoc/Circuitpython_Keyboard_Layouts/blob/main/PICODUCKY.md) Copied from [Neradoc/Circuitpython_Keyboard_Layouts](https://github.com/Neradoc/Circuitpython_Keyboard_Layouts/blob/main/PICODUCKY.md)
@@ -197,3 +261,9 @@ You can find the tool [here](https://github.com/raspberrydeveloper/pyducky)
[USB Rubber Ducky playlist by **Hak5**](https://www.youtube.com/playlist?list=PLW5y1tjAOzI0YaJslcjcI4zKI366tMBYk) [USB Rubber Ducky playlist by **Hak5**](https://www.youtube.com/playlist?list=PLW5y1tjAOzI0YaJslcjcI4zKI366tMBYk)
[CircuitPython tutorial on the Raspberry Pi Pico by **DroneBot Workshop**](https://www.youtube.com/watch?v=07vG-_CcDG0) [CircuitPython tutorial on the Raspberry Pi Pico by **DroneBot Workshop**](https://www.youtube.com/watch?v=07vG-_CcDG0)
## Related Projects
[Defcon31-ducky](https://github.com/iot-pwn/defcon31-ducky)
There are still a few of these available to purchase, US only.

View File

@@ -24,10 +24,10 @@ noStorageStatus = noStoragePin.value
# GP15 not connected == USB NOT visible # GP15 not connected == USB NOT visible
# GP15 connected to GND == USB visible # GP15 connected to GND == USB visible
if(board.board_id == 'raspberry_pi_pico'): if(board.board_id == 'raspberry_pi_pico' or board.board_id == 'raspberry_pi_pico2'):
# On Pi Pico, default to USB visible # On Pi Pico, default to USB visible
noStorage = not noStorageStatus noStorage = not noStorageStatus
elif(board.board_id == 'raspberry_pi_pico_w'): elif(board.board_id == 'raspberry_pi_pico_w' or board.board_id == 'raspberry_pi_pico2_w'):
# on Pi Pico W, default to USB hidden by default # on Pi Pico W, default to USB hidden by default
# so webapp can access storage # so webapp can access storage
noStorage = noStorageStatus noStorage = noStorageStatus

View File

@@ -0,0 +1,125 @@
import os
import shutil
import re
import sys
import zipfile
languages = [ "MAC_FR",
"US_DVO",
"WIN_BR",
"WIN_CZ",
"WIN_CZ1",
"WIN_DA",
"WIN_DE",
"WIN_ES",
"WIN_FR",
"WIN_HU",
"WIN_IT",
"WIN_PO",
"WIN_SW",
"WIN_TR",
"WIN_UK" ]
supported_boards = ["raspberry_pi_pico",
"raspberry_pi_pico_w",
"raspberry_pi_pico2",
"raspberry_pi_pico2_w"]
files_to_bundle = ["boot.py",
"code.py",
"duckyinpython.py",
"wsgiserver.py",
"webapp.py",
"secrets.py",
"payload.dd",
"payload2.dd",
"payload3.dd",
"payload4.dd",
"INSTALL.txt"]
dirs_to_bundle = ["lib"]
def bundle_files_to_zip(source_dir, destination_dir, file_list, target_file, replacement_dict, version):
"""
Bundles files from a source directory into a new directory with a unique name.
Args:
source_dir: Path to the source directory containing the files.
destination_dir: Path to the destination directory where bundles will be created.
file_list: List of filenames to be included in the bundle.
target_file: Filename of the file to be modified.
replacement_dict: Dictionary containing key-value pairs for text replacements.
Returns:
None
"""
if not os.path.exists(destination_dir):
os.makedirs(destination_dir)
# Generate a unique bundle name (e.g., using a timestamp)
bundle_name = f"pico-ducky-{version}-{destination_dir}.zip"
bundle_path = os.path.join(destination_dir, bundle_name)
# Create a temporary directory for the bundle contents
temp_dir = os.path.join(destination_dir, "temp_bundle")
os.makedirs(temp_dir)
for filename in file_list:
source_file = os.path.join(source_dir, filename)
destination_file = os.path.join(temp_dir, filename)
if filename == target_file:
with open(source_file, 'r') as f:
file_content = f.read()
for key, value in replacement_dict.items():
file_content = re.sub(key, value, file_content)
with open(destination_file, 'w') as f:
f.write(file_content)
else:
shutil.copy2(source_file, destination_file)
for dir in dirs_to_bundle:
shutil.copytree(os.path.join(source_dir,dir),os.path.join(temp_dir,dir))
#find uf2 files for supported boards
for root, dirs, files in os.walk(source_dir):
for file in files:
for board in supported_boards:
if '-'+board+'-' in file:
source_file = os.path.join(source_dir, file)
destination_file = os.path.join(temp_dir, file)
shutil.copy2(source_file, destination_file)
# Create the ZIP archive
with zipfile.ZipFile(bundle_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(temp_dir):
for file in files:
file_path = os.path.join(root, file)
archive_path = os.path.relpath(file_path, temp_dir)
zipf.write(file_path, archive_path)
# Remove the temporary directory
shutil.rmtree(temp_dir)
def main(argv):
version = argv[0]
for dest_dir in languages:
source_directory = "US"
target_file_to_modify = "duckyinpython.py"
replacements = {
"#from keyboard_layout_win_LANG": "from keyboard_layout_"+dest_dir.lower(),
"#from keycode_win_LANG": "from keycode_"+dest_dir.lower(),
"from adafruit_hid.keyboard": "#from adafruit_hid.keyboard",
"from adafruit_hid.keycode": "#from adafruit_hid.keycode"
}
bundle_files_to_zip(source_directory, dest_dir, files_to_bundle,
target_file_to_modify, replacements, version)
if __name__ == "__main__":
main(sys.argv[1:])

View File

@@ -12,7 +12,7 @@ import digitalio
from board import * from board import *
import board import board
from duckyinpython import * from duckyinpython import *
if(board.board_id == 'raspberry_pi_pico_w'): if(board.board_id == 'raspberry_pi_pico_w' or board.board_id == 'raspberry_pi_pico2_w'):
import wifi import wifi
from webapp import * from webapp import *
@@ -41,9 +41,9 @@ def startWiFi():
#supervisor.disable_autoreload() #supervisor.disable_autoreload()
supervisor.runtime.autoreload = False supervisor.runtime.autoreload = False
if(board.board_id == 'raspberry_pi_pico'): if(board.board_id == 'raspberry_pi_pico' or board.board_id == 'raspberry_pi_pico2'):
led = pwmio.PWMOut(board.LED, frequency=5000, duty_cycle=0) led = pwmio.PWMOut(board.LED, frequency=5000, duty_cycle=0)
elif(board.board_id == 'raspberry_pi_pico_w'): elif(board.board_id == 'raspberry_pi_pico_w' or board.board_id == 'raspberry_pi_pico2_w'):
led = digitalio.DigitalInOut(board.LED) led = digitalio.DigitalInOut(board.LED)
led.switch_to_output() led.switch_to_output()
@@ -68,7 +68,7 @@ async def main_loop():
global led,button1 global led,button1
button_task = asyncio.create_task(monitor_buttons(button1)) button_task = asyncio.create_task(monitor_buttons(button1))
if(board.board_id == 'raspberry_pi_pico_w'): if(board.board_id == 'raspberry_pi_pico_w' or board.board_id == 'raspberry_pi_pico2_w'):
pico_led_task = asyncio.create_task(blink_pico_w_led(led)) pico_led_task = asyncio.create_task(blink_pico_w_led(led))
print("Starting Wifi") print("Starting Wifi")
startWiFi() startWiFi()

View File

@@ -2,7 +2,7 @@
# copyright (c) 2023 Dave Bailey # copyright (c) 2023 Dave Bailey
# Author: Dave Bailey (dbisu, @daveisu) # Author: Dave Bailey (dbisu, @daveisu)
import re
import time import time
import digitalio import digitalio
from digitalio import DigitalInOut, Pull from digitalio import DigitalInOut, Pull
@@ -20,7 +20,7 @@ from adafruit_hid.keycode import Keycode
# uncomment these lines for non_US keyboards # uncomment these lines for non_US keyboards
# replace LANG with appropriate language # replace LANG with appropriate language
#from keyboard_layout_win_LANG import KeyboardLayout #from keyboard_layout_win_LANG import KeyboardLayout as KeyboardLayout
#from keycode_win_LANG import Keycode #from keycode_win_LANG import Keycode
duckyCommands = { duckyCommands = {
@@ -47,6 +47,10 @@ duckyCommands = {
'F12': Keycode.F12, 'F12': Keycode.F12,
} }
variables = {}
functions = {}
def convertLine(line): def convertLine(line):
newline = [] newline = []
# print(line) # print(line)
@@ -68,15 +72,17 @@ def convertLine(line):
return newline return newline
def runScriptLine(line): def runScriptLine(line):
if isinstance(line, str):
line = convertLine(line)
for k in line: for k in line:
kbd.press(k) kbd.press(k)
kbd.release_all() kbd.release_all()
def sendString(line): def sendString(line):
layout.write(line) layout.write(line)
def parseLine(line): def parseLine(line, script_lines):
global defaultDelay global defaultDelay, variables, functions
line = line.strip()
if(line[0:3] == "REM"): if(line[0:3] == "REM"):
# ignore ducky script comments # ignore ducky script comments
pass pass
@@ -97,6 +103,59 @@ def parseLine(line):
led.value = False led.value = False
else: else:
led.value = True led.value = True
elif(line[0:21] == "WAIT_FOR_BUTTON_PRESS"):
button_pressed = False
# NOTE: we don't use assincio in this case because we want to block code execution
while not button_pressed:
button1.update()
button1Pushed = button1.fell
button1Released = button1.rose
button1Held = not button1.value
if(button1Pushed):
print("Button 1 pushed")
button_pressed = True
elif line.startswith("VAR"):
_, var, _, value = line.split()
variables[var] = int(value)
elif line.startswith("FUNCTION"):
func_name = line.split()[1]
functions[func_name] = []
line = next(script_lines).strip()
while line != "END_FUNCTION":
functions[func_name].append(line)
line = next(script_lines).strip()
elif line.startswith("WHILE"):
condition = re.search(r'\((.*?)\)', line).group(1)
var_name, _, condition_value = condition.split()
condition_value = int(condition_value)
loop_code = []
line = next(script_lines).strip()
while line != "END_WHILE":
if not (line.startswith("WHILE")):
loop_code.append(line)
line = next(script_lines).strip()
while variables[var_name] > condition_value:
for loop_line in loop_code:
parseLine(loop_line, {})
variables[var_name] -= 1
elif line in functions:
updated_lines = []
inside_while_block = False
for func_line in functions[line]:
if func_line.startswith("WHILE"):
inside_while_block = True # Start skipping lines
updated_lines.append(func_line)
elif func_line.startswith("END_WHILE"):
inside_while_block = False # Stop skipping lines
updated_lines.append(func_line)
parseLine(updated_lines[0], iter(updated_lines))
updated_lines = [] # Clear updated_lines after parsing
elif inside_while_block:
updated_lines.append(func_line)
elif not (func_line.startswith("END_WHILE") or func_line.startswith("WHILE")):
parseLine(func_line, iter(functions[line]))
else: else:
newScriptLine = convertLine(line) newScriptLine = convertLine(line)
runScriptLine(newScriptLine) runScriptLine(newScriptLine)
@@ -138,17 +197,19 @@ def runScript(file):
duckyScriptPath = file duckyScriptPath = file
try: try:
f = open(duckyScriptPath,"r",encoding='utf-8') with open(duckyScriptPath, "r", encoding='utf-8') as f:
script_lines = iter(f.readlines())
previousLine = "" previousLine = ""
for line in f: for line in script_lines:
line = line.rstrip() print(f"runScript: {line}")
if(line[0:6] == "REPEAT"): if(line[0:6] == "REPEAT"):
for i in range(int(line[7:])): for i in range(int(line[7:])):
#repeat the last command #repeat the last command
parseLine(previousLine) parseLine(previousLine, script_lines)
time.sleep(float(defaultDelay) / 1000) time.sleep(float(defaultDelay) / 1000)
else: else:
parseLine(line) parseLine(line, script_lines)
previousLine = line previousLine = line
time.sleep(float(defaultDelay) / 1000) time.sleep(float(defaultDelay) / 1000)
except OSError as e: except OSError as e:
@@ -188,9 +249,9 @@ def selectPayload():
async def blink_led(led): async def blink_led(led):
print("Blink") print("Blink")
if(board.board_id == 'raspberry_pi_pico'): if(board.board_id == 'raspberry_pi_pico' or board.board_id == 'raspberry_pi_pico2'):
blink_pico_led(led) blink_pico_led(led)
elif(board.board_id == 'raspberry_pi_pico_w'): elif(board.board_id == 'raspberry_pi_pico_w' or board.board_id == 'raspberry_pi_pico2_w'):
blink_pico_w_led(led) blink_pico_w_led(led)
async def blink_pico_led(led): async def blink_pico_led(led):

13
examples/functions.dd Normal file
View File

@@ -0,0 +1,13 @@
REM Example Function
FUNCTION COUNTDOWN()
REM The next four lines open Notepad in Windows and type "Hello World!"
GUI r
DELAY 1000
STRING notepad
ENTER
DELAY 2000
STRING Hello World!
ENTER
END_FUNCTION
COUNTDOWN()

7
examples/while_loops.dd Normal file
View File

@@ -0,0 +1,7 @@
VAR $TIME = 3
WHILE ($TIME > 0)
STRING .
DELAY 500
STRING While Looop!!
ENTER
END_WHILE

View File

@@ -232,7 +232,7 @@ async def startWebService():
PORT = 80 # Port to listen on PORT = 80 # Port to listen on
print(HOST,PORT) print(HOST,PORT)
wsgiServer = server.WSGIServer(80, application=web_app) wsgiServer = server.WSGIServer(PORT, application=web_app)
print(f"open this IP in your browser: http://{HOST}:{PORT}/") print(f"open this IP in your browser: http://{HOST}:{PORT}/")