-Return Address Overwrite 개념-
return address overwrite란 stack frame의 끝에 존재하는 return address 영역을 overwrite 함으로써, 함수가 끝날 때 원하는 주소, 원하는 함수의 코드로 분기를 변경하도록 하는 기법이다. 가장 기본적인 공격 방법이며, 이 공격을 기반으로 rtl, rop 등의 공격이 연계되므로 잘 이해하고 있어야 한다.
return address overwrite를 이해하려면 call 명령어와 ret 명령어에 대해서 이해하고 있어야 한다.
먼저 call 명령어는 call 명령어의 다음 주소를 복귀 주소로 stack에 저장한 뒤, 지정한 주소로 이동한다. 여기서 복귀 주소가 저장된 곳이 return address 영역이다.
함수로 이동한 후에는 push ebp; mov ebp, esp 명령어를 통해 이전 함수의 ebp 값(스택 프레임)을 저장하고 ebp를 esp 위치로 옮김으로써 해당 함수의 스택 프레임을 형성한다. 이를 함수 프롤로그라고 부른다.
-함수 프롤로그-
push ebp
mov ebp, esp
위 사진은 buffer overflow 취약점을 정리할 때 사용한 bof 바이너리인데, 이 바이너리를 gdb로 열어본 후 pd main(disas main) 명령어를 통해 메인 함수를 확인해보면 push ebp; mov ebp, esp 명령어가 존재하는 것을 볼 수 있다. (main 함수도 함수다.)
ret 명령어는 함수 에필로그에서 leave 명령어 다음으로 호출되는 명령어로 pop eip(rip), jmp eip 명령을 수행한다. 즉 esp가 가리키는 stack 영역에 저장된 값을 명령어 레지스터인 eip에 저장해준 뒤, eip 주소로 점프하는 것이다.
-함수 에필로그-
leave # mov esp, ebp; pop ebp
ret # pop eip; jmp eip
이 ret 명령어가 수행될 때에는 leave 명령어(mov esp, ebp; pop ebp;)로 인해 esp가 call 명령어를 수행했을 때(이 함수를 호출했을 때) 저장한 return address 영역을 가리킨 상태다. 즉 복귀 주소가 eip에 들어가고 eip로 jmp 함으로써 저장한 복귀 주소로 돌아가는 것이다.
-Return Address Overwrite 실습-
bof 취약점을 이용해, 함수 에필로그 과정에서 pop eip를 통해 eip에 들어가는 return address 영역에 접근한 후 이를 다른 함수의 주소로 변조하는 공격을 수행해 보겠다.
// ubuntu 18.04
// gcc -m32 -o ret ret.c -z execstack -fno-stack-protector -no-pie -z relro
#include <stdio.h>
void vuln()
{
char buf[10];
printf("input : ");
scanf("%s", buf);
printf("%s\n", buf);
}
void success()
{
printf("success!\n");
}
int main()
{
vuln();
return;
}
실습에 사용할 파일의 소스코드다. scanf 함수로 입력을 받지만 서식 지정자로 %s를 썼기 때문에 입력 값의 길이 제한이 없어져 bof 취약점이 발생한다.
vuln, success라는 두 함수가 존재하는데, vuln 함수는 scanf("%s", buf); 코드가 있는 취약점이 발생하는 함수, success는 success라는 문구를 출력해주는 함수로 사실상 main, vuln 함수 어디에서도 이 함수를 호출하지 않기 때문에, 정상적인 흐름으로는 호출할 수 없는 함수다.
실습의 목적은 vuln 함수에서의 bof 취약점을 통한 return address 영역 변조로 success 함수의 코드를 실행시키는 것이다.
파일을 실행해보면 입력한 값을 그대로 출력해주는 동작을 한다는 것을 알 수 있다. 입력 값의 길이를 buf의 크기인 10을 초과해서 넣어주면 Segmentation fault 오류가 발생하는 것을 볼 수 있다.
gdb를 통해 해당 파일을 열어본 후 disas main 명령어를 통해 main 함수를 살펴보면 vuln 함수를 호출(call)하는 어셈블리 코드를 볼 수 있다.
vuln 함수를 호출하는 부분에 bp를 걸고 실행한 후 si 명령어를 통해 vuln 함수 내부로 들어간다면 스택에 함수의 복귀 주소인 0x08048540이 저장돼 있는 것을 볼 수 있다. 이 영역이 return address(복귀 주소)다.
main 함수에서 return address 0x08048540 부분을 확인해보면 vuln을 call하는 명령어의 바로 다음 명령어의 주소인 것을 알 수 있다. 즉 return address는 함수를 호출하는 명령어의 바로 다음 명령어의 주소가 된다.
ret 명령어가 있는 vuln+78로 온다면 그전에 실행된 leave 명령어를 통해 해당 함수에서 사용한 스택 프레임이 정리되고 esp가 return address가 있는 0xffffce7c를 가리키는 것을 볼 수 있다. 이 상태에서 pop eip를 한다면 eip에 return address가 들어가고 jmp eip 명령어를 통해 return address로 이동할 것이다.
그렇다면 이제는 return address 영역의 스택 주소와 입력 값이 저장되는 스택 주소의 거리를 구한 뒤, 이를 이용해 return address를 success 함수의 주소로 변조할 것이다.
gdb-peda$ pd vuln
Dump of assembler code for function vuln:
0x080484a6 <+0>: push ebp
0x080484a7 <+1>: mov ebp,esp
0x080484a9 <+3>: push ebx
0x080484aa <+4>: sub esp,0x14
0x080484ad <+7>: call 0x80483e0 <__x86.get_pc_thunk.bx>
0x080484b2 <+12>: add ebx,0x1b4e
0x080484b8 <+18>: sub esp,0xc
0x080484bb <+21>: lea eax,[ebx-0x1a30]
0x080484c1 <+27>: push eax
0x080484c2 <+28>: call 0x8048340 <printf@plt>
0x080484c7 <+33>: add esp,0x10
0x080484ca <+36>: sub esp,0x8
0x080484cd <+39>: lea eax,[ebp-0x12]
0x080484d0 <+42>: push eax
0x080484d1 <+43>: lea eax,[ebx-0x1a27]
0x080484d7 <+49>: push eax
0x080484d8 <+50>: call 0x8048370 <__isoc99_scanf@plt>
0x080484dd <+55>: add esp,0x10
0x080484e0 <+58>: sub esp,0xc
0x080484e3 <+61>: lea eax,[ebp-0x12]
0x080484e6 <+64>: push eax
0x080484e7 <+65>: call 0x8048350 <puts@plt>
0x080484ec <+70>: add esp,0x10
0x080484ef <+73>: nop
0x080484f0 <+74>: mov ebx,DWORD PTR [ebp-0x4]
0x080484f3 <+77>: leave
0x080484f4 <+78>: ret
위는 vuln 함수의 어셈 코드들이다. return address의 스택 주소를 구하기 위해서는 vuln 함수의 함수 프롤로그가 시작되는 부분에 bp를 걸고 스택을 확인하면 되고, 입력 값이 저장되는 스택 주소는 입력을 받는 부분(vuln+50)에 bp를 걸고 실행한 뒤 스택을 확인한다면 알 수 있다.
return address 영역은 0xffffce7c, 입력 값이 저장되는 주소는 0xffffce66인 것을 알 수 있다.(x/2wx 명령어로 스택에서 8byte만 확인한 이유는 scanf 함수는 2개의 매개변수를 받기 때문이다.)
그렇다면 return address 영역과 입력 값이 저장되는 주소의 거리는 22byte인 것을 알 수 있다. 22byte의 더미 값 + success 함수의 주소를 32bit little endian으로 입력한다면 return address 영역이 success 함수의 주소로 변조돼 vuln 함수의 ret 명령어가 실행되는 순간 success 함수로 이동하는 것이다.
success 함수의 주소는 0x080484f5인 것을 알 수 있다.
'A'*22byte+ (32bit little endian으로 패킹된 success 함수의 주소)를 입력 값으로 주니 success 문자열이 출력된 것을 볼 수 있다.
-마무리-
지금은 단순히 성공 문자열을 띄우도록 return address를 바꿨지만, 이 기법을 이용한다면 환경 변수나 스택에 쉘 코드를 넣어두고 쉘 코드가 있는 주소로 return address를 변조하는 식으로 공격을 할 수도 있다. 즉 공격자가 원하는 코드를 실행시키도록 할 수 있다는 것이다.
쉘 코드를 이용한 return address overwrite 공격은 ftz 11번 write up 등을 통해 확인할 수 있다.
이 글에서는 복귀 주소 영역을 직관적으로 return address 영역이라 불렀는데, 다른 게시글에서는 ret라고 칭하므로 이 점 유의하면 좋을 것 같다.
'Old (2021.01 ~ 2021.12) > Pwnable' 카테고리의 다른 글
[Linux-x64] main 함수 호출, 종료 과정 (0) | 2021.07.17 |
---|---|
Buffer Overflow (0) | 2021.07.04 |
[Linux-x86] shellcode 제작 (0) | 2021.06.26 |
우리 집에 GDB 있는데 메모리 보고 갈래? (3) (0) | 2021.03.13 |
우리 집에 GDB 있는데 메모리 보고 갈래? (2) (0) | 2021.03.13 |