((resolve, reject) => {
65 | if (!this.client || !this.rawEncoder) {
66 | return reject(new Error('client is null'));
67 | }
68 |
69 | if (this.rawEncoder.write(data)) {
70 | process.nextTick(resolve);
71 | } else {
72 | this.client.once('drain', () => {
73 | resolve();
74 | });
75 | }
76 | });
77 | }
78 |
79 | private onConnect() {
80 | if (!this.client) {
81 | throw new Error('client is null');
82 | }
83 |
84 | this.encoder = new RpcMessageEncoderStream();
85 | this.rawEncoder = new RpcRawEncoderStream();
86 | this.decoder = new RpcMessageDecoderStream();
87 |
88 | this.client.pipe(this.decoder);
89 | this.encoder.pipe(this.client);
90 | this.rawEncoder.pipe(this.client);
91 |
92 | this.decoder.on('data', this.onData.bind(this));
93 | this.client.on('end', this.onDisconnect.bind(this));
94 | this.client.on('drain', this.onDrain.bind(this));
95 |
96 | console.log('connected to LLRT pipe');
97 |
98 | this.emit('connect');
99 | }
100 |
101 | private onDisconnect() {
102 | console.log('disconnected from LLRT pipe');
103 | this.emit('close');
104 | }
105 |
106 | private onData(packed: PackedRpcMessage | Buffer) {
107 | console.log('received data from LLRT pipe');
108 | console.dir(packed);
109 |
110 | if (!(packed instanceof PackedRpcMessage)) {
111 | throw new Error('received unexpected raw data from LLRT pipe');
112 | }
113 |
114 | const data = packed.data;
115 | switch (packed.type) {
116 | case RpcMessageType.Resize:
117 | this.uiPainter.handleResize(data.width, data.height);
118 | break;
119 | }
120 | }
121 |
122 | private onDrain() {
123 | this.emit('drain');
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/hlrt/src/main/rpc/codec.ts:
--------------------------------------------------------------------------------
1 | import { Packr, Unpackr } from 'msgpackr';
2 | import { Transform, TransformCallback } from 'stream';
3 | import { RpcMessageType } from './messages';
4 |
5 | abstract class LengthDecoderStream extends Transform {
6 | private incompleteChunk: Buffer | null = null;
7 |
8 | constructor() {
9 | super({
10 | objectMode: true,
11 | });
12 | }
13 |
14 | readFullChunk(chunk: Buffer): Buffer | null {
15 | if (this.incompleteChunk) {
16 | chunk = Buffer.concat([this.incompleteChunk, chunk]);
17 | this.incompleteChunk = null;
18 | }
19 |
20 | // read a little-endian 32-bit integer from the start of the chunk
21 | const length = chunk.readUInt32LE(0);
22 | if (chunk.length >= length + 4) {
23 | // if there's anything left in the chunk, it's the start of the next message - save it
24 | if (chunk.length > length + 4) {
25 | this.incompleteChunk = chunk.subarray(length + 4);
26 | }
27 |
28 | // we have a complete chunk, trim the chunk to size and return it
29 | return chunk.subarray(4, length + 4);
30 | }
31 |
32 | return null;
33 | }
34 | }
35 |
36 | abstract class LengthEncoderStream extends Transform {
37 | constructor() {
38 | super({
39 | writableObjectMode: true,
40 | });
41 | }
42 |
43 | writeFullChunk(chunk: Buffer) {
44 | // prepend the length
45 | const length = Buffer.alloc(4);
46 | length.writeUInt32LE(chunk.length, 0);
47 |
48 | // push the encoded message
49 | this.push(Buffer.concat([length, chunk]));
50 | }
51 | }
52 |
53 | export class RpcMessageDecoderStream extends LengthDecoderStream {
54 | private readonly codec: Unpackr;
55 |
56 | constructor() {
57 | super();
58 | this.codec = new Unpackr({ useRecords: false });
59 | }
60 |
61 | _transform(
62 | partialChunk: Buffer,
63 | encoding: string,
64 | callback: TransformCallback,
65 | ) {
66 | const fullChunk = this.readFullChunk(partialChunk);
67 | if (fullChunk) {
68 | // optimization: if the first byte isn't within 0x80-0x8f or 0xde-0xdf, then we know it's not a
69 | // valid msgpack structure for our purposes (since we only use maps), so we can skip the
70 | // deserialization step and treat it as a raw message
71 | // currently, the UI doesn't _receive_ any raw messages, but this is here for completeness
72 | if (
73 | fullChunk[0] < 0x80 ||
74 | (fullChunk[0] > 0x8f && fullChunk[0] < 0xde) ||
75 | fullChunk[0] > 0xdf
76 | ) {
77 | this.push(fullChunk);
78 | } else {
79 | // decode the message
80 | const decoded = this.codec.decode(fullChunk);
81 | console.dir(decoded);
82 |
83 | // extract the message type
84 | const type = Object.keys(decoded.Ui)[0] as RpcMessageType;
85 |
86 | // push the decoded message
87 | this.push(new PackedRpcMessage(type, decoded.Ui[type]));
88 | }
89 | }
90 |
91 | callback();
92 | }
93 | }
94 |
95 | export class RpcMessageEncoderStream extends LengthEncoderStream {
96 | private readonly codec: Packr;
97 |
98 | constructor() {
99 | super();
100 | this.codec = new Packr({ useRecords: false });
101 | }
102 |
103 | _transform(
104 | message: PackedRpcMessage,
105 | encoding: string,
106 | callback: () => void,
107 | ) {
108 | const encoded = this.codec.encode(message.into());
109 | this.writeFullChunk(encoded);
110 |
111 | callback();
112 | }
113 | }
114 |
115 | export class RpcRawEncoderStream extends LengthEncoderStream {
116 | _transform(message: Buffer, encoding: string, callback: () => void) {
117 | this.writeFullChunk(message);
118 |
119 | callback();
120 | }
121 | }
122 |
123 | export class PackedRpcMessage {
124 | constructor(
125 | public readonly type: RpcMessageType,
126 | public readonly data: any, // todo
127 | ) {}
128 |
129 | into() {
130 | return {
131 | Ui: {
132 | [this.type.toString()]: this.data,
133 | },
134 | };
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/hlrt/src/main/rpc/messages.ts:
--------------------------------------------------------------------------------
1 | import { Type } from 'class-transformer';
2 |
3 | export enum RpcMessageType {
4 | Resize = 'Resize',
5 | }
6 |
7 | export class RpcMessageResize {}
8 |
9 | export class PackedRpcMessage {
10 | public readonly type: RpcMessageType;
11 |
12 | @Type(() => Object, {
13 | discriminator: {
14 | property: 'type',
15 | subTypes: [{ value: RpcMessageResize, name: RpcMessageType.Resize }],
16 | },
17 | })
18 | public readonly data: any;
19 |
20 | constructor(type: RpcMessageType, data: any) {
21 | this.type = type;
22 | this.data = data;
23 | }
24 |
25 | into() {
26 | return {
27 | Ui: {
28 | [this.type.toString()]: this.data,
29 | },
30 | };
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/hlrt/src/preload/index.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from 'electron';
2 |
3 | contextBridge.exposeInMainWorld(
4 | 'grebuloffUiMode',
5 | process.env['SHOW_NO_PIPE'] ? 'no-pipe' : 'pipe',
6 | );
7 |
--------------------------------------------------------------------------------
/hlrt/src/renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/hlrt/src/renderer/src/App.tsx:
--------------------------------------------------------------------------------
1 | import './assets/App.css';
2 |
3 | function App(): JSX.Element {
4 | return (
5 |
6 |
Hello from Grebuloff!
7 |
8 | );
9 | }
10 |
11 | export default App;
12 |
--------------------------------------------------------------------------------
/hlrt/src/renderer/src/assets/App.css:
--------------------------------------------------------------------------------
1 | .container {
2 | /* placeholder for testing */
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | margin-top: 20%;
7 | font-size: 1.5em;
8 | }
9 |
10 | .spin {
11 | animation: spin 2s linear infinite;
12 | }
13 |
14 | @keyframes spin {
15 | 50% {
16 | transform: rotate(180deg);
17 | font-size: 3em;
18 | }
19 |
20 | 100% {
21 | transform: rotate(360deg);
22 | font-size: 1.5em;
23 | }
24 | }
--------------------------------------------------------------------------------
/hlrt/src/renderer/src/assets/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | overflow: hidden;
9 | background: transparent;
10 | color: red;
11 | }
12 |
13 | body.no-pipe {
14 | background: black !important;
15 | }
16 |
17 | code {
18 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
19 | monospace;
20 | }
21 |
--------------------------------------------------------------------------------
/hlrt/src/renderer/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/hlrt/src/renderer/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './assets/index.css';
4 | import App from './App';
5 |
6 | declare global {
7 | interface Window {
8 | grebuloffUiMode: 'pipe' | 'no-pipe';
9 | }
10 | }
11 |
12 | if (window.grebuloffUiMode === 'no-pipe') {
13 | document.body.classList.add('no-pipe');
14 | }
15 |
16 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
17 |
18 |
19 | ,
20 | );
21 |
--------------------------------------------------------------------------------
/hlrt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "experimentalDecorators": true,
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "composite": true,
16 | },
17 | "exclude": [
18 | "build.cjs",
19 | ],
20 | "references": [
21 | {
22 | "path": "./tsconfig.node.json"
23 | },
24 | {
25 | "path": "./tsconfig.web.json"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/hlrt/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": [
4 | "electron.vite.config.*",
5 | "src/main/**/*.{ts,tsx}",
6 | "src/preload/**/*.{ts,tsx}",
7 | ],
8 | "compilerOptions": {
9 | "composite": true,
10 | "types": [
11 | "node",
12 | "electron-vite/node"
13 | ],
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/hlrt/tsconfig.web.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": [
4 | "src/renderer/src/env.d.ts",
5 | "src/renderer/src/**/*.{ts,tsx}",
6 | ],
7 | "compilerOptions": {
8 | "lib": [
9 | "ESNext",
10 | "DOM",
11 | "DOM.Iterable"
12 | ],
13 | "composite": true,
14 | "jsx": "react-jsx",
15 | "baseUrl": ".",
16 | "paths": {
17 | "@renderer/*": [
18 | "src/renderer/src/*"
19 | ]
20 | },
21 | }
22 | }
--------------------------------------------------------------------------------
/injector/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "grebuloff-injector"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | windows = { workspace = true }
8 | dll-syringe = { workspace = true, features = ["syringe", "rpc"] }
9 | clap = { version = "4.3.11", features = ["derive"] }
10 | sysinfo = "0.29.0"
11 |
--------------------------------------------------------------------------------
/injector/src/main.rs:
--------------------------------------------------------------------------------
1 | #![allow(unsafe_code)]
2 |
3 | use clap::{Parser, Subcommand};
4 | use dll_syringe::{process::OwnedProcess, Syringe};
5 | use std::os::windows::io::FromRawHandle;
6 | use std::path::PathBuf;
7 | use std::ptr::addr_of_mut;
8 | use sysinfo::{PidExt, ProcessExt, SystemExt};
9 | use windows::Win32::Foundation::{CloseHandle, LUID};
10 | use windows::Win32::Security::Authorization::{
11 | GetSecurityInfo, SetSecurityInfo, GRANT_ACCESS, SE_KERNEL_OBJECT,
12 | };
13 | use windows::Win32::Security::{
14 | AdjustTokenPrivileges, LookupPrivilegeValueW, PrivilegeCheck, ACE_FLAGS, ACL,
15 | DACL_SECURITY_INFORMATION, LUID_AND_ATTRIBUTES, PRIVILEGE_SET, SECURITY_DESCRIPTOR,
16 | SE_DEBUG_NAME, SE_PRIVILEGE_ENABLED, SE_PRIVILEGE_REMOVED, TOKEN_ADJUST_PRIVILEGES,
17 | TOKEN_PRIVILEGES, TOKEN_QUERY,
18 | };
19 | use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken, CREATE_SUSPENDED};
20 | use windows::{
21 | core::{HSTRING, PWSTR},
22 | imp::GetLastError,
23 | Win32::{
24 | Foundation::{BOOL, HANDLE},
25 | Security::{
26 | Authorization::{BuildExplicitAccessWithNameW, SetEntriesInAclW, EXPLICIT_ACCESS_W},
27 | InitializeSecurityDescriptor, SetSecurityDescriptorDacl, PSECURITY_DESCRIPTOR,
28 | SECURITY_ATTRIBUTES,
29 | },
30 | System::Threading::{CreateProcessW, ResumeThread, STARTUPINFOW},
31 | },
32 | };
33 |
34 | const DEFAULT_INJECTION_DELAY: u64 = 3000;
35 |
36 | #[derive(Parser)]
37 | struct Args {
38 | #[clap(subcommand)]
39 | command: Commands,
40 |
41 | #[clap(short = 'I', long)]
42 | injection_delay: Option,
43 | }
44 |
45 | #[derive(Subcommand)]
46 | enum Commands {
47 | Launch {
48 | #[clap(short, long)]
49 | game_path: PathBuf,
50 | },
51 | Inject,
52 | }
53 |
54 | #[derive(Debug)]
55 | struct ProcessInfo {
56 | pid: u32,
57 | process_handle: HANDLE,
58 | thread_handle: HANDLE,
59 | }
60 |
61 | impl Drop for ProcessInfo {
62 | fn drop(&mut self) {
63 | unsafe {
64 | CloseHandle(self.thread_handle);
65 | CloseHandle(self.process_handle);
66 | }
67 | }
68 | }
69 |
70 | // ported from Dalamud launch code
71 | unsafe fn spawn_game_process(game_path: PathBuf) -> ProcessInfo {
72 | let mut explicit_access = std::mem::zeroed::();
73 |
74 | let username = std::env::var("USERNAME").unwrap();
75 | let pcwstr = HSTRING::from(username);
76 |
77 | BuildExplicitAccessWithNameW(
78 | &mut explicit_access,
79 | &pcwstr,
80 | // STANDARD_RIGHTS_ALL | SPECIFIC_RIGHTS_ALL & ~PROCESS_VM_WRITE
81 | 0x001F0000 | 0x0000FFFF & !0x20,
82 | GRANT_ACCESS,
83 | ACE_FLAGS(0),
84 | );
85 |
86 | let mut newacl = std::ptr::null_mut();
87 |
88 | let result = SetEntriesInAclW(Some(&[explicit_access]), None, addr_of_mut!(newacl));
89 | if result.is_err() {
90 | panic!("SetEntriesInAclA failed with error code {}", result.0);
91 | }
92 |
93 | let mut sec_desc = std::mem::zeroed::();
94 | let psec_desc = PSECURITY_DESCRIPTOR(&mut sec_desc as *mut _ as *mut _);
95 | if !InitializeSecurityDescriptor(psec_desc, 1).as_bool() {
96 | panic!("InitializeSecurityDescriptor failed");
97 | }
98 |
99 | if !SetSecurityDescriptorDacl(psec_desc, true, Some(newacl), false).as_bool() {
100 | panic!("SetSecurityDescriptorDacl failed");
101 | }
102 |
103 | let mut process_information =
104 | std::mem::zeroed::();
105 | let process_attributes = SECURITY_ATTRIBUTES {
106 | nLength: std::mem::size_of::() as u32,
107 | lpSecurityDescriptor: psec_desc.0,
108 | bInheritHandle: BOOL(0),
109 | };
110 | let mut startup_info = std::mem::zeroed::();
111 | startup_info.cb = std::mem::size_of::() as u32;
112 |
113 | let cmd_line = format!(
114 | "\"{}\" DEV.TestSID=0 language=1 DEV.MaxEntitledExpansionID=4 DEV.GameQuitMessageBox=0\0",
115 | game_path.to_str().unwrap()
116 | );
117 |
118 | let game_dir = game_path.parent().unwrap();
119 |
120 | let res = CreateProcessW(
121 | None,
122 | PWSTR(cmd_line.encode_utf16().collect::>().as_mut_ptr()),
123 | Some(&process_attributes),
124 | None,
125 | BOOL(0),
126 | CREATE_SUSPENDED,
127 | None,
128 | &HSTRING::from(game_dir.to_str().unwrap()),
129 | &startup_info,
130 | &mut process_information,
131 | );
132 | let last_error = GetLastError();
133 | if res == BOOL(0) {
134 | panic!("CreateProcessW failed with error code {}", last_error);
135 | }
136 |
137 | // strip SeDebugPrivilege/ACL from the process
138 | let mut token_handle = std::mem::zeroed::();
139 |
140 | if !OpenProcessToken(
141 | process_information.hProcess,
142 | TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES,
143 | &mut token_handle,
144 | )
145 | .as_bool()
146 | {
147 | panic!("OpenProcessToken failed");
148 | }
149 |
150 | let mut luid_debug_privilege = std::mem::zeroed::();
151 | if !LookupPrivilegeValueW(None, SE_DEBUG_NAME, &mut luid_debug_privilege).as_bool() {
152 | panic!("LookupPrivilegeValueW failed");
153 | }
154 |
155 | let mut required_privileges = PRIVILEGE_SET {
156 | PrivilegeCount: 1,
157 | Control: 1,
158 | Privilege: [LUID_AND_ATTRIBUTES {
159 | Luid: luid_debug_privilege,
160 | Attributes: SE_PRIVILEGE_ENABLED,
161 | }],
162 | };
163 |
164 | let mut b_result: i32 = 0;
165 | if !PrivilegeCheck(token_handle, &mut required_privileges, &mut b_result).as_bool() {
166 | panic!("PrivilegeCheck failed");
167 | }
168 |
169 | // remove SeDebugPrivilege
170 | if b_result != 0 {
171 | println!("removing SeDebugPrivilege");
172 | let mut token_privileges = TOKEN_PRIVILEGES {
173 | PrivilegeCount: 1,
174 | Privileges: [LUID_AND_ATTRIBUTES {
175 | Luid: luid_debug_privilege,
176 | Attributes: SE_PRIVILEGE_REMOVED,
177 | }],
178 | };
179 |
180 | if !AdjustTokenPrivileges(
181 | token_handle,
182 | false,
183 | Some(&mut token_privileges),
184 | 0,
185 | None,
186 | None,
187 | )
188 | .as_bool()
189 | {
190 | panic!("AdjustTokenPrivileges failed");
191 | }
192 | }
193 |
194 | CloseHandle(token_handle);
195 |
196 | ProcessInfo {
197 | pid: process_information.dwProcessId,
198 | process_handle: process_information.hProcess,
199 | thread_handle: process_information.hThread,
200 | }
201 | }
202 |
203 | unsafe fn copy_acl_from_self_to_target(target_process: HANDLE) {
204 | println!("copying current acl to target process...");
205 |
206 | let mut acl = std::ptr::null_mut() as *mut ACL;
207 |
208 | if !GetSecurityInfo(
209 | GetCurrentProcess(),
210 | SE_KERNEL_OBJECT,
211 | DACL_SECURITY_INFORMATION.0,
212 | None,
213 | None,
214 | Some(addr_of_mut!(acl)),
215 | None,
216 | None,
217 | )
218 | .is_ok()
219 | {
220 | panic!("GetSecurityInfo failed");
221 | }
222 |
223 | if !SetSecurityInfo(
224 | target_process,
225 | SE_KERNEL_OBJECT,
226 | DACL_SECURITY_INFORMATION.0,
227 | None,
228 | None,
229 | Some(acl),
230 | None,
231 | )
232 | .is_ok()
233 | {
234 | panic!("SetSecurityInfo failed");
235 | }
236 | }
237 |
238 | fn await_game_process() -> u32 {
239 | let pid;
240 |
241 | 'wait: loop {
242 | std::thread::sleep(std::time::Duration::from_millis(100));
243 |
244 | let system = sysinfo::System::new_all();
245 | let processes = system.processes();
246 |
247 | for (_pid, process) in processes {
248 | if process.name() == "ffxiv_dx11.exe" {
249 | pid = _pid.as_u32();
250 | break 'wait;
251 | }
252 | }
253 | }
254 |
255 | pid
256 | }
257 |
258 | fn main() {
259 | let args = Args::parse();
260 |
261 | let process_info;
262 |
263 | match args.command {
264 | Commands::Launch { game_path } => {
265 | process_info = unsafe { spawn_game_process(game_path) };
266 | }
267 | Commands::Inject => {
268 | process_info = ProcessInfo {
269 | pid: await_game_process(),
270 | process_handle: HANDLE(0),
271 | thread_handle: HANDLE(0),
272 | };
273 | }
274 | }
275 |
276 | println!(
277 | "pid: {} - tid: {}",
278 | process_info.pid, process_info.thread_handle.0
279 | );
280 |
281 | let target;
282 | if process_info.process_handle.0 != 0 {
283 | target = unsafe {
284 | OwnedProcess::from_raw_handle(std::mem::transmute(process_info.process_handle))
285 | };
286 | } else {
287 | target = OwnedProcess::from_pid(process_info.pid).unwrap();
288 | }
289 |
290 | let syringe = Syringe::for_process(target);
291 |
292 | let current_exe = std::env::current_exe().unwrap();
293 | let mut grebuloff_path = current_exe.parent().unwrap().to_path_buf();
294 |
295 | #[cfg(debug_assertions)]
296 | {
297 | // HACK: if this is a debug build, cargo is probably executing it
298 | // from the target directory. we'd rather execute from the dist
299 | // directory, so we'll try to find the dist directory and use that
300 | // instead.
301 | if grebuloff_path.file_name().unwrap() == "debug" {
302 | grebuloff_path.pop();
303 | grebuloff_path.pop();
304 | grebuloff_path.pop();
305 | grebuloff_path.push("dist");
306 |
307 | println!("[debug build] using grebuloff_path: {:?}", grebuloff_path);
308 | }
309 | }
310 |
311 | let llrt_path = &grebuloff_path.join("grebuloff.dll");
312 |
313 | unsafe {
314 | if process_info.thread_handle.0 != 0 {
315 | ResumeThread(process_info.thread_handle);
316 |
317 | if process_info.process_handle.0 != 0 {
318 | // the idea here is to change the process acl once the window is created,
319 | // because at that point, the game has already checked its acls.
320 | // we should actually query to see if the window is open here,
321 | // but this should suffice for now
322 | std::thread::sleep(std::time::Duration::from_millis(1000));
323 | copy_acl_from_self_to_target(process_info.process_handle);
324 | }
325 | }
326 | }
327 |
328 | let injection_delay = std::time::Duration::from_millis(
329 | args.injection_delay
330 | .unwrap_or(DEFAULT_INJECTION_DELAY)
331 | .clamp(0, 60000),
332 | );
333 |
334 | if !injection_delay.is_zero() {
335 | println!(
336 | "waiting {}ms before injecting...",
337 | injection_delay.as_millis()
338 | );
339 | std::thread::sleep(injection_delay);
340 | }
341 |
342 | println!("injecting...");
343 | let injected_payload = syringe.inject(llrt_path).unwrap();
344 |
345 | println!("calling entrypoint...");
346 | let remote_load =
347 | unsafe { syringe.get_payload_procedure::)>(injected_payload, "init_injected") }
348 | .unwrap()
349 | .unwrap();
350 | let str_as_vec = grebuloff_path.to_str().unwrap().as_bytes().to_vec();
351 | remote_load.call(&str_as_vec).unwrap();
352 |
353 | println!("done!");
354 | }
355 |
--------------------------------------------------------------------------------
/loader/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "grebuloff-loader"
3 | version = "0.1.0"
4 | edition = "2021"
5 | build = "build.rs"
6 |
7 | [lib]
8 | crate-type = ["cdylib"]
9 | name = "winhttp"
10 |
11 | [dependencies]
12 | plthook = "0.2.2"
13 | windows = { workspace = true }
14 |
--------------------------------------------------------------------------------
/loader/build.rs:
--------------------------------------------------------------------------------
1 | // this is pretty nasty, but it works
2 | fn main() {
3 | println!("cargo:rustc-link-lib=winhttp");
4 | println!("cargo:rustc-cdylib-link-arg=/export:SvchostPushServiceGlobals=C:\\Windows\\system32\\winhttp.SvchostPushServiceGlobals,@5");
5 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpAddRequestHeaders=C:\\Windows\\system32\\winhttp.WinHttpAddRequestHeaders,@6");
6 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpAddRequestHeadersEx=C:\\Windows\\system32\\winhttp.WinHttpAddRequestHeadersEx,@7");
7 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpAutoProxySvcMain=C:\\Windows\\system32\\winhttp.WinHttpAutoProxySvcMain,@8");
8 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCheckPlatform=C:\\Windows\\system32\\winhttp.WinHttpCheckPlatform,@9");
9 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCloseHandle=C:\\Windows\\system32\\winhttp.WinHttpCloseHandle,@10");
10 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnect=C:\\Windows\\system32\\winhttp.WinHttpConnect,@11");
11 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionDeletePolicyEntries=C:\\Windows\\system32\\winhttp.WinHttpConnectionDeletePolicyEntries,@12");
12 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionDeleteProxyInfo=C:\\Windows\\system32\\winhttp.WinHttpConnectionDeleteProxyInfo,@13");
13 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionFreeNameList=C:\\Windows\\system32\\winhttp.WinHttpConnectionFreeNameList,@14");
14 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionFreeProxyInfo=C:\\Windows\\system32\\winhttp.WinHttpConnectionFreeProxyInfo,@15");
15 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionFreeProxyList=C:\\Windows\\system32\\winhttp.WinHttpConnectionFreeProxyList,@16");
16 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionGetNameList=C:\\Windows\\system32\\winhttp.WinHttpConnectionGetNameList,@17");
17 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionGetProxyInfo=C:\\Windows\\system32\\winhttp.WinHttpConnectionGetProxyInfo,@18");
18 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionGetProxyList=C:\\Windows\\system32\\winhttp.WinHttpConnectionGetProxyList,@19");
19 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionSetPolicyEntries=C:\\Windows\\system32\\winhttp.WinHttpConnectionSetPolicyEntries,@20");
20 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionSetProxyInfo=C:\\Windows\\system32\\winhttp.WinHttpConnectionSetProxyInfo,@21");
21 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpConnectionUpdateIfIndexTable=C:\\Windows\\system32\\winhttp.WinHttpConnectionUpdateIfIndexTable,@22");
22 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCrackUrl=C:\\Windows\\system32\\winhttp.WinHttpCrackUrl,@23");
23 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCreateProxyResolver=C:\\Windows\\system32\\winhttp.WinHttpCreateProxyResolver,@24");
24 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpCreateUrl=C:\\Windows\\system32\\winhttp.WinHttpCreateUrl,@25");
25 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpDetectAutoProxyConfigUrl=C:\\Windows\\system32\\winhttp.WinHttpDetectAutoProxyConfigUrl,@26");
26 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpFreeProxyResult=C:\\Windows\\system32\\winhttp.WinHttpFreeProxyResult,@27");
27 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpFreeProxyResultEx=C:\\Windows\\system32\\winhttp.WinHttpFreeProxyResultEx,@28");
28 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpFreeProxySettings=C:\\Windows\\system32\\winhttp.WinHttpFreeProxySettings,@29");
29 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetDefaultProxyConfiguration=C:\\Windows\\system32\\winhttp.WinHttpGetDefaultProxyConfiguration,@30");
30 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetIEProxyConfigForCurrentUser=C:\\Windows\\system32\\winhttp.WinHttpGetIEProxyConfigForCurrentUser,@31");
31 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyForUrl=C:\\Windows\\system32\\winhttp.WinHttpGetProxyForUrl,@32");
32 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyForUrlEx=C:\\Windows\\system32\\winhttp.WinHttpGetProxyForUrlEx,@33");
33 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyForUrlEx2=C:\\Windows\\system32\\winhttp.WinHttpGetProxyForUrlEx2,@34");
34 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyForUrlHvsi=C:\\Windows\\system32\\winhttp.WinHttpGetProxyForUrlHvsi,@35");
35 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyResult=C:\\Windows\\system32\\winhttp.WinHttpGetProxyResult,@36");
36 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxyResultEx=C:\\Windows\\system32\\winhttp.WinHttpGetProxyResultEx,@37");
37 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetProxySettingsVersion=C:\\Windows\\system32\\winhttp.WinHttpGetProxySettingsVersion,@38");
38 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpGetTunnelSocket=C:\\Windows\\system32\\winhttp.WinHttpGetTunnelSocket,@39");
39 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpOpen=C:\\Windows\\system32\\winhttp.WinHttpOpen,@40");
40 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpOpenRequest=C:\\Windows\\system32\\winhttp.WinHttpOpenRequest,@41");
41 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpProbeConnectivity=C:\\Windows\\system32\\winhttp.WinHttpProbeConnectivity,@43");
42 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryAuthSchemes=C:\\Windows\\system32\\winhttp.WinHttpQueryAuthSchemes,@44");
43 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryDataAvailable=C:\\Windows\\system32\\winhttp.WinHttpQueryDataAvailable,@45");
44 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryHeaders=C:\\Windows\\system32\\winhttp.WinHttpQueryHeaders,@46");
45 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryHeadersEx=C:\\Windows\\system32\\winhttp.WinHttpQueryHeadersEx,@47");
46 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpQueryOption=C:\\Windows\\system32\\winhttp.WinHttpQueryOption,@48");
47 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReadData=C:\\Windows\\system32\\winhttp.WinHttpReadData,@49");
48 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReadDataEx=C:\\Windows\\system32\\winhttp.WinHttpReadDataEx,@50");
49 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReadProxySettings=C:\\Windows\\system32\\winhttp.WinHttpReadProxySettings,@51");
50 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReadProxySettingsHvsi=C:\\Windows\\system32\\winhttp.WinHttpReadProxySettingsHvsi,@52");
51 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpReceiveResponse=C:\\Windows\\system32\\winhttp.WinHttpReceiveResponse,@53");
52 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpResetAutoProxy=C:\\Windows\\system32\\winhttp.WinHttpResetAutoProxy,@54");
53 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSaveProxyCredentials=C:\\Windows\\system32\\winhttp.WinHttpSaveProxyCredentials,@55");
54 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSendRequest=C:\\Windows\\system32\\winhttp.WinHttpSendRequest,@56");
55 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetCredentials=C:\\Windows\\system32\\winhttp.WinHttpSetCredentials,@57");
56 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetDefaultProxyConfiguration=C:\\Windows\\system32\\winhttp.WinHttpSetDefaultProxyConfiguration,@58");
57 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetOption=C:\\Windows\\system32\\winhttp.WinHttpSetOption,@59");
58 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetProxySettingsPerUser=C:\\Windows\\system32\\winhttp.WinHttpSetProxySettingsPerUser,@60");
59 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetStatusCallback=C:\\Windows\\system32\\winhttp.WinHttpSetStatusCallback,@61");
60 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpSetTimeouts=C:\\Windows\\system32\\winhttp.WinHttpSetTimeouts,@62");
61 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpTimeFromSystemTime=C:\\Windows\\system32\\winhttp.WinHttpTimeFromSystemTime,@63");
62 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpTimeToSystemTime=C:\\Windows\\system32\\winhttp.WinHttpTimeToSystemTime,@64");
63 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketClose=C:\\Windows\\system32\\winhttp.WinHttpWebSocketClose,@65");
64 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketCompleteUpgrade=C:\\Windows\\system32\\winhttp.WinHttpWebSocketCompleteUpgrade,@66");
65 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketQueryCloseStatus=C:\\Windows\\system32\\winhttp.WinHttpWebSocketQueryCloseStatus,@67");
66 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketReceive=C:\\Windows\\system32\\winhttp.WinHttpWebSocketReceive,@68");
67 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketSend=C:\\Windows\\system32\\winhttp.WinHttpWebSocketSend,@69");
68 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWebSocketShutdown=C:\\Windows\\system32\\winhttp.WinHttpWebSocketShutdown,@70");
69 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWriteData=C:\\Windows\\system32\\winhttp.WinHttpWriteData,@71");
70 | println!("cargo:rustc-cdylib-link-arg=/export:WinHttpWriteProxySettings=C:\\Windows\\system32\\winhttp.WinHttpWriteProxySettings,@72");
71 | }
72 |
--------------------------------------------------------------------------------
/loader/src/lib.rs:
--------------------------------------------------------------------------------
1 | use plthook::{ObjectFile, Replacement};
2 | use std::{
3 | ffi::{c_void, CString},
4 | mem::ManuallyDrop,
5 | os::windows::prelude::OsStringExt,
6 | };
7 | use windows::{
8 | core::{ComInterface, PCSTR},
9 | Win32::{
10 | Foundation::HANDLE,
11 | Graphics::Dxgi::IDXGIFactory2,
12 | System::LibraryLoader::{GetModuleFileNameW, LoadLibraryA},
13 | },
14 | };
15 | use windows::{
16 | core::{HRESULT, HSTRING},
17 | Win32::{
18 | Foundation::HWND,
19 | System::{
20 | LibraryLoader::{GetProcAddress, LoadLibraryExA, LOAD_WITH_ALTERED_SEARCH_PATH},
21 | SystemServices::DLL_PROCESS_ATTACH,
22 | },
23 | UI::WindowsAndMessaging::{MessageBoxW, MB_ICONERROR, MB_OK},
24 | },
25 | };
26 |
27 | const ROOT_ENV_VAR: &'static str = "GREBULOFF_ROOT";
28 | const ROOT_FILE: &'static str = "grebuloff_root.txt";
29 |
30 | const ERROR_NO_ROOT: &'static str = r#"Could not find the Grebuloff root directory!
31 |
32 | We checked the following locations:
33 | 1. "GREBULOFF_ROOT" environment variable passed to the game executable
34 | 2. "grebuloff_root.txt" in the same directory as the game executable
35 | 3. The default installation directory: %AppData%\Grebuloff
36 |
37 | None of the paths searched contained a valid Grebuloff installation, so loading cannot continue.
38 | If you are trying to uninstall Grebuloff, delete "winhttp.dll" from the game directory.
39 |
40 | The game will now exit."#;
41 |
42 | static mut IAT_HOOK: Option> = None;
43 |
44 | #[no_mangle]
45 | #[allow(non_snake_case)]
46 | unsafe extern "system" fn DllMain(
47 | _hinstDLL: HANDLE,
48 | fdwReason: u32,
49 | _lpvReserved: *const std::ffi::c_void,
50 | ) -> bool {
51 | if fdwReason == DLL_PROCESS_ATTACH {
52 | let wakeup_cnt = std::env::var("FFIXV_WAKEUP_CNT");
53 | if !wakeup_cnt.is_ok() {
54 | // FFXIV sets this env var for the fork where it executes for real.
55 | // If the env var isn't set, the process we just loaded into is about to
56 | // get restarted, so we should just exit.
57 | return true;
58 | }
59 |
60 | // we redirect CreateDXGIFactory here and return, so that we can load Grebuloff
61 | // it's expressly forbidden to load libraries in DllMain, and has a tendency to deadlock
62 | redirect_dxgi();
63 | }
64 |
65 | true
66 | }
67 |
68 | unsafe fn load_grebuloff() {
69 | let root = get_grebuloff_root();
70 | let load_result = LoadLibraryExA(root.dll_path, None, LOAD_WITH_ALTERED_SEARCH_PATH);
71 |
72 | match load_result {
73 | Ok(dll) => {
74 | // get the address of init_loader
75 | let init_loader: Option ()> =
76 | GetProcAddress(dll, PCSTR::from_raw(b"init_loader\0".as_ptr()))
77 | .map(|func| std::mem::transmute(func));
78 |
79 | match init_loader {
80 | Some(init_loader) => {
81 | // call init_loader
82 | let runtime_dir = CString::new(root.runtime_path).unwrap();
83 | init_loader(&runtime_dir);
84 | }
85 | None => {
86 | display_error(&format!(
87 | r#"Failed to find init_loader in Grebuloff at {}!
88 |
89 | The game will now exit."#,
90 | root.dll_path.to_string().unwrap(),
91 | ));
92 | std::process::exit(3);
93 | }
94 | }
95 | }
96 | Err(e) => {
97 | display_error(&format!(
98 | r#"Failed to load Grebuloff at {}!
99 |
100 | The error was: {:?}
101 |
102 | The game will now exit."#,
103 | root.dll_path.to_string().unwrap(),
104 | e
105 | ));
106 | std::process::exit(2);
107 | }
108 | }
109 | }
110 |
111 | unsafe extern "system" fn create_dxgi_factory_wrapper(
112 | _riid: *const (),
113 | pp_factory: *mut *mut c_void,
114 | ) -> HRESULT {
115 | // remove the IAT hook now that we've been called
116 | if let Some(hook) = IAT_HOOK.take() {
117 | ManuallyDrop::into_inner(hook);
118 | } else {
119 | display_error("...huh? IAT_HOOK was None...");
120 | std::process::exit(5);
121 | }
122 |
123 | // load Grebuloff
124 | load_grebuloff();
125 |
126 | // call CreateDXGIFactory1 from dxgi.dll
127 | // we use CreateDXGIFactory1 instead of CreateDXGIFactory, passing in IDXGIFactory2 as the riid,
128 | // to create a DXGI 1.2 factory, as opposed to the DXGI 1.0 factory that the game creates
129 | // this shouldn't break anything, but it does allow us to use surface sharing from Chromium
130 | // (once we implement that), for high-performance UI rendering
131 | let dxgi_dll = LoadLibraryA(PCSTR::from_raw(b"dxgi.dll\0".as_ptr())).unwrap();
132 | let original_func: Option HRESULT> =
133 | GetProcAddress(dxgi_dll, PCSTR::from_raw(b"CreateDXGIFactory1\0".as_ptr()))
134 | .map(|func| std::mem::transmute(func));
135 |
136 | // CreateDXGIFactory1()
137 | match original_func {
138 | Some(original_func) => original_func(&IDXGIFactory2::IID, pp_factory),
139 | None => {
140 | display_error("...huh? failed to find CreateDXGIFactory1 in dxgi.dll...");
141 | std::process::exit(4);
142 | }
143 | }
144 | }
145 |
146 | fn display_error(msg: &str) {
147 | let msg = HSTRING::from(msg);
148 | let title = HSTRING::from("Grebuloff Loader");
149 | unsafe {
150 | MessageBoxW(HWND::default(), &msg, &title, MB_OK | MB_ICONERROR);
151 | }
152 | }
153 |
154 | struct GrebuloffRoot {
155 | runtime_path: String,
156 | dll_path: PCSTR,
157 | }
158 |
159 | impl TryFrom for GrebuloffRoot {
160 | type Error = ();
161 |
162 | fn try_from(runtime_path: String) -> Result {
163 | let mut dll_path = std::path::PathBuf::from(&runtime_path);
164 | dll_path.push("grebuloff.dll");
165 |
166 | if !dll_path.exists() {
167 | return Err(());
168 | }
169 |
170 | let dll_path = dll_path.to_str().unwrap().to_owned();
171 |
172 | Ok(GrebuloffRoot {
173 | runtime_path,
174 | dll_path: PCSTR::from_raw(dll_path.as_ptr()),
175 | })
176 | }
177 | }
178 |
179 | fn get_grebuloff_root() -> GrebuloffRoot {
180 | // try in this order:
181 | // 1. `GREBULOFF_ROOT` env var, if set
182 | // 2. `grebuloff_root.txt` in the same directory as the game's EXE
183 | // 3. default to %AppData%\Grebuloff
184 | // if none of these exist, we can't continue - display an error message and exit
185 | std::env::var(ROOT_ENV_VAR)
186 | .or_else(|_| {
187 | // usually we'll be in the game directory, but we might not be
188 | // ensure we search for grebuloff_root.txt in the game directory
189 | let game_dir = unsafe {
190 | let mut exe_path = [0u16; 1024];
191 | let exe_path_len = GetModuleFileNameW(None, &mut exe_path);
192 |
193 | std::path::PathBuf::from(std::ffi::OsString::from_wide(
194 | &exe_path[..exe_path_len as usize],
195 | ))
196 | };
197 |
198 | std::fs::read_to_string(
199 | game_dir
200 | .parent()
201 | .map(|p| p.join(ROOT_FILE))
202 | .unwrap_or(ROOT_FILE.into()),
203 | )
204 | .map(|s| s.trim().to_owned())
205 | })
206 | .or_else(|_| {
207 | if let Ok(appdata) = std::env::var("APPDATA") {
208 | let mut path = std::path::PathBuf::from(appdata);
209 | path.push("Grebuloff");
210 | if path.exists() {
211 | return Ok(path.to_str().map(|s| s.to_owned()).unwrap());
212 | }
213 | }
214 |
215 | Err(())
216 | })
217 | .map(GrebuloffRoot::try_from)
218 | .unwrap_or_else(|_| {
219 | display_error(ERROR_NO_ROOT);
220 | std::process::exit(1);
221 | })
222 | .unwrap()
223 | }
224 |
225 | unsafe fn redirect_dxgi() {
226 | let source = CString::new("CreateDXGIFactory").unwrap();
227 | let obj = ObjectFile::open_main_program().unwrap();
228 |
229 | for symbol in obj.symbols() {
230 | if symbol.name == source {
231 | // replace the address of CreateDXGIFactory with our own init function
232 | let _ = IAT_HOOK.insert(ManuallyDrop::new(
233 | obj.replace(
234 | source.to_str().unwrap(),
235 | create_dxgi_factory_wrapper as *const _,
236 | )
237 | .unwrap(),
238 | ));
239 |
240 | break;
241 | }
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/macros/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "grebuloff-macros"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | proc-macro = true
8 |
9 | [dependencies]
10 | proc-macro2 = "1.0.60"
11 | quote = "1.0.28"
12 | syn = { version = "2", features = ["full"] }
13 | walkdir = "2.3.3"
14 |
--------------------------------------------------------------------------------
/macros/src/lib.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::Ident;
2 | use quote::{format_ident, quote};
3 | use syn::{Data, ItemFn, TraitItemFn, Type};
4 |
5 | fn addr_table_name(name: String) -> Ident {
6 | format_ident!("{}AddressTable", name)
7 | }
8 |
9 | #[proc_macro_derive(VTable, attributes(vtable_base))]
10 | pub fn derive_vtable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
11 | // find the field marked with vtable_base
12 | let input = syn::parse_macro_input!(input as syn::DeriveInput);
13 |
14 | let vtable_base = match input.data {
15 | Data::Struct(ref s) => {
16 | let vtable_base = s
17 | .fields
18 | .iter()
19 | .find(|f| f.attrs.iter().any(|a| a.path().is_ident("vtable_base")))
20 | .expect("no field marked with #[vtable_base]");
21 |
22 | match &vtable_base.ty {
23 | Type::Ptr(_) => vtable_base.ident.clone().unwrap(),
24 | _ => panic!("vtable_base field must be a pointer"),
25 | }
26 | }
27 | _ => panic!("#[derive(VTable)] can only be used on structs"),
28 | };
29 |
30 | let struct_name = input.ident;
31 | let addr_table_name = addr_table_name(struct_name.to_string());
32 |
33 | quote! {
34 | impl #struct_name {
35 | fn address_table(&self) -> #addr_table_name {
36 | #addr_table_name {
37 | base: self.#vtable_base as *const (),
38 | }
39 | }
40 |
41 | fn vtable_base(&self) -> *const () {
42 | self.#vtable_base as *const ()
43 | }
44 | }
45 |
46 | struct #addr_table_name {
47 | base: *const (),
48 | }
49 | }
50 | .into()
51 | }
52 |
53 | #[proc_macro]
54 | pub fn vtable_functions(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
55 | let input = syn::parse_macro_input!(input as syn::ItemImpl);
56 |
57 | let struct_name = input.self_ty;
58 | let addr_table_name = addr_table_name(match struct_name.as_ref() {
59 | Type::Path(path) => path.path.segments.last().unwrap().ident.to_string(),
60 | _ => panic!("#[vtable_functions] can only be used on structs"),
61 | });
62 |
63 | let mut output_addrs = quote! {};
64 | let mut output_impls = quote! {};
65 |
66 | for item in input.items {
67 | match item {
68 | syn::ImplItem::Verbatim(verbatim) => {
69 | let impl_fn = syn::parse2::(verbatim)
70 | .expect("vtable_functions only supports trait-like functions without bodies");
71 |
72 | let fn_name = impl_fn.sig.ident;
73 | let vtable_index = impl_fn
74 | .attrs
75 | .iter()
76 | .find(|a| a.path().is_ident("vtable_fn"))
77 | .expect("no #[vtable_fn] attribute")
78 | .parse_args::()
79 | .expect("invalid #[vtable_fn] attribute")
80 | .base10_parse::()
81 | .expect("invalid #[vtable_fn] attribute");
82 |
83 | // ensure the function is marked as unsafe
84 | if impl_fn.sig.unsafety.is_none() {
85 | panic!("#[vtable_fn] functions must be marked as unsafe");
86 | }
87 |
88 | // preserve doc comments
89 | let doc = impl_fn
90 | .attrs
91 | .iter()
92 | .filter(|a| a.path().is_ident("doc"))
93 | .cloned()
94 | .collect::>();
95 |
96 | // return c_void instead of the unit type, since that means we just don't know/don't care about the return type
97 | let return_type = match impl_fn.sig.output {
98 | syn::ReturnType::Default => quote! { *mut std::ffi::c_void },
99 | syn::ReturnType::Type(_, ty) => quote! { #ty },
100 | };
101 |
102 | let mut args_input = vec![];
103 | let mut args_typed = vec![];
104 | let mut args_named = vec![];
105 |
106 | for arg in impl_fn.sig.inputs.iter() {
107 | args_input.push(quote! { #arg });
108 |
109 | match arg {
110 | syn::FnArg::Receiver(_) => {
111 | panic!("vtable_fn functions cannot take self as an arg (you probably want to use a *const / *mut pointer)");
112 | }
113 | syn::FnArg::Typed(pat) => {
114 | let ty = &pat.ty;
115 | args_typed.push(quote! { #ty });
116 |
117 | match &*pat.pat {
118 | syn::Pat::Ident(ident) => {
119 | args_named.push(quote! { #ident });
120 | }
121 | _ => panic!("vtable_fn arguments must be named"),
122 | }
123 | }
124 | }
125 | }
126 |
127 | output_addrs = quote! {
128 | #output_addrs
129 |
130 | fn #fn_name (&self) -> *const *const () {
131 | unsafe { (self.base as *const usize).add(#vtable_index) as *const *const () }
132 | }
133 | };
134 |
135 | output_impls = quote! {
136 | #output_impls
137 |
138 | #(#doc)*
139 | #[doc = ""]
140 | #[doc = " # Safety"]
141 | #[doc = ""]
142 | #[doc = " This function is unsafe because it calls a C++ virtual function by address."]
143 | unsafe fn #fn_name (&self, #(#args_input),*) -> #return_type {
144 | let address = self.address_table().#fn_name();
145 | let func: extern "C" fn(#(#args_typed),*) -> #return_type = std::mem::transmute(address);
146 | func(#(#args_named),*)
147 | }
148 | };
149 | }
150 | _ => panic!("vtable_functions only supports trait-like functions without bodies"),
151 | }
152 | }
153 |
154 | quote! {
155 | impl #addr_table_name {
156 | #output_addrs
157 | }
158 |
159 | impl #struct_name {
160 | #output_impls
161 | }
162 | }
163 | .into()
164 | }
165 |
166 | #[proc_macro_attribute]
167 | pub fn function_hook(
168 | _attr: proc_macro::TokenStream,
169 | input: proc_macro::TokenStream,
170 | ) -> proc_macro::TokenStream {
171 | let impl_fn = syn::parse_macro_input!(input as ItemFn);
172 |
173 | let fn_name = impl_fn.sig.ident;
174 | let hook_name = format_ident!("__hook__{}", fn_name);
175 | let detour_name = format_ident!("__detour__{}", fn_name);
176 |
177 | // ensure the function is marked as unsafe
178 | if impl_fn.sig.unsafety.is_none() {
179 | panic!("function hooks must be marked as unsafe");
180 | }
181 |
182 | // preserve doc comments
183 | let doc = impl_fn
184 | .attrs
185 | .iter()
186 | .filter(|a| a.path().is_ident("doc"))
187 | .cloned()
188 | .collect::>();
189 |
190 | let return_type = match impl_fn.sig.output {
191 | syn::ReturnType::Default => quote! { () },
192 | syn::ReturnType::Type(_, ty) => quote! { #ty },
193 | };
194 |
195 | let mut args_input = vec![];
196 | let mut args_named = vec![];
197 | let mut fn_type_args = vec![];
198 |
199 | for arg in impl_fn.sig.inputs.iter() {
200 | args_input.push(quote! { #arg });
201 |
202 | match arg {
203 | syn::FnArg::Typed(pat) => {
204 | let ty = &pat.ty;
205 | fn_type_args.push(quote! { #ty });
206 |
207 | match &*pat.pat {
208 | syn::Pat::Ident(ident) => {
209 | args_named.push(quote! { #ident });
210 | }
211 | _ => panic!("function_hook arguments must be named"),
212 | }
213 | }
214 | _ => {}
215 | }
216 | }
217 |
218 | let fn_type = quote! {
219 | fn(#(#fn_type_args),*) -> #return_type
220 | };
221 | let fn_body = impl_fn.block.stmts;
222 |
223 | // preserve calling convention, if specified on the function, otherwise default to C
224 | let abi = impl_fn
225 | .sig
226 | .abi
227 | .as_ref()
228 | .map(|abi| quote! { #abi })
229 | .unwrap_or_else(|| quote! { extern "C" });
230 |
231 | quote! {
232 | #[doc = "Auto-generated function hook."]
233 | #(#doc)*
234 | #[allow(non_upper_case_globals)]
235 | static_detour! {
236 | static #hook_name: unsafe #abi #fn_type;
237 | }
238 |
239 | #[doc = "Auto-generated function hook."]
240 | #[doc = ""]
241 | #(#doc)*
242 | #[doc = "# Safety"]
243 | #[doc = "This function is unsafe and should be treated as such, despite its lack of an `unsafe` keyword."]
244 | #[doc = "This function should not be called outside of hooked native game code."]
245 | #[allow(non_snake_case)]
246 | fn #detour_name (#(#args_input),*) -> #return_type {
247 | // wrap everything in an unsafe block here
248 | // we can't easily pass an unsafe function to initialize otherwise, since we would
249 | // have to wrap it in a closure, which would require knowing the closure signature,
250 | // which we don't know at compile time/from a proc macro, at least not easily
251 | let original = hook_name;
252 | unsafe {
253 | #(#fn_body)*
254 | }
255 | }
256 | }
257 | .into()
258 | }
259 |
260 | #[proc_macro]
261 | pub fn __fn_hook_symbol(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
262 | let fn_name = syn::parse_macro_input!(input as Ident);
263 |
264 | let sym = format_ident!("__hook__{}", fn_name);
265 |
266 | quote! { #sym }.into()
267 | }
268 |
269 | #[proc_macro]
270 | pub fn __fn_detour_symbol(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
271 | let fn_name = syn::parse_macro_input!(input as Ident);
272 |
273 | let sym = format_ident!("__detour__{}", fn_name);
274 |
275 | quote! { #sym }.into()
276 | }
277 |
--------------------------------------------------------------------------------
/rpc/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "grebuloff-rpc"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | anyhow = { workspace = true }
8 | log = { workspace = true }
9 | tokio = { workspace = true }
10 | rmp = { workspace = true }
11 | rmp-serde = { workspace = true }
12 | serde = { workspace = true }
13 | serde_json = { workspace = true }
14 | bytes = { workspace = true }
15 | async-trait = "0.1.71"
16 |
--------------------------------------------------------------------------------
/rpc/src/lib.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | pub mod ui;
4 |
5 | #[derive(Debug, PartialEq, Deserialize, Serialize)]
6 | #[serde(untagged)]
7 | pub enum RpcMessageDirection {
8 | /// Serverbound (client-to-server) communication.
9 | #[serde(skip_serializing)]
10 | Serverbound(RpcServerboundMessage),
11 |
12 | /// Clientbound (server-to-client) communication.
13 | #[serde(skip_deserializing)]
14 | Clientbound(RpcClientboundMessage),
15 | }
16 |
17 | #[derive(Debug, PartialEq, Deserialize)]
18 | pub enum RpcServerboundMessage {
19 | Ui(ui::UiRpcServerboundMessage),
20 | }
21 |
22 | #[derive(Debug, PartialEq, Serialize)]
23 | pub enum RpcClientboundMessage {
24 | Ui(ui::UiRpcClientboundMessage),
25 | }
26 |
--------------------------------------------------------------------------------
/rpc/src/ui.rs:
--------------------------------------------------------------------------------
1 | use super::{RpcClientboundMessage, RpcServerboundMessage};
2 | use anyhow::{bail, Result};
3 | use bytes::{Buf, Bytes, BytesMut};
4 | use serde::Deserialize;
5 | use serde::Serialize;
6 |
7 | #[derive(Debug, PartialEq, Deserialize)]
8 | pub enum UiRpcServerboundMessage {}
9 |
10 | impl TryFrom for UiRpcServerboundMessage {
11 | type Error = ();
12 |
13 | fn try_from(msg: RpcServerboundMessage) -> Result {
14 | match msg {
15 | RpcServerboundMessage::Ui(msg) => Ok(msg),
16 | _ => Err(()),
17 | }
18 | }
19 | }
20 |
21 | impl From for RpcClientboundMessage {
22 | fn from(msg: UiRpcClientboundMessage) -> Self {
23 | RpcClientboundMessage::Ui(msg)
24 | }
25 | }
26 |
27 | // note to future self: use actual structs instead of enum variant values
28 | // since rmp-serde doesn't properly (how we want it to, anyways) support
29 | // variant values
30 | #[derive(Debug, PartialEq, Serialize)]
31 | pub enum UiRpcClientboundMessage {
32 | /// Sent when the game window is resized.
33 | /// Triggers a resize of the UI.
34 | Resize(UiRpcClientboundResize),
35 | }
36 |
37 | #[derive(Debug, PartialEq)]
38 | pub struct UiRpcServerboundPaint {
39 | pub width: u16,
40 | pub height: u16,
41 | pub format: ImageFormat,
42 | pub data: Bytes,
43 | }
44 |
45 | impl UiRpcServerboundPaint {
46 | pub fn from_raw(mut buf: BytesMut) -> Result {
47 | let data = buf.split_off(5).freeze();
48 |
49 | // image format is first, so we don't overlap 0x80..=0x8F | 0xDE..=0xDF (msgpack map)
50 | let format = match buf.get_u8() {
51 | 0 => ImageFormat::BGRA8,
52 | _ => bail!("invalid image format"),
53 | };
54 | let width = buf.get_u16_le();
55 | let height = buf.get_u16_le();
56 |
57 | Ok(Self {
58 | width,
59 | height,
60 | format,
61 | data,
62 | })
63 | }
64 | }
65 |
66 | #[derive(Debug, PartialEq, Serialize)]
67 | pub struct UiRpcClientboundResize {
68 | pub width: u32,
69 | pub height: u32,
70 | }
71 |
72 | /// Represents supported image formats.
73 | #[derive(Debug, PartialEq, Deserialize, Serialize)]
74 | #[repr(u8)]
75 | pub enum ImageFormat {
76 | BGRA8,
77 | }
78 |
79 | impl ImageFormat {
80 | pub fn byte_size_of(&self, width: usize, height: usize) -> usize {
81 | width * height * self.bytes_per_pixel() as usize
82 | }
83 |
84 | pub fn bytes_per_pixel(&self) -> u32 {
85 | match self {
86 | ImageFormat::BGRA8 => 4,
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | # Newer nightlies break V8 build currently - will be fixed in v8@0.74.0
3 | # I'd love to use stable here, but dll-syringe requires nightly features
4 | channel = "nightly-2023-06-02"
5 |
--------------------------------------------------------------------------------
/src/dalamud.rs:
--------------------------------------------------------------------------------
1 | use log::info;
2 | use std::sync::Mutex;
3 | use std::time::Duration;
4 | use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient};
5 | use tokio::time;
6 | use windows::Win32::Foundation::ERROR_PIPE_BUSY;
7 |
8 | #[derive(Debug)]
9 | pub struct DalamudPipe {
10 | pipe_name: String,
11 | pipe_client: Mutex