-
-
Save Pldare/ebf704c752a8d77ff9603d4adfe54083 to your computer and use it in GitHub Desktop.
| using System; | |
| using System.Security.Cryptography; | |
| using System.IO; | |
| using System.IO.Compression; | |
| namespace Vroid | |
| { | |
| class Vroiddec | |
| { | |
| static void Main(string[] args) | |
| { | |
| using (FileStream fs = new FileStream(args[0],FileMode.Open)) { | |
| using (BinaryReader bs = new BinaryReader(fs)) { | |
| int buff_size=(int)(fs.Length)-48; | |
| RijndaelManaged rDel=new RijndaelManaged(); | |
| rDel.IV=bs.ReadBytes(16); | |
| rDel.Key=bs.ReadBytes(32); | |
| rDel.Mode=CipherMode.CBC; | |
| byte[] resultarray=rDel.CreateDecryptor().TransformFinalBlock(bs.ReadBytes(buff_size),0,buff_size); | |
| using(MemoryStream ms =new MemoryStream(resultarray)){ | |
| using(GZipStream gzs=new GZipStream(ms,CompressionMode.Decompress)){ | |
| using(FileStream df=new FileStream(args[0]+".dec",FileMode.OpenOrCreate,FileAccess.Write)) | |
| { | |
| int data; | |
| while((data=gzs.ReadByte())!=-1) | |
| { | |
| df.WriteByte((byte)data); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| log_msg("Done!"); | |
| } | |
| static void log_msg(string msg) | |
| { | |
| Console.WriteLine(msg); | |
| } | |
| } | |
| } |
| import requests | |
| import os | |
| import json | |
| import argparse | |
| USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/114.0" | |
| HOST = "https://hub.vroid.com" | |
| API_VERSION = "11" | |
| def download_model_from_vroid(model_id, subdir=None): | |
| #model_path_base = os.path.join( | |
| # subdir if subdir else args.directory, model_id) | |
| model_api_url = f"{HOST}/api/character_models/{model_id}" | |
| #print(model_api_url) | |
| return model_api_url | |
| def get_user_model_ids(user_id): | |
| model_ids = [] | |
| api_url = f"{HOST}/api/users/{user_id}/character_models?antisocial_or_hate_usage=&characterization_allowed_user=&corporate_commercial_use=&credit=&modification=&personal_commercial_use=&political_or_religious_usage=&redistribution=&sexual_expression=&violent_expression=" | |
| page_num = 1 | |
| while api_url: | |
| user_r = requests.get( | |
| api_url, headers={"User-Agent": USER_AGENT, "X-Api-Version": API_VERSION}) | |
| if not user_r.ok: | |
| print( | |
| f"[user:{user_id}:page:{page_num}] got bad response from vroid hub, {user_r.status_code}") | |
| break | |
| user_j = user_r.json() | |
| if "next" in user_j["_links"]: | |
| api_url = HOST + user_j["_links"]["next"]["href"] | |
| else: | |
| api_url = None | |
| for model in user_j["data"]: | |
| model_ids.append(model["id"]) | |
| print(f"[user:{user_id}] found {len(model_ids)} models") | |
| return model_ids | |
| def download_user_from_vroid(user_id): | |
| user_api_url = f"{HOST}/api/users/{user_id}" | |
| user_api_r = requests.get(user_api_url, headers={ | |
| "User-Agent": USER_AGENT, "X-Api-Version": API_VERSION}) | |
| if not user_api_r.ok: | |
| print( | |
| f"[user:{user_id}:api] got bad response from vroid hub, user might not exist, {user_api_r.status_code}") | |
| return | |
| user_api_j = user_api_r.json() | |
| username = user_api_j["data"]["user"]["name"] | |
| #user_base_path = os.path.join(args.directory, f"{username} ({user_id})") | |
| #if not os.path.isdir(user_base_path): | |
| # os.makedirs(user_base_path) | |
| #json_path = f"{user_base_path}.info.json" | |
| #if args.write_info_json: | |
| # with open(json_path, "w") as json_file: | |
| # json_file.write(json.dumps(user_api_j["data"])) | |
| # print(f"[user:{user_id}:api] wrote '{os.path.basename(json_path)}'") | |
| model_ids = get_user_model_ids(user_id) | |
| all_url=[] | |
| for model_id in model_ids: | |
| url=download_model_from_vroid(model_id)#, user_base_path) | |
| all_url.append(url) | |
| return all_url | |
| if __name__ == "__main__": | |
| #parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) | |
| #parser.add_argument("-d", "--directory", type=str, | |
| # help="save directory (defaults to current)", default=os.getcwd()) | |
| #args = parser.parse_args() | |
| #print(args.directory) | |
| #https://hub.vroid.com/en/users/36667771 | |
| download_user_from_vroid(36667771) |
| #use project https://github.com/Pldare/vrh-deobfuscator/tree/MatchAndBatch | |
| #use example | |
| #python dlhelp.py users_id or vroid_model_view_url | |
| import os,sys | |
| import re | |
| import checkusersmodel | |
| import time | |
| def dl_glb(url,sdir): | |
| print(url) | |
| os.system("node src/index2.js {}".format(url)) | |
| url=sys.argv[1] | |
| if "http" in url: | |
| dl_glb(url,"model") | |
| else: | |
| all_url=checkusersmodel.download_user_from_vroid(url) | |
| cou=len(all_url) | |
| idd=1 | |
| for gurl in all_url: | |
| print("{}/{}".format(idd,cou)) | |
| dl_glb(gurl,str(url)) | |
| #time.sleep(0) | |
| idd+=1 |
VRoid Hub obfuscates the mesh of the preview models. You can read more on this here: https://toon.link/blog/1740863435/borrowing-intellectual-property.
Nice post!
I checked the differences between the downloaded VRM and the VRM in the browser, and found that the vertex coordinate data had undergone some transformations
true dude, I hope this will shows how it works:
https://toon.link/blog/1740863435/borrowing-intellectual-property
Hello everyone, you can finally use this project https://github.com/Pldare/vrh-deobfuscator/tree/MatchAndBatch to download all or a single model from your user's collection (this was mostly made for research purposes). If you like vroid model, please support model author.
Fix Summary
Root cause: VRoid Hub updated their deobfuscation logic in two ways:
New timestamp 58245139 was added with seed value 9402684
Seed computation changed — all entries now use XOR (value ^ hashInt) instead of the old mixed approach (XOR for 4058237768, addition with overflow wrapping for others)
How I found it: Downloaded VRoid Hub's frontend JS, extracted webpack module 73648 (the seed map computation module), decoded its RC4-obfuscated string table, and read the actual seed values and computation formula directly from VRoid Hub's production code.
Changes to
src/index.js
:
Added 58245139: 9402684 to seedMapStartingState
Changed
computeSeedMap
to use XOR universally for all seed entries
New timestamp 58245139 was added with seed value 9402684
const seedMapStartingState = {
98756153: 74670526,
53816997: 38325553,
4058237768: 1289559305,
58245139: 9402684,
};
and a small update for XOR
const computeSeedMap = async (inputValue, url) => {
console.log("Computing seed map...");
if (url?.includes("s=op")) {
const apiVersionOffset = ["/v1/", "/v2/"].some((prefix) =>
url.includes(prefix),
)
? 6
: 5;
const path = url.split("/").slice(apiVersionOffset).join("/");
const hash = createHash("sha1");
hash.update(new TextEncoder().encode(path));
const hashBuffer = hash.digest().buffer;
const hashInt = new DataView(hashBuffer).getInt32(
hashBuffer.byteLength - 4,
true,
);
return Object.fromEntries(
Object.entries(seedMapStartingState).map(([key, value]) => [
key,
// VRoid Hub now uses XOR for all seed entries
value ^ hashInt,
]),
);
}
return Object.fromEntries(
Object.entries(seedMapStartingState).map(([key, value]) => [
key,
value + Number.parseInt(inputValue, 10),
]),
);
};
how to find the value for seed timestamp 2664362260 ?
any method to find values for new timestamps in future ?
I checked the differences between the downloaded VRM and the VRM in the browser, and found that the vertex coordinate data had undergone some transformations