text
[RRG-proxmark3.git] / client / luascripts / hf_legic_clone.lua
blob01d44ebe6405f6309c20bb27bb06ff4a1e1fee21
1 local utils = require('utils')
2 local cmds = require('commands')
3 local getopt = require('getopt')
4 local ansicolors = require('ansicolors')
6 --[[
7 script to create a clone-dump with new crc
8 Author: mosci
9 my Fork: https://github.com/icsom/proxmark3.git
11 1. read tag-dump, xor byte 22..end with byte 0x05 of the inputfile
12 2. write to outfile
13 3. set byte 0x05 to newcrc
14 4. until byte 0x21 plain like in inputfile
15 5. from 0x22..end xored with newcrc
16 6. calculate new crc on each segment (needs to know the new MCD & MSN0..2)
18 simplest usage:
19 Dump a legic tag with 'hf legic dump'
20 place your 'empty' tag on the reader and run
21 'script run hf_legic_clone -i orig.bin -w'
23 you will see some output like:
25 read 1024 bytes from orig.bin
27 place your empty tag onto the PM3 to read and display the MCD & MSN0..2
28 the values will be shown below
29 confirm when ready [y/n] ?y
31 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 !!
32 type in MCD as 2-digit value - e.g.: 00 (default: 79 )
33 > 0b
34 type in MSN0 as 2-digit value - e.g.: 01 (default: 28 )
35 > ad
36 type in MSN1 as 2-digit value - e.g.: 02 (default: d1 )
37 > c0
38 type in MSN2 as 2-digit value - e.g.: 03 (default: 43 )
39 > de
40 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
42 wrote 1024 bytes to myLegicClone.hex
43 enter number of bytes to write? (default: 86 )
45 loaded 1024 samples
46 #db# setting up legic card
47 #db# MIM 256 card found, writing 0x00 - 0x01 ...
48 #db# write successful
49 ...
50 #db# setting up legic card
51 #db# MIM 256 card found, writing 0x56 - 0x01 ...
52 #db# write successful
53 proxmark3>
55 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
56 and your clone should be ready (except there has to be a additional KGH-CRC to be calculated - which credentials are unknown until yet)
58 the '-w' switch will only work with my fork - it needs the binary legic_crc8 which is not part of the proxmark3-master-branch
59 also the ability to write DCF is not possible with the proxmark3-master-branch
60 but creating dumpfile-clone files will be possible (without valid segment-crc - this has to done manually with)
63 (example) Legic-Prime Layout with 'Kaba Group Header'
64 +----+----+----+----+----+----+----+----+
65 0x00|MCD |MSN0|MSN1|MSN2|MCC | 60 | ea | 9f |
66 +----+----+----+----+----+----+----+----+
67 0x08| ff | 00 | 00 | 00 | 11 |Bck0|Bck1|Bck2|
68 +----+----+----+----+----+----+----+----+
69 0x10|Bck3|Bck4|Bck5|BCC | 00 | 00 |Seg0|Seg1|
70 +----+----+----+----+----+----+----+----+
71 0x18|Seg2|Seg3|SegC|Stp0|Stp1|Stp2|Stp3|UID0|
72 +----+----+----+----+----+----+----+----+
73 0x20|UID1|UID2|kghC|
74 +----+----+----+
76 MCD= ManufacturerID (1 Byte)
77 MSN0..2= ManufactureSerialNumber (3 Byte)
78 MCC= CRC (1 Byte) calculated over MCD,MSN0..2
79 DCF= DecrementalField (2 Byte) 'credential' (enduser-Tag) seems to have always DCF-low=0x60 DCF-high=0xea
80 Bck0..5= Backup (6 Byte) Bck0 'dirty-flag', Bck1..5 SegmentHeader-Backup
81 BCC= BackupCRC (1 Byte) CRC calculated over Bck1..5
82 Seg0..3= SegmentHeader (on MIM 4 Byte )
83 SegC= SegmentCRC (1 Byte) calculated over MCD,MSN0..2,Seg0..3
84 Stp0..n= Stamp0... (variable length) length = Segment-Len - UserData - 1
85 UID0..n= UserDater (variable length - with KGH hex 0x00-0x63 / dec 0-99) length = Segment-Len - WRP - WRC - 1
86 kghC= KabaGroupHeader (1 Byte + addr 0x0c must be 0x11)
87 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)
88 --]]
90 copyright = ''
91 author = 'Mosci'
92 version = 'v1.0.2'
93 desc = [[
94 This is a script which creates a clone-dump of a dump from a LEGIC Prime Tag (MIM256 or MIM1024)
95 Create a dump by running `hf legic dump`.
97 example = [[
98 script run hf_legic_clone -i my_dump.bin -o my_clone.bin -c f8
99 script run hf_legic_clone -i my_dump.bin -d -s
101 usage = [[
102 script run hf_legic_clone [-h] [-i <file>] [-o <file>] [-c <crc>] [-d] [-s] [-w]
104 arguments = [[
105 required :
106 -i <input file> - file to read data from, must be in binary format (*.bin)
108 optional :
109 -h - Help text
110 -o <output file> - requires option -c to be given
111 -c <new-tag crc> - requires option -o to be given
112 -d - Display content of found Segments
113 -s - Display summary at the end
114 -w - write directly to tag - a file hf-legic-UID-dump.bin will also be generated
116 e.g.:
117 hint: using the CRC '00' will result in a plain dump ( -c 00 )
119 local DEBUG = true
120 local bxor = bit32.bxor
122 -- This is only meant to be used when errors occur
123 local function dbg(args)
124 if not DEBUG then return end
125 if type(args) == 'table' then
126 local i = 1
127 while args[i] do
128 dbg(args[i])
129 i = i+1
131 else
132 print('###', args)
135 -- we need always 2 digits
136 local function prepend_zero(s)
137 if s == nil then return '..' end
139 if (#s == 1) then
140 return '0' .. s
141 else
142 if (#s == 0) then
143 return '00'
144 else
145 return s
150 -- This is only meant to be used when errors occur
151 local function oops(err)
152 print('ERROR:', err)
153 core.clearCommandBuffer()
154 return nil, err
157 -- Usage help
158 local function help()
159 print(copyright)
160 print(author)
161 print(version)
162 print(desc)
163 print(ansicolors.cyan..'Usage'..ansicolors.reset)
164 print(usage)
165 print(ansicolors.cyan..'Arguments'..ansicolors.reset)
166 print(arguments)
167 print(ansicolors.cyan..'Example usage'..ansicolors.reset)
168 print(example)
170 -- read LEGIC info
171 local function readlegicinfo()
172 -- Read data
173 local c = Command:newNG{cmd = cmds.CMD_HF_LEGIC_INFO, data = nil}
174 local result, err = c:sendNG(false, 2000)
175 if not result then return oops(err) end
176 -- result is a packed data structure, data starts at offset 33
177 return result
180 -- Check availability of file
181 local function file_check(file_name)
182 local exists = io.open(file_name, "r")
183 if not exists then
184 exists = false
185 else
186 exists = true
188 return exists
191 --- xor-wrapper
192 -- xor all from addr 0x22 (start counting from 1 => 23)
193 local function xorme(hex, xor, index)
194 if ( index >= 23 ) then
195 return ('%02x'):format(bxor( tonumber(hex, 16) , tonumber(xor, 16) ))
196 else
197 return hex
201 -- read input-file into array
202 local function getInputBytes(infile)
203 local bytes = {}
204 local f = io.open(infile, "rb")
205 if f == nil then print("OOps ... failed to read from file ".. infile); return false; end
207 local str = f:read("*all")
208 f:close()
210 for c in (str or ''):gmatch'.' do
211 bytes[#bytes + 1] = ('%02x'):format(c:byte())
214 print("\nread ".. #bytes .." bytes from "..ansicolors.yellow..infile..ansicolors.reset)
215 return bytes
218 -- write to file
219 local function writeOutputBytes(bytes, outfile)
220 local fho,err = io.open(outfile, "wb")
221 if err then print("OOps ... failed to open output-file ".. outfile); return false; end
223 for i = 1, #bytes do
224 fho:write(string.char(tonumber(bytes[i], 16)))
226 fho:close()
227 print("\nwrote ".. #bytes .." bytes to " .. outfile)
228 return true
231 -- xore certain bytes
232 local function xorBytes(inBytes, crc)
233 local bytes = {}
234 for index = 1, #inBytes do
235 bytes[index] = xorme(inBytes[index], crc, index)
237 if (#inBytes == #bytes) then
238 -- replace crc
239 bytes[5] = string.sub(crc, -2)
240 return bytes
241 else
242 print("error: byte-count missmatch")
243 return false
247 -- get raw segment-data
248 local function getSegmentData(bytes, start, index)
249 local raw, len, valid, last, wrp, wrc, rd, crc
250 local segment = {}
251 segment[0] = bytes[start]..' '..bytes[start + 1]..' '..bytes[start + 2]..' '..bytes[start + 3]
252 -- flag = high nibble of byte 1
253 segment[1] = string.sub(bytes[start + 1], 0, 1)
255 -- valid = bit 6 of byte 1
256 segment[2] = tonumber(bit32.extract('0x'..bytes[start + 1], 6, 1), 16)
258 -- last = bit 7 of byte 1
259 segment[3] = tonumber(bit32.extract('0x'..bytes[start + 1], 7, 1), 16)
261 -- len = (byte 0)+(bit0-3 of byte 1)
262 segment[4] = tonumber(('%03x'):format(tonumber(bit32.extract('0x'..bytes[start + 1], 0, 3), 16)..tonumber(bytes[start], 16)), 16)
264 -- wrp (write proteted) = byte 2
265 segment[5] = tonumber(bytes[start + 2])
267 -- wrc (write control) - bit 4-6 of byte 3
268 segment[6] = tonumber(bit32.extract('0x'..bytes[start + 3], 4, 3), 16)
270 -- rd (read disabled) - bit 7 of byte 3
271 segment[7] = tonumber(bit32.extract('0x'..bytes[start + 3], 7, 1), 16)
273 -- crc byte 4
274 segment[8] = bytes[start + 4]
276 -- segment index
277 segment[9] = index
279 -- # crc-byte
280 segment[10] = start + 4
281 return segment
284 --- Kaba Group Header
285 -- checks if a segment does have a kghCRC
286 -- returns boolean false if no kgh has being detected or the kghCRC if a kgh was detected
287 local function CheckKgh(bytes, segStart, segEnd)
288 if (bytes[8] == '9f' and bytes[9] == 'ff' and bytes[13] == '11') then
289 local i
290 local data = {}
291 segStart = tonumber(segStart, 10)
292 segEnd = tonumber(segEnd, 10)
293 local dataLen = segEnd - segStart - 5
294 --- gather creadentials for verify
295 local WRP = bytes[(segStart + 2)]
296 local WRC = ("%02x"):format(tonumber(bit32.extract("0x"..bytes[segStart+3], 4, 3), 16))
297 local RD = ("%02x"):format(tonumber(bit32.extract("0x"..bytes[segStart+3], 7, 1), 16))
298 local XX = "00"
299 cmd = bytes[1]..bytes[2]..bytes[3]..bytes[4]..WRP..WRC..RD..XX
300 for i = (segStart + 5), (segStart + 5 + dataLen - 2) do
301 cmd = cmd..bytes[i]
303 local KGH = ("%02x"):format(utils.Crc8Legic(cmd))
304 if (KGH == bytes[segEnd - 1]) then
305 return KGH
306 else
307 return false
309 else
310 return false
314 -- get only the addresses of segemnt-crc's and the length of bytes
315 local function getSegmentCrcBytes(bytes)
316 local start = 23
317 local index = 0
318 local crcbytes = {}
319 repeat
320 seg = getSegmentData(bytes, start, index)
321 crcbytes[index] = seg[10]
322 start = start + seg[4]
323 index = index + 1
324 until (seg[3] == 1 or tonumber(seg[9]) == 126 )
325 crcbytes[index] = start
326 return crcbytes
329 -- print Segment values
330 local function printSegment(SegmentData)
331 res = "\nSegment "..SegmentData[9]..": "
332 res = res.. "raw header="..SegmentData[0]..", "
333 res = res.. "flag="..SegmentData[1].." (valid="..SegmentData[2].." last="..SegmentData[3].."), "
334 res = res.. "len="..("%04d"):format(SegmentData[4])..", "
335 res = res.. "WRP="..prepend_zero(SegmentData[5])..", "
336 res = res.. "WRC="..prepend_zero(SegmentData[6])..", "
337 res = res.. "RD="..SegmentData[7]..", "
338 res = res.. "crc="..SegmentData[8]
339 print(res)
342 -- print segment-data (hf legic info like)
343 local function displaySegments(bytes)
345 --display segment header(s)
346 start = 23
347 index = '00'
349 --repeat until last-flag ist set to 1 or segment-index has reached 126
350 repeat
351 wrc = ''
352 wrp = ''
353 pld = ''
354 Seg = getSegmentData(bytes, start, index)
355 if Seg == nil then return OOps("segment is nil") end
357 KGH = CheckKgh(bytes, start, (start + tonumber(Seg[4], 10)))
359 printSegment(Seg)
361 -- wrc
362 if (Seg[6] > 0) then
363 print("WRC protected area:")
364 -- length of wrc = wrc
365 for i = 1, Seg[6] do
366 -- starts at (segment-start + segment-header + segment-crc)-1
367 wrc = wrc..bytes[(start + 4 + 1 + i) - 1]..' '
369 print(wrc)
370 elseif (Seg[5] > 0) then
371 print("Remaining write protected area:")
372 -- length of wrp = (wrp-wrc)
373 for i = 1, (Seg[5] - Seg[6]) do
374 -- starts at (segment-start + segment-header + segment-crc + wrc)-1
375 wrp = wrp..bytes[(start + 4 + 1 + Seg[6] + i) - 1]..' '
377 print(wrp)
380 -- payload
381 print("Remaining segment payload:")
382 --length of payload = segment-len - segment-header - segment-crc - wrp -wrc
383 for i = 1, (Seg[4] - 4 - 1 - Seg[5] - Seg[6]) do
384 -- starts at (segment-start + segment-header + segment-crc + segment-wrp + segemnt-wrc)-1
385 pld = pld..bytes[(start + 4 + 1 + Seg[5] + Seg[6] + i) - 1]..' '
387 print(pld)
388 if (KGH) then
389 print(ansicolors.yellow.."'Kaba Group Header' detected"..ansicolors.reset)
391 start = start + Seg[4]
392 index = prepend_zero(tonumber(Seg[9]) + 1)
394 until (Seg[3] == 1 or tonumber(Seg[9]) == 126 )
397 -- write clone-data to tag
398 local function writeToTag(plainBytes)
399 local SegCrcs = {}
400 local output
401 local readbytes
402 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
403 return
406 readbytes = readlegicinfo()
407 -- gather MCD & MSN from new Tag - this must be enterd manually
408 print("\nthese are the MCD MSN0 MSN1 MSN2 from the Tag that has being read:")
410 -- readbytes is a table with uid data as hex string in Data key
411 plainBytes[1] = readbytes.Data:sub(1,2)
412 plainBytes[2] = readbytes.Data:sub(3,4)
413 plainBytes[3] = readbytes.Data:sub(5,6)
414 plainBytes[4] = readbytes.Data:sub(7,8)
416 MCD = plainBytes[1]
417 MSN0 = plainBytes[2]
418 MSN1 = plainBytes[3]
419 MSN2 = plainBytes[4]
420 -- calculate crc8 over MCD & MSN
421 cmd = MCD..MSN0..MSN1..MSN2
422 MCC = ("%02x"):format(utils.Crc8Legic(cmd))
423 print("MCD:"..ansicolors.green..MCD..ansicolors.reset..", MSN:"..ansicolors.green..MSN0.." "..MSN1.." "..MSN2..ansicolors.reset..", MCC:"..MCC)
425 -- calculate new Segment-CRC for each valid segment
426 SegCrcs = getSegmentCrcBytes(plainBytes)
427 for i = 0, (#SegCrcs - 1) do
428 -- SegCrcs[i]-4 = address of first byte of segmentHeader (low byte segment-length)
429 segLen = tonumber(("%1x"):format(tonumber(bit32.extract("0x"..plainBytes[(SegCrcs[i] - 3)], 0, 3), 16))..("%02x"):format(tonumber(plainBytes[SegCrcs[i] - 4], 16)), 16)
430 segStart = (SegCrcs[i] - 4)
431 segEnd = (SegCrcs[i] - 4 + segLen)
432 KGH = CheckKgh(plainBytes, segStart, segEnd)
433 if (KGH) then
434 print("'Kaba Group Header' detected - re-calculate...")
436 cmd = MCD..MSN0..MSN1..MSN2..plainBytes[SegCrcs[i]-4]..plainBytes[SegCrcs[i]-3]..plainBytes[SegCrcs[i]-2]..plainBytes[SegCrcs[i]-1]
437 plainBytes[SegCrcs[i]] = ("%02x"):format(utils.Crc8Legic(cmd))
440 -- apply MCD & MSN to plain data
441 plainBytes[1] = MCD
442 plainBytes[2] = MSN0
443 plainBytes[3] = MSN1
444 plainBytes[4] = MSN2
445 plainBytes[5] = MCC
447 -- prepare plainBytes for writing (xor plain data with new MCC)
448 bytes = xorBytes(plainBytes, MCC)
450 -- write data to file
451 if (writeOutputBytes(bytes, "hf-legic-UID-dump.bin")) then
452 -- write pm3-buffer to Tag
453 cmd = ('hf legic restore -f hf-legic-UID-dump')
454 core.console(cmd)
458 -- main function
459 local function main(args)
460 -- some variables
461 local i = 0
462 local oldcrc, newcrc, infile, outfile
463 local bytes = {}
464 local segments = {}
466 -- parse arguments for the script
467 for o, a in getopt.getopt(args, 'hwsdc:i:o:') do
468 -- output file
469 if o == 'o' then
470 outfile = a
471 ofs = true
472 if (file_check(a)) then
473 local answer = utils.confirm('\nthe output-file '..a..' already exists!\nthis will delete the previous content!\ncontinue?')
474 if (answer == false) then return oops('quiting') end
477 -- input file
478 if o == 'i' then
479 infile = a
480 if (file_check(infile) == false) then return oops('input file: '..infile..' not found') end
482 bytes = getInputBytes(infile)
483 oldcrc = bytes[5]
484 ifs = true
485 if (bytes == false) then return oops('couldnt read file') end
487 i = i + 1
489 -- new crc
490 if o == 'c' then
491 newcrc = a:lower()
492 ncs = true
494 -- display segments switch
495 if o == 'd' then ds = true; end
496 -- display summary switch
497 if o == 's' then ss = true; end
498 -- write to tag switch
499 if o == 'w' then ws = true; end
500 -- help
501 if o == 'h' then return help() end
504 if (not ifs) then return oops('option -i <input file> is required') end
506 -- bytes to plain
507 bytes = xorBytes(bytes, oldcrc)
509 -- show segments (works only on plain bytes)
510 if (ds) then
511 print("+------------------------------------------- Segments -------------------------------------------+")
512 displaySegments(bytes);
515 if (ofs and ncs) then
516 -- xor bytes with new crc
517 newBytes = xorBytes(bytes, newcrc)
518 -- write output
519 if (writeOutputBytes(newBytes, outfile)) then
520 -- show summary if requested
521 if (ss) then
522 -- information
523 res = "\n+-------------------------------------------- Summary -------------------------------------------+"
524 res = res .."\ncreated clone_dump from\n\t"..infile.." crc: "..oldcrc.."\ndump_file:"
525 res = res .."\n\t"..outfile.." crc: "..string.sub(newcrc, -2)
526 res = res .."\nyou may load the new file with:"
527 res = res ..ansicolors.yellow.."hf legic eload -f "..outfile..ansicolors.reset
528 res = res .."\n\nif you don't write to tag immediately ('-w' switch) you will need to recalculate each segmentCRC"
529 res = res .."\nafter writing this dump to a tag!"
530 res = res .."\n\na segmentCRC gets calculated over MCD,MSN0..3, Segment-Header0..3"
531 res = res .."\ne.g. (based on Segment00 of the data from "..infile.."):"
532 res = res .."\n"
533 res = res ..ansicolors.yellow.."hf legic crc -d "..bytes[1]..bytes[2]..bytes[3]..bytes[4]..bytes[23]..bytes[24]..bytes[25]..bytes[26].." --mcc "..newcrc.." -t 8"..ansicolors.reset
534 -- this can not be calculated without knowing the new MCD, MSN0..2
535 print(res)
538 else
539 if (ss) then
540 -- show why the output-file was not written
541 print("\nnew file not written - some arguments are missing ..")
542 print("output file: ".. (ofs and outfile or "not given"))
543 print("new crc: ".. (ncs and newcrc or "not given"))
546 -- write to tag
547 if (ws and ( #bytes == 1024 or #bytes == 256)) then
548 writeToTag(bytes)
552 -- call main with arguments
553 main(args)