Beispiel #1
0
 def patch_pebble_lib(src, dest):
     # We take advantage of a fortuitous nop at the end of this method to insert another LDR command
     # Thus adding another layer of indirection, such that we only need to swap the table address out with the address of the main app's placeholder, not the table itself
     pre  = "03 A3 18 68 08 44 02 68 94 46 0F BC 60 47 00 BF A8 A8 A8 A8"
     post = "03 A3 18 68 00 68 08 44 02 68 94 46 0F BC 60 47 A8 A8 A8 A8"
     pre, post = (unhexify(item) for item in (pre, post))
     bin_contents = open(src, "rb").read()
     bin_contents = check_replace(bin_contents, pre, post)
     open(dest, "wb").write(bin_contents)
Beispiel #2
0
 def _inspect_called_syscall_indices(self):
     # Unlike _inspect_mod_proxied_syscalls, we can't just dump the symbols from the stripped binary
     # Instead, we scan for the syscall stubs in assembly
     # Which are of form...
     # 00000234 <window_stack_get_top_window>:
     #  234:   b40f        push    {r0, r1, r2, r3}
     #  236:   4901        ldr r1, [pc, #4]    ; (23c <window_stack_get_top_window+0x8>)
     #         @ The header for this opcode is just 0b11110 - so we don't attempt to match against it
     #  238:   f7ff bf3c   b.w b4 <jump_to_pbl_function>
     #  23c:   00000470    .word   0x00000470 @ This is the syscall index
     # It's not super critical if we "discover" some non-existent syscalls, but we should never under-report calls
     syscall_stub_pattern = unhexify("0fb4 0149")
     called_syscall_indices = set()
     # Can't use regex with a file stream :(
     self._bin_file.seek(0)
     bin_contents = self._bin_file.read()
     for stub_match in re.finditer(syscall_stub_pattern, bin_contents):
         called_syscall_idx = self._read_value_at_offset(stub_match.end() + 4, "<L")[0]
         called_syscall_indices.add(called_syscall_idx)
     return called_syscall_indices
Beispiel #3
0
    def patch(self, mod_sources, new_uuid=None, new_app_type=None, enable_js=None, cflags=None):
        # Make sure the platform binaries are ready to go
        if not self._platform.patched:
            self._platform.patch(scratch_dir=self._scratch_dir)

        # The following functions are stolen from SDK:
        def get_virtual_size(elf_file):
            readelf_bss_process=subprocess.Popen([SDK.arm_tool("readelf"), "-S", elf_file], stdout=subprocess.PIPE)
            readelf_bss_output=readelf_bss_process.communicate()[0].decode("utf-8")
            last_section_end_addr=0
            for line in readelf_bss_output.splitlines():
                if len(line)<10:
                    continue
                line=line[6:]
                columns=line.split()
                if len(columns)<6:
                    continue
                if columns[0]=='.bss':
                    addr=int(columns[2],16)
                    size=int(columns[4],16)
                    last_section_end_addr=addr+size
                elif columns[0]=='.data'and last_section_end_addr==0:
                    addr=int(columns[2],16)
                    size=int(columns[4],16)
                    last_section_end_addr=addr+size
            if last_section_end_addr!=0:
                return last_section_end_addr
            raise Exception("Failed to parse ELF sections while calculating the virtual size", readelf_bss_output)
        def get_relocate_entries(elf_file):
            entries=[]
            readelf_relocs_process=subprocess.Popen([SDK.arm_tool("readelf"),'-r',elf_file],stdout=subprocess.PIPE)
            readelf_relocs_output=readelf_relocs_process.communicate()[0].decode("utf-8")
            lines=readelf_relocs_output.splitlines()
            i=0
            reading_section=False
            while i<len(lines):
                if not reading_section:
                    if lines[i].startswith("Relocation section '.rel.data"):
                        reading_section=True
                        i+=1
                else:
                    if len(lines[i])==0:
                        reading_section=False
                    else:
                        entries.append(int(lines[i].split(' ')[0],16))
                i+=1
            readelf_relocs_process=subprocess.Popen([SDK.arm_tool("readelf"),'--sections',elf_file],stdout=subprocess.PIPE)
            readelf_relocs_output=readelf_relocs_process.communicate()[0].decode("utf-8")
            lines=readelf_relocs_output.splitlines()
            for line in lines:
                if'.got'in line and'.got.plt'not in line:
                    words=line.split(' ')
                    while''in words:
                        words.remove('')
                    section_label_idx=words.index('.got')
                    addr=int(words[section_label_idx+2],16)
                    length=int(words[section_label_idx+4],16)
                    for i in range(addr,addr+length,4):
                        entries.append(i)
                    break
            return entries
        def get_symbol_addr(nm_output,symbol):
            for sym in nm_output:
                if symbol==sym[-1]and len(sym)==3:
                    return int(sym[0],16)
            raise Exception("Could not locate symbol <%s> in binary! Failed to inject app metadata"%(symbol))

        # Make sure this really is a Pebble binary, or at least claims to be
        self._verify_header()

        # Figure out the end of the .data+.text section (immediately before relocs) in the main app
        load_size = self._read_value_at_offset(LOAD_SIZE_ADDR, "<H")[0]
        # ...and the end of .data+.text+.bss (which includes the relocation table, which we will relocate to the end of the binary)
        virtual_size = self._read_value_at_offset(VIRTUAL_SIZE_ADDR, "<H")[0]
        main_entrypoint = self._read_value_at_offset(OFFSET_ADDR, "<L")[0]
        jump_table = self._read_value_at_offset(JUMP_TABLE_ADDR, "<L")[0]
        logger.info("Main binary:\n\tLoad size\t%x\n\tVirt size\t%x\n\tEntry pt\t%x\n\tJump tbl\t%x", load_size, virtual_size, main_entrypoint, jump_table)

        # We rewrite the UUID, etc. here since, if the patch binary is empty, we'll bail quite soon after this point
        self._update_header_extraneous_metadata(new_uuid=new_uuid, new_app_type=new_app_type, enable_js=enable_js)

        # Prep the mods by compiling the user code so we can see which functions they wish to patch (proxied_syscalls)
        mod_user_object_path = os.path.join(self._scratch_dir, "mods_user.o")
        self._compile_mod_user_object(mod_sources, mod_user_object_path, cflags=cflags)
        proxied_syscalls = self._inspect_mod_proxied_syscalls(mod_user_object_path)
        if not proxied_syscalls:
            logger.warning("Patch binary exports no __patch methods - nothing to do")
            return

        # We know which syscalls they want to patch - but which does the app actually use?
        # (we only want to include the intersection, for obvious reasons)
        called_syscall_indices = self._inspect_called_syscall_indices()
        applicable_proxied_syscalls = {}
        for method_name, method_idx in proxied_syscalls.items():
            if method_idx in called_syscall_indices:
                applicable_proxied_syscalls[method_name] = method_idx
            else:
                logger.debug("Discarding %s (%d) - not called by main app" % (method_name, method_idx))

        if not applicable_proxied_syscalls:
            logger.warning("All __patch functions in patch binary discarded - nothing to do")
            return

        # Now that we know which syscalls they will end up patching (applicable_proxied_syscalls), generate the assembly used to redirect those calls
        proxy_asm_path = os.path.join(self._scratch_dir, "mods_proxy.s")
        open(proxy_asm_path, "w").write(self._generate_proxy_asm(applicable_proxied_syscalls))

        # It's quite difficult to tell if a given symbol was discarded by the linker, so we add some additional defines to the user code so it can know if the app uses a given SDK call
        cflags = (cflags if cflags else []) + ["-D%s_CALLED" % method_name.upper() for method_name in applicable_proxied_syscalls.keys()]

        # Compile the final binary once, since we need to know its dimensions to set the BSS section correctly the second time around
        mod_link_sources = [mod_user_object_path, proxy_asm_path]
        mods_final_intermediate_path = os.path.join(self._scratch_dir, "mods_final.o")
        mods_final_path = os.path.join(self._scratch_dir, "mods_final.bin")
        self._compile_mod_bin(mod_link_sources, mods_final_intermediate_path, mods_final_path, app_addr=0x00, bss_addr=0x00, bss_section="APP", cflags=cflags)

        # Then, compile it again with the BSS set to the end of the virtual_size (i.e. the eventual end of the main app's bss), now that we know it
        # This is a bit sketch since, in order to know the final virtual_size, we need to know the size of the mod's code and BSS
        # ...which requires compiling it
        # ...so I hope the size doesn't somehow change when we move the BSS (it shouldn't, it looks like all BSS stuff is ending up in the GOT)
        # We also need some word-alignment padding to make things work properly in ARM-land
        mod_true_load_size = os.stat(mods_final_path).st_size # Without the padding
        mod_pre_pad = 2
        mod_post_pad = (4 - (mod_true_load_size + mod_pre_pad) % 4) if (mod_true_load_size + mod_pre_pad) % 4 != 0 else 0
        mod_load_size = mod_true_load_size + mod_pre_pad + mod_post_pad # With the padding, which we actually insert at a later point
        mod_virtual_size = get_virtual_size(mods_final_intermediate_path) + mod_pre_pad + mod_post_pad
        logger.info("Patch binary:\n\tLoad size\t%x\n\tVirt size\t%x\n\tPrec Pad\t%x\n\tPost pad\t%x", mod_load_size, mod_virtual_size, mod_pre_pad, mod_post_pad)
        self._compile_mod_bin(mod_link_sources, mods_final_intermediate_path, mods_final_path, app_addr=STRUCT_SIZE_BYTES + mod_pre_pad, bss_addr=virtual_size + mod_load_size, cflags=cflags)
        # Load it in again, and check that the size didn't change on us
        mod_binary = open(mods_final_path, "rb").read()
        assert len(mod_binary) == mod_true_load_size, "Mod binary size changed after relocating BSS/APP sections"
        mod_binary = b'\0' * mod_pre_pad + mod_binary + b'\0' * mod_post_pad

        # Update their relocation table's entries and targets by the amount we're about to insert between the header and the main app
        self._offset_main_relocation_table(table_location=load_size, offset=mod_load_size)

        # Now that it's updated, grab the code and the relocation table separately as we're soon to overwrite both
        self._bin_file.seek(STRUCT_SIZE_BYTES)
        main_binary = self._bin_file.read(load_size - STRUCT_SIZE_BYTES)
        main_reloc_table = self._bin_file.read()

        # Find jump_to_pbl_function in the main app - this is what we modify to redirect the app's syscalls
        jump_to_pbl_function_signature = unhexify("03 A3 18 68 08 44 02 68 94 46 0F BC 60 47 00 BF A8 A8 A8 A8")
        jump_to_pbl_function_addr = main_binary.index(jump_to_pbl_function_signature)
        # Replace it with something that still reads the offset table addr (we want that), but immediately branches to the the address we specify (our jump_to_pbl_function__proxy)
        mod_binary_nm_output = self._get_nm_output(mods_final_intermediate_path)
        mod_syscall_proxy_addr = get_symbol_addr(mod_binary_nm_output, "jump_to_pbl_function__proxy")
        mod_syscall_proxy_jmp_addr = mod_syscall_proxy_addr + 1 # +1 to indicate THUMB 16-bit instruction
        replacement_fcn = unhexify("03 A3 18 68 00 4A 10 47") + struct.pack("<L", mod_syscall_proxy_jmp_addr) + unhexify("00 BF 00 BF A8 A8 A8 A8")
        main_binary = check_replace(main_binary, jump_to_pbl_function_signature, replacement_fcn)
        logger.info("Patching main binary jump routine at %x to use proxy at %x", jump_to_pbl_function_addr, mod_syscall_proxy_addr)

        # That's half of the job done - the app's syscalls now get sent to the mod - but where do the mod's syscalls go?
        # We need to update the mod's binary with the (eventual) address of the main app's jump table address placeholder
        # (in platforms.py we patched libpebble to follow this address to find the correct syscall destination)
        mod_jump_table_ptr_addr = None
        try:
            mod_jump_table_ptr_addr = mod_binary.index(unhexify("a8a8a8a8"))
        except ValueError:
            logger.info("Patch binary does not make any SDK calls, no need to patch its jump indirection value")
        else:
            relocated_main_jump_table = jump_table + mod_load_size
            logger.info("Writing patch binary's jump indirection value at %x to %x", mod_jump_table_ptr_addr, relocated_main_jump_table)
            mod_binary = check_replace(mod_binary, unhexify("a8a8a8a8"), struct.pack("<L", relocated_main_jump_table))


        # Now we can rewrite the entire binary from scratch (ish)
        self._bin_file.seek(STRUCT_SIZE_BYTES)
        # First, insert the mod binary
        self._bin_file.write(mod_binary)
        # Then their binary and relocation table
        self._bin_file.write(main_binary)
        self._bin_file.write(main_reloc_table)
        # And the mod's relocation table
        mod_reloc_entries = get_relocate_entries(mods_final_intermediate_path)
        logger.info("Appending %d relocation entries from patch binary", len(mod_reloc_entries))
        for entry in mod_reloc_entries:
            self._bin_file.write(struct.pack('<L',entry))

        # And finally, some predefined relocation entries for our proxy infrastructure
        # (we're adding STRUCT_SIZE_BYTES by hand since our mod binary doesn't include the header struct, while the final binary does)
        infr_reloc_entries = []
        infr_reloc_entries.append(STRUCT_SIZE_BYTES + mod_load_size + jump_to_pbl_function_addr + 8) # For their jump to our proxy
        if mod_jump_table_ptr_addr:
            infr_reloc_entries.append(STRUCT_SIZE_BYTES + mod_jump_table_ptr_addr) # For our jump table indirection ptr thing
        logger.debug("Appending %d infrastructure relocation entries" % len(infr_reloc_entries))
        for entry in infr_reloc_entries:
            self._bin_file.write(struct.pack('<L',entry))

        # Make sure we're not breaking the rules
        if self._bin_file.tell() > self._platform.max_binary_size:
            raise SizeLimitExceededError("Binary exceeds maximum size of %d bytes, is %d bytes" % (self._platform.max_binary_size, self._bin_file.tell()))

        # Update the executable header to reflect our changes
        final_entrypoint = main_entrypoint + mod_load_size
        final_jump_table = jump_table + mod_load_size
        final_virtual_size = virtual_size + mod_virtual_size
        final_load_size = load_size + mod_load_size
        logger.info("Final binary:\n\tLoad size\t%x\n\tVirt size\t%x\n\tEntry pt\t%x\n\tJump tbl\t%x", final_load_size, final_virtual_size, final_entrypoint, final_jump_table)
        if final_virtual_size > self._platform.max_memory_size:
            raise SizeLimitExceededError("App exceeds memory limit of %d bytes, is %d bytes" % (self._platform.max_memory_size, final_virtual_size))

        main_reloc_table_size = self._read_value_at_offset(NUM_RELOC_ENTRIES_ADDR, "<L")[0]
        final_crc = crc32(mod_binary + main_binary)
        logger.debug("Final CRC: %d" % final_crc)

        self._write_value_at_offset(CRC_ADDR, "<L", final_crc)
        self._write_value_at_offset(NUM_RELOC_ENTRIES_ADDR, "<L", main_reloc_table_size + len(mod_reloc_entries) + len(infr_reloc_entries))
        self._write_value_at_offset(OFFSET_ADDR, "<L", final_entrypoint)
        self._write_value_at_offset(VIRTUAL_SIZE_ADDR, "<H", final_virtual_size)
        self._write_value_at_offset(LOAD_SIZE_ADDR, "<H", final_load_size)
        self._write_value_at_offset(JUMP_TABLE_ADDR, "<L", final_jump_table)

        assert mod_syscall_proxy_addr % 4 == 0, "Mod code not word-aligned, falls at %x" % (mod_syscall_proxy_addr + STRUCT_SIZE_BYTES)