From f44a4cfcbd843160b9b4bf2a8626b29964f934ed Mon Sep 17 00:00:00 2001 From: DaddyParodz <45983094+daddyparodz@users.noreply.github.com> Date: Mon, 5 Jan 2026 05:14:39 +0100 Subject: [PATCH] Add simple card/tag cloner script --- pm3-clone.sh | 471 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) create mode 100755 pm3-clone.sh diff --git a/pm3-clone.sh b/pm3-clone.sh new file mode 100755 index 000000000..5721e39f5 --- /dev/null +++ b/pm3-clone.sh @@ -0,0 +1,471 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +PM3_BIN="pm3" +WORKDIR="/tmp/pm3" +LOGDIR="$WORKDIR/logs" +PM3_TIMEOUT=30 +ALLOW_BLOCK0_WRITE="0" # Set to 1 to allow protected block writes (e.g., block 0/UID). +ALLOW_UID_WRITE="0" # Set to 1 to allow UID special writes for Ultralight/NTAG magic tags. + +# Optional key overrides (leave empty to use autopwn). +# SRC_KEYFILE="/path/to/source-keys.bin" # If set, skip source autopwn and dump with these keys. +# DST_KEYFILE="/path/to/dest-keys.bin" # If set, skip destination autopwn and restore/verify with these keys. +# MFU_KEY="AABBCCDD" # Optional key/pwd for Ultralight/NTAG (4 bytes) or UL-C (16 bytes). +SRC_KEYFILE="" +DST_KEYFILE="" +MFU_KEY="" + +mkdir -p "$WORKDIR" "$LOGDIR" +cd "$WORKDIR" + +check_pm3() { + if ! command -v "$PM3_BIN" >/dev/null 2>&1; then + echo "[!] Proxmark3 client not found in PATH: $PM3_BIN" + exit 1 + fi + + if ! compgen -G "/dev/ttyACM*" >/dev/null; then + echo "[!] Proxmark3 device node not found (expected /dev/ttyACM*)" + exit 1 + fi + + local output="" + local status=0 + + if command -v timeout >/dev/null 2>&1; then + output="$(timeout "${PM3_TIMEOUT}s" $PM3_BIN -c "hw version; quit" 2>&1)" || status=$? + if [ "$status" -eq 124 ]; then + if printf "%s" "$output" | grep -qiE "waiting for proxmark3 to appear"; then + echo "[!] Proxmark3 not detected or not connected" + else + echo "[!] Proxmark3 not responding (timed out after ${PM3_TIMEOUT}s)" + fi + exit 1 + fi + else + output="$($PM3_BIN -c "hw version; quit" 2>&1)" || status=$? + fi + + if [ "$status" -ne 0 ]; then + if printf "%s" "$output" | grep -qiE "waiting for proxmark3 to appear|no device|not found|not connected|could not open|open failed|usb|serial"; then + echo "[!] Proxmark3 not detected or not connected" + else + echo "[!] Proxmark3 failed to respond" + fi + if [ -n "$output" ]; then + echo "[!] Details: $output" + fi + exit 1 + fi +} + + +get_uid() { + $PM3_BIN -c "hf 14a info; quit" 2>/dev/null \ + | awk '/UID:/{sub(/.*UID:/,""); gsub(/[^0-9A-Fa-f]/,"",$0); if (length($0)>0){print toupper($0); exit}}' +} + +detect_tag_family() { + local output="" + if command -v timeout >/dev/null 2>&1; then + output="$(timeout "${PM3_TIMEOUT}s" $PM3_BIN -c "hf 14a info; quit" 2>&1)" || true + else + output="$($PM3_BIN -c "hf 14a info; quit" 2>&1)" || true + fi + if printf "%s" "$output" | grep -qiE "Ultralight|NTAG"; then + echo "mfu" + return 0 + fi + if printf "%s" "$output" | grep -qiE "MIFARE Classic|Classic 1k|Classic 4k|S50|S70"; then + echo "mf" + return 0 + fi + echo "unknown" + return 0 +} + +ensure_mfu_dump_ok() { + local dump_file="$1" + local log_file="$2" + if [ ! -s "$dump_file" ]; then + echo "[!] Ultralight/NTAG dump not created: $dump_file" + exit 1 + fi + if grep -q "Failed dumping card" "$log_file"; then + echo "[!] Ultralight/NTAG dump failed (see $log_file)" + exit 1 + fi +} + +ensure_mfu_read_ok() { + local log_file="$1" + if grep -qiE "Failed dumping card" "$log_file"; then + echo "[!] Read failed for Ultralight/NTAG. Provide MFU_KEY if needed." + exit 1 + fi + if grep -qiE "read failed|authentication failed|can't authenticate" "$log_file"; then + local bad_pages="" + bad_pages="$(awk ' + /read failed|authentication failed|can.t authenticate/ { + if (match($0, /page[^0-9]*([0-9]+)/, m)) {print m[1]} + } + ' "$log_file" | sort -n | uniq | tr '\n' ' ')" + if [ -z "$bad_pages" ]; then + echo "[!] Read failed for Ultralight/NTAG. Provide MFU_KEY if needed." + exit 1 + fi + if echo "$bad_pages" | awk '{for(i=1;i<=NF;i++){if($i+0>3){exit 1}}}'; then + echo "[*] Read failures only in UID pages (0-3); treating as OK" + else + echo "[!] Read failed for pages: ${bad_pages}Provide MFU_KEY if needed." + exit 1 + fi + fi +} + +ensure_autopwn_keys_ok() { + local log_file="$1" + local bad=0 + bad="$(awk -F'|' ' + $1 ~ /[0-9]/ { + sec=$1 + resA=$4 + resB=$6 + gsub(/[[:space:]]+/,"",sec) + gsub(/[[:space:]]+/,"",resA) + gsub(/[[:space:]]+/,"",resB) + if (sec != "" && sec+0 > 0 && (resA=="0" || resB=="0")) {print 1; exit} + } + ' "$log_file")" + if [ "$bad" = "1" ]; then + echo "[!] Autopwn did not recover keys for some sectors (>0). Provide a keyfile." + exit 1 + fi +} +run_pm3_cmd() { + local desc="$1" + local cmd="$2" + local log_file="$3" + local status=0 + + if [ -n "$log_file" ]; then + $PM3_BIN -c "$cmd; quit" | tee "$log_file" + status=${PIPESTATUS[0]} + else + $PM3_BIN -c "$cmd; quit" + status=$? + fi + + if [ "$status" -ne 0 ]; then + echo "[!] pm3 command failed during: $desc" + exit 1 + fi +} + +ensure_read_ok_classic() { + local log_file="$1" + local bad="" + bad="$(awk -F'|' ' + /\( fail \)/ { + blk=$1 + gsub(/[^0-9]/,"",blk) + if (blk != "" && blk+0 > 0) {print blk} + } + ' "$log_file" | sort -n | uniq | tr '\n' ' ')" + if [ -n "$bad" ]; then + echo "[!] Read failed for blocks: ${bad}Provide a keyfile." + exit 1 + fi +} + + +compare_dump_sizes() { + local src="$1" + local dst="$2" + local src_size="" + local dst_size="" + if [ -f "$src" ] && [ -f "$dst" ]; then + src_size="$(stat -c '%s' "$src" 2>/dev/null || true)" + dst_size="$(stat -c '%s' "$dst" 2>/dev/null || true)" + if [ -n "$src_size" ] && [ -n "$dst_size" ] && [ "$dst_size" -gt "$src_size" ]; then + echo "[*] Note: destination tag dump ($dst_size bytes) is larger than source ($src_size bytes)" + fi + fi +} + +ensure_restore_ok_classic() { + local log_file="$1" + local allow_block0="$2" + local bad="" + bad="$(awk -F'|' ' + /\( fail \)/ { + blk=$1 + gsub(/[^0-9]/,"",blk) + if (blk != "" && (blk+0 > 0 || allow_block0==1)) {print blk} + } + ' allow_block0="$allow_block0" "$log_file" | sort -n | uniq | tr '\n' ' ')" + if [ -n "$bad" ]; then + echo "[!] Restore failed for locked blocks: ${bad}Provide a keyfile." + exit 1 + fi +} + +wait_for_card_tag() { + local label="$1" + local uid="" + echo "[*] $label" >&2 + while true; do + uid="$(get_uid)" + if [ -n "$uid" ]; then + echo "[*] UID detected: $uid" >&2 + echo "$uid" + return 0 + fi + sleep 1 + done +} + +wait_for_new_card() { + local label="$1" + local exclude_uid_a="$2" + local exclude_uid_b="$3" + local uid="" + if [ -n "$label" ]; then + echo "[*] $label" >&2 + fi + while true; do + uid="$(get_uid)" + if [ -z "$uid" ]; then + sleep 1 + continue + fi + if [ -n "$exclude_uid_a" ] && [ "$uid" = "$exclude_uid_a" ]; then + echo "[*] Still detecting the source card/tag ($uid). Waiting for change..." >&2 + sleep 1 + continue + fi + if [ -n "$exclude_uid_b" ] && [ "$uid" = "$exclude_uid_b" ]; then + echo "[*] Still detecting the previous destination card/tag ($uid). Waiting for change..." >&2 + sleep 1 + continue + fi + echo "[*] Destination card/tag UID detected: $uid" >&2 + echo "$uid" + return 0 + done +} + +# 1. Wait for source card/tag +check_pm3 +echo "[*] Starting Proxmark3 in batch mode" +SRC_UID="$(wait_for_card_tag "Place the source card/tag")" + +if [ -z "$SRC_UID" ]; then + echo "[!] Source UID not found" + exit 1 +fi + +KEYFILE="" +DUMPFILE="" +TAG_FAMILY="$(detect_tag_family)" +if [ "$TAG_FAMILY" = "unknown" ]; then + echo "[!] Unable to detect tag family from hf 14a info. Replace card/tag." + exit 1 +fi +echo "[*] Detected tag family: $TAG_FAMILY" + +if [ "$TAG_FAMILY" = "mfu" ]; then + if [ -n "$SRC_KEYFILE" ] || [ -n "$DST_KEYFILE" ]; then + echo "[!] SRC_KEYFILE/DST_KEYFILE are for Classic only. Use MFU_KEY for Ultralight/NTAG." + exit 1 + fi + echo "[*] Step 1: dump source card/tag (Ultralight/NTAG)" + DUMPFILE="$WORKDIR/src-${SRC_UID}-dump.bin" + MFU_KEY_ARG="" + if [ -n "$MFU_KEY" ]; then + MFU_KEY_ARG="-k $MFU_KEY" + fi + run_pm3_cmd "dump source card/tag (Ultralight/NTAG)" "hf mfu dump -f $DUMPFILE $MFU_KEY_ARG" "$LOGDIR/dump.log" + ensure_mfu_dump_ok "$DUMPFILE" "$LOGDIR/dump.log" + ensure_mfu_read_ok "$LOGDIR/dump.log" +else + if [ -n "$SRC_KEYFILE" ]; then + echo "[*] Step 1: using provided source keyfile (skipping autopwn)" + KEYFILE="$SRC_KEYFILE" + else + echo "[*] Step 1: autopwn source card/tag" + run_pm3_cmd "autopwn source card/tag" "hf mf autopwn" "$LOGDIR/autopwn.log" + ensure_autopwn_keys_ok "$LOGDIR/autopwn.log" + fi + + if [ -n "$SRC_KEYFILE" ]; then + KEYFILE="$SRC_KEYFILE" + DUMPFILE="$WORKDIR/src-${SRC_UID}-dump.bin" + run_pm3_cmd "dump source card/tag" "hf mf dump -f $DUMPFILE -k $KEYFILE" "$LOGDIR/dump.log" + run_pm3_cmd "verify readable sectors" "hf mf dump -f /tmp/pm3/src-${SRC_UID}-readcheck.bin -k $KEYFILE" "$LOGDIR/readcheck.log" + ensure_read_ok_classic "$LOGDIR/readcheck.log" + else + KEYFILE="$(awk -F'`' '/Found keys have been dumped to/{print $2; exit}' "$LOGDIR/autopwn.log")" + DUMPFILE="$(awk -F'`' '/Saved 1024 bytes to binary file/{print $2; exit}' "$LOGDIR/autopwn.log")" + fi + + if [ -z "$KEYFILE" ] || [ ! -f "$KEYFILE" ]; then + echo "[!] Keyfile not found: $KEYFILE" + exit 1 + fi + run_pm3_cmd "verify readable sectors" "hf mf dump -f /tmp/pm3/src-${SRC_UID}-readcheck.bin -k $KEYFILE" "$LOGDIR/readcheck.log" + ensure_read_ok_classic "$LOGDIR/readcheck.log" +fi + +# 2. Full dump +if [ -z "$DUMPFILE" ] || [ ! -f "$DUMPFILE" ]; then + echo "[!] Dump not found: $DUMPFILE" + exit 1 +fi + +if [ -n "$KEYFILE" ] && [ "$(dirname "$KEYFILE")" != "$WORKDIR" ]; then + mv -f "$KEYFILE" "$WORKDIR/" + KEYFILE="$WORKDIR/$(basename "$KEYFILE")" + echo "[*] Moved source keyfile to $KEYFILE" +fi +if [ "$(dirname "$DUMPFILE")" != "$WORKDIR" ]; then + mv -f "$DUMPFILE" "$WORKDIR/" + DUMPFILE="$WORKDIR/$(basename "$DUMPFILE")" + echo "[*] Moved source dump to $DUMPFILE" +fi + +echo "[*] Dump completed: $DUMPFILE" + +# 3. Clone to destination card(s) +echo +echo "=== REMOVE THE SOURCE CARD/TAG ===" +echo "=== PLACE THE DESTINATION CARD/TAG ===" +PREV_TGT_UID="$SRC_UID" + +while true; do + TGT_UID="$(wait_for_new_card "" "$SRC_UID" "$PREV_TGT_UID")" + TGT_FAMILY="$(detect_tag_family)" + if [ "$TGT_FAMILY" = "unknown" ]; then + echo "[!] Unable to detect destination tag family. Replace destination card/tag." + PREV_TGT_UID="$TGT_UID" + continue + fi + if [ "$TGT_FAMILY" != "$TAG_FAMILY" ]; then + echo "[!] Destination tag family ($TGT_FAMILY) does not match source ($TAG_FAMILY)." + echo "[!] Replace destination card/tag." + PREV_TGT_UID="$TGT_UID" + continue + fi + + if [ "$TAG_FAMILY" = "mfu" ]; then + # 4. Restore (Ultralight/NTAG) + echo "[*] Step 3: restore to destination card/tag (Ultralight/NTAG)" + MFU_KEY_ARG="" + if [ -n "$MFU_KEY" ]; then + MFU_KEY_ARG="-k $MFU_KEY" + fi + MFU_UID_ARG="" + if [ "${ALLOW_UID_WRITE:-0}" = "1" ]; then + echo "[*] ALLOW_UID_WRITE=1 set; enabling special UID write (-s)" + MFU_UID_ARG="-s" + else + echo "[*] UID pages will be skipped (set ALLOW_UID_WRITE=1 to override)" + fi + run_pm3_cmd "restore destination card/tag (Ultralight/NTAG)" "hf mfu restore -f $DUMPFILE $MFU_KEY_ARG $MFU_UID_ARG" "$LOGDIR/restore.log" + + # 5. Final verification (dump + compare) + echo "[*] Step 4: final verification (dump + compare)" + DEST_DUMP="$WORKDIR/dst-${TGT_UID}-dump.bin" + run_pm3_cmd "verify destination card/tag (Ultralight/NTAG)" "hf mfu dump -f $DEST_DUMP $MFU_KEY_ARG" "$LOGDIR/verify.log" + else + # 4. Autopwn on destination card/tag + if [ -n "$DST_KEYFILE" ]; then + echo "[*] Step 3: using provided destination keyfile (skipping autopwn)" + KEYFILE_DST="$DST_KEYFILE" + else + echo "[*] Step 3: autopwn destination card/tag" + run_pm3_cmd "autopwn destination card/tag" "hf mf autopwn" "$LOGDIR/autopwn-dst.log" + ensure_autopwn_keys_ok "$LOGDIR/autopwn-dst.log" + KEYFILE_DST="$(awk -F'`' '/Found keys have been dumped to/{print $2; exit}' "$LOGDIR/autopwn-dst.log")" + fi + + if [ ! -f "$KEYFILE_DST" ]; then + echo "[!] Destination keyfile not found: $KEYFILE_DST" + exit 1 + fi + + if [ "$(dirname "$KEYFILE_DST")" != "$WORKDIR" ]; then + mv -f "$KEYFILE_DST" "$WORKDIR/" + KEYFILE_DST="$WORKDIR/$(basename "$KEYFILE_DST")" + echo "[*] Moved destination keyfile to $KEYFILE_DST" + fi + + # 5. Restore (skip protected blocks unless explicitly allowed) + echo "[*] Step 4: restore to destination card/tag" + RESTORE_FORCE="" + if [ "${ALLOW_BLOCK0_WRITE:-0}" = "1" ]; then + echo "[*] ALLOW_BLOCK0_WRITE=1 set; allowing protected block writes" + RESTORE_FORCE="--force" + else + echo "[*] Protected blocks will be skipped (set ALLOW_BLOCK0_WRITE=1 to override)" + fi + run_pm3_cmd "restore destination card/tag" "hf mf restore -f $DUMPFILE -k $KEYFILE_DST $RESTORE_FORCE" "$LOGDIR/restore.log" + if [ -z "$DST_KEYFILE" ]; then + ensure_restore_ok_classic "$LOGDIR/restore.log" "${ALLOW_BLOCK0_WRITE:-0}" + fi + + # 6. Final verification (dump + compare) + echo "[*] Step 5: final verification (dump + compare)" + DEST_DUMP="$WORKDIR/dst-${TGT_UID}-dump.bin" + run_pm3_cmd "verify destination card/tag" "hf mf dump -f $DEST_DUMP -k $KEYFILE_DST" "$LOGDIR/verify.log" + fi + + if [ ! -f "$DEST_DUMP" ]; then + echo "[!] Destination dump not created: $DEST_DUMP" + exit 1 + fi + compare_dump_sizes "$DUMPFILE" "$DEST_DUMP" + + if ! cmp -s "$DUMPFILE" "$DEST_DUMP"; then + echo "[!] Verification mismatch: destination dump differs from source" + cmp -l "$DUMPFILE" "$DEST_DUMP" > "$LOGDIR/verify-diff.raw" || true + echo "[*] Showing all differing bytes (offset -> block: src != dst)" + awk '{off=$1-1; blk=int(off/16); printf " offset %d (block %d): %03o != %03o\n", off, blk, $2, $3}' \ + "$LOGDIR/verify-diff.raw" | tee "$LOGDIR/verify-diff.log" + + if [ "$TAG_FAMILY" = "mfu" ]; then + if [ "${ALLOW_UID_WRITE:-0}" != "1" ]; then + if awk '{off=$1-1; if (off>=12) {exit 1}}' "$LOGDIR/verify-diff.raw"; then + echo "[*] Differences are only in UID pages and ALLOW_UID_WRITE is not set; treating as OK" + else + exit 1 + fi + else + exit 1 + fi + else + if [ "${ALLOW_BLOCK0_WRITE:-0}" != "1" ]; then + if awk '{off=$1-1; blk=int(off/16); if (blk!=0) {exit 1}}' "$LOGDIR/verify-diff.raw"; then + echo "[*] Differences are only in block 0 and ALLOW_BLOCK0_WRITE is not set; treating as OK" + else + exit 1 + fi + else + exit 1 + fi + fi + fi + + echo + echo "[+] Clone completed for UID $TGT_UID (source: $SRC_UID)" + PREV_TGT_UID="$TGT_UID" + read -r -p "Clone same source to another destination card/tag? (y/N): " REPLY + if [ "${REPLY:-n}" != "y" ] && [ "${REPLY:-n}" != "Y" ]; then + break + fi + echo + echo "=== PLACE THE NEXT DESTINATION CARD/TAG (same source dump) ===" +done