For years, a large part of my payment systems research has followed a similar process. I connect some hardware, capture a transaction, inspect the APDUs, modify something, execute the transaction again and compare the results. Sometimes I am trying to understand how a terminal interprets a particular EMV tag. In other cases, I want to know exactly what the card returned, what the terminal changed locally, or what information eventually continued into the authorization side of the payment.

This approach works well when the experiment is small, but the laboratory quickly becomes more complicated as the research grows. A contact EMV experiment can involve a real payment terminal, SIMtrace2, a physical card, a PC/SC reader, a virtual smart-card interface, several Python processes, mutation rules and multiple logs. Moving the same experiment to NFC introduces another set of constraints around RF roles, ISO-DEP, frame sizes and timing. Moving higher into the payment infrastructure changes the problem again, because the transaction is no longer represented only by APDUs but also by ISO 8583 messages and the EMV information transported inside them.

Eventually, I realized that I was spending too much time rebuilding the environment around the research question itself. The problem was no longer simply capturing an APDU or writing another script. What I wanted was a repeatable environment where I could observe a payment transaction, understand what the card supports, modify one specific part of the communication, see exactly what changed and preserve that information for the next experiment.

That is what became ATRIUM.

ATRIUM is a research workbench for EMV payment systems. It brings together the different pieces I normally need during a payment security assessment: hardware discovery, APDU relay, protocol decoding, card fingerprinting, controlled mutations, contact and contactless transports, persistent card intelligence, an optional AI research agent and a host-side layer for working with ISO 8583. The objective is not to implement one particular EMV attack. The objective is to create an environment where a payment security question can be converted into a controlled and reproducible experiment.

Why ATRIUM?

The name started with one of the first things a contact smart card gives us: the ATR, or Answer To Reset. The ATR is transmitted when the card begins a contact session and provides information about the communication characteristics supported by the card before the EMV application itself really starts. ATRIUM also worked as a name because an atrium is a central space connecting different parts of a structure, which became increasingly appropriate as the project expanded beyond a simple card relay.

At the beginning, the project was mostly concerned with the communication between a card and a payment terminal. Over time, it became clear that many of the questions I wanted to investigate could not be answered by looking only at that boundary. A payment transaction continues beyond the terminal through an acquirer, gateway, processor and issuer. Those systems may communicate using completely different protocols, but they are still processing information that originated during the EMV transaction.

A simplified view of the transaction looks like this:

Card <-> Terminal <-> Acquirer <-> Issuer
APDUs ISO 8583

The card side and the host side operate in very different protocol domains, but one of the places where they meet is DE55. Field 55 of an ISO 8583 authorization message can carry ICC data encoded as BER-TLV, which means that information created during the EMV interaction can continue into the authorization infrastructure. This creates an interesting research opportunity because an experiment does not necessarily need to stop after determining what the terminal saw. We can also ask what information ultimately reached the issuer and whether the original condition remained observable further upstream.

Sitting between the card and the terminal

For contact EMV, the basic ATRIUM setup places SIMtrace2 on the terminal-facing side of the experiment and a PC/SC reader on the card side. SIMtrace2 presents the interface expected by the real payment terminal while ATRIUM forwards complete APDUs toward the physical card.

Payment Terminal
|
| ISO 7816
v
SIMtrace2
|
| APDUs
v
ATRIUM
|
| PC/SC
v
Physical Reader
|
v
EMV Card

From the perspective of the terminal, it is still communicating with a card. From the perspective of the physical card, normal APDU commands continue to arrive through the reader. ATRIUM exists between those two endpoints and can observe everything crossing that boundary.

The relay itself is useful, but the interesting part for me is what can be done around it. Every command and response can be logged and decoded, the card can be fingerprinted, specific parts of the transaction can be modified and the resulting behavior can be compared against the original exchange. This turns the relay from a transport mechanism into a research environment.

It is also important that ATRIUM preserves the distinction between what the card originally returned and what the terminal eventually received. Once we modify traffic, there are effectively two versions of the same exchange. If the tooling records only the modified message, part of the evidence is lost. A researcher coming back to that capture later could easily assume that a particular value originated from the card when it was actually introduced by a mutation rule. For that reason, the intervention performed by the workbench needs to remain visible in the trace.

Controlled mutations

A normal APDU logger answers the question of what happened during the transaction. ATRIUM is designed to help answer the question that normally comes immediately afterward: what happens if I change this?

The mutation engine operates inside the relay path and can modify information traveling in either direction. For example, when the terminal constructs GET PROCESSING OPTIONS, the PDOL may contain values such as terminal capabilities, transaction amount, country code or other information requested by the card. ATRIUM can parse that structure and replace one specific value before the APDU reaches the card.

A simple research rule might look like this:

pdol_mutations:
- tag: "9F02"
value: "000000000001"
enabled: true
comment: "Research amount handling"

The specific value is less important than the ability to control the experiment precisely. If I modify many fields at the same time and the behavior changes, it becomes difficult to know which modification mattered. If I modify a single value while leaving the rest of the transaction intact, the result becomes much easier to reason about.

The same concept applies in the opposite direction. ATRIUM can intercept a response from the card, identify a BER-TLV object and apply a controlled operation before the terminal receives it. Depending on the experiment, that operation can replace a value, remove a tag, modify selected bytes or flip an individual bit. These operations are intentionally generic because they are research primitives rather than predefined attacks. The researcher decides what question the mutation is intended to answer.

Fingerprinting the card before changing anything

Before deciding what to modify, I normally want to understand the card itself. ATRIUM includes a fingerprinting layer that reads the card and builds a structured profile containing information useful for later experiments. This can include the available AIDs, AIP capabilities, AFL, PDOL structure, CVM rules, CDOL information and other characteristics exposed by the application.

A basic fingerprint can be generated directly from the command line:

python3 card_fingerprint.py --reader 0

The complete profile can also be written to JSON so that it can be analyzed or compared later:

python3 card_fingerprint.py \
--reader 0 \
--output profile.json

There is also an optional scan outside the AFL for experiments where I want to inspect additional SFIs:

python3 card_fingerprint.py \
--reader 0 \
--brute-sfi

The resulting profile is normalized and hashed so the same card can be recognized across later sessions. This changes the way I approach an experiment because the first step does not have to be running a list of tests blindly. Instead, I can first understand what the card advertises and then decide which experiments make sense for that particular configuration.

A card that does not expose the capability required for a particular hypothesis has already given us useful information before the mutation engine is ever enabled.

Research that remembers previous sessions

Payment research rarely happens in one continuous session. I may test a card today, work on another problem for several weeks and eventually return to the original card. Without persistent context, I normally have to reconstruct what happened by reading old traces, notes and screenshots.

ATRIUM maintains a card intelligence database to preserve that context. A card fingerprint can be associated with information collected during previous sessions, including AIDs, AIP flags, PDOL and CDOL structures, CVM rules, transaction-counter observations, research notes and the results of earlier mutation experiments.

Conceptually, the flow looks like this:

Card
|
v
Fingerprint
|
v
Card Intelligence
|
+--> Capabilities
+--> Previous sessions
+--> Mutation outcomes
+--> Research notes

The purpose is not simply to create another database. The useful part is that a new research session can begin with information from the previous one. If a particular experiment already produced an interesting result, I can see that. If another configuration failed, I do not necessarily need to repeat it blindly. Over time, the laboratory begins to accumulate knowledge rather than producing isolated captures.

This becomes particularly useful when an AI agent is placed above the research workflow.

AI as a research orchestrator

I have been using AI increasingly in my security research, but I am not particularly interested in adding a chatbot to a security tool simply so the interface can claim it uses AI. For me, the interesting part starts when the model can interact with actual research components and reason over structured evidence.

The AI agent in ATRIUM operates above the deterministic parts of the workbench. The APDU relay remains responsible for transporting commands. The parser still interprets the protocol. The mutation engine continues to execute explicit rules. The fingerprinting layer retrieves actual information from the card. The model does not replace any of those components; instead, it can use them as tools.

A typical workflow may start by fingerprinting the card and loading previous card intelligence. The model can then reason about the available capabilities, decide which experiment is relevant, configure the corresponding mutation rules and start the relay. After the transaction finishes, it can inspect the mutation log, understand which rules actually fired and record the result for future sessions.

Fingerprint card
|
v
Load previous intelligence
|
v
Understand capabilities
|
v
Configure experiment
|
v
Start relay
|
v
Observe transaction
|
v
Read results
|
v
Store outcome

This is much closer to the way I want to use AI for security research. If the model wants to know which CVMs a card supports, it can retrieve that information from the fingerprint instead of inventing an answer from the prompt. If it needs to know whether a mutation really executed, it can read the mutation log. If the same card was tested previously, it can retrieve the historical results.

The protocol remains visible throughout the process. The purpose of the AI layer is not to hide EMV behind natural language but to help automate parts of the reasoning and orchestration while leaving the underlying evidence accessible to the researcher.

Basic installation

The current ATRIUM environment is primarily designed for Linux. Debian and Ubuntu are the systems I normally use because the PC/SC and smart-card tooling is straightforward to configure. A basic installation requires Python 3.11 or later, PC/SC, the development libraries used by pyscard, and a few standard build tools.

The Python version is worth confirming before anything else, because Debian 11 still ships 3.9 as python3 and the failure arrives much later than the cause:

python3 --version

If that reports anything below 3.11, install a newer interpreter first and use it for the virtual environment created further down.

sudo apt install -y cmake libudev-dev help2man
git clone https://github.com/frankmorgner/vsmartcard.git
cd vsmartcard/virtualsmartcard
autoreconf --verbose --install
./configure --sysconfdir=/etc
make
sudo make install

ne detail here is easy to miss. make install copies the native vpcd files, but it does not always install the Python package into the interpreter you are actually using. The result is a build that reports success and an import that fails later, during the part of the session where it is least convenient to discover.

I prefer to check it immediately:

python3 -c "import virtualsmartcard"

If that fails, the package can be linked into the interpreter’s site directory by hand. From the directory where you just built it:

python3 -c "import site; print(site.getsitepackages()[0])"sudo ln -s "$PWD/src/vpicc/virtualsmartcard" \
/usr/local/lib/python3.11/dist-packages/virtualsmartcard

Adjust the destination to match the directory the first command printed, then confirm:

python3 -c "from virtualsmartcard.VirtualSmartcard import SmartcardOS; print('virtualsmartcard OK')"

Once the system dependencies are installed, ATRIUM can be cloned and installed inside a Python virtual environment:

git clone https://github.com/salmg/atrium.gitcd atriumpython3 -m \
venv .venvsource .venv/bin/activatepip install -r requirements.txt

Before starting ATRIUM, I normally verify the physical smart-card environment independently. This removes one layer of uncertainty before introducing the relay.

sudo systemctl enable --now pcscd
pcsc_scan

If pcsc_scan cannot see the reader or the card, I prefer solving that problem first. Debugging payment research environments becomes unnecessarily difficult when several layers are being diagnosed at the same time.

Reader discovery

One small problem that becomes important once Virtual Smart Card is involved is that PC/SC reader indexes are not necessarily what we expect. The virtual reader can appear alongside the physical hardware, so assuming that index 0 represents the physical card is not reliable.

ATRIUM can enumerate and classify the available readers:

python3 atrium.py readers

A system may contain something similar to:

[0] Virtual PCD 00 00 (virtual)
[1] ACS ACR122U PICC Interface 00 (contactless)
[2] Gemalto PC Twin Reader 00 00 (contact)

The virtual reader represents part of ATRIUM’s terminal-facing path and is not the location of the physical card. ATRIUM therefore attempts to classify and automatically select suitable hardware instead of depending exclusively on numeric reader indexes. When necessary, a reader can still be selected using its index or part of its PC/SC name.

Starting ATRIUM

The web interface can be started with:

python3 atrium.py serve

By default, it is available locally at http://127.0.0.1:8000. The relay and dashboard can also be started together with:

python3 atrium.py all

The dashboard organizes the experiment around the state of the hardware, the relay session, the card fingerprint, active mutations, the optional AI agent and the resulting traces. I do not think of these as mandatory steps that every experiment must follow in exactly the same order. Their main purpose is to make the current state of the laboratory visible.

If SIMtrace2 is not detected, I want to know that before trying a transaction. If a playbook is loaded but not armed, I want the interface to distinguish those states. If the card has not been fingerprinted, that should also be obvious. And if no AI backend has been configured, the rest of the environment should continue operating normally because AI is an optional layer rather than a dependency of the relay.

Configuring the AI backend

ATRIUM can work without any model configured. The relay, fingerprinting, mutations, logs and card intelligence remain available independently. If I want to enable the agent, the configuration can be as small as providing an API key or the address of a local OpenAI-compatible model server.

For OpenAI:

export OPENAI_API_KEY="..."

For Anthropic:

export ANTHROPIC_API_KEY="..."

A local model can also be used. For example, an Ollama instance exposing an OpenAI-compatible endpoint can be configured with:

export ATRIUM_LLM_BASE_URL="http://localhost:11434/v1"
export ATRIUM_LLM_MODEL="qwen2.5:14b"

The important requirement is tool calling. The model needs to be capable of invoking operations such as card fingerprinting, loading previous intelligence, configuring mutations and controlling relay sessions. A model that can only produce conversational text cannot perform the complete research loop.

Once a backend is available, the agent can be started normally:

python3 atrium.py agent

A specific research objective can also be supplied directly:

python3 atrium.py agent \
--task "Fingerprint the card and inspect its AIP, PDOL and CVM configuration"

Environmental information can be passed separately:

python3 atrium.py agent \
--system-extra "Terminal: laboratory payment terminal. Contact EMV environment."

I prefer keeping these concepts separate because the task describes what the agent should investigate, while the additional system context describes the environment in which the experiment is taking place.

SIMtrace2 and contact EMV

SIMtrace2 is an important part of the contact side of ATRIUM. My previous research extending SIMtrace2 around ISO 7816-3 T=1 provided a way to place an open research platform directly between an EMV terminal and the rest of the laboratory.

One of the interesting observations from that work is that the two physical sides of a relay do not necessarily need to use the same lower-level smart-card protocol if the research boundary operates on complete APDUs. The terminal-facing side and the real-card side can therefore be treated as separate protocol domains, while ATRIUM works above them with complete APDU commands and responses.

Payment Terminal
|
| ISO 7816
v
SIMtrace2
|
| complete APDUs
v
ATRIUM
|
| PC/SC
v
Real EMV Card

That separation is useful because it creates a stable research point above the physical protocol. The hardware and lower-level transport can change while the mutation, logging and fingerprinting logic continues to operate against the same APDU abstraction.

Contactless changes the physical constraints

Many of the questions that are interesting over contact EMV are also interesting over contactless, but the physical behavior of the two interfaces is very different. ATRIUM supports contactless experiments through devices such as the ACR122U, which contains a PN532 behind a CCID bridge.

Basic hardware information can be retrieved with:

python3 atrium.py nfc info

A card in the reader field can be inspected with:

python3 atrium.py nfc scan

The target side can be started using:

python3 atrium.py nfc emulate

One important limitation becomes apparent immediately: a single ACR122U cannot normally perform both sides of the relay. The reader facing the payment terminal is operating as an RF target, while the real card requires another device acting as an initiator. Those are different roles.

A basic contactless arrangement therefore looks like:

Payment Terminal
|
| RF
v
ACR122U #1
target
|
v
ATRIUM
|
v
ACR122U #2
initiator
|
| RF
v
Real Card

The card source does not necessarily need to be another ACR122U. It can also be a remote card, a recorded transaction or an Android device using NFCGate. The important architectural point is that the layers above the transport should continue seeing the same card abstraction.

Timing is part of the experiment

Contactless research makes it very clear that the bytes alone do not describe the whole protocol. A command can be valid, the response can contain the correct TLV structure and the transaction can still fail simply because the answer arrived too late.

Every USB round trip, RF exchange, network hop or injected command consumes part of the terminal’s timing budget. An injected APDU, for example, is not simply another line in the trace. The command has to travel to the real card, the card needs to process it, the response needs to return, and only then can the original transaction continue.

On a contact interface, the additional delay may be acceptable. On contactless, that same operation may cause the kernel to abandon the transaction.

This distinction is important because without timing visibility a researcher can reach the wrong conclusion. It may appear that a particular protocol modification was rejected when the actual result was that the relay delivered a perfectly valid response outside the time in which the terminal was willing to accept it.

ISO/IEC 14443-4 provides mechanisms such as the frame waiting time and S(WTX) to deal with some of these timing conditions. ATRIUM includes experiments around these mechanisms because once the timing can be measured, the question changes from “maybe the terminal timed out” to something much more useful: the terminal had a particular budget, the relay consumed a measurable amount of that budget, and the exchange exceeded it.

Hardware limitations should remain visible

The ACR122U has also been useful because its limitations expose several boundaries that are easy to overlook. The PN532 performs parts of ISO-DEP internally, frame sizes are constrained, some target-mode parameters are controlled by the chip rather than the host, and the commands required for card emulation may need to travel through the CCID escape channel.

On Linux, for example, libccid may need to be configured to allow those escape commands:

sudo sed -i \
's|<string>0x0000</string>|<string>0x0001</string>|' \
/etc/libccid_Info.plist
sudo systemctl restart pcscd

I do not consider these hardware details separate from the research. They are part of it. If a transaction fails because a reader cannot perform a particular operation, the tooling should help distinguish that from a card rejecting the command or a terminal rejecting the response.

A hardware limitation and a protocol result are not the same thing, even if both appear to the researcher as a timeout.

NFCGate as another card transport

ATRIUM can also use an Android device running NFCGate as the card-side transport. In this configuration, the phone operates in reader mode, communicates with the physical card over NFC and forwards the APDUs into the ATRIUM session.

ATRIUM
|
v
NFCGate session
|
v
Android phone
|
| RF
v
EMV Card

The interesting part is that the higher layers of ATRIUM do not need to know that the card is behind a phone. Fingerprinting still operates against the same card abstraction, the mutation engine still receives APDUs, and the live trace continues recording commands and responses in the same way.

The tradeoff is timing. The transaction now includes the phone’s NFC stack and an additional network path, so contactless timing becomes even more important. This is another reason I prefer to treat the transport as part of the experiment rather than as an invisible implementation detail.

Remote cards

The same transport abstraction also allows the physical card to exist on another machine. A PC/SC reader can be connected to a remote host while SIMtrace2 and the payment terminal remain on the research rig.

A pairing identity can be generated using:

python3 atrium.py pair \
--advertise-host 203.0.113.9 \
--port 7654

The card-side proxy can then be started with:

python3 card_proxy.py \
--secure \
--host 0.0.0.0 \
--port 7654 \
--reader 0

The remote transport uses TLS, certificate pinning and an access token. This matters because a remote card service is not a normal network application; it exposes the ability to exchange APDUs with the physical card. I therefore prefer the default configuration to prevent accidental plaintext exposure rather than making the insecure configuration the easiest one to start.

For the same reason, plaintext operation is intended for loopback or for an explicitly created tunnel rather than for direct exposure to a network.

Following the transaction into ISO 8583

The host side of ATRIUM extends the research beyond the card-present boundary. At this level, the interesting question changes from what the terminal saw to what the rest of the payment infrastructure eventually received.

The connection between those two worlds is especially visible in DE55, where ICC data can continue from the EMV transaction into the authorization message. This allows experiments to follow a value beyond the terminal.

Suppose I modify one piece of EMV information during the card transaction and the terminal accepts it. That result is already interesting, but it is not necessarily the end of the investigation. We can now inspect what the authorization system received, whether the modified condition survived into DE55 and whether another field still allowed an issuer or processor to reconstruct the original state.

This is where some payment security problems stop being simple protocol problems and become architectural ones.

The host tooling is intentionally separate from the card hardware and can be run without pyscard or Virtual Smart Card. A basic observation proxy can look like this:

python3 -m host.cli proxy \
--target sim.test:5000 \
--allow sim.test:5000 \
--capture logs/host.jsonl

Keeping the host layer independent means that the card laboratory and authorization laboratory do not have to exist on the same machine. They can observe different parts of the same transaction while sharing the same EMV parsing concepts.

Protecting the research workbench

Once a security tool can control hardware and modify live payment protocol traffic, the tool itself becomes part of the security model. ATRIUM therefore binds its web interface to loopback by default rather than exposing the control plane to the network.

If the interface needs to listen on a wider address, an API token can be configured:

export ATRIUM_API_TOKEN="$(
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
)"
export ATRIUM_ALLOWED_HOSTS="atrium.lab.internal"
python3 atrium.py serve --host 0.0.0.0

Even in that configuration, I normally prefer an SSH tunnel when remote access is needed:

ssh -L 8000:127.0.0.1:8000 user@research-host

SIMtrace2 introduces another privilege boundary, because raw USB access to the board may otherwise require running its daemon with elevated permissions. The tempting design is to let the web interface start that daemon on the researcher’s behalf, and it is the wrong one. A web application that can launch a privileged process on request has quietly become a root shell for anything that can reach the port.

ATRIUM does not do that. It starts no subprocess at all. The daemon runs in your own terminal, and ATRIUM locates it by reading /proc, which requires no privilege of its own. That is the kind of property that erodes the moment somebody adds one convenient feature, so it is enforced rather than documented: tests/test_security.py walks the module’s AST and fails the build if an execution call ever reappears there.

That still leaves the board itself, which a udev rule can hand to the logged-in session:

echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="60e3", MODE="0660", TAG+="uaccess"' \
| sudo tee /etc/udev/rules.d/60-simtrace2.rules
sudo udevadm control --reload

After reconnecting the board, simtrace2-remsim runs under the user’s normal session without sudo. I prefer this type of separation because a payment security workbench should not quietly become a general-purpose privileged execution interface.

What I want ATRIUM to become

ATRIUM is still evolving, and I do not consider it a finished application. I see it more as the environment I wish I had when I started building some of these experiments.

The goal is to have one place where I can connect the hardware, understand what the card supports, observe the transaction, introduce a controlled modification, see exactly where the change happened, measure the effect of the transport, preserve the outcome and eventually follow the same information into another layer of the payment infrastructure.

I also want the AI component to help organize and accelerate that process without hiding the protocol underneath it. The evidence should continue to come from real APDUs, card profiles, mutation logs, timing measurements and authorization messages. The model can help reason about those observations, but it should never become a substitute for them.

The question that interests me is therefore not whether we can build another EMV attack tool. There are already many individual tools that are useful for studying different parts of payment systems. The more interesting question is whether we can build a workbench where a payment security hypothesis can move from an idea to a repeatable experiment without rebuilding the laboratory every time.

That is what I want ATRIUM to become: a place where more of the payment transaction is visible, where changes are explicit, where limitations are measurable and where the result of today’s experiment can become the starting point for tomorrow’s research.

ATRIUM is intended for security research on payment cards, terminals, hosts and laboratory environments that you own or have explicit authorization to test. The ability to inspect and modify protocol communication is exactly what makes this type of tooling useful, and also why controlling the environment in which it is used remains an important part of the research itself.

Repository: https://github.com/salmg/atrium

ATRIUM is also one of the practical tools used in SM Academy. Students use it during the payment systems security training to move beyond theory and work directly with EMV transactions, APDUs, card behavior, protocol data and controlled experiments.

The objective is for students not only to understand how EMV works, but to be able to observe it, interact with it and test their own hypotheses in a laboratory environment.

Academy: https://academy.salmg.net/