proxmark3/tools/pm3-clone.sh
2026-01-10 15:18:05 +01:00

471 lines
14 KiB
Bash
Executable file

#!/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