Joycon,一个使用蓝牙与Switch通信的手柄;Amiibo,一种内嵌了NTAG215的塑料玩具。两者的硬件基础都是大路货,没有什么特别之处,这让使用普通硬件模拟它们具有了可能性。不过,不论是蓝牙通信的模式还是NFC芯片内部的数据,都属于任天堂的私有内容,这是想要模拟它们所面临的主要困难。
坏消息是,博主根本不懂无线安全。不论是蓝牙还是NFC,工具没有,技术不通,属于给我时间都无从下手的领域。好消息是,有大佬替我们分析完了,只需要会用大佬写的东西就好了。本文将站在前人的肩膀上,讲讲如何用手边的便宜硬件,兼职宏手柄和Amiibo钥匙扣的活。
准备阶段
你先停停,目标是什么?
本文的目标是:在未破解的Switch上实现按键序列的可编程输入,并模拟具有任意序列号的Amiibo。
通俗来说,就是可以写脚本操作Switch,并且假装我是买了一仓库Amiibo换着刷的富哥,从而不需要在某些游戏里刷一次改一下主机日期。说的就是你,塞尔达
打算站在谁的肩膀上?
手柄通信模拟使用joycontrol。该项目的nfc功能因为许可证问题被干掉了,这是一个保留了nfc功能的fork。
然而joycontrol仅负责模拟手柄通信行为,自带的命令行控制台并不算好用。这里需要套上一层更方便进行控制与自动化的外皮。在这里选择了较为古老的RasCon_NS,该项目已经很久没有更新,需要一些小小的改动以适配最新的joycontrol。相关内容将在后文讲解。
改写Amiibo序列号需要对数据进行解密,Amiibo二进制数据的加解密使用amiitool。
需要准备点什么?
硬件方面,博主使用了树莓派3B。但理论上,一个具有蓝牙适配器的Linux电脑都应当可以。至于是不是真的可以,只能自己试试才知道。这个issue给出了部分硬件的表现,但是很久没有更新了。
数据方面,还需要准备一个名叫key_retail.bin的神奇妙妙文件。这个文件是通过主机固件逆向工程得到的,amiitool需要其作为加解密密钥,但如果直接随代码提供的话仓库会被光速拿下。同时,还需要准备好想要模拟的Amiibo dump文件。模拟手柄仅省去了NFC通信过程,我们仍需要装载完整的数据以供主机“读取”。上述的两类数据可以在这里,或者通过搜索引擎自行取得。
博主还用到了诸如010 Editor等软件进行数据分析,但如果对过程不感兴趣那么可以忽略。
模拟手柄
系统
首先是给树莓派装上Raspberry Pi OS(虽然我更习惯叫它Raspbian)。如果曾经的你已经装好了,那么不必重装,更老的系统版本并不会影响什么。此部分流程本文不再给出。
不过顺便一提,如果你用了bullseye或者更新版的系统,系统不再自带创建用户名为“pi”的账户了。如果你也像我一样完全不打算用官方的安装器而是直接把镜像写进tf卡的话,你可能需要按这篇文章的指引创建一个userconf文件用于配置账户。
按你的使用习惯配置好shell、用户、文件编辑器等部分。镜像源之类的也得准备一下。因为作者只在这个树莓派上模拟手柄,并且不担心安全问题,因此以下所有命令都默认使用root用户执行。
环境
安装依赖,禁用部分蓝牙功能。
apt install python3-dbus libhidapi-hidraw0 libbluetooth-dev bluez -y
pip3 install aioconsole hid crc8 -y
sed -i.bak 's/^\(ExecStart=\S*bluetoothd\).*$/\1 -C -P sap,input,avrcp/g' /lib/systemd/system/bluetooth.service
systemctl daemon-reload
systemctl restart bluetooth.service
更改蓝牙适配器mac至任天堂区间内以改善连接性。注意,以下内容仅适用于树莓派特定型号,如果你使用其他蓝牙适配器,应当使用baddr或者其他命令序列。详情参考这个issue。
hcitool cmd 0x3f 0x001 0x66 0x55 0x44 0xCB 0x58 0x94
hciconfig hci0 reset
可以使用hciconfig -a确认更改是否成功。
软件
cd ~
git clone https://github.com/Poohl/joycontrol.git
cd joycontrol
python3 run_controller_cli.py PRO_CONTROLLER
打开switch的手柄->更改握法/顺序界面,等待joycontrol库进行配对。如果配对成功,那么可以看到switch上显示了一个手柄,在程序中输入换行便可见到命令提示符。至此,已经可以通过程序模拟手柄了。
但joycontrol自带的命令行控制台并不算好用,也不适合编写自动化脚本。接下来配置RasCon_NS作为控制和脚本代理。
cd ~
apt install apache2 -y
pip3 install dbus-python flask -y
git clone https://github.com/SkyoKen/RasCon_NS.git
cp -r joycontrol/joycontrol RasCon_NS/
同时需要对RasCon_NS做一些更改以适配最新的joycontrol,将以下patch应用于RasCon_NS即可。
分别运行python3 web.py和python3 run.py,启动RasCon_NS。打开web.py给出的url,即可看到图形化的控制界面与脚本运行区。
自动化操作
接下来以王国之泪1.0~1.1.1存在的拉弓++复制bug进行操练。
有一位购买了王国之泪实体卡带的玩家在发售后二十天才收到自己的卡带,可是当时最新版1.1.2已经修复了已知的复制bug,而卡带的1.0版本又无法使用便捷的YB复制bug。请你帮他写一份使用++复制的自动化脚本,实现简便的大批量物品复制。
不过,joycontrol的运行速度并不算是非常理想。脚本运行中两个按键的发送间隔会出现随机波动,最终导致脚本并不能完美复现复制bug。过快的两次按键会使得游戏忽视第二次按键,而过慢的按键会导致bug复现失败。就最终效果,以上脚本的复制成功率略高于50%,但胜在可以挂机运行。
同时,该脚本不具备任何闭环反馈。仅依赖流程设计应对各种情况。在面对血月演出等现象时难以自恢复。同时RasCon_NS并未给出长按支持,无法在不断开连接的前提下切换刷取物。
在以请朋友一顿KFC为代价,通过本地同步版本方式更新到1.1.1后,上文提到的这位玩家还试图用脚本复现YB复制。结果表明,YB复制要求的按键间隔已经小于joycontrol的最小发送间隔。在不改进joycontrol的前提下,这种尝试是无意义的。
在后续调研中发现,存在名为EasyCon的,基于单片机进行有线手柄模拟的方案。该方案具有更稳定的连接,以及基础的图像识别支持。同时硬件成本较低,且一直在更新新的硬件方案。如果需要更加稳定的自动化操作,并且不介意使用有线连接,笔者强烈建议使用该方案以寻求更高的上限。
Amiibo解析
与其说本节是Amiibo解析,不如说是NTAG215解析。本节将不会对Amiibo内部承载的数据做过多分析,仅会对以其序列号为主的固定结构进行解析。如果希望了解更加深入的读者可以移步Amiibo模拟笔记,该文章从通信抓取开始对Amiibo进行了分析,并对加解密等流程进行了详实的叙述。本文后续使用的序列号生成方法参考了该文章。如果对内部实现不感兴趣,则可直接跳至下一节。
一般来说,我们下载到的Amiibo数据是一个长度为540字节的二进制文件,该文件实际是对Amiibo手办内嵌NTAG215数据的转储。自然,想要解析它需要的首先是NTAG215定义。
NTAG手册中8.5节Fig 6给出了NTAG215的数据定义,如下图所示。依此我们可以编写模板对二进制dump进行解析,在此笔者直接使用这个适用于010 Editor的NTAG215解析模板。

同时,8.5.1节对我们关心的序列号模式进行了描述。该小节描述了如下的序列号模式:

由此可得知,序列号以及BCC穿插存储于二进制文件的前9字节。其中第一字节固定为NXP的ID:0x04。此外,还给出了BCC0及BCC1的定义如下:
BCC0 = CT XOR SN0 XOR SN1 XOR SN2BCC1 = SN3 XOR SN4 XOR SN5 XOR SN6CT: Cascade Tag (value 88h) as defined in ISO/IEC 14443-3 Type A
此外,amiitool在加解密时对数据进行重排,输出的解密后文件为internal,而加密的文件称为tag。重排规则如下:
void nfc3d_amiibo_tag_to_internal(const uint8_t * tag, uint8_t * intl) {
memcpy(intl + 0x000, tag + 0x008, 0x008);
memcpy(intl + 0x008, tag + 0x080, 0x020);
memcpy(intl + 0x028, tag + 0x010, 0x024);
memcpy(intl + 0x04C, tag + 0x0A0, 0x168);
memcpy(intl + 0x1B4, tag + 0x034, 0x020);
memcpy(intl + 0x1D4, tag + 0x000, 0x008);
memcpy(intl + 0x1DC, tag + 0x054, 0x02C);
}
void nfc3d_amiibo_internal_to_tag(const uint8_t * intl, uint8_t * tag) {
memcpy(tag + 0x008, intl + 0x000, 0x008);
memcpy(tag + 0x080, intl + 0x008, 0x020);
memcpy(tag + 0x010, intl + 0x028, 0x024);
memcpy(tag + 0x0A0, intl + 0x04C, 0x168);
memcpy(tag + 0x034, intl + 0x1B4, 0x020);
memcpy(tag + 0x000, intl + 0x1D4, 0x008);
memcpy(tag + 0x054, intl + 0x1DC, 0x02C);
}
因此,想要修改原位于文件开头9字节的序列号及其校验,实际需要修改解密后文件0x1D4位置的8字节及0x000位置的1字节。
Amiibo序列号改写
至此,我们已经知道了如何去生成一个符合规范的序列号了。虽然后续的一些线索表明似乎不符合规定的序列号也可以被接受
根据上一节的结论,可以编写以下脚本来更改Amiibo序列号。本脚本的基于此讨论,并根据上一部分分析修正了其中的序列号生产及写入逻辑。本脚本适用于windows平台,使用时将脚本置于amiitool.exe同目录下,且目录下含有key_retail.bin即可。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import random
import sys
try:
bin_file = sys.argv[1]
except:
print("\nUsage: %s /path/to/amiibo.bin\n" % sys.argv[0])
raise SystemExit(0)
def changeserial():
uid0 = 0x04
uid1 = random.randint(0, 255)
uid2 = random.randint(0, 255)
bcc0 = uid0 ^ uid1 ^ uid2 ^ 0x88
uid3 = random.randint(0, 255)
uid4 = random.randint(0, 255)
uid5 = random.randint(0, 255)
uid6 = random.randint(0, 255)
bcc1 = uid3 ^ uid4 ^ uid5 ^ uid6
print(
"Using new serial: %02X%02X%02X%02X%02X%02X%02X%02X%02X"
% (uid0, uid1, uid2, bcc0, uid3, uid4, uid5, uid6, bcc1)
)
print("\nDecrypting %s\n" % bin_file)
# decrypting to decrypt.bin
command = f"amiitool.exe -d -k key_retail.bin -i '{bin_file}' -o decrypt.bin"
print(command + "\n")
os.system(command)
try:
with open("decrypt.bin", "r+b") as fh:
print("File decrypted\n")
print("Injecting new serial\n")
fh.seek(0, 0)
fh.write(bytes([bcc1]))
fh.seek(0x1D4, 0)
fh.write(bytes([uid0, uid1, uid2, bcc0, uid3, uid4, uid5, uid6]))
fh.close()
except:
print("\nAborting!\n")
raise SystemExit(0)
print("Encrypting file\n")
command = "amiitool.exe -e -k key_retail.bin -i decrypt.bin"
command += f" -o '{bin_file[:-4]}_%02X%02X%02X%02X%02X%02X%02X%02X%02X.bin'"
command = command % (uid0, uid1, uid2, bcc0, uid3, uid4, uid5, uid6, bcc1)
print(command + "\n")
os.system(command)
print("Cleaning up...")
os.system("del decrypt.bin")
print("Operation Complete\n\n")
while True:
quantity = input("\nQuantity [1]: ")
if quantity == "":
quantity = 1
elif quantity.isdigit() is False:
print("\nPositive numbers only\n")
continue
for i in range(int(quantity)):
changeserial()
break
SystemExit(0)
输入参数为Amiibo bin文件路径,启动后程序会询问需要生成的数量。结合RasCon_NS即可上传本机生成的bin至树莓派,并模拟读取行为。
后记
本来这篇文章是等卡带无聊时写的,结果没写完卡带就到了,遂咕许久。
好消息是游戏打的告一段落了,终于想起来写博客了。
坏消息是草稿里还存着一篇咕了俩月的,不知道什么时候写完。

