-
SFP OverflowSystem Hacking/Techniques 2017. 8. 30. 04:56
SFP Overflow란?
이번 글에서는 SFP(Stack Frame Pointer) Overflow, FPO(Frame Pointer Overwrite), 1 Byte Overflow라고도 불리는 공격 기법을 알아볼 것이다.
SFP Overflow는 단 한 바이트 만으로 IP(Instruction Pointer)의 흐름을 제어할 수 있는 공격 기법이다.
SFP의 마지막 한 바이트를 쉘 코드의 주소가 저장되어 있는 스택의 주소로 변조한다면
함수 에필로그에 인해 흐름을 제어할 수 있게 된다.
SFP Overflow를 이해하기 위해서는 당연히 SFP(Stack Frame Pointer)가 무엇인지 먼저 알아야 할 것이다.
SFP는 Saved Frame Pointer라고도 불리며, 이전 함수의 EBP 주소를 저장하고 있는 공간이다.
일단 SFP가 무엇인지 알기 전에 스택 프레임에 대해 자세히 알 필요가 있다.
일단 스택은 함수에서 사용하는 영역으로, 함수의 Return Address, 지역 변수 등이 저장되며 사용되는 공간인데,
스택 프레임이란 바로 함수가 호출될 때, 그 함수만의 스택 영역을 구분하기 위하여 생성되는 공간이다.
쉽게 말해 함수 각각의 영역을 표현하는 것이다.
스택 프레임은 함수의 영역을 표현하는 것이라 했는데, 이 영역을 표현하기 위해 함수 프롤로그(Prolog)와 함수 에필로그(Epilog)라는 것을 수행한다.
Function Prolog
push ebp
mov ebp, esp
함수 프롤로그란 함수가 호출되면 그 함수의 영역을 설정하기 위한 것으로, 위와 같은 두 개의 명령어를 사용한다.
ebp를 push 함으로써 이전 함수. 그러니까 callee 함수를 호출한 caller 함수의 ebp 주소를 스택에 저장해 놓는다.
그리고 esp를 ebp로 복사함으로써 callee 함수의 ebp를 설정해주는 것이다.
ebp는 함수의 기준점을 나타낸다. 이 ebp를 기준으로 함수의 Return Address나 argc, argv, 함수의 지역변수 등의 위치를 쉽게 알 수 있다.
이러한 과정으로 인해 함수의 스택 프레임을 형성한다. 그리고 이것을 함수 프롤로그라고 한다.
이제 함수 에필로그를 알아보자.
함수 에필로그는 callee 함수가 끝나서 다시 caller 함수로 돌아갈 때, 스택을 정리하는 과정이다.
다시 말해, 스택을 다시 callee 함수가 호출되기 이전으로 되돌리는 것을 말한다.
Function Epilog
leave
ret
함수 에필로그는 위와 같은 두 개의 명령어로 이루어져 있는데,
Internal of "leave"
mov esp, ebp
pop ebp
leave 명령어는 내부적으로 위와 같은 명령어를 수행한다.
ebp를 esp로 복사하여 함수의 지역 변수를 정리하고, sfp를 ebp에 pop 하여 caller의 ebp로 되돌려 놓는다.
Internal of "ret"
pop eip
jmp eip
그리고 ret 명령어는 내부적으로 위와 같은 명령어를 수행한다.
leave 명령으로 인해 esp가 ebp+4로 이동되며,
그때의 esp는 callee의 RET를 가리키고 있을 것이다.
그 상태에서 pop eip를 하면, eip에 RET가 저장될 것이고, jmp eip를 하면 RET로 점프할 것이다. 그러니깐 caller 함수의 call callee 명령어의 다음 명령어로 점프하게 될 것이다.
이렇게 함수 프롤로그와 에필로그를 알아보았다.
이쯤이면, SFP가 무엇인지 설명하지 않아도 이미 알고 있을 것이다.
SFP는 이전 함수의 ebp를 저장하고 있으며, SFP+4는 RET이 될 것이다.
이제, 이 공격 기법이 어떻게 수행될지 감이 오지 않는가?
SFP의 마지막 Byte를 Overwrite 하여 SFP를 쉘 코드의 주소가 들어있는 스택 주소 -4로 한다면?
caller 함수의 에필로그가 수행되면서 우리가 원하는 주소로 흐름을 조작할 수 있게 될 것이다.
이것이 SFP Overflow의 전부이다. SFP의 1 Byte만으로 간단하면서도 파괴적인 공격이 가능하다.
이제 실습을 통해 SFP Overflow 공격 기법에 대해 자세히 알아보겠다.
SFP Overflow 실습
실습에 사용할 취약한 프로그램의 소스 코드이다.
main() 함수에서 argv[1] 인자를 가지고 vuln() 함수를 호출하고,
vuln() 함수에서는 인자 s를 strncpy() 함수로 지역 변수인 buf에 복사한다.
이 때 Caller는 main() 함수이고, Callee는 vuln() 함수이다.
각종 메모리 보호 기법을 해제하는 옵션을 지정하여 컴파일 한 후, 실행해보자.
그리고 이제 gdb로 분석하며 SFP Overflow에 대해 자세히 알아보자.
main() 함수를 디스어셈블 해보면 아까 살펴본 함수 프롤로그가 보인다.
main() 함수의 프롤로그가 수행되고 난 후인 main+3 지점에 break를 걸자.
그리고, argv[1]의 주소인 ebp+12를 인자로 vuln() 함수를 호출한다.
ESP 레지스터와 EBP 레지스터의 상태 변화를 자세히 분석하며 vuln() 함수가 호출되는 과정을 살펴보자.
vuln() 함수의 인자를 스택에 push 하는 main+11에 break를 걸자.
이제 vuln() 함수를 디스어셈블 해보자.
vuln() 함수 역시 스택 프레임을 형성하기 위해 함수 프롤로그를 수행한다.
esp를 0x28(40) 만큼 빼, 지역 변수인 buf를 할당한다.
우리는 공격을 위해 함수 에필로그 바로 전의 스택 상태를 살펴봐야 하기 때문에 함수 에필로그가 수행되기 전인
vuln+35에 break를 건다.
이제 모든 준비는 끝났다. 'A' 40개를 인자로 하여 실행하고, 분석을 시작해보자.
main() 함수의 프롤로그가 수행되고 난 후의 지점인 Break Point 1이다.
이때, EBP를 살펴보면 main() 함수의 EBP는 0xffffc678이다.
이 값을 잘 기억해 두 자.
이제 vuln() 함수가 호출되는 과정을 살펴보기 위해 c(Continue) 명령어를 입력하자.
EIP를 보면, 다음 명령어는 vuln() 함수의 인자를 스택에 push 하는 명령어이다.
n(Next Instruction)을 입력해보자.
vuln() 함수의 인자인 argv[1]이 스택의 push된 모습이다.
push eax 명령으로 인해 esp가 -4 된 걸 볼 수 있다.
이제 s(Step Into) 명령어를 입력해, vuln() 함수 안으로 들어가 보자.
vuln() 함수 안으로 들어왔다. 그런데 esp가 또 -4 되었다.
이유는 call 명령의 내부 명령어가 push RET, jmp vuln이기 때문이다.
때문에, esp는 -4 되고 esp 안에는 call vuln의 다음 명령어인 add esp, 0x4의 주소가 push 되는 것이다.
이제 vuln() 함수를 디스어셈블 해보면 EIP의 화살표가 vuln+0을 가리키는 것을 볼 수 있고,
그것은 vuln() 함수의 프롤로그이다.
n 명령어를 입력하여 vuln() 함수의 프롤로그 과정을 살펴보자.
vuln() 함수의 프롤로그 과정인 push ebp로 인해 스택에 main() 함수의 ebp인 0xffffc678이 push 되었고,
esp는 -4 된 걸 볼 수 있다.
이때, 스택을 한 번 살펴보자. esp를 보면 vuln() 함수의 SFP와 RET가 스택에 저장되어 있다.
main() 함수를 디스어셈블하여 0x08048472를 찾아보면 call vuln 명령어의 다음 명령어 주소인 걸 확인할 수 있을 것이다.
그리고 0xffffc678은 main() 함수의 ebp이다.
이것들은 또한, 각각 vuln() 함수의 SFP와 RET가 된다.
n 명령어를 입력하여 함수 프롤로그를 마저 수행해보자.
mov ebp, esp 명령으로 인하여 ebp가 0xffffc66c가 되었고, 이는 vuln() 함수의 ebp가 되는 것이다.
n 명령어를 입력해 vuln() 함수의 지역 변수 할당을 살펴보자.
sub esp, 0x28 명령으로 인해 esp가 -40 되면서, vuln() 함수의 지역 변수인 buf가 할당되는 모습이다.
이쯤에서 스택을 그려 이해를 더 확실히 해보겠다.
c 명령어로 다음 Break Point까지 실행하여 vuln() 함수의 에필로그 바로 전으로 가보자.
이때, esp-8을 살펴보자. 저 값들은 바로 전에 호출된 puts(buf)의 스택 값들이 남아 있는 것이다.
puts() 함수의 RET와 인자일 것이다. 0xffffc640에 들어있는 값인 0xffffc644는 buf의 주소이다.
puts() 함수의 인자로 들어갔을 테니 말이다.
그리고 buf의 끝에 0xffffc66c가 보인다. 이것은 vuln() 함수의 ebp이다. 그런데 그 안의 값은 main() 함수의 ebp이어야 할 것인데,
마지막 1 Byte가 0x78이 아닌 0x00이다. 이유는 strncpy() 함수의 size 인자로 41 Byte를 설정했지만,
입력은 40 Byte를 했으니 null로 채워진 것이다.
만약 이 상태로 vuln() 함수의 에필로그가 진행되면 어떻게 되겠는가?
leave의 mov esp, ebp로 esp는 0xffffc66c가 되고, pop ebp로 ebp가 0xffffc600이 되며, 또 esp는 +4 하여 0xffffc670이 될 것이다.
그리고 ret 명령어가 실행되면, pop eip로 인해 eip가 0xffffc670의 값으로 변조될 것이다.
n 명령어로 우리의 예측이 맞나 확인해보자.
leave가 실행되니, 예측한 대로 ebp는 0xffffc600이 되었고, esp는 0xffffc670이 되었다.
이제 다음 명령인 ret가 실행되면 pop eip로 인하여 esp는 0xffffc674가 될 것이고, eip는 0x08048472가 될 것이다.
n 명령어를 입력해 예측을 또 한번 확인해보자.
예측한 대로 실행되었다. 이제 공격을 어떻게 해야 할지 감이 오는가?
다시 실행하여 vuln() 함수의 에필로그가 실행되기 전으로 가서 스택을 살펴보고 공격 payload를 구성해보자.
ebp의 마지막 1 Byte를 0x3c로 Overwrite 하면 0xffffc66c의 값인 SFP는 0xffffc63c가 될 것이고,
그렇게 된다면 main의 RET는 ebp+4인 0xffffc640이 된다.
0xffffc640에는 0xffffc644가 저장되어 있고, 이는 vuln() 함수의 buf의 주소이다.
그렇다면 buf에 쉘 코드를 넣고 vuln() 함수의 SFP의 마지막 1 Byte를 3c로 변조한다면,
main() 함수가 Return 될 때 buf의 주소로 Return 된다는 소리이고, 이는 쉘 코드가 실행된다는 소리이다.
이제 payload를 구성해보자.
SHELLCODE(25) + NOP(15) + SFP(1)
구성한 payload에 맞춰 공격을 수행해보자.
쉘이 실행되었다.
마무리
이것으로 SFP Overflow 공격 기법에 대해서 알아보았다.
방금의 경우는 puts() 함수로 인해 EIP를 쉘 코드의 주소로 쉽게 조작할 수 있는 상황이 마련됐었지만,
만약 puts() 함수가 호출된 것과 같은 상황이 없는 경우에는 직접 Return 할 쉘 코드의 주소를 찾거나 입력하여,
SFP를 변조하고, 공격을 성공적으로 수행할 수 있을 것이다.
'System Hacking > Techniques' 카테고리의 다른 글
RTL Chaining (30) 2017.10.23 RTL (Return-to-libc) (11) 2017.10.07 SFP Overflow (2) 2017.08.30 Format String Bug (0) 2017.08.05 Buffer Overflow 기초 (10) 2017.07.27 [Linux/x86] Shellcode 기초 (4) 2017.05.04 댓글 2
정리가 너무 잘 되어있네요!
2019.01.18 13:58좋은 정보 감사합니다
너무 잘되있어요 .. 정말 감사합니다
2019.07.07 21:36