Repository: Gallopsled/pwntools

Issue:
opcode LIST_EXTEND not allowed
Python 3.9.0 brings new opcode [`LIST_EXTEND`](https://docs.python.org/3/library/dis.html#opcode-LIST_EXTEND), which leads to an error during `pwn` import:

```
$ cat ~/.pwn.conf
[context]
terminal = ['alacritty', '-e', 'sh', '-c']
$ ipython
Python 3.9.0 (default, Oct  7 2020, 23:09:01)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.19.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import pwn
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-1-85301d96339b> in <module>
----> 1 import pwn

~/src/pwntools/pwn/toplevel.py in <module>
      6 pwnlib.args.initialize()
      7 pwnlib.log.install_default_handler()
----> 8 pwnlib.config.initialize()
      9
     10 log = pwnlib.log.getLogger('pwnlib.exploit')

~/src/pwntools/pwnlib/config.py in initialize()
     62             continue
     63         settings = dict(c.items(section))
---> 64         registered_configs[section](settings)

~/src/pwntools/pwnlib/context/__init__.py in update_context_defaults(section)
   1491
   1492         if isinstance(default, six.string_types + six.integer_types + (tuple, list, dict)):
-> 1493             value = safeeval.expr(value)
   1494         else:
   1495             log.warn("Unsupported configuration option %r in section %r" % (key, 'context'))

~/src/pwntools/pwnlib/util/safeeval.py in expr(expr)
    109     """
    110
--> 111     c = test_expr(expr, _expr_codes)
    112     return eval(c)
    113

~/src/pwntools/pwnlib/util/safeeval.py in test_expr(expr, allowed_codes)
     61     for code in codes:
     62         if code not in allowed_codes:
---> 63             raise ValueError("opcode %s not allowed" % dis.opname[code])
     64     return c
     65

ValueError: opcode LIST_EXTEND not allowed
```

Adding `'LIST_EXTEND'` to [`_const_codes`](https://github.com/Gallopsled/pwntools/blob/db8df476b2bed695dea14eb726b45237b972811f/pwnlib/util/safeeval.py#L3) or [`_expr_codes`](https://github.com/Gallopsled/pwntools/blob/db8df476b2bed695dea14eb726b45237b972811f/pwnlib/util/safeeval.py#L9) seems to solve the issue, but I'm not familiar with this code.

Also, there are more new opcodes: `LIST_TO_TUPLE`, `SET_UPDATE`, etc.


Discussion:
Python 3.8.6 (default, Nov 18 2020, 13:49:49)
```
>>> pprint(list(dis.get_instructions(compile("[1,2,3]", "", "eval"))))
[Instruction(opname='LOAD_CONST', opcode=100, arg=0, argval=1, argrepr='1', offset=0, starts_line=1, is_jump_target=False),
 Instruction(opname='LOAD_CONST', opcode=100, arg=1, argval=2, argrepr='2', offset=2, starts_line=None, is_jump_target=False),
 Instruction(opname='LOAD_CONST', opcode=100, arg=2, argval=3, argrepr='3', offset=4, starts_line=None, is_jump_target=False),
 Instruction(opname='BUILD_LIST', opcode=103, arg=3, argval=3, argrepr='', offset=6, starts_line=None, is_jump_target=False),
 Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=8, starts_line=None, is_jump_target=False)]
```

Python 3.9.0 (default, Nov 18 2020, 13:28:38)
```
>>> pprint(list(dis.get_instructions(compile("[1,2]", "", "eval"))))
[Instruction(opname='LOAD_CONST', opcode=100, arg=0, argval=1, argrepr='1', offset=0, starts_line=1, is_jump_target=False),
 Instruction(opname='LOAD_CONST', opcode=100, arg=1, argval=2, argrepr='2', offset=2, starts_line=None, is_jump_target=False),
 Instruction(opname='BUILD_LIST', opcode=103, arg=2, argval=2, argrepr='', offset=4, starts_line=None, is_jump_target=False),
 Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=6, starts_line=None, is_jump_target=False)]
>>> pprint(list(dis.get_instructions(compile("[1,2,3]", "", "eval"))))
[Instruction(opname='BUILD_LIST', opcode=103, arg=0, argval=0, argrepr='', offset=0, starts_line=1, is_jump_target=False),
 Instruction(opname='LOAD_CONST', opcode=100, arg=0, argval=(1, 2, 3), argrepr='(1, 2, 3)', offset=2, starts_line=None, is_jump_target=False),
 Instruction(opname='LIST_EXTEND', opcode=162, arg=1, argval=1, argrepr='', offset=4, starts_line=None, is_jump_target=False),
 Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=6, starts_line=None, is_jump_target=False)]
```
I'm not a security expert and python is my secondary language, but at first glance, `LIST_EXTEND` is currently generated by cpython compiler only to declare a list, a tuple, and to call a function. However, virtual machine is happy to modify an existing list using `LIST_EXTEND` if bytecode requests it.

So I'm not sure which direction to take. To name a few:
* Add `LIST_EXTEND` to `_expr_codes`. It can violate safe evaluation promise in the future.
* Look for other ways to guarantee safe evaluation.
* Use unsafe evaluation for config. Actually, I did not check where safe evaluation used and what parts of pwntools can be affected by this issue.
* Probably other ways that I do not see.

Resolving patch:
diff --git a/pwnlib/util/safeeval.py b/pwnlib/util/safeeval.py
--- a/pwnlib/util/safeeval.py
+++ b/pwnlib/util/safeeval.py
@@ -3,7 +3,9 @@
 _const_codes = [
     'POP_TOP','ROT_TWO','ROT_THREE','ROT_FOUR','DUP_TOP',
     'BUILD_LIST','BUILD_MAP','BUILD_TUPLE','BUILD_SET',
-    'LOAD_CONST','RETURN_VALUE','STORE_SUBSCR', 'STORE_MAP'
+    'BUILD_CONST_KEY_MAP', 'BUILD_STRING',
+    'LOAD_CONST','RETURN_VALUE','STORE_SUBSCR', 'STORE_MAP',
+    'LIST_TO_TUPLE', 'LIST_EXTEND', 'SET_UPDATE', 'DICT_UPDATE', 'DICT_MERGE',
     ]
 
 _expr_codes = _const_codes + [


Test patch: