Можно ли взломать функцию печати Python?

151

Примечание. Этот вопрос носит исключительно информационный характер. Мне интересно посмотреть, как глубоко во внутренностях Python можно пойти с этим.

Не так давно внутри определенного вопроса началось обсуждение того, можно ли изменить строки, переданные в операторы print, после / во время вызова print. Например, рассмотрим функцию:

def print_something():
    print('This cat was scared.')

Теперь, когда printвыполняется, то вывод на терминал должен отображать:

This dog was scared.

Обратите внимание, слово «кошка» было заменено словом «собака». Что-то где-то каким-то образом смогло изменить эти внутренние буферы, чтобы изменить то, что было напечатано. Предположим, что это делается без явного разрешения автора исходного кода (следовательно, взлома / угона).

Этот комментарий от мудрого @abarnert, в частности, заставил меня задуматься:

Есть несколько способов сделать это, но все они очень уродливы, и никогда не должны быть сделаны. Наименее уродливый способ - это заменить codeобъект внутри функции на другой с другим co_consts списком. Далее, вероятно, доступ к C API для доступа к внутреннему буферу str. [...]

Так что, похоже, это действительно возможно.

Вот мой наивный способ решения этой проблемы:

>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.

Конечно, execэто плохо, но это на самом деле не отвечает на вопрос, потому что на самом деле ничего не изменяет во время вызова / после print вызова.

Как бы это было сделано, как объяснил @abarnert?

cs95
источник
3
Кстати, внутреннее хранилище для целых намного проще, чем строки, и даже больше. И, в качестве бонуса, гораздо более очевидно, почему плохая идея изменить значение 42на, 23чем плохая идея изменить значение "My name is Y"на "My name is X".
Абарнерт

Ответы:

243

Во-первых, на самом деле есть гораздо менее хакерский способ. Все, что мы хотим сделать, это изменить то print, что печатает, верно?

_print = print
def print(*args, **kw):
    args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
            for arg in args)
    _print(*args, **kw)

Или, аналогично, вы можете sys.stdoutвместо monkeypatch print.


Кроме того, нет ничего плохого в exec … getsource …идее. Ну, конечно, с этим много чего не так, но здесь меньше того, что следует ...


Но если вы хотите изменить константы кода объекта функции, мы можем это сделать.

Если вы действительно хотите поэкспериментировать с объектами кода по-настоящему, вы должны использовать библиотеку, например bytecode(когда она закончится) или byteplay(до тех пор, или для более старых версий Python), вместо того, чтобы делать это вручную. Даже для чего-то такого тривиального CodeTypeинициализатор - это боль; если вам действительно нужно что-то вроде ремонта lnotab, то только сумасшедший сделает это вручную.

Кроме того, само собой разумеется, что не все реализации Python используют объекты кода в стиле CPython. Этот код будет работать в CPython 3.7, и, вероятно, все версии вернутся по крайней мере до 2.2 с некоторыми незначительными изменениями (и не с хакерскими кодами, а с такими вещами, как выражения генератора), но он не будет работать с любой версией IronPython.

import types

def print_function():
    print ("This cat was scared.")

def main():
    # A function object is a wrapper around a code object, with
    # a bit of extra stuff like default values and closure cells.
    # See inspect module docs for more details.
    co = print_function.__code__
    # A code object is a wrapper around a string of bytecode, with a
    # whole bunch of extra stuff, including a list of constants used
    # by that bytecode. Again see inspect module docs. Anyway, inside
    # the bytecode for string (which you can read by typing
    # dis.dis(string) in your REPL), there's going to be an
    # instruction like LOAD_CONST 1 to load the string literal onto
    # the stack to pass to the print function, and that works by just
    # reading co.co_consts[1]. So, that's what we want to change.
    consts = tuple(c.replace("cat", "dog") if isinstance(c, str) else c
                   for c in co.co_consts)
    # Unfortunately, code objects are immutable, so we have to create
    # a new one, copying over everything except for co_consts, which
    # we'll replace. And the initializer has a zillion parameters.
    # Try help(types.CodeType) at the REPL to see the whole list.
    co = types.CodeType(
        co.co_argcount, co.co_kwonlyargcount, co.co_nlocals,
        co.co_stacksize, co.co_flags, co.co_code,
        consts, co.co_names, co.co_varnames, co.co_filename,
        co.co_name, co.co_firstlineno, co.co_lnotab,
        co.co_freevars, co.co_cellvars)
    print_function.__code__ = co
    print_function()

main()

Что может пойти не так со взломом объектов кода? В основном это просто ошибки segfaults, RuntimeErrorкоторые поглощают весь стек, более обычные RuntimeErrors, которые можно обработать, или значения мусора, которые, вероятно, просто вызовут a TypeErrorили AttributeErrorкогда вы попытаетесь их использовать. Например, попробуйте создать объект кода, в RETURN_VALUEкотором ничего не стоит в стеке (байт-код b'S\0'для 3.6+, b'S'ранее), или с пустым кортежем, co_constsкогда LOAD_CONST 0в байт-коде есть a , или с varnamesуменьшенным на 1, так что наивысший LOAD_FASTфактически загружает freevar. / Cellvar Cell. Для некоторого реального удовольствия, если вы поймете lnotabнеправильно, ваш код будет только segfault при запуске в отладчике.

Использование bytecodeили byteplayне защитит вас от всех этих проблем, но у них есть некоторые базовые проверки работоспособности и хорошие помощники, которые позволяют вам делать такие вещи, как вставка фрагмента кода, и позволяют ему беспокоиться об обновлении всех смещений и меток, чтобы вы могли ' неправильно, и так далее. (Кроме того, вам не нужно вводить этот смешной 6-строчный конструктор и отлаживать глупые опечатки, возникающие при этом.)


Теперь перейдем к # 2.

Я упоминал, что объекты кода неизменны. И, конечно же, константы - это кортеж, поэтому мы не можем изменить это напрямую. И вещь в кортеже const - это строка, которую мы также не можем изменить напрямую. Вот почему я должен был создать новую строку, чтобы создать новый кортеж для создания нового объекта кода.

Но что, если бы вы могли изменить строку напрямую?

Ну, достаточно глубоко под прикрытием, все это просто указатель на некоторые данные C, верно? Если вы используете CPython, есть API C для доступа к объектам , и вы можете использовать его ctypesдля доступа к этому API из самого Python, что является настолько ужасной идеей, что они помещают ее pythonapiпрямо в ctypesмодуль stdlib . :) Самый важный трюк, который вам нужно знать, id(x)это фактический указатель xв памяти (как int).

К сожалению, C API для строк не позволит нам безопасно попасть во внутреннее хранилище уже замороженной строки. Так что винт, давайте просто прочитаем заголовочные файлы и сами найдем это хранилище.

Если вы используете CPython 3.4 - 3.7 (он отличается от старых версий и знает, что будет в будущем), строковый литерал из модуля, который состоит из чистого ASCII, будет храниться в компактном формате ASCII, что означает структуру заканчивается рано, и буфер байтов ASCII немедленно следует в памяти. Это сломается (как, вероятно, в segfault), если вы поместите не-ASCII-символ в строку или некоторые виды не-литеральных строк, но вы можете прочитать другие 4 способа доступа к буферу для различных типов строк.

Чтобы упростить задачу, я использую superhackyinternalsпроект с моего GitHub. (Это намеренно не устанавливается в pip, потому что вы действительно не должны использовать это, кроме как для экспериментов с вашей локальной сборкой интерпретатора и т.п.).

import ctypes
import internals # https://github.com/abarnert/superhackyinternals/blob/master/internals.py

def print_function():
    print ("This cat was scared.")

def main():
    for c in print_function.__code__.co_consts:
        if isinstance(c, str):
            idx = c.find('cat')
            if idx != -1:
                # Too much to explain here; just guess and learn to
                # love the segfaults...
                p = internals.PyUnicodeObject.from_address(id(c))
                assert p.compact and p.ascii
                addr = id(c) + internals.PyUnicodeObject.utf8_length.offset
                buf = (ctypes.c_int8 * 3).from_address(addr + idx)
                buf[:3] = b'dog'

    print_function()

main()

Если вы хотите поиграть с этим, intнамного проще под покровом, чем str. И гораздо проще угадать, что можно сломать, изменив значение 2на 1, верно? На самом деле, забудьте о воображении, давайте просто сделаем это (используя типы superhackyinternalsснова):

>>> n = 2
>>> pn = PyLongObject.from_address(id(n))
>>> pn.ob_digit[0]
2
>>> pn.ob_digit[0] = 1
>>> 2
1
>>> n * 3
3
>>> i = 10
>>> while i < 40:
...     i *= 2
...     print(i)
10
10
10

... притворимся, что кодовое поле имеет полосу прокрутки бесконечной длины.

Я попробовал то же самое в IPython, и в первый раз, когда я попытался оценить 2в приглашении, он вошел в какой-то непрерывный бесконечный цикл. Предположительно он использует число 2для чего-то в цикле REPL, в то время как стандартный интерпретатор - нет?

abarnert
источник
11
@ cᴏʟᴅsᴘᴇᴇᴅ Код-munging это возможно разумно Python, хотя обычно вы только хотите , чтобы коснуться объектами коды для гораздо лучше причин (например, запуск байткода через пользовательский оптимизатор). С PyUnicodeObjectдругой стороны,
доступ
4
Ваш первый фрагмент кода повышается NameError: name 'arg' is not defined. Возможно , вы имели в виду: args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]? , Возможно , лучший способ , чтобы написать это было бы: args = [str(arg).replace('cat', 'dog') for arg in args]. Другой, еще короче, вариант: args = map(lambda a: str(a).replace('cat', 'dog'), args). Это имеет дополнительное преимущество, которое argsявляется ленивым (что также может быть достигнуто путем замены приведенного выше понимания списка генератором - *argsработает в любом случае).
Константин
1
@ cᴏʟᴅsᴘᴇᴇᴅ Да, IIRC Я только использую PyUnicodeObjectопределение структуры, но копирование этого в ответ, я думаю, только мешает , и я думаю, что комментарии readme и / или источника на superhackyinternalsсамом деле объясняют, как получить доступ к буферу (по крайней мере, достаточно хорошо, чтобы напомнить мне в следующий раз, когда я забочусь; не уверен, будет ли этого достаточно для кого-то еще…), в который я не хотел входить. Соответствующая часть - как добраться от живого объекта Python до его PyObject *через ctypes. (И, возможно, имитирует указатель арифметики, избегая автоматическогоchar_p преобразований и т. Д.)
abarnert
1
@ jpmc26 Не думаю, что вам нужно делать это перед импортом модулей, если вы делаете это до того, как они будут напечатаны. Модули будут выполнять поиск имени каждый раз, если они явно не связаны printс именем. Вы также можете связать имя printдля них: import yourmodule; yourmodule.print = badprint.
Leewz
1
@abarnert: я заметил, что вы часто предупреждали об этом (например, «вы никогда не хотите делать это на самом деле» , «почему плохая идея менять значение» и т. д.). Не совсем понятно, что может пойти не так (сарказм), не могли бы вы остановиться на этом подробнее? Это может помочь тем, кто испытывает желание слепо попробовать.
L'L'л
37

Обезьяна-патч print

printявляется встроенной функцией, поэтому она будет использовать printфункцию, определенную в builtinsмодуле (или __builtin__в Python 2). Поэтому, когда вы хотите изменить или изменить поведение встроенной функции, вы можете просто переназначить имя в этом модуле.

Этот процесс называется monkey-patching.

# Store the real print function in another variable otherwise
# it will be inaccessible after being modified.
_print = print  

# Actual implementation of the new print
def custom_print(*args, **options):
    _print('custom print called')
    _print(*args, **options)

# Change the print function globally
import builtins
builtins.print = custom_print

После этого каждый printвызов будет проходить custom_print, даже если он printнаходится во внешнем модуле.

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

_print = print  

def custom_print(*args, **options):
    # Get the desired seperator or the default whitspace
    sep = options.pop('sep', ' ')
    # Create the final string
    printed_string = sep.join(args)
    # Modify the final string
    printed_string = printed_string.replace('cat', 'dog')
    # Call the default print function
    _print(printed_string, **options)

import builtins
builtins.print = custom_print

И действительно, если вы запустите

>>> def print_something():
...     print('This cat was scared.')
>>> print_something()
This dog was scared.

Или, если вы пишете это в файл:

test_file.py

def print_something():
    print('This cat was scared.')

print_something()

и импортировать его:

>>> import test_file
This dog was scared.
>>> test_file.print_something()
This dog was scared.

Так что это действительно работает как задумано.

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

import builtins

class ChangePrint(object):
    def __init__(self):
        self.old_print = print

    def __enter__(self):
        def custom_print(*args, **options):
            # Get the desired seperator or the default whitspace
            sep = options.pop('sep', ' ')
            # Create the final string
            printed_string = sep.join(args)
            # Modify the final string
            printed_string = printed_string.replace('cat', 'dog')
            # Call the default print function
            self.old_print(printed_string, **options)

        builtins.print = custom_print

    def __exit__(self, *args, **kwargs):
        builtins.print = self.old_print

Поэтому, когда вы запускаете, это зависит от контекста, который печатается:

>>> with ChangePrint() as x:
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.

Так вот, как вы могли бы «взломать» printпутем исправления обезьян.

Изменить цель вместо print

Если вы посмотрите на подпись printвы заметите fileаргумент, который sys.stdoutпо умолчанию. Обратите внимание, что это динамический аргумент по умолчанию (он действительно отображается при sys.stdoutкаждом вызове print), а не обычный аргумент по умолчанию в Python. Таким образом, если вы измените, sys.stdout printто на самом деле печатать на другую цель будет еще удобнее, так как Python также предоставляет redirect_stdoutфункцию (начиная с Python 3.4, но легко создать эквивалентную функцию для более ранних версий Python).

Недостатком является то, что он не будет работать для printутверждений, которые не печатаются, sys.stdoutи что создание собственного stdoutне очень просто.

import io
import sys

class CustomStdout(object):
    def __init__(self, *args, **kwargs):
        self.current_stdout = sys.stdout

    def write(self, string):
        self.current_stdout.write(string.replace('cat', 'dog'))

Однако это также работает:

>>> import contextlib
>>> with contextlib.redirect_stdout(CustomStdout()):
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.

Резюме

@Abarnet уже упоминал о некоторых из этих моментов, но я хотел бы изучить эти варианты более подробно. Особенно о том, как изменить его в разных модулях (используя builtins/ __builtin__) и как сделать это изменение только временным (используя контекстные менеджеры).

MSeifert
источник
4
Да, самая близкая вещь к этому вопросу, которую каждый должен когда-либо на самом деле хотеть сделать redirect_stdout, так это хорошо иметь четкий ответ, который приводит к этому.
Абарнерт
6

Простой способ захватить весь вывод из print функции и затем обработать ее - изменить поток вывода на что-то другое, например, файл.

Я буду использовать PHPсоглашения об именовании ( ob_start , ob_get_contents , ...)

from functools import partial
output_buffer = None
print_orig = print
def ob_start(fname="print.txt"):
    global print
    global output_buffer
    print = partial(print_orig, file=output_buffer)
    output_buffer = open(fname, 'w')
def ob_end():
    global output_buffer
    close(output_buffer)
    print = print_orig
def ob_get_contents(fname="print.txt"):
    return open(fname, 'r').read()

Использование:

print ("Hi John")
ob_start()
print ("Hi John")
ob_end()
print (ob_get_contents().replace("Hi", "Bye"))

Будет печатать

Привет Джон Пока Джон

Ури Горен
источник
5

Давайте совместим это с самоанализом кадра!

import sys

_print = print

def print(*args, **kw):
    frame = sys._getframe(1)
    _print(frame.f_code.co_name)
    _print(*args, **kw)

def greetly(name, greeting = "Hi")
    print(f"{greeting}, {name}!")

class Greeter:
    def __init__(self, greeting = "Hi"):
        self.greeting = greeting
    def greet(self, name):
        print(f"{self.greeting}, {name}!")

Вы найдете этот трюк в начале каждого приветствия с вызывающей функцией или методом. Это может быть очень полезно для регистрации или отладки; тем более, что он позволяет «перехватывать» операторы печати в стороннем коде.

Рафаэль Дера
источник