Last active
September 7, 2024 13:32
-
-
Save gretel/dd80c854e22c2afd20f5aebc62015096 to your computer and use it in GitHub Desktop.
query, read, encode (using amiitool), write and lock NTAG215 (using uFR Nano hardware) for the purpose of researching Nintendo's Amiibo infrastructure Raw
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# This is a companion script to https://github.com/konstantin-kelemen/arduino-amiibo-tools | |
# The original post this was crafted for was https://games.kel.mn/en/create-amiibo-clones-with-arduino/ | |
# For more info go to https://games.kel.mn/en/companion-script-to-simplify-amiibo-cloning-with-arduino/ | |
#requirements: | |
#sha1sum (part of coreutils) | |
#xxd (part of vim) | |
#hexdump | |
#amiitool (https://github.com/socram8888/amiitool) | |
hash xxd 2>/dev/null || { echo >&2 "require xxd but it's not installed. Aborting."; exit 1; } | |
hash sha1sum 2>/dev/null || { echo >&2 "require sha1sum but it's not installed. Aborting."; exit 1; } | |
hash hexdump 2>/dev/null || { echo >&2 "require hexdump but it's not installed. Aborting."; exit 1; } | |
hash ./amiitool 2>/dev/null || { echo >&2 "require amiitool but it's not installed or in the currect directory. Aborting."; exit 1; } | |
if [ $# -ne 3 ] | |
then | |
echo "usage: $0 key_file encrypted_dump_file blank_tagid" | |
exit | |
fi | |
if [ "$(sha1sum "$1" |cut -d' ' -f1)" != "bbdbb49a917d14f7a997d327ba40d40c39e606ce" ] | |
then | |
echo "key_file not sane" | |
exit | |
fi | |
base=${2%%.*} | |
#get the empty tag uid: | |
taguid=$3 | |
taguid0="$(echo "$taguid" | cut -b1,2)" # Byte 0 (should bx 0x04) | |
taguid1="$(echo "$taguid" | cut -b3,4)" # Byte 1 (we count from 0) | |
taguid2="$(echo "$taguid" | cut -b5,6)" # Byte 2 | |
if [ ${#3} -eq 18 ]; then # Check if user provided a long taguid | |
taguid3="$(echo "$taguid" | cut -b9,10)" # Byte 4 | |
taguid4="$(echo "$taguid" | cut -b11,12)" # Byte 5 | |
taguid5="$(echo "$taguid" | cut -b13,14)" # Byte 6 | |
taguid6="$(echo "$taguid" | cut -b15,16)" # Byte 7 | |
uid="$(echo "$taguid" | cut -b1-16)" | |
BCC1="$(echo "$taguid" | cut -b17,18)" # Pull out the BCC1 for use later | |
elif [ ${#3} -eq 14 ]; then # Check if user provided a short taguid | |
taguid3="$(echo "$taguid" | cut -b7,8)" # Byte 3 | |
taguid4="$(echo "$taguid" | cut -b9,10)" # Byte 4 | |
taguid5="$(echo "$taguid" | cut -b11,12)" # Byte 5 | |
taguid6="$(echo "$taguid" | cut -b13,14)" # Byte 6 | |
# Convert 7byte to 9byte for script | |
BCC0="$(printf '%02X\n' $(( 0x88 ^ 0x$taguid0 ^ 0x$taguid1 ^ 0x$taguid2 )))" # Calculate the BCC0 | |
BCC1="$(printf '%02X\n' $(( 0x$taguid3 ^ 0x$taguid4 ^ 0x$taguid5 ^ 0x$taguid6 )))" # Calculate the BCC1 | |
uid="$taguid0$taguid1$taguid2$BCC0$taguid3$taguid4$taguid5$taguid6" | |
fi | |
if [ ${#uid} -ne 16 ]; then | |
echo "please pick a valid 7 or 9 byte uid" | |
exit | |
fi | |
# Generate the password from the tag | |
pw1="$(printf '%02X\n' $(( 0xAA ^ 0x$taguid1 ^ 0x$taguid3 )))" | |
pw2="$(printf '%02X\n' $(( 0x55 ^ 0x$taguid2 ^ 0x$taguid4 )))" | |
pw3="$(printf '%02X\n' $(( 0xAA ^ 0x$taguid3 ^ 0x$taguid5 )))" | |
pw4="$(printf '%02X\n' $(( 0x55 ^ 0x$taguid4 ^ 0x$taguid6 )))" | |
#decrypt the dump | |
#echo Using Amiibo Tool to decrypt ${2%%.*} | |
./amiitool -d -k "$1" -i "$2" -o ${base}_dec.bin || exit 2 | |
#modify the uid record | |
echo "01D4: $uid" | xxd -r - ${base}_dec.bin | |
#add password | |
echo "0214: $pw1$pw2$pw3$pw4" | xxd -r - ${base}_dec.bin #pw | |
echo "0218: 8080" | xxd -r - ${base}_dec.bin | |
#set the default values | |
echo "0208: 000000" | xxd -r - ${base}_dec.bin | |
echo "0000: $BCC1" | xxd -r - ${base}_dec.bin | |
echo "0002: 0000" | xxd -r - ${base}_dec.bin | |
enc_file="${base}_${uid}.bin" | |
#reencrypt the uid modified dump | |
#echo Using Amiibo Tool to encrypt ${2%%.*} | |
./amiitool -e -k "$1" -i ${base}_dec.bin -o ${enc_file} || exit 3 | |
rm ${base}_dec.bin | |
echo "${enc_file}" | |
# echo "**** START OF HEXDUMP ****" | |
# hexdump -v -e " 4/1 \"0x%02X, \" \"\n\"" "enc.bin" > hexdump | |
# truncate -s -2 hexdump | |
# echo "" >> hexdump | |
# echo "" >> hexdump | |
# cat hexdump |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
if [ $# -ne 2 ] | |
then | |
echo "usage: $0 amiibo.bin uuid" | |
exit 1 | |
fi | |
KEY='retail_key.bin' | |
FILE=$1 | |
UUID=$2 | |
./amiibo.sh ${KEY} ${FILE} ${UUID} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# https://gist.github.com/gretel/dd80c854e22c2afd20f5aebc62015096 | |
# https://www.d-logic.net/nfc-rfid-reader-sdk/products/nano-nfc-rfid-reader/ | |
# https://code.d-logic.net/nfc-rfid-reader-sdk/ufr-lib | |
# | |
from ctypes import * | |
import argparse | |
import os, threading, time, sys | |
import pyperclip | |
import subprocess | |
import traceback | |
import ufr_constants, ufr_errors | |
# oh i love argparse | |
parser = argparse.ArgumentParser(description='query, read, encode (using amiitool), write and lock NTAG215 (using uFR Nano hardware) for the purpose of researching Nintendo\'s Amiibo infrastructure') | |
parser.add_argument('-f', '--filename', dest='filename', help='name of the file containing binary data') | |
parser.add_argument('-r', '--read', dest='read', action='store_true', help='read data from tag') | |
parser.add_argument('-e', '--encode', dest='encode', action='store_true', help='encode data (calls "encode.sh" when uid is known)') | |
parser.add_argument('-w', '--write', dest='write', action='store_true', help='write data to tag') | |
parser.add_argument('-x', '--lock', dest='lock', action='store_true', help='set dynamic and static lock bytes (!)') | |
parser.add_argument('-l', '--loop', dest='loop', action='store_true', help='do not exit on completion but loop (useful for batch jobs)') | |
args = parser.parse_args() | |
class NanoAmii(threading.Thread): | |
def load_blob(self, filename : str) -> bytearray: | |
# first argument (required) | |
# TODO: fail gracefully if argument missing/file nonexistant | |
self.filename = filename | |
# read file as binary | |
print('LOAD', self.filename) | |
with open(self.filename, 'rb') as binary: | |
self.data = bytearray(binary.read()) | |
# store length | |
self.data_len = len(self.data) | |
# TODO: check for min/max length | |
print(' read', self.data_len, self.data) | |
def save_blob(self, filename : str, data : bytes, mode : str = 'wb'): | |
print('SAVE', repr(filename)) | |
with open(filename, mode) as binary: | |
binary.write(data) | |
binary.close() | |
def main_thread(self): | |
# loop | |
while self.run: | |
self.loop() | |
def __init__(self): | |
#print('__init__') | |
threading.Thread().__init__() | |
print(sys.argv[0], args) | |
self.block_len = 0 | |
self.blocks_num = 0 | |
self.card_size_linear = c_uint8() | |
self.card_size_raw = c_uint8() | |
self.card_type = None | |
self.card_uid = (c_ubyte * 10)() | |
self.connected = False | |
self.run = True | |
# TODO: abstraction | |
self.ufr = cdll.LoadLibrary(os.getcwd() + '/ufr-lib/osx/x86_64/libuFCoder.dylib') | |
self.ufr.ReaderSoftRestart() | |
self.data = bytearray() | |
self.data_len = 0 | |
if(args.write and args.filename == None): | |
self.abort('filename required') | |
# start thread | |
threading.Thread(target = self.main_thread).start() | |
def open(self) -> int: | |
reader_fw_bld = c_uint32() | |
reader_fw_maj = c_uint32() | |
reader_fw_min = c_uint32() | |
reader_type = c_uint32() | |
print('OPEN') | |
# open device | |
call_result = self.ufr.ReaderOpen() | |
if call_result == ufr_constants.DL_OK: | |
self.ufr.GetReaderType(byref(reader_type)) | |
print(' type', reader_type.value) | |
self.ufr.GetReaderFirmwareVersion(byref(reader_fw_min), byref(reader_fw_maj)) | |
self.ufr.GetBuildNumber(byref(reader_fw_bld)) | |
print(' firmware %s.%s.%s' % (reader_fw_min.value, reader_fw_maj.value, reader_fw_bld.value)) | |
#self.ufr.ReaderUISignal(ufr_constants.FUNCT_LIGHT_OK, ufr_constants.FUNCT_SOUND_OK) | |
self.ufr.AutoSleepSet(10) | |
self.connected = True | |
else: | |
print(' error', ufr_errors.UFCODER_ERROR_CODES[call_result]) | |
self.connected = False | |
return call_result | |
def encode(self, filename : str, uid) -> int: | |
print('ENCODE', filename, uid) | |
try: | |
output = subprocess.check_output(['bash', 'encode.sh', filename, uid]) | |
args.filename = output.decode('utf-8').strip() | |
except subprocess.CalledProcessError as e: | |
self.abort('error on subprocess: %s' % e.output) | |
print(' filename', args.filename) | |
self.load_blob(args.filename) | |
def close(self) -> int: | |
# reset | |
self.card_size_linear = c_uint8() | |
self.card_size_raw = c_uint8() | |
self.card_type = c_uint8() | |
self.connected = False | |
self.card_uid = (c_ubyte * 10)() | |
# close device | |
call_result = self.ufr.ReaderClose() | |
print('CLOSE', hex(call_result)) | |
return call_result | |
def read_tag(self) -> bytearray: | |
block = (c_uint8 * self.block_len)() | |
block_pos = 0 | |
errors = 0 | |
result = bytearray() | |
print('READ', self.blocks_num, self.block_len) | |
for r in range(0, self.blocks_num): | |
block_pos = r | |
call_result = self.ufr.BlockRead(byref(block), block_pos, ufr_constants.MIFARE_AUTHENT1A, 0) | |
if(call_result == ufr_constants.DL_OK): | |
print(' R', block_pos, ufr_errors.UFCODER_ERROR_CODES[call_result], repr(bytes(block))) | |
for b in range(0, self.block_len): | |
result.append(block[b]) | |
else: | |
errors += 1 | |
print(' !R', block_pos, ufr_errors.UFCODER_ERROR_CODES[call_result]) | |
return errors, result | |
def write_tag(self) -> int: | |
block = bytes() | |
block_pos = c_uint8() | |
call_result = c_uint8() | |
data_pos = 0 | |
errors = 0 | |
print('WRITE', self.blocks_num, self.block_len) | |
for block_pos in range(0, self.blocks_num): | |
if(block_pos < 3 or block_pos == 130): | |
# skip lock bytes | |
print(' -W', block_pos) | |
else: | |
block = bytes(self.data[data_pos:data_pos + self.block_len]) | |
call_result = self.ufr.BlockWrite(block, block_pos, ufr_constants.MIFARE_AUTHENT1A, 0) | |
if call_result == ufr_constants.DL_OK: | |
print(' W', block_pos, ufr_errors.UFCODER_ERROR_CODES[call_result], repr(block)) | |
else: | |
errors = 1 | |
print(' !W', block_pos, ufr_errors.UFCODER_ERROR_CODES[call_result], repr(block)) | |
break | |
data_pos += 4 | |
return errors | |
def write_dynlock(self) -> int: | |
b = 0x01, 0x00, 0x0F, 0xBD | |
block = bytes(b) | |
block_pos = 130 | |
call_result = c_uint8() | |
errors = 0 | |
print('DYNLOCK', block_pos, block) | |
call_result = self.ufr.BlockWrite(block, block_pos, ufr_constants.MIFARE_AUTHENT1A, 0) | |
if call_result == ufr_constants.DL_OK: | |
print(' DL', block_pos, ufr_errors.UFCODER_ERROR_CODES[call_result], repr(block)) | |
else: | |
errors = 1 | |
print(' !DL', block_pos, ufr_errors.UFCODER_ERROR_CODES[call_result], repr(block)) | |
return errors | |
def write_statlock(self) -> int: | |
b = 0x0F, 0xE0, 0x0F, 0xE0 | |
block = bytes(b) | |
block_pos = 2 | |
call_result = c_uint8() | |
errors = 0 | |
print('STATLOCK', block_pos, block) | |
call_result = self.ufr.BlockWrite(block, block_pos, ufr_constants.MIFARE_AUTHENT1A, 0) | |
if call_result == ufr_constants.DL_OK: | |
print(' SL', block_pos, ufr_errors.UFCODER_ERROR_CODES[call_result], repr(block)) | |
else: | |
errors = 1 | |
print(' !SL', block_pos, ufr_errors.UFCODER_ERROR_CODES[call_result], repr(block)) | |
return errors | |
def query(self) -> bool: | |
card_size_linear = c_uint8() | |
card_size_raw = c_uint8() | |
card_type = c_uint8() | |
card_uidsize = c_uint8() | |
card_uid = (c_ubyte * 10)() | |
# get tag type | |
call_result = self.ufr.GetDlogicCardType(byref(card_type)) | |
if call_result == ufr_constants.DL_OK: | |
print('QUERY', ufr_errors.UFCODER_ERROR_CODES[call_result]) | |
print(' type', card_type.value, 'name', ufr_constants.CardName(card_type.value)) | |
self.card_type = card_type.value | |
else: | |
return False | |
# size of memory, length of block | |
self.blocks_num = ufr_constants.MaxBlock(self.card_type) | |
self.block_len = ufr_constants.BlockLength(self.card_type) | |
# get tag identifier (uid) | |
call_result = self.ufr.GetCardIdEx(byref(card_type), card_uid, byref(card_uidsize)) | |
if call_result == ufr_constants.DL_OK: | |
# compose uid | |
c = '' | |
for n in range(card_uidsize.value): | |
c = c + format(card_uid[n], '02x') | |
print(' uid', c) | |
self.card_uid = c | |
else: | |
self.abort('error getting uid of tag') | |
# copy to clipboard | |
pyperclip.copy(c) | |
#self.save_blob('/tmp/nanoamii.last_uid', c, 'w') | |
call_result = self.ufr.GetCardSize(byref(card_size_linear), byref(card_size_raw)) | |
if call_result == ufr_constants.DL_OK: | |
self.card_size_raw = card_size_raw.value | |
self.card_size_linear = card_size_linear.value | |
print(' size linear', card_size_linear.value, 'raw', card_size_raw.value) | |
else: | |
self.abort('error getting memory sizes of tag') | |
return True | |
def contact(self): | |
errors = 0 | |
tag = self.query() | |
if(tag != True): | |
return | |
if(args.read): | |
read_data = '' | |
errors, read_data = self.read_tag() | |
if(errors > 0): | |
self.abort('error on read') | |
# TODO error handling | |
if(args.filename != None): | |
filename = args.filename | |
else: | |
filename = self.card_uid + '.bin' | |
self.save_blob(filename, read_data) | |
if(args.encode): | |
if(args.filename): | |
self.encode(args.filename, self.card_uid) | |
else: | |
self.abort('filename required') | |
if(args.write): | |
self.load_blob(args.filename) | |
errors = self.write_tag() | |
if(errors > 0): | |
self.abort('error on write') | |
if(args.lock): | |
errors = self.write_dynlock() | |
if(errors > 0): | |
self.abort('error on writing dynamic lock bytes') | |
else: | |
errors = self.write_statlock() | |
if(errors > 0): | |
self.abort('error on writing static lock bytes') | |
self.ufr.ReaderUISignal(ufr_constants.FUNCT_LIGHT_OK, ufr_constants.FUNCT_SOUND_OK) | |
if(args.loop != True): | |
self.run = False | |
else: | |
time.sleep(1.5) | |
def loop(self): | |
try: | |
if self.connected != True: | |
# open | |
self.open() | |
elif self.connected: | |
# connected | |
result = self.contact() | |
if result == 0xa4: | |
# happens on usb disconnection - allow reconnection | |
self.close() | |
else: | |
# ensure | |
self.close() | |
except: | |
# catch exceptions | |
print('\nEXCEPTION', traceback.format_exc()) | |
sys.exit(1) | |
finally: | |
# dont run wild | |
time.sleep(0.5) | |
def abort(self, reason): | |
self.ufr.ReaderUISignal(ufr_constants.FUNCT_LIGHT_ERROR, ufr_constants.FUNCT_SOUND_ERROR) | |
raise SystemExit(reason) | |
if __name__ == '__main__': | |
#print('__main__') | |
nanoamii = NanoAmii() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment