├── 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 | ![fgohook logging requests](pic.png) 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 | --------------------------------------------------------------------------------