본문 바로가기

시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리/11장: AAPCS(함수 호출 규약)

[Arm][AAPCS] 스택 오염이 발생한 후에는 시스템은 어떻게 동작할까


스택 오염이 발생한 후에는 시스템은 어떻게 동작할까요? Arm 프로세서가 스택 오염을 바로 감지하고 익셉션을 유발하면 좋겠지만, 스택에 저장된 데이터가 오염되는 패턴을 예측할 수 없으니 어떻게 오동작할지 예상하기 어렵습니다. 가장 대표적인 증상은 'Invalid branch'인데, 스택에 저장된 링크 레지스터의 값이 오염됐으므로, 오염된 값으로 프로그램 카운터를 브랜치하다가 메모리 어보트 익셉션이 유발됩니다. 소프트웨어 개발자들이 사용하는 용어로 '크래시가 발생'하게 됩니다.

 


그림 8.1에서 스택 오염으로 오동작하는 상황을 어셈블리 명령어와 함께 설명하겠습니다.

01 NSR:C0BDB698|E92D4800  schedule: push    {r11,r14}
...
02 NSR:C0BDB718|E24BD004            sub     r13,r11,#0x4     ; r13,r11,#4
03 NSR:C0BDB71C|E8BD4800            pop     {r11,r14}

 


01번째 줄은 schedule() 함수에서 가장 먼저 실행되는 명령어인데, r11와 r14(링크 레지스터)를 스택에 푸시합니다. 그런데 스택이 오염되서 스택에 저장된 r14 값이 0x0으로 바뀐 것입니다.

이후 03번째 줄을 실행하면 프로세스의 스택에 저장된 r14 레지스터의 값을 PC로 로딩하는데, 이 때 0x0을 프로그램 카운터로 로딩하게 됩니다.

만약 스택 오염이 커널 드라이버에서 발생하면 어떻게 동작할까요? 대부분 다음과 같은 로그와 함께 커널 크래시가 발생합니다.

01 [262.401303] Unable to handle kernel NULL pointer dereference at virtual address 00000000
02 [262.401365] pgd = dbdc4000
03 [262.401389] [00000000] *pgd=00000000
04 [262.401433] Internal error: Oops: 80000005 [#1] PREEMPT SMP ARM
05 [262.401459] Modules linked in:
06 [262.401495] CPU: 0 PID: 1113 Comm: bash Tainted: G        W    5.10.49-g356bd9f-00007-gadca646 #1
07 [262.401522] task: da6b0540 ti: d9412000 task.ti: d9412000
08 [262.401549] PC is at 0x0
09 [262.401590] LR is at worker_thread+0x4c/0x58
10 [262.401619] pc : [<00000000>]    lr : [<c0adc274>]    psr: a00f0013
11 [262.401619] sp : d9413c68  ip : c0ac6c20  fp : 0000dd86
12 [262.401654] r10: 0000010e  r9 : 0000010a  r8 : de0ddc20
13 [262.401678] r7 : c13ddf00  r6 : 00000500  r5 : d9094540  r4 : c13e3780
14 [262.401703] r3 : 00000000  r2 : 00000001  r1 : 00000500  r0 : d9094540

 

 

01번째 줄을 보면 Arm 프로세서 내에 있는 MMU가 0x0 주소를 처리할 수 없다는 메시지를 읽을 수 있습니다. 위 커널 로그에서 08번째 줄을 보면 PC가 0x0입니다. 

원래 0xc000cff8 주소에는 복귀할 주소인 0x80138f10가 저장돼 있습니다. 원래 이 주소를 PC에 로딩하면 복귀할 주소로 분기하게 됩니다. 문제는 스택 오염으로 0xc000cff8 주소에는 0x80138f10 주소 대신에 0x0이 저장돼 있다는 사실입니다. 

    “결국 0x0을 PC에 로딩하게 되면 Arm 프로세서는 0x0 주소에 접근하다가 유효하지 
    않는 주소라는 것을 판단하면 어보트가 유발됩니다.” 

 


스택이 오염돼 링크 레지스터의 값을 PC에 0으로 변경하니 이런 메시지와 함께 커널 크래시가 발생한 것입니다.

[정보]
스택 오염이 발생하면 대부분 상황에서 크래시가 유발되며, 유저 애플리케이션 코드에서 위와 같은 상황이 발생하면 ASSERT와 함께 애플리케이션이 종료됩니다. 만약 커널 드라이버인 경우는 커널 크래시가 유발되며 시스템은 리셋됩니다.

 


그렇다면 위와 같은 스택 오염 증상은 어떤 코드를 실행하면 발생할까요? 예제 코드는 다음과 같습니다. 

01 static void corrupt_stack(void)
02 {
03 /* Use default char array length that triggers stack protection. */
04 unsigned int data[2];
05
06 memset((void *)data, 0, 6);
07 }

 

04번째 줄에서는 data란 이름의 배열의 크기가 2인데, 06번째 줄에서는 6만큼 메모리 복사를 수행했습니다. 원래 06번째 줄은 'memset((void *)data, 0, sizeof(data));'와 같이 입력하는 게 맞습니다. 배열의 크기만큼 데이터를 복사해야 배열 사이즈의 경계를 넘어서 데이터를 복사하지 않겠죠.

06번째 줄과 같이 배열의 경계를 넘어서 데이터를 복사하는 증상을 'Out of Bound'라고 합니다. 실전 프로젝트나 각종 오픈 소스 커뮤니티에서 자주 볼 수 있는 용어입니다.

 

[정보]
사실 위 코드는 리눅스 커널에서 사용된 코드를 응용한 것입니다. 

https://elixir.bootlin.com/linux/v3.18.140/source/drivers/misc/lkdtm.c
static noinline void corrupt_stack(void)
{
/* Use default char array length that triggers stack protection. */
char data[8];

memset((void *)data, 0, 64);
}

 

리눅스 커널의 drivers/misc/lkdtm.c 소스 파일은 일부러 오류를 유발하는 코드로 구성돼 있습니다. 오류가 있는 코드가 실행되는 시점에 제대로 오류 메시지를 커널 로그로 출력되는지 혹은 메모리 덤프가 추출되는지 테스트하는 용도의 드라이버입니다.