Skip to content

lemonyte/safe-exec

Repository files navigation

safe-exec

safe-exec provides a context manager that prevents calls to exec and eval in Python code, and reveals the code in calls that were attempted.

This is useful for retrieving deobfuscated code from obfuscated malware samples that use exec or eval as the entry point.

Caution

This package does not guarantee that a script is safe to run. It is only a tool to help deobfuscate code. It does not sandbox code, nor does it intend to be foolproof. Never run potentially malicious code outside of a secure sandboxed environment.

Installation

pip install safe-exec

Or install from source:

pip install git+https://github.com/lemonyte/safe-exec.git

Python versions 3.9 through 3.13 are supported.

Simple example

The safe context manager takes either exec or eval as an argument.

>>> from safe_exec import safe
>>> with safe(exec):
...     from base64 import b64decode
...     exec(b64decode("cHJpbnQoJ29iZnVzY2F0ZWQgY29kZScp"))

safe_exec.ExecBlockedError: blocked execution of b"print('obfuscated code')"

You can also use the runpy module to execute a different script in the safe context:

>>> import runpy
>>> with safe(exec), safe(eval):
...     runpy.run_path("path/to/script.py")

Advanced usage

safe can also be used as a decorator to block calls to exec or eval in a function:

from safe_exec import safe

@safe(exec)
@safe(eval)
def untrusted_func():
    exec("print('This is an untrusted function')")

untrusted_func()

And as a function to register a third-party function as an untrusted caller:

from safe_exec import safe
import runpy

runpy.run_path = safe(exec)(runpy.run_path)

However, the context manager is recommended for most use cases, as it ensures the functions that are overwritten are restored after the context exits.

Allowing certain calls

safe-exec provides allow_exec and allow_eval context decorators to allow specific calls to exec and eval, respectively.

These can be used as decorators on functions that should be allowed to call exec or eval:

from safe_exec import allow_exec, allow_eval

@allow_exec
@allow_eval
def trusted_func():
    exec("print('This is a trusted function')")
    eval("print('This is a trusted function')")

trusted_func()

Caution

If other code calls the decorated function, the exec and eval calls will still be allowed.

As context managers to temporarily allow a function to call exec or eval:

from safe_exec import allow_exec, allow_eval

def semi_trusted_func():
    exec("print('This is a semi-trusted function')")
    eval("print('This is a semi-trusted function')")

with allow_exec(semi_trusted_func), allow_eval(semi_trusted_func):
    semi_trusted_func()  # Calls are allowed here.
semi_trusted_func()  # Calls are blocked here.

Or as functions to register a third-party function as an allowed caller:

from safe_exec import allow_eval
import collections

allow_eval(collections.namedtuple)

safe vs allow_exec and allow_eval

The two groups of context managers have slightly different purposes, and therefore different interfaces.

safe creates a 'safe' context wherein calls to exec or eval are blocked. If used as a decorator, it does not do anything with the decorated function itself.

allow_exec and allow_eval on the other hand need to take a callable (besides exec or eval) as an argument. This is because they register the callable as a trusted caller of exec or eval, whether they are used as decorators or context managers.

False positives

Because exec and eval are used in a number of places within the Python standard library, safe-exec may prevent some necessary code from running. If you encounter a false positive, please open an issue.

As a temporary workaround, if you can determine the calling function that depends on exec or eval, you can use the allow_exec and allow_eval context managers to register it as a trusted caller.

Legitimate calls to exec and eval are allowed by inspecting the caller's frame. If the caller's code object is present in the list of trusted callers, the call is allowed. Such calls are logged as informational messages.

>>> from safe_exec import safe
>>> import logging
>>> logging.basicConfig(level=logging.INFO)
>>> from typing import NamedTuple
>>> with safe(eval):
...     Foo = NamedTuple("Foo", [("bar", int)])

INFO:safe_exec:allowed eval call by 'namedtuple' from '.../lib/collections/__init__.py':345

License

MIT License

Packages

No packages published

Contributors 2

  •  
  •  

Languages