Add IOS CA loading patch

This commit is contained in:
Spotlight 2021-12-31 01:16:11 -06:00
parent 75c8a4d4cf
commit c597e8f17d
No known key found for this signature in database
GPG Key ID: 874AA355B3209BDC
5 changed files with 241 additions and 6 deletions

107
docs/patch_custom_ca_ios.md Normal file
View File

@ -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.

14
main.go
View File

@ -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

View File

@ -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))
}

104
patch_custom_ca.go Normal file
View File

@ -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(),
},
}
}

View File

@ -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)
}