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:
YoungJules
2026-05-07 11:19:57 +02:00
parent 2893cf6c9b
commit 52005ec95c
2 changed files with 225 additions and 52 deletions
+67 -8
View File
@@ -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
+158 -44
View File
@@ -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