본문 바로가기

Core BSP 분석/커널 트러블슈팅

[Linux][Kernel] Kernel Panic @__stack_chk_fail - 스택 카나리 (Stack canary Feature)

#커널 크래시 디버깅 및 TroubleShooting
 
최근 흥미로운 커널 패닉이 나왔는데요. 디버깅 과정을 공유 좀 하고자 해요.
 
일단 콜스택부터 볼께요. sock_has_perm() 함수가 돌다가 갑자기 __stack_chk_fail() 함수 호출로 panic()이 일어났거든요. 왜 이런 현상이 발생했을까요?
crash> bt e5752c00
PID: 1787   TASK: e5752c00  CPU: 4   COMMAND: "net_socket"
bt: WARNING:  stack address:0xe853fa38, program counter:0xc0ee5b60
 #0 [<c0ed8b64>] (panic) from [<c0125038>]
 #1 [<c0125038>] (__stack_chk_fail) from [<c032b6cc>]
 #2 [<c032b6cc>] (sock_has_perm) from [<c0327d00>]
 #3 [<c0327d00>] (security_socket_recvmsg) from [<c0ceb1c8>]
 #4 [<c0ceb1c8>] (sock_recvmsg) from [<c0cec474>]
 #5 [<c0cec474>] (___sys_recvmsg) from [<c0ced5b4>]
 #6 [<c0ced5b4>] (__sys_recvmsg) from [<c0106820>]
 
Trace32로 콜스택을 잡으면 아래 정보를 확인할 수 있습니다. 커널 패닉이 발생한 흔적이 보이죠.
-000|panic(fmt = 0xC1379761)
-001|__stack_chk_fail()
-002|sock_has_perm(?, sk = 0xE98D3400, perms = 2)
-003|security_socket_recvmsg(?, ?, ?, ?)
-004|__sock_recvmsg(inline)
-004|sock_recvmsg(sock = 0xC3C99C00, msg = 0xE853FF7C, size = 65536, flags = 0)
-005|___sys_recvmsg.part.5(sock = 0xC3C99C00, msg = 0xA08813B0, msg_sys = 0xE853FF7C, flags = 0, nosec = 0)
-006|fput_light(inline)
-006|__sys_recvmsg(?, msg = 0xA08813B0, flags = 0)
-007|ret_fast_syscall(asm)
 
T32(Trace32)로 UP 버튼으로 security_socket_recvmsg() 함수가 호출될 시점 스택 주소를 확인하면, 0xE853FDD8 임을 알 수 있습니다.
-003|security_socket_recvmsg(?, ?, ?, ?)
-004|__sock_recvmsg(inline)
-004|sock_recvmsg(sock = 0xC3C99C00, msg = 0xE853FF7C, size = 65536, flags = 0)
-005|___sys_recvmsg.part.5(sock = 0xC3C99C00, msg = 0xA08813B0, msg_sys = 0xE853FF7C, flags = 0, nosec =
-006|fput_light(inline)
-006|__sys_recvmsg(?, msg = 0xA08813B0, flags = 0)
-007|ret_fast_syscall(asm)
N _  R0   C1379761  R8   E5752C00
Z _  R1   E853FD54  R9          0
C _  R2   DC8CB01F  R10  C3C99C00
V _  R3          0  R11         0
Q _  R4   C3C99C00  R12         0
     R5   00010000  R13  E853FDD8
0 _  R6   E853FF7C  R14         0
1 _  R7          0  PC   C0327D00
2 _  SPSR       10  CPSR     01D3
 
"d.v %y.l 0xE853FDD8" 명령어로 스택에 푸쉬된 심볼을 확인할 수 있습니다.
   NSD:E853FD4C| 38 50 12 C0  0xC0125038         \\vmlinux\panic\__stack_chk_fail+0x10
   NSD:E853FD50| 61 97 37 C1  0xC1379761         \\vmlinux\Global\kallsyms_token_index+0x5D41
   NSD:E853FD54| CC B6 32 C0  0xC032B6CC         \\vmlinux\hooks\sock_has_perm+0x9C
   NSD:E853FD58| 1F B0 8C DC  0xDC8CB01F
   NSD:E853FD5C| 00 00 00 00  0x0
   NSD:E853FD60| 00 00 00 00  0x0
   NSD:E853FD64| CC B6 32 C0  0xC032B6CC         \\vmlinux\hooks\sock_has_perm+0x9C
   NSD:E853FD68| 74 FD 53 E8  0xE853FD74
   NSD:E853FD6C| A0 FD 53 E8  0xE853FDA0
   NSD:E853FD70| 7C FF 53 E8  0xE853FF7C
   NSD:E853FD74| 02 05 00 00  0x502              \\vmlinux\Global\NR_syscalls+0x37E
   NSD:E853FD78| 84 FD 53 E8  0xE853FD84
   NSD:E853FD7C| C4 04 D4 C0  0xC0D404C4         \\vmlinux\af_netlink\netlink_recvmsg\out+0x38
   NSD:E853FD80| 70 08 FD EC  0xECFD0870
   NSD:E853FD84| 00 00 00 00  0x0
   NSD:E853FD88| 00 34 8D E9  0xE98D3400
   NSD:E853FD8C| 00 00 00 00  0x0
   NSD:E853FD90| 00 00 00 00  0x0
   NSD:E853FD94| 00 00 00 00  0x0
   NSD:E853FD98| 00 00 00 00  0x0
   NSD:E853FD9C| 00 00 00 00  0x0
   NSD:E853FDA0| 00 00 00 00  0x0
   NSD:E853FDA4| 00 00 00 00  0x0
   NSD:E853FDA8| 00 00 00 00  0x0
   NSD:E853FDAC| 00 00 00 00  0x0
   NSD:E853FDB0| 00 00 00 00  0x0
   NSD:E853FDB4| 1F B0 8C DC  0xDC8CB01F
   NSD:E853FDB8| 00 0A 82 ED  0xED820A00  // E853FDBC - 0x54 = 0xE853FD68 "where: sub     r13,r13,#0x54    ; r13,r13,#84"
   NSD:E853FDBC| 00 9C C9 C3  0xC3C99C00  //<<-- R4
   NSD:E853FDC0| 00 00 01 00  0x10000        //<<-- R5
   NSD:E853FDC4| 7C FF 53 E8  0xE853FF7C    //<<-- R6
   NSD:E853FDC8| 00 00 00 00  0x0              //<<-- R7
   NSD:E853FDCC| 00 2C 75 E5  0xE5752C00  //<<-- R8
   NSD:E853FDD0| 00 00 00 00  0x0               //<<-- R9
   NSD:E853FDD4| 00 7D 32 C0  0xC0327D00         \\vmlinux\security\security_socket_recvmsg+0x14  //<<-- R14
   NSD:E853FDD8| 00 00 00 00  0x0   //<<-- 스택 주소
   NSD:E853FDDC| C8 B1 CE C0  0xC0CEB1C8         \\vmlinux\socket\sock_recvmsg+0x58
 
 
security_socket_recvmsg 함수에서 sock_has_perm 함수 호출 시 0x54 바이트 만큼 스택을 잡습니다.
NSR:C032B630|E92D43F0  sock_has_perm:  push    {r4-r9,r14}
NSR:C032B634|E24DD054                  sub     r13,r13,#0x54    ; r13,r13,#84
 
이제 코드 리뷰를 좀 해보겠습니다.
[1]: sock_has_perm() 함수가 처음 security_socket_recvmsg() 함수로부터 호출되었을 때 스택 주소는 0xE853FDD8이네요.
 
[2]: 스택이 푸쉬된 다음에 스택 주소는 0xE853FDBC로 업데이트 되었네요. 
 Stack: 0xE853FDBC(0xE853FDD8-0x1C)
 
[3]: 로컬 변수들은 스택 메모리에서 돌죠. 로컬 변수가 스택을 쓸 수 있도록 0x54 바이트 만큼 방을 비워줍니다. 
 Stack: 0xE853FD68(0xE853FDBC-0x54)
 
[4]: R7는 0xC1809948 을 갖고 있는데 여기서 __stack_chk_guard란 함수를 링크하네요.
c032b6d4:       c1809948        .word   0xc1809948
 
crash> p &__stack_chk_guard
$3 = (unsigned long *) 0xc1809948 <__stack_chk_guard>.
 
[5]: R3는 stack canary 매직값 0xdc8cb01f을 갖고 있는데 이런 계산으로  R3 = 0xdc8cb01f =  *0xC1809948 업데이트되네요.
아래 명령어를 치면 0xC1809948 주소에 dc8cb01f란 값이 위치해 있어요.
.crash> rd 0xC1809948
c1809948:  dc8cb01f    
 
[6]: Stack canary 매직 덤프값 0xdc8cb01f이 스택으로 푸쉬되고 있네요. R3이 갖고 있는 값을 스택에 푸쉬하는 거죠. 0xE853FDB4 주소로 푸쉬되죠.
0xE853FDB4=0xE853FD68+0x4c = SP+0x4c
 
[7]: [6]번 인스트럭션에서 stack canary magic dump 값을 스택(0xE853FD68+0x4c = SP+0x4c) 공간에 푸쉬했잖아요.
똑같은 메모리 공간에 접근해서 stack canary magic dump 값을 꺼내와요. 이 값 0xdc8cb01f은 R2에 업데이트되죠.
 
[8]: 여기서 중요한 점은 [6] 명령어가 실행된 다음에 R7 레지스터는 변경된 적이 없다는 점인데요.
R7 레지스터가 0xC1809948 값을 갖고 있어야 하는데 0xC1800048 값이네요.
그래서 R3이 갑자기 0x0으로 바뀌어 있어요.
 
원래 R7 레지스터가 0xC1809948를 갖고 있으면 당연히 R3은 0xdc8cb01f일텐데 말이죠.
0xc032b638 <sock_has_perm+8>: ldr r7, [pc, #148]
 
[9]: R2과 R3값이 다르니 커널 패닉이 발생하네요. kernel panic occurs
#Code
0xc032b630 <sock_has_perm>:     push    {r4, r5, r6, r7, r8, r9, lr} //<<--[2]
0xc032b634 <sock_has_perm+0x4>: sub     sp, sp, #84     ; 0x54  //<<--[3]
0xc032b638 <sock_has_perm+0x8>: ldr     r7, [pc, #148]  ; 0xc032b6d4 <sock_has_perm+0xa4> //<<--[4]
0xc032b63c <sock_has_perm+0xc>: add     r6, sp, #28
0xc032b640 <sock_has_perm+0x10>:        mov     r9, r0
0xc032b644 <sock_has_perm+0x14>:        ldr     r4, [r1, #420]  ; 0x1a4
0xc032b648 <sock_has_perm+0x18>:        mov     r5, r1
0xc032b64c <sock_has_perm+0x1c>:        mov     r8, r2
0xc032b650 <sock_has_perm+0x20>:        ldr     r3, [r7]  //<<--[5]
0xc032b654 <sock_has_perm+36>:  mov     r1, #0
0xc032b658 <sock_has_perm+40>:  mov     r2, #48 ; 0x30
0xc032b65c <sock_has_perm+44>:  mov     r0, r6
0xc032b660 <sock_has_perm+48>:  str     r3, [sp, #76]   ; 0x4c //<<--[6]
// snip
0xc032b6b8 <sock_has_perm+0x88>:        ldr     r2, [sp, #76]   ; 0x4c  //<<--[7]
0xc032b6bc <sock_has_perm+0x8c>:        ldr     r3, [r7]  //<<--[8]
0xc032b6c0 <sock_has_perm+0x90>:        cmp     r2, r3
0xc032b6c4 <sock_has_perm+0x94>:        beq     0xc032b6cc <sock_has_perm+156>
0xc032b6c8 <sock_has_perm+0x98>:        bl      0xc0125028 <__stack_chk_fail>  //<<--[9]
 
 
좀 어렵게 설명한 것 같은데요. 쉽게 C코드로 변경하면 아래와 같아요.
void sock_has_perm(void)
{
     int stack_check = &__stack_chk_guard;
     int loc_stack = 0;
 
     loc_stack = stack_check;
 
    // 여러 서브 함수들이 호출됨. 각 함수가 호출이 일어날 때 마다 당연 스택 푸쉬/팝 동작이 수행됨
     A();
     B();
     C();
     D();         
 
     loc_stack값과 stack_check값이 다르다는 것은 스택이 오염되었다고 판단하므로, 커널 패닉 유발!
     if (loc_stack != stack_check) {
        __stack_chk_fail();
     }
}
 
0xdc8cb01f은 R2로 업데이트되는데요. "ldr     r2, [sp, #76]   ; 0x4c"
만약 스택이 오염되었으면 R2가 이상한 값을 갖고 있어야 하는데, 이번 경우는 R3이 이상한 값을 갖고 있어서 커널 패닉이 발생한 거거든요.
 
이 이슈의 핵심 포인트는 R7 레지스터가 0xC1809948 값을 갖고 있어야 하는데 0xC1800048 값이라는 거구요.
붉은 색으로 책한 공간에 비트 플립이 일어나 발생한 문제 -> 0xC1800048(0xC1809948)로 확인되었어요.
 
사실 이 디바이스 상태가 좀 안 좋았거든요. 그래서 메모리를 바꾸어서 테스트 해 보았더니 별 문제 없더라구요.
 
스택 카너리에 대해 좀 더 설명을 하면요. boot_init_stack_canary() 함수에서 스택 카너리를 초기화합니다. 참고로 이 함수는 start_kernel()에서 호출됩니다. 아래 코드를 잠깐 보면 task descriptor(TCB)의 멤버에 stack_canary에 canary 값을 초기화합니다.
extern unsigned long __stack_chk_guard;
 
static __always_inline void boot_init_stack_canary(void)
{
unsigned long canary;
 
/* Try to get a semi random initial value. */
get_random_bytes(&canary, sizeof(canary));
canary ^= LINUX_VERSION_CODE;
 
current->stack_canary = canary;
__stack_chk_guard = current->stack_canary;
}
658 ifdef CONFIG_CC_STACKPROTECTOR_REGULAR
659   stackp-flag := -fstack-protector
660   ifeq ($(call cc-option, $(stackp-flag)),)
661     $(warning Cannot use CONFIG_CC_STACKPROTECTOR_REGULAR: \
662              -fstack-protector not supported by compiler)
663   endif
 
이 플래그를 설정하면, 리눅스 커널에서 기본으로 호출되는 여러 함수들 하단에 __stack_chk_fail 스택 오염 진단 루틴을 추가합니다.
0xc0100974 <do_one_initcall+404>:       bl      0xc0125028 <__stack_chk_fail>
0xc0100d8c <name_to_dev_t+828>: bl      0xc0125028 <__stack_chk_fail>
0xc0107854 <__show_regs+700>:   bl      0xc0125028 <__stack_chk_fail>
0xc01219c0 <sha384_neon_final+108>:     bl      0xc0125028 <__stack_chk_fail>
0xc0133c34 <sys_newuname+352>:  bl      0xc0125028 <__stack_chk_fail>
0xc0133d68 <sys_sethostname+276>:       bl      0xc0125028 <__stack_chk_fail>
 
 
# Reference: For more information on 'Linux Kernel';
 
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
 
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2