dbisu / pico-ducky

Create a USB Rubber Ducky like device using a Raspberry PI Pico
GNU General Public License v2.0
2.43k stars 440 forks source link

Turing-Complete DuckyScript 3.0 (No Advanced Features) #125

Closed Desperationis closed 1 year ago

Desperationis commented 1 year ago

DuckyScript 3.0

This is an interpreter for DuckyScript 3.0 that can handle variables, constants, conditional statements, nested conditional statements, loops, and functions. Almost all of the functionality in duckyinpython.py is implemented except payload-selection using GPIO pins. Other than that, it is a drop-in replacement that makes the language turing-complete.

No advanced features have been added. This means randomization, holding keys, payload control, jitter, payload hiding, storage activity, lock keys, exfiltration, and extensions have not been added. No internal variables either. However, adding the implementation of these commands should be super easy based on the structure of this script.

Example

REM This is a testing file to max out all aspects of DuckyScript 3.0 Programming Features.

STRINGLN Hello World!
STRINGLN Wait a minute, is it raining?

$RAINING = TRUE
IF ( $RAINING == TRUE) THEN
    STRINGLN It is raining! That sucks.
END_IF

STRINGLN At the very least the sky isn't RAINING and HAILING right?
VAR $HAILING = FALSE

IF ( $RAINING == TRUE && $HAILING == TRUE ) THEN
    STRINGLN It is raining and hailing! That sucks.
ELSE IF ( $RAINING == TRUE && $HAILING == FALSE) THEN
    STRINGLN See, it is only raining!
END_IF

STRINGLN But if it's RAINING and NOT HAILING, then is it snowing?
IF ( $RAINING == TRUE) THEN
    IF ( $HAILING == FALSE) THEN
        DEFINE SNOWING TRUE

        IF (SNOWING == TRUE) THEN
            STRINGLN Aw man, it is snowing!
        END_IF
    END_IF
END_IF

STRINGLN If we count to 5, then I'm sure it will all go away.
$I = 0
WHILE ($I < 5) 
    $I = ($I + 1)
    STRINGLN $I
END_WHILE

STRINGLN It didn't work! We need to only count even integers! Let's try again.

$I = 0
WHILE ($I < 5) 
    $I = ($I + 1)
    IF ($I % 2 == 0) THEN
        STRINGLN $I
    END_IF
END_WHILE

STRINGLN Now its a clear and sunny day! Lets see if we can win the lottery.
STRINGLN      

DEFINE WINNING_NUMBER 7823

FUNCTION CHECK_LOTTERY_NUMBER()
    STRING Your number is 
    STRINGLN $LOTTERY_NUM

    IF ($LOTTERY_NUM == WINNING_NUMBER) THEN
        STRINGLN Congrats! You got the winning number!
    ELSE IF ($LOTTERY_NUM != WINNING_NUMBER) THEN
        STRINGLN You didn't win, sorry.
    END_IF
END_FUNCTION

VAR $LOTTERY_NUM = 3423
CHECK_LOTTERY_NUMBER()

VAR $LOTTERY_NUM = 2038
CHECK_LOTTERY_NUMBER()

VAR $LOTTERY_NUM = 7823
CHECK_LOTTERY_NUMBER()

STRINGLN   
STRINGLN Lets go! With our billions in cash, lets see if we can buy a ferrari.

FUNCTION CAN_BUY_FERRARI()
    RETURN ((FALSE && TRUE) && ((TRUE || FALSE) && TRUE))
END_FUNCTION

IF (CAN_BUY_FERRARI() == TRUE ) THEN
    STRINGLN We got a ferrari!
ELSE IF (CAN_BUY_FERRARI()==FALSE) THEN
    STRINGLN What's this? We've been scammed! There is no money!
END_IF

STRINGLN   
STRING This is the end of the program.

Example Output (Written by my own pico-ducky)

Hello World!
Wait a minute, is it raining?
It is raining! That sucks.
At the very least the sky isn't RAINING and HAILING right?
See, it is only raining!
But if it's not RAINING and NOT HAILING, then is it snowing?
Aw man, it is snowing!
If we count to 5, then I'm sure it will all go away.
1
2
3
4
5
It didn't work! We need to only count even integers! Let's try again.
2
4
Now its a clear and sunny day! Lets see if we can win the lottery.

Your number is 3423
You didn't win, sorry.
Your number is 2038
You didn't win, sorry.
Your number is 7823
Congrats! You got the winning number!

Lets go! With our billions in cash, lets see if we can buy a ferrari.
What's this? We've been scammed! There is no money!

This is the end of the program.

Implementation Details

DuckyScript is luckily a pretty basic language so the interpreter is not too complicated. Here is the general outline of the algorithm for interpretation:

  1. Load file into memory.
  2. For each line in the file:
    1. Split line into "tokens" and catch any syntax errors or obvious errors. This job is given to DuckyParser.
    2. If the line is a control flow operator (DuckyInterpreter):
      1. If its a if statement and the condition is true, run the code then skip any ELSE IF's. If it's false, skip until either a ELSE IF appears or END_IF appears. Check each ELSE IF to see if it should be run, and if it should, run the code then go to END_IF.
      2. If its a function, record function location and skip it.
      3. If its a while loop and the condition is true, run code, return, and re-evaluate condition. If it's false, skip the while loop.
    3. If the line is a regular command (DuckyInterpreter):
      1. If it's a call to function, push current location to stack, lookup function location, run function, then come back with a return value.
      2. If it's a variable assignment, evaluate its value (it might be an expression) and save it for later.
      3. If it's a constant assignment, throw an error if it's already been saved. Otherwise, save it and don't interpret it yet.
      4. STRING(LN) -> Type it out
      5. DELAY -> Delay
      6. If it's a keystroke, then pass it over to the Keyboard HID class (DuckyKeyboard).

Drawbacks

DuckyScript Ambiguity

I don't own a Ducky myself, so I had to take some creative liberties in deciding how certain things (that aren't mentioned in the documentation) worked. My approach was to make the language probably more flexible than DuckyScript itself so I wouldn't miss anything.

Performance

The speed at which the interpreter runs should be as fast as the original duckyinpython.py even with the included control flow operations.

The real bottleneck of running this interpreter is memory. Compared to the original duckyinpython.py, this program's base memory usage is around 10-20kb more than the original (numbers may be off since it's hard to measure memory usage). Considering that the Pico only has about 220k left after CircuitPython is installed makes this not negligible. From my testing, this can run around ~800-1000 lines of DuckyScript with no nested structures before running out of memory.

Recursion in python makes the memory issue worse. Nested IF, WHILE, and FUNCTION statements all rely on recursion, so using any of these inside another IF, WHILE, FUNCTION uses way more memory. It is for this reason that using && and || is heavily encouraged to not use as many nested structures.

Issues with CircuitPython

Circuit python has a fixed amount of memory reserved for the stack, 1.5kb. While you should in theory be able go around 30-40 levels deep in IFs, WHILEs, and FUNCTIONs without running out memory, CircuitPython crashes at around 3-4 levels deep with pystack exhausted.

The fix for this would be to manually compile CircuitPython and change CIRCUITPY_PYSTACK_SIZE to a bigger size in the code as defined in https://github.com/adafruit/circuitpython/issues/3742 . Alternatively, you could just avoid using nested IF statements that don't go deeper than 3 levels.

Final Remarks

This interpreter is in no way perfect, so fixes or optimizations would be greatly appreciated. I hope that by passing this milestone more attention can be put in developing the advanced functions in DuckyScript 3.0 rather than worrying about the language itself.

Hope this helps #117

Happy New Years!

dbisu commented 1 year ago

I am not likely to merge this commit. While I appreciate the effort, this doesn't match my coding preferences and style.

I have no issues if you want to create something similar using the Pico C++ interface. I created this tool because it was useful to me and others have found it useful also. If it would be more useful to you to use C++, then by all means go for it.