Pertama, sebenarnya ada cara yang jauh lebih tidak retas. Yang ingin kami lakukan adalah mengubah print
cetakan 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.stdout
bukan 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, CodeType
penginisialisasi 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, RuntimeError
s yang memakan seluruh tumpukan, lebih banyak RuntimeError
s normal yang dapat ditangani, atau nilai-nilai sampah yang mungkin hanya akan menaikkan TypeError
atau AttributeError
ketika Anda mencoba menggunakannya. Sebagai contoh, coba buat objek kode hanya RETURN_VALUE
dengan tanpa apa-apa di stack (bytecode b'S\0'
for 3.6+, b'S'
sebelumnya), atau dengan tuple kosong co_consts
ketika ada LOAD_CONST 0
dalam bytecode, atau dengan varnames
decremented oleh 1 sehingga yang tertinggi LOAD_FAST
sebenarnya memuat freevar / sel cellvar. Untuk bersenang-senang nyata, jika Anda mendapatkan lnotab
kesalahan yang cukup, kode Anda hanya akan segfault ketika dijalankan di debugger.
Menggunakan bytecode
atau byteplay
tidak 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 ctypes
untuk mengakses API dari dalam Python itu sendiri, yang merupakan ide yang mengerikan sehingga mereka menempatkannya pythonapi
di sana di ctypes
modul stdlib . :) Trik paling penting yang perlu Anda ketahui adalah itu id(x)
adalah pointer aktual ke x
dalam 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 superhackyinternals
proyek 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, int
jauh lebih sederhana di bawah selimut daripada str
. Dan jauh lebih mudah untuk menebak apa yang bisa Anda hancurkan dengan mengubah nilai 2
to 1
, kan? Sebenarnya, lupakan membayangkan, mari kita lakukan saja (menggunakan tipe dari superhackyinternals
lagi):
>>> 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 2
pada prompt, itu masuk ke semacam loop tak terbatas yang tidak terputus. Mungkin itu menggunakan nomor 2
untuk sesuatu dalam loop REPL, sedangkan penerjemah saham tidak?
42
menjadi23
daripada mengapa itu adalah ide yang buruk untuk mengubah nilai"My name is Y"
menjadi"My name is X"
.