스택 카나리 (Stack Canary)
1. 스택 카나리
1. 스택 카나리 (Stack Canary) 란?
BOF 공격으로부터 반환 주소를 보호하기 위해 스택 카나리 (Stack Canary) 보호기법이 생겼다.
스택 카나리는 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고,
함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법이다.
카나리 값의 변조가 확인되면 프로세스는 강제로 종료 된다.
BOF 공격에서 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 하므로
카나리를 모르는 공격자는 반환 주소를 덮을 때 카나리 값을 변조하게 된다.
2. 카나리 정적 분석
1. 스택 카나리 비활성화
// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
BOF 가 발생하는 C 코드이다.
gcc 는 기본적으로 스택 카나리를 적용하여 컴파일한다.
-fno-stack-protector
따라서 컴파일 옵션으로 -fno-stack-protector 을 추가해야 카나리 없이 컴파일 할 수 있다
$ gcc -o no_canary canary.c -fno-stack-protector
$ ./no_canary
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
카나리가 없을 때는 반환주소가 덮이기 때문에 Segmentation fault 에러가 발생한다
$ gcc -o canary canary.c
$ ./canary
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: terminated
Aborted (core dumped)
카나리가 있을 때 BOF 가 발생하면 stack smashing detected 에러와 Aborted 에러가 발생한다.
이는 BOF가 발생하여 프로세스가 강제종료 된 것이다.
2. 어셈블리 비교
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
카나리가 적용된 파일의 어셈블리이다.
0x0000000000001149 <+0>: endbr64
0x000000000000114d <+4>: push rbp
0x000000000000114e <+5>: mov rbp,rsp
0x0000000000001151 <+8>: sub rsp,0x10
0x0000000000001155 <+12>: lea rax,[rbp-0x8]
0x0000000000001159 <+16>: mov edx,0x20
0x000000000000115e <+21>: mov rsi,rax
0x0000000000001161 <+24>: mov edi,0x0
0x0000000000001166 <+29>: call 0x1050 <read@plt>
0x000000000000116b <+34>: mov eax,0x0
0x0000000000001170 <+39>: leave
0x0000000000001171 <+40>: ret
카나리가 적용되지 않은 파일의 어셈블리이다.
두 어셈블리를 비교해보면
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>
함수 에필로그에는 위와 같은 코드가 추가되었다.
3. 카나리 동적 분석
1. 카나리 저장 (프롤로그)
추가된 프롤로그의 코드를 다시 보자.
0x0000000000001175 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000000117e <+21>: mov QWORD PTR [rbp-0x8],rax
0x0000000000001182 <+25>: xor eax,eax
main+12는 fs:0x28 의 데이터를 읽어서 rax 에 저장한다.
프로세스가 시작될 때 fs:0x28 에 랜덤 값을 저장한다.
fs:0x28 에 저장되는 값을 master canary 라고 한다.
main+12 에 break 를 걸고 실행 후 rax 의 값을 확인하면
pwndbg> p /a $rax
$2 = 0x9754f337edc93300
첫 바이트가 NULL 인 8바이트 데이터가 저장되어 있다.
이 값이 master canary 이다.
main+21 에서 master canary 를 rbp-0x8 에 저장한다.
pwndbg> x/gx $rbp-0x8
0x7fffffffd148: 0x9754f337edc93300
스택 프레임에 저장되는 값을 stack cananry 라고 한다.
2. 카나리 검사 (에필로그)
추가된 에필로그의 코드를 다시 보자.
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>
main+54 에서 rbp-0x8 에 저장한 master canary 값을 rcx에 옮긴다
main+58 에서 rcx값과 fs:0x28값을 xor 연산한다
pwndbg> p /a $rcx
$6 = 0x9754f337edc93300
카나리가 변조되지 않아 rcx 의 값이 fs:0x28의 값과 같으면
xor 연산했을 때 0이 되어 je 의 조건을 만족한다.
pwndbg> ni
[Inferior 1 (process 20990) exited normally]
프로그램이 정상적으로 종료된다.
pwndbg> p /a $rcx
$7 = 0x7ffff7ed81f2 <__GI___libc_read+18>
카나리가 변조되어 rcx 의 값이 fs:0x28의 값과 다르면
xor 연산했을 때 0이 되지 않아 __stack_chk_fail 함수가 호출된다.
pwndbg> ni
Program terminated with signal SIGABRT, Aborted.
The program no longer exists.
프로그램이 강제로 종료된다.
4. 카나리 생성 과정
1. TLS 주소 파악
카나리 값(master canary) 은 프로세스가 시작될 때 TLS 에 전역변수로 저장된다.
fs 레지스터는 TLS 를 가리키기 때문에 fs 의 값을 알면 TLS 의 주소를 알 수 있다.
그러나 리눅스에서 fs 의 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있다.
gdb 에서 print, info 명령어 등으로 값을 알아낼 수 없다.
fs 의 값을 설정할 때 호출하는 시스템 콜은
arch_prctl(ARCH_SET_FS, addr) 의 형태로 호출된다.
이렇게 호출하면 fs 의 값이 addr 로 설정된다.
따라서 arch_prctl 시스템 콜에 break 를 설정해서 fs 의 값을 알아내보자.
gdb catch (catchpoint)
catch 명령어는 특정 이벤트가 발생했을 때 프로세스를 중지하는 명령어이다.
init_tls() 안에서 catchpoint 에 도달할 때까지 continue 명령어를 실행한다.
pwndbg> catch syscall arch_prctl
Catchpoint 1 (syscall 'arch_prctl' [158])
pwndbg> r
Catchpoint 1 (call to syscall arch_prctl), init_tls () at rtld.c:758
758 rtld.c: No such file or directory.
► 0x7ffff7fd0cc0 <init_tls+208> test eax, eax
0x7ffff7fd0cc2 <init_tls+210> je init_tls+258 <init_tls+258>
0x7ffff7fd0cc4 <init_tls+212> lea rbx, [rip + 0x24805]
0x7ffff7fd0ccb <init_tls+219> nop dword ptr [rax + rax]
pwndbg> i r $rdi
rdi 0x1002 4098 // ARCH_SET_FS 0x1002
pwndbg> i r $rsi
rsi 0x7ffff7fbd540 140737353864512
pwndbg> x/gx 0x7ffff7fbd540 + 0x28
0x7ffff7fbd568: 0x0000000000000000
rdi 의 값인 0x1002 는 ARCH_SET_FS 의 상수값이다.
rsi 의 값은 0x7ffff7fbd540 이므로 TLS 를 0x7ffff7fbd540 에 저장하고 fs 는 이를 가리키게 된다.
카나리가 저장될 fs+0x28 (0x7ffff7fbd540 + 0x28) 의 값은 아무것도 설정되어 있지 않은 것을 볼 수 있다.
2. 카나리 값 설정
TLS + 0x28 에 값을 쓸 때 watchpoint 를 설정한다.
gdb watch (watchpoint)
watch 명령어는 특정 주소에 지정된 값이 변경되면 프로세스를 중단시키는 명령어이다.
pwndbg> watch *(0x7ffff7fbd540 + 0x28)
Hardware watchpoint 2: *(0x7ffff7fbd540 + 0x28)
pwndbg> c
Continuing.
Hardware watchpoint 2: *(0x7ffff7fbd540 + 0x28)
Old value = 0
New value = 187826944
dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at ../sysdeps/unix/sysv/linux/dl-osinfo.h:77
77 ../sysdeps/unix/sysv/linux/dl-osinfo.h: No such file or directory.
pwndbg> x/gx 0x7ffff7fbd540 + 0x28
0x7ffff7fbd568: 0xb698679e6b32eb00
TLS + 0x28 의 값을 조회하면 master canary 값이 0xb698679e6b32eb00 으로 설정된 것을 확인할 수 있다.
0xb698679e6b32eb00 의 값이 실제 main 함수에서 사용되는 값인지 확인해보자.
0x555555555175 <main+12> mov rax, qword ptr fs:[0x28]
main +12 부분에 break를 걸고 실행하면
pwndbg> b *main +12
Breakpoint 3 at 0x555555555175
pwndbg> c
Continuing.
0x555555555175 <main+12> mov rax, qword ptr fs:[0x28]
► 0x55555555517e <main+21> mov qword ptr [rbp - 8], rax
0x555555555182 <main+25> xor eax, eax
0x555555555184 <main+27> lea rax, [rbp - 0x10]
pwndbg> i r $rax
rax 0xb698679e6b32eb00 -5289363832245654784
rax 의 값에 0xb698679e6b32eb00 카나리 값이 들어간 것을 확인할 수 있다.
5. 카나리 우회
1. 무차별 대입 (Brute Force)
x64
8byte 의 canary 가 생성된다.
첫 바이트는 NULL 이므로 7바이트의 랜덤한 값이다.
2567번 연산이 필요하다
연산량이 많아 사실상 무차별 대입 공격으로 canary 를 알아내는 것은 불가능하다.
x86
4byte 의 canary 가 생성된다.
첫 바이트는 NULL 이므로 3바이트의 랜덤한 값이다.
2563번의 연산이 필요하다
canary 를 구할순 있지만 실제 서버를 대상으로 무차별 대입 공격을 시도하는 것은 불가능하다.
2. TLS 접근
canary 는 TLS 에 전역변수로 저장된다.
매 함수마다 이를 참조해서 사용한다.
TLS 의 주소는 매 실행마다 바뀌지만 만약 실행 중에 TLS의 주소를 알 수 있고,
임의 주소에 대한 읽기 쓰기가 가능하다면 TLS 에 설정된 카나리 값을 읽거나, 조작할 수 있다.
BOF 를 할 때 알아낸 카나리값이나 조작한 카나리 값으로 카나리 검사를 우회할 수 있다
3. 카나리 릭 (Canary Leak)
// Name: canary.c
// Compile : gcc -z execstack -no-pie -o canary_leak canary.c
#include <stdio.h>
#include <unistd.h>
void setup_environment() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main() {
setup_environment();
char buf[8];
read(0, buf, 32);
printf("%s", buf);
return 0;
}
카나리 릭을 테스트할 C 코드이다.
위의 코드와 동일하지만
입출력이 버퍼를 사용하지 않고 직접적으로 이루어지도록 하는 함수만 추가했다.
어셈블리를 확인해보자
0x00000000004011fb <+0>: endbr64
0x00000000004011ff <+4>: push rbp
0x0000000000401200 <+5>: mov rbp,rsp
0x0000000000401203 <+8>: sub rsp,0x10
0x0000000000401207 <+12>: mov rax,QWORD PTR fs:0x28
0x0000000000401210 <+21>: mov QWORD PTR [rbp-0x8],rax
0x0000000000401214 <+25>: xor eax,eax
0x0000000000401216 <+27>: mov eax,0x0
0x000000000040121b <+32>: call 0x401196 <setup_environment>
0x0000000000401220 <+37>: lea rax,[rbp-0x10] ; buf = rbp-0x10
0x0000000000401224 <+41>: mov edx,0x20 ; 32
0x0000000000401229 <+46>: mov rsi,rax ; buf
0x000000000040122c <+49>: mov edi,0x0 ; 0
0x0000000000401231 <+54>: call 0x401090 <read@plt> ; read(0, buf, 32)
0x0000000000401236 <+59>: lea rax,[rbp-0x10]
0x000000000040123a <+63>: mov rsi,rax
0x000000000040123d <+66>: lea rdi,[rip+0xdc0] # 0x402004
0x0000000000401244 <+73>: mov eax,0x0
0x0000000000401249 <+78>: call 0x401080 <printf@plt>
0x000000000040124e <+83>: mov eax,0x0
0x0000000000401253 <+88>: mov rcx,QWORD PTR [rbp-0x8]
0x0000000000401257 <+92>: xor rcx,QWORD PTR fs:0x28
0x0000000000401260 <+101>: je 0x401267 <main+108>
0x0000000000401262 <+103>: call 0x401070 <__stack_chk_fail@plt>
0x0000000000401267 <+108>: leave
0x0000000000401268 <+109>: ret
위의 코드를 확인하면 buf 는 rbp-0x10 이다
C 코드를 보면 buf 는 분명 8이다
그런데 어셈을 보면 buf 는 0x10 인 것을 알 수 있다.
이 차이는 스택을 보면 알 수 있다.
스택 프레임 구조를 살펴보자.
buf 는 8
64bit 아키텍처이기 때문에 canary 도 8이다
따라서 buf 는 0x10 , 16이 된다.
buf 에 8 만큼 dummy 값을 넣고
canary 의 첫 바이트인 NULL 값 1 만큼을 덮으면
canary 값이 출력될 것이다.
$ ./canary_leak
aaaaaaaaa
aaaaaaaaa
�_q�?g*** stack smashing detected ***: terminated
]Aborted (core dumped)
예상대로 카나리 값이 출력되었다.
깨져서 보이는 이유는 카나리 값에 대응하는 ASCII 코드가 없어서 그렇다.
'Hacking > Pwnable' 카테고리의 다른 글
공유 라이브러리(PLT, GOT) (0) | 2024.06.17 |
---|---|
NX-bit, ASLR (0) | 2024.05.10 |
셸코드 (Shellcode) (0) | 2024.04.29 |
스택 버퍼 오버플로우 (Stack Buffer Overflow) (1) | 2024.04.28 |
함수 호출 규약 (Calling Convention) (0) | 2024.03.27 |