luau-obfuscator 1.0.4 → 1.0.5
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/generated/grammers.luau +3377 -3410
- package/generated/tests.luau +1817 -1910
- package/package.json +1 -1
- package/scripts/test.js +21 -21
- package/smoketest/grammers.luau +1174 -1174
- package/smoketest/tests.luau +114 -114
- package/src/passes/ConstantArray.ts +90 -90
- package/src/passes/EncryptNumbers.ts +122 -122
- package/src/passes/EncryptStrings.ts +162 -162
- package/src/passes/GlobalMapping.ts +193 -193
- package/src/passes/InsertJunk.ts +184 -184
- package/src/passes/Minify.ts +5 -5
- package/src/passes/StripTypes.ts +191 -191
- package/src/passes/WrapInFunction.ts +31 -31
- package/tsconfig.json +20 -20
- package/tsup.config.ts +9 -9
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
import type { Program, Expression } from "luau-parser"
|
|
2
|
-
import { transformExpressions } from "./walk"
|
|
3
|
-
import {
|
|
4
|
-
identifier, vmNumberLiteral, call, member, index, table, positionalField,
|
|
5
|
-
localFunctionStatement, functionParam, functionBody, block, localStatement,
|
|
6
|
-
assignmentStatement, numericForStatement, returnStatement, unary, binary,
|
|
7
|
-
} from "./nodeFactory"
|
|
8
|
-
|
|
9
|
-
export interface EncryptStringsOptions {}
|
|
10
|
-
|
|
11
|
-
const MIN_KEY_LEN = 4
|
|
12
|
-
const MAX_KEY_LEN = 12
|
|
13
|
-
|
|
14
|
-
function randomInt(min: number, max: number): number {
|
|
15
|
-
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function randomName(): string {
|
|
19
|
-
return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Luau 소스는 UTF-8 바이트 스트림이므로 charCodeAt이 아니라 실제 UTF-8 바이트로 변환. */
|
|
23
|
-
function toUtf8Bytes(value: string): number[] {
|
|
24
|
-
return Array.from(new TextEncoder().encode(value))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** bit32.lrotate(x, disp)와 동일한 32비트 좌회전. disp는 0~31 범위로 정규화. */
|
|
28
|
-
function lrotate32(x: number, disp: number): number {
|
|
29
|
-
const d = ((disp % 32) + 32) % 32
|
|
30
|
-
const xu = x >>> 0
|
|
31
|
-
if (d === 0) return xu
|
|
32
|
-
return ((xu << d) | (xu >>> (32 - d))) >>> 0
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* i번째(1-indexed) 바이트에 적용할 키스트림 바이트.
|
|
37
|
-
* - 롤링 키 배열에서 위치별로 다른 키 바이트를 뽑고
|
|
38
|
-
* - 좌회전으로 비트를 섞은 뒤
|
|
39
|
-
* - 위치(i)에 종속적인 바이트와 다시 섞는다.
|
|
40
|
-
* 같은 평문 바이트가 반복돼도 위치가 다르면 암호문 바이트가 달라져
|
|
41
|
-
* (반복 키 XOR과 달리) 단순 빈도분석/Kasiski류 공격에 내성을 갖는다.
|
|
42
|
-
*/
|
|
43
|
-
function keystreamByte(keys: number[], mult: number, i: number): number {
|
|
44
|
-
const nKeys = keys.length
|
|
45
|
-
const keyByte = keys[(i - 1) % nKeys]
|
|
46
|
-
const rotated = lrotate32(keyByte, i % 8) & 0xFF
|
|
47
|
-
const posByte = (i * mult) & 0xFF
|
|
48
|
-
return (rotated ^ posByte) & 0xFF
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 숫자 리터럴을 곧이곧대로 노출하지 않고, 산술식/bit32.bxor 호출로 위장한다.
|
|
53
|
-
* "decoder(literal, literal)" 패턴을 그대로 상수 접기(constant fold)하는
|
|
54
|
-
* 단순 정적 분석 스크립트를 무력화하기 위함.
|
|
55
|
-
*/
|
|
56
|
-
function obfuscatedNumber(value: number): Expression {
|
|
57
|
-
const variant = randomInt(0, 2)
|
|
58
|
-
if (variant === 0) {
|
|
59
|
-
const a = randomInt(0, value)
|
|
60
|
-
return binary("+", vmNumberLiteral(a), vmNumberLiteral(value - a))
|
|
61
|
-
}
|
|
62
|
-
if (variant === 1) {
|
|
63
|
-
const a = randomInt(0, 0xFFFFFF)
|
|
64
|
-
return binary("-", vmNumberLiteral(value + a), vmNumberLiteral(a))
|
|
65
|
-
}
|
|
66
|
-
const a = randomInt(0, 0xFFFFFF)
|
|
67
|
-
const b = (value ^ a) >>> 0
|
|
68
|
-
return call(member(identifier("bit32"), "bxor"), [vmNumberLiteral(a), vmNumberLiteral(b)])
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 런타임 디코더를 조립:
|
|
73
|
-
* local function <name>(data, keys, mult)
|
|
74
|
-
* local nKeys = #keys
|
|
75
|
-
* local out = {}
|
|
76
|
-
* for i = 1, #data do
|
|
77
|
-
* local keyByte = keys[((i - 1) % nKeys) + 1]
|
|
78
|
-
* local mixed = bit32.band(bit32.lrotate(keyByte, i % 8), 0xFF)
|
|
79
|
-
* local posByte = bit32.band(i * mult, 0xFF)
|
|
80
|
-
* out[i] = string.char(bit32.bxor(data[i], bit32.bxor(mixed, posByte)))
|
|
81
|
-
* end
|
|
82
|
-
* return table.concat(out)
|
|
83
|
-
* end
|
|
84
|
-
*/
|
|
85
|
-
function buildDecoderStatement(name: string) {
|
|
86
|
-
const dataParam = randomName()
|
|
87
|
-
const keysParam = randomName()
|
|
88
|
-
const multParam = randomName()
|
|
89
|
-
const outVar = randomName()
|
|
90
|
-
const iVar = randomName()
|
|
91
|
-
const nKeysVar = randomName()
|
|
92
|
-
|
|
93
|
-
const idxExpr = binary(
|
|
94
|
-
"+",
|
|
95
|
-
binary("%", binary("-", identifier(iVar), vmNumberLiteral(1)), identifier(nKeysVar)),
|
|
96
|
-
vmNumberLiteral(1),
|
|
97
|
-
)
|
|
98
|
-
const keyByte = index(identifier(keysParam), idxExpr)
|
|
99
|
-
const mixed = call(member(identifier("bit32"), "band"), [
|
|
100
|
-
call(member(identifier("bit32"), "lrotate"), [keyByte, binary("%", identifier(iVar), vmNumberLiteral(8))]),
|
|
101
|
-
vmNumberLiteral(0xFF),
|
|
102
|
-
])
|
|
103
|
-
const posByte = call(member(identifier("bit32"), "band"), [
|
|
104
|
-
binary("*", identifier(iVar), identifier(multParam)),
|
|
105
|
-
vmNumberLiteral(0xFF),
|
|
106
|
-
])
|
|
107
|
-
const keystream = call(member(identifier("bit32"), "bxor"), [mixed, posByte])
|
|
108
|
-
const finalByte = call(member(identifier("bit32"), "bxor"), [
|
|
109
|
-
index(identifier(dataParam), identifier(iVar)),
|
|
110
|
-
keystream,
|
|
111
|
-
])
|
|
112
|
-
|
|
113
|
-
const body = block([
|
|
114
|
-
localStatement(nKeysVar, unary("#", identifier(keysParam))),
|
|
115
|
-
localStatement(outVar, table([])),
|
|
116
|
-
numericForStatement(
|
|
117
|
-
iVar,
|
|
118
|
-
vmNumberLiteral(1),
|
|
119
|
-
unary("#", identifier(dataParam)),
|
|
120
|
-
block([
|
|
121
|
-
assignmentStatement(
|
|
122
|
-
[index(identifier(outVar), identifier(iVar))],
|
|
123
|
-
[call(member(identifier("string"), "char"), [finalByte])],
|
|
124
|
-
),
|
|
125
|
-
]),
|
|
126
|
-
),
|
|
127
|
-
returnStatement([call(member(identifier("table"), "concat"), [identifier(outVar)])]),
|
|
128
|
-
])
|
|
129
|
-
|
|
130
|
-
return localFunctionStatement(
|
|
131
|
-
name,
|
|
132
|
-
functionBody([functionParam(dataParam), functionParam(keysParam), functionParam(multParam)], body),
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function runEncryptStrings(program: Program, _options: EncryptStringsOptions): void {
|
|
137
|
-
const decoderName = randomName()
|
|
138
|
-
let used = false
|
|
139
|
-
|
|
140
|
-
transformExpressions(program, (expr: Expression) => {
|
|
141
|
-
if (expr.type !== "StringLiteral") return
|
|
142
|
-
if (expr.value.length === 0) return
|
|
143
|
-
|
|
144
|
-
used = true
|
|
145
|
-
|
|
146
|
-
const keyLen = randomInt(MIN_KEY_LEN, MAX_KEY_LEN)
|
|
147
|
-
const keys = Array.from({ length: keyLen }, () => randomInt(1, 255))
|
|
148
|
-
const mult = randomInt(1, 255)
|
|
149
|
-
|
|
150
|
-
const plainBytes = toUtf8Bytes(expr.value)
|
|
151
|
-
const cipherBytes = plainBytes.map((b, idx) => b ^ keystreamByte(keys, mult, idx + 1))
|
|
152
|
-
|
|
153
|
-
const dataTable = table(cipherBytes.map((b) => positionalField(vmNumberLiteral(b))))
|
|
154
|
-
const keysTable = table(keys.map((k) => positionalField(obfuscatedNumber(k))))
|
|
155
|
-
const multExpr = obfuscatedNumber(mult)
|
|
156
|
-
|
|
157
|
-
return call(identifier(decoderName), [dataTable, keysTable, multExpr])
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
if (!used) return
|
|
161
|
-
|
|
162
|
-
program.body.statements.unshift(buildDecoderStatement(decoderName))
|
|
1
|
+
import type { Program, Expression } from "luau-parser"
|
|
2
|
+
import { transformExpressions } from "./walk"
|
|
3
|
+
import {
|
|
4
|
+
identifier, vmNumberLiteral, call, member, index, table, positionalField,
|
|
5
|
+
localFunctionStatement, functionParam, functionBody, block, localStatement,
|
|
6
|
+
assignmentStatement, numericForStatement, returnStatement, unary, binary,
|
|
7
|
+
} from "./nodeFactory"
|
|
8
|
+
|
|
9
|
+
export interface EncryptStringsOptions {}
|
|
10
|
+
|
|
11
|
+
const MIN_KEY_LEN = 4
|
|
12
|
+
const MAX_KEY_LEN = 12
|
|
13
|
+
|
|
14
|
+
function randomInt(min: number, max: number): number {
|
|
15
|
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function randomName(): string {
|
|
19
|
+
return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Luau 소스는 UTF-8 바이트 스트림이므로 charCodeAt이 아니라 실제 UTF-8 바이트로 변환. */
|
|
23
|
+
function toUtf8Bytes(value: string): number[] {
|
|
24
|
+
return Array.from(new TextEncoder().encode(value))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** bit32.lrotate(x, disp)와 동일한 32비트 좌회전. disp는 0~31 범위로 정규화. */
|
|
28
|
+
function lrotate32(x: number, disp: number): number {
|
|
29
|
+
const d = ((disp % 32) + 32) % 32
|
|
30
|
+
const xu = x >>> 0
|
|
31
|
+
if (d === 0) return xu
|
|
32
|
+
return ((xu << d) | (xu >>> (32 - d))) >>> 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* i번째(1-indexed) 바이트에 적용할 키스트림 바이트.
|
|
37
|
+
* - 롤링 키 배열에서 위치별로 다른 키 바이트를 뽑고
|
|
38
|
+
* - 좌회전으로 비트를 섞은 뒤
|
|
39
|
+
* - 위치(i)에 종속적인 바이트와 다시 섞는다.
|
|
40
|
+
* 같은 평문 바이트가 반복돼도 위치가 다르면 암호문 바이트가 달라져
|
|
41
|
+
* (반복 키 XOR과 달리) 단순 빈도분석/Kasiski류 공격에 내성을 갖는다.
|
|
42
|
+
*/
|
|
43
|
+
function keystreamByte(keys: number[], mult: number, i: number): number {
|
|
44
|
+
const nKeys = keys.length
|
|
45
|
+
const keyByte = keys[(i - 1) % nKeys]
|
|
46
|
+
const rotated = lrotate32(keyByte, i % 8) & 0xFF
|
|
47
|
+
const posByte = (i * mult) & 0xFF
|
|
48
|
+
return (rotated ^ posByte) & 0xFF
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 숫자 리터럴을 곧이곧대로 노출하지 않고, 산술식/bit32.bxor 호출로 위장한다.
|
|
53
|
+
* "decoder(literal, literal)" 패턴을 그대로 상수 접기(constant fold)하는
|
|
54
|
+
* 단순 정적 분석 스크립트를 무력화하기 위함.
|
|
55
|
+
*/
|
|
56
|
+
function obfuscatedNumber(value: number): Expression {
|
|
57
|
+
const variant = randomInt(0, 2)
|
|
58
|
+
if (variant === 0) {
|
|
59
|
+
const a = randomInt(0, value)
|
|
60
|
+
return binary("+", vmNumberLiteral(a), vmNumberLiteral(value - a))
|
|
61
|
+
}
|
|
62
|
+
if (variant === 1) {
|
|
63
|
+
const a = randomInt(0, 0xFFFFFF)
|
|
64
|
+
return binary("-", vmNumberLiteral(value + a), vmNumberLiteral(a))
|
|
65
|
+
}
|
|
66
|
+
const a = randomInt(0, 0xFFFFFF)
|
|
67
|
+
const b = (value ^ a) >>> 0
|
|
68
|
+
return call(member(identifier("bit32"), "bxor"), [vmNumberLiteral(a), vmNumberLiteral(b)])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 런타임 디코더를 조립:
|
|
73
|
+
* local function <name>(data, keys, mult)
|
|
74
|
+
* local nKeys = #keys
|
|
75
|
+
* local out = {}
|
|
76
|
+
* for i = 1, #data do
|
|
77
|
+
* local keyByte = keys[((i - 1) % nKeys) + 1]
|
|
78
|
+
* local mixed = bit32.band(bit32.lrotate(keyByte, i % 8), 0xFF)
|
|
79
|
+
* local posByte = bit32.band(i * mult, 0xFF)
|
|
80
|
+
* out[i] = string.char(bit32.bxor(data[i], bit32.bxor(mixed, posByte)))
|
|
81
|
+
* end
|
|
82
|
+
* return table.concat(out)
|
|
83
|
+
* end
|
|
84
|
+
*/
|
|
85
|
+
function buildDecoderStatement(name: string) {
|
|
86
|
+
const dataParam = randomName()
|
|
87
|
+
const keysParam = randomName()
|
|
88
|
+
const multParam = randomName()
|
|
89
|
+
const outVar = randomName()
|
|
90
|
+
const iVar = randomName()
|
|
91
|
+
const nKeysVar = randomName()
|
|
92
|
+
|
|
93
|
+
const idxExpr = binary(
|
|
94
|
+
"+",
|
|
95
|
+
binary("%", binary("-", identifier(iVar), vmNumberLiteral(1)), identifier(nKeysVar)),
|
|
96
|
+
vmNumberLiteral(1),
|
|
97
|
+
)
|
|
98
|
+
const keyByte = index(identifier(keysParam), idxExpr)
|
|
99
|
+
const mixed = call(member(identifier("bit32"), "band"), [
|
|
100
|
+
call(member(identifier("bit32"), "lrotate"), [keyByte, binary("%", identifier(iVar), vmNumberLiteral(8))]),
|
|
101
|
+
vmNumberLiteral(0xFF),
|
|
102
|
+
])
|
|
103
|
+
const posByte = call(member(identifier("bit32"), "band"), [
|
|
104
|
+
binary("*", identifier(iVar), identifier(multParam)),
|
|
105
|
+
vmNumberLiteral(0xFF),
|
|
106
|
+
])
|
|
107
|
+
const keystream = call(member(identifier("bit32"), "bxor"), [mixed, posByte])
|
|
108
|
+
const finalByte = call(member(identifier("bit32"), "bxor"), [
|
|
109
|
+
index(identifier(dataParam), identifier(iVar)),
|
|
110
|
+
keystream,
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
const body = block([
|
|
114
|
+
localStatement(nKeysVar, unary("#", identifier(keysParam))),
|
|
115
|
+
localStatement(outVar, table([])),
|
|
116
|
+
numericForStatement(
|
|
117
|
+
iVar,
|
|
118
|
+
vmNumberLiteral(1),
|
|
119
|
+
unary("#", identifier(dataParam)),
|
|
120
|
+
block([
|
|
121
|
+
assignmentStatement(
|
|
122
|
+
[index(identifier(outVar), identifier(iVar))],
|
|
123
|
+
[call(member(identifier("string"), "char"), [finalByte])],
|
|
124
|
+
),
|
|
125
|
+
]),
|
|
126
|
+
),
|
|
127
|
+
returnStatement([call(member(identifier("table"), "concat"), [identifier(outVar)])]),
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
return localFunctionStatement(
|
|
131
|
+
name,
|
|
132
|
+
functionBody([functionParam(dataParam), functionParam(keysParam), functionParam(multParam)], body),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function runEncryptStrings(program: Program, _options: EncryptStringsOptions): void {
|
|
137
|
+
const decoderName = randomName()
|
|
138
|
+
let used = false
|
|
139
|
+
|
|
140
|
+
transformExpressions(program, (expr: Expression) => {
|
|
141
|
+
if (expr.type !== "StringLiteral") return
|
|
142
|
+
if (expr.value.length === 0) return
|
|
143
|
+
|
|
144
|
+
used = true
|
|
145
|
+
|
|
146
|
+
const keyLen = randomInt(MIN_KEY_LEN, MAX_KEY_LEN)
|
|
147
|
+
const keys = Array.from({ length: keyLen }, () => randomInt(1, 255))
|
|
148
|
+
const mult = randomInt(1, 255)
|
|
149
|
+
|
|
150
|
+
const plainBytes = toUtf8Bytes(expr.value)
|
|
151
|
+
const cipherBytes = plainBytes.map((b, idx) => b ^ keystreamByte(keys, mult, idx + 1))
|
|
152
|
+
|
|
153
|
+
const dataTable = table(cipherBytes.map((b) => positionalField(vmNumberLiteral(b))))
|
|
154
|
+
const keysTable = table(keys.map((k) => positionalField(obfuscatedNumber(k))))
|
|
155
|
+
const multExpr = obfuscatedNumber(mult)
|
|
156
|
+
|
|
157
|
+
return call(identifier(decoderName), [dataTable, keysTable, multExpr])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (!used) return
|
|
161
|
+
|
|
162
|
+
program.body.statements.unshift(buildDecoderStatement(decoderName))
|
|
163
163
|
}
|