Closed gpavelski closed 1 year ago
Yes, the patch can be modified to use offsets for the Japanese version of Dream Radar.
Since I don't have the Japanese version of Dream Radar, I won't be able to get the offsets. However, I can write some instructions to find the offsets if you or someone else with the game is willing to help.
If it may help, I used gm9 to dump that title into a .cia file, that can be found here (link removed). Otherwise, if you give me the instructions, I can see what I can do.
Thanks
Sorry for the late response! I've been sick for the past week.
I removed the cia link since those shouldn't be shared.
As for the instructions, are you willing to use Ghidra? That would be the easiest option. If not, I can send instructions for finding the offsets using a hex editor, but that might not work as well.
I am sorry to hear that, hope you are getting well.
Yes, Ghidra is fine, I downloaded it just now and it seems to be working.
I'm doing much better, thank you.
If you have any questions or some of these steps do not work, please let me know.
make_elf.py
script from hereexefs/.code
and extheader.bin
files to your computerextheader.bin
to exh.bin
ExeFS
and move .code
to ExeFS/code.bin
exh.bin
make_elf.py
script to create ExeFS.elf
ExeFS.elf
in GhidraSearch for the value on the left, and get the address of the function using it:
Find the fs:USER file handle address:
fs:USER
GetServiceHandleDirect
is the handleFind the wrapper function for FSUSER_CardSlotIsInserted:
Find the function that reads the nds card Id:
Find the function that writes nds save data:
00 10 a0 e3 07 00 44 e0 04 20 8d e2 00 10 8d e5 0b 00 80 e0 41 00 82 e8
Find an address with lot of empty space at the bottom of the .text
section.
Ok, I followed the steps until the Ghidra part, where I think something is wrong. So, just to recapitulate, the ".code" file dumped using gm9 should be renamed to "code.bin" and placed in the folder ExeFS, is that right?
When I run the python script, I get as output:
b'Name: POKE AR' Flag: 03 [compressed][sd app] Rev.: 0000 .text addr: 00100000 .text page: 000000BE .text size: 000BD9E4 stack size: 00040000 .read addr: 001BE000 .read page: 00000015 .read size: 00014ED8 .data addr: 001D3000 .data page: 00000015 .data size: 000143FC .bss size: 00040494
The ExeFS.elf file is created, but then I open Ghidra, I create a new Project, I import the .elf file on it, and try to use the Code Browser to read it. However, in line 107 of the Decompiler, it shows a "halt_baddata();" command (Error [Bad Instruction]: Unable to resolve constructor at 0x0010c85c). So, I don't know if the problem is in the dumped files, the python script or in the Ghidra. Any ideas?
Thank you.
So, just to recapitulate, the ".code" file dumped using gm9 should be renamed to "code.bin" and placed in the folder ExeFS, is that right?
Yep, that is correct. The output from the python script looks good too.
However, in line 107 of the Decompiler, it shows a "halt_baddata();" command (Error [Bad Instruction]: Unable to resolve constructor at 0x0010c85c). So, I don't know if the problem is in the dumped files, the python script or in the Ghidra. Any ideas?
halt_baddata
happens when ghidra doesn't recognize an instruction, and possibly in a few other scenarios where it can't detect the correct flow. This might occasionally come up, but as long as most functions don't show this, the file was loaded correctly.
Do most functions show this, or only a few?
Are you still able to search for the values in my last comment?
Actually, I am not able to search for these values. When I load the ELF file into Ghidra, it displays this additional info:
----- Loading /C:/Users/Desktop/MakeElf/ExeFS.elf ----- Skipping zero-length segment [1,Loadable segment] at address ram:001be000 Skipping zero-length segment [2,Loadable segment] at address ram:001d3000 Skipping section [.fini] with invalid size 0x0 Skipping section [.rodata] with invalid size 0x0 Skipping section [.memregion] with invalid size 0x0 Skipping section [.data] with invalid size 0x0
It skips the important sections of the program for some reason and there is only one function being detected.
ExeFS.zip (link removed)
That's interesting. I wonder if GM9 didn't decompress the .code
when it was dumped. I thought it did, but maybe I was mistaken.
Try decompressing it with ctrtool:
ctrtool -t lzss --lzssout=decompressed-code.bin code.bin
decompressed-code.bin
to ExeFS/code.bin
make_elf.py
and load the result into GhidraIf that doesn't work, try this:
exefs.bin
ctrtool -t exefs --decompresscode --exefsdir=ExeFS --exheader=exh.bin exefs.bin
code.bin
make_elf.py
and load the result into GhidraYes, you are right, After I used the ctrtool, it worked.
I found all the function addresses that you asked me for, even the one with the long sequence of bytes.
About the handle, I found these two statements:
uVar2 = FUN_0016de1c(s_fs:USER_001c417f); iVar1 = FUN_0016dde8(&DAT_001d9a1c,s_fs:USER_001c417f,uVar2,0);
So does that mean that the handle address is 001d8ac1c?
What are the next steps?
Awesome, I'm glad to hear it.
0x001d9a1c
should be the fs handle. FUN_0016de1c
is strlen
, and FUN_0016dde8
is srvGetServiceHandleDirect.
There's one more address I forgot to have you find: the address that reads nds save data. Similar to the write function, this one is a little difficult to find, so I'm hoping some of the bytes will match. Get the address of the function that has these bytes: 60 00 95 e8 60 a0 8d e2 60 00 8a e8 10 50 82 e2 58 10 cd e5
.
Next steps:
A few weeks ago I made a branch called armips-needs-testing that rewrites this patch to use armips. I did this because I think it's easier to read and make changes. This is the perfect chance to use it.
To build the patches, you'll need armips, which creates a patched code.bin
, and flips, which creates a code.ips
from the original and patched code.bin
s.
Once you have armips and flips:
FSUSER_CardSlotIsInserted
wrapper address.text
radar.s
with your copied fileActually, I still have one question concerning the "large amount of empty space in .text". Is there a more specific way to find this address?
After I use flips to generate the ips patch, I noticed that the string "/roms/nds/saves/white2.sav" appears is in the wrong location:
As you can see in the picture, it should appear exactly before the EOF, but it is appearing much earlier.
And besides, on Ghidra, the last instruction of .text is 001bd9e3 e1 ?? E1h
, then .rodata starts with DAT_001be000
, should this address be in this interval?
Thank you
Yeah, you have the right idea. .text
is where executable instructions are stored, so we want to find empty space in between the last instruction of .text
and the first byte of .rodata
. The address that is used needs to be a multiple of 4.
These lines will fill the empty space with the sd save utils and check_if_save_exists
. It looks like that uses 58 instructions, two words, and the save file name, so ~270 bytes are needed.
After I use flips to generate the ips patch, I noticed that the string "/roms/nds/saves/white2.sav" appears is in the wrong location
The armips-needs-testing
branch makes a few size optimizations and moves things around, so it won't look identical to the currently released patches. In the new branch, the save path is at the end of the sd save utils, which is included before check_if_save_exists.
Is the patch not working?
If it isn't, you might try modifying these values in the python script instead.
After I apply the patch, I boot the game, then when I press the second button (to transfer the data), it gives me an error.
Error type: generic
Process ID: 41
Process name: POKE AR
Process title ID: 0x0004000000073200
Address: 0x00137b48
Errror code: 0xfff8e41c
If I send you my radar.s and my ExeFS.elf, could you help me to check if the addresses are correct?
Files.zip (link removed)
Thanks again.
I found a few bugs in my patch rewrite. I tested and pushed fixes to the armips-needs-testing
branch. Pull the latest changes and use the updated radar.s
file to make your patch.
I also wrote a python script that finds and prints the addresses you need. Run that script with ghidra and update radar.s
with the addresses the script prints. After that, build and test the new patch.
Here is the script:
# Prints addresses for the Pokemon Radar Cart Redirect patch
# @category 3ds
#
from array import array
from ghidra.app.decompiler import DecompInterface
# ----------------------------------------
# Globals
# ----------------------------------------
memory = currentProgram.getMemory()
decomp = DecompInterface()
decomp.openProgram(currentProgram)
# ----------------------------------------
# Utils
# ----------------------------------------
def bytesToByteStr(bytes):
encoded_bytes = map(lambda byte : chr(byte), bytes)
return ''.join(encoded_bytes)
def bytesToByteArr(bytes):
return array('b', bytesToByteStr(bytes))
def findBytes(start_addr, byte_arr):
return memory.findBytes(start_addr, byte_arr, None, True, monitor)
def findFirstByteArray(bytes):
byte_arr = bytesToByteArr(bytes)
current_addr = findBytes(toAddr(0), byte_arr)
while current_addr.getOffset() % 4 != 0:
current_addr = findBytes(current_addr.add(4), byte_arr)
return current_addr
def findFirstByteString(byte_str):
return findFirstByteArray(array('B', byte_str.decode('hex')))
def findFirstUint(num):
num_bytes = [num & 0xff, (num >> 8) & 0xff, (num >> 16) & 0xff, (num >> 24) & 0xff]
return findFirstByteArray(num_bytes)
def findFirstFunctionUsingUint(num):
num_addr = findFirstUint(num)
num_ref = getReferencesTo(num_addr)[0]
num_use = num_ref.getFromAddress()
func = getFunctionContaining(num_use)
return func.getEntryPoint()
def findFirstFunctionWithBytes(byte_str):
bytes_addr = findFirstByteString(byte_str)
func = getFunctionContaining(bytes_addr)
return func.getEntryPoint()
def getFirstCallingFunction(func_addr):
callers_refs = getReferencesTo(func_addr)
first_caller_addr = callers_refs[0].getFromAddress()
return getFunctionContaining(first_caller_addr).getEntryPoint()
def findFirstString(find_str):
find_bytes = array('b', find_str)
return findBytes(toAddr(0), find_bytes)
def findNextFunctionCallRef(addr):
while addr < currentProgram.getMaxAddress():
refs = getReferencesFrom(addr)
for ref in refs:
if ref.getReferenceType().isCall():
return ref
addr = addr.add(4)
return None
def getCallArgs(caller_func, callee_addr):
decompiled_func = decomp.decompileFunction(caller_func, 60, monitor)
high_func = decompiled_func.getHighFunction()
if not high_func:
return []
opiter = high_func.getPcodeOps()
while opiter.hasNext():
op = opiter.next()
mnemonic = str(op.getMnemonic())
if mnemonic == 'CALL':
inputs = op.getInputs()
addr = inputs[0].getAddress()
if addr == callee_addr:
return inputs[1:]
return []
def getCallArgsFromRef(call_ref):
from_addr = call_ref.getFromAddress()
to_addr = call_ref.getToAddress()
func = getFunctionContaining(from_addr)
return getCallArgs(func, to_addr)
# ----------------------------------------
# Find radar addresses
# ----------------------------------------
# Get base addresses for most functions
fsuser_openfiledirectly = findFirstFunctionUsingUint(0x8030204)
fsfile_read = findFirstFunctionUsingUint(0x80200C2)
fsfile_write = findFirstFunctionUsingUint(0x8030102)
fsfile_close = findFirstFunctionUsingUint(0x8080000)
fsuser_cardslotisinserted = findFirstFunctionUsingUint(0x8210000)
fsuser_getlegacyromheader2 = findFirstFunctionUsingUint(0x8460102)
read_nds_save = findFirstFunctionWithBytes('600095e860a08de260008ae8105082e25810cde5')
write_nds_save = findFirstFunctionWithBytes('0010a0e3070044e004208de200108de50b0080e0410082e8')
# Get wrapper and calling function addresses
read_nds_save_wrapper = getFirstCallingFunction(read_nds_save)
fsuser_cardslotisinserted_wrapper = getFirstCallingFunction(fsuser_cardslotisinserted)
fsuser_getlegacyromheader2_wrapper = getFirstCallingFunction(fsuser_getlegacyromheader2)
read_nds_cart_id = getFirstCallingFunction(fsuser_getlegacyromheader2_wrapper)
# Find fs handle
fs_user_str = findFirstString('fs:USER')
# First ref is typically strlen
# Second ref should be with GetServiceHandleDirect
fs_user_ref = getReferencesTo(fs_user_str)[1].getFromAddress()
get_fs_handle_call = findNextFunctionCallRef(fs_user_ref)
get_fs_handle_args = getCallArgsFromRef(get_fs_handle_call)
fs_handle_ref_offset = get_fs_handle_args[0].getAddress()
fs_handle = memory.getInt(fs_handle_ref_offset)
# Print results
print('fsuser_openfiledirectly {}'.format(fsuser_openfiledirectly))
print('fsfile_read {}'.format(fsfile_read))
print('fsfile_write {}'.format(fsfile_write))
print('fsfile_close {}'.format(fsfile_close))
print('fsuser_cardslotisinserted_wrapper {}'.format(fsuser_cardslotisinserted_wrapper))
print('read_nds_save_wrapper {}'.format(read_nds_save_wrapper))
print('read_nds_cart_id {}'.format(read_nds_cart_id))
print('write_nds_save {}'.format(write_nds_save))
print('fs_handle {:x}'.format(fs_handle))
Well, it's almost working now. Actually, running the Python script showed that I got some addresses wrong. Now the patch detects my white2.sav and even prompts to send the data. However, for some reason, the data is not sent. It displays that animation of sending data, but it fails in the end. I tried to create a new save data, but it still didn't finish the transfer.
Do you think it may be related to the region of the game?
I don't think the region would cause any issues.
Just to make sure, are you using the Nintendo 3ds link after launching White 2?
Have you tried replacing these values in the python script and making a patch that way?
Just to make sure, are you using the Nintendo 3ds link after launching White 2?
Yes, when I open the Unova 3DS Link, it says there is no data to be transferred from Dream Radar.
Have you tried replacing these values in the python script and making a patch that way?
I can try, but how do I make sure what is my card_id
? How do I know if it is IRDO
or other value?
I tried to replace the values and run this command on a CMD window:
python poke_redirect.py "pokear" "IRDO" "/nds/saves/white2.sav"
However, it only printed that as output:
�����☺ ��☻ ��♠��� ♂�|
�O-�4 �� �� 0��☺@��(P��☺��♥p�� ���▬���♥��� ����☼-�t����☼������∟�↔ /nds/saves/white2.sav EOF
Shouldn't it create a file somewhere?
You need to pipe the output somewhere. If you're on *nix/mac, you can use:
python poke_redirect.py "pokear" "IRDO" "/nds/saves/white2.sav" > code.ips
IRDO is the English version.
Here is a table of the language associated with each game id: | Language | White 2 game id | Black 2 game id |
---|---|---|---|
German | IRDD | IRED | |
French | IRDF | IREF | |
Italian | IRDI | IREI | |
Japanese | IRDJ | IREJ | |
Korean | IRDK | IREK | |
English | IRDO | IREO | |
Spanish | IRDS | IRES |
In the armips branch, you can change that at the end of this line. In the future it should be configurable with a default instead of hardcoded.
Running the poke_redirect.py
script as you described generates the patch file. However, when I try to apply it to the game, it crashes before loading the 3DS logo.
What is curious is that I installed a .cia of the European Dream Radar and repeated the steps above to create the patch from the radar.s
file, and it worked pretty well, as it also works with the latest release version of the .ips file.
That makes me think that there may be some small differences between the JP and EUR version that should be considered.
Otherwise, the only address that may still be wrong in the radar.s
file is the one after the end of .text
, I am using 0x1bdec4
.
Anyway, it is also fine if it doesn't work with the Japanese version, I could use the other version for transferring the Pokémon instead.
Sorry to hear it's still not working.
I'm skeptical it would be handling save writing differently, which makes me think the address used might be incorrect.
I modified the script to adjust how it searches for the nds write save function. If you're still interested in getting this to work, here's the updated version (and if not, no worries!):
# Prints addresses for the Pokemon Radar Cart Redirect patch
# @category 3ds
#
from array import array
from ghidra.app.decompiler import DecompInterface
# ----------------------------------------
# Globals
# ----------------------------------------
memory = currentProgram.getMemory()
decomp = DecompInterface()
decomp.openProgram(currentProgram)
# ----------------------------------------
# Utils
# ----------------------------------------
def bytesToByteStr(bytes):
encoded_bytes = map(lambda byte : chr(byte), bytes)
return ''.join(encoded_bytes)
def bytesToByteArr(bytes):
return array('b', bytesToByteStr(bytes))
def findBytes(start_addr, byte_arr):
return memory.findBytes(start_addr, byte_arr, None, True, monitor)
def findFirstByteArray(bytes):
byte_arr = bytesToByteArr(bytes)
current_addr = findBytes(toAddr(0), byte_arr)
while current_addr.getOffset() % 4 != 0:
current_addr = findBytes(current_addr.add(4), byte_arr)
return current_addr
def findFirstByteString(byte_str):
return findFirstByteArray(array('B', byte_str.decode('hex')))
def findFirstUint(num):
num_bytes = [num & 0xff, (num >> 8) & 0xff, (num >> 16) & 0xff, (num >> 24) & 0xff]
return findFirstByteArray(num_bytes)
def findFirstFunctionUsingUint(num):
num_addr = findFirstUint(num)
num_ref = getReferencesTo(num_addr)[0]
num_use = num_ref.getFromAddress()
func = getFunctionContaining(num_use)
return func.getEntryPoint()
def findFirstFunctionWithBytes(byte_str):
bytes_addr = findFirstByteString(byte_str)
func = getFunctionContaining(bytes_addr)
return func.getEntryPoint()
def getFirstCallingFunction(func_addr):
callers_refs = getReferencesTo(func_addr)
first_caller_addr = callers_refs[0].getFromAddress()
return getFunctionContaining(first_caller_addr).getEntryPoint()
def findFirstString(find_str):
find_bytes = array('b', find_str)
return findBytes(toAddr(0), find_bytes)
def findNextFunctionCallRef(addr):
while addr < currentProgram.getMaxAddress():
refs = getReferencesFrom(addr)
for ref in refs:
if ref.getReferenceType().isCall():
return ref
addr = addr.add(4)
return None
def getCallArgs(caller_func, callee_addr):
decompiled_func = decomp.decompileFunction(caller_func, 60, monitor)
high_func = decompiled_func.getHighFunction()
if not high_func:
return []
opiter = high_func.getPcodeOps()
while opiter.hasNext():
op = opiter.next()
mnemonic = str(op.getMnemonic())
if mnemonic == 'CALL':
inputs = op.getInputs()
addr = inputs[0].getAddress()
if addr == callee_addr:
return inputs[1:]
return []
def getCallArgsFromRef(call_ref):
from_addr = call_ref.getFromAddress()
to_addr = call_ref.getToAddress()
func = getFunctionContaining(from_addr)
return getCallArgs(func, to_addr)
# ----------------------------------------
# Find radar addresses
# ----------------------------------------
# Get base addresses for most functions
fsuser_openfiledirectly = findFirstFunctionUsingUint(0x8030204)
fsfile_read = findFirstFunctionUsingUint(0x80200C2)
fsfile_write = findFirstFunctionUsingUint(0x8030102)
fsfile_close = findFirstFunctionUsingUint(0x8080000)
fsuser_cardslotisinserted = findFirstFunctionUsingUint(0x8210000)
fsuser_getlegacyromheader2 = findFirstFunctionUsingUint(0x8460102)
read_nds_save = findFirstFunctionWithBytes('600095e860a08de260008ae8105082e25810cde5')
write_nds_save = findFirstFunctionWithBytes('0400dce50430a0e10520a0e1080050e3')
# Get wrapper and calling function addresses
read_nds_save_wrapper = getFirstCallingFunction(read_nds_save)
fsuser_cardslotisinserted_wrapper = getFirstCallingFunction(fsuser_cardslotisinserted)
fsuser_getlegacyromheader2_wrapper = getFirstCallingFunction(fsuser_getlegacyromheader2)
read_nds_cart_id = getFirstCallingFunction(fsuser_getlegacyromheader2_wrapper)
# Find fs handle
fs_user_str = findFirstString('fs:USER')
# First ref is typically strlen
# Second ref should be with GetServiceHandleDirect
fs_user_ref = getReferencesTo(fs_user_str)[1].getFromAddress()
get_fs_handle_call = findNextFunctionCallRef(fs_user_ref)
get_fs_handle_args = getCallArgsFromRef(get_fs_handle_call)
fs_handle_ref_offset = get_fs_handle_args[0].getAddress()
fs_handle = memory.getInt(fs_handle_ref_offset)
# Print results
print('fsuser_openfiledirectly {}'.format(fsuser_openfiledirectly))
print('fsfile_read {}'.format(fsfile_read))
print('fsfile_write {}'.format(fsfile_write))
print('fsfile_close {}'.format(fsfile_close))
print('fsuser_cardslotisinserted_wrapper {}'.format(fsuser_cardslotisinserted_wrapper))
print('read_nds_save_wrapper {}'.format(read_nds_save_wrapper))
print('read_nds_cart_id {}'.format(read_nds_cart_id))
print('write_nds_save {}'.format(write_nds_save))
print('fs_handle {:x}'.format(fs_handle))
It worked! With the new write save address it transfers the Pokemon from the JP Dream Radar. That's awesome, thanks a lot!
Awesome! I'm glad it worked!
Did you use the armips branch or the python script?
Would you be able to post the offsets here or make a pull request with the offsets? I'd love to make sure other people can use this too.
Did you use the armips branch or the python script?
I used the armips branch.
Would you be able to post the offsets here or make a pull request with the offsets?
Sure, here is the radar.s
file that I used to create the patch:
.3ds
.open "radar.bin", "radar_patched.bin", 0x100000
fs_handle equ 0x1d9a1c
FSUSER_OpenFileDirectly equ 0x124d88
FSFILE_ReadFile equ 0x138dd8
FSFILE_WriteFile equ 0x138e5c
FSFILE_Close equ 0x138e30
.org 0x137b24
check_if_cart_is_inserted:
b check_if_save_exists
.org 0x13d7ec
read_save:
b read_save_from_sd
.org 0x13da18
.include "read-cart-id.s"
.org 0x187ebc
write_save:
b write_save_to_sd
.org 0x1bdec4
.include "sd-save-utils.s"
.align 4
check_if_save_exists:
stmdb sp!, { r1, r2, lr }
stmdb sp!, { r0 } ; push extra stack location to store file handle
mov r1, sp ; set r1 to sp to store the file handle
bl open_sd_save ; store openSDSaveFile result in r0
movs r2, r0, lsr #0x1f ; get the most significant bit of the result - 0 if success, 1 if error
eor r2, r2, #0x1 ; flip the bit so true if success, false if error
mov r0, sp
bl FSFILE_Close ; FSFILE_Close is located at 0x1390b0
ldmia sp!, { r0 } ; pop extra stack location
mov r0, r2 ; set r0 to result
ldmia sp!, { r1, r2, pc }
.close
Just one more question. My White 2 ROM in the Twilight Menu++ has a different name, that is quite long.
For transferring data between the game and the Dream Radar, I simply make a copy of the save file and rename it to white2.sav.
However, then after I transfer the data, I need to rename it again to the ROM name in order to use the Unova Link.
So, for saving time I was thinking about generating the patch with the original name of the ROM.
I simply run the command armips radar.s -strequ SD_SAVE_PATH "/roms/nds/saves/Pokemon White 2 (Experience + Trade Evolution Patched) (NoSSL).sav" -strequ GAME_ID "IRDO"
instead of the traditional armips radar.s -strequ SD_SAVE_PATH "/roms/nds/saves/white2.sav" -strequ GAME_ID "IRDO"
and keep all the other files unchanged. The patch is created as expected, but then when I try to boot the game, it just crashes. Do you think that it may be due to the length of the name or is it because it contains spaces?
Hello,
Thanks for your work, that is exactly what I was looking for.
Following the instructions here, I was able to make it work for the Poke Transporter.
However, for the Dream Radar, it does not work. Actually, I have the Dream Radar downloaded from the Japanese eshop, and the Title ID is 0004000000073200, and not 00040000000AE100 as in that post. So, when I tried to add the ips code to that folder, it crashes the console.
My question is: is there a way to modify the IPS patch so that it will also work with the Japanese title?
Thank you.