Ursnif (aka Gozi/Gozi-ISFB) is one of the oldest banking malware
families still in active distribution. While the first major version
of Ursnif was identified in 2006, several subsequent versions have
been released in large part due source code leaks. FireEye reported on
a previously unidentified variant of the Ursnif malware family to our
threat
intelligence subscribers in September 2019 after identification
of a server that hosted a collection of tools, which included multiple
point-of-sale malware families. This malware self-identified as
"SaiGon version 3.50 rev 132," and our analysis suggests it
is likely based on the source code of the v3 (RM3) variant of Ursnif.
Notably, rather than being a full-fledged banking malware, SAIGON’s
capabilities suggest it is a more generic backdoor, perhaps tailored
for use in targeted cybercrime operations.
Technical Analysis
Behavior
SAIGON appears on an infected computer as a Base64-encoded shellcode
blob stored in a registry key, which is launched using PowerShell via
a scheduled task. As with other Ursnif variants, the main component of
the malware is a DLL file. This DLL has a single exported function,
DllRegisterServer, which is an unused empty function. All the
relevant functionality of the malware executes when the DLL is loaded
and initialized via its entry point.
Upon initial execution, the malware generates a machine ID using the
creation timestamp of either %SystemDrive%\pagefile.sys or
%SystemDrive%\hiberfil.sys (whichever is identified first).
Interestingly, the system drive is queried in a somewhat uncommon way,
directly from the KUSER_SHARED_DATA structure (via
SharedUserData→NtSystemRoot). KUSER_SHARED_DATA is a
structure located in a special part of kernel memory that is mapped
into the memory space of all user-mode processes (thus shared), and
always located at a fixed memory address (0x7ffe0000, pointed
to by the SharedUserData symbol).
The code then looks for the current shell process by using a call to
GetWindowThreadProcessId(GetShellWindow(), …). The code also
features a special check; if the checksum calculated from the name of
the shell’s parent process matches the checksum of explorer.exe
(0xc3c07cf0), it will attempt to inject into the parent process instead.
SAIGON then injects into this process using the classic
VirtualAllocEx / WriteProcessMemory / CreateRemoteThread
combination of functions. Once this process is injected, it loads two
embedded files from within its binary:
- A PUBLIC.KEY file, which is used to verify and decrypt
other embedded files and data coming from the malware’s command and
control (C2) server - A RUN.PS1 file, which is a
PowerShell loader script template that contains a
"@SOURCE@" placeholder within the script:
|
$hanksefksgu = |
The malware replaces the "@SOURCE@" placeholder
from this PowerShell script template with a Base64-encoded version of
itself, and writes the PowerShell script to a registry value named
"PsRun" under the
"HKEY_CURRENT_USER\Identities\{<random_guid>}"
registry key (Figure 1).

Figure 1: PowerShell script written to PsRun
The instance of SAIGON then creates a new scheduled task (Figure 2)
with the name "Power<random_word>" (e.g.
PowerSgs). If this is unsuccessful for any reason, it falls
back to using the
"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run"
registry key to enable itself to maintain persistence through system reboot.

Figure 2: Scheduled task
Regardless of the persistence mechanism used, the command that
executes the binary from the registry is similar to the following:
|
PowerShell.exe -windowstyle hidden |
After removing the Base64 encoding from this command, it looks
something like "iex (gp
‘HKCU:\\Identities\\{43B95E5B-D218-0AB8-5D7F-2C789C59B1DF}’).PsRun."
When executed, this command retrieves the contents of the
previous registry value using Get-ItemProperty (gp) and
executes it using Invoke-Expression (iex).
Finally, the PowerShell code in the registry allocates a block of
memory, copies the Base64-decoded shellcode blob into it, launches a
new thread pointing to the area using CreateRemoteThread, and
waits for the thread to complete. The following script is a
deobfuscated and beautified version of the PowerShell.
|
$hanksefksgu = $tskvo = [DllImport("user32")] [DllImport("kernel32")] [DllImport("kernel32")] [DllImport("kernel32")] $tskaaxotxe = |
Once it has established a foothold on the machine, SAIGON loads and
parses its embedded LOADER.INI configuration (see the
Configuration section for details) and starts its main worker thread,
which continuously polls the C2 server for commands.
Configuration
The Ursnif source code incorporated a concept referred to as
"joined data," which is a set of compressed/encrypted files
bundled with the executable file. Early variants relied on a special
structure after the PE header and marked with specific magic bytes
("JF," "FJ," "J1,"
"JJ," depending on the Ursnif version). In Ursnif v3
(Figure 3), this data is no longer simply after the PE header but
pointed to by the Security Directory in the PE header, and the magic
bytes have also been changed to "WD" (0x4457).

Figure 3: Ursnif v3 joined data
This structure defines the various properties (offset, size, and
type) of the bundled files. This is the same exact method used by
SAIGON for storing its three embedded files:
- PUBLIC.KEY – RSA public key
- RUN.PS1 – PowerShell script template
- LOADER.INI – Malware configuration
The following is a list of configuration options observed:
|
Name Checksum |
Name |
Description |
|
0x97ccd204 |
HostsList |
List of C2 URLs |
|
0xd82bcb60 |
ServerKey |
Serpent key used |
|
0x23a02904 |
Group |
Botnet ID |
|
0x776c71c0 |
IdlePeriod |
Number of |
|
0x22aa2818 |
MinimumUptime |
Waits until |
|
0x5beb543e |
LoadPeriod |
Number of |
|
0x84485ef2 |
HostKeepTime |
The number of |
Table 1: Configuration options
Communication
While the network communication structure of SAIGON is very similar
to Ursnif v3, there are some subtle differences. SAIGON beacons are
sent to the C2 servers as multipart/form-data encoded requests via
HTTP POST to the "/index.html" URL path. The payload
to be sent is first encrypted using Serpent encryption (in ECB mode vs
CBC mode), then Base64-encoded. Responses from the server are
encrypted with the same Serpent key and signed with the server’s RSA
private key.
SAIGON uses the following User-Agent header in its HTTP requests:
"Mozilla/5.0 (Windows NT <os_version>; rv:58.0)
Gecko/20100101 Firefox/58.0," where
<os_version> consists of the operating system’s major and
minor version number (e.g. 10.0 on Windows 10, and 6.1 on Windows 7)
and the string "; Win64; x64" is appended when the
operating system is 64-bit. This yields the following example User
Agent strings:
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0)
Gecko/20100101 Firefox/58.0" on Windows 10 64-bit - "Mozilla/5.0 (Windows NT 6.1; rv:58.0) Gecko/20100101
Firefox/58.0" on Windows 7 32-bit
The request format is also somewhat similar to the one used by other
Ursnif variants described in Table 2:
|
ver=%u&group=%u&id=%08x%08x%08x%08x&type=%u&uptime=%u&knock=%u |
|
Name |
Description |
|
ver |
Bot version (unlike |
|
group |
Botnet ID |
|
id |
Client ID |
|
type |
Request type (0 – |
|
uptime |
Machine uptime in |
|
knock |
The bot |
Table 2: Request format components
Capabilities
SAIGON implements the bot commands described in Table 3.
|
Name Checksum |
Name |
Description |
|
0x45d4bf54 |
SELF_DELETE |
Uninstalls |
|
0xd86c3bdc |
LOAD_UPDATE |
Download data |
|
0xeac44e42 |
GET_SYSINFO |
Collects and
|
|
0x83bf8ea0 |
LOAD_DLL |
Download data |
|
0xa8e78c43 |
LOAD_EXE |
Download data |
Table 3: SAIGON bot commands
Comparison to Ursnif v3
Table 4 shows the similarities between Ursnif v3 and the analyzed
SAIGON samples (differences are highlighted in bold):
|
|
Ursnif v3 (RM3) |
Saigon (Ursnif v3.5?) |
|
Persistence method |
Scheduled task that executes code stored in a |
Scheduled task that executes code stored in a |
|
Configuration storage |
Security PE directory points to embedded binary |
Security PE directory points to embedded binary |
|
PRNG algorithm |
xorshift64* |
xorshift64* |
|
Checksum algorithm |
JAMCRC (aka. CRC32 with all the bits flipped) |
CRC32, with the result rotated to the right by 1 |
|
Data compression |
aPLib |
aPLib |
|
Encryption/Decryption |
Serpent CBC |
Serpent ECB |
|
Data integrity verification |
RSA signature |
RSA |
|
Communication method |
HTTP |
HTTP POST |
|
Payload encoding |
Unpadded |
Unpadded Base64 (‘+’ and |
|
Uses URL path mimicking? |
Yes |
No |
|
Uses PX file format? |
Yes |
No |
Table 4: Similarities and differences between
Ursnif v3 and SAIGON samples
Figure 4 shows Ursnif v3’s use of URL path mimicking. This tactic
has not been seen in other Ursnif variants, including SAIGON.

Figure 4: Ursnif v3 mimicking (red)
previously seen benign browser traffic (green) not seen in SAIGON samples
Implications
It is currently unclear whether SAIGON is representative of a
broader evolution in the Ursnif malware ecosystem. The low number of
SAIGON samples identified thus far—all of which have compilations
timestamps in 2018—may suggest that SAIGON was a temporary branch of
Ursnif v3 adapted for use in a small number of operations. Notably,
SAIGON’s capabilities also distinguish it from typical banking malware
and may be more suited toward supporting targeted intrusion
operations. This is further supported via our prior identification of
SAIGON on a server that hosted tools used in point-of-sale intrusion
operations as well as VISA’s
recent notification of the malware appearing on a compromised
hospitality organization’s network along with tools previously used by FIN8.
Acknowledgements
The authors would like to thank Kimberly Goody, Jeremy Kennelly and
James Wyke for their support on this blog post.
Appendix A: Samples
The following is a list of samples including their embedded configuration:
Sample SHA256:
8ded07a67e779b3d67f362a9591cce225a7198d2b86ec28bbc3e4ee9249da8a5
Sample Version: 3.50.132
PE Timestamp: 2018-07-07T14:51:30
XOR Cookie: 0x40d822d9
C2 URLs:
- https://google-download[.]com
- https://cdn-google-eu[.]com
- https://cdn-gmail-us[.]com
Group / Botnet ID: 1001
Server Key: rvXxkdL5DqOzIRfh
Idle Period: 30
Load Period: 300
Host Keep Time:
1440
RSA Public Key:
(0xd2185e9f2a77f781526f99baf95dff7974e15feb4b7c7a025116dec10aec8b38c808f5f0bb21ae575672b1502ccb5c
021c565359255265e0ca015290112f3b6cb72c7863309480f749e38b7d955e410cb53fb3ecf7c403f593518a2cf4915
d0ff70c3a536de8dd5d39a633ffef644b0b4286ba12273d252bbac47e10a9d3d059, 0x10001)
Sample SHA256:
c6a27a07368abc2b56ea78863f77f996ef4104692d7e8f80c016a62195a02af6
Sample Version: 3.50.132
PE Timestamp: 2018-07-07T14:51:41
XOR Cookie: 0x40d822d9
C2 URLs:
- https://google-download[.]com
- https://cdn-google-eu[.]com
- https://cdn-gmail-us[.]com
Group / Botnet ID: 1001
Server Key: rvXxkdL5DqOzIRfh
Idle Period: 30
Load Period: 300
Host Keep Time:
1440
RSA Public Key:
(0xd2185e9f2a77f781526f99baf95dff7974e15feb4b7c7a025116dec10aec8b38c808f5f0bb21ae575672b1502ccb5c
021c565359255265e0ca015290112f3b6cb72c7863309480f749e38b7d955e410cb53fb3ecf7c403f593518a2cf4915
d0ff70c3a536de8dd5d39a633ffef644b0b4286ba12273d252bbac47e10a9d3d059, 0x10001)
Sample SHA256:
431f83b1af8ab7754615adaef11f1d10201edfef4fc525811c2fcda7605b5f2e
Sample Version: 3.50.199
PE Timestamp: 2018-11-15T11:17:09
XOR Cookie: 0x40d822d9
C2 URLs:
- https://mozilla-yahoo[.]com
- https://cdn-mozilla-sn45[.]com
- https://cdn-digicert-i31[.]com
Group / Botnet ID: 1000
Server Key: rvXxkdL5DqOzIRfh
Idle Period: 60
Load Period: 300
Host Keep Time:
1440
RSA Public Key:
(0xd2185e9f2a77f781526f99baf95dff7974e15feb4b7c7a025116dec10aec8b38c808f5f0bb21ae575672b15
02ccb5c021c565359255265e0ca015290112f3b6cb72c7863309480f749e38b7d955e410cb53fb3ecf7c403f5
93518a2cf4915d0ff70c3a536de8dd5d39a633ffef644b0b4286ba12273d252bbac47e10a9d3d059, 0x10001)
Sample SHA256:
628cad1433ba2573f5d9fdc6d6ac2c7bd49a8def34e077dbbbffe31fb6b81dc9
Sample Version: 3.50.209
PE Timestamp: 2018-12-04T10:47:56
XOR Cookie: 0x40d822d9
C2 URLs
- http://softcloudstore[.]com
- http://146.0.72.76
- http://setworldtime[.]com
- https://securecloudbase[.]com
Botnet ID: 1000
Server Key: 0123456789ABCDEF
Idle
Period: 20
Minimum Uptime: 300
Load Period: 1800
Host Keep Time: 360
RSA Public Key:
(0xdb7c3a9ea68fbaf5ba1aebc782be3a9e75b92e677a114b52840d2bbafa8ca49da40a64664d80cd62d9453
34f8457815dd6e75cffa5ee33ae486cb6ea1ddb88411d97d5937ba597e5c430a60eac882d8207618d14b660
70ee8137b4beb8ecf348ef247ddbd23f9b375bb64017a5607cb3849dc9b7a17d110ea613dc51e9d2aded, 0x10001)
Appendix B: IOCs
Sample hashes:
- 8ded07a67e779b3d67f362a9591cce225a7198d2b86ec28bbc3e4ee9249da8a5
- c6a27a07368abc2b56ea78863f77f996ef4104692d7e8f80c016a62195a02af6
- 431f83b1af8ab7754615adaef11f1d10201edfef4fc525811c2fcda7605b5f2e
[VT] - 628cad1433ba2573f5d9fdc6d6ac2c7bd49a8def34e077dbbbffe31fb6b81dc9
[VT]
C2 servers:
- https://google-download[.]com
- https://cdn-google-eu[.]com
- https://cdn-gmail-us[.]com
- https://mozilla-yahoo[.]com
- https://cdn-mozilla-sn45[.]com
- https://cdn-digicert-i31[.]com
- http://softcloudstore[.]com
- http://146.0.72.76
- http://setworldtime[.]com
- https://securecloudbase[.]com
User-Agent:
- "Mozilla/5.0 (Windows NT <os_version>;
rv:58.0) Gecko/20100101 Firefox/58.0"
Other host-based indicators:
- "Power<random_string>" scheduled
task - "PsRun" value under the
HKCU\Identities\{<random_guid>} registry key
Appendix C: Shellcode Converter Script
The following Python script is intended to ease analysis of this
malware. This script converts the SAIGON shellcode blob back into its
original DLL form by removing the PE loader and restoring its PE
header. These changes make the analysis of SAIGON shellcode blobs much
simpler (e.g. allow loading of the files in IDA), however, the created
DLLs will still crash when run in a debugger as the malware still
relies on its (now removed) PE loader during the process injection
stage of its execution. After this conversion process, the sample is
relatively easy to analyze due to its small size and because it is not obfuscated.
|
#!/usr/bin/env python3 MZ_HEADER = bytes.fromhex( def with open(args.sample, if data.startswith(b’MZ’): struct.pack_into(‘=I’, data, 0, 0x00004550) # file alignment base_of_code, base_of_data = if magic == print(‘Base of data[0x18 + optional_header_size : size_of_header = struct.unpack_from(‘=I’, data, data_size = 0x3000 section = 0 if magic lfanew =
if __name__ == "__main__": |