From 7f273cb17c211e9fdc959487d76f7ab78e86f482 Mon Sep 17 00:00:00 2001 From: BillyCool Date: Tue, 24 Feb 2026 20:22:08 +1100 Subject: [PATCH] Update hooks and server to get past asset and master db download, initial user data download --- frida/hooks.js | 109 ++++++++++++++++++++++++--- src/Program.cs | 142 ++++++++++++++++++++++++++++++++++-- src/Services/DataService.cs | 29 +++++++- src/Services/UserService.cs | 2 +- 4 files changed, 262 insertions(+), 20 deletions(-) diff --git a/frida/hooks.js b/frida/hooks.js index 304d483..6771d69 100644 --- a/frida/hooks.js +++ b/frida/hooks.js @@ -139,11 +139,6 @@ function readString(addr) { return addr.add(0x14).readUtf16String(); } -function logStackTrace() { - console.log(Thread.backtrace(this.context, Backtracer.ACCURATE) - .map(DebugSymbol.fromAddress).join('\n') + '\n'); -} - //#endregion // #region Private Server @@ -199,7 +194,6 @@ function Config_Api_MakeWebViewUrl(offset) { onEnter(args) { this.basePath = readString(args[0]); this.path = readString(args[1]); - onEnterLogWrapper(Config_Api_MakeWebViewUrl.name, `basePath=${this.basePath}, path=${this.path}`); }, onLeave(result) { const getLanguagePathFunc = new NativeFunction(libil2cpp.add(0x2E5B5C4), 'pointer', []); @@ -214,11 +208,10 @@ function Config_Api_MakeMasterDataUrl(offset) { const func_ptr = libil2cpp.add(offset); Interceptor.attach(func_ptr, { onEnter(args) { - this.param = readString(args[0]); - onEnterLogWrapper(Config_Api_MakeMasterDataUrl.name, this.param); + this.masterVersion = readString(args[0]); }, onLeave(result) { - writeString(result, `https://${SERVER_ADDRESS}/`) + writeString(result, `https://${SERVER_ADDRESS}/assets/release/${this.masterVersion}/database.bin.e`) onLeaveLogWrapper(Config_Api_MakeMasterDataUrl.name, readString(result)); } }); @@ -266,6 +259,18 @@ function HandleNet_Decrypt(offset) { } } +function OctoAPI_DecryptAes(offset) { + const func_ptr = libil2cpp.add(offset); + try { + Interceptor.replace(func_ptr, new NativeCallback(function (thisArg, bytes) { + return bytes; + }, 'pointer', ['pointer', 'pointer'])); + } + catch (e) { + console.log('OctoAPI_DecryptAes replace error:', e); + } +} + function LocalizeText_GetWordOrDefault(offset) { let matchDic = { '© SQUARE ENIX Developed by Applibot, Inc.': '© Marie\'s Wonderland', @@ -302,7 +307,75 @@ function DarkClient_InvokeAsync(offset) { const func_ptr = libil2cpp.add(offset); Interceptor.attach(func_ptr, { onEnter(args) { - onEnterLogWrapper(DarkClient_InvokeAsync.name, `path=${readString(args[1])}`); + this.path = readString(args[1]); + onEnterLogWrapper(DarkClient_InvokeAsync.name, `path=${this.path}`); + } + }); +} + +function DataManager_SetUrls(offset) { + const func_ptr = libil2cpp.add(offset); + Interceptor.attach(func_ptr, { + onEnter(args) { + try { + const db = args[0]; + if (db.isNull()) { + onEnterLogWrapper(DataManager_SetUrls.name, 'db NULL'); + return; + } + + const revision = db.add(0x10).readInt(); + const assetBundleListPtr = db.add(0x18).readPointer(); + const tagnamePtr = db.add(0x20).readPointer(); + const resourceListPtr = db.add(0x28).readPointer(); + const urlFormatPtr = db.add(0x30).readPointer(); + + const safeListCount = (ptr) => { + try { + if (ptr.isNull()) return 0; + return ptr.add(0x10).readInt(); + } catch (e) { return 'err'; } + }; + + const urlFormat = (urlFormatPtr.isNull()) ? null : readString(urlFormatPtr); + const assetBundleCount = safeListCount(assetBundleListPtr); + const tagnameCount = safeListCount(tagnamePtr); + const resourceCount = safeListCount(resourceListPtr); + + onEnterLogWrapper(DataManager_SetUrls.name, `revision=${revision}, urlFormat=${urlFormat}`); + onEnterLogWrapper(DataManager_SetUrls.name, `assetBundleList=${assetBundleCount}, tagname=${tagnameCount}, resourceList=${resourceCount}`); + } + catch (e) { + console.log('DataManager_SetUrls onEnter error:', e); + } + } + }); +} + +function DataManager_ApplyToDatabase(offset) { + const func_ptr = libil2cpp.add(offset); + Interceptor.attach(func_ptr, { + onEnter(args) { + this.instance = args[0]; + }, + onLeave(result) { + try { + const inst = this.instance; + if (!inst || inst.isNull()) { + onLeaveLogWrapper(DataManager_ApplyToDatabase.name, 'Applied to database (instance NULL)'); + return; + } + const revision = inst.add(0x3C).readInt(); + const urlFormatPtr = inst.add(0x40).readPointer(); + let urlFormat = null; + try { + if (!urlFormatPtr.isNull()) urlFormat = readString(urlFormatPtr); + } catch (e) { urlFormat = ''; } + onLeaveLogWrapper(DataManager_ApplyToDatabase.name, `Applied to database, Revision=${revision}, urlFormat=${urlFormat}`); + } + catch (e) { + console.log('DataManager_ApplyToDatabase onLeave error:', e); + } } }); } @@ -313,7 +386,7 @@ const SERVER_ADDRESS = 'humbly-tops-calf.ngrok-free.app'; awaitLibil2cpp(() => { callbackWrapper(LocalizeText_GetWordOrDefault, 0x2ACE4D8); // Text replacements - callbackWrapper(DarkOctoSetupper_CreateSetting, 0x3639410); + //callbackWrapper(DarkOctoSetupper_CreateSetting, 0x3639410); callbackWrapper(DarkOctoSetupper_GetE, 0x3638FC0); callbackWrapper(Config_Api_MakeWebViewUrl, 0x2E5B00C); // WebView URL callbackWrapper(Config_Api_MakeMasterDataUrl, 0x2E5B114); // Master db URL @@ -322,6 +395,20 @@ awaitLibil2cpp(() => { callbackWrapper(HandleNet_Encrypt, 0x279410C); // Bypass GRPC encryption callbackWrapper(HandleNet_Decrypt, 0x279420C); // Bypass GRPC decryption callbackWrapper(DarkClient_InvokeAsync, 0x38AC274); // GRPC requests logging + callbackWrapper(OctoAPI_DecryptAes, 0x4C27410); + //callbackWrapper(DataManager_SetUrls, 0x3DA0170); + //callbackWrapper(DataManager_ApplyToDatabase, 0x3D9F5EC); }); +function StackTrace(offset) { + const func_ptr = libil2cpp.add(offset); + Interceptor.attach(func_ptr, { + onEnter(args) { + // Log stack trace + console.log(Thread.backtrace(this.context, Backtracer.ACCURATE) + .map(DebugSymbol.fromAddress).join('\n') + '\n'); + } + }); +} + // frida -Uf com.square_enix.android_googleplay.nierspww -l "path\to\hooks.js" \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index c9c138f..8063653 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -5,6 +5,12 @@ namespace MariesWonderland; public static class Program { + // https://archive.org/download/nier_reincarnation_assets/resource_dump_android.7z + private const string AssetDatabaseBasePath = @"path\to\resource_dump_android\revisions"; + + // https://archive.org/download/nierreincarnation/Global/master_data/bin/ + private const string MasterDatabaseBasePath = @"path\to\master_data\bin"; + public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); @@ -31,12 +37,136 @@ public static class Program // Add HTTP middleware app.MapGet("/", () => "Marie's Wonderland is open for business :marie:"); - app.MapGet("/web/static/{languagePath}/terms/termsofuse", (string languagePath) => $"Terms of Service

Terms of Service

Language: {languagePath}

Version: ###123###

"); // Expects the version wrapped in delimiters like "###123###". - app.MapGet("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}"); - app.MapPost("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}"); - app.MapPut("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}"); - app.MapDelete("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}"); - app.MapPatch("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}"); + + // ToS. Expects the version wrapped in delimiters like "###123###". + app.MapGet("/web/static/{languagePath}/terms/termsofuse", (string languagePath) => $"Terms of Service

Terms of Service

Language: {languagePath}

Version: ###123###

"); + + // Asset Database + app.MapGet("/v2/pub/a/301/v/300116832/list/{revision}", async (string revision) => + await File.ReadAllBytesAsync(Path.Combine(AssetDatabaseBasePath, revision, "list.bin"))); + + // Master Database + app.MapMethods("/assets/release/{masterVersion}/database.bin.e", ["GET", "HEAD"], async (HttpContext ctx, string masterVersion) => + { + var filePath = Path.Combine(MasterDatabaseBasePath, $"{masterVersion}.bin.e"); + + var fileInfo = new FileInfo(filePath); + long totalLength = fileInfo.Length; + + // Advertise range support + ctx.Response.Headers.AcceptRanges = "bytes"; + + // Simple ETag using last write ticks & length (optional but useful for clients) + ctx.Response.Headers.ETag = $"\"{fileInfo.LastWriteTimeUtc.Ticks:x}-{totalLength:x}\""; + + // Handle HEAD quickly (send headers only) + bool isHead = string.Equals(ctx.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase); + + // Parse Range header if present + if (ctx.Request.Headers.TryGetValue("Range", out var rangeHeader)) + { + // Expect single range of form: bytes=start-end + var raw = rangeHeader.ToString(); + if (!raw.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase)) + { + // Malformed range; respond with 416 + ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable; + ctx.Response.Headers.ContentRange = $"bytes */{totalLength}"; + return; + } + + var rangePart = raw["bytes=".Length..].Trim(); + var parts = rangePart.Split('-', 2); + if (parts.Length != 2) + { + ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable; + ctx.Response.Headers.ContentRange = $"bytes */{totalLength}"; + return; + } + + bool startParsed = long.TryParse(parts[0], out long start); + bool endParsed = long.TryParse(parts[1], out long end); + + if (!startParsed && endParsed) + { + // suffix range: last 'end' bytes + long suffixLength = end; + if (suffixLength <= 0) + { + ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable; + ctx.Response.Headers.ContentRange = $"bytes */{totalLength}"; + return; + } + start = Math.Max(0, totalLength - suffixLength); + end = totalLength - 1; + } + else if (startParsed && !endParsed) + { + // range from start to end of file + end = totalLength - 1; + } + else if (!startParsed && !endParsed) + { + ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable; + ctx.Response.Headers.ContentRange = $"bytes */{totalLength}"; + return; + } + + // Validate + if (start < 0 || end < start || start >= totalLength) + { + ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable; + ctx.Response.Headers.ContentRange = $"bytes */{totalLength}"; + return; + } + + long length = end - start + 1; + + ctx.Response.StatusCode = StatusCodes.Status206PartialContent; + ctx.Response.Headers.ContentRange = $"bytes {start}-{end}/{totalLength}"; + ctx.Response.ContentType = "application/octet-stream"; + ctx.Response.ContentLength = length; + + if (isHead) + { + return; + } + + // Stream the requested range + await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + fs.Seek(start, SeekOrigin.Begin); + + var buffer = new byte[64 * 1024]; + long remaining = length; + while (remaining > 0) + { + int toRead = (int)Math.Min(buffer.Length, remaining); + int read = await fs.ReadAsync(buffer, 0, toRead); + if (read == 0) break; + await ctx.Response.Body.WriteAsync(buffer.AsMemory(0, read)); + remaining -= read; + } + + return; + } + + // No Range header: return full file + ctx.Response.StatusCode = StatusCodes.Status200OK; + ctx.Response.ContentType = "application/octet-stream"; + ctx.Response.ContentLength = totalLength; + + if (isHead) + { + return; + } + + // Stream the whole file + await using var fullFs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + await fullFs.CopyToAsync(ctx.Response.Body); + }); + + // Catch all + app.MapMethods("/{**catchAll}", ["GET", "POST", "PUT", "DELETE", "PATCH",], (string catchAll) => $"You requested: {catchAll}"); app.Run(); } diff --git a/src/Services/DataService.cs b/src/Services/DataService.cs index 3c34306..38354a6 100644 --- a/src/Services/DataService.cs +++ b/src/Services/DataService.cs @@ -6,13 +6,38 @@ namespace MariesWonderland.Services; public class DataService : Art.Framework.ApiNetwork.Grpc.Api.Data.DataService.DataServiceBase { + private const string LatestMasterDataVersion = "20240404193219"; + private const string UserDataBasePath = @"path\to\user\data"; + public override Task GetLatestMasterDataVersion(Empty request, ServerCallContext context) { - return Task.FromResult(new MasterDataGetLatestVersionResponse()); + return Task.FromResult(new MasterDataGetLatestVersionResponse + { + LatestMasterDataVersion = LatestMasterDataVersion + }); + } + + public override Task GetUserDataNameV2(Empty request, ServerCallContext context) + { + UserDataGetNameResponseV2 response = new(); + TableNameList tableNameList = new(); + tableNameList.TableName.AddRange(Directory.EnumerateFiles(UserDataBasePath, "*.json").Select(x => Path.GetFileNameWithoutExtension(x))); + response.TableNameList.Add(tableNameList); + + return Task.FromResult(response); } public override Task GetUserData(UserDataGetRequest request, ServerCallContext context) { - return Task.FromResult(new UserDataGetResponse()); + UserDataGetResponse response = new(); + + foreach (var tableName in request.TableName) + { + var filePath = Path.Combine(UserDataBasePath, tableName + ".json"); + var jsonContent = File.ReadAllText(filePath); + response.UserDataJson.Add(tableName, jsonContent); + } + + return Task.FromResult(response); } } diff --git a/src/Services/UserService.cs b/src/Services/UserService.cs index c577bef..fa1e0b6 100644 --- a/src/Services/UserService.cs +++ b/src/Services/UserService.cs @@ -22,7 +22,7 @@ public class UserService : Art.Framework.ApiNetwork.Grpc.Api.User.UserService.Us ExpireDatetime = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(30)), UserId = 1234567890123450000, SessionKey = "1234567890", - Signature = "V2UnbGxQbGF5QWdhaW5Tb21lZGF5TXJNb25zdGVyIQ==" + Signature = request.Signature }); }