악성코드 하나를 분석하던 중, 아주 흔하면서도 지금까지 짜증나게 만들었던 루틴이 하나 등장했습니다.

단순하게 LoadLibrary와 GetProcAddress를 통해 모듈 핸들과 함수 주소를 가져오고, 이를 전역변수에 대입하여 다른 코드들에서 사용하는 것으로 IAT에 자신이 사용하는 함수명을 표시하지 않게 하는 흔한 기법이지만, 몇가지 문제점이 있었습니다.



[그림 1. 첫번째 문제]


 첫째, 양이 많았습니다. 한 함수 내에서 40개가 넘어가는 함수들의 주소를 가져오는데, 이는 다음 문제점과 더해 미친듯한 시너지를 발휘합니다.



[그림 2. 두번째 문제]


 둘째, 변수명이 스택에 저장되어있습니다. 바이트 단위로 할당되어있기 때문에 보통이라면 이걸 전부 아스키 문자로 바꿔서 하나하나 읽고 무슨 함수인지를 파악해야 합니다. 함수가 엄청나게 많다 보니 1500바이트가 넘어가는 양입니다.




[그림 3. 세번째 문제]


 셋째, 변수 값이 암호화되어있습니다. 정확히는 0x08이라는 고정된 값으로 XOR되어있는 것이지만, 어쨌든 그냥 봐서는 어떤 함수인지 알기가 힘듭니다. 셋중 하나라도 문제가 없었으면 그냥 복사해서 파이썬 스크립트로 복호화하거나 해서 하나하나 네이밍해줬겠지만, 도저히 그럴 양이 아니었습니다.



 여기에서 잠시 현자타임이 왔고, 이걸 어떻게 해결할까 하다가 지금까지 너무 무식하게 분석해왔다는 생각이 들어서 IDAPython을 이용하여 자동 스크립트를 짜기로 결심했습니다.




[그림 04. 스크립트 동작 전]


 작성한 스크립트를 돌리기 전의 모습입니다. 



[그림 05. 스크립트 로그]


 스크립트를 동작시키면 IDAPython 콘솔창에 로그가 남습니다.




[그림 06. 스크립트 동작 후]


 스크립트를 동작한 후 새로고침해주면 위와 같이 GetProcAddress를 이용하여 대입한 값들이 네이밍됩니다. 예전 BoB 4기에서 잠깐 한 적이 있기는 했는데 사실 기억에는 전혀 남아있지 않아서 혼자 여기저기 검색하면서 조잡하게 짰습니다.


이 스크립트에는 아직 개선할 점이 몇 가지 있습니다.


- GetProcAddress의 결과를 전역변수가 아닌 지역변수나 Struct의 멤버변수같은것에 대입하는 경우는 네이밍이 되지 않습니다.

- 특정 함수 내에 커서를 둔 채로 실행해야 하며, 해당 함수 내에서의 GetProcAddress에 대해서만 네이밍을 진행합니다.

- GetProcAddress함수의 주소를 직접 구해서 RenameGetProcAddrVars 함수에 인자로 넣어야 합니다.


스크립트를 사용하면서 나중에 조금씩 업데이트해갈텐데, 그러면서 하나씩 해결해갈 예정입니다.


소스코드 및 정리한 내용은 아래에 있습니다.


IDAPythonUtils.py




 소스코드 내에서 사용한 idaapi의 함수들은 다음과 같습니다.


※ ea라는 용어는 Effective Address의 약자로, 주소값을 의미합니다.

- Byte : 인자로 ea를 받으며 해당 주소의 1바이트 값을 반환합니다. 리터럴 상수처럼 정적으로 값이 확인 가능해야 합니다.
- GetFunctionAttr : 첫번째 인자로 ea, 두번째 인자로 받아올 함수의 속성을 입력받아 해당 함수의 속성을 가져옵니다. 여기에서는 함수 시작 주소와 끝 주소를 받아오는데에 사용했습니다.
- GetDisasm : 인자로 ea를 받아 해당 주소의 값을 디스어셈블하여 문자열로 리턴합니다.
- GetOpnd : 첫번째 인자로 ea를, 두번째 인자로 가져올 오퍼랜드를 0-Base로 입력받습니다. 디스어셈블된 오퍼랜드 자체를 넘겨줍니다. (예:  lea eax, [esp+674h+var_617]의 경우 :[esp+674h+var_617]: 리턴)
- GetOperandValue : 첫번째 인자로 ea를, 두번째 인자로 가져올 오퍼랜드를 0-Base로 입력받습니다. 오퍼랜드에 넘어간 값을 강제로 int형으로 바꿔서 반환하는 것 같습니다. (예: lea eax, [esp+674h+var_617]의 경우 93 리턴)
- here : 현재 커서가 가리키는 주소 반환
- PrevHead : ea를 인자로 받아 바로 이전 명령의 주소 반환
- NextHead : ea를 인자로 받아 바로 이후 명령의 주소 반환
- XrefsTo : ea를 인자로 받아 해당 주소를 참조하는 주소들을 iterable하게 반환




블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,



 Python 2.7과 selenium + webdriver(chromedriver), BeautifulSoup4를 이용하여 만든 크롤러입니다. 네이버에 로그인하고, 메일 페이지에서 특정 사용자로 검색 및 안 읽은 메일만 가져오도록 필터링하여 메일 내부의 첨부파일을 특정 위치로 다운로드받도록 하여 영상을 찍어 봤습니다. 네이버의 경우 로그인할 때 의심스러운 행위를 자동으로 감지하여 캡차로 막기 때문에 이를 우회하기 위해 네이버 메인을 경유하여 로그인 페이지로 이동하고, 중간중간 랜덤한 시간동안 Sleep하는 코드를 넣어두었습니다. 소스코드는 다음과 같으며, 라이센스는 MIT 이므로 누구나 원하는 곳에 사용이 가능하지만 이 프로그램을 사용함에 있어 발생하는 모든 책임은 사용자에게 있습니다.




소스코드


NaverMailCrawler.py



Utils.py

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,


 그냥 전에 올렸던 PyV8을 이용한 스크립트 분석기를 계속 개발하다 보니 여러 모로 개선해야 할 점이 많아서 좀 끄적거려본다.

 특정 상황에만 행위를 하거나 True/False를 체크하여 행위를 한다거나 하는 악성코드도 있어서 이런걸 커스터마이징할 수 있도록 해 볼 예정... 또 자바스크립트 내에 주석 등을 통해 Dummy Code를 넣는다거나 하는 경우도 있는데 이런것도 정규식을 이용하던지 해서 좀 제거해봐야 할 듯 하다...

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

[Python] PyV8을 이용한 Javascript 분석


여기에 작성된 글을 보고 이것을 이용하면 난독화된 악성 스크립트를 안전하고 효율적으로 분석이 가능하겠다는 생각이 들어서 비슷한 방식으로 만들어보기로 마음먹었다.


[PyV8은 여기에서 다운받을 수 있다. 마지막 업로드가 2012년인 것이 조금 걸리지만...]

 PyV8은 구글에서 만든 자바스크립트 엔진인 V8의 파이썬 래퍼로, 자바스크립트 코드를 해석하고 실행하여 그 결과를 파이썬에서 받아볼 수 있다. 하지만 PyV8은 자바스크립트 문법을 해석해 줄 뿐 운영체제의 API에 연결되어 있지 않기 때문에, 악성 스크립트에서 자주 사용하는 WScript.Shell 등을 사용하면 정의되지 않았다면서 에러가 발생한다. 그래서 직접 더미 API를 만들어서 넘겨주어야 하는 필요성이 있고, 위에서 언급했던 블로그에서는 각 모듈과 함수들을 직접 정의하여 넘겨주는 방식으로 제작했었다. 그런데 그렇게 만들게 되면 모든 모듈과 함수에 대응하는 클래스 및 메서드를 직접 다 만들어서 전달해야 한다는 불편함이 있었고, 결국 내가 원하는 것은 '어떤 함수가 어떤 인자를 받아서 실행되었는가' 이기 때문에 전부 간단하게 추상화시킬 수 있다고 판단하여 결국 간단하게 추상화 하는 것에 성공하였다. 자세한 내용은 소스코드를 보면서 작성하겠다.


http://5kyc1ad.kr/files/ScriptAnalyzer.zip

(악성코드 분석하려고 만든건데 티스토리에서 악성코드라며 첨부파일을 막아버렸네요; 첨부파일은 제 서버 주소로 대체하겠습니다.

꽤 시간이 흐른 뒤에 발견해버린 터라 실제 본문의 스크립트와는 상당히 달라졌을 수 있습니다.)

ScriptAnalyzer.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import os
 
import PyV8
 
from DummyAPI.Global import Global
 
class ScriptAnalyzer:
    def __init__(self):
        pass
 
    def __del__(self):
        pass
 
    def Analyze(self, targetJSFile):
        if not os.path.exists(targetJSFile):
            print "[-] Target File not found : %s" % (targetJSFile)
            return False
 
        with open(targetJSFile, "rb") as f:
            javaScript = f.read()
 
        ctx = PyV8.JSContext(Global())
        ctx.enter()
        ctx.eval(javaScript)
 
        return True
 
def main():
    scriptAnalyzer = ScriptAnalyzer()
    scriptAnalyzer.Analyze("..\\script4.js-")
 
if __name__ == '__main__':
    main()
cs

 PyV8을 이용하여 Javascript를 실행하는 분석기와 그를 이용하는 main 함수가 존재하는 소스코드 파일이다. 별다른건 없고 이후 나올 DummyAPI.Global 모듈에서 Global 클래스를 가져와 넘기는 것만 확인하면 된다.


Global.py

1
2
3
4
5
6
7
import PyV8
 
from DummyAPI.AbstractType import AbstractType
 
class Global(PyV8.JSClass):
    def __getattr__(self, name):
        return AbstractType(name)
cs

 추상화에 성공하여 무척 간결해져 버린 Global 클래스의 소스코드이다. 원래라면 클래스 멤버 변수로 각 모듈들에 대응하는 클래스의 인스턴스를 가지고 있어야 하지만, 어떤 모듈을 호출할 지 모르므로 __getattr__ 메서드를 정의하는 것으로 한번에 해결해 버렸다. __getattr__ 메서드는 classA.not_exist_value 처럼 클래스 내부에 존재하지 않는 변수, 혹은 메서드에 접근했을 경우 호출되는 함수이다. 현재 Global 클래스 내부에 저 함수 외에는 아무것도 정의되지 않았으므로 Javascript 내에서 어떤 모듈 혹은 Global 함수를 호출하던 간에 AbstractType(name) 을 리턴받게 될 것이다. AbstractType은 직접 작성한 타입으로, 다음 소스코드에서 확인이 가능하다.


AbstractType.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import PyV8
import Utils
 
class AbstractType(PyV8.JSClass):
    def __init__(self, thisName):
        self.thisName = thisName
 
    # Type is Function
    def __call__(self, *args, **kwargs):
        if len(args) > 0 and len(kwargs.keys()) > 0:
            print ("[*] %s(%s, %s)" % (self.thisName, Utils.argsToStr(args), Utils.kwargsToStr(kwargs)))
        elif len(kwargs.keys()) > 0:
            print ("[*] %s(%s)" % (self.thisName, Utils.kwargsToStr(kwargs)))
        else:
            print ("[*] %s(%s)" % (self.thisName, Utils.argsToStr(args)))
 
        if len(args) > 0:
            return AbstractType(args[0])
        else:
            return AbstractType("By_%s" % (self.thisName))
 
    # Type is Module
    def __getattr__(self, name):
        return AbstractType("%s.%s" % (self.thisName, name))
cs

 이번에 추상화에서 얻어낸 가장 큰 산물인 AbstractType 클래스이다. 생성자에서 호출한 이름을 인자로 받으며(Global.py 참고), __call__ 메서드를 정의하여 인스턴스가 함수로써 사용될 경우 *args, **kwargs 등 가변 길이 변수를 받아서 print로 이름과 함께 로깅하도록 하였다. 또한 Global.py에서처럼 __getattr__ 메서드를 정의하여 모듈 내의 함수에 접근하는 것과 같은 행위를 할 경우 현재 이름에 새 이름을 붙여 AbstractType 클래스를 새로 생성해 리턴하는 것으로 함수의 경우 위에서 정의한 __call__ 에 의해 자동으로 로깅이 되도록 하였다.


 이를 이용하면 따로 모듈과 함수를 직접 구현해주지 않고도 호출하는 문자열을 이용해 그때그때 상황에 맞는 AbstractType을 생성하여 로깅하는 것이 가능해지며, 악성 스크립트 분석에 큰 도움이 될 수 있다.


[실제 악성 스크립트 분석 결과]


 실제로 스크립트 분석 결과 구체적인 모듈이나 함수 정의는 전혀 하지 않았음에도 모듈 및 함수, 그 인자까지 전부 로깅되는 것을 볼 수 있다. 한 언어에 대한 이해가 깊어지면 이렇게 효율적이고 추상적인 코드 작성이 가능해진다는 것을 제대로 느낄 수 있는 프로젝트였다. 현재는 print로밖에 로깅을 진행하지 않지만 이후 파일이나 리턴 값으로 받을 수 있게 하면 자동 분석에도 응용이 가능할 것으로 보인다. 좀 더 고도화를 진행해 볼 예정이다.


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

Python TimedRotatingFileHandler.doRollOver() Error 32 문제


 Python의 logging 모듈에서 기본적으로 제공하는 TimedRotatingFileHandler를 사용하는 도중, 아래와 같은 에러가 발생했다.


https://stackoverflow.com/questions/29220435/windowserror-using-rotatingfilehandler-and-subprocess-popen


Traceback (most recent call last):

  File "c:\Python27\lib\logging\handlers.py", line 77, in emit

self.doRollover()

  File "c:\Python27\lib\logging\handlers.py", line 142, in doRollover

    os.rename(self.baseFilename, dfn)

WindowsError: [Error 32] The process cannot access the file because it is being used by another process


 doRollOverTimedRotatingFileHandler에 지정한 기간이 지났을 경우 사용하던 파일을 백업하고 새로운 로그 파일을 생성하도록 하는 함수이다. Error 32는 다른 프로세스나 스레드에서 같은 파일의 핸들러를 가지고 있기 때문에 변경이 불가능할 때 발생하는 에러이다. 당시 Multiprocessing 환경에서 사용중이었기 때문에 이것이 문제인가 싶어서 프로세스별로 다른 Logger를 사용하도록 변경해 보았으나 그래도 마찬가지로 발생하였고, 이를 해결하려고 수없이 돌아다녔다.

=== [2019-04-03] 업데이트

한동안 발생하지 않아서 방심하고 있었는데 여전히 버그는 발생했다. Error 32가 뜻하는 바도 그렇고 분명 뭔가 멀티프로세스 환경에서 다른 프로세스가 핸들을 물고 있어서 Rotating이 되지 않는 것이 분명했으므로 이를 확인하기 위해 테스트 코드를 짜 봤다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import os
import time
import logging
import logging.handlers
import multiprocessing
 
def GetParentDirectory(filePath):
    return os.path.sep.join(filePath.split(os.path.sep)[:-1])
 
def Mkdirs(dirPath):
    if not os.path.exists(dirPath):
        os.makedirs(dirPath)
 
def CreateLogger(loggerName):
    # Create Logger
    logger = logging.getLogger(loggerName)
    if len(logger.handlers) > 0:
        return logger
 
    logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter('\n[%(levelname)s|%(name)s|%(filename)s:%(lineno)s] %(asctime)s > %(message)s')
 
    # Create Handlers
    logPath = os.path.join(os.path.realpath(""), "logs", loggerName + ".log")
    Mkdirs(GetParentDirectory(logPath))
    streamHandler = logging.StreamHandler()
    streamHandler.setLevel(logging.DEBUG)
    streamHandler.setFormatter(formatter)
    rotatingHandler = logging.handlers.TimedRotatingFileHandler(logPath, when='M', backupCount=10)
    rotatingHandler.setLevel(logging.DEBUG)
    rotatingHandler.setFormatter(formatter)
 
    # Add handlers to logger
    logger.addHandler(streamHandler)
    logger.addHandler(rotatingHandler)
    return logger
 
def LoggingTest(pid):
    subLogger = CreateLogger("Process_%d" % (pid))
    count = 0
    while True:
        subLogger.info("Log : 0x%08x" % count)
        time.sleep(5)
        count += 1
 
def main():
    PROC_COUNT = 10
    mainLogger = CreateLogger("Main-Process")
 
    mainLogger.info("Start Creating Process")
    procList = []
    for i in range(PROC_COUNT):
        procList.append(multiprocessing.Process(target=LoggingTest, args=(i,)))
        procList[i].start()
        time.sleep(0.5)
    mainLogger.info("End Creating Process")
 
    count = 0
    while True:
        mainLogger.info("Main-Log : 0x%08x" % count)
        time.sleep(5)
        count += 1
 
if __name__ == '__main__':
    main()
cs


 1분마다 Rotating하도록 설정하여 돌려봤는데, 아니나다를까 Main-Process 로거에서 doRollOver 에러가 발생한다. multiprocessing.Process로 생성한 자식 프로세스에서 main의 mainLogger 핸들러를 물고 있는 것이 아닌가 하는데... 


 돌려놓고 Rotating이 발생하기 전에 Process Hacker를 이용해 파일 핸들러를 찍어본 모습이다. Main과 Sub 모두 자신의 핸들러를 가지고 있는데, Sub에서 Main의 핸들러를 물고 있는 것을 볼 수 있다.



 doRollOver Error32가 발생한 이후 찍은 모습이다. Main에서는 Rotating을 위해 핸들러를 닫았지만 Sub에서는 계속 물고 있는 것을 볼 수 있다. 이래서 Error32가 발생한 것으로 파악할 수 있었다. 문제는 이걸 어떻게 해결하냐는 것인데... 파이썬 logging 라이브러리 자체가 멀티 프로세싱 환경을 고려하지 않고 만들어졌기 때문에 발생한 일이라 간단하게 해결하긴 힘들어 보인다. 여기(https://docs.python.org/ko/3/howto/logging-cookbook.html)에서는 소켓에 로그를 작성하고 이를 읽어서 기록하는 스레드를 따로 만들어 두고 돌리는 방식으로 처리가 가능함을 설명하고 있으며 실제로 multilog(https://pypi.org/project/multilog/)라고 하는 모듈이 이미 존재한다. 어느 쪽이든 로깅 모듈이 쓰이던 곳이 한두군데가 아니었기 때문에 고치기는 힘들겠지만 최대한 노력해 봐야겠다 (._.


 Windows 환경 해결법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import os
import time
import platform
import logging
import logging.handlers
import multiprocessing
 
def GetParentDirectory(filePath):
    return os.path.sep.join(filePath.split(os.path.sep)[:-1])
 
def Mkdirs(dirPath):
    if not os.path.exists(dirPath):
        os.makedirs(dirPath)
 
def CreateLogger(loggerName):
    # Create Logger
    logger = logging.getLogger(loggerName)
    if len(logger.handlers) > 0:
        print "Already Exists : %s" % (loggerName)
        return logger
    print "Non-Exists : %s" % (loggerName)
    logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter('\n[%(levelname)s|%(name)s|%(filename)s:%(lineno)s] %(asctime)s > %(message)s')
 
    # Create Handlers
    logPath = os.path.join(os.path.realpath(""), "logs", loggerName + ".log")
    Mkdirs(GetParentDirectory(logPath))
    streamHandler = logging.StreamHandler() 
    streamHandler.setLevel(logging.DEBUG)
    streamHandler.setFormatter(formatter)
    rotatingHandler = logging.handlers.TimedRotatingFileHandler(logPath, when='M', backupCount=10)
    rotatingHandler.setLevel(logging.DEBUG)
    rotatingHandler.setFormatter(formatter)
    if platform.system() == 'Windows':
        import msvcrt
        import win32api
        import win32con
        win32api.SetHandleInformation(msvcrt.get_osfhandle(rotatingHandler.stream.fileno()), win32con.HANDLE_FLAG_INHERIT, 0)
 
    # Add handlers to logger
    logger.addHandler(streamHandler)
    logger.addHandler(rotatingHandler)
    
    return logger
 
def LoggingTest(pid):
    subLogger = CreateLogger("Process_%d" % (pid))
    count = 0
    while True:
        subLogger.info("Log : 0x%08x" % count)
        time.sleep(5)
        count += 1
 
def main():
    PROC_COUNT = 10
    mainLogger = CreateLogger("Main-Process")
 
    mainLogger.info("Start Creating Process")
    procList = []
    for i in range(PROC_COUNT):
        procList.append(multiprocessing.Process(target=LoggingTest, args=(i,)))
        procList[i].start()
        time.sleep(0.5)
    mainLogger.info("End Creating Process")
 
    count = 0
    while True:
        mainLogger.info("Main-Log : 0x%08x" % count)
        time.sleep(5)
        count += 1
 
if __name__ == '__main__':
    main()
cs


CreateLogger 함수 내부, TimedRotatingFileHandler를 생성하는 부분 밑에 다음의 코드가 추가되었다.

1
2
3
4
5
    if platform.system() == 'Windows':
        import msvcrt
        import win32api
        import win32con
        win32api.SetHandleInformation(msvcrt.get_osfhandle(rotatingHandler.stream.fileno()), win32con.HANDLE_FLAG_INHERIT, 0)
cs


파이썬 2.7~3.4에서는 multiprocessing으로 프로세스를 생성했을 때, 자식 프로세스가 부모 프로세스의 핸들러들을 상속받아서 같이 물고 있기 때문에 위와 같은 버그가 발생했었는데, 핸들러의 File Handle을 직접 받아와서 SetHandleInformation API를 사용해 상속하지 않도록 설정해주면 자식 프로세스가 생성될때 받아가지 않는다. (참고 : https://stackoverflow.com/questions/948119/preventing-file-handle-inheritance-in-multiprocessing-lib)


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

파이썬 공부를 계속하다가 데코레이터(Decorator)를 공부하게 되었고, 그 내용을 정리하고자 포스팅한다.


여기 아주 엄청난 일을 하는 함수가 있다.

1
2
def HelloName(name):
    print "Hello, %s!" % (name)
cs

 무려 인자로 이름을 입력받아 인사를 해주는 무척 대단한 함수이다. 아무 문제 없이 함수를 사용하다 보니, 이제 만났을때 하는 인사 외에도 헤어질 때 하는 인사를 만들고 싶어졌다. 정말 열심히 공을 들여서 다음과 같은 함수를 만들어냈다.


1
2
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
cs

 만났을 때 인사하고, 헤어질 때 인사하고 나니 안부를 묻는 말도 만들고 싶어졌다. 또 열심히 공을 들여서 다음과 같은 함수를 만들어냈다.


1
2
def HowAreYouName(name):
    print "How are you, %s?" % (name)
cs

 뿌듯하게 바라보는 와중, 문득 머릿속에서 이 함수들의 기능만으로는 너무 밋밋하다는 생각이 들어서 출력하기 전에 위아래에 경계선을 출력하도록 만들고 싶어졌다. 그래서 만들어진 결과가 다음과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
def HelloName(name):
    print "============================"
    print "Hello, %s!" % (name)
    print "============================"
 
def GoodbyeName(name):
    print "============================"
    print "Goodbye, %s!" % (name)
    print "============================"
 
def HowAreYouName(name):
    print "============================"
    print "How are you, %s?" % (name)
    print "============================"
cs

 딱 봐도 무엇인가 많이 반복되고 있고, 불편함이 느껴진다. 이 함수들은 모두 다른 함수들이지만, 공통적으로 위아래에 경계선을 출력한다는 행위는 동일하다.

 파이썬에서 함수는 다른 함수의 인자로 넘길 수도 있고, 리턴할 수도 있는 특징을 가지고 있다. 이를 이용해서 일단 데코레이터를 사용하지 않고 원시적인 방법을 사용하여 이 문제를 해결해 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def AddLine(targetFunction, args):
    print "============================"
    targetFunction(args)
    print "============================"
 
def HelloName(name):
    print "Hello, %s!" % (name)
 
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    AddLine(HelloName, "5kyc1ad")
    AddLine(GoodbyeName, "5kyc1ad")
    AddLine(HowAreYouName, "5kyc1ad")
cs

 함수와 그 인자를 넘겨 함수 시작 전, 함수가 끝난 후에 경계선을 출력하도록 하는 새로운 함수를 만들고, 그것을 이용해 각자의 함수를 실행하도록 변경해 보았다. 원하던 대로 결과가 나오기는 하지만, 무엇인가 불편한 기분이 드는 것은 여전하다. 호출하고 싶은 함수는 HelloName, GoodbyeName, HowAreYouName 인데 실제 호출하는 함수는 AddLine이고, 인자의 전달도 직관적으로 눈에 들어오지 않는다.

직관적으로 HelloName 함수를 실행하는 것이 보이면서 인자의 전달도 깔끔하게 정리할 수 있는 방법이 없을까?

답은 '함수를 새로 만들어서 사용하는 것' 이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def AddLine(targetFunction):
    def wrapper(args):
        print "============================"
        targetFunction(args)
        print "============================"
    return wrapper
 
def HelloName(name):
    print "Hello, %s!" % (name)
 
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    newHelloName     = AddLine(HelloName)
    newGoodbyeName   = AddLine(GoodbyeName)
    newHowAreYouName = AddLine(HowAreYouName)
 
    newHelloName("5kyc1ad")
    newGoodbyeName("5kyc1ad")
    newHowAreYouName("5kyc1ad")
cs

 AddLine 함수 내에 wrapper라는 이름으로 다른 함수 하나를 선언하고, 내부에서 인자로 전달받은 함수를 실행하기 전, 실행이 끝난 후에 줄을 추가로 출력하도록 한 후, AddLine 함수가 끝날 때 wrapper 함수 자체를 반환하도록 만들었다. 이제 main 에서 AddLine 함수에 원하는 출력 함수를 넣은 후 리턴된 함수를 받아와서 사용해주면 된다.

 정답에 아주 가까워졌지만 여전히 불편한 점이 몇가지 있다. 내가 선언한 함수는 HelloName인데 새로 newHelloName을 만들어서 사용해주어야 한다는 점, 함수마다 전부 저 작업을 추가로 해주어야 한다는 점 등이다. 선언한 함수를 그냥 사용할 수 없다는 점에서 또 직관성이 떨어지는 코드이기도 하다.

 이 모든 문제점을 해결하기 위해 사용하는 것이 데코레이터(Decorator)이다. 다른 함수들의 시작 전이나 후에 특정한 장식(Decoration)을 붙여 주는 것이다. 여기의 예제에서는 위아래에 붙은 경계선이 '장식' 되시겠다.

사용법은 생각보다 무척이나 간단하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def AddLine(targetFunction):
    def wrapper(*args, **kwargs):
        print "============================"
        targetFunction(*args, **kwargs)
        print "============================"
    return wrapper
 
@AddLine
def HelloName(name):
    print "Hello, %s!" % (name)
 
@AddLine
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
@AddLine
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    HelloName("5kyc1ad")
    GoodbyeName("5kyc1ad")
    HowAreYouName("5kyc1ad")
cs

wrapper 에서 받는 인자가 *args, **kwargs 가 되어서 당황할 수 있지만, 사실 이는 가변 인자를 넘겨주기 위한 것으로 생각보다 간단한 개념이다. 여기에 잘 설명되어 있으므로 참고.

 AddLine 함수는 달라진 점이 전혀 없고, 각 함수 위에 @AddLine 한 줄씩이 추가된 것을 볼 수 있다. @AddLine은 데코레이터 AddLine을 이 함수에 적용시키겠다는 의미이다. 각 함수들은 선언 순간 데코레이터 AddLine이 적용되어 리턴된 wrapper 함수를 자기 자신으로서 갖게 되고, 따라서 사용할 때는 별도의 작업 없이 선언한 함수를 그대로 사용하면 된다.

이것이 가장 기초적인 데코레이터의 사용법이다.


이제 여기서 한 계단만 더 올라가보자.

 데코레이터를 사용하다 보면 데코레이터 자체가 인자를 받는 모습을 볼 수 있다. 예를 들면 Flask를 이용한 REST API 개발을 해본 사람이라면, 다음과 같은 코드가 함수 앞에 붙는 것을 봤을 것이다.


1
2
3
@app.route("/file", methods=["GET"])
def blah_blah():
    pass
cs

 딱 봐서는 blah_blah 라는 함수에 app.route라는 데코레이터를 적용하는 코드로 보이는데, 지금까지 보지 못한 부분이 추가되었다.

바로 app.route에 ("/file", methods=["GET"]) 로 인자를 받는 부분이 추가되었다는 것이다.


 생각해 보면 지금까지 작성한 예시에서 데코레이터가 하는 일을 나타내어 봤을 때 '함수 시작 전과 끝난 후에 특정 줄을 하나씩 출력한다'라고 표현할 수 있다. 그런데, 모든 함수가 다 같은 줄만을 출력하는 것이 아닌 특정 문자열을 출력하고 싶은 것일 수도 있다. 즉, 같은 데코레이터를 이용하여 다른 내용을 출력할 수 있도록 만들고 싶을 수 있는 것이다.

 이렇게 데코레이터에 인자를 넣기 위해서는 데코레이터 함수를 한번 더 감싸주면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def AddLine(targetStr):
    def RealDecorator(targetFunction):
        def wrapper(*args, **kwargs):
            print targetStr
            targetFunction(*args, **kwargs)
            print targetStr
        return wrapper
    return RealDecorator
 
@AddLine("===HelloName===")
def HelloName(name):
    print "Hello, %s!" % (name)
 
@AddLine("===GoodbyeName===")
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
@AddLine("===HowAreYouName===")
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    HelloName("5kyc1ad")
    GoodbyeName("5kyc1ad")
    HowAreYouName("5kyc1ad")
cs

이런 식으로 말이다.

함수 위에 @AddLine으로 명시된 함수들은 선언 순간 AddLine 데코레이터를 적용시키게 되며, AddLine 데코레이터에는 출력할 문자열을 인자로 넘기도록 되어 있다. 위 예시의 실행 결과는 다음과 같다.


각각의 함수에서 AddLine 데코레이터에 인자로 넘긴 문자열이 함수의 시작과 끝에서 정상적으로 출력되는 모습을 확인할 수 있다.


 데코레이터를 잘만 이용하면 Repeat 등의 이름을 가진 데코레이터를 만들어서 한 함수를 여러 번 반복시키는 것도 가능하다.

대표적인 문제가 codefights.com 의 Arcade - Python의 77번째 문제인 'Merging Vines' 이다.

상당히 재미있고 데코레이터를 익히는 데에 적절한 문제이므로 한번씩 풀어보는 것도 좋을 것 같다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,


Codefights.com에서 Arcade - Python 을 풀다가 상당히 재미있는 버그를 하나 알게 되어 작성한다.


1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
 
int division(int x, int y)
{
    return x / y;
}
int main()
{
    std::clog << division(15-4<< std::endl;
    return 0;
}
cs

 C에서 이렇게 int끼리의 나눗셈의 결과가 나머지가 존재할 경우, '/' 연산자 특성상 나머지를 버리고 몫만을 리턴하기 때문에 결과 값은 -3이 된다. 소숫점으로 나타내면 -3.xx~ 로 나타나겠지만 int 형으로 형변환하면서 -3이 되는 것이다. 

( C에서는 두 오퍼랜드 모두 Integer형일 경우 Integer를 반환하고, 하나라도 Float일 경우 Float형으로 소숫점을 붙여 반환한다. 이를 Classic Division이라고 한다. )


1
2
3
4
def division(x, y):
    return x / y
 
print division(15-4)
cs

 하지만 같은 역할을 할 법한 코드를 Python 2에서 작성하여 쓸 경우, 예상과는 달리 -4가 리턴된다. 

 이는 흔히 Floor Division 이라고 일컫는 정수 나눗셈 방법인데, 두 오퍼랜드가 모두 정수이고 나누어져서 나온 결과가 정수가 아니라면 항상 나온 값보다 작은 정수를 리턴하는 방식이다. 예를 들어, 5/3 의 경우 1.xx 로 1보다는 크고 2보다는 작은 값이 나오므로 1이 리턴된다. 따라서 위의 예시인 15/-4 를 할 경우 -3과 -4 사이의 값이므로 더 작은 값인 -4가 리턴되는 것이다. 이 방식이 문제가 되는 이유는, 흔히 참이라고 생각하는 연산이 거짓이 된다는 점에 있다.

 예를 들면, 정수 x와 y에 대해서, (x/-y) == -(x/y) 는 당연히 참이 되어야 하지만, 실제 위의 예시를 들어 보면 (15/-4) 는 -4가 되고 -(15/4) 는 -3이 되므로 거짓이 되어버린다. 

 이 문제는 Python 3 버전에서는 고쳐졌다고 한다. Python 3에서는 정수를 정수로 나누어도 Float을 반환해버리는 True Division을 사용하기 때문에 애초부터 저런 문제가 발생하지 않으며, Floor Division을 사용하고 싶을 때에는 15//-4 처럼 '//' 연산자를 사용하면 된다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

Windows에서 python-magic 사용하기


리눅스 환경에서는 python-magic을 사용하기 위한 Dependency가 이미 충족되어 있기 때문에, 혹은 쉽게 설치가 가능하기 때문에 python-magic을 사용하는 데 큰 문제가 없지만, 윈도우에서는 그런 게 없기 때문에 몇 가지 절차를 거친 후에야 python-magic을 사용할 수 있다.


python-magic Github : https://github.com/ahupp/python-magic


 python-magic의 Github에 씌여 있는 설명대로 진행하면 윈도우에서도 python-magic을 문제없이 사용할 수 있다.

우선 magic의 사용에 필요한 라이브러리를 준비해야 한다.

magic1.dll, regex2.dll, zlib1.dll의 세 가지인데, 이는 각각 다음 주소에서 다운받을 수 있다.


magic1.dll : http://gnuwin32.sourceforge.net/packages/file.htm

regex2.dll : http://gnuwin32.sourceforge.net/packages/regex.htm

zlib1.dll  : http://gnuwin32.sourceforge.net/packages/zlib.htm


 굳이 Complete package를 다운받을 필요 없이 Binaries만 다운받아서 압축을 풀고, bin 폴더 안으로 들어가면 필요한 dll을 확인할 수 있다. 이 세개의 dll 파일을 다운받고, DLL이 들어 있는 폴더의 경로를 환경변수 PATH에 추가하던지 환경변수 PATH에 있는 폴더에 DLL을 복사하던지 둘 중 편한 방식을 사용하면 된다. 필자의 경우 후자가 편하기 때문에 PATH 내에 들어 있는 C:\Python27 내부에 라이브러리를 복사해 두었다.

 다음으로 위에서 magic1.dll을 위해 다운받아서 압축을 푼 폴더 기준으로 share\misc\ 에 있는 magic과 magic.mgc 파일을 원하는 위치에 두고 사용하면 된다.

 python-magic은 magic.Magic 클래스의 인스턴스를 생성하여 이 인스턴스의 메서드를 호출하여 파일의 타입을 판별하도록 해야 한다. 사용법 자체는 Github에도 있고 구글링해도 쉽게 나오므로 따로 작성하지는 않겠다.


** magic 파일을 환경변수 PATH에 지정된 폴더에 넣고 사용할 경우 다음과 같은 에러가 발생할 수 있다.

파이썬에서 import magic을 할 경우 다음과 같은 에러가 발생한다.

에러가 발생한 C:\Python27\lib\site-packages\magic.py를 열고 확인해 보면 다음과 같은 부분이 문제이다.

이 소스대로라면 리눅스의 magic 라이브러리를 먼저 탐색하기 때문에 먼저 'magic'을 찾고, 찾지 못했을 경우에 'magic1'을 찾는다. 우리가 다운로드 받은 magic 라이브러리는 magic1.dll 이기 때문에 magic을 먼저 찾는 과정에서 magic 바이너리가 발견되었고, 이것을 그대로 로드하려다가 문제가 발생한 것이다.

 따라서 ctypes.util.find_library('magic') 부분을 뒤로 보내서 먼저 'magic1'을 찾게 하던지, 소스 자체를 삭제하던지 편할대로 하면 된다. 필자는 확실하게 하기 위해 아예 삭제해버렸고, 그 소스는 다음과 같이 변한다.

이제 이를 저장하고 위에 적힌 대로 사용하면 된다.


** 64비트 파이썬을 사용하는 경우에는 magic1.dll 대신 64비트로 빌드된 libmagic 라이브러리를 사용해야 한다.

이는 다음 링크에서 다운로드 받을 수 있다.

64-bit libmagic: https://github.com/pidydx/libmagicwin64


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,



 파이썬을 사용하는 사람이라면 List의 편리함을 아주 잘 알고 있을 것이라고 생각한다. 이번에 파이썬으로 프로그램을 좀 짜던 중, 리스트를 복사해야 하는 일이 생겼는데 늘 해왔던 대로 변수를 복사하듯이 list2 = list1 이런 식으로 대입했더니 list2를 변경하면 list1까지 같이 변경되어버리는 문제가 생겼다. 파이썬이 C 기반으로 만들어진 언어인데, 내부적으로 얕은 복사를 사용하는 모양이었다.



(이런식으로...)



 이 방식을 해결하고 독립적으로 내부 값들을 전부 복사하여 새 리스트를 생성하는 간단한 방법은 다음 두 가지의 방법이 있다.



list2 = list1[:]



list2 = list(list1)



 하지만 위와 같은 방식을 사용하면 겉으로는 깊은 복사처럼 보이지만 리스트 내의 변수를 새로운 리스트에 담아서 주는 것 뿐이기 때문에 리스트 내의 리스트를 사용할 경우 해당 내부의 리스트까지 제대로 깊은 복사가 되지 않는다. 결국은 얕은 복사라는 의미이다. 


(여전히 얕은 복사이다)



이걸 해결하기 위해서는 copy.deepcopy 함수를 사용하면 된다.




블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,