Ninja Cards
Intro
At the 2023 edition of hexacon there were various CTF challenges, one of which organized at the venue by the event’s organizer, Synacktiv. It involved a smartcard reader attached to small computer and a screen, and a stack of smartcards. The goal of the challenge is to somehow authenticate as admin, which should pop up a specific message on the screen that shows “Waiting for NFC tag…” in Figure 1.
We grabbed one card from the stack on the bottom left and read it out using MIFARE Classic Tool. Fortunately it used one of the standard keys included in the application. The dump showed some data in sectors 1, 2 and 3 as shown in Figure 2.
The sectors 1 and 3 contain some interesting strings as shown in Listing 1.
The URL brings us to a zip archive containing files shown in Listing 2. This is a subset of the code running on the small computer, handling the data read from the card’s sectors.
The README.md
explains the challenge:
Dear Hexacon guest,
If you read these lines, you probably visited the Synacktiv stand
and some ninjas might have briefed you.
Otherwise, the goal is to exploit vulnerabilities inside the NFC
reader to authenticate successfully as admin.
The sources of the challenge are available into the `src/` directory.
NB: Some files such as `ipc.c` and `nfc.C` which implement some
used functions are not part of the archive, this is intended, as
they are not needed to complete the challenge.
Have fun ! o/
The main
(Listing 3) of the application, initializes an admin token stored in the filesystem of the machine with get_admin_token
and then enters a reading loop. When presented a card, the notify
calls update messages on the screen.
Bug 1
read_card
(Listing 4) moves data from the card’s sectors into the global context
. This function contains the first potential vulnerability, as it uses strcpy
to move the content from the third sector. This function will keep reading data from the card until a 0-byte is encountered.
The data
field in the context
structure has space for the amount of data (0x30 bytes) of sector 3. context
is stored in the globals together with the admin_token
, key
and iterations
(see Listing 3). So by overflowing the data
field we can overwrite key
and iterations
. iterations
determines the amount of hash iterations are performed in the PKCS5_PKBDF2_HMAC
key derivation step in derive_key
(see Listing 5).
If we can obtain the admin_token
, we can create a card where the username
is hexacon_admin
and the token
is admin_token
. This should lead us to the authenticated admin screen.
In a normal run iterations
is set to 1000
(see Listing 3), which is not very high for a proper key derivation system. So it could be that admin_token
is some brute-forcible string that could be found in a wordlist, as the salt
input is set to NULL
. With the overflow we could change the amount of iterations to 1
, so then just a single SHA256
hash is calculated over the password. However it is more likely that the designer of the challenge just put a random 0x20
byte string in there. In this case even bruteforcing a single 256-bit hash is not feasible.
We considered setting iterations
to 0
, so that perhaps no hash is calculated at all. This case is normaly caught by the OpenSSL implementation that was likely used, looking at the function signature. So this would not work either.
In the compute_token
function (Listing 6), the key[]
array, resulting from derive_key
function, is used to perform authentication by XORing it with the SHA256
hash of the provided username
, stored in sector 1 of the card.
The unmodified card passes the token checks as guest, as shown on the screen, so we know that the token in sector 2 matches the username in sector 3. Therefore, we can calculate the value of key
by simply XORing it with the hash of the username
from sector 1 as shown below.
We can now forge tokens for any username
, including hexacon_admin
, which should pop up the notify(ADMIN_AUTH)
. However, if we set username
like this, the check_token
function (Listing 6) will take a different path where it compares against the admin_token
, which we still don’t know, and also cannot reach with the strcpy
overflow.
Bug 2
Then we noticed the application reads a second time from the card while in the non-admin branch of check_token
, which will refill the global context
structure. This means the application contains a TOCTOU type vulnerability where some checks are performed on one copy of the data, and then performs actions based on another copy of the data. Namely, the check on username
to reach the right branch is performed on the first copy from the card, while the check in the main
loop, which determines which final message we reach, is performed on the second read. So if we can switch the username
portion of the card’s data fast enough, we can reach the right branch in check_token
while setting the username to hexacon_admin
and triggering the notify(ADMIN_AUTH)
. With something like a Proxmark or a Flipper Zero it’s probably possible to switch between card data fast enough. Alternatively, we could simply use two cards, write the different username
s with matching token
s, and switch them in between reads, if we are fast enough. However, our ninja skills were fast enough :(
Ninja skills
Recall the iterations
variable that we could overwrite with the overflow. This determines the amount of times the pseudorandom function (HMAC
in this case) is applied on the potentially weak password input in the PBKDF2_HMAC
function. Setting this value really high, rather than really low, also means more computation time is needed in the application reading the cards. Furthermore, this calculation is performed between the two card reads, so this might give us enough time to manually switch between two cards.
int main(int argc, char *argv[]) {
...
for (;;) {
...
// v- first card read
(&context);
read_card
// v- PBKDF in here
if (derive_key(admin_token, iterations, key) == 0) {
...
}
// v- second card read in here
= check_token(&context, admin_token, key);
status_t status ...
if (status == VALID_TOKEN) {
if (!strcmp(context.username, ADMIN_USERNAME)) {
(ADMIN_AUTH);
notify} else {
(GUEST_AUTH);
notify}
}
}
}
We now need to set up two cards in the following way to combine both the buffer overflow and the TOCTOU bugs:
- One card will contain any
username
string in sector 1 that is nothexacon_admin
. This card needs to trigger the overflow to overwriteiterations
. By doing this, it will also overwritekey[]
, so we need to replace thetoken
in sector 2 such that it matches with the overwrittenkey[]
. Due to the offsets, the last 0x10 bytes of sector 3 will end up inkey[]
which contain some special values. The functionread_nfc_tag
fromnfc.h
warns us that “the NFC tag A key MUST be ”FFFFFFFFFFFF””, which refers to to the light green portion of Figure 2. We just left all those bytes as they were, so we recalculated thetoken
as shown in Listing 7, i.e.token = sha256(username) ^ key
. The first 0x10 bytes of sector 4 will be the second half of the key, we just filled this with 0xff-bytes. Then the following 4 bytes of sector 4 can be used to control theiterations
variable to something large like0xffffffff
. - The second card will contain the
hexacon_admin
string in theusername
’s sector 1. This card will need also a valid token matching the previously overwrittenkey[]
, which is generated following the sametoken = sha256(username) ^ key
calculation.
By presenting first the guest card, and then swapping to the admin card while the PBKDF2_HMAC
calculation is working, we should land on the notify(ADMIN_AUTH)
page.
The Python script in Listing 8 generates the data for both cards’ sectors. Listing 9 shows the data for the relevant card sectors.
Success!
Even with our slow ninja skills, we now are fast enough to see the success screen!
We were not fast enough to win the first prize, but we were fast enough to win the second prize, a very cool o.mg.lol cable! Great challenge, very creative way of triggering and using a buffer overflow!