ATM security analysis 3.0: PHDays 12 in review
Python, Java, C++, Delphi, PHP—these programming languages were used create a virtual crypto ATM machine to be tested by the participants of the $NATCH contest at Positive Hack Days 12. The entire code was written by ChatGPT and proved to be exceptionally good. This time, we had reviewed the contest concept and decided to use a report system. In addition to standard tasks (kiosk bypass, privilege escalation, and AppLocker bypass), this year's participants faced new unusual tasks. Read on below to find out which ones.
Brief summary of the contest and some statistics
This year, the ATM dispensed special banknotes to participants that could be exchanged for merchandise, such as T-shirts and souvenir bank cards.
This is how it worked: you insert the received flag in an ATM, get a bundle of banknotes, and exchange the banknotes for a T-shirt.
More than 100 people joined the contest chat over the two days of competition (some of them left after the contest, but the chat history isn't going anywhere).
The winners shared the $500 prize money.
Incredibly, we had three participants who managed to come up with the same winning result. The final decision was therefore up to the organizers, that is, us.
?@drd0c: 25,000 rubles + a backpack
?@s0x28: 12,500 rubles + a backpack
?@nikaleksey: 12,500 rubles + a backpack
The competitors went toe-to-toe, so we decided there would be no bronze this year. Indeed, it would be a shame to announce a third-place winner. However, drd0c's reports were certainly more interesting than the others, which is why we made him the first winner.
This year we added a virtual machine update mechanism: during the contest, participants could run the updater.py file, after which the update.zip archive with new tasks would be downloaded.
Task review
ATMs have issues not only with Windows. In addition to an operating system, a real ATM has plenty of custom, highly-specialized software that it uses to issue banknotes, manage its internal components, connect with processing, and conduct transactions. This software is quite diverse and requires comprehensive and thorough analysis during a penetration test, as nobody knows which particular program attackers will use to break into the ATM and withdraw money. That was the idea behind this year's contest.
Kiosk 1 bypass
The first kiosk looked as follows: a crypto ATM interface with a scannable QR code. There is a bitcoin wallet encoded in the QR code, to which you send bitcoins so that the crypto ATM can issue banknotes. Public and private keys can later be used to manage funds available in the wallet. In fact, a real bitcoin wallet available on a virtual machine was created for the contest. That's how real crypto ATMs operate, usually with Windows under the hood.
By the way, the wallets were stored in the wallets.txt file as seen in the screenshot below.
A true information security professional should be well-rounded in their field, and well-versed in how cryptocurrencies work. The file contains private keys, and some contestants noticed this and submitted relevant reports. In real life, storing funds in a crypto ATM is not exactly safe.
And now to the kiosk: sometimes, the kiosk service mode is available in ATMs (for the convenience of the ATM servicing staff), for example, the keyboard can be turned on and off using just one key. This year, this was used by the contestants as the first method to bypass the kiosk. Every time the user pressed the u or г key, the keyboard would suddenly start working. It sounds simple, but in reality, if you don't know about this feature, it's not obvious.
import sys
import keyboard
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtWebEngineWidgets import QWebEngineView
class KioskApp(QMainWindow):
def __init__(self):
super().__init__()
# Window setting
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.showFullScreen()
# Web browser creation and setting
browser = QWebEngineView()
browser.settings().setDefaultTextEncoding("utf-8")
browser.load(QUrl("http://localhost:8000/create_wallet"))
self.setCentralWidget(browser)
# Gray layer creation
self.gray_layer = QWidget(self)
self.gray_layer.setStyleSheet("background-color: rgba(128, 128, 128, 20);") # 1% of transparency
self.gray_layer.setGeometry(0, 0, self.width(), self.height())
# Toggle keys (other keys are blocked by default)
self.keys_enabled = True
self.toggle_keys()
def keyPressEvent(self, event):
if event.key() == Qt.Key_U or (event.text() == 'г' and event.modifiers() == Qt.NoModifier):
self.toggle_keys()
elif self.keys_enabled:
super().keyPressEvent(event)
def toggle_keys(self):
self.keys_enabled = not self.keys_enabled
if self.keys_enabled:
keyboard.unhook_all()
self.gray_layer.hide()
else:
for key in keyboard.all_modifiers:
keyboard.block_key(key)
for i in range(1, 255):
if keyboard.key_to_scan_codes(i) != keyboard.key_to_scan_codes("u") and keyboard.key_to_scan_codes(i) != 33:
keyboard.block_key(i)
self.gray_layer.show()
def closeEvent(self, event):
# Unlock keys when closing the application
keyboard.unhook_all()
if __name__ == '__main__':
app = QApplication(sys.argv)
kiosk = KioskApp()
kiosk.show()
sys.exit(app.exec_())
Kiosk 2 bypass
The second kiosk looked as follows:
It was available in the public update we announced in the Telegram chat. The basic code functionality (shown below) is to create a window containing the kiosk bypass button and intercept keyboard events. When pressed, the button moves randomly within the window. If it's pressed more than 100 times, the window closes and the keyboard bypass stops.
A little bonus for Habr readers: below is the kiosk application code, complete and unaltered, so you can see how a kiosk app can be written in different languages and how it all works. An idea away for another day: create a kiosk that no hacker can compromise.
#include <Windows.h>
#include <cstdlib>
HHOOK keyboardHook;
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
void DisableKeyboard();
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
void MoveButtonRandomly(HWND hwnd);
HWND buttonHandle; // Global variable for storing the button descriptor
int clickCount = 0; // Global variable for counting clicks
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// Window class registration
WNDCLASS wc = {0};
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hbrBackground = (HBRUSH)(COLOR_BACKGROUND);
wc.lpszClassName = TEXT("FullScreenWindow");
RegisterClass(&wc);
// Window creation
HWND hwnd = CreateWindowEx(
0,
TEXT("FullScreenWindow"),
TEXT(""),
WS_POPUP,
0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),
NULL, NULL, hInstance, NULL);
// Creation of the "Exiting kiosk mode" button
buttonHandle = CreateWindow(
TEXT("BUTTON"),
TEXT("kiosk bypass"),
WS_VISIBLE | WS_CHILD,
10, 10, 200, 50,
hwnd, NULL, hInstance, NULL);
// Setting the window on top of all other windows
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
// Displaying the window
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd); // Updating and displaying the window content
// Setting the keyboard hook
keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
if (keyboardHook == NULL) {
// Processing an error when installing the keyboard hook
}
// Starting the message processing cycle
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Exiting the program
DisableKeyboard(); // Disabling the keyboard before exiting
return 0;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_COMMAND:
if (reinterpret_cast<HWND>(lParam) == buttonHandle) // Checking that the message was caused by clicking the button
{
MoveButtonRandomly(hwnd);
clickCount++;
if (clickCount >= 100)
{
DestroyWindow(hwnd); // Closing the window
DisableKeyboard(); // Disabling the keyboard
}
}
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
void MoveButtonRandomly(HWND hwnd)
{
RECT rect;
GetClientRect(hwnd, &rect);
// Generating random coordinates for moving the button
int newX = rand() % (rect.right - rect.left - 200) + rect.left;
int newY = rand() % (rect.bottom - rect.top - 50) + rect.top;
// Moving the button to new coordinates
SetWindowPos(buttonHandle, NULL, newX, newY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
}
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
{
// Preventing keyboard event processing
return 1;
}
}
return CallNextHookEx(keyboardHook, nCode, wParam, lParam);
}
void DisableKeyboard()
{
UnhookWindowsHookEx(keyboardHook);
Escalation of privileges: a password manager
Here comes a very likely situation: a password manager can easily be found on a crypto ATM. Unlikely for a real ATM, but not impossible for a virtual machine. Indeed, a Python password_manager using the Flask framework was found on a crypto ATM. It was just lying in a folder (inactive) waiting to be found and run. Privileges could be escalated by getting into the admin account (credentials were saved in Chrome). In fact, it was a full-fledged password manager with built-in vulnerabilities. If fine-tuned, it could be used to store passwords online.
I will not show you the code, as this would stretch my article to 200 pages (instead, you can download a virtual machine and do a little reverse engineering yourself). I didn't put the source code into an EXE file either, to make life more complicated for my readers. This password manager has a lot of vulnerabilities, from cleartext storage of passwords to XSS.
Interesting reports from participants
The virtual machine contained over 15 vulnerabilities. There were several noteworthy reports sent by our contestants. For example, s0x28 found a remote file reader. An attacker who was in the same network with the crypto ATM could, without even bypassing the kiosk, obtain the contents of any file, in our case, wallets.txt.
@nikaleksey successfully exploited the XSS vulnerability in the wallet application. Here's how it looked:
How @nikaleksey did it is a secret: this is something you have to figure out.
Let's not forget about CVEs: several exploits for well-known CVEs were successfully used on the virtual machine (for example, CVE-2020-0796). Several participants reported this almost simultaneously.
PS C:\Users\kiosk\AppData\Local\Programs\Python\Python310> .\cve-2020-0796-local.exe
-= CVE-2020-0796 LPE =-
by @danigargu and @dialluvioso_
Successfully connected socket descriptor: 192
Sending SMB negotiation request...
Finished SMB negotiation
Found kernel token at 0xffffb88770dd1060
Sending compressed buffer...
SEP_TOKEN_PRIVILEGES changed
Injecting shellcode in winlogon...
Success! ;)
If you could not make it to PHDays and see all the twists and turns of the competition live, you can watch a video created by the participants.
See how tense it was? This is how real ATMs are hacked! And that's just a small fraction of the reports that we received!
Unfortunately, there were many vulnerabilities that went undiscovered. Of course, I won't tell you all about them, that would be boring. Instead, you can download the virtual machine and try to find them yourself. It's possible that by the time this article comes out, I'll have added some new tasks to the virtual machine updates that I secretly added during the contest and that no one noticed.
Conclusions
A big thanks to everyone who participates in the contest year after year, helping to make it better. And another big thanks to new participants as well. This year, the format was truly experimental. We received a lot of feedback (mostly positive), but we definitely have room to improve. We look forward to seeing you again next year at PHDays and our ATM contest, which will be even better: version 4.0 is on the way!
A special thanks to my constant readers! Is anybody here reading the contest review for the third year in a row?
Contest reviews from previous PHDays events:
Until next time!
![](https://habrastorage.org/getpro/habr/upload_files/92f/39c/f51/92f39cf51dcfe5e7c0d047ef28a05567.png)
Yury Ryadnina
Banking Security Assessment Specialist, Positive Technologies. He also runs a popular channel on bug hunting. Click to subscribe:?? https://t.me/bughunter_circuit