620 likes | 787 Vues
errata. EuSecWest website references IOActive No longer IOActive employee Independent contractor with IOActive Research is the work of myself and does not relate to IOActive No one at IOActive doing similar research. What?!. Interpreters serve as abstraction layer
E N D
errata • EuSecWest website references IOActive • No longer IOActive employee • Independent contractor with IOActive • Research is the work of myself and does not relate to IOActive • No one at IOActive doing similar research
What?! • Interpreters serve as abstraction layer • Conceptually similar to VMs used in managed languages (i.e. Java or .Net) • Attacks against interpreted languages typically focus around ‘traditional web-app attacks’ • Poison NULL byte complications • SQL Injection • Et cetera • Traditionally thought of as being immune to problems that plague other languages- i.e. buffer overflows /* PSz 12 Nov 03 * * Be proud that perl(1) may proclaim: * Setuid Perl scripts are safer than C programs … * Do not abandon (deprecate) suidperl. Do not advocate C wrappers. *
Reason for Rhyme • Usage of web and managed applications only going to increase • Gap between how these are attacked • Protections against application based attack are C-centric • Stack cookies • Higher layers of abstraction may have their own call stacks • Heap cookies protect against heap memory corruption • Many languages implement their own allocator that lack cookies • The unlink() macro sanity checks the forward/backward pointers • Many languages implement their own allocator that lack sanity checks • Linked lists often implemented on top of block of memory • NX protects against execution • Byte code is read/interpreted, not executed • ASLR protects against return-to-libc/et cetera • Still valid
But, the future of insecurity? • Hacking community is largely content with the world as it is • World is changing • Most OSs ship with some hardening anymore • GCC ships with SSP • Visual Studio 2005 is pretty effective • Interpreted & Managed language use on the rise • We don’t get to choose what the applications we break are written in • Adapt or die • Maybe not the future • But, I’m at least thinking about it
Goals & Prior Art • Goals: • Memory Corruption bug in interpreter • Attack interpreted language metadata • Return into interpreted language bytecode • Stephan Esser • Hardened PHP, Month of PHP bugs, et cetera • Mark Dowd • Leveraging the ActionScript Virtual Machine
Damn the torpedoes • In Mid-April 2008 Google rolled out ‘AppEngine’ • AppEngine ‘enables you to build web applications on the same scalable systems that power Google applications’ • AKA Here’s a python interpreter, you can’t break us. • A flagship example is the shell application • Literally a web-based interface to the interpreter • Interpreter runs in a restricted environment • All file-based I/O is (supposed to be) disabled and a Google specific datastore API is provided • Subprocesses, threads, et cetera disabled • No sockets • Many modules disabled or modified • Perfect target.
Abba Zabba, you my only friend • Having direct access to the interpreter allows *a lot* of flexibility • Stopping address space leaks becomes incredibly problematic (sys._getframe() ?) • Attacker can manipulate interpreter state to match necessary conditions • Situation is the same that shared hosting providers have faced for years • Except now its Google, they’re pushing this for enterprise use, and the attack surface has been acknowledged
But interpreted languages don’t have buffer overflows… • More common than expected • CVE-2008-1679: Multiple integer overflows in imageop module • CVE-2008-1887: Signedness issues in PyString_FromStringAndSize() • CVE-2008-1721: Signedness issues cause buffer overflow in zlib module • CVE-2008-XXXX: Integer overflow leads to buffer overflow in Unicode processing • CVE-2008-XXXX: Integer overflow leads to buffer overflow in Buffer objects • Et cetera • Interpreter code still relatively ‘virgin’ • Many in Python due to extensive use of signed integers
On 0-day • Over next few slides several bugs are discussed • Some are reported and patched • Some are reported and unpatched • Some are undisclosed and unpatched • Not all bugs are equal • Most occur in unusual circumstances • Most require direct interpreter access • Others are typically unexploitable (i.e. memcpy() of 4G) • Most undisclosed were found in a very short period of time • Point is, they exist & they’re not hard to find
Ethics of 0-day • Arguments would be easier to take serious if contracts didn’t have clauses like this:
When good APIs go bad • Patched in CVS, broken in Python versions up to 2.5.2 • Also in PyBytes_FromStringAndSize() & PyUnicode_FromStringAndSize() 52 PyObject * 53 PyString_FromStringAndSize(const char *str, Py_ssize_t size) 54 { 55 register PyStringObject *op; 56 assert(size >= 0); 57 if (size == 0 && (op = nullstring) != NULL) { […] 63 } 64 if (size == 1 && str != NULL && 65 (op = characters[*str & UCHAR_MAX]) != NULL) 66 { […] 72 } 73 74 /* Inline PyObject_NewVar */ 75 op = (PyStringObject *)PyObject_MALLOC(sizeof(PyStringObject) + size);
Where the wild things roam.. • Currently reported but unpatched • Like previous example causes faults in numerous places– including core data types 85 #define PyMem_New(type, n) \ 86 (assert((n) <= PY_SIZE_MAX / sizeof(type)) , \ 87 ((type *) PyMem_Malloc((n) * sizeof(type)))) 88 #define PyMem_NEW(type, n) \ 89 (assert((n) <= PY_SIZE_MAX / sizeof(type)) , \ 90 ((type *) PyMem_MALLOC((n) * sizeof(type)))) 91 92 #define PyMem_Resize(p, type, n) \ 93 (assert((n) <= PY_SIZE_MAX / sizeof(type)) , \ 94 ((p) = (type *) PyMem_Realloc((p), (n) * sizeof(type)))) 95 #define PyMem_RESIZE(p, type, n) \ 96 (assert((n) <= PY_SIZE_MAX / sizeof(type)) , \ 97 ((p) = (type *) PyMem_REALLOC((p), (n) * sizeof(type))))
0xbadc0ded • Reported, but currently unpatched static int unicode_resize(register PyUnicodeObject *unicode, Py_ssize_t length) {[...]oldstr = unicode->str;PyMem_RESIZE(unicode->str, Py_UNICODE, length + 1); [...]unicode->str[length] = 0; static PyUnicodeObject *_PyUnicode_New(Py_ssize_t length) { [...] /* Unicode freelist & memory allocation */ if (unicode_freelist) { […] if ((unicode->length < length) && unicode_resize(unicode, length) < 0) { […] else {unicode->str = PyMem_NEW(Py_UNICODE, length + 1);
0xbadc0ded • Reported and patched in CVS, versions up to 2.5.2 are vulnerable 768 static PyObject * 769 PyZlib_unflush(compobject *self, PyObject *args) 770 { 771 int err, length = DEFAULTALLOC; 772 PyObject * retval = NULL; 773 unsigned long start_total_out; 774 775 776 if (!PyArg_ParseTuple(args, "|i:flush", &length)) 777 return NULL; 778 if (!(retval = PyString_FromStringAndSize(NULL, length))) 779 return NULL; […] 783 start_total_out = self->zst.total_out; 784 self->zst.avail_out = length; 785 self->zst.next_out = (Byte *)PyString_AS_STRING(retval); 786 787 Py_BEGIN_ALLOW_THREADS 788 err = inflate(&(self->zst), Z_FINISH);
0xbadc0ded • Currently undisclosed & unpatched static PyObject * array_fromunicode(arrayobject *self, PyObject *args) { Py_UNICODE *ustr; Py_ssize_t n; if (!PyArg_ParseTuple(args, “u#:fromunicode", &ustr, &n)) return NULL; [...] if (n > 0) { Py_UNICODE *item = (Py_UNICODE *) self->ob_item; if (self->ob_size > PY_SSIZE_T_MAX - n) { return PyErr_NoMemory(); } PyMem_RESIZE(item, Py_UNICODE, self->ob_size + n); if (item == NULL) { PyErr_NoMemory(); return NULL; } self->ob_item = (char *) item; self->ob_size += n; self->allocated = self->ob_size;memcpy(item + self->ob_size - n, ustr, n * sizeof(Py_UNICODE));
0xbadc0ded • Currently undisclosed & unpatched static PyObject * encoder_encode_stateful(MultibyteStatefulEncoderContext *ctx, PyObject *unistr, int final) { PyObject *ucvt, *r = NULL; Py_UNICODE *inbuf, *inbuf_end, *inbuf_tmp = NULL; Py_ssize_t datalen, origpending; [...] datalen = PyUnicode_GET_SIZE(unistr); origpending = ctx->pendingsize;if (origpending > 0) { if (datalen > PY_SSIZE_T_MAX - ctx->pendingsize) { […] }inbuf_tmp = PyMem_New(Py_UNICODE, datalen + ctx->pendingsize); […] memcpy(inbuf_tmp, ctx->pending, Py_UNICODE_SIZE *ctx->pendingsize);
ya dig? • Currently undisclosed & unpatched static PyObject * posix_execv(PyObject *self, PyObject *args) { […] if (!PyArg_ParseTuple(args, "etO:execv", Py_FileSystemDefaultEncoding, &path, &argv)) return NULL; if (PyList_Check(argv)) { argc = PyList_Size(argv); […] else if (PyTuple_Check(argv)) { argc = PyTuple_Size(argv); […]argvlist = PyMem_NEW(char *, argc+1); [...] for (i = 0; i < argc; i++) { if (!PyArg_Parse((*getitem)(argv, i), "et", Py_FileSystemDefaultEncoding, &argvlist[i])) { [...]
Goals review • Goal 0: memory corruption bugs • Bugs are just as prevalent as other traditional applications • Some of them are pretty silly • Lots are still easy to spot and require very little in the way of deep thinking • Some are exploitable, some require specific circumstances, others are just bugs • Goal 1: attack interpreter level metadata..
Python Call stack • Simple test program: #!/usr/bin/python import time while 1: time.sleep(500) gdb> r […] Program received signal SIGINT, Interrupt. [Switching to Thread 0x2b9d5bdd4d00 (LWP 28588)] 0x00002b9d5bb4f043 in select () from /lib/libc.so.6 gdb> bt #0 0x00002b9d5bb4f043 in select () from /lib/libc.so.6 #1 0x00002b9d5bdd869f in time_sleep […] #2 0x0000000000486097 in PyEval_EvalFrameEx […] #3 0x0000000000488002 in PyEval_EvalCodeEx […] #4 0x00000000004882a2 in PyEval_EvalCode […] #5 0x00000000004a969e in PyRun_FileExFlags […] #6 0x00000000004a9930 in PyRun_SimpleFileExFlags […] #7 0x0000000000414630 in Py_Main […] #8 0x00002b9d5bab2b74 in __libc_start_main […] #9 0x0000000000413b89 in _start ()
Python Objects • Most (interesting) object types start with a reference to PyObject_VAR_HEAD • i.e.: typedef struct _xyz { PyObject_VAR_HEAD […] • PyObject_VAR_HEAD macro expands to: • Contain the objects reference count • Contain pointers to next/previous in-use object (doubly linked list) • Contains a pointer to the objects type • This point is way more important at first may seem
PyCodeObject /* Bytecode object */ typedef struct { PyObject_HEAD int co_argcount; /* #arguments, except *args */ int co_nlocals; /* #local variables */ int co_stacksize; /* #entries needed for evaluation stack */ int co_flags; /* CO_..., see below */ PyObject *co_code; /* instruction opcodes */ PyObject *co_consts; /* list (constants used) */ PyObject *co_names; /* list of strings (names used) */ PyObject *co_varnames; /* tuple of strings (local variable names) */ PyObject *co_freevars; /* tuple of strings (free variable names) */ PyObject *co_cellvars; /* tuple of strings (cell variable names) */ PyObject *co_filename; /* string (where it was loaded from) */ PyObject *co_name; /* string (name, for reference) */ int co_firstlineno; /* first source line number */ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ } PyCodeObject;
PyEval_EvalCodeEx() • PyEval_EvalCode() is a simple wrapper to PyEval_EvalCodeEx() • Uses default arguments for last seven parameters – passes NULL or 0 • Takes a PyCodeObject as a parameter • Creates a PyFrameObject • Sets up local/global/et cetera variables • Serves essentially to setup environment
PyFrameObject typedef struct _frame { PyObject_VAR_HEAD struct _frame *f_back; /* previous frame, or NULL */ PyCodeObject *f_code; /* code segment */ PyObject *f_builtins; /* builtin symbol table (PyDictObject) */ PyObject *f_globals; /* global symbol table (PyDictObject) */ PyObject *f_locals; /* local symbol table (any mapping) */ PyObject **f_valuestack; /* points after the last local */ /* Next free slot in f_valuestack. Frame creation sets to f_valuestack. Frame evaluation usually NULLs it, but a frame that yields sets I to the current stack top. */ PyObject **f_stacktop PyObject *f_trace; /* Trace function */ […] PyThreadState *f_tstate; int f_lasti; /* Last instruction if called */ […] int f_iblock; /* index in f_blockstack */ PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */ } PyFrameObject;
PyFrameObject destruction • As frames go out of scope, frame_dealloc() is called to destroy them • During destruction, only locals, exception and debugging information is cleared • Frame can end up in the PyCodeObject’s zombie frame, the free list, or just destroyed void frame_dealloc(PyFrameObject *f) { [...]for (p = f->f_localsplus; p < valuestack; p++) Py_CLEAR(*p);[...]Py_CLEAR(f->f_locals);Py_CLEAR(f->f_trace);Py_CLEAR(f->f_exc_type);Py_CLEAR(f->f_exc_value);Py_CLEAR(f->f_exc_traceback);co = f->f_code;if (co->co_zombieframe == NULL) co->co_zombieframe = f;else if (numfree < MAXFREELIST) { ++numfree; f->f_back = free_list; free_list = f;}
Zombies attack!!1 PyFrameObject *PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals, PyObject *locals) { [...] if (code->co_zombieframe != NULL) { f = code->co_zombieframe; code->co_zombieframe = NULL; assert(f->code == code); } else { [...]} f->f_stacktop = f->f_valuestack; f->f_builtins = builtins;Py_XINCREF(back);f->f_back = back;Py_INCREF(code);Py_INCREF(globals);f->f_globals = globals;[...]return f; }
Unleashing your zombie army.. • Attacking zombie frame not always necessary, or doing so may not make sense • Many heap overflows occur in direct control of byte stream • Many others either also allow direct control of the argument stack or both • Plenty of instances where you don’t hit either • Zombie frame is useful for pointer sized writes anywhere in memory • On smaller overflows, fairly typical to corrupt members of object • Many objects destructors use linked lists with unprotected unlinking functionality
PyEval_EvalFrameEx() • Implements state-machine for processing bytecode #define INSTR_OFFSET() ((int))(next_instr – first_instr)) #define NEXTOP() (*next_instr++) #define NEXTARG() (next_instr += 2, (next_instr[-1]<<8) + next_instr[-2]) PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) {[...]first_instr = (unsigned char*) PyString_AS_STRING(co->co_code);[...]next_instr = first_instr + f->f_lasti + 1;stack_pointer = f->f_stacktop;[...]for (;;) { [...] f->f_lasti = INSTR_OFFSET(); [...]opcode = NEXTOP(); oparg = 0; /* allows oparg to be stored in a register because it doesn't have to be remembered across a full loop */ if (HAS_ARG(opcode))oparg = NEXTARG(); […] switch (opcode) {
Important variables • first_instr: • Taken directly from f->f_code->co_code • Determines first instruction in PyCodeObject/bytecode to be executed • Local (stack based) variable in PyEval_EvalFrameEx() • Points to heap data • next_instr: • Derived from first_instr– starts out pointing to same location • Incremented by one to three bytes per opcode • Dictates next instruction in bytecode to be interpreted • Local (stack based) variable in PyEval_EvalFrameEx() • Points to heap data • stack_pointer: • Derived from f->f_stacktop • Determines next argument to given opcode • Makes up data stack • Local (stack based) variable in PyEval_EvalFrameEx() • Points to heap data
Only handguns and tequila allow bigger mistakes faster #define JUMPTO(x) (next_instr = first_instr + (x))while (why != WHY_NOT && f->f_iblock > 0) { PyTryBlock *b = PyFrame_BlockPop(f); assert(why != WHY_YIELD); if (b->b_type == SETUP_LOOP && why == WHY_CONTINUE) { PyFrame_BlockSetup(f, b->b_type, b->b_handler,b->b_level); why = WHY_NOT;JUMPTO(PyInt_AS_LONG(retval)); Py_DECREF(retval); break; } [...] if (b->b_type == SETUP_LOOP && why == WHY_BREAK) { why = WHY_NOT;JUMPTO(b->b_handler); break; } if (b->b_type == SETUP_FINALLY || (b->b_type == SETUP_EXCEPT && why == WHY_EXCEPTION)) { [...] why = WHY_NOT;JUMPTO(b->b_handler); break; }} /* unwind stack */
Other interpreter targets.. • Python’s debugging functionality allows for tracing of the application • Whether the currently executing byte-code is being traced is determined by a member in the stack frame • Tracing allows for calls before opcode execution, function entry, exception handling, et cetera • Many Objects have functionality that can be abused with small amounts of memory corruption • Be creative, they haven’t been beat on like the various libc’s, so they haven’t hardened their implementations
Goal Review • Goal 1: Attack interpreter level metadata • In most cases overwriting a PyCodeObject or the stack_pointer is trivial • In others attacking the zombie frame allows for an interesting and humorous exercise • Python’s exception handling can also be abused • Objects are unhardened • This allows us to bypass many of the hardening functionality in existence, i.e. stack/heap cookies, unlink() hardening, et cetera • Goal 2: Return into byte-code…
opcodes • Python opcodes are a single char • As of 2.5.2 there are 103 opcodes • Opcodes take optional 16-bit modifier • Can be thought of as like a sub-opcode • Arguments/parameters are pointed to by stack_pointer • Thus, parameters need to be placed on the stack first, then the opcode in question called • i.e.: >>> def test(): … print “PsychoAlphaDiscoBetaBioAquaDoLoop” … >>> __import__(‘dis’).dis(test) 2 0 LOAD_CONST 1 ('PsychoAlphaDiscoBetaBioAquaDoLoop') 3 PRINT_ITEM 4 PRINT_NEWLINE 5 LOAD_CONST 0 (None) 8 RETURN_VALUE
Our House.. • Easiest method is to abuse the support for run-time functions (lambda’s) • Opcode is MAKE_FUNCTION case MAKE_FUNCTION: v = POP(); /* code object */ x = PyFunction_New(v, f->f_globals); Py_DECREF(v); /* XXX Maybe this should be a separate opcode? */ if (x != NULL && oparg > 0) { v = PyTuple_New(oparg); if (v == NULL) { Py_DECREF(x); x = NULL; break; } while (--oparg >= 0) { w = POP(); PyTuple_SET_ITEM(v, oparg, w); } err = PyFunction_SetDefaults(x, v); Py_DECREF(v); } PUSH(x); break;
In the middle of our street.. • Python natively generates code that has a STORE_FAST/LOAD_FAST • Don’t think they’re necessary • Pressed for time, so didn’t investigate whether they were necessary or not #define GETLOCAL(i) (fastlocals[i]) #define SETLOCAL(i, value) do { PyObject *tmp = GETLOCAL(i); \ GETLOCAL(i) = value; \ Py_XDECREF(tmp); } while (0) case LOAD_FAST: x = GETLOCAL(oparg); if (x != NULL) { Py_INCREF(x); PUSH(x); goto fast_next_opcode; } […] break; case STORE_FAST: v = POP(); SETLOCAL(oparg, v); goto fast_next_opcode;
Let there be light. • Now that we’ve built the function and setup the argument stack, its just time to call it • Accomplished via CALL_FUNCTION opcode case CALL_FUNCTION: { PyObject **sp; PCALL(PCALL_ALL); sp = stack_pointer;#ifdef WITH_TSC x = call_function(&sp, oparg, &intr0, &intr1);#else x = call_function(&sp, oparg);#endif stack_pointer = sp; PUSH(x); if (x != NULL) continue; break; }
Calling the call_function() function static PyObject * call_function(PyObject ***pp_stack, int oparg#ifdef WITH_TSC , uint64* pintr0, uint64* pintr1#endif ) { int na = oparg & 0xff;int nk = (oparg>>8) & 0xff;int n = na + 2 * nk;PyObject **pfunc = (*pp_stack) - n - 1;PyObject *func = *pfunc;PyObject *x, *w; if (PyCFunction_Check(func) && nk == 0) { [...] if (flags & METH_NOARGS && na == 0) { C_TRACE(x, (*meth)(self,NULL)); } else if (flags & METH_O && na == 1) { PyObject *arg = EXT_POP(*pp_stack);C_TRACE(x, (*meth)(self,arg)); Py_DECREF(arg); } [...]
calling the call_function() function static PyObject * call_function(PyObject ***pp_stack, int oparg #ifdef WITH_TSC , uint64* pintr0, uint64* pintr1 #endif ) { […] if (PyCFunction_Check(func) && nk == 0) { [...] } else { if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) { PyObject *self = PyMethod_GET_SELF(func); [...] } else [...] if (PyFunction_Check(func)) x = fast_function(func, pp_stack, n, na, nk); […]
A final note about call_function() • Often after overflow the code returned into is call_function() • call_function() cleans up the argument stack after calling the function: while ((*pp_stack) > pfunc) { w = EXT_POP(*pp_stack); Py_DECREF(w); PCALL(PCALL_POP); } • Py_DECREF() will almost certainly cause a destructor to get called • If data stack_pointer pointed to was corrupted, this will be the first place its felt • Unless you’re ready for a ret-into-libc type attack, make sure that w points to valid memory that has a value greater than 1
PyCodeObject’s don’t grow on trees you know! • Where to get a PyCodeObject? • Two options, dependant on context: • AppEngine, et al: x = unicode(compile(‘print “zdravstvoyte mir!”, ‘<string>’, ‘exec’)) • x will contain string along the lines of: <code object <module> at 0x4e058f1c37daf18, file “<string>”, line 1> • Now stack_pointer just needs to point at 0x4e058f1c37daf18 • Less controlled environments: • Use compile() to obtain code object • References in header need to be updated-- PyCodeObject->co_code • Requires address space leak
But.. • Returning into bytecode in AppEngine doesn’t make much sense? • Already have control of the interpreter • Return into same restricted environment • Not exactly true-- but ret-into-libc or similar eventually becomes necessary • Ret-into-libc requires address space info • Non-AppEngine attacks require address space info
Tell me about your mother.. • One reason for return into byte-code on AppEngine: PRINT_EXPR opcode case PRINT_EXPR: v = POP();w = PySys_GetObject("displayhook"); if (w == NULL) { PyErr_SetString(PyExc_RuntimeError, "lost sys.displayhook"); err = -1; x = NULL;}if (err == 0) { x = PyTuple_Pack(1, v); if (x == NULL) err = -1;}if (err == 0) { w = PyEval_CallObject(w, x); Py_XDECREF(w); if (w == NULL) err = -1;}Py_DECREF(v);Py_XDECREF(x);break;
All we had to do was ask.. • Typical results of memory leak: ‘\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x80\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xa0\x91\x81\x00\x00\x00\x00\x00\x00\x90\x91\x81\x00\x00\x00\xb0\x91\x81\x12\x1e\x01\x00\x00\x02\[…]’ • Leaking heap addresses in this example: 0x8191XXXX • If stack_pointer is controlled, can point anywhere in the address space • Leak is really only bounded by how much byte-code you can get into the stream • Objects used to verify typing information are statically allocated and use fixed offsets– thus once you know the low-order bytes, you can spot them easily • Problematic because it prints to standard out, which can be redirected but post-overflow is a lot of trouble • Good strategy is to take advantage of fact that we’re dealing with a web-app that can crash an infinite number of times
Loose APIs sink ships. • Python is one of those overly helpful languages • Pretty much all objects can be printed out • When you print an object, the address of the object is leak • Object can also be converted to a string where the same string that gets printed ends up in string, i.e. unicode(compile(…)) gives you the address of a PyCodeObject • Leak PyFrameObject addresses: • sys._getframe() – returns frame object • sys._currentframes() – returns a dictionary with each threads current frame • builtin function id() • Each object has a unique id • This is accomplished by using the address of the object for the id • i.e. id(None) yields address of None object (think about that in context of obtaining type object addresses) • Builtin functions dir() and getattr() • dir() allows you to enumerate attributes of an object • getattr() allows you to obtain its value • Useful when function pointers cannot be avoided
Goals in review • Goal 2: return into interpreter byte-code • Easier to accomplish than initially thought • Process of executing byte-code is more problematic due to type-checks • Successful exploitation absolutely requires address space leaks • Python provides us with nice opcodes to allow leaking • Easiest method is to use MAKE_FUNCTION/CALL_FUNCTION combination as bootstrap mechanism • Restricted interpreters require return-into-C code to break out of • Returning into byte-code provides these advantages: • Non-executable memory is not necessary- byte-code is interpreted not executed • Because its not executed we can use it to dump address space info
2+2 • Overall process: • Obtain address space information via memory corruption and executing PRINT_EXPR opcode • Obtain addresses that were valid at one time and fix up data with addresses • Craft a PyCodeObject either in the address space or in the shellcode, update PyCodeObject header information if injecting into address space • Execute following opcodes: MAKE_FUNCTION/STORE_FAST/LOAD_FAST/CALL_FUNCTION • Return into PyCodeObject • From PyCodeObject return into executable memory
Other available opcodes • LOAD_CONST – loads constant onto argument stack using oparg index into consts member of PyCodeObject • POP_TOP – removes member from top of argument stack, decreases reference • ROT_TWO/ROT_THREE/ROT_FOUR – rotates position of argument stack, moving 2, 3 or 4 arguments around • DUP_TOPX – duplicates either 2 or 3 pointers on argument stack • STORE_SLICE+X – some code paths do not have type checks and instead create a new object, allows object creation • PRINT_ITEM_TO – allows redirection of output, pops output data from variable stack, falls through to PRINT_ITEM which may redirect to stdout • LOAD_LOCALS – places f->f_locals onto argument stack, great opcode to prefix PRINT_X opcodes • YIELD_VALUE – takes return value from argument stack, sets f->f_stacktop to point to stack_ponter • POP_BLOCK – Obtains PyTryBlock from argument stack, decrements references for each record • STORE_GLOBAL / LOAD_GLOBAL – same as with other STORE_X/LOAD_X opcodes, except operates on globals
More opcodes • JUMP_ABSOLUTE – like JUMP_FORWARD except is not relative to first_instr • FOR_ITER – makes function pointer call from pointer retrieved from argument stack, good once address space layout is known • EXTENDED_ARGS – advances to next opcode, obtains another 16-bit oparg and combines it with existing 16-bit oparg, combined with JUMP_ABSOLUTE allows byte-code to exist anywhere (on 32-bit machines) • LOAD_CLOSURE – places pointer on argument stack from different section of heap memory • BUILD_X – several opcodes, allows for creation of Tuples, Lists, Maps and Slices • JUMP_FORWARD – advances position in bytecode by oparg bytes • Other opcodes perform type checks or have callbacks • Many of the ones with type checks can be coaxed into usage by first building an object via one of the BUILD_X opcodes • Once address space is know, opcodes with callbacks are not dangerous
python.fin() • Bugs in Python exist and are easy to find • Data structures and general metadata is easy to abuse • Byte-code is position independent and thus easy to make • Because of its PIC nature– argument stack exists elsewhere • Ownership can be transferred with one or the other, but made more difficult • Hardest part of returning into byte-code is ASLR • Python is really helpful there.