In our initial approach, we were manually booting all stages from the debugger, and jumping directly back to the debugger (without using the USB protocol). This was done to keep the debugger alive throughout the boot chain, but this became very difficult after BL31, as the MMU didn't allow us to read/write to most spaces, cutting off our access to BL31 during boot at some point. And losing our debugger interface in the process.
This is also why there is some ghost code within the project. We were trying to keep the debugger alive throughout the boot chain, but this was not easily feasible.
Get the correct payloads for the bootROM stages from samsung firmware files, or from `Exynos8890 usbdl-recovery images/firmwares <https://github.com/ananjaser1211/exynos8890-exynos-usbdl-recovery>`_.
`Gupje <https://git.herreweb.nl/EljakimHerrewijnen/Samsung_S7>`_ is the debugger we'll be loading onto the device and will be moving around throughout the bootchain.
Gupje needs to be built and loaded onto the device. Throughout the exploit, we've been moving the debugger to different spaces in memory on the device. This was necessary as the space we initially used, was in the way of the space used for BL31 or BL2. After BL31, we lost access to the debugger, as BL2 was overwriting our last available space. At some point we moved the debugger to 0x11200000, as we saw that this space was used by BL31. Here below the most important arguments to build the debugger. This space is executable when the MMU is off.
After initial exploitation the goal was to fully boot the device. We're now moving into the next phase: 1) loading a debugger 2) and then to continue booting the device. If we manage to keep access to the debugger throughout the boot, this gives us room to exploit the device. But, the debugger needs to be kept 'alive' through the boot chain. The main difficulties here are the location of the debugger in memory (as it gets overwritten) and the MMU being enabled after BL31.
After a cache flush, the debugger seems to be cleared as well, so the debugger is relocated to ``0x20c0000``, with _stack and _storage now at ``0x20c2000`` and ``0x20c4000`` respectively. This is done by running:
We searched for quite some time for a space which was both writeable and executable. After BL31, most space became unreachable, with the MMU not allowing read/write at most spaces. We tried putting the debugger in the GPU cache, and tried some other spaces visible in the dtsi files, but eventually we found a space at ``0x11200000``. This space is executable when the MMU is off. With the MMU on, we can read/write but not execute here.
Frederic created a payload called 'Exynos8890dump_bootrom', which used the usb dwc3 protocol (USB Synopsys DesignWare USB 3.0), to read and dump the bootrom. This payload was slightly modified, to keep the USB connection alive (stage1.bin). Frederic's C code was implemented in python.
res = libusb1.libusb_bulk_transfer(self.handle._USBDeviceHandle__handle, ENDPOINT_BULK_OUT, rop_chain, len(rop_chain), ctypes.byref(transferred), 0)
assert res == 0, "Error sending ROP chain"
return
After this exploitation, we're able to send custom payloads. The first payload that is sent, sets up the debugger. In order to run the debugger, a small amount of the bootROM was reversed in order to implement send/recv functionality.
@EljakimHerrewijnen: what send/recv did you reverse? What code from the bootROM did you reverse?
- Overwrite the USB return address pointer (`0x02020f60`) to jump back to the debugger. ``self.cd.memwrite_region(0x02020f60, p64(DEBUGGER_ADDR))``
- Set link register to debugger and jump into the boot USB (`0x000064e0`) function. ``self.cd.arch_dbg.state.LR = DEBUGGER_ADDR`` and then ``self.cd.restore_stack_and_jump(0x000064e0)``
- Now we can send the BL1 binary to the device. ``self.send_normal_stage(open("../S7/g930f_latest/g930f_sboot.bin.1.bin", "rb").read())``. At this point, we retain access to the debugger.
- To patch the authentication, we set X0 and X1 to 1, then again set the link register to the debugger, and jump into the authentication function at ``0x00012848``. ``self.cd.arch_dbg.state.X0 = 1`` and ``self.cd.arch_dbg.state.X1 = 1`` and ``self.cd.arch_dbg.state.LR = lr`` and then ``self.cd.restore_stack_and_jump(0x00012848)``
- We flush the cache ``self.usb_write(b"FLSH")`` (Frederic did this as well).
- Now we hijack the USB download function to jump back to the debugger. ``self.cd.memwrite_region(0x020200dc, p32(DEBUGGER_ADDR))`` and ``self.cd.memwrite_region(0x02021880, self.cd.arch_dbg.sc.branch_absolute(DEBUGGER_ADDR, branch_ins="br"))``
- And finally, we again restore our link register to the debugger, and jump into BL1 ``self.cd.restore_stack_and_jump(0x000002c0)``
At this point, after loading and executing BL1, the device returns to the debugger. Normally, the device would boot into BL1, and would then wait for the next boot stage to be sent over USB. But because we hijacked the USB return address pointer, the device returns to the debugger.
Regarding auth_bl1: Initially we thought that 0x0 indicated a verified boot state (as is plausible when reading the decompiled code in Ghidra). But after modifying BL1 in the header and contents, this value did not change.
Next up is BL31, which is loaded by BL1. BL31 is written at ``0x02024000`` with the entry point at ``0x02024010``, it ends at ``0x02048000``. ``BL31`` is the secure monitor. The monitor uses memory that is also being used by the debugger, so we will have to relocate it to keep code exeuction.
BL31 also configures the VBAR_EL3 and MMU so the memory mapping will probably change after this stage (preparation for trustzone?). Here we decided to move the debugger to 0x02048000, as this space is still accessible after BL31, but this space will get overwritten by BL2.
At this point we switched our approach to booting the device, as we were unable to keep the debugger alive throughout the boot chain. We now boot the device normally, and then try to get our debugger after booting each stage. Because of this, we didn't need to modify a lot after BL1. Essentially all we did was:
This boot process restores us to the debugger, but after this, we're unable to access most memory spaces. Notably, we tried getting access to the TTBR0_EL3. But something prohibits us reading there. We weren't able to find any executable space that was still available, for after BL2. BL2 would overwrite our debugger at this point.
While looking for flags which were used for the MMU, we found a function at ``0x020244e8`` which we were able to turn off, but which still allowed a full boot into recovery. The MMU however stated to being disabled.
Additionally we found a space at ``0x11207010``, while looking for bit flags in ghidra, which seemed to be a memory read/write space. This space was not executable, unless the MMU was turned off. We used this space to store our debugger, then before booting BL31, we patched the if-statement above to disable the MMU. And booted.
To keep access to the debugger, but allow modifications, we need to again load the BL33, but not boot it. We need to set some registers to accomplish this. We continue our bootflow to allow the device to load BL33, and then after it is done loading, let it jump back to the debugger:
- Store BL33 address in X0: ``self.cd.arch_dbg.state.X0 = BL33_ptr``
- Store BL33 link register in LR: ``self.cd.arch_dbg.state.LR = BL33_LR``
- Set LR to debugger: ``self.cd.arch_dbg.state.LR = DEBUGGER_ADDR``
- Jump into the function that loads BL33: ``self.cd.restore_stack_and_jump(hijacked_fun)``
Now we can again load our next boot stage (BL33), send it, and verify a return to the debugger.
At this point, we tried patching something in memory at BL33 - like we did at BL31. But this always failed. After consultation, it seemed most likely that something was checking the integrity of the next boot stages. When we modified and reverted our changes, the boot was proper. The link register to continue our boot flow was at ``0x2024eec``. We did not succeed to manually do a verification, and continue the boot flow. This function at 0x2024eec does also not return, but instead continues onwards.
..figure:: images/_integrity_check_BL2-BL33.png
:align:center
Possible integrity check of boot stages at BL2 and BL33.
The decompilation is a bit broken, but we noticed that there are multiple calls to the same function, not just at the location where BL33 was returning from. With most specific things related to BL33 already done before this function. A similar verification seemed to have been done at ``0x02024e5c``. At this address, the same function was executed as at ``0x02024eec``, so instead of jumpingo into the function at ``0x02024eec``, we jumped into the function at ``0x02024e5c``. This worked, and allowed us to patch BL33, while continuing our boot flow. I assume, that we're doing an integrity check over BL2, while booting BL33.
There are some differences between the Head Unit and the phone (obviously), with the MIB3 boot stages being generally quite a bit larger. For instance, BL1 (or the second part of BL1) is 8.0Kb on the Samsung S7, but 28 Kb on the MIB3. As a result, the BL31 starts at ``0x020c0000`` on the MIB3, but at ``0x02024000`` on the Samsung S7. This also means that some pointers are at different addresses. On the S7, we overwrite a pointer at ``0x02021880`` on the MIB3, this pointer is at ``0x02021890``. Similarly, the pointer at BL31 changes because the BL31 starts at a different address. Mitigated most changes, except for the ``Authentication??`` part in BL31 (when booting from BL2 into BL33, something is done in BL31 to either verify or do something.)