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

Примечание. Этот вопрос носит исключительно информационный характер. Мне интересно посмотреть, как глубоко во внутренностях 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?

4 ответа

Решение

Во-первых, на самом деле есть гораздо менее хакерский способ. Все, что мы хотим сделать, это изменить то, что 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)

Или, аналогично, вы можете monkeypatch sys.stdout вместо 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()

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

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


Теперь на #2.

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

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

Ну, достаточно глубоко под прикрытием, все это просто указатель на некоторые данные C, верно? Если вы используете CPython, есть C API для доступа к объектам, и вы можете использоватьctypes получить доступ к этому API из самого Python, что является настолько ужасной идеей, что ониpythonapi прямо в stdlib's ctypes модуль.:) Самый важный трюк, который вам нужно знать, это то, что 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, в то время как фондовый интерпретатор - нет?

Обезьяна-патч 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.stdoutprint будет на самом деле печатать на другую цель еще удобнее, что 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__) и как сделать это изменение только временным (с помощью контекстных менеджеров).

Простой способ захватить весь вывод из 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"))

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

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

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

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}!")

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

Другие вопросы по тегам