Reverse Engineering: Malware Analysis of Ursnif
In this post, I will document my analysis of a malware sample from the Ursnif family. The analysis is only concerned with dissecting the DLL sample, and not the Delivery
, and Weaponization
procedures.
Ursnif employs quite a few tricks to make the analysis more challenging and hide itself from intruding eyes, such as:
- Dynamically importing functions from libraries
- Storing things encrypted
- Process Injection
- Corrupted PE headers
- Using .rdata to store encrypted payloads
- encrypted C2 communication
Let’s get started.
Static Analysis
Static analysis is the first step to get an idea of what we are up against. One of the first tools I use for this is PEStudio
.
There are a few things we can infer from this:
- It is a DLL file, and has no exported functions
- There are not many imported functions to reveal its functionality
- .rdata is bigger than .text section and it takes up almost half of the file’s total size.
Next, let’s check some of the strings to see if we can find anything useful:
The pdb string looks unique to be an identifier for our sample. I’ve included it at the bottom of the post for further identification/detection purposes.
First Execution
Ursnif unpacks itself and delivers its payload in multiple stages. So, it would be easier to let it run once, observe its behavior with Procmon
to get an overview of what it is doing, and try to follow the steps with the debugger next. The DLL can be executed with one of the following commands:
rundll32 ursnif.dll, DllRegisterServer
regsvr32.exe ursnif.dll
Now, let’s examine the output from procmon.
First, it seems like ursnif creates 2 threads. After those threads exit, a 3rd one is created. Within the 3rd thread, ursnif’s first stage payload is executed. A bit down below, this payload is analyzed in more detail.
One of the interesting things I noticed was that this payload is 296KB, whereas the DLL file itself (from which we started execution) is 790KB. This means that the first 2 threads do some sort of unpacking, and a 3rd thread is created with the unpacked executable.
1st Stage - Mailslots
Let’s start the analysis of the first 2 threads that we’ve seen on Procmon.
I will use x32dbg to analyze this sample. Make sure that you are breaking on Thread Entry
, which can be checked under your debugger’s preferences.
Below you can see the entry of the first thread. Ursnif first obtains the current process id, which on my system is 0xFFC
. It uses this PID to generate a name for the mailslot. On my environment, the mailslot’s name is: \\\\.\\mailslot\\slffc
If like me, your first question was “wtf is a mailslot?”, it is, quoting from Wikipedia:
a one-way interprocess communication mechanism, available on the Microsoft Windows operating system, that allows communication between processes both locally and over a network.
Ursnif utilizes mailslots to pass an executable between the two threads it creates. In the first thread, it calls CreateMailSlot
, and then ReadFile
with the handle of the previously created mailslot. If the call fails, it calls Sleep
and tries again later.
At the second thread, it calls CreateFile
with the handle of previously created mailslot. If CreateFile
fails, ursnif calls Sleep
and tries again in a while. The screenshot above is from the 2nd thread.
In the screenshot below, you can see a PE file with an erased header that ursnif writes. This will be inspected in detail at 2nd stage.
Ursnif then calls WriteFile
to copy the PE file in the memory to the mailslot. It writes 0x1000
bytes at a time. After the call, it sleeps for 64ms
and then the routine above is repeated, until all of the file is written to the mailslot.
The other thread calls ReadFile
to retrieve the PE file and places it in the heap
. After the routine finishes, the first 2 threads are killed and execution continues from the 3rd thread.
2nd Stage
After the first 2 threads are finished executing, the execution is handed over to the 3rd thread and we land on this piece of code:
002245E | FF7424 04 | push dword ptr ss:[esp+4] |
002245E | E8 F1F7FFFF | call 223DE2 |
002245F | B9 E4752200 | mov ecx,2275E4 |
002245F | 83CA FF | or edx,FFFFFFFF |
002245F | F0:0FC111 | lock xadd dword ptr ds:[ecx],edx |
002245F | C2 0400 | ret 4 |
The calls we see here are quite different than the ones we see at static analysis of 1st stage. The ones below are especially worth of note, as they are commonly used for Process Injection
. There are also some calls to file and registry read/write APIs.
<ntdll.ZwCreateSection>
<ntdll.ZwMapViewOfSection>
<ntdll.ZwUnmapViewOfSection>
<kernel32.VirtualProtect>
<kernel32.VirtualAlloc>
<kernel32.SuspendThread>
<advapi32.RegSetValueExA>
While analyzing ursnif, you will constantly run into calls related to heap
. I’ve seen some other posts detailing the way ursnif makes use of the heap, so I will skip that. Debuggers don’t do justice to keeping track of the heap, so this makes analysis a bit more challenging. The relevant calls can be seen in the list of referenced calls:
<kernel32.HeapCreate>
<kernel32.HeapFree>
<ntdll.RtlAllocateHeap>
At this point, I wanted to check where we are executing from, and whether we could dump the executed module. The dumped file is 296kb, whereas the original sample was 790kb. As can be seen below, ursnif corrupts the PE headers (the one that was written mentioned earlier), which makes it evade automated dumping.
Upon execution, one of the first things ursnif does is checking the file C_1252NLS
under System32
. The reason for this is stated in the leaked Gozi/ISFB
loader as:
Opening c_1252.nls file and getting it’s write time.
Thus we can determine a time when OS was installed.
Source: windows/gozi-isfb/install.c#L112
Although ursnif is based on the leaked source code, it is modified and constantly updated. However, this behavior seems to have been persisting, so it can be used as a unique identifier.
After this, within the same function call, Ursnif “generates module names”. By this I mean the name of the DLL file to be dropped, and the name of the autorun key to execute that DLL file. This routine as well is present in the leaked Gozi-ISFB source code. You can check out the relevant source code by the following links:
This is added to the list of unique identifiers as well.
At the end of this routine, on my system the generated names for the dll and folder are:
002241F | BF D0762200 | mov edi,2276D0 | 2276D0:&L"blb_OMEX";this is the key name for autorun
00222F1 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] | [ebp+8]:&"ddraWNet";folder name to be created under %appdata%\Microsoft\
After this, the following key is acquired. This is important, as ursnif drops some important keys under here.
00223BD | A3 DC762200 | mov dword ptr ds:[2276DC],eax | eax:"Software\\AppDataLow\\Software\\Microsoft\\FD56FF74-382C-3782-2A81-EC5BFE45E0BF"
And the name of the DLL file to be dropped under %appdata% is copied:
00223F1 | FF35 D47622 | push dword ptr ds:[2276D4] | 002276D4:&L"bcdpbres.dll"
To spare some details, I will accompany some of the important calls with procmon logs. That way we can just look at the end results and where the calls have originated from.
002C508 | FF15 34602C | call dword ptr ds:[<&RegOpenKeyExA>] |
Procmon Log: 5:39:47.2296762 AM rundll32.exe 2800 RegOpenKey HKU SUCCESS Desired Access: Maximum Allowed, Granted Access: All Access
002C50D | FF15 08602C | call dword ptr ds:[<&RegEnumKeyExA>] |
002C512 | E8 69D4FFFF | call 2C2599 |;we have to dive into one more call
After here, we dive into the call and follow the steps from there:
002C266 | 8B35 38602C | mov esi,dword ptr ds:[<&RegQueryValueExW |
002C267 | FFD6 | call esi |
Procmon Logs:
5:46:50.8931454 AM rundll32.exe 2800 RegQueryValue HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\AppData SUCCESS Type: REG_SZ, Length: 64, Data: C:\Users\IEUser\AppData\Roaming
5:59:07.4587868 AM rundll32.exe 2800 RegQueryValue HKU\S-1-5-21-3583694148-1414552638-2922671848-1002\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\AppData NAME NOT FOUND Length: 144
This is done to acquire the %appdata%
path for each user account to be infected. A call to PathCombineW
is made to append \Microsoft
to absolute path of appdata. The previously generated system specific directory (in my case ddraWNet
) is created at this location.
Here, ursnif checks whether it is being executed from the dropped location under appdata. If it does, it skips the rest of the function because this would mean the payload has already been executed once, and there is no need to drop itself again.
002C271 | FF35 08762C | push dword ptr ds:[2C7608] | 002C7608:&L"C:\\Users\\IEUser\\Desktop\\sample\\75a86f64c1ab7d8a56532b793a8547b7.dll"
002C271 | 57 | push edi | edi:L"C:\\Users\\IEUser\\AppData\\Roaming\\Microsoft\\DdraWNet\\bcdpbres.dll"
002C271 | FF15 6C602C | call dword ptr ds:[<&lstrcmpiW>] |
002C272 | 85C0 | test eax,eax | eax:L"C:\\Users\\IEUser\\AppData\\Roaming\\Microsoft\\DdraWNet\\bcdpbres.dll"
002C272 | 0F84 D80100 | je 2C2902 |
Since this is our first run, we don’t take the jump and continue to this:
002C486 | 8B35 34612C | mov esi,dword ptr ds:[<&WriteFile>] |
002C48A | FFD6 | call esi |
002C48B | FF15 2C612C | call dword ptr ds:[<&SetEndOfFile>] |
002C490 | FF15 38612C | call dword ptr ds:[<&FlushFileBuffers>] |
Now the sample has dropped itself on disk. The dropped one is the same as the one we started the execution from(790KB). Since DdraWNet\\bcdpbres.dll
will be added to startup under Run
key, and it is the main loader, this means in each startup, it will unpack itself, inject itself to another process, and continue execution from there.
002C27B | FF75 1C | push dword ptr ss:[ebp+1C] | [ebp+1C]:L"rundll32 \"C:\\Users\\IEUser\\AppData\\Roaming\\Microsoft\\DdraWNet\\bcdpbres.dll\",DllRegisterServer"
002C27C | FF15 BC602C | call dword ptr ds:[<&lstrcpy>] |
002C27D | 50 | push eax | eax:"Software\\AppDataLow\\Software\\Microsoft\\FD56FF74-382C-3782-2A81-EC5BFE45E0BF"
002C27D | FF15 20602C | call dword ptr ds:[<&RegCreateKeyA>] |
002C280 | FF15 1C602C | call dword ptr ds:[<&RegQueryValueExA>] |
6:52:26.0547460 AM rundll32.exe 2800 RegSetValue HKCU\Software\AppDataLow\Software\Microsoft\FD56FF74-382C-3782-2A81-EC5BFE45E0BF\Client32 SUCCESS Type: REG_BINARY, Length: 204,288, Data: 96 F8 2A 18 2F F1 C5 2F C9 E9 60 47 5E E2 FC 5E
Here ursnif sets a few keys under AppDataLow for persistency.
After this, every string that was stored in the heap is released. These include the names of the registry keys, appdata, dropped DLL’s name etc. Like mentioned before, ursnif tries to retrieve things dynamically as much as possible. At the end of the routine, it releases the heap memory. This leaves us with a very narrow window to peek into the strings.
After it copies itself under %appdata%
and drops the keys at appdatalow
key, ursnif attempts to delete
itself. If it fails, it calls MoveFileExW
with the argument 0x4
, which according to Microsoft’s documentation corresponds to MOVEFILE_DELAY_UNTIL_REBOOT
. Quoting from the documentation:
The system does not move the file until the operating system is restarted. The system moves the file immediately after AUTOCHK is executed, but before creating any paging files.
Since this is the file we are currently executing from, DeleteFileW
will fail and it will do this operation after the reboot.
Process Injection
Ursnif then calls a function to perform process injection.
Inside this function, a call is made to the RtlGetNativeSystemInformation
API. Among other things, this is used to retrieve a list of running processes on the system.
esi
holds the address to the struct to be filled, which can be seen in the dump. Inside 381A3D
, a call is made to 002C346A
, which contains the process injection routine. This function takes the struct shown above as an argument.
The above checks one of the struct members got from the undocumented call and if it is 0, it jumps to the end of the routine. ursnif uses this struct to retrieve a list of the running processes. It then enumerates these processes one by one, calculates the checksum
of each process’ name, and compares them against the precalculated ones:
002C34B | 3D 121725AC | cmp eax,AC251712 |
002C34B | 74 47 | je 2C3504 |
002C34B | 3D 8A8EFE58 | cmp eax,58FE8E8A |
002C34C | 74 40 | je 2C3504 |
002C34C | 3D 65496DA7 | cmp eax,A76D4965 |
002C34C | 74 39 | je 2C3504 |
002C34C | 3D 12D72C96 | cmp eax,962CD712 |
002C34D | 74 32 | je 2C3504 |
002C34D | 3D F5BBE0A6 | cmp eax,A6E0BBF5 |;explorer.exe
002C34D | 75 36 | jne 2C350F |
After looping through the process list, ursnif finally reaches explorer.exe
, whose value corresponds to A6E0BBF5
. I did not confirm this, but based on what I’ve seen on other reports for this family, one of the other values might be for svchost.exe
.
The jump takes us to another function call, which lets ursnif inject itself into explorer.exe
. I was curious where the other jumps above would lead us to, and one of them seems to create a process. My guess is that if it does not find a suitable process to be injected, it creates one for itself.
Continuing, ursnif calls OpenProcess
to acquire a handle to explorer.exe
. Then it makes a call to GetProcAddress
with the handle of explorer.exe
to get the address of ntdll.RtlExitUserThread
.
Next, it calls GetProcAddress
again. This time to get the address of CreateRemoteThread
. And then, it calls CreateRemoteThread
, with the LPTHREAD_START_ROUTINE lpStartAddress
of ntdll.RtlExitUserThread
. So, the injected thread will start executing from RtlExitUserThread
. The thread is also created in suspended
state.
After the thread is created, ursnif calls ZwReadVirtualMemory
to read the first 4 bytes from the created thread’s EIP
. These 4 bytes are saved in a buffer. Ursnif then calls VirtualProtectEx
to make the page
from RtlExitUserThread
writable. It uses ZwWriteVirtualMemory
to write the bytes EB FE CC CC
to the buffer, and resumes the thread.
EB FE
is quite a famous piece of opcode. It is an infinite loop
, achieved by a relative short jump. Since short jumps takes 2 bytes, by jumping back 2 bytes, an infinite loop can be created. After the infinite loop, the next two bytes are CC CC
, which are used for software breakpoints (more specifically, debug exception handler
).
To sum up:
- Ursnif acquires a handle to
explorer.exe
- From
explorer.exe
, gets address ofntdll.RtlExitUserThread
- Creates a suspended thread with EP set to
ntdll.RtlExitUserThread
- Reads the first 4 bytes from the thread
- Replaces the first 4 bytes from EP with
EBFECCCC
, which creates an infinite loop
After seeing the disassembly for this function, I’ve checked some of the material published on this malware family. In the blog referred below, the behavior listed above is described in pseudo-code.
https://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/
/* source link above, copied from arielkoren.com
/* Obtain the process pid to inject to */
dwPID = GetInjectProcess()
hProcess = OpenProcess(dwPID, ...)
pAddress = GetProcAddress("ntdll.dll", "RtlExitUserThread")
/* Create remote thread in suspended mode */
hThread = CreateRemoteThread(hProcess, /* Remote process handle */
CREATE_SUSPENDED, /* creation flags */
pAddress, /* thread function address */
...)
/* Read remote procedure first four bytes */
dwBackupData = ZwReadVirtualMemory(hProcess, /* Remote process handle */
pAddress, /* Address to read */
4, /* number of bytes to read */
...)
/* Change address protection to `writable` */
VirtualProtectEx(hProcess, /* Remote process handle */
READ_WRITE_EXECUTE, /* New protection flags */
pAddress, /* Address to change protection on */
4, /* Size of address */
...)
ZwWriteVirtualMemory(hProcess, /* Remote process handle */
pAddress, /* Address to overwrite */
0xCCCCFEEB, /* Data to write */
4, /* Size of data */
...)
During my first analysis of ursnif, I noticed that a couple of seconds into the execution, CPU usage spiked to quite a high level, and the environment became almost unresponsive for a moment.
Looking at the screenshot below, I realized why. Since ursnif writes infinite loop at the entry point of RtlExitUserThread
, whenever a user thread exited during this stage, it started running the infinite loop. This caused several threads to be in an infinite loop, draining a lof of CPU time. You can see that at least 4 threads have their EIP pointed at the infinite loop. However, only one of the threads’ EP points to the infinite loop, and it is suspended.
After this, ursnif resumes the thread for a short period for initialization, and suspends again. It calls NtGetContextThread
, and uses the returned struct
to modify the entry point of the thread. During the following routine, it calls
ZwCreateSection
ZwMapViewOfSection
GetModuleHandleA
GetModuleFileNameW
And then it uses these to retrieve addresses of LdrLoadDll
, LdrGetProcedureAddress
, and ZwProtectVirtualMemory
. Ursnif then calls ZwAllocateVirtualMemory
to allocate memory in explorer.exe’s address space. In the screenshot below, the allocated address is 01FD0000
, and allocated space is 0x1000
bytes big.
Ursnif then copies the payload to explorer’s address space. In the screenshot below, the payload can be seen in dump:
The disassembly of the payload looks like this:
01A193E | 8B48 08 | mov ecx,dword ptr ds:[eax+8] |
01A193E | FF70 10 | push dword ptr ds:[eax+10] |
01A193E | FF30 | push dword ptr ds:[eax] |
01A193E | 51 | push ecx |
01A193E | C3 | ret |
Since there is a ret to push
, we will have to wait until the thread is executed to see where we will jump.
Ursnif then calls ZwUnmapViewofSection
on a region with the size of 0x68000
. The section contains a 416KB PE file with erased header.
Continuing, ursnif calls ZwWriteVirtualMemory
with the address of previously allocated space in explorer.exe, 01FD0000
, and the payload shown above.
Then ursnif calls ZwSetContextThread
, with the new EIP
written into the context. In the screenshot below, new EIP
can be seen as 01FD0218
. Now we can attach to explorer.exe, set our breakpoint there, and let explorer.exe continue its execution.
Ursnif calls ZwWriteVirtualMemory
again, this time to restore the previously overwritten 4 bytes; EB FE CC CC
. It then calls VirtualProtectEx
to restore the page
rights. After this, process injection routine is finished, and ursnif calls ResumeThread
.
explorer.exe
Immediately, our breakpoint at 01A193E
is triggered. The push ecx, ret
routine takes the execution to another memory region, with the size 0x68000
(416KB). This is the PE file with erased header, previously used with ZwUnmapViewOfSection
.
At this point, the analysis has become another side project that’s waiting to see the light of day again. So that’s it for this post. Who knows, maybe I can publish part 2 of this one day.
IOC
MD5: 75a86f64c1ab7d8a56532b793a8547b7
SHA256: 0f06bc2f24494d9206d8d985f0a2f13ec6975a225c0bdcbab6d7f74474ff70a6
File Access: c:\windows\system32\c_1252.nls
PDB String: c:\\path\\Know\\School\\Rest\\sing\\Pick\\officerun.pdb
File Write: %appdata%/Roaming/-RandomName-/-RandomName-.dll
Registry Write: HKCU\Software\Microsoft\Windows\CurrentVersion\Run
Registry Write: HKCU\Software\AppDataLow\Software\Microsoft\{GUID}
File Write: %temp%/[A-Fa-f0-9]{4}.bin
Network Data Format(Before Encryption): soft=%u&version=%u&user=%08x%08x%08x%08x&server=%u&id=%u&crc=%x
Network Data Format(After Encryption): images/[encryted_data]/.[extension]
References and Further Reading
https://forums.juniper.net/t5/Threat-Research/The-Gozi-Sleeper-Cell/ba-p/329691
https://www.vmray.com/cyber-security-blog/analyzing-ursnif-behavior-malware-sandbox/
https://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/
https://www.netskope.com/blog/ursnif-data-theft-malware-shared-on-microsoft-onedrive