├── 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 | * 
15 | 3. Add an `External` Virtual Switch.
16 | * 
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 | * 
19 | 4. Create a new virtual machine.
20 | 1. Open `New Virtual Machine Wizard` with `New > Virtual Machine`.
21 | * 
22 | 2. Pick `Generation 2` in `Specify Generation`.
23 | * 
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 | * 
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 | * 
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 | * 
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 |
--------------------------------------------------------------------------------