diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5e26a02..35bf0769e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac - Fixed `hf mf fchk --mem` to actually use flash dict (@doegox) - Fixed `make install` on OSX thanks DaveItsLong (@doegox) - Added new standalone mode `HF_ST25_TEAROFF` to store/restore ST25TB tags with tearoff for counters (@seclabz) +- Added `hf_mfu_ultra.lua` script enables restoring dump to ULTRA/UL-5 tags and clearing previously written ULTRA tags (@mak-42) ## [Blue Ice.4.20142][2025-03-25] - Added `des_talk.py` script for easier MIFARE DESFire handling (@trigat) diff --git a/client/luascripts/hf_mfu_ultra.lua b/client/luascripts/hf_mfu_ultra.lua new file mode 100644 index 000000000..f96becad8 --- /dev/null +++ b/client/luascripts/hf_mfu_ultra.lua @@ -0,0 +1,357 @@ +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) diff --git a/doc/magic_cards_notes.md b/doc/magic_cards_notes.md index 0ef15de1b..1b43873e5 100644 --- a/doc/magic_cards_notes.md +++ b/doc/magic_cards_notes.md @@ -2356,6 +2356,36 @@ After communication to iKey LLC (importer of those tags to Russian market), new [+] ATS: 85 00 00 A0 00 00 0A 3C 00 04 03 01 01 00 0E 03 [ C8 1D ] ``` +### Magic commands + +^[Top](#top) + +Use the script `hf_mfu_ultra.lua` to restore (write) dump to tag or clear previously written tag. + +Usage: +1. Restore dump to tag: + ``` + script run hf_mfu_ultra -f -k -r + ``` +2. Wipe tag (use it to prepare tag for restoring another dump): + ``` + script run hf_mfu_ultra -k -w + ``` +3. Show help: + ``` + script run hf_mfu_ultra -h + ``` + +Examples: +1. Restore dump to tag: + ``` + script run hf_mfu_ultra -f hf-mfu-3476FF1514D866-dump.bin -k ffffffff -r + ``` +2. Wipe tag: + ``` + script run hf_mfu_ultra -k 1d237f76 -w + ``` + ## UL-5 ^[Top](#top) @@ -2389,6 +2419,28 @@ The manufacturer confirmed unpersonalized tags could be identified by first 2 by * `AA 55...` +### Magic commands + +^[Top](#top) + +Use the script `hf_mfu_ultra.lua` to restore (write) dump to tag. + +Usage: +1. Restore dump to tag: + ``` + script run hf_mfu_ultra -f -k -r + ``` +3. Show help: + ``` + script run hf_mfu_ultra -h + ``` + +Examples: +1. Restore dump to tag: + ``` + script run hf_mfu_ultra -f hf-mfu-3476FF1514D866-dump.bin -k ffffffff -r + ``` + ## UL, other chips ** TODO **