mirror of
https://github.com/BillyCool/MariesWonderland.git
synced 2026-03-21 22:42:19 +01:00
Update hooks and server to get past asset and master db download, initial user data download
This commit is contained in:
109
frida/hooks.js
109
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 = '<err reading>'; }
|
||||
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"
|
||||
142
src/Program.cs
142
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) => $"<html><head><title>Terms of Service</title></head><body><h1>Terms of Service</h1><p>Language: {languagePath}</p><p>Version: ###123###</p></body></html>"); // 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) => $"<html><head><title>Terms of Service</title></head><body><h1>Terms of Service</h1><p>Language: {languagePath}</p><p>Version: ###123###</p></body></html>");
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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<MasterDataGetLatestVersionResponse> GetLatestMasterDataVersion(Empty request, ServerCallContext context)
|
||||
{
|
||||
return Task.FromResult(new MasterDataGetLatestVersionResponse());
|
||||
return Task.FromResult(new MasterDataGetLatestVersionResponse
|
||||
{
|
||||
LatestMasterDataVersion = LatestMasterDataVersion
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<UserDataGetNameResponseV2> 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<UserDataGetResponse> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user