-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathducky_to_hid.py
362 lines (315 loc) · 12.8 KB
/
ducky_to_hid.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
'''
This script converts ducky script output from
'ducky_script_parser.DuckyScriptConverter'
and produces a valid c array containing all the HID_keys
'''
from abc import ABC, abstractmethod
from typing import List, Set
import string
class DuckyScriptConverter(object):
'''
Use this class to parse DuckyScript.
It implements chain of responsibility pattern to match each type of line
that can be found in the DuckyScript language
'''
def __init__(self):
self.first_parser = WindowsConverter(set(['GUI']))
# Create chain of responsibility
# StringConverter MUST be last!
self.first_parser.set_next(MenuConverter(set(['MENU', 'APP'])))\
.set_next(ShiftConverter(set(['SHIFT'])))\
.set_next(AltConverter(set(['ALT'])))\
.set_next(ControlConverter(set(['CONTROL', 'CTRL'])))\
.set_next(ArrorConverter(set(['DOWN', 'LEFT', 'RIGHT',
'UP'])))\
.set_next(ExtendedConverter(
set(ExtendedConverter.allowed)))\
.set_next(DelayConverter(set('DELAY')))\
.set_next(StringConverter(set(
[x for x in string.printable])))
def convert(self, lines: List[str]) -> str:
result = ['HID_ENTER'] # LaFortuna misses first key for some reason
for line in lines:
if not line:
continue
parsed = self.first_parser.handle(line)
result.append(parsed)
# Generate c code here instead of printing
return ', '.join(result)
class LineConverter(ABC):
def __init__(self, line_start: Set[str]):
self.line_startings = line_start
self.next_parser = None
def set_next(self, parser):
self.next_parser = parser
return self.next_parser
def handle(self, line: str) -> str:
if self.__should_parse(line):
parsed = self._convert(line)
return parsed
else:
if self.next_parser is not None:
return self.next_parser.handle(line)
raise Exception(f'No available converter found to convert: {line}')
@abstractmethod
def _convert(self, line) -> str:
'''
This method parses the line and returns a string which has to be send
from the keyboard device and ready to be converted to HID values.
'''
pass
def __should_parse(self, line: str) -> bool:
return any(
[line.startswith(start) for start in self.line_startings]
)
class StringConverter(LineConverter):
def _convert(self, line: str) -> str:
'''
This method creates parses plain strings
For each character it converts it to its HID value
'''
result = []
for char in line:
if char.isdigit():
result.append('HID_' + char)
elif char.isalpha() and char.islower():
result.append('HID_' + char.upper())
elif char.isalpha() and char.isupper():
result.append('HID_CAPS_LOCK, ' + 'HID_' + char
+ ', HID_CAPS_LOCK')
else:
result.append(self.convert_special(char))
return ', '.join(result)
def convert_special(self, char: str) -> str:
if len(char) != 1:
raise ValueError(
f'This method converts single chars only, got {char}')
result = ''
if char == '"':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, '\
+ 'HID_SINGLEQUOTE'
elif char == "'":
result = 'HID_SINGLEQUOTE'
elif char == '(':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, ' \
+ 'HID_9'
elif char == ')':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, ' \
+ 'HID_0'
elif char == '*':
result = 'HID_KEYPAD_MULTIPLY'
elif char == '+':
result = 'HID_KEYPAD_PLUS'
elif char == '-':
result = 'HID_KEYPAD_MINUS'
elif char == '/':
result = 'HID_KEYPAD_DIVIDE'
elif char == ',':
result = 'HID_COMMA'
elif char == '.':
result = 'HID_DOT'
elif char == ';':
result = 'HID_SEMICOLON'
elif char == ':':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, ' \
+ 'HID_SEMICOLON'
elif char == '<':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, ' \
+ 'HID_COMMA'
elif char == '>':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, ' \
+ 'HID_DOT'
elif char == '=':
result = 'HID_KEYPAD_EQUAL'
elif char == '?':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, ' \
+ 'HID_SLASH'
elif char == '@':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, ' \
+ 'HID_2'
elif char == '[':
result = 'HID_LEFT_SQUARE_BRACKET'
elif char == ']':
result = 'HID_RIGHT_SQUARE_BRACKET'
elif char == ' ':
result = 'HID_SPACEBAR'
elif char == '!':
result = 'ESCAPE_KEY_START + 1, ' + 'HID_MODIFIER_LEFT_SHIFT, ' \
+ 'HID_1'
# TODO: implemente all string.printable
else:
raise ValueError(f'Char not implemented as HID: {char}')
return result
class WindowsConverter(LineConverter):
def _convert(self, line: str) -> str:
'''
This method converts the "GUI" key press to HID_VALUE
If another key is pressed with it, the GUI button is passed as modifier
'''
splitted = line.split()
if len(splitted) == 1:
# just keyword
result = 'HID_MODIFIER_LEFT_GUI'
elif len(splitted) == 2:
# keyword and char
if len(splitted[1]) != 1:
raise Exception(f'GUI button can be pressed with a single char,\
got {line}')
# splitted[1].upper() may not be the best, but should work for now
result = 'ESCAPE_KEY_START + 1, HID_MODIFIER_LEFT_GUI, '\
+ 'HID_' + splitted[1].upper()
else:
# disallow GUI and multiple buttons
raise Exception(
f'DuckyScript allows GUI button and maximum 1 char: {line}')
return result
class MenuConverter(LineConverter):
def _convert(self, line: str) -> str:
'''
This method parses "APP" and "MENU" directives.
Returns a "SHIFT-F10" key combo
'''
result = 'ESCAPE_KEY_START + 1, HID_MODIFIER_LEFT_SHIFT, HID_F10'
return result
class ShiftConverter(LineConverter):
allowed = ['DELETE', 'HOME', 'INSERT', 'PAGEUP', 'PAGEDOWN',
'WINDOWS', 'GUI', 'UP', 'DOWN', 'LEFT',
'RIGHT', 'TAB']
def _convert(self, line: str) -> str:
'''
This method parses "SHIFT" directive.
Capital letters are not written using shift but with caps lock.
Shift is used for special combos - shift arrow, shift delete, etc...
'''
splitted = line.split()
if len(splitted) == 1:
# only shift
result = 'HID_MODIFIER_LEFT_SHIFT'
return result
elif len(splitted) == 2:
# shift + special key
# check if key is in allowed
if not (splitted[1] in self.allowed):
raise Exception(
f'"SHIFT" is allowed only in combination with: \
{self.allowed}')
result = 'ESCAPE_KEY_START + 1, HID_MODIFIER_LEFT_SHIFT, '\
+ 'HID_' + splitted[1]
return result # return checked line
else:
raise Exception(
f'"SHIFT" can be used with at most 1 argument, got: {line}')
class AltConverter(LineConverter):
allowed = ['END', 'ESCAPE', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6',
'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'SPACE', 'TAB']
def _convert(self, line: str) -> str:
'''
This method parses "ALT" directive.
A single optional parameter is allowed
'''
splitted = line.split()
if len(splitted) == 1:
# only alt
result = 'HID_MODIFIER_LEFT_ALT'
return result
elif len(splitted) == 2:
# alt + special key or single char
# if key is not in allowed, and (is not a letter, or has len > 1
if splitted[1] not in self.allowed and\
(len(splitted[1]) != 1 or not splitted[1].isalpha()):
raise Exception(
f'"ALT" is allowed only in combination with: \
{self.allowed} and any single chars')
# quick fix space
if splitted[1] == 'SPACE':
splitted[1] = 'SPACEBAR'
result = 'ESCAPE_KEY_START + 1, HID_MODIFIER_LEFT_ALT, '\
+ 'HID_' + splitted[1]
return result # return checked line
else:
raise Exception(
f'"ALT" can be used with at most 1 argument, got: {line}')
class ControlConverter(LineConverter):
allowed = ['ENTER', 'BREAK', 'ESC', 'ESCAPE', 'F1', 'F2', 'F3', 'F4',
'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'PAUSE']
allowed_modifiers = ['ALT', 'SHIFT']
def _convert(self, line: str) -> str:
'''
This method parses "Control" directive.
A single optional parameter is allowed
'''
raise Exception(f'Not implemented yet: {line}')
splitted = line.split()
if len(splitted) == 2:
# control + special key or single char
# if key is not in allowed, and (is not a letter, or has len > 1)
if splitted[1] not in self.allowed and\
(len(splitted[1]) != 1 or not splitted[1].isalpha()):
raise Exception(
f'"CONTROL" is allowed only in combination with: \
{self.allowed} and any single chars, got: {line}')
# result = 'CONTROL ' + splitted[1]
else:
raise Exception(
f'"CONTROL" can be used with at most 1 argument, got: {line}')
# check for 'CONTROL-MODIFIER' combo
control_combo = splitted[0].split('-')
if len(control_combo) > 1:
# check if second key is allowed
if control_combo[1] not in self.allowed_modifiers:
raise Exception(f'You can use control with another modifier\
from the list: {self.allowed_modifiers},\
got: {line}')
return line # return checked line
class ArrorConverter(LineConverter):
def _convert(self, line: str) -> str:
'''
This method parses arrow directives.
Trims the "ARROR" suffix
'''
splitted = line.split('ARROW')
if len(splitted) != 2 and\
(splitted[0] not in {'LEFT', 'RIGHT', 'UP', 'DOWN'}):
raise Exception(f'Invalid input: {line}. Allowed arrow inputs\
are (UP|DOWN|LEFT|RIGHT)[ARROW]')
result = 'HID_' + splitted[0]
return result
class ExtendedConverter(LineConverter):
allowed = ['SPACE', 'INSERT', 'ENTER', 'CAPSLOCK',
'DELETE', 'END', 'HOME', 'INSERT', 'ESCAPE']
def _convert(self, line: str) -> str:
'''
This method handles some additional special keys that are not typeable.
'''
if line in self.allowed: # if line is only 1 keyword
# quickfix space
if line is 'SPACE':
line = 'SPACEBAR'
return 'HID_' + line
else:
raise Exception(f'One keyword per statement, got: "{line}"')
class DelayConverter(LineConverter):
def _convert(self, line: str) -> str:
'''
This method parses the "DELAY" directive.
Sleeping is done in C code so just check everything is okay here.
Directive format is: DELAY <int>
'''
splitted = line.split()
if len(splitted) != 2 or not self.try_parse(splitted[1], int)\
or int(splitted[1]) < 0:
raise ValueError('DELAY format is: DELAY <positive int>')
# C code support delays multiple of 100(ms) and up to 8bit multiples
# This means max delay supported currently is 100 * (2**8)
delay = int(splitted[1])
delay //= 100 # 100 is the current single delay
if delay > 2**8:
delay = 2**8 - 1 # max sleep is 2**8 - 1
result = f'SLEEP_KEY, {delay}'
return result
def try_parse(self, val: str, t: type) -> bool:
try:
t(val)
return True
except ValueError:
return False