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
53
54
55
56
57
63
64
65
66
72
73
74
75
PyObject *
PyString_FromStringAndSize(const char *str, Py_ssize_t size)
{
register PyStringObject *op;
assert(size >= 0);
if (size == 0 && (op = nullstring) != NULL) {
[…]
}
if (size == 1 && str != NULL &&
(op = characters[*str & UCHAR_MAX]) != NULL)
{
[…]
}
/* Inline PyObject_NewVar */
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 ()
Bytecode flow overview
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\x0
0\x01\x00\x00\x00\xa0\x91\x81\x00\x00\x00\x00\x00\x00\x90\x91\x81\x
00\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 typechecks
▫ 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/CAL
L_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.
What about PERL?
□ PERL has bugs too
□ Reading PERL is an exercise in patience
▫ Friend: ‘I still maintain that PERL was not
written.. It was found.. On a crashed UFO’
□ Yeah, it is that bad
□ Be careful when looking into the abyss..
Ugh, wtf?
□ Just an example (from 5.8.8):
int
perl_parse(PTHXx_, XSINIT_t xsinit, int argc, char **argv, char **env)
{
[…]
#ifdef PERL_FLEXIBLE_EXCEPTIONS
CALLPROTECT(aTHX_ pcur_env, &ret,
MEMBER_TO_FPTR(S_vparse_body),
env, xsinit);
#else
JMPENV_PUSH(ret)
#endif
Are you kidding me?
□ If PERL_FLEXIBLE_EXCEPTIONS is defined…
#define CALLPROTECT CALL_FPTR(PL_protect)
#define CALL_FPTR(fptr) (*fptr)
#define PL_protect (aTHX->Tprotect)
#define aTHX PERL_GET_THX
#define aTHX_ aTHX,
#ifdef USE_5005THREADS
#define PERL_GET_THX ((struct perl_thread *)PERL_GET_CONTEXT)
#else
#ifdef MULTIPLICITY
#define PERL_GET_THX ((PerlInterpreter *)PERL_GET_CONTEXT)
#endif
#endif
Larry Wall is trying to kill me
□ And some more…
#ifndef PERL_GET_CONTEXT
#define PERL_GET_CONTEXT ((void *)NULL)
#define PERL_GET_CONTEXT Perl_get_context()
#define MEMBER_TO_FPTR(name) name
□ So, on conditional compilation expands to:
perl_get_context()->Tprotect((struct perl_thread *)Perl_get_context(),
pcur_env,
&ret,
S_Vparse_body,
env,
xsinit);
You outsourced to the guy who wrote
procmail didn’t you?!
□
If PERL_FLEXIBLE_EXCEPTIONS is not defined…
#define JMPENV_PUSH(v) JMPENV_PUSH_ENV(*(JMPENV*)pcur_env, v)
#define JMPENV_PUSH_ENV(ce, v)
\
STMT_START {
\
if (!(ce).je_noset) {
\
DEBUG_1(Perl_deb(aTHX_ “Setting up jumplevel %p, was %p\n”, \
cl, PL_top_env));
\
JMPENV_PUSH_INIT_ENV(ce, NULL) \
EXCEPT_SET_ENV(ce, PerlProc_setjmp((ce).je_buf,
SCOPE_SAVES_SIGNAL_MASK));
\
(ce).je_noset = 1;
\
}
\
else
\
EXCEPT_SET_ENV(ce, 0);
\
JMPENV_POST_CATCH_ENV(ce);
\
(v) = EXCEPT_GET_ENV(ce);
\
} STMT_END
sigh
□ Does do { […] } while(0) not work somewhere??
#if defined(__GNUC__) &&
!defined(PERL_GCC_BRACE_GROUPS_FORBIDDEN) && !defined
(__cplusplus)
#define STMT_START (void) {
#define STMT_END }
#else
#if (VOIDFLAGS) && (defined(sun) || defined(__sun__)) &&
!defined(__GNUC__)
#define STMT_START if (1)
#define STMT_END else (void)0
#else
#define STMT_START do
#define STMT_END while (0)
#endif
#endif
…
□
We’re just gonna skip expanding Debug_1() and guess that it probably deals with
debugging output…
#define JMPENV_PUSH_INIT_ENV(ce, THROWFUNC)
STMT_START {
(ce).je_throw = (THROWFUNC);
(ce).je_ret = -1;
(ce).je_mustcatch = FALSE;
(ce).je_prev = PL_top_env;
PL_TOP_env = &(ce);
OP_REG_TO_MEM;
} STMT_END
#define PL_top_env (aTHX->Ttop_env)
#ifdef OP_IN_REGISTER
#define OP_REG_TO_MEM PL_opsave = op
[…]
#else
#define OP_REG_TO_MEM NOOP
#define PL_opsave (aTHX->Top_save)
\
\
\
\
\
\
\
\
Anyone wanna bet how many slides it
takes to explain one line of PERL?
□ Almost there …
#define EXCEPT_SET_ENV(ce, v) ((ce).je_ret =
(v))
#define JMPENV_POST_CATCH_ENV(ce) \
STMT_START {
\
OP_MEM_TO_REG; \
PL_top_env = &(ce); \
} STMT_END
#define EXCEPT_GET_ENV(ce) ((ce).je_ret)
Huzzah! One line expanded!
□ seven slides later..
□ Two of over a dozen possible conditional
compilations were explored
□ We’ve successfully decoded *one line* of perl
□ Except we haven’t, now we have to find out
where the function pointers get initialized…
□ And of course, discover where the heck op came
from
□ I don’t think an hour is long enough to cover just
PERL, much less PERL and Python
0xbadc0ded
□
Undisclosed & unpatched
do {
i_img *im = read_one_tiff(tif, 0);
if (!im)
break;
if (++*count > result_alloc) {
if (result_alloc == 0) {
result_alloc = 5;
results = mymalloc(result_alloc * sizeof(i_img *));
}
else {
i_img **newresults;
result_alloc *= 2;
newresults = myrealloc(results, result_alloc * sizeof(i_img *));
if (!newresults) {
i_img_destroy(im); /* don't leak it */
break;
}
results = newresults;
}
}
results[*count-1] = im;
} while (TIFFSetDirectory(tif, ++dirnum));
0xbadc0ded
□
Undisclosed & unpatched
static i_img *
read_one_rgb_tiled(TIFF *tif, int width, int height, int allow_incomplete) {
[...]
uint32* raster = NULL;
[...]
uint32 tile_width, tile_height;
i_color *line;
[...]
TIFFGetField(tif, TIFFTAG_TILEWIDTH, &tile_width);
TIFFGetField(tif, TIFFTAG_TILELENGTH, &tile_height);
[...]
raster = (uint32*)_TIFFmalloc(tile_width * tile_height * sizeof (uint32));
[...]
line = mymalloc(tile_width * sizeof(i_color));
for( row = 0; row < height; row += tile_height ) {
for( col = 0; col < width; col += tile_width ) {
/* Read the tile into an RGBA array */
if (myTIFFReadRGBATile(&img, col, row, raster)) {
[...]
0xbadc0ded
□
Undisclosed & unpatched
i_img *
read_one_rgb_lines(TIFF *tif, int width, int height, int allow_incomplete) {
i_img *im;
uint32* raster = NULL;
uint32 rowsperstrip, row;
i_color *line_buf;
int alpha_chan;
int rc;
im = make_rgb(tif, width, height, &alpha_chan);
[...]
rc = TIFFGetField(tif, TIFFTAG_ROWSPERSTRIP, &rowsperstrip);
[...]
raster = (uint32*)_TIFFmalloc(width * rowsperstrip * sizeof (uint32));
[...]
line_buf = mymalloc(sizeof(i_color) * width);
for( row = 0; row < height; row += rowsperstrip ) {
uint32 newrows, i_row;
if (!TIFFReadRGBAStrip(tif, row, raster)) {
Descargar

Slide 1