Dreamhack | Linux Exploitation & Mitigation Part 3

133210·2021년 7월 22일
0

2020 시스템

목록 보기
9/25
post-thumbnail

1.1 SSP

Stack Smashing Protector. 메모리 커럽션 취약점 중 스택 버퍼 오버플로우 취약점을 막기 위해 개발된 보호 기법.

- 스택 버퍼와 스택 프레임 포인터 사이에 랜덤 값을 삽입하여 함수 종료 시점에서 랜덤 값 변조 여부를 검사함으로써 스택이 망가뜨려졌는지 확인함.
- SSP 보호 기법이 적용되어 있다면 함수에서 스택을 사용할 때 카나리가 생성됨. 마스터 카나리는 main 함수가 호출되기 전에 랜덤으로 생성된 카나리를 스레드 별 전역 변수로 사용되는 TLS(Thread Loal Storage)에 저장.
1) TLS: TLS 영역은 _dl_allocate_tls_storage 함수에서 __libc_memalign 함수를 호출하여 할당 됨.
TLS는 tcbhead_t 구조체를 가지는데, 다음과 같은 멤버 변수들이 존재.

2) security_init: _dl_setup_stack_chk_guard 함수에서 반환한 랜덤 카나리 값을 설정. THREAD_SET_STACK_GUARD 매크로는 TLS 영역의 header.stack_guard에 카나리의 값을 삽입하는 역할을 함.

3) _dl_allocate_tls_storage

1.2 master1.c

1) master1.c: 256바이트 배열 buf를 할당하고 read 함수를 통해서 입력받는 예제. SSP 보호기법이 적용되어 있고 지역변수를 사용하므로 main 함수에서 카나리를 삽입하고 검사하는 루틴이 존재
2) 코드

3) 카나리의 위치 찾기

0x8048485에서 eax 레지스터를 보면 카나리의 값이 0x4dc1e800인 것을 확인 가능.

find 명령어를 통해 카나리의 위치 찾기. master1 프로세스의 메모리 맵 중 TLS 영역인 0xf7dff000 – 0xf7e00000의 header.stack_guard에 카나리가 존재하는 것을 확인할 수 있음. 이는 gs:0x14를 접근함으로써 참조 가능

1.3 ssp와 no_ssp

1) 컴파일러에서 SSP 보호기법을 적용하는 경우 스택 배열을 사용하는 함수가 있으면 함수의 시작 부분과 끝 부분에 ssp.c와 같이 stack_guard 체크 코드가 삽입됨.
2) 코드

no_ssp.c

ssp

3) 디스어셈블리 결과

no_ssp.c

ssp

no_ssp의 디스어셈블리 결과와는 달리, ssp에서는 함수의 프롤로그와 에필로그에 스택 카나리 검증 루틴이 추가됨.

4) 스택 버퍼 오버플로우 취약점 트리거

no_ssp.c

ssp

no_ssp 바이너리의 경우 Segmentation fault 예외가 출력되며 프로그램이 비정상 종료되는 반면, ssp 바이너리는 __stack_chk_fail 함수가 호출되어 “stack smashing detected” 문자열을 출력하며 프로그램이 종료됨.

1.4 Bypassing SSP-1 (example6.c)

1) SSP 보호 기법을 우회하기 위해서는 스택 메모리에 존재하는 스택 카나리의 값을 변조시키지 않은 채로 익스플로잇 해야 함.
2) example6.c: main 함수에서 read 함수를 호출할 때, buf의 크기보다 더 큰 크기를 입력받아 스택 버퍼오버플로우가 두 번 발생함. 그러나 example6 바이너리에는 SSP가 설정되어 있기 때문에 스택 카나리의 값을 알아내지 못한다면 스택 버퍼 오버플로우 취약점만으로는 실행 흐름을 조작할 수 없음.
3) 코드

%s 포맷 스트링을 이용해 buf의 내용을 출력하는 코드 존재
printf 함수의 %s 포맷 스트링은 NULL 바이트를 만날 때까지 출력해줌. 하지만 buf 배열의 끝이 NULL 바이트가 아니라면 buf 배열 밖의 메모리까지 출력할 수 있게 됨.

ex. buf 배열의 끝이 NULL 값이 아니여서 secret 버퍼의 내용 출력 가능

4) 스택 카나리 + 1부터 buf까지의 오프셋 구하기

main disassemble 결과

read 함수를 호출하는 부분에 break point 설정하고 실행

스택 카나리 + 1 부터 buf 까지의 오프셋은 0x21

5) 스택 카나리의 값을 구하는 파이썬 스크립트인 example6_leak.py를 실행하여 스택 카나리의 값 출력 가능.

gdb를 이용해 give_shell 함수의 주소 구하기.

리턴 주소를 give_shell 함수의 주소인 0x804854b로 바꾸어 셸을 획득하는 파이썬 스크립트인 exaple6.py 실행하여 셸 획득

1.5 Bypassing SSP - 2

1) ssp_fork1.c
- ssp_fork1.c: fork 함수를 사용하여 자식 프로세스를 생성하는 코드. fork 함수는 부모 프로세스의 TLS 영역과 스택 메모리 등을 복제해 자식 프로세스를 생성함. 따라서 부모와 자식 프로세스의 스택 카나리 값은 동일.

코드

부모 프로세스와 자식 프로세스의 카나리 값 확인: 부모 프로세스와 자식 프로세스의 스택 카나리 값이 동일하다는 것을 알 수 있음.

부모 프로세스

자식 프로세스

2) ssp_server.c
- ssp_server.c: fork를 이용한 서버 프로그램. 31337번 포트에 TCP 서버를 연 후 클라이언트의 연결이 들어오면 자식 프로세스를 생성한 후 handler 함수를 호출함. handler 함수에서는 32 바이트 버퍼에 1024 바이트 입력을 클라이언트로부터 받기 때문에 스택 버퍼 오버플로우가 존재함. 그러나 ssp_server에서는 SSP 보호기법이 적용되어 있기 때문에 스택 버퍼 오버플로우를 익스플로잇 하기 위해서는 SSP를 우회해야 함.
부모 프로세스에서는 자식 프로세스의 시그널을 처리하는 루틴이 없기 때문에 자식 프로세스에서 SIGSEGV나 SIGABRT 예외가 발생해도 부모 프로세스는 종료되지 않음. 만약 handler 함수가 정상적으로 리턴된다면 bye 문자열을 출력하고, 스택 카나리 검사가 실패해 자식 프로세스가 SIGABRT 예외로 종료된다면 bye 문자열을 출력하지 않음.
bye 문자열의 출력 유무를 통해 스택 카나리 검사를 통과했는지 알 수 있다는 점과, 새로운 연결로부터 생성된 자식 프로세스와 부모 프로세스의 스택 카나리와 같다는 점을 이용하면 브루트 포싱 공격을 통해 스택 카나리의 값을 한 바이트씩 알아낼 수 있음.
- ssp_server.py: 브루트 포싱 공격을 통해 ssp_server의 스택 카나리 값을 알아낸 후 handler 함수의 리턴 주소를 critical 함수의 주소인 0x80486db로 덮어 critical 함수를 호출하는 공격 코드.

ssp_server.py의 실행 결과
critical 함수가 호출되어 critical_msg가 출력된 것을 확인 가능

1.6 SSP 설정 여부 확인

1) SSP가 적용되어 있는 바이너리의 디스어셈블러 결과를 보면, 스택 카나리가 변조되었을 때 함수의 에필로그에서 stack_chk_fail 함수를 호출하는 코드를 확인할 수 있음.
2) stack_chk_fail: 표준 라이브러리인 libc.so.6에 존재하는 함수이기 때문에, 바이너리에 이 함수의 심볼이 존재하는지 확인하는 것만으로도 SSP 적용 여부를 확인할 수 있음.
3) readelf를 이용해 확인 가능. SSP가 적용되지 않은 no_ssp 바이너리는 stack_chk_fail이 없는 반면 SSP가 적용되어 있는 ssp 바이너리에는 stack_chk_fail 심볼이 존재하는 것을 확인 가능

2.1 RELRO: Relocation Read-Only

1) Lazy Binding: 함수가 처음 호출될 때 주소를 찾는 방식. GOT에 사용되는 방식. Lazy Binding을 할 때는 프로그램이 실행되고 있는 도중 GOT에 라이브러리 함수의 주소를 덮어써야 하기 때문에 GOT에 쓰기 권한 필요. GOT에 값을 쓸 수 있다는 특징 때문에 이전 장의 GOT Overwrite와 같은 공격 가능. 하지만 Relocation Read-Only(RELRO) 보호기법이 설정되어 있으면 GOT와 같은 다이나믹 섹션이 읽기 권한만을 가지게 됨.

2.2 Bypassing RELRO(example7.c)

1) example7.c: 1번 메뉴에서는 임의 주소 읽기가, 2번 메뉴에서는 임의 주소 쓰기가 가능한 것을 확인 가능.
2) 코드

3) system 함수의 주소를 구하기 위해 libc.so.6 라이브러리 주소를 릭해봄. RELRO 보호 기법이 적용되어 있다 하더라도 GOT에 라이브러리 주소가 저장되어 있기 때문에 GOT를 이용하면 libc.so.6 라이브러리의 주소를 구할 수 있음.

  • menu 함수에 있는 puts@plt로부터 puts@got의 위치 찾기
  • puts@got의 주소는 0x8049fec임을 알 수 있음.

4) example7_leak.py: 1번 메뉴에 puts의 GOT 주소인 0x8049fec를 입력하여 puts 함수의 주소를 구하는 코드. arb_read 함수를 구현하여 1번 메뉴의 사용을 구현. 실행하면 puts 함수의 주소가 출력되는 것을 볼 수 있음.

5) 2번 메뉴의 기능인 arbitrary write를 통해 실행 흐름 조작.

example7의 메모리 권한

GOT에 쓰기 권한이 없음을 확인 가능 그러나 RELRO 보호기법이 적용되어 있더라도 스택 등 동적으로 데이터를 써야하는 메모리에는 쓰기 권한이 존재함. 2번 메뉴인 임의 주소 쓰기 기능을 이용해 main 함수의 리턴 주소, 즉 스택 메모리를 덮어 써 example7 바이너리를 익스플로잇 하기.
6) 스택의 주소 구하기
- libc.so.6 라이브러리의 전역 변수에는 프로그램의 argv, 스택 메모리 주소가 존재. gdb의 find 명령어를 통해 main 함수의 두번째 인자인 argv 주소를 libc.so.6 라이브러리에서 찾아봄.

main 함수에 break point를 설정하고 라이브러리에서 argv 포인터를 검색해 라이브러리의 베이스 주소부터 argv 포인터 위치까지의 오프셋 계산. 계산 결과 라이브러리의 베이스 주소에서부터 0x1b55f0만큼 떨어진 곳에 argv, 스택 포인터가 존재한다는 것을 알 수 있음

argv 주소부터 main 함수의 리턴 주소까지의 오프셋 계산
argv 주소부터 0x98만큼 떨어진 위치에 main 함수의 리턴 주소가 존재함을 알 수 있음.

system 함수와 “/bin/sh” 문자열의 주소는 각각 라이브러리 베이스 주소로부터 0x3ada0, 0x15ba0b만큼 떨어져 있음을 알 수 있음.

7) example7.py: example7에 대한 익스플로잇 코드. arb_write 함수를 선언해 2번 메뉴의 사용 구현. arb_write를 이용해 main 함수의 리턴 주소를 system 함수 주소로, main의 리턴 주소+8을 “bin/sh” 문자열의 주소로 덮은 후 0번 메뉴로 main 함수를 리턴시켜 셸 획득

2.3 RELRO 설정 확인 방법

No RELRO: 바이너리에 RELRO 보호기법이 아예 적용되어 있지 않은 상태
Partial RELRO: .init_array나 .fini_array 등 non-PLT GOT에 대한 쓰기 권한을 제거한 상태
Full RELRO: GOT 섹션에 대한 쓰기 권한까지 제거해 .bss 영역을 제외한 모든 바이너리 섹션에서 쓰기 권한이 제거된 상태

1) readelf를 이용하면 ELF 바이너리에 설정된 RELRO 보호기법 체크 가능
2) Full RELRO는 해당 바이너리가 Now Binding을 하는지 Lazy Binding을 하는지에 대한 검사를 통해 확인 가능. 간단하게 readelf -a의 출력 결과에 BIND_NOW 문자열을 grep 해봄으로써 체크 가능
3) 만약 바이너리의 readelf 출력 결과에 BIND_NOW 문자열이 없으면 GNU_RELRO 문자열의 검사를 통해 Partial RELRO 적용 여부 확인 가능
4) readelf -a 출력 결과에서 두 문자열 모두 존재하지 않는다면, 해당 바이너리는 RELRO 보호기법이 적용되어 있지 않다고 볼 수 있음

3.1 PIE: Position Independent Executable

1) PIE: 바이너리가 로딩될 때 랜덤한 주소에 매핑되는 보호기법.
2) 원리는 공유 라이브러리와 비슷함. 컴파일러는 바이너리가 메모리 어디에 매핑되어도 실행에 지장이 없도록 바이너리를 위치 독립적으로 컴파일. 이는 결국 코드 영역의 주소 랜덤화를 가능하게 해줌. PIE가 설정되어 있으면 코드 영역의 주소가 실행될 때마다 변하기 때문에 ROP와 같은 코드 재사용 공격을 막을 수 있음
3) no_pie.c & pie.c: 함수의 주소를 출력해주는 소스코드

no_pie.c와 pie.c의 C 코드는 동일.

PIE 보호 기법이 적용되어있지 않은 no_pie의 main 함수 주소는 0x804840b로 일정하지만 pie의 main 함수 주소는 실행될 때마다 바뀜.
PIE가 설정되어 있으면 코드, 힙, 라이브러리, 스택 등 모든 메모리 영역의 주소가 랜덤화 됨.

3.2 Bypassing PIE (example8.c)

1) PIE 보호기법을 우회하기 위해서는 코드 영역의 주소를 알아내야함. PIE 보호기법이 설정되어 있을 때 코드 영역은 공유 라이브러리처럼 메모리에 로딩되기 때문에 libc.so.6 라이브러리 주소를 구하는 과정과 같이 특정 코드 영역의 주소를 알아낸다면 코드 영역 베이스 주소를 구할 수 있음. 코드 영역 베이스 주소를 구한다면, 오프셋 계산을 통해 코드나 데이터 영역의 주소를 구할 수 있음.
2) example8: PIE 보호기법을 설정함.

두가지 취약점 존재.
line 13, line 18에 존재하는 스택 버퍼 오버플로우 취약점과 line 15에 존재하는 포맷 스트링 버그 취약점

vuln 함수 디스어셈블 시 주소가 오프셋 형태로 출력되는 것을 확인할 수 있음.

3) 바이너리에는 셸을 실행시켜주는 give_shell 함수가 존재하는 것을 확인 가능. 포맷 스트링 버그를 이용해 give_shell 함수의 주소를 구한 후, 스택 버퍼 오버플로우 취약점으로 리턴 주소를 give_shell 함수의 주소로 덮어 셸을 실행시키기.

print(buf)를 실행하는 시점(두번째 printf 함수 호출)에 브레이크 포인트를 설정함.

x/40wx $esp로 출력된 메모리는 printf(buf)를 실행하는 시점의 스택 메모리. 0xffffd080에 바이너리 코드 영역의 주소인 0x565557be이 저장되어 있는 것을 확인 가능. gdb로 확인하면 0x565557be가 vuln 함수의 리턴주소인 main+66의 주소임을 알 수 있음.

포맷 스트링 버그를 이용하여 0x61616161이 저장되어 있는 0xffffd528이 첫번째 포맷 스트링 인자 위치이므로 11번째 포맷에서 스택에 저장된 0x565557be 출력 가능
11번째 “%x”의 결과로 565557be가 출력되어 바이너리의 코드 주소를 알아냄.

give_shell의 주소 계산.
give_shell의 주소 = 0ㅌ565557be – 0xee임을 확인 가능

3) example8_leak.py: give_shell 함수의 주소를 구하는 파이썬 스크립트

실행 시 give_shell 함수의 주소가 출력됨

4) example8.c의 스택 오버플로우 취약점을 이용해 vuln 함수의 리턴 주소를 give_shell 주소로 덮어 셸 실행하기
buf로부터 리턴 주소까지의 오프셋은 40 바이트이므로 최종 공격 페이로드는 다음과 같음

“A” * 40 + give_shell

5) example8.py: 익스플로잇 코드

셸이 획득되는 것을 확인 가능

3.3 PIE가 설정되어 있는지 확인하는 방법

1) readelf를 이용해 바이너리의 type header를 검사하는 것으로 바이너리의 PIE 적용 여부 체크 가능

type header의 경우 일반적인 실행 파일은 EXEC, 라이브러리와 같은 shared 파일은 DYN 값을 가짐.
PIE가 적용되어 있지 않은 바이너리의 타입은 EXEC인 반면, PIE가 적용되어 있는 바이너리의 타입은 DYN임을 확인 가능

0개의 댓글