Статьи

Взлом в Python Объекты Внутренние


Вы знаете, Python представляет каждый объект, используя низкоуровневую структуру C API
PyObject (или
PyVarObject для объектов переменного размера), так что, конкретно, вы можете привести любой указатель объекта Python к этому типу; это наследование создается вручную, каждый новый объект должен иметь ведущий макрос
PyObject_HEAD, который определяет
заголовок
PyObject для объекта. Структура
PyObject объявлена ​​в
файле Include / object.h как:

Первоначально Автор Кристиан С. Пероне

typedef struct _object {
    PyObject_HEAD
} PyObject;

и макрос PyObject_HEAD определяется как:

#define PyObject_HEAD                   \
    _PyObject_HEAD_EXTRA                \
    Py_ssize_t ob_refcnt;               \
    struct _typeobject *ob_type;

… С двумя полями (
забудьте _PyObject_HEAD_EXTRA , он используется только для функции отладки трассировки ) с
именами ob_refcnt и
ob_type , представляющими счетчик ссылок для объекта и тип объекта. Я знаю, что вы можете использовать
sys.getrefcount для подсчета ссылок на объект, но взлом памяти объекта с помощью ctypes намного более эффективен, поскольку вы можете получить содержимое любого поля объекта (в случаях, когда вы этого не делаете есть собственный API для этого), я покажу больше примеров позже, но давайте сосредоточимся на поле подсчета ссылок объекта.

Получение количества ссылок (ob_refcnt)

Итак, в Python у нас есть встроенная функция id () , эта функция возвращает идентификатор объекта, но, посмотрев на его определение по реализации CPython, вы заметите, что id () возвращает адрес памяти объекта см. источник в Python / bltinmodule.c :

static PyObject *
builtin_id(PyObject *self, PyObject *v)
{
    return PyLong_FromVoidPtr(v);
}

… Функция PyLong_FromVoidPtr возвращает длинный объект Python из пустого указателя. Итак, в CPython это значение является адресом объекта в памяти, как показано ниже:

>>> value = 666
>>> hex(id(value))
'0x8998e50' # memory address of the 'value' object

Теперь, когда у нас есть адрес памяти объекта, мы можем использовать модуль Python ctypes для подсчета ссылок, используя атрибут ob_refcnt , вот код, необходимый для этого:

>>> value = 666
>>> value_address = id(value)
>>>
>>> ob_refcnt = ctypes.c_long.from_address(value_address)
>>> ob_refcnt
c_long(1)

Здесь я получаю целочисленное значение из атрибута ob_refcnt объекта PyObject в памяти. Давайте добавим новую ссылку для созданного нами объекта «value», а затем снова проверим счетчик ссылок:

>>> value_ref = value
>>> id(value_ref) == id(value)
True
>>> ob_refcnt
c_long(2)

Обратите внимание, что счетчик ссылок был увеличен на 1 из-за новой переменной ссылки под названием ‘value_ref’.

Состояние интернированных строк (ob_sstate)

Теперь подсчет ссылок был даже не смешным, для этого у нас уже был API sys.getrefcount , но как насчет интернированного состояния строк ? Чтобы избежать создания различных выделений для одной и той же строки (и для ускорения сравнений), Python использует словарь, который работает как «кеш» для строк, этот словарь определен в Objects / stringobject.c :

/* This dictionary holds all interned strings.  Note that references to
strings in this dictionary are *not* counted in the string's ob_refcnt.
When the interned string reaches a refcnt of 0 the string deallocation
function will delete the reference from this dictionary.

Another way to look at this is that to say that the actual reference
count of a string is:  s->ob_refcnt + (s->ob_sstate?2:0)
*/
static PyObject *interned;

Я также скопировал здесь комментарий о словаре, потому что интересно отметить, что строки в словаре не учитываются в строке ob_refcnt .

Итак, интернированное состояние строкового объекта сохраняется в атрибуте ob_sstate строкового объекта, давайте посмотрим определение строкового объекта Python:

typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;
    int ob_sstate;
    char ob_sval[1];

    /* Invariants:
    *     ob_sval contains space for 'ob_size+1' elements.
    *     ob_sval[ob_size] == 0.
    *     ob_shash is the hash of the string or -1 if not computed yet.
    *     ob_sstate != 0 iff the string object is in stringobject.c's
    *       'interned' dictionary; in this case the two references
    *       from 'interned' to this object are *not counted* in ob_refcnt.
    */
} PyStringObject;

Как вы можете заметить, строковые объекты наследуются от макроса PyObject_VAR_HEAD, который определяет другой атрибут заголовка, давайте посмотрим определение, чтобы получить полное представление о структуре:

#define PyObject_VAR_HEAD               \
    PyObject_HEAD                       \
    Py_ssize_t ob_size; /* Number of items in variable part */

PyObject_VAR_HEAD макрос добавляет еще одно поле , называемое ob_size , который является количество элементов на переменной части объекта Python (то есть количество элементов на объект списка). Поэтому, прежде чем перейти к полю ob_sstate , нам нужно сместить наше смещение, чтобы пропустить поля ob_refcnt (long) , ob_type (void *) (из PyObject_HEAD ), поле ob_size (long) (из PyObject_VAR_HEAD ) и поле ob_shash (long ) из PyStringObject . Конкретно, нам нужно пропустить это смещение (3 поля с размером long и одно поле с размером void *байт)

>>> ob_sstate_offset = ctypes.sizeof(ctypes.c_long)*3 + ctypes.sizeof(ctypes.c_voidp)
>>> ob_sstate_offset
16

Теперь давайте подготовим два случая, один из которых, как мы знаем, не интернирован, а другой, безусловно, интернирован, а затем заставим интернированное состояние другой не интернированной строки проверить результат атрибута ob_sstate :

>>> a = "lero"
>>> b = "".join(["l", "e", "r", "o"])
>>> ctypes.c_long.from_address(id(a) + ob_sstate_offset)
c_long(1)
>>> ctypes.c_long.from_address(id(b) + ob_sstate_offset)
c_long(0)
>>> ctypes.c_long.from_address(id(intern(b)) + ob_sstate_offset)
c_long(1)

Обратите внимание, что внутреннее состояние для объекта «a» равно 1, а для объекта «b» — 0. После принудительного определения внутреннего состояния переменной «b» мы можем видеть, что поле ob_sstate изменилось на 1.

Изменение внутренних состояний (режим зла)

Теперь предположим, что мы хотим изменить некоторое внутреннее состояние объекта Python через интерпретатор. Попробуем изменить значение объекта int. Объекты Int определены в файле Include / intobject.h :

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

Как видите, внутреннее значение int хранится в поле ob_ival , для его изменения нам просто нужно пропустить ob_refcnt (long) и ob_type (void *) из PyObject_HEAD :

>>> value = 666
>>> ob_ival_offset = ctypes.sizeof(ctypes.c_long) + ctypes.sizeof(ctypes.c_voidp)
>>> ob_ival = ctypes.c_int.from_address(id(value)+ob_ival_offset)
>>> ob_ival
c_long(666)
>>> ob_ival.value = 8
>>> value
8

И это все, мы изменили значение int прямо в памяти.

Надеюсь, вам понравилось, вы можете поиграть с множеством других объектов Python, таких как списки и дикты, обратите внимание, что этот метод предназначен просто для того, чтобы показать, как объекты Python структурированы в памяти и как вы можете изменить их с помощью собственного API, но очевидно, вы не должны использовать это, чтобы изменить значение целых чисел.


Обновление 29.11.11
: вы не должны делать такие вещи в своем производственном коде или что-то в этом роде, в этом посте я делаю ленивые предположения о деталях арки, таких как размеры примитивов и т. Д. Будьте осторожны .

Источник: http://pyevolve.sourceforge.net/wordpress/?p=2171