diff --git a/core/__pycache__/obfuscator.cpython-311.pyc b/core/__pycache__/obfuscator.cpython-311.pyc index b54a4f0..0913f67 100644 Binary files a/core/__pycache__/obfuscator.cpython-311.pyc and b/core/__pycache__/obfuscator.cpython-311.pyc differ diff --git a/core/__pycache__/parser.cpython-311.pyc b/core/__pycache__/parser.cpython-311.pyc index c444df7..95de243 100644 Binary files a/core/__pycache__/parser.cpython-311.pyc and b/core/__pycache__/parser.cpython-311.pyc differ diff --git a/core/obfuscator.py b/core/obfuscator.py index ad98441..ce867d4 100644 --- a/core/obfuscator.py +++ b/core/obfuscator.py @@ -46,7 +46,7 @@ class LuauVMObfuscator: def minify(self, code): code = re.sub(r'--.*', '', code) code = re.sub(r'\s+', ' ', code) - for op in ['==', '~=', '<=', '>=', '=', r'\+', '-', r'\*', '/', '>', '<', r'\(', r'\)', r'\{', r'\}', r'\[', r'\]', ',', ';', ':']: + for op in ['==', '~=', '<=', '>=', '=', r'\+', r'\*', '/', '>', '<', ',', ';', ':']: code = re.sub(r'\s*' + op + r'\s*', op.replace('\\', ''), code) return code.strip() @@ -69,7 +69,7 @@ class LuauVMObfuscator: k = (i * 149 + salt) % 256; enc = self.encrypt_string(c['value'], k) consts.append({"t": 1, "v": base64.b64encode(enc.encode('latin-1')).decode()}) elif c['type'] == 'number': consts.append({"t": 2, "v": c['value']}) - else: consts.append({"t": 3, "v": c['value']}) + else: consts.append({"t": 3, "v": str(c['value'])}) v = { "BIT": "bit32", "ENV": self.get_var("env"), "DEC": self.get_var("dec"), @@ -81,59 +81,64 @@ class LuauVMObfuscator: "CHARS": self.get_var("chars"), "LOOKUP": self.get_var("lookup"), "GETC": self.get_var("getc") } - vm_lua = f"""local {v['ENV']}=setmetatable({{}},{{__index=getfenv()}}) -local {v['CHARS']}='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' -local {v['LOOKUP']}={{}} -for i=1,64 do {v['LOOKUP']}[{v['CHARS']}:sub(i,i)]=i-1 end + # Using triple single quotes to avoid confusion + vm_lua = f'''local {v['ENV']}=setmetatable({{}},{{__index=getfenv()}}); +local {v['CHARS']}='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +local {v['LOOKUP']}={{}}; +for i=1,64 do {v['LOOKUP']}[{v['CHARS']}:sub(i,i)]=i-1 end; local function {v['DEC']}(data) - data=data:gsub('[^%a%d%+/]','') - local res={{}} + data=data:gsub('[^%a%d%+/]',''); + local res={{}}; for i=1,#data,4 do - local a,b,c,d={v['LOOKUP']}[data:sub(i,i)],{v['LOOKUP']}[data:sub(i+1,i+1)],{v['LOOKUP']}[data:sub(i+2,i+2)],{v['LOOKUP']}[data:sub(i+3,i+3)] - local n={v['BIT']}.lshift(a or 0,18)+{v['BIT']}.lshift(b or 0,12)+{v['BIT']}.lshift(c or 0,6)+(d or 0) - res[#res+1]=string.char({v['BIT']}.extract(n,16,8)) - if c then res[#res+1]=string.char({v['BIT']}.extract(n,8,8)) end - if d then res[#res+1]=string.char({v['BIT']}.extract(n,0,8)) end - end - return table.concat(res) -end -local {v['INST']}={v['DEC']}('{inst_b64}') -local {v['CONSTS']}={v['JSON']}:JSONDecode([=[{json.dumps(consts)}]=]) + local a,b,c,d={v['LOOKUP']}[data:sub(i,i)],{v['LOOKUP']}[data:sub(i+1,i+1)],{v['LOOKUP']}[data:sub(i+2,i+2)],{v['LOOKUP']}[data:sub(i+3,i+3)]; + local n={v['BIT']}.lshift(a or 0,18)+{v['BIT']}.lshift(b or 0,12)+{v['BIT']}.lshift(c or 0,6)+(d or 0); + res[#res+1]=string.char({v['BIT']}.extract(n,16,8)); + if c then res[#res+1]=string.char({v['BIT']}.extract(n,8,8)) end; + if d then res[#res+1]=string.char({v['BIT']}.extract(n,0,8)) end; + end; + return table.concat(res); +end; +local {v['INST']}={v['DEC']}(''{inst_b64}'); +local {v['CONSTS']}={v['JSON']}:JSONDecode([=[{json.dumps(consts)}]=]); local {v['EXEC']}=function() - local {v['REGS']}={{}} - local {v['CURR']}={self.to_expr(pos_map[0])} + local {v['REGS']}={{}}; + local {v['CURR']}={self.to_expr(pos_map[0])}; local function {v['GETC']}(idx) - local c={v['CONSTS']}[idx+1] - if not c then return end + local c={v['CONSTS']}[idx+1]; + if not c then return end; if c.t==1 then - local r,k={v['DEC']}(c.v),(idx*149+{v['SALT']})%256 - local res,lst={{}},k%256 + local r,k={v['DEC']}(c.v),(idx*149+{v['SALT']})%256; + local res,lst={{}},k%256; for i=1,#r do - local char={v['BIT']}.bxor(string.byte(r,i),(k+i+lst-1)%256) - res[i]=string.char(char) - lst=char - end - return table.concat(res) - elseif c.t==3 then return c.v=='true' and true or (c.v=='false' and false or nil) end - return c.v - end + local char={v['BIT']}.bxor(string.byte(r,i),(k+i+lst-1)%256); + res[i]=string.char(char); + lst=char; + end; + return table.concat(res); + elseif c.t==3 then return c.v=='true' and true or (c.v=='false' and false or nil) end; + return c.v; + end; while true do - local {v['PTR']}={v['CURR']}*7+1 - local b1,b2,b3,b4,b5,b6,b7=string.byte({v['INST']},{v['PTR']},{v['PTR']}+6) - if not b1 then break end - {v['CURR']}=b6+({v['BIT']}.lshift(b7,8)) - local {v['OP']}={v['BIT']}.bxor(b1+{v['BIT']}.lshift(b2,8),{self.to_expr(self.k2)})-{self.to_expr(self.k1)} - local {v['A']},{v['B']},{v['C']}=b3,b4,b5 + local {v['PTR']}={v['CURR']}*7+1; + local b1,b2,b3,b4,b5,b6,b7=string.byte({v['INST']},{v['PTR']},{v['PTR']}+6); + if not b1 then break end; + {v['CURR']}=b6+({v['BIT']}.lshift(b7,8)); + local {v['OP']}={v['BIT']}.bxor(b1+{v['BIT']}.lshift(b2,8),{self.to_expr(self.k2)})-{self.to_expr(self.k1)}; + local {v['A']},{v['B']},{v['C']}=b3,b4,b5; if {v['OP']}=={self.opcodes.index('MOVE')} then {v['REGS']}[{v['A']}]={v['REGS']}[{v['B']}] elseif {v['OP']}=={self.opcodes.index('LOADK')} then {v['REGS']}[{v['A']}]={v['GETC']}({v['B']}) elseif {v['OP']}=={self.opcodes.index('GETGLOBAL')} then {v['REGS']}[{v['A']}]={v['ENV']}[{v['GETC']}({v['B']})] elseif {v['OP']}=={self.opcodes.index('SETGLOBAL')} then {v['ENV']}[{v['GETC']}({v['B']})]={v['REGS']}[{v['A']}] elseif {v['OP']}=={self.opcodes.index('GETTABLE')} then - local b={v['REGS']}[{v['B']}] + local b={v['REGS']}[{v['B']}]; if b then {v['REGS']}[{v['A']}]=b[{v['REGS']}[{v['C']}] or {v['GETC']}({v['C']})] end elseif {v['OP']}=={self.opcodes.index('SETTABLE')} then - local a={v['REGS']}[{v['A']}] + local a={v['REGS']}[{v['A']}]; if a then a[{v['REGS']}[{v['B']}] or {v['GETC']}({v['B']})]={v['REGS']}[{v['C']}] end + elseif {v['OP']}=={self.opcodes.index('SELF')} then + local b={v['REGS']}[{v['B']}]; + {v['REGS']}[{v['A']}+1]=b; + {v['REGS']}[{v['A']}]=b[{v['GETC']}({v['C']})]; elseif {v['OP']}=={self.opcodes.index('ADD')} then {v['REGS']}[{v['A']}]=({v['REGS']}[{v['B']}] or 0)+({v['REGS']}[{v['C']}] or 0) elseif {v['OP']}=={self.opcodes.index('SUB')} then {v['REGS']}[{v['A']}]=({v['REGS']}[{v['B']}] or 0)-({v['REGS']}[{v['C']}] or 0) elseif {v['OP']}=={self.opcodes.index('MUL')} then {v['REGS']}[{v['A']}]=({v['REGS']}[{v['B']}] or 0)*({v['REGS']}[{v['C']}] or 0) @@ -142,24 +147,24 @@ local {v['EXEC']}=function() elseif {v['OP']}=={self.opcodes.index('NOT')} then {v['REGS']}[{v['A']}]=not {v['REGS']}[{v['B']}] elseif {v['OP']}=={self.opcodes.index('LEN')} then {v['REGS']}[{v['A']}]=#{v['REGS']}[{v['B']}] or 0 elseif {v['OP']}=={self.opcodes.index('CALL')} then - local f={v['REGS']}[{v['A']}] + local f={v['REGS']}[{v['A']}]; if f then - local args={{}} - if {v['B']}>1 then for i=1,{v['B']}-1 do args[i]={v['REGS']}[{v['A']}+i] end end - local res={{f(({v['UNP']})(args))}} - if {v['C']}>1 then for i=1,{v['C']}-1 do {v['REGS']}[{v['A']}+i-1]=res[i] end end + local args={{}}; + if {v['B']}>1 then for i=1,{v['B']}-1 do args[i]={v['REGS']}[{v['A']} + i] end end; + local res={{f(({v['UNP']})(args))}}; + if {v['C']}>1 then for i=1,{v['C']}-1 do {v['REGS']}[{v['A']} + i - 1]=res[i] end end; end - elseif {v['OP']}=={self.opcodes.index('RETURN')} then break end - end -end -{v['SPW']}({v['EXEC']})""" + elseif {v['OP']}=={self.opcodes.index('RETURN')} then break end; + end; +end; +{v['SPW']}({v['EXEC']});''' return self.minify(vm_lua) def compile_to_bytecode(self, ast): constants, instructions, locals_map = [], [], {} self.next_reg = 0 def add_const(val): - if isinstance(val, str) and ((val.startswith("'") and val.endswith("'")) or (val.startswith('"') and val.endswith('"'))): val = val[1:-1] + if isinstance(val, str) and ((val.startswith("'" ) and val.endswith("'" )) or (val.startswith('"') and val.endswith('"'))): val = val[1:-1] for i, c in enumerate(constants): if c['value'] == val: return i t = 'string' if isinstance(val, str) else 'number' @@ -177,8 +182,12 @@ end else: emit("GETGLOBAL", target, add_const(expr['name'])) elif expr['type'] == 'index': br = self.next_reg; self.next_reg += 1; gen_expr(expr['base'], br) - kr = self.next_reg; self.next_reg += 1; gen_expr(expr['key'], kr) - emit("GETTABLE", target, br, kr); self.next_reg -= 2 + if expr['key']['type'] == 'STRING': + emit("GETTABLE", target, br, add_const(expr['key']['value'])) + else: + kr = self.next_reg; self.next_reg += 1; gen_expr(expr['key'], kr) + emit("GETTABLE", target, br, kr); self.next_reg -= 1 + self.next_reg -= 1 elif expr['type'] == 'binary': lr = self.next_reg; self.next_reg += 1; gen_expr(expr['left'], lr) rr = self.next_reg; self.next_reg += 1; gen_expr(expr['right'], rr) @@ -189,16 +198,25 @@ end op_m = {'-': 'UNM', '#': 'LEN', 'not': 'NOT'} emit(op_m.get(expr['op'], 'UNM'), target, or_reg); self.next_reg -= 1 elif expr['type'] == 'call': gen_call(expr, target) + elif expr['type'] == 'method_call': gen_method_call(expr, target) def gen_call(node, target): fr = self.next_reg; self.next_reg += 1 - if node['func']['type'] == 'variable': - if node['func']['name'] in locals_map: emit("MOVE", fr, locals_map[node['func']['name']]) - else: emit("GETGLOBAL", fr, add_const(node['func']['name'])) - else: gen_expr(node['func'], fr) + gen_expr(node['func'], fr) for i, arg in enumerate(node['args']): ar = self.next_reg; self.next_reg += 1; gen_expr(arg, ar) emit("CALL", fr, len(node['args']) + 1, 2) - emit("MOVE", target, fr) + if target != fr: emit("MOVE", target, fr) + self.next_reg = fr + def gen_method_call(node, target): + fr = self.next_reg; self.next_reg += 1 + br = self.next_reg; self.next_reg += 1 + gen_expr(node['base'], br) + emit("SELF", fr, br, add_const(node['method'])) + self.next_reg = fr + 2 + for i, arg in enumerate(node['args']): + ar = self.next_reg; self.next_reg += 1; gen_expr(arg, ar) + emit("CALL", fr, len(node['args']) + 2, 2) + if target != fr: emit("MOVE", target, fr) self.next_reg = fr for node in ast: if node['type'] == 'assign': @@ -208,10 +226,15 @@ end t = node['target'] if t['type'] == 'index': br = self.next_reg; self.next_reg += 1; gen_expr(t['base'], br) - kr = self.next_reg; self.next_reg += 1; gen_expr(t['key'], kr) - emit("SETTABLE", br, kr, vr); self.next_reg -= 2 + if t['key']['type'] == 'STRING': + emit("SETTABLE", br, add_const(t['key']['value']), vr) + else: + kr = self.next_reg; self.next_reg += 1; gen_expr(t['key'], kr) + emit("SETTABLE", br, kr, vr); self.next_reg -= 1 + self.next_reg -= 1 else: emit("SETGLOBAL", vr, add_const(node['name'])) elif node['type'] == 'call': gen_call(node, self.next_reg) + elif node['type'] == 'method_call': gen_method_call(node, self.next_reg) emit("RETURN") return {"instructions": instructions, "constants": constants} @@ -230,4 +253,4 @@ end return f"-- Obfuscation Error: {traceback.format_exc()}" def obfuscate(code): - return LuauVMObfuscator().obfuscate(code) \ No newline at end of file + return LuauVMObfuscator().obfuscate(code) diff --git a/core/parser.py b/core/parser.py index ee0fcda..2b977fb 100644 --- a/core/parser.py +++ b/core/parser.py @@ -6,6 +6,7 @@ class Lexer: self.tokens = [] self.pos = 0 LB, RB, DQ, SQ, BS = chr(91), chr(93), chr(34), chr(39), chr(92) + # Added : to OP_LIST OP_LIST = r'==|~=|<=|>=|\.\.\.|\.\.|>>|<<|\+|\-|\*|/|%|\^|#|=|\<|\>|\(|\)|\{|\}|' + BS + LB + '|' + BS + RB + '|;|:|,|\.' self.rules = [ ('COMMENT', re.compile('--' + LB + LB + '.*?' + RB + RB + '|--.*', re.DOTALL)), @@ -43,7 +44,7 @@ class Parser: while self.pos < len(self.tokens): node = self.parse_statement() if node: nodes.append(node) - else: self.pos += 1 + else: self.pos += 1 # Still skipping, but hopefully parse_statement is better return nodes def parse_statement(self): tk = self.peek() @@ -51,12 +52,15 @@ class Parser: if tk[1] == 'local': self.consume(); ident = self.consume('IDENT') if ident: - if self.peek()[1] == '=': self.consume(); return {'type': 'assign', 'name': ident[1], 'value': self.parse_expression(), 'local': True} + if self.peek()[1] == '=': + self.consume(); return {'type': 'assign', 'name': ident[1], 'value': self.parse_expression(), 'local': True} + return {'type': 'assign', 'name': ident[1], 'value': {'type': 'KEYWORD', 'value': 'nil'}, 'local': True} return None start = self.pos; expr = self.parse_prefix_expression() if expr: - if self.peek()[1] == '=': self.consume(); return {'type': 'assign', 'target': expr, 'value': self.parse_expression()} - elif expr['type'] == 'call': return expr + if self.peek()[1] == '=': + self.consume(); return {'type': 'assign', 'target': expr, 'value': self.parse_expression()} + elif expr['type'] in ['call', 'method_call']: return expr self.pos = start; return None def parse_prefix_expression(self): tk = self.peek() @@ -65,8 +69,24 @@ class Parser: while True: nt = self.peek() if nt[1] == '.': - self.consume(); m = self.consume('IDENT'); expr = {'type': 'index', 'base': expr, 'key': {'type': 'STRING', 'value': '"'+m[1]+'"'}} - elif nt[1] == '[': self.consume(); key = self.parse_expression(); self.consume('OP', ']'); expr = {'type': 'index', 'base': expr, 'key': key} + self.consume(); m = self.consume('IDENT') + if m: expr = {'type': 'index', 'base': expr, 'key': {'type': 'STRING', 'value': '"'+m[1]+'"'}} + else: break + elif nt[1] == ':': + self.consume(); m = self.consume('IDENT') + if m: + # Method call + if self.peek()[1] == '(': + self.consume(); args = [] + if self.peek()[1] != ')': + args.append(self.parse_expression()) + while self.peek()[1] == ',': self.consume(); args.append(self.parse_expression()) + self.consume('OP', ')'); expr = {'type': 'method_call', 'base': expr, 'method': m[1], 'args': args} + else: # Not a call, just a colon access? (Rare in Lua, usually an error, but let's be safe) + expr = {'type': 'index', 'base': expr, 'key': {'type': 'STRING', 'value': '"'+m[1]+'"'}} + else: break + elif nt[1] == '[': + self.consume(); key = self.parse_expression(); self.consume('OP', ']'); expr = {'type': 'index', 'base': expr, 'key': key} elif nt[1] == '(': self.consume(); args = [] if self.peek()[1] != ')': @@ -77,16 +97,19 @@ class Parser: return expr def parse_expression(self): left = self.parse_primary_expression() + if not left: return None while self.peek()[1] in ['+', '-', '*', '/', '..', '==', '<', '>', '<=', '>=', '~=']: op = self.consume()[1]; right = self.parse_primary_expression() + if not right: break left = {'type': 'binary', 'op': op, 'left': left, 'right': right} return left def parse_primary_expression(self): tk = self.peek() if not tk: return None if tk[1] in ['-', '#', 'not']: - op = self.consume()[1]; return {'type': 'unary', 'op': op, 'operand': self.parse_primary_expression()} + op = self.consume()[1]; sub = self.parse_primary_expression() + return {'type': 'unary', 'op': op, 'operand': sub} if sub else None if tk[0] in ['STRING', 'NUMBER']: v = tk[1]; self.consume(); return {'type': tk[0], 'value': v} if tk[1] in ['true', 'false', 'nil']: return {'type': 'KEYWORD', 'value': self.consume()[1]} if tk[1] == '(': self.consume(); e = self.parse_expression(); self.consume('OP', ')'); return e - return self.parse_prefix_expression() \ No newline at end of file + return self.parse_prefix_expression()