{
	"id": "5e62942a-4a0c-4cf0-b883-bd96e1d76d19",
	"created_at": "2026-04-06T00:17:48.831257Z",
	"updated_at": "2026-04-10T03:20:34.891765Z",
	"deleted_at": null,
	"sha1_hash": "53af2b777e020a5481ac4ee947bbcacb92da866f",
	"title": "BernhardPOS",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 214846,
	"plain_text": "BernhardPOS\r\nPublished: 2015-07-14 · Archived: 2026-04-05 13:34:53 UTC\r\nIntroduction\r\nYet another new credit card dumping utility has been discovered. BernhardPOS is named after (presumably) its\r\nauthor who left in the build path of C:\\bernhard\\Debug\\bernhard.pdb and also uses the name Bernhard in\r\ncreating the mutex OPSEC_BERNHARD . This utility does several interesting things to evade antivirus detection.\r\nWe’ll talk over some of them in depth. Details about the sample, including a hash are available at the end of this\r\nwriteup.\r\nAt the time of discovery it was scoring a low 3/56 detection on VirusTotal.\r\nDigging Deeper\r\nBy just looking at the strings, it’s not entirely obvious what the features of Bernhard are. Pasted below are all of\r\nthe strings.\r\nA 0x4d !This program cannot be run in DOS mode.$\r\nA 0xb0 Rich\r\nA 0x1c0 .textbss\r\nA 0x1e8 .text\r\nA 0x20f `.rdata\r\nA 0x237 @.data\r\nA 0x260 .idata\r\nA 0x287 @.reloc\r\nA 0x3480 OPSEC_BERNHARD\r\nA 0x3634 RSDS\r\nA 0x364c C:\\bernhard\\Debug\\bernhard.pdb\r\nA 0x3d66 Sleep\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 1 of 9\n\nA 0x3d6e ExitProcess\r\nA 0x3d7c CreateThread\r\nA 0x3d8c lstrlenA\r\nA 0x3d98 lstrcatA\r\nA 0x3da4 VirtualAlloc\r\nA 0x3db4 VirtualFree\r\nA 0x3dc2 GetCurrentProcess\r\nA 0x3dd6 GetLastError\r\nA 0x3de6 CloseHandle\r\nA 0x3df4 GetSystemInfo\r\nA 0x3e04 WideCharToMultiByte\r\nA 0x3e18 KERNEL32.dll\r\nA 0x3e28 CharUpperA\r\nA 0x3e34 USER32.dll\r\nThe main thread is responsible for running the following items (in order):\r\nManually building a base64 dictionary for use later\r\nDecoding and building imports\r\nLoadLibraries for later use / Get function addresses\r\nCreate the Mutex\r\nAdjust/Check Privs\r\nSet up sockets\r\nCreate Mailslot \u0026 Monitor for Credit Card Data\r\nSet up persistence\r\nInject and search for CC data\r\nThe reader may notice that imports like ReadProcessMemory, VirtualQueryEx, OpenProcess, etc.. are not present\r\nin this strings dump, they will be imported later. These API’s are commonly used in credit card dumpers and used\r\nto crawl process memory space. Bernhard seems to take some care to not get immediately detected.\r\nThese APIs are resolved using standard shellcode practices. It manually parses through Kernel32’s PE header to\r\nfind its list of exported functions, then hashes the name of each one until it matches the hash of the API it’s\r\nlooking for (LoadLibraryA). It uses similar logic to resolve the other API’s it needs. It does hide the names of the\r\ndll’s it needs by decoding them at runtime using the xor key [0x0B,0x0A,0x17,0x0D,0x1A,0x1F] (same one used\r\nfor exfil below). It also xor’s the resulting plaintext again when it is finished so they’re only plaintext in memory\r\nfor a tiny sliver of time, likely to try to avoid being caught by memory scans.\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 2 of 9\n\nWhile crawling through kernel32’s PE header, the shellcode does an interesting trick. To avoid being picked up by\r\nAV, the malware places junk instructions in between the MOV operations. Notice the ADD’s followed\r\nimmediately by the SUB, resulting in no change in EAX. This is simply meant to throw off AV scanners that look\r\nfor the FS[:30] shellcode technique.\r\nThe string OPSEC_BERNHARD correlates to the name of the mutex. Traditionally a mutex is used to make sure\r\nthat only one instance of the malware is running on the machine.\r\nIn addition to creating a mutex, Bernhard will also create a mailslot named ww2. This is used as a temporary\r\nstorage for the found credit card numbers.\r\nPersistence\r\nTo establish persistence on the host, the following command is decoded by the malware and executed. (Where in\r\nthis case cdcdc7331e3ba74709b0d47e828338c4fcc350d7af9ae06412f2dd16bd9a089f is the filename of the\r\nbinary)\r\nschtasks /create /tn ww /sc HOURLY /tr \\\"C:\\cdcdc7331e3ba74709b0d47e828338c4fcc350d7af9ae06412f2dd16bd9a089f\\\"\"\r\nThe options are\r\nTask name - ww\r\nSchedule - Hourly\r\nRun as user - System\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 3 of 9\n\nIt also sets up an autorun key\r\nHKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\coreService\r\nProcess Injection\r\nProcess Enumeration and Filtering\r\nAfter all of the initialization code, the sample begins its main injection routine which will run every 3 minutes\r\nindefinitely. Like most POS samples, it iterates over running processes. Unlike most, (which use\r\nCreateToolhelp32Snapshot) it uses ZwQuerySystemInformation (/w SystemInformationClass =\r\nSystemProcessInformation). This returns an array of structures describing each process running on the system.\r\nThe malware then iterates over these structures, passing each pid and process name to a filtering function which\r\ndetermines whether to inject or not. The following processes are blacklisted (not an exhaustive list, just the ones\r\nskipped over on my personal analysis machine):\r\nPID 0\r\nPID 4\r\nItself\r\ncsrss.exe\r\nwinlogon.exe\r\nlsass.exe\r\nsvchost\r\nexplorer\r\nalg.exe\r\nwscntfy.exe\r\nInjection\r\nOnce a process has passed the filtering the actual injection occurs:\r\n1. ZwQueryInformationProcess is used to get the address of the PEB in the remote process.\r\n2. The PEB is read. One of the fields in the PEB contains the load address of the target module.\r\n3. The first 40 bytes of the remote process are read. A marker of 0x029A is written in the header of the remote\r\nprocess (offset 0x24). This appears to never be referenced again which is strange.\r\n4. Standard code injection via WriteProcessMemory \u0026 CreateRemoteThread is used to deploy the CC track\r\ndata scraper to the remote process.\r\nInjected Code\r\nThe injected code just iterates over all virtual memory sections in the remote process. If a memory section has\r\nproperty MEM_COMMIT and access PAGE_READ_WRITE, then the code begins searching for valid track data\r\nusing a custom algorithm. When valid track data is found, it is immediately sent to the mailslot. The main process\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 4 of 9\n\nreads them from the mailslot, verifies them /w Luhn’s and sends them out to the C2 (See Exfiltration). The\r\nfollowing code is a similar implementation to how the authors implemented Luhns.\r\nint IsValidCC(const char* cc,int CClen)\r\n {\r\n const int m[] = {0,2,4,6,8,1,3,5,7,9}; // mapping for rule 3\r\n int i, odd = 1, sum = 0;\r\n for (i = CClen; i--; odd = !odd) {\r\n int digit = cc[i] - '0';\r\n sum += odd ? digit : m[digit];\r\n }\r\n return sum % 10 == 0;\r\n }\r\nExfiltration\r\nExfiltration is done via DNS to 29a.de. (5.101.147.126)\r\nThe C2 is manually constructed\r\nand a DNS request looks like the following.\r\nThe credit card numbers in the DNS requests are base64 encoded and xor’d using a key of “0B 0A 17 0D 1A 1F”.\r\nWith the following simple ruby script these can be decoded.\r\nrequire 'base64'\r\nxor_key = [0x0B,0x0A,0x17,0x0D,0x1A,0x1F]\r\nrequest = \"PzMnPiosOD4nOCwuOzomPS4nNjovPS8uOzsnNCstODkjOCwoMwAA.29a.de\"\r\ncc_num = request.split(\".\").first\r\nenc_num = Base64.decode64(cc_num)\r\ncount = 0\r\nenc_num.bytes.each do |byte|\r\n print \"#{((byte ^ xor_key[count % xor_key.length]) % 0xff).chr}\"\r\n count += 1\r\nend\r\n \r\n=begin\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 5 of 9\n\n#example dns query\r\n#16 43.022113000 10.0.2.15 5.101.147.126 DNS 119 Standard query 0x0065 A PzMnPiosOD4nOCwuOzom\r\n#running the script\r\n490303340561001048=080510109123345678\r\n=end\r\nVirustotal DNS also has some interesting history on the IP 5.101.147.126\r\nDetection\r\nThe following yara rule will detect BernhardPOS.\r\nrule BernhardPOS {\r\n meta:\r\n author = \"Nick Hoffman / Jeremy Humble\"\r\n last_update = \"2015-07-14\"\r\n source = \"Booz Allen Inc.\"\r\n description = \"BernhardPOS Credit Card dumping tool\"\r\n strings:\r\n /*\r\n 33C0 xor eax, eax\r\n 83C014 add eax, 0x14\r\n 83E814 sub eax, 0x14\r\n 64A130000000 mov eax, dword ptr fs:[0x30]\r\n 83C028 add eax, 0x28\r\n 83E828 sub eax, 0x28\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 6 of 9\n\n8B400C mov eax, dword ptr [eax + 0xc]\r\n 83C063 add eax, 0x63\r\n 83E863 sub eax, 0x63\r\n 8B4014 mov eax, dword ptr [eax + 0x14]\r\n 83C078 add eax, 0x78\r\n 83E878 sub eax, 0x78\r\n 8B00 mov eax, dword ptr [eax]\r\n 05DF030000 add eax, 0x3df\r\n 2DDF030000 sub eax, 0x3df\r\n 8B00 mov eax, dword ptr [eax]\r\n 83C057 add eax, 0x57\r\n 83E857 sub eax, 0x57\r\n 8B4010 mov eax, dword ptr [eax + 0x10]\r\n 83C063 add eax, 0x63\r\n */\r\n $shellcode_kernel32_with_junk_code = { 33 c0 83 ?? ?? 83 ?? ?? 64 a1 30 00 00 00 83 ?? ?? 83 ?? ?? 8b 40 0c\r\n $mutex_name = \"OPSEC_BERNHARD\"\r\n $build_path = \"C:\\\\bernhard\\\\Debug\\\\bernhard.pdb\"\r\n /*\r\n 55 push ebp\r\n 8BEC mov ebp, esp\r\n 83EC50 sub esp, 0x50\r\n 53 push ebx\r\n 56 push esi\r\n 57 push edi\r\n A178404100 mov eax, dword ptr [0x414078]\r\n 8945F8 mov dword ptr [ebp - 8], eax\r\n 668B0D7C404100 mov cx, word ptr [0x41407c]\r\n 66894DFC mov word ptr [ebp - 4], cx\r\n 8A157E404100 mov dl, byte ptr [0x41407e]\r\n 8855FE mov byte ptr [ebp - 2], dl\r\n 8D45F8 lea eax, dword ptr [ebp - 8]\r\n 50 push eax\r\n FF150CB04200 call dword ptr [0x42b00c]\r\n 8945F0 mov dword ptr [ebp - 0x10], eax\r\n C745F400000000 mov dword ptr [ebp - 0xc], 0\r\n EB09 jmp 0x412864\r\n 8B45F4 mov eax, dword ptr [ebp - 0xc]\r\n 83C001 add eax, 1\r\n 8945F4 mov dword ptr [ebp - 0xc], eax\r\n 8B4508 mov eax, dword ptr [ebp + 8]\r\n 50 push eax\r\n FF150CB04200 call dword ptr [0x42b00c]\r\n 3945F4 cmp dword ptr [ebp - 0xc], eax\r\n 7D21 jge 0x412894\r\n 8B4508 mov eax, dword ptr [ebp + 8]\r\n 0345F4 add eax, dword ptr [ebp - 0xc]\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 7 of 9\n\n0FBE08 movsx ecx, byte ptr [eax]\r\n 8B45F4 mov eax, dword ptr [ebp - 0xc]\r\n 99 cdq\r\n F77DF0 idiv dword ptr [ebp - 0x10]\r\n 0FBE5415F8 movsx edx, byte ptr [ebp + edx - 8]\r\n 33CA xor ecx, edx\r\n 8B4508 mov eax, dword ptr [ebp + 8]\r\n 0345F4 add eax, dword ptr [ebp - 0xc]\r\n 8808 mov byte ptr [eax], cl\r\n EBC7 jmp 0x41285b\r\n 5F pop edi\r\n 5E pop esi\r\n 5B pop ebx\r\n 8BE5 mov esp, ebp\r\n 5D pop ebp\r\n */\r\n $string_decode_routine = { 55 8b ec 83 ec 50 53 56 57 a1 ?? ?? ?? ?? 89 45 f8 66 8b 0d ?? ?? ?? ?? 66 89 4d\r\n condition:\r\n any of them\r\n }\r\nConclusion\r\nWhat makes BernhardPOS stand out is the use of code that continues to evade AV detection. Between manually\r\nresolving imports when they are needed and inserting junk code between legit operations, this malware stays\r\nsuccessfully hidden. It manually encodes the strings that it needs to in order to evade a simple string based rule.\r\nAnd it doesn’t heavily pack or encrypt itself in a way that would set off high entropy rules. In most network\r\nscenarios, DNS is a port left wide open due to machines needing to communicate with one another and the larger\r\nInternet. Leveraging DNS allows the malware authors to not worry about being blocked by a firewall or hindered\r\nby network restrictions.\r\nThere doesn’t seem to be a stop to attacks on point of sale machines. By using the same technique of finding credit\r\ncard information in a processes memory space, malware samples like these continue to be successful.\r\nSample Details\r\nChecksums\r\nFilename - cdcdc7331e3ba74709b0d47e828338c4fcc350d7af9ae06412f2dd16bd9a089f\r\nMD5Sum - e49820ef02ba5308ff84e4c8c12e7c3d\r\nSHA1 - a0601921795d56be9e51b82f8dbb0035c96ab2d6\r\nSHA256 - cdcdc7331e3ba74709b0d47e828338c4fcc350d7af9ae06412f2dd16bd9a089f\r\nSHA512 - c693533d68f38cf2d7107c14b1c2fa1157dc16fc93a976851de59e8ab819898a538104a4164e9648291d323fb93b38930d810\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 8 of 9\n\nIMPHash - fd8af1cc60e7046c1e08e4d95bac68f7\r\nPEHash - ece74afd17d0d18d819d687ea550cad97d703e94\r\nSource: https://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nhttps://securitykitten.github.io/2015/07/14/bernhardpos.html\r\nPage 9 of 9",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"ETDA",
		"Malpedia"
	],
	"references": [
		"https://securitykitten.github.io/2015/07/14/bernhardpos.html"
	],
	"report_names": [
		"bernhardpos.html"
	],
	"threat_actors": [],
	"ts_created_at": 1775434668,
	"ts_updated_at": 1775791234,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/53af2b777e020a5481ac4ee947bbcacb92da866f.pdf",
		"text": "https://archive.orkl.eu/53af2b777e020a5481ac4ee947bbcacb92da866f.txt",
		"img": "https://archive.orkl.eu/53af2b777e020a5481ac4ee947bbcacb92da866f.jpg"
	}
}