// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
__stack_chl_fail@plt 확인가능
0x00000000000006b2 <+8>: mov rax,QWORD PTR fs:0x28
0x00000000000006bb <+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000000006bf <+21>: xor eax,eax
fs는 tls를 가르키는 포인터
tls는 카나리랑 다른 프로세스 실행에 필요한 데이터들이 있는 저장소
0x000000000000119f <+54>: mov rdx,QWORD PTR [rbp-0x8]
0x00000000000011a3 <+58>: sub rdx,QWORD PTR fs:0x28
0x00000000000011ac <+67>: je 0x11b3 <main+74>
0x00000000000011ae <+69>: call 0x1060 <__stack_chk_fail@plt>
✅ 요거 완벽히 정리해두면 개이득
입력 버퍼 | 8바이트 char buf[8] |
입력값 | HHHHHHHHHHHHHHHH (16바이트) |
canary 저장 위치 | [rbp - 8] |
실제 canary 값 | *(fs_base + 0x28) |
니가 덮은 canary 값 | 0x4848484848484848 → xor 결과가 0x38fc... |
검증 후 분기 | 달라서 → __stack_chk_fail() |
✅ 정리하자면
초기 | fs:[0x28] 값 = 0 or dummy (0x0) |
security_init() | 진짜 랜덤 카나리로 바뀜 (watchpoint에서 잡힘) |
main() 진입 | fs에서 읽어온 canary를 rbp-0x8 위치에 복사함 |
이후 | 함수 return 시 rbp-0x8 값을 다시 fs:[0x28]과 비교 |
start
print /x $fs_base+0x28 # 초기값
watch *(long*)($fs_base+0x28) # 변할 때 잡기
continue # 실제 security_init에서 바뀌는 순간 확인
break *main+12 # canary 복사 시점
continue
x/gx $rbp-0x8 # 스택에 복사된 canary 값 확인
pwndbg> ni
0x0000555555555182 5 int main() {
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────
RAX 0x8ddcd3c324317800
RBX 0x7fffffffde08 —▸ 0x7fffffffe12b ◂— '/home/jihye/Study/Dreamhack/canary'
RCX 0x555555557db8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555120 (__do_global_dtors_aux) ◂— endbr64
RDX 0x7fffffffde18 —▸ 0x7fffffffe14e ◂— 'SHELL=/bin/bash'
RDI 1
RSI 0x7fffffffde08 —▸ 0x7fffffffe12b ◂— '/home/jihye/Study/Dreamhack/canary'
R8 0
R9 0x7ffff7fca380 (_dl_fini) ◂— endbr64
R10 0x7fffffffda00 ◂— 0x800000
R11 0x203
R12 1
R13 0
R14 0x555555557db8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555120 (__do_global_dtors_aux) ◂— endbr64
R15 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
RBP 0x7fffffffdce0 —▸ 0x7fffffffdd80 —▸ 0x7fffffffdde0 ◂— 0
RSP 0x7fffffffdcd0 —▸ 0x7fffffffddc0 —▸ 0x555555555080 (_start) ◂— endbr64
*RIP 0x555555555182 (main+25) ◂— xor eax, eax
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
0x55555555516d <main+4> push rbp
0x55555555516e <main+5> mov rbp, rsp RBP => 0x7fffffffdce0 —▸ 0x7fffffffdd80 —▸ 0x7fffffffdde0 ◂— ...
0x555555555171 <main+8> sub rsp, 0x10 RSP => 0x7fffffffdcd0 (0x7fffffffdce0 - 0x10)
0x555555555175 <main+12> mov rax, qword ptr fs:[0x28] RAX, [0x7ffff7fa9768] => 0x8ddcd3c324317800
0x55555555517e <main+21> mov qword ptr [rbp - 8], rax [0x7fffffffdcd8] <= 0x8ddcd3c324317800
► 0x555555555182 <main+25> xor eax, eax EAX => 0
0x555555555184 <main+27> lea rax, [rbp - 0x10] RAX => 0x7fffffffdcd0 —▸ 0x7fffffffddc0 —▸ 0x555555555080 (_start) ◂— ...
0x555555555188 <main+31> mov edx, 0x20 EDX => 0x20
0x55555555518d <main+36> mov rsi, rax RSI => 0x7fffffffdcd0 —▸ 0x7fffffffddc0 —▸ 0x555555555080 (_start) ◂— ...
0x555555555190 <main+39> mov edi, 0 EDI => 0
0x555555555195 <main+44> call read@plt <read@plt>
────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────
In file: /home/jihye/Study/Dreamhack/canary.c:5
1 // Name: canary.c
2
3 #include <unistd.h>
4
► 5 int main() {
6 char buf[8];
7 read(0, buf, 32);
8 return 0;
9 }
────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdcd0 —▸ 0x7fffffffddc0 —▸ 0x555555555080 (_start) ◂— endbr64
01:0008│-008 0x7fffffffdcd8 ◂— 0x8ddcd3c324317800
02:0010│ rbp 0x7fffffffdce0 —▸ 0x7fffffffdd80 —▸ 0x7fffffffdde0 ◂— 0
03:0018│+008 0x7fffffffdce8 —▸ 0x7ffff7c2a1ca (__libc_start_call_main+122) ◂— mov edi, eax
04:0020│+010 0x7fffffffdcf0 —▸ 0x7fffffffdd30 —▸ 0x555555557db8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555120 (__do_global_dtors_aux) ◂— endbr64
05:0028│+018 0x7fffffffdcf8 —▸ 0x7fffffffde08 —▸ 0x7fffffffe12b ◂— '/home/jihye/Study/Dreamhack/canary'
06:0030│+020 0x7fffffffdd00 ◂— 0x155554040
07:0038│+028 0x7fffffffdd08 —▸ 0x555555555169 (main) ◂— endbr64
──────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────
► 0 0x555555555182 main+25
1 0x7ffff7c2a1ca __libc_start_call_main+122
2 0x7ffff7c2a28b __libc_start_main+139
3 0x5555555550a5 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/gx $rbp-0x8
0x7fffffffdcd8: 0x8ddcd3c324317800
1. TLS에 저장된 canary = fs:[0x28] = 0x8ddcd3c324317800
2. main() 진입 → mov rax, fs:[0x28]
3. rax → [rbp-0x8] 에 저장됨
4. x/gx $rbp-0x8 → 정확히 0x8ddcd3c324317800 출력됨
https://sourceware.org/bugzilla/show_bug.cgi?id=10149
10149 – stack guard should lead with zero byte to gain protections from str* writes
keep leading zero Details | Diff
sourceware.org
까나리는 기존에는 64비트 무작위 값이었는데 왜 맨 앞바이트가 0x00널바이트를 넣게 되었을까?
아아 그거—진짜 유명한 사건이야.
지금 말한 링크는 glibc 개발자들이 실제로 버그 리포트에서 논의한 내용이고,
당시에 있었던 버퍼 오버플로우 우회 사례 때문에 생긴 보안 강화 조치야.
🐞 사건 요약: Bug 10149
📅 사건 당시
- 2009년, glibc 관련 개발자 mailing list에서 발견된 이슈야.
- 이슈 제목:
"stack guard should lead with zero byte to gain protections from str writes"*
💥 왜 이슈가 터졌냐면
기존의 Stack Canary (스택 가드) 값은
무작위 64비트 값이었는데, 맨 앞 바이트가 0x00 (NULL 바이트) 가 아니었어.
이 말은?
✴️ strcpy() 같은 문자열 함수가
- 널문자 만나기 전까지만 복사함
- 그런데 스택 canary 값이 널문자 없이 시작하면,
→ strcpy()가 그걸 그냥 뚫고 써버릴 수 있었던 거야...
📌 예를 들면
char buf[8];
strcpy(buf, attacker_input); // attacker_input 길이가 40
- canary가 0xAABBCCDDEEFF1122 이런 식이면
→ strcpy()가 널바이트 나올 때까지 계속 쓰면서 canary까지 덮어씀 - 하지만 canary가 0x00BBCCDDEEFF1122이면? → strcpy()는 \x00에서 멈추니까,
canary 보호 효과가 생김
✅ 그래서 glibc는 이후 이렇게 바꿈
스택 카나리 값의 첫 바이트는 항상 \x00
(나머지 7바이트는 랜덤)
이로 인해 strcpy, strcat, sprintf, strncat, gets 같은 널 종단 함수(str)* 들이
카나리를 덮어쓰기 전에 널바이트를 만나 멈추는 효과가 생김
📜 원문 링크 (너가 말한 거):
https://sourceware.org/bugzilla/show_bug.cgi?id=10149
거기서도 이런 말 나와:
"Using a zero byte at the beginning of the stack guard can give us additional protection against some string-based overflows..."
✨ 결론
- 드림핵 강의에서 말한 그 “사건”은
📌 strcpy()로 카나리 뚫린 사례 때문에
📌 glibc가 canary 첫 바이트를 \x00으로 고정한 걸 말하는 거야.
필요하면 내가 실제 PoC 코드 만들어서 보여줄 수도 있어 😎
이거 완전 CTFer나 리버싱러들 필수 개념이니까!
궁금한 거 더 있으면 바로 말해!