├── .gitignore ├── LogToFile ├── Makefile ├── README.md ├── make.cmd └── spython.c ├── LogToStderr ├── Makefile ├── README.md ├── make.cmd └── spython.c ├── LogToStderrMinimal ├── Makefile ├── README.md ├── make.cmd └── spython.c ├── NetworkPrompt ├── Makefile ├── README.md ├── make.cmd ├── network_prompt.py └── spython.c ├── StartupControl ├── Makefile ├── README.md ├── make.cmd └── spython.c ├── WindowsAMSI ├── README.md ├── amsi_test.py ├── make.cmd └── spython.c ├── WindowsCatFile ├── README.md ├── make.cmd └── spython.c ├── WindowsDefenderAppControl ├── README.md ├── make.cmd ├── policy.xml ├── reset.ps1 ├── spython.c └── update.ps1 ├── WindowsEventLog ├── README.md ├── disable.cmd ├── enable.cmd ├── events.man ├── make.cmd ├── run.cmd └── spython.cpp ├── execveat ├── Makefile ├── readme.md └── spython.c ├── journald ├── Makefile ├── readme.md └── spython.c ├── journald_network ├── Makefile ├── readme.md └── spython.c ├── linux_xattr ├── Makefile ├── mkxattr.py ├── readme.md └── spython.c ├── readme.md └── syslog ├── Makefile ├── readme.md └── spython.c /.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | *.exe 3 | *.obj 4 | *.exe.log 5 | *.pdb 6 | *.ilk 7 | *.obj 8 | *.o 9 | **/spython 10 | **/spython.log 11 | __pycache__/ -------------------------------------------------------------------------------- /LogToFile/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3.8-config --cflags) 4 | 5 | LDFLAGS+=$(shell python3.8-config --ldflags --embed) 6 | 7 | objects=spython.o 8 | 9 | all: spython 10 | 11 | %.o: %.c 12 | $(CC) -c $< $(CFLAGS) 13 | 14 | spython: spython.o 15 | $(CC) -o $@ $^ $(LDFLAGS) 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -rf *.o spython 20 | -------------------------------------------------------------------------------- /LogToFile/README.md: -------------------------------------------------------------------------------- 1 | LogToFile 2 | ========= 3 | 4 | This sample writes messages to a file and limits some events. 5 | 6 | It also limits `open_code` to only allow Python (.py) and path (.pth) files, provided they do not contain the string `I am a virus` (you can probably find a better heuristic). 7 | 8 | To build on Windows, open the Visual Studio Developer Command prompt of your choice (making sure you have a suitable Python install or build). Run `set PYTHONDIR=`, then run `make.cmd` to build. Once built, the new `spython.exe` will need to be moved into `%PYTHONDIR%`. 9 | 10 | To build on Linux, run `make` with Python 3.8.0rc1 or later installed. 11 | -------------------------------------------------------------------------------- /LogToFile/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | cl -nologo %_MD% -c spython.c -Foobj\spython.obj -Iobj -Zi -O2 %_PYTHONINCLUDE% 25 | @if errorlevel 1 exit /B %ERRORLEVEL% 26 | link /nologo obj\spython.obj /out:spython.exe /debug:FULL /pdb:spython.pdb /libpath:"%_PYTHONLIB%" 27 | @if errorlevel 1 exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /LogToFile/spython.c: -------------------------------------------------------------------------------- 1 | /* Minimal main program -- everything is loaded from the library */ 2 | 3 | #include "Python.h" 4 | #include "opcode.h" 5 | #include 6 | #include 7 | 8 | #ifdef __FreeBSD__ 9 | #include 10 | #endif 11 | 12 | static int 13 | hook_addaudithook(const char *event, PyObject *args, FILE *audit_log) 14 | { 15 | fprintf(audit_log, "%s: hook was not added\n", event); 16 | PyErr_SetString(PyExc_SystemError, "hook not permitted"); 17 | return -1; 18 | } 19 | 20 | 21 | // Note that this event is raised by our hook below - it is not a "standard" audit item 22 | static int 23 | hook_open_code(const char *event, PyObject *args, FILE *audit_log) 24 | { 25 | PyObject *path = PyTuple_GetItem(args, 0); 26 | PyObject *disallow = PyTuple_GetItem(args, 1); 27 | 28 | PyObject *msg = PyUnicode_FromFormat("'%S'; allowed = %S", 29 | path, disallow); 30 | if (!msg) { 31 | return -1; 32 | } 33 | 34 | fprintf(audit_log, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 35 | Py_DECREF(msg); 36 | 37 | return 0; 38 | } 39 | 40 | 41 | static int 42 | hook_import(const char *event, PyObject *args, FILE *audit_log) 43 | { 44 | PyObject *module, *filename, *sysPath, *sysMetaPath, *sysPathHooks; 45 | if (!PyArg_ParseTuple(args, "OOOOO", &module, &filename, &sysPath, 46 | &sysMetaPath, &sysPathHooks)) { 47 | return -1; 48 | } 49 | 50 | PyObject *msg; 51 | if (PyObject_IsTrue(filename)) { 52 | msg = PyUnicode_FromFormat("importing %S from %S", 53 | module, filename); 54 | } else { 55 | msg = PyUnicode_FromFormat("importing %S:\n" 56 | " sys.path=%S\n" 57 | " sys.meta_path=%S\n" 58 | " sys.path_hooks=%S", 59 | module, sysPath, sysMetaPath, 60 | sysPathHooks); 61 | } 62 | 63 | if (!msg) { 64 | return -1; 65 | } 66 | 67 | fprintf(audit_log, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 68 | Py_DECREF(msg); 69 | 70 | return 0; 71 | } 72 | 73 | 74 | static int 75 | hook_compile(const char *event, PyObject *args, FILE *audit_log) 76 | { 77 | PyObject *code, *filename, *_; 78 | if (!PyArg_ParseTuple(args, "OO", &code, &filename, 79 | &_, &_, &_, &_, &_, &_)) { 80 | return -1; 81 | } 82 | 83 | if (!PyUnicode_Check(code)) { 84 | code = PyObject_Repr(code); 85 | if (!code) { 86 | return -1; 87 | } 88 | } else { 89 | Py_INCREF(code); 90 | } 91 | 92 | if (PyUnicode_GetLength(code) > 200) { 93 | Py_SETREF(code, PyUnicode_Substring(code, 0, 200)); 94 | if (!code) { 95 | return -1; 96 | } 97 | Py_SETREF(code, PyUnicode_FromFormat("%S...", code)); 98 | if (!code) { 99 | return -1; 100 | } 101 | } 102 | 103 | PyObject *msg; 104 | if (PyObject_IsTrue(filename)) { 105 | if (code == Py_None) { 106 | msg = PyUnicode_FromFormat("compiling from file %S", 107 | filename); 108 | } else { 109 | msg = PyUnicode_FromFormat("compiling %S: %S", 110 | filename, code); 111 | } 112 | } else { 113 | msg = PyUnicode_FromFormat("compiling: %R", code); 114 | } 115 | Py_DECREF(code); 116 | if (!msg) { 117 | return -1; 118 | } 119 | 120 | fprintf(audit_log, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 121 | Py_DECREF(msg); 122 | return 0; 123 | } 124 | 125 | 126 | static int 127 | hook_code_new(const char *event, PyObject *args, FILE *audit_log) 128 | { 129 | PyObject *code, *filename, *name; 130 | int argcount, kwonlyargcount, nlocals, stacksize, flags; 131 | if (!PyArg_ParseTuple(args, "OOOiiiii", &code, &filename, &name, 132 | &argcount, &kwonlyargcount, &nlocals, 133 | &stacksize, &flags)) { 134 | return -1; 135 | } 136 | 137 | PyObject *msg = PyUnicode_FromFormat("compiling: %R", filename); 138 | if (!msg) { 139 | return -1; 140 | } 141 | 142 | fprintf(audit_log, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 143 | Py_DECREF(msg); 144 | 145 | if (!PyBytes_Check(code)) { 146 | PyErr_SetString(PyExc_TypeError, "Invalid bytecode object"); 147 | return -1; 148 | } 149 | 150 | // As an example, let's validate that no STORE_FAST operations are 151 | // going to overflow nlocals. 152 | char *wcode; 153 | Py_ssize_t wlen; 154 | if (PyBytes_AsStringAndSize(code, &wcode, &wlen) < 0) { 155 | return -1; 156 | } 157 | 158 | for (Py_ssize_t i = 0; i < wlen; i += 2) { 159 | if (wcode[i] == STORE_FAST) { 160 | if (wcode[i + 1] > nlocals) { 161 | PyErr_SetString(PyExc_ValueError, "invalid code object"); 162 | fprintf(audit_log, "%s: code stores to local %d " 163 | "but only allocates %d\n", 164 | event, wcode[i + 1], nlocals); 165 | return -1; 166 | } 167 | } 168 | } 169 | 170 | return 0; 171 | } 172 | 173 | 174 | static int 175 | hook_pickle_find_class(const char *event, PyObject *args, FILE *audit_log) 176 | { 177 | PyObject *mod = PyTuple_GetItem(args, 0); 178 | PyObject *global = PyTuple_GetItem(args, 1); 179 | 180 | PyObject *msg = PyUnicode_FromFormat("finding %R.%R blocked", 181 | mod, global); 182 | if (!msg) { 183 | return -1; 184 | } 185 | 186 | fprintf(audit_log, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 187 | Py_DECREF(msg); 188 | PyErr_SetString(PyExc_RuntimeError, 189 | "unpickling arbitrary objects is disallowed"); 190 | return -1; 191 | } 192 | 193 | 194 | static int 195 | hook_system(const char *event, PyObject *args, FILE *audit_log) 196 | { 197 | PyObject *cmd = PyTuple_GetItem(args, 0); 198 | 199 | PyObject *msg = PyUnicode_FromFormat("%S", cmd); 200 | if (!msg) { 201 | return -1; 202 | } 203 | 204 | fprintf(audit_log, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 205 | Py_DECREF(msg); 206 | 207 | PyErr_SetString(PyExc_RuntimeError, "os.system() is disallowed"); 208 | return -1; 209 | } 210 | 211 | 212 | static int 213 | default_spython_hook(const char *event, PyObject *args, void *userData) 214 | { 215 | assert(userData); 216 | 217 | if (strcmp(event, "sys.addaudithook") == 0) { 218 | return hook_addaudithook(event, args, (FILE*)userData); 219 | } 220 | 221 | if (strcmp(event, "spython.open_code") == 0) { 222 | return hook_open_code(event, args, (FILE*)userData); 223 | } 224 | 225 | if (strcmp(event, "import") == 0) { 226 | return hook_import(event, args, (FILE*)userData); 227 | } 228 | 229 | if (strcmp(event, "compile") == 0) { 230 | return hook_compile(event, args, (FILE*)userData); 231 | } 232 | 233 | if (strcmp(event, "code.__new__") == 0) { 234 | return hook_code_new(event, args, (FILE*)userData); 235 | } 236 | 237 | if (strcmp(event, "pickle.find_class") == 0) { 238 | return hook_pickle_find_class(event, args, (FILE*)userData); 239 | } 240 | 241 | if (strcmp(event, "os.system") == 0) { 242 | return hook_system(event, args, (FILE*)userData); 243 | } 244 | 245 | // All other events just get printed 246 | PyObject *msg = PyObject_Repr(args); 247 | if (!msg) { 248 | return -1; 249 | } 250 | 251 | fprintf((FILE*)userData, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 252 | Py_DECREF(msg); 253 | 254 | return 0; 255 | } 256 | 257 | static PyObject * 258 | spython_open_code(PyObject *path, void *userData) 259 | { 260 | static PyObject *io = NULL; 261 | PyObject *stream = NULL, *buffer = NULL, *err = NULL; 262 | 263 | const char *ext = strrchr(PyUnicode_AsUTF8(path), '.'); 264 | int disallow = !ext || ( 265 | PyOS_stricmp(ext, ".py") != 0 266 | && PyOS_stricmp(ext, ".pth") != 0); 267 | 268 | PyObject *b = PyBool_FromLong(!disallow); 269 | if (PySys_Audit("spython.open_code", "OO", path, b) < 0) { 270 | Py_DECREF(b); 271 | return NULL; 272 | } 273 | Py_DECREF(b); 274 | 275 | if (disallow) { 276 | PyErr_SetString(PyExc_OSError, "invalid format - only .py"); 277 | return NULL; 278 | } 279 | 280 | if (!io) { 281 | io = PyImport_ImportModule("_io"); 282 | if (!io) { 283 | return NULL; 284 | } 285 | } 286 | 287 | stream = PyObject_CallMethod(io, "open", "Osisssi", path, "rb", 288 | -1, NULL, NULL, NULL, 1); 289 | if (!stream) { 290 | return NULL; 291 | } 292 | 293 | buffer = PyObject_CallMethod(stream, "read", "(i)", -1); 294 | 295 | if (!buffer) { 296 | Py_DECREF(stream); 297 | return NULL; 298 | } 299 | 300 | err = PyObject_CallMethod(stream, "close", NULL); 301 | Py_DECREF(stream); 302 | if (!err) { 303 | return NULL; 304 | } 305 | 306 | /* Here is a good place to validate the contents of 307 | * buffer and raise an error if not permitted 308 | */ 309 | if (strstr(PyBytes_AsString(buffer), "I am a virus")) { 310 | Py_DECREF(buffer); 311 | PyErr_SetString(PyExc_OSError, "loading this file is not allowed"); 312 | return NULL; 313 | } 314 | 315 | return PyObject_CallMethod(io, "BytesIO", "N", buffer); 316 | } 317 | 318 | static int 319 | spython_usage(int exitcode, wchar_t *program) 320 | { 321 | FILE *f = exitcode ? stderr : stdout; 322 | 323 | fprintf(f, "usage: %ls file [arg] ...\n" , program); 324 | 325 | return exitcode; 326 | } 327 | 328 | static int 329 | spython_main(int argc, wchar_t **argv, FILE *audit_log) 330 | { 331 | if (argc == 1) { 332 | return spython_usage(1, argv[0]); 333 | } 334 | 335 | /* The auditing log should be opened by the platform-specific main */ 336 | if (!audit_log) { 337 | Py_FatalError("failed to open log file"); 338 | return 1; 339 | } 340 | 341 | /* Run the interactive loop. This should be removed for production use */ 342 | if (wcscmp(argv[1], L"-i") == 0) { 343 | fclose(audit_log); 344 | audit_log = stderr; 345 | } 346 | 347 | PySys_AddAuditHook(default_spython_hook, audit_log); 348 | PyFile_SetOpenCodeHook(spython_open_code, NULL); 349 | 350 | Py_IgnoreEnvironmentFlag = 1; 351 | Py_NoUserSiteDirectory = 1; 352 | Py_DontWriteBytecodeFlag = 1; 353 | 354 | Py_SetProgramName(argv[0]); 355 | Py_Initialize(); 356 | PySys_SetArgv(argc - 1, &argv[1]); 357 | 358 | /* Run the interactive loop. This should be removed for production use */ 359 | if (wcscmp(argv[1], L"-i") == 0) { 360 | PyRun_InteractiveLoop(stdin, ""); 361 | Py_Finalize(); 362 | return 0; 363 | } 364 | 365 | FILE *fp = _Py_wfopen(argv[1], L"r"); 366 | if (fp != NULL) { 367 | (void)PyRun_SimpleFile(fp, "__main__"); 368 | PyErr_Clear(); 369 | fclose(fp); 370 | } else { 371 | fprintf(stderr, "failed to open source file %ls\n", argv[1]); 372 | } 373 | 374 | Py_Finalize(); 375 | return 0; 376 | } 377 | 378 | #ifdef MS_WINDOWS 379 | int 380 | wmain(int argc, wchar_t **argv) 381 | { 382 | FILE *audit_log; 383 | wchar_t *log_path = NULL; 384 | size_t log_path_len; 385 | 386 | if (_wgetenv_s(&log_path_len, NULL, 0, L"SPYTHONLOG") == 0 && 387 | log_path_len) { 388 | log_path_len += 1; 389 | log_path = (wchar_t*)malloc(log_path_len * sizeof(wchar_t)); 390 | _wgetenv_s(&log_path_len, log_path, log_path_len, L"SPYTHONLOG"); 391 | } else { 392 | log_path_len = wcslen(argv[0]) + 5; 393 | log_path = (wchar_t*)malloc(log_path_len * sizeof(wchar_t)); 394 | wcscpy_s(log_path, log_path_len, argv[0]); 395 | wcscat_s(log_path, log_path_len, L".log"); 396 | } 397 | 398 | if (_wfopen_s(&audit_log, log_path, L"w")) { 399 | fwprintf_s(stderr, 400 | L"Fatal Python error: failed to open log file: %s\n", 401 | log_path); 402 | return 1; 403 | } 404 | free(log_path); 405 | 406 | return spython_main(argc, argv, audit_log); 407 | } 408 | 409 | #else 410 | 411 | int 412 | main(int argc, char **argv) 413 | { 414 | wchar_t **argv_copy; 415 | /* We need a second copy, as Python might modify the first one. */ 416 | wchar_t **argv_copy2; 417 | int i, res; 418 | FILE *audit_log; 419 | 420 | argv_copy = (wchar_t **)malloc(sizeof(wchar_t*) * (argc+1)); 421 | argv_copy2 = (wchar_t **)malloc(sizeof(wchar_t*) * (argc+1)); 422 | if (!argv_copy || !argv_copy2) { 423 | fprintf(stderr, "out of memory\n"); 424 | return 1; 425 | } 426 | 427 | /* Convert from char to wchar_t based on the locale settings */ 428 | for (i = 0; i < argc; i++) { 429 | argv_copy[i] = Py_DecodeLocale(argv[i], NULL); 430 | if (!argv_copy[i]) { 431 | fprintf(stderr, "Fatal Python error: " 432 | "unable to decode the command line argument #%i\n", 433 | i + 1); 434 | return 1; 435 | } 436 | argv_copy2[i] = argv_copy[i]; 437 | } 438 | argv_copy2[argc] = argv_copy[argc] = NULL; 439 | 440 | if (getenv("SPYTHONLOG")) { 441 | audit_log = fopen(getenv("SPYTHONLOG"), "w"); 442 | if (!audit_log) { 443 | fprintf(stderr, "Fatal Python error: " 444 | "failed to open log file: %s\n", getenv("SPYTHONLOG")); 445 | return 1; 446 | } 447 | } else { 448 | unsigned int log_path_len = strlen(argv[0]) + 5; 449 | char *log_path = (char*)malloc(log_path_len); 450 | strcpy(log_path, argv[0]); 451 | strcat(log_path, ".log"); 452 | audit_log = fopen(log_path, "w"); 453 | if (!audit_log) { 454 | fprintf(stderr, "Fatal Python error: " 455 | "failed to open log file: %s\n", log_path); 456 | return 1; 457 | } 458 | free(log_path); 459 | } 460 | 461 | res = spython_main(argc, argv_copy, audit_log); 462 | 463 | for (i = 0; i < argc; i++) { 464 | free(argv_copy2[i]); 465 | } 466 | free(argv_copy); 467 | free(argv_copy2); 468 | return res; 469 | } 470 | #endif 471 | -------------------------------------------------------------------------------- /LogToStderr/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3.8-config --cflags) 4 | 5 | LDFLAGS+=$(shell python3.8-config --ldflags --embed) 6 | 7 | objects=spython.o 8 | 9 | all: spython 10 | 11 | %.o: %.c 12 | $(CC) -c $< $(CFLAGS) 13 | 14 | spython: spython.o 15 | $(CC) -o $@ $^ $(LDFLAGS) 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -rf *.o spython 20 | -------------------------------------------------------------------------------- /LogToStderr/README.md: -------------------------------------------------------------------------------- 1 | LogToStderr 2 | =========== 3 | 4 | This sample writes messages to standard output. That's all it does. 5 | 6 | To build on Windows, open the Visual Studio Developer Command prompt of your choice (making sure you have a suitable Python install or build). Run `set PYTHONDIR=`, then run `make.cmd` to build. Once built, the new `spython.exe` will need to be moved into `%PYTHONDIR%` or you can also set `PYTHONHOME` 7 | 8 | To bulid on Linux, run `make` with Python 3.8.0rc1 or later installed. 9 | -------------------------------------------------------------------------------- /LogToStderr/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | cl -nologo %_MD% -c spython.c -Foobj\spython.obj -Iobj -Zi -O2 %_PYTHONINCLUDE% 25 | @if errorlevel 1 exit /B %ERRORLEVEL% 26 | link /nologo obj\spython.obj /out:spython.exe /debug:FULL /pdb:spython.pdb /libpath:"%_PYTHONLIB%" 27 | @if errorlevel 1 exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /LogToStderr/spython.c: -------------------------------------------------------------------------------- 1 | /* Minimal main program -- everything is loaded from the library */ 2 | 3 | #include "Python.h" 4 | #include "opcode.h" 5 | #include 6 | #include 7 | 8 | #ifdef __FreeBSD__ 9 | #include 10 | #endif 11 | 12 | static int 13 | hook_compile(const char *event, PyObject *args) 14 | { 15 | PyObject *code, *filename; 16 | if (!PyArg_ParseTuple(args, "OO", &code, &filename)) { 17 | return -1; 18 | } 19 | 20 | if (!PyUnicode_Check(code)) { 21 | code = PyObject_Repr(code); 22 | if (!code) { 23 | return -1; 24 | } 25 | } else { 26 | Py_INCREF(code); 27 | } 28 | 29 | if (PyUnicode_GetLength(code) > 200) { 30 | Py_SETREF(code, PyUnicode_Substring(code, 0, 200)); 31 | if (!code) { 32 | return -1; 33 | } 34 | Py_SETREF(code, PyUnicode_FromFormat("%S...", code)); 35 | if (!code) { 36 | return -1; 37 | } 38 | } 39 | 40 | PyObject *msg; 41 | if (PyObject_IsTrue(filename)) { 42 | if (code == Py_None) { 43 | msg = PyUnicode_FromFormat("compiling from file %S", 44 | filename); 45 | } else { 46 | msg = PyUnicode_FromFormat("compiling %S: %S", 47 | filename, code); 48 | } 49 | } else { 50 | msg = PyUnicode_FromFormat("compiling: %R", code); 51 | } 52 | Py_DECREF(code); 53 | if (!msg) { 54 | return -1; 55 | } 56 | 57 | fprintf(stderr, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 58 | Py_DECREF(msg); 59 | return 0; 60 | } 61 | 62 | static int 63 | default_spython_hook(const char *event, PyObject *args, void *userData) 64 | { 65 | if (!Py_IsInitialized()) { 66 | fprintf(stderr, "%s: during startup/shutdown we cannot call repr() on arguments\n", event); 67 | return 0; 68 | } 69 | 70 | /* We handle compile() separately to trim the very long code argument */ 71 | if (strcmp(event, "compile") == 0) { 72 | return hook_compile(event, args); 73 | } 74 | 75 | // All other events just get printed 76 | PyObject *msg = PyObject_Repr(args); 77 | if (!msg) { 78 | return -1; 79 | } 80 | 81 | fprintf(stderr, "%s: %s\n", event, PyUnicode_AsUTF8(msg)); 82 | Py_DECREF(msg); 83 | 84 | return 0; 85 | } 86 | 87 | static PyObject * 88 | spython_open_code(PyObject *path, void *userData) 89 | { 90 | static PyObject *io = NULL; 91 | PyObject *stream = NULL, *buffer = NULL, *err = NULL; 92 | 93 | if (PySys_Audit("spython.open_code", "O", path) < 0) { 94 | return NULL; 95 | } 96 | 97 | if (!io) { 98 | io = PyImport_ImportModule("_io"); 99 | if (!io) { 100 | return NULL; 101 | } 102 | } 103 | 104 | stream = PyObject_CallMethod(io, "open", "Osisssi", path, "rb", 105 | -1, NULL, NULL, NULL, 1); 106 | if (!stream) { 107 | return NULL; 108 | } 109 | 110 | buffer = PyObject_CallMethod(stream, "read", "(i)", -1); 111 | 112 | if (!buffer) { 113 | Py_DECREF(stream); 114 | return NULL; 115 | } 116 | 117 | err = PyObject_CallMethod(stream, "close", NULL); 118 | Py_DECREF(stream); 119 | if (!err) { 120 | return NULL; 121 | } 122 | 123 | /* Here is a good place to validate the contents of 124 | * buffer and raise an error if not permitted 125 | */ 126 | 127 | return PyObject_CallMethod(io, "BytesIO", "N", buffer); 128 | } 129 | 130 | #ifdef MS_WINDOWS 131 | int 132 | wmain(int argc, wchar_t **argv) 133 | { 134 | PySys_AddAuditHook(default_spython_hook, NULL); 135 | PyFile_SetOpenCodeHook(spython_open_code, NULL); 136 | return Py_Main(argc, argv); 137 | } 138 | #else 139 | int 140 | main(int argc, char **argv) 141 | { 142 | PySys_AddAuditHook(default_spython_hook, NULL); 143 | PyFile_SetOpenCodeHook(spython_open_code, NULL); 144 | return Py_BytesMain(argc, argv); 145 | } 146 | #endif 147 | -------------------------------------------------------------------------------- /LogToStderrMinimal/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3.8-config --cflags) 4 | 5 | LDFLAGS+=$(shell python3.8-config --ldflags --embed) 6 | 7 | objects=spython.o 8 | 9 | all: spython 10 | 11 | %.o: %.c 12 | $(CC) -c $< $(CFLAGS) 13 | 14 | spython: spython.o 15 | $(CC) -o $@ $^ $(LDFLAGS) 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -rf *.o spython 20 | -------------------------------------------------------------------------------- /LogToStderrMinimal/README.md: -------------------------------------------------------------------------------- 1 | LogToStderrMinimal 2 | ================== 3 | 4 | This sample writes messages to standard output. Compared to the `LogToStderr` sample, this is the minimal amount of code to see some output for every event. 5 | 6 | To build on Windows, open the Visual Studio Developer Command prompt of your choice (making sure you have a suitable Python install or build). Run `set PYTHONDIR=`, then run `make.cmd` to build. Once built, the new `spython.exe` will need to be moved into `%PYTHONDIR%` or you can also set `PYTHONHOME` 7 | 8 | To build on Linux, run `make` with Python 3.8.0rc1 or later installed. 9 | -------------------------------------------------------------------------------- /LogToStderrMinimal/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | cl -nologo %_MD% -c spython.c -Foobj\spython.obj -Iobj -Zi -O2 %_PYTHONINCLUDE% 25 | @if errorlevel 1 exit /B %ERRORLEVEL% 26 | link /nologo obj\spython.obj /out:spython.exe /debug:FULL /pdb:spython.pdb /libpath:"%_PYTHONLIB%" 27 | @if errorlevel 1 exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /LogToStderrMinimal/spython.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | #include 3 | 4 | int audit_hook(const char *event, PyObject *args, void *userData) 5 | { 6 | if (Py_IsInitialized()) { 7 | PyObject *argsRepr = PyObject_Repr(args); 8 | if (!argsRepr) { 9 | return -1; 10 | } 11 | printf("Event occurred: %s(%.80s)\n", event, PyUnicode_AsUTF8(argsRepr)); 12 | Py_DECREF(argsRepr); 13 | return 0; 14 | } 15 | 16 | printf("Event occurred: %s\n", event); 17 | return 0; 18 | } 19 | 20 | int main(int argc, char **argv) 21 | { 22 | PySys_AddAuditHook(audit_hook, NULL); 23 | return Py_BytesMain(argc, argv); 24 | } 25 | -------------------------------------------------------------------------------- /NetworkPrompt/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3.8-config --cflags) 4 | 5 | LDFLAGS+=$(shell python3.8-config --ldflags --embed) 6 | 7 | objects=spython.o 8 | 9 | all: spython 10 | 11 | %.o: %.c 12 | $(CC) -c $< $(CFLAGS) 13 | 14 | spython: spython.o 15 | $(CC) -o $@ $^ $(LDFLAGS) 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -rf *.o spython 20 | -------------------------------------------------------------------------------- /NetworkPrompt/README.md: -------------------------------------------------------------------------------- 1 | NetworkPrompt 2 | ============= 3 | 4 | This sample intercepts socket events and prompts on stderr/stdin before continuing. 5 | 6 | Example output 7 | -------------- 8 | 9 | ``` 10 | $ python 11 | >>> import network_prompt 12 | Enabled prompting on network access. 13 | >>> from urllib.request import urlopen 14 | >>> urlopen("http://example.com").read() 15 | WARNING: Attempt to resolve example.com:80. Continue [Y/n] 16 | y 17 | WARNING: Attempt to connect 93.184.216.34:80. Continue [Y/n] 18 | y 19 | b'\n\n\n ... 20 | >>> 21 | ``` 22 | 23 | To try this sample without compiling Python, just `import network_prompt` in a standard Python 3.8 build. 24 | 25 | To use as native code, compile `spython.c` and launch that instead of `python`. 26 | -------------------------------------------------------------------------------- /NetworkPrompt/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | cl -nologo %_MD% -c spython.c -Foobj\spython.obj -Iobj -Zi -O2 %_PYTHONINCLUDE% 25 | @if errorlevel 1 exit /B %ERRORLEVEL% 26 | link /nologo obj\spython.obj /out:spython.exe /debug:FULL /pdb:spython.pdb /libpath:"%_PYTHONLIB%" 27 | @if errorlevel 1 exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /NetworkPrompt/network_prompt.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Python implementation of the NetworkPrompt example. 3 | 4 | `import network_prompt` to enable the prompts in Python 3.8. 5 | ''' 6 | 7 | import sys 8 | 9 | def network_prompt_hook(event, args): 10 | # Only care about 'socket.' events 11 | if not event.startswith("socket."): 12 | return 13 | 14 | if event == "socket.getaddrinfo": 15 | msg = "WARNING: Attempt to resolve {}:{}".format(args[0], args[1]) 16 | elif event == "socket.connect": 17 | addro = args[0] 18 | msg = "WARNING: Attempt to connect {}:{}".format(addro[0], addro[1]) 19 | else: 20 | msg = "WARNING: {} (event not handled)".format(event) 21 | 22 | ch = input(msg + ". Continue [Y/n]\n") 23 | if ch == 'n' or ch == 'N': 24 | sys.exit(1) 25 | 26 | sys.addaudithook(network_prompt_hook) 27 | print("Enabled prompting on network access.") 28 | -------------------------------------------------------------------------------- /NetworkPrompt/spython.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | #include "opcode.h" 3 | #include 4 | #include 5 | 6 | static int 7 | network_prompt_hook(const char *event, PyObject *args, void *userData) 8 | { 9 | /* Only care about 'socket.' events */ 10 | if (strncmp(event, "socket.", 7) != 0) { 11 | return 0; 12 | } 13 | 14 | PyObject *msg = NULL; 15 | 16 | /* So yeah, I'm very lazily using PyTuple_GET_ITEM here. 17 | Not best practice! PyArg_ParseTuple is much better! */ 18 | if (strcmp(event, "socket.getaddrinfo") == 0) { 19 | msg = PyUnicode_FromFormat("WARNING: Attempt to resolve %S:%S", 20 | PyTuple_GET_ITEM(args, 0), PyTuple_GET_ITEM(args, 1)); 21 | } else if (strcmp(event, "socket.connect") == 0) { 22 | PyObject *addro = PyTuple_GET_ITEM(args, 1); 23 | msg = PyUnicode_FromFormat("WARNING: Attempt to connect %S:%S", 24 | PyTuple_GET_ITEM(addro, 0), PyTuple_GET_ITEM(addro, 1)); 25 | } else { 26 | msg = PyUnicode_FromFormat("WARNING: %s (event not handled)", event); 27 | } 28 | 29 | if (!msg) { 30 | return -1; 31 | } 32 | 33 | fprintf(stderr, "%s. Continue [Y/n]\n", PyUnicode_AsUTF8(msg)); 34 | Py_DECREF(msg); 35 | int ch = fgetc(stdin); 36 | if (ch == 'n' || ch == 'N') { 37 | exit(1); 38 | } 39 | 40 | while (ch != '\n') { 41 | ch = fgetc(stdin); 42 | } 43 | 44 | return 0; 45 | } 46 | 47 | 48 | #ifdef MS_WINDOWS 49 | int 50 | wmain(int argc, wchar_t **argv) 51 | { 52 | PySys_AddAuditHook(network_prompt_hook, NULL); 53 | return Py_Main(argc, argv); 54 | } 55 | #else 56 | int 57 | main(int argc, char **argv) 58 | { 59 | PySys_AddAuditHook(network_prompt_hook, NULL); 60 | return _Py_UnixMain(argc, argv); 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /StartupControl/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3.8-config --cflags) 4 | 5 | LDFLAGS+=$(shell python3.8-config --ldflags --embed) 6 | 7 | objects=spython.o 8 | 9 | all: spython 10 | 11 | %.o: %.c 12 | $(CC) -c $< $(CFLAGS) 13 | 14 | spython: spython.o 15 | $(CC) -o $@ $^ $(LDFLAGS) 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -rf *.o spython 20 | -------------------------------------------------------------------------------- /StartupControl/README.md: -------------------------------------------------------------------------------- 1 | StartupControl 2 | ============== 3 | 4 | This sample disallows all ways of launching Python other than passing a filename. 5 | 6 | To build on Windows, open the Visual Studio Developer Command prompt of your choice (making sure you have a suitable Python install or build). Run `set PYTHONDIR=`, then run `make.cmd` to build. Once built, the new `spython.exe` will need to be moved into `%PYTHONDIR%` or you can also set `PYTHONHOME` 7 | 8 | To build on Linux, run `make` with Python 3.8.0rc1 or later installed. 9 | -------------------------------------------------------------------------------- /StartupControl/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | cl -nologo %_MD% -c spython.c -Foobj\spython.obj -Iobj -Zi -O2 %_PYTHONINCLUDE% 25 | @if errorlevel 1 exit /B %ERRORLEVEL% 26 | link /nologo obj\spython.obj /out:spython.exe /debug:FULL /pdb:spython.pdb /libpath:"%_PYTHONLIB%" 27 | @if errorlevel 1 exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /StartupControl/spython.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | 3 | int startup_hook(const char *event, PyObject *args, void *userData) 4 | { 5 | /* cpython.run_*(*) - disable launch options except run_file */ 6 | if (strncmp(event, "cpython.run_", 12) == 0 7 | && strcmp(event, "cpython.run_file") != 0) { 8 | PyErr_Format(PyExc_OSError, "'%.100s' is disabled by policy", &event[8]); 9 | return -1; 10 | } 11 | return 0; 12 | } 13 | 14 | int main(int argc, char **argv) 15 | { 16 | PySys_AddAuditHook(startup_hook, NULL); 17 | return Py_BytesMain(argc, argv); 18 | } 19 | -------------------------------------------------------------------------------- /WindowsAMSI/README.md: -------------------------------------------------------------------------------- 1 | WindowsAMSI 2 | =========== 3 | 4 | This sample uses the [Antimalware Scan Interface](https://docs.microsoft.com/windows/win32/amsi/) (AMSI) available in Windows 10 or Windows Server 2016 to allow active antimalware software to scan dynamically compiled Python code. 5 | 6 | To build, open the Visual Studio Developer Command prompt of your choice (making sure you have a suitable Python install or build). Run `set PYTHONDIR=`, then run `make.cmd` to build. 7 | 8 | For testing, you can execute the [amsi_test.py](amsi_test.py) file in this directory, or pass the [EICAR test file](http://www.eicar.org/anti_virus_test_file.htm) to a `compile()` or `exec()` call. Typing the test file at an interactive is not sufficient, because Python compiles directly from `stdin` and no `compile` event is raised. 9 | 10 | This sample does not handle the `cpython.run_command` auditing event, and so code passed on the command line using `-c` will not be scanned (though any calls to `compile()` or `exec()` _will_ be scanned). A more robust implementation would also handle this event. 11 | -------------------------------------------------------------------------------- /WindowsAMSI/amsi_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test file containing a string that Windows Defender will identify as a virus. 3 | 4 | To verify your build, launch 'spython amsi_test.py'. 5 | """ 6 | 7 | def get_amsi_test_string(): 8 | import base64 9 | # The string is encrypted to avoid detection on disk. 10 | return bytes( 11 | x ^ 0x33 for x in 12 | base64.b64decode(b"FHJ+YHoTZ1ZARxNgUl5DX1YJEwRWBAFQAFBWHgsFAlEeBwAACh4LBAcDHgNSUAIHCwdQAgALBRQ=") 13 | ).decode() 14 | 15 | if __name__ == "__main__": 16 | print(eval(get_amsi_test_string())) 17 | -------------------------------------------------------------------------------- /WindowsAMSI/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | cl -nologo %_MD% -c spython.c -Foobj\spython.obj -Iobj -Zi -O2 %_PYTHONINCLUDE% 25 | @if errorlevel 1 exit /B %ERRORLEVEL% 26 | link /nologo obj\spython.obj amsi.lib /out:spython.exe /debug:FULL /pdb:spython.pdb /libpath:"%_PYTHONLIB%" 27 | @if errorlevel 1 exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /WindowsAMSI/spython.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | 3 | #include 4 | 5 | typedef struct _AmsiContext { 6 | HAMSICONTEXT hContext; 7 | HAMSISESSION hSession; 8 | } AmsiContext; 9 | 10 | int amsi_compile_hook(PyObject *args, AmsiContext* context); 11 | int amsi_scan_bytes(PyObject *buffer, PyObject *filename, AmsiContext *context); 12 | 13 | /* Our audit hook callback */ 14 | static int 15 | amsi_hook(const char *event, PyObject *args, void *userData) 16 | { 17 | AmsiContext * const context = (AmsiContext*)userData; 18 | 19 | /* We only handle compile() events, but recommend also 20 | * handling 'cpython.run_command' (for '-c' code execution) */ 21 | if (strcmp(event, "compile") == 0) { 22 | return amsi_compile_hook(args, context); 23 | } 24 | 25 | return 0; 26 | } 27 | 28 | /* AMSI handling for the 'compile' event */ 29 | static int 30 | amsi_compile_hook(PyObject *args, AmsiContext* context) 31 | { 32 | PyObject *code, *filename; 33 | if (!PyArg_ParseTuple(args, "OO", &code, &filename)) { 34 | return -1; 35 | } 36 | 37 | if (code == Py_None) { 38 | /* code is passed as None when compiling directly from a file or stdin. 39 | * We have to ignore it or Python will not start, but the disk access 40 | * should trigger an antimalware scan of a file, and interactive 41 | * execution can be prevented in other ways. */ 42 | return 0; 43 | } 44 | 45 | if (PyBytes_Check(code)) { 46 | /* code is already bytes - fast path */ 47 | return amsi_scan_bytes(code, filename, context); 48 | } else if (!PyUnicode_Check(code)) { 49 | /* Only AST nodes (from ast.parse) should make it here */ 50 | PyErr_Format(PyExc_TypeError, "cannot compile '%.200s' objects", 51 | Py_TYPE(code)->tp_name); 52 | return -1; 53 | } 54 | 55 | PyObject *buffer = PyUnicode_AsUTF8String(code); 56 | if (!buffer) { 57 | return -1; 58 | } 59 | 60 | int result = amsi_scan_bytes(buffer, filename, context); 61 | Py_DECREF(buffer); 62 | return result; 63 | } 64 | 65 | static int 66 | amsi_scan_bytes(PyObject *buffer, PyObject *filename, AmsiContext *context) 67 | { 68 | char *b; 69 | Py_ssize_t cb; 70 | if (PyBytes_AsStringAndSize(buffer, &b, &cb) < 0) { 71 | return -1; 72 | } 73 | if (cb == 0) { 74 | /* Zero-length buffers do not require scanning */ 75 | return 0; 76 | } 77 | if (cb < 0 || cb > ULONG_MAX) { 78 | PyErr_SetString(PyExc_ValueError, "cannot compile this much code"); 79 | return -1; 80 | } 81 | 82 | /* Use the filename as AMSI's content reference */ 83 | const wchar_t *contentName = NULL; 84 | PyObject *filenameRepr = NULL; 85 | if (PyUnicode_Check(filename)) { 86 | contentName = PyUnicode_AsWideCharString(filename, NULL); 87 | } else if (filename != Py_None) { 88 | filenameRepr = PyObject_Repr(filename); 89 | if (!filenameRepr) { 90 | return -1; 91 | } 92 | contentName = PyUnicode_AsWideCharString(filenameRepr, NULL); 93 | } 94 | 95 | if (!contentName && PyErr_Occurred()) { 96 | Py_XDECREF(filenameRepr); 97 | return -1; 98 | } 99 | 100 | AMSI_RESULT result; 101 | HRESULT hr = AmsiScanBuffer( 102 | context->hContext, 103 | (LPVOID)b, 104 | (ULONG)cb, 105 | contentName, 106 | context->hSession, 107 | &result 108 | ); 109 | 110 | Py_XDECREF(filenameRepr); 111 | 112 | if (FAILED(hr)) { 113 | PyErr_SetFromWindowsErr(hr); 114 | return -1; 115 | } 116 | 117 | if (AmsiResultIsMalware(result)) { 118 | PyErr_SetString( 119 | PyExc_OSError, 120 | "Compilation blocked by antimalware scan. " 121 | "Check your antimalware protection history for details." 122 | ); 123 | return -1; 124 | } 125 | 126 | return 0; 127 | } 128 | 129 | 130 | int 131 | wmain(int argc, wchar_t **argv) 132 | { 133 | /* Initialize AMSI at startup. We use a single session to correlate 134 | * all events in this process, and argv[0] to correlate across sessions. */ 135 | HRESULT hr; 136 | AmsiContext *context = (AmsiContext*)PyMem_RawMalloc(sizeof(AmsiContext)); 137 | if (FAILED(hr = AmsiInitialize(argv[0], &context->hContext))) { 138 | fprintf(stderr, "AMSI initialization failed (0x%08X)\n", hr); 139 | return hr; 140 | } 141 | if (FAILED(hr = AmsiOpenSession(context->hContext, &context->hSession))) { 142 | fprintf(stderr, "AMSI session creation failed (0x%08X)\n", hr); 143 | return hr; 144 | } 145 | 146 | PySys_AddAuditHook(amsi_hook, (void*)context); 147 | int result = Py_Main(argc, argv); 148 | 149 | AmsiCloseSession(context->hContext, context->hSession); 150 | AmsiUninitialize(context->hContext); 151 | PyMem_RawFree((void*)context); 152 | 153 | return result; 154 | } 155 | -------------------------------------------------------------------------------- /WindowsCatFile/README.md: -------------------------------------------------------------------------------- 1 | WindowsCatFile 2 | ============== 3 | 4 | This sample uses the python_lib.cat file to verify all imports. 5 | 6 | To build, open the Visual Studio Developer Command prompt of your choice (making sure you have a suitable Python install or build). Run `set PYTHONDIR=`, then run `make.cmd` to build. 7 | 8 | Once built, the new `spython.exe` will need to be moved into a regular install directory. It expects a `DLLs\python_lib.cat` file to exist and be signed. A more complete example may include a search for matching catalog files and/or restricting the valid certificates to a known set. 9 | -------------------------------------------------------------------------------- /WindowsCatFile/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | cl -nologo %_MD% -c spython.c -Foobj\spython.obj -Iobj -Zi -O2 %_PYTHONINCLUDE% 25 | @if errorlevel 1 exit /B %ERRORLEVEL% 26 | link /nologo obj\spython.obj advapi32.lib wintrust.lib /out:spython.exe /debug:FULL /pdb:spython.pdb /libpath:"%_PYTHONLIB%" 27 | @if errorlevel 1 exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /WindowsCatFile/spython.c: -------------------------------------------------------------------------------- 1 | /* Minimal main program -- everything is loaded from the library */ 2 | 3 | #include "Python.h" 4 | 5 | #define WIN32_LEAN_AND_MEAN 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | static wchar_t wszCatalog[512]; 13 | 14 | static int 15 | default_spython_hook(const char *event, PyObject *args, void *userData) 16 | { 17 | if (strcmp(event, "cpython.run_command") == 0) { 18 | PyErr_SetString(PyExc_RuntimeError, "use of '-c' is disabled"); 19 | return -1; 20 | } 21 | 22 | return 0; 23 | } 24 | 25 | static int 26 | verify_trust(HANDLE hFile) 27 | { 28 | static const GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; 29 | BYTE hash[256]; 30 | wchar_t memberTag[256]; 31 | 32 | WINTRUST_CATALOG_INFO wci = { 33 | .cbStruct = sizeof(WINTRUST_CATALOG_INFO), 34 | .hMemberFile = hFile, 35 | .pbCalculatedFileHash = hash, 36 | .cbCalculatedFileHash = sizeof(hash), 37 | .pcwszCatalogFilePath = wszCatalog, 38 | .pcwszMemberTag = memberTag, 39 | }; 40 | WINTRUST_DATA wd = { 41 | .cbStruct = sizeof(WINTRUST_DATA), 42 | .dwUIChoice = WTD_UI_NONE, 43 | .fdwRevocationChecks = WTD_REVOKE_WHOLECHAIN, 44 | .dwUnionChoice = WTD_CHOICE_CATALOG, 45 | .pCatalog = &wci 46 | }; 47 | 48 | if (!CryptCATAdminCalcHashFromFileHandle(hFile, &wci.cbCalculatedFileHash, hash, 0)) { 49 | return -1; 50 | } 51 | 52 | for (DWORD i = 0; i < wci.cbCalculatedFileHash; ++i) { 53 | swprintf(&memberTag[i*2], 3, L"%02X", hash[i]); 54 | } 55 | 56 | HRESULT hr = WinVerifyTrust(NULL, (LPGUID)&action, &wd); 57 | if (FAILED(hr)) { 58 | PyErr_SetExcFromWindowsErr(PyExc_OSError, GetLastError()); 59 | return -1; 60 | } 61 | return 0; 62 | } 63 | 64 | static PyObject * 65 | spython_open_stream(HANDLE hFile) 66 | { 67 | PyObject *io = NULL, *buffer = NULL, *stream = NULL; 68 | DWORD cbFile; 69 | LARGE_INTEGER filesize; 70 | 71 | if (!hFile || hFile == INVALID_HANDLE_VALUE) { 72 | return NULL; 73 | } 74 | 75 | if (!GetFileSizeEx(hFile, &filesize)) { 76 | return NULL; 77 | } 78 | 79 | if (filesize.QuadPart > MAXDWORD) { 80 | PyErr_SetString(PyExc_OSError, "file too large"); 81 | return NULL; 82 | } 83 | 84 | if (filesize.QuadPart == 0) { 85 | /* Zero-length files have no signatures, so do not verify */ 86 | if (!(buffer = PyBytes_FromStringAndSize(NULL, 0))) { 87 | return NULL; 88 | } 89 | } else { 90 | if (verify_trust(hFile) < 0) { 91 | return NULL; 92 | } 93 | 94 | cbFile = (DWORD)filesize.QuadPart; 95 | if (!(buffer = PyBytes_FromStringAndSize(NULL, cbFile))) { 96 | return NULL; 97 | } 98 | 99 | if (!ReadFile(hFile, (LPVOID)PyBytes_AS_STRING(buffer), cbFile, &cbFile, NULL)) { 100 | Py_DECREF(buffer); 101 | return NULL; 102 | } 103 | } 104 | 105 | if (!(io = PyImport_ImportModule("_io"))) { 106 | Py_DECREF(buffer); 107 | return NULL; 108 | } 109 | 110 | stream = PyObject_CallMethod(io, "BytesIO", "N", buffer); 111 | Py_DECREF(io); 112 | 113 | return stream; 114 | } 115 | 116 | static PyObject * 117 | spython_open_code(PyObject *path, void *userData) 118 | { 119 | static PyObject *io = NULL; 120 | const wchar_t *filename; 121 | Py_ssize_t filename_len; 122 | HANDLE hFile; 123 | PyObject *stream = NULL; 124 | DWORD err; 125 | 126 | if (PySys_Audit("spython.open_code", "O", path) < 0) { 127 | return NULL; 128 | } 129 | 130 | if (!PyUnicode_Check(path)) { 131 | PyErr_SetString(PyExc_TypeError, "invalid type passed to open_code"); 132 | return NULL; 133 | } 134 | 135 | if (!(filename = PyUnicode_AsWideCharString(path, &filename_len))) { 136 | return NULL; 137 | } 138 | 139 | hFile = CreateFileW(filename, GENERIC_READ, FILE_SHARE_READ, NULL, 140 | OPEN_EXISTING, 0, NULL); 141 | 142 | PyMem_Free((void*)filename); 143 | 144 | stream = spython_open_stream(hFile); 145 | err = GetLastError(); 146 | 147 | CloseHandle(hFile); 148 | 149 | if (!stream && !PyErr_Occurred()) { 150 | PyErr_SetExcFromWindowsErrWithFilenameObject(PyExc_OSError, err, path); 151 | } 152 | 153 | return stream; 154 | } 155 | 156 | int 157 | wmain(int argc, wchar_t **argv) 158 | { 159 | GetModuleFileNameW(NULL, wszCatalog, 512); 160 | wcscpy(wcsrchr(wszCatalog, L'\\') + 1, L"DLLs\\python_lib.cat"); 161 | 162 | PySys_AddAuditHook(default_spython_hook, NULL); 163 | PyFile_SetOpenCodeHook(spython_open_code, NULL); 164 | int result = Py_Main(argc, argv); 165 | 166 | return result; 167 | } 168 | -------------------------------------------------------------------------------- /WindowsDefenderAppControl/README.md: -------------------------------------------------------------------------------- 1 | WindowsDefenderAppControl 2 | ========================= 3 | 4 | This sample uses the [code integrity protection](https://docs.microsoft.com/windows/security/threat-protection/windows-defender-application-control/windows-defender-application-control) available in Windows 10 or Windows Server 2016 (with all updates) to protect against unsigned files being executed. 5 | 6 | To build, open the Visual Studio Developer Command prompt of your choice (making sure you have a suitable Python install or build). Run `set PYTHONDIR=`, then run `make.cmd` to build. 7 | 8 | Once built, the new `spython.exe` will need to be moved into a regular install directory. Additionally, you will need to enable code integrity protection (**warning**: this may make your machine unusable!): 9 | 10 | ``` 11 | PS> .\update.ps1 12 | Removing old policy... 13 | Merging new policy... 14 | Updating options... 15 | Updating current policy... 16 | Getting recent Event Log entries... 17 | Verifying enforcement status... 18 | * enforced 19 | ``` 20 | 21 | To delete the policy, use the `reset.ps1` script: 22 | 23 | ``` 24 | PS> .\reset.ps1 25 | Policy successfully deleted. You may need to restart your PC. 26 | ``` 27 | 28 | Once the policy is enabled, try running `python.exe` and `spython.exe`. You will find that `python.exe` is blocked, because of a specific rule in the `policy.xml` file. And , which will fail because it cannot verify any of the standard library. 29 | 30 | ``` 31 | PS> .\python.exe 32 | Program 'python.exe' failed to run: Your organization used Device Guard to block this app. Contact your support person 33 | for more infoAt line:1 char:1 34 | 35 | PS> .\spython.exe 36 | Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding 37 | Traceback (most recent call last): 38 | File "", line 991, in _find_and_load 39 | File "", line 975, in _find_and_load_unlocked 40 | File "", line 671, in _load_unlocked 41 | File "", line 776, in exec_module 42 | File "", line 912, in get_code 43 | File "", line 969, in get_data 44 | OSError: loading 'C:\Users\Administrator\Desktop\WDAC2\tools\lib\encodings\__init__.py' is blocked by policy 45 | ``` 46 | 47 | To enable `spython.exe` to operate, you will need to install the catalog file that is included with Python. This can be deleted later simply by removing the file. 48 | 49 | ``` 50 | PS> copy .\DLLs\python.cat "C:\Windows\System32\CatRoot\{127D0A1D-4EF2-11D1-8608-00C04FC295EE}\python.cat" 51 | PS> .\spython.exe 52 | Python 3.8.0b2 (tags/v3.8.0b2:21dd01d, Jul 4 2019, 16:00:09) [MSC v.1916 64 bit (AMD64)] on win32 53 | Type "help", "copyright", "credits" or "license" for more information. 54 | >>> 55 | ``` 56 | 57 | (If you are using your own build, you will need to sign this yourself, update the contents of `policy.xml` with your certificate, and run `update.ps1` again. Or just use a standard installation that's already signed.) 58 | 59 | Set the `SPYTHON_AUDIT` environment variable to see more detailed output for each file as it is validated. 60 | 61 | The default `policy.xml` file includes additional rules to prevent the `ssl`, `sqlite3` and `ctypes` modules being loaded. Try importing them to see the result. 62 | 63 | Via the `cpython.run_command` auditing event, this entry point will also block (or warn about) use of the `-c` option when integrity enforcement is enabled. To completely disable enforcement, you will likely need to run the `reset.ps1` script and reboot your PC. 64 | -------------------------------------------------------------------------------- /WindowsDefenderAppControl/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | rc -nologo -foobj\spython.res spython.rc 25 | cl -nologo %_MD% -c spython.c -Foobj\spython.obj -Iobj -Zi -O2 %_PYTHONINCLUDE% 26 | @if errorlevel 1 exit /B %ERRORLEVEL% 27 | link /nologo obj\spython.obj obj\spython.res advapi32.lib wintrust.lib /out:spython.exe /debug:FULL /pdb:spython.pdb /libpath:"%_PYTHONLIB%" 28 | @if errorlevel 1 exit /B %ERRORLEVEL% 29 | -------------------------------------------------------------------------------- /WindowsDefenderAppControl/policy.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 10.0.0.0 4 | {C4B93A16-4AE5-407C-ABAE-06FAF10C92C1} 5 | {C4B93A16-4AE5-407C-ABAE-06FAF10C92C1} 6 | {2E07F7E4-194C-4D20-B7C9-6F44A6C5A234} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 0 52 | 53 | -------------------------------------------------------------------------------- /WindowsDefenderAppControl/reset.ps1: -------------------------------------------------------------------------------- 1 | del -EA 0 "${env:SystemRoot}\System32\CodeIntegrity\SiPolicy.bin" 2 | $r = Invoke-CimMethod -Namespace root/microsoft/Windows/CI -ClassName PS_UpdateAndCompareCIPolicy -MethodName Delete 3 | 4 | if ($r.ReturnValue) { 5 | "[ERROR] Returned from policy update. You may need to reboot your PC" 6 | $r 7 | } else { 8 | "Policy successfully deleted. You may need to restart your PC" 9 | } 10 | -------------------------------------------------------------------------------- /WindowsDefenderAppControl/spython.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #include 5 | #include 6 | 7 | /* We need to GetProcAddress this method, and the header does not provide 8 | * a definition for it, so here it is. */ 9 | typedef HRESULT (WINAPI *PWLDP_GETLOCKDOWNPOLICY_API)( 10 | _In_opt_ PWLDP_HOST_INFORMATION hostInformation, 11 | _Out_ PDWORD lockdownState, 12 | _In_ DWORD lockdownFlags 13 | ); 14 | 15 | static int 16 | IsVerbose(void) 17 | { 18 | static int result = -1; 19 | if (result < 0) { 20 | const char *e = getenv("SPYTHON_AUDIT"); 21 | result = (e && *e) ? 1 : 0; 22 | } 23 | return result; 24 | } 25 | 26 | /* Returns the current lockdown policy for the specified file. */ 27 | static HRESULT 28 | GetLockdownPolicy(LPCWSTR path, HANDLE hFile, LPDWORD lockdownState) 29 | { 30 | static HMODULE wldp = NULL; 31 | static PWLDP_GETLOCKDOWNPOLICY_API getLockdownPolicy = NULL; 32 | 33 | if (!wldp) { 34 | Py_BEGIN_ALLOW_THREADS 35 | wldp = LoadLibraryW(WLDP_DLL); 36 | getLockdownPolicy = wldp ? (PWLDP_GETLOCKDOWNPOLICY_API)GetProcAddress(wldp, WLDP_GETLOCKDOWNPOLICY_FN) : NULL; 37 | Py_END_ALLOW_THREADS 38 | if (!getLockdownPolicy) { 39 | return E_FAIL; 40 | } 41 | } 42 | 43 | WLDP_HOST_INFORMATION hostInfo = { 44 | .dwRevision = WLDP_HOST_INFORMATION_REVISION, 45 | /* Lie and claim to be PowerShell. */ 46 | .dwHostId = WLDP_HOST_ID_POWERSHELL, 47 | .szSource = path, 48 | .hSource = hFile 49 | }; 50 | 51 | HRESULT hr; 52 | Py_BEGIN_ALLOW_THREADS 53 | hr = getLockdownPolicy(&hostInfo, lockdownState, 0); 54 | 55 | if (IsVerbose()) { 56 | printf_s("error=0x%08X, state=0x%08X, path=%ls\n", hr, *lockdownState, path ? path : L""); 57 | } 58 | 59 | Py_END_ALLOW_THREADS 60 | 61 | return hr; 62 | } 63 | 64 | 65 | static int 66 | default_spython_hook(const char *event, PyObject *args, void *userData) 67 | { 68 | /* cpython.run_command(command) */ 69 | if (strcmp(event, "cpython.run_command") == 0) { 70 | DWORD lockdownPolicy = 0; 71 | HRESULT hr = GetLockdownPolicy(NULL, NULL, &lockdownPolicy); 72 | if (FAILED(hr)) { 73 | PyErr_SetExcFromWindowsErr(PyExc_OSError, (int)hr); 74 | return -1; 75 | } 76 | 77 | if (WLDP_LOCKDOWN_IS_ENFORCE(lockdownPolicy)) { 78 | PyErr_SetString(PyExc_RuntimeError, "use of '-c' is disabled"); 79 | return -1; 80 | } 81 | 82 | if (!WLDP_LOCKDOWN_IS_OFF(lockdownPolicy)) { 83 | fprintf_s(stderr, "Allowing use of '-c' in audit-only mode\n"); 84 | } 85 | return 0; 86 | } 87 | 88 | /* spython.code_integrity_failure(path, audit_mode) */ 89 | if (strcmp(event, "spython.code_integrity_failure") == 0 && IsVerbose()) { 90 | PyObject *opath = PyTuple_GetItem(args, 0); 91 | if (!opath) { 92 | return -1; 93 | } 94 | 95 | PyObject *is_audit = PyTuple_GetItem(args, 1); 96 | if (!is_audit) { 97 | return -1; 98 | } 99 | 100 | const wchar_t *path = PyUnicode_AsWideCharString(opath, NULL); 101 | if (!path) { 102 | return -1; 103 | } 104 | 105 | fprintf_s(stderr, "Failed to verify integrity%s: %ls\n", 106 | is_audit && PyObject_IsTrue(is_audit) ? " (audit)" : "", 107 | path); 108 | 109 | PyMem_Free((void *)path); 110 | return 0; 111 | } 112 | 113 | return 0; 114 | } 115 | 116 | static int 117 | verify_trust(LPCWSTR path, HANDLE hFile) 118 | { 119 | DWORD lockdownState = 0; 120 | HRESULT hr = GetLockdownPolicy(path, hFile, &lockdownState); 121 | 122 | if (FAILED(hr)) { 123 | PyErr_SetExcFromWindowsErr(PyExc_OSError, (int)hr); 124 | return -1; 125 | } 126 | 127 | if (WLDP_LOCKDOWN_IS_ENFORCE(lockdownState)) { 128 | if (PySys_Audit("spython.code_integrity_failure", "ui", path, 0) < 0) { 129 | return -1; 130 | } 131 | PyObject *opath = PyUnicode_FromWideChar(path, -1); 132 | if (opath) { 133 | PyErr_Format(PyExc_OSError, "loading '%.300S' is blocked by policy", opath); 134 | Py_DECREF(opath); 135 | } 136 | return -1; 137 | } 138 | 139 | if (WLDP_LOCKDOWN_IS_AUDIT(lockdownState)) { 140 | if (PySys_Audit("spython.code_integrity_failure", "ui", path, 1) < 0) { 141 | return -1; 142 | } 143 | } 144 | 145 | return 0; 146 | } 147 | 148 | static PyObject * 149 | spython_open_stream(LPCWSTR filename, HANDLE hFile) 150 | { 151 | PyObject *io = NULL, *buffer = NULL, *stream = NULL; 152 | DWORD cbFile; 153 | LARGE_INTEGER filesize; 154 | 155 | if (!hFile || hFile == INVALID_HANDLE_VALUE) { 156 | return NULL; 157 | } 158 | 159 | if (!GetFileSizeEx(hFile, &filesize)) { 160 | return NULL; 161 | } 162 | 163 | if (filesize.QuadPart > MAXDWORD) { 164 | PyErr_SetString(PyExc_OSError, "file too large"); 165 | return NULL; 166 | } 167 | 168 | if (filesize.QuadPart == 0) { 169 | /* Zero-length files have no signatures, so do not verify */ 170 | if (!(buffer = PyBytes_FromStringAndSize(NULL, 0))) { 171 | return NULL; 172 | } 173 | } else { 174 | if (verify_trust(filename, hFile) < 0) { 175 | return NULL; 176 | } 177 | 178 | cbFile = (DWORD)filesize.QuadPart; 179 | if (!(buffer = PyBytes_FromStringAndSize(NULL, cbFile))) { 180 | return NULL; 181 | } 182 | 183 | if (!ReadFile(hFile, (LPVOID)PyBytes_AS_STRING(buffer), cbFile, &cbFile, NULL)) { 184 | Py_DECREF(buffer); 185 | return NULL; 186 | } 187 | } 188 | 189 | if (!(io = PyImport_ImportModule("_io"))) { 190 | Py_DECREF(buffer); 191 | return NULL; 192 | } 193 | 194 | stream = PyObject_CallMethod(io, "BytesIO", "N", buffer); 195 | Py_DECREF(io); 196 | 197 | return stream; 198 | } 199 | 200 | static PyObject * 201 | spython_open_code(PyObject *path, void *userData) 202 | { 203 | static PyObject *io = NULL; 204 | const wchar_t *filename; 205 | HANDLE hFile; 206 | PyObject *stream = NULL; 207 | DWORD err; 208 | 209 | if (PySys_Audit("spython.open_code", "O", path) < 0) { 210 | return NULL; 211 | } 212 | 213 | if (!PyUnicode_Check(path)) { 214 | PyErr_SetString(PyExc_TypeError, "invalid type passed to open_code"); 215 | return NULL; 216 | } 217 | 218 | if (!(filename = PyUnicode_AsWideCharString(path, NULL))) { 219 | return NULL; 220 | } 221 | 222 | hFile = CreateFileW(filename, GENERIC_READ, FILE_SHARE_READ, NULL, 223 | OPEN_EXISTING, 0, NULL); 224 | 225 | stream = spython_open_stream(filename, hFile); 226 | err = GetLastError(); 227 | 228 | PyMem_Free((void*)filename); 229 | CloseHandle(hFile); 230 | 231 | if (!stream && !PyErr_Occurred()) { 232 | PyErr_SetExcFromWindowsErrWithFilenameObject(PyExc_OSError, err, path); 233 | } 234 | 235 | return stream; 236 | } 237 | 238 | int 239 | wmain(int argc, wchar_t **argv) 240 | { 241 | PySys_AddAuditHook(default_spython_hook, NULL); 242 | PyFile_SetOpenCodeHook(spython_open_code, NULL); 243 | int result = Py_Main(argc, argv); 244 | 245 | return result; 246 | } 247 | -------------------------------------------------------------------------------- /WindowsDefenderAppControl/update.ps1: -------------------------------------------------------------------------------- 1 | $start = (Get-Date).AddSeconds(-2) 2 | 3 | "Removing old policy..." 4 | $r = Invoke-CimMethod -Namespace root/microsoft/Windows/CI -ClassName PS_UpdateAndCompareCIPolicy -MethodName Delete 5 | 6 | "Enabling SPython in new policy..." 7 | $spython = Get-SystemDriver -ScanPath . -UserPEs -NoScript -NoShadowCopy -PathToCatroot . 8 | $rules = New-CIPolicyRule -DriverFiles ` 9 | ($spython | ?{ $_.FileName -ieq "spython.exe" }) ` 10 | -Level Hash ` 11 | -Fallback FileName 12 | 13 | "Merging new policy..." 14 | $m = Merge-CIPolicy -PolicyPaths ` 15 | "${env:SystemRoot}\schemas\CodeIntegrity\ExamplePolicies\AllowMicrosoft.xml", ` 16 | .\policy.xml ` 17 | -Rules $rules ` 18 | -OutputFilePath merged.xml 19 | if (-not $?) { 20 | exit 21 | } 22 | 23 | "Updating options..." 24 | 25 | # Set audit mode 26 | Set-RuleOption .\merged.xml -Option 3 27 | #Set-RuleOption .\merged.xml -Option 3 -Delete 28 | 29 | # Do not disable scripts or everything will be blocked 30 | Set-RuleOption .\merged.xml -Option 11 -Delete 31 | 32 | # Disable inherited policy 33 | Set-RuleOption .\merged.xml -Option 5 -Delete 34 | 35 | # Enable rebootless updates 36 | Set-RuleOption .\merged.xml -Option 16 37 | 38 | Set-CIPolicyIdInfo .\merged.xml -PolicyName "SPython Code Integrity Demo" -PolicyId (Get-Date) 39 | 40 | # Updating policy 41 | "Updating current policy..." 42 | $p = ConvertFrom-CIPolicy merged.xml "${env:SystemRoot}\System32\CodeIntegrity\SiPolicy.bin" 43 | $r = Invoke-CimMethod -Namespace root/microsoft/Windows/CI -ClassName PS_UpdateAndCompareCIPolicy -MethodName Update -Arguments @{ FilePath = $p } 44 | 45 | if ($r.ReturnValue) { 46 | "[ERROR] Returned from policy update. You may need to reboot your PC" 47 | $r 48 | exit 49 | } 50 | 51 | "Getting recent Event Log entries from Microsoft-Windows-CodeIntegrity/Operational..." 52 | Sleep -Seconds 1 53 | Get-WinEvent -LogName Microsoft-Windows-CodeIntegrity/Operational -Force -MaxEvents 50 ` 54 | | ?{ $_.TimeCreated -ge $start } ` 55 | | %{ "* $($_.Message)" } 56 | 57 | "Verifying enforcement status..." 58 | $dg = (Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard) 59 | if ($dg.CodeIntegrityPolicyEnforcementStatus -eq 0) { 60 | "* not enforced" 61 | } elseif ($dg.CodeIntegrityPolicyEnforcementStatus -eq 1) { 62 | "* auditing only" 63 | } elseif ($dg.CodeIntegrityPolicyEnforcementStatus -eq 2) { 64 | "* enforced" 65 | } 66 | -------------------------------------------------------------------------------- /WindowsEventLog/README.md: -------------------------------------------------------------------------------- 1 | WindowsEventLog 2 | =============== 3 | 4 | This sample writes a small selection of messages to the Windows Event Log. 5 | 6 | To build, open the Visual Studio Developer Command prompt of your choice (making sure you have a suitable Python install or build). Run `set PYTHONDIR=`, then run `make.cmd` to build. 7 | 8 | After building, you will need an elevated command prompt (not necessarily a Visual Studio prompt) in the same directory to run `enable.cmd`. This will add a node in Event Viewer called `Example-SPythonProvider` to contain all logged events. 9 | 10 | Once enabled, you can run `run.cmd` from the original prompt (this one also needs `PYTHONDIR` to be set), and do whatever you like. All imports and compiled code will be added to event viewer. 11 | 12 | When you are done, you can clear the log through Event Viewer and then run `disable.cmd` from the elevated command prompt to remove the entry. Simply running `disable.cmd` does not clear the log, and if you enable it again later your previous entries will still be there. 13 | -------------------------------------------------------------------------------- /WindowsEventLog/disable.cmd: -------------------------------------------------------------------------------- 1 | wevtutil um events.man 2 | -------------------------------------------------------------------------------- /WindowsEventLog/enable.cmd: -------------------------------------------------------------------------------- 1 | wevtutil im events.man /rf:"%CD%\spython.exe" /mf:"%CD%\spython.exe" 2 | -------------------------------------------------------------------------------- /WindowsEventLog/events.man: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 14 | 15 | 19 | 20 | 21 | 25 | 26 | 30 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 50 | 54 | 55 | 56 | 57 | 65 | 73 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /WindowsEventLog/make.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before building && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\PC" -I"%PYTHONDIR%\include" 6 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 7 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\win32 8 | ) else ( 9 | set _PYTHONLIB=%PYTHONDIR%\PCbuild\amd64 10 | ) 11 | ) else ( 12 | set _PYTHONINCLUDE=-I"%PYTHONDIR%\include" 13 | set _PYTHONLIB=%PYTHONDIR%\libs 14 | ) 15 | 16 | if exist "%_PYTHONLIB%\python_d.exe" ( 17 | set _MD=-MDd 18 | ) else ( 19 | set _MD=-MD 20 | ) 21 | 22 | @echo on 23 | @if not exist obj mkdir obj 24 | mc -um -h obj -r obj events.man 25 | @if errorlevel 1 exit /B %ERRORLEVEL% 26 | cl -nologo %_MD% -c spython.cpp -Foobj\spython.obj -Iobj %_PYTHONINCLUDE% 27 | @if errorlevel 1 exit /B %ERRORLEVEL% 28 | rc /nologo /fo obj\events.res obj\events.rc 29 | @if errorlevel 1 exit /B %ERRORLEVEL% 30 | link /nologo obj\spython.obj obj\events.res advapi32.lib /out:spython.exe /libpath:"%_PYTHONLIB%" 31 | @if errorlevel 1 exit /B %ERRORLEVEL% 32 | -------------------------------------------------------------------------------- /WindowsEventLog/run.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | if not defined PYTHONDIR echo PYTHONDIR must be set before running && exit /B 1 4 | if exist "%PYTHONDIR%\PCbuild" ( 5 | if "%VSCMD_ARG_TGT_ARCH%" == "x86" ( 6 | set _PYTHONDIR=%PYTHONDIR%\PCbuild\win32 7 | ) else ( 8 | set _PYTHONDIR=%PYTHONDIR%\PCbuild\amd64 9 | ) 10 | ) else ( 11 | set _PYTHONDIR=%PYTHONDIR% 12 | ) 13 | set PATH=%PATH%;%_PYTHONDIR% 14 | 15 | spython.exe %* 16 | -------------------------------------------------------------------------------- /WindowsEventLog/spython.cpp: -------------------------------------------------------------------------------- 1 | /* Minimal main program -- everything is loaded from the library */ 2 | 3 | #include "Python.h" 4 | #include 5 | #include "events.h" 6 | 7 | static int 8 | hook_compile(const char *event, PyObject *args) 9 | { 10 | PyObject *code, *filename; 11 | const char *u8code = NULL, *u8filename = NULL; 12 | 13 | /* Avoid doing extra work if the event won't be recorded */ 14 | if (!EventEnabledIMPORT_COMPILE()) { 15 | return 0; 16 | } 17 | 18 | if (!PyArg_ParseTuple(args, "OO", &code, &filename)) { 19 | return -1; 20 | } 21 | 22 | Py_INCREF(code); 23 | if (!PyObject_IsTrue(code)) { 24 | u8code = ""; 25 | } else if (PyUnicode_Check(code)) { 26 | u8code = PyUnicode_AsUTF8(code); 27 | } else if (PyBytes_Check(code)) { 28 | u8code = PyBytes_AS_STRING(code); 29 | } else if (Py_IsInitialized()) { 30 | Py_SETREF(code, PyObject_Repr(code)); 31 | if (!code) { 32 | return -1; 33 | } 34 | u8code = PyUnicode_AsUTF8(code); 35 | } else { 36 | u8code = ""; 37 | } 38 | 39 | Py_INCREF(filename); 40 | if (!PyObject_IsTrue(filename)) { 41 | u8filename = ""; 42 | } else if (PyUnicode_Check(filename)) { 43 | u8filename = PyUnicode_AsUTF8(filename); 44 | } else if (PyBytes_Check(filename)) { 45 | u8filename = PyBytes_AS_STRING(filename); 46 | } else if (Py_IsInitialized()) { 47 | Py_SETREF(filename, PyObject_Repr(filename)); 48 | if (!filename) { 49 | Py_DECREF(code); 50 | return -1; 51 | } 52 | u8filename = PyUnicode_AsUTF8(code); 53 | } else { 54 | u8filename = ""; 55 | } 56 | 57 | if (!u8code) { 58 | Py_DECREF(code); 59 | Py_DECREF(filename); 60 | return -1; 61 | } 62 | if (!u8filename) { 63 | Py_DECREF(code); 64 | Py_DECREF(filename); 65 | return -1; 66 | } 67 | 68 | EventWriteIMPORT_COMPILE(u8code, u8filename); 69 | 70 | Py_DECREF(code); 71 | Py_DECREF(filename); 72 | 73 | return 0; 74 | } 75 | 76 | static int 77 | hook_import(const char *event, PyObject *args) 78 | { 79 | PyObject *name, *_, *syspath; 80 | if (!PyArg_ParseTuple(args, "OOOOO", &name, &_, &syspath, &_, &_)) { 81 | return -1; 82 | } 83 | 84 | if (!PyUnicode_Check(name)) { 85 | PyErr_SetString(PyExc_TypeError, "require x"); 86 | return -1; 87 | } 88 | if (syspath != Py_None && !PyList_CheckExact(syspath)) { 89 | PyErr_Format(PyExc_TypeError, "cannot use %.200s for sys.path", Py_TYPE(syspath)->tp_name); 90 | return -1; 91 | } 92 | 93 | if (EventEnabledIMPORT_RESOLVE()) { 94 | const char *u8name, *u8path = "\0"; 95 | PyObject *paths = NULL; 96 | Py_ssize_t u8path_len = 1; 97 | 98 | u8name = PyUnicode_AsUTF8(name); 99 | if (!u8name) { 100 | return -1; 101 | } 102 | 103 | if (Py_IsInitialized() && syspath != Py_None) { 104 | PyObject *sep = PyUnicode_FromString(";"); 105 | if (!sep) { 106 | return -1; 107 | } 108 | paths = PyUnicode_Join(sep, syspath); 109 | Py_DECREF(sep); 110 | if (!paths) { 111 | return -1; 112 | } 113 | u8path = PyUnicode_AsUTF8(paths); 114 | if (!u8path) { 115 | Py_DECREF(paths); 116 | return -1; 117 | } 118 | } 119 | 120 | EventWriteIMPORT_RESOLVE(u8name, u8path); 121 | 122 | Py_XDECREF(paths); 123 | } 124 | 125 | return 0; 126 | } 127 | 128 | static int 129 | default_spython_hook(const char *event, PyObject *args, void *userData) 130 | { 131 | if (strcmp(event, "compile") == 0) { 132 | return hook_compile(event, args); 133 | } else if (strcmp(event, "import") == 0) { 134 | return hook_import(event, args); 135 | } 136 | 137 | return 0; 138 | } 139 | 140 | static PyObject * 141 | spython_open_code(PyObject *path, void *userData) 142 | { 143 | static PyObject *io = NULL; 144 | PyObject *stream = NULL, *buffer = NULL, *err = NULL; 145 | PyObject *exc_type, *exc_value, *exc_tb; 146 | 147 | /* path is always provided as PyUnicodeObject */ 148 | assert(PyUnicode_Check(path)); 149 | 150 | EventWriteIMPORT_OPEN(PyUnicode_AsUTF8(path)); 151 | 152 | if (!io) { 153 | io = PyImport_ImportModule("_io"); 154 | if (!io) { 155 | return NULL; 156 | } 157 | } 158 | 159 | stream = PyObject_CallMethod(io, "open", "Osisssi", path, "rb", 160 | -1, NULL, NULL, NULL, 1); 161 | if (!stream) { 162 | return NULL; 163 | } 164 | 165 | buffer = PyObject_CallMethod(stream, "read", "(i)", -1); 166 | 167 | /* Carefully preserve any exception while we close 168 | * the stream. 169 | */ 170 | PyErr_Fetch(&exc_type, &exc_value, &exc_tb); 171 | err = PyObject_CallMethod(stream, "close", NULL); 172 | Py_DECREF(stream); 173 | if (!buffer) { 174 | /* An error occurred reading, so raise that one */ 175 | PyErr_Restore(exc_type, exc_value, exc_tb); 176 | return NULL; 177 | } 178 | /* These should be clear, but xdecref just in case */ 179 | Py_XDECREF(exc_type); 180 | Py_XDECREF(exc_value); 181 | Py_XDECREF(exc_tb); 182 | if (!err) { 183 | return NULL; 184 | } 185 | 186 | /* Here is a good place to validate the contents of 187 | * buffer and raise an error if not permitted 188 | */ 189 | 190 | return PyObject_CallMethod(io, "BytesIO", "N", buffer); 191 | } 192 | 193 | int 194 | wmain(int argc, wchar_t **argv) 195 | { 196 | int res; 197 | 198 | EventRegisterExample_PythonProvider(); 199 | 200 | PySys_AddAuditHook(default_spython_hook, NULL); 201 | PyFile_SetOpenCodeHook(spython_open_code, NULL); 202 | res = Py_Main(argc, argv); 203 | 204 | EventUnregisterExample_PythonProvider(); 205 | return res; 206 | } 207 | -------------------------------------------------------------------------------- /execveat/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3-config --cflags) 4 | 5 | LDFLAGS+=$(shell python3-config --ldflags --embed) 6 | 7 | objects=spython.o 8 | 9 | all: spython 10 | 11 | %.o: %.c 12 | $(CC) -c $< $(CFLAGS) 13 | 14 | spython: spython.o 15 | $(CC) -o $@ $^ $(LDFLAGS) 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -rf *.o spython 20 | -------------------------------------------------------------------------------- /execveat/readme.md: -------------------------------------------------------------------------------- 1 | execveat 2 | ======== 3 | 4 | This sample uses the proposed `AT_CHECK` flag to `execveat` to verify 5 | that files are allowed to be executed before loading them. 6 | 7 | It additionally blocks `-c`, `-m` and interactive launches when the 8 | check+restrict mode is enabled. 9 | 10 | To build on Linux, run `make` with a copy of Python 3.8 or later 11 | installed. You will also currently need a patched kernel. 12 | -------------------------------------------------------------------------------- /execveat/spython.c: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | 3 | static PyObject* 4 | spython_open_stream(const char *filename, int fd) 5 | { 6 | PyObject *iomod = NULL; 7 | PyObject *fileio = NULL; 8 | 9 | char *args[] = { (char *)filename, NULL }; 10 | char *env[] = { NULL }; 11 | 12 | // We always call execveat(), but ignore failures when the 13 | // SECBIT_EXEC_RESTRICT_FILE bit is not set. This allows the 14 | // kernel to know that the script execution is about to occur, 15 | // allowing it to log or record it, rather than restricting it. 16 | if (execveat(fd, "", args, env, AT_EMPTY_PATH | AT_CHECK) < 0) { 17 | unsigned secbits = prctl(PR_GET_SECUREBITS); 18 | if (secbits & SECBIT_EXEC_RESTRICT_FILE) { 19 | PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); 20 | return NULL; 21 | } 22 | } 23 | 24 | if ((iomod = PyImport_ImportModule("_io")) == NULL) { 25 | return NULL; 26 | } 27 | 28 | fileio = PyObject_CallMethod(iomod, "FileIO", "isi", fd, "r", 1); 29 | 30 | Py_DECREF(iomod); 31 | return fileio; 32 | } 33 | 34 | 35 | static PyObject* 36 | spython_open_code(PyObject *path, void *userData) 37 | { 38 | PyObject *filename_obj = NULL; 39 | const char *filename; 40 | int fd = -1; 41 | PyObject *stream = NULL; 42 | 43 | if (!PyUnicode_FSConverter(path, &filename_obj)) { 44 | goto end; 45 | } 46 | filename = PyBytes_AS_STRING(filename_obj); 47 | 48 | fd = _Py_open(filename, O_RDONLY); 49 | if (fd > 0) { 50 | stream = spython_open_stream(filename, fd); 51 | if (stream) { 52 | /* stream will close the fd for us */ 53 | fd = -1; 54 | } 55 | } 56 | 57 | end: 58 | Py_XDECREF(filename_obj); 59 | if (fd >= 0) { 60 | close(fd); 61 | } 62 | return stream; 63 | } 64 | 65 | 66 | static int 67 | execveatHook(const char *event, PyObject *args, void *userData) 68 | { 69 | // Fast exit if we've already handled a run event 70 | int *inspected = (int *)userData; 71 | if (*inspected) { 72 | return 0; 73 | } 74 | 75 | 76 | // Open the launch file and validate it 77 | if (strcmp(event, "cpython.run_file") == 0) { 78 | *inspected = 1; 79 | // We always open the launch file and let the open_code handler 80 | // decide whether to abort or not. 81 | PyObject *pathname; 82 | if (PyArg_ParseTuple(args, "U", &pathname)) { 83 | PyObject *stream = spython_open_code(pathname, NULL); 84 | if (!stream) { 85 | return -1; 86 | } 87 | Py_DECREF(stream); 88 | } else { 89 | return -1; 90 | } 91 | return 0; 92 | } 93 | 94 | // Other run options depend on the global setting. 95 | if (strncmp(event, "cpython.run_", 12) == 0) { 96 | *inspected = 1; 97 | unsigned secbits = prctl(PR_GET_SECUREBITS); 98 | if (secbits & SECBIT_EXEC_DENY_INTERACTIVE) { 99 | PyErr_Format(PyExc_OSError, "'%.20s' is disabled by policy", &event[8]); 100 | return -1; 101 | } 102 | return 0; 103 | } 104 | 105 | return 0; 106 | } 107 | 108 | 109 | int 110 | main(int argc, char **argv) 111 | { 112 | unsigned secbits = prctl(PR_GET_SECUREBITS); 113 | int inspected = 0; 114 | if (secbits & (SECBIT_EXEC_RESTRICT_FILE | SECBIT_EXEC_DENY_INTERACTIVE)) { 115 | // Either bit set means we need to inspect launch events 116 | PySys_AddAuditHook(spython_launch_hook, &inspected); 117 | } 118 | 119 | // All open_code calls will be hooked regardless of initial settings, 120 | // as these settings may change during runtime. Additionally, a kernel 121 | // may be auditing execveat() calls without restricting them, and so 122 | // we have to make sure those calls occur. 123 | PyFile_SetOpenCodeHook(spython_open_code, NULL); 124 | return Py_BytesMain(argc, argv); 125 | } 126 | -------------------------------------------------------------------------------- /journald/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3-config --cflags) 4 | CFLAGS+=$(shell pkg-config --cflags) 5 | 6 | LDFLAGS+=$(shell python3-config --ldflags --embed) 7 | LDLAGS+=$(shell pkg-config --libs libsystemd-journal) 8 | # Fedora 9 | LDFLAGS+=$(shell pkg-config --libs libsystemd) 10 | 11 | objects=spython.o 12 | 13 | all: spython 14 | 15 | %.o: %.c 16 | $(CC) -c $< $(CFLAGS) 17 | 18 | spython: spython.o 19 | $(CC) -o $@ $^ $(LDFLAGS) 20 | 21 | .PHONY: clean 22 | clean: 23 | rm -rf *.o spython 24 | -------------------------------------------------------------------------------- /journald/readme.md: -------------------------------------------------------------------------------- 1 | linux_journald 2 | ============ 3 | 4 | This sample writes messages using [journald](https://www.freedesktop.org/software/systemd/man/latest/systemd-journald.service.html). 5 | 6 | To build on Linux, run `make` with a copy of Python 3.8.0rc1 or later 7 | installed. Ensure to have sd-journal.h (Fedora systemd-devel or Debian libsystemd-dev) 8 | and update Makefile with your python version if necessary. 9 | 10 | You may need to enable a journald service on your machine in order to 11 | receive the events. For example, if using `journald`, you might use 12 | these commands: 13 | 14 | ``` 15 | $ make 16 | $ sudo systemctl start systemd-journald 17 | $ ./spython test-file.py 18 | $ sudo systemctl stop systemd-journald 19 | $ sudo journalctl -l --no-pager -t spython -o json -n 3 20 | ``` 21 | 22 | References 23 | * [systemd for Developers III](https://0pointer.de/blog/projects/journal-submit.html) 24 | 25 | Known issues 26 | * "TypeError: signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object" when using spython (Fedora-41, python-3.13.2). Check your current terminal aka env TERM. Setting TERM=dumb is a valid workaround. 27 | * "Fatal Python error: none_dealloc: deallocating None: bug likely caused by a refcount error in a C extension\nPython runtime state: finalizing (tstate=0x000073676bf60018)\n\nCurrent thread 0x000073676bfbdd00 (most recent call first):\n ": Debian /usr/sbin/ifup, /usr/bin/apt-listchanges, some ansible modules under condition. Revert interpreter to normal python. Some of those are due to os.system() disabled in spython.c. 28 | -------------------------------------------------------------------------------- /journald/spython.c: -------------------------------------------------------------------------------- 1 | /* journald example using PySys_AddAuditHook 2 | */ 3 | #include 4 | 5 | /* logging */ 6 | #include 7 | 8 | int 9 | journaldHook(const char *event, PyObject *args, void *userData) 10 | { 11 | if (strcmp(event, "import") == 0) { 12 | PyObject *module, *filename, *sysPath, *sysMetaPath, *sysPathHooks; 13 | if (!PyArg_ParseTuple(args, "OOOOO", &module, &filename, 14 | &sysPath, &sysMetaPath, &sysPathHooks)) { 15 | return -1; 16 | } 17 | if (filename == Py_None) { 18 | sd_journal_send("MESSAGE=importing module %s", PyUnicode_AsUTF8(module), 19 | "MESSAGE_ID=697945225c004609a15d0d57fcda3ead", 20 | "PRIORITY=5", 21 | "USER=%s", getenv("USER"), 22 | "HOME=%s", getenv("HOME"), 23 | "PWD=%s", getenv("PWD"), 24 | "TERM=%s", getenv("TERM"), 25 | "PAGE_SIZE=%li", sysconf(_SC_PAGESIZE), 26 | "N_CPUS=%li", sysconf(_SC_NPROCESSORS_ONLN), 27 | NULL); 28 | } else { 29 | sd_journal_send("MESSAGE=importing module %s from %s", 30 | PyUnicode_AsUTF8(module), 31 | PyUnicode_AsUTF8(filename), 32 | "MESSAGE_ID=697945225c004609a15d0d57fcda3ead", 33 | "PRIORITY=5", 34 | "USER=%s", getenv("USER"), 35 | "HOME=%s", getenv("HOME"), 36 | "PWD=%s", getenv("PWD"), 37 | "TERM=%s", getenv("TERM"), 38 | "PAGE_SIZE=%li", sysconf(_SC_PAGESIZE), 39 | "N_CPUS=%li", sysconf(_SC_NPROCESSORS_ONLN), 40 | NULL); 41 | } 42 | Py_DECREF(filename); 43 | return 0; 44 | } 45 | 46 | if (strcmp(event, "os.system") == 0 || 47 | /* additional check for bug in 3.8.0rc1 */ 48 | strcmp(event, "system") == 0) { 49 | PyObject *command; 50 | if (!PyArg_ParseTuple(args, "O&", PyUnicode_FSConverter, &command)) { 51 | return -1; 52 | } 53 | sd_journal_send("MESSAGE=os.system('%s') attempted", 54 | PyBytes_AsString(command), 55 | "MESSAGE_ID=697945225c004609a15d0d57fcda3ead", 56 | "PRIORITY=5", 57 | "USER=%s", getenv("USER"), 58 | "HOME=%s", getenv("HOME"), 59 | "PWD=%s", getenv("PWD"), 60 | "TERM=%s", getenv("TERM"), 61 | "PAGE_SIZE=%li", sysconf(_SC_PAGESIZE), 62 | "N_CPUS=%li", sysconf(_SC_NPROCESSORS_ONLN), 63 | NULL); 64 | Py_DECREF(command); 65 | PyErr_SetString(PyExc_OSError, "os.system is disabled"); 66 | return -1; 67 | } 68 | 69 | return 0; 70 | } 71 | 72 | int 73 | main(int argc, char **argv) 74 | { 75 | PyStatus status; 76 | PyConfig config; 77 | 78 | /* configure journald */ 79 | openlog(NULL, LOG_PID, LOG_USER); 80 | 81 | PySys_AddAuditHook(journaldHook, NULL); 82 | 83 | /* initialize Python in isolated mode, but allow argv */ 84 | PyConfig_InitIsolatedConfig(&config); 85 | 86 | /* handle and parse argv */ 87 | config.parse_argv = 1; 88 | status = PyConfig_SetBytesArgv(&config, argc, argv); 89 | if (PyStatus_Exception(status)) { 90 | goto fail; 91 | } 92 | 93 | /* perform remaining initialization */ 94 | status = PyConfig_Read(&config); 95 | if (PyStatus_Exception(status)) { 96 | goto fail; 97 | } 98 | 99 | status = Py_InitializeFromConfig(&config); 100 | if (PyStatus_Exception(status)) { 101 | goto fail; 102 | } 103 | PyConfig_Clear(&config); 104 | 105 | return Py_RunMain(); 106 | 107 | fail: 108 | PyConfig_Clear(&config); 109 | if (PyStatus_IsExit(status)) { 110 | return status.exitcode; 111 | } 112 | /* Display the error message and exit the process with 113 | non-zero exit code */ 114 | Py_ExitStatusException(status); 115 | } 116 | -------------------------------------------------------------------------------- /journald_network/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3-config --cflags) 4 | CFLAGS+=$(shell pkg-config --cflags) 5 | 6 | LDFLAGS+=$(shell python3-config --ldflags --embed) 7 | LDLAGS+=$(shell pkg-config --libs libsystemd-journal) 8 | # Fedora 9 | LDFLAGS+=$(shell pkg-config --libs libsystemd) 10 | 11 | objects=spython.o 12 | 13 | all: spython 14 | 15 | %.o: %.c 16 | $(CC) -c $< $(CFLAGS) 17 | 18 | spython: spython.o 19 | $(CC) -o $@ $^ $(LDFLAGS) 20 | 21 | .PHONY: clean 22 | clean: 23 | rm -rf *.o spython 24 | -------------------------------------------------------------------------------- /journald_network/readme.md: -------------------------------------------------------------------------------- 1 | linux_journald 2 | ============ 3 | 4 | This sample writes messages using [journald](https://www.freedesktop.org/software/systemd/man/latest/systemd-journald.service.html). 5 | 6 | To build on Linux, run `make` with a copy of Python 3.8.0rc1 or later 7 | installed. Ensure to have sd-journal.h (Fedora systemd-devel or Debian libsystemd-dev) 8 | and update Makefile with your python version if necessary. 9 | 10 | You may need to enable a journald service on your machine in order to 11 | receive the events. For example, if using `rjournald`, you might use 12 | these commands: 13 | 14 | ``` 15 | $ make 16 | $ sudo systemctl start systemd-journald 17 | $ ./spython test-file.py 18 | $ sudo systemctl stop systemd-journald 19 | $ sudo journalctl -l --no-pager -t spython -o json -n 3 20 | ``` 21 | 22 | References 23 | * [systemd for Developers III](https://0pointer.de/blog/projects/journal-submit.html) 24 | 25 | Known issues 26 | * "TypeError: signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object" when using spython (Fedora-41, python-3.13.2). Check your current terminal aka env TERM. Setting TERM=dumb is a valid workaround. 27 | * "Fatal Python error: none_dealloc: deallocating None: bug likely caused by a refcount error in a C extension\nPython runtime state: finalizing (tstate=0x000073676bf60018)\n\nCurrent thread 0x000073676bfbdd00 (most recent call first):\n ": Debian /usr/sbin/ifup, /usr/bin/apt-listchanges, some ansible modules under condition. Revert interpreter to normal python. Some of those are due to os.system() disabled in spython.c. 28 | -------------------------------------------------------------------------------- /journald_network/spython.c: -------------------------------------------------------------------------------- 1 | /* journald example using PySys_AddAuditHook 2 | */ 3 | #include 4 | 5 | /* logging */ 6 | #include 7 | 8 | int 9 | journaldHook(const char *event, PyObject *args, void *userData) 10 | { 11 | if (strcmp(event, "import") == 0) { 12 | PyObject *module, *filename, *sysPath, *sysMetaPath, *sysPathHooks; 13 | if (!PyArg_ParseTuple(args, "OOOOO", &module, &filename, 14 | &sysPath, &sysMetaPath, &sysPathHooks)) { 15 | return -1; 16 | } 17 | if (filename == Py_None) { 18 | sd_journal_send("MESSAGE=importing module %s", PyUnicode_AsUTF8(module), 19 | "MESSAGE_ID=697945225c004609a15d0d57fcda3ead", 20 | "PRIORITY=5", 21 | "USER=%s", getenv("USER"), 22 | "HOME=%s", getenv("HOME"), 23 | "PWD=%s", getenv("PWD"), 24 | "TERM=%s", getenv("TERM"), 25 | "PAGE_SIZE=%li", sysconf(_SC_PAGESIZE), 26 | "N_CPUS=%li", sysconf(_SC_NPROCESSORS_ONLN), 27 | NULL); 28 | } else { 29 | sd_journal_send("MESSAGE=importing module %s from %s", 30 | PyUnicode_AsUTF8(module), 31 | PyUnicode_AsUTF8(filename), 32 | "MESSAGE_ID=697945225c004609a15d0d57fcda3ead", 33 | "PRIORITY=5", 34 | "USER=%s", getenv("USER"), 35 | "HOME=%s", getenv("HOME"), 36 | "PWD=%s", getenv("PWD"), 37 | "TERM=%s", getenv("TERM"), 38 | "PAGE_SIZE=%li", sysconf(_SC_PAGESIZE), 39 | "N_CPUS=%li", sysconf(_SC_NPROCESSORS_ONLN), 40 | NULL); 41 | } 42 | Py_DECREF(filename); 43 | return 0; 44 | } 45 | 46 | if (strcmp(event, "os.system") == 0 || 47 | /* additional check for bug in 3.8.0rc1 */ 48 | strcmp(event, "system") == 0) { 49 | PyObject *command; 50 | if (!PyArg_ParseTuple(args, "O&", PyUnicode_FSConverter, &command)) { 51 | return -1; 52 | } 53 | sd_journal_send("MESSAGE=os.system('%s') attempted", 54 | PyBytes_AsString(command), 55 | "MESSAGE_ID=697945225c004609a15d0d57fcda3ead", 56 | "PRIORITY=5", 57 | "USER=%s", getenv("USER"), 58 | "HOME=%s", getenv("HOME"), 59 | "PWD=%s", getenv("PWD"), 60 | "TERM=%s", getenv("TERM"), 61 | "PAGE_SIZE=%li", sysconf(_SC_PAGESIZE), 62 | "N_CPUS=%li", sysconf(_SC_NPROCESSORS_ONLN), 63 | NULL); 64 | Py_DECREF(command); 65 | PyErr_SetString(PyExc_OSError, "os.system is disabled"); 66 | return -1; 67 | } 68 | 69 | return 0; 70 | } 71 | 72 | static int 73 | network_hook(const char *event, PyObject *args, void *userData) 74 | { 75 | /* Only care about 'socket.' events */ 76 | if (strncmp(event, "socket.", 7) != 0) { 77 | return 0; 78 | } 79 | 80 | PyObject *msg = NULL; 81 | 82 | /* So yeah, I'm very lazily using PyTuple_GET_ITEM here. 83 | Not best practice! PyArg_ParseTuple is much better! */ 84 | if (strcmp(event, "socket.getaddrinfo") == 0) { 85 | msg = PyUnicode_FromFormat("network: Attempt to resolve %S:%S", 86 | PyTuple_GET_ITEM(args, 0), PyTuple_GET_ITEM(args, 1)); 87 | } else if (strcmp(event, "socket.connect") == 0) { 88 | PyObject *addro = PyTuple_GET_ITEM(args, 1); 89 | msg = PyUnicode_FromFormat("network: Attempt to connect %S:%S", 90 | PyTuple_GET_ITEM(addro, 0), PyTuple_GET_ITEM(addro, 1)); 91 | } else if (strcmp(event, "socket.bind") == 0) { 92 | PyObject *addro = PyTuple_GET_ITEM(args, 1); 93 | msg = PyUnicode_FromFormat("network: Attempt to bind %S:%S", 94 | PyTuple_GET_ITEM(addro, 0), PyTuple_GET_ITEM(addro, 1)); 95 | } else { 96 | msg = PyUnicode_FromFormat("network: %s (event not handled)", event); 97 | } 98 | 99 | if (!msg) { 100 | return -1; 101 | } 102 | Py_DECREF(msg); 103 | sd_journal_send("MESSAGE=%s", 104 | PyUnicode_AsUTF8(msg), 105 | "MESSAGE_ID=697945225c004609a15d0d57fcda3ead", 106 | "PRIORITY=5", 107 | "USER=%s", getenv("USER"), 108 | "HOME=%s", getenv("HOME"), 109 | "PWD=%s", getenv("PWD"), 110 | "TERM=%s", getenv("TERM"), 111 | "PAGE_SIZE=%li", sysconf(_SC_PAGESIZE), 112 | "N_CPUS=%li", sysconf(_SC_NPROCESSORS_ONLN), 113 | NULL); 114 | 115 | return 0; 116 | } 117 | 118 | int 119 | main(int argc, char **argv) 120 | { 121 | PyStatus status; 122 | PyConfig config; 123 | 124 | /* configure journald */ 125 | openlog(NULL, LOG_PID, LOG_USER); 126 | 127 | PySys_AddAuditHook(journaldHook, NULL); 128 | PySys_AddAuditHook(network_hook, NULL); 129 | 130 | /* initialize Python in isolated mode, but allow argv */ 131 | PyConfig_InitIsolatedConfig(&config); 132 | 133 | /* handle and parse argv */ 134 | config.parse_argv = 1; 135 | status = PyConfig_SetBytesArgv(&config, argc, argv); 136 | if (PyStatus_Exception(status)) { 137 | goto fail; 138 | } 139 | 140 | /* perform remaining initialization */ 141 | status = PyConfig_Read(&config); 142 | if (PyStatus_Exception(status)) { 143 | goto fail; 144 | } 145 | 146 | status = Py_InitializeFromConfig(&config); 147 | if (PyStatus_Exception(status)) { 148 | goto fail; 149 | } 150 | PyConfig_Clear(&config); 151 | 152 | return Py_RunMain(); 153 | 154 | fail: 155 | PyConfig_Clear(&config); 156 | if (PyStatus_IsExit(status)) { 157 | return status.exitcode; 158 | } 159 | /* Display the error message and exit the process with 160 | non-zero exit code */ 161 | Py_ExitStatusException(status); 162 | } 163 | -------------------------------------------------------------------------------- /linux_xattr/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3.8-config --cflags) 4 | CFLAGS+=$(shell pkg-config libcrypto --cflags) 5 | CFLAGS+=$(shell pkg-config libseccomp --cflags) 6 | 7 | LDFLAGS+=$(shell python3.8-config --ldflags --embed) 8 | LDFLAGS+=$(shell pkg-config libcrypto --libs) 9 | LDFLAGS+=$(shell pkg-config libseccomp --libs) 10 | 11 | objects=spython.o 12 | 13 | all: spython 14 | 15 | %.o: %.c 16 | $(CC) -c $< $(CFLAGS) 17 | 18 | spython: spython.o 19 | $(CC) -o $@ $^ $(LDFLAGS) 20 | 21 | .PHONY: clean 22 | clean: 23 | rm -rf *.o spython -------------------------------------------------------------------------------- /linux_xattr/mkxattr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.8 2 | """Add spython extended attributes to Python files 3 | """ 4 | import argparse 5 | import compileall 6 | import hashlib 7 | import os 8 | 9 | BASE = os.path.abspath(os.path.dirname(os.__file__)) 10 | XATTR_NAME = "user.org.python.x-spython-hash" 11 | 12 | parser = argparse.ArgumentParser("mkxattr for spython") 13 | parser.add_argument("--basedir", default=BASE) 14 | parser.add_argument("--xattr-name", default=XATTR_NAME) 15 | parser.add_argument("--hash", default="sha256") 16 | parser.add_argument("--verbose", action="store_true") 17 | 18 | 19 | def main(): 20 | args = parser.parse_args() 21 | setxattr(args) 22 | 23 | 24 | def setxattr(args): 25 | xattr_name = args.xattr_name.encode("ascii") 26 | for root, dirs, files in os.walk(args.basedir, topdown=True): 27 | for filename in sorted(files): 28 | if not filename.endswith((".py", ".pyc")): 29 | continue 30 | filename = os.path.join(root, filename) 31 | hasher = hashlib.new(args.hash) 32 | with open(filename, "rb") as f: 33 | hasher.update(f.read()) 34 | hexdigest = hasher.hexdigest().encode("ascii") 35 | try: 36 | value = os.getxattr(f.fileno(), xattr_name) 37 | except OSError: 38 | value = None 39 | if value != hexdigest: 40 | if args.verbose: 41 | if value is None: 42 | print(f"Adding spython hash to '{filename}'") 43 | else: 44 | print(f"Updating spython hash of '{filename}'") 45 | # it's likely that the pyc file is also out of sync 46 | compileall.compile_file(filename, quiet=2) 47 | os.setxattr(filename, xattr_name, hexdigest) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /linux_xattr/readme.md: -------------------------------------------------------------------------------- 1 | # PEP 578 spython proof of concept for Linux 2 | 3 | This is an experimental **proof of concept** implementation of 4 | ``spython`` for Linux. It uses extended file attributes to flag 5 | permitted ``py``/``pyc`` files. The extended file attribute 6 | ``user.org.python.x-spython-hash`` contains a SHA-256 hashsum of the 7 | file content. The ``spython`` interpreter refuses to load any Python 8 | file that has no or an invalid hashsum. 9 | 10 | Files must also be regular files that resides on an executable file system. 11 | 12 | setxattr syscalls are blocked with libseccomp. 13 | -------------------------------------------------------------------------------- /linux_xattr/spython.c: -------------------------------------------------------------------------------- 1 | /* Experimental spython interpreter for Linux 2 | * 3 | * Christian Heimes 4 | * 5 | * Licensed to PSF under a Contributor Agreement. 6 | */ 7 | #include "Python.h" 8 | #include "pystrhex.h" 9 | 10 | /* xattr, stat */ 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | /* seccomp */ 18 | #include 19 | #include 20 | 21 | /* hashing */ 22 | #include 23 | 24 | /* logging */ 25 | #include 26 | 27 | // 2 MB 28 | #define MAX_PY_FILE_SIZE (2*1024*1024) 29 | 30 | #define XATTR_NAME "user.org.python.x-spython-hash" 31 | #define XATTR_LENGTH ((EVP_MAX_MD_SIZE * 2) + 1) 32 | 33 | /* Block setxattr syscalls 34 | */ 35 | static int 36 | spython_seccomp_setxattr(int kill) { 37 | scmp_filter_ctx *ctx = NULL; 38 | uint32_t action; 39 | int rc = ENOMEM; 40 | unsigned int i; 41 | int syscalls[] = { 42 | SCMP_SYS(setxattr), 43 | SCMP_SYS(fsetxattr), 44 | SCMP_SYS(lsetxattr) 45 | }; 46 | 47 | if (kill) { 48 | action = SCMP_ACT_KILL_PROCESS; 49 | } else { 50 | action = SCMP_ACT_ERRNO(EPERM); 51 | } 52 | /* execve(2) does not grant additional privileges */ 53 | if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { 54 | perror("PR_SET_NO_NEW_PRIVS=1\n"); 55 | return -1; 56 | } 57 | /* allow all syscalls by default */ 58 | ctx = seccomp_init(SCMP_ACT_ALLOW); 59 | if (ctx == NULL) { 60 | goto end; 61 | } 62 | /* block setxattr syscalls */ 63 | for (i=0; i < (sizeof(syscalls)/sizeof(syscalls[0])); i++) { 64 | rc = seccomp_rule_add(ctx, action, syscalls[i], 0); 65 | if (rc < 0) { 66 | goto end; 67 | } 68 | } 69 | /* load seccomp rules into Kernel */ 70 | rc = seccomp_load(ctx); 71 | if (rc < 0) { 72 | goto end; 73 | } 74 | 75 | end: 76 | seccomp_release(ctx); 77 | if (rc != 0) { 78 | perror("seccomp failed.\n"); 79 | } 80 | return rc; 81 | } 82 | 83 | /* very file properties */ 84 | static int 85 | spython_check_file(const char *filename, int fd) 86 | { 87 | struct stat sb; 88 | struct statvfs sbvfs; 89 | 90 | if (fstat(fd, &sb) == -1) { 91 | PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); 92 | return -1; 93 | } 94 | 95 | /* Only open regular files */ 96 | if (!S_ISREG(sb.st_mode)) { 97 | errno = EINVAL; 98 | PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); 99 | return -1; 100 | } 101 | 102 | /* limit file size */ 103 | if (sb.st_size > MAX_PY_FILE_SIZE) { 104 | errno = EFBIG; 105 | PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); 106 | return -1; 107 | } 108 | 109 | /* check that mount point is not NOEXEC */ 110 | if (fstatvfs(fd, &sbvfs) == -1) { 111 | PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); 112 | return -1; 113 | } 114 | if ((sbvfs.f_flag & ST_NOEXEC) == ST_NOEXEC) { 115 | errno = EINVAL; 116 | PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); 117 | return -1; 118 | } 119 | return 0; 120 | } 121 | 122 | 123 | /* hash a Python bytes object with OpenSSL */ 124 | static PyObject* 125 | spython_hash_bytes(const char *filename, PyObject *buffer) 126 | { 127 | char *buf; 128 | Py_ssize_t size = 0; 129 | const EVP_MD *md = EVP_sha256(); 130 | EVP_MD_CTX *ctx = NULL; 131 | unsigned char digest[EVP_MAX_MD_SIZE]; 132 | unsigned int digest_size; 133 | PyObject *result = NULL; 134 | 135 | if (PyBytes_AsStringAndSize(buffer, &buf, &size) == -1) { 136 | goto end; 137 | } 138 | 139 | if ((ctx = EVP_MD_CTX_new()) == NULL) { 140 | PyErr_SetString(PyExc_ValueError, "EVP_MD_CTX_new() failed"); 141 | goto end; 142 | } 143 | if (!EVP_DigestInit(ctx, md)) { 144 | PyErr_SetString(PyExc_ValueError, "EVP_DigestInit SHA-256 failed"); 145 | goto end; 146 | } 147 | if (!EVP_DigestUpdate(ctx, (const void*)buf, (unsigned int)size)) { 148 | PyErr_SetString(PyExc_ValueError, "EVP_DigestUpdate() failed"); 149 | goto end; 150 | } 151 | if (!EVP_DigestFinal_ex(ctx, digest, &digest_size)) { 152 | PyErr_SetString(PyExc_ValueError, "EVP_DigestFinal() failed"); 153 | goto end; 154 | } 155 | result = _Py_strhex((const char *)digest, (Py_ssize_t)digest_size); 156 | 157 | end: 158 | EVP_MD_CTX_free(ctx); 159 | return result; 160 | } 161 | 162 | static PyObject* 163 | spython_fgetxattr(const char *filename, int fd) 164 | { 165 | char buf[XATTR_LENGTH]; 166 | Py_ssize_t size; 167 | 168 | size = fgetxattr(fd, XATTR_NAME, (void*)buf, sizeof(buf)); 169 | if (size == -1) { 170 | PyErr_Format(PyExc_OSError, "File %s has no xattr %s.", filename, XATTR_NAME); 171 | return NULL; 172 | } 173 | return PyUnicode_DecodeASCII(buf, size, "strict"); 174 | } 175 | 176 | 177 | static PyObject* 178 | spython_open_stream(const char *filename, int fd) 179 | { 180 | PyObject *stream = NULL; 181 | PyObject *iomod = NULL; 182 | PyObject *fileio = NULL; 183 | PyObject *buffer = NULL; 184 | PyObject *res = NULL; 185 | PyObject *file_hash = NULL; 186 | PyObject *xattr_hash = NULL; 187 | int cmp; 188 | 189 | if (spython_check_file(filename, fd) != 0) { 190 | goto end; 191 | } 192 | 193 | if ((iomod = PyImport_ImportModule("_io")) == NULL) { 194 | goto end; 195 | } 196 | 197 | /* read file with _io module */ 198 | fileio = PyObject_CallMethod(iomod, "FileIO", "isi", fd, "r", 0); 199 | if (fileio == NULL) { 200 | goto end; 201 | } 202 | buffer = PyObject_CallMethod(fileio, "readall", NULL); 203 | res = PyObject_CallMethod(fileio, "close", NULL); 204 | if ((buffer == NULL) || (res == NULL)) { 205 | goto end; 206 | } 207 | 208 | if ((file_hash = spython_hash_bytes(filename, buffer)) == NULL) { 209 | goto end; 210 | } 211 | if ((xattr_hash = spython_fgetxattr(filename, fd)) == NULL) { 212 | goto end; 213 | } 214 | cmp = PyObject_RichCompareBool(file_hash, xattr_hash, Py_EQ); 215 | switch(cmp) { 216 | case 1: 217 | stream = PyObject_CallMethod(iomod, "BytesIO", "O", buffer); 218 | break; 219 | case 0: 220 | PyErr_Format(PyExc_ValueError, 221 | "File hash mismatch: %s (expected: %R, got %R)", 222 | filename, xattr_hash, file_hash); 223 | goto end; 224 | default: 225 | goto end; 226 | } 227 | 228 | end: 229 | Py_XDECREF(buffer); 230 | Py_XDECREF(iomod); 231 | Py_XDECREF(fileio); 232 | Py_XDECREF(res); 233 | Py_XDECREF(file_hash); 234 | Py_XDECREF(xattr_hash); 235 | return stream; 236 | } 237 | 238 | static PyObject* 239 | spython_open_code(PyObject *path, void *userData) 240 | { 241 | PyObject *filename_obj = NULL; 242 | const char *filename; 243 | int fd = -1; 244 | PyObject *stream = NULL; 245 | 246 | if (PySys_Audit("spython.open_code", "O", path) < 0) { 247 | goto end; 248 | } 249 | 250 | if (!PyUnicode_FSConverter(path, &filename_obj)) { 251 | goto end; 252 | } 253 | filename = PyBytes_AS_STRING(filename_obj); 254 | 255 | fd = _Py_open(filename, O_RDONLY); 256 | if (fd < 0) { 257 | goto end; 258 | } 259 | 260 | stream = spython_open_stream(filename, fd); 261 | if (stream == NULL) { 262 | syslog(LOG_CRIT, "spython failed to verify file %s.", filename); 263 | } 264 | 265 | end: 266 | Py_XDECREF(filename_obj); 267 | if (fd >= 0) { 268 | close(fd); 269 | } 270 | return stream; 271 | } 272 | 273 | int 274 | main(int argc, char **argv) 275 | { 276 | PyStatus status; 277 | PyConfig config; 278 | 279 | /* block syscalls */ 280 | if (spython_seccomp_setxattr(0) < 0) { 281 | exit(1); 282 | } 283 | 284 | /* configure syslog */ 285 | openlog(NULL, LOG_CONS | LOG_PERROR | LOG_PID, LOG_USER); 286 | 287 | /* initialize Python in isolated mode, but allow argv */ 288 | status = PyConfig_InitIsolatedConfig(&config); 289 | if (PyStatus_Exception(status)) { 290 | goto fail; 291 | } 292 | 293 | /* install hooks */ 294 | PyFile_SetOpenCodeHook(spython_open_code, NULL); 295 | 296 | /* handle and parse argv */ 297 | config.parse_argv = 1; 298 | status = PyConfig_SetBytesArgv(&config, argc, argv); 299 | if (PyStatus_Exception(status)) { 300 | goto fail; 301 | } 302 | 303 | /* perform remaining initialization */ 304 | status = PyConfig_Read(&config); 305 | if (PyStatus_Exception(status)) { 306 | goto fail; 307 | } 308 | 309 | status = Py_InitializeFromConfig(&config); 310 | if (PyStatus_Exception(status)) { 311 | goto fail; 312 | } 313 | PyConfig_Clear(&config); 314 | 315 | return Py_RunMain(); 316 | 317 | fail: 318 | PyConfig_Clear(&config); 319 | if (PyStatus_IsExit(status)) { 320 | return status.exitcode; 321 | } 322 | /* Display the error message and exit the process with 323 | non-zero exit code */ 324 | Py_ExitStatusException(status); 325 | } 326 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | spython 2 | ======= 3 | 4 | This repository contains sample implementations of CPython entry points 5 | using the hooks added in [PEP 578](https://www.python.org/dev/peps/pep-0578/). 6 | 7 | Python 3.8 is required for these samples, or you can build Python yourself 8 | from the [3.8](https://github.com/python/cpython/tree/3.8) or 9 | [master](https://github.com/python/cpython) branch. 10 | 11 | LogToStdErr 12 | ----------- 13 | 14 | The implementation in [`LogToStderr`](LogToStderr) is nearly the simplest 15 | possible code. It takes every event and prints its arguments to standard 16 | error. 17 | 18 | Two points are worth calling out: 19 | * during initialisation, it does not render arguments, but this is only 20 | because `PyObject_Repr` does not always work correctly 21 | * `compile` is handled specially to avoid printing the full code of 22 | every module 23 | 24 | Also see [`LogToStderrMinimal`](LogToStderrMinimal), which is actually 25 | the simplest possible code to displays a message for each event. 26 | 27 | NetworkPrompt 28 | ------------- 29 | 30 | The implementation in [`NetworkPrompt`](NetworkPrompt) is a hook that 31 | prompts the user on every `socket.*` event. If the user types `n`, the 32 | process is aborted. 33 | 34 | The `network_prompt.py` module uses a Python hook to implement the same 35 | prompt. 36 | 37 | StartupControl 38 | -------------- 39 | 40 | The implementation in [`StartupControl`](StartupControl) limits how 41 | Python may be launched and requires that a startup file is specified. 42 | This prevents the use of the `-c` and `-m` options, as well as 43 | interactive mode. 44 | 45 | WindowsCatFile 46 | -------------- 47 | 48 | The implementation in [`WindowsCatFile`](WindowsCatFile) 49 | uses a signed `python_lib.cat` file to verify all imported modules. 50 | 51 | This sample only works on Windows. 52 | 53 | WindowsEventLog 54 | --------------- 55 | 56 | The implementation in [`WindowsEventLog`](WindowsEventLog) 57 | writes a selection of events to a section of the Windows event log. 58 | 59 | This sample only works on Windows. 60 | 61 | syslog 62 | ------ 63 | 64 | The implementation in [`syslog`](syslog) writes a selection of events 65 | to the current syslog listener. 66 | 67 | This sample requires a syslog implementation. 68 | 69 | linux_xattr 70 | ----------- 71 | 72 | The implementation in [`linux_xattr`](linux_xattr) is a proof of 73 | concept for Linux. It verifies all imported modules by hashing their 74 | content with OpenSSL and comparing the hashes against stored hashes in 75 | extended file attributes. 76 | 77 | See the readme in that directory for more information. 78 | 79 | This sample only works on Linux and requires OpenSSL and libseccomp. -------------------------------------------------------------------------------- /syslog/Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O0 -g -pipe 3 | CFLAGS+=$(shell python3.8-config --cflags) 4 | 5 | LDFLAGS+=$(shell python3.8-config --ldflags --embed) 6 | 7 | objects=spython.o 8 | 9 | all: spython 10 | 11 | %.o: %.c 12 | $(CC) -c $< $(CFLAGS) 13 | 14 | spython: spython.o 15 | $(CC) -o $@ $^ $(LDFLAGS) 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -rf *.o spython 20 | -------------------------------------------------------------------------------- /syslog/readme.md: -------------------------------------------------------------------------------- 1 | linux_syslog 2 | ============ 3 | 4 | This sample writes messages using [syslog](https://wikipedia.org/wiki/Syslog). 5 | 6 | To build on Linux, run `make` with a copy of Python 3.8.0rc1 or later 7 | installed. 8 | 9 | You may need to enable a syslog service on your machine in order to 10 | receive the events. For example, if using `rsyslog`, you might use 11 | these commands: 12 | 13 | ``` 14 | $ make 15 | $ sudo service rsyslog start 16 | $ ./spython test-file.py 17 | $ sudo service rsyslog stop 18 | $ cat /var/log/syslog 19 | ``` 20 | -------------------------------------------------------------------------------- /syslog/spython.c: -------------------------------------------------------------------------------- 1 | /* syslog example using PySys_AddAuditHook 2 | */ 3 | #include 4 | 5 | /* logging */ 6 | #include 7 | 8 | int 9 | syslogHook(const char *event, PyObject *args, void *userData) 10 | { 11 | if (strcmp(event, "import") == 0) { 12 | PyObject *module, *filename, *sysPath, *sysMetaPath, *sysPathHooks; 13 | if (!PyArg_ParseTuple(args, "OOOOO", &module, &filename, 14 | &sysPath, &sysMetaPath, &sysPathHooks)) { 15 | return -1; 16 | } 17 | if (filename == Py_None) { 18 | syslog(LOG_INFO, "importing %s", PyUnicode_AsUTF8(module)); 19 | } else { 20 | syslog(LOG_INFO, "importing %s from %s", 21 | PyUnicode_AsUTF8(module), 22 | PyUnicode_AsUTF8(filename)); 23 | } 24 | Py_DECREF(filename); 25 | return 0; 26 | } 27 | 28 | if (strcmp(event, "os.system") == 0 || 29 | /* additional check for bug in 3.8.0rc1 */ 30 | strcmp(event, "system") == 0) { 31 | PyObject *command; 32 | if (!PyArg_ParseTuple(args, "O&", PyUnicode_FSConverter, &command)) { 33 | return -1; 34 | } 35 | syslog(LOG_ERR, "os.system('%s') attempted", PyBytes_AsString(command)); 36 | Py_DECREF(command); 37 | PyErr_SetString(PyExc_OSError, "os.system is disabled"); 38 | return -1; 39 | } 40 | 41 | return 0; 42 | } 43 | 44 | int 45 | main(int argc, char **argv) 46 | { 47 | PyStatus status; 48 | PyConfig config; 49 | 50 | /* configure syslog */ 51 | openlog(NULL, LOG_PID, LOG_USER); 52 | 53 | PySys_AddAuditHook(syslogHook, NULL); 54 | 55 | /* initialize Python in isolated mode, but allow argv */ 56 | PyConfig_InitIsolatedConfig(&config); 57 | 58 | /* handle and parse argv */ 59 | config.parse_argv = 1; 60 | status = PyConfig_SetBytesArgv(&config, argc, argv); 61 | if (PyStatus_Exception(status)) { 62 | goto fail; 63 | } 64 | 65 | /* perform remaining initialization */ 66 | status = PyConfig_Read(&config); 67 | if (PyStatus_Exception(status)) { 68 | goto fail; 69 | } 70 | 71 | status = Py_InitializeFromConfig(&config); 72 | if (PyStatus_Exception(status)) { 73 | goto fail; 74 | } 75 | PyConfig_Clear(&config); 76 | 77 | return Py_RunMain(); 78 | 79 | fail: 80 | PyConfig_Clear(&config); 81 | if (PyStatus_IsExit(status)) { 82 | return status.exitcode; 83 | } 84 | /* Display the error message and exit the process with 85 | non-zero exit code */ 86 | Py_ExitStatusException(status); 87 | } 88 | --------------------------------------------------------------------------------