첫째, 실제로 해킹이 훨씬 덜합니다. 우리가하고 싶은 것은 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)
또는 이와 유사하게 sys.stdout
대신에 monkeypatch를 사용할 수 있습니다 print
.
또한 exec … getsource …
아이디어 에는 아무런 문제가 없습니다 . 물론 여기에는 많은 문제가 있지만 여기에 나오는 것보다 적습니다.
그러나 함수 객체의 코드 상수를 수정하고 싶다면 그렇게 할 수 있습니다.
실제 코드 객체로 실제로 놀고 싶다면 수동으로하는 대신 ( bytecode
완료 byteplay
될 때 ) 또는 (이전까지 또는 이전 Python 버전 과 같은) 라이브러리를 사용해야합니다 . 이 사소한 일조차도 CodeType
초기화 프로그램은 고통입니다. 실제로 lnotab
fix up과 같은 작업을 수행 해야하는 경우 미치광이 만 수동으로 수행합니다.
또한 모든 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()
코드 객체를 해킹하면 무엇이 잘못 될 수 있습니까? 대부분 segfaults, RuntimeError
전체 스택을 먹는 s, RuntimeError
처리 할 수있는 보다 정상적인 s TypeError
또는 AttributeError
사용하려고 할 때 가비지 값이 발생할 수 있습니다 . 예를 들어 RETURN_VALUE
스택에 아무 것도없는 코드 객체 ( b'S\0'
3.6 + b'S'
이전 바이트 코드) 또는 바이트 코드에 co_consts
a가 있거나 빈 값이 1 인 경우 빈 튜플을 사용 LOAD_CONST 0
하여 코드 객체를 만들어보십시오. / cellvar 셀. 약간의 재미를 위해, 충분히 잘못되면 코드가 디버거에서 실행될 때만 segfault가됩니다.varnames
LOAD_FAST
lnotab
이러한 문제를 모두 사용 bytecode
하거나 byteplay
보호하지는 않지만 기본적인 위생 검사 및 코드 덩어리 삽입과 같은 작업을 수행하고 모든 오프셋 및 레이블 업데이트에 대해 걱정할 수있는 훌륭한 도우미가 있습니다. 잘못 이해하는 등. (또한, 그들은 당신이 그 우스운 6 줄 생성자를 타이핑하지 말고, 그렇게함으로써 어리석은 오타를 디버깅하지 않아도됩니다.)
이제 # 2로 넘어갑니다.
코드 객체는 변경할 수 없다고 언급했습니다. 물론 const는 튜플이므로 직접 변경할 수는 없습니다. 그리고 const 튜플의 것은 문자열이며, 우리는 직접 변경할 수도 없습니다. 그래서 새로운 코드 객체를 만들기 위해 새로운 튜플을 만들기 위해 새로운 문자열을 만들어야했습니다.
그러나 문자열을 직접 변경할 수 있다면 어떨까요?
글쎄, 엄밀히 살펴보면 모든 것이 C 데이터에 대한 포인터 일뿐입니다. 당신이 CPython의를 사용하는 경우, 거기 는 C API는 액세스 객체 , 그리고 당신이 사용할 수있는 ctypes
접근이 그들이 넣어 그런 끔찍한 생각이다 파이썬 자체 내에서 API pythonapi
다음 stdlib의 바로 거기 ctypes
모듈 . :) 알아야 할 가장 중요한 트릭 은 메모리에서 id(x)
실제 포인터입니다 x
( int
).
불행히도 문자열에 대한 C API를 사용하면 이미 고정 된 문자열의 내부 저장소를 안전하게 가져올 수 없습니다. 안전하게 조이 십시오. 헤더 파일을 읽고 그 저장소를 직접 찾으십시오.
CPython 3.4-3.7을 사용하는 경우 (이전 버전과 다르고 미래를 아는 사람) 순수 ASCII로 만들어진 모듈의 문자열 리터럴은 컴팩트 ASCII 형식을 사용하여 저장됩니다. 일찍 끝나고 ASCII 바이트 버퍼는 메모리에서 즉시 뒤 따릅니다. 문자열에 ASCII가 아닌 문자 또는 특정 종류의 문자가 아닌 문자열을 넣으면 (아마도 segfault에서와 같이) 중단되지만 다른 종류의 문자열에 대한 버퍼에 액세스하는 다른 4 가지 방법을 읽을 수 있습니다.
조금 더 쉽게하기 위해 superhackyinternals
GitHub 에서 프로젝트를 사용하고 있습니다. (이것은 인터프리터 등의 로컬 빌드를 실험하는 것을 제외하고는 이것을 사용하지 않아야하기 때문에 의도적으로 pip-installable이 아닙니다.)
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 루프에서 무언가에 숫자 를 사용하고 있습니까?
42
에23
이 값 변경하는 나쁜 생각이 왜 이상"My name is Y"
에를"My name is X"
.