Apakah mungkin untuk "meretas" fungsi cetak Python?

151

Catatan: Pertanyaan ini hanya untuk tujuan informasi. Saya tertarik untuk melihat seberapa dalam ke internal Python adalah mungkin untuk pergi dengan ini.

Belum lama ini, sebuah diskusi dimulai di dalam pertanyaan tertentu mengenai apakah string yang diteruskan ke pernyataan cetak dapat dimodifikasi setelah / selama panggilan printtelah dibuat. Misalnya, perhatikan fungsinya:

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

Sekarang, ketika printdijalankan, maka output ke terminal akan ditampilkan:

This dog was scared.

Perhatikan kata "kucing" telah digantikan oleh kata "anjing". Sesuatu di suatu tempat entah bagaimana dapat memodifikasi buffer internal untuk mengubah apa yang dicetak. Anggap ini dilakukan tanpa izin eksplisit dari pembuat kode asli (karenanya, peretasan / pembajakan).

Ini komentar dari @abarnert bijak, khususnya, membuat saya berpikir:

Ada beberapa cara untuk melakukan itu, tetapi semuanya sangat jelek, dan tidak boleh dilakukan. Cara paling jelek adalah mungkin mengganti codeobjek di dalam fungsi dengan satu dengan co_consts daftar yang berbeda . Selanjutnya mungkin menjangkau ke dalam API C untuk mengakses buffer internal str. [...]

Jadi, sepertinya ini benar-benar mungkin.

Inilah cara naif saya dalam mendekati masalah ini:

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

Tentu saja execitu buruk, tetapi itu tidak benar-benar menjawab pertanyaan, karena itu tidak benar-benar mengubah apa pun ketika / setelah print dipanggil.

Bagaimana ini bisa dilakukan seperti yang dijelaskan @abarnert?

cs95
sumber
3
Omong-omong, penyimpanan internal untuk int jauh lebih sederhana daripada string, dan bahkan lebih mengapung. Dan, sebagai bonus, jauh lebih jelas mengapa itu adalah ide yang buruk untuk mengubah nilai 42menjadi 23daripada mengapa itu adalah ide yang buruk untuk mengubah nilai "My name is Y"menjadi "My name is X".
abarnert

Jawaban:

244

Pertama, sebenarnya ada cara yang jauh lebih tidak retas. Yang ingin kami lakukan adalah mengubah printcetakan apa , bukan?

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

Atau, sama, Anda dapat monkeypatch sys.stdoutbukan print.


Juga, tidak ada yang salah dengan exec … getsource …idenya. Yah, tentu saja ada banyak yang salah dengan itu, tetapi kurang dari apa yang terjadi di sini ...


Tetapi jika Anda ingin memodifikasi konstanta kode objek fungsi, kita bisa melakukannya.

Jika Anda benar-benar ingin bermain-main dengan objek kode secara nyata, Anda harus menggunakan perpustakaan seperti bytecode(ketika selesai) atau byteplay(sampai saat itu, atau untuk versi Python yang lebih lama) daripada melakukannya secara manual. Bahkan untuk sesuatu yang sepele ini, CodeTypepenginisialisasi adalah rasa sakit; jika Anda benar-benar perlu melakukan hal-hal seperti memperbaiki lnotab, hanya orang gila yang akan melakukannya secara manual.

Juga, tidak perlu dikatakan bahwa tidak semua implementasi Python menggunakan objek kode gaya CPython. Kode ini akan bekerja di CPython 3.7, dan mungkin semua versi kembali ke setidaknya 2.2 dengan beberapa perubahan kecil (dan bukan hal peretasan kode, tetapi hal-hal seperti ekspresi generator), tetapi tidak akan berfungsi dengan versi IronPython apa pun.

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()

Apa yang salah dengan meretas objek kode? Sebagian besar hanya segfault, RuntimeErrors yang memakan seluruh tumpukan, lebih banyak RuntimeErrors normal yang dapat ditangani, atau nilai-nilai sampah yang mungkin hanya akan menaikkan TypeErroratau AttributeErrorketika Anda mencoba menggunakannya. Sebagai contoh, coba buat objek kode hanya RETURN_VALUEdengan tanpa apa-apa di stack (bytecode b'S\0'for 3.6+, b'S'sebelumnya), atau dengan tuple kosong co_constsketika ada LOAD_CONST 0dalam bytecode, atau dengan varnamesdecremented oleh 1 sehingga yang tertinggi LOAD_FASTsebenarnya memuat freevar / sel cellvar. Untuk bersenang-senang nyata, jika Anda mendapatkan lnotabkesalahan yang cukup, kode Anda hanya akan segfault ketika dijalankan di debugger.

Menggunakan bytecodeatau byteplaytidak akan melindungi Anda dari semua masalah itu, tetapi mereka memang memiliki beberapa pemeriksaan kewarasan dasar, dan pembantu yang baik yang memungkinkan Anda melakukan hal-hal seperti memasukkan sepotong kode dan membiarkannya khawatir tentang memperbarui semua offset dan label sehingga Anda dapat ' jangan salah, dan sebagainya. (Plus, mereka membuat Anda tidak perlu mengetikkan konstruktor 6-garis konyol itu, dan harus men-debug kesalahan ketik konyol yang muncul karena melakukan hal itu.)


Sekarang ke # 2.

Saya menyebutkan bahwa objek kode tidak dapat diubah. Dan tentu saja const adalah tuple, jadi kita tidak bisa mengubahnya secara langsung. Dan hal dalam tuple const adalah string, yang juga tidak dapat kita ubah secara langsung. Itu sebabnya saya harus membuat string baru untuk membangun tuple baru untuk membangun objek kode baru.

Tetapi bagaimana jika Anda bisa mengubah string secara langsung?

Nah, cukup dalam di bawah selimut, semuanya hanya sebuah penunjuk ke beberapa data C, kan? Jika Anda menggunakan CPython, ada API C untuk mengakses objek , dan Anda dapat menggunakannya ctypesuntuk mengakses API dari dalam Python itu sendiri, yang merupakan ide yang mengerikan sehingga mereka menempatkannya pythonapidi sana di ctypesmodul stdlib . :) Trik paling penting yang perlu Anda ketahui adalah itu id(x)adalah pointer aktual ke xdalam memori (sebagaiint ).

Sayangnya, API C untuk string tidak akan membiarkan kami dengan aman mendapatkan penyimpanan internal dari string yang sudah beku. Jadi sekrup aman, mari kita baca file header dan menemukan penyimpanan itu sendiri.

Jika Anda menggunakan CPython 3.4 - 3.7 (berbeda untuk versi yang lebih lama, dan siapa yang tahu untuk masa depan), string literal dari modul yang terbuat dari ASCII murni akan disimpan menggunakan format ASCII yang ringkas, yang berarti berakhir lebih awal dan buffer byte ASCII segera menyusul dalam memori. Ini akan pecah (seperti dalam mungkin segfault) jika Anda meletakkan karakter non-ASCII dalam string, atau jenis string non-literal tertentu, tetapi Anda dapat membaca tentang 4 cara lain untuk mengakses buffer untuk berbagai jenis string.

Untuk mempermudah, saya menggunakan superhackyinternalsproyek dari GitHub saya. (Sengaja tidak dapat diinstal melalui pip karena Anda benar-benar tidak boleh menggunakan ini kecuali untuk bereksperimen dengan penerjemah lokal Anda dan sejenisnya.)

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()

Jika Anda ingin bermain dengan barang-barang ini, intjauh lebih sederhana di bawah selimut daripada str. Dan jauh lebih mudah untuk menebak apa yang bisa Anda hancurkan dengan mengubah nilai 2to 1, kan? Sebenarnya, lupakan membayangkan, mari kita lakukan saja (menggunakan tipe dari superhackyinternalslagi):

>>> 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

... berpura-pura bahwa kotak kode memiliki bilah gulir panjang tak terbatas.

Saya mencoba hal yang sama di IPython, dan pertama kali saya mencoba untuk mengevaluasi 2pada prompt, itu masuk ke semacam loop tak terbatas yang tidak terputus. Mungkin itu menggunakan nomor 2untuk sesuatu dalam loop REPL, sedangkan penerjemah saham tidak?

abarnert
sumber
11
@ cᴏʟᴅsᴘᴇᴇᴅ Kode-munging adalah Python yang bisa dibilang masuk akal, meskipun Anda umumnya hanya ingin menyentuh objek kode untuk alasan yang jauh lebih baik (misalnya, menjalankan bytecode melalui pengoptimal khusus). Mengakses penyimpanan internal dari PyUnicodeObject, di sisi lain, itu mungkin benar-benar hanya Python dalam arti bahwa juru bahasa Python akan menjalankannya ...
abarnert
4
Cuplikan kode pertama Anda muncul NameError: name 'arg' is not defined. Apakah maksud Anda: args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]? Cara dibilang lebih baik untuk menulis ini akan menjadi: args = [str(arg).replace('cat', 'dog') for arg in args]. Lain, bahkan lebih pendek, pilihan: args = map(lambda a: str(a).replace('cat', 'dog'), args). Ini memiliki manfaat tambahan yang argsmalas (yang juga bisa dicapai dengan mengganti pemahaman daftar di atas dengan generator satu - *argsbekerja dengan cara baik).
Konstantin
1
@ cᴏʟᴅsᴘᴇᴇᴅ Ya, IIRC Saya hanya menggunakan PyUnicodeObjectdefinisi struct, tetapi menyalinnya ke dalam jawaban akan saya pikir hanya menghalangi, dan saya pikir readme dan / atau komentar sumber untuk superhackyinternalsbenar - benar menjelaskan cara mengakses buffer (setidaknya cukup baik untuk mengingatkan saya lain kali saya peduli; tidak yakin apakah itu akan cukup untuk orang lain ...), yang saya tidak ingin masuk ke sini. Bagian yang relevan adalah cara untuk mendapatkan dari objek Python langsung ke PyObject *via ctypes. (Dan mungkin mensimulasikan aritmatika pointer, menghindari char_pkonversi otomatis , dll.)
abarnert
1
@ jpmc26 Saya rasa Anda tidak perlu melakukannya sebelum mengimpor modul, asalkan Anda melakukannya sebelum mereka mencetak. Modul akan melakukan pencarian nama setiap kali, kecuali jika mereka secara eksplisit mengikat printnama. Anda juga dapat mengikat nama printuntuk mereka: import yourmodule; yourmodule.print = badprint.
leewz
1
@abarnert: Saya perhatikan Anda sudah sering memperingatkan tentang melakukan ini (mis. "Anda tidak pernah benar-benar ingin melakukan ini" , "mengapa itu ide yang buruk untuk mengubah nilai" , dll.). Tidak jelas apa yang mungkin salah (sarkasme), maukah Anda menjelaskan sedikit tentang itu? Mungkin bisa membantu bagi mereka yang tergoda untuk mencobanya secara membabi buta.
l'L'l
37

Patch monyet print

printadalah fungsi bawaan sehingga akan menggunakan printfungsi yang ditentukan dalam builtinsmodul (atau__builtin__ dengan Python 2). Jadi, setiap kali Anda ingin memodifikasi atau mengubah perilaku fungsi builtin Anda dapat dengan mudah menetapkan kembali nama dalam modul itu.

Proses ini disebut 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

Setelah itu setiap printpanggilan akan melalui custom_print, bahkan jika printada dalam modul eksternal.

Namun Anda tidak benar-benar ingin mencetak teks tambahan, Anda ingin mengubah teks yang dicetak. Salah satu cara untuk melakukannya adalah menggantinya dengan string yang akan dicetak:

_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

Dan memang jika Anda menjalankan:

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

Atau jika Anda menulis itu ke file:

test_file.py

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

print_something()

dan impor:

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

Jadi itu benar-benar berfungsi sebagaimana dimaksud.

Namun, jika Anda hanya ingin sementara mencetak monkey-patch Anda dapat membungkusnya dalam konteks-manajer:

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

Jadi ketika Anda menjalankannya tergantung pada konteks apa yang dicetak:

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

Jadi begitulah cara Anda bisa "meretas" printdengan menambal monyet.

Ubah target alih-alih print

Jika Anda melihat tanda tangan printAnda akan melihat fileargumen yang secara sys.stdoutdefault. Perhatikan bahwa ini adalah argumen default dinamis (itu benar - benar terlihat sys.stdoutsetiap kali Anda menelepon print) dan tidak seperti argumen default normal di Python. Jadi jika Anda mengubah sys.stdout printsebenarnya akan mencetak ke target yang berbeda bahkan lebih nyaman bahwa Python juga menyediakanredirect_stdout fungsi (dari Python 3.4 on, tetapi mudah untuk membuat fungsi yang setara untuk versi Python sebelumnya).

Kelemahannya adalah tidak akan berfungsi untuk printpernyataan yang tidak bisa dicetak sys.stdoutdan pembuatan sendiri stdouttidak terlalu mudah.

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'))

Namun ini juga berfungsi:

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

Ringkasan

Beberapa poin ini sudah disebutkan oleh @abarnet tapi saya ingin menjelajahi opsi ini lebih detail. Terutama cara memodifikasinya lintas modul (menggunakan builtins/ __builtin__) dan bagaimana membuat perubahan itu hanya sementara (menggunakan manajer konteks).

MSeifert
sumber
4
Ya, hal yang paling dekat dengan pertanyaan ini yang harus benar-benar ingin dilakukan oleh siapa pun adalah redirect_stdout, jadi senang memiliki jawaban yang jelas yang mengarah ke sana.
abarnert
6

Cara sederhana untuk menangkap semua output dari suatu printfungsi dan kemudian memprosesnya, adalah mengubah aliran output ke sesuatu yang lain, misalnya file.

Saya akan menggunakan PHPkonvensi penamaan ( 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()

Pemakaian:

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

Akan mencetak

Hai John Bye John

Uri Goren
sumber
5

Mari kita gabungkan ini dengan introspeksi frame!

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

Anda akan menemukan trik ini untuk setiap salam dengan fungsi atau metode pemanggilan. Ini mungkin sangat berguna untuk logging atau debugging; terutama karena memungkinkan Anda "membajak" mencetak laporan dalam kode pihak ketiga.

Rafaël Dera
sumber