FinSpy analysis – Round Two

Well, it’s been a long time coming but here’s round two of my analysis of the FinSpy sample discussed in my previous blog post. The sample’s hash is 2bbc8f46a6efc6c824e55dc3ec18e2cf4a6d594b3d4f6fa54b95a4521e0a503e and is an executable masquerading as an Adobe Flash Installer/Uninstaller.
Once again I started by dumping the PE info using pefile.

This file contains 15 PE resources, of which the following look interesting:

  • [0x2]_[0x1]_[0x409], [0x2]_[0x2]_[0x409]
  • [0x2]_[0x65]_[0x409], [0x2]_[0x66]_[0x409]

These resources make up about 627Kb (of a 715Kb file) and appear to be encrypted so once again it’s time open up IDA to take a closer look at the executable.
The entry point seems normal; it first calls a routine that sets up the stack canary then calls the CRT-main/WinMain routine. Tracing through the main routine we eventually end up at the executable’s real WinMain routine. Now things start to get interesting…
What looks to be a unique key or identifier (0xF6DB9A6A) is pushed on to the stack before calling a function I’ve named DecodeInstruction. There are many references to the DecodeInstruction routine, and each reference is usually close to the start of a function (just after the normal x86 preamble). The DecodeInstruction routine makes a backup of the CPU registers and flags before using the classic call-pop technique to determine the current address of EIP. The routine (and much of what follows) uses a semi-annoying trick of doing a near jump to EIP + 1. As this instruction is two bytes (EB FF), IDA doesn’t like it, gets confused and stops disassembling.

DecodeInstruction

The fix is easy. Simply undefine the near jump instruction and instruct IDA that EIP + 1 is where the code starts (that is, skip the EB byte and start disassembling at FF). As a result you end up with a jmp eax.

NewDecodeInstruction
So before jumping to the value in eax (404D72), this routine sets up 4049C6 as the return address.

This contiguous region of shellcode (from 0x004049B1 to 0x00404DFB) constantly uses this annoying trick. The shellcode uses position-independent code (PIC) to perform the following tasks:

  • Decrypt and run some shellcode to disable DEP.
  • Setup an array of ThreadPackerStruct’s (where the thread-id is used as an index into the array). Size is 0x10000.
  • Setup a ThreadPackerStruct for the current thread
  • Decrypt the APLib Depack shellcode.
  • Decompress (using APLib Depack) a table of virtual instructions (InstructionTable).
  • Lookup a virtual instruction (given a 32-bit key) and interpret the virtual instruction.

Most of these operations are achieved by using values stored in a packer configuration structure (PackerConfigStruct). The PackerConfigStruct can be found at address 0x404DFC and looks like:

PackerConfigStruct
Based on the shellcode, I’ve labelled the members of PackerConfigStruct with their purpose:

PackerConfigStruct_Correct
In the above steps, both DisableDepShellcode and APLibDepackShellcode are decrypted using a routine I’ve called DecryptedShellcode:

DecryptShellCode is invoked like so:

“DecryptShellCode( VirtualAllocd_Memory, PackerConfigStruct->DisableDepShellcodeSize, PackerConfigStruct->ShellCodeBufferKey )”

NOTE: The PackerConfigStruct->DisableDepShellcodeSize is 0xDC and PackerConfigStruct->ShellCodeBufferKey is 0x54AD934E.

DecryptShellCode
The DecryptShellCode routine looks quite familiar and is in fact one of the FinSpy signatures proposed by Citizen Lab. Interestingly enough, the 64-bit FinSpy executables use the exact same routine (bytes and all). Here’s a python script I hacked up that is able to decrypt a buffer.


# Copied from Didier Steven's site
def rol(byte, count):
    count = count % 8
    while count > 0:
        byte = (byte << 1 | byte >> 7) & 0xFF
        count -= 1
    return byte

def ror(byte, count):
    count = count % 8
    while count > 0:
        byte = (byte >> 1 | byte << 7) & 0xFF
        count -= 1
    return byte

class Usage(Exception):
    def __init__(self, msg):
        self.msg = msg

def main(argv=None):
    if argv is None:
        argv = sys.argv
    try:
        enc_file = open( argv[ 1 ], 'rb' )

        shellcode_data = enc_file.read()

        enc_file.close()

        dec_file = open( argv[ 1 ] + '.shellcode.decrypted', 'wb' )

        key = '\x4E\x93\xAD\x54'

        byte_counter = 0

        for i in range( len(shellcode_data) ):
            # First 4 bytes are not obfuscated
            byte = ord( shellcode_data[ i ] )

            if byte_counter >= 4:
                byte = byte ^ ord( key[ 0 ] )
                byte = rol( byte, byte_counter - 4 )
                byte = ( byte + 0x45 ) & 0xFF
                byte = ( byte - ord( key[ 1 ] ) ) & 0xFF

                byte = ror( byte, byte_counter - 4 )
                byte = ( byte - ( byte_counter - 4 ) ) & 0xFF
                byte = ( byte + ord( key[ 2 ] ) ) & 0xFF
                byte = byte ^ 0xF5
                byte = rol( byte, byte_counter - 4 )
                byte = ( byte + ord( key[ 3 ] ) ) & 0xFF

            byte_counter = byte_counter + 1

            if byte_counter >= 24:
                byte_counter = 0

            dec_file.write( chr( byte ) )

        dec_file.close()

    except Usage, err:
        print >>sys.stderr, err.msg
        print >>sys.stderr, "for help use --help"
        return 2

if __name__ == "__main__":
    sys.exit(main())

Thanks to Dider Stevens for his rol/ror python functions.

This FinSpy sample uses a virtualized packer to obfuscate its code. The virtualized packer works by interpreting “virtual” instructions that are stored in a table (InstructionTable). Each virtual instruction stored in the table is 0x18 bytes and has the following structure:

struct InstructionEntry
{
ULONG Key;
UCHAR JumpTableIndex;
UCHAR SizeOfInstructionData;
USHORT Padding;
UCHAR InstructionData[ 18 ];
}

Each virtual instruction can be referenced by a unique key. For instance, the key mentioned above (0xF6DB9A6A) corresponds to the following InstructionEntry:

{ 0xF6DB9A6A, 0x04, 0x03, 0x00, { 0x83, 0xec, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } }

Each virtual instruction is interpreted using one of the following eleven “virtual instruction interpreter” functions:

  1. .text:00404DA0 2B 18 40 00 dd offset loc_40182B
  2. .text:00404DA4 8D 14 40 00 dd offset loc_40148D
  3. .text:00404DA8 B0 18 40 00 dd offset loc_4018B0
  4. .text:00404DAC 46 19 40 00 dd offset loc_401946
  5. .text:00404DB0 DD 14 40 00 dd offset loc_4014DD
  6. .text:00404DB4 34 15 40 00 dd offset loc_401534
  7. .text:00404DB8 48 15 40 00 dd offset loc_401548
  8. .text:00404DBC 92 15 40 00 dd offset loc_401592
  9. .text:00404DC0 E2 15 40 00 dd offset loc_4015E2
  10. .text:00404DC4 F6 15 40 00 dd offset loc_4015F6
  11. .text:00404DC8 39 16 40 00 dd offset loc_401639

The JumpTableIndex is used as an index into the above function table. Continuing with the 0xF6DB9A6A virtual instruction, it has a JumpTableIndex of 0x03 so the virtual instruction interpreter at loc_401946 is responsible for translating the virtual instruction into x86 instructions. This interpreter simply copies SizeOfInstructionData bytes from InstructionData to a buffer and executes the bytes. As such, the 0xF6DB9A6A virtual instruction translates into:

( 83 EC 04 ) sub esp, 4

There are eleven virtual instruction interpreters, each of which has a different purpose:

  1. loc_40182B – Short jump, with InstructionData[0] as the jump type (eg 0x75 == jnz). If jump condition is true, jump to the virtual instruction with key InstructionData[1-4]. If not true, advance to the next virtual instruction in the table.
  2. loc_40148D – Execute InstructionData and advance to the next InstructionEntry.
  3. loc_4018B0 – Call (executable base address + InstructionData[4-7]). After call, return to the virtual instruction with key InstructionData[0-3].
  4. loc_401946 – Execute InstructionData[3:] and advance to the virtual instruction with key InstructionData[0-3].
  5. loc_4014DD – Execute InstructionData and advance to the next InstructionEntry.
  6. loc_401534 – Saves the value at InstructionData[0-3] into ThreadPackerStruct+8.
  7. loc_401548 – Saves the value of a register (stored on the stack via pusha) into ThreadPackerStruct+8.
  8. loc_401592 – Shifts the stack down by one dword, then puts the value stored in ThreadPackerStruct+8 on the stack (above all the saved registers). Essentially, it’s pushing the value stored at ThreadPackerStruct+8 on to the stack.
  9. loc_4015E2 – Dereferences the address at ThreadPackerStruct+8, and stores the result in ThreadPackerStruct+8.
  10. loc_4015F6 – Saves the value at ThreadPackerStruct+8 into a stored register (stored on the stack via pusha).
  11. loc_401639 – Updates a virtual address (global variable) with the value stored at ThreadPackerStruct+8.

The ThreadPackerStruct referred to above is allocated for each FinSpy thread and is used to store the state of various virtualized packer fields. The ThreadPackerStruct is of the following form:

ThreadPackerStruct+0 = The address of the thread’s current InstructionEntry
ThreadPackerStruct+4 = ThreadPackerStruct + 0xFFFC (used as the thread’s stack pointer)
ThreadPackerStruct+8 = Register Value or Virtual Address.
ThreadPackerStruct+0xC = 0x404BA1 (Prologue – epilogue jumps to here when done)
ThreadPackerStruct+0x10 = 0x404DCC (Epilogue – called after most jump table functions)
ThreadPackerStruct+14 = Pointer to the InstructionTable
ThreadPackerStruct+0x18 = 0x404BA8
ThreadPackerStruct+1C = Base Address of the executable
ThreadPackerStruct+0x20 = original esp
ThreadPackerStruct+0x24 = original esp
ThreadPackerStruct+0x28 = Routine that is used to find an InstructionEntry given a unique id.
ThreadPackerStruct+0x2C = Decrypted InstructionEntry (InstructionEntry.Key)
ThreadPackerStruct+0x30 = InstructionEntry.JumpTableIndex
ThreadPackerStruct+0x31 = InstructionEntry.SizeOfInstructionData
ThreadPackerStruct+0x34 = InstructionEntry.InstructionData
ThreadPackerStruct+44 = ThreadPackerStruct+48

Given the information described above, deobfuscating a FinSpy sample is quite straight-forward. Simply locate the PackerConfigStruct, depack the InstructionTable, and translate each InstructionEntry into equivalent x86 instructions. I’ve hacked together a python script that attempts to recreate the original (unpacked) FinSpy executable (thanks to n0p for his SectionDoubleP code).

import pefile
import sys
import struct

class SectionDoublePError(Exception):
    pass

class SectionDoubleP:
    def __init__(self, pe):
        self.pe = pe

    def __adjust_optional_header(self):
        """ Recalculates the SizeOfImage, SizeOfCode, SizeOfInitializedData and
            SizeOfUninitializedData of the optional header.
        """

        # SizeOfImage = ((VirtualAddress + VirtualSize) of the new last section)
        self.pe.OPTIONAL_HEADER.SizeOfImage = (self.pe.sections[-1].VirtualAddress +
                                                self.pe.sections[-1].Misc_VirtualSize)

        self.pe.OPTIONAL_HEADER.SizeOfCode = 0
        self.pe.OPTIONAL_HEADER.SizeOfInitializedData = 0
        self.pe.OPTIONAL_HEADER.SizeOfUninitializedData = 0

        # Recalculating the sizes by iterating over every section and checking if
        # the appropriate characteristics are set.
        for section in self.pe.sections:
            if section.Characteristics & 0x00000020:
                # Section contains code.
                self.pe.OPTIONAL_HEADER.SizeOfCode += section.SizeOfRawData
            if section.Characteristics & 0x00000040:
                # Section contains initialized data.
                self.pe.OPTIONAL_HEADER.SizeOfInitializedData += section.SizeOfRawData
            if section.Characteristics & 0x00000080:
                # Section contains uninitialized data.
                self.pe.OPTIONAL_HEADER.SizeOfUninitializedData += section.SizeOfRawData

    def __add_header_space(self):
        """ To make space for a new section header a buffer filled with nulls is added at the
            end of the headers. The buffer has the size of one file alignment.
            The data between the last section header and the end of the headers is copied to
            the new space (everything moved by the size of one file alignment). If any data
            directory entry points to the moved data the pointer is adjusted.
        """

        FileAlignment = self.pe.OPTIONAL_HEADER.FileAlignment
        SizeOfHeaders = self.pe.OPTIONAL_HEADER.SizeOfHeaders

        data = '\x00' * FileAlignment

        # Adding the null buffer.
        self.pe.__data__ = (self.pe.__data__[:SizeOfHeaders] + data +
                            self.pe.__data__[SizeOfHeaders:])

        section_table_offset = (self.pe.DOS_HEADER.e_lfanew + 4 +
                        self.pe.FILE_HEADER.sizeof() + self.pe.FILE_HEADER.SizeOfOptionalHeader)

        # Copying the data between the last section header and SizeOfHeaders to the newly allocated
        # space.
        new_section_offset = section_table_offset + self.pe.FILE_HEADER.NumberOfSections*0x28
        size = SizeOfHeaders - new_section_offset
        data = self.pe.get_data(new_section_offset, size)
        self.pe.set_bytes_at_offset(new_section_offset + FileAlignment, data)

        # Filling the space, from which the data was copied from, with NULLs.
        self.pe.set_bytes_at_offset(new_section_offset, '\x00' * FileAlignment)

        data_directory_offset = section_table_offset - self.pe.OPTIONAL_HEADER.NumberOfRvaAndSizes * 0x8

        # Checking data directories if anything points to the space between the last section header
        # and the former SizeOfHeaders. If that's the case the pointer is increased by FileAlignment.
        for data_offset in xrange(data_directory_offset, section_table_offset, 0x8):
            data_rva = self.pe.get_dword_from_offset(data_offset)

            if new_section_offset <= data_rva and data_rva < SizeOfHeaders:
                self.pe.set_dword_at_offset(data_offset, data_rva + FileAlignment)

        SizeOfHeaders_offset = (self.pe.DOS_HEADER.e_lfanew + 4 +
                        self.pe.FILE_HEADER.sizeof() + 0x3C)

        # Adjusting the SizeOfHeaders value.
        self.pe.set_dword_at_offset(SizeOfHeaders_offset, SizeOfHeaders + FileAlignment)

        section_raw_address_offset = section_table_offset + 0x14

        # The raw addresses of the sections are adjusted.
        for section in self.pe.sections:
            if section.PointerToRawData != 0:
                self.pe.set_dword_at_offset(section_raw_address_offset, section.PointerToRawData+FileAlignment)

            section_raw_address_offset += 0x28

        # All changes in this method were made to the raw data (__data__). To make these changes
        # accessbile in self.pe __data__ has to be parsed again. Since a new pefile is parsed during
        # the init method, the easiest way is to replace self.pe with a new pefile based on __data__
        # of the old self.pe.
        self.pe = pefile.PE(data=self.pe.__data__)

    def __is_null_data(self, data):
        """ Checks if the given data contains just null bytes.
        """

        for char in data:
            if char != '\x00':
                return False
        return True

    def pop_back(self):
        """ Removes the last section of the section table.
            Deletes the section header in the section table, the data of the section in the file,
            pops the last section in the sections list of pefile and adjusts the sizes in the
            optional header.
        """

        # Checking if there are any sections to pop.
        if (    self.pe.FILE_HEADER.NumberOfSections > 0
            and self.pe.FILE_HEADER.NumberOfSections == len(self.pe.sections)):

            # Stripping the data of the section from the file.
            if self.pe.sections[-1].SizeOfRawData != 0:
                self.pe.__data__ = (self.pe.__data__[:self.pe.sections[-1].PointerToRawData] + \
                                    self.pe.__data__[self.pe.sections[-1].PointerToRawData + \
                                                        self.pe.sections[-1].SizeOfRawData:])

            # Overwriting the section header in the binary with nulls.
            # Getting the address of the section table and manually overwriting
            # the header with nulls unfortunally didn't work out.
            self.pe.sections[-1].Name = '\x00'*8
            self.pe.sections[-1].Misc_VirtualSize = 0x00000000
            self.pe.sections[-1].VirtualAddress = 0x00000000
            self.pe.sections[-1].SizeOfRawData = 0x00000000
            self.pe.sections[-1].PointerToRawData = 0x00000000
            self.pe.sections[-1].PointerToRelocations = 0x00000000
            self.pe.sections[-1].PointerToLinenumbers = 0x00000000
            self.pe.sections[-1].NumberOfRelocations = 0x0000
            self.pe.sections[-1].NumberOfLinenumbers = 0x0000
            self.pe.sections[-1].Characteristics = 0x00000000

            self.pe.sections.pop()

            self.pe.FILE_HEADER.NumberOfSections -=1

            self.__adjust_optional_header()
        else:
            raise SectionDoublePError("There's no section to pop.")

    def push_back(self, Name=".NewSec", VirtualSize=0x00000000, VirtualAddress=0x00000000,
                RawSize=0x00000000, RawAddress=0x00000000, RelocAddress=0x00000000,
                Linenumbers=0x00000000, RelocationsNumber=0x0000, LinenumbersNumber=0x0000,
                Characteristics=0xE00000E0, Data=""):
        """ Adds the section, specified by the functions parameters, at the end of the section
            table.
            If the space to add an additional section header is insufficient, a buffer is inserted
            after SizeOfHeaders. Data between the last section header and the end of SizeOfHeaders
            is copied to +1 FileAlignment. Data directory entries pointing to this data are fixed.

            A call with no parameters creates the same section header as LordPE does. But for the
            binary to be executable without errors a VirtualSize > 0 has to be set.

            If a RawSize > 0 is set or Data is given the data gets aligned to the FileAlignment and
            is attached at the end of the file.
        """

        if self.pe.FILE_HEADER.NumberOfSections == len(self.pe.sections):

            FileAlignment = self.pe.OPTIONAL_HEADER.FileAlignment
            SectionAlignment = self.pe.OPTIONAL_HEADER.SectionAlignment

            if len(Name) > 8:
                raise SectionDoublePError("The name is too long for a section.")

            if (    VirtualAddress < (self.pe.sections[-1].Misc_VirtualSize +
                                        self.pe.sections[-1].VirtualAddress)
                or  VirtualAddress % SectionAlignment != 0):

                if (self.pe.sections[-1].Misc_VirtualSize % SectionAlignment) != 0:
                    VirtualAddress =    \
                        (self.pe.sections[-1].VirtualAddress + self.pe.sections[-1].Misc_VirtualSize -
                        (self.pe.sections[-1].Misc_VirtualSize % SectionAlignment) + SectionAlignment)
                else:
                    VirtualAddress =    \
                        (self.pe.sections[-1].VirtualAddress + self.pe.sections[-1].Misc_VirtualSize)

            if VirtualSize < len(Data):
                VirtualSize = len(Data)

            if (len(Data) % FileAlignment) != 0:
                # Padding the data of the section.
                Data += '\x00' * (FileAlignment - (len(Data) % FileAlignment))

            if RawSize != len(Data):
                if (    RawSize > len(Data)
                    and (RawSize % FileAlignment) == 0):
                    Data += '\x00' * (RawSize - (len(Data) % RawSize))
                else:
                    RawSize = len(Data)

            section_table_offset = (self.pe.DOS_HEADER.e_lfanew + 4 +
                self.pe.FILE_HEADER.sizeof() + self.pe.FILE_HEADER.SizeOfOptionalHeader)

            # If the new section header exceeds the SizeOfHeaders there won't be enough space
            # for an additional section header. Besides that it's checked if the 0x28 bytes
            # (size of one section header) after the last current section header are filled
            # with nulls/ are free to use.
            if (        self.pe.OPTIONAL_HEADER.SizeOfHeaders <
                        section_table_offset + (self.pe.FILE_HEADER.NumberOfSections+1)*0x28
                or not  self.__is_null_data(self.pe.get_data(section_table_offset +
                        (self.pe.FILE_HEADER.NumberOfSections)*0x28, 0x28))):

                # Checking if more space can be added.
                if self.pe.OPTIONAL_HEADER.SizeOfHeaders < self.pe.sections[0].VirtualAddress:

                    self.__add_header_space()
                    print "Additional space to add a new section header was allocated."
                else:
                    raise SectionDoublePError("No more space can be added for the section header.")

            # The validity check of RawAddress is done after space for a new section header may
            # have been added because if space had been added the PointerToRawData of the previous
            # section would have changed.
            if (RawAddress != (self.pe.sections[-1].PointerToRawData +
                                    self.pe.sections[-1].SizeOfRawData)):
                    RawAddress =     \
                        (self.pe.sections[-1].PointerToRawData + self.pe.sections[-1].SizeOfRawData)

            # Appending the data of the new section to the file.
            if len(Data) > 0:
                self.pe.__data__ = (self.pe.__data__[:RawAddress] + Data + \
                                    self.pe.__data__[RawAddress:])

            section_offset = section_table_offset + self.pe.FILE_HEADER.NumberOfSections*0x28

            # Manually writing the data of the section header to the file.
            self.pe.set_bytes_at_offset(section_offset, Name)
            self.pe.set_dword_at_offset(section_offset+0x08, VirtualSize)
            self.pe.set_dword_at_offset(section_offset+0x0C, VirtualAddress)
            self.pe.set_dword_at_offset(section_offset+0x10, RawSize)
            self.pe.set_dword_at_offset(section_offset+0x14, RawAddress)
            self.pe.set_dword_at_offset(section_offset+0x18, RelocAddress)
            self.pe.set_dword_at_offset(section_offset+0x1C, Linenumbers)
            self.pe.set_word_at_offset(section_offset+0x20, RelocationsNumber)
            self.pe.set_word_at_offset(section_offset+0x22, LinenumbersNumber)
            self.pe.set_dword_at_offset(section_offset+0x24, Characteristics)

            self.pe.FILE_HEADER.NumberOfSections +=1

            # Parsing the section table of the file again to add the new section to the sections
            # list of pefile.
            self.pe.parse_sections(section_table_offset)

            self.__adjust_optional_header()
        else:
            raise SectionDoublePError("The NumberOfSections specified in the file header and the " +
                "size of the sections list of pefile don't match.")

        return self.pe

def get_sdata_offset( sdata_buffer, key ):
    offset = 0
    counter = 0

    while counter < len( sdata_buffer ):
        if sdata_buffer[ counter : counter + 4 ] == key:
            return offset

        functionIndex = ord( sdata_buffer[ counter + 4 ] )

        if functionIndex == 0:
            offset += 6

            # Jump if below
            jumpLocation = struct.unpack("<L", sdata_buffer[ counter + 9 : counter + 13 ])[0]

            if jumpLocation == 0:
                # Absolute jump
                absoluteJump = struct.unpack("<L", sdata_buffer[ counter + 13 : counter + 17 ])[0]
                print 'Nowhere to jump...continue'
        elif functionIndex == 2:
            offset += 6

            newIndex = struct.unpack("<L", sdata_buffer[ counter + 8 : counter + 12 ])[0]
            nextIndex = struct.unpack("<L", sdata_buffer[ counter + 0x18 : counter + 0x1C ])[0]
            if nextIndex != newIndex:
                print 'Not cool 2 ', hex( newIndex )
        elif functionIndex == 3:
            numBytes = ord( sdata_buffer[ counter + 5 ] )

            if numBytes > 0:
                offset += numBytes
            else:
                print 'Weird...no bytes to copy'

            newIndex = struct.unpack("<L", sdata_buffer[ counter + 8 : counter + 12 ])[0]
            nextIndex = struct.unpack("<L", sdata_buffer[ counter + 0x18 : counter + 0x1C ])[0]
            if nextIndex != newIndex:
                print 'Not cool 3 ', hex( newIndex )
        elif functionIndex == 4:
            numBytes = ord( sdata_buffer[ counter + 5 ] )

            if numBytes > 0:
                offset += numBytes
            else:
                print 'Weird...no bytes to copy'
        elif functionIndex == 5:
            nextFunctionIndex = ord( sdata_buffer[ counter + 24 + 4 ] )

            if nextFunctionIndex == 7:
                offset += 5
                counter += 0x18
            elif nextFunctionIndex == 8:
                offset += 6
                counter += 0x18
            else:
                print 'Weird...no number 7 after a 5', counter
                break
        elif functionIndex == 6:
            nextFunctionIndex = ord( sdata_buffer[ counter + 24 + 4 ] )

            if nextFunctionIndex == 7:
                offset += 1
                counter += 0x18
            elif nextFunctionIndex == 10:
                offset += 6
                counter += 0x18
            else:
                print 'Weird...no number 7 after a 5', counter
                break

        elif functionIndex == 7:
            print 'Nothing to do for a 7'
        elif functionIndex == 8:
            print 'Nothing to do for a 8'
        elif functionIndex == 9:
            offset += 1
        else:
            print 'Failed to decode instruction %d' % ord( sdata_buffer[ counter + 4 ] )
            print 'Index is %d' % counter
            break

        counter += 0x18
    return 0

def table_to_instructions( table, base_address ):
    outFile = ''

    counter = 0
    offset = 0

    while counter < len( table ):
        functionIndex = ord( table[ counter + 4 ] )

        if functionIndex == 0:
            jump_instruction = ord( table[ counter + 8 ] )

            if jump_instruction == 0xeb:
                outFile += '\x90\xe9'
            elif jump_instruction >= 0x80:
                print "Fuck", jump_instruction
                break
            elif jump_instruction <= 0x6F:
                print "Fuck", jump_instruction
                break
            else:
                jump_instruction = jump_instruction + 0x10
                outFile += '\x0f'
                outFile += chr( jump_instruction )

            offset += 6

            # Jump if below
            jumpLocation = struct.unpack("<L", table[ counter + 9 : counter + 13 ])[0]

            if jumpLocation != 0:
                # Lookup entry in sdata
                # print hex( jumpLocation )
                jump_offset = get_sdata_offset( table, table[ counter + 9 : counter + 13 ] )

                if jump_offset >= offset:
                    jump_offset -= offset
                else:
                    jump_offset = offset - jump_offset
                    jump_offset = 0xFFFFFFFF - jump_offset
                    jump_offset += 1

                outFile += struct.pack("<L", jump_offset )
            else:
                print 'Nowhere to jump...continue'
        elif functionIndex == 2:
            jumpLocation = struct.unpack("<L", table[ counter + 12 : counter + 16 ])[0]

            jumpLocation = jumpLocation + base_address
            outFile += '\xFF\x15'
            outFile += struct.pack( "<L", jumpLocation )
            offset += 6

            newIndex = struct.unpack("<L", table[ counter + 8 : counter + 12 ])[0]
            nextIndex = struct.unpack("<L", table[ counter + 0x18 : counter + 0x1C ])[0]
            if nextIndex != newIndex:
                print 'Not cool 2 ', hex( newIndex )
        elif functionIndex == 3:
            numBytes = ord( table[ counter + 5 ] )

            if numBytes > 0:
                instruction = table[ counter + 12 : counter + 12 + numBytes ]

                # convert a jump into a call
                if( numBytes >= 2 and instruction[ 0 ] == '\xff' ):
                    instruction = instruction[ 0 ] + chr( ord( instruction[ 1 ] ) - 0x10 ) + instruction[ 2 : ]

                outFile += instruction
                offset += numBytes
            else:
                print 'Weird...no bytes to copy'

            newIndex = struct.unpack("<L", table[ counter + 8 : counter + 12 ])[0]
            nextIndex = struct.unpack("<L", table[ counter + 0x18 : counter + 0x1C ])[0]
            if nextIndex != newIndex:
                print 'Not cool 3 ', hex( newIndex )
        elif functionIndex == 4:
            numBytes = ord( table[ counter + 5 ] )

            if numBytes > 0:
                outFile += table[ counter + 8 : counter + 8 + numBytes ]
                offset += numBytes
            else:
                print 'Weird...no bytes to copy'
        elif functionIndex == 5:
            nextFunctionIndex = ord( table[ counter + 24 + 4 ] )

            if nextFunctionIndex == 7:
                outFile += '\x68'
                outFile += table[ counter + 8 : counter + 12 ]
                offset += 5
                counter += 0x18
            elif nextFunctionIndex == 8:
                # Will need to deref
                outFile += '\xFF\x35'
                outFile += table[ counter + 8 : counter + 12 ]
                offset += 6

                counter += 0x18
            else:
                print 'Weird...no number 7 after a 5', counter
                break
        elif functionIndex == 6:
            stackIndex = ord( table[ counter + 8 ] )

            if stackIndex > 7:
                print 'Terrible things happening in 6'
                break

            # PostAmble?
            # pop     edi
            # pop     esi
            # pop     ebp
            # pop     ebx
            # pop     ebx
            # pop     edx
            # pop     ecx
            # pop     eax
            nextFunctionIndex = ord( table[ counter + 24 + 4 ] )

            if nextFunctionIndex == 7:
                registerPushList = [ '\x50', '\x51', '\x52', '\x53', '\x53', '\x55', '\x56', '\x57' ]
                outFile += registerPushList[ stackIndex ]
                offset += 1
                counter += 0x18
            elif nextFunctionIndex == 10:
                # Will need to deref
                if stackIndex != 0:
                    outFile += '\x89'
                else:
                    outFile += '\x90'

                registerPushList = [ '\xa3', '\x0d', '\x15', '\x1d', '\x1d', '\x2d', '\x35', '\x3d' ]
                outFile += registerPushList[ stackIndex ]
                outFile += table[ counter + 24 + 8 : counter + 24 + 12 ]
                offset += 6
                counter += 0x18
            else:
                print 'Weird...no number 7 after a 5', counter
                break

        elif functionIndex == 7:
            print 'Nothing to do for a 7'
        elif functionIndex == 8:
            print 'Nothing to do for a 8'
        elif functionIndex == 9:
            stackIndex = ord( table[ counter + 8 ] )

            if stackIndex > 7:
                print 'Terrible things happening in 6'
                break

            registerPushList = [ '\x58', '\x59', '\x5a', '\x5b', '\x5b', '\x5d', '\x5e', '\x5f' ]
            outFile += registerPushList[ stackIndex ]
            offset += 1
        else:
            print 'Failed to decode instruction %d' % ord( table[ counter + 4 ] )
            print 'Index is %d' % counter
            break

        counter += 0x18

    return outFile

def main(argv=None):
    global byte_count
    if argv is None:
        argv = sys.argv

    pe = pefile.PE( argv[ 1 ] )
    table_file = open( argv[ 2 ], 'rb' )
    table = table_file.read()
    table_file.close()

    # first add the table as a section
    print( hex( pe.OPTIONAL_HEADER.ImageBase ) )
    instructions = table_to_instructions( table, pe.OPTIONAL_HEADER.ImageBase )

    try:
        sections = SectionDoubleP( pe )
        # Characteristics: Executable as code, Readable, Contains executable code
        pe = sections.push_back(Characteristics=0x60000020, Data=instructions)
    except SectionDoublePError as e:
        print e

    instructions_section_base = pe.sections[-1].VirtualAddress

    text_section = None
    for section in pe.sections:
        if( '.text' in section.Name ):
            text_section = section
            break

    if( text_section != None ):
        text_data = text_section.get_data()

        if( len( text_data ) > 10 ):
            for index in range( len( text_data ) - 10 ):
                # Look for:
                # push xxxx
                # call xxxx
                if( '\x68' == text_data[ index ] and '\xE9' == text_data[ index + 5 ] ):
                    key_offset = get_sdata_offset( table, text_data[ index + 1 : index + 5 ] )
                    key_offset += instructions_section_base
                    key_offset = key_offset - ( text_section.VirtualAddress + index + 5 )
                    key_offset = struct.pack("<L", key_offset )
                    pe.set_bytes_at_rva( text_section.VirtualAddress + index, '\xe9' + key_offset )

    pe.write( filename=argv[ 3 ])

if __name__ == "__main__":
    sys.exit(main())
Advertisements

FinSpy analysis – Round One

Last month a number of FinSpy samples were found and later analysed by CitizenLab (see https://citizenlab.org/2012/07/from-bahrain-with-love-finfishers-spy-kit-exposed/). The details provided in the CitizenLab post are quite high-level; the aim of this blog is to dig deep into FinSpy and provide detailed analysis.

The samples from the CitizenLab post can be easily found on the internet by Googling the hashes. I found the following files:

  • 2ec6814e4bad0cb03db6e241aabdc5e59661fb580bd870bdb50a39f1748b1d14
  • 39b325bd19e0fe6e3e0fca355c2afddfe19cdd14ebda7a5fc96491fc66e0faba
  • 49000fc53412bfda157417e2335410cf69ac26b66b0818a3be7eff589669d040
  • cc3b65a0f559fa5e6bf4e60eef3bffe8d568a93dbb850f78bdd3560f38218b5c
  • e48bfeab2aca1741e6da62f8b8fc9e39078db574881691a464effe797222e632

After searching VirusTotal for the hashes listed above, I was relieved to see that the AV industry appeared to be on top of this threat, with around 30 of the 40 or so AVs detecting the files as malicious.  Little did I know how poor an effort all but two AVs had actually done.

For my research I focused on the first file (2ec6814e4bad0cb03db6e241aabdc5e59661fb580bd870bdb50a39f1748b1d14), but from what I could tell all of the files listed above look similar (in terms of their disassembly) so the following details are most probably applicable to the other files.

The first step I took was to run Ero Carrera’s pefile script (see http://code.google.com/p/pefile/) against the sample. Nothing too interesting popped up except for the number of resources the file has. I went ahead and dumped the resources and after looking through each resource, I found the biggest one (resource [0x5] – [0x1] – [0x409]) didn’t appear to have any file format structure to it (eg jpg, ico) and looked to be obfuscated or encrypted. I couldn’t immediately see how to deobfuscate/decrypt the file, so I put it aside and cracked open the sample file in IDA.

Opening up  the sample  in IDA Pro didn’t show anything too interesting at first – mainly boring win32 GUI code. The WinMain routine isn’t very big at all and looks like it just tries to load a couple of icons/strings from the PE resources before starting a Windows message loop.

Close to the top of WinMain though, there’s a call to a routine that uses some manual PE traversing and VirtualProtect to update two of the imports (RegisterClassExW and CreateWindowExW) for the sample PE file.

So what do these routines do? Well, the first routine that gets called is NewRegisterClassExW, which just  copies the sample’s PE file into the temp directory when the routine is first run (I’ll talk about some funky stuff it does when it’s called again later).

Almost directly after the call to the NewRegisterClassExW routine, the NewCreateWindowExW routine gets called. This routine is responsible for launching the copy of the sample in the temp directory. Under normal circumstances, this would just require a quick call to CreateProcess, however the authors went for a more complicated and sneaky approach. Here are the steps they take:

  • Start the new process in a suspended state
  • Map in their own private copy of ntdll
  • Get the address of NtUnmapViewOfSection from their private ntdll (using their own implementation of GetProcAddress)
  • Make a copy of the memory of the executable from the new process
  • Use NtUnmapViewOfSection to unmap the executable from the new process
  • Rewrite the executable into the new process’ address space, but also update a global variable that stores a handle to the current (parent) process.
  • Resume the new process and terminate the current process.

I believe the point of this is to allow the new process to self-delete on cleanup, as the Windows loader will no longer have a handle open to the executable (it will be unmapped).

So now the new process starts up and performs the steps outlined above, except its control flow is altered when it enters the NewRegisterClassExW because the global variable that stores a process handle has been set. The sample now enters a huge routine I’ve called DecryptAndDropFiles.

This routine pulls out two of the PE resources from itself (using PE traversal rather than the Win32 API) and deobfuscates them using a simple rolling XOR algorithm. One of the resources (resource [0x5] – [0x2] – [0x409]) deobfuscates to a JPEG file that is then used as a replacement to the original sample file. The other resource (resource [0x5] – [0x1] – [0x409]) is a PE file that is later loaded into the current process’ address space using a custom PE loader. From now on I’ll refer to this resource as the payload. The routine launches the replaced sample file (which is now a JPEG), then it drops and executes a bat file to clean up after itself.

I’m definitely no python expert and know barely enough to get around with it, so please excuse the ugliness of the following script I used to deobfuscate the PE resources:

encrypted_file = open( 'encrypted', 'rb' )

encrypted_data = encrypted_file.read( )

encrypted_file.close()

decrypted_file = open( 'decrypted', 'wb+' )

key = '\x67\xCA\x1E\x5F'

for i in range( len(encrypted_data) / 4 ):
    decrypted_file.write( chr( ord( key[0] ) ^ ord( encrypted_data[i*4 + 0] ) ) )
    decrypted_file.write( chr( ord( key[1] ) ^ ord( encrypted_data[i*4 + 1] ) ) )
    decrypted_file.write( chr( ord( key[2] ) ^ ord( encrypted_data[i*4 + 2] ) ) )
    decrypted_file.write( chr( ord( key[3] ) ^ ord( encrypted_data[i*4 + 3] ) ) )
    key = encrypted_data[i * 4 + 0] + encrypted_data[i * 4 + 1] + encrypted_data[i * 4 + 2] + encrypted_data[i * 4 + 3]

decrypted_file.close()

As you can see, the deobfuscation is pretty simple:

  • Start with 0x5F1ECA67 and XOR that with the first 4 bytes.
  • XOR the next 4 bytes with the (obfuscated) previous 4 bytes.

It’s worth noting that the seed value (0x5F1ECA67) is used in all of the samples I listed earlier. As such, I was able to use this script to deobfuscate all of the payloads from each sample. The hashes for the payloads are as follows:

  • 2bbc8f46a6efc6c824e55dc3ec18e2cf4a6d594b3d4f6fa54b95a4521e0a503e (disguised as FlashUtil.exe, Adobe Flash Installer/Uninstaller)
  • a99fca440934ea43ec71cecb8f2aa1a60c0350eef939450c17eb94fecf8453ee (disguised as Opera.exe, Opera Internet Browser)
  • a9da850395755704d33ff8c4c5f469dfcbcec9f373a5cf5b0b3290dff2a5c43f (disguised as Opera.exe, Opera Internet Browser)
  • 9011cc655228333dd35b2e8fe079861325ef511a32e45819bcc7dff13f9d2440 (disguised as autoruns.exe, Autostart program viewer)
  • a436042896aa7af9a16af04a5e568db4b8c5ddf7ccb013af402ac9e4930da693 (disguised as Opera.exe, Opera Internet Browser)

To my utter amazement, these PE files are (at the time of this post) being detected by just 2 AVs of the 40 or so on VirusTotal. Congratulations go to ESET and AVG for taking the time to protect their users.

That’s it for the first round of analysis. Stay tuned for round two where I dig into the payloads to uncover more Gamma Group goodies!