A walk through the full stack of fud.su, a subscription crypter served as a website. The auth flow and the session internals, the credit system, the Bitcart crypto payments, the crypt builder, the chunk by chunk JavaScript load plan, an admin story told through a compiled but undeployed build artifact, and a complete API map.
This is the second entry in my Reversals series. Last time I went native and pulled apart an anticheat. This one is pure web. I spent an afternoon taking fud.su apart through a logging proxy, a handful of throwaway accounts, and a lot of pretty-printed JavaScript. The goal was to understand exactly what an online crypter actually is, how it bills you, and where the trust boundaries sit between the pretty page and the code that charges your card in bitcoin.
Quick note on scope and language before the details. A crypter is a tool that alters a Windows executable so antivirus products stop flagging it. Running a crypter as a service means someone built a website around that. Whatever you think about the customers, the engineering is worth studying, and the way these platforms get built tends to follow a pattern you can recognize anywhere. I registered a disposable account, clicked through the builder, and let the traffic talk. I did not pay anything, and nothing here coughs up a working free credits exploit. The interesting part was never going to be that. It was the gap between how the site presents itself and how the backend actually decides things.
What the site claims to beh2
fud.su presents a single product: upload a PE file, pick an option, get back a processed executable that your antivirus of choice will stop tripping over. The pricing model is credits. You buy credits with crypto, then spend them per build. A native x86 build costs one credit, a DLL sideload build costs two. The whole flow lives behind an account with an email, a password, and a CAPTCHA that turned out to be remarkably easy to beat with a pair of eyes.
The domain name tells you the intended market. FUD stands for fully undetectable, which is the working definition these operations advertise. The site tries to look like a polished SaaS product, and in a lot of ways it succeeds. There is a pricing page, a support page, an affiliate system, a transaction history, an API key manager, an FAQ, even a Nuxt based checkout modal rendered from a separate admin domain. Someone paid for real engineering time. That is part of why this is interesting. The people running this are not hobbyists scribbling in a script. It reads like a funded startup that happens to sell antivirus bypasses.
It is also, at the network level, a fairly ordinary small stack. Cloudflare sits in front. A Next.js front end serves the pages. A Python API behind it runs the accounts, the credits, and the crypting itself. And a self hosted Bitcart instance handles the crypto payments. Once you know the stack, the reversal is mostly about mapping which decisions the server makes versus which decisions it trusts the browser to make. That turns out to be the whole story.
First contact with the stackh2
I loaded the site with a fresh profile and a logging proxy in the middle at 127.0.0.1<8000>8000>. The first thing worth noting: every API response carries a Content Security Policy header, and it is a real one, not a default. The script-src allows self plus just one external host, and the connect-src list is where the interesting bits hide. Here is the header verbatim from the register call:
content-security-policy: default-src 'self' https://bitcart.fud.su https://fud.su; img-src 'self' data: https: *; script-src 'self' 'unsafe-inline' https://badmin.fud.su/; style-src 'self' 'unsafe-inline'; frame-src 'self' https://bitcart.fud.su https://fud.su https://badmin.fud.su/; connect-src 'self' * http://localhost:6969 https://bitcart.fud.su https://fud.suTwo clues jump out immediately. The admin bitcart host is badmin.fud.su, which means whoever built this runs a second Nuxt application just to render the payment modal. And the connect-src list contains http://localhost:6969. A development origin, sitting in the production CSP of a service that sells evasion. That on its own is a small confession: the image the site projects and the environment it grew up in are not far apart.
The page itself comes out of the Next.js app router. The initial HTML is thin, a hollow shell, and the real UI hydrates from chunked JavaScript in /_next/static/chunks. The user profile page, the credits page, and the crypt builder each ship their API calls inside their own bundle. That is convenient for us. The API surface is basically sitting on the public internet in readable form, and the page bundles tell you its entire shape without you having to guess.
The session cookie is the next tell. Fud uses a cookie literally named session, with the shape payload.signature. That two segment layout with a base64 payload and a dotted signature is the classic Starlette signed cookie format. The header on login gets set like this:
session=<payload>.<signature>; Expires=Fri, 09 Oct 2026 16:38:01 GMT; Secure; HttpOnly; Path=/; SameSite=LaxSeven day expiry, HttpOnly, Secure, Lax. I spent a little time decoding the payload, expecting JSON with a user id. It decoded to nothing useful. Thirty two opaque bytes with a leading 0x05, not UTF-8, not zlib, not a JWT. So the cookie is a signed handle rather than an inline session blob. The server holds the real state and this token just points at it. The front end is never in charge of its own identity, which is a refreshingly server-authoritative decision in a product that makes plenty of browser-authoritative ones elsewhere.
The JavaScript layer, chunk by chunkh2
Because the app router hydrates into a thin shell, the loader plan tells a story before a single request matters. Every page ships the same boot set first: the webpack runtime, the polyfills, two vendor blobs at roughly a hundred and seventy kilobytes each, a half kilobyte app stub, then the shared library set. That shared set is eight chunks, and the heavyweight in it is chunk 243 at around a hundred and thirty six kilobytes of motion and radix internals, which is how you know this site cares about how the buttons feel.
| chunk | size | role |
|---|---|---|
| runtime | 3.6 KB | loader map |
| polyfills | small | bootstrapping |
| 4bd1b696 | 169 KB | vendor |
| 493 | 169 KB | vendor |
| 358 | 0.5 KB | app entry |
| 243 | 136 KB | motion, radix |
| 239 | 28 KB | shared utilities |
| 289 | 62 KB | shared utilities |
| 260 | 10 KB | radix pieces |
| 506 | 8.6 KB | shared utilities |
| 292 | 14 KB | shared utilities |
| 806 | 25 KB | cards, chips |
| 943 | 11.6 KB | the auth core |
After the boot set comes the layout chunk, then the page chunk for the current route pulls in whatever it alone needs. Most routes add nothing.
| route | page chunk | extras |
|---|---|---|
| home | 974 | none |
| login | 520 | 111 card set |
| register | 454 | 111 card set |
| credits | 699 | none |
| crypt | 541 | 148 builder widgets |
| profile | 636 | none |
| support | 174 | none |
| new ticket | 486 | none |
| affiliate | 362 | none |
| news | 324 | none |
| faq | 505 | none |
Chunk 148 is seventy three kilobytes of builder specific widgets and shows up only on the crypt page. Chunk 111 is the ten kilobyte card set that keeps login and register light. News is the one route chunk that does not import the auth core directly, but it does not matter, because the layout header imports it on every page, so the auth machine boots everywhere regardless. There is no page here that is structurally separate from the API, not even the ones that look public.
Chunk 943 alone carries seven modules, and they are the entire identity machine in one dependency: the auth api client, the typed error object, the auth provider, the axios instance, the URL constants, the routing helpers, and the shared error helpers. Any page that imports the auth core gets all seven, which is why the whole app behaves like a single sign in no matter which chunk loaded first.
The layout chunk is where the site puts its face on every page, and it is more interesting than a menu deserves to be. The nav bar renders six links when you are signed in, Profile, Credits, Crypt, Affiliate, Support and News, and two of them are conditional in the code: Affiliate renders only when the affiliate flag is true, Admin only when the is_admin flag is true, so the admin entrance is drawn and then permission dressed around it. The logout button lives in the same bar. The error handling is a two tier boundary, which is an unusually telling detail. The application level boundary prints a card titled “Application Error” with no reset at all. Inside it, the page level boundary prints “Page Error” with a Try Again button that clears the error and reloads the page. The app restarts the page for small failures and refuses to for structural ones, which is the exact shape of a product that has suffered both kinds of production outage.
If you want the module level map instead of the story version, here is the compact table. Each row names a chunk and the run ids the webpack runtime registers inside it, and the numbered ids are the ones a reader can chase through the minified source with a search:
| chunk | module ids inside |
|---|---|
| 943 | 3193 auth api, 5106 error object, 5324 provider, 5725 axios, 7116 constants, 9568 routing, 9745 helpers |
| 806 | 3262 cn helper, 4270 card atoms, 5690 empty state, 5703 alert toast |
| 111 | 9746 icon atoms, card set exports BT Wu ZB Zp aR wL |
| credits | 2016 api client, 4988 page |
| crypt | 2078 api client, 8072 page |
| news | 8035 news api, 8831 date format, 2016 api client |
| support | 9586 page, 2016 api client |
| newticket | 8563 page, 1537 + 2478 atoms, 3850 captcha helper |
| affiliate | 4043 api client, 4145 page |
| profile | 6386 api client, 8687 page |
| register | 9115 page, 3850 captcha helper |
| login | 1943 page |
| faq | 7465 dataset, 9942 page |
| homepage | 3035 page, 7356 lander |
| layout | 3673 error boundary, 9073 nav |
Then there is the missing chunk. The webpack runtime served to every visitor maps chunk 378 to a file literally named admin-app.ca16cf51dcaad391.js. The file returns 404 on the public host, and the route /admin returns 404 as well. The build graph contains a compiled admin application, the string admin-app sits in the loader map of every page you open, and the asset is not served. That is archaeology, not a feature list, and it earns its own section below.
Reading the minutes, or how the bundle speaksh2
Everything above came out of files that look like the following, and since this is the craft as much as the result, here is the raw material plus the translation rules that turn it back into the readable code you have been reading. This is the actual register page bootstrap, a module that exists only to load the real page:
9115:(e,a,t)=>{Promise.resolve().then(t.bind(t,443))}And this is the helpers module that shows up on half the pages, export map first:
9745:(e,t,r)=>{r.d(t,{Tv:()=>a,fJ:()=>i,fK:()=>s,ok:()=>o,rj:()=>l})let a={validateStatus:e=>e<600};Five idioms carry the whole app, and once they are yours, every chunk opens:
- A module is a function of three arguments, e, t and r, shorthand for require, module and module.exports. Everything inside the function body is that module’s source.
- The declaration
r.d(t, { letter: () => value })publishes an export. The single letter, often A, bound to some function, is the entire surface. Renaming that letter to the real name, login or getBalance or processFile, is the whole game of reading exports. - Calls written as (0, z.tool)(args) are the comma operator. It means call tool with args and a clean this. The first letter is the module, the second is the export inside it, so (0,z.fK)(data, “Operation failed”) reads as “call formatError from the helpers module with this error body and a fallback string.”
- Promise.resolve().then(t.bind(t, 443)) is the dynamic import shim. It tells webpack to fetch module 443, the real page, asynchronously. Almost every chunk file you save is a trampoline that lands on the true page module a few modules down.
- The spread at the end of axios configs, …z.Tv, pulls the Tv export of the helpers module, which is one object and nothing else,
{ validateStatus: s => s < 600 }. Every API call is told to treat any status under six hundred as a normal response, which is why 402, 403 and 500 all arrive as data instead of thrown exceptions, and the client branches on status by hand. That single line is why the error branch code reads the way it does.
One more alphabet helps for module 9745 because it appears in every file: Tv is the axios config spread, fK formats an error message out of a Blob or an object, fJ builds the failure envelope, ok wraps a value in the success envelope, and rj turns a Blob back into JSON when the server answers an error request with a document instead of a body. Five letters, five jobs, every page importing the same five.
That is the whole toolkit. Everything else in this writeup is the same three steps applied over and over, find the module by number, rename its export letters by reading what they do, then read the strings as data. The minifier shrank names, it did not hide meaning, and nothing in these files was built to survive a reader with a text editor. The reversals you just read took shape in exactly that order.
The auth flowh2
The register endpoint is POST /api/auth/register with a JSON body that carries username, email, password, and a captcha response. Here is the exact request I made with the disposable account, with the personal bits scrubbed:
{ "username": "redacted", "email": "redacted@gmail.com", "password": "redacted", "captcha_response": "ad8xdk"}The response is a 201 Created:
{ "credits": 0, "message": "User created and logged in successfully", "role": "user", "username": "redacted"}Register and login are one call in that the server logs you in immediately and sets the session cookie. There is a separate remember_me flag on login:
{ "username": "redacted", "password": "redacted", "remember_me": true}The login response carries everything the client could want, including the birth date of your account, in one shot:
{ "created_at": "2026-10-02T16:38:00.849352", "credits": 0, "email": "redacted@gmail.com", "message": "Login successful", "role": "user", "username": "redacted"}There is a role field on every account object, and the profile page checks it directly to render an Admin badge. The API returns your role in the clear on every authenticated request. Nothing here screams privilege bug by itself, but field names like this are how you start a mental map of an admin API that we will never get to see. And it is worth stating the read only part plainly. The client never sets this field. The profile update call accepts exactly two fields, username and email, the register call accepts username, email, password, and captcha, and nothing else in the app proposes an authority field in any payload. The admin flag the page renders is derived, not stored. The auth provider computes is_admin as a strict string compare against the role the server returned on the last request. So from the browser’s point of view, role is a subscription to a broadcast, not a dial you turn.
Profile is GET /api/auth/profile, the same call the layout makes on every page load to decide whether you are signed in. Unauthenticated, it returns 401. Authenticated, it shows the full account:
{ "api_key_created_at": null, "created_at": "2026-10-02T16:38:00.849352", "credits": 0, "email": "redacted@gmail.com", "has_api_key": false, "role": "user", "username": "redacted"}The profile page also exposes password changing with a fairly strict policy, at least eight characters with upper, lower, digit, and special, and an API key manager. An existing key is masked on screen with only the first four and last four shown, and revoking it is a confirm() away. The API key endpoints follow a GET/POST/DELETE /api/auth/api-key pattern. With no key yet, a GET returns 404, which is how the client knows to show the generate button. The exact strings the page prints are worth keeping for their tone: “Generate and manage keys for programmatic access.”, “You don’t have an API key yet. Generate one to access the API programmatically.”, “Password and API access.”, and “Update your account password”, with a Change Password button sitting beside it.
The masking is a single expression, and the show/hide toggle around it is the whole UX:
let shown = key.length <= 8 ? "••••••••" : key.substring(0, 4) + "••••••••" + key.substring(key.length - 4);So a key becomes something like abcd••••••••wxyz, with eight bullets between the halves, and an eye button flips it to full when held. This is the same masking philosophy as the balance display: the browser is allowed to look, the lookup never leaves the page. The lifecycle around it is spelled in toasts, each one a sentence the support team would recognize: “API key generated successfully”, “API key revoked successfully”, and the errors “Failed to load API key information”, “Failed to generate API key”, “Failed to revoke API key”. Revocation is the one destructive act on the whole profile page, and it is wrapped in a window.confirm with a sentence that takes no prisoners: “Are you sure you want to revoke this API key? This action cannot be undone.”
The password change form mirrors it exactly. Three fields, current password, new password, confirm new password, a Change Password button, and a single success string, “Password changed successfully!”. The profile page is the quietest page in the app, which makes sense. It is the vault, and vaults do not need landing page copy.
The login path is where the server shows its teeth, and the error strings prove the API keeps real state. The client surfaces distinct server responses for an account that does not exist, an account typing a wrong password, an account that has been disabled, and an account that has been temporarily locked after too many failed attempts. That last one is a 429 with its own vocabulary, plus a generic rate limit message for everything around it. So login does have anti brute force tooling, which deserves credit. The mistake would be assuming the same applies to registration and to the coupon check, which show none of it.
The remember me checkbox defaults to off, and it decides how long the cookie lasts. When on, the cookie returns with that seven day Expires from the header up top. When off, the session is meant to die with the browser. Storing identity is not a client decision anywhere else, either. The axios instance runs with credentials attached, so the cookie rides along and the server decides who is who. There is no localStorage token in the whole application and no bearer string worth lifting.
The axios instance is worth quoting in full, because it is the single IO surface of the entire client. One base URL, credentials on, a JSON habit, and an interceptor that owns the session state from every page:
const api = axios.create({ baseURL: API_BASE, // API_BASE is the literal "/api/" from the constants module withCredentials: true, headers: { "Content-Type": "application/json", Accept: "application/json" },});api.interceptors.response.use( (r) => r, async (error) => { if (error.response && error.response.status === 401) { const path = window.location.pathname; const pub = ["/", "/login", "/register", "/forgot-password", "/news", "/faq"]; if (!pub.some((x) => path === x || path.startsWith(x + "/")) && path !== "/login") { window.location.href = "/login"; } } return Promise.reject(error); });The comment on the first line is mine. The rest is the minified file cleaned up. The interesting design choice is the whitelist prefix match: the interceptor matches both the exact public routes and anything under them, and it hard redirects with a full navigation rather than a state change, because a session that died is not a UI problem, it is a request for a fresh login.
The only browser side storage anywhere is a sessionStorage key called postLoginRedirect, used to carry someone from a payment modal to the login screen and then back to the credits page once they sign in. The login route also accepts a returnTo query parameter, but only when it starts with a single slash, a small sanitization pass that stops an odd little open redirect from forming.
The auth provider is strict about the public map. The whitelist of pages you may see signed out is home, login, register, forgot password, news, and faq. Any other route while signed out bounces you to /login with the destination remembered, with one deliberate exception, routes under /admin bounce to home instead, as if the app will not even confirm the admin page exists to someone without a session. Two details rhyme with that. /forgot-password sits in the whitelist but returns 404 on the running site, a stale entry for a page that has since been removed. And the axios interceptor, on any 401 outside the whitelist, yanks the whole tab to /login. The front end thinks in terms of sessions expiring mid flight, which matches the signed handle theory from earlier.
The captcha is anchored to the session, not to a separate challenge state. Register loads the image as a blob, and on a rejection the client prints guidance that captchas expire after one attempt or if your session changes. That means the text challenge is tied to the same server held session the cookie points at, which is exactly why a login lockout and an open registration wall can both be true. Same backend, different decisions.
The CAPTCHA deserves its own paragraph because it is the single weakest link in the auth funnel. Instead of hCaptcha or reCAPTCHA, fud runs a self hosted text CAPTCHA. GET /api/auth/captcha hands back a URL:
{ "captcha_url": "/auth/captcha-image?t=4006654235"}That URL returns an image of a short token, and you just type in what the picture says. No turing dance, no privacy joke with Google cookies, just a picture of text. I read it, typed it, and the account existed. From an abuse standpoint this matters a lot. Bot that solves text CAPTCHAs is an old solved problem, and with no visible rate limiting on registration or on the coupon check, a script can walk the pipeline all weekend. Login carries its own lockout logic, which we covered above. Registration, as far as the code and the traffic show, does not. The affiliate system even hands out coupon codes, which we will come back to.
The client helper that decides whether a submit error is captcha shaped is brutally simple, a regex and a pair of canned hints:
/captcha|verification code/ihint: "Refresh the captcha image, enter the new code, and try again. Captchas expire after one attempt or if your session changes."429: "Too many attempts. Please wait a moment before trying again."login page variant of the 429 hint: "Rate limits reset after a short waiting period."The regex matches the message the server sends, not a header or a state, which is why the same function works on registration and on the ticket form. It also means the copy lives in the client, so a redesign that reworded the server error would quietly break the pattern the helper depends on.
The auth client, and the server’s error vocabularyh2
Module 3193 is the auth api client, and it is worth quoting because it turns the HTTP status into a typed error object. The messages inside it are the server’s vocabulary, and two of them arrive as plain strings, “Account not found” and “Incorrect password”, which the client matches with a strict equality check before it decides what to show. The network layer below is deliberately quiet: any 400 range answer becomes a structured error object, a missing response becomes “No response received from server”, and a request that never left the machine becomes a network error. The login body is three fields, and one of them is a flag nobody on the marketing site mentions:
{ "username": "...", "password": "...", "remember_me": false }The builder for that object, cleaned up but strings verbatim:
function buildLoginError(status, data) { if (data.error && data.details) { return new AuthError({ title: data.error, detail: data.details }); } if (status === 429) { return data.error === "Account temporarily locked" ? new AuthError({ code: "ACCOUNT_LOCKED", title: "Account temporarily locked", detail: data.details || "Too many failed login attempts. Please try again in a few minutes.", hint: "This is a security measure to protect your account." }) : new AuthError({ code: "RATE_LIMITED", title: data.error || "Too many requests", detail: data.details || "Please wait a moment before trying again." }); } if (status === 403) { return new AuthError({ code: "ACCOUNT_DISABLED", title: "Account disabled", detail: "Your account has been disabled. Please contact support for assistance.", hint: "If you believe this is a mistake, please contact our support team." }); } if (status === 401) { if (data.error === "Account not found") { return new AuthError({ code: "ACCOUNT_NOT_FOUND", title: "Account not found", detail: "No account exists with this username." }); } if (data.error === "Incorrect password") { return new AuthError({ code: "INCORRECT_PASSWORD", title: "Incorrect password", detail: "The password you entered is incorrect." }); } return new AuthError({ code: "INVALID_CREDENTIALS", title: "Invalid credentials", detail: "Please check your username and password." }); } if (status === 400) { return new AuthError({ code: "INVALID_REQUEST", title: "Invalid request", detail: "Please enter both username and password." }); } return new AuthError({ title: data.error || "An unexpected error occurred", detail: "Please try again." });}The same short circuit that defeated the captcha regex lives here. The two most useful diagnostics, the wrong password and the missing account, are identified by comparing the server’s own sentence against fixed strings. If the backend ever reworded “Incorrect password”, every client log line that tells a user what went wrong would silently fall through to the generic invalid credentials branch. The front end types the server’s copy twice, once in the message it shows and once as the key it matches, and that doubling is where the whole error vocabulary is laid out for a reader.
The password policy, meanwhile, is enforced in the browser before the request is ever built, by a validator with an exact character class:
function validatePassword(password) { const upper = /[A-Z]/.test(password); const lower = /[a-z]/.test(password); const digit = /\d/.test(password); const special = /[!@#$%^&*(),.?":{}|<>]/.test(password); return password.length >= 8 && upper && lower && digit && special;}The special character set is worth keeping because it is the operator’s own definition of hard: a keyboard drawer full of symbols, no spaces allowed. When that check fails, the register page prints “Password must be at least 8 characters and include uppercase, lowercase, number, and special character.” The rest of the form validation is a row of short toasts, each one a gate: “Username is required”, “Email is required”, “Password is required”, “Passwords do not match”, and “Please complete the captcha verification”. The order is instructive, username first, email second, password third, captcha last, so a bot racing the form learns the server never sees anything until the picture is right.
The register submit then does something the marketing copy never advertises: it signs you in. A successful register call is immediately followed by a login call with the same credentials, and only then does the router push home. One form produces an account, a session cookie, and a signed in page, with no second round trip on the user’s part. The captcha image that protected all of it is kept as a blob URL in a ref, and when the page unmounts, if the URL still starts with blob:, it is revoked with URL.revokeObjectURL. Cleanup code that runs on a blob the user could have refreshed out from under you is the mark of a form that shipped with leaks and got patched.
The purchase talks to the login pageh2
The login page is also where the payment flow ends when you were not signed in. The Bitcart modal closes, the provider bounces you to a URL, and the login route reads a payment_status query parameter before it draws a single input. It is a small reconciliation layer, and both banner objects are bunded verbatim in the login page module:
let successBanner = { title: "Payment initiated", detail: "Please sign in to access your account. Your credits will be added after the payment is confirmed.", hint: "You'll be redirected to your credits page after signing in.",};let cancelledBanner = { title: "Payment cancelled", detail: "If you still wish to purchase credits, sign in and try again.",};On success it stores a redirect under a sessionStorage key and moves on. On cancel it just shows the banner. The key and the flow are small enough to hold in your head at once:
const KEY = "postLoginRedirect";if (payment_status === "success") { show(successBanner); sessionStorage.setItem(KEY, "/credits?payment_status=success&from_login=true");}if (payment_status === "cancelled") show(cancelledBanner);router.replace("/login" + (returnTo ? "?returnTo=" + encodeURIComponent(returnTo) : ""), { scroll: false });After a successful sign in, the redirect target is resolved in one expression: sessionStorage.getItem("postLoginRedirect") || returnTo || "/". The stored key is read and removed in the same breath, so the debt is settled exactly once. Buy while signed out, sign in, land on your credits page with the banner state preserved in the URL. That is a purchase loop that was shaped by real customers tripping over it.
The returnTo value that arrives on the URL is guarded before it is trusted, and the guard is a one liner worth framing:
function sanitizeReturnTo(search) { let s = search.get("returnTo"); return (!s || !s.startsWith("/") || s.startsWith("//")) ? null : s;}Paths must start with exactly one slash, which admits /credits and /history, and rejects //evil.example and anything scheme shaped. That is an open redirect check done right in four lines. The browser is only ever pushed back into the same origin.
The credits systemh2
Everything costs credits, so the credits page is the economic heart of the site. Its API client module is one tidy little bundle, and it maps cleanly to four endpoints.
GET /api/credits/balance returns the server side truth:
{ "credits": 0, "username": "redacted"}Note the username riding along. That is a small touch of defense, a way for the client to render the balance against the right account, and a pattern that says the developers have thought about multi-account weirdness at least once.
GET /api/credits/history?page=1&per_page=10 returns an empty ledger for a new account:
{ "current_page": 1, "pages": 0, "per_page": 10, "total": 0, "transactions": []}The transaction objects the UI knows about carry type, amount, status, a spent figure, and a coupon code. The columns on the page are Date, Type, Amount, Status, Cost, and Coupon. So the ledger is designed to record purchases, usage, and refunds, with the coupon that an affiliate generated saved for later.
GET /api/credits/packages is the pricing page. This is where the business model gets concrete:
{ "packages": [ { "id": 1, "name": "Starter Pack", "credits": 1, "price": 30 }, { "id": 2, "name": "Popular Pack", "credits": 10, "price": 270 }, { "id": 3, "name": "Pro Pack", "credits": 50, "price": 1200 } ]}Three tiers. One credit for thirty dollars, ten for two seventy, fifty for twelve hundred. The effective rate is thirty, then twenty seven, then twenty four dollars per credit. So a single x86 build is thirty dollars and a DLL sideload build runs sixty. That is the price of undetectability in 2026, apparently. There is no bonus field in the response, even though the credits page JavaScript knows how to render a bonus row. The packages are just flat credit amounts, so the bonus rendering path is dead code waiting for a bigger sale.
POST /api/credits/validate-coupon takes one field:
{ "code": "BUYME" }A random code produced this response, clean and fast:
{ "error": "Invalid coupon code" }Status 400 for a bad code. The coupon object the success path expects has a discount type, percentage or fixed, a discount value, and an optional minimum purchase amount. The affiliate endpoint, GET /api/affiliate/status, drives the whole coupon economy:
{ "coupon_count": 0, "is_affiliate": false, "total_earnings": 0.0}An affiliate generates coupon codes, hands them out, and earns a cut of what those coupons buy. Do not miss what a coupon endpoint without a lockout means: it is a guessable oracle. You can POST codes until one comes back 200 with a discount attached. Affiliate coupons in the wild are short stamps, usually things like VIP or 10OFF patterns, and the endpoint will quietly tell you when you got one right. We never hammered it, and nothing here is proof of a working exploit, but the surface exists and it is completely unprotected.
The client also ships the preview math, a small function that shows what a code would do before the server has said a word:
function discounted(amount, coupon) { if (!coupon) return amount; if (coupon.min_purchase_amount && amount < coupon.min_purchase_amount) return amount; if (coupon.discount_type === "percentage") return amount * (1 - coupon.discount_value / 100); return Math.max(0, amount - coupon.discount_value);}The interesting thing about this function is what it is not. It is not the authority. The same coupon is revalidated at invoice creation, server side, and the invoice response carries its own final_cost and savings. The client preview exists so the checkout page can paint a number before the server commits, and the server ignores the client’s math entirely if the two disagree. That is the same division of labor as the credit balance: the browser may preview, the backend may decide.
The api client behind all four of those calls is module 2729, and it is compact enough to quote whole. The interesting number is the purchase timeout. The Bitcart config hinted at a thirty second default and a full minute for payment, but here the purchase call carries its own private clock of forty five seconds, with a timeout message that a real user must have seen often enough to earn its own string:
let creditsApi = { getBalance: async () => (await api.get("/credits/balance")).data, getHistory: async (page = 1, per_page = 10) => (await api.get("/credits/history", { params: { page, per_page } })).data, getPackages: async () => (await api.get("/credits/packages")).data, purchaseCredits: async (amount, opts = {}) => { try { let body = { amount, ...opts }; let res = await api.post("/credits/purchase", body, { timeout: 45000, timeoutErrorMessage: "Payment provider took too long to respond. Please try again.", }); if (res.status >= 400) return { success: false, error: "Failed to process purchase", status: res.status }; if (res.data.success) { return { success: true, message: res.data.message, amount: res.data.amount, credits_added: res.data.credits_added, new_balance: res.data.new_balance, savings: res.data.savings, }; } return res.data; } catch (e) { return { success: false, error: e.message || "Failed to process purchase. Please try again later." }; } }, validateCoupon: async (code) => { let res = await api.post("/credits/validate-coupon", { code }); if (res.status >= 400) return { error: "Failed to validate coupon" }; return res.data; },};Three clocks, then, not two. The modal script gets ten seconds, ordinary api calls live on thirty, the purchase call sits at forty five, and the payment state inside the modal earns the full minute. The purchase response also names the ledger fields the page will paint, credits_added, new_balance and savings, so the discount a coupon produces arrives as a first class number, not a deduction the client infers. Overriding the amount field buys nothing here, because the same client that sends the number never reads it back with authority; it takes the server’s new_balance and repaints.
The purchase and the Bitcart checkouth2
Buying credits is the part of the flow that touches real money, so I clicked all the way through it to see the handoff. The client hits POST /api/credits/purchase with the credit amount and an optional coupon code:
{ "amount": 1}The response is a small gem, an invoice ready to show:
{ "final_cost": 30, "invoice_id": "01M3YQY4AHF5FADJVXYQQS4F1T", "original_cost": 30, "savings": 0}Three observations. The amount you send is a credit count, not a dollar figure. The server looks up the package, prices it from its own table, and returns the cost. And the invoice id is a ULID, the exact format Bitcart uses when you create an invoice through its API. So the fud backend is a thin wallet that mints Bitcart invoices server side. The browser never tells the server how much anything costs. If you POST amount 50, you get the Pro pack price, twelve hundred, not thirty times fifty. Overriding the amount field does not change the price, because the server owns the price table. That single fact is half the answer to the override question, and we will come back to it.
The checkout itself runs from the admin domain. The credits page loads https://badmin.fud.su/modal/bitcart.js, an async script, and then opens an invoice UI at https://badmin.fud.su/i/<invoice_id>?modal=true. Inside that modal the browser calls the Bitcart API on bitcart.fud.su:
GET /invoices/<invoice_id>to fetch the invoice stateGET /stores/<store_id>to pull store config, and yes the store id is out there in plain trafficwss://bitcart.fud.su/ws/invoices/<invoice_id>a live websocket that pushes payment statusGET /cryptos/tokens/btc/abifor the token list, which for a new invoice means bitcoin
The loader itself is a one object configuration plus one function, and it is worth reading because it names the admin host and the timeouts in the same breath:
const bitcart = { bitcartAdminUrl: "https://badmin.fud.su", bitcartScripts: { modal: { path: "/modal/bitcart.js" } }, apiTimeouts: { default: 30000, payment: 60000 },};function loadBitcartScript() { const old = document.getElementById("bitcart-script"); if (old) old.remove(); const s = document.createElement("script"); s.id = "bitcart-script"; s.async = true; s.src = "https://badmin.fud.su/modal/bitcart.js"; document.body.appendChild(s);}Ten seconds after injection, if the script has not loaded, the page gives up and offers a Retry Loading button. The two apiTimeouts are the thirty seconds and the full minute you already read about, and the checkout is the only caller that earns the full minute while everything else lives on the shorter clock. Every payment attempt rebuilds the script tag from scratch, which is a small reload pattern that tells you the modal had a habit of wedging in production.
The failure strings are even plainer than the numbers. The load helper builds one promise, arms a ten second timer against it, and names the timer inside the error message itself:
const timer = setTimeout(() => reject(Error("BitCart script loading timed out after 10 seconds")), 1e4);tag.onerror = () => { clearTimeout(timer); reject(Error("Failed to load BitCart script from " + tag.src)); };Then it waits half a second after the tag reports success, a settle window, and only then inspects the global the script was supposed to create, with a distinct failure string for each way it can be missing:
if (!window.bitcart) throw Error("BitCart script loaded but window.bitcart is not defined. This may be a temporary issue.");if (typeof window.bitcart.showInvoice !== "function") throw Error("BitCart script loaded but showInvoice function is missing");Two separate messages for two separate failure modes, one where nothing landed and one where the object landed without its checkout method. On any of these the page retries on a two second cadence, and when the attempts run out it prints the terminal line, “Failed to load payment system. Please try again later.” Those strings were written the way they are because the modal has failed in every one of these ways in front of real customers. A service that ships four distinct payment error messages has been burned by payments more than once.
The purchase POST is where the money versions of those errors live, and each branch is committed to its own string. A 429 is the rate limit, and throttled attempts are told to wait:
if (res.status === 429) throw "Too many purchase attempts. Please wait a moment before trying again.";if (res.status >= 500) throw "Server error while processing payment. Please try again later.";throw res.error || "Failed to process purchase. Please try again later.";The page reads payment_status from the URL the login flow left behind, and the wording shifts by way of entry. Arrive straight from the modal and you get “Payment was cancelled. No credits will be added to your account.” Return through the login screen, which stamped its own redirect first, and the same cancelled event reads “Your previous payment was cancelled. No credits will be added to your account.” The success side splits the same way, “Your payment was initiated successfully! Your credits will be added to your account after the payment is confirmed.” on the direct path and “Your previous payment was initiated successfully!” through login, and whenever the server omits a message the client falls back to the template "N credits have been added to your account."
So the flow is classic Bitcart: the main site creates an invoice, the browser opens a hosted checkout, the webhook or websocket tells the backend when crypto clears, and the server credits the account. When the modal closes, the page re-fetches balance and history, and the URL can carry ?status=success or ?status=cancelled back to the credits page so the UI knows what happened. It is a clean, standard payment handoff. Nothing about the payments part is improvised. The crypto rails under it are the same ones every dark web shop trusts.
The refresh after the modal closes is not instant, and the timing is a small fingerprint of its own. The page stamps the moment the modal opened, and when the modal leaves it compares: if it stayed open under ten seconds, wait three seconds before re-pulling balance and history, otherwise wait one and a half. That split exists because fast closes still need the payment callback to settle. On a paid return the page then smooth-scrolls to the transaction history row and rewrites the URL with history.replaceState to drop the payment_status query, so the banner state does not survive a refresh. Those little numbers are soldered in for a reason, users in the real world pushed the flow until it behaved.
There is one more habit worth flagging in this flow: the payment timeout. The axios instance carries two timeouts, thirty seconds for the general API and a full minute for payment, and the page labels the first failure as “Payment provider took too long to respond.” The Bitcart script itself loads under a ten second timeout, and when a payment error does surface, the page renders a Retry Loading button instead of a dead end. What that means operationally is that the site has already trained its user base to treat payment stalls as a coin flip? No. It means the developers wrote real error handling, which tells you payments have broken in production enough times to hurt. Painful experience, made visible in the client code, is the most reliable fingerprint a reverser gets.
The affiliate dashboardh2
The affiliate area is the other half of the coupon economy, and its client is the shortest of the lot. On load it asks one thing, GET /api/affiliate/status, and only if the answer says is_affiliate does it bother with the other two calls, coupons and earnings, in that order. The errors are polite and specific: “Unable to verify affiliate status. Please try again later.”, “Could not load your affiliate coupons. Please try again later.”, and a console error for a failed earnings pull. So a non affiliate is a silent guest, the page renders an empty state rather than an error, and the affiliate flag on the account is the single switch that unlocks the whole revenue share screen.
The dashboard paints three numbers: Total Earnings in dollars with toFixed(2), Pending Earnings read from the earnings response’s summary.earnings_by_status.pending, and Your Discount Code, which is simply the code of the first coupon in the list. Renaming a coupon is a PUT to /affiliate/coupons/<id> with a new code, and the client enforces the shape before the request leaves the page, in the exact words you will see in the field:
let code = input.trim().toUpperCase();if (!code) throw "Coupon code is required";if (code.length < 3) throw "Coupon code must be at least 3 characters long";if (!/^[A-Z0-9_-]+$/.test(code)) throw "Coupon code can only contain letters, numbers, hyphens, and underscores";await api.put("/affiliate/coupons/" + coupon.id, { code });Three gates in a row, uppercased before send, and the last one is the pattern that matters. Allowed letters, digits, hyphen, underscore, nothing else. The same regex that guards the coupon field is the shape every code in the wild obeys, VIP, 10OFF, and the rest of the short stamps, which is why the brute force story from earlier holds together. The codes are human sized, the endpoint has no lockout, and the client even tells you the naming convention for free. A successful rename prints “Coupon updated successfully”.
The crypt builderh2
Now to the actual product. The crypt page is a single page app with four modes:
| option | description | cost |
|---|---|---|
| standard | x86 native executable | 1 credit |
| standard64 | x64 native executable | 1 credit |
| sideloading | x86 DLL sideloading | 2 credits |
| sideloading64 | x64 DLL sideloading | 2 credits |
GET /api/crypt/options returns them verbatim:
{ "options": [ { "option": "standard", "description": "x86 Native Executable", "credits_required": 1 }, { "option": "sideloading", "description": "x86 DLL Sideloading", "credits_required": 2 }, { "option": "standard64", "description": "x64 Native Executable", "credits_required": 1 }, { "option": "sideloading64","description": "x64 DLL Sideloading", "credits_required": 2 } ]}The other builder endpoint, GET /api/crypt/programs?architecture=x86, feeds the DLL sideload picker, and it takes the architecture as a plain query value, x86 or x64, with x86 as the page default. Sideloading needs a signed host program from a curated list, and the client renders that list from the response with one icon per program. Each entry is a two field object, a Program label and an icon_path:
{ "Program": "...", "icon_path": "..." }The page never trusts the full icon path. It takes the last path segment off whatever the server returned and re-serves it from /assets/icons/ on the main origin, so the product visuals and the homepage AV set under /icons/av/ live in two separate asset trees. The mount effect fires the calls in a strict order: balance first, but only when signed in, then the options endpoint unconditionally, then the program list for the currently selected architecture. The mode to option mapping in the code does the rest, standard becomes standard64 and sideloading becomes sideloading64, so the four options and the programs list are two views of the same table.
The real endpoint is POST /api/crypt/process. It is multipart, the kind of request that uploads a file plus a stack of fields:
file the PE you want processedoption standard, standard64, sideloading, sideloading64is_web_ui always true from the pageicon optional image uploadversion_info optional JSON of version metadatainstall_path optional, %appdata%\\<random>\\<random>.exeelevated_install_path optional, %programfiles%\\<random>\\<random>.exeinjection_target optional, svchost.exeantivm booleanadmin booleanselected_program required for sideloading, the host appThe client assembles this with FormData and a header of Content-Type: multipart/form-data, and it asks for the response as a blob.
And the page stamps the difference between its own door and the API door right in the payload. is_web_ui is always literally “true” when the web client builds the form, and the curl generator, which we will get to, never writes that field. The server can tell a browser build from an API build without asking, and it clearly wants to. antivm and admin go out as booleans, version_info as a JSON string, and the rest as plain text. A multipart request is just a form with an attitude, but this one carries flags the marketing site would rather not print.
The submit handler that builds that form is short, and its exact order is the wire contract:
let form = new FormData();form.append("file", file);form.append("option", option);form.append("is_web_ui", "true");if (icon) form.append("icon", icon);if (Object.values(versionInfo).some((v) => v !== "")) { form.append("version_info", JSON.stringify(versionInfo));}if (state.install_path) form.append("install_path", state.install_path);if (state.elevated_install_path) form.append("elevated_install_path", state.elevated_install_path);if (state.injection_target) form.append("injection_target", state.injection_target);form.append("antivm", state.antivm || false);form.append("admin", state.admin || false);if (isSideload && selectedProgram) form.append("selected_program", selectedProgram);Three readings fall out of it. icon and version_info only board the request when they are actually set, so the bare build sends none of the extras. antivm and admin ride along unconditionally as booleans, false included, which means the server sees the flag even when the box is off. And selected_program only appears on sideload builds, so the architecture pick and the host choice are inseparable in the upload. Before any of this is handed to the network there are two front gates, “Please select a file” and “Please select an operation”, then the price check, which reads the option object’s own price:
let required = option.credits_required || 1;if (balance !== null && balance < required) { toast("You need at least " + required + " credit(s) to process a file with this option. Please purchase more credits.");}The credit requirement lives on the option object the server returns, so a new mode with a different price, or a sale, is just a server edit. The toast is the client’s last word before the POST.
The feature set around it is the full crypter vocabulary:
- Anti VM protection. A checkbox that wraps the build in VM detection.
- Startup persistence. A checkbox that strands an install path in %appdata% and an elevated path in %programfiles%, each one a randomized ten and eight character folder and file name. The site even ships a randomize button that generates a new path.
- Admin privileges. Requests elevation at run time, with a warning that people will see UAC dialogs.
- Process injection. A picker with svchost.exe, notepad.exe, calc.exe, and dialer.exe as hosts, so the payload runs inside a signed system process.
- Icon replacement. Drag a PNG in, it becomes the output icon.
- Version info override. Company name, product name, file version strings and more, all editable, all set to a generic template by default that makes every first build look exactly alike.
That default template deserves to be quoted, because it is the fingerprint of a careless first build. Everyone who clicks through without touching the fields gets the same eight values, including a copyright line that still says 2025:
{ "FileVersion": "1.0.0.0", "ProductVersion": "1.0.0.0", "CompanyName": "My Company", "FileDescription": "My Application", "ProductName": "My Product", "LegalCopyright": "Copyright \u00a9 2025", "OriginalFilename": "app.exe", "InternalName": "My Application"}The identifier field, OriginalFilename, defaults to app.exe even when you upload something called my-tool.exe. The build pipeline leaves the default template’s name in the output version resource unless the customer edits it, which means the very first unedited build from this service is byte identical across every customer. If you ever see that version resource in the wild, you know exactly which product made it and that the builder ran with its defaults.
The install paths are generated the same way every time, from a pair of functions that deserve their own line because they show the shape of the leak. A random string builder over a sixty two character alphabet, then two paths that sandwich ten and eight of those letters into a Windows jail:
function rand(len = 8) { let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let out = ""; for (let i = 0; i < len; i++) out += chars.charAt(Math.floor(Math.random() * chars.length)); return out;}function installPath() { return "%appdata%\\" + rand(10) + "\\" + rand(8) + ".exe"; }function elevatedPath() { return "%programfiles%\\" + rand(10) + "\\" + rand(8) + ".exe"; }Ten characters for the folder, eight for the file, no dots, no specials, so a typical path reads %appdata%\klJd9xP2a1\aIb71KzQ.exe. The paths are regenerated, not stable: after every successful build the page rolls fresh values into both fields if they are enabled, so the artifact does not reuse the install location shown on the previous screenshot. The randomize button does the same on demand, and when someone toggles installation on, the injection target is preset to a single canonical value, svchost.exe. Nothing here is random about the strategy. A folder that looks like machine noise every time is the entire point, and the alphabet choice says the authors wanted no hyphen in a directory name that might land in a quoting bug, no underscore, no dash, just letters and digits that stay legal in every shell.
Each controller on that page is a React hook over simple state, and the bundle is minified but entirely readable. There is no obfuscation worth the word. The names survive, the endpoint paths survive, the error strings survive. Whatever money went into the backend, the front end wears everything on its sleeve.
The page will also write out a complete curl command for the current settings, and it is the API contract in raw form. The URL is not typed by hand. A small client helper joins the path onto the axios base, so the target ends up on the main origin at /api/crypt/process, the same host the web app lives on. The auth header is X-API-Key with the placeholder YOUR_API_KEY, and when a signed in user has a real key the builder fetches it through the same GET /api/auth/api-key endpoint and substitutes it before copying to the clipboard. The flags match the multipart table you just read, plus selected_program for sideload builds and the same result.exe versus result.zip split at the end. One copy button, the whole API.
The copy button pulls from an embedded API reference that lives on the page itself, an accordion with its own headings and its own plain language. The strings are bundled like any other copy, and the reader is told the contract in five sentences, the operation guide, the usage heading, the error code list, the response shape, and the pricing:
{ title: "How crypting works and API access.", usage: "Usage Information", codes: "Common Error Codes:", response: "The API returns the processed executable in the response body.", cost: "Each successful request deducts credits based on the selected option.", guide: "Select an operation above to generate the curl command for your current settings.", missing: "Select an operation before copying the API command.",}That last string is the copy guard in action. No operation selected, no clipboard write, and the tooltip is the refusal speech. The reference panel is the site explaining its own surface in the same calm tone reserved for paying customers, and it is printed on the same page that sells the eradication of analysis.
The response handling is where the second half of the override answer lives, and also the most interesting single line in the codebase. Native options return the processed executable as the response body. Sideload options return a password protected ZIP, and the password comes back in a response header. Literally.
X-File-Password: <the zip password, in plaintext>The client reads the header, shows the password in a dialog, and offers a copy button. The response object the page actually consumes is assembled in the api client around that header, and the password never travels through JSON at all:
let res = await axios.post("/crypt/process", formData, { responseType: "blob", headers: { "Content-Type": "multipart/form-data" },});let password = null;if (res.headers) { password = res.headers["x-file-password"] || res.headers["X-File-Password"] || null;}return ok({ data: res.data, password });The body comes back as a blob because of responseType, the password comes back as a header, and the ok helper merely wraps the pair with a success flag, so the caller receives { success: true, data: { data: <blob>, password: <string or null> } }. The header name is checked in exactly two spellings, the lowercase one and the title case one, and nothing else. From that one object the page decides the whole download shape, if the password string exists the build is a protected ZIP, if it is null it is the bare executable. So the password concession the service makes is not a boundary, it is a courtesy wrapper, one more accessor in the chain. If the point was to keep the output from leaking in transit, a header is the weakest possible choice. If the point was to make the download feel protected to the customer, it does the job. The gap between those two intentions is the whole site in miniature.
Downloads get names too. Native builds come back as crypt_<timestamp>.<original-ext>, sideload builds as protected_<timestamp>.zip. After every successful build the client happily subtracts the credit cost from the local balance display and re-pulls the profile. The server deducts credits too, on the same request, via the 402 path.
The error codes, or the server is the bill collectorh2
The client code is explicit about what the server can come back with, and it reads like a billing system’s list of grudges:
401 Authentication failed (invalid or missing API key)402 Insufficient credits400 Invalid request (missing parameters or invalid format)403 Operation not allowed500 Server error or operation execution failedThe web client branches them for the user, and the branching tells you which ones have actually happened. 402 renders “Not enough credits.”, 403 renders “Operation not allowed.”, 400 renders the raw server error as an invalid request, and any 5xx whose message mentions architecture swaps the whole error row for a dialog with the supported file types spelled out, x86 executables, x64 executables, .NET assemblies, plus advice to pick the right architecture or recompile. A 401 never renders as a build error at all, because the axios interceptor grabs it first and bounces the whole tab to /login. The crypt endpoint does not trust your session badge. The instant the session is gone, you are on the login page.
That architecture dialog is fully assembled in the bundle, right down to the headings and bullets, and it is worth quoting because it is the most honest piece of documentation the site ships:
Supported file types: x86 (32-bit) Windows executables (.exe) x64 (64-bit) Windows executables (.exe) .NET assemblies (.exe, .dll)
What you can do: Select the correct architecture option (x86 or x64) for your file Use a different version of your application if available Recompile your .NET application for the correct architecture Check if your file is actually a valid PE file
How to fix this: Select the correct architecture option that matches your file For x86 files: Use "x86 Native Executable" or "x86 DLL Sideloading" ...Two headings exist in the code path, “What you can do” and “How to fix this”, and the renderer picks one based on which server message the request hit, a generic architecture complaint or the literal string “Architecture mismatch”. The advice is competent enough that a stranger could repair the build without the site at all, which is not usually how the support dialog of a paid product reads. A free diagnosis, delivered at the exact moment the money gate rejects you.
Two client side gates protect the door before the server ever sees a byte. The first is size, checked before the upload even registers:
if (file.size > 50 * 1024 * 1024) { setError("File is too large. Maximum size is 50MB. Your file is " + (file.size / 1048576).toFixed(2) + "MB."); resetFileInput();}Fifty megabytes, hard capped in the browser, and the complaint is measured to two decimals whether the server would have accepted the file or not. The second gate routes the architecture family. Two server messages, the literal string “Unsupported architecture” or the lowercase pair “architecture detected”, flip the error row into the full help dialog you just read, and anything else renders as the plain error body:
if (message.includes("Unsupported architecture") || message.includes("architecture detected")) { showArchitectureHelp(message);}A 50MB ceiling and an architecture family check, both client side, both free to bypass with the same curl the page writes for you in the next section. The web form is a polite lobby, and the API is the express lane with the same credit meter at the other end.
That 402 in the middle is the load bearing wall of this entire business. Your crypt does not happen until the server has taken its credits. The UI disables the button when your balance is short, but the button is decoration. The server is where the money is taken, and it takes it before any processing starts. This matters hugely for the override story, so let’s walk it properly.
The client is a thin lobby. It checks credits_required against the balance and shows a red “Low credit balance” banner. It refuses to build when you are broke, and it optimistically decrements the shown balance after every successful run. But none of that is binding. The binding check is server side, and it is an HTTP status.
Someone pointed a real deal at that 402 and it held. Overriding the amount in a purchase does not change the invoice because the server owns the price table. Overriding a coupon value client side does nothing because the server revalidates the code at invoice creation. Overriding the local balance display makes the button light up and buys you nothing, because the server still answers 402 when you POST a build with zero credits in the database. Every visible gate in this application is cosmetic, and the fact that the sites and their customers believe in them anyway is exactly why they survive.
The admin that is and is not thereh2
The role question gets a literal answer from three angles. The account object always carries a role. The profile page renders an Admin badge when the client sees the string admin. And the nav bar draws an Admin link only when the derived is_admin flag is true. None of those are entrances. No request body written anywhere in the client sets a role. The profile update takes username and email only, the register call takes username, email, password, and captcha, and nothing else proposes an authority field in any payload.
The deeper tell is the phantom build artifact. The webpack runtime shipped on every page maps chunk 378 to a file literally called admin-app, and that file returns 404 on the public host, just like the /admin route. An admin side was compiled, and then it was taken out of public reach. What stays visible is a rim of admin flavored endpoints the public client itself knows about and, in two cases, calls. The news module ships a complete create, update, delete set plus a dedicated GET /api/news/admin listing. The affiliate program is invite only, and payouts move through a human reviewed ticket with its own payout category. So the Python API behind all of this almost certainly carries an admin facet of its own, behind that rim, on the same backend the compiled admin app was built against. The browser never sees it, the public host never serves it, and probing it is the one thing this writeup refuses to do.
The honest reading is a good one for the site. Whatever lives behind the missing admin page, the decision about who may look is made server side, and every visitor starts at role user. The role is not an override target from the browser because the browser never proposes it. The attack that would matter is not sending a different role past a form. It is being handed one by a backend mistake, and nothing in the captured traffic suggests that mistake exists.
Where the trust actually livesh2
I want to be precise now, because this is the part people usually get wrong. A “hack the credits” story only exists where the client makes a binding decision and the server trusts it. That is the classic mistake: the server believes the price you typed, or the balance you showed, or the coupon you pasted. Fud does not make that mistake at the price layer. The server answers every purchase with its own cost, mints its own invoice, and only credits the account when the invoice clears. That is a payment rail that would survive most weekend attempts.
In three specific places, though, the client does make decisions that the backend then trusts, and those are the seams worth studying honestly.
First, the coupon oracle. POST /api/credits/validate-coupon returns a clean 400 with a normal JSON body for a bad code and, on the success path, the full discount. There is no lockout header, no throttle that we could see, no CAPTCHA on the coupons page. Affiliate coupon codes are human sized strings. Any idiot with curl, or a bored cron job, can grind codes. That is a discount theft path, and it is a real one. Getting a solid 30% off coupon into your account from a leaked or brute forced affiliate code is “overriding” the economics without touching a single invoice.
Second, registration volume. The CAPTCHA is an image of text, solvable by any OCR that has existed for a decade, and every account ships with the affiliate endpoint attached. Rinse and repeat a few thousand registrations and you have a coupon farm with a built-in distribution channel. The attackers here do not need admin. They need patience.
Third, the localhost leak in connect-src. connect-src ... http://localhost:6969 ... means the product CSP is telling the browser it is normal to connect to a development server on the operator’s own machine. Combined with any front end injection, and the whole reverse side is the audience, that origin becomes a foot in the door that should never have shipped. We never tried to exploit it, and we had no reason to. But a production CSP that opens connections to localhost is the sort of confession one usually finds in an autopsy, not a running service.
Fourth, the password in the header. The sideload ZIP password travels as X-File-Password on the process response. Any proxy, any CDN log, any browser extension, any HTTPS inspection, any future Acme coupon farmer gets the output key for free. The password is the entire “protection” on that ZIP, and the header is its shipping crate. It is not a vulnerability the server can quietly ignore. It is a policy decision made at the wrong layer.
What I could not change, and what I refuse to proveh2
I never had enough credits to run a live build, so the crypt response stayed a bundle signed promise rather than a captured blob. The sideload header behavior is read from the client code, which decodes it explicitly, and the browser would have shown the password dialog, but I did not pay money to watch a check happen. Similarly, the coupon brute force is real and visible, but I did not grind live codes against a production service. Nothing here is a proof of concept against the running site. It is a map of where the map is weakest, drawn from the code and the traffic, left pointing at the seams without handing anyone a key.
And that is the honest line. A blog that claims to have eaten the site on the way in would be a blog that tried, and trying costs you nothing when you have already been told the answer lives server side. The correct output of a reversal like this is not a bug report with a signature. It is a model of the machine, labeled, with the failure modes called out by name.
The API map, front to backh2
Time for the complete inventory. The axios base is /api/, so every relative path in the bundles resolves against the main origin with that prefix. Public endpoints need nothing. Protected endpoints need the session cookie. The crypt endpoint, usefully, has a second door, a scripted caller may pass an X-API-Key header instead, and the site itself writes out that curl for you, which we will see in a moment.
The URL resolver is the reason every relative endpoint lands on the main origin instead of floating relative to whatever page you are on, and it is the whole path grammar in one function:
let API_BASE = "/api/";let BADMIN_URL = "https://badmin.fud.su";function join(path) { const absolute = /^https?:\/\//i.test(path) ? path : API_BASE.replace(/\/$/, "") + "/" + String(path).replace(/^\//, ""); return /^https?:\/\//i.test(absolute) ? absolute : window.location.origin + (absolute.startsWith("/") ? absolute : "/" + absolute);}Two rules fall out of it. Relative strings like crypt/process are joined onto /api/ and then onto the page origin, so the whole public api lives on fud.su itself. Anything that already starts with a scheme passes straight through untouched, which is the doorway the badmin modal and any future absolute URL walk through. A single function decides what stays on the origin and what is allowed to leave it, which is exactly the sort of helper worth reading when you audit where the browser is allowed to point.
Auth
| method | path | what it does |
|---|---|---|
| POST | /api/auth/register | create an account, log in, set the cookie |
| POST | /api/auth/login | start a session, remember_me flag |
| POST | /api/auth/logout | end the session |
| GET | /api/auth/profile | the account object, 401 when signed out |
| PUT | /api/auth/profile | username and email only |
| POST | /api/auth/change-password | current and new password |
| GET | /api/auth/api-key | the current key, 404 when empty |
| POST | /api/auth/api-key | mint a key |
| DELETE | /api/auth/api-key | revoke the key |
| GET | /api/auth/captcha | a captcha url |
| GET | /auth/captcha-image | the challenge image itself |
Credits
| method | path | what it does |
|---|---|---|
| GET | /api/credits/balance | the server side truth |
| GET | /api/credits/history | a paged ledger |
| GET | /api/credits/packages | the price table |
| POST | /api/credits/purchase | amount, plus an optional coupon code |
| POST | /api/credits/validate-coupon | the oracle |
Crypt
| method | path | what it does |
|---|---|---|
| GET | /api/crypt/options | the four build modes |
| GET | /api/crypt/programs?architecture= | sideload hosts for x86 or x64 |
| POST | /api/crypt/process | the build itself |
Affiliate
| method | path | what it does |
|---|---|---|
| GET | /api/affiliate/status | invite flag and lifetime totals |
| GET | /api/affiliate/coupons | your codes |
| PUT | /api/affiliate/coupons/{id} | rename a code |
| GET | /api/affiliate/earnings | earnings per status |
Tickets
| method | path | what it does |
|---|---|---|
| GET | /api/tickets | your tickets, status filter available |
| POST | /api/tickets | open one, captcha included |
| GET | /api/tickets/{id} | the thread |
| POST | /api/tickets/{id}/messages | reply to the thread |
| PUT | /api/tickets/{id}/status | flip open, in progress, closed |
News
| method | path | what it does |
|---|---|---|
| GET | /api/news | paged articles |
| GET | /api/news/{id} | one article |
| GET | /api/news/admin | the admin listing |
| POST | /api/news | create an article |
| PUT | /api/news/{id} | edit an article |
| DELETE | /api/news/{id} | remove an article |
The field names in the responses are the schema of the business, so they deserve a sentence each. A transaction carries a type, a signed amount, a status, a spent figure, a coupon code, and a timestamp. A coupon carries a code, a discount type of percentage or fixed, a discount value, and an optional minimum purchase. An affiliate coupon is a fixed 10 percent discount and a 10 percent commission, with a current use count, an optional cap, lifetime earnings, and an active flag. The client enforces the shape when a user renames one: at least three characters, only letters, digits, hyphens and underscores, always uppercased before it is sent. A profile carries the account fields plus role, has_api_key, and api_key_created_at. A news article carries a title, a summary, a body, an author, a hero image, and a publish date. Dates are formatted client side by a one line wrapper around toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }). A ticket carries a subject, a status, a priority, a category, timestamps, and an awaiting response pulse for the ones where the ball is in your court. The category set is a fixed enum, billing, technical, feature request, affiliate system, affiliate payout and other, and the priority set is low, normal, high and urgent, with statuses open, in progress and closed.
Even the chip colors are a contract, three tiny maps compiled into the support bundle:
let statusColors = { open: "primary", in_progress: "warning", closed: "secondary" };let priorityColors = { urgent: "error", high: "warning", normal: "info", low: "success" };let categoryColors = { billing: "secondary", technical: "info", feature_request: "success", affiliate_system: "secondary", affiliate_payout: "warning" };Open tickets glow primary, in progress warns, closed fades to secondary. The chip labels are the raw enum strings with underscores swapped for spaces by a one line label mapper, which is why the statuses read as “In Progress” even though the wire format keeps the underscore. The awaiting response pulse is a separate flag that renders only when the status is anything but closed, so the support page literally glows at the customer who is owed an answer.
The status tabs above the list are the same enum in user terms, and switching one refires the list with a status filter attached:
let filters = [ { label: "All Tickets", value: "all" }, { label: "Open", value: "open" }, { label: "In Progress", value: "in_progress" }, { label: "Closed", value: "closed" },];let params = status !== "all" ? { status } : {};let res = await api.get("/tickets", { params });The detail, and it is a quiet one, is that the label string and the wire value are two different shapes. The page maps a human label to the underscore value, sends that to the server, and the server filters on the same enum entries that the color maps render. One vocabulary, four surfaces, the tab, the chip, the wire, and the label mapper, all agreeing because they were compiled from the same handful of strings. Rows show the ticket id, subject, the three chips, and an “Updated” timestamp rendered through the same date helper the news pages use.
Then the identifiers, since you asked where they come from. The client never generates a UUID anywhere. There is no randomUUID call in the entire bundle set. Every identifier the front end touches was issued by a server. The invoice id is a ULID because that is the format Bitcart mints, and the payment store id is the classic thirty two character ULID shape. Tickets and news articles use small server issued numbers, the kind a table hands out. And the session is the one identifier the app itself cannot explain, because it is an opaque signed handle, thirty two opaque bytes and a signature, server held, which is how you build identity without giving the browser anything worth stealing.
Last, the two doors. The web app authenticates with the session cookie riding along on every request. The scripted path authenticates with X-API-Key, and the crypt page will assemble the whole command for you. The generated curl carries the exact flags, file, option, antivm, admin, icon, version_info, install_path, elevated_install_path, injection_target, and selected_program for sideload builds, with an output name that flips between result.exe and result.zip depending on the mode. The key it drops in is YOUR_API_KEY by default, or your real key when one exists, fetched on demand and pasted in. That command is the cleanest API contract documentation an operator could ship, and it sits one copy button away from every visitor.
Since the builder assembles the command from an array of strings rather than from a template, the output is fully predictable. A clean native x86 build with no extras looks like this:
curl -X POST "https://fud.su/api/crypt/process" \ -H "X-API-Key: YOUR_API_KEY" \ -F "file=@payload.exe" \ -F "option=standard" \ -F "antivm=false" \ -F "admin=false" \ --output result.exeAnd a fully loaded sideload build shows every conditional it knows, with the backslashes in the install path doubled because the same escaper that protects the JSON quotes doubles the path separators too:
curl -X POST "https://fud.su/api/crypt/process" \ -H "X-API-Key: YOUR_API_KEY" \ -F "file=@payload.exe" \ -F "option=sideloading" \ -F "antivm=true" \ -F "admin=false" \ -F "icon=@icon.png" \ -F "version_info={\"CompanyName\":\"My Company\",\"OriginalFilename\":\"app.exe\"}" \ -F "install_path=%appdata%\\\\klJd9xP2a1\\\\aIb71KzQ.exe" \ -F "selected_program=svchost.exe" \ --output result.zipThe URL is not typed by hand, the join helper puts the path on the page origin, and the output name is decided by the mode before the command is built. The API build never writes is_web_ui, so the server can sort scripted calls from browser calls on a single field, and the sideload build earns the zip name that the web client also gets. Watch the escaping and you can read the whole serializer: every user supplied value passes through a function that doubles backslashes and backslash quotes, then the pieces are joined with newlines.
What a page calls the moment it loadsh2
Worth collecting the mount sequence for every page, since it is the difference between reading an API map and knowing a site. Each row below is what the browser does on entry, not what happens after clicks.
| page | calls on load |
|---|---|
| home | none, a server rendered shell |
| login | none, but it reads returnTo and payment_status from the URL |
| register | captcha url, then the captcha image as a blob |
| credits | balance, history, and packages, all three, on every entry |
| crypt | options always, balance only when signed in, programs for the current architecture |
| profile | the profile, then the api key, which 404s when none yet |
| support | the ticket list with the active status filter |
| new ticket | the captcha pair, with category and subject pre-filled from the URL |
| affiliate | status, coupons, and earnings, in that order |
| news | the article list |
| faq | none |
Two locks in that table are public by design. The options endpoint serves anyone, signed in or not, which makes sense because the price list has to render before login does. The captcha pair belongs to the session, not to a user. Everything else leans on the cookie, and the pages know it, which is why the site behaves like one machine after a single login instead of a dozen disconnected forms.
The new ticket page is the one mount that reads and trusts the URL before anything else. It parses the search string once, seeds the form from it, and defaults the priority, all in one state call:
const params = new URLSearchParams(window.location.search);const category = params.get("category") || "";const subject = params.get("subject") || "";const [form, setForm] = useState({ subject, message: "", category, priority: "normal" });So /support/new-ticket?category=affiliate_payout&subject=Refund opens a prefilled payout ticket. That is a convenience feature with a small cost: the URL is the seed, which means support links can carry category and subject around as if they were query arguments, and whoever routes those links chooses the defaults for the people who click them.
The category select on the page is a literal list of six options, and the last one is conditional. The values are the same keys the support color map used, billing, technical, and feature_request, plus two affiliate doors and a catchall:
<select> <option value="">Select a category</option> <option value="billing">Billing</option> <option value="technical">Technical Support</option> <option value="feature_request">Feature Request</option> <option value="affiliate_system">Affiliate System</option> <option value="affiliate_payout">Affiliate Payout</option> <option value="other">Other</option></select>The payout row only renders when the current account passes the affiliate status check, and choosing it swaps the caption for the hint “Add your payout details in the message below.” That is the payout flow in miniature: the affiliate opens a prefilled ticket, the body carries the details, and the operator pays on the schedule the FAQ promises. The option value is the same affiliate_payout string the dashboard writes into the URL, so the pocket built for payouts is closed unless the account is visibly an affiliate.
The fingerprint, for defendersh2
If you ever see this stack again, here is what it looks like from the outside and you should recognize it fast. Cloudflare records on a Next.js app. A session cookie with the two segment signed shape. A CSP that casually mentions localhost. Pages under /crypt, /credits, /profile, /support. A badmin. subdomain rendering nuxt chunks and a /modal/bitcart.js loader. Invoices that are ULIDs answered by bitcart. subdomains. A BMP of a clean SaaS attached to a product category that has no clean customers.
The build also has no guests, which is worth checking because almost every service has at least one. There is no analytics script anywhere in these chunks. The only gtag string that appears is webpack’s Symbol.toStringTag noise, and the operators are not even counting visitors inside the client. There is no fetch call at all, every request routes through the one axios instance. There is no localStorage, no direct document.cookie read, no sendBeacon, no service worker, no client side websocket, the payment socket lives inside the Bitcart modal, not on the site. The Twitterbot and Discordbot strings that show up in the big vendor chunk are the Next.js bot user agent detector, framework code, not social integrations. Knowing what is not there is half of knowing where the seams are. The backend holds everything, and the browser is a display.
The payment pattern is the giveaway more than the branding. Any service where the front end mints an invoice id and a foreign checkout domain renders it, while the main domain holds a ledger in credits, is a wrapper over a payment rail. Those rails are honest where the marketing is not. The invoice always comes out with a real number on it, and the credits only ever move when the crypto clears.
So when people ask where the vulnerability is in a service like this, the temptation is to point at the buttons the users see and the flags the page trusts. The correct answer, as with most well-built small stacks, is that the visible surface is a lobby, the backend is the building, and the building charges rent. The doors you can actually open are the ones the operators leaned on while they were building it: the lazy captcha, the unprotected coupon check, the dev port in the allowed list, the password gift wrapped in a response header. Those are the cracks. Everything else is just a website doing its job.
The marketing layer, and the twelve names on the wallh2
The homepage is a server rendered shell with no API calls, so the entire story it tells lives in the static bundle. The hero runs a fake terminal that types out commands and answers, and it reads as if someone actually ran the tool while writing the copy:
fud init --onlinefud.su crypter v3 · session readyengine status --modulespolymorphic · evasion · analysis-resistant · sideloadengine scan-file output.exescan complete · 0/36 detections · FUDauth statusno active session · sign up or sign in to cryptThe scan counter is a nice marketing tell: thirty six detections, exactly three times the twelve logos in the marquee below it, whether deliberate or not, and none of it from any real run. The hero asks the browser for nothing, literally nothing, not one API call, so the numbers only exist in the copy. Under the title, which is simply fud.su Online Crypter, a subheading reads “Create an account to start crypting.”, and the primary button reads Start Crypting. The twist is in the wiring: that button ships disabled while you are signed out, which forces exactly the one click the business wants, and the enabled button beside it points at /credits instead of the builder.
Below the hero sits a three step strip that explains the product without saying crypting a file out loud. Create an account: sign up in seconds and purchase credits when you’re ready, no subscription required. Upload and configure: upload your file, choose your crypt options, and configure evasion settings to fit your needs. Download your build: receive your crypted file instantly, and original uploads are processed and deleted immediately.
Under it sit six reason cards, one per claim, each a single sentence of marketing with a single sentence of product behind it. Secure says files are processed and deleted, which matches the FAQ and nothing else in the codebase. Evasion and Polymorphic make the obvious promises. Trusted Execution advertises running from legitimate signed executables to bypass SmartScreen and Chrome download warnings with longer FUD windows, which matches the actual sideloading mechanism we traced in the builder. Analysis Resistant promises runtime checks that only execute in real intended environments, which is the anti VM checkbox by its nicer name. And the Pay as you go card prints the sentence that matters most to the economics: credits never expire.
Then the wall. Under the hero, a marquee strip of antivirus logos with the headline “Antiviruses we bypass.” Twelve logos, and the bundle carries the whole dataset in one literal, names and slugs paired, plus an icon map that resolves each slug to a path under /icons/av/ on the main origin:
let z = [ { name: "Windows Defender", slug: "microsoftdefender" }, { name: "Kaspersky", slug: "kaspersky" }, { name: "Bitdefender", slug: "bitdefender" }, { name: "Norton", slug: "norton" }, { name: "McAfee", slug: "mcafee" }, { name: "Avast", slug: "avast" }, { name: "AVG", slug: "avg" }, { name: "ESET", slug: "eset" }, { name: "Malwarebytes", slug: "malwarebytes" }, { name: "Sophos", slug: "sophos" }, { name: "Trend Micro", slug: "trendmicro" }, { name: "CrowdStrike", slug: "crowdstrike" },];let v = { microsoftdefender: "/icons/av/microsoftdefender.svg", kaspersky: "/icons/av/kaspersky.svg", ... };A structural quirk makes the strip easy to verify: every one of those twelve names appears exactly four times in the homepage bundle, once as the display name, once as the slug, once as the icon map key, and once inside the icon filename itself. Windows Defender shows eight hits because two brands carry the word defender in their slug. Four buildings means one literal, and one literal means the wall is one static claim: twelve logos, compiled once, served to everyone, standing as the entire security promise of the site.
The antivirus scan, step by steph2
Because the marquee is such a loud claim, I wanted to verify it the boring way: a case insensitive substring count for every major brand across the whole chunk set. The important finding is not the homepage numbers. It is that the homepage numbers are the only numbers. The full scan:
| file | pattern | count | what it really is |
|---|---|---|---|
| homepage.js | each of the 12 | 4 | the marquee literal, declared once |
| homepage.js | Defender | 8 | two brands carrying the word |
| chunk148.js | eset | 5 | floating ui placement reset, reset:{placement:t} |
| chunk243.js | eset | 20 | motion internals, this.node.animationState.reset() |
| chunk243.js | trend | 9 | render phase identifiers |
| chunk260.js | eset | 2 | a radix form hook, addEventListener("reset",t) |
| chunk289.js | eset | 1 | the axios status alias ResetContent:205 |
| chunk493.js | eset | 12 | router boundary reset, reset:()=>this.setState({redirect:null}) |
| chunk493.js | trend | 9 | getRenderedPathname and friends |
| vendor.js | eset | 8 | react-dom attribute and event lists |
| vendor.js | trend | 19 | css property names like textRendering |
| layout.js | eset | 5 | the error boundary’s reset handlers |
| newticket.js | eset | 1 | the 429 rate limit hint |
| register.js | eset | 1 | the same 429 rate limit hint |
Every one of the chunk 148, 243, 260, 289, 493, 506, vendor and layout hits is the letters eset sitting inside the word reset, or the letters trend sitting inside a longer identifier, and all of them live in framework internals, not product code. The lesson bears repeating because it is easy to get wrong. Outside that one homepage marquee there is zero antivirus branding anywhere in the client bundle. No vendor name ever reaches an API call, no build parameter names a scanner, and the backend never hints at which products it evades. The wall of twelve logos is a purely visual claim painted on the landing page. The scanner that actually crafts builds never mentions a single product. When the marketing layer and the product layer disagree about which layer holds the claim, that distance is the fingerprint.
The FAQ, as a confessionh2
An FAQ is a document of what people ask when the product corrodes, and fud ships a real one, six categories of actual questions with actual answers, statically bundled just like the landing page. Skipping it would be malpractice, because it is the only place the product speaks plainly about its own limits. The six categories are General Questions, Credits System, Account Management, Technical Support, DLL Sideloading, and Affiliate Program. The answers worth keeping are the ones that quietly contradict the marketing or fill its blanks:
- Files are not stored. “We do not store your files. All files are processed and deleted after the operation is complete.” That is the Secure card claim in prose, and it is also the load bearing denial of the entire platform, so it appears identically in two places.
- DLL sideloading has limits. Some DLL files require manual crypting with an additional fee, which is the FAQ admitting the flagship cheap mode does not just work.
- Binding is free of charge. Cost a customer nothing, so the answer to how much is nothing.
- Sideloading advantages are stated plainly: it bypasses SmartScreen and Google Chrome download warnings and delivers longer FUD windows, the same two claims the homepage makes, spelled out here as if documenting a tool rather than selling one.
- Distribution advice is blunt. Pack results in a zip, because “you will lose the SmartScreen and Google Chrome bypass advantages if packed” means packing properly keeps them. Renaming the exe is fine. “Do not rename the dlls” is worth its own line, because it is the single most actionable sentence the site publishes and it is written like a support ticket’s postmortem.
- Payouts are cryptocurrency only, take one to three business days, and are requested on the affiliate dashboard, where the button opens a support ticket that routes into the payout category we mapped in the API.
The FAQ links inside the answers point at /affiliate, /credits, /profile and /support, so the document doubles as a sitemap of where the money lives. Read front to back it is a licensing manual for an evasion product written in the tone of a utility’s helpdesk, and that dissonance is the whole business in one page.
The document itself is a static array in module 7465, bundled exactly like the homepage copy, one category object per section and one item object per question. The answers are two kinds, a plain string or a rich array of text segments and link objects, which is how the support links get embedded mid sentence. Here is the shape with the answers that matter most:
let faq = [ { category: "General Questions", slug: "general-questions", items: [ { slug: "do-you-store-my-files", question: "Do you store my files?", answer: "No, we do not store your files. All files are processed and deleted after the operation is complete.", }, { slug: "do-you-offer-a-subscription", question: "Do you offer a subscription?", answer: ["We do not currently offer a subscription service. Please contact our ", { text: "support team", href: "/support" }, " if you need help integrating the API."], }, { slug: "what-settings-should-i-use-when-crypting-my-file", question: "What settings should I use when crypting my file?", answer: "We recommend using AntiVM when there is no need to run in a virtual environment or RDP. Use injection if user detection is a greater concern than AV/EDR detection. If your software offers its own optional startup, use your judgement on which one to use. Do not use both the startup from your software and the crypter's startup.", }, { slug: "what-files-can-i-rename", question: "What files can I rename?", answer: "You can rename the exe. Do not rename the dlls. You can add additional decoy files if desired.", }, ], },];The settings answer is the site’s own threat model in one paragraph, and it deserves to be read as one. AntiVM when you trust the machine, injection when you fear the watchers, and never both start mechanisms at once. “You can add additional decoy files if desired” is a sentence no legitimate software publisher writes. The array answers are the sitemap part, the href values are the same four money routes the prose lists, and since the page renders those hrefs from data, the whole FAQ is a static config file that the layout just displays.
Six categories hold the entire document, and the dataset is a literal in the bundle, so it doubles as a table of contents with the slugs that show up in URLs:
let faqCategories = [ { category: "General Questions", slug: "general-questions" }, { category: "DLL Sideloading", slug: "dll-sideloading" }, { category: "Credits System", slug: "credits-system" }, { category: "Account Management", slug: "account-management" }, { category: "Affiliate Program", slug: "affiliate-program" }, { category: "Technical Support", slug: "technical-support" },];The one line answers carry the market position better than the marketing page does. Credits never decay, “No, credits do not expire and will remain in your account until used.” .NET uploads are accepted outright, “Yes, we support .NET files.” DLL support is narrower and priced in the same sentence, the answer admits the files “require manual crypting with an additional fee.” File binding, attaching your payload to a signed host program, is offered as a free service, “We provide a binding service free of charge.” Account deletion and recovery are not self serve at all, both answers route you to the support team through a ticket, and payouts settle on a stated schedule, one to three business days with a notification when the money moves. Read the category names alone and the FAQ is a complete inventory of the business: general operation, rotating DLL payloads, an account ledger, an affiliate funnel, and the tickets that keep the whole thing quiet.