├── Captcha Solver ├── README.MD ├── example.py └── solver.py ├── Intercepting Requests └── README.md ├── README.md └── Signing └── Mobile ├── Other ├── README.md ├── x-ss-stub.py └── xor.py └── X-Gorgon ├── README.md ├── example.py └── signature.py /Captcha Solver/README.MD: -------------------------------------------------------------------------------- 1 | # TikTok Captcha Solver 2 | 3 | TikTok's puzzle captcha is a mechanism to distinguish humans from bots using a sliding puzzle. This code solves it programmatically, automating the process while maintaining user authenticity. 4 | 5 | TikTok binds the captcha to the user's `device_id` & `install_id`. 6 | 7 | ### Processing Images 8 | This function is responsible for preprocessing the puzzle and piece images. 9 | It begins by decoding the base64 strings into image data, then converting it to grayscale. 10 | A Gaussian blur is applied to the grayscale image to reduce noise, and Sobel operators are employed to compute horizontal and vertical gradients. 11 | These gradients are combined using a weighted average to enhance edges and features in the image. 12 | 13 | ```py 14 | @staticmethod 15 | def process_image(buffer: BufferedReader): 16 | nparr = frombuffer(buffer, uint8) 17 | image = cv2.imdecode(nparr, cv2.IMREAD_COLOR) 18 | blurred = cv2.GaussianBlur(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY), (3, 3), 0) 19 | return cv2.addWeighted( 20 | cv2.convertScaleAbs(cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize=3)), 21 | 0.5, 22 | cv2.convertScaleAbs(cv2.Sobel(blurred, cv2.CV_16S, 0, 1, ksize=3)), 23 | 0.5, 24 | 0, 25 | ) 26 | ``` 27 | 28 | ### Solving Captcha 29 | This function initiates by sending a request to `https://rc-verification-i18n.tiktokv.com/captcha/get` to retrieve captcha data, including the URLs for puzzle and piece images. 30 | These images are then processed through the `process_image` function, which involves decoding the base64 string, converting it into a grayscale image, and applying a weighted gradient to enhance relevant features. 31 | After preprocessing, a random sliding length is generated for the puzzle solver. Template matching is performed on the puzzle and piece images, identifying the best match and its position. 32 | Using this position and the sliding length, a response payload is crafted, simulating the sliding action required for the captcha challenge. 33 | Finally, a POST request is made to `https://rc-verification-i18n.tiktokv.com/captcha/verify` with the generated payload, aiming to verify the captcha challenge. The response from this request is then returned. 34 | 35 | ```py 36 | def solve_captcha(self) -> dict: 37 | captcha = self.session.get( 38 | url=f"{self.base_url}/captcha/get", 39 | params=self.params 40 | ).json() 41 | puzzle, piece = [ 42 | self.process_image( 43 | self.session.get(captcha["data"]["question"][f"url{url}"]).content 44 | ) 45 | for url in [1, 2] 46 | ] 47 | 48 | time.sleep(1) 49 | 50 | randlength = round(random.uniform(50, 100)) 51 | max_loc = cv2.minMaxLoc(cv2.matchTemplate(puzzle, piece, cv2.TM_CCOEFF_NORMED))[3][0] 52 | 53 | return self.session.post( 54 | url=f"{self.base_url}/captcha/verify", 55 | params=self.params, 56 | json={ 57 | "modified_img_width": 552, 58 | "id": captcha["data"]["id"], 59 | "mode": "slide", 60 | "reply": [ 61 | { 62 | "relative_time": (i * randlength), 63 | "x": round(max_loc / (randlength / i)), 64 | "y": captcha["data"]["question"]["tip_y"], 65 | } 66 | for i in range(1, randlength) 67 | ], 68 | }, 69 | ).json() 70 | ``` 71 | 72 | ## Example Usage 73 | 74 | ```py 75 | from solver import TikTokCaptchaSolver 76 | 77 | # Replace with an actual device_id and install_id 78 | print(TikTokCaptchaSolver(device_id=1234567891012345678, install_id=1234567891012345678).solve_captcha()) 79 | 80 | # Expected Output: 81 | # {'code': 200, 'data': None, 'message': 'Verification complete', 'msg_code': '200', 'msg_sub_code': 'success'} 82 | ``` -------------------------------------------------------------------------------- /Captcha Solver/example.py: -------------------------------------------------------------------------------- 1 | from solver import TikTokCaptchaSolver 2 | 3 | # Replace with an actual device_id and install_id 4 | print( 5 | TikTokCaptchaSolver( 6 | device_id=1234567891012345678, install_id=1234567891012345678 7 | ).solve_captcha() 8 | ) 9 | 10 | # Expected Output: 11 | # {'code': 200, 'data': None, 'message': 'Verification complete', 'msg_code': '200', 'msg_sub_code': 'success'} 12 | -------------------------------------------------------------------------------- /Captcha Solver/solver.py: -------------------------------------------------------------------------------- 1 | # github.com/angelillija 2 | 3 | import random 4 | import time 5 | import cv2 6 | from io import BufferedReader 7 | from numpy import uint8, frombuffer 8 | from httpx import Client as Session 9 | 10 | 11 | class TikTokCaptchaSolver: 12 | def __init__(self, device_id: int, install_id: int) -> None: 13 | self.session = Session() 14 | self.base_url = "https://rc-verification-i18n.tiktokv.com" 15 | self.params = { 16 | "aid": "1233", 17 | "os_type": "0", 18 | "type": "verify", 19 | "subtype": "slide", 20 | "did": device_id, 21 | "iid": install_id, 22 | } 23 | 24 | @staticmethod 25 | def process_image(buffer: BufferedReader): 26 | nparr = frombuffer(buffer, uint8) 27 | image = cv2.imdecode(nparr, cv2.IMREAD_COLOR) 28 | blurred = cv2.GaussianBlur(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY), (3, 3), 0) 29 | return cv2.addWeighted( 30 | cv2.convertScaleAbs(cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize=3)), 31 | 0.5, 32 | cv2.convertScaleAbs(cv2.Sobel(blurred, cv2.CV_16S, 0, 1, ksize=3)), 33 | 0.5, 34 | 0, 35 | ) 36 | 37 | def solve_captcha(self) -> dict: 38 | captcha = self.session.get( 39 | url=f"{self.base_url}/captcha/get", params=self.params 40 | ).json() 41 | puzzle, piece = [ 42 | self.process_image( 43 | self.session.get(captcha["data"]["question"][f"url{url}"]).content 44 | ) 45 | for url in [1, 2] 46 | ] 47 | 48 | time.sleep(1) 49 | 50 | randlength = round(random.uniform(50, 100)) 51 | max_loc = cv2.minMaxLoc(cv2.matchTemplate(puzzle, piece, cv2.TM_CCOEFF_NORMED))[ 52 | 3 53 | ][0] 54 | 55 | return self.session.post( 56 | url=f"{self.base_url}/captcha/verify", 57 | params=self.params, 58 | json={ 59 | "modified_img_width": 552, 60 | "id": captcha["data"]["id"], 61 | "mode": "slide", 62 | "reply": [ 63 | { 64 | "relative_time": (i * randlength), 65 | "x": round(max_loc / (randlength / i)), 66 | "y": captcha["data"]["question"]["tip_y"], 67 | } 68 | for i in range(1, randlength) 69 | ], 70 | }, 71 | ).json() 72 | -------------------------------------------------------------------------------- /Intercepting Requests/README.md: -------------------------------------------------------------------------------- 1 | # Intercepting TikTok Mobile Requests 2 | 3 | Intercepting TikTok requests involves capturing and analyzing network traffic between the app and its servers, providing insights into the app's communication and enabling the automation of every function. 4 | 5 | 6 | ### Understanding TikTok's SSL Pinning: 7 | TikTok implements SSL pinning to enhance security by ensuring that the app only communicates with trusted servers, verified by specific SSL certificates. 8 | 9 | Typically, I'll patch the APK using Frida to bypass SSL pinning. However, in this case, patching the APK using Frida isn't necessary since we're using an old Android version. 10 | 11 | 12 | ### Requirements 13 | - [MEmu Emulator](https://www.memuplay.com/) 14 | - [HTTP Toolkit](https://httptoolkit.com/) 15 | - [TikTok APK](https://tiktok.en.uptodown.com/android/post-download) 16 | 17 | ## Steps: 18 | 19 | 1. **Launch MEmu Emulator & HTTP Toolkit** 20 | 21 | 2. **Create a New Android Device in MEmu Emulator**: 22 | - Create a new virtual Android device with Android version 7.1 (recommended version). 23 | - In the device settings, enable "Root Device." 24 | 25 | 3. **Start MEmu then drag & drop the TikTok APK into it to install.** 26 | 27 | 4. **In HTTP Toolkit, go to the "Intercept" tab and select "Android Device via ADB".** 28 | 29 | 5. **Open Tiktok & Switch to the "View" tab in HTTP Toolkit to view the requests.** -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### TikTok Reverse Enginering Documentation 2 | 3 | This is my TikTok reverse engineering journey, where I completely dissect every aspect of TikTok's functionality. I will unravel every security measure used & how to automate different features using their private mobile API. 4 | 5 | ### Table of Contents 6 | - [Intercepting Requests](https://github.com/angelillija/TikTok/tree/main/Intercepting%20Requests) 7 | - [Signing](https://github.com/angelillija/TikTok/tree/main/Signing) 8 | - [Mobile](https://github.com/angelillija/TikTok/tree/main/Signing/Mobile) 9 | - [X-Gorgon](https://github.com/angelillija/TikTok/tree/main/Signing/Mobile/X-Gorgon) 10 | - [Other](https://github.com/angelillija/TikTok/tree/main/Signing/Mobile/Other) 11 | - [X-SS-STUB](https://github.com/angelillija/TikTok/tree/main/Signing/Mobile/Other#tiktok-x-ss-stub) 12 | - [XOR Encryption](https://github.com/angelillija/TikTok/tree/main/Signing/Mobile/Other#tiktok-email-username--password-encryption) 13 | - [Captcha Solver](https://github.com/angelillija/TikTok/tree/main/Captcha%20Solver) 14 | -------------------------------------------------------------------------------- /Signing/Mobile/Other/README.md: -------------------------------------------------------------------------------- 1 | # TikTok X-SS-STUB 2 | 3 | When intercepting TikTok's mobile API, you will notice a header called `X-SS-STUB`, this is added whenever there is a post request. 4 | 5 | 6 | ![image](https://github.com/angelillija/priv/assets/105955582/0c21dbfe-672c-4663-af13-5b70e1cfde2f) 7 | 8 | At first glance, you can clearly tell this is just MD5 hashed, in uppercase. We can verify this by going to [Hashes.com](https://hashes.com/en/tools/hash_identifier) & checking which hash this is. 9 | 10 | ![image](https://github.com/angelillija/priv/assets/105955582/60faf354-0fa1-4325-9759-c89d09db8061) 11 | 12 | The encrypted data is the urlencoded form of the request body. 13 | 14 | Here is the code that can generate this: 15 | 16 | - `body=null` is encoded into bytes using the UTF-8 encoding because hashlib.md5() requires bytes as input. 17 | - `hashlib.md5().hexdigest()` computes the MD5 hash of the input bytes. 18 | - The `.upper()` method is used to convert the MD5 hash to uppercase. 19 | ```py 20 | import hashlib 21 | 22 | data = "body=null" 23 | 24 | print(hashlib.md5(data.encode()).hexdigest().upper()) 25 | ``` 26 | Just like shown in the first image, the generated output of the exact same request body is `46C03B52742B3F2615A3ABDF1636B754` 27 | 28 | # Tiktok XOR Encryption 29 | 30 | When intercepting TikTok's mobile API & looking at requests such as login, register, etc, you'll see that the email/username and password is encrypted. 31 | 32 | ![image](https://github.com/angelillija/priv/assets/105955582/0443758c-2622-4cb4-a505-6370bb523a07) 33 | 34 | We can check what this is by going to [Hashes.com](https://hashes.com/en/tools/hash_identifier) & can see it's hex encoded. 35 | 36 | ![image](https://github.com/angelillija/priv/assets/105955582/8975b309-59a4-4044-a4fb-117e92e3176c) 37 | 38 | ```Dkb`iJkQju``` is a bytes literal in Python, representing a sequence of bytes. Each character in the string is represented as its ASCII byte value. 39 | 40 | 41 | Here is code to decode it: 42 | 43 | - ```for byte in b"Dkb`iJkQju"``` is a loop that iterates over each byte (ASCII value of a character) in the bytes literal. 44 | - `byte ^ 5`: For each byte (ASCII value), the XOR (^) operation is performed with the constant value 5. 45 | - The `bytes([...])` method constructs a new bytes object from the resulting list of XORed values. 46 | - `.decode('utf-8')` decodes the bytes into a string using UTF-8 encoding. 47 | 48 | ```py 49 | print(bytes([byte ^ 5 for byte in b"Dkb`iJkQju"]).decode('utf-8')) 50 | ``` 51 | 52 | The output is `AngelOnTop` which is correct but this is only decrypting it, we need a way to encrypt it. 53 | 54 | - For each character, it performs an XOR operation between the ASCII value of the character and 5. 55 | - It converts the XOR result to its hexadecimal representation using `hex(ord(c) ^ 5)`. 56 | - It takes the hexadecimal representation (excluding the "0x" prefix) of each XOR result and concatenates them to form the encrypted string. 57 | 58 | ```py 59 | def encrypt(string): 60 | return "".join([hex(ord(c) ^ 5)[2:] for c in string]) 61 | 62 | print(encrypt("AngelOnTop")) 63 | ``` 64 | Output: `446b6260694a6b516a75` As shown in the image in the request body, this is completely valid. 65 | -------------------------------------------------------------------------------- /Signing/Mobile/Other/x-ss-stub.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | # Add your urlencoded request body 4 | data = "" 5 | 6 | print(hashlib.md5(data.encode()).hexdigest().upper()) 7 | -------------------------------------------------------------------------------- /Signing/Mobile/Other/xor.py: -------------------------------------------------------------------------------- 1 | def encrypt(string): 2 | return "".join([hex(ord(c) ^ 5)[2:] for c in string]) 3 | 4 | 5 | # Add string to encrypt 6 | print(encrypt("")) 7 | -------------------------------------------------------------------------------- /Signing/Mobile/X-Gorgon/README.md: -------------------------------------------------------------------------------- 1 | # X-Gorgon 0404 & X-Khronos 2 | 3 | Two of the five current TikTok's security headers in their mobile android API are X-Gorgon & X-Khronos. These two are used by TikTok to authenticate and secure API requests & were the first two ever added. 4 | 5 | - X-Gorgon: This header is created by hashing key components and combining them with a version and timestamp. It ensures the validity and integrity of requests to TikTok's servers. 6 | 7 | - X-Khronos: This is a hexadecimal timestamp representing the time at which the request is made. 8 | 9 | ### Hashed String Example: 10 | - Params Hash -> 9336ebf25087d91c818ee6e9ec29f8c1 11 | - No Data -> 00000000000000000000000000000 12 | - No Cookies -> 00000000000000000000000000000000000 13 | 14 | ### Encrypted String example: 15 | - Version -> 0404 16 | - Static hash -> b0d30000 17 | - Encrypted hash string -> 3f2892cbeb57387ccee15dbc3694a6f7df5311c6 18 | 19 | TikTok decrypts the X-Gorgon header to validate the request. If the decryption is successful, the request is considered valid and access to the API is granted; otherwise, an empty response or an error status code may be returned. 20 | 21 | ### Base String & Encryption Key Generation: 22 | 23 | - The base string is generated by concatenating MD5 hashes of URL parameters, post data, and cookies, along with a hexadecimal timestamp. 24 | - If either the data or cookies are not provided, 32 zeros are used in place of their hashes. 25 | - This base string is then converted to a hex list and serves as the encryption key. 26 | - The header is contructed by using the version (0404), a static hash (b0d30000), and the encrypted hash string. It also includes the X-Khronos header with the current timestamp. 27 | 28 | ```py 29 | def get_value(self) -> dict: 30 | gorgon = self.encrypt( 31 | "".join( 32 | hashlib.md5(value.encode()).hexdigest() 33 | if value 34 | else "00000000000000000000000000000000" 35 | for value in [self.params, self.data, self.cookies] 36 | ) 37 | ) 38 | return { 39 | "X-Gorgon": f"0404b0d30000{gorgon}", 40 | "X-Khronos": str(int(time.time())) 41 | } 42 | 43 | ``` 44 | 45 | ### Base String Encryption: 46 | 47 | The encrypt method takes the base string, processes it, applies bitwise operations and XOR with a predefined encryption key, and then converts the resulting integers to a hexadecimal string, which represents the encrypted version of the base string. 48 | 49 | ```py 50 | @staticmethod 51 | def encrypt(data: str) -> str: 52 | unix = int(time.time()) 53 | length = 0x14 54 | key = [ 55 | 0xDF, 56 | 0x77, 57 | 0xB9, 58 | 0x40, 59 | 0xB9, 60 | 0x9B, 61 | 0x84, 62 | 0x83, 63 | 0xD1, 64 | 0xB9, 65 | 0xCB, 66 | 0xD1, 67 | 0xF7, 68 | 0xC2, 69 | 0xB9, 70 | 0x85, 71 | 0xC3, 72 | 0xD0, 73 | 0xFB, 74 | 0xC3, 75 | ] 76 | 77 | 78 | param_list = [ 79 | int(data[8 * i : 8 * (i + 1)][j * 2 : (j + 1) * 2], 16) 80 | for i in range(3) 81 | for j in range(4) 82 | ] 83 | param_list.extend([0x0, 0x6, 0xB, 0x1C]) 84 | 85 | unix_bytes = unix.to_bytes(4, byteorder="big") 86 | param_list.extend(unix_bytes) 87 | 88 | eor_result_list = [A ^ B for A, B in zip(param_list, key)] 89 | 90 | for i in range(length): 91 | eor_result_list[i] = (~eor_result_list[i] ^ length) & 0xFF 92 | 93 | return "".join(f"{param:02x}" for param in eor_result_list) 94 | ``` 95 | 96 | ## Example Usage: 97 | 98 | ```py 99 | from signature import Signature 100 | 101 | signer = Signature(params="", data="", cookies="").get_value() 102 | 103 | print(f"X-Gorgon: {signer['X-Gorgon']} X-Khronos: {signer['X-Khronos']}") 104 | ``` 105 | -------------------------------------------------------------------------------- /Signing/Mobile/X-Gorgon/example.py: -------------------------------------------------------------------------------- 1 | from signature import Signature 2 | 3 | signer = Signature(params="", data="", cookies="").get_value() 4 | 5 | print(f"X-Gorgon: {signer['X-Gorgon']} X-Khronos: {signer['X-Khronos']}") 6 | -------------------------------------------------------------------------------- /Signing/Mobile/X-Gorgon/signature.py: -------------------------------------------------------------------------------- 1 | # github.com/angelillija 2 | 3 | import hashlib 4 | import time 5 | 6 | 7 | class Signature: 8 | def __init__(self, params: str, data: str, cookies: str) -> None: 9 | self.params = params 10 | self.data = data 11 | self.cookies = cookies 12 | 13 | def get_value(self) -> dict: 14 | gorgon = self.encrypt( 15 | "".join( 16 | hashlib.md5(value.encode()).hexdigest() 17 | if value 18 | else "00000000000000000000000000000000" 19 | for value in [self.params, self.data, self.cookies] 20 | ) 21 | ) 22 | return { 23 | "X-Gorgon": f"0404b0d30000{gorgon}", 24 | "X-Khronos": str(int(time.time())) 25 | } 26 | 27 | @staticmethod 28 | def encrypt(data: str) -> str: 29 | unix = int(time.time()) 30 | length = 0x14 31 | key = [ 32 | 0xDF, 33 | 0x77, 34 | 0xB9, 35 | 0x40, 36 | 0xB9, 37 | 0x9B, 38 | 0x84, 39 | 0x83, 40 | 0xD1, 41 | 0xB9, 42 | 0xCB, 43 | 0xD1, 44 | 0xF7, 45 | 0xC2, 46 | 0xB9, 47 | 0x85, 48 | 0xC3, 49 | 0xD0, 50 | 0xFB, 51 | 0xC3, 52 | ] 53 | 54 | param_list = [ 55 | int(data[8 * i : 8 * (i + 1)][j * 2 : (j + 1) * 2], 16) 56 | for i in range(3) 57 | for j in range(4) 58 | ] 59 | param_list.extend([0x0, 0x6, 0xB, 0x1C]) 60 | 61 | unix_bytes = unix.to_bytes(4, byteorder="big") 62 | param_list.extend(unix_bytes) 63 | 64 | eor_result_list = [A ^ B for A, B in zip(param_list, key)] 65 | 66 | for i in range(length): 67 | eor_result_list[i] = (~eor_result_list[i] ^ length) & 0xFF 68 | 69 | return "".join(f"{param:02x}" for param in eor_result_list) 70 | --------------------------------------------------------------------------------