We see it in movies, read about it on security blogs, and, the more sinister among us, dream about doing it – but what does it really take to perform a jackpotting attack on a bank ATM?
As part of a contract with a large commercial bank, we were tasked with assessing the security of an ATM protected by a well-known security product meant to block unauthorized code execution on sensitive systems. We were given full network and physical access to an NCR ATM — a very common ATM extensively used worldwide — and asked to find possible attack vectors.
So… How does this ATM malarkey work?
This post describes the challenges we faced in the process of compromising the ATM. So, before we get to the fun part, which involves bypassing that security software and making the ATM spew cash notes, we first have to understand the software architecture of modern ATMs:
A typical ATM is based on a Windows machine with many hardware components, such as a notes and coins dispenser, camera, touchscreen, card reader, and so on. Every ATM manufacturer creates their own drivers and service providers for these hardware components. In theory, a common middleware named XFS, which most ATM manufacturers adhere to, makes running the same application across hardware vendors possible. XFS deals purely with the aspect of delegating instructions to the hardware component and has no security mechanisms. If you can run compiled code on the ATM, its game over. We used that middleware to instruct the cash dispenser to repeatedly dispense notes until it had none left.
Having no experience in developing with XFS middleware, we tried to find documentation and example code online. Sadly, we couldn’t find any beyond the CEN generic and often documentation was missing. Luckily, we were not the first ones to try this attack. There are several known ATM malwares capable of dispensing cash from NCR ATMs. While obviously no source code is available, there are many writeups about ATM malwares that does wonders in filling the gaps about the NCR XFS implementation . We even went as far as looking at the “GreenDispencer” malware, just to catch some arguments passed to XFS APIs:
Eventually, we came up with the following routine:
Import msxfs.dll Call WFSStartUp Call WFSOpen with the name of the cash dispenser device (obtained from the ATM registry) Call WFSExecute with the WFS_CMD_CDM_DISPENSE command and WFSCDMDISPENSE structure with the desired amount to be dispensed set Call WFSExecute with the WFS_CMD_CDM_PRESENT command to present the cash notes Loop until cash cassettes are empty
Bypassing weak security
Now that we had a sense of what complied code we wanted to execute on the ATM, we were left with bypassing the endpoint hardening software, which seemed challenging at first — all executables on the system were put on whitelist and any attempt to execute something out of place was blocked. Luckily though, the bank was kind enough to whitelist the PowerShell executable, as the ATM startup routine uses some ps1 scripts. That essentially means we could run compiled DLL by using PowerShell to reflectively load it, similar to the method used by Invoke-MimiKatz. This method has the added benefit of being relatively stealthy as it does not write our malware to disk – its “file-less”.
To put everything together, our malware would be a PowerShell script that loads an embedded base64 encoded DLL, which in turn uses the XFS middleware to dispense cash notes. Obviously, we skipped some of the implementation details, and will not be providing sample code as the bank was understandably sensitive about it. However, filling the gaps should be straight forward from here.
Now that we had our malware, we came up with 2 attack vectors:
Attackers with access to the bank ATM network could remotely access the ATM and execute the malwareAttackers with physical access to the ATM could plug a “rubber ducky” loaded with the script and have it executed within minutes.
The first one was straight forward. With enough access any ATM can be targeted remotely from the bank’s internal network. How hard it will be to get that access level depends on the security architecture of the bank’s network, which is beyond the scope of this blog. We were given a remote desktop connection to a lab ATM and successfully executed the malware, dispensing (fake) notes to the surprise of the technicians manning the lab.
The second vector presented more challenges. Surprisingly, we discovered that ATMs run with a local administrator logged in. All we had to do was plug in a keyboard. Granted, getting to an exposed USB port on an ATM is everything but trivial (yet not impossible ). Additionally, attacks by insiders are not unheard of. After plugging the keyboard, we quickly found a key sequence which escapes the kiosk mode and provides us with a fully functioning windows machine:
Using that sequence, we built a rubber ducky script containing our PowerShell script. Since the rubber ducky is typing the script character by character, and the script ended up just shy of 100KB after being compressed and base64 encoded, the loading time of the rubber ducky was about 10 minutes. It can be reduced by further minimizing the PowerShell script and dynamically linking the embedded DLL.
Standard-issue Komodosec uniform
After the security assessment, the bank justifiably dropped the security software in favor of another solution, which among other security mechanisms puts PowerShell into Constrained Language Mode. This limits the capability of scripts to use WIN32 APIs, hindering attempts to reflectively load compiled executables – making our malware obsolete.
Yoni Zach, information security specialist