v2.1 - #78
Conversation
Serpensin
commented
Jun 22, 2026
- Replaced bytecode encoding with a polymorphic chunked loader using PRNG-based byte transformation.
- Reworked string encoding with per-string keys and multiple decode strategies.
- Added a new constant_encoding module for primitive and integer constant obfuscation.
- Replaced the old control-flow TODO wrapper with a state-machine style obfuscator.
- Added VM dispatch randomization and fake opcode handlers.
- Adjusted pipeline ordering so VM/bytecode combinations remain runtime-safe.
- Hardened target compatibility for Luau/GLua, including safer anti-tamper behavior.
- Fixed compressor output that produced Luau-invalid do;/then; style syntax.
- Increased speed for testing and generating examples
- Updated docs/version text to 2.1.0.
- Introduced a new module for constant encoding to obfuscate numeric and primitive constants. - Updated the manifest to include the new constant encoding preset and module. - Adjusted pipeline orders for existing modules to accommodate the new module. - Enhanced the VMGenerator to include randomized fake handlers for opcode dispatching. - Modified the anti-tamper module to remove debug keys for improved security. - Refactored the bytecode encoder to implement a more complex encoding scheme with randomization. - Improved the compressor to handle semicolon removal more effectively. - Revamped the control flow obfuscator to utilize randomized naming and fake blocks. - Updated the string encoder to enhance encoding methods and improve variable naming. - Added tests to ensure the new constant encoding module is properly integrated and functional.
zeusssz
left a comment
There was a problem hiding this comment.
overall good. only the changes i highlighted on specific files are something to note. lgtm otherwise
zeusssz
left a comment
There was a problem hiding this comment.
some of these changes dont necessarily apply, because of the pipeline, but since some presets dont use some modules, and we can also run with custom modules, it may be better to seal the gaps mentioned, as they wont cause much harm. after these, it can be merged.
There was a problem hiding this comment.
seen this, and i have a few questions. in line 98
local num = code:match("^(%d+)", pos)only handles decimal integers.
so something like
local x = 3.5would be left untouched,
since we aim to be compatible with most flavours, i.e luau, glua, i think handling of things like floats is good for compatibility in general. even things like binary literals, which are supported in Luau will be ignored, so i think broadening the scope is a good idea.
i also think if we run
type X = true | falsethe obfuscator will say type X = (not not 1) | (not 1), which is incorrect.
and from a security perspective, the replacement itself is trivially automated. from what i can see, it either ends up as
(#"xxxxx"+k)or
((n+a)-a)this predictability is also present in other places, i.e. in true/false, nil, etc.
maybe selecting from a pool of replacements is a good idea (you will have to factor in how expensive one is as compared to the other as well, when selecting your pool)
for these numbers especially, i believe you could utilise recursive generation, i.e. instead of 123 becoming
(123+456)-456put out
(((56+67)+431)-431) -- where 56 and 67 are themselves generatedso it becomes something like
((((20+37)+66)+431)-431)also, backticks are not recognised.
There was a problem hiding this comment.
the anti tamper is actually a bit weaker than older versions...
like here
["string.byte"] = string.bytefor one, the reference is resolved when the code starts running, not when output is generated.
and if someone overrides it, so something like
string.byte = fakeso the checks still pass, because youre comparing the function to itself. it itself isnt immutable.
one more thing, it only checks
if T(ref) ~= "function" thennot actually seeing if its a real function
so if someone does the below it will still pass.
math.abs = function(x)
return 123
endalso, the script only compares type(mt.__index) against "function", "table" etc. if someone replaces __index with another function, the check still succeeds.
again, you're checking the type, not the object, when ideally, you should.
and the debug library check is also similar, and it suffers from the problem of someone replacing, let us say, debug.getinfo with
function(...)
return {}
endand it still passes. it isnt that strong of an anti hook measure..
There was a problem hiding this comment.
it assumes load, string.dump, loadstring exist in all envs, but may not be viable in luau/other lua envs.
chunk sizes are always 37–113 bytes, maybe we should derive this from the size rather than it being static
i have a question too, here youve done mod = 2147483647. i presume its the mersenne prime for the lcg mentioned below, but if not, might be worth having it vary so every script wont have the same prng (only applies if it was chosen intentionally)
another thing about that, is there reason three multipliers were chosen with a modulus of 2^31 - 1? from what i saw online, its seen often with different moduli (i.e. 2^31/2^32), since the approach isnt wrong, i thought it would be worth asking why so
Note Below (Not necessarily needed changes)
i researched on what you implemented here, and it seems to be lcg with a byte extracted from state. but it isnt really cryptographic
state = (state * mul + inc) % modrecovering the original bytecode is straightforward because the seed, the parameters, the algorithm are all embedded. i guess this is unavoidable, but worth noting
There was a problem hiding this comment.
the fake blocks have conditions that are provably true (a+b == b+a), so the dead branch is going to be flattened by any deobfuscator/optimiser worth its salt. it may be worth making these predicates less obviously constant
the real branch also has the same issue
if ((guard*guard)-(guard*guard)) == 0 thenwhich is 0==0, so else branch is dead
state machine also only visits one real state, so effectively we reach the real code and exit, and every other fake branch also exits. i suggest linking fake states together randomly instead of making them all terminate, because currently its unreachable decorations on a single real node, and so the CFG is less linear
output is also very uniform for this one module, its like this
do
while true do
if ...
elseif ...
elseif ...
else
break
end
if state == exit then
break
end
end
endand since this is the same for each file, its automatable, and combined with the above, its pretty easy for it to be sidestepped
There was a problem hiding this comment.
long comments are detected but improperly parsed, so the the code ends up treating it like a comment only up till the newline, so any words in the long bracket comments gets treated as a string, and may be encoded
if ch == "-" and code:sub(pos + 1, pos + 1) == "-" then
local nl = code:find("\n", pos)
...
endanother question i had was around using
load("return " .. literal)to evaluate string literals. i dont immediately see an issue because only quoted literals reach this point, but i was wondering if theres a reason for evaluating them instead of decoding escape sequences, because from research, it seems to also be a viable approach.
the generated decoder is also identical between outputs. it may be good to be varying the decoder structure a bit more, otherwise it becomes recognisable and then it is easier to reverse, especially since this occurs after the vm
also long bracket strings ([[...]]/[=[...]=]) are currently skipped entirely, so those string literals remain unobfuscated
a minor note is the XOR implementation, it is a bit slow, but this is a minor problem/nitpick
There was a problem hiding this comment.
one thing i did notice is that the fake handlers are always emitted before the real ones. although the real handlers are shuffled, the fake block is still a recognisable prefix to it, so maybe itll be worth inserting the fake handlers into dispatch_ops (or another combined list) before shuffling it, so both real and fake handlers are interleaved and harder to separate..
also, the fake handlers currently all have essentially the same body, as below.
local _hx = ...
_hx = (_hx * 3) % 997since they're never actually reached, it isn't a correctness issue, but the repeated structure makes them easy to see and wipe. introducing a small pool of different fake handler things might make them blend in a little better, despite the steps after it, it might be a good change
There was a problem hiding this comment.
maybe change the filename to TESTING.md, and it could be worth manually writing this, as it might leave a bad taste in the mouth of some that it has been ai generated.
|
Just make the changes yourself. I don't really want to spend that much time on this. |