luau-obfuscator 1.0.5 → 1.0.7

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.
@@ -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
  }