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