Project Status | Finished |
Project Duration | 5 months |
Software Used | Visual Studio |
Languages Used | C++, C#, C, Assembly, Lua |
uTAS is a software suite for the Nintendo Wii U entirely realized with the use of Homebrew. It allows to create TAS (Tool Assisted Speedruns) directly on a console for the first time ever. There have been devices for other consoles to playback an existing TAS that was made with an emulator on PC, but not to create one from scratch.
Excerpt from Wikipedia: "A tool-assisted speedrun is a set sequence of controller inputs used to perform a task in a video game. The input sequence is usually created by emulating the game and using tools such as slow motion, frame-by-frame advance, memory watch, and save states to create an extremely precise series of inputs. The idea is not to make gameplay easier for players, but rather to produce a demonstration of gameplay that would be practically impossible for a human. Tool-assisted speedruns often feature gameplay that would otherwise be impossible or prohibitively difficult to perform in real time. Producers of tool-assisted speedruns do not compete with "unassisted" speedrunners of video games; on the contrary, collaborative efforts between the two groups often take place."
Without the aforementioned features creating a good TAS is next to impossible. Ultimately realizing nearly all of the bullet points on an actual console instead of an emulator took several months of intense work and research. The result is uTAS:
The following features are supported as of the initial release:
The currently available toolchains to create Homebrew for the Wii U are extremely powerful. An exploit in the webbrowser allows you to launch the Homebrew Launcher. The Homebrew Launcher has the ability to load apps in the form of ELF executables compiled by devkitPro to the PowerPC architecture.
After starting an app the HBL will hook itself into the system application loader calling the main entry point of your app if the user selects a game to start from the console home screen. Since your app code is stored in a special unused memory region of the console and the main entry point is called after the console wiped the main RAM in preparation for the game to be launched, you gain the ability to launch your own threads that will run alongside the game.
The entire console API and all its system libraries are available to the Homebrew code allowing you to use network sockets to communicate with external devices, accessing storage media such as the SD card and rendering things using the native GPU functions.
The following Twitch.tv clip demonstrates a boss fight in The Legend of Zelda: Twilight Princess HD on the Wii U that's being vastly speed-up with the help of uTAS and TAS only glitches:
The full TAS of this particular testing session can be found here.
What follows is a short presentation on how to create a TAS with uTAS:
The core pillars of uTAS are:
Function hooks were paramount in realizing uTAS. Due the advanced state of Wii U Homebrew it is actually possible to hook into system as well as game functions and execute your own code before and after calling the "legit" function. Or perhaps not even calling the real function at all. Manipulation of the input and output parameters before returning control to the actual game code are also possible this way.
The following code snippet illustrates how a basic function hook into the VPADRead function was achieved - the function a game has to call to get the current input data from the gamepad. To record and playback inputs for uTAS hooking into this function was essential:
//Buffer that will store our 7 new PPC instructions needed to call the proper real system function later unsigned int realFunctionBuffer[7]; //Function pointer to our buffer so the real function remains accessible in our program code int(*real_VPADRead)(int channel, void *buffer, unsigned int bufferSize, int *error) = &realFunctionBuffer[0]; //Our new VPAD Read function implementation (will get called whenever the game polls the Wii U Gamepad) int my_VPADRead(int channel, void *buffer, unsigned int bufferSize, int *error) { log_printf("Hey the game wants to poll the gamepad"); return real_VPADRead(channel, buffer, bufferSize, error); //Reads the actual inputs from the real system function and returns it to the caller because we are nice } unsigned int realFuncAddr = 0xFFFFFFFF; //Would actually be the address of the Wii U VPADRead function from the vpad.rpl library unsigned int restoreInstruction = 0; //Will store a copy of the first instruction from the real function we need to replace. Used to restore the function later if desired void PatchFunction() { unsigned int *space = &realFunctionBuffer[0]; log_printf("Patching function VPADRead..."); unsigned int physical = (unsigned int)OSEffectiveToPhysical((void*)realFuncAddr); //Gets the physical hardware address of the real function if (!physical) { log_printf("Error. Something is wrong with the physical address!"); return; } //Adds a new DBAT (Data Block Address Translation) entry with the help of a Kernel function Homebrew gained access to //For safety reasons the Wii U does not map its system library address region by default to avoid tampering bat_table_t my_dbat_table; KernelSetDBATsForDynamicFunction(&my_dbat_table, physical); //Copy first instruction from real function to our buffer since we will replace it soon and also safety safe it for restoration ability *space = *(unsigned int*)(physical); space++; restoreInstruction = *(unsigned int*)(physical); //Write our 6 other PPC instructions that perform a jump to realFuncAddr + 4 /* stw r3,-32(r1) lis r3, realFuncAddr+4@h ori r3,r3, realFuncAddr+4@l mtctr r3 lwz r3,-32(r1) bctr */ *space = 0x9061FFE0; //stw r3,-32(r1)...the calling function could be using r3 for an input parameter, so safe it first space++; *space = 0x3C600000 | (((realFuncAddr + 4) >> 16) & 0x0000FFFF); //lis r3, realFuncAddr+4@h space++; *space = 0x60630000 | ((realFuncAddr + 4) & 0x0000ffff); //ori r3, r3, realFuncAddr+4@l...r3 now contains the address to the real system function + 4 (=its second instruction) space++; *space = 0x7C6903A6; //mtctr r3...move r3 to the count register space++; *space = 0x8061FFE0; //lwz r3,-32(r1)...restore input parameter we saved earlier space++; *space = 0x4E800420; //bctr...branch to count register = realFuncAddr + 4 space++; unsigned int instrCount = 7; unsigned int flushLength = 4 * instrCount; //4 bytes per instruction DCFlushRange((void*)(space - instrCount), flushLength); //Immediately flush from cache to actual RAM ICInvalidateRange((unsigned char*)(space - instrCount), flushLength); //Invalidate the cache blocks //Builds a branch instruction to my_VPADRead we copy over the first instruction of the real function unsigned int replaceInstr = 0x48000002 | ((unsigned int)(&my_VPADRead) & 0x03fffffc); *(unsigned int*)(physical) = replaceInstr; ICInvalidateRange((void*)(realFuncAddr), 4); //Important we invalidate the used cache blocks because we restore the old DBAT table next and don't want invalid addresses sitting there //Restore DBAT table KernelRestoreDBATs(&my_dbat_table); log_print("Done with patching!"); }
After the hook we have the following setup:
The question where to store the generated input data and savestates was one I had to solve early on. Initially I was considering storing the content entirely off console by constantly streaming it through the network, but this proved to be unwieldy and not a very good idea in case of unexpected connection failure.
So I opted to store the data on the SD card the user has inserted into his console. Since the files have to be accessible and manageable externally I had to create a semi 'virtual file system' with its own set of commands. A folder would be created on the SD card carrying the game id as its name which would be mapped as the root folder after boot.
The connected PC app is able to retrieve a file and folder list, navigate the tree structure, move, delete and copy files. Commands are packaged up and sent to a listening server socket on the console which translates them into system calls appropriate for the Wii U file system.
The following snippet running on the Wii U is a small excerpt from the command handler showing how the console reads the contents of a folder the desktop application requests and returns the found entries. The second case in the snippet is triggered when the user requests the properties of a TAS movie file which are retrieved from the file header:
//Note: These definitions normally reside in fs_defs.h //FS defines #define FS_STATUS_OK 0 #define FS_RET_NO_ERROR 0x0000 #define FS_RET_ALL_ERROR (unsigned int)(-1) #define FS_STAT_FLAG_IS_DIRECTORY 0x80000000 #define FS_MAX_ENTNAME_SIZE 256 //Max length of a file/folder name //Note: This definition normally resides in tas.h #pragma pack(push, 1) typedef struct //File header for a WUM (Wii U Movie) file, a custom file format for uTAS { u8 filetype[4]; // Unique Identifier (always "WUM"0x01) ; 0x01 is the initial WUM version u64 titleID; // The Game ID u8 countWUMFiles; // Count of WUM files in this recording (each file is roughly 25 MB big) u8 controllerID; // Controller Type used u8 reserved[2]; // Reserved for future use u32 frameCount; // Number of frames in the recording u32 framesLoadCount; // Number of loading frames in the recording (variable) u32 numRerecords; // Number of rerecords/'cuts' of this TAS u8 author[32]; // Author's name (encoded in ASCII) u64 recordingStartTime; // ticks since year 2000 that recording started u8 reserved2[60]; // Make header settings 128 bytes, reserved for new options u64 md5Table[240]; //120 MD5 verification entries possible; 1920 bytes ; each md5 entry has to handle at least 2 MB } WUMHeader; static_assert(sizeof(WUMHeader) == 2048, "WUMHeader should be 2048 bytes"); #pragma pack(pop) #define CHECK_ERROR(cond) if (cond) { bss->line = __LINE__; goto error; } //FS vars static void *pClient = NULL; static void *pCmd = NULL; static char basePath[32]; static char titlePath[64]; static int TASMain(struct pygecko_bss_t *bss, int clientfd) { int ret; unsigned char buffer[0x401] u64 titleID = OSGetTitleID(); strcpy(basePath, "/vol/external01/wiiu/TAS"); sprintf(titlePath, "%s/%llX", basePath, titleID); while (1) { OSSleepTicks(MILLISECS_TO_TICKS(11)); //90 FPS refresh ret = checkbyteTAS(bss, clientfd); //Attempts to read a single byte from the socket but returns if no message is in the queue if (ret < 0) continue; switch (ret) { //FS Functions case 0xF0: //Read Folder Content { int status = -1; int handle = 0; char finalPath[255]; //Wait out async tasks before doing any new FS commands on the SD card WaitForAsyncTask(); //Receive path length ret = recvwaitTAS(bss, clientfd, buffer, 1); //Blocks until message has been received fully CHECK_ERROR(ret < 0); u8 pathLength = buffer[0]; if (pathLength > 0) { //Receive path string ret = recvwaitTAS(bss, clientfd, buffer, pathLength); CHECK_ERROR(ret < 0); const char* checkPath = (const char*)&buffer[0]; sprintf(finalPath, "%s/%s", titlePath, checkPath); } else //Dump Base Title Path { strcpy(finalPath, titlePath); } log_printf("Open and read Path: %s", finalPath); if ((status = FSOpenDir(pClient, pCmd, finalPath, &handle, FS_RET_ALL_ERROR)) == FS_STATUS_OK) { int countFolders = 0; int countFiles = 0; int folderPos = 0; int filePos = 0; char fileBuffer[8192]; char folderBuffer[8192]; FSDirEntry dir_entry; while (FSReadDir(pClient, pCmd, handle, &dir_entry, FS_RET_ALL_ERROR) == FS_STATUS_OK) { if ((dir_entry.stat.flag & FS_STAT_FLAG_IS_DIRECTORY) == FS_STAT_FLAG_IS_DIRECTORY) //Folder { if (!strcmp(dir_entry.name, "_temp")) //Exclude temp movie folder (hidden) continue; u8 length = strlen(dir_entry.name) + 1; folderBuffer[folderPos] = length; folderPos += 1; memcpy(&folderBuffer[folderPos], dir_entry.name, length); folderPos += length; countFolders++; } else //Files { if (strstr(dir_entry.name, ".wum.") == 0 && strstr(dir_entry.name, ".sav.") == 0) //Only grab main movie and state files (exclude .1, .2 chunk movie files too) { u8 length = strlen(dir_entry.name) + 1; fileBuffer[filePos] = length; filePos += 1; memcpy(&fileBuffer[filePos], dir_entry.name, length); filePos += length; countFiles++; } } } if ((status = FSCloseDir(pClient, pCmd, handle, FS_RET_NO_ERROR)) < FS_STATUS_OK) { log_printf("Error while closing dir"); ret = sendbyteTAS(bss, clientfd, (u8)status); //Send error code to connected PC app CHECK_ERROR(ret < 0); } else { //Send status OK ret = sendbyteTAS(bss, clientfd, 0); CHECK_ERROR(ret < 0); //Send folder count ret = sendbyteTAS(bss, clientfd, (u8)countFolders); CHECK_ERROR(ret < 0); //Stream directory list if (countFolders > 0) { folderPos = 0; for (int n = 0; n < countFolders; n++) { //Len of directory name u8 length = folderBuffer[folderPos]; folderPos += 1; ret = sendbyteTAS(bss, clientfd, length); CHECK_ERROR(ret < 0); //Directory name memcpy(&buffer[0], &folderBuffer[folderPos], length); ret = sendwaitTAS(bss, clientfd, buffer, length); CHECK_ERROR(ret < 0); folderPos += length; } } //Send file count ret = sendbyteTAS(bss, clientfd, (u8)countFiles); CHECK_ERROR(ret < 0); //Stream file list if (countFiles > 0) { filePos = 0; for (int n = 0; n < countFiles; n++) { u8 length = fileBuffer[filePos]; filePos += 1; ret = sendbyteTAS(bss, clientfd, length); CHECK_ERROR(ret < 0); memcpy(&buffer[0], &fileBuffer[filePos], length); ret = sendwaitTAS(bss, clientfd, buffer, length); CHECK_ERROR(ret < 0); filePos += length; } } } } else { log_printf("Error opening dir!"); ret = sendbyteTAS(bss, clientfd, (u8)status); CHECK_ERROR(ret < 0); } break; } case 0xF4: //Get WUM Header Info { int status = -1; int handle = 0; char finalPath[255]; WaitForAsyncTask(); //Receive file path length ret = recvwaitTAS(bss, clientfd, buffer, 1); CHECK_ERROR(ret < 0); u8 pathLength = buffer[0]; if (pathLength > 0) { //Receive .wum file path string ret = recvwaitTAS(bss, clientfd, buffer, pathLength); CHECK_ERROR(ret < 0); const char* checkPath = (const char*)&buffer[0]; sprintf(finalPath, "%s/%s", titlePath, checkPath); //Load Movie file (.wum) if ((status = FSOpenFile(pClient, pCmd, finalPath, "r", &handle, -1)) == FS_STATUS_OK) { //Read WUM Header WUMHeader* header = (WUMHeader*)memalign(64, sizeof(WUMHeader)); memset(header, 0, sizeof(WUMHeader)); status = -1; if ((status = FSReadFile(pClient, pCmd, header, sizeof(WUMHeader), 1, handle, 0, -1)) >= FS_STATUS_OK) { status = 0; //OK //Status ret = sendbyteTAS(bss, clientfd, (u8)status); CHECK_ERROR(ret < 0); //Title ID ret = sendwaitTAS(bss, clientfd, &header->titleID, 8); CHECK_ERROR(ret < 0); //Controller ID ret = sendwaitTAS(bss, clientfd, &header->controllerID, 1); CHECK_ERROR(ret < 0); //Frame Count ret = sendwaitTAS(bss, clientfd, &header->frameCount, 4); CHECK_ERROR(ret < 0); //Load Count ret = sendwaitTAS(bss, clientfd, &header->framesLoadCount, 4); CHECK_ERROR(ret < 0); //Re-record Count ret = sendwaitTAS(bss, clientfd, &header->numRerecords, 4); CHECK_ERROR(ret < 0); //Author Name ret = sendwaitTAS(bss, clientfd, &header->author[0], 32); CHECK_ERROR(ret < 0); //Recording Start Time ret = sendwaitTAS(bss, clientfd, &header->recordingStartTime, 8); CHECK_ERROR(ret < 0); free(header); } FSCloseFile(pClient, pCmd, handle, -1); } } if (status != 0) //Send error code as final message if operation failed { ret = sendbyteTAS(bss, clientfd, (u8)status); CHECK_ERROR(ret < 0); } break; } default: log_printf("Ret is: %i ; N/A!", ret); CHECK_ERROR(ret == 0); //Connection was terminated normally break; } } }
Responses are passed back to the PC application which interprets them and performs updates on its visual presentation of the files and folders. The view is specifically geared towards TASing and will only show files with extensions that indicate them clearly belonging to uTAS.
The control panel of the entire operation. Maintains several threads and socket based connections to the Wii U to, for one, retrieve memory values for the Data Display which can be easily manipulated in real time by editing a config file:
//maximum of 20 entries allowed //allowed value types: byte, short, int, float, stringX (X indicating number of chars to read) //allowed display types: dec, hex Name="Speed:" Address=0x10681E40 + 0x5C ValueType=float Name="Facing:" Address=0x10681E40 + 0x16 ValueType=short DisplayAs=dec Name="Link X:" Address=0x10681E40 + 0x0 ValueType=float Name="Stage:" Address=0x1064CDE8 ValueType=string8 Name="Room ID:" Address=0x106813D4 ValueType=byte DisplayAs=dec Name="Spawn ID:" Address=0x1064CDF1 ValueType=byte DisplayAs=hex Name="State:" Address=0x1062915B ValueType=string1
This C# snippet from the PC app handles transferring the watch list (read out from the config file) to the Wii U so it can start 'watching' the desired memory addresses:
using System; using System.Net.Sockets; using System.Threading; struct watchListEntry { public bool usePtr; public UInt32 address; public UInt32 offset; public byte type; public byte dataSize; public byte viewMode; }; private void dataThread(object client_obj) { NetworkStream dataStream = (NetworkStream)client_obj; EndianBinaryReader reader = new EndianBinaryReader(dataStream); EndianBinaryWriter writer = new EndianBinaryWriter(dataStream); List<watchListEntry> watchList = new List<watchListEntry>(); /* ..... */ //Send Watch List over to Wii U if (watchList.Count > 0) { //Calculate total byte count int totalBytes = 0; foreach (watchListEntry entry in watchList) { totalBytes += 1; //(bool) is ptr? totalBytes += 4; //(uint) normal address or ptr if (entry.usePtr) totalBytes += 4; //(uint) offset totalBytes += 1; //(byte) dataSize } Byte[] buffer = new Byte[totalBytes]; //Write Watch List into buffer UInt32 index = 0; foreach (watchListEntry entry in watchList) { if (entry.usePtr) { //is Ptr = true buffer[index] = 1; index += 1; //Ptr Address Buffer.BlockCopy(BitConverter.GetBytes(ByteSwap.Swap(entry.address)), 0, buffer, index, 4); //Swap bytes since Wii U uses big endian index += 4; //Offset Buffer.BlockCopy(BitConverter.GetBytes(ByteSwap.Swap(entry.offset)), 0, buffer, index, 4); index += 4; } else { //is Ptr = false buffer[index] = 0; index += 1; //Normal Address Buffer.BlockCopy(BitConverter.GetBytes(ByteSwap.Swap(entry.address)), 0, buffer, index, 4); index += 4; } //Data Size buffer[index] = entry.dataSize; index += 1; } //Send total byte count to Wii U first, then the Watch List writer.Write(totalBytes); writer.Write(buffer, 0, totalBytes); } else { int noData = 0; writer.Write(noData); //Send empty message } }
Furthermore the desktop application manages updates to its virtual file system representation and pushes input data and user commands to the console. On top of that it can run Lua scripts on the side and react Windows wide to the use of hotkeys if desired.
uTAS ships with a robust Lua integration that allows to invoke all the essential functionality from a script environment which can be very useful in creating input templates for example.
The available functions:
private void RegisterLuaFunctions() { //Basic Functions System.Reflection.MethodBase cancelScript = typeof(LuaHandler).GetMethod("LUA_cancelScript"); state.RegisterFunction("CancelScript", cancelScript); System.Reflection.MethodBase msgBox = typeof(LuaHandler).GetMethod("LUA_msgBox"); state.RegisterFunction("MsgBox", msgBox); System.Reflection.MethodBase frameAdvance = typeof(LuaHandler).GetMethod("LUA_frameAdvance"); state.RegisterFunction("FrameAdvance", frameAdvance); System.Reflection.MethodBase getFrameCount = typeof(LuaHandler).GetMethod("LUA_getFrameCount"); state.RegisterFunction("GetFrameCount", getFrameCount); //Memory Read System.Reflection.MethodBase read8 = typeof(LuaHandler).GetMethod("LUA_read8"); state.RegisterFunction("ReadValue8", read8); System.Reflection.MethodBase read16 = typeof(LuaHandler).GetMethod("LUA_read16"); state.RegisterFunction("ReadValue16", read16); System.Reflection.MethodBase read32 = typeof(LuaHandler).GetMethod("LUA_read32"); state.RegisterFunction("ReadValue32", read32); System.Reflection.MethodBase readFloat = typeof(LuaHandler).GetMethod("LUA_readFloat"); state.RegisterFunction("ReadValueFloat", readFloat); System.Reflection.MethodBase readString = typeof(LuaHandler).GetMethod("LUA_readString"); state.RegisterFunction("ReadValueString", readString); //Memory Write System.Reflection.MethodBase write8 = typeof(LuaHandler).GetMethod("LUA_write8"); state.RegisterFunction("WriteValue8", write8); System.Reflection.MethodBase write16 = typeof(LuaHandler).GetMethod("LUA_write16"); state.RegisterFunction("WriteValue16", write16); System.Reflection.MethodBase write32 = typeof(LuaHandler).GetMethod("LUA_write32"); state.RegisterFunction("WriteValue32", write32); System.Reflection.MethodBase writeFloat = typeof(LuaHandler).GetMethod("LUA_writeFloat"); state.RegisterFunction("WriteValueFloat", writeFloat); System.Reflection.MethodBase writeString = typeof(LuaHandler).GetMethod("LUA_writeString"); state.RegisterFunction("WriteValueString", writeString); //Input Manipulation System.Reflection.MethodBase pressButton = typeof(LuaHandler).GetMethod("LUA_pressButton"); state.RegisterFunction("PressButton", pressButton); System.Reflection.MethodBase releaseButton = typeof(LuaHandler).GetMethod("LUA_releaseButton"); state.RegisterFunction("ReleaseButton", releaseButton); System.Reflection.MethodBase pressMainX = typeof(LuaHandler).GetMethod("LUA_setMainStickX"); state.RegisterFunction("SetMainStickX", pressMainX); System.Reflection.MethodBase pressMainY = typeof(LuaHandler).GetMethod("LUA_setMainStickY"); state.RegisterFunction("SetMainStickY", pressMainY); System.Reflection.MethodBase pressCX = typeof(LuaHandler).GetMethod("LUA_setCStickX"); state.RegisterFunction("SetCStickX", pressCX); System.Reflection.MethodBase pressCY = typeof(LuaHandler).GetMethod("LUA_setCStickY"); state.RegisterFunction("SetCStickY", pressCY); //Savestate System.Reflection.MethodBase saveState = typeof(LuaHandler).GetMethod("LUA_saveState"); state.RegisterFunction("SaveState", saveState); System.Reflection.MethodBase loadState = typeof(LuaHandler).GetMethod("LUA_loadState"); state.RegisterFunction("LoadState", loadState); }
HelloWorld.lua
startCounter = 0 function onScriptStart() --called on script launch startCounter = GetFrameCount() FrameAdvance() --advances a frame end function onScriptCancel() MsgBox("I will sleep now") end function onScriptUpdate() --called every frame mainFunc() end function onLoadStateSuccess() end function onLoadStateFail() end function round(num, idp) local mult = 10^(idp or 0) return math.floor(num * mult + 0.5) / mult end function mainFunc() local counter = GetFrameCount() if counter >= startCounter + 6 then local secs = counter / 30 secs = round(secs, 1) MsgBox(string.format("Hello World! We are %.1f seconds into the TAS!", secs)) CancelScript() --ends the script end FrameAdvance() end
This example Lua file ships with the uTAS v1.0 release.
I knew that with uTAS the door could be wide opened to potential abuse for example by passing off a TAS playback as a real run performed by a human. Since it would occur on a legit console in one segment it would be almost impossible to prove whether or not someone cheated through analysing the video and audio footage alone.
Therefore I decided to do a limited release at the start and release my tool to trustworthy people in the community only. To enforce it came up with a serial verification system.
This snippet is responsible for verifying the unique console id which I use as the serial against a list of allowed ids:
//Lib Curl static u32 curlDataPos = 0; //Curl callback function writing received data into a destination buffer static size_t curlWriteBuffer(void *buffer, size_t size, size_t count, char *receiveStream) { int totalSize = size * count; memcpy(receiveStream + curlDataPos, buffer, totalSize); curlDataPos += totalSize; return totalSize; } static int VerifyConsole() { int outputStatus = -1; //Init Curl Libs InitCurlFunctionPointers(); if (NSSLInit() != 0) { log_printf("No SSL available!"); return 0; } int sslContext = NSSLCreateContext(); int pkiVal1 = 0; int pkiVal2 = 0; //Add PKI Group Certificates if (NSSLAddServerPKIGroups(sslContext, 3, &pkiVal1, &pkiVal2) != 0) { log_printf("No Group Certificates available!"); NSSLDestroyContext(sslContext); NSSLFinish(); return 0; } //Init Curl n_curl_global_init(CURL_GLOBAL_ALL); CURL* curl = n_curl_easy_init(); if (!curl) { log_printf("Curl handle could not be created!"); NSSLDestroyContext(sslContext); NSSLFinish(); return 0; } char* keyBuffer = (char*)malloc(10000); //10 KB should be plenty memset(keyBuffer, 0, 10000); //Set SSL Context and PKI Group Count n_curl_easy_setopt(curl, 210, sslContext); n_curl_easy_setopt(curl, 211, 3); //Set Options n_curl_easy_setopt(curl, CURLoption::CURLOPT_URL, "https://pastebin.com/raw/XXXX"); //Try unlisted Pastebin verification file first n_curl_easy_setopt(curl, CURLoption::CURLOPT_HEADER, false); n_curl_easy_setopt(curl, CURLoption::CURLOPT_FOLLOWLOCATION, false); n_curl_easy_setopt(curl, CURLoption::CURLOPT_ENCODING, ""); n_curl_easy_setopt(curl, CURLoption::CURLOPT_USERAGENT, "uTAS"); n_curl_easy_setopt(curl, CURLoption::CURLOPT_AUTOREFERER, false); n_curl_easy_setopt(curl, CURLoption::CURLOPT_CONNECTTIMEOUT, 5); n_curl_easy_setopt(curl, CURLoption::CURLOPT_TIMEOUT, 10); n_curl_easy_setopt(curl, CURLoption::CURLOPT_MAXREDIRS, 3); n_curl_easy_setopt(curl, CURLoption::CURLOPT_WRITEFUNCTION, curlWriteBuffer); n_curl_easy_setopt(curl, CURLoption::CURLOPT_WRITEDATA, keyBuffer); log_printf("Perform Curl"); curlDataPos = 0; CURLcode response = n_curl_easy_perform(curl); if (response == 0) { //log_printf("Received total data from pastebin: %i", curlDataPos); if (!strstr(keyBuffer, "Authentication List uTAS:")) { curlDataPos = 0; } else //List is correct { outputStatus = 1; } } else { log_printf("Curl returned error: %i", response); curlDataPos = 0; } if (curlDataPos == 0) { //Try Dropbox as fallback if pastebin is down/displays heavy load site n_curl_easy_setopt(curl, CURLoption::CURLOPT_URL, "https://dl.dropboxusercontent.com/s/XXXXXX"); memset(keyBuffer, 0, 10000); response = n_curl_easy_perform(curl); if (response == 0) { //log_printf("Received total data from dropbox: %i", curlDataPos); if (!strstr(keyBuffer, "Authentication List uTAS:")) { log_printf("Both pastebin and dropbox are invalid lists, critical!"); outputStatus = 0; } else //List is correct { outputStatus = 1; } } else { log_printf("Both pastebin and dropbox returned errors, critical!"); outputStatus = 0; } } if (outputStatus == 1) //Key List received, compare with console ID { outputStatus = -1; char* consoleKey = (char*)malloc(36); memset(consoleKey, 0, 36); sprintf(consoleKey, "%08X-%08X-%08X-%08X", g_consoleID[0], g_consoleID[1], g_consoleID[2], g_consoleID[3]); //log_printf("Console key: %s", consoleKey); const char delimiters[] = "\r\n"; char* running = keyBuffer; char* token; token = strsep(&running, delimiters); while (token != NULL) { if (strlen(token) > 34) { if (!strcmp(token, consoleKey)) { //log_printf("match found with %s!!", token); outputStatus = 1; break; } } token = strsep(&running, delimiters); } free(consoleKey); } //Cleanup free(keyBuffer); n_curl_easy_cleanup(curl); NSSLDestroyContext(sslContext); NSSLFinish(); return outputStatus; } static int start_TASThread(int argc, void *argv) { int sockfd = -1, clientfd = -1, ret = 0, len; int optval = 1; int handshakeAttempts = 0; struct sockaddr_in addr; struct pygecko_bss_t *bss = (pygecko_bss_t*)argv; int status = -1; log_printf("TAS Thread Started!"); while (1) { addr.sin_family = AF_INET; addr.sin_port = 7340; //TAS Tool uses port 7340 addr.sin_addr.s_addr = 0; //Use Wii U IP sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //Open a handle to socket CHECK_ERROR(sockfd == -1); setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval); //Ensures we can bind the port @Wii U IP in every case even when a connection just died ret = bind(sockfd, (sockaddr*)&addr, 16); CHECK_ERROR(ret < 0); ret = listen(sockfd, 20); //Listens for incoming connections from the TAS Tool CHECK_ERROR(ret < 0); log_printf("Ready for a connection to the TAS Tool..."); len = 16; clientfd = accept(sockfd, (sockaddr*)&addr, &len); //Block the thread until a client connects CHECK_ERROR(clientfd == -1); log_printf("TAS Tool connected!"); handshakeAttempts = 0; while (handshakeAttempts < 5000) //Wait 5 seconds for the handshake signal, else abort { ret = checkbyteTAS(bss, clientfd); if (ret == 0xFF) //0xFF is the handshake signal break; OSSleepTicks(MILLISECS_TO_TICKS(1)); handshakeAttempts++; } if (handshakeAttempts >= 5000) goto error; status = VerifyConsole(); if (status == -1) //Console failed verification, send consoleID to TAS Tool to prompt the user to verify himself { log_printf("Verification failed, ask user to authenticate"); ret = sendbyteTAS(bss, clientfd, 0); CHECK_ERROR(ret < 0); ret = sendwaitTAS(bss, clientfd, &g_consoleID[0], 4); CHECK_ERROR(ret < 0); ret = sendwaitTAS(bss, clientfd, &g_consoleID[1], 4); CHECK_ERROR(ret < 0); ret = sendwaitTAS(bss, clientfd, &g_consoleID[2], 4); CHECK_ERROR(ret < 0); ret = sendwaitTAS(bss, clientfd, &g_consoleID[3], 4); goto error; } else if (status == 0) //Servers are not reachable { log_printf("Internet/Server error, inform user"); ret = sendbyteTAS(bss, clientfd, 1); goto error; } else if (status == 1) //Console passed verification { log_printf("Console passed verification, unlock TAS Tool"); ret = sendbyteTAS(bss, clientfd, 2); CHECK_ERROR(ret < 0); } TASToolConnected = 1; ret = TASMain(bss, clientfd); //Run main loop of TAS thread log_printf("TAS connection has closed!"); TASToolConnected = 0; if (clientfd != -1) socketclose(clientfd); if (sockfd != -1) socketclose(sockfd); bss->error = ret; clientfd = -1; continue; error: if (clientfd != -1) socketclose(clientfd); if (sockfd != -1) socketclose(sockfd); bss->error = ret; clientfd = -1; sockfd = -1; } return 0; }
The unique console id is read out from the kernel by uTAS on boot, hashed and finally verified with a pastebin/dropbox document to confirm if the id has been granted access. In the case it hasn't the hashed id is presented to the user with a warning and to move on the id needs to be submitted and activated.