From c597e8f17ddba3f56189774785266181bd08067e Mon Sep 17 00:00:00 2001 From: Spotlight Date: Fri, 31 Dec 2021 01:16:11 -0600 Subject: [PATCH] Add IOS CA loading patch --- docs/patch_custom_ca_ios.md | 107 ++++++++++++++++++++++++++++++++++++ main.go | 14 ++++- modify_dol.go | 11 ++-- patch_custom_ca.go | 104 +++++++++++++++++++++++++++++++++++ powerpc.go | 11 ++++ 5 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 docs/patch_custom_ca_ios.md create mode 100644 patch_custom_ca.go diff --git a/docs/patch_custom_ca_ios.md b/docs/patch_custom_ca_ios.md new file mode 100644 index 0000000..181eb13 --- /dev/null +++ b/docs/patch_custom_ca_ios.md @@ -0,0 +1,107 @@ +# Patch: Load Custom CA within IOS + +## Motivation +When interacting with the EC library via JavaScript, +certain functions will remotely POST SOAP requests to the configured server. + +We already have a custom CA certificate loaded and trusted within Opera. +However, Opera utilizes its own SSL library with its own separate trust store as IOS only permits loading 3 certificate "slots" at a time for usage. +(Opera's credits cite it utilizing OpenSSL, however it is most likely RSA's [BSAFE](https://en.wikipedia.org/wiki/BSAFE) +as many strings match.) + +As EC utilizes NHTTP - utilizing IOS's SSL trust store - we need to additionally load our CA certificate into IOS. + +## Explanation +NHTTP is the name of Nintendo's first-party HTTP client on the Wii and other platforms of the time. +All SSL functionality present is via IOS's [`/dev/net/ssl`](https://wiibrew.org/wiki//dev/net/ssl). + +Via symbols within the main ARC, we are able to see what functions it has available. + +We are able to identify a function named `ipl::netconnect::NetConnect::connectThread` with two tasks: +initializing sockets (i.e. awaiting DHCP), and a second function named `NHTTPi_Startup`. + +Among other tasks, this NHTTP-specific startup function creates a separate thread to manage HTTP tasks. +This thread manages things such as sending headers in time, properly sending data, and keeping track of sockets requests. + +When a request is ready to connect to a socket, the aptly-named function `NHTTPi_SocConnect` is invoked. +It checks whether the current NHTTP object has SSL enabled, and if so, utilizes `NHTTPi_SocSSLConnect` going forward. + +As part of `/dev/net/ssl` as noted above, a few wrapping functions are available from Nintendo's first-party SSL SDK library. +These are utilized by `NHTTPi_SocSSLConnect`. + +The following things occur within: + - `SSLNew`, opening a new SSL socket and returning its file descriptor. + - The NHTTP object is checked on whether the built-in client certificate in IOS should be loaded. + - If so, `SSLSetBuiltinClientCert` is called, loading the certificate to a specified index. + - If not, and a client certificate is present, `SSLSetClientCert` is called, again loading to a specified index. + - The NHTTP object is checked on whether the built-in Nintendo CA certificate should be loaded. + - If so, `SSLSetBuiltinRootCA` is called, loading its certificate to a specified index. + - If not, and a CA certificate is present, `SSLSetRootCA` is called to load the certificate at a given buffer. + - `SSLConnect` is called. + - `SSLDoHandshake` is called, optionally throwing an error if present. + - The function returns, and HTTP request data is written. + +## Execution +We have no interest in ever utilizing the built-in root CA. We can edit out this logic, freeing room to only load our root certificate. + +First, we need to determine an appropriate place to put our certificate within the binary itself. +(It has been discussed to potentially utilize `ARCOpen` and similar functions, placing our certificate there. +However, this is tricky, and has not been attempted.) + +Within a data segment in our binary, we find 928 consecutive empty bytes of space, starting at `0x802e97b8`. + +> This appears to be utilized for a Unicode to Shift-JIS conversion table, utilized for the keyboard. +> +> It's unclear on why so much data is continuously empty - presumably this is an unallocated region in UTF-16. +> Should this ever cause a conflict with Japanese users entering text, we should switch to another method. +> However, this is unlikely, as Nintendo handles keyboard text as a wchar_t (UTF-16 BE, following Windows' sizing). + +Second, we need to modify the flow of checks for `SSLSetBuiltinRootCA`. + +The following pseudocode represents the original flow, starting at `0x800acad0`: +```c +int NHTTPi_SocSSLConnect(/* parameters */) { + // Assuming the NHTTP object type is named NHTTPInternals + NHTTPInternals* internals; + + if (internals->ca_cert == NULL) { + int result = SSLSetBuiltinRootCA(internals->ssl_fd, internals->ssl_index); + if (result != 0) { + return -1004; + } + } else { + int result = SSLSetRootCA(internals->ssl_fd, internals->ca_cert, internals->cert_length; + if (iVar4 != 0) { + return -1004; + } + } +} +``` + +We are able to remove the conditional branch entirely and directly call. Note that `{certLen}` is a placeholder value found by the patcher throughout operation. +```asm +; Our certificate is present at 0x802e97b8 as identified above. +; r4 is the second parameter of SSLSetRootCA, the ca_cert pointer. +lis r4, 0x802e +ori r4, r4, 0x97b8 + +; r5 is the third parameter of SSLSetRootCA, the cert_length field. +li r5, {certLen} + +; r3 is the first parameter of SSLSetRootCA, the ssl_fd. +; We load it exactly as Nintendo does. +lwz r3, 0xac(r28) + +; We then continue with logic as Nintendo does. +bl SSLSetRootCA + +; Error checking +cmpwi r3, 0x0 +beq CONTINUE_CONNECTING + +; It has failed. +li r3, 1004 +b FUNCTION_EPILOG +``` + +The remaining bytes after this function can simply be `nop`'d out, and original function flow continues. \ No newline at end of file diff --git a/main.go b/main.go index f729452..b0c7979 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ var mainDol []byte // mainArc holds the main ARC - our content at index 2. var mainArc *arclib.ARC + // filePresent returns whether the specified path is present on disk. func filePresent(path string) bool { _, err := os.Stat(path) @@ -77,7 +78,18 @@ func main() { // Determine whether a certificate authority was provided, or generated previously. if !filePresent("./output/root.cer") { log.Println("Generating root certificates...") - createCertificates() + rootCertificate = createCertificates() + } else { + rootCertificate, err = ioutil.ReadFile("./output/root.cer") + check(err) + } + + // Ensure the loaded certificate has a suitable length. + // It must not be longer than 928 bytes. + if len(rootCertificate) > 928 { + fmt.Println("The passed root certificate exceeds the maximum length possible, 928 bytes.") + fmt.Println("Please verify parameters passed for generation and reduce its size.") + os.Exit(-1) } // Load main DOL diff --git a/modify_dol.go b/modify_dol.go index 4e1766d..48b4702 100644 --- a/modify_dol.go +++ b/modify_dol.go @@ -71,12 +71,13 @@ func applyPatchSet(setName string, set PatchSet) { } } -// applyDefaultPatches iterates through a list of default patches. -func applyDefaultPatches() { - applyPatchSet("Overwrite IOS Syscall for ES", OverwriteIOSPatch) -} - // emptyBytes returns an empty byte array of the given length. func emptyBytes(length int) []byte { return bytes.Repeat([]byte{0x00}, length) } + +// applyDefaultPatches iterates through a list of default patches. +func applyDefaultPatches() { + applyPatchSet("Overwrite IOS Syscall for ES", OverwriteIOSPatch) + applyPatchSet("Load Custom CA within IOS", LoadCustomCA(rootCertificate)) +} diff --git a/patch_custom_ca.go b/patch_custom_ca.go new file mode 100644 index 0000000..e55b3c5 --- /dev/null +++ b/patch_custom_ca.go @@ -0,0 +1,104 @@ +package main + +// LoadCustomCA loads our custom certificate, either generated or loaded, +// into the IOS trust store for EC usage. +// It is assumed that rootCertificate has been loaded upon invoking this patchset. +// See docs/patch_custom_ca_ios.md for more information. +func LoadCustomCA(rootCert []byte) PatchSet { + return PatchSet{ + Patch{ + Name: "Insert custom CA into free space", + AtOffset: 3037368, + + Before: emptyBytes(len(rootCert)), + After: rootCert, + }, + Patch{ + Name: "Modify NHTTPi_SocSSLConnect to load cert", + AtOffset: 644624, + + Before: Instructions{ + // Check whether internals->ca_cert is null + LWZ(R4, 0xc0, R28), + // cmpwi r4, 0 + CMPWI(R4, 0), + + // If it is, load the built-in root certificate. + // beq LOAD_BUILTIN_ROOT_CA + Instruction{0x41, 0x82, 0x00, 0x20}, + + // --- + + // It seems we are loading a custom certificate. + // r3 -> ssl_fd + // r4 -> ca_cert, loaded previously + // r5 -> cert_length + LWZ(R3, 0xac, R28), + LWZ(R5, 0xc4, R28), + // SSLSetRootCA(ssl_fd, ca_cert, cert_index) + Instruction{0x48, 0x01, 0x59, 0x49}, + + // Check if successful + CMPWI(R3, 0), + // beq CONTINUE_CONNECTING + Instruction{0x41, 0x82, 0x00, 0x28}, + + // Return error -1004 if failed + LI(R3, 0xfc14), + // b FUNCTION_PROLOG + Instruction{0x48, 0x00, 0x00, 0xbc}, + + // ---- + + // It seems we are loading the built-in root CA. + // r3 -> ssl_fd + // r4 -> cert_length + LWZ(R3, 0xac, R28), + LWZ(R4, 0xd8, R28), + // SSLSetBuiltinRootCA(ssl_fd, cert_index) + Instruction{0x48, 0x01, 0x5a, 0x75}, + + // Check if successful + CMPWI(R3, 0), + // beq CONTINUE_CONNECTING + Instruction{0x41, 0x82, 0x00, 0x0c}, + + // Return error -1004 if failed + LI(R3, 0xfc14), + // b FUNCTION_PROLOG + Instruction{0x48, 0x00, 0x00, 0xa0}, + }.toBytes(), + After: Instructions{ + // Our certificate is present at 0x802e97b8. + // r4 is the second parameter of SSLSetRootCA, the ca_cert pointer. + LIS(R4, 0x802e), + ORI(R4, R4, 0x97b8), + + // r5 is the third parameter of SSLSetRootCA, the cert_length field. + // xor r5, r5, r5 + Instruction{0x7c, 0xa5, 0x2a, 0x78}, + ADDI(R5, R5, uint16(len(rootCert))), + + // r3 is the first parameter of SSLSetRootCA, the ssl_fd. + // We load it exactly as Nintendo does. + LWZ(R3, 0xac, R28), + + // SSLSetRootCA(ssl_fd, ca_cert, cert_index) + Instruction{0x48, 0x01, 0x59, 0x49}, + + // Check for errors + CMPWI(R3, 0), + // beq CONTINUE_CONNECTING + Instruction{0x41, 0x82, 0x00, 0x28}, + + // Return error -1004 if failed + LI(R3, 0xfc14), + // b FUNCTION_PROLOG + Instruction{0x48, 0x00, 0x00, 0xbc}, + + // NOP the rest in order to allow execution to continue. + NOP(), NOP(), NOP(), NOP(), NOP(), NOP(), NOP(), + }.toBytes(), + }, + } +} diff --git a/powerpc.go b/powerpc.go index 6136dfb..d520060 100644 --- a/powerpc.go +++ b/powerpc.go @@ -74,3 +74,14 @@ func STW(rS Register, offset uint16, rA Register) Instruction { func LWZ(rT Register, offset uint16, rA Register) Instruction { return EncodeInstrDForm(32, rT, rA, offset) } + +// NOP represents the nop mnemonic for PowerPC. +func NOP() Instruction { + return ORI(R0, R0, 0) +} + +// CMPWI represents the cmpwi mnemonic for PowerPC. +// It does not support any other CR fields asides from 0. +func CMPWI(rA Register, value uint16) Instruction { + return EncodeInstrDForm(11, 0, rA, value) +}