Po pierwsze, w rzeczywistości istnieje znacznie mniej hakerski sposób. Chcemy tylko zmienić jakie print
wydruki, prawda?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
Lub, podobnie, sys.stdout
zamiast tego możesz użyć monkeypatch print
.
Nie ma też nic złego w exec … getsource …
pomyśle. Cóż, oczywiście jest w tym wiele nie tak, ale mniej niż to, co następuje tutaj…
Ale jeśli chcesz zmodyfikować stałe kodu obiektu funkcji, możemy to zrobić.
Jeśli naprawdę chcesz naprawdę bawić się obiektami kodu, powinieneś użyć biblioteki takiej jak bytecode
(po zakończeniu) lub byteplay
(do tego czasu lub dla starszych wersji Pythona) zamiast robić to ręcznie. Nawet w przypadku czegoś tak trywialnego CodeType
inicjalizacja jest uciążliwa; jeśli naprawdę musisz zrobić coś takiego jak naprawianie lnotab
, tylko szaleniec zrobiłby to ręcznie.
Ponadto jest oczywiste, że nie wszystkie implementacje Pythona używają obiektów kodu w stylu CPythona. Ten kod będzie działał w CPythonie 3.7 i prawdopodobnie we wszystkich wersjach co najmniej 2.2 z kilkoma drobnymi zmianami (nie dotyczy to hakowania kodu, ale rzeczy takie jak wyrażenia generatora), ale nie będzie działać z żadną wersją 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()
Co może pójść nie tak z hakowaniem obiektów kodu? Przeważnie to zwykłe segfaulty, RuntimeError
które pochłaniają cały stack, bardziej normalne, RuntimeError
które można obsłużyć, lub śmieciowe wartości, które prawdopodobnie po prostu podniosą a TypeError
lub AttributeError
gdy spróbujesz ich użyć. Na przykład spróbuj utworzyć obiekt kodu zawierający tylko znak RETURN_VALUE
z niczym na stosie (kod bajtowy b'S\0'
dla 3.6+, b'S'
wcześniej) lub z pustą krotką, co_consts
gdy w kodzie bajtowym znajduje się znak LOAD_CONST 0
lub z varnames
dekrementacją o 1, aby najwyższy LOAD_FAST
faktycznie ładował freevar / cellvar cell. Dla prawdziwej zabawy, jeśli lnotab
pomylisz się wystarczająco, twój kod będzie segfaultowany tylko wtedy, gdy zostanie uruchomiony w debugerze.
Używanie bytecode
lub byteplay
nie ochroni Cię przed wszystkimi tymi problemami, ale mają kilka podstawowych testów poczytalności i fajnych pomocników, które pozwalają ci zrobić takie rzeczy, jak wstawienie kawałka kodu i niech martwi się o aktualizację wszystkich przesunięć i etykiet, abyś mógł '' nie zrozumiem tego źle i tak dalej. (Poza tym nie musisz wpisywać tego śmiesznego 6-liniowego konstruktora i debugować głupie literówki, które z tego wynikają).
Teraz przejdźmy do # 2.
Wspomniałem, że obiekty kodu są niezmienne. Oczywiście stałe są krotką, więc nie możemy tego bezpośrednio zmienić. A rzeczą w stałej krotce jest łańcuch, którego również nie możemy bezpośrednio zmienić. Dlatego musiałem zbudować nowy ciąg, aby zbudować nową krotkę i zbudować nowy obiekt kodu.
Ale co by było, gdybyś mógł bezpośrednio zmienić ciąg?
Cóż, wystarczająco głęboko pod kołdrą, wszystko jest tylko wskaźnikiem do niektórych danych w C, prawda? Jeśli używasz CPythona, istnieje C API, aby uzyskać dostęp do obiektów , i możesz go użyć, ctypes
aby uzyskać dostęp do tego API z samego Pythona, co jest tak okropnym pomysłem, że umieścili pythonapi
tam bezpośrednio w ctypes
module stdlib . :) Najważniejszą sztuczką, którą musisz wiedzieć, id(x)
jest faktyczny wskaźnik x
w pamięci (jako int
).
Niestety, C API dla stringów nie pozwala nam bezpiecznie dostać się do wewnętrznej pamięci już zamrożonego łańcucha. Więc chrzanić bezpiecznie, po prostu przeczytajmy pliki nagłówkowe i sami znajdźmy to miejsce .
Jeśli używasz CPython 3.4 - 3.7 (jest inny dla starszych wersji i kto wie na przyszłość), literał ciągu z modułu, który jest wykonany z czystego ASCII, będzie przechowywany w kompaktowym formacie ASCII, co oznacza, że struktura kończy się wcześniej, a bufor bajtów ASCII następuje natychmiast w pamięci. To się zepsuje (jak w prawdopodobnie segfault), jeśli umieścisz w ciągu znak inny niż ASCII lub pewne rodzaje nieliteralnych ciągów, ale możesz przeczytać pozostałe 4 sposoby dostępu do bufora dla różnych rodzajów ciągów.
Aby trochę ułatwić, używam superhackyinternals
projektu poza moim GitHubem. (Celowo nie można go zainstalować za pomocą pip, ponieważ naprawdę nie powinieneś go używać, z wyjątkiem eksperymentowania z lokalną kompilacją interpretera i tym podobnymi).
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()
Jeśli chcesz się tym bawić, pod kołdrą int
jest o wiele prostsze niż str
. O wiele łatwiej jest zgadnąć, co można złamać, zmieniając wartość 2
na 1
, prawda? Właściwie zapomnij o wyobrażeniach, po prostu zróbmy to (używając superhackyinternals
ponownie typów z ):
>>> 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
… Udawaj, że skrzynka z kodem ma pasek przewijania o nieskończonej długości.
Wypróbowałem to samo w IPythonie i kiedy pierwszy raz spróbowałem ocenić 2
w zachęcie, wszedł on w jakąś nieprzerwaną nieskończoną pętlę. Prawdopodobnie używa numeru 2
do czegoś w swojej pętli REPL, podczas gdy interpreter zapasów nie?
42
na23
ponad dlaczego jest to zły pomysł, aby zmienić wartość"My name is Y"
na"My name is X"
.