From 89c23828a0c19717aa2b10e94a72585274376e31 Mon Sep 17 00:00:00 2001 From: YoungJules Date: Wed, 19 Aug 2026 10:50:05 +0200 Subject: [PATCH 01/10] Create a small kgh_tools.exe that runs in Windows and can be used from another program to read a badge number, write a new KGH badge (given a badge number). You also have to provide a stamp (as .bin file, 4 bytes). The tool can also get a full set of info from a KGH badge in JSON form (including checking KGH crc for correctness). All based on publicly available knowledge, existing lua scripts, and careful obeservations! --- client/experimental_lib/CMakeLists.txt | 66 ++ client/experimental_lib/KGH_TOOLS_BUILD.md | 30 + .../example_c/legic_migrate_tool.c | 584 ++++++++++++++++++ client/src/cmdhflegic.c | 61 +- client/src/cmdhflegic.h | 6 + client/src/pm3line.c | 23 +- 6 files changed, 755 insertions(+), 15 deletions(-) create mode 100644 client/experimental_lib/KGH_TOOLS_BUILD.md create mode 100644 client/experimental_lib/example_c/legic_migrate_tool.c diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index 5c82868fb..764b25933 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -56,6 +56,13 @@ if (NOT SKIPPYTHON EQUAL 1) pkg_search_module(PYTHON3EMBED QUIET ${PYTHON3_PKGCONFIG}-embed) endif (NOT SKIPPYTHON EQUAL 1) +if (MINGW) + # Prefer static archives for kiosk-style single-exe deployments. + set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(ZLIB_USE_STATIC_LIBS ON) + set(BZIP2_USE_STATIC_LIBS ON) +endif() + # If cross-compiled, we need to init source and build. if (CMAKE_TOOLCHAIN_FILE) if (ANDROID) @@ -495,17 +502,26 @@ endif(NOT SKIPREADLINE EQUAL 1) if (BZIP2_FOUND) set(ADDITIONAL_DIRS ${BZIP2_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) + if (MINGW) + set(BZIP2_LIBRARIES /mingw64/lib/libbz2.a) + endif() set(ADDITIONAL_LNK ${BZIP2_LIBRARIES} ${ADDITIONAL_LNK}) endif (BZIP2_FOUND) if (LZ4_FOUND) set(ADDITIONAL_DIRS ${LZ4_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) + if (MINGW) + set(LZ4_LIBRARIES /mingw64/lib/liblz4.a) + endif() 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}) + if (MINGW) + set(ZLIB_LIBRARIES /mingw64/lib/libz.a) + endif() set(ADDITIONAL_LNK ${ZLIB_LIBRARIES} ${ADDITIONAL_LNK}) endif (ZLIB_FOUND) @@ -671,7 +687,32 @@ add_library(pm3rrg_rdv4 SHARED ${ADDITIONAL_SRC} ) +add_library(pm3rrg_rdv4_static STATIC + ${PM3_ROOT}/client/src/proxmark3.c + ${PM3_ROOT}/client/src/guidummy.cpp + ${TARGET_SOURCES} + ${ADDITIONAL_SRC} +) + +add_executable(kgh_tools + ${PM3_ROOT}/client/experimental_lib/example_c/legic_migrate_tool.c +) + +set_target_properties(kgh_tools PROPERTIES OUTPUT_NAME "kgh_tools") +if (MINGW) + target_link_options(kgh_tools PRIVATE -static -static-libgcc) +endif() + +target_link_libraries(kgh_tools PRIVATE pm3rrg_rdv4_static) +target_include_directories(kgh_tools PRIVATE + ${PM3_ROOT}/common + ${PM3_ROOT}/client/include + ${PM3_ROOT}/client/src + ${PM3_ROOT}/include +) + target_compile_definitions(pm3rrg_rdv4 PRIVATE LIBPM3) +target_compile_definitions(pm3rrg_rdv4_static PRIVATE LIBPM3) if (UNIX AND NOT APPLE) # Fail the build if a client source file is missing from TARGET_SOURCES. @@ -680,6 +721,7 @@ if (UNIX AND NOT APPLE) endif() target_compile_options(pm3rrg_rdv4 PUBLIC -Wall -Werror -O3) +target_compile_options(pm3rrg_rdv4_static PUBLIC -Wall -Werror -O3) if (EMBED_READLINE) if (NOT SKIPREADLINE EQUAL 1) add_dependencies(pm3rrg_rdv4 ncurses readline) @@ -726,6 +768,15 @@ target_include_directories(pm3rrg_rdv4 PRIVATE ${ADDITIONAL_DIRS} ) +target_include_directories(pm3rrg_rdv4_static PRIVATE + ${PM3_ROOT}/common + ${PM3_ROOT}/common_fpga + ${PM3_ROOT}/include + ${PM3_ROOT}/client/src + ${PM3_ROOT}/client/include + ${ADDITIONAL_DIRS} +) + if (NOT APPLE) # required for Raspberry Pi, but breaks with clang (OSX). Need to be at the end of the linker line. set(ADDITIONAL_LNK ${ADDITIONAL_LNK} -Wl,--as-needed -latomic -Wl,--no-as-needed) @@ -763,8 +814,23 @@ target_link_libraries(pm3rrg_rdv4 PRIVATE pm3rrg_rdv4_vec ${ADDITIONAL_LNK}) +target_link_libraries(pm3rrg_rdv4_static PRIVATE + m + pm3rrg_rdv4_mbedtls + pm3rrg_rdv4_cliparser + pm3rrg_rdv4_lua + pm3rrg_rdv4_tinycbor + pm3rrg_rdv4_amiibo + pm3rrg_rdv4_reveng + pm3rrg_rdv4_hardnested + pm3rrg_rdv4_id48 + pm3rrg_rdv4_mqtt + pm3rrg_rdv4_vec + ${ADDITIONAL_LNK}) + if (NOT SKIPPTHREAD EQUAL 1) target_link_libraries(pm3rrg_rdv4 PRIVATE pthread) + target_link_libraries(pm3rrg_rdv4_static PRIVATE pthread) endif (NOT SKIPPTHREAD EQUAL 1) if (NOT SKIPPYTHON EQUAL 1) diff --git a/client/experimental_lib/KGH_TOOLS_BUILD.md b/client/experimental_lib/KGH_TOOLS_BUILD.md new file mode 100644 index 000000000..bd969dc05 --- /dev/null +++ b/client/experimental_lib/KGH_TOOLS_BUILD.md @@ -0,0 +1,30 @@ +# kgh_tools Build Notes + +## Configure And Build + +Do this in ProxSpace: + +```bash +cd ~/proxmark3_rfrg/client/experimental_lib +rm -rf build-lite +cmake -G "MSYS Makefiles" -S . -B build-lite \ + -DSKIPPYTHON=1 \ + -DSKIPREADLINE=1 \ + -DSKIPJANSSYSTEM=1 \ + -DSKIPWHEREAMISYSTEM=1 \ + -DSKIPBT=1 +cmake --build build-lite --target kgh_tools -j +objdump -p build-lite/kgh_tools.exe | grep -i "DLL Name" +``` + +## Static Link Notes + +- `kgh_tools` is linked against `pm3rrg_rdv4_static`. +- On MinGW, CMake adds `-static -static-libgcc` for the target. +- If CMakeCache paths change between ProxSpace and a native shell, delete `build-lite` and reconfigure. + +## Existing Helper Scripts + +- `01make_lib.sh` creates `build/`, runs `cmake ..`, then `make -j`. +- `01make_lib_continue.sh` reruns `make -j` in `build/`. +- `00make_swig.sh` regenerates the SWIG wrappers before building the library. diff --git a/client/experimental_lib/example_c/legic_migrate_tool.c b/client/experimental_lib/example_c/legic_migrate_tool.c new file mode 100644 index 000000000..bb6e87eb1 --- /dev/null +++ b/client/experimental_lib/example_c/legic_migrate_tool.c @@ -0,0 +1,584 @@ +#include +#include +#include +#include +#include + +#include "pm3.h" +#include "pm3_cmd.h" +#include "legic.h" +#include "cmdhflegic.h" +#include "ui.h" +#include "util.h" +#include "util_posix.h" +#include "crc.h" + +static pm3 *open_pm3_with_retry(const char *port) { + for (int i = 0; i < 3; ++i) { + pm3 *dev = pm3_open(port); + if (g_session.pm3_present) { + return dev; + } + msleep(250); + } + return pm3_open(port); +} + +static bool has_flag(int argc, char *argv[], const char *flag) { + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], flag) == 0) { + return true; + } + } + return false; +} + +static const uint8_t k_legic_template_head[35] = { + 0x3e, 0x00, 0x00, 0xb6, 0x00, 0x60, 0xea, 0x9f, 0xff, 0x00, 0x00, 0x00, 0x11, 0x01, 0x06, 0x80, + 0x00, 0x00, 0xeb, 0xa1, 0x00, 0x00, 0x0d, 0xc0, 0x08, 0x00, 0x2f, 0x73, 0x73, 0x73, 0x73, 0x62, + 0x62, 0x62, 0x54 +}; + +static bool encode_badge_bcd(uint32_t badge, uint8_t out[3]) { + if (badge > 999999u) { + return false; + } + + uint32_t digits[6] = {0}; + for (int i = 5; i >= 0; --i) { + digits[i] = badge % 10u; + badge /= 10u; + } + + out[0] = (uint8_t)((digits[0] << 4) | digits[1]); + out[1] = (uint8_t)((digits[2] << 4) | digits[3]); + out[2] = (uint8_t)((digits[4] << 4) | digits[5]); + return true; +} + +static bool decode_badge_bcd(const uint8_t in[3], uint32_t *badge_out) { + uint32_t digits[6] = { + (uint32_t)(in[0] >> 4), (uint32_t)(in[0] & 0x0F), + (uint32_t)(in[1] >> 4), (uint32_t)(in[1] & 0x0F), + (uint32_t)(in[2] >> 4), (uint32_t)(in[2] & 0x0F) + }; + + for (size_t i = 0; i < 6; ++i) { + if (digits[i] > 9u) { + return false; + } + } + + *badge_out = digits[0] * 100000u + digits[1] * 10000u + digits[2] * 1000u + digits[3] * 100u + digits[4] * 10u + digits[5]; + return true; +} + +static bool parse_badge_text(const char *text, uint32_t *badge_out) { + if (text == NULL || *text == '\0') { + return false; + } + + char *end = NULL; + unsigned long badge = strtoul(text, &end, 10); + if (end == text || *end != '\0' || badge > 999999UL) { + return false; + } + + *badge_out = (uint32_t)badge; + return true; +} + +static bool parse_stamp_hex(const char *text, uint8_t stamp[4]) { + if (text == NULL || strlen(text) != 8) { + return false; + } + + for (int i = 0; i < 4; ++i) { + char byte_str[3] = { text[i * 2], text[i * 2 + 1], '\0' }; + char *end = NULL; + long value = strtol(byte_str, &end, 16); + if (end == byte_str || *end != '\0' || value < 0 || value > 255) { + return false; + } + stamp[i] = (uint8_t)value; + } + + return true; +} + +static bool load_stamp_file(const char *path, uint8_t stamp[4]) { + FILE *fp = fopen(path, "rb"); + if (fp == NULL) { + return false; + } + + uint8_t buf[4] = {0}; + size_t read_len = fread(buf, 1, sizeof(buf), fp); + int extra = fgetc(fp); + fclose(fp); + + if (read_len != sizeof(buf) || extra != EOF) { + return false; + } + + memcpy(stamp, buf, sizeof(buf)); + return true; +} + +static bool get_arg_value(int argc, char *argv[], const char *flag, const char **value) { + for (int i = 3; i < argc - 1; ++i) { + if (strcmp(argv[i], flag) == 0) { + *value = argv[i + 1]; + return true; + } + } + return false; +} + +static void apply_stamp(uint8_t *dump, size_t dump_len, const uint8_t stamp[4]) { + if (dump_len <= 30) { + return; + } + memcpy(dump + 27, stamp, 4); +} + +static void build_migration_dump(uint32_t badge, const uint8_t stamp[4], uint8_t *dump, size_t dump_len) { + memset(dump, 0, dump_len); + memcpy(dump, k_legic_template_head, sizeof(k_legic_template_head)); + apply_stamp(dump, dump_len, stamp); + uint8_t bcd[3] = {0}; + if (!encode_badge_bcd(badge, bcd)) { + fprintf(stderr, "badge number out of range\n"); + exit(EXIT_FAILURE); + } + + dump[31] = bcd[0]; + dump[32] = bcd[1]; + dump[33] = bcd[2]; + dump[34] = 0xBD; +} + +typedef struct { + uint8_t *dump; + uint16_t len; + legic_card_select_t card; +} legic_read_result_t; + +static bool read_legic_dump(legic_read_result_t *res, bool verbose) { + memset(res, 0, sizeof(*res)); + + uint8_t prev_print = g_printAndLog; + g_printAndLog &= ~PRINTANDLOG_PRINT; + + for (int attempt = 0; attempt < 3; ++attempt) { + free(res->dump); + memset(res, 0, sizeof(*res)); + + if (legic_get_type(&res->card) != PM3_SUCCESS) { + if (verbose) { + fprintf(stderr, "read_info retry %d: legic_get_type failed\n", attempt + 1); + } + msleep(100); + continue; + } + + res->dump = calloc(res->card.cardsize, sizeof(uint8_t)); + if (res->dump == NULL) { + g_printAndLog = prev_print; + return false; + } + + int status = legic_read_mem(0, res->card.cardsize, 0x55, res->dump, &res->len); + if (status != PM3_SUCCESS || res->len < 35) { + if (verbose) { + fprintf(stderr, "read_info retry %d: legic_read_mem failed\n", attempt + 1); + } + msleep(100); + continue; + } + + uint8_t crc = res->dump[4]; + if (CRC8Legic(res->dump, 4) != crc) { + if (verbose) { + fprintf(stderr, "read_info retry %d: header crc mismatch\n", attempt + 1); + } + msleep(100); + continue; + } + + for (uint16_t i = 22; i < res->len; ++i) { + res->dump[i] ^= crc; + } + + g_printAndLog = prev_print; + return true; + } + + free(res->dump); + memset(res, 0, sizeof(*res)); + g_printAndLog = prev_print; + return false; +} + +static void free_legic_read_result(legic_read_result_t *res) { + free(res->dump); + res->dump = NULL; + res->len = 0; + memset(&res->card, 0, sizeof(res->card)); +} + +static const char *classify_legic_card(const uint8_t *dump, uint16_t len) { + if (len < 9) { + return "unknown"; + } + + uint16_t dcf = ((uint16_t)dump[6] << 8) | dump[5]; + if (dcf == 0xFFFF) { + return "NM"; + } + + if (dcf > 60000u) { + return "IAM"; + } + + if ((dump[7] & 0x0F) == 0x0F && dump[8] == 0xFF) { + return "KGH"; + } + + return "unknown"; +} + +static bool check_kgh_crc_ok(const uint8_t *dump, uint16_t len) { + if (strcmp(classify_legic_card(dump, len), "KGH") != 0) { + return false; + } + + if (len <= 34) { + return false; + } + + uint8_t *copy = calloc(len, sizeof(uint8_t)); + if (copy == NULL) { + return false; + } + + memcpy(copy, dump, len); + const uint8_t uid[4] = { copy[0], copy[1], copy[2], copy[3] }; + bool ok = legic_clone_update_segment_crcs(copy, len, uid) && legic_clone_update_kgh_crcs(copy, len, uid) && (copy[34] == dump[34]); + free(copy); + return ok; +} + +static bool check_kgh_crc_calc(const uint8_t *dump, uint16_t len, uint8_t *crc_out) { + if (strcmp(classify_legic_card(dump, len), "KGH") != 0) { + return false; + } + + if (len <= 34) { + return false; + } + + uint8_t *copy = calloc(len, sizeof(uint8_t)); + if (copy == NULL) { + return false; + } + + memcpy(copy, dump, len); + const uint8_t uid[4] = { copy[0], copy[1], copy[2], copy[3] }; + if (!legic_clone_update_segment_crcs(copy, len, uid) || !legic_clone_update_kgh_crcs(copy, len, uid)) { + free(copy); + return false; + } + + *crc_out = copy[34]; + bool ok = (*crc_out == dump[34]); + free(copy); + return ok; +} + +static bool has_extra_segments(const uint8_t *dump, uint16_t len) { + size_t start = 22; + bool saw_first = false; + + while (start + 5 <= len) { + uint16_t seg_len = (((uint16_t)dump[start + 1] & 0x0F) << 8) | dump[start]; + if (seg_len < 5 || start + seg_len > len) { + break; + } + + if (saw_first) { + return true; + } + + saw_first = true; + if (start + seg_len >= len) { + break; + } + + start += seg_len; + } + + return false; +} + +static bool check_segment_crc_ok(const uint8_t *dump, uint16_t len) { + if (len < 27) { + return false; + } + + uint8_t prev_print = g_printAndLog; + g_printAndLog &= ~PRINTANDLOG_PRINT; + + uint8_t *copy = calloc(len, sizeof(uint8_t)); + if (copy == NULL) { + g_printAndLog = prev_print; + return false; + } + + memcpy(copy, dump, len); + const uint8_t uid[4] = { copy[0], copy[1], copy[2], copy[3] }; + bool ok = legic_clone_update_segment_crcs(copy, len, uid) && (memcmp(copy, dump, len) == 0); + free(copy); + g_printAndLog = prev_print; + return ok; +} + +static bool check_segment_crc_calc(const uint8_t *dump, uint16_t len, uint8_t *crc_out) { + if (len < 27) { + return false; + } + + uint8_t prev_print = g_printAndLog; + g_printAndLog &= ~PRINTANDLOG_PRINT; + + uint8_t *copy = calloc(len, sizeof(uint8_t)); + if (copy == NULL) { + g_printAndLog = prev_print; + return false; + } + + memcpy(copy, dump, len); + const uint8_t uid[4] = { copy[0], copy[1], copy[2], copy[3] }; + if (!legic_clone_update_segment_crcs(copy, len, uid)) { + free(copy); + g_printAndLog = prev_print; + return false; + } + + *crc_out = copy[26]; + bool ok = (memcmp(copy, dump, len) == 0); + free(copy); + g_printAndLog = prev_print; + return ok; +} + +static bool decode_badge_value(const uint8_t *dump, uint16_t len, uint32_t *badge_out) { + if (len < 34) { + return false; + } + return decode_badge_bcd(&dump[31], badge_out); +} + +static void print_json_string(const char *key, const char *value, bool last) { + printf("\"%s\": \"%s\"%s", key, value, last ? "" : ","); +} + +static void print_json_bool(const char *key, bool value, bool last) { + printf("\"%s\": %s%s", key, value ? "true" : "false", last ? "" : ","); +} + +static void print_json_uint32(const char *key, uint32_t value, bool last) { + printf("\"%s\": %u%s", key, value, last ? "" : ","); +} + +static void print_json_null(const char *key, bool last) { + printf("\"%s\": null%s", key, last ? "" : ","); +} + +static void print_read_info_json(const legic_read_result_t *res) { + const uint8_t *dump = res->dump; + uint16_t len = res->len; + uint32_t calc_mcc = (uint32_t)CRC8Legic((uint8_t *)dump, 4); + bool mcc_ok = (calc_mcc == dump[4]); + bool is_kgh = (strcmp(classify_legic_card(dump, len), "KGH") == 0); + bool segment_crc_ok = check_segment_crc_ok(dump, len); + bool kgh_crc_ok = check_kgh_crc_ok(dump, len); + uint8_t segment_header_crc_calc = 0; + uint8_t kgh_segment_crc_calc = 0; + (void)check_segment_crc_calc(dump, len, &segment_header_crc_calc); + (void)(is_kgh ? check_kgh_crc_calc(dump, len, &kgh_segment_crc_calc) : false); + char segment_header_crc_calc_hex[3] = {0}; + char kgh_segment_crc_calc_hex[3] = {0}; + snprintf(segment_header_crc_calc_hex, sizeof(segment_header_crc_calc_hex), "%02X", segment_header_crc_calc); + if (is_kgh) { + snprintf(kgh_segment_crc_calc_hex, sizeof(kgh_segment_crc_calc_hex), "%02X", kgh_segment_crc_calc); + } + bool extra_segments = has_extra_segments(dump, len); + + uint32_t badge = 0; + bool badge_ok = decode_badge_value(dump, len, &badge); + char badge_raw[16] = {0}; + if (!badge_ok) { + snprintf(badge_raw, sizeof(badge_raw), "0x%02X%02X%02X", dump[31], dump[32], dump[33]); + } + + printf("{"); + print_json_string("card_type", classify_legic_card(dump, len), false); + print_json_string("mcd", sprint_hex_inrow((uint8_t *)dump, 1), false); + print_json_string("msn", sprint_hex_inrow((uint8_t *)dump + 1, 3), false); + print_json_string("mcc", sprint_hex_inrow((uint8_t *)dump + 4, 1), false); + print_json_bool("mcc_ok", mcc_ok, false); + print_json_string("dcf", sprint_hex_inrow((uint8_t *)dump + 5, 2), false); + print_json_bool("segment_header_crc_ok", segment_crc_ok, false); + print_json_string("segment_header_crc", sprint_hex_inrow((uint8_t *)dump + 26, 1), false); + print_json_string("segment_header_crc_calc", segment_header_crc_calc_hex, false); + print_json_bool("is_kgh", is_kgh, false); + print_json_bool("has_extra_segments", extra_segments, false); + + if (is_kgh) { + char stamp[16] = {0}; + snprintf(stamp, sizeof(stamp), "%02X%02X%02X%02X", dump[27], dump[28], dump[29], dump[30]); + print_json_string("stamp", stamp, false); + print_json_string("kgh_segment_crc", sprint_hex_inrow((uint8_t *)dump + 34, 1), false); + print_json_string("kgh_segment_crc_calc", kgh_segment_crc_calc_hex, false); + if (badge_ok) { + print_json_uint32("badge_number", badge, false); + print_json_null("badge_number_raw", false); + print_json_string("badge_number_format", "dec", false); + } else { + print_json_null("badge_number", false); + print_json_string("badge_number_raw", badge_raw, false); + print_json_string("badge_number_format", "hex", false); + } + print_json_bool("kgh_segment_crc_ok", kgh_crc_ok, true); + } else { + print_json_null("stamp", false); + print_json_null("kgh_segment_crc", false); + print_json_null("kgh_segment_crc_calc", false); + print_json_null("badge_number", false); + print_json_null("badge_number_raw", false); + print_json_null("badge_number_format", false); + print_json_bool("kgh_segment_crc_ok", false, true); + } + printf("}\n"); +} + +static int read_badge_from_card(uint32_t *badge_out) { + uint8_t prev_print = g_printAndLog; + g_printAndLog &= ~PRINTANDLOG_PRINT; + + legic_card_select_t card; + if (legic_get_type(&card) != PM3_SUCCESS) { + g_printAndLog = prev_print; + return 1; + } + + uint8_t *dump = calloc(card.cardsize, sizeof(uint8_t)); + if (dump == NULL) { + g_printAndLog = prev_print; + return 1; + } + + uint16_t datalen = 0; + int status = legic_read_mem(0, card.cardsize, 0x55, dump, &datalen); + if (status != PM3_SUCCESS || datalen < 35) { + free(dump); + g_printAndLog = prev_print; + return 1; + } + + uint8_t crc = dump[4]; + if (CRC8Legic(dump, 4) != crc) { + free(dump); + g_printAndLog = prev_print; + return 1; + } + + for (uint16_t i = 22; i < datalen; ++i) { + dump[i] ^= crc; + } + + bool ok = decode_badge_bcd(&dump[31], badge_out); + free(dump); + g_printAndLog = prev_print; + return ok ? 0 : 1; +} + +int main(int argc, char *argv[]) { + if (argc < 3) { + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " %s read_badge_number [-v|--verbose]\n", argv[0]); + fprintf(stderr, " %s read_info\n", argv[0]); + fprintf(stderr, " %s write_card (--stamp-file | --stamp-hex <8hex>)\n", argv[0]); + return EXIT_FAILURE; + } + + g_session.incognito = true; + bool verbose = has_flag(argc, argv, "-v") || has_flag(argc, argv, "--verbose"); + g_printAndLog = verbose ? PRINTANDLOG_PRINT : 0; + pm3 *dev = open_pm3_with_retry(argv[1]); + int rc = EXIT_FAILURE; + + if (strcmp(argv[2], "read_badge_number") == 0) { + uint32_t badge = 0; + if (read_badge_from_card(&badge) == 0) { + printf("%u\n", badge); + rc = EXIT_SUCCESS; + } + } else if (strcmp(argv[2], "read_info") == 0) { + legic_read_result_t res; + if (read_legic_dump(&res, verbose)) { + print_read_info_json(&res); + rc = EXIT_SUCCESS; + free_legic_read_result(&res); + } else if (verbose) { + fprintf(stderr, "failed to read card info\n"); + } + } else if (strcmp(argv[2], "write_card") == 0) { + if (argc < 4) { + fprintf(stderr, "missing badge number\n"); + } else { + uint32_t badge = 0; + if (!parse_badge_text(argv[3], &badge)) { + fprintf(stderr, "invalid badge number\n"); + pm3_close(dev); + return EXIT_FAILURE; + } + const char *stamp_file = NULL; + const char *stamp_hex = NULL; + bool has_file = get_arg_value(argc, argv, "--stamp-file", &stamp_file); + bool has_hex = get_arg_value(argc, argv, "--stamp-hex", &stamp_hex); + if (has_file == has_hex) { + fprintf(stderr, "specify exactly one of --stamp-file or --stamp-hex\n"); + pm3_close(dev); + return EXIT_FAILURE; + } + + uint8_t stamp[4] = {0}; + if (has_file) { + if (!load_stamp_file(stamp_file, stamp)) { + fprintf(stderr, "failed to load stamp file\n"); + pm3_close(dev); + return EXIT_FAILURE; + } + } else if (!parse_stamp_hex(stamp_hex, stamp)) { + fprintf(stderr, "invalid stamp hex\n"); + pm3_close(dev); + return EXIT_FAILURE; + } + + uint8_t dump[35] = {0}; + build_migration_dump(badge, stamp, dump, sizeof(dump)); + if (legic_migrate_dump(dump, sizeof(dump), true, (const uint8_t[2]){0x60, 0xEA}, true) == PM3_SUCCESS) { + rc = EXIT_SUCCESS; + } else { + fprintf(stderr, "failed to write card\n"); + } + } + } else { + fprintf(stderr, "unknown command: %s\n", argv[2]); + } + + pm3_close(dev); + return rc; +} diff --git a/client/src/cmdhflegic.c b/client/src/cmdhflegic.c index 8d90fa6f6..367f9a86c 100644 --- a/client/src/cmdhflegic.c +++ b/client/src/cmdhflegic.c @@ -58,13 +58,13 @@ static bool legic_xor(uint8_t *data, uint16_t cardsize) { return true; } -static void legic_xor_with_crc(uint8_t *data, uint16_t cardsize, uint8_t crc) { +void legic_xor_with_crc(uint8_t *data, uint16_t cardsize, uint8_t crc) { for (uint16_t i = 22; i < cardsize; i++) { data[i] ^= crc; } } -static bool legic_clone_update_segment_crcs(uint8_t *data, size_t bytes_read, const uint8_t uid[4]) { +bool legic_clone_update_segment_crcs(uint8_t *data, size_t bytes_read, const uint8_t uid[4]) { // Segment headers are parsed from offset 22 everywhere else in LEGIC decoding. size_t start = 22; bool found_segment = false; @@ -96,7 +96,7 @@ static bool legic_clone_update_segment_crcs(uint8_t *data, size_t bytes_read, co return true; } -static bool legic_clone_update_kgh_crcs(uint8_t *data, size_t bytes_read, const uint8_t uid[4]) { +bool legic_clone_update_kgh_crcs(uint8_t *data, size_t bytes_read, const uint8_t uid[4]) { // Decoded segmented payload starts at byte 22. size_t start = 22; bool found_kgh = false; @@ -142,7 +142,7 @@ static bool legic_clone_update_kgh_crcs(uint8_t *data, size_t bytes_read, const return true; } -static int legic_write_bytes_to_tag(uint16_t offset, uint8_t iv, const uint8_t *data, size_t bytes_read, const char *verb) { +int legic_write_bytes_to_tag(uint16_t offset, uint8_t iv, const uint8_t *data, size_t bytes_read, const char *verb) { PrintAndLogEx(SUCCESS, "%s", verb); // fast push mode @@ -183,21 +183,68 @@ static int legic_write_bytes_to_tag(uint16_t offset, uint8_t iv, const uint8_t * PrintAndLogEx(NORMAL, ""); if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "Failed writing tag"); + PrintAndLogEx(WARNING, "Failed writing tag at offset %zu len %zu. Status: %d", i, len, resp.status); g_conn.block_after_ACK = false; return PM3_ERFTRANS; } - PrintAndLogEx(SUCCESS, "Wrote chunk [offset %zu | len %zu | total %zu", i, len, i + len); + PrintAndLogEx(SUCCESS, "Wrote chunk [offset %zu | len %zu | end %zu of %zu]", i, len, i + len, bytes_read); } g_conn.block_after_ACK = false; return PM3_SUCCESS; } -static int legic_write_dump_to_tag(uint8_t *dump, size_t bytes_read) { +int legic_write_dump_to_tag(uint8_t *dump, size_t bytes_read) { return legic_write_bytes_to_tag(7, 0x55, dump, bytes_read, "Restoring to card"); } +int legic_migrate_dump(uint8_t *dump, size_t bytes_read, bool rewrite_kgh, const uint8_t dcf[2], bool allow_dcf) { + if (dump == NULL || bytes_read <= 22) { + return PM3_EINVARG; + } + + if (dcf != NULL && !allow_dcf) { + return PM3_EINVARG; + } + + legic_card_select_t card; + if (legic_get_type(&card) != PM3_SUCCESS) { + return PM3_ESOFT; + } + + if (card.cardsize < bytes_read) { + return PM3_EFILE; + } + + uint8_t target_uid[4] = {0}; + memcpy(target_uid, card.uid, sizeof(target_uid)); + uint8_t target_mcc = (uint8_t)CRC8Legic(target_uid, sizeof(target_uid)); + + if (!legic_clone_update_segment_crcs(dump, bytes_read, target_uid)) { + return PM3_EFAILED; + } + if (rewrite_kgh && !legic_clone_update_kgh_crcs(dump, bytes_read, target_uid)) { + return PM3_EFAILED; + } + + memcpy(dump, target_uid, sizeof(target_uid)); + dump[4] = target_mcc; + legic_xor_with_crc(dump, bytes_read, dump[4]); + + int write_res = legic_write_dump_to_tag(dump, bytes_read); + if (write_res != PM3_SUCCESS) { + return write_res; + } + + if (dcf != NULL) { + uint8_t dcf_dump[7] = {0}; + memcpy(dcf_dump + 5, dcf, 2); + return legic_write_bytes_to_tag(5, 0x55, dcf_dump, sizeof(dcf_dump), "Applying explicit DCF update"); + } + + return PM3_SUCCESS; +} + static int CmdLegicMigrate(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf legic migrate", diff --git a/client/src/cmdhflegic.h b/client/src/cmdhflegic.h index 9f5681845..3ad60fe43 100644 --- a/client/src/cmdhflegic.h +++ b/client/src/cmdhflegic.h @@ -31,5 +31,11 @@ int legic_get_type(legic_card_select_t *card); void legic_chk_iv(uint32_t *iv); void legic_seteml(uint8_t *src, uint32_t offset, uint32_t numofbytes); int legic_read_mem(uint32_t offset, uint32_t len, uint32_t iv, uint8_t *out, uint16_t *outlen); +int legic_migrate_dump(uint8_t *dump, size_t bytes_read, bool rewrite_kgh, const uint8_t dcf[2], bool allow_dcf); +void legic_xor_with_crc(uint8_t *data, uint16_t cardsize, uint8_t crc); +bool legic_clone_update_segment_crcs(uint8_t *data, size_t bytes_read, const uint8_t uid[4]); +bool legic_clone_update_kgh_crcs(uint8_t *data, size_t bytes_read, const uint8_t uid[4]); +int legic_write_bytes_to_tag(uint16_t offset, uint8_t iv, const uint8_t *data, size_t bytes_read, const char *verb); +int legic_write_dump_to_tag(uint8_t *dump, size_t bytes_read); #endif diff --git a/client/src/pm3line.c b/client/src/pm3line.c index 5270a5fbe..27f03d1a0 100644 --- a/client/src/pm3line.c +++ b/client/src/pm3line.c @@ -186,15 +186,22 @@ char *pm3line_read(const char *s) { return linenoise(s); #else printf("%s", s); - char *answer = NULL; - size_t anslen = 0; - int ret; - if ((ret = getline(&answer, &anslen, stdin)) < 0) { - // TODO this happens also when kbd_enter_pressed() is used, with a key pressed or not - printf("DEBUG: getline returned %i", ret); - free(answer); - answer = NULL; + char input[1024] = {0}; + if (fgets(input, sizeof(input), stdin) == NULL) { + return NULL; } + + size_t len = strlen(input); + while (len > 0 && (input[len - 1] == '\n' || input[len - 1] == '\r')) { + input[--len] = '\0'; + } + + char *answer = calloc(len + 1, sizeof(char)); + if (answer == NULL) { + return NULL; + } + + memcpy(answer, input, len); return answer; #endif } From b1bc5b11522895996b0d66912923d179141651d6 Mon Sep 17 00:00:00 2001 From: YoungJules Date: Wed, 19 Aug 2026 11:25:46 +0200 Subject: [PATCH 02/10] Added quick doco on how to ssh from ProxSpace --- .../Development/GitHub_ssh_from_ProxSpace.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 doc/md/Development/GitHub_ssh_from_ProxSpace.md diff --git a/doc/md/Development/GitHub_ssh_from_ProxSpace.md b/doc/md/Development/GitHub_ssh_from_ProxSpace.md new file mode 100644 index 000000000..875f64c59 --- /dev/null +++ b/doc/md/Development/GitHub_ssh_from_ProxSpace.md @@ -0,0 +1,113 @@ +# GitHub SSH from ProxSpace + +This is a quick guide to configuring Git and SSH authentication for GitHub from ProxSpace. + +## Configure Git + +Configure your Git email address and username: + +```bash +git config --global user.email "You@YourEmail.com" +git config --global user.name "YourUserName" +``` + +## Generate an SSH key + +Use `ssh-keygen` to generate an Ed25519 SSH key pair if you do not already have one: + +```bash +ssh-keygen -t ed25519 -C "You@YourEmail.com" +``` + +Accept the defaults: + +```text +Enter file in which to save the key (/pm3/.ssh/id_ed25519): +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +``` + +This creates: + +```text +/pm3/.ssh/id_ed25519 # Private key +/pm3/.ssh/id_ed25519.pub # Public key +``` + +## Add the public key to GitHub + +Display the public key: + +```bash +cat /pm3/.ssh/id_ed25519.pub +``` + +Copy the complete output and add it to your GitHub account under: + +**Settings → SSH and GPG keys → New SSH key** + +Only the `.pub` file should be added to GitHub. Keep the private key private. + +## Test SSH authentication + +Test SSH authentication to GitHub using the specified private key, without attempting to open a shell session: + +```bash +ssh -i /pm3/.ssh/id_ed25519 -T git@github.com +``` + +A successful response looks like: + +```text +Hi YourUserName! You've successfully authenticated, but GitHub does not provide shell access. +``` + +## Check the Git remote + +Check that the repository remote is configured to use SSH rather than HTTPS: + +```bash +git remote -v +``` + +If it shows HTTPS: + +```text +origin https://github.com/YourUserName/proxmark3.git (fetch) +origin https://github.com/YourUserName/proxmark3.git (push) +``` + +Git will still try to authenticate over HTTPS and may prompt for a username/password. + +Change the remote to use SSH: + +```bash +git remote set-url origin git@github.com:YourUserName/proxmark3.git +``` + +Check it again: + +```bash +git remote -v +``` + +It should now show: + +```text +origin git@github.com:YourUserName/proxmark3.git (fetch) +origin git@github.com:YourUserName/proxmark3.git (push) +``` + +## Push + +Git should now use SSH authentication when pushing: + +```bash +git push origin +``` + +For example: + +```bash +git push origin feature/my_fab_feature +``` From e3ca960ae00d97e82605f72ef5a1de76c3329c42 Mon Sep 17 00:00:00 2001 From: YoungJules Date: Wed, 19 Aug 2026 11:32:36 +0200 Subject: [PATCH 03/10] Update wording --- client/experimental_lib/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index 764b25933..e49c52d4a 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -57,7 +57,7 @@ if (NOT SKIPPYTHON EQUAL 1) endif (NOT SKIPPYTHON EQUAL 1) if (MINGW) - # Prefer static archives for kiosk-style single-exe deployments. + # Prefer static archives for single-exe deployments. set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) set(ZLIB_USE_STATIC_LIBS ON) set(BZIP2_USE_STATIC_LIBS ON) From 2b463e4bf910d5d023ace3cc78a1fdff973d6a59 Mon Sep 17 00:00:00 2001 From: YoungJules Date: Wed, 19 Aug 2026 11:53:06 +0200 Subject: [PATCH 04/10] Remove suffix from repo name in build doco --- client/experimental_lib/KGH_TOOLS_BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/experimental_lib/KGH_TOOLS_BUILD.md b/client/experimental_lib/KGH_TOOLS_BUILD.md index bd969dc05..991665b3c 100644 --- a/client/experimental_lib/KGH_TOOLS_BUILD.md +++ b/client/experimental_lib/KGH_TOOLS_BUILD.md @@ -5,7 +5,7 @@ Do this in ProxSpace: ```bash -cd ~/proxmark3_rfrg/client/experimental_lib +cd ~/proxmark3/client/experimental_lib rm -rf build-lite cmake -G "MSYS Makefiles" -S . -B build-lite \ -DSKIPPYTHON=1 \ From 51752c3e1df8cf0b48a6acdffbe48cd3863435b8 Mon Sep 17 00:00:00 2001 From: YoungJules Date: Wed, 19 Aug 2026 12:22:30 +0200 Subject: [PATCH 05/10] Updated documentation to add usage, how to build in Linux as well as Windows (ProxSpace) --- client/experimental_lib/KGH_TOOLS.md | 74 ++++++++++++++++++++++ client/experimental_lib/KGH_TOOLS_BUILD.md | 30 --------- 2 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 client/experimental_lib/KGH_TOOLS.md delete mode 100644 client/experimental_lib/KGH_TOOLS_BUILD.md diff --git a/client/experimental_lib/KGH_TOOLS.md b/client/experimental_lib/KGH_TOOLS.md new file mode 100644 index 000000000..26a7c93d4 --- /dev/null +++ b/client/experimental_lib/KGH_TOOLS.md @@ -0,0 +1,74 @@ +# kgh_tools + +## Running + +`kgh_tools` is a small CLI wrapper around the LEGIC migration helpers. + +Examples: + +```bash +./kgh_tools COM13 read_badge_number +./kgh_tools COM13 read_info +./kgh_tools COM13 write_card 600001 --stamp-file stamp.bin +``` + +Commands: + +- `read_badge_number` prints the badge number only. +- `read_info` prints one-line JSON. +- `write_card (--stamp-file | --stamp-hex <8hex>)` writes the card. + +Notes: + +- Windows ProxSpace examples use `COM13`. +- The stamp is 4 bytes and specific to your site. + It can be specified as a .bin (use .e.g hexedit to create it) + Or as a command-line option (less secure) as --stamp-hex (e.g.) 003EF417 + You can find the stamp on any card in use at your site by using read_info (see above) +- Linux examples usually use `/dev/ttyACM0` or similar. +- `--verbose` can be useful for `read_badge_number` and `write_card`, if they fail for mysterious reasons. + +## Building in ProxSpace (for Windows) + +Do this in ProxSpace: + +```bash +cd ~/proxmark3/client/experimental_lib +rm -rf build-lite +cmake -G "MSYS Makefiles" -S . -B build-lite \ + -DSKIPPYTHON=1 \ + -DSKIPREADLINE=1 \ + -DSKIPJANSSYSTEM=1 \ + -DSKIPWHEREAMISYSTEM=1 \ + -DSKIPBT=1 +cmake --build build-lite --target kgh_tools -j +objdump -p build-lite/kgh_tools.exe | grep -i "DLL Name" +``` + +## Building Linux + +```bash +cd client/experimental_lib +rm -rf build-linux +cmake -S . -B build-linux \ + -DSKIPPYTHON=1 \ + -DSKIPREADLINE=1 \ + -DSKIPJANSSYSTEM=1 \ + -DSKIPWHEREAMISYSTEM=1 \ + -DSKIPBT=1 +cmake --build build-linux --target kgh_tools -j +ldd build-linux/kgh_tools +``` + +## Static Link Notes + +- `kgh_tools` is linked against `pm3rrg_rdv4_static`. +- On MinGW, CMake adds `-static -static-libgcc` for the target. +- Linux builds are not the same as the ProxSpace single-EXE deployment. +- If CMakeCache paths change between ProxSpace and a native shell, delete the build dir and reconfigure. + +## Existing Helper Scripts + +- `01make_lib.sh` creates `build/`, runs `cmake ..`, then `make -j`. +- `01make_lib_continue.sh` reruns `make -j` in `build/`. +- `00make_swig.sh` regenerates the SWIG wrappers before building the library. diff --git a/client/experimental_lib/KGH_TOOLS_BUILD.md b/client/experimental_lib/KGH_TOOLS_BUILD.md deleted file mode 100644 index 991665b3c..000000000 --- a/client/experimental_lib/KGH_TOOLS_BUILD.md +++ /dev/null @@ -1,30 +0,0 @@ -# kgh_tools Build Notes - -## Configure And Build - -Do this in ProxSpace: - -```bash -cd ~/proxmark3/client/experimental_lib -rm -rf build-lite -cmake -G "MSYS Makefiles" -S . -B build-lite \ - -DSKIPPYTHON=1 \ - -DSKIPREADLINE=1 \ - -DSKIPJANSSYSTEM=1 \ - -DSKIPWHEREAMISYSTEM=1 \ - -DSKIPBT=1 -cmake --build build-lite --target kgh_tools -j -objdump -p build-lite/kgh_tools.exe | grep -i "DLL Name" -``` - -## Static Link Notes - -- `kgh_tools` is linked against `pm3rrg_rdv4_static`. -- On MinGW, CMake adds `-static -static-libgcc` for the target. -- If CMakeCache paths change between ProxSpace and a native shell, delete `build-lite` and reconfigure. - -## Existing Helper Scripts - -- `01make_lib.sh` creates `build/`, runs `cmake ..`, then `make -j`. -- `01make_lib_continue.sh` reruns `make -j` in `build/`. -- `00make_swig.sh` regenerates the SWIG wrappers before building the library. From 591551c2366d863e773ec240fdfbaa89aa510702 Mon Sep 17 00:00:00 2001 From: YoungJules Date: Wed, 19 Aug 2026 22:50:41 +0200 Subject: [PATCH 06/10] Removed extra doc file not relevant to kgh tools pr --- .../Development/GitHub_ssh_from_ProxSpace.md | 113 ------------------ 1 file changed, 113 deletions(-) delete mode 100644 doc/md/Development/GitHub_ssh_from_ProxSpace.md diff --git a/doc/md/Development/GitHub_ssh_from_ProxSpace.md b/doc/md/Development/GitHub_ssh_from_ProxSpace.md deleted file mode 100644 index 875f64c59..000000000 --- a/doc/md/Development/GitHub_ssh_from_ProxSpace.md +++ /dev/null @@ -1,113 +0,0 @@ -# GitHub SSH from ProxSpace - -This is a quick guide to configuring Git and SSH authentication for GitHub from ProxSpace. - -## Configure Git - -Configure your Git email address and username: - -```bash -git config --global user.email "You@YourEmail.com" -git config --global user.name "YourUserName" -``` - -## Generate an SSH key - -Use `ssh-keygen` to generate an Ed25519 SSH key pair if you do not already have one: - -```bash -ssh-keygen -t ed25519 -C "You@YourEmail.com" -``` - -Accept the defaults: - -```text -Enter file in which to save the key (/pm3/.ssh/id_ed25519): -Enter passphrase (empty for no passphrase): -Enter same passphrase again: -``` - -This creates: - -```text -/pm3/.ssh/id_ed25519 # Private key -/pm3/.ssh/id_ed25519.pub # Public key -``` - -## Add the public key to GitHub - -Display the public key: - -```bash -cat /pm3/.ssh/id_ed25519.pub -``` - -Copy the complete output and add it to your GitHub account under: - -**Settings → SSH and GPG keys → New SSH key** - -Only the `.pub` file should be added to GitHub. Keep the private key private. - -## Test SSH authentication - -Test SSH authentication to GitHub using the specified private key, without attempting to open a shell session: - -```bash -ssh -i /pm3/.ssh/id_ed25519 -T git@github.com -``` - -A successful response looks like: - -```text -Hi YourUserName! You've successfully authenticated, but GitHub does not provide shell access. -``` - -## Check the Git remote - -Check that the repository remote is configured to use SSH rather than HTTPS: - -```bash -git remote -v -``` - -If it shows HTTPS: - -```text -origin https://github.com/YourUserName/proxmark3.git (fetch) -origin https://github.com/YourUserName/proxmark3.git (push) -``` - -Git will still try to authenticate over HTTPS and may prompt for a username/password. - -Change the remote to use SSH: - -```bash -git remote set-url origin git@github.com:YourUserName/proxmark3.git -``` - -Check it again: - -```bash -git remote -v -``` - -It should now show: - -```text -origin git@github.com:YourUserName/proxmark3.git (fetch) -origin git@github.com:YourUserName/proxmark3.git (push) -``` - -## Push - -Git should now use SSH authentication when pushing: - -```bash -git push origin -``` - -For example: - -```bash -git push origin feature/my_fab_feature -``` From 3632166bb4be65c8ee289a6aa7464aef0b3b82a8 Mon Sep 17 00:00:00 2001 From: YoungJules Date: Tue, 25 Aug 2026 17:20:12 +0200 Subject: [PATCH 07/10] Revert pm3line fallback change --- client/src/pm3line.c | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/client/src/pm3line.c b/client/src/pm3line.c index 27f03d1a0..5270a5fbe 100644 --- a/client/src/pm3line.c +++ b/client/src/pm3line.c @@ -186,22 +186,15 @@ char *pm3line_read(const char *s) { return linenoise(s); #else printf("%s", s); - char input[1024] = {0}; - if (fgets(input, sizeof(input), stdin) == NULL) { - return NULL; + char *answer = NULL; + size_t anslen = 0; + int ret; + if ((ret = getline(&answer, &anslen, stdin)) < 0) { + // TODO this happens also when kbd_enter_pressed() is used, with a key pressed or not + printf("DEBUG: getline returned %i", ret); + free(answer); + answer = NULL; } - - size_t len = strlen(input); - while (len > 0 && (input[len - 1] == '\n' || input[len - 1] == '\r')) { - input[--len] = '\0'; - } - - char *answer = calloc(len + 1, sizeof(char)); - if (answer == NULL) { - return NULL; - } - - memcpy(answer, input, len); return answer; #endif } From 9def79f8be8492c9ceaf95b3776c44dbf2442e5b Mon Sep 17 00:00:00 2001 From: YoungJules Date: Tue, 25 Aug 2026 17:24:09 +0200 Subject: [PATCH 08/10] Fix extra segments identification if first segment also the last --- client/experimental_lib/example_c/legic_migrate_tool.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/experimental_lib/example_c/legic_migrate_tool.c b/client/experimental_lib/example_c/legic_migrate_tool.c index bb6e87eb1..1f4d298a6 100644 --- a/client/experimental_lib/example_c/legic_migrate_tool.c +++ b/client/experimental_lib/example_c/legic_migrate_tool.c @@ -302,6 +302,7 @@ static bool has_extra_segments(const uint8_t *dump, uint16_t len) { while (start + 5 <= len) { uint16_t seg_len = (((uint16_t)dump[start + 1] & 0x0F) << 8) | dump[start]; + bool is_last = ((dump[start + 1] & 0x08) != 0); if (seg_len < 5 || start + seg_len > len) { break; } @@ -311,7 +312,7 @@ static bool has_extra_segments(const uint8_t *dump, uint16_t len) { } saw_first = true; - if (start + seg_len >= len) { + if (is_last) { break; } From 08cf2e430ef29adb2fd0e987bf0fb93622b566e9 Mon Sep 17 00:00:00 2001 From: YoungJules Date: Mon, 31 Aug 2026 13:14:35 +0200 Subject: [PATCH 09/10] Added possibilty to write a found stamp to a binary file. --- client/experimental_lib/KGH_TOOLS.md | 3 ++ .../example_c/legic_migrate_tool.c | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/client/experimental_lib/KGH_TOOLS.md b/client/experimental_lib/KGH_TOOLS.md index 26a7c93d4..c646e2c47 100644 --- a/client/experimental_lib/KGH_TOOLS.md +++ b/client/experimental_lib/KGH_TOOLS.md @@ -9,6 +9,8 @@ Examples: ```bash ./kgh_tools COM13 read_badge_number ./kgh_tools COM13 read_info +./kgh_tools COM13 read_stamp +./kgh_tools COM13 read_stamp -o stamp.bin ./kgh_tools COM13 write_card 600001 --stamp-file stamp.bin ``` @@ -16,6 +18,7 @@ Commands: - `read_badge_number` prints the badge number only. - `read_info` prints one-line JSON. +- `read_stamp` prints the stamp as 8 hex chars, or writes raw 4 bytes with `-o/--output-file`. - `write_card (--stamp-file | --stamp-hex <8hex>)` writes the card. Notes: diff --git a/client/experimental_lib/example_c/legic_migrate_tool.c b/client/experimental_lib/example_c/legic_migrate_tool.c index 1f4d298a6..bedbcef3a 100644 --- a/client/experimental_lib/example_c/legic_migrate_tool.c +++ b/client/experimental_lib/example_c/legic_migrate_tool.c @@ -125,6 +125,17 @@ static bool load_stamp_file(const char *path, uint8_t stamp[4]) { return true; } +static bool save_stamp_file(const char *path, const uint8_t stamp[4]) { + FILE *fp = fopen(path, "wb"); + if (fp == NULL) { + return false; + } + + size_t written = fwrite(stamp, 1, 4, fp); + fclose(fp); + return written == 4; +} + static bool get_arg_value(int argc, char *argv[], const char *flag, const char **value) { for (int i = 3; i < argc - 1; ++i) { if (strcmp(argv[i], flag) == 0) { @@ -380,6 +391,15 @@ static bool decode_badge_value(const uint8_t *dump, uint16_t len, uint32_t *badg return decode_badge_bcd(&dump[31], badge_out); } +static bool extract_stamp(const uint8_t *dump, uint16_t len, uint8_t stamp[4]) { + if (len <= 30) { + return false; + } + + memcpy(stamp, dump + 27, 4); + return true; +} + static void print_json_string(const char *key, const char *value, bool last) { printf("\"%s\": \"%s\"%s", key, value, last ? "" : ","); } @@ -510,6 +530,7 @@ int main(int argc, char *argv[]) { fprintf(stderr, "Usage:\n"); fprintf(stderr, " %s read_badge_number [-v|--verbose]\n", argv[0]); fprintf(stderr, " %s read_info\n", argv[0]); + fprintf(stderr, " %s read_stamp [-o|--output-file ]\n", argv[0]); fprintf(stderr, " %s write_card (--stamp-file | --stamp-hex <8hex>)\n", argv[0]); return EXIT_FAILURE; } @@ -535,6 +556,29 @@ int main(int argc, char *argv[]) { } else if (verbose) { fprintf(stderr, "failed to read card info\n"); } + } else if (strcmp(argv[2], "read_stamp") == 0) { + const char *output_file = NULL; + bool has_output = get_arg_value(argc, argv, "-o", &output_file) || get_arg_value(argc, argv, "--output-file", &output_file); + + legic_read_result_t res; + if (read_legic_dump(&res, verbose)) { + uint8_t stamp[4] = {0}; + if (extract_stamp(res.dump, res.len, stamp)) { + if (has_output) { + if (save_stamp_file(output_file, stamp)) { + rc = EXIT_SUCCESS; + } else { + fprintf(stderr, "failed to write stamp file\n"); + } + } else { + printf("%02X%02X%02X%02X\n", stamp[0], stamp[1], stamp[2], stamp[3]); + rc = EXIT_SUCCESS; + } + } + free_legic_read_result(&res); + } else if (verbose) { + fprintf(stderr, "failed to read stamp\n"); + } } else if (strcmp(argv[2], "write_card") == 0) { if (argc < 4) { fprintf(stderr, "missing badge number\n"); From 71ca59a8ad3e4a269c32409be936e5de05dd5dea Mon Sep 17 00:00:00 2001 From: Julian Isaac Date: Fri, 4 Sep 2026 23:14:22 +0200 Subject: [PATCH 10/10] Moved the new tool to tools/kgh and renamed it legic_kgh_tool. Added some validation so we don't return random bytes from non-kgh cards. Applied some styling to the new code. Added GPL header to new source file. Cleaned up the static build. --- client/experimental_lib/CMakeLists.txt | 128 +++++++----------- tools/kgh/CMakeLists.txt | 35 +++++ .../KGH_TOOLS.md => tools/kgh/README.md | 46 +++---- .../kgh/legic_kgh_tool.c | 38 +++++- 4 files changed, 135 insertions(+), 112 deletions(-) create mode 100644 tools/kgh/CMakeLists.txt rename client/experimental_lib/KGH_TOOLS.md => tools/kgh/README.md (52%) rename client/experimental_lib/example_c/legic_migrate_tool.c => tools/kgh/legic_kgh_tool.c (92%) diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index 417dba71e..ea54e5577 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -523,26 +523,17 @@ endif(NOT SKIPREADLINE EQUAL 1) if (BZIP2_FOUND) set(ADDITIONAL_DIRS ${BZIP2_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) - if (MINGW) - set(BZIP2_LIBRARIES /mingw64/lib/libbz2.a) - endif() set(ADDITIONAL_LNK ${BZIP2_LIBRARIES} ${ADDITIONAL_LNK}) endif (BZIP2_FOUND) if (LZ4_FOUND) set(ADDITIONAL_DIRS ${LZ4_INCLUDE_DIRS} ${ADDITIONAL_DIRS}) - if (MINGW) - set(LZ4_LIBRARIES /mingw64/lib/liblz4.a) - endif() 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}) - if (MINGW) - set(ZLIB_LIBRARIES /mingw64/lib/libz.a) - endif() set(ADDITIONAL_LNK ${ZLIB_LIBRARIES} ${ADDITIONAL_LNK}) endif (ZLIB_FOUND) @@ -715,25 +706,19 @@ add_library(pm3rrg_rdv4_static STATIC ${ADDITIONAL_SRC} ) -add_executable(kgh_tools - ${PM3_ROOT}/client/experimental_lib/example_c/legic_migrate_tool.c -) - -set_target_properties(kgh_tools PROPERTIES OUTPUT_NAME "kgh_tools") -if (MINGW) - target_link_options(kgh_tools PRIVATE -static -static-libgcc) -endif() - -target_link_libraries(kgh_tools PRIVATE pm3rrg_rdv4_static) -target_include_directories(kgh_tools PRIVATE - ${PM3_ROOT}/common - ${PM3_ROOT}/client/include - ${PM3_ROOT}/client/src - ${PM3_ROOT}/include -) - -target_compile_definitions(pm3rrg_rdv4 PRIVATE LIBPM3) -target_compile_definitions(pm3rrg_rdv4_static PRIVATE LIBPM3) +set(PM3_LIBRARY_TARGETS pm3rrg_rdv4 pm3rrg_rdv4_static) +foreach (target IN LISTS PM3_LIBRARY_TARGETS) + target_compile_definitions(${target} PRIVATE LIBPM3) + target_compile_options(${target} PUBLIC -Wall -Werror -O3) + target_include_directories(${target} PRIVATE + ${PM3_ROOT}/common + ${PM3_ROOT}/common_fpga + ${PM3_ROOT}/include + ${PM3_ROOT}/client/src + ${PM3_ROOT}/client/include + ${ADDITIONAL_DIRS} + ) +endforeach() if (UNIX AND NOT APPLE) # Fail the build if a client source file is missing from TARGET_SOURCES. @@ -741,18 +726,22 @@ if (UNIX AND NOT APPLE) set_property(TARGET pm3rrg_rdv4 APPEND_STRING PROPERTY LINK_FLAGS " -Wl,--no-undefined") endif() -target_compile_options(pm3rrg_rdv4 PUBLIC -Wall -Werror -O3) -target_compile_options(pm3rrg_rdv4_static PUBLIC -Wall -Werror -O3) if (EMBED_READLINE) if (NOT SKIPREADLINE EQUAL 1) - add_dependencies(pm3rrg_rdv4 ncurses readline) + foreach (target IN LISTS PM3_LIBRARY_TARGETS) + add_dependencies(${target} ncurses readline) + endforeach() endif (NOT SKIPREADLINE EQUAL 1) endif (EMBED_READLINE) if (EMBED_BZIP2) - add_dependencies(pm3rrg_rdv4 bzip2) + foreach (target IN LISTS PM3_LIBRARY_TARGETS) + add_dependencies(${target} bzip2) + endforeach() endif (EMBED_BZIP2) if (EMBED_LZ4) - add_dependencies(pm3rrg_rdv4 lz4) + foreach (target IN LISTS PM3_LIBRARY_TARGETS) + add_dependencies(${target} lz4) + endforeach() endif (EMBED_LZ4) if (MINGW) @@ -760,7 +749,9 @@ if (MINGW) # and setting _ISOC99_SOURCE sets internally __USE_MINGW_ANSI_STDIO=1 # FTR __USE_MINGW_ANSI_STDIO seems deprecated in Mingw32 # but not Mingw64 https://fr.osdn.net/projects/mingw/lists/archive/users/2019-January/000199.html - target_compile_definitions(pm3rrg_rdv4 PRIVATE _ISOC99_SOURCE) + foreach (target IN LISTS PM3_LIBRARY_TARGETS) + target_compile_definitions(${target} PRIVATE _ISOC99_SOURCE) + endforeach() set(CMAKE_C_FLAGS "-mno-ms-bitfields -fexec-charset=cp850 ${CMAKE_C_FLAGS}") set(CMAKE_CXX_FLAGS "-mno-ms-bitfields -fexec-charset=cp850 ${CMAKE_CXX_FLAGS}") @@ -769,24 +760,6 @@ if (MINGW) set(ADDITIONAL_LNK ws2_32 ${ADDITIONAL_LNK}) endif (MINGW) -target_include_directories(pm3rrg_rdv4 PRIVATE - ${PM3_ROOT}/common - ${PM3_ROOT}/common_fpga - ${PM3_ROOT}/include - ${PM3_ROOT}/client/src - ${PM3_ROOT}/client/include - ${ADDITIONAL_DIRS} -) - -target_include_directories(pm3rrg_rdv4_static PRIVATE - ${PM3_ROOT}/common - ${PM3_ROOT}/common_fpga - ${PM3_ROOT}/include - ${PM3_ROOT}/client/src - ${PM3_ROOT}/client/include - ${ADDITIONAL_DIRS} -) - if (NOT APPLE) # required for Raspberry Pi, but breaks with clang (OSX). Need to be at the end of the linker line. set(ADDITIONAL_LNK ${ADDITIONAL_LNK} -Wl,--as-needed -latomic -Wl,--no-as-needed) @@ -810,37 +783,26 @@ if (NOT WHEREAMI_FOUND) set(ADDITIONAL_LNK pm3rrg_rdv4_whereami ${ADDITIONAL_LNK}) endif (NOT WHEREAMI_FOUND) -target_link_libraries(pm3rrg_rdv4 PRIVATE - m - pm3rrg_rdv4_mbedtls - pm3rrg_rdv4_cliparser - pm3rrg_rdv4_lua - pm3rrg_rdv4_tinycbor - pm3rrg_rdv4_amiibo - pm3rrg_rdv4_reveng - pm3rrg_rdv4_hardnested - pm3rrg_rdv4_id48 - pm3rrg_rdv4_mqtt - pm3rrg_rdv4_vec - ${ADDITIONAL_LNK}) - -target_link_libraries(pm3rrg_rdv4_static PRIVATE - m - pm3rrg_rdv4_mbedtls - pm3rrg_rdv4_cliparser - pm3rrg_rdv4_lua - pm3rrg_rdv4_tinycbor - pm3rrg_rdv4_amiibo - pm3rrg_rdv4_reveng - pm3rrg_rdv4_hardnested - pm3rrg_rdv4_id48 - pm3rrg_rdv4_mqtt - pm3rrg_rdv4_vec - ${ADDITIONAL_LNK}) +foreach (target IN LISTS PM3_LIBRARY_TARGETS) + target_link_libraries(${target} PRIVATE + m + pm3rrg_rdv4_mbedtls + pm3rrg_rdv4_cliparser + pm3rrg_rdv4_lua + pm3rrg_rdv4_tinycbor + pm3rrg_rdv4_amiibo + pm3rrg_rdv4_reveng + pm3rrg_rdv4_hardnested + pm3rrg_rdv4_id48 + pm3rrg_rdv4_mqtt + pm3rrg_rdv4_vec + ${ADDITIONAL_LNK}) +endforeach() if (NOT SKIPPTHREAD EQUAL 1) - target_link_libraries(pm3rrg_rdv4 PRIVATE pthread) - target_link_libraries(pm3rrg_rdv4_static PRIVATE pthread) + foreach (target IN LISTS PM3_LIBRARY_TARGETS) + target_link_libraries(${target} PRIVATE pthread) + endforeach() endif (NOT SKIPPTHREAD EQUAL 1) if (NOT SKIPPYTHON EQUAL 1) @@ -852,4 +814,6 @@ if (NOT SKIPPYTHON EQUAL 1) endif (PYTHON3EMBED_FOUND OR PYTHON3_FOUND) endif (NOT SKIPPYTHON EQUAL 1) -target_link_directories(pm3rrg_rdv4 PRIVATE ${ADDITIONAL_LNKDIRS}) +foreach (target IN LISTS PM3_LIBRARY_TARGETS) + target_link_directories(${target} PRIVATE ${ADDITIONAL_LNKDIRS}) +endforeach() diff --git a/tools/kgh/CMakeLists.txt b/tools/kgh/CMakeLists.txt new file mode 100644 index 000000000..27c62eb4f --- /dev/null +++ b/tools/kgh/CMakeLists.txt @@ -0,0 +1,35 @@ +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +cmake_minimum_required(VERSION 3.10) +project(legic_kgh_tool) + +set(PM3_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..) + +add_subdirectory(${PM3_ROOT}/client/experimental_lib ${CMAKE_BINARY_DIR}/experimental_lib) + +add_executable(legic_kgh_tool legic_kgh_tool.c) +target_link_libraries(legic_kgh_tool PRIVATE pm3rrg_rdv4_static) +target_include_directories(legic_kgh_tool PRIVATE + ${PM3_ROOT}/common + ${PM3_ROOT}/client/include + ${PM3_ROOT}/client/src + ${PM3_ROOT}/include +) + +if (MINGW) + target_link_options(legic_kgh_tool PRIVATE -static -static-libgcc) +endif() diff --git a/client/experimental_lib/KGH_TOOLS.md b/tools/kgh/README.md similarity index 52% rename from client/experimental_lib/KGH_TOOLS.md rename to tools/kgh/README.md index c646e2c47..d3fff500e 100644 --- a/client/experimental_lib/KGH_TOOLS.md +++ b/tools/kgh/README.md @@ -1,17 +1,18 @@ -# kgh_tools +# legic_kgh_tool ## Running -`kgh_tools` is a small CLI wrapper around the LEGIC migration helpers. +`legic_kgh_tool` reads KGH LEGIC card information, badge numbers, and stamps, +and writes the supported KGH card layout. Examples: ```bash -./kgh_tools COM13 read_badge_number -./kgh_tools COM13 read_info -./kgh_tools COM13 read_stamp -./kgh_tools COM13 read_stamp -o stamp.bin -./kgh_tools COM13 write_card 600001 --stamp-file stamp.bin +./legic_kgh_tool COM13 read_badge_number +./legic_kgh_tool COM13 read_info +./legic_kgh_tool COM13 read_stamp +./legic_kgh_tool COM13 read_stamp -o stamp.bin +./legic_kgh_tool COM13 write_card 600001 --stamp-file stamp.bin ``` Commands: @@ -19,15 +20,14 @@ Commands: - `read_badge_number` prints the badge number only. - `read_info` prints one-line JSON. - `read_stamp` prints the stamp as 8 hex chars, or writes raw 4 bytes with `-o/--output-file`. -- `write_card (--stamp-file | --stamp-hex <8hex>)` writes the card. +- `write_card (--stamp-file | --stamp-hex <8hex>)` writes the supported KGH layout. Notes: - Windows ProxSpace examples use `COM13`. - The stamp is 4 bytes and specific to your site. - It can be specified as a .bin (use .e.g hexedit to create it) - Or as a command-line option (less secure) as --stamp-hex (e.g.) 003EF417 - You can find the stamp on any card in use at your site by using read_info (see above) + It can be specified as a `.bin` file or as `--stamp-hex` (for example, `003EF417`). +- You can retrieve the stamp from a supported KGH card with `read_info` or `read_stamp`. - Linux examples usually use `/dev/ttyACM0` or similar. - `--verbose` can be useful for `read_badge_number` and `write_card`, if they fail for mysterious reasons. @@ -36,42 +36,36 @@ Notes: Do this in ProxSpace: ```bash -cd ~/proxmark3/client/experimental_lib +cd ~/proxmark3/tools/kgh rm -rf build-lite cmake -G "MSYS Makefiles" -S . -B build-lite \ -DSKIPPYTHON=1 \ -DSKIPREADLINE=1 \ - -DSKIPJANSSYSTEM=1 \ + -DSKIPJANSSONSYSTEM=1 \ -DSKIPWHEREAMISYSTEM=1 \ -DSKIPBT=1 -cmake --build build-lite --target kgh_tools -j -objdump -p build-lite/kgh_tools.exe | grep -i "DLL Name" +cmake --build build-lite --target legic_kgh_tool -j +objdump -p build-lite/legic_kgh_tool.exe | grep -i "DLL Name" ``` ## Building Linux ```bash -cd client/experimental_lib +cd tools/kgh rm -rf build-linux cmake -S . -B build-linux \ -DSKIPPYTHON=1 \ -DSKIPREADLINE=1 \ - -DSKIPJANSSYSTEM=1 \ + -DSKIPJANSSONSYSTEM=1 \ -DSKIPWHEREAMISYSTEM=1 \ -DSKIPBT=1 -cmake --build build-linux --target kgh_tools -j -ldd build-linux/kgh_tools +cmake --build build-linux --target legic_kgh_tool -j +ldd build-linux/legic_kgh_tool ``` ## Static Link Notes -- `kgh_tools` is linked against `pm3rrg_rdv4_static`. +- `legic_kgh_tool` is linked against `pm3rrg_rdv4_static`. - On MinGW, CMake adds `-static -static-libgcc` for the target. - Linux builds are not the same as the ProxSpace single-EXE deployment. - If CMakeCache paths change between ProxSpace and a native shell, delete the build dir and reconfigure. - -## Existing Helper Scripts - -- `01make_lib.sh` creates `build/`, runs `cmake ..`, then `make -j`. -- `01make_lib_continue.sh` reruns `make -j` in `build/`. -- `00make_swig.sh` regenerates the SWIG wrappers before building the library. diff --git a/client/experimental_lib/example_c/legic_migrate_tool.c b/tools/kgh/legic_kgh_tool.c similarity index 92% rename from client/experimental_lib/example_c/legic_migrate_tool.c rename to tools/kgh/legic_kgh_tool.c index bedbcef3a..429eea54a 100644 --- a/client/experimental_lib/example_c/legic_migrate_tool.c +++ b/tools/kgh/legic_kgh_tool.c @@ -1,3 +1,19 @@ +//----------------------------------------------------------------------------- +// 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 #include #include @@ -238,6 +254,14 @@ static void free_legic_read_result(legic_read_result_t *res) { memset(&res->card, 0, sizeof(res->card)); } +static bool is_kgh_card(const uint8_t *dump, uint16_t len) { + return len >= sizeof(k_legic_template_head) && + dump[5] == 0x60 && dump[6] == 0xEA && + dump[7] == 0x9F && dump[8] == 0xFF && + dump[22] == 0x0D && dump[23] == 0xC0 && + dump[24] == 0x08 && dump[25] == 0x00; +} + static const char *classify_legic_card(const uint8_t *dump, uint16_t len) { if (len < 9) { return "unknown"; @@ -252,7 +276,7 @@ static const char *classify_legic_card(const uint8_t *dump, uint16_t len) { return "IAM"; } - if ((dump[7] & 0x0F) == 0x0F && dump[8] == 0xFF) { + if (is_kgh_card(dump, len)) { return "KGH"; } @@ -519,7 +543,11 @@ static int read_badge_from_card(uint32_t *badge_out) { dump[i] ^= crc; } - bool ok = decode_badge_bcd(&dump[31], badge_out); + bool is_kgh = is_kgh_card(dump, datalen); + bool ok = is_kgh && decode_badge_bcd(&dump[31], badge_out); + if (!is_kgh) { + fprintf(stderr, "not a supported KGH card\n"); + } free(dump); g_printAndLog = prev_print; return ok ? 0 : 1; @@ -563,7 +591,7 @@ int main(int argc, char *argv[]) { legic_read_result_t res; if (read_legic_dump(&res, verbose)) { uint8_t stamp[4] = {0}; - if (extract_stamp(res.dump, res.len, stamp)) { + if (is_kgh_card(res.dump, res.len) && extract_stamp(res.dump, res.len, stamp)) { if (has_output) { if (save_stamp_file(output_file, stamp)) { rc = EXIT_SUCCESS; @@ -574,6 +602,8 @@ int main(int argc, char *argv[]) { printf("%02X%02X%02X%02X\n", stamp[0], stamp[1], stamp[2], stamp[3]); rc = EXIT_SUCCESS; } + } else { + fprintf(stderr, "not a supported KGH card\n"); } free_legic_read_result(&res); } else if (verbose) { @@ -614,7 +644,7 @@ int main(int argc, char *argv[]) { uint8_t dump[35] = {0}; build_migration_dump(badge, stamp, dump, sizeof(dump)); - if (legic_migrate_dump(dump, sizeof(dump), true, (const uint8_t[2]){0x60, 0xEA}, true) == PM3_SUCCESS) { + if (legic_migrate_dump(dump, sizeof(dump), true, (const uint8_t[2]) {0x60, 0xEA}, true) == PM3_SUCCESS) { rc = EXIT_SUCCESS; } else { fprintf(stderr, "failed to write card\n");