여태 공부했던 공격기법과 보호기법을 순서대로 정리해보면 다음과 같다.
Return Address Overwrite : 반환 주소를 악성함수의 주소로 덮어서 셸 획득
Stack Canary : 스택 프레임의 반환 주소 전에 랜덤한 카나리를 주입하여 반환 주소를 덮기 어렵게 함
Return to Shellcode : 카나리를 우회하고, 셸 코드를 주입한 버퍼의 주소로 반환 주소를 덮어서 셸 획득
ASLR : 임의 버퍼의 주소를 알기 어렵게 함
NX : 각 세그먼트에 불필요한 실행권한을 제거함으로써 공격자가 임의 버퍼에 주입한 코드를 실행하기 어렵게함
흐름을 살펴보면 공격 기법과 보호 기법이 서로 어떤 영향을 주고받으며 발전해왔을지 알 수 있다. 또한, 한편으론 NX의 등장으로 인해 이를 우회하는 공격 기법이 다시금 등장 했을 것이라 예상할 수 있다. 실제로 NX가 리눅스에 도입된 2004년 전후로 이를 우회하는 공격 기법이 여러 컨퍼런스에 발표됐다.
이번 강의에선 NX를 우회하는 RTL에 대해 공부한다.
NX로 인해 공격자가 버퍼에 주입한 셸 코드를 실행하기는 어렵지만, 스택 버퍼 오버플로우 취약점으로 반환 주소를 덮는 것은 여전히 가능했다.그래서 공격자들은 실행권한이 남아있는 코드 영역으로 반환 주소를 덮는 공격기법을 고안했다.
프로세스에 실행 권한이 있는 메모리 영역은 일반적으로 바이너리의 코드 영역과 바이너리가 참조하는 라이브러리의 코드 영역이다.
이 중, 공격ㄱ자들이 주목한 것은 다양한 함수가 구현된 라이브러리였다. 몇몇 라이브러리에는 공격에 유용한 함수들이 구현되어있다. 예를들어, 리눅스에서 C언어로 작성된 프로그램이 참조하는 libc에는 system
, execve
등 프로세스의 실행과 관련된 함수들이 구현되어 있다.
공격자들은 libc의 함수들로 NX를 우회하고 셸을 획득하는 공격 기법을 개발하였고, 이를 Return to Libc
라고 이름 지었다. 다른 라이브러리도 공격에 활용될 수 있으므로 이 공격 기법은 Return to Library
라고도 불린다. 유사한 공격 기법으로 Return To PLT가 있는데 이 공격 기법도 라이브러리의 코드를 사용하는것이 핵심이므로, 이 코스는 RTL의 하위 분류로 본다.
rtl.c의 7번째 줄은 "/bin/sh"를 코드 섹션에 추가하기 위해 작성된 코드이다. ASLR이 적용되도 PIE가 적용되지 않으면 코드 세그먼트와 데이터 세그먼트의 주소는 고정되므로, "/bin/sh"의 주소는 고정되어 있습니다.
rtl.c의 9번째 줄은 PLT에 system
을 추가하기 위해 작성된 코드이다. 지난 코스에서 배웠듯 PLT와 GOT는 라이브러리 함수의 참조를 위해 사용하는 테이블이다. 그 중 PLT에는 함수의 주소가 resolve되지 않았을때, 함수의 주소를 구하고 실행하는코드가 적혀있다.
따라서 PLT에 어떤 라이브러리 함수가 등록되어 있다면, 그 함수의 PLT 엔트리를 실행함으로써 함수를 실행할 수 이싿. ASLR이 걸려 있어도 PIE가 적용되어 있지 않다면 PLT의 주소는 고정되므로, 무작위의 주소에 매핑되는 라이브러리의 베이스 주소를 몰라도 이 방법으로 라이브러리 함수를 실행할 수 있다. 이 공격기법을 Return to PLT
라고 부른다.
라이브러리의 베이스 주소를 구하여 ASLR을 우회하는 기법은 다음 코스에서 다루고, 이 코스에서는 PLT를 이용하여 NX를 우회하도록 한다.
ELF의 PLT에는 ELF가 실행되는 라이브러리 함수만 포함된다. 따라서 다음 코드를 작성하면 PLT에 system
함수를 추가할 수 있다.
rtl.c의 Leak canary부터 Overwrite return address주석까지가 두번의 오버플로우를 통해 스택카나리를 우회하고, 반환 주소를 덮을 수 있도록 작성된 코드이다.
RTL 실습 코드
//rtl.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> const char* binsh = "/bin/sh"; int main() { char buf[0x30]; setvbuf(stdin, 0, _IONBF, 0); setvbuf(stdout, 0, _IONBF, 0); // Add system function to plt's entry system("echo 'system@plt'"); //9번째 줄 // Leak canary printf("[1] Leak Canary\n"); printf("Buf: "); read(0, buf, 0x100); printf("Buf: %s\n", buf); // Overwrite return address printf("[2] Overwrite return address\n"); printf("Buf: "); read(0, buf, 0x100); return 0; }
카나리 우회
첫번째 입력에서 적절한 길이의 데이터를 입력하면 카나리를 구할수 있다.
rdi값을 "/bin/sh"의 주소로 설정 및 셸 획득
카나리를 구했으면 이제 두 번째 입력으로 반환 주소를 덮을 수 있다. 그러나NX로 인해 지난코스에서와 같이 buf에 셸코드를 주입하고 이를 실행할 수는 없다.
알고있는정보
sytstem
함수의 PLT 주소를 안다. ==> system
함수를 호출할 수 있다.RAO에서system("/bin/sh")
을 호출하면 셸을 획득할수 있음을 배웠다. x86-64의 호출 규약에 따르면 이는 rdi"/bin/sh"주소인 상태에서 system
함수를 호출한 것과 같다.
이 예제에서는 "/bin/sh"의 주소를 알고, system
함수를 호출할 수 있으므로 "/bin/sh"의 주소를 rdi값으로 설정한다면 system("/bin/sh")
을 실행할수 있다. 이를 위해선 리턴 가젯
을 활용해야한다.
리턴 가젯은 다음과 같이 ret로 끝나는 어셈블리 코드 조각을 의미한다.
0x0000000000400853 : pop rdi ; ret
이제까지의 코스에서는 어떤 함수의 주소 또는 셸 코드의 주소로 반환 주소를 덮어서 한 번에 셸을 획득했다. 그러나 NX로 인해 셸 코드를 실행할 수 없는 상황에서, 단 한 번의 함수 실행으로 셸을 획득하는 것은 일반적으로 불가능하다
리턴 가젯은 반환 주소를 덮는 공격의 유연성을 높여서 익스플로잇에 필요한 조건을 만족할 수 있도록 돕는다. 예를들어 이 예제에서 rdi의 값을 "/bin/sh"의 주소로 설정하고, system함수를 호출해야한다. 리턴 가젯을 사용하여 반환 주소와 이후의 버퍼를 다음과 같이 덮으면, pop rdi
로 rdi
를 "/bin/sh"의 주소로 설정하고, 이어지는 ret
로 system
함수를 호출 할 수있다.
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
대부분의 함수는 ret로 종료 되므로,함수들도 리턴 가젯으로 사용될 수 있다.
리턴 가젯을 찾는 방법은 다양하지만, 일반적으로 ROPgadget을 사용한다.
명령어sudo apt install python3-ropgadget
를 이용해 설치를 완료했고 ROPgadget -v
를 이용해 설치가 정상적으로 이뤄졌는지 확인 할수 있다.
다음 명령어로 필요한 가젯을 찾을 수 있다. --re
옵션을 사용하면 정규표현식으로 가젯을 필터링할 수 있다. 일반적으로 바이너리에 포함된 가젯의 수가 매우 많으므로 필터링하여 가젯을 찾는걸 추첫한다.