diff --git a/client/luascripts/hf_legic.lua b/client/luascripts/hf_legic.lua index 495b1a951..cc51cfdf4 100644 --- a/client/luascripts/hf_legic.lua +++ b/client/luascripts/hf_legic.lua @@ -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 diff --git a/client/luascripts/hf_legic_clone.lua b/client/luascripts/hf_legic_clone.lua index c760d6d6a..55673ce2f 100644 --- a/client/luascripts/hf_legic_clone.lua +++ b/client/luascripts/hf_legic_clone.lua @@ -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 ] [-o ] [-c ] [-d] [-s] [-w] ]] arguments = [[ -required : - -i - file to read data from, must be in binary format (*.bin) + required : + -i - 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