local utils = require('utils') local cmds = require('commands') local getopt = require('getopt') local ansicolors = require('ansicolors') --[[ script to create a clone-dump with new crc 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) simplest usage: Dump a legic tag with 'hf legic dump' place your 'empty' tag on the reader and run 'script run legic_clone -i orig.bin -w' you will see some output like: read 1024 bytes from orig.bin place your empty tag onto the PM3 to read and display the MCD & MSN0..2 the values will be shown below confirm when ready [y/n] ?y 0b ad c0 de <- !! here you'll see the MCD & MSN of your empty tag, which has to be typed in manually as seen below !! type in MCD as 2-digit value - e.g.: 00 (default: 79 ) > 0b type in MSN0 as 2-digit value - e.g.: 01 (default: 28 ) > ad type in MSN1 as 2-digit value - e.g.: 02 (default: d1 ) > c0 type in MSN2 as 2-digit value - e.g.: 03 (default: 43 ) > de MCD:0b, MSN:ad c0 de, MCC:79 <- this crc is calculated from the MCD & MSN and must match the one on yout empty tag wrote 1024 bytes to myLegicClone.hex enter number of bytes to write? (default: 86 ) loaded 1024 samples #db# setting up legic card #db# MIM 256 card found, writing 0x00 - 0x01 ... #db# write successful ... #db# setting up legic card #db# MIM 256 card found, writing 0x56 - 0x01 ... #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) 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 but creating dumpfile-clone files will be possible (without valid segment-crc - this has to done manually with) (example) Legic-Prime Layout with 'Kaba Group Header' +----+----+----+----+----+----+----+----+ 0x00|MCD |MSN0|MSN1|MSN2|MCC | 60 | ea | 9f | +----+----+----+----+----+----+----+----+ 0x08| ff | 00 | 00 | 00 | 11 |Bck0|Bck1|Bck2| +----+----+----+----+----+----+----+----+ 0x10|Bck3|Bck4|Bck5|BCC | 00 | 00 |Seg0|Seg1| +----+----+----+----+----+----+----+----+ 0x18|Seg2|Seg3|SegC|Stp0|Stp1|Stp2|Stp3|UID0| +----+----+----+----+----+----+----+----+ 0x20|UID1|UID2|kghC| +----+----+----+ MCD= ManufacturerID (1 Byte) MSN0..2= ManufactureSerialNumber (3 Byte) MCC= CRC (1 Byte) calculated over MCD,MSN0..2 DCF= DecrementalField (2 Byte) 'credential' (enduser-Tag) seems to have always DCF-low=0x60 DCF-high=0xea Bck0..5= Backup (6 Byte) Bck0 'dirty-flag', Bck1..5 SegmentHeader-Backup BCC= BackupCRC (1 Byte) CRC calculated over Bck1..5 Seg0..3= SegmentHeader (on MIM 4 Byte ) SegC= SegmentCRC (1 Byte) calculated over MCD,MSN0..2,Seg0..3 Stp0..n= Stamp0... (variable length) length = Segment-Len - UserData - 1 UID0..n= UserDater (variable length - with KGH hex 0x00-0x63 / dec 0-99) length = Segment-Len - WRP - WRC - 1 kghC= KabaGroupHeader (1 Byte + addr 0x0c must be 0x11) as seen on this example: addr 0x05..0x08 & 0x0c must have been set to this values - otherwise kghCRC will not be created by a official reader (not accepted) --]] 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`. ]] example = [[ script run legic_clone -i my_dump.bin -o my_clone.bin -c f8 script run legic_clone -i my_dump.bin -d -s ]] usage = [[ script run legic_clone [-h] [-i ] [-o ] [-c ] [-d] [-s] [-w] ]] arguments = [[ required : -i - file to read data from, must be in binary format (*.bin) optional : -h - Help text -o - requires option -c to be given -c - requires option -o to be given -d - Display content of found Segments -s - Display summary at the end -w - write directly to tag - a file hf-legic-UID-dump.bin will also be generated e.g.: hint: using the CRC '00' will result in a plain dump ( -c 00 ) ]] local DEBUG = true local bxor = bit32.bxor --- -- This is only meant to be used when errors occur local function dbg(args) if not DEBUG then return end if type(args) == 'table' then local i = 1 while args[i] do dbg(args[i]) i = i+1 end else print('###', args) end end -- we need always 2 digits local function prepend_zero(s) if s == nil then return '..' end if (#s == 1) then return '0' .. s else if (#s == 0) then return '00' else return s end end end --- -- This is only meant to be used when errors occur local function oops(err) print('ERROR:', err) core.clearCommandBuffer() return nil, err end --- -- Usage 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 -- read LEGIC data local function readlegicdata(offset, length, iv) -- Read data local command = Command:newMIX{ cmd = cmds.CMD_HF_LEGIC_READER , arg1 = offset , arg2 = length , arg3 = iv , data = nil } local result, err = command:sendMIX() if not result then return oops(err) end -- result is a packed data structure, data starts at offset 33 return result end -- Check availability of file local function file_check(file_name) local exists = io.open(file_name, "r") if not exists then exists = false else exists = true end return exists end --- xor-wrapper -- xor all from addr 0x22 (start counting from 1 => 23) local function xorme(hex, xor, index) if ( index >= 23 ) then return ('%02x'):format(bxor( tonumber(hex, 16) , tonumber(xor, 16) )) else return hex end end -- read input-file into array local function getInputBytes(infile) local bytes = {} local f = io.open(infile, "rb") if f == nil then print("OOps ... failed to read from file ".. infile); return false; end local str = f:read("*all") f:close() for c in (str or ''):gmatch'.' do bytes[#bytes + 1] = ('%02x'):format(c:byte()) end print("\nread ".. #bytes .." bytes from "..ansicolors.yellow..infile..ansicolors.reset) return bytes end -- write to file local function writeOutputBytes(bytes, outfile) local fho,err = io.open(outfile, "wb") if err then print("OOps ... faild to open output-file ".. outfile); return false; end for i = 1, #bytes do fho:write(string.char(tonumber(bytes[i], 16))) end fho:close() print("\nwrote ".. #bytes .." bytes to " .. outfile) return true end -- xore certain bytes local function xorBytes(inBytes, crc) local bytes = {} for index = 1, #inBytes do bytes[index] = xorme(inBytes[index], crc, index) end if (#inBytes == #bytes) then -- replace crc bytes[5] = string.sub(crc, -2) return bytes else print("error: byte-count missmatch") return false end end -- get raw segment-data local function getSegmentData(bytes, start, index) local raw, len, valid, last, wrp, wrc, rd, crc local segment = {} segment[0] = bytes[start]..' '..bytes[start + 1]..' '..bytes[start + 2]..' '..bytes[start + 3] -- flag = high nibble of byte 1 segment[1] = string.sub(bytes[start + 1], 0, 1) -- valid = bit 6 of byte 1 segment[2] = tonumber(bit32.extract('0x'..bytes[start + 1], 6, 1), 16) -- last = bit 7 of byte 1 segment[3] = tonumber(bit32.extract('0x'..bytes[start + 1], 7, 1), 16) -- len = (byte 0)+(bit0-3 of byte 1) segment[4] = tonumber(('%03x'):format(tonumber(bit32.extract('0x'..bytes[start + 1], 0, 3), 16)..tonumber(bytes[start], 16)), 16) -- wrp (write proteted) = byte 2 segment[5] = tonumber(bytes[start + 2]) -- wrc (write control) - bit 4-6 of byte 3 segment[6] = tonumber(bit32.extract('0x'..bytes[start + 3], 4, 3), 16) -- rd (read disabled) - bit 7 of byte 3 segment[7] = tonumber(bit32.extract('0x'..bytes[start + 3], 7, 1), 16) -- crc byte 4 segment[8] = bytes[start + 4] -- segment index segment[9] = index -- # crc-byte segment[10] = start + 4 return segment 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 local function CheckKgh(bytes, segStart, segEnd) if (bytes[8] == '9f' and bytes[9] == 'ff' and bytes[13] == '11') then local i local data = {} segStart = tonumber(segStart, 10) segEnd = tonumber(segEnd, 10) local dataLen = segEnd - segStart - 5 --- gather creadentials for verify local WRP = bytes[(segStart + 2)] local WRC = ("%02x"):format(tonumber(bit32.extract("0x"..bytes[segStart+3], 4, 3), 16)) local RD = ("%02x"):format(tonumber(bit32.extract("0x"..bytes[segStart+3], 7, 1), 16)) local XX = "00" cmd = bytes[1]..bytes[2]..bytes[3]..bytes[4]..WRP..WRC..RD..XX for i = (segStart + 5), (segStart + 5 + dataLen - 2) do cmd = cmd..bytes[i] end local KGH = ("%02x"):format(utils.Crc8Legic(cmd)) if (KGH == bytes[segEnd - 1]) then return KGH else return false end else return false end end -- get only the addresses of segemnt-crc's and the length of bytes local function getSegmentCrcBytes(bytes) local start = 23 local index = 0 local crcbytes = {} repeat seg = getSegmentData(bytes, start, index) crcbytes[index] = seg[10] start = start + seg[4] index = index + 1 until (seg[3] == 1 or tonumber(seg[9]) == 126 ) crcbytes[index] = start return crcbytes end -- print Segment values local function printSegment(SegmentData) res = "\nSegment "..SegmentData[9]..": " res = res.. "raw header="..SegmentData[0]..", " res = res.. "flag="..SegmentData[1].." (valid="..SegmentData[2].." last="..SegmentData[3].."), " res = res.. "len="..("%04d"):format(SegmentData[4])..", " res = res.. "WRP="..prepend_zero(SegmentData[5])..", " res = res.. "WRC="..prepend_zero(SegmentData[6])..", " res = res.. "RD="..SegmentData[7]..", " res = res.. "crc="..SegmentData[8] print(res) end -- print segment-data (hf legic info like) local function displaySegments(bytes) --display segment header(s) start = 23 index = '00' --repeat until last-flag ist set to 1 or segment-index has reached 126 repeat wrc = '' wrp = '' pld = '' Seg = getSegmentData(bytes, start, index) if Seg == nil then return OOps("segment is nil") end KGH = CheckKgh(bytes, start, (start + tonumber(Seg[4], 10))) printSegment(Seg) -- wrc if (Seg[6] > 0) then print("WRC protected area:") -- 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]..' ' end print(wrc) elseif (Seg[5] > 0) then print("Remaining write protected area:") -- 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]..' ' end print(wrp) end -- payload print("Remaining segment payload:") --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]..' ' end print(pld) if (KGH) then print(ansicolors.yellow.."'Kaba Group Header' detected"..ansicolors.reset) end start = start + Seg[4] index = prepend_zero(tonumber(Seg[9]) + 1) until (Seg[3] == 1 or tonumber(Seg[9]) == 126 ) end -- write clone-data to tag local function writeToTag(plainBytes) local SegCrcs = {} local output local readbytes if (utils.confirm("\nplace your empty tag onto the PM3 to restore the data of the input file\nthe CRCs will be calculated as needed\n confirm when ready") == false) then return end readbytes = readlegicdata(0, 4, 0x55) -- gather MCD & MSN from new Tag - this must be enterd manually print("\nthese are the MCD MSN0 MSN1 MSN2 from the Tag that has being read:") -- readbytes is a usbcommandOLD package, hence 32 bytes offset until data. plainBytes[1] = ('%02x'):format(readbytes:byte(33)) plainBytes[2] = ('%02x'):format(readbytes:byte(34)) plainBytes[3] = ('%02x'):format(readbytes:byte(35)) plainBytes[4] = ('%02x'):format(readbytes:byte(36)) MCD = plainBytes[1] MSN0 = plainBytes[2] MSN1 = plainBytes[3] MSN2 = plainBytes[4] -- calculate crc8 over MCD & MSN cmd = MCD..MSN0..MSN1..MSN2 MCC = ("%02x"):format(utils.Crc8Legic(cmd)) print("MCD:"..ansicolors.green..MCD..ansicolors.reset..", MSN:"..ansicolors.green..MSN0.." "..MSN1.." "..MSN2..ansicolors.reset..", MCC:"..MCC) -- 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(tonumber(bit32.extract("0x"..plainBytes[(SegCrcs[i] - 3)], 0, 3), 16))..("%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 -- apply MCD & MSN to plain data plainBytes[1] = MCD plainBytes[2] = MSN0 plainBytes[3] = MSN1 plainBytes[4] = MSN2 plainBytes[5] = MCC -- prepare plainBytes for writing (xor plain data with new MCC) bytes = xorBytes(plainBytes, MCC) -- write data to file if (writeOutputBytes(bytes, "hf-legic-UID-dump.bin")) then -- write pm3-buffer to Tag cmd = ('hf legic restore f hf-legic-UID-dump') core.console(cmd) end end -- main function local function main(args) -- some variables local i = 0 local oldcrc, newcrc, infile, outfile local bytes = {} local segments = {} -- parse arguments for the script for o, a in getopt.getopt(args, 'hwsdc:i:o:') do -- output file if o == 'o' then outfile = a ofs = true if (file_check(a)) then local answer = utils.confirm('\nthe output-file '..a..' already exists!\nthis will delete the previous content!\ncontinue?') if (answer == false) then return oops('quiting') end end end -- input file if o == 'i' then infile = a if (file_check(infile) == false) then return oops('input file: '..infile..' not found') end bytes = getInputBytes(infile) oldcrc = bytes[5] ifs = true if (bytes == false) then return oops('couldnt read file') end i = i + 1 end -- new crc if o == 'c' then newcrc = a:lower() ncs = true end -- display segments switch if o == 'd' then ds = true; end -- display summary switch if o == 's' then ss = true; end -- write to tag switch if o == 'w' then ws = true; end -- help if o == 'h' then return help() end end if (not ifs) then return oops('option -i is required') end -- bytes to plain bytes = xorBytes(bytes, oldcrc) -- show segments (works only on plain bytes) if (ds) then print("+------------------------------------------- Segments -------------------------------------------+") displaySegments(bytes); end if (ofs and ncs) then -- xor bytes with new crc newBytes = xorBytes(bytes, newcrc) -- write output if (writeOutputBytes(newBytes, outfile)) then -- show summary if requested if (ss) then -- information res = "\n+-------------------------------------------- Summary -------------------------------------------+" 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 .."\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" res = res .."\ne.g. (based on Segment00 of the data from "..infile.."):" res = res .."\n" res = res ..ansicolors.yellow.."hf legic crc d "..bytes[1]..bytes[2]..bytes[3]..bytes[4]..bytes[23]..bytes[24]..bytes[25]..bytes[26].." u "..newcrc.." c 8"..ansicolors.reset -- this can not be calculated without knowing the new MCD, MSN0..2 print(res) end 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")) end end -- write to tag if (ws and ( #bytes == 1024 or #bytes == 256)) then writeToTag(bytes) end end -- call main with arguments main(args)