스택은 임시 데이터를 저장하는 공간이다.
메모리에서 계속 데이터를 가져오기보다, 스택에 저장해서 접근하는 방식이다.
빈번하게 사용하는 데이터를 레지스터에 넣는다.
함수 프롤로그와 에필로그에서 스택 포인터를 조정한 후, 스택 포인터를 사용해서 데이터를 push하고 pop한다.
Intel x86 Stack Frame은 아래로 커진다. (high address -> low address 방향으로)
즉, 메모리 상의 임의의 주소부터 더 낮은 주소 아래로 자란다.
따라서 x86에서 스택의 맨 위를 말한다면 스택이 메모리에서 차지하는 주소의 공간 중 가장 낮은 곳을 말한다. (가장 낮은 주소)
x86에서는 ESP(Extended Stack Pointer) 레지스터가 스택과 함께 작동한다.
ESP는 항상 스택의 상단 부분을 가리키고 있다.
따라서 항상 가장 low address를 가리키고 있는 것이다.
heap은 low address에서 high address로 자라서 스택과 힙은 서로 반대방향으로 자란다.
stack은 왜 high address에서 low address로, 즉 반대로 자랄까?
그 이유는 stack 바로 및에 kernel 영역이 존재하기 때문이다.
stack에 overflow가 발생한다면 kernel 영역을 침범할 수 있는데, stack은 low address 방향으로 자라므로 kernel 영역을 절대 침범할 수 없다.
0xDEADBEEF라는 새로운 데이터를 스택에 넣는다고 하자.
그럼 push 명령어를 사용하는데, push는 먼저 esp를 4 감소(ESP + 4h) 시킨다. (스택이 더 낮은 주소로 커지므로 esp를 4만큼 감소시키는 것이다.) 이후 esp가 가리키는 곳에 0xDEADBEEF 데이터를 저장한다.
push eax 는
sub esp, 4
mov [esp], eax
코드와 같은 의미이다.
0xDEADBEEF를 스택에 저장한 후의 모습이다.
pop 명령어는 스택의 상단에 있는 데이터 값을 없애고(가져오고) 스택 포인터를 증가(ESP - 4h)시킨다.
pop eax 는
mov eax, [esp]
add esp, 4
코드와 같다.
위 상황의 경우 0xDEADBEEF 값은 eax에 들어간다. 따로 다른 값으로 덮지 않는 한 0xDEADBEEF 값은 0x9080ABC8 주소에 계속 적혀있을 것이다.
C언어에서 사용되는 방식이며, 호출자에서(함수를 호출한 곳) 스택을 정리한다.
매개변수는 스택에 push하여 함수 호출 시 전달하고, 어떤 add() 함수의 caller가 main 함수라면 main함수에서 스택을 정리(ADD ESP, 8)한다.
stdcall 방식은 Win32 API에서 사용되며 cdecl과 반대로 Callee에서 스택을 정리한다.
C언어는 기본적으로 cdecl이므로 stdcall 방식으로 컴파일하고 싶으면 함수 앞에 _stdcall 키워드를 붙이면 된다.
스택에 매개변수를 push해서 함수 호출 시 전달하는 방식은 cdecl 과 동일하고, Callee 함수에서 스택을 정리하는 방식이 차이점이다.
cdecl방식에서는 main()함수에서 add() 함수 호출 후 ADD ESP, 8 명령으로 스택을 정리했다면, stdcall 방식에서는 add() 함수의 RETN 명령에 정리할 스택 크기를 포함하여 RETN 8(RETN + POP 8) 명령으로 스택을 정리한다.
슽택을 정리하는 코드가 없으므로 cdecl 방식과 비교했을 때 코드의 크기가 줄어든다는 장점이 있다.
fastcall 방식은 stdcall 방식과 같지만, 함수에 전달하는 매개변수 일부(2개까지)를 스택이 아닌 레지스터를 이용한다.
ECX, EDX 레지스터를 사용해서 함수에 매개변수를 전달한다.
int foobar(int a, int b, int c)
{
int xx = a + 2;
int yy = b + 3;
int zz = c + 4;
int sum = xx + yy + zz;
return xx * yy * zz + sum;
}
int main()
{
return foobar(77, 88, 99);
}
32bit 크기의 범용 레지스터 8개
휘발성 레지스터(temporary register): ecx, edx
비휘발성 레지스터: ebx, esi, edi, ebp
eax - return 값
esp - 스택 포인터
eip - nest instruction
함수 호출 규약에서는 스택을 사용해서 매개변수가 함수 안으로 전달되고, 지역변수가 스택에 할당된다.
foobar 함수의 매개변수와 지역변수는 foobar 함수가 호출된 스택에 저장된다.
스택 안의 데이터 집합: 프레임
foobar 함수의 return 문이 실행되기 전 foobar 함수의 스택 프레임이다.
초록색 영역은 호출 규약에 따라 스택에 넣어진 영역이고, 파란색 영역은 foobar 함수의 스택이다.
esp를 움직이면서 함수를 실행하므로 ebp(base pointer, frame pointer) 레지스터는 함수의 매개변수와 지역변수를 찾기 쉽게 하는 상대적인 지표로 사용된다.
지역변수가 ebp 하단에 있으면 매개변수는 스택 내 ebp 상단에 위치한다.
함수를 호출할 때 최초 6개의 정수나 포인터 인자의 경우 RDI, RSI, RDX, RCX, R8, R9에 순서대로 들어가고, 다음부터는 스택을 통해 전달하게 된다.
스택 정리는 함수를 호출한 쪽에서(Caller)에서 수행한다.
void Func1(int p1);
코드가 있고, Func1(3)을 호출한다고 할 때,
mov rcx, 3
call Func1
과정대로 호출하면 될 것 같지만 먼저 홈 공간이 필요하다. 홈 공간은 매개변수가 레지스터를 통해 함수에 전달되더라도 그 값을 저장하기 위한 스택 공간을 호출하는 쪽에서 제공하는 것이다.
따라서
sub rsp, 8 //홈 공간 확보
mov rcx, 3
call Func1
add rsp, 8 //스택 정리
saved RBP(SFP)를 기준으로 RBP+8은 return address이고 RBP+16부터 남은 인자들이 차례로 들어가게 된다.
실수 인자는 xmm0~xmm7까지 총 8개를 순서대로 사용하고 그 다음부터는 스택을 통해 전달된다.
리턴값은 rax, rdx를 사용한다.
x86 스택 프레임에는 없던 128바이트 크기의 red zone이 있는데, %rsp가 가리키는 공간은 예약되어 있고 signal이나 interrupt handler에 의해서 변경될 수 없다.
따라서 함수 호출 간에 보존될 필요없는 휘발성 데이터가 이 영역(red zone)을 사용할 수 있다.
특히, 말단 함수는 그들의 전체 스택 프레임에서 프롤로그와 에필로그에서 스택 포인터를 조정하기 보다 이 영역을 사용할 수 있다.
32bit CPU일 때는 함수 호출 규약이 여러 개(cdecl, stdcall, fastcall) 있지만, 64bit CPU에는 fastcall(함수의 인자가 레지스터로 전달되는 방식) 호출 규약 1개를 사용한다.
64bit 레지스터로 기존의 레지스터에서 확장된 레지스터를 사용하고(rdi, rsi, rdx, rcx, rax, rbx, rsp) 8개의 레지스터(r8~r15)가 추가되었다.
Linux에서는 인자 전달을 위한 레지스터를 rdi, rsi, rdx, rcx, r8, r9 레지스터 6개를 사용한다. (그 이상은 스택에 저장)
반환값으로는 정수 타입 rax, rdx를 사용하고 실수 타입 xmm0, xmm1을 사용한다.
Window에서는 인자 전달을 위해 rcx, rdx, r8, r9 레지스터 순서로 4개를 사용한다.
나머지는 스택에 전달한다.
실수 타입의 경우 xmm0~xmm3까지 4개가 차례로 사용되며 나머지는 또한 스택으로 전달한다.
Tcache (0) | 2020.06.10 |
---|---|
Master canary (0) | 2020.06.08 |
[Back to Basic] Linux exploitation & Mitigation #1 (0) | 2020.04.06 |
[Exploit Tech] Return to dl resolve (0) | 2020.02.21 |
[Heap Exploitation] Poison null byte(Shrink chunk) (0) | 2020.01.30 |