Python/2.7 information

Hello world 난독화하기.(번역)

qkqhxla1 2015. 11. 28. 15:15

source : https://benkurtovic.com/2014/06/01/obfuscating-hello-world.html 번역(translation)


이상한 해석이나 틀린점 있으면 지적해주시면 감사하겠습니다. 일부 의역 했습니다.

(번역을 다 하긴 했는데 몇번 읽어보고 이해후 이상한 부분 수정이나 추가 설명 추가할 계획입니다.)


몇달 전에 엄청나게 난독화한 Hello world!라는 문자열을 출력하는 프로그램을 만들기 위해

Code Golf contest를 처음으로 갔다. 아래 코드가 결과물이고, 어떻게 작동하는지 설명해보기로 하겠다. 파이썬 2.7 환경에서 돌아간다.

(lambda _, __, ___, ____, _____, ______, _______, ________:
    getattr(
        __import__(True.__class__.__name__[_] + [].__class__.__name__[__]),
        ().__class__.__eq__.__class__.__name__[:__] +
        ().__iter__().__class__.__name__[_____:________]
    )(
        _, (lambda _, __, ___: _(_, __, ___))(
            lambda _, __, ___:
                chr(___ % __) + _(_, __, ___ // __) if ___ else
                (lambda: _).func_code.co_lnotab,
            _ << ________,
            (((_____ << ____) + _) << ((___ << _____) - ___)) + (((((___ << __)
            - _) << ___) + _) << ((_____ << ____) + (_ << _))) + (((_______ <<
            __) - _) << (((((_ << ___) + _)) << ___) + (_ << _))) + (((_______
            << ___) + _) << ((_ << ______) + _)) + (((_______ << ____) - _) <<
            ((_______ << ___))) + (((_ << ____) - _) << ((((___ << __) + _) <<
            __) - _)) - (_______ << ((((___ << __) - _) << __) + _)) + (_______
            << (((((_ << ___) + _)) << __))) - ((((((_ << ___) + _)) << __) +
            _) << ((((___ << __) + _) << _))) + (((_______ << __) - _) <<
            (((((_ << ___) + _)) << _))) + (((___ << ___) + _) << ((_____ <<
            _))) + (_____ << ______) + (_ << ___)
        )
    )
)(
    *(lambda _, __, ___: _(_, __, ___))(
        (lambda _, __, ___:
            [__(___[(lambda: _).func_code.co_nlocals])] +
            _(_, __, ___[(lambda _: _).func_code.co_nlocals:]) if ___ else []
        ),
        lambda _: _.func_code.co_argcount,
        (
            lambda _: _,
            lambda _, __: _,
            lambda _, __, ___: _,
            lambda _, __, ___, ____: _,
            lambda _, __, ___, ____, _____: _,
            lambda _, __, ___, ____, _____, ______: _,
            lambda _, __, ___, ____, _____, ______, _______: _,
            lambda _, __, ___, ____, _____, ______, _______, ________: _
        )
    )
)

시작.

print를 쓸수없는데, stdout을 써서 출력하는건 가능하다.

import sys
sys.stdout.write("Hello world!\n")

한줄로 표현하려고 __import__()을 사용했다.

__import__("os").write(1, "Hello world!\n")

write()를 getattr()에 넣어서 더 난독화가 가능하다.

getattr(__import__("os"), "write")(1, "Hello world!\n")

이게 시작이고 지금부터 3개의 문자열과 숫자를 난독화할거다.

os와 write은 바꾸기 쉽다. 다양한 built-in클래스의 이름의 일부를 가져와서 합치는 방식으로 해볼거고, 많은 방법이 있지만 난 아래의 방법을 사용했다.

'o'는 bool의 두번째 글자 : True.__class__.__name__[1]

's'는 list의 세번째 글자 : [].__class__.__name__[2]

'wr'은 wrapper_descriptor의 첫번째와 두번째 글자다. CPython에서 구현된 이미 만들어진 클래스의 메소드의 한종류로 자세한건 여길 보자 : ().__class__.__eq__.__class__.__name__[:2]

'ite'는 튜플에서 iter()을 호출하는 객체중 하나인 tupleiterator의 6~8번째의 글자에서 가져왔다 : ().__iter__().__class__.__name__[5:8]


위에 것들을 사용해서 진도를 더 나가 보자.

getattr(
    __import__(True.__class__.__name__[1] + [].__class__.__name__[2]),
    ().__class__.__eq__.__class__.__name__[:2] +
    ().__iter__().__class__.__name__[5:8]
)(1, "Hello world!\n")


Hello world!\n가 더 복잡해졌다. 이것을 큰 숫자(big integer)로 인코딩할거다. 아스키코드로 구성되있으며, 각각 글자(아스키값)는 (256**각각 글자의 인덱스값)한값을 곱해주면 된다. 그것의 합이 결과값이고, 다시 적자면 아래의 합과 같다. (뭔말인지 잘 이해가 안가시는 분들은 아래의 파이썬 코드와 번갈아서 보시면 이해가 쉬울겁니다.)

L은 문자열의 길이이고 cn은 문자열에서 n번째 글자의 아스키코드값이다.

>>> codes = [ord(c) for c in "Hello world!\n"] #codes에는 Hello world!\n의 아스키코드값을 리스트로 저장해둔다.
>>> num = sum(codes[i] * (256 ** i) for i in xrange(len(codes))) 
#codes리스트에서 아스키값을 하나하나 가져와서 256**i한 값을 곱해준다. 그리고 그 값을 전부 더한 값을 num에 저장한다.
>>> print num
802616035175250124568770929992

이제 우리는 이 큰 숫자를 다시 문자열로 바꿔야한다. 간단한 재귀 알고리즘을 사용했다.

>>> def convert(num):
...     if num:
...         return chr(num % 256) + convert(num // 256)
...     else:
...         return ""
...
>>> convert(802616035175250124568770929992)
'Hello world!\n'

람다 함수를 이용해서 한줄로 다시 써보자.

convert = lambda num: chr(num % 256) + convert(num // 256) if num else ""

이제 하나의 표현으로 변환시키기 위해 익명 재귀를 사용할수 있다. 이건 combinator라는걸 필요로 한다.

이걸로 시작하자.

>>> comb = lambda f, n: f(f, n)
>>> convert = lambda f, n: chr(n % 256) + f(f, n // 256) if n else ""
>>> comb(convert, 802616035175250124568770929992)
'Hello world!\n'

이제 두개의 정의를 하나의 표현에 넣었고, 아래가 결과물이다.

>>> (lambda f, n: f(f, n))(
...     lambda f, n: chr(n % 256) + f(f, n // 256) if n else "",
...     802616035175250124568770929992)
'Hello world!\n'

이제 이걸 이전 코드에 넣을수 있다. 몇몇 변수의 이름을 이와같이 대체하자. (f → _, n → __):

getattr(
    __import__(True.__class__.__name__[1] + [].__class__.__name__[2]),
    ().__class__.__eq__.__class__.__name__[:2] +
    ().__iter__().__class__.__name__[5:8]
)(
    1, (lambda _, __: _(_, __))(
        lambda _, __: chr(__ % 256) + _(_, __ // 256) if __ else "",
        802616035175250124568770929992
    )
)

함수 내부.

위에서 변환 함수의 몸체에 ""를 남겨두었다.(까먹지말자. ''같은 문자 리터럴은 사용하면 안된다.), 또 우리가 숨겨야 할 큰 숫자가 있다. 빈 문자열부터 살펴보자. 몇몇 랜덤 함수의 내부를 검사하면서 빈 문자열 하나를 만들 수 있다. 

(lambda: 0).func_code.co_lnotab #출력해보면 공백문자가 출력된다. 위에서 ''같은 문자 리터럴 사용 금지라고 해서 이걸 만든거 같다.

우리가 여기서 진짜로 해야할 것은 함수 내부에 포함되어 있는 코드 객체의 line number table을 찾는 것이다. 익명이어서 line numbers가 없고, 문자열이 비어있다.

(문자열이 비어있다는건 위의 (lambda: 0).func_code.co_Inotab값이 빈 문자열이라는걸 설명하는것 같네요.) 

더 난독화하기 위해 0을 _로 대체하고 집어넣자.(함수가 호출되지 않기 때문에 이것은 문제가 안된다.) 또 256을 리팩터링(refactor)해서 인자에 집어넣을거다. 이건 우리의 난독화된 convert()함수에 상속되서 숫자처럼 쓰일것이다.(요부분 좀 제 해석이 요상하네요.. 맞는지 잘 모르겠습니다..) 이건 combinator에 인자를 추가하는걸 필요로한다.

getattr(
    __import__(True.__class__.__name__[1] + [].__class__.__name__[2]),
    ().__class__.__eq__.__class__.__name__[:2] +
    ().__iter__().__class__.__name__[5:8]
)(
    1, (lambda _, __, ___: _(_, __, ___))( #위에 있던 람다함수보다 인자가 하나 더 늘어났습니다. 256을 인자로 받기 위함으로 보입니다.
        lambda _, __, ___:
            chr(___ % __) + _(_, __, ___ // __) if ___ else #else뒤에있던 ""가 위에 설명대로 사라지고 (lambda: _).func_code.co_lnotab가 대신 들어왔습니다. 
            (lambda: _).func_code.co_lnotab,
        256,
        802616035175250124568770929992
    )
)

우회로.

다른 문제를 살펴보자. 위의 코드에서 숫자를 암호화시키는 방법을 찾고 싶다. 그런데 사용되는 시간동안 재생성하는게 어렵다. 구현한다면, range(1, 9) == [1, 2, 3, 4, 5, 6, 7, 8]이다. 그러면 1부터 8까지의 숫자를 포함하는 변수를 함수에 넣고, 감싸고(wrap), 몸체에서의 숫자의 발생을 이 변수들로 대체할수 있다.(숫자를 변수로 난독화한다는 소리입니다.)

(lambda n1, n2, n3, n4, n5, n6, n7, n8: 
#원래는 아래의 getattr(이 시작이었는데 숫자를 암호화시키기 위해서 바깥에 람다 함수 하나를 더 넣었다. 그리고 맨 아래에
    getattr(
        __import__(True.__class__.__name__[n1] + [].__class__.__name__[n2]), #위의 코드와 비교해보면 n1은 원래 1이다. 그런데 암호화시키기 위해 이렇게 lambda함수로 한번 더 감쌌다.
        ...
    )(
        ...
    )
)(*range(1, 9)) #range(1,9)로 함수를 호출함으로서 n1,n2~n8에 1,2,3,4~8까지 들어가게 하였다.

256의 형태와 802616035175250124568770929992 또한 필요하지만 이것들은 '기초적인' 8가지 숫자들에서 산술 연산자를 사용해서 만들어질 수 있다. 1~8중에서 선택은 임의다. 그러나 좋은 절충안을 찾은것 같다.


code 객체를 사용해서 함수 인자의 숫자를 얻을 수 있다.

>>> (lambda a, b, c: 0).func_code.co_argcount
3

함수의 인자 1~8 을 argcounts(위에서의 func_code.co_argcount를 말하는듯 싶네요.)를 이용해서 튜플로 만들어보자.

funcs = (
    lambda _: _,
    lambda _, __: _,
    lambda _, __, ___: _,
    lambda _, __, ___, ____: _,
    lambda _, __, ___, ____, _____: _,
    lambda _, __, ___, ____, _____, ______: _,
    lambda _, __, ___, ____, _____, ______, _______: _,
    lambda _, __, ___, ____, _____, ______, _______, ________: _
)

재귀 알고리즘을 사용해서, 이것을 range(1,9)의 출력으로 바꿀 수 있다.

>>> def convert(L):
...     if L:
...         return [L[0].func_code.co_argcount] + convert(L[1:])
...     else:
...         return []
...
>>> convert(funcs)
[1, 2, 3, 4, 5, 6, 7, 8]

그전에 lambda 형태로 바꾸자,

convert = lambda L: [L[0].func_code.co_argcount] + convert(L[1:]) if L else []

그러고 익명 재귀 형태로 바꾸자.

>>> (lambda f, L: f(f, L))(
...     lambda f, L: [L[0].func_code.co_argcount] + f(f, L[1:]) if L else [],
...     funcs)
[1, 2, 3, 4, 5, 6, 7, 8]

재미를 위해서 argcount operation을 추가적인 함수의 인자로 뽑아내고, 몇몇 변수의 이름을 난독화하자.

(lambda _, __, ___: _(_, __, ___))(
    (lambda _, __, ___:
        [__(___[0])] + _(_, __, ___[1:]) if ___ else []
    ),
    lambda _: _.func_code.co_argcount,
    funcs
)

0과 1을 숨길 방법이 필요하다. 임의의 함수 내에서 로컬 변수를 분석함으로서 값을 가져와서 해결할수 있다.

>>> (lambda: _).func_code.co_nlocals
0
>>> (lambda _: _).func_code.co_nlocals
1

함수 몸체가 똑같이 보이지만 첫번째 함수의 _는 인자가 아니고, 함수에 정의된 것도 아니므로 파이썬 해석기는 이걸 전역 변수로 해석한다.

>>> import dis
>>> dis.dis(lambda: _)
  1           0 LOAD_GLOBAL              0 (_)
              3 RETURN_VALUE
>>> dis.dis(lambda _: _)
  1           0 LOAD_FAST                0 (_)
              3 RETURN_VALUE

이건 _가 전역 영역에 진짜로 정의되있는지에 상관없이 발생한다. 이걸 넣자.

(lambda _, __, ___: _(_, __, ___))(
    (lambda _, __, ___:
        [__(___[(lambda: _).func_code.co_nlocals])] +
        _(_, __, ___[(lambda _: _).func_code.co_nlocals:]) if ___ else []
    ),
    lambda _: _.func_code.co_argcount,
    funcs
)

이제 funcs의 값을 대체할수 있고, *을 사용해서 여덟개의 변수로 나뉘어진 숫자의 최종 리스트를 통과할수 있다. 최종적으로 아래를 얻었다.

(lambda n1, n2, n3, n4, n5, n6, n7, n8:
    getattr(
        __import__(True.__class__.__name__[n1] + [].__class__.__name__[n2]),
        ().__class__.__eq__.__class__.__name__[:n2] +
        ().__iter__().__class__.__name__[n5:n8]
    )(
        n1, (lambda _, __, ___: _(_, __, ___))(
            lambda _, __, ___:
                chr(___ % __) + _(_, __, ___ // __) if ___ else
                (lambda: _).func_code.co_lnotab,
            256,
            802616035175250124568770929992
        )
    )
)(
    *(lambda _, __, ___: _(_, __, ___))(
        (lambda _, __, ___:
            [__(___[(lambda: _).func_code.co_nlocals])] +
            _(_, __, ___[(lambda _: _).func_code.co_nlocals:]) if ___ else []
        ),
        lambda _: _.func_code.co_argcount,
        (
            lambda _: _,
            lambda _, __: _,
            lambda _, __, ___: _,
            lambda _, __, ___, ____: _,
            lambda _, __, ___, ____, _____: _,
            lambda _, __, ___, ____, _____, ______: _,
            lambda _, __, ___, ____, _____, ______, _______: _,
            lambda _, __, ___, ____, _____, ______, _______, ________: _
        )
    )
)

비트 옮기기.


거의 끝났다. 변수들이 우리의 안쪽 함수에서 사용됬기 때문에 보기에 매우 혼란스럽고, 이제 n{1..8}변수를 _,__,___,____ 등등으로 바꿀거다. 이건 스코핑 룰에서 맞게 사용될거기 때문에 괜찮다. 이건 또한 왜 우리가 1대신 _을 사용한 난독화된 convert()함수에서 256이 재결합되서 나와야 하는 이유중에 하나다.(해석이 좀 요상하네요 제가보기에.. ) 이건 오래걸리므로 첫번째 반 부분의 코드만 붙여넣어보겠다.

(lambda _, __, ___, ____, _____, ______, _______, ________:
    getattr(
        __import__(True.__class__.__name__[_] + [].__class__.__name__[__]),
        ().__class__.__eq__.__class__.__name__[:__] +
        ().__iter__().__class__.__name__[_____:________]
    )(
        _, (lambda _, __, ___: _(_, __, ___))(
            lambda _, __, ___:
                chr(___ % __) + _(_, __, ___ // __) if ___ else
                (lambda: _).func_code.co_lnotab,
            256,
            802616035175250124568770929992
        )
    )
)

오직 두개만 더 왼쪽에 놔두자. 쉬운것부터 시작할것이다 : 256. 256 = 2**8이다. 그러므로 1<<8로 다시쓸수있다. (left bit shift를 사용해서.) 또는 우리의 복잡한 변수인 _<< ________ 를 사용할 것이다.


802616035175250124568770929992 도 같은 아이디어를 사용할거다. 간단한 분할 정복 알고리즘(divide-and-conquer)으로 해결할수 있다.(숫자에 대한 설명인데 해석해봤자 이상할거같아서 그냥 안쓰겠습니다.) 예를들면 112를 가지고있으면 96+16으로 나누고, (3<<5) + (2<<3)으로 나눌수 있다. 난 bit shifts를 사용하는걸 좋아하는데 <<이 c++에서의 std::cout<<"foo" 또는 파이썬 인터프리터에서의 >>를 떠올리게 하기 때문이다. 둘다 I/O를 다른 방법으로 하기 위한 방법이다.


숫자는 다양한 방법으로 분해될 수 있다. 어떤 메소드도 맞지 않다.(결국에는 재미는 없지만 (1<<0) + (1<<0) + ... 이런걸로 분해할거다.) 우리는 몇몇의 상당한 양의 서브루틴들의 집합(nesting)을 갖고 있지만 대부분은 숫자 변수를 아직 사용한다.

손으로 하는건 진짜 재미없으므로 아래의 알고리즘을 쓸거다. 의사 코드를 보자 : 

func encode(num):
    if num <= 8:
        return "_" * num
    else:
        return "(" + convert(num) + ")"

func convert(num):
    base = shift = 0
    diff = num
    span = ...
    for test_base in range(span):
        for test_shift in range(span):
            test_diff = |num| - (test_base << test_shift)
            if |test_diff| < |diff|:
                diff = test_diff
                base = test_base
                shift = test_shift
    encoded = "(" + encode(base) + " << " + encode(shift) + ")"
    if diff == 0:
        return encoded
    else:
        return encoded + " + " + convert(diff)

convert(802616035175250124568770929992)

기본 아이디어는 우리가 특정한 범위의 다양한 수의 조합을 두개의 수(base<<shift되는 num에 가능한한 가까운 base와 shift이다.)를 찾아낼때까지 실행한다.(예를들면 우리는 그것들의 절대값의 차이를 최소화할거다. diff라는 변수가 이 역할을 한다.) 그러고 나서 분할 정복 알고리즘(divide-and-conquer) 을 이용해서 best_base와 best_shift를 찾아내고, diff의 과정을 통해서,구문의 합을 계속 계산하면서 그게 0이 될때까지 반복할거다. 


range()의 인자인 span은 탐색할 공간의 너비를 뜻한다. 이것(span) 은 너무 커질 수 없고, 우리의 num을 얻으면 끝낼거다. num을 우리의 base와 0으로서, 이것을 우리의 shift로(diff가 0이기때문에.), 그리고 base가 하나의 변수로 대표될 수 없기 때문에 무한히 반복되고 재귀될것이다. 만약 그게 너무 적으면, 우리는 위에서 언급한 (1<<0) + (1<<0) + ... 같은걸로 끝낼거다. 실제로, 우리는 span을 재귀의 깊이를 더 깊게 함으로서 더 적은 값을 얻길 원한다. 시험과 에러를 겪고 나는 잘 작동하는 공식을 발견했다.

의사 코드를 해석해서 파이썬에 넣고 조금 꼬아보자.(depth를 인자에 추가하고 몇몇 음수를 포함할수 있게 선언하자.(해석이 이상해요 ㅠㅠ.))

이걸 얻었다 : 

from math import ceil, log

def encode(num, depth):
    if num == 0:
        return "_ - _"
    if num <= 8:
        return "_" * num
    return "(" + convert(num, depth + 1) + ")"

def convert(num, depth=0):
    result = ""
    while num:
        base = shift = 0
        diff = num
        span = int(ceil(log(abs(num), 1.5))) + (16 >> depth)
        for test_base in xrange(span):
            for test_shift in xrange(span):
                test_diff = abs(num) - (test_base << test_shift)
                if abs(test_diff) < abs(diff):
                    diff = test_diff
                    base = test_base
                    shift = test_shift
        if result:
            result += " + " if num > 0 else " - "
        elif num < 0:
            base = -base
        if shift == 0:
            result += encode(base, depth)
        else:
            result += "(%s << %s)" % (encode(base, depth),
                                      encode(shift, depth))
        num = diff if num > 0 else -diff
    return result

그리고 convert(802616035175250124568770929992) 를 부르면 좋은 분할결과를 볼 수 있다 : 

>>> convert(802616035175250124568770929992)

(((_____ << ____) + _) << ((___ << _____) - ___)) + (((((___ << __) - _) << ___) + _) << ((_____ << ____) + (_ << _))) + (((_______ << __) - _) << (((((_ << ___) + _)) << ___) + (_ << _))) + (((_______ << ___) + _) << ((_ << ______) + _)) + (((_______ << ____) - _) << ((_______ << ___))) + (((_ << ____) - _) << ((((___ << __) + _) << __) - _)) - (_______ << ((((___ << __) - _) << __) + _)) + (_______ << (((((_ << ___) + _)) << __))) - ((((((_ << ___) + _)) << __) + _) << ((((___ << __) + _) << _))) + (((_______ << __) - _) << (((((_ << ___) + _)) << _))) + (((___ << ___) + _) << ((_____ << _))) + (_____ << ______) + (_ << ___)

이걸 802616035175250124568770929992 대신 넣자. 그리고 모든 부분을 같이 보자.

(lambda _, __, ___, ____, _____, ______, _______, ________:
    getattr(
        __import__(True.__class__.__name__[_] + [].__class__.__name__[__]),
        ().__class__.__eq__.__class__.__name__[:__] +
        ().__iter__().__class__.__name__[_____:________]
    )(
        _, (lambda _, __, ___: _(_, __, ___))(
            lambda _, __, ___:
                chr(___ % __) + _(_, __, ___ // __) if ___ else
                (lambda: _).func_code.co_lnotab,
            _ << ________,
            (((_____ << ____) + _) << ((___ << _____) - ___)) + (((((___ << __)
            - _) << ___) + _) << ((_____ << ____) + (_ << _))) + (((_______ <<
            __) - _) << (((((_ << ___) + _)) << ___) + (_ << _))) + (((_______
            << ___) + _) << ((_ << ______) + _)) + (((_______ << ____) - _) <<
            ((_______ << ___))) + (((_ << ____) - _) << ((((___ << __) + _) <<
            __) - _)) - (_______ << ((((___ << __) - _) << __) + _)) + (_______
            << (((((_ << ___) + _)) << __))) - ((((((_ << ___) + _)) << __) +
            _) << ((((___ << __) + _) << _))) + (((_______ << __) - _) <<
            (((((_ << ___) + _)) << _))) + (((___ << ___) + _) << ((_____ <<
            _))) + (_____ << ______) + (_ << ___)
        )
    )
)(
    *(lambda _, __, ___: _(_, __, ___))(
        (lambda _, __, ___:
            [__(___[(lambda: _).func_code.co_nlocals])] +
            _(_, __, ___[(lambda _: _).func_code.co_nlocals:]) if ___ else []
        ),
        lambda _: _.func_code.co_argcount,
        (
            lambda _: _,
            lambda _, __: _,
            lambda _, __, ___: _,
            lambda _, __, ___, ____: _,
            lambda _, __, ___, ____, _____: _,
            lambda _, __, ___, ____, _____, ______: _,
            lambda _, __, ___, ____, _____, ______, _______: _,
            lambda _, __, ___, ____, _____, ______, _______, ________: _
        )
    )
)

코드를 얻었다. 


파이썬 난독화에 관심이 있으면(있고, 영어를 잘하면) http://pyvideo.org/video/398/pycon-2011--how-to-write-obfuscated-python 도 한번 보자.