Saturday, 7 April 2012

Decompiling the EVE client

I've realized that in order to make my own injections, I need to be able to read the Python source code from the EVE client. It took some time, but I've finally managed to work out how.
First you need to install Python on your system. Python can be found at http://python.org/download/, I use version 2.7.3.

Second you need a tool to dump the source code of a running python process. The injection is done with either InEve (available via this thread http://www.publicdemands.co.uk/showthread.php?935-Ineve-packaged-compling-python-injector) or lolpy4 (available via this thread http://www.publicdemands.co.uk/showthread.php?910-lolpy3), both obtained  from publicdemands. You'll have to create an account there to be able to download either injector.

I use lolpy4 for no specific reason, I'm sure InEve will get the job done as well.

Third, create a dump/work directory. I use c:\eve and this walk-through will assume you do as well.

After obtaining the injector (and saving it in c:\eve) you need to inject a python script that dumps all the code. I can't remember where I found the script, but the listing is here: (simply cut and paste to a new text document and save it as dump.py in c:\eve)

from nasty import nasty, UnjumbleString
import cPickle
import blue
import struct
import imp
import os
import zipfile

store_path="c:/eve/eve-%.2f.%s/" % (boot.version, nasty.GetAppDataCompiledCodePath().build or boot.build)
   
root_store_path = store_path + "eve/"
script_store_path = store_path + "eve/client/script/"

(fileData, fileInfo,) = blue.win32.AtomicFileRead(nasty.compiledCodeFile)
datain = cPickle.loads(fileData)
code = cPickle.loads(datain[1])["code"]
for (k, v,) in code:
    c = v[0]
    c = UnjumbleString(c, True)
    ksplit = k[0].split(':/')
    filename = script_store_path  if ksplit[0] == "script" else  root_store_path
    filename += ksplit[1] +"c"
    print filename
    (dir,file) = os.path.split(filename)
    if not os.path.exists(dir):
        os.makedirs(dir)
    with open(filename,"wb") as x:
        mtime = os.path.getmtime(filename)
        mtime = struct.pack('<i', mtime)
        x.write(imp.get_magic() + mtime)
        x.write(c)
for root, dirs, files in os.walk(blue.paths.ResolvePath(u'lib:/')):
        for libname in files:
            zf = zipfile.ZipFile(os.path.join(root, libname), 'r')
            out = store_path + "lib/" + libname[:-4] + "/"
            for path in zf.namelist():
                tgt = os.path.join(out, path)[:-1]+"c"
                print tgt
                tgtdir = os.path.dirname(tgt)
                if not os.path.exists(tgtdir):
                    os.makedirs(tgtdir)
                with open(tgt, 'wb') as fp:
                    fp.write(UnjumbleString(zf.read(path), True))

All you gotta do now is start up EVE, don't log in or anything, and then run the command:

lolpy4.exe dump.py

You'll now have a new directory in c:\eve that is named after the version of the EVE client you just dumped. Unfortunately the files are still compiled python files and are as such unreadable. You still need to decompile them. For this we use another tool; uncompyle2 which can be found here: (https://github.com/wibiti/uncompyle2).

To use uncompyle2 you need to install it as a site-package. Simply browse to the directory where you unpacked uncompyle2 and install Setup.py using a command similar to this:

c:\python27\python.exe setup.py install

When the installation is done, you should be have a directory similar to this under your python installation directory:

C:\Python27\Lib\site-packages\uncompyle2

Time to decompile EVE. I use a script I've found somewhere, again I don't remember where: (Cut and paste into a new text document, save it as uncompyle.py in c:\eve)

#!c:\python27\python.exe
# Mode: -*- python -*-
#
# Copyright (c) 2000-2002 by hartmut Goebel <hartmut@goebel.noris.de>
#
"""
Usage: uncompyle [OPTIONS]... [ FILE | DIR]...

Examples:
  uncompyle      foo.pyc bar.pyc       # uncompyle foo.pyc, bar.pyc to stdout
  uncompyle -o . foo.pyc bar.pyc       # uncompyle to ./foo.dis and ./bar.dis
  uncompyle -o /tmp /usr/lib/python1.5 # uncompyle whole library

Options:
  -o <path>     output decompiled files to this path:
                if multiple input files are decompiled, the common prefix
                is stripped from these names and the remainder appended to
                <path>
                  uncompyle -o /tmp bla/fasel.pyc bla/foo.pyc
                    -> /tmp/fasel.dis, /tmp/foo.dis
                  uncompyle -o /tmp bla/fasel.pyc bar/foo.pyc
                    -> /tmp/bla/fasel.dis, /tmp/bar/foo.dis
                  uncompyle -o /tmp /usr/lib/python1.5
                    -> /tmp/smtplib.dis ... /tmp/lib-tk/FixTk.dis
  -c <file>     attempts a disassembly after compiling <file>
  -d            do not print timestamps
  -p <integer>  use <integer> number of processes
  -r            recurse directories looking for .pyc and .pyo files
  --verify      compare generated source with input byte-code
                (requires -o)
  --help        show this message

Debugging Options:
  --showasm   -a  include byte-code                  (disables --verify)
  --showast   -t  include AST (abstract syntax tree) (disables --verify)

Extensions of generated files:
  '.dis'             successfully decompiled (and verified if --verify)
  '.dis_unverified'  successfully decompile but --verify failed
  '.nodis'           uncompyle failed (contact author for enhancement)
"""
from threading import Thread
from multiprocessing import Process, Queue
from Queue import Empty
from uncompyle2 import main, verify

def process_func(src_base, out_base, codes, outfile, showasm, showast, do_verify, fqueue, rqueue):
    try:
      (tot_files, okay_files, failed_files, verify_failed_files) = (0,0,0,0)
      while 1:
          f = fqueue.get()
          if f == None:
              break
          (t, o, f, v) = \
              main(src_base, out_base, [f], codes, outfile, showasm, showast, do_verify)
          tot_files += t
          okay_files += o
          failed_files += f
          verify_failed_files += v
    except (Empty, KeyboardInterrupt, OSError):
      pass
    rqueue.put((tot_files, okay_files, failed_files, verify_failed_files))
    rqueue.close()

if __name__ == '__main__':
    Usage_short = \
    "decomyple [--help] [--verify] [--showasm] [--showast] [-o <path>] FILE|DIR..."

    import sys, os, getopt
    import os.path
    import time

    showasm = showast = do_verify = numproc = recurse_dirs = 0
    outfile = '-'
    out_base = None
    codes = []
    timestamp = True
    timestampfmt = "# %Y.%m.%d %H:%M:%S %Z"

    try:
        opts, files = getopt.getopt(sys.argv[1:], 'hatdro:c:p:',
                               ['help', 'verify', 'showast', 'showasm'])
    except getopt.GetoptError, e:
        print >>sys.stderr, '%s: %s' % (os.path.basename(sys.argv[0]), e)
        sys.exit(-1)   

    for opt, val in opts:
        if opt in ('-h', '--help'):
            print __doc__
            sys.exit(0)
        elif opt == '--verify':
            do_verify = 1
        elif opt in ('--showasm', '-a'):
            showasm = 1
            do_verify = 0
        elif opt in ('--showast', '-t'):
            showast = 1
            do_verify = 0
        elif opt == '-o':
            outfile = val
        elif opt == '-d':
            timestamp = False
        elif opt == '-c':
            codes.append(val)
        elif opt == '-p':
            numproc = int(val)
        elif opt == '-r':
            recurse_dirs = 1
        else:
            print opt
            print Usage_short
            sys.exit(1)

    # expand directory if specified
    if recurse_dirs:
        expanded_files = []
        for f in files:
            if os.path.isdir(f):
                for root, _, dir_files in os.walk(f):
                    for df in dir_files:
                        if df.endswith('.pyc') or df.endswith('.pyo'):
                            expanded_files.append(os.path.join(root, df))
        files = expanded_files

    # argl, commonprefix works on strings, not on path parts,
    # thus we must handle the case with files in 'some/classes'
    # and 'some/cmds'
    src_base = os.path.commonprefix(files)
    if src_base[-1:] != os.sep:
        src_base = os.path.dirname(src_base)
    if src_base:
        sb_len = len( os.path.join(src_base, '') )
        files = map(lambda f: f[sb_len:], files)
        del sb_len
       
    if outfile == '-':
        outfile = None # use stdout
    elif outfile and os.path.isdir(outfile):
        out_base = outfile; outfile = None
    elif outfile and len(files) > 1:
        out_base = outfile; outfile = None

    if timestamp:
        print time.strftime(timestampfmt)
    if numproc <= 1:
        try:
            result = main(src_base, out_base, files, codes, outfile, showasm, showast, do_verify)
            print '# decompiled %i files: %i okay, %i failed, %i verify failed' % result
        except (KeyboardInterrupt, OSError):
            pass
        except verify.VerifyCmpError:
            raise
    else:
        fqueue = Queue(len(files)+numproc)
        for f in files:
            fqueue.put(f)
        for i in range(numproc):
            fqueue.put(None)
           
        rqueue = Queue(numproc)
       
        try:
            procs = [Process(target=process_func, args=(src_base, out_base, codes, outfile, showasm, showast, do_verify, fqueue, rqueue)) for i in range(numproc)]
            for p in procs:
                p.start()
            for p in procs:
                p.join()
            try:
                (tot_files, okay_files, failed_files, verify_failed_files) = (0,0,0,0)
                while 1:
                    (t, o, f, v) = rqueue.get(False)
                    tot_files += t
                    okay_files += o
                    failed_files += f
                    verify_failed_files += v
            except Empty:
                pass
            print '# decompiled %i files: %i okay, %i failed, %i verify failed' % \
                  (tot_files, okay_files, failed_files, verify_failed_files)
        except (KeyboardInterrupt, OSError):
            pass
           
    if timestamp:
        print time.strftime(timestampfmt)

Now, from the directory where you dumped EVE into, run the following command: (replace [dumpdir] with the name of the directory that got created when using lolpy4 - eg. eve-7.21.123456)

c:\python27\python.exe uncompyle.py -o [dumpdir]-DECOMPILED -r [dumpdir]

This will take some time but when finished you'll have all the source code for the EVE-client.

Finally you might want to rename the files so they can be opened by you favourite python editor, to do this run this command from the command prompt in the directory you just created.

for /r %x in (*.pyc_dis) do ren "%x" *.py

Enjoy reading!

1 comment: