├── README.md └── objc-stub-fixer.py /README.md: -------------------------------------------------------------------------------- 1 | ### Hopper Objective-C Method Stub Fixer 2 | 3 | #### Description 4 | 5 | This script for Hopper Disassembler identifies and renames Objective-C method stubs using their selector names. 6 | 7 | 8 | ![Before_and_After_Comparison](https://github.com/EthanArbuckle/Hopper-Objective-C-Stub-Fixer/assets/4250718/ca44cca4-2cef-459a-9e8a-66a89b68d95f) 9 | 10 | 11 | https://github.com/EthanArbuckle/Hopper-Objective-C-Stub-Fixer/assets/4250718/cf5fae93-9525-4ca0-8c74-6ac435d36a7e 12 | 13 | -------------------------------------------------------------------------------- /objc-stub-fixer.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | def read_c_string(doc: Document, address: int) -> str: 5 | """ 6 | Read a null-terminated C string from the document at the given address. 7 | """ 8 | result = bytearray() 9 | while True: 10 | byte = doc.readByte(address) 11 | if byte == 0: 12 | break 13 | result.append(byte) 14 | address += 1 15 | return result.decode() 16 | 17 | 18 | def is_objc_stub(doc: Document, address: int) -> bool: 19 | """ 20 | Check if the function at the given address is an Objective-C stub. 21 | """ 22 | seg = doc.getCurrentSegment() 23 | instructions = [seg.getInstructionAtAddress(address + i * 4) for i in range(5)] 24 | if any(inst is None for inst in instructions): 25 | return False 26 | 27 | instruction_strings = [inst.getInstructionString() for inst in instructions] 28 | expected_instructions = ["adrp", "ldr", "adrp", "ldr", "br"] 29 | return all(inst.startswith(expected) for inst, expected in zip(instruction_strings, expected_instructions)) 30 | 31 | 32 | def parse_offset(ldr_inst_offset_str: str) -> Optional[int]: 33 | """ 34 | Parse the offset from the ldr instruction string. 35 | """ 36 | try: 37 | parts = ldr_inst_offset_str.split("[")[1].split("]")[0].split(",") 38 | offset = int(parts[1].strip()[1:], 16) 39 | return offset 40 | except (IndexError, ValueError) as e: 41 | print(f"Error parsing offset: {e}") 42 | return None 43 | 44 | 45 | def get_page_address(adrp_inst: Instruction) -> Optional[int]: 46 | """ 47 | Get the page address from the adrp instruction. 48 | """ 49 | try: 50 | page_address = int(adrp_inst.getFormattedArgument(1)[1:], 16) 51 | return page_address 52 | except ValueError as e: 53 | print(f"Error parsing page address: {e}") 54 | return None 55 | 56 | 57 | def get_selector(doc: Document, address: int) -> Optional[str]: 58 | """ 59 | Extract the Objective-C selector from a stub function. 60 | """ 61 | seg = doc.getCurrentSegment() 62 | adrp_inst = seg.getInstructionAtAddress(address) 63 | ldr_inst = seg.getInstructionAtAddress(address + 4) 64 | 65 | if not adrp_inst or not ldr_inst: 66 | print(f"Error: Couldn't read instructions at 0x{address:X} for selector extraction") 67 | return None 68 | 69 | page = get_page_address(adrp_inst) 70 | offset = parse_offset(str(ldr_inst.getFormattedArgument(1))) 71 | 72 | if page is None or offset is None: 73 | return None 74 | 75 | selector_addr = page + offset 76 | selector_ptr = doc.readUInt64LE(selector_addr) 77 | if selector_ptr: 78 | return read_c_string(doc, selector_ptr) 79 | 80 | return None 81 | 82 | 83 | def rename_objc_stubs() -> None: 84 | """ 85 | Main function to rename Objective-C stub functions in the current document. 86 | """ 87 | doc = Document.getCurrentDocument() 88 | seg = doc.getSegmentByName("__TEXT") 89 | if not seg: 90 | print("Error: __TEXT segment not found") 91 | return 92 | 93 | renamed_functions = 0 94 | for procedure_address in seg.getNamedAddresses(): 95 | procedure_name = doc.getNameAtAddress(procedure_address) 96 | if not procedure_name.startswith("sub_"): 97 | continue 98 | 99 | try: 100 | if not is_objc_stub(doc, procedure_address): 101 | continue 102 | 103 | selector = get_selector(doc, procedure_address) 104 | if not selector: 105 | print(f"Error: Failed to extract selector from stub at 0x{procedure_address:X} ({procedure_name})") 106 | continue 107 | except Exception as exc: 108 | print(f"Failed to analyze function 0x{procedure_address:X} ({procedure_name}): {exc}") 109 | continue 110 | 111 | renamed_functions += 1 112 | doc.setNameAtAddress(procedure_address, selector) 113 | 114 | print(f"Renamed {renamed_functions} stubs") 115 | 116 | 117 | if __name__ == "__main__": 118 | rename_objc_stubs() 119 | --------------------------------------------------------------------------------