├── requirements.txt ├── LICENSE ├── README.md ├── main.py └── utils.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pystyle 2 | pycryptodome 3 | httpx -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 lululepu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blank-Ungrabber 2 | 3 | ![GitHub](https://img.shields.io/github/license/lululepu/Blank-Ungrabber) 4 | ![Python](https://img.shields.io/badge/Python-3.x-blue) 5 | 6 | Decrypt and analyze a Blank Grabber executable. 7 | 8 | Thanks to extremecoders-re for pyinstxtractor 9 | 10 | You can now use ungrabber module its free and easy to use work with BlankGrabber, CrealGrabber, PySilon, BCStealer, CStealer, Empyrean and many more : https://github.com/lululepu/Ungrabber 11 | 12 | ## About 13 | 14 | This Python script is designed to decrypt and analyze Blank Grabber executables. It extracts and decrypts the contents, deobfuscates code, and identifies potential Discord webhooks used by the executable. 15 | 16 | ## Features 17 | 18 | - Decrypt Blank Grabber executables 19 | - Deobfuscate code 20 | - Identify Discord webhooks 21 | - Test identified webhooks 22 | 23 | ## Requirements 24 | 25 | - Python 3.x 26 | - Additional Python libraries (install using `pip install`): 27 | - pystyle 28 | - crypto 29 | - httpx 30 | 31 | ## Installation 32 | 33 | ```cmd 34 | git clone https://github.com/lululepu/Blank-Ungrabber 35 | cd Blank-Ungrabber 36 | py main.py 37 | ``` 38 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from pystyle import * 2 | from typing import Generator 3 | from Crypto.Cipher import AES 4 | import os, base64, zlib, shutil, string, codecs, lzma, httpx, time, ast, io, zipfile, utils, sys 5 | 6 | if os.name == 'nt': 7 | os.system('cls') 8 | else: 9 | os.system('clear') 10 | 11 | asciiart='''______ _ _ _ _ _ _ 12 | | ___ \ | | | | | | | | | | | 13 | | |_/ / | __ _ _ __ | | ________| | | |_ __ __ _ _ __ __ _| |__ | |__ ___ _ __ 14 | | ___ \ |/ _` | '_ \| |/ /______| | | | '_ \ / _` | '__/ _` | '_ \| '_ \ / _ \ '__| 15 | | |_/ / | (_| | | | | < | |_| | | | | (_| | | | (_| | |_) | |_) | __/ | 16 | \____/|_|\__,_|_| |_|_|\_\ \___/|_| |_|\__, |_| \__,_|_.__/|_.__/ \___|_| 17 | __/ | 18 | |___/ ''' 19 | System.Title('Blank-Ungrabber') 20 | print(Colorate.Vertical(Colors.yellow_to_red, Center.XCenter(asciiart))) 21 | executable=input('\n'+Colorate.Vertical(Colors.red_to_yellow, 'Executable Path:')+' ') 22 | 23 | be=time.time() 24 | 25 | extracted = utils.Extract(executable) 26 | 27 | def log(message) -> None: 28 | print(Colorate.Vertical(Colors.red_to_yellow, message)) 29 | 30 | def strings(data: str) -> Generator: 31 | data = str(data) 32 | result = '' 33 | for c in data: 34 | if c in string.printable: 35 | result += c 36 | continue 37 | if len(result) >= 4: 38 | yield result 39 | result = '' 40 | if len(result) >= 4: 41 | yield result 42 | 43 | def get_var(code: str, var: str) -> str: # Get a variable from a given code by name 44 | tree = ast.parse(code) 45 | for node in ast.walk(tree): 46 | if isinstance(node, ast.Assign): 47 | for target in node.targets: 48 | if isinstance(target, ast.Name) and target.id == var: 49 | return node.value.value 50 | return 51 | 52 | def get_file(name) -> bytearray: # Get file from the files list 53 | if extracted[name]: 54 | return extracted[name] 55 | return False 56 | 57 | 58 | if not os.path.isfile(executable): 59 | log('Please input a valid file') 60 | sys.exit(1) 61 | if not get_file('blank.aes'): 62 | log('This is not a blank grabber file') 63 | sys.exit(1) 64 | 65 | # Try to get the file containing the key and IV for decrypting 66 | try: 67 | data: str = get_file('loader-o') 68 | except: 69 | for i,v in extracted.items(): 70 | if len(i) >= 35: 71 | data: str = v 72 | 73 | # Parse the key and iv from the file 74 | data = data.split(b'stub-oz,')[-1].split(b'\x63\x03')[0].split(b'\x10') 75 | print('') 76 | try: 77 | key = base64.b64decode(data[0].split(b'\xDA')[0]) 78 | iv = base64.b64decode(data[-1]) 79 | log('[+] Got Key and IV') 80 | except: 81 | log('[!] Invalid file if you think its an error please contact on discord: lululepu.off') 82 | sys.exit(1) 83 | 84 | 85 | def decrypt(key, iv, ciphertext) -> bytes: 86 | cipher = AES.new(key, AES.MODE_GCM, nonce=iv) 87 | decrypted = cipher.decrypt(ciphertext) 88 | return decrypted 89 | 90 | # Decrypt the blank.aes file 91 | ciphertext = get_file('blank.aes') 92 | try: 93 | decrypted = decrypt(key, iv, zlib.decompress(ciphertext[::-1])) 94 | with io.BytesIO(decrypted) as zip_buffer: 95 | with zipfile.ZipFile(zip_buffer, 'r') as zip: 96 | with zip.open('stub-o.pyc', 'r') as f: 97 | content = f.read() 98 | parsed: str = lzma.decompress(b'\xFD\x37\x7A\x58\x5A\x00'+content.split(b'\xFD\x37\x7A\x58\x5A\x00')[-1]) 99 | log('[+] Decrypted the blank file') 100 | except: 101 | log('[!] An error occured while decrypting the file please contact on discord: lululepu.off') 102 | sys.exit(1) 103 | 104 | # Deobfuscate the code from the decrypted blank.aes zip file 105 | try: 106 | ____ = get_var(parsed, '____') 107 | _____ = get_var(parsed, '_____') 108 | ______ = get_var(parsed, '______') 109 | _______ = get_var(parsed, '_______') 110 | deobfuscated = base64.b64decode(codecs.decode(____, 'rot13')+_____+______[::-1]+_______) 111 | content = deobfuscated.decode('utf-8', errors='replace') 112 | log('[+] Deobfuscated the code') 113 | except: 114 | log('[!] Error occured while deobfuscating please contact on discord: lululepu.off') 115 | sys.exit(1) 116 | 117 | # Get the webhook in all deobfuscated file (its compiled python) 118 | for i in strings(content): 119 | i=bytes(i, encoding='utf8') 120 | try: 121 | a=base64.b64decode(i) 122 | if 'discord.com/api/webhooks/' in a.decode(): 123 | webhook=a.decode() 124 | except:... 125 | 126 | if not webhook: 127 | log('[?] Webhook not found please contact on discord: lululepu.off') 128 | sys.exit(1) 129 | 130 | # Clean/End of the process 131 | log('[+] Got the webhook') 132 | log('[*] Found in {:0.5f}'.format(time.time() - be)) 133 | log('[*] Testing the webhook...') 134 | 135 | # Test the webhook 136 | res=httpx.get(webhook) 137 | if res.status_code != 404: 138 | log('[+] The webhooks is working :') 139 | log(webhook) 140 | else: 141 | rp: str = input(Colorate.Vertical(Colors.red_to_yellow, '[!] The webhooks is not working do you want to get it anyway [y/n]: ')+' ') 142 | if rp.lower() == 'y': 143 | log(webhook) 144 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import zlib 3 | import sys 4 | import os 5 | from uuid import uuid4 as uniquename 6 | 7 | class CTOCEntry: 8 | def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name): 9 | self.position = position 10 | self.cmprsdDataSize = cmprsdDataSize 11 | self.uncmprsdDataSize = uncmprsdDataSize 12 | self.cmprsFlag = cmprsFlag 13 | self.typeCmprsData = typeCmprsData 14 | self.name = name 15 | 16 | class PyInstArchive: 17 | PYINST20_COOKIE_SIZE = 24 18 | PYINST21_COOKIE_SIZE = 24 + 64 19 | MAGIC = b'MEI\014\013\012\013\016' 20 | 21 | def __init__(self, path): 22 | self.barePycList = [] 23 | self.filePath = path 24 | self.pycMagic = b'\0' * 4 25 | self.archiveDict = {} 26 | 27 | def open(self): 28 | try: 29 | self.fPtr = open(self.filePath, 'rb') 30 | self.fileSize = os.stat(self.filePath).st_size 31 | except: 32 | return False 33 | return True 34 | 35 | def close(self): 36 | try: 37 | self.fPtr.close() 38 | except: 39 | pass 40 | 41 | def checkFile(self): 42 | searchChunkSize = 8192 43 | endPos = self.fileSize 44 | self.cookiePos = -1 45 | 46 | if endPos < len(self.MAGIC): 47 | return False 48 | 49 | while True: 50 | startPos = endPos - searchChunkSize if endPos >= searchChunkSize else 0 51 | chunkSize = endPos - startPos 52 | 53 | if chunkSize < len(self.MAGIC): 54 | break 55 | 56 | self.fPtr.seek(startPos, os.SEEK_SET) 57 | data = self.fPtr.read(chunkSize) 58 | 59 | offs = data.rfind(self.MAGIC) 60 | 61 | if offs != -1: 62 | self.cookiePos = startPos + offs 63 | break 64 | 65 | endPos = startPos + len(self.MAGIC) - 1 66 | 67 | if startPos == 0: 68 | break 69 | 70 | if self.cookiePos == -1: 71 | return False 72 | 73 | self.fPtr.seek(self.cookiePos + self.PYINST20_COOKIE_SIZE, os.SEEK_SET) 74 | 75 | if b'python' in self.fPtr.read(64).lower(): 76 | self.pyinstVer = 21 77 | else: 78 | self.pyinstVer = 20 79 | 80 | return True 81 | 82 | def getCArchiveInfo(self): 83 | try: 84 | if self.pyinstVer == 20: 85 | self.fPtr.seek(self.cookiePos, os.SEEK_SET) 86 | (magic, lengthofPackage, toc, tocLen, pyver) = struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE)) 87 | elif self.pyinstVer == 21: 88 | self.fPtr.seek(self.cookiePos, os.SEEK_SET) 89 | (magic, lengthofPackage, toc, tocLen, pyver, pylibname) = struct.unpack('!8sIIii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE)) 90 | except: 91 | return False 92 | 93 | self.pymaj, self.pymin = (pyver//100, pyver%100) if pyver >= 100 else (pyver//10, pyver%10) 94 | 95 | tailBytes = self.fileSize - self.cookiePos - (self.PYINST20_COOKIE_SIZE if self.pyinstVer == 20 else self.PYINST21_COOKIE_SIZE) 96 | self.overlaySize = lengthofPackage + tailBytes 97 | self.overlayPos = self.fileSize - self.overlaySize 98 | self.tableOfContentsPos = self.overlayPos + toc 99 | self.tableOfContentsSize = tocLen 100 | 101 | return True 102 | 103 | def parseTOC(self): 104 | self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET) 105 | self.tocList = [] 106 | parsedLen = 0 107 | 108 | while parsedLen < self.tableOfContentsSize: 109 | (entrySize, ) = struct.unpack('!i', self.fPtr.read(4)) 110 | nameLen = struct.calcsize('!iIIIBc') 111 | (entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = struct.unpack('!IIIBc{0}s'.format(entrySize - nameLen), self.fPtr.read(entrySize - 4)) 112 | 113 | try: 114 | name = name.decode("utf-8").rstrip("\0") 115 | except UnicodeDecodeError: 116 | newName = str(uniquename()) 117 | name = newName 118 | 119 | if len(name) == 0: 120 | name = str(uniquename()) 121 | 122 | self.tocList.append( 123 | CTOCEntry( 124 | self.overlayPos + entryPos, 125 | cmprsdDataSize, 126 | uncmprsdDataSize, 127 | cmprsFlag, 128 | typeCmprsData, 129 | name 130 | ) 131 | ) 132 | 133 | parsedLen += entrySize 134 | 135 | def extractFiles(self): 136 | for entry in self.tocList: 137 | self.fPtr.seek(entry.position, os.SEEK_SET) 138 | data = self.fPtr.read(entry.cmprsdDataSize) 139 | 140 | if entry.cmprsFlag == 1: 141 | try: 142 | data = zlib.decompress(data) 143 | except zlib.error: 144 | continue 145 | 146 | if entry.typeCmprsData == b'd' or entry.typeCmprsData == b'o': 147 | continue 148 | 149 | content = None 150 | 151 | if entry.typeCmprsData == b's': 152 | if self.pycMagic == b'\0' * 4: 153 | self.barePycList.append(entry.name + '.pyc') 154 | content = self._readPyc(data) 155 | 156 | elif entry.typeCmprsData == b'M' or entry.typeCmprsData == b'm': 157 | if data[2:4] == b'\r\n': 158 | if self.pycMagic == b'\0' * 4: 159 | self.pycMagic = data[0:4] 160 | content = data 161 | else: 162 | content = self._readPyc(data) 163 | 164 | else: 165 | content = data 166 | 167 | self.addToDict(entry.name, content) 168 | 169 | if entry.typeCmprsData == b'z' or entry.typeCmprsData == b'Z': 170 | self._extractPyz(entry.name) 171 | 172 | def addToDict(self, filename, content): 173 | parts = filename.split(os.path.sep) 174 | current_dict = self.archiveDict 175 | for part in parts[:-1]: 176 | if part not in current_dict: 177 | current_dict[part] = {} 178 | current_dict = current_dict[part] 179 | current_dict[parts[-1]] = content 180 | 181 | def _readPyc(self, data): 182 | content = bytearray() 183 | content.extend(self.pycMagic) 184 | content.extend(b'\0' * 4) # Pyc header 185 | content.extend(data) 186 | return content 187 | 188 | def _extractPyz(self, name): 189 | pass 190 | 191 | def Extract(file): 192 | arch = PyInstArchive(file) 193 | if arch.open(): 194 | if arch.checkFile(): 195 | if arch.getCArchiveInfo(): 196 | arch.parseTOC() 197 | arch.extractFiles() 198 | arch.close() 199 | return arch.archiveDict 200 | --------------------------------------------------------------------------------