└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # SupercellID-Client 2 | 3 | Since the latest versions of the game, Supercell has added protection against fake requests to their API called Request Forgery Protection (RFP). 4 | 5 | ## How does RFP work? 6 | #### Before hashing, a string of the following order is collected: 7 | `1738503470GET/api/rewards/sdk/v1/rewards.statusauthorization=Bearer user-agent=scid/1.5.8-f (iPadOS 18.2; laser-prod; iPad8,6) com.supercell.laser.K3X97T336N/59.184x-supercell-device-id=5A8F68A1-A0D8-5702-95A8-875CF3F421F8` 8 | #### And this is what it looks like in the final version: 9 | `RFPv1 Timestamp=1738503470,SignedHeaders=authorization;user-agent;x-supercell-device-id,Signature=nCahKjUPwvOcGQW5tLt7cLZb3Ol6yU3Q_KVFjx7Z5Vc` 10 | 11 | #### Therefore, we can determine exactly what data is collected for hashing: 12 | - Datetime (secs since epoch) 13 | - Body 14 | - Method (POST, GET, etc.) 15 | - API Path 16 | - Some headers (User-agent, authorization (bearer), etc.) 17 | 18 | ### Next, go to the `sub_BA8004` (signer) function! 19 | This is not secure, such sensitive code cannot be 100% protected JUST by Promon Shield... :/ 20 | 21 | #### Let's imagine that you have a Supercell ID menu like this: 22 | Снимок экрана 2025-02-02 в 22 09 18 23 | 24 | #### Once you press the "LOG IN" button, a series of Dart calls follow, and eventually we follow this chain: 25 | 1. ↓ [scid_flutter/src/id_services.dart] IdServices::_sign ↓ 26 | 2. ↓ [scid_flutter/src/communication/scid_message_channel.dart] ScidMessageChannel::sign ↓ 27 | 3. --> [flutter/src/services/platform_channel.dart] MethodChannel::invokeMethod <-- 28 | 29 | ### MethodChannel::invokeMethod? What is this? 30 | In Dart (on Android) `MethodChannel::invokeMethod` is used to call native (Java/C++) code from a Flutter app. This is part of the Platform Channel mechanism (Method Channel), which allows Flutter to interact with native Android code. 31 | 32 | **In our case, the function is taken by libscid_sdk.so**, this is where our "receiver" of these calls is located 33 |

34 | Снимок экрана 2025-02-02 в 22 19 35 35 |
36 | Yep! There is! This is the handler for our "sign" message 37 |

38 | We see that next comes a jump into something 39 | 40 | ```c 41 | (*(void (__fastcall **)(void ***__return_ptr, _QWORD, unsigned __int64, unsigned __int64, void *, void **, unsigned __int64))(**(_QWORD **)(this + 8) + 336LL))( 42 | &v419, 43 | *(_QWORD *)(this + 8), 44 | (unsigned __int64)v412 & 0xFFFFFFFFFFFFFFFELL, 45 | v413 & 0xFFFFFFFFFFFFFFFELL, 46 | v414, 47 | &p_dest, 48 | *((_QWORD *)&v413 + 1) & 0xFFFFFFFFFFFFFFFELL); 49 | ``` 50 | A simple VTable function, which leads to `libg.so` at `sub_BA8004`! 51 | 52 | 53 | ##### Below is my deobfuscated version of this function: 54 | ```cpp 55 | // The function generates a packet signature using: 56 | // – a string representation of the timestamp (the primary parameter), 57 | // – header data (parameters a2), 58 | // – the request body (parameters a1), 59 | // – and some key (parameters a5), using an additional structure (a4) 60 | // to get additional data. 61 | // The resulting signature is written to the buffer specified in a6. 62 | // Note that the function returns a long double value – this can be the result of a 63 | // calculation (e.g., a hash), which is then copied to the output buffer. 64 | long double generateSignature( 65 | unsigned char* reqData, // a1 – request data (or request header) 66 | int64_t headerStruct, // a2 – pointer to packet header structure 67 | std::string requestBody, // a3 – request body (passed as string) 68 | int64_t keyStruct, // a4 – structure used for additional calculations 69 | unsigned char* keyData, // a5 – key data 70 | int64_t outSignatureBuf // a6 – pointer to resulting structure (buffer) for signature 71 | ) 72 | { 73 | // 74 | // 1. Convert timestamp to string. 75 | // In the original, std::to_string is called, the result is not explicitly saved, 76 | // but is then used to form the final signature. 77 | // 78 | std::string timestampStr = std::to_string(reinterpret_cast(reqData)); 79 | 80 | // 81 | // 2. Processing packet header data from the headerStruct structure. 82 | // The first byte stores a flag that determines how the data is interpreted: 83 | // – if the least significant bit is 0, the data is “inline”: immediately after the flag is a string of length (flag >> 1) 84 | // – otherwise, the data is located at the address stored in the structure field. 85 | // 86 | uint8_t headerFlag = *(reinterpret_cast(headerStruct)); 87 | bool headerIsInline = ((headerFlag & 1) == 0); 88 | uint64_t headerDataLen = headerFlag >> 1; 89 | const char* headerData = nullptr; 90 | if ( headerIsInline ) 91 | headerData = reinterpret_cast(headerStruct + 1); 92 | else 93 | headerData = *reinterpret_cast(headerStruct + 16); 94 | 95 | // If the data is "inline" - the length is taken from the flag, otherwise from the structure field. 96 | uint64_t headerStrLen = headerIsInline ? headerDataLen : *reinterpret_cast(headerStruct + 8); 97 | 98 | std::string headerStr; 99 | headerStr.append(headerData, headerStrLen); 100 | // After calling string::append the original "clears" the temporary buffers - here we just move on. 101 | 102 | 103 | // 104 | // 3. Processing the request body from reqData (similar algorithm as for the header). 105 | // 106 | uint8_t reqFlag = *reqData; 107 | bool reqIsInline = ((reqFlag & 1) == 0); 108 | uint64_t reqDataLen = reqFlag >> 1; 109 | const char* reqContent = nullptr; 110 | if ( reqIsInline ) 111 | reqContent = reinterpret_cast(reqData + 1); 112 | else 113 | reqContent = *reinterpret_cast(reqData + 16); 114 | 115 | uint64_t reqStrLen = reqIsInline ? reqDataLen : *reinterpret_cast(reqData + 8); 116 | 117 | std::string reqStr; 118 | reqStr.append(reqContent, reqStrLen); 119 | // "Clearing" the temporary reqStr buffer should occurs automatically. 120 | 121 | 122 | // 123 | // 4. Processing key data from keyData (AGAIN the similar logic). 124 | // 125 | uint8_t keyFlag = *keyData; 126 | bool keyIsInline = ((keyFlag & 1) == 0); 127 | uint64_t keyDataLen = keyFlag >> 1; 128 | const char* keyContent = nullptr; 129 | if ( keyIsInline ) 130 | keyContent = reinterpret_cast(keyData + 1); 131 | else 132 | keyContent = *reinterpret_cast(keyData + 16); 133 | 134 | uint64_t keyStrLen = keyIsInline ? keyDataLen : *reinterpret_cast(keyData + 8); 135 | 136 | std::string keyStr; 137 | keyStr.append(keyContent, keyStrLen); 138 | // The temporary keyStr is "cleared" next. 139 | 140 | 141 | // 142 | // 5. Preparing additional constant strings for signature generation. 143 | // String literals are used here, for example: "Authorization" and "X-Supercell-Device-Id" and other one (i can't remember it, sadly :c). 144 | // 145 | const char* authKey = "Authorization"; 146 | const char* deviceKey = "X-Supercell-Device-Id"; 147 | 148 | // Initialize buffers (simulate creation of string objects with dynamically allocated memory) 149 | std::string authStr(authKey); 150 | std::string deviceStr(deviceKey); 151 | 152 | 153 | // 154 | // 6. Processing the authKey string with possible memory allocation by size. 155 | // if the string length exceeds the threshold (>= 23 characters), an aligned block is allocated, 156 | // otherwise, copying is performed via a pointer to an internal buffer. 157 | // 158 | size_t authLen = strlen(authKey); 159 | std::string processedAuth; 160 | if ( authLen >= 0x17 /* 23 */ ) { 161 | // Select a block with alignment (analog of the expression (authLen+16)&~0xF) 162 | processedAuth.resize( (authLen + 16) & ~static_cast(0xF) ); 163 | memcpy(&processedAuth[0], authKey, authLen); 164 | } 165 | else { 166 | // If the string is small, just copy it 167 | processedAuth = authKey; 168 | } 169 | processedAuth.push_back('\0'); // UTF-8 end. 170 | 171 | 172 | // 173 | // 7. Calling sub_BB0DD8, probably to process the header line. 174 | // The result is compared with (a4 + 8) - if it does not match, we go to the loop for processing the set of rows. 175 | // 176 | int64_t processedHeaderPtr = sub_BB0DD8(keyStruct, reinterpret_cast(&headerStr)); 177 | if ( (keyStruct + 8) != processedHeaderPtr ) 178 | { 179 | // v31 – pointer to an array of strings (for example, header names); 180 | // here represented as an array of pointers to char. 181 | const char** headerArray = /* pointer to an array of strings obtained earlier (v169) */; 182 | 183 | // Process each element of the headerArray array in a loop 184 | // (the exact number of elements is determined based on the data, here we will denote it as headerCount, but usually there should be 3) 185 | size_t headerCount = 0x3; 186 | for (size_t i = 0; i < headerCount; i++) { 187 | const char* curHeader = headerArray[i]; 188 | size_t curLen = strlen(curHeader); 189 | std::string curHeaderStr; 190 | if ( curLen >= 0x17 ) { 191 | curHeaderStr.resize( (curLen + 16) & ~static_cast(0xF) ); 192 | memcpy(&curHeaderStr[0], curHeader, curLen); 193 | } else { 194 | curHeaderStr = curHeader; 195 | } 196 | curHeaderStr.push_back('\0'); 197 | 198 | // Apply character conversion - convert capital letters to lowercase. 199 | // (In the src, this part is implemented using NEON instructions) 200 | for (char &ch : curHeaderStr) { 201 | if ( ch >= 'A' && ch <= 'Z' ) 202 | ch |= 0x20; 203 | } 204 | 205 | // Add the processed string to the shared buffer (analogous to sub_3BC7EC) 206 | // For example: 207 | // intermediateBuffer.append(curHeaderStr); 208 | 209 | // Reset temp variables for the current element (analogous to operator::delete for a temporary buffer) 210 | 211 | // Next, add the "=" symbol to the end of the processed string: 212 | // intermediateBuffer.append("=", 1); 213 | 214 | // And call another loc_BAB868 to get an additional block 215 | int64_t locResult = loc_BAB868(keyStruct, /* v163 */, &curHeaderStr); 216 | if (!locResult) 217 | abort(); // if the result is zero - crash 218 | 219 | // From the structure returned by locResult, extracting: 220 | uint8_t locFlag = *(reinterpret_cast(locResult + 56)); 221 | bool locInline = ((locFlag & 1) == 0); 222 | uint64_t locLen = locFlag >> 1; 223 | const char* locData = locInline ? reinterpret_cast(locResult + 57) 224 | : *reinterpret_cast(locResult + 72); 225 | uint64_t locDataLen = locInline ? locLen : *reinterpret_cast(locResult + 64); 226 | 227 | // Append the received data to the general buffer (maybe intermediateBuffer.append(locData, locDataLen);) 228 | 229 | } 230 | } 231 | 232 | 233 | // 234 | // 8. Next, a strange cycle occurs, in which from a set of blocks (from the memory area from v177 to v178) 235 | // a string is sequentially formed, between blocks (if there are several of them) 236 | // an additional line is added (from v169/v170). The original calculates the number of blocks 237 | // using multiplication by the constant 0xAAAAAAAAAAAAAAABLL. 238 | // 239 | std::string combinedBuffer; 240 | { 241 | // The number of blocks is defined as: 242 | size_t blockCount = /* (v178 - v177) / 8 */; 243 | for (size_t blockIndex = 0; blockIndex < blockCount; blockIndex++) { 244 | // Retrieve a 24-byte block from the memory area (v177 + blockIndex*24) 245 | // and add it to combinedBuffer: 246 | // combinedBuffer.append(v177_block, 24); 247 | 248 | // If this is not the last block, add an intermediate separator, 249 | // which is taken from v169/v170 (see source for details) 250 | if ( blockIndex < blockCount - 1 ) { 251 | // combinedBuffer.append(разделитель); 252 | } 253 | } 254 | // If necessary, free the mem allocated for v169 (if the flag shows that memory was allocated, and it should be there AS AS MUCH AS I REMEMBER) 255 | } 256 | 257 | 258 | // 259 | // 9. We reset the previous lines and write in them the 32-byte key used for hashing 260 | // 261 | // Here: 262 | // v169 = 0; 263 | // v170 = 0; 264 | // v167 = xmmword_1B4FB8; 265 | // v168 = unk_1B4FC8; 266 | // 267 | // And finally: 268 | sub_BA8D68(reinterpret_cast(&v167), &v165); 269 | 270 | 271 | // 272 | // 10. The most important part of our DJ shitcore party! Hash calculation based on the key. 273 | // Called sub_BB1B34, which receives: 274 | // – pointer to v165, 275 | // – length (0x20), 276 | // – key data (from keyStr), 277 | // – pointer to v169, 278 | // – and one more parameter 0x20. 279 | // 280 | int hashInput = sub_BB1B34(&v165, 0x20, reinterpret_cast(keyStr.data()), keyStr.size(), &v169, 0x20); 281 | 282 | // Next, sub_D2FED8 is called, the result of return is (4 * ((len + 2) / 3)) | 1; 283 | int hashTotalLen = sub_D2FED8(hashInput); 284 | int finalHashLen = hashTotalLen - 1; 285 | 286 | // aLLlLLlOooOOOCCCccAaaAttiiIiiNgggG AGGGAAIN a buffer for the final hash. 287 | std::string finalHash; 288 | if ( finalHashLen >= 0x17 ) { 289 | // Selecting block with alignment: 290 | size_t allocSize = (hashTotalLen + 15) & ~static_cast(0xF); 291 | finalHash.resize(allocSize); 292 | } 293 | else { 294 | finalHash.resize(finalHashLen); 295 | } 296 | memset(&finalHash[0], 0, finalHashLen); 297 | finalHash[finalHashLen] = '\0'; 298 | 299 | // Depending on the flag (memory allocation inline or through a pointer), we get a pointer to the data: 300 | char* finalHashData = &finalHash[0]; // (if the flag is inline) otherwise - otherwise 301 | // Fill the resulting hash buffer: 302 | sub_D2FF04(finalHashData, reinterpret_cast(&v169), hashInput); 303 | 304 | 305 | // 306 | // 11. This is also an important part!! Post-processing of the final hash string: 307 | // – Replacing the '+' symbol with '-' (implemented using vector (NEON) operations, 308 | // if the buffer length allows, otherwise - loop through the bytes). 309 | // – Similar processing of the symbol '/' → '_'. 310 | // – Remove '=' characters (if they appear at the end). 311 | // 312 | size_t hashSize = finalHash.size(); 313 | if ( hashSize >= 8 ) { 314 | // Processing in blocks of 8 bytes (in the original - with NEON instructions). 315 | for (size_t i = 0; i < hashSize; i += 8) { 316 | for (size_t j = i; j < std::min(hashSize, i + 8); j++) { 317 | if ( finalHash[j] == '+' ) 318 | finalHash[j] = '-'; 319 | } 320 | } 321 | } 322 | else { 323 | // Each byte! 324 | for (size_t i = 0; i < hashSize; i++) { 325 | if ( finalHash[i] == '+' ) 326 | finalHash[i] = '-'; 327 | } 328 | } 329 | 330 | // '/' to '_' 331 | for (size_t i = 0; i < hashSize; i++) { 332 | if ( finalHash[i] == '/' ) 333 | finalHash[i] = '_'; 334 | } 335 | 336 | // Remove '=' characters. If '=' is encountered, all subsequent characters are excluded. 337 | size_t eqPos = finalHash.find('='); 338 | if ( eqPos != std::string::npos && eqPos + 1 < finalHash.size() ) { 339 | std::string tmp; 340 | for (size_t i = eqPos; i < finalHash.size(); i++) { 341 | if ( finalHash[i] != '=' ) 342 | tmp.push_back(finalHash[i]); 343 | } 344 | finalHash = tmp; 345 | } 346 | 347 | 348 | // 349 | // 12. Formation of the final signature. 350 | // A final string is created consisting of: 351 | // – Prefix "RFPv1 Timestamp=" with the addition of timestampStr, 352 | // – Then, through calls to std::to_string and std::insert, add 353 | // other components (for example, intermediate buffers obtained in steps 7–8), 354 | // – And finally, the final hash (finalHash). 355 | // 356 | std::string finalSignature; 357 | finalSignature.append("RFPv1 Timestamp="); 358 | finalSignature.append(timestampStr); 359 | 360 | // Example of inserting an intermediate buffer (for example, obtained from authStr and processed earlier) 361 | std::string authInserted = std::string("RFPv1 Timestamp=") + timestampStr; 362 | // Next we add the result of std::string::append() from v159/v161, etc. 363 | // (the exact sequence of operations in the original is quite heavily obfuscated - below is a pseudoexample) 364 | authInserted.append(" "); 365 | authInserted.append(/* some line from the intermediate buffer formed in steps 7–8 */); 366 | authInserted.append(" "); 367 | authInserted.append(finalHash); 368 | 369 | // The result is written to the output buffer (i think that outSignatureBuf is a std::string structure) 370 | *reinterpret_cast(outSignatureBuf) = authInserted; 371 | 372 | } 373 | ``` 374 | 375 | Here is my working crappy Python test script: 376 | 377 | ```py 378 | key = bytes.fromhex("ae584daf58a3757be21fb506dfcfc478fad4600e688d5bb6f3e51ccb2ebfc373") 379 | def hmac_sha256_manual(key: bytes, message: bytes) -> bytes: 380 | """ 381 | manual implementation of HMAC-SHA256, because why not? 382 | """ 383 | block_size = 64 384 | if len(key) > block_size: key = hashlib.sha256(key).digest() 385 | if len(key) < block_size: key = key.ljust(block_size, b'\x00') 386 | 387 | ipad = bytes((b ^ 0x36) for b in key) 388 | opad = bytes((b ^ 0x5C) for b in key) 389 | 390 | inner = hashlib.sha256(ipad + message).digest() 391 | outer = hashlib.sha256(opad + inner).digest() 392 | return outer 393 | 394 | def RFPv1(data, method, useragent): 395 | currenttime = int(time.time()) 396 | 397 | signstr = str(currenttime) + "POST" + method + urllib.parse.urlencode(data) + "user-agent=" + useragent + "x-supercell-device-id=" 398 | 399 | result = "RFPv1 Timestamp=" 400 | result += str(currenttime) 401 | result += ",SignedHeaders=user-agent;x-supercell-device-id," 402 | 403 | sign = base64.b64encode(hmac_sha256_manual(key, signstr.encode())).decode().replace("+", "-").replace("/", "_").replace("=", "") 404 | result += "Signature=" + sign 405 | 406 | return result 407 | 408 | data = { 409 | "lang": "en", 410 | "email": mail, 411 | "remember": "true", 412 | "game": "laser", 413 | "env": "prod", 414 | "unified_flow": "LOGIN", 415 | "recaptchaToken": "FAILED_EXECUTION", 416 | "recaptchaSiteKey": "6Lf3ThsqAAAAABuxaWIkogybKxfxoKxtR-aq5g7l", 417 | } 418 | 419 | encoded_data = urllib.parse.urlencode(data) 420 | content_length = str(len(encoded_data)) 421 | 422 | headers = { 423 | "accept-encoding": "gzip", 424 | "accept-language": "ru", 425 | "content-length": content_length, 426 | "content-type": "application/x-www-form-urlencoded; charset=utf-8", 427 | "host": "id.supercell.com", 428 | "user-agent": "scid/1.5.8-f (Android 13; laser-prod; Test) com.supercell.laser/59.184", 429 | "x-supercell-device-id": "", 430 | "x-supercell-request-forgery-protection": RFPv1(data, "/api/ingame/account/login", "scid/1.5.8-f (Android 13; laser-prod; Test) com.supercell.laser/59.184")[0] 431 | } 432 | 433 | response = httpx.post("https://id.supercell.com/api/ingame/account/login", headers=headers, content=encoded_data) 434 | 435 | ``` 436 | 437 | 438 | 439 | ## RFP Keys: 440 | - Laser v59: `ae584daf58a3757be21fb506dfcfc478fad4600e688d5bb6f3e51ccb2ebfc373` 441 | --------------------------------------------------------------------------------