✔️ Stack Canary 공부
이번 코스에서는 스택 버퍼 오버플로우로부터 반환 주소를 보호하는 스택 카나리(Stack Canary)에
대해서 배워본다. 스택 카나리는 함수의 프롤로그, 도입부에서 스택 버퍼와 반환 주소 사이에
임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조롤 확인하는 보호 기법이다.
카나리 값의 변조가 확인되면 프로세스는 강제로 종료된다.
스택 버퍼 오버플로우로 return address를 덮으려면 반드시 카나리를 먼저 덮어야 하므로
카나리 값을 모르는 공격자가 반환 주소를 덮을 때 카나리 값을 변조하게 되어
공격자는 실행 flow를 획득하지 못하게 된다.
위의 예제 코드에서는 스택 버퍼 오버플로우 취약점이 존재하는데,
길이가 긴 입력을 주게 되면 반환 주소가 덮여서 세그먼테이션 폴트 에러가 발생하게 된다.
-fno-stack-protector 없이 컴파일을 하여 카나리를 적용하게 되면,
위와 다르게 stack smashing detected와 Aborted라는 에러를 발생하게 되는데
해당 에러는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제로 종료된 것이다.
canary 파일을 gdb로 들어가서 main+8 주소에 중단점을 걸고 실행시키면 다음과 같다.
fs:0x28의 데이터를 읽어서 rax에 저장하는데, fs는 목적이 정해지지 않아 운영체제가
임의로 사용할 수 있는 레지스터로 Thread Local Storage를 가리키는 포인터로 사용된다.
여기에서는 fs:0x28에 랜덤 값을 저장하게 된다.
다음 지점에서 rax 값을 확인해보면 랜덤 값이 생성된것을 볼 수 있다.
그리고나서 다음 지점을 확인해보면 rbp-0x8에 rax 값이 저장된 것을 확인할 수 있다.
main+50에 rbp-8에 저장한 카나리를 rcx로 옮기고 +54에서 fs:0x28에 저장된
카나리와 xor을 하는데, 해당 연산 결과가 같으면 0이되면서 main 함수가 정상적으로 반환된다.
하지만, 두 값이 동일하지 않으면 __stac_chk_fail이 호출되면서 프로그램이 강제 종료된다.
최상단에 rax 값을 확인해보면 0x4848484848484848 값으로 rbp-0x8에 저장된 카나리 값이
버퍼 오버플로우로 인해서 바뀐 것을 확인할 수 있고 해당 연산 결과가 0이 아니므로
main+63에서 main+70으로 분기하는 것이 아니라 main+65로 분기하게 된다.
main+65로 분기하여 들어온 곳은 __stack_chk_fail에 해당하는 부분인 것을 확인할 수 있다.
이러한 카나리, 카나리의 값은 프로세스가 시작될 때 TLS에 전역 변수로 저장된다.
그리고 각 함수마다 프롤로그와 에필로그에서 이 값을 참조하게 된다.
위에서 보듯이 fs는 TLS를 가리키므로 fs의 값을 알면 TLS 주소를 알 수 있다.
하지만, 리눅스에서는 fs의 값은 특정 시스템 콜을 사용해야만 조회하거나
설정할 수 있어서 info register fs, print $fs와 같은 방식으로는 값을 알 수 없다.
그래서 여기에서는 fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr)
시스템 콜에 중단점을 설정하여 fs가 어떤 값으로 설정되는지 확인해본다.
해당 시스템 콜을 arch_prctl(ARCH_SET_FS, addr) 형태로 호출하면 fs의 값은 add로 설정된다.
gdb에는 특정한 이벤트가 발생하면 프로세스를 중지시키는 catch라는 명령어가 있는데
해당 명령어를 통해 arch_prctl에 catchpoint를 설정하고 실습에 사용했던
canary를 실행하도록 한다.
catchpoint에 도달하면 rdi의 값이 0x1002인데 해당 값은 ARCH_SET_FS의 상숫값을
나타낸다고 한다. rsi의 값이 0x7ffff7fe34c0이므로 해당 프로세스는 TLS를
0x7ffff7fe34c0에 저장할 것이고 fs는 이를 가리키게 된다.
rdi와 rsi에 해당하는 레지스터 주소를 확인하고 fs+0x28이므로
rsi의 값에 28만큼을 더해주는 곳을 보면 카나리가 저장될 곳에 아무 값도
설정되어 있지 않은 것을 확인할 수 있다.
TLS의 주소를 알게되어 gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때
프로세스를 중단시킬 수 있다. watch는 특정 주소에 저장된 값이 변경되면
프로세스를 중단시키는 명령어이다.
watchpoint 설정 이후에 프로세스를 계속 진행시키면
0xd2fd39179625d200 값이 카나리로 설정된 것을 확인할 수 있다.
실제로 해당 mov rax, QWORD PTR fs:0x28 연산으로 가서
rax 값을 확인해보면 해당 카나리 값이 들어간 것을 확인할 수 있다.
이러한 카나리 설정에 대해 해커들은 우회할 수 있는 방법들을 연구했고,
카나리를 우회하는 방법으로는 다음과 같은 것들이 존재한다.
① 무차별 대입 (Brute Force)
- x64 아키텍처에서는 8바이트의 카나리가, x86 아키텍처에서는 4바이트 카나리가 생성된다.
- 각각의 카나리에는 NULL 바이트가 포함되어 있어 실제로는 7바이트와 3바이트의 랜덤 값이다.
- 무차별 대입으로는 최대 256^7, 256^3의 연산이 필요한데 해당 공격으로 카나리의 값을 알아내는 것은
현실적으로 어려우면서 실제 서버를 대상으로 시도하는 것도 불가능하다.
② TLS 접근
- 위에서 본 것처럼 TLS에 전역변수로 저장되는 특성을 활용한다.
- TLS 주소는 매 실행마다 바뀌지만 만약 실행중에 TLS 주소를 알 수 있고,
임의 주소에 대한 읽기나 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나
이를 임의의 값으로 조작할 수 있게 된다.
- 그 뒤, 스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면
함수의 에플로그에 있는 카나리 검사를 우회할 수 있게 된다.
해당 예제는 스택 카나리 릭, 카나리 우회에 대한 실습 예제이다.
해당 예제의 입력을 받아보면 "AAAAAAAABBBBCCCCDDDDEEEEFFFFGGGG"
다음과 같은 값을 일정하게 넣어줄 때 memo에서 덮여서 hello까지 바뀌는 것을 확인할 수 있다.
근데 canary 주소번지에 해당 하는 값이 보이지 않고 나머지가 채워진 것을 볼 수 있는데
역으로 생각하면 DDDDEEEE에 해당하는 부분이 canary 주소인 것을 확인할 수 있고
해당 번지를 찾아내서 카나리 검사를 우회할 수 있을 것이다.
🔰 퀴즈
1) 카나리를 저장하는 주소는 fs+0x28 이다.
✔️ ssp_001
문제 정보는 다음과 같다.
이 문제는 작동하고 있는 서비스(ssp_001)의 바이너리와 소스코드가 주어집니다.
프로그램의 취약점을 찾고 SSP 방어 기법을 우회하여 익스플로잇해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.
해당 문제의 코드이다.
우선 눈에 들어오는 것은 get_shell 함수에서 셸 코드를 실행할 수 있는 것같다.
그리고 print_box에서는 인덱스 요소를 출력해주는 출력문이 보이고,
그 다음 메인 함수에서는 box, name, select, idx를 받아서 각각의 입력값을 받고
스위치문에 따라서 F, P, E에 따른 조건문을 수행하는 것으로 보인다.
보면은 중간에 menu라는 함수에서 3가지 문자열을 확인할 수 있다.
코드를 보게되면 scanf 함수 사용할 때 idx 변수의 크기를 확인하지 않아서
메모리 릭 취약점이 발생할 수 있고, Exit에서 크기와 값을 넣을 때
반환 주소 변조가 가능할 것으로 생각된다.
출력이 가능하고 입력이 가능하기 때문에
print the box에서 카나리를 확인하고 exit에서 get_shell을 싱행시켜보자.
우선 gdb로 확인했을 때는 arch_prctl syscall이 잡히지 않는다.
그 외에 각 함수에 대한 주소를 기록해놓는다.
메인함수에 중단점을 걸고 실행시켜보았다.
진행하다보면 gs:0x14를 eax에 넣는 것을 볼 수 있고 해당 값은 0x1826a400이라는 것을 확인할 수 있다.
그리고나서 다음 스텝에서 확인해보면 ebp-0x8에 해당 값이 들어간 것을 확인할 수 있다.
그리고 바래 아래 나오는 epb-0x88 부분이
혹시 몰라서 __stack_chk_fail 부분의 지점을 확인해보니 0xf7ee9b80이라는 곳이 나왔다.
해당 주소의 주변에는 이러한 로직이 존재하는 것을 볼 수 있다.
위에 나온 내용을 토대로 살펴보면 mov eax, dword ptr gs:0x14에 해당하는 부분과
mov dword ptr [ebp-8], eax에서 카나리 값을 스택에 저장하는 것을 확인할 수 있다.
그리고 stach_chk_fail에서 카나리 변조 여부를 체크하게 된다고 보여진다.
해당 로직에서 0x46이 F 0x50이 P 0x45가 E라는 것을 확인할 수 있다.
box의 크기를 알기 위해 진입해보면
해당 부분에서 push 0x40을 하고 ebp-0x88을 한 크기만큼을 가지게 되는 것을 볼 수 있다.
E에 해당하는 name 부분은 ebp-0x48 만큼의 크기를 갖는 것을 확인할 수 있다.
print_box가 실행된 이후를 살펴보면 mov, xor, jmp를 통해 카나리가 같은지 확인하는 부분을
찾아볼 수 있고 해당 dword ptr ebp-0x8부터 4바이트를 읽는 것을 확인할 수 있다.
이것은 32비트에서의 카나리 크기인 4바이트와 쓰레기값 4바이트 만큼을 채우고
그 다음 name, box, select 순으로 메모리에 할당되는 스택프레임을 생각할 수 있을 것이다.
대충 이런 느낌으로다가 메모리 구조를 확인할 수 있을텐데,
복귀 주소위에 쌓이는 값들 만큼을 페이로드에 넣어주고
복귀 주소에는 get_shell 주소를 가리키면서 쉘을 실행시킬 수 있을 것이다.
해당 익스플로잇 코드의 중간은 여러 사이트를 참고해서 만든 부분인데,
해석하면 다음과 같다.
canary 값을 읽는데 뒤에 payload를 작성할 때 p32비트 리틀 엔디언 패킹을 진행하기 때문에
0x83부터 0x7f까지 NULL을 끝으로 읽으면서 read(), scanf()함수가 진행되고
int 2의 값 만큼 카나리값을 가져오게 된다. 그리고나서 저장된 카나리를 16진수로 바꿔주고 저장한다.
그 다음부터는 40의 크기만큼을 더미값 0x40으로 채우고, 카나리를 채워준다음에
rdi와 sfp 크기만큼 다시 더미값으로 채우고 get_shell 함수를 실행하는 주소(위에서 확인한 0x80486b9)를
p32비트 패킹을 통해서 ret 주소를 변조하도록 만들어준다.
그리고나서 E가 실행될 때 전달되는 출력문 뒤에 보낼 함수를 적어주고 액티브한다.
(참고: https://dokhakdubini.tistory.com/236)
✔️ Return to Shellcode
해당 코스는 함께 실습을 통해서 셸 코드와 Return Address Overwrite를 이용해
셸을 획득하는 실습을 진행한다.
해당 프로그램을 실행하고 input값을 작성해주면 다음과 같이 강제로 종료된다.
코드를 자세하게 살펴보면 스택 버퍼인 buf에 두 번의 입력을 받는데
취약한 함수를 사용하고 있고 read함수에서는 해당 buf 크기보다 큰 크기를 받고,
gets는 취약한 함수이므로 해당 내용을 이용하여 셸을 획득해야 한다.
근데 이제 해당 값을 크기 안쪽으로 넣어주게 되면 강제 종료가 아닌 정상 종료를 하게 된다.
__builtin_frame_address에서 buf를 뺀 만큼의 rbp와 buf 크기가 96이라고 출력되는데,
카나리 릭을 위한 입력값을 넣어주는 것으로 보인다.
보호기법을 탐지하기 위해서 checksec을 설치하고 확인해보자.
canary found 라는 것을 확인할 수 있고, 해당 프로그램에 카나리가 적용되어 있음을 확인할 수 있다.
카나리가 조작되면 위에 처럼 강제로 프로그램이 종료되기 때문에,
카나리를 우선 첫 번째 입력에서 구하고, 두 번째 입력에서는 셸 획득을 위해
ret 주소를 덮어서 셸을 실행시킨다. 셸을 실행시키는 코드가 프로그램 내부에 존재하지 않기 때문에
셸을 획득하는 코드를 직접 삽입하고, 해당 주소로 실행 flow를 가리키면 될 것이다.
메인 함수는 다음과 같은 위치로 와서 확인할 수 있는데,
여기서 보면 fs:0x28이라는 64비트 체제의 카나리를 확인할 수 있고,
rbp-8 만큼을 가진다는 것을 확인할 수 있다.
조금 더 자세하게 살펴보기 위해서 main+229 주소까지 내려와보면,
카나리 값을 확인해서 __stack_chk_fail로 call을 호출하는 곳이 보이게 된다.
조금 더 실행해서 내려오게 되면 rbp-8 만큼의 영역에서
fs:0x28과 xor해서 같게 되면 je로 main+259로 가게 되는데 해당 카나리 값을 비교 연산해서
프로그램 종료에 대한 조건 연산을 수행하게 되는 것이다.
buf에 해당하는 주소와 sfp 및 canary 사이 크기 값을 구하기 위해서
pwntools를 활용하여 코드를 구현하게 되면,
0x7ffd3f735f60, 0x60, 0x58 이라는 값을 확인할 수 있다.
그리고 나서 payload 코드로 더미값을 채워주고 난후의 input 값 뒤에
payload를 보낸다음에 카나리 값을 확인하기 위해서 64비트 리틀엔디안 언패킹을 진행한 값에
데이터 7바이트 만큼의 값을 더해준 것을 hex 값으로 출력하게 되면 카나리가 나온다.
buf에 셸 코드를 삽입하고 카나리 값으로 덤픈 다음에 ret를 buf로 만들게 되면
셸 코드를 실행할 수 있다. 해당 과정에서 asm과 shellcraft를 사용하여 쉽게 셸 코드를
삽입하고, 페이로드에 스택 프레임 구조처럼 작성하는 방법을 배울 수 있다.
(기능 참고: https://koharinn.tistory.com/67)
마지막 페이로드 작성방법은 asm을 활용하게 되는데,
asm을 사용하게 되면 한번에 값을 세팅할 수 있게 된다.
shellcraft의 shell을 asm에 담아주는데, asm함수는 어셈블리 언어를 목적코드로 변환해주는
역할을 하게 된다고 한다. 그래서 페이로드의 앞 부분은 셸 코드 삽입을 위해 shellcraft를 이용하고,
ljust를 활용하여 카나리 크기만큼에 더미값을 채워준다.
그리고 64비트 패킹한 카나리와 64비트 sfp는 8바이트 크기를 가지기 때문에
더미값도 8바이트 크기만큼 채워주고 ret를 buf 주소로 향하게 만들어서 오버플로우를 일으킨다.
마지막으로 익스플로잇에 성공하게 되면 정상적으로 셸 코드가 주입되어
id, ls 등 셸 명령어를 실행할 수 있게 된다.
셸 코드를 삽입해서 실행하는 공격 기법은 코드를 삽입할 수 있는 임의의 버퍼가 존재하고
해당 버퍼의 주소를 알거나 구할 수 있다면 실행 흐름을 옮겨서 셸 코드를 실행시킬 수 있다.
컴퓨터 과학에서는 이러한 임의의 코드를 실행하는 것을 Arbitary Code Execution,
임의 코드 실행이라고 부르고 원격 서버를 대상으로 수행하면 RCE 라는 명칭을 갖게 된다고 한다.
해당 코스에서 함께 실습한 부분도 RCE 기법이라고 하는 것이다.
이러한 RCE 위험을 줄이기 위해서 코드 섹션 외 모든 섹션에 실행 권한을 없애는
NX(Not a eXecutable)가 있고, 임의 주소에 스택과 힙을 할당하는
ASLR(Address Space Layout Randomization)이 있다고 한다.
해당 실습에서는 -zexecstack 옵션으로 NX를 해제하여
buf에 삽입된 셸 코드를 실행할 수 있게 되었다.
위에서 작성한 해당 익스플로잇 코드에 remote로 문제 접속 정보를 연결하고
익스플로잇을 시도한 뒤 ls로 flag 파일을 확인하고 cat으로 내용을 확인하면 플래그가 출력된다.
pwntools 코드를 작성하는 데 아직 어려움이 있고,
여러가지 검색을 해가면서 작성하는 부분이 있는데
해당 부분에 익숙해지기 위해서 조금 더 공부해보고 다양한
포너블 문제를 풀어봐야할 것 같다.
화이팅 💪
'CTF > 시스템해킹' 카테고리의 다른 글
[Dreamhack] Bypass PIE & RELRO (0) | 2022.10.27 |
---|---|
[Dreamhack] Bypass NX & ASLR (0) | 2022.10.25 |
[Dreamhack] Stack Buffer Overflow (0) | 2022.10.22 |
[Dreamhack] Shellcode + shell_basic (0) | 2022.10.22 |
[Dreamhack] Tool Installation (0) | 2022.10.21 |