├── README.md ├── .gitignore └── GimmeShelter.py /README.md: -------------------------------------------------------------------------------- 1 | # GimmeShelter 2 | 3 | Situational Awareness script to identify how and where to run implants 4 | 5 | For more information, refer to [this blogpost](https://rwxstoned.github.io/2024-12-06-GimmeShelter/) 6 | 7 | # Usage 8 | 9 | ```python 10 | usage: .\GimmeShelter.py [options] 11 | 12 | options: 13 | -h, --help show this help message and exit 14 | -v, --verbose Show details on odd modules and RWX sections found 15 | 16 | Filtering: 17 | -d, --dotnet Display DotNet processes 18 | -s, --signed Only show signed processes. WARNING: the validity of signature is NOT checked by this script 19 | -n, --net-only Only show processes with winhttp or wininet already loaded 20 | ``` 21 | 22 | # Example Output 23 | 24 | ``` 25 | ------------------- 26 | RuntimeBroker.exe [8224] 27 | [C:\Windows\System32\RuntimeBroker.exe] 28 | Sha256: 579dfced8f02a7e1e6e8df10c400117d3127ead7231776a1e467eb507261e920 29 | ------------------- 30 | (!) Signed 31 | (!) has loaded winhttp.dll 32 | 33 | 34 | ------------------- 35 | Code.exe [13648] 36 | [C:\Users\mickjagger\AppData\Local\Programs\Microsoft VS Code\Code.exe] 37 | Sha256: 292e6e6c7d9db5889c170cc71245ced1e8843599673a973841b7d47aa151efb6 38 | ------------------- 39 | (!) Signed 40 | (!) has loaded winhttp.dll 41 | 42 | 43 | ==== [ Unusual Modules ] ==== 44 | 45 | c:\users\mickjagger\appdata\local\programs\microsoft vs code\ffmpeg.dll 46 | f903f4752f23ad88b08638b8d85c09fc2f1b17084ec655dac89aa559c17387a5 47 | 48 | ==== [ Private memory pages with RWX ] ==== 49 | 50 | ---> 0x18e335201000 49152 bytes 51 | ---> 0x18e335221000 24576 bytes 52 | ---> 0x18e335243000 102400 bytes 53 | ---> 0x7ff6de900000 5505024 bytes 54 | 55 | ------------------- 56 | firefox.exe [1248] 57 | [C:\Program Files\Mozilla Firefox\firefox.exe] 58 | Sha256: 6835705aad4472891a216ab9ae3a6a7805a27310cdaa0614763b21d352d0a624 59 | ------------------- 60 | (!) Signed 61 | 62 | 63 | ==== [ Unusual Modules ] ==== 64 | 65 | c:\program files\mozilla firefox\lgpllibs.dll 66 | 1c9c617f13feac3e94a6311eae20997f944da41bebca124002322ac6863d67e8 67 | c:\program files\mozilla firefox\xul.dll 68 | fa43976fbbbeca97f1116aaf26241daccacf0c29466f17b692d3cf4ee404f1e2 69 | c:\program files\mozilla firefox\gkcodecs.dll 70 | 188f2d44534f978cb28d60e1118e287499adb2f19b0c36929f5badb7c1f94dfd 71 | c:\program files\mozilla firefox\nss3.dll 72 | 8c121892b1bd1a964f194188685ea5d0e057b7d2bf9f9bc328ab5e005518c64d 73 | c:\program files\mozilla firefox\freebl3.dll 74 | c06ba1a174b47e01cbba84f88d1d66429b6bbda22e389bf579598c24ea2b06d8 75 | c:\program files\mozilla firefox\mozglue.dll 76 | 292e38bd831a5d8948936748f48b1d47cfe49999514e95f5e9b165ef80f1203e 77 | c:\program files\mozilla firefox\softokn3.dll 78 | 3dfa45616bcd5fe594b0dd39acf7f5216f8a3816cee188ef672ff8d44e53a2b8 79 | 80 | ==== [ Private memory pages with RWX ] ==== 81 | 82 | ---> 0x1bfd47b1000 65536 bytes 83 | ---> 0x1bfd48b1000 65536 bytes 84 | ---> 0x1bfd48d1000 131072 bytes 85 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /GimmeShelter.py: -------------------------------------------------------------------------------- 1 | import sys,os 2 | import argparse 3 | import psutil 4 | import pefile 5 | import ctypes 6 | import hashlib 7 | from ctypes.wintypes import WORD,DWORD,LPVOID 8 | from ctypes import c_void_p 9 | 10 | 11 | PROCESS_QUERY_INFORMATION = 0x0400 12 | PROCESS_VM_READ = 0x0010 13 | MEM_COMMIT = 0x1000 14 | MEM_FREE = 0x10000 15 | MEM_RESERVE = 0x2000 16 | MEM_IMAGE = 0x1000000 17 | MEM_MAPPED = 0x40000 18 | MEM_PRIVATE = 0x20000 19 | 20 | PAGE_READWRITE = 0x04 21 | PAGE_EXECUTE_READWRITE = 0x40 22 | PAGE_EXECUTE_WRITECOPY = 0x80 23 | 24 | class SYSTEM_INFO(ctypes.Structure): 25 | """https://msdn.microsoft.com/en-us/library/ms724958""" 26 | class _U(ctypes.Union): 27 | class _S(ctypes.Structure): 28 | _fields_ = (('wProcessorArchitecture', WORD), 29 | ('wReserved', WORD)) 30 | _fields_ = (('dwOemId', DWORD), # obsolete 31 | ('_s', _S)) 32 | _anonymous_ = ('_s',) 33 | 34 | 35 | if ctypes.sizeof(ctypes.c_void_p) == ctypes.sizeof(ctypes.c_ulonglong): 36 | DWORD_PTR = ctypes.c_ulonglong 37 | elif ctypes.sizeof(ctypes.c_void_p) == ctypes.sizeof(ctypes.c_ulong): 38 | DWORD_PTR = ctypes.c_ulong 39 | 40 | _fields_ = (('_u', _U), 41 | ('dwPageSize', DWORD), 42 | ('lpMinimumApplicationAddress', LPVOID), 43 | ('lpMaximumApplicationAddress', LPVOID), 44 | ('dwActiveProcessorMask', DWORD_PTR), 45 | ('dwNumberOfProcessors', DWORD), 46 | ('dwProcessorType', DWORD), 47 | ('dwAllocationGranularity', DWORD), 48 | ('wProcessorLevel', WORD), 49 | ('wProcessorRevision', WORD)) 50 | _anonymous_ = ('_u',) 51 | 52 | class MEMORY_BASIC_INFORMATION(ctypes.Structure): 53 | """https://msdn.microsoft.com/en-us/library/aa366775""" 54 | PVOID = LPVOID 55 | SIZE_T = ctypes.c_size_t 56 | _fields_ = (('BaseAddress', PVOID), 57 | ('AllocationBase', PVOID), 58 | ('AllocationProtect', DWORD), 59 | ('RegionSize', SIZE_T), 60 | ('State', DWORD), 61 | ('Protect', DWORD), 62 | ('Type', DWORD)) 63 | 64 | 65 | def findRWX(p): 66 | k32 = ctypes.WinDLL('kernel32', use_last_error=True) 67 | 68 | #Get System Info 69 | LPSYSTEM_INFO = ctypes.POINTER(SYSTEM_INFO) 70 | k32.GetSystemInfo.restype = None 71 | k32.GetSystemInfo.argtypes = (LPSYSTEM_INFO,) 72 | ReadProcessMemory = k32.ReadProcessMemory 73 | sysinfo = SYSTEM_INFO() 74 | k32.GetSystemInfo(ctypes.byref(sysinfo)) 75 | startAddr=sysinfo.lpMinimumApplicationAddress 76 | currAddr = sysinfo.lpMinimumApplicationAddress 77 | endAddr = sysinfo.lpMaximumApplicationAddress 78 | pageSize = sysinfo.dwPageSize 79 | 80 | p = k32.OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ, False, p) 81 | RWXPrivateRanges = [] 82 | RWXImagesRanges = [] 83 | mbi = MEMORY_BASIC_INFORMATION() 84 | while currAddr < endAddr: 85 | ret = k32.VirtualQueryEx(p,c_void_p(currAddr),ctypes.byref(mbi), ctypes.sizeof(mbi)) 86 | if(ret == 0): 87 | print("Error running VirtualQueryEx") 88 | if mbi.Protect == PAGE_EXECUTE_READWRITE: 89 | if mbi.Type == MEM_PRIVATE: 90 | RWXPrivateRanges.append((currAddr,mbi.RegionSize)) 91 | if mbi.Type == MEM_IMAGE: 92 | RWXImagesRanges.append((currAddr,mbi.RegionSize)) 93 | currAddr = currAddr + mbi.RegionSize 94 | 95 | ret = k32.CloseHandle(p) 96 | return RWXPrivateRanges,RWXImagesRanges 97 | 98 | def isCurrentUser(n): 99 | currentUser = os.getlogin() 100 | if (n == currentUser or currentUser == n.split('\\')[1]): 101 | return True 102 | return False 103 | 104 | def isOddModule(m): 105 | normalModules = [ 106 | "c:\\windows\\system32", 107 | "c:\\windows\\fonts", 108 | "c:\\windows\\globalization\\sorting\\", 109 | "c:\\windows\\winsxs\\", 110 | "c:\\windows\\assembly\\", 111 | "c:\\windows\\microsoft.net\\", 112 | "c:\\windows\\syswow64\\", 113 | "c:\\windows\\systemapps\\" 114 | ] 115 | for path in normalModules: 116 | if m.startswith(path): 117 | return False 118 | return True 119 | 120 | def hasWininet(m): 121 | for x in m: 122 | if x.endswith("wininet.dll"): 123 | return True 124 | return False 125 | 126 | def isDotNet(m): 127 | for x in m: 128 | if x.endswith("clr.dll") or x.endswith("mscoree.dll"): 129 | return True 130 | return False 131 | 132 | 133 | def hasWinhttp(m): 134 | for x in m: 135 | if x.endswith("winhttp.dll"): 136 | return True 137 | return False 138 | 139 | def isDll(m): 140 | return m.endswith(".dll") 141 | 142 | def getHash(filepath): 143 | sha256 = hashlib.sha256() 144 | with open(filepath,"rb") as f: 145 | for b in iter(lambda: f.read(4096),b""): 146 | sha256.update(b) 147 | return sha256.hexdigest() 148 | 149 | def printProcess(d): 150 | ''' 151 | If there are interesting things to display for that process, print them. 152 | If not, pass. 153 | ''' 154 | if not (d["hasWininet"] or d["hasWinhttp"] or len(d["rwxImgs"])>0 or len(d["rwxPriv"])>0 or len(d["oddModules"])>0 or d["rwxSections"]): 155 | return 156 | print("-------------------") 157 | print(f"{d['name']}\t[{d['pid']}]") 158 | print(f" [{d['exe']}]") 159 | hash = getHash(d["exe"]) 160 | print(f" Sha256: {hash}") 161 | print("-------------------") 162 | if(d["hasSignature"]): print("\t\t (!) Signed") 163 | if(d["noCFG"]): print("\t\t (!) no CF Guard") 164 | if(d["isDotNet"]): print("\t\t (!) dotNET ") 165 | if(d["hasWininet"]): print("\t\t (!) has loaded wininet.dll ") 166 | if(d["hasWinhttp"]): print("\t\t (!) has loaded winhttp.dll ") 167 | print("") 168 | if len(d["oddModules"])>0: 169 | if(not args.verbose): 170 | print("\t\t (!) Unusual modules found ") 171 | else: 172 | print("\n\t ==== [ Unusual Modules ] ====\n") 173 | for dll in d["oddModules"]: 174 | print(f"\t\t {dll}") 175 | print(f"\t\t\t {getHash(dll)}") 176 | 177 | if(d["rwxSections"]): 178 | if(not args.verbose): 179 | print("\t\t (!) RWX Sections found") 180 | else: 181 | print("\n\t ==== [ RWX Sections ] ====\n") 182 | print(d["rwxSections"]) 183 | 184 | if( len(d["rwxImgs"])>0 ): 185 | if(not args.verbose): 186 | print("\t\t (!) RWX Sections in Images found") 187 | else: 188 | print("\n\t ==== [ Images with RWX ] ====\n") 189 | for r in d["rwxImgs"]: 190 | print(f"\t\t ---> 0x{r[0]:08x}\t{r[1]} bytes") 191 | 192 | if( len(d["rwxPriv"])>0 ): 193 | if(not args.verbose): 194 | print("\t\t (!) Private RWX Sections in Images found") 195 | else: 196 | print("\n\t ==== [ Private memory pages with RWX ] ====\n") 197 | for r in d["rwxPriv"]: 198 | print(f"\t\t ---> 0x{r[0]:08x}\t{r[1]} bytes") 199 | 200 | print("\n") 201 | 202 | def scanPE(process): 203 | IMAGE_SCN_MEM_EXECUTE = 0x20000000 204 | IMAGE_SCN_MEM_READ = 0x40000000 205 | IMAGE_SCN_MEM_WRITE = 0x80000000 206 | pe = pefile.PE(process["exe"], fast_load=True) 207 | process["noCFG"] = not (pe.OPTIONAL_HEADER.DllCharacteristics >> 14) & 0x1 208 | 209 | rwxSections = [] 210 | for s in pe.sections: 211 | if (s.Characteristics & IMAGE_SCN_MEM_EXECUTE) and (s.Characteristics & IMAGE_SCN_MEM_READ) and (s.Characteristics & IMAGE_SCN_MEM_WRITE): 212 | rwxSections.append(s.Name) 213 | process["rwxSections"] = rwxSections 214 | 215 | pe.parse_data_directories( directories=[ 216 | pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY'] 217 | ]) 218 | 219 | process["hasSignature"] = False 220 | for s in pe.__structures__: 221 | if s.name == "IMAGE_DIRECTORY_ENTRY_SECURITY": 222 | process["hasSignature"]=(s.VirtualAddress != 0 and s.Size !=0) 223 | 224 | 225 | pe.close() 226 | 227 | def opts(argv): 228 | p = argparse.ArgumentParser(prog = argv[0], 229 | usage = "%(prog)s [options]") 230 | 231 | p.add_argument('-v','--verbose', action='store_true', help='Show details on odd modules and RWX sections found') 232 | 233 | filter = p.add_argument_group("Filtering") 234 | filter.add_argument('-d','--dotnet', action='store_true', help='Display DotNet processes') 235 | filter.add_argument('-s','--signed', action='store_true', help='Only show signed processes. WARNING: the validity of signature is NOT checked by this script') 236 | filter.add_argument('-n','--net-only', action='store_true', help='Only show processes with winhttp or wininet already loaded') 237 | 238 | return p.parse_args() 239 | 240 | def main(argv): 241 | global args 242 | 243 | print(''' 244 | \t\t\t -*-*- [ GimmeShelter.py ] -*-*- \t\t\t 245 | 246 | Author: RWXstoned\t rwxstoned/at/proton[.]me 247 | --- 248 | Find a shelter for your implants ! 249 | Situational awareness Python script which will help you better blend in when trying to identify how and where to run implants.\n 250 | Review which DLLs are loaded and where; find out what opportunities might be there for module stomping, DLL hijacking, hosting code in RWX sections, etc... 251 | If an executable or DLL is of interest to you, make note of its SHA256 and review it in a lab... 252 | 253 | NOTES: 254 | - When an executable is marked "Signed", the check is fairly rudimentary and does not actually check the validity of that signature - which would imply running extra-commands and be less stealthy. 255 | - This is not a privesc tool. Only processes running under the current user are checked. 256 | --- 257 | ''') 258 | args = opts(argv) 259 | 260 | currentProcesses = [] 261 | pids = psutil.pids() 262 | for pid in pids: 263 | p = psutil.Process(pid) 264 | try: 265 | u = p.username() 266 | except psutil.AccessDenied: 267 | continue 268 | if isCurrentUser(u): 269 | currentProcesses.append(p) 270 | 271 | for p in currentProcesses: 272 | process = {} 273 | try: 274 | process["name"] = p.name() 275 | process["pid"] = p.pid 276 | process["exe"] = p.exe() 277 | except psutil.NoSuchProcess: 278 | continue 279 | try: 280 | mmaps = p.memory_maps() 281 | except psutil.AccessDenied: 282 | continue 283 | all_modules = [m.path.lower() for m in mmaps] 284 | odd_modules = [m for m in all_modules if isOddModule(m)] 285 | odd_modules_dll = [m for m in odd_modules if isDll(m)] 286 | process["isDotNet"] = isDotNet(all_modules) 287 | 288 | if(not args.dotnet and process["isDotNet"]): 289 | continue 290 | 291 | process["hasWininet"] = hasWininet(all_modules) 292 | process["hasWinhttp"] = hasWinhttp(all_modules) 293 | if(args.net_only and not (process["hasWininet"] or process["hasWinhttp"])): 294 | continue 295 | 296 | process["rwxPriv"],process["rwxImgs"] = findRWX(p.pid) 297 | process["oddModules"] = odd_modules_dll 298 | 299 | scanPE(process) 300 | if(args.signed and not process["hasSignature"]): 301 | continue 302 | printProcess(process) 303 | 304 | if __name__ == '__main__': 305 | main(sys.argv) --------------------------------------------------------------------------------