Fault 201 Tutorials: Has Anyone Recently Reproduced the AES/RSA Fault Attacks on Husky?

Hi everyone,

Over the past few days, I’ve been trying to run the fault injection tutorials from the ChipWhisperer GitHub Jupyter notebooks. I started with the AES skip attack tutorial (Fault 1.1B), but I have not been able to reproduce the expected results on my setup. I also noticed that some parts of the code appear questionable—for example, the “glitched” ciphertext seems to correspond only to the encryption of an all-zero plaintext.

While searching through previous discussions, I found comments suggesting that these experiments can be highly dependent on compiler versions and build settings, and that some of the tutorials may be outdated.

I also reviewed the remaining notebooks in the Fault 201 series, but many of them seem quite old. Therefore, I was wondering whether anyone has successfully run these fault attack tutorials recently, particularly the AES or RSA examples. If so, which projects would you recommend as the most practical to reproduce today without requiring extensive modifications?

Our current hardware setup consists of a ChipWhisperer Husky/Husky Plus with STM32F3, STM32F4, and SAM4S target boards.

Any advice or recent experience would be greatly appreciated. Thanks for your time!

Have you done the fault101 series? If not, do start there.

We have not updated the fault201 tutorials specifically for Husky. scope.glitch.width and scope.glitch.offset are what need to change, and finding good settings for that through the fault101 series should lead you in the right direction.

The other variable is scope.glitch.ext_offset (which clock cycle to glitch on), and this is where the target and the compiler can move things around.

Hi, thanks for the reply.

We first completed the Fault 101 counter example and were able to find multiple glitch points (width, offset, ext_offset) that successfully affected the target execution. We then reused one of these glitch configurations in the Fault 201 AES skip attack tutorial (1.1B).

Before running the experiment, we modified some of the course code. In the current notebook, gold_ct appears to be computed only for an all-zero plaintext. Later, the notebook compares faulty encryptions of plaintexts 1 and 2 against this same reference ciphertext, which did not seem correct to us. Therefore, we modified the code so that a separate gold_ct is computed for each plaintext before fault injection.

We also followed the notebook instructions and added volatile to the relevant variable(s) in TinyAES (aes.c). Using the glitch parameters obtained from Fault 101, we were able to observe faults, indicating that the glitch is affecting execution.

However, the later steps still fail. When encrypting plaintexts 1 and 2 under fault injection, the resulting faulty ciphertext is always identical, regardless of the plaintext. Because of this, the differential fault analysis stage cannot recover the key as expected.

We are not sure where the issue originates. Could our modifications to the notebook logic be incorrect? Or is it possible that the compiler is generating code that differs significantly from what the tutorial expects?

Has anyone recently reproduced this attack on Husky/Husky Plus with STM32F3/F4 or SAM4S targets and observed similar behavior?

Thanks again for your help.

I think you are right that gold_ct should be changed for plaintexts 1 and 2. I think that the notebook can still work as-is if the glitch has a very high success rate (i.e. it doesn’t matter that we’re comparing against the wrong gold_ct because we are in fact getting a corrupted ciphertext).

This suggests that you are not glitching at the correct time, and so the glitch is not having the intended effect. What is the constant faulty ciphertext that you get?

Hi, thanks for your reply.

Here is the fault attack part, excluding the normal setup code. Our hardware setup is Husky Plus + CW312 board + STM32F4 target. For compiling, the platform I select is CW308_STM32F4 and ver is SS_VER_1_1.

First, we changed gold_ct into a function, so that we can compute a separate reference ciphertext for each plaintext. In our test, we generated three reference ciphertexts for plaintexts 0, 1, and 2.

scope.clock.clkgen_src = "system"
scope.clock.adc_mul = 1
fixed_key = bytearray([0]*16)
print(fixed_key)

def get_gold_ct(pt):
    reboot_flush()

    # set fixed AES key
    target.simpleserial_write('k', fixed_key)
    target.simpleserial_wait_ack()

    scope.arm()
    target.simpleserial_write('p', pt)

    ret = scope.capture()
    wave = scope.get_last_trace()
    if ret:
        raise RuntimeError("Capture timeout when generating golden ciphertext")

    output = target.simpleserial_read_witherrors('r', 16)
    if not output['valid']:
        raise RuntimeError("Invalid response when generating golden ciphertext")

    return bytearray(output['payload']),wave

pt0 = bytearray([0]*16)
pt1 = bytearray([1]*16)
pt2 = bytearray([2]*16)

gold_ct0, wave0 = get_gold_ct(pt0)
gold_ct1, wave1 = get_gold_ct(pt1)
gold_ct2, wave2 = get_gold_ct(pt2)

print("gold_ct0 =", gold_ct0.hex())
print("gold_ct1 =", gold_ct1.hex())
print("gold_ct2 =", gold_ct2.hex())
print("key      =", fixed_key.hex())

The output is:

bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
gold_ct0 = 66e94bd4ef8a2c3b884cfa59ca342b2e
gold_ct1 = e14d5d0ee27715df08b4152ba23da8e0
gold_ct2 = 5eba73f89142c54880f68594373c5c37
key      = 00000000000000000000000000000000

Then we use this setting obtained from fault101 for glitch_loc, width, offset, and ext_offset

if scope._is_husky:
    scope.glitch.enabled = True

scope.glitch.clk_src = "pll"
scope.glitch.output = "clock_xor"
scope.glitch.trigger_src = "ext_single"

scope.glitch.repeat = 1
scope.io.hs2 = "glitch"

glitch_loc = range(0, 40)
scope.glitch.width      = 300
scope.glitch.offset     = 100
scope.glitch.ext_offset = 0

I follow the original main loop idea for 0 except use a new gold_ct0:

wave = None
logging.getLogger().setLevel(logging.ERROR)
reboot_flush()
for i in glitch_loc:
    scope.adc.timeout = 0.2
    scope.glitch.ext_offset = i
    ack = None
    while ack is None:
        target.simpleserial_write('k', fixed_key)
        ack = target.simpleserial_wait_ack()
        if ack is None:
            reboot_flush()
            time.sleep(0.1)

    scope.arm()

    pt = bytearray([0]*16)
    target.simpleserial_write('p', pt)
    ret = scope.capture()
    if ret:
        reboot_flush() #bad if we accidentally didn't have this work
        time.sleep(0.1)
        print("timed out!")
        continue
    output = target.simpleserial_read_witherrors('r', 16, glitch_timeout = 1)
    if output['valid']:
        if output['payload'] != gold_ct0:
            print("Glitched at {}".format(i))
            wave = scope.get_last_trace()
            break
    else:
        reboot_flush()

It shows a successful glitch and by output it shows:

glitched_ct0 = bytearray(output['payload'])
print(glitched_ct0.hex())
print(gold_ct0.hex())

b5044600f0d5ff00f042fa204600f0ce
66e94bd4ef8a2c3b884cfa59ca342b2e

Then I reuse the loop by replacing plaintext as 1 and gold_ct1,

wave = None
logging.getLogger().setLevel(logging.ERROR)
reboot_flush()
while True:
    scope.adc.timeout = 0.2
    scope.glitch.ext_offset = i
    ack = None
    while ack is None:
        target.simpleserial_write('k', fixed_key)
        ack = target.simpleserial_wait_ack()
        if ack is None:
            reboot_flush()
            time.sleep(0.1)

    scope.arm()

    pt = bytearray([2]*16)
    target.simpleserial_write('p', pt)
    ret = scope.capture()
    if ret:
        reboot_flush() #bad if we accidentally didn't have this work
        time.sleep(0.1)
        print("timed out!")
        continue
    output = target.simpleserial_read_witherrors('r', 16, glitch_timeout = 1)
    if output['valid']:
        if output['payload'] != gold_ct2:
            print("Glitched at {}".format(i))
            wave = scope.get_last_trace()
            break
    else:
        reboot_flush()

However, now the incorrect parts appear, as for 1, here is the fault output and correct ones:

glitched_ct2 = bytearray(output['payload'])
print(glitched_ct2.hex())
print(gold_ct2.hex())

b5044600f0d5ff00f042fa204600f0ce
3a69dc4d3eec2c4bb025b8ef40d1259e

This is incorrect, as for faulty encryption of 0 and encryption of 1 is the same. I consult GPT it suggests that this one looks not like a AES encryption but more like a error code.

P.S. What I successfully run up to now is 2.1 of RSA fault attack which shows True in the end.

As I said it looks like you might be glitching at the wrong time. Why did you change glitch_loc to range(0, 40)? The correct range will depend on the target, but using a Husky instead of CW-lite has no bearing on this.

Further, if you follow the explanation of what this attack is trying to do, you can calculate the expected glitched ciphertext for a given plaintext; use this to confirm whether you are on the right track.

Hi,

I followed your suggestion and changed glitch_loc to around 300–400. After doing so, the ciphertexts and faulty ciphertexts for plaintexts 0, 1, and 2 are now different, which suggests that the fault injection is having an effect.

However, the key recovery step still does not seem to work. Even with the three ciphertext/faulty-ciphertext pairs for plaintexts 0, 1, and 2, the key-guessing process does not produce a candidate key.

Could this still be related to my choice of glitch parameters (e.g., width and offset)? Or does this indicate that the fault model is not the one expected by the attack?

Thanks for your help.

Maybe and maybe. The attack works if the glitch has a very specific effect on the target code execution; we describe this in good detail. Have a look at the generated code to ensure that it can be glitched.

Then, compute what the glitched ciphertext should be. Sweep your glitch parameters until you get that result; then the attack will work.