LusyPOS and Tor Published: 2014-12-01 · Archived: 2026-04-02 11:01:40 UTC Introduction At our dayjobs, as reverse engineers at CBTS, Jeremy and I have been hunting new POS malware. A new sample appeared on Virustotal this week that had a very interesting name “lusypos.exe”. There have been very few references to this particular family and it appears to be fairly new. Google searching was able to give me the following information: The sample that I’ll be talking about in this post is bc7bf2584e3b039155265642268c94c7. At the time of this writing the malware is currently flagged on Virustotal by 7/54 engines. Interestingly, some of the signatures seem to be hitting on the copy of tor.exe that is in the bundle. Analysis This malware clocks in around 4.0 MB in size, so it’s not small. For comparison, getmypass POS malware was 17k in size. https://securitykitten.github.io/2014/12/01/lusypos-and-tor.html Page 1 of 9 The first thing of note when executing this in a sandbox is that this malware drops a copy of tor.exe, libcurl.dll, and zlib1.dll. It also copies itself to the %APPDATA% directory on the victim host. The following are the locations and MD5’s of the dropped files are below: The file mbambservice.exe is the copy of tor.exe d0f3b3aaa109a1ea8978c83d23055eb1 C:\Documents and Settings\\Application Data\VeriFone32\libcurl.dll 4407393c1542782bac2ba9d017f27dc9 C:\Documents and Settings\\Application Data\VeriFone32\mbambservice.exe bc7bf2584e3b039155265642268c94c7 C:\Documents and Settings\\Application Data\VeriFone32\verifone32.exe b8a9e91134e7c89440a0f95470d5e47b C:\Documents and Settings\\Application Data\VeriFone32\zlib1.dll The malware will also create the mutex “prowin32Mutex” and injects code into iexplore.exe. This was a strange mix of dexter-like behavior mixed with Chewbacca-like techniques. While running in a sandbox, the malware communicated out to 86.59.21.38 212.112.245.170 128.31.0.39 154.35.32.5 193.23.244.244 Now let’s get to the good stuff. Decoding The malware has an interesting method of decoding strings that are statically defined in the binary. https://securitykitten.github.io/2014/12/01/lusypos-and-tor.html Page 2 of 9 For the non-asm folks on here, the malware is using a lookup table with structures containing a one byte xor key, pointer to the string, and length of the string. It will perform an additional xor operation at the end. A decoder for this is written (in python below) #!/usr/bin/env python # ----------------------------------------------------------------------------- # # Author: Jeremy Humble - CBTS ACS # Description: POC LusyPOC String Extractor. Strings are stored in an array # of 8 byte structs with the following structure: {short xor_key, # short length, char* encoded_string} # ----------------------------------------------------------------------------- # import sys import struct import binascii import pefile import simplejson as json from pprint import pprint from optparse import OptionParser # Option Parsing usage = "lusypos_parser.py [-j] lusypos_sample1 [lusypos_sample2] ..." opt_parser = OptionParser(usage=usage) opt_parser.add_option("-j", "--json", action="store_true",dest="json_output", help="Output all information on each string in json format") opt_parser.add_option("-p", "--pretty", action="store_true",dest="pretty_json_output", https://securitykitten.github.io/2014/12/01/lusypos-and-tor.html Page 3 of 9 help="Output all information on each string in pretty json format") (options, args) = opt_parser.parse_args() if options.json_output and options.pretty_json_output: sys.stderr.write('Use either -j or -p, not both') exit() class LusyEncodedString: def __init__(self,raw_data,file_content,pe): self.xor_key = struct.unpack('H',raw_data[0:2])[0] self.length = struct.unpack('H',raw_data[2:4])[0] self.virtual_offset = struct.unpack('I', raw_data[4:8])[0] self.raw_offset = pe.get_offset_from_rva(self.virtual_offset - pe.OPTIONAL_HEADER.ImageBas self.encoded_str = file_content[self.raw_offset:self.raw_offset+self.length] self._decode() def _decode(self): self.decoded_str = "" for i in range(0,self.length): self.decoded_str += chr(ord(self.encoded_str[i]) ^ self.xor_key ^ i) def __str__(self): return str(self.to_dict()) def to_dict(self): d = {'xor key': hex(self.xor_key), 'length': self.length, 'raw offset': hex(self.raw_offse 'virtual offset': hex(self.virtual_offset), 'encoded string': self.encoded_str, 'deco return d # For now we'll assume the table is always at RVA 401000 (raw 0x400) as hardcoded in bc7bf2584e3b0 # With a little more refinement this could probably be found dynamically. AFAIK it's always locate # Until I see a sample that shows otherwise, there's no point in doing this def parse_table(content,pe,table_rva=0x1000): encoded_strings = [] raw_offset = pe.get_physical_by_rva(table_rva) i = 0 while True: raw_struct = content[raw_offset+i*8:raw_offset+i*8+8] # The last struct in the table is all null bytes. Stop parsing when we hit it if struct.unpack('