├── img ├── hv1.png ├── hv2.png ├── hv3.png ├── hv4.png ├── hv5.png ├── hv6.png ├── hv7.png └── running.png ├── .gitignore ├── OodleHelper ├── OodleHelper.vcxproj.user ├── OodleHelper.vcxproj.filters ├── OodleHelper.sln ├── OodleHelper.vcxproj └── OodleHelper.cpp ├── dumpfirehose.py ├── Hyper-V-Enabler.bat ├── README.md ├── LICENSE ├── test.py └── mitigate.py /img/hv1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/HEAD/img/hv1.png -------------------------------------------------------------------------------- /img/hv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/HEAD/img/hv2.png -------------------------------------------------------------------------------- /img/hv3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/HEAD/img/hv3.png -------------------------------------------------------------------------------- /img/hv4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/HEAD/img/hv4.png -------------------------------------------------------------------------------- /img/hv5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/HEAD/img/hv5.png -------------------------------------------------------------------------------- /img/hv6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/HEAD/img/hv6.png -------------------------------------------------------------------------------- /img/hv7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/HEAD/img/hv7.png -------------------------------------------------------------------------------- /img/running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/HEAD/img/running.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | definitions.json 2 | 3 | ffxiv.exe 4 | ffxiv_dx11.exe 5 | *.sig.*.dat 6 | Debug 7 | x64 8 | .vcxproj.user 9 | .idea 10 | .vs -------------------------------------------------------------------------------- /OodleHelper/OodleHelper.vcxproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /OodleHelper/OodleHelper.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /dumpfirehose.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4 | s.connect(("127.0.0.1", 1234)) 5 | 6 | while True: 7 | data = s.recv(int.from_bytes(s.recv(4), "little")) 8 | direction = int.from_bytes(data[:8], "little") 9 | xivbundle = data[8:] 10 | print(direction, xivbundle) 11 | -------------------------------------------------------------------------------- /Hyper-V-Enabler.bat: -------------------------------------------------------------------------------- 1 | pushd "%~dp0" 2 | dir /b %SystemRoot%\servicing\Packages\*Hyper-V*.mum >hyper-v.txt 3 | for /f %%i in ('findstr /i . hyper-v.txt 2^>nul') do dism /online /norestart /add-package:"%SystemRoot%\servicing\Packages\%%i" 4 | del hyper-v.txt 5 | Dism /online /enable-feature /featurename:Microsoft-Hyper-V -All /LimitAccess /ALL 6 | pause -------------------------------------------------------------------------------- /OodleHelper/OodleHelper.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33213.308 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OodleHelper", "OodleHelper.vcxproj", "{DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}.Debug|x64.ActiveCfg = Debug|x64 17 | {DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}.Debug|x64.Build.0 = Debug|x64 18 | {DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}.Debug|x86.ActiveCfg = Debug|Win32 19 | {DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}.Debug|x86.Build.0 = Debug|Win32 20 | {DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}.Release|x64.ActiveCfg = Release|x64 21 | {DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}.Release|x64.Build.0 = Release|x64 22 | {DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}.Release|x86.ActiveCfg = Release|Win32 23 | {DC00C7EA-55B6-41AD-BF67-D45F7C7E456D}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {F90BE519-2741-4889-B8CB-B78F46026E1B} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XivMitmLatencyMitigator 2 | Double weave for everyone, including PC, Mac, and PS4 users. No more being forced to single weaving because you are physically far away from game servers. 3 | 4 | If you're running a custom VPN server on linux, just run the script as root, and you're good to go. 5 | 6 | If you're running Windows, you can use [XivAlexander addon](https://github.com/Soreepeong/XivAlexander) instead. You can use both solutions at the same time, in which case, XivAlexander will take precedence. 7 | 8 | ## How to use 9 | 1. If you have a linux machine running in same network, skip to 5. 10 | 2. If you have a virtualization solution installed regardless of host operating system, add an empty virtual machine with only DVD Drive enabled without hard drive, mount linux installation ISO into VM, power on, and then skip to 5. 11 | 3. For Windows: install Hyper-V, using `Hyper-V-Enabler.bat`. 12 | 1. Open `Hyper-V Manager`. You can find this by typing so in Start Menu. 13 | 2. Open `Virtual Switch Manager` on the menu at right. 14 | * ![](img/hv1.png) 15 | 3. Add an `External` Virtual Switch. 16 | * ![](img/hv2.png) 17 | 4. Select the adapter connected to the network which the machine you're going to play game from is in, then press OK. 18 | * ![](img/hv3.png) 19 | 4. Create a new virtual machine. 20 | 1. Open `New Virtual Machine Wizard` with `New > Virtual Machine`. 21 | * ![](img/hv4.png) 22 | 2. Pick `Generation 2` in `Specify Generation`. 23 | * ![](img/hv5.png) 24 | 3. Write `256MB` or above into `Startup Memory` in `Assign Memory`. 25 | 4. Pick the new adapter you've set up from 3-4 in `Connection` in `Configure Networking`. 26 | 5. Pick `Attach a virtual hard disk later` in `Connect Virtual Hard Disk`. 27 | 6. Using default choices for everything else, finish the wizard. 28 | 7. Open `Settings` at right bottom panel of `Hyper-V Manager`. 29 | * ![](img/hv6.png) 30 | 8. Go to `SCSI Controller` at the left panel, and add a `DVD Drive`. 31 | 9. Go to `DVD Drive` at the left panel, pick `Image file:`, click on `Browse`, and select a linux distribution of your choice. 32 | 10. Go to `Security`, and turn off `Enable Secure Boot`. 33 | 11. Go to `Firmware`, select `DVD Drive`, and press `Move Up`. 34 | * ![](img/hv7.png) 35 | 10. Press `OK`. 36 | 11. Press `Connect...` at the right bottom panel, and then power on the virtual machine. 37 | 5. Run `ip addr show` to figure out your virtual machine's IP address. It should result in something like the following: 38 | ``` 39 | 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 40 | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 41 | inet 127.0.0.1/8 scope host lo 42 | valid_lft forever preferred_lft forever 43 | inet6 ::1/128 scope host 44 | valid_lft forever preferred_lft forever 45 | 2: eth0: mtu 1500 qdisc mq state UP group default qlen 1000 46 | link/ether 00:15:5d:12:1d:00 brd ff:ff:ff:ff:ff:ff 47 | inet 192.168.0.5/24 brd 192.168.0.255 scope global eth0 48 | valid_lft forever preferred_lft forever 49 | inet6 fe80::215:5dff:fe12:1d00/64 scope link 50 | valid_lft forever preferred_lft forever 51 | ``` 52 | 6. Run `iptables -t nat -A POSTROUTING -s -o -j MASQUERADE`, replacing: 53 | * `` with the equivalent of `eth0` on above output. 54 | * `` with the equivalent of `192.168.0.5/24` on above output. 55 | 7. Copy `ffxiv.exe` if VM is x86 or `ffxiv_dx11.exe` if VM is x64 from your local Windows/Mac game installation into the VM. 56 | * Download and run [HFS](https://www.rejetto.com/hfs/?f=dl), and then drag `ffxiv.exe` or `ffxiv_dx11.exe` into its main interface. 57 | * You should be able to download that by typing `curl -O ffxiv.exe http:///ffxiv.exe`, replacing `` 58 | with the displayed IP address from HFS. 59 | * Copy both files if you're not sure. 60 | 8. Run `curl https://raw.githubusercontent.com/Soreepeong/XivMitmLatencyMitigator/main/mitigate.py | python` 61 | 9. Configure your gaming device to use the virtual machine to route game traffic, replacing `` with the equivalent of `192.168.0.5` on above output. 62 | * **Windows**: Run a `Command Prompt` as Administrator, and then run the following. 63 | ``` 64 | route add 119.252.36.0/24 mask 255.255.255.0 65 | route add 119.252.37.0/24 mask 255.255.255.0 66 | route add 153.254.80.0/24 mask 255.255.255.0 67 | route add 204.2.29.0/24 mask 255.255.255.0 68 | route add 80.239.145.0/24 mask 255.255.255.0 69 | ``` 70 | * **Mac**: Run a `Terminal`, and the run the following. 71 | ``` 72 | sudo route -n add -net 119.252.36.0/24 73 | sudo route -n add -net 119.252.37.0/24 74 | sudo route -n add -net 153.254.80.0/24 75 | sudo route -n add -net 204.2.29.0/24 76 | sudo route -n add -net 80.239.145.0/24 77 | ``` 78 | * **PS4**: Set up Static IP ([Guide](https://www.linksys.com/gb/support-article?articleNum=216429)), but use `` instead for `Default Gateway`. 79 | 10. Run the game and see things get printed in the virtual machine, and if it does, it's working. 80 | * ![](img/running.png) 81 | 11. When you're done, you can force quit the virtual machine without "safe" procedures. 82 | 83 | ## License 84 | Apache License 2.0 85 | -------------------------------------------------------------------------------- /OodleHelper/OodleHelper.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | Win32Proj 24 | {dc00c7ea-55b6-41ad-bf67-d45f7c7e456d} 25 | OodleHelper 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v143 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v143 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v143 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Level3 76 | true 77 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 78 | true 79 | stdcpplatest 80 | stdc17 81 | 82 | 83 | Console 84 | true 85 | 86 | 87 | 88 | 89 | Level3 90 | true 91 | true 92 | true 93 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 94 | true 95 | stdcpplatest 96 | stdc17 97 | 98 | 99 | Console 100 | true 101 | true 102 | true 103 | 104 | 105 | 106 | 107 | Level3 108 | true 109 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 110 | true 111 | stdcpplatest 112 | stdc17 113 | 114 | 115 | Console 116 | true 117 | 118 | 119 | 120 | 121 | Level3 122 | true 123 | true 124 | true 125 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 126 | true 127 | stdcpplatest 128 | stdc17 129 | 130 | 131 | Console 132 | true 133 | true 134 | true 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /OodleHelper/OodleHelper.cpp: -------------------------------------------------------------------------------- 1 | #define _CRT_SECURE_NO_WARNINGS 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 13 | #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 14 | #define IMAGE_SIZEOF_SHORT_NAME 8 15 | 16 | struct IMAGE_DOS_HEADER { 17 | uint16_t e_magic; 18 | uint16_t e_cblp; 19 | uint16_t e_cp; 20 | uint16_t e_crlc; 21 | uint16_t e_cparhdr; 22 | uint16_t e_minalloc; 23 | uint16_t e_maxalloc; 24 | uint16_t e_ss; 25 | uint16_t e_sp; 26 | uint16_t e_csum; 27 | uint16_t e_ip; 28 | uint16_t e_cs; 29 | uint16_t e_lfarlc; 30 | uint16_t e_ovno; 31 | uint16_t e_res[4]; 32 | uint16_t e_oemid; 33 | uint16_t e_oeminfo; 34 | uint16_t e_res2[10]; 35 | uint32_t e_lfanew; 36 | }; 37 | 38 | struct IMAGE_FILE_HEADER { 39 | uint16_t Machine; 40 | uint16_t NumberOfSections; 41 | uint32_t TimeDateStamp; 42 | uint32_t PointerToSymbolTable; 43 | uint32_t NumberOfSymbols; 44 | uint16_t SizeOfOptionalHeader; 45 | uint16_t Characteristics; 46 | }; 47 | 48 | struct IMAGE_DATA_DIRECTORY { 49 | uint32_t VirtualAddress; 50 | uint32_t Size; 51 | }; 52 | 53 | struct IMAGE_OPTIONAL_HEADER32 { 54 | uint16_t Magic; 55 | uint8_t MajorLinkerVersion; 56 | uint8_t MinorLinkerVersion; 57 | uint32_t SizeOfCode; 58 | uint32_t SizeOfInitializedData; 59 | uint32_t SizeOfUninitializedData; 60 | uint32_t AddressOfEntryPoint; 61 | uint32_t BaseOfCode; 62 | uint32_t BaseOfData; 63 | uint32_t ImageBase; 64 | uint32_t SectionAlignment; 65 | uint32_t FileAlignment; 66 | uint16_t MajorOperatingSystemVersion; 67 | uint16_t MinorOperatingSystemVersion; 68 | uint16_t MajorImageVersion; 69 | uint16_t MinorImageVersion; 70 | uint16_t MajorSubsystemVersion; 71 | uint16_t MinorSubsystemVersion; 72 | uint32_t Win32VersionValue; 73 | uint32_t SizeOfImage; 74 | uint32_t SizeOfHeaders; 75 | uint32_t CheckSum; 76 | uint16_t Subsystem; 77 | uint16_t DllCharacteristics; 78 | uint32_t SizeOfStackReserve; 79 | uint32_t SizeOfStackCommit; 80 | uint32_t SizeOfHeapReserve; 81 | uint32_t SizeOfHeapCommit; 82 | uint32_t LoaderFlags; 83 | uint32_t NumberOfRvaAndSizes; 84 | IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; 85 | }; 86 | 87 | struct IMAGE_OPTIONAL_HEADER64 { 88 | uint16_t Magic; 89 | uint8_t MajorLinkerVersion; 90 | uint8_t MinorLinkerVersion; 91 | uint32_t SizeOfCode; 92 | uint32_t SizeOfInitializedData; 93 | uint32_t SizeOfUninitializedData; 94 | uint32_t AddressOfEntryPoint; 95 | uint32_t BaseOfCode; 96 | uint64_t ImageBase; 97 | uint32_t SectionAlignment; 98 | uint32_t FileAlignment; 99 | uint16_t MajorOperatingSystemVersion; 100 | uint16_t MinorOperatingSystemVersion; 101 | uint16_t MajorImageVersion; 102 | uint16_t MinorImageVersion; 103 | uint16_t MajorSubsystemVersion; 104 | uint16_t MinorSubsystemVersion; 105 | uint32_t Win32VersionValue; 106 | uint32_t SizeOfImage; 107 | uint32_t SizeOfHeaders; 108 | uint32_t CheckSum; 109 | uint16_t Subsystem; 110 | uint16_t DllCharacteristics; 111 | uint64_t SizeOfStackReserve; 112 | uint64_t SizeOfStackCommit; 113 | uint64_t SizeOfHeapReserve; 114 | uint64_t SizeOfHeapCommit; 115 | uint32_t LoaderFlags; 116 | uint32_t NumberOfRvaAndSizes; 117 | IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; 118 | }; 119 | 120 | template 121 | struct IMAGE_NT_HEADERS_SIZED { 122 | uint32_t Signature; 123 | IMAGE_FILE_HEADER FileHeader; 124 | TOptionalHeader OptionalHeader; 125 | }; 126 | 127 | struct IMAGE_SECTION_HEADER { 128 | char Name[IMAGE_SIZEOF_SHORT_NAME]; 129 | union { 130 | uint32_t PhysicalAddress; 131 | uint32_t VirtualSize; 132 | } Misc; 133 | uint32_t VirtualAddress; 134 | uint32_t SizeOfRawData; 135 | uint32_t PointerToRawData; 136 | uint32_t PointerToRelocations; 137 | uint32_t PointerToLinenumbers; 138 | uint16_t NumberOfRelocations; 139 | uint16_t NumberOfLinenumbers; 140 | uint32_t Characteristics; 141 | }; 142 | 143 | struct IMAGE_BASE_RELOCATION { 144 | uint32_t VirtualAddress; 145 | uint32_t SizeOfBlock; 146 | }; 147 | 148 | #define FIELD_OFFSET(type, field) ((int32_t)(int64_t)&(((type *)0)->field)) 149 | #define IMAGE_FIRST_SECTION( ntheader ) ((IMAGE_SECTION_HEADER*) \ 150 | ((const char*)(ntheader) + \ 151 | FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) + \ 152 | ((ntheader))->FileHeader.SizeOfOptionalHeader \ 153 | )) 154 | 155 | #if defined(_WIN64) 156 | #define STDCALL __stdcall 157 | const auto GamePath = LR"(C:\Program Files (x86)\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game\ffxiv_dx11.exe)"; 158 | 159 | extern "C" void* __stdcall VirtualAlloc(void* lpAddress, size_t dwSize, uint32_t flAllocationType, uint32_t flProtect); 160 | void* executable_allocate(size_t size) { 161 | return VirtualAlloc(nullptr, size, 0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x40 /* PAGE_EXECUTE_READWRITE */); 162 | } 163 | 164 | using IMAGE_NT_HEADERS = IMAGE_NT_HEADERS_SIZED; 165 | 166 | #elif defined(_WIN32) 167 | #define STDCALL __stdcall 168 | const auto GamePath = LR"(C:\Program Files (x86)\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game\ffxiv.exe)"; 169 | 170 | extern "C" void* __stdcall VirtualAlloc(void* lpAddress, size_t dwSize, uint32_t flAllocationType, uint32_t flProtect); 171 | void* executable_allocate(size_t size) { 172 | return VirtualAlloc(nullptr, size, 0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x40 /* PAGE_EXECUTE_READWRITE */); 173 | } 174 | 175 | using IMAGE_NT_HEADERS = IMAGE_NT_HEADERS_SIZED; 176 | 177 | #elif defined(__linux__) 178 | #define STDCALL __attribute__((stdcall)) 179 | const auto GamePath = R"(ffxiv.exe)"; 180 | 181 | #include 182 | #include 183 | #include 184 | #include 185 | #include 186 | void* executable_allocate(size_t size) { 187 | const auto p = memalign(sysconf(_SC_PAGE_SIZE), size); 188 | mprotect(p, size, PROT_READ | PROT_WRITE | PROT_EXEC); 189 | return p; 190 | } 191 | 192 | using IMAGE_NT_HEADERS = IMAGE_NT_HEADERS_SIZED; 193 | 194 | #endif 195 | 196 | void* STDCALL my_malloc(size_t size, int align) { 197 | const auto pRaw = (char*)malloc(size + align + sizeof(void*) - 1); 198 | if (!pRaw) 199 | return nullptr; 200 | 201 | const auto pAligned = (void*)(((size_t)pRaw + align + 7) & (size_t)-align); 202 | *((void**)pAligned - 1) = pRaw; 203 | return pAligned; 204 | } 205 | 206 | void STDCALL my_free(void* p) { 207 | free(*((void**)p - 1)); 208 | } 209 | 210 | using OodleNetwork1_Shared_Size = std::remove_pointer_t; 211 | using OodleNetwork1_Shared_SetWindow = std::remove_pointer_t; 212 | using OodleNetwork1_Proto_Train = std::remove_pointer_t; 213 | using OodleNetwork1_Proto_State_Size = std::remove_pointer_t; 214 | using OodleNetwork1_UDP_Decode = std::remove_pointer_t; 215 | using OodleNetwork1_UDP_Encode = std::remove_pointer_t; 216 | using OodleNetwork1_TCP_Decode = std::remove_pointer_t; 217 | using OodleNetwork1_TCP_Encode = std::remove_pointer_t; 218 | using Oodle_Malloc = std::remove_pointer_t; 219 | using Oodle_Free = std::remove_pointer_t; 220 | using Oodle_SetMallocFree = std::remove_pointer_t; 221 | 222 | class ScanResult { 223 | std::cmatch m_match; 224 | 225 | public: 226 | ScanResult() = default; 227 | ScanResult(const ScanResult&) = default; 228 | ScanResult(ScanResult&&) noexcept = default; 229 | ScanResult& operator=(const ScanResult&) = default; 230 | ScanResult& operator=(ScanResult&&) noexcept = default; 231 | 232 | ScanResult(std::cmatch match) 233 | : m_match(std::move(match)) { 234 | } 235 | 236 | template 237 | T& Get(size_t matchIndex) const { 238 | return *reinterpret_cast(const_cast(static_cast(m_match[matchIndex].first))); 239 | } 240 | 241 | template 242 | T* ResolveAddress(size_t matchIndex) const { 243 | return reinterpret_cast(m_match[matchIndex].first + 4 + Get(matchIndex)); 244 | } 245 | 246 | template 247 | T* begin(size_t matchIndex) { 248 | return reinterpret_cast(const_cast(static_cast(&*m_match[matchIndex].first))); 249 | } 250 | 251 | template 252 | void* end(size_t matchIndex) { 253 | return reinterpret_cast(const_cast(static_cast(&*m_match[matchIndex].second))); 254 | } 255 | }; 256 | 257 | class RegexSignature { 258 | const std::regex m_pattern; 259 | 260 | public: 261 | template 262 | RegexSignature(const char(&data)[Length]) 263 | : m_pattern{ data, data + Length - 1 } { 264 | } 265 | 266 | bool Lookup(const void* data, size_t length, ScanResult& result, bool next = false) const { 267 | std::cmatch match; 268 | 269 | if (next) { 270 | const auto end = static_cast(data) + length; 271 | const auto prevEnd = result.end(0); 272 | if (prevEnd >= end) 273 | return false; 274 | data = prevEnd; 275 | } 276 | 277 | if (!std::regex_search(static_cast(data), static_cast(data) + length, match, m_pattern)) 278 | return false; 279 | 280 | result = ScanResult(std::move(match)); 281 | return true; 282 | } 283 | 284 | template 285 | bool Lookup(std::span data, ScanResult& result, bool next = false) const { 286 | return Lookup(data.data(), data.size_bytes(), result, next); 287 | } 288 | }; 289 | 290 | template 291 | void DisplaceBy(T*& what, size_t offset) { 292 | what = reinterpret_cast(reinterpret_cast(what) + offset); 293 | } 294 | 295 | struct OodleXiv { 296 | int HtBits; 297 | int Window; 298 | void* BaseAddress; 299 | OodleNetwork1_Shared_Size* SharedSize; 300 | Oodle_SetMallocFree* SetMallocFree; 301 | OodleNetwork1_Shared_SetWindow* SharedSetWindow; 302 | OodleNetwork1_Proto_State_Size* UdpStateSize; 303 | OodleNetwork1_Proto_State_Size* TcpStateSize; 304 | OodleNetwork1_Proto_Train* TcpTrain; 305 | OodleNetwork1_Proto_Train* UdpTrain; 306 | OodleNetwork1_TCP_Decode* TcpDecode; 307 | OodleNetwork1_UDP_Decode* UdpDecode; 308 | OodleNetwork1_TCP_Encode* TcpEncode; 309 | OodleNetwork1_UDP_Encode* UdpEncode; 310 | bool Ready = false; 311 | 312 | bool Lookup(std::span virt) { 313 | if (Ready) { 314 | if (BaseAddress != virt.data()) { 315 | const auto displacement = reinterpret_cast(virt.data()) - reinterpret_cast(BaseAddress); 316 | BaseAddress = virt.data(); 317 | 318 | DisplaceBy(SharedSize, displacement); 319 | DisplaceBy(SetMallocFree, displacement); 320 | DisplaceBy(SharedSetWindow, displacement); 321 | DisplaceBy(UdpStateSize, displacement); 322 | DisplaceBy(TcpStateSize, displacement); 323 | DisplaceBy(TcpTrain, displacement); 324 | DisplaceBy(UdpTrain, displacement); 325 | DisplaceBy(TcpDecode, displacement); 326 | DisplaceBy(UdpDecode, displacement); 327 | DisplaceBy(TcpEncode, displacement); 328 | DisplaceBy(UdpEncode, displacement); 329 | } 330 | return true; 331 | } 332 | 333 | #ifdef _WIN64 334 | const auto InitOodle = RegexSignature(R"(\x75[\s\S]\x48\x8d\x15[\s\S][\s\S][\s\S][\s\S]\x48\x8d\x0d[\s\S][\s\S][\s\S][\s\S]\xe8([\s\S][\s\S][\s\S][\s\S])\xc6\x05[\s\S][\s\S][\s\S][\s\S]\x01[\s\S]{0,256}\x75[\s\S]\xb9([\s\S][\s\S][\s\S][\s\S])\xe8([\s\S][\s\S][\s\S][\s\S])\x45\x33\xc0\x33\xd2\x48\x8b\xc8\xe8[\s\S][\s\S][\s\S][\s\S][\s\S]{0,6}\x41\xb9([\s\S][\s\S][\s\S][\s\S])\xba[\s\S][\s\S][\s\S][\s\S][\s\S]{0,6}\x48\x8b\xc8\xe8([\s\S][\s\S][\s\S][\s\S]))"); 335 | #else 336 | const auto InitOodle = RegexSignature(R"(\x75\x16\x68[\s\S][\s\S][\s\S][\s\S]\x68[\s\S][\s\S][\s\S][\s\S]\xe8([\s\S][\s\S][\s\S][\s\S])\xc6\x05[\s\S][\s\S][\s\S][\s\S]\x01[\s\S]{0,256}\x75\x27\x6a([\s\S])\xe8([\s\S][\s\S][\s\S][\s\S])\x6a\x00\x6a\x00\x50\xe8[\s\S][\s\S][\s\S][\s\S]\x83\xc4[\s\S]\x89\x46[\s\S]\x68([\s\S][\s\S][\s\S][\s\S])\xff\x76[\s\S]\x6a[\s\S]\x50\xe8([\s\S][\s\S][\s\S][\s\S]))"); 337 | #endif 338 | if (ScanResult sr; InitOodle.Lookup(virt, sr)) { 339 | SetMallocFree = sr.ResolveAddress(1); 340 | HtBits = sr.Get(2); 341 | SharedSize = sr.ResolveAddress(3); 342 | Window = sr.Get(4); 343 | SharedSetWindow = sr.ResolveAddress(5); 344 | } else 345 | return false; 346 | 347 | #ifdef _WIN64 348 | const auto SetUpStatesAndTrain = RegexSignature(R"(\x75\x04\x48\x89\x7e[\s\S]\xe8([\s\S][\s\S][\s\S][\s\S])\x4c[\s\S][\s\S]\xe8([\s\S][\s\S][\s\S][\s\S])[\s\S]{0,256}\x01\x75\x0a\x48\x8b\x0f\xe8([\s\S][\s\S][\s\S][\s\S])\xeb\x09\x48\x8b\x4f\x08\xe8([\s\S][\s\S][\s\S][\s\S]))"); 349 | #else 350 | const auto SetUpStatesAndTrain = RegexSignature(R"(\xe8([\s\S][\s\S][\s\S][\s\S])\x8b\xd8\xe8([\s\S][\s\S][\s\S][\s\S])\x83\x7d\x10\x01[\s\S]{0,256}\x83\x7d\x10\x01\x6a\x00\x6a\x00\x6a\x00\xff\x77[\s\S]\x75\x09\xff[\s\S]\xe8([\s\S][\s\S][\s\S][\s\S])\xeb\x08\xff\x76[\s\S]\xe8([\s\S][\s\S][\s\S][\s\S]))"); 351 | #endif 352 | if (ScanResult sr; SetUpStatesAndTrain.Lookup(virt, sr)) { 353 | UdpStateSize = sr.ResolveAddress(1); 354 | TcpStateSize = sr.ResolveAddress(2); 355 | TcpTrain = sr.ResolveAddress(3); 356 | UdpTrain = sr.ResolveAddress(4); 357 | } else 358 | return false; 359 | 360 | #ifdef _WIN64 361 | const auto DecodeOodle = RegexSignature(R"(\x4d\x85\xd2\x74\x0a\x49\x8b\xca\xe8([\s\S][\s\S][\s\S][\s\S])\xeb\x09\x48\x8b\x49\x08\xe8([\s\S][\s\S][\s\S][\s\S]))"); 362 | const auto EncodeOodle = RegexSignature(R"(\x48\x85\xc0\x74\x0d\x48\x8b\xc8\xe8([\s\S][\s\S][\s\S][\s\S])\x48[\s\S][\s\S]\xeb\x0b\x48\x8b\x49\x08\xe8([\s\S][\s\S][\s\S][\s\S]))"); 363 | if (ScanResult sr1, sr2; DecodeOodle.Lookup(virt, sr1) && EncodeOodle.Lookup(virt, sr2)) { 364 | TcpDecode = sr1.ResolveAddress(1); 365 | UdpDecode = sr1.ResolveAddress(2); 366 | TcpEncode = sr2.ResolveAddress(1); 367 | UdpEncode = sr2.ResolveAddress(2); 368 | } else 369 | return false; 370 | #else 371 | const auto TcpCodecOodle = RegexSignature(R"(\x85\xc0\x74[\s\S]\x50\xe8([\s\S][\s\S][\s\S][\s\S])\x57\x8b\xf0\xff\x15)"); 372 | const auto UdpCodecOodle = RegexSignature(R"(\xff\x71\x04\xe8([\s\S][\s\S][\s\S][\s\S])\x57\x8b\xf0\xff\x15)"); 373 | if (ScanResult sr1, sr2; TcpCodecOodle.Lookup(virt, sr1) && UdpCodecOodle.Lookup(virt, sr2)) { 374 | TcpEncode = sr1.ResolveAddress(1); 375 | UdpEncode = sr2.ResolveAddress(1); 376 | // NOTE: compressed buffer size must be (8 + input.size) 377 | 378 | if (TcpCodecOodle.Lookup(virt, sr1, true) && UdpCodecOodle.Lookup(virt, sr2, true)) { 379 | TcpDecode = sr1.ResolveAddress(1); 380 | UdpDecode = sr2.ResolveAddress(1); 381 | } else 382 | return false; 383 | } else 384 | return false; 385 | #endif 386 | 387 | BaseAddress = virt.data(); 388 | Ready = true; 389 | return true; 390 | } 391 | }; 392 | 393 | class Oodle { 394 | OodleXiv m_oodleXiv; 395 | 396 | std::vector m_shared; 397 | std::vector m_state; 398 | std::vector m_window; 399 | 400 | bool m_isTcp{}; 401 | 402 | public: 403 | Oodle() = default; 404 | Oodle(Oodle&&) noexcept = default; 405 | Oodle(const Oodle&) = delete; 406 | Oodle& operator=(Oodle&&) noexcept = default; 407 | Oodle& operator=(const Oodle&) = delete; 408 | 409 | void SetupUdp(const OodleXiv& oodleXiv) { 410 | m_oodleXiv = oodleXiv; 411 | m_isTcp = false; 412 | 413 | m_shared.clear(); 414 | m_state.clear(); 415 | m_window.clear(); 416 | m_shared.resize(m_oodleXiv.SharedSize(m_oodleXiv.HtBits)); 417 | m_state.resize(m_oodleXiv.UdpStateSize()); 418 | m_window.resize(m_oodleXiv.Window); 419 | 420 | m_oodleXiv.SharedSetWindow(&m_shared[0], m_oodleXiv.HtBits, &m_window[0], m_oodleXiv.Window); 421 | m_oodleXiv.UdpTrain(&m_state[0], &m_shared[0], nullptr, nullptr, 0); 422 | } 423 | 424 | void SetupTcp(const OodleXiv& oodleXiv) { 425 | m_oodleXiv = oodleXiv; 426 | m_isTcp = true; 427 | 428 | m_shared.clear(); 429 | m_state.clear(); 430 | m_window.clear(); 431 | m_shared.resize(m_oodleXiv.SharedSize(m_oodleXiv.HtBits)); 432 | m_state.resize(m_oodleXiv.TcpStateSize()); 433 | m_window.resize(m_oodleXiv.Window); 434 | 435 | m_oodleXiv.SharedSetWindow(&m_shared[0], m_oodleXiv.HtBits, &m_window[0], m_oodleXiv.Window); 436 | m_oodleXiv.TcpTrain(&m_state[0], &m_shared[0], nullptr, nullptr, 0); 437 | } 438 | 439 | size_t Encode(const void* source, size_t sourceLength, void* target, size_t targetLength) { 440 | if (targetLength < CompressedBufferSizeNeeded(sourceLength)) 441 | return (std::numeric_limits::max)(); 442 | 443 | if (m_isTcp) 444 | return m_oodleXiv.TcpEncode(m_state.data(), m_shared.data(), source, sourceLength, target); 445 | else 446 | return m_oodleXiv.UdpEncode(m_state.data(), m_shared.data(), source, sourceLength, target); 447 | } 448 | 449 | size_t Decode(const void* source, size_t sourceLength, void* target, size_t targetLength) { 450 | if (m_isTcp) 451 | return m_oodleXiv.TcpDecode(m_state.data(), m_shared.data(), source, sourceLength, target, targetLength); 452 | else 453 | return m_oodleXiv.UdpDecode(m_state.data(), m_shared.data(), source, sourceLength, target, targetLength); 454 | } 455 | 456 | static size_t CompressedBufferSizeNeeded(size_t n) { 457 | return n + 8; 458 | } 459 | }; 460 | 461 | int main() { 462 | freopen(NULL, "rb", stdin); 463 | freopen(NULL, "wb", stdout); 464 | 465 | std::ifstream game(GamePath, std::ios::binary); 466 | game.seekg(0, std::ios::end); 467 | std::vector buf((static_cast(game.tellg()) + 3) / 4 * 4); 468 | game.seekg(0, std::ios::beg); 469 | game.read(&buf[0], buf.size()); 470 | 471 | auto simplehash = static_cast(buf.size()); 472 | for (size_t i = 0; i < buf.size(); i += 4) 473 | simplehash ^= *reinterpret_cast(&buf[i]); 474 | 475 | const auto& dosh = *(IMAGE_DOS_HEADER*)(&buf[0]); 476 | const auto& nth = *(IMAGE_NT_HEADERS*)(&buf[dosh.e_lfanew]); 477 | 478 | std::span virt((char*)executable_allocate(nth.OptionalHeader.SizeOfImage), nth.OptionalHeader.SizeOfImage); 479 | fprintf(stderr, "Base: 0x%zX\n", (size_t)virt.data()); 480 | 481 | const auto ddoff = dosh.e_lfanew + sizeof(uint32_t) + sizeof(IMAGE_FILE_HEADER) + nth.FileHeader.SizeOfOptionalHeader; 482 | memcpy(&virt[0], &buf[0], ddoff + sizeof(IMAGE_SECTION_HEADER) * nth.FileHeader.NumberOfSections); 483 | for (const auto& s : std::span((IMAGE_SECTION_HEADER*)&buf[ddoff], nth.FileHeader.NumberOfSections)) { 484 | const auto src = std::span(&buf[s.PointerToRawData], s.SizeOfRawData); 485 | const auto dst = std::span(&virt[s.VirtualAddress], s.Misc.VirtualSize); 486 | memcpy(&dst[0], &src[0], std::min(src.size(), dst.size())); 487 | } 488 | 489 | const auto base = nth.OptionalHeader.ImageBase; 490 | for (size_t i = nth.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress, 491 | i_ = i + nth.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size; 492 | i < i_; ) { 493 | const auto& page = *(IMAGE_BASE_RELOCATION*)&virt[i]; 494 | for (const auto relo : std::span((uint16_t*)(&page + 1), (page.SizeOfBlock - sizeof page) / 2)) { 495 | if ((relo >> 12) == 0) 496 | void(); 497 | else if ((relo >> 12) == 3) 498 | *(uint32_t*)&virt[(size_t)page.VirtualAddress + (relo & 0xFFF)] += (uint32_t)((size_t)&virt[0] - base); 499 | else if ((relo >> 12) == 10) 500 | *(uint64_t*)&virt[(size_t)page.VirtualAddress + (relo & 0xFFF)] += (uint64_t)((size_t)&virt[0] - base); 501 | else 502 | std::abort(); 503 | } 504 | 505 | i += page.SizeOfBlock; 506 | } 507 | 508 | char cacheFileName[256]; 509 | snprintf(cacheFileName, sizeof cacheFileName, "ffxiv.sig.%08x.dat", simplehash); 510 | 511 | OodleXiv oodleXiv{}; 512 | 513 | auto writeAfterLookup = true; 514 | if (const auto fprev = fopen(cacheFileName, "rb")) { 515 | fread(&oodleXiv, sizeof oodleXiv, 1, fprev); 516 | fclose(fprev); 517 | 518 | writeAfterLookup = !oodleXiv.Ready; 519 | } 520 | 521 | if (!oodleXiv.Lookup(virt)) { 522 | fprintf(stderr, "Failed to look for signatures.\n"); 523 | return -1; 524 | } else { 525 | fprintf(stderr, "All signatures resolved.\n"); 526 | 527 | fprintf(stderr, "htbits: 0x%zX\n", oodleXiv.HtBits); 528 | fprintf(stderr, "window size: 0x%zX\n", oodleXiv.Window); 529 | fprintf(stderr, "SharedSize: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.SharedSize) - virt.data()); 530 | fprintf(stderr, "SetMallocFree: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.SetMallocFree) - virt.data()); 531 | fprintf(stderr, "SharedSetWindow: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.SharedSetWindow) - virt.data()); 532 | fprintf(stderr, "UdpStateSize: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.UdpStateSize) - virt.data()); 533 | fprintf(stderr, "TcpStateSize: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.TcpStateSize) - virt.data()); 534 | fprintf(stderr, "TcpTrain: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.TcpTrain) - virt.data()); 535 | fprintf(stderr, "UdpTrain: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.UdpTrain) - virt.data()); 536 | fprintf(stderr, "TcpDecode: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.TcpDecode) - virt.data()); 537 | fprintf(stderr, "UdpDecode: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.UdpDecode) - virt.data()); 538 | fprintf(stderr, "TcpEncode: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.TcpEncode) - virt.data()); 539 | fprintf(stderr, "UdpEncode: ffxiv.exe+0x%zX\n", reinterpret_cast(oodleXiv.UdpEncode) - virt.data()); 540 | } 541 | 542 | if (writeAfterLookup) { 543 | const auto f = fopen(cacheFileName, "wb"); 544 | if (f) { 545 | fwrite(&oodleXiv, sizeof oodleXiv, 1, f); 546 | fclose(f); 547 | } 548 | } 549 | 550 | memset(reinterpret_cast(oodleXiv.TcpTrain) + 0xaba - 0xAB0, 0x90, 6); 551 | memset(reinterpret_cast(oodleXiv.TcpTrain) + 0xad2 - 0xAB0, 0x90, 6); 552 | memset(reinterpret_cast(oodleXiv.TcpTrain) + 0xbb4 - 0xAB0, 0x90, 7); 553 | memset(reinterpret_cast(oodleXiv.TcpTrain) + 0xbc8 - 0xAB0, 0x90, 7); 554 | 555 | oodleXiv.SetMallocFree(&my_malloc, &my_free); 556 | 557 | Oodle oodleUdp; 558 | oodleUdp.SetupUdp(oodleXiv); 559 | 560 | Oodle oodleTcp[4]; 561 | for (auto& o : oodleTcp) 562 | o.SetupTcp(oodleXiv); 563 | 564 | fprintf(stderr, "Oodle helper running.\n"); 565 | 566 | std::vector src, dst; 567 | while (true) { 568 | struct my_header_t { 569 | uint32_t SourceLength; 570 | uint32_t TargetLength; 571 | uint32_t Channel; 572 | } hdr{}; 573 | static_assert(sizeof(my_header_t) == 12); 574 | 575 | fread(&hdr, sizeof(hdr), 1, stdin); 576 | if (!hdr.SourceLength) 577 | return 0; 578 | 579 | src.resize(hdr.SourceLength); 580 | fread(&src[0], 1, src.size(), stdin); 581 | 582 | if (hdr.TargetLength == 0xFFFFFFFFu) { 583 | dst.resize(Oodle::CompressedBufferSizeNeeded(src.size())); 584 | if (hdr.Channel == 0xFFFFFFFFu) 585 | dst.resize(oodleUdp.Encode(src.data(), src.size(), dst.data(), dst.size())); 586 | else if (hdr.Channel < 4) 587 | dst.resize(oodleTcp[hdr.Channel].Encode(src.data(), src.size(), dst.data(), dst.size())); 588 | else 589 | dst.resize(0); 590 | 591 | } else { 592 | dst.resize(hdr.TargetLength); 593 | auto ok = false; 594 | if (hdr.Channel == 0xFFFFFFFFu) 595 | ok = oodleUdp.Decode(src.data(), src.size(), dst.data(), dst.size()); 596 | else if (hdr.Channel < 4) 597 | ok = oodleTcp[hdr.Channel].Decode(src.data(), src.size(), dst.data(), dst.size()); 598 | if (!ok) { 599 | dst.resize(0); 600 | dst.resize(hdr.TargetLength); 601 | } 602 | } 603 | uint32_t size = (uint32_t)dst.size(); 604 | fwrite(&size, sizeof(size), 1, stdout); 605 | fwrite(&dst[0], 1, dst.size(), stdout); 606 | fflush(stdout); 607 | } 608 | } -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import ctypes.util 3 | import os 4 | import pathlib 5 | import re 6 | import secrets 7 | import sys 8 | import typing 9 | 10 | import mmap 11 | 12 | # region PE Structures 13 | 14 | IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16 15 | IMAGE_DIRECTORY_ENTRY_BASERELOC = 5 16 | IMAGE_SIZEOF_SHORT_NAME = 8 17 | 18 | 19 | class ImageDosHeader(ctypes.LittleEndianStructure): 20 | _fields_ = ( 21 | ("e_magic", ctypes.c_uint16), 22 | ("e_cblp", ctypes.c_uint16), 23 | ("e_cp", ctypes.c_uint16), 24 | ("e_crlc", ctypes.c_uint16), 25 | ("e_cparhdr", ctypes.c_uint16), 26 | ("e_minalloc", ctypes.c_uint16), 27 | ("e_maxalloc", ctypes.c_uint16), 28 | ("e_ss", ctypes.c_uint16), 29 | ("e_sp", ctypes.c_uint16), 30 | ("e_csum", ctypes.c_uint16), 31 | ("e_ip", ctypes.c_uint16), 32 | ("e_cs", ctypes.c_uint16), 33 | ("e_lfarlc", ctypes.c_uint16), 34 | ("e_ovno", ctypes.c_uint16), 35 | ("e_res", ctypes.c_uint16 * 4), 36 | ("e_oemid", ctypes.c_uint16), 37 | ("e_oeminfo", ctypes.c_uint16), 38 | ("e_res2", ctypes.c_uint16 * 10), 39 | ("e_lfanew", ctypes.c_uint32), 40 | ) 41 | e_magic: int | ctypes.c_uint16 42 | e_cblp: int | ctypes.c_uint16 43 | e_cp: int | ctypes.c_uint16 44 | e_crlc: int | ctypes.c_uint16 45 | e_cparhdr: int | ctypes.c_uint16 46 | e_minalloc: int | ctypes.c_uint16 47 | e_maxalloc: int | ctypes.c_uint16 48 | e_ss: int | ctypes.c_uint16 49 | e_sp: int | ctypes.c_uint16 50 | e_csum: int | ctypes.c_uint16 51 | e_ip: int | ctypes.c_uint16 52 | e_cs: int | ctypes.c_uint16 53 | e_lfarlc: int | ctypes.c_uint16 54 | e_ovno: int | ctypes.c_uint16 55 | e_res: typing.Sequence[int] | ctypes.c_uint16 * 4 56 | e_oemid: int | ctypes.c_uint16 57 | e_oeminfo: int | ctypes.c_uint16 58 | e_res2: typing.Sequence[int] | ctypes.c_uint16 * 10 59 | e_lfanew: int | ctypes.c_uint32 60 | 61 | 62 | class ImageFileHeader(ctypes.LittleEndianStructure): 63 | _fields_ = ( 64 | ("Machine", ctypes.c_uint16), 65 | ("NumberOfSections", ctypes.c_uint16), 66 | ("TimeDateStamp", ctypes.c_uint32), 67 | ("PointerToSymbolTable", ctypes.c_uint32), 68 | ("NumberOfSymbols", ctypes.c_uint32), 69 | ("SizeOfOptionalHeader", ctypes.c_uint16), 70 | ("Characteristics", ctypes.c_uint16), 71 | ) 72 | Machine: int | ctypes.c_uint16 73 | NumberOfSections: int | ctypes.c_uint16 74 | TimeDateStamp: int | ctypes.c_uint32 75 | PointerToSymbolTable: int | ctypes.c_uint32 76 | NumberOfSymbols: int | ctypes.c_uint32 77 | SizeOfOptionalHeader: int | ctypes.c_uint16 78 | Characteristics: int | ctypes.c_uint16 79 | 80 | 81 | class ImageDataDirectory(ctypes.LittleEndianStructure): 82 | _fields_ = ( 83 | ("VirtualAddress", ctypes.c_uint32), 84 | ("Size", ctypes.c_uint32), 85 | ) 86 | VirtualAddress: int | ctypes.c_uint32 87 | Size: int | ctypes.c_uint32 88 | 89 | 90 | class ImageOptionalHeader32(ctypes.LittleEndianStructure): 91 | _fields_ = ( 92 | ("Magic", ctypes.c_uint16), 93 | ("MajorLinkerVersion", ctypes.c_uint8), 94 | ("MinorLinkerVersion", ctypes.c_uint8), 95 | ("SizeOfCode", ctypes.c_uint32), 96 | ("SizeOfInitializedData", ctypes.c_uint32), 97 | ("SizeOfUninitializedData", ctypes.c_uint32), 98 | ("AddressOfEntryPoint", ctypes.c_uint32), 99 | ("BaseOfCode", ctypes.c_uint32), 100 | ("BaseOfData", ctypes.c_uint32), 101 | ("ImageBase", ctypes.c_uint32), 102 | ("SectionAlignment", ctypes.c_uint32), 103 | ("FileAlignment", ctypes.c_uint32), 104 | ("MajorOperatingSystemVersion", ctypes.c_uint16), 105 | ("MinorOperatingSystemVersion", ctypes.c_uint16), 106 | ("MajorImageVersion", ctypes.c_uint16), 107 | ("MinorImageVersion", ctypes.c_uint16), 108 | ("MajorSubsystemVersion", ctypes.c_uint16), 109 | ("MinorSubsystemVersion", ctypes.c_uint16), 110 | ("Win32VersionValue", ctypes.c_uint32), 111 | ("SizeOfImage", ctypes.c_uint32), 112 | ("SizeOfHeaders", ctypes.c_uint32), 113 | ("CheckSum", ctypes.c_uint32), 114 | ("Subsystem", ctypes.c_uint16), 115 | ("DllCharacteristics", ctypes.c_uint16), 116 | ("SizeOfStackReserve", ctypes.c_uint32), 117 | ("SizeOfStackCommit", ctypes.c_uint32), 118 | ("SizeOfHeapReserve", ctypes.c_uint32), 119 | ("SizeOfHeapCommit", ctypes.c_uint32), 120 | ("LoaderFlags", ctypes.c_uint32), 121 | ("NumberOfRvaAndSizes", ctypes.c_uint32), 122 | ("DataDirectory", ImageDataDirectory * IMAGE_NUMBEROF_DIRECTORY_ENTRIES), 123 | ) 124 | Magic: int | ctypes.c_uint16 125 | MajorLinkerVersion: int | ctypes.c_uint8 126 | MinorLinkerVersion: int | ctypes.c_uint8 127 | SizeOfCode: int | ctypes.c_uint32 128 | SizeOfInitializedData: int | ctypes.c_uint32 129 | SizeOfUninitializedData: int | ctypes.c_uint32 130 | AddressOfEntryPoint: int | ctypes.c_uint32 131 | BaseOfCode: int | ctypes.c_uint32 132 | BaseOfData: int | ctypes.c_uint32 133 | ImageBase: int | ctypes.c_uint32 134 | SectionAlignment: int | ctypes.c_uint32 135 | FileAlignment: int | ctypes.c_uint32 136 | MajorOperatingSystemVersion: int | ctypes.c_uint16 137 | MinorOperatingSystemVersion: int | ctypes.c_uint16 138 | MajorImageVersion: int | ctypes.c_uint16 139 | MinorImageVersion: int | ctypes.c_uint16 140 | MajorSubsystemVersion: int | ctypes.c_uint16 141 | MinorSubsystemVersion: int | ctypes.c_uint16 142 | Win32VersionValue: int | ctypes.c_uint32 143 | SizeOfImage: int | ctypes.c_uint32 144 | SizeOfHeaders: int | ctypes.c_uint32 145 | CheckSum: int | ctypes.c_uint32 146 | Subsystem: int | ctypes.c_uint16 147 | DllCharacteristics: int | ctypes.c_uint16 148 | SizeOfStackReserve: int | ctypes.c_uint32 149 | SizeOfStackCommit: int | ctypes.c_uint32 150 | SizeOfHeapReserve: int | ctypes.c_uint32 151 | SizeOfHeapCommit: int | ctypes.c_uint32 152 | LoaderFlags: int | ctypes.c_uint32 153 | NumberOfRvaAndSizes: int | ctypes.c_uint32 154 | DataDirectory: typing.Sequence[ImageDataDirectory] | ImageDataDirectory * IMAGE_NUMBEROF_DIRECTORY_ENTRIES 155 | 156 | 157 | class ImageOptionalHeader64(ctypes.LittleEndianStructure): 158 | _fields_ = ( 159 | ("Magic", ctypes.c_uint16), 160 | ("MajorLinkerVersion", ctypes.c_uint8), 161 | ("MinorLinkerVersion", ctypes.c_uint8), 162 | ("SizeOfCode", ctypes.c_uint32), 163 | ("SizeOfInitializedData", ctypes.c_uint32), 164 | ("SizeOfUninitializedData", ctypes.c_uint32), 165 | ("AddressOfEntryPoint", ctypes.c_uint32), 166 | ("BaseOfCode", ctypes.c_uint32), 167 | ("ImageBase", ctypes.c_uint64), 168 | ("SectionAlignment", ctypes.c_uint32), 169 | ("FileAlignment", ctypes.c_uint32), 170 | ("MajorOperatingSystemVersion", ctypes.c_uint16), 171 | ("MinorOperatingSystemVersion", ctypes.c_uint16), 172 | ("MajorImageVersion", ctypes.c_uint16), 173 | ("MinorImageVersion", ctypes.c_uint16), 174 | ("MajorSubsystemVersion", ctypes.c_uint16), 175 | ("MinorSubsystemVersion", ctypes.c_uint16), 176 | ("Win32VersionValue", ctypes.c_uint32), 177 | ("SizeOfImage", ctypes.c_uint32), 178 | ("SizeOfHeaders", ctypes.c_uint32), 179 | ("CheckSum", ctypes.c_uint32), 180 | ("Subsystem", ctypes.c_uint16), 181 | ("DllCharacteristics", ctypes.c_uint16), 182 | ("SizeOfStackReserve", ctypes.c_uint64), 183 | ("SizeOfStackCommit", ctypes.c_uint64), 184 | ("SizeOfHeapReserve", ctypes.c_uint64), 185 | ("SizeOfHeapCommit", ctypes.c_uint64), 186 | ("LoaderFlags", ctypes.c_uint32), 187 | ("NumberOfRvaAndSizes", ctypes.c_uint32), 188 | ("DataDirectory", ImageDataDirectory * IMAGE_NUMBEROF_DIRECTORY_ENTRIES), 189 | ) 190 | Magic: int | ctypes.c_uint16 191 | MajorLinkerVersion: int | ctypes.c_uint8 192 | MinorLinkerVersion: int | ctypes.c_uint8 193 | SizeOfCode: int | ctypes.c_uint32 194 | SizeOfInitializedData: int | ctypes.c_uint32 195 | SizeOfUninitializedData: int | ctypes.c_uint32 196 | AddressOfEntryPoint: int | ctypes.c_uint32 197 | BaseOfCode: int | ctypes.c_uint32 198 | ImageBase: int | ctypes.c_uint64 199 | SectionAlignment: int | ctypes.c_uint32 200 | FileAlignment: int | ctypes.c_uint32 201 | MajorOperatingSystemVersion: int | ctypes.c_uint16 202 | MinorOperatingSystemVersion: int | ctypes.c_uint16 203 | MajorImageVersion: int | ctypes.c_uint16 204 | MinorImageVersion: int | ctypes.c_uint16 205 | MajorSubsystemVersion: int | ctypes.c_uint16 206 | MinorSubsystemVersion: int | ctypes.c_uint16 207 | Win32VersionValue: int | ctypes.c_uint32 208 | SizeOfImage: int | ctypes.c_uint32 209 | SizeOfHeaders: int | ctypes.c_uint32 210 | CheckSum: int | ctypes.c_uint32 211 | Subsystem: int | ctypes.c_uint16 212 | DllCharacteristics: int | ctypes.c_uint16 213 | SizeOfStackReserve: int | ctypes.c_uint64 214 | SizeOfStackCommit: int | ctypes.c_uint64 215 | SizeOfHeapReserve: int | ctypes.c_uint64 216 | SizeOfHeapCommit: int | ctypes.c_uint64 217 | LoaderFlags: int | ctypes.c_uint32 218 | NumberOfRvaAndSizes: int | ctypes.c_uint32 219 | DataDirectory: typing.Sequence[ImageDataDirectory] | ImageDataDirectory * IMAGE_NUMBEROF_DIRECTORY_ENTRIES 220 | 221 | 222 | class ImageNtHeaders32(ctypes.LittleEndianStructure): 223 | _fields_ = ( 224 | ("Signature", ctypes.c_uint32), 225 | ("FileHeader", ImageFileHeader), 226 | ("OptionalHeader", ImageOptionalHeader32), 227 | ) 228 | Signature: int | ctypes.c_uint32 229 | FileHeader: ImageFileHeader 230 | OptionalHeader: ImageOptionalHeader32 231 | 232 | 233 | class ImageNtHeaders64(ctypes.LittleEndianStructure): 234 | _fields_ = ( 235 | ("Signature", ctypes.c_uint32), 236 | ("FileHeader", ImageFileHeader), 237 | ("OptionalHeader", ImageOptionalHeader64), 238 | ) 239 | Signature: int | ctypes.c_uint32 240 | FileHeader: ImageFileHeader 241 | OptionalHeader: ImageOptionalHeader64 242 | 243 | 244 | class ImageSectionHeader(ctypes.LittleEndianStructure): 245 | _fields_ = ( 246 | ("Name", ctypes.c_char * IMAGE_SIZEOF_SHORT_NAME), 247 | ("VirtualSize", ctypes.c_uint32), 248 | ("VirtualAddress", ctypes.c_uint32), 249 | ("SizeOfRawData", ctypes.c_uint32), 250 | ("PointerToRawData", ctypes.c_uint32), 251 | ("PointerToRelocations", ctypes.c_uint32), 252 | ("PointerToLinenumbers", ctypes.c_uint32), 253 | ("NumberOfRelocations", ctypes.c_uint16), 254 | ("NumberOfLinenumbers", ctypes.c_uint16), 255 | ("Characteristics", ctypes.c_uint32), 256 | ) 257 | Name: bytes | ctypes.c_char * IMAGE_SIZEOF_SHORT_NAME 258 | VirtualSize: int | ctypes.c_uint32 259 | VirtualAddress: int | ctypes.c_uint32 260 | SizeOfRawData: int | ctypes.c_uint32 261 | PointerToRawData: int | ctypes.c_uint32 262 | PointerToRelocations: int | ctypes.c_uint32 263 | PointerToLinenumbers: int | ctypes.c_uint32 264 | NumberOfRelocations: int | ctypes.c_uint16 265 | NumberOfLinenumbers: int | ctypes.c_uint16 266 | Characteristics: int | ctypes.c_uint32 267 | 268 | 269 | class ImageBaseRelocation(ctypes.LittleEndianStructure): 270 | _fields_ = ( 271 | ("VirtualAddress", ctypes.c_uint32), 272 | ("SizeOfBlock", ctypes.c_uint32), 273 | ) 274 | VirtualAddress: int | ctypes.c_uint32 275 | SizeOfBlock: int | ctypes.c_uint32 276 | 277 | 278 | # endregion 279 | 280 | # region x86/x64-specific system ffi definitions 281 | 282 | POINTER_SIZE = ctypes.sizeof(ctypes.c_void_p) 283 | if os.name == 'nt': 284 | crt_malloc = ctypes.cdll.msvcrt.malloc 285 | crt_free = ctypes.cdll.msvcrt.free 286 | 287 | 288 | def allocate_executable_memory(length: int): 289 | virtualalloc = ctypes.windll.kernel32.VirtualAlloc 290 | virtualalloc.argtypes = (ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_uint32) 291 | virtualalloc.restype = ctypes.c_void_p 292 | return ctypes.c_void_p(virtualalloc(0, 293 | length, 294 | 0x3000, # MEM_RESERVE | MEM_COMMIT 295 | 0x40)) # PAGE_EXECUTE_READWRITE 296 | 297 | 298 | def free_executable_memory(ptr: ctypes.c_void_p): 299 | ctypes.windll.kernel32.VirtualFree(ptr, 0, 0x8000) 300 | else: 301 | libc = ctypes.CDLL(ctypes.util.find_library("c")) 302 | crt_malloc = libc.malloc 303 | crt_free = libc.free 304 | 305 | # close enough definitions 306 | libc.memalign.argtypes = ctypes.c_size_t, ctypes.c_size_t 307 | libc.memalign.restype = ctypes.c_size_t 308 | libc.mprotect.argtypes = ctypes.c_size_t, ctypes.c_size_t, ctypes.c_size_t 309 | 310 | 311 | def allocate_executable_memory(length: int): 312 | p = libc.memalign(mmap.PAGESIZE, length) 313 | libc.mprotect(p, length, mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC) 314 | return ctypes.c_void_p(p) 315 | 316 | 317 | def free_executable_memory(ptr: ctypes.c_void_p): 318 | crt_free(ptr.value) 319 | 320 | crt_malloc.argtypes = (ctypes.c_size_t,) 321 | crt_malloc.restype = ctypes.c_size_t 322 | crt_free.argtypes = (ctypes.c_size_t,) 323 | 324 | PyMemoryView_FromMemory = ctypes.pythonapi.PyMemoryView_FromMemory 325 | PyMemoryView_FromMemory.argtypes = (ctypes.c_void_p, ctypes.c_ssize_t, ctypes.c_int) 326 | PyMemoryView_FromMemory.restype = ctypes.py_object 327 | 328 | 329 | # endregion 330 | 331 | # region budget windows stdcall <-> linux cdecl ABI converters 332 | 333 | class PeImage: 334 | def __init__(self, data: bytearray | bytes): 335 | self._data = data if isinstance(data, bytearray) else bytearray(data) 336 | 337 | self.dos = ImageDosHeader.from_buffer(self._data, 0) 338 | if self.dos.e_magic != 0x5a4d: 339 | raise ValueError("bad dos header") 340 | 341 | if POINTER_SIZE == 8: 342 | self.nt = ImageNtHeaders64.from_buffer(self._data, self.dos.e_lfanew) 343 | else: 344 | self.nt = ImageNtHeaders32.from_buffer(self._data, self.dos.e_lfanew) 345 | if self.nt.Signature != 0x4550: 346 | raise ValueError("bad nt header") 347 | 348 | self.sections: typing.Sequence[ImageSectionHeader] | ctypes.Array[ImageSectionHeader] = ( 349 | ImageSectionHeader * self.nt.FileHeader.NumberOfSections).from_buffer( 350 | self._data, self.dos.e_lfanew + ctypes.sizeof(self.nt)) 351 | 352 | self.address: ctypes.c_void_p = allocate_executable_memory(self.nt.OptionalHeader.SizeOfImage) 353 | self.view: memoryview = PyMemoryView_FromMemory( 354 | self.address, 355 | self.nt.OptionalHeader.SizeOfImage, 356 | 0x200, # Read/Write 357 | ) 358 | 359 | self._map_headers_and_sections() 360 | self._relocate() 361 | 362 | def _map_headers_and_sections(self): 363 | ctypes.memmove(self.address, 364 | ctypes.addressof(ctypes.c_byte.from_buffer(self._data)), 365 | self.nt.OptionalHeader.SizeOfHeaders) 366 | for shdr in self.sections: 367 | ctypes.memmove(ctypes.addressof(ctypes.c_byte.from_buffer(self.view, shdr.VirtualAddress)), 368 | ctypes.addressof(ctypes.c_byte.from_buffer(self._data, shdr.PointerToRawData)), 369 | min(shdr.SizeOfRawData, shdr.VirtualSize)) 370 | 371 | def _relocate(self): 372 | rva = int(self.nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress) 373 | rva_to = rva + int(self.nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size) 374 | displacement = self.address.value - self.nt.OptionalHeader.ImageBase 375 | while rva < rva_to: 376 | page = ctypes.cast(ctypes.c_void_p(self.address.value + rva), ctypes.POINTER(ImageBaseRelocation)).contents 377 | page_data = ctypes.cast(ctypes.c_void_p(self.address.value + rva + ctypes.sizeof(page)), 378 | ctypes.POINTER(ctypes.c_uint16 * ((page.SizeOfBlock - ctypes.sizeof(page)) // 2)) 379 | ).contents 380 | for relo in page_data: 381 | absptr_address = self.address.value + page.VirtualAddress + (relo & 0xFFF) 382 | if relo >> 12 == 0: 383 | pass 384 | elif relo >> 12 == 3: 385 | ptr = ctypes.cast(absptr_address, ctypes.POINTER(ctypes.c_uint32)) 386 | ctypes.memmove(absptr_address, 387 | ctypes.addressof(ctypes.c_uint32(ptr.contents.value + displacement)), 4) 388 | elif relo >> 12 == 10: 389 | ptr = ctypes.cast(absptr_address, ctypes.POINTER(ctypes.c_uint64)) 390 | ctypes.memmove(absptr_address, 391 | ctypes.addressof(ctypes.c_uint64(ptr.contents.value + displacement)), 8) 392 | else: 393 | raise RuntimeError("Unsupported relocation type") 394 | rva += page.SizeOfBlock 395 | 396 | def section_header(self, name: bytes): 397 | try: 398 | return next(s for s in self.sections if s.Name == name) 399 | except StopIteration: 400 | return KeyError 401 | 402 | def section(self, section: bytes | ImageSectionHeader) -> memoryview: 403 | if not isinstance(section, ImageSectionHeader): 404 | section = self.section_header(section) 405 | return self.view[section.VirtualAddress:section.VirtualAddress + section.VirtualSize] 406 | 407 | def resolve_rip_relative(self, addr: int): 408 | if self.view[addr] in (0xE8, 0xE9): 409 | return addr + 5 + int.from_bytes(self.view[addr + 1:addr + 5], "little", signed=True) 410 | else: 411 | raise NotImplementedError 412 | 413 | 414 | class StdCallFunc32ByPythonFunction: 415 | def __init__(self, pyctypefn, fn: callable, arglen: int, name: str): 416 | self._inner = pyctypefn(fn) 417 | self._fn = fn 418 | self._name = name 419 | inner_address = ctypes.cast(self._inner, ctypes.c_void_p) 420 | 421 | codelen = 1 + arglen // 4 * 7 + 5 + 2 + 6 + 3 422 | codeptr = allocate_executable_memory(codelen) 423 | buf = (ctypes.c_uint8 * codelen).from_address(codeptr.value) 424 | buf[0] = 0x90 425 | i = 1 426 | for j in range(0, arglen, 4): 427 | buf[i] = 0xff 428 | buf[i + 1] = 0xb4 429 | buf[i + 2] = 0x24 430 | ctypes.c_uint32.from_address(codeptr.value + i + 3).value = arglen 431 | i += 7 432 | 433 | buf[i] = 0xb8 434 | ctypes.c_void_p.from_address(codeptr.value + i + 1).value = inner_address.value 435 | i += 5 436 | 437 | buf[i] = 0xff 438 | buf[i + 1] = 0xd0 439 | i += 2 440 | 441 | buf[i + 0] = 0x81 442 | buf[i + 1] = 0xc4 443 | ctypes.c_uint32.from_address(codeptr.value + i + 2).value = arglen 444 | i += 6 445 | 446 | buf[i] = 0xc2 447 | ctypes.c_uint16.from_address(codeptr.value + i + 1).value = arglen 448 | 449 | self._address = codeptr 450 | 451 | def address(self): 452 | return self._address 453 | 454 | def __call__(self, *args): 455 | return self._fn(*args) 456 | 457 | 458 | class StdCallFunc64ByPythonFunction: 459 | def __init__(self, pyctypefn, fn: callable, arglen: int, name: str): 460 | self._inner = pyctypefn(fn) 461 | self._fn = fn 462 | self._name = name 463 | inner_address = ctypes.cast(self._inner, ctypes.c_void_p) 464 | 465 | cmds = [ 466 | b"\x48\x83\xec\x38", # sub rsp, 0x38 467 | b"\x57", # push rdi 468 | b"\x56", # push rsi 469 | b"\x48\x89\xCF", # mov rdi, rcx 470 | b"\x48\x89\xD6", # mov rsi, rdx 471 | b"\x4C\x89\xC2", # mov rdx, r8 472 | b"\x4C\x89\xC9", # mov rcx, r9 473 | b"\x48\xB8", inner_address.value.to_bytes(8, "little"), # movabs rax, 0x0 474 | b"\xFF\xD0", # call rax 475 | b"\x5E", # pop rsi 476 | b"\x5F", # pop rdi 477 | b"\x48\x83\xc4\x38", # add rsp, 0x38 478 | b"\xC3", # ret 479 | ] 480 | 481 | cmds = bytearray().join(cmds) 482 | self._address = allocate_executable_memory(len(cmds)) 483 | ctypes.memmove(self._address.value, 484 | ctypes.addressof((ctypes.c_uint8 * len(cmds)).from_buffer(cmds)), 485 | len(cmds)) 486 | 487 | def address(self): 488 | return self._address 489 | 490 | def __call__(self, *args): 491 | return self._fn(*args) 492 | 493 | 494 | class StdCallFunc32ByFunctionPointer: 495 | def __init__(self, ptr: int, argtypes, noargtypefn, name: str): 496 | self._name = name 497 | self._ptr = ptr 498 | self._argtypes = argtypes 499 | self._codelen = 1 + sum((ctypes.sizeof(argtype) + 3) // 4 * 5 for argtype in argtypes) + 8 500 | self._noargtypefn = noargtypefn 501 | 502 | def address(self): 503 | return ctypes.c_void_p(self._ptr) 504 | 505 | def __call__(self, *args): 506 | codeptr = allocate_executable_memory(self._codelen) 507 | buf = (ctypes.c_uint8 * self._codelen).from_address(codeptr.value) 508 | buf[0] = 0x90 509 | i = 1 510 | for argtype, arg in zip(reversed(self._argtypes), reversed(args)): 511 | arglen = ctypes.sizeof(argtype) 512 | if not isinstance(arg, argtype): 513 | arg = argtype(arg) 514 | argb = (ctypes.c_uint8 * arglen).from_address(ctypes.addressof(arg)) 515 | j = 0 516 | while j < arglen - 3: 517 | buf[i] = 0x68 518 | buf[i + 1] = argb[0] 519 | buf[i + 2] = argb[1] 520 | buf[i + 3] = argb[2] 521 | buf[i + 4] = argb[3] 522 | i += 5 523 | j += 4 524 | if j != arglen: 525 | buf[i] = 0x68 526 | buf[i + 1] = argb[0] 527 | buf[i + 2] = argb[1] if j + 1 <= arglen else 0 528 | buf[i + 3] = argb[2] if j + 2 <= arglen else 0 529 | buf[i + 4] = argb[3] if j + 3 <= arglen else 0 530 | i += 5 531 | buf[i] = 0xb8 532 | ctypes.c_uint32.from_address(codeptr.value + i + 1).value = self._ptr 533 | buf[i + 5] = 0xff 534 | buf[i + 6] = 0xd0 535 | buf[i + 7] = 0xc3 536 | 537 | res = self._noargtypefn(codeptr.value)() 538 | free_executable_memory(codeptr) 539 | 540 | return res 541 | 542 | 543 | class StdCallFunc64ByFunctionPointer: 544 | def __init__(self, ptr: int, argtypes, noargtypefn, name: str): 545 | self._ptr = ptr 546 | self._argtypes = argtypes 547 | self._noargtypefn = noargtypefn 548 | self._name = name 549 | 550 | movabs_regs = ( 551 | b"\x48\xb9", # movabs rcx, imm 552 | b"\x48\xba", # movabs rdx, imm 553 | b"\x49\xb8", # movabs r8, imm 554 | b"\x49\xb9", # movabs r9, imm 555 | ) 556 | 557 | cmds = [ 558 | b"\x57", # push rdi 559 | b"\x56", # push rsi 560 | 561 | # sub rsp, imm 562 | b"\x48\x83\xec", 563 | (0x8 + (len(argtypes) + 1) // 2 * 2 * 8).to_bytes(1, "little"), 564 | ] 565 | self._offsets = [] 566 | 567 | for i, argtype in enumerate(self._argtypes): 568 | if i < len(movabs_regs): 569 | cmds.append(movabs_regs[i]) 570 | self._offsets.append(sum(len(x) for x in cmds)) 571 | cmds.append(bytes(8)) 572 | else: 573 | # movabs rax, imm 574 | cmds.append(b"\x48\xb8") 575 | self._offsets.append(sum(len(x) for x in cmds)) 576 | cmds.append(bytes(8)) 577 | 578 | # mov qword ptr [rsp + N], rax 579 | cmds.append(b"\x48\x89\x44\x24") 580 | cmds.append((i * 8).to_bytes(1, "little")) 581 | 582 | # movabs rax, imm 583 | cmds.append(b"\x48\xb8") 584 | cmds.append(self._ptr.to_bytes(8, "little")) 585 | 586 | cmds.append(b"\xff\xd0") # call rax 587 | 588 | # add rsp, imm 589 | cmds.append(b"\x48\x83\xc4" + (0x8 + (len(argtypes) + 1) // 2 * 2 * 8).to_bytes(1, "little")) 590 | 591 | cmds.append(b"\x5e") # pop rsi 592 | cmds.append(b"\x5f") # pop rdi 593 | cmds.append(b"\xc3") # ret 594 | 595 | self._template = bytearray().join(cmds) 596 | 597 | def address(self): 598 | return ctypes.c_void_p(self._ptr) 599 | 600 | def __call__(self, *args): 601 | codeptr = allocate_executable_memory(len(self._template)) 602 | ctypes.memmove(codeptr.value, 603 | ctypes.addressof(ctypes.c_uint8.from_buffer(self._template)), 604 | len(self._template)) 605 | 606 | cmd = self._template 607 | for argtype, arg, offset in zip(self._argtypes, args, self._offsets): 608 | arglen = ctypes.sizeof(argtype) 609 | if not isinstance(arg, argtype): 610 | arg = argtype(arg) 611 | ctypes.memmove(codeptr.value + offset, ctypes.addressof(arg), arglen) 612 | 613 | res = self._noargtypefn(codeptr.value)() 614 | free_executable_memory(codeptr) 615 | return res 616 | 617 | 618 | class StdCallFunc32Type: 619 | def __init__(self, restype, *argtypes, name: typing.Optional[str] = None): 620 | self._name = name or ("(" + ", ".join(str(x) for x in (restype, *argtypes)) + ")") 621 | self._restype = restype 622 | self._argtypes = argtypes 623 | self._arglen = sum((ctypes.sizeof(argtype) + 3) // 4 * 4 for argtype in self._argtypes) 624 | self._noarg_type = ctypes.CFUNCTYPE(restype) 625 | self._pytype = ctypes.CFUNCTYPE(restype, *argtypes) 626 | 627 | def __call__(self, ptr): 628 | if callable(ptr): 629 | return StdCallFunc32ByPythonFunction(self._pytype, ptr, self._arglen, self._name) 630 | elif isinstance(ptr, int): 631 | return StdCallFunc32ByFunctionPointer(ptr, self._argtypes, self._noarg_type, self._name) 632 | else: 633 | raise TypeError 634 | 635 | 636 | class StdCallFunc64Type: 637 | def __init__(self, restype, *argtypes, name: typing.Optional[str] = None): 638 | self._name = name or ("(" + ", ".join(str(x) for x in (restype, *argtypes)) + ")") 639 | self._restype = restype 640 | self._argtypes = argtypes 641 | self._arglen = sum((ctypes.sizeof(argtype) + 3) // 4 * 4 for argtype in self._argtypes) 642 | self._noarg_type = ctypes.CFUNCTYPE(restype) 643 | self._pytype = ctypes.CFUNCTYPE(restype, *argtypes) 644 | 645 | def __call__(self, ptr): 646 | if callable(ptr): 647 | return StdCallFunc64ByPythonFunction(self._pytype, ptr, self._arglen, self._name) 648 | elif isinstance(ptr, int): 649 | return StdCallFunc64ByFunctionPointer(ptr, self._argtypes, self._noarg_type, self._name) 650 | else: 651 | raise TypeError 652 | 653 | 654 | if POINTER_SIZE == 4: 655 | StdCallFuncType = StdCallFunc32Type 656 | else: 657 | StdCallFuncType = StdCallFunc64Type 658 | 659 | # endregion 660 | 661 | # region Oodle typedefs 662 | 663 | OodleNetwork1_Shared_Size = StdCallFuncType(ctypes.c_int32, ctypes.c_int32, name="OodleNetwork1_Shared_Size") 664 | OodleNetwork1_Shared_SetWindow = StdCallFuncType(None, ctypes.c_void_p, ctypes.c_int32, ctypes.c_void_p, ctypes.c_int32, 665 | name="OodleNetwork1_Shared_SetWindow") 666 | OodleNetwork1_Proto_Train = StdCallFuncType(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), 667 | ctypes.POINTER(ctypes.c_int32), ctypes.c_int32, 668 | name="OodleNetwork1_Proto_Train") 669 | OodleNetwork1_Proto_Decode = StdCallFuncType(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, 670 | ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, 671 | name="OodleNetwork1_Proto_Decode") 672 | OodleNetwork1_Proto_Encode = StdCallFuncType(ctypes.c_int32, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, 673 | ctypes.c_size_t, ctypes.c_void_p, name="OodleNetwork1_Proto_Encode") 674 | OodleNetwork1_Proto_State_Size = StdCallFuncType(ctypes.c_int32, name="OodleNetwork1_Proto_State_Size") 675 | Oodle_Malloc = StdCallFuncType(ctypes.c_size_t, ctypes.c_size_t, ctypes.c_int32, name="Oodle_Malloc") 676 | Oodle_Free = StdCallFuncType(None, ctypes.c_size_t, name="Oodle_Free") 677 | Oodle_SetMallocFree = StdCallFuncType(None, ctypes.c_void_p, ctypes.c_void_p, name="Oodle_SetMallocFree") 678 | 679 | 680 | # endregion 681 | 682 | # region Oodle wrappers 683 | 684 | def oodle_malloc_impl(size: int, align: int) -> int: 685 | raw = crt_malloc(size + align + POINTER_SIZE - 1) 686 | if raw == 0: 687 | return 0 688 | 689 | aligned = (raw + align + POINTER_SIZE - 1) & ((~align & (sys.maxsize * 2 + 1)) + 1) 690 | ctypes.c_void_p.from_address(aligned - POINTER_SIZE).value = raw 691 | return aligned 692 | 693 | 694 | def oodle_free_impl(aligned: int): 695 | crt_free(ctypes.c_void_p.from_address(aligned - POINTER_SIZE).value) 696 | 697 | 698 | class OodleModule: 699 | def __init__(self, image: PeImage): 700 | self._image = image 701 | 702 | text = image.section_header(b".text") 703 | text_view = image.section(text) 704 | 705 | if POINTER_SIZE == 8: 706 | pattern = br"\x75.\x48\x8d\x15....\x48\x8d\x0d....\xe8(....)\xc6\x05....\x01.{0,256}\x75.\xb9(....)\xe8(....)\x45\x33\xc0\x33\xd2\x48\x8b\xc8\xe8.....{0,6}\x41\xb9(....)\xba.....{0,6}\x48\x8b\xc8\xe8(....)" 707 | else: 708 | pattern = br"\x75\x16\x68....\x68....\xe8(....)\xc6\x05....\x01.{0,256}\x75\x27\x6a(.)\xe8(....)\x6a\x00\x6a\x00\x50\xe8....\x83\xc4.\x89\x46.\x68(....)\xff\x76.\x6a.\x50\xe8(....)" 709 | match = re.search(pattern, text_view, re.DOTALL) 710 | if not match: 711 | raise RuntimeError("Could not find InitOodle.") 712 | self.set_malloc_free_address = image.address.value + image.resolve_rip_relative( 713 | text.VirtualAddress + match.start(1) - 1) 714 | self.htbits = int.from_bytes(match.group(2), "little") 715 | self.shared_size_address = image.address.value + image.resolve_rip_relative( 716 | text.VirtualAddress + match.start(3) - 1) 717 | self.window = int.from_bytes(match.group(4), "little") 718 | self.shared_set_window_address = image.address.value + image.resolve_rip_relative( 719 | text.VirtualAddress + match.start(5) - 1) 720 | 721 | if POINTER_SIZE == 8: 722 | pattern = br"\x75\x04\x48\x89\x7e.\xe8(....)\x4c..\xe8(....).{0,256}\x01\x75\x0a\x48\x8b\x0f\xe8(....)\xeb\x09\x48\x8b\x4f\x08\xe8(....)" 723 | else: 724 | pattern = br"\xe8(....)\x8b\xd8\xe8(....)\x83\x7d\x10\x01.{0,256}\x83\x7d\x10\x01\x6a\x00\x6a\x00\x6a\x00\xff\x77.\x75\x09\xff.\xe8(....)\xeb\x08\xff\x76.\xe8(....)" 725 | match = re.search(pattern, text_view, re.DOTALL) 726 | if not match: 727 | raise RuntimeError("Could not find SetUpStatesAndTrain.") 728 | self.udp_state_size_address = image.address.value + image.resolve_rip_relative( 729 | text.VirtualAddress + match.start(1) - 1) 730 | self.tcp_state_size_address = image.address.value + image.resolve_rip_relative( 731 | text.VirtualAddress + match.start(2) - 1) 732 | self.tcp_train_address = image.address.value + image.resolve_rip_relative( 733 | text.VirtualAddress + match.start(3) - 1) 734 | self.udp_train_address = image.address.value + image.resolve_rip_relative( 735 | text.VirtualAddress + match.start(4) - 1) 736 | 737 | if POINTER_SIZE == 8: 738 | match = re.search(br"\x4d\x85\xd2\x74\x0a\x49\x8b\xca\xe8(....)\xeb\x09\x48\x8b\x49\x08\xe8(....)", 739 | text_view, re.DOTALL) 740 | if not match: 741 | raise RuntimeError("Could not find Tcp/UdpDecode.") 742 | self.tcp_decode_address = image.address.value + image.resolve_rip_relative( 743 | text.VirtualAddress + match.start(1) - 1) 744 | self.udp_decode_address = image.address.value + image.resolve_rip_relative( 745 | text.VirtualAddress + match.start(2) - 1) 746 | 747 | match = re.search( 748 | br"\x48\x85\xc0\x74\x0d\x48\x8b\xc8\xe8(....)\x48..\xeb\x0b\x48\x8b\x49\x08\xe8(....)", 749 | text_view, re.DOTALL) 750 | if not match: 751 | raise RuntimeError("Could not find Tcp/UdpEncode.") 752 | self.tcp_encode_address = image.address.value + image.resolve_rip_relative( 753 | text.VirtualAddress + match.start(1) - 1) 754 | self.udp_encode_address = image.address.value + image.resolve_rip_relative( 755 | text.VirtualAddress + match.start(2) - 1) 756 | else: 757 | pattern = re.compile(br"\x85\xc0\x74.\x50\xe8(....)\x57\x8b\xf0\xff\x15", re.DOTALL) 758 | match = pattern.search(text_view) 759 | if not match: 760 | raise RuntimeError("Could not find TcpEncode.") 761 | self.tcp_encode_address = image.address.value + image.resolve_rip_relative( 762 | text.VirtualAddress + match.start(1) - 1) 763 | match = pattern.search(text_view, match.end()) 764 | if not match: 765 | raise RuntimeError("Could not find TcpDecode.") 766 | self.tcp_decode_address = image.address.value + image.resolve_rip_relative( 767 | text.VirtualAddress + match.start(1) - 1) 768 | 769 | pattern = re.compile(br"\xff\x71\x04\xe8(....)\x57\x8b\xf0\xff\x15", re.DOTALL) 770 | match = pattern.search(text_view) 771 | if not match: 772 | raise RuntimeError("Could not find UdpEncode.") 773 | self.udp_encode_address = image.address.value + image.resolve_rip_relative( 774 | text.VirtualAddress + match.start(1) - 1) 775 | match = pattern.search(text_view, match.end()) 776 | if not match: 777 | raise RuntimeError("Could not find UdpDecode.") 778 | self.udp_decode_address = image.address.value + image.resolve_rip_relative( 779 | text.VirtualAddress + match.start(1) - 1) 780 | 781 | self.set_malloc_free = Oodle_SetMallocFree(self.set_malloc_free_address) 782 | self.shared_size = OodleNetwork1_Shared_Size(self.shared_size_address) 783 | self.shared_set_window = OodleNetwork1_Shared_SetWindow(self.shared_set_window_address) 784 | self.udp_state_size = OodleNetwork1_Proto_State_Size(self.udp_state_size_address) 785 | self.tcp_state_size = OodleNetwork1_Proto_State_Size(self.tcp_state_size_address) 786 | self.tcp_train = OodleNetwork1_Proto_Train(self.tcp_train_address) 787 | self.udp_train = OodleNetwork1_Proto_Train(self.udp_train_address) 788 | self.tcp_decode = OodleNetwork1_Proto_Decode(self.tcp_decode_address) 789 | self.udp_decode = OodleNetwork1_Proto_Decode(self.udp_decode_address) 790 | self.tcp_encode = OodleNetwork1_Proto_Encode(self.tcp_encode_address) 791 | self.udp_encode = OodleNetwork1_Proto_Encode(self.udp_encode_address) 792 | 793 | if POINTER_SIZE == 8: 794 | # patch _alloca_probe 795 | pattern = br"\x48\x83\xec\x10\x4c\x89\x14\x24\x4c\x89\x5c\x24\x08\x4d\x33\xdb" 796 | match = re.search(pattern, text_view) 797 | if not match: 798 | raise RuntimeError("_alloca_probe not found") 799 | image.view[text.VirtualAddress + match.start(0)] = 0xc3 800 | 801 | else: 802 | # patch fs register access 803 | ctypes.memset(self.tcp_train_address + 0xaba - 0xAB0, 0x90, 6) 804 | ctypes.memset(self.tcp_train_address + 0xad2 - 0xAB0, 0x90, 6) 805 | ctypes.memset(self.tcp_train_address + 0xbb4 - 0xAB0, 0x90, 7) 806 | ctypes.memset(self.tcp_train_address + 0xbc8 - 0xAB0, 0x90, 7) 807 | 808 | self._c_oodle_malloc_impl = Oodle_Malloc(oodle_malloc_impl) 809 | self._c_oodle_free_impl = Oodle_Free(oodle_free_impl) 810 | 811 | self.set_malloc_free(self._c_oodle_malloc_impl.address(), self._c_oodle_free_impl.address()) 812 | 813 | 814 | class OodleInstance: 815 | def __init__(self, module: OodleModule, use_tcp: bool): 816 | self._state = (ctypes.c_uint8 * (module.tcp_state_size() if use_tcp else module.udp_state_size()))() 817 | self._shared = (ctypes.c_uint8 * module.shared_size(module.htbits))() 818 | self._window = (ctypes.c_uint8 * module.window)() 819 | module.shared_set_window( 820 | ctypes.addressof(self._shared), module.htbits, 821 | ctypes.addressof(self._window), len(self._window)) 822 | (module.tcp_train if use_tcp else module.udp_train)( 823 | ctypes.addressof(self._state), 824 | ctypes.addressof(self._shared), 825 | ctypes.POINTER(ctypes.c_void_p)(), 826 | ctypes.POINTER(ctypes.c_int32)(), 827 | 0) 828 | self._encode_function = module.tcp_encode if use_tcp else module.udp_encode 829 | self._decode_function = module.tcp_decode if use_tcp else module.udp_decode 830 | 831 | def encode(self, src: bytes | bytearray | memoryview) -> bytearray: 832 | if not isinstance(src, (bytearray, memoryview)): 833 | src = bytearray(src) 834 | enc = bytearray(len(src) + 8) 835 | del enc[self._encode_function( 836 | ctypes.addressof(self._state), 837 | ctypes.addressof(self._shared), 838 | ctypes.addressof(ctypes.c_byte.from_buffer(src)), len(src), 839 | ctypes.addressof(ctypes.c_byte.from_buffer(enc))):] 840 | return enc 841 | 842 | def decode(self, enc: bytes | bytearray | memoryview, result_length: int) -> bytearray: 843 | dec = bytearray(result_length) 844 | if not self._decode_function( 845 | ctypes.addressof(self._state), 846 | ctypes.addressof(self._shared), 847 | ctypes.addressof(ctypes.c_byte.from_buffer(enc)), len(enc), 848 | ctypes.addressof(ctypes.c_byte.from_buffer(dec)), len(dec)): 849 | raise RuntimeError("Oodle decode fail") 850 | return dec 851 | 852 | 853 | # endregion 854 | 855 | def __main__(): 856 | if POINTER_SIZE not in (4, 8): 857 | raise NotImplementedError 858 | 859 | img = PeImage(pathlib.Path("ffxiv_dx11.exe" if POINTER_SIZE == 8 else "ffxiv.exe").read_bytes()) 860 | oodle_module = OodleModule(img) 861 | 862 | test_data = b'000003211561116515468046454464656456456456' * 16 863 | 864 | oodle_udp = OodleInstance(oodle_module, False) 865 | if test_data != oodle_udp.decode(oodle_udp.encode(test_data), len(test_data)): 866 | print("Fail UDP") 867 | 868 | oodle_tcp1 = OodleInstance(oodle_module, True) 869 | oodle_tcp2 = OodleInstance(oodle_module, True) 870 | while True: 871 | test_data = secrets.token_bytes(16384) 872 | if test_data != oodle_tcp2.decode(xd := oodle_tcp1.encode(test_data), len(test_data)): 873 | print("Fail TCP") 874 | 875 | print("OK") 876 | 877 | 878 | if __name__ == "__main__": 879 | exit(__main__()) 880 | -------------------------------------------------------------------------------- /mitigate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sudo python 2 | import argparse 3 | import collections 4 | import contextlib 5 | import ctypes 6 | import ctypes.util 7 | import dataclasses 8 | import datetime 9 | import enum 10 | import errno 11 | import io 12 | import ipaddress 13 | import json 14 | import logging.handlers 15 | import os 16 | import pathlib 17 | import random 18 | import re 19 | import signal 20 | import socket 21 | import struct 22 | import sys 23 | import time 24 | import typing 25 | import urllib.request 26 | 27 | import math 28 | import mmap 29 | import select 30 | import zlib 31 | 32 | # region Miscellaneous constants and typedefs 33 | 34 | ACTION_ID_AUTO_ATTACK = 0x0007 35 | ACTION_ID_AUTO_ATTACK_MCH = 0x0008 36 | AUTO_ATTACK_DELAY = 0.1 37 | SO_ORIGINAL_DST = 80 38 | OPCODE_DEFINITION_LIST_URL = "https://api.github.com/repos/Soreepeong/XivAlexander/contents/StaticData/OpcodeDefinition" 39 | SCRIPT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 40 | 41 | EXTRA_DELAY_HELP = """Server responses have been usually taking between 50ms and 100ms on below-1ms latency to server, so 75ms is a good average. 42 | The server will do sanity check on the frequency of action use requests, 43 | and it's very easy to identify whether you're trying to go below allowed minimum value. 44 | This addon is already in gray area. Do NOT decrease this value. You've been warned. 45 | Feel free to increase and see how does it feel like to play on high latency instead, though.""" 46 | 47 | T = typing.TypeVar("T") 48 | ArgumentTuple = collections.namedtuple( 49 | "ArgumentTuple", 50 | ("region", "extra_delay", "measure_ping", "update_opcodes", "write_sysctl", "json_path") 51 | ) 52 | 53 | 54 | def clamp(v: T, min_: T, max_: T) -> T: 55 | return max(min_, min(max_, v)) 56 | 57 | 58 | class InvalidDataException(ValueError): 59 | pass 60 | 61 | 62 | class RootRequiredError(RuntimeError): 63 | pass 64 | 65 | 66 | class TcpInfo(ctypes.Structure): 67 | """TCP_INFO struct in linux 4.2 68 | see /usr/include/linux/tcp.h for details""" 69 | 70 | __u8 = ctypes.c_uint8 71 | __u32 = ctypes.c_uint32 72 | __u64 = ctypes.c_uint64 73 | 74 | _fields_ = [ 75 | ("tcpi_state", __u8), 76 | ("tcpi_ca_state", __u8), 77 | ("tcpi_retransmits", __u8), 78 | ("tcpi_probes", __u8), 79 | ("tcpi_backoff", __u8), 80 | ("tcpi_options", __u8), 81 | ("tcpi_snd_wscale", __u8, 4), ("tcpi_rcv_wscale", __u8, 4), 82 | 83 | ("tcpi_rto", __u32), 84 | ("tcpi_ato", __u32), 85 | ("tcpi_snd_mss", __u32), 86 | ("tcpi_rcv_mss", __u32), 87 | 88 | ("tcpi_unacked", __u32), 89 | ("tcpi_sacked", __u32), 90 | ("tcpi_lost", __u32), 91 | ("tcpi_retrans", __u32), 92 | ("tcpi_fackets", __u32), 93 | 94 | # Times 95 | ("tcpi_last_data_sent", __u32), 96 | ("tcpi_last_ack_sent", __u32), 97 | ("tcpi_last_data_recv", __u32), 98 | ("tcpi_last_ack_recv", __u32), 99 | # Metrics 100 | ("tcpi_pmtu", __u32), 101 | ("tcpi_rcv_ssthresh", __u32), 102 | ("tcpi_rtt", __u32), 103 | ("tcpi_rttvar", __u32), 104 | ("tcpi_snd_ssthresh", __u32), 105 | ("tcpi_snd_cwnd", __u32), 106 | ("tcpi_advmss", __u32), 107 | ("tcpi_reordering", __u32), 108 | 109 | ("tcpi_rcv_rtt", __u32), 110 | ("tcpi_rcv_space", __u32), 111 | 112 | ("tcpi_total_retrans", __u32), 113 | 114 | ("tcpi_pacing_rate", __u64), 115 | ("tcpi_max_pacing_rate", __u64), 116 | 117 | # RFC4898 tcpEStatsAppHCThruOctetsAcked 118 | ("tcpi_bytes_acked", __u64), 119 | # RFC4898 tcpEStatsAppHCThruOctetsReceived 120 | ("tcpi_bytes_received", __u64), 121 | # RFC4898 tcpEStatsPerfSegsOut 122 | ("tcpi_segs_out", __u32), 123 | # RFC4898 tcpEStatsPerfSegsIn 124 | ("tcpi_segs_in", __u32), 125 | ] 126 | del __u8, __u32, __u64 127 | 128 | def __repr__(self): 129 | keyval = ["{}={!r}".format(x[0], getattr(self, x[0])) 130 | for x in self._fields_] 131 | fields = ", ".join(keyval) 132 | return "{}({})".format(self.__class__.__name__, fields) 133 | 134 | @classmethod 135 | def from_socket(cls, sock: socket.socket): 136 | """Takes a socket, and attempts to get TCP_INFO stats on it. Returns a 137 | TcpInfo struct""" 138 | # http://linuxgazette.net/136/pfeiffer.html 139 | padsize = ctypes.sizeof(TcpInfo) 140 | data = sock.getsockopt(socket.SOL_TCP, socket.TCP_INFO, padsize) 141 | # On older kernels, we get fewer bytes, pad with null to fit 142 | padded = data.ljust(padsize, b'\0') 143 | return cls.from_buffer_copy(padded) 144 | 145 | @classmethod 146 | def get_latency(cls, sock: socket.socket) -> typing.Optional[float]: 147 | info = cls.from_socket(sock) 148 | if info.tcpi_rtt: 149 | return info.tcpi_rtt / 1000000 150 | else: 151 | return None 152 | 153 | 154 | # endregion 155 | 156 | # region PE Structures 157 | 158 | IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16 159 | IMAGE_DIRECTORY_ENTRY_BASERELOC = 5 160 | IMAGE_SIZEOF_SHORT_NAME = 8 161 | 162 | 163 | class ImageDosHeader(ctypes.LittleEndianStructure): 164 | _fields_ = ( 165 | ("e_magic", ctypes.c_uint16), 166 | ("e_cblp", ctypes.c_uint16), 167 | ("e_cp", ctypes.c_uint16), 168 | ("e_crlc", ctypes.c_uint16), 169 | ("e_cparhdr", ctypes.c_uint16), 170 | ("e_minalloc", ctypes.c_uint16), 171 | ("e_maxalloc", ctypes.c_uint16), 172 | ("e_ss", ctypes.c_uint16), 173 | ("e_sp", ctypes.c_uint16), 174 | ("e_csum", ctypes.c_uint16), 175 | ("e_ip", ctypes.c_uint16), 176 | ("e_cs", ctypes.c_uint16), 177 | ("e_lfarlc", ctypes.c_uint16), 178 | ("e_ovno", ctypes.c_uint16), 179 | ("e_res", ctypes.c_uint16 * 4), 180 | ("e_oemid", ctypes.c_uint16), 181 | ("e_oeminfo", ctypes.c_uint16), 182 | ("e_res2", ctypes.c_uint16 * 10), 183 | ("e_lfanew", ctypes.c_uint32), 184 | ) 185 | e_magic: typing.Union[int, ctypes.c_uint16] 186 | e_cblp: typing.Union[int, ctypes.c_uint16] 187 | e_cp: typing.Union[int, ctypes.c_uint16] 188 | e_crlc: typing.Union[int, ctypes.c_uint16] 189 | e_cparhdr: typing.Union[int, ctypes.c_uint16] 190 | e_minalloc: typing.Union[int, ctypes.c_uint16] 191 | e_maxalloc: typing.Union[int, ctypes.c_uint16] 192 | e_ss: typing.Union[int, ctypes.c_uint16] 193 | e_sp: typing.Union[int, ctypes.c_uint16] 194 | e_csum: typing.Union[int, ctypes.c_uint16] 195 | e_ip: typing.Union[int, ctypes.c_uint16] 196 | e_cs: typing.Union[int, ctypes.c_uint16] 197 | e_lfarlc: typing.Union[int, ctypes.c_uint16] 198 | e_ovno: typing.Union[int, ctypes.c_uint16] 199 | e_res: typing.Union[typing.Sequence[int], ctypes.c_uint16 * 4] 200 | e_oemid: typing.Union[int, ctypes.c_uint16] 201 | e_oeminfo: typing.Union[int, ctypes.c_uint16] 202 | e_res2: typing.Union[typing.Sequence[int], ctypes.c_uint16 * 10] 203 | e_lfanew: typing.Union[int, ctypes.c_uint32] 204 | 205 | 206 | class ImageFileHeader(ctypes.LittleEndianStructure): 207 | _fields_ = ( 208 | ("Machine", ctypes.c_uint16), 209 | ("NumberOfSections", ctypes.c_uint16), 210 | ("TimeDateStamp", ctypes.c_uint32), 211 | ("PointerToSymbolTable", ctypes.c_uint32), 212 | ("NumberOfSymbols", ctypes.c_uint32), 213 | ("SizeOfOptionalHeader", ctypes.c_uint16), 214 | ("Characteristics", ctypes.c_uint16), 215 | ) 216 | Machine: typing.Union[int, ctypes.c_uint16] 217 | NumberOfSections: typing.Union[int, ctypes.c_uint16] 218 | TimeDateStamp: typing.Union[int, ctypes.c_uint32] 219 | PointerToSymbolTable: typing.Union[int, ctypes.c_uint32] 220 | NumberOfSymbols: typing.Union[int, ctypes.c_uint32] 221 | SizeOfOptionalHeader: typing.Union[int, ctypes.c_uint16] 222 | Characteristics: typing.Union[int, ctypes.c_uint16] 223 | 224 | 225 | class ImageDataDirectory(ctypes.LittleEndianStructure): 226 | _fields_ = ( 227 | ("VirtualAddress", ctypes.c_uint32), 228 | ("Size", ctypes.c_uint32), 229 | ) 230 | VirtualAddress: typing.Union[int, ctypes.c_uint32] 231 | Size: typing.Union[int, ctypes.c_uint32] 232 | 233 | 234 | class ImageOptionalHeader32(ctypes.LittleEndianStructure): 235 | _fields_ = ( 236 | ("Magic", ctypes.c_uint16), 237 | ("MajorLinkerVersion", ctypes.c_uint8), 238 | ("MinorLinkerVersion", ctypes.c_uint8), 239 | ("SizeOfCode", ctypes.c_uint32), 240 | ("SizeOfInitializedData", ctypes.c_uint32), 241 | ("SizeOfUninitializedData", ctypes.c_uint32), 242 | ("AddressOfEntryPoint", ctypes.c_uint32), 243 | ("BaseOfCode", ctypes.c_uint32), 244 | ("BaseOfData", ctypes.c_uint32), 245 | ("ImageBase", ctypes.c_uint32), 246 | ("SectionAlignment", ctypes.c_uint32), 247 | ("FileAlignment", ctypes.c_uint32), 248 | ("MajorOperatingSystemVersion", ctypes.c_uint16), 249 | ("MinorOperatingSystemVersion", ctypes.c_uint16), 250 | ("MajorImageVersion", ctypes.c_uint16), 251 | ("MinorImageVersion", ctypes.c_uint16), 252 | ("MajorSubsystemVersion", ctypes.c_uint16), 253 | ("MinorSubsystemVersion", ctypes.c_uint16), 254 | ("Win32VersionValue", ctypes.c_uint32), 255 | ("SizeOfImage", ctypes.c_uint32), 256 | ("SizeOfHeaders", ctypes.c_uint32), 257 | ("CheckSum", ctypes.c_uint32), 258 | ("Subsystem", ctypes.c_uint16), 259 | ("DllCharacteristics", ctypes.c_uint16), 260 | ("SizeOfStackReserve", ctypes.c_uint32), 261 | ("SizeOfStackCommit", ctypes.c_uint32), 262 | ("SizeOfHeapReserve", ctypes.c_uint32), 263 | ("SizeOfHeapCommit", ctypes.c_uint32), 264 | ("LoaderFlags", ctypes.c_uint32), 265 | ("NumberOfRvaAndSizes", ctypes.c_uint32), 266 | ("DataDirectory", ImageDataDirectory * IMAGE_NUMBEROF_DIRECTORY_ENTRIES), 267 | ) 268 | Magic: typing.Union[int, ctypes.c_uint16] 269 | MajorLinkerVersion: typing.Union[int, ctypes.c_uint8] 270 | MinorLinkerVersion: typing.Union[int, ctypes.c_uint8] 271 | SizeOfCode: typing.Union[int, ctypes.c_uint32] 272 | SizeOfInitializedData: typing.Union[int, ctypes.c_uint32] 273 | SizeOfUninitializedData: typing.Union[int, ctypes.c_uint32] 274 | AddressOfEntryPoint: typing.Union[int, ctypes.c_uint32] 275 | BaseOfCode: typing.Union[int, ctypes.c_uint32] 276 | BaseOfData: typing.Union[int, ctypes.c_uint32] 277 | ImageBase: typing.Union[int, ctypes.c_uint32] 278 | SectionAlignment: typing.Union[int, ctypes.c_uint32] 279 | FileAlignment: typing.Union[int, ctypes.c_uint32] 280 | MajorOperatingSystemVersion: typing.Union[int, ctypes.c_uint16] 281 | MinorOperatingSystemVersion: typing.Union[int, ctypes.c_uint16] 282 | MajorImageVersion: typing.Union[int, ctypes.c_uint16] 283 | MinorImageVersion: typing.Union[int, ctypes.c_uint16] 284 | MajorSubsystemVersion: typing.Union[int, ctypes.c_uint16] 285 | MinorSubsystemVersion: typing.Union[int, ctypes.c_uint16] 286 | Win32VersionValue: typing.Union[int, ctypes.c_uint32] 287 | SizeOfImage: typing.Union[int, ctypes.c_uint32] 288 | SizeOfHeaders: typing.Union[int, ctypes.c_uint32] 289 | CheckSum: typing.Union[int, ctypes.c_uint32] 290 | Subsystem: typing.Union[int, ctypes.c_uint16] 291 | DllCharacteristics: typing.Union[int, ctypes.c_uint16] 292 | SizeOfStackReserve: typing.Union[int, ctypes.c_uint32] 293 | SizeOfStackCommit: typing.Union[int, ctypes.c_uint32] 294 | SizeOfHeapReserve: typing.Union[int, ctypes.c_uint32] 295 | SizeOfHeapCommit: typing.Union[int, ctypes.c_uint32] 296 | LoaderFlags: typing.Union[int, ctypes.c_uint32] 297 | NumberOfRvaAndSizes: typing.Union[int, ctypes.c_uint32] 298 | DataDirectory: typing.Union[ 299 | typing.Sequence[ImageDataDirectory], 300 | ImageDataDirectory * IMAGE_NUMBEROF_DIRECTORY_ENTRIES, 301 | ] 302 | 303 | 304 | class ImageOptionalHeader64(ctypes.LittleEndianStructure): 305 | _fields_ = ( 306 | ("Magic", ctypes.c_uint16), 307 | ("MajorLinkerVersion", ctypes.c_uint8), 308 | ("MinorLinkerVersion", ctypes.c_uint8), 309 | ("SizeOfCode", ctypes.c_uint32), 310 | ("SizeOfInitializedData", ctypes.c_uint32), 311 | ("SizeOfUninitializedData", ctypes.c_uint32), 312 | ("AddressOfEntryPoint", ctypes.c_uint32), 313 | ("BaseOfCode", ctypes.c_uint32), 314 | ("ImageBase", ctypes.c_uint64), 315 | ("SectionAlignment", ctypes.c_uint32), 316 | ("FileAlignment", ctypes.c_uint32), 317 | ("MajorOperatingSystemVersion", ctypes.c_uint16), 318 | ("MinorOperatingSystemVersion", ctypes.c_uint16), 319 | ("MajorImageVersion", ctypes.c_uint16), 320 | ("MinorImageVersion", ctypes.c_uint16), 321 | ("MajorSubsystemVersion", ctypes.c_uint16), 322 | ("MinorSubsystemVersion", ctypes.c_uint16), 323 | ("Win32VersionValue", ctypes.c_uint32), 324 | ("SizeOfImage", ctypes.c_uint32), 325 | ("SizeOfHeaders", ctypes.c_uint32), 326 | ("CheckSum", ctypes.c_uint32), 327 | ("Subsystem", ctypes.c_uint16), 328 | ("DllCharacteristics", ctypes.c_uint16), 329 | ("SizeOfStackReserve", ctypes.c_uint64), 330 | ("SizeOfStackCommit", ctypes.c_uint64), 331 | ("SizeOfHeapReserve", ctypes.c_uint64), 332 | ("SizeOfHeapCommit", ctypes.c_uint64), 333 | ("LoaderFlags", ctypes.c_uint32), 334 | ("NumberOfRvaAndSizes", ctypes.c_uint32), 335 | ("DataDirectory", ImageDataDirectory * IMAGE_NUMBEROF_DIRECTORY_ENTRIES), 336 | ) 337 | Magic: typing.Union[int, ctypes.c_uint16] 338 | MajorLinkerVersion: typing.Union[int, ctypes.c_uint8] 339 | MinorLinkerVersion: typing.Union[int, ctypes.c_uint8] 340 | SizeOfCode: typing.Union[int, ctypes.c_uint32] 341 | SizeOfInitializedData: typing.Union[int, ctypes.c_uint32] 342 | SizeOfUninitializedData: typing.Union[int, ctypes.c_uint32] 343 | AddressOfEntryPoint: typing.Union[int, ctypes.c_uint32] 344 | BaseOfCode: typing.Union[int, ctypes.c_uint32] 345 | ImageBase: typing.Union[int, ctypes.c_uint64] 346 | SectionAlignment: typing.Union[int, ctypes.c_uint32] 347 | FileAlignment: typing.Union[int, ctypes.c_uint32] 348 | MajorOperatingSystemVersion: typing.Union[int, ctypes.c_uint16] 349 | MinorOperatingSystemVersion: typing.Union[int, ctypes.c_uint16] 350 | MajorImageVersion: typing.Union[int, ctypes.c_uint16] 351 | MinorImageVersion: typing.Union[int, ctypes.c_uint16] 352 | MajorSubsystemVersion: typing.Union[int, ctypes.c_uint16] 353 | MinorSubsystemVersion: typing.Union[int, ctypes.c_uint16] 354 | Win32VersionValue: typing.Union[int, ctypes.c_uint32] 355 | SizeOfImage: typing.Union[int, ctypes.c_uint32] 356 | SizeOfHeaders: typing.Union[int, ctypes.c_uint32] 357 | CheckSum: typing.Union[int, ctypes.c_uint32] 358 | Subsystem: typing.Union[int, ctypes.c_uint16] 359 | DllCharacteristics: typing.Union[int, ctypes.c_uint16] 360 | SizeOfStackReserve: typing.Union[int, ctypes.c_uint64] 361 | SizeOfStackCommit: typing.Union[int, ctypes.c_uint64] 362 | SizeOfHeapReserve: typing.Union[int, ctypes.c_uint64] 363 | SizeOfHeapCommit: typing.Union[int, ctypes.c_uint64] 364 | LoaderFlags: typing.Union[int, ctypes.c_uint32] 365 | NumberOfRvaAndSizes: typing.Union[int, ctypes.c_uint32] 366 | DataDirectory: typing.Union[ 367 | typing.Sequence[ImageDataDirectory], 368 | ImageDataDirectory * IMAGE_NUMBEROF_DIRECTORY_ENTRIES, 369 | ] 370 | 371 | 372 | class ImageNtHeaders32(ctypes.LittleEndianStructure): 373 | _fields_ = ( 374 | ("Signature", ctypes.c_uint32), 375 | ("FileHeader", ImageFileHeader), 376 | ("OptionalHeader", ImageOptionalHeader32), 377 | ) 378 | Signature: typing.Union[int, ctypes.c_uint32] 379 | FileHeader: ImageFileHeader 380 | OptionalHeader: ImageOptionalHeader32 381 | 382 | 383 | class ImageNtHeaders64(ctypes.LittleEndianStructure): 384 | _fields_ = ( 385 | ("Signature", ctypes.c_uint32), 386 | ("FileHeader", ImageFileHeader), 387 | ("OptionalHeader", ImageOptionalHeader64), 388 | ) 389 | Signature: typing.Union[int, ctypes.c_uint32] 390 | FileHeader: ImageFileHeader 391 | OptionalHeader: ImageOptionalHeader64 392 | 393 | 394 | class ImageSectionHeader(ctypes.LittleEndianStructure): 395 | _fields_ = ( 396 | ("Name", ctypes.c_char * IMAGE_SIZEOF_SHORT_NAME), 397 | ("VirtualSize", ctypes.c_uint32), 398 | ("VirtualAddress", ctypes.c_uint32), 399 | ("SizeOfRawData", ctypes.c_uint32), 400 | ("PointerToRawData", ctypes.c_uint32), 401 | ("PointerToRelocations", ctypes.c_uint32), 402 | ("PointerToLinenumbers", ctypes.c_uint32), 403 | ("NumberOfRelocations", ctypes.c_uint16), 404 | ("NumberOfLinenumbers", ctypes.c_uint16), 405 | ("Characteristics", ctypes.c_uint32), 406 | ) 407 | Name: typing.Union[bytes, ctypes.c_char * IMAGE_SIZEOF_SHORT_NAME] 408 | VirtualSize: typing.Union[int, ctypes.c_uint32] 409 | VirtualAddress: typing.Union[int, ctypes.c_uint32] 410 | SizeOfRawData: typing.Union[int, ctypes.c_uint32] 411 | PointerToRawData: typing.Union[int, ctypes.c_uint32] 412 | PointerToRelocations: typing.Union[int, ctypes.c_uint32] 413 | PointerToLinenumbers: typing.Union[int, ctypes.c_uint32] 414 | NumberOfRelocations: typing.Union[int, ctypes.c_uint16] 415 | NumberOfLinenumbers: typing.Union[int, ctypes.c_uint16] 416 | Characteristics: typing.Union[int, ctypes.c_uint32] 417 | 418 | 419 | class ImageBaseRelocation(ctypes.LittleEndianStructure): 420 | _fields_ = ( 421 | ("VirtualAddress", ctypes.c_uint32), 422 | ("SizeOfBlock", ctypes.c_uint32), 423 | ) 424 | VirtualAddress: typing.Union[int, ctypes.c_uint32] 425 | SizeOfBlock: typing.Union[int, ctypes.c_uint32] 426 | 427 | 428 | # endregion 429 | 430 | # region x86/x64-specific system ffi definitions 431 | 432 | POINTER_SIZE = ctypes.sizeof(ctypes.c_void_p) 433 | if os.name == 'nt': 434 | crt_malloc = ctypes.cdll.msvcrt.malloc 435 | crt_free = ctypes.cdll.msvcrt.free 436 | 437 | 438 | def allocate_executable_memory(length: int): 439 | virtualalloc = ctypes.windll.kernel32.VirtualAlloc 440 | virtualalloc.argtypes = (ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_uint32) 441 | virtualalloc.restype = ctypes.c_void_p 442 | return ctypes.c_void_p(virtualalloc(0, 443 | length, 444 | 0x3000, # MEM_RESERVE | MEM_COMMIT 445 | 0x40)) # PAGE_EXECUTE_READWRITE 446 | 447 | 448 | def free_executable_memory(ptr: ctypes.c_void_p): 449 | ctypes.windll.kernel32.VirtualFree(ptr, 0, 0x8000) 450 | else: 451 | libc = ctypes.CDLL(ctypes.util.find_library("c")) 452 | crt_malloc = libc.malloc 453 | crt_free = libc.free 454 | 455 | # close enough definitions 456 | libc.memalign.argtypes = ctypes.c_size_t, ctypes.c_size_t 457 | libc.memalign.restype = ctypes.c_size_t 458 | libc.mprotect.argtypes = ctypes.c_size_t, ctypes.c_size_t, ctypes.c_size_t 459 | 460 | 461 | def allocate_executable_memory(length: int): 462 | p = libc.memalign(mmap.PAGESIZE, length) 463 | libc.mprotect(p, length, mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC) 464 | return ctypes.c_void_p(p) 465 | 466 | 467 | def free_executable_memory(ptr: ctypes.c_void_p): 468 | crt_free(ptr.value) 469 | 470 | crt_malloc.argtypes = (ctypes.c_size_t,) 471 | crt_malloc.restype = ctypes.c_size_t 472 | crt_free.argtypes = (ctypes.c_size_t,) 473 | 474 | PyMemoryView_FromMemory = ctypes.pythonapi.PyMemoryView_FromMemory 475 | PyMemoryView_FromMemory.argtypes = (ctypes.c_void_p, ctypes.c_ssize_t, ctypes.c_int) 476 | PyMemoryView_FromMemory.restype = ctypes.py_object 477 | 478 | 479 | # endregion 480 | 481 | # region budget windows stdcall <-> linux cdecl ABI converters 482 | 483 | class PeImage: 484 | def __init__(self, data: typing.Union[bytearray, bytes]): 485 | self._data = data if isinstance(data, bytearray) else bytearray(data) 486 | 487 | self.dos = ImageDosHeader.from_buffer(self._data, 0) 488 | if self.dos.e_magic != 0x5a4d: 489 | raise ValueError("bad dos header") 490 | 491 | if POINTER_SIZE == 8: 492 | self.nt = ImageNtHeaders64.from_buffer(self._data, self.dos.e_lfanew) 493 | else: 494 | self.nt = ImageNtHeaders32.from_buffer(self._data, self.dos.e_lfanew) 495 | if self.nt.Signature != 0x4550: 496 | raise ValueError("bad nt header") 497 | 498 | self.sections: typing.Union[typing.Sequence[ImageSectionHeader], ctypes.Array[ImageSectionHeader]] = ( 499 | ImageSectionHeader * self.nt.FileHeader.NumberOfSections).from_buffer( 500 | self._data, self.dos.e_lfanew + ctypes.sizeof(self.nt)) 501 | 502 | self.address: ctypes.c_void_p = allocate_executable_memory(self.nt.OptionalHeader.SizeOfImage) 503 | self.view: memoryview = PyMemoryView_FromMemory( 504 | self.address, 505 | self.nt.OptionalHeader.SizeOfImage, 506 | 0x200, # Read/Write 507 | ) 508 | 509 | self._map_headers_and_sections() 510 | self._relocate() 511 | 512 | def _map_headers_and_sections(self): 513 | ctypes.memmove(self.address, 514 | ctypes.addressof(ctypes.c_byte.from_buffer(self._data)), 515 | self.nt.OptionalHeader.SizeOfHeaders) 516 | for shdr in self.sections: 517 | ctypes.memmove(ctypes.addressof(ctypes.c_byte.from_buffer(self.view, shdr.VirtualAddress)), 518 | ctypes.addressof(ctypes.c_byte.from_buffer(self._data, shdr.PointerToRawData)), 519 | min(shdr.SizeOfRawData, shdr.VirtualSize)) 520 | 521 | def _relocate(self): 522 | rva = int(self.nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress) 523 | rva_to = rva + int(self.nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size) 524 | displacement = self.address.value - self.nt.OptionalHeader.ImageBase 525 | while rva < rva_to: 526 | page = ctypes.cast(ctypes.c_void_p(self.address.value + rva), ctypes.POINTER(ImageBaseRelocation)).contents 527 | page_data = ctypes.cast(ctypes.c_void_p(self.address.value + rva + ctypes.sizeof(page)), 528 | ctypes.POINTER(ctypes.c_uint16 * ((page.SizeOfBlock - ctypes.sizeof(page)) // 2)) 529 | ).contents 530 | for relo in page_data: 531 | absptr_address = self.address.value + page.VirtualAddress + (relo & 0xFFF) 532 | if relo >> 12 == 0: 533 | pass 534 | elif relo >> 12 == 3: 535 | ptr = ctypes.cast(absptr_address, ctypes.POINTER(ctypes.c_uint32)) 536 | ctypes.memmove(absptr_address, 537 | ctypes.addressof(ctypes.c_uint32(ptr.contents.value + displacement)), 4) 538 | elif relo >> 12 == 10: 539 | ptr = ctypes.cast(absptr_address, ctypes.POINTER(ctypes.c_uint64)) 540 | ctypes.memmove(absptr_address, 541 | ctypes.addressof(ctypes.c_uint64(ptr.contents.value + displacement)), 8) 542 | else: 543 | raise RuntimeError("Unsupported relocation type") 544 | rva += page.SizeOfBlock 545 | 546 | def section_header(self, name: bytes): 547 | try: 548 | return next(s for s in self.sections if s.Name == name) 549 | except StopIteration: 550 | return KeyError 551 | 552 | def section(self, section: typing.Union[bytes, ImageSectionHeader]) -> memoryview: 553 | if not isinstance(section, ImageSectionHeader): 554 | section = self.section_header(section) 555 | return self.view[section.VirtualAddress:section.VirtualAddress + section.VirtualSize] 556 | 557 | def resolve_rip_relative(self, addr: int): 558 | if self.view[addr] in (0xE8, 0xE9): 559 | return addr + 5 + int.from_bytes(self.view[addr + 1:addr + 5], "little", signed=True) 560 | else: 561 | raise NotImplementedError 562 | 563 | 564 | class StdCallFunc32ByPythonFunction: 565 | def __init__(self, pyctypefn, fn: callable, arglen: int, name: str): 566 | self._inner = pyctypefn(fn) 567 | self._fn = fn 568 | self._name = name 569 | inner_address = ctypes.cast(self._inner, ctypes.c_void_p) 570 | 571 | codelen = 1 + arglen // 4 * 7 + 5 + 2 + 6 + 3 572 | codeptr = allocate_executable_memory(codelen) 573 | buf = (ctypes.c_uint8 * codelen).from_address(codeptr.value) 574 | buf[0] = 0x90 575 | i = 1 576 | for j in range(0, arglen, 4): 577 | buf[i] = 0xff 578 | buf[i + 1] = 0xb4 579 | buf[i + 2] = 0x24 580 | ctypes.c_uint32.from_address(codeptr.value + i + 3).value = arglen 581 | i += 7 582 | 583 | buf[i] = 0xb8 584 | ctypes.c_void_p.from_address(codeptr.value + i + 1).value = inner_address.value 585 | i += 5 586 | 587 | buf[i] = 0xff 588 | buf[i + 1] = 0xd0 589 | i += 2 590 | 591 | buf[i + 0] = 0x81 592 | buf[i + 1] = 0xc4 593 | ctypes.c_uint32.from_address(codeptr.value + i + 2).value = arglen 594 | i += 6 595 | 596 | buf[i] = 0xc2 597 | ctypes.c_uint16.from_address(codeptr.value + i + 1).value = arglen 598 | 599 | self._address = codeptr 600 | 601 | def address(self): 602 | return self._address 603 | 604 | def __call__(self, *args): 605 | return self._fn(*args) 606 | 607 | 608 | class StdCallFunc64ByPythonFunction: 609 | def __init__(self, pyctypefn, fn: callable, arglen: int, name: str): 610 | self._inner = pyctypefn(fn) 611 | self._fn = fn 612 | self._name = name 613 | inner_address = ctypes.cast(self._inner, ctypes.c_void_p) 614 | 615 | cmds = [ 616 | b"\x48\x83\xec\x38", # sub rsp, 0x38 617 | b"\x57", # push rdi 618 | b"\x56", # push rsi 619 | b"\x48\x89\xCF", # mov rdi, rcx 620 | b"\x48\x89\xD6", # mov rsi, rdx 621 | b"\x4C\x89\xC2", # mov rdx, r8 622 | b"\x4C\x89\xC9", # mov rcx, r9 623 | b"\x48\xB8", inner_address.value.to_bytes(8, "little"), # movabs rax, 0x0 624 | b"\xFF\xD0", # call rax 625 | b"\x5E", # pop rsi 626 | b"\x5F", # pop rdi 627 | b"\x48\x83\xc4\x38", # add rsp, 0x38 628 | b"\xC3", # ret 629 | ] 630 | 631 | cmds = bytearray().join(cmds) 632 | self._address = allocate_executable_memory(len(cmds)) 633 | ctypes.memmove(self._address.value, 634 | ctypes.addressof((ctypes.c_uint8 * len(cmds)).from_buffer(cmds)), 635 | len(cmds)) 636 | 637 | def address(self): 638 | return self._address 639 | 640 | def __call__(self, *args): 641 | return self._fn(*args) 642 | 643 | 644 | class StdCallFunc32ByFunctionPointer: 645 | def __init__(self, ptr: int, argtypes, noargtypefn, name: str): 646 | self._name = name 647 | self._ptr = ptr 648 | self._argtypes = argtypes 649 | self._codelen = 1 + sum((ctypes.sizeof(argtype) + 3) // 4 * 5 for argtype in argtypes) + 8 650 | self._noargtypefn = noargtypefn 651 | self._codeptr = allocate_executable_memory(self._codelen) 652 | 653 | def address(self): 654 | return ctypes.c_void_p(self._ptr) 655 | 656 | def __call__(self, *args): 657 | buf = (ctypes.c_uint8 * self._codelen).from_address(self._codeptr.value) 658 | buf[0] = 0x90 659 | i = 1 660 | for argtype, arg in zip(reversed(self._argtypes), reversed(args)): 661 | arglen = ctypes.sizeof(argtype) 662 | if not isinstance(arg, argtype): 663 | arg = argtype(arg) 664 | argb = (ctypes.c_uint8 * arglen).from_address(ctypes.addressof(arg)) 665 | j = 0 666 | while j < arglen - 3: 667 | buf[i] = 0x68 668 | buf[i + 1] = argb[0] 669 | buf[i + 2] = argb[1] 670 | buf[i + 3] = argb[2] 671 | buf[i + 4] = argb[3] 672 | i += 5 673 | j += 4 674 | if j != arglen: 675 | buf[i] = 0x68 676 | buf[i + 1] = argb[0] 677 | buf[i + 2] = argb[1] if j + 1 <= arglen else 0 678 | buf[i + 3] = argb[2] if j + 2 <= arglen else 0 679 | buf[i + 4] = argb[3] if j + 3 <= arglen else 0 680 | i += 5 681 | buf[i] = 0xb8 682 | ctypes.c_uint32.from_address(self._codeptr.value + i + 1).value = self._ptr 683 | buf[i + 5] = 0xff 684 | buf[i + 6] = 0xd0 685 | buf[i + 7] = 0xc3 686 | 687 | res = self._noargtypefn(self._codeptr.value)() 688 | 689 | return res 690 | 691 | 692 | class StdCallFunc64ByFunctionPointer: 693 | def __init__(self, ptr: int, argtypes, noargtypefn, name: str): 694 | self._ptr = ptr 695 | self._argtypes = argtypes 696 | self._noargtypefn = noargtypefn 697 | self._name = name 698 | 699 | movabs_regs = ( 700 | b"\x48\xb9", # movabs rcx, imm 701 | b"\x48\xba", # movabs rdx, imm 702 | b"\x49\xb8", # movabs r8, imm 703 | b"\x49\xb9", # movabs r9, imm 704 | ) 705 | 706 | cmds = [ 707 | b"\x57", # push rdi 708 | b"\x56", # push rsi 709 | 710 | # sub rsp, imm 711 | b"\x48\x83\xec", 712 | (0x8 + (len(argtypes) + 1) // 2 * 2 * 8).to_bytes(1, "little"), 713 | ] 714 | self._offsets = [] 715 | 716 | for i, argtype in enumerate(self._argtypes): 717 | if i < len(movabs_regs): 718 | cmds.append(movabs_regs[i]) 719 | self._offsets.append(sum(len(x) for x in cmds)) 720 | cmds.append(bytes(8)) 721 | else: 722 | # movabs rax, imm 723 | cmds.append(b"\x48\xb8") 724 | self._offsets.append(sum(len(x) for x in cmds)) 725 | cmds.append(bytes(8)) 726 | 727 | # mov qword ptr [rsp + N], rax 728 | cmds.append(b"\x48\x89\x44\x24") 729 | cmds.append((i * 8).to_bytes(1, "little")) 730 | 731 | # movabs rax, imm 732 | cmds.append(b"\x48\xb8") 733 | cmds.append(self._ptr.to_bytes(8, "little")) 734 | 735 | cmds.append(b"\xff\xd0") # call rax 736 | 737 | # add rsp, imm 738 | cmds.append(b"\x48\x83\xc4" + (0x8 + (len(argtypes) + 1) // 2 * 2 * 8).to_bytes(1, "little")) 739 | 740 | cmds.append(b"\x5e") # pop rsi 741 | cmds.append(b"\x5f") # pop rdi 742 | cmds.append(b"\xc3") # ret 743 | 744 | self._template = bytearray().join(cmds) 745 | self._codeptr = allocate_executable_memory(len(self._template)) 746 | ctypes.memmove(self._codeptr.value, 747 | ctypes.addressof(ctypes.c_uint8.from_buffer(self._template)), 748 | len(self._template)) 749 | 750 | def address(self): 751 | return ctypes.c_void_p(self._ptr) 752 | 753 | def __call__(self, *args): 754 | for argtype, arg, offset in zip(self._argtypes, args, self._offsets): 755 | arglen = ctypes.sizeof(argtype) 756 | if not isinstance(arg, argtype): 757 | arg = argtype(arg) 758 | ctypes.memmove(self._codeptr.value + offset, ctypes.addressof(arg), arglen) 759 | 760 | res = self._noargtypefn(self._codeptr.value)() 761 | return res 762 | 763 | 764 | class StdCallFunc32Type: 765 | def __init__(self, restype, *argtypes, name: typing.Optional[str] = None): 766 | self._name = name or ("(" + ", ".join(str(x) for x in (restype, *argtypes)) + ")") 767 | self._restype = restype 768 | self._argtypes = argtypes 769 | self._arglen = sum((ctypes.sizeof(argtype) + 3) // 4 * 4 for argtype in self._argtypes) 770 | self._noarg_type = ctypes.CFUNCTYPE(restype) 771 | self._pytype = ctypes.CFUNCTYPE(restype, *argtypes) 772 | 773 | def __call__(self, ptr): 774 | if callable(ptr): 775 | return StdCallFunc32ByPythonFunction(self._pytype, ptr, self._arglen, self._name) 776 | elif isinstance(ptr, int): 777 | return StdCallFunc32ByFunctionPointer(ptr, self._argtypes, self._noarg_type, self._name) 778 | else: 779 | raise TypeError 780 | 781 | 782 | class StdCallFunc64Type: 783 | def __init__(self, restype, *argtypes, name: typing.Optional[str] = None): 784 | self._name = name or ("(" + ", ".join(str(x) for x in (restype, *argtypes)) + ")") 785 | self._restype = restype 786 | self._argtypes = argtypes 787 | self._arglen = sum((ctypes.sizeof(argtype) + 3) // 4 * 4 for argtype in self._argtypes) 788 | self._noarg_type = ctypes.CFUNCTYPE(restype) 789 | self._pytype = ctypes.CFUNCTYPE(restype, *argtypes) 790 | 791 | def __call__(self, ptr): 792 | if callable(ptr): 793 | return StdCallFunc64ByPythonFunction(self._pytype, ptr, self._arglen, self._name) 794 | elif isinstance(ptr, int): 795 | return StdCallFunc64ByFunctionPointer(ptr, self._argtypes, self._noarg_type, self._name) 796 | else: 797 | raise TypeError 798 | 799 | 800 | if POINTER_SIZE == 4: 801 | StdCallFuncType = StdCallFunc32Type 802 | else: 803 | StdCallFuncType = StdCallFunc64Type 804 | 805 | # endregion 806 | 807 | # region Oodle typedefs 808 | 809 | OodleNetwork1_Shared_Size = StdCallFuncType(ctypes.c_int32, ctypes.c_int32, name="OodleNetwork1_Shared_Size") 810 | OodleNetwork1_Shared_SetWindow = StdCallFuncType(None, ctypes.c_void_p, ctypes.c_int32, ctypes.c_void_p, ctypes.c_int32, 811 | name="OodleNetwork1_Shared_SetWindow") 812 | OodleNetwork1_Proto_Train = StdCallFuncType(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), 813 | ctypes.POINTER(ctypes.c_int32), ctypes.c_int32, 814 | name="OodleNetwork1_Proto_Train") 815 | OodleNetwork1_Proto_Decode = StdCallFuncType(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, 816 | ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, 817 | name="OodleNetwork1_Proto_Decode") 818 | OodleNetwork1_Proto_Encode = StdCallFuncType(ctypes.c_int32, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, 819 | ctypes.c_size_t, ctypes.c_void_p, name="OodleNetwork1_Proto_Encode") 820 | OodleNetwork1_Proto_State_Size = StdCallFuncType(ctypes.c_int32, name="OodleNetwork1_Proto_State_Size") 821 | Oodle_Malloc = StdCallFuncType(ctypes.c_size_t, ctypes.c_size_t, ctypes.c_int32, name="Oodle_Malloc") 822 | Oodle_Free = StdCallFuncType(None, ctypes.c_size_t, name="Oodle_Free") 823 | Oodle_SetMallocFree = StdCallFuncType(None, ctypes.c_void_p, ctypes.c_void_p, name="Oodle_SetMallocFree") 824 | 825 | 826 | # endregion 827 | 828 | # region ZiPatch typedefs 829 | 830 | 831 | class ZiPatchHeader(ctypes.BigEndianStructure): 832 | SIGNATURE = b"\x91\x5A\x49\x50\x41\x54\x43\x48\x0d\x0a\x1a\x0a" 833 | 834 | _fields_ = ( 835 | ("signature", ctypes.c_char * 12), 836 | ) 837 | 838 | signature: typing.Union[int, ctypes.c_char * 12] 839 | 840 | 841 | class ZiPatchChunkHeader(ctypes.BigEndianStructure): 842 | _fields_ = ( 843 | ("size", ctypes.c_uint32), 844 | ("type", ctypes.c_char * 4), 845 | ) 846 | 847 | size: typing.Union[int, ctypes.c_uint32] 848 | type: typing.Union[bytes, ctypes.c_char * 4] 849 | 850 | 851 | class ZiPatchChunkFooter(ctypes.BigEndianStructure): 852 | _fields_ = ( 853 | ("crc32", ctypes.c_uint32), 854 | ) 855 | 856 | crc32: typing.Union[int, ctypes.c_uint32] 857 | 858 | 859 | class ZiPatchSqpackHeader(ctypes.BigEndianStructure): 860 | _fields_ = ( 861 | ("size", ctypes.c_uint32), 862 | ("command", ctypes.c_char * 4), 863 | ) 864 | 865 | size: typing.Union[int, ctypes.c_uint32] 866 | command: typing.Union[bytes, ctypes.c_char * 4] 867 | 868 | 869 | class ZiPatchSqpackFileAddHeader(ctypes.BigEndianStructure): 870 | COMMAND = b'FA' 871 | 872 | _fields_ = ( 873 | ("offset", ctypes.c_uint64), 874 | ("size", ctypes.c_uint64), 875 | ("path_size", ctypes.c_uint32), 876 | ("expac_id", ctypes.c_uint16), 877 | ("padding1", ctypes.c_uint16), 878 | ) 879 | 880 | offset: typing.Union[int, ctypes.c_uint16] 881 | size: typing.Union[int, ctypes.c_uint64] 882 | path_size: typing.Union[int, ctypes.c_uint32] 883 | expac_id: typing.Union[int, ctypes.c_uint16] 884 | padding1: typing.Union[int, ctypes.c_uint16] 885 | 886 | 887 | class ZiPatchSqpackFileDeleteHeader(ctypes.BigEndianStructure): 888 | COMMAND = b'FD' 889 | 890 | _fields_ = ( 891 | ("offset", ctypes.c_uint64), 892 | ("size", ctypes.c_uint64), 893 | ("path_size", ctypes.c_uint32), 894 | ("expac_id", ctypes.c_uint16), 895 | ("padding1", ctypes.c_uint16), 896 | ) 897 | 898 | offset: typing.Union[int, ctypes.c_uint16] 899 | size: typing.Union[int, ctypes.c_uint64] 900 | path_size: typing.Union[int, ctypes.c_uint32] 901 | expac_id: typing.Union[int, ctypes.c_uint16] 902 | padding1: typing.Union[int, ctypes.c_uint16] 903 | 904 | 905 | class ZiPatchSqpackFileResolver(ctypes.BigEndianStructure): 906 | _fields_ = ( 907 | ("main_id", ctypes.c_uint16), 908 | ("sub_id", ctypes.c_uint16), 909 | ("file_id", ctypes.c_uint32), 910 | ) 911 | 912 | main_id: typing.Union[int, ctypes.c_uint16] 913 | sub_id: typing.Union[int, ctypes.c_uint16] 914 | file_id: typing.Union[int, ctypes.c_uint32] 915 | 916 | @property 917 | def expac_id(self): 918 | return self.sub_id >> 8 919 | 920 | @property 921 | def path(self): 922 | if self.expac_id == 0: 923 | return f"sqpack/ffxiv/{self.main_id:02x}{self.sub_id:04x}.win32" 924 | else: 925 | return f"sqpack/ex{self.expac_id}/{self.main_id:02x}{self.sub_id:04x}.win32" 926 | 927 | 928 | class ZiPatchSqpackAddData(ZiPatchSqpackFileResolver): 929 | COMMAND = b'A' 930 | 931 | _fields_ = ( 932 | ("block_offset_value", ctypes.c_uint32), 933 | ("block_size_value", ctypes.c_uint32), 934 | ("clear_size_value", ctypes.c_uint32), 935 | ) 936 | 937 | @property 938 | def block_offset(self): 939 | return self.block_offset_value * 128 940 | 941 | @property 942 | def block_size(self): 943 | return self.block_size_value * 128 944 | 945 | @property 946 | def clear_size(self): 947 | return self.clear_size_value * 128 948 | 949 | @property 950 | def path(self): 951 | return super().path + f".dat{self.file_id}" 952 | 953 | 954 | class ZiPatchSqpackZeroData(ZiPatchSqpackFileResolver): 955 | COMMANDS = {b'E', b'D'} 956 | 957 | _fields_ = ( 958 | ("block_offset_value", ctypes.c_uint32), 959 | ("block_size_value", ctypes.c_uint32), 960 | ) 961 | 962 | @property 963 | def block_offset(self): 964 | return self.block_offset_value * 128 965 | 966 | @property 967 | def block_size(self): 968 | return self.block_size_value * 128 969 | 970 | @property 971 | def path(self): 972 | return super().path + f".dat{self.file_id}" 973 | 974 | 975 | class BlockHeader(ctypes.LittleEndianStructure): 976 | COMPRESSED_SIZE_NOT_COMPRESSED = 32000 977 | 978 | _fields_ = ( 979 | ("header_length", ctypes.c_uint32), 980 | ("version", ctypes.c_uint32), 981 | ("compressed_size", ctypes.c_uint32), 982 | ("decompressed_size", ctypes.c_uint32), 983 | ) 984 | 985 | header_length: int 986 | version: int 987 | compressed_size: int 988 | decompressed_size: int 989 | 990 | data: typing.Optional[bytes] = None 991 | 992 | def is_compressed(self): 993 | return self.compressed_size != BlockHeader.COMPRESSED_SIZE_NOT_COMPRESSED and self.decompressed_size != 1 994 | 995 | 996 | # endregion 997 | 998 | # region Game network typedefs 999 | 1000 | class XivMessageIpcActionEffect(ctypes.LittleEndianStructure): 1001 | _fields_ = ( 1002 | ("animation_target_actor", ctypes.c_uint32), 1003 | ("unknown_0x004", ctypes.c_uint32), 1004 | ("action_id", ctypes.c_uint32), 1005 | ("global_effect_counter", ctypes.c_uint32), 1006 | ("animation_lock_duration", ctypes.c_float), 1007 | ("unknown_target_id", ctypes.c_uint32), 1008 | ("source_sequence", ctypes.c_uint16), 1009 | ("rotation", ctypes.c_uint16), 1010 | ("action_animation_id", ctypes.c_uint16), 1011 | ("variation", ctypes.c_uint8), 1012 | ("effect_display_type", ctypes.c_uint8), 1013 | ("unknonw_0x020", ctypes.c_uint8), 1014 | ("effect_count", ctypes.c_uint8), 1015 | ("padding_0x022", ctypes.c_uint16), 1016 | ) 1017 | 1018 | animation_target_actor: typing.Union[int, ctypes.c_uint32] 1019 | unknown_0x004: typing.Union[int, ctypes.c_uint32] 1020 | action_id: typing.Union[int, ctypes.c_uint32] 1021 | global_effect_counter: typing.Union[int, ctypes.c_uint32] 1022 | animation_lock_duration: typing.Union[float, ctypes.c_float] 1023 | unknown_target_id: typing.Union[int, ctypes.c_uint32] 1024 | source_sequence: typing.Union[int, ctypes.c_uint16] 1025 | rotation: typing.Union[int, ctypes.c_uint16] 1026 | action_animation_id: typing.Union[int, ctypes.c_uint16] 1027 | variation: typing.Union[int, ctypes.c_uint8] 1028 | effect_display_type: typing.Union[int, ctypes.c_uint8] 1029 | unknown_0x020: typing.Union[int, ctypes.c_uint8] 1030 | effect_count: typing.Union[int, ctypes.c_uint8] 1031 | padding_0x022: typing.Union[int, ctypes.c_uint16] 1032 | 1033 | 1034 | class XivMessageIpcActorControlCategory(enum.IntEnum): 1035 | CancelCast = 0x000f 1036 | Rollback = 0x02bc 1037 | 1038 | 1039 | class XivMessageIpcActorControl(ctypes.LittleEndianStructure): 1040 | _fields_ = ( 1041 | ("category_int", ctypes.c_uint16), 1042 | ("padding_0x002", ctypes.c_uint16), 1043 | ("param_1", ctypes.c_uint32), 1044 | ("param_2", ctypes.c_uint32), 1045 | ("param_3", ctypes.c_uint32), 1046 | ("param_4", ctypes.c_uint32), 1047 | ("padding_0x014", ctypes.c_uint32), 1048 | ) 1049 | 1050 | category_int: typing.Union[int, ctypes.c_uint16] 1051 | padding_0x002: typing.Union[int, ctypes.c_uint16] 1052 | param_1: typing.Union[int, ctypes.c_uint32] 1053 | param_2: typing.Union[int, ctypes.c_uint32] 1054 | param_3: typing.Union[int, ctypes.c_uint32] 1055 | param_4: typing.Union[int, ctypes.c_uint32] 1056 | padding_0x014: typing.Union[int, ctypes.c_uint32] 1057 | 1058 | @property 1059 | def category(self): 1060 | try: 1061 | return XivMessageIpcActorControlCategory(self.category_int) 1062 | except ValueError: 1063 | return None 1064 | 1065 | @category.setter 1066 | def category(self, value: typing.Union[int, XivMessageIpcActorControlCategory]): 1067 | self.category_int = int(value) 1068 | 1069 | 1070 | class XivMessageIpcActorControlSelf(ctypes.LittleEndianStructure): 1071 | _fields_ = ( 1072 | ("category_int", ctypes.c_uint16), 1073 | ("padding_0x002", ctypes.c_uint16), 1074 | ("param_1", ctypes.c_uint32), 1075 | ("param_2", ctypes.c_uint32), 1076 | ("param_3", ctypes.c_uint32), 1077 | ("param_4", ctypes.c_uint32), 1078 | ("param_5", ctypes.c_uint32), 1079 | ("param_6", ctypes.c_uint32), 1080 | ("padding_0x01c", ctypes.c_uint32), 1081 | ) 1082 | 1083 | category_int: typing.Union[int, ctypes.c_uint16] 1084 | padding_0x002: typing.Union[int, ctypes.c_uint16] 1085 | param_1: typing.Union[int, ctypes.c_uint32] 1086 | param_2: typing.Union[int, ctypes.c_uint32] 1087 | param_3: typing.Union[int, ctypes.c_uint32] 1088 | param_4: typing.Union[int, ctypes.c_uint32] 1089 | param_5: typing.Union[int, ctypes.c_uint32] 1090 | param_6: typing.Union[int, ctypes.c_uint32] 1091 | padding_0x01c: typing.Union[int, ctypes.c_uint32] 1092 | 1093 | @property 1094 | def category(self): 1095 | try: 1096 | return XivMessageIpcActorControlCategory(self.category_int) 1097 | except ValueError: 1098 | return None 1099 | 1100 | @category.setter 1101 | def category(self, value: typing.Union[int, XivMessageIpcActorControlCategory]): 1102 | self.category_int = int(value) 1103 | 1104 | 1105 | class XivMessageIpcActorCast(ctypes.LittleEndianStructure): 1106 | _fields_ = ( 1107 | ("action_id", ctypes.c_uint16), 1108 | ("skill_type", ctypes.c_uint8), 1109 | ("unknown_0x003", ctypes.c_uint8), 1110 | ("action_id_2", ctypes.c_uint16), 1111 | ("unknown_0x006", ctypes.c_uint16), 1112 | ("cast_time", ctypes.c_float), 1113 | ("target_id", ctypes.c_uint32), 1114 | ("rotation", ctypes.c_float), 1115 | ("unknown_0x014", ctypes.c_uint32), 1116 | ("x", ctypes.c_uint16), 1117 | ("y", ctypes.c_uint16), 1118 | ("z", ctypes.c_uint16), 1119 | ("unknown_0x01e", ctypes.c_uint16), 1120 | ) 1121 | 1122 | action_id: typing.Union[int, ctypes.c_uint16] 1123 | skill_type: typing.Union[int, ctypes.c_uint8] 1124 | unknown_0x003: typing.Union[int, ctypes.c_uint8] 1125 | action_id_2: typing.Union[int, ctypes.c_uint16] 1126 | unknown_0x006: typing.Union[int, ctypes.c_uint16] 1127 | cast_time: typing.Union[float, ctypes.c_float] 1128 | target_id: typing.Union[int, ctypes.c_uint32] 1129 | rotation: typing.Union[float, ctypes.c_float] 1130 | unknown_0x014: typing.Union[int, ctypes.c_uint32] 1131 | x: typing.Union[int, ctypes.c_uint16] 1132 | y: typing.Union[int, ctypes.c_uint16] 1133 | z: typing.Union[int, ctypes.c_uint16] 1134 | unknown_0x01e: typing.Union[int, ctypes.c_uint16] 1135 | 1136 | 1137 | class XivMessageIpcActionRequestCommon(ctypes.LittleEndianStructure): 1138 | _fields_ = ( 1139 | ("action_id", ctypes.c_uint32), 1140 | ("unknown_0x002", ctypes.c_uint16), 1141 | ("sequence", ctypes.c_uint16), 1142 | ) 1143 | 1144 | action_id: typing.Union[int, ctypes.c_uint32] 1145 | sequence: typing.Union[int, ctypes.c_uint16] 1146 | 1147 | 1148 | class XivMessageIpcCustomOriginalWaitTime(ctypes.LittleEndianStructure): 1149 | _fields_ = ( 1150 | ("source_sequence", ctypes.c_uint16), 1151 | ("padding_0x002", ctypes.c_uint16), 1152 | ("original_wait_time", ctypes.c_float), 1153 | ) 1154 | 1155 | source_sequence: typing.Union[int, ctypes.c_uint16] 1156 | padding_0x002: typing.Union[int, ctypes.c_uint16] = 0 1157 | original_wait_time: typing.Union[float, ctypes.c_float] 1158 | 1159 | 1160 | class XivMessageIpcType(enum.IntEnum): 1161 | UnknownButInterested = 0x0014 1162 | XivMitmLatencyMitigatorCustom = 0xe852 1163 | 1164 | 1165 | class XivMitmLatencyMitigatorCustomSubtype(enum.IntEnum): 1166 | OriginalWaitTime = 0x0000 1167 | 1168 | 1169 | class XivMessageIpcHeader(ctypes.LittleEndianStructure): 1170 | _fields_ = ( 1171 | ("type_int", ctypes.c_uint16), 1172 | ("subtype", ctypes.c_uint16), 1173 | ("unknown_0x004", ctypes.c_uint16), 1174 | ("server_id", ctypes.c_uint16), 1175 | ("epoch", ctypes.c_uint32), 1176 | ("unknown_0x00c", ctypes.c_uint32), 1177 | ) 1178 | 1179 | type_int: typing.Union[int, ctypes.c_uint16] 1180 | subtype: typing.Union[int, ctypes.c_uint16] 1181 | unknown_0x004: typing.Union[int, ctypes.c_uint16] 1182 | server_id: typing.Union[int, ctypes.c_uint16] 1183 | epoch: typing.Union[int, ctypes.c_uint32] 1184 | unknown_0x00c: typing.Union[int, ctypes.c_uint32] 1185 | 1186 | @property 1187 | def type(self): 1188 | try: 1189 | return XivMessageIpcType(self.type_int) 1190 | except ValueError: 1191 | return None 1192 | 1193 | @type.setter 1194 | def type(self, value: typing.Union[int, XivMessageIpcType]): 1195 | self.type_int = int(value) 1196 | 1197 | 1198 | class XivMessageType(enum.IntEnum): 1199 | Ipc = 3 1200 | 1201 | 1202 | class XivMessageHeader(ctypes.LittleEndianStructure): 1203 | _fields_ = ( 1204 | ("length", ctypes.c_uint32), 1205 | ("source_actor", ctypes.c_uint32), 1206 | ("target_actor", ctypes.c_uint32), 1207 | ("type_int", ctypes.c_uint16), 1208 | ("unknown_0x00e", ctypes.c_uint16), 1209 | ) 1210 | 1211 | length: typing.Union[int, ctypes.c_uint32] 1212 | source_actor: typing.Union[int, ctypes.c_uint32] 1213 | target_actor: typing.Union[int, ctypes.c_uint32] 1214 | type_int: typing.Union[int, ctypes.c_uint16] 1215 | unknown_0x00e: typing.Union[int, ctypes.c_uint16] 1216 | 1217 | @property 1218 | def type(self): 1219 | try: 1220 | return XivMessageType(self.type_int) 1221 | except ValueError: 1222 | return None 1223 | 1224 | @type.setter 1225 | def type(self, value: typing.Union[int, XivMessageType]): 1226 | self.type_int = int(value) 1227 | 1228 | 1229 | class XivBundleHeader(ctypes.LittleEndianStructure): 1230 | MAGIC_CONSTANT_1: typing.ClassVar[bytes] = b"\x52\x52\xa0\x41\xff\x5d\x46\xe2\x7f\x2a\x64\x4d\x7b\x99\xc4\x75" 1231 | MAGIC_CONSTANT_2: typing.ClassVar[bytes] = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 1232 | MAX_LENGTH: typing.ClassVar[int] = 65536 1233 | 1234 | _fields_ = ( 1235 | ("magic", ctypes.c_byte * 16), 1236 | ("timestamp", ctypes.c_uint64), 1237 | ("length", ctypes.c_uint32), 1238 | ("conn_type", ctypes.c_uint16), 1239 | ("message_count", ctypes.c_uint16), 1240 | ("encoding", ctypes.c_uint8), 1241 | ("compression", ctypes.c_uint8), 1242 | ("unknown_0x022", ctypes.c_uint16), 1243 | ("decoded_body_length", ctypes.c_uint32), 1244 | ) 1245 | 1246 | magic: typing.Union[bytearray, ctypes.c_byte * 16] 1247 | timestamp: typing.Union[int, ctypes.c_uint64] 1248 | length: typing.Union[int, ctypes.c_uint32] 1249 | conn_type: typing.Union[int, ctypes.c_uint16] 1250 | message_count: typing.Union[int, ctypes.c_uint16] 1251 | encoding: typing.Union[int, ctypes.c_uint8] 1252 | compression: typing.Union[int, ctypes.c_uint8] 1253 | unknown_0x022: typing.Union[int, ctypes.c_uint16] 1254 | decoded_body_length: typing.Union[int, ctypes.c_uint32] 1255 | 1256 | @classmethod 1257 | def find(cls, data: typing.Union[bytearray, memoryview], oodle: 'BaseOodleHelper', oodle_channel: int): 1258 | offset = 0 1259 | while offset < len(data): 1260 | available_bytes = len(data) - offset 1261 | if available_bytes >= len(cls.MAGIC_CONSTANT_1): 1262 | mc1 = data.find(cls.MAGIC_CONSTANT_1, offset) 1263 | mc2 = data.find(cls.MAGIC_CONSTANT_2, offset) 1264 | else: 1265 | mc1 = data.find(cls.MAGIC_CONSTANT_1[:available_bytes], offset) 1266 | mc2 = data.find(cls.MAGIC_CONSTANT_2[:available_bytes], offset) 1267 | if mc1 == -1: 1268 | i = mc2 1269 | elif mc2 == -1: 1270 | i = mc1 1271 | else: 1272 | i = min(mc1, mc2) 1273 | if i == -1: # no hope 1274 | yield data[offset:] 1275 | offset = len(data) 1276 | break 1277 | if i != offset: 1278 | yield data[offset:i] 1279 | offset = i 1280 | 1281 | try: 1282 | bundle_header = cls.from_buffer(data, offset) 1283 | if len(data) - offset < bundle_header.length: 1284 | raise ValueError 1285 | bundle_data = data[offset + ctypes.sizeof(bundle_header):offset + bundle_header.length] 1286 | bundle_length = bundle_header.length # copy it, as it may get changed later 1287 | except ValueError: 1288 | break # incomplete data 1289 | 1290 | try: 1291 | if bundle_header.compression == 0: 1292 | pass 1293 | elif bundle_header.compression == 1: 1294 | bundle_data = bytearray(zlib.decompress(bundle_data)) 1295 | elif bundle_header.compression == 2: 1296 | bundle_data = bytearray(oodle.decode(oodle_channel, bundle_data, bundle_header.decoded_body_length)) 1297 | else: 1298 | raise RuntimeError(f"Unsupported compression method {bundle_header.compression}") 1299 | 1300 | bundle_data_offset = 0 1301 | messages = list() 1302 | for i in range(bundle_header.message_count): 1303 | message_header = XivMessageHeader.from_buffer(bundle_data, bundle_data_offset) 1304 | if message_header.length < ctypes.sizeof(message_header): 1305 | raise InvalidDataException 1306 | message_data = bundle_data[bundle_data_offset + ctypes.sizeof(message_header): 1307 | bundle_data_offset + message_header.length] 1308 | messages.append((message_header, message_data)) 1309 | bundle_data_offset += message_header.length 1310 | if bundle_data_offset > len(bundle_data): 1311 | raise InvalidDataException 1312 | 1313 | offset += bundle_length 1314 | yield bundle_header, messages 1315 | 1316 | except Exception as e: 1317 | if not isinstance(e, InvalidDataException): 1318 | logging.exception("Unknown error occurred while trying to parse bundle") 1319 | yield data[offset:offset + 1] 1320 | offset += 1 1321 | return offset 1322 | 1323 | 1324 | # endregion 1325 | 1326 | # region Opcode definition and misc game version specific configuration 1327 | 1328 | @dataclasses.dataclass 1329 | class OpcodeDefinition: 1330 | Name: str 1331 | C2S_ActionRequest: int 1332 | C2S_ActionRequestGroundTargeted: int 1333 | S2C_ActionEffect01: int 1334 | S2C_ActionEffect08: int 1335 | S2C_ActionEffect16: int 1336 | S2C_ActionEffect24: int 1337 | S2C_ActionEffect32: int 1338 | S2C_ActorCast: int 1339 | S2C_ActorControl: int 1340 | S2C_ActorControlSelf: int 1341 | Common_UseOodleTcp: bool 1342 | Server_IpRange: typing.List[typing.Union[ipaddress.IPv4Network, 1343 | typing.Tuple[ipaddress.IPv4Address, ipaddress.IPv4Address]]] 1344 | Server_PortRange: typing.List[typing.Tuple[int, int]] 1345 | 1346 | @classmethod 1347 | def from_dict(cls, data: dict): 1348 | kwargs = {} 1349 | for field in dataclasses.fields(cls): 1350 | field: dataclasses.Field 1351 | if field.type is int: 1352 | kwargs[field.name] = int(data[field.name], 0) 1353 | elif field.name == "Server_IpRange": 1354 | iplist = [] 1355 | for partstr in data[field.name].split(","): 1356 | part = [x.strip() for x in partstr.split("-")] 1357 | try: 1358 | if len(part) == 1: 1359 | iplist.append(ipaddress.IPv4Network(part[0])) 1360 | elif len(part) == 2: 1361 | iplist.append(tuple(sorted(ipaddress.IPv4Address(x) for x in part))) 1362 | else: 1363 | raise ValueError 1364 | except ValueError: 1365 | print("Skipping invalid IP address definition", partstr) 1366 | kwargs[field.name] = iplist 1367 | elif field.name == "Server_PortRange": 1368 | portlist = [] 1369 | for partstr in data[field.name].split(","): 1370 | part = [x.strip() for x in partstr.split("-")] 1371 | try: 1372 | if len(part) == 1: 1373 | portlist.append((int(part[0], 0), int(part[0], 0))) 1374 | elif len(part) == 2: 1375 | portlist.append((int(part[0], 0), int(part[1], 0))) 1376 | else: 1377 | raise ValueError 1378 | except ValueError: 1379 | print("Skipping invalid port definition", partstr) 1380 | kwargs[field.name] = portlist 1381 | else: 1382 | kwargs[field.name] = None if data[field.name] is None else field.type(data[field.name]) 1383 | return OpcodeDefinition(**kwargs) 1384 | 1385 | def is_action_effect(self, opcode: int): 1386 | return (opcode == self.S2C_ActionEffect01 1387 | or opcode == self.S2C_ActionEffect08 1388 | or opcode == self.S2C_ActionEffect16 1389 | or opcode == self.S2C_ActionEffect24 1390 | or opcode == self.S2C_ActionEffect32) 1391 | 1392 | 1393 | def load_definitions(update_opcodes: bool, json_path: typing.Optional[str]) -> typing.List[OpcodeDefinition]: 1394 | if json_path is not None and json_path.strip() != "": 1395 | with open(json_path) as fp: 1396 | return [OpcodeDefinition.from_dict({"Name": json_path, **json.load(fp)})] 1397 | 1398 | definitions_filepath = os.path.join(SCRIPT_DIRECTORY, "definitions.json") 1399 | if os.path.exists(definitions_filepath): 1400 | try: 1401 | if update_opcodes: 1402 | raise RuntimeError("Force update requested") 1403 | if os.path.getmtime(definitions_filepath) + 60 * 60 < time.time(): 1404 | raise RuntimeError("Definitions file older than an hour") 1405 | with open(definitions_filepath, "r") as fp: 1406 | return [OpcodeDefinition.from_dict(x) for x in json.load(fp)] 1407 | except Exception as e: 1408 | logging.info(f"Failed to read previous opcode definition files: {e}") 1409 | 1410 | definitions_raw = [] 1411 | logging.info("Downloading opcode definition files...") 1412 | try: 1413 | with urllib.request.urlopen(OPCODE_DEFINITION_LIST_URL) as resp: 1414 | filelist = json.load(resp) 1415 | 1416 | for f in filelist: 1417 | if f["name"][-5:].lower() != '.json': 1418 | continue 1419 | with urllib.request.urlopen(f["download_url"]) as resp: 1420 | data = json.load(resp) 1421 | data["Name"] = f["name"] 1422 | definitions_raw.append(data) 1423 | except Exception as e: 1424 | raise RuntimeError(f"Failed to load opcode definition") from e 1425 | with open(definitions_filepath, "w") as fp: 1426 | json.dump(definitions_raw, fp) 1427 | definitions = [OpcodeDefinition.from_dict(x) for x in definitions_raw] 1428 | return definitions 1429 | 1430 | 1431 | def load_rules(port: int, definitions: typing.List[OpcodeDefinition], nftables: bool) -> typing.Set[str]: 1432 | rules = set() 1433 | for definition in definitions: 1434 | for iprange in definition.Server_IpRange: 1435 | if nftables: 1436 | if isinstance(iprange, ipaddress.IPv4Network): 1437 | rule = f"ip daddr {iprange}" 1438 | else: 1439 | rule = f"ip daddr {iprange[0]}-{iprange[1]}" 1440 | for port1, port2 in definition.Server_PortRange: 1441 | rules.add(f"{rule} tcp dport {port1}-{port2}") 1442 | else: 1443 | rule = [ 1444 | "-p tcp", 1445 | "-m multiport", 1446 | "--dports", ",".join(str(port1) if port1 == port2 else f"{port1}:{port2}" 1447 | for port1, port2 in definition.Server_PortRange) 1448 | ] 1449 | if isinstance(iprange, ipaddress.IPv4Network): 1450 | rule += ["-d", str(iprange)] 1451 | else: 1452 | rule += ["-m", "iprange", "--dst-range", f"{iprange[0]}-{iprange[1]}"] 1453 | rules.add(" ".join(rule)) 1454 | return rules 1455 | 1456 | 1457 | # endregion 1458 | 1459 | # region Implementation 1460 | 1461 | 1462 | @dataclasses.dataclass 1463 | class SocketSet: 1464 | oodle_tcp_base_channel: int 1465 | source: socket.socket 1466 | target: socket.socket 1467 | log_prefix: str 1468 | process_function: callable 1469 | incoming: typing.Optional[bytearray] = dataclasses.field(default_factory=bytearray) 1470 | outgoing: typing.Optional[bytearray] = dataclasses.field(default_factory=bytearray) 1471 | 1472 | 1473 | @dataclasses.dataclass 1474 | class PendingAction: 1475 | action_id: int 1476 | sequence: int 1477 | request_timestamp: float = dataclasses.field(default_factory=time.time) 1478 | response_timestamp: float = 0 1479 | original_wait_time: float = 0 1480 | is_cast: bool = False 1481 | 1482 | 1483 | class NumericStatisticsTracker: 1484 | def __init__(self, count: int, max_age: typing.Optional[float] = None): 1485 | self._count = count 1486 | self._max_age = max_age 1487 | self._values = collections.deque() 1488 | self._expiry = collections.deque() 1489 | 1490 | def add(self, v: float): 1491 | self._values.append(v) 1492 | if self._max_age is not None: 1493 | self._expiry.append(time.time() + self._max_age) 1494 | while len(self._values) > self._count: 1495 | self._values.popleft() 1496 | if self._max_age is not None: 1497 | self._expiry.popleft() 1498 | 1499 | def min(self) -> typing.Optional[float]: 1500 | return min(self._values) if self._values else None 1501 | 1502 | def max(self) -> typing.Optional[float]: 1503 | return max(self._values) if self._values else None 1504 | 1505 | def mean(self) -> typing.Optional[float]: 1506 | return sum(self._values) / len(self._values) if self._values else None 1507 | 1508 | def median(self) -> typing.Optional[float]: 1509 | if not self._values: 1510 | return None 1511 | s = list(sorted(self._values)) 1512 | if len(s) % 2 == 0: 1513 | return (s[len(s) // 2] + s[len(s) // 2 - 1]) / 2 1514 | else: 1515 | return s[len(s) // 2] 1516 | 1517 | def deviation(self) -> typing.Optional[float]: 1518 | if not self._values: 1519 | return None 1520 | mean = self.mean() 1521 | return math.sqrt(sum(pow(x - mean, 2) for x in self._values) / len(self._values)) 1522 | 1523 | def __bool__(self): 1524 | return not not self._values 1525 | 1526 | 1527 | class Connection: 1528 | pending_actions: typing.Deque[PendingAction] = collections.deque() 1529 | opcodes: typing.Optional[OpcodeDefinition] 1530 | 1531 | def __init__(self, sock: socket.socket, source: typing.Tuple[str, int], 1532 | definitions: typing.List[OpcodeDefinition], args: ArgumentTuple, 1533 | firehose_write_fd: int): 1534 | self.args = args 1535 | self.firehose_write_fd = firehose_write_fd 1536 | 1537 | log_path = f"/tmp/xmlm.{datetime.datetime.now():%Y%m%d%H%M%S}.{os.getpid()}.log" 1538 | logging.basicConfig(level=logging.INFO, force=True, 1539 | format="%(asctime)s\t%(process)d\t%(levelname)s\t%(message)s", 1540 | handlers=[ 1541 | logging.FileHandler(log_path, "w"), 1542 | logging.StreamHandler(sys.stderr), 1543 | ]) 1544 | logging.info(f"Log will be saved to {log_path}") 1545 | 1546 | self.source = source 1547 | self.downstream = sock 1548 | self.downstream.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) 1549 | self.downstream.setsockopt(socket.SOL_TCP, socket.TCP_QUICKACK, 1) 1550 | self.downstream.setblocking(False) 1551 | 1552 | self.screen_prefix = f"[{os.getpid():>6}]" 1553 | srv_port, srv_ip = struct.unpack("!2xH4s8x", self.downstream.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16)) 1554 | self.destination = (socket.inet_ntoa(srv_ip), srv_port) 1555 | self.upstream = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1556 | self.upstream.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) 1557 | self.upstream.setsockopt(socket.SOL_TCP, socket.TCP_QUICKACK, 1) 1558 | self.upstream.setblocking(False) 1559 | 1560 | self.last_animation_lock_ends_at = 0 1561 | self.last_successful_request = PendingAction(0, 0) 1562 | 1563 | self.latency_application = NumericStatisticsTracker(10) 1564 | self.latency_upstream = NumericStatisticsTracker(10) 1565 | self.latency_downstream = NumericStatisticsTracker(10) 1566 | self.latency_exaggeration = NumericStatisticsTracker(10, 30.) 1567 | 1568 | dest_ip = ipaddress.IPv4Address(self.destination[0]) 1569 | for definition in definitions: 1570 | for iprange in definition.Server_IpRange: 1571 | if isinstance(iprange, ipaddress.IPv4Network): 1572 | if dest_ip in iprange: 1573 | break 1574 | else: 1575 | if iprange[0] <= dest_ip <= iprange[1]: 1576 | break 1577 | else: 1578 | continue 1579 | 1580 | for port1, port2 in definition.Server_PortRange: 1581 | if port1 <= self.destination[1] <= port2: 1582 | break 1583 | else: 1584 | continue 1585 | 1586 | dn = definition.Name 1587 | self.opcodes = definition 1588 | self.oodle = OodleHelper() 1589 | self._use_oodle_tcp = definition.Common_UseOodleTcp 1590 | 1591 | break 1592 | else: 1593 | self.opcodes = None 1594 | self.oodle = None 1595 | dn = "-" 1596 | logging.info(f"New[{dn}] {self.downstream.getsockname()} {self.downstream.getpeername()} {self.destination}") 1597 | 1598 | def to_upstream(self, bundle_header: XivBundleHeader, 1599 | messages: typing.List[typing.Tuple[XivMessageHeader, bytearray]]): 1600 | for message_header, message_data in messages: 1601 | if message_header.type != XivMessageType.Ipc: 1602 | continue 1603 | try: 1604 | ipc = XivMessageIpcHeader.from_buffer(message_data) 1605 | if ipc.type != XivMessageIpcType.UnknownButInterested: 1606 | continue 1607 | 1608 | if ipc.subtype not in (self.opcodes.C2S_ActionRequest, self.opcodes.C2S_ActionRequestGroundTargeted): 1609 | continue 1610 | 1611 | request = XivMessageIpcActionRequestCommon.from_buffer(message_data, ctypes.sizeof(ipc)) 1612 | self.pending_actions.append(PendingAction(request.action_id, request.sequence)) 1613 | 1614 | # If somehow latest action request has been made before last animation lock end time, keep it. 1615 | # Otherwise... 1616 | if self.pending_actions[-1].request_timestamp > self.last_animation_lock_ends_at: 1617 | 1618 | # If there was no action queued to begin with before the current one, 1619 | # update the base lock time to now. 1620 | if len(self.pending_actions) == 1: 1621 | self.last_animation_lock_ends_at = self.pending_actions[-1].request_timestamp 1622 | 1623 | logging.info(f"C2S_ActionRequest: actionId={request.action_id:04x} sequence={request.sequence:04x}") 1624 | except Exception as e: 1625 | logging.exception(f"unknown error {e} occurred in upstream handler; skipping") 1626 | return bundle_header, messages 1627 | 1628 | def to_downstream(self, bundle_header: XivBundleHeader, 1629 | messages: typing.List[typing.Tuple[XivMessageHeader, bytearray]]): 1630 | message_insertions: typing.List[typing.Tuple[int, XivMessageHeader, bytearray]] = [] 1631 | wait_time_dict: typing.Dict[int, float] = {} 1632 | for i, (message_header, message_data) in enumerate(messages): 1633 | if not message_header.type == XivMessageType.Ipc: 1634 | continue 1635 | if message_header.source_actor != message_header.target_actor: 1636 | continue 1637 | try: 1638 | ipc = XivMessageIpcHeader.from_buffer(message_data) 1639 | if (ipc.type == XivMessageIpcType.XivMitmLatencyMitigatorCustom 1640 | and ipc.subtype == XivMitmLatencyMitigatorCustomSubtype.OriginalWaitTime): 1641 | data = XivMessageIpcCustomOriginalWaitTime.from_buffer(message_data, ctypes.sizeof(ipc)) 1642 | wait_time_dict[data.source_sequence] = data.original_wait_time 1643 | if ipc.type != XivMessageIpcType.UnknownButInterested: 1644 | continue 1645 | if self.opcodes.is_action_effect(ipc.subtype): 1646 | effect = XivMessageIpcActionEffect.from_buffer(message_data, ctypes.sizeof(ipc)) 1647 | original_wait_time = wait_time_dict.get(effect.source_sequence, effect.animation_lock_duration) 1648 | wait_time = original_wait_time 1649 | now = time.time() 1650 | extra_message = "" 1651 | 1652 | if effect.source_sequence == 0: 1653 | # Process actions originating from server. 1654 | if (not self.last_successful_request.is_cast 1655 | and self.last_successful_request.sequence 1656 | and self.last_animation_lock_ends_at > now): 1657 | self.last_successful_request.action_id = effect.action_id 1658 | self.last_successful_request.sequence = 0 1659 | self.last_animation_lock_ends_at += ( 1660 | (original_wait_time + now) 1661 | - (self.last_successful_request.original_wait_time 1662 | + self.last_successful_request.response_timestamp) 1663 | ) 1664 | self.last_animation_lock_ends_at = max(self.last_animation_lock_ends_at, 1665 | now + AUTO_ATTACK_DELAY) 1666 | wait_time = self.last_animation_lock_ends_at - now 1667 | 1668 | extra_message += " serverOriginated" 1669 | 1670 | else: 1671 | while self.pending_actions and self.pending_actions[0].sequence != effect.source_sequence: 1672 | item = self.pending_actions.popleft() 1673 | logging.info(f"\t┎ ActionRequest ignored for processing: actionId={item.action_id:04x} " 1674 | f"sequence={item.sequence:04x}") 1675 | 1676 | if self.pending_actions: 1677 | self.last_successful_request = self.pending_actions.popleft() 1678 | self.last_successful_request.response_timestamp = now 1679 | self.last_successful_request.original_wait_time = original_wait_time 1680 | # 100ms animation lock after cast ends stays. 1681 | # Modify animation lock duration for instant actions only. 1682 | # Since no other action is in progress right before the cast ends, 1683 | # we can safely replace the animation lock with the latest after-cast lock. 1684 | if not self.last_successful_request.is_cast: 1685 | rtt = (self.last_successful_request.response_timestamp 1686 | - self.last_successful_request.request_timestamp) 1687 | self.latency_application.add(rtt) 1688 | extra_message += f" rtt={rtt * 1000:.0f}ms" 1689 | delay, message_append = self.resolve_adjusted_extra_delay(rtt) 1690 | extra_message += message_append 1691 | self.last_animation_lock_ends_at += original_wait_time + delay 1692 | wait_time = self.last_animation_lock_ends_at - now 1693 | 1694 | if math.isclose(wait_time, original_wait_time): 1695 | logging.info(f"S2C_ActionEffect: actionId={effect.action_id:04x} " 1696 | f"sourceSequence={effect.source_sequence:04x} " 1697 | f"wait={int(original_wait_time * 1000)}ms{extra_message}") 1698 | else: 1699 | logging.info(f"S2C_ActionEffect: actionId={effect.action_id:04x} " 1700 | f"sourceSequence={effect.source_sequence:04x} " 1701 | f"wait={int(original_wait_time * 1000)}ms->{int(wait_time * 1000)}ms" 1702 | f"{extra_message}") 1703 | effect.animation_lock_duration = max(0., wait_time) 1704 | 1705 | custom_message_data = bytearray(ctypes.sizeof(XivMessageIpcCustomOriginalWaitTime) 1706 | + ctypes.sizeof(XivMessageIpcHeader)) 1707 | 1708 | custom_ipc = XivMessageIpcHeader.from_buffer(custom_message_data) 1709 | custom_ipc.type = XivMessageIpcType.XivMitmLatencyMitigatorCustom 1710 | custom_ipc.subtype = XivMitmLatencyMitigatorCustomSubtype.OriginalWaitTime 1711 | custom_ipc.server_id = ipc.server_id 1712 | custom_ipc.epoch = ipc.epoch 1713 | 1714 | custom_ipc_original_wait_time = XivMessageIpcCustomOriginalWaitTime.from_buffer( 1715 | custom_message_data, ctypes.sizeof(custom_ipc)) 1716 | custom_ipc_original_wait_time.source_sequence = effect.source_sequence 1717 | 1718 | custom_message = XivMessageHeader() 1719 | custom_message.source_actor = message_header.source_actor 1720 | custom_message.target_actor = message_header.target_actor 1721 | custom_message.type = XivMessageType.Ipc 1722 | custom_message.length = sum(ctypes.sizeof(x) for x in (custom_ipc_original_wait_time, 1723 | custom_ipc, custom_message)) 1724 | 1725 | message_insertions.append((i, custom_message, custom_message_data)) 1726 | 1727 | elif ipc.subtype == self.opcodes.S2C_ActorControlSelf: 1728 | control = XivMessageIpcActorControlSelf.from_buffer(message_data, ctypes.sizeof(ipc)) 1729 | if control.category == XivMessageIpcActorControlCategory.Rollback: 1730 | action_id = control.param_3 1731 | source_sequence = control.param_6 1732 | while (self.pending_actions 1733 | and ( 1734 | (source_sequence and self.pending_actions[0].sequence != source_sequence) 1735 | or (not source_sequence and self.pending_actions[0].action_id != action_id) 1736 | )): 1737 | item = self.pending_actions.popleft() 1738 | logging.info(f"\t┎ ActionRequest ignored for processing: actionId={item.action_id:04x} " 1739 | f"sequence={item.sequence:04x}") 1740 | 1741 | if self.pending_actions: 1742 | self.pending_actions.popleft() 1743 | 1744 | logging.info(f"S2C_ActorControlSelf/ActionRejected: " 1745 | f"actionId={action_id:04x} " 1746 | f"sourceSequence={source_sequence:08x}") 1747 | 1748 | elif ipc.subtype == self.opcodes.S2C_ActorControl: 1749 | control = XivMessageIpcActorControl.from_buffer(message_data, ctypes.sizeof(ipc)) 1750 | if control.category == XivMessageIpcActorControlCategory.CancelCast: 1751 | action_id = control.param_3 1752 | while self.pending_actions and self.pending_actions[0].action_id != action_id: 1753 | item = self.pending_actions.popleft() 1754 | logging.info(f"\t┎ ActionRequest ignored for processing: actionId={item.action_id:04x} " 1755 | f"sequence={item.sequence:04x}") 1756 | 1757 | if self.pending_actions: 1758 | self.pending_actions.popleft() 1759 | 1760 | logging.info(f"S2C_ActorControl/CancelCast: actionId={action_id:04x}") 1761 | 1762 | elif ipc.subtype == self.opcodes.S2C_ActorCast: 1763 | cast = XivMessageIpcActorCast.from_buffer(message_data, ctypes.sizeof(ipc)) 1764 | 1765 | # Mark that the last request was a cast. 1766 | # If it indeed is a cast, the game UI will block the user from generating additional requests, 1767 | # so first item is guaranteed to be the cast action. 1768 | if self.pending_actions: 1769 | self.pending_actions[0].is_cast = True 1770 | 1771 | logging.info(f"S2C_ActorCast: actionId={cast.action_id:04x} type={cast.skill_type:04x} " 1772 | f"action_id_2={cast.action_id_2:04x} time={cast.cast_time:.3f} " 1773 | f"target_id={cast.target_id:08x}") 1774 | 1775 | except Exception as e: 1776 | logging.exception(f"unknown error {e} occurred in downstream handler; skipping") 1777 | for i, message_header, message_data in reversed(message_insertions): 1778 | messages.insert(i, (message_header, message_data)) 1779 | return bundle_header, messages 1780 | 1781 | def run(self): 1782 | bundle_header: XivBundleHeader 1783 | messages: typing.List[typing.Tuple[XivMessageHeader, bytearray]] 1784 | 1785 | self.upstream.settimeout(3) 1786 | with contextlib.ExitStack() as estack: 1787 | estack.enter_context(self.downstream) 1788 | estack.enter_context(self.upstream) 1789 | if self.oodle is not None: 1790 | estack.enter_context(self.oodle) 1791 | 1792 | try: 1793 | self.upstream.connect((str(self.destination[0]), self.destination[1])) 1794 | self.upstream.settimeout(None) 1795 | 1796 | check_targets = ( 1797 | (self.downstream, SocketSet(0, self.downstream, self.upstream, "D->U", self.to_upstream)), 1798 | (self.upstream, SocketSet(2, self.upstream, self.downstream, "U->D", self.to_downstream)), 1799 | ) 1800 | while True: 1801 | rlist = [ 1802 | k for k, v in check_targets if v.incoming is not None 1803 | ] 1804 | wlist = [ 1805 | k for k, v in check_targets if v.outgoing 1806 | ] 1807 | if not rlist and not wlist: 1808 | break 1809 | 1810 | rlist, wlist, _ = select.select(rlist, wlist, [], 60) 1811 | if not rlist and not wlist: # timeout or empty 1812 | break 1813 | 1814 | for direction, (_, target) in enumerate(check_targets): 1815 | if target.source in rlist: 1816 | try: 1817 | data = target.source.recv(65536) 1818 | if not data: 1819 | raise EOFError 1820 | except (OSError, EOFError): 1821 | logging.info(f"{target.log_prefix} Read finish") 1822 | target.incoming = None 1823 | continue 1824 | 1825 | if self.opcodes is None: 1826 | target.outgoing.extend(data) 1827 | else: 1828 | target.incoming.extend(data) 1829 | it = XivBundleHeader.find( 1830 | bytearray(target.incoming), self.oodle, 1831 | target.oodle_tcp_base_channel if self._use_oodle_tcp else 0xFFFFFFFF) 1832 | while True: 1833 | try: 1834 | bundle = next(it) 1835 | except StopIteration as e: 1836 | del target.incoming[0:e.value] 1837 | break 1838 | 1839 | if isinstance(bundle, (bytes, bytearray)): 1840 | logging.info(f"{target.log_prefix} discarded " + 1841 | " ".join(f"{x:02x}" for x in bundle)) 1842 | target.outgoing.extend(bundle) 1843 | else: 1844 | bundle_header, messages = bundle 1845 | bundle_header, messages = target.process_function(bundle_header, messages) 1846 | 1847 | message_bytes = bytearray() 1848 | for message_header, message_data in messages: 1849 | # noinspection PyTypeChecker 1850 | message_bytes.extend(bytes(message_header)) 1851 | message_bytes.extend(message_data) 1852 | 1853 | bundle_header.decoded_body_length = len(message_bytes) 1854 | bundle_header.message_count = len(messages) 1855 | 1856 | if self.firehose_write_fd != -1: 1857 | original_compression = bundle_header.compression 1858 | bundle_header.compression = 0 1859 | bundle_header.length = ctypes.sizeof(bundle_header) + len(message_bytes) 1860 | os.write(self.firehose_write_fd, 1861 | (8 + bundle_header.length).to_bytes(4, "little")) 1862 | os.write(self.firehose_write_fd, direction.to_bytes(8, "little")) 1863 | 1864 | # noinspection PyTypeChecker 1865 | os.write(self.firehose_write_fd, bytes(bundle_header)) 1866 | os.write(self.firehose_write_fd, message_bytes) 1867 | bundle_header.compression = original_compression 1868 | 1869 | if bundle_header.compression == 1: 1870 | message_bytes = zlib.compress(message_bytes) 1871 | elif bundle_header.compression == 2: 1872 | message_bytes = self.oodle.encode( 1873 | target.oodle_tcp_base_channel + 1 if self._use_oodle_tcp else 0xFFFFFFFF, 1874 | message_bytes) 1875 | 1876 | bundle_header.length = ctypes.sizeof(bundle_header) + len(message_bytes) 1877 | 1878 | # noinspection PyTypeChecker 1879 | target.outgoing.extend(bytes(bundle_header)) 1880 | target.outgoing.extend(message_bytes) 1881 | 1882 | for direction, target in check_targets: 1883 | if target.outgoing is None: 1884 | continue 1885 | if target.outgoing: 1886 | try: 1887 | target.target.send(target.outgoing) 1888 | except socket.error as e: 1889 | if e.errno not in (socket.EWOULDBLOCK, socket.EAGAIN): 1890 | raise 1891 | continue 1892 | target.outgoing.clear() 1893 | if target.incoming is None: 1894 | target.outgoing = None 1895 | logging.info(f"{target.log_prefix} Source read and target write shutdown") 1896 | try: 1897 | target.source.shutdown(socket.SHUT_RD) 1898 | except OSError: 1899 | pass 1900 | try: 1901 | target.target.shutdown(socket.SHUT_WR) 1902 | except OSError: 1903 | pass 1904 | logging.info("Closed") 1905 | except Exception as e: 1906 | logging.info(f"Closed, exception occurred: {type(e)} {e}", exc_info=True) 1907 | return -1 1908 | except KeyboardInterrupt: 1909 | # do no cleanup 1910 | # noinspection PyProtectedMember,PyUnresolvedReferences 1911 | os._exit(0) 1912 | return 0 1913 | 1914 | def resolve_adjusted_extra_delay(self, rtt: float) -> typing.Tuple[float, str]: 1915 | if not self.args.measure_ping: 1916 | return self.args.extra_delay, "" 1917 | 1918 | extra_message = "" 1919 | latency_downstream = TcpInfo.get_latency(self.downstream) 1920 | latency_upstream = TcpInfo.get_latency(self.upstream) 1921 | if latency_downstream is not None: 1922 | self.latency_downstream.add(latency_downstream) 1923 | extra_message += f" downstream={int(latency_downstream * 1000)}ms" 1924 | if latency_upstream is not None: 1925 | self.latency_upstream.add(latency_upstream) 1926 | extra_message += f" upstream={int(latency_upstream * 1000)}ms" 1927 | if latency_downstream is None or latency_upstream is None: 1928 | return self.args.extra_delay, extra_message 1929 | 1930 | latency = latency_downstream + latency_upstream 1931 | if latency > rtt: 1932 | self.latency_exaggeration.add(latency - rtt) 1933 | 1934 | if self.latency_exaggeration: 1935 | exaggeration = self.latency_exaggeration.median() 1936 | extra_message += f" latency={latency * 1000:.0f}ms->{1000 * (latency - exaggeration):.0f}ms" 1937 | latency -= exaggeration 1938 | else: 1939 | extra_message += f" latency={latency * 1000:.0f}ms" 1940 | 1941 | if rtt > 100 and latency < 5: 1942 | extra_message += " unreliableLatency" 1943 | return self.args.extra_delay, extra_message 1944 | 1945 | rtt_min = self.latency_application.min() 1946 | rtt_mean = self.latency_application.mean() 1947 | rtt_deviation = self.latency_application.deviation() 1948 | latency_mean = self.latency_upstream.mean() + self.latency_downstream.mean() 1949 | latency_deviation = self.latency_upstream.deviation() + self.latency_downstream.deviation() 1950 | 1951 | # Correct latency and server response time values in case of outliers. 1952 | latency = clamp(latency, latency_mean - latency_deviation, latency_mean + latency_deviation) 1953 | rtt = clamp(rtt, rtt_mean - rtt_deviation, rtt_mean + rtt_deviation) 1954 | 1955 | # Estimate latency based on server response time statistics. 1956 | latency_estimate = (rtt + rtt_min + rtt_mean) / 3 - rtt_deviation 1957 | extra_message += f" latencyEstimate={latency_estimate * 1000:.0f}ms" 1958 | 1959 | # Correct latency value based on estimate if server response time is stable. 1960 | latency = max(latency_estimate, latency) 1961 | 1962 | # This delay is based on server's processing time. 1963 | # If the server is busy, everyone should feel the same effect. 1964 | # * Only the player's ping is taken out of the equation. (- latencyAdjusted) 1965 | # * Prevent accidentally too high ExtraDelay. (Clamp above 1ms) 1966 | delay = clamp(rtt - latency, 0.001, self.args.extra_delay * 2) 1967 | extra_message += f" delayAdjusted={delay * 1000:.0f}ms" 1968 | return delay, extra_message 1969 | 1970 | 1971 | # endregion 1972 | 1973 | # region ZiPatch download/unpacker 1974 | 1975 | def download_exe(src_url: str): 1976 | print("Downloading:", src_url) 1977 | with (open(src_url, "rb") if os.path.exists(src_url) else urllib.request.urlopen(src_url)) as resp: 1978 | data = bytearray(resp.read()) 1979 | if data[0:2] == b'MZ': 1980 | dosh = ImageDosHeader.from_buffer(data) 1981 | nth = ImageNtHeaders32.from_buffer(dosh.e_lfanew) 1982 | if nth.FileHeader.Machine == 0x014c: 1983 | print("x86 binary detected; treating the file as ffxiv.exe") 1984 | with open("ffxiv.exe", "wb") as fp: 1985 | fp.write(data) 1986 | elif nth.FileHeader.Machine == 0x8664: 1987 | print("x86 binary detected; treating the file as ffxiv_dx11.exe") 1988 | with open("ffxiv_dx11.exe", "wb") as fp: 1989 | fp.write(data) 1990 | return 1991 | 1992 | print("Looking for ffxiv.exe and ffxiv_dx11.exe in the downloaded patch file...") 1993 | with io.BytesIO(data) as fp: 1994 | fp.seek(0, os.SEEK_SET) 1995 | fp: typing.Union[typing.BinaryIO, io.BytesIO] 1996 | fp.readinto(hdr := ZiPatchHeader()) 1997 | if hdr.signature != ZiPatchHeader.SIGNATURE: 1998 | raise RuntimeError("downloaded file is neither a .patch file or .exe file") 1999 | 2000 | target_files = { 2001 | "ffxiv.exe": [], 2002 | "ffxiv_dx11.exe": [], 2003 | } 2004 | 2005 | while fp.readinto(hdr := ZiPatchChunkHeader()): 2006 | offset = fp.tell() 2007 | if hdr.type == b"SQPK": 2008 | fp.readinto(sqpkhdr := ZiPatchSqpackHeader()) 2009 | if sqpkhdr.command in (b"T", b"X"): 2010 | pass 2011 | 2012 | elif sqpkhdr.command == ZiPatchSqpackFileAddHeader.COMMAND: 2013 | fp.readinto(sqpkhdr2 := ZiPatchSqpackFileAddHeader()) 2014 | path = fp.read(sqpkhdr2.path_size).split(b"\0", 1)[0].decode("utf-8") 2015 | target_file = target_files.get(path, None) 2016 | chunks = [] 2017 | 2018 | current_file_offset = sqpkhdr2.offset 2019 | if target_file is not None: 2020 | if current_file_offset == 0: 2021 | target_file.clear() 2022 | target_file.append((current_file_offset, chunks)) 2023 | while fp.tell() < offset + hdr.size: 2024 | fp.readinto(block_header := BlockHeader()) 2025 | block_data_size = block_header.compressed_size if block_header.is_compressed() else block_header.decompressed_size 2026 | padded_block_size = (block_data_size + ctypes.sizeof(block_header) + 127) & 0xFFFFFF80 2027 | if target_file is not None: 2028 | x = fp.read(padded_block_size - ctypes.sizeof(block_header))[:block_data_size] 2029 | if block_header.is_compressed(): 2030 | x = zlib.decompress(x, -zlib.MAX_WBITS) 2031 | if len(x) != block_header.decompressed_size: 2032 | raise RuntimeError("Corrupt patch file") 2033 | chunks.append(x) 2034 | else: 2035 | fp.seek(padded_block_size - ctypes.sizeof(block_header), os.SEEK_CUR) 2036 | current_file_offset += block_header.decompressed_size 2037 | 2038 | elif sqpkhdr.command == ZiPatchSqpackFileDeleteHeader.COMMAND: 2039 | fp.readinto(sqpkhdr2 := ZiPatchSqpackFileDeleteHeader()) 2040 | fp.seek(sqpkhdr2.path_size, io.SEEK_CUR) 2041 | 2042 | elif sqpkhdr.command == ZiPatchSqpackAddData.COMMAND: 2043 | fp.readinto(sqpkhdr2 := ZiPatchSqpackAddData()) 2044 | 2045 | elif sqpkhdr.command in ZiPatchSqpackZeroData.COMMANDS: 2046 | fp.readinto(sqpkhdr2 := ZiPatchSqpackZeroData()) 2047 | 2048 | elif sqpkhdr.command in {b'HDV', b'HDI', b'HDD', b'HIV', b'HII', b'HID'}: 2049 | fp.readinto(sqpkhdr2 := ZiPatchSqpackFileResolver()) 2050 | 2051 | else: 2052 | print(f"Skipping {hdr.type}:{sqpkhdr.command}") 2053 | 2054 | fp.seek(offset + hdr.size, os.SEEK_SET) 2055 | fp.readinto(ZiPatchChunkFooter()) 2056 | if hdr.type == b"EOF_": 2057 | break 2058 | elif hdr.type != b"SQPK": 2059 | print(f"Skipping {hdr.type}") 2060 | 2061 | found_any_file = False 2062 | for target_file_name, target_file_data in target_files.items(): 2063 | if not target_file_data: 2064 | continue 2065 | with open(target_file_name, "wb") as fp: 2066 | for offset, chunks in target_file_data: 2067 | fp.seek(offset, os.SEEK_SET) 2068 | fp.writelines(chunks) 2069 | print(f"Saved: {target_file_name}") 2070 | found_any_file = True 2071 | 2072 | if not found_any_file: 2073 | raise RuntimeError("downloaded patch file does not contain a .exe file") 2074 | 2075 | 2076 | # endregion 2077 | 2078 | # region Oodle 2079 | 2080 | # region Oodle wrappers 2081 | 2082 | def oodle_malloc_impl(size: int, align: int) -> int: 2083 | raw = crt_malloc(size + align + POINTER_SIZE - 1) 2084 | if raw == 0: 2085 | return 0 2086 | 2087 | aligned = (raw + align + POINTER_SIZE - 1) & ((~align & (sys.maxsize * 2 + 1)) + 1) 2088 | ctypes.c_void_p.from_address(aligned - POINTER_SIZE).value = raw 2089 | return aligned 2090 | 2091 | 2092 | def oodle_free_impl(aligned: int): 2093 | crt_free(ctypes.c_void_p.from_address(aligned - POINTER_SIZE).value) 2094 | 2095 | 2096 | class OodleModule: 2097 | def __init__(self, image: PeImage): 2098 | self._image = image 2099 | 2100 | text = image.section_header(b".text") 2101 | text_view = image.section(text) 2102 | 2103 | if POINTER_SIZE == 8: 2104 | pattern = br"\x75.\x48\x8d\x15....\x48\x8d\x0d....\xe8(....)\xc6\x05....\x01.{0,256}\x75.\xb9(....)\xe8(....)\x45\x33\xc0\x33\xd2\x48\x8b\xc8\xe8.....{0,6}\x41\xb9(....)\xba.....{0,6}\x48\x8b\xc8\xe8(....)" 2105 | else: 2106 | pattern = br"\x75\x16\x68....\x68....\xe8(....)\xc6\x05....\x01.{0,256}\x75\x27\x6a(.)\xe8(....)\x6a\x00\x6a\x00\x50\xe8....\x83\xc4.\x89\x46.\x68(....)\xff\x76.\x6a.\x50\xe8(....)" 2107 | match = re.search(pattern, text_view, re.DOTALL) 2108 | if not match: 2109 | raise RuntimeError("Could not find InitOodle.") 2110 | self.set_malloc_free_address = image.address.value + image.resolve_rip_relative( 2111 | text.VirtualAddress + match.start(1) - 1) 2112 | self.htbits = int.from_bytes(match.group(2), "little") 2113 | self.shared_size_address = image.address.value + image.resolve_rip_relative( 2114 | text.VirtualAddress + match.start(3) - 1) 2115 | self.window = int.from_bytes(match.group(4), "little") 2116 | self.shared_set_window_address = image.address.value + image.resolve_rip_relative( 2117 | text.VirtualAddress + match.start(5) - 1) 2118 | 2119 | if POINTER_SIZE == 8: 2120 | pattern = br"\x75\x04\x48\x89..\xe8(....)\x4c..\xe8(....).{0,256}\x01\x75\x0a\x48\x8b.\xe8(....)\xeb\x09\x48\x8b.\x08\xe8(....)" 2121 | else: 2122 | pattern = br"\xe8(....)\x8b\xd8\xe8(....)\x83\x7d\x10\x01.{0,256}\x83\x7d\x10\x01\x6a\x00\x6a\x00\x6a\x00\xff\x77.\x75\x09\xff.\xe8(....)\xeb\x08\xff\x76.\xe8(....)" 2123 | match = re.search(pattern, text_view, re.DOTALL) 2124 | if not match: 2125 | raise RuntimeError("Could not find SetUpStatesAndTrain.") 2126 | self.udp_state_size_address = image.address.value + image.resolve_rip_relative( 2127 | text.VirtualAddress + match.start(1) - 1) 2128 | self.tcp_state_size_address = image.address.value + image.resolve_rip_relative( 2129 | text.VirtualAddress + match.start(2) - 1) 2130 | self.tcp_train_address = image.address.value + image.resolve_rip_relative( 2131 | text.VirtualAddress + match.start(3) - 1) 2132 | self.udp_train_address = image.address.value + image.resolve_rip_relative( 2133 | text.VirtualAddress + match.start(4) - 1) 2134 | 2135 | if POINTER_SIZE == 8: 2136 | match = re.search(br"\x4d\x85\xd2\x74\x0a\x49\x8b\xca\xe8(....)\xeb\x09\x48\x8b\x49\x08\xe8(....)", 2137 | text_view, re.DOTALL) 2138 | if not match: 2139 | raise RuntimeError("Could not find Tcp/UdpDecode.") 2140 | self.tcp_decode_address = image.address.value + image.resolve_rip_relative( 2141 | text.VirtualAddress + match.start(1) - 1) 2142 | self.udp_decode_address = image.address.value + image.resolve_rip_relative( 2143 | text.VirtualAddress + match.start(2) - 1) 2144 | 2145 | match = re.search( 2146 | br"\x48\x85\xc0\x74\x0d\x48\x8b\xc8\xe8(....)\x48..\xeb\x0b\x48\x8b\x49\x08\xe8(....)", 2147 | text_view, re.DOTALL) 2148 | if not match: 2149 | raise RuntimeError("Could not find Tcp/UdpEncode.") 2150 | self.tcp_encode_address = image.address.value + image.resolve_rip_relative( 2151 | text.VirtualAddress + match.start(1) - 1) 2152 | self.udp_encode_address = image.address.value + image.resolve_rip_relative( 2153 | text.VirtualAddress + match.start(2) - 1) 2154 | else: 2155 | pattern = re.compile(br"\x85\xc0\x74.\x50\xe8(....)\x57\x8b\xf0\xff\x15", re.DOTALL) 2156 | match = pattern.search(text_view) 2157 | if not match: 2158 | raise RuntimeError("Could not find TcpEncode.") 2159 | self.tcp_encode_address = image.address.value + image.resolve_rip_relative( 2160 | text.VirtualAddress + match.start(1) - 1) 2161 | match = pattern.search(text_view, match.end()) 2162 | if not match: 2163 | raise RuntimeError("Could not find TcpDecode.") 2164 | self.tcp_decode_address = image.address.value + image.resolve_rip_relative( 2165 | text.VirtualAddress + match.start(1) - 1) 2166 | 2167 | pattern = re.compile(br"\xff\x71\x04\xe8(....)\x57\x8b\xf0\xff\x15", re.DOTALL) 2168 | match = pattern.search(text_view) 2169 | if not match: 2170 | raise RuntimeError("Could not find UdpEncode.") 2171 | self.udp_encode_address = image.address.value + image.resolve_rip_relative( 2172 | text.VirtualAddress + match.start(1) - 1) 2173 | match = pattern.search(text_view, match.end()) 2174 | if not match: 2175 | raise RuntimeError("Could not find UdpDecode.") 2176 | self.udp_decode_address = image.address.value + image.resolve_rip_relative( 2177 | text.VirtualAddress + match.start(1) - 1) 2178 | 2179 | self.set_malloc_free = Oodle_SetMallocFree(self.set_malloc_free_address) 2180 | self.shared_size = OodleNetwork1_Shared_Size(self.shared_size_address) 2181 | self.shared_set_window = OodleNetwork1_Shared_SetWindow(self.shared_set_window_address) 2182 | self.udp_state_size = OodleNetwork1_Proto_State_Size(self.udp_state_size_address) 2183 | self.tcp_state_size = OodleNetwork1_Proto_State_Size(self.tcp_state_size_address) 2184 | self.tcp_train = OodleNetwork1_Proto_Train(self.tcp_train_address) 2185 | self.udp_train = OodleNetwork1_Proto_Train(self.udp_train_address) 2186 | self.tcp_decode = OodleNetwork1_Proto_Decode(self.tcp_decode_address) 2187 | self.udp_decode = OodleNetwork1_Proto_Decode(self.udp_decode_address) 2188 | self.tcp_encode = OodleNetwork1_Proto_Encode(self.tcp_encode_address) 2189 | self.udp_encode = OodleNetwork1_Proto_Encode(self.udp_encode_address) 2190 | 2191 | if POINTER_SIZE == 8: 2192 | # patch _alloca_probe 2193 | pattern = br"\x48\x83\xec\x10\x4c\x89\x14\x24\x4c\x89\x5c\x24\x08\x4d\x33\xdb" 2194 | match = re.search(pattern, text_view) 2195 | if not match: 2196 | raise RuntimeError("_alloca_probe not found") 2197 | image.view[text.VirtualAddress + match.start(0)] = 0xc3 2198 | 2199 | else: 2200 | # patch fs register access 2201 | ctypes.memset(self.tcp_train_address + 0xaba - 0xAB0, 0x90, 6) 2202 | ctypes.memset(self.tcp_train_address + 0xad2 - 0xAB0, 0x90, 6) 2203 | ctypes.memset(self.tcp_train_address + 0xbb4 - 0xAB0, 0x90, 7) 2204 | ctypes.memset(self.tcp_train_address + 0xbc8 - 0xAB0, 0x90, 7) 2205 | 2206 | self._c_oodle_malloc_impl = Oodle_Malloc(oodle_malloc_impl) 2207 | self._c_oodle_free_impl = Oodle_Free(oodle_free_impl) 2208 | 2209 | self.set_malloc_free(self._c_oodle_malloc_impl.address(), self._c_oodle_free_impl.address()) 2210 | 2211 | 2212 | class OodleInstance: 2213 | def __init__(self, module: OodleModule, use_tcp: bool): 2214 | self._state = (ctypes.c_uint8 * (module.tcp_state_size() if use_tcp else module.udp_state_size()))() 2215 | self._shared = (ctypes.c_uint8 * module.shared_size(module.htbits))() 2216 | self._window = (ctypes.c_uint8 * module.window)() 2217 | module.shared_set_window( 2218 | ctypes.addressof(self._shared), module.htbits, 2219 | ctypes.addressof(self._window), len(self._window)) 2220 | (module.tcp_train if use_tcp else module.udp_train)( 2221 | ctypes.addressof(self._state), 2222 | ctypes.addressof(self._shared), 2223 | ctypes.POINTER(ctypes.c_void_p)(), 2224 | ctypes.POINTER(ctypes.c_int32)(), 2225 | 0) 2226 | self._encode_function = module.tcp_encode if use_tcp else module.udp_encode 2227 | self._decode_function = module.tcp_decode if use_tcp else module.udp_decode 2228 | 2229 | def encode(self, src: typing.Union[bytes, bytearray, memoryview]) -> bytearray: 2230 | if not isinstance(src, (bytearray, memoryview)): 2231 | src = bytearray(src) 2232 | enc = bytearray(len(src) + 8) 2233 | del enc[self._encode_function( 2234 | ctypes.addressof(self._state), 2235 | ctypes.addressof(self._shared), 2236 | ctypes.addressof(ctypes.c_byte.from_buffer(src)), len(src), 2237 | ctypes.addressof(ctypes.c_byte.from_buffer(enc))):] 2238 | return enc 2239 | 2240 | def decode(self, enc: typing.Union[bytes, bytearray, memoryview], result_length: int) -> bytearray: 2241 | dec = bytearray(result_length) 2242 | if not self._decode_function( 2243 | ctypes.addressof(self._state), 2244 | ctypes.addressof(self._shared), 2245 | ctypes.addressof(ctypes.c_byte.from_buffer(enc)), len(enc), 2246 | ctypes.addressof(ctypes.c_byte.from_buffer(dec)), len(dec)): 2247 | raise RuntimeError("Oodle decode fail") 2248 | return dec 2249 | 2250 | 2251 | # endregion 2252 | 2253 | 2254 | class BaseOodleHelper: 2255 | def __enter__(self): 2256 | raise NotImplementedError 2257 | 2258 | def __exit__(self, exc_type, exc_val, exc_tb): raise NotImplementedError 2259 | 2260 | def encode(self, channel: int, data: bytes) -> bytes: 2261 | raise NotImplementedError 2262 | 2263 | def decode(self, channel: int, data: bytes, declen: int) -> bytes: 2264 | raise NotImplementedError 2265 | 2266 | 2267 | class OodleWithBudgetAbiThunks(BaseOodleHelper): 2268 | _module: typing.ClassVar[typing.Optional[OodleModule]] = None 2269 | 2270 | def __init__(self): 2271 | self._channels = { 2272 | 0xFFFFFFFF: OodleInstance(self._module, False), 2273 | 0: OodleInstance(self._module, True), 2274 | 1: OodleInstance(self._module, True), 2275 | 2: OodleInstance(self._module, True), 2276 | 3: OodleInstance(self._module, True), 2277 | } 2278 | 2279 | def __enter__(self): 2280 | return self 2281 | 2282 | def __exit__(self, exc_type, exc_val, exc_tb): 2283 | pass 2284 | 2285 | def encode(self, channel: int, data: bytes) -> bytes: 2286 | return self._channels[channel].encode(data) 2287 | 2288 | def decode(self, channel: int, data: bytes, declen: int) -> bytes: 2289 | return self._channels[channel].decode(data, declen) 2290 | 2291 | @classmethod 2292 | def init_module(cls): 2293 | ffxiv_exe_filepath = os.path.join(SCRIPT_DIRECTORY, "ffxiv.exe") 2294 | ffxiv_dx11_exe_filepath = os.path.join(SCRIPT_DIRECTORY, "ffxiv_dx11.exe") 2295 | if POINTER_SIZE == 4: 2296 | if not os.path.exists(ffxiv_exe_filepath): 2297 | raise RuntimeError("Need ffxiv.exe in the same directory. " 2298 | "Copy one from your local Windows/Mac installation.") 2299 | 2300 | cls._module = OodleModule(PeImage(pathlib.Path(ffxiv_exe_filepath).read_bytes())) 2301 | elif POINTER_SIZE == 8: 2302 | if not os.path.exists(ffxiv_dx11_exe_filepath): 2303 | raise RuntimeError("Need ffxiv_dx11.exe in the same directory. " 2304 | "Copy one from your local Windows/Mac installation.") 2305 | 2306 | cls._module = OodleModule(PeImage(pathlib.Path(ffxiv_dx11_exe_filepath).read_bytes())) 2307 | 2308 | 2309 | OodleHelper = OodleWithBudgetAbiThunks 2310 | 2311 | 2312 | def test_oodle(): 2313 | testval = b'\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x04' * 16 2314 | with OodleHelper() as oodle: 2315 | enc = oodle.encode(0, testval) 2316 | dec = oodle.decode(1, enc, len(testval)) 2317 | if testval != dec: 2318 | print(testval) 2319 | print(enc) 2320 | print(dec) 2321 | print(f"Oodle test fail (TCP)") 2322 | return False 2323 | enc = oodle.encode(0xFFFFFFFF, testval) 2324 | dec = oodle.decode(0xFFFFFFFF, enc, len(testval)) 2325 | if testval != dec: 2326 | print(testval) 2327 | print(enc) 2328 | print(dec) 2329 | print(f"Oodle test fail (UDP)") 2330 | return False 2331 | return True 2332 | 2333 | 2334 | # endregion 2335 | 2336 | 2337 | def __main__() -> int: 2338 | if sys.version_info < (3, 8): 2339 | print("This script requires at least python 3.8") 2340 | return -1 2341 | 2342 | logging.basicConfig(level=logging.INFO, force=True, 2343 | format="%(asctime)s\t%(process)d(main)\t%(levelname)s\t%(message)s", 2344 | handlers=[ 2345 | logging.StreamHandler(sys.stderr), 2346 | ]) 2347 | 2348 | parser = argparse.ArgumentParser("XivMitmLatencyMitigator: https://github.com/Soreepeong/XivMitmLatencyMitigator") 2349 | parser.add_argument("-r", "--region", action="append", dest="region", default=[], choices=("JP", "CN", "KR"), 2350 | help="Filters connection by regions. Can be specified multiple times. Does nothing if -j is specified.") 2351 | parser.add_argument("-e", "--extra-delay", action="store", dest="extra_delay", default=0.075, type=float, 2352 | help=EXTRA_DELAY_HELP) 2353 | parser.add_argument("-m", "--measure-ping", action="store_true", dest="measure_ping", default=False, 2354 | help="Use measured latency information from sockets to server and client to adjust extra delay." 2355 | ) 2356 | parser.add_argument("-u", "--update-opcodes", action="store_true", dest="update_opcodes", default=False, 2357 | help="Download new opcodes again; do not use cached opcodes file.") 2358 | parser.add_argument("-j", "--json-path", action="store", dest="json_path", default=None, 2359 | help="Read opcode definition JSON file from the given path.") 2360 | parser.add_argument("-x", "--exe", action="append", dest="exe_url", default=[], 2361 | help="Download ffxiv.exe and/or ffxiv_dx11.exe from specified URL (exe or patch file.)") 2362 | parser.add_argument("-n", "--nftables", action="store_true", dest="nftables", default=False, 2363 | help="Use nft instead of iptables.") 2364 | parser.add_argument("--firehose", action="store", dest="firehose", default=None, 2365 | help="Open a TCP listening socket that will receive all decoded game networking data transferred. (ex: 0.0.0.0:1234)") 2366 | parser.add_argument("--no-sysctl", action="store_false", dest="write_sysctl", default=True, 2367 | help="Do not change sysctl ip_forward and route_localnet.") 2368 | 2369 | args: typing.Union[ArgumentTuple, argparse.Namespace] = parser.parse_args() 2370 | 2371 | if args.extra_delay < 0: 2372 | logging.warning("Extra delay cannot be a negative number.") 2373 | return -1 2374 | 2375 | for url in args.exe_url: 2376 | url = url.strip() 2377 | if url: 2378 | download_exe(url) 2379 | 2380 | if args.json_path: 2381 | logging.info(f"Opcode definition JSON file path: {args.json_path}") 2382 | else: 2383 | logging.info(f"Region filter: {', '.join(args.region) if args.region else '(None)'}") 2384 | logging.info(f"Extra delay: {args.extra_delay}s") 2385 | logging.info(f"Use measured socket latency: {'yes' if args.measure_ping else 'no'}") 2386 | logging.info(f"Write sysctl values: {'yes' if args.write_sysctl else 'no'}") 2387 | 2388 | if sys.platform == 'linux': 2389 | OodleWithBudgetAbiThunks.init_module() 2390 | else: 2391 | logging.error("Only linux is supported at the moment.") 2392 | return -1 2393 | 2394 | if not test_oodle(): 2395 | print("Oodle test fail") 2396 | return -1 2397 | 2398 | poller = select.poll() 2399 | 2400 | if args.firehose: 2401 | firehose_listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 2402 | poller.register(firehose_listener.fileno(), select.POLLIN) 2403 | ip, port = args.firehose.split(":") 2404 | port = int(port) 2405 | firehose_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 2406 | firehose_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 2407 | firehose_listener.bind((ip, port)) 2408 | firehose_listener.listen(32) 2409 | logging.info(f"Firehose listening on {firehose_listener.getsockname()}...") 2410 | else: 2411 | firehose_listener = None 2412 | 2413 | listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 2414 | poller.register(listener.fileno(), select.POLLIN) 2415 | if hasattr(socket, "TCP_NODELAY"): 2416 | listener.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) 2417 | if hasattr(socket, "TCP_QUICKACK"): 2418 | listener.setsockopt(socket.SOL_TCP, socket.TCP_QUICKACK, 1) 2419 | while True: 2420 | port = random.randint(10000, 65535) 2421 | try: 2422 | listener.bind(("0.0.0.0", port)) 2423 | except OSError: 2424 | continue 2425 | break 2426 | 2427 | definitions = load_definitions(args.update_opcodes, args.json_path) 2428 | if args.region and (args.json_path is None or args.json_path.strip() == ""): 2429 | definitions = [x for x in definitions if any(r.lower() in x.Name.lower() for r in args.region)] 2430 | 2431 | removal_cmds = [] 2432 | err = 0 2433 | is_child = False 2434 | cleanup_filepath = os.path.join(SCRIPT_DIRECTORY, ".cleanup.sh") 2435 | if os.path.exists(cleanup_filepath): 2436 | os.system(cleanup_filepath) 2437 | 2438 | child_pid_to_rfd: typing.Dict[int, int] = {} 2439 | child_rfds: typing.Set[int] = set() 2440 | firehose_clients: typing.Dict[int, socket.socket] = {} 2441 | firehose_backlog: typing.Dict[int, typing.Deque[typing.Tuple[int, bytearray]]] = {} 2442 | 2443 | try: 2444 | with open(cleanup_filepath, "w") as fp: 2445 | fp.write("#!/bin/sh\n") 2446 | for i, rule in enumerate(load_rules(port, definitions, args.nftables)): 2447 | if args.nftables: 2448 | add_cmd = f"nft rule ip nat PREROUTING {rule} dnat 127.0.0.1:{port} comment XMLM_{os.getpid()}_{i}_" 2449 | else: 2450 | add_cmd = f"iptables -t nat -I PREROUTING {rule} -j REDIRECT --to {port}" 2451 | logging.info(f"Running: {add_cmd}") 2452 | if os.system(add_cmd): 2453 | raise RootRequiredError 2454 | 2455 | if args.nftables: 2456 | h = ( 2457 | os.popen(f"nft --handle list ruleset | grep XMLM_{os.getpid()}_{i}_") 2458 | .read().strip().split(" ")[-1] 2459 | ) 2460 | remove_cmd = f"nft delete rule ip nat PREROUTING handle {h}" 2461 | else: 2462 | remove_cmd = f"iptables -t nat -D PREROUTING {rule} -j REDIRECT --to {port}" 2463 | removal_cmds.append(remove_cmd) 2464 | fp.write(f"{remove_cmd}\n") 2465 | 2466 | if args.nftables: 2467 | if os.system(f"nft rule ip filter INPUT tcp dport {port} accept comment XMLM_{os.getpid()}_P"): 2468 | raise RootRequiredError 2469 | h = os.popen(f"nft --handle list ruleset | grep XMLM_{os.getpid()}_P").read().strip().split(" ")[-1] 2470 | remove_cmd = f"nft delete rule ip nat PREROUTING handle {h}" 2471 | removal_cmds.append(remove_cmd) 2472 | fp.write(f"{remove_cmd}\n") 2473 | os.chmod(cleanup_filepath, 0o777) 2474 | 2475 | if args.write_sysctl: 2476 | removal_cmds.append("sysctl -w " + os.popen("sysctl net.ipv4.ip_forward") 2477 | .read().strip().replace(" ", "")) 2478 | os.system("sysctl -w net.ipv4.ip_forward=1") 2479 | 2480 | removal_cmds.append("sysctl -w " + os.popen("sysctl net.ipv4.conf.all.route_localnet") 2481 | .read().strip().replace(" ", "")) 2482 | os.system("sysctl -w net.ipv4.conf.all.route_localnet=1") 2483 | else: 2484 | logging.info("Skipping sysctl commands.") 2485 | 2486 | listener.listen(32) 2487 | logging.info(f"Listening on {listener.getsockname()}...") 2488 | logging.info("Press Ctrl+C to quit.") 2489 | 2490 | def on_child_exit(signum, frame): 2491 | if child_pid_to_rfd: 2492 | pid, status = os.waitpid(-1, os.WNOHANG) 2493 | if pid: 2494 | logging.info(f"[{pid:<6}] has exit with status code {status}.") 2495 | fd = child_pid_to_rfd.pop(pid, None) 2496 | if fd is not None: 2497 | try: 2498 | poller.unregister(fd) 2499 | except KeyError: 2500 | pass 2501 | 2502 | signal.signal(signal.SIGCHLD, on_child_exit) 2503 | 2504 | while True: 2505 | for child_pid in child_pid_to_rfd: 2506 | try: 2507 | os.kill(child_pid, 0) 2508 | except OSError: 2509 | rfd = child_pid_to_rfd.pop(child_pid, None) 2510 | if rfd is not None: 2511 | child_rfds.discard(rfd) 2512 | try: 2513 | poller.unregister(rfd) 2514 | except KeyError: 2515 | pass 2516 | os.close(rfd) 2517 | 2518 | for fd, event_type in poller.poll(): 2519 | # print(fd, event_type) 2520 | if fd == listener.fileno(): 2521 | if event_type & select.POLLIN: 2522 | sock, source = listener.accept() 2523 | 2524 | if firehose_listener is None: 2525 | rfd = wfd = -1 2526 | else: 2527 | rfd, wfd = os.pipe() 2528 | 2529 | child_pid = os.fork() 2530 | if child_pid == 0: 2531 | is_child = True 2532 | child_pid_to_rfd.clear() 2533 | listener.close() 2534 | if firehose_listener is not None: 2535 | os.close(rfd) 2536 | firehose_listener.close() 2537 | for c in firehose_clients.values(): 2538 | c.close() 2539 | firehose_clients.clear() 2540 | firehose_backlog.clear() 2541 | 2542 | return Connection(sock, source, definitions, args, wfd).run() 2543 | 2544 | sock.close() 2545 | if wfd != -1: 2546 | os.close(wfd) 2547 | if rfd != -1: 2548 | poller.register(rfd, select.POLLIN) 2549 | child_pid_to_rfd[child_pid] = rfd 2550 | child_rfds.add(rfd) 2551 | 2552 | elif fd in child_rfds: 2553 | if event_type & select.POLLIN: 2554 | try: 2555 | data = [os.read(fd, 4)] 2556 | cb = int.from_bytes(data[0], "little") 2557 | 2558 | offset = 0 2559 | while offset < cb: 2560 | data.append(os.read(fd, cb - offset)) 2561 | offset += len(data[-1]) 2562 | if not len(data[-1]): 2563 | print("Child read fail") 2564 | break 2565 | else: 2566 | data = bytearray().join(data) 2567 | # print(f"Child read: {cb} bytes =>{len(data)}") 2568 | for clientfd, backlog in firehose_backlog.items(): 2569 | poller.modify(clientfd, select.POLLIN | select.POLLOUT | select.POLLRDHUP) 2570 | backlog.append((0, data)) 2571 | except OSError: 2572 | pass 2573 | 2574 | elif event_type & select.POLLHUP: 2575 | try: 2576 | poller.unregister(fd) 2577 | except KeyError: 2578 | pass 2579 | 2580 | elif firehose_listener is not None and fd == firehose_listener.fileno(): 2581 | if event_type & select.POLLIN: 2582 | try: 2583 | sock, source = firehose_listener.accept() 2584 | firehose_clients[sock.fileno()] = sock 2585 | firehose_backlog[sock.fileno()] = collections.deque() 2586 | poller.register(sock.fileno(), select.POLLRDHUP | select.POLLIN) 2587 | sock.setblocking(False) 2588 | except OSError: 2589 | pass 2590 | 2591 | elif fd in firehose_clients: 2592 | abandon_sock = bool(event_type & (select.POLLRDHUP | select.POLLHUP)) 2593 | try: 2594 | sock = firehose_clients[fd] 2595 | 2596 | if event_type & select.POLLIN: 2597 | while True: 2598 | try: 2599 | recv_data = sock.recv(4096) 2600 | except socket.error as e: 2601 | if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): 2602 | break 2603 | raise 2604 | if not recv_data: 2605 | break 2606 | firehose_backlog[fd].append((0, bytearray().join(( 2607 | int.to_bytes(len(recv_data) + 8, 4, "little"), 2608 | b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF', 2609 | recv_data, 2610 | )))) 2611 | poller.modify(fd, select.POLLIN | select.POLLOUT | select.POLLRDHUP) 2612 | 2613 | if event_type & select.POLLOUT: 2614 | sock = firehose_clients[fd] 2615 | while firehose_backlog[fd]: 2616 | offset, data = firehose_backlog[fd][0] 2617 | while offset < len(data): 2618 | try: 2619 | sent = sock.send(data) 2620 | if not sent: 2621 | break 2622 | offset += sent 2623 | except socket.error as e: 2624 | if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): 2625 | break 2626 | raise 2627 | if offset == len(data): 2628 | firehose_backlog[fd].popleft() 2629 | else: 2630 | firehose_backlog[fd][0] = offset, data 2631 | else: 2632 | poller.modify(fd, select.POLLRDHUP | select.POLLIN) 2633 | except socket.error as e: 2634 | print(e) 2635 | abandon_sock = True 2636 | 2637 | if abandon_sock: 2638 | poller.unregister(fd) 2639 | firehose_backlog.pop(fd) 2640 | firehose_clients.pop(fd).close() 2641 | 2642 | except RootRequiredError: 2643 | logging.error("This program requires root permissions.\n") 2644 | err = -1 2645 | 2646 | except KeyboardInterrupt: 2647 | pass 2648 | 2649 | finally: 2650 | for child_pid in child_pid_to_rfd.keys(): 2651 | try: 2652 | os.kill(child_pid, signal.SIGINT) 2653 | except OSError: 2654 | pass 2655 | 2656 | if not is_child: 2657 | logging.info("Cleaning up...") 2658 | for removal_cmd in removal_cmds: 2659 | logging.info(f"Running: {removal_cmd}") 2660 | exit_code = os.system(removal_cmd) 2661 | if exit_code: 2662 | logging.warning(f"\t=> Failed with exit code {exit_code}") 2663 | err = -1 2664 | os.remove(cleanup_filepath) 2665 | if err: 2666 | logging.error("One or more error have occurred during cleanup.") 2667 | err = -1 2668 | else: 2669 | logging.info("Cleanup complete.") 2670 | return err 2671 | 2672 | 2673 | if __name__ == "__main__": 2674 | exit(__main__()) 2675 | --------------------------------------------------------------------------------