luau-obfuscator 1.0.2 → 1.0.3
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/dist/index.cjs +26 -8
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +26 -8
- package/generated/grammers.luau +3431 -5262
- package/generated/tests.luau +1947 -3184
- package/package.json +1 -1
- package/scripts/test.js +1 -3
- package/src/passes/StringsToExpressions.ts +6 -6
- package/src/passes/Vmify.ts +14 -9
- package/src/passes/vmify/compiler.ts +18 -0
package/package.json
CHANGED
package/scripts/test.js
CHANGED
|
@@ -15,10 +15,8 @@ for(const filename of fs.readdirSync(smokedir)) {
|
|
|
15
15
|
const timeStart = performance.now()
|
|
16
16
|
const obfuscated = obfuscate(content, {
|
|
17
17
|
Minify: {active: false},
|
|
18
|
-
NumbersToExpressions: {active: false},
|
|
19
|
-
StringsToExpressions: {active: false}
|
|
20
18
|
})
|
|
21
19
|
const timeEnd = performance.now()
|
|
22
20
|
fs.writeFileSync(path.join(GENERATE_DIR, `${filename}`), obfuscated)
|
|
23
|
-
console.log(`obfuscating ${filename} took ${(timeEnd - timeStart) / 1000}s, length: ${content.length} -> ${obfuscated.length}`)
|
|
21
|
+
console.log(`obfuscating ${filename} took ${(timeEnd - timeStart) / 1000}s, length: ${content.length} -> ${obfuscated.length} (x${obfuscated.length / content.length})`)
|
|
24
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Program, Expression, Statement } from "luau-parser"
|
|
2
2
|
import { transformExpressions } from "./walk"
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
vmNumberLiteral, binary, call, member, identifier, table, positionalField, index,
|
|
5
5
|
localStatement, localFunctionStatement, functionParam, functionBody, functionExpression,
|
|
6
6
|
block, ifStatement, ifClause, returnStatement, paren, unary, stringLiteral,
|
|
7
7
|
numericForStatement, assignmentStatement,
|
|
@@ -50,7 +50,7 @@ function splitByteChunks(bytes: number[], min: number, max: number): number[][]
|
|
|
50
50
|
|
|
51
51
|
function stringCharCall(bytes: number[]): Expression {
|
|
52
52
|
// string.char(b1, b2, ...) — 원문 텍스트가 소스에 전혀 남지 않음
|
|
53
|
-
return call(member(identifier("string"), "char"), bytes.map(
|
|
53
|
+
return call(member(identifier("string"), "char"), bytes.map(vmNumberLiteral))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -107,7 +107,7 @@ function buildShuffledTables(chunks: number[][]): { storeVar: string; orderVar:
|
|
|
107
107
|
for (let slot = 0; slot < n; slot++) {
|
|
108
108
|
order[storageOrder[slot]] = slot + 1
|
|
109
109
|
}
|
|
110
|
-
const orderFields = order.map((slot) => positionalField(
|
|
110
|
+
const orderFields = order.map((slot) => positionalField(vmNumberLiteral(slot)))
|
|
111
111
|
|
|
112
112
|
return {
|
|
113
113
|
storeVar,
|
|
@@ -140,7 +140,7 @@ function buildForLoopStringExpr(chunks: number[][]): Expression {
|
|
|
140
140
|
localStatement(accVar, stringLiteral("")),
|
|
141
141
|
numericForStatement(
|
|
142
142
|
iVar,
|
|
143
|
-
|
|
143
|
+
vmNumberLiteral(1),
|
|
144
144
|
unary("#", identifier(orderVar)),
|
|
145
145
|
block([
|
|
146
146
|
assignmentStatement(
|
|
@@ -187,7 +187,7 @@ function buildRecursiveStringExpr(chunks: number[][]): Expression {
|
|
|
187
187
|
]),
|
|
188
188
|
returnStatement([
|
|
189
189
|
call(identifier(recName), [
|
|
190
|
-
binary("+", identifier(iParam),
|
|
190
|
+
binary("+", identifier(iParam), vmNumberLiteral(1)),
|
|
191
191
|
binary(
|
|
192
192
|
"..",
|
|
193
193
|
identifier(accParam),
|
|
@@ -200,7 +200,7 @@ function buildRecursiveStringExpr(chunks: number[][]): Expression {
|
|
|
200
200
|
return iife([
|
|
201
201
|
...stmts,
|
|
202
202
|
localFunctionStatement(recName, functionBody([functionParam(iParam), functionParam(accParam)], body)),
|
|
203
|
-
returnStatement([call(identifier(recName), [
|
|
203
|
+
returnStatement([call(identifier(recName), [vmNumberLiteral(1), stringLiteral("")])]),
|
|
204
204
|
])
|
|
205
205
|
}
|
|
206
206
|
|
package/src/passes/Vmify.ts
CHANGED
|
@@ -24,12 +24,12 @@ function markRuntimeNumbersAsStructural(body: { statements: Statement[] }): void
|
|
|
24
24
|
|
|
25
25
|
export interface VmifyOptions {
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
27
|
+
* 스코프 분석에 미리 등록해둘 전역 이름 시드 목록. 여기 없는 이름이라도 프로그램에서
|
|
28
|
+
* 실제로 참조되면(로컬/파라미터/업밸류로 안 풀리는 식별자는 전부) 스코프 분석이
|
|
29
|
+
* 자동으로 전역으로 잡아내므로, 브릿지 테이블은 이 옵션이 아니라
|
|
30
|
+
* VmCompiler.getUsedGlobalNames()(실제 사용된 전역 전체)로 만든다 — 옛날엔 이 목록에
|
|
31
|
+
* 없는 이름을 쓰면 컴파일은 되는데 GETGLOBAL이 조용히 nil을 반환해서
|
|
32
|
+
* "attempt to call a nil value"로 터졌었음.
|
|
33
33
|
*/
|
|
34
34
|
builtinGlobals?: readonly string[]
|
|
35
35
|
/**
|
|
@@ -56,17 +56,22 @@ export interface VmifyOptions {
|
|
|
56
56
|
* 사라지므로, 이후 패스(RenameVariables, EncryptStrings 등)가 손댈 원본 AST가 없다.
|
|
57
57
|
*/
|
|
58
58
|
export function runVmify(program: Program, options: VmifyOptions): void {
|
|
59
|
-
const
|
|
59
|
+
const builtinGlobals = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS
|
|
60
60
|
const random = options.random ?? Math.random
|
|
61
61
|
|
|
62
62
|
const opcodeMap = createOpcodeMap(random)
|
|
63
63
|
const names = generateVmNames(random)
|
|
64
64
|
|
|
65
|
-
const compiler = new VmCompiler(program,
|
|
65
|
+
const compiler = new VmCompiler(program, builtinGlobals, opcodeMap)
|
|
66
66
|
const topProto = compiler.compile()
|
|
67
67
|
|
|
68
|
+
// builtinGlobals(시드 목록)가 아니라 실제로 프로그램이 참조한 전역 전체로 브릿지를
|
|
69
|
+
// 만든다 — game/task 같은 시드 목록에 없는 Roblox API나 실행기 전역을 스크립트가
|
|
70
|
+
// 쓰더라도 스코프 분석이 이미 전역으로 잡아뒀으므로 여기서 다 커버된다.
|
|
71
|
+
const usedGlobalNames = compiler.getUsedGlobalNames()
|
|
72
|
+
|
|
68
73
|
const globalsTable = table(
|
|
69
|
-
|
|
74
|
+
usedGlobalNames.map((name) => namedField(name, identifier(name))),
|
|
70
75
|
)
|
|
71
76
|
|
|
72
77
|
const runtimeSource = buildVmRuntimeSource(names, opcodeMap)
|
|
@@ -42,6 +42,24 @@ export class VmCompiler {
|
|
|
42
42
|
private protoIdCounter = 0
|
|
43
43
|
private declToBinding = new Map<object, BindingId>()
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 실제로 프로그램이 참조하는 모든 전역 이름(사전 등록된 builtinGlobals +
|
|
47
|
+
* 스코프 분석이 자동으로 찾아낸, 로컬/파라미터/업밸류로 안 풀리는 나머지 식별자
|
|
48
|
+
* 전부)을 돌려준다. Vmify.ts가 이 목록으로 브릿지 테이블(globals)을 만든다.
|
|
49
|
+
*
|
|
50
|
+
* 예전엔 호출자가 넘긴 builtinGlobals 목록만 브릿지에 넣었는데, 그 목록에
|
|
51
|
+
* 없는 전역(예: Roblox API나 실행기 전용 전역을 다 못 채운 경우)을 참조하면
|
|
52
|
+
* 컴파일은 성공하지만 GETGLOBAL이 조용히 nil을 반환해서 "attempt to call a
|
|
53
|
+
* nil value"로 터진다 — 게다가 원인이 VM 디스패치 쪽 스택에서만 보여서
|
|
54
|
+
* "opcode 매핑이 깨졌나?" 하고 엉뚱한 곳을 의심하게 만든다. analyzeScopes는
|
|
55
|
+
* 어차피 로컬/업밸류로 못 푸는 식별자를 전부 global 바인딩으로 잡아두므로,
|
|
56
|
+
* builtinGlobals에 없던 이름도 여기 globalsByName에는 다 들어있다 —
|
|
57
|
+
* 하드코딩된 허용목록 대신 이걸 그대로 쓰면 커버리지 문제 자체가 사라진다.
|
|
58
|
+
*/
|
|
59
|
+
getUsedGlobalNames(): string[] {
|
|
60
|
+
return Array.from(this.analysis.globalsByName.keys())
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
constructor(
|
|
46
64
|
private program: Program,
|
|
47
65
|
builtinGlobals: readonly string[] = DEFAULT_BUILTIN_GLOBALS,
|