Compare commits

...

13 Commits

Author SHA1 Message Date
47LeCoste
d5b46ffefb Update .gitignore
All checks were successful
Build Dev Firmware / build (push) Successful in 6m33s
2026-03-20 15:53:17 +00:00
D4rk$1d3
9d2298114c Update application.fam
All checks were successful
Build Dev Firmware / build (push) Successful in 6m32s
2026-03-18 19:18:49 -03:00
D4rk$1d3
b93a970647 Delete applications/main/KeylessGoSniffer directory 2026-03-18 19:17:43 -03:00
47LeCoste
c6265ea29b Delete CHANGELOG.md
All checks were successful
Build Dev Firmware / build (push) Successful in 10m8s
2026-03-18 19:41:47 +00:00
47LeCoste
8e0a81b89d Update fiat_marelli.h 2026-03-18 19:40:46 +00:00
grugnoymeme
6f39fd4803 removed problematics and fixed f3
All checks were successful
Build Dev Firmware / build (push) Successful in 6m34s
2026-03-18 19:45:16 +01:00
grugnoymeme
41d10f9b3d Merge remote-tracking branch 'refs/remotes/origin/main' 2026-03-18 19:26:36 +01:00
grugnoymeme
1f97aa2e3c reduced datarate for F3, renamed 1,2,A1,F1,F3, introduced AU_1,RF_1 for test 2026-03-18 19:26:21 +01:00
D4rk$1d3
5b9038173b Update README.md 2026-03-18 15:04:38 -03:00
grugnoymeme
fde0a57595 forgtten to close a }
All checks were successful
Build Dev Firmware / build (push) Successful in 6m30s
2026-03-18 16:23:59 +01:00
MX
3fb40944e6 NFC: Add Mifare Ultralight C Write Support
by haw8411
2026-03-18 16:16:04 +01:00
grugnoymeme
e61cfa765a bft force seed value @MMX 2026-03-18 16:07:20 +01:00
grugnoymeme
fd0dd6c324 subghz fix very big issue with tx on read screen @MMX 2026-03-18 16:04:22 +01:00
20 changed files with 362 additions and 1148 deletions

View File

@@ -1,66 +0,0 @@
## Main changes
- Current API: 87.6
* SubGHz: Signal Settings Improvements (PR #968 | by @Dmitry422)
* Apps: Build tag (**17feb2026**) - **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev)
## Other changes
* MFKey: Update to v4.1 (by @noproto & @dchristle)
<br><br>
#### Known NFC post-refactor regressions list:
- Mifare Mini clones reading is broken (original mini working fine) (OFW)
- While reading some EMV capable cards via NFC->Read flipper may crash due to Desfire poller issue, read those cards via Extra actions->Read specific card type->EMV
----
[-> How to install firmware](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/HowToInstall.md)
[-> Unleashed FW Web Installer](https://web.unleashedflip.com)
## Please support development of the project
| Service | Remark | QR Code | Link/Wallet |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| <img src="https://cdn.simpleicons.org/patreon/dark/white" alt="Patreon" width="14"/> **Patreon** | | <div align="center"><a href="https://github.com/user-attachments/assets/a88a90a5-28c3-40b4-864a-0c0b79494a42"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | [patreon.com/mmxdev](https://patreon.com/mmxdev) |
| <img src="https://cdn.simpleicons.org/boosty" alt="Boosty" width="14"/> **Boosty** | patreon alternative | <div align="center"><a href="https://github.com/user-attachments/assets/893c0760-f738-42c1-acaa-916019a7bdf8"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | [boosty.to/mmxdev](https://boosty.to/mmxdev) |
| <img src="https://gist.githubusercontent.com/m-xim/255a3ef36c886dec144a58864608084c/raw/71da807b4abbd1582e511c9ea30fad27f78d642a/cloudtips_icon.svg" alt="Cloudtips" width="14"/> CloudTips | only RU payments accepted | <div align="center"><a href="https://github.com/user-attachments/assets/5de31d6a-ef24-4d30-bd8e-c06af815332a"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | [pay.cloudtips.ru/p/7b3e9d65](https://pay.cloudtips.ru/p/7b3e9d65) |
| <img src="https://raw.githubusercontent.com/gist/PonomareVlad/55c8708f11702b4df629ae61129a9895/raw/1657350724dab66f2ad68ea034c480a2df2a1dfd/YooMoney.svg" alt="YooMoney" width="14"/> YooMoney | only RU payments accepted | <div align="center"><a href="https://github.com/user-attachments/assets/33454f79-074b-4349-b453-f94fdadc3c68"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | [yoomoney.ru/fundraise/XA49mgQLPA0.221209](https://yoomoney.ru/fundraise/XA49mgQLPA0.221209) |
| <img src="https://cdn.simpleicons.org/tether" alt="USDT" width="14"/> USDT | TRC20 | <div align="center"><a href="https://github.com/user-attachments/assets/0500498d-18ed-412d-a1a4-8a66d0b6f057"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs` |
| <img src="https://cdn.simpleicons.org/ethereum" alt="ETH" width="14"/> ETH | BSC/ERC20-Tokens | <div align="center"><a href="https://github.com/user-attachments/assets/0f323e98-c524-4f41-abb2-f4f1cec83ab6"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a` |
| <img src="https://cdn.simpleicons.org/bitcoin" alt="BTC" width="14"/> BTC | | <div align="center"><a href="https://github.com/user-attachments/assets/5a904d45-947e-4b92-9f0f-7fbaaa7b37f8"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9` |
| <img src="https://cdn.simpleicons.org/solana" alt="SOL" width="13"/> SOL | Solana/Tokens | <div align="center"><a href="https://github.com/user-attachments/assets/ab33c5e0-dd59-497b-9c91-ceb89c36b34d"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8` |
| <img src="https://cdn.simpleicons.org/dogecoin" alt="DOGE" width="14"/> DOGE | | <div align="center"><a href="https://github.com/user-attachments/assets/2937edd0-5c85-4465-a444-14d4edb481c0"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `D6R6gYgBn5LwTNmPyvAQR6bZ9EtGgFCpvv` |
| <img src="https://cdn.simpleicons.org/litecoin" alt="LTC" width="14"/> LTC | | <div align="center"><a href="https://github.com/user-attachments/assets/441985fe-f028-4400-83c1-c215760c1e74"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `ltc1q3ex4ejkl0xpx3znwrmth4lyuadr5qgv8tmq8z9` |
| <img src="https://bitcoincash.org/img/green/bitcoin-cash-circle.svg" alt="BCH" width="14"/> BCH | | <div align="center"><a href="https://github.com/user-attachments/assets/7f365976-19a3-4777-b17e-4bfba5f69eff"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `qquxfyzntuqufy2dx0hrfr4sndp0tucvky4sw8qyu3` |
| <img src="https://cdn.simpleicons.org/monero" alt="XMR" width="14"/> XMR | Monero | <div align="center"><a href="https://github.com/user-attachments/assets/96186c06-61e7-4b4d-b716-6eaf1779bfd8"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `41xUz92suUu1u5Mu4qkrcs52gtfpu9rnZRdBpCJ244KRHf6xXSvVFevdf2cnjS7RAeYr5hn9MsEfxKoFDRSctFjG5fv1Mhn` |
| <img src="https://cdn.simpleicons.org/ton" alt="TON" width="14"/> TON | | <div align="center"><a href="https://github.com/user-attachments/assets/92a57e57-7462-42b7-a342-6f22c6e600c1"><img src="https://github.com/user-attachments/assets/da3a864d-d1c7-42cc-8a86-6fcaf26663ec" alt="QR image"/></a></div> | `UQCOqcnYkvzOZUV_9bPE_8oTbOrOF03MnF-VcJyjisTZmsxa` |
#### Thanks to our sponsors who supported project in the past and special thanks to sponsors who supports us on regular basis:
@mishamyte, ClaraCrazy, Pathfinder [Count Zero cDc], callmezimbra, Quen0n, MERRON, grvpvl (lvpvrg), art_col, ThurstonWaffles, Moneron, UterGrooll, LUCFER, Northpirate, zloepuzo, T.Rat, Alexey B., ionelife, ...
and all other great people who supported our project and me (xMasterX), thanks to you all!
## **Recommended update option - Web Updater**
### What `e`, ` `, `c` means? What I need to download if I don't want to use Web updater?
What build I should download and what this name means - `flipper-z-f7-update-(version)(e / c).tgz` ? <br>
`flipper-z` = for Flipper Zero device<br>
`f7` = Hardware version - same for all flipper zero devices<br>
`update` = Update package, contains updater, all assets (plugins, IR libs, etc.), and firmware itself<br>
`(version)` = Firmware version<br>
| Designation | [Base Apps](https://github.com/xMasterX/all-the-plugins#default-pack) | [Extra Apps](https://github.com/xMasterX/all-the-plugins#extra-pack) |
|-----|:---:|:---:|
| ` ` | ✅ | |
| `c` | | |
| `e` | ✅ | ✅ |
**To enable RGB Backlight support go into LCD & Notifications settings**
RGB backlight [hardware mod](https://github.com/quen0n/flipperzero-firmware-rgb#readme), works only on modded flippers! do not enable on non modded device!
Firmware Self-update package (update from microSD) - `flipper-z-f7-update-(version).tgz` for mobile app / qFlipper / web<br>
Archive of `scripts` folder (contains scripts for FW/plugins development) - `flipper-z-any-scripts-(version).tgz`<br>
SDK files for plugins development and uFBT - `flipper-z-f7-sdk-(version).zip`

View File

@@ -182,7 +182,7 @@ Contributions are welcome if they:
> Non-automotive features are considered out-of-scope for now.
### This code is a mess!
![Talk is cheap, submit patches](arf_pictures/send_patches.jpeg)
![Talk is cheap, submit patches](.arf_pictures/send_patches.jpeg)
---
## Citations & References

View File

@@ -1,11 +0,0 @@
App(
appid="lf_sniffer",
name="KeylessGO Sniffer",
apptype=FlipperAppType.MENUEXTERNAL,
entry_point="lf_sniffer_app",
requires=["gui", "notification", "storage"],
stack_size=4 * 1024,
fap_category="GPIO",
fap_version=(1, 0),
fap_description="LF 125kHz challenge sniffer for KeylessGO analysis",
)

View File

@@ -1,506 +0,0 @@
#!/usr/bin/env python3
"""
LF Analyzer -- Decodes captures from the LF Sniffer FAP
=========================================================
Analyzes edge timings captured by the Flipper Zero FAP and determines:
- Modulation type (ASK/FSK/PSK)
- Encoding (Manchester, Biphase, NRZ, PWM)
- Bit period and carrier frequency
- Decoded bit stream
- Likely protocol (Hitag2, Hitag S, EM4100, etc.)
- KeylessGO challenge candidate if applicable
Usage:
python3 lf_analyze.py capture_0000.csv
python3 lf_analyze.py capture_0000.csv --verbose
python3 lf_analyze.py capture_0000.csv --protocol hitag2
"""
import argparse
import csv
import sys
import math
from collections import Counter
from pathlib import Path
# -- CSV loader ----------------------------------------------------------------
def load_csv(path: str) -> tuple[list, dict]:
edges = []
meta = {}
with open(path, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('#'):
# Parse key:value pairs from metadata comment lines
if 'edges:' in line:
for token in line.split():
if ':' in token:
k, v = token.split(':', 1)
try:
meta[k.lstrip('#')] = int(v)
except ValueError:
pass
continue
if line.startswith('index'):
continue # skip column header
parts = line.split(',')
if len(parts) >= 3:
try:
edges.append({
'idx': int(parts[0]),
'dur_us': int(parts[1]),
'level': int(parts[2]),
'note': parts[3].strip() if len(parts) > 3 else ''
})
except ValueError:
pass
print(f"[*] Loaded {len(edges)} edges")
if meta:
print(f" Metadata: {meta}")
return edges, meta
# -- Carrier frequency detection ----------------------------------------------
def analyze_carrier(edges: list, verbose: bool = False) -> dict:
"""
Detects the LF carrier by measuring the shortest pulses in the capture.
These correspond to half-periods of the unmodulated carrier.
"""
durations = [e['dur_us'] for e in edges if 1 < e['dur_us'] < 50]
if not durations:
return {'carrier_hz': 0, 'half_period_us': 0}
counter = Counter(durations)
most_common = counter.most_common(20)
if verbose:
print("\n[*] Most frequent edge durations (us):")
for dur, cnt in most_common[:10]:
print(f" {dur:4d} us x{cnt:5d}")
# Carrier half-period = most frequent pulse in the 1-20 us range
short = [(d, c) for d, c in most_common if 1 < d < 20]
if not short:
short = [(d, c) for d, c in most_common if d < 50]
if not short:
return {'carrier_hz': 0, 'half_period_us': 0}
half_period = short[0][0]
carrier_hz = int(1_000_000 / (2 * half_period)) if half_period > 0 else 0
print(f"\n[*] Detected carrier:")
print(f" Half-period : {half_period} us")
print(f" Frequency : {carrier_hz:,} Hz (~{carrier_hz/1000:.1f} kHz)")
if 100_000 < carrier_hz < 150_000:
print(f" [+] Matches 125 kHz LF band (KeylessGO / RFID)")
elif 110_000 < carrier_hz < 140_000:
print(f" [~] Close to 125 kHz")
return {
'carrier_hz': carrier_hz,
'half_period_us': half_period,
'period_us': half_period * 2,
}
# -- Packet segmentation ------------------------------------------------------
def find_packets(edges: list, gap_threshold_us: int = 5000) -> list:
"""Splits the edge list into packets separated by gaps."""
packets = []
start = 0
for i, e in enumerate(edges):
if e['dur_us'] > gap_threshold_us:
if i - start > 8:
packets.append(edges[start:i])
start = i + 1
if len(edges) - start > 8:
packets.append(edges[start:])
print(f"\n[*] Detected packets: {len(packets)}")
for i, pkt in enumerate(packets):
total = sum(e['dur_us'] for e in pkt)
print(f" PKT {i}: {len(pkt)} edges, {total} us ({total/1000:.1f} ms)")
return packets
# -- Bit period detection -----------------------------------------------------
def detect_bit_period(packet: list, carrier: dict, verbose: bool = False) -> dict:
"""
Estimates the bit period from modulated pulse widths.
KeylessGO uses Manchester at ~4 kbps over 125 kHz, so bit_period ~250 us.
EM4100 / Hitag use 125 kHz / 32 = ~256 us per bit.
"""
min_dur = carrier.get('half_period_us', 4)
durations = [e['dur_us'] for e in packet if e['dur_us'] > min_dur * 2]
if not durations:
return {'bit_period_us': 0, 'baud_rate': 0}
# Histogram with 10 us resolution
hist = Counter(round(d / 10) * 10 for d in durations)
peaks = sorted(hist.items(), key=lambda x: x[1], reverse=True)
if verbose:
print("\n Modulated pulse durations (top 15):")
for dur, cnt in peaks[:15]:
print(f" {dur:5d} us x{cnt:4d}")
if not peaks:
return {'bit_period_us': 0, 'baud_rate': 0}
# Bit period = smallest frequently-occurring modulated pulse
candidates = [d for d, c in peaks if c >= 3]
if not candidates:
candidates = [peaks[0][0]]
half_period = min(candidates)
# Check if this is a half-period (consecutive same-width pulses that
# alternate level = Manchester half-periods). If so, double it.
# Two equal half-periods make one Manchester bit period.
same_width_pairs = sum(
1 for i in range(len(packet)-1)
if abs(packet[i]['dur_us'] - half_period) < half_period*0.3
and abs(packet[i+1]['dur_us'] - half_period) < half_period*0.3
)
total_half_pulses = sum(
1 for e in packet
if abs(e['dur_us'] - half_period) < half_period*0.3
)
# If >80% of pulses are this width, they are half-periods
if total_half_pulses > len(packet) * 0.7:
bit_period = half_period * 2
is_half = True
else:
bit_period = half_period
is_half = False
baud_rate = int(1_000_000 / bit_period) if bit_period > 0 else 0
print(f"\n[*] Estimated bit period: {bit_period} us ({baud_rate} baud)")
if is_half:
print(f" (half-period detected: {half_period} us x2)")
if 3800 < baud_rate < 4200:
print(" -> Likely: Manchester 4 kbps (Hitag2 / Hitag S / EM4100 / Keyless)")
elif 7500 < baud_rate < 8500:
print(" -> Likely: Manchester 8 kbps")
elif 1900 < baud_rate < 2100:
print(" -> Likely: 2 kbps biphase (EM4100)")
return {
'bit_period_us': bit_period,
'baud_rate': baud_rate,
'half_bit_us': bit_period // 2,
}
# -- Manchester decoder -------------------------------------------------------
def decode_manchester(packet: list, bit_period_us: int, verbose: bool = False) -> list:
"""
Decodes Manchester-encoded bit stream from edge timings.
Both half-period and full-period pulses are handled.
Returns a list of integers (0 or 1).
"""
if bit_period_us == 0:
return []
half = bit_period_us // 2
tolerance = half // 2 # +/- 50% of the half-period
bits = []
i = 0
while i < len(packet):
dur = packet[i]['dur_us']
if abs(dur - half) < tolerance:
# Half-period pulse -- needs the next one to complete a bit
if i + 1 < len(packet):
dur2 = packet[i + 1]['dur_us']
if abs(dur2 - half) < tolerance:
level = packet[i]['level']
bits.append(1 if level else 0)
i += 2
continue
i += 1
elif abs(dur - bit_period_us) < tolerance:
# Full-period pulse -- encodes one bit by itself
level = packet[i]['level']
bits.append(1 if level else 0)
i += 1
else:
if verbose:
print(f" [!] Unexpected duration at edge {i}: {dur} us")
i += 1
return bits
# -- Bit / byte helpers -------------------------------------------------------
def bits_to_bytes(bits: list) -> bytes:
result = bytearray()
for i in range(0, len(bits) - len(bits) % 8, 8):
byte = 0
for j in range(8):
byte = (byte << 1) | bits[i + j]
result.append(byte)
return bytes(result)
def print_bits(bits: list, label: str = "Bits"):
print(f"\n[*] {label} ({len(bits)} bits):")
for i in range(0, len(bits), 64):
chunk = bits[i:i + 64]
groups = [chunk[j:j + 8] for j in range(0, len(chunk), 8)]
line = ' '.join(''.join(str(b) for b in g) for g in groups)
print(f" {i//8:4d}: {line}")
if len(bits) >= 8:
data = bits_to_bytes(bits)
hex_str = ' '.join(f'{b:02X}' for b in data)
print(f"\n HEX: {hex_str}")
# -- Protocol identification --------------------------------------------------
def identify_protocol(bits: list, baud_rate: int, carrier_hz: int) -> str:
n = len(bits)
print(f"\n[*] Protocol identification:")
print(f" Bits: {n} | Baud: {baud_rate} | Carrier: {carrier_hz:,} Hz")
# EM4100: 64-bit frame, 9-bit preamble of all-ones, Manchester
if 50 < n < 80 and baud_rate > 3000:
for start in range(n - 9):
if all(bits[start + j] == 1 for j in range(9)):
print(f" -> EM4100 candidate (preamble at bit {start})")
data_bits = bits[start + 9:]
if len(data_bits) >= 55:
_decode_em4100(data_bits[:55])
break
# Hitag2: ~96-bit frame, Manchester 4 kbps
if 80 < n < 120 and 3500 < baud_rate < 4500:
print(f" -> Hitag2 candidate ({n} bits)")
_analyze_hitag2(bits)
# Hitag S: variable length, Manchester 4 kbps
if n > 120 and 3500 < baud_rate < 4500:
print(f" -> Hitag S candidate ({n} bits)")
# KeylessGO challenge: typically 32-64 data bits
if 20 < n < 80 and 3500 < baud_rate < 4500:
print(f" -> KeylessGO challenge candidate")
_analyze_keylessgo_challenge(bits)
return "unknown"
def _decode_em4100(bits: list):
if len(bits) < 55:
return
# EM4100 data layout: [version 8b][data 32b][col_parity 4b][stop 1b]
# Each row: 4 data bits + 1 parity bit
print("\n EM4100 decode:")
customer = (bits[0] << 7) | (bits[1] << 6) | (bits[2] << 5) | \
(bits[3] << 4) | (bits[5] << 3) | (bits[6] << 2) | \
(bits[7] << 1) | bits[8]
print(f" Customer code: 0x{customer:02X} ({customer})")
def _analyze_hitag2(bits: list):
if len(bits) < 32:
return
data = bits_to_bytes(bits[:32])
print(f"\n Hitag2 first 32 bits: {data.hex().upper()}")
print(f" As uint32 BE : 0x{int.from_bytes(data, 'big'):08X}")
def _analyze_keylessgo_challenge(bits: list):
"""
Looks for a KeylessGO challenge structure.
The car transmits: [sync/preamble] [32-bit challenge] [checksum]
"""
print(f"\n Keyless challenge analysis:")
# Alternating preamble (010101...) is typically used as sync
for start in range(min(len(bits) - 8, 20)):
window = bits[start:start + 8]
alternating = all(window[j] != window[j + 1] for j in range(7))
if alternating:
print(f" Alternating sync at bit {start}: {''.join(str(b) for b in window)}")
payload_start = start + 8
if len(bits) - payload_start >= 32:
challenge_bits = bits[payload_start:payload_start + 32]
challenge_bytes = bits_to_bytes(challenge_bits)
challenge_val = int.from_bytes(challenge_bytes, 'big')
print(f" Challenge candidate (32b): 0x{challenge_val:08X}")
print(f" As bytes : {challenge_bytes.hex().upper()}")
break
# Also print every 32-bit aligned window as a candidate
print(f"\n All 32-bit blocks in capture:")
for offset in range(0, min(len(bits) - 32, 48), 4):
chunk = bits[offset:offset + 32]
val = 0
for b in chunk:
val = (val << 1) | b
preview = ''.join(str(b) for b in chunk[:16])
print(f" offset {offset:3d}: 0x{val:08X} [{preview}...]")
# -- Full analysis entry point ------------------------------------------------
def analyze_capture(path: str, verbose: bool = False, protocol_hint: str = None):
print(f"\n{'='*60}")
print(f"LF Capture Analyzer -- {Path(path).name}")
print('='*60)
edges, meta = load_csv(path)
if not edges:
print("ERROR: No data found in file.")
return
# Step 1: detect carrier
carrier = analyze_carrier(edges, verbose)
# Step 2: segment packets
packets = find_packets(edges)
if not packets:
print("\n[!] No packets detected. Check:")
print(" - Car is emitting 125 kHz LF field")
print(" - Coil is connected correctly to PB2")
print(" - LM393 is powered from 3.3V")
return
# Step 3: analyze each packet
for i, pkt in enumerate(packets):
print(f"\n{''*50}")
print(f"PACKET {i} -- {len(pkt)} edges")
print(''*50)
bit_info = detect_bit_period(pkt, carrier, verbose)
bit_period = bit_info.get('bit_period_us', 0)
baud_rate = bit_info.get('baud_rate', 0)
if bit_period == 0:
print(" [!] Could not determine bit period")
continue
# Step 4: Manchester decode
bits = decode_manchester(pkt, bit_period, verbose)
if len(bits) < 8:
print(f" [!] Only {len(bits)} bits decoded -- likely noise or raw carrier")
if verbose:
raw = [e['dur_us'] for e in pkt[:40]]
print(f" RAW first durations: {raw}")
continue
print_bits(bits, f"Manchester decoded ({len(bits)} bits)")
# Step 5: identify protocol
identify_protocol(bits, baud_rate, carrier.get('carrier_hz', 0))
# Step 6: apply explicit decoder if requested
if protocol_hint == 'hitag2':
decode_hitag2_explicit(bits)
print(f"\n{'='*60}")
print("SUMMARY")
print('='*60)
print(f" Carrier : {carrier.get('carrier_hz', 0):,} Hz")
print(f" Packets : {len(packets)}")
print(f" Total edges: {len(edges)}")
print()
print("NEXT STEPS:")
print(" 1. If you see 'Keyless challenge candidate' -> use the 0xXXXXXXXX value")
print(" with your AUT64 implementation to compute the expected response.")
print(" 2. If Hitag2 is detected -> the protocol is HMAC-SHA1 / AUT64 depending")
print(" on the generation.")
print(" 3. If carrier is 125 kHz but no packets appear -> car is not emitting")
print(" a challenge (get closer, < 30 cm).")
def decode_hitag2_explicit(bits: list):
"""Explicit Hitag2 frame decode when --protocol hitag2 is specified."""
print("\n[*] Hitag2 frame decode:")
if len(bits) < 5:
return
print(f" Start of frame : {bits[0]}")
if len(bits) > 5:
cmd = 0
for b in bits[1:5]:
cmd = (cmd << 1) | b
cmds = {
0b0001: 'REQUEST',
0b0011: 'SELECT',
0b0101: 'READ',
0b1001: 'WRITE'
}
print(f" Command : 0b{cmd:04b} = {cmds.get(cmd, 'UNKNOWN')}")
if len(bits) >= 37:
uid_bits = bits[5:37]
uid_val = 0
for b in uid_bits:
uid_val = (uid_val << 1) | b
print(f" UID candidate : 0x{uid_val:08X}")
# -- CLI ----------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description='Analyze LF captures from the Flipper Zero LF Sniffer FAP',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 lf_analyze.py capture_0000.csv
python3 lf_analyze.py capture_0000.csv --verbose
python3 lf_analyze.py capture_0000.csv --protocol hitag2
"""
)
parser.add_argument('capture', help='CSV file from LF Sniffer FAP')
parser.add_argument('--verbose', '-v', action='store_true')
parser.add_argument('--protocol',
choices=['hitag2', 'em4100', 'Keyless', 'auto'],
default='auto',
help='Force a specific protocol decoder (default: auto)')
args = parser.parse_args()
if not Path(args.capture).exists():
print(f"ERROR: {args.capture} not found")
sys.exit(1)
analyze_capture(
args.capture,
verbose=args.verbose,
protocol_hint=args.protocol if args.protocol != 'auto' else None
)
if __name__ == '__main__':
main()

View File

@@ -1,446 +0,0 @@
#include "lf_sniffer.h"
#include <furi.h>
#include <furi_hal_gpio.h>
#include <furi_hal_resources.h>
#include <furi_hal_cortex.h>
#include <gui/gui.h>
#include <notification/notification_messages.h>
#include <storage/storage.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define TAG "LFSniffer"
#define DWT_CYCCNT (*(volatile uint32_t*)0xE0001004)
#define DWT_CTRL (*(volatile uint32_t*)0xE0001000)
#define DEMCR (*(volatile uint32_t*)0xE000EDFC)
static inline void dwt_init(void) {
DEMCR |= (1U << 24);
DWT_CYCCNT = 0;
DWT_CTRL |= 1U;
}
static inline uint32_t dwt_us(void) {
return DWT_CYCCNT / 64;
}
static volatile LFEdge* g_edges = NULL;
static volatile uint32_t g_edge_count = 0;
static volatile uint32_t g_last_time = 0;
static volatile bool g_active = false;
static volatile bool g_overflow = false;
static void lf_gpio_isr(void* ctx) {
UNUSED(ctx);
if(!g_active) return;
uint32_t now = dwt_us();
uint32_t delta = now - g_last_time;
g_last_time = now;
if(g_edge_count >= LF_MAX_EDGES) {
g_overflow = true;
g_active = false;
return;
}
bool current_level = furi_hal_gpio_read(LF_INPUT_PIN);
g_edges[g_edge_count].duration_us = delta;
g_edges[g_edge_count].level = !current_level;
g_edge_count++;
}
static void draw_callback(Canvas* canvas, void* ctx) {
LFSnifferApp* app = ctx;
furi_mutex_acquire(app->mutex, FuriWaitForever);
canvas_clear(canvas);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 2, 12, "KeylessGO Sniffer");
canvas_set_font(canvas, FontSecondary);
switch(app->state) {
case LFStateIdle:
canvas_draw_str(canvas, 2, 26, "Pin: PB2 (header pin 6)");
canvas_draw_str(canvas, 2, 36, "OK = Start capture");
canvas_draw_str(canvas, 2, 46, "Approach car without key");
canvas_draw_str(canvas, 2, 58, "Back = Exit");
break;
case LFStateCapturing: {
char buf[32];
canvas_draw_str(canvas, 2, 26, "CAPTURING...");
snprintf(buf, sizeof(buf), "Edges: %lu", (unsigned long)app->edge_count);
canvas_draw_str(canvas, 2, 36, buf);
snprintf(buf, sizeof(buf), "Packets: %lu", (unsigned long)app->packet_count);
canvas_draw_str(canvas, 2, 46, buf);
canvas_draw_str(canvas, 2, 58, "OK/Back = Stop");
break;
}
case LFStateDone: {
char buf[48];
snprintf(buf, sizeof(buf), "Edges:%lu Pkts:%lu",
(unsigned long)app->edge_count,
(unsigned long)app->packet_count);
canvas_draw_str(canvas, 2, 26, buf);
snprintf(buf, sizeof(buf), "Min:%luus Max:%luus",
(unsigned long)app->min_pulse_us,
(unsigned long)app->max_pulse_us);
canvas_draw_str(canvas, 2, 36, buf);
canvas_draw_str(canvas, 2, 46, "OK = Save to SD");
canvas_draw_str(canvas, 2, 58, "Back = Discard");
break;
}
case LFStateSaving:
canvas_draw_str(canvas, 2, 26, "Saving...");
canvas_draw_str(canvas, 2, 36, app->filename);
break;
case LFStateSaved:
canvas_draw_str(canvas, 2, 26, "Saved OK:");
canvas_draw_str(canvas, 2, 36, app->filename);
canvas_draw_str(canvas, 2, 46, app->status_msg);
canvas_draw_str(canvas, 2, 58, "Back = New capture");
break;
case LFStateError:
canvas_draw_str(canvas, 2, 26, "ERROR:");
canvas_draw_str(canvas, 2, 36, app->status_msg);
canvas_draw_str(canvas, 2, 58, "Back = Retry");
break;
}
furi_mutex_release(app->mutex);
}
static void input_callback(InputEvent* event, void* ctx) {
LFSnifferApp* app = ctx;
furi_message_queue_put(app->queue, event, FuriWaitForever);
}
static void lf_analyze_packets(LFSnifferApp* app) {
app->packet_count = 0;
app->min_pulse_us = 0xFFFFFFFF;
app->max_pulse_us = 0;
app->carrier_pulses = 0;
app->data_pulses = 0;
if(app->edge_count < 4) return;
uint32_t pkt_start = 0;
bool in_packet = false;
for(uint32_t i = 1; i < app->edge_count; i++) {
uint32_t dur = app->edges[i].duration_us;
if(dur < app->min_pulse_us) app->min_pulse_us = dur;
if(dur > app->max_pulse_us) app->max_pulse_us = dur;
if(dur < 12) {
app->carrier_pulses++;
} else if(dur < 15) {
app->data_pulses++;
}
if(dur > LF_GAP_THRESHOLD_US) {
if(in_packet && i > pkt_start + 8) {
if(app->packet_count < LF_MAX_PACKETS) {
app->packets[app->packet_count].start_idx = pkt_start;
app->packets[app->packet_count].edge_count = i - pkt_start;
app->packets[app->packet_count].duration_us = 0;
for(uint32_t j = pkt_start; j < i; j++)
app->packets[app->packet_count].duration_us +=
app->edges[j].duration_us;
app->packet_count++;
}
}
in_packet = false;
pkt_start = i;
} else {
if(!in_packet) {
in_packet = true;
pkt_start = i;
}
}
}
if(in_packet && app->edge_count > pkt_start + 8) {
if(app->packet_count < LF_MAX_PACKETS) {
app->packets[app->packet_count].start_idx = pkt_start;
app->packets[app->packet_count].edge_count = app->edge_count - pkt_start;
app->packets[app->packet_count].duration_us = 0;
for(uint32_t j = pkt_start; j < app->edge_count; j++)
app->packets[app->packet_count].duration_us +=
app->edges[j].duration_us;
app->packet_count++;
}
}
}
static bool lf_save_csv(LFSnifferApp* app) {
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_common_mkdir(storage, "/ext/keyless_sniffer");
snprintf(app->filename, sizeof(app->filename),
"/ext/keyless_sniffer/capture_%04lu.csv",
(unsigned long)app->file_index);
File* file = storage_file_alloc(storage);
bool ok = false;
if(storage_file_open(file, app->filename, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
const char* header = "index,duration_us,level,note\n";
storage_file_write(file, header, strlen(header));
char meta[128];
snprintf(meta, sizeof(meta),
"# Keyless Sniffer capture -- edges:%lu packets:%lu\n",
(unsigned long)app->edge_count,
(unsigned long)app->packet_count);
storage_file_write(file, meta, strlen(meta));
snprintf(meta, sizeof(meta),
"# min_us:%lu max_us:%lu carrier:%lu data:%lu\n",
(unsigned long)app->min_pulse_us,
(unsigned long)app->max_pulse_us,
(unsigned long)app->carrier_pulses,
(unsigned long)app->data_pulses);
storage_file_write(file, meta, strlen(meta));
uint32_t pkt_idx = 0;
char line[64];
for(uint32_t i = 0; i < app->edge_count; i++) {
const char* note = "";
if(pkt_idx < app->packet_count &&
app->packets[pkt_idx].start_idx == i) {
note = "PKT_START";
pkt_idx++;
}
snprintf(line, sizeof(line),
"%lu,%lu,%d,%s\n",
(unsigned long)i,
(unsigned long)app->edges[i].duration_us,
app->edges[i].level ? 1 : 0,
note);
storage_file_write(file, line, strlen(line));
}
storage_file_write(file, "# PACKETS\n", 10);
for(uint32_t p = 0; p < app->packet_count; p++) {
snprintf(meta, sizeof(meta),
"# PKT %lu: start=%lu edges=%lu dur=%luus\n",
(unsigned long)p,
(unsigned long)app->packets[p].start_idx,
(unsigned long)app->packets[p].edge_count,
(unsigned long)app->packets[p].duration_us);
storage_file_write(file, meta, strlen(meta));
}
storage_file_close(file);
ok = true;
snprintf(app->status_msg, sizeof(app->status_msg),
"%lu edges, %lu packets",
(unsigned long)app->edge_count,
(unsigned long)app->packet_count);
} else {
snprintf(app->status_msg, sizeof(app->status_msg), "Failed to open file");
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
return ok;
}
static void lf_start_capture(LFSnifferApp* app) {
furi_mutex_acquire(app->mutex, FuriWaitForever);
app->edge_count = 0;
app->packet_count = 0;
app->total_time_us = 0;
app->min_pulse_us = 0xFFFFFFFF;
app->max_pulse_us = 0;
app->carrier_pulses = 0;
app->data_pulses = 0;
g_overflow = false;
g_edges = app->edges;
g_edge_count = 0;
g_last_time = dwt_us();
g_active = true;
furi_hal_gpio_init(LF_INPUT_PIN, GpioModeInterruptRiseFall,
GpioPullUp, GpioSpeedVeryHigh);
furi_hal_gpio_add_int_callback(LF_INPUT_PIN, lf_gpio_isr, NULL);
app->state = LFStateCapturing;
furi_mutex_release(app->mutex);
notification_message(app->notif, &sequence_blink_green_100);
FURI_LOG_I(TAG, "Capture started");
}
static void lf_stop_capture(LFSnifferApp* app) {
g_active = false;
furi_hal_gpio_remove_int_callback(LF_INPUT_PIN);
furi_hal_gpio_init(LF_INPUT_PIN, GpioModeInput, GpioPullUp, GpioSpeedLow);
furi_mutex_acquire(app->mutex, FuriWaitForever);
app->edge_count = g_edge_count;
lf_analyze_packets(app);
app->state = LFStateDone;
furi_mutex_release(app->mutex);
notification_message(app->notif, &sequence_blink_blue_100);
FURI_LOG_I(TAG, "Capture stopped: %lu edges, %lu packets",
(unsigned long)app->edge_count,
(unsigned long)app->packet_count);
}
int32_t lf_sniffer_app(void* p) {
UNUSED(p);
dwt_init();
LFSnifferApp* app = malloc(sizeof(LFSnifferApp));
furi_check(app != NULL);
memset(app, 0, sizeof(LFSnifferApp));
app->edges = malloc(LF_MAX_EDGES * sizeof(LFEdge));
furi_check(app->edges != NULL);
app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
app->queue = furi_message_queue_alloc(16, sizeof(InputEvent));
app->notif = furi_record_open(RECORD_NOTIFICATION);
app->view_port = view_port_alloc();
app->state = LFStateIdle;
app->file_index = 0;
view_port_draw_callback_set(app->view_port, draw_callback, app);
view_port_input_callback_set(app->view_port, input_callback, app);
app->gui = furi_record_open(RECORD_GUI);
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
FURI_LOG_I(TAG, "Keyless Sniffer started");
InputEvent event;
bool running = true;
while(running) {
if(app->state == LFStateCapturing) {
furi_mutex_acquire(app->mutex, FuriWaitForever);
app->edge_count = g_edge_count;
if(g_overflow) {
furi_mutex_release(app->mutex);
lf_stop_capture(app);
furi_mutex_acquire(app->mutex, FuriWaitForever);
snprintf(app->status_msg, sizeof(app->status_msg),
"Buffer full (%d edges)", LF_MAX_EDGES);
}
furi_mutex_release(app->mutex);
view_port_update(app->view_port);
}
FuriStatus status = furi_message_queue_get(app->queue, &event, 100);
if(status != FuriStatusOk) continue;
if(event.type != InputTypeShort && event.type != InputTypeLong) continue;
switch(app->state) {
case LFStateIdle:
if(event.key == InputKeyOk) {
lf_start_capture(app);
view_port_update(app->view_port);
} else if(event.key == InputKeyBack) {
running = false;
}
break;
case LFStateCapturing:
if(event.key == InputKeyOk || event.key == InputKeyBack) {
lf_stop_capture(app);
view_port_update(app->view_port);
}
break;
case LFStateDone:
if(event.key == InputKeyOk) {
furi_mutex_acquire(app->mutex, FuriWaitForever);
app->state = LFStateSaving;
furi_mutex_release(app->mutex);
view_port_update(app->view_port);
bool saved = lf_save_csv(app);
furi_mutex_acquire(app->mutex, FuriWaitForever);
app->state = saved ? LFStateSaved : LFStateError;
app->file_index++;
furi_mutex_release(app->mutex);
view_port_update(app->view_port);
if(saved) {
notification_message(app->notif, &sequence_success);
FURI_LOG_I(TAG, "Saved: %s", app->filename);
}
} else if(event.key == InputKeyBack) {
furi_mutex_acquire(app->mutex, FuriWaitForever);
app->state = LFStateIdle;
app->edge_count = 0;
app->packet_count = 0;
furi_mutex_release(app->mutex);
view_port_update(app->view_port);
}
break;
case LFStateSaved:
case LFStateError:
if(event.key == InputKeyBack) {
furi_mutex_acquire(app->mutex, FuriWaitForever);
app->state = LFStateIdle;
app->edge_count = 0;
app->packet_count = 0;
furi_mutex_release(app->mutex);
view_port_update(app->view_port);
}
break;
default:
break;
}
}
if(app->state == LFStateCapturing) {
g_active = false;
furi_hal_gpio_remove_int_callback(LF_INPUT_PIN);
furi_hal_gpio_init(LF_INPUT_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
}
view_port_enabled_set(app->view_port, false);
gui_remove_view_port(app->gui, app->view_port);
view_port_free(app->view_port);
furi_record_close(RECORD_GUI);
furi_record_close(RECORD_NOTIFICATION);
furi_mutex_free(app->mutex);
furi_message_queue_free(app->queue);
free(app->edges);
free(app);
return 0;
}

View File

@@ -1,80 +0,0 @@
#pragma once
#include <furi.h>
#include <gui/gui.h>
#include <gui/view_port.h>
#include <notification/notification.h>
#include <input/input.h>
#include <storage/storage.h>
#include <furi_hal_gpio.h>
#include <furi_hal_resources.h>
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define LF_MAX_EDGES 4096
#define LF_GAP_THRESHOLD_US 5000
#define LF_CARRIER_HZ 125000
#define LF_BIT_PERIOD_US 250
#define LF_TIMEOUT_MS 10000
#define LF_MAX_PACKETS 16
#define LF_INPUT_PIN (&gpio_ext_pb2)
typedef struct {
uint32_t duration_us;
bool level;
} LFEdge;
typedef struct {
uint32_t start_idx;
uint32_t edge_count;
uint32_t duration_us;
} LFPacket;
typedef enum {
LFStateIdle,
LFStateCapturing,
LFStateDone,
LFStateSaving,
LFStateSaved,
LFStateError,
} LFState;
typedef struct {
Gui* gui;
ViewPort* view_port;
FuriMessageQueue* queue;
FuriMutex* mutex;
NotificationApp* notif;
LFState state;
char status_msg[64];
LFEdge* edges;
uint32_t edge_count;
uint32_t total_time_us;
LFPacket packets[LF_MAX_PACKETS];
uint32_t packet_count;
uint32_t last_edge_time_us;
bool capturing;
uint32_t carrier_pulses;
uint32_t data_pulses;
uint32_t min_pulse_us;
uint32_t max_pulse_us;
char filename[128];
uint32_t file_index;
} LFSnifferApp;
int32_t lf_sniffer_app(void* p);
#ifdef __cplusplus
}
#endif

View File

@@ -9,7 +9,6 @@ App(
"nfc",
"subghz",
"rolljam",
"lf_sniffer",
"subghz_bruteforcer",
"archive",
"subghz_remote",

View File

@@ -14,7 +14,9 @@ enum {
SubmenuIndexUnlock = SubmenuIndexCommonMax,
SubmenuIndexUnlockByReader,
SubmenuIndexUnlockByPassword,
SubmenuIndexDictAttack
SubmenuIndexDictAttack,
SubmenuIndexWriteKeepKey, // ULC: write data pages, keep target card's existing key
SubmenuIndexWriteCopyKey, // ULC: write all pages including key from source card
};
enum {
@@ -214,8 +216,26 @@ static void nfc_scene_read_and_saved_menu_on_enter_mf_ultralight(NfcApp* instanc
if(is_locked ||
(data->type != MfUltralightTypeNTAG213 && data->type != MfUltralightTypeNTAG215 &&
data->type != MfUltralightTypeNTAG216 && data->type != MfUltralightTypeUL11 &&
data->type != MfUltralightTypeUL21 && data->type != MfUltralightTypeOrigin)) {
data->type != MfUltralightTypeUL21 && data->type != MfUltralightTypeOrigin &&
data->type != MfUltralightTypeMfulC)) {
submenu_remove_item(submenu, SubmenuIndexCommonWrite);
} else if(data->type == MfUltralightTypeMfulC) {
// Replace the generic Write item with two ULC-specific options so the user
// can choose whether to keep or overwrite the target card's 3DES key.
// This avoids any mid-write dialog/view-switching complexity entirely.
submenu_remove_item(submenu, SubmenuIndexCommonWrite);
submenu_add_item(
submenu,
"Write (Keep Key)",
SubmenuIndexWriteKeepKey,
nfc_protocol_support_common_submenu_callback,
instance);
submenu_add_item(
submenu,
"Write (Copy Key)",
SubmenuIndexWriteCopyKey,
nfc_protocol_support_common_submenu_callback,
instance);
}
if(is_locked) {
@@ -291,6 +311,14 @@ static bool nfc_scene_read_and_saved_menu_on_event_mf_ultralight(
scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCDictAttack);
}
consumed = true;
} else if(event.event == SubmenuIndexWriteKeepKey) {
instance->mf_ultralight_c_write_context.copy_key = false;
scene_manager_next_scene(instance->scene_manager, NfcSceneWrite);
consumed = true;
} else if(event.event == SubmenuIndexWriteCopyKey) {
instance->mf_ultralight_c_write_context.copy_key = true;
scene_manager_next_scene(instance->scene_manager, NfcSceneWrite);
consumed = true;
}
}
return consumed;
@@ -307,12 +335,139 @@ static NfcCommand
if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestMode) {
mf_ultralight_event->data->poller_mode = MfUltralightPollerModeWrite;
furi_string_reset(instance->text_box_store);
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected);
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeAuthRequest) {
// Skip auth during the read phase of write - we'll authenticate
// against the target card in RequestWriteData using source key or dict attack
mf_ultralight_event->data->auth_context.skip_auth = true;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestKey) {
// Dict attack key provider - user dict first, then system dict
if(!instance->mf_ultralight_c_dict_context.dict &&
instance->mf_ultralight_c_write_context.dict_state == NfcMfUltralightCWriteDictIdle) {
if(keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) {
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictUser;
}
if(!instance->mf_ultralight_c_dict_context.dict) {
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state =
NfcMfUltralightCWriteDictSystem;
}
}
MfUltralightC3DesAuthKey key = {};
bool got_key = false;
if(instance->mf_ultralight_c_dict_context.dict) {
got_key = keys_dict_get_next_key(
instance->mf_ultralight_c_dict_context.dict,
key.data,
sizeof(MfUltralightC3DesAuthKey));
}
if(!got_key &&
instance->mf_ultralight_c_write_context.dict_state == NfcMfUltralightCWriteDictUser) {
// Exhausted user dict, switch to system dict
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictSystem;
if(instance->mf_ultralight_c_dict_context.dict) {
got_key = keys_dict_get_next_key(
instance->mf_ultralight_c_dict_context.dict,
key.data,
sizeof(MfUltralightC3DesAuthKey));
}
}
if(got_key) {
mf_ultralight_event->data->key_request_data.key = key;
mf_ultralight_event->data->key_request_data.key_provided = true;
FURI_LOG_D(
"MfULC",
"Trying dict key: "
"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
key.data[0],
key.data[1],
key.data[2],
key.data[3],
key.data[4],
key.data[5],
key.data[6],
key.data[7],
key.data[8],
key.data[9],
key.data[10],
key.data[11],
key.data[12],
key.data[13],
key.data[14],
key.data[15]);
} else {
mf_ultralight_event->data->key_request_data.key_provided = false;
FURI_LOG_D("MfULC", "Dict exhausted - no more keys");
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
}
instance->mf_ultralight_c_write_context.dict_state =
NfcMfUltralightCWriteDictExhausted;
}
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestWriteData) {
mf_ultralight_event->data->write_data =
nfc_device_get_data(instance->nfc_device, NfcProtocolMfUltralight);
// Reset dict context so RequestKey starts fresh for the write-phase auth
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
}
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteKeyRequest) {
// Apply the user's key choice - read from static, not scene state (scene manager
// resets state to 0 on scene entry, wiping any value set before next_scene).
bool keep_key = !instance->mf_ultralight_c_write_context.copy_key;
mf_ultralight_event->data->write_key_skip = keep_key;
if(mf_ultralight_event->data->key_request_data.key_provided) {
MfUltralightC3DesAuthKey found_key = mf_ultralight_event->data->key_request_data.key;
FURI_LOG_D(
"MfULC",
"WriteKeyRequest: target key = "
"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
found_key.data[0],
found_key.data[1],
found_key.data[2],
found_key.data[3],
found_key.data[4],
found_key.data[5],
found_key.data[6],
found_key.data[7],
found_key.data[8],
found_key.data[9],
found_key.data[10],
found_key.data[11],
found_key.data[12],
found_key.data[13],
found_key.data[14],
found_key.data[15]);
}
FURI_LOG_D(
"MfULC",
"WriteKeyRequest: decision = %s (copy_key=%d)",
keep_key ? "KEEP target key (pages 44-47 NOT written)" :
"OVERWRITE with source key (pages 44-47 WILL be written)",
(int)instance->mf_ultralight_c_write_context.copy_key);
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeCardMismatch) {
furi_string_set(instance->text_box_store, "Card of the same\ntype should be\n presented");
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventWrongCard);
@@ -323,6 +478,7 @@ static NfcCommand
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerFailure);
command = NfcCommandStop;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteFail) {
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerFailure);
command = NfcCommandStop;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteSuccess) {
furi_string_reset(instance->text_box_store);
@@ -334,9 +490,18 @@ static NfcCommand
}
static void nfc_scene_write_on_enter_mf_ultralight(NfcApp* instance) {
// Free any dict the write callback opened (dict_state != Idle means we own it).
// After a DictAttack scene, on_exit now NULLs the pointer so a simple NULL check
// is safe here too — but the state enum is the authoritative ownership record.
if(instance->mf_ultralight_c_write_context.dict_state != NfcMfUltralightCWriteDictIdle &&
instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
furi_string_set(instance->text_box_store, "\nApply the\ntarget\ncard now");
instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfUltralight);
nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_mf_ultralight, instance);
furi_string_set(instance->text_box_store, "Apply the initial\ncard only");
}
const NfcProtocolSupportBase nfc_protocol_support_mf_ultralight = {

View File

@@ -126,6 +126,18 @@ typedef struct {
size_t dict_keys_current;
} NfcMfUltralightCDictContext;
typedef enum {
NfcMfUltralightCWriteDictIdle, /**< No dict open; safe to open either dict. */
NfcMfUltralightCWriteDictUser, /**< User dict currently open. */
NfcMfUltralightCWriteDictSystem, /**< System dict currently open. */
NfcMfUltralightCWriteDictExhausted, /**< All dicts tried; do not re-open. */
} NfcMfUltralightCWriteDictState;
typedef struct {
bool copy_key; /**< True = overwrite target 3DES key with source key pages. */
NfcMfUltralightCWriteDictState dict_state; /**< Which dict is open for write-phase auth. */
} NfcMfUltralightCWriteContext;
struct NfcApp {
DialogsApp* dialogs;
Storage* storage;
@@ -165,6 +177,7 @@ struct NfcApp {
SlixUnlock* slix_unlock;
NfcMfClassicDictAttackContext nfc_dict_context;
NfcMfUltralightCDictContext mf_ultralight_c_dict_context;
NfcMfUltralightCWriteContext mf_ultralight_c_write_context;
Mfkey32Logger* mfkey32_logger;
MfUserDict* mf_user_dict;
MfClassicKeyCache* mfc_key_cache;

View File

@@ -77,6 +77,15 @@ void nfc_scene_mf_ultralight_c_dict_attack_prepare_view(NfcApp* instance) {
// Set attack type to Ultralight C
dict_attack_set_type(instance->dict_attack, DictAttackTypeMfUltralightC);
// Guard: if a previous write phase left a dict handle open, close it now.
// Without this, navigating write->back->read->dict-attack would open the same
// file twice, corrupting VFS state and causing a ViewPort lockup.
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
}
if(state == DictAttackStateUserDictInProgress) {
do {
if(!keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) {
@@ -167,6 +176,7 @@ bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerE
nfc_poller_stop(instance->poller);
nfc_poller_free(instance->poller);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
scene_manager_set_scene_state(
instance->scene_manager,
NfcSceneMfUltralightCDictAttack,
@@ -199,6 +209,7 @@ bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerE
nfc_poller_stop(instance->poller);
nfc_poller_free(instance->poller);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
scene_manager_set_scene_state(
instance->scene_manager,
NfcSceneMfUltralightCDictAttack,
@@ -230,6 +241,7 @@ void nfc_scene_mf_ultralight_c_dict_attack_on_exit(void* context) {
NfcSceneMfUltralightCDictAttack,
DictAttackStateUserDictInProgress);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_dict_context.dict_keys_total = 0;
instance->mf_ultralight_c_dict_context.dict_keys_current = 0;
instance->mf_ultralight_c_dict_context.auth_success = false;

View File

@@ -92,19 +92,30 @@ Hopping_Preset: FM95
# Custom preset
# format for CC1101 "Custom_preset_data:" XX YY XX YY .. 00 00 ZZ ZZ ZZ ZZ ZZ ZZ ZZ ZZ, where: XX-register, YY - register data, 00 00 - end load register, ZZ - 8 byte Pa table register
Custom_preset_name: A1
Custom_preset_name: OOK_LR
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 0B 06 08 32 07 04 14 00 13 00 12 30 11 F8 10 C9 15 14 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 00 21 55 00 00 00 С0 00 00 00 00 00 00
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 F8 11 32 12 30 14 00 15 00 18 18 19 16 1B 07 1C 00 1D B1 20 FB 21 B6 22 11 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: FM95
Custom_preset_name: OOK_U
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 83 10 67 15 24 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 F8 11 32 12 30 14 00 15 00 17 0C 18 18 19 16 1B 07 1C 00 1D B1 20 FB 21 B6 22 11 2C 81 2D 35 2E 09 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: AM_1
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 C9 11 F8 12 30 14 00 15 14 18 18 19 16 1B 07 1C 00 1D 91 20 FB 21 55 22 00 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: FSK_1
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 0C 10 C7 11 93 12 00 13 22 14 F8 15 35 18 18 19 1D 1B 04 1C 00 1D 92 20 FB 21 B6 22 17 00 00 12 0E 1D 34 60 84 C8 C0
Custom_preset_name: F3
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 06 0C 00 0D 10 0E B0 0F 71 10 CA 11 83 12 80 13 22 14 F8 15 42 16 07 17 30 18 18 19 1D 1A 1C 1B 43 1C 40 1D 91 20 FB 21 B6 22 00 23 E9 24 2A 25 00 26 1F 2C 81 2D 35 2E 09 00 00 12 0E 1D 34 60 84 C8 C0
Custom_preset_data: 02 0D 03 47 08 32 0B 06 0C 00 0D 10 0D 0D 10 C5 11 83 12 80 13 22 14 F8 15 42 16 07 17 30 18 18 19 1D 1A 1C 1B 43 1C 40 1D 91 20 FB 21 B6 22 00 23 E9 24 2A 25 00 26 1F 2C 81 2D 35 2E 09 00 00 12 0E 1D 34 60 84 C8 C0
Custom_preset_name: FM95
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 67 11 83 12 04 13 02 15 24 18 18 19 16 1B 07 1C 00 1D 91 20 FB 21 56 22 10 00 00 C0 00 00 00 00 00 00 00
#2-FSK 200khz BW / 135kHz Filter/ 15.86Khz Deviation + Ramping
Custom_preset_name: FM15k
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 06 15 32 14 00 13 00 12 00 11 32 10 A7 18 18 19 1D 1D 92 1C 00 1B 04 20 FB 22 17 21 B6 00 00 00 12 0E 34 60 C5 C1 C0
Custom_preset_data: 02 0D 03 47 08 32 0B 06 10 A7 11 32 12 00 13 00 14 00 15 32 18 18 19 1D 1B 04 1C 00 1D 92 20 FB 21 B6 22 17 00 00 00 12 0E 34 60 C5 C1 C0

View File

@@ -133,21 +133,21 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
}
//CC1101 Stop RX -> Start TX
subghz_txrx_hopper_pause(subghz->txrx);
// key concept: we start endless TX until user release OK button, and after this we send last
// protocols repeats - this guarantee that one press OK will
// be guarantee send the required minimum protocol data packets
// for all of this we use subghz_block_generic_global.endless_tx in protocols _yield function.
subghz->state_notifications = SubGhzNotificationStateTx;
subghz_block_generic_global.endless_tx = true;
if(!subghz_tx_start(
subghz,
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen))) {
subghz_txrx_rx_start(subghz->txrx);
subghz_txrx_hopper_unpause(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateRx;
} else {
// key concept: we start endless TX until user release OK button, and after this we send last
// protocols repeats - this guarantee that one press OK will
// be guarantee send the required minimum protocol data packets
// for all of this we use subghz_block_generic_global.endless_tx in protocols _yield function.
subghz->state_notifications = SubGhzNotificationStateTx;
subghz_block_generic_global.endless_tx = true;
subghz_block_generic_global.endless_tx = false;
return true;
}
return true;
} else if(event.event == SubGhzCustomEventSceneReceiverInfoTxStop) {
//CC1101 Stop Tx -> next tick event Start RX
// user release OK

View File

@@ -0,0 +1 @@
*

View File

@@ -575,13 +575,15 @@ uint8_t mf_ultralight_get_write_end_page(MfUltralightType type) {
furi_assert(
type == MfUltralightTypeUL11 || type == MfUltralightTypeUL21 ||
type == MfUltralightTypeNTAG213 || type == MfUltralightTypeNTAG215 ||
type == MfUltralightTypeNTAG216 || type == MfUltralightTypeOrigin);
type == MfUltralightTypeNTAG216 || type == MfUltralightTypeOrigin ||
type == MfUltralightTypeMfulC);
uint8_t end_page = mf_ultralight_get_config_page_num(type);
if(type == MfUltralightTypeNTAG213 || type == MfUltralightTypeNTAG215 ||
type == MfUltralightTypeNTAG216) {
end_page -= 1;
} else if(type == MfUltralightTypeOrigin) {
} else if(type == MfUltralightTypeOrigin || type == MfUltralightTypeMfulC) {
// ULC: 48 pages total, write pages 4-47 (includes auth config + 3DES key)
end_page = mf_ultralight_features[type].total_pages;
}

View File

@@ -186,7 +186,7 @@ static MfUltralightCommand
uint16_t pages_total = instance->data->pages_total;
MfUltralightCommand command = MfUltralightCommandNotProcessedNAK;
FURI_LOG_T(TAG, "CMD_WRITE");
FURI_LOG_T(TAG, "CMD_WRITE page %d", start_page);
do {
bool do_i2c_check = mf_ultralight_is_i2c_tag(instance->data->type);
@@ -197,12 +197,22 @@ static MfUltralightCommand
break;
}
if(!mf_ultralight_listener_check_access(
instance, start_page, MfUltralightListenerAccessTypeWrite))
break;
// PATCHED: For Ultralight-C, allow writes to pages 44-47 (3DES key area)
// This enables "magic card" emulation for key grabbing
bool is_ulc_key_page = (instance->data->type == MfUltralightTypeMfulC) &&
(start_page >= 44 && start_page <= 47);
if(mf_ultralight_static_lock_check_page(instance->static_lock, start_page)) break;
if(mf_ultralight_dynamic_lock_check_page(instance, start_page)) break;
if(!is_ulc_key_page) {
// Normal access check for all other pages
if(!mf_ultralight_listener_check_access(
instance, start_page, MfUltralightListenerAccessTypeWrite))
break;
if(mf_ultralight_static_lock_check_page(instance->static_lock, start_page)) break;
if(mf_ultralight_dynamic_lock_check_page(instance, start_page)) break;
} else {
FURI_LOG_I(TAG, "MAGIC: Allowing write to ULC key page %d", start_page);
}
const uint8_t* rx_data = bit_buffer_get_data(buffer);
command =

View File

@@ -456,7 +456,8 @@ static NfcCommand mf_ultralight_poller_handler_auth_ultralight_c(MfUltralightPol
if(instance->mfu_event.data->key_request_data.key_provided) {
instance->auth_context.tdes_key = instance->mfu_event.data->key_request_data.key;
} else if(instance->mode == MfUltralightPollerModeDictAttack) {
// TODO: Can logic be rearranged to request this key before reaching mf_ultralight_poller_handler_auth_ultralight_c in poller?
// TODO: -nofl Can logic be rearranged to request this key
// before reaching mf_ultralight_poller_handler_auth_ultralight_c in poller?
FURI_LOG_D(TAG, "No initial key provided, requesting key from dictionary");
// Trigger dictionary key request
instance->mfu_event.type = MfUltralightPollerEventTypeRequestKey;
@@ -697,9 +698,12 @@ static NfcCommand mf_ultralight_poller_handler_request_write_data(MfUltralightPo
instance->mfu_event.type = MfUltralightPollerEventTypeRequestWriteData;
instance->callback(instance->general_event, instance->context);
const MfUltralightData* write_data = instance->mfu_event.data->write_data;
// Save write_data to instance field before any further events clobber the union
instance->write_data = instance->mfu_event.data->write_data;
const MfUltralightData* write_data = instance->write_data;
const MfUltralightData* tag_data = instance->data;
uint32_t features = mf_ultralight_get_feature_support_set(tag_data->type);
instance->write_skip_key = false;
bool check_passed = false;
do {
@@ -737,6 +741,71 @@ static NfcCommand mf_ultralight_poller_handler_request_write_data(MfUltralightPo
check_passed = true;
} while(false);
// ULC: authenticate the target card before writing.
// The read phase left the card unauthenticated (write-mode callback skips read-phase auth).
// Ask callback for keys (cache first, then dict) until one authenticates, then fire
// WriteKeyRequest so the callback can cache the found key and return the skip decision.
if(check_passed &&
mf_ultralight_support_feature(features, MfUltralightFeatureSupportAuthenticate)) {
bool auth_ok = false;
while(!auth_ok) {
// Request next key from callback (tries dict entries)
memset(instance->mfu_event.data, 0, sizeof(MfUltralightPollerEventData));
instance->mfu_event.type = MfUltralightPollerEventTypeRequestKey;
instance->callback(instance->general_event, instance->context);
if(!instance->mfu_event.data->key_request_data.key_provided) {
FURI_LOG_D(TAG, "ULC write: all keys exhausted");
break;
}
instance->auth_context.tdes_key = instance->mfu_event.data->key_request_data.key;
// Halt+activate so the card is in a clean state for auth
iso14443_3a_poller_halt(instance->iso14443_3a_poller);
if(iso14443_3a_poller_activate(instance->iso14443_3a_poller, NULL) !=
Iso14443_3aErrorNone) {
FURI_LOG_E(TAG, "ULC write: card not responding (locked out?)");
break;
}
uint8_t output[MF_ULTRALIGHT_C_AUTH_DATA_SIZE];
uint8_t RndA[MF_ULTRALIGHT_C_AUTH_RND_BLOCK_SIZE] = {0};
furi_hal_random_fill_buf(RndA, sizeof(RndA));
if(mf_ultralight_poller_authenticate_start(instance, RndA, output) !=
MfUltralightErrorNone) {
break;
}
uint8_t decoded_RndA[MF_ULTRALIGHT_C_AUTH_RND_BLOCK_SIZE] = {0};
const uint8_t* RndB = output + MF_ULTRALIGHT_C_AUTH_RND_B_BLOCK_OFFSET;
if(mf_ultralight_poller_authenticate_end(instance, RndB, output, decoded_RndA) !=
MfUltralightErrorNone)
continue;
mf_ultralight_3des_shift_data(RndA);
auth_ok = (memcmp(RndA, decoded_RndA, sizeof(decoded_RndA)) == 0);
FURI_LOG_D(TAG, "ULC write auth attempt: %s", auth_ok ? "success" : "fail");
}
if(auth_ok) {
// Notify callback with the found key: it caches it and returns the skip decision
MfUltralightC3DesAuthKey found_key = instance->auth_context.tdes_key;
memset(instance->mfu_event.data, 0, sizeof(MfUltralightPollerEventData));
instance->mfu_event.data->key_request_data.key = found_key;
instance->mfu_event.data->key_request_data.key_provided = true;
instance->mfu_event.type = MfUltralightPollerEventTypeWriteKeyRequest;
instance->callback(instance->general_event, instance->context);
instance->write_skip_key = instance->mfu_event.data->write_key_skip;
FURI_LOG_D(
TAG,
"ULC write: key %s",
instance->write_skip_key ? "kept (target unchanged)" : "overwrite with source");
} else {
FURI_LOG_E(TAG, "ULC write auth failed - card locked");
check_passed = false;
instance->mfu_event.type = MfUltralightPollerEventTypeCardLocked;
}
}
if(!check_passed) {
iso14443_3a_poller_halt(instance->iso14443_3a_poller);
command = instance->callback(instance->general_event, instance->context);
@@ -751,15 +820,52 @@ static NfcCommand mf_ultralight_poller_handler_write_pages(MfUltralightPoller* i
NfcCommand command = NfcCommandContinue;
do {
const MfUltralightData* write_data = instance->mfu_event.data->write_data;
// Use the saved write_data pointer - the union was overwritten by WriteKeyRequest
const MfUltralightData* write_data = instance->write_data;
uint8_t end_page = mf_ultralight_get_write_end_page(write_data->type);
// If user chose to keep target's key, stop before the ULC key pages (44-47)
if(instance->write_skip_key && write_data->type == MfUltralightTypeMfulC &&
end_page > 44) {
end_page = 44;
}
if(instance->current_page == end_page) {
instance->state = MfUltralightPollerStateWriteSuccess;
break;
}
FURI_LOG_D(TAG, "Writing page %d", instance->current_page);
MfUltralightError error = mf_ultralight_poller_write_page(
instance, instance->current_page, &write_data->page[instance->current_page]);
// For ULC key pages (44-47): byte-order correction required.
// Flipper stores each 8-byte DES sub-key MSB-first; the card expects each half reversed.
// Transform: card_bytes = reverse(flipper[0..7]) || reverse(flipper[8..15])
MfUltralightPage page_to_write;
if(instance->current_page >= 44 && instance->current_page <= 47 &&
write_data->type == MfUltralightTypeMfulC) {
const uint8_t* raw = (const uint8_t*)&write_data->page[44];
uint8_t xformed[16];
for(int i = 0; i < 8; i++) {
xformed[i] = raw[7 - i];
}
for(int i = 0; i < 8; i++) {
xformed[8 + i] = raw[15 - i];
}
uint8_t page_offset = (instance->current_page - 44) * 4;
memcpy(page_to_write.data, xformed + page_offset, 4);
FURI_LOG_D(
TAG,
"Writing KEY page %d (byte-order corrected): %02X %02X %02X %02X",
instance->current_page,
page_to_write.data[0],
page_to_write.data[1],
page_to_write.data[2],
page_to_write.data[3]);
} else {
page_to_write = write_data->page[instance->current_page];
FURI_LOG_D(TAG, "Writing page %d", instance->current_page);
}
MfUltralightError error =
mf_ultralight_poller_write_page(instance, instance->current_page, &page_to_write);
if(error != MfUltralightErrorNone) {
instance->state = MfUltralightPollerStateWriteFail;
instance->error = error;

View File

@@ -28,6 +28,7 @@ typedef enum {
MfUltralightPollerEventTypeWriteSuccess, /**< Poller wrote card successfully. */
MfUltralightPollerEventTypeWriteFail, /**< Poller failed to write card. */
MfUltralightPollerEventTypeRequestKey, /**< Poller requests key for dict attack. */
MfUltralightPollerEventTypeWriteKeyRequest, /**< Poller asks user whether to overwrite 3DES key on target. */
} MfUltralightPollerEventType;
/**
@@ -67,6 +68,7 @@ typedef union {
const MfUltralightData* write_data; /**< Data to be written to card. */
MfUltralightPollerMode poller_mode; /**< Mode to operate in. */
MfUltralightPollerKeyRequestData key_request_data; /**< Key request data. */
bool write_key_skip; /**< Set to true by callback to skip writing 3DES key pages. */
} MfUltralightPollerEventData;
/**

View File

@@ -88,6 +88,8 @@ struct MfUltralightPoller {
uint8_t tearing_flag_read;
uint8_t tearing_flag_total;
uint16_t current_page;
bool write_skip_key; // If true, skip writing pages 44-47 (3DES key) during ULC write
const MfUltralightData* write_data; // Saved pointer to source data for write phase
MfUltralightError error;
mbedtls_des3_context des_context;

View File

@@ -3,7 +3,7 @@
#include "base.h"
#include <flipper_format/flipper_format.h>
#define FIAT_MARELLI_PROTOCOL_NAME "Fiat Marelli"
#define FIAT_MARELLI_PROTOCOL_NAME "MARELLI"
typedef struct SubGhzProtocolDecoderFiatMarelli SubGhzProtocolDecoderFiatMarelli;
typedef struct SubGhzProtocolEncoderFiatMarelli SubGhzProtocolEncoderFiatMarelli;

View File

@@ -1055,10 +1055,10 @@ static uint32_t subghz_protocol_keeloq_check_remote_controller_selector(
case KEELOQ_LEARNING_SECURE:
bool reset_seed_back = false;
if((strcmp(furi_string_get_cstr(manufacture_code->name), "BFT") == 0)) {
if(instance->seed == 0) {
instance->seed = (fix & 0xFFFFFFF);
reset_seed_back = true;
}
//if(instance->seed == 0) {
instance->seed = (fix & 0xFFFFFFF);
reset_seed_back = true;
//}
}
man = subghz_protocol_keeloq_common_secure_learning(
fix, instance->seed, manufacture_code->key);