luau-obfuscator 1.0.3 → 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.
@@ -1,115 +1,115 @@
1
- -- ObfuscationTest.lua
2
- --
3
- -- 사용법:
4
- -- 1) 이 스크립트를 원본 그대로 한 번 Roblox Studio(ServerScriptService 등)에서 실행해서
5
- -- 콘솔에 "ALL TESTS PASSED" 가 뜨는지 확인 (원본이 정상 동작하는지 베이스라인 확인).
6
- -- 2) 이 파일을 obfuscator에 통과시켜 나온 결과물을 같은 위치에 붙여넣고 다시 실행.
7
- -- 3) 두 경우 모두 콘솔 출력이 "[OK] ..." 목록 + 마지막 "ALL TESTS PASSED"로 동일하면
8
- -- 난독화가 값을 깨뜨리지 않았다는 뜻.
9
- -- 중간에 assert가 실패하면 어떤 케이스가 깨졌는지 에러 메시지로 바로 알 수 있음.
10
-
11
- local failCount = 0
12
-
13
- local function check(name, actual, expected)
14
- if actual == expected then
15
- print(string.format("[OK] %s -> %s", name, tostring(actual)))
16
- else
17
- failCount += 1
18
- warn(string.format(
19
- "[FAIL] %s -> got %s (%s), expected %s (%s)",
20
- name, tostring(actual), typeof(actual), tostring(expected), typeof(expected)
21
- ))
22
- end
23
- end
24
-
25
- print("===== ObfuscationTest start =====")
26
-
27
- -- 1) 문자열 리터럴: ASCII / 한글 / 이모지 / 빈 문자열 / 특수문자(따옴표, 개행, 이스케이프)
28
- check("ascii string", "Hello, Roblox!", "Hello, Roblox!")
29
- check("korean string", "안녕하세요, 로블록스", "안녕하세요, 로블록스")
30
- check("emoji string", "😀🔥✨", "😀🔥✨")
31
- check("empty string", "", "")
32
- check("quote/escape string", "he said \"hi\"\nnext line\ttab", "he said \"hi\"\nnext line\ttab")
33
-
34
- -- 2) 문자열 연결(concat) : 리터럴끼리, 변수와 리터럴 혼합
35
- local name = "World"
36
- check("concat literal+var", "Hello, " .. name .. "! Welcome to Roblox.", "Hello, World! Welcome to Roblox.")
37
-
38
- local parts = "A" .. "B" .. "C" .. "D" .. "E" .. "F" .. "G" .. "H"
39
- check("long concat chain", parts, "ABCDEFGH")
40
-
41
- -- 3) 숫자 리터럴: 0, 양수, 음수, 큰 정수, 소수
42
- check("zero", 0, 0)
43
- check("positive int", 42, 42)
44
- check("negative int", -17, -17)
45
- check("large int", 1234567, 1234567)
46
- check("negative large int", -987654, -987654)
47
- check("float", 3.14, 3.14)
48
- check("negative float", -2.5, -2.5)
49
-
50
- -- 4) 산술식으로 위장됐을 법한 값들의 실제 계산 결과
51
- local total = 5 + 10 * 2
52
- check("arithmetic literal combo", total, 25)
53
-
54
- local mixed = (100 - 37) * 2 + 1
55
- check("mixed arithmetic", mixed, 127)
56
-
57
- -- 5) 테이블 안의 숫자 리터럴들
58
- local nums = { 1, 2, 3, 4, 5, -6, 7, 100 }
59
- local sum = 0
60
- for i = 1, #nums do
61
- sum += nums[i]
62
- end
63
- check("table of numbers sum", sum, 116)
64
-
65
- -- 6) 테이블 안의 문자열 리터럴들 + 순회
66
- local words = { "alpha", "beta", "gamma", "delta" }
67
- local joined = table.concat(words, "-")
68
- check("table of strings joined", joined, "alpha-beta-gamma-delta")
69
-
70
- -- 7) 반복문 경계값(숫자 리터럴)이 정확히 유지되는지
71
- local loopSum = 0
72
- for i = 1, 10 do
73
- loopSum += i
74
- end
75
- check("for-loop 1..10 sum", loopSum, 55)
76
-
77
- local countdown = 0
78
- local n = 5
79
- while n > 0 do
80
- countdown += 1
81
- n -= 1
82
- end
83
- check("while-loop countdown", countdown, 5)
84
-
85
- -- 8) 함수 인자/리턴 값으로 쓰인 문자열, 숫자
86
- local function greet(personName, times)
87
- local msg = ""
88
- for i = 1, times do
89
- msg = msg .. personName .. ";"
90
- end
91
- return msg
92
- end
93
- check("function with literal args", greet("Bob", 3), "Bob;Bob;Bob;")
94
-
95
- -- 9) 재귀 함수 자체가 난독화 후에도 정상 동작하는지 (사용자 스크립트 자체의 재귀, 별개 검증용)
96
- local function factorial(k)
97
- if k <= 1 then
98
- return 1
99
- end
100
- return k * factorial(k - 1)
101
- end
102
- check("recursive factorial(6)", factorial(6), 720)
103
-
104
- -- 10) 문자열 인덱싱/길이 (UTF-8 바이트 변환이 안 깨졌는지 간접 확인)
105
- check("string length ascii", #"Roblox", 6)
106
- check("string.upper", string.upper("mixedCase"), "MIXEDCASE")
107
- check("string.sub", string.sub("Hello, Roblox!", 1, 5), "Hello")
108
-
109
- print("===== ObfuscationTest end =====")
110
-
111
- if failCount == 0 then
112
- print("ALL TESTS PASSED")
113
- else
114
- warn(string.format("%d TEST(S) FAILED", failCount))
1
+ -- ObfuscationTest.lua
2
+ --
3
+ -- 사용법:
4
+ -- 1) 이 스크립트를 원본 그대로 한 번 Roblox Studio(ServerScriptService 등)에서 실행해서
5
+ -- 콘솔에 "ALL TESTS PASSED" 가 뜨는지 확인 (원본이 정상 동작하는지 베이스라인 확인).
6
+ -- 2) 이 파일을 obfuscator에 통과시켜 나온 결과물을 같은 위치에 붙여넣고 다시 실행.
7
+ -- 3) 두 경우 모두 콘솔 출력이 "[OK] ..." 목록 + 마지막 "ALL TESTS PASSED"로 동일하면
8
+ -- 난독화가 값을 깨뜨리지 않았다는 뜻.
9
+ -- 중간에 assert가 실패하면 어떤 케이스가 깨졌는지 에러 메시지로 바로 알 수 있음.
10
+
11
+ local failCount = 0
12
+
13
+ local function check(name, actual, expected)
14
+ if actual == expected then
15
+ print(string.format("[OK] %s -> %s", name, tostring(actual)))
16
+ else
17
+ failCount += 1
18
+ warn(string.format(
19
+ "[FAIL] %s -> got %s (%s), expected %s (%s)",
20
+ name, tostring(actual), typeof(actual), tostring(expected), typeof(expected)
21
+ ))
22
+ end
23
+ end
24
+
25
+ print("===== ObfuscationTest start =====")
26
+
27
+ -- 1) 문자열 리터럴: ASCII / 한글 / 이모지 / 빈 문자열 / 특수문자(따옴표, 개행, 이스케이프)
28
+ check("ascii string", "Hello, Roblox!", "Hello, Roblox!")
29
+ check("korean string", "안녕하세요, 로블록스", "안녕하세요, 로블록스")
30
+ check("emoji string", "😀🔥✨", "😀🔥✨")
31
+ check("empty string", "", "")
32
+ check("quote/escape string", "he said \"hi\"\nnext line\ttab", "he said \"hi\"\nnext line\ttab")
33
+
34
+ -- 2) 문자열 연결(concat) : 리터럴끼리, 변수와 리터럴 혼합
35
+ local name = "World"
36
+ check("concat literal+var", "Hello, " .. name .. "! Welcome to Roblox.", "Hello, World! Welcome to Roblox.")
37
+
38
+ local parts = "A" .. "B" .. "C" .. "D" .. "E" .. "F" .. "G" .. "H"
39
+ check("long concat chain", parts, "ABCDEFGH")
40
+
41
+ -- 3) 숫자 리터럴: 0, 양수, 음수, 큰 정수, 소수
42
+ check("zero", 0, 0)
43
+ check("positive int", 42, 42)
44
+ check("negative int", -17, -17)
45
+ check("large int", 1234567, 1234567)
46
+ check("negative large int", -987654, -987654)
47
+ check("float", 3.14, 3.14)
48
+ check("negative float", -2.5, -2.5)
49
+
50
+ -- 4) 산술식으로 위장됐을 법한 값들의 실제 계산 결과
51
+ local total = 5 + 10 * 2
52
+ check("arithmetic literal combo", total, 25)
53
+
54
+ local mixed = (100 - 37) * 2 + 1
55
+ check("mixed arithmetic", mixed, 127)
56
+
57
+ -- 5) 테이블 안의 숫자 리터럴들
58
+ local nums = { 1, 2, 3, 4, 5, -6, 7, 100 }
59
+ local sum = 0
60
+ for i = 1, #nums do
61
+ sum += nums[i]
62
+ end
63
+ check("table of numbers sum", sum, 116)
64
+
65
+ -- 6) 테이블 안의 문자열 리터럴들 + 순회
66
+ local words = { "alpha", "beta", "gamma", "delta" }
67
+ local joined = table.concat(words, "-")
68
+ check("table of strings joined", joined, "alpha-beta-gamma-delta")
69
+
70
+ -- 7) 반복문 경계값(숫자 리터럴)이 정확히 유지되는지
71
+ local loopSum = 0
72
+ for i = 1, 10 do
73
+ loopSum += i
74
+ end
75
+ check("for-loop 1..10 sum", loopSum, 55)
76
+
77
+ local countdown = 0
78
+ local n = 5
79
+ while n > 0 do
80
+ countdown += 1
81
+ n -= 1
82
+ end
83
+ check("while-loop countdown", countdown, 5)
84
+
85
+ -- 8) 함수 인자/리턴 값으로 쓰인 문자열, 숫자
86
+ local function greet(personName, times)
87
+ local msg = ""
88
+ for i = 1, times do
89
+ msg = msg .. personName .. ";"
90
+ end
91
+ return msg
92
+ end
93
+ check("function with literal args", greet("Bob", 3), "Bob;Bob;Bob;")
94
+
95
+ -- 9) 재귀 함수 자체가 난독화 후에도 정상 동작하는지 (사용자 스크립트 자체의 재귀, 별개 검증용)
96
+ local function factorial(k)
97
+ if k <= 1 then
98
+ return 1
99
+ end
100
+ return k * factorial(k - 1)
101
+ end
102
+ check("recursive factorial(6)", factorial(6), 720)
103
+
104
+ -- 10) 문자열 인덱싱/길이 (UTF-8 바이트 변환이 안 깨졌는지 간접 확인)
105
+ check("string length ascii", #"Roblox", 6)
106
+ check("string.upper", string.upper("mixedCase"), "MIXEDCASE")
107
+ check("string.sub", string.sub("Hello, Roblox!", 1, 5), "Hello")
108
+
109
+ print("===== ObfuscationTest end =====")
110
+
111
+ if failCount == 0 then
112
+ print("ALL TESTS PASSED")
113
+ else
114
+ warn(string.format("%d TEST(S) FAILED", failCount))
115
115
  end
@@ -1,91 +1,91 @@
1
- import type { Program, TableField } from "luau-parser"
2
- import { transformExpressions } from "./walk"
3
- import {
4
- stringLiteral, numberLiteral, vmNumberLiteral, index as indexExpr, identifier, table, localStatement,
5
- vmStructuralNumbers,
6
- } from "./nodeFactory"
7
-
8
- export interface ConstantArrayOptions {
9
- /** 상수 배열을 담을 local 변수 이름. 생략하면 랜덤 생성됨
10
- * (어차피 RenameVariables가 이후에 한 번 더 이름을 바꿔줌). */
11
- arrayName?: string
12
- }
13
-
14
- type Entry = { kind: "string"; value: string } | { kind: "number"; value: number }
15
- type ConstKey = string
16
-
17
- function keyOf(kind: Entry["kind"], value: string | number): ConstKey {
18
- return kind === "string" ? `s:${value}` : `n:${value}`
19
- }
20
-
21
- function shuffleIndices(length: number): number[] {
22
- const order = Array.from({ length }, (_, i) => i)
23
- for (let i = order.length - 1; i > 0; i--) {
24
- const j = Math.floor(Math.random() * (i + 1))
25
- ;[order[i], order[j]] = [order[j], order[i]]
26
- }
27
- return order
28
- }
29
-
30
- function defaultArrayName(): string {
31
- return "_CA" + Math.random().toString(36).slice(2, 8)
32
- }
33
-
34
- export function runConstantArray(program: Program, options: ConstantArrayOptions): void {
35
- const order: Entry[] = []
36
- const seen = new Set<ConstKey>()
37
-
38
- transformExpressions(program, (expr) => {
39
- if (expr.type === "StringLiteral") {
40
- const k = keyOf("string", expr.value)
41
- if (!seen.has(k)) {
42
- seen.add(k)
43
- order.push({ kind: "string", value: expr.value })
44
- }
45
- } else if (expr.type === "NumberLiteral" && !vmStructuralNumbers.has(expr)) {
46
- const k = keyOf("number", expr.value)
47
- if (!seen.has(k)) {
48
- seen.add(k)
49
- order.push({ kind: "number", value: expr.value })
50
- }
51
- }
52
- return undefined
53
- })
54
-
55
- if (order.length === 0) return
56
-
57
- // 2) 배열 안에서 실제로 놓일 위치를 등장 순서와 다르게 섞음.
58
- // shuffledOrder[슬롯] = order 배열에서의 원래 인덱스
59
- const shuffledOrder = shuffleIndices(order.length)
60
- const indexOf = new Map<ConstKey, number>() // 상수 키 -> 1-based 배열 인덱스
61
- shuffledOrder.forEach((originalIndex, slot) => {
62
- const entry = order[originalIndex]
63
- indexOf.set(keyOf(entry.kind, entry.value), slot + 1)
64
- })
65
-
66
- const arrayName = options.arrayName ?? defaultArrayName()
67
-
68
- // 3) 기존 트리를 돌면서 리터럴을 ARR[idx] 접근으로 치환.
69
- // 배열 선언문은 아직 트리에 넣지 않은 상태에서 해야 함 — 넣고 나서 돌리면
70
- // 배열 자신의 값들까지 자기 자신을 가리키게 치환돼서 깨짐.
71
- transformExpressions(program, (expr) => {
72
- if (expr.type === "StringLiteral") {
73
- const idx = indexOf.get(keyOf("string", expr.value))!
74
- return indexExpr(identifier(arrayName), vmNumberLiteral(idx))
75
- }
76
- if (expr.type === "NumberLiteral" && !vmStructuralNumbers.has(expr)) {
77
- const idx = indexOf.get(keyOf("number", expr.value))!
78
- return indexExpr(identifier(arrayName), vmNumberLiteral(idx))
79
- }
80
- return undefined
81
- })
82
-
83
- // 4) 치환이 끝난 뒤에야 배열 선언문을 맨 위에 삽입.
84
- const fields: TableField[] = shuffledOrder.map((originalIndex) => {
85
- const entry = order[originalIndex]
86
- const value = entry.kind === "string" ? stringLiteral(entry.value) : numberLiteral(entry.value)
87
- return { type: "TableFieldPositional", value }
88
- })
89
-
90
- program.body.statements.unshift(localStatement(arrayName, table(fields)))
1
+ import type { Program, TableField } from "luau-parser"
2
+ import { transformExpressions } from "./walk"
3
+ import {
4
+ stringLiteral, numberLiteral, vmNumberLiteral, index as indexExpr, identifier, table, localStatement,
5
+ vmStructuralNumbers,
6
+ } from "./nodeFactory"
7
+
8
+ export interface ConstantArrayOptions {
9
+ /** 상수 배열을 담을 local 변수 이름. 생략하면 랜덤 생성됨
10
+ * (어차피 RenameVariables가 이후에 한 번 더 이름을 바꿔줌). */
11
+ arrayName?: string
12
+ }
13
+
14
+ type Entry = { kind: "string"; value: string } | { kind: "number"; value: number }
15
+ type ConstKey = string
16
+
17
+ function keyOf(kind: Entry["kind"], value: string | number): ConstKey {
18
+ return kind === "string" ? `s:${value}` : `n:${value}`
19
+ }
20
+
21
+ function shuffleIndices(length: number): number[] {
22
+ const order = Array.from({ length }, (_, i) => i)
23
+ for (let i = order.length - 1; i > 0; i--) {
24
+ const j = Math.floor(Math.random() * (i + 1))
25
+ ;[order[i], order[j]] = [order[j], order[i]]
26
+ }
27
+ return order
28
+ }
29
+
30
+ function defaultArrayName(): string {
31
+ return "_CA" + Math.random().toString(36).slice(2, 8)
32
+ }
33
+
34
+ export function runConstantArray(program: Program, options: ConstantArrayOptions): void {
35
+ const order: Entry[] = []
36
+ const seen = new Set<ConstKey>()
37
+
38
+ transformExpressions(program, (expr) => {
39
+ if (expr.type === "StringLiteral") {
40
+ const k = keyOf("string", expr.value)
41
+ if (!seen.has(k)) {
42
+ seen.add(k)
43
+ order.push({ kind: "string", value: expr.value })
44
+ }
45
+ } else if (expr.type === "NumberLiteral" && !vmStructuralNumbers.has(expr)) {
46
+ const k = keyOf("number", expr.value)
47
+ if (!seen.has(k)) {
48
+ seen.add(k)
49
+ order.push({ kind: "number", value: expr.value })
50
+ }
51
+ }
52
+ return undefined
53
+ })
54
+
55
+ if (order.length === 0) return
56
+
57
+ // 2) 배열 안에서 실제로 놓일 위치를 등장 순서와 다르게 섞음.
58
+ // shuffledOrder[슬롯] = order 배열에서의 원래 인덱스
59
+ const shuffledOrder = shuffleIndices(order.length)
60
+ const indexOf = new Map<ConstKey, number>() // 상수 키 -> 1-based 배열 인덱스
61
+ shuffledOrder.forEach((originalIndex, slot) => {
62
+ const entry = order[originalIndex]
63
+ indexOf.set(keyOf(entry.kind, entry.value), slot + 1)
64
+ })
65
+
66
+ const arrayName = options.arrayName ?? defaultArrayName()
67
+
68
+ // 3) 기존 트리를 돌면서 리터럴을 ARR[idx] 접근으로 치환.
69
+ // 배열 선언문은 아직 트리에 넣지 않은 상태에서 해야 함 — 넣고 나서 돌리면
70
+ // 배열 자신의 값들까지 자기 자신을 가리키게 치환돼서 깨짐.
71
+ transformExpressions(program, (expr) => {
72
+ if (expr.type === "StringLiteral") {
73
+ const idx = indexOf.get(keyOf("string", expr.value))!
74
+ return indexExpr(identifier(arrayName), vmNumberLiteral(idx))
75
+ }
76
+ if (expr.type === "NumberLiteral" && !vmStructuralNumbers.has(expr)) {
77
+ const idx = indexOf.get(keyOf("number", expr.value))!
78
+ return indexExpr(identifier(arrayName), vmNumberLiteral(idx))
79
+ }
80
+ return undefined
81
+ })
82
+
83
+ // 4) 치환이 끝난 뒤에야 배열 선언문을 맨 위에 삽입.
84
+ const fields: TableField[] = shuffledOrder.map((originalIndex) => {
85
+ const entry = order[originalIndex]
86
+ const value = entry.kind === "string" ? stringLiteral(entry.value) : numberLiteral(entry.value)
87
+ return { type: "TableFieldPositional", value }
88
+ })
89
+
90
+ program.body.statements.unshift(localStatement(arrayName, table(fields)))
91
91
  }
@@ -1,123 +1,123 @@
1
- import type { Program, Expression } from "luau-parser"
2
- import { transformExpressions } from "./walk"
3
- import {
4
- identifier, numberLiteral, call, member,
5
- localFunctionStatement, functionParam, functionBody, block, returnStatement,
6
- localStatement, binary, vmStructuralNumbers,
7
- } from "./nodeFactory"
8
-
9
- export interface EncryptNumbersOptions {}
10
-
11
- // bit32.bxor는 32비트 부호 없는 정수만 다룸. 실수/음수/범위 밖 값은 그대로 둔다
12
- // (그런 값은 NumbersToExpressions 쪽 산술식 위장으로 커버).
13
- const UINT32_MAX = 0xFFFFFFFF
14
- const MOD32 = 4294967296 // 2^32
15
-
16
- function randomInt(min: number, max: number): number {
17
- return Math.floor(Math.random() * (max - min + 1)) + min
18
- }
19
-
20
- function randomName(): string {
21
- return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
22
- }
23
-
24
- function isEncryptable(value: number): boolean {
25
- return Number.isInteger(value) && value >= 0 && value <= UINT32_MAX
26
- }
27
-
28
- /** JS의 ^ 연산자는 32비트 부호 있는 정수로 계산하므로 >>> 0으로 다시
29
- * 부호 없는 32비트 표현으로 맞춰줌 — bit32.bxor와 동일한 비트 결과가 나옴. */
30
- function xor32(a: number, b: number): number {
31
- return (a ^ b) >>> 0
32
- }
33
-
34
- /** Luau의 %는 항상 피제수와 같은 부호(양수 나눗셈이면 0 이상)를 반환하므로
35
- * 음수가 나와도 자동으로 [0, 2^32) 범위로 랩어라운드된다. */
36
- function modAdd32(a: number, b: number): number {
37
- return ((a + b) % MOD32 + MOD32) % MOD32
38
- }
39
-
40
- /**
41
- * 숫자 리터럴을 곧이곧대로 노출하지 않고 산술식/bit32.bxor 호출로 위장한다.
42
- * "decoder(literal, literal, literal)" 형태를 그대로 상수 접기(constant fold)
43
- * 하는 단순 정적 분석 스크립트를 무력화하기 위함.
44
- */
45
- function obfuscatedNumber(value: number): Expression {
46
- const variant = randomInt(0, 2)
47
- if (variant === 0) {
48
- const a = randomInt(0, value)
49
- return binary("+", numberLiteral(a), numberLiteral(value - a))
50
- }
51
- if (variant === 1) {
52
- const a = randomInt(0, 0xFFFFFF)
53
- return binary("-", numberLiteral(value + a), numberLiteral(a))
54
- }
55
- const a = randomInt(0, 0xFFFFFF)
56
- const b = xor32(value, a)
57
- return call(member(identifier("bit32"), "bxor"), [numberLiteral(a), numberLiteral(b)])
58
- }
59
-
60
- /**
61
- * 런타임 디코더를 조립. 단순 XOR 한 번이 아니라 XOR + 모듈러 덧셈 두 단계를 거치므로,
62
- * "bit32.bxor(literalA, literalB)" 같은 단일 호출 패턴 매칭으로는 상수 접기가 안 된다:
63
- *
64
- * local function <name>(x, keyA, keyB)
65
- * local t = (x - keyB) % 4294967296
66
- * return bit32.bxor(t, keyA)
67
- * end
68
- */
69
- function buildDecoderStatement(name: string) {
70
- const xParam = randomName()
71
- const keyAParam = randomName()
72
- const keyBParam = randomName()
73
- const tVar = randomName()
74
-
75
- const body = block([
76
- localStatement(
77
- tVar,
78
- binary(
79
- "%",
80
- binary("-", identifier(xParam), identifier(keyBParam)),
81
- numberLiteral(MOD32),
82
- ),
83
- ),
84
- returnStatement([call(member(identifier("bit32"), "bxor"), [identifier(tVar), identifier(keyAParam)])]),
85
- ])
86
-
87
- return localFunctionStatement(
88
- name,
89
- functionBody([functionParam(xParam), functionParam(keyAParam), functionParam(keyBParam)], body),
90
- )
91
- }
92
-
93
- export function runEncryptNumbers(program: Program, _options: EncryptNumbersOptions): void {
94
- const decoderName = randomName()
95
- let used = false
96
-
97
- transformExpressions(program, (expr: Expression) => {
98
- if (expr.type !== "NumberLiteral") return
99
- if (vmStructuralNumbers.has(expr)) return
100
- if (!isEncryptable(expr.value)) return
101
-
102
- used = true
103
-
104
- const keyA = randomInt(1, 0xFFFFFF)
105
- const keyB = randomInt(1, 0xFFFFFF)
106
-
107
- // decode(encode(value)) == value 가 되도록 순전파를 정의:
108
- // t1 = value xor keyA
109
- // encoded = (t1 + keyB) mod 2^32
110
- const t1 = xor32(expr.value, keyA)
111
- const encoded = modAdd32(t1, keyB)
112
-
113
- return call(identifier(decoderName), [
114
- obfuscatedNumber(encoded),
115
- obfuscatedNumber(keyA),
116
- obfuscatedNumber(keyB),
117
- ])
118
- })
119
-
120
- if (!used) return
121
-
122
- program.body.statements.unshift(buildDecoderStatement(decoderName))
1
+ import type { Program, Expression } from "luau-parser"
2
+ import { transformExpressions } from "./walk"
3
+ import {
4
+ identifier, numberLiteral, call, member,
5
+ localFunctionStatement, functionParam, functionBody, block, returnStatement,
6
+ localStatement, binary, vmStructuralNumbers,
7
+ } from "./nodeFactory"
8
+
9
+ export interface EncryptNumbersOptions {}
10
+
11
+ // bit32.bxor는 32비트 부호 없는 정수만 다룸. 실수/음수/범위 밖 값은 그대로 둔다
12
+ // (그런 값은 NumbersToExpressions 쪽 산술식 위장으로 커버).
13
+ const UINT32_MAX = 0xFFFFFFFF
14
+ const MOD32 = 4294967296 // 2^32
15
+
16
+ function randomInt(min: number, max: number): number {
17
+ return Math.floor(Math.random() * (max - min + 1)) + min
18
+ }
19
+
20
+ function randomName(): string {
21
+ return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
22
+ }
23
+
24
+ function isEncryptable(value: number): boolean {
25
+ return Number.isInteger(value) && value >= 0 && value <= UINT32_MAX
26
+ }
27
+
28
+ /** JS의 ^ 연산자는 32비트 부호 있는 정수로 계산하므로 >>> 0으로 다시
29
+ * 부호 없는 32비트 표현으로 맞춰줌 — bit32.bxor와 동일한 비트 결과가 나옴. */
30
+ function xor32(a: number, b: number): number {
31
+ return (a ^ b) >>> 0
32
+ }
33
+
34
+ /** Luau의 %는 항상 피제수와 같은 부호(양수 나눗셈이면 0 이상)를 반환하므로
35
+ * 음수가 나와도 자동으로 [0, 2^32) 범위로 랩어라운드된다. */
36
+ function modAdd32(a: number, b: number): number {
37
+ return ((a + b) % MOD32 + MOD32) % MOD32
38
+ }
39
+
40
+ /**
41
+ * 숫자 리터럴을 곧이곧대로 노출하지 않고 산술식/bit32.bxor 호출로 위장한다.
42
+ * "decoder(literal, literal, literal)" 형태를 그대로 상수 접기(constant fold)
43
+ * 하는 단순 정적 분석 스크립트를 무력화하기 위함.
44
+ */
45
+ function obfuscatedNumber(value: number): Expression {
46
+ const variant = randomInt(0, 2)
47
+ if (variant === 0) {
48
+ const a = randomInt(0, value)
49
+ return binary("+", numberLiteral(a), numberLiteral(value - a))
50
+ }
51
+ if (variant === 1) {
52
+ const a = randomInt(0, 0xFFFFFF)
53
+ return binary("-", numberLiteral(value + a), numberLiteral(a))
54
+ }
55
+ const a = randomInt(0, 0xFFFFFF)
56
+ const b = xor32(value, a)
57
+ return call(member(identifier("bit32"), "bxor"), [numberLiteral(a), numberLiteral(b)])
58
+ }
59
+
60
+ /**
61
+ * 런타임 디코더를 조립. 단순 XOR 한 번이 아니라 XOR + 모듈러 덧셈 두 단계를 거치므로,
62
+ * "bit32.bxor(literalA, literalB)" 같은 단일 호출 패턴 매칭으로는 상수 접기가 안 된다:
63
+ *
64
+ * local function <name>(x, keyA, keyB)
65
+ * local t = (x - keyB) % 4294967296
66
+ * return bit32.bxor(t, keyA)
67
+ * end
68
+ */
69
+ function buildDecoderStatement(name: string) {
70
+ const xParam = randomName()
71
+ const keyAParam = randomName()
72
+ const keyBParam = randomName()
73
+ const tVar = randomName()
74
+
75
+ const body = block([
76
+ localStatement(
77
+ tVar,
78
+ binary(
79
+ "%",
80
+ binary("-", identifier(xParam), identifier(keyBParam)),
81
+ numberLiteral(MOD32),
82
+ ),
83
+ ),
84
+ returnStatement([call(member(identifier("bit32"), "bxor"), [identifier(tVar), identifier(keyAParam)])]),
85
+ ])
86
+
87
+ return localFunctionStatement(
88
+ name,
89
+ functionBody([functionParam(xParam), functionParam(keyAParam), functionParam(keyBParam)], body),
90
+ )
91
+ }
92
+
93
+ export function runEncryptNumbers(program: Program, _options: EncryptNumbersOptions): void {
94
+ const decoderName = randomName()
95
+ let used = false
96
+
97
+ transformExpressions(program, (expr: Expression) => {
98
+ if (expr.type !== "NumberLiteral") return
99
+ if (vmStructuralNumbers.has(expr)) return
100
+ if (!isEncryptable(expr.value)) return
101
+
102
+ used = true
103
+
104
+ const keyA = randomInt(1, 0xFFFFFF)
105
+ const keyB = randomInt(1, 0xFFFFFF)
106
+
107
+ // decode(encode(value)) == value 가 되도록 순전파를 정의:
108
+ // t1 = value xor keyA
109
+ // encoded = (t1 + keyB) mod 2^32
110
+ const t1 = xor32(expr.value, keyA)
111
+ const encoded = modAdd32(t1, keyB)
112
+
113
+ return call(identifier(decoderName), [
114
+ obfuscatedNumber(encoded),
115
+ obfuscatedNumber(keyA),
116
+ obfuscatedNumber(keyB),
117
+ ])
118
+ })
119
+
120
+ if (!used) return
121
+
122
+ program.body.statements.unshift(buildDecoderStatement(decoderName))
123
123
  }