Skip to content

Commit 50bc73d

Browse files
committed
1 parent 303d2d3 commit 50bc73d

File tree

7 files changed

+687
-149
lines changed

7 files changed

+687
-149
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file.
33
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
44

55
## [unreleased][unreleased]
6+
- Enhanced Nested attack with additional parity bit check to reduce possible keys (@20321587)
7+
- `hf mf elog --decrypt` skip records with found keys (@taichunmin)
68
- Added `firmware/docker-compose.yml` to build firmware in local docker (@taichunmin)
79
- Added cmd to acquire nonces for hardnested(Protocol doc need update) (@xianglin1998)
810
- Added command to check keys of multiple sectors at once (@taichunmin)

firmware/application/src/rfid/reader/hf/mf1_toolbox.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,7 @@ static uint8_t nested_recover_core(mf1_nested_core_t *pnc, uint64_t keyKnown, ui
861861
pnc->par |= ((oddparity8(answer[0]) != parity[0]) << 0);
862862
pnc->par |= ((oddparity8(answer[1]) != parity[1]) << 1);
863863
pnc->par |= ((oddparity8(answer[2]) != parity[2]) << 2);
864+
pnc->par |= ((oddparity8(answer[3]) != parity[3]) << 3);
864865
return STATUS_HF_TAG_OK;
865866
}
866867

software/script/chameleon_cli_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def get_prompt(self):
6868
6969
:return: current cmd prompt
7070
"""
71-
device_string = f"{CG}USB" if self.device_com.isOpen(
71+
device_string = f"{CG}USB" if self.device_com.is_open(
7272
) else f"{CR}Offline"
7373
status = f"[{device_string}{C0}] chameleon --> "
7474
return status

software/script/chameleon_cli_unit.py

Lines changed: 238 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -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):
519519
class 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')
14531644
class HFMFRDBL(MF1AuthArgsUnit):
14541645
def args_parser(self) -> ArgumentParserNoExit:
@@ -1693,32 +1884,34 @@ def _run_mfkey32v2(items):
16931884

16941885

16951886
class 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

Comments
 (0)