✔️ Bypass RELRO
저번 시간에 함수가 처음 호출될 때 함수의 주소를 구하고,
이를 got에 적는 Lazy Binding을 봤었는데 바이너리의 실행 중 got 테이블을
업데이트할 수 있어야 하므로 got에 쓰기 권한이 부여됐다.
이러한 내용은 바이너리를 취약하게 만드는 원인이 되는데,
이에 더해 elf의 데이터 세그먼트에 프로세스 초기화 및 종료와 관련된
.init_array, .fini_array가 있다. 해당하는곳을 조작하여 프로세스 흐름을 바꿀 수 있다.
이러한 문제를 해결하고자 프로세스의 데이터 세그먼트를 보호하는
RELocation Read-Only(RELRO)가 나오게 되었는데,
RELRO란 쓰기 권한이 불필요한 데이터 세그먼트에 쓰기 권한을 제거한다.
또한, RELRO를 적용하는 범위에 따라 부분적으로 적용하면 Partial RELRO이고,
가장 넓은 영역에 RELRO를 적용하는 것이 Full RELRO이다.
실습 환경에서 gcc는 Full RELRO를 기본적으로 적용하고,
PIE를 해제하면 Partial RELRO를 적용한다.
해당 적용 여부는 checksec으로 검사할 수 있다.
해당 내용처럼 RELRO가 부분 적용되어 있는 것을 볼 수 있다.
해당 예는 자신의 메모리 맵을 출력하는 바이너리의 소스 코드이다.
해당 프로그램을 실행해보면 0x601000부터 602000까지의 주소에
쓰기 권한이 있는 것을 확인해볼 수 있다.
그리고 해당 파일의 섹션 헤더를 참조해보면
해당 영역에 .got.plt, .data, .bss가 할당되어 있는 것을 볼 수 있다.
해당 섹션들에는 쓰기가 가능하다.
(601000, 601030, 601040)
반면에 .init_array와 .fini_array는 600e10과 600e20에
할당되어 있으므로 쓰기가 불가능하다.
여기서 나오는 .got와 .got.plt 섹션 두 개가 존재하는데,
전역 변수 중 실행되는 시점에 바인딩되는 변수는 .got에 위치한다.
바이너리가 실행될 때는 이미 바인딩이 완료되어있으므로
해당 영역에 쓰기 권한을 부여하지 않게 된다.
하지만, 실행 중에 바인딩(lazy binding)되는 변수는
.got.plt에 위치하여 실행 중에 값이 써져야 하므로 쓰기 권한이 부여된다.
Partial RELRO가 적용된 바이너리에서는 대부분 함수들의 GOT 엔트리는
.got.plt에 저장된다.
RELRO를 해제하지 않은 기본 컴파일은 Full RELRO가 적용된다.
그 다음 frelro를 실행하여 메모리 맵을 확인해보자.
두 사진의 내용을 종합해보면, got에는 쓰기 권한이 제거되어 있고
data와 bss 영역에서만 쓰기 권한이 있다는 것을 확인할 수 있다.
Full RELRO가 적용되면 라이브러리 함수들의 주소가
바이너리 로딩 시점에 모두 바인딩되어 got에 쓰기 권한 부여되지 않는 것이다.
이러한 RELRO 기법을 우회하기 위해서는
Partial RELRO의 경우에 .init_array와 .fini_array에 대한 쓰기 권한이 제거되어
두 영역을 덮어 쓰는 공격을 수행하기 어려워지는 대신 .got.plt 영역에 대한
쓰기 권한이 존재하여 GOT overwrite 공격을 활용할 수 있다.
Full RELRO의 경우에는 .init_array와 .fini_array, .got 영역 모두 쓰기 권한이 제거되어
공격이 어려워졌다. 그래서 공격자들은 덮어쓸 수 있는 다른 함수 포인터를 찾다가
라이브러리에 위치한 hook을 찾아냈다고 한다.
라이브러리 함수의 대표적인 hook이 malloc hook과 free hook인데,
해당 함수 포인터는 동적 메모리 할당과 해제 과정에서 발생하는
버그를 디버깅하기 쉽게 하려고 만들어졌다.
malloc 함수의 경우에는 __malloc_hook이 존재하는지 if문을 통해 검사하고,
존재하면 이를 호출하는데 해당 __malloc_hook은 libc.so에서
쓰기 가능한 영역에 위치한다. 그래서 공격자는 libc가 매핑된 주소를
알 때 이 변수를 조작하고 malloc을 호출해 실행 흐름을 조작할 수 있다.
이러한 공격 기법을 Hook Overwrite라고 부른다.
🔰 퀴즈
1) O
2) X
3) O
4) X
✔️ Bypass PIE
ASLR이 적용되면 바이너리가 실행될 때마다 스택과 힙과 공유 라이브러리 등이
무작위 주소에 매핑되므로 공격에 어려워 진다.
하지만, main 함수의 주소는 매번 같은 특징을 활용해 공격자는 고정된 주소의
코드 가젯을 활용한 ROP를 수행할 수 있었다.
이번 코스에서 배울 것은 Position-Independent Executable(PIE)로
ASL이 코드 영역에도 적용되게 해주는 기술이다.
이 기술은 보안성 향상을 위해 도입된 것이 아니라서 엄밀하게는 보호 기법이 아니지만,
실제로는 ASLR과 맞물려서 공격을 어렵게 들었기에 보호 기법이라고 소개된다.
리눅스에서 ELF는 실행 파일과 공유 오브젝트(SO)로 존재하는데
실행 파일은 일반적인 실행 파일에 해당되고 공유 오브젝트는 지난번 풀때 제공된
libc-2.27.so와 같은 라이브러리 파일에 해당한다.
공유 오브젝트는 기본적으로 재배치(Relocation)가 가능하도록 설계되어 있다.
재배치가 가능하다는 것은 메모리의 어느 주소에 적재되어도 코드의 의미가
훼손되지 않음을 의미하는데 이러한 것을 컴퓨터 과학에서는
Position-Independent Code(PIC)라고 부른다.
그리고 gcc는 PIC 컴파일을 지원한다.
이번 실습에서 사용하는 pic 예제이다.
no_pic에서 main 함수의 printf에 전달하는 문자열을 보면 pic와 전달 방식이
조금 다른 것을 확인할 수 있는데 위에서 보면 no_pic에서는 0x4005a1이라는
절대 주소로 문자열을 참조하고 있다.
반면에 pic에는 문자열 주소를 rip+0xa2로 참조하고 있음을 확인할 수 있다.
바이너리가 매핑되는 주소가 바뀌면 0x4005a1에 있던 데이터도 함께 이동하기 때문에
no_pic 코드는 제대로 실행되지 못한다. 그러나 pic 코드는 rip를 기준으로 데이터를
상대 참조하기 때문에 바이너리가 무작위 주소에 매핑돼도 제대로 실행될 수 있다.
PIE은 무작위 주소에 매핑돼도 실행 가능한 실행 파일을 의미한다.
이미 널리 사용되는 실행 파일의 형식을 변경하면 호환성 문제가
발생할 것이므로 개발자들은 원래 재배치가 가능했던 공유 오브젝트를 실행 파일로
사용하기로 했고 실제로 /bin/ls는 공유 오브젝트 형식을 띄고 있다.
PIE는 재배치가 가능하므로, ASLR이 적용된 시스템에서 실행 파일도 무작위 주소에
적재되는데, PIE가 적용된 프로그램의 실행결과를 보면 main 함수의 주소가
매 실행마다 바뀌고 있음을 확인할 수 있다.
이러한 PIE를 우회하기 위해서는 코드 베이스를 구하는 방법과
Partial Overwrite라는 기법이 존재한다.
코드 영역의 가젯을 사용하거나 데이터 영역에 접근하려면 바이너리가 적재된
주소를 알아야 하는데, 이 주소를 PIE 베이스 또는 코드 베이스라고 부른다.
코드 베이스를 구하려면 라이브러리의 베이스 주소를 구할때처럼 코드 영역의 임의 주소를 읽고,
그 주소에서 오프셋을 빼는 ROP 과정과 비슷하게 진행된다.
Partial Overwrite는 코드 베이스를 구하기 어려울 때 반환 주소의 일부 바이트만
덮는 공격을 의미하는데 일반적으로 함수 반환 주소가 호출 함수의 내부를 가리킬 때
특정 함수의 호출 관계를 정적 또는 동적 분석으로 쉽게 확인할 수 있으므로
공격자는 반환주소를 예측할 수 있게 된다.
ASLR 특성 상, 코드 영역의 주소도 하위 12비트 값은 항상 같기 때문에
코드 가젯 주소가 반환 주소와 하위 한 바이트만 다르다면,
해당 값만 덮어서 원하는 코드를 실행시킬 수 있다.
하지만 만약 두 바이트 이상이 다른 주소로 실행 흐름을 옮기고자 하면,
ASLR로 뒤섞이는 주소를 맞춰야하기 때문에 브루트 포싱이 필요하고
공격이 확률에 따라 성공하게 될 것이다.
🔰 퀴즈
1) O
2) O
3) B
✔️ Hook Overwrite (함께)
운영체제가 어떤 코드를 실행하려 할 때 갈고리(Hook)를 던져 낚아채어
다른 코드가 실행되게 하는 것을 Hooking(후킹)이라고 하고 이때 실행되는
코드를 Hook(훅)이라고 부른다.
후킹은 굉장히 다양한 용도로 사용되는데 함수에 훅을 심어서 함수의 호출을
모니터링 하거나 함수에 기능을 추가할 수도 있고, 아예 다른 코드를 심어서
실행 흐름을 변조할 수도 있다.
예를 들어서 malloc과 free에 훅을 설치하면 소프트웨어에 할당하고 해제하는
메모리를 모니터랑 할 수 있고 이를 통해 실행 중 호출하는 함수를
모두 추적할 수도 있지만 이러한 기능은 해커에 의해 악용될 수 있다.
이번 코스에서 Hook Overwrite를 통해 malloc과 free 함수를 후킹하여
각 함수가 호출될 때 공격자가 작성한 악의적인 코드가 실행되게 하는 것을 배우고
Full RELRO가 적용되더라도 libc 데이터 영역에는 쓰기가 가능하므로
Full RELRO를 우회하는 기법으로도 활용할 수 있다.
C언어에서 메모리 동적 할당과 해제를 담당하는 함수로는 malloc과
free, realloc가 대표적인데 각 함수는 libc.so에 구현되어 있다.
libc에는 해당 함수들의 디버깅 편의를 위해서 훅 변수가 정의되어 있는데,
malloc 함수는 __malloc_hook 변수 값이 NULL이 아닌지 검사하고 malloc을 수행하기 전
__malloc_hook이 가리키는 함수를 먼저 실행한다. free, realloc도 각 훅 변수를 사용한다.
hook도 마찬가지로 libc.so에 정의되어 있다.
이 변수들의 오프셋은 각각 0x3ed8e8, 0x3ebc30, 0x3ebc28인데 섹션 헤더 정보를
참조하면 libc.so의 bss 섹션에 포함된 것을 확인할 수 있고
해당 섹션은 쓰기가 가능하므로 이 변수들은 값을 조작할 수 있다.
위에 나온 내용을 바탕으로 malloc, free, realloc에는 각각 대응되는
훅 변수가 존재하고 libc의 bss 섹션이 쓰기가 가능하므로 실행 중에 덮어쓰는 것이
가능하기 때문에 훅을 실행할 때 기존 함수에 전달할 인자를 같이 전달해주는 것을 통해
__malloc_hook을 system 함수의 주소로 덮고, malloc("/bin/sh")을 호출하여
셸을 획득하는 공격이 가능하게 된다.
해당 코드를 컴파일 하여 실행하게 되면,
다음과 같이 __free_hook을 system 함수로 덮고, bin/sh를 free 함수로 호출하여
셸이 획득되는 것을 확인할 수 있다.
이러한 것은 Full RELRO가 적용된 바이너리에도 라이브러리 훅에 쓰기 권한이
남아있으므로 공격을 시도해볼 수 있다.
실습 예제는 다음과 같다.
checksec 명령어를 통해 확인해보면 모든 보호 기법이 적용되어
있는 것을 확인해볼 수 있다.
다음과 같이 main은 disass 했을때 상당히 긴 과정이 출력된다.
위에 나온 코드를 보면 buf 공간은 0x30인데 read 함수에서 0x100만큼을 받아준다.
해당 공간에서 오버플로우가 발생하지만 정보가 많이 없으므로 앞선 카나리 우회와
RET 조작도 어려우므로 스택에 있는 데이터를 읽어오는데 사용할 수 있다.
그 다음 코드는 주소를 입력하면 해당 주소에 값을 작성할 수 있다.
그리고 나서 다음 주소를 입력하고 나면 해당 주소의 메모리를 해제할 수 있다.
각각 puts와 free 함수를 통해 이루어지는 과정이다.
이러한 과정에서 공격자는 스택의 어떠한 값을 읽고 임의의 주소에 임의의 값을 쓰고,
임의의 주소를 free 함수에서 해제함으로써 셸을 획득하는 과정을 거치게 된다.
이러한 정보를 획득한 후 gdb를 통해 분석해보면서
라이브러리 변수와 함수들의 주소를 구해보도록 한다.
앞선 명령어들의 주소를 구할 수 있는데, 메인 함수의 라이브러리 함수 호출을 의미하는
__libc_start_main도 확인할 수 있다. __free_hook, system, /bin/sh는 libc.so에 정의되어 있어
libc.so의 매핑된 주소를 구해서 위의 주소들을 계산하도록 한다.
그리고나서 __free_hook 값을 위에서 봣듯이 system 함수 주소로 덮어쓰고
/bin/sh를 해제하여 system("/bin/sh")가 될 수 있게 만들어준다.
so 파일에서 bin/sh만 offset값을 구해주었다.
그리고나서 구성해준 익스플로잇 코드를 통해 함수들의 주소를 구할 수 있었다.
순서대로 libc의 베이스 주소와 system, __free_hook, /bin/sh에 대한 주소이다.
구해낸 주소를 바탕으로 코드를 작성해서 셸을 획득해보자.
참고로 /bin/sh 에러가 난다면 b를 문자열 앞에 작성해주면서 실행시킬 수 있다.
접근하여 셸을 획득할 수 있다.
해당 내용과 더불어 one_gadget 또는 magic_gadget을 실행하면 셸을 획득하는
코드 뭉치에 대한 내용도 코스에서 배울 수 있는데, 해당 one_gadget 도구를 사용해서
libc에서 쉽게 one_gadget을 찾을 수 있다고 한다.
해당 명령어를 통해서 제약 조건을 만족하는 one_gadget을 찾아내고
이를 호출해서 셸을 획득하는 방법도 사용할 수 있다.
위에서 libc.rip에서 본 libc_start_main_ret 주소를 libcbase에 적어주고,
one_gadget으로 찾은 0x4f302를 대입했을 때 페이로드로 og를 보내주면
이후에 덮어쓰는 값이 어떤 값이 와도 셸을 실행시킬 수 있게 된다.
정상적으로 실행이 가능하다.
process부분을 remote로 수정하고 파일 다운로드 후 libc.so파일을
폴더내부에 옮겨주고 해당 파일을 ELF 경로로 설정하면 정상적으로 플래그를 획득한다.
✔️ oneshot
문제 정보는 다음과 같다.
이 문제는 작동하고 있는 서비스(oneshot)의 바이너리와 소스코드가 주어집니다.
프로그램의 취약점을 찾고 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.
해당 문제의 소스코드는 다음과 같다.
msg 공간 16만큼의 크기를 받고 출력하는 함수와
read 함수를 사용하고 check가 0 이상이면 exit 함수를 실행한다.
해당 파일은 부분 RELRO가 적용되어있고, 카나리없이 NX가 적용된 상태이다.
main 함수를 보면 다음과 같다.
main 함수에서 main 75 이후 main+80부터 보게되면,
read함수에 edi에는 0, edx에는 0x2e를 담고 메세지를 보내는 것을 확인할 수 있따.
그리고나서 cmp 구문을 통해 8만큼의 공간에서 0과 비교하여
check가 0이면 main+119로 점프한다.
8만큼을 ret, rbp, check 순서대로 공간을 차지하고 msg에는 rbp-0x24에서 보듯이
16만큼의 공간에 8만큼의 공간만큼을 할당해주어야 정상적으로 익스플로잇이 성공할 것이라 생각했다.
해당 문자열을 출력하는 주소 0xb7d, 0xb89, 0xb8f도 확인해볼 수 있다.
gdb를 분석하면서 따라가다보면 __libc_start_main 부분을 확인할 수 있다.
/bin/sh의 offset은 18cd57이라는 것을 확인할 수 있다.
해당 프로그램은 stdout에 buf 주소를 출력하고 msg에 작성해준 문자열을 출력한다.
그렇다면 페이로드는 해당 stdout을 recvuntil을 통해 뒤에 오는 주소를 받아주고,
해당 주소에서 stdout 심볼을 뺀 나머지를 베이스 주소로 설정한다.
그 다음 가젯 offset 만큼을 더한 가젯 주소를 찾아서 앞에 msg 16만큼은 더미값으로,
check 8만큼은 0으로 처리해야하기 때문에 0으로, 뒤에 rbp 8만큼은 더미값으로
마지막 ret는 가젯 주소로 페이로드를 작성해서 보내면 될 것이다.
그러기 위해서 가젯 주소를 미리 찾은 다음에 익스플로잇 코드를 구상해보자.
다음과 같이 0x45216이라는 가젯 주소를 획득할 수 있다.
그리고 stdout의 심볼은 _IO_2_1_stdout_ 이라는 것을 검색을 통해 알게 되었다.
(참고: https://xerxes-break.tistory.com/302)
위에서 설명한대로 가젯 주소를 적고, stdout 다음에 오는 주소에서 stdout 만큼을 빼서
base 주소를 구한뒤 가젯 주소를 다시 작성해줌으로써 ret에 가젯 주소의 "/bin/sh"를
호출하여 셸을 정상적으로 실행시킬 수 있다.
정상적으로 플래그를 획득할 수 있다.
✔️ hook
문제 정보는 다음과 같다.
이 문제는 작동하고 있는 서비스(hook)의 바이너리와 소스코드가 주어집니다.
프로그램의 취약점을 찾고 _hook Overwrite 공격 기법으로 익스플로잇해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.
문제에서 주어진 해당 코드와 보호기법을 살펴보자.
이전 문제와 비슷하게 stdout을 출력해주는 것을 확인할 수 있다.
이전과는 다르게 Size를 받아주고 있고 이것을 malloc 함수로 할당한다.
그리고 ptr을 선언하고 free에 담아주는 것을 확인할 수 있고 system으로
셸을 실행할 수 있도록 선언되어 있다.
전 문제와 동일하게 stdout 주소를 통해서 base 주소를 stdout 심볼만큼을 빼서 구하고,
보호기법을 살펴보면 Full RELRO가 적용되어 있기 때문에
free hook의 심볼을 베이스 주소에 더해주어서 해당 훅을 사용하도록 하자.
그리고 가젯 주소를 구해서 페이로드에 같이 작성해주도록 한다.
가젯 주소는 이전과 동일한데, 놓칠수도 있는 부분이 그냥 ptr이 아닌 ptr+1이 선언되어 있기 때문에
확보된 공간만큼이 더해진 가젯 주소를 사용해야 한다. 해당 주소를 정확하게 가리키면서
ptr+1이 system("/bin/sh") 값을 가리키도록 바꾸어준다.
rpb - 0x10이 ptr 주소를 가리키고 있을 때 rax = ptr+8이 되어 ptr[1]을 가리키게 되고
ptr은 ptr+8의 포인터를 가리키기 때문에 ptr[1]을 가리키게 된다.
이전처럼 동일하게 첫번째 가젯 주소를 사용하다가는 삽질할 수 있다..
그래서 0x45216이 아닌 0x4526a를 가젯으로 설정해두고 익스플로잇 코드를 구성해야 한다.
그리고 나서 Size를 통해 입력받는 크기는 해당 이후에 올 페이로드를 등을 고려하여
큰 값인 400을 사용하여 보내고 Data: 뒤에 페이로드를 보내주어 익스플로잇을 시도한다.
정상적으로 플래그를 획득할 수 있다.
화이팅 💪
'CTF > 시스템해킹' 카테고리의 다른 글
[Dreamhack] Format String Bug (0) | 2022.10.29 |
---|---|
[Dreamhack] Out of bounds (0) | 2022.10.28 |
[Dreamhack] Bypass NX & ASLR (0) | 2022.10.25 |
[Dreamhack] Stack Canary (0) | 2022.10.23 |
[Dreamhack] Stack Buffer Overflow (0) | 2022.10.22 |