# Cats with power tools > Collaborative programmer blog Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts. Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`). ## Pages ### About URL: https://blog.pixelmelt.dev/about/ Last updated: 2026-08-23T04:24:51.000Z 🧹 This blog **may contain an excessive amount of intricate detail that may only interest a specific audience**. ## Favorite blogs [mango.pdf.zoneBlog and dumb websites by the hacker known as “Alex”![](https://blog.pixelmelt.dev/content/images/icon/favicon-96efe351-1bc6-499f-a131-f1f35e81da32.ico)mango.pdf.zonemangopdf![](https://pbs.twimg.com/profile_images/1019746918728019969/CdUJIbpP_400x400.jpg)](https://mango.pdf.zone/?ref=blog.pixelmelt.dev) Super fun writing style and partially my inspiration to start writing one! [Read the Tea LeavesSoftware and other dark arts, by Nolan Lawson![](https://blog.pixelmelt.dev/content/images/icon/favicon-1c9576a4-077a-4435-bf2d-4e051a7c5af6.png)Read the Tea Leaves![](https://blog.pixelmelt.dev/content/images/thumbnail/favicon-2ef2f881-7669-4440-9776-a80252e2e9a9.png)](https://nolanlawson.com/?ref=blog.pixelmelt.dev) Perfectly described how I've been feeling about LLMs in CS, no need to write a post on that now I guess... [UnsungA blog about software craft and quality![](https://blog.pixelmelt.dev/content/images/icon/favicon-fbff6984-8944-4949-9601-3c35491b4c12.png)Marcin Wichary![](https://blog.pixelmelt.dev/content/images/thumbnail/1.1088w-8355d1b8-ee7b-4bc1-bab2-26e932de6559.avif)](https://unsung.aresluna.org/?ref=blog.pixelmelt.dev) Bothersome UI/UX things that I would never notice without reading about first. [PILK #1 | I Want You To Create Something Original!THROW OFF YOUR SUBSTACK CHAINS! DON’T CONFINE YOUR CREATIONS TO THE INBOX! ESCAPE THE ALGORITHMIC PRISON!![](https://blog.pixelmelt.dev/content/images/icon/favicon-42cce6f0-103f-483e-ba12-9769cb6e9c0b.png)PILK![](https://pilk.website/2/create-something-original-wide.png)](https://pilk.website/1/i-want-you-to-create-something-original?ref=blog.pixelmelt.dev) Every post is a new design, I love it! ## Favorite web comics - [https://xkcd.com/](https://xkcd.com/?ref=blog.pixelmelt.dev) - [](https://www.buttersafe.com/?ref=blog.pixelmelt.dev)[https://www.buttersafe.com/](https://www.buttersafe.com/?ref=blog.pixelmelt.dev) - [https://invisiblebread.com/](https://invisiblebread.com/?ref=blog.pixelmelt.dev) - [https://www.monkeyuser.com/](https://www.monkeyuser.com/?ref=blog.pixelmelt.dev) ### For LLMs URL: https://blog.pixelmelt.dev/llms-txt/ Last updated: 2026-08-15T00:28:53.000Z FLAG{found\_1t} ## Posts ### Training a Reinforcement Learning Model to Play Bonk.io URL: https://blog.pixelmelt.dev/training-a-reinforcement-learning-model-to-play-bonk-io/ Last updated: 2026-08-15T02:52:17.000Z This is [Bonk.io](https://bonk.io/?ref=blog.pixelmelt.dev), a simple browser game where you, a circle, try to knock other circles off of a map sumo style. I've been playing this game for a few years at this point and recently had the idea of trying to train a neural network to play the game. It should be interesting since despite looking simple, there is quite a high skill ceiling. It should be easy, **right**? ![](https://blog.pixelmelt.dev/content/images/2026/07/image-3.png) A player sitting on a platform in game. Lets look at the state we will need to teach the bot to output as well as read on other players. ![](https://blog.pixelmelt.dev/content/images/2026/07/image-7.png) A list of the different inputs a player can preform. Pretty simple, but now we need to create the environment where the network will fight against itself in a loop to learn to play the game. ## Building the Training Harness This is where the hard part begins, since Bonk.io was originally a flash game, it is using [Box2DWeb](https://github.com/hecht-software/box2dweb/?ref=blog.pixelmelt.dev), a JavaScript port of the same Box2D engine used in the flash era. Also, the game client is obfuscated with [JScrambler](https://jscrambler.com/?ref=blog.pixelmelt.dev) and completely incomprehensible. Creating this environment is not going to be easy, we have a few options at this point. ## Browser Manipulation We could use an instrumented browser where we send key presses to the DOM and read positions out of the games memory. This is probably the most time efficient way to pull this off, and some other people have done this! But if you end up doing this you are stuck running the game in real time. One browser tab gives you one game environment running at 30 fps, and reading state back out means hooking the frame drawing APIs of an obfuscated program. Reinforcement learning needs a lot of data, my current training run just passed 10 billion frames. At 30 fps that's over ten years of continuous play for a single tab, so this approach would need a server full of browsers running for months, a server that I don't have. ## Reimplement the Physics? Option two, I reimplement the physics myself from how the game looks and feels. The problem is that "close enough" is not close enough. If I train on an approximation, the bot learns my approximation. Every mistake in my engine becomes a misconception in the final model, and it makes no sense to begin with since I would need to hook the browser anyway just to run the finished model against the real game. Here is the thing, bonk.io uses deterministic lockstep networking. Every client in a match simulates the entire game locally, and the only thing that crosses the wire is each player's keypresses. For that to work, the physics has to be a pure function: same state plus same inputs gives the same floats, bit for bit, on every machine. Which means that **the game ships its own engine to every browser that opens the page**. I just had to find and use it. ![](https://blog.pixelmelt.dev/content/images/2026/07/image-8.png) Bonk.io's JScrambler protected source code ## Stealing the Physics The first thing I had to do was make the client readable at all. Most of that work was not mine, my friend [Ciaran](https://exploit.cat/about/ciaran/?ref=blog.pixelmelt.dev) did the heavy lifting on [deobfuscating the JScrambler build](https://github.com/Ciarands/jscrambler-deobfuscator?ref=blog.pixelmelt.dev), I only had to reverse some non JScrambler bonk specific code obfuscation. Ciaran has [his own technical blog](https://exploit.cat/?ref=blog.pixelmelt.dev) along with [some articles on this one](https://blog.pixelmelt.dev/author/ciaran/) if you want to see more from him. After a lot of staring at the 31,339 lines that came out the other end, I figured out that the physics the game runs is is a modified build of [Box2DWeb](https://github.com/hecht-software/box2dweb/?ref=blog.pixelmelt.dev). And even better, the game rebuilds the entire physics world from scratch every frame. There is no persistent world. Each frame, every body, fixture and joint is recreated from a plain JSON game state, the world is stepped once, and the results are read back into a brand new state. This means that the engine is a pure function over data, exactly what I needed. ![](https://blog.pixelmelt.dev/content/images/2026/07/image-9.png) Also small Easter egg, the player's friction constant is **0.001337**! ## Speed Initially I started out by using the JavaScript ripped from the client. But the siren song of making an LLM rewrite the library in rust was too strong. ![](https://blog.pixelmelt.dev/content/images/2026/07/image-11.png) That's about how it went This is actually a great use of LLMs, since the goals for the project are extremely verifiable in their correctness, in this case we have two things to optimize for, **execution speed and parity to the JavaScript implementation**. Getting the rust port "close" to the JavaScript version fails for the same reason that rewriting based on how it looks fails. So the port mirrors every float expression in the original in shape and evaluation order. All math goes through wrappers that round to 7 decimals, exactly like the game's SafeTrig utility functions. Even the JSON parser had to be correctly rounded, because the default one isn't, and parity breaks at the first division. Then the test harness. I used a corpus of 1,961 real maps from the game. Forcing the LLM to do a full frame-by-frame comparison against the original implementation was successful and came out to 1961/1961 bit-identical in the end. ## Training a brain With real physics in hand, the actual machine learning could start. The trainer is TypeScript running on Bun, with the Rust engine loaded over FFI. I initially started out with TensorFlow.js on the GPU, but doing that made training slower than the equivalent TF.js CPU build (4k fps vs 41k), because the network is tiny, it spends most of its time on overhead. So its time to call up our friend again. ![](https://blog.pixelmelt.dev/content/images/2026/07/image-12.png) I'm seeing a pattern here Just like with before, we can validate the accuracy of the output against the existing tensorflow implementation, this assertion is a little more shaky and potentially prone to edge cases, but in the end it did work so who am I to complain. Now that we have PPO written from scratch, cuBLAS for the matmuls and 31 custom CUDA kernels for everything else. The bot makes a decision every 2 physics frames (15 Hz). Eight rollout workers run 512 game instances against each other, a central inference server batches every worker's policy evaluations into big GPU waves so they don't fight over the card, and PPO updates happen on the same GPU between waves. The bot sees 14 numbers about its own disc and 19 about the opponent, positions, velocities, ability charge, who's touching what, stacked over 7 frames at offsets 0, 1, 3, 7, 15, 30 and 60, so it gets a couple of seconds of history to inform its next decision. Plus decaying traces of its own recent buttons, 16 raycasts into the map geometry reporting wall proximity and death-zone proximity, stacked 4 deep. 385 floats total. Its output options are left, right or neither, up, down or neither, heavy on or off. The reward while playing is +1 for a win, −1 for a loss, −0.3 for draws. And for the opponents I used a league. About a third of games against the models current self, a third against a reservoir of past versions, and a third against "exploiters", which are policies trained specifically to beat the current main agent, so it can't settle into strategies with holes in them. Every sample is also mirrored horizontally, doubling the data and forcing the policy to be symmetric. ## Where it's at now The bot beats plenty of casual players and still loses to strong ones. On a live Elo table my best deployed policy sits 5th out of 522 tracked players, behind four very good humans, and ahead of every other regular who has joined the room. ![](https://blog.pixelmelt.dev/content/images/2026/08/com7_vs_SPHRX.gif) The robot wins in the end Big thank you to legendboss123 for guiding me on this project and providing in game replays for pretraining. [LEGENDBOSS123 - Overview100% totally a professional coder frfr. LEGENDBOSS123 has 50 repositories available. Follow their code on GitHub.![](https://blog.pixelmelt.dev/content/images/icon/favicon-ed27cd99-a74d-4a2a-874a-1f0370a69af2.svg)GitHub![](https://blog.pixelmelt.dev/content/images/thumbnail/113867435-00deb9a7-794e-422a-a812-b97d195a9c04)](https://github.com/LEGENDBOSS123?ref=blog.pixelmelt.dev) ### The Webs Digital Locks have Never had a Stronger Opponent URL: https://blog.pixelmelt.dev/the-webs-digital-locks/ Last updated: 2026-09-03T14:36:33.000Z Recently I've been benchmarking my JavaScript code protection against the strongest Claude model. I'm up to 22 minutes (and 170k tokens) before it can crack a small protected sample open starting from 0 knowledge. ([More about the protection scheme here](https://blog.pixelmelt.dev/analysing-pistoljsvm/)) Its kind of disappointing for how much time I've put into my work, but man is it good at helping point out flaws in the system, and showing what I'm doing well based on where it get tripped up in the process. This made me think about reverse engineering and the potential of LLM's in it in general. To me the appeal of LLM's is in taking on massive amounts of tedious mental work quickly. I could hire someone to do the same thing, but the LLM gets to the same output quicker. The whole point of code protection is to make reversal (which is mathematically impossible to stop) take as long as possible. ![](https://blog.pixelmelt.dev/content/images/2026/03/image-2.png) The basic idea ## Information Wants To Be Free Based on my own observations I wouldn't be hesitant to claim that we are already in a renaissance era of reverse engineering, everything can be taken apart, the defenders are going to be on the back foot until we figure out some way to cope with LLM's. Keep in mind this is currently strictly in the JavaScript ecosystem. I am not a binary reverse engineer, but I would expect it'll happen to the binary side of things eventually too, once the training data side of things gets there. ## Evidence? Just the other week I had Claude rip a friends college textbook out of a proprietary online reader with very little input from me, it took 30 minutes when before that would have taken hours potentially days. ( expect some cool blog posts soon :> ) The floor for reverse engineering the web has been raised immensely. When an uninformed user can say "deobfuscate this code" and a frontier LLM will rip through even a commercial antibot or an ebook platforms code protection, it raises questions on whether these schemes are even doing what they were originally designed for anymore. The difference you get from paying 50k is the difference between any Joe schmoe defeating your protection and a hobbyist doing it. The experts have always been able to reverse protections but now that they can command the tedious task solver machine they can reverse much faster than before. Before it was 'make it take longer before it inevitably falls' and then update it to hit back at the attackers, now I would be hard pressed to see the attackers being kicked down for more then a few hours while they identify what changed and how to break that too. My point is LLMs annihilate the upfront cost of reverse engineering a code sample. It can reverse my protection and it can reverse commercial protections, exposing data to the user over the web is less safe then it has ever been before. ## You Can Just Generate Source Maps Now You can just take a bundled app and regenerate the source files. I've seen this done to three separate websites. Remember when [Apple's app store source map was posted accidentally](https://news.ycombinator.com/item?id=45804664&ref=blog.pixelmelt.dev)? You can do this to any website you want! One was minified and bundled with webpack. One was written in a JavaScript derivative language (Coffeescript). One was protected with Jscrambler obfuscation. No difference in the outcome. You want to get minified variables back? No problem, want to port the whole codebase to typescript too? Might need a little bit more instructing, maybe write a harness but sure why not? ![](https://blog.pixelmelt.dev/content/images/2026/03/image-1.png) Part of the code from a game reversed with an LLM ## The defenders have LLMs too, just pit them against each other? There's no anti LLM obfuscations yet and it's Interesting to think about what that would look like, but also, the traditional use case of obfuscation can't mesh with that idea in the first place, Once I crack your game or program with IP in it, Its over you don't get another chance, Its only antibots who have the luxury of being able to evolve their protection in real time. ## Whats next? Is there a world where the defense side gets equivalent agentic tooling? Automated protection that evolves faster than an agent can characterize it? Or is that a fundamentally asymmetric problem where offense wins by default because reversing is easier to automate than protecting? It very well could happen, I've seen no evidence of it though, perhaps because defense requires generating novel complexity, its harder to automate then offense, which only requires classifying existing complexity. ## Related Also see Antoine Vastel's post on the same subject. [While anti-bot vendors talk about AI agents, classical bots are getting the bypasses⚠️ A quick disclaimer on AI agent management AI agents will clearly change how people interact with websites. They will search, compare products, make purchases, fill forms, and perform actions that used to require a human in front of a browser. Websites will need ways to decide which agents they accept,![](https://blog.pixelmelt.dev/content/images/icon/brandmark-ol-d6c21b29-d879-4758-a2a9-66d10cae8de9.png)The Castle blogAntoine Vastel![](https://blog.pixelmelt.dev/content/images/thumbnail/download--4--326c81fa-c811-43ac-8977-48c9ce288241.png)](https://blog.castle.io/while-anti-bot-vendors-talk-about-ai-agents-classical-bots-are-getting-the-bypasses/?ref=blog.pixelmelt.dev) ### How I Reversed Amazon's Kindle Web Obfuscation Because Their App Sucked URL: https://blog.pixelmelt.dev/kindle-web-drm/ Last updated: 2026-09-23T15:26:15.000Z [How I bypassed Amazon’s Kindle web DRM | Hacker News![](https://blog.pixelmelt.dev/content/images/icon/y18.svg)Hacker News![](https://blog.pixelmelt.dev/content/images/thumbnail/y18.svg)](https://news.ycombinator.com/item?id=45610226&ref=blog.pixelmelt.dev) This article hit #1 on Hacker News, thanks all! ## TL;DR - I bought my first ebook from amazon - Amazon's Kindle Android app was really buggy and crashed a bunch - Tried to download my book to use with a functioning reader app - Realized Amazon no longer lets you do that - Decided to reverse engineer their obfuscation system out of spite - Discovered multiple layers of protection including randomized alphabets - Defeated all of them with font matching wizardry ## Amazon Made This Personal ### The One Time I Tried To Do Things The Right Way I've been reading ebooks from various sources for years. But this time, I thought: "Let's support the author." Download Kindle app on Android. Open book. **Crash.** ### I Just Wanted To Read My Book App crashes. Fine, I'll use the web reader. Oh wait, can't download it for offline reading. What if I'm on a plane? Hold on, I can't even export it to Calibre? Where I keep ALL my other books? So let me get this straight: - I paid money for this book - I can only read it in Amazon's broken app - I can't download it - I can't back it up - I don't actually own it - Amazon can delete it whenever they want **This is a rental, not a purchase.** ![](https://blog.pixelmelt.dev/content/images/2025/10/image.png) This does not say "Rent" ### It Becomes Personal I could've refunded and "obtained" it in 30 seconds. Would've been easier. But that's not the point. The point is I PAID FOR THIS BOOK. It's mine. And I'm going to read it in Calibre with the rest of my library even if I have to reverse engineer their web client to do it. ### Reversal Time Kindle Cloud Reader (the web version) actually works. While looking through the network requests, I spotted this: ``` https://read.amazon.com/renderer/render ``` To download anything, you need: 1\. Session cookies - standard Amazon login 2\. Rendering token - from the startReading API call 3\. ADP session token - extra auth layer Sending the same headers and cookies the browser does returns a TAR file. ### What's Inside The TAR? ``` page_data_0_4.json # The "text" glyphs.json # SVG definitions for every character toc.json # Table of contents metadata.json # Book info location_map.json # Position mappings ``` ## Amazon's Obfuscation Layers of Ebook Hell Downloaded the first few pages, expected to see text. Got this instead: ```JSON { "type": "TextRun", "glyphs": [24, 25, 74, 123, 91, 18, 19, 30, 4, ...], "style": "paragraph" } ``` These aren't letters. They're glyph IDs. Character 'T' isn't Unicode 84, it's glyph 24. And glyph 24 is just a series of numbers that define a stroke path, its just an image of a letter. It's a substitution cipher! Each character maps to a non-sequential glyph ID. ### The Alphabet Changes Every. Five. Pages. Downloaded the next batch of pages. Same letter 'T' is now glyph 87. Next batch? Glyph 142. **They randomize the entire alphabet on EVERY request.** This means: - You can only get 5 pages at a time (API hard limit) - Each request gets completely new glyph mappings - Glyph IDs are meaningless across requests - You can't build one mapping table for the whole book ### Let Me Show You How Bad This Is For my 920-page book: - **184 separate API requests** needed - **184 different random alphabets** to crack - **361 unique glyphs** discovered (a-z, A-Z, punctuation, ligatures) - **1,051,745 total glyphs** to decode ### Fake Font Hints (They're Getting Sneaky) Some SVG paths contained this garbage: ``` M695.068,0 L697.51,-27.954 m3,1 m1,6 m-4,-7 L699.951,-55.908 ... ``` Looking at it, we see these tiny `m3,1 m1,6 m-4,-7` commands, they are micro MoveTo operations. **Why this is evil:** - Browsers handle them fine (native Path2D) - Python SVG libraries create spurious connecting lines - Makes glyphs look corrupted when rendered naively - Breaks path-sampling approaches This is deliberate anti-scraping. The glyphs render perfectly in browser but make it so we cant just compare paths in our parser. Take a look ![](https://blog.pixelmelt.dev/content/images/2025/10/image-2.png) Fun! Eventually I figured out that filling in the complete path mitigated this. ![](https://blog.pixelmelt.dev/content/images/2025/10/id_131_bookerly.png) ### Multiple Font Variants Not just one font. FOUR variants: - bookerly\_normal (99% of glyphs) - bookerly\_italic (emphasis) - bookerly\_bold (headings) - bookerly\_bolditalic (emphasized headings) Plus special ligatures: ff, fi, fl, ffi, ffl More variations = more unique glyphs to crack = more pain. ### OCR Is Mid (My Failed Attempt) Tried running OCR on rendered glyphs. Results: - 178/348 glyphs recognized (51%) - 170 glyphs failed completely OCR just sucks at single characters without context. Confused 'l' with 'I' with '1'. Couldn't handle punctuation. Gave up on ligatures entirely. OCR probably need words and sentences to work well. ## The Solution That Actually Worked Every request includes \`glyphs.json\` with SVG path definitions: ```JSON { "24": { "path": "M 450 1480 L 820 1480 L 820 0 L 1050 0 L 1050 1480 ...", "fontFamily": "bookerly_normal" }, "87": { "path": "M 450 1480 L 820 1480 L 820 0 L 1050 0 L 1050 1480 ...", "fontFamily": "bookerly_normal" } } ``` **Glyph IDs change, but SVG shapes don't.** ### Why Direct SVG Comparison Failed First attempt: normalize and compare SVG path coordinates. Failed because: - Coordinates vary slightly - Path commands represented differently ### Pixel-Perfect Matching Screw coordinate comparison. Let's just render everything and compare pixels. ![](https://blog.pixelmelt.dev/content/images/2025/10/id_028_bookerly.png) Render that A 1\. **Render every SVG as an image** - Use cairosvg (lets us handle those fake font hints correctly) - Render at 512 x 512px for accuracy 2\. **Generate perceptual hashes** - Hash each rendered image - The hash becomes the unique identifier - Same shape = same hash, regardless of glyph ID 3\. **Build normalized glyph space** - Map all 184 random alphabets to hash-based IDs - Now glyph "a1b2c3d4..." always means letter 'T' 4\. **Match to actual characters** - Download Bookerly TTF fonts - Render every character (A-Z, a-z, 0-9, punctuation) - Use SSIM (Structural Similarity Index) to match ### Why SSIM Is Perfect For This SSIM compares image structure, not pixels directly. It handles: - Slight rendering differences - Anti-aliasing variations - Minor scaling issues For each unknown glyph, find the TTF character with highest SSIM score. That's your letter. ### Handling The Edge Cases **Ligatures:** ff, fi, fl, ffi, ffl - These are single glyphs for multiple characters - Had to add them to TTF library manually **Special characters:** em-dash, quotes, bullets - Extended character set beyond basic ASCII - Matched against full Unicode range in Bookerly **Font variants:** Bold, italic, bold-italic - Built separate libraries for each variant - Match against all libraries, pick best score ## Now It All Works ### Final Statistics ``` === NORMALIZATION PHASE === Total batches processed: 184 Unique glyphs found: 361 Total glyphs in book: 1,051,745 === MATCHING PHASE === Successfully matched 361/361 unique glyphs (100.00%) Failed to match: 0 glyphs Average SSIM score: 0.9527 === DECODED OUTPUT === Total characters: 5,623,847 Pages: 920 ``` Perfect. Every single character decoded correctly. ### EPUB Reconstruction With Perfect Formatting The JSON includes positioning for every text run: ```JSON { "glyphs": [24, 25, 74], "rect": {"left": 100, "top": 200, "right": 850, "bottom": 220}, "fontStyle": "italic", "fontWeight": 700, "fontSize": 12.5, "link": {"positionId": 7539} } ``` I used this to preserve: - Paragraph breaks (Y-coordinate changes) - Text alignment (X-coordinate patterns) - Bold/italic styling - Font sizes - Internal links The final EPUB is near indistinguishable from the original! ## The Real Conclusion Amazon put real effort into their web obfuscation. ### Was It Worth It? To read one book? No. To prove a point? Absolutely. To learn about SVG rendering, perceptual hashing, and font metrics? Probably yes. ### Use This Knowledge Responsibly This is for backing up books YOU PURCHASED. Don't get me sued into oblivion thanks. Due to the nature of this post, if you are in any way affiliated with Amazon, please reach out to pixelmelt + protonmail.com. Additional reading: [Improving PixelMelt’s Kindle Web DeobfuscatorA few days ago, someone called PixelMelt published a way for Amazon’s customers to download their purchased books without DRM. Well… sort of. In their post “How I Reversed Amazon’s Kindle Web Obfuscation Because Their App Sucked” they describe the process of spoofing a web browser, downloading a bunch of JSON files, reconstructing the obfuscated SVGs used to draw individual letters, and running O…![](https://blog.pixelmelt.dev/content/images/icon/cropped-avatar-270x270-07c3aaf5-ceea-4550-afd0-a31f16405155.jpeg)Terence Eden’s BlogTerence Eden![](https://blog.pixelmelt.dev/content/images/thumbnail/Likebook-Ares-239bcfa3-a591-466a-9e73-b323cd131b5e.png)](https://shkspr.mobi/blog/2025/10/improving-pixelmelts-kindle-web-deobfuscator/?ref=blog.pixelmelt.dev) [Capturing Kindle Web Reader pagesExploring how the Kindle Web Reader fetches DRM-protected content and how to capture the rendered pages automatically.![](https://blog.pixelmelt.dev/content/images/icon/web-app-manifest-512x512-b986db15-7350-41fd-a0cd-29ca1802ada2.png)√(noham)² | NohamNoham![](https://blog.pixelmelt.dev/content/images/thumbnail/blob.4571f1bc02b1aebb9962f4af1c8d4495-dd8ce95f-5d7f-4207-9ebf-1a1da37a2871.png)](https://noh.am/en/posts/kindle-web-reader-pages/?ref=blog.pixelmelt.dev) ### Building The Language Model Nobody Asked For URL: https://blog.pixelmelt.dev/building-the-language-model-nobody-asked-for/ Last updated: 2025-08-27T02:16:53.000Z I lied in the title, well, sort of. Nobody explicitly asked for this but the signs were always there. ### LLMs don't really serve the user *Have you ever really watched someone interact with an LLM for the first time?* They might try a few questions to see how smart it is, hooked on the promise that it is a new, somehow better, source of information. Then when the model regurgitates a few Wikipedia-esque emoji ridden paragraphs they will promptly get bored of it. ![](https://blog.pixelmelt.dev/content/images/2025/08/image-12.png) Then they try something different, something *funny*. They might ask something nonsensical or even mildly inappropriate, and what do they get in return? ![](https://blog.pixelmelt.dev/content/images/2025/08/image-4.png) Why wont you try anyways? Ok... Well how about a joke then? ![](https://blog.pixelmelt.dev/content/images/2025/08/image-5.png) They boiled a glass of water for this? The main issue with good ol' ChatGPT here is that it wasn't trained to be funny, it was trained to be intelligent, to win on benchmarks, to push the boundaries of AI as we know it. AKA trained upon boring, polite, sanitized, textbook like data. ## We can fix this. All we need to do is take an open weights LLM and add a bit of secret sauce to make it more interesting. But first, which LLM do we use? Our options are numerous but essentially boil down to 3: The Llama family of models from Facebook but... - Facebook is evil - Llama was recently announced to be going closed source - The most recent Llama model wasn't good The Qwen family of models from Alibaba but... - Known for their over-trained models - Often way too big for one person to run The Mistral family of models from MistralAI: ![](https://blog.pixelmelt.dev/content/images/2025/08/image-8.png) They're perfect The choice is obvious. ## Enter: Mistral We will be using Mistral Small 3 24B to create our new and improved LLM! It is able to be trained on a single 24gb NVIDIA 3090 consumer graphics card and is available publicly for download. Mistral Small 3 by default is already pretty good with generating human-like text, mostly due to not using synthetic data (meaning LLM's talking to LLM's). > Note that Mistral Small 3 is neither trained with RL nor synthetic data - [Mistral.ai](https://mistral.ai/news/mistral-small-3?ref=blog.pixelmelt.dev) Now all we need to do is find some data that will give our creation more spice, more hilarity, more humanity even. But what kind of data would we even use to make an LLM more interesting? Well when I think of what the opposite of corporate speak and overt correctness is, I think of a few different websites. ![](https://blog.pixelmelt.dev/content/images/2025/08/image-10.png) To get our coveted interesting data we will need to pull it from these websites, turn it into a chat like format, clean it up and then train! I'm keeping this part brief on purpose, I half explained how I trained a model in [this other post](https://blog.pixelmelt.dev/clone-your-friends/) but it became out of date very quickly, your best bet to learn how is to research it by yourself. Look up "Unsloth train QLORA" if you really want to do it the same way as me. ⁉️ Funnily enough, one of these sites had bot defenses set up but when I actually got down to scraping them it had been turned off for some reason. ## Lets see how they stack up against each other ![](https://blog.pixelmelt.dev/content/images/2025/08/image-11.png) I would say that response is much better then Chatgpt's it accurately describes Bill Nye, if the user wanted more information then they should look it up online rather then ask a corporate controlled hallucination machine! ## End results I am really happy about how this model came out, I previously trained a model on a discord server and while it did make the model a lot more chaotic, it also completely destroyed its brain. This new model is much smarter as well as being pretty funny. You can run it yourself if you have a newer gaming gpu or use a cloud gpu. ⚠️ Warning, this model is NOT nice, if you are sensitive to assorted internet meanness I do not recommend interacting with this model Model weights are available here in the raw [FP16 HF GGUF](https://huggingface.co/pixelmelt/Incelgpt-24B%5Fv1.1?ref=blog.pixelmelt.dev), or a [Q4 GGUF](https://huggingface.co/pixelmelt/Incelgpt-24B%5Fv1.1%5FQ4%5FK%5FM%5FGGUF?ref=blog.pixelmelt.dev) for people with less VRAM. ### Defeating DevTools Detection URL: https://blog.pixelmelt.dev/defeating-devtools-detection/ Last updated: 2025-09-12T16:06:04.000Z ### ⚠️ LibreWolf currently doesn't have `devtools-bypass.patch` enabled (the patch required for this to work) ⚠️ You can use this patcher I wrote which will modify your existing LibreWolf installation: [librewolf-hotpatcher.pyGitHub Gist: instantly share code, notes, and snippets.![](https://blog.pixelmelt.dev/content/images/icon/pinned-octocat-093da3e6fa40-7.svg)Gist262588213843476![](https://blog.pixelmelt.dev/content/images/thumbnail/gist-og-image-54fd7dc0713e-1.png)](https://gist.github.com/Ciarands/be182eb4f58ae1338715e03f91010c4d?ref=blog.pixelmelt.dev) (slightly dangerous, may break your librewolf) ### TL;DR You can't have a lot of features if you want a catch-all solution, DevTools are inherently pretty detectable, you need a patched browser to bypass. 1. Download [LibreWolf](https://librewolf.net/?ref=blog.pixelmelt.dev) which comes pre-patched. 2. In the search bar type `about:config` 3. Change the following settings: - `librewolf.console.logging_disabled` to `true` - LibreWolf patch which detaches console API - `librewolf.debugger.force_detach` to `true` - LibreWolf patch which force detaches debuggers - `devtools.toolbox.host` to `window` - Defeats common resize detection vector - `devtools.source-map.client-service.enabled` to `false` - Defeats common source mapping detection - `webgl.disabled` to `false` - Optional: Some extra evil sites detect this - `privacy.resistFingerprinting` to `false` - Optional: Some extra evil sites detect this You should have a fully undetectable browser (although primitive, its a good initial analysis environment). 🎉 --- ### Scenario You're browsing a website, and something is seriously irritating you. Maybe there's an annoying popup that takes you to a shady site when you click close? Could be a big annoying window saying "Agree to sell us your soul" (accept cookies). If you're more technical you probably figure you can just open up DevTools and delete whatever element is annoying you. You go to open up DevTools and immediately get redirected or even worse your entire page, browser, or even ***computer*** crashes. "What? How?" You ask yourself, and go searching for an answer. **Example of a callback which can crash a computer. (don't look at my system time ;-;)* --- ### Googling `"bypass devtools detection"` And How These Solutions Are Still Detected So you go to search for an answer, and are greeted with thousands of results. Lets take a look at the top 4 search results, each of these, while many good; are still vulnerable to a number of detection vectors due to the nature of how they're designed. Here's a simple check I made for this article which will detect all of these scripts. This detection follows a similar principle to timing-based detection's when logging a large amount of data using `console.table`: ```js function checkTimingDelay() { const error = new Error("Error for timing test"); const start = performance.now(); for (let i = 0; i < 35000; i++) { console.error(i, error.stack); // "i" prevents some browsers from folding errors causing it to have to render each time, // when devtools are open or "error" is tampered with, it will cause a significant difference in timing } const end = performance.now(); return end - start; } ``` ![](https://blog.pixelmelt.dev/content/images/2025/07/-4E85D44E-C097-4B8A-BEEB-4BC64FE5E820-.png) Default state of our detection **1) Bypass disable-devtool Github Gist** [Bypass disable-devtoolBypass disable-devtool. GitHub Gist: instantly share code, notes, and snippets.![](https://blog.pixelmelt.dev/content/images/icon/pinned-octocat-093da3e6fa40-4.svg)Gist262588213843476![](https://blog.pixelmelt.dev/content/images/thumbnail/gist-og-image-54fd7dc0713e.png)](https://gist.github.com/aravindanve/3e13d995fac35e4a07c236b11cc432c7?ref=blog.pixelmelt.dev) This is often your first search result result. However, if you're being detected by anything other than [*theajack's disable-devtool*](https://github.com/theajack/disable-devtool?ref=blog.pixelmelt.dev) this probably wont help. It is still very good for the specific script it targets, utilizing a clever solution with [*javascript url's*](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript?ref=blog.pixelmelt.dev): `javascript:DisableDevtool.isSuspend = true` **2) Bypass DevTools Detection UserScript** [Bypass DevTools DetectionPrevent websites from detecting DevTools (Inspect Element) is open![](https://blog.pixelmelt.dev/content/images/icon/blacklogo16-DftkYuVe.png)Cristion Kjeldgard (Frisk)![](https://blog.pixelmelt.dev/content/images/thumbnail/blacklogo96-CxYTSM_T.png)](https://greasyfork.org/en/scripts/534968-bypass-devtools-detection?ref=blog.pixelmelt.dev) This is a more recent UserScript, it should bypass most detection's you'll run into. However it will still immediately be detected by our check due to this offending snippet: ```js // Override console methods to disable traps and weird behaviors ['log', 'debug', 'error', 'info', 'warn'].forEach(method => { const originalMethod = console[method]; console[method] = function(...args) { // Skip logging if args include certain trap images or weird data (optional) for (const arg of args) { if (arg instanceof Image) return; } return originalMethod.apply(console, args); }; }); ``` The modification of the `error` method leads to a significant delay clearly signifying tampering has occurred. ![](https://blog.pixelmelt.dev/content/images/2025/07/-A870C155-C1A5-48FB-B576-CC9839CA9CAD-.png) Tampering detected. ![](https://blog.pixelmelt.dev/content/images/2025/07/-5229964C-F0C1-44A9-86B4-C82CD35D4A95-.png) Site functions normally when targeted function isn't being tampered with. It has a few more detection vectors and mistakes, such as attempting to no-op non existent properties: ```js // --- Override debugger to noop --- window.debugger = function() {}; ``` `debugger` is a reserved keyword; not a property or method of the `window` object, a `typeof` check would flag this. **3) Anti-anti debug extension** [Anti Anti Debug - Chrome Web StoreBe able to use developer tools again![](https://blog.pixelmelt.dev/content/images/icon/icon_144px.png)Chrome Web Store![](https://blog.pixelmelt.dev/content/images/thumbnail/QG2w9thm4entJ7WOxsmbQ89sCwGxbZS8h3fqb6uCtbrLNv-xc9gqvB-BKeh5n6NoZT10WhDEWcOXsMMrI4Mnu2G9-s128-rj-sc0x00ffffff)](https://chromewebstore.google.com/detail/anti-anti-debug/mnmnmcmdkigakhlfkcdimghndnmomfeo?ref=blog.pixelmelt.dev) This popular extension is good, but quite outdated and very vulnerable to a number of modern detection vectors. ![](https://blog.pixelmelt.dev/content/images/2025/07/-1894C05A-3A32-4CC6-BF41-EF29B78AC5AC-.png) Not flagged immediately with install. ![](https://blog.pixelmelt.dev/content/images/2025/07/-E5EAB7A4-0884-4F1F-9884-67DCE8C9B82B-.png) Flagged on DevTools open due to significant delay rendering 35k errors. **4) How to prevent websites from detecting the dev console? - Reddit** You may even see [Reddit posts](https://www.reddit.com/r/browsers/comments/rpvlpn/how%5Fto%5Fprevent%5Fwebsites%5Ffrom%5Fdetecting%5Fthe%5Fdev/?ref=blog.pixelmelt.dev), claiming its impossible! "Conclusion: **It is** ***NOT*** **possible**. This question has been asked for many years and no solution has yet been found that is not specific to the website." - u/coomerpile (😭) This isn't true, but a lot of the replies are unhelpful. --- ### A True Solution To Becoming Undetected You may think its hopeless, how are you even supposed to manually patch a malicious script if you're crashing before you can even add the override? What if its obfuscated and you can't easily override it? The problem is, we've been fighting on the wrong battlefield. All the solutions we've looked at; extensions and scripts, are trying to patch the problem from the inside. Like trying to stop a leak from inside a submarine with duct tape. The browser itself is telling on you. Every standard browser (Chrome, Firefox, Safari, ect) is built with developers in mind. APIs such as `console` are a deeply integrated property of the `window` object. There's honestly too much to consider when attempting to perfectly mirror the behavior of closed DevTools. For example, in recent history, a malicious script could define properties on certain objects, and if you opened DevTools, you would be detected. ```js const err = new Error(); Object.defineProperty(err, 'stack', { get() { console.log("You're detection. :( ") } }); console.log(err); ``` [https://datadome.co/threat-research/how-new-headless-chrome-the-cdp-signal-are-impacting-bot-detection/](https://datadome.co/threat-research/how-new-headless-chrome-the-cdp-signal-are-impacting-bot-detection/?ref=blog.pixelmelt.dev) This was finally fixed in [**d435f290e382f0296f647016cb5d70d54eca3fd8** ](https://chromium.googlesource.com/v8/v8/+/d435f290e382f0296f647016cb5d70d54eca3fd8%5E%21/?ref=blog.pixelmelt.dev#:~:text=commit%20d435f290e382f0296f647016cb5d70d54eca3fd8,diff)(April 4, 2024) However this doesn't by any means mean that this form of attack is closed, this specifically has been solved, but there are still a number of properties that are accessed in such a way that they provide a reliable detection vector. The only real solution is to use a browser where these "features" are either fundamentally broken or completely ripped out. We need a browser that has been patched before it was even compiled. --- ### LibreWolf Enters For those unfamiliar, LibreWolf is a fork of Firefox which is hyper-focused on privacy and security. It comes pre-packaged with a bunch of modifications and patches designed to prevent tracking and fingerprinting. First, go and download it here: [LibreWolf BrowserA custom version of Firefox, focused on privacy, security and freedom.![](https://blog.pixelmelt.dev/content/images/icon/icon.svg)LibreWolf![](https://blog.pixelmelt.dev/content/images/thumbnail/og_image.png)](https://librewolf.net/installation/?ref=blog.pixelmelt.dev) ### Configuration We need to tweak some of LibreWolf's settings. To do this, we'll use Firefox's configuration editor. In the search bar, type `about:config` and press Enter. You'll see a warning page. Click "Accept the Risk and Continue" ![](https://blog.pixelmelt.dev/content/images/2025/07/-C73FFE45-7FF9-428D-AA91-4EFEEB085337-.png) You're going to want to use the search bar at the top of this page to find and change the following settings. **1) Stop Handling Console API** - **Setting:** `librewolf.console.logging_disabled` - **Change to:** `true` This is a custom patch included in LibreWolf that guts the console. It modifies `onConsoleAPILogEvent` in `resource://devtools/server/actors/webconsole/listeners/console-api.js` adding this check: ![](https://blog.pixelmelt.dev/content/images/2025/07/image-2.png) Internally this will act as if there is no handler for a received object from the `ConsoleAPIStorage` service. In the eyes of the `console` API this *should* perfectly mimic the behavior of not having DevTools open at all. **2) Force-Detach Debuggers** - **Setting:** `librewolf.debugger.force_detach` - **Change to:** `true` Another LibreWolf-specific patch. It modifies `attach` in `resource://devtools/server/actors/thread.js` adding this check: ![](https://blog.pixelmelt.dev/content/images/2025/07/image-2-1.png) Internally, this will hit the same early return condition that `this.alreadyAttached` triggers, blocking all detection vectors from the Debugger tab (another surface for a timing-based side-channel attack) and preventing the `debugger` keyword from being handled, avoiding all debugger traps. ```js const start = performance.now(); debugger; const end = performance.now(); if (end - start > 100) { triggerAntiTamper(); } ``` Example of a debugger trap. ![](https://blog.pixelmelt.dev/content/images/2025/07/Serving-Up-Tycoon-2FAs-Secrets-Figure11.png) Phishing webpage malware sample which abuses debugger traps as an anti-analysis technique, redirecting on detection. Credit: [https://www.esentire.com/blog/phish-chips-serving-up-tycoon-2fas-secrets](https://www.esentire.com/blog/phish-chips-serving-up-tycoon-2fas-secrets?ref=blog.pixelmelt.dev) **3) Defeat Resize Detection** - **Setting:** `devtools.toolbox.host` - **Change to:** `window` One of the oldest tricks in the book. When you open DevTools, it's typically docked to the side or bottom of your browser, which changes the size of the main viewport. Scripts can have a listener that just waits for the window to be resized. If it resizes past a certain threshold instantly, it’s a dead giveaway that DevTools are open. By changing this setting from `side` to `window`, you tell the browser to open DevTools in a completely separate window. This way, the original webpage's viewport size never changes. **4) Defeat Source Mapper** - **Setting:** `devtools.source-map.client-service.enabled` - **Change to:** `false` Source maps are supposed to help developers debug minified or transpiled code by mapping it back to the original source. Some anti-devtools scripts work by requesting a non-existent source map file. When you open DevTools, the browser automatically tries to fetch this file, and the website's server logs the request, flagging you instantly. By disabling this, your browser won't try to fetch those files. [Javascript Anti Debugging - Abusing SourceMappingURLAbusing SourceMappingURL feature can allow attackers to create one of the strongest Cross Browsers Javascript Anti Debugging techniques![](https://blog.pixelmelt.dev/content/images/icon/apple-touch-icon-144-precomposed.png)Gal WeizmanGal Weizman![](https://weizman.github.io/content/img/previews/)](https://weizmangal.com/page-js-anti-debug-1/?ref=blog.pixelmelt.dev#:~:text=,before%20we%20start) ### Optional Tweaks for Extra Evil Sites The settings above will get you past anti-DevTools scripts. However, some particularly nasty sites use broad fingerprinting techniques which can cause LibreWolf itself to become detected. Some of LibreWolf's defaults can sometimes be a bit too unique. If you're still having trouble, consider relaxing these two settings. - **Setting:** `webgl.disabled` - **Change to:** `false` - **Why:** LibreWolf disables WebGL by default for privacy. However, so few users have it disabled that its absence can be a fingerprint. Enabling it makes you look more like a standard user. - **Setting:** `privacy.resistFingerprinting` - **Change to:** `false` - **Why:** This is Firefox's built-in "Tor-Uplift" mode. It standardises many things to make you blend in with other privacy-conscious users. Ironically resisting fingerprinting has become a fingerprint itself. Disabling this can, paradoxically, make you less conspicuous. --- ### Celebrate Being Undetected! 🎉 Test your detection here: [devtools-detector demo![](https://blog.pixelmelt.dev/content/images/icon/faviconV2)](https://blog.aepkill.com/demos/devtools-detector/?ref=blog.pixelmelt.dev) Even our demo cant catch us! 🎉 --- ### Contact If you find a site which manages to avoid all this and still detects your DevTools, I'd love to hear about it, reach out to me at: [ciaran@exploit.cat](mailto:ciaran@exploit.cat) ### Attacking a stack based JavaScript virtual machine URL: https://blog.pixelmelt.dev/analysing-pistoljsvm/ Last updated: 2025-07-16T18:23:53.000Z I was asked by my friend [@PixelMelt](https://github.com/PixelMelt?ref=blog.pixelmelt.dev) to help out with a research project he was working on, focused on the analysis of a few JavaScript obfuscators. I happily agreed to take part and was asked to analyse one of three different obfuscators: 1. **Obfuscator.io**: Likely the most common JavaScript obfuscator. 2. **JS-Confuser:** A newer obfuscator with some nice techniques. 3. **PISTOL:** PixelMelt's JavaScript Virtualisation Obfuscation. I chose to go with PISTOL as I am already familiar enough with the others, and VM based obfuscation is much more interesting than transformation obfuscation. --- ## Preface: ``` You are going to be given 3 JavaScript samples to attempt to retrieve a flag from, easy, medium, and hard. You will stop after attempting to retrieve the flag from each sample either after 120 minutes pass, or you find the flag. The flag is a string in the format “FLAG{}”. Once you complete each test you will fill out the following form with your results. You should start with the easiest sample and work up to hard. You will have until June 28th to submit the results. ``` Pix did also make it clear that the primary goal of this study was not just to successfully reverse the samples, but to gather useful and thoughtful feedback on the challenges and techniques used. The analysis and insight provided being more important than just recovering the flag. With this information I set out downloading the PISTOL samples `1.0.js`, `2.o.js` & `3.o.js`, then writing down a few goals I had before starting. --- ## Follow along: If you would like to follow along with this post or attempt to solve the samples yourself beforehand (little competition between us lol) you can find the samples and my full solutions here: [GitHub - Ciarands/PistolAnalysis: Challenges and solutions for a CTF-style JSVM reversing research studyChallenges and solutions for a CTF-style JSVM reversing research study - Ciarands/PistolAnalysis![](https://blog.pixelmelt.dev/content/images/icon/pinned-octocat-093da3e6fa40-3.svg)GitHubCiarands![](https://opengraph.githubassets.com/edfb06bcbdd307cfcdb8f371f6e3fab9c5027993877bd28cd121f7dcf54a1e63/Ciarands/PistolAnalysis)](https://github.com/Ciarands/PistolAnalysis/tree/main?ref=blog.pixelmelt.dev) --- ## Goals: 1. Complete all 3 challenges within the 120 minute time constraint 2. Reverse each sample properly but I also want to try and attack it in ways that I am maybe not expected to 3. We are supposed to be emulating an attacker, so thinking like an attacker I want to try the simplest possible attack vectors first 4. It will be funny if I can guess a flag :> --- ## `1.0.js`: **Swimming in the Constant Pool** After downloading the challenges, I saw what appeared to be [Terser](https://terser.org/?ref=blog.pixelmelt.dev) output, a popular aggressive JavaScript mangler and compressor. I ran the sample which revealed in its default state the program returns `false`, our goal is likely to make the program return `true`. Establishing this goal I skimmed the code, making some observations: - The sample abuses quirks such as invoking functions via JavaScript's backticks e.g. V\`\` - Around the bottom I noticed that what I assumed to be the VMs bytecode. It looked to be base64 encoded. - `module.check` at the very bottom seems to be what validates our flag, but `check` isn't defined anywhere. Using that information, we want to try and look for base64 related functions to determine the VM entry point as it will need to be decoded first. We also know that `module.check` must be registered by the VM, we maybe want to do some tracing to try and figure out where its created. Now we know what we're looking for, I started by doing a simple Ctrl \+ f search for "atob" and "base64" which didn't yield any immediate results. So I decided to look a bit closer, searching for logic which contained functions that are commonly used for string manipulation and came across 2 functions. #### Function 1: ```js R = ((A, e = 0) => { for (l = A[E], n = (3 * l + 1) >> 2, a = e ? (n % 1 ? -~n : n / e) * e : n, f = new w(a), i = 0, _ = 0, s = 0; s < l; s++) if (((p = 3 & s), (i |= ((r = A[s].charCodeAt(0)), ((r > 64) & (r < 91) ? r - 65 : (r > 96) & (r < 123) ? r - 71 : (r > 47) & (r < 58) ? r + 4 : 43 == r ? 62 : 47 == r ? 63 : 0) << (6 * (3 - p)) )), 3 == p || l - s == 1)) { for (d = 0; d < 3 && _ < a; d++, _++) f[_] = (i >>> ((16 >>> d) & 24)) & 255; i = 0; } return f; })(e) ``` This is the Base64 decoder, it's manually implemented but has the same character-to-value lookup table as standard b64 implementations. Ctrl \+ clicking on the identifer **R** reveals that it is passed to an IIFE, which just so happens to be our second function. 👀 #### Function 2: ```js x = ((A) => { let e = 0; for (; 35 != A[e] || 74 != A[e + 1] || 13 != A[e + 2] || 91 != A[e + 3]; ) e++; e += 4; let t = new c(A[U][b](e, e + 2))[0], r = new g(A[U][b](e + 2, e + 6))[0]; e += 6; let l = new c(A[U][b](e, e + 6 * t)), n = {}; for (let A = 0; A < l[E]; A += 3) n[(l[A + 2] << 16) | l[A + 1]] = String.fromCharCode(l[A]); e += 6 * t; let o = '', a = 0, f = 0, i = 0; for (let t of A[b](e)) for (let A = 7; A >= 0 && i < r; A--) { (a = (a << 1) | ((t >> A) & 1)), f++, i++; let e = (a << 16) | f; n[e] && ((o += n[e]), (a = 0), (f = 0)); } return o.split('|~|'); })(R) ``` It's pretty clear this function is doing some more manipulation on our now-decoded bytecode, and notably returning a string split at the separator `|~|`. This really stood out to me, so I added a `console.log` to figure out what it was. Running the program a second time the console outputted: ``` VM62:96 FLAG{l3vel-0ne}|~|check VM62:96 FLAG{l3vel-0ne}|~|check false ``` Solving the challenge with the first try with the flag living in the constant pool.🎉 **|** Time taken (HH-MM): **00:05** --- ## `2.o.js`: **Chasing a Rolling Key** ## Since the flag was so simple, in the first challenge and the constraints of the challenge allow for flag retrieval "by any means necessary". I followed the "Keep it Simple, Stupid" (KISS) principle, I attempted to run it again with a guessed flag. The previous flag was `FLAG{l3vel-0ne}`, so a logical guess was `FLAG{l3vel-Tw0}`. ### Picking at the Seams Glancing over the code once again, it looked virtually identical to the previous sample, so I decided to once again inspect the constant pool, outputting: `length|~|charCodeAt|~|check` This suggested the program's logic would: 1. `length` \- Perform a length check on our input. 2. `charCodeAt` \- Iterate through the character codes of the flag. 3. `check` \- Register the check function. Another "Find Usages" on the IIFE revealed it was only called by a single instruction. Pulling up the chrome debugger and breakpointing this instruction revealed the first few operations: - The VM accessed `check` to create the `module.check` function, just as it had before. - It indexed length and the program terminated. My assumption was that my empty input flag was failing the length check immediately. With this info I had two paths forward: - Bruteforce the length by trying different inputs and counting instructions executed. - Inspect the stack to see if the correct length was ever directly compared. I chose the latter, as it would likely reveal more information. ### A Glimmer in the Stack Upon inspecting the stack, before any length-related logic even executed, something caught my eye; a newly created array of numbers. `[207, 130, 193, 133, 250, 206, 188, 200, 237, 148, 255, 134, 254, 222, 226, 144]` My first instinct was to map these to characters, but the result was a jumble of unprintable data: `Ï\x82Á\x85úμÈí\x94ÿ\x86þÞâ\x90`. This screamed "encrypted data." I mentally tagged it as the `encrypted_array` and kept digging. Just after the `length` string was pulled from the constant pool, two numbers were pushed to the stack. The second was 16 the exact same length as my `encrypted_array`. The pieces were starting to click. I set my input flag to a dummy value of the correct length, `FLAG{xxxxxxxxxx}`, and re-ran the debugger. Stepping through I saw the VM performed a strict not-equal (!==) check between my flag's length and 16, then took the logical NOT of the result (false). This inverted logic meant my flag of length 16 passed the first check. 🎉 ### Unraveling the Cipher My working theory was that the program XORed the character codes of my input against the `encrypted_array`. So I tried to find any XOR related opcodes and placed breakpoints on them. My assumption was close, but slightly off. Stepping through the execution revealed the following sequence for the first character, 'F': 1. The flag `FLAG{xxxxxxxxxx}` and `charCodeAt` are popped from the stack to get the character code of the character at index 0. ``` <- POPPED: FLAG{xxxxxxxxxx} <- POPPED: charCodeAt <- POPPED: 0 -> PUSHED: 70 // charCode for 'F' ``` 1. This character code is then XORed with `137`. I'd overlooked it before, but 137 was pushed to the stack right before the `encrypted_array`. This was our initial `key`. ``` <- POPPED: 70 <- POPPED: 137 -> PUSHED: 207 ``` 1. This result, `207`, is then XORed against the character's index in the string (0). ``` <- POPPED: 207 <- POPPED: 0 -> PUSHED: 207 ``` 1. Finally, this result is compared against the first element of our `encrypted_array`. So, `encrypted_array` was really `validation_array`. Following the execution for the second character, 'L' which was accessed sequentially, revealed the key wasn't static; it was a **rolling key**. ``` <- POPPED: 76 <- POPPED: 207 -> PUSHED: 131 ``` The character code for 'L' (76) is retrieved, then it's XORed against the result of the previous operation. This result now serves as the key for the current character. ### Reversing the final algorithm The full validation logic was essentially doing: ```js result = (key ^ char_code) ^ index; is_valid = (result === validation_arr[index]); ``` Since XOR is a reversible operation we can do the exact opposite to generate our flag: ```js char_code = (validation_array ^ index) ^ key; ``` Mapping the result to a string, revealed the final flag: `FLAG{1ts-part-2}`. 🎉 **|** Time taken (HH-MM): **00:17** --- ## `3.o.js`: When Tracers Lie ## The third challenge began with familiar routine, with the script once again looking very similar to the previous samples. Running the script revealed another length check, this time for `20` characters, using the same strict not-equal (!==) comparison as before. I padded my input to the correct length, then started my stack inspection. This is where the routine broke down. The volume of data being pushed and popped was overwhelming; it was impossible to follow the logic mentally. I needed to see the machine's instructions, not just its data. I decided to pivot and build a naive tracer by asking Google Gemini to inject console.log statements for each opcode. In retrospect, I should have done this myself... :/ The trace worked, but it produced over `18,000` lines of output. While still massive, it was a structured view of the program's execution and we could hunt for patterns. ### A Fingerprint: Key-Scheduling Algorithm Almost immediately, a repeating block of operations at the very beginning of the trace caught my eye. ```js [PUSH_CONST] 256 [GET_CLOSURE_VAR] id: 12 [LT] 256 < 0 [BRANCH_IF_FALSE] cond: true target: 553 [PUSH_CONST] 257 [GET_CLOSURE_VAR] id: 12 [GET_CLOSURE_VAR] id: 11 [GET_CLOSURE_VAR] id: 9 [MUL] 6 * 251 [ADD] 0 + 1506 [MOD] 257 % 1506 [SET_CLOSURE_VAR] id: 9, scope: 0, value: 221 ... [INC] 0 + 1 [SET_CLOSURE_VAR] id: 12, scope: 0, value: 1 ... [JUMP] target: 350 ``` This pattern of `MUL` \-> `ADD` \-> `MOD` was executing inside a loop that incremented from 0 to 255\. Reconstructing this into a high-level representation: ```js // Closure variables let S = []; // v7 let j = 251; // v9 (initial value from first iteration) const C1 = 6; // v11 (constant value from first iteration) // Loop from i = 0 to 255 for (let i = 0; i < 256; i++) { // Update state variable 'j' j = (i + C1 * j) % 257; // Store the new 'j' in array 'S' at the current index 'i' S[i] = j; } ``` My pattern-recognition alarms went off. This reminded me of a modified version of the Key-Scheduling Algorithm (KSA) from the RC4 stream cipher. The program was initialising a state array, or S-box, for a symmetric encryption scheme. ``` for i from 0 to 255 S[i] := i endfor j := 0 for i from 0 to 255 j := (j + S[i] + key[i mod keylength]) mod 256 swap values of S[i] and S[j] endfor ``` RC4 Psudeocode for reference ### Sanitising the S-Box After this first loop, the `KSA` array was fully populated but contained duplicate values. The program entered a second 256-iteration loop. ```js [PUSH_FALSE] [PUSH_CONST] "fill" [PUSH_CONST] 256 [PUSH_CONST] "Array" [GET_GLOBAL] d[Array] [NEW] constructor: 256 args count: 1 [CALL_PROP] obj: false prop: fill args count: 1 [SET_CLOSURE_VAR] id: 14, scope: 1, value: (256) [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, …] [POP_MULTI] count: 1 [PUSH_CONST] 256 [PUSH_CONST] "Array" [GET_GLOBAL] d[Array] [NEW] constructor: 256 args count: 1 [SET_CLOSURE_VAR] id: 16, scope: 1, value: (256) [empty × 256] [POP_MULTI] count: 1 [PUSH_CONST] 0 [SET_CLOSURE_VAR] id: 18, scope: 1, value: 0 [POP_MULTI] count: 1 [PUSH_CONST] 0 [SET_CLOSURE_VAR] id: 19, scope: 1, value: 0 ``` This section initialises some new variables: - `id: 14`: An array of 256 `false` values. Let's call this `unconfident_array`. - `id: 16`: A new empty array of size 256\. Let's call it `KSA2`. - `id: 18`: A counter initialised to 0\. Let's call this `fallback_counter`. - `id: 19`: Another counter initialised to 0\. This is the main loop counter. Tracing its logic revealed a sanitisation process: - It iterates through the initial `KSA`. - For each value `v = KSA[i]`, it checks if `v` has been seen before using the `unconfident_array`. - If `v` is new, it's placed into a second array, `KSA2`, at the current index `i`, and marked as "seen". - If `v` is a duplicate, the program uses the `fallback_counter` to find the next available integer (from 0-255) that hasn't been used, places that into `KSA2[i]`, and marks it as seen. The goal of this entire process was to create `KSA2`: a valid permutation where every number from `0-255` appears exactly once. This was the final, clean S-box for the cipher. ### The Cipher's Core With the key schedule established, the final validation loop began. ```js [SET_CLOSURE_VAR] id: 21, scope: 1, value: 83 [CREATE_ARRAY] with 20 items from stack [SET_CLOSURE_VAR] id: 23, scope: 1, value: (20) [85, 39, 30, ...] [SET_CLOSURE_VAR] id: 25, scope: 1, value: 83 ``` It initialised a state variable (id: 21) to `83` and loads a hardcoded 20-byte array, which I tagged `target_data`. This was the ciphertext my input needed to produce. For each character of my 20-character input, the VM performed the following steps: 1. Calculate an index into the S-box: `x = state ^ i ^ charCodeAt(i)` 2. Get the resulting keystream byte: `result = KSA2[x]` 3. Check its valid: `is_valid = (result === target_data[i])` 4. Update the state for the next round: `state = result` ### Reversing the final algorithm and a Painful Twist Since the entire cipher was based on XOR and S-box lookups, it was reversible. To find the original character code, I just needed to work backward: 1. For a given `target_data[i]`, find the index `j` in `KSA2` such that `KSA2[j] == target_data[i]`. 2. This `j` is the result of the initial `XOR` chain: `j = state ^ i ^ charCodeAt(i)`. 3. Solve for the unknown: `charCodeAt(i) = state ^ i ^ j`. 4. Remember that the state for the next iteration is simply the current `target_data[i]`. I wrote a solver script to perform the KSA generation and then run this decryption logic... Which gave me the flag `FLAG{chster-thr2e2}`... ...what? ```js // This array (closure var 7) is generated by the first major loop const initial_ksa = [ 221, 42, 254, 242, 171, 3, 24, 151, 143, 96, 72, 186, 100, 99, 94, 65, 149, 140, 87, 27, 182, 85, 18, 131, 39, 2, 38, 255, 16, 125, 9, 85, 28, 201, 212, 22, 168, 17, 140, 108, 174, 57, 127, 34, 248, 248, 249, 256, 42, 44, 57, 136, 97, 121, 9, 109, 196, 205, 3, 77, 8, 109, 202, 247, 4, 89, 86, 69, 225, 134, 103, 175, 94, 123, 41, 64, 203, 10, 138, 136, 125, 60, 185, 165, 46, 104, 196, 235, 213, 82, 68, 242, 2, 105, 210, 70, 2, 109, 238, 242, 10, 161, 40, 86, 106, 227, 183, 177, 142, 190, 222, 158, 32, 48, 145, 214, 115, 36, 77, 67, 8, 169, 108, 0, 124, 98, 200, 42, 123, 96, 192, 255, 120, 82, 112, 36, 95, 193, 11, 205, 85, 137, 193, 16, 240, 43, 147, 1, 154, 45, 163, 101, 244, 75, 90, 181, 214, 156, 66, 41, 149, 27, 67, 51, 213, 158, 86, 169, 154, 65, 46, 190, 27, 78, 128, 172, 180, 229, 10, 239, 72, 99, 5, 213, 177, 219, 215, 192, 55, 5, 220, 226, 6, 229, 26, 94, 246, 131, 213, 192, 67, 89, 222, 250, 162, 149, 72, 125, 187, 46, 229, 43, 213, 206, 165, 177, 250, 175, 240, 117, 151, 99, 45, 236, 98, 42, 221, 11, 37, 194, 109, 114, 145, 75, 170, 227, 56, 59, 78, 193, 113, 148, 102, 84, 234, 107, 117, 178, 31, 178, 33, 192, 119, 196, 145, 97, ]; const KSA2 = new Array(256); const unconfident_array = new Array(256).fill(false); let fallback_counter = 0; for (let i = 0; i < 256; i++) { const value_from_ksa = initial_ksa[i]; if (!unconfident_array[value_from_ksa]) { KSA2[i] = value_from_ksa; unconfident_array[value_from_ksa] = true; } else { while (unconfident_array[fallback_counter]) { fallback_counter++; } KSA2[i] = fallback_counter; unconfident_array[fallback_counter] = true; } } // The target ciphertext array (closure var 23) const target_data = [ 85, 39, 30, 68, 77, 34, 203, 235, 240, 137, 195, 215, 230, 71, 201, 110, 203, 226, 129, 216, ]; // correct array // [85,39,30,68,77,34,203,232,240,137,195,215,230,71,201,110,203,226,129,216] // The initial state (closure var 21) const initial_state = 83; let flag = ""; let state = initial_state; target_data.map((keystream_byte, i) => { const j = KSA2.indexOf(keystream_byte); // Original: j = state ^ i ^ charCode // Reversed: charCode = state ^ i ^ j const charCode = state ^ i ^ j; flag += String.fromCharCode(charCode); state = keystream_byte; }) console.log("Recovered Flag:", flag); ``` solve-flag.js Not being weary of how much time I had left I started re-analysing. I thought I must have missed some novel technique that was influencing the `target_data` somehow. The `target_data` array was the perfect length for our flag, but the flag string result `FLAG{chster-thr2e2}` was `19` characters, we needed `20`? After trying and failing to understand where I had went wrong, I finally noticed I had ran out of time :( and just guessed the flag: "chster"? That had to be "chapter." Our flag `FLAG{ch4pter-thr2e2}`, passed in the original VM but not my traced version? I had the flag, but the fact that it only worked in the untampered VM revealed that the issue was with my own tooling. ### Postmortem The most frustrating part of this challenge was this near-miss. My logic was fine but the solver seemingly for no reason produced a bad result. The AI-generated trace was the culprit. In its task to add logging into the VM's instructions, Gemini had made a single, almost imperceptible change. It had converted one character in the Base64-encoded bytecode from an `'A'` to a `'Y'`. - 'A' in Base64 is index 0 (6-bit `000000`). - 'Y' in Base64 is index 24 (6-bit `011000`). This two-bit flip cascaded through the decoding process, corrupting a single byte in the hardcoded `target_data` array, arguably the worst thing it could have broken. The value `232` became `235`. My reliance on an LLM for a "simple" but critical task introduced a bug that was nearly impossible to spot, leading me down a rabbit hole of debugging my already correct logic. This experience was a harsh lesson in trusting black-box tools: the blame lies with me for not validating the output. :/ **|** Time taken (HH-MM): **01:00** --- ## Did I achieve my goals?: | Goal | Success | | ---------------------------------------------- | ------------------- | | Complete all challenges within time constraint | ❌ | | Attack in novel way | ✅ | | Simple attack vectors | ✅ | | Guess a flag | ✅ \*not how I hoped | --- ### Thanks Big thank you to [@PixelMelt](https://github.com/PixelMelt?ref=blog.pixelmelt.dev) for providing the opportunity to take part and the fun challenge! :> ### Recursive vs Linear JSVM Disassembly URL: https://blog.pixelmelt.dev/recursive-vs-linear-jsvm-disassembly/ Last updated: 2025-09-22T16:29:05.000Z ## Linear disassembly Your disassembler goes through decoding every opcode in the bytecode linearly. Here is an example of where linear disassembly works ### VM Bytecode: 0, 4, 0, 6, 1 ### key generated or created by attacker: 0 = op\_number, 1 subsequent num argument 1 = op\_add ### Step through bytes linearly to disassemble: op\_number, 4 op\_number, 6 op\_add --- Simple, but If the VM changes things around during runtime, like opcode IDs or how instructions are decrypted, just going through instruction by instruction will not work since there are bytecode modifiers that change how the bytecode is interpreted at runtime. Linearly decoding a VM that does this runtime modification would lead to an invalid disassembly because linearly moving through the bytecode would not preform the bytecode modification operations in the right order. That is when you are forced to use ## Recursive disassembly Your disassembler follows jumps and if statements while decoding every opcode. Here is an example of where recursive disassembly works ### VM Bytecode: 0, 4, 9, 3, 5, 11, 12, 8, 3, 9, 4 ### With incrementation removed (for clarity) 0, 4, 9, 3, 2, 8, 9, 5, 0, 6, 1 ### Key generated or created by attacker: 0 = op\_number, 1 subsequent num argument 1 = op\_add 2 = op\_jump, 1 subsequent num argument 9 = op\_add\_n\_to\_op\_ids, 1 subsequent num argument ### Step through bytes recursively to disassemble: op\_number, 4 op\_add\_n\_to\_op\_ids, 3 (every opcode's id from here on is incremented by 3) op\_jump, 8 (this skips over the second op\_add\_n\_to\_op\_ids, if we counted that then all subsequent ops would be wrong, it is essentially dead code) op\_add\_n\_to\_op\_ids, 5 (not executed) op\_number, 6 op\_add --- > "I'd prefer linear when it's possible because you get everything there is, and recursive might skip something etc. But sometimes you have to use a recursive approach, because they either put random bytes after a jump so the disasm just breaks or they change their opcode order after a jump" - [Kian](https://github.com/xkiian?ref=blog.pixelmelt.dev), SneakerDev Discord ![](https://blog.pixelmelt.dev/content/images/2025/07/image.png) Google/Botguard Disassembly - CSolver, SneakerDev Discord You can do linear disassembly for ~~Kasada~~(No longer possible), but not for Google/Botguard since > "They bring in new opcodes during runtime which are reconstructed into strings then eval'd for usage, and modify both the encryption keys & the seed which have to match with the byte position. They use keys/seed to determine the next byte, so when changed, the control flow changes with it. That would be a prime example for when linear would not work." - CSolver, SneakerDev Discord This changing of bytecode during runtime forces any reverse engineer attempting a disassembly to take a more active approach to it. Slightly unrelated but here's some other interesting information about Google/Botguard. > "The anti debug feature is easily one of the hardest parts. They hide it very well while having a couple anti debug functions easily visible. So you would think u removed all anti debug and patched it, but there will still be more. They also have integrity checks for modifications. and they override a lot of prototypes so logging variables changes things in the program. their timing checks are very precise too" - CSolver, SneakerDev Discord > "for example if they override .toString and you try to log a variable then it executes another function which modifies the variable" - CSolver, SneakerDev Discord ### A Clever (but not so good) Scraper Protection URL: https://blog.pixelmelt.dev/a-clever-but-not-so-good-scraper-protection/ Last updated: 2026-09-08T14:12:40.000Z Here is an interesting anti scraping measure that was pointed out to me by a [reddit post](https://www.reddit.com/r/webscraping/comments/1kycbg3/how%5Fto%5Fovercome%5Fthis/?embed%5Fhost%5Furl=https://blog.pixelmelt.dev/p/0377a158-9ea1-4235-9cee-2ef891077a87/&ref=blog.pixelmelt.dev) recently. Take a look at this displayed text and then the HTML content for that exact same text. ![](https://blog.pixelmelt.dev/content/images/2025/06/image.png) Text content ![](https://blog.pixelmelt.dev/content/images/2025/06/image-1.png) HTML content The HTML appears to be a random mess of characters, this must be some JavaScript trickery right? Nope! Even when disabling JavaScript on the web page the text still renders as readable content and the HTML remains jumbled! So how does this protection work? Well if you take a closer look you might notice ![](https://blog.pixelmelt.dev/content/images/2025/06/image-2.png) Sneaky This class must be doing something to the text that causes the seemingly random letters to appear readable, and yep, the class "jum" is applying the font "OpenSans-Jmbld" to the text. If we download this font file and open it up in [FontForge](https://fontforge.org/en-US/?ref=blog.pixelmelt.dev) we can see that all of the letters have been re mapped to other letters! ![](https://blog.pixelmelt.dev/content/images/2025/06/image-4.png) This is how they are displaying the correct text! Now it becomes more interesting if you think about how you would go about bypassing this. You'd just need to manually go through and create a map for every letter right? But what if the website decided to start randomly generating a different jumbled font on *every page load*?Not as simple anymore is it? How can you programmatically determine what visual letter each character represents? Fonts aren't easily parsable, if we take a look at what the letter R has been mapped to, its in an SVG like format! ![](https://blog.pixelmelt.dev/content/images/2025/06/image-5.png) How do you parse an image? Now you could go about bypassing this new automatically randomized font by creating a mapping from letters to image representations of letters and then rebuild the character mapping that way. But what if your target is even more clever? They realize you have a mapping and decide to add micro variations to their fonts. (ex: what if the top right of that N was 1 unit wider?) This ruins your mapping since you cant 1:1 match letters anymore, so what are you left with? Your only other options are to give up and start wasting lots of resources rendering and using Optical Character Recognition (OCR) on the webpage, or if you think you have it in you, start making your character mapping builder even more complex by adding AI or fuzzy searching for characters within specific bounds. I have to note though, the site that this was noted as being on removed it soon after, I assume their jumbled text, while interesting, did not do very well when fed to Googles web crawler for search ranking. ## Related [Scraper protection without javascript![](https://blog.pixelmelt.dev/content/images/icon/faviconV2-b35d1088-6307-45fa-afc0-8767936e6a9c)![](https://blog.pixelmelt.dev/content/images/thumbnail/umatrix-faz-6731985c-752a-4bf3-be42-35b871ca6a4f.png)](https://defgsus.github.io/nn-experiments/html/posts/2025/10/26/scraper-protection.html?ref=blog.pixelmelt.dev) ### Try-Catch Control Flow Obfuscation in JavaScript URL: https://blog.pixelmelt.dev/try-catch-control-flow-obfuscation/ Last updated: 2026-08-15T01:36:15.000Z > "a lot of vm's i worked on in the past would also exploit errors to force a catch execution to continue" - Draco ## Error-Driven Control Flow Obfuscation JavaScript exception handling can be exploited to create non-linear code flow that confuses analysis. Given you probably don't want to make the fact that the function or operation you called is always going to throw an error, as that makes it easier for attackers to identify that you're doing this. ```javascript try { // code that appears to be the main execution path // but is deliberately designed to throw an error undefinedFunction(); // forces immediate jump to catch // this code is never executed, go wild } catch (e) { if (e instanceof ReferenceError) { // actual intended functionality executeRealFunctionality(); } } ``` ### Example: ```javascript function foo(input) { let stage = 0; try { // set state before error stage = 42; // force error after setting state if (stage === 42) { nonExistentFunction(); // ReferenceError } // never executes return false; } catch (e) { // only executes if the try block ran first and set the stage if (stage === 42) { // this is the actual payload return input === "secret_value"; } // if catch is accessed directly (bypassing try), this runs instead return false; } } foo("abc") // false foo("secret_value") // true ``` ### Chain multiple try-catch blocks to create complex execution paths: ```javascript try { throw {stage: 1}; } catch (e1) { if (e1.stage === 1) { try { throw {stage: 2}; } catch (e2) { if (e2.stage === 2) { // Execution only reaches here through specific error chain console.log("foo"); } } } } ``` ### Overview of JavaScript Virtualization Obfuscation URL: https://blog.pixelmelt.dev/overview-of-javascript-virtualization-obfuscation/ Last updated: 2025-07-07T18:04:54.000Z ## The Nature of Code "Protection" Given that no protection is impossible to break by nature of how computers work. (they must be able to see instructions to execute them) All you can really do is abstract the way the program runs. ## Abstraction by Virtualization One way to abstract any given program is to virtualize the code, essentially (re)compiling the input code and creating a fantasy CPU architecture implemented in your language of choice to run the compiler output. If you know how Java has its [Java Virtual Machine](https://www.geeksforgeeks.org/jvm-works-jvm-architecture/?ref=blog.pixelmelt.dev) so that any Java code written can be run on every CPU architecture that the JVM supports then you will likely get what we are doing here. As it turns out its a lot harder to read a binary file then JavaScript's interpreted text based syntax. So we take that text and turn it into a binary that will run on our fantasy CPU. Web protections are usually easier to crack than binary protection since binaries have a more established security scene, while browser based code protection is relatively newer. Hardware protections like TPUs present much greater challenges than our sandboxed JavaScript that can be easily analyzed and debugged. Here is a really basic overview of how this is implemented in JavaScript, here you can see a program being run on a virtual instruction set: ```javascript let program = [1, 1, 1, 2, 2] let datapointer = 0 let memory = [] while(datapointer != program.length){ switch(program[datapointer++]){ case 1: // Instruction 1: push the next number in the program to memory memory.push(program[datapointer++]) break; case 2: // Instruction 2: add the top two numbers in memory memory.push(memory.pop() + memory.pop()) break; } } ``` This program is semantically identical to ```javascript 1 + 2; ``` They both add 1 and 2 together. If you want to see a slightly more advanced implementation, check out my [JavaScript crackme's](https://github.com/PixelMelt/Javascript-Crackme?ref=blog.pixelmelt.dev). Just like executable programs, you can break the first two of those by monitoring the programs memory. That's all I got for now! ### Javascript obfuscation tricks URL: https://blog.pixelmelt.dev/javascript-obfuscation-tricks/ Last updated: 2025-07-07T18:06:35.000Z ## Functions reading themselves ### Comments are included when a function reads itself Which lets you include arbitrary string data inside a comment and then use it in your program. *Most* websites that disallow obfuscated code will likely not have automated tools that check for this. Some deobfuscators unknowingly remove comments, permanently breaking the reversed output (As they are missing vital pieces) Source: [doctor8296](https://github.com/MichaelXF/js-confuser/issues/151?ref=blog.pixelmelt.dev#issue-2640912992) ```js (function func() { // foo = "Y29uc29sZS5sb2coJ2hlbGxvLCB3b3JsZCEnKQ"