23 July 2019

ServHelper Vigenère encryption

MD5:  eb7cdf5a96ae5f5a596a6ed423f786a7
Post: https://twitter.com/VK_Intel/status/1152669759382654976
ServHelper: https://www.proofpoint.com/us/threat-insight/post/servhelper-and-flawedgrace-new-malware-introduced-ta505


Threat: ServHelper
Campaign ID: "ju11"
Key:  "exchanger"
Xor Key: "lol"
Vigenère Key: "XMGG"


C&Cs:
  • towerprod3[.]com/docs/saz.php
  • lotmoji[.]com/docs/saz.php
  • gdr55asf3145zadgsgsdw[.]icu/docs/saz.php

Previous ServHelper samples had all their strings unencrypted. 


1.Early version example


As for the communication with the C&C observed only later versions of tunnel variant to encrypt/decrypt the request/response data with a simple xor function.

2. Xor encryption of request parameters values


In latest version of ServHelper (Campaign ju11) most of the string are encrypted with Vigenère cipher (Keys observed: XMGG, XBJB, ...)

3. Encrypted strings



IDA PRO Decryption script.
# ServHelper vigenere decrypt
# @Tera0017
from idc import *
from idaapi import *


def prepare_string(s, alphabet):
    temp = s
    alphaList = list(alphabet)
    sList = list(temp)
    for index in range(len(sList)):
        for index2 in range(len(alphaList)):
            if temp[index] == alphaList[index2]:
                break
            if index2 + 1 == len(alphaList):
                sList.remove(temp[index])

    final = ''.join(sList)
    return final


def c2i(c, alphabet):
    return alphabet.index(c)


def i2c(i, alphabet):
    return alphabet[i]


def vigenere_decode(ciphertext, key, alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"):
    cipherList = list(ciphertext)
    keyList = list(key)
    alphaList = list(alphabet)
    decodeText = ""

    for index in range(len(ciphertext)):
        if ciphertext[index] not in alphaList:
            decodeText += ciphertext[index]
        else:
            cipherIndex = c2i(cipherList[index], alphabet)
            keyIndex = c2i((keyList[index % len(key)]), alphabet)
            decodeTextIndex = (cipherIndex - keyIndex) % 26
            decodeText += i2c(decodeTextIndex, alphabet)

    return decodeText


def get_string(addr):
    out = ""
    bytes2zero = 0
    while True:
        if Byte(addr) != 0:
            out += chr(Byte(addr))
            bytes2zero = 0
        else:
            bytes2zero += 1
            if bytes2zero == 2:
                break
        addr += 1
    return out


def decrypt_str(addr, KEY):
    print 'org:', '%08x  ' % addr,
    addr_c = addr
    for i in range(5):
        addr = idc.PrevHead(addr)
        mnem = GetMnem(addr)
        # X86 and X64 support
        if ((mnem == "mov" and GetOpnd(addr, 0) == "eax" and i == 0 and 'offset' in GetOpnd(addr, 1)) or (mnem == "lea" and GetOpnd(addr, 0) == "rdx" and i == 1)):
            off_addr = GetOperandValue(addr, 1)
            encrypted = get_string(off_addr)
            decrypted = vigenere_decode(encrypted, KEY)
            print decrypted
            MakeComm(addr_c, decrypted)
     MakeComm(off_addr, decrypted)
            break
        elif i >= 1:
            print 'Error finding encrypted'
            break

# TODO find vigenere function and add address + add the Key
decrstr_func = 0x50f4d8
refs = [addr.frm for addr in XrefsTo(decrstr_func, flags=0)]
for ref in refs:
    decrypt_str(ref, KEY='XMGG')
 




22 January 2019

GandCrab Version 5.1 "Anti-Disassembly"


MD5: 3f61255bbe14bc3ecae46c0c7fb7b6d0
POST: https://twitter.com/ValthekOn/status/1085896672897650688

Again new trick related to the anti-disassembly from the GandCrab Ransomware in version 5.1.


Below the "problem", which can be found in most of the functions.



IDA is not able to disassembly correctly the opcodes. Find below a manually "fixed" disassembly.

Part 1:


Explanation (Part 1):
      1. Stores to ecx 0x1C3D2
      2. Calls sub_40572E, and makes [esp] = 0x405723 (return pointer)
      3. Adds to the return ptr [esp] + 0x12 == 0x405735. So ones the return instruction is found won't return to 0x405723 but to 0x405735.
      4. Jumps to 0x405723
      5. Compares the previously stored ecx value to 0x1FEFF58.
      6. Will jump to 0x405719+2 as the previous comparison will set the sign flag.

Part 2:

  

Explanation (Part 2):
      7. As mentioned in the 6th step the process will jump to the address 0x405719+2, the opcode at this location is C3, which is translated 
         to the retn instruction. Now the program will return to the new return pointer which was edited in 3rd step, 0x405735.
       8. First instruction out of the anti-disassembly technique.


By replacing those hex values "B9D2C30100E80B00000081F958FFFE0178F079F8E983042412E2EFE8", with "90909090909090909090909090909090909090909090909090909090" we manage to fix the code to be viewable in IDA Graph view, able to decompile corectly and to be usable (without changing addresses).

IDA Graph View after applying the "fix" 

1. Change Example (OEP)

2. Change Example (Now possible to decompile the code correctly)


Code to fix this "problem".
1:  # GandCrab version 5.1 anti-dissasembly fix  
2:  import os  
3:  import sys  
4:    
5:  import os.path  
6:    
7:  def pre_checks():  
8:      # pre-checks & returns in & out file  
9:      if len(sys.argv) not in [2,3]:  
10:          error_log('Too few or too many arguments, --help')  
11:      in_file = sys.argv[1]  
12:      try:  
13:          out_file = sys.argv[2]  
14:      except IndexError:  
15:          out_file = get_output(in_file)  
16:      if not os.path.isfile(in_file):  
17:          error_log("Input file doesn't exist")  
18:          exit()  
19:      return in_file, out_file  
20:    
21:  def error_log(msg):  
22:      print msg  
23:      exit()  
24:    
25:  def get_output(fname):  
26:      if not os.path.isfile(fname):  
27:          error_log("Input file doesn't exist")  
28:      path = os.path.dirname(os.path.realpath(fname))  
29:      return path + "/output.bin"  
30:        
31:  def main(in_, out_):  
32:      bytes1 = "\xB9\xD2\xC3\x01\x00\xE8\x0B\x00\x00\x00\x81\xF9\x58\xFF\xFE\x01\x78\xF0\x79\xF8\xE9\x83\x04\x24\x12\xE2\xEF\xE8"  
33:      bytes2 = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"  
34:      fp1 = open(in_, 'rb')  
35:      fd1 = fp1.read()  
36:      fp1.close()  
37:      fp2 = open(out_, 'wb')  
38:      fd2 = fd1.replace(bytes1, bytes2)  
39:      fp2.write(fd2)  
40:      print out_, " successfully created!!"  
41:    
42:  if __name__ == "__main__":  
43:      in_file, out_file = pre_checks()  
44:      main(in_file, out_file)