In Blog

This is the fourth part of the story of the smart card track at NSEC 2013. You can see the first three parts: part 1, part 2 and part 3.

Now, I had 3 of the 5 unknown components required to create the encryption keys mentioned in the smart card web page. I needed to find the Ben Pyr app data and the xored access logs.

I decided to look for Ben Pyr app data, whatever that was. The third applet on the card was the Ben Pyr applet, the text mentioned that it was put there to validate that the card belonged to the company or something. The text mentioned that to do that, it encrypted some secret data with a secret key and that you could validate it by sending back the ciphertext.

This applet had two functions, ENCRYPT (which required 0 bytes in input as visible in the HEX header indicated next to the command) and DECRYPT (which required 16 bytes of data). I first issued ENCRYPT and received no error and a 16 byte ciphertext. I then issued DECRYPT with the ciphertext and received no error and… nothing else. Usually when decrypting, the goal is to get something back, but in this case, the goal was simply to validate that the data you sent was good.

At this point, I knew that we would have to play with the DECRYPT function because it was the only one accepting input. I talked to Daniel Boteanu because he’s our expert in these kinds of attack. After I told him what I had, we decided to change the last byte of input to make sure we got an error and we did. Then, Daniel told me to change the first byte of input (and leave the rest of the ciphertext good). This time, we got no error. Since we could change the ciphertext and not get an error, we got the idea that the application was only checking for the right padding.

Then, Daniel told me that it looked like a PKCS5 Padding oracle attack. I immediately googled it and found a blog post that explained it well https://www.skullsecurity.org.

This attack is based on how PKCS5 padding works. In PKCS5 padding, if the last block has 7 bytes, you append a single byte with the value 1. If it has 6 bytes, you append 2 bytes with the value 2 and so on. If the last block has 8 bytes, you append a full block containing 8 bytes with the value 8. Therefore, when decrypting, you can always read the last byte of the decrypted plaintext and remove the number of bytes indicated in this byte. That way you recover a message of the same length as the one you encrypted.

The attack is also based on how the CBC mode of chaining block works. When decrypting in CBC mode, you decrypt the ciphertext with the key, then you XOR the result with the ciphertext of the previous block in order to get the plaintext (See this Wikipedia page for more details: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation).

The key is always secret, so normally you can never do the decryption step properly and you get random garbage as the supposed plaintext. But, this is where the oracle comes in. The oracle is a program which decrypts the ciphertext with the right key and tells you whether the padding is right. We can now see that the oracle is doing the hard work for us and decrypting the ciphertext. Let’s look at it in more details. When decrypting the last block, the oracle will decrypt the plain text with the right key, then XOR it with the previous blocks ciphertext and then check whether the padding is right.

Now, if we send two blocks of ciphertext to the oracle, the first block is all 0’s and the second block is the actual last block of ciphertext. The oracle decrypts the first block of ciphertext (all 0’s) and gets some garbage, then it decrypts the second block of ciphertext (at that point this result is the plaintext XORED with the actual previous block of ciphertext), then it will XOR it with the previous block of ciphertext that we sent it (all 0’s). Then it will check the padding.

We see that the oracle is doing this:

Now the simplest padding combination to get right is to have a single byte equal to 1 at the end. So, we increment the last byte of our fake previous block ciphertext until the oracle tells us that the padding is right. When this happens, we know the last byte of our fake cipher text, we know the last byte of the real previous block ciphertext (we have all the ciphertext) and we know the last byte of those 2 XORED with the plaintext (its equal to 1). So we get the last byte of plaintext by XORING 1 with the two ciphertexts!

Now, we know the last byte of plaintext, so we can now set the last byte of our fake ciphertext so that it gives 2 and increment the next to last byte in our fake ciphertext until the oracle tells us we are right once again! We can iteratively get all the bytes of the plaintext!

One tricky part remaining is what to do with the first block since we don’t know what is the IV (equivalent to the previous block of ciphertext). In this case, I guessed that the IV was all 0’s and it turned out to be right.

I coded this entire algorithm in java using the smartcard DECRYPT function as my oracle and the result of the ENCRYPT function as my good ciphertext.

Here is the code as it stood when I finished the attack:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;

import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.smartcardio.TerminalFactory;

public class Oracle {
	static byte[] selectApdu = new byte[] {0x00, (byte) 0xA4, 0x04, 0x00, 0x07, 0x42, 0x45, 0x4E, 0x50, 0x59, 0x52, 0x31};
	static byte[] decryptApdu = new byte[] {0x00, (byte) 0x88, 0x02, 0x00, 0x10};
	static byte[] iv = new byte[8];
	static byte[] cipherBlock1 = {(byte) 0xD5, (byte) 0xE1, 0x2B, 0x2B, (byte) 0x8B, 0x2D, 0x79, 0x25};
	static byte[] cipherBlock2 = {0x01, (byte) 0xBC, 0x34, 0x37, 0x68, 0x10, (byte) 0xD4, (byte) 0xBB};
	static byte[] plain2 = new byte[] {0x72, 0x53, 0x75, 0x63, 0x6B, 0x73, 0x02, 0x02};
	static String value = "4F6E696F6E4F7461725375636B73";
	static String converted = "OnionOtarSucks";

	public static void main(String[] args) throws Exception {

		// show the list of available terminals
        javax.smartcardio.TerminalFactory factory = TerminalFactory.getDefault();
        List<CardTerminal> terminals = factory.terminals().list();
        System.out.println("Terminals: " + terminals);
        // get the first terminal
        CardTerminal terminal = terminals.get(0);
        // establish a connection with the card
        Card card = terminal.connect("T=1");
        System.out.println("card: " + card);
        CardChannel channel = card.getBasicChannel();

        ResponseAPDU r = channel.transmit(new CommandAPDU(selectApdu));
        System.out.println("response: " + bytesToHex(r.getBytes()));

        byte[] newCipher = new byte[8];
        byte[] plainText = new byte[8];

        for(int k = 1; k <= 8; ++k) {
        	boolean success = false;
        	for(int i = 1; i < k; ++i) {
        		// We correct the padding for all lower bytes
        		newCipher[8-i] = (byte)(k ^ plainText[8-i] ^ iv[8-i]);
        	}
        	while(!success) {
            	success = tryDecrypt(channel, cipherBlock1, newCipher);
            	if(!success) {
            		newCipher[8-k]++;
            	}
        	}
        	// We found the right padding
        	plainText[8-k] = (byte) (k ^ newCipher[8-k] ^ iv[8-k]);
        }

        System.out.println("response: " + bytesToHex(plainText));

        // disconnect
        card.disconnect(false);
	}

	private static boolean tryDecrypt(CardChannel channel, byte[] goodBlock, byte[] newCipher) throws Exception {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        os.write(decryptApdu);
        os.write(newCipher);
        os.write(goodBlock);

        ResponseAPDU r = channel.transmit(new CommandAPDU(os.toByteArray()));
        byte[] resp = r.getBytes();
        System.out.println("response: " + bytesToHex(r.getBytes()));
        return resp[0] == (byte)0x90 && resp[1] == 00;
	}

	public static String bytesToHex(byte[] bytes) {
	    final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
	    char[] hexChars = new char[bytes.length * 2];
	    int v;
	    for ( int j = 0; j < bytes.length; j++ ) {
	        v = bytes[j] & 0xFF;
	        hexChars[j * 2] = hexArray[v >>> 4];
	        hexChars[j * 2 + 1] = hexArray[v & 0x0F];
	    }
	    return new String(hexChars);
	}
}

I also cleaned up the code a little to separate the algorithm from the oracle:

An interface to define an Oracle:

package com.okiok.comp;

/**
 * This interface represents the oracle which tells whether after decipherment, the PKCS5 padding is correct
 * @author evigeant
 */
public interface DecipherPaddingOracle {

	/**
	 * @param buffer The buffer to decipher (length should be a multiple of block length)
	 * @return True when the padding after decipherment is good, false otherwise
	 * @throws Exception if there was any error unrelated to padding
	 */
	boolean decipher(byte[] buffer) throws Exception;

	/**
	 * @return The block length of the block cipher used
	 */
	int getBlockLength();
}

The solver class:

package com.okiok.comp;
import java.nio.ByteBuffer;

/**
 * Perform Oracle padding attack
 * @author evigeant
 *
 */
public class OraclePaddingSolver {

	/**
	 * Perform the Oracle Padding attack. In order for the attack to be successful, the block cipher must use CBC to chain blocks and pad using PKCS5.
	 *
	 * @param iv The iv used in the encryption. If this value is wrong, the first block of the plain text will be wrong.
	 * @param validCipherText The valid ciphertext from which we attempt to recover the plaintext. Length should be a multiple of the block length, otherwise there was an error in padding...
	 * @param oracle The oracle which tells us if the padding is right or wrong after decryption
	 * @return The plaintext recovered
	 * @throws Exception
	 */
	public static byte[] findPlainText(byte[] iv, byte[] validCipherText, DecipherPaddingOracle oracle) throws Exception {
		// First break into blocks and attack each block separately
		int blockSize = oracle.getBlockLength();
		ByteBuffer outputBuffer = ByteBuffer.allocate(validCipherText.length);
		for(int i = 0; i < validCipherText.length / blockSize; ++i) {
			byte[] currentBlock = new byte[blockSize];
			System.arraycopy(validCipherText, blockSize*i, currentBlock, 0, blockSize);
			byte[] previousBlock = new byte[blockSize];
			if(i > 0) {
				System.arraycopy(validCipherText, blockSize*(i-1), previousBlock, 0, blockSize);
			} else {
				previousBlock = iv;
			}
			byte[] plainBlock = findBlockPlainText(previousBlock, currentBlock, oracle);
			outputBuffer.put(plainBlock);
		}
		return outputBuffer.array();
	}

	private static byte[] findBlockPlainText(byte[] previousBlockCipherText, byte[] currentBlockCipherText, DecipherPaddingOracle oracle) throws Exception {
		int blockSize = oracle.getBlockLength();
        byte[] plainText = new byte[blockSize]; // This is the plaintext we have worked out so far
        byte[] newCipher = new byte[blockSize]; // This is a fake block of ciphertext we play with

        // We proceed one character at a time in the block
        for(int k = 1; k <= blockSize; ++k) {
    		// We correct the padding for all lower bytes (bytes that we have already worked out)
        	// If we are looking for the first byte, this will do nothing
        	for(int i = 1; i < k; ++i) {
        		newCipher[blockSize-i] = (byte)(k ^ plainText[blockSize-i] ^ previousBlockCipherText[blockSize-i]);
        	}

        	boolean success = false;
    		ByteBuffer buf = ByteBuffer.allocate(2*blockSize);
        	for(int tries=newCipher[blockSize-k]; tries < 256 && !success; ++tries) {
        		buf.clear();
        		buf.put(newCipher);
        		buf.put(currentBlockCipherText);
        		// The oracle does not give us the result of the decryption, but it tells us if the decryption failed because of a padding error. (Padding errors are the usual suspect when decryption fails)
        		// The oracle will decrypt the second block using the right key, it will therefore get: [plaintext] XOR [REAL prev. block ciphertext] and because it is CBC, it will then XOR [our FAKE ciphertext]
            	success = oracle.decipher(buf.array());
            	if(!success) {
            		newCipher[blockSize-k]++;
            	}
        	}
        	if(success) {
	        	// We found the right padding (success = true)
	    		// It will then check the padding of the block since it is the last block. PKCS5 pads in the following way: XXXXXXX1, XXXXXX22, XXXXX333, etc. So the number of bytes added as padding is the number used in padding.
	    		// Also, if the plaintext is a multiple of the block size, it will add one full block (ex: 88888888).
	    		// So if the oracle doesn't report an error, we know that (for character k): PLAIN[k] XOR REAL_PREVIOUS_CIPHER[k] XOR FAKE[k] = k
	        	plainText[blockSize-k] = (byte) (k ^ newCipher[blockSize-k] ^ previousBlockCipherText[blockSize-k]);
        	} else if(k > 1) {
            	// WARNING: There is a possibility that while looking for the right padding of the FIRST character (ex: XXXXXXX1), by a bad luck, we find a valid padding of the form (XXXXXX22 or XXXXX333, etc.), this is 256x less likely but it could happen.
            	// In that case, we would need to continue the search for the first character passed the problem and find the right value that yields (XXXXXXX1).
        		newCipher[blockSize-k] = 0; // We reset the current character
        		newCipher[blockSize-k+1]++; // We increment the previous character
        		k-=2; // Go back to the previous character
        	} else {
        		throw new Exception("There is a problem with the oracle, it returned false for all 256 characters");
        	}
        }
		return plainText;
	}
}

Finally an example of using the solver to solve the challenge from the competition:

package com.okiok.comp;
import java.io.ByteArrayOutputStream;
import java.util.List;

import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.smartcardio.TerminalFactory;

/**
 * Perform Oracle padding attack
 *
 * @author evigeant
 */
public class OraclePaddingExample {
	static byte[] iv = new byte[8]; // Guessed at 0
	// This was retrieved from the smartcard by issuing the ENCRYPT command (without any data)
	static byte[] cipherBlock = {(byte) 0xD5, (byte) 0xE1, 0x2B, 0x2B, (byte) 0x8B, 0x2D, 0x79, 0x25, 0x01, (byte) 0xBC, 0x34, 0x37, 0x68, 0x10, (byte) 0xD4, (byte) 0xBB};

	public static void main(String[] args) throws Exception {
		NsecBenPyrApp oracle = new NsecBenPyrApp();
		byte[] plaintext = OraclePaddingSolver.findPlainText(iv, cipherBlock, oracle);

        System.out.println("response: " + bytesToHex(plaintext));
        oracle.close();
	}

	private static String bytesToHex(byte[] bytes) {
	    final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
	    char[] hexChars = new char[bytes.length * 2];
	    int v;
	    for ( int j = 0; j < bytes.length; j++ ) {
	        v = bytes[j] & 0xFF;
	        hexChars[j * 2] = hexArray[v >>> 4];
	        hexChars[j * 2 + 1] = hexArray[v & 0x0F];
	    }
	    return new String(hexChars);
	}

	/**
	 * For NSEC 2013 Smartcard track. This class selects the BENPYR app on the smart card and sends a decrypt command.
	 * The card returns an error when padding is wrong and returns success (0x9000) when the padding is right.
	 *
	 * @author evigeant
	 */
	private static class NsecBenPyrApp implements DecipherPaddingOracle {
		static byte[] selectApdu = new byte[] {0x00, (byte) 0xA4, 0x04, 0x00, 0x07, 0x42, 0x45, 0x4E, 0x50, 0x59, 0x52, 0x31};
		static byte[] decryptApdu = new byte[] {0x00, (byte) 0x88, 0x02, 0x00, 0x10};

		private Card card;
		private CardChannel channel;

		public NsecBenPyrApp() throws Exception {
			// show the list of available terminals
	        javax.smartcardio.TerminalFactory factory = TerminalFactory.getDefault();
	        List<CardTerminal> terminals = factory.terminals().list();
	        System.out.println("Terminals: " + terminals);
	        // get the first terminal
	        CardTerminal terminal = terminals.get(0);
	        // establish a connection with the card
	        card = terminal.connect("T=1");
	        System.out.println("card: " + card);
	        channel = card.getBasicChannel();

	        ResponseAPDU r = channel.transmit(new CommandAPDU(selectApdu));
	        System.out.println("response: " + bytesToHex(r.getBytes()));
		}

		@Override
		public boolean decipher(byte[] buffer) throws Exception {
	        ByteArrayOutputStream os = new ByteArrayOutputStream();
	        os.write(decryptApdu);
	        os.write(buffer);

	        ResponseAPDU r = channel.transmit(new CommandAPDU(os.toByteArray()));
	        byte[] resp = r.getBytes();
	        System.out.println("response: " + bytesToHex(r.getBytes()));
	        return resp[0] == (byte)0x90 && resp[1] == 00;
		}

		@Override
		public int getBlockLength() {
			// USED 3DES CBC
			return 8;
		}

		public void close() throws CardException {
			card.disconnect(false);
		}
	}
}

When I ran the code and saw the bytes coming out one by one (because of the success messages from the smartcard) I was really happy. At that point, I was so happy to have cracked this, I felt that I had accomplished what I had come for. I had learned a new attack and successfully exploited it in practice.

This flag turned out to be worth a lot of points, so I was contributing to the team effort and was rewarded for all the time I put in.

There is more coming in the next part.

Leave a Comment

Start typing and press Enter to search