This example shows how to recover the key from the whitebox qualifier challenge of Rhme 2017. The challenge can be completely solved using the SideChannelMarvels framework as described in this Deadpool writeup. Here we do it somewhat differently.
We will use the wrapper from Deadpool to trace the whitebox binary with Intel Pin. For recovery, we will use Jlsca to illustrate trace pre-processing techniques that it offers. For this toy binary, the effect of these techniques is not so pronounced, however it is significant for more serious challenges.
Before computing the correlation, we perform pre-processing on the traces to automatically remove samples that are irrelevant for the analysis. Such point-of-interest selection drastically reduces the length of traces without the visual inspection of the trace graph and manual filter configuration. As you can see from the log lines staring with Reduction
, what remains are about 20 bits per key byte. This is what goes into correlation computation. As a result, the total time of the attack (and the amount of human input) is reduced.
The detailed description of these pre-procesing techniques is available in https://eprint.iacr.org/2018/095
The correlation part of the attack is the same as in Daredevil. The output of the pre-processing could be fed out to Daredevil. However, the point selection is different per key byte, so we would need to script 16 separate Daredevil runs with different tracesets. You can do it as an exercise though.
In case you do not feel like tracing the binary yourself, tracesets for analysis are available alongside this notebook in rhme2017-qual-wb-traces.tar.bz2.
We do it in a standard Deadpool way based on the examples therein. The acquisition script is leaving default filters on acquired ranges.
#!/usr/bin/env python
import sys
sys.path.insert(0, '../../')
from deadpool_dca import *
def processinput(iblock, blocksize):
return (None, ['--stdin < <(echo %0*x|xxd -r -p)' % (2*blocksize, iblock)])
def processoutput(output, blocksize):
return int(''.join([x for x in output.split(' ')]), 16)
T=TracerPIN('./whitebox', processinput, processoutput, ARCH.amd64, blocksize=16, shell=True)
T.run(100)
bin2trs(None, None, False) # get the bit-unpacked trs, keeping the originals
bin2daredevil() # get the daredevil "split binary", erasing the originals
We execute this script in the environment provided by the Orka docker image refreshed to the latest state. From several output files, for further steps we need mem_addr1_rw1_100_42808.trace
and mem_addr1_rw1_100_42808.input
. Other memory ranges can be analysed in the same manner.
Though Jlsca accepts the "split binary" format directly, we will convert the traces to bit-packed representation and save it as trs. For the short traces of this example it hardly matters, so just as an illustration.
Due to the current limitations of the converter we add only the input. This is enough for the attack but we will not be able to verify the key. For this challenge, the key will be distinguishable by its entropy, but in general the converter deserves improvement. :)
Deadpool's bin2trs
converter from deadpool_dca.py
can also be used (see the tracing script above), it just does not pack the bits. As an excercise, you can run the attack below on the mem_addr1_rw1_100_42808.trs
and see what happens.
In [4]:
using Jlsca.Trs
filename = "rhme2017-qual-wb-traces/mem_addr1_rw1_100_42808_bitpacked.trs"
if !isfile(filename)
# the true parameter in the end tells the converter to pack the bits
splitbin2trs("rhme2017-qual-wb-traces/mem_addr1_rw1_100_42808.input", 16, "rhme2017-qual-wb-traces/mem_addr1_rw1_100_42808.trace", 42808, UInt8, 100, true)
run(`mv output_UInt8_100t.trs rhme2017-qual-wb-traces/mem_addr1_rw1_100_42808_bitpacked.trs`)
end
In [5]:
using Jlsca.Sca
using Jlsca.Trs
using Jlsca.Aes
# attack configuration
attack = AesSboxAttack() # attacking AES S-box
attack.keyLength = KL128 # attacking AES-128
attack.mode = CIPHER # encryption (INVCIPHER would have been for decryption)
attack.direction = FORWARD # attacking from input
analysis = CPA() # use correlation as a distinguisher
analysis.leakages = [Bit(i) for i in 0:7] # absolute-sum DPA with bitwise "leakages"
params = DpaAttack(attack, analysis) # tie the attack and analysis together
params.dataOffset = 1 # data starts from the very first byte (remember Julia is 1-based)
# pre-processing setup
trs = InspectorTrace(filename) # open traceset with efficient readout of packed bits
addSamplePass(trs, BitPass()) # add bit-unpacking pass over samples
params.analysis.postProcessor = CondReduce
# TODO: expalin the difference between a sample pass and a post-processor in Jlsca readme
# attack using all available traces
rankData = sca(trs, params, 1, length(trs))
key = getKey(params, rankData)
Out[5]:
As said before, we did not include the output into the traceset. But apparently this key has entropy low enough to see that it is the right one. :)
In [6]:
print(String(key))