Study/System

까나리..아니 canary

imaginefuture-1 2025. 4. 10. 17:30

 

// Name: canary.c

#include <unistd.h>

int main() {
  char buf[8];
  read(0, buf, 32);
  return 0;
}

 

 

 

 

 

 

 

왼쪽: 까나리x 오른쪽 까나리o

 

__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>

 

 

 

 

 

달라서 _stack_chk_fail() 호출됨

 

✅ 요거 완벽히 정리해두면 개이득

항목값
입력 버퍼 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

 

 

tls에 저장해둔 카나리값이랑 같다!!!!!!!!!!

 

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나 리버싱러들 필수 개념이니까!

궁금한 거 더 있으면 바로 말해!