#!/usr/bin/python2.5
# coding: latin1
#TODO fix naming conventions, make them consistent
#TODO is it ok to use run trace information from after hitting the OEP?
#TODO add support for "less efficient" method for API function calls to IAT finder

import sys
try:
    import sqlite3 as sqlite
except:
    from pysqlite2 import dbapi2 as sqlite

import Image
import ImageDraw
import ImageFont

import time
sys.path.append( '/opt/libdasm/lib') # path for the pydasm extension
sys.path.append( '/home/lutz/Thesis/bochs-stuff')
sys.path.append( '/home/lutz/Thesis/bochs-stuff/bochs/instrument/unpacking')

sys.path.append( '/home/lutz/pandora/bochs-stuff')
sys.path.append( '/home/lutz/pandora/bochs-stuff/bochs/instrument/unpacking')

import pydasm

from Structures import *

#from pylab import *



class IAT( PEList):

    def __init__( self, analysis, offset):

        self.analysis = analysis

        self.pe_obj = self.analysis.Images[ self.analysis.imagename]

        PEList.__init__( self, self.pe_obj, offset, "I")
        print self
        

        count = 0
        self.imports_info = []
        self.Imagecounts =  {}
        print "Going up the possible IAT: 0x%08x" % offset

        while 0 != self[ count] and None != self.analysis.getExportByAddress( self[ count]):

            function_address = self[ count]

            image = self.analysis.getImageByAddress( function_address)
            image_name = image.name

            export = image.Exports.by_va( function_address)
            function_name = export[ 2]

            if image_name in self.Imagecounts:
                self.Imagecounts[ image_name] += 1
            else:
                self.Imagecounts[ image_name] = 1


            self.imports_info.append( { "name": function_name, "image": image_name})
            print "0x%08x:0x%08x:%s!%s" % ( count, \
                function_address, \
                image_name, \
                function_name)
            count += 1

        self.len = count #FIXME what about a non-zero entry that's nowhere to be found in any image?

        for name in self.Imagecounts:
            print "%s: %u" %( name, self.Imagecounts[ name])

        self.normalize()

    def normalize(self):

        image_max = 0
        image_name = ""
        self.HintNameTable = ""

        # determine the name of the DLL providing the majority of imports in this IAT
        for name in self.Imagecounts:
            if self.Imagecounts[ name] > image_max:
                image_max = self.Imagecounts[ name]
                image_name = name

        successful = True

        for index in range( self.len):
            # image_name is the name of the DLL that holds the majority in this IAT


            # need to fetch images before working on them
            # self is to be treated as a list


            if self.imports_info[ index][ "image"] != image_name:
                print image_name, "0x%08x" % self[ index],
                function_name = self.imports_info[ index][ "name"]
                forwarded_image = self.analysis.getImageByAddress( self[ index])
                forwarded_name = forwarded_image.name.partition( ".")[0] + "." + function_name
                print forwarded_name
                export = self.analysis.Images[ image_name].Exports.by_forwarder( forwarded_name)
                successful &= ( None != export)

                if successful:
                    self.imports_info[ index][ "image"] = image_name
                    self.imports_info[ index][ "name"] = export[ 2]
                    self[ index] = self.analysis.Images[ image_name].Headers.OptionalHeader.WindowsSpecific.ImageBase + export[ 1]
                    print "Found %s in" % export[ 2], image_name, "0x%08x" % self[ index]

            if not successful:
                print "Couldn't find export for forwarded name %s" % forwarded_name, export
                for rva in self.analysis.Images[ image_name].Exports.ExportAddressTable:
                    print self.analysis.Images[ image_name].Exports.by_rva( rva)
#                raise Exception( "Couldn't fix up IAT properly for DLL %s" % image_name)

            # create the Hint/Name Table, use a hint of 0 for all imports
            # self.imports_info[ index][ "HintNameTableOffset"] = len( self.HintNameTable)
            # self.HintNameTable += "\x00\x00" + self.imports_info[ index][ "name"]

        print "Decided on %s: %u" % (image_name, image_max)
        self.image_name = str(image_name)



class Analysis( object):

    def __init__( self, dbfilename, imagename, time):

        self.dbfilename = dbfilename
        self.dbcon = sqlite.connect( dbfilename)
        self.dbcon.text_factory = str
        self.imagename = imagename
        self.time = time
        self.IATs = []
        self.Images = {}

        query = "SELECT dump, images.base, size, basedllname FROM image_dumps " \
                "INNER JOIN images ON image_dumps.base = images.base AND image_dumps.pdb = images.pdb " \
                "WHERE /* time = ?1 AND */ image_dumps.pdb = (SELECT pdb FROM images WHERE basedllname = ?2)"

        for (dump, base, size, name) in self.dbcon.execute( query, (self.time, self.imagename)):
            print "0x%08x" % base, size, name
            self.Images[ name] = PE( CopyingStringBackend( dump), name, True)

#        self.fix_entrypoint() FIXME!
        self.fix_sections()
        self.fix_imports()


    def fix_entrypoint( self):
        # FIXME
        # for now, assume that the time of hitting the entrypoint equals the time of dump
        (entrypoint,) = self.dbcon.execute( "SELECT target FROM branches WHERE time = ?1", (self.time,)).fetchone()
        imagebase = self.Images[ self.imagename].Headers.OptionalHeader.WindowsSpecific.ImageBase
        self.Images[ self.imagename].Headers.OptionalHeader.Standard.AddressOfEntryPoint = entrypoint - imagebase
        print "Set new entrypoint to 0x%08x" % entrypoint

    def fix_imports( self):
        # approach:
        # - find IATs
        # - reconstruct Import Directory
        #    - in place or append a new one
        #    - what to do with the original import directory?
        # - fix the IATs
        # - update data directory
        
        self.find_IATs()

        # Size of one Import Directory Entry is 20 bytes
        ide_size = 20

        new_import_section = GenericStruct()
        new_import_section.buf = "" # to avoid unicode string issues
        new_import_section.import_directory = []

        # we're going to append to the end of the image
        imagesize = self.Images[ self.imagename].Headers.OptionalHeader.WindowsSpecific.SizeOfImage

        # reserve space for all import directories
        for iat in self.IATs:
            offset = len( new_import_section.buf)
            new_import_section.buf += "\0" * ide_size
            ide = ImportDirectoryEntry( StringBackend( new_import_section) , offset) # FIXME is this correct?
            new_import_section.import_directory.append( ide)

        # All-Zero import directory entry
        new_import_section.buf += "\0" * ide_size


        for i in range( len( self.IATs)):
            iat = self.IATs[ i]
            ide = new_import_section.import_directory[ i]
            rva = len( new_import_section.buf) + imagesize

            ide.NameRVA = rva
            new_import_section.buf += iat.image_name + "\0"

            rva = len( new_import_section.buf) + imagesize
            ide.ImportAddressTableRVA = iat.offset

            for j in range(len( iat)):
                rva = len( new_import_section.buf) + imagesize
                if rva % 2:
                    new_import_section.buf += "\0"
                    rva += 1
                # add Hint/Name Table Entry
                new_import_section.buf += "\0\0" + str( iat.imports_info[ j][ "name"]) + "\0"
                iat[ j] = rva

            # zero-terminate the IAT
            new_import_section.buf += "\0\0\0\0"

               


        # FIXME create a new section instead of extending the last mess:
        # check if there's space after the section headers
        # aditionally check if that space is all-zero
        # increase the number of section in the COFF Header by one
        # create a new section header at that location
        # append the new import section to the PE buffer
        # put meaningful values into the new section header
        # fix the import data directory
        image = self.Images[ self.imagename]

        section_alignment = image.Headers.OptionalHeader.WindowsSpecific.SectionAlignment
        align = lambda x: x + (( x % section_alignment) and section_alignment - (x % section_alignment))

        # section-align the new import section
        length = len( new_import_section.buf)
        diff = align( length) - length
        new_import_section.buf += "\0" * diff

        offset = image.Headers.SectionHeaders[ -1].offset
        length = len( image.Headers.SectionHeaders[ -1])

        if align( offset) - offset >= length:
            # create a new section
            section = SectionHeader( image, offset + length)

            # section-align the image, if it is not aligned already
            length = len( image.backend)
            diff = align(length) - length
            image.backend.append( "\0" * diff)

            section.VirtualAddress = length + diff
            section.VirtualSize = len( new_import_section.buf)
            section.PointerToRawData = section.VirtualAddress
            section.SizeOfRawData = section.VirtualSize
            section.Name = ".pandora"
            section.Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE # from PE document for .idata section

            section.PointerToRelocations = 0
            section.PointerToLineNumbers = 0
            section.NumberOfRelocations = 0
            section.NumberOfLineNumbers = 0

            image.Headers.OptionalHeader.DataDirectories.ImportTable.VirtualAddress = section.VirtualAddress
            image.Headers.OptionalHeader.DataDirectories.ImportTable.Size = section.VirtualSize
            image.backend.append( new_import_section.buf)


            # add the new section to the section header list
            image.Headers.COFFFileHeader.NumberOfSections += 1
            image.Headers.OptionalHeader.WindowsSpecific.SizeOfImage += section.VirtualSize
            image.Headers.SectionHeaders.append( section)

            
        else:
            raise Exception( "No space for additional section header. Boohooo :( !")

        self.fix_sections() # to update SizeOfCode, SizeOfData, etc.
        
#        f = open( "iattest", "w")
#        f.write( self.Images[ self.imagename].buf)
#        f.close()
 


    def fix_sections( self):
        image = self.Images[ self.imagename]

        image.Headers.OptionalHeader.Standard.SizeOfCode = 0
        image.Headers.OptionalHeader.Standard.SizeOfInitializedData = 0
        image.Headers.OptionalHeader.Standard.SizeOfUninitializedData = 0
        image.Headers.OptionalHeader.Standard.BaseOfCode = len( image.backend) #FIXME len( image.backend) not pretty
        image.Headers.OptionalHeader.Standard.BaseOfData = len( image.backend) 
 
        section_alignment = image.Headers.OptionalHeader.WindowsSpecific.SectionAlignment
        image.Headers.OptionalHeader.WindowsSpecific.FileAlignment = section_alignment
        imagebase = image.Headers.OptionalHeader.WindowsSpecific.ImageBase
        align = lambda x: x + (( x % section_alignment) and section_alignment - (x % section_alignment))

        # sections should be in ascending order and adjacent, according to the specs
        # FIXME: research whether the windows PE loader really needs that
        for sct in image.Headers.SectionHeaders:
            sct.VirtualSize = align( sct.VirtualSize)
            sct.PointerToRawData = sct.VirtualAddress
            sct.SizeOfRawData = sct.VirtualSize
            sct.Characteristics &= ~IMAGE_SCN_CNT_UNINITIALIZED_DATA
            sct.Characteristics |= IMAGE_SCN_CNT_INITIALIZED_DATA
            # should this section be marked executable?
            query = "SELECT * FROM branches WHERE " \
                  + "(?1 <= target AND target < ?1 + ?2) OR " \
                  + "(?1 <= source AND source < ?1 + ?2) LIMIT 1"
#            print sct.VirtualAddress, ":",sct.VirtualSize
            sct.Characteristics |= IMAGE_SCN_MEM_READ
            sct.Characteristics |= IMAGE_SCN_MEM_WRITE # FIXME just do this for all sections, or be more picky?

            if None != self.dbcon.execute( query, (sct.VirtualAddress + imagebase, sct.VirtualSize + imagebase)).fetchone():
#                print "executable"
                sct.Characteristics |= IMAGE_SCN_MEM_EXECUTE
                sct.Characteristics |= IMAGE_SCN_CNT_CODE

            if sct.Characteristics & IMAGE_SCN_CNT_CODE and sct.VirtualAddress < image.Headers.OptionalHeader.Standard.BaseOfCode:
                image.Headers.OptionalHeader.Standard.BaseOfCode = sct.VirtualAddress
            elif sct.Characteristics & IMAGE_SCN_CNT_CODE:
                image.Headers.OptionalHeader.Standard.SizeOfCode += sct.VirtualSize
            elif sct.Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA and sct.VirtualAddress < image.Headers.OptionalHeader.Standard.BaseOfData:
                image.Headers.OptionalHeader.Standard.BaseOfData = sct.VirtualAddress
            elif sct.Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA:
                image.Headers.OptionalHeader.Standard.SizeOfInitializedData += sct.VirtualSize

    def getExportByAddress( self, address):
        for name in self.Images:
            try:
                result = self.Images[ name].Exports.by_va( address)
            except:
                result = None
            # first match wins
            if None != result:
                print "0x%08x %s" % (address, result)
                return result
        print "0x%08x %s" % (address, result)
        return None

    def getExportByName( self, export_name):
        for name in self.Images:
             image = self.Images[ name]
             export = image.Exports.by_name( export_name)
             # first match wins
             if export != None:
                 return export
        return None

    def getExportByForwarder( self, forwarder_name):
        for name in self.Images:
             image = self.Images[ name]
             export = image.Exports.by_forwarder( forwarder_name)
             # first match wins
             if export != None:
                 return export
        return None
 
    def getImageByAddress( self, address):
        for name in self.Images:
            image = self.Images[ name]
            imagebase = image.Headers.OptionalHeader.WindowsSpecific.ImageBase
            imagesize = image.Headers.OptionalHeader.WindowsSpecific.SizeOfImage
            # first match wins
            if imagebase <= address and address < imagebase + imagesize:
                return image

        return None


    def find_IATs( self):
        indirections = []

        # "find" the IATs from the import section
        iat_count = 0
        self.IATs = []

# FIXME see if we can do without the initial imports
# IDA Pro for instance doesn't like it when the IATs are spread across the whole file
        for ide in self.Images[ self.imagename].Imports.ImportDirectoryTable:
            self.IATs.append( IAT( self, self.Images[ self.imagename].rva2raw( ide.ImportAddressTableRVA)))
            iat_count += 1

        imagebase = self.Images[ self.imagename].Headers.OptionalHeader.WindowsSpecific.ImageBase
        imagesize = self.Images[ self.imagename].Headers.OptionalHeader.WindowsSpecific.SizeOfImage
        if imagesize != len(self.Images[ self.imagename].backend):
            raise Exception( "SizeOfImage %u != size of dump %u" % ( imagesize, len(self.Images[ self.imagename].backend)))



        # select all branches from inside the image to outside of the image
        # those are likely to be API calls
        query = "SELECT DISTINCT source,target FROM branches " + \
                "WHERE ?1 < source AND source < ?1 + ?2 AND NOT ( ?1 < target AND target < ?1 + ?2) " + \
                "ORDER BY source"

        for (source, target) in self.dbcon.execute( query, (imagebase, imagesize)):

            # fetch the instruction at the source of the call
            insn = pydasm.get_instruction( self.Images[ self.imagename].backend.read( source - imagebase, 50), pydasm.MODE_32) # FIXME use real x86 instruction length limit here

            # as the instruction is the source of a branch, it should be a branch instruction
            # and its single operand should be the branch target operand we need
            # in the case of heavily self-modifying code, it might be necessary to check
            # for legal branch instructions as well
            #
            # the only kind of branch that is useful to us as it is, is an indirect jump
            # referencing a memory address, as all we only have a memory dump, and no register
            # values available. again, heavily self-modifying code might fool this code
            #
            # So if the operand is a memory reference and not based on any register,
            # Append the displacement to the list of memory references used in branch targets
            if None != insn and pydasm.OPERAND_TYPE_MEMORY == insn.op1.type and pydasm.REGISTER_NOP == insn.op1.basereg:
                indirections.append( insn.op1.displacement)

                insn_formatted = pydasm.get_instruction_string( insn, pydasm.FORMAT_INTEL, 0)
                print "Source: 0x%08x, Target: 0x%08x, Jump instruction: %s, indirect operand is 0x%08x, value there is 0x%08x" \
                     % (source, target, insn_formatted, insn.op1.displacement, \
                     getUnsignedInt( self.Images[ self.imagename].backend, imagebase, insn.op1.displacement))

        # now sort the list of indirections so that it is easier to find the first such reference.
        indirections.sort()

        if len( indirections) == 0:
            print "Failed to find indirect calls, returning"
            return

        offset = indirections[0]

        # now move down in memory until we find the first memory reference that does not
        # point to within any DLL memory image and that is not NULL
        print "Going down the possible IAT: 0x%08x" % offset

        while offset - imagebase - 4 >= 0 and (0 == getUnsignedInt( self.Images[ self.imagename].backend, imagebase, offset) \
           or None != self.getExportByAddress( getUnsignedInt( self.Images[ self.imagename].backend, imagebase, offset))):
            print "0x%08x 0x%08x" % (offset-imagebase, imagesize)
            offset = offset - 4

        iat_start = offset + 4

        # Work our way upwards up to the first memory reference pointing to within a DLL
        print "Going up the possible IAT: 0x%08x" % offset
        while offset-imagebase + 4 < imagesize and (0 == getUnsignedInt( self.Images[ imagename].backend, imagebase, offset) \
           or None != self.getExportByAddress( getUnsignedInt( self.Images[ imagename].backend, imagebase, offset))):
            print "0x%08x 0x%08x" % (offset-imagebase, imagesize)
            offset += 4

        iat_end = offset - 4

        offset = iat_start

        while offset-imagebase + 4 < imagesize and (0 == getUnsignedInt( self.Images[ imagename].backend, imagebase, offset) \
           or None != self.getExportByAddress( getUnsignedInt( self.Images[ imagename].backend, imagebase, offset))):
            if 0 != getUnsignedInt( self.Images[ imagename].backend, imagebase, offset):
                self.IATs.append( IAT( self, self.Images[ self.imagename].rva2raw(offset-imagebase)))
                offset += len( self.IATs[ iat_count]) * 4
                iat_count += 1
            else:
                offset += 4


 
command = sys.argv[ 1]
dbfilename = sys.argv[ 2]
if len( sys.argv) > 3: # and command in ( 'ei', 'di', 'dc', "plot", 'graph'):
   imagename = sys.argv[ 3]
if len(sys.argv) > 4:
   dumptime = sys.argv[ 4]
   if len( sys.argv) > 5:
       skip = int( sys.argv[ 5])
   else:
       skip = 0
   if len( sys.argv) > 6:
       cutoff = int( sys.argv[ 6])
   else:
       cutoff = 0

dbcon = sqlite.connect( dbfilename)

if 'li' == command:
   for row in dbcon.execute( "SELECT time, pdb, base, length(dump), tag FROM image_dumps"):
      print "%u, 0x%08x, 0x%08x, %u, %s" % row
elif 'ei' == command:
   analysis = Analysis( dbfilename, imagename, dumptime)
   f = open( imagename+".extracted", "w")
   f.write( analysis.Images[ imagename].backend.raw()) # FIXME not really generic
   f.close()
elif 'di' == command:
    query = "SELECT dump FROM image_dumps " \
            "WHERE time = ?1 AND tag = ?2"

    dump = dbcon.execute( query, (dumptime, imagename)).fetchone()[ 0]
    file = open( imagename + ".dump", "w")
    file.write( dump)
    file.close()
elif 'epsig' == command:
    query = "SELECT dump,base FROM image_dumps " \
            "WHERE time = ?1 AND tag = ?2"

    dump, base = dbcon.execute( query, (dumptime, imagename)).fetchone()
    ep = dbcon.execute( 'SELECT target FROM branches WHERE time = ?1', (dumptime, )).fetchone()[ 0]
    print "entrypoint is 0x%08x" % ep
    for i in xrange( 10):
        for j in xrange( 32):
            print "%02x" % ord(dump[ep-base + i*32+j]),
        print

elif 'dc' == command:
   print
elif 'plot' == command:
    try:
        (imagebase, imagesize, pdb) = dbcon.execute( "SELECT base, LENGTH(dump), pdb FROM image_dumps WHERE tag LIKE ?1", (imagename,)).fetchone()
        image = dbcon.execute( "SELECT dump FROM image_dumps WHERE pdb = ?1 and time = ?2 and base = ?3)", (pdb, dumptime, imagebase)).fetchone()[0]
    except:
        (imagebase, imagesize, pdb) = dbcon.execute( "SELECT base, length(dump), pdb FROM image_dumps WHERE time = ?1", (dumptime,)).fetchone()
        image = dbcon.execute( "SELECT dump FROM image_dumps WHERE time = ?1 and base = ?2" , (dumptime, imagebase)).fetchone()[0]

    mintime = dbcon.execute( "SELECT min(time) FROM branches WHERE pdb = ?1", (pdb,)).fetchone()[0]
    mintime = min( mintime, dbcon.execute( "SELECT min(time) FROM writes WHERE pdb = ?1", (pdb,)).fetchone()[0])
    mintime += skip

    maxtime = dbcon.execute( "SELECT max(time) FROM branches WHERE pdb = ?1", (pdb,)).fetchone()[0]
    maxtime = max( maxtime, dbcon.execute( "SELECT max(time) FROM writes WHERE pdb = ?1", (pdb,)).fetchone()[0])
    if cutoff != 0:
        maxtime -= cutoff

    duration = maxtime - mintime

    print mintime, maxtime, duration



    vsize = 768
    vscale = imagesize / vsize

    hsize = 1024
    hscale = duration / hsize

    print vsize, vscale, hsize, hscale

    rows = {}

    im = Image.new( "RGB", (hsize, vsize), (255,255,255) )
    draw = ImageDraw.Draw( im)
    try:
        font = ImageFont.truetype( "/usr/share/fonts/truetype/ttf-bitstream-vera/VeraMono.ttf", 10)
    except IOError:
        font = ImageFont.truetype( "VeraMono.ttf", 10)

    print len( image)
    backend = CopyingStringBackend( image)
    colors = [( 128, 160, 192), ( 64, 96, 128)]
    draw.rectangle( [(0,0),(hsize-1,vsize-1)], fill = colors[ 0])
    try:
        pe = PE( backend, imagename, True)
        i = 0
        for sct_hdr in pe.Headers.SectionHeaders:
            color = colors [ i % 2]
            i += 1
            start = sct_hdr.VirtualAddress
            end = start + sct_hdr.VirtualSize
            start = start / vscale
            end = end / vscale
            draw.rectangle( [(0,start),(hsize-1,end)], fill = color)
            draw.line( [(0,start),(hsize-1,start)], fill =(0,0,0))
            draw.line( [(0,end),(hsize-1,end)], fill =(0,0,0))
            name = sct_hdr.Name.strip('\0')
            draw.text( (5, start + 5), name, font = font, fill = (0,0,0))
    except Exception, ex:
        print "Error when drawing sections:"
        print ex
        pass #FIXME make sure the above code works correctly
    print "."


    for write in dbcon.execute( "SELECT DISTINCT (time - ?1)/?5,(vaddress-?2)/?6 FROM writes WHERE ?2 <= vaddress AND vaddress < ?2 + ?3 AND pdb = ?4", ( mintime, imagebase, imagesize, pdb, hscale, vscale)):
        y = write[ 1]
        xstart = write[ 0]
        if y in rows:
            rows[ y] = min( rows[ y] * 1.05, 255)
        else:
            rows[ y] = 128
        try:
            im.putpixel( (xstart,y), (int(rows[y]),0,0))
        except:
            print (xstart, y, rows[y])
        draw.line( [ (xstart+1, y), (hsize-1,y)], fill = (int(rows[ y])/2, 0, 0))
#        for x in range( xstart, hsize):
#            im.putpixel( (x, y), (255, 0, 0, 255))


    rows = {}

    for write in dbcon.execute( "SELECT DISTINCT (time - ?1)/?5,(target-?2)/?6 FROM branches WHERE ?2 <= target AND target < ?2 + ?3 AND pdb = ?4", ( mintime, imagebase, imagesize, pdb, hscale, vscale)):
        try:
            im.putpixel( write, (0, 192, 0))
        except IndexError:
            print write

    rows = {}

    for write in dbcon.execute( "SELECT DISTINCT (time - ?1)/?5,(source-?2)/?6 FROM branches WHERE ?2 <= source AND source < ?2 + ?3 AND pdb = ?4", ( mintime, imagebase, imagesize, pdb, hscale, vscale)):
        try:
            im.putpixel( write, (0, 192, 0))
        except IndexError:
            print write


    for time in dbcon.execute( "SELECT DISTINCT (time - ?1)/?2 FROM image_dumps WHERE pdb = ?3 AND tag LIKE ?4", (mintime, hscale, pdb, '%'+imagename.strip('\\')+'%')):
        time = time[ 0]
        for i in range( 0, vsize):
            draw.line( [ (time, 0),(time,vsize-1)], fill = (0,0,0))


    im.save( imagename+ ".png")


elif 'hist' == command:
    pdb = sys.argv[ 3]
    if pdb.startswith( "0x"):
        pdb = int( pdb, 16)
    else:
        pdb = int( pdb)

    vsize = 2000
#    vscale = XXXX  / vsize # FIXME can only determine afterwards

    KERNEL_USER_SPLIT = 0x80000000
    hsize = 2000
    hscale = KERNEL_USER_SPLIT / hsize

    binwidth = 1

    bins = hsize / binwidth

    binsize = 4096
    
    (count,) = dbcon.execute( "SELECT COUNT() FROM writes WHERE pdb = ?1", (pdb,)).fetchone()
    print pdb, count

    (minbin,) = dbcon.execute( "SELECT MIN(vaddress) FROM writes WHERE pdb = ?1", (pdb,)).fetchone()
    minbin = minbin / binsize
    (maxbin,) = dbcon.execute( "SELECT MAX(vaddress) FROM writes WHERE pdb = ?1", (pdb,)).fetchone()
    maxbin = maxbin / binsize
    data = []
    print minbin, maxbin
    for i in range( minbin, maxbin+1):
        (count,) = dbcon.execute( "SELECT COUNT() FROM writes WHERE pdb = ?1 AND ?2 <= vaddress AND vaddress < ?3", (pdb, i* binsize,(i+1)*binsize)).fetchone()
        data.append( count)
        print count

    print vsize, vscale, hsize, hscale

    rows = {}

    im = Image.new( "RGB", (hsize, vsize), (255,255,255) )
    draw = ImageDraw.Draw( im)
elif 'graph' == command:
    # FIXME useless, without basic block semantics
    (imagebase, imagesize, pdb) = dbcon.execute( "SELECT base, size, pdb FROM images WHERE basedllname LIKE ?1", (imagename,)).fetchone()
    branches = [ branch for branch in dbcon.execute( "SELECT source, target FROM branches WHERE pdb = ?1 ORDER BY time ASC", (pdb, ))]
#    print len(branches)

    dllsplit = dbcon.execute( "SELECT base FROM images WHERE basedllname LIKE ?1 ORDER BY base ASC", ('%.dll',)).fetchone()[0]
#    print "0x%08x" % dllsplit
    basicblocks = {}

    def insertbb( basicblocks, start, end):
        if start in basicblocks:
            bbend = basicblocks[ start]
            if bbend != end:
                if bbend > end:
                    print "8",
                    basicblocks[ start] = end
                    insertbb( basicblocks, end + 1, bbend)
                elif bbend < end:
                    print "*",
                    insertbb( basicblocks, bbend + 1, end)
            else:
                pass # all is well
        else:
            basicblocks[ start] = end

    for i in xrange( len(branches) - 1):
        start = branches[ i][ 1]
        end = branches[ i + 1][ 0]
        diff = end - start
        if abs(diff) < 500 and start < dllsplit:
#            print "0x%08x ... 0x%08x - %10u" % (branches[ i][ 1], branches[ i + 1][0], diff)
            if start in basicblocks:
                insertbb( basicblocks, start, end)
            else:
                basicblocks[ start] = end
#    print len(basicblocks)

    bbs = [ bb for bb in basicblocks.items()]

    print "digraph \"branches\" {"
    for bb in bbs:
        print "# 0x%08x ... 0x%08x" % (bb[ 0], bb[ 1])
        for (source, target) in dbcon.execute( "SELECT DISTINCT source, target FROM branches WHERE pdb=?1 " \
                                             + "AND (source >= ?2 AND source <= ?3)", (pdb, bb[0], bb[1])):
            print "x%08x -> x%08x;" % (bb[0], target)
        for (source, target) in dbcon.execute( "SELECT DISTINCT source, target FROM branches WHERE pdb=?1 " \
                                             + "AND (target >= ?2 AND target <= ?3)", (pdb, bb[0], bb[1])):
            sources = [ b for b in bbs if b[ 0] <= source and source <= b[1] ]
            for b in sources:
                print "x%08x -> x%08x;" % (b[0], target)
    print "}"
    sys.exit( 0)
    print "digraph \"branches\" {"
    for (source, target) in dbcon.execute( "SELECT DISTINCT source, target FROM branches WHERE pdb=?1 " \
                                         + "AND ((source >= ?2 AND source < ?2 + ?3)" \
                                         + " AND (target >= ?2 AND target < ?2 + ?3)) ORDER BY TIME", (pdb, imagebase, imagesize)):

        print "x%08x -> x%08x;" % (source, target)
    print "}"
