스펙지수 계산기

저는 몇 달 전부터 방치형 모바일 게임을 하나 하고 있습니다. 그 게임에선 유저들 사이에서 "님 스펙 몇?" 같은 방식으로 사용되는 "스펙지수"라는 수치가 존재합니다. 자신의 유닛들의 몇몇 수치를 입력하면 자신의 투자효율을 계산해주는 것이죠. 그런데 모두가 사용하는 계산기는 EXE로 되어있어서 맥 유저인 저는 써볼 수가 없었습니다. 그래서 EXE 파일 내에 있는 공식을 어떻게 뜯어낼 수 없을까 하는 생각이 들었습니다.

너의 정체는.

일단 가장 먼저 해본 것은 이 파일이 C#으로 작성되었다고 가정하는 것이었습니다. 이유는 단순, C#으로 작성된 EXE는 Visual Studio에 던져넣는것 만으로도 코드를 볼 수 있기 때문입니다. 하지만 Visual Studio님은 EXE를 던져넣자 응답없음 상태가 되셨죠.

적어도 C#은 아닌 것 같아서 다른 분석툴을 찾다가 Hopper Disassembler를 알게 되었습니다. 이 프로그램은 상용 프로그램이지만 데모 버전으로도 EXE를 풀 수 있는지 정도는 알 수 있을테고, 간단히 훑어보고 어려워보이면 바로 포기할 생각이었죠. 그래서 일단 열어보았습니다.

Hopper Disassembler가 보여주는 Symbol들

코드를 읽을 순 없었지만 뭔가 친근한 단어들이 보입니다.

이거 혹시 Python 코드인가?

뜯어보자!

그래서 Python으로 만들어진 EXE를 뜯어보는 방법을 조사하다가 좋은 글1을 발견했습니다. 이 글의 내용을 간단하게 요약하면

  1. Python 스크립트를 EXE로 만드는 툴로는 py2exePyInstaller가 널리 쓰인다.
  2. 두 종류 다 대응하는 Awesome한 script를 GitHub에 올려두었으니 사용해라.

그래서, 감사히 사용해보기로 했습니다.

설치

아주 당연한 이야기지만 Python이 필요합니다. 하지만 Python 3.7에서는 잘 안되므로 주의가 필요합니다.

$ git clone https://github.com/countercept/python-exe-unpacker.git
$ cd python-exe-unpacker
$ pip install -r requirements.txt

준비

경로를 길게 입력하는건 취미가 아니므로 분해할 파일을 현재 디렉토리로 옮겨왔습니다.

$ cp path/of/program.exe ./program.exe

분해

$ python python_exe_unpack.py -i program.exe
[*] On Python 3.6
[*] Processing program.exe
[*] Pyinstaller version: 2.1+
[*] This exe is packed using pyinstaller
[*] Unpacking the binary now
[*] Python version: 36
[*] Length of package: 7627814 bytes
[*] Found 931 files in CArchive
[*] Beginning extraction...please standby
[*] Found 139 files in PYZ archive
[*] Successfully extracted pyinstaller exe.

생각보다 순식간에 분해가 되었습니다.

끝이 아니었다

분해 결과물을 보기 위해 뒤적여봤지만 정작 제가 원하는 본체가 보이질 않았습니다. 일단 파일명이 tk라던가 tcl로 시작하는 파일은 제 관심사가 아닌게 확실해보였습니다.

$ rm -rf unpacked/program.exe/tk* unpacked/program.exe/tcl*

불필요한 파일을 깡그리 지우고 나니 내용물이 정리가 되었습니다.

  1. .pyd 파일
  2. .zip 파일 - 압축을 풀면 Python 기본 라이브러리의 .py 파일들이 들어있습니다.
  3. .dll 파일 - Python이나 VC 관련이므로 볼 필요가 없을게 자명했습니다.
  4. out00-PYZ.pyz_extracted 디렉토리 - 안에는 Python 기본 라이브러리의 .pyc 파일들이 들어있습니다.
  5. 메인 로직이 담긴 파일 (확장자가 없다)
  6. 기타 정체를 알 수 없는 확장자 없는 파일들

GitHub README에서는 .pyc 파일을 이용해서 나머지 파일들을 해독할 수 있다고 적혀있었지만, 문서에 적힌 명령어로는 해독이 되질 않았습니다.

Python magic numbers

.pyc 파일을 읽을 수 있는 형태로 되돌리려면 Python magic number라는 값이 필요했습니다. 이것이 남아있는 파일은 uncompyle6을 이용해서 내용을 볼 수 있지만, 그렇지 않은 파일은 볼 수 없는 것이죠. PyInstaller는 아무래도 main파일에서 magic number를 없애버리는 모양이었습니다. 하지만 분해 결과에는 magic number가 남아있는 .pyc 파일이 많이 있었죠. uncompyle6로 열어볼 수 있는 파일들의 시작부분과 메인 로직의 시작부분을 대조해보니 다른 점이 보였습니다.2

  • 해독이 되는 파일: 330d 0d0a 0000 0000 0000 0000 3f00 로 시작한다
  • 해독이 안 되는 파일: 3f00 로 시작한다.

즉, 제게 필요한 magic number 값은 330d 0d0a 0000 0000 0000 0000 이었습니다.

$ printf "\x33\x0d\x0d\x0a\x00\x00\x00\x00\x00\x00\x00\x00" | cat - unpacked/program.exe/1.1Ver > main.pyc
$ uncompyle6 main.pyc > main.py
$ cat main.py

열렸다!

결과물

뒷 이야기

그렇게 풀린 결과를 열심히 읽어서 결국 원하는 공식을 추출해내는데 성공했지만 생각보다 함정이 많았습니다. 리버스 엔지니어링과는 연이 없는 저였기에 모든 과정이 어려웠지만 가장 당혹스러웠던것은 가장 마지막 결과물이었습니다.

Python이지만 Python이 아닌

일부 코드는 코드로 반환되지 않고 저런 형태로 출력되더군요. 이것은 Python의 내장모듈인 dis를 사용하면 나오는 코드입니다. 제 경우에는 생각보다 읽을만(?) 했던지라 수작업으로 해독했습니다.