How to solve The DEF CON Credit Card Hacking Challenge

The main goal of this challenge was to show how dangerous access to the Transaction Stream could be for banks, payment systems and cardholders.

This challenge was more of an open question, "what could go wrong" for participants. I will describe in detail some potential solutions for the card hacking tasks that we presented at DEF CON this year.

Also, something always goes wrong whenever you do the challenges for the first time. So I will cover that part as well.

To reiterate, this year, we made a SoftPOS - the Android application that accepts NFC payments from the cards. And we issue our contactless credit cards. These are pretty much the same cards as Visa or MasterCard, but a different brand - PaymentVillage.Org cards. Therefore you can't pay with those cards anywhere except through our POS app. At the same time, our app can't use cards from any other payment system, like Visa or MasterCard. We used even the same EMV tags, so you can refer to different EMV tags KB like https://emvlab.org/emvtags/ to find the description of each tag.

Because we couldn't make it to Vegas this year, we passed 40 cards to the Retail Hacking Village organisers, who kindly offered their help.

For OFFZONE conference that took place a couple of weeks after DEF CON, we were not able to ship our Dual Interface cards in time – they were stuck in Hungarian customs. So I had to come up with a workaround – we took cards that organisers already had from the 2019 Java cards challenge and uploaded our code to these cards. The only problem here – they are not contactless. Hence we needed to migrate the challenge to a contact-based set-up. For this purpose, I quickly sketched Python code that replicates the code on our SoftPOS:

POSSim.py

from smartcard.System import readers

from smartcard.util import toHexString

import random

from datetime import date

import requests

 

r_list=readers()

i = 0

for r in r_list:

print str(i)+": "+str(r)

i = i+1

 

id = input("Select reader:")

 

connection = r_list[int(id)].createConnection()

connection.connect()

 

amount = input("Enter transaction amount without (,) E.g. for $10.01 enter 1001:")

if (amount>1000):

    exit("Price cannot be more than $10 as we do not accept PIN (yet).")

def APDU(cmd):

res = ""

for i in cmd:

     res = res + "%0.2X" % i

print ">>"+res

data, sw1, sw2 = connection.transmit( cmd )

res = ""

#print "%x %x" % (sw1, sw2)

for i in data:

     res = res + "%0.2X" % i

res = res + "%0.2X" % sw1+ "%0.2X" %sw2

    

print "<<"+res

return res

SELECT = [0x00, 0xA4, 0x04, 0x00, 0x0E, 0x32, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31, 0x00]

APDU(SELECT)

 

SELECT = [0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10, 0x00]

APDU(SELECT)

 

#80 A8 00 00 11 83 0F 70 55 81 F9 00 00 00 00 01 00 22 08 03 08 40 00

#9F 37 04 -- Unpredictable Number

#9F 02 06 -- Amount, Authorised (Numeric)

#9A 03 -- Transaction Date

#5F 2A 02 -- Transaction Currency Code

 

today = date.today()

 

UN = []

UN_str = ""

for i in range (0,4):

rand = random.randrange(0,255)

    UN.append(rand)

UN_str = UN_str + "%0.2X" % rand

 

amount_full = str(amount).rjust(12,'0')

amount_list = []

for i in range(0,6):

    amount_list.append(int(amount_full[i*2:(i*2+2)],16))

 

GENERATE_AC = [0x80, 0xA8, 0x00, 0x00, 0x11, 0x83, 0x0F] + UN + amount_list

GENERATE_AC.append(int(today.strftime("%y"),16))

GENERATE_AC.append(int(today.strftime("%m"),16))

GENERATE_AC.append(int(today.strftime("%d"),16))

GENERATE_AC = GENERATE_AC + [0x08, 0x40] # Currency - USD

GENERATE_AC = GENERATE_AC + [0x00] # end byte

 

Response = APDU(GENERATE_AC)

ATC = Response[Response.find("9F3602", 0)+6:Response.find("9F3602", 0)+10]

ARQC = Response[Response.find("9F2608", 0)+6:Response.find("9F2608", 0)+22]

PAN = Response[Response.find("570E", 0)+4:Response.find("570E", 0)+20]

AIP = Response[Response.find("8202", 0)+4:Response.find("8202", 0)+8]

 

AuthString = "amount="+str(amount)+"&trans_type=EMV&9f36="+ATC+"&9f26="+ARQC+"&82="+AIP+"&5a="+PAN+"&9f37="+UN_str+"&5f2a=0840"+"&9a="+today.strftime("%y%m%d")+"&9f02="+amount_full

 

#print AuthString

#print "https://www.paymentvillageprocessing.com/auth_host.php?"+AuthString

response = requests.get("https://www.paymentvillageprocessing.com/auth_host.php?"+AuthString, verify=True)

print "======================================="

print response.content.replace("<br>", "\r\n")


This code would work with any of our Paymentvillage.Org Credit Cards, as long as you have the right readers, such as SC3310.

Statistics

-        16 people made at least one transaction with the card

-        four people solved the first task

-        one person partially solved the second task

Task 0. Hacking SoftPOS APK.

 

PCI standard for SoftPOS - CPoC, stipulates how essential is execution environment control. Practical steps for this requirement include anti-debugging features like code obfuscation and environment and application attestation. Attestation could be done using various Android features and solutions. SafetyNet is one of the best integrity control features explicitly mentioned in CPoC. I swear, one day, we will implement SafetyNet or a similar framework in our SoftPOS, but this year our lazy DevOps team used a simple code for root detection:

Root detection

if ((!(new File("/system/bin/su")).exists()) &&

                 (!(new File("/system/xbin/su")).exists()) &&

                (!(new File("/sbin/su")).exists()) &&

                 (!(new File("/system/su")).exists()) &&

                         (!(new File("/system/bin/.ext/.su")).exists()) &&

                                    (!(new File("/system/usr/we-need-root/su-backup")).exists()) &&

                                            (!(new File("/system/xbin/mu")).exists()) )

         {intent = new Intent(this, NFCCardReading.class);

                     ***

As well as SSL Certificate Pinning:

<pin-set>

   <pin digest="SHA-256">yBHZ4rCAO4LBhxV03oWwrZeURO+gM4Mrcny3bkqHx+U=</pin>

</pin-set>

That would give us some superficial level of security: you can't launch the app on rooted devices where tools like Xposed SSLUnpinning will help to intercept traffic, and you can't easily implement MiTM attacks on unrooted devices. It will cause errors like this:

08-04 16:02:38.787 21908 21994 E AndroidRuntime: Caused by: javax.net.ssl.SSLHandshakeException: Pin verification failed

I can think of three easiest ways to bypass these checks:

- decompile APK to SMALI, change if-nez to if-eqz in the root checking section of NfcHome.smali and use SSLUnpinning (https://repo.xposed.info/module/mobi.acpm.sslunpinning) on a rooted device;

- rewrite Android manifest files by adding your certificate to the pin-set tag and arrange the MiTM attack using a non-rooted device;

- debug the URL to reconstruct card authentication requests, so there will be no need in the original APK.

Let's do the last trick - add a debug string to EmvParser$1.smali and run the app:

85:   const-string v4, "log-tag"
86:   invoke-static {v4, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
87:    invoke-static {v1}, Lorg/apache/commons/io/IOUtils;->toByteArray(Ljava/net/URL;)[B

When we can debug the process, it will be helpful to record all communication steps between the card and the POS and then the POS and the authorisation server. The options here:

- Proxmark3 passively sniffs all the ISO14443 packets of the communication.

- two PN532 readers acting as a bridge - my favourite method. If you want to try it yourself, use our GitHub project (https://github.com/a66at/NFCMiTM)

- two NFC mobile phones and a web server. I used open-source Android apps to create the bridge for some of our videos (https://www.forbes.com/video/6064363471001/). It's discreet, but when you have to use two NFC-equipped phones, one card and one more phone for the SoftPOS app, it becomes too much of a hassle.

- debug the requests and responses on the SoftPOS. The easiest way!

Fortunately, our "lazy SoftPOS developers" left debug on the app. So you can see all the communication steps using ADB:

ADB debug

08-03 19:42:40.416 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider: Terminal: 80 A8 00 00 11 83 0F 70 55 81 F9 00 00 00 00 01 00 22 08 03 08 40 00

08-03 19:42:40.418 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider: Response: 77 24 9F 36 02 00 41 9F 26 08 A2 DC 5F 69 F9 87 B0 86 82 02 00 40 57 0E 12 34 80 86 80 00 67 25 D2 40 42 01 12 3F 90 00

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider: resp:

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider: 77 24 -- Response Message Template Format 2

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:   9F 36 02 -- Application Transaction Counter (ATC)

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:            00 41 (BINARY)

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:   9F 26 08 -- Application Cryptogram

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:            A2 DC 5F 69 F9 87 B0 86 (BINARY)

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:   82 02 -- Application Interchange Profile

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:         00 40 (BINARY)

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:   57 0E -- Track 2 Equivalent Data08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:         12 34 80 86 80 00 67 25 D2 40 42 01 12 3F (BINARY)

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider: 90 00 -- Command successfully executed (OK)

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider: resp: Command successfully executed (OK)

08-03 19:42:40.421 31959 32203 D com.peerbits.creditCardNfcReader.utils.ResponseUtils: 2022-08-03 19:42:40.421 org.slf4j.impl.AndroidLoggerAdapter#log:55

08-03 19:42:40.421 31959 32203 D com.peerbits.creditCardNfcReader.utils.ResponseUtils:  Response Status <9000> : Command successfully executed (OK)

08-03 19:42:40.427 31959 32228 D log-tag : https://www.paymentvillageprocessing.com/auth_host.php?amount=100&trans_type=EMV&9f36=0041&9f26=A2DC5F69F987B086&82=0040&5a=1234808680006725&57=1234808680006725D2404201123F&9f37=705581F9&5f2a=0840&9a=220803&9f02=000000000100

If you want to understand each step of the payment, we recommend watching a video from Steven Murdoch about EMV tags.

And read our whitepaper where we explain transaction steps for Visa Contactless cards.

You can also see that developers didn't bother too much with security. All the parameters are passed in GET, there's no API authentication, and SoftPOS sends card details in plaintext during the authorisation request. Although having SSL and Certificate Pinning might be enough even to pass PCI attestation.

For contact cards, the communication steps were also displayed in the output by default. But the easiest way to find the URL was found by durcm. You would need to disable the Internet connection, and the complete URL would be disclosed in the exception:

Now we are equipped for solving actual card hacking tasks by simulating the Transaction Stream Fraud!

Task 1. Bypass the $10 limit for contactless payments.

The contactless limit is set in the app:

SMALI code

if-le v0, v1, :cond_2

invoke-virtual {p0, v2}, Lcom/peerbits/nfccardread/NfcHome;->findViewById(I)Landroid/view/View;

move-result-object v0

check-cast v0, Landroid/widget/TextView;

const-string v1, "100"

invoke-virtual {p1, v1}, Landroid/widget/EditText;->setText(Ljava/lang/CharSequence;)V

    const/16 p1, 0x64

iput p1, p0, Lcom/peerbits/nfccardread/NfcHome;->amount:I

const-string p1, "Price cannot be more than $10 as we do not accept PIN (yet)."

invoke-virtual {v0, p1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

goto :goto_0

Rebuilding the app and changing the limits would be the simplest solution. But we also have direct access to the transaction stream so let's try and use it. What does the transaction authorisation request look like for a $1 payment?

GET /auth_host.php?amount=100&trans_type=EMV&9f36=0041&9f26=A2DC5F69F987B086&82=0040&5a=1234808680006725&57=1234808680006725D2404201123F&9f37=705581F9&5f2a=0840&9a=220803&9f02=000000000100 HTTP/1.1
Host: www.paymentvillageprocessing.com

What immediately should catch your eye is the fact that the transaction amount appears in the request twice: first is the amount parameter, indicating how much the merchant charges the customer. The second is the EMV Amount Field (9f02) used for generating the cryptogram:

Our card requires the Amount and the Currency from SoftPOS to generate the cryptogram successfully.

Last year we showed that Payment giants like Visa, MasterCard and AMEX allow authorising transactions where these amount fields differ.

For example, I can request a cryptogram for $1,01, but I will use it to charge the card $50:

https://www.paymentvillageprocessing.com/auth_host.php?amount=5000&trans_type=EMV&9f36=0043&9f26=EA423B5F9415B7F9&82=0040&5a=1234808680006725&57=1234808680006725D2404201123F&9f37=92D667DA&5f2a=0840&9a=220803&9f02=000000000101 

Why is that possible? The short answer is "legacy". EMV didn't exist fifty years ago, and a magstripe transaction authorisation request looked similar to that:

Later, when EMV flow was integrated into existing payment schemes, Visa and MasterCard created "Field 55" - the field where all the EMV tags would be stored. In our payment scheme, Field 55 was replaced by nine EMV parameters sent separately:

9f36 (Application Transaction Counter), 9f26 (Transaction Cryptogram), 82 (AIP), 5a (PAN), 57 (Track2 Equivalent), 9f37 (UN), 5f2a (Transaction Currency), 9a (Date), 9f02 (Amount). That is how we ended up with two price fields in the authorisation request.

The right thing to do here will be to check the consistency between two Amount fields if EMV/NFC operations occur, but no one does that.

Five years ago, we found the same vulnerability in Apple Pay implementation when the authorised amount could be different from the charged amount (https://www.blackhat.com/docs/us-17/thursday/us-17-Yunusov-The-Future-Of-Applepwn-How-To-Save-Your-Money.pdf)

Because of the ambiguity of the Amount field in payment specification, it's possible to abuse transport or transit schemes for Apple, Google and Samsung Pay and to commit fraud.

Bottom line, our vulnerable payment system, like Visa or MasterCard, would charge you the price indicated in the amount field even if the presented cryptogram authorised a completely different price.

Four people solved that task.

A few participants (Boringdreams, durcm, Endycoffeeman) went further. They put a negative value in the amount field, like -50000, and that request made the USD account worth $500. This gave them all the $500 required for solving the next and final task. I had to fix the code urgently and published the hint that it was not the right direction for the challenge.

PS: It’s unlikely that such an attack could be found in the real world - 99% of the transactions go through Visa-Net, MasterCard-Net or similar, where operations with negative values would be rejected.

Task 2. Using one card, make total payments equivalent to 500 USD.

Step 1. After the first $100 was taken, processing response with a helpful "Card **** Not enough money!" error. Criminals can use this for balance-checking attacks.

Step 2. There's no more money on the USD account, but our payment system supports multiple currencies; otherwise, there would be no need in the 5f2a EMV Field (Transaction Currency). As well as EMV, we use ISO-4217 currency codes. What if we will put random currency there:

Here's valuable information - the payment system supports three currencies:

USD - 0840

EUR - 0978

MXN - 0484

And if we change the currency from USD to EUR, the transaction will be authorised!

Wait, how is that possible? Should the currency be part of the cryptogram? Because the card requests the currency field in the PDOL list, it doesn't mean that this field is one of the inputs for the cryptogram. We found that in 2017 when hacked Visa's most popular contactless cryptogram version, CVN17. It turned out that many fields requested by Visa cards are used only for on-card risk management steps and won't be checked during the transaction authorisation.

But if we will try to generate a cryptogram for another EUR payment, we will receive the "Sorry! This card doesn't support more than one transaction per month!" error. What to do with that? Let's try an infamous cryptogram replay attack. To remind you, a cryptogram always has two variables changing every transaction: ATC – a consecutive transaction counter, and UN – random 4 bytes generated by the terminal. But what if ATC, UN, and every other field are the same? That means that a cryptogram is valid! That means exactly the same cryptogram could be used to authorise more than one transaction. Many banks still don't check the ATC out of order and allow cryptogram replay attacks, but we see evidence that some banks learned a lesson and changed their rules to counteract the replay attacks.

It was possible to drain another 200 EUR from the card with the cryptogram replay attack.

Boringdreams was able to steal 100 from the EUR account using the original USD cryptogram.

Step 3. When you try to make an MXN transaction, you'll get another piece of information:

Sorry! These cards do not support EMV. Transaction type should be magstripe and parameters: track2, amount, currency.

So this is some sort of legacy – transactions in Mexican Pesos are allowed only with magstripe cards. Magstripe transactions don't have security features like cryptograms, so all information that the terminal sends is Track2 data. But how could we get track2 data if there's only EMV/NFC on the card? If you read our last research about abusing Track2 Equivalent, it will become obvious that some banks will accept security codes from the EMV/NFC data in magstripe transactions. Let's read Track2 Equivalent from the NFC card and compose the correct request:

08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:   57 0E -- Track 2 Equivalent Data08-03 19:42:40.419 31959 32203 D com.peerbits.creditCardNfcReader.utils.Provider:         12 34 80 86 80 00 67 25 D2 40 42 01 12 3F (BINARY)

https://www.paymentvillageprocessing.com/auth_host.php?amount=10000&trans_type=magstripe&track2=1234808680006725D2404201123F&currency=0484 

Repeating that request 41 times will allow us to drain another 4100MXN (~$200), which will make a total of $500 withdrawn from the card!

User Boringdreams tried to apply this attack but unsuccessfully. Although it didn't stop him from getting first place in our challenge.