@@ -180,7 +180,7 @@ class DeviceRequiredUnit(BaseCLIUnit):
180180 """
181181
182182 def before_exec (self , args : argparse .Namespace ):
183- ret = self .device_com .isOpen ()
183+ ret = self .device_com .is_open ()
184184 if ret :
185185 return True
186186 else :
@@ -519,13 +519,17 @@ def on_exec(self, args: argparse.Namespace):
519519class HWConnect (BaseCLIUnit ):
520520 def args_parser (self ) -> ArgumentParserNoExit :
521521 parser = ArgumentParserNoExit ()
522- parser .description = 'Connect to chameleon by serial port'
523- parser .add_argument ('-p' , '--port' , type = str , required = False )
522+ parser .description = 'Connect to chameleon by serial port or TCP'
523+ parser .add_argument ('-p' , '--port' , type = str , required = False , help = 'Serial port name/path' )
524+ parser .add_argument ('-t' , '--tcp' , type = str , required = False , help = 'TCP server to connect to (host:port)' )
524525 return parser
525526
527+ def before_exec (self , args : argparse .Namespace ):
528+ return True
529+
526530 def on_exec (self , args : argparse .Namespace ):
527531 try :
528- if args .port is None : # Chameleon auto-detect if no port is supplied
532+ if args .port is None and args . tcp is None : # Chameleon auto-detect if no port is supplied
529533 platform_name = uname ().release
530534 if 'Microsoft' in platform_name :
531535 path = os .environ ["PATH" ].split (os .pathsep )
@@ -555,9 +559,13 @@ def on_exec(self, args: argparse.Namespace):
555559 args .port = port .device
556560 break
557561 if args .port is None : # If no chameleon was found, exit
558- print ("Chameleon not found, please connect the device or try connecting manually with the -p flag." )
562+ print ("Chameleon not found, please connect the device or try connecting manually with the -p or -t flag." )
559563 return
560- self .device_com .open (args .port )
564+
565+ if args .tcp :
566+ self .device_com .open (chameleon_com .ChameleonTCPTransport (args .tcp ))
567+ else :
568+ self .device_com .open (chameleon_com .ChameleonSerialTransport (args .port ))
561569 self .device_com .commands = self .cmd .get_device_capabilities ()
562570 major , minor = self .cmd .get_app_version ()
563571 model = ['Ultra' , 'Lite' ][self .cmd .get_device_model ()]
@@ -1449,6 +1457,189 @@ def on_exec(self, args: argparse.Namespace):
14491457 print (f"( { CR } 0{ C0 } : Failed, { CG } 1{ C0 } : Success )\n \n " )
14501458
14511459
1460+ @hf_mf .command ('dump' )
1461+ class HFMFDump (MF1AuthArgsUnit ):
1462+ def args_parser (self ) -> ArgumentParserNoExit :
1463+ parser = ArgumentParserNoExit ()
1464+ parser .description = 'Mifare Classic dump tag'
1465+ parser .add_argument ('-t' , '--dump-file-type' , type = str , required = False ,
1466+ help = "Dump file content type" , choices = ['bin' , 'hex' ])
1467+ parser .add_argument ('-f' , '--dump-file' , type = argparse .FileType ("wb" ), required = True ,
1468+ help = "Dump file to write data from tag" )
1469+ parser .add_argument ('-d' , '--dic' , type = argparse .FileType ("r" ), required = True ,
1470+ help = "Read keys (to communicate with tag to dump) from .dic format file" )
1471+ return parser
1472+
1473+ def on_exec (self , args : argparse .Namespace ):
1474+ # check dump type
1475+ if args .dump_file_type is None :
1476+ if args .dump_file .name .endswith ('.bin' ):
1477+ content_type = 'bin'
1478+ elif args .dump_file .name .endswith ('.eml' ):
1479+ content_type = 'hex'
1480+ else :
1481+ raise Exception ("Unknown file format, Specify content type with -t option" )
1482+ else :
1483+ content_type = args .dump_file_type
1484+
1485+ # read keys from file
1486+ keys = [bytes .fromhex (line .strip ()) for line in args .dic if line .strip ()]
1487+
1488+ # data to write to dump file
1489+ buffer = bytearray ()
1490+
1491+ # iterate over sectors
1492+ for s in range (16 ):
1493+ # try all keys for this sector
1494+ typ = None
1495+ key_found = None
1496+ for key in keys :
1497+ # first try key B
1498+ try :
1499+ self .cmd .mf1_read_one_block (4 * s , MfcKeyType .B , key )
1500+ typ = MfcKeyType .B
1501+ key_found = key
1502+ break
1503+ except UnexpectedResponseError :
1504+ # ignore read errors at this stage as we want to try key A
1505+ pass
1506+ # try with key A if B was unsuccessful
1507+ try :
1508+ self .cmd .mf1_read_one_block (4 * s , MfcKeyType .A , key )
1509+ typ = MfcKeyType .A
1510+ key_found = key
1511+ break
1512+ except UnexpectedResponseError :
1513+ pass
1514+ else :
1515+ raise Exception (f"No key found for sector { s } " )
1516+
1517+ # iterate over blocks
1518+ for b in range (4 ):
1519+ try :
1520+ block_data = self .cmd .mf1_read_one_block (4 * s + b , typ , key_found )
1521+ # add data to buffer
1522+ if content_type == 'bin' :
1523+ buffer .extend (block_data )
1524+ elif content_type == 'hex' :
1525+ buffer .extend (block_data .hex ().encode ("utf-8" ))
1526+ except Exception as e :
1527+ print (f"Error reading block { 4 * s + b } : { e } " )
1528+ # Fill with zeros if we can't read the block
1529+ if content_type == 'bin' :
1530+ buffer .extend (bytes (16 ))
1531+ else :
1532+ buffer .extend (b'0' * 32 )
1533+
1534+ # write buffer to file
1535+ args .dump_file .write (buffer )
1536+ args .dump_file .close ()
1537+ print (f"Dump completed and saved to { args .dump_file .name } " )
1538+
1539+
1540+ @hf_mf .command ('clone' )
1541+ class HFMFClone (MF1AuthArgsUnit ):
1542+ def args_parser (self ) -> ArgumentParserNoExit :
1543+ parser = ArgumentParserNoExit ()
1544+ parser .description = 'Mifare Classic clone tag from dump'
1545+ parser .add_argument ('-t' , '--dump-file-type' , type = str , required = False ,
1546+ help = "Dump file content type" , choices = ['bin' , 'hex' ])
1547+ parser .add_argument ('-a' , '--clone-access' , action = 'store_true' ,
1548+ help = "Write ACL from original dump (use with caution, could brick your tag)" )
1549+ parser .add_argument ('-f' , '--dump-file' , type = argparse .FileType ("rb" ), required = True ,
1550+ help = "Dump file containing data to write on new tag" )
1551+ parser .add_argument ('-d' , '--dic' , type = argparse .FileType ("r" ), required = True ,
1552+ help = "Read keys (to communicate with tag to write) from .dic format file" )
1553+ return parser
1554+
1555+ def on_exec (self , args : argparse .Namespace ):
1556+ # check dump type
1557+ if args .dump_file_type is None :
1558+ if args .dump_file .name .endswith ('.bin' ):
1559+ content_type = 'bin'
1560+ elif args .dump_file .name .endswith ('.eml' ):
1561+ content_type = 'hex'
1562+ else :
1563+ raise Exception ("Unknown file format, Specify content type with -t option" )
1564+ else :
1565+ content_type = args .dump_file_type
1566+
1567+ # read data from dump file
1568+ if content_type == 'bin' :
1569+ buffer = bytearray (args .dump_file .read ())
1570+ else : # hex
1571+ buffer = bytearray .fromhex (args .dump_file .read ().decode ())
1572+
1573+ if len (buffer ) % 16 != 0 :
1574+ raise Exception ("Data block not aligned to 16 bytes" )
1575+ if len (buffer ) // 16 > 256 :
1576+ raise Exception ("Data block memory overflow" )
1577+
1578+ # read keys from file
1579+ keys = [bytes .fromhex (line .strip ()) for line in args .dic if line .strip ()]
1580+
1581+ # iterate over sectors
1582+ for s in range (16 ):
1583+ # try all keys for this sector
1584+ keyA , keyB = None , None
1585+ for key in keys :
1586+ # first try key B
1587+ try :
1588+ self .cmd .mf1_read_one_block (4 * s , MfcKeyType .B , key )
1589+ keyB = key
1590+ except UnexpectedResponseError :
1591+ pass
1592+ # try with key A if B was unsuccessful
1593+ try :
1594+ self .cmd .mf1_read_one_block (4 * s , MfcKeyType .A , key )
1595+ keyA = key
1596+ except UnexpectedResponseError :
1597+ pass
1598+ # both keys were found, no need to continue iterating
1599+ if keyA and keyB :
1600+ break
1601+
1602+ if not keyA and not keyB :
1603+ print (f"Warning: No key found for sector { s } , skipping" )
1604+ continue
1605+
1606+ # iterate over blocks
1607+ for b in range (4 ):
1608+ block_data = buffer [(4 * s + b )* 16 :(4 * s + b + 1 )* 16 ]
1609+ if not block_data or len (block_data ) != 16 :
1610+ print (f"Warning: Invalid block data for block { 4 * s + b } , skipping" )
1611+ continue
1612+
1613+ # special case for last block of each sector (sector trailer)
1614+ if b == 3 and not args .clone_access :
1615+ # if option is not specified, use generic ACL to be able to write again
1616+ block_data = block_data [:6 ] + bytes .fromhex ("08778F" ) + block_data [9 :]
1617+
1618+ # try writing with available keys
1619+ written = False
1620+ if keyB :
1621+ try :
1622+ self .cmd .mf1_write_one_block (4 * s + b , MfcKeyType .B , keyB , block_data )
1623+ written = True
1624+ except Exception as e :
1625+ pass
1626+
1627+ if not written and keyA :
1628+ try :
1629+ self .cmd .mf1_write_one_block (4 * s + b , MfcKeyType .A , keyA , block_data )
1630+ written = True
1631+ except Exception as e :
1632+ pass
1633+
1634+ if not written :
1635+ print (f"Warning: Failed to write block { 4 * s + b } " )
1636+ else :
1637+ print (f"Wrote block { 4 * s + b } " )
1638+
1639+ print ("Clone operation completed" )
1640+
1641+
1642+
14521643@hf_mf .command ('rdbl' )
14531644class HFMFRDBL (MF1AuthArgsUnit ):
14541645 def args_parser (self ) -> ArgumentParserNoExit :
@@ -1693,32 +1884,34 @@ def _run_mfkey32v2(items):
16931884
16941885
16951886class ItemGenerator :
1696- def __init__ (self , rs , i = 0 , j = 1 ):
1697- self .rs = rs
1887+ def __init__ (self , rs , uid_found_keys = set ()):
1888+ self .rs : list = rs
1889+ self .progress = 0
16981890 self .i = 0
16991891 self .j = 1
17001892 self .found = set ()
17011893 self .keys = set ()
1894+ for known_key in uid_found_keys :
1895+ self .test_key (known_key )
17021896
17031897 def __iter__ (self ):
17041898 return self
17051899
17061900 def __next__ (self ):
1707- try :
1708- item_i = self .rs [self .i ]
1709- except IndexError :
1710- raise StopIteration
1711- if self .key_from_item (item_i ) in self .found :
1901+ size = len (self .rs )
1902+ if self .j >= size :
17121903 self .i += 1
1904+ if self .i >= size - 1 :
1905+ raise StopIteration
17131906 self .j = self .i + 1
1714- return next (self )
1715- try :
1716- item_j = self .rs [self .j ]
1717- except IndexError :
1907+ item_i , item_j = self .rs [self .i ], self .rs [self .j ]
1908+ self .progress += 1
1909+ self .j += 1
1910+ if self .key_from_item (item_i ) in self .found :
1911+ self .progress += max (0 , size - self .j )
17181912 self .i += 1
17191913 self .j = self .i + 1
17201914 return next (self )
1721- self .j += 1
17221915 if self .key_from_item (item_j ) in self .found :
17231916 return next (self )
17241917 return item_i , item_j
@@ -1727,16 +1920,20 @@ def __next__(self):
17271920 def key_from_item (item ):
17281921 return "{uid}-{nt}-{nr}-{ar}" .format (** item )
17291922
1730- def key_found (self , key , items ):
1731- self .keys .add (key )
1732- for item in items :
1733- try :
1734- if item == self .rs [self .i ]:
1735- self .i += 1
1736- self .j = self .i + 1
1737- except IndexError :
1738- break
1739- self .found .update (self .key_from_item (item ) for item in items )
1923+ def test_key (self , key , items = list ()):
1924+ for item in self .rs :
1925+ item_key = self .key_from_item (item )
1926+ if item_key in self .found :
1927+ continue
1928+ if (item in items ) or (Crypto1 .mfkey32_is_reader_has_key (
1929+ int (item ['uid' ], 16 ),
1930+ int (item ['nt' ], 16 ),
1931+ int (item ['nr' ], 16 ),
1932+ int (item ['ar' ], 16 ),
1933+ key ,
1934+ )):
1935+ self .keys .add (key )
1936+ self .found .add (item_key )
17401937
17411938
17421939@hf_mf .command ('elog' )
@@ -1749,7 +1946,7 @@ def args_parser(self) -> ArgumentParserNoExit:
17491946 parser .add_argument ('--decrypt' , action = 'store_true' , help = "Decrypt key from MF1 log list" )
17501947 return parser
17511948
1752- def decrypt_by_list (self , rs : list ):
1949+ def decrypt_by_list (self , rs : list , uid_found_keys : set = set () ):
17531950 """
17541951 Decrypt key from reconnaissance log list
17551952
@@ -1759,16 +1956,14 @@ def decrypt_by_list(self, rs: list):
17591956 msg1 = f" > { len (rs )} records => "
17601957 msg2 = f"/{ (len (rs )* (len (rs )- 1 ))// 2 } combinations. "
17611958 msg3 = " key(s) found"
1762- n = 1
1763- gen = ItemGenerator ( rs )
1959+ gen = ItemGenerator ( rs , uid_found_keys )
1960+ print ( f" { msg1 } { gen . progress } { msg2 } { len ( gen . keys ) } { msg3 } \r " , end = "" )
17641961 with Pool (cpu_count ()) as pool :
17651962 for result in pool .imap (_run_mfkey32v2 , gen ):
1766- # TODO: if some keys already recovered, test them on item before running mfkey32 on item
17671963 if result is not None :
1768- gen .key_found (* result )
1769- print (f"{ msg1 } { n } { msg2 } { len (gen .keys )} { msg3 } \r " , end = "" )
1770- n += 1
1771- print ()
1964+ gen .test_key (* result )
1965+ print (f"{ msg1 } { gen .progress } { msg2 } { len (gen .keys )} { msg3 } \r " , end = "" )
1966+ print (f"{ msg1 } { gen .progress } { msg2 } { len (gen .keys )} { msg3 } " )
17721967 return gen .keys
17731968
17741969 def on_exec (self , args : argparse .Namespace ):
@@ -1810,21 +2005,13 @@ def on_exec(self, args: argparse.Namespace):
18102005 print (f" - Detection log for uid [{ uid .upper ()} ]" )
18112006 result_maps_for_uid = result_maps [uid ]
18122007 for block in result_maps_for_uid :
1813- print (f" > Block { block } detect log decrypting..." )
1814- if 'A' in result_maps_for_uid [block ]:
1815- # print(f" - A record: { result_maps[block]['A'] }")
1816- records = result_maps_for_uid [block ]['A' ]
1817- if len (records ) > 1 :
1818- result_maps [uid ][block ]['A' ] = self .decrypt_by_list (records )
1819- else :
1820- print (f" > { len (records )} record" )
1821- if 'B' in result_maps_for_uid [block ]:
1822- # print(f" - B record: { result_maps[block]['B'] }")
1823- records = result_maps_for_uid [block ]['B' ]
1824- if len (records ) > 1 :
1825- result_maps [uid ][block ]['B' ] = self .decrypt_by_list (records )
1826- else :
1827- print (f" > { len (records )} record" )
2008+ for keyType in 'AB' :
2009+ records = result_maps_for_uid [block ][keyType ] if keyType in result_maps_for_uid [block ] else []
2010+ if len (records ) < 1 :
2011+ continue
2012+ print (f" > Decrypting block { block } key { keyType } detect log..." )
2013+ result_maps [uid ][block ][keyType ] = self .decrypt_by_list (records , uid_found_keys )
2014+ uid_found_keys .update (result_maps [uid ][block ][keyType ])
18282015 print (" > Result ---------------------------" )
18292016 for block in result_maps_for_uid .keys ():
18302017 if 'A' in result_maps_for_uid [block ]:
0 commit comments