maur keys
[RRG-proxmark3.git] / client / luascripts / hf_legic_clone.lua
blob757d65fa0a1b5691c0df5ea0c9c83becc5313a82
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 data
171 local function readlegicdata(offset, length, iv)
172 -- Read data
173 local command = Command:newMIX{
174 cmd = cmds.CMD_HF_LEGIC_READER
175 , arg1 = offset
176 , arg2 = length
177 , arg3 = iv
178 , data = nil
180 local result, err = command:sendMIX()
181 if not result then return oops(err) end
182 -- result is a packed data structure, data starts at offset 33
183 return result
186 -- Check availability of file
187 local function file_check(file_name)
188 local exists = io.open(file_name, "r")
189 if not exists then
190 exists = false
191 else
192 exists = true
194 return exists
197 --- xor-wrapper
198 -- xor all from addr 0x22 (start counting from 1 => 23)
199 local function xorme(hex, xor, index)
200 if ( index >= 23 ) then
201 return ('%02x'):format(bxor( tonumber(hex, 16) , tonumber(xor, 16) ))
202 else
203 return hex
207 -- read input-file into array
208 local function getInputBytes(infile)
209 local bytes = {}
210 local f = io.open(infile, "rb")
211 if f == nil then print("OOps ... failed to read from file ".. infile); return false; end
213 local str = f:read("*all")
214 f:close()
216 for c in (str or ''):gmatch'.' do
217 bytes[#bytes + 1] = ('%02x'):format(c:byte())
220 print("\nread ".. #bytes .." bytes from "..ansicolors.yellow..infile..ansicolors.reset)
221 return bytes
224 -- write to file
225 local function writeOutputBytes(bytes, outfile)
226 local fho,err = io.open(outfile, "wb")
227 if err then print("OOps ... faild to open output-file ".. outfile); return false; end
229 for i = 1, #bytes do
230 fho:write(string.char(tonumber(bytes[i], 16)))
232 fho:close()
233 print("\nwrote ".. #bytes .." bytes to " .. outfile)
234 return true
237 -- xore certain bytes
238 local function xorBytes(inBytes, crc)
239 local bytes = {}
240 for index = 1, #inBytes do
241 bytes[index] = xorme(inBytes[index], crc, index)
243 if (#inBytes == #bytes) then
244 -- replace crc
245 bytes[5] = string.sub(crc, -2)
246 return bytes
247 else
248 print("error: byte-count missmatch")
249 return false
253 -- get raw segment-data
254 local function getSegmentData(bytes, start, index)
255 local raw, len, valid, last, wrp, wrc, rd, crc
256 local segment = {}
257 segment[0] = bytes[start]..' '..bytes[start + 1]..' '..bytes[start + 2]..' '..bytes[start + 3]
258 -- flag = high nibble of byte 1
259 segment[1] = string.sub(bytes[start + 1], 0, 1)
261 -- valid = bit 6 of byte 1
262 segment[2] = tonumber(bit32.extract('0x'..bytes[start + 1], 6, 1), 16)
264 -- last = bit 7 of byte 1
265 segment[3] = tonumber(bit32.extract('0x'..bytes[start + 1], 7, 1), 16)
267 -- len = (byte 0)+(bit0-3 of byte 1)
268 segment[4] = tonumber(('%03x'):format(tonumber(bit32.extract('0x'..bytes[start + 1], 0, 3), 16)..tonumber(bytes[start], 16)), 16)
270 -- wrp (write proteted) = byte 2
271 segment[5] = tonumber(bytes[start + 2])
273 -- wrc (write control) - bit 4-6 of byte 3
274 segment[6] = tonumber(bit32.extract('0x'..bytes[start + 3], 4, 3), 16)
276 -- rd (read disabled) - bit 7 of byte 3
277 segment[7] = tonumber(bit32.extract('0x'..bytes[start + 3], 7, 1), 16)
279 -- crc byte 4
280 segment[8] = bytes[start + 4]
282 -- segment index
283 segment[9] = index
285 -- # crc-byte
286 segment[10] = start + 4
287 return segment
290 --- Kaba Group Header
291 -- checks if a segment does have a kghCRC
292 -- returns boolean false if no kgh has being detected or the kghCRC if a kgh was detected
293 local function CheckKgh(bytes, segStart, segEnd)
294 if (bytes[8] == '9f' and bytes[9] == 'ff' and bytes[13] == '11') then
295 local i
296 local data = {}
297 segStart = tonumber(segStart, 10)
298 segEnd = tonumber(segEnd, 10)
299 local dataLen = segEnd - segStart - 5
300 --- gather creadentials for verify
301 local WRP = bytes[(segStart + 2)]
302 local WRC = ("%02x"):format(tonumber(bit32.extract("0x"..bytes[segStart+3], 4, 3), 16))
303 local RD = ("%02x"):format(tonumber(bit32.extract("0x"..bytes[segStart+3], 7, 1), 16))
304 local XX = "00"
305 cmd = bytes[1]..bytes[2]..bytes[3]..bytes[4]..WRP..WRC..RD..XX
306 for i = (segStart + 5), (segStart + 5 + dataLen - 2) do
307 cmd = cmd..bytes[i]
309 local KGH = ("%02x"):format(utils.Crc8Legic(cmd))
310 if (KGH == bytes[segEnd - 1]) then
311 return KGH
312 else
313 return false
315 else
316 return false
320 -- get only the addresses of segemnt-crc's and the length of bytes
321 local function getSegmentCrcBytes(bytes)
322 local start = 23
323 local index = 0
324 local crcbytes = {}
325 repeat
326 seg = getSegmentData(bytes, start, index)
327 crcbytes[index] = seg[10]
328 start = start + seg[4]
329 index = index + 1
330 until (seg[3] == 1 or tonumber(seg[9]) == 126 )
331 crcbytes[index] = start
332 return crcbytes
335 -- print Segment values
336 local function printSegment(SegmentData)
337 res = "\nSegment "..SegmentData[9]..": "
338 res = res.. "raw header="..SegmentData[0]..", "
339 res = res.. "flag="..SegmentData[1].." (valid="..SegmentData[2].." last="..SegmentData[3].."), "
340 res = res.. "len="..("%04d"):format(SegmentData[4])..", "
341 res = res.. "WRP="..prepend_zero(SegmentData[5])..", "
342 res = res.. "WRC="..prepend_zero(SegmentData[6])..", "
343 res = res.. "RD="..SegmentData[7]..", "
344 res = res.. "crc="..SegmentData[8]
345 print(res)
348 -- print segment-data (hf legic info like)
349 local function displaySegments(bytes)
351 --display segment header(s)
352 start = 23
353 index = '00'
355 --repeat until last-flag ist set to 1 or segment-index has reached 126
356 repeat
357 wrc = ''
358 wrp = ''
359 pld = ''
360 Seg = getSegmentData(bytes, start, index)
361 if Seg == nil then return OOps("segment is nil") end
363 KGH = CheckKgh(bytes, start, (start + tonumber(Seg[4], 10)))
365 printSegment(Seg)
367 -- wrc
368 if (Seg[6] > 0) then
369 print("WRC protected area:")
370 -- length of wrc = wrc
371 for i = 1, Seg[6] do
372 -- starts at (segment-start + segment-header + segment-crc)-1
373 wrc = wrc..bytes[(start + 4 + 1 + i) - 1]..' '
375 print(wrc)
376 elseif (Seg[5] > 0) then
377 print("Remaining write protected area:")
378 -- length of wrp = (wrp-wrc)
379 for i = 1, (Seg[5] - Seg[6]) do
380 -- starts at (segment-start + segment-header + segment-crc + wrc)-1
381 wrp = wrp..bytes[(start + 4 + 1 + Seg[6] + i) - 1]..' '
383 print(wrp)
386 -- payload
387 print("Remaining segment payload:")
388 --length of payload = segment-len - segment-header - segment-crc - wrp -wrc
389 for i = 1, (Seg[4] - 4 - 1 - Seg[5] - Seg[6]) do
390 -- starts at (segment-start + segment-header + segment-crc + segment-wrp + segemnt-wrc)-1
391 pld = pld..bytes[(start + 4 + 1 + Seg[5] + Seg[6] + i) - 1]..' '
393 print(pld)
394 if (KGH) then
395 print(ansicolors.yellow.."'Kaba Group Header' detected"..ansicolors.reset)
397 start = start + Seg[4]
398 index = prepend_zero(tonumber(Seg[9]) + 1)
400 until (Seg[3] == 1 or tonumber(Seg[9]) == 126 )
403 -- write clone-data to tag
404 local function writeToTag(plainBytes)
405 local SegCrcs = {}
406 local output
407 local readbytes
408 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
409 return
412 readbytes = readlegicdata(0, 4, 0x55)
413 -- gather MCD & MSN from new Tag - this must be enterd manually
414 print("\nthese are the MCD MSN0 MSN1 MSN2 from the Tag that has being read:")
416 -- readbytes is a usbcommandOLD package, hence 32 bytes offset until data.
417 plainBytes[1] = ('%02x'):format(readbytes:byte(33))
418 plainBytes[2] = ('%02x'):format(readbytes:byte(34))
419 plainBytes[3] = ('%02x'):format(readbytes:byte(35))
420 plainBytes[4] = ('%02x'):format(readbytes:byte(36))
422 MCD = plainBytes[1]
423 MSN0 = plainBytes[2]
424 MSN1 = plainBytes[3]
425 MSN2 = plainBytes[4]
426 -- calculate crc8 over MCD & MSN
427 cmd = MCD..MSN0..MSN1..MSN2
428 MCC = ("%02x"):format(utils.Crc8Legic(cmd))
429 print("MCD:"..ansicolors.green..MCD..ansicolors.reset..", MSN:"..ansicolors.green..MSN0.." "..MSN1.." "..MSN2..ansicolors.reset..", MCC:"..MCC)
431 -- calculate new Segment-CRC for each valid segment
432 SegCrcs = getSegmentCrcBytes(plainBytes)
433 for i = 0, (#SegCrcs - 1) do
434 -- SegCrcs[i]-4 = address of first byte of segmentHeader (low byte segment-length)
435 segLen = tonumber(("%1x"):format(tonumber(bit32.extract("0x"..plainBytes[(SegCrcs[i] - 3)], 0, 3), 16))..("%02x"):format(tonumber(plainBytes[SegCrcs[i] - 4], 16)), 16)
436 segStart = (SegCrcs[i] - 4)
437 segEnd = (SegCrcs[i] - 4 + segLen)
438 KGH = CheckKgh(plainBytes, segStart, segEnd)
439 if (KGH) then
440 print("'Kaba Group Header' detected - re-calculate...")
442 cmd = MCD..MSN0..MSN1..MSN2..plainBytes[SegCrcs[i]-4]..plainBytes[SegCrcs[i]-3]..plainBytes[SegCrcs[i]-2]..plainBytes[SegCrcs[i]-1]
443 plainBytes[SegCrcs[i]] = ("%02x"):format(utils.Crc8Legic(cmd))
446 -- apply MCD & MSN to plain data
447 plainBytes[1] = MCD
448 plainBytes[2] = MSN0
449 plainBytes[3] = MSN1
450 plainBytes[4] = MSN2
451 plainBytes[5] = MCC
453 -- prepare plainBytes for writing (xor plain data with new MCC)
454 bytes = xorBytes(plainBytes, MCC)
456 -- write data to file
457 if (writeOutputBytes(bytes, "hf-legic-UID-dump.bin")) then
458 -- write pm3-buffer to Tag
459 cmd = ('hf legic restore -f hf-legic-UID-dump')
460 core.console(cmd)
464 -- main function
465 local function main(args)
466 -- some variables
467 local i = 0
468 local oldcrc, newcrc, infile, outfile
469 local bytes = {}
470 local segments = {}
472 -- parse arguments for the script
473 for o, a in getopt.getopt(args, 'hwsdc:i:o:') do
474 -- output file
475 if o == 'o' then
476 outfile = a
477 ofs = true
478 if (file_check(a)) then
479 local answer = utils.confirm('\nthe output-file '..a..' already exists!\nthis will delete the previous content!\ncontinue?')
480 if (answer == false) then return oops('quiting') end
483 -- input file
484 if o == 'i' then
485 infile = a
486 if (file_check(infile) == false) then return oops('input file: '..infile..' not found') end
488 bytes = getInputBytes(infile)
489 oldcrc = bytes[5]
490 ifs = true
491 if (bytes == false) then return oops('couldnt read file') end
493 i = i + 1
495 -- new crc
496 if o == 'c' then
497 newcrc = a:lower()
498 ncs = true
500 -- display segments switch
501 if o == 'd' then ds = true; end
502 -- display summary switch
503 if o == 's' then ss = true; end
504 -- write to tag switch
505 if o == 'w' then ws = true; end
506 -- help
507 if o == 'h' then return help() end
510 if (not ifs) then return oops('option -i <input file> is required') end
512 -- bytes to plain
513 bytes = xorBytes(bytes, oldcrc)
515 -- show segments (works only on plain bytes)
516 if (ds) then
517 print("+------------------------------------------- Segments -------------------------------------------+")
518 displaySegments(bytes);
521 if (ofs and ncs) then
522 -- xor bytes with new crc
523 newBytes = xorBytes(bytes, newcrc)
524 -- write output
525 if (writeOutputBytes(newBytes, outfile)) then
526 -- show summary if requested
527 if (ss) then
528 -- information
529 res = "\n+-------------------------------------------- Summary -------------------------------------------+"
530 res = res .."\ncreated clone_dump from\n\t"..infile.." crc: "..oldcrc.."\ndump_file:"
531 res = res .."\n\t"..outfile.." crc: "..string.sub(newcrc, -2)
532 res = res .."\nyou may load the new file with:"
533 res = res ..ansicolors.yellow.."hf legic eload -f "..outfile..ansicolors.reset
534 res = res .."\n\nif you don't write to tag immediately ('-w' switch) you will need to recalculate each segmentCRC"
535 res = res .."\nafter writing this dump to a tag!"
536 res = res .."\n\na segmentCRC gets calculated over MCD,MSN0..3, Segment-Header0..3"
537 res = res .."\ne.g. (based on Segment00 of the data from "..infile.."):"
538 res = res .."\n"
539 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
540 -- this can not be calculated without knowing the new MCD, MSN0..2
541 print(res)
544 else
545 if (ss) then
546 -- show why the output-file was not written
547 print("\nnew file not written - some arguments are missing ..")
548 print("output file: ".. (ofs and outfile or "not given"))
549 print("new crc: ".. (ncs and newcrc or "not given"))
552 -- write to tag
553 if (ws and ( #bytes == 1024 or #bytes == 256)) then
554 writeToTag(bytes)
558 -- call main with arguments
559 main(args)