diff --git a/gen_stubs.py b/gen_stubs.py index 5f4fa84..9c86197 100644 --- a/gen_stubs.py +++ b/gen_stubs.py @@ -120,7 +120,6 @@ def main(): "EOS_Sanctions_QueryActivePlayerSanctions", "EOS_Sanctions_GetPlayerSanctionCount", "EOS_Sanctions_CopyPlayerSanctionByIndex", - ] funcs = [] diff --git a/keyDumper/encrypt_bundles.py b/keyDumper/encrypt_bundles.py new file mode 100644 index 0000000..70242b4 --- /dev/null +++ b/keyDumper/encrypt_bundles.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import os, sys, struct, argparse +from Crypto.Cipher import AES + +KEY_DEFAULT = b"rK7CcATuZk7LAmhqqU4iBLNmAq8QbK3s" +KEY_ALTERNATE = b"x4DZmD6D2HhkzT6qD8HpKeZdgM9HCmXP" + + +def xor_crypt(data, key): + cipher = AES.new(key, AES.MODE_ECB) + out = bytearray(len(data)) + for off in range(0, len(data), 16): + ctr = off // 16 + 1 + ks = cipher.encrypt(struct.pack(' {os.path.basename(outpath)}") + return True + encrypted = xor_crypt(data, key) + os.makedirs(os.path.dirname(outpath) or '.', exist_ok=True) + with open(outpath, 'wb') as f: + f.write(encrypted) + print(f" {os.path.basename(inpath)} -> {os.path.basename(outpath)} ({len(data)} bytes)") + return True + except Exception as e: + print(f" ERROR {os.path.basename(inpath)}: {e}", file=sys.stderr) + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Re-encrypt decrypted Unity bundles for Holo's Hanafuda") + parser.add_argument('input', help='Input .unity3d file or directory of .unity3d files') + parser.add_argument('-o', '--output', help='Output file or directory') + parser.add_argument('--key-alternate', action='store_true', + help='Use alternate bundle key candidate (x4DZmD6D2HhkzT6qD8HpKeZdgM9HCmXP)') + parser.add_argument('--key-hex', metavar='HEX', + help='64-char hex AES-256 key (overrides defaults)') + parser.add_argument('--in-place', action='store_true', + help='Write output next to the original .bundle in StandaloneWindows64/') + parser.add_argument('--dry-run', action='store_true', + help='List what would be encrypted without writing') + args = parser.parse_args() + + if args.key_hex: + key = bytes.fromhex(args.key_hex) + if len(key) != 32: + print("Error: --key-hex must be exactly 64 hex chars (32 bytes)", file=sys.stderr) + sys.exit(1) + elif args.key_alternate: + key = KEY_ALTERNATE + else: + key = KEY_DEFAULT + + inpath = args.input + + if os.path.isdir(inpath): + files = sorted(f for f in os.listdir(inpath) if f.endswith('.unity3d')) + if not files: + print(f"No .unity3d files found in '{inpath}'") + return + + if args.output: + outdir = args.output + elif args.in_place: + outdir = os.path.normpath(os.path.join(inpath, '..', '..', 'StandaloneWindows64')) + else: + outdir = os.path.normpath(os.path.join(inpath, '..', 'reencrypted')) + + if not args.dry_run: + os.makedirs(outdir, exist_ok=True) + + ok = errs = 0 + for fname in files: + fpath = os.path.join(inpath, fname) + base = fname.removesuffix('.unity3d') + outpath = os.path.join(outdir, base + '.bundle') + if process_file(fpath, outpath, key, args.dry_run): + ok += 1 + else: + errs += 1 + + total = len(files) + print(f"\n{ok} OK, {errs} errors of {total}") + if not args.dry_run: + print(f"Output directory: {outdir}/") + + else: + if not os.path.exists(inpath): + print(f"Error: '{inpath}' not found", file=sys.stderr) + sys.exit(1) + + if args.output: + outpath = args.output + elif args.in_place: + base = os.path.splitext(os.path.basename(inpath))[0] + bundle_name = base + '.bundle' + candidates = [ + os.path.join(os.path.dirname(inpath), bundle_name), + os.path.normpath(os.path.join( + os.path.dirname(inpath), '..', '..', 'StandaloneWindows64', bundle_name)), + ] + outpath = None + for c in candidates: + if os.path.exists(c): + outpath = c + break + if not outpath: + print(f"Error: --in-place but no existing .bundle found for '{inpath}'", + file=sys.stderr) + sys.exit(1) + else: + basedir = os.path.dirname(inpath) or '.' + basename = os.path.splitext(os.path.basename(inpath))[0] + '.bundle' + outpath = os.path.join(basedir, basename) + + process_file(inpath, outpath, key, args.dry_run) + + +if __name__ == '__main__': + main() diff --git a/main.cpp b/main.cpp index 2674305..e15d6c9 100644 --- a/main.cpp +++ b/main.cpp @@ -48,6 +48,7 @@ static void DrainCallbackQueue() { // logging system FILE *logFile = NULL; FILE *packetFile = NULL; +FILE *telemetryFile = NULL; static CRITICAL_SECTION g_packetCs; void InitLogFiles(); @@ -65,6 +66,27 @@ void Log(const char *format, ...) { va_end(args); } +void LogTelemetry(const char *format, ...) { + va_list args; + va_start(args, format); + SYSTEMTIME st; + GetLocalTime(&st); + if (logFile) { + fprintf(logFile, "[%02u:%02u:%02u.%03u] ", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); + vfprintf(logFile, format, args); + fprintf(logFile, "\n"); + } + va_end(args); + va_start(args, format); + if (telemetryFile) { + fprintf(telemetryFile, "[%02u:%02u:%02u.%03u] ", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); + vfprintf(telemetryFile, format, args); + fprintf(telemetryFile, "\n"); + fflush(telemetryFile); + } + va_end(args); +} + static void HexDump(const void *data, size_t len, char *out) { const unsigned char *p = (const unsigned char *)data; for (size_t i = 0; i < len; ++i) sprintf(out + i * 3, "%02X ", p[i]); @@ -242,6 +264,12 @@ static const DWORD kAutoTeardownSuppressWindowMs = 100; static LONG g_autoTeardownSuppressed = 0; static volatile DWORD g_autoTeardownSuppressTime = 0; +// Telemetry blocking config +static bool g_telemetryBlock = false; +static bool g_dumpTraffic = false; +static char g_blockedDomains[1024] = "api.epicgames.dev"; +static char g_blockedIPs[1024] = "104.18.125.108"; + void LogStateSnapshot(const char *context) { Log("[State] %s | isHost=%d peerConnected=%d lobbyHasRemoteMember=%d initialized=%d clientSawSearchResult=%d", context ? context : "???", (int)AtomicRead(&g_isHost), (int)AtomicRead(&g_peerConnected), @@ -2369,10 +2397,31 @@ void ProxySdkLogCallback(const EOS_LogMessage* Message) { if (Message) { InterlockedIncrement(&g_sdkLogCount); - Log("[SDK/%s] %s: %s", - Message->Category ? Message->Category : "?", - LogLevelToStr(Message->Level), - Message->Message ? Message->Message : ""); + bool isTelemetry = Message->Message && + (strstr(Message->Message, "api.epicgames.dev") || + strstr(Message->Message, "0.0.0.0") || + strstr(Message->Message, "Couldn't connect to server") || + strstr(Message->Message, "Connection refused") || + strstr(Message->Message, "Failed to connect to the backend") || + strstr(Message->Message, "EOS_NoConnection")); + + if (isTelemetry) { + if (telemetryFile) { + SYSTEMTIME st; + GetLocalTime(&st); + fprintf(telemetryFile, "[%02u:%02u:%02u.%03u] [SDK/%s] %s: %s\n", + st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, + Message->Category ? Message->Category : "?", + LogLevelToStr(Message->Level), + Message->Message ? Message->Message : ""); + fflush(telemetryFile); + } + } else { + Log("[SDK/%s] %s: %s", + Message->Category ? Message->Category : "?", + LogLevelToStr(Message->Level), + Message->Message ? Message->Message : ""); + } } if (g_gameLogCallback) g_gameLogCallback(Message); } @@ -2448,10 +2497,13 @@ void InitLogFiles() { else path[0] = '\0'; char logPath[MAX_PATH]; char pktPath[MAX_PATH]; + char telPath[MAX_PATH]; strcpy(logPath, path); strcat(logPath, "proxy_p2p.log"); strcpy(pktPath, path); strcat(pktPath, "proxy_packets.log"); + strcpy(telPath, path); strcat(telPath, "telemetry_logs.log"); logFile = fopen(logPath, "a"); packetFile = fopen(pktPath, "a"); + telemetryFile = fopen(telPath, "a"); if (logFile) { setvbuf(logFile, NULL, _IOFBF, 1 << 16); @@ -2468,14 +2520,135 @@ void InitLogFiles() { fprintf(packetFile, "Packet dump started at %02u:%02u:%02u\n", st.wHour, st.wMinute, st.wSecond); fflush(packetFile); } + if (telemetryFile) { + SYSTEMTIME st; + GetLocalTime(&st); + fprintf(telemetryFile, "\n========== Telemetry Log %02u:%02u:%02u ==========\n", + st.wHour, st.wMinute, st.wSecond); + fflush(telemetryFile); + } } void CloseLogFiles() { if (logFile) { fclose(logFile); logFile = NULL; } if (packetFile) { fclose(packetFile); packetFile = NULL; } + if (telemetryFile) { fclose(telemetryFile); telemetryFile = NULL; } DeleteCriticalSection(&g_packetCs); } +// === WinSock Telemetry Blocking Hooks === + +typedef int (WSAAPI *GetAddrInfoFn)(PCSTR, PCSTR, const ADDRINFOA *, PADDRINFOA *); +typedef int (WSAAPI *ConnectFn)(SOCKET, const struct sockaddr *, int); + +static GetAddrInfoFn Real_GetAddrInfo = NULL; +static ConnectFn Real_Connect = NULL; + +static bool PatchIat(HMODULE module, const char *sourceDll, const char *funcName, + WORD ordinal, void *hook, void **orig) { + if (!module) return false; + PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)module; + if (dos->e_magic != 0x5A4D) return false; + PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((char*)module + dos->e_lfanew); + if (nt->Signature != 0x00004550) return false; + PIMAGE_DATA_DIRECTORY importDir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (!importDir->VirtualAddress || !importDir->Size) return false; + static const uintptr_t kOrdFlag = 0x8000000000000000ULL; + PIMAGE_IMPORT_DESCRIPTOR imports = (PIMAGE_IMPORT_DESCRIPTOR)((char*)module + importDir->VirtualAddress); + for (; imports->Name; imports++) { + char *dllName = (char*)module + imports->Name; + if (_stricmp(dllName, sourceDll) != 0) continue; + PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((char*)module + imports->FirstThunk); + PIMAGE_THUNK_DATA origThunk = (PIMAGE_THUNK_DATA)((char*)module + imports->OriginalFirstThunk); + for (; thunk->u1.Function; thunk++, origThunk++) { + bool match = false; + if (funcName) { + if (origThunk->u1.Ordinal & kOrdFlag) continue; + PIMAGE_IMPORT_BY_NAME imp = (PIMAGE_IMPORT_BY_NAME)((char*)module + origThunk->u1.AddressOfData); + if (strcmp(imp->Name, funcName) == 0) match = true; + } else { + if (!(origThunk->u1.Ordinal & kOrdFlag)) continue; + if ((origThunk->u1.Ordinal & 0xFFFF) == ordinal) match = true; + } + if (!match) continue; + if (orig) *orig = (void*)(uintptr_t)thunk->u1.Function; + DWORD old; + if (VirtualProtect(&thunk->u1.Function, sizeof(void*), PAGE_READWRITE, &old)) { + thunk->u1.Function = (uintptr_t)(LONG_PTR)hook; + VirtualProtect(&thunk->u1.Function, sizeof(void*), old, &old); + return true; + } + return false; + } + } + return false; +} + +static int my_tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +static bool Listed(const char *value, const char *list) { + if (!value || !list || !list[0]) return false; + const char *p = list; + size_t vlen = strlen(value); + while (*p) { + while (*p == ' ' || *p == ',' || *p == ';') p++; + if (!*p) break; + const char *start = p; + while (*p && *p != ' ' && *p != ',' && *p != ';') p++; + size_t len = p - start; + if (len != vlen) continue; + bool match = true; + for (size_t i = 0; i < len; i++) { + if (my_tolower((unsigned char)value[i]) != my_tolower((unsigned char)start[i])) { + match = false; + break; + } + } + if (match) return true; + } + return false; +} + +static int WSAAPI Hook_GetAddrInfo(PCSTR name, PCSTR svc, const ADDRINFOA *hints, PADDRINFOA *res) { + int ret = Real_GetAddrInfo(name, svc, hints, res); + if (ret != 0) return ret; + if (name && Listed(name, g_blockedDomains)) { + if (g_telemetryBlock) { + LogTelemetry("[Telemetry] Redirected DNS: %s -> 0.0.0.0 (block via connect)", name); + ADDRINFOA *cur = *res; + while (cur) { + if (cur->ai_family == AF_INET && cur->ai_addr) { + ((struct sockaddr_in *)cur->ai_addr)->sin_addr.s_addr = htonl(INADDR_ANY); + } + cur = cur->ai_next; + } + } else if (g_dumpTraffic) { + LogTelemetry("[Telemetry] Traffic: DNS lookup %s (ALLOWED, dump mode)", name); + } + } + return ret; +} + +static int WSAAPI Hook_Connect(SOCKET s, const struct sockaddr *addr, int addrlen) { + if (addr && addr->sa_family == AF_INET) { + struct sockaddr_in *sin = (struct sockaddr_in *)addr; + char ip[16]; + strcpy(ip, inet_ntoa(sin->sin_addr)); + bool isBlocked = g_telemetryBlock && Listed(ip, g_blockedIPs); + if (isBlocked) { + LogTelemetry("[Telemetry] Blocked connect to %s:%d", ip, ntohs(sin->sin_port)); + WSASetLastError(WSAECONNREFUSED); + return SOCKET_ERROR; + } + if (g_dumpTraffic && Listed(ip, g_blockedIPs)) { + LogTelemetry("[Telemetry] Traffic: connect to %s:%d (ALLOWED, dump mode)", ip, ntohs(sin->sin_port)); + } + } + return Real_Connect(s, addr, addrlen); +} + BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { @@ -2492,6 +2665,25 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv if (dirSlash) *(dirSlash + 1) = '\0'; else g_dllDir[0] = '\0'; InitLogFiles(); + + // Load telemetry config + { + char iniPath2[MAX_PATH]; + snprintf(iniPath2, sizeof(iniPath2), "%sdirectip.ini", g_dllDir); + char telemetryStr[8] = "true"; + GetPrivateProfileStringA("Network", "Telemetry", "true", telemetryStr, sizeof(telemetryStr), iniPath2); + g_telemetryBlock = (_stricmp(telemetryStr, "false") == 0 || strcmp(telemetryStr, "0") == 0); + char dumpStr[8] = "false"; + GetPrivateProfileStringA("Telemetry", "DumpTraffic", "false", dumpStr, sizeof(dumpStr), iniPath2); + g_dumpTraffic = (_stricmp(dumpStr, "true") == 0 || strcmp(dumpStr, "1") == 0); + GetPrivateProfileStringA("Telemetry", "BlockDomains", "api.epicgames.dev", g_blockedDomains, sizeof(g_blockedDomains), iniPath2); + GetPrivateProfileStringA("Telemetry", "BlockIPs", "104.18.125.108", g_blockedIPs, sizeof(g_blockedIPs), iniPath2); + // Do NOT add 0.0.0.0 to the block list — let connect fail naturally + // so the EOS SDK's HttpManagerThread doesn't crash on synthetic errors. + LogTelemetry("[Telemetry] Config: block=%d dump=%d domains=%s ips=%s", + (int)g_telemetryBlock, (int)g_dumpTraffic, g_blockedDomains, g_blockedIPs); + } + char *lastSlash = strrchr(path, '\\'); if (!lastSlash) lastSlash = strrchr(path, '/'); if (lastSlash) { @@ -2525,6 +2717,16 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv } else { Log("[Proxy] Failed to load original DLL! Error: %d", GetLastError()); } + + // Patch EOS SDK IAT for ws2_32 telemetry hooks + bool okGa = PatchIat(hOriginalDll, "ws2_32.dll", "getaddrinfo", 0, + (void*)Hook_GetAddrInfo, (void**)&Real_GetAddrInfo); + bool okCo = PatchIat(hOriginalDll, "ws2_32.dll", NULL, 4, + (void*)Hook_Connect, (void**)&Real_Connect); + LogTelemetry("[Telemetry] IAT hooks: getaddrinfo=%s(%p) connect=%s(%p)", + okGa ? "OK" : "FAIL", (void*)Real_GetAddrInfo, + okCo ? "OK" : "FAIL", (void*)Real_Connect); + break; } case DLL_PROCESS_DETACH: