EIP는 main()함수의 시작점을, ESP는 스텍의 맨 꼭대기를 가리킨다.
ESP가 스텍의 맨 꼭대기를 가리키는 이유는 피로그램이 수행되면서 수많은 PUSH와 POP 명령을 할 것이기 때문이다.
PUSH명령이 ESP가 가리키는 아래 지점에다 데이터를 넣을 것인지는 시스템 구조에 따라 다르다.
마찬가지로 POP명령이 ESP가 가리키는 지점의 데이터를 가져갈 것인지 아니면 ESP가 가리키는 지점 위의 데이터를 가져갈 것인지 역시 다르게 동작한다.
ebp를 저장하는 이유는 이전에 수행하던 함수의 데이터를 보존하기 위해서이다.
(이것을 base pointer라고도 부른다.)
함수가 시작될 때에는 이렇게 stack pointer와 base pointer를 새로 지정하는데 이러한 과정을 함수 프롤로그 과정이라고 한다.
push %ebp를 수행하여 이전 함수의 base pointer를 저장하면 stack pointer는 4바이트 아래인 0xbfffe678를 가리키게 될 것이다.
mov %rsp, %rbp를 수행하여 rsp 값을 rbp에 복사하였다. 이렇게 함으로써 함수의 base pointer와 stack pointer가 같은 지점을 가리키게 된다.
sub $0x8,%esp는 esp에서 8을 빼는 명령이다.
따라서 esp는 8바이트 아래 지점을 가리키게 되고 스택에 8바이트의 공간이 생긴다. (이것을 스택이 8바이트 확장되었다고 말한다.)
이 명령이 수행되고 나면 esp에는 0x8fffe670이 들어가게 된다.
and $0xfffffff0,%esp는 esp와 11111111 11111111 11111111 11110000과 AND 연산을 한다. 이것은 esp의 주소 값의 맨 뒤 4bit를 0으로 만들기 위함이다. (별 의미 없는 명령)
mov $0x0, %eax에서 EAX레지스터에 0을 넣고~
sub %eax,%esp에서 ESP에 들어있는 값에서 eax 들어 있는 값만큼 뺀다.
이것은 역시 stack pointer를 eax만큼 확장시키는 것이다.(하지만 eax에는 0이 들어있으므로 의미없는 명령이다.)
sub $0x4, %esp에서 스택을 4바이트 확장한다.
그럼 ESP에 들어있는 값은 0xbfffe66c 가 된다.
지금까지의 명령을 수행한 모습은 아래와 같다.
현제 ESP는 총 12바이트 이동하였다.
push $0x3, push $0x2, push $0x1
이것은 function(1,2,3)을 수행하기 위해 인자값 1,2,3을 차례로 넣어준다.
(순서가 3,2,1 인 이유는 스택에서 꺼낼때 거꾸로 나오기 때문)
call 0x80482f4 명령은 0x80482f4에 있는 명령을 수행하라는 것이다.
보는 것과 같이 0x80482f4는 function함수가 자리잡은 곳이다.
call 명령은 함수를 호출할 때 사용되는 명령으로 함수 실행이 끝난 다음의 명령을 계속 수행할 수 있도록
이 후 명령이 있는 주소를 스택에 넣은 다음 EIP함수의 시작 지점의 주소를 넣는다.
"add $0x10,%esp"명령이 있는 주소이다.
따라서 함수 수행이 끝나고 나면 이제 어디에 있는 명령을 수행해야하는지 스택에서 POP하여 알 수 있게 되는 것이다.
이것이 buffer overflow에서 return address이다.
이제 EIP에는 function 함수가 있는 0x80482f8 주소값이 들어가게 된다.
이제 EIP는 function()함수가 시작되는 지점을 가리키고 있고 스택에는 main()함수에서 넣었던 값들이 차족차족 쌓여있다.
push %ebp
mov %esp, %ebp
function()함수에서도 마찬가지로 함수 프롤로그가 수행된다.
main()함수에서 사용하던 base pointer가 저장되고 stack pointer를 function()함수의 base pointer로 삼는다.
(오타 - 0xbfffe678입니다.)
sub $0x28,%esp 에서 스택을 40바이트 확장한다.
40바이트가 된 이유는
simple.c의 function()함수에서 지역변수로 buffer1[15], buffer2[10]을 선언했기 때문이다.
buffer1[15]는 총 15바이트가 필요하지만 스택은 4바이트 단위로 자라기 때문에 16바이트를 할당하고
버퍼2는 12바이트를 할당한다. 따라서 확장되어야할 스택의 크기는 28바이트이다.
(gcc 2.96 버전 이후의 스택은 16배수로 할당된다. 단 8바이트 이하의 버퍼는 1 word(4바이트)로 할당되지만 9바이트 이상의 버퍼는 4 word (16바이트) 단위로 할당된다. 또한 8바이트의 더미 값이 들어간다.)
()안의 이유로 buffer1[15]를 위해서 16바이트가, buffer2[10]을 위해서 16바이트가 할당된다. 그리고 추가로 8바이트의 더미가 들어가 총 40바이트의 스텍이 확장된다.
(8바이트의 더미는 쓸데없이 공간이 소모되는 것)
그리고 function함수의 인자는 function()함수의 basepointer와 return address위에 존재하게 된다.
이렇게 만들어진 버퍼에는 필요한 데이터를 쓸수 있게 된다.
보통 mov $0x41, [$esp -4] 와 같은 형식으로 ESP를 기준으로 스텍의 특정 지점에 데이터를 복사해 넣는 방식으로 동작한다.
(여기서는 데이터를 넣는 과정이 없으므로 스택이 만들어진 과정까지만 확인한다.)
지금까지의 스택을 살펴보면
이렇게 된다.
leave instruction을 수행했다.
leave instruction은 함수 프롤로그 작업을 되돌리는 역활을 한다.
함수 프롤로그는 push %ebp, mov %dsp, %ebp 였다.
이것을 되돌리는 작업은 mov %ebp, %esp, pop %ebp이다.
이 두가지 일을 leave instruction에서 한꺼번에 하는 것이다.
stack pointer를 이전의 base pointer로 잡아서 function()합수에서 확장했던 스택 공간을 없애버리고 PUSH해서 저장해 두었던 이전 함수, 즉 main()함수의 base pointer를 복원 시킨다.
POP을 했으므로 stack pointer는 1 word(4byte) 위로 올라갈 것이다.
이제 stack pointer는 return address가 있는 지점을 가리키고 있을 것이다.
ret instruction은 이전 함수로 return하라는 의미이다.
EIP 레지스터에 return address를 POP하여 집어 넣는 역활을 한다.
ret를 수행하고 나면 return address는 POP되어 EIP에 저장되고 stack pointer는 1 word( 4byte) 위로 올라간다.
add $0x10, %esp는 스택을 16바이트 줄인다.
따라서 stack pointer는 0x804830c의 명령을 수행하기 이전의 위치에 돌아가게 된다.
leave, ret 을 수행하게 되면 각 레지스터들의 값은 main()함수 프롤로그 작업을 되돌리고
main()함수 이전으로 돌아가게 된다. 이것은 아마 init_process()함수로 되돌아가게 될 것이다.
참고 : 해커 지망생이 알아야할 bof 기초 -달고나
'Security > Pwnable' 카테고리의 다른 글
Buffer Overflow 원리 - 쉘코드 (0) | 2021.08.06 |
---|---|
Buffer Overflow 원리 - Buffer overflow의 이해 (0) | 2021.08.06 |
Buffer Overflow 원리 - 프로그램 구동시의 segment (1) (0) | 2021.08.06 |
Buffer Overflow 원리 - 8086CPU 레지스터 구조 (0) | 2021.08.06 |
Buffer Overflow 원리 - 8086 메모리구조 (0) | 2021.08.06 |