Return To Library, 즉 RTL 이라는 불리는 공격기법에 대해서 알아보자
RTL이란 말 그대로 "라이브러리로 돌아가라!" 라는 의미를 가지고 있다.
그림 1
그림 1을 보면 해당 코드안에 setreuid 함수가 있기 때문에 strcpy 함수의 취약점을 이용하여 쉽게 버퍼 오버 플로우 공격이 가능하다. (다른 방어기법이 적용이 안되있다는 가정)
따라서 리턴 주소에 원하는 쉘코드를 삽입하여 공격이 가능하다.
하지만 이는 DEP 라는 방어 기법으로 불가능해 졌다.
※ DEP란?
•
버퍼 오버플로우로 인한 메모리 영역의 침범이 발생하더라도, 해당 영역에서 실행을 방지함으로써 공격을 방어하는 기법이다.
•
위와 같은 상황에서 리턴주소에서 쉘코드가 실행되려 할때 Access Violation이 발생하면서 프로그램이 종료가 된다
※ NX-bit란?
•
가상 메모리의 최소 단위인 페이지 기존에 읽고 쓰는 권한 이외에 실행 권한에 대해서 체크하여 메모리 공격으로부터 시스템을 보호하는 기법
•
기본적으로으로 NX-bit는 공격자가 스택, 힙, 데이터 영역에서의 공격코드를 실행하지 못하게 한다
•
DEP는 윈도우 운영체제에서 사용되는 방어기법이고 리눅스에서는 동일한 방어기법인 NX-bit가 있다. DEP와 NX-bit는 같은 방어기법이라고 보면 된다.
1. RTL을 알기전에..
RTL은 공유라이브러리 함수의 주소를 이용해서 현재 코드에 사용되지 않는 함수를 실행 시킬 수 있다.
여기서 말하는 함수는 사용자가 만든 함수가 아닌 printf, scanf 등과 같은 라이브러리 함수를 말한다.
그림 2
예를 들어 다음과 같은 코드가 있다고 하자. 현재 코드상에 gets함수를 통하여 오버플로우를 일으킬 수 있지만 그림1처럼 setreuid 함수가 없어 권한을 변경할 수 있는 방법이 없다.
이런 경우에 현재 코드에 없는 setreuid와 system 라이브러리를 이용할 수 있는 방법이 바로 RTL이다.
그럼 이러한 공유라이브러리는 어떻게 실행이 되는 걸까?
이는 PLT 와 GOT를 통해 설명이 가능하다.
PLT(Procedure Linkage table)
사용자가 만든 함수는 PLT를 참조할 필요가 없지만 외부 라이브러리에서 가져다 쓸 경우 PLT를 참조한다
GOT(Global Offest table)
함수들의 주소를 담고 있는 테이블이다. 라이브러리에서 함수를 호출할 때 PLT가 GOT를 참조함
쉽게 말하자면 PLT는 어떠한 함수들이 있는지 나열되어 있는 테이블이고 GOT는 실제 그 함수의 주소가 담겨져 있는 테이블이다.
그림 3
위의 그림처럼 PLT에는 함수들이 들어있는데 만약 printf 함수가 코드내에서 호출이 되면 PLT에서 printf 함수를 찾고 이 printf 함수의 실제주소룰 GOT를 통해서 얻게된다.
이 라이브러리들은 오른쪽의 메모리구조의 스택과 힙 영역 사이에 들어있다.
사실 PLT와 GOT도 복잡한 과정을 거치지만 이는 추후에 따로 정리하겠다.
(여기서 GOT 테이블을 변조하여 got테이블에 우리가 원하는 주소를 덮어버리면 특정 함수가 실행되지 않고 우리가 원하는 주소로 실행이 되는데 이를 GOT overrite 기법이라고 한다.)
2. RTL 들어가기
이제 RTL을 이해하기 위한 준비가 끝났다.
Ret에 라이브러리 주소 삽입
gets 함수나 strcpy 함수 등 취약한 함수를 사용하여 오버플로우를 일으킨다.
sfp까지 값을 채운다음에 ret주소에 스택프레임의 호출 전으로 돌아가야 할 주소가 아닌 system 함수가 위치하는 주소를 넣어 system() 함수를 Call 한다.
여기서 잠깐 !!*
ret주소에 라이브러리 주소를 넣어서 특정 함수를 call하는 건 일반적인 함수의 Call과는 다른 차이가 있다.
1) 일반적인 함수 Call
그림 4
그림 4는 어떤 프로그램의 데이터 영역에 들어있는 코드이다. 빨간 박스를 보면 SUM 이라는 함수를 call을 하게 되는데
이때 Call 명령어는 두가지를 행하게 된다.
Call
1) push eip
2) jmp "SUM 주소"
우선 그림 4를 보면 main+30 주소에 SUM 함수를 call 하게 되는데 해당 명령어가 끝나면 그 다음 명령어인 add 를 실행시켜야 한다. 따라서 add 명령어가 실행되는 주소를 스택에 저장하게 되는 부분이 "push eip" 부분이다.
그런다음 해당 SUM 함수의 위치로 가서 SUM 함수 부분의 명령어를 실행하게 된다
그림 5
위 설명을 그림으로 표현하면 다음과 같다. 참고로 해당 코드는 메인함수에서 system 함수를 호출하고 system 함수의 동작이 끝난후 에필로그를 진행하는 부분이다. (사실 SUM 함수인데 그냥 system함수라고 봐주길..)
Ret에 main+30 코드 다음으로 실행되어야하는 add 명령어가 존재하는 0x08048317 주소가 들어가 있다.
3번 pop eip를 통하여 다음의 실행할 명령어 주소를 eip에 넣게되고 esp는 하나 올라간다.
2) 비정상적인 함수 호출
코드영역에 call 명령어를 통해 함수를 호출하면 위의 과정을 거치게 되지만 Ret주소에 바로 특정 함수의 주소를 넣어 호출을 해버리면 "push eip" 에 대한 부분 없이 바로 해당 함수로 jump를 하게 된다.
따라서 호출된 함수의 스택프레임에서는 sfp 위에 존재하는 ret 공간에 되돌아갈 주소가 저장이 안된다. 이 설명을 그림으로 표현하면 다음과 같다.
그림 6
Call system 함수 가 아닌 ret 로 인한 jump로 system 함수로 이동하였기 때문에 ret 부분에 push eip 값이 저장이 안되어서 현재 ????? 로 들어가있는걸 확인할 수 있다. 즉 쓰레기 값이 채워져 있다는 것이다.
이제부터 자세하게 Get함수에서 ret로인한 system함수의 호출과정을 설명하겠다.
현재상황 :
gets 함수의 진행이 완료되고 스택프레임이 정리되고 있다. 현재 에필로그가 진행중이다.주소 방향 : 스택의 위가 높은 주소이다. 아래는 낮은 주소.
1.
그림1. Gets 함수 에필로그
첫번째로 Gets함수가 수행되다가 이제 에필로그를 진행하는 상대이다.
(불투명한 부분은 에필로그가 진행되서 지워진 부분을 뜻함)
1)leave : leave가 진행이 되고 ebp는 이전 ebp를 가리켜 어딘가로 올라가게 되고 esp는 pop ebp를 통해 한칸 위로 올 라간다.
2)ret : 현재 ret에 정상적인 eip 값이 아닌 시스템 함수의 주소인 0x12345678로 jump를 하게 되고 esp는 한칸올라가게 된다.
2.
그림 2. system함수의 프롤로그 ---과정--- 에필로그
그림 1에서 정상적인 ret가 아닌 시스템함수의 주소가 들어있었기 때문에 eip는 시스템 함수의 주소를 가리켜 현재 시스템 함수가 호출된 상황이다.
함수 호출이 진행되었기 때문에 프롤로그가 진행된다.
원래 esp는 인자2를 가리키고 있었고 ebp는 저~~~위 어딘가를 가리키고 있었는데 push ebp, mov esp, ebp를 통해 3)과 같은 상태가 되었다.
또한 system 함수가 쭉 진행이되고 함수를 정리하는 에필로그 부분이 진행되는데 그게 바로 4) 이다.
4)는 현재 에필로그 중 leave가 끝난 상황이다. leave는 mov esp, ebp + pop ebp 이므로 leave가 끝난 시점에 esp는 인자2를 가리키고 있게 된다.
3.
그림 3. system 함수의 ret
바로 이전에 system 함수의 에필로그 중 leave까지 수행했다. 그림 3은 leave 다음에 ret를 수행한 결과이다.
ret은 pop eip라고 보면 되므로 인자2에 들어 있던 값을 eip에 넣고 한칸 위로 올라간다.
자. 여기서 먼가 이상하지 않은가?
정상적인 흐름에서는 RET명령어를 수행할 때, sfp 위에 있는 값을 빼서 eip에 넣어야 하는데 현재 인자2의 값을 eip에 넣고나서 system 함수의 에필로그가 끝난다.
이것은 바로 함수가 정상적인 call에 의한 호출인지, 아니면 ret에 주소로 인한 호출(정확히는 점프)에 따른 이유 때문이다.
따라서 gets 함수와 system 함수가 에필로그의 leave를 거쳐 ret 즉, pop eip를 하기 바로 직전!!!의 상황을 비교해보자.
4.
그림 4. gets 함수 vs system 함수
사실 두 함수는 따로 수행되는 것이 아니라 gets함수가 정리되고 그 스택에서 다시 system 함수의 스택프레임이 쌓이는 과정이지만, 두 스택 프레임이 정리되는 과정을 비교하기 위해 만들어봤다.
gets함수는 ret과정에서 Ret(0x12345678)의 값을 pop 하게되고, system함수는 ret과정에서 인자2를 pop 하게 된다.
두개를 합치면 실제 진행되는 모습을 확인 가능하다.
5.
그림 4의 두개의 스택을 합친 그림이다.
즉 사실은 하나의 스택에서 스택이 증가되고 감소되면서 진행이된다.
현재 과정 : gets함수의 ret시 -> system함수가 호출 -> system함수 종료시
이렇게 진행이 된다면 현재 esp는 인자2를 가리키게 된다. 이부분이 바로 ret가 진행되기 직전이기 때문에 여기 값을 다음에 실행할 명령어의 주소라고 생각한다.
3. 결론
따라서 만약 내가 RTL 공격을 이용하여 버퍼오버플로우를 진행하려면 다음과 같은 구조로 스택을 공격을 해야한다.
rtl 페이로드 작성
만약 라이브러리를 한번만 호출하고 끝나면 다음 함수를 호출할 필요가 없기 때문에 인자2의 위치에 그냥 아무 더미 값이나 들어가도 된다. 또한 인자값도 필요하지 않으면 넣어주지 않아도 된다.
추가적으로 RTL 체인을 설명하겠다.
4. RTL 체인
rtl로 풀수 있는 문제
##해당 문제는 ftz 19번 문제임.
다시 그림2의 코드를 봐보자. 해당 코드는 gets함수를 이용하여 버퍼오버플로우를 일으킬수 있지만, setreuid의 함수도 없고, 다음 레벨문제의 쉘을 얻을수 잇는 system함수도 없다.
따라서 setreuid함수를 이용하여 권한을 변경하고, 변경된 권한으로 쉘을 띄우는것이 목적이다.
1. setreuid("실제 사용자 ID","유효사용자 ID") --> 인자가 2개 필요 (다음 레벨의 ID값)
(이건 password파일을 보면 20번문제의 id값을 확인 가능하다.)
•
*setreuid 함수의 필요인자 : 3092, 3092
페이로드 : [ 버퍼 ] + [ sfp ] + [ setreuid함수 주소 ] + [ system함수 ] + [ "3092" ] + [ "3092" ]
여기서 잠깐 생각해야 할 것이 있다.
함수가 호출될 때 현재 esp+8 위치에 있는 인자값을 가져와서 해당 함수의 스택프레임이 형성된다.
따라서 위의 페이로드를 보면 setreuid함수는 호출될 때 esp+8에 있는 3092와 esp+12에 있는 3092를 인자로 가지고 실행이 된다.
setreuid함수가 종료되면서 system함수가 호출될때 esp+8위치에 3092값을 system함수의 인자로 넣게 된다.
이상하지 않는가? 목적대로라면 system함수의 인자는 /bin/bash가 저장되어 있는 문자열의 주소를 넣어줘야 하지 않는가?
따라서 저 페이로드는 잘못된 페이로드이다. system함수의 인자값에 /bin/bash 넣어줘야 하기 때문에 가젯이라는 것을 사용해야한다.
가젯이란?
출처 : https://shayete.tistory.com/entry/6-Return-Oriented-Programming?category=857069
가젯을 사용하여 페이로드를 구성해 보자.
1. Setreuid가 끝나고 ret을 하기 직전에 esp는 가젯을 가리키고 있을 것이다. (위에서 설명한 더미 값)
2. ret을 하면 가젯이 실행되는데 가젯은 함수의 인자를 정리하기 위한 명령어의 집합 이라고 보면 편하다.
3. 현재 인자가 2개 있기 때문에 pop pop ret 명령어가 위치하는 주소를 가젯에 넣는다.
4. setreuid의 에필로그의 마지막인 ret이 수행되면 esp는 현재 가젯, 즉 pop pop ret 명령어의 주소를 eip에 넣고 esp를 하나 증가시킨다.
1. 다음 실행할 명령어는 pop pop ret이므로 esp가 가리키고 있는 첫번째 인자를 pop한다.
2. 그다음 인자를 pop 한다
3. 마지막 ret을 해야하는데 2번까지 진행됬으면 esp는 system 함수의 주소를 가리키고 있을 것이다.
4.pop pop ret의 마지막인 ret에 의해서 system 함수가 호출되게 된다.
5. system 함수가 호출될때 그때 esp + 8에 /bin/bash 문자열의 주소가 들어있으므로 이를 인자로 가지고 들어간다.
6. 더이상 다른 함수를 호출 할 필요가 없으므로 더미값에는 아무값이나 넣어도 상관없다.
여기서 가젯은 반드시 ret으로 끝나야 하고 정리해야 할 인자의 개수에 따라서 pop의 개수를 정해주면 된다.
5. 진짜 결론
남들한텐 쉬운 부분일 수도 있지만 나한테는 왜 더미값에 다음 함수의 주소가 들어가야 하는지 이해가 잘 안됬다. 결론은 내가 스택구조를 빠삭하게 알고 있지 않았다는 뜻이다..
이번에 정리를 하면서 스택을 자세히 파헤쳐볼수 있어서 보람찼다.
이제 ROP를 파헤치러 가봅시다..