Kevin Froman's blog

Blog on security, programming, & other musings

Preventing Arbitrary Code Execution in Python3.8 With Auditing

2019-12-16-

In Python 3.8+, support for system auditing hooks was added. System auditing uses two main functions:

sys.addaudithook: accepts a callable to execute when auditing events are raised anywhere in the current Python interpreter.

sys.audit: takes an event name and passes *args (related to current execution) to all hooks added by the above.

Part of the benefit of the above is that it let's us validate or block certain behavior in not only own own code, but also the standard library and dependencies.

Not counting bugs in underlying C libraries, security bugs in Python code tend to come from eval()/exec(), os.system(), subproccess.POpen() and others. We can use audit hooks to prevent execution of these that we do not approve of.

Example

Say we want to block all os.system usage in our run time:

        import sys
        
        class DisabledForSecurityReasons(Exception): pass
        
        def block_sys(event, cmd):
            if event == "system":
                print(cmd)
                raise DisabledForSecurityReasons
        
        sys.addaudithook(block_sys)
        

If the above is executed anywhere, all usage of os.system() anywhere in the interpreter will be interrupted by DisabledForSecurityReasons exception.

If needed, cmd could be parsed to allow or disallow certain commands (whitelisting would be the best approach).

Real World Example

Onionr is my project for decentralized private networking. Since one of the goals of the project is to enable anonymous communication, it is important that no network connections leak to the clearnet)

To enforce this and other policies, I created a new submodule with an ironic name: bigbrother.

Big Brother uses a series of 'ministries' to enforce

Big Brother monitors for socket.connect and other events:

def sys_hook_entrypoint(event, info):
            if event == 'socket.connect':
                ministry.ofcommunication.detect_socket_leaks(info)
            elif event == 'exec':
                # logs and block both exec and eval
                ministry.ofexec.block_exec(event, info)
            elif event == 'system':
                ministry.ofexec.block_system(info)
        

As hooks cannot be called selectively based on event name, I think it is best to have a single hook wrap other functions.

This is the function wrapped by my hook called for socket connections:

        import ipaddress
        
        import logger
        from onionrexceptions import NetworkLeak
        
        
        def detect_socket_leaks(socket_event):
            """is called by the big brother broker whenever
            a socket connection happens.
            raises exception & logs if not to loopback
            """
            ip_address = socket_event[1][0]
        
            # validate is valid ip address (no hostname, etc)
            # raises NetworkLeak if not
            try:
                ipaddress.ip_address(ip_address)
            except ValueError:
                logger.warn(f'Conn made to {ip_address} outside of Tor/similar')
                raise \
                    NetworkLeak('Conn to host/non local IP, this is a privacy issue!')
        
            # Validate that the IP is localhost ipv4
        
            if not ip_address.startswith('127'):
                logger.warn(f'Conn made to {ip_address} outside of Tor/similar')
                raise NetworkLeak('Conn to non local IP, this is a privacy concern!')
        

This code ensures that a socket was created to only ipv4 localhost, which in the context of Onionr would be either a proxy (Tor) or the Onionr daemon.

Conclusion

Using sys auditing, we can make our Python run times somewhat more secure with generally acceptable speed trade offs.

My above examples and usage in Onionr are likely not perfect and are not meant to stop an intentionally malicious module. To take full advantage of sys.audit, one would need to audit quite a lot of events.