mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-05-12 11:18:11 -07:00
Some improvements to a couple lua scripts for Legic. Improved help, made the purpose clearer, added json dump support, better support for blank cards, look for KGH (Kaba Group Header) etc.
This commit is contained in:
@@ -102,6 +102,7 @@ Known issues; needs to be fixed:
|
||||
local utils = require('utils')
|
||||
local getopt = require('getopt')
|
||||
local ansicolors = require('ansicolors')
|
||||
local json = require('dkjson')
|
||||
|
||||
---
|
||||
-- global variables / defines
|
||||
@@ -194,7 +195,7 @@ it's kinda interactive with following commands in three categories:
|
||||
without the need of changing anything - MCD,MSN,MCC will be read from the tag
|
||||
before and applied to the output.
|
||||
|
||||
lf: 'load file' - load a (xored) binary file (*.bin) from the local Filesystem into the 'virtual inTag'
|
||||
lf: 'load file' - load a (xored) binary file (*.bin) or Proxmark JSON dump (*.json) into the 'virtual inTag'
|
||||
sf: 'save file' - saves the 'virtual inTag' to the local Filesystem as eml and bin (xored with Tag-MCC)
|
||||
xf: 'xor file' - saves the 'virtual inTag' to the local Filesystem (xored with chosen MCC - use '00' for plain values)
|
||||
|
||||
@@ -351,6 +352,11 @@ end
|
||||
function file_check(file_name)
|
||||
if not file_name then return false, "" end
|
||||
|
||||
local home = os.getenv("HOME") or os.getenv("USERPROFILE")
|
||||
if home and (file_name == "~" or file_name:sub(1, 2) == "~/" or file_name:sub(1, 2) == "~\\") then
|
||||
file_name = home .. file_name:sub(2)
|
||||
end
|
||||
|
||||
local arr = split(file_name, ".")
|
||||
local ext = table.remove(arr)
|
||||
local name = join(arr, '.')
|
||||
@@ -395,20 +401,72 @@ function getInputBytes(infile)
|
||||
local line
|
||||
local bytes = {}
|
||||
|
||||
local home = os.getenv("HOME") or os.getenv("USERPROFILE")
|
||||
if home and (infile == "~" or infile:sub(1, 2) == "~/" or infile:sub(1, 2) == "~\\") then
|
||||
infile = home .. infile:sub(2)
|
||||
end
|
||||
|
||||
local arr = split(infile, ".")
|
||||
local ext = table.remove(arr)
|
||||
local name = join(arr, '.')
|
||||
local path = core.search_file(name, "."..ext)
|
||||
if (path == nil) then oops("failed to read from file ".. infile); return false; end
|
||||
|
||||
local fhi,err = io.open(path,"rb")
|
||||
if err then oops("failed to read from file ".. path); return false; end
|
||||
if ext:lower() == "json" then
|
||||
local fhi, err = io.open(path, "r")
|
||||
if err then oops("failed to read from file ".. path); return false; end
|
||||
|
||||
file_data = fhi:read("*a");
|
||||
for i = 1, #file_data do
|
||||
bytes[i] = string.format("%x",file_data:byte(i))
|
||||
local file_data = fhi:read("*a")
|
||||
fhi:close()
|
||||
|
||||
local obj, pos, jerr = json.decode(file_data, 1, nil)
|
||||
if jerr then
|
||||
oops("failed to parse json dump ".. path ..": ".. jerr)
|
||||
return false
|
||||
end
|
||||
|
||||
if type(obj) ~= "table" or type(obj.blocks) ~= "table" then
|
||||
oops("json dump does not contain a blocks table: ".. path)
|
||||
return false
|
||||
end
|
||||
|
||||
local keys = {}
|
||||
for k in pairs(obj.blocks) do
|
||||
local n = tonumber(k)
|
||||
if n ~= nil then
|
||||
keys[#keys + 1] = n
|
||||
end
|
||||
end
|
||||
table.sort(keys)
|
||||
|
||||
for _, key in ipairs(keys) do
|
||||
local block = obj.blocks[tostring(key)] or obj.blocks[key]
|
||||
if type(block) ~= "string" then
|
||||
oops("block ".. key .." is missing or invalid in json dump ".. path)
|
||||
return false
|
||||
end
|
||||
|
||||
block = block:gsub("%s", "")
|
||||
if (#block % 2) ~= 0 then
|
||||
oops("block ".. key .." has an odd number of hex digits in json dump ".. path)
|
||||
return false
|
||||
end
|
||||
|
||||
for c in block:gmatch("..") do
|
||||
bytes[#bytes + 1] = c:lower()
|
||||
end
|
||||
end
|
||||
else
|
||||
local fhi,err = io.open(path,"rb")
|
||||
if err then oops("failed to read from file ".. path); return false; end
|
||||
|
||||
file_data = fhi:read("*a");
|
||||
for i = 1, #file_data do
|
||||
bytes[i] = string.format("%x",file_data:byte(i))
|
||||
end
|
||||
fhi:close()
|
||||
end
|
||||
fhi:close()
|
||||
|
||||
if (bytes[7]=='00') then return false end
|
||||
print(#bytes .. " bytes from "..path.." loaded")
|
||||
return bytes
|
||||
@@ -668,7 +726,7 @@ local function readFile(filename)
|
||||
end
|
||||
|
||||
bytes = getInputBytes(path)
|
||||
if bytes == false then return oops('couldnt get input bytes') end
|
||||
if bytes == false then return oops('could not get input bytes') end
|
||||
|
||||
-- make plain bytes
|
||||
bytes = xorBytes(bytes,bytes[5])
|
||||
@@ -2835,6 +2893,7 @@ function main(args)
|
||||
-- set init colors/switch (can be toggled with 'tac' => 'toggle ansicolors')
|
||||
load_colors(colored_output)
|
||||
if (#args == 0 ) then modifyMode() end
|
||||
if args and args:match('^%-%-help%s*$') then return help() end
|
||||
--- variables
|
||||
local inTAG, backupTAG, outTAG, outfile, interactive, crc
|
||||
local ofs=false
|
||||
|
||||
@@ -2,23 +2,23 @@ local utils = require('utils')
|
||||
local cmds = require('commands')
|
||||
local getopt = require('getopt')
|
||||
local ansicolors = require('ansicolors')
|
||||
local json = require('dkjson')
|
||||
|
||||
--[[
|
||||
script to create a clone-dump with new crc
|
||||
script to rewrite a LEGIC Prime dump for a different target tag
|
||||
Author: mosci
|
||||
my Fork: https://github.com/icsom/proxmark3.git
|
||||
|
||||
1. read tag-dump, xor byte 22..end with byte 0x05 of the inputfile
|
||||
2. write to outfile
|
||||
3. set byte 0x05 to newcrc
|
||||
4. until byte 0x21 plain like in inputfile
|
||||
5. from 0x22..end xored with newcrc
|
||||
6. calculate new crc on each segment (needs to know the new MCD & MSN0..2)
|
||||
1. read tag-dump and load the raw bytes
|
||||
2. if the dump contains parseable segments, recalculate the dependent CRCs
|
||||
3. update the target card identity bytes from the new tag
|
||||
4. xor the writable image with the target MCC
|
||||
5. write the resulting dump to disk and/or restore it to the target tag
|
||||
|
||||
simplest usage:
|
||||
Dump a legic tag with 'hf legic dump'
|
||||
place your 'empty' tag on the reader and run
|
||||
'script run hf_legic_clone -i orig.bin -w'
|
||||
simplest usage:
|
||||
Dump a legic tag with 'hf legic dump'
|
||||
place the target tag on the reader and run
|
||||
'script run hf_legic_clone -i orig.bin -w'
|
||||
|
||||
you will see some output like:
|
||||
|
||||
@@ -52,8 +52,8 @@ local ansicolors = require('ansicolors')
|
||||
#db# write successful
|
||||
proxmark3>
|
||||
|
||||
the default value (number of bytes to write) is calculated over all valid segments and should be ok - just hit enter, wait until write has finished
|
||||
and your clone should be ready (except there has to be a additional KGH-CRC to be calculated - which credentials are unknown until yet)
|
||||
when the dump has parseable segments, the script recalculates the dependent CRCs
|
||||
for raw/blank images, it simply restores the image content to the target tag
|
||||
|
||||
the '-w' switch will only work with my fork - it needs the binary legic_crc8 which is not part of the proxmark3-master-branch
|
||||
also the ability to write DCF is not possible with the proxmark3-master-branch
|
||||
@@ -91,19 +91,21 @@ copyright = ''
|
||||
author = 'Mosci'
|
||||
version = 'v1.0.2'
|
||||
desc = [[
|
||||
This is a script which creates a clone-dump of a dump from a LEGIC Prime Tag (MIM256 or MIM1024)
|
||||
Create a dump by running `hf legic dump`.
|
||||
This script rewrites a LEGIC Prime dump so it can be written to a different tag (MIM256 or MIM1024).
|
||||
It handles both segmented dumps and raw/blank LEGIC images.
|
||||
Create the source dump by running `hf legic dump`.
|
||||
]]
|
||||
example = [[
|
||||
script run hf_legic_clone -i my_dump.bin -o my_clone.bin -c f8
|
||||
script run hf_legic_clone -i my_dump.json -d -s
|
||||
script run hf_legic_clone -i my_dump.bin -d -s
|
||||
]]
|
||||
usage = [[
|
||||
script run hf_legic_clone [-h] [-i <file>] [-o <file>] [-c <crc>] [-d] [-s] [-w]
|
||||
]]
|
||||
arguments = [[
|
||||
required :
|
||||
-i <input file> - file to read data from, must be in binary format (*.bin)
|
||||
required :
|
||||
-i <input file> - file to read data from, binary (*.bin) or Proxmark JSON (*.json)
|
||||
|
||||
optional :
|
||||
-h - Help text
|
||||
@@ -169,13 +171,25 @@ local function readlegicinfo()
|
||||
end
|
||||
|
||||
-- Check availability of file
|
||||
local function expand_user_path(path)
|
||||
if path == nil then return nil end
|
||||
|
||||
local home = os.getenv("HOME") or os.getenv("USERPROFILE")
|
||||
if home and (path == "~" or path:sub(1, 2) == "~/" or path:sub(1, 2) == "~\\") then
|
||||
return home .. path:sub(2)
|
||||
end
|
||||
|
||||
return path
|
||||
end
|
||||
|
||||
local function file_check(file_name)
|
||||
local exists = io.open(file_name, "r")
|
||||
if not exists then
|
||||
exists = false
|
||||
file_name = expand_user_path(file_name)
|
||||
local exists = io.open(file_name, "r")
|
||||
if not exists then
|
||||
exists = false
|
||||
else
|
||||
exists = true
|
||||
end
|
||||
end
|
||||
return exists
|
||||
end
|
||||
|
||||
@@ -192,6 +206,56 @@ end
|
||||
-- read input-file into array
|
||||
local function getInputBytes(infile)
|
||||
local bytes = {}
|
||||
infile = expand_user_path(infile)
|
||||
local lower = infile:lower()
|
||||
|
||||
if lower:sub(-5) == ".json" then
|
||||
local f = io.open(infile, "r")
|
||||
if f == nil then print("OOps ... failed to read from file ".. infile); return false; end
|
||||
|
||||
local str = f:read("*all")
|
||||
f:close()
|
||||
|
||||
local obj, pos, err = json.decode(str, 1, nil)
|
||||
if err then
|
||||
print("OOps ... failed to parse json dump ".. infile ..": ".. err)
|
||||
return false
|
||||
end
|
||||
|
||||
if type(obj) ~= "table" or type(obj.blocks) ~= "table" then
|
||||
print("OOps ... json dump does not contain a blocks table: ".. infile)
|
||||
return false
|
||||
end
|
||||
|
||||
local keys = {}
|
||||
for k in pairs(obj.blocks) do
|
||||
local n = tonumber(k)
|
||||
if n ~= nil then
|
||||
keys[#keys + 1] = n
|
||||
end
|
||||
end
|
||||
table.sort(keys)
|
||||
|
||||
for _, key in ipairs(keys) do
|
||||
local block = obj.blocks[tostring(key)] or obj.blocks[key]
|
||||
if type(block) ~= "string" then
|
||||
print("OOps ... block ".. key .." is missing or invalid in json dump ".. infile)
|
||||
return false
|
||||
end
|
||||
block = block:gsub("%s", "")
|
||||
if (#block % 2) ~= 0 then
|
||||
print("OOps ... block ".. key .." has an odd number of hex digits in json dump ".. infile)
|
||||
return false
|
||||
end
|
||||
for c in block:gmatch("..") do
|
||||
bytes[#bytes + 1] = c:lower()
|
||||
end
|
||||
end
|
||||
|
||||
print("\nread ".. #bytes .." bytes from "..ansicolors.yellow..infile..ansicolors.reset)
|
||||
return bytes
|
||||
end
|
||||
|
||||
local f = io.open(infile, "rb")
|
||||
if f == nil then print("OOps ... failed to read from file ".. infile); return false; end
|
||||
|
||||
@@ -251,10 +315,10 @@ local function getSegmentData(bytes, start, index)
|
||||
segment[3] = str_bit_extract(bytes[start + 1], 7, 1)
|
||||
|
||||
-- len = (byte 0)+(bit0-3 of byte 1)
|
||||
segment[4] = str_bit_extract(bytes[start + 1], 0, 3) << 8 + tonumber(bytes[start], 16)
|
||||
segment[4] = (str_bit_extract(bytes[start + 1], 0, 3) << 8) + tonumber(bytes[start], 16)
|
||||
|
||||
-- wrp (write proteted) = byte 2
|
||||
segment[5] = tonumber(bytes[start + 2])
|
||||
segment[5] = tonumber(bytes[start + 2], 16)
|
||||
|
||||
-- wrc (write control) - bit 4-6 of byte 3
|
||||
segment[6] = str_bit_extract(bytes[start + 3], 4, 3)
|
||||
@@ -273,6 +337,19 @@ local function getSegmentData(bytes, start, index)
|
||||
return segment
|
||||
end
|
||||
|
||||
local function segmentLooksValid(bytes, segment, start)
|
||||
if segment == nil or segment[4] == nil or segment[4] < 5 then
|
||||
return false
|
||||
end
|
||||
|
||||
return (start + segment[4] - 1) <= #bytes
|
||||
end
|
||||
|
||||
local function hasParseableSegments(bytes)
|
||||
local seg = getSegmentData(bytes, 23, 0)
|
||||
return segmentLooksValid(bytes, seg, 23)
|
||||
end
|
||||
|
||||
--- Kaba Group Header
|
||||
-- checks if a segment does have a kghCRC
|
||||
-- returns boolean false if no kgh has being detected or the kghCRC if a kgh was detected
|
||||
@@ -306,6 +383,11 @@ local function getSegmentCrcBytes(bytes)
|
||||
local start = 23
|
||||
local index = 0
|
||||
local crcbytes = {}
|
||||
|
||||
if not hasParseableSegments(bytes) then
|
||||
return nil
|
||||
end
|
||||
|
||||
repeat
|
||||
seg = getSegmentData(bytes, start, index)
|
||||
crcbytes[index] = seg[10]
|
||||
@@ -332,6 +414,19 @@ end
|
||||
-- print segment-data (hf legic info like)
|
||||
local function displaySegments(bytes)
|
||||
|
||||
local function appendByte(out, idx, label)
|
||||
local b = bytes[idx]
|
||||
if b == nil then
|
||||
return nil, oops(label.." is out of range at byte "..idx.." in input dump")
|
||||
end
|
||||
return out .. b .. ' '
|
||||
end
|
||||
|
||||
if not hasParseableSegments(bytes) then
|
||||
print("No parseable LEGIC Prime segments found; treating dump as raw data.")
|
||||
return
|
||||
end
|
||||
|
||||
--display segment header(s)
|
||||
start = 23
|
||||
index = 0
|
||||
@@ -344,8 +439,11 @@ local function displaySegments(bytes)
|
||||
Seg = getSegmentData(bytes, start, index)
|
||||
if Seg == nil then return oops("segment is nil") end
|
||||
|
||||
KGH = CheckKgh(bytes, start, (start + Seg[4]))
|
||||
if not segmentLooksValid(bytes, Seg, start) then
|
||||
return oops("invalid segment length at segment "..Seg[9].." in input dump")
|
||||
end
|
||||
|
||||
KGH = CheckKgh(bytes, start, (start + Seg[4]))
|
||||
printSegment(Seg)
|
||||
|
||||
-- wrc
|
||||
@@ -354,7 +452,9 @@ local function displaySegments(bytes)
|
||||
-- length of wrc = wrc
|
||||
for i = 1, Seg[6] do
|
||||
-- starts at (segment-start + segment-header + segment-crc)-1
|
||||
wrc = wrc..bytes[(start + 4 + 1 + i) - 1]..' '
|
||||
local updated, err = appendByte(wrc, (start + 4 + 1 + i) - 1, "WRC protected area")
|
||||
if not updated then return err end
|
||||
wrc = updated
|
||||
end
|
||||
print(wrc)
|
||||
elseif (Seg[5] > 0) then
|
||||
@@ -362,7 +462,9 @@ local function displaySegments(bytes)
|
||||
-- length of wrp = (wrp-wrc)
|
||||
for i = 1, (Seg[5] - Seg[6]) do
|
||||
-- starts at (segment-start + segment-header + segment-crc + wrc)-1
|
||||
wrp = wrp..bytes[(start + 4 + 1 + Seg[6] + i) - 1]..' '
|
||||
local updated, err = appendByte(wrp, (start + 4 + 1 + Seg[6] + i) - 1, "write protected area")
|
||||
if not updated then return err end
|
||||
wrp = updated
|
||||
end
|
||||
print(wrp)
|
||||
end
|
||||
@@ -372,7 +474,9 @@ local function displaySegments(bytes)
|
||||
--length of payload = segment-len - segment-header - segment-crc - wrp -wrc
|
||||
for i = 1, (Seg[4] - 4 - 1 - Seg[5] - Seg[6]) do
|
||||
-- starts at (segment-start + segment-header + segment-crc + segment-wrp + segemnt-wrc)-1
|
||||
pld = pld..bytes[(start + 4 + 1 + Seg[5] + Seg[6] + i) - 1]..' '
|
||||
local updated, err = appendByte(pld, (start + 4 + 1 + Seg[5] + Seg[6] + i) - 1, "segment payload")
|
||||
if not updated then return err end
|
||||
pld = updated
|
||||
end
|
||||
print(pld)
|
||||
if (KGH) then
|
||||
@@ -414,17 +518,21 @@ local function writeToTag(plainBytes)
|
||||
|
||||
-- calculate new Segment-CRC for each valid segment
|
||||
SegCrcs = getSegmentCrcBytes(plainBytes)
|
||||
for i = 0, (#SegCrcs - 1) do
|
||||
-- SegCrcs[i]-4 = address of first byte of segmentHeader (low byte segment-length)
|
||||
segLen = tonumber(("%1x"):format(bit32.extract("0x"..plainBytes[(SegCrcs[i] - 3)], 0, 3))..("%02x"):format(tonumber(plainBytes[SegCrcs[i] - 4], 16)), 16)
|
||||
segStart = (SegCrcs[i] - 4)
|
||||
segEnd = (SegCrcs[i] - 4 + segLen)
|
||||
KGH = CheckKgh(plainBytes, segStart, segEnd)
|
||||
if (KGH) then
|
||||
print("'Kaba Group Header' detected - re-calculate...")
|
||||
if SegCrcs ~= nil then
|
||||
for i = 0, (#SegCrcs - 1) do
|
||||
-- SegCrcs[i]-4 = address of first byte of segmentHeader (low byte segment-length)
|
||||
segLen = tonumber(("%1x"):format(bit32.extract("0x"..plainBytes[(SegCrcs[i] - 3)], 0, 3))..("%02x"):format(tonumber(plainBytes[SegCrcs[i] - 4], 16)), 16)
|
||||
segStart = (SegCrcs[i] - 4)
|
||||
segEnd = (SegCrcs[i] - 4 + segLen)
|
||||
KGH = CheckKgh(plainBytes, segStart, segEnd)
|
||||
if (KGH) then
|
||||
print("'Kaba Group Header' detected - re-calculate...")
|
||||
end
|
||||
cmd = MCD..MSN0..MSN1..MSN2..plainBytes[SegCrcs[i]-4]..plainBytes[SegCrcs[i]-3]..plainBytes[SegCrcs[i]-2]..plainBytes[SegCrcs[i]-1]
|
||||
plainBytes[SegCrcs[i]] = ("%02x"):format(utils.Crc8Legic(cmd))
|
||||
end
|
||||
cmd = MCD..MSN0..MSN1..MSN2..plainBytes[SegCrcs[i]-4]..plainBytes[SegCrcs[i]-3]..plainBytes[SegCrcs[i]-2]..plainBytes[SegCrcs[i]-1]
|
||||
plainBytes[SegCrcs[i]] = ("%02x"):format(utils.Crc8Legic(cmd))
|
||||
else
|
||||
print("No parseable segments found; restoring raw LEGIC image.")
|
||||
end
|
||||
|
||||
-- apply MCD & MSN to plain data
|
||||
@@ -453,6 +561,8 @@ local function main(args)
|
||||
local bytes = {}
|
||||
local segments = {}
|
||||
|
||||
if args and args:match('^%-%-help%s*$') then return help() end
|
||||
|
||||
-- parse arguments for the script
|
||||
for o, a in getopt.getopt(args, 'hwsdc:i:o:') do
|
||||
-- output file
|
||||
@@ -466,13 +576,13 @@ local function main(args)
|
||||
end
|
||||
-- input file
|
||||
if o == 'i' then
|
||||
infile = a
|
||||
infile = expand_user_path(a)
|
||||
if (file_check(infile) == false) then return oops('input file: '..infile..' not found') end
|
||||
|
||||
bytes = getInputBytes(infile)
|
||||
if (bytes == false) then return oops('could not read file') end
|
||||
oldcrc = bytes[5]
|
||||
ifs = true
|
||||
if (bytes == false) then return oops('couldnt read file') end
|
||||
|
||||
i = i + 1
|
||||
end
|
||||
@@ -514,7 +624,7 @@ local function main(args)
|
||||
res = res .."\ncreated clone_dump from\n\t"..infile.." crc: "..oldcrc.."\ndump_file:"
|
||||
res = res .."\n\t"..outfile.." crc: "..string.sub(newcrc, -2)
|
||||
res = res .."\nyou may load the new file with:"
|
||||
res = res ..ansicolors.yellow.."hf legic eload -f "..outfile..ansicolors.reset
|
||||
res = res ..ansicolors.yellow.."hf legic restore -f "..outfile..ansicolors.reset
|
||||
res = res .."\n\nif you don't write to tag immediately ('-w' switch) you will need to recalculate each segmentCRC"
|
||||
res = res .."\nafter writing this dump to a tag!"
|
||||
res = res .."\n\na segmentCRC gets calculated over MCD,MSN0..3, Segment-Header0..3"
|
||||
@@ -527,10 +637,14 @@ local function main(args)
|
||||
end
|
||||
else
|
||||
if (ss) then
|
||||
-- show why the output-file was not written
|
||||
print("\nnew file not written - some arguments are missing ..")
|
||||
print("output file: ".. (ofs and outfile or "not given"))
|
||||
print("new crc: ".. (ncs and newcrc or "not given"))
|
||||
if (ofs or ncs) then
|
||||
-- show why the output-file was not written
|
||||
print("\nnew file not written - some arguments are missing ..")
|
||||
print("output file: ".. (ofs and outfile or "not given"))
|
||||
print("new crc: ".. (ncs and newcrc or "not given"))
|
||||
else
|
||||
print("\ndisplay-only mode - no output file or target CRC requested")
|
||||
end
|
||||
end
|
||||
end
|
||||
-- write to tag
|
||||
|
||||
Reference in New Issue
Block a user