luau-obfuscator 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +56 -56
- package/dist/index.cjs +2186 -95
- package/dist/index.d.cts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +2186 -95
- package/generated/grammers.luau +13643 -0
- package/generated/tests.luau +5567 -0
- package/package.json +2 -2
- package/scripts/test.js +24 -8
- package/{scripts/example.luau → smoketest/grammers.luau} +1174 -1174
- package/smoketest/tests.luau +115 -0
- package/src/config.ts +2 -1
- package/src/passes/ConstantArray.ts +90 -89
- package/src/passes/EncryptNumbers.ts +122 -65
- package/src/passes/EncryptStrings.ts +162 -82
- package/src/passes/GlobalMapping.ts +193 -193
- package/src/passes/InsertJunk.ts +184 -184
- package/src/passes/Minify.ts +5 -5
- package/src/passes/NumbersToExpressions.ts +183 -2
- package/src/passes/RenameVariables.ts +2 -9
- package/src/passes/StringsToExpressions.ts +195 -6
- package/src/passes/StripTypes.ts +191 -191
- package/src/passes/Vmify.ts +87 -0
- package/src/passes/WrapInFunction.ts +31 -31
- package/src/passes/nodeFactory.ts +36 -1
- package/src/passes/vmify/chunk.ts +54 -0
- package/src/passes/vmify/compiler.ts +1026 -0
- package/src/passes/vmify/names.ts +72 -0
- package/src/passes/vmify/opcodes.ts +80 -0
- package/src/passes/vmify/registers.ts +63 -0
- package/src/passes/vmify/runtime.ts +334 -0
- package/src/passes/vmify/scope-walk.ts +175 -0
- package/src/passes/vmify/serialize.ts +68 -0
- package/src/passes/walk.ts +141 -43
- package/src/pipeline.ts +5 -6
- package/tsconfig.json +20 -20
- package/tsup.config.ts +9 -9
- package/generated/final.luau +0 -1029
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Program, Statement, Expression } from "luau-parser"
|
|
2
|
+
import { luauparser } from "luau-parser"
|
|
3
|
+
import { VmCompiler, DEFAULT_BUILTIN_GLOBALS } from "./vmify/compiler"
|
|
4
|
+
import { serializeProto } from "./vmify/serialize"
|
|
5
|
+
import { buildVmRuntimeSource } from "./vmify/runtime"
|
|
6
|
+
import { createOpcodeMap } from "./vmify/opcodes"
|
|
7
|
+
import { generateVmNames } from "./vmify/names"
|
|
8
|
+
import {
|
|
9
|
+
localStatement, identifier, call, table, namedField, returnStatement, vararg,
|
|
10
|
+
vmStructuralNumbers,
|
|
11
|
+
} from "./nodeFactory"
|
|
12
|
+
import { transformExpressions } from "./walk"
|
|
13
|
+
|
|
14
|
+
/** 파싱된 서브트리(런타임 인터프리터 소스) 안의 모든 NumberLiteral을 vmStructuralNumbers로
|
|
15
|
+
* 표시한다 — 인터프리터 자신의 +1/-1/패딩 같은 구현 디테일 상수는 사용자 데이터가 아닌데,
|
|
16
|
+
* 수십 개 opcode 핸들러에 반복 등장해서 EncryptNumbers/NumbersToExpressions/ConstantArray가
|
|
17
|
+
* 건드리면 곱셈적으로 부풀어 오른다. */
|
|
18
|
+
function markRuntimeNumbersAsStructural(body: { statements: Statement[] }): void {
|
|
19
|
+
transformExpressions({ body } as Program, (expr) => {
|
|
20
|
+
if (expr.type === "NumberLiteral") vmStructuralNumbers.add(expr)
|
|
21
|
+
return undefined
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VmifyOptions {
|
|
26
|
+
/**
|
|
27
|
+
* VM 밖에서 값을 그대로 참조해야 하는 전역 이름 목록(GETGLOBAL/SETGLOBAL 대상).
|
|
28
|
+
* Luau엔 getfenv가 없어서 "임의의 전역 읽기"를 흉내낼 수 없기 때문에,
|
|
29
|
+
* 여기 나열된 이름들만 브리지 테이블에 실제 값으로 미리 채워 넣는다.
|
|
30
|
+
* 나열되지 않은 전역을 참조하면 컴파일은 되지만 런타임에 nil이 나온다 — 실사용 전
|
|
31
|
+
* 반드시 프로그램에서 실제로 쓰는 전역 이름을 전부 이 목록에 채워야 함(TODO: 컴파일러가
|
|
32
|
+
* ScopeAnalysis.globalsByName을 이용해 자동으로 목록을 뽑아주도록 개선 가능).
|
|
33
|
+
*/
|
|
34
|
+
builtinGlobals?: readonly string[]
|
|
35
|
+
/**
|
|
36
|
+
* opcode 번호 셔플 + 전역/필드 이름 무작위화에 쓸 난수 생성기. 지정하지 않으면
|
|
37
|
+
* Math.random. 재현 가능한 빌드가 필요하면(테스트 등) 시드 고정 PRNG를 넘길 것.
|
|
38
|
+
*/
|
|
39
|
+
random?: () => number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* program 전체를 바이트코드로 컴파일하고, program.body를 다음 구조로 치환한다:
|
|
44
|
+
*
|
|
45
|
+
* local <globals> = { print = print, game = game, ... }
|
|
46
|
+
* <런타임 인터프리터 함수 정의 — 식별자/opcode 번호는 매 빌드 무작위>
|
|
47
|
+
* local <protoRoot> = { ...직렬화된 바이트코드... }
|
|
48
|
+
* return <execute>(<protoRoot>, {}, ...)
|
|
49
|
+
*
|
|
50
|
+
* opcode 번호(opcodes.ts:createOpcodeMap)와 전역/필드 이름(names.ts:generateVmNames)을
|
|
51
|
+
* 매 빌드 새로 뽑기 때문에, 컴파일된 청크와 런타임 인터프리터가 산출물마다 구조적으로
|
|
52
|
+
* 달라진다 — "이 VM은 opcode 6이 GETGLOBAL이다" 같은 지식이 다음 빌드에는 안 통함.
|
|
53
|
+
*
|
|
54
|
+
* 주의: 이 패스는 pipeline.ts상 반드시 "가장 먼저"(StripTypes 다음) 실행돼야 한다 —
|
|
55
|
+
* 컴파일이 끝나면 원본 statement/expression 노드 대부분이 바이트코드 숫자로 바뀌어
|
|
56
|
+
* 사라지므로, 이후 패스(RenameVariables, EncryptStrings 등)가 손댈 원본 AST가 없다.
|
|
57
|
+
*/
|
|
58
|
+
export function runVmify(program: Program, options: VmifyOptions): void {
|
|
59
|
+
const globalNames = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS
|
|
60
|
+
const random = options.random ?? Math.random
|
|
61
|
+
|
|
62
|
+
const opcodeMap = createOpcodeMap(random)
|
|
63
|
+
const names = generateVmNames(random)
|
|
64
|
+
|
|
65
|
+
const compiler = new VmCompiler(program, globalNames, opcodeMap)
|
|
66
|
+
const topProto = compiler.compile()
|
|
67
|
+
|
|
68
|
+
const globalsTable = table(
|
|
69
|
+
globalNames.map((name) => namedField(name, identifier(name))),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const runtimeSource = buildVmRuntimeSource(names, opcodeMap)
|
|
73
|
+
const runtimeProgram = luauparser.parse(runtimeSource)
|
|
74
|
+
const runtimeStatements: Statement[] = runtimeProgram.body.statements
|
|
75
|
+
markRuntimeNumbersAsStructural(runtimeProgram.body)
|
|
76
|
+
|
|
77
|
+
const protoLiteral = serializeProto(topProto, names)
|
|
78
|
+
|
|
79
|
+
const newBody: Statement[] = [
|
|
80
|
+
localStatement(names.globals, globalsTable),
|
|
81
|
+
...runtimeStatements,
|
|
82
|
+
localStatement(names.protoRoot, protoLiteral),
|
|
83
|
+
returnStatement([call(identifier(names.execute), [identifier(names.protoRoot), table([]), vararg()])]),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
program.body.statements = newBody
|
|
87
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import type { Program } from "luau-parser"
|
|
2
|
-
import {
|
|
3
|
-
block, functionBody, functionExpression, paren, call, vararg, returnStatement,
|
|
4
|
-
} from "./nodeFactory"
|
|
5
|
-
|
|
6
|
-
export interface WrapInFunctionOptions {}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 전체 프로그램을:
|
|
10
|
-
* return (function(...)
|
|
11
|
-
* <원래 코드>
|
|
12
|
-
* end)(...)
|
|
13
|
-
* 로 감쌈.
|
|
14
|
-
*
|
|
15
|
-
* - `return`을 쓰는 이유: Script/LocalScript에서는 top-level return이 그냥 청크를
|
|
16
|
-
* 조기 종료시킬 뿐 무해하고, ModuleScript라면 IIFE의 결과값이 그대로 require()
|
|
17
|
-
* 호출자에게 전달돼야 하므로 필요함. 즉 스크립트 종류를 가리지 않고 안전.
|
|
18
|
-
* - `...`을 파라미터로 받아서 다시 그대로 넘겨주는 이유: 원본 청크가 최상위에서
|
|
19
|
-
* `...`(스크립트 인자)을 참조하는 경우를 대비. 그냥 지워버리면 그런 코드가
|
|
20
|
-
* 깨짐.
|
|
21
|
-
*
|
|
22
|
-
* 반드시 파이프라인 맨 마지막 근처에서 실행돼야 함 — 이 패스 이후에 실행되는
|
|
23
|
-
* 다른 패스가 top-level 스코프를 순회/변형한다면 이미 감싸인 함수 내부까지
|
|
24
|
-
* 안 보고 지나칠 수 있음.
|
|
25
|
-
*/
|
|
26
|
-
export function runWrapInFunction(program: Program, _options: WrapInFunctionOptions): void {
|
|
27
|
-
const innerBody = program.body
|
|
28
|
-
const wrapper = functionExpression(functionBody([], innerBody, true))
|
|
29
|
-
const iife = call(paren(wrapper), [vararg()])
|
|
30
|
-
|
|
31
|
-
program.body = block([returnStatement([iife])])
|
|
1
|
+
import type { Program } from "luau-parser"
|
|
2
|
+
import {
|
|
3
|
+
block, functionBody, functionExpression, paren, call, vararg, returnStatement,
|
|
4
|
+
} from "./nodeFactory"
|
|
5
|
+
|
|
6
|
+
export interface WrapInFunctionOptions {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 전체 프로그램을:
|
|
10
|
+
* return (function(...)
|
|
11
|
+
* <원래 코드>
|
|
12
|
+
* end)(...)
|
|
13
|
+
* 로 감쌈.
|
|
14
|
+
*
|
|
15
|
+
* - `return`을 쓰는 이유: Script/LocalScript에서는 top-level return이 그냥 청크를
|
|
16
|
+
* 조기 종료시킬 뿐 무해하고, ModuleScript라면 IIFE의 결과값이 그대로 require()
|
|
17
|
+
* 호출자에게 전달돼야 하므로 필요함. 즉 스크립트 종류를 가리지 않고 안전.
|
|
18
|
+
* - `...`을 파라미터로 받아서 다시 그대로 넘겨주는 이유: 원본 청크가 최상위에서
|
|
19
|
+
* `...`(스크립트 인자)을 참조하는 경우를 대비. 그냥 지워버리면 그런 코드가
|
|
20
|
+
* 깨짐.
|
|
21
|
+
*
|
|
22
|
+
* 반드시 파이프라인 맨 마지막 근처에서 실행돼야 함 — 이 패스 이후에 실행되는
|
|
23
|
+
* 다른 패스가 top-level 스코프를 순회/변형한다면 이미 감싸인 함수 내부까지
|
|
24
|
+
* 안 보고 지나칠 수 있음.
|
|
25
|
+
*/
|
|
26
|
+
export function runWrapInFunction(program: Program, _options: WrapInFunctionOptions): void {
|
|
27
|
+
const innerBody = program.body
|
|
28
|
+
const wrapper = functionExpression(functionBody([], innerBody, true))
|
|
29
|
+
const iife = call(paren(wrapper), [vararg()])
|
|
30
|
+
|
|
31
|
+
program.body = block([returnStatement([iife])])
|
|
32
32
|
}
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
LocalStatement, LocalFunctionStatement, FunctionBody, FunctionParameter,
|
|
6
6
|
TypedIdentifier, Block, Statement, AssignmentStatement, NumericForStatement,
|
|
7
7
|
ReturnStatement, DoStatement, IfStatement, IfClause, VarargExpression,
|
|
8
|
-
FunctionExpression,
|
|
8
|
+
FunctionExpression, BooleanLiteral, NilLiteral, WhileStatement,
|
|
9
9
|
} from "luau-parser"
|
|
10
10
|
|
|
11
11
|
/** 합성 노드는 실제 소스 위치가 없으므로 0으로 채움(printer는 구조만 봄). */
|
|
@@ -27,6 +27,25 @@ export function numberLiteral(value: number): NumberLiteral {
|
|
|
27
27
|
return { type: "NumberLiteral", value, raw: String(value), line: POS, column: POS }
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Vmify가 인스트럭션 op/a/b/c, numParams, maxRegs 같은 "VM 구조 메타데이터"용으로
|
|
32
|
+
* 찍어내는 숫자는 이 표시를 달아 만든다. 이런 값은 원본 소스에 쓰인 상수가 아니라
|
|
33
|
+
* Vmify 자신이 만들어낸, 개수가 코드 크기에 비례해 수천~수만 개까지 불어날 수 있는
|
|
34
|
+
* 내부 값이고, 로직 자체는 이미 Vmify 컴파일로 구조적으로 숨겨져 있다.
|
|
35
|
+
* NumbersToExpressions/EncryptNumbers 같은 뒤쪽 패스가 이 값들까지 하나하나
|
|
36
|
+
* for-loop/재귀 함수/디코더 호출로 부풀리면, 두 패스가 곱셈적으로 상호작용해
|
|
37
|
+
* 출력이 기하급수적으로 커진다 (실측: 4400줄 샘플에서 Vmify+NumbersToExpressions만
|
|
38
|
+
* 켜면 0.9MB가 아니라 7.7MB가 나옴). 그래서 이 표시가 붙은 노드는 뒤쪽 숫자
|
|
39
|
+
* 난독화 패스들이 건드리지 않고 그대로 둔다.
|
|
40
|
+
*/
|
|
41
|
+
export const vmStructuralNumbers = new WeakSet<NumberLiteral>()
|
|
42
|
+
|
|
43
|
+
export function vmNumberLiteral(value: number): NumberLiteral {
|
|
44
|
+
const node = numberLiteral(value)
|
|
45
|
+
vmStructuralNumbers.add(node)
|
|
46
|
+
return node
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
export function binary(
|
|
31
50
|
operator: BinaryExpression["operator"],
|
|
32
51
|
left: Expression,
|
|
@@ -67,6 +86,18 @@ export function positionalField(value: Expression): TableField {
|
|
|
67
86
|
return { type: "TableFieldPositional", value }
|
|
68
87
|
}
|
|
69
88
|
|
|
89
|
+
export function namedField(name: string, value: Expression): TableField {
|
|
90
|
+
return { type: "TableFieldNamed", name: identifier(name), value }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function booleanLiteral(value: boolean): BooleanLiteral {
|
|
94
|
+
return { type: "BooleanLiteral", value, line: POS, column: POS }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function nilLiteral(): NilLiteral {
|
|
98
|
+
return { type: "NilLiteral", line: POS, column: POS }
|
|
99
|
+
}
|
|
100
|
+
|
|
70
101
|
export function localStatement(name: string, init: Expression): LocalStatement {
|
|
71
102
|
return {
|
|
72
103
|
type: "LocalStatement",
|
|
@@ -138,6 +169,10 @@ export function ifStatement(clauses: IfClause[], alternate?: Block): IfStatement
|
|
|
138
169
|
return { type: "IfStatement", clauses, alternate, line: POS, column: POS }
|
|
139
170
|
}
|
|
140
171
|
|
|
172
|
+
export function whileStatement(condition: Expression, body: Block): WhileStatement {
|
|
173
|
+
return { type: "WhileStatement", condition, body, line: POS, column: POS }
|
|
174
|
+
}
|
|
175
|
+
|
|
141
176
|
export function vararg(): VarargExpression {
|
|
142
177
|
return { type: "VarargExpression", line: POS, column: POS }
|
|
143
178
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Instr } from "./opcodes"
|
|
2
|
+
|
|
3
|
+
/** 상수 풀에 들어갈 수 있는 값. */
|
|
4
|
+
export type ConstValue = string | number | boolean | null
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* upvalue 하나가 부모 함수 기준으로 어디서 오는지.
|
|
8
|
+
* - "local": 부모 함수의 레지스터(박스)를 그대로 캡처
|
|
9
|
+
* - "upval": 부모 함수 자신의 upvalue를 한 단계 더 전달(중첩 클로저)
|
|
10
|
+
*/
|
|
11
|
+
export interface UpvalDesc {
|
|
12
|
+
kind: "local" | "upval"
|
|
13
|
+
index: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 함수 하나(최상위 청크 포함)에 대응하는 컴파일 결과. Lua의 Proto와 동일한 역할. */
|
|
17
|
+
export interface Proto {
|
|
18
|
+
id: number
|
|
19
|
+
numParams: number
|
|
20
|
+
hasVarargs: boolean
|
|
21
|
+
maxRegs: number
|
|
22
|
+
code: Instr[]
|
|
23
|
+
consts: ConstValue[]
|
|
24
|
+
/** consts 배열과 별도로 유지하는 value -> index 조회용 인덱스 (interning을 O(1)로). */
|
|
25
|
+
constIndex: Map<ConstValue, number>
|
|
26
|
+
/** 이 함수가 캡처하는 upvalue들이 "부모" 기준 어디서 오는지. */
|
|
27
|
+
upvalDescs: UpvalDesc[]
|
|
28
|
+
/** 중첩 함수 표현식들. CLOSURE 명령의 B는 이 배열의 인덱스. */
|
|
29
|
+
protos: Proto[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createProto(id: number): Proto {
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
numParams: 0,
|
|
36
|
+
hasVarargs: false,
|
|
37
|
+
maxRegs: 0,
|
|
38
|
+
code: [],
|
|
39
|
+
consts: [],
|
|
40
|
+
constIndex: new Map(),
|
|
41
|
+
upvalDescs: [],
|
|
42
|
+
protos: [],
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 상수 풀에 값 추가(중복 제거) 후 인덱스 반환. */
|
|
47
|
+
export function internConst(proto: Proto, value: ConstValue): number {
|
|
48
|
+
const existing = proto.constIndex.get(value)
|
|
49
|
+
if (existing !== undefined) return existing
|
|
50
|
+
proto.consts.push(value)
|
|
51
|
+
const idx = proto.consts.length - 1
|
|
52
|
+
proto.constIndex.set(value, idx)
|
|
53
|
+
return idx
|
|
54
|
+
}
|