├── .gitignore ├── LICENSE.txt ├── README.md ├── nxo64-ida.py ├── nxo64 ├── __init__.py ├── compat.py ├── consts.py ├── files.py ├── memory │ ├── __init__.py │ └── builder.py ├── nxo_exceptions.py ├── symbols.py └── utils.py ├── pyproject.toml └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .venv/ 3 | .venv2/ 4 | .idea/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 Reswitched Team 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nxo64 2 | ======== 3 | 4 | nxo64 is a python (2 & 3) library for reading NSO / NRO files and an IDAPython loader. 5 | 6 | Installation 7 | ============ 8 | 9 | Install the modules from `requirements.txt` so IDAPython can import them. 10 | 11 | Copy `nxo64-ida.py` and `nxo64` into IDA's `loaders` directory. 12 | 13 | Credits 14 | ======= 15 | 16 | I want to thank the following people for their help and/or other involvement with this or the original project: 17 | 18 | - [@ReSwitched](https://github.com/reswitched) for creating [loaders](https://github.com/reswitched/loaders). 19 | - [@SciresM](https://github.com/SciresM) for sharing the fixes for `kip1_blz_decompress()`. 20 | - [@hthh](https://github.com/hthh) for creating [switch-reversing](https://github.com/hthh/switch-reversing), which contains a modified version of `nxo64.py`. Some modifications are integrated into this repo. 21 | -------------------------------------------------------------------------------- /nxo64-ida.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Reswitched Team 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any purpose with or 4 | # without fee is hereby granted, provided that the above copyright notice and this permission 5 | # notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS 8 | # SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL 9 | # THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 10 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF 11 | # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE 12 | # OR PERFORMANCE OF THIS SOFTWARE. 13 | 14 | # nxo64.py: IDA loader (and library for reading nso/nro files) 15 | 16 | from __future__ import print_function 17 | 18 | from nxo64.compat import * 19 | from nxo64.consts import * 20 | 21 | from nxo64.files import load_nxo 22 | from nxo64.memory import SegmentKind 23 | 24 | try: 25 | import idaapi 26 | import idc 27 | except ImportError: 28 | pass 29 | else: 30 | # IDA specific code 31 | def accept_file(li, n): 32 | print('accept_file') 33 | if not isinstance(n, int_types) or n == 0: 34 | li.seek(0) 35 | if li.read(4) == b'NSO0': 36 | return 'nxo.py: Switch binary (NSO)' 37 | li.seek(0) 38 | if li.read(4) == b'KIP1': 39 | return 'nxo.py: Switch binary (KIP)' 40 | li.seek(0x10) 41 | if li.read(4) == b'NRO0': 42 | return 'nxo.py: Switch binary (NRO)' 43 | return 0 44 | 45 | def ida_make_offset(f, ea): 46 | if f.armv7: 47 | idaapi.create_data(ea, idc.FF_DWORD, 4, idaapi.BADADDR) 48 | else: 49 | idaapi.create_data(ea, idc.FF_QWORD, 8, idaapi.BADADDR) 50 | idc.op_plain_offset(ea, 0, 0) 51 | 52 | def find_bl_targets(text_start, text_end): 53 | targets = set() 54 | for pc in range(text_start, text_end, 4): 55 | d = idc.get_wide_dword(pc) 56 | if (d & 0xfc000000) == 0x94000000: 57 | imm = d & 0x3ffffff 58 | if imm & 0x2000000: 59 | imm |= ~0x1ffffff 60 | if 0 <= imm <= 2: 61 | continue 62 | target = pc + imm * 4 63 | if text_start <= target < text_end: 64 | targets.add(target) 65 | return targets 66 | 67 | def load_file(li, neflags, format): 68 | idaapi.set_processor_type("arm", idaapi.SETPROC_LOADER_NON_FATAL | idaapi.SETPROC_LOADER) 69 | f = load_nxo(li) 70 | if f.armv7: 71 | idc.set_inf_attr(idc.INF_LFLAGS, idc.get_inf_attr(idc.INF_LFLAGS) | idc.LFLG_PC_FLAT) 72 | else: 73 | idc.set_inf_attr(idc.INF_LFLAGS, idc.get_inf_attr(idc.INF_LFLAGS) | idc.LFLG_64BIT) 74 | 75 | idc.set_inf_attr(idc.INF_DEMNAMES, idaapi.DEMNAM_GCC3) 76 | idaapi.set_compiler_id(idaapi.COMP_GNU) 77 | idaapi.add_til('gnulnx_arm' if f.armv7 else 'gnulnx_arm64', 1) 78 | 79 | loadbase = 0x60000000 if f.armv7 else 0x7100000000 80 | 81 | f.binfile.seek(0) 82 | as_string = f.binfile.read(f.bssoff) 83 | idaapi.mem2base(as_string, loadbase) 84 | if f.text[1] is not None: 85 | li.file2base(f.text[1], loadbase + f.text[2], loadbase + f.text[2] + f.text[3], True) 86 | if f.ro[1] is not None: 87 | li.file2base(f.ro[1], loadbase + f.ro[2], loadbase + f.ro[2] + f.ro[3], True) 88 | if f.data[1] is not None: 89 | li.file2base(f.data[1], loadbase + f.data[2], loadbase + f.data[2] + f.data[3], True) 90 | 91 | for start, end, name, kind in f.sections: 92 | if name.startswith('.got'): 93 | kind = SegmentKind.CONST 94 | idaapi.add_segm(0, loadbase+start, loadbase+end, name, kind) 95 | segm = idaapi.get_segm_by_name(name) 96 | if kind == SegmentKind.CONST: 97 | segm.perm = idaapi.SEGPERM_READ 98 | elif kind == SegmentKind.CODE: 99 | segm.perm = idaapi.SEGPERM_READ | idaapi.SEGPERM_EXEC 100 | elif kind == SegmentKind.DATA: 101 | segm.perm = idaapi.SEGPERM_READ | idaapi.SEGPERM_WRITE 102 | elif kind == SegmentKind.BSS: 103 | segm.perm = idaapi.SEGPERM_READ | idaapi.SEGPERM_WRITE 104 | idaapi.update_segm(segm) 105 | idaapi.set_segm_addressing(segm, 1 if f.armv7 else 2) 106 | 107 | # do imports 108 | # TODO: can we make imports show up in "Imports" window? 109 | undef_count = 0 110 | for s in f.symbols: 111 | if not s.shndx and s.name: 112 | undef_count += 1 113 | last_ea = max(loadbase + end for start, end, name, kind in f.sections) 114 | undef_entry_size = 8 115 | undef_ea = ((last_ea + 0xFFF) & ~0xFFF) + undef_entry_size # plus 8 so we don't end up on the "end" symbol 116 | idaapi.add_segm(0, undef_ea, undef_ea+undef_count*undef_entry_size, "UNDEF", "XTRN") 117 | segm = idaapi.get_segm_by_name("UNDEF") 118 | segm.type = idaapi.SEG_XTRN 119 | idaapi.update_segm(segm) 120 | for i, s in enumerate(f.symbols): 121 | if not s.shndx and s.name: 122 | idaapi.create_data(undef_ea, idc.FF_QWORD, 8, idaapi.BADADDR) 123 | idaapi.force_name(undef_ea, s.name) 124 | s.resolved = undef_ea 125 | undef_ea += undef_entry_size 126 | elif i != 0: 127 | assert s.shndx 128 | s.resolved = loadbase + s.value 129 | if s.name: 130 | if s.type == STT.FUNC: 131 | print(hex(s.resolved), s.name) 132 | idaapi.add_entry(s.resolved, s.resolved, s.name, 0) 133 | else: 134 | idaapi.force_name(s.resolved, s.name) 135 | 136 | else: 137 | # NULL symbol 138 | s.resolved = 0 139 | 140 | funcs = set() 141 | for s in f.symbols: 142 | if s.name and s.shndx and s.value: 143 | if s.type == STT.FUNC: 144 | funcs.add(loadbase+s.value) 145 | 146 | got_name_lookup = {} 147 | for offset, r_type, sym, addend in f.relocations: 148 | target = offset + loadbase 149 | if r_type in (R_Arm.GLOB_DAT, R_Arm.JUMP_SLOT, R_Arm.ABS32): 150 | if not sym: 151 | print('error: relocation at %X failed' % target) 152 | else: 153 | idaapi.put_dword(target, sym.resolved) 154 | elif r_type == R_Arm.RELATIVE: 155 | idaapi.put_dword(target, idaapi.get_dword(target) + loadbase) 156 | elif r_type in (R_AArch64.GLOB_DAT, R_AArch64.JUMP_SLOT, R_AArch64.ABS64): 157 | idaapi.put_qword(target, sym.resolved + addend) 158 | if addend == 0: 159 | got_name_lookup[offset] = sym.name 160 | elif r_type == R_AArch64.RELATIVE: 161 | idaapi.put_qword(target, loadbase + addend) 162 | if addend < f.textsize: 163 | funcs.add(loadbase + addend) 164 | elif r_type == R_FAKE_RELR: 165 | assert not f.armv7 # TODO 166 | addend = idaapi.get_qword(target) 167 | idaapi.put_qword(target, addend + loadbase) 168 | if addend < f.textsize: 169 | funcs.add(loadbase + addend) 170 | else: 171 | print('TODO r_type %d' % (r_type,)) 172 | ida_make_offset(f, target) 173 | 174 | for func, target in f.plt_entries: 175 | if target in got_name_lookup: 176 | addr = loadbase + func 177 | funcs.add(addr) 178 | idaapi.force_name(addr, got_name_lookup[target]) 179 | 180 | funcs |= find_bl_targets(loadbase, loadbase+f.textsize) 181 | 182 | for addr in sorted(funcs, reverse=True): 183 | idc.AutoMark(addr, idc.AU_CODE) 184 | idc.AutoMark(addr, idc.AU_PROC) 185 | 186 | return 1 187 | -------------------------------------------------------------------------------- /nxo64/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TSRBerry/nxo64/096cbb12452e97e70fa8d4565964376582bb19be/nxo64/__init__.py -------------------------------------------------------------------------------- /nxo64/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] > 2: 4 | iter_range = range 5 | int_types = (int,) 6 | ascii_string = lambda b: b.decode('ascii') 7 | bytes_to_list = lambda b: list(b) 8 | list_to_bytes = lambda l: bytes(l) 9 | get_ord = lambda b: b 10 | else: 11 | iter_range = xrange 12 | int_types = (int, long) 13 | ascii_string = lambda b: str(b) 14 | bytes_to_list = lambda b: map(ord, b) 15 | list_to_bytes = lambda l: ''.join(map(chr, l)) 16 | get_ord = lambda b: ord(b) 17 | -------------------------------------------------------------------------------- /nxo64/consts.py: -------------------------------------------------------------------------------- 1 | try: 2 | from enum import IntEnum 3 | except ImportError: 4 | from aenum import IntEnum 5 | 6 | from .compat import iter_range 7 | 8 | 9 | class STB(IntEnum): 10 | LOCAL = 0 11 | GLOBAL = 1 12 | WEAK = 2 13 | 14 | 15 | class STT(IntEnum): 16 | NOTYPE = 0 17 | OBJECT = 1 18 | FUNC = 2 19 | SECTION = 3 20 | 21 | 22 | R_FAKE_RELR = -1 23 | 24 | 25 | class R_AArch64(IntEnum): 26 | ABS64 = 257 27 | GLOB_DAT = 1025 28 | JUMP_SLOT = 1026 29 | RELATIVE = 1027 30 | TLSDESC = 1031 31 | 32 | 33 | class R_Arm(IntEnum): 34 | ABS32 = 2 35 | TLS_DESC = 13 36 | GLOB_DAT = 21 37 | JUMP_SLOT = 22 38 | RELATIVE = 23 39 | 40 | 41 | class DT(IntEnum): 42 | (NULL, NEEDED, PLTRELSZ, PLTGOT, HASH, STRTAB, SYMTAB, RELA, RELASZ, 43 | RELAENT, STRSZ, SYMENT, INIT, FINI, SONAME, RPATH, SYMBOLIC, REL, 44 | RELSZ, RELENT, PLTREL, DEBUG, TEXTREL, JMPREL, BIND_NOW, INIT_ARRAY, 45 | FINI_ARRAY, INIT_ARRAYSZ, FINI_ARRAYSZ, RUNPATH, FLAGS) = iter_range(31) 46 | 47 | RELRSZ = 0x23 48 | RELR = 0x24 49 | RELRENT = 0x25 50 | 51 | GNU_HASH = 0x6ffffef5 52 | VERSYM = 0x6ffffff0 53 | RELACOUNT = 0x6ffffff9 54 | RELCOUNT = 0x6ffffffa 55 | FLAGS_1 = 0x6ffffffb 56 | VERDEF = 0x6ffffffc 57 | VERDEFNUM = 0x6ffffffd 58 | 59 | 60 | MULTIPLE_DTS = {DT.NEEDED} 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /nxo64/files.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import re 4 | import struct 5 | from io import BytesIO 6 | 7 | try: 8 | from enum import IntFlag 9 | except ImportError: 10 | from aenum import IntFlag 11 | 12 | from lz4.block import decompress as uncompress 13 | 14 | from .memory import SegmentKind 15 | from .memory.builder import SegmentBuilder 16 | from .compat import iter_range, ascii_string 17 | from .consts import MULTIPLE_DTS, DT, R_AArch64, R_Arm, R_FAKE_RELR 18 | from .nxo_exceptions import NxoException 19 | from .symbols import ElfSym 20 | from .utils import kip1_blz_decompress 21 | 22 | 23 | class NxoFlags(IntFlag): 24 | TEXT_COMPRESSED = 1 25 | RO_COMPRESSED = 2 26 | DATA_COMPRESSED = 4 27 | TEXT_HASH = 8 28 | RO_HASH = 16 29 | DATA_HASH = 32 30 | 31 | 32 | def load_nxo(fileobj): 33 | """ 34 | :type fileobj: io.BytesIO | io.BinaryIO 35 | :rtype: NsoFile | NroFile | KipFile 36 | """ 37 | fileobj.seek(0) 38 | header = fileobj.read(0x14) 39 | 40 | if header[:4] == b'NSO0': 41 | return NsoFile(fileobj) 42 | elif header[0x10:0x14] == b'NRO0': 43 | return NroFile(fileobj) 44 | elif header[:4] == b'KIP1': 45 | return KipFile(fileobj) 46 | else: 47 | raise NxoException("not an NRO or NSO or KIP file") 48 | 49 | 50 | def get_file_size(f): 51 | """ 52 | :type f: io.BytesIO | BinFile 53 | :rtype: int 54 | """ 55 | if isinstance(f, BinFile): 56 | return f.size() 57 | else: 58 | ptell = f.tell() 59 | f.seek(0, 2) 60 | filesize = f.tell() 61 | f.seek(ptell) 62 | return filesize 63 | 64 | 65 | class BinFile(object): 66 | def __init__(self, li): 67 | """ 68 | :type li: io.BytesIO 69 | """ 70 | self._f = li 71 | 72 | def read(self, arg=None): 73 | """ 74 | :type arg: str | int | None 75 | :rtype: bytes | tuple[Any, ...] 76 | """ 77 | if isinstance(arg, str): 78 | fmt = '<' + arg 79 | size = struct.calcsize(fmt) 80 | raw = self._f.read(size) 81 | out = struct.unpack(fmt, raw) 82 | if len(out) == 1: 83 | return out[0] 84 | return out 85 | elif arg is None: 86 | return self._f.read() 87 | else: 88 | out = self._f.read(arg) 89 | return out 90 | 91 | def read_to_end(self): 92 | """ 93 | :rtype: bytes | tuple[Any, ...] 94 | """ 95 | return self.read(self.size() - self.tell()) 96 | 97 | def size(self): 98 | """ 99 | :rtype: int 100 | """ 101 | return get_file_size(self._f) 102 | 103 | def read_from(self, arg, offset): 104 | """ 105 | :param arg: str | int | None 106 | :param offset: int 107 | """ 108 | old = self.tell() 109 | try: 110 | self.seek(offset) 111 | out = self.read(arg) 112 | finally: 113 | self.seek(old) 114 | return out 115 | 116 | def seek(self, off): 117 | """ 118 | :type off: int 119 | """ 120 | self._f.seek(off) 121 | 122 | def skip(self, dist): 123 | self.seek(self.tell() + dist) 124 | 125 | def close(self): 126 | self._f.close() 127 | 128 | def tell(self): 129 | """ 130 | :rtype: int 131 | """ 132 | return self._f.tell() 133 | 134 | 135 | class NxoFileBase(object): 136 | # segment = (content, file offset, vaddr, vsize) 137 | def __init__(self, text, ro, data, bsssize): 138 | """ 139 | :type text: tuple[bytes, int, int, int] 140 | :type ro: tuple[bytes, int, int, int] 141 | :type data: tuple[bytes, int, int, int] 142 | :type bsssize: int 143 | """ 144 | self.text = text 145 | self.ro = ro 146 | self.data = data 147 | self.bsssize = bsssize 148 | self.textoff = text[2] 149 | self.textsize = text[3] 150 | self.rodataoff = ro[2] 151 | self.rodatasize = ro[3] 152 | self.dataoff = data[2] 153 | flatsize = data[2] + data[3] 154 | 155 | full = text[0] 156 | if ro[2] >= len(full): 157 | full += b'\x00' * (ro[2] - len(full)) 158 | else: 159 | print('truncating .text?') 160 | full = full[:ro[2]] 161 | full += ro[0] 162 | if data[2] > len(full): 163 | full += b'\x00' * (data[2] - len(full)) 164 | else: 165 | print('truncating .rodata?') 166 | full += data[0] 167 | f = BinFile(BytesIO(full)) 168 | 169 | self.binfile = f 170 | 171 | # read MOD 172 | self.modoff = f.read_from('I', 4) 173 | 174 | f.seek(self.modoff) 175 | if f.read('4s') != b'MOD0': 176 | raise NxoException('invalid MOD0 magic') 177 | 178 | self.dynamicoff = self.modoff + f.read('i') 179 | self.bssoff = self.modoff + f.read('i') 180 | self.bssend = self.modoff + f.read('i') 181 | self.unwindoff = self.modoff + f.read('i') 182 | self.unwindend = self.modoff + f.read('i') 183 | self.moduleoff = self.modoff + f.read('i') 184 | 185 | self.datasize = self.bssoff - self.dataoff 186 | self.bsssize = self.bssend - self.bssoff 187 | 188 | self.isLibnx = False 189 | if f.read('4s') == b'LNY0': 190 | self.isLibnx = True 191 | self.libnx_got_start = self.modoff + f.read('i') 192 | self.libnx_got_end = self.modoff + f.read('i') 193 | 194 | self.segment_builder = builder = SegmentBuilder() 195 | for off, sz, name, kind in [ 196 | (self.textoff, self.textsize, ".text", SegmentKind.CODE), 197 | (self.rodataoff, self.rodatasize, ".rodata", SegmentKind.CONST), 198 | (self.dataoff, self.datasize, ".data", SegmentKind.DATA), 199 | (self.bssoff, self.bsssize, ".bss", SegmentKind.BSS), 200 | ]: 201 | builder.add_segment(off, sz, name, kind) 202 | 203 | # read dynamic 204 | self.armv7 = (f.read_from('Q', self.dynamicoff) > 0xFFFFFFFF 205 | or f.read_from('Q', self.dynamicoff + 0x10) > 0xFFFFFFFF) 206 | self.offsize = 4 if self.armv7 else 8 207 | 208 | f.seek(self.dynamicoff) 209 | self.dynamic = dynamic = {} 210 | for i in MULTIPLE_DTS: 211 | dynamic[i] = [] 212 | for _ in iter_range((flatsize - self.dynamicoff) // 0x10): 213 | tag, val = f.read('II' if self.armv7 else 'QQ') 214 | if tag == DT.NULL: 215 | break 216 | if tag in MULTIPLE_DTS: 217 | dynamic[tag].append(val) 218 | else: 219 | dynamic[tag] = val 220 | self.dynamicsize = f.tell() - self.dynamicoff 221 | builder.add_section('.dynamic', self.dynamicoff, end=self.dynamicoff + self.dynamicsize) 222 | builder.add_section('.eh_frame_hdr', self.unwindoff, end=self.unwindend) 223 | 224 | # read .dynstr 225 | if DT.STRTAB in dynamic and DT.STRSZ in dynamic: 226 | f.seek(dynamic[DT.STRTAB]) 227 | self.dynstr = f.read(dynamic[DT.STRSZ]) 228 | else: 229 | self.dynstr = b'\x00' 230 | print('warning: no dynstr') 231 | 232 | for startkey, szkey, name in [ 233 | (DT.STRTAB, DT.STRSZ, '.dynstr'), 234 | (DT.INIT_ARRAY, DT.INIT_ARRAYSZ, '.init_array'), 235 | (DT.FINI_ARRAY, DT.FINI_ARRAYSZ, '.fini_array'), 236 | (DT.RELA, DT.RELASZ, '.rela.dyn'), 237 | (DT.REL, DT.RELSZ, '.rel.dyn'), 238 | (DT.RELR, DT.RELRSZ, '.relr.dyn'), 239 | (DT.JMPREL, DT.PLTRELSZ, ('.rel.plt' if self.armv7 else '.rela.plt')), 240 | ]: 241 | if startkey in dynamic and szkey in dynamic: 242 | builder.add_section(name, dynamic[startkey], size=dynamic[szkey]) 243 | 244 | # TODO 245 | # build_id = content.find('\x04\x00\x00\x00\x14\x00\x00\x00\x03\x00\x00\x00GNU\x00') 246 | # if build_id >= 0: 247 | # builder.add_section('.note.gnu.build-id', build_id, size=0x24) 248 | # else: 249 | # build_id = content.index('\x04\x00\x00\x00\x10\x00\x00\x00\x03\x00\x00\x00GNU\x00') 250 | # if build_id >= 0: 251 | # builder.add_section('.note.gnu.build-id', build_id, size=0x20) 252 | 253 | if DT.HASH in dynamic: 254 | hash_start = dynamic[DT.HASH] 255 | f.seek(hash_start) 256 | nbucket, nchain = f.read('II') 257 | f.skip(nbucket * 4) 258 | f.skip(nchain * 4) 259 | hash_end = f.tell() 260 | builder.add_section('.hash', hash_start, end=hash_end) 261 | 262 | if DT.GNU_HASH in dynamic: 263 | gnuhash_start = dynamic[DT.GNU_HASH] 264 | f.seek(gnuhash_start) 265 | nbuckets, symoffset, bloom_size, bloom_shift = f.read('IIII') 266 | f.skip(bloom_size * self.offsize) 267 | buckets = [f.read('I') for _ in range(nbuckets)] 268 | 269 | max_symix = max(buckets) if buckets else 0 270 | if max_symix >= symoffset: 271 | f.skip((max_symix - symoffset) * 4) 272 | while (f.read('I') & 1) == 0: 273 | pass 274 | gnuhash_end = f.tell() 275 | builder.add_section('.gnu.hash', gnuhash_start, end=gnuhash_end) 276 | 277 | self.needed = [self.get_dynstr(i) for i in self.dynamic[DT.NEEDED]] 278 | 279 | # load .dynsym 280 | self.symbols = symbols = [] 281 | if DT.SYMTAB in dynamic and DT.STRTAB in dynamic: 282 | f.seek(dynamic[DT.SYMTAB]) 283 | while True: 284 | if dynamic[DT.SYMTAB] < dynamic[DT.STRTAB] <= f.tell(): 285 | break 286 | if self.armv7: 287 | st_name, st_value, st_size, st_info, st_other, st_shndx = f.read('IIIBBH') 288 | else: 289 | st_name, st_info, st_other, st_shndx, st_value, st_size = f.read('IBBHQQ') 290 | if st_name > len(self.dynstr): 291 | break 292 | symbols.append(ElfSym(self.get_dynstr(st_name), st_info, st_other, st_shndx, st_value, st_size)) 293 | builder.add_section('.dynsym', dynamic[DT.SYMTAB], end=f.tell()) 294 | 295 | self.plt_entries = [] 296 | self.relocations = [] 297 | locations = set() 298 | plt_got_end = None 299 | if DT.REL in dynamic and DT.RELSZ in dynamic: 300 | locations |= self.process_relocations(f, symbols, dynamic[DT.REL], dynamic[DT.RELSZ]) 301 | 302 | if DT.RELA in dynamic and DT.RELASZ in dynamic: 303 | locations |= self.process_relocations(f, symbols, dynamic[DT.RELA], dynamic[DT.RELASZ]) 304 | 305 | if DT.RELR in dynamic: 306 | locations |= self.process_relocations_relr(f, dynamic[DT.RELR], dynamic[DT.RELRSZ]) 307 | 308 | if DT.JMPREL in dynamic and DT.PLTRELSZ in dynamic: 309 | pltlocations = self.process_relocations(f, symbols, dynamic[DT.JMPREL], dynamic[DT.PLTRELSZ]) 310 | locations |= pltlocations 311 | 312 | plt_got_start = min(pltlocations) 313 | plt_got_end = max(pltlocations) + self.offsize 314 | if DT.PLTGOT in dynamic: 315 | builder.add_section('.got.plt', dynamic[DT.PLTGOT], end=plt_got_end) 316 | 317 | if not self.armv7: 318 | f.seek(0) 319 | text = f.read(self.textsize) 320 | last = 12 321 | while True: 322 | pos = text.find(struct.pack('> 5) & 0x7ffff 333 | immlo = (a >> 29) & 3 334 | paddr = base + ((immlo << 12) | (immhi << 14)) 335 | poff = ((b >> 10) & 0xfff) << 3 336 | target = paddr + poff 337 | if plt_got_start <= target < plt_got_end: 338 | self.plt_entries.append((off, target)) 339 | if len(self.plt_entries) > 0: 340 | builder.add_section('.plt', min(self.plt_entries)[0], end=max(self.plt_entries)[0] + 0x10) 341 | 342 | if not self.isLibnx: 343 | # try to find the ".got" which should follow the ".got.plt" 344 | good = False 345 | got_start = (plt_got_end if plt_got_end is not None else self.dynamicoff + self.dynamicsize) 346 | got_end = self.offsize + got_start 347 | while (got_end in locations or (plt_got_end is None and got_end < dynamic[DT.INIT_ARRAY])) and ( 348 | DT.INIT_ARRAY not in dynamic or got_end < dynamic[DT.INIT_ARRAY] 349 | or dynamic[DT.INIT_ARRAY] < got_start): 350 | good = True 351 | got_end += self.offsize 352 | 353 | if good: 354 | self.got_start = got_start 355 | self.got_end = got_end 356 | builder.add_section('.got', self.got_start, end=self.got_end) 357 | else: 358 | builder.add_section('.got', self.libnx_got_start, end=self.libnx_got_end) 359 | 360 | self.eh_table = [] 361 | if not self.armv7: 362 | f.seek(self.unwindoff) 363 | version, eh_frame_ptr_enc, fde_count_enc, table_enc = f.read('BBBB') 364 | if not any(i == 0xff for i in (eh_frame_ptr_enc, fde_count_enc, table_enc)): # DW_EH_PE_omit 365 | # assert eh_frame_ptr_enc == 0x1B # DW_EH_PE_pcrel | DW_EH_PE_sdata4 366 | # assert fde_count_enc == 0x03 # DW_EH_PE_absptr | DW_EH_PE_udata4 367 | # assert table_enc == 0x3B # DW_EH_PE_datarel | DW_EH_PE_sdata4 368 | if eh_frame_ptr_enc == 0x1B and fde_count_enc == 0x03 and table_enc == 0x3B: 369 | base_offset = f.tell() 370 | eh_frame = base_offset + f.read('i') 371 | 372 | fde_count = f.read('I') 373 | # assert 8 * fde_count == self.unwindend - f.tell() 374 | if 8 * fde_count <= self.unwindend - f.tell(): 375 | for i in range(fde_count): 376 | pc = self.unwindoff + f.read('i') 377 | entry = self.unwindoff + f.read('i') 378 | self.eh_table.append((pc, entry)) 379 | 380 | # TODO: we miss the last one, but better than nothing 381 | last_entry = sorted(self.eh_table, key=lambda x: x[1])[-1][1] 382 | builder.add_section('.eh_frame', eh_frame, end=last_entry) 383 | 384 | self.sections = [] 385 | for start, end, name, kind in builder.flatten(): 386 | self.sections.append((start, end, name, kind)) 387 | 388 | def process_relocations(self, f, symbols, offset, size): 389 | """ 390 | :type f: BinFile 391 | :type symbols: list[ElfSym] 392 | :type offset: int 393 | :type size: int 394 | :rtype: set[int] 395 | """ 396 | locations = set() 397 | f.seek(offset) 398 | relocsize = 8 if self.armv7 else 0x18 399 | for _ in iter_range(size // relocsize): 400 | # NOTE: currently assumes all armv7 relocs have no addends, 401 | # and all 64-bit ones do. 402 | if self.armv7: 403 | offset, info = f.read('II') 404 | addend = None 405 | r_type = info & 0xff 406 | r_sym = info >> 8 407 | else: 408 | offset, info, addend = f.read('QQq') 409 | r_type = info & 0xffffffff 410 | r_sym = info >> 32 411 | 412 | sym = symbols[r_sym] if r_sym != 0 else None 413 | 414 | if r_type != R_AArch64.TLSDESC and r_type != R_Arm.TLS_DESC: 415 | locations.add(offset) 416 | self.relocations.append((offset, r_type, sym, addend)) 417 | return locations 418 | 419 | def process_relocations_relr(self, f, offset, size): 420 | locations = set() 421 | f.seek(offset) 422 | relocsize = 8 423 | for _ in iter_range(size // relocsize): 424 | entry = f.read('Q') 425 | if entry & 1: 426 | entry >>= 1 427 | i = 0 428 | while i < (relocsize * 8) - 1: 429 | if entry & (1 << i): 430 | locations.add(where + i * relocsize) 431 | self.relocations.append((where + i * relocsize, R_FAKE_RELR, None, 0)) 432 | i += 1 433 | where += relocsize * ((relocsize * 8) - 1) 434 | else: 435 | # Where 436 | where = entry 437 | locations.add(where) 438 | self.relocations.append((where, R_FAKE_RELR, None, 0)) 439 | where += relocsize 440 | return locations 441 | 442 | def get_dynstr(self, o): 443 | """ 444 | :type o: int 445 | """ 446 | return ascii_string(self.dynstr[o:self.dynstr.index(b'\x00', o)]) 447 | 448 | def get_path_or_name(self): 449 | """ 450 | :rtype: bytes | None 451 | """ 452 | path = None 453 | for off, end, name, class_ in self.sections: 454 | if name == '.rodata' and 0x1000 > end - off > 8: 455 | id_ = self.binfile.read_from(end - off, off).lstrip(b'\x00') 456 | if len(id_) > 0: 457 | length = struct.unpack_from('= self.start and other._inclend <= self._inclend 45 | 46 | def __repr__(self): 47 | """ 48 | :rtype: str 49 | """ 50 | return 'Range(0x%X -> 0x%X)' % (self.start, self.end) 51 | 52 | 53 | class Section(object): 54 | def __init__(self, r, name): 55 | """ 56 | :type r: Range 57 | :type name: str 58 | """ 59 | self.range = r 60 | self.name = name 61 | 62 | def __repr__(self): 63 | """ 64 | :rtype: str 65 | """ 66 | return 'Section(%r, %r)' % (self.range, self.name) 67 | 68 | 69 | class Segment(object): 70 | def __init__(self, r, name, kind): 71 | """ 72 | :type r: Range 73 | :type name: str 74 | :type kind: SegmentKind 75 | """ 76 | self.range = r 77 | self.name = name 78 | self.kind = kind 79 | self.sections = [] # type: list[Section] 80 | 81 | def add_section(self, s): 82 | """ 83 | :type s: Section 84 | """ 85 | for i in self.sections: 86 | assert not i.range.overlaps(s.range), '%r overlaps %r' % (s, i) 87 | self.sections.append(s) 88 | -------------------------------------------------------------------------------- /nxo64/memory/builder.py: -------------------------------------------------------------------------------- 1 | from . import Range, Section, Segment, SegmentKind 2 | from ..utils import suffixed_name 3 | 4 | 5 | class SegmentBuilder(object): 6 | def __init__(self): 7 | self.segments = [] # type: list[Segment] 8 | 9 | def add_segment(self, start, size, name, kind): 10 | """ 11 | :type start: int 12 | :type size: int 13 | :type name: str 14 | :type kind: SegmentKind 15 | """ 16 | r = Range(start, size) 17 | for i in self.segments: 18 | assert not r.overlaps(i.range) 19 | self.segments.append(Segment(r, name, kind)) 20 | 21 | def add_section(self, name, start, end=None, size=None): 22 | """ 23 | :type name: str 24 | :type start: int 25 | :type end: int 26 | :type size: int 27 | """ 28 | assert end is None or size is None 29 | if size == 0: 30 | return 31 | if size is None: 32 | size = end - start 33 | r = Range(start, size) 34 | for i in self.segments: 35 | if i.range.includes(r): 36 | i.add_section(Section(r, name)) 37 | return 38 | assert False, "no containing segment for %r" % (name,) 39 | 40 | def flatten(self): 41 | self.segments.sort(key=lambda s: s.range.start) 42 | parts = [] 43 | for segment in self.segments: 44 | suffix = 0 45 | segment.sections.sort(key=lambda s: s.range.start) 46 | pos = segment.range.start 47 | for section in segment.sections: 48 | if pos < section.range.start: 49 | parts.append((pos, section.range.start, suffixed_name(segment.name, suffix), segment.kind)) 50 | suffix += 1 51 | pos = section.range.start 52 | parts.append((section.range.start, section.range.end, section.name, segment.kind)) 53 | pos = section.range.end 54 | if pos < segment.range.end: 55 | parts.append((pos, segment.range.end, suffixed_name(segment.name, suffix), segment.kind)) 56 | suffix += 1 57 | pos = segment.range.end 58 | return parts 59 | -------------------------------------------------------------------------------- /nxo64/nxo_exceptions.py: -------------------------------------------------------------------------------- 1 | class NxoException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /nxo64/symbols.py: -------------------------------------------------------------------------------- 1 | class ElfSym(object): 2 | resolved = None 3 | 4 | def __init__(self, name, info, other, shndx, value, size): 5 | """ 6 | :type name: str 7 | :type info: int 8 | :type other: int 9 | :type shndx: int 10 | :type value: int 11 | :type size: int 12 | """ 13 | self.name = name 14 | self.shndx = shndx 15 | self.value = value 16 | self.size = size 17 | 18 | self.vis = other & 3 19 | self.type = info & 0xF 20 | self.bind = info >> 4 21 | 22 | def __repr__(self): 23 | return 'Sym(name=%r, shndx=0x%X, value=0x%X, size=0x%X, vis=%r, type=%r, bind=%r)' % ( 24 | self.name, self.shndx, self.value, self.size, self.vis, self.type, self.bind) 25 | -------------------------------------------------------------------------------- /nxo64/utils.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from .compat import bytes_to_list, iter_range, list_to_bytes 4 | 5 | 6 | def kip1_blz_decompress(compressed): 7 | """ 8 | :type compressed: bytearray 9 | """ 10 | compressed_size, init_index, uncompressed_addl_size = struct.unpack(' 0: 20 | cmp_ofs -= 1 21 | control = decompressed[cmp_start + cmp_ofs] 22 | for _ in iter_range(8): 23 | if control & 0x80: 24 | if cmp_ofs < 2 - cmp_start: 25 | raise ValueError('Compression out of bounds!') 26 | cmp_ofs -= 2 27 | segmentoffset = compressed[cmp_start + cmp_ofs] | (compressed[cmp_start + cmp_ofs + 1] << 8) 28 | segmentsize = ((segmentoffset >> 12) & 0xF) + 3 29 | segmentoffset &= 0x0FFF 30 | segmentoffset += 2 31 | if out_ofs < segmentsize - cmp_start: 32 | raise ValueError('Compression out of bounds!') 33 | for _ in iter_range(segmentsize): 34 | if out_ofs + segmentoffset >= decompressed_size: 35 | raise ValueError('Compression out of bounds!') 36 | data = decompressed[cmp_start + out_ofs + segmentoffset] 37 | out_ofs -= 1 38 | decompressed[cmp_start + out_ofs] = data 39 | else: 40 | if out_ofs < 1 - cmp_start: 41 | raise ValueError('Compression out of bounds!') 42 | out_ofs -= 1 43 | cmp_ofs -= 1 44 | decompressed[cmp_start + out_ofs] = decompressed[cmp_start + cmp_ofs] 45 | control <<= 1 46 | control &= 0xFF 47 | if not out_ofs: 48 | break 49 | return list_to_bytes(decompressed) 50 | 51 | 52 | def suffixed_name(name, suffix): 53 | """ 54 | :type name: str 55 | :type suffix: int 56 | :rtype: str 57 | """ 58 | if suffix == 0: 59 | return name 60 | return '%s.%d' % (name, suffix) 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "nxo64" 7 | version = "0.0.2" 8 | authors = [ 9 | { name = "ReSwitched Team" }, 10 | { name = "TSRBerry" }, 11 | ] 12 | description = "Library for reading NSO / NRO files" 13 | readme = "README.md" 14 | requires-python = ">=2.7" 15 | license = "ISC" 16 | dependencies = [ 17 | "lz4~=2.2.1; python_version < '3'", 18 | "lz4~=4.0.2; python_version >= '3'", 19 | "aenum~=3.1.15; python_version < '3.4'" 20 | ] 21 | classifiers = [ 22 | "Operating System :: OS Independent", 23 | ] 24 | 25 | [project.urls] 26 | Repository = "https://github.com/TSRBerry/nxo64" 27 | ReSwitched = "https://github.com/reswitched" 28 | 29 | [tool.hatch.build] 30 | only-packages = true 31 | include = [ 32 | "nxo64/", 33 | ] 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lz4~=2.2.1 2 | aenum~=3.1.15 --------------------------------------------------------------------------------