Skip to content

Commit d413c47

Browse files
committed
StringSwitchTransformer
Stability: 3
1 parent abf48a3 commit d413c47

File tree

4 files changed

+189
-0
lines changed

4 files changed

+189
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.gradle
2+
.idea
3+
build/

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ recommended to only enable computeMaxs and disable controlflow obfuscation when
5454
* [X] [5]NumberEncrypt
5555
* [X] [5]FloatingPointEncrypt
5656
* [X] [5]StringEncrypt
57+
* [X] [3]StringSwitch
5758

5859
### Redirect
5960

grunt-main/src/main/kotlin/net/spartanb312/grunt/process/Transformers.kt

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ object Transformers : Collection<Transformer> by mutableListOf(
3333
ClonedClassTransformer order 10,
3434
TrashClassTransformer order 11,
3535
HWIDAuthenticatorTransformer order 12,
36+
StringSwitchTransformer order 18,
3637
//ControlflowTransformer order 20,
3738
StringEncryptTransformer order 30,
3839
NumberEncryptTransformer order 31,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package net.spartanb312.grunt.process.transformers.encrypt
2+
3+
import net.spartanb312.grunt.config.setting
4+
import net.spartanb312.grunt.process.MethodProcessor
5+
import net.spartanb312.grunt.process.Transformer
6+
import net.spartanb312.grunt.process.resource.ResourceCache
7+
import net.spartanb312.grunt.utils.builder.*
8+
import net.spartanb312.grunt.utils.count
9+
import net.spartanb312.grunt.utils.extensions.isInterface
10+
import net.spartanb312.grunt.utils.getRandomString
11+
import net.spartanb312.grunt.utils.logging.Logger
12+
import org.objectweb.asm.Opcodes
13+
import org.objectweb.asm.Type
14+
import org.objectweb.asm.tree.*
15+
import kotlin.random.Random
16+
17+
object StringSwitchTransformer : Transformer("StringSwitch", category = Category.Encryption), MethodProcessor {
18+
private val times by setting("Intensity", 1)
19+
private val exclusion by setting("Exclusions", listOf())
20+
21+
override fun ResourceCache.transform() {
22+
Logger.info(" - Encrypting strings...")
23+
val count = count {
24+
repeat(times) { t ->
25+
if (times > 1) Logger.info(" Encrypting strings ${t + 1} of $times times")
26+
nonExcluded.asSequence()
27+
.filter { c -> !c.isInterface
28+
&& c.version > Opcodes.V1_5
29+
&& exclusion.none {
30+
c.name.startsWith(it) }
31+
}
32+
.forEach { classNode ->
33+
classNode.methods.toList().forEach { methodNode ->
34+
if (EncryptionMethod.FlattenedSwitch.transform(classNode, methodNode)) add()
35+
}
36+
}
37+
}
38+
}.get()
39+
Logger.info(" Encrypted $count strings")
40+
}
41+
42+
override fun transformMethod(owner: ClassNode, method: MethodNode) {
43+
EncryptionMethod.FlattenedSwitch.transform(owner, method)
44+
}
45+
46+
enum class EncryptionMethod {
47+
FlattenedSwitch {
48+
override fun transform(owner: ClassNode, method: MethodNode): Boolean {
49+
var transformed = false
50+
method.instructions.toList().forEach insn@{ insn ->
51+
if ((insn is LdcInsnNode && insn.cst is String)) {
52+
val (decrypt, newString) =
53+
generateDecryptMethod(
54+
insn.cst as String,
55+
owner,
56+
) ?: return@insn
57+
58+
method.instructions.insertBefore(insn, insnList {
59+
LDC(newString)
60+
INVOKESTATIC(owner.name, decrypt.name, decrypt.desc)
61+
})
62+
method.instructions.remove(insn)
63+
transformed = true
64+
}
65+
}
66+
return transformed
67+
}
68+
69+
private fun generateDecryptMethod(
70+
rawString: String,
71+
classNode: ClassNode
72+
): Pair<MethodNode, String>? {
73+
if (rawString.length <= 1) return null
74+
75+
val keys = buildList { repeat(rawString.length) { add(Random.nextInt()) } }
76+
val encryptedString = buildString {
77+
rawString.forEachIndexed { i, c ->
78+
append((c.code xor keys[i]).toChar())
79+
}
80+
}
81+
require(encryptedString.length == rawString.length)
82+
val cases = rawString.indices.toList().shuffled()
83+
val casesMapping = mutableMapOf<Int, InsnList>()
84+
// here instancing LabelNode directly is soundness because we only use each node for one time.
85+
val labels = Array(cases.size) { LabelNode() }
86+
val decryptMethod = method(Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC,
87+
getRandomString(10), "(Ljava/lang/String;)Ljava/lang/String;") {
88+
var currentChar = 0
89+
var currentKey = keys[currentChar]
90+
val dispatchLabel = LabelNode()
91+
InsnList {
92+
// 0 -> encrypted string
93+
// 1 -> stringbuilder
94+
// 2 -> current char's decrypt key(transformed before decrypt in each switch case)
95+
// 3 -> program counter
96+
NEW("java/lang/StringBuilder")
97+
DUP
98+
INVOKESPECIAL("java/lang/StringBuilder", "<init>", "()V")
99+
ASTORE(1)
100+
LDC(keys[currentChar])
101+
ISTORE(2) // decrypt key
102+
LDC(cases[currentChar])
103+
ISTORE(3) // pc
104+
+dispatchLabel
105+
ILOAD(3)
106+
+TableSwitchInsnNode(0, rawString.length - 1, labels[cases.last()], *labels)
107+
}
108+
109+
casesMapping[cases[currentChar]] = insnList {
110+
// the first case uses decrypt key directly
111+
+labels[cases[currentChar]]
112+
ALOAD(1) // builder
113+
ALOAD(0) // raw str
114+
require(currentChar == 0)
115+
LDC(currentChar)
116+
INVOKEVIRTUAL("java/lang/String", "charAt", "(I)C")
117+
ILOAD(2)
118+
IXOR
119+
INVOKEVIRTUAL("java/lang/StringBuilder",
120+
"append", "(C)Ljava/lang/StringBuilder;")
121+
POP
122+
123+
currentChar++
124+
LDC(cases[currentChar])
125+
ISTORE(3) // pc
126+
GOTO(dispatchLabel)
127+
}
128+
129+
while (currentChar < rawString.length - 1) {
130+
casesMapping[cases[currentChar]] = insnList {
131+
+labels[cases[currentChar]]
132+
LDC(keys[currentChar])
133+
ISTORE(2)
134+
currentKey = keys[currentChar]
135+
require(currentKey == keys[currentChar])
136+
137+
ALOAD(1)
138+
ALOAD(0)
139+
LDC(currentChar)
140+
INVOKEVIRTUAL("java/lang/String", "charAt", "(I)C")
141+
ILOAD(2) // key
142+
IXOR
143+
INVOKEVIRTUAL("java/lang/StringBuilder",
144+
"append", "(C)Ljava/lang/StringBuilder;")
145+
POP
146+
currentChar++
147+
LDC(cases[currentChar])
148+
ISTORE(3) // pc
149+
GOTO(dispatchLabel)
150+
}
151+
}
152+
153+
casesMapping.toList().sortedBy { it.first }.forEach { (_, insnList) ->
154+
+insnList
155+
}
156+
157+
InsnList {
158+
+labels[cases.last()]
159+
LDC(keys.last())
160+
ISTORE(2)
161+
currentKey = keys.last()
162+
require(currentKey == keys.last())
163+
ALOAD(1)
164+
ALOAD(0)
165+
LDC(currentChar)
166+
INVOKEVIRTUAL("java/lang/String", "charAt", "(I)C")
167+
ILOAD(2)
168+
IXOR
169+
INVOKEVIRTUAL("java/lang/StringBuilder",
170+
"append", "(C)Ljava/lang/StringBuilder;")
171+
POP
172+
ALOAD(1)
173+
INVOKEVIRTUAL("java/lang/StringBuilder", "toString", "()Ljava/lang/String;")
174+
ARETURN
175+
}
176+
}
177+
classNode.methods.add(decryptMethod)
178+
return decryptMethod to encryptedString
179+
}
180+
};
181+
182+
abstract fun transform(owner: ClassNode, method: MethodNode): Boolean
183+
}
184+
}

0 commit comments

Comments
 (0)