local ansicolors = require('ansicolors') local cmds = require('commands') local getopt = require('getopt') local lib14a = require('read14a') local utils = require('utils') -- globals copyright = '' author = 'Dmitry Malenok' version = 'v1.0.0' desc = [[ The script provides functionality for writing Mifare Ultralight Ultra/UL-5 tags. ]] example = [[ -- restpre (write) dump to tag ]]..ansicolors.yellow..[[script run hf_mfu_ultra -f hf-mfu-3476FF1514D866-dump.bin -k ffffffff -r]]..ansicolors.reset..[[ -- wipe tag (]]..ansicolors.red..[[Do not use it with UL-5!]]..ansicolors.reset..[[) ]]..ansicolors.yellow..[[script run hf_mfu_ultra -k 1d237f76 -w ]]..ansicolors.reset..[[ ]] usage = [[ script run hf_mfu_ultra -h -f -k -w -r ]] arguments = [[ -h this help -f filename for the datadump to read (bin) -k pwd to use with the restore and wipe operations -r restore a binary dump to tag -w wipe tag (]]..ansicolors.red..[[Do not use it with UL-5!]]..ansicolors.reset..[[) ]] local _password = nil local _defaultPassword = 'FFFFFFFF' local _dumpstart = 0x38*2 + 1 --- --- Handles errors local function error(err) print(ansicolors.red.."ERROR:"..ansicolors.reset, err) core.clearCommandBuffer() return nil, err end --- -- sets the global password variable local function setPassword(password) if password == nil or #password == 0 then _password = nil; elseif #password ~= 8 then return false, 'Password must be 4 hex bytes' else _password = password end return true, 'Sets' end --- Parses response data local function parseResponse(rawResponse) local resp = Command.parse(rawResponse) local len = tonumber(resp.arg1) * 2 return string.sub(tostring(resp.data), 0, len); end --- --- Sends raw data to PM3 and returns raw response if any local function sendRaw(rawdata, options) local flags = lib14a.ISO14A_COMMAND.ISO14A_RAW if options.keep_signal then flags = flags + lib14a.ISO14A_COMMAND.ISO14A_NO_DISCONNECT end if options.connect then flags = flags + lib14a.ISO14A_COMMAND.ISO14A_CONNECT end if options.no_select then flags = flags + lib14a.ISO14A_COMMAND.ISO14A_NO_SELECT end if options.append_crc then flags = flags + lib14a.ISO14A_COMMAND.ISO14A_APPEND_CRC end local arg2 = #rawdata / 2 if options.bits7 then arg2 = arg2 | tonumber(bit32.lshift(7, 16)) end local command = Command:newMIX{cmd = cmds.CMD_HF_ISO14443A_READER, arg1 = flags, arg2 = arg2, data = rawdata} return command:sendMIX(options.ignore_response) end --- --- Sends raw data to PM3 and returns parsed response local function sendWithResponse(payload, options) local opts; if options then opts = options else opts = {ignore_response = false, keep_signal = true, append_crc = true} end local rawResp, err = sendRaw(payload, opts) if err then return err end return parseResponse(rawResp) end --- -- Authenticates if password is provided local function authenticate(password) if password then local resp, err = sendWithResponse('1B'..password) if err then return err end -- looking for 2 bytes (4 symbols) of PACK and 2 bytes (4 symbols) of CRC if not resp or #resp ~=8 then return false, 'It seems that password is wrong' end return true end return true end -- -- selects tag and authenticates if password is provided local function connect() core.clearCommandBuffer() local info, err = lib14a.read(true, true) if err then lib14a.disconnect() return false, err end core.clearCommandBuffer() return authenticate(_password) end -- -- reconnects and selects tag again local function reconnect() lib14a.disconnect() utils.Sleep(1) local info, err = connect() if not info then return false, "Unable to select tag: "..err end return true end -- -- checks tag version local function checkTagVersion() local resp, err = sendWithResponse('60'); if err or resp == nil then return false, err end if string.find(resp, '0034210101000E03') ~= 1 then return false, 'Wrong tag version: '..string.sub(resp,1,-5) end return true end -- -- sends magic wakeup command local function magicWakeup() io.write('Sending magic wakeup command...') local resp, err = sendRaw('5000', {ignore_response = false, append_crc = true}) if err or resp == nil then return false, "Unable to send first magic wakeup command: "..err end resp, err = sendRaw('40', {connect = true, no_select = true, ignore_response = false, keep_signal = true, append_crc = false, bits7 = true}) if err or resp == nil then return false, "Unable to send first magic wakeup command: "..err end resp, err = sendRaw('43', {ignore_response = false, keep_signal = true, append_crc = false}) if err or resp == nil then return false, "Unable to send second magic wakeup command: "..err end print(ansicolors.green..'done'..ansicolors.reset..'.') return true end -- -- Writes dump to tag local function writeDump(filename) print(string.rep('--',20)) local info, err = connect() if not info then return false, "Unable to select tag: "..err end info, err = checkTagVersion() if not info then return info, err end -- load dump from file if not filename then return false, 'No dump filename provided' end io.write('Loading dump from file '..filename..'...') local dump dump, err = utils.ReadDumpFile(filename) if not dump then return false, err end if #dump ~= _dumpstart - 1 + 0xa4*2 then return false, 'Invalid dump file' end print(ansicolors.green..'done'..ansicolors.reset..'.') local resp for i = 3, 0x23 do local blockStart = i * 8 + _dumpstart local block = string.sub(dump, blockStart, blockStart + 7) local cblock = string.format('%02x',i) io.write('Writing block 0x'..cblock..'...') resp, err = sendWithResponse('A2'..cblock..block) if err ~= nil then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') end -- set password io.write('Setting password and pack ') info, err = reconnect() if not info then return false, err end local passwordStart = 0x27*8 + _dumpstart local password = string.sub(dump, passwordStart, passwordStart + 7) local packBlock = string.sub(dump, passwordStart+8, passwordStart + 15) io.write('(password: '..password..') (pack block: '..packBlock..')...') resp, err = sendWithResponse('A227'..password) if err ~= nil then return false, err end resp, err = sendWithResponse('A228'..packBlock) if err ~= nil then return false, err end if not setPassword(password) then return false, 'Unable to set password' end info, err = reconnect() if not info then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') -- set configs and locks for i = 0x24, 0x26 do local blockStart = i * 8 + _dumpstart local block = string.sub(dump, blockStart, blockStart + 7) local cblock = string.format('%02x',i) io.write('Writing block 0x'..cblock..'...') resp, err = sendWithResponse('A2'..cblock..block) if err ~= nil then return false, err end info, err = reconnect() if not info then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') end info, err = magicWakeup() if not info then return false, err end -- set uid and locks for i = 0x2, 0x0, -1 do local blockStart = i * 8 + _dumpstart local block = string.sub(dump, blockStart, blockStart + 7) local cblock = string.format('%02x',i) io.write('Writing block 0x'..cblock..'...') resp, err = sendWithResponse('A2'..cblock..block, {connect = i == 0x2, ignore_response = false, keep_signal = i ~= 0, append_crc = true}) if err ~= nil then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') end print(ansicolors.green..'The dump has been written to the tag.'..ansicolors.reset) return true end -- -- Wipes tag local function wipe() print(string.rep('--',20)) print('Wiping tag') local info, err = connect() if not info then return false, "Unable to select tag: "..err end info, err = checkTagVersion() if not info then return info, err end local resp -- clear lock bytes on page 0x02 resp, err = sendWithResponse('3000') if err or resp == nil then return false, err end local currentLowLockPage = string.sub(resp,17,24) if(string.sub(currentLowLockPage,5,8) ~= '0000') then info, err = magicWakeup() if not info then return false, err end local newLowLockPage = string.sub(currentLowLockPage,1,4)..'0000' io.write('Clearing lock bytes on page 0x02...') resp, err = sendWithResponse('A202'..newLowLockPage, {connect = true, ignore_response = false, keep_signal = true, append_crc = true}) if err ~= nil then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') end -- clear lock bytes on page 0x24 io.write('Clearing lock bytes on page 0x24...') info, err = reconnect() if not info then return false, err end resp, err = sendWithResponse('A224000000BD') if err ~= nil then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') -- clear configs io.write('Clearing cfg0 and cfg1...') resp, err = sendWithResponse('A225000000FF') if err ~= nil then return false, err end resp, err = sendWithResponse('A22600050000') if err ~= nil then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') -- clear password io.write('Reseting password (and pack) to default ('.._defaultPassword..') and 0000...') info, err = reconnect() if not info then return false, err end resp, err = sendWithResponse('A227'.._defaultPassword) if err ~= nil then return false, err end resp, err = sendWithResponse('A22800000000') if err ~= nil then return false, err end if not setPassword(_defaultPassword) then return false, 'Unable to set password' end info, err = reconnect() if not info then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') -- clear other blocks for i = 3, 0x23 do local cblock = string.format('%02x',i) io.write('Clearing block 0x'..cblock..'...') resp, err = sendWithResponse('A2'..cblock..'00000000') if err ~= nil then return false, err end print(ansicolors.green..'done'..ansicolors.reset..'.') end print(ansicolors.green..'The tag has been wiped.'..ansicolors.reset) lib14a.disconnect() return true end -- -- Prints 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 --- -- The main entry point local function main(args) if #args == 0 then return help() end local dumpFilename = nil for opt, value in getopt.getopt(args, 'hf:k:rw') do local res, err res = true if opt == "h" then return help() end if opt == "f" then dumpFilename = value end if opt == 'k' then res, err = setPassword(value) end if opt == 'r' then res, err = writeDump(dumpFilename) end if opt == 'w' then res, err = wipe() end if not res then return error(err) end end end main(args)