├── pic.png
├── UNLICENSE
├── README.md
└── fgohook.c
/pic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Francesco149/fgohook/HEAD/pic.png
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | logs fate grand order HTTP traffic through a stub library.
2 |
3 | 
4 |
5 | architecture is arm32 only
6 |
7 | addresses are dynamically scanned through unity metadata. as long as the
8 | binary doesn't change radically it should update itself
9 |
10 | # building (linux)
11 | download the latest android ndk standalone and extract it somewhere
12 |
13 | set CC to your ndk location like so and run build.sh
14 |
15 | ```sh
16 | export CC=~/android-ndk-r20/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang
17 | ./build.sh
18 | ```
19 |
20 | # usage
21 | have a rooted device with magisk hide enabled for the game. connect over
22 | adb, or just ssh into it
23 |
24 | replace the original library
25 |
26 | ```sh
27 | adb root
28 | adb push libmain.so /data/app/
29 | adb shell
30 |
31 | cd /data/app/com.aniplex.fategrandorder-*/lib/arm/
32 | mv libmain.so{,.bak}
33 | mv /data/app/libmain.so .
34 | chmod 755 libmain.so
35 | chown system:system libmain.so
36 | exit
37 | ```
38 |
39 | clear logcat and start logging
40 |
41 | ```sh
42 | adb shell logcat -c
43 | adb shell logcat | grep --line-buffered fgohook
44 | ```
45 |
46 | now start the game and watch the log. I usually pipe the above command into
47 | a file, like
48 | `adb shell logcat -d | grep --line-buffered fgohook > log.txt`
49 | so you can read it in your favorite editor
50 |
51 | if it fails to open global-metadata.dat, you might need to change
52 | permissions on the directory it's located at. for example to make it work
53 | on bluestacks I had to do `chmod -R 777 /data/media` as root.
54 |
55 | my personal setup is a bit different, I host the binary on a local http
56 | server and then adb shell over lan into my android machine, and wget it
57 | as root using this script from adb shell
58 |
59 | ```sh
60 | #!/system/bin/sh
61 |
62 | PACKAGE_NAME="com.aniplex.fategrandorder"
63 | MAIN_ACTIVITY="jp.delightworks.Fgo.player.AndroidPlugin"
64 | am force-stop "${PACKAGE_NAME}"
65 | cd /data/app/${PACKAGE_NAME}*/lib/arm
66 | [ ! -e libmain.so.bak ] && cp libmain.so libmain.so.bak
67 | wget -O libmain.so 192.168.1.2:8080
68 | chmod 755 libmain.so
69 | chown system:system libmain.so
70 | am start -n "${PACKAGE_NAME}/${MAIN_ACTIVITY}"
71 | logcat -c
72 | sleep 1
73 | logcat | grep fgohook | sed 's/^.*fgohook\.c: //g'
74 | ```
75 |
--------------------------------------------------------------------------------
/fgohook.c:
--------------------------------------------------------------------------------
1 | /*
2 | * This is free and unencumbered software released into the public domain.
3 | * see the attached UNLICENSE or http://unlicense.org/
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 |
18 | #define LOG_MAX 2048
19 |
20 | void log_n(char* s, int n) {
21 | char buf[LOG_MAX + 1];
22 | char *p, *end;
23 | for (p = s, end = p + n; p < end; p += LOG_MAX) {
24 | int len = end - p;
25 | len = len > LOG_MAX ? LOG_MAX : len;
26 | memcpy(buf, p, len);
27 | buf[len] = 0;
28 | __android_log_write(ANDROID_LOG_DEBUG, __FILE__, buf);
29 | }
30 | }
31 |
32 | void log_s(char* s) { log_n(s, strlen(s)); }
33 | void (*_JNI_OnLoad)(void* env);
34 |
35 | /*
36 | make memory readadable, writable and executable. size is
37 | ceiled to a multiple of PAGESIZE and addr is aligned to
38 | PAGESIZE
39 | */
40 | #define PROT_RWX (PROT_READ | PROT_WRITE | PROT_EXEC)
41 | #define PAGESIZE sysconf(_SC_PAGESIZE)
42 | #define PAGEOF(addr) (void*)((int)(addr) & ~(PAGESIZE - 1))
43 | #define PAGE_ROUND_UP(x) \
44 | ((((int)(x)) + PAGESIZE - 1) & (~(PAGESIZE - 1)))
45 | #define munprotect(addr, n) \
46 | mprotect(PAGEOF(addr), PAGE_ROUND_UP(n), PROT_RWX)
47 |
48 | typedef struct {
49 | char unknown[8];
50 | int Length;
51 | unsigned short Data[1];
52 | } String;
53 |
54 | /* truncate to ascii. good enough for now */
55 | static
56 | char* String_c(String* str) {
57 | int i;
58 | char* buf;
59 | if (!str) {
60 | return strdup("(null string)");
61 | }
62 | if (!str->Length) {
63 | return strdup("(empty string)");
64 | }
65 | buf = malloc(str->Length + 1);
66 | for (i = 0; i < str->Length; ++i) {
67 | buf[i] = (char)str->Data[i];
68 | }
69 | buf[i] = 0;
70 | return buf;
71 | }
72 |
73 | static
74 | void String_log(String* str) {
75 | char* buf = String_c(str);
76 | log_s(buf);
77 | free(buf);
78 | }
79 |
80 | typedef struct {
81 | char unknown[12];
82 | int Length;
83 | char Data[1];
84 | } Array;
85 |
86 | static
87 | void Array_log_ascii(Array* arr) {
88 | char* buf;
89 | if (!arr) {
90 | log_s("(null array)");
91 | return;
92 | }
93 | buf = malloc(arr->Length + 1);
94 | if (!buf) {
95 | log_s("(empty array or OOM)");
96 | return;
97 | }
98 | memcpy(buf, arr->Data, arr->Length);
99 | buf[arr->Length] = 0;
100 | log_s(buf);
101 | free(buf);
102 | }
103 |
104 | static
105 | void Array_log(Array* arr) {
106 | char *buf, *p;
107 | int i;
108 | if (!arr) {
109 | log_s("(null array)");
110 | return;
111 | }
112 | buf = malloc(arr->Length * 2 + 1);
113 | if (!buf) {
114 | log_s("(empty array or OOM)");
115 | return;
116 | }
117 | p = buf;
118 | for (i = 0; i < arr->Length; ++i) {
119 | p += sprintf(p, "%02x", arr->Data[i]);
120 | }
121 | *p = 0;
122 | log_s(buf);
123 | free(buf);
124 | }
125 |
126 | typedef struct {
127 | void* vtable;
128 | void* unknown;
129 | char isUnixFilePath;
130 | String* source;
131 | String* scheme;
132 | String* host;
133 | int port;
134 | String* path;
135 | String* query;
136 | String* fragment;
137 | String* userinfo;
138 | char isUnc;
139 | char isOpaquePart;
140 | char isAbsoluteUri;
141 | Array* segments; /* array of String* */
142 | char userEscaped;
143 | String* cachedAbsoluteUri;
144 | String* cachedToString;
145 | String* cachedLocalPath;
146 | int cachedHashCode;
147 | void* parser; /* UriParser */
148 | } Uri;
149 |
150 | typedef struct _HTTPRequest HTTPRequest;
151 |
152 | typedef struct {
153 | void* vtable;
154 | void* unknown;
155 | int VersionMajor;
156 | int VersionMinor;
157 | int StatusCode;
158 | String* Message;
159 | char IsStreamed;
160 | char IsStreamingFinished;
161 | char IsFromCache;
162 | void* Headers;
163 | Array* Data;
164 | char IsUpgraded;
165 | void* Cookies;
166 | Array* dataAsText;
167 | void* texture;
168 | char IsClosedManually;
169 | HTTPRequest* baseRequest;
170 | void* Stream;
171 | void* streamedFragments;
172 | void* SyncRoot;
173 | Array* fragmentBuffer;
174 | int fragmentBufferDataLength;
175 | void* cacheStream;
176 | int allFragmentSize;
177 | } HTTPResponse;
178 |
179 | typedef struct _HTTPRequest {
180 | void* vtable;
181 | void* unknown;
182 | Uri* Uri;
183 | int MethodType;
184 | Array* RawData;
185 | void* UploadStream;
186 | char DisposeUploadStream;
187 | char UseUploadStreamLength;
188 | void* OnUploadProgress;
189 | void* Callback;
190 | void* OnProgress;
191 | void* OnUpgraded;
192 | char DisableRetry;
193 | char IsRedirected;
194 | Uri* RedirectUri;
195 | HTTPResponse* Response;
196 | HTTPResponse* ProxyResponse;
197 | void* Exception;
198 | void* Tag;
199 | void* Credentials;
200 | void* Proxy;
201 | int MaxRedirects;
202 | char UseAlternateSSL;
203 | char IsCookiesEnabled;
204 | void* customCookies;
205 | void* FormUsage;
206 | int State;
207 | int RedirectCount;
208 | void* CustomCertificationValidator;
209 | uint64_t ConnectTimeout;
210 | uint64_t Timeout;
211 | char EnableTimoutForStreaming;
212 | int Priority;
213 | void* CustomCertificateVerifyer;
214 | void* unk;
215 | void* ProtocolHandler;
216 | void* onBeforeRedirection;
217 | void* unk1;
218 | int Downloaded;
219 | int DownloadLength;
220 | char DownloadProgressChanged;
221 | int64_t Uploaded;
222 | int64_t UploadLength;
223 | char UploadProgressChanged;
224 | char IsKeepAlive;
225 | char disableCache;
226 | int streamFragmentSize;
227 | char useStreaming;
228 | void* Headers;
229 | void* FieldCollector;
230 | void* FormImpl;
231 | } HTTPRequest;
232 |
233 | static void (*original_SendRequestImpl)(HTTPRequest* req);
234 |
235 | static void hooked_SendRequestImpl(HTTPRequest* req) {
236 | log_s("==============================================================");
237 | String_log(req->Uri->source);
238 | log_s("");
239 | log_s("request:");
240 | Array_log_ascii(req->RawData);
241 | original_SendRequestImpl(req);
242 | }
243 |
244 | static void (*original_CallCallback)(HTTPRequest*);
245 |
246 | static void hooked_CallCallback(HTTPRequest* req) {
247 | char buf[320];
248 | if (req->Response) {
249 | sprintf(buf, "version_minor: %d", req->Response->VersionMinor);
250 | log_s(buf);
251 | sprintf(buf, "version_major: %d", req->Response->VersionMajor);
252 | log_s(buf);
253 | sprintf(buf, "code: %d", req->Response->StatusCode);
254 | log_s(buf);
255 | log_s("response:");
256 | Array_log_ascii(req->Response->Data);
257 | }
258 | original_CallCallback(req);
259 | log_s("done");
260 | }
261 |
262 | static void log_header(char* dir, String* k, String* v) {
263 | char* buf = malloc(k->Length + v->Length + 4 + strlen(dir));
264 | char* ks = String_c(k);
265 | char* vs = String_c(v);
266 | sprintf(buf, "%s %s: %s", dir, ks, vs);
267 | log_s(buf);
268 | free(buf);
269 | free(ks);
270 | free(vs);
271 | }
272 |
273 | static void (*original_HTTPRequest_AddHeader)(HTTPRequest*,
274 | String*, String*);
275 |
276 | static void hooked_HTTPRequest_AddHeader(HTTPRequest* resp,
277 | String* k, String* v)
278 | {
279 | log_header("->", k, v);
280 | original_HTTPRequest_AddHeader(resp, k, v);
281 | }
282 |
283 | static void (*original_HTTPResponse_AddHeader)(HTTPResponse*,
284 | String*, String*);
285 |
286 | static void hooked_HTTPResponse_AddHeader(HTTPResponse* resp,
287 | String* k, String* v)
288 | {
289 | log_header("<-", k, v);
290 | original_HTTPResponse_AddHeader(resp, k, v);
291 | }
292 |
293 | #define THUMB (1<<1)
294 |
295 | static
296 | void hook(char* name, char* addr, void** ptrampoline, void* dst, int fl) {
297 | char buf[512];
298 | int i;
299 | char *p;
300 | unsigned* code;
301 |
302 | unsigned absolute_jump =
303 | (fl & THUMB) ?
304 | 0xF000F8DF: /* thumb mode: ldr pc,[pc] */
305 | 0xE51FF004; /* arm mode: ldr pc,[pc,#-4] */
306 |
307 | p = buf;
308 | p += sprintf(p, "%s at %p: ", name, addr);
309 | for (i = 0; i < 8; ++i) {
310 | p += sprintf(p, "%02x ", addr[i]);
311 | }
312 | log_s(buf);
313 | sprintf(buf, "-> %p", dst);
314 | log_s(buf);
315 |
316 | /*
317 | * alloc a trampoline to call the original function.
318 | * it's a copy of the instructions we overwrote followed by a jmp to
319 | * right after where hook jump will be in the original function
320 | * again using an abosolute jump like in the asm trampolines
321 | */
322 | *ptrampoline = malloc(8 + 8);
323 | code = (unsigned*)*ptrampoline;
324 | munprotect(code, 8 + 8);
325 | memcpy(code, addr, 8);
326 | code[2] = absolute_jump;
327 | code[3] = (unsigned)addr + 8;
328 |
329 | /*
330 | * overwrite the original function's first 8 bytes with an absolute jump
331 | * to our hook
332 | */
333 | code = (unsigned*)addr;
334 | munprotect(code, 8);
335 | code[0] = absolute_jump;
336 | code[1] = (unsigned)dst;
337 | }
338 |
339 | typedef struct {
340 | unsigned magic;
341 | int version;
342 | int strings;
343 | int strings_size;
344 | int string_data;
345 | int string_data_size;
346 | int metadata_strings;
347 | int metadata_strings_size;
348 | int events;
349 | int events_size;
350 | int properties;
351 | int properties_size;
352 | int methods;
353 | int methods_size;
354 | } __attribute__((packed))
355 | il2cpp_metadata_header_t;
356 |
357 | typedef struct {
358 | int name; /* index into metadata strings, null terminated */
359 | int declaring_type;
360 | int return_type;
361 | int parameter_start;
362 | /*int custom_attrib;*/
363 | int generic_container;
364 | int index; /* index into methods table */
365 | int invoker_index;
366 | int delegate_wrapper_index;
367 | int rgctx_start_index;
368 | int rgctx_count;
369 | unsigned token;
370 | unsigned short flags;
371 | unsigned short iflags;
372 | unsigned short slot;
373 | unsigned short num_parameters;
374 | } __attribute__((packed))
375 | il2cpp_method_definition_t;
376 |
377 | typedef struct {
378 | int method_pointers_size;
379 | unsigned* method_pointers;
380 | } __attribute__((packed))
381 | il2cpp_code_registration_t;
382 |
383 | unsigned char pattern_v24[] = {
384 | 0x01, 0x10, 0x9f, 0xe7, /* ldr r1,[pc,r1] */
385 | 0x00, 0x00, 0x8f, 0xe0, /* add r0,pc,r0 */
386 | 0x02, 0x20, 0x8f, 0xe0 /* add r2,pc,r2 */
387 | };
388 |
389 | static
390 | int phdr_callback(struct dl_phdr_info* info, size_t size, void* data) {
391 | il2cpp_code_registration_t** pcode_reg = data;
392 | int i;
393 | char buf[512];
394 | if (!strstr(info->dlpi_name, "libil2cpp.so")) return 0;
395 | sprintf(buf, "il2cpp at %08x", info->dlpi_addr);
396 | log_s(buf);
397 | for (i = 0; i < info->dlpi_phnum; ++i) {
398 | Elf32_Phdr const* hdr = &info->dlpi_phdr[i];
399 | Elf32_Addr start = hdr->p_vaddr;
400 | Elf32_Addr end = hdr->p_vaddr + hdr->p_memsz;
401 | if (hdr->p_type != PT_LOAD) continue;
402 | if (!(hdr->p_flags & PF_X)) continue;
403 | Elf32_Addr p;
404 | for (p = start; p <= end - sizeof(pattern_v24); p += 1) {
405 | if (!memcmp(pattern_v24, (char*)info->dlpi_addr + p, sizeof(pattern_v24))) {
406 | /* this is unreadable but it works trust me */
407 | Elf32_Addr code_registration =
408 | *(Elf32_Addr*)(info->dlpi_addr + p + 0x14) +
409 | p + 0xc;
410 | Elf32_Addr metadata_registration =
411 | *(Elf32_Addr*)(
412 | info->dlpi_addr +
413 | *(Elf32_Addr*)(info->dlpi_addr + p + 0x10)
414 | + p + 0x8
415 | ) - info->dlpi_addr;
416 | sprintf(buf, "code registration: %08x | "
417 | " metadata registration: %08x",
418 | code_registration, metadata_registration);
419 | log_s(buf);
420 | *pcode_reg = (void*)(info->dlpi_addr + code_registration);
421 | return 0;
422 | }
423 | }
424 | }
425 | return 0;
426 | }
427 |
428 | static
429 | void hook_from_metadata() {
430 | char buf[256], buf2[256];
431 | char const* p;
432 | char* dst;
433 | Dl_info dli;
434 | FILE* f;
435 | int i, num_methods;
436 | il2cpp_metadata_header_t hdr;
437 | il2cpp_method_definition_t* methods = 0;
438 | il2cpp_code_registration_t* code_reg = 0;
439 | unsigned* method_pointers;
440 | char* metadata_strings = 0;
441 | int addheader_count = 0;
442 | if (!dladdr(hook_from_metadata, &dli)) {
443 | log_s("failed to get own path");
444 | }
445 | // /data/app/com.klab.lovelive.allstars-mi_*/lib/arm/*.so
446 | sprintf(buf, "running as %s\n", dli.dli_fname);
447 | log_s(buf);
448 | for (p = dli.dli_fname; *p && strstr(p, "com.") != p; ++p);
449 | // com.klab.lovelive.allstars-*/lib/arm/*.so
450 | for (dst = buf; *p && *p != '-'; *dst++ = *p++);
451 | *dst = 0;
452 | // com.klab.lovelive.allstars-*
453 | sprintf(buf2,
454 | "/data/data/%s/files/il2cpp/Metadata/global-metadata.dat", buf);
455 | log_s(buf2);
456 | f = fopen(buf2, "rb");
457 | if (!f) {
458 | /* on bluestacks it seems to have this different path. maybe on
459 | * real device as well */
460 | sprintf(buf2, "/data/media/0/Android/data/%s/files"
461 | "/il2cpp/Metadata/global-metadata.dat", buf);
462 | log_s(buf2);
463 | f = fopen(buf2, "rb");
464 | }
465 | if (!f) {
466 | log_s(strerror(errno));
467 | log_s("failed to open metadata file");
468 | return;
469 | }
470 | if (fread(&hdr, sizeof(hdr), 1, f) != 1) {
471 | log_s("failed to read metadata header");
472 | goto cleanup;
473 | }
474 | if (hdr.magic != 0xFAB11BAF) {
475 | log_s("not a valid metadata file");
476 | goto cleanup;
477 | }
478 | sprintf(buf, "metadata version %d", hdr.version);
479 | log_s(buf);
480 | if (fseek(f, hdr.methods, SEEK_SET)) {
481 | log_s("failed to seek to methods table");
482 | goto cleanup;
483 | }
484 | methods = malloc(hdr.methods_size);
485 | if (!methods) {
486 | log_s("failed to alloc method table");
487 | goto cleanup;
488 | }
489 | if (fread(methods, hdr.methods_size, 1, f) != 1) {
490 | log_s("failed to read method table");
491 | goto cleanup;
492 | }
493 | dl_iterate_phdr(phdr_callback, &code_reg);
494 | if (!code_reg) {
495 | log_s("failed to find code registration");
496 | goto cleanup;
497 | }
498 | num_methods = hdr.methods_size / sizeof(il2cpp_method_definition_t);
499 | method_pointers = code_reg->method_pointers;
500 | metadata_strings = malloc(hdr.metadata_strings_size);
501 | if (!metadata_strings) {
502 | log_s("failed to alloc metadata strings");
503 | goto cleanup;
504 | }
505 | if (fseek(f, hdr.metadata_strings, SEEK_SET)) {
506 | log_s("failed to seek to metadata strings");
507 | goto cleanup;
508 | }
509 | if (fread(metadata_strings, hdr.metadata_strings_size, 1, f) != 1) {
510 | log_s("failed to read metadata strings");
511 | goto cleanup;
512 | }
513 | sprintf(buf, "scanning %d methods", num_methods);
514 | log_s(buf);
515 | for (i = 0; i < num_methods; ++i) {
516 | char* name = metadata_strings + methods[i].name;
517 | if (methods[i].index >= 0) {
518 | /* TODO: generic methods */
519 | /* TODO: get class name for better reliability */
520 | #define h(name, addr) \
521 | hook(#name, (char*)addr, (void**)&original_##name, \
522 | hooked_##name, 0)
523 | if (!strcmp(name, "SendRequestImpl")) {
524 | /* HTTPManager$$SendRequestImpl */
525 | h(SendRequestImpl, method_pointers[methods[i].index]);
526 | } else if (!strcmp(name, "CallCallback")) {
527 | /* HTTPRequest$$CallCallback */
528 | h(CallCallback, method_pointers[methods[i].index]);
529 | } else if (!strcmp(name, "AddHeader")) {
530 | switch (addheader_count) {
531 | case 2:
532 | h(HTTPRequest_AddHeader, method_pointers[methods[i].index]);
533 | break;
534 | case 3:
535 | h(HTTPResponse_AddHeader, method_pointers[methods[i].index]);
536 | break;
537 | }
538 | ++addheader_count;
539 | }
540 | #undef h
541 | }
542 | }
543 | cleanup:
544 | log_s("scanned");
545 | free(metadata_strings);
546 | free(methods);
547 | fclose(f);
548 | }
549 |
550 | static
551 | void init() {
552 | void *original, *stub;
553 | log_s("hello from the stub library!");
554 | dlopen("libil2cpp.so", RTLD_LAZY);
555 | original = dlopen("libmain.so.bak", RTLD_LAZY);
556 | stub = dlopen("libmain.so", RTLD_LAZY);
557 | *(void**)&_JNI_OnLoad = dlsym(original, "JNI_OnLoad");
558 | hook_from_metadata();
559 | }
560 |
561 | void JNI_OnLoad(void* env) {
562 | init();
563 | _JNI_OnLoad(env);
564 | }
565 |
--------------------------------------------------------------------------------