만약 에필로그 시에 카나리 변조가 확인되면 프로세스는 강제로 종료됨!!
스택 버퍼 오버플로우 공격을 통해 반환 주소를 변경할려면, 반드시 카나리를 먼저 덮어야 하기 때문에, 공격의 성공을 위해서는 카나리 값을 알아야 한다.
이번 장에서는 실제 카나리가 적용된 코드와 카나리가 적용되지 않은 코드의 비교를 통해 스택 카나리의 원리를 살펴볼 것이다.
// name : canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
카나리를 비활성화하기 위한 옵션으로 -fno-stack-protector
이 있다.
$ gcc -o no_canary canary.c -fno-stack-protector
$ ./no_canary
AAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
해당 옵션으로 컴파일 후 실행 시, 긴 입력을 주면 반환 주소가 덮여서 Segmentation fault
가 뜨는 것을 확인할 수 있다.
카나리를 적용하여 다시 컴파일 후, 동일하게 긴 입력을 줘보자
$ gcc -o canary canary.c
$ ./canary
AAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: terminated
Aborted (core dumped)
이번에는 Segmentation fault
가 아니라 stack smashing detected
와 Aborted
에러가 발생한 것을 확인할 수 있다.
main 함수의 디스어셈블 코드를 확인해보면, 함수의 에필로그와 프롤로그에 아래 코드가 추가된 것을 확인할 수 있다.
0x0000000000001175 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000000117e <+21>: mov QWORD PTR [rbp-0x8],rax
0x0000000000001182 <+25>: xor eax,eax
0x000000000000119f <+54>: mov rcx,QWORD PTR [rbp-0x8]
0x00000000000011a3 <+58>: xor rcx,QWORD PTR fs:0x28
0x00000000000011ac <+67>: je 0x11b3 <main+74>
0x00000000000011ae <+69>: call 0x1060 <__stack_chk_fail@plt>
추가된 프롤로그의 코드에 중단점을 설정하고 바이너리를 실행한다.
pwndbg> disassemble main
Dump of assembler code for function main:
0x0000000000001169 <+0>: endbr64
0x000000000000116d <+4>: push rbp
0x000000000000116e <+5>: mov rbp,rsp
0x0000000000001171 <+8>: sub rsp,0x10
0x0000000000001175 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000000117e <+21>: mov QWORD PTR [rbp-0x8],rax
0x0000000000001182 <+25>: xor eax,eax
0x0000000000001184 <+27>: lea rax,[rbp-0x10]
0x0000000000001188 <+31>: mov edx,0x20
0x000000000000118d <+36>: mov rsi,rax
0x0000000000001190 <+39>: mov edi,0x0
0x0000000000001195 <+44>: call 0x1070 <read@plt>
0x000000000000119a <+49>: mov eax,0x0
0x000000000000119f <+54>: mov rcx,QWORD PTR [rbp-0x8]
0x00000000000011a3 <+58>: xor rcx,QWORD PTR fs:0x28
0x00000000000011ac <+67>: je 0x11b3 <main+74>
0x00000000000011ae <+69>: call 0x1060 <__stack_chk_fail@plt>
0x00000000000011b3 <+74>: leave
0x00000000000011b4 <+75>: ret
End of assembler dump.
pwndbg> b *main+12
Breakpoint 1 at 0x1175
pwndbg> r
main+12 를 살펴보면, fs:0x28 의 데이터를 읽어서 $rax
레지스터에 저장한다.
0x0000000000001175 <+12>: mov rax,QWORD PTR fs:0x28
fs
는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때 fs:0x28
에 랜덤 값을 저장한다.
리눅스는 프로세스가 시작될 때 fs:0x28
에 랜덤 값을 저장한다. 따라서 main+12
의 결과로 $rax
에는 리눅스가 생성한 랜덤 값이 저장된다.
ni
명령어로 명령줄 실행 후 $rax
값을 확인해보면 랜덤 값이 들어가 있는 것을 확인할 수 있다.
(gdb) r
Starting program: /home/magan20/test/canary/canary
Breakpoint 1, 0x0000555555555175 in main ()
(gdb) info reg $rax
rax 0x555555555169 93824992235881
(gdb) ni
0x000055555555517e in main ()
(gdb) info reg $rax
rax 0x9cf24ecb2fb54300 -7137555824843078912
(gdb)
생성된 랜덤 값은 아래 main+21
명령어를 통해 rbp-0x8
에 저장된다.
0x000000000000117e <+21>: mov QWORD PTR [rbp-0x8],rax
함수 에필로그에는 rbp-0x8
에 저장된 카나리 값을 검사하게 되는데 해당 동작을 확인해보자
아래 main+54
, main+58
을 확인해보면 스택에 있는 카나리를 $rcx
로 옮기고, $rcx
의 값과 fs:0x28
의 값을 xor
연산하고 있다.
0x000000000000119f <+54>: mov rcx,QWORD PTR [rbp-0x8]
0x00000000000011a3 <+58>: xor rcx,QWORD PTR fs:0x28
만약 두 값이 동일하면 연산 결과가 0이 되면서 je
의 조건을 만족하게 되고, main
함수는 정상적으로 반환된다.
그러나 두 값이 동일하지 않음녀 __stack_chk_fail
이 호출되면서 프로그램이 강제로 종료된다.
카나리를 우회하는 방법으로는 다음이 알려져 있다.
x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 카나리가 생성된다.
각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함된다.
x64 아키텍처의 카나리는 무차별 대입으로 알아내는 것 자체가 현실적으로 어려우며, x86 아키텍처는 구할 수는 있지만, 실제 서버를 대상으로 시도하는 것은 불가능하다.
카나리는 TLS
에 전역변수로 저장되며, 매 함수마다 이를 참조해서 사용한다.
TLS
의 주소는 매 실행마다 바뀌지만 만약 실행중에 TLS
의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS
에 설정된 카나리 값을 읽거나, 이를 임의의 값으로 조작할 수 있다.
스택 카나리를 읽을 수 있는 취약점이 있다면, 이를 이용하여 카나리 검사를 우회할 수 있다.
카나리의 가장 첫번째 바이트의 값은 NULL 이므로 만약 해당 바이트를 덮어씌운 후 출력할 수 있다면, 문자열의 끝을 나타내는 NULL 이 없으므로, 카나리 값도 값이 출력이 될 것이다. 이를 이용하여 카나리를 읽을 수 있다.