mirror of
https://wiilab.wiimart.org/wiimart/WiiMart-Patcher
synced 2025-09-04 04:21:19 +02:00
Add IOS syscall patch
This commit is contained in:
parent
f905d22563
commit
bd625c0779
@ -34,6 +34,6 @@ Throughout its operation, the patcher will perform the following:
|
|||||||
- If `output/root.cer` is not present, a 1024-bit (RSA), SHA-1 CA certificate will be generated.
|
- If `output/root.cer` is not present, a 1024-bit (RSA), SHA-1 CA certificate will be generated.
|
||||||
- At the same time, `*.<basedomain>` will be issued for ease of use. See `output/server.pem` and `output/server.key` for usage with nginx or similar servers.
|
- At the same time, `*.<basedomain>` will be issued for ease of use. See `output/server.pem` and `output/server.key` for usage with nginx or similar servers.
|
||||||
- Modifications are made to the application's main `.arc` (within content index 2) to permit Opera loading the base domain, and the customized certificates.
|
- Modifications are made to the application's main `.arc` (within content index 2) to permit Opera loading the base domain, and the customized certificates.
|
||||||
- Patches to the application's main dol are also performed. Please see `patches.go` for more information on what these contain.
|
- Patches to the application's main dol are also performed. Please see `docs/patch_<name>.md` for more information on what these contain.
|
||||||
- The patched WAD is written to disk.
|
- The patched WAD is written to disk.
|
||||||
|
|
@ -2,5 +2,6 @@
|
|||||||
This directory contains documentation about given patches applied.
|
This directory contains documentation about given patches applied.
|
||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
- `opcacrt6.yml`: A [Kaitai](https://kaitai.io) structure describing a very basic `opcacrt6.dat`.
|
- [[`opcacrt6.yml`]]: A [Kaitai](https://kaitai.io) structure describing a very basic `opcacrt6.dat`.
|
||||||
It does not attempt to handle things such as client certificates or user passwords.
|
It does not attempt to handle things such as client certificates or user passwords.
|
||||||
|
- [[`patch_overwrite_ec.md`]]: An explanation as to why IOS needs to be patched for our operations.
|
153
docs/patch_overwrite_ios.md
Normal file
153
docs/patch_overwrite_ios.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# Patch: Overwrite IOS Syscall for ES
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
When installing a custom title via the Wii Shop Channel, the signature expects a valid signature.
|
||||||
|
Most homebrew-based WAD installers apply IOS patches or manually insert contents to their proper place.
|
||||||
|
However, as we use Nintendo's official code, we lack this luxury.
|
||||||
|
|
||||||
|
When EC finishes downloading a title, it calls [ES](https://wiibrew.org/wiki//dev/es) within IOS to handle title installation.
|
||||||
|
Along the process, the WAD's certificate is verified, and content signatures are verified.
|
||||||
|
This is done via `IOSC_VerifyPublicKeySign`, an [IOS syscall](https://wiibrew.org/wiki/IOS/Syscalls) available for ES.
|
||||||
|
|
||||||
|
As we are not Nintendo and lack their private key, we cannot sign officially, and fail all checks.
|
||||||
|
We need to bypass this.
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
It was determined that the best solution was to overwrite `IOSC_VerifyPublicKeySign` itself.
|
||||||
|
|
||||||
|
The Wii Shop Channel uses IOS 56. Across a Wii and a vWii, analysis of the IOSP module (roughly its primary kernel)
|
||||||
|
shows that the `IOSC_VerifyPublicKeySign` syscall has its function present at `0x13a73ad4` within MEM2. (Dolphin silently ignores this write.)
|
||||||
|
|
||||||
|
The function - in ARM THUMB mode - looks similar to the following (as copied from Ghidra):
|
||||||
|
```
|
||||||
|
**************************************************************
|
||||||
|
* *
|
||||||
|
* FUNCTION *
|
||||||
|
**************************************************************
|
||||||
|
int __stdcall IOSC_VerifyPublicKeySign(uint8_t * inputDa
|
||||||
|
assume LRset = 0x0
|
||||||
|
assume TMode = 0x1
|
||||||
|
int r0:4 <RETURN>
|
||||||
|
uint8_t * r0:4 inputData
|
||||||
|
u32 r1:4 inputSize
|
||||||
|
int r2:4 publicHandle
|
||||||
|
uint8_t * r3:4 signData
|
||||||
|
undefined4 Stack[-0x24]:4 local_24 XREF[1]: 13a73b54(R)
|
||||||
|
undefined4 Stack[-0x28]:4 local_28 XREF[2]: 13a73ae4(W),
|
||||||
|
13a73b8a(R)
|
||||||
|
undefined4 Stack[-0x2c]:4 local_2c XREF[2]: 13a73b68(W),
|
||||||
|
13a73b8c(W)
|
||||||
|
IOSC_VerifyPublicKeySign+1 XREF[0,1]: ffff9580(*)
|
||||||
|
IOSC_VerifyPublicKeySign
|
||||||
|
13a73ad4 b5 f0 push { r4, r5, r6, r7, lr }
|
||||||
|
13a73ad6 46 57 mov r7, r10
|
||||||
|
13a73ad8 46 4e mov r6, r9
|
||||||
|
13a73ada 46 45 mov r5, r8
|
||||||
|
13a73adc b4 e0 push { r5, r6, r7 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Thankfully, IOS utilizes [TPCS](https://developer.arm.com/documentation/espc0002/1-0), the Thumb Procedure Call Standard.
|
||||||
|
This states that `r0` is utilized as the first return value, and Ghidra accurately identifies this.
|
||||||
|
|
||||||
|
Therefore, we are able to simply return `IOS_SUCCESS` (or 0) to negate the entire check:
|
||||||
|
```
|
||||||
|
IOSC_VerifyPublicKeySign
|
||||||
|
13a73ad4 20 00 mov r0, #0x0
|
||||||
|
13a73ad6 47 70 bx lr
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
This leaves us with the challenge of writing 4 bytes - `20 00 47 70` - to `0xd3a73ad4` as identified previously.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
Ideally, we would not rely on a hardcoded address for this patch, and would instead choose to iterate through memory.
|
||||||
|
However, space constraints for our patch made this difficult. We chose to hardcode the address.
|
||||||
|
(Further testing may result in this patch having a different method of application.)
|
||||||
|
|
||||||
|
By default, the PowerPC core (Broadway) has memory protections enabled, preventing from us editing IOS's memory in MEM2.
|
||||||
|
We need to apply several patches to achieve our goal.
|
||||||
|
|
||||||
|
1. e need to obtain access to overwriting IOS memory.
|
||||||
|
We set the Access Rights field in the WAD's Title Metadata (or `.tmd`) to 0x00000003, permitting memory access. We will use this with `MEM2_PROT` later.
|
||||||
|
|
||||||
|
This is thankfully a very quick fix.
|
||||||
|
|
||||||
|
2. We need to find space to put our own custom function within the binary.
|
||||||
|
|
||||||
|
Via symbols within the main ARC, we find a C++ class named `textinput::EventObserver` with 4 functions in a row that
|
||||||
|
immediately `blr` - returning with no other logic:
|
||||||
|
- At `0x80014420`, `textinput::EventObserver::onSE`
|
||||||
|
- At `0x80014430`, `textinput::EventObserver::onEvent`
|
||||||
|
- At `0x80014440`, `textinput::EventObserver::onCommand`
|
||||||
|
- At `0x80014450`, `textinput::EventObserver::onInput`
|
||||||
|
|
||||||
|
An unrelated function resides at `0x80014460`, so we cannot continue. However, we do not need to.
|
||||||
|
|
||||||
|
We consolidate these four functions into a single function at `0x80014420` - our own `textinput::EventObserver::doNothing`, if you will.
|
||||||
|
We must additionally update references to this single at two separate virtual tables:
|
||||||
|
- `textinput::EventObserver` at `0x802f7a9`
|
||||||
|
- `ipl::keyboard::EventObserver` at `0x802f8418`
|
||||||
|
|
||||||
|
3. We need to devise a way to have the channel overwrite IOS memory.
|
||||||
|
|
||||||
|
We have carved out our own space at `0x80014428` to put a function.
|
||||||
|
Thankfully, the operation is fairly simple:
|
||||||
|
- Write to [`MEM_PROT`](https://wiibrew.org/wiki/Hardware/Hollywood_Registers) and disable it. It is on by default.
|
||||||
|
- We use `0xcd8b420a` instead of `0x0d8b420a` as that appears to be where it is mapped for us.
|
||||||
|
- Dolphin appears to silently ignore this MMIO access. One day, we may want to not apply the patch should we be able to open `/dev/dolphin`.
|
||||||
|
- Write `20 00 47 70` as described above to `0x13a73ad4` to negate `IOSC_VerifyPublicKeySign`.
|
||||||
|
- Again, we must actually use `0xd3a73ad4` due to mapping.
|
||||||
|
- Dolphin once again appears to ignore this, thankfully.
|
||||||
|
- Clear cache
|
||||||
|
- TODO: Is this actually functional?
|
||||||
|
|
||||||
|
We write and apply the following PowerPC assembly to achieve this task:
|
||||||
|
```asm
|
||||||
|
overwriteIOSPatch:
|
||||||
|
; Load 0x0d8b420a, location of MEM_PROT, to r9.
|
||||||
|
lis r9, 0xcd8b
|
||||||
|
ori r9, r9, 0x420a
|
||||||
|
; We wish to write 0x2 in order to disable.
|
||||||
|
li r10, 0x2
|
||||||
|
|
||||||
|
; And... write!
|
||||||
|
sth r10, 0x0(r9)
|
||||||
|
eieio
|
||||||
|
|
||||||
|
; Load 0xd3a73ad4, location of of IOSC_VerifyPublicKeySig, to r9.
|
||||||
|
lis r9, 0xd3a7
|
||||||
|
ori r9, r9, 0x73ad4
|
||||||
|
; 0x20004770 represents our actual patch.
|
||||||
|
lis r10, 0x2000
|
||||||
|
ori r10, r10, 0x4770
|
||||||
|
|
||||||
|
; And... write.
|
||||||
|
stw r10, 0x0(r9)
|
||||||
|
|
||||||
|
; Clear cache
|
||||||
|
dcbi 0, r10
|
||||||
|
blr
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
4. We need to determine the best way to call our custom patching function.
|
||||||
|
Using the aforementioned symbols we find `ES_InitLib`, called once during initialization to open a handle with `/dev/es`.
|
||||||
|
|
||||||
|
We insert a call to our function in its epilog, immediately before loading the previous LR from stack and branching back.
|
||||||
|
This makes its flow roughly the following pseudocode:
|
||||||
|
```c
|
||||||
|
int ES_InitLib() {
|
||||||
|
int fd = 0;
|
||||||
|
if (ES_HANDLE < 0) {
|
||||||
|
ES_HANDLE = IOS_Open("/dev/es", 0);
|
||||||
|
if (ES_HANDLE < 0) {
|
||||||
|
fd = ES_HANDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our custom code
|
||||||
|
overwriteIOSMemory();
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
```
|
8
main.go
8
main.go
@ -84,6 +84,14 @@ func main() {
|
|||||||
mainDol, err = originalWad.GetContent(1)
|
mainDol, err = originalWad.GetContent(1)
|
||||||
check(err)
|
check(err)
|
||||||
|
|
||||||
|
// Permit r/w access to MEM2_PROT via the TMD.
|
||||||
|
// See docs/patch_overwrite_ios.md for more information!
|
||||||
|
originalWad.TMD.AccessRightsFlags = 0x3
|
||||||
|
|
||||||
|
// Apply all DOL patches
|
||||||
|
log.Println("Applying DOL patches...")
|
||||||
|
applyDefaultPatches()
|
||||||
|
|
||||||
// Load main ARC
|
// Load main ARC
|
||||||
arcData, err := originalWad.GetContent(2)
|
arcData, err := originalWad.GetContent(2)
|
||||||
check(err)
|
check(err)
|
||||||
|
82
modify_dol.go
Normal file
82
modify_dol.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInconsistentPatch = errors.New("before and after data present within file are not the same size")
|
||||||
|
ErrPatchOutOfRange = errors.New("patch cannot be applied past binary size")
|
||||||
|
ErrInvalidPatch = errors.New("before data present within patch did not exist in file")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Patch represents a patch applied to the main binary.
|
||||||
|
type Patch struct {
|
||||||
|
// Name is an optional name for this patch.
|
||||||
|
// If present, its name will be logged upon application.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// AtOffset is the offset within the file this patch should be applied at.
|
||||||
|
AtOffset int
|
||||||
|
|
||||||
|
// Before is an array of the bytes to find for, i.e. present within the original file.
|
||||||
|
Before []byte
|
||||||
|
|
||||||
|
// After is an array of the bytes to replace with.
|
||||||
|
After []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchSet represents multiple patches available to be applied.
|
||||||
|
type PatchSet []Patch
|
||||||
|
|
||||||
|
// applyPatch applies the given patch to the main DOL.
|
||||||
|
func applyPatch(patch Patch) error {
|
||||||
|
// Print name if present
|
||||||
|
if patch.Name != "" {
|
||||||
|
log.Println("Applying patch " + patch.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure consistency
|
||||||
|
if len(patch.Before) != len(patch.After) {
|
||||||
|
return ErrInconsistentPatch
|
||||||
|
}
|
||||||
|
if patch.AtOffset > len(mainDol) {
|
||||||
|
return ErrPatchOutOfRange
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either Before or After should return the same length.
|
||||||
|
patchLen := len(patch.Before)
|
||||||
|
|
||||||
|
// Ensure original bytes are present
|
||||||
|
originalBytes := mainDol[patch.AtOffset : patch.AtOffset+patchLen]
|
||||||
|
if !bytes.Equal(originalBytes, patch.Before) {
|
||||||
|
return ErrInvalidPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply patch
|
||||||
|
copy(mainDol[patch.AtOffset:], patch.After)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyPatchSet iterates through all possible patches, noting their name.
|
||||||
|
func applyPatchSet(setName string, set PatchSet) {
|
||||||
|
log.Printf("Handling patch set \"%s\":", setName)
|
||||||
|
|
||||||
|
for _, patch := range set {
|
||||||
|
err := applyPatch(patch)
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
142
patches.go
Normal file
142
patches.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// OverwriteIOSPatch effectively nullifies IOSC_VerifyPublicKeySign.
|
||||||
|
// See docs/patch_overwrite_ios.md for more information.
|
||||||
|
var OverwriteIOSPatch = PatchSet{
|
||||||
|
Patch{
|
||||||
|
Name: "Clear textinput::EventObserver functions",
|
||||||
|
AtOffset: 20320,
|
||||||
|
|
||||||
|
Before: Instructions{
|
||||||
|
// Function: textinput::EventObserver::onSE
|
||||||
|
BLR(),
|
||||||
|
padding,
|
||||||
|
padding,
|
||||||
|
padding,
|
||||||
|
// Function: textinput::EventObserver::onEvent
|
||||||
|
BLR(),
|
||||||
|
padding,
|
||||||
|
padding,
|
||||||
|
padding,
|
||||||
|
// Function: textinput::EventObserver::onCommand
|
||||||
|
BLR(),
|
||||||
|
padding,
|
||||||
|
padding,
|
||||||
|
padding,
|
||||||
|
// Function: textinput::EventObserver::onInput
|
||||||
|
BLR(),
|
||||||
|
padding,
|
||||||
|
padding,
|
||||||
|
padding,
|
||||||
|
}.toBytes(),
|
||||||
|
|
||||||
|
// We wish to clear these so that our custom overwriteIOSMemory
|
||||||
|
// function does not somehow conflict.
|
||||||
|
After: emptyBytes(64),
|
||||||
|
},
|
||||||
|
Patch{
|
||||||
|
Name: "Repair textinput::EventObserver vtable",
|
||||||
|
AtOffset: 3095452,
|
||||||
|
|
||||||
|
Before: []byte{
|
||||||
|
0x80, 0x01, 0x44, 0x50, // onSE
|
||||||
|
0x80, 0x01, 0x44, 0x40, // onEvent
|
||||||
|
0x80, 0x01, 0x44, 0x30, // onCommand
|
||||||
|
0x80, 0x01, 0x44, 0x20, // onInput
|
||||||
|
},
|
||||||
|
After: []byte{
|
||||||
|
// These are all pointers to our so-called doNothing.
|
||||||
|
0x80, 0x01, 0x44, 0x20,
|
||||||
|
0x80, 0x01, 0x44, 0x20,
|
||||||
|
0x80, 0x01, 0x44, 0x20,
|
||||||
|
0x80, 0x01, 0x44, 0x20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Patch{
|
||||||
|
Name: "Repair ipl::keyboard::EventObserver vtable",
|
||||||
|
AtOffset: 3097888,
|
||||||
|
|
||||||
|
Before: []byte{
|
||||||
|
0x80, 0x01, 0x44, 0x50, // onSE
|
||||||
|
0x80, 0x01, 0x84, 0xE0, // ipl::keyboard::EventObserver::onCommand - not patched
|
||||||
|
0x80, 0x01, 0x44, 0x30, // onCommand
|
||||||
|
},
|
||||||
|
After: []byte{
|
||||||
|
0x80, 0x01, 0x44, 0x20, // doNothing
|
||||||
|
0x80, 0x01, 0x84, 0xE0, // ipl::keyboard::EventObserver::onCommand - not patched
|
||||||
|
0x80, 0x01, 0x44, 0x20, // doNothing
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Patch{
|
||||||
|
Name: "Insert overwriteIOSMemory",
|
||||||
|
AtOffset: 20328,
|
||||||
|
|
||||||
|
// This area should be cleared.
|
||||||
|
Before: emptyBytes(48),
|
||||||
|
After: []byte{
|
||||||
|
// We want r9 to store the location of MEM_PROT at 0x0d8b420a.
|
||||||
|
// For us, this is mapped to 0xcd8b420a.
|
||||||
|
// lis r9, 0xcd8b
|
||||||
|
0x3D, 0x20, 0xCD, 0x8B,
|
||||||
|
// ori r9, r9, 0x420a
|
||||||
|
0x61, 0x29, 0x42, 0x0A,
|
||||||
|
|
||||||
|
// We want to write 0x2 and unlock everything.
|
||||||
|
// li r10, 0x02
|
||||||
|
0x39, 0x40, 0x00, 0x02,
|
||||||
|
|
||||||
|
// Write!
|
||||||
|
// sth r10, 0x0(r9)
|
||||||
|
0xB1, 0x49, 0x00, 0x00,
|
||||||
|
// Flush memory
|
||||||
|
// eieio
|
||||||
|
0x7C, 0x00, 0x06, 0xAC,
|
||||||
|
|
||||||
|
// Location of IOSC_VerifyPublicKeySign
|
||||||
|
// lis r9, 0xd3a7
|
||||||
|
0x3D, 0x20, 0xD3, 0xA7,
|
||||||
|
// ori r9, r9, 0x3ad4
|
||||||
|
0x61, 0x29, 0x3A, 0xD4,
|
||||||
|
|
||||||
|
// Write our custom THUMB.
|
||||||
|
// 0x20004770 is equivalent to:
|
||||||
|
// mov r0, #0x0
|
||||||
|
// bx lr
|
||||||
|
// lis r10, 0x2000
|
||||||
|
0x3D, 0x40, 0x20, 0x00,
|
||||||
|
// ori r10, r10, 0x4770
|
||||||
|
0x61, 0x4A, 0x47, 0x70,
|
||||||
|
|
||||||
|
// Write!
|
||||||
|
// stw r10, 0x0(r9)
|
||||||
|
0x91, 0x49, 0x00, 0x00,
|
||||||
|
// Possibly clear cache
|
||||||
|
// TODO(spotlightishere): Is this needed?
|
||||||
|
// dcbi 0, r10
|
||||||
|
0x7C, 0x00, 0x53, 0xAC,
|
||||||
|
// And finish.
|
||||||
|
// blr
|
||||||
|
0x4E, 0x80, 0x00, 0x20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Patch{
|
||||||
|
Name: "Modify ES_InitLib",
|
||||||
|
AtOffset: 2399844,
|
||||||
|
|
||||||
|
// We inject in the epilog of the function.
|
||||||
|
Before: []byte{
|
||||||
|
0x80, 0x01, 0x00, 0x14, // lwz r0, local_res4(r1)
|
||||||
|
0x7C, 0x08, 0x03, 0xA6, // mtspr LR, r0
|
||||||
|
0x38, 0x21, 0x00, 0x10, // addi r1, r1, 0x10
|
||||||
|
0x4E, 0x80, 0x00, 0x20, // blr
|
||||||
|
0x00, 0x00, 0x00, 0x00, // ; empty space following function
|
||||||
|
},
|
||||||
|
After: []byte{
|
||||||
|
0x80, 0x01, 0x00, 0x14, // lwz r0, local_res4(r1)
|
||||||
|
0x4B, 0xDB, 0xB1, 0x01, // bl overwriteIOSMemory @ 0x80014428
|
||||||
|
0x7C, 0x08, 0x03, 0xA6, // mtspr LR, r0
|
||||||
|
0x38, 0x21, 0x00, 0x10, // addi r1, r1, 0x10
|
||||||
|
0x4E, 0x80, 0x00, 0x20, // blr
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
26
powerpc.go
Normal file
26
powerpc.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Instruction represents a 4-byte PowerPC instruction.
|
||||||
|
type Instruction [4]byte
|
||||||
|
|
||||||
|
// Instructions represents a group of PowerPC instructions.
|
||||||
|
type Instructions []Instruction
|
||||||
|
|
||||||
|
// toBytes returns the bytes necessary to represent these instructions.
|
||||||
|
func (i Instructions) toBytes() []byte {
|
||||||
|
var contents []byte
|
||||||
|
|
||||||
|
for _, instruction := range i {
|
||||||
|
contents = append(contents, instruction[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents
|
||||||
|
}
|
||||||
|
|
||||||
|
// padding is not an actual instruction - it represents 4 zeros.
|
||||||
|
var padding Instruction = [4]byte{0x00, 0x00, 0x00, 0x00}
|
||||||
|
|
||||||
|
// BLR represents the blr PowerPC instruction.
|
||||||
|
func BLR() Instruction {
|
||||||
|
return [4]byte{0x4E, 0x80, 0x00, 0x20}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user