Skip to content

Commit 0352806

Browse files
committed
hf mf elog --decrypt skip records with found keys
1 parent 8a797b8 commit 0352806

File tree

4 files changed

+237
-46
lines changed

4 files changed

+237
-46
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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+
- `hf mf elog --decrypt` skip records with found keys (@taichunmin)
67
- Added command to check keys of multiple sectors at once (@taichunmin)
78
- Fixed unused target key type parameter for nested (@petepriority)
89
- Skip already used items `hf mf elog --decrypt` (@p-l-)

software/script/chameleon_cli_unit.py

+44-46
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from chameleon_enum import Command, Status, SlotNumber, TagSenseType, TagSpecificType
2626
from chameleon_enum import MifareClassicWriteMode, MifareClassicPrngType, MifareClassicDarksideStatus, MfcKeyType
2727
from chameleon_enum import AnimationMode, ButtonPressFunction, ButtonType, MfcValueBlockOperator
28+
from crypto1 import Crypto1
2829

2930
# NXP IDs based on https://www.nxp.com/docs/en/application-note/AN10833.pdf
3031
type_id_SAK_dict = {0x00: "MIFARE Ultralight Classic/C/EV1/Nano | NTAG 2xx",
@@ -1240,32 +1241,34 @@ def _run_mfkey32v2(items):
12401241

12411242

12421243
class ItemGenerator:
1243-
def __init__(self, rs, i=0, j=1):
1244-
self.rs = rs
1244+
def __init__(self, rs, uid_found_keys = set()):
1245+
self.rs: list = rs
1246+
self.progress = 0
12451247
self.i = 0
12461248
self.j = 1
12471249
self.found = set()
12481250
self.keys = set()
1251+
for known_key in uid_found_keys:
1252+
self.test_key(known_key)
12491253

12501254
def __iter__(self):
12511255
return self
12521256

12531257
def __next__(self):
1254-
try:
1255-
item_i = self.rs[self.i]
1256-
except IndexError:
1257-
raise StopIteration
1258-
if self.key_from_item(item_i) in self.found:
1258+
size = len(self.rs)
1259+
if self.j >= size:
12591260
self.i += 1
1261+
if self.i >= size - 1:
1262+
raise StopIteration
12601263
self.j = self.i + 1
1261-
return next(self)
1262-
try:
1263-
item_j = self.rs[self.j]
1264-
except IndexError:
1264+
item_i, item_j = self.rs[self.i], self.rs[self.j]
1265+
self.progress += 1
1266+
self.j += 1
1267+
if self.key_from_item(item_i) in self.found:
1268+
self.progress += max(0, size - self.j)
12651269
self.i += 1
12661270
self.j = self.i + 1
12671271
return next(self)
1268-
self.j += 1
12691272
if self.key_from_item(item_j) in self.found:
12701273
return next(self)
12711274
return item_i, item_j
@@ -1274,17 +1277,20 @@ def __next__(self):
12741277
def key_from_item(item):
12751278
return "{uid}-{nt}-{nr}-{ar}".format(**item)
12761279

1277-
def key_found(self, key, items):
1278-
self.keys.add(key)
1279-
for item in items:
1280-
try:
1281-
if item == self.rs[self.i]:
1282-
self.i += 1
1283-
self.j = self.i + 1
1284-
except IndexError:
1285-
break
1286-
self.found.update(self.key_from_item(item) for item in items)
1287-
1280+
def test_key(self, key, items = list()):
1281+
for item in self.rs:
1282+
item_key = self.key_from_item(item)
1283+
if item_key in self.found:
1284+
continue
1285+
if (item in items) or (Crypto1.mfkey32_is_reader_has_key(
1286+
int(item['uid'], 16),
1287+
int(item['nt'], 16),
1288+
int(item['nr'], 16),
1289+
int(item['ar'], 16),
1290+
key,
1291+
)):
1292+
self.keys.add(key)
1293+
self.found.add(item_key)
12881294

12891295
@hf_mf.command('elog')
12901296
class HFMFELog(DeviceRequiredUnit):
@@ -1296,7 +1302,7 @@ def args_parser(self) -> ArgumentParserNoExit:
12961302
parser.add_argument('--decrypt', action='store_true', help="Decrypt key from MF1 log list")
12971303
return parser
12981304

1299-
def decrypt_by_list(self, rs: list):
1305+
def decrypt_by_list(self, rs: list, uid_found_keys: set = set()):
13001306
"""
13011307
Decrypt key from reconnaissance log list
13021308
@@ -1306,16 +1312,14 @@ def decrypt_by_list(self, rs: list):
13061312
msg1 = f" > {len(rs)} records => "
13071313
msg2 = f"/{(len(rs)*(len(rs)-1))//2} combinations. "
13081314
msg3 = " key(s) found"
1309-
n = 1
1310-
gen = ItemGenerator(rs)
1315+
gen = ItemGenerator(rs, uid_found_keys)
1316+
print(f"{msg1}{gen.progress}{msg2}{len(gen.keys)}{msg3}\r", end="")
13111317
with Pool(cpu_count()) as pool:
13121318
for result in pool.imap(_run_mfkey32v2, gen):
1313-
# TODO: if some keys already recovered, test them on item before running mfkey32 on item
13141319
if result is not None:
1315-
gen.key_found(*result)
1316-
print(f"{msg1}{n}{msg2}{len(gen.keys)}{msg3}\r", end="")
1317-
n += 1
1318-
print()
1320+
gen.test_key(*result)
1321+
print(f"{msg1}{gen.progress}{msg2}{len(gen.keys)}{msg3}\r", end="")
1322+
print(f"{msg1}{gen.progress}{msg2}{len(gen.keys)}{msg3}")
13191323
return gen.keys
13201324

13211325
def on_exec(self, args: argparse.Namespace):
@@ -1356,22 +1360,16 @@ def on_exec(self, args: argparse.Namespace):
13561360
for uid in result_maps.keys():
13571361
print(f" - Detection log for uid [{uid.upper()}]")
13581362
result_maps_for_uid = result_maps[uid]
1363+
uid_found_keys = set()
13591364
for block in result_maps_for_uid:
1360-
print(f" > Block {block} detect log decrypting...")
1361-
if 'A' in result_maps_for_uid[block]:
1362-
# print(f" - A record: { result_maps[block]['A'] }")
1363-
records = result_maps_for_uid[block]['A']
1364-
if len(records) > 1:
1365-
result_maps[uid][block]['A'] = self.decrypt_by_list(records)
1366-
else:
1367-
print(f" > {len(records)} record")
1368-
if 'B' in result_maps_for_uid[block]:
1369-
# print(f" - B record: { result_maps[block]['B'] }")
1370-
records = result_maps_for_uid[block]['B']
1371-
if len(records) > 1:
1372-
result_maps[uid][block]['B'] = self.decrypt_by_list(records)
1373-
else:
1374-
print(f" > {len(records)} record")
1365+
for keyType in 'AB':
1366+
records = result_maps_for_uid[block][keyType] if keyType in result_maps_for_uid[block] else []
1367+
if len(records) < 1:
1368+
continue
1369+
print(f" > Decrypting block {block} key {keyType} detect log...")
1370+
result_maps[uid][block][keyType] = self.decrypt_by_list(records, uid_found_keys)
1371+
uid_found_keys.update(result_maps[uid][block][keyType])
1372+
13751373
print(" > Result ---------------------------")
13761374
for block in result_maps_for_uid.keys():
13771375
if 'A' in result_maps_for_uid[block]:

software/script/crypto1.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import re
2+
3+
LFSR48_FILTER_A = 0x9E98
4+
LFSR48_FILTER_B = 0xB48E
5+
LFSR48_FILTER_C = 0xEC57E80A
6+
LFSR48_POLY = 0xE882B0AD621
7+
U8_TO_ODD4 = [((i & 0x80) >> 4) + ((i & 0x20) >> 3) + ((i & 0x08) >> 2) + ((i & 0x02) >> 1) for i in range(256)]
8+
EVEN_PARITY_U8 = [0 for i in range(256)]
9+
10+
def u8_to_odd4(u8):
11+
return U8_TO_ODD4[u8 & 0xFF]
12+
13+
def get_bit(num, x = 0):
14+
return (num >> x) & 1
15+
16+
for i in range(256):
17+
tmp = i
18+
tmp ^= tmp >> 4
19+
tmp ^= tmp >> 2
20+
EVEN_PARITY_U8[i] = (tmp ^ (tmp >> 1)) & 1
21+
22+
def even_parity_u8(u8):
23+
return EVEN_PARITY_U8[u8 & 0xFF]
24+
25+
def odd_parity_u8(u8):
26+
return even_parity_u8(u8) ^ 1
27+
28+
def even_parity_u16(u16):
29+
return even_parity_u8((u16 >> 8) ^ u16)
30+
31+
def even_parity_u48(u48):
32+
return even_parity_u16((u48 >> 32) ^ (u48 >> 16) ^ u48)
33+
34+
def swap_endian_u16(u16):
35+
return ((u16 & 0xFF) << 8) | ((u16 >> 8) & 0xFF)
36+
37+
def swap_endian_u32(u32):
38+
return swap_endian_u16(u32 & 0xFFFF) << 16 | swap_endian_u16((u32 >> 16) & 0xFFFF)
39+
40+
"""
41+
ref: https://web.archive.org/web/20081010065744/http://sar.informatik.hu-berlin.de/research/publications/SAR-PR-2008-21/SAR-PR-2008-21_.pdf
42+
"""
43+
class Crypto1:
44+
def __init__(self, new_lfsr48: int = 0):
45+
self.lfsr48 = new_lfsr48
46+
47+
@property
48+
def key(self) -> bytearray:
49+
tmp, key = self.lfsr48, bytearray(6)
50+
for i in range(6):
51+
key[i] = tmp & 0xFF
52+
tmp >>= 8
53+
return key.hex()
54+
55+
@key.setter
56+
def key(self, key: str):
57+
if not re.match(r"^[a-fA-F0-9]{12}$", key):
58+
raise ValueError(f"Invalid hex format key: {key}")
59+
tmp, self.lfsr48 = int(key, 16), 0
60+
for i in range(6):
61+
self.lfsr48 = (self.lfsr48 << 8) | tmp & 0xFF
62+
tmp >>= 8
63+
64+
def lfsr48_filter(self):
65+
f = 0
66+
f |= get_bit(LFSR48_FILTER_B, u8_to_odd4(self.lfsr48 >> 8)) # fb4
67+
f |= get_bit(LFSR48_FILTER_A, u8_to_odd4(self.lfsr48 >> 16)) << 1 # fa4
68+
f |= get_bit(LFSR48_FILTER_A, u8_to_odd4(self.lfsr48 >> 24)) << 2 # fa4
69+
f |= get_bit(LFSR48_FILTER_B, u8_to_odd4(self.lfsr48 >> 32)) << 3 # fb4
70+
f |= get_bit(LFSR48_FILTER_A, u8_to_odd4(self.lfsr48 >> 40)) << 4 # fa4
71+
return get_bit(LFSR48_FILTER_C, f)
72+
73+
def lfsr48_bit(self, bit_in: int = 0, is_encrypted: bool = False) -> int:
74+
out_bit = self.lfsr48_filter()
75+
bit_feedback = even_parity_u48(LFSR48_POLY & self.lfsr48) ^ (bit_in & 1) ^ (is_encrypted & out_bit)
76+
self.lfsr48 = (bit_feedback << 47) | (self.lfsr48 >> 1)
77+
return out_bit
78+
79+
def lfsr48_u8(self, u8_in: int = 0, is_encrypted: bool = False) -> int:
80+
out_u8 = 0
81+
for i in range(8):
82+
tmp = self.lfsr48_bit(u8_in >> i, is_encrypted) << i
83+
out_u8 |= tmp
84+
return out_u8
85+
86+
def lfsr48_u32(self, u32_in: int = 0, is_encrypted: bool = False) -> int:
87+
out_u32 = 0
88+
for i in range(3, -1, -1):
89+
bit_offset = i << 3
90+
out_u32 |= self.lfsr48_u8(u32_in >> bit_offset, is_encrypted) << bit_offset
91+
return out_u32
92+
93+
@staticmethod
94+
def prng_next(lfsr32: int, n: int = 1) -> int:
95+
lfsr32 = swap_endian_u32(lfsr32)
96+
for i in range(n):
97+
lfsr32 = even_parity_u8(0x2D & (lfsr32 >> 16)) << 31 | (lfsr32 >> 1)
98+
return swap_endian_u32(lfsr32)
99+
100+
@staticmethod
101+
def mfkey32_is_reader_has_key(uid: int, nt: int, nrEnc: int, arEnc: int, key: str) -> bool:
102+
state = Crypto1()
103+
state.key = key
104+
state.lfsr48_u32(uid ^ nt, False) # ks0
105+
state.lfsr48_u32(nrEnc, True) # ks1
106+
ks2 = state.lfsr48_u32(0, False) # ks2
107+
ar = arEnc ^ ks2
108+
result = ar == Crypto1.prng_next(nt, 64)
109+
# print(f'uid: {hex(uid)}, nt: {hex(nt)}, nrEnc: {hex(nrEnc)}, arEnc: {hex(arEnc)}, key: {key}, result = {result}')
110+
return result

software/script/tests/test_crypto1.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/usr/bin/env python3
2+
import os, sys, unittest
3+
4+
CURRENT_DIR = os.path.split(os.path.abspath(__file__))[0]
5+
config_path = CURRENT_DIR.rsplit(os.sep, 1)[0]
6+
sys.path.append(config_path)
7+
print(config_path)
8+
from crypto1 import Crypto1
9+
10+
class TestCrypto1(unittest.TestCase):
11+
12+
def test_key_getter_setter(self):
13+
state = Crypto1()
14+
state.key = 'a0a1a2a3a4a5'
15+
self.assertEqual(state.key, 'a0a1a2a3a4a5')
16+
17+
def test_prng_next(self):
18+
self.assertEqual(Crypto1.prng_next(0x2C198BE4, 64), 0xCC14C013)
19+
20+
def test_reader_three_pass_auth(self):
21+
uid, nt, nr, atEnc = 0x65535D33, 0xBE2B7B5D, 0x0B4271BA, 0x36081500
22+
reader = Crypto1()
23+
reader.key = '974C262B9278'
24+
ks0 = reader.lfsr48_u32(uid ^ nt, False)
25+
self.assertEqual(ks0, 0xAC93C1A4, 'ks0 assert failed')
26+
ks1 = reader.lfsr48_u32(nr, False)
27+
self.assertEqual(ks1, 0xBAA3C92B, 'ks1 assert failed')
28+
nrEnc = nr ^ ks1
29+
self.assertEqual(nrEnc, 0xB1E1B891, 'nrEnc assert failed')
30+
ar = Crypto1.prng_next(nt, 64)
31+
self.assertEqual(ar, 0xF0928568, 'ar assert failed')
32+
ks2 = reader.lfsr48_u32(0, False)
33+
self.assertEqual(ks2, 0xDC652720, 'ks2 assert failed')
34+
arEnc = ar ^ ks2
35+
self.assertEqual(arEnc, 0x2CF7A248, 'arEnc assert failed')
36+
ks3 = reader.lfsr48_u32(0, False)
37+
self.assertEqual(ks3, 0xC6F4A093, 'ks3 assert failed')
38+
at = atEnc ^ ks3
39+
nt96 = Crypto1.prng_next(nt, 96)
40+
self.assertEqual(at, nt96, 'at assert failed')
41+
42+
def test_tag_three_pass_auth(self):
43+
uid, nt, nrEnc, arEnc = 0x65535D33, 0xBE2B7B5D, 0xB1E1B891, 0x2CF7A248
44+
tag = Crypto1()
45+
tag.key = '974C262B9278'
46+
ks0 = tag.lfsr48_u32(uid ^ nt, False)
47+
self.assertEqual(ks0, 0xAC93C1A4, 'ks0 assert failed')
48+
ks1 = tag.lfsr48_u32(nrEnc, True)
49+
self.assertEqual(ks1, 0xBAA3C92B, 'ks1 assert failed')
50+
nr = ks1 ^ nrEnc
51+
self.assertEqual(nr, 0x0B4271BA, 'nr assert failed')
52+
ks2 = tag.lfsr48_u32(0, False)
53+
self.assertEqual(ks2, 0xDC652720, 'ks2 assert failed')
54+
ar = ks2 ^ arEnc
55+
self.assertEqual(ar, 0xF0928568, 'ar assert failed')
56+
at = Crypto1.prng_next(nt, 96)
57+
self.assertEqual(at, 0xF0FCB593, 'at assert failed')
58+
ks3 = tag.lfsr48_u32(0, False)
59+
self.assertEqual(ks3, 0xC6F4A093, 'ks3 assert failed')
60+
atEnc = at ^ ks3
61+
self.assertEqual(atEnc, 0x36081500, 'atEnc assert failed')
62+
63+
def test_mfkey32_is_reader_has_key_true(self):
64+
self.assertTrue(Crypto1.mfkey32_is_reader_has_key(
65+
uid = 0x65535D33,
66+
nt = 0x2C198BE4,
67+
nrEnc = 0xFEDAC6D2,
68+
arEnc = 0xCF0A3C7E,
69+
key = 'A9AC67832330'
70+
))
71+
72+
def test_mfkey32_is_reader_has_key_false(self):
73+
self.assertFalse(Crypto1.mfkey32_is_reader_has_key(
74+
uid = 0x65535D33,
75+
nt = 0x2C198BE4,
76+
nrEnc = 0xFEDAC6D2,
77+
arEnc = 0xCF0A3C7E,
78+
key = 'FFFFFFFFFFFF'
79+
))
80+
81+
if __name__ == '__main__':
82+
unittest.main()

0 commit comments

Comments
 (0)