forked from risporce/Supercell-jailbreak
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsc_protector_file_parser.py
423 lines (371 loc) · 18.8 KB
/
sc_protector_file_parser.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# decrypted bi.txt parsing
# see https://github.com/frida/frida for installation instruction on your computer (use option 1 with only pip commands to execute)
# see https://frida.re/docs/ios/ for installation instruction on your jailbroken iOS device
from datetime import datetime
import argparse, struct, sys, frida, platform, subprocess
#pip install macholib, needs to install this library
from macholib.MachO import MachO
parser = argparse.ArgumentParser()
parser.add_argument('--game', help='The game name you want to patch, enter its exact binary filename as string ex: "laser", "Clash of Clans", "Clash_Royale"')
game_code_name = {
"Hay Day": "soil",
"Clash_Royale": "scroll",
"laser": "laser",
"Squad": "squad",
"Clash of Clans": "magic",
"Boom Beach": "reef",
}
#protector bi.txt
VALUE_SEPARATOR = ";"
INIT_INDICATOR = 10
V0_INDICATOR = 0
V5_INDICATOR = 5
POINTER_SIZE = 8
SYMBOL_TABLE_INFO_LENGTH = 16
#binary
BINARY_BASE_ADDRESS = 0x100_000_000
lazyBindingFixingAddress = None # hd 1.63.204 //0x2461fe8
stringTableFixingAddress = None #String Table Address found in mach-o header in linkedit # hd 1.63.204 after mh_execute_header //0x267246e
symbolTableStartAddress = None # hd 1.63.204 after mh_execute_header //0x2662220
stringTableStartAddress = None # hd 1.63.204 //0x26713a8
startCountingAddress = None #= stringTableFixingAddress - stringTableStartAddress
exportOff = None # hd 1.63.204 //0x2473080
exportSize = None # hd 1.63.204 //0x1bdcc8
newExportOff = None # hd 1.63.204 //0x2473850
newExportSize = None # hd 1.63.204 //0x1bd4f8
newLazyBindingSize = None
lc_dyld_info_onlyStartAddress = None
nameOffset = None
####
decrypted_bi = None
fileToOpen = None
protectorLoaderPatchBytes = None
### protector loader
protectorLoaderStartAddress = None # hd 1.63.204 //0x1348s
def getProtectorPatchBytes():
global protectorLoaderPatchBytes
four_or_five_letters_codename = ["laser", "Clash of Clans", "Squad", "Clash Mini", "Boom Beach", "Hay Day"]
if fileToOpen in four_or_five_letters_codename:
protectorLoaderPatchBytes = bytearray.fromhex("180000803800000018000000010000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000")
elif fileToOpen == "Clash_Royale":
protectorLoaderPatchBytes = bytearray.fromhex("180000804000000018000000010000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000")
def is_arm_mac():
if platform.system() == "Darwin":
architecture = platform.machine()
return architecture == "arm64"
return False
def check_sip():
sip_status = subprocess.check_output(["csrutil", "status"], text=True).strip()
if "disabled" in sip_status.lower():
return True
else:
print("[WARN] SIP is enabled! Unable to use your macbook to patch...")
def check_security_args():
try:
boot_args = subprocess.check_output(["nvram", "boot-args"], text=True).strip()
# Setting these args disables almost all security features on macOS, Frida can now work without the need for code-signing free as a fish in water
required_args = [
"arm64e_preview_abi",
"thid_should_crash=0",
"tss_should_crash=0",
"amfi_get_out_of_my_way=1"
]
return all(arg in boot_args for arg in required_args)
except subprocess.CalledProcessError:
print("[ERROR] Failed to get boot args. SIP may be enabled.")
def setup():
global lazyBindingFixingAddress
global stringTableStartAddress
global newLazyBindingSize
global exportOff
global exportSize
global newExportOff
global newExportSize
global lc_dyld_info_onlyStartAddress
global protectorLoaderStartAddress
global nameOffset
loader_found = False
getProtectorPatchBytes()
binary = MachO(fileToOpen)
current_offset = 0
for header in binary.headers:
current_offset += header.header._size_
for cmd in header.commands:
load_cmd = cmd[0]
#print(load_cmd.get_cmd_name())
if load_cmd.get_cmd_name() == 'LC_DYLD_INFO_ONLY': # in clash royale new format this is inexistant so it will never enter here
lazyBindingFixingAddress = cmd[1].lazy_bind_off
newLazyBindingSize = cmd[1].lazy_bind_size + 1500 # what is this??? the size of the lazy binding section is not large enough to contain all the data,
# it's usually missing about 150-200 bytes in every binary or a number close to that, so to be sure
# i always set it to a big number so that we are sure it'll always have enough room for all the data
# but this may cause problems at some point, we never know, for now it's working
exportOff = cmd[1].export_off
exportSize = cmd[1].export_size
newExportOff = exportOff + 2000 # if we enlarge the size of the lazy binding, a solution is to trim the size of the export table since it's not all used anyway
newExportSize = exportSize - 2000
lc_dyld_info_onlyStartAddress = current_offset
print(f"[INFO] Found LC_DYLD_INFO_ONLY at offset at : {hex(current_offset)}")
elif load_cmd.get_cmd_name() == 'LC_SYMTAB': # this is here in all format so far
stringTableStartAddress = cmd[1].stroff
find__mh_execute_header_strtab_and_symbtab_offset(cmd[1].stroff, cmd[1].strsize, cmd[1].symoff, cmd[1].nsyms)
elif load_cmd.get_cmd_name() == 'LC_LOAD_DYLIB':
if loader_found:
return
# protector loader is always always the first one, so we can stop after it reached the first, this means
nameOffset = cmd[1].name
protectorLoaderStartAddress = current_offset
print(f"[INFO] Found protector loader at: {hex(current_offset)}")
loader_found = True
current_offset += load_cmd.cmdsize
def find__mh_execute_header_strtab_and_symbtab_offset(strTableStartOffset, strTableLength, symTableStartOffset, symTableNbOffsets):
global stringTableFixingAddress
global symbolTableStartAddress
global startCountingAddress
search_string = "__mh_execute_header".encode()
search_bytes_symbol = bytes.fromhex("0f0110000000000001000000") # mh_execute_header symbols data
with open(fileToOpen, 'rb') as f:
#string table index
f.seek(strTableStartOffset)
string_table = f.read(strTableLength)
string_index = string_table.find(search_string)
if string_index == -1:
print(f"Error: {search_string} not found in that range")
return None
stringTableFixingAddress = string_index + strTableStartOffset + len(search_string) +1
print(f"[INFO] Found string table fixing address at: {hex(lazyBindingFixingAddress)}")
f.seek(symTableStartOffset)
symbol_table = f.read(symTableNbOffsets * SYMBOL_TABLE_INFO_LENGTH)
symbol_index = symbol_table.find(search_bytes_symbol)
if string_index == -1:
print(f"Error: {search_bytes_symbol} not found in that range")
return None
symbolTableStartAddress = symbol_index + symTableStartOffset + len(search_bytes_symbol)
startCountingAddress = stringTableFixingAddress - stringTableStartAddress
print(f"[INFO] Found symbol table start address at: {hex(symbolTableStartAddress)}")
f.close()
def on_message(message, data):
global decrypted_bi
if message['type'] == 'send':
decrypted_bi = message['payload']
session.detach()
lines = decrypted_bi.splitlines()
mainFixing(lines)
elif message['type'] == 'error':
print(f"[ERROR] {message['stack']}")
def removeProtectorLoader(binf):
binf.seek(protectorLoaderStartAddress + nameOffset, 0)
if read_null_terminated_string(binf) == f"@rpath/{game}x.framework/{game}x":
binf.seek(protectorLoaderStartAddress, 0)
binf.write(protectorLoaderPatchBytes)
print("[INFO] removed protector loader")
else:
print("[WARNING] protector loader is not present in the executable, has it been removed already?")
def fixExport(binf):
if lc_dyld_info_onlyStartAddress is not None:
patch_size = struct.pack('<III', newLazyBindingSize, newExportOff, newExportSize)
binf.seek(lc_dyld_info_onlyStartAddress + 36)
binf.write(patch_size)
binf.seek(exportOff, 0)
data = binf.read(exportSize)
binf.seek(exportOff, 0)
binf.write(b'\x00' * exportSize)
binf.seek(newExportOff, 0)
binf.write(data)
print("[INFO] fixed export functions data and size")
def uleb128Encode(number):
stringResult = ""
while True:
byte = number & 0x7F
number >>= 7
if number != 0:
byte |= 0x80
stringResult += f'{byte:02x}'
if number == 0:
break
return stringResult
def fixInitArray(binf, line):
initArrayAddress = line[3]
initArrayFunctionAddress = line[4]
initArrayAddress = int(initArrayAddress) - BINARY_BASE_ADDRESS
initArrayFunctionAddress = f'{int(initArrayFunctionAddress):x}'
if len(initArrayFunctionAddress) % 2 != 0:
initArrayFunctionAddress = "0" + initArrayFunctionAddress
toBytes = bytearray.fromhex(initArrayFunctionAddress)
toBytes.reverse()
binf.seek(initArrayAddress, 0)
binf.write(toBytes)
def fixLazyBindingSection(binf, starting_char, symbol_ordinal, function_string, function_data_and_name_length_additionned, pointer_bytes):
classChar = "@"
starting_byte = "72"
classByteStart = "20"
if (int(symbol_ordinal) < 16):
classByteStart = "1"
classByte = f'{int(symbol_ordinal):x}'
classBytes = classByteStart + classByte
end_bytes = "9000"
pointerString = uleb128Encode(int(pointer_bytes))
functionFinalString = (classChar + starting_char + function_string)
functionFinalString = (bytes(functionFinalString, 'utf-8') + b'\x00').hex()
finalString = (starting_byte + pointerString + classBytes + functionFinalString + end_bytes)
binf.seek(int(function_data_and_name_length_additionned) + lazyBindingFixingAddress, 0)
binf.write(bytearray.fromhex(finalString))
def fixStringTable(binf, starting_char, function_string, stringIndexInStringTable):
fixingString = starting_char + function_string
finalString = (bytes(fixingString, 'utf-8') + b'\x00').hex()
binf.seek(stringTableFixingAddress + stringIndexInStringTable, 0)
binf.write(bytearray.fromhex(finalString))
return stringIndexInStringTable + len(bytearray.fromhex(finalString))
def fixSymbolTable(binf, stringIndexInStringTable, symbol_ordinal, count, functionPointer = None):
finalDataString = ""
bytesAwayFromStringInStringTable = struct.pack("<I", startCountingAddress + int(stringIndexInStringTable))
finalDataString+= bytesAwayFromStringInStringTable.hex() + "010000" + str(hex(int(symbol_ordinal))[2:].zfill(2)) + "0000000000000000"
binf.seek(symbolTableStartAddress + (count * SYMBOL_TABLE_INFO_LENGTH), 0)
binf.write(bytearray.fromhex(finalDataString))
def fixBinary(binf, biFile):
count = 0
stringIndexInStringTable = 0
symbolTableDataIndex = 0
for line in biFile:
list_data = line.split(VALUE_SEPARATOR)
if (int(list_data[2]) == INIT_INDICATOR):
fixInitArray(binf, list_data)
elif (int(list_data[2]) == V0_INDICATOR): # protector v0 where it is used in all supercell games except new clash royale
startingChar = "_"
symbolTableDataIndex = list_data[3] #this is true but seems like protector not optimized and put some null bytes which are not supposed to be here, which makes it needed to increase lazy binding array
symbolName = list_data[5]
symbolOrdinalPosition = list_data[6]
pointerBytes = list_data[8]
fixLazyBindingSection(binf, startingChar, symbolOrdinalPosition, symbolName, symbolTableDataIndex, pointerBytes)
fixSymbolTable(binf, stringIndexInStringTable, symbolOrdinalPosition, count)
stringIndexInStringTable = fixStringTable(binf, startingChar, symbolName, stringIndexInStringTable)
count+=1
elif (int(list_data[2]) == V5_INDICATOR): #new Clash Royale has this to 5, it's a different fix to apply
# the thing i noticed is that we need to fix the symbol table and the string table but some minor modification from the code of v0, like it doesn't need the starting_char = "_", function names already have it
functionPointer = int(list_data[3]) # refers to the got section, but why?
symbolName = list_data[4]
symbolOrdinalPosition = list_data[5]
count+=1
def read_null_terminated_string(binf):
string = b''
while True:
char = binf.read(1)
if char == b'\x00' or not char:
break
string += char
return string.decode('utf-8')
def main(game):
global session # frida script, needs to have a jailbroken ios device with frida-server installed. this is getting the necessary data in order to fix the binary
isHostUsed = False
if is_arm_mac():
if not check_sip(): print("[WARN] ARM-based macOS device detected, but SIP is enabled...")
if not check_security_args(): print("[WARN] ARM-based macOS device detected, but security features is enabled...")
if is_arm_mac() and check_sip() and check_security_args():
print("[*] ARM-based macOS device detected, we try to use your host instead of a phone")
isHostUsed = True
device = frida.get_local_device()
game_app_name = {"laser": "Brawl Stars"}
subprocess.check_output(["open", f"/Applications/{game_app_name[game]}.app"], text=True).strip()
pid = int(subprocess.check_output(["pgrep", game], text=True).strip())
else:
device = frida.get_usb_device()
pid = device.spawn([f"com.supercell.{game}"])
session = device.attach(pid) # the address of protectorBase.add(0x0) can change any new build of protector supercell is shipping in their client, at this moment it's 0x429728
if game == 'squad' or game == 'laser' or game == 'magic' or game == 'reef':
script = session.create_script(f'''
var protectorBase = Module.findBaseAddress("{game}x");
var StringFunctionEmulation = protectorBase.add(0x292cec);
function writeBLFunction(address, newFunctionAddress) {{
Memory.patchCode(address, 8, code => {{
const Patcher = new Arm64Writer(code, {{pc: address}});
Patcher.putBlImm(newFunctionAddress);
Patcher.flush();
}});
}}
writeBLFunction(protectorBase.add(0xadd20), StringFunctionEmulation)
var unk;
var encryptedInput;
var decryptedOutput;
var contentLength;
var readEncryptedFilesContent = Interceptor.attach(protectorBase.add(0x29bba0), {{
onEnter(args) {{
unk = args[0];
encryptedInput = args[1];
decryptedOutput = args[2];
contentLength = args[3].toInt32();
}},
onLeave : function(retval) {{
send(decryptedOutput.readUtf8String());
console.log(decryptedOutput.readUtf8String());
}}
}});
''')
elif game == 'scroll':
script = session.create_script(f'''
var protectorBase = Module.findBaseAddress("{game}x");
var StringFunctionEmulation = protectorBase.add(0x1b18d8);
function writeBLFunction(address, newFunctionAddress) {{
Memory.patchCode(address, 8, code => {{
const Patcher = new Arm64Writer(code, {{pc: address}});
Patcher.putBlImm(newFunctionAddress);
Patcher.flush();
}});
}}
writeBLFunction(protectorBase.add(0x711a28), StringFunctionEmulation)
var unk;
var encryptedInput;
var decryptedOutput;
var contentLength;
var readEncryptedFilesContent = Interceptor.attach(protectorBase.add(0xa72c0), {{
onEnter(args) {{
unk = args[0];
encryptedInput = args[1];
decryptedOutput = args[2];
contentLength = args[3].toInt32();
}},
onLeave : function(retval) {{
send(decryptedOutput.readUtf8String());
console.log(decryptedOutput.readUtf8String());
}}
}});
''')
else:
script = session.create_script(f'''
var protectorBase = Module.findBaseAddress("{game}x");
var unk;
var encryptedInput;
var decryptedOutput;
var contentLength;
var readEncryptedFilesContent = Interceptor.attach(protectorBase.add(0x429728), {{
onEnter(args) {{
unk = args[0];
encryptedInput = args[1];
decryptedOutput = args[2];
contentLength = args[3].toInt32();
}},
onLeave : function(retval) {{
send(decryptedOutput.readUtf8String());
}}
}});
''')
script.on('message', on_message)
script.load()
device.resume(pid)
print("[INFO] setup done, don't exit, the computer can take up to a minute to exit frida session")
sys.stdin.read()
start_time = datetime.now()
def mainFixing(biFile):
with open(fileToOpen, 'r+b') as binf:
removeProtectorLoader(binf)
fixExport(binf) # in clash royale protector v5 this is non-existent, a check is in place to not fix it in case of cr
fixBinary(binf, biFile)
end_time = datetime.now()
print("[SUCCESS] finished fixing binary file, you may now exit pressing CTRL+C")
print('[DEBUG] Duration: {}'.format(end_time - start_time))
if __name__ == '__main__':
args = parser.parse_args()
a = args.game
fileToOpen = a
a = game_code_name.get(fileToOpen)
game = a
setup()
main(game)